From 1120b4ab510f1ce11b3fb923f235eaa26f884937 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Jan 2014 16:29:16 +0100 Subject: [PATCH 001/468] Remove Elixir library Update SQLAlchemy --- couchpotato/core/settings/model.py | 454 +- couchpotato/runner.py | 2 +- libs/elixir/__init__.py | 114 - libs/elixir/collection.py | 125 - libs/elixir/entity.py | 1039 --- libs/elixir/events.py | 27 - libs/elixir/ext/__init__.py | 5 - libs/elixir/ext/associable.py | 234 - libs/elixir/ext/encrypted.py | 124 - libs/elixir/ext/perform_ddl.py | 106 - libs/elixir/ext/versioned.py | 288 - libs/elixir/fields.py | 191 - libs/elixir/options.py | 274 - libs/elixir/properties.py | 244 - libs/elixir/relationships.py | 1247 ---- libs/elixir/statements.py | 59 - libs/sqlalchemy/__init__.py | 41 +- libs/sqlalchemy/cextension/processors.c | 245 +- libs/sqlalchemy/cextension/resultproxy.c | 116 +- libs/sqlalchemy/cextension/utils.c | 225 + libs/sqlalchemy/connectors/__init__.py | 3 +- libs/sqlalchemy/connectors/mxodbc.py | 43 +- libs/sqlalchemy/connectors/mysqldb.py | 29 +- libs/sqlalchemy/connectors/pyodbc.py | 53 +- libs/sqlalchemy/connectors/zxJDBC.py | 5 +- libs/sqlalchemy/databases/__init__.py | 24 +- libs/sqlalchemy/dialects/__init__.py | 33 +- libs/sqlalchemy/dialects/access/__init__.py | 0 libs/sqlalchemy/dialects/access/base.py | 451 -- libs/sqlalchemy/dialects/drizzle/base.py | 27 +- libs/sqlalchemy/dialects/drizzle/mysqldb.py | 17 +- libs/sqlalchemy/dialects/firebird/__init__.py | 8 +- libs/sqlalchemy/dialects/firebird/base.py | 172 +- libs/sqlalchemy/dialects/firebird/fdb.py | 115 + .../dialects/firebird/kinterbasdb.py | 96 +- libs/sqlalchemy/dialects/informix/__init__.py | 9 - libs/sqlalchemy/dialects/informix/base.py | 596 -- .../dialects/informix/informixdb.py | 73 - libs/sqlalchemy/dialects/maxdb/__init__.py | 9 - libs/sqlalchemy/dialects/maxdb/base.py | 1117 ---- libs/sqlalchemy/dialects/maxdb/sapdb.py | 23 - libs/sqlalchemy/dialects/mssql/__init__.py | 4 +- libs/sqlalchemy/dialects/mssql/adodbapi.py | 24 +- libs/sqlalchemy/dialects/mssql/base.py | 689 +- .../dialects/mssql/information_schema.py | 30 +- libs/sqlalchemy/dialects/mssql/mxodbc.py | 82 +- libs/sqlalchemy/dialects/mssql/pymssql.py | 44 +- libs/sqlalchemy/dialects/mssql/pyodbc.py | 48 +- libs/sqlalchemy/dialects/mssql/zxjdbc.py | 36 +- libs/sqlalchemy/dialects/mysql/__init__.py | 8 +- libs/sqlalchemy/dialects/mysql/base.py | 738 ++- libs/sqlalchemy/dialects/mysql/cymysql.py | 69 + libs/sqlalchemy/dialects/mysql/gaerdbms.py | 58 +- .../dialects/mysql/mysqlconnector.py | 47 +- libs/sqlalchemy/dialects/mysql/mysqldb.py | 26 +- libs/sqlalchemy/dialects/mysql/oursql.py | 127 +- libs/sqlalchemy/dialects/mysql/pymysql.py | 33 +- libs/sqlalchemy/dialects/mysql/pyodbc.py | 26 +- libs/sqlalchemy/dialects/mysql/zxjdbc.py | 32 +- libs/sqlalchemy/dialects/oracle/__init__.py | 4 +- libs/sqlalchemy/dialects/oracle/base.py | 479 +- libs/sqlalchemy/dialects/oracle/cx_oracle.py | 302 +- libs/sqlalchemy/dialects/oracle/zxjdbc.py | 38 +- libs/sqlalchemy/dialects/postgres.py | 2 +- .../dialects/postgresql/__init__.py | 27 +- libs/sqlalchemy/dialects/postgresql/base.py | 1319 ++-- .../dialects/postgresql/constraints.py | 73 + libs/sqlalchemy/dialects/postgresql/hstore.py | 369 ++ libs/sqlalchemy/dialects/postgresql/json.py | 199 + libs/sqlalchemy/dialects/postgresql/pg8000.py | 43 +- .../dialects/postgresql/psycopg2.py | 295 +- .../dialects/postgresql/pypostgresql.py | 31 +- libs/sqlalchemy/dialects/postgresql/ranges.py | 160 + libs/sqlalchemy/dialects/postgresql/zxjdbc.py | 21 +- libs/sqlalchemy/dialects/sqlite/__init__.py | 9 +- libs/sqlalchemy/dialects/sqlite/base.py | 365 +- libs/sqlalchemy/dialects/sqlite/pysqlite.py | 127 +- libs/sqlalchemy/dialects/sybase/__init__.py | 29 +- libs/sqlalchemy/dialects/sybase/base.py | 504 +- libs/sqlalchemy/dialects/sybase/mxodbc.py | 19 +- libs/sqlalchemy/dialects/sybase/pyodbc.py | 25 +- libs/sqlalchemy/dialects/sybase/pysybase.py | 22 +- libs/sqlalchemy/engine/__init__.py | 149 +- libs/sqlalchemy/engine/base.py | 2455 +------ libs/sqlalchemy/engine/ddl.py | 182 - libs/sqlalchemy/engine/default.py | 276 +- libs/sqlalchemy/engine/interfaces.py | 846 +++ libs/sqlalchemy/engine/reflection.py | 280 +- libs/sqlalchemy/engine/result.py | 1018 +++ libs/sqlalchemy/engine/strategies.py | 75 +- libs/sqlalchemy/engine/threadlocal.py | 34 +- libs/sqlalchemy/engine/url.py | 135 +- libs/sqlalchemy/engine/util.py | 72 + libs/sqlalchemy/event.py | 460 -- libs/sqlalchemy/event/__init__.py | 10 + libs/sqlalchemy/event/api.py | 107 + libs/sqlalchemy/event/attr.py | 376 ++ libs/sqlalchemy/event/base.py | 217 + libs/sqlalchemy/event/legacy.py | 156 + libs/sqlalchemy/event/registry.py | 236 + libs/sqlalchemy/events.py | 472 +- libs/sqlalchemy/exc.py | 113 +- libs/sqlalchemy/ext/__init__.py | 3 +- libs/sqlalchemy/ext/associationproxy.py | 203 +- libs/sqlalchemy/ext/automap.py | 840 +++ libs/sqlalchemy/ext/compiler.py | 148 +- .../__init__.py} | 1148 +--- libs/sqlalchemy/ext/declarative/api.py | 512 ++ libs/sqlalchemy/ext/declarative/base.py | 506 ++ .../sqlalchemy/ext/declarative/clsregistry.py | 305 + libs/sqlalchemy/ext/horizontal_shard.py | 42 +- libs/sqlalchemy/ext/hybrid.py | 251 +- libs/sqlalchemy/ext/instrumentation.py | 407 ++ libs/sqlalchemy/ext/mutable.py | 194 +- libs/sqlalchemy/ext/orderinglist.py | 14 +- libs/sqlalchemy/ext/serializer.py | 69 +- libs/sqlalchemy/ext/sqlsoup.py | 811 --- libs/sqlalchemy/inspection.py | 92 + libs/sqlalchemy/interfaces.py | 23 +- libs/sqlalchemy/log.py | 18 +- libs/sqlalchemy/orm/__init__.py | 1602 +---- libs/sqlalchemy/orm/attributes.py | 658 +- libs/sqlalchemy/orm/base.py | 453 ++ libs/sqlalchemy/orm/collections.py | 596 +- libs/sqlalchemy/orm/dependency.py | 92 +- libs/sqlalchemy/orm/deprecated_interfaces.py | 24 +- libs/sqlalchemy/orm/descriptor_props.py | 317 +- libs/sqlalchemy/orm/dynamic.py | 212 +- libs/sqlalchemy/orm/evaluator.py | 40 +- libs/sqlalchemy/orm/events.py | 752 ++- libs/sqlalchemy/orm/exc.py | 63 +- libs/sqlalchemy/orm/identity.py | 90 +- libs/sqlalchemy/orm/instrumentation.py | 507 +- libs/sqlalchemy/orm/interfaces.py | 672 +- libs/sqlalchemy/orm/loading.py | 610 ++ libs/sqlalchemy/orm/mapper.py | 1694 +++-- libs/sqlalchemy/orm/path_registry.py | 261 + libs/sqlalchemy/orm/persistence.py | 432 +- libs/sqlalchemy/orm/properties.py | 1554 +---- libs/sqlalchemy/orm/query.py | 2040 +++--- libs/sqlalchemy/orm/relationships.py | 2511 +++++++ libs/sqlalchemy/orm/scoping.py | 105 +- libs/sqlalchemy/orm/session.py | 1196 ++-- libs/sqlalchemy/orm/shard.py | 15 - libs/sqlalchemy/orm/state.py | 536 +- libs/sqlalchemy/orm/strategies.py | 935 +-- libs/sqlalchemy/orm/strategy_options.py | 924 +++ libs/sqlalchemy/orm/sync.py | 58 +- libs/sqlalchemy/orm/unitofwork.py | 163 +- libs/sqlalchemy/orm/util.py | 991 +-- libs/sqlalchemy/pool.py | 418 +- libs/sqlalchemy/processors.py | 24 +- libs/sqlalchemy/schema.py | 3242 +-------- libs/sqlalchemy/sql/__init__.py | 30 +- libs/sqlalchemy/sql/annotation.py | 182 + libs/sqlalchemy/sql/base.py | 348 + libs/sqlalchemy/sql/compiler.py | 1651 +++-- libs/sqlalchemy/sql/ddl.py | 864 +++ libs/sqlalchemy/sql/default_comparator.py | 278 + libs/sqlalchemy/sql/dml.py | 770 +++ libs/sqlalchemy/sql/elements.py | 2880 ++++++++ libs/sqlalchemy/sql/expression.py | 5821 +---------------- libs/sqlalchemy/sql/functions.py | 482 +- libs/sqlalchemy/sql/operators.py | 364 +- libs/sqlalchemy/sql/schema.py | 3150 +++++++++ libs/sqlalchemy/sql/selectable.py | 3001 +++++++++ libs/sqlalchemy/sql/sqltypes.py | 1628 +++++ libs/sqlalchemy/sql/type_api.py | 1053 +++ libs/sqlalchemy/sql/util.py | 587 +- libs/sqlalchemy/sql/visitors.py | 96 +- libs/sqlalchemy/types.py | 2467 +------ libs/sqlalchemy/util/__init__.py | 39 +- libs/sqlalchemy/util/_collections.py | 366 +- libs/sqlalchemy/util/compat.py | 338 +- libs/sqlalchemy/util/deprecations.py | 51 +- libs/sqlalchemy/util/langhelpers.py | 723 +- libs/sqlalchemy/util/queue.py | 43 +- libs/sqlalchemy/util/topological.py | 14 +- 178 files changed, 43610 insertions(+), 35304 deletions(-) delete mode 100644 libs/elixir/__init__.py delete mode 100644 libs/elixir/collection.py delete mode 100644 libs/elixir/entity.py delete mode 100644 libs/elixir/events.py delete mode 100644 libs/elixir/ext/__init__.py delete mode 100644 libs/elixir/ext/associable.py delete mode 100644 libs/elixir/ext/encrypted.py delete mode 100644 libs/elixir/ext/perform_ddl.py delete mode 100644 libs/elixir/ext/versioned.py delete mode 100644 libs/elixir/fields.py delete mode 100644 libs/elixir/options.py delete mode 100644 libs/elixir/properties.py delete mode 100644 libs/elixir/relationships.py delete mode 100644 libs/elixir/statements.py create mode 100644 libs/sqlalchemy/cextension/utils.c delete mode 100644 libs/sqlalchemy/dialects/access/__init__.py delete mode 100644 libs/sqlalchemy/dialects/access/base.py create mode 100644 libs/sqlalchemy/dialects/firebird/fdb.py delete mode 100644 libs/sqlalchemy/dialects/informix/__init__.py delete mode 100644 libs/sqlalchemy/dialects/informix/base.py delete mode 100644 libs/sqlalchemy/dialects/informix/informixdb.py delete mode 100644 libs/sqlalchemy/dialects/maxdb/__init__.py delete mode 100644 libs/sqlalchemy/dialects/maxdb/base.py delete mode 100644 libs/sqlalchemy/dialects/maxdb/sapdb.py create mode 100644 libs/sqlalchemy/dialects/mysql/cymysql.py create mode 100644 libs/sqlalchemy/dialects/postgresql/constraints.py create mode 100644 libs/sqlalchemy/dialects/postgresql/hstore.py create mode 100644 libs/sqlalchemy/dialects/postgresql/json.py create mode 100644 libs/sqlalchemy/dialects/postgresql/ranges.py delete mode 100644 libs/sqlalchemy/engine/ddl.py create mode 100644 libs/sqlalchemy/engine/interfaces.py create mode 100644 libs/sqlalchemy/engine/result.py create mode 100644 libs/sqlalchemy/engine/util.py delete mode 100644 libs/sqlalchemy/event.py create mode 100644 libs/sqlalchemy/event/__init__.py create mode 100644 libs/sqlalchemy/event/api.py create mode 100644 libs/sqlalchemy/event/attr.py create mode 100644 libs/sqlalchemy/event/base.py create mode 100644 libs/sqlalchemy/event/legacy.py create mode 100644 libs/sqlalchemy/event/registry.py create mode 100644 libs/sqlalchemy/ext/automap.py rename libs/sqlalchemy/ext/{declarative.py => declarative/__init__.py} (51%) mode change 100755 => 100644 create mode 100644 libs/sqlalchemy/ext/declarative/api.py create mode 100644 libs/sqlalchemy/ext/declarative/base.py create mode 100644 libs/sqlalchemy/ext/declarative/clsregistry.py create mode 100644 libs/sqlalchemy/ext/instrumentation.py delete mode 100644 libs/sqlalchemy/ext/sqlsoup.py create mode 100644 libs/sqlalchemy/inspection.py create mode 100644 libs/sqlalchemy/orm/base.py create mode 100644 libs/sqlalchemy/orm/loading.py create mode 100644 libs/sqlalchemy/orm/path_registry.py create mode 100644 libs/sqlalchemy/orm/relationships.py delete mode 100644 libs/sqlalchemy/orm/shard.py create mode 100644 libs/sqlalchemy/orm/strategy_options.py create mode 100644 libs/sqlalchemy/sql/annotation.py create mode 100644 libs/sqlalchemy/sql/base.py create mode 100644 libs/sqlalchemy/sql/ddl.py create mode 100644 libs/sqlalchemy/sql/default_comparator.py create mode 100644 libs/sqlalchemy/sql/dml.py create mode 100644 libs/sqlalchemy/sql/elements.py create mode 100644 libs/sqlalchemy/sql/schema.py create mode 100644 libs/sqlalchemy/sql/selectable.py create mode 100644 libs/sqlalchemy/sql/sqltypes.py create mode 100644 libs/sqlalchemy/sql/type_api.py diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index ef6e8a5c..09d46afa 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -1,23 +1,20 @@ +import uuid +import datetime + +from sqlalchemy import Column, ForeignKey, Table, Index +from sqlalchemy.ext.associationproxy import AssociationProxy +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship, object_mapper, ColumnProperty, class_mapper +from sqlalchemy.orm.exc import UnmappedInstanceError +from sqlalchemy.orm.query import Query +from sqlalchemy.ext.declarative import declarative_base from couchpotato.core.helpers.encoding import toUnicode -from elixir.entity import Entity -from elixir.fields import Field -from elixir.options import options_defaults, using_options -from elixir.relationships import ManyToMany, OneToMany, ManyToOne from sqlalchemy.ext.mutable import Mutable from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \ TypeDecorator import json import time -options_defaults["shortnames"] = True - -# We would like to be able to create this schema in a specific database at -# will, so we can test it easily. -# Make elixir not bind to any session to make this possible. -# -# http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata -__session__ = None - class SetEncoder(json.JSONEncoder): def default(self, obj): @@ -73,74 +70,227 @@ class MutableDict(Mutable, dict): MutableDict.associate_with(JsonType) +Base = declarative_base() + +COLUMN_BLACKLIST = ('_sa_polymorphic_on', ) + +def is_mapped_class(cls): + try: + class_mapper(cls) + return True + except: + return False + +def is_like_list(instance, relation): + """Returns ``True`` if and only if the relation of `instance` whose name is +`relation` is list-like. + +A relation may be like a list if, for example, it is a non-lazy one-to-many +relation, or it is a dynamically loaded one-to-many. + +""" + if relation in instance._sa_class_manager: + return instance._sa_class_manager[relation].property.uselist + related_value = getattr(type(instance), relation, None) + return isinstance(related_value, AssociationProxy) + +class TableHelper(): + def to_dict(self, deep = None, exclude = None, include = None, + exclude_relations = None, include_relations = None, + include_methods = None): + instance = self + + if (exclude is not None or exclude_relations is not None) and \ + (include is not None or include_relations is not None): + raise ValueError('Cannot specify both include and exclude.') + # create a list of names of columns, including hybrid properties + try: + columns = [p.key for p in object_mapper(instance).iterate_properties + if isinstance(p, ColumnProperty)] + except UnmappedInstanceError: + return instance + for parent in type(instance).mro(): + columns += [key for key, value in parent.__dict__.items() + if isinstance(value, hybrid_property)] + # filter the columns based on exclude and include values + if exclude is not None: + columns = (c for c in columns if c not in exclude) + elif include is not None: + columns = (c for c in columns if c in include) + # create a dictionary mapping column name to value + result = dict((col, getattr(instance, col)) for col in columns + if not (col.startswith('__') or col in COLUMN_BLACKLIST)) + # add any included methods + if include_methods is not None: + result.update(dict((method, getattr(instance, method)()) for method in include_methods if not '.' in method)) + # Check for objects in the dictionary that may not be serializable by + # default. Specifically, convert datetime and date objects to ISO 8601 + # format, and convert UUID objects to hexadecimal strings. + for key, value in result.items(): + # TODO We can get rid of this when issue #33 is resolved. + if isinstance(value, datetime.date): + result[key] = value.isoformat() + elif isinstance(value, uuid.UUID): + result[key] = str(value) + elif is_mapped_class(type(value)): + result[key] = value.to_dict() + # recursively call _to_dict on each of the `deep` relations + deep = deep or {} + for relation, rdeep in deep.items(): + # Get the related value so we can see if it is None, a list, a query + # (as specified by a dynamic relationship loader), or an actual + # instance of a model. + relatedvalue = getattr(instance, relation) + if relatedvalue is None: + result[relation] = None + continue + # Determine the included and excluded fields for the related model. + newexclude = None + newinclude = None + if exclude_relations is not None and relation in exclude_relations: + newexclude = exclude_relations[relation] + elif (include_relations is not None and + relation in include_relations): + newinclude = include_relations[relation] + # Determine the included methods for the related model. + newmethods = None + if include_methods is not None: + newmethods = [method.split('.', 1)[1] for method in include_methods + if method.split('.', 1)[0] == relation] + if is_like_list(instance, relation): + result[relation] = [inst.to_dict(rdeep, exclude = newexclude, + include = newinclude, + include_methods = newmethods) + for inst in relatedvalue] + continue + # If the related value is dynamically loaded, resolve the query to get + # the single instance. + if isinstance(relatedvalue, Query): + relatedvalue = relatedvalue.one() + result[relation] = relatedvalue.to_dict(rdeep, exclude = newexclude, + include = newinclude, + include_methods = newmethods) + + return result + + +movie_files = Table('movie_files__file_movie', Base.metadata, + Column('movie_id', Integer, ForeignKey('movie.id'), nullable = False), + Column('file_id', Integer, ForeignKey('file.id'), nullable = False), + Index('movie_files_idx', 'movie_id', 'file_id', unique = True) +) + +release_files = Table('release_files__file_release', Base.metadata, + Column('release_id', Integer, ForeignKey('release.id'), nullable = False), + Column('file_id', Integer, ForeignKey('file.id'), nullable = False), + Index('release_files_idx', 'release_id', 'file_id', unique = True) +) + +library_files = Table('library_files__file_library', Base.metadata, + Column('library_id', Integer, ForeignKey('library.id'), nullable = False), + Column('file_id', Integer, ForeignKey('file.id'), nullable = False), + Index('library_files_idx', 'library_id', 'file_id', unique = True) +) + +class Movie(Base, TableHelper): + __tablename__ = 'movie' + id = Column(Integer, primary_key = True) -class Movie(Entity): """Movie Resource a movie could have multiple releases The files belonging to the movie object are global for the whole movie such as trailers, nfo, thumbnails""" - last_edit = Field(Integer, default = lambda: int(time.time()), index = True) + last_edit = Column(Integer, default = lambda: int(time.time()), index = True) type = 'movie' # Compat tv branch - library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True) - status = ManyToOne('Status') - profile = ManyToOne('Profile') - category = ManyToOne('Category') - releases = OneToMany('Release', cascade = 'all, delete-orphan') - files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) + library_id = Column(Integer, ForeignKey('library.id'), index = True) + status_id = Column(Integer, ForeignKey('status.id'), index = True) + profile_id = Column(Integer, ForeignKey('profile.id'), index = True) + category_id = Column(Integer, ForeignKey('category.id'), index = True) + + library = relationship('Library') #cascade = 'delete, delete-orphan', single_parent = True) + status = relationship('Status') + profile = relationship('Profile') + category = relationship('Category') + releases = relationship('Release') #, cascade = 'all, delete-orphan') + files = relationship('File', secondary = movie_files) #, cascade = 'all, delete-orphan', single_parent = True) Media = Movie # Compat tv branch -class Library(Entity): +class Library(Base, TableHelper): + __tablename__ = 'library' + id = Column(Integer, primary_key = True) + """""" - year = Field(Integer) - identifier = Field(String(20), index = True) + year = Column(Integer) + identifier = Column(String(20), index = True) - plot = Field(UnicodeText) - tagline = Field(UnicodeText(255)) - info = Field(JsonType) + plot = Column(UnicodeText) + tagline = Column(UnicodeText(255)) + info = Column(JsonType) - status = ManyToOne('Status') - movies = OneToMany('Movie', cascade = 'all, delete-orphan') - titles = OneToMany('LibraryTitle', cascade = 'all, delete-orphan') - files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) + status_id = Column(Integer, ForeignKey('status.id'), index = True) + status = relationship('Status') + + movies = relationship('Movie') #, cascade = 'all, delete-orphan') + titles = relationship('LibraryTitle', order_by="desc(LibraryTitle.default)") #, cascade = 'all, delete-orphan') + files = relationship('File', secondary = library_files) #, cascade = 'all, delete-orphan', single_parent = True) -class LibraryTitle(Entity): - """""" - using_options(order_by = '-default') +class LibraryTitle(Base, TableHelper): + __tablename__ = 'librarytitle' + id = Column(Integer, primary_key = True) - title = Field(Unicode) - simple_title = Field(Unicode, index = True) - default = Field(Boolean, default = False, index = True) - - language = OneToMany('Language') - libraries = ManyToOne('Library') - - -class Language(Entity): """""" - identifier = Field(String(20), index = True) - label = Field(Unicode) + #using_options(order_by = '-default') - titles = ManyToOne('LibraryTitle') + title = Column(Unicode) + simple_title = Column(Unicode, index = True) + default = Column(Boolean, default = False, index = True) + + language = relationship('Language') + + libraries_id = Column(Integer, ForeignKey('library.id'), index = True) + libraries = relationship('Library') -class Release(Entity): +class Language(Base, TableHelper): + __tablename__ = 'language' + id = Column(Integer, primary_key = True) + + """""" + + identifier = Column(String(20), index = True) + label = Column(Unicode) + + titles_id = Column(Integer, ForeignKey('librarytitle.id'), index = True) + titles = relationship('LibraryTitle') + + +class Release(Base, TableHelper): + __tablename__ = 'release' + id = Column(Integer, primary_key = True) + """Logically groups all files that belong to a certain release, such as parts of a movie, subtitles.""" - last_edit = Field(Integer, default = lambda: int(time.time()), index = True) - identifier = Field(String(100), index = True) + last_edit = Column(Integer, default = lambda: int(time.time()), index = True) + identifier = Column(String(100), index = True) - movie = ManyToOne('Movie') - status = ManyToOne('Status') - quality = ManyToOne('Quality') - files = ManyToMany('File') - info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan') + movie_id = Column(Integer, ForeignKey('movie.id'), index = True) + movie = relationship('Movie') + + status_id = Column(Integer, ForeignKey('status.id'), index = True) + status = relationship('Status') + + quality_id = Column(Integer, ForeignKey('quality.id'), index = True) + quality = relationship('Quality') + + files = relationship('File', secondary = release_files) + info = relationship('ReleaseInfo') #, cascade = 'all, delete-orphan') def to_dict(self, deep = None, exclude = None): if not exclude: exclude = [] @@ -162,51 +312,66 @@ class Release(Entity): return orig_dict -class ReleaseInfo(Entity): +class ReleaseInfo(Base, TableHelper): + __tablename__ = 'releaseinfo' + id = Column(Integer, primary_key = True) + """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(50), index = True) - value = Field(Unicode(255), nullable = False) + identifier = Column(String(50), index = True) + value = Column(Unicode(255), nullable = False) - release = ManyToOne('Release') + release_id = Column(Integer, ForeignKey('release.id'), index = True) + release = relationship('Release') -class Status(Entity): +class Status(Base, TableHelper): + __tablename__ = 'status' + id = Column(Integer, primary_key = True) + """The status of a release, such as Downloaded, Deleted, Wanted etc""" - identifier = Field(String(20), unique = True) - label = Field(Unicode(20)) + identifier = Column(String(20), unique = True) + label = Column(Unicode(20)) - releases = OneToMany('Release') - movies = OneToMany('Movie') + releases = relationship('Release') + movies = relationship('Movie') -class Quality(Entity): +class Quality(Base, TableHelper): + __tablename__ = 'quality' + id = Column(Integer, primary_key = True) + """Quality name of a release, DVD, 720p, DVD-Rip etc""" - using_options(order_by = 'order') - identifier = Field(String(20), unique = True) - label = Field(Unicode(20)) - order = Field(Integer, default = 0, index = True) + #using_options(order_by = 'order') - size_min = Field(Integer) - size_max = Field(Integer) + identifier = Column(String(20), unique = True) + label = Column(Unicode(20)) + order = Column(Integer, default = 0, index = True) - releases = OneToMany('Release') - profile_types = OneToMany('ProfileType') + size_min = Column(Integer) + size_max = Column(Integer) + + releases = relationship('Release') + profile_types = relationship('ProfileType', order_by="asc(ProfileType.order)") -class Profile(Entity): +class Profile(Base, TableHelper): + __tablename__ = 'profile' + id = Column(Integer, primary_key = True) + """""" - using_options(order_by = 'order') - label = Field(Unicode(50)) - order = Field(Integer, default = 0, index = True) - core = Field(Boolean, default = False) - hide = Field(Boolean, default = False) + #using_options(order_by = 'order') - movie = OneToMany('Movie') - types = OneToMany('ProfileType', cascade = 'all, delete-orphan') + label = Column(Unicode(50)) + order = Column(Integer, default = 0, index = True) + core = Column(Boolean, default = False) + hide = Column(Boolean, default = False) + + movie = relationship('Movie') + types = relationship('ProfileType', order_by="asc(ProfileType.order)") #, cascade = 'all, delete-orphan') def to_dict(self, deep = None, exclude = None): if not exclude: exclude = [] @@ -219,100 +384,119 @@ class Profile(Entity): return orig_dict -class Category(Entity): +class Category(Base, TableHelper): + __tablename__ = 'category' + id = Column(Integer, primary_key = True) + """""" - using_options(order_by = 'order') - label = Field(Unicode(50)) - order = Field(Integer, default = 0, index = True) - required = Field(Unicode(255)) - preferred = Field(Unicode(255)) - ignored = Field(Unicode(255)) - destination = Field(Unicode(255)) + #using_options(order_by = 'order') - movie = OneToMany('Movie') + label = Column(Unicode(50)) + order = Column(Integer, default = 0, index = True) + required = Column(Unicode(255)) + preferred = Column(Unicode(255)) + ignored = Column(Unicode(255)) + destination = Column(Unicode(255)) + + movie = relationship('Movie') -class ProfileType(Entity): +class ProfileType(Base, TableHelper): + __tablename__ = 'profiletype' + id = Column(Integer, primary_key = True) + """""" - using_options(order_by = 'order') - order = Field(Integer, default = 0, index = True) - finish = Field(Boolean, default = True) - wait_for = Field(Integer, default = 0) + #using_options(order_by = 'order') - quality = ManyToOne('Quality') - profile = ManyToOne('Profile') + order = Column(Integer, default = 0, index = True) + finish = Column(Boolean, default = True) + wait_for = Column(Integer, default = 0) + + quality_id = Column(Integer, ForeignKey('quality.id'), index = True) + quality = relationship('Quality') + + profile_id = Column(Integer, ForeignKey('profile.id'), index = True) + profile = relationship('Profile') -class File(Entity): +class File(Base, TableHelper): + __tablename__ = 'file' + id = Column(Integer, primary_key = True) + """File that belongs to a release.""" - path = Field(Unicode(255), nullable = False, unique = True) - part = Field(Integer, default = 1) - available = Field(Boolean, default = True) + path = Column(Unicode(255), nullable = False, unique = True) + part = Column(Integer, default = 1) + available = Column(Boolean, default = True) - type = ManyToOne('FileType') - properties = OneToMany('FileProperty') + type_id = Column(Integer, ForeignKey('filetype.id'), index = True) + type = relationship('FileType') - history = OneToMany('RenameHistory') - movie = ManyToMany('Movie') - release = ManyToMany('Release') - library = ManyToMany('Library') + properties = relationship('FileProperty') + + movie = relationship('Movie', secondary = movie_files) + release = relationship('Release', secondary = release_files) + library = relationship('Library', secondary = library_files) -class FileType(Entity): +class FileType(Base, TableHelper): + __tablename__ = 'filetype' + id = Column(Integer, primary_key = True) + """Types could be trailer, subtitle, movie, partial movie etc.""" - identifier = Field(String(20), unique = True) - type = Field(Unicode(20)) - name = Field(Unicode(50), nullable = False) + identifier = Column(String(20), unique = True) + type = Column(Unicode(20)) + name = Column(Unicode(50), nullable = False) - files = OneToMany('File') + files = relationship('File') -class FileProperty(Entity): +class FileProperty(Base, TableHelper): + __tablename__ = 'fileproperty' + id = Column(Integer, primary_key = True) + """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(20), index = True) - value = Field(Unicode(255), nullable = False) + identifier = Column(String(20), index = True) + value = Column(Unicode(255), nullable = False) - file = ManyToOne('File') + file_id = Column(Integer, ForeignKey('file.id'), index = True) + file = relationship('File') -class RenameHistory(Entity): - """Remembers from where to where files have been moved.""" +class Notification(Base, TableHelper): + __tablename__ = 'notification' + id = Column(Integer, primary_key = True) - old = Field(Unicode(255)) - new = Field(Unicode(255)) + """""" - file = ManyToOne('File') + #using_options(order_by = 'added') + + added = Column(Integer, default = lambda: int(time.time()), index = True) + read = Column(Boolean, default = False, index = True) + message = Column(Unicode(255)) + data = Column(JsonType) -class Notification(Entity): - using_options(order_by = 'added') +class Properties(Base, TableHelper): + __tablename__ = 'properties' + id = Column(Integer, primary_key = True) - added = Field(Integer, default = lambda: int(time.time())) - read = Field(Boolean, default = False) - message = Field(Unicode(255)) - data = Field(JsonType) + """""" - -class Properties(Entity): - - identifier = Field(String(50), index = True) - value = Field(Unicode(255), nullable = False) + identifier = Column(String(50), index = True) + value = Column(Unicode(255), nullable = False) def setup(): """Setup the database and create the tables that don't exists yet""" - from elixir import setup_all, create_all from couchpotato.environment import Env engine = Env.getEngine() - - setup_all() - create_all(engine) + Base.metadata.create_all(engine) try: engine.execute("PRAGMA journal_mode = WAL") diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 5c175201..bd033983 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -188,7 +188,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db_exists = os.path.isfile(toUnicode(db_path)) # Load migrations - if db_exists: + if False and db_exists: from migrate.versioning.api import version_control, db_version, version, upgrade repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') diff --git a/libs/elixir/__init__.py b/libs/elixir/__init__.py deleted file mode 100644 index a242b538..00000000 --- a/libs/elixir/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -''' -Elixir package - -A declarative layer on top of the `SQLAlchemy library -`_. It is a fairly thin wrapper, which provides -the ability to create simple Python classes that map directly to relational -database tables (this pattern is often referred to as the Active Record design -pattern), providing many of the benefits of traditional databases -without losing the convenience of Python objects. - -Elixir is intended to replace the ActiveMapper SQLAlchemy extension, and the -TurboEntity project but does not intend to replace SQLAlchemy's core features, -and instead focuses on providing a simpler syntax for defining model objects -when you do not need the full expressiveness of SQLAlchemy's manual mapper -definitions. -''' - -try: - set -except NameError: - from sets import Set as set - -import sqlalchemy -from sqlalchemy.types import * - -from elixir.options import using_options, using_table_options, \ - using_mapper_options, options_defaults, \ - using_options_defaults -from elixir.entity import Entity, EntityBase, EntityMeta, EntityDescriptor, \ - setup_entities, cleanup_entities -from elixir.fields import has_field, Field -from elixir.relationships import belongs_to, has_one, has_many, \ - has_and_belongs_to_many, \ - ManyToOne, OneToOne, OneToMany, ManyToMany -from elixir.properties import has_property, GenericProperty, ColumnProperty, \ - Synonym -from elixir.statements import Statement -from elixir.collection import EntityCollection, GlobalEntityCollection - - -__version__ = '0.8.0dev' - -__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection', - 'entities', - 'Field', 'has_field', - 'has_property', 'GenericProperty', 'ColumnProperty', 'Synonym', - 'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many', - 'ManyToOne', 'OneToOne', 'OneToMany', 'ManyToMany', - 'using_options', 'using_table_options', 'using_mapper_options', - 'options_defaults', 'using_options_defaults', - 'metadata', 'session', - 'create_all', 'drop_all', - 'setup_all', 'cleanup_all', - 'setup_entities', 'cleanup_entities'] + \ - sqlalchemy.types.__all__ - -__doc_all__ = ['create_all', 'drop_all', - 'setup_all', 'cleanup_all', - 'metadata', 'session'] - -# default session -session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker()) - -# default metadata -metadata = sqlalchemy.MetaData() - -metadatas = set() - -# default entity collection -entities = GlobalEntityCollection() - - -def create_all(*args, **kwargs): - '''Create the necessary tables for all declared entities''' - for md in metadatas: - md.create_all(*args, **kwargs) - - -def drop_all(*args, **kwargs): - '''Drop tables for all declared entities''' - for md in metadatas: - md.drop_all(*args, **kwargs) - - -def setup_all(create_tables=False, *args, **kwargs): - '''Setup the table and mapper of all entities in the default entity - collection. - ''' - setup_entities(entities) - - # issue the "CREATE" SQL statements - if create_tables: - create_all(*args, **kwargs) - - -def cleanup_all(drop_tables=False, *args, **kwargs): - '''Clear all mappers, clear the session, and clear all metadatas. - Optionally drops the tables. - ''' - session.close() - - cleanup_entities(entities) - - sqlalchemy.orm.clear_mappers() - entities.clear() - - if drop_tables: - drop_all(*args, **kwargs) - - for md in metadatas: - md.clear() - metadatas.clear() - - diff --git a/libs/elixir/collection.py b/libs/elixir/collection.py deleted file mode 100644 index 78127e3e..00000000 --- a/libs/elixir/collection.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -Default entity collection implementation -''' -import sys -import re - -class BaseCollection(list): - def __init__(self, entities=None): - list.__init__(self) - if entities is not None: - self.extend(entities) - - def extend(self, entities): - for e in entities: - self.append(e) - - def clear(self): - del self[:] - - def resolve_absolute(self, key, full_path, entity=None, root=None): - if root is None: - root = entity._descriptor.resolve_root - if root: - full_path = '%s.%s' % (root, full_path) - module_path, classname = full_path.rsplit('.', 1) - module = sys.modules[module_path] - res = getattr(module, classname, None) - if res is None: - if entity is not None: - raise Exception("Couldn't resolve target '%s' <%s> in '%s'!" - % (key, full_path, entity.__name__)) - else: - raise Exception("Couldn't resolve target '%s' <%s>!" - % (key, full_path)) - return res - - def __getattr__(self, key): - return self.resolve(key) - -# default entity collection -class GlobalEntityCollection(BaseCollection): - def __init__(self, entities=None): - # _entities is a dict of entities keyed on their name. - self._entities = {} - super(GlobalEntityCollection, self).__init__(entities) - - def append(self, entity): - ''' - Add an entity to the collection. - ''' - super(EntityCollection, self).append(entity) - - existing_entities = self._entities.setdefault(entity.__name__, []) - existing_entities.append(entity) - - def resolve(self, key, entity=None): - ''' - Resolve a key to an Entity. The optional `entity` argument is the - "source" entity when resolving relationship targets. - ''' - # Do we have a fully qualified entity name? - if '.' in key: - return self.resolve_absolute(key, key, entity) - else: - # Otherwise we look in the entities of this collection - res = self._entities.get(key, None) - if res is None: - if entity: - raise Exception("Couldn't resolve target '%s' in '%s'" - % (key, entity.__name__)) - else: - raise Exception("This collection does not contain any " - "entity corresponding to the key '%s'!" - % key) - elif len(res) > 1: - raise Exception("'%s' resolves to several entities, you should" - " use the full path (including the full module" - " name) to that entity." % key) - else: - return res[0] - - def clear(self): - self._entities = {} - super(GlobalEntityCollection, self).clear() - -# backward compatible name -EntityCollection = GlobalEntityCollection - -_leading_dots = re.compile('^([.]*).*$') - -class RelativeEntityCollection(BaseCollection): - # the entity=None does not make any sense with a relative entity collection - def resolve(self, key, entity): - ''' - Resolve a key to an Entity. The optional `entity` argument is the - "source" entity when resolving relationship targets. - ''' - full_path = key - - if '.' not in key or key.startswith('.'): - # relative target - - # any leading dot is stripped and with each dot removed, - # the entity_module is stripped of one more chunk (starting with - # the last one). - num_dots = _leading_dots.match(full_path).end(1) - full_path = full_path[num_dots:] - chunks = entity.__module__.split('.') - chunkstokeep = len(chunks) - num_dots - if chunkstokeep < 0: - raise Exception("Couldn't resolve relative target " - "'%s' relative to '%s'" % (key, entity.__module__)) - entity_module = '.'.join(chunks[:chunkstokeep]) - - if entity_module and entity_module is not '__main__': - full_path = '%s.%s' % (entity_module, full_path) - - root = '' - else: - root = None - return self.resolve_absolute(key, full_path, entity, root=root) - - def __getattr__(self, key): - raise NotImplementedError - diff --git a/libs/elixir/entity.py b/libs/elixir/entity.py deleted file mode 100644 index 87f5154c..00000000 --- a/libs/elixir/entity.py +++ /dev/null @@ -1,1039 +0,0 @@ -''' -This module provides the ``Entity`` base class, as well as its metaclass -``EntityMeta``. -''' - -import sys -import types -import warnings - -from copy import deepcopy - -import sqlalchemy -from sqlalchemy import Table, Column, Integer, desc, ForeignKey, and_, \ - ForeignKeyConstraint -from sqlalchemy.orm import MapperExtension, mapper, object_session, \ - EXT_CONTINUE, polymorphic_union, ScopedSession, \ - ColumnProperty -from sqlalchemy.sql import ColumnCollection - -import elixir -from elixir.statements import process_mutators, MUTATORS -from elixir import options -from elixir.properties import Property - -DEBUG = False - -__doc_all__ = ['Entity', 'EntityMeta'] - - -def session_mapper_factory(scoped_session): - def session_mapper(cls, *args, **kwargs): - if kwargs.pop('save_on_init', True): - old_init = cls.__init__ - def __init__(self, *args, **kwargs): - old_init(self, *args, **kwargs) - scoped_session.add(self) - cls.__init__ = __init__ - cls.query = scoped_session.query_property() - return mapper(cls, *args, **kwargs) - return session_mapper - - -class EntityDescriptor(object): - ''' - EntityDescriptor describes fields and options needed for table creation. - ''' - - def __init__(self, entity): - self.entity = entity - self.parent = None - - bases = [] - for base in entity.__bases__: - if isinstance(base, EntityMeta): - if is_entity(base) and not is_abstract_entity(base): - if self.parent: - raise Exception( - '%s entity inherits from several entities, ' - 'and this is not supported.' - % self.entity.__name__) - else: - self.parent = base - bases.extend(base._descriptor.bases) - self.parent._descriptor.children.append(entity) - else: - bases.append(base) - self.bases = bases - if not is_entity(entity) or is_abstract_entity(entity): - return - - # entity.__module__ is not always reliable (eg in mod_python) - self.module = sys.modules.get(entity.__module__) - - self.builders = [] - - #XXX: use entity.__subclasses__ ? - self.children = [] - - # used for multi-table inheritance - self.join_condition = None - self.has_pk = False - self._pk_col_done = False - - # columns and constraints waiting for a table to exist - self._columns = ColumnCollection() - self.constraints = [] - - # properties (it is only useful for checking dupe properties at the - # moment, and when adding properties before the mapper is created, - # which shouldn't happen). - self.properties = {} - - # - self.relationships = [] - - # set default value for options - self.table_args = [] - - # base class(es) options_defaults - options_defaults = self.options_defaults() - - complete_defaults = options.options_defaults.copy() - complete_defaults.update({ - 'metadata': elixir.metadata, - 'session': elixir.session, - 'collection': elixir.entities - }) - - # set default value for other options - for key in options.valid_options: - value = options_defaults.get(key, complete_defaults[key]) - if isinstance(value, dict): - value = value.copy() - setattr(self, key, value) - - # override options with module-level defaults defined - for key in ('metadata', 'session', 'collection'): - attr = '__%s__' % key - if hasattr(self.module, attr): - setattr(self, key, getattr(self.module, attr)) - - def options_defaults(self): - base_defaults = {} - for base in self.bases: - base_defaults.update(base._descriptor.options_defaults()) - base_defaults.update(getattr(self.entity, 'options_defaults', {})) - return base_defaults - - def setup_options(self): - ''' - Setup any values that might depend on the "using_options" class - mutator. For example, the tablename or the metadata. - ''' - elixir.metadatas.add(self.metadata) - if self.collection is not None: - self.collection.append(self.entity) - - entity = self.entity - if self.parent: - if self.inheritance == 'single': - self.tablename = self.parent._descriptor.tablename - - if not self.tablename: - if self.shortnames: - self.tablename = entity.__name__.lower() - else: - modulename = entity.__module__.replace('.', '_') - tablename = "%s_%s" % (modulename, entity.__name__) - self.tablename = tablename.lower() - elif hasattr(self.tablename, '__call__'): - self.tablename = self.tablename(entity) - - if not self.identity: - if 'polymorphic_identity' in self.mapper_options: - self.identity = self.mapper_options['polymorphic_identity'] - else: - #TODO: include module name (We could have b.Account inherit - # from a.Account) - self.identity = entity.__name__.lower() - elif 'polymorphic_identity' in self.mapper_options: - raise Exception('You cannot use the "identity" option and the ' - 'polymorphic_identity mapper option at the same ' - 'time.') - elif hasattr(self.identity, '__call__'): - self.identity = self.identity(entity) - - if self.polymorphic: - if not isinstance(self.polymorphic, basestring): - self.polymorphic = options.DEFAULT_POLYMORPHIC_COL_NAME - - #--------------------- - # setup phase methods - - def setup_autoload_table(self): - self.setup_table(True) - - def create_pk_cols(self): - """ - Create primary_key columns. That is, call the 'create_pk_cols' - builders then add a primary key to the table if it hasn't already got - one and needs one. - - This method is "semi-recursive" in some cases: it calls the - create_keys method on ManyToOne relationships and those in turn call - create_pk_cols on their target. It shouldn't be possible to have an - infinite loop since a loop of primary_keys is not a valid situation. - """ - if self._pk_col_done: - return - - self.call_builders('create_pk_cols') - - if not self.autoload: - if self.parent: - if self.inheritance == 'multi': - # Add columns with foreign keys to the parent's primary - # key columns - parent_desc = self.parent._descriptor - tablename = parent_desc.table_fullname - join_clauses = [] - fk_columns = [] - for pk_col in parent_desc.primary_keys: - colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \ - {'entity': self.parent.__name__.lower(), - 'key': pk_col.key} - - # It seems like SA ForeignKey is not happy being given - # a real column object when said column is not yet - # attached to a table - pk_col_name = "%s.%s" % (tablename, pk_col.key) - col = Column(colname, pk_col.type, primary_key=True) - fk_columns.append(col) - self.add_column(col) - join_clauses.append(col == pk_col) - self.join_condition = and_(*join_clauses) - self.add_constraint( - ForeignKeyConstraint(fk_columns, - parent_desc.primary_keys, ondelete='CASCADE')) - elif self.inheritance == 'concrete': - # Copy primary key columns from the parent. - for col in self.parent._descriptor.columns: - if col.primary_key: - self.add_column(col.copy()) - elif not self.has_pk and self.auto_primarykey: - if isinstance(self.auto_primarykey, basestring): - colname = self.auto_primarykey - else: - colname = options.DEFAULT_AUTO_PRIMARYKEY_NAME - - self.add_column( - Column(colname, options.DEFAULT_AUTO_PRIMARYKEY_TYPE, - primary_key=True)) - self._pk_col_done = True - - def setup_relkeys(self): - self.call_builders('create_non_pk_cols') - - def before_table(self): - self.call_builders('before_table') - - def setup_table(self, only_autoloaded=False): - ''' - Create a SQLAlchemy table-object with all columns that have been - defined up to this point. - ''' - if self.entity.table is not None: - return - - if self.autoload != only_autoloaded: - return - - kwargs = self.table_options - if self.autoload: - args = self.table_args - kwargs['autoload'] = True - else: - if self.parent: - if self.inheritance == 'single': - # we know the parent is setup before the child - self.entity.table = self.parent.table - - # re-add the entity columns to the parent entity so that - # they are added to the parent's table (whether the - # parent's table is already setup or not). - for col in self._columns: - self.parent._descriptor.add_column(col) - for constraint in self.constraints: - self.parent._descriptor.add_constraint(constraint) - return - elif self.inheritance == 'concrete': - #TODO: we should also copy columns from the parent table - # if the parent is a base (abstract?) entity (whatever the - # inheritance type -> elif will need to be changed) - - # Copy all non-primary key columns from parent table - # (primary key columns have already been copied earlier). - for col in self.parent._descriptor.columns: - if not col.primary_key: - self.add_column(col.copy()) - - for con in self.parent._descriptor.constraints: - self.add_constraint( - ForeignKeyConstraint( - [e.parent.key for e in con.elements], - [e.target_fullname for e in con.elements], - name=con.name, #TODO: modify it - onupdate=con.onupdate, ondelete=con.ondelete, - use_alter=con.use_alter)) - - if self.polymorphic and \ - self.inheritance in ('single', 'multi') and \ - self.children and not self.parent: - self.add_column(Column(self.polymorphic, - options.POLYMORPHIC_COL_TYPE)) - - if self.version_id_col: - if not isinstance(self.version_id_col, basestring): - self.version_id_col = options.DEFAULT_VERSION_ID_COL_NAME - self.add_column(Column(self.version_id_col, Integer)) - - args = list(self.columns) + self.constraints + self.table_args - self.entity.table = Table(self.tablename, self.metadata, - *args, **kwargs) - if DEBUG: - print self.entity.table.repr2() - - def setup_reltables(self): - self.call_builders('create_tables') - - def after_table(self): - self.call_builders('after_table') - - def setup_events(self): - def make_proxy_method(methods): - def proxy_method(self, mapper, connection, instance): - for func in methods: - ret = func(instance) - # I couldn't commit myself to force people to - # systematicaly return EXT_CONTINUE in all their event - # methods. - # But not doing that diverge to how SQLAlchemy works. - # I should try to convince Mike to do EXT_CONTINUE by - # default, and stop processing as the special case. -# if ret != EXT_CONTINUE: - if ret is not None and ret != EXT_CONTINUE: - return ret - return EXT_CONTINUE - return proxy_method - - # create a list of callbacks for each event - methods = {} - - all_methods = getmembers(self.entity, - lambda a: isinstance(a, types.MethodType)) - - for name, method in all_methods: - for event in getattr(method, '_elixir_events', []): - event_methods = methods.setdefault(event, []) - event_methods.append(method) - - if not methods: - return - - # transform that list into methods themselves - for event in methods: - methods[event] = make_proxy_method(methods[event]) - - # create a custom mapper extension class, tailored to our entity - ext = type('EventMapperExtension', (MapperExtension,), methods)() - - # then, make sure that the entity's mapper has our mapper extension - self.add_mapper_extension(ext) - - def before_mapper(self): - self.call_builders('before_mapper') - - def _get_children(self): - children = self.children[:] - for child in self.children: - children.extend(child._descriptor._get_children()) - return children - - def translate_order_by(self, order_by): - if isinstance(order_by, basestring): - order_by = [order_by] - - order = [] - 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('-')) - if colname.startswith('-'): - col = desc(col) - order.append(col) - return order - - def setup_mapper(self): - ''' - Initializes and assign a mapper to the entity. - At this point the mapper will usually have no property as they are - added later. - ''' - if self.entity.mapper: - return - - # for now we don't support the "abstract" parent class in a concrete - # inheritance scenario as demonstrated in - # sqlalchemy/test/orm/inheritance/concrete.py - # this should be added along other - kwargs = {} - if self.order_by: - kwargs['order_by'] = self.translate_order_by(self.order_by) - - if self.version_id_col: - kwargs['version_id_col'] = self.get_column(self.version_id_col) - - if self.inheritance in ('single', 'concrete', 'multi'): - if self.parent and \ - (self.inheritance != 'concrete' or self.polymorphic): - # non-polymorphic concrete doesn't need this - kwargs['inherits'] = self.parent.mapper - - if self.inheritance == 'multi' and self.parent: - kwargs['inherit_condition'] = self.join_condition - - if self.polymorphic: - if self.children: - if self.inheritance == 'concrete': - keys = [(self.identity, self.entity.table)] - keys.extend([(child._descriptor.identity, child.table) - for child in self._get_children()]) - # Having the same alias name for an entity and one of - # its child (which is a parent itself) shouldn't cause - # any problem because the join shouldn't be used at - # the same time. But in reality, some versions of SA - # do misbehave on this. Since it doesn't hurt to have - # different names anyway, here they go. - pjoin = polymorphic_union( - dict(keys), self.polymorphic, - 'pjoin_%s' % self.identity) - - kwargs['with_polymorphic'] = ('*', pjoin) - kwargs['polymorphic_on'] = \ - getattr(pjoin.c, self.polymorphic) - elif not self.parent: - kwargs['polymorphic_on'] = \ - self.get_column(self.polymorphic) - - if self.children or self.parent: - kwargs['polymorphic_identity'] = self.identity - - if self.parent and self.inheritance == 'concrete': - kwargs['concrete'] = True - - if self.parent and self.inheritance == 'single': - args = [] - else: - args = [self.entity.table] - - # let user-defined kwargs override Elixir-generated ones, though that's - # not very usefull since most of them expect Column instances. - kwargs.update(self.mapper_options) - - #TODO: document this! - if 'primary_key' in kwargs: - cols = self.entity.table.c - kwargs['primary_key'] = [getattr(cols, colname) for - colname in kwargs['primary_key']] - - # do the mapping - if self.session is None: - self.entity.mapper = mapper(self.entity, *args, **kwargs) - elif isinstance(self.session, ScopedSession): - session_mapper = session_mapper_factory(self.session) - self.entity.mapper = session_mapper(self.entity, *args, **kwargs) - else: - raise Exception("Failed to map entity '%s' with its table or " - "selectable. You can only bind an Entity to a " - "ScopedSession object or None for manual session " - "management." - % self.entity.__name__) - - def after_mapper(self): - self.call_builders('after_mapper') - - def setup_properties(self): - self.call_builders('create_properties') - - def finalize(self): - self.call_builders('finalize') - self.entity._setup_done = True - - #---------------- - # helper methods - - def call_builders(self, what): - for builder in self.builders: - if hasattr(builder, what): - getattr(builder, what)() - - def add_column(self, col, check_duplicate=None): - '''when check_duplicate is None, the value of the allowcoloverride - option of the entity is used. - ''' - if check_duplicate is None: - check_duplicate = not self.allowcoloverride - - if col.key in self._columns: - if check_duplicate: - raise Exception("Column '%s' already exist in '%s' ! " % - (col.key, self.entity.__name__)) - else: - del self._columns[col.key] - # are indexed on col.key - self._columns.add(col) - - if col.primary_key: - self.has_pk = True - - table = self.entity.table - if table is not None: - if check_duplicate and col.key in table.columns.keys(): - raise Exception("Column '%s' already exist in table '%s' ! " % - (col.key, table.name)) - table.append_column(col) - if DEBUG: - print "table.append_column(%s)" % col - - def add_constraint(self, constraint): - self.constraints.append(constraint) - - table = self.entity.table - if table is not None: - table.append_constraint(constraint) - - def add_property(self, name, property, check_duplicate=True): - if check_duplicate and name in self.properties: - raise Exception("property '%s' already exist in '%s' ! " % - (name, self.entity.__name__)) - self.properties[name] = property - -#FIXME: something like this is needed to propagate the relationships from -# parent entities to their children in a concrete inheritance scenario. But -# this doesn't work because of the backref matching code. In most case -# (test_concrete.py) it doesn't even happen at all. -# if self.children and self.inheritance == 'concrete': -# for child in self.children: -# child._descriptor.add_property(name, property) - - mapper = self.entity.mapper - if mapper: - mapper.add_property(name, property) - if DEBUG: - print "mapper.add_property('%s', %s)" % (name, repr(property)) - - def add_mapper_extension(self, extension): - extensions = self.mapper_options.get('extension', []) - if not isinstance(extensions, list): - extensions = [extensions] - extensions.append(extension) - self.mapper_options['extension'] = extensions - - def get_column(self, key, check_missing=True): - #TODO: this needs to work whether the table is already setup or not - #TODO: support SA table/autoloaded entity - try: - return self.columns[key] - except KeyError: - if check_missing: - raise Exception("No column named '%s' found in the table of " - "the '%s' entity!" - % (key, self.entity.__name__)) - - def get_inverse_relation(self, rel, check_reverse=True): - ''' - Return the inverse relation of rel, if any, None otherwise. - ''' - - matching_rel = None - for other_rel in self.relationships: - if rel.is_inverse(other_rel): - if matching_rel is None: - matching_rel = other_rel - else: - raise Exception( - "Several relations match as inverse of the '%s' " - "relation in entity '%s'. You should specify " - "inverse relations manually by using the inverse " - "keyword." - % (rel.name, rel.entity.__name__)) - # When a matching inverse is found, we check that it has only - # one relation matching as its own inverse. We don't need the result - # of the method though. But we do need to be careful not to start an - # infinite recursive loop. - if matching_rel and check_reverse: - rel.entity._descriptor.get_inverse_relation(matching_rel, False) - - return matching_rel - - def find_relationship(self, name): - for rel in self.relationships: - if rel.name == name: - return rel - if self.parent: - return self.parent._descriptor.find_relationship(name) - else: - return None - - #------------------------ - # some useful properties - - @property - def table_fullname(self): - ''' - Complete name of the table for the related entity. - Includes the schema name if there is one specified. - ''' - schema = self.table_options.get('schema', None) - if schema is not None: - return "%s.%s" % (schema, self.tablename) - else: - return self.tablename - - @property - def columns(self): - if self.entity.table is not None: - return self.entity.table.columns - else: - #FIXME: depending on the type of inheritance, we should also - # return the parent entity's columns (for example for order_by - # using a column defined in the parent. - return self._columns - - @property - def primary_keys(self): - """ - Returns the list of primary key columns of the entity. - - This property isn't valid before the "create_pk_cols" phase. - """ - if self.autoload: - return [col for col in self.entity.table.primary_key.columns] - else: - if self.parent and self.inheritance == 'single': - return self.parent._descriptor.primary_keys - else: - return [col for col in self.columns if col.primary_key] - - @property - def table(self): - if self.entity.table is not None: - return self.entity.table - else: - return FakeTable(self) - - @property - def primary_key_properties(self): - """ - Returns the list of (mapper) properties corresponding to the primary - key columns of the table of the entity. - - This property caches its value, so it shouldn't be called before the - entity is fully set up. - """ - if not hasattr(self, '_pk_props'): - col_to_prop = {} - mapper = self.entity.mapper - for prop in mapper.iterate_properties: - if isinstance(prop, ColumnProperty): - 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: - col_to_prop[col] = prop - 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] - return self._pk_props - -class FakePK(object): - def __init__(self, descriptor): - self.descriptor = descriptor - - @property - def columns(self): - return self.descriptor.primary_keys - -class FakeTable(object): - def __init__(self, descriptor): - self.descriptor = descriptor - self.primary_key = FakePK(descriptor) - - @property - def columns(self): - return self.descriptor.columns - - @property - def fullname(self): - ''' - Complete name of the table for the related entity. - Includes the schema name if there is one specified. - ''' - schema = self.descriptor.table_options.get('schema', None) - if schema is not None: - return "%s.%s" % (schema, self.descriptor.tablename) - else: - return self.descriptor.tablename - - -def is_entity(cls): - """ - Scan the bases classes of `cls` to see if any is an instance of - EntityMeta. If we don't find any, it means it is either an unrelated class - or an entity base class (like the 'Entity' class). - """ - for base in cls.__bases__: - if isinstance(base, EntityMeta): - return True - return False - - -# Note that we don't use inspect.getmembers because of -# http://bugs.python.org/issue1785 -# See also http://elixir.ematia.de/trac/changeset/262 -def getmembers(object, predicate=None): - base_props = [] - for key in dir(object): - try: - value = getattr(object, key) - except AttributeError: - continue - if not predicate or predicate(value): - base_props.append((key, value)) - return base_props - -def is_abstract_entity(dict_or_cls): - if not isinstance(dict_or_cls, dict): - dict_or_cls = dict_or_cls.__dict__ - for mutator, args, kwargs in dict_or_cls.get(MUTATORS, []): - if 'abstract' in kwargs: - return kwargs['abstract'] - - return False - -def instrument_class(cls): - """ - Instrument a class as an Entity. This is usually done automatically through - the EntityMeta metaclass. - """ - # Create the entity descriptor - desc = cls._descriptor = EntityDescriptor(cls) - - # Process mutators - # We *do* want mutators to be processed for base/abstract classes - # (so that statements like using_options_defaults work). - process_mutators(cls) - - # We do not want to do any more processing for base/abstract classes - # (Entity et al.). - if not is_entity(cls) or is_abstract_entity(cls): - return - - cls.table = None - cls.mapper = None - - # Copy the properties ('Property' instances) of the entity base class(es). - # We use getmembers (instead of __dict__) so that we also get the - # properties from the parents of the base class if any. - base_props = [] - for base in cls.__bases__: - if isinstance(base, EntityMeta) and \ - (not is_entity(base) or is_abstract_entity(base)): - base_props += [(name, deepcopy(attr)) for name, attr in - getmembers(base, lambda a: isinstance(a, Property))] - - # Process attributes (using the assignment syntax), looking for - # 'Property' instances and attaching them to this entity. - properties = [(name, attr) for name, attr in cls.__dict__.iteritems() - if isinstance(attr, Property)] - sorted_props = sorted(base_props + properties, - key=lambda i: i[1]._counter) - for name, prop in sorted_props: - prop.attach(cls, name) - - # setup misc options here (like tablename etc.) - desc.setup_options() - - -class EntityMeta(type): - """ - Entity meta class. - You should only use it directly if you want to define your own base class - for your entities (ie you don't want to use the provided 'Entity' class). - """ - - def __init__(cls, name, bases, dict_): - instrument_class(cls) - - def __setattr__(cls, key, value): - if isinstance(value, Property): - if hasattr(cls, '_setup_done'): - raise Exception('Cannot set attribute on a class after ' - 'setup_all') - else: - value.attach(cls, key) - else: - type.__setattr__(cls, key, value) - - -def setup_entities(entities): - '''Setup all entities in the list passed as argument''' - - for entity in entities: - # delete all Elixir properties so that it doesn't interfere with - # SQLAlchemy. At this point they should have be converted to - # builders. - for name, attr in entity.__dict__.items(): - if isinstance(attr, Property): - delattr(entity, name) - - for method_name in ( - 'setup_autoload_table', 'create_pk_cols', 'setup_relkeys', - 'before_table', 'setup_table', 'setup_reltables', 'after_table', - 'setup_events', - 'before_mapper', 'setup_mapper', 'after_mapper', - 'setup_properties', - 'finalize'): -# if DEBUG: -# print "=" * 40 -# print method_name -# print "=" * 40 - for entity in entities: -# print entity.__name__, "...", - if hasattr(entity, '_setup_done'): -# print "already done" - continue - method = getattr(entity._descriptor, method_name) - method() -# print "ok" - - -def cleanup_entities(entities): - """ - Try to revert back the list of entities passed as argument to the state - they had just before their setup phase. - - 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 - properties do not work yet as those would need to be regenerated (since the - columns they are based on are regenerated too -- and as such the - corresponding joins are not correct) but this doesn't happen because of - the way relationship setup is designed to be called only once (especially - the backref stuff in create_properties). - """ - for entity in entities: - desc = entity._descriptor - - if hasattr(entity, '_setup_done'): - del entity._setup_done - - entity.table = None - entity.mapper = None - - desc._pk_col_done = False - desc.has_pk = False - desc._columns = ColumnCollection() - desc.constraints = [] - desc.properties = {} - -class EntityBase(object): - """ - This class holds all methods of the "Entity" base class, but does not act - as a base class itself (it does not use the EntityMeta metaclass), but - rather as a parent class for Entity. This is meant so that people who want - to provide their own base class but don't want to loose or copy-paste all - the methods of Entity can do so by inheriting from EntityBase: - - .. sourcecode:: python - - class MyBase(EntityBase): - __metaclass__ = EntityMeta - - def myCustomMethod(self): - # do something great - """ - - def __init__(self, **kwargs): - self.set(**kwargs) - - def set(self, **kwargs): - for key, value in kwargs.iteritems(): - setattr(self, key, value) - - @classmethod - def update_or_create(cls, data, surrogate=True): - pk_props = cls._descriptor.primary_key_properties - - # if all pk are present and not None - if not [1 for p in pk_props if data.get(p.key) is None]: - pk_tuple = tuple([data[prop.key] for prop in pk_props]) - record = cls.query.get(pk_tuple) - if record is None: - if surrogate: - raise Exception("Cannot create surrogate with pk") - else: - record = cls() - else: - if surrogate: - record = cls() - else: - raise Exception("Cannot create non surrogate without pk") - record.from_dict(data) - return record - - def from_dict(self, data): - """ - Update a mapped class with data from a JSON-style nested dict/list - structure. - """ - # surrogate can be guessed from autoincrement/sequence but I guess - # that's not 100% reliable, so we'll need an override - - mapper = sqlalchemy.orm.object_mapper(self) - - for key, value in data.iteritems(): - if isinstance(value, dict): - dbvalue = getattr(self, key) - rel_class = mapper.get_property(key).mapper.class_ - pk_props = rel_class._descriptor.primary_key_properties - - # If the data doesn't contain any pk, and the relationship - # already has a value, update that record. - if not [1 for p in pk_props if p.key in data] and \ - dbvalue is not None: - dbvalue.from_dict(value) - else: - record = rel_class.update_or_create(value) - setattr(self, key, record) - elif isinstance(value, list) and \ - value and isinstance(value[0], dict): - - rel_class = mapper.get_property(key).mapper.class_ - new_attr_value = [] - for row in value: - if not isinstance(row, dict): - raise Exception( - 'Cannot send mixed (dict/non dict) data ' - 'to list relationships in from_dict data.') - record = rel_class.update_or_create(row) - new_attr_value.append(record) - setattr(self, key, new_attr_value) - else: - setattr(self, key, value) - - def to_dict(self, deep={}, exclude=[]): - """Generate a JSON-style nested dict/list structure from an object.""" - col_prop_names = [p.key for p in self.mapper.iterate_properties \ - if isinstance(p, ColumnProperty)] - data = dict([(name, getattr(self, name)) - for name in col_prop_names if name not in exclude]) - for rname, rdeep in deep.iteritems(): - dbdata = getattr(self, rname) - #FIXME: use attribute names (ie coltoprop) instead of column names - fks = self.mapper.get_property(rname).remote_side - exclude = [c.name for c in fks] - if dbdata is None: - data[rname] = None - elif isinstance(dbdata, list): - data[rname] = [o.to_dict(rdeep, exclude) for o in dbdata] - else: - data[rname] = dbdata.to_dict(rdeep, exclude) - return data - - # session methods - def flush(self, *args, **kwargs): - return object_session(self).flush([self], *args, **kwargs) - - def delete(self, *args, **kwargs): - return object_session(self).delete(self, *args, **kwargs) - - def expire(self, *args, **kwargs): - return object_session(self).expire(self, *args, **kwargs) - - def refresh(self, *args, **kwargs): - return object_session(self).refresh(self, *args, **kwargs) - - def expunge(self, *args, **kwargs): - return object_session(self).expunge(self, *args, **kwargs) - - # This bunch of session methods, along with all the query methods below - # only make sense when using a global/scoped/contextual session. - @property - def _global_session(self): - return self._descriptor.session.registry() - - #FIXME: remove all deprecated methods, possibly all of these - def merge(self, *args, **kwargs): - return self._global_session.merge(self, *args, **kwargs) - - def save(self, *args, **kwargs): - return self._global_session.save(self, *args, **kwargs) - - def update(self, *args, **kwargs): - return self._global_session.update(self, *args, **kwargs) - - # only exist in SA < 0.5 - # IMO, the replacement (session.add) doesn't sound good enough to be added - # here. For example: "o = Order(); o.add()" is not very telling. It's - # better to leave it as "session.add(o)" - def save_or_update(self, *args, **kwargs): - return self._global_session.save_or_update(self, *args, **kwargs) - - # query methods - @classmethod - def get_by(cls, *args, **kwargs): - """ - Returns the first instance of this class matching the given criteria. - This is equivalent to: - session.query(MyClass).filter_by(...).first() - """ - return cls.query.filter_by(*args, **kwargs).first() - - @classmethod - def get(cls, *args, **kwargs): - """ - Return the instance of this class based on the given identifier, - or None if not found. This is equivalent to: - session.query(MyClass).get(...) - """ - return cls.query.get(*args, **kwargs) - - -class Entity(EntityBase): - ''' - The base class for all entities - - All Elixir model objects should inherit from this class. Statements can - appear within the body of the definition of an entity to define its - fields, relationships, and other options. - - Here is an example: - - .. sourcecode:: python - - class Person(Entity): - name = Field(Unicode(128)) - birthdate = Field(DateTime, default=datetime.now) - - Please note, that if you don't specify any primary keys, Elixir will - automatically create one called ``id``. - - For further information, please refer to the provided examples or - tutorial. - ''' - __metaclass__ = EntityMeta - - diff --git a/libs/elixir/events.py b/libs/elixir/events.py deleted file mode 100644 index 293a8a4a..00000000 --- a/libs/elixir/events.py +++ /dev/null @@ -1,27 +0,0 @@ -from sqlalchemy.orm import reconstructor - -__all__ = [ - 'before_insert', - 'after_insert', - 'before_update', - 'after_update', - 'before_delete', - 'after_delete', - 'reconstructor' -] - -def create_decorator(event_name): - def decorator(func): - if not hasattr(func, '_elixir_events'): - func._elixir_events = [] - func._elixir_events.append(event_name) - return func - return decorator - -before_insert = create_decorator('before_insert') -after_insert = create_decorator('after_insert') -before_update = create_decorator('before_update') -after_update = create_decorator('after_update') -before_delete = create_decorator('before_delete') -after_delete = create_decorator('after_delete') - diff --git a/libs/elixir/ext/__init__.py b/libs/elixir/ext/__init__.py deleted file mode 100644 index c8708f25..00000000 --- a/libs/elixir/ext/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -''' -Ext package - -Additional Elixir statements and functionality. -''' diff --git a/libs/elixir/ext/associable.py b/libs/elixir/ext/associable.py deleted file mode 100644 index b31c5a74..00000000 --- a/libs/elixir/ext/associable.py +++ /dev/null @@ -1,234 +0,0 @@ -''' -Associable Elixir Statement Generator - -========== -Associable -========== - -About Polymorphic Associations ------------------------------- - -A frequent pattern in database schemas is the has_and_belongs_to_many, or a -many-to-many table. Quite often multiple tables will refer to a single one -creating quite a few many-to-many intermediate tables. - -Polymorphic associations lower the amount of many-to-many tables by setting up -a table that allows relations to any other table in the database, and relates -it to the associable table. In some implementations, this layout does not -enforce referential integrity with database foreign key constraints, this -implementation uses an additional many-to-many table with foreign key -constraints to avoid this problem. - -.. note: - SQLite does not support foreign key constraints, so referential integrity - can only be enforced using database backends with such support. - -Elixir Statement Generator for Polymorphic Associations -------------------------------------------------------- - -The ``associable`` function generates the intermediary tables for an Elixir -entity that should be associable with other Elixir entities and returns an -Elixir Statement for use with them. This automates the process of creating the -polymorphic association tables and ensuring their referential integrity. - -Matching select_XXX and select_by_XXX are also added to the associated entity -which allow queries to be run for the associated objects. - -Example usage: - -.. sourcecode:: python - - class Tag(Entity): - name = Field(Unicode) - - acts_as_taggable = associable(Tag) - - class Entry(Entity): - title = Field(Unicode) - acts_as_taggable('tags') - - class Article(Entity): - title = Field(Unicode) - acts_as_taggable('tags') - -Or if one of the entities being associated should only have a single member of -the associated table: - -.. sourcecode:: python - - class Address(Entity): - street = Field(String(130)) - city = Field(String(100)) - - is_addressable = associable(Address, 'addresses') - - class Person(Entity): - name = Field(Unicode) - orders = OneToMany('Order') - is_addressable() - - class Order(Entity): - order_num = Field(primary_key=True) - item_count = Field(Integer) - person = ManyToOne('Person') - is_addressable('address', uselist=False) - - home = Address(street='123 Elm St.', city='Spooksville') - user = Person(name='Jane Doe') - user.addresses.append(home) - - neworder = Order(item_count=4) - neworder.address = home - user.orders.append(neworder) - - # Queries using the added helpers - Person.select_by_addresses(city='Cupertino') - Person.select_addresses(and_(Address.c.street=='132 Elm St', - Address.c.city=='Smallville')) - -Statement Options ------------------ - -The generated Elixir Statement has several options available: - -+---------------+-------------------------------------------------------------+ -| Option Name | Description | -+===============+=============================================================+ -| ``name`` | Specify a custom name for the Entity attribute. This is | -| | used to declare the attribute used to access the associated | -| | table values. Otherwise, the name will use the plural_name | -| | provided to the associable call. | -+---------------+-------------------------------------------------------------+ -| ``uselist`` | Whether or not the associated table should be represented | -| | as a list, or a single property. It should be set to False | -| | when the entity should only have a single associated | -| | entity. Defaults to True. | -+---------------+-------------------------------------------------------------+ -| ``lazy`` | Determines eager loading of the associated entity objects. | -| | Defaults to False, to indicate that they should not be | -| | lazily loaded. | -+---------------+-------------------------------------------------------------+ -''' -from elixir.statements import Statement -import sqlalchemy as sa - -__doc_all__ = ['associable'] - - -def associable(assoc_entity, plural_name=None, lazy=True): - ''' - Generate an associable Elixir Statement - ''' - interface_name = assoc_entity._descriptor.tablename - able_name = interface_name + 'able' - - if plural_name: - attr_name = "%s_rel" % plural_name - else: - plural_name = interface_name - attr_name = "%s_rel" % interface_name - - class GenericAssoc(object): - - def __init__(self, tablename): - self.type = tablename - - #TODO: inherit from entity builder - class Associable(object): - """An associable Elixir Statement object""" - - def __init__(self, entity, name=None, uselist=True, lazy=True): - self.entity = entity - self.lazy = lazy - self.uselist = uselist - - if name is None: - self.name = plural_name - else: - self.name = name - - def after_table(self): - col = sa.Column('%s_assoc_id' % interface_name, sa.Integer, - sa.ForeignKey('%s.id' % able_name)) - self.entity._descriptor.add_column(col) - - if not hasattr(assoc_entity, '_assoc_table'): - metadata = assoc_entity._descriptor.metadata - association_table = sa.Table("%s" % able_name, metadata, - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('type', sa.String(40), nullable=False), - ) - tablename = "%s_to_%s" % (able_name, interface_name) - association_to_table = sa.Table(tablename, metadata, - sa.Column('assoc_id', sa.Integer, - sa.ForeignKey(association_table.c.id, - ondelete="CASCADE"), - primary_key=True), - #FIXME: this assumes a single id col - sa.Column('%s_id' % interface_name, sa.Integer, - sa.ForeignKey(assoc_entity.table.c.id, - ondelete="RESTRICT"), - primary_key=True), - ) - - assoc_entity._assoc_table = association_table - assoc_entity._assoc_to_table = association_to_table - - def after_mapper(self): - if not hasattr(assoc_entity, '_assoc_mapper'): - assoc_entity._assoc_mapper = sa.orm.mapper( - GenericAssoc, assoc_entity._assoc_table, properties={ - 'targets': sa.orm.relation( - assoc_entity, - secondary=assoc_entity._assoc_to_table, - lazy=lazy, backref='associations', - order_by=assoc_entity.mapper.order_by) - }) - - entity = self.entity - entity.mapper.add_property( - attr_name, - sa.orm.relation(GenericAssoc, lazy=self.lazy, - backref='_backref_%s' % entity.table.name) - ) - - if self.uselist: - def get(self): - if getattr(self, attr_name) is None: - setattr(self, attr_name, - GenericAssoc(entity.table.name)) - return getattr(self, attr_name).targets - setattr(entity, self.name, property(get)) - else: - # scalar based property decorator - def get(self): - attr = getattr(self, attr_name) - if attr is not None: - return attr.targets[0] - else: - return None - def set(self, value): - if getattr(self, attr_name) is None: - setattr(self, attr_name, - GenericAssoc(entity.table.name)) - getattr(self, attr_name).targets = [value] - setattr(entity, self.name, property(get, set)) - - # self.name is both set via mapper synonym and the python - # property, but that's how synonym properties work. - # adding synonym property after "real" property otherwise it - # breaks when using SQLAlchemy > 0.4.1 - entity.mapper.add_property(self.name, sa.orm.synonym(attr_name)) - - # add helper methods - def select_by(cls, **kwargs): - return cls.query.join(attr_name, 'targets') \ - .filter_by(**kwargs).all() - setattr(entity, 'select_by_%s' % self.name, classmethod(select_by)) - - def select(cls, *args, **kwargs): - return cls.query.join(attr_name, 'targets') \ - .filter(*args, **kwargs).all() - setattr(entity, 'select_%s' % self.name, classmethod(select)) - - return Statement(Associable) diff --git a/libs/elixir/ext/encrypted.py b/libs/elixir/ext/encrypted.py deleted file mode 100644 index 410855d2..00000000 --- a/libs/elixir/ext/encrypted.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -An encryption plugin for Elixir utilizing the excellent PyCrypto library, which -can be downloaded here: http://www.amk.ca/python/code/crypto - -Values for columns that are specified to be encrypted will be transparently -encrypted and safely encoded for storage in a unicode column using the powerful -and secure Blowfish Cipher using a specified "secret" which can be passed into -the plugin at class declaration time. - -Example usage: - -.. sourcecode:: python - - from elixir import * - from elixir.ext.encrypted import acts_as_encrypted - - class Person(Entity): - name = Field(Unicode) - password = Field(Unicode) - ssn = Field(Unicode) - acts_as_encrypted(for_fields=['password', 'ssn'], - with_secret='secret') - -The above Person entity will automatically encrypt and decrypt the password and -ssn columns on save, update, and load. Different secrets can be specified on -an entity by entity basis, for added security. - -**Important note**: instance attributes are encrypted in-place. This means that -if one of the encrypted attributes of an instance is accessed after the -instance has been flushed to the database (and thus encrypted), the value for -that attribute will be crypted in the in-memory object in addition to the -database row. -''' - -from Crypto.Cipher import Blowfish -from elixir.statements import Statement -from sqlalchemy.orm import MapperExtension, EXT_CONTINUE, EXT_STOP - -try: - from sqlalchemy.orm import EXT_PASS - SA05orlater = False -except ImportError: - SA05orlater = True - -__all__ = ['acts_as_encrypted'] -__doc_all__ = [] - - -# -# encryption and decryption functions -# - -def encrypt_value(value, secret): - return Blowfish.new(secret, Blowfish.MODE_CFB) \ - .encrypt(value).encode('string_escape') - -def decrypt_value(value, secret): - return Blowfish.new(secret, Blowfish.MODE_CFB) \ - .decrypt(value.decode('string_escape')) - - -# -# acts_as_encrypted statement -# - -class ActsAsEncrypted(object): - - def __init__(self, entity, for_fields=[], with_secret='abcdef'): - - def perform_encryption(instance, encrypt=True): - encrypted = getattr(instance, '_elixir_encrypted', None) - if encrypted is encrypt: - # skipping encryption or decryption, as it is already done - return - else: - # marking instance as already encrypted/decrypted - instance._elixir_encrypted = encrypt - - if encrypt: - func = encrypt_value - else: - func = decrypt_value - - for column_name in for_fields: - current_value = getattr(instance, column_name) - if current_value: - setattr(instance, column_name, - func(current_value, with_secret)) - - def perform_decryption(instance): - perform_encryption(instance, encrypt=False) - - class EncryptedMapperExtension(MapperExtension): - - def before_insert(self, mapper, connection, instance): - perform_encryption(instance) - return EXT_CONTINUE - - def before_update(self, mapper, connection, instance): - perform_encryption(instance) - return EXT_CONTINUE - - if SA05orlater: - def reconstruct_instance(self, mapper, instance): - perform_decryption(instance) - # no special return value is required for - # reconstruct_instance, but you never know... - return EXT_CONTINUE - else: - def populate_instance(self, mapper, selectcontext, row, - instance, *args, **kwargs): - mapper.populate_instance(selectcontext, instance, row, - *args, **kwargs) - perform_decryption(instance) - # EXT_STOP because we already did populate the instance and - # the normal processing should not happen - return EXT_STOP - - # make sure that the entity's mapper has our mapper extension - entity._descriptor.add_mapper_extension(EncryptedMapperExtension()) - - -acts_as_encrypted = Statement(ActsAsEncrypted) - diff --git a/libs/elixir/ext/perform_ddl.py b/libs/elixir/ext/perform_ddl.py deleted file mode 100644 index bb8528df..00000000 --- a/libs/elixir/ext/perform_ddl.py +++ /dev/null @@ -1,106 +0,0 @@ -''' -DDL statements for Elixir. - -Entities having the perform_ddl statement, will automatically execute the -given DDL statement, at the given moment: ether before or after the table -creation in SQL. - -The 'when' argument can be either 'before-create' or 'after-create'. -The 'statement' argument can be one of: - -- a single string statement -- a list of string statements, in which case, each of them will be executed - in turn. -- a callable which should take no argument and return either a single string - or a list of strings. - -In each string statement, you may use the special '%(fullname)s' construct, -that will be replaced with the real table name including schema, if unknown -to you. Also, self explained '%(table)s' and '%(schema)s' may be used here. - -You would use this extension to handle non elixir sql statemts, like triggers -etc. - -.. sourcecode:: python - - class Movie(Entity): - title = Field(Unicode(30), primary_key=True) - year = Field(Integer) - - perform_ddl('after-create', - "insert into %(fullname)s values ('Alien', 1979)") - -preload_data is a more specific statement meant to preload data in your -entity table from a list of tuples (of fields values for each row). - -.. sourcecode:: python - - class Movie(Entity): - title = Field(Unicode(30), primary_key=True) - year = Field(Integer) - - preload_data(('title', 'year'), - [(u'Alien', 1979), (u'Star Wars', 1977)]) - preload_data(('year', 'title'), - [(1982, u'Blade Runner')]) - preload_data(data=[(u'Batman', 1966)]) -''' - -from elixir.statements import Statement -from elixir.properties import EntityBuilder -from sqlalchemy import DDL - -__all__ = ['perform_ddl', 'preload_data'] -__doc_all__ = [] - -# -# the perform_ddl statement -# -class PerformDDLEntityBuilder(EntityBuilder): - - def __init__(self, entity, when, statement, on=None, context=None): - self.entity = entity - self.when = when - self.statement = statement - self.on = on - self.context = context - - def after_table(self): - statement = self.statement - if hasattr(statement, '__call__'): - statement = statement() - if not isinstance(statement, list): - statement = [statement] - for s in statement: - ddl = DDL(s, self.on, self.context) - ddl.execute_at(self.when, self.entity.table) - -perform_ddl = Statement(PerformDDLEntityBuilder) - -# -# the preload_data statement -# -class PreloadDataEntityBuilder(EntityBuilder): - - def __init__(self, entity, columns=None, data=None): - self.entity = entity - self.columns = columns - self.data = data - - def after_table(self): - all_columns = [col.name for col in self.entity.table.columns] - def onload(event, schema_item, connection): - columns = self.columns - if columns is None: - columns = all_columns - data = self.data - if hasattr(data, '__call__'): - data = data() - insert = schema_item.insert() - connection.execute(insert, - [dict(zip(columns, values)) for values in data]) - - self.entity.table.append_ddl_listener('after-create', onload) - -preload_data = Statement(PreloadDataEntityBuilder) - diff --git a/libs/elixir/ext/versioned.py b/libs/elixir/ext/versioned.py deleted file mode 100644 index 75f406b0..00000000 --- a/libs/elixir/ext/versioned.py +++ /dev/null @@ -1,288 +0,0 @@ -''' -A versioning plugin for Elixir. - -Entities that are marked as versioned with the `acts_as_versioned` statement -will automatically have a history table created and a timestamp and version -column added to their tables. In addition, versioned entities are provided -with four new methods: revert, revert_to, compare_with and get_as_of, and one -new attribute: versions. Entities with compound primary keys are supported. - -The `versions` attribute will contain a list of previous versions of the -instance, in increasing version number order. - -The `get_as_of` method will retrieve a previous version of the instance "as of" -a specified datetime. If the current version is the most recent, it will be -returned. - -The `revert` method will rollback the current instance to its previous version, -if possible. Once reverted, the current instance will be expired from the -session, and you will need to fetch it again to retrieve the now reverted -instance. - -The `revert_to` method will rollback the current instance to the specified -version number, if possibe. Once reverted, the current instance will be expired -from the session, and you will need to fetch it again to retrieve the now -reverted instance. - -The `compare_with` method will compare the instance with a previous version. A -dictionary will be returned with each field difference as an element in the -dictionary where the key is the field name and the value is a tuple of the -format (current_value, version_value). Version instances also have a -`compare_with` method so that two versions can be compared. - -Also included in the module is a `after_revert` decorator that can be used to -decorate methods on the versioned entity that will be called following that -instance being reverted. - -The acts_as_versioned statement also accepts an optional `ignore` argument -that consists of a list of strings, specifying names of fields. Changes in -those fields will not result in a version increment. In addition, you can -pass in an optional `check_concurrent` argument, which will use SQLAlchemy's -built-in optimistic concurrency mechanisms. - -Note that relationships that are stored in mapping tables will not be included -as part of the versioning process, and will need to be handled manually. Only -values within the entity's main table will be versioned into the history table. -''' - -from datetime import datetime -import inspect - -from sqlalchemy import Table, Column, and_, desc -from sqlalchemy.orm import mapper, MapperExtension, EXT_CONTINUE, \ - object_session - -from elixir import Integer, DateTime -from elixir.statements import Statement -from elixir.properties import EntityBuilder -from elixir.entity import getmembers - -__all__ = ['acts_as_versioned', 'after_revert'] -__doc_all__ = [] - -# -# utility functions -# - -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) - - -def get_history_where(instance): - clauses = [] - history_columns = instance.__history_table__.primary_key.columns - for column in instance.table.primary_key.columns: - instance_value = getattr(instance, column.name) - history_column = getattr(history_columns, column.name) - clauses.append(history_column==instance_value) - return and_(*clauses) - - -# -# a mapper extension to track versions on insert, update, and delete -# - -class VersionedMapperExtension(MapperExtension): - def before_insert(self, mapper, connection, instance): - version_colname, timestamp_colname = \ - instance.__class__.__versioned_column_names__ - setattr(instance, version_colname, 1) - setattr(instance, timestamp_colname, datetime.now()) - return EXT_CONTINUE - - def before_update(self, mapper, connection, instance): - old_values = instance.table.select(get_entity_where(instance)) \ - .execute().fetchone() - - # SA might've flagged this for an update even though it didn't change. - # This occurs when a relation is updated, thus marking this instance - # for a save/update operation. We check here against the last version - # to ensure we really should save this version and update the version - # data. - ignored = instance.__class__.__ignored_fields__ - version_colname, timestamp_colname = \ - instance.__class__.__versioned_column_names__ - for key in instance.table.c.keys(): - if key in ignored: - continue - if getattr(instance, key) != old_values[key]: - # the instance was really updated, so we create a new version - dict_values = dict(old_values.items()) - connection.execute( - instance.__class__.__history_table__.insert(), dict_values) - old_version = getattr(instance, version_colname) - setattr(instance, version_colname, old_version + 1) - setattr(instance, timestamp_colname, datetime.now()) - break - - return EXT_CONTINUE - - def before_delete(self, mapper, connection, instance): - connection.execute(instance.__history_table__.delete( - get_history_where(instance) - )) - return EXT_CONTINUE - - -versioned_mapper_extension = VersionedMapperExtension() - - -# -# the acts_as_versioned statement -# - -class VersionedEntityBuilder(EntityBuilder): - - def __init__(self, entity, ignore=None, check_concurrent=False, - column_names=None): - self.entity = entity - self.add_mapper_extension(versioned_mapper_extension) - #TODO: we should rather check that the version_id_col isn't set - # externally - self.check_concurrent = check_concurrent - - # Changes in these fields will be ignored - if column_names is None: - column_names = ['version', 'timestamp'] - entity.__versioned_column_names__ = column_names - if ignore is None: - ignore = [] - ignore.extend(column_names) - entity.__ignored_fields__ = ignore - - def create_non_pk_cols(self): - # add a version column to the entity, along with a timestamp - version_colname, timestamp_colname = \ - self.entity.__versioned_column_names__ - #XXX: fail in case the columns already exist? - #col_names = [col.name for col in self.entity._descriptor.columns] - #if version_colname not in col_names: - self.add_table_column(Column(version_colname, Integer)) - #if timestamp_colname not in col_names: - self.add_table_column(Column(timestamp_colname, DateTime)) - - # add a concurrent_version column to the entity, if required - if self.check_concurrent: - self.entity._descriptor.version_id_col = 'concurrent_version' - - # we copy columns from the main entity table, so we need it to exist first - def after_table(self): - entity = self.entity - version_colname, timestamp_colname = \ - entity.__versioned_column_names__ - - # look for events - after_revert_events = [] - for name, func in getmembers(entity, inspect.ismethod): - if getattr(func, '_elixir_after_revert', False): - after_revert_events.append(func) - - # create a history table for the entity - skipped_columns = [version_colname] - if self.check_concurrent: - skipped_columns.append('concurrent_version') - - columns = [ - column.copy() for column in entity.table.c - if column.name not in skipped_columns - ] - columns.append(Column(version_colname, Integer, primary_key=True)) - table = Table(entity.table.name + '_history', entity.table.metadata, - *columns - ) - entity.__history_table__ = table - - # create an object that represents a version of this entity - class Version(object): - pass - - # map the version class to the history table for this entity - Version.__name__ = entity.__name__ + 'Version' - Version.__versioned_entity__ = entity - mapper(Version, entity.__history_table__) - - version_col = getattr(table.c, version_colname) - timestamp_col = getattr(table.c, timestamp_colname) - - # attach utility methods and properties to the entity - def get_versions(self): - v = object_session(self).query(Version) \ - .filter(get_history_where(self)) \ - .order_by(version_col) \ - .all() - # history contains all the previous records. - # Add the current one to the list to get all the versions - v.append(self) - return v - - def get_as_of(self, dt): - # if the passed in timestamp is older than our current version's - # time stamp, then the most recent version is our current version - if getattr(self, timestamp_colname) < dt: - return self - - # otherwise, we need to look to the history table to get our - # older version - sess = object_session(self) - query = sess.query(Version) \ - .filter(and_(get_history_where(self), - timestamp_col <= dt)) \ - .order_by(desc(timestamp_col)).limit(1) - return query.first() - - def revert_to(self, to_version): - if isinstance(to_version, Version): - to_version = getattr(to_version, version_colname) - - old_version = table.select(and_( - get_history_where(self), - version_col == to_version - )).execute().fetchone() - - entity.table.update(get_entity_where(self)).execute( - dict(old_version.items()) - ) - - table.delete(and_(get_history_where(self), - version_col >= to_version)).execute() - self.expire() - for event in after_revert_events: - event(self) - - def revert(self): - assert getattr(self, version_colname) > 1 - self.revert_to(getattr(self, version_colname) - 1) - - def compare_with(self, version): - differences = {} - for column in self.table.c: - if column.name in (version_colname, 'concurrent_version'): - continue - this = getattr(self, column.name) - that = getattr(version, column.name) - if this != that: - differences[column.name] = (this, that) - return differences - - entity.versions = property(get_versions) - entity.get_as_of = get_as_of - entity.revert_to = revert_to - entity.revert = revert - entity.compare_with = compare_with - Version.compare_with = compare_with - -acts_as_versioned = Statement(VersionedEntityBuilder) - - -def after_revert(func): - """ - Decorator for watching for revert events. - """ - func._elixir_after_revert = True - return func - - diff --git a/libs/elixir/fields.py b/libs/elixir/fields.py deleted file mode 100644 index 8659cdd8..00000000 --- a/libs/elixir/fields.py +++ /dev/null @@ -1,191 +0,0 @@ -''' -This module provides support for defining the fields (columns) of your -entities. Elixir currently supports two syntaxes to do so: the default -`Attribute-based syntax`_ as well as the has_field_ DSL statement. - -Attribute-based syntax ----------------------- - -Here is a quick example of how to use the object-oriented syntax. - -.. sourcecode:: python - - class Person(Entity): - id = Field(Integer, primary_key=True) - name = Field(String(50), required=True) - ssn = Field(String(50), unique=True) - biography = Field(Text) - join_date = Field(DateTime, default=datetime.datetime.now) - photo = Field(Binary, deferred=True) - _email = Field(String(20), colname='email', synonym='email') - - def _set_email(self, email): - self._email = email - def _get_email(self): - return self._email - email = property(_get_email, _set_email) - - -The Field class takes one mandatory argument, which is its type. Please refer -to SQLAlchemy documentation for a list of `types supported by SQLAlchemy -`_. - -Following that first mandatory argument, fields can take any number of -optional keyword arguments. Please note that all the **arguments** that are -**not specifically processed by Elixir**, as mentioned in the documentation -below **are passed on to the SQLAlchemy ``Column`` object**. Please refer to -the `SQLAlchemy Column object's documentation -`_ for more details about other -supported keyword arguments. - -The following Elixir-specific arguments are supported: - -+-------------------+---------------------------------------------------------+ -| Argument Name | Description | -+===================+=========================================================+ -| ``required`` | Specify whether or not this field can be set to None | -| | (left without a value). Defaults to ``False``, unless | -| | the field is a primary key. | -+-------------------+---------------------------------------------------------+ -| ``colname`` | Specify a custom name for the column of this field. By | -| | default the column will have the same name as the | -| | attribute. | -+-------------------+---------------------------------------------------------+ -| ``deferred`` | Specify whether this particular column should be | -| | fetched by default (along with the other columns) when | -| | an instance of the entity is fetched from the database | -| | or rather only later on when this particular column is | -| | first referenced. This can be useful when one wants to | -| | avoid loading a large text or binary field into memory | -| | when its not needed. Individual columns can be lazy | -| | loaded by themselves (by using ``deferred=True``) | -| | or placed into groups that lazy-load together (by using | -| | ``deferred`` = `"group_name"`). | -+-------------------+---------------------------------------------------------+ -| ``synonym`` | Specify a synonym name for this field. The field will | -| | also be usable under that name in keyword-based Query | -| | functions such as filter_by. The Synonym class (see the | -| | `properties` module) provides a similar functionality | -| | with an (arguably) nicer syntax, but a limited scope. | -+-------------------+---------------------------------------------------------+ - -has_field ---------- - -The `has_field` statement allows you to define fields one at a time. - -The first argument is the name of the field, the second is its type. Following -these, any number of keyword arguments can be specified for additional -behavior. The following arguments are supported: - -+-------------------+---------------------------------------------------------+ -| Argument Name | Description | -+===================+=========================================================+ -| ``through`` | Specify a relation name to go through. This field will | -| | not exist as a column on the database but will be a | -| | property which automatically proxy values to the | -| | ``attribute`` attribute of the object pointed to by the | -| | relation. If the ``attribute`` argument is not present, | -| | the name of the current field will be used. In an | -| | has_field statement, you can only proxy through a | -| | belongs_to or an has_one relationship. | -+-------------------+---------------------------------------------------------+ -| ``attribute`` | Name of the "endpoint" attribute to proxy to. This | -| | should only be used in combination with the ``through`` | -| | argument. | -+-------------------+---------------------------------------------------------+ - - -Here is a quick example of how to use ``has_field``. - -.. sourcecode:: python - - class Person(Entity): - has_field('id', Integer, primary_key=True) - has_field('name', String(50)) -''' -from sqlalchemy import Column -from sqlalchemy.orm import deferred, synonym -from sqlalchemy.ext.associationproxy import association_proxy - -from elixir.statements import ClassMutator -from elixir.properties import Property - -__doc_all__ = ['Field'] - - -class Field(Property): - ''' - Represents the definition of a 'field' on an entity. - - This class represents a column on the table where the entity is stored. - ''' - - def __init__(self, type, *args, **kwargs): - super(Field, self).__init__() - - self.colname = kwargs.pop('colname', None) - self.synonym = kwargs.pop('synonym', None) - self.deferred = kwargs.pop('deferred', False) - if 'required' in kwargs: - kwargs['nullable'] = not kwargs.pop('required') - self.type = type - self.primary_key = kwargs.get('primary_key', False) - - self.column = None - self.property = None - - self.args = args - self.kwargs = kwargs - - def attach(self, entity, name): - # If no colname was defined (through the 'colname' kwarg), set - # it to the name of the attr. - if self.colname is None: - self.colname = name - super(Field, self).attach(entity, name) - - def create_pk_cols(self): - if self.primary_key: - self.create_col() - - def create_non_pk_cols(self): - if not self.primary_key: - self.create_col() - - def create_col(self): - self.column = Column(self.colname, self.type, - *self.args, **self.kwargs) - self.add_table_column(self.column) - - def create_properties(self): - if self.deferred: - group = None - if isinstance(self.deferred, basestring): - group = self.deferred - self.property = deferred(self.column, group=group) - elif self.name != self.colname: - # if the property name is different from the column name, we need - # to add an explicit property (otherwise nothing is needed as it's - # done automatically by SA) - self.property = self.column - - if self.property is not None: - self.add_mapper_property(self.name, self.property) - - if self.synonym: - self.add_mapper_property(self.synonym, synonym(self.name)) - - -def has_field_handler(entity, name, *args, **kwargs): - if 'through' in kwargs: - setattr(entity, name, - association_proxy(kwargs.pop('through'), - kwargs.pop('attribute', name), - **kwargs)) - return - field = Field(*args, **kwargs) - field.attach(entity, name) - -has_field = ClassMutator(has_field_handler) diff --git a/libs/elixir/options.py b/libs/elixir/options.py deleted file mode 100644 index 27d7d195..00000000 --- a/libs/elixir/options.py +++ /dev/null @@ -1,274 +0,0 @@ -''' -This module provides support for defining several options on your Elixir -entities. There are three different kinds of options that can be set -up, and for this there are three different statements: using_options_, -using_table_options_ and using_mapper_options_. - -Alternatively, these options can be set on all Elixir entities by modifying -the `options_defaults` dictionary before defining any entity. - -`using_options` ---------------- -The 'using_options' DSL statement allows you to set up some additional -behaviors on your model objects, including table names, ordering, and -more. To specify an option, simply supply the option as a keyword -argument onto the statement, as follows: - -.. sourcecode:: python - - class Person(Entity): - name = Field(Unicode(64)) - - using_options(shortnames=True, order_by='name') - -The list of supported arguments are as follows: - -+---------------------+-------------------------------------------------------+ -| Option Name | Description | -+=====================+=======================================================+ -| ``inheritance`` | Specify the type of inheritance this entity must use. | -| | It can be one of ``single``, ``concrete`` or | -| | ``multi``. Defaults to ``single``. | -| | Note that polymorphic concrete inheritance is | -| | currently not implemented. See: | -| | http://www.sqlalchemy.org/docs/05/mappers.html | -| | #mapping-class-inheritance-hierarchies for an | -| | explanation of the different kinds of inheritances. | -+---------------------+-------------------------------------------------------+ -| ``abstract`` | Set 'abstract'=True to declare abstract entity. | -| | Abstract base classes are useful when you want to put | -| | some common information into a number of other | -| | entities. Abstract entity will not be used to create | -| | any database table. Instead, when it is used as a base| -| | class for other entity, its fields will be added to | -| | those of the child class. | -+---------------------+-------------------------------------------------------+ -| ``polymorphic`` | Whether the inheritance should be polymorphic or not. | -| | Defaults to ``True``. The column used to store the | -| | type of each row is named "row_type" by default. You | -| | can change this by passing the desired name for the | -| | column to this argument. | -+---------------------+-------------------------------------------------------+ -| ``identity`` | Specify a custom polymorphic identity. When using | -| | polymorphic inheritance, this value (usually a | -| | string) will represent this particular entity (class) | -| | . It will be used to differentiate it from other | -| | entities (classes) in your inheritance hierarchy when | -| | loading from the database instances of different | -| | entities in that hierarchy at the same time. | -| | This value will be stored by default in the | -| | "row_type" column of the entity's table (see above). | -| | You can either provide a | -| | plain string or a callable. The callable will be | -| | given the entity (ie class) as argument and must | -| | return a value (usually a string) representing the | -| | polymorphic identity of that entity. | -| | By default, this value is automatically generated: it | -| | is the name of the entity lower-cased. | -+---------------------+-------------------------------------------------------+ -| ``metadata`` | Specify a custom MetaData for this entity. | -| | By default, entities uses the global | -| | ``elixir.metadata``. | -| | This option can also be set for all entities of a | -| | module by setting the ``__metadata__`` attribute of | -| | that module. | -+---------------------+-------------------------------------------------------+ -| ``autoload`` | Automatically load column definitions from the | -| | existing database table. | -+---------------------+-------------------------------------------------------+ -| ``tablename`` | Specify a custom tablename. You can either provide a | -| | plain string or a callable. The callable will be | -| | given the entity (ie class) as argument and must | -| | return a string representing the name of the table | -| | for that entity. By default, the tablename is | -| | automatically generated: it is a concatenation of the | -| | full module-path to the entity and the entity (class) | -| | name itself. The result is lower-cased and separated | -| | by underscores ("_"), eg.: for an entity named | -| | "MyEntity" in the module "project1.model", the | -| | generated table name will be | -| | "project1_model_myentity". | -+---------------------+-------------------------------------------------------+ -| ``shortnames`` | Specify whether or not the automatically generated | -| | table names include the full module-path | -| | to the entity. If ``shortnames`` is ``True``, only | -| | the entity name is used. Defaults to ``False``. | -+---------------------+-------------------------------------------------------+ -| ``auto_primarykey`` | If given as string, it will represent the | -| | auto-primary-key's column name. If this option | -| | is True, it will allow auto-creation of a primary | -| | key if there's no primary key defined for the | -| | corresponding entity. If this option is False, | -| | it will disallow auto-creation of a primary key. | -| | Defaults to ``True``. | -+---------------------+-------------------------------------------------------+ -| ``version_id_col`` | If this option is True, it will create a version | -| | column automatically using the default name. If given | -| | as string, it will create the column using that name. | -| | This can be used to prevent concurrent modifications | -| | to the entity's table rows (i.e. it will raise an | -| | exception if it happens). Defaults to ``False``. | -+---------------------+-------------------------------------------------------+ -| ``order_by`` | How to order select results. Either a string or a | -| | list of strings, composed of the field name, | -| | optionally lead by a minus (for descending order). | -+---------------------+-------------------------------------------------------+ -| ``session`` | Specify a custom contextual session for this entity. | -| | By default, entities uses the global | -| | ``elixir.session``. | -| | This option takes a ``ScopedSession`` object or | -| | ``None``. In the later case your entity will be | -| | mapped using a non-contextual mapper which requires | -| | manual session management, as seen in pure SQLAlchemy.| -| | This option can also be set for all entities of a | -| | module by setting the ``__session__`` attribute of | -| | that module. | -+---------------------+-------------------------------------------------------+ -| ``allowcoloverride``| Specify whether it is allowed to override columns. | -| | By default, Elixir forbids you to add a column to an | -| | entity's table which already exist in that table. If | -| | you set this option to ``True`` it will skip that | -| | check. Use with care as it is easy to shoot oneself | -| | in the foot when overriding columns. | -+---------------------+-------------------------------------------------------+ - -For examples, please refer to the examples and unit tests. - -`using_table_options` ---------------------- -The 'using_table_options' DSL statement allows you to set up some -additional options on your entity table. It is meant only to handle the -options which are not supported directly by the 'using_options' statement. -By opposition to the 'using_options' statement, these options are passed -directly to the underlying SQLAlchemy Table object (both non-keyword arguments -and keyword arguments) without any processing. - -For further information, please refer to the `SQLAlchemy table's documentation -`_. - -You might also be interested in the section about `constraints -`_. - -`using_mapper_options` ----------------------- -The 'using_mapper_options' DSL statement allows you to set up some -additional options on your entity mapper. It is meant only to handle the -options which are not supported directly by the 'using_options' statement. -By opposition to the 'using_options' statement, these options are passed -directly to the underlying SQLAlchemy mapper (as keyword arguments) -without any processing. - -For further information, please refer to the `SQLAlchemy mapper -function's documentation -`_. - -`using_options_defaults` ------------------------- -The 'using_options_defaults' DSL statement allows you to set up some -default options on a custom base class. These will be used as the default value -for options of all its subclasses. Note that any option not set within the -using_options_defaults (nor specifically on a particular Entity) will use the -global defaults, so you don't have to provide a default value for all options, -but only those you want to change. Please also note that this statement does -not work on normal entities, and the normal using_options statement does not -work on base classes (because normal options do not and should not propagate to -the children classes). -''' - -from sqlalchemy import Integer, String - -from elixir.statements import ClassMutator - -__doc_all__ = ['options_defaults'] - -OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s" -ALTERNATE_M2MCOL_NAMEFORMAT = "%(inversename)s_%(key)s" - -def default_m2m_column_formatter(data): - if data['selfref']: - return ALTERNATE_M2MCOL_NAMEFORMAT % data - else: - return OLD_M2MCOL_NAMEFORMAT % data - -NEW_M2MCOL_NAMEFORMAT = default_m2m_column_formatter - -# format constants -FKCOL_NAMEFORMAT = "%(relname)s_%(key)s" -M2MCOL_NAMEFORMAT = NEW_M2MCOL_NAMEFORMAT -CONSTRAINT_NAMEFORMAT = "%(tablename)s_%(colnames)s_fk" -MULTIINHERITANCECOL_NAMEFORMAT = "%(entity)s_%(key)s" - -# other global constants -DEFAULT_AUTO_PRIMARYKEY_NAME = "id" -DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer -DEFAULT_VERSION_ID_COL_NAME = "row_version" -DEFAULT_POLYMORPHIC_COL_NAME = "row_type" -POLYMORPHIC_COL_SIZE = 40 -POLYMORPHIC_COL_TYPE = String(POLYMORPHIC_COL_SIZE) - -# debugging/migration help -MIGRATION_TO_07_AID = False - -# -options_defaults = dict( - abstract=False, - inheritance='single', - polymorphic=True, - identity=None, - autoload=False, - tablename=None, - shortnames=False, - auto_primarykey=True, - version_id_col=False, - allowcoloverride=False, - order_by=None, - resolve_root=None, - mapper_options={}, - table_options={} -) - -valid_options = options_defaults.keys() + [ - 'metadata', - 'session', - 'collection' -] - - -def using_options_defaults_handler(entity, **kwargs): - for kwarg in kwargs: - if kwarg not in valid_options: - raise Exception("'%s' is not a valid option for Elixir entities." - % kwarg) - - # We use __dict__ instead of hasattr to not check its presence within the - # parent, and thus update the parent dict instead of creating a local dict. - if not entity.__dict__.get('options_defaults'): - entity.options_defaults = {} - entity.options_defaults.update(kwargs) - - -def using_options_handler(entity, *args, **kwargs): - for kwarg in kwargs: - if kwarg in valid_options: - setattr(entity._descriptor, kwarg, kwargs[kwarg]) - else: - raise Exception("'%s' is not a valid option for Elixir entities." - % kwarg) - - -def using_table_options_handler(entity, *args, **kwargs): - entity._descriptor.table_args.extend(list(args)) - entity._descriptor.table_options.update(kwargs) - - -def using_mapper_options_handler(entity, *args, **kwargs): - entity._descriptor.mapper_options.update(kwargs) - - -using_options_defaults = ClassMutator(using_options_defaults_handler) -using_options = ClassMutator(using_options_handler) -using_table_options = ClassMutator(using_table_options_handler) -using_mapper_options = ClassMutator(using_mapper_options_handler) diff --git a/libs/elixir/properties.py b/libs/elixir/properties.py deleted file mode 100644 index 68ff8fab..00000000 --- a/libs/elixir/properties.py +++ /dev/null @@ -1,244 +0,0 @@ -''' -This module provides support for defining properties on your entities. It both -provides, the `Property` class which acts as a building block for common -properties such as fields and relationships (for those, please consult the -corresponding modules), but also provides some more specialized properties, -such as `ColumnProperty` and `Synonym`. It also provides the GenericProperty -class which allows you to wrap any SQLAlchemy property, and its DSL-syntax -equivalent: has_property_. - -`has_property` --------------- -The ``has_property`` statement allows you to define properties which rely on -their entity's table (and columns) being defined before they can be declared -themselves. The `has_property` statement takes two arguments: first the name of -the property to be defined and second a function (often given as an anonymous -lambda) taking one argument and returning the desired SQLAlchemy property. That -function will be called whenever the entity table is completely defined, and -will be given the .c attribute of the entity as argument (as a way to access -the entity columns). - -Here is a quick example of how to use ``has_property``. - -.. sourcecode:: python - - class OrderLine(Entity): - has_field('quantity', Float) - has_field('unit_price', Float) - has_property('price', - lambda c: column_property( - (c.quantity * c.unit_price).label('price'))) -''' - -from elixir.statements import PropertyStatement -from sqlalchemy.orm import column_property, synonym - -__doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty', - 'ColumnProperty'] - -class EntityBuilder(object): - ''' - Abstract base class for all entity builders. An Entity builder is a class - of objects which can be added to an Entity (usually by using special - properties or statements) to "build" that entity. Building an entity, - meaning to add columns to its "main" table, create other tables, add - properties to its mapper, ... To do so an EntityBuilder must override the - corresponding method(s). This is to ensure the different operations happen - in the correct order (for example, that the table is fully created before - the mapper that use it is defined). - ''' - def create_pk_cols(self): - pass - - def create_non_pk_cols(self): - pass - - def before_table(self): - pass - - def create_tables(self): - ''' - Subclasses may override this method to create tables. - ''' - - def after_table(self): - pass - - def create_properties(self): - ''' - Subclasses may override this method to add properties to the involved - entity. - ''' - - def before_mapper(self): - pass - - def after_mapper(self): - pass - - def finalize(self): - pass - - # helper methods - def add_table_column(self, column): - self.entity._descriptor.add_column(column) - - def add_mapper_property(self, name, prop): - self.entity._descriptor.add_property(name, prop) - - def add_mapper_extension(self, ext): - self.entity._descriptor.add_mapper_extension(ext) - - -class CounterMeta(type): - ''' - A simple meta class which adds a ``_counter`` attribute to the instances of - the classes it is used on. This counter is simply incremented for each new - instance. - ''' - counter = 0 - - def __call__(self, *args, **kwargs): - instance = type.__call__(self, *args, **kwargs) - instance._counter = CounterMeta.counter - CounterMeta.counter += 1 - return instance - - -class Property(EntityBuilder): - ''' - Abstract base class for all properties of an Entity. - ''' - __metaclass__ = CounterMeta - - def __init__(self, *args, **kwargs): - self.entity = None - self.name = None - - def attach(self, entity, name): - """Attach this property to its entity, using 'name' as name. - - Properties will be attached in the order they were declared. - """ - self.entity = entity - self.name = name - - # register this property as a builder - entity._descriptor.builders.append(self) - - def __repr__(self): - return "Property(%s, %s)" % (self.name, self.entity) - - -class GenericProperty(Property): - ''' - Generic catch-all class to wrap an SQLAlchemy property. - - .. sourcecode:: python - - class OrderLine(Entity): - quantity = Field(Float) - unit_price = Field(Numeric) - price = GenericProperty(lambda c: column_property( - (c.quantity * c.unit_price).label('price'))) - ''' - - def __init__(self, prop, *args, **kwargs): - super(GenericProperty, self).__init__(*args, **kwargs) - self.prop = prop - #XXX: move this to Property? - self.args = args - self.kwargs = kwargs - - def create_properties(self): - if hasattr(self.prop, '__call__'): - prop_value = self.prop(self.entity.table.c) - else: - prop_value = self.prop - prop_value = self.evaluate_property(prop_value) - self.add_mapper_property(self.name, prop_value) - - def evaluate_property(self, prop): - if self.args or self.kwargs: - raise Exception('superfluous arguments passed to GenericProperty') - return prop - - -class ColumnProperty(GenericProperty): - ''' - A specialized form of the GenericProperty to generate SQLAlchemy - ``column_property``'s. - - It takes a function (often given as an anonymous lambda) as its first - argument. Other arguments and keyword arguments are forwarded to the - column_property construct. That first-argument function must accept exactly - one argument and must return the desired (scalar-returning) SQLAlchemy - ClauseElement. - - The function will be called whenever the entity table is completely - defined, and will be given - the .c attribute of the table of the entity as argument (as a way to - access the entity columns). The ColumnProperty will first wrap your - ClauseElement in an - "empty" label (ie it will be labelled automatically during queries), - then wrap that in a column_property. - - .. sourcecode:: python - - class OrderLine(Entity): - quantity = Field(Float) - unit_price = Field(Numeric) - price = ColumnProperty(lambda c: c.quantity * c.unit_price, - deferred=True) - - Please look at the `corresponding SQLAlchemy - documentation `_ for details. - ''' - - def evaluate_property(self, prop): - return column_property(prop.label(None), *self.args, **self.kwargs) - - -class Synonym(GenericProperty): - ''' - This class represents a synonym property of another property (column, ...) - of an entity. As opposed to the `synonym` kwarg to the Field class (which - share the same goal), this class can be used to define a synonym of a - property defined in a parent class (of the current class). On the other - hand, it cannot define a synonym for the purpose of using a standard python - property in queries. See the Field class for details on that usage. - - .. sourcecode:: python - - class Person(Entity): - name = Field(String(30)) - primary_email = Field(String(100)) - email_address = Synonym('primary_email') - - class User(Person): - user_name = Synonym('name') - password = Field(String(20)) - ''' - - def evaluate_property(self, prop): - return synonym(prop, *self.args, **self.kwargs) - -#class Composite(GenericProperty): -# def __init__(self, prop): -# super(GenericProperty, self).__init__() -# self.prop = prop - -# def evaluate_property(self, prop): -# return composite(prop.label(self.name)) - -#start = Composite(Point, lambda c: (c.x1, c.y1)) - -#mapper(Vertex, vertices, properties={ -# 'start':composite(Point, vertices.c.x1, vertices.c.y1), -# 'end':composite(Point, vertices.c.x2, vertices.c.y2) -#}) - - -has_property = PropertyStatement(GenericProperty) - diff --git a/libs/elixir/relationships.py b/libs/elixir/relationships.py deleted file mode 100644 index 6c14dbb6..00000000 --- a/libs/elixir/relationships.py +++ /dev/null @@ -1,1247 +0,0 @@ -''' -This module provides support for defining relationships between your Elixir -entities. Elixir currently supports two syntaxes to do so: the default -`Attribute-based syntax`_ which supports the following types of relationships: -ManyToOne_, OneToMany_, OneToOne_ and ManyToMany_, as well as a -`DSL-based syntax`_ which provides the following statements: belongs_to_, -has_many_, has_one_ and has_and_belongs_to_many_. - -====================== -Attribute-based syntax -====================== - -The first argument to all these "normal" relationship classes is the name of -the class (entity) you are relating to. - -Following that first mandatory argument, any number of additional keyword -arguments can be specified for advanced behavior. See each relationship type -for a list of their specific keyword arguments. At this point, we'll just note -that all the arguments that are not specifically processed by Elixir, as -mentioned in the documentation below are passed on to the SQLAlchemy -``relation`` function. So, please refer to the `SQLAlchemy relation function's -documentation `_ for further detail about which -keyword arguments are supported. - -You should keep in mind that the following -keyword arguments are automatically generated by Elixir and should not be used -unless you want to override the value provided by Elixir: ``uselist``, -``remote_side``, ``secondary``, ``primaryjoin`` and ``secondaryjoin``. - -Additionally, if you want a bidirectionnal relationship, you should define the -inverse relationship on the other entity explicitly (as opposed to how -SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will -match relationships together automatically. If there are several relationships -of the same type between two entities, Elixir is not able to determine which -relationship is the inverse of which, so you have to disambiguate the -situation by giving the name of the inverse relationship in the ``inverse`` -keyword argument. - -Here is a detailed explanation of each relation type: - -`ManyToOne` ------------ - -Describes the child's side of a parent-child relationship. For example, -a `Pet` object may belong to its owner, who is a `Person`. This could be -expressed like so: - -.. sourcecode:: python - - class Pet(Entity): - owner = ManyToOne('Person') - -Behind the scene, assuming the primary key of the `Person` entity is -an integer column named `id`, the ``ManyToOne`` relationship will -automatically add an integer column named `owner_id` to the entity, with a -foreign key referencing the `id` column of the `Person` entity. - -In addition to the keyword arguments inherited from SQLAlchemy's relation -function, ``ManyToOne`` relationships accept the following optional arguments -which will be directed to the created column: - -+----------------------+------------------------------------------------------+ -| Option Name | Description | -+======================+======================================================+ -| ``colname`` | Specify a custom name for the foreign key column(s). | -| | This argument accepts either a single string or a | -| | list of strings. The number of strings passed must | -| | match the number of primary key columns of the target| -| | entity. If this argument is not used, the name of the| -| | column(s) is generated with the pattern | -| | defined in options.FKCOL_NAMEFORMAT, which is, by | -| | default: "%(relname)s_%(key)s", where relname is the | -| | name of the ManyToOne relationship, and 'key' is the | -| | name (key) of the primary column in the target | -| | entity. That's with, in the above Pet/owner example, | -| | the name of the column would be: "owner_id". | -+----------------------+------------------------------------------------------+ -| ``required`` | Specify whether or not this field can be set to None | -| | (left without a value). Defaults to ``False``, | -| | unless the field is a primary key. | -+----------------------+------------------------------------------------------+ -| ``primary_key`` | Specify whether or not the column(s) created by this | -| | relationship should act as a primary_key. | -| | Defaults to ``False``. | -+----------------------+------------------------------------------------------+ -| ``column_kwargs`` | A dictionary holding any other keyword argument you | -| | might want to pass to the Column. | -+----------------------+------------------------------------------------------+ -| ``target_column`` | Name (or list of names) of the target column(s). | -| | If this argument is not specified, the target entity | -| | primary key column(s) are used. | -+----------------------+------------------------------------------------------+ - -The following optional arguments are also supported to customize the -ForeignKeyConstraint that is created: - -+----------------------+------------------------------------------------------+ -| Option Name | Description | -+======================+======================================================+ -| ``use_alter`` | If True, SQLAlchemy will add the constraint in a | -| | second SQL statement (as opposed to within the | -| | create table statement). This permits to define | -| | tables with a circular foreign key dependency | -| | between them. | -+----------------------+------------------------------------------------------+ -| ``ondelete`` | Value for the foreign key constraint ondelete clause.| -| | May be one of: ``cascade``, ``restrict``, | -| | ``set null``, or ``set default``. | -+----------------------+------------------------------------------------------+ -| ``onupdate`` | Value for the foreign key constraint onupdate clause.| -| | May be one of: ``cascade``, ``restrict``, | -| | ``set null``, or ``set default``. | -+----------------------+------------------------------------------------------+ -| ``constraint_kwargs``| A dictionary holding any other keyword argument you | -| | might want to pass to the Constraint. | -+----------------------+------------------------------------------------------+ - -In some cases, you may want to declare the foreign key column explicitly, -instead of letting it be generated automatically. There are several reasons to -that: it could be because you want to declare it with precise arguments and -using column_kwargs makes your code ugly, or because the name of -your column conflicts with the property name (in which case an error is -thrown). In those cases, you can use the ``field`` argument to specify an -already-declared field to be used for the foreign key column. - -For example, for the Pet example above, if you want the database column -(holding the foreign key) to be called 'owner', one should use the field -parameter to specify the field manually. - -.. sourcecode:: python - - class Pet(Entity): - owner_id = Field(Integer, colname='owner') - owner = ManyToOne('Person', field=owner_id) - -+----------------------+------------------------------------------------------+ -| Option Name | Description | -+======================+======================================================+ -| ``field`` | Specify the previously-declared field to be used for | -| | the foreign key column. Use of this parameter is | -| | mutually exclusive with the colname and column_kwargs| -| | arguments. | -+----------------------+------------------------------------------------------+ - - -Additionally, Elixir supports the belongs_to_ statement as an alternative, -DSL-based, syntax to define ManyToOne_ relationships. - - -`OneToMany` ------------ - -Describes the parent's side of a parent-child relationship when there can be -several children. For example, a `Person` object has many children, each of -them being a `Person`. This could be expressed like so: - -.. sourcecode:: python - - class Person(Entity): - parent = ManyToOne('Person') - children = OneToMany('Person') - -Note that a ``OneToMany`` relationship **cannot exist** without a -corresponding ``ManyToOne`` relationship in the other way. This is because the -``OneToMany`` relationship needs the foreign key created by the ``ManyToOne`` -relationship. - -In addition to keyword arguments inherited from SQLAlchemy, ``OneToMany`` -relationships accept the following optional (keyword) arguments: - -+--------------------+--------------------------------------------------------+ -| Option Name | Description | -+====================+========================================================+ -| ``order_by`` | Specify which field(s) should be used to sort the | -| | results given by accessing the relation field. | -| | Note that this sort order is only applied when loading | -| | objects from the database. Objects appended to the | -| | collection afterwards are not re-sorted in-memory on | -| | the fly. | -| | This argument accepts either a string or a list of | -| | strings, each corresponding to the name of a field in | -| | the target entity. These field names can optionally be | -| | prefixed by a minus (for descending order). | -+--------------------+--------------------------------------------------------+ -| ``filter`` | Specify a filter criterion (as a clause element) for | -| | this relationship. This criterion will be ``and_`` ed | -| | with the normal join criterion (primaryjoin) generated | -| | by Elixir for the relationship. For example: | -| | boston_addresses = | -| | OneToMany('Address', filter=Address.city == 'Boston') | -+--------------------+--------------------------------------------------------+ - -Additionally, Elixir supports an alternate, DSL-based, syntax to define -OneToMany_ relationships, with the has_many_ statement. - - -`OneToOne` ----------- - -Describes the parent's side of a parent-child relationship when there is only -one child. For example, a `Car` object has one gear stick, which is -represented as a `GearStick` object. This could be expressed like so: - -.. sourcecode:: python - - class Car(Entity): - gear_stick = OneToOne('GearStick', inverse='car') - - class GearStick(Entity): - car = ManyToOne('Car') - -Note that a ``OneToOne`` relationship **cannot exist** without a corresponding -``ManyToOne`` relationship in the other way. This is because the ``OneToOne`` -relationship needs the foreign_key created by the ``ManyToOne`` relationship. - -Additionally, Elixir supports an alternate, DSL-based, syntax to define -OneToOne_ relationships, with the has_one_ statement. - - -`ManyToMany` ------------- - -Describes a relationship in which one kind of entity can be related to several -objects of the other kind but the objects of that other kind can be related to -several objects of the first kind. For example, an `Article` can have several -tags, but the same `Tag` can be used on several articles. - -.. sourcecode:: python - - class Article(Entity): - tags = ManyToMany('Tag') - - class Tag(Entity): - articles = ManyToMany('Article') - -Behind the scene, the ``ManyToMany`` relationship will automatically create an -intermediate table to host its data. - -Note that you don't necessarily need to define the inverse relationship. In -our example, even though we want tags to be usable on several articles, we -might not be interested in which articles correspond to a particular tag. In -that case, we could have omitted the `Tag` side of the relationship. - -If your ``ManyToMany`` relationship is self-referencial, the entity -containing it is autoloaded (and you don't intend to specify both the -primaryjoin and secondaryjoin arguments manually), you must specify at least -one of either the ``remote_colname`` or ``local_colname`` argument. - -In addition to keyword arguments inherited from SQLAlchemy, ``ManyToMany`` -relationships accept the following optional (keyword) arguments: - -+--------------------+--------------------------------------------------------+ -| Option Name | Description | -+====================+========================================================+ -| ``tablename`` | Specify a custom name for the intermediary table. This | -| | can be used both when the tables needs to be created | -| | and when the table is autoloaded/reflected from the | -| | database. If this argument is not used, a name will be | -| | automatically generated by Elixir depending on the name| -| | of the tables of the two entities of the relationship, | -| | the name of the relationship, and, if present, the name| -| | of its inverse. Even though this argument is optional, | -| | it is wise to use it if you are not sure what are the | -| | exact consequence of using a generated table name. | -+--------------------+--------------------------------------------------------+ -| ``schema`` | Specify a custom schema for the intermediate table. | -| | This can be used both when the tables needs to | -| | be created and when the table is autoloaded/reflected | -| | from the database. | -+--------------------+--------------------------------------------------------+ -| ``remote_colname`` | A string or list of strings specifying the names of | -| | the column(s) in the intermediary table which | -| | reference the "remote"/target entity's table. | -+--------------------+--------------------------------------------------------+ -| ``local_colname`` | A string or list of strings specifying the names of | -| | the column(s) in the intermediary table which | -| | reference the "local"/current entity's table. | -+--------------------+--------------------------------------------------------+ -| ``table`` | Use a manually created table. If this argument is | -| | used, Elixir will not generate a table for this | -| | 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 | -| | results given by accessing the relation field. | -| | Note that this sort order is only applied when loading | -| | objects from the database. Objects appended to the | -| | collection afterwards are not re-sorted in-memory on | -| | the fly. | -| | This argument accepts either a string or a list of | -| | strings, each corresponding to the name of a field in | -| | the target entity. These field names can optionally be | -| | prefixed by a minus (for descending order). | -+----------------------+------------------------------------------------------+ -| ``ondelete`` | Value for the foreign key constraint ondelete clause. | -| | May be one of: ``cascade``, ``restrict``, | -| | ``set null``, or ``set default``. | -+--------------------+--------------------------------------------------------+ -| ``onupdate`` | Value for the foreign key constraint onupdate clause. | -| | May be one of: ``cascade``, ``restrict``, | -| | ``set null``, or ``set default``. | -+--------------------+--------------------------------------------------------+ -| ``table_kwargs`` | A dictionary holding any other keyword argument you | -| | might want to pass to the underlying Table object. | -+--------------------+--------------------------------------------------------+ - - -================ -DSL-based syntax -================ - -The following DSL statements provide an alternative way to define relationships -between your entities. The first argument to all those statements is the name -of the relationship, the second is the 'kind' of object you are relating to -(it is usually given using the ``of_kind`` keyword). - -`belongs_to` ------------- - -The ``belongs_to`` statement is the DSL syntax equivalent to the ManyToOne_ -relationship. As such, it supports all the same arguments as ManyToOne_ -relationships. - -.. sourcecode:: python - - class Pet(Entity): - belongs_to('feeder', of_kind='Person') - belongs_to('owner', of_kind='Person', colname="owner_id") - - -`has_many` ----------- - -The ``has_many`` statement is the DSL syntax equivalent to the OneToMany_ -relationship. As such, it supports all the same arguments as OneToMany_ -relationships. - -.. sourcecode:: python - - class Person(Entity): - belongs_to('parent', of_kind='Person') - has_many('children', of_kind='Person') - -There is also an alternate form of the ``has_many`` relationship that takes -only two keyword arguments: ``through`` and ``via`` in order to encourage a -richer form of many-to-many relationship that is an alternative to the -``has_and_belongs_to_many`` statement. Here is an example: - -.. sourcecode:: python - - class Person(Entity): - has_field('name', Unicode) - has_many('assignments', of_kind='Assignment') - has_many('projects', through='assignments', via='project') - - class Assignment(Entity): - has_field('start_date', DateTime) - belongs_to('person', of_kind='Person') - belongs_to('project', of_kind='Project') - - class Project(Entity): - has_field('title', Unicode) - has_many('assignments', of_kind='Assignment') - -In the above example, a `Person` has many `projects` through the `Assignment` -relationship object, via a `project` attribute. - - -`has_one` ---------- - -The ``has_one`` statement is the DSL syntax equivalent to the OneToOne_ -relationship. As such, it supports all the same arguments as OneToOne_ -relationships. - -.. sourcecode:: python - - class Car(Entity): - has_one('gear_stick', of_kind='GearStick', inverse='car') - - class GearStick(Entity): - belongs_to('car', of_kind='Car') - - -`has_and_belongs_to_many` -------------------------- - -The ``has_and_belongs_to_many`` statement is the DSL syntax equivalent to the -ManyToMany_ relationship. As such, it supports all the same arguments as -ManyToMany_ relationships. - -.. sourcecode:: python - - class Article(Entity): - has_and_belongs_to_many('tags', of_kind='Tag') - - class Tag(Entity): - has_and_belongs_to_many('articles', of_kind='Article') - -''' - -import warnings - -from sqlalchemy import ForeignKeyConstraint, Column, Table, and_ -from sqlalchemy.orm import relation, backref, class_mapper -from sqlalchemy.ext.associationproxy import association_proxy - -import options -from elixir.statements import ClassMutator -from elixir.properties import Property -from elixir.entity import EntityMeta, DEBUG - -__doc_all__ = [] - - -class Relationship(Property): - ''' - Base class for relationships. - ''' - - def __init__(self, of_kind, inverse=None, *args, **kwargs): - super(Relationship, self).__init__() - - self.of_kind = of_kind - self.inverse_name = inverse - - self._target = None - - self.property = None # sqlalchemy property - self.backref = None # sqlalchemy backref - - #TODO: unused for now - self.args = args - self.kwargs = kwargs - - def attach(self, entity, name): - super(Relationship, self).attach(entity, name) - entity._descriptor.relationships.append(self) - - def create_pk_cols(self): - self.create_keys(True) - - def create_non_pk_cols(self): - self.create_keys(False) - - def create_keys(self, pk): - ''' - Subclasses (ie. concrete relationships) may override this method to - create foreign keys. - ''' - - def create_properties(self): - if self.property or self.backref: - return - - kwargs = self.get_prop_kwargs() - if 'order_by' in kwargs: - kwargs['order_by'] = \ - self.target._descriptor.translate_order_by(kwargs['order_by']) - - # transform callable arguments - for arg in ('primaryjoin', 'secondaryjoin', 'remote_side', - 'foreign_keys'): - kwarg = kwargs.get(arg, None) - if hasattr(kwarg, '__call__'): - kwargs[arg] = kwarg() - - # viewonly relationships need to create "standalone" relations (ie - # shouldn't be a backref of another relation). - if self.inverse and not kwargs.get('viewonly', False): - # check if the inverse was already processed (and thus has already - # defined a backref we can use) - if self.inverse.backref: - # let the user override the backref argument - if 'backref' not in kwargs: - kwargs['backref'] = self.inverse.backref - else: - # SQLAlchemy doesn't like when 'secondary' is both defined on - # the relation and the backref - kwargs.pop('secondary', None) - - # define backref for use by the inverse - self.backref = backref(self.name, **kwargs) - return - - self.property = relation(self.target, **kwargs) - self.add_mapper_property(self.name, self.property) - - @property - def target(self): - if not self._target: - if isinstance(self.of_kind, basestring): - collection = self.entity._descriptor.collection - self._target = collection.resolve(self.of_kind, self.entity) - else: - self._target = self.of_kind - return self._target - - @property - def inverse(self): - if not hasattr(self, '_inverse'): - if self.inverse_name: - desc = self.target._descriptor - inverse = desc.find_relationship(self.inverse_name) - if inverse is None: - raise Exception( - "Couldn't find a relationship named '%s' in " - "entity '%s' or its parent entities." - % (self.inverse_name, self.target.__name__)) - assert self.match_type_of(inverse), \ - "Relationships '%s' in entity '%s' and '%s' in entity " \ - "'%s' cannot be inverse of each other because their " \ - "types do not form a valid combination." % \ - (self.name, self.entity.__name__, - self.inverse_name, self.target.__name__) - else: - check_reverse = not self.kwargs.get('viewonly', False) - if isinstance(self.target, EntityMeta): - inverse = self.target._descriptor.get_inverse_relation( - self, check_reverse=check_reverse) - else: - inverse = None - self._inverse = inverse - if inverse and not self.kwargs.get('viewonly', False): - inverse._inverse = self - - return self._inverse - - def match_type_of(self, other): - return False - - def is_inverse(self, other): - # viewonly relationships are not symmetrical: a viewonly relationship - # should have exactly one inverse (a ManyToOne relationship), but that - # inverse shouldn't have the viewonly relationship as its inverse. - return not other.kwargs.get('viewonly', False) and \ - other is not self and \ - self.match_type_of(other) and \ - self.entity == other.target and \ - other.entity == self.target and \ - (self.inverse_name == other.name or not self.inverse_name) and \ - (other.inverse_name == self.name or not other.inverse_name) - - -class ManyToOne(Relationship): - ''' - - ''' - - def __init__(self, of_kind, - column_kwargs=None, - colname=None, required=None, primary_key=None, - field=None, - constraint_kwargs=None, - use_alter=None, ondelete=None, onupdate=None, - target_column=None, - *args, **kwargs): - - # 1) handle column-related args - - # check that the column arguments don't conflict - assert not (field and (column_kwargs or colname)), \ - "ManyToOne can accept the 'field' argument or column " \ - "arguments ('colname' or 'column_kwargs') but not both!" - - if colname and not isinstance(colname, list): - colname = [colname] - self.colname = colname or [] - - column_kwargs = column_kwargs or {} - # kwargs go by default to the relation(), so we need to manually - # extract those targeting the Column - if required is not None: - column_kwargs['nullable'] = not required - if primary_key is not None: - column_kwargs['primary_key'] = primary_key - # by default, created columns will have an index. - column_kwargs.setdefault('index', True) - self.column_kwargs = column_kwargs - - if field and not isinstance(field, list): - field = [field] - self.field = field or [] - - # 2) handle constraint kwargs - constraint_kwargs = constraint_kwargs or {} - if use_alter is not None: - constraint_kwargs['use_alter'] = use_alter - if ondelete is not None: - constraint_kwargs['ondelete'] = ondelete - if onupdate is not None: - constraint_kwargs['onupdate'] = onupdate - self.constraint_kwargs = constraint_kwargs - - # 3) misc arguments - if target_column and not isinstance(target_column, list): - target_column = [target_column] - self.target_column = target_column - - self.foreign_key = [] - self.primaryjoin_clauses = [] - - super(ManyToOne, self).__init__(of_kind, *args, **kwargs) - - def match_type_of(self, other): - return isinstance(other, (OneToMany, OneToOne)) - - @property - def target_table(self): - if isinstance(self.target, EntityMeta): - return self.target._descriptor.table - else: - return class_mapper(self.target).local_table - - def create_keys(self, pk): - ''' - Find all primary keys on the target and create foreign keys on the - source accordingly. - ''' - - if self.foreign_key: - return - - if self.column_kwargs.get('primary_key', False) != pk: - return - - source_desc = self.entity._descriptor - if isinstance(self.target, EntityMeta): - # 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() - - #XXX: another option, instead of the FakeTable, would be to create an - # EntityDescriptor for the SA class. - target_table = self.target_table - - if source_desc.autoload: - #TODO: allow target_column to be used as an alternative to - # specifying primaryjoin, to be consistent with non-autoloaded - # tables - if self.colname: - if 'primaryjoin' not in self.kwargs: - self.primaryjoin_clauses = \ - _get_join_clauses(self.entity.table, - self.colname, None, - target_table)[0] - if not self.primaryjoin_clauses: - colnames = ', '.join(self.colname) - raise Exception( - "Couldn't find a foreign key constraint in table " - "'%s' using the following columns: %s." - % (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: - raise NotImplementedError( - "'field' argument not allowed on autoloaded table " - "relationships.") - else: - fk_refcols = [] - fk_colnames = [] - - if self.target_column is None: - target_columns = target_table.primary_key.columns - else: - target_columns = [target_table.columns[col] - for col in self.target_column] - - if not target_columns: - raise Exception("No primary key found in target table ('%s') " - "for the '%s' relationship of the '%s' entity." - % (target_table.name, self.name, - self.entity.__name__)) - if self.colname and \ - len(self.colname) != len(target_columns): - raise Exception( - "The number of column names provided in the colname " - "keyword argument of the '%s' relationship of the " - "'%s' entity is not the same as the number of columns " - "of the primary key of '%s'." - % (self.name, self.entity.__name__, - self.target.__name__)) - - for key_num, target_col in enumerate(target_columns): - if self.field: - col = self.field[key_num].column - else: - if self.colname: - colname = self.colname[key_num] - else: - colname = options.FKCOL_NAMEFORMAT % \ - {'relname': self.name, - 'key': target_col.key} - - # We can't add the column to the table directly as the - # table might not be created yet. - col = Column(colname, target_col.type, - **self.column_kwargs) - source_desc.add_column(col) - - # If the column name was specified, and it is the same as - # this property's name, there is going to be a conflict. - # Don't allow this to happen. - if col.key == self.name: - raise ValueError( - "ManyToOne named '%s' in '%s' conficts " - " with the column of the same name. " - "You should probably define the foreign key " - "field manually and use the 'field' " - "argument on the ManyToOne relationship" - % (self.name, self.entity.__name__)) - - # Build the list of local columns which will be part of - # the foreign key - self.foreign_key.append(col) - - # Store the names of those columns - fk_colnames.append(col.key) - - # Build the list of column "paths" the foreign key will - # point to - fk_refcols.append("%s.%s" % \ - (target_table.fullname, target_col.key)) - - # Build up the primary join. This is needed when you have - # several ManyToOne relationships between two objects - self.primaryjoin_clauses.append(col == target_col) - - if 'name' not in self.constraint_kwargs: - # In some databases (at least MySQL) the constraint name needs - # to be unique for the whole database, instead of per table. - fk_name = options.CONSTRAINT_NAMEFORMAT % \ - {'tablename': source_desc.tablename, - 'colnames': '_'.join(fk_colnames)} - self.constraint_kwargs['name'] = fk_name - - source_desc.add_constraint( - ForeignKeyConstraint(fk_colnames, fk_refcols, - **self.constraint_kwargs)) - - def get_prop_kwargs(self): - kwargs = {'uselist': False} - - if self.entity.table is self.target_table: - # this is needed because otherwise SA has no way to know what is - # the direction of the relationship since both columns present in - # the primaryjoin belong to the same table. In other words, it is - # necessary to know if this particular relation - # is the many-to-one side, or the one-to-xxx side. The foreignkey - # doesn't help in this case. - kwargs['remote_side'] = \ - [col for col in self.target_table.primary_key.columns] - - if self.primaryjoin_clauses: - kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) - - kwargs.update(self.kwargs) - - return kwargs - - -class OneToOne(Relationship): - uselist = False - - def __init__(self, of_kind, filter=None, *args, **kwargs): - self.filter = filter - if filter is not None: - # We set viewonly to True by default for filtered relationships, - # unless manually overridden. - # This is not strictly necessary, as SQLAlchemy allows non viewonly - # relationships with a custom join/filter. The example at: - # SADOCS/05/mappers.html#advdatamapping_relation_customjoin - # is not viewonly. Those relationships can be used as if the extra - # filter wasn't present when inserting. This can lead to a - # confusing behavior (if you insert data which doesn't match the - # extra criterion it'll get inserted anyway but you won't see it - # when you query back the attribute after a round-trip to the - # database). - if 'viewonly' not in kwargs: - kwargs['viewonly'] = True - super(OneToOne, self).__init__(of_kind, *args, **kwargs) - - def match_type_of(self, other): - return isinstance(other, ManyToOne) - - def create_keys(self, pk): - # make sure an inverse relationship exists - if self.inverse is None: - raise Exception( - "Couldn't find any relationship in '%s' which " - "match as inverse of the '%s' relationship " - "defined in the '%s' entity. If you are using " - "inheritance you " - "might need to specify inverse relationships " - "manually by using the 'inverse' argument." - % (self.target, self.name, - self.entity)) - - def get_prop_kwargs(self): - kwargs = {'uselist': self.uselist} - - #TODO: for now, we don't break any test if we remove those 2 lines. - # So, we should either complete the selfref test to prove that they - # are indeed useful, or remove them. It might be they are indeed - # useless because the remote_side is already setup in the other way - # (ManyToOne). - if self.entity.table is self.target.table: - # When using a manual/autoloaded table, it will be assigned - # an empty list, which doesn't seem to upset SQLAlchemy - kwargs['remote_side'] = self.inverse.foreign_key - - # Contrary to ManyToMany relationships, we need to specify the join - # clauses even if this relationship is not self-referencial because - # there could be several ManyToOne from the target class to us. - joinclauses = self.inverse.primaryjoin_clauses - if self.filter: - # We need to make a copy of the joinclauses, to not add the filter - # on the backref - joinclauses = joinclauses[:] + [self.filter(self.target.table.c)] - if joinclauses: - kwargs['primaryjoin'] = and_(*joinclauses) - - kwargs.update(self.kwargs) - - return kwargs - - -class OneToMany(OneToOne): - uselist = True - - -class ManyToMany(Relationship): - uselist = True - - def __init__(self, of_kind, tablename=None, - local_colname=None, remote_colname=None, - ondelete=None, onupdate=None, - table=None, schema=None, - filter=None, - table_kwargs=None, - *args, **kwargs): - self.user_tablename = tablename - - if local_colname and not isinstance(local_colname, list): - local_colname = [local_colname] - self.local_colname = local_colname or [] - if remote_colname and not isinstance(remote_colname, list): - remote_colname = [remote_colname] - self.remote_colname = remote_colname or [] - - self.ondelete = ondelete - self.onupdate = onupdate - - self.table = table - self.schema = schema - - #TODO: this can probably be simplified/moved elsewhere since the - #argument disappeared - self.column_format = options.M2MCOL_NAMEFORMAT - if not hasattr(self.column_format, '__call__'): - # we need to store the format in a variable so that the - # closure of the lambda is correct - format = self.column_format - self.column_format = lambda data: format % data - if options.MIGRATION_TO_07_AID: - self.column_format = \ - migration_aid_m2m_column_formatter( - lambda data: options.OLD_M2MCOL_NAMEFORMAT % data, - self.column_format) - - self.filter = filter - if filter is not None: - # We set viewonly to True by default for filtered relationships, - # unless manually overridden. - if 'viewonly' not in kwargs: - kwargs['viewonly'] = True - - self.table_kwargs = table_kwargs or {} - - self.primaryjoin_clauses = [] - self.secondaryjoin_clauses = [] - - super(ManyToMany, self).__init__(of_kind, *args, **kwargs) - - def match_type_of(self, other): - return isinstance(other, ManyToMany) - - def create_tables(self): - if self.table is not None: - if 'primaryjoin' not in self.kwargs or \ - 'secondaryjoin' not in self.kwargs: - self._build_join_clauses() - assert self.inverse is None or self.inverse.table is None or \ - self.inverse.table is self.table - return - - if self.inverse: - inverse = self.inverse - if inverse.table is not None: - self.table = inverse.table - self.primaryjoin_clauses = inverse.secondaryjoin_clauses - self.secondaryjoin_clauses = inverse.primaryjoin_clauses - return - - assert not inverse.user_tablename or not self.user_tablename or \ - inverse.user_tablename == self.user_tablename - assert not inverse.remote_colname or not self.local_colname or \ - inverse.remote_colname == self.local_colname - assert not inverse.local_colname or not self.remote_colname or \ - inverse.local_colname == self.remote_colname - assert not inverse.schema or not self.schema or \ - inverse.schema == self.schema - assert not inverse.table_kwargs or not self.table_kwargs or \ - inverse.table_kwargs == self.table_kwargs - - self.user_tablename = inverse.user_tablename or self.user_tablename - self.local_colname = inverse.remote_colname or self.local_colname - self.remote_colname = inverse.local_colname or self.remote_colname - self.schema = inverse.schema or self.schema - self.local_colname = inverse.remote_colname or self.local_colname - - # compute table_kwargs - complete_kwargs = options.options_defaults['table_options'].copy() - complete_kwargs.update(self.table_kwargs) - - #needs: table_options['schema'], autoload, tablename, primary_keys, - #entity.__name__, table_fullname - e1_desc = self.entity._descriptor - e2_desc = self.target._descriptor - - e1_schema = e1_desc.table_options.get('schema', None) - e2_schema = e2_desc.table_options.get('schema', None) - schema = (self.schema is not None) and self.schema or e1_schema - - assert e1_schema == e2_schema or self.schema, \ - "Schema %r for entity %s differs from schema %r of entity %s." \ - " Consider using the schema-parameter. "\ - % (e1_schema, self.entity.__name__, - e2_schema, self.target.__name__) - - # First, we compute the name of the table. Note that some of the - # intermediary variables are reused later for the constraint - # names. - - # We use the name of the relation for the first entity - # (instead of the name of its primary key), so that we can - # have two many-to-many relations between the same objects - # without having a table name collision. - source_part = "%s_%s" % (e1_desc.tablename, self.name) - - # And we use only the name of the table of the second entity - # when there is no inverse, so that a many-to-many relation - # can be defined without an inverse. - if self.inverse: - target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name) - else: - target_part = e2_desc.tablename - - if self.user_tablename: - tablename = self.user_tablename - else: - # We need to keep the table name consistent (independant of - # whether this relation or its inverse is setup first). - if self.inverse and source_part < target_part: - #XXX: use a different scheme for selfref (to not include the - # table name twice)? - tablename = "%s__%s" % (target_part, source_part) - else: - tablename = "%s__%s" % (source_part, target_part) - - if options.MIGRATION_TO_07_AID: - oldname = (self.inverse and - e1_desc.tablename < e2_desc.tablename) and \ - "%s__%s" % (target_part, source_part) or \ - "%s__%s" % (source_part, target_part) - if oldname != tablename: - warnings.warn( - "The generated table name for the '%s' relationship " - "on the '%s' entity changed from '%s' (the name " - "generated by Elixir 0.6.1 and earlier) to '%s'. " - "You should either rename the table in the database " - "to the new name or use the tablename argument on the " - "relationship to force the old name: tablename='%s'!" - % (self.name, self.entity.__name__, oldname, - tablename, oldname)) - - if e1_desc.autoload: - if not e2_desc.autoload: - raise Exception( - "Entity '%s' is autoloaded and its '%s' " - "ManyToMany relationship points to " - "the '%s' entity which is not autoloaded" - % (self.entity.__name__, self.name, - self.target.__name__)) - - self.table = Table(tablename, e1_desc.metadata, autoload=True, - **complete_kwargs) - if 'primaryjoin' not in self.kwargs or \ - 'secondaryjoin' not in self.kwargs: - self._build_join_clauses() - else: - # We pre-compute the names of the foreign key constraints - # pointing to the source (local) entity's table and to the - # target's table - - # In some databases (at least MySQL) the constraint names need - # to be unique for the whole database, instead of per table. - source_fk_name = "%s_fk" % source_part - if self.inverse: - target_fk_name = "%s_fk" % target_part - else: - target_fk_name = "%s_inverse_fk" % source_part - - columns = [] - constraints = [] - - for num, desc, fk_name, rel, inverse, colnames, join_clauses in ( - (0, e1_desc, source_fk_name, self, self.inverse, - self.local_colname, self.primaryjoin_clauses), - (1, e2_desc, target_fk_name, self.inverse, self, - self.remote_colname, self.secondaryjoin_clauses)): - - fk_colnames = [] - fk_refcols = [] - if colnames: - assert len(colnames) == len(desc.primary_keys) - else: - # The data generated here will be fed to the M2M column - # formatter to generate the name of the columns of the - # intermediate table for *one* side of the relationship, - # that is, from the intermediate table to the current - # entity, as stored in the "desc" variable. - data = {# A) relationships info - - # the name of the rel going *from* the entity - # we are currently generating a column pointing - # *to*. This is generally *not* what you want to - # use. eg in a "Post" and "Tag" example, with - # relationships named 'tags' and 'posts', when - # creating the columns from the intermediate - # table to the "Post" entity, 'relname' will - # contain 'tags'. - 'relname': rel and rel.name or 'inverse', - - # the name of the inverse relationship. In the - # above example, 'inversename' will contain - # 'posts'. - 'inversename': inverse and inverse.name - or 'inverse', - # is A == B? - 'selfref': e1_desc is e2_desc, - # provided for backward compatibility, DO NOT USE! - 'num': num, - # provided for backward compatibility, DO NOT USE! - 'numifself': e1_desc is e2_desc and str(num + 1) - or '', - # B) target information (from the perspective of - # the intermediate table) - 'target': desc.entity, - 'entity': desc.entity.__name__.lower(), - 'tablename': desc.tablename, - - # C) current (intermediate) table name - 'current_table': tablename - } - colnames = [] - for pk_col in desc.primary_keys: - data.update(key=pk_col.key) - colnames.append(self.column_format(data)) - - for pk_col, colname in zip(desc.primary_keys, colnames): - col = Column(colname, pk_col.type, primary_key=True) - columns.append(col) - - # Build the list of local columns which will be part - # of the foreign key. - fk_colnames.append(colname) - - # Build the list of column "paths" the foreign key will - # point to - target_path = "%s.%s" % (desc.table_fullname, pk_col.key) - fk_refcols.append(target_path) - - # Build join clauses (in case we have a self-ref) - if self.entity is self.target: - join_clauses.append(col == pk_col) - - onupdate = rel and rel.onupdate - ondelete = rel and rel.ondelete - - #FIXME: fk_name is misleading - constraints.append( - ForeignKeyConstraint(fk_colnames, fk_refcols, - name=fk_name, onupdate=onupdate, - ondelete=ondelete)) - - args = columns + constraints - - self.table = Table(tablename, e1_desc.metadata, - schema=schema, *args, **complete_kwargs) - if DEBUG: - print self.table.repr2() - - def _build_join_clauses(self): - # In the case we have a self-reference, we need to build join clauses - if self.entity is self.target: - if not self.local_colname and not self.remote_colname: - raise Exception( - "Self-referential ManyToMany " - "relationships in autoloaded entities need to have at " - "least one of either 'local_colname' or 'remote_colname' " - "argument specified. The '%s' relationship in the '%s' " - "entity doesn't have either." - % (self.name, self.entity.__name__)) - - self.primaryjoin_clauses, self.secondaryjoin_clauses = \ - _get_join_clauses(self.table, - self.local_colname, self.remote_colname, - self.entity.table) - - def get_prop_kwargs(self): - kwargs = {'secondary': self.table, - 'uselist': self.uselist} - - if self.filter: - # we need to make a copy of the joinclauses - secondaryjoin_clauses = self.secondaryjoin_clauses[:] + \ - [self.filter(self.target.table.c)] - else: - secondaryjoin_clauses = self.secondaryjoin_clauses - - if self.target is self.entity or self.filter: - kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) - kwargs['secondaryjoin'] = and_(*secondaryjoin_clauses) - - kwargs.update(self.kwargs) - - return kwargs - - def is_inverse(self, other): - return super(ManyToMany, self).is_inverse(other) and \ - (self.user_tablename == other.user_tablename or - (not self.user_tablename and not other.user_tablename)) - - -def migration_aid_m2m_column_formatter(oldformatter, newformatter): - def debug_formatter(data): - old_name = oldformatter(data) - new_name = newformatter(data) - if new_name != old_name: - complete_data = data.copy() - complete_data.update(old_name=old_name, - new_name=new_name, - targetname=data['target'].__name__) - # Specifying a stacklevel is useless in this case as the name - # generation is triggered by setup_all(), not by the declaration - # of the offending relationship. - warnings.warn("The '%(old_name)s' column in the " - "'%(current_table)s' table, used as the " - "intermediate table for the '%(relname)s' " - "relationship on the '%(targetname)s' entity " - "was renamed to '%(new_name)s'." - % complete_data) - return new_name - return debug_formatter - - -def _get_join_clauses(local_table, local_cols1, local_cols2, target_table): - primary_join, secondary_join = [], [] - cols1 = local_cols1[:] - cols1.sort() - cols1 = tuple(cols1) - - if local_cols2 is not None: - cols2 = local_cols2[:] - cols2.sort() - cols2 = tuple(cols2) - else: - cols2 = None - - # Build a map of fk constraints pointing to the correct table. - # The map is indexed on the local col names. - constraint_map = {} - for constraint in local_table.constraints: - if isinstance(constraint, ForeignKeyConstraint): - use_constraint = True - fk_colnames = [] - - # if all columns point to the correct table, we use the constraint - #TODO: check that it contains as many columns as the pk of the - #target entity, or even that it points to the actual pk columns - for fk in constraint.elements: - if fk.references(target_table): - # local column key - fk_colnames.append(fk.parent.key) - else: - use_constraint = False - if use_constraint: - fk_colnames.sort() - constraint_map[tuple(fk_colnames)] = constraint - - # Either the fk column names match explicitely with the columns given for - # one of the joins (primary or secondary), or we assume the current - # columns match because the columns for this join were not given and we - # know the other join is either not used (is None) or has an explicit - # match. - -#TODO: rewrite this. Even with the comment, I don't even understand it myself. - for cols, constraint in constraint_map.iteritems(): - if cols == cols1 or (cols != cols2 and - not cols1 and (cols2 in constraint_map or - cols2 is None)): - join = primary_join - elif cols == cols2 or (cols2 == () and cols1 in constraint_map): - join = secondary_join - else: - continue - for fk in constraint.elements: - join.append(fk.parent == fk.column) - return primary_join, secondary_join - - -def rel_mutator_handler(target): - def handler(entity, name, of_kind=None, through=None, via=None, - *args, **kwargs): - if through and via: - setattr(entity, name, - association_proxy(through, via, **kwargs)) - return - elif through or via: - raise Exception("'through' and 'via' relationship keyword " - "arguments should be used in combination.") - rel = target(of_kind, *args, **kwargs) - rel.attach(entity, name) - return handler - - -belongs_to = ClassMutator(rel_mutator_handler(ManyToOne)) -has_one = ClassMutator(rel_mutator_handler(OneToOne)) -has_many = ClassMutator(rel_mutator_handler(OneToMany)) -has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany)) diff --git a/libs/elixir/statements.py b/libs/elixir/statements.py deleted file mode 100644 index c21bf305..00000000 --- a/libs/elixir/statements.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys - -MUTATORS = '__elixir_mutators__' - -class ClassMutator(object): - ''' - DSL-style syntax - - A ``ClassMutator`` object represents a DSL term. - ''' - - def __init__(self, handler): - ''' - Create a new ClassMutator, using the `handler` callable to process it - when the time will come. - ''' - self.handler = handler - - # called when a mutator (eg. "has_field(...)") is parsed - def __call__(self, *args, **kwargs): - # self in this case is the "generic" mutator (eg "has_field") - - # jam this mutator into the class's mutator list - class_locals = sys._getframe(1).f_locals - mutators = class_locals.setdefault(MUTATORS, []) - mutators.append((self, args, kwargs)) - - def process(self, entity, *args, **kwargs): - ''' - Process one mutator. This version simply calls the handler callable, - but another mutator (sub)class could do more processing. - ''' - self.handler(entity, *args, **kwargs) - - -#TODO: move this to the super class (to be created here) of EntityMeta -def process_mutators(entity): - ''' - Apply all mutators of the given entity. That is, loop over all mutators - in the class's mutator list and process them. - ''' - # we don't use getattr here to not inherit from the parent mutators - # inadvertantly if the current entity hasn't defined any mutator. - mutators = entity.__dict__.get(MUTATORS, []) - for mutator, args, kwargs in mutators: - mutator.process(entity, *args, **kwargs) - -class Statement(ClassMutator): - - def process(self, entity, *args, **kwargs): - builder = self.handler(entity, *args, **kwargs) - entity._descriptor.builders.append(builder) - -class PropertyStatement(ClassMutator): - - def process(self, entity, name, *args, **kwargs): - prop = self.handler(*args, **kwargs) - prop.attach(entity, name) - diff --git a/libs/sqlalchemy/__init__.py b/libs/sqlalchemy/__init__.py index 9a21a70f..f6667f7d 100644 --- a/libs/sqlalchemy/__init__.py +++ b/libs/sqlalchemy/__init__.py @@ -1,15 +1,11 @@ # sqlalchemy/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -import inspect -import sys -import sqlalchemy.exc as exceptions - -from sqlalchemy.sql import ( +from .sql import ( alias, and_, asc, @@ -25,6 +21,7 @@ from sqlalchemy.sql import ( except_all, exists, extract, + false, func, insert, intersect, @@ -42,6 +39,7 @@ from sqlalchemy.sql import ( select, subquery, text, + true, tuple_, type_coerce, union, @@ -49,7 +47,7 @@ from sqlalchemy.sql import ( update, ) -from sqlalchemy.types import ( +from .types import ( BIGINT, BINARY, BLOB, @@ -94,12 +92,11 @@ from sqlalchemy.types import ( ) -from sqlalchemy.schema import ( +from .schema import ( CheckConstraint, Column, ColumnDefault, Constraint, - DDL, DefaultClause, FetchedValue, ForeignKey, @@ -112,17 +109,25 @@ from sqlalchemy.schema import ( Table, ThreadLocalMetaData, UniqueConstraint, - ) - -from sqlalchemy.engine import create_engine, engine_from_config + DDL, +) -__all__ = sorted(name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj))) +from .inspection import inspect +from .engine import create_engine, engine_from_config -__version__ = '0.7.10' +__version__ = '0.9.1' -del inspect, sys +def __go(lcls): + global __all__ -from sqlalchemy import util as _sa_util -_sa_util.importlater.resolve_all() + from . import events + from . import util as _sa_util + + import inspect as _inspect + + __all__ = sorted(name for name, obj in lcls.items() + if not (name.startswith('_') or _inspect.ismodule(obj))) + + _sa_util.dependencies.resolve_all("sqlalchemy") +__go(locals()) \ No newline at end of file diff --git a/libs/sqlalchemy/cextension/processors.c b/libs/sqlalchemy/cextension/processors.c index 427db5d8..003c2e32 100644 --- a/libs/sqlalchemy/cextension/processors.c +++ b/libs/sqlalchemy/cextension/processors.c @@ -1,6 +1,7 @@ /* processors.c -Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com +Copyright (C) 2010-2014 the SQLAlchemy authors and contributors +Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com This module is part of SQLAlchemy and is released under the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -9,13 +10,15 @@ the MIT License: http://www.opensource.org/licenses/mit-license.php #include #include +#define MODULE_NAME "cprocessors" +#define MODULE_DOC "Module containing C versions of data processing functions." + #if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) typedef int Py_ssize_t; #define PY_SSIZE_T_MAX INT_MAX #define PY_SSIZE_T_MIN INT_MIN #endif - static PyObject * int_to_boolean(PyObject *self, PyObject *arg) { @@ -25,7 +28,12 @@ int_to_boolean(PyObject *self, PyObject *arg) if (arg == Py_None) Py_RETURN_NONE; + +#if PY_MAJOR_VERSION >= 3 + l = PyLong_AsLong(arg); +#else l = PyInt_AsLong(arg); +#endif if (l == 0) { res = Py_False; } else if (l == 1) { @@ -64,23 +72,48 @@ to_float(PyObject *self, PyObject *arg) static PyObject * str_to_datetime(PyObject *self, PyObject *arg) { +#if PY_MAJOR_VERSION >= 3 + PyObject *bytes; + PyObject *err_bytes; +#endif const char *str; + int numparsed; unsigned int year, month, day, hour, minute, second, microsecond = 0; PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; +#if PY_MAJOR_VERSION >= 3 + bytes = PyUnicode_AsASCIIString(arg); + if (bytes == NULL) + str = NULL; + else + str = PyBytes_AS_STRING(bytes); +#else str = PyString_AsString(arg); +#endif if (str == NULL) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse datetime string '%.200s' " + "- value is not a string.", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse datetime string '%.200s' " "- value is not a string.", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } @@ -91,15 +124,30 @@ str_to_datetime(PyObject *self, PyObject *arg) not accept "2000-01-01 00:00:00.". I don't know which is better, but they should be coherent. */ - if (sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day, - &hour, &minute, &second, µsecond) < 6) { + numparsed = sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day, + &hour, &minute, &second, µsecond); +#if PY_MAJOR_VERSION >= 3 + Py_DECREF(bytes); +#endif + if (numparsed < 6) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse datetime string: %.200s", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse datetime string: %.200s", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } @@ -110,22 +158,47 @@ str_to_datetime(PyObject *self, PyObject *arg) static PyObject * str_to_time(PyObject *self, PyObject *arg) { +#if PY_MAJOR_VERSION >= 3 + PyObject *bytes; + PyObject *err_bytes; +#endif const char *str; + int numparsed; unsigned int hour, minute, second, microsecond = 0; PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; +#if PY_MAJOR_VERSION >= 3 + bytes = PyUnicode_AsASCIIString(arg); + if (bytes == NULL) + str = NULL; + else + str = PyBytes_AS_STRING(bytes); +#else str = PyString_AsString(arg); +#endif if (str == NULL) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; + +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse time string '%.200s' - value is not a string.", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse time string '%.200s' - value is not a string.", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } @@ -136,15 +209,30 @@ str_to_time(PyObject *self, PyObject *arg) not accept "00:00:00.". I don't know which is better, but they should be coherent. */ - if (sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second, - µsecond) < 3) { + numparsed = sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second, + µsecond); +#if PY_MAJOR_VERSION >= 3 + Py_DECREF(bytes); +#endif + if (numparsed < 3) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse time string: %.200s", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse time string: %.200s", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } @@ -154,34 +242,73 @@ str_to_time(PyObject *self, PyObject *arg) static PyObject * str_to_date(PyObject *self, PyObject *arg) { +#if PY_MAJOR_VERSION >= 3 + PyObject *bytes; + PyObject *err_bytes; +#endif const char *str; + int numparsed; unsigned int year, month, day; PyObject *err_repr; if (arg == Py_None) Py_RETURN_NONE; +#if PY_MAJOR_VERSION >= 3 + bytes = PyUnicode_AsASCIIString(arg); + if (bytes == NULL) + str = NULL; + else + str = PyBytes_AS_STRING(bytes); +#else str = PyString_AsString(arg); +#endif if (str == NULL) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse date string '%.200s' - value is not a string.", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse date string '%.200s' - value is not a string.", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } - if (sscanf(str, "%4u-%2u-%2u", &year, &month, &day) != 3) { + numparsed = sscanf(str, "%4u-%2u-%2u", &year, &month, &day); +#if PY_MAJOR_VERSION >= 3 + Py_DECREF(bytes); +#endif + if (numparsed != 3) { err_repr = PyObject_Repr(arg); if (err_repr == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(err_repr); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_ValueError, + "Couldn't parse date string: %.200s", + PyBytes_AS_STRING(err_bytes)); + Py_DECREF(err_bytes); +#else PyErr_Format( PyExc_ValueError, "Couldn't parse date string: %.200s", PyString_AsString(err_repr)); +#endif Py_DECREF(err_repr); return NULL; } @@ -218,17 +345,35 @@ UnicodeResultProcessor_init(UnicodeResultProcessor *self, PyObject *args, PyObject *encoding, *errors = NULL; static char *kwlist[] = {"encoding", "errors", NULL}; +#if PY_MAJOR_VERSION >= 3 + if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|U:__init__", kwlist, + &encoding, &errors)) + return -1; +#else if (!PyArg_ParseTupleAndKeywords(args, kwds, "S|S:__init__", kwlist, &encoding, &errors)) return -1; +#endif +#if PY_MAJOR_VERSION >= 3 + encoding = PyUnicode_AsASCIIString(encoding); +#else Py_INCREF(encoding); +#endif self->encoding = encoding; if (errors) { +#if PY_MAJOR_VERSION >= 3 + errors = PyUnicode_AsASCIIString(errors); +#else Py_INCREF(errors); +#endif } else { +#if PY_MAJOR_VERSION >= 3 + errors = PyBytes_FromString("strict"); +#else errors = PyString_FromString("strict"); +#endif if (errors == NULL) return -1; } @@ -247,11 +392,19 @@ UnicodeResultProcessor_process(UnicodeResultProcessor *self, PyObject *value) if (value == Py_None) Py_RETURN_NONE; +#if PY_MAJOR_VERSION >= 3 + if (PyBytes_AsStringAndSize(value, &str, &len)) + return NULL; + + encoding = PyBytes_AS_STRING(self->encoding); + errors = PyBytes_AS_STRING(self->errors); +#else if (PyString_AsStringAndSize(value, &str, &len)) return NULL; encoding = PyString_AS_STRING(self->encoding); errors = PyString_AS_STRING(self->errors); +#endif return PyUnicode_Decode(str, len, encoding, errors); } @@ -261,7 +414,11 @@ UnicodeResultProcessor_dealloc(UnicodeResultProcessor *self) { Py_XDECREF(self->encoding); Py_XDECREF(self->errors); +#if PY_MAJOR_VERSION >= 3 + Py_TYPE(self)->tp_free((PyObject*)self); +#else self->ob_type->tp_free((PyObject*)self); +#endif } static PyMethodDef UnicodeResultProcessor_methods[] = { @@ -271,8 +428,7 @@ static PyMethodDef UnicodeResultProcessor_methods[] = { }; static PyTypeObject UnicodeResultProcessorType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ + PyVarObject_HEAD_INIT(NULL, 0) "sqlalchemy.cprocessors.UnicodeResultProcessor", /* tp_name */ sizeof(UnicodeResultProcessor), /* tp_basicsize */ 0, /* tp_itemsize */ @@ -322,7 +478,11 @@ DecimalResultProcessor_init(DecimalResultProcessor *self, PyObject *args, { PyObject *type, *format; +#if PY_MAJOR_VERSION >= 3 + if (!PyArg_ParseTuple(args, "OU", &type, &format)) +#else if (!PyArg_ParseTuple(args, "OS", &type, &format)) +#endif return -1; Py_INCREF(type); @@ -342,11 +502,21 @@ DecimalResultProcessor_process(DecimalResultProcessor *self, PyObject *value) if (value == Py_None) Py_RETURN_NONE; + /* Decimal does not accept float values directly */ + /* SQLite can also give us an integer here (see [ticket:2432]) */ + /* XXX: starting with Python 3.1, we could use Decimal.from_float(f), + but the result wouldn't be the same */ + args = PyTuple_Pack(1, value); if (args == NULL) return NULL; +#if PY_MAJOR_VERSION >= 3 + str = PyUnicode_Format(self->format, args); +#else str = PyString_Format(self->format, args); +#endif + Py_DECREF(args); if (str == NULL) return NULL; @@ -361,7 +531,11 @@ DecimalResultProcessor_dealloc(DecimalResultProcessor *self) { Py_XDECREF(self->type); Py_XDECREF(self->format); +#if PY_MAJOR_VERSION >= 3 + Py_TYPE(self)->tp_free((PyObject*)self); +#else self->ob_type->tp_free((PyObject*)self); +#endif } static PyMethodDef DecimalResultProcessor_methods[] = { @@ -371,8 +545,7 @@ static PyMethodDef DecimalResultProcessor_methods[] = { }; static PyTypeObject DecimalResultProcessorType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ + PyVarObject_HEAD_INIT(NULL, 0) "sqlalchemy.DecimalResultProcessor", /* tp_name */ sizeof(DecimalResultProcessor), /* tp_basicsize */ 0, /* tp_itemsize */ @@ -412,11 +585,6 @@ static PyTypeObject DecimalResultProcessorType = { 0, /* tp_new */ }; -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - - static PyMethodDef module_methods[] = { {"int_to_boolean", int_to_boolean, METH_O, "Convert an integer to a boolean."}, @@ -433,23 +601,53 @@ static PyMethodDef module_methods[] = { {NULL, NULL, 0, NULL} /* Sentinel */ }; +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif + + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + MODULE_NAME, + MODULE_DOC, + -1, + module_methods +}; + +#define INITERROR return NULL + +PyObject * +PyInit_cprocessors(void) + +#else + +#define INITERROR return + PyMODINIT_FUNC initcprocessors(void) + +#endif + { PyObject *m; UnicodeResultProcessorType.tp_new = PyType_GenericNew; if (PyType_Ready(&UnicodeResultProcessorType) < 0) - return; + INITERROR; DecimalResultProcessorType.tp_new = PyType_GenericNew; if (PyType_Ready(&DecimalResultProcessorType) < 0) - return; + INITERROR; - m = Py_InitModule3("cprocessors", module_methods, - "Module containing C versions of data processing functions."); +#if PY_MAJOR_VERSION >= 3 + m = PyModule_Create(&module_def); +#else + m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); +#endif if (m == NULL) - return; + INITERROR; PyDateTime_IMPORT; @@ -460,5 +658,8 @@ initcprocessors(void) Py_INCREF(&DecimalResultProcessorType); PyModule_AddObject(m, "DecimalResultProcessor", (PyObject *)&DecimalResultProcessorType); -} +#if PY_MAJOR_VERSION >= 3 + return m; +#endif +} diff --git a/libs/sqlalchemy/cextension/resultproxy.c b/libs/sqlalchemy/cextension/resultproxy.c index ca9d28e6..481352a4 100644 --- a/libs/sqlalchemy/cextension/resultproxy.c +++ b/libs/sqlalchemy/cextension/resultproxy.c @@ -1,6 +1,7 @@ /* resultproxy.c -Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com +Copyright (C) 2010-2014 the SQLAlchemy authors and contributors +Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com This module is part of SQLAlchemy and is released under the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,6 +9,9 @@ the MIT License: http://www.opensource.org/licenses/mit-license.php #include +#define MODULE_NAME "cresultproxy" +#define MODULE_DOC "Module containing C versions of core ResultProxy classes." + #if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) typedef int Py_ssize_t; #define PY_SSIZE_T_MAX INT_MAX @@ -121,7 +125,7 @@ BaseRowProxy_reduce(PyObject *self) if (state == NULL) return NULL; - module = PyImport_ImportModule("sqlalchemy.engine.base"); + module = PyImport_ImportModule("sqlalchemy.engine.result"); if (module == NULL) return NULL; @@ -149,7 +153,11 @@ BaseRowProxy_dealloc(BaseRowProxy *self) Py_XDECREF(self->row); Py_XDECREF(self->processors); Py_XDECREF(self->keymap); +#if PY_MAJOR_VERSION >= 3 + Py_TYPE(self)->tp_free((PyObject *)self); +#else self->ob_type->tp_free((PyObject *)self); +#endif } static PyObject * @@ -244,14 +252,21 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) PyObject *processor, *value, *processed_value; PyObject *row, *record, *result, *indexobject; PyObject *exc_module, *exception, *cstr_obj; +#if PY_MAJOR_VERSION >= 3 + PyObject *bytes; +#endif char *cstr_key; long index; int key_fallback = 0; int tuple_check = 0; +#if PY_MAJOR_VERSION < 3 if (PyInt_CheckExact(key)) { index = PyInt_AS_LONG(key); - } else if (PyLong_CheckExact(key)) { + } +#endif + + if (PyLong_CheckExact(key)) { index = PyLong_AsLong(key); if ((index == -1) && PyErr_Occurred()) /* -1 can be either the actual value, or an error flag. */ @@ -304,7 +319,21 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) cstr_obj = PyObject_Str(key); if (cstr_obj == NULL) return NULL; + +/* + FIXME: raise encoding error exception (in both versions below) + if the key contains non-ascii chars, instead of an + InvalidRequestError without any message like in the + python version. +*/ +#if PY_MAJOR_VERSION >= 3 + bytes = PyUnicode_AsASCIIString(cstr_obj); + if (bytes == NULL) + return NULL; + cstr_key = PyBytes_AS_STRING(bytes); +#else cstr_key = PyString_AsString(cstr_obj); +#endif if (cstr_key == NULL) { Py_DECREF(cstr_obj); return NULL; @@ -317,7 +346,11 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) return NULL; } +#if PY_MAJOR_VERSION >= 3 + index = PyLong_AsLong(indexobject); +#else index = PyInt_AsLong(indexobject); +#endif if ((index == -1) && PyErr_Occurred()) /* -1 can be either the actual value, or an error flag. */ return NULL; @@ -356,13 +389,23 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) static PyObject * BaseRowProxy_getitem(PyObject *self, Py_ssize_t i) { - return BaseRowProxy_subscript((BaseRowProxy*)self, PyInt_FromSsize_t(i)); + PyObject *index; + +#if PY_MAJOR_VERSION >= 3 + index = PyLong_FromSsize_t(i); +#else + index = PyInt_FromSsize_t(i); +#endif + return BaseRowProxy_subscript((BaseRowProxy*)self, index); } static PyObject * BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name) { PyObject *tmp; +#if PY_MAJOR_VERSION >= 3 + PyObject *err_bytes; +#endif if (!(tmp = PyObject_GenericGetAttr((PyObject *)self, name))) { if (!PyErr_ExceptionMatches(PyExc_AttributeError)) @@ -374,11 +417,23 @@ BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name) tmp = BaseRowProxy_subscript(self, name); if (tmp == NULL && PyErr_ExceptionMatches(PyExc_KeyError)) { + +#if PY_MAJOR_VERSION >= 3 + err_bytes = PyUnicode_AsASCIIString(name); + if (err_bytes == NULL) + return NULL; + PyErr_Format( + PyExc_AttributeError, + "Could not locate column in row for column '%.200s'", + PyBytes_AS_STRING(err_bytes) + ); +#else PyErr_Format( PyExc_AttributeError, "Could not locate column in row for column '%.200s'", PyString_AsString(name) ); +#endif return NULL; } return tmp; @@ -406,7 +461,7 @@ BaseRowProxy_setparent(BaseRowProxy *self, PyObject *value, void *closure) return -1; } - module = PyImport_ImportModule("sqlalchemy.engine.base"); + module = PyImport_ImportModule("sqlalchemy.engine.result"); if (module == NULL) return -1; @@ -564,8 +619,7 @@ static PyMappingMethods BaseRowProxy_as_mapping = { }; static PyTypeObject BaseRowProxyType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ + PyVarObject_HEAD_INIT(NULL, 0) "sqlalchemy.cresultproxy.BaseRowProxy", /* tp_name */ sizeof(BaseRowProxy), /* tp_basicsize */ 0, /* tp_itemsize */ @@ -605,34 +659,60 @@ static PyTypeObject BaseRowProxyType = { 0 /* tp_new */ }; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - - static PyMethodDef module_methods[] = { {"safe_rowproxy_reconstructor", safe_rowproxy_reconstructor, METH_VARARGS, "reconstruct a RowProxy instance from its pickled form."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif + + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + MODULE_NAME, + MODULE_DOC, + -1, + module_methods +}; + +#define INITERROR return NULL + +PyObject * +PyInit_cresultproxy(void) + +#else + +#define INITERROR return + PyMODINIT_FUNC initcresultproxy(void) + +#endif + { PyObject *m; BaseRowProxyType.tp_new = PyType_GenericNew; if (PyType_Ready(&BaseRowProxyType) < 0) - return; + INITERROR; - m = Py_InitModule3("cresultproxy", module_methods, - "Module containing C versions of core ResultProxy classes."); +#if PY_MAJOR_VERSION >= 3 + m = PyModule_Create(&module_def); +#else + m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); +#endif if (m == NULL) - return; + INITERROR; Py_INCREF(&BaseRowProxyType); PyModule_AddObject(m, "BaseRowProxy", (PyObject *)&BaseRowProxyType); +#if PY_MAJOR_VERSION >= 3 + return m; +#endif } - diff --git a/libs/sqlalchemy/cextension/utils.c b/libs/sqlalchemy/cextension/utils.c new file mode 100644 index 00000000..3c556e1c --- /dev/null +++ b/libs/sqlalchemy/cextension/utils.c @@ -0,0 +1,225 @@ +/* +utils.c +Copyright (C) 2012-2014 the SQLAlchemy authors and contributors + +This module is part of SQLAlchemy and is released under +the MIT License: http://www.opensource.org/licenses/mit-license.php +*/ + +#include + +#define MODULE_NAME "cutils" +#define MODULE_DOC "Module containing C versions of utility functions." + +/* + Given arguments from the calling form *multiparams, **params, + return a list of bind parameter structures, usually a list of + dictionaries. + + In the case of 'raw' execution which accepts positional parameters, + it may be a list of tuples or lists. + + */ +static PyObject * +distill_params(PyObject *self, PyObject *args) +{ + PyObject *multiparams, *params; + PyObject *enclosing_list, *double_enclosing_list; + PyObject *zero_element, *zero_element_item; + Py_ssize_t multiparam_size, zero_element_length; + + if (!PyArg_UnpackTuple(args, "_distill_params", 2, 2, &multiparams, ¶ms)) { + return NULL; + } + + if (multiparams != Py_None) { + multiparam_size = PyTuple_Size(multiparams); + if (multiparam_size < 0) { + return NULL; + } + } + else { + multiparam_size = 0; + } + + if (multiparam_size == 0) { + if (params != Py_None && PyDict_Size(params) != 0) { + enclosing_list = PyList_New(1); + if (enclosing_list == NULL) { + return NULL; + } + Py_INCREF(params); + if (PyList_SetItem(enclosing_list, 0, params) == -1) { + Py_DECREF(params); + Py_DECREF(enclosing_list); + return NULL; + } + } + else { + enclosing_list = PyList_New(0); + if (enclosing_list == NULL) { + return NULL; + } + } + return enclosing_list; + } + else if (multiparam_size == 1) { + zero_element = PyTuple_GetItem(multiparams, 0); + if (PyTuple_Check(zero_element) || PyList_Check(zero_element)) { + zero_element_length = PySequence_Length(zero_element); + + if (zero_element_length != 0) { + zero_element_item = PySequence_GetItem(zero_element, 0); + if (zero_element_item == NULL) { + return NULL; + } + } + else { + zero_element_item = NULL; + } + + if (zero_element_length == 0 || + ( + PyObject_HasAttrString(zero_element_item, "__iter__") && + !PyObject_HasAttrString(zero_element_item, "strip") + ) + ) { + /* + * execute(stmt, [{}, {}, {}, ...]) + * execute(stmt, [(), (), (), ...]) + */ + Py_XDECREF(zero_element_item); + Py_INCREF(zero_element); + return zero_element; + } + else { + /* + * execute(stmt, ("value", "value")) + */ + Py_XDECREF(zero_element_item); + enclosing_list = PyList_New(1); + if (enclosing_list == NULL) { + return NULL; + } + Py_INCREF(zero_element); + if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { + Py_DECREF(zero_element); + Py_DECREF(enclosing_list); + return NULL; + } + return enclosing_list; + } + } + else if (PyObject_HasAttrString(zero_element, "keys")) { + /* + * execute(stmt, {"key":"value"}) + */ + enclosing_list = PyList_New(1); + if (enclosing_list == NULL) { + return NULL; + } + Py_INCREF(zero_element); + if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { + Py_DECREF(zero_element); + Py_DECREF(enclosing_list); + return NULL; + } + return enclosing_list; + } else { + enclosing_list = PyList_New(1); + if (enclosing_list == NULL) { + return NULL; + } + double_enclosing_list = PyList_New(1); + if (double_enclosing_list == NULL) { + Py_DECREF(enclosing_list); + return NULL; + } + Py_INCREF(zero_element); + if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { + Py_DECREF(zero_element); + Py_DECREF(enclosing_list); + Py_DECREF(double_enclosing_list); + return NULL; + } + if (PyList_SetItem(double_enclosing_list, 0, enclosing_list) == -1) { + Py_DECREF(zero_element); + Py_DECREF(enclosing_list); + Py_DECREF(double_enclosing_list); + return NULL; + } + return double_enclosing_list; + } + } + else { + zero_element = PyTuple_GetItem(multiparams, 0); + if (PyObject_HasAttrString(zero_element, "__iter__") && + !PyObject_HasAttrString(zero_element, "strip") + ) { + Py_INCREF(multiparams); + return multiparams; + } + else { + enclosing_list = PyList_New(1); + if (enclosing_list == NULL) { + return NULL; + } + Py_INCREF(multiparams); + if (PyList_SetItem(enclosing_list, 0, multiparams) == -1) { + Py_DECREF(multiparams); + Py_DECREF(enclosing_list); + return NULL; + } + return enclosing_list; + } + } +} + +static PyMethodDef module_methods[] = { + {"_distill_params", distill_params, METH_VARARGS, + "Distill an execute() parameter structure."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + MODULE_NAME, + MODULE_DOC, + -1, + module_methods + }; +#endif + + +#if PY_MAJOR_VERSION >= 3 +PyObject * +PyInit_cutils(void) +#else +PyMODINIT_FUNC +initcutils(void) +#endif +{ + PyObject *m; + +#if PY_MAJOR_VERSION >= 3 + m = PyModule_Create(&module_def); +#else + m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); +#endif + +#if PY_MAJOR_VERSION >= 3 + if (m == NULL) + return NULL; + return m; +#else + if (m == NULL) + return; +#endif +} + diff --git a/libs/sqlalchemy/connectors/__init__.py b/libs/sqlalchemy/connectors/__init__.py index a4e017c4..761024fe 100644 --- a/libs/sqlalchemy/connectors/__init__.py +++ b/libs/sqlalchemy/connectors/__init__.py @@ -1,5 +1,5 @@ # connectors/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,4 +7,3 @@ class Connector(object): pass - diff --git a/libs/sqlalchemy/connectors/mxodbc.py b/libs/sqlalchemy/connectors/mxodbc.py index 2848f200..e5562a25 100644 --- a/libs/sqlalchemy/connectors/mxodbc.py +++ b/libs/sqlalchemy/connectors/mxodbc.py @@ -1,5 +1,5 @@ # connectors/mxodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -22,14 +22,15 @@ import sys import re import warnings -from sqlalchemy.connectors import Connector +from . import Connector + class MxODBCConnector(Connector): - driver='mxodbc' + driver = 'mxodbc' supports_sane_multi_rowcount = False - supports_unicode_statements = False - supports_unicode_binds = False + supports_unicode_statements = True + supports_unicode_binds = True supports_native_decimal = True @@ -47,7 +48,7 @@ class MxODBCConnector(Connector): elif platform == 'darwin': from mx.ODBC import iODBC as module else: - raise ImportError, "Unrecognized platform for mxODBC import" + raise ImportError("Unrecognized platform for mxODBC import") return module @classmethod @@ -73,15 +74,15 @@ class MxODBCConnector(Connector): emit Python standard warnings. """ from mx.ODBC.Error import Warning as MxOdbcWarning - def error_handler(connection, cursor, errorclass, errorvalue): + def error_handler(connection, cursor, errorclass, errorvalue): if issubclass(errorclass, MxOdbcWarning): errorclass.__bases__ = (Warning,) warnings.warn(message=str(errorvalue), category=errorclass, stacklevel=2) else: - raise errorclass, errorvalue + raise errorclass(errorvalue) return error_handler def create_connect_args(self, url): @@ -130,21 +131,19 @@ class MxODBCConnector(Connector): version.append(n) return tuple(version) - def do_execute(self, cursor, statement, parameters, context=None): + def _get_direct(self, context): if context: native_odbc_execute = context.execution_options.\ get('native_odbc_execute', 'auto') - if native_odbc_execute is True: - # user specified native_odbc_execute=True - cursor.execute(statement, parameters) - elif native_odbc_execute is False: - # user specified native_odbc_execute=False - cursor.executedirect(statement, parameters) - elif context.is_crud: - # statement is UPDATE, DELETE, INSERT - cursor.execute(statement, parameters) - else: - # all other statements - cursor.executedirect(statement, parameters) + # default to direct=True in all cases, is more generally + # compatible especially with SQL Server + return False if native_odbc_execute is True else True else: - cursor.executedirect(statement, parameters) + return True + + def do_executemany(self, cursor, statement, parameters, context=None): + cursor.executemany( + statement, parameters, direct=self._get_direct(context)) + + def do_execute(self, cursor, statement, parameters, context=None): + cursor.execute(statement, parameters, direct=self._get_direct(context)) diff --git a/libs/sqlalchemy/connectors/mysqldb.py b/libs/sqlalchemy/connectors/mysqldb.py index be1f3530..0f250dfd 100644 --- a/libs/sqlalchemy/connectors/mysqldb.py +++ b/libs/sqlalchemy/connectors/mysqldb.py @@ -1,19 +1,26 @@ +# connectors/mysqldb.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + """Define behaviors common to MySQLdb dialects. Currently includes MySQL and Drizzle. """ -from sqlalchemy.connectors import Connector -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy import exc, log, schema, sql, types as sqltypes, util -from sqlalchemy import processors +from . import Connector +from ..engine import base as engine_base, default +from ..sql import operators as sql_operators +from .. import exc, log, schema, sql, types as sqltypes, util, processors import re + # the subclassing of Connector by all classes # here is not strictly necessary + class MySQLDBExecutionContext(Connector): @property @@ -23,19 +30,23 @@ class MySQLDBExecutionContext(Connector): else: return self.cursor.rowcount + class MySQLDBCompiler(Connector): - def visit_mod(self, binary, **kw): - return self.process(binary.left) + " %% " + self.process(binary.right) + def visit_mod_binary(self, binary, operator, **kw): + return self.process(binary.left, **kw) + " %% " + \ + self.process(binary.right, **kw) def post_process_text(self, text): return text.replace('%', '%%') + class MySQLDBIdentifierPreparer(Connector): def _escape_identifier(self, value): value = value.replace(self.escape_quote, self.escape_to_quote) return value.replace("%", "%%") + class MySQLDBConnector(Connector): driver = 'mysqldb' supports_unicode_statements = False @@ -76,7 +87,8 @@ class MySQLDBConnector(Connector): # query string. ssl = {} - for key in ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']: + keys = ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher'] + for key in keys: if key in opts: ssl[key[4:]] = opts[key] util.coerce_kw_type(ssl, key[4:], str) @@ -148,4 +160,3 @@ class MySQLDBConnector(Connector): "combination of MySQL server and MySQL-python. " "MySQL-python >= 1.2.2 is recommended. Assuming latin1.") return 'latin1' - diff --git a/libs/sqlalchemy/connectors/pyodbc.py b/libs/sqlalchemy/connectors/pyodbc.py index 5be65d2d..284de288 100644 --- a/libs/sqlalchemy/connectors/pyodbc.py +++ b/libs/sqlalchemy/connectors/pyodbc.py @@ -1,23 +1,27 @@ # connectors/pyodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.connectors import Connector -from sqlalchemy.util import asbool +from . import Connector +from .. import util + import sys import re -import urllib + class PyODBCConnector(Connector): - driver='pyodbc' + driver = 'pyodbc' supports_sane_multi_rowcount = False - # PyODBC unicode is broken on UCS-4 builds - supports_unicode = sys.maxunicode == 65535 - supports_unicode_statements = supports_unicode + + if util.py2k: + # PyODBC unicode is broken on UCS-4 builds + supports_unicode = sys.maxunicode == 65535 + supports_unicode_statements = supports_unicode + supports_native_decimal = True default_paramstyle = 'named' @@ -55,15 +59,15 @@ class PyODBCConnector(Connector): connect_args = {} for param in ('ansi', 'unicode_results', 'autocommit'): if param in keys: - connect_args[param] = asbool(keys.pop(param)) + connect_args[param] = util.asbool(keys.pop(param)) if 'odbc_connect' in keys: - connectors = [urllib.unquote_plus(keys.pop('odbc_connect'))] + connectors = [util.unquote_plus(keys.pop('odbc_connect'))] else: dsn_connection = 'dsn' in keys or \ ('host' in keys and 'database' not in keys) if dsn_connection: - connectors= ['dsn=%s' % (keys.pop('host', '') or \ + connectors = ['dsn=%s' % (keys.pop('host', '') or \ keys.pop('dsn', ''))] else: port = '' @@ -73,7 +77,7 @@ class PyODBCConnector(Connector): connectors = ["DRIVER={%s}" % keys.pop('driver', self.pyodbc_driver_name), 'Server=%s%s' % (keys.pop('host', ''), port), - 'Database=%s' % keys.pop('database', '') ] + 'Database=%s' % keys.pop('database', '')] user = keys.pop("user", None) if user: @@ -90,8 +94,8 @@ class PyODBCConnector(Connector): connectors.append("AutoTranslate=%s" % keys.pop("odbc_autotranslate")) - connectors.extend(['%s=%s' % (k,v) for k,v in keys.iteritems()]) - return [[";".join (connectors)], connect_args] + connectors.extend(['%s=%s' % (k, v) for k, v in keys.items()]) + return [[";".join(connectors)], connect_args] def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.ProgrammingError): @@ -117,19 +121,22 @@ class PyODBCConnector(Connector): )) if self.freetds: - self.freetds_driver_version = dbapi_con.getinfo(pyodbc.SQL_DRIVER_VER) + self.freetds_driver_version = dbapi_con.getinfo( + pyodbc.SQL_DRIVER_VER) + + self.supports_unicode_statements = ( + not util.py2k or + (not self.freetds and not self.easysoft) + ) - # the "Py2K only" part here is theoretical. - # have not tried pyodbc + python3.1 yet. - # Py2K - self.supports_unicode_statements = not self.freetds and not self.easysoft if self._user_supports_unicode_binds is not None: self.supports_unicode_binds = self._user_supports_unicode_binds + elif util.py2k: + self.supports_unicode_binds = ( + not self.freetds or self.freetds_driver_version >= '0.91' + ) and not self.easysoft else: - self.supports_unicode_binds = (not self.freetds or - self.freetds_driver_version >= '0.91' - ) and not self.easysoft - # end Py2K + self.supports_unicode_binds = True # run other initialization which asks for user name, etc. super(PyODBCConnector, self).initialize(connection) diff --git a/libs/sqlalchemy/connectors/zxJDBC.py b/libs/sqlalchemy/connectors/zxJDBC.py index e2bfed2e..e0bbc573 100644 --- a/libs/sqlalchemy/connectors/zxJDBC.py +++ b/libs/sqlalchemy/connectors/zxJDBC.py @@ -1,11 +1,12 @@ # connectors/zxJDBC.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php import sys -from sqlalchemy.connectors import Connector +from . import Connector + class ZxJDBCConnector(Connector): driver = 'zxjdbc' diff --git a/libs/sqlalchemy/databases/__init__.py b/libs/sqlalchemy/databases/__init__.py index bb0b370e..915eefa4 100644 --- a/libs/sqlalchemy/databases/__init__.py +++ b/libs/sqlalchemy/databases/__init__.py @@ -1,5 +1,5 @@ # databases/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,26 +8,20 @@ compatibility with pre 0.6 versions. """ -from sqlalchemy.dialects.sqlite import base as sqlite -from sqlalchemy.dialects.postgresql import base as postgresql +from ..dialects.sqlite import base as sqlite +from ..dialects.postgresql import base as postgresql postgres = postgresql -from sqlalchemy.dialects.mysql import base as mysql -from sqlalchemy.dialects.drizzle import base as drizzle -from sqlalchemy.dialects.oracle import base as oracle -from sqlalchemy.dialects.firebird import base as firebird -from sqlalchemy.dialects.maxdb import base as maxdb -from sqlalchemy.dialects.informix import base as informix -from sqlalchemy.dialects.mssql import base as mssql -from sqlalchemy.dialects.access import base as access -from sqlalchemy.dialects.sybase import base as sybase +from ..dialects.mysql import base as mysql +from ..dialects.drizzle import base as drizzle +from ..dialects.oracle import base as oracle +from ..dialects.firebird import base as firebird +from ..dialects.mssql import base as mssql +from ..dialects.sybase import base as sybase __all__ = ( - 'access', 'drizzle', 'firebird', - 'informix', - 'maxdb', 'mssql', 'mysql', 'postgresql', diff --git a/libs/sqlalchemy/dialects/__init__.py b/libs/sqlalchemy/dialects/__init__.py index a427cde4..974d4f78 100644 --- a/libs/sqlalchemy/dialects/__init__.py +++ b/libs/sqlalchemy/dialects/__init__.py @@ -1,15 +1,12 @@ # dialects/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php __all__ = ( -# 'access', 'drizzle', 'firebird', -# 'informix', -# 'maxdb', 'mssql', 'mysql', 'oracle', @@ -17,3 +14,31 @@ __all__ = ( 'sqlite', 'sybase', ) + +from .. import util + +def _auto_fn(name): + """default dialect importer. + + plugs into the :class:`.PluginLoader` + as a first-hit system. + + """ + if "." in name: + dialect, driver = name.split(".") + else: + dialect = name + driver = "base" + try: + module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects + except ImportError: + return None + + module = getattr(module, dialect) + if hasattr(module, driver): + module = getattr(module, driver) + return lambda: module.dialect + else: + return None + +registry = util.PluginLoader("sqlalchemy.dialects", auto_fn=_auto_fn) diff --git a/libs/sqlalchemy/dialects/access/__init__.py b/libs/sqlalchemy/dialects/access/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/sqlalchemy/dialects/access/base.py b/libs/sqlalchemy/dialects/access/base.py deleted file mode 100644 index f107c9c8..00000000 --- a/libs/sqlalchemy/dialects/access/base.py +++ /dev/null @@ -1,451 +0,0 @@ -# access/base.py -# Copyright (C) 2007-2011 the SQLAlchemy authors and contributors -# Copyright (C) 2007 Paul Johnston, paj@pajhome.org.uk -# Portions derived from jet2sql.py by Matt Keranen, mksql@yahoo.com -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -Support for the Microsoft Access database. - -.. note:: - - The Access dialect is **non-functional as of SQLAlchemy 0.6**, - pending development efforts to bring it up-to-date. - - -""" -from sqlalchemy import sql, schema, types, exc, pool -from sqlalchemy.sql import compiler, expression -from sqlalchemy.engine import default, base, reflection -from sqlalchemy import processors - -class AcNumeric(types.Numeric): - def get_col_spec(self): - return "NUMERIC" - - def bind_processor(self, dialect): - return processors.to_str - - def result_processor(self, dialect, coltype): - return None - -class AcFloat(types.Float): - def get_col_spec(self): - return "FLOAT" - - def bind_processor(self, dialect): - """By converting to string, we can use Decimal types round-trip.""" - return processors.to_str - -class AcInteger(types.Integer): - def get_col_spec(self): - return "INTEGER" - -class AcTinyInteger(types.Integer): - def get_col_spec(self): - return "TINYINT" - -class AcSmallInteger(types.SmallInteger): - def get_col_spec(self): - return "SMALLINT" - -class AcDateTime(types.DateTime): - def get_col_spec(self): - return "DATETIME" - -class AcDate(types.Date): - - def get_col_spec(self): - return "DATETIME" - -class AcText(types.Text): - def get_col_spec(self): - return "MEMO" - -class AcString(types.String): - def get_col_spec(self): - return "TEXT" + (self.length and ("(%d)" % self.length) or "") - -class AcUnicode(types.Unicode): - def get_col_spec(self): - return "TEXT" + (self.length and ("(%d)" % self.length) or "") - - def bind_processor(self, dialect): - return None - - def result_processor(self, dialect, coltype): - return None - -class AcChar(types.CHAR): - def get_col_spec(self): - return "TEXT" + (self.length and ("(%d)" % self.length) or "") - -class AcBinary(types.LargeBinary): - def get_col_spec(self): - return "BINARY" - -class AcBoolean(types.Boolean): - def get_col_spec(self): - return "YESNO" - -class AcTimeStamp(types.TIMESTAMP): - def get_col_spec(self): - return "TIMESTAMP" - -class AccessExecutionContext(default.DefaultExecutionContext): - def _has_implicit_sequence(self, column): - if column.primary_key and column.autoincrement: - if isinstance(column.type, types.Integer) and \ - not column.foreign_keys: - if column.default is None or \ - (isinstance(column.default, schema.Sequence) and \ - column.default.optional): - return True - return False - - def post_exec(self): - """If we inserted into a row with a COUNTER column, fetch the ID""" - - if self.compiled.isinsert: - tbl = self.compiled.statement.table - if not hasattr(tbl, 'has_sequence'): - tbl.has_sequence = None - for column in tbl.c: - if getattr(column, 'sequence', False) or \ - self._has_implicit_sequence(column): - tbl.has_sequence = column - break - - if bool(tbl.has_sequence): - # TBD: for some reason _last_inserted_ids doesn't exist here - # (but it does at corresponding point in mssql???) - #if not len(self._last_inserted_ids) or - # self._last_inserted_ids[0] is None: - self.cursor.execute("SELECT @@identity AS lastrowid") - row = self.cursor.fetchone() - self._last_inserted_ids = [int(row[0])] - #+ self._last_inserted_ids[1:] - # print "LAST ROW ID", self._last_inserted_ids - - super(AccessExecutionContext, self).post_exec() - - -const, daoEngine = None, None -class AccessDialect(default.DefaultDialect): - colspecs = { - types.Unicode : AcUnicode, - types.Integer : AcInteger, - types.SmallInteger: AcSmallInteger, - types.Numeric : AcNumeric, - types.Float : AcFloat, - types.DateTime : AcDateTime, - types.Date : AcDate, - types.String : AcString, - types.LargeBinary : AcBinary, - types.Boolean : AcBoolean, - types.Text : AcText, - types.CHAR: AcChar, - types.TIMESTAMP: AcTimeStamp, - } - name = 'access' - supports_sane_rowcount = False - supports_sane_multi_rowcount = False - - ported_sqla_06 = False - - def type_descriptor(self, typeobj): - newobj = types.adapt_type(typeobj, self.colspecs) - return newobj - - def __init__(self, **params): - super(AccessDialect, self).__init__(**params) - self.text_as_varchar = False - self._dtbs = None - - @classmethod - def dbapi(cls): - import win32com.client, pythoncom - - global const, daoEngine - if const is None: - const = win32com.client.constants - for suffix in (".36", ".35", ".30"): - try: - daoEngine = win32com.client.\ - gencache.\ - EnsureDispatch("DAO.DBEngine" + suffix) - break - except pythoncom.com_error: - pass - else: - raise exc.InvalidRequestError( - "Can't find a DB engine. Check " - "http://support.microsoft.com/kb/239114 for details.") - - import pyodbc as module - return module - - def create_connect_args(self, url): - opts = url.translate_connect_args() - connectors = ["Driver={Microsoft Access Driver (*.mdb)}"] - connectors.append("Dbq=%s" % opts["database"]) - user = opts.get("username", None) - if user: - connectors.append("UID=%s" % user) - connectors.append("PWD=%s" % opts.get("password", "")) - return [[";".join(connectors)], {}] - - def last_inserted_ids(self): - return self.context.last_inserted_ids - - def do_execute(self, cursor, statement, params, context=None): - if params == {}: - params = () - super(AccessDialect, self).\ - do_execute(cursor, statement, params, **kwargs) - - def _execute(self, c, statement, parameters): - try: - if parameters == {}: - parameters = () - c.execute(statement, parameters) - self.context.rowcount = c.rowcount - except Exception, e: - raise exc.DBAPIError.instance(statement, parameters, e) - - def has_table(self, connection, tablename, schema=None): - # This approach seems to be more reliable that using DAO - try: - connection.execute('select top 1 * from [%s]' % tablename) - return True - except Exception, e: - return False - - def reflecttable(self, connection, table, include_columns): - # This is defined in the function, as it relies on win32com constants, - # that aren't imported until dbapi method is called - if not hasattr(self, 'ischema_names'): - self.ischema_names = { - const.dbByte: AcBinary, - const.dbInteger: AcInteger, - const.dbLong: AcInteger, - const.dbSingle: AcFloat, - const.dbDouble: AcFloat, - const.dbDate: AcDateTime, - const.dbLongBinary: AcBinary, - const.dbMemo: AcText, - const.dbBoolean: AcBoolean, - const.dbText: AcUnicode, # All Access strings are - # unicode - const.dbCurrency: AcNumeric, - } - - # A fresh DAO connection is opened for each reflection - # This is necessary, so we get the latest updates - dtbs = daoEngine.OpenDatabase(connection.engine.url.database) - - try: - for tbl in dtbs.TableDefs: - if tbl.Name.lower() == table.name.lower(): - break - else: - raise exc.NoSuchTableError(table.name) - - for col in tbl.Fields: - coltype = self.ischema_names[col.Type] - if col.Type == const.dbText: - coltype = coltype(col.Size) - - colargs = \ - { - 'nullable': not(col.Required or - col.Attributes & const.dbAutoIncrField), - } - default = col.DefaultValue - - if col.Attributes & const.dbAutoIncrField: - colargs['default'] = schema.Sequence(col.Name + '_seq') - elif default: - if col.Type == const.dbBoolean: - default = default == 'Yes' and '1' or '0' - colargs['server_default'] = \ - schema.DefaultClause(sql.text(default)) - - table.append_column( - schema.Column(col.Name, coltype, **colargs)) - - # TBD: check constraints - - # Find primary key columns first - for idx in tbl.Indexes: - if idx.Primary: - for col in idx.Fields: - thecol = table.c[col.Name] - table.primary_key.add(thecol) - if isinstance(thecol.type, AcInteger) and \ - not (thecol.default and - isinstance( - thecol.default.arg, - schema.Sequence - )): - thecol.autoincrement = False - - # Then add other indexes - for idx in tbl.Indexes: - if not idx.Primary: - if len(idx.Fields) == 1: - col = table.c[idx.Fields[0].Name] - if not col.primary_key: - col.index = True - col.unique = idx.Unique - else: - pass # TBD: multi-column indexes - - - for fk in dtbs.Relations: - if fk.ForeignTable != table.name: - continue - scols = [c.ForeignName for c in fk.Fields] - rcols = ['%s.%s' % (fk.Table, c.Name) for c in fk.Fields] - table.append_constraint( - schema.ForeignKeyConstraint(scols, rcols,\ - link_to_name=True)) - - finally: - dtbs.Close() - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - # A fresh DAO connection is opened for each reflection - # This is necessary, so we get the latest updates - dtbs = daoEngine.OpenDatabase(connection.engine.url.database) - - names = [t.Name for t in dtbs.TableDefs - if t.Name[:4] != "MSys" and t.Name[:4] != "~TMP"] - dtbs.Close() - return names - - -class AccessCompiler(compiler.SQLCompiler): - extract_map = compiler.SQLCompiler.extract_map.copy() - extract_map.update ({ - 'month': 'm', - 'day': 'd', - 'year': 'yyyy', - 'second': 's', - 'hour': 'h', - 'doy': 'y', - 'minute': 'n', - 'quarter': 'q', - 'dow': 'w', - 'week': 'ww' - }) - - def visit_select_precolumns(self, select): - """Access puts TOP, it's version of LIMIT here """ - s = select.distinct and "DISTINCT " or "" - if select.limit: - s += "TOP %s " % (select.limit) - if select.offset: - raise exc.InvalidRequestError( - 'Access does not support LIMIT with an offset') - return s - - def limit_clause(self, select): - """Limit in access is after the select keyword""" - return "" - - def binary_operator_string(self, binary): - """Access uses "mod" instead of "%" """ - return binary.operator == '%' and 'mod' or binary.operator - - def label_select_column(self, select, column, asfrom): - if isinstance(column, expression.Function): - return column.label() - else: - return super(AccessCompiler, self).\ - label_select_column(select, column, asfrom) - - function_rewrites = {'current_date': 'now', - 'current_timestamp': 'now', - 'length': 'len', - } - def visit_function(self, func): - """Access function names differ from the ANSI SQL names; - rewrite common ones""" - func.name = self.function_rewrites.get(func.name, func.name) - return super(AccessCompiler, self).visit_function(func) - - def for_update_clause(self, select): - """FOR UPDATE is not supported by Access; silently ignore""" - return '' - - # Strip schema - def visit_table(self, table, asfrom=False, **kwargs): - if asfrom: - return self.preparer.quote(table.name, table.quote) - else: - return "" - - def visit_join(self, join, asfrom=False, **kwargs): - return (self.process(join.left, asfrom=True) + \ - (join.isouter and " LEFT OUTER JOIN " or " INNER JOIN ") + \ - self.process(join.right, asfrom=True) + " ON " + \ - self.process(join.onclause)) - - def visit_extract(self, extract, **kw): - field = self.extract_map.get(extract.field, extract.field) - return 'DATEPART("%s", %s)' % \ - (field, self.process(extract.expr, **kw)) - -class AccessDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, **kwargs): - colspec = self.preparer.format_column(column) + " " + \ - column.type.dialect_impl(self.dialect).get_col_spec() - - # install a sequence if we have an implicit IDENTITY column - if (not getattr(column.table, 'has_sequence', False)) and \ - column.primary_key and \ - column.autoincrement and \ - isinstance(column.type, types.Integer) and \ - not column.foreign_keys: - if column.default is None or \ - (isinstance(column.default, schema.Sequence) and - column.default.optional): - column.sequence = schema.Sequence(column.name + '_seq') - - if not column.nullable: - colspec += " NOT NULL" - - if hasattr(column, 'sequence'): - column.table.has_sequence = column - colspec = self.preparer.format_column(column) + " counter" - else: - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - return colspec - - def visit_drop_index(self, drop): - index = drop.element - self.append("\nDROP INDEX [%s].[%s]" % \ - (index.table.name, - self._index_identifier(index.name))) - -class AccessIdentifierPreparer(compiler.IdentifierPreparer): - reserved_words = compiler.RESERVED_WORDS.copy() - reserved_words.update(['value', 'text']) - def __init__(self, dialect): - super(AccessIdentifierPreparer, self).\ - __init__(dialect, initial_quote='[', final_quote=']') - - -dialect = AccessDialect -dialect.poolclass = pool.SingletonThreadPool -dialect.statement_compiler = AccessCompiler -dialect.ddlcompiler = AccessDDLCompiler -dialect.preparer = AccessIdentifierPreparer -dialect.execution_ctx_cls = AccessExecutionContext diff --git a/libs/sqlalchemy/dialects/drizzle/base.py b/libs/sqlalchemy/dialects/drizzle/base.py index 0165a2aa..b5addb42 100644 --- a/libs/sqlalchemy/dialects/drizzle/base.py +++ b/libs/sqlalchemy/dialects/drizzle/base.py @@ -1,12 +1,15 @@ # drizzle/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # Copyright (C) 2010-2011 Monty Taylor # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Drizzle database. +""" + +.. dialect:: drizzle + :name: Drizzle Drizzle is a variant of MySQL. Unlike MySQL, Drizzle's default storage engine is InnoDB (transactions, foreign-keys) rather than MyISAM. For more @@ -16,10 +19,6 @@ the `Drizzle Documentation `_. The SQLAlchemy Drizzle dialect leans heavily on the MySQL dialect, so much of the :doc:`SQLAlchemy MySQL ` documentation is also relevant. -Connecting ----------- - -See the individual driver sections below for details on connecting. """ @@ -183,7 +182,7 @@ class BIGINT(sqltypes.BIGINT): super(BIGINT, self).__init__(**kw) -class _DrizzleTime(mysql_dialect._MSTime): +class TIME(mysql_dialect.TIME): """Drizzle TIME type.""" @@ -317,7 +316,7 @@ class _DrizzleBoolean(sqltypes.Boolean): colspecs = { sqltypes.Numeric: NUMERIC, sqltypes.Float: FLOAT, - sqltypes.Time: _DrizzleTime, + sqltypes.Time: TIME, sqltypes.Enum: ENUM, sqltypes.Boolean: _DrizzleBoolean, } @@ -418,6 +417,7 @@ class DrizzleIdentifierPreparer(mysql_dialect.MySQLIdentifierPreparer): pass +@log.class_logger class DrizzleDialect(mysql_dialect.MySQLDialect): """Details of the Drizzle dialect. @@ -447,16 +447,6 @@ class DrizzleDialect(mysql_dialect.MySQLDialect): conn.autocommit(False) return connect - def do_commit(self, connection): - """Execute a COMMIT.""" - - connection.commit() - - def do_rollback(self, connection): - """Execute a ROLLBACK.""" - - connection.rollback() - @reflection.cache def get_table_names(self, connection, schema=None, **kw): """Return a Unicode SHOW TABLES from a given schema.""" @@ -506,4 +496,3 @@ class DrizzleDialect(mysql_dialect.MySQLDialect): self._backslash_escapes = False -log.class_logger(DrizzleDialect) diff --git a/libs/sqlalchemy/dialects/drizzle/mysqldb.py b/libs/sqlalchemy/dialects/drizzle/mysqldb.py index ce9518a8..7d91cc36 100644 --- a/libs/sqlalchemy/dialects/drizzle/mysqldb.py +++ b/libs/sqlalchemy/dialects/drizzle/mysqldb.py @@ -1,15 +1,10 @@ -"""Support for the Drizzle database via the mysql-python adapter. +""" +.. dialect:: drizzle+mysqldb + :name: MySQL-Python + :dbapi: mysqldb + :connectstring: drizzle+mysqldb://:@[:]/ + :url: http://sourceforge.net/projects/mysql-python -MySQL-Python is available at: - - http://sourceforge.net/projects/mysql-python - -Connecting ------------ - -Connect string format:: - - drizzle+mysqldb://:@[:]/ """ diff --git a/libs/sqlalchemy/dialects/firebird/__init__.py b/libs/sqlalchemy/dialects/firebird/__init__.py index 2a3b756f..094ac3e8 100644 --- a/libs/sqlalchemy/dialects/firebird/__init__.py +++ b/libs/sqlalchemy/dialects/firebird/__init__.py @@ -1,12 +1,12 @@ # firebird/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.dialects.firebird import base, kinterbasdb +from sqlalchemy.dialects.firebird import base, kinterbasdb, fdb -base.dialect = kinterbasdb.dialect +base.dialect = fdb.dialect from sqlalchemy.dialects.firebird.base import \ SMALLINT, BIGINT, FLOAT, FLOAT, DATE, TIME, \ @@ -18,5 +18,3 @@ __all__ = ( 'TEXT', 'NUMERIC', 'FLOAT', 'TIMESTAMP', 'VARCHAR', 'CHAR', 'BLOB', 'dialect' ) - - diff --git a/libs/sqlalchemy/dialects/firebird/base.py b/libs/sqlalchemy/dialects/firebird/base.py index a0bb9c20..b9af6e58 100644 --- a/libs/sqlalchemy/dialects/firebird/base.py +++ b/libs/sqlalchemy/dialects/firebird/base.py @@ -1,16 +1,16 @@ # firebird/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for the Firebird database. -Connectivity is usually supplied via the kinterbasdb_ DBAPI module. +.. dialect:: firebird + :name: Firebird -Dialects -~~~~~~~~ +Firebird Dialects +----------------- Firebird offers two distinct dialects_ (not to be confused with a SQLAlchemy ``Dialect``): @@ -27,7 +27,7 @@ support for dialect 1 is not well tested and probably has incompatibilities. Locking Behavior -~~~~~~~~~~~~~~~~ +---------------- Firebird locks tables aggressively. For this reason, a DROP TABLE may hang until other transactions are released. SQLAlchemy does its best @@ -47,7 +47,7 @@ The above use case can be alleviated by calling ``first()`` on the all remaining cursor/connection resources. RETURNING support -~~~~~~~~~~~~~~~~~ +----------------- Firebird 2.0 supports returning a result set from inserts, and 2.1 extends that to deletes and updates. This is generically exposed by @@ -69,7 +69,7 @@ the SQLAlchemy ``returning()`` method, such as:: """ -import datetime, re +import datetime from sqlalchemy import schema as sa_schema from sqlalchemy import exc, types as sqltypes, sql, util @@ -78,9 +78,8 @@ from sqlalchemy.engine import base, default, reflection from sqlalchemy.sql import compiler -from sqlalchemy.types import (BIGINT, BLOB, BOOLEAN, DATE, - FLOAT, INTEGER, NUMERIC, SMALLINT, - TEXT, TIME, TIMESTAMP) +from sqlalchemy.types import (BIGINT, BLOB, DATE, FLOAT, INTEGER, NUMERIC, + SMALLINT, TEXT, TIME, TIMESTAMP, Integer) RESERVED_WORDS = set([ @@ -126,36 +125,49 @@ RESERVED_WORDS = set([ class _StringType(sqltypes.String): """Base for Firebird string types.""" - def __init__(self, charset = None, **kw): + def __init__(self, charset=None, **kw): self.charset = charset super(_StringType, self).__init__(**kw) + class VARCHAR(_StringType, sqltypes.VARCHAR): """Firebird VARCHAR type""" __visit_name__ = 'VARCHAR' - def __init__(self, length = None, **kwargs): + def __init__(self, length=None, **kwargs): super(VARCHAR, self).__init__(length=length, **kwargs) + class CHAR(_StringType, sqltypes.CHAR): """Firebird CHAR type""" __visit_name__ = 'CHAR' - def __init__(self, length = None, **kwargs): + def __init__(self, length=None, **kwargs): super(CHAR, self).__init__(length=length, **kwargs) + +class _FBDateTime(sqltypes.DateTime): + def bind_processor(self, dialect): + def process(value): + if type(value) == datetime.date: + return datetime.datetime(value.year, value.month, value.day) + else: + return value + return process + colspecs = { + sqltypes.DateTime: _FBDateTime } ischema_names = { 'SHORT': SMALLINT, - 'LONG': BIGINT, + 'LONG': INTEGER, 'QUAD': FLOAT, 'FLOAT': FLOAT, 'DATE': DATE, 'TIME': TIME, 'TEXT': TEXT, - 'INT64': NUMERIC, + 'INT64': BIGINT, 'DOUBLE': FLOAT, 'TIMESTAMP': TIMESTAMP, 'VARYING': VARCHAR, @@ -192,20 +204,42 @@ class FBTypeCompiler(compiler.GenericTypeCompiler): return self._extend_string(type_, basic) def visit_VARCHAR(self, type_): + if not type_.length: + raise exc.CompileError( + "VARCHAR requires a length on dialect %s" % + self.dialect.name) basic = super(FBTypeCompiler, self).visit_VARCHAR(type_) return self._extend_string(type_, basic) - class FBCompiler(sql.compiler.SQLCompiler): """Firebird specific idiosyncrasies""" - def visit_mod(self, binary, **kw): - # Firebird lacks a builtin modulo operator, but there is - # an equivalent function in the ib_udf library. + ansi_bind_rules = True + + #def visit_contains_op_binary(self, binary, operator, **kw): + # cant use CONTAINING b.c. it's case insensitive. + + #def visit_notcontains_op_binary(self, binary, operator, **kw): + # cant use NOT CONTAINING b.c. it's case insensitive. + + def visit_now_func(self, fn, **kw): + return "CURRENT_TIMESTAMP" + + def visit_startswith_op_binary(self, binary, operator, **kw): + return '%s STARTING WITH %s' % ( + binary.left._compiler_dispatch(self, **kw), + binary.right._compiler_dispatch(self, **kw)) + + def visit_notstartswith_op_binary(self, binary, operator, **kw): + return '%s NOT STARTING WITH %s' % ( + binary.left._compiler_dispatch(self, **kw), + binary.right._compiler_dispatch(self, **kw)) + + def visit_mod_binary(self, binary, operator, **kw): return "mod(%s, %s)" % ( - self.process(binary.left), - self.process(binary.right)) + self.process(binary.left, **kw), + self.process(binary.right, **kw)) def visit_alias(self, alias, asfrom=False, **kwargs): if self.dialect._version_two: @@ -249,7 +283,7 @@ class FBCompiler(sql.compiler.SQLCompiler): # may require parens - see similar example in the oracle # dialect if func.clauses is not None and len(func.clauses): - return self.process(func.clause_expr) + return self.process(func.clause_expr, **kw) else: return "" @@ -267,9 +301,9 @@ class FBCompiler(sql.compiler.SQLCompiler): result = "" if select._limit: - result += "FIRST %s " % self.process(sql.literal(select._limit)) + result += "FIRST %s " % self.process(sql.literal(select._limit)) if select._offset: - result +="SKIP %s " % self.process(sql.literal(select._offset)) + result += "SKIP %s " % self.process(sql.literal(select._offset)) if select._distinct: result += "DISTINCT " return result @@ -280,15 +314,11 @@ class FBCompiler(sql.compiler.SQLCompiler): return "" def returning_clause(self, stmt, returning_cols): - columns = [ - self.process( - self.label_select_column(None, c, asfrom=False), - within_columns_clause=True, - result_map=self.result_map - ) + self._label_select_column(None, c, True, False, {}) for c in expression._select_iterables(returning_cols) ] + return 'RETURNING ' + ', '.join(columns) @@ -329,6 +359,7 @@ class FBIdentifierPreparer(sql.compiler.IdentifierPreparer): """Install Firebird specific reserved words.""" reserved_words = RESERVED_WORDS + illegal_initial_characters = compiler.ILLEGAL_INITIAL_CHARACTERS.union(['_']) def __init__(self, dialect): super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True) @@ -444,18 +475,34 @@ class FBDialect(default.DefaultDialect): @reflection.cache def get_table_names(self, connection, schema=None, **kw): + # there are two queries commonly mentioned for this. + # this one, using view_blr, is at the Firebird FAQ among other places: + # http://www.firebirdfaq.org/faq174/ s = """ - SELECT DISTINCT rdb$relation_name - FROM rdb$relation_fields - WHERE rdb$system_flag=0 AND rdb$view_context IS NULL + select rdb$relation_name + from rdb$relations + where rdb$view_blr is null + and (rdb$system_flag is null or rdb$system_flag = 0); """ + + # the other query is this one. It's not clear if there's really + # any difference between these two. This link: + # http://www.alberton.info/firebird_sql_meta_info.html#.Ur3vXfZGni8 + # states them as interchangeable. Some discussion at [ticket:2898] + # SELECT DISTINCT rdb$relation_name + # FROM rdb$relation_fields + # WHERE rdb$system_flag=0 AND rdb$view_context IS NULL + return [self.normalize_name(row[0]) for row in connection.execute(s)] @reflection.cache def get_view_names(self, connection, schema=None, **kw): + # see http://www.firebirdfaq.org/faq174/ s = """ - SELECT distinct rdb$view_name - FROM rdb$view_relations + select rdb$relation_name + from rdb$relations + where rdb$view_blr is not null + and (rdb$system_flag is null or rdb$system_flag = 0); """ return [self.normalize_name(row[0]) for row in connection.execute(s)] @@ -474,7 +521,7 @@ class FBDialect(default.DefaultDialect): return None @reflection.cache - def get_primary_keys(self, connection, table_name, schema=None, **kw): + def get_pk_constraint(self, connection, table_name, schema=None, **kw): # Query to extract the PK/FK constrained fields of the given table keyqry = """ SELECT se.rdb$field_name AS fname @@ -486,7 +533,7 @@ class FBDialect(default.DefaultDialect): # get primary key fields c = connection.execute(keyqry, ["PRIMARY KEY", tablename]) pkfields = [self.normalize_name(r['fname']) for r in c.fetchall()] - return pkfields + return {'constrained_columns': pkfields, 'name': None} @reflection.cache def get_column_sequence(self, connection, @@ -541,7 +588,8 @@ class FBDialect(default.DefaultDialect): ORDER BY r.rdb$field_position """ # get the PK, used to determine the eventual associated sequence - pkey_cols = self.get_primary_keys(connection, table_name) + pk_constraint = self.get_pk_constraint(connection, table_name) + pkey_cols = pk_constraint['constrained_columns'] tablename = self.denormalize_name(table_name) # get all of the fields for this table @@ -561,8 +609,8 @@ class FBDialect(default.DefaultDialect): util.warn("Did not recognize type '%s' of column '%s'" % (colspec, name)) coltype = sqltypes.NULLTYPE - elif colspec == 'INT64': - coltype = coltype( + elif issubclass(coltype, Integer) and row['fprec'] != 0: + coltype = NUMERIC( precision=row['fprec'], scale=row['fscale'] * -1) elif colspec in ('VARYING', 'CSTRING'): @@ -593,11 +641,11 @@ class FBDialect(default.DefaultDialect): # Redundant defvalue = None col_d = { - 'name' : name, - 'type' : coltype, - 'nullable' : not bool(row['null_flag']), - 'default' : defvalue, - 'autoincrement':defvalue is None + 'name': name, + 'type': coltype, + 'nullable': not bool(row['null_flag']), + 'default': defvalue, + 'autoincrement': defvalue is None } if orig_colname.lower() == orig_colname: @@ -605,7 +653,7 @@ class FBDialect(default.DefaultDialect): # if the PK is a single field, try to see if its linked to # a sequence thru a trigger - if len(pkey_cols)==1 and name==pkey_cols[0]: + if len(pkey_cols) == 1 and name == pkey_cols[0]: seq_d = self.get_column_sequence(connection, tablename, name) if seq_d is not None: col_d['sequence'] = seq_d @@ -635,12 +683,12 @@ class FBDialect(default.DefaultDialect): tablename = self.denormalize_name(table_name) c = connection.execute(fkqry, ["FOREIGN KEY", tablename]) - fks = util.defaultdict(lambda:{ - 'name' : None, - 'constrained_columns' : [], - 'referred_schema' : None, - 'referred_table' : None, - 'referred_columns' : [] + fks = util.defaultdict(lambda: { + 'name': None, + 'constrained_columns': [], + 'referred_schema': None, + 'referred_table': None, + 'referred_columns': [] }) for row in c: @@ -653,7 +701,7 @@ class FBDialect(default.DefaultDialect): self.normalize_name(row['fname'])) fk['referred_columns'].append( self.normalize_name(row['targetfname'])) - return fks.values() + return list(fks.values()) @reflection.cache def get_indexes(self, connection, table_name, schema=None, **kw): @@ -669,7 +717,7 @@ class FBDialect(default.DefaultDialect): ic.rdb$index_name WHERE ix.rdb$relation_name=? AND ix.rdb$foreign_key IS NULL AND rdb$relation_constraints.rdb$constraint_type IS NULL - ORDER BY index_name, field_name + ORDER BY index_name, ic.rdb$field_position """ c = connection.execute(qry, [self.denormalize_name(table_name)]) @@ -684,17 +732,5 @@ class FBDialect(default.DefaultDialect): indexrec['column_names'].append( self.normalize_name(row['field_name'])) - return indexes.values() + return list(indexes.values()) - def do_execute(self, cursor, statement, parameters, context=None): - # kinterbase does not accept a None, but wants an empty list - # when there are no arguments. - cursor.execute(statement, parameters or []) - - def do_rollback(self, connection): - # Use the retaining feature, that keeps the transaction going - connection.rollback(True) - - def do_commit(self, connection): - # Use the retaining feature, that keeps the transaction going - connection.commit(True) diff --git a/libs/sqlalchemy/dialects/firebird/fdb.py b/libs/sqlalchemy/dialects/firebird/fdb.py new file mode 100644 index 00000000..4d94ef0d --- /dev/null +++ b/libs/sqlalchemy/dialects/firebird/fdb.py @@ -0,0 +1,115 @@ +# firebird/fdb.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +""" +.. dialect:: firebird+fdb + :name: fdb + :dbapi: pyodbc + :connectstring: firebird+fdb://user:password@host:port/path/to/db[?key=value&key=value...] + :url: http://pypi.python.org/pypi/fdb/ + + fdb is a kinterbasdb compatible DBAPI for Firebird. + + .. versionadded:: 0.8 - Support for the fdb Firebird driver. + + .. versionchanged:: 0.9 - The fdb dialect is now the default dialect + under the ``firebird://`` URL space, as ``fdb`` is now the official + Python driver for Firebird. + +Arguments +---------- + +The ``fdb`` dialect is based on the :mod:`sqlalchemy.dialects.firebird.kinterbasdb` +dialect, however does not accept every argument that Kinterbasdb does. + +* ``enable_rowcount`` - True by default, setting this to False disables + the usage of "cursor.rowcount" with the + Kinterbasdb dialect, which SQLAlchemy ordinarily calls upon automatically + after any UPDATE or DELETE statement. When disabled, SQLAlchemy's + ResultProxy will return -1 for result.rowcount. The rationale here is + that Kinterbasdb requires a second round trip to the database when + .rowcount is called - since SQLA's resultproxy automatically closes + the cursor after a non-result-returning statement, rowcount must be + called, if at all, before the result object is returned. Additionally, + cursor.rowcount may not return correct results with older versions + of Firebird, and setting this flag to False will also cause the + SQLAlchemy ORM to ignore its usage. The behavior can also be controlled on a + per-execution basis using the ``enable_rowcount`` option with + :meth:`.Connection.execution_options`:: + + conn = engine.connect().execution_options(enable_rowcount=True) + r = conn.execute(stmt) + print r.rowcount + +* ``retaining`` - False by default. Setting this to True will pass the + ``retaining=True`` keyword argument to the ``.commit()`` and ``.rollback()`` + methods of the DBAPI connection, which can improve performance in some + situations, but apparently with significant caveats. + Please read the fdb and/or kinterbasdb DBAPI documentation in order to + understand the implications of this flag. + + .. versionadded:: 0.8.2 - ``retaining`` keyword argument specifying + transaction retaining behavior - in 0.8 it defaults to ``True`` + for backwards compatibility. + + .. versionchanged:: 0.9.0 - the ``retaining`` flag defaults to ``False``. + In 0.8 it defaulted to ``True``. + + .. seealso:: + + http://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - information + on the "retaining" flag. + +""" + +from .kinterbasdb import FBDialect_kinterbasdb +from ... import util + + +class FBDialect_fdb(FBDialect_kinterbasdb): + + def __init__(self, enable_rowcount=True, + retaining=False, **kwargs): + super(FBDialect_fdb, self).__init__( + enable_rowcount=enable_rowcount, + retaining=retaining, **kwargs) + + @classmethod + def dbapi(cls): + return __import__('fdb') + + def create_connect_args(self, url): + opts = url.translate_connect_args(username='user') + if opts.get('port'): + opts['host'] = "%s/%s" % (opts['host'], opts['port']) + del opts['port'] + opts.update(url.query) + + util.coerce_kw_type(opts, 'type_conv', int) + + return ([], opts) + + def _get_server_version_info(self, connection): + """Get the version of the Firebird server used by a connection. + + Returns a tuple of (`major`, `minor`, `build`), three integers + representing the version of the attached server. + """ + + # This is the simpler approach (the other uses the services api), + # that for backward compatibility reasons returns a string like + # LI-V6.3.3.12981 Firebird 2.0 + # where the first version is a fake one resembling the old + # Interbase signature. + + isc_info_firebird_version = 103 + fbconn = connection.connection + + version = fbconn.db_info(isc_info_firebird_version) + + return self._parse_version_info(version) + +dialect = FBDialect_fdb diff --git a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py b/libs/sqlalchemy/dialects/firebird/kinterbasdb.py index ddca91db..b8a83a07 100644 --- a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py +++ b/libs/sqlalchemy/dialects/firebird/kinterbasdb.py @@ -1,58 +1,48 @@ # firebird/kinterbasdb.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -The most common way to connect to a Firebird engine is implemented by -kinterbasdb__, currently maintained__ directly by the Firebird people. +.. dialect:: firebird+kinterbasdb + :name: kinterbasdb + :dbapi: kinterbasdb + :connectstring: firebird+kinterbasdb://user:password@host:port/path/to/db[?key=value&key=value...] + :url: http://firebirdsql.org/index.php?op=devel&sub=python -The connection URL is of the form -``firebird[+kinterbasdb]://user:password@host:port/path/to/db[?key=value&key=value...]``. +Arguments +---------- -Kinterbasedb backend specific keyword arguments are: +The Kinterbasdb backend accepts the ``enable_rowcount`` and ``retaining`` +arguments accepted by the :mod:`sqlalchemy.dialects.firebird.fdb` dialect. In addition, it +also accepts the following: -* type_conv - select the kind of mapping done on the types: by default - SQLAlchemy uses 200 with Unicode, datetime and decimal support (see - details__). +* ``type_conv`` - select the kind of mapping done on the types: by default + SQLAlchemy uses 200 with Unicode, datetime and decimal support. See + the linked documents below for further information. -* concurrency_level - set the backend policy with regards to threading - issues: by default SQLAlchemy uses policy 1 (see details__). +* ``concurrency_level`` - set the backend policy with regards to threading + issues: by default SQLAlchemy uses policy 1. See the linked documents + below for futher information. -* enable_rowcount - True by default, setting this to False disables - the usage of "cursor.rowcount" with the - Kinterbasdb dialect, which SQLAlchemy ordinarily calls upon automatically - after any UPDATE or DELETE statement. When disabled, SQLAlchemy's - ResultProxy will return -1 for result.rowcount. The rationale here is - that Kinterbasdb requires a second round trip to the database when - .rowcount is called - since SQLA's resultproxy automatically closes - the cursor after a non-result-returning statement, rowcount must be - called, if at all, before the result object is returned. Additionally, - cursor.rowcount may not return correct results with older versions - of Firebird, and setting this flag to False will also cause the - SQLAlchemy ORM to ignore its usage. The behavior can also be controlled on a - per-execution basis using the `enable_rowcount` option with - :meth:`execution_options()`:: +.. seealso:: - conn = engine.connect().execution_options(enable_rowcount=True) - r = conn.execute(stmt) - print r.rowcount + http://sourceforge.net/projects/kinterbasdb + + http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_param_conv_dynamic_type_translation + + http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurrency -__ http://sourceforge.net/projects/kinterbasdb -__ http://firebirdsql.org/index.php?op=devel&sub=python -__ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_param_conv_dynamic_type_translation -__ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurrency """ -from sqlalchemy.dialects.firebird.base import FBDialect, \ - FBCompiler, FBExecutionContext -from sqlalchemy import util, types as sqltypes -from sqlalchemy.util.compat import decimal +from .base import FBDialect, FBExecutionContext +from ... import util, types as sqltypes from re import match +import decimal -class _FBNumeric_kinterbasdb(sqltypes.Numeric): +class _kinterbasdb_numeric(object): def bind_processor(self, dialect): def process(value): if isinstance(value, decimal.Decimal): @@ -61,6 +51,13 @@ class _FBNumeric_kinterbasdb(sqltypes.Numeric): return value return process +class _FBNumeric_kinterbasdb(_kinterbasdb_numeric, sqltypes.Numeric): + pass + +class _FBFloat_kinterbasdb(_kinterbasdb_numeric, sqltypes.Float): + pass + + class FBExecutionContext_kinterbasdb(FBExecutionContext): @property def rowcount(self): @@ -70,6 +67,7 @@ class FBExecutionContext_kinterbasdb(FBExecutionContext): else: return -1 + class FBDialect_kinterbasdb(FBDialect): driver = 'kinterbasdb' supports_sane_rowcount = False @@ -81,24 +79,37 @@ class FBDialect_kinterbasdb(FBDialect): colspecs = util.update_copy( FBDialect.colspecs, { - sqltypes.Numeric:_FBNumeric_kinterbasdb, + sqltypes.Numeric: _FBNumeric_kinterbasdb, + sqltypes.Float: _FBFloat_kinterbasdb, } ) def __init__(self, type_conv=200, concurrency_level=1, - enable_rowcount=True, **kwargs): + enable_rowcount=True, + retaining=False, **kwargs): super(FBDialect_kinterbasdb, self).__init__(**kwargs) self.enable_rowcount = enable_rowcount self.type_conv = type_conv self.concurrency_level = concurrency_level + self.retaining = retaining if enable_rowcount: self.supports_sane_rowcount = True @classmethod def dbapi(cls): - k = __import__('kinterbasdb') - return k + return __import__('kinterbasdb') + + def do_execute(self, cursor, statement, parameters, context=None): + # kinterbase does not accept a None, but wants an empty list + # when there are no arguments. + cursor.execute(statement, parameters or []) + + def do_rollback(self, dbapi_connection): + dbapi_connection.rollback(self.retaining) + + def do_commit(self, dbapi_connection): + dbapi_connection.commit(self.retaining) def create_connect_args(self, url): opts = url.translate_connect_args(username='user') @@ -117,7 +128,8 @@ class FBDialect_kinterbasdb(FBDialect): initialized = getattr(self.dbapi, 'initialized', None) if initialized is None: # CVS rev 1.96 changed the name of the attribute: - # http://kinterbasdb.cvs.sourceforge.net/viewvc/kinterbasdb/Kinterbasdb-3.0/__init__.py?r1=1.95&r2=1.96 + # http://kinterbasdb.cvs.sourceforge.net/viewvc/kinterbasdb/ + # Kinterbasdb-3.0/__init__.py?r1=1.95&r2=1.96 initialized = getattr(self.dbapi, '_initialized', False) if not initialized: self.dbapi.init(type_conv=type_conv, diff --git a/libs/sqlalchemy/dialects/informix/__init__.py b/libs/sqlalchemy/dialects/informix/__init__.py deleted file mode 100644 index bd633da5..00000000 --- a/libs/sqlalchemy/dialects/informix/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# informix/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.informix import base, informixdb - -base.dialect = informixdb.dialect \ No newline at end of file diff --git a/libs/sqlalchemy/dialects/informix/base.py b/libs/sqlalchemy/dialects/informix/base.py deleted file mode 100644 index 07561f8d..00000000 --- a/libs/sqlalchemy/dialects/informix/base.py +++ /dev/null @@ -1,596 +0,0 @@ -# informix/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# coding: gbk -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Support for the Informix database. - -.. note:: - - The Informix dialect functions on current SQLAlchemy versions - but is not regularly tested, and may have many issues and - caveats not currently handled. - -""" - - -import datetime - -from sqlalchemy import sql, schema, exc, pool, util -from sqlalchemy.sql import compiler, text -from sqlalchemy.engine import default, reflection -from sqlalchemy import types as sqltypes - -RESERVED_WORDS = set( - ["abs", "absolute", "access", "access_method", "acos", "active", "add", - "address", "add_months", "admin", "after", "aggregate", "alignment", - "all", "allocate", "all_rows", "alter", "and", "ansi", "any", "append", - "array", "as", "asc", "ascii", "asin", "at", "atan", "atan2", "attach", - "attributes", "audit", "authentication", "authid", "authorization", - "authorized", "auto", "autofree", "auto_reprepare", "auto_stat_mode", - "avg", "avoid_execute", "avoid_fact", "avoid_full", "avoid_hash", - "avoid_index", "avoid_index_sj", "avoid_multi_index", "avoid_nl", - "avoid_star_join", "avoid_subqf", "based", "before", "begin", - "between", "bigint", "bigserial", "binary", "bitand", "bitandnot", - "bitnot", "bitor", "bitxor", "blob", "blobdir", "boolean", "both", - "bound_impl_pdq", "buffered", "builtin", "by", "byte", "cache", "call", - "cannothash", "cardinality", "cascade", "case", "cast", "ceil", "char", - "character", "character_length", "char_length", "check", "class", - "class_origin", "client", "clob", "clobdir", "close", "cluster", - "clustersize", "cobol", "codeset", "collation", "collection", - "column", "columns", "commit", "committed", "commutator", "component", - "components", "concat", "concurrent", "connect", "connection", - "connection_name", "connect_by_iscycle", "connect_by_isleaf", - "connect_by_rootconst", "constraint", "constraints", "constructor", - "context", "continue", "copy", "cos", "costfunc", "count", "crcols", - "create", "cross", "current", "current_role", "currval", "cursor", - "cycle", "database", "datafiles", "dataskip", "date", "datetime", - "day", "dba", "dbdate", "dbinfo", "dbpassword", "dbsecadm", - "dbservername", "deallocate", "debug", "debugmode", "debug_env", "dec", - "decimal", "declare", "decode", "decrypt_binary", "decrypt_char", - "dec_t", "default", "default_role", "deferred", "deferred_prepare", - "define", "delay", "delete", "deleting", "delimited", "delimiter", - "deluxe", "desc", "describe", "descriptor", "detach", "diagnostics", - "directives", "dirty", "disable", "disabled", "disconnect", "disk", - "distinct", "distributebinary", "distributesreferences", - "distributions", "document", "domain", "donotdistribute", "dormant", - "double", "drop", "dtime_t", "each", "elif", "else", "enabled", - "encryption", "encrypt_aes", "encrypt_tdes", "end", "enum", - "environment", "error", "escape", "exception", "exclusive", "exec", - "execute", "executeanywhere", "exemption", "exists", "exit", "exp", - "explain", "explicit", "express", "expression", "extdirectives", - "extend", "extent", "external", "fact", "false", "far", "fetch", - "file", "filetoblob", "filetoclob", "fillfactor", "filtering", "first", - "first_rows", "fixchar", "fixed", "float", "floor", "flush", "for", - "force", "forced", "force_ddl_exec", "foreach", "foreign", "format", - "format_units", "fortran", "found", "fraction", "fragment", - "fragments", "free", "from", "full", "function", "general", "get", - "gethint", "global", "go", "goto", "grant", "greaterthan", - "greaterthanorequal", "group", "handlesnulls", "hash", "having", "hdr", - "hex", "high", "hint", "hold", "home", "hour", "idslbacreadarray", - "idslbacreadset", "idslbacreadtree", "idslbacrules", - "idslbacwritearray", "idslbacwriteset", "idslbacwritetree", - "idssecuritylabel", "if", "ifx_auto_reprepare", "ifx_batchedread_table", - "ifx_int8_t", "ifx_lo_create_spec_t", "ifx_lo_stat_t", "immediate", - "implicit", "implicit_pdq", "in", "inactive", "increment", "index", - "indexes", "index_all", "index_sj", "indicator", "informix", "init", - "initcap", "inline", "inner", "inout", "insert", "inserting", "instead", - "int", "int8", "integ", "integer", "internal", "internallength", - "interval", "into", "intrvl_t", "is", "iscanonical", "isolation", - "item", "iterator", "java", "join", "keep", "key", "label", "labeleq", - "labelge", "labelglb", "labelgt", "labelle", "labellt", "labellub", - "labeltostring", "language", "last", "last_day", "leading", "left", - "length", "lessthan", "lessthanorequal", "let", "level", "like", - "limit", "list", "listing", "load", "local", "locator", "lock", "locks", - "locopy", "loc_t", "log", "log10", "logn", "long", "loop", "lotofile", - "low", "lower", "lpad", "ltrim", "lvarchar", "matched", "matches", - "max", "maxerrors", "maxlen", "maxvalue", "mdy", "median", "medium", - "memory", "memory_resident", "merge", "message_length", "message_text", - "middle", "min", "minute", "minvalue", "mod", "mode", "moderate", - "modify", "module", "money", "month", "months_between", "mounting", - "multiset", "multi_index", "name", "nchar", "negator", "new", "next", - "nextval", "next_day", "no", "nocache", "nocycle", "nomaxvalue", - "nomigrate", "nominvalue", "none", "non_dim", "non_resident", "noorder", - "normal", "not", "notemplatearg", "notequal", "null", "nullif", - "numeric", "numrows", "numtodsinterval", "numtoyminterval", "nvarchar", - "nvl", "octet_length", "of", "off", "old", "on", "online", "only", - "opaque", "opclass", "open", "optcompind", "optical", "optimization", - "option", "or", "order", "ordered", "out", "outer", "output", - "override", "page", "parallelizable", "parameter", "partition", - "pascal", "passedbyvalue", "password", "pdqpriority", "percaltl_cos", - "pipe", "pli", "pload", "policy", "pow", "power", "precision", - "prepare", "previous", "primary", "prior", "private", "privileges", - "procedure", "properties", "public", "put", "raise", "range", "raw", - "read", "real", "recordend", "references", "referencing", "register", - "rejectfile", "relative", "release", "remainder", "rename", - "reoptimization", "repeatable", "replace", "replication", "reserve", - "resolution", "resource", "restart", "restrict", "resume", "retain", - "retainupdatelocks", "return", "returned_sqlstate", "returning", - "returns", "reuse", "revoke", "right", "robin", "role", "rollback", - "rollforward", "root", "round", "routine", "row", "rowid", "rowids", - "rows", "row_count", "rpad", "rtrim", "rule", "sameas", "samples", - "sampling", "save", "savepoint", "schema", "scroll", "seclabel_by_comp", - "seclabel_by_name", "seclabel_to_char", "second", "secondary", - "section", "secured", "security", "selconst", "select", "selecting", - "selfunc", "selfuncargs", "sequence", "serial", "serial8", - "serializable", "serveruuid", "server_name", "session", "set", - "setsessionauth", "share", "short", "siblings", "signed", "sin", - "sitename", "size", "skall", "skinhibit", "skip", "skshow", - "smallfloat", "smallint", "some", "specific", "sql", "sqlcode", - "sqlcontext", "sqlerror", "sqlstate", "sqlwarning", "sqrt", - "stability", "stack", "standard", "start", "star_join", "statchange", - "statement", "static", "statistics", "statlevel", "status", "stdev", - "step", "stop", "storage", "store", "strategies", "string", - "stringtolabel", "struct", "style", "subclass_origin", "substr", - "substring", "sum", "support", "sync", "synonym", "sysdate", - "sysdbclose", "sysdbopen", "system", "sys_connect_by_path", "table", - "tables", "tan", "task", "temp", "template", "test", "text", "then", - "time", "timeout", "to", "today", "to_char", "to_date", - "to_dsinterval", "to_number", "to_yminterval", "trace", "trailing", - "transaction", "transition", "tree", "trigger", "triggers", "trim", - "true", "trunc", "truncate", "trusted", "type", "typedef", "typeid", - "typename", "typeof", "uid", "uncommitted", "under", "union", - "unique", "units", "unknown", "unload", "unlock", "unsigned", - "update", "updating", "upon", "upper", "usage", "use", - "uselastcommitted", "user", "use_hash", "use_nl", "use_subqf", - "using", "value", "values", "var", "varchar", "variable", "variance", - "variant", "varying", "vercols", "view", "violations", "void", - "volatile", "wait", "warning", "weekday", "when", "whenever", "where", - "while", "with", "without", "work", "write", "writedown", "writeup", - "xadatasource", "xid", "xload", "xunload", "year" - ]) - -class InfoDateTime(sqltypes.DateTime): - def bind_processor(self, dialect): - def process(value): - if value is not None: - if value.microsecond: - value = value.replace(microsecond=0) - return value - return process - -class InfoTime(sqltypes.Time): - def bind_processor(self, dialect): - def process(value): - if value is not None: - if value.microsecond: - value = value.replace(microsecond=0) - return value - return process - - def result_processor(self, dialect, coltype): - def process(value): - if isinstance(value, datetime.datetime): - return value.time() - else: - return value - return process - -colspecs = { - sqltypes.DateTime : InfoDateTime, - sqltypes.TIMESTAMP: InfoDateTime, - sqltypes.Time: InfoTime, -} - - -ischema_names = { - 0 : sqltypes.CHAR, # CHAR - 1 : sqltypes.SMALLINT, # SMALLINT - 2 : sqltypes.INTEGER, # INT - 3 : sqltypes.FLOAT, # Float - 3 : sqltypes.Float, # SmallFloat - 5 : sqltypes.DECIMAL, # DECIMAL - 6 : sqltypes.Integer, # Serial - 7 : sqltypes.DATE, # DATE - 8 : sqltypes.Numeric, # MONEY - 10 : sqltypes.DATETIME, # DATETIME - 11 : sqltypes.LargeBinary, # BYTE - 12 : sqltypes.TEXT, # TEXT - 13 : sqltypes.VARCHAR, # VARCHAR - 15 : sqltypes.NCHAR, # NCHAR - 16 : sqltypes.NVARCHAR, # NVARCHAR - 17 : sqltypes.Integer, # INT8 - 18 : sqltypes.Integer, # Serial8 - 43 : sqltypes.String, # LVARCHAR - -1 : sqltypes.BLOB, # BLOB - -1 : sqltypes.CLOB, # CLOB -} - - -class InfoTypeCompiler(compiler.GenericTypeCompiler): - def visit_DATETIME(self, type_): - return "DATETIME YEAR TO SECOND" - - def visit_TIME(self, type_): - return "DATETIME HOUR TO SECOND" - - def visit_TIMESTAMP(self, type_): - return "DATETIME YEAR TO SECOND" - - def visit_large_binary(self, type_): - return "BYTE" - - def visit_boolean(self, type_): - return "SMALLINT" - -class InfoSQLCompiler(compiler.SQLCompiler): - def default_from(self): - return " from systables where tabname = 'systables' " - - def get_select_precolumns(self, select): - s = "" - if select._offset: - s += "SKIP %s " % select._offset - if select._limit: - s += "FIRST %s " % select._limit - s += select._distinct and "DISTINCT " or "" - return s - - def visit_select(self, select, asfrom=False, parens=True, **kw): - text = compiler.SQLCompiler.visit_select(self, select, asfrom, parens, **kw) - if asfrom and parens and self.dialect.server_version_info < (11,): - #assuming that 11 version doesn't need this, not tested - return "table(multiset" + text + ")" - else: - return text - - def limit_clause(self, select): - return "" - - def visit_function(self, func, **kw): - if func.name.lower() == 'current_date': - return "today" - elif func.name.lower() == 'current_time': - return "CURRENT HOUR TO SECOND" - elif func.name.lower() in ('current_timestamp', 'now'): - return "CURRENT YEAR TO SECOND" - else: - return compiler.SQLCompiler.visit_function(self, func, **kw) - - def visit_mod(self, binary, **kw): - return "MOD(%s, %s)" % (self.process(binary.left), self.process(binary.right)) - - -class InfoDDLCompiler(compiler.DDLCompiler): - - def visit_add_constraint(self, create): - preparer = self.preparer - return "ALTER TABLE %s ADD CONSTRAINT %s" % ( - self.preparer.format_table(create.element.table), - self.process(create.element) - ) - - def get_column_specification(self, column, **kw): - colspec = self.preparer.format_column(column) - first = None - if column.primary_key and column.autoincrement: - try: - first = [c for c in column.table.primary_key.columns - if (c.autoincrement and - isinstance(c.type, sqltypes.Integer) and - not c.foreign_keys)].pop(0) - except IndexError: - pass - - if column is first: - colspec += " SERIAL" - else: - colspec += " " + self.dialect.type_compiler.process(column.type) - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - if not column.nullable: - colspec += " NOT NULL" - - return colspec - - def get_column_default_string(self, column): - if (isinstance(column.server_default, schema.DefaultClause) and - isinstance(column.server_default.arg, basestring)): - if isinstance(column.type, (sqltypes.Integer, sqltypes.Numeric)): - return self.sql_compiler.process(text(column.server_default.arg)) - - return super(InfoDDLCompiler, self).get_column_default_string(column) - - ### Informix wants the constraint name at the end, hence this ist c&p from sql/compiler.py - def visit_primary_key_constraint(self, constraint): - if len(constraint) == 0: - return '' - text = "PRIMARY KEY " - text += "(%s)" % ', '.join(self.preparer.quote(c.name, c.quote) - for c in constraint) - text += self.define_constraint_deferrability(constraint) - - if constraint.name is not None: - text += " CONSTRAINT %s" % self.preparer.format_constraint(constraint) - return text - - def visit_foreign_key_constraint(self, constraint): - preparer = self.dialect.identifier_preparer - remote_table = list(constraint._elements.values())[0].column.table - text = "FOREIGN KEY (%s) REFERENCES %s (%s)" % ( - ', '.join(preparer.quote(f.parent.name, f.parent.quote) - for f in constraint._elements.values()), - preparer.format_table(remote_table), - ', '.join(preparer.quote(f.column.name, f.column.quote) - for f in constraint._elements.values()) - ) - text += self.define_constraint_cascades(constraint) - text += self.define_constraint_deferrability(constraint) - - if constraint.name is not None: - text += " CONSTRAINT %s " % \ - preparer.format_constraint(constraint) - return text - - def visit_unique_constraint(self, constraint): - text = "UNIQUE (%s)" % (', '.join(self.preparer.quote(c.name, c.quote) for c in constraint)) - text += self.define_constraint_deferrability(constraint) - - if constraint.name is not None: - text += "CONSTRAINT %s " % self.preparer.format_constraint(constraint) - return text - -class InformixIdentifierPreparer(compiler.IdentifierPreparer): - - reserved_words = RESERVED_WORDS - - -class InformixDialect(default.DefaultDialect): - name = 'informix' - - max_identifier_length = 128 # adjusts at runtime based on server version - - type_compiler = InfoTypeCompiler - statement_compiler = InfoSQLCompiler - ddl_compiler = InfoDDLCompiler - colspecs = colspecs - ischema_names = ischema_names - preparer = InformixIdentifierPreparer - default_paramstyle = 'qmark' - - def __init__(self, has_transactions=True, *args, **kwargs): - self.has_transactions = has_transactions - default.DefaultDialect.__init__(self, *args, **kwargs) - - def initialize(self, connection): - super(InformixDialect, self).initialize(connection) - - # http://www.querix.com/support/knowledge-base/error_number_message/error_200 - if self.server_version_info < (9, 2): - self.max_identifier_length = 18 - else: - self.max_identifier_length = 128 - - def do_begin(self, connection): - cu = connection.cursor() - cu.execute('SET LOCK MODE TO WAIT') - if self.has_transactions: - cu.execute('SET ISOLATION TO REPEATABLE READ') - - def do_commit(self, connection): - if self.has_transactions: - connection.commit() - - def do_rollback(self, connection): - if self.has_transactions: - connection.rollback() - - def _get_table_names(self, connection, schema, type, **kw): - schema = schema or self.default_schema_name - s = "select tabname, owner from systables where owner=? and tabtype=?" - return [row[0] for row in connection.execute(s, schema, type)] - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - return self._get_table_names(connection, schema, 'T', **kw) - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - return self._get_table_names(connection, schema, 'V', **kw) - - @reflection.cache - def get_schema_names(self, connection, **kw): - s = "select owner from systables" - return [row[0] for row in connection.execute(s)] - - def has_table(self, connection, table_name, schema=None): - schema = schema or self.default_schema_name - cursor = connection.execute( - """select tabname from systables where tabname=? and owner=?""", - table_name, schema) - return cursor.first() is not None - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - schema = schema or self.default_schema_name - c = connection.execute( - """select colname, coltype, collength, t3.default, t1.colno from - syscolumns as t1 , systables as t2 , OUTER sysdefaults as t3 - where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=? - and t3.tabid = t2.tabid and t3.colno = t1.colno - order by t1.colno""", table_name, schema) - - primary_cols = self.get_primary_keys(connection, table_name, schema, **kw) - - columns = [] - rows = c.fetchall() - for name, colattr, collength, default, colno in rows: - name = name.lower() - - autoincrement = False - primary_key = False - - if name in primary_cols: - primary_key = True - - # in 7.31, coltype = 0x000 - # ^^-- column type - # ^-- 1 not null, 0 null - not_nullable, coltype = divmod(colattr, 256) - if coltype not in (0, 13) and default: - default = default.split()[-1] - - if coltype == 6: # Serial, mark as autoincrement - autoincrement = True - - if coltype == 0 or coltype == 13: # char, varchar - coltype = ischema_names[coltype](collength) - if default: - default = "'%s'" % default - elif coltype == 5: # decimal - precision, scale = (collength & 0xFF00) >> 8, collength & 0xFF - if scale == 255: - scale = 0 - coltype = sqltypes.Numeric(precision, scale) - else: - try: - coltype = ischema_names[coltype] - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (coltype, name)) - coltype = sqltypes.NULLTYPE - - column_info = dict(name=name, type=coltype, nullable=not not_nullable, - default=default, autoincrement=autoincrement, - primary_key=primary_key) - columns.append(column_info) - return columns - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - schema_sel = schema or self.default_schema_name - c = connection.execute( - """select t1.constrname as cons_name, - t4.colname as local_column, t7.tabname as remote_table, - t6.colname as remote_column, t7.owner as remote_owner - from sysconstraints as t1 , systables as t2 , - sysindexes as t3 , syscolumns as t4 , - sysreferences as t5 , syscolumns as t6 , systables as t7 , - sysconstraints as t8 , sysindexes as t9 - where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=? and t1.constrtype = 'R' - and t3.tabid = t2.tabid and t3.idxname = t1.idxname - and t4.tabid = t2.tabid and t4.colno in (t3.part1, t3.part2, t3.part3, - t3.part4, t3.part5, t3.part6, t3.part7, t3.part8, t3.part9, t3.part10, - t3.part11, t3.part11, t3.part12, t3.part13, t3.part4, t3.part15, t3.part16) - and t5.constrid = t1.constrid and t8.constrid = t5.primary - and t6.tabid = t5.ptabid and t6.colno in (t9.part1, t9.part2, t9.part3, - t9.part4, t9.part5, t9.part6, t9.part7, t9.part8, t9.part9, t9.part10, - t9.part11, t9.part11, t9.part12, t9.part13, t9.part4, t9.part15, t9.part16) and t9.idxname = - t8.idxname - and t7.tabid = t5.ptabid""", table_name, schema_sel) - - - def fkey_rec(): - return { - 'name' : None, - 'constrained_columns' : [], - 'referred_schema' : None, - 'referred_table' : None, - 'referred_columns' : [] - } - - fkeys = util.defaultdict(fkey_rec) - - rows = c.fetchall() - for cons_name, local_column, \ - remote_table, remote_column, remote_owner in rows: - - rec = fkeys[cons_name] - rec['name'] = cons_name - local_cols, remote_cols = \ - rec['constrained_columns'], rec['referred_columns'] - - if not rec['referred_table']: - rec['referred_table'] = remote_table - if schema is not None: - rec['referred_schema'] = remote_owner - - if local_column not in local_cols: - local_cols.append(local_column) - if remote_column not in remote_cols: - remote_cols.append(remote_column) - - return fkeys.values() - - @reflection.cache - def get_primary_keys(self, connection, table_name, schema=None, **kw): - schema = schema or self.default_schema_name - - # Select the column positions from sysindexes for sysconstraints - data = connection.execute( - """select t2.* - from systables as t1, sysindexes as t2, sysconstraints as t3 - where t1.tabid=t2.tabid and t1.tabname=? and t1.owner=? - and t2.idxname=t3.idxname and t3.constrtype='P'""", - table_name, schema - ).fetchall() - - colpositions = set() - - for row in data: - colpos = set([getattr(row, 'part%d' % x) for x in range(1,16)]) - colpositions |= colpos - - if not len(colpositions): - return [] - - # Select the column names using the columnpositions - # TODO: Maybe cache a bit of those col infos (eg select all colnames for one table) - place_holder = ','.join('?'*len(colpositions)) - c = connection.execute( - """select t1.colname - from syscolumns as t1, systables as t2 - where t2.tabname=? and t1.tabid = t2.tabid and - t1.colno in (%s)""" % place_holder, - table_name, *colpositions - ).fetchall() - - return reduce(lambda x,y: list(x)+list(y), c, []) - - @reflection.cache - def get_indexes(self, connection, table_name, schema, **kw): - # TODO: schema... - c = connection.execute( - """select t1.* - from sysindexes as t1 , systables as t2 - where t1.tabid = t2.tabid and t2.tabname=?""", - table_name) - - indexes = [] - for row in c.fetchall(): - colnames = [getattr(row, 'part%d' % x) for x in range(1,16)] - colnames = [x for x in colnames if x] - place_holder = ','.join('?'*len(colnames)) - c = connection.execute( - """select t1.colname - from syscolumns as t1, systables as t2 - where t2.tabname=? and t1.tabid = t2.tabid and - t1.colno in (%s)""" % place_holder, - table_name, *colnames - ).fetchall() - c = reduce(lambda x,y: list(x)+list(y), c, []) - indexes.append({ - 'name': row.idxname, - 'unique': row.idxtype.lower() == 'u', - 'column_names': c - }) - return indexes - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - schema = schema or self.default_schema_name - c = connection.execute( - """select t1.viewtext - from sysviews as t1 , systables as t2 - where t1.tabid=t2.tabid and t2.tabname=? - and t2.owner=? order by seqno""", - view_name, schema).fetchall() - - return ''.join([row[0] for row in c]) - - def _get_default_schema_name(self, connection): - return connection.execute('select CURRENT_ROLE from systables').scalar() diff --git a/libs/sqlalchemy/dialects/informix/informixdb.py b/libs/sqlalchemy/dialects/informix/informixdb.py deleted file mode 100644 index 8b543467..00000000 --- a/libs/sqlalchemy/dialects/informix/informixdb.py +++ /dev/null @@ -1,73 +0,0 @@ -# informix/informixdb.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -Support for the informixdb DBAPI. - -informixdb is available at: - - http://informixdb.sourceforge.net/ - -Connecting -^^^^^^^^^^ - -Sample informix connection:: - - engine = create_engine('informix+informixdb://user:password@host/dbname') - -""" - -import re - -from sqlalchemy.dialects.informix.base import InformixDialect -from sqlalchemy.engine import default - -VERSION_RE = re.compile(r'(\d+)\.(\d+)(.+\d+)') - -class InformixExecutionContext_informixdb(default.DefaultExecutionContext): - def post_exec(self): - if self.isinsert: - self._lastrowid = self.cursor.sqlerrd[1] - - def get_lastrowid(self): - return self._lastrowid - - -class InformixDialect_informixdb(InformixDialect): - driver = 'informixdb' - execution_ctx_cls = InformixExecutionContext_informixdb - - @classmethod - def dbapi(cls): - return __import__('informixdb') - - def create_connect_args(self, url): - if url.host: - dsn = '%s@%s' % (url.database, url.host) - else: - dsn = url.database - - if url.username: - opt = {'user': url.username, 'password': url.password} - else: - opt = {} - - return ([dsn], opt) - - def _get_server_version_info(self, connection): - # http://informixdb.sourceforge.net/manual.html#inspecting-version-numbers - v = VERSION_RE.split(connection.connection.dbms_version) - return (int(v[1]), int(v[2]), v[3]) - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.OperationalError): - return 'closed the connection' in str(e) \ - or 'connection not open' in str(e) - else: - return False - - -dialect = InformixDialect_informixdb diff --git a/libs/sqlalchemy/dialects/maxdb/__init__.py b/libs/sqlalchemy/dialects/maxdb/__init__.py deleted file mode 100644 index 9d1d6418..00000000 --- a/libs/sqlalchemy/dialects/maxdb/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# maxdb/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.maxdb import base, sapdb - -base.dialect = sapdb.dialect \ No newline at end of file diff --git a/libs/sqlalchemy/dialects/maxdb/base.py b/libs/sqlalchemy/dialects/maxdb/base.py deleted file mode 100644 index 68ae630e..00000000 --- a/libs/sqlalchemy/dialects/maxdb/base.py +++ /dev/null @@ -1,1117 +0,0 @@ -# maxdb/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Support for the MaxDB database. - -.. note:: - - The MaxDB dialect is **non-functional as of SQLAlchemy 0.6**, - pending development efforts to bring it up-to-date. - -Overview --------- - -The ``maxdb`` dialect is **experimental** and has only been tested on 7.6.03.007 -and 7.6.00.037. Of these, **only 7.6.03.007 will work** with SQLAlchemy's ORM. -The earlier version has severe ``LEFT JOIN`` limitations and will return -incorrect results from even very simple ORM queries. - -Only the native Python DB-API is currently supported. ODBC driver support -is a future enhancement. - -Connecting ----------- - -The username is case-sensitive. If you usually connect to the -database with sqlcli and other tools in lower case, you likely need to -use upper case for DB-API. - -Implementation Notes --------------------- - -With the 7.6.00.37 driver and Python 2.5, it seems that all DB-API -generated exceptions are broken and can cause Python to crash. - -For 'somecol.in_([])' to work, the IN operator's generation must be changed -to cast 'NULL' to a numeric, i.e. NUM(NULL). The DB-API doesn't accept a -bind parameter there, so that particular generation must inline the NULL value, -which depends on [ticket:807]. - -The DB-API is very picky about where bind params may be used in queries. - -Bind params for some functions (e.g. MOD) need type information supplied. -The dialect does not yet do this automatically. - -Max will occasionally throw up 'bad sql, compile again' exceptions for -perfectly valid SQL. The dialect does not currently handle these, more -research is needed. - -MaxDB 7.5 and Sap DB <= 7.4 reportedly do not support schemas. A very -slightly different version of this dialect would be required to support -those versions, and can easily be added if there is demand. Some other -required components such as an Max-aware 'old oracle style' join compiler -(thetas with (+) outer indicators) are already done and available for -integration- email the devel list if you're interested in working on -this. - -Versions tested: 7.6.03.07 and 7.6.00.37, native Python DB-API - -* MaxDB has severe limitations on OUTER JOINs, which are essential to ORM - eager loading. And rather than raise an error if a SELECT can't be serviced, - the database simply returns incorrect results. -* Version 7.6.03.07 seems to JOIN properly, however the docs do not show the - OUTER restrictions being lifted (as of this writing), and no changelog is - available to confirm either. If you are using a different server version and - your tasks require the ORM or any semi-advanced SQL through the SQL layer, - running the SQLAlchemy test suite against your database is HIGHLY - recommended before you begin. -* Version 7.6.00.37 is LHS/RHS sensitive in `FROM lhs LEFT OUTER JOIN rhs ON - lhs.col=rhs.col` vs `rhs.col=lhs.col`! -* Version 7.6.00.37 is confused by `SELECT DISTINCT col as alias FROM t ORDER - BY col` - these aliased, DISTINCT, ordered queries need to be re-written to - order by the alias name. -* Version 7.6.x supports creating a SAVEPOINT but not its RELEASE. -* MaxDB supports autoincrement-style columns (DEFAULT SERIAL) and independent - sequences. When including a DEFAULT SERIAL column in an insert, 0 needs to - be inserted rather than NULL to generate a value. -* MaxDB supports ANSI and "old Oracle style" theta joins with (+) outer join - indicators. -* The SQLAlchemy dialect is schema-aware and probably won't function correctly - on server versions (pre-7.6?). Support for schema-less server versions could - be added if there's call. -* ORDER BY is not supported in subqueries. LIMIT is not supported in - subqueries. In 7.6.00.37, TOP does work in subqueries, but without limit not - so useful. OFFSET does not work in 7.6 despite being in the docs. Row number - tricks in WHERE via ROWNO may be possible but it only seems to allow - less-than comparison! -* Version 7.6.03.07 can't LIMIT if a derived table is in FROM: `SELECT * FROM - (SELECT * FROM a) LIMIT 2` -* MaxDB does not support sql's CAST and can only usefullly cast two types. - There isn't much implicit type conversion, so be precise when creating - `PassiveDefaults` in DDL generation: `'3'` and `3` aren't the same. - -sapdb.dbapi -^^^^^^^^^^^ - -* As of 2007-10-22 the Python 2.4 and 2.5 compatible versions of the DB-API - are no longer available. A forum posting at SAP states that the Python - driver will be available again "in the future". The last release from MySQL - AB works if you can find it. -* sequence.NEXTVAL skips every other value! -* No rowcount for executemany() -* If an INSERT into a table with a DEFAULT SERIAL column inserts the results - of a function `INSERT INTO t VALUES (LENGTH('foo'))`, the cursor won't have - the serial id. It needs to be manually yanked from tablename.CURRVAL. -* Super-duper picky about where bind params can be placed. Not smart about - converting Python types for some functions, such as `MOD(5, ?)`. -* LONG (text, binary) values in result sets are read-once. The dialect uses a - caching RowProxy when these types are present. -* Connection objects seem like they want to be either `close()`d or garbage - collected, but not both. There's a warning issued but it seems harmless. - - -""" -import datetime, itertools, re - -from sqlalchemy import exc, schema, sql, util, processors -from sqlalchemy.sql import operators as sql_operators, expression as sql_expr -from sqlalchemy.sql import compiler, visitors -from sqlalchemy.engine import base as engine_base, default, reflection -from sqlalchemy import types as sqltypes - - -class _StringType(sqltypes.String): - _type = None - - def __init__(self, length=None, encoding=None, **kw): - super(_StringType, self).__init__(length=length, **kw) - self.encoding = encoding - - def bind_processor(self, dialect): - if self.encoding == 'unicode': - return None - else: - def process(value): - if isinstance(value, unicode): - return value.encode(dialect.encoding) - else: - return value - return process - - def result_processor(self, dialect, coltype): - #XXX: this code is probably very slow and one should try (if at all - # possible) to determine the correct code path on a per-connection - # basis (ie, here in result_processor, instead of inside the processor - # function itself) and probably also use a few generic - # processors, or possibly per query (though there is no mechanism - # for that yet). - def process(value): - while True: - if value is None: - return None - elif isinstance(value, unicode): - return value - elif isinstance(value, str): - if self.convert_unicode or dialect.convert_unicode: - return value.decode(dialect.encoding) - else: - return value - elif hasattr(value, 'read'): - # some sort of LONG, snarf and retry - value = value.read(value.remainingLength()) - continue - else: - # unexpected type, return as-is - return value - return process - - -class MaxString(_StringType): - _type = 'VARCHAR' - - -class MaxUnicode(_StringType): - _type = 'VARCHAR' - - def __init__(self, length=None, **kw): - kw['encoding'] = 'unicode' - super(MaxUnicode, self).__init__(length=length, **kw) - - -class MaxChar(_StringType): - _type = 'CHAR' - - -class MaxText(_StringType): - _type = 'LONG' - - def __init__(self, length=None, **kw): - super(MaxText, self).__init__(length, **kw) - - def get_col_spec(self): - spec = 'LONG' - if self.encoding is not None: - spec = ' '.join((spec, self.encoding)) - elif self.convert_unicode: - spec = ' '.join((spec, 'UNICODE')) - - return spec - - -class MaxNumeric(sqltypes.Numeric): - """The FIXED (also NUMERIC, DECIMAL) data type.""" - - def __init__(self, precision=None, scale=None, **kw): - kw.setdefault('asdecimal', True) - super(MaxNumeric, self).__init__(scale=scale, precision=precision, - **kw) - - def bind_processor(self, dialect): - return None - - -class MaxTimestamp(sqltypes.DateTime): - def bind_processor(self, dialect): - def process(value): - if value is None: - return None - elif isinstance(value, basestring): - return value - elif dialect.datetimeformat == 'internal': - ms = getattr(value, 'microsecond', 0) - return value.strftime("%Y%m%d%H%M%S" + ("%06u" % ms)) - elif dialect.datetimeformat == 'iso': - ms = getattr(value, 'microsecond', 0) - return value.strftime("%Y-%m-%d %H:%M:%S." + ("%06u" % ms)) - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % ( - dialect.datetimeformat,)) - return process - - def result_processor(self, dialect, coltype): - if dialect.datetimeformat == 'internal': - def process(value): - if value is None: - return None - else: - return datetime.datetime( - *[int(v) - for v in (value[0:4], value[4:6], value[6:8], - value[8:10], value[10:12], value[12:14], - value[14:])]) - elif dialect.datetimeformat == 'iso': - def process(value): - if value is None: - return None - else: - return datetime.datetime( - *[int(v) - for v in (value[0:4], value[5:7], value[8:10], - value[11:13], value[14:16], value[17:19], - value[20:])]) - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % - dialect.datetimeformat) - return process - - -class MaxDate(sqltypes.Date): - def bind_processor(self, dialect): - def process(value): - if value is None: - return None - elif isinstance(value, basestring): - return value - elif dialect.datetimeformat == 'internal': - return value.strftime("%Y%m%d") - elif dialect.datetimeformat == 'iso': - return value.strftime("%Y-%m-%d") - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % ( - dialect.datetimeformat,)) - return process - - def result_processor(self, dialect, coltype): - if dialect.datetimeformat == 'internal': - def process(value): - if value is None: - return None - else: - return datetime.date(int(value[0:4]), int(value[4:6]), - int(value[6:8])) - elif dialect.datetimeformat == 'iso': - def process(value): - if value is None: - return None - else: - return datetime.date(int(value[0:4]), int(value[5:7]), - int(value[8:10])) - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % - dialect.datetimeformat) - return process - - -class MaxTime(sqltypes.Time): - def bind_processor(self, dialect): - def process(value): - if value is None: - return None - elif isinstance(value, basestring): - return value - elif dialect.datetimeformat == 'internal': - return value.strftime("%H%M%S") - elif dialect.datetimeformat == 'iso': - return value.strftime("%H-%M-%S") - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % ( - dialect.datetimeformat,)) - return process - - def result_processor(self, dialect, coltype): - if dialect.datetimeformat == 'internal': - def process(value): - if value is None: - return None - else: - return datetime.time(int(value[0:4]), int(value[4:6]), - int(value[6:8])) - elif dialect.datetimeformat == 'iso': - def process(value): - if value is None: - return None - else: - return datetime.time(int(value[0:4]), int(value[5:7]), - int(value[8:10])) - else: - raise exc.InvalidRequestError( - "datetimeformat '%s' is not supported." % - dialect.datetimeformat) - return process - - -class MaxBlob(sqltypes.LargeBinary): - def bind_processor(self, dialect): - return processors.to_str - - def result_processor(self, dialect, coltype): - def process(value): - if value is None: - return None - else: - return value.read(value.remainingLength()) - return process - -class MaxDBTypeCompiler(compiler.GenericTypeCompiler): - def _string_spec(self, string_spec, type_): - if type_.length is None: - spec = 'LONG' - else: - spec = '%s(%s)' % (string_spec, type_.length) - - if getattr(type_, 'encoding'): - spec = ' '.join([spec, getattr(type_, 'encoding').upper()]) - return spec - - def visit_text(self, type_): - spec = 'LONG' - if getattr(type_, 'encoding', None): - spec = ' '.join((spec, type_.encoding)) - elif type_.convert_unicode: - spec = ' '.join((spec, 'UNICODE')) - - return spec - - def visit_char(self, type_): - return self._string_spec("CHAR", type_) - - def visit_string(self, type_): - return self._string_spec("VARCHAR", type_) - - def visit_large_binary(self, type_): - return "LONG BYTE" - - def visit_numeric(self, type_): - if type_.scale and type_.precision: - return 'FIXED(%s, %s)' % (type_.precision, type_.scale) - elif type_.precision: - return 'FIXED(%s)' % type_.precision - else: - return 'INTEGER' - - def visit_BOOLEAN(self, type_): - return "BOOLEAN" - -colspecs = { - sqltypes.Numeric: MaxNumeric, - sqltypes.DateTime: MaxTimestamp, - sqltypes.Date: MaxDate, - sqltypes.Time: MaxTime, - sqltypes.String: MaxString, - sqltypes.Unicode:MaxUnicode, - sqltypes.LargeBinary: MaxBlob, - sqltypes.Text: MaxText, - sqltypes.CHAR: MaxChar, - sqltypes.TIMESTAMP: MaxTimestamp, - sqltypes.BLOB: MaxBlob, - sqltypes.Unicode: MaxUnicode, - } - -ischema_names = { - 'boolean': sqltypes.BOOLEAN, - 'char': sqltypes.CHAR, - 'character': sqltypes.CHAR, - 'date': sqltypes.DATE, - 'fixed': sqltypes.Numeric, - 'float': sqltypes.FLOAT, - 'int': sqltypes.INT, - 'integer': sqltypes.INT, - 'long binary': sqltypes.BLOB, - 'long unicode': sqltypes.Text, - 'long': sqltypes.Text, - 'long': sqltypes.Text, - 'smallint': sqltypes.SmallInteger, - 'time': sqltypes.Time, - 'timestamp': sqltypes.TIMESTAMP, - 'varchar': sqltypes.VARCHAR, - } - -# TODO: migrate this to sapdb.py -class MaxDBExecutionContext(default.DefaultExecutionContext): - def post_exec(self): - # DB-API bug: if there were any functions as values, - # then do another select and pull CURRVAL from the - # autoincrement column's implicit sequence... ugh - if self.compiled.isinsert and not self.executemany: - table = self.compiled.statement.table - index, serial_col = _autoserial_column(table) - - if serial_col and (not self.compiled._safeserial or - not(self._last_inserted_ids) or - self._last_inserted_ids[index] in (None, 0)): - if table.schema: - sql = "SELECT %s.CURRVAL FROM DUAL" % ( - self.compiled.preparer.format_table(table)) - else: - sql = "SELECT CURRENT_SCHEMA.%s.CURRVAL FROM DUAL" % ( - self.compiled.preparer.format_table(table)) - - rs = self.cursor.execute(sql) - id = rs.fetchone()[0] - - if not self._last_inserted_ids: - # This shouldn't ever be > 1? Right? - self._last_inserted_ids = \ - [None] * len(table.primary_key.columns) - self._last_inserted_ids[index] = id - - super(MaxDBExecutionContext, self).post_exec() - - def get_result_proxy(self): - if self.cursor.description is not None: - for column in self.cursor.description: - if column[1] in ('Long Binary', 'Long', 'Long Unicode'): - return MaxDBResultProxy(self) - return engine_base.ResultProxy(self) - - @property - def rowcount(self): - if hasattr(self, '_rowcount'): - return self._rowcount - else: - return self.cursor.rowcount - - def fire_sequence(self, seq): - if seq.optional: - return None - return self._execute_scalar("SELECT %s.NEXTVAL FROM DUAL" % ( - self.dialect.identifier_preparer.format_sequence(seq))) - -class MaxDBCachedColumnRow(engine_base.RowProxy): - """A RowProxy that only runs result_processors once per column.""" - - def __init__(self, parent, row): - super(MaxDBCachedColumnRow, self).__init__(parent, row) - self.columns = {} - self._row = row - self._parent = parent - - def _get_col(self, key): - if key not in self.columns: - self.columns[key] = self._parent._get_col(self._row, key) - return self.columns[key] - - def __iter__(self): - for i in xrange(len(self._row)): - yield self._get_col(i) - - def __repr__(self): - return repr(list(self)) - - def __eq__(self, other): - return ((other is self) or - (other == tuple([self._get_col(key) - for key in xrange(len(self._row))]))) - def __getitem__(self, key): - if isinstance(key, slice): - indices = key.indices(len(self._row)) - return tuple([self._get_col(i) for i in xrange(*indices)]) - else: - return self._get_col(key) - - def __getattr__(self, name): - try: - return self._get_col(name) - except KeyError: - raise AttributeError(name) - - -class MaxDBResultProxy(engine_base.ResultProxy): - _process_row = MaxDBCachedColumnRow - -class MaxDBCompiler(compiler.SQLCompiler): - - function_conversion = { - 'CURRENT_DATE': 'DATE', - 'CURRENT_TIME': 'TIME', - 'CURRENT_TIMESTAMP': 'TIMESTAMP', - } - - # These functions must be written without parens when called with no - # parameters. e.g. 'SELECT DATE FROM DUAL' not 'SELECT DATE() FROM DUAL' - bare_functions = set([ - 'CURRENT_SCHEMA', 'DATE', 'FALSE', 'SYSDBA', 'TIME', 'TIMESTAMP', - 'TIMEZONE', 'TRANSACTION', 'TRUE', 'USER', 'UID', 'USERGROUP', - 'UTCDATE', 'UTCDIFF']) - - def visit_mod(self, binary, **kw): - return "mod(%s, %s)" % \ - (self.process(binary.left), self.process(binary.right)) - - def default_from(self): - return ' FROM DUAL' - - def for_update_clause(self, select): - clause = select.for_update - if clause is True: - return " WITH LOCK EXCLUSIVE" - elif clause is None: - return "" - elif clause == "read": - return " WITH LOCK" - elif clause == "ignore": - return " WITH LOCK (IGNORE) EXCLUSIVE" - elif clause == "nowait": - return " WITH LOCK (NOWAIT) EXCLUSIVE" - elif isinstance(clause, basestring): - return " WITH LOCK %s" % clause.upper() - elif not clause: - return "" - else: - return " WITH LOCK EXCLUSIVE" - - def function_argspec(self, fn, **kw): - if fn.name.upper() in self.bare_functions: - return "" - elif len(fn.clauses) > 0: - return compiler.SQLCompiler.function_argspec(self, fn, **kw) - else: - return "" - - def visit_function(self, fn, **kw): - transform = self.function_conversion.get(fn.name.upper(), None) - if transform: - fn = fn._clone() - fn.name = transform - return super(MaxDBCompiler, self).visit_function(fn, **kw) - - def visit_cast(self, cast, **kwargs): - # MaxDB only supports casts * to NUMERIC, * to VARCHAR or - # date/time to VARCHAR. Casts of LONGs will fail. - if isinstance(cast.type, (sqltypes.Integer, sqltypes.Numeric)): - return "NUM(%s)" % self.process(cast.clause) - elif isinstance(cast.type, sqltypes.String): - return "CHR(%s)" % self.process(cast.clause) - else: - return self.process(cast.clause) - - def visit_sequence(self, sequence): - if sequence.optional: - return None - else: - return ( - self.dialect.identifier_preparer.format_sequence(sequence) + - ".NEXTVAL") - - class ColumnSnagger(visitors.ClauseVisitor): - def __init__(self): - self.count = 0 - self.column = None - def visit_column(self, column): - self.column = column - self.count += 1 - - def _find_labeled_columns(self, columns, use_labels=False): - labels = {} - for column in columns: - if isinstance(column, basestring): - continue - snagger = self.ColumnSnagger() - snagger.traverse(column) - if snagger.count == 1: - if isinstance(column, sql_expr._Label): - labels[unicode(snagger.column)] = column.name - elif use_labels: - labels[unicode(snagger.column)] = column._label - - return labels - - def order_by_clause(self, select, **kw): - order_by = self.process(select._order_by_clause, **kw) - - # ORDER BY clauses in DISTINCT queries must reference aliased - # inner columns by alias name, not true column name. - if order_by and getattr(select, '_distinct', False): - labels = self._find_labeled_columns(select.inner_columns, - select.use_labels) - if labels: - for needs_alias in labels.keys(): - r = re.compile(r'(^| )(%s)(,| |$)' % - re.escape(needs_alias)) - order_by = r.sub((r'\1%s\3' % labels[needs_alias]), - order_by) - - # No ORDER BY in subqueries. - if order_by: - if self.is_subquery(): - # It's safe to simply drop the ORDER BY if there is no - # LIMIT. Right? Other dialects seem to get away with - # dropping order. - if select._limit: - raise exc.CompileError( - "MaxDB does not support ORDER BY in subqueries") - else: - return "" - return " ORDER BY " + order_by - else: - return "" - - def get_select_precolumns(self, select): - # Convert a subquery's LIMIT to TOP - sql = select._distinct and 'DISTINCT ' or '' - if self.is_subquery() and select._limit: - if select._offset: - raise exc.InvalidRequestError( - 'MaxDB does not support LIMIT with an offset.') - sql += 'TOP %s ' % select._limit - return sql - - def limit_clause(self, select): - # The docs say offsets are supported with LIMIT. But they're not. - # TODO: maybe emulate by adding a ROWNO/ROWNUM predicate? - # TODO: does MaxDB support bind params for LIMIT / TOP ? - if self.is_subquery(): - # sub queries need TOP - return '' - elif select._offset: - raise exc.InvalidRequestError( - 'MaxDB does not support LIMIT with an offset.') - else: - return ' \n LIMIT %s' % (select._limit,) - - def visit_insert(self, insert): - self.isinsert = True - self._safeserial = True - - colparams = self._get_colparams(insert) - for value in (insert.parameters or {}).itervalues(): - if isinstance(value, sql_expr.Function): - self._safeserial = False - break - - return ''.join(('INSERT INTO ', - self.preparer.format_table(insert.table), - ' (', - ', '.join([self.preparer.format_column(c[0]) - for c in colparams]), - ') VALUES (', - ', '.join([c[1] for c in colparams]), - ')')) - - -class MaxDBIdentifierPreparer(compiler.IdentifierPreparer): - reserved_words = set([ - 'abs', 'absolute', 'acos', 'adddate', 'addtime', 'all', 'alpha', - 'alter', 'any', 'ascii', 'asin', 'atan', 'atan2', 'avg', 'binary', - 'bit', 'boolean', 'byte', 'case', 'ceil', 'ceiling', 'char', - 'character', 'check', 'chr', 'column', 'concat', 'constraint', 'cos', - 'cosh', 'cot', 'count', 'cross', 'curdate', 'current', 'curtime', - 'database', 'date', 'datediff', 'day', 'dayname', 'dayofmonth', - 'dayofweek', 'dayofyear', 'dec', 'decimal', 'decode', 'default', - 'degrees', 'delete', 'digits', 'distinct', 'double', 'except', - 'exists', 'exp', 'expand', 'first', 'fixed', 'float', 'floor', 'for', - 'from', 'full', 'get_objectname', 'get_schema', 'graphic', 'greatest', - 'group', 'having', 'hex', 'hextoraw', 'hour', 'ifnull', 'ignore', - 'index', 'initcap', 'inner', 'insert', 'int', 'integer', 'internal', - 'intersect', 'into', 'join', 'key', 'last', 'lcase', 'least', 'left', - 'length', 'lfill', 'list', 'ln', 'locate', 'log', 'log10', 'long', - 'longfile', 'lower', 'lpad', 'ltrim', 'makedate', 'maketime', - 'mapchar', 'max', 'mbcs', 'microsecond', 'min', 'minute', 'mod', - 'month', 'monthname', 'natural', 'nchar', 'next', 'no', 'noround', - 'not', 'now', 'null', 'num', 'numeric', 'object', 'of', 'on', - 'order', 'packed', 'pi', 'power', 'prev', 'primary', 'radians', - 'real', 'reject', 'relative', 'replace', 'rfill', 'right', 'round', - 'rowid', 'rowno', 'rpad', 'rtrim', 'second', 'select', 'selupd', - 'serial', 'set', 'show', 'sign', 'sin', 'sinh', 'smallint', 'some', - 'soundex', 'space', 'sqrt', 'stamp', 'statistics', 'stddev', - 'subdate', 'substr', 'substring', 'subtime', 'sum', 'sysdba', - 'table', 'tan', 'tanh', 'time', 'timediff', 'timestamp', 'timezone', - 'to', 'toidentifier', 'transaction', 'translate', 'trim', 'trunc', - 'truncate', 'ucase', 'uid', 'unicode', 'union', 'update', 'upper', - 'user', 'usergroup', 'using', 'utcdate', 'utcdiff', 'value', 'values', - 'varchar', 'vargraphic', 'variance', 'week', 'weekofyear', 'when', - 'where', 'with', 'year', 'zoned' ]) - - def _normalize_name(self, name): - if name is None: - return None - if name.isupper(): - lc_name = name.lower() - if not self._requires_quotes(lc_name): - return lc_name - return name - - def _denormalize_name(self, name): - if name is None: - return None - elif (name.islower() and - not self._requires_quotes(name)): - return name.upper() - else: - return name - - def _maybe_quote_identifier(self, name): - if self._requires_quotes(name): - return self.quote_identifier(name) - else: - return name - - -class MaxDBDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, **kw): - colspec = [self.preparer.format_column(column), - self.dialect.type_compiler.process(column.type)] - - if not column.nullable: - colspec.append('NOT NULL') - - default = column.default - default_str = self.get_column_default_string(column) - - # No DDL default for columns specified with non-optional sequence- - # this defaulting behavior is entirely client-side. (And as a - # consequence, non-reflectable.) - if (default and isinstance(default, schema.Sequence) and - not default.optional): - pass - # Regular default - elif default_str is not None: - colspec.append('DEFAULT %s' % default_str) - # Assign DEFAULT SERIAL heuristically - elif column.primary_key and column.autoincrement: - # For SERIAL on a non-primary key member, use - # DefaultClause(text('SERIAL')) - try: - first = [c for c in column.table.primary_key.columns - if (c.autoincrement and - (isinstance(c.type, sqltypes.Integer) or - (isinstance(c.type, MaxNumeric) and - c.type.precision)) and - not c.foreign_keys)].pop(0) - if column is first: - colspec.append('DEFAULT SERIAL') - except IndexError: - pass - return ' '.join(colspec) - - def get_column_default_string(self, column): - if isinstance(column.server_default, schema.DefaultClause): - if isinstance(column.default.arg, basestring): - if isinstance(column.type, sqltypes.Integer): - return str(column.default.arg) - else: - return "'%s'" % column.default.arg - else: - return unicode(self._compile(column.default.arg, None)) - else: - return None - - def visit_create_sequence(self, create): - """Creates a SEQUENCE. - - TODO: move to module doc? - - start - With an integer value, set the START WITH option. - - increment - An integer value to increment by. Default is the database default. - - maxdb_minvalue - maxdb_maxvalue - With an integer value, sets the corresponding sequence option. - - maxdb_no_minvalue - maxdb_no_maxvalue - Defaults to False. If true, sets the corresponding sequence option. - - maxdb_cycle - Defaults to False. If true, sets the CYCLE option. - - maxdb_cache - With an integer value, sets the CACHE option. - - maxdb_no_cache - Defaults to False. If true, sets NOCACHE. - """ - sequence = create.element - - if (not sequence.optional and - (not self.checkfirst or - not self.dialect.has_sequence(self.connection, sequence.name))): - - ddl = ['CREATE SEQUENCE', - self.preparer.format_sequence(sequence)] - - sequence.increment = 1 - - if sequence.increment is not None: - ddl.extend(('INCREMENT BY', str(sequence.increment))) - - if sequence.start is not None: - ddl.extend(('START WITH', str(sequence.start))) - - opts = dict([(pair[0][6:].lower(), pair[1]) - for pair in sequence.kwargs.items() - if pair[0].startswith('maxdb_')]) - - if 'maxvalue' in opts: - ddl.extend(('MAXVALUE', str(opts['maxvalue']))) - elif opts.get('no_maxvalue', False): - ddl.append('NOMAXVALUE') - if 'minvalue' in opts: - ddl.extend(('MINVALUE', str(opts['minvalue']))) - elif opts.get('no_minvalue', False): - ddl.append('NOMINVALUE') - - if opts.get('cycle', False): - ddl.append('CYCLE') - - if 'cache' in opts: - ddl.extend(('CACHE', str(opts['cache']))) - elif opts.get('no_cache', False): - ddl.append('NOCACHE') - - return ' '.join(ddl) - - -class MaxDBDialect(default.DefaultDialect): - name = 'maxdb' - supports_alter = True - supports_unicode_statements = True - max_identifier_length = 32 - supports_sane_rowcount = True - supports_sane_multi_rowcount = False - - preparer = MaxDBIdentifierPreparer - statement_compiler = MaxDBCompiler - ddl_compiler = MaxDBDDLCompiler - execution_ctx_cls = MaxDBExecutionContext - - ported_sqla_06 = False - - colspecs = colspecs - ischema_names = ischema_names - - # MaxDB-specific - datetimeformat = 'internal' - - def __init__(self, _raise_known_sql_errors=False, **kw): - super(MaxDBDialect, self).__init__(**kw) - self._raise_known = _raise_known_sql_errors - - if self.dbapi is None: - self.dbapi_type_map = {} - else: - self.dbapi_type_map = { - 'Long Binary': MaxBlob(), - 'Long byte_t': MaxBlob(), - 'Long Unicode': MaxText(), - 'Timestamp': MaxTimestamp(), - 'Date': MaxDate(), - 'Time': MaxTime(), - datetime.datetime: MaxTimestamp(), - datetime.date: MaxDate(), - datetime.time: MaxTime(), - } - - def do_execute(self, cursor, statement, parameters, context=None): - res = cursor.execute(statement, parameters) - if isinstance(res, int) and context is not None: - context._rowcount = res - - def do_release_savepoint(self, connection, name): - # Does MaxDB truly support RELEASE SAVEPOINT ? All my attempts - # produce "SUBTRANS COMMIT/ROLLBACK not allowed without SUBTRANS - # BEGIN SQLSTATE: I7065" - # Note that ROLLBACK TO works fine. In theory, a RELEASE should - # just free up some transactional resources early, before the overall - # COMMIT/ROLLBACK so omitting it should be relatively ok. - pass - - def _get_default_schema_name(self, connection): - return self.identifier_preparer._normalize_name( - connection.execute( - 'SELECT CURRENT_SCHEMA FROM DUAL').scalar()) - - def has_table(self, connection, table_name, schema=None): - denormalize = self.identifier_preparer._denormalize_name - bind = [denormalize(table_name)] - if schema is None: - sql = ("SELECT tablename FROM TABLES " - "WHERE TABLES.TABLENAME=? AND" - " TABLES.SCHEMANAME=CURRENT_SCHEMA ") - else: - sql = ("SELECT tablename FROM TABLES " - "WHERE TABLES.TABLENAME = ? AND" - " TABLES.SCHEMANAME=? ") - bind.append(denormalize(schema)) - - rp = connection.execute(sql, bind) - return bool(rp.first()) - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - if schema is None: - sql = (" SELECT TABLENAME FROM TABLES WHERE " - " SCHEMANAME=CURRENT_SCHEMA ") - rs = connection.execute(sql) - else: - sql = (" SELECT TABLENAME FROM TABLES WHERE " - " SCHEMANAME=? ") - matchname = self.identifier_preparer._denormalize_name(schema) - rs = connection.execute(sql, matchname) - normalize = self.identifier_preparer._normalize_name - return [normalize(row[0]) for row in rs] - - def reflecttable(self, connection, table, include_columns): - denormalize = self.identifier_preparer._denormalize_name - normalize = self.identifier_preparer._normalize_name - - st = ('SELECT COLUMNNAME, MODE, DATATYPE, CODETYPE, LEN, DEC, ' - ' NULLABLE, "DEFAULT", DEFAULTFUNCTION ' - 'FROM COLUMNS ' - 'WHERE TABLENAME=? AND SCHEMANAME=%s ' - 'ORDER BY POS') - - fk = ('SELECT COLUMNNAME, FKEYNAME, ' - ' REFSCHEMANAME, REFTABLENAME, REFCOLUMNNAME, RULE, ' - ' (CASE WHEN REFSCHEMANAME = CURRENT_SCHEMA ' - ' THEN 1 ELSE 0 END) AS in_schema ' - 'FROM FOREIGNKEYCOLUMNS ' - 'WHERE TABLENAME=? AND SCHEMANAME=%s ' - 'ORDER BY FKEYNAME ') - - params = [denormalize(table.name)] - if not table.schema: - st = st % 'CURRENT_SCHEMA' - fk = fk % 'CURRENT_SCHEMA' - else: - st = st % '?' - fk = fk % '?' - params.append(denormalize(table.schema)) - - rows = connection.execute(st, params).fetchall() - if not rows: - raise exc.NoSuchTableError(table.fullname) - - include_columns = set(include_columns or []) - - for row in rows: - (name, mode, col_type, encoding, length, scale, - nullable, constant_def, func_def) = row - - name = normalize(name) - - if include_columns and name not in include_columns: - continue - - type_args, type_kw = [], {} - if col_type == 'FIXED': - type_args = length, scale - # Convert FIXED(10) DEFAULT SERIAL to our Integer - if (scale == 0 and - func_def is not None and func_def.startswith('SERIAL')): - col_type = 'INTEGER' - type_args = length, - elif col_type in 'FLOAT': - type_args = length, - elif col_type in ('CHAR', 'VARCHAR'): - type_args = length, - type_kw['encoding'] = encoding - elif col_type == 'LONG': - type_kw['encoding'] = encoding - - try: - type_cls = ischema_names[col_type.lower()] - type_instance = type_cls(*type_args, **type_kw) - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (col_type, name)) - type_instance = sqltypes.NullType - - col_kw = {'autoincrement': False} - col_kw['nullable'] = (nullable == 'YES') - col_kw['primary_key'] = (mode == 'KEY') - - if func_def is not None: - if func_def.startswith('SERIAL'): - if col_kw['primary_key']: - # No special default- let the standard autoincrement - # support handle SERIAL pk columns. - col_kw['autoincrement'] = True - else: - # strip current numbering - col_kw['server_default'] = schema.DefaultClause( - sql.text('SERIAL')) - col_kw['autoincrement'] = True - else: - col_kw['server_default'] = schema.DefaultClause( - sql.text(func_def)) - elif constant_def is not None: - col_kw['server_default'] = schema.DefaultClause(sql.text( - "'%s'" % constant_def.replace("'", "''"))) - - table.append_column(schema.Column(name, type_instance, **col_kw)) - - fk_sets = itertools.groupby(connection.execute(fk, params), - lambda row: row.FKEYNAME) - for fkeyname, fkey in fk_sets: - fkey = list(fkey) - if include_columns: - key_cols = set([r.COLUMNNAME for r in fkey]) - if key_cols != include_columns: - continue - - columns, referants = [], [] - quote = self.identifier_preparer._maybe_quote_identifier - - for row in fkey: - columns.append(normalize(row.COLUMNNAME)) - if table.schema or not row.in_schema: - referants.append('.'.join( - [quote(normalize(row[c])) - for c in ('REFSCHEMANAME', 'REFTABLENAME', - 'REFCOLUMNNAME')])) - else: - referants.append('.'.join( - [quote(normalize(row[c])) - for c in ('REFTABLENAME', 'REFCOLUMNNAME')])) - - constraint_kw = {'name': fkeyname.lower()} - if fkey[0].RULE is not None: - rule = fkey[0].RULE - if rule.startswith('DELETE '): - rule = rule[7:] - constraint_kw['ondelete'] = rule - - table_kw = {} - if table.schema or not row.in_schema: - table_kw['schema'] = normalize(fkey[0].REFSCHEMANAME) - - ref_key = schema._get_table_key(normalize(fkey[0].REFTABLENAME), - table_kw.get('schema')) - if ref_key not in table.metadata.tables: - schema.Table(normalize(fkey[0].REFTABLENAME), - table.metadata, - autoload=True, autoload_with=connection, - **table_kw) - - constraint = schema.ForeignKeyConstraint( - columns, referants, link_to_name=True, - **constraint_kw) - table.append_constraint(constraint) - - def has_sequence(self, connection, name): - # [ticket:726] makes this schema-aware. - denormalize = self.identifier_preparer._denormalize_name - sql = ("SELECT sequence_name FROM SEQUENCES " - "WHERE SEQUENCE_NAME=? ") - - rp = connection.execute(sql, denormalize(name)) - return bool(rp.first()) - - -def _autoserial_column(table): - """Finds the effective DEFAULT SERIAL column of a Table, if any.""" - - for index, col in enumerate(table.primary_key.columns): - if (isinstance(col.type, (sqltypes.Integer, sqltypes.Numeric)) and - col.autoincrement): - if isinstance(col.default, schema.Sequence): - if col.default.optional: - return index, col - elif (col.default is None or - (not isinstance(col.server_default, schema.DefaultClause))): - return index, col - - return None, None - diff --git a/libs/sqlalchemy/dialects/maxdb/sapdb.py b/libs/sqlalchemy/dialects/maxdb/sapdb.py deleted file mode 100644 index 51f272a2..00000000 --- a/libs/sqlalchemy/dialects/maxdb/sapdb.py +++ /dev/null @@ -1,23 +0,0 @@ -# maxdb/sapdb.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.maxdb.base import MaxDBDialect - -class MaxDBDialect_sapdb(MaxDBDialect): - driver = 'sapdb' - - @classmethod - def dbapi(cls): - from sapdb import dbapi as _dbapi - return _dbapi - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - opts.update(url.query) - return [], opts - - -dialect = MaxDBDialect_sapdb \ No newline at end of file diff --git a/libs/sqlalchemy/dialects/mssql/__init__.py b/libs/sqlalchemy/dialects/mssql/__init__.py index b3acbf3a..7a2dfa60 100644 --- a/libs/sqlalchemy/dialects/mssql/__init__.py +++ b/libs/sqlalchemy/dialects/mssql/__init__.py @@ -1,5 +1,5 @@ # mssql/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -23,4 +23,4 @@ __all__ = ( 'DATETIME2', 'DATETIMEOFFSET', 'DATE', 'TIME', 'SMALLDATETIME', 'BINARY', 'VARBINARY', 'BIT', 'REAL', 'IMAGE', 'TIMESTAMP', 'MONEY', 'SMALLMONEY', 'UNIQUEIDENTIFIER', 'SQL_VARIANT', 'dialect' -) \ No newline at end of file +) diff --git a/libs/sqlalchemy/dialects/mssql/adodbapi.py b/libs/sqlalchemy/dialects/mssql/adodbapi.py index 05ac6d6f..95cf4242 100644 --- a/libs/sqlalchemy/dialects/mssql/adodbapi.py +++ b/libs/sqlalchemy/dialects/mssql/adodbapi.py @@ -1,11 +1,20 @@ # mssql/adodbapi.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -The adodbapi dialect is not implemented for 0.6 at this time. +.. dialect:: mssql+adodbapi + :name: adodbapi + :dbapi: adodbapi + :connectstring: mssql+adodbapi://:@ + :url: http://adodbapi.sourceforge.net/ + +.. note:: + + The adodbapi dialect is not implemented SQLAlchemy versions 0.6 and + above at this time. """ import datetime @@ -13,6 +22,7 @@ from sqlalchemy import types as sqltypes, util from sqlalchemy.dialects.mssql.base import MSDateTime, MSDialect import sys + class MSDateTime_adodbapi(MSDateTime): def result_processor(self, dialect, coltype): def process(value): @@ -40,7 +50,7 @@ class MSDialect_adodbapi(MSDialect): colspecs = util.update_copy( MSDialect.colspecs, { - sqltypes.DateTime:MSDateTime_adodbapi + sqltypes.DateTime: MSDateTime_adodbapi } ) @@ -49,18 +59,18 @@ class MSDialect_adodbapi(MSDialect): connectors = ["Provider=SQLOLEDB"] if 'port' in keys: - connectors.append ("Data Source=%s, %s" % + connectors.append("Data Source=%s, %s" % (keys.get("host"), keys.get("port"))) else: - connectors.append ("Data Source=%s" % keys.get("host")) - connectors.append ("Initial Catalog=%s" % keys.get("database")) + connectors.append("Data Source=%s" % keys.get("host")) + connectors.append("Initial Catalog=%s" % keys.get("database")) user = keys.get("user") if user: connectors.append("User Id=%s" % user) connectors.append("Password=%s" % keys.get("password", "")) else: connectors.append("Integrated Security=SSPI") - return [[";".join (connectors)], {}] + return [[";".join(connectors)], {}] def is_disconnect(self, e, connection, cursor): return isinstance(e, self.dbapi.adodbapi.DatabaseError) and \ diff --git a/libs/sqlalchemy/dialects/mssql/base.py b/libs/sqlalchemy/dialects/mssql/base.py index b6e0d881..90fd1f38 100644 --- a/libs/sqlalchemy/dialects/mssql/base.py +++ b/libs/sqlalchemy/dialects/mssql/base.py @@ -1,15 +1,13 @@ # mssql/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Microsoft SQL Server database. +""" +.. dialect:: mssql + :name: Microsoft SQL Server -Connecting ----------- - -See the individual driver sections below for details on connecting. Auto Increment Behavior ----------------------- @@ -47,13 +45,10 @@ does in other dialects and results in an ``IDENTITY`` column. Collation Support ----------------- -MSSQL specific string types support a collation parameter that -creates a column-level specific collation for the column. The -collation parameter accepts a Windows Collation Name or a SQL -Collation Name. Supported types are MSChar, MSNChar, MSString, -MSNVarchar, MSText, and MSNText. For example:: +Character collations are supported by the base string types, +specified by the string argument "collation":: - from sqlalchemy.dialects.mssql import VARCHAR + from sqlalchemy import VARCHAR Column('login', VARCHAR(32, collation='Latin1_General_CI_AS')) When such a column is associated with a :class:`.Table`, the @@ -61,6 +56,9 @@ CREATE TABLE statement for this column will yield:: login VARCHAR(32) COLLATE Latin1_General_CI_AS NULL +.. versionadded:: 0.8 Character collations are now part of the base string + types. + LIMIT/OFFSET Support -------------------- @@ -103,6 +101,50 @@ The DATE and TIME types are not available for MSSQL 2005 and previous - if a server version below 2008 is detected, DDL for these types will be issued as DATETIME. +.. _mssql_indexes: + +MSSQL-Specific Index Options +----------------------------- + +The MSSQL dialect supports special options for :class:`.Index`. + +CLUSTERED +^^^^^^^^^^ + +The ``mssql_clustered`` option adds the CLUSTERED keyword to the index:: + + Index("my_index", table.c.x, mssql_clustered=True) + +would render the index as ``CREATE CLUSTERED INDEX my_index ON table (x)`` + +.. versionadded:: 0.8 + +INCLUDE +^^^^^^^ + +The ``mssql_include`` option renders INCLUDE(colname) for the given string names:: + + Index("my_index", table.c.x, mssql_include=['y']) + +would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)`` + +.. versionadded:: 0.8 + +Index ordering +^^^^^^^^^^^^^^ + +Index ordering is available via functional expressions, such as:: + + Index("my_index", table.c.x.desc()) + +would render the index as ``CREATE INDEX my_index ON table (x DESC)`` + +.. versionadded:: 0.8 + +.. seealso:: + + :ref:`schema_indexes_functional` + Compatibility Levels -------------------- MSSQL supports the notion of setting compatibility levels at the @@ -158,30 +200,6 @@ following ALTER DATABASE commands executed at the SQL prompt:: Background on SQL Server snapshot isolation is available at http://msdn.microsoft.com/en-us/library/ms175095.aspx. -Scalar Select Comparisons -------------------------- - -.. deprecated:: 0.8 - The MSSQL dialect contains a legacy behavior whereby comparing - a scalar select to a value using the ``=`` or ``!=`` operator - will resolve to IN or NOT IN, respectively. This behavior - will be removed in 0.8 - the ``s.in_()``/``~s.in_()`` operators - should be used when IN/NOT IN are desired. - -For the time being, the existing behavior prevents a comparison -between scalar select and another value that actually wants to use ``=``. -To remove this behavior in a forwards-compatible way, apply this -compilation rule by placing the following code at the module import -level:: - - from sqlalchemy.ext.compiler import compiles - from sqlalchemy.sql.expression import _BinaryExpression - from sqlalchemy.sql.compiler import SQLCompiler - - @compiles(_BinaryExpression, 'mssql') - def override_legacy_binary(element, compiler, **kw): - return SQLCompiler.visit_binary(compiler, element, **kw) - Known Issues ------------ @@ -190,20 +208,23 @@ Known Issues SQL Server 2005 """ -import datetime, operator, re +import datetime +import operator +import re -from sqlalchemy import sql, schema as sa_schema, exc, util -from sqlalchemy.sql import select, compiler, expression, \ - operators as sql_operators, \ +from ... import sql, schema as sa_schema, exc, util +from ...sql import compiler, expression, \ util as sql_util, cast -from sqlalchemy.engine import default, base, reflection -from sqlalchemy import types as sqltypes -from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \ +from ... import engine +from ...engine import reflection, default +from ... import types as sqltypes +from ...types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \ FLOAT, TIMESTAMP, DATETIME, DATE, BINARY,\ - VARBINARY, BLOB + VARBINARY, TEXT, VARCHAR, NVARCHAR, CHAR, NCHAR -from sqlalchemy.dialects.mssql import information_schema as ischema +from ...util import update_wrapper +from . import information_schema as ischema MS_2008_VERSION = (10,) MS_2005_VERSION = (9,) @@ -240,6 +261,7 @@ RESERVED_WORDS = set( 'writetext', ]) + class REAL(sqltypes.REAL): __visit_name__ = 'REAL' @@ -248,6 +270,7 @@ class REAL(sqltypes.REAL): kw['precision'] = 24 super(REAL, self).__init__(**kw) + class TINYINT(sqltypes.Integer): __visit_name__ = 'TINYINT' @@ -267,11 +290,12 @@ class _MSDate(sqltypes.Date): return process _reg = re.compile(r"(\d+)-(\d+)-(\d+)") + def result_processor(self, dialect, coltype): def process(value): if isinstance(value, datetime.datetime): return value.date() - elif isinstance(value, basestring): + elif isinstance(value, util.string_types): return datetime.date(*[ int(x or 0) for x in self._reg.match(value).groups() @@ -280,6 +304,7 @@ class _MSDate(sqltypes.Date): return value return process + class TIME(sqltypes.TIME): def __init__(self, precision=None, **kwargs): self.precision = precision @@ -298,17 +323,20 @@ class TIME(sqltypes.TIME): return process _reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d{0,6}))?") + def result_processor(self, dialect, coltype): def process(value): if isinstance(value, datetime.datetime): return value.time() - elif isinstance(value, basestring): + elif isinstance(value, util.string_types): return datetime.time(*[ int(x or 0) for x in self._reg.match(value).groups()]) else: return value return process +_MSTime = TIME + class _DateTimeBase(object): def bind_processor(self, dialect): @@ -319,12 +347,15 @@ class _DateTimeBase(object): return value return process + class _MSDateTime(_DateTimeBase, sqltypes.DateTime): pass + class SMALLDATETIME(_DateTimeBase, sqltypes.DateTime): __visit_name__ = 'SMALLDATETIME' + class DATETIME2(_DateTimeBase, sqltypes.DateTime): __visit_name__ = 'DATETIME2' @@ -340,135 +371,28 @@ class DATETIMEOFFSET(sqltypes.TypeEngine): def __init__(self, precision=None, **kwargs): self.precision = precision + class _StringType(object): """Base for MSSQL string types.""" def __init__(self, collation=None): - self.collation = collation + super(_StringType, self).__init__(collation=collation) -class TEXT(_StringType, sqltypes.TEXT): - """MSSQL TEXT type, for variable-length text up to 2^31 characters.""" - def __init__(self, length=None, collation=None, **kw): - """Construct a TEXT. - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - """ - _StringType.__init__(self, collation) - sqltypes.Text.__init__(self, length, **kw) - -class NTEXT(_StringType, sqltypes.UnicodeText): +class NTEXT(sqltypes.UnicodeText): """MSSQL NTEXT type, for variable-length unicode text up to 2^30 characters.""" __visit_name__ = 'NTEXT' - def __init__(self, length=None, collation=None, **kw): - """Construct a NTEXT. - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - - """ - _StringType.__init__(self, collation) - sqltypes.UnicodeText.__init__(self, length, **kw) - - -class VARCHAR(_StringType, sqltypes.VARCHAR): - """MSSQL VARCHAR type, for variable-length non-Unicode data with a maximum - of 8,000 characters.""" - - def __init__(self, length=None, collation=None, **kw): - """Construct a VARCHAR. - - :param length: Optinal, maximum data length, in characters. - - :param convert_unicode: defaults to False. If True, convert - ``unicode`` data sent to the database to a ``str`` - bytestring, and convert bytestrings coming back from the - database into ``unicode``. - - Bytestrings are encoded using the dialect's - :attr:`~sqlalchemy.engine.base.Dialect.encoding`, which - defaults to `utf-8`. - - If False, may be overridden by - :attr:`sqlalchemy.engine.base.Dialect.convert_unicode`. - - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - - """ - _StringType.__init__(self, collation) - sqltypes.VARCHAR.__init__(self, length, **kw) - -class NVARCHAR(_StringType, sqltypes.NVARCHAR): - """MSSQL NVARCHAR type. - - For variable-length unicode character data up to 4,000 characters.""" - - def __init__(self, length=None, collation=None, **kw): - """Construct a NVARCHAR. - - :param length: Optional, Maximum data length, in characters. - - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - - """ - _StringType.__init__(self, collation) - sqltypes.NVARCHAR.__init__(self, length, **kw) - -class CHAR(_StringType, sqltypes.CHAR): - """MSSQL CHAR type, for fixed-length non-Unicode data with a maximum - of 8,000 characters.""" - - def __init__(self, length=None, collation=None, **kw): - """Construct a CHAR. - - :param length: Optinal, maximum data length, in characters. - - :param convert_unicode: defaults to False. If True, convert - ``unicode`` data sent to the database to a ``str`` - bytestring, and convert bytestrings coming back from the - database into ``unicode``. - - Bytestrings are encoded using the dialect's - :attr:`~sqlalchemy.engine.base.Dialect.encoding`, which - defaults to `utf-8`. - - If False, may be overridden by - :attr:`sqlalchemy.engine.base.Dialect.convert_unicode`. - - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - - """ - _StringType.__init__(self, collation) - sqltypes.CHAR.__init__(self, length, **kw) - -class NCHAR(_StringType, sqltypes.NCHAR): - """MSSQL NCHAR type. - - For fixed-length unicode character data up to 4,000 characters.""" - - def __init__(self, length=None, collation=None, **kw): - """Construct an NCHAR. - - :param length: Optional, Maximum data length, in characters. - - :param collation: Optional, a column-level collation for this string - value. Accepts a Windows Collation Name or a SQL Collation Name. - - """ - _StringType.__init__(self, collation) - sqltypes.NCHAR.__init__(self, length, **kw) class IMAGE(sqltypes.LargeBinary): __visit_name__ = 'IMAGE' + class BIT(sqltypes.TypeEngine): __visit_name__ = 'BIT' @@ -476,12 +400,15 @@ class BIT(sqltypes.TypeEngine): class MONEY(sqltypes.TypeEngine): __visit_name__ = 'MONEY' + class SMALLMONEY(sqltypes.TypeEngine): __visit_name__ = 'SMALLMONEY' + class UNIQUEIDENTIFIER(sqltypes.TypeEngine): __visit_name__ = "UNIQUEIDENTIFIER" + class SQL_VARIANT(sqltypes.TypeEngine): __visit_name__ = 'SQL_VARIANT' @@ -510,30 +437,30 @@ MSUniqueIdentifier = UNIQUEIDENTIFIER MSVariant = SQL_VARIANT ischema_names = { - 'int' : INTEGER, + 'int': INTEGER, 'bigint': BIGINT, - 'smallint' : SMALLINT, - 'tinyint' : TINYINT, - 'varchar' : VARCHAR, - 'nvarchar' : NVARCHAR, - 'char' : CHAR, - 'nchar' : NCHAR, - 'text' : TEXT, - 'ntext' : NTEXT, - 'decimal' : DECIMAL, - 'numeric' : NUMERIC, - 'float' : FLOAT, - 'datetime' : DATETIME, - 'datetime2' : DATETIME2, - 'datetimeoffset' : DATETIMEOFFSET, + 'smallint': SMALLINT, + 'tinyint': TINYINT, + 'varchar': VARCHAR, + 'nvarchar': NVARCHAR, + 'char': CHAR, + 'nchar': NCHAR, + 'text': TEXT, + 'ntext': NTEXT, + 'decimal': DECIMAL, + 'numeric': NUMERIC, + 'float': FLOAT, + 'datetime': DATETIME, + 'datetime2': DATETIME2, + 'datetimeoffset': DATETIMEOFFSET, 'date': DATE, 'time': TIME, - 'smalldatetime' : SMALLDATETIME, - 'binary' : BINARY, - 'varbinary' : VARBINARY, + 'smalldatetime': SMALLDATETIME, + 'binary': BINARY, + 'varbinary': VARBINARY, 'bit': BIT, - 'real' : REAL, - 'image' : IMAGE, + 'real': REAL, + 'image': IMAGE, 'timestamp': TIMESTAMP, 'money': MONEY, 'smallmoney': SMALLMONEY, @@ -609,8 +536,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler): return self._extend("TEXT", type_) def visit_VARCHAR(self, type_): - return self._extend("VARCHAR", type_, - length = type_.length or 'max') + return self._extend("VARCHAR", type_, length=type_.length or 'max') def visit_CHAR(self, type_): return self._extend("CHAR", type_) @@ -619,8 +545,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler): return self._extend("NCHAR", type_) def visit_NVARCHAR(self, type_): - return self._extend("NVARCHAR", type_, - length = type_.length or 'max') + return self._extend("NVARCHAR", type_, length=type_.length or 'max') def visit_date(self, type_): if self.dialect.server_version_info < MS_2008_VERSION: @@ -664,6 +589,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler): def visit_SQL_VARIANT(self, type_): return 'SQL_VARIANT' + class MSExecutionContext(default.DefaultExecutionContext): _enable_identity_insert = False _select_lastrowid = False @@ -693,7 +619,7 @@ class MSExecutionContext(default.DefaultExecutionContext): self.root_connection._cursor_execute(self.cursor, "SET IDENTITY_INSERT %s ON" % self.dialect.identifier_preparer.format_table(tbl), - ()) + (), self) def post_exec(self): """Disable IDENTITY_INSERT if enabled.""" @@ -702,25 +628,24 @@ class MSExecutionContext(default.DefaultExecutionContext): if self._select_lastrowid: if self.dialect.use_scope_identity: conn._cursor_execute(self.cursor, - "SELECT scope_identity() AS lastrowid", ()) + "SELECT scope_identity() AS lastrowid", (), self) else: conn._cursor_execute(self.cursor, - "SELECT @@identity AS lastrowid", ()) + "SELECT @@identity AS lastrowid", (), self) # fetchall() ensures the cursor is consumed without closing it row = self.cursor.fetchall()[0] self._lastrowid = int(row[0]) if (self.isinsert or self.isupdate or self.isdelete) and \ self.compiled.returning: - self._result_proxy = base.FullyBufferedResultProxy(self) + self._result_proxy = engine.FullyBufferedResultProxy(self) if self._enable_identity_insert: conn._cursor_execute(self.cursor, "SET IDENTITY_INSERT %s OFF" % self.dialect.identifier_preparer. format_table(self.compiled.statement.table), - () - ) + (), self) def get_lastrowid(self): return self._lastrowid @@ -740,7 +665,8 @@ class MSExecutionContext(default.DefaultExecutionContext): if self._result_proxy: return self._result_proxy else: - return base.ResultProxy(self) + return engine.ResultProxy(self) + class MSSQLCompiler(compiler.SQLCompiler): returning_precedes_values = True @@ -770,12 +696,18 @@ class MSSQLCompiler(compiler.SQLCompiler): def visit_char_length_func(self, fn, **kw): return "LEN%s" % self.function_argspec(fn, **kw) - def visit_concat_op(self, binary, **kw): + def visit_concat_op_binary(self, binary, operator, **kw): return "%s + %s" % \ (self.process(binary.left, **kw), self.process(binary.right, **kw)) - def visit_match_op(self, binary, **kw): + def visit_true(self, expr, **kw): + return '1' + + def visit_false(self, expr, **kw): + return '0' + + def visit_match_op_binary(self, binary, operator, **kw): return "CONTAINS (%s, %s)" % ( self.process(binary.left, **kw), self.process(binary.right, **kw)) @@ -843,8 +775,8 @@ class MSSQLCompiler(compiler.SQLCompiler): else: return None - def visit_table(self, table, mssql_aliased=False, **kwargs): - if mssql_aliased is table: + def visit_table(self, table, mssql_aliased=False, iscrud=False, **kwargs): + if mssql_aliased is table or iscrud: return super(MSSQLCompiler, self).visit_table(table, **kwargs) # alias schema-qualified tables @@ -871,7 +803,7 @@ class MSSQLCompiler(compiler.SQLCompiler): return ("ROLLBACK TRANSACTION %s" % self.preparer.format_savepoint(savepoint_stmt)) - def visit_column(self, column, result_map=None, **kwargs): + def visit_column(self, column, add_to_result_map=None, **kwargs): if column.table is not None and \ (not self.isupdate and not self.isdelete) or self.is_subquery(): # translate for schema-qualified table aliases @@ -879,20 +811,19 @@ class MSSQLCompiler(compiler.SQLCompiler): if t is not None: converted = expression._corresponding_column_or_error( t, column) - - if result_map is not None: - result_map[column.name.lower()] = \ - (column.name, (column, column.name, - column.key), - column.type) + if add_to_result_map is not None: + add_to_result_map( + column.name, + column.name, + (column, column.name, column.key), + column.type + ) return super(MSSQLCompiler, self).\ - visit_column(converted, - result_map=None, **kwargs) + visit_column(converted, **kwargs) - return super(MSSQLCompiler, self).visit_column(column, - result_map=result_map, - **kwargs) + return super(MSSQLCompiler, self).visit_column( + column, add_to_result_map=add_to_result_map, **kwargs) def visit_binary(self, binary, **kwargs): """Move bind parameters to the right-hand side of an operator, where @@ -900,40 +831,16 @@ class MSSQLCompiler(compiler.SQLCompiler): """ if ( - isinstance(binary.left, expression._BindParamClause) + isinstance(binary.left, expression.BindParameter) and binary.operator == operator.eq - and not isinstance(binary.right, expression._BindParamClause) + and not isinstance(binary.right, expression.BindParameter) ): return self.process( - expression._BinaryExpression(binary.right, + expression.BinaryExpression(binary.right, binary.left, binary.operator), **kwargs) - else: - if ( - (binary.operator is operator.eq or - binary.operator is operator.ne) - and ( - (isinstance(binary.left, expression._FromGrouping) - and isinstance(binary.left.element, - expression._ScalarSelect)) - or (isinstance(binary.right, expression._FromGrouping) - and isinstance(binary.right.element, - expression._ScalarSelect)) - or isinstance(binary.left, expression._ScalarSelect) - or isinstance(binary.right, expression._ScalarSelect) - ) - ): - op = binary.operator == operator.eq and "IN" or "NOT IN" - util.warn_deprecated("Comparing a scalar select using ``=``/``!=`` will " - "no longer produce IN/NOT IN in 0.8. To remove this " - "behavior immediately, use the recipe at " - "http://www.sqlalchemy.org/docs/07/dialects/mssql.html#scalar-select-comparisons") - return self.process( - expression._BinaryExpression(binary.left, - binary.right, op), - **kwargs) - return super(MSSQLCompiler, self).visit_binary(binary, **kwargs) + return super(MSSQLCompiler, self).visit_binary(binary, **kwargs) def returning_clause(self, stmt, returning_cols): @@ -943,21 +850,13 @@ class MSSQLCompiler(compiler.SQLCompiler): target = stmt.table.alias("deleted") adapter = sql_util.ClauseAdapter(target) - def col_label(col): - adapted = adapter.traverse(col) - if isinstance(col, expression._Label): - return adapted.label(c.key) - else: - return self.label_select_column(None, adapted, asfrom=False) columns = [ - self.process( - col_label(c), - within_columns_clause=True, - result_map=self.result_map - ) - for c in expression._select_iterables(returning_cols) - ] + self._label_select_column(None, adapter.traverse(c), + True, False, {}) + for c in expression._select_iterables(returning_cols) + ] + return 'OUTPUT ' + ', '.join(columns) def get_cte_preamble(self, recursive): @@ -1004,6 +903,7 @@ class MSSQLCompiler(compiler.SQLCompiler): fromhints=from_hints, **kw) for t in [from_table] + extra_froms) + class MSSQLStrictCompiler(MSSQLCompiler): """A subclass of MSSQLCompiler which disables the usage of bind parameters where not allowed natively by MS-SQL. @@ -1014,24 +914,20 @@ class MSSQLStrictCompiler(MSSQLCompiler): """ ansi_bind_rules = True - def visit_in_op(self, binary, **kw): + def visit_in_op_binary(self, binary, operator, **kw): kw['literal_binds'] = True return "%s IN %s" % ( self.process(binary.left, **kw), self.process(binary.right, **kw) ) - def visit_notin_op(self, binary, **kw): + def visit_notin_op_binary(self, binary, operator, **kw): kw['literal_binds'] = True return "%s NOT IN %s" % ( self.process(binary.left, **kw), self.process(binary.right, **kw) ) - def visit_function(self, func, **kw): - kw['literal_binds'] = True - return super(MSSQLStrictCompiler, self).visit_function(func, **kw) - def render_literal_value(self, value, type_): """ For date and datetime values, convert to a string @@ -1051,13 +947,15 @@ class MSSQLStrictCompiler(MSSQLCompiler): return super(MSSQLStrictCompiler, self).\ render_literal_value(value, type_) + class MSDDLCompiler(compiler.DDLCompiler): def get_column_specification(self, column, **kwargs): colspec = (self.preparer.format_column(column) + " " + self.dialect.type_compiler.process(column.type)) if column.nullable is not None: - if not column.nullable or column.primary_key: + if not column.nullable or column.primary_key or \ + isinstance(column.default, sa_schema.Sequence): colspec += " NOT NULL" else: colspec += " NULL" @@ -1067,18 +965,16 @@ class MSDDLCompiler(compiler.DDLCompiler): "mssql requires Table-bound columns " "in order to generate DDL") - seq_col = column.table._autoincrement_column - - # install a IDENTITY Sequence if we have an implicit IDENTITY column - if seq_col is column: - sequence = isinstance(column.default, sa_schema.Sequence) and \ - column.default - if sequence: - start, increment = sequence.start or 1, \ - sequence.increment or 1 + # install an IDENTITY Sequence if we either a sequence or an implicit IDENTITY column + if isinstance(column.default, sa_schema.Sequence): + if column.default.start == 0: + start = 0 else: - start, increment = 1, 1 - colspec += " IDENTITY(%s,%s)" % (start, increment) + start = column.default.start or 1 + + colspec += " IDENTITY(%s,%s)" % (start, column.default.increment or 1) + elif column is column.table._autoincrement_column: + colspec += " IDENTITY(1,1)" else: default = self.get_column_default_string(column) if default is not None: @@ -1086,14 +982,46 @@ class MSDDLCompiler(compiler.DDLCompiler): return colspec - def visit_drop_index(self, drop): - return "\nDROP INDEX %s.%s" % ( - self.preparer.quote_identifier(drop.element.table.name), - self.preparer.quote( - self._index_identifier(drop.element.name), - drop.element.quote) - ) + def visit_create_index(self, create, include_schema=False): + index = create.element + self._verify_index_table(index) + preparer = self.preparer + text = "CREATE " + if index.unique: + text += "UNIQUE " + # handle clustering option + if index.kwargs.get("mssql_clustered"): + text += "CLUSTERED " + + text += "INDEX %s ON %s (%s)" \ + % ( + self._prepared_index_name(index, + include_schema=include_schema), + preparer.format_table(index.table), + ', '.join( + self.sql_compiler.process(expr, + include_table=False, literal_binds=True) for + expr in index.expressions) + ) + + # handle other included columns + if index.kwargs.get("mssql_include"): + inclusions = [index.table.c[col] + if isinstance(col, util.string_types) else col + for col in index.kwargs["mssql_include"]] + + text += " INCLUDE (%s)" \ + % ', '.join([preparer.quote(c.name) + for c in inclusions]) + + return text + + def visit_drop_index(self, drop): + return "\nDROP INDEX %s ON %s" % ( + self._prepared_index_name(drop.element, include_schema=False), + self.preparer.format_table(drop.element.table) + ) class MSIdentifierPreparer(compiler.IdentifierPreparer): reserved_words = RESERVED_WORDS @@ -1105,11 +1033,48 @@ class MSIdentifierPreparer(compiler.IdentifierPreparer): def _escape_identifier(self, value): return value - def quote_schema(self, schema, force=True): + def quote_schema(self, schema, force=None): """Prepare a quoted table and schema name.""" result = '.'.join([self.quote(x, force) for x in schema.split('.')]) return result + +def _db_plus_owner_listing(fn): + def wrap(dialect, connection, schema=None, **kw): + dbname, owner = _owner_plus_db(dialect, schema) + return _switch_db(dbname, connection, fn, dialect, connection, + dbname, owner, schema, **kw) + return update_wrapper(wrap, fn) + + +def _db_plus_owner(fn): + def wrap(dialect, connection, tablename, schema=None, **kw): + dbname, owner = _owner_plus_db(dialect, schema) + return _switch_db(dbname, connection, fn, dialect, connection, + tablename, dbname, owner, schema, **kw) + return update_wrapper(wrap, fn) + + +def _switch_db(dbname, connection, fn, *arg, **kw): + if dbname: + current_db = connection.scalar("select db_name()") + connection.execute("use %s" % dbname) + try: + return fn(*arg, **kw) + finally: + if dbname: + connection.execute("use %s" % current_db) + + +def _owner_plus_db(dialect, schema): + if not schema: + return None, dialect.default_schema_name + elif "." in schema: + return schema.split(".", 1) + else: + return None, schema + + class MSDialect(default.DefaultDialect): name = 'mssql' supports_default_values = True @@ -1120,9 +1085,9 @@ class MSDialect(default.DefaultDialect): schema_name = "dbo" colspecs = { - sqltypes.DateTime : _MSDateTime, - sqltypes.Date : _MSDate, - sqltypes.Time : TIME, + sqltypes.DateTime: _MSDateTime, + sqltypes.Date: _MSDate, + sqltypes.Time: TIME, } ischema_names = ischema_names @@ -1142,7 +1107,7 @@ class MSDialect(default.DefaultDialect): query_timeout=None, use_scope_identity=True, max_identifier_length=None, - schema_name=u"dbo", **opts): + schema_name="dbo", **opts): self.query_timeout = int(query_timeout or 0) self.schema_name = schema_name @@ -1162,7 +1127,7 @@ class MSDialect(default.DefaultDialect): def initialize(self, connection): super(MSDialect, self).initialize(connection) - if self.server_version_info[0] not in range(8, 17): + if self.server_version_info[0] not in list(range(8, 17)): # FreeTDS with version 4.2 seems to report here # a number like "95.10.255". Don't know what # that is. So emit warning. @@ -1171,13 +1136,13 @@ class MSDialect(default.DefaultDialect): "behaviors may not function properly. If using ODBC " "with FreeTDS, ensure server version 7.0 or 8.0, not 4.2, " "is configured in the FreeTDS configuration." % - ".".join(str(x) for x in self.server_version_info) ) + ".".join(str(x) for x in self.server_version_info)) if self.server_version_info >= MS_2005_VERSION and \ 'implicit_returning' not in self.__dict__: self.implicit_returning = True def _get_default_schema_name(self, connection): - user_name = connection.scalar("SELECT user_name() as user_name;") + user_name = connection.scalar("SELECT user_name()") if user_name is not None: # now, get the default schema query = sql.text(""" @@ -1189,25 +1154,20 @@ class MSDialect(default.DefaultDialect): try: default_schema_name = connection.scalar(query, name=user_name) if default_schema_name is not None: - return unicode(default_schema_name) + return util.text_type(default_schema_name) except: pass return self.schema_name - def _unicode_cast(self, column): - if self.server_version_info >= MS_2005_VERSION: - return cast(column, NVARCHAR(_warn_on_bytestring=False)) - else: - return column - - def has_table(self, connection, tablename, schema=None): - current_schema = schema or self.default_schema_name + @_db_plus_owner + def has_table(self, connection, tablename, dbname, owner, schema): columns = ischema.columns - whereclause = self._unicode_cast(columns.c.table_name)==tablename - if current_schema: + whereclause = columns.c.table_name == tablename + + if owner: whereclause = sql.and_(whereclause, - columns.c.table_schema==current_schema) + columns.c.table_schema == owner) s = sql.select([columns], whereclause) c = connection.execute(s) return c.first() is not None @@ -1221,13 +1181,13 @@ class MSDialect(default.DefaultDialect): return schema_names @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - current_schema = schema or self.default_schema_name + @_db_plus_owner_listing + def get_table_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables s = sql.select([tables.c.table_name], sql.and_( - tables.c.table_schema == current_schema, - tables.c.table_type == u'BASE TABLE' + tables.c.table_schema == owner, + tables.c.table_type == 'BASE TABLE' ), order_by=[tables.c.table_name] ) @@ -1235,13 +1195,13 @@ class MSDialect(default.DefaultDialect): return table_names @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - current_schema = schema or self.default_schema_name + @_db_plus_owner_listing + def get_view_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables s = sql.select([tables.c.table_name], sql.and_( - tables.c.table_schema == current_schema, - tables.c.table_type == u'VIEW' + tables.c.table_schema == owner, + tables.c.table_type == 'VIEW' ), order_by=[tables.c.table_name] ) @@ -1249,15 +1209,13 @@ class MSDialect(default.DefaultDialect): return view_names @reflection.cache - def get_indexes(self, connection, tablename, schema=None, **kw): + @_db_plus_owner + def get_indexes(self, connection, tablename, dbname, owner, schema, **kw): # using system catalogs, don't support index reflection # below MS 2005 if self.server_version_info < MS_2005_VERSION: return [] - current_schema = schema or self.default_schema_name - full_tname = "%s.%s" % (current_schema, tablename) - rp = connection.execute( sql.text("select ind.index_id, ind.is_unique, ind.name " "from sys.indexes as ind join sys.tables as tab on " @@ -1269,20 +1227,20 @@ class MSDialect(default.DefaultDialect): bindparams=[ sql.bindparam('tabname', tablename, sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', current_schema, + sql.bindparam('schname', owner, sqltypes.String(convert_unicode=True)) ], - typemap = { - 'name':sqltypes.Unicode() + typemap={ + 'name': sqltypes.Unicode() } ) ) indexes = {} for row in rp: indexes[row['index_id']] = { - 'name':row['name'], - 'unique':row['is_unique'] == 1, - 'column_names':[] + 'name': row['name'], + 'unique': row['is_unique'] == 1, + 'column_names': [] } rp = connection.execute( sql.text( @@ -1298,24 +1256,21 @@ class MSDialect(default.DefaultDialect): bindparams=[ sql.bindparam('tabname', tablename, sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', current_schema, + sql.bindparam('schname', owner, sqltypes.String(convert_unicode=True)) ], - typemap = { - 'name':sqltypes.Unicode() - } + typemap={'name': sqltypes.Unicode()} ), ) for row in rp: if row['index_id'] in indexes: indexes[row['index_id']]['column_names'].append(row['name']) - return indexes.values() + return list(indexes.values()) @reflection.cache - def get_view_definition(self, connection, viewname, schema=None, **kw): - current_schema = schema or self.default_schema_name - + @_db_plus_owner + def get_view_definition(self, connection, viewname, dbname, owner, schema, **kw): rp = connection.execute( sql.text( "select definition from sys.sql_modules as mod, " @@ -1328,7 +1283,7 @@ class MSDialect(default.DefaultDialect): bindparams=[ sql.bindparam('viewname', viewname, sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', current_schema, + sql.bindparam('schname', owner, sqltypes.String(convert_unicode=True)) ] ) @@ -1339,17 +1294,18 @@ class MSDialect(default.DefaultDialect): return view_def @reflection.cache - def get_columns(self, connection, tablename, schema=None, **kw): + @_db_plus_owner + def get_columns(self, connection, tablename, dbname, owner, schema, **kw): # Get base columns - current_schema = schema or self.default_schema_name columns = ischema.columns - if current_schema: - whereclause = sql.and_(columns.c.table_name==tablename, - columns.c.table_schema==current_schema) + if owner: + whereclause = sql.and_(columns.c.table_name == tablename, + columns.c.table_schema == owner) else: - whereclause = columns.c.table_name==tablename + whereclause = columns.c.table_name == tablename s = sql.select([columns], whereclause, order_by=[columns.c.ordinal_position]) + c = connection.execute(s) cols = [] while True: @@ -1393,11 +1349,11 @@ class MSDialect(default.DefaultDialect): coltype = coltype(**kwargs) cdict = { - 'name' : name, - 'type' : coltype, - 'nullable' : nullable, - 'default' : default, - 'autoincrement':False, + 'name': name, + 'type': coltype, + 'nullable': nullable, + 'default': default, + 'autoincrement': False, } cols.append(cdict) # autoincrement and identity @@ -1407,7 +1363,7 @@ class MSDialect(default.DefaultDialect): # We also run an sp_columns to check for identity columns: cursor = connection.execute("sp_columns @table_name = '%s', " "@table_owner = '%s'" - % (tablename, current_schema)) + % (tablename, owner)) ic = None while True: row = cursor.fetchone() @@ -1423,7 +1379,7 @@ class MSDialect(default.DefaultDialect): cursor.close() if ic is not None and self.server_version_info >= MS_2005_VERSION: - table_fullname = "%s.%s" % (current_schema, tablename) + table_fullname = "%s.%s" % (owner, tablename) cursor = connection.execute( "select ident_seed('%s'), ident_incr('%s')" % (table_fullname, table_fullname) @@ -1432,53 +1388,40 @@ class MSDialect(default.DefaultDialect): row = cursor.first() if row is not None and row[0] is not None: colmap[ic]['sequence'].update({ - 'start' : int(row[0]), - 'increment' : int(row[1]) + 'start': int(row[0]), + 'increment': int(row[1]) }) return cols @reflection.cache - def get_primary_keys(self, connection, tablename, schema=None, **kw): - current_schema = schema or self.default_schema_name + @_db_plus_owner + def get_pk_constraint(self, connection, tablename, dbname, owner, schema, **kw): pkeys = [] - # information_schema.referential_constraints - RR = ischema.ref_constraints - # information_schema.table_constraints TC = ischema.constraints - # information_schema.constraint_column_usage: - # the constrained column - C = ischema.key_constraints.alias('C') - # information_schema.constraint_column_usage: - # the referenced column - R = ischema.key_constraints.alias('R') + C = ischema.key_constraints.alias('C') # Primary key constraints - s = sql.select([C.c.column_name, TC.c.constraint_type], + s = sql.select([C.c.column_name, TC.c.constraint_type, C.c.constraint_name], sql.and_(TC.c.constraint_name == C.c.constraint_name, - TC.c.table_schema == C.c.table_schema, + TC.c.table_schema == C.c.table_schema, C.c.table_name == tablename, - C.c.table_schema == current_schema) + C.c.table_schema == owner) ) c = connection.execute(s) + constraint_name = None for row in c: if 'PRIMARY' in row[TC.c.constraint_type.name]: pkeys.append(row[0]) - return pkeys + if constraint_name is None: + constraint_name = row[C.c.constraint_name.name] + return {'constrained_columns': pkeys, 'name': constraint_name} @reflection.cache - def get_foreign_keys(self, connection, tablename, schema=None, **kw): - current_schema = schema or self.default_schema_name - # Add constraints - #information_schema.referential_constraints + @_db_plus_owner + def get_foreign_keys(self, connection, tablename, dbname, owner, schema, **kw): RR = ischema.ref_constraints - # information_schema.table_constraints - TC = ischema.constraints - # information_schema.constraint_column_usage: - # the constrained column - C = ischema.key_constraints.alias('C') - # information_schema.constraint_column_usage: - # the referenced column - R = ischema.key_constraints.alias('R') + C = ischema.key_constraints.alias('C') + R = ischema.key_constraints.alias('R') # Foreign key constraints s = sql.select([C.c.column_name, @@ -1487,16 +1430,14 @@ class MSDialect(default.DefaultDialect): RR.c.update_rule, RR.c.delete_rule], sql.and_(C.c.table_name == tablename, - C.c.table_schema == current_schema, + C.c.table_schema == owner, C.c.constraint_name == RR.c.constraint_name, R.c.constraint_name == RR.c.unique_constraint_name, C.c.ordinal_position == R.c.ordinal_position ), - order_by = [ - RR.c.constraint_name, - R.c.ordinal_position]) - + order_by=[RR.c.constraint_name, R.c.ordinal_position] + ) # group rows by constraint ID, to handle multi-column FKs fkeys = [] @@ -1504,11 +1445,11 @@ class MSDialect(default.DefaultDialect): def fkey_rec(): return { - 'name' : None, - 'constrained_columns' : [], - 'referred_schema' : None, - 'referred_table' : None, - 'referred_columns' : [] + 'name': None, + 'constrained_columns': [], + 'referred_schema': None, + 'referred_table': None, + 'referred_columns': [] } fkeys = util.defaultdict(fkey_rec) @@ -1520,8 +1461,9 @@ class MSDialect(default.DefaultDialect): rec['name'] = rfknm if not rec['referred_table']: rec['referred_table'] = rtbl - - if schema is not None or current_schema != rschema: + if schema is not None or owner != rschema: + if dbname: + rschema = dbname + "." + rschema rec['referred_schema'] = rschema local_cols, remote_cols = \ @@ -1531,5 +1473,4 @@ class MSDialect(default.DefaultDialect): local_cols.append(scol) remote_cols.append(rcol) - return fkeys.values() - + return list(fkeys.values()) diff --git a/libs/sqlalchemy/dialects/mssql/information_schema.py b/libs/sqlalchemy/dialects/mssql/information_schema.py index 0dcddae9..26e70f7f 100644 --- a/libs/sqlalchemy/dialects/mssql/information_schema.py +++ b/libs/sqlalchemy/dialects/mssql/information_schema.py @@ -1,13 +1,17 @@ # mssql/information_schema.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php # TODO: should be using the sys. catalog with SQL Server, not information schema -from sqlalchemy import Table, MetaData, Column -from sqlalchemy.types import String, Unicode, Integer, TypeDecorator +from ... import Table, MetaData, Column +from ...types import String, Unicode, UnicodeText, Integer, TypeDecorator +from ... import cast +from ... import util +from ...sql import expression +from ...ext.compiler import compiles ischema = MetaData() @@ -15,12 +19,25 @@ class CoerceUnicode(TypeDecorator): impl = Unicode def process_bind_param(self, value, dialect): - # Py2K - if isinstance(value, str): + if util.py2k and isinstance(value, util.binary_type): value = value.decode(dialect.encoding) - # end Py2K return value + def bind_expression(self, bindvalue): + return _cast_on_2005(bindvalue) + +class _cast_on_2005(expression.ColumnElement): + def __init__(self, bindvalue): + self.bindvalue = bindvalue + +@compiles(_cast_on_2005) +def _compile(element, compiler, **kw): + from . import base + if compiler.dialect.server_version_info < base.MS_2005_VERSION: + return compiler.process(element.bindvalue, **kw) + else: + return compiler.process(cast(element.bindvalue, Unicode), **kw) + schemata = Table("SCHEMATA", ischema, Column("CATALOG_NAME", CoerceUnicode, key="catalog_name"), Column("SCHEMA_NAME", CoerceUnicode, key="schema_name"), @@ -95,4 +112,3 @@ views = Table("VIEWS", ischema, Column("CHECK_OPTION", String, key="check_option"), Column("IS_UPDATABLE", String, key="is_updatable"), schema="INFORMATION_SCHEMA") - diff --git a/libs/sqlalchemy/dialects/mssql/mxodbc.py b/libs/sqlalchemy/dialects/mssql/mxodbc.py index 56a72f41..5b686c47 100644 --- a/libs/sqlalchemy/dialects/mssql/mxodbc.py +++ b/libs/sqlalchemy/dialects/mssql/mxodbc.py @@ -1,28 +1,18 @@ # mssql/mxodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for MS-SQL via mxODBC. - -mxODBC is available at: - - http://www.egenix.com/ - -This was tested with mxODBC 3.1.2 and the SQL Server Native -Client connected to MSSQL 2005 and 2008 Express Editions. - -Connecting -~~~~~~~~~~ - -Connection is via DSN:: - - mssql+mxodbc://:@ +.. dialect:: mssql+mxodbc + :name: mxODBC + :dbapi: mxodbc + :connectstring: mssql+mxodbc://:@ + :url: http://www.egenix.com/ Execution Modes -~~~~~~~~~~~~~~~ +--------------- mxODBC features two styles of statement execution, using the ``cursor.execute()`` and ``cursor.executedirect()`` methods (the second being @@ -52,14 +42,38 @@ of ``False`` will unconditionally use string-escaped parameters. """ -from sqlalchemy import types as sqltypes -from sqlalchemy.connectors.mxodbc import MxODBCConnector -from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc -from sqlalchemy.dialects.mssql.base import (MSDialect, +from ... import types as sqltypes +from ...connectors.mxodbc import MxODBCConnector +from .pyodbc import MSExecutionContext_pyodbc, _MSNumeric_pyodbc +from .base import (MSDialect, MSSQLStrictCompiler, - _MSDateTime, _MSDate, TIME) + _MSDateTime, _MSDate, _MSTime) +class _MSNumeric_mxodbc(_MSNumeric_pyodbc): + """Include pyodbc's numeric processor. + """ + + +class _MSDate_mxodbc(_MSDate): + def bind_processor(self, dialect): + def process(value): + if value is not None: + return "%s-%s-%s" % (value.year, value.month, value.day) + else: + return None + return process + + +class _MSTime_mxodbc(_MSTime): + def bind_processor(self, dialect): + def process(value): + if value is not None: + return "%s:%s:%s" % (value.hour, value.minute, value.second) + else: + return None + return process + class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc): """ @@ -71,23 +85,27 @@ class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc): # is really only being used in cases where OUTPUT # won't work. + class MSDialect_mxodbc(MxODBCConnector, MSDialect): - # TODO: may want to use this only if FreeTDS is not in use, - # since FreeTDS doesn't seem to use native binds. - statement_compiler = MSSQLStrictCompiler + # this is only needed if "native ODBC" mode is used, + # which is now disabled by default. + #statement_compiler = MSSQLStrictCompiler + execution_ctx_cls = MSExecutionContext_mxodbc + + # flag used by _MSNumeric_mxodbc + _need_decimal_fix = True + colspecs = { - #sqltypes.Numeric : _MSNumeric, - sqltypes.DateTime : _MSDateTime, - sqltypes.Date : _MSDate, - sqltypes.Time : TIME, + sqltypes.Numeric: _MSNumeric_mxodbc, + sqltypes.DateTime: _MSDateTime, + sqltypes.Date: _MSDate_mxodbc, + sqltypes.Time: _MSTime_mxodbc, } - - def __init__(self, description_encoding='latin-1', **params): + def __init__(self, description_encoding=None, **params): super(MSDialect_mxodbc, self).__init__(**params) self.description_encoding = description_encoding dialect = MSDialect_mxodbc - diff --git a/libs/sqlalchemy/dialects/mssql/pymssql.py b/libs/sqlalchemy/dialects/mssql/pymssql.py index 8229d6ce..021219cb 100644 --- a/libs/sqlalchemy/dialects/mssql/pymssql.py +++ b/libs/sqlalchemy/dialects/mssql/pymssql.py @@ -1,32 +1,18 @@ # mssql/pymssql.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for the pymssql dialect. - -This dialect supports pymssql 1.0 and greater. - -pymssql is available at: - - http://pymssql.sourceforge.net/ - -Connecting -^^^^^^^^^^ - -Sample connect string:: - - mssql+pymssql://:@ - -Adding "?charset=utf8" or similar will cause pymssql to return -strings as Python unicode objects. This can potentially improve -performance in some scenarios as decoding of strings is -handled natively. +.. dialect:: mssql+pymssql + :name: pymssql + :dbapi: pymssql + :connectstring: mssql+pymssql://:@?charset=utf8 + :url: http://pymssql.sourceforge.net/ Limitations -^^^^^^^^^^^ +----------- pymssql inherits a lot of limitations from FreeTDS, including: @@ -38,10 +24,11 @@ pymssql inherits a lot of limitations from FreeTDS, including: Please consult the pymssql documentation for further information. """ -from sqlalchemy.dialects.mssql.base import MSDialect -from sqlalchemy import types as sqltypes, util, processors +from .base import MSDialect +from ... import types as sqltypes, util, processors import re + class _MSNumeric_pymssql(sqltypes.Numeric): def result_processor(self, dialect, type_): if not self.asdecimal: @@ -49,6 +36,7 @@ class _MSNumeric_pymssql(sqltypes.Numeric): else: return sqltypes.Numeric.result_processor(self, dialect, type_) + class MSDialect_pymssql(MSDialect): supports_sane_rowcount = False driver = 'pymssql' @@ -56,16 +44,17 @@ class MSDialect_pymssql(MSDialect): colspecs = util.update_copy( MSDialect.colspecs, { - sqltypes.Numeric:_MSNumeric_pymssql, - sqltypes.Float:sqltypes.Float, + sqltypes.Numeric: _MSNumeric_pymssql, + sqltypes.Float: sqltypes.Float, } ) + @classmethod def dbapi(cls): module = __import__('pymssql') # pymmsql doesn't have a Binary method. we use string # TODO: monkeypatching here is less than ideal - module.Binary = str + module.Binary = lambda x: x if hasattr(x, 'decode') else str(x) client_ver = tuple(int(x) for x in module.__version__.split(".")) if client_ver < (1, ): @@ -96,6 +85,9 @@ class MSDialect_pymssql(MSDialect): def is_disconnect(self, e, connection, cursor): for msg in ( + "Adaptive Server connection timed out", + "Net-Lib error during Connection reset by peer", + "message 20003", # connection timeout "Error 10054", "Not connected to any MS SQL server", "Connection is closed" diff --git a/libs/sqlalchemy/dialects/mssql/pyodbc.py b/libs/sqlalchemy/dialects/mssql/pyodbc.py index 389018c6..8c43eb8a 100644 --- a/libs/sqlalchemy/dialects/mssql/pyodbc.py +++ b/libs/sqlalchemy/dialects/mssql/pyodbc.py @@ -1,18 +1,18 @@ # mssql/pyodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for MS-SQL via pyodbc. +.. dialect:: mssql+pyodbc + :name: PyODBC + :dbapi: pyodbc + :connectstring: mssql+pyodbc://:@ + :url: http://pypi.python.org/pypi/pyodbc/ -pyodbc is available at: - - http://pypi.python.org/pypi/pyodbc/ - -Connecting -^^^^^^^^^^ +Additional Connection Examples +------------------------------- Examples of pyodbc connection string URLs: @@ -81,7 +81,7 @@ the python shell. For example:: 'dsn%3Dmydsn%3BDatabase%3Ddb' Unicode Binds -^^^^^^^^^^^^^ +------------- The current state of PyODBC on a unix backend with FreeTDS and/or EasySoft is poor regarding unicode; different OS platforms and versions of UnixODBC @@ -111,23 +111,23 @@ for unix + PyODBC. """ -from sqlalchemy.dialects.mssql.base import MSExecutionContext, MSDialect -from sqlalchemy.connectors.pyodbc import PyODBCConnector -from sqlalchemy import types as sqltypes, util +from .base import MSExecutionContext, MSDialect +from ...connectors.pyodbc import PyODBCConnector +from ... import types as sqltypes, util import decimal -class _MSNumeric_pyodbc(sqltypes.Numeric): +class _ms_numeric_pyodbc(object): + """Turns Decimals with adjusted() < 0 or > 7 into strings. - This is the only method that is proven to work with Pyodbc+MSSQL - without crashing (floats can be used but seem to cause sporadic - crashes). + The routines here are needed for older pyodbc versions + as well as current mxODBC versions. """ def bind_processor(self, dialect): - super_process = super(_MSNumeric_pyodbc, self).\ + super_process = super(_ms_numeric_pyodbc, self).\ bind_processor(dialect) if not dialect._need_decimal_fix: @@ -164,7 +164,7 @@ class _MSNumeric_pyodbc(sqltypes.Numeric): result = "%s%s%s" % ( (value < 0 and '-' or ''), "".join([str(s) for s in _int]), - "0" * (value.adjusted() - (len(_int)-1))) + "0" * (value.adjusted() - (len(_int) - 1))) else: if (len(_int) - 1) > value.adjusted(): result = "%s%s.%s" % ( @@ -180,6 +180,11 @@ class _MSNumeric_pyodbc(sqltypes.Numeric): [str(s) for s in _int][0:value.adjusted() + 1])) return result +class _MSNumeric_pyodbc(_ms_numeric_pyodbc, sqltypes.Numeric): + pass + +class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float): + pass class MSExecutionContext_pyodbc(MSExecutionContext): _embedded_scope_identity = False @@ -219,7 +224,7 @@ class MSExecutionContext_pyodbc(MSExecutionContext): # without closing it (FreeTDS particularly) row = self.cursor.fetchall()[0] break - except self.dialect.dbapi.Error, e: + except self.dialect.dbapi.Error as e: # no way around this - nextset() consumes the previous set # so we need to just keep flipping self.cursor.nextset() @@ -238,11 +243,12 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect): colspecs = util.update_copy( MSDialect.colspecs, { - sqltypes.Numeric:_MSNumeric_pyodbc + sqltypes.Numeric: _MSNumeric_pyodbc, + sqltypes.Float: _MSFloat_pyodbc } ) - def __init__(self, description_encoding='latin-1', **params): + def __init__(self, description_encoding=None, **params): super(MSDialect_pyodbc, self).__init__(**params) self.description_encoding = description_encoding self.use_scope_identity = self.use_scope_identity and \ diff --git a/libs/sqlalchemy/dialects/mssql/zxjdbc.py b/libs/sqlalchemy/dialects/mssql/zxjdbc.py index 842225da..706eef3a 100644 --- a/libs/sqlalchemy/dialects/mssql/zxjdbc.py +++ b/libs/sqlalchemy/dialects/mssql/zxjdbc.py @@ -1,32 +1,22 @@ # mssql/zxjdbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Microsoft SQL Server database via the zxjdbc JDBC -connector. +""" +.. dialect:: mssql+zxjdbc + :name: zxJDBC for Jython + :dbapi: zxjdbc + :connectstring: mssql+zxjdbc://user:pass@host:port/dbname[?key=value&key=value...] + :driverurl: http://jtds.sourceforge.net/ -JDBC Driver ------------ - -Requires the jTDS driver, available from: http://jtds.sourceforge.net/ - -Connecting ----------- - -URLs are of the standard form of -``mssql+zxjdbc://user:pass@host:port/dbname[?key=value&key=value...]``. - -Additional arguments which may be specified either as query string -arguments on the URL, or as keyword arguments to -:func:`~sqlalchemy.create_engine()` will be passed as Connection -properties to the underlying JDBC driver. """ -from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector -from sqlalchemy.dialects.mssql.base import MSDialect, MSExecutionContext -from sqlalchemy.engine import base +from ...connectors.zxJDBC import ZxJDBCConnector +from .base import MSDialect, MSExecutionContext +from ... import engine + class MSExecutionContext_zxjdbc(MSExecutionContext): @@ -46,13 +36,13 @@ class MSExecutionContext_zxjdbc(MSExecutionContext): try: row = self.cursor.fetchall()[0] break - except self.dialect.dbapi.Error, e: + except self.dialect.dbapi.Error: self.cursor.nextset() self._lastrowid = int(row[0]) if (self.isinsert or self.isupdate or self.isdelete) and \ self.compiled.returning: - self._result_proxy = base.FullyBufferedResultProxy(self) + self._result_proxy = engine.FullyBufferedResultProxy(self) if self._enable_identity_insert: table = self.dialect.identifier_preparer.format_table( diff --git a/libs/sqlalchemy/dialects/mysql/__init__.py b/libs/sqlalchemy/dialects/mysql/__init__.py index c41dd0b1..4eb8cc6d 100644 --- a/libs/sqlalchemy/dialects/mysql/__init__.py +++ b/libs/sqlalchemy/dialects/mysql/__init__.py @@ -1,17 +1,17 @@ # mysql/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.dialects.mysql import base, mysqldb, oursql, \ +from . import base, mysqldb, oursql, \ pyodbc, zxjdbc, mysqlconnector, pymysql,\ - gaerdbms + gaerdbms, cymysql # default dialect base.dialect = mysqldb.dialect -from sqlalchemy.dialects.mysql.base import \ +from .base import \ BIGINT, BINARY, BIT, BLOB, BOOLEAN, CHAR, DATE, DATETIME, \ DECIMAL, DOUBLE, ENUM, DECIMAL,\ FLOAT, INTEGER, INTEGER, LONGBLOB, LONGTEXT, MEDIUMBLOB, \ diff --git a/libs/sqlalchemy/dialects/mysql/base.py b/libs/sqlalchemy/dialects/mysql/base.py index ea180eee..a3942e89 100644 --- a/libs/sqlalchemy/dialects/mysql/base.py +++ b/libs/sqlalchemy/dialects/mysql/base.py @@ -1,10 +1,13 @@ # mysql/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database. +""" + +.. dialect:: mysql + :name: MySQL Supported Versions and Features ------------------------------- @@ -17,10 +20,7 @@ example, they won't work in SQLAlchemy either. See the official MySQL documentation for detailed information about features supported in any given server release. -Connecting ----------- - -See the API documentation on individual drivers for details on connecting. +.. _mysql_connection_timeouts: Connection Timeouts ------------------- @@ -55,7 +55,9 @@ creation option can be specified in this syntax:: .. seealso:: - `The InnoDB Storage Engine `_ - on the MySQL website. + `The InnoDB Storage Engine + `_ - + on the MySQL website. Case Sensitivity and Table Reflection ------------------------------------- @@ -125,32 +127,20 @@ engines:: Column('id', Integer, primary_key=True) ) -SQL Mode --------- +Ansi Quoting Style +------------------ -MySQL SQL modes are supported. Modes that enable ``ANSI_QUOTES`` (such as -``ANSI``) require an engine option to modify SQLAlchemy's quoting style. -When using an ANSI-quoting mode, supply ``use_ansiquotes=True`` when -creating your ``Engine``:: +MySQL features two varieties of identifier "quoting style", one using +backticks and the other using quotes, e.g. ```some_identifier``` vs. +``"some_identifier"``. All MySQL dialects detect which version +is in use by checking the value of ``sql_mode`` when a connection is first +established with a particular :class:`.Engine`. This quoting style comes +into play when rendering table and column names as well as when reflecting +existing database structures. The detection is entirely automatic and +no special configuration is needed to use either quoting style. - create_engine('mysql://localhost/test', use_ansiquotes=True) - -This is an engine-wide option and is not toggleable on a per-connection basis. -SQLAlchemy does not presume to ``SET sql_mode`` for you with this option. For -the best performance, set the quoting style server-wide in ``my.cnf`` or by -supplying ``--sql-mode`` to ``mysqld``. You can also use a -:class:`sqlalchemy.pool.Pool` listener hook to issue a ``SET SESSION -sql_mode='...'`` on connect to configure each connection. - -If you do not specify ``use_ansiquotes``, the regular MySQL quoting style is -used by default. - -If you do issue a ``SET sql_mode`` through SQLAlchemy, the dialect must be -updated if the quoting style is changed. Again, this change will affect all -connections:: - - connection.execute('SET sql_mode="ansi"') - connection.dialect.use_ansiquotes = True +.. versionchanged:: 0.6 detection of ANSI quoting style is entirely automatic, + there's no longer any end-user ``create_engine()`` options in this regard. MySQL SQL Extensions -------------------- @@ -202,13 +192,13 @@ detection, instead rendering the internal expression directly. CAST may still not be desirable on an early MySQL version post-4.0.2, as it didn't add all datatype support until 4.1.1. If your application falls into this -narrow area, the behavior of CAST can be controlled using the :ref:`sqlalchemy.ext.compiler_toplevel` -system, as per the recipe below:: +narrow area, the behavior of CAST can be controlled using the +:ref:`sqlalchemy.ext.compiler_toplevel` system, as per the recipe below:: - from sqlalchemy.sql.expression import _Cast + from sqlalchemy.sql.expression import Cast from sqlalchemy.ext.compiler import compiles - @compiles(_Cast, 'mysql') + @compiles(Cast, 'mysql') def _check_mysql_version(element, compiler, **kw): if compiler.dialect.server_version_info < (4, 1, 0): return compiler.process(element.clause, **kw) @@ -239,11 +229,18 @@ become part of the index. SQLAlchemy provides this feature via the Index('my_index', my_table.c.data, mysql_length=10) + Index('a_b_idx', my_table.c.a, my_table.c.b, mysql_length={'a': 4, 'b': 9}) + Prefix lengths are given in characters for nonbinary string types and in bytes -for binary string types. The value passed to the keyword argument will be -simply passed through to the underlying CREATE INDEX command, so it *must* be -an integer. MySQL only allows a length for an index if it is for a CHAR, -VARCHAR, TEXT, BINARY, VARBINARY and BLOB. +for binary string types. The value passed to the keyword argument *must* be +either an integer (and, thus, specify the same prefix length value for all +columns of the index) or a dict in which keys are column names and values are +prefix length values for corresponding columns. MySQL only allows a length for +a column of an index if it is for a CHAR, VARCHAR, TEXT, BINARY, VARBINARY and +BLOB. + +.. versionadded:: 0.8.2 ``mysql_length`` may now be specified as a dictionary + for use with composite indexes. Index Types ~~~~~~~~~~~~~ @@ -268,26 +265,62 @@ http://dev.mysql.com/doc/refman/5.0/en/create-index.html http://dev.mysql.com/doc/refman/5.0/en/create-table.html +.. _mysql_foreign_keys: + +MySQL Foreign Key Options +------------------------- + +MySQL does not support the foreign key arguments "DEFERRABLE", "INITIALLY", +or "MATCH". Using the ``deferrable`` or ``initially`` keyword argument with +:class:`.ForeignKeyConstraint` or :class:`.ForeignKey` will have the effect of these keywords being +rendered in a DDL expression, which will then raise an error on MySQL. +In order to use these keywords on a foreign key while having them ignored +on a MySQL backend, use a custom compile rule:: + + from sqlalchemy.ext.compiler import compiles + from sqlalchemy.schema import ForeignKeyConstraint + + @compiles(ForeignKeyConstraint, "mysql") + def process(element, compiler, **kw): + element.deferrable = element.initially = None + return compiler.visit_foreign_key_constraint(element, **kw) + +.. versionchanged:: 0.9.0 - the MySQL backend no longer silently ignores + the ``deferrable`` or ``initially`` keyword arguments of :class:`.ForeignKeyConstraint` + and :class:`.ForeignKey`. + +The "MATCH" keyword is in fact more insidious, and is explicitly disallowed +by SQLAlchemy in conjunction with the MySQL backend. This argument is silently +ignored by MySQL, but in addition has the effect of ON UPDATE and ON DELETE options +also being ignored by the backend. Therefore MATCH should never be used with the +MySQL backend; as is the case with DEFERRABLE and INITIALLY, custom compilation +rules can be used to correct a MySQL ForeignKeyConstraint at DDL definition time. + +.. versionadded:: 0.9.0 - the MySQL backend will raise a :class:`.CompileError` + when the ``match`` keyword is used with :class:`.ForeignKeyConstraint` + or :class:`.ForeignKey`. + """ -import datetime, inspect, re, sys +import datetime +import inspect +import re +import sys -from sqlalchemy import schema as sa_schema -from sqlalchemy import exc, log, sql, util -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy.sql import functions as sql_functions -from sqlalchemy.sql import compiler +from ... import schema as sa_schema +from ... import exc, log, sql, util +from ...sql import compiler from array import array as _array -from sqlalchemy.engine import reflection -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy import types as sqltypes -from sqlalchemy.util import topological -from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \ +from ...engine import reflection +from ...engine import default +from ... import types as sqltypes +from ...util import topological +from ...types import DATE, DATETIME, BOOLEAN, TIME, \ BLOB, BINARY, VARBINARY RESERVED_WORDS = set( - ['accessible', 'add', 'all', 'alter', 'analyze','and', 'as', 'asc', + ['accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc', 'asensitive', 'before', 'between', 'bigint', 'binary', 'blob', 'both', 'by', 'call', 'cascade', 'case', 'change', 'char', 'character', 'check', 'collate', 'column', 'condition', 'constraint', 'continue', 'convert', @@ -322,10 +355,20 @@ RESERVED_WORDS = set( 'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use', 'using', 'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary', 'varchar', 'varcharacter', 'varying', 'when', 'where', 'while', 'with', - 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0 - 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1 + + 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0 + + 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1 + 'accessible', 'linear', 'master_ssl_verify_server_cert', 'range', - 'read_only', 'read_write', # 5.1 + 'read_only', 'read_write', # 5.1 + + 'general', 'ignore_server_ids', 'master_heartbeat_period', 'maxvalue', + 'resignal', 'signal', 'slow', # 5.5 + + 'get', 'io_after_gtids', 'io_before_gtids', 'master_bind', 'one_shot', + 'partition', 'sql_after_gtids', 'sql_before_gtids', # 5.6 + ]) AUTOCOMMIT_RE = re.compile( @@ -337,13 +380,22 @@ SET_RE = re.compile( class _NumericType(object): - """Base for MySQL numeric types.""" + """Base for MySQL numeric types. + + This is the base both for NUMERIC as well as INTEGER, hence + it's a mixin. + + """ def __init__(self, unsigned=False, zerofill=False, **kw): self.unsigned = unsigned self.zerofill = zerofill super(_NumericType, self).__init__(**kw) + def __repr__(self): + return util.generic_repr(self, + to_inspect=[_NumericType, sqltypes.Numeric]) + class _FloatType(_NumericType, sqltypes.Float): def __init__(self, precision=None, scale=None, asdecimal=True, **kw): if isinstance(self, (REAL, DOUBLE)) and \ @@ -354,48 +406,42 @@ class _FloatType(_NumericType, sqltypes.Float): raise exc.ArgumentError( "You must specify both precision and scale or omit " "both altogether.") - super(_FloatType, self).__init__(precision=precision, asdecimal=asdecimal, **kw) self.scale = scale + def __repr__(self): + return util.generic_repr(self, + to_inspect=[_FloatType, _NumericType, sqltypes.Float]) + class _IntegerType(_NumericType, sqltypes.Integer): def __init__(self, display_width=None, **kw): self.display_width = display_width super(_IntegerType, self).__init__(**kw) + def __repr__(self): + return util.generic_repr(self, + to_inspect=[_IntegerType, _NumericType, sqltypes.Integer]) + class _StringType(sqltypes.String): """Base for MySQL string types.""" def __init__(self, charset=None, collation=None, - ascii=False, binary=False, + ascii=False, binary=False, unicode=False, national=False, **kw): self.charset = charset + # allow collate= or collation= - self.collation = kw.pop('collate', collation) + kw.setdefault('collation', kw.pop('collate', collation)) + self.ascii = ascii - # We have to munge the 'unicode' param strictly as a dict - # otherwise 2to3 will turn it into str. - self.__dict__['unicode'] = kw.get('unicode', False) - # sqltypes.String does not accept the 'unicode' arg at all. - if 'unicode' in kw: - del kw['unicode'] + self.unicode = unicode self.binary = binary self.national = national super(_StringType, self).__init__(**kw) def __repr__(self): - attributes = inspect.getargspec(self.__init__)[0][1:] - attributes.extend(inspect.getargspec(_StringType.__init__)[0][1:]) - - params = {} - for attr in attributes: - val = getattr(self, attr) - if val is not None and val is not False: - params[attr] = val - - return "%s(%s)" % (self.__class__.__name__, - ', '.join(['%s=%r' % (k, params[k]) for k in params])) - + return util.generic_repr(self, + to_inspect=[_StringType, sqltypes.String]) class NUMERIC(_NumericType, sqltypes.NUMERIC): """MySQL NUMERIC type.""" @@ -418,7 +464,8 @@ class NUMERIC(_NumericType, sqltypes.NUMERIC): numeric. """ - super(NUMERIC, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw) + super(NUMERIC, self).__init__(precision=precision, + scale=scale, asdecimal=asdecimal, **kw) class DECIMAL(_NumericType, sqltypes.DECIMAL): @@ -454,6 +501,14 @@ class DOUBLE(_FloatType): def __init__(self, precision=None, scale=None, asdecimal=True, **kw): """Construct a DOUBLE. + .. note:: + + The :class:`.DOUBLE` type by default converts from float + to Decimal, using a truncation that defaults to 10 digits. Specify + either ``scale=n`` or ``decimal_return_scale=n`` in order to change + this scale, or ``asdecimal=False`` to return values directly as + Python floating points. + :param precision: Total digits in this number. If scale and precision are both None, values are stored to limits allowed by the server. @@ -470,6 +525,7 @@ class DOUBLE(_FloatType): super(DOUBLE, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw) + class REAL(_FloatType, sqltypes.REAL): """MySQL REAL type.""" @@ -478,6 +534,14 @@ class REAL(_FloatType, sqltypes.REAL): def __init__(self, precision=None, scale=None, asdecimal=True, **kw): """Construct a REAL. + .. note:: + + The :class:`.REAL` type by default converts from float + to Decimal, using a truncation that defaults to 10 digits. Specify + either ``scale=n`` or ``decimal_return_scale=n`` in order to change + this scale, or ``asdecimal=False`` to return values directly as + Python floating points. + :param precision: Total digits in this number. If scale and precision are both None, values are stored to limits allowed by the server. @@ -494,6 +558,7 @@ class REAL(_FloatType, sqltypes.REAL): super(REAL, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw) + class FLOAT(_FloatType, sqltypes.FLOAT): """MySQL FLOAT type.""" @@ -521,6 +586,7 @@ class FLOAT(_FloatType, sqltypes.FLOAT): def bind_processor(self, dialect): return None + class INTEGER(_IntegerType, sqltypes.INTEGER): """MySQL INTEGER type.""" @@ -541,6 +607,7 @@ class INTEGER(_IntegerType, sqltypes.INTEGER): """ super(INTEGER, self).__init__(display_width=display_width, **kw) + class BIGINT(_IntegerType, sqltypes.BIGINT): """MySQL BIGINTEGER type.""" @@ -561,6 +628,7 @@ class BIGINT(_IntegerType, sqltypes.BIGINT): """ super(BIGINT, self).__init__(display_width=display_width, **kw) + class MEDIUMINT(_IntegerType): """MySQL MEDIUMINTEGER type.""" @@ -581,6 +649,7 @@ class MEDIUMINT(_IntegerType): """ super(MEDIUMINT, self).__init__(display_width=display_width, **kw) + class TINYINT(_IntegerType): """MySQL TINYINT type.""" @@ -589,10 +658,6 @@ class TINYINT(_IntegerType): def __init__(self, display_width=None, **kw): """Construct a TINYINT. - Note: following the usual MySQL conventions, TINYINT(1) columns - reflected during Table(..., autoload=True) are treated as - Boolean columns. - :param display_width: Optional, maximum display width for this number. :param unsigned: a boolean, optional. @@ -605,6 +670,7 @@ class TINYINT(_IntegerType): """ super(TINYINT, self).__init__(display_width=display_width, **kw) + class SMALLINT(_IntegerType, sqltypes.SMALLINT): """MySQL SMALLINTEGER type.""" @@ -625,6 +691,7 @@ class SMALLINT(_IntegerType, sqltypes.SMALLINT): """ super(SMALLINT, self).__init__(display_width=display_width, **kw) + class BIT(sqltypes.TypeEngine): """MySQL BIT type. @@ -654,34 +721,68 @@ class BIT(sqltypes.TypeEngine): def process(value): if value is not None: - v = 0L + v = 0 for i in map(ord, value): v = v << 8 | i return v return value return process -class _MSTime(sqltypes.Time): - """MySQL TIME type.""" + +class TIME(sqltypes.TIME): + """MySQL TIME type. + + Recent versions of MySQL add support for + fractional seconds precision. While the + :class:`.mysql.TIME` type now supports this, + note that many DBAPI drivers may not yet + include support. + + """ __visit_name__ = 'TIME' + def __init__(self, timezone=False, fsp=None): + """Construct a MySQL TIME type. + + :param timezone: not used by the MySQL dialect. + :param fsp: fractional seconds precision value. + MySQL 5.6 supports storage of fractional seconds; + this parameter will be used when emitting DDL + for the TIME type. Note that many DBAPI drivers + may not yet have support for fractional seconds, + however. + + .. versionadded:: 0.8 The MySQL-specific TIME + type as well as fractional seconds support. + + """ + super(TIME, self).__init__(timezone=timezone) + self.fsp = fsp + def result_processor(self, dialect, coltype): time = datetime.time + def process(value): # convert from a timedelta value if value is not None: + microseconds = value.microseconds seconds = value.seconds - minutes = seconds / 60 - return time(minutes / 60, minutes % 60, seconds - minutes * 60) + minutes = seconds // 60 + return time(minutes // 60, + minutes % 60, + seconds - minutes * 60, + microsecond=microseconds) else: return None return process + class TIMESTAMP(sqltypes.TIMESTAMP): """MySQL TIMESTAMP type.""" __visit_name__ = 'TIMESTAMP' + class YEAR(sqltypes.TypeEngine): """MySQL YEAR type, for single byte storage of years 1901-2155.""" @@ -690,6 +791,7 @@ class YEAR(sqltypes.TypeEngine): def __init__(self, display_width=None): self.display_width = display_width + class TEXT(_StringType, sqltypes.TEXT): """MySQL TEXT type, for text up to 2^16 characters.""" @@ -725,6 +827,7 @@ class TEXT(_StringType, sqltypes.TEXT): """ super(TEXT, self).__init__(length=length, **kw) + class TINYTEXT(_StringType): """MySQL TINYTEXT type, for text up to 2^8 characters.""" @@ -756,6 +859,7 @@ class TINYTEXT(_StringType): """ super(TINYTEXT, self).__init__(**kwargs) + class MEDIUMTEXT(_StringType): """MySQL MEDIUMTEXT type, for text up to 2^24 characters.""" @@ -787,6 +891,7 @@ class MEDIUMTEXT(_StringType): """ super(MEDIUMTEXT, self).__init__(**kwargs) + class LONGTEXT(_StringType): """MySQL LONGTEXT type, for text up to 2^32 characters.""" @@ -850,6 +955,7 @@ class VARCHAR(_StringType, sqltypes.VARCHAR): """ super(VARCHAR, self).__init__(length=length, **kwargs) + class CHAR(_StringType, sqltypes.CHAR): """MySQL CHAR type, for fixed-length character data.""" @@ -870,6 +976,7 @@ class CHAR(_StringType, sqltypes.CHAR): """ super(CHAR, self).__init__(length=length, **kwargs) + class NVARCHAR(_StringType, sqltypes.NVARCHAR): """MySQL NVARCHAR type. @@ -922,24 +1029,66 @@ class NCHAR(_StringType, sqltypes.NCHAR): super(NCHAR, self).__init__(length=length, **kwargs) - - class TINYBLOB(sqltypes._Binary): """MySQL TINYBLOB type, for binary data up to 2^8 bytes.""" __visit_name__ = 'TINYBLOB' + class MEDIUMBLOB(sqltypes._Binary): """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes.""" __visit_name__ = 'MEDIUMBLOB' + class LONGBLOB(sqltypes._Binary): """MySQL LONGBLOB type, for binary data up to 2^32 bytes.""" __visit_name__ = 'LONGBLOB' -class ENUM(sqltypes.Enum, _StringType): +class _EnumeratedValues(_StringType): + def _init_values(self, values, kw): + self.quoting = kw.pop('quoting', 'auto') + + if self.quoting == 'auto' and len(values): + # What quoting character are we using? + q = None + for e in values: + if len(e) == 0: + self.quoting = 'unquoted' + break + elif q is None: + q = e[0] + + if len(e) == 1 or e[0] != q or e[-1] != q: + self.quoting = 'unquoted' + break + else: + self.quoting = 'quoted' + + if self.quoting == 'quoted': + util.warn_deprecated( + 'Manually quoting %s value literals is deprecated. Supply ' + 'unquoted values and use the quoting= option in cases of ' + 'ambiguity.' % self.__class__.__name__) + + values = self._strip_values(values) + + self._enumerated_values = values + length = max([len(v) for v in values] + [0]) + return values, length + + @classmethod + def _strip_values(cls, values): + strip_values = [] + for a in values: + if a[0:1] == '"' or a[0:1] == "'": + # strip enclosing quotes and unquote interior + a = a[1:-1].replace(a[0] * 2, a[0]) + strip_values.append(a) + return strip_values + +class ENUM(sqltypes.Enum, _EnumeratedValues): """MySQL ENUM type.""" __visit_name__ = 'ENUM' @@ -947,9 +1096,9 @@ class ENUM(sqltypes.Enum, _StringType): def __init__(self, *enums, **kw): """Construct an ENUM. - Example: + E.g.:: - Column('myenum', MSEnum("foo", "bar", "baz")) + Column('myenum', ENUM("foo", "bar", "baz")) :param enums: The range of valid values for this ENUM. Values will be quoted when generating the schema according to the quoting flag (see @@ -993,53 +1142,24 @@ class ENUM(sqltypes.Enum, _StringType): literals for you. This is a transitional option. """ - self.quoting = kw.pop('quoting', 'auto') - - if self.quoting == 'auto' and len(enums): - # What quoting character are we using? - q = None - for e in enums: - if len(e) == 0: - self.quoting = 'unquoted' - break - elif q is None: - q = e[0] - - if e[0] != q or e[-1] != q: - self.quoting = 'unquoted' - break - else: - self.quoting = 'quoted' - - if self.quoting == 'quoted': - util.warn_deprecated( - 'Manually quoting ENUM value literals is deprecated. Supply ' - 'unquoted values and use the quoting= option in cases of ' - 'ambiguity.') - enums = self._strip_enums(enums) - + values, length = self._init_values(enums, kw) self.strict = kw.pop('strict', False) - length = max([len(v) for v in enums] + [0]) kw.pop('metadata', None) kw.pop('schema', None) kw.pop('name', None) kw.pop('quote', None) kw.pop('native_enum', None) + kw.pop('inherit_schema', None) _StringType.__init__(self, length=length, **kw) - sqltypes.Enum.__init__(self, *enums) + sqltypes.Enum.__init__(self, *values) - @classmethod - def _strip_enums(cls, enums): - strip_enums = [] - for a in enums: - if a[0:1] == '"' or a[0:1] == "'": - # strip enclosing quotes and unquote interior - a = a[1:-1].replace(a[0] * 2, a[0]) - strip_enums.append(a) - return strip_enums + def __repr__(self): + return util.generic_repr(self, + to_inspect=[ENUM, _StringType, sqltypes.Enum]) def bind_processor(self, dialect): super_convert = super(ENUM, self).bind_processor(dialect) + def process(value): if self.strict and value is not None and value not in self.enums: raise exc.InvalidRequestError('"%s" not a valid value for ' @@ -1054,7 +1174,8 @@ class ENUM(sqltypes.Enum, _StringType): kw['strict'] = self.strict return sqltypes.Enum.adapt(self, impltype, **kw) -class SET(_StringType): + +class SET(_EnumeratedValues): """MySQL SET type.""" __visit_name__ = 'SET' @@ -1062,15 +1183,16 @@ class SET(_StringType): def __init__(self, *values, **kw): """Construct a SET. - Example:: + E.g.:: - Column('myset', MSSet("'foo'", "'bar'", "'baz'")) + Column('myset', SET("foo", "bar", "baz")) :param values: The range of valid values for this SET. Values will be - used exactly as they appear when generating schemas. Strings must - be quoted, as in the example above. Single-quotes are suggested for - ANSI compatibility and are required for portability to servers with - ANSI_QUOTES enabled. + quoted when generating the schema according to the quoting flag (see + below). + + .. versionchanged:: 0.9.0 quoting is applied automatically to + :class:`.mysql.SET` in the same way as for :class:`.mysql.ENUM`. :param charset: Optional, a column-level character set for this string value. Takes precedence to 'ascii' or 'unicode' short-hand. @@ -1089,18 +1211,27 @@ class SET(_StringType): BINARY in schema. This does not affect the type of data stored, only the collation of character data. + :param quoting: Defaults to 'auto': automatically determine enum value + quoting. If all enum values are surrounded by the same quoting + character, then use 'quoted' mode. Otherwise, use 'unquoted' mode. + + 'quoted': values in enums are already quoted, they will be used + directly when generating the schema - this usage is deprecated. + + 'unquoted': values in enums are not quoted, they will be escaped and + surrounded by single quotes when generating the schema. + + Previous versions of this type always required manually quoted + values to be supplied; future versions will always quote the string + literals for you. This is a transitional option. + + .. versionadded:: 0.9.0 + """ - self._ddl_values = values + values, length = self._init_values(values, kw) + self.values = tuple(values) - strip_values = [] - for a in values: - if a[0:1] == '"' or a[0:1] == "'": - # strip enclosing quotes and unquote interior - a = a[1:-1].replace(a[0] * 2, a[0]) - strip_values.append(a) - - self.values = strip_values - kw.setdefault('length', max([len(v) for v in strip_values] + [0])) + kw.setdefault('length', length) super(SET, self).__init__(**kw) def result_processor(self, dialect, coltype): @@ -1109,13 +1240,10 @@ class SET(_StringType): # No ',' quoting issues- commas aren't allowed in SET values # The bad news: # Plenty of driver inconsistencies here. - if isinstance(value, util.set_types): + if isinstance(value, set): # ..some versions convert '' to an empty set if not value: value.add('') - # ..some return sets.Set, even for pythons that have __builtin__.set - if not isinstance(value, set): - value = set(value) return value # ...and some versions return strings if value is not None: @@ -1126,8 +1254,9 @@ class SET(_StringType): def bind_processor(self, dialect): super_convert = super(SET, self).bind_processor(dialect) + def process(value): - if value is None or isinstance(value, (int, long, basestring)): + if value is None or isinstance(value, util.int_types + util.string_types): pass else: if None in value: @@ -1142,7 +1271,7 @@ class SET(_StringType): return process # old names -MSTime = _MSTime +MSTime = TIME MSSet = SET MSEnum = ENUM MSLongBlob = LONGBLOB @@ -1174,9 +1303,12 @@ MSFloat = FLOAT MSInteger = INTEGER colspecs = { + _IntegerType: _IntegerType, + _NumericType: _NumericType, + _FloatType: _FloatType, sqltypes.Numeric: NUMERIC, sqltypes.Float: FLOAT, - sqltypes.Time: _MSTime, + sqltypes.Time: TIME, sqltypes.Enum: ENUM, } @@ -1218,20 +1350,20 @@ ischema_names = { 'year': YEAR, } + class MySQLExecutionContext(default.DefaultExecutionContext): def should_autocommit_text(self, statement): return AUTOCOMMIT_RE.match(statement) + class MySQLCompiler(compiler.SQLCompiler): render_table_with_column_in_update_from = True """Overridden from base SQLCompiler value""" extract_map = compiler.SQLCompiler.extract_map.copy() - extract_map.update ({ - 'milliseconds': 'millisecond', - }) + extract_map.update({'milliseconds': 'millisecond'}) def visit_random_func(self, fn, **kw): return "rand%s" % self.function_argspec(fn) @@ -1242,11 +1374,13 @@ class MySQLCompiler(compiler.SQLCompiler): def visit_sysdate_func(self, fn, **kw): return "SYSDATE()" - def visit_concat_op(self, binary, **kw): - return "concat(%s, %s)" % (self.process(binary.left), self.process(binary.right)) + def visit_concat_op_binary(self, binary, operator, **kw): + return "concat(%s, %s)" % (self.process(binary.left), + self.process(binary.right)) - def visit_match_op(self, binary, **kw): - return "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % (self.process(binary.left), self.process(binary.right)) + def visit_match_op_binary(self, binary, operator, **kw): + return "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % \ + (self.process(binary.left), self.process(binary.right)) def get_from_hint_text(self, table, text): return text @@ -1260,7 +1394,8 @@ class MySQLCompiler(compiler.SQLCompiler): return 'SIGNED INTEGER' elif isinstance(type_, sqltypes.TIMESTAMP): return 'DATETIME' - elif isinstance(type_, (sqltypes.DECIMAL, sqltypes.DateTime, sqltypes.Date, sqltypes.Time)): + elif isinstance(type_, (sqltypes.DECIMAL, sqltypes.DateTime, + sqltypes.Date, sqltypes.Time)): return self.dialect.type_compiler.process(type_) elif isinstance(type_, sqltypes.Text): return 'CHAR' @@ -1273,7 +1408,8 @@ class MySQLCompiler(compiler.SQLCompiler): elif isinstance(type_, sqltypes._Binary): return 'BINARY' elif isinstance(type_, sqltypes.NUMERIC): - return self.dialect.type_compiler.process(type_).replace('NUMERIC', 'DECIMAL') + return self.dialect.type_compiler.process( + type_).replace('NUMERIC', 'DECIMAL') else: return None @@ -1304,7 +1440,7 @@ class MySQLCompiler(compiler.SQLCompiler): of a SELECT. """ - if isinstance(select._distinct, basestring): + if isinstance(select._distinct, util.string_types): return select._distinct.upper() + " " elif select._distinct: return "DISTINCT " @@ -1312,11 +1448,6 @@ class MySQLCompiler(compiler.SQLCompiler): return "" def visit_join(self, join, asfrom=False, **kwargs): - # 'JOIN ... ON ...' for inner joins isn't available until 4.0. - # Apparently < 3.23.17 requires theta joins for inner joins - # (but not outer). Not generating these currently, but - # support can be added, preferably after dialects are - # refactored to be version-sensitive. return ''.join( (self.process(join.left, asfrom=True, **kwargs), (join.isouter and " LEFT OUTER JOIN " or " INNER JOIN "), @@ -1325,10 +1456,10 @@ class MySQLCompiler(compiler.SQLCompiler): self.process(join.onclause, **kwargs))) def for_update_clause(self, select): - if select.for_update == 'read': - return ' LOCK IN SHARE MODE' + if select._for_update_arg.read: + return " LOCK IN SHARE MODE" else: - return super(MySQLCompiler, self).for_update_clause(select) + return " FOR UPDATE" def limit_clause(self, select): # MySQL supports: @@ -1389,10 +1520,11 @@ class MySQLCompiler(compiler.SQLCompiler): class MySQLDDLCompiler(compiler.DDLCompiler): def create_table_constraints(self, table): """Get table constraints.""" - constraint_string = super(MySQLDDLCompiler, self).create_table_constraints(table) + constraint_string = super( + MySQLDDLCompiler, self).create_table_constraints(table) engine_key = '%s_engine' % self.dialect.name - is_innodb = table.kwargs.has_key(engine_key) and \ + is_innodb = engine_key in table.kwargs and \ table.kwargs[engine_key].lower() == 'innodb' auto_inc_column = table._autoincrement_column @@ -1404,14 +1536,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler): constraint_string += ", \n\t" constraint_string += "KEY %s (%s)" % ( self.preparer.quote( - "idx_autoinc_%s" % auto_inc_column.name, None + "idx_autoinc_%s" % auto_inc_column.name ), self.preparer.format_column(auto_inc_column) ) return constraint_string - def get_column_specification(self, column, **kw): """Builds column DDL.""" @@ -1430,7 +1561,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler): elif column.nullable and is_timestamp and default is None: colspec.append('NULL') - if column is column.table._autoincrement_column and column.server_default is None: + if column is column.table._autoincrement_column and \ + column.server_default is None: colspec.append('AUTO_INCREMENT') return ' '.join(colspec) @@ -1442,7 +1574,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler): opts = dict( ( - k[len(self.dialect.name)+1:].upper(), + k[len(self.dialect.name) + 1:].upper(), v ) for k, v in table.kwargs.items() @@ -1471,31 +1603,47 @@ class MySQLDDLCompiler(compiler.DDLCompiler): table_opts.append(joiner.join((opt, arg))) return ' '.join(table_opts) - def visit_create_index(self, create): index = create.element + self._verify_index_table(index) preparer = self.preparer table = preparer.format_table(index.table) - columns = [preparer.quote(c.name, c.quote) for c in index.columns] - name = preparer.quote( - self._index_identifier(index.name), - index.quote) + columns = [self.sql_compiler.process(expr, include_table=False, + literal_binds=True) + for expr in index.expressions] + + name = self._prepared_index_name(index) text = "CREATE " if index.unique: text += "UNIQUE " text += "INDEX %s ON %s " % (name, table) - columns = ', '.join(columns) if 'mysql_length' in index.kwargs: length = index.kwargs['mysql_length'] - text += "(%s(%d))" % (columns, length) + + if isinstance(length, dict): + # length value can be a (column_name --> integer value) mapping + # specifying the prefix length for each column of the index + columns = ', '.join( + ('%s(%d)' % (col, length[col]) + if col in length else '%s' % col) + for col in columns + ) + else: + # or can be an integer value specifying the same + # prefix length for all columns of the index + columns = ', '.join( + '%s(%d)' % (col, length) + for col in columns + ) else: - text += "(%s)" % (columns) + columns = ', '.join(columns) + text += '(%s)' % columns if 'mysql_using' in index.kwargs: using = index.kwargs['mysql_using'] - text += " USING %s" % (preparer.quote(using, index.quote)) + text += " USING %s" % (preparer.quote(using)) return text @@ -1504,17 +1652,15 @@ class MySQLDDLCompiler(compiler.DDLCompiler): visit_primary_key_constraint(constraint) if "mysql_using" in constraint.kwargs: using = constraint.kwargs['mysql_using'] - text += " USING %s" % ( - self.preparer.quote(using, constraint.quote)) + text += " USING %s" % (self.preparer.quote(using)) return text def visit_drop_index(self, drop): index = drop.element - return "\nDROP INDEX %s ON %s" % \ - (self.preparer.quote( - self._index_identifier(index.name), index.quote - ), + return "\nDROP INDEX %s ON %s" % ( + self._prepared_index_name(index, + include_schema=False), self.preparer.format_table(index.table)) def visit_drop_constraint(self, drop): @@ -1535,6 +1681,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler): (self.preparer.format_table(constraint.table), qual, const) + def define_constraint_match(self, constraint): + if constraint.match is not None: + raise exc.CompileError( + "MySQL ignores the 'MATCH' keyword while at the same time " + "causes ON UPDATE/ON DELETE clauses to be ignored.") + return "" + class MySQLTypeCompiler(compiler.GenericTypeCompiler): def _extend_numeric(self, type_, spec): "Extend a numeric-type declaration with MySQL specific extensions." @@ -1593,7 +1746,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): else: return self._extend_numeric(type_, "NUMERIC(%(precision)s, %(scale)s)" % - {'precision': type_.precision, 'scale' : type_.scale}) + {'precision': type_.precision, + 'scale': type_.scale}) def visit_DECIMAL(self, type_): if type_.precision is None: @@ -1605,21 +1759,24 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): else: return self._extend_numeric(type_, "DECIMAL(%(precision)s, %(scale)s)" % - {'precision': type_.precision, 'scale' : type_.scale}) + {'precision': type_.precision, + 'scale': type_.scale}) def visit_DOUBLE(self, type_): if type_.precision is not None and type_.scale is not None: - return self._extend_numeric(type_, "DOUBLE(%(precision)s, %(scale)s)" % + return self._extend_numeric(type_, + "DOUBLE(%(precision)s, %(scale)s)" % {'precision': type_.precision, - 'scale' : type_.scale}) + 'scale': type_.scale}) else: return self._extend_numeric(type_, 'DOUBLE') def visit_REAL(self, type_): if type_.precision is not None and type_.scale is not None: - return self._extend_numeric(type_, "REAL(%(precision)s, %(scale)s)" % + return self._extend_numeric(type_, + "REAL(%(precision)s, %(scale)s)" % {'precision': type_.precision, - 'scale' : type_.scale}) + 'scale': type_.scale}) else: return self._extend_numeric(type_, 'REAL') @@ -1630,7 +1787,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): return self._extend_numeric(type_, "FLOAT(%s, %s)" % (type_.precision, type_.scale)) elif type_.precision is not None: - return self._extend_numeric(type_, "FLOAT(%s)" % (type_.precision,)) + return self._extend_numeric(type_, + "FLOAT(%s)" % (type_.precision,)) else: return self._extend_numeric(type_, "FLOAT") @@ -1660,7 +1818,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): def visit_TINYINT(self, type_): if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, "TINYINT(%s)" % type_.display_width) + return self._extend_numeric(type_, + "TINYINT(%s)" % type_.display_width) else: return self._extend_numeric(type_, "TINYINT") @@ -1686,7 +1845,10 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): return "DATE" def visit_TIME(self, type_): - return "TIME" + if getattr(type_, 'fsp', None): + return "TIME(%d)" % type_.fsp + else: + return "TIME" def visit_TIMESTAMP(self, type_): return 'TIMESTAMP' @@ -1722,7 +1884,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): def visit_CHAR(self, type_): if type_.length: - return self._extend_string(type_, {}, "CHAR(%(length)s)" % {'length' : type_.length}) + return self._extend_string(type_, {}, "CHAR(%(length)s)" % + {'length': type_.length}) else: return self._extend_string(type_, {}, "CHAR") @@ -1730,18 +1893,21 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): # We'll actually generate the equiv. "NATIONAL VARCHAR" instead # of "NVARCHAR". if type_.length: - return self._extend_string(type_, {'national':True}, "VARCHAR(%(length)s)" % {'length': type_.length}) + return self._extend_string(type_, {'national': True}, + "VARCHAR(%(length)s)" % {'length': type_.length}) else: raise exc.CompileError( "NVARCHAR requires a length on dialect %s" % self.dialect.name) def visit_NCHAR(self, type_): - # We'll actually generate the equiv. "NATIONAL CHAR" instead of "NCHAR". + # We'll actually generate the equiv. + # "NATIONAL CHAR" instead of "NCHAR". if type_.length: - return self._extend_string(type_, {'national':True}, "CHAR(%(length)s)" % {'length': type_.length}) + return self._extend_string(type_, {'national': True}, + "CHAR(%(length)s)" % {'length': type_.length}) else: - return self._extend_string(type_, {'national':True}, "CHAR") + return self._extend_string(type_, {'national': True}, "CHAR") def visit_VARBINARY(self, type_): return "VARBINARY(%d)" % type_.length @@ -1753,7 +1919,7 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): if not type_.native_enum: return super(MySQLTypeCompiler, self).visit_enum(type_) else: - return self.visit_ENUM(type_) + return self._visit_enumerated_values("ENUM", type_, type_.enums) def visit_BLOB(self, type_): if type_.length: @@ -1770,14 +1936,21 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): def visit_LONGBLOB(self, type_): return "LONGBLOB" - def visit_ENUM(self, type_): + def _visit_enumerated_values(self, name, type_, enumerated_values): quoted_enums = [] - for e in type_.enums: + for e in enumerated_values: quoted_enums.append("'%s'" % e.replace("'", "''")) - return self._extend_string(type_, {}, "ENUM(%s)" % ",".join(quoted_enums)) + return self._extend_string(type_, {}, "%s(%s)" % ( + name, ",".join(quoted_enums)) + ) + + def visit_ENUM(self, type_): + return self._visit_enumerated_values("ENUM", type_, + type_._enumerated_values) def visit_SET(self, type_): - return self._extend_string(type_, {}, "SET(%s)" % ",".join(type_._ddl_values)) + return self._visit_enumerated_values("SET", type_, + type_._enumerated_values) def visit_BOOLEAN(self, type): return "BOOL" @@ -1803,6 +1976,8 @@ class MySQLIdentifierPreparer(compiler.IdentifierPreparer): return tuple([self.quote_identifier(i) for i in ids if i is not None]) + +@log.class_logger class MySQLDialect(default.DefaultDialect): """Details of the MySQL dialect. Not used directly in application code.""" @@ -1817,6 +1992,7 @@ class MySQLDialect(default.DefaultDialect): supports_sane_rowcount = True supports_sane_multi_rowcount = False + supports_multivalues_insert = True default_paramstyle = 'format' colspecs = colspecs @@ -1833,7 +2009,8 @@ class MySQLDialect(default.DefaultDialect): _backslash_escapes = True _server_ansiquotes = False - def __init__(self, use_ansiquotes=None, isolation_level=None, **kwargs): + def __init__(self, isolation_level=None, **kwargs): + kwargs.pop('use_ansiquotes', None) # legacy default.DefaultDialect.__init__(self, **kwargs) self.isolation_level = isolation_level @@ -1866,9 +2043,11 @@ class MySQLDialect(default.DefaultDialect): cursor.execute('SELECT @@tx_isolation') val = cursor.fetchone()[0] cursor.close() + if util.py3k and isinstance(val, bytes): + val = val.decode() return val.upper().replace("-", " ") - def do_commit(self, connection): + def do_commit(self, dbapi_connection): """Execute a COMMIT.""" # COMMIT/ROLLBACK were introduced in 3.23.15. @@ -1877,7 +2056,7 @@ class MySQLDialect(default.DefaultDialect): # Ignore commit/rollback if support isn't present, otherwise even basic # operations via autocommit fail. try: - connection.commit() + dbapi_connection.commit() except: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args @@ -1885,11 +2064,11 @@ class MySQLDialect(default.DefaultDialect): return raise - def do_rollback(self, connection): + def do_rollback(self, dbapi_connection): """Execute a ROLLBACK.""" try: - connection.rollback() + dbapi_connection.rollback() except: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args @@ -1932,17 +2111,20 @@ class MySQLDialect(default.DefaultDialect): return False def _compat_fetchall(self, rp, charset=None): - """Proxy result rows to smooth over MySQL-Python driver inconsistencies.""" + """Proxy result rows to smooth over MySQL-Python driver + inconsistencies.""" return [_DecodingRowProxy(row, charset) for row in rp.fetchall()] def _compat_fetchone(self, rp, charset=None): - """Proxy a result row to smooth over MySQL-Python driver inconsistencies.""" + """Proxy a result row to smooth over MySQL-Python driver + inconsistencies.""" return _DecodingRowProxy(rp.fetchone(), charset) def _compat_first(self, rp, charset=None): - """Proxy a result row to smooth over MySQL-Python driver inconsistencies.""" + """Proxy a result row to smooth over MySQL-Python driver + inconsistencies.""" return _DecodingRowProxy(rp.first(), charset) @@ -1952,7 +2134,6 @@ class MySQLDialect(default.DefaultDialect): def _get_default_schema_name(self, connection): return connection.execute('SELECT DATABASE()').scalar() - def has_table(self, connection, table_name, schema=None): # SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly # on macosx (and maybe win?) with multibyte table names. @@ -1964,7 +2145,6 @@ class MySQLDialect(default.DefaultDialect): # full_name = self.identifier_preparer.format_table(table, # use_schema=True) - full_name = '.'.join(self.identifier_preparer._quote_free_identifiers( schema, table_name)) @@ -1973,10 +2153,10 @@ class MySQLDialect(default.DefaultDialect): try: try: rs = connection.execute(st) - have = rs.rowcount > 0 + have = rs.fetchone() is not None rs.close() return have - except exc.DBAPIError, e: + except exc.DBAPIError as e: if self._extract_error_code(e.orig) == 1146: return False raise @@ -1987,14 +2167,12 @@ class MySQLDialect(default.DefaultDialect): def initialize(self, connection): default.DefaultDialect.initialize(self, connection) self._connection_charset = self._detect_charset(connection) - self._server_casing = self._detect_casing(connection) - self._server_collations = self._detect_collations(connection) self._detect_ansiquotes(connection) if self._server_ansiquotes: # if ansiquotes == True, build a new IdentifierPreparer # with the new setting self.identifier_preparer = self.preparer(self, - server_ansiquotes=self._server_ansiquotes) + server_ansiquotes=self._server_ansiquotes) @property def _supports_cast(self): @@ -2018,13 +2196,15 @@ class MySQLDialect(default.DefaultDialect): if self.server_version_info < (5, 0, 2): rp = connection.execute("SHOW TABLES FROM %s" % self.identifier_preparer.quote_identifier(current_schema)) - return [row[0] for row in self._compat_fetchall(rp, charset=charset)] + return [row[0] for + row in self._compat_fetchall(rp, charset=charset)] else: rp = connection.execute("SHOW FULL TABLES FROM %s" % self.identifier_preparer.quote_identifier(current_schema)) - return [row[0] for row in self._compat_fetchall(rp, charset=charset)\ - if row[1] == 'BASE TABLE'] + return [row[0] + for row in self._compat_fetchall(rp, charset=charset) + if row[1] == 'BASE TABLE'] @reflection.cache def get_view_names(self, connection, schema=None, **kw): @@ -2037,34 +2217,39 @@ class MySQLDialect(default.DefaultDialect): charset = self._connection_charset rp = connection.execute("SHOW FULL TABLES FROM %s" % self.identifier_preparer.quote_identifier(schema)) - return [row[0] for row in self._compat_fetchall(rp, charset=charset)\ - if row[1] in ('VIEW', 'SYSTEM VIEW')] + return [row[0] + for row in self._compat_fetchall(rp, charset=charset) + if row[1] in ('VIEW', 'SYSTEM VIEW')] @reflection.cache def get_table_options(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw) + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) return parsed_state.table_options @reflection.cache def get_columns(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw) + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) return parsed_state.columns @reflection.cache - def get_primary_keys(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw) + def get_pk_constraint(self, connection, table_name, schema=None, **kw): + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) for key in parsed_state.keys: if key['type'] == 'PRIMARY': # There can be only one. - ##raise Exception, str(key) - return [s[0] for s in key['columns']] - return [] + cols = [s[0] for s in key['columns']] + return {'constrained_columns': cols, 'name': None} + return {'constrained_columns': [], 'name': None} @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw) + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) default_schema = None fkeys = [] @@ -2085,17 +2270,17 @@ class MySQLDialect(default.DefaultDialect): ref_names = spec['foreign'] con_kw = {} - for opt in ('name', 'onupdate', 'ondelete'): + for opt in ('onupdate', 'ondelete'): if spec.get(opt, False): con_kw[opt] = spec[opt] fkey_d = { - 'name' : spec['name'], - 'constrained_columns' : loc_names, - 'referred_schema' : ref_schema, - 'referred_table' : ref_name, - 'referred_columns' : ref_names, - 'options' : con_kw + 'name': spec['name'], + 'constrained_columns': loc_names, + 'referred_schema': ref_schema, + 'referred_table': ref_name, + 'referred_columns': ref_names, + 'options': con_kw } fkeys.append(fkey_d) return fkeys @@ -2103,7 +2288,8 @@ class MySQLDialect(default.DefaultDialect): @reflection.cache def get_indexes(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw) + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) indexes = [] for spec in parsed_state.keys: @@ -2127,6 +2313,21 @@ class MySQLDialect(default.DefaultDialect): indexes.append(index_d) return indexes + @reflection.cache + def get_unique_constraints(self, connection, table_name, + schema=None, **kw): + parsed_state = self._parsed_state_or_create( + connection, table_name, schema, **kw) + + return [ + { + 'name': key['name'], + 'column_names': [col[0] for col in key['columns']] + } + for key in parsed_state.keys + if key['type'] == 'UNIQUE' + ] + @reflection.cache def get_view_definition(self, connection, view_name, schema=None, **kw): @@ -2137,7 +2338,8 @@ class MySQLDialect(default.DefaultDialect): full_name=full_name) return sql - def _parsed_state_or_create(self, connection, table_name, schema=None, **kw): + def _parsed_state_or_create(self, connection, table_name, + schema=None, **kw): return self._setup_parser( connection, table_name, @@ -2225,7 +2427,7 @@ class MySQLDialect(default.DefaultDialect): row = self._compat_first( connection.execute("SHOW VARIABLES LIKE 'sql_mode'"), - charset=self._connection_charset) + charset=self._connection_charset) if not row: mode = '' @@ -2252,7 +2454,7 @@ class MySQLDialect(default.DefaultDialect): rp = None try: rp = connection.execute(st) - except exc.DBAPIError, e: + except exc.DBAPIError as e: if self._extract_error_code(e.orig) == 1146: raise exc.NoSuchTableError(full_name) else: @@ -2276,7 +2478,7 @@ class MySQLDialect(default.DefaultDialect): try: try: rp = connection.execute(st) - except exc.DBAPIError, e: + except exc.DBAPIError as e: if self._extract_error_code(e.orig) == 1146: raise exc.NoSuchTableError(full_name) else: @@ -2287,6 +2489,7 @@ class MySQLDialect(default.DefaultDialect): rp.close() return rows + class ReflectedState(object): """Stores raw information about a SHOW CREATE TABLE statement.""" @@ -2297,6 +2500,8 @@ class ReflectedState(object): self.keys = [] self.constraints = [] + +@log.class_logger class MySQLTableDefinitionParser(object): """Parses the results of a SHOW CREATE TABLE statement.""" @@ -2319,7 +2524,8 @@ class MySQLTableDefinitionParser(object): pass elif line.startswith('CREATE '): self._parse_table_name(line, state) - # Not present in real reflection, but may be if loading from a file. + # Not present in real reflection, but may be if + # loading from a file. elif not line: pass else: @@ -2332,7 +2538,6 @@ class MySQLTableDefinitionParser(object): state.constraints.append(spec) else: pass - return state def _parse_constraints(self, line): @@ -2461,8 +2666,8 @@ class MySQLTableDefinitionParser(object): if spec.get(kw, False): type_kw[kw] = spec[kw] - if type_ == 'enum': - type_args = ENUM._strip_enums(type_args) + if issubclass(col_type, _EnumeratedValues): + type_args = _EnumeratedValues._strip_values(type_args) type_instance = col_type(*type_args, **type_kw) @@ -2573,7 +2778,6 @@ class MySQLTableDefinitionParser(object): # 123 or 123,456 self._re_csv_int = _re_compile(r'\d+') - # `colname` [type opts] # (NOT NULL | NULL) # DEFAULT ('value' | CURRENT_TIMESTAMP...) @@ -2637,7 +2841,7 @@ class MySQLTableDefinitionParser(object): # # unique constraints come back as KEYs kw = quotes.copy() - kw['on'] = 'RESTRICT|CASCASDE|SET NULL|NOACTION' + kw['on'] = 'RESTRICT|CASCADE|SET NULL|NOACTION' self._re_constraint = _re_compile( r' ' r'CONSTRAINT +' @@ -2658,7 +2862,8 @@ class MySQLTableDefinitionParser(object): self._re_partition = _re_compile(r'(?:.*)(?:SUB)?PARTITION(?:.*)') # Table-level options (COLLATE, ENGINE, etc.) - # Do the string options first, since they have quoted strings we need to get rid of. + # Do the string options first, since they have quoted + # strings we need to get rid of. for option in _options_of_type_string: self._add_option_string(option) @@ -2681,8 +2886,8 @@ class MySQLTableDefinitionParser(object): regex = (r'(?P%s)%s' r"'(?P(?:[^']|'')*?)'(?!')" % (re.escape(directive), self._optional_equals)) - self._pr_options.append( - _pr_compile(regex, lambda v: v.replace("\\\\","\\").replace("''", "'"))) + self._pr_options.append(_pr_compile(regex, lambda v: + v.replace("\\\\", "\\").replace("''", "'"))) def _add_option_word(self, directive): regex = (r'(?P%s)%s' @@ -2699,8 +2904,6 @@ class MySQLTableDefinitionParser(object): _options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY', 'PASSWORD', 'CONNECTION') -log.class_logger(MySQLTableDefinitionParser) -log.class_logger(MySQLDialect) class _DecodingRowProxy(object): @@ -2724,11 +2927,8 @@ class _DecodingRowProxy(object): item = self.rowproxy[index] if isinstance(item, _array): item = item.tostring() - # Py2K - if self.charset and isinstance(item, str): - # end Py2K - # Py3K - #if self.charset and isinstance(item, bytes): + + if self.charset and isinstance(item, util.binary_type): return item.decode(self.charset) else: return item @@ -2737,11 +2937,7 @@ class _DecodingRowProxy(object): item = getattr(self.rowproxy, attr) if isinstance(item, _array): item = item.tostring() - # Py2K - if self.charset and isinstance(item, str): - # end Py2K - # Py3K - #if self.charset and isinstance(item, bytes): + if self.charset and isinstance(item, util.binary_type): return item.decode(self.charset) else: return item @@ -2752,8 +2948,8 @@ def _pr_compile(regex, cleanup=None): return (_re_compile(regex), cleanup) + def _re_compile(regex): """Compile a string to regex, I and UNICODE.""" return re.compile(regex, re.I | re.UNICODE) - diff --git a/libs/sqlalchemy/dialects/mysql/cymysql.py b/libs/sqlalchemy/dialects/mysql/cymysql.py new file mode 100644 index 00000000..e81a79b8 --- /dev/null +++ b/libs/sqlalchemy/dialects/mysql/cymysql.py @@ -0,0 +1,69 @@ +# mysql/cymysql.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +""" + +.. dialect:: mysql+cymysql + :name: CyMySQL + :dbapi: cymysql + :connectstring: mysql+cymysql://:@/[?] + :url: https://github.com/nakagami/CyMySQL + +""" + +from .mysqldb import MySQLDialect_mysqldb +from .base import (BIT, MySQLDialect) +from ... import util + +class _cymysqlBIT(BIT): + def result_processor(self, dialect, coltype): + """Convert a MySQL's 64 bit, variable length binary string to a long. + """ + + def process(value): + if value is not None: + v = 0 + for i in util.iterbytes(value): + v = v << 8 | i + return v + return value + return process + + +class MySQLDialect_cymysql(MySQLDialect_mysqldb): + driver = 'cymysql' + + description_encoding = None + supports_sane_rowcount = True + supports_sane_multi_rowcount = False + supports_unicode_statements = True + + colspecs = util.update_copy( + MySQLDialect.colspecs, + { + BIT: _cymysqlBIT, + } + ) + + @classmethod + def dbapi(cls): + return __import__('cymysql') + + def _extract_error_code(self, exception): + return exception.errno + + def is_disconnect(self, e, connection, cursor): + if isinstance(e, self.dbapi.OperationalError): + return self._extract_error_code(e) in \ + (2006, 2013, 2014, 2045, 2055) + elif isinstance(e, self.dbapi.InterfaceError): + # if underlying connection is closed, + # this is the error you get + return True + else: + return False + +dialect = MySQLDialect_cymysql diff --git a/libs/sqlalchemy/dialects/mysql/gaerdbms.py b/libs/sqlalchemy/dialects/mysql/gaerdbms.py index 2203504f..13203fce 100644 --- a/libs/sqlalchemy/dialects/mysql/gaerdbms.py +++ b/libs/sqlalchemy/dialects/mysql/gaerdbms.py @@ -1,26 +1,20 @@ # mysql/gaerdbms.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for Google Cloud SQL on Google App Engine. +""" +.. dialect:: mysql+gaerdbms + :name: Google Cloud SQL + :dbapi: rdbms + :connectstring: mysql+gaerdbms:///?instance= + :url: https://developers.google.com/appengine/docs/python/cloud-sql/developers-guide -This dialect is based primarily on the :mod:`.mysql.mysqldb` dialect with minimal -changes. + This dialect is based primarily on the :mod:`.mysql.mysqldb` dialect with minimal + changes. -.. versionadded:: 0.7.8 + .. versionadded:: 0.7.8 -Connecting ----------- - -Connect string format:: - - mysql+gaerdbms:/// - -E.g.:: - - create_engine('mysql+gaerdbms:///mydb', - connect_args={"instance":"instancename"}) Pooling ------- @@ -32,11 +26,17 @@ default. """ -from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb -from sqlalchemy.pool import NullPool +import os + +from .mysqldb import MySQLDialect_mysqldb +from ...pool import NullPool import re +def _is_dev_environment(): + return os.environ.get('SERVER_SOFTWARE', '').startswith('Development/') + + class MySQLDialect_gaerdbms(MySQLDialect_mysqldb): @classmethod @@ -49,7 +49,10 @@ class MySQLDialect_gaerdbms(MySQLDialect_mysqldb): # see also http://stackoverflow.com/q/14224679/34549 from google.appengine.api import apiproxy_stub_map - if apiproxy_stub_map.apiproxy.GetStub('rdbms'): + if _is_dev_environment(): + from google.appengine.api import rdbms_mysqldb + return rdbms_mysqldb + elif apiproxy_stub_map.apiproxy.GetStub('rdbms'): from google.storage.speckle.python.api import rdbms_apiproxy return rdbms_apiproxy else: @@ -63,21 +66,18 @@ class MySQLDialect_gaerdbms(MySQLDialect_mysqldb): def create_connect_args(self, url): opts = url.translate_connect_args() - # 'dsn' and 'instance' are because we are skipping - # the traditional google.api.rdbms wrapper - - opts['dsn'] = '' - opts['instance'] = url.query['instance'] + if not _is_dev_environment(): + # 'dsn' and 'instance' are because we are skipping + # the traditional google.api.rdbms wrapper + opts['dsn'] = '' + opts['instance'] = url.query['instance'] return [], opts def _extract_error_code(self, exception): - match = re.compile(r"^(\d+):").match(str(exception)) + match = re.compile(r"^(\d+)L?:|^\((\d+)L?,").match(str(exception)) # The rdbms api will wrap then re-raise some types of errors # making this regex return no matches. - if match: - code = match.group(1) - else: - code = None + code = match.group(1) or match.group(2) if match else None if code: return int(code) diff --git a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py index bd8ee013..b6e7c75f 100644 --- a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -1,34 +1,25 @@ # mysql/mysqlconnector.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via the MySQL Connector/Python adapter. +""" +.. dialect:: mysql+mysqlconnector + :name: MySQL Connector/Python + :dbapi: myconnpy + :connectstring: mysql+mysqlconnector://:@[:]/ + :url: https://launchpad.net/myconnpy -MySQL Connector/Python is available at: - - https://launchpad.net/myconnpy - -Connecting ------------ - -Connect string format:: - - mysql+mysqlconnector://:@[:]/ """ -import re - -from sqlalchemy.dialects.mysql.base import (MySQLDialect, +from .base import (MySQLDialect, MySQLExecutionContext, MySQLCompiler, MySQLIdentifierPreparer, BIT) -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy import exc, log, schema, sql, types as sqltypes, util -from sqlalchemy import processors +from ... import util + class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext): @@ -37,24 +28,28 @@ class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext): class MySQLCompiler_mysqlconnector(MySQLCompiler): - def visit_mod(self, binary, **kw): - return self.process(binary.left) + " %% " + self.process(binary.right) + def visit_mod_binary(self, binary, operator, **kw): + return self.process(binary.left, **kw) + " %% " + \ + self.process(binary.right, **kw) def post_process_text(self, text): return text.replace('%', '%%') + class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer): def _escape_identifier(self, value): value = value.replace(self.escape_quote, self.escape_to_quote) return value.replace("%", "%%") + class _myconnpyBIT(BIT): def result_processor(self, dialect, coltype): """MySQL-connector already converts mysql bits, so.""" return None + class MySQLDialect_mysqlconnector(MySQLDialect): driver = 'mysqlconnector' supports_unicode_statements = True @@ -84,12 +79,13 @@ class MySQLDialect_mysqlconnector(MySQLDialect): def create_connect_args(self, url): opts = url.translate_connect_args(username='user') + opts.update(url.query) util.coerce_kw_type(opts, 'buffered', bool) util.coerce_kw_type(opts, 'raise_on_warnings', bool) - opts['buffered'] = True - opts['raise_on_warnings'] = True + opts.setdefault('buffered', True) + opts.setdefault('raise_on_warnings', True) # FOUND_ROWS must be set in ClientFlag to enable # supports_sane_rowcount. @@ -116,9 +112,10 @@ class MySQLDialect_mysqlconnector(MySQLDialect): def is_disconnect(self, e, connection, cursor): errnos = (2006, 2013, 2014, 2045, 2055, 2048) - exceptions = (self.dbapi.OperationalError,self.dbapi.InterfaceError) + exceptions = (self.dbapi.OperationalError, self.dbapi.InterfaceError) if isinstance(e, exceptions): - return e.errno in errnos + return e.errno in errnos or \ + "MySQL Connection not available." in str(e) else: return False diff --git a/libs/sqlalchemy/dialects/mysql/mysqldb.py b/libs/sqlalchemy/dialects/mysql/mysqldb.py index c6ae5333..c6942ae2 100644 --- a/libs/sqlalchemy/dialects/mysql/mysqldb.py +++ b/libs/sqlalchemy/dialects/mysql/mysqldb.py @@ -1,23 +1,17 @@ # mysql/mysqldb.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via the MySQL-python adapter. +""" -MySQL-Python is available at: +.. dialect:: mysql+mysqldb + :name: MySQL-Python + :dbapi: mysqldb + :connectstring: mysql+mysqldb://:@[:]/ + :url: http://sourceforge.net/projects/mysql-python - http://sourceforge.net/projects/mysql-python - -At least version 1.2.1 or 1.2.2 should be used. - -Connecting ------------ - -Connect string format:: - - mysql+mysqldb://:@[:]/ Unicode ------- @@ -54,15 +48,16 @@ It is strongly advised to use the latest version of MySQL-Python. """ -from sqlalchemy.dialects.mysql.base import (MySQLDialect, MySQLExecutionContext, +from .base import (MySQLDialect, MySQLExecutionContext, MySQLCompiler, MySQLIdentifierPreparer) -from sqlalchemy.connectors.mysqldb import ( +from ...connectors.mysqldb import ( MySQLDBExecutionContext, MySQLDBCompiler, MySQLDBIdentifierPreparer, MySQLDBConnector ) + class MySQLExecutionContext_mysqldb(MySQLDBExecutionContext, MySQLExecutionContext): pass @@ -74,6 +69,7 @@ class MySQLCompiler_mysqldb(MySQLDBCompiler, MySQLCompiler): class MySQLIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, MySQLIdentifierPreparer): pass + class MySQLDialect_mysqldb(MySQLDBConnector, MySQLDialect): execution_ctx_cls = MySQLExecutionContext_mysqldb statement_compiler = MySQLCompiler_mysqldb diff --git a/libs/sqlalchemy/dialects/mysql/oursql.py b/libs/sqlalchemy/dialects/mysql/oursql.py index d6d8e9ff..e6b50f33 100644 --- a/libs/sqlalchemy/dialects/mysql/oursql.py +++ b/libs/sqlalchemy/dialects/mysql/oursql.py @@ -1,21 +1,16 @@ # mysql/oursql.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via the oursql adapter. +""" -OurSQL is available at: - - http://packages.python.org/oursql/ - -Connecting ------------ - -Connect string format:: - - mysql+oursql://:@[:]/ +.. dialect:: mysql+oursql + :name: OurSQL + :dbapi: oursql + :connectstring: mysql+oursql://:@[:]/ + :url: http://packages.python.org/oursql/ Unicode ------- @@ -40,13 +35,8 @@ defaults to, there is a separate parameter:: import re -from sqlalchemy.dialects.mysql.base import (BIT, MySQLDialect, MySQLExecutionContext, - MySQLCompiler, MySQLIdentifierPreparer) -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy import exc, log, schema, sql, types as sqltypes, util -from sqlalchemy import processors - +from .base import (BIT, MySQLDialect, MySQLExecutionContext) +from ... import types as sqltypes, util class _oursqlBIT(BIT): @@ -62,12 +52,13 @@ class MySQLExecutionContext_oursql(MySQLExecutionContext): def plain_query(self): return self.execution_options.get('_oursql_plain_query', False) + class MySQLDialect_oursql(MySQLDialect): driver = 'oursql' -# Py2K - supports_unicode_binds = True - supports_unicode_statements = True -# end Py2K + + if util.py2k: + supports_unicode_binds = True + supports_unicode_statements = True supports_native_decimal = True @@ -99,12 +90,11 @@ class MySQLDialect_oursql(MySQLDialect): connection.cursor().execute('BEGIN', plain_query=True) def _xa_query(self, connection, query, xid): -# Py2K - arg = connection.connection._escape_string(xid) -# end Py2K -# Py3K -# charset = self._connection_charset -# arg = connection.connection._escape_string(xid.encode(charset)).decode(charset) + if util.py2k: + arg = connection.connection._escape_string(xid) + else: + charset = self._connection_charset + arg = connection.connection._escape_string(xid.encode(charset)).decode(charset) arg = "'%s'" % arg connection.execution_options(_oursql_plain_query=True).execute(query % arg) @@ -135,64 +125,67 @@ class MySQLDialect_oursql(MySQLDialect): # Q: why didn't we need all these "plain_query" overrides earlier ? # am i on a newer/older version of OurSQL ? def has_table(self, connection, table_name, schema=None): - return MySQLDialect.has_table(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - table_name, schema) - - def get_table_options(self, connection, table_name, schema=None, **kw): - return MySQLDialect.get_table_options(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - table_name, - schema = schema, - **kw + return MySQLDialect.has_table( + self, + connection.connect().execution_options(_oursql_plain_query=True), + table_name, + schema ) + def get_table_options(self, connection, table_name, schema=None, **kw): + return MySQLDialect.get_table_options( + self, + connection.connect().execution_options(_oursql_plain_query=True), + table_name, + schema=schema, + **kw + ) def get_columns(self, connection, table_name, schema=None, **kw): - return MySQLDialect.get_columns(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - table_name, - schema=schema, - **kw + return MySQLDialect.get_columns( + self, + connection.connect().execution_options(_oursql_plain_query=True), + table_name, + schema=schema, + **kw ) def get_view_names(self, connection, schema=None, **kw): - return MySQLDialect.get_view_names(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - schema=schema, - **kw + return MySQLDialect.get_view_names( + self, + connection.connect().execution_options(_oursql_plain_query=True), + schema=schema, + **kw ) def get_table_names(self, connection, schema=None, **kw): - return MySQLDialect.get_table_names(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - schema + return MySQLDialect.get_table_names( + self, + connection.connect().execution_options(_oursql_plain_query=True), + schema ) def get_schema_names(self, connection, **kw): - return MySQLDialect.get_schema_names(self, - connection.connect().\ - execution_options(_oursql_plain_query=True), - **kw + return MySQLDialect.get_schema_names( + self, + connection.connect().execution_options(_oursql_plain_query=True), + **kw ) def initialize(self, connection): return MySQLDialect.initialize( - self, - connection.execution_options(_oursql_plain_query=True) - ) + self, + connection.execution_options(_oursql_plain_query=True) + ) def _show_create_table(self, connection, table, charset=None, full_name=None): - return MySQLDialect._show_create_table(self, - connection.contextual_connect(close_with_result=True). - execution_options(_oursql_plain_query=True), - table, charset, full_name) + return MySQLDialect._show_create_table( + self, + connection.contextual_connect(close_with_result=True). + execution_options(_oursql_plain_query=True), + table, charset, full_name + ) def is_disconnect(self, e, connection, cursor): if isinstance(e, self.dbapi.ProgrammingError): diff --git a/libs/sqlalchemy/dialects/mysql/pymysql.py b/libs/sqlalchemy/dialects/mysql/pymysql.py index f5aaa122..74de09c4 100644 --- a/libs/sqlalchemy/dialects/mysql/pymysql.py +++ b/libs/sqlalchemy/dialects/mysql/pymysql.py @@ -1,21 +1,16 @@ # mysql/pymysql.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via the pymysql adapter. +""" -pymysql is available at: - - http://code.google.com/p/pymysql/ - -Connecting ----------- - -Connect string:: - - mysql+pymysql://:@/[?] +.. dialect:: mysql+pymysql + :name: PyMySQL + :dbapi: pymysql + :connectstring: mysql+pymysql://:@/[?] + :url: http://code.google.com/p/pymysql/ MySQL-Python Compatibility -------------------------- @@ -26,14 +21,24 @@ the pymysql driver as well. """ -from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb +from .mysqldb import MySQLDialect_mysqldb +from ...util import py3k class MySQLDialect_pymysql(MySQLDialect_mysqldb): driver = 'pymysql' description_encoding = None + if py3k: + supports_unicode_statements = True + @classmethod def dbapi(cls): return __import__('pymysql') -dialect = MySQLDialect_pymysql \ No newline at end of file + if py3k: + def _extract_error_code(self, exception): + if isinstance(exception.args[0], Exception): + exception = exception.args[0] + return exception.args[0] + +dialect = MySQLDialect_pymysql diff --git a/libs/sqlalchemy/dialects/mysql/pyodbc.py b/libs/sqlalchemy/dialects/mysql/pyodbc.py index 5d631afb..e60e39ce 100644 --- a/libs/sqlalchemy/dialects/mysql/pyodbc.py +++ b/libs/sqlalchemy/dialects/mysql/pyodbc.py @@ -1,21 +1,18 @@ # mysql/pyodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via the pyodbc adapter. +""" -pyodbc is available at: - http://pypi.python.org/pypi/pyodbc/ +.. dialect:: mysql+pyodbc + :name: PyODBC + :dbapi: pyodbc + :connectstring: mysql+pyodbc://:@ + :url: http://pypi.python.org/pypi/pyodbc/ -Connecting ----------- - -Connect string:: - - mysql+pyodbc://:@ Limitations ----------- @@ -27,12 +24,12 @@ of OurSQL, MySQLdb, or MySQL-connector/Python. """ -from sqlalchemy.dialects.mysql.base import MySQLDialect, MySQLExecutionContext -from sqlalchemy.connectors.pyodbc import PyODBCConnector -from sqlalchemy.engine import base as engine_base -from sqlalchemy import util +from .base import MySQLDialect, MySQLExecutionContext +from ...connectors.pyodbc import PyODBCConnector +from ... import util import re + class MySQLExecutionContext_pyodbc(MySQLExecutionContext): def get_lastrowid(self): @@ -42,6 +39,7 @@ class MySQLExecutionContext_pyodbc(MySQLExecutionContext): cursor.close() return lastrowid + class MySQLDialect_pyodbc(PyODBCConnector, MySQLDialect): supports_unicode_statements = False execution_ctx_cls = MySQLExecutionContext_pyodbc diff --git a/libs/sqlalchemy/dialects/mysql/zxjdbc.py b/libs/sqlalchemy/dialects/mysql/zxjdbc.py index df479043..b5fcfbda 100644 --- a/libs/sqlalchemy/dialects/mysql/zxjdbc.py +++ b/libs/sqlalchemy/dialects/mysql/zxjdbc.py @@ -1,23 +1,16 @@ # mysql/zxjdbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the MySQL database via Jython's zxjdbc JDBC connector. +""" -JDBC Driver ------------ - -The official MySQL JDBC driver is at -http://dev.mysql.com/downloads/connector/j/. - -Connecting ----------- - -Connect string format: - - mysql+zxjdbc://:@[:]/ +.. dialect:: mysql+zxjdbc + :name: zxjdbc for Jython + :dbapi: zxjdbc + :connectstring: mysql+zxjdbc://:@[:]/ + :driverurl: http://dev.mysql.com/downloads/connector/j/ Character Sets -------------- @@ -31,9 +24,10 @@ overriden via a ``create_engine`` URL parameter. """ import re -from sqlalchemy import types as sqltypes, util -from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector -from sqlalchemy.dialects.mysql.base import BIT, MySQLDialect, MySQLExecutionContext +from ... import types as sqltypes, util +from ...connectors.zxJDBC import ZxJDBCConnector +from .base import BIT, MySQLDialect, MySQLExecutionContext + class _ZxJDBCBit(BIT): def result_processor(self, dialect, coltype): @@ -43,7 +37,7 @@ class _ZxJDBCBit(BIT): return value if isinstance(value, bool): return int(value) - v = 0L + v = 0 for i in value: v = v << 8 | (i & 0xff) value = v @@ -103,7 +97,7 @@ class MySQLDialect_zxjdbc(ZxJDBCConnector, MySQLDialect): if c: return int(c) - def _get_server_version_info(self,connection): + def _get_server_version_info(self, connection): dbapi_con = connection.connection version = [] r = re.compile('[.\-]') diff --git a/libs/sqlalchemy/dialects/oracle/__init__.py b/libs/sqlalchemy/dialects/oracle/__init__.py index a1e2a8dd..070e387d 100644 --- a/libs/sqlalchemy/dialects/oracle/__init__.py +++ b/libs/sqlalchemy/dialects/oracle/__init__.py @@ -1,5 +1,5 @@ # oracle/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -12,7 +12,7 @@ from sqlalchemy.dialects.oracle.base import \ VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, NUMBER,\ BLOB, BFILE, CLOB, NCLOB, TIMESTAMP, RAW,\ FLOAT, DOUBLE_PRECISION, LONG, dialect, INTERVAL,\ - VARCHAR2, NVARCHAR2, ROWID + VARCHAR2, NVARCHAR2, ROWID, dialect __all__ = ( diff --git a/libs/sqlalchemy/dialects/oracle/base.py b/libs/sqlalchemy/dialects/oracle/base.py index f82991bc..e5a16044 100644 --- a/libs/sqlalchemy/dialects/oracle/base.py +++ b/libs/sqlalchemy/dialects/oracle/base.py @@ -1,15 +1,14 @@ # oracle/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Oracle database. +""" +.. dialect:: oracle + :name: Oracle -Oracle version 8 through current (11g at the time of this writing) are supported. - -For information on connecting via specific drivers, see the documentation -for that driver. + Oracle version 8 through current (11g at the time of this writing) are supported. Connect Arguments ----------------- @@ -17,12 +16,12 @@ Connect Arguments The dialect supports several :func:`~sqlalchemy.create_engine()` arguments which affect the behavior of the dialect regardless of driver in use. -* *use_ansi* - Use ANSI JOIN constructs (see the section on Oracle 8). Defaults +* ``use_ansi`` - Use ANSI JOIN constructs (see the section on Oracle 8). Defaults to ``True``. If ``False``, Oracle-8 compatible constructs are used for joins. -* *optimize_limits* - defaults to ``False``. see the section on LIMIT/OFFSET. +* ``optimize_limits`` - defaults to ``False``. see the section on LIMIT/OFFSET. -* *use_binds_for_limits* - defaults to ``True``. see the section on LIMIT/OFFSET. +* ``use_binds_for_limits`` - defaults to ``True``. see the section on LIMIT/OFFSET. Auto Increment Behavior ----------------------- @@ -100,6 +99,41 @@ http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowFunctionsByDefault which installs a select compiler that overrides the generation of limit/offset with a window function. +RETURNING Support +----------------- + +The Oracle database supports a limited form of RETURNING, in order to retrieve result +sets of matched rows from INSERT, UPDATE and DELETE statements. Oracle's +RETURNING..INTO syntax only supports one row being returned, as it relies upon +OUT parameters in order to function. In addition, supported DBAPIs have further +limitations (see :ref:`cx_oracle_returning`). + +SQLAlchemy's "implicit returning" feature, which employs RETURNING within an INSERT +and sometimes an UPDATE statement in order to fetch newly generated primary key values +and other SQL defaults and expressions, is normally enabled on the Oracle +backend. By default, "implicit returning" typically only fetches the value of a +single ``nextval(some_seq)`` expression embedded into an INSERT in order to increment +a sequence within an INSERT statement and get the value back at the same time. +To disable this feature across the board, specify ``implicit_returning=False`` to +:func:`.create_engine`:: + + engine = create_engine("oracle://scott:tiger@dsn", implicit_returning=False) + +Implicit returning can also be disabled on a table-by-table basis as a table option:: + + # Core Table + my_table = Table("my_table", metadata, ..., implicit_returning=False) + + + # declarative + class MyClass(Base): + __tablename__ = 'my_table' + __table_args__ = {"implicit_returning": False} + +.. seealso:: + + :ref:`cx_oracle_returning` - additional cx_oracle-specific restrictions on implicit returning. + ON UPDATE CASCADE ----------------- @@ -134,16 +168,16 @@ Synonym/DBLINK Reflection ------------------------- When using reflection with Table objects, the dialect can optionally search for tables -indicated by synonyms that reference DBLINK-ed tables by passing the flag -oracle_resolve_synonyms=True as a keyword argument to the Table construct. If DBLINK -is not in use this flag should be left off. +indicated by synonyms, either in local or remote schemas or accessed over DBLINK, +by passing the flag oracle_resolve_synonyms=True as a +keyword argument to the Table construct. If synonyms are not in use +this flag should be left off. """ -import random, re +import re -from sqlalchemy import schema as sa_schema -from sqlalchemy import util, sql, log +from sqlalchemy import util, sql from sqlalchemy.engine import default, base, reflection from sqlalchemy.sql import compiler, visitors, expression from sqlalchemy.sql import operators as sql_operators, functions as sql_functions @@ -165,18 +199,22 @@ RESERVED_WORDS = \ NO_ARG_FNS = set('UID CURRENT_DATE SYSDATE USER ' 'CURRENT_TIME CURRENT_TIMESTAMP'.split()) + class RAW(sqltypes._Binary): __visit_name__ = 'RAW' OracleRaw = RAW + class NCLOB(sqltypes.Text): __visit_name__ = 'NCLOB' + class VARCHAR2(VARCHAR): __visit_name__ = 'VARCHAR2' NVARCHAR2 = NVARCHAR + class NUMBER(sqltypes.Numeric, sqltypes.Integer): __visit_name__ = 'NUMBER' @@ -202,18 +240,22 @@ class NUMBER(sqltypes.Numeric, sqltypes.Integer): class DOUBLE_PRECISION(sqltypes.Numeric): __visit_name__ = 'DOUBLE_PRECISION' + def __init__(self, precision=None, scale=None, asdecimal=None): if asdecimal is None: asdecimal = False super(DOUBLE_PRECISION, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal) + class BFILE(sqltypes.LargeBinary): __visit_name__ = 'BFILE' + class LONG(sqltypes.Text): __visit_name__ = 'LONG' + class INTERVAL(sqltypes.TypeEngine): __visit_name__ = 'INTERVAL' @@ -244,6 +286,7 @@ class INTERVAL(sqltypes.TypeEngine): def _type_affinity(self): return sqltypes.Interval + class ROWID(sqltypes.TypeEngine): """Oracle ROWID type. @@ -253,33 +296,32 @@ class ROWID(sqltypes.TypeEngine): __visit_name__ = 'ROWID' - class _OracleBoolean(sqltypes.Boolean): def get_dbapi_type(self, dbapi): return dbapi.NUMBER colspecs = { - sqltypes.Boolean : _OracleBoolean, - sqltypes.Interval : INTERVAL, + sqltypes.Boolean: _OracleBoolean, + sqltypes.Interval: INTERVAL, } ischema_names = { - 'VARCHAR2' : VARCHAR, - 'NVARCHAR2' : NVARCHAR, - 'CHAR' : CHAR, - 'DATE' : DATE, - 'NUMBER' : NUMBER, - 'BLOB' : BLOB, - 'BFILE' : BFILE, - 'CLOB' : CLOB, - 'NCLOB' : NCLOB, - 'TIMESTAMP' : TIMESTAMP, - 'TIMESTAMP WITH TIME ZONE' : TIMESTAMP, - 'INTERVAL DAY TO SECOND' : INTERVAL, - 'RAW' : RAW, - 'FLOAT' : FLOAT, - 'DOUBLE PRECISION' : DOUBLE_PRECISION, - 'LONG' : LONG, + 'VARCHAR2': VARCHAR, + 'NVARCHAR2': NVARCHAR, + 'CHAR': CHAR, + 'DATE': DATE, + 'NUMBER': NUMBER, + 'BLOB': BLOB, + 'BFILE': BFILE, + 'CLOB': CLOB, + 'NCLOB': NCLOB, + 'TIMESTAMP': TIMESTAMP, + 'TIMESTAMP WITH TIME ZONE': TIMESTAMP, + 'INTERVAL DAY TO SECOND': INTERVAL, + 'RAW': RAW, + 'FLOAT': FLOAT, + 'DOUBLE PRECISION': DOUBLE_PRECISION, + 'LONG': LONG, } @@ -336,9 +378,11 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler): if precision is None: return name elif scale is None: - return "%(name)s(%(precision)s)" % {'name':name,'precision': precision} + n = "%(name)s(%(precision)s)" + return n % {'name': name, 'precision': precision} else: - return "%(name)s(%(precision)s, %(scale)s)" % {'name':name,'precision': precision, 'scale' : scale} + n = "%(name)s(%(precision)s, %(scale)s)" + return n % {'name': name, 'precision': precision, 'scale': scale} def visit_string(self, type_): return self.visit_VARCHAR2(type_) @@ -354,13 +398,14 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler): return self._visit_varchar(type_, '', '') def _visit_varchar(self, type_, n, num): - if not n and self.dialect._supports_char_length: - return "VARCHAR%(two)s(%(length)s CHAR)" % { - 'length' : type_.length, - 'two':num} + if not type_.length: + return "%(n)sVARCHAR%(two)s" % {'two': num, 'n': n} + elif not n and self.dialect._supports_char_length: + varchar = "VARCHAR%(two)s(%(length)s CHAR)" + return varchar % {'length': type_.length, 'two': num} else: - return "%(n)sVARCHAR%(two)s(%(length)s)" % {'length' : type_.length, - 'two':num, 'n':n} + varchar = "%(n)sVARCHAR%(two)s(%(length)s)" + return varchar % {'length': type_.length, 'two': num, 'n': n} def visit_text(self, type_): return self.visit_CLOB(type_) @@ -382,13 +427,14 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler): def visit_RAW(self, type_): if type_.length: - return "RAW(%(length)s)" % {'length' : type_.length} + return "RAW(%(length)s)" % {'length': type_.length} else: return "RAW" def visit_ROWID(self, type_): return "ROWID" + class OracleCompiler(compiler.SQLCompiler): """Oracle compiler modifies the lexical structure of Select statements to work under non-ANSI configured Oracle databases, if @@ -398,7 +444,7 @@ class OracleCompiler(compiler.SQLCompiler): compound_keywords = util.update_copy( compiler.SQLCompiler.compound_keywords, { - expression.CompoundSelect.EXCEPT : 'MINUS' + expression.CompoundSelect.EXCEPT: 'MINUS' } ) @@ -407,8 +453,9 @@ class OracleCompiler(compiler.SQLCompiler): self._quoted_bind_names = {} super(OracleCompiler, self).__init__(*args, **kwargs) - def visit_mod(self, binary, **kw): - return "mod(%s, %s)" % (self.process(binary.left), self.process(binary.right)) + def visit_mod_binary(self, binary, operator, **kw): + return "mod(%s, %s)" % (self.process(binary.left, **kw), + self.process(binary.right, **kw)) def visit_now_func(self, fn, **kw): return "CURRENT_TIMESTAMP" @@ -416,8 +463,15 @@ class OracleCompiler(compiler.SQLCompiler): def visit_char_length_func(self, fn, **kw): return "LENGTH" + self.function_argspec(fn, **kw) - def visit_match_op(self, binary, **kw): - return "CONTAINS (%s, %s)" % (self.process(binary.left), self.process(binary.right)) + def visit_match_op_binary(self, binary, operator, **kw): + return "CONTAINS (%s, %s)" % (self.process(binary.left), + self.process(binary.right)) + + def visit_true(self, expr, **kw): + return '1' + + def visit_false(self, expr, **kw): + return '0' def get_select_hint_text(self, byfroms): return " ".join( @@ -444,8 +498,13 @@ class OracleCompiler(compiler.SQLCompiler): return compiler.SQLCompiler.visit_join(self, join, **kwargs) else: kwargs['asfrom'] = True + if isinstance(join.right, expression.FromGrouping): + right = join.right.element + else: + right = join.right return self.process(join.left, **kwargs) + \ - ", " + self.process(join.right, **kwargs) + ", " + self.process(right, **kwargs) + def _get_nonansi_join_whereclause(self, froms): clauses = [] @@ -454,18 +513,20 @@ class OracleCompiler(compiler.SQLCompiler): if join.isouter: def visit_binary(binary): if binary.operator == sql_operators.eq: - if binary.left.table is join.right: + if join.right.is_derived_from(binary.left.table): binary.left = _OuterJoinColumn(binary.left) - elif binary.right.table is join.right: + elif join.right.is_derived_from(binary.right.table): binary.right = _OuterJoinColumn(binary.right) clauses.append(visitors.cloned_traverse(join.onclause, {}, - {'binary':visit_binary})) + {'binary': visit_binary})) else: clauses.append(join.onclause) for j in join.left, join.right: if isinstance(j, expression.Join): visit_join(j) + elif isinstance(j, expression.FromGrouping): + visit_join(j.element) for f in froms: if isinstance(f, expression.Join): @@ -498,20 +559,25 @@ class OracleCompiler(compiler.SQLCompiler): return self.process(alias.original, **kwargs) def returning_clause(self, stmt, returning_cols): + columns = [] + binds = [] + for i, column in enumerate(expression._select_iterables(returning_cols)): + if column.type._has_column_expression: + col_expr = column.type.column_expression(column) + else: + col_expr = column + outparam = sql.outparam("ret_%d" % i, type_=column.type) + self.binds[outparam.key] = outparam + binds.append(self.bindparam_string(self._truncate_bindparam(outparam))) + columns.append(self.process(col_expr, within_columns_clause=False)) + self.result_map[outparam.key] = ( + outparam.key, + (column, getattr(column, 'name', None), + getattr(column, 'key', None)), + column.type + ) - def create_out_param(col, i): - bindparam = sql.outparam("ret_%d" % i, type_=col.type) - self.binds[bindparam.key] = bindparam - return self.bindparam_string(self._truncate_bindparam(bindparam)) - - columnlist = list(expression._select_iterables(returning_cols)) - - # within_columns_clause =False so that labels (foo AS bar) don't render - columns = [self.process(c, within_columns_clause=False, result_map=self.result_map) for c in columnlist] - - binds = [create_out_param(c, i) for i, c in enumerate(columnlist)] - - return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) + return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) def _TODO_visit_compound_select(self, select): """Need to determine how to get ``LIMIT``/``OFFSET`` into a ``UNION`` for Oracle.""" @@ -524,12 +590,8 @@ class OracleCompiler(compiler.SQLCompiler): if not getattr(select, '_oracle_visit', None): if not self.dialect.use_ansi: - if self.stack and 'from' in self.stack[-1]: - existingfroms = self.stack[-1]['from'] - else: - existingfroms = None - - froms = select._get_display_froms(existingfroms) + froms = self._display_froms_for_select( + select, kwargs.get('asfrom', False)) whereclause = self._get_nonansi_join_whereclause(froms) if whereclause is not None: select = select.where(whereclause) @@ -570,7 +632,7 @@ class OracleCompiler(compiler.SQLCompiler): # If needed, add the ora_rn, and wrap again with offset. if select._offset is None: - limitselect.for_update = select.for_update + limitselect._for_update_arg = select._for_update_arg select = limitselect else: limitselect = limitselect.column( @@ -579,7 +641,7 @@ class OracleCompiler(compiler.SQLCompiler): limitselect._is_wrapper = True offsetselect = sql.select( - [c for c in limitselect.c if c.key!='ora_rn']) + [c for c in limitselect.c if c.key != 'ora_rn']) offsetselect._oracle_visit = True offsetselect._is_wrapper = True @@ -587,9 +649,9 @@ class OracleCompiler(compiler.SQLCompiler): if not self.dialect.use_binds_for_limits: offset_value = sql.literal_column("%d" % offset_value) offsetselect.append_whereclause( - sql.literal_column("ora_rn")>offset_value) + sql.literal_column("ora_rn") > offset_value) - offsetselect.for_update = select.for_update + offsetselect._for_update_arg = select._for_update_arg select = offsetselect kwargs['iswrapper'] = getattr(select, '_is_wrapper', False) @@ -601,10 +663,20 @@ class OracleCompiler(compiler.SQLCompiler): def for_update_clause(self, select): if self.is_subquery(): return "" - elif select.for_update == "nowait": - return " FOR UPDATE NOWAIT" - else: - return super(OracleCompiler, self).for_update_clause(select) + + tmp = ' FOR UPDATE' + + if select._for_update_arg.of: + tmp += ' OF ' + ', '.join( + self.process(elem) for elem in + select._for_update_arg.of + ) + + if select._for_update_arg.nowait: + tmp += " NOWAIT" + + return tmp + class OracleDDLCompiler(compiler.DDLCompiler): @@ -623,17 +695,22 @@ class OracleDDLCompiler(compiler.DDLCompiler): return text + def visit_create_index(self, create, **kw): + return super(OracleDDLCompiler, self).\ + visit_create_index(create, include_schema=True) + + class OracleIdentifierPreparer(compiler.IdentifierPreparer): reserved_words = set([x.lower() for x in RESERVED_WORDS]) - illegal_initial_characters = set(xrange(0, 10)).union(["_", "$"]) + illegal_initial_characters = set(range(0, 10)).union(["_", "$"]) def _bindparam_requires_quotes(self, value): """Return True if the given identifier requires quoting.""" lc_value = value.lower() return (lc_value in self.reserved_words or value[0] in self.illegal_initial_characters - or not self.legal_characters.match(unicode(value)) + or not self.legal_characters.match(util.text_type(value)) ) def format_savepoint(self, savepoint): @@ -647,6 +724,7 @@ class OracleExecutionContext(default.DefaultExecutionContext): self.dialect.identifier_preparer.format_sequence(seq) + ".nextval FROM DUAL", type_) + class OracleDialect(default.DefaultDialect): name = 'oracle' supports_alter = True @@ -736,10 +814,9 @@ class OracleDialect(default.DefaultDialect): def normalize_name(self, name): if name is None: return None - # Py2K - if isinstance(name, str): - name = name.decode(self.encoding) - # end Py2K + if util.py2k: + if isinstance(name, str): + name = name.decode(self.encoding) if name.upper() == name and \ not self.identifier_preparer._requires_quotes(name.lower()): return name.lower() @@ -751,16 +828,15 @@ class OracleDialect(default.DefaultDialect): return None elif name.lower() == name and not self.identifier_preparer._requires_quotes(name.lower()): name = name.upper() - # Py2K - if not self.supports_unicode_binds: - name = name.encode(self.encoding) - else: - name = unicode(name) - # end Py2K + if util.py2k: + if not self.supports_unicode_binds: + name = name.encode(self.encoding) + else: + name = unicode(name) return name def _get_default_schema_name(self, connection): - return self.normalize_name(connection.execute(u'SELECT USER FROM DUAL').scalar()) + return self.normalize_name(connection.execute('SELECT USER FROM DUAL').scalar()) def _resolve_synonym(self, connection, desired_owner=None, desired_synonym=None, desired_table=None): """search for a local synonym matching the given desired owner/name. @@ -770,14 +846,15 @@ class OracleDialect(default.DefaultDialect): returns the actual name, owner, dblink name, and synonym name if found. """ - q = "SELECT owner, table_owner, table_name, db_link, synonym_name FROM all_synonyms WHERE " + q = "SELECT owner, table_owner, table_name, db_link, "\ + "synonym_name FROM all_synonyms WHERE " clauses = [] params = {} if desired_synonym: clauses.append("synonym_name = :synonym_name") params['synonym_name'] = desired_synonym if desired_owner: - clauses.append("table_owner = :desired_owner") + clauses.append("owner = :desired_owner") params['desired_owner'] = desired_owner if desired_table: clauses.append("table_name = :tname") @@ -808,19 +885,29 @@ class OracleDialect(default.DefaultDialect): if resolve_synonyms: actual_name, owner, dblink, synonym = self._resolve_synonym( - connection, - desired_owner=self.denormalize_name(schema), - desired_synonym=self.denormalize_name(table_name) - ) + connection, + desired_owner=self.denormalize_name(schema), + desired_synonym=self.denormalize_name(table_name) + ) else: actual_name, owner, dblink, synonym = None, None, None, None if not actual_name: actual_name = self.denormalize_name(table_name) - if not dblink: - dblink = '' - if not owner: + + if dblink: + # using user_db_links here since all_db_links appears + # to have more restricted permissions. + # http://docs.oracle.com/cd/B28359_01/server.111/b28310/ds_admin005.htm + # will need to hear from more users if we are doing + # the right thing here. See [ticket:2619] + owner = connection.scalar( + sql.text("SELECT username FROM user_db_links " + "WHERE db_link=:link"), link=dblink) + dblink = "@" + dblink + elif not owner: owner = self.denormalize_name(schema or self.default_schema_name) - return (actual_name, owner, dblink, synonym) + + return (actual_name, owner, dblink or '', synonym) @reflection.cache def get_schema_names(self, connection, **kw): @@ -843,7 +930,6 @@ class OracleDialect(default.DefaultDialect): cursor = connection.execute(s, owner=schema) return [self.normalize_name(row[0]) for row in cursor] - @reflection.cache def get_view_names(self, connection, schema=None, **kw): schema = self.denormalize_name(schema or self.default_schema_name) @@ -877,18 +963,24 @@ class OracleDialect(default.DefaultDialect): else: char_length_col = 'data_length' - c = connection.execute(sql.text( - "SELECT column_name, data_type, %(char_length_col)s, data_precision, data_scale, " - "nullable, data_default FROM ALL_TAB_COLUMNS%(dblink)s " - "WHERE table_name = :table_name AND owner = :owner " - "ORDER BY column_id" % {'dblink': dblink, 'char_length_col':char_length_col}), - table_name=table_name, owner=schema) + params = {"table_name": table_name} + text = "SELECT column_name, data_type, %(char_length_col)s, "\ + "data_precision, data_scale, "\ + "nullable, data_default FROM ALL_TAB_COLUMNS%(dblink)s "\ + "WHERE table_name = :table_name" + if schema is not None: + params['owner'] = schema + text += " AND owner = :owner " + text += " ORDER BY column_id" + text = text % {'dblink': dblink, 'char_length_col': char_length_col} + + c = connection.execute(sql.text(text), **params) for row in c: (colname, orig_colname, coltype, length, precision, scale, nullable, default) = \ - (self.normalize_name(row[0]), row[0], row[1], row[2], row[3], row[4], row[5]=='Y', row[6]) + (self.normalize_name(row[0]), row[0], row[1], row[2], row[3], row[4], row[5] == 'Y', row[6]) - if coltype == 'NUMBER' : + if coltype == 'NUMBER': coltype = NUMBER(precision, scale) elif coltype in ('VARCHAR2', 'NVARCHAR2', 'CHAR'): coltype = self.ischema_names.get(coltype)(length) @@ -908,7 +1000,7 @@ class OracleDialect(default.DefaultDialect): 'type': coltype, 'nullable': nullable, 'default': default, - 'autoincrement':default is None + 'autoincrement': default is None } if orig_colname.lower() == orig_colname: cdict['quote'] = True @@ -920,33 +1012,40 @@ class OracleDialect(default.DefaultDialect): def get_indexes(self, connection, table_name, schema=None, resolve_synonyms=False, dblink='', **kw): - info_cache = kw.get('info_cache') (table_name, schema, dblink, synonym) = \ self._prepare_reflection_args(connection, table_name, schema, resolve_synonyms, dblink, info_cache=info_cache) indexes = [] - q = sql.text(""" - SELECT a.index_name, a.column_name, b.uniqueness - FROM ALL_IND_COLUMNS%(dblink)s a, - ALL_INDEXES%(dblink)s b - WHERE - a.index_name = b.index_name - AND a.table_owner = b.table_owner - AND a.table_name = b.table_name - AND a.table_name = :table_name - AND a.table_owner = :schema - ORDER BY a.index_name, a.column_position""" % {'dblink': dblink}) - rp = connection.execute(q, table_name=self.denormalize_name(table_name), - schema=self.denormalize_name(schema)) + params = {'table_name': table_name} + text = \ + "SELECT a.index_name, a.column_name, b.uniqueness "\ + "\nFROM ALL_IND_COLUMNS%(dblink)s a, "\ + "\nALL_INDEXES%(dblink)s b "\ + "\nWHERE "\ + "\na.index_name = b.index_name "\ + "\nAND a.table_owner = b.table_owner "\ + "\nAND a.table_name = b.table_name "\ + "\nAND a.table_name = :table_name " + + if schema is not None: + params['schema'] = schema + text += "AND a.table_owner = :schema " + + text += "ORDER BY a.index_name, a.column_position" + + text = text % {'dblink': dblink} + + q = sql.text(text) + rp = connection.execute(q, **params) indexes = [] last_index_name = None - pkeys = self.get_primary_keys(connection, table_name, schema, - resolve_synonyms=resolve_synonyms, - dblink=dblink, - info_cache=kw.get('info_cache')) + pk_constraint = self.get_pk_constraint( + connection, table_name, schema, resolve_synonyms=resolve_synonyms, + dblink=dblink, info_cache=kw.get('info_cache')) + pkeys = pk_constraint['constrained_columns'] uniqueness = dict(NONUNIQUE=False, UNIQUE=True) oracle_sys_col = re.compile(r'SYS_NC\d+\$', re.IGNORECASE) @@ -982,46 +1081,43 @@ class OracleDialect(default.DefaultDialect): def _get_constraint_data(self, connection, table_name, schema=None, dblink='', **kw): - rp = connection.execute( - sql.text("""SELECT - ac.constraint_name, - ac.constraint_type, - loc.column_name AS local_column, - rem.table_name AS remote_table, - rem.column_name AS remote_column, - rem.owner AS remote_owner, - loc.position as loc_pos, - rem.position as rem_pos - FROM all_constraints%(dblink)s ac, - all_cons_columns%(dblink)s loc, - all_cons_columns%(dblink)s rem - WHERE ac.table_name = :table_name - AND ac.constraint_type IN ('R','P') - AND ac.owner = :owner - AND ac.owner = loc.owner - AND ac.constraint_name = loc.constraint_name - AND ac.r_owner = rem.owner(+) - AND ac.r_constraint_name = rem.constraint_name(+) - AND (rem.position IS NULL or loc.position=rem.position) - ORDER BY ac.constraint_name, loc.position""" % {'dblink': dblink}), - table_name=table_name, owner=schema) + params = {'table_name': table_name} + + text = \ + "SELECT"\ + "\nac.constraint_name,"\ + "\nac.constraint_type,"\ + "\nloc.column_name AS local_column,"\ + "\nrem.table_name AS remote_table,"\ + "\nrem.column_name AS remote_column,"\ + "\nrem.owner AS remote_owner,"\ + "\nloc.position as loc_pos,"\ + "\nrem.position as rem_pos"\ + "\nFROM all_constraints%(dblink)s ac,"\ + "\nall_cons_columns%(dblink)s loc,"\ + "\nall_cons_columns%(dblink)s rem"\ + "\nWHERE ac.table_name = :table_name"\ + "\nAND ac.constraint_type IN ('R','P')" + + if schema is not None: + params['owner'] = schema + text += "\nAND ac.owner = :owner" + + text += \ + "\nAND ac.owner = loc.owner"\ + "\nAND ac.constraint_name = loc.constraint_name"\ + "\nAND ac.r_owner = rem.owner(+)"\ + "\nAND ac.r_constraint_name = rem.constraint_name(+)"\ + "\nAND (rem.position IS NULL or loc.position=rem.position)"\ + "\nORDER BY ac.constraint_name, loc.position" + + text = text % {'dblink': dblink} + rp = connection.execute(sql.text(text), **params) constraint_data = rp.fetchall() return constraint_data - def get_primary_keys(self, connection, table_name, schema=None, **kw): - """ - - kw arguments can be: - - oracle_resolve_synonyms - - dblink - - """ - return self._get_primary_keys(connection, table_name, schema, **kw)[0] - @reflection.cache - def _get_primary_keys(self, connection, table_name, schema=None, **kw): + def get_pk_constraint(self, connection, table_name, schema=None, **kw): resolve_synonyms = kw.get('oracle_resolve_synonyms', False) dblink = kw.get('dblink', '') info_cache = kw.get('info_cache') @@ -1037,22 +1133,13 @@ class OracleDialect(default.DefaultDialect): info_cache=kw.get('info_cache')) for row in constraint_data: - #print "ROW:" , row (cons_name, cons_type, local_column, remote_table, remote_column, remote_owner) = \ row[0:2] + tuple([self.normalize_name(x) for x in row[2:6]]) if cons_type == 'P': if constraint_name is None: constraint_name = self.normalize_name(cons_name) pkeys.append(local_column) - return pkeys, constraint_name - - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - cols, name = self._get_primary_keys(connection, table_name, schema=schema, **kw) - - return { - 'constrained_columns':cols, - 'name':name - } + return {'constrained_columns': pkeys, 'name': constraint_name} @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): @@ -1066,7 +1153,7 @@ class OracleDialect(default.DefaultDialect): """ - requested_schema = schema # to check later on + requested_schema = schema # to check later on resolve_synonyms = kw.get('oracle_resolve_synonyms', False) dblink = kw.get('dblink', '') info_cache = kw.get('info_cache') @@ -1082,11 +1169,11 @@ class OracleDialect(default.DefaultDialect): def fkey_rec(): return { - 'name' : None, - 'constrained_columns' : [], - 'referred_schema' : None, - 'referred_table' : None, - 'referred_columns' : [] + 'name': None, + 'constrained_columns': [], + 'referred_schema': None, + 'referred_table': None, + 'referred_columns': [] } fkeys = util.defaultdict(fkey_rec) @@ -1101,7 +1188,7 @@ class OracleDialect(default.DefaultDialect): util.warn( ("Got 'None' querying 'table_name' from " "all_cons_columns%(dblink)s - does the user have " - "proper rights to the table?") % {'dblink':dblink}) + "proper rights to the table?") % {'dblink': dblink}) continue rec = fkeys[cons_name] @@ -1128,7 +1215,7 @@ class OracleDialect(default.DefaultDialect): local_cols.append(local_column) remote_cols.append(remote_column) - return fkeys.values() + return list(fkeys.values()) @reflection.cache def get_view_definition(self, connection, view_name, schema=None, @@ -1138,25 +1225,25 @@ class OracleDialect(default.DefaultDialect): self._prepare_reflection_args(connection, view_name, schema, resolve_synonyms, dblink, info_cache=info_cache) - s = sql.text(""" - SELECT text FROM all_views - WHERE owner = :schema - AND view_name = :view_name - """) - rp = connection.execute(s, - view_name=view_name, schema=schema).scalar() + + params = {'view_name': view_name} + text = "SELECT text FROM all_views WHERE view_name=:view_name" + + if schema is not None: + text += " AND owner = :schema" + params['schema'] = schema + + rp = connection.execute(sql.text(text), **params).scalar() if rp: - return rp.decode(self.encoding) + if util.py2k: + rp = rp.decode(self.encoding) + return rp else: return None - class _OuterJoinColumn(sql.ClauseElement): __visit_name__ = 'outer_join_column' def __init__(self, column): self.column = column - - - diff --git a/libs/sqlalchemy/dialects/oracle/cx_oracle.py b/libs/sqlalchemy/dialects/oracle/cx_oracle.py index 0154180d..c427e4bc 100644 --- a/libs/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/libs/sqlalchemy/dialects/oracle/cx_oracle.py @@ -1,48 +1,57 @@ # oracle/cx_oracle.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Oracle database via the cx_oracle driver. +""" -Driver ------- +.. dialect:: oracle+cx_oracle + :name: cx-Oracle + :dbapi: cx_oracle + :connectstring: oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...] + :url: http://cx-oracle.sourceforge.net/ -The Oracle dialect uses the cx_oracle driver, available at -http://cx-oracle.sourceforge.net/ . The dialect has several behaviors -which are specifically tailored towards compatibility with this module. -Version 5.0 or greater is **strongly** recommended, as SQLAlchemy makes -extensive use of the cx_oracle output converters for numeric and -string conversions. +Additional Connect Arguments +---------------------------- -Connecting ----------- +When connecting with ``dbname`` present, the host, port, and dbname tokens are +converted to a TNS name using +the cx_oracle ``makedsn()`` function. Otherwise, the host token is taken +directly as a TNS name. -Connecting with create_engine() uses the standard URL approach of -``oracle://user:pass@host:port/dbname[?key=value&key=value...]``. If dbname is present, the -host, port, and dbname tokens are converted to a TNS name using the cx_oracle -:func:`makedsn()` function. Otherwise, the host token is taken directly as a TNS name. +Additional arguments which may be specified either as query string arguments +on the URL, or as keyword arguments to :func:`.create_engine()` are: -Additional arguments which may be specified either as query string arguments on the -URL, or as keyword arguments to :func:`~sqlalchemy.create_engine()` are: +* allow_twophase - enable two-phase transactions. Defaults to ``True``. -* *allow_twophase* - enable two-phase transactions. Defaults to ``True``. - -* *arraysize* - set the cx_oracle.arraysize value on cursors, in SQLAlchemy +* arraysize - set the cx_oracle.arraysize value on cursors, in SQLAlchemy it defaults to 50. See the section on "LOB Objects" below. -* *auto_convert_lobs* - defaults to True, see the section on LOB objects. +* auto_convert_lobs - defaults to True, see the section on LOB objects. -* *auto_setinputsizes* - the cx_oracle.setinputsizes() call is issued for all bind parameters. - This is required for LOB datatypes but can be disabled to reduce overhead. Defaults - to ``True``. +* auto_setinputsizes - the cx_oracle.setinputsizes() call is issued for + all bind parameters. This is required for LOB datatypes but can be + disabled to reduce overhead. Defaults to ``True``. Specific types + can be excluded from this process using the ``exclude_setinputsizes`` + parameter. -* *mode* - This is given the string value of SYSDBA or SYSOPER, or alternatively an - integer value. This value is only available as a URL query string argument. +* exclude_setinputsizes - a tuple or list of string DBAPI type names to + be excluded from the "auto setinputsizes" feature. The type names here + must match DBAPI types that are found in the "cx_Oracle" module namespace, + such as cx_Oracle.UNICODE, cx_Oracle.NCLOB, etc. Defaults to + ``(STRING, UNICODE)``. -* *threaded* - enable multithreaded access to cx_oracle connections. Defaults - to ``True``. Note that this is the opposite default of cx_oracle itself. + .. versionadded:: 0.8 specific DBAPI types can be excluded from the + auto_setinputsizes feature via the exclude_setinputsizes attribute. + +* mode - This is given the string value of SYSDBA or SYSOPER, or alternatively + an integer value. This value is only available as a URL query string + argument. + +* threaded - enable multithreaded access to cx_oracle connections. Defaults + to ``True``. Note that this is the opposite default of the cx_Oracle DBAPI + itself. Unicode ------- @@ -56,6 +65,27 @@ of the encoding to be used. Note that this behavior is disabled when Oracle 8 is detected, as it has been observed that issues remain when passing Python unicodes to cx_oracle with Oracle 8. +.. _cx_oracle_returning: + +RETURNING Support +----------------- + +cx_oracle supports a limited subset of Oracle's already limited RETURNING support. +Typically, results can only be guaranteed for at most one column being returned; +this is the typical case when SQLAlchemy uses RETURNING to get just the value of a +primary-key-associated sequence value. Additional column expressions will +cause problems in a non-determinative way, due to cx_oracle's lack of support for +the OCI_DATA_AT_EXEC API which is required for more complex RETURNING scenarios. + +.. seealso:: + + http://docs.oracle.com/cd/B10501_01/appdev.920/a96584/oci05bnd.htm#420693 - OCI documentation for RETURNING + + http://sourceforge.net/mailarchive/message.php?msg_id=31338136 - cx_oracle developer commentary + + + + LOB Objects ----------- @@ -66,7 +96,7 @@ like result.fetchmany() and result.fetchall(). This means that by default, LOB objects are fully fetched unconditionally by SQLAlchemy, and the linkage to a live cursor is broken. -To disable this processing, pass ``auto_convert_lobs=False`` to :func:`create_engine()`. +To disable this processing, pass ``auto_convert_lobs=False`` to :func:`.create_engine()`. Two Phase Transaction Support ----------------------------- @@ -99,7 +129,7 @@ the application can make one of several choices: * For ad-hoc two-phase operations without disabling pooling, the DBAPI connection in use can be evicted from the connection pool using the - :class:`.Connection.detach` method. + :meth:`.Connection.detach` method. .. versionchanged:: 0.8.0b2,0.7.10 Support for cx_oracle prepared transactions has been implemented @@ -173,16 +203,18 @@ a period "." as the decimal character. """ -from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, \ - OracleExecutionContext -from sqlalchemy.dialects.oracle import base as oracle -from sqlalchemy.engine import base +from __future__ import absolute_import + +from .base import OracleCompiler, OracleDialect, OracleExecutionContext +from . import base as oracle +from ...engine import result as _result from sqlalchemy import types as sqltypes, util, exc, processors import random import collections -from sqlalchemy.util.compat import decimal +import decimal import re + class _OracleNumeric(sqltypes.Numeric): def bind_processor(self, dialect): # cx_oracle accepts Decimal objects and floats @@ -200,10 +232,8 @@ class _OracleNumeric(sqltypes.Numeric): if dialect.supports_native_decimal: if self.asdecimal: - if self.scale is None: - fstring = "%.10f" - else: - fstring = "%%.%df" % self.scale + fstring = "%%.%df" % self._effective_decimal_return_scale + def to_decimal(value): if value is None: return None @@ -211,6 +241,7 @@ class _OracleNumeric(sqltypes.Numeric): return value else: return decimal.Decimal(fstring % value) + return to_decimal else: if self.precision is None and self.scale is None: @@ -226,6 +257,7 @@ class _OracleNumeric(sqltypes.Numeric): return super(_OracleNumeric, self).\ result_processor(dialect, coltype) + class _OracleDate(sqltypes.Date): def bind_processor(self, dialect): return None @@ -238,6 +270,7 @@ class _OracleDate(sqltypes.Date): return value return process + class _LOBMixin(object): def result_processor(self, dialect, coltype): if not dialect.auto_convert_lobs: @@ -251,38 +284,40 @@ class _LOBMixin(object): return value return process + class _NativeUnicodeMixin(object): - # Py3K - #pass - # Py2K - def bind_processor(self, dialect): - if dialect._cx_oracle_with_unicode: - def process(value): - if value is None: - return value - else: - return unicode(value) - return process - else: - return super(_NativeUnicodeMixin, self).bind_processor(dialect) - # end Py2K + if util.py2k: + def bind_processor(self, dialect): + if dialect._cx_oracle_with_unicode: + def process(value): + if value is None: + return value + else: + return unicode(value) + return process + else: + return super(_NativeUnicodeMixin, self).bind_processor(dialect) # we apply a connection output handler that returns # unicode in all cases, so the "native_unicode" flag # will be set for the default String.result_processor. + class _OracleChar(_NativeUnicodeMixin, sqltypes.CHAR): def get_dbapi_type(self, dbapi): return dbapi.FIXED_CHAR + class _OracleNVarChar(_NativeUnicodeMixin, sqltypes.NVARCHAR): def get_dbapi_type(self, dbapi): return getattr(dbapi, 'UNICODE', dbapi.STRING) + class _OracleText(_LOBMixin, sqltypes.Text): def get_dbapi_type(self, dbapi): return dbapi.CLOB + class _OracleLong(oracle.LONG): # a raw LONG is a text type, but does *not* # get the LobMixin with cx_oracle. @@ -293,6 +328,7 @@ class _OracleLong(oracle.LONG): class _OracleString(_NativeUnicodeMixin, sqltypes.String): pass + class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText): def get_dbapi_type(self, dbapi): return dbapi.NCLOB @@ -311,6 +347,7 @@ class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText): return string_processor(lob_processor(value)) return process + class _OracleInteger(sqltypes.Integer): def result_processor(self, dialect, coltype): def to_int(val): @@ -319,6 +356,7 @@ class _OracleInteger(sqltypes.Integer): return val return to_int + class _OracleBinary(_LOBMixin, sqltypes.LargeBinary): def get_dbapi_type(self, dbapi): return dbapi.BLOB @@ -326,20 +364,26 @@ class _OracleBinary(_LOBMixin, sqltypes.LargeBinary): def bind_processor(self, dialect): return None + class _OracleInterval(oracle.INTERVAL): def get_dbapi_type(self, dbapi): return dbapi.INTERVAL + class _OracleRaw(oracle.RAW): pass + class _OracleRowid(oracle.ROWID): def get_dbapi_type(self, dbapi): return dbapi.ROWID + class OracleCompiler_cx_oracle(OracleCompiler): def bindparam_string(self, name, **kw): - if self.preparer._bindparam_requires_quotes(name): + quote = getattr(name, 'quote', None) + if quote is True or quote is not False and \ + self.preparer._bindparam_requires_quotes(name): quoted_name = '"%s"' % name self._quoted_bind_names[name] = quoted_name return OracleCompiler.bindparam_string(self, quoted_name, **kw) @@ -375,7 +419,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext): # on String, including that outparams/RETURNING # breaks for varchars self.set_input_sizes(quoted_bind_names, - exclude_types=self.dialect._cx_oracle_exclude_setinputsizes + exclude_types=self.dialect.exclude_setinputsizes ) # if a single execute, check for outparams @@ -387,11 +431,12 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext): if not hasattr(self, 'out_parameters'): self.out_parameters = {} if dbtype is None: - raise exc.InvalidRequestError("Cannot create out parameter for parameter " - "%r - it's type %r is not supported by" - " cx_oracle" % - (name, bindparam.type) - ) + raise exc.InvalidRequestError( + "Cannot create out parameter for parameter " + "%r - it's type %r is not supported by" + " cx_oracle" % + (bindparam.key, bindparam.type) + ) name = self.compiled.bind_names[bindparam] self.out_parameters[name] = self.cursor.var(dbtype) self.parameters[0][quoted_bind_names.get(name, name)] = \ @@ -417,10 +462,10 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext): for column in self.cursor.description: type_code = column[1] if type_code in self.dialect._cx_oracle_binary_types: - result = base.BufferedColumnResultProxy(self) + result = _result.BufferedColumnResultProxy(self) if result is None: - result = base.ResultProxy(self) + result = _result.ResultProxy(self) if hasattr(self, 'out_parameters'): if self.compiled_parameters is not None and \ @@ -448,6 +493,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext): return result + class OracleExecutionContext_cx_oracle_with_unicode(OracleExecutionContext_cx_oracle): """Support WITH_UNICODE in Python 2.xx. @@ -463,13 +509,14 @@ class OracleExecutionContext_cx_oracle_with_unicode(OracleExecutionContext_cx_or """ def __init__(self, *arg, **kw): OracleExecutionContext_cx_oracle.__init__(self, *arg, **kw) - self.statement = unicode(self.statement) + self.statement = util.text_type(self.statement) def _execute_scalar(self, stmt): return super(OracleExecutionContext_cx_oracle_with_unicode, self).\ - _execute_scalar(unicode(stmt)) + _execute_scalar(util.text_type(stmt)) -class ReturningResultProxy(base.FullyBufferedResultProxy): + +class ReturningResultProxy(_result.FullyBufferedResultProxy): """Result proxy which stuffs the _returning clause + outparams into the fetch.""" def __init__(self, context, returning_params): @@ -478,19 +525,16 @@ class ReturningResultProxy(base.FullyBufferedResultProxy): def _cursor_description(self): returning = self.context.compiled.returning - - ret = [] - for c in returning: - if hasattr(c, 'name'): - ret.append((c.name, c.type)) - else: - ret.append((c.anon_label, c.type)) - return ret + return [ + ("ret_%d" % i, None) + for i, col in enumerate(returning) + ] def _buffer_rows(self): return collections.deque([tuple(self._returning_params["ret_%d" % i] for i, c in enumerate(self._returning_params))]) + class OracleDialect_cx_oracle(OracleDialect): execution_ctx_cls = OracleExecutionContext_cx_oracle statement_compiler = OracleCompiler_cx_oracle @@ -499,34 +543,36 @@ class OracleDialect_cx_oracle(OracleDialect): colspecs = colspecs = { sqltypes.Numeric: _OracleNumeric, - sqltypes.Date : _OracleDate, # generic type, assume datetime.date is desired + sqltypes.Date: _OracleDate, # generic type, assume datetime.date is desired oracle.DATE: oracle.DATE, # non generic type - passthru - sqltypes.LargeBinary : _OracleBinary, - sqltypes.Boolean : oracle._OracleBoolean, - sqltypes.Interval : _OracleInterval, - oracle.INTERVAL : _OracleInterval, - sqltypes.Text : _OracleText, - sqltypes.String : _OracleString, - sqltypes.UnicodeText : _OracleUnicodeText, - sqltypes.CHAR : _OracleChar, + sqltypes.LargeBinary: _OracleBinary, + sqltypes.Boolean: oracle._OracleBoolean, + sqltypes.Interval: _OracleInterval, + oracle.INTERVAL: _OracleInterval, + sqltypes.Text: _OracleText, + sqltypes.String: _OracleString, + sqltypes.UnicodeText: _OracleUnicodeText, + sqltypes.CHAR: _OracleChar, # a raw LONG is a text type, but does *not* # get the LobMixin with cx_oracle. oracle.LONG: _OracleLong, - sqltypes.Integer : _OracleInteger, # this is only needed for OUT parameters. - # it would be nice if we could not use it otherwise. + # this is only needed for OUT parameters. + # it would be nice if we could not use it otherwise. + sqltypes.Integer: _OracleInteger, + oracle.RAW: _OracleRaw, sqltypes.Unicode: _OracleNVarChar, - sqltypes.NVARCHAR : _OracleNVarChar, + sqltypes.NVARCHAR: _OracleNVarChar, oracle.ROWID: _OracleRowid, } - execute_sequence_format = list def __init__(self, auto_setinputsizes=True, + exclude_setinputsizes=("STRING", "UNICODE"), auto_convert_lobs=True, threaded=True, allow_twophase=True, @@ -536,22 +582,25 @@ class OracleDialect_cx_oracle(OracleDialect): self.threaded = threaded self.arraysize = arraysize self.allow_twophase = allow_twophase - self.supports_timestamp = self.dbapi is None or hasattr(self.dbapi, 'TIMESTAMP' ) + self.supports_timestamp = self.dbapi is None or \ + hasattr(self.dbapi, 'TIMESTAMP') self.auto_setinputsizes = auto_setinputsizes self.auto_convert_lobs = auto_convert_lobs if hasattr(self.dbapi, 'version'): - self.cx_oracle_ver = tuple([int(x) for x in self.dbapi.version.split('.')]) + self.cx_oracle_ver = tuple([int(x) for x in + self.dbapi.version.split('.')]) else: self.cx_oracle_ver = (0, 0, 0) def types(*names): - return set([ - getattr(self.dbapi, name, None) for name in names - ]).difference([None]) + return set( + getattr(self.dbapi, name, None) for name in names + ).difference([None]) - self._cx_oracle_exclude_setinputsizes = types("STRING", "UNICODE") - self._cx_oracle_string_types = types("STRING", "UNICODE", "NCLOB", "CLOB") + self.exclude_setinputsizes = types(*(exclude_setinputsizes or ())) + self._cx_oracle_string_types = types("STRING", "UNICODE", + "NCLOB", "CLOB") self._cx_oracle_unicode_types = types("UNICODE", "NCLOB") self._cx_oracle_binary_types = types("BFILE", "CLOB", "NCLOB", "BLOB") self.supports_unicode_binds = self.cx_oracle_ver >= (5, 0) @@ -573,19 +622,23 @@ class OracleDialect_cx_oracle(OracleDialect): self.supports_unicode_statements = True self.supports_unicode_binds = True self._cx_oracle_with_unicode = True - # Py2K - # There's really no reason to run with WITH_UNICODE under Python 2.x. - # Give the user a hint. - util.warn("cx_Oracle is compiled under Python 2.xx using the " - "WITH_UNICODE flag. Consider recompiling cx_Oracle without " - "this flag, which is in no way necessary for full support of Unicode. " - "Otherwise, all string-holding bind parameters must " - "be explicitly typed using SQLAlchemy's String type or one of its subtypes," - "or otherwise be passed as Python unicode. Plain Python strings " - "passed as bind parameters will be silently corrupted by cx_Oracle." - ) - self.execution_ctx_cls = OracleExecutionContext_cx_oracle_with_unicode - # end Py2K + + if util.py2k: + # There's really no reason to run with WITH_UNICODE under Python 2.x. + # Give the user a hint. + util.warn( + "cx_Oracle is compiled under Python 2.xx using the " + "WITH_UNICODE flag. Consider recompiling cx_Oracle " + "without this flag, which is in no way necessary for full " + "support of Unicode. Otherwise, all string-holding bind " + "parameters must be explicitly typed using SQLAlchemy's " + "String type or one of its subtypes," + "or otherwise be passed as Python unicode. " + "Plain Python strings passed as bind parameters will be " + "silently corrupted by cx_Oracle." + ) + self.execution_ctx_cls = \ + OracleExecutionContext_cx_oracle_with_unicode else: self._cx_oracle_with_unicode = False @@ -603,9 +656,10 @@ class OracleDialect_cx_oracle(OracleDialect): self.dbapi.BLOB: oracle.BLOB(), self.dbapi.BINARY: oracle.RAW(), } + @classmethod def dbapi(cls): - cx_Oracle = __import__('cx_Oracle') + import cx_Oracle return cx_Oracle def initialize(self, connection): @@ -668,6 +722,7 @@ class OracleDialect_cx_oracle(OracleDialect): return cx_Oracle = self.dbapi + def output_type_handler(cursor, name, defaultType, size, precision, scale): # convert all NUMBER with precision + positive scale to Decimal @@ -695,7 +750,7 @@ class OracleDialect_cx_oracle(OracleDialect): arraysize=cursor.arraysize) # allow all strings to come back natively as Unicode elif defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR): - return cursor.var(unicode, size, cursor.arraysize) + return cursor.var(util.text_type, size, cursor.arraysize) def on_connect(conn): conn.outputtypehandler = output_type_handler @@ -730,20 +785,19 @@ class OracleDialect_cx_oracle(OracleDialect): twophase=self.allow_twophase, ) - # Py2K - if self._cx_oracle_with_unicode: - for k, v in opts.items(): - if isinstance(v, str): - opts[k] = unicode(v) - else: - for k, v in opts.items(): - if isinstance(v, unicode): - opts[k] = str(v) - # end Py2K + if util.py2k: + if self._cx_oracle_with_unicode: + for k, v in opts.items(): + if isinstance(v, str): + opts[k] = unicode(v) + else: + for k, v in opts.items(): + if isinstance(v, unicode): + opts[k] = str(v) if 'mode' in url.query: opts['mode'] = url.query['mode'] - if isinstance(opts['mode'], basestring): + if isinstance(opts['mode'], util.string_types): mode = opts['mode'].upper() if mode == 'SYSDBA': opts['mode'] = self.dbapi.SYSDBA @@ -769,8 +823,9 @@ class OracleDialect_cx_oracle(OracleDialect): # ORA-03113: end-of-file on communication channel # ORA-03135: connection lost contact # ORA-01033: ORACLE initialization or shutdown in progress + # ORA-02396: exceeded maximum idle time, please connect again # TODO: Others ? - return error.code in (28, 3114, 3113, 3135, 1033) + return error.code in (28, 3114, 3113, 3135, 1033, 2396) else: return False @@ -783,6 +838,11 @@ class OracleDialect_cx_oracle(OracleDialect): id = random.randint(0, 2 ** 128) return (0x1234, "%032x" % id, "%032x" % 9) + def do_executemany(self, cursor, statement, parameters, context=None): + if isinstance(parameters, tuple): + parameters = list(parameters) + cursor.executemany(statement, parameters) + def do_begin_twophase(self, connection, xid): connection.connection.begin(*xid) diff --git a/libs/sqlalchemy/dialects/oracle/zxjdbc.py b/libs/sqlalchemy/dialects/oracle/zxjdbc.py index e4a12ce0..710645b2 100644 --- a/libs/sqlalchemy/dialects/oracle/zxjdbc.py +++ b/libs/sqlalchemy/dialects/oracle/zxjdbc.py @@ -1,16 +1,15 @@ # oracle/zxjdbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the Oracle database via the zxjdbc JDBC connector. - -JDBC Driver ------------ - -The official Oracle JDBC driver is at -http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html. +""" +.. dialect:: oracle+zxjdbc + :name: zxJDBC for Jython + :dbapi: zxjdbc + :connectstring: oracle+zxjdbc://user:pass@host/dbname + :driverurl: http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html. """ import decimal @@ -19,12 +18,13 @@ import re from sqlalchemy import sql, types as sqltypes, util from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, OracleExecutionContext -from sqlalchemy.engine import base, default +from sqlalchemy.engine import result as _result from sqlalchemy.sql import expression import collections SQLException = zxJDBC = None + class _ZxJDBCDate(sqltypes.Date): def result_processor(self, dialect, coltype): @@ -78,7 +78,7 @@ class OracleCompiler_zxjdbc(OracleCompiler): self.binds[bindparam.key] = bindparam binds.append(self.bindparam_string(self._truncate_bindparam(bindparam))) - return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) + return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) class OracleExecutionContext_zxjdbc(OracleExecutionContext): @@ -95,8 +95,8 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext): try: try: rrs = self.statement.__statement__.getReturnResultSet() - rrs.next() - except SQLException, sqle: + next(rrs) + except SQLException as sqle: msg = '%s [SQLCode: %d]' % (sqle.getMessage(), sqle.getErrorCode()) if sqle.getSQLState() is not None: msg += ' [SQLState: %s]' % sqle.getSQLState() @@ -113,7 +113,7 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext): pass self.statement.close() - return base.ResultProxy(self) + return _result.ResultProxy(self) def create_cursor(self): cursor = self._dbapi_connection.cursor() @@ -121,7 +121,7 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext): return cursor -class ReturningResultProxy(base.FullyBufferedResultProxy): +class ReturningResultProxy(_result.FullyBufferedResultProxy): """ResultProxy backed by the RETURNING ResultSet results.""" @@ -178,7 +178,7 @@ class OracleDialect_zxjdbc(ZxJDBCConnector, OracleDialect): colspecs = util.update_copy( OracleDialect.colspecs, { - sqltypes.Date : _ZxJDBCDate, + sqltypes.Date: _ZxJDBCDate, sqltypes.Numeric: _ZxJDBCNumeric } ) @@ -189,17 +189,19 @@ class OracleDialect_zxjdbc(ZxJDBCConnector, OracleDialect): from java.sql import SQLException from com.ziclix.python.sql import zxJDBC from com.ziclix.python.sql.handler import OracleDataHandler - class OracleReturningDataHandler(OracleDataHandler): + class OracleReturningDataHandler(OracleDataHandler): """zxJDBC DataHandler that specially handles ReturningParam.""" def setJDBCObject(self, statement, index, object, dbtype=None): if type(object) is ReturningParam: statement.registerReturnParameter(index, object.type) elif dbtype is None: - OracleDataHandler.setJDBCObject(self, statement, index, object) + OracleDataHandler.setJDBCObject( + self, statement, index, object) else: - OracleDataHandler.setJDBCObject(self, statement, index, object, dbtype) + OracleDataHandler.setJDBCObject( + self, statement, index, object, dbtype) self.DataHandler = OracleReturningDataHandler def initialize(self, connection): diff --git a/libs/sqlalchemy/dialects/postgres.py b/libs/sqlalchemy/dialects/postgres.py index 82d1a39c..6ed7e18b 100644 --- a/libs/sqlalchemy/dialects/postgres.py +++ b/libs/sqlalchemy/dialects/postgres.py @@ -1,5 +1,5 @@ # dialects/postgres.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php diff --git a/libs/sqlalchemy/dialects/postgresql/__init__.py b/libs/sqlalchemy/dialects/postgresql/__init__.py index 04ae413c..180e9fc7 100644 --- a/libs/sqlalchemy/dialects/postgresql/__init__.py +++ b/libs/sqlalchemy/dialects/postgresql/__init__.py @@ -1,20 +1,29 @@ # postgresql/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.dialects.postgresql import base, psycopg2, pg8000, pypostgresql, zxjdbc +from . import base, psycopg2, pg8000, pypostgresql, zxjdbc base.dialect = psycopg2.dialect -from sqlalchemy.dialects.postgresql.base import \ - INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, INET, \ - CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME,\ - DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect +from .base import \ + INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, \ + INET, CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME, \ + DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect, array, Any, All, \ + TSVECTOR +from .constraints import ExcludeConstraint +from .hstore import HSTORE, hstore +from .json import JSON, JSONElement +from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \ + TSTZRANGE __all__ = ( -'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC', 'FLOAT', 'REAL', 'INET', -'CIDR', 'UUID', 'BIT', 'MACADDR', 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', -'DATE', 'BYTEA', 'BOOLEAN', 'INTERVAL', 'ARRAY', 'ENUM', 'dialect' + 'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC', + 'FLOAT', 'REAL', 'INET', 'CIDR', 'UUID', 'BIT', 'MACADDR', + 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN', + 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE', + 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE', + 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONElement' ) diff --git a/libs/sqlalchemy/dialects/postgresql/base.py b/libs/sqlalchemy/dialects/postgresql/base.py index 384b7616..b7979a3e 100644 --- a/libs/sqlalchemy/dialects/postgresql/base.py +++ b/libs/sqlalchemy/dialects/postgresql/base.py @@ -1,13 +1,13 @@ # postgresql/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the PostgreSQL database. +""" +.. dialect:: postgresql + :name: PostgreSQL -For information on connecting using specific drivers, see the documentation -section regarding that driver. Sequences/SERIAL ---------------- @@ -41,23 +41,40 @@ case. To force the usage of RETURNING by default off, specify the flag ``implicit_returning=False`` to :func:`.create_engine`. +.. _postgresql_isolation_level: + Transaction Isolation Level --------------------------- -:func:`.create_engine` accepts an ``isolation_level`` parameter which results -in the command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL -`` being invoked for every new connection. Valid values for this -parameter are ``READ COMMITTED``, ``READ UNCOMMITTED``, ``REPEATABLE READ``, -and ``SERIALIZABLE``:: +All Postgresql dialects support setting of transaction isolation level +both via a dialect-specific parameter ``isolation_level`` +accepted by :func:`.create_engine`, +as well as the ``isolation_level`` argument as passed to :meth:`.Connection.execution_options`. +When using a non-psycopg2 dialect, this feature works by issuing the +command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL +`` for each new connection. + +To set isolation level using :func:`.create_engine`:: engine = create_engine( "postgresql+pg8000://scott:tiger@localhost/test", isolation_level="READ UNCOMMITTED" ) -When using the psycopg2 dialect, a psycopg2-specific method of setting -transaction isolation level is used, but the API of ``isolation_level`` -remains the same - see :ref:`psycopg2_isolation`. +To set using per-connection execution options:: + + connection = engine.connect() + connection = connection.execution_options(isolation_level="READ COMMITTED") + +Valid values for ``isolation_level`` include: + +* ``READ COMMITTED`` +* ``READ UNCOMMITTED`` +* ``REPEATABLE READ`` +* ``SERIALIZABLE`` + +The :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect also offers the special level ``AUTOCOMMIT``. See +:ref:`psycopg2_isolation_level` for details. Remote / Cross-Schema Table Introspection @@ -114,6 +131,62 @@ use the :meth:`._UpdateBase.returning` method on a per-statement basis:: where(table.c.name=='foo') print result.fetchall() +.. _postgresql_match: + +Full Text Search +---------------- + +SQLAlchemy makes available the Postgresql ``@@`` operator via the +:meth:`.ColumnElement.match` method on any textual column expression. +On a Postgresql dialect, an expression like the following:: + + select([sometable.c.text.match("search string")]) + +will emit to the database:: + + SELECT text @@ to_tsquery('search string') FROM table + +The Postgresql text search functions such as ``to_tsquery()`` +and ``to_tsvector()`` are available +explicitly using the standard :attr:`.func` construct. For example:: + + select([ + func.to_tsvector('fat cats ate rats').match('cat & rat') + ]) + +Emits the equivalent of:: + + SELECT to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat') + +The :class:`.postgresql.TSVECTOR` type can provide for explicit CAST:: + + from sqlalchemy.dialects.postgresql import TSVECTOR + from sqlalchemy import select, cast + select([cast("some text", TSVECTOR)]) + +produces a statement equivalent to:: + + SELECT CAST('some text' AS TSVECTOR) AS anon_1 + + +FROM ONLY ... +------------------------ + +The dialect supports PostgreSQL's ONLY keyword for targeting only a particular +table in an inheritance hierarchy. This can be used to produce the +``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...`` +syntaxes. It uses SQLAlchemy's hints mechanism:: + + # SELECT ... FROM ONLY ... + result = table.select().with_hint(table, 'ONLY', 'postgresql') + print result.fetchall() + + # UPDATE ONLY ... + table.update(values=dict(foo='bar')).with_hint('ONLY', + dialect_name='postgresql') + + # DELETE FROM ONLY ... + table.delete().with_hint('ONLY', dialect_name='postgresql') .. _postgresql_indexes: @@ -136,9 +209,10 @@ Operator Classes ^^^^^^^^^^^^^^^^^ PostgreSQL allows the specification of an *operator class* for each column of -an index (see http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html). -The :class:`.Index` construct allows these to be specified via the ``postgresql_ops`` -keyword argument:: +an index (see +http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html). +The :class:`.Index` construct allows these to be specified via the +``postgresql_ops`` keyword argument:: Index('my_index', my_table.c.id, my_table.c.data, postgresql_ops={ @@ -150,15 +224,15 @@ keyword argument:: ``postgresql_ops`` keyword argument to :class:`.Index` construct. Note that the keys in the ``postgresql_ops`` dictionary are the "key" name of -the :class:`.Column`, i.e. the name used to access it from the ``.c`` collection -of :class:`.Table`, which can be configured to be different than the actual -name of the column as expressed in the database. +the :class:`.Column`, i.e. the name used to access it from the ``.c`` +collection of :class:`.Table`, which can be configured to be different than +the actual name of the column as expressed in the database. Index Types ^^^^^^^^^^^^ -PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well as -the ability for users to create their own (see +PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well +as the ability for users to create their own (see http://www.postgresql.org/docs/8.3/static/indexes-types.html). These can be specified on :class:`.Index` using the ``postgresql_using`` keyword argument:: @@ -169,13 +243,13 @@ underlying CREATE INDEX command, so it *must* be a valid index type for your version of PostgreSQL. """ - +from collections import defaultdict import re -from sqlalchemy import sql, schema, exc, util -from sqlalchemy.engine import default, reflection -from sqlalchemy.sql import compiler, expression, util as sql_util -from sqlalchemy import types as sqltypes +from ... import sql, schema, exc, util +from ...engine import default, reflection +from ...sql import compiler, expression, operators +from ... import types as sqltypes try: from uuid import UUID as _python_UUID @@ -194,7 +268,7 @@ RESERVED_WORDS = set( "default", "deferrable", "desc", "distinct", "do", "else", "end", "except", "false", "fetch", "for", "foreign", "from", "grant", "group", "having", "in", "initially", "intersect", "into", "leading", "limit", - "localtime", "localtimestamp", "new", "not", "null", "off", "offset", + "localtime", "localtimestamp", "new", "not", "null", "of", "off", "offset", "old", "on", "only", "or", "order", "placing", "primary", "references", "returning", "select", "session_user", "some", "symmetric", "table", "then", "to", "trailing", "true", "union", "unique", "user", "using", @@ -208,24 +282,30 @@ _DECIMAL_TYPES = (1231, 1700) _FLOAT_TYPES = (700, 701, 1021, 1022) _INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016) + class BYTEA(sqltypes.LargeBinary): __visit_name__ = 'BYTEA' + class DOUBLE_PRECISION(sqltypes.Float): __visit_name__ = 'DOUBLE_PRECISION' + class INET(sqltypes.TypeEngine): __visit_name__ = "INET" PGInet = INET + class CIDR(sqltypes.TypeEngine): __visit_name__ = "CIDR" PGCidr = CIDR + class MACADDR(sqltypes.TypeEngine): __visit_name__ = "MACADDR" PGMacAddr = MACADDR + class TIMESTAMP(sqltypes.TIMESTAMP): def __init__(self, timezone=False, precision=None): super(TIMESTAMP, self).__init__(timezone=timezone) @@ -237,6 +317,7 @@ class TIME(sqltypes.TIME): super(TIME, self).__init__(timezone=timezone) self.precision = precision + class INTERVAL(sqltypes.TypeEngine): """Postgresql INTERVAL type. @@ -245,6 +326,7 @@ class INTERVAL(sqltypes.TypeEngine): """ __visit_name__ = 'INTERVAL' + def __init__(self, precision=None): self.precision = precision @@ -258,8 +340,10 @@ class INTERVAL(sqltypes.TypeEngine): PGInterval = INTERVAL + class BIT(sqltypes.TypeEngine): __visit_name__ = 'BIT' + def __init__(self, length=None, varying=False): if not varying: # BIT without VARYING defaults to length 1 @@ -271,6 +355,7 @@ class BIT(sqltypes.TypeEngine): PGBit = BIT + class UUID(sqltypes.TypeEngine): """Postgresql UUID type. @@ -295,15 +380,15 @@ class UUID(sqltypes.TypeEngine): """ if as_uuid and _python_UUID is None: raise NotImplementedError( - "This version of Python does not support the native UUID type." - ) + "This version of Python does not support the native UUID type." + ) self.as_uuid = as_uuid def bind_processor(self, dialect): if self.as_uuid: def process(value): if value is not None: - value = str(value) + value = util.text_type(value) return value return process else: @@ -321,19 +406,302 @@ class UUID(sqltypes.TypeEngine): PGUuid = UUID -class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): +class TSVECTOR(sqltypes.TypeEngine): + """The :class:`.postgresql.TSVECTOR` type implements the Postgresql + text search type TSVECTOR. + + It can be used to do full text queries on natural language + documents. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :ref:`postgresql_match` + + """ + __visit_name__ = 'TSVECTOR' + + + +class _Slice(expression.ColumnElement): + __visit_name__ = 'slice' + type = sqltypes.NULLTYPE + + def __init__(self, slice_, source_comparator): + self.start = source_comparator._check_literal( + source_comparator.expr, + operators.getitem, slice_.start) + self.stop = source_comparator._check_literal( + source_comparator.expr, + operators.getitem, slice_.stop) + + +class Any(expression.ColumnElement): + """Represent the clause ``left operator ANY (right)``. ``right`` must be + an array expression. + + .. seealso:: + + :class:`.postgresql.ARRAY` + + :meth:`.postgresql.ARRAY.Comparator.any` - ARRAY-bound method + + """ + __visit_name__ = 'any' + + def __init__(self, left, right, operator=operators.eq): + self.type = sqltypes.Boolean() + self.left = expression._literal_as_binds(left) + self.right = right + self.operator = operator + + +class All(expression.ColumnElement): + """Represent the clause ``left operator ALL (right)``. ``right`` must be + an array expression. + + .. seealso:: + + :class:`.postgresql.ARRAY` + + :meth:`.postgresql.ARRAY.Comparator.all` - ARRAY-bound method + + """ + __visit_name__ = 'all' + + def __init__(self, left, right, operator=operators.eq): + self.type = sqltypes.Boolean() + self.left = expression._literal_as_binds(left) + self.right = right + self.operator = operator + + +class array(expression.Tuple): + """A Postgresql ARRAY literal. + + This is used to produce ARRAY literals in SQL expressions, e.g.:: + + from sqlalchemy.dialects.postgresql import array + from sqlalchemy.dialects import postgresql + from sqlalchemy import select, func + + stmt = select([ + array([1,2]) + array([3,4,5]) + ]) + + print stmt.compile(dialect=postgresql.dialect()) + + Produces the SQL:: + + SELECT ARRAY[%(param_1)s, %(param_2)s] || + ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1 + + An instance of :class:`.array` will always have the datatype + :class:`.ARRAY`. The "inner" type of the array is inferred from + the values present, unless the ``type_`` keyword argument is passed:: + + array(['foo', 'bar'], type_=CHAR) + + .. versionadded:: 0.8 Added the :class:`~.postgresql.array` literal type. + + See also: + + :class:`.postgresql.ARRAY` + + """ + __visit_name__ = 'array' + + def __init__(self, clauses, **kw): + super(array, self).__init__(*clauses, **kw) + self.type = ARRAY(self.type) + + def _bind_param(self, operator, obj): + return array(*[ + expression.BindParameter(None, o, _compared_to_operator=operator, + _compared_to_type=self.type, unique=True) + for o in obj + ]) + + def self_group(self, against=None): + return self + + +class ARRAY(sqltypes.Concatenable, sqltypes.TypeEngine): """Postgresql ARRAY type. Represents values as Python lists. - The ARRAY type may not be supported on all DBAPIs. + An :class:`.ARRAY` type is constructed given the "type" + of element:: + + mytable = Table("mytable", metadata, + Column("data", ARRAY(Integer)) + ) + + The above type represents an N-dimensional array, + meaning Postgresql will interpret values with any number + of dimensions automatically. To produce an INSERT + construct that passes in a 1-dimensional array of integers:: + + connection.execute( + mytable.insert(), + data=[1,2,3] + ) + + The :class:`.ARRAY` type can be constructed given a fixed number + of dimensions:: + + mytable = Table("mytable", metadata, + Column("data", ARRAY(Integer, dimensions=2)) + ) + + This has the effect of the :class:`.ARRAY` type + specifying that number of bracketed blocks when a :class:`.Table` + is used in a CREATE TABLE statement, or when the type is used + within a :func:`.expression.cast` construct; it also causes + the bind parameter and result set processing of the type + to optimize itself to expect exactly that number of dimensions. + Note that Postgresql itself still allows N dimensions with such a type. + + SQL expressions of type :class:`.ARRAY` have support for "index" and + "slice" behavior. The Python ``[]`` operator works normally here, given + integer indexes or slices. Note that Postgresql arrays default + to 1-based indexing. The operator produces binary expression + constructs which will produce the appropriate SQL, both for + SELECT statements:: + + select([mytable.c.data[5], mytable.c.data[2:7]]) + + as well as UPDATE statements when the :meth:`.Update.values` method + is used:: + + mytable.update().values({ + mytable.c.data[5]: 7, + mytable.c.data[2:7]: [1, 2, 3] + }) + + :class:`.ARRAY` provides special methods for containment operations, + e.g.:: + + mytable.c.data.contains([1, 2]) + + For a full list of special methods see :class:`.ARRAY.Comparator`. + + .. versionadded:: 0.8 Added support for index and slice operations + to the :class:`.ARRAY` type, including support for UPDATE + statements, and special array containment operations. + + The :class:`.ARRAY` type may not be supported on all DBAPIs. It is known to work on psycopg2 and not pg8000. + See also: + + :class:`.postgresql.array` - produce a literal array value. """ __visit_name__ = 'ARRAY' - def __init__(self, item_type, mutable=False, as_tuple=False): + class Comparator(sqltypes.Concatenable.Comparator): + """Define comparison operations for :class:`.ARRAY`.""" + + def __getitem__(self, index): + if isinstance(index, slice): + index = _Slice(index, self) + return_type = self.type + else: + return_type = self.type.item_type + return self._binary_operate(self.expr, operators.getitem, index, + result_type=return_type) + + def any(self, other, operator=operators.eq): + """Return ``other operator ANY (array)`` clause. + + Argument places are switched, because ANY requires array + expression to be on the right hand-side. + + E.g.:: + + from sqlalchemy.sql import operators + + conn.execute( + select([table.c.data]).where( + table.c.data.any(7, operator=operators.lt) + ) + ) + + :param other: expression to be compared + :param operator: an operator object from the + :mod:`sqlalchemy.sql.operators` + package, defaults to :func:`.operators.eq`. + + .. seealso:: + + :class:`.postgresql.Any` + + :meth:`.postgresql.ARRAY.Comparator.all` + + """ + return Any(other, self.expr, operator=operator) + + def all(self, other, operator=operators.eq): + """Return ``other operator ALL (array)`` clause. + + Argument places are switched, because ALL requires array + expression to be on the right hand-side. + + E.g.:: + + from sqlalchemy.sql import operators + + conn.execute( + select([table.c.data]).where( + table.c.data.all(7, operator=operators.lt) + ) + ) + + :param other: expression to be compared + :param operator: an operator object from the + :mod:`sqlalchemy.sql.operators` + package, defaults to :func:`.operators.eq`. + + .. seealso:: + + :class:`.postgresql.All` + + :meth:`.postgresql.ARRAY.Comparator.any` + + """ + return All(other, self.expr, operator=operator) + + def contains(self, other, **kwargs): + """Boolean expression. Test if elements are a superset of the + elements of the argument array expression. + """ + return self.expr.op('@>')(other) + + def contained_by(self, other): + """Boolean expression. Test if elements are a proper subset of the + elements of the argument array expression. + """ + return self.expr.op('<@')(other) + + def overlap(self, other): + """Boolean expression. Test if array has elements in common with + an argument array expression. + """ + return self.expr.op('&&')(other) + + def _adapt_expression(self, op, other_comparator): + if isinstance(op, operators.custom_op): + if op.opstring in ['@>', '<@', '&&']: + return op, sqltypes.Boolean + return sqltypes.Concatenable.Comparator.\ + _adapt_expression(self, op, other_comparator) + + comparator_factory = Comparator + + def __init__(self, item_type, as_tuple=False, dimensions=None): """Construct an ARRAY. E.g.:: @@ -345,31 +713,20 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): :param item_type: The data type of items of this array. Note that dimensionality is irrelevant here, so multi-dimensional arrays like ``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as - ``ARRAY(ARRAY(Integer))`` or such. The type mapping figures out on - the fly - - :param mutable=False: Specify whether lists passed to this - class should be considered mutable - this enables - "mutable types" mode in the ORM. Be sure to read the - notes for :class:`.MutableType` regarding ORM - performance implications. - - .. versionchanged:: 0.7.0 - Default changed from ``True``\ . - - .. versionchanged:: 0.7 - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. + ``ARRAY(ARRAY(Integer))`` or such. :param as_tuple=False: Specify whether return results should be converted to tuples from lists. DBAPIs such as psycopg2 return lists by default. When tuples are - returned, the results are hashable. This flag can only - be set to ``True`` when ``mutable`` is set to - ``False``. + returned, the results are hashable. - .. versionadded:: 0.6.5 + :param dimensions: if non-None, the ARRAY will assume a fixed + number of dimensions. This will cause the DDL emitted for this + ARRAY to include the exact number of bracket clauses ``[]``, + and will also optimize the performance of the type overall. + Note that PG arrays are always implicitly "non-dimensioned", + meaning they can store any number of dimensions no matter how + they were declared. """ if isinstance(item_type, ARRAY): @@ -378,77 +735,68 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): if isinstance(item_type, type): item_type = item_type() self.item_type = item_type - self.mutable = mutable - if mutable and as_tuple: - raise exc.ArgumentError( - "mutable must be set to False if as_tuple is True." - ) self.as_tuple = as_tuple - - def copy_value(self, value): - if value is None: - return None - elif self.mutable: - return list(value) - else: - return value + self.dimensions = dimensions def compare_values(self, x, y): return x == y - def is_mutable(self): - return self.mutable + def _proc_array(self, arr, itemproc, dim, collection): + if dim is None: + arr = list(arr) + if dim == 1 or dim is None and ( + # this has to be (list, tuple), or at least + # not hasattr('__iter__'), since Py3K strings + # etc. have __iter__ + not arr or not isinstance(arr[0], (list, tuple))): + if itemproc: + return collection(itemproc(x) for x in arr) + else: + return collection(arr) + else: + return collection( + self._proc_array( + x, itemproc, + dim - 1 if dim is not None else None, + collection) + for x in arr + ) def bind_processor(self, dialect): - item_proc = self.item_type.dialect_impl(dialect).bind_processor(dialect) - if item_proc: - def convert_item(item): - if isinstance(item, (list, tuple)): - return [convert_item(child) for child in item] - else: - return item_proc(item) - else: - def convert_item(item): - if isinstance(item, (list, tuple)): - return [convert_item(child) for child in item] - else: - return item + item_proc = self.item_type.\ + dialect_impl(dialect).\ + bind_processor(dialect) + def process(value): if value is None: return value - return [convert_item(item) for item in value] + else: + return self._proc_array( + value, + item_proc, + self.dimensions, + list) return process def result_processor(self, dialect, coltype): - item_proc = self.item_type.dialect_impl(dialect).result_processor(dialect, coltype) - if item_proc: - def convert_item(item): - if isinstance(item, list): - r = [convert_item(child) for child in item] - if self.as_tuple: - r = tuple(r) - return r - else: - return item_proc(item) - else: - def convert_item(item): - if isinstance(item, list): - r = [convert_item(child) for child in item] - if self.as_tuple: - r = tuple(r) - return r - else: - return item + item_proc = self.item_type.\ + dialect_impl(dialect).\ + result_processor(dialect, coltype) + def process(value): if value is None: return value - r = [convert_item(item) for item in value] - if self.as_tuple: - r = tuple(r) - return r + else: + return self._proc_array( + value, + item_proc, + self.dimensions, + tuple if self.as_tuple else list) return process + PGArray = ARRAY + class ENUM(sqltypes.Enum): """Postgresql ENUM type. @@ -585,71 +933,107 @@ class ENUM(sqltypes.Enum): self.drop(bind=bind, checkfirst=checkfirst) colspecs = { - sqltypes.Interval:INTERVAL, - sqltypes.Enum:ENUM, + sqltypes.Interval: INTERVAL, + sqltypes.Enum: ENUM, } ischema_names = { - 'integer' : INTEGER, - 'bigint' : BIGINT, - 'smallint' : SMALLINT, - 'character varying' : VARCHAR, - 'character' : CHAR, - '"char"' : sqltypes.String, - 'name' : sqltypes.String, - 'text' : TEXT, - 'numeric' : NUMERIC, - 'float' : FLOAT, - 'real' : REAL, + 'integer': INTEGER, + 'bigint': BIGINT, + 'smallint': SMALLINT, + 'character varying': VARCHAR, + 'character': CHAR, + '"char"': sqltypes.String, + 'name': sqltypes.String, + 'text': TEXT, + 'numeric': NUMERIC, + 'float': FLOAT, + 'real': REAL, 'inet': INET, 'cidr': CIDR, 'uuid': UUID, 'bit': BIT, 'bit varying': BIT, 'macaddr': MACADDR, - 'double precision' : DOUBLE_PRECISION, - 'timestamp' : TIMESTAMP, - 'timestamp with time zone' : TIMESTAMP, - 'timestamp without time zone' : TIMESTAMP, - 'time with time zone' : TIME, - 'time without time zone' : TIME, - 'date' : DATE, + 'double precision': DOUBLE_PRECISION, + 'timestamp': TIMESTAMP, + 'timestamp with time zone': TIMESTAMP, + 'timestamp without time zone': TIMESTAMP, + 'time with time zone': TIME, + 'time without time zone': TIME, + 'date': DATE, 'time': TIME, - 'bytea' : BYTEA, - 'boolean' : BOOLEAN, - 'interval':INTERVAL, - 'interval year to month':INTERVAL, - 'interval day to second':INTERVAL, + 'bytea': BYTEA, + 'boolean': BOOLEAN, + 'interval': INTERVAL, + 'interval year to month': INTERVAL, + 'interval day to second': INTERVAL, + 'tsvector' : TSVECTOR } - class PGCompiler(compiler.SQLCompiler): - def visit_match_op(self, binary, **kw): + def visit_array(self, element, **kw): + return "ARRAY[%s]" % self.visit_clauselist(element, **kw) + + def visit_slice(self, element, **kw): + return "%s:%s" % ( + self.process(element.start, **kw), + self.process(element.stop, **kw), + ) + + def visit_any(self, element, **kw): + return "%s%sANY (%s)" % ( + self.process(element.left, **kw), + compiler.OPERATORS[element.operator], + self.process(element.right, **kw) + ) + + def visit_all(self, element, **kw): + return "%s%sALL (%s)" % ( + self.process(element.left, **kw), + compiler.OPERATORS[element.operator], + self.process(element.right, **kw) + ) + + def visit_getitem_binary(self, binary, operator, **kw): + return "%s[%s]" % ( + self.process(binary.left, **kw), + self.process(binary.right, **kw) + ) + + def visit_match_op_binary(self, binary, operator, **kw): return "%s @@ to_tsquery(%s)" % ( - self.process(binary.left), - self.process(binary.right)) + self.process(binary.left, **kw), + self.process(binary.right, **kw)) - def visit_ilike_op(self, binary, **kw): + def visit_ilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) - return '%s ILIKE %s' % \ - (self.process(binary.left), self.process(binary.right)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') - def visit_notilike_op(self, binary, **kw): + return '%s ILIKE %s' % \ + (self.process(binary.left, **kw), + self.process(binary.right, **kw)) \ + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) + + def visit_notilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return '%s NOT ILIKE %s' % \ - (self.process(binary.left), self.process(binary.right)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + (self.process(binary.left, **kw), + self.process(binary.right, **kw)) \ + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def render_literal_value(self, value, type_): value = super(PGCompiler, self).render_literal_value(value, type_) - # TODO: need to inspect "standard_conforming_strings" + if self.dialect._backslash_escapes: value = value.replace('\\', '\\\\') return value @@ -660,13 +1044,18 @@ class PGCompiler(compiler.SQLCompiler): def limit_clause(self, select): text = "" if select._limit is not None: - text += " \n LIMIT " + self.process(sql.literal(select._limit)) + text += " \n LIMIT " + self.process(sql.literal(select._limit)) if select._offset is not None: if select._limit is None: text += " \n LIMIT ALL" text += " OFFSET " + self.process(sql.literal(select._offset)) return text + def format_from_hint_text(self, sqltext, table, hint, iscrud): + if hint.upper() != 'ONLY': + raise exc.CompileError("Unrecognized hint: %r" % hint) + return "ONLY " + sqltext + def get_select_precolumns(self, select): if select._distinct is not False: if select._distinct is True: @@ -674,70 +1063,72 @@ class PGCompiler(compiler.SQLCompiler): elif isinstance(select._distinct, (list, tuple)): return "DISTINCT ON (" + ', '.join( [self.process(col) for col in select._distinct] - )+ ") " + ) + ") " else: return "DISTINCT ON (" + self.process(select._distinct) + ") " else: return "" def for_update_clause(self, select): - if select.for_update == 'nowait': - return " FOR UPDATE NOWAIT" - elif select.for_update == 'read': - return " FOR SHARE" - elif select.for_update == 'read_nowait': - return " FOR SHARE NOWAIT" + + if select._for_update_arg.read: + tmp = " FOR SHARE" else: - return super(PGCompiler, self).for_update_clause(select) + tmp = " FOR UPDATE" + + if select._for_update_arg.of: + tables = util.OrderedSet( + c.table if isinstance(c, expression.ColumnClause) + else c for c in select._for_update_arg.of) + tmp += " OF " + ", ".join( + self.process(table, ashint=True) + for table in tables + ) + + if select._for_update_arg.nowait: + tmp += " NOWAIT" + + return tmp def returning_clause(self, stmt, returning_cols): columns = [ - self.process( - self.label_select_column(None, c, asfrom=False), - within_columns_clause=True, - result_map=self.result_map) + self._label_select_column(None, c, True, False, {}) for c in expression._select_iterables(returning_cols) ] return 'RETURNING ' + ', '.join(columns) - def visit_extract(self, extract, **kwargs): - field = self.extract_map.get(extract.field, extract.field) - if extract.expr.type: - affinity = extract.expr.type._type_affinity - else: - affinity = None - casts = { - sqltypes.Date:'date', - sqltypes.DateTime:'timestamp', - sqltypes.Interval:'interval', sqltypes.Time:'time' - } - cast = casts.get(affinity, None) - if isinstance(extract.expr, sql.ColumnElement) and cast is not None: - expr = extract.expr.op('::')(sql.literal_column(cast)) + def visit_substring_func(self, func, **kw): + s = self.process(func.clauses.clauses[0], **kw) + start = self.process(func.clauses.clauses[1], **kw) + if len(func.clauses.clauses) > 2: + length = self.process(func.clauses.clauses[2], **kw) + return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length) else: - expr = extract.expr - return "EXTRACT(%s FROM %s)" % ( - field, self.process(expr)) + return "SUBSTRING(%s FROM %s)" % (s, start) class PGDDLCompiler(compiler.DDLCompiler): def get_column_specification(self, column, **kwargs): + colspec = self.preparer.format_column(column) impl_type = column.type.dialect_impl(self.dialect) if column.primary_key and \ column is column.table._autoincrement_column and \ - not isinstance(impl_type, sqltypes.SmallInteger) and \ ( + self.dialect.supports_smallserial or + not isinstance(impl_type, sqltypes.SmallInteger) + ) and ( column.default is None or ( isinstance(column.default, schema.Sequence) and column.default.optional - ) - ): + )): if isinstance(impl_type, sqltypes.BigInteger): colspec += " BIGSERIAL" + elif isinstance(impl_type, sqltypes.SmallInteger): + colspec += " SMALLSERIAL" else: colspec += " SERIAL" else: @@ -755,7 +1146,9 @@ class PGDDLCompiler(compiler.DDLCompiler): return "CREATE TYPE %s AS ENUM (%s)" % ( self.preparer.format_type(type_), - ",".join("'%s'" % e for e in type_.enums) + ", ".join( + self.sql_compiler.process(sql.literal(e), literal_binds=True) + for e in type_.enums) ) def visit_drop_enum_type(self, drop): @@ -768,46 +1161,67 @@ class PGDDLCompiler(compiler.DDLCompiler): def visit_create_index(self, create): preparer = self.preparer index = create.element + self._verify_index_table(index) text = "CREATE " if index.unique: text += "UNIQUE " - ops = index.kwargs.get('postgresql_ops', {}) text += "INDEX %s ON %s " % ( - preparer.quote( - self._index_identifier(index.name), index.quote), + self._prepared_index_name(index, + include_schema=False), preparer.format_table(index.table) ) if 'postgresql_using' in index.kwargs: using = index.kwargs['postgresql_using'] - text += "USING %s " % preparer.quote(using, index.quote) + text += "USING %s " % preparer.quote(using) + ops = index.kwargs.get('postgresql_ops', {}) text += "(%s)" \ % ( ', '.join([ - preparer.format_column(c) + + self.sql_compiler.process( + expr.self_group() + if not isinstance(expr, expression.ColumnClause) + else expr, + include_table=False, literal_binds=True) + (c.key in ops and (' ' + ops[c.key]) or '') - for c in index.columns]) + for expr, c in zip(index.expressions, index.columns)]) ) - if "postgres_where" in index.kwargs: - whereclause = index.kwargs['postgres_where'] - util.warn_deprecated( - "The 'postgres_where' argument has been renamed " - "to 'postgresql_where'.") - elif 'postgresql_where' in index.kwargs: + if 'postgresql_where' in index.kwargs: whereclause = index.kwargs['postgresql_where'] else: whereclause = None if whereclause is not None: - whereclause = sql_util.expression_as_ddl(whereclause) - where_compiled = self.sql_compiler.process(whereclause) + where_compiled = self.sql_compiler.process( + whereclause, include_table=False, + literal_binds=True) text += " WHERE " + where_compiled return text + def visit_exclude_constraint(self, constraint): + text = "" + if constraint.name is not None: + text += "CONSTRAINT %s " % \ + self.preparer.format_constraint(constraint) + elements = [] + for c in constraint.columns: + op = constraint.operators[c.name] + elements.append(self.preparer.quote(c.name) + ' WITH '+op) + text += "EXCLUDE USING %s (%s)" % (constraint.using, ', '.join(elements)) + if constraint.where is not None: + text += ' WHERE (%s)' % self.sql_compiler.process( + constraint.where, + literal_binds=True) + text += self.define_constraint_deferrability(constraint) + return text + class PGTypeCompiler(compiler.GenericTypeCompiler): + def visit_TSVECTOR(self, type): + return "TSVECTOR" + def visit_INET(self, type_): return "INET" @@ -829,6 +1243,30 @@ class PGTypeCompiler(compiler.GenericTypeCompiler): def visit_BIGINT(self, type_): return "BIGINT" + def visit_HSTORE(self, type_): + return "HSTORE" + + def visit_JSON(self, type_): + return "JSON" + + def visit_INT4RANGE(self, type_): + return "INT4RANGE" + + def visit_INT8RANGE(self, type_): + return "INT8RANGE" + + def visit_NUMRANGE(self, type_): + return "NUMRANGE" + + def visit_DATERANGE(self, type_): + return "DATERANGE" + + def visit_TSRANGE(self, type_): + return "TSRANGE" + + def visit_TSTZRANGE(self, type_): + return "TSTZRANGE" + def visit_datetime(self, type_): return self.visit_TIMESTAMP(type_) @@ -880,7 +1318,9 @@ class PGTypeCompiler(compiler.GenericTypeCompiler): return "BYTEA" def visit_ARRAY(self, type_): - return self.process(type_.item_type) + '[]' + return self.process(type_.item_type) + ('[]' * (type_.dimensions + if type_.dimensions + is not None else 1)) class PGIdentifierPreparer(compiler.IdentifierPreparer): @@ -897,11 +1337,12 @@ class PGIdentifierPreparer(compiler.IdentifierPreparer): if not type_.name: raise exc.CompileError("Postgresql ENUM type requires a name.") - name = self.quote(type_.name, type_.quote) + name = self.quote(type_.name) if not self.omit_schema and use_schema and type_.schema is not None: - name = self.quote_schema(type_.schema, type_.quote) + "." + name + name = self.quote_schema(type_.schema) + "." + name return name + class PGInspector(reflection.Inspector): def __init__(self, conn): @@ -913,11 +1354,14 @@ class PGInspector(reflection.Inspector): return self.dialect.get_table_oid(self.bind, table_name, schema, info_cache=self.info_cache) + class CreateEnumType(schema._CreateDropBase): - __visit_name__ = "create_enum_type" + __visit_name__ = "create_enum_type" + class DropEnumType(schema._CreateDropBase): - __visit_name__ = "drop_enum_type" + __visit_name__ = "drop_enum_type" + class PGExecutionContext(default.DefaultExecutionContext): def fire_sequence(self, seq, type_): @@ -947,7 +1391,8 @@ class PGExecutionContext(default.DefaultExecutionContext): col = column.name tab = tab[0:29 + max(0, (29 - len(col)))] col = col[0:29 + max(0, (29 - len(tab)))] - column._postgresql_seq_name = seq_name = "%s_%s_seq" % (tab, col) + name = "%s_%s_seq" % (tab, col) + column._postgresql_seq_name = seq_name = name sch = column.table.schema if sch is not None: @@ -961,6 +1406,7 @@ class PGExecutionContext(default.DefaultExecutionContext): return super(PGExecutionContext, self).get_insert_default(column) + class PGDialect(default.DefaultDialect): name = 'postgresql' supports_alter = True @@ -969,6 +1415,7 @@ class PGDialect(default.DefaultDialect): supports_native_enum = True supports_native_boolean = True + supports_smallserial = True supports_sequences = True sequences_optional = True @@ -977,6 +1424,7 @@ class PGDialect(default.DefaultDialect): supports_default_values = True supports_empty_insert = False + supports_multivalues_insert = True default_paramstyle = 'pyformat' ischema_names = ischema_names colspecs = colspecs @@ -989,12 +1437,14 @@ class PGDialect(default.DefaultDialect): inspector = PGInspector isolation_level = None - # TODO: need to inspect "standard_conforming_strings" _backslash_escapes = True - def __init__(self, isolation_level=None, **kwargs): + def __init__(self, isolation_level=None, json_serializer=None, + json_deserializer=None, **kwargs): default.DefaultDialect.__init__(self, **kwargs) self.isolation_level = isolation_level + self._json_deserializer = json_deserializer + self._json_serializer = json_serializer def initialize(self, connection): super(PGDialect, self).initialize(connection) @@ -1008,6 +1458,13 @@ class PGDialect(default.DefaultDialect): # psycopg2, others may have placed ENUM here as well self.colspecs.pop(ENUM, None) + # http://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689 + self.supports_smallserial = self.server_version_info >= (9, 2) + + self._backslash_escapes = connection.scalar( + "show standard_conforming_strings" + ) == 'off' + def on_connect(self): if self.isolation_level is not None: def connect(conn): @@ -1082,12 +1539,13 @@ class PGDialect(default.DefaultDialect): return connection.scalar("select current_schema()") def has_schema(self, connection, schema): + query = "select nspname from pg_namespace where lower(nspname)=:schema" cursor = connection.execute( sql.text( - "select nspname from pg_namespace where lower(nspname)=:schema", + query, bindparams=[ sql.bindparam( - 'schema', unicode(schema.lower()), + 'schema', util.text_type(schema.lower()), type_=sqltypes.Unicode)] ) ) @@ -1103,7 +1561,7 @@ class PGDialect(default.DefaultDialect): "n.oid=c.relnamespace where n.nspname=current_schema() and " "relname=:name", bindparams=[ - sql.bindparam('name', unicode(table_name), + sql.bindparam('name', util.text_type(table_name), type_=sqltypes.Unicode)] ) ) @@ -1115,9 +1573,9 @@ class PGDialect(default.DefaultDialect): "relname=:name", bindparams=[ sql.bindparam('name', - unicode(table_name), type_=sqltypes.Unicode), + util.text_type(table_name), type_=sqltypes.Unicode), sql.bindparam('schema', - unicode(schema), type_=sqltypes.Unicode)] + util.text_type(schema), type_=sqltypes.Unicode)] ) ) return bool(cursor.first()) @@ -1131,7 +1589,7 @@ class PGDialect(default.DefaultDialect): "n.nspname=current_schema() " "and relname=:name", bindparams=[ - sql.bindparam('name', unicode(sequence_name), + sql.bindparam('name', util.text_type(sequence_name), type_=sqltypes.Unicode) ] ) @@ -1143,10 +1601,10 @@ class PGDialect(default.DefaultDialect): "n.oid=c.relnamespace where relkind='S' and " "n.nspname=:schema and relname=:name", bindparams=[ - sql.bindparam('name', unicode(sequence_name), + sql.bindparam('name', util.text_type(sequence_name), type_=sqltypes.Unicode), sql.bindparam('schema', - unicode(schema), type_=sqltypes.Unicode) + util.text_type(schema), type_=sqltypes.Unicode) ] ) ) @@ -1154,12 +1612,6 @@ class PGDialect(default.DefaultDialect): return bool(cursor.first()) def has_type(self, connection, type_name, schema=None): - bindparams = [ - sql.bindparam('typname', - unicode(type_name), type_=sqltypes.Unicode), - sql.bindparam('nspname', - unicode(schema), type_=sqltypes.Unicode), - ] if schema is not None: query = """ SELECT EXISTS ( @@ -1169,6 +1621,7 @@ class PGDialect(default.DefaultDialect): AND n.nspname = :nspname ) """ + query = sql.text(query) else: query = """ SELECT EXISTS ( @@ -1177,12 +1630,25 @@ class PGDialect(default.DefaultDialect): AND pg_type_is_visible(t.oid) ) """ - cursor = connection.execute(sql.text(query, bindparams=bindparams)) + query = sql.text(query) + query = query.bindparams( + sql.bindparam('typname', + util.text_type(type_name), type_=sqltypes.Unicode), + ) + if schema is not None: + query = query.bindparams( + sql.bindparam('nspname', + util.text_type(schema), type_=sqltypes.Unicode), + ) + cursor = connection.execute(query) return bool(cursor.scalar()) def _get_server_version_info(self, connection): v = connection.execute("select version()").scalar() - m = re.match('PostgreSQL (\d+)\.(\d+)(?:\.(\d+))?(?:devel)?', v) + m = re.match( + '.*(?:PostgreSQL|EnterpriseDB) ' + '(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?(?:devel)?', + v) if not m: raise AssertionError( "Could not determine version from string '%s'" % v) @@ -1211,15 +1677,13 @@ class PGDialect(default.DefaultDialect): """ % schema_where_clause # Since we're binding to unicode, table_name and schema_name must be # unicode. - table_name = unicode(table_name) + table_name = util.text_type(table_name) if schema is not None: - schema = unicode(schema) - s = sql.text(query, bindparams=[ - sql.bindparam('table_name', type_=sqltypes.Unicode), - sql.bindparam('schema', type_=sqltypes.Unicode) - ], - typemap={'oid':sqltypes.Integer} - ) + schema = util.text_type(schema) + s = sql.text(query).bindparams(table_name=sqltypes.Unicode) + s = s.columns(oid=sqltypes.Integer) + if schema: + s = s.bindparams(sql.bindparam('schema', type_=sqltypes.Unicode)) c = connection.execute(s, table_name=table_name, schema=schema) table_oid = c.scalar() if table_oid is None: @@ -1235,13 +1699,13 @@ class PGDialect(default.DefaultDialect): """ rp = connection.execute(s) # what about system tables? - # Py3K - #schema_names = [row[0] for row in rp \ - # if not row[0].startswith('pg_')] - # Py2K - schema_names = [row[0].decode(self.encoding) for row in rp \ + + if util.py2k: + schema_names = [row[0].decode(self.encoding) for row in rp \ + if not row[0].startswith('pg_')] + else: + schema_names = [row[0] for row in rp \ if not row[0].startswith('pg_')] - # end Py2K return schema_names @reflection.cache @@ -1252,17 +1716,16 @@ class PGDialect(default.DefaultDialect): current_schema = self.default_schema_name result = connection.execute( - sql.text(u"SELECT relname FROM pg_class c " + sql.text("SELECT relname FROM pg_class c " "WHERE relkind = 'r' " "AND '%s' = (select nspname from pg_namespace n " "where n.oid = c.relnamespace) " % current_schema, - typemap = {'relname':sqltypes.Unicode} + typemap={'relname': sqltypes.Unicode} ) ) return [row[0] for row in result] - @reflection.cache def get_view_names(self, connection, schema=None, **kw): if schema is not None: @@ -1276,12 +1739,12 @@ class PGDialect(default.DefaultDialect): AND '%(schema)s' = (select nspname from pg_namespace n where n.oid = c.relnamespace) """ % dict(schema=current_schema) - # Py3K - #view_names = [row[0] for row in connection.execute(s)] - # Py2K - view_names = [row[0].decode(self.encoding) + + if util.py2k: + view_names = [row[0].decode(self.encoding) for row in connection.execute(s)] - # end Py2K + else: + view_names = [row[0] for row in connection.execute(s)] return view_names @reflection.cache @@ -1298,11 +1761,10 @@ class PGDialect(default.DefaultDialect): rp = connection.execute(sql.text(s), view_name=view_name, schema=current_schema) if rp: - # Py3K - #view_def = rp.scalar() - # Py2K - view_def = rp.scalar().decode(self.encoding) - # end Py2K + if util.py2k: + view_def = rp.scalar().decode(self.encoding) + else: + view_def = rp.scalar() return view_def @reflection.cache @@ -1313,8 +1775,7 @@ class PGDialect(default.DefaultDialect): SQL_COLS = """ SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), - (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) - for 128) + (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) @@ -1327,7 +1788,7 @@ class PGDialect(default.DefaultDialect): """ s = sql.text(SQL_COLS, bindparams=[sql.bindparam('table_oid', type_=sqltypes.Integer)], - typemap={'attname':sqltypes.Unicode, 'default':sqltypes.Unicode} + typemap={'attname': sqltypes.Unicode, 'default': sqltypes.Unicode} ) c = connection.execute(s, table_oid=table_oid) rows = c.fetchall() @@ -1337,117 +1798,125 @@ class PGDialect(default.DefaultDialect): # format columns columns = [] for name, format_type, default, notnull, attnum, table_oid in rows: - ## strip (5) from character varying(5), timestamp(5) - # with time zone, etc - attype = re.sub(r'\([\d,]+\)', '', format_type) - - # strip '[]' from integer[], etc. - attype = re.sub(r'\[\]', '', attype) - - nullable = not notnull - is_array = format_type.endswith('[]') - charlen = re.search('\(([\d,]+)\)', format_type) - if charlen: - charlen = charlen.group(1) - kwargs = {} - args = None - - if attype == 'numeric': - if charlen: - prec, scale = charlen.split(',') - args = (int(prec), int(scale)) - else: - args = () - elif attype == 'double precision': - args = (53, ) - elif attype == 'integer': - args = () - elif attype in ('timestamp with time zone', - 'time with time zone'): - kwargs['timezone'] = True - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif attype in ('timestamp without time zone', - 'time without time zone', 'time'): - kwargs['timezone'] = False - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif attype == 'bit varying': - kwargs['varying'] = True - if charlen: - args = (int(charlen),) - else: - args = () - elif attype in ('interval','interval year to month', - 'interval day to second'): - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif charlen: - args = (int(charlen),) - else: - args = () - - while True: - if attype in self.ischema_names: - coltype = self.ischema_names[attype] - break - elif attype in enums: - enum = enums[attype] - coltype = ENUM - if "." in attype: - kwargs['schema'], kwargs['name'] = attype.split('.') - else: - kwargs['name'] = attype - args = tuple(enum['labels']) - break - elif attype in domains: - domain = domains[attype] - attype = domain['attype'] - # A table can't override whether the domain is nullable. - nullable = domain['nullable'] - if domain['default'] and not default: - # It can, however, override the default - # value, but can't set it to null. - default = domain['default'] - continue - else: - coltype = None - break - - if coltype: - coltype = coltype(*args, **kwargs) - if is_array: - coltype = ARRAY(coltype) - else: - util.warn("Did not recognize type '%s' of column '%s'" % - (attype, name)) - coltype = sqltypes.NULLTYPE - # adjust the default value - autoincrement = False - if default is not None: - match = re.search(r"""(nextval\(')([^']+)('.*$)""", default) - if match is not None: - autoincrement = True - # the default is related to a Sequence - sch = schema - if '.' not in match.group(2) and sch is not None: - # unconditionally quote the schema name. this could - # later be enhanced to obey quoting rules / - # "quote schema" - default = match.group(1) + \ - ('"%s"' % sch) + '.' + \ - match.group(2) + match.group(3) - - column_info = dict(name=name, type=coltype, nullable=nullable, - default=default, autoincrement=autoincrement) + column_info = self._get_column_info( + name, format_type, default, notnull, domains, enums, schema) columns.append(column_info) return columns + def _get_column_info(self, name, format_type, default, + notnull, domains, enums, schema): + ## strip (*) from character varying(5), timestamp(5) + # with time zone, geometry(POLYGON), etc. + attype = re.sub(r'\(.*\)', '', format_type) + + # strip '[]' from integer[], etc. + attype = re.sub(r'\[\]', '', attype) + + nullable = not notnull + is_array = format_type.endswith('[]') + charlen = re.search('\(([\d,]+)\)', format_type) + if charlen: + charlen = charlen.group(1) + args = re.search('\((.*)\)', format_type) + if args and args.group(1): + args = tuple(re.split('\s*,\s*', args.group(1))) + else: + args = () + kwargs = {} + + if attype == 'numeric': + if charlen: + prec, scale = charlen.split(',') + args = (int(prec), int(scale)) + else: + args = () + elif attype == 'double precision': + args = (53, ) + elif attype == 'integer': + args = () + elif attype in ('timestamp with time zone', + 'time with time zone'): + kwargs['timezone'] = True + if charlen: + kwargs['precision'] = int(charlen) + args = () + elif attype in ('timestamp without time zone', + 'time without time zone', 'time'): + kwargs['timezone'] = False + if charlen: + kwargs['precision'] = int(charlen) + args = () + elif attype == 'bit varying': + kwargs['varying'] = True + if charlen: + args = (int(charlen),) + else: + args = () + elif attype in ('interval', 'interval year to month', + 'interval day to second'): + if charlen: + kwargs['precision'] = int(charlen) + args = () + elif charlen: + args = (int(charlen),) + + while True: + if attype in self.ischema_names: + coltype = self.ischema_names[attype] + break + elif attype in enums: + enum = enums[attype] + coltype = ENUM + if "." in attype: + kwargs['schema'], kwargs['name'] = attype.split('.') + else: + kwargs['name'] = attype + args = tuple(enum['labels']) + break + elif attype in domains: + domain = domains[attype] + attype = domain['attype'] + # A table can't override whether the domain is nullable. + nullable = domain['nullable'] + if domain['default'] and not default: + # It can, however, override the default + # value, but can't set it to null. + default = domain['default'] + continue + else: + coltype = None + break + + if coltype: + coltype = coltype(*args, **kwargs) + if is_array: + coltype = ARRAY(coltype) + else: + util.warn("Did not recognize type '%s' of column '%s'" % + (attype, name)) + coltype = sqltypes.NULLTYPE + # adjust the default value + autoincrement = False + if default is not None: + match = re.search(r"""(nextval\(')([^']+)('.*$)""", default) + if match is not None: + autoincrement = True + # the default is related to a Sequence + sch = schema + if '.' not in match.group(2) and sch is not None: + # unconditionally quote the schema name. this could + # later be enhanced to obey quoting rules / + # "quote schema" + default = match.group(1) + \ + ('"%s"' % sch) + '.' + \ + match.group(2) + match.group(3) + + column_info = dict(name=name, type=coltype, nullable=nullable, + default=default, autoincrement=autoincrement) + return column_info + @reflection.cache - def get_primary_keys(self, connection, table_name, schema=None, **kw): + def get_pk_constraint(self, connection, table_name, schema=None, **kw): table_oid = self.get_table_oid(connection, table_name, schema, info_cache=kw.get('info_cache')) @@ -1479,16 +1948,7 @@ class PGDialect(default.DefaultDialect): """ t = sql.text(PK_SQL, typemap={'attname': sqltypes.Unicode}) c = connection.execute(t, table_oid=table_oid) - primary_keys = [r[0] for r in c.fetchall()] - return primary_keys - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - cols = self.get_primary_keys(connection, table_name, - schema=schema, **kw) - - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) + cols = [r[0] for r in c.fetchall()] PK_CONS_SQL = """ SELECT conname @@ -1496,13 +1956,11 @@ class PGDialect(default.DefaultDialect): WHERE r.conrelid = :table_oid AND r.contype = 'p' ORDER BY 1 """ - t = sql.text(PK_CONS_SQL, typemap={'conname':sqltypes.Unicode}) + t = sql.text(PK_CONS_SQL, typemap={'conname': sqltypes.Unicode}) c = connection.execute(t, table_oid=table_oid) name = c.scalar() - return { - 'constrained_columns':cols, - 'name':name - } + + return {'constrained_columns': cols, 'name': name} @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): @@ -1524,22 +1982,34 @@ class PGDialect(default.DefaultDialect): n.oid = c.relnamespace ORDER BY 1 """ + # http://www.postgresql.org/docs/9.0/static/sql-createtable.html + FK_REGEX = re.compile( + r'FOREIGN KEY \((.*?)\) REFERENCES (?:(.*?)\.)?(.*?)\((.*?)\)' + r'[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?' + r'[\s]?(ON UPDATE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?' + r'[\s]?(ON DELETE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?' + r'[\s]?(DEFERRABLE|NOT DEFERRABLE)?' + r'[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?' + ) t = sql.text(FK_SQL, typemap={ - 'conname':sqltypes.Unicode, - 'condef':sqltypes.Unicode}) + 'conname': sqltypes.Unicode, + 'condef': sqltypes.Unicode}) c = connection.execute(t, table=table_oid) fkeys = [] for conname, condef, conschema in c.fetchall(): - m = re.search('FOREIGN KEY \((.*?)\) REFERENCES ' - '(?:(.*?)\.)?(.*?)\((.*?)\)', condef).groups() + m = re.search(FK_REGEX, condef).groups() constrained_columns, referred_schema, \ - referred_table, referred_columns = m + referred_table, referred_columns, \ + _, match, _, onupdate, _, ondelete, \ + deferrable, _, initially = m + if deferrable is not None: + deferrable = True if deferrable == 'DEFERRABLE' else False constrained_columns = [preparer._unquote_identifier(x) for x in re.split(r'\s*,\s*', constrained_columns)] if referred_schema: - referred_schema =\ + referred_schema = \ preparer._unquote_identifier(referred_schema) elif schema is not None and schema == conschema: # no schema was returned by pg_get_constraintdef(). This @@ -1553,11 +2023,18 @@ class PGDialect(default.DefaultDialect): referred_columns = [preparer._unquote_identifier(x) for x in re.split(r'\s*,\s', referred_columns)] fkey_d = { - 'name' : conname, - 'constrained_columns' : constrained_columns, - 'referred_schema' : referred_schema, - 'referred_table' : referred_table, - 'referred_columns' : referred_columns + 'name': conname, + 'constrained_columns': constrained_columns, + 'referred_schema': referred_schema, + 'referred_table': referred_table, + 'referred_columns': referred_columns, + 'options': { + 'onupdate': onupdate, + 'ondelete': ondelete, + 'deferrable': deferrable, + 'initially': initially, + 'match': match + } } fkeys.append(fkey_d) return fkeys @@ -1567,11 +2044,14 @@ class PGDialect(default.DefaultDialect): table_oid = self.get_table_oid(connection, table_name, schema, info_cache=kw.get('info_cache')) + # cast indkey as varchar since it's an int2vector, + # returned as a list by some drivers such as pypostgresql + IDX_SQL = """ SELECT i.relname as relname, ix.indisunique, ix.indexprs, ix.indpred, - a.attname + a.attname, a.attnum, ix.indkey::varchar FROM pg_class t join pg_index ix on t.oid = ix.indrelid @@ -1588,14 +2068,15 @@ class PGDialect(default.DefaultDialect): i.relname """ - t = sql.text(IDX_SQL, typemap={'attname':sqltypes.Unicode}) + t = sql.text(IDX_SQL, typemap={'attname': sqltypes.Unicode}) c = connection.execute(t, table_oid=table_oid) - index_names = {} - indexes = [] + indexes = defaultdict(lambda: defaultdict(dict)) + sv_idx_name = None for row in c.fetchall(): - idx_name, unique, expr, prd, col = row + idx_name, unique, expr, prd, col, col_num, idx_key = row + if expr: if idx_name != sv_idx_name: util.warn( @@ -1604,22 +2085,61 @@ class PGDialect(default.DefaultDialect): % idx_name) sv_idx_name = idx_name continue + if prd and not idx_name == sv_idx_name: util.warn( "Predicate of partial index %s ignored during reflection" % idx_name) sv_idx_name = idx_name - if idx_name in index_names: - index_d = index_names[idx_name] - else: - index_d = {'column_names':[]} - indexes.append(index_d) - index_names[idx_name] = index_d - index_d['name'] = idx_name + + index = indexes[idx_name] if col is not None: - index_d['column_names'].append(col) - index_d['unique'] = unique - return indexes + index['cols'][col_num] = col + index['key'] = [int(k.strip()) for k in idx_key.split()] + index['unique'] = unique + + return [ + {'name': name, + 'unique': idx['unique'], + 'column_names': [idx['cols'][i] for i in idx['key']]} + for name, idx in indexes.items() + ] + + @reflection.cache + def get_unique_constraints(self, connection, table_name, + schema=None, **kw): + table_oid = self.get_table_oid(connection, table_name, schema, + info_cache=kw.get('info_cache')) + + UNIQUE_SQL = """ + SELECT + cons.conname as name, + cons.conkey as key, + a.attnum as col_num, + a.attname as col_name + FROM + pg_catalog.pg_constraint cons + join pg_attribute a + on cons.conrelid = a.attrelid AND a.attnum = ANY(cons.conkey) + WHERE + cons.conrelid = :table_oid AND + cons.contype = 'u' + """ + + t = sql.text(UNIQUE_SQL, typemap={'col_name': sqltypes.Unicode}) + c = connection.execute(t, table_oid=table_oid) + + uniques = defaultdict(lambda: defaultdict(dict)) + for row in c.fetchall(): + uc = uniques[row.name] + uc["key"] = row.key + uc["cols"][row.col_num] = row.col_name + + return [ + {'name': name, + 'column_names': [uc["cols"][i] for i in uc["key"]]} + for name, uc in uniques.items() + ] def _load_enums(self, connection): if not self.supports_native_enum: @@ -1641,8 +2161,8 @@ class PGDialect(default.DefaultDialect): """ s = sql.text(SQL_ENUMS, typemap={ - 'attname':sqltypes.Unicode, - 'label':sqltypes.Unicode}) + 'attname': sqltypes.Unicode, + 'label': sqltypes.Unicode}) c = connection.execute(s) enums = {} @@ -1679,7 +2199,7 @@ class PGDialect(default.DefaultDialect): WHERE t.typtype = 'd' """ - s = sql.text(SQL_DOMAINS, typemap={'attname':sqltypes.Unicode}) + s = sql.text(SQL_DOMAINS, typemap={'attname': sqltypes.Unicode}) c = connection.execute(s) domains = {} @@ -1696,10 +2216,9 @@ class PGDialect(default.DefaultDialect): name = "%s.%s" % (domain['schema'], domain['name']) domains[name] = { - 'attype':attype, + 'attype': attype, 'nullable': domain['nullable'], 'default': domain['default'] } return domains - diff --git a/libs/sqlalchemy/dialects/postgresql/constraints.py b/libs/sqlalchemy/dialects/postgresql/constraints.py new file mode 100644 index 00000000..f45cef1a --- /dev/null +++ b/libs/sqlalchemy/dialects/postgresql/constraints.py @@ -0,0 +1,73 @@ +# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +from sqlalchemy.schema import ColumnCollectionConstraint +from sqlalchemy.sql import expression + +class ExcludeConstraint(ColumnCollectionConstraint): + """A table-level EXCLUDE constraint. + + Defines an EXCLUDE constraint as described in the `postgres + documentation`__. + + __ http://www.postgresql.org/docs/9.0/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE + """ + + __visit_name__ = 'exclude_constraint' + + where = None + + def __init__(self, *elements, **kw): + """ + :param \*elements: + A sequence of two tuples of the form ``(column, operator)`` where + column must be a column name or Column object and operator must + be a string containing the operator to use. + + :param name: + Optional, the in-database name of this constraint. + + :param deferrable: + Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when + issuing DDL for this constraint. + + :param initially: + Optional string. If set, emit INITIALLY when issuing DDL + for this constraint. + + :param using: + Optional string. If set, emit USING when issuing DDL + for this constraint. Defaults to 'gist'. + + :param where: + Optional string. If set, emit WHERE when issuing DDL + for this constraint. + + """ + ColumnCollectionConstraint.__init__( + self, + *[col for col, op in elements], + name=kw.get('name'), + deferrable=kw.get('deferrable'), + initially=kw.get('initially') + ) + self.operators = {} + for col_or_string, op in elements: + name = getattr(col_or_string, 'name', col_or_string) + self.operators[name] = op + self.using = kw.get('using', 'gist') + where = kw.get('where') + if where: + self.where = expression._literal_as_text(where) + + def copy(self, **kw): + elements = [(col, self.operators[col]) + for col in self.columns.keys()] + c = self.__class__(*elements, + name=self.name, + deferrable=self.deferrable, + initially=self.initially) + c.dispatch._update(self.dispatch) + return c + diff --git a/libs/sqlalchemy/dialects/postgresql/hstore.py b/libs/sqlalchemy/dialects/postgresql/hstore.py new file mode 100644 index 00000000..76562088 --- /dev/null +++ b/libs/sqlalchemy/dialects/postgresql/hstore.py @@ -0,0 +1,369 @@ +# postgresql/hstore.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import re + +from .base import ARRAY, ischema_names +from ... import types as sqltypes +from ...sql import functions as sqlfunc +from ...sql.operators import custom_op +from ... import util + +__all__ = ('HSTORE', 'hstore') + +# My best guess at the parsing rules of hstore literals, since no formal +# grammar is given. This is mostly reverse engineered from PG's input parser +# behavior. +HSTORE_PAIR_RE = re.compile(r""" +( + "(?P (\\ . | [^"])* )" # Quoted key +) +[ ]* => [ ]* # Pair operator, optional adjoining whitespace +( + (?P NULL ) # NULL value + | "(?P (\\ . | [^"])* )" # Quoted value +) +""", re.VERBOSE) + +HSTORE_DELIMITER_RE = re.compile(r""" +[ ]* , [ ]* +""", re.VERBOSE) + + +def _parse_error(hstore_str, pos): + """format an unmarshalling error.""" + + ctx = 20 + hslen = len(hstore_str) + + parsed_tail = hstore_str[max(pos - ctx - 1, 0):min(pos, hslen)] + residual = hstore_str[min(pos, hslen):min(pos + ctx + 1, hslen)] + + if len(parsed_tail) > ctx: + parsed_tail = '[...]' + parsed_tail[1:] + if len(residual) > ctx: + residual = residual[:-1] + '[...]' + + return "After %r, could not parse residual at position %d: %r" % ( + parsed_tail, pos, residual) + + +def _parse_hstore(hstore_str): + """Parse an hstore from it's literal string representation. + + Attempts to approximate PG's hstore input parsing rules as closely as + possible. Although currently this is not strictly necessary, since the + current implementation of hstore's output syntax is stricter than what it + accepts as input, the documentation makes no guarantees that will always + be the case. + + + + """ + result = {} + pos = 0 + pair_match = HSTORE_PAIR_RE.match(hstore_str) + + while pair_match is not None: + key = pair_match.group('key').replace(r'\"', '"').replace("\\\\", "\\") + if pair_match.group('value_null'): + value = None + else: + value = pair_match.group('value').replace(r'\"', '"').replace("\\\\", "\\") + result[key] = value + + pos += pair_match.end() + + delim_match = HSTORE_DELIMITER_RE.match(hstore_str[pos:]) + if delim_match is not None: + pos += delim_match.end() + + pair_match = HSTORE_PAIR_RE.match(hstore_str[pos:]) + + if pos != len(hstore_str): + raise ValueError(_parse_error(hstore_str, pos)) + + return result + + +def _serialize_hstore(val): + """Serialize a dictionary into an hstore literal. Keys and values must + both be strings (except None for values). + + """ + def esc(s, position): + if position == 'value' and s is None: + return 'NULL' + elif isinstance(s, util.string_types): + return '"%s"' % s.replace("\\", "\\\\").replace('"', r'\"') + else: + raise ValueError("%r in %s position is not a string." % + (s, position)) + + return ', '.join('%s=>%s' % (esc(k, 'key'), esc(v, 'value')) + for k, v in val.items()) + + +class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine): + """Represent the Postgresql HSTORE type. + + The :class:`.HSTORE` type stores dictionaries containing strings, e.g.:: + + data_table = Table('data_table', metadata, + Column('id', Integer, primary_key=True), + Column('data', HSTORE) + ) + + with engine.connect() as conn: + conn.execute( + data_table.insert(), + data = {"key1": "value1", "key2": "value2"} + ) + + :class:`.HSTORE` provides for a wide range of operations, including: + + * Index operations:: + + data_table.c.data['some key'] == 'some value' + + * Containment operations:: + + data_table.c.data.has_key('some key') + + data_table.c.data.has_all(['one', 'two', 'three']) + + * Concatenation:: + + data_table.c.data + {"k1": "v1"} + + For a full list of special methods see :class:`.HSTORE.comparator_factory`. + + For usage with the SQLAlchemy ORM, it may be desirable to combine + the usage of :class:`.HSTORE` with :class:`.MutableDict` dictionary + now part of the :mod:`sqlalchemy.ext.mutable` + extension. This extension will allow "in-place" changes to the + dictionary, e.g. addition of new keys or replacement/removal of existing + keys to/from the current dictionary, to produce events which will be detected + by the unit of work:: + + from sqlalchemy.ext.mutable import MutableDict + + class MyClass(Base): + __tablename__ = 'data_table' + + id = Column(Integer, primary_key=True) + data = Column(MutableDict.as_mutable(HSTORE)) + + my_object = session.query(MyClass).one() + + # in-place mutation, requires Mutable extension + # in order for the ORM to detect + my_object.data['some_key'] = 'some value' + + session.commit() + + When the :mod:`sqlalchemy.ext.mutable` extension is not used, the ORM + will not be alerted to any changes to the contents of an existing dictionary, + unless that dictionary value is re-assigned to the HSTORE-attribute itself, + thus generating a change event. + + .. versionadded:: 0.8 + + .. seealso:: + + :class:`.hstore` - render the Postgresql ``hstore()`` function. + + + """ + + __visit_name__ = 'HSTORE' + + class comparator_factory(sqltypes.Concatenable.Comparator): + """Define comparison operations for :class:`.HSTORE`.""" + + def has_key(self, other): + """Boolean expression. Test for presence of a key. Note that the + key may be a SQLA expression. + """ + return self.expr.op('?')(other) + + def has_all(self, other): + """Boolean expression. Test for presence of all keys in the PG + array. + """ + return self.expr.op('?&')(other) + + def has_any(self, other): + """Boolean expression. Test for presence of any key in the PG + array. + """ + return self.expr.op('?|')(other) + + def defined(self, key): + """Boolean expression. Test for presence of a non-NULL value for + the key. Note that the key may be a SQLA expression. + """ + return _HStoreDefinedFunction(self.expr, key) + + def contains(self, other, **kwargs): + """Boolean expression. Test if keys are a superset of the keys of + the argument hstore expression. + """ + return self.expr.op('@>')(other) + + def contained_by(self, other): + """Boolean expression. Test if keys are a proper subset of the + keys of the argument hstore expression. + """ + return self.expr.op('<@')(other) + + def __getitem__(self, other): + """Text expression. Get the value at a given key. Note that the + key may be a SQLA expression. + """ + return self.expr.op('->', precedence=5)(other) + + def delete(self, key): + """HStore expression. Returns the contents of this hstore with the + given key deleted. Note that the key may be a SQLA expression. + """ + if isinstance(key, dict): + key = _serialize_hstore(key) + return _HStoreDeleteFunction(self.expr, key) + + def slice(self, array): + """HStore expression. Returns a subset of an hstore defined by + array of keys. + """ + return _HStoreSliceFunction(self.expr, array) + + def keys(self): + """Text array expression. Returns array of keys.""" + return _HStoreKeysFunction(self.expr) + + def vals(self): + """Text array expression. Returns array of values.""" + return _HStoreValsFunction(self.expr) + + def array(self): + """Text array expression. Returns array of alternating keys and + values. + """ + return _HStoreArrayFunction(self.expr) + + def matrix(self): + """Text array expression. Returns array of [key, value] pairs.""" + return _HStoreMatrixFunction(self.expr) + + def _adapt_expression(self, op, other_comparator): + if isinstance(op, custom_op): + if op.opstring in ['?', '?&', '?|', '@>', '<@']: + return op, sqltypes.Boolean + elif op.opstring == '->': + return op, sqltypes.Text + return sqltypes.Concatenable.Comparator.\ + _adapt_expression(self, op, other_comparator) + + def bind_processor(self, dialect): + if util.py2k: + encoding = dialect.encoding + def process(value): + if isinstance(value, dict): + return _serialize_hstore(value).encode(encoding) + else: + return value + else: + def process(value): + if isinstance(value, dict): + return _serialize_hstore(value) + else: + return value + return process + + def result_processor(self, dialect, coltype): + if util.py2k: + encoding = dialect.encoding + def process(value): + if value is not None: + return _parse_hstore(value.decode(encoding)) + else: + return value + else: + def process(value): + if value is not None: + return _parse_hstore(value) + else: + return value + return process + + +ischema_names['hstore'] = HSTORE + + +class hstore(sqlfunc.GenericFunction): + """Construct an hstore value within a SQL expression using the + Postgresql ``hstore()`` function. + + The :class:`.hstore` function accepts one or two arguments as described + in the Postgresql documentation. + + E.g.:: + + from sqlalchemy.dialects.postgresql import array, hstore + + select([hstore('key1', 'value1')]) + + select([ + hstore( + array(['key1', 'key2', 'key3']), + array(['value1', 'value2', 'value3']) + ) + ]) + + .. versionadded:: 0.8 + + .. seealso:: + + :class:`.HSTORE` - the Postgresql ``HSTORE`` datatype. + + """ + type = HSTORE + name = 'hstore' + + +class _HStoreDefinedFunction(sqlfunc.GenericFunction): + type = sqltypes.Boolean + name = 'defined' + + +class _HStoreDeleteFunction(sqlfunc.GenericFunction): + type = HSTORE + name = 'delete' + + +class _HStoreSliceFunction(sqlfunc.GenericFunction): + type = HSTORE + name = 'slice' + + +class _HStoreKeysFunction(sqlfunc.GenericFunction): + type = ARRAY(sqltypes.Text) + name = 'akeys' + + +class _HStoreValsFunction(sqlfunc.GenericFunction): + type = ARRAY(sqltypes.Text) + name = 'avals' + + +class _HStoreArrayFunction(sqlfunc.GenericFunction): + type = ARRAY(sqltypes.Text) + name = 'hstore_to_array' + + +class _HStoreMatrixFunction(sqlfunc.GenericFunction): + type = ARRAY(sqltypes.Text) + name = 'hstore_to_matrix' diff --git a/libs/sqlalchemy/dialects/postgresql/json.py b/libs/sqlalchemy/dialects/postgresql/json.py new file mode 100644 index 00000000..2e29185e --- /dev/null +++ b/libs/sqlalchemy/dialects/postgresql/json.py @@ -0,0 +1,199 @@ +# postgresql/json.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +from __future__ import absolute_import + +import json + +from .base import ischema_names +from ... import types as sqltypes +from ...sql.operators import custom_op +from ... import sql +from ...sql import elements +from ... import util + +__all__ = ('JSON', 'JSONElement') + + +class JSONElement(elements.BinaryExpression): + """Represents accessing an element of a :class:`.JSON` value. + + The :class:`.JSONElement` is produced whenever using the Python index + operator on an expression that has the type :class:`.JSON`:: + + expr = mytable.c.json_data['some_key'] + + The expression typically compiles to a JSON access such as ``col -> key``. + Modifiers are then available for typing behavior, including :meth:`.JSONElement.cast` + and :attr:`.JSONElement.astext`. + + """ + def __init__(self, left, right, astext=False, opstring=None, result_type=None): + self._astext = astext + if opstring is None: + if hasattr(right, '__iter__') and \ + not isinstance(right, util.string_types): + opstring = "#>" + right = "{%s}" % (", ".join(util.text_type(elem) for elem in right)) + else: + opstring = "->" + + self._json_opstring = opstring + operator = custom_op(opstring, precedence=5) + right = left._check_literal(left, operator, right) + super(JSONElement, self).__init__(left, right, operator, type_=result_type) + + @property + def astext(self): + """Convert this :class:`.JSONElement` to use the 'astext' operator + when evaluated. + + E.g.:: + + select([data_table.c.data['some key'].astext]) + + .. seealso:: + + :meth:`.JSONElement.cast` + + """ + if self._astext: + return self + else: + return JSONElement( + self.left, + self.right, + astext=True, + opstring=self._json_opstring + ">", + result_type=sqltypes.String(convert_unicode=True) + ) + + def cast(self, type_): + """Convert this :class:`.JSONElement` to apply both the 'astext' operator + as well as an explicit type cast when evaulated. + + E.g.:: + + select([data_table.c.data['some key'].cast(Integer)]) + + .. seealso:: + + :attr:`.JSONElement.astext` + + """ + if not self._astext: + return self.astext.cast(type_) + else: + return sql.cast(self, type_) + + +class JSON(sqltypes.TypeEngine): + """Represent the Postgresql JSON type. + + The :class:`.JSON` type stores arbitrary JSON format data, e.g.:: + + data_table = Table('data_table', metadata, + Column('id', Integer, primary_key=True), + Column('data', JSON) + ) + + with engine.connect() as conn: + conn.execute( + data_table.insert(), + data = {"key1": "value1", "key2": "value2"} + ) + + :class:`.JSON` provides several operations: + + * Index operations:: + + data_table.c.data['some key'] + + * Index operations returning text (required for text comparison):: + + data_table.c.data['some key'].astext == 'some value' + + * Index operations with a built-in CAST call:: + + data_table.c.data['some key'].cast(Integer) == 5 + + * Path index operations:: + + data_table.c.data[('key_1', 'key_2', ..., 'key_n')] + + * Path index operations returning text (required for text comparison):: + + data_table.c.data[('key_1', 'key_2', ..., 'key_n')].astext == 'some value' + + Index operations return an instance of :class:`.JSONElement`, which represents + an expression such as ``column -> index``. This element then defines + methods such as :attr:`.JSONElement.astext` and :meth:`.JSONElement.cast` + for setting up type behavior. + + The :class:`.JSON` type, when used with the SQLAlchemy ORM, does not detect + in-place mutations to the structure. In order to detect these, the + :mod:`sqlalchemy.ext.mutable` extension must be used. This extension will + allow "in-place" changes to the datastructure to produce events which + will be detected by the unit of work. See the example at :class:`.HSTORE` + for a simple example involving a dictionary. + + Custom serializers and deserializers are specified at the dialect level, + that is using :func:`.create_engine`. The reason for this is that when + using psycopg2, the DBAPI only allows serializers at the per-cursor + or per-connection level. E.g.:: + + engine = create_engine("postgresql://scott:tiger@localhost/test", + json_serializer=my_serialize_fn, + json_deserializer=my_deserialize_fn + ) + + When using the psycopg2 dialect, the json_deserializer is registered + against the database using ``psycopg2.extras.register_default_json``. + + .. versionadded:: 0.9 + + """ + + __visit_name__ = 'JSON' + + class comparator_factory(sqltypes.Concatenable.Comparator): + """Define comparison operations for :class:`.JSON`.""" + + def __getitem__(self, other): + """Get the value at a given key.""" + + return JSONElement(self.expr, other) + + def _adapt_expression(self, op, other_comparator): + if isinstance(op, custom_op): + if op.opstring == '->': + return op, sqltypes.Text + return sqltypes.Concatenable.Comparator.\ + _adapt_expression(self, op, other_comparator) + + def bind_processor(self, dialect): + json_serializer = dialect._json_serializer or json.dumps + if util.py2k: + encoding = dialect.encoding + def process(value): + return json_serializer(value).encode(encoding) + else: + def process(value): + return json_serializer(value) + return process + + def result_processor(self, dialect, coltype): + json_deserializer = dialect._json_deserializer or json.loads + if util.py2k: + encoding = dialect.encoding + def process(value): + return json_deserializer(value.decode(encoding)) + else: + def process(value): + return json_deserializer(value) + return process + + +ischema_names['json'] = JSON diff --git a/libs/sqlalchemy/dialects/postgresql/pg8000.py b/libs/sqlalchemy/dialects/postgresql/pg8000.py index dc72555e..bc73f975 100644 --- a/libs/sqlalchemy/dialects/postgresql/pg8000.py +++ b/libs/sqlalchemy/dialects/postgresql/pg8000.py @@ -1,16 +1,15 @@ # postgresql/pg8000.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the PostgreSQL database via the pg8000 driver. - -Connecting ----------- - -URLs are of the form -``postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...]``. +""" +.. dialect:: postgresql+pg8000 + :name: pg8000 + :dbapi: pg8000 + :connectstring: postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...] + :url: http://pybrary.net/pg8000/ Unicode ------- @@ -27,19 +26,22 @@ Passing data from/to the Interval type is not supported as of yet. """ -from sqlalchemy import util, exc -from sqlalchemy.util.compat import decimal -from sqlalchemy import processors -from sqlalchemy import types as sqltypes -from sqlalchemy.dialects.postgresql.base import PGDialect, \ +from ... import util, exc +import decimal +from ... import processors +from ... import types as sqltypes +from .base import PGDialect, \ PGCompiler, PGIdentifierPreparer, PGExecutionContext,\ _DECIMAL_TYPES, _FLOAT_TYPES, _INT_TYPES + class _PGNumeric(sqltypes.Numeric): def result_processor(self, dialect, coltype): if self.asdecimal: if coltype in _FLOAT_TYPES: - return processors.to_decimal_processor_factory(decimal.Decimal) + return processors.to_decimal_processor_factory( + decimal.Decimal, + self._effective_decimal_return_scale) elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: # pg8000 returns Decimal natively for 1700 return None @@ -61,17 +63,20 @@ class _PGNumericNoBind(_PGNumeric): def bind_processor(self, dialect): return None + class PGExecutionContext_pg8000(PGExecutionContext): pass class PGCompiler_pg8000(PGCompiler): - def visit_mod(self, binary, **kw): - return self.process(binary.left) + " %% " + self.process(binary.right) + def visit_mod_binary(self, binary, operator, **kw): + return self.process(binary.left, **kw) + " %% " + \ + self.process(binary.right, **kw) def post_process_text(self, text): if '%%' in text: - util.warn("The SQLAlchemy postgresql dialect now automatically escapes '%' in text() " + util.warn("The SQLAlchemy postgresql dialect " + "now automatically escapes '%' in text() " "expressions to '%%'.") return text.replace('%', '%%') @@ -99,8 +104,8 @@ class PGDialect_pg8000(PGDialect): colspecs = util.update_copy( PGDialect.colspecs, { - sqltypes.Numeric : _PGNumericNoBind, - sqltypes.Float : _PGNumeric + sqltypes.Numeric: _PGNumericNoBind, + sqltypes.Float: _PGNumeric } ) diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg2.py b/libs/sqlalchemy/dialects/postgresql/psycopg2.py index ecc8d331..e9f64f82 100644 --- a/libs/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/libs/sqlalchemy/dialects/postgresql/psycopg2.py @@ -1,42 +1,39 @@ # postgresql/psycopg2.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the PostgreSQL database via the psycopg2 driver. +""" +.. dialect:: postgresql+psycopg2 + :name: psycopg2 + :dbapi: psycopg2 + :connectstring: postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...] + :url: http://pypi.python.org/pypi/psycopg2/ -Driver ------- - -The psycopg2 driver is available at http://pypi.python.org/pypi/psycopg2/ . -The dialect has several behaviors which are specifically tailored towards compatibility -with this module. - -Note that psycopg1 is **not** supported. - -Connecting ----------- - -URLs are of the form -``postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]``. +psycopg2 Connect Arguments +----------------------------------- psycopg2-specific keyword arguments which are accepted by :func:`.create_engine()` are: -* *server_side_cursors* - Enable the usage of "server side cursors" for SQL +* ``server_side_cursors``: Enable the usage of "server side cursors" for SQL statements which support this feature. What this essentially means from a psycopg2 point of view is that the cursor is created using a name, e.g. ``connection.cursor('some name')``, which has the effect that result rows are not immediately pre-fetched and buffered after statement execution, but are instead left on the server and only retrieved as needed. SQLAlchemy's - :class:`~sqlalchemy.engine.base.ResultProxy` uses special row-buffering + :class:`~sqlalchemy.engine.ResultProxy` uses special row-buffering behavior when this feature is enabled, such that groups of 100 rows at a time are fetched over the wire to reduce conversational overhead. Note that the ``stream_results=True`` execution option is a more targeted way of enabling this mode on a per-execution basis. -* *use_native_unicode* - Enable the usage of Psycopg2 "native unicode" mode - per connection. True by default. +* ``use_native_unicode``: Enable the usage of Psycopg2 "native unicode" mode + per connection. True by default. +* ``isolation_level``: This option, available for all Posgtresql dialects, + includes the ``AUTOCOMMIT`` isolation level when using the psycopg2 + dialect. See :ref:`psycopg2_isolation_level`. + Unix Domain Connections ------------------------ @@ -66,11 +63,14 @@ The following DBAPI-specific options are respected when used with :meth:`.Query.execution_options`, in addition to those not specific to DBAPIs: * isolation_level - Set the transaction isolation level for the lifespan of a - :class:`.Connection` (can only be set on a connection, not a statement or query). - This includes the options ``SERIALIZABLE``, ``READ COMMITTED``, - ``READ UNCOMMITTED`` and ``REPEATABLE READ``. -* stream_results - Enable or disable usage of server side cursors. - If ``None`` or not set, the ``server_side_cursors`` option of the :class:`.Engine` is used. + :class:`.Connection` (can only be set on a connection, not a statement + or query). See :ref:`psycopg2_isolation_level`. + +* stream_results - Enable or disable usage of psycopg2 server side cursors - + this feature makes use of "named" cursors in combination with special + result handling methods so that result rows are not fully buffered. + If ``None`` or not set, the ``server_side_cursors`` option of the + :class:`.Engine` is used. Unicode ------- @@ -98,13 +98,14 @@ on all new connections based on the value passed to This overrides the encoding specified in the Postgresql client configuration. .. versionadded:: 0.7.3 - The psycopg2-specific ``client_encoding`` parameter to :func:`.create_engine`. + The psycopg2-specific ``client_encoding`` parameter to + :func:`.create_engine`. SQLAlchemy can also be instructed to skip the usage of the psycopg2 ``UNICODE`` extension and to instead utilize it's own unicode encode/decode services, which are normally reserved only for those DBAPIs that don't -fully support unicode directly. Passing ``use_native_unicode=False`` -to :func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``. +fully support unicode directly. Passing ``use_native_unicode=False`` to +:func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``. SQLAlchemy will instead encode data itself into Python bytestrings on the way in and coerce from bytes on the way back, using the value of the :func:`.create_engine` ``encoding`` parameter, which @@ -118,16 +119,31 @@ Transactions The psycopg2 dialect fully supports SAVEPOINT and two-phase commit operations. -.. _psycopg2_isolation: +.. _psycopg2_isolation_level: -Transaction Isolation Level ---------------------------- +Psycopg2 Transaction Isolation Level +------------------------------------- -The ``isolation_level`` parameter of :func:`.create_engine` here makes use +As discussed in :ref:`postgresql_isolation_level`, +all Postgresql dialects support setting of transaction isolation level +both via the ``isolation_level`` parameter passed to :func:`.create_engine`, +as well as the ``isolation_level`` argument used by :meth:`.Connection.execution_options`. +When using the psycopg2 dialect, these options make use of psycopg2's ``set_isolation_level()`` connection method, rather than -issuing a ``SET SESSION CHARACTERISTICS`` command. This because psycopg2 -resets the isolation level on each new transaction, and needs to know -at the API level what level should be used. +emitting a Postgresql directive; this is because psycopg2's API-level +setting is always emitted at the start of each transaction in any case. + +The psycopg2 dialect supports these constants for isolation level: + +* ``READ COMMITTED`` +* ``READ UNCOMMITTED`` +* ``REPEATABLE READ`` +* ``SERIALIZABLE`` +* ``AUTOCOMMIT`` + +.. versionadded:: 0.8.2 support for AUTOCOMMIT isolation level when using + psycopg2. + NOTICE logging --------------- @@ -138,22 +154,32 @@ The psycopg2 dialect will log Postgresql NOTICE messages via the import logging logging.getLogger('sqlalchemy.dialects.postgresql').setLevel(logging.INFO) +HSTORE type +------------ + +The psycopg2 dialect will make use of the +``psycopg2.extensions.register_hstore()`` extension when using the HSTORE +type. This replaces SQLAlchemy's pure-Python HSTORE coercion which takes +effect for other DBAPIs. """ +from __future__ import absolute_import import re import logging -from sqlalchemy import util, exc -from sqlalchemy.util.compat import decimal -from sqlalchemy import processors -from sqlalchemy.engine import base -from sqlalchemy.sql import expression -from sqlalchemy import types as sqltypes -from sqlalchemy.dialects.postgresql.base import PGDialect, PGCompiler, \ +from ... import util, exc +import decimal +from ... import processors +from ...engine import result as _result +from ...sql import expression +from ... import types as sqltypes +from .base import PGDialect, PGCompiler, \ PGIdentifierPreparer, PGExecutionContext, \ ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES,\ _INT_TYPES +from .hstore import HSTORE +from .json import JSON logger = logging.getLogger('sqlalchemy.dialects.postgresql') @@ -166,7 +192,9 @@ class _PGNumeric(sqltypes.Numeric): def result_processor(self, dialect, coltype): if self.asdecimal: if coltype in _FLOAT_TYPES: - return processors.to_decimal_processor_factory(decimal.Decimal) + return processors.to_decimal_processor_factory( + decimal.Decimal, + self._effective_decimal_return_scale) elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: # pg8000 returns Decimal natively for 1700 return None @@ -183,24 +211,37 @@ class _PGNumeric(sqltypes.Numeric): raise exc.InvalidRequestError( "Unknown PG numeric type: %d" % coltype) -class _PGEnum(ENUM): - def __init__(self, *arg, **kw): - super(_PGEnum, self).__init__(*arg, **kw) - # Py2K - if self.convert_unicode: - self.convert_unicode = "force" - # end Py2K -class _PGArray(ARRAY): - def __init__(self, *arg, **kw): - super(_PGArray, self).__init__(*arg, **kw) - # Py2K - # FIXME: this check won't work for setups that - # have convert_unicode only on their create_engine(). - if isinstance(self.item_type, sqltypes.String) and \ - self.item_type.convert_unicode: - self.item_type.convert_unicode = "force" - # end Py2K +class _PGEnum(ENUM): + def result_processor(self, dialect, coltype): + if util.py2k and self.convert_unicode is True: + # we can't easily use PG's extensions here because + # the OID is on the fly, and we need to give it a python + # function anyway - not really worth it. + self.convert_unicode = "force_nocheck" + return super(_PGEnum, self).result_processor(dialect, coltype) + +class _PGHStore(HSTORE): + def bind_processor(self, dialect): + if dialect._has_native_hstore: + return None + else: + return super(_PGHStore, self).bind_processor(dialect) + + def result_processor(self, dialect, coltype): + if dialect._has_native_hstore: + return None + else: + return super(_PGHStore, self).result_processor(dialect, coltype) + + +class _PGJSON(JSON): + + def result_processor(self, dialect, coltype): + if dialect._has_native_json: + return None + else: + return super(_PGJSON, self).result_processor(dialect, coltype) # When we're handed literal SQL, ensure it's a SELECT-query. Since # 8.3, combining cursors and "FOR UPDATE" has been fine. @@ -210,6 +251,7 @@ SERVER_SIDE_CURSOR_RE = re.compile( _server_side_id = util.counter() + class PGExecutionContext_psycopg2(PGExecutionContext): def create_cursor(self): # TODO: coverage for server side cursors + select.for_update() @@ -221,12 +263,13 @@ class PGExecutionContext_psycopg2(PGExecutionContext): or \ ( (not self.compiled or - isinstance(self.compiled.statement, expression._TextClause)) + isinstance(self.compiled.statement, expression.TextClause)) and self.statement and SERVER_SIDE_CURSOR_RE.match(self.statement)) ) ) else: - is_server_side = self.execution_options.get('stream_results', False) + is_server_side = \ + self.execution_options.get('stream_results', False) self.__is_server_side = is_server_side if is_server_side: @@ -243,9 +286,9 @@ class PGExecutionContext_psycopg2(PGExecutionContext): self._log_notices(self.cursor) if self.__is_server_side: - return base.BufferedRowResultProxy(self) + return _result.BufferedRowResultProxy(self) else: - return base.ResultProxy(self) + return _result.ResultProxy(self) def _log_notices(self, cursor): for notice in cursor.connection.notices: @@ -257,8 +300,9 @@ class PGExecutionContext_psycopg2(PGExecutionContext): class PGCompiler_psycopg2(PGCompiler): - def visit_mod(self, binary, **kw): - return self.process(binary.left) + " %% " + self.process(binary.right) + def visit_mod_binary(self, binary, operator, **kw): + return self.process(binary.left, **kw) + " %% " + \ + self.process(binary.right, **kw) def post_process_text(self, text): return text.replace('%', '%%') @@ -269,11 +313,12 @@ class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer): value = value.replace(self.escape_quote, self.escape_to_quote) return value.replace('%', '%%') + class PGDialect_psycopg2(PGDialect): driver = 'psycopg2' - # Py2K - supports_unicode_statements = False - # end Py2K + if util.py2k: + supports_unicode_statements = False + default_paramstyle = 'pyformat' supports_sane_multi_rowcount = False execution_ctx_cls = PGExecutionContext_psycopg2 @@ -281,21 +326,28 @@ class PGDialect_psycopg2(PGDialect): preparer = PGIdentifierPreparer_psycopg2 psycopg2_version = (0, 0) + _has_native_hstore = False + _has_native_json = False + colspecs = util.update_copy( PGDialect.colspecs, { - sqltypes.Numeric : _PGNumeric, - ENUM : _PGEnum, # needs force_unicode - sqltypes.Enum : _PGEnum, # needs force_unicode - ARRAY : _PGArray, # needs force_unicode + sqltypes.Numeric: _PGNumeric, + ENUM: _PGEnum, # needs force_unicode + sqltypes.Enum: _PGEnum, # needs force_unicode + HSTORE: _PGHStore, + JSON: _PGJSON } ) def __init__(self, server_side_cursors=False, use_native_unicode=True, - client_encoding=None, **kwargs): + client_encoding=None, + use_native_hstore=True, + **kwargs): PGDialect.__init__(self, **kwargs) self.server_side_cursors = server_side_cursors self.use_native_unicode = use_native_unicode + self.use_native_hstore = use_native_hstore self.supports_unicode_binds = use_native_unicode self.client_encoding = client_encoding if self.dbapi and hasattr(self.dbapi, '__version__'): @@ -307,19 +359,27 @@ class PGDialect_psycopg2(PGDialect): for x in m.group(1, 2, 3) if x is not None) + def initialize(self, connection): + super(PGDialect_psycopg2, self).initialize(connection) + self._has_native_hstore = self.use_native_hstore and \ + self._hstore_oids(connection.connection) \ + is not None + self._has_native_json = self.psycopg2_version >= (2, 5) + @classmethod def dbapi(cls): - psycopg = __import__('psycopg2') - return psycopg + import psycopg2 + return psycopg2 @util.memoized_property def _isolation_lookup(self): - extensions = __import__('psycopg2.extensions').extensions + from psycopg2 import extensions return { - 'READ COMMITTED':extensions.ISOLATION_LEVEL_READ_COMMITTED, - 'READ UNCOMMITTED':extensions.ISOLATION_LEVEL_READ_UNCOMMITTED, - 'REPEATABLE READ':extensions.ISOLATION_LEVEL_REPEATABLE_READ, - 'SERIALIZABLE':extensions.ISOLATION_LEVEL_SERIALIZABLE + 'AUTOCOMMIT': extensions.ISOLATION_LEVEL_AUTOCOMMIT, + 'READ COMMITTED': extensions.ISOLATION_LEVEL_READ_COMMITTED, + 'READ UNCOMMITTED': extensions.ISOLATION_LEVEL_READ_UNCOMMITTED, + 'REPEATABLE READ': extensions.ISOLATION_LEVEL_REPEATABLE_READ, + 'SERIALIZABLE': extensions.ISOLATION_LEVEL_SERIALIZABLE } def set_isolation_level(self, connection, level): @@ -335,6 +395,8 @@ class PGDialect_psycopg2(PGDialect): connection.set_isolation_level(level) def on_connect(self): + from psycopg2 import extras, extensions + fns = [] if self.client_encoding is not None: def on_connect(conn): @@ -347,9 +409,28 @@ class PGDialect_psycopg2(PGDialect): fns.append(on_connect) if self.dbapi and self.use_native_unicode: - extensions = __import__('psycopg2.extensions').extensions def on_connect(conn): extensions.register_type(extensions.UNICODE, conn) + extensions.register_type(extensions.UNICODEARRAY, conn) + fns.append(on_connect) + + if self.dbapi and self.use_native_hstore: + def on_connect(conn): + hstore_oids = self._hstore_oids(conn) + if hstore_oids is not None: + oid, array_oid = hstore_oids + if util.py2k: + extras.register_hstore(conn, oid=oid, + array_oid=array_oid, + unicode=True) + else: + extras.register_hstore(conn, oid=oid, + array_oid=array_oid) + fns.append(on_connect) + + if self.dbapi and self._json_deserializer: + def on_connect(conn): + extras.register_default_json(conn, loads=self._json_deserializer) fns.append(on_connect) if fns: @@ -360,6 +441,15 @@ class PGDialect_psycopg2(PGDialect): else: return None + @util.memoized_instancemethod + def _hstore_oids(self, conn): + if self.psycopg2_version >= (2, 4): + from psycopg2 import extras + oids = extras.HstoreAdapter.get_oids(conn) + if oids is not None and oids[0]: + return oids[0:2] + return None + def create_connect_args(self, url): opts = url.translate_connect_args(username='user') if 'port' in opts: @@ -368,24 +458,27 @@ class PGDialect_psycopg2(PGDialect): return ([], opts) def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.OperationalError): - # these error messages from libpq: interfaces/libpq/fe-misc.c. - # TODO: these are sent through gettext in libpq and we can't - # check within other locales - consider using connection.closed - return 'terminating connection' in str(e) or \ - 'closed the connection' in str(e) or \ - 'connection not open' in str(e) or \ - 'could not receive data from server' in str(e) - elif isinstance(e, self.dbapi.InterfaceError): - # psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h - return 'connection already closed' in str(e) or \ - 'cursor already closed' in str(e) - elif isinstance(e, self.dbapi.ProgrammingError): - # not sure where this path is originally from, it may - # be obsolete. It really says "losed", not "closed". - return "losed the connection unexpectedly" in str(e) - else: - return False + if isinstance(e, self.dbapi.Error): + str_e = str(e).partition("\n")[0] + for msg in [ + # these error messages from libpq: interfaces/libpq/fe-misc.c + # and interfaces/libpq/fe-secure.c. + # TODO: these are sent through gettext in libpq and we can't + # check within other locales - consider using connection.closed + 'terminating connection', + 'closed the connection', + 'connection not open', + 'could not receive data from server', + # psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h + 'connection already closed', + 'cursor already closed', + # not sure where this path is originally from, it may + # be obsolete. It really says "losed", not "closed". + 'losed the connection unexpectedly' + ]: + idx = str_e.find(msg) + if idx >= 0 and '"' not in str_e[:idx]: + return True + return False dialect = PGDialect_psycopg2 - diff --git a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py b/libs/sqlalchemy/dialects/postgresql/pypostgresql.py index 5303d047..f030d2c1 100644 --- a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py +++ b/libs/sqlalchemy/dialects/postgresql/pypostgresql.py @@ -1,22 +1,23 @@ # postgresql/pypostgresql.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the PostgreSQL database via py-postgresql. - -Connecting ----------- - -URLs are of the form ``postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...]``. +""" +.. dialect:: postgresql+pypostgresql + :name: py-postgresql + :dbapi: pypostgresql + :connectstring: postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...] + :url: http://python.projects.pgfoundry.org/ """ -from sqlalchemy import util -from sqlalchemy import types as sqltypes -from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext -from sqlalchemy import processors +from ... import util +from ... import types as sqltypes +from .base import PGDialect, PGExecutionContext +from ... import processors + class PGNumeric(sqltypes.Numeric): def bind_processor(self, dialect): @@ -28,9 +29,11 @@ class PGNumeric(sqltypes.Numeric): else: return processors.to_float + class PGExecutionContext_pypostgresql(PGExecutionContext): pass + class PGDialect_pypostgresql(PGDialect): driver = 'pypostgresql' @@ -48,8 +51,10 @@ class PGDialect_pypostgresql(PGDialect): colspecs = util.update_copy( PGDialect.colspecs, { - sqltypes.Numeric : PGNumeric, - sqltypes.Float: sqltypes.Float, # prevents PGNumeric from being used + sqltypes.Numeric: PGNumeric, + + # prevents PGNumeric from being used + sqltypes.Float: sqltypes.Float, } ) diff --git a/libs/sqlalchemy/dialects/postgresql/ranges.py b/libs/sqlalchemy/dialects/postgresql/ranges.py new file mode 100644 index 00000000..57b0c4c3 --- /dev/null +++ b/libs/sqlalchemy/dialects/postgresql/ranges.py @@ -0,0 +1,160 @@ +# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .base import ischema_names +from ... import types as sqltypes + +__all__ = ('INT4RANGE', 'INT8RANGE', 'NUMRANGE') + +class RangeOperators(object): + """ + This mixin provides functionality for the Range Operators + listed in Table 9-44 of the `postgres documentation`__ for Range + Functions and Operators. It is used by all the range types + provided in the ``postgres`` dialect and can likely be used for + any range types you create yourself. + + __ http://www.postgresql.org/docs/devel/static/functions-range.html + + No extra support is provided for the Range Functions listed in + Table 9-45 of the postgres documentation. For these, the normal + :func:`~sqlalchemy.sql.expression.func` object should be used. + + .. versionadded:: 0.8.2 Support for Postgresql RANGE operations. + + """ + + class comparator_factory(sqltypes.Concatenable.Comparator): + """Define comparison operations for range types.""" + + def __ne__(self, other): + "Boolean expression. Returns true if two ranges are not equal" + return self.expr.op('<>')(other) + + def contains(self, other, **kw): + """Boolean expression. Returns true if the right hand operand, + which can be an element or a range, is contained within the + column. + """ + return self.expr.op('@>')(other) + + def contained_by(self, other): + """Boolean expression. Returns true if the column is contained + within the right hand operand. + """ + return self.expr.op('<@')(other) + + def overlaps(self, other): + """Boolean expression. Returns true if the column overlaps + (has points in common with) the right hand operand. + """ + return self.expr.op('&&')(other) + + def strictly_left_of(self, other): + """Boolean expression. Returns true if the column is strictly + left of the right hand operand. + """ + return self.expr.op('<<')(other) + + __lshift__ = strictly_left_of + + def strictly_right_of(self, other): + """Boolean expression. Returns true if the column is strictly + right of the right hand operand. + """ + return self.expr.op('>>')(other) + + __rshift__ = strictly_right_of + + def not_extend_right_of(self, other): + """Boolean expression. Returns true if the range in the column + does not extend right of the range in the operand. + """ + return self.expr.op('&<')(other) + + def not_extend_left_of(self, other): + """Boolean expression. Returns true if the range in the column + does not extend left of the range in the operand. + """ + return self.expr.op('&>')(other) + + def adjacent_to(self, other): + """Boolean expression. Returns true if the range in the column + is adjacent to the range in the operand. + """ + return self.expr.op('-|-')(other) + + def __add__(self, other): + """Range expression. Returns the union of the two ranges. + Will raise an exception if the resulting range is not + contigous. + """ + return self.expr.op('+')(other) + +class INT4RANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql INT4RANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'INT4RANGE' + +ischema_names['int4range'] = INT4RANGE + +class INT8RANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql INT8RANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'INT8RANGE' + +ischema_names['int8range'] = INT8RANGE + +class NUMRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql NUMRANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'NUMRANGE' + +ischema_names['numrange'] = NUMRANGE + +class DATERANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql DATERANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'DATERANGE' + +ischema_names['daterange'] = DATERANGE + +class TSRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql TSRANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'TSRANGE' + +ischema_names['tsrange'] = TSRANGE + +class TSTZRANGE(RangeOperators, sqltypes.TypeEngine): + """Represent the Postgresql TSTZRANGE type. + + .. versionadded:: 0.8.2 + + """ + + __visit_name__ = 'TSTZRANGE' + +ischema_names['tstzrange'] = TSTZRANGE diff --git a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py b/libs/sqlalchemy/dialects/postgresql/zxjdbc.py index 4aea9c9b..67e7d53e 100644 --- a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py +++ b/libs/sqlalchemy/dialects/postgresql/zxjdbc.py @@ -1,19 +1,21 @@ # postgresql/zxjdbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the PostgreSQL database via the zxjdbc JDBC connector. +""" +.. dialect:: postgresql+zxjdbc + :name: zxJDBC for Jython + :dbapi: zxjdbc + :connectstring: postgresql+zxjdbc://scott:tiger@localhost/db + :driverurl: http://jdbc.postgresql.org/ -JDBC Driver ------------ - -The official Postgresql JDBC driver is at http://jdbc.postgresql.org/. """ -from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector -from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext +from ...connectors.zxJDBC import ZxJDBCConnector +from .base import PGDialect, PGExecutionContext + class PGExecutionContext_zxjdbc(PGExecutionContext): @@ -37,6 +39,7 @@ class PGDialect_zxjdbc(ZxJDBCConnector, PGDialect): self.DataHandler = PostgresqlDataHandler def _get_server_version_info(self, connection): - return tuple(int(x) for x in connection.connection.dbversion.split('.')) + parts = connection.connection.dbversion.split('.') + return tuple(int(x) for x in parts) dialect = PGDialect_zxjdbc diff --git a/libs/sqlalchemy/dialects/sqlite/__init__.py b/libs/sqlalchemy/dialects/sqlite/__init__.py index c1157b63..a9b23575 100644 --- a/libs/sqlalchemy/dialects/sqlite/__init__.py +++ b/libs/sqlalchemy/dialects/sqlite/__init__.py @@ -1,5 +1,5 @@ # sqlite/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -15,6 +15,7 @@ from sqlalchemy.dialects.sqlite.base import \ NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect __all__ = ( - 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', 'INTEGER', - 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', 'dialect', 'REAL' -) \ No newline at end of file + 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', + 'INTEGER', 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', + 'REAL', 'dialect' +) diff --git a/libs/sqlalchemy/dialects/sqlite/base.py b/libs/sqlalchemy/dialects/sqlite/base.py index 9118ace2..ac644f8d 100644 --- a/libs/sqlalchemy/dialects/sqlite/base.py +++ b/libs/sqlalchemy/dialects/sqlite/base.py @@ -1,25 +1,26 @@ # sqlite/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the SQLite database. +""" +.. dialect:: sqlite + :name: SQLite -For information on connecting using a specific driver, see the documentation -section regarding that driver. Date and Time Types ------------------- -SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite does not provide -out of the box functionality for translating values between Python `datetime` objects -and a SQLite-supported format. SQLAlchemy's own :class:`~sqlalchemy.types.DateTime` -and related types provide date formatting and parsing functionality when SQlite is used. -The implementation classes are :class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`. -These types represent dates and times as ISO formatted strings, which also nicely -support ordering. There's no reliance on typical "libc" internals for these functions -so historical dates are fully supported. +SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite +does not provide out of the box functionality for translating values between +Python `datetime` objects and a SQLite-supported format. SQLAlchemy's own +:class:`~sqlalchemy.types.DateTime` and related types provide date formatting +and parsing functionality when SQlite is used. The implementation classes are +:class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`. +These types represent dates and times as ISO formatted strings, which also +nicely support ordering. There's no reliance on typical "libc" internals +for these functions so historical dates are fully supported. Auto Incrementing Behavior -------------------------- @@ -46,44 +47,47 @@ to the Table construct:: Transaction Isolation Level --------------------------- -:func:`.create_engine` accepts an ``isolation_level`` parameter which results in -the command ``PRAGMA read_uncommitted `` being invoked for every new -connection. Valid values for this parameter are ``SERIALIZABLE`` and -``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively. +:func:`.create_engine` accepts an ``isolation_level`` parameter which +results in the command ``PRAGMA read_uncommitted `` being invoked for +every new connection. Valid values for this parameter are ``SERIALIZABLE`` +and ``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively. See the section :ref:`pysqlite_serializable` for an important workaround when using serializable isolation with Pysqlite. Database Locking Behavior / Concurrency --------------------------------------- -Note that SQLite is not designed for a high level of concurrency. The database -itself, being a file, is locked completely during write operations and within -transactions, meaning exactly one connection has exclusive access to the database -during this period - all other connections will be blocked during this time. +Note that SQLite is not designed for a high level of concurrency. The +database itself, being a file, is locked completely during write operations +and within transactions, meaning exactly one connection has exclusive access +to the database during this period - all other connections will be blocked +during this time. The Python DBAPI specification also calls for a connection model that is always -in a transaction; there is no BEGIN method, only commit and rollback. This implies -that a SQLite DBAPI driver would technically allow only serialized access to a -particular database file at all times. The pysqlite driver attempts to ameliorate this by -deferring the actual BEGIN statement until the first DML (INSERT, UPDATE, or -DELETE) is received within a transaction. While this breaks serializable isolation, -it at least delays the exclusive locking inherent in SQLite's design. +in a transaction; there is no BEGIN method, only commit and rollback. This +implies that a SQLite DBAPI driver would technically allow only serialized +access to a particular database file at all times. The pysqlite driver +attempts to ameliorate this by deferring the actual BEGIN statement until +the first DML (INSERT, UPDATE, or DELETE) is received within a +transaction. While this breaks serializable isolation, it at least delays +the exclusive locking inherent in SQLite's design. SQLAlchemy's default mode of usage with the ORM is known -as "autocommit=False", which means the moment the :class:`.Session` begins to be -used, a transaction is begun. As the :class:`.Session` is used, the autoflush -feature, also on by default, will flush out pending changes to the database -before each query. The effect of this is that a :class:`.Session` used in its -default mode will often emit DML early on, long before the transaction is actually -committed. This again will have the effect of serializing access to the SQLite -database. If highly concurrent reads are desired against the SQLite database, -it is advised that the autoflush feature be disabled, and potentially even -that autocommit be re-enabled, which has the effect of each SQL statement and -flush committing changes immediately. +as "autocommit=False", which means the moment the :class:`.Session` begins to +be used, a transaction is begun. As the :class:`.Session` is used, the +autoflush feature, also on by default, will flush out pending changes to the +database before each query. The effect of this is that a :class:`.Session` +used in its default mode will often emit DML early on, long before the +transaction is actually committed. This again will have the effect of +serializing access to the SQLite database. If highly concurrent reads are +desired against the SQLite database, it is advised that the autoflush feature +be disabled, and potentially even that autocommit be re-enabled, which has +the effect of each SQL statement and flush committing changes immediately. For more information on SQLite's lack of concurrency by design, please -see `Situations Where Another RDBMS May Work Better - High Concurrency `_ -near the bottom of the page. +see `Situations Where Another RDBMS May Work Better - High +Concurrency `_ near the bottom of +the page. .. _sqlite_foreign_keys: @@ -123,7 +127,8 @@ for new connections through the usage of events:: """ -import datetime, re +import datetime +import re from sqlalchemy import sql, exc from sqlalchemy.engine import default, base, reflection @@ -132,8 +137,10 @@ from sqlalchemy import util from sqlalchemy.sql import compiler from sqlalchemy import processors -from sqlalchemy.types import BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL,\ - FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR +from sqlalchemy.types import BIGINT, BLOB, BOOLEAN, CHAR,\ + DECIMAL, FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT,\ + TIMESTAMP, VARCHAR + class _DateTimeMixin(object): _reg = None @@ -146,15 +153,26 @@ class _DateTimeMixin(object): if storage_format is not None: self._storage_format = storage_format + def adapt(self, cls, **kw): + if self._storage_format: + kw["storage_format"] = self._storage_format + if self._reg: + kw["regexp"] = self._reg + return util.constructor_copy(self, cls, **kw) + + def literal_processor(self, dialect): + bp = self.bind_processor(dialect) + def process(value): + return "'%s'" % bp(value) + return process + + class DATETIME(_DateTimeMixin, sqltypes.DateTime): """Represent a Python datetime object in SQLite using a string. The default string storage format is:: - "%04d-%02d-%02d %02d:%02d:%02d.%06d" % (value.year, - value.month, value.day, - value.hour, value.minute, - value.second, value.microsecond) + "%(year)04d-%(month)02d-%(day)02d %(hour)02d:%(min)02d:%(second)02d.%(microsecond)06d" e.g.:: @@ -167,37 +185,68 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime): from sqlalchemy.dialects.sqlite import DATETIME dt = DATETIME( - storage_format="%04d/%02d/%02d %02d-%02d-%02d-%06d", - regexp=re.compile("(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)(?:-(\d+))?") - ) + storage_format="%(year)04d/%(month)02d/%(day)02d %(hour)02d:%(min)02d:%(second)02d", + regexp=r"(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)" + ) :param storage_format: format string which will be applied to the - tuple ``(value.year, value.month, value.day, value.hour, - value.minute, value.second, value.microsecond)``, given a - Python datetime.datetime() object. + dict with keys year, month, day, hour, minute, second, and microsecond. :param regexp: regular expression which will be applied to - incoming result rows. The resulting match object is applied to - the Python datetime() constructor via ``*map(int, - match_obj.groups(0))``. + incoming result rows. If the regexp contains named groups, the + resulting match dict is applied to the Python datetime() constructor + as keyword arguments. Otherwise, if positional groups are used, the + the datetime() constructor is called with positional arguments via + ``*map(int, match_obj.groups(0))``. """ - _storage_format = "%04d-%02d-%02d %02d:%02d:%02d.%06d" + _storage_format = ( + "%(year)04d-%(month)02d-%(day)02d " + "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" + ) + + def __init__(self, *args, **kwargs): + truncate_microseconds = kwargs.pop('truncate_microseconds', False) + super(DATETIME, self).__init__(*args, **kwargs) + if truncate_microseconds: + assert 'storage_format' not in kwargs, "You can specify only "\ + "one of truncate_microseconds or storage_format." + assert 'regexp' not in kwargs, "You can specify only one of "\ + "truncate_microseconds or regexp." + self._storage_format = ( + "%(year)04d-%(month)02d-%(day)02d " + "%(hour)02d:%(minute)02d:%(second)02d" + ) + def bind_processor(self, dialect): datetime_datetime = datetime.datetime datetime_date = datetime.date format = self._storage_format + def process(value): if value is None: return None elif isinstance(value, datetime_datetime): - return format % (value.year, value.month, value.day, - value.hour, value.minute, value.second, - value.microsecond) + return format % { + 'year': value.year, + 'month': value.month, + 'day': value.day, + 'hour': value.hour, + 'minute': value.minute, + 'second': value.second, + 'microsecond': value.microsecond, + } elif isinstance(value, datetime_date): - return format % (value.year, value.month, value.day, - 0, 0, 0, 0) + return format % { + 'year': value.year, + 'month': value.month, + 'day': value.day, + 'hour': 0, + 'minute': 0, + 'second': 0, + 'microsecond': 0, + } else: raise TypeError("SQLite DateTime type only accepts Python " "datetime and date objects as input.") @@ -210,12 +259,13 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime): else: return processors.str_to_datetime + class DATE(_DateTimeMixin, sqltypes.Date): """Represent a Python date object in SQLite using a string. The default string storage format is:: - "%04d-%02d-%02d" % (value.year, value.month, value.day) + "%(year)04d-%(month)02d-%(day)02d" e.g.:: @@ -228,31 +278,36 @@ class DATE(_DateTimeMixin, sqltypes.Date): from sqlalchemy.dialects.sqlite import DATE d = DATE( - storage_format="%02d/%02d/%02d", - regexp=re.compile("(\d+)/(\d+)/(\d+)") + storage_format="%(month)02d/%(day)02d/%(year)04d", + regexp=re.compile("(?P\d+)/(?P\d+)/(?P\d+)") ) :param storage_format: format string which will be applied to the - tuple ``(value.year, value.month, value.day)``, - given a Python datetime.date() object. + dict with keys year, month, and day. :param regexp: regular expression which will be applied to - incoming result rows. The resulting match object is applied to - the Python date() constructor via ``*map(int, - match_obj.groups(0))``. - + incoming result rows. If the regexp contains named groups, the + resulting match dict is applied to the Python date() constructor + as keyword arguments. Otherwise, if positional groups are used, the + the date() constructor is called with positional arguments via + ``*map(int, match_obj.groups(0))``. """ - _storage_format = "%04d-%02d-%02d" + _storage_format = "%(year)04d-%(month)02d-%(day)02d" def bind_processor(self, dialect): datetime_date = datetime.date format = self._storage_format + def process(value): if value is None: return None elif isinstance(value, datetime_date): - return format % (value.year, value.month, value.day) + return format % { + 'year': value.year, + 'month': value.month, + 'day': value.day, + } else: raise TypeError("SQLite Date type only accepts Python " "date objects as input.") @@ -265,14 +320,13 @@ class DATE(_DateTimeMixin, sqltypes.Date): else: return processors.str_to_date + class TIME(_DateTimeMixin, sqltypes.Time): """Represent a Python time object in SQLite using a string. The default string storage format is:: - "%02d:%02d:%02d.%06d" % (value.hour, value.minute, - value.second, - value.microsecond) + "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" e.g.:: @@ -285,32 +339,47 @@ class TIME(_DateTimeMixin, sqltypes.Time): from sqlalchemy.dialects.sqlite import TIME t = TIME( - storage_format="%02d-%02d-%02d-%06d", - regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?") - ) + storage_format="%(hour)02d-%(minute)02d-%(second)02d-%(microsecond)06d", + regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?") + ) - :param storage_format: format string which will be applied - to the tuple ``(value.hour, value.minute, value.second, - value.microsecond)``, given a Python datetime.time() object. + :param storage_format: format string which will be applied to the + dict with keys hour, minute, second, and microsecond. :param regexp: regular expression which will be applied to - incoming result rows. The resulting match object is applied to - the Python time() constructor via ``*map(int, - match_obj.groups(0))``. - + incoming result rows. If the regexp contains named groups, the + resulting match dict is applied to the Python time() constructor + as keyword arguments. Otherwise, if positional groups are used, the + the time() constructor is called with positional arguments via + ``*map(int, match_obj.groups(0))``. """ - _storage_format = "%02d:%02d:%02d.%06d" + _storage_format = "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" + + def __init__(self, *args, **kwargs): + truncate_microseconds = kwargs.pop('truncate_microseconds', False) + super(TIME, self).__init__(*args, **kwargs) + if truncate_microseconds: + assert 'storage_format' not in kwargs, "You can specify only "\ + "one of truncate_microseconds or storage_format." + assert 'regexp' not in kwargs, "You can specify only one of "\ + "truncate_microseconds or regexp." + self._storage_format = "%(hour)02d:%(minute)02d:%(second)02d" def bind_processor(self, dialect): datetime_time = datetime.time format = self._storage_format + def process(value): if value is None: return None elif isinstance(value, datetime_time): - return format % (value.hour, value.minute, value.second, - value.microsecond) + return format % { + 'hour': value.hour, + 'minute': value.minute, + 'second': value.second, + 'microsecond': value.microsecond, + } else: raise TypeError("SQLite Time type only accepts Python " "time objects as input.") @@ -330,6 +399,7 @@ colspecs = { } ischema_names = { + 'BIGINT': sqltypes.BIGINT, 'BLOB': sqltypes.BLOB, 'BOOL': sqltypes.BOOLEAN, 'BOOLEAN': sqltypes.BOOLEAN, @@ -347,10 +417,11 @@ ischema_names = { 'TIME': sqltypes.TIME, 'TIMESTAMP': sqltypes.TIMESTAMP, 'VARCHAR': sqltypes.VARCHAR, + 'NVARCHAR': sqltypes.NVARCHAR, + 'NCHAR': sqltypes.NCHAR, } - class SQLiteCompiler(compiler.SQLCompiler): extract_map = util.update_copy( compiler.SQLCompiler.extract_map, @@ -391,7 +462,9 @@ class SQLiteCompiler(compiler.SQLCompiler): def visit_extract(self, extract, **kw): try: return "CAST(STRFTIME('%s', %s) AS INTEGER)" % ( - self.extract_map[extract.field], self.process(extract.expr, **kw)) + self.extract_map[extract.field], + self.process(extract.expr, **kw) + ) except KeyError: raise exc.CompileError( "%s is not a valid extract argument." % extract.field) @@ -399,7 +472,7 @@ class SQLiteCompiler(compiler.SQLCompiler): def limit_clause(self, select): text = "" if select._limit is not None: - text += "\n LIMIT " + self.process(sql.literal(select._limit)) + text += "\n LIMIT " + self.process(sql.literal(select._limit)) if select._offset is not None: if select._limit is None: text += "\n LIMIT " + self.process(sql.literal(-1)) @@ -416,7 +489,8 @@ class SQLiteCompiler(compiler.SQLCompiler): class SQLiteDDLCompiler(compiler.DDLCompiler): def get_column_specification(self, column, **kwargs): - colspec = self.preparer.format_column(column) + " " + self.dialect.type_compiler.process(column.type) + coltype = self.dialect.type_compiler.process(column.type) + colspec = self.preparer.format_column(column) + " " + coltype default = self.get_column_default_string(column) if default is not None: colspec += " DEFAULT " + default @@ -424,12 +498,12 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): if not column.nullable: colspec += " NOT NULL" - if column.primary_key and \ - column.table.kwargs.get('sqlite_autoincrement', False) and \ - len(column.table.primary_key.columns) == 1 and \ - issubclass(column.type._type_affinity, sqltypes.Integer) and \ - not column.foreign_keys: - colspec += " PRIMARY KEY AUTOINCREMENT" + if (column.primary_key and + column.table.kwargs.get('sqlite_autoincrement', False) and + len(column.table.primary_key.columns) == 1 and + issubclass(column.type._type_affinity, sqltypes.Integer) and + not column.foreign_keys): + colspec += " PRIMARY KEY AUTOINCREMENT" return colspec @@ -450,7 +524,7 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): def visit_foreign_key_constraint(self, constraint): - local_table = constraint._elements.values()[0].parent.table + local_table = list(constraint._elements.values())[0].parent.table remote_table = list(constraint._elements.values())[0].column.table if local_table.schema != remote_table.schema: @@ -464,23 +538,15 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): return preparer.format_table(table, use_schema=False) def visit_create_index(self, create): - index = create.element - preparer = self.preparer - text = "CREATE " - if index.unique: - text += "UNIQUE " - text += "INDEX %s ON %s (%s)" \ - % (preparer.format_index(index, - name=self._index_identifier(index.name)), - preparer.format_table(index.table, use_schema=False), - ', '.join(preparer.quote(c.name, c.quote) - for c in index.columns)) - return text + return super(SQLiteDDLCompiler, self).\ + visit_create_index(create, include_table_schema=False) + class SQLiteTypeCompiler(compiler.GenericTypeCompiler): def visit_large_binary(self, type_): return self.visit_BLOB(type_) + class SQLiteIdentifierPreparer(compiler.IdentifierPreparer): reserved_words = set([ 'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc', @@ -492,14 +558,15 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer): 'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive', 'explain', 'false', 'fail', 'for', 'foreign', 'from', 'full', 'glob', 'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index', - 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', 'into', 'is', - 'isnull', 'join', 'key', 'left', 'like', 'limit', 'match', 'natural', - 'not', 'notnull', 'null', 'of', 'offset', 'on', 'or', 'order', 'outer', - 'plan', 'pragma', 'primary', 'query', 'raise', 'references', - 'reindex', 'rename', 'replace', 'restrict', 'right', 'rollback', - 'row', 'select', 'set', 'table', 'temp', 'temporary', 'then', 'to', - 'transaction', 'trigger', 'true', 'union', 'unique', 'update', 'using', - 'vacuum', 'values', 'view', 'virtual', 'when', 'where', + 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', + 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit', + 'match', 'natural', 'not', 'notnull', 'null', 'of', 'offset', 'on', + 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query', + 'raise', 'references', 'reindex', 'rename', 'replace', 'restrict', + 'right', 'rollback', 'row', 'select', 'set', 'table', 'temp', + 'temporary', 'then', 'to', 'transaction', 'trigger', 'true', 'union', + 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual', + 'when', 'where', ]) def format_index(self, index, use_schema=True, name=None): @@ -508,10 +575,14 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer): if name is None: name = index.name result = self.quote(name, index.quote) - if not self.omit_schema and use_schema and getattr(index.table, "schema", None): - result = self.quote_schema(index.table.schema, index.table.quote_schema) + "." + result + if (not self.omit_schema and + use_schema and + getattr(index.table, "schema", None)): + result = self.quote_schema( + index.table.schema, index.table.quote_schema) + "." + result return result + class SQLiteExecutionContext(default.DefaultExecutionContext): @util.memoized_property def _preserve_raw_colnames(self): @@ -536,6 +607,8 @@ class SQLiteDialect(default.DefaultDialect): supports_default_values = True supports_empty_insert = False supports_cast = True + supports_multivalues_insert = True + supports_right_nested_joins = False default_paramstyle = 'qmark' execution_ctx_cls = SQLiteExecutionContext @@ -567,6 +640,9 @@ class SQLiteDialect(default.DefaultDialect): self.dbapi.sqlite_version_info >= (3, 3, 8) self.supports_cast = \ self.dbapi.sqlite_version_info >= (3, 2, 3) + self.supports_multivalues_insert = \ + self.dbapi.sqlite_version_info >= (3, 7, 11) + # http://www.sqlite.org/releaselog/3_7_11.html # see http://www.sqlalchemy.org/trac/ticket/2568 # as well as http://www.sqlite.org/src/info/600482d161 @@ -575,9 +651,10 @@ class SQLiteDialect(default.DefaultDialect): _isolation_lookup = { - 'READ UNCOMMITTED':1, - 'SERIALIZABLE':0 + 'READ UNCOMMITTED': 1, + 'SERIALIZABLE': 0 } + def set_isolation_level(self, connection, level): try: isolation_level = self._isolation_lookup[level.replace('_', ' ')] @@ -650,7 +727,8 @@ class SQLiteDialect(default.DefaultDialect): else: pragma = "PRAGMA " qtable = quote(table_name) - cursor = _pragma_cursor(connection.execute("%stable_info(%s)" % (pragma, qtable))) + statement = "%stable_info(%s)" % (pragma, qtable) + cursor = _pragma_cursor(connection.execute(statement)) row = cursor.fetchone() # consume remaining rows, to work around @@ -716,9 +794,8 @@ class SQLiteDialect(default.DefaultDialect): else: pragma = "PRAGMA " qtable = quote(table_name) - c = _pragma_cursor( - connection.execute("%stable_info(%s)" % - (pragma, qtable))) + statement = "%stable_info(%s)" % (pragma, qtable) + c = _pragma_cursor(connection.execute(statement)) rows = c.fetchall() columns = [] @@ -752,7 +829,7 @@ class SQLiteDialect(default.DefaultDialect): coltype = sqltypes.NullType() if default is not None: - default = unicode(default) + default = util.text_type(default) return { 'name': name, @@ -764,13 +841,13 @@ class SQLiteDialect(default.DefaultDialect): } @reflection.cache - def get_primary_keys(self, connection, table_name, schema=None, **kw): + def get_pk_constraint(self, connection, table_name, schema=None, **kw): cols = self.get_columns(connection, table_name, schema, **kw) pkeys = [] for col in cols: if col['primary_key']: pkeys.append(col['name']) - return pkeys + return {'constrained_columns': pkeys, 'name': None} @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): @@ -830,7 +907,8 @@ class SQLiteDialect(default.DefaultDialect): pragma = "PRAGMA " include_auto_indexes = kw.pop('include_auto_indexes', False) qtable = quote(table_name) - c = _pragma_cursor(connection.execute("%sindex_list(%s)" % (pragma, qtable))) + statement = "%sindex_list(%s)" % (pragma, qtable) + c = _pragma_cursor(connection.execute(statement)) indexes = [] while True: row = c.fetchone() @@ -838,13 +916,15 @@ class SQLiteDialect(default.DefaultDialect): break # ignore implicit primary key index. # http://www.mail-archive.com/sqlite-users@sqlite.org/msg30517.html - elif not include_auto_indexes and row[1].startswith('sqlite_autoindex'): + elif (not include_auto_indexes and + row[1].startswith('sqlite_autoindex')): continue indexes.append(dict(name=row[1], column_names=[], unique=row[2])) # loop thru unique indexes to get the column names. for idx in indexes: - c = connection.execute("%sindex_info(%s)" % (pragma, quote(idx['name']))) + statement = "%sindex_info(%s)" % (pragma, quote(idx['name'])) + c = connection.execute(statement) cols = idx['column_names'] while True: row = c.fetchone() @@ -853,6 +933,27 @@ class SQLiteDialect(default.DefaultDialect): cols.append(row[2]) return indexes + @reflection.cache + def get_unique_constraints(self, connection, table_name, + schema=None, **kw): + UNIQUE_SQL = """ + SELECT sql + FROM + sqlite_master + WHERE + type='table' AND + name=:table_name + """ + c = connection.execute(UNIQUE_SQL, table_name=table_name) + table_data = c.fetchone()[0] + + UNIQUE_PATTERN = 'CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)' + return [ + {'name': name, + 'column_names': [col.strip(' "') for col in cols.split(',')]} + for name, cols in re.findall(UNIQUE_PATTERN, table_data) + ] + def _pragma_cursor(cursor): """work around SQLite issue whereby cursor.description diff --git a/libs/sqlalchemy/dialects/sqlite/pysqlite.py b/libs/sqlalchemy/dialects/sqlite/pysqlite.py index 826eefd8..b53f4d4a 100644 --- a/libs/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/libs/sqlalchemy/dialects/sqlite/pysqlite.py @@ -1,13 +1,18 @@ # sqlite/pysqlite.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for the SQLite database via pysqlite. +""" +.. dialect:: sqlite+pysqlite + :name: pysqlite + :dbapi: sqlite3 + :connectstring: sqlite+pysqlite:///file_path + :url: http://docs.python.org/library/sqlite3.html -Note that pysqlite is the same driver as the ``sqlite3`` -module included with the Python distribution. + Note that ``pysqlite`` is the same driver as the ``sqlite3`` + module included with the Python distribution. Driver ------ @@ -26,37 +31,36 @@ this explicitly:: from sqlite3 import dbapi2 as sqlite e = create_engine('sqlite+pysqlite:///file.db', module=sqlite) -Full documentation on pysqlite is available at: -``_ Connect Strings --------------- -The file specification for the SQLite database is taken as the "database" portion of -the URL. Note that the format of a url is:: +The file specification for the SQLite database is taken as the "database" +portion of the URL. Note that the format of a SQLAlchemy url is:: driver://user:pass@host/database -This means that the actual filename to be used starts with the characters to the -**right** of the third slash. So connecting to a relative filepath looks like:: +This means that the actual filename to be used starts with the characters to +the **right** of the third slash. So connecting to a relative filepath +looks like:: # relative path e = create_engine('sqlite:///path/to/database.db') -An absolute path, which is denoted by starting with a slash, means you need **four** -slashes:: +An absolute path, which is denoted by starting with a slash, means you +need **four** slashes:: # absolute path e = create_engine('sqlite:////path/to/database.db') -To use a Windows path, regular drive specifications and backslashes can be used. -Double backslashes are probably needed:: +To use a Windows path, regular drive specifications and backslashes can be +used. Double backslashes are probably needed:: # absolute path on Windows e = create_engine('sqlite:///C:\\\\path\\\\to\\\\database.db') -The sqlite ``:memory:`` identifier is the default if no filepath is present. Specify -``sqlite://`` and nothing else:: +The sqlite ``:memory:`` identifier is the default if no filepath is +present. Specify ``sqlite://`` and nothing else:: # in-memory database e = create_engine('sqlite://') @@ -83,22 +87,24 @@ nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES can be forced if one configures "native_datetime=True" on create_engine():: engine = create_engine('sqlite://', - connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES}, - native_datetime=True - ) + connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES}, + native_datetime=True + ) -With this flag enabled, the DATE and TIMESTAMP types (but note - not the DATETIME -or TIME types...confused yet ?) will not perform any bind parameter or result -processing. Execution of "func.current_date()" will return a string. +With this flag enabled, the DATE and TIMESTAMP types (but note - not the +DATETIME or TIME types...confused yet ?) will not perform any bind parameter +or result processing. Execution of "func.current_date()" will return a string. "func.current_timestamp()" is registered as returning a DATETIME type in SQLAlchemy, so this function still receives SQLAlchemy-level result processing. +.. _pysqlite_threading_pooling: + Threading/Pooling Behavior --------------------------- Pysqlite's default behavior is to prohibit the usage of a single connection -in more than one thread. This is originally intended to work with older versions -of SQLite that did not support multithreaded operation under +in more than one thread. This is originally intended to work with older +versions of SQLite that did not support multithreaded operation under various circumstances. In particular, older SQLite versions did not allow a ``:memory:`` database to be used in multiple threads under any circumstances. @@ -114,17 +120,17 @@ thread-safety to make this usage worth it. SQLAlchemy sets up pooling to work with Pysqlite's default behavior: -* When a ``:memory:`` SQLite database is specified, the dialect by default will use - :class:`.SingletonThreadPool`. This pool maintains a single connection per - thread, so that all access to the engine within the current thread use the - same ``:memory:`` database - other threads would access a different - ``:memory:`` database. -* When a file-based database is specified, the dialect will use :class:`.NullPool` - as the source of connections. This pool closes and discards connections - which are returned to the pool immediately. SQLite file-based connections - have extremely low overhead, so pooling is not necessary. The scheme also - prevents a connection from being used again in a different thread and works - best with SQLite's coarse-grained file locking. +* When a ``:memory:`` SQLite database is specified, the dialect by default + will use :class:`.SingletonThreadPool`. This pool maintains a single + connection per thread, so that all access to the engine within the current + thread use the same ``:memory:`` database - other threads would access a + different ``:memory:`` database. +* When a file-based database is specified, the dialect will use + :class:`.NullPool` as the source of connections. This pool closes and + discards connections which are returned to the pool immediately. SQLite + file-based connections have extremely low overhead, so pooling is not + necessary. The scheme also prevents a connection from being used again in + a different thread and works best with SQLite's coarse-grained file locking. .. versionchanged:: 0.7 Default selection of :class:`.NullPool` for SQLite file-based databases. @@ -137,9 +143,10 @@ Using a Memory Database in Multiple Threads To use a ``:memory:`` database in a multithreaded scenario, the same connection object must be shared among threads, since the database exists -only within the scope of that connection. The :class:`.StaticPool` implementation -will maintain a single connection globally, and the ``check_same_thread`` flag -can be passed to Pysqlite as ``False``:: +only within the scope of that connection. The +:class:`.StaticPool` implementation will maintain a single connection +globally, and the ``check_same_thread`` flag can be passed to Pysqlite +as ``False``:: from sqlalchemy.pool import StaticPool engine = create_engine('sqlite://', @@ -152,13 +159,14 @@ version of SQLite. Using Temporary Tables with SQLite ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Due to the way SQLite deals with temporary tables, if you wish to use a temporary table -in a file-based SQLite database across multiple checkouts from the connection pool, such -as when using an ORM :class:`.Session` where the temporary table should continue to remain -after :meth:`.commit` or :meth:`.rollback` is called, -a pool which maintains a single connection must be used. Use :class:`.SingletonThreadPool` -if the scope is only needed within the current thread, or :class:`.StaticPool` is scope is -needed within multiple threads for this case:: +Due to the way SQLite deals with temporary tables, if you wish to use a +temporary table in a file-based SQLite database across multiple checkouts +from the connection pool, such as when using an ORM :class:`.Session` where +the temporary table should continue to remain after :meth:`.Session.commit` or +:meth:`.Session.rollback` is called, a pool which maintains a single connection must +be used. Use :class:`.SingletonThreadPool` if the scope is only needed +within the current thread, or :class:`.StaticPool` is scope is needed within +multiple threads for this case:: # maintain the same connection per thread from sqlalchemy.pool import SingletonThreadPool @@ -171,17 +179,17 @@ needed within multiple threads for this case:: engine = create_engine('sqlite:///mydb.db', poolclass=StaticPool) -Note that :class:`.SingletonThreadPool` should be configured for the number of threads -that are to be used; beyond that number, connections will be closed out in a non deterministic -way. +Note that :class:`.SingletonThreadPool` should be configured for the number +of threads that are to be used; beyond that number, connections will be +closed out in a non deterministic way. Unicode ------- -The pysqlite driver only returns Python ``unicode`` objects in result sets, never -plain strings, and accommodates ``unicode`` objects within bound parameter -values in all cases. Regardless of the SQLAlchemy string type in use, -string-based result values will by Python ``unicode`` in Python 2. +The pysqlite driver only returns Python ``unicode`` objects in result sets, +never plain strings, and accommodates ``unicode`` objects within bound +parameter values in all cases. Regardless of the SQLAlchemy string type in +use, string-based result values will by Python ``unicode`` in Python 2. The :class:`.Unicode` type should still be used to indicate those columns that require unicode, however, so that non-``unicode`` values passed inadvertently will emit a warning. Pysqlite will emit an error if a non-``unicode`` string @@ -221,6 +229,7 @@ from sqlalchemy import util import os + class _SQLite_pysqliteTimeStamp(DATETIME): def bind_processor(self, dialect): if dialect.native_datetime: @@ -234,6 +243,7 @@ class _SQLite_pysqliteTimeStamp(DATETIME): else: return DATETIME.result_processor(self, dialect, coltype) + class _SQLite_pysqliteDate(DATE): def bind_processor(self, dialect): if dialect.native_datetime: @@ -247,19 +257,20 @@ class _SQLite_pysqliteDate(DATE): else: return DATE.result_processor(self, dialect, coltype) + class SQLiteDialect_pysqlite(SQLiteDialect): default_paramstyle = 'qmark' colspecs = util.update_copy( SQLiteDialect.colspecs, { - sqltypes.Date:_SQLite_pysqliteDate, - sqltypes.TIMESTAMP:_SQLite_pysqliteTimeStamp, + sqltypes.Date: _SQLite_pysqliteDate, + sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp, } ) - # Py3K - #description_encoding = None + if not util.py2k: + description_encoding = None driver = 'pysqlite' @@ -279,9 +290,9 @@ class SQLiteDialect_pysqlite(SQLiteDialect): def dbapi(cls): try: from pysqlite2 import dbapi2 as sqlite - except ImportError, e: + except ImportError as e: try: - from sqlite3 import dbapi2 as sqlite #try the 2.5+ stdlib name. + from sqlite3 import dbapi2 as sqlite # try 2.5+ stdlib name. except ImportError: raise e return sqlite diff --git a/libs/sqlalchemy/dialects/sybase/__init__.py b/libs/sqlalchemy/dialects/sybase/__init__.py index 528ebf23..85f9dd9c 100644 --- a/libs/sqlalchemy/dialects/sybase/__init__.py +++ b/libs/sqlalchemy/dialects/sybase/__init__.py @@ -1,26 +1,27 @@ # sybase/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php from sqlalchemy.dialects.sybase import base, pysybase, pyodbc - -from base import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\ - TEXT,DATE,DATETIME, FLOAT, NUMERIC,\ - BIGINT,INT, INTEGER, SMALLINT, BINARY,\ - VARBINARY,UNITEXT,UNICHAR,UNIVARCHAR,\ - IMAGE,BIT,MONEY,SMALLMONEY,TINYINT - # default dialect base.dialect = pyodbc.dialect +from .base import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\ + TEXT, DATE, DATETIME, FLOAT, NUMERIC,\ + BIGINT, INT, INTEGER, SMALLINT, BINARY,\ + VARBINARY, UNITEXT, UNICHAR, UNIVARCHAR,\ + IMAGE, BIT, MONEY, SMALLMONEY, TINYINT,\ + dialect + + __all__ = ( - 'CHAR', 'VARCHAR', 'TIME', 'NCHAR', 'NVARCHAR', - 'TEXT','DATE','DATETIME', 'FLOAT', 'NUMERIC', - 'BIGINT','INT', 'INTEGER', 'SMALLINT', 'BINARY', - 'VARBINARY','UNITEXT','UNICHAR','UNIVARCHAR', - 'IMAGE','BIT','MONEY','SMALLMONEY','TINYINT', - 'dialect' + 'CHAR', 'VARCHAR', 'TIME', 'NCHAR', 'NVARCHAR', + 'TEXT', 'DATE', 'DATETIME', 'FLOAT', 'NUMERIC', + 'BIGINT', 'INT', 'INTEGER', 'SMALLINT', 'BINARY', + 'VARBINARY', 'UNITEXT', 'UNICHAR', 'UNIVARCHAR', + 'IMAGE', 'BIT', 'MONEY', 'SMALLMONEY', 'TINYINT', + 'dialect' ) diff --git a/libs/sqlalchemy/dialects/sybase/base.py b/libs/sqlalchemy/dialects/sybase/base.py index f551bff9..2f58aed9 100644 --- a/libs/sqlalchemy/dialects/sybase/base.py +++ b/libs/sqlalchemy/dialects/sybase/base.py @@ -1,5 +1,5 @@ # sybase/base.py -# Copyright (C) 2010-2011 the SQLAlchemy authors and contributors +# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors # get_select_precolumns(), limit_clause() implementation # copyright (C) 2007 Fisch Asset Management # AG http://www.fam.ch, with coding by Alexander Houben @@ -8,18 +8,21 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Support for Sybase Adaptive Server Enterprise (ASE). +""" + +.. dialect:: sybase + :name: Sybase .. note:: The Sybase dialect functions on current SQLAlchemy versions but is not regularly tested, and may have many issues and - caveats not currently handled. In particular, the table - and database reflection features are not implemented. + caveats not currently handled. """ - import operator +import re + from sqlalchemy.sql import compiler, expression, text, bindparam from sqlalchemy.engine import default, base, reflection from sqlalchemy import types as sqltypes @@ -28,10 +31,10 @@ from sqlalchemy import schema as sa_schema from sqlalchemy import util, sql, exc from sqlalchemy.types import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\ - TEXT,DATE,DATETIME, FLOAT, NUMERIC,\ - BIGINT,INT, INTEGER, SMALLINT, BINARY,\ + TEXT, DATE, DATETIME, FLOAT, NUMERIC,\ + BIGINT, INT, INTEGER, SMALLINT, BINARY,\ VARBINARY, DECIMAL, TIMESTAMP, Unicode,\ - UnicodeText + UnicodeText, REAL RESERVED_WORDS = set([ "add", "all", "alter", "and", @@ -99,35 +102,44 @@ class _SybaseUnitypeMixin(object): def result_processor(self, dialect, coltype): def process(value): if value is not None: - return str(value) #.decode("ucs-2") + return str(value) # decode("ucs-2") else: return None return process + class UNICHAR(_SybaseUnitypeMixin, sqltypes.Unicode): __visit_name__ = 'UNICHAR' + class UNIVARCHAR(_SybaseUnitypeMixin, sqltypes.Unicode): __visit_name__ = 'UNIVARCHAR' + class UNITEXT(_SybaseUnitypeMixin, sqltypes.UnicodeText): __visit_name__ = 'UNITEXT' + class TINYINT(sqltypes.Integer): __visit_name__ = 'TINYINT' + class BIT(sqltypes.TypeEngine): __visit_name__ = 'BIT' + class MONEY(sqltypes.TypeEngine): __visit_name__ = "MONEY" + class SMALLMONEY(sqltypes.TypeEngine): __visit_name__ = "SMALLMONEY" + class UNIQUEIDENTIFIER(sqltypes.TypeEngine): __visit_name__ = "UNIQUEIDENTIFIER" + class IMAGE(sqltypes.LargeBinary): __visit_name__ = 'IMAGE' @@ -170,32 +182,68 @@ class SybaseTypeCompiler(compiler.GenericTypeCompiler): return "UNIQUEIDENTIFIER" ischema_names = { - 'integer' : INTEGER, - 'unsigned int' : INTEGER, # TODO: unsigned flags - 'unsigned smallint' : SMALLINT, # TODO: unsigned flags - 'unsigned bigint' : BIGINT, # TODO: unsigned flags 'bigint': BIGINT, - 'smallint' : SMALLINT, - 'tinyint' : TINYINT, - 'varchar' : VARCHAR, - 'long varchar' : TEXT, # TODO - 'char' : CHAR, - 'decimal' : DECIMAL, - 'numeric' : NUMERIC, - 'float' : FLOAT, - 'double' : NUMERIC, # TODO - 'binary' : BINARY, - 'varbinary' : VARBINARY, - 'bit': BIT, - 'image' : IMAGE, - 'timestamp': TIMESTAMP, + 'int': INTEGER, + 'integer': INTEGER, + 'smallint': SMALLINT, + 'tinyint': TINYINT, + 'unsigned bigint': BIGINT, # TODO: unsigned flags + 'unsigned int': INTEGER, # TODO: unsigned flags + 'unsigned smallint': SMALLINT, # TODO: unsigned flags + 'numeric': NUMERIC, + 'decimal': DECIMAL, + 'dec': DECIMAL, + 'float': FLOAT, + 'double': NUMERIC, # TODO + 'double precision': NUMERIC, # TODO + 'real': REAL, + 'smallmoney': SMALLMONEY, 'money': MONEY, - 'smallmoney': MONEY, + 'smalldatetime': DATETIME, + 'datetime': DATETIME, + 'date': DATE, + 'time': TIME, + 'char': CHAR, + 'character': CHAR, + 'varchar': VARCHAR, + 'character varying': VARCHAR, + 'char varying': VARCHAR, + 'unichar': UNICHAR, + 'unicode character': UNIVARCHAR, + 'nchar': NCHAR, + 'national char': NCHAR, + 'national character': NCHAR, + 'nvarchar': NVARCHAR, + 'nchar varying': NVARCHAR, + 'national char varying': NVARCHAR, + 'national character varying': NVARCHAR, + 'text': TEXT, + 'unitext': UNITEXT, + 'binary': BINARY, + 'varbinary': VARBINARY, + 'image': IMAGE, + 'bit': BIT, + +# not in documentation for ASE 15.7 + 'long varchar': TEXT, # TODO + 'timestamp': TIMESTAMP, 'uniqueidentifier': UNIQUEIDENTIFIER, } +class SybaseInspector(reflection.Inspector): + + def __init__(self, conn): + reflection.Inspector.__init__(self, conn) + + def get_table_id(self, table_name, schema=None): + """Return the table id from `table_name` and `schema`.""" + + return self.dialect.get_table_id(self.bind, table_name, schema, + info_cache=self.info_cache) + + class SybaseExecutionContext(default.DefaultExecutionContext): _enable_identity_insert = False @@ -243,12 +291,11 @@ class SybaseExecutionContext(default.DefaultExecutionContext): self.root_connection.connection.connection, True) - def post_exec(self): - if self.isddl: + if self.isddl: self.set_ddl_autocommit(self.root_connection, False) - if self._enable_identity_insert: + if self._enable_identity_insert: self.cursor.execute( "SET IDENTITY_INSERT %s OFF" % self.dialect.identifier_preparer. @@ -262,6 +309,7 @@ class SybaseExecutionContext(default.DefaultExecutionContext): cursor.close() return lastrowid + class SybaseSQLCompiler(compiler.SQLCompiler): ansi_bind_rules = True @@ -288,7 +336,7 @@ class SybaseSQLCompiler(compiler.SQLCompiler): # FIXME: sybase doesn't allow an offset without a limit # so use a huge value for TOP here s += "TOP 1000000 " - s += "START AT %s " % (select._offset+1,) + s += "START AT %s " % (select._offset + 1,) return s def get_from_hint_text(self, table, text): @@ -303,6 +351,9 @@ class SybaseSQLCompiler(compiler.SQLCompiler): return 'DATEPART("%s", %s)' % ( field, self.process(extract.expr, **kw)) + def visit_now_func(self, fn, **kw): + return "GETDATE()" + def for_update_clause(self, select): # "FOR UPDATE" is only allowed on "DECLARE CURSOR" # which SQLAlchemy doesn't use @@ -345,29 +396,31 @@ class SybaseDDLCompiler(compiler.DDLCompiler): # TODO: need correct syntax for this colspec += " IDENTITY(%s,%s)" % (start, increment) else: + default = self.get_column_default_string(column) + if default is not None: + colspec += " DEFAULT " + default + if column.nullable is not None: if not column.nullable or column.primary_key: colspec += " NOT NULL" else: colspec += " NULL" - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - return colspec def visit_drop_index(self, drop): index = drop.element return "\nDROP INDEX %s.%s" % ( self.preparer.quote_identifier(index.table.name), - self.preparer.quote( - self._index_identifier(index.name), index.quote) + self._prepared_index_name(drop.element, + include_schema=False) ) + class SybaseIdentifierPreparer(compiler.IdentifierPreparer): reserved_words = RESERVED_WORDS + class SybaseDialect(default.DefaultDialect): name = 'sybase' supports_unicode_statements = False @@ -385,11 +438,12 @@ class SybaseDialect(default.DefaultDialect): statement_compiler = SybaseSQLCompiler ddl_compiler = SybaseDDLCompiler preparer = SybaseIdentifierPreparer + inspector = SybaseInspector def _get_default_schema_name(self, connection): return connection.scalar( text("SELECT user_name() as user_name", - typemap={'user_name':Unicode}) + typemap={'user_name': Unicode}) ) def initialize(self, connection): @@ -400,39 +454,361 @@ class SybaseDialect(default.DefaultDialect): else: self.max_identifier_length = 255 + def get_table_id(self, connection, table_name, schema=None, **kw): + """Fetch the id for schema.table_name. + + Several reflection methods require the table id. The idea for using + this method is that it can be fetched one time and cached for + subsequent calls. + + """ + + table_id = None + if schema is None: + schema = self.default_schema_name + + TABLEID_SQL = text(""" + SELECT o.id AS id + FROM sysobjects o JOIN sysusers u ON o.uid=u.uid + WHERE u.name = :schema_name + AND o.name = :table_name + AND o.type in ('U', 'V') + """) + + if util.py2k: + if isinstance(schema, unicode): + schema = schema.encode("ascii") + if isinstance(table_name, unicode): + table_name = table_name.encode("ascii") + result = connection.execute(TABLEID_SQL, + schema_name=schema, + table_name=table_name) + table_id = result.scalar() + if table_id is None: + raise exc.NoSuchTableError(table_name) + return table_id + + @reflection.cache + def get_columns(self, connection, table_name, schema=None, **kw): + table_id = self.get_table_id(connection, table_name, schema, + info_cache=kw.get("info_cache")) + + COLUMN_SQL = text(""" + SELECT col.name AS name, + t.name AS type, + (col.status & 8) AS nullable, + (col.status & 128) AS autoincrement, + com.text AS 'default', + col.prec AS precision, + col.scale AS scale, + col.length AS length + FROM systypes t, syscolumns col LEFT OUTER JOIN syscomments com ON + col.cdefault = com.id + WHERE col.usertype = t.usertype + AND col.id = :table_id + ORDER BY col.colid + """) + + results = connection.execute(COLUMN_SQL, table_id=table_id) + + columns = [] + for (name, type_, nullable, autoincrement, default, precision, scale, + length) in results: + col_info = self._get_column_info(name, type_, bool(nullable), + bool(autoincrement), default, precision, scale, + length) + columns.append(col_info) + + return columns + + def _get_column_info(self, name, type_, nullable, autoincrement, default, + precision, scale, length): + + coltype = self.ischema_names.get(type_, None) + + kwargs = {} + + if coltype in (NUMERIC, DECIMAL): + args = (precision, scale) + elif coltype == FLOAT: + args = (precision,) + elif coltype in (CHAR, VARCHAR, UNICHAR, UNIVARCHAR, NCHAR, NVARCHAR): + args = (length,) + else: + args = () + + if coltype: + coltype = coltype(*args, **kwargs) + #is this necessary + #if is_array: + # coltype = ARRAY(coltype) + else: + util.warn("Did not recognize type '%s' of column '%s'" % + (type_, name)) + coltype = sqltypes.NULLTYPE + + if default: + default = re.sub("DEFAULT", "", default).strip() + default = re.sub("^'(.*)'$", lambda m: m.group(1), default) + else: + default = None + + column_info = dict(name=name, type=coltype, nullable=nullable, + default=default, autoincrement=autoincrement) + return column_info + + @reflection.cache + def get_foreign_keys(self, connection, table_name, schema=None, **kw): + + table_id = self.get_table_id(connection, table_name, schema, + info_cache=kw.get("info_cache")) + + table_cache = {} + column_cache = {} + foreign_keys = [] + + table_cache[table_id] = {"name": table_name, "schema": schema} + + COLUMN_SQL = text(""" + SELECT c.colid AS id, c.name AS name + FROM syscolumns c + WHERE c.id = :table_id + """) + + results = connection.execute(COLUMN_SQL, table_id=table_id) + columns = {} + for col in results: + columns[col["id"]] = col["name"] + column_cache[table_id] = columns + + REFCONSTRAINT_SQL = text(""" + SELECT o.name AS name, r.reftabid AS reftable_id, + r.keycnt AS 'count', + r.fokey1 AS fokey1, r.fokey2 AS fokey2, r.fokey3 AS fokey3, + r.fokey4 AS fokey4, r.fokey5 AS fokey5, r.fokey6 AS fokey6, + r.fokey7 AS fokey7, r.fokey1 AS fokey8, r.fokey9 AS fokey9, + r.fokey10 AS fokey10, r.fokey11 AS fokey11, r.fokey12 AS fokey12, + r.fokey13 AS fokey13, r.fokey14 AS fokey14, r.fokey15 AS fokey15, + r.fokey16 AS fokey16, + r.refkey1 AS refkey1, r.refkey2 AS refkey2, r.refkey3 AS refkey3, + r.refkey4 AS refkey4, r.refkey5 AS refkey5, r.refkey6 AS refkey6, + r.refkey7 AS refkey7, r.refkey1 AS refkey8, r.refkey9 AS refkey9, + r.refkey10 AS refkey10, r.refkey11 AS refkey11, + r.refkey12 AS refkey12, r.refkey13 AS refkey13, + r.refkey14 AS refkey14, r.refkey15 AS refkey15, + r.refkey16 AS refkey16 + FROM sysreferences r JOIN sysobjects o on r.tableid = o.id + WHERE r.tableid = :table_id + """) + referential_constraints = connection.execute(REFCONSTRAINT_SQL, + table_id=table_id) + + REFTABLE_SQL = text(""" + SELECT o.name AS name, u.name AS 'schema' + FROM sysobjects o JOIN sysusers u ON o.uid = u.uid + WHERE o.id = :table_id + """) + + for r in referential_constraints: + reftable_id = r["reftable_id"] + + if reftable_id not in table_cache: + c = connection.execute(REFTABLE_SQL, table_id=reftable_id) + reftable = c.fetchone() + c.close() + table_info = {"name": reftable["name"], "schema": None} + if (schema is not None or + reftable["schema"] != self.default_schema_name): + table_info["schema"] = reftable["schema"] + + table_cache[reftable_id] = table_info + results = connection.execute(COLUMN_SQL, table_id=reftable_id) + reftable_columns = {} + for col in results: + reftable_columns[col["id"]] = col["name"] + column_cache[reftable_id] = reftable_columns + + reftable = table_cache[reftable_id] + reftable_columns = column_cache[reftable_id] + + constrained_columns = [] + referred_columns = [] + for i in range(1, r["count"] + 1): + constrained_columns.append(columns[r["fokey%i" % i]]) + referred_columns.append(reftable_columns[r["refkey%i" % i]]) + + fk_info = { + "constrained_columns": constrained_columns, + "referred_schema": reftable["schema"], + "referred_table": reftable["name"], + "referred_columns": referred_columns, + "name": r["name"] + } + + foreign_keys.append(fk_info) + + return foreign_keys + + @reflection.cache + def get_indexes(self, connection, table_name, schema=None, **kw): + table_id = self.get_table_id(connection, table_name, schema, + info_cache=kw.get("info_cache")) + + INDEX_SQL = text(""" + SELECT object_name(i.id) AS table_name, + i.keycnt AS 'count', + i.name AS name, + (i.status & 0x2) AS 'unique', + index_col(object_name(i.id), i.indid, 1) AS col_1, + index_col(object_name(i.id), i.indid, 2) AS col_2, + index_col(object_name(i.id), i.indid, 3) AS col_3, + index_col(object_name(i.id), i.indid, 4) AS col_4, + index_col(object_name(i.id), i.indid, 5) AS col_5, + index_col(object_name(i.id), i.indid, 6) AS col_6, + index_col(object_name(i.id), i.indid, 7) AS col_7, + index_col(object_name(i.id), i.indid, 8) AS col_8, + index_col(object_name(i.id), i.indid, 9) AS col_9, + index_col(object_name(i.id), i.indid, 10) AS col_10, + index_col(object_name(i.id), i.indid, 11) AS col_11, + index_col(object_name(i.id), i.indid, 12) AS col_12, + index_col(object_name(i.id), i.indid, 13) AS col_13, + index_col(object_name(i.id), i.indid, 14) AS col_14, + index_col(object_name(i.id), i.indid, 15) AS col_15, + index_col(object_name(i.id), i.indid, 16) AS col_16 + FROM sysindexes i, sysobjects o + WHERE o.id = i.id + AND o.id = :table_id + AND (i.status & 2048) = 0 + AND i.indid BETWEEN 1 AND 254 + """) + + results = connection.execute(INDEX_SQL, table_id=table_id) + indexes = [] + for r in results: + column_names = [] + for i in range(1, r["count"]): + column_names.append(r["col_%i" % (i,)]) + index_info = {"name": r["name"], + "unique": bool(r["unique"]), + "column_names": column_names} + indexes.append(index_info) + + return indexes + + @reflection.cache + def get_pk_constraint(self, connection, table_name, schema=None, **kw): + table_id = self.get_table_id(connection, table_name, schema, + info_cache=kw.get("info_cache")) + + PK_SQL = text(""" + SELECT object_name(i.id) AS table_name, + i.keycnt AS 'count', + i.name AS name, + index_col(object_name(i.id), i.indid, 1) AS pk_1, + index_col(object_name(i.id), i.indid, 2) AS pk_2, + index_col(object_name(i.id), i.indid, 3) AS pk_3, + index_col(object_name(i.id), i.indid, 4) AS pk_4, + index_col(object_name(i.id), i.indid, 5) AS pk_5, + index_col(object_name(i.id), i.indid, 6) AS pk_6, + index_col(object_name(i.id), i.indid, 7) AS pk_7, + index_col(object_name(i.id), i.indid, 8) AS pk_8, + index_col(object_name(i.id), i.indid, 9) AS pk_9, + index_col(object_name(i.id), i.indid, 10) AS pk_10, + index_col(object_name(i.id), i.indid, 11) AS pk_11, + index_col(object_name(i.id), i.indid, 12) AS pk_12, + index_col(object_name(i.id), i.indid, 13) AS pk_13, + index_col(object_name(i.id), i.indid, 14) AS pk_14, + index_col(object_name(i.id), i.indid, 15) AS pk_15, + index_col(object_name(i.id), i.indid, 16) AS pk_16 + FROM sysindexes i, sysobjects o + WHERE o.id = i.id + AND o.id = :table_id + AND (i.status & 2048) = 2048 + AND i.indid BETWEEN 1 AND 254 + """) + + results = connection.execute(PK_SQL, table_id=table_id) + pks = results.fetchone() + results.close() + + constrained_columns = [] + for i in range(1, pks["count"] + 1): + constrained_columns.append(pks["pk_%i" % (i,)]) + return {"constrained_columns": constrained_columns, + "name": pks["name"]} + + @reflection.cache + def get_schema_names(self, connection, **kw): + + SCHEMA_SQL = text("SELECT u.name AS name FROM sysusers u") + + schemas = connection.execute(SCHEMA_SQL) + + return [s["name"] for s in schemas] + @reflection.cache def get_table_names(self, connection, schema=None, **kw): if schema is None: schema = self.default_schema_name - result = connection.execute( - text("select sysobjects.name from sysobjects, sysusers " - "where sysobjects.uid=sysusers.uid and " - "sysusers.name=:schemaname and " - "sysobjects.type='U'", - bindparams=[ - bindparam('schemaname', schema) - ]) - ) - return [r[0] for r in result] + TABLE_SQL = text(""" + SELECT o.name AS name + FROM sysobjects o JOIN sysusers u ON o.uid = u.uid + WHERE u.name = :schema_name + AND o.type = 'U' + """) - def has_table(self, connection, tablename, schema=None): + if util.py2k: + if isinstance(schema, unicode): + schema = schema.encode("ascii") + + tables = connection.execute(TABLE_SQL, schema_name=schema) + + return [t["name"] for t in tables] + + @reflection.cache + def get_view_definition(self, connection, view_name, schema=None, **kw): if schema is None: schema = self.default_schema_name - result = connection.execute( - text("select sysobjects.name from sysobjects, sysusers " - "where sysobjects.uid=sysusers.uid and " - "sysobjects.name=:tablename and " - "sysusers.name=:schemaname and " - "sysobjects.type='U'", - bindparams=[ - bindparam('tablename', tablename), - bindparam('schemaname', schema) - ]) - ) - return result.scalar() is not None + VIEW_DEF_SQL = text(""" + SELECT c.text + FROM syscomments c JOIN sysobjects o ON c.id = o.id + WHERE o.name = :view_name + AND o.type = 'V' + """) - def reflecttable(self, connection, table, include_columns): - raise NotImplementedError() + if util.py2k: + if isinstance(view_name, unicode): + view_name = view_name.encode("ascii") + view = connection.execute(VIEW_DEF_SQL, view_name=view_name) + + return view.scalar() + + @reflection.cache + def get_view_names(self, connection, schema=None, **kw): + if schema is None: + schema = self.default_schema_name + + VIEW_SQL = text(""" + SELECT o.name AS name + FROM sysobjects o JOIN sysusers u ON o.uid = u.uid + WHERE u.name = :schema_name + AND o.type = 'V' + """) + + if util.py2k: + if isinstance(schema, unicode): + schema = schema.encode("ascii") + views = connection.execute(VIEW_SQL, schema_name=schema) + + return [v["name"] for v in views] + + def has_table(self, connection, table_name, schema=None): + try: + self.get_table_id(connection, table_name, schema) + except exc.NoSuchTableError: + return False + else: + return True diff --git a/libs/sqlalchemy/dialects/sybase/mxodbc.py b/libs/sqlalchemy/dialects/sybase/mxodbc.py index db60b9b2..f14d1c42 100644 --- a/libs/sqlalchemy/dialects/sybase/mxodbc.py +++ b/libs/sqlalchemy/dialects/sybase/mxodbc.py @@ -1,22 +1,31 @@ # sybase/mxodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php - """ -Support for Sybase via mxodbc. -This dialect is a stub only and is likely non functional at this time. +.. dialect:: sybase+mxodbc + :name: mxODBC + :dbapi: mxodbc + :connectstring: sybase+mxodbc://:@ + :url: http://www.egenix.com/ + +.. note:: + + This dialect is a stub only and is likely non functional at this time. """ -from sqlalchemy.dialects.sybase.base import SybaseDialect, SybaseExecutionContext +from sqlalchemy.dialects.sybase.base import SybaseDialect +from sqlalchemy.dialects.sybase.base import SybaseExecutionContext from sqlalchemy.connectors.mxodbc import MxODBCConnector + class SybaseExecutionContext_mxodbc(SybaseExecutionContext): pass + class SybaseDialect_mxodbc(MxODBCConnector, SybaseDialect): execution_ctx_cls = SybaseExecutionContext_mxodbc diff --git a/libs/sqlalchemy/dialects/sybase/pyodbc.py b/libs/sqlalchemy/dialects/sybase/pyodbc.py index 8e3729b3..f773e5a6 100644 --- a/libs/sqlalchemy/dialects/sybase/pyodbc.py +++ b/libs/sqlalchemy/dialects/sybase/pyodbc.py @@ -1,18 +1,16 @@ # sybase/pyodbc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for Sybase via pyodbc. +.. dialect:: sybase+pyodbc + :name: PyODBC + :dbapi: pyodbc + :connectstring: sybase+pyodbc://:@[/] + :url: http://pypi.python.org/pypi/pyodbc/ -http://pypi.python.org/pypi/pyodbc/ - -Connect strings are of the form:: - - sybase+pyodbc://:@/ - sybase+pyodbc://:@/ Unicode Support --------------- @@ -37,8 +35,9 @@ Currently *not* supported are:: from sqlalchemy.dialects.sybase.base import SybaseDialect,\ SybaseExecutionContext from sqlalchemy.connectors.pyodbc import PyODBCConnector -from sqlalchemy import types as sqltypes, util, processors -from sqlalchemy.util.compat import decimal +from sqlalchemy import types as sqltypes, processors +import decimal + class _SybNumeric_pyodbc(sqltypes.Numeric): """Turns Decimals with adjusted() < -6 into floats. @@ -50,7 +49,7 @@ class _SybNumeric_pyodbc(sqltypes.Numeric): """ def bind_processor(self, dialect): - super_process = super(_SybNumeric_pyodbc,self).\ + super_process = super(_SybNumeric_pyodbc, self).\ bind_processor(dialect) def process(value): @@ -66,6 +65,7 @@ class _SybNumeric_pyodbc(sqltypes.Numeric): return value return process + class SybaseExecutionContext_pyodbc(SybaseExecutionContext): def set_ddl_autocommit(self, connection, value): if value: @@ -73,11 +73,12 @@ class SybaseExecutionContext_pyodbc(SybaseExecutionContext): else: connection.autocommit = False + class SybaseDialect_pyodbc(PyODBCConnector, SybaseDialect): execution_ctx_cls = SybaseExecutionContext_pyodbc colspecs = { - sqltypes.Numeric:_SybNumeric_pyodbc, + sqltypes.Numeric: _SybNumeric_pyodbc, } dialect = SybaseDialect_pyodbc diff --git a/libs/sqlalchemy/dialects/sybase/pysybase.py b/libs/sqlalchemy/dialects/sybase/pysybase.py index bf8c2096..664bd9ac 100644 --- a/libs/sqlalchemy/dialects/sybase/pysybase.py +++ b/libs/sqlalchemy/dialects/sybase/pysybase.py @@ -1,17 +1,15 @@ # sybase/pysybase.py -# Copyright (C) 2010-2011 the SQLAlchemy authors and contributors +# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Support for Sybase via the python-sybase driver. - -http://python-sybase.sourceforge.net/ - -Connect strings are of the form:: - - sybase+pysybase://:@/[database name] +.. dialect:: sybase+pysybase + :name: Python-Sybase + :dbapi: Sybase + :connectstring: sybase+pysybase://:@/[database name] + :url: http://python-sybase.sourceforge.net/ Unicode Support --------------- @@ -33,6 +31,7 @@ class _SybNumeric(sqltypes.Numeric): else: return sqltypes.Numeric.result_processor(self, dialect, type_) + class SybaseExecutionContext_pysybase(SybaseExecutionContext): def set_ddl_autocommit(self, dbapi_connection, value): @@ -55,14 +54,15 @@ class SybaseSQLCompiler_pysybase(SybaseSQLCompiler): def bindparam_string(self, name, **kw): return "@" + name + class SybaseDialect_pysybase(SybaseDialect): driver = 'pysybase' execution_ctx_cls = SybaseExecutionContext_pysybase statement_compiler = SybaseSQLCompiler_pysybase - colspecs={ - sqltypes.Numeric:_SybNumeric, - sqltypes.Float:sqltypes.Float + colspecs = { + sqltypes.Numeric: _SybNumeric, + sqltypes.Float: sqltypes.Float } @classmethod diff --git a/libs/sqlalchemy/engine/__init__.py b/libs/sqlalchemy/engine/__init__.py index 6ff8ba15..890c7664 100644 --- a/libs/sqlalchemy/engine/__init__.py +++ b/libs/sqlalchemy/engine/__init__.py @@ -1,5 +1,5 @@ # engine/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -50,54 +50,47 @@ url.py within a URL. """ -# not sure what this was used for -#import sqlalchemy.databases +from .interfaces import ( + Connectable, + Dialect, + ExecutionContext, -from sqlalchemy.engine.base import ( + # backwards compat + Compiled, + TypeCompiler +) + +from .base import ( + Connection, + Engine, + NestedTransaction, + RootTransaction, + Transaction, + TwoPhaseTransaction, + ) + +from .result import ( BufferedColumnResultProxy, BufferedColumnRow, BufferedRowResultProxy, - Compiled, - Connectable, - Connection, - Dialect, - Engine, - ExecutionContext, - NestedTransaction, + FullyBufferedResultProxy, ResultProxy, - RootTransaction, RowProxy, - Transaction, - TwoPhaseTransaction, - TypeCompiler - ) -from sqlalchemy.engine import strategies -from sqlalchemy import util - - -__all__ = ( - 'BufferedColumnResultProxy', - 'BufferedColumnRow', - 'BufferedRowResultProxy', - 'Compiled', - 'Connectable', - 'Connection', - 'Dialect', - 'Engine', - 'ExecutionContext', - 'NestedTransaction', - 'ResultProxy', - 'RootTransaction', - 'RowProxy', - 'Transaction', - 'TwoPhaseTransaction', - 'TypeCompiler', - 'create_engine', - 'engine_from_config', ) +from .util import ( + connection_memoize + ) + + +from . import util, strategies + +# backwards compat +from ..sql import ddl default_strategy = 'plain' + + def create_engine(*args, **kwargs): """Create a new :class:`.Engine` instance. @@ -117,11 +110,11 @@ def create_engine(*args, **kwargs): the URL can be an instance of :class:`~sqlalchemy.engine.url.URL`. ``**kwargs`` takes a wide variety of options which are routed - towards their appropriate components. Arguments may be - specific to the :class:`.Engine`, the underlying :class:`.Dialect`, as well as the - :class:`.Pool`. Specific dialects also accept keyword arguments that - are unique to that dialect. Here, we describe the parameters - that are common to most :func:`.create_engine()` usage. + towards their appropriate components. Arguments may be specific + to the :class:`.Engine`, the underlying :class:`.Dialect`, as well as + the :class:`.Pool`. Specific dialects also accept keyword + arguments that are unique to that dialect. Here, we describe the + parameters that are common to most :func:`.create_engine()` usage. Once established, the newly resulting :class:`.Engine` will request a connection from the underlying :class:`.Pool` once @@ -133,15 +126,17 @@ def create_engine(*args, **kwargs): See also: - :ref:`engines_toplevel` + :doc:`/core/engines` :ref:`connections_toplevel` - :param assert_unicode: Deprecated. This flag - sets an engine-wide default value for - the ``assert_unicode`` flag on the - :class:`.String` type - see that - type for further details. + :param case_sensitive=True: if False, result column names + will match in a case-insensitive fashion, that is, + ``row['SomeColumn']``. + + .. versionchanged:: 0.8 + By default, result row names match case-sensitively. + In version 0.7 and prior, all matches were case-insensitive. :param connect_args: a dictionary of options which will be passed directly to the DBAPI's ``connect()`` method as @@ -231,7 +226,7 @@ def create_engine(*args, **kwargs): :param execution_options: Dictionary execution options which will be applied to all connections. See - :meth:`~sqlalchemy.engine.base.Connection.execution_options` + :meth:`~sqlalchemy.engine.Connection.execution_options` :param implicit_returning=True: When ``True``, a RETURNING- compatible construct, if available, will be used to @@ -262,13 +257,13 @@ def create_engine(*args, **kwargs): opened above and beyond the pool_size setting, which defaults to five. this is only used with :class:`~sqlalchemy.pool.QueuePool`. - :param module=None: reference to a Python module object (the module itself, not - its string name). Specifies an alternate DBAPI module to be used - by the engine's dialect. Each sub-dialect references a specific DBAPI which - will be imported before first connect. This parameter causes the - import to be bypassed, and the given module to be used instead. - Can be used for testing of DBAPIs as well as to inject "mock" - DBAPI implementations into the :class:`.Engine`. + :param module=None: reference to a Python module object (the module + itself, not its string name). Specifies an alternate DBAPI module to + be used by the engine's dialect. Each sub-dialect references a + specific DBAPI which will be imported before first connect. This + parameter causes the import to be bypassed, and the given module to + be used instead. Can be used for testing of DBAPIs as well as to + inject "mock" DBAPI implementations into the :class:`.Engine`. :param pool=None: an already-constructed instance of :class:`~sqlalchemy.pool.Pool`, such as a @@ -291,7 +286,8 @@ def create_engine(*args, **kwargs): id. :param pool_size=5: the number of connections to keep open - inside the connection pool. This used with :class:`~sqlalchemy.pool.QueuePool` as + inside the connection pool. This used with + :class:`~sqlalchemy.pool.QueuePool` as well as :class:`~sqlalchemy.pool.SingletonThreadPool`. With :class:`~sqlalchemy.pool.QueuePool`, a ``pool_size`` setting of 0 indicates no limit; to disable pooling, set ``poolclass`` to @@ -325,7 +321,8 @@ def create_engine(*args, **kwargs): :ref:`threadlocal_strategy`; * the ``mock`` strategy, which dispatches all statement execution to a function passed as the argument ``executor``. - See `example in the FAQ `_. + See `example in the FAQ + `_. :param executor=None: a function taking arguments ``(sql, *multiparams, **params)``, to which the ``mock`` strategy will @@ -337,6 +334,7 @@ def create_engine(*args, **kwargs): strategy = strategies.strategies[strategy] return strategy.create(*args, **kwargs) + def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): """Create a new Engine instance using a configuration dictionary. @@ -350,27 +348,16 @@ def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): arguments. """ - opts = _coerce_config(configuration, prefix) - opts.update(kwargs) - url = opts.pop('url') - return create_engine(url, **opts) - -def _coerce_config(configuration, prefix): - """Convert configuration values to expected types.""" - options = dict((key[len(prefix):], configuration[key]) for key in configuration if key.startswith(prefix)) - for option, type_ in ( - ('convert_unicode', util.bool_or_str('force')), - ('pool_timeout', int), - ('echo', util.bool_or_str('debug')), - ('echo_pool', util.bool_or_str('debug')), - ('pool_recycle', int), - ('pool_size', int), - ('max_overflow', int), - ('pool_threadlocal', bool), - ('use_native_unicode', bool), - ): - util.coerce_kw_type(options, option, type_) - return options + options['_coerce_config'] = True + options.update(kwargs) + url = options.pop('url') + return create_engine(url, **options) + + +__all__ = ( + 'create_engine', + 'engine_from_config', + ) diff --git a/libs/sqlalchemy/engine/base.py b/libs/sqlalchemy/engine/base.py index 302fb779..0e888ca4 100644 --- a/libs/sqlalchemy/engine/base.py +++ b/libs/sqlalchemy/engine/base.py @@ -1,842 +1,21 @@ # engine/base.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php +from __future__ import with_statement +"""Defines :class:`.Connection` and :class:`.Engine`. -"""Basic components for SQL execution and interfacing with DB-API. - -Defines the basic components used to interface DB-API modules with -higher-level statement-construction, connection-management, execution -and result contexts. """ -__all__ = [ - 'BufferedColumnResultProxy', 'BufferedColumnRow', - 'BufferedRowResultProxy','Compiled', 'Connectable', 'Connection', - 'Dialect', 'Engine','ExecutionContext', 'NestedTransaction', - 'ResultProxy', 'RootTransaction','RowProxy', 'SchemaIterator', - 'StringIO', 'Transaction', 'TwoPhaseTransaction', - 'connection_memoize'] -import inspect, StringIO, sys, operator -from itertools import izip -from sqlalchemy import exc, schema, util, types, log, interfaces, \ - event, events -from sqlalchemy.sql import expression, util as sql_util -from sqlalchemy import processors -import collections - -class Dialect(object): - """Define the behavior of a specific database and DB-API combination. - - Any aspect of metadata definition, SQL query generation, - execution, result-set handling, or anything else which varies - between databases is defined under the general category of the - Dialect. The Dialect acts as a factory for other - database-specific object implementations including - ExecutionContext, Compiled, DefaultGenerator, and TypeEngine. - - All Dialects implement the following attributes: - - name - identifying name for the dialect from a DBAPI-neutral point of view - (i.e. 'sqlite') - - driver - identifying name for the dialect's DBAPI - - positional - True if the paramstyle for this Dialect is positional. - - paramstyle - the paramstyle to be used (some DB-APIs support multiple - paramstyles). - - convert_unicode - True if Unicode conversion should be applied to all ``str`` - types. - - encoding - type of encoding to use for unicode, usually defaults to - 'utf-8'. - - statement_compiler - a :class:`~Compiled` class used to compile SQL statements - - ddl_compiler - a :class:`~Compiled` class used to compile DDL statements - - server_version_info - a tuple containing a version number for the DB backend in use. - This value is only available for supporting dialects, and is - typically populated during the initial connection to the database. - - default_schema_name - the name of the default schema. This value is only available for - supporting dialects, and is typically populated during the - initial connection to the database. - - execution_ctx_cls - a :class:`.ExecutionContext` class used to handle statement execution - - execute_sequence_format - either the 'tuple' or 'list' type, depending on what cursor.execute() - accepts for the second argument (they vary). - - preparer - a :class:`~sqlalchemy.sql.compiler.IdentifierPreparer` class used to - quote identifiers. - - supports_alter - ``True`` if the database supports ``ALTER TABLE``. - - max_identifier_length - The maximum length of identifier names. - - supports_unicode_statements - Indicate whether the DB-API can receive SQL statements as Python - unicode strings - - supports_unicode_binds - Indicate whether the DB-API can receive string bind parameters - as Python unicode strings - - supports_sane_rowcount - Indicate whether the dialect properly implements rowcount for - ``UPDATE`` and ``DELETE`` statements. - - supports_sane_multi_rowcount - Indicate whether the dialect properly implements rowcount for - ``UPDATE`` and ``DELETE`` statements when executed via - executemany. - - preexecute_autoincrement_sequences - True if 'implicit' primary key functions must be executed separately - in order to get their value. This is currently oriented towards - Postgresql. - - implicit_returning - use RETURNING or equivalent during INSERT execution in order to load - newly generated primary keys and other column defaults in one execution, - which are then available via inserted_primary_key. - If an insert statement has returning() specified explicitly, - the "implicit" functionality is not used and inserted_primary_key - will not be available. - - dbapi_type_map - A mapping of DB-API type objects present in this Dialect's - DB-API implementation mapped to TypeEngine implementations used - by the dialect. - - This is used to apply types to result sets based on the DB-API - types present in cursor.description; it only takes effect for - result sets against textual statements where no explicit - typemap was present. - - colspecs - A dictionary of TypeEngine classes from sqlalchemy.types mapped - to subclasses that are specific to the dialect class. This - dictionary is class-level only and is not accessed from the - dialect instance itself. - - supports_default_values - Indicates if the construct ``INSERT INTO tablename DEFAULT - VALUES`` is supported - - supports_sequences - Indicates if the dialect supports CREATE SEQUENCE or similar. - - sequences_optional - If True, indicates if the "optional" flag on the Sequence() construct - should signal to not generate a CREATE SEQUENCE. Applies only to - dialects that support sequences. Currently used only to allow Postgresql - SERIAL to be used on a column that specifies Sequence() for usage on - other backends. - - supports_native_enum - Indicates if the dialect supports a native ENUM construct. - This will prevent types.Enum from generating a CHECK - constraint when that type is used. - - supports_native_boolean - Indicates if the dialect supports a native boolean construct. - This will prevent types.Boolean from generating a CHECK - constraint when that type is used. - - """ - - def create_connect_args(self, url): - """Build DB-API compatible connection arguments. - - Given a :class:`~sqlalchemy.engine.url.URL` object, returns a tuple - consisting of a `*args`/`**kwargs` suitable to send directly - to the dbapi's connect function. - - """ - - raise NotImplementedError() - - @classmethod - def type_descriptor(cls, typeobj): - """Transform a generic type to a dialect-specific type. - - Dialect classes will usually use the - :func:`~sqlalchemy.types.adapt_type` function in the types module to - make this job easy. - - The returned result is cached *per dialect class* so can - contain no dialect-instance state. - - """ - - raise NotImplementedError() - - def initialize(self, connection): - """Called during strategized creation of the dialect with a - connection. - - Allows dialects to configure options based on server version info or - other properties. - - The connection passed here is a SQLAlchemy Connection object, - with full capabilities. - - The initalize() method of the base dialect should be called via - super(). - - """ - - pass - - def reflecttable(self, connection, table, include_columns=None): - """Load table description from the database. - - Given a :class:`.Connection` and a - :class:`~sqlalchemy.schema.Table` object, reflect its columns and - properties from the database. If include_columns (a list or - set) is specified, limit the autoload to the given column - names. - - The default implementation uses the - :class:`~sqlalchemy.engine.reflection.Inspector` interface to - provide the output, building upon the granular table/column/ - constraint etc. methods of :class:`.Dialect`. - - """ - - raise NotImplementedError() - - def get_columns(self, connection, table_name, schema=None, **kw): - """Return information about columns in `table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return column - information as a list of dictionaries with these keys: - - name - the column's name - - type - [sqlalchemy.types#TypeEngine] - - nullable - boolean - - default - the column's default value - - autoincrement - boolean - - sequence - a dictionary of the form - {'name' : str, 'start' :int, 'increment': int} - - Additional column attributes may be present. - """ - - raise NotImplementedError() - - def get_primary_keys(self, connection, table_name, schema=None, **kw): - """Return information about primary keys in `table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return primary - key information as a list of column names. - - """ - raise NotImplementedError() - - def get_pk_constraint(self, table_name, schema=None, **kw): - """Return information about the primary key constraint on - table_name`. - - Given a string `table_name`, and an optional string `schema`, return - primary key information as a dictionary with these keys: - - constrained_columns - a list of column names that make up the primary key - - name - optional name of the primary key constraint. - - """ - raise NotImplementedError() - - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - """Return information about foreign_keys in `table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return foreign - key information as a list of dicts with these keys: - - name - the constraint's name - - constrained_columns - a list of column names that make up the foreign key - - referred_schema - the name of the referred schema - - referred_table - the name of the referred table - - referred_columns - a list of column names in the referred table that correspond to - constrained_columns - """ - - raise NotImplementedError() - - def get_table_names(self, connection, schema=None, **kw): - """Return a list of table names for `schema`.""" - - raise NotImplementedError - - def get_view_names(self, connection, schema=None, **kw): - """Return a list of all view names available in the database. - - schema: - Optional, retrieve names from a non-default schema. - """ - - raise NotImplementedError() - - def get_view_definition(self, connection, view_name, schema=None, **kw): - """Return view definition. - - Given a :class:`.Connection`, a string - `view_name`, and an optional string `schema`, return the view - definition. - """ - - raise NotImplementedError() - - def get_indexes(self, connection, table_name, schema=None, **kw): - """Return information about indexes in `table_name`. - - Given a :class:`.Connection`, a string - `table_name` and an optional string `schema`, return index - information as a list of dictionaries with these keys: - - name - the index's name - - column_names - list of column names in order - - unique - boolean - """ - - raise NotImplementedError() - - def normalize_name(self, name): - """convert the given name to lowercase if it is detected as - case insensitive. - - this method is only used if the dialect defines - requires_name_normalize=True. - - """ - raise NotImplementedError() - - def denormalize_name(self, name): - """convert the given name to a case insensitive identifier - for the backend if it is an all-lowercase name. - - this method is only used if the dialect defines - requires_name_normalize=True. - - """ - raise NotImplementedError() - - def has_table(self, connection, table_name, schema=None): - """Check the existence of a particular table in the database. - - Given a :class:`.Connection` object and a string - `table_name`, return True if the given table (possibly within - the specified `schema`) exists in the database, False - otherwise. - """ - - raise NotImplementedError() - - def has_sequence(self, connection, sequence_name, schema=None): - """Check the existence of a particular sequence in the database. - - Given a :class:`.Connection` object and a string - `sequence_name`, return True if the given sequence exists in - the database, False otherwise. - """ - - raise NotImplementedError() - - def _get_server_version_info(self, connection): - """Retrieve the server version info from the given connection. - - This is used by the default implementation to populate the - "server_version_info" attribute and is called exactly - once upon first connect. - - """ - - raise NotImplementedError() - - def _get_default_schema_name(self, connection): - """Return the string name of the currently selected schema from - the given connection. - - This is used by the default implementation to populate the - "default_schema_name" attribute and is called exactly - once upon first connect. - - """ - - raise NotImplementedError() - - def do_begin(self, connection): - """Provide an implementation of *connection.begin()*, given a - DB-API connection.""" - - raise NotImplementedError() - - def do_rollback(self, connection): - """Provide an implementation of *connection.rollback()*, given - a DB-API connection.""" - - raise NotImplementedError() - - def create_xid(self): - """Create a two-phase transaction ID. - - This id will be passed to do_begin_twophase(), - do_rollback_twophase(), do_commit_twophase(). Its format is - unspecified. - """ - - raise NotImplementedError() - - def do_commit(self, connection): - """Provide an implementation of *connection.commit()*, given a - DB-API connection.""" - - raise NotImplementedError() - - def do_savepoint(self, connection, name): - """Create a savepoint with the given name on a SQLAlchemy - connection.""" - - raise NotImplementedError() - - def do_rollback_to_savepoint(self, connection, name): - """Rollback a SQL Alchemy connection to the named savepoint.""" - - raise NotImplementedError() - - def do_release_savepoint(self, connection, name): - """Release the named savepoint on a SQL Alchemy connection.""" - - raise NotImplementedError() - - def do_begin_twophase(self, connection, xid): - """Begin a two phase transaction on the given connection.""" - - raise NotImplementedError() - - def do_prepare_twophase(self, connection, xid): - """Prepare a two phase transaction on the given connection.""" - - raise NotImplementedError() - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - """Rollback a two phase transaction on the given connection.""" - - raise NotImplementedError() - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - """Commit a two phase transaction on the given connection.""" - - raise NotImplementedError() - - def do_recover_twophase(self, connection): - """Recover list of uncommited prepared two phase transaction - identifiers on the given connection.""" - - raise NotImplementedError() - - def do_executemany(self, cursor, statement, parameters, context=None): - """Provide an implementation of ``cursor.executemany(statement, - parameters)``.""" - - raise NotImplementedError() - - def do_execute(self, cursor, statement, parameters, context=None): - """Provide an implementation of ``cursor.execute(statement, - parameters)``.""" - - raise NotImplementedError() - - def do_execute_no_params(self, cursor, statement, parameters, context=None): - """Provide an implementation of ``cursor.execute(statement)``. - - The parameter collection should not be sent. - - """ - - raise NotImplementedError() - - def is_disconnect(self, e, connection, cursor): - """Return True if the given DB-API error indicates an invalid - connection""" - - raise NotImplementedError() - - def connect(self): - """return a callable which sets up a newly created DBAPI connection. - - The callable accepts a single argument "conn" which is the - DBAPI connection itself. It has no return value. - - This is used to set dialect-wide per-connection options such as - isolation modes, unicode modes, etc. - - If a callable is returned, it will be assembled into a pool listener - that receives the direct DBAPI connection, with all wrappers removed. - - If None is returned, no listener will be generated. - - """ - return None - - def reset_isolation_level(self, dbapi_conn): - """Given a DBAPI connection, revert its isolation to the default.""" - - raise NotImplementedError() - - def set_isolation_level(self, dbapi_conn, level): - """Given a DBAPI connection, set its isolation level.""" - - raise NotImplementedError() - - def get_isolation_level(self, dbapi_conn): - """Given a DBAPI connection, return its isolation level.""" - - raise NotImplementedError() - - -class ExecutionContext(object): - """A messenger object for a Dialect that corresponds to a single - execution. - - ExecutionContext should have these data members: - - connection - Connection object which can be freely used by default value - generators to execute SQL. This Connection should reference the - same underlying connection/transactional resources of - root_connection. - - root_connection - Connection object which is the source of this ExecutionContext. This - Connection may have close_with_result=True set, in which case it can - only be used once. - - dialect - dialect which created this ExecutionContext. - - cursor - DB-API cursor procured from the connection, - - compiled - if passed to constructor, sqlalchemy.engine.base.Compiled object - being executed, - - statement - string version of the statement to be executed. Is either - passed to the constructor, or must be created from the - sql.Compiled object by the time pre_exec() has completed. - - parameters - bind parameters passed to the execute() method. For compiled - statements, this is a dictionary or list of dictionaries. For - textual statements, it should be in a format suitable for the - dialect's paramstyle (i.e. dict or list of dicts for non - positional, list or list of lists/tuples for positional). - - isinsert - True if the statement is an INSERT. - - isupdate - True if the statement is an UPDATE. - - should_autocommit - True if the statement is a "committable" statement. - - postfetch_cols - a list of Column objects for which a server-side default or - inline SQL expression value was fired off. Applies to inserts - and updates. - """ - - def create_cursor(self): - """Return a new cursor generated from this ExecutionContext's - connection. - - Some dialects may wish to change the behavior of - connection.cursor(), such as postgresql which may return a PG - "server side" cursor. - """ - - raise NotImplementedError() - - def pre_exec(self): - """Called before an execution of a compiled statement. - - If a compiled statement was passed to this ExecutionContext, - the `statement` and `parameters` datamembers must be - initialized after this statement is complete. - """ - - raise NotImplementedError() - - def post_exec(self): - """Called after the execution of a compiled statement. - - If a compiled statement was passed to this ExecutionContext, - the `last_insert_ids`, `last_inserted_params`, etc. - datamembers should be available after this method completes. - """ - - raise NotImplementedError() - - def result(self): - """Return a result object corresponding to this ExecutionContext. - - Returns a ResultProxy. - """ - - raise NotImplementedError() - - def handle_dbapi_exception(self, e): - """Receive a DBAPI exception which occurred upon execute, result - fetch, etc.""" - - raise NotImplementedError() - - def should_autocommit_text(self, statement): - """Parse the given textual statement and return True if it refers to - a "committable" statement""" - - raise NotImplementedError() - - def lastrow_has_defaults(self): - """Return True if the last INSERT or UPDATE row contained - inlined or database-side defaults. - """ - - raise NotImplementedError() - - def get_rowcount(self): - """Return the DBAPI ``cursor.rowcount`` value, or in some - cases an interpreted value. - - See :attr:`.ResultProxy.rowcount` for details on this. - - """ - - raise NotImplementedError() - - -class Compiled(object): - """Represent a compiled SQL or DDL expression. - - The ``__str__`` method of the ``Compiled`` object should produce - the actual text of the statement. ``Compiled`` objects are - specific to their underlying database dialect, and also may - or may not be specific to the columns referenced within a - particular set of bind parameters. In no case should the - ``Compiled`` object be dependent on the actual values of those - bind parameters, even though it may reference those values as - defaults. - """ - - def __init__(self, dialect, statement, bind=None): - """Construct a new ``Compiled`` object. - - :param dialect: ``Dialect`` to compile against. - - :param statement: ``ClauseElement`` to be compiled. - - :param bind: Optional Engine or Connection to compile this - statement against. - """ - - self.dialect = dialect - self.bind = bind - if statement is not None: - self.statement = statement - self.can_execute = statement.supports_execution - self.string = self.process(self.statement) - - @util.deprecated("0.7", ":class:`.Compiled` objects now compile " - "within the constructor.") - def compile(self): - """Produce the internal string representation of this element.""" - pass - - @property - def sql_compiler(self): - """Return a Compiled that is capable of processing SQL expressions. - - If this compiler is one, it would likely just return 'self'. - - """ - - raise NotImplementedError() - - def process(self, obj, **kwargs): - return obj._compiler_dispatch(self, **kwargs) - - def __str__(self): - """Return the string text of the generated SQL or DDL.""" - - return self.string or '' - - def construct_params(self, params=None): - """Return the bind params for this compiled object. - - :param params: a dict of string/object pairs whose values will - override bind values compiled in to the - statement. - """ - - raise NotImplementedError() - - @property - def params(self): - """Return the bind params for this compiled object.""" - return self.construct_params() - - def execute(self, *multiparams, **params): - """Execute this compiled object.""" - - e = self.bind - if e is None: - raise exc.UnboundExecutionError( - "This Compiled object is not bound to any Engine " - "or Connection.") - return e._execute_compiled(self, multiparams, params) - - def scalar(self, *multiparams, **params): - """Execute this compiled object and return the result's - scalar value.""" - - return self.execute(*multiparams, **params).scalar() - - -class TypeCompiler(object): - """Produces DDL specification for TypeEngine objects.""" - - def __init__(self, dialect): - self.dialect = dialect - - def process(self, type_): - return type_._compiler_dispatch(self) - - -class Connectable(object): - """Interface for an object which supports execution of SQL constructs. - - The two implementations of :class:`.Connectable` are :class:`.Connection` and - :class:`.Engine`. - - Connectable must also implement the 'dialect' member which references a - :class:`.Dialect` instance. - - """ - - def connect(self, **kwargs): - """Return a :class:`.Connection` object. - - Depending on context, this may be ``self`` if this object - is already an instance of :class:`.Connection`, or a newly - procured :class:`.Connection` if this object is an instance - of :class:`.Engine`. - - """ - - def contextual_connect(self): - """Return a :class:`.Connection` object which may be part of an ongoing - context. - - Depending on context, this may be ``self`` if this object - is already an instance of :class:`.Connection`, or a newly - procured :class:`.Connection` if this object is an instance - of :class:`.Engine`. - - """ - - raise NotImplementedError() - - @util.deprecated("0.7", "Use the create() method on the given schema " - "object directly, i.e. :meth:`.Table.create`, " - ":meth:`.Index.create`, :meth:`.MetaData.create_all`") - def create(self, entity, **kwargs): - """Emit CREATE statements for the given schema entity.""" - - raise NotImplementedError() - - @util.deprecated("0.7", "Use the drop() method on the given schema " - "object directly, i.e. :meth:`.Table.drop`, " - ":meth:`.Index.drop`, :meth:`.MetaData.drop_all`") - def drop(self, entity, **kwargs): - """Emit DROP statements for the given schema entity.""" - - raise NotImplementedError() - - def execute(self, object, *multiparams, **params): - """Executes the given construct and returns a :class:`.ResultProxy`.""" - raise NotImplementedError() - - def scalar(self, object, *multiparams, **params): - """Executes and returns the first column of the first row. - - The underlying cursor is closed after execution. - """ - raise NotImplementedError() - - def _run_visitor(self, visitorcallable, element, - **kwargs): - raise NotImplementedError() - - def _execute_clauseelement(self, elem, multiparams=None, params=None): - raise NotImplementedError() +import sys +from .. import exc, util, log, interfaces +from ..sql import expression, util as sql_util, schema, ddl +from .interfaces import Connectable, Compiled +from .util import _distill_params +import contextlib class Connection(Connectable): @@ -865,7 +44,9 @@ class Connection(Connectable): """ def __init__(self, engine, connection=None, close_with_result=False, - _branch=False, _execution_options=None): + _branch=False, _execution_options=None, + _dispatch=None, + _has_events=None): """Construct a new Connection. The constructor here is not public and is only called only by an @@ -881,7 +62,14 @@ class Connection(Connectable): self.__savepoint_seq = 0 self.__branch = _branch self.__invalid = False - self._has_events = engine._has_events + self.__can_reconnect = True + if _dispatch: + self.dispatch = _dispatch + elif engine._has_events: + self.dispatch = self.dispatch._join(engine.dispatch) + self._has_events = _has_events or ( + _has_events is None and engine._has_events) + self._echo = self.engine._should_log_info() if _execution_options: self._execution_options =\ @@ -889,6 +77,9 @@ class Connection(Connectable): else: self._execution_options = engine._execution_options + if self._has_events: + self.dispatch.engine_connect(self, _branch) + def _branch(self): """Return a new Connection which references this Connection's engine and connection; but does not have close_with_result enabled, @@ -900,7 +91,10 @@ class Connection(Connectable): return self.engine._connection_cls( self.engine, - self.__connection, _branch=True) + self.__connection, + _branch=True, + _has_events=self._has_events, + _dispatch=self.dispatch) def _clone(self): """Create a shallow copy of this Connection. @@ -924,16 +118,21 @@ class Connection(Connectable): the same underlying DBAPI connection, but also defines the given execution options which will take effect for a call to :meth:`execute`. As the new :class:`.Connection` references the same - underlying resource, it is probably best to ensure that the copies + underlying resource, it's usually a good idea to ensure that the copies would be discarded immediately, which is implicit if used as in:: result = connection.execution_options(stream_results=True).\\ execute(stmt) - :meth:`.Connection.execution_options` accepts all options as those - accepted by :meth:`.Executable.execution_options`. Additionally, - it includes options that are applicable only to - :class:`.Connection`. + Note that any key/value can be passed to + :meth:`.Connection.execution_options`, and it will be stored in the + ``_execution_options`` dictionary of the :class:`.Connection`. It + is suitable for usage by end-user schemes to communicate with + event listeners, for example. + + The keywords that are currently recognized by SQLAlchemy itself + include all those listed under :meth:`.Executable.execution_options`, + as well as others that are specific to :class:`.Connection`. :param autocommit: Available on: Connection, statement. When True, a COMMIT will be invoked after execution @@ -1005,22 +204,17 @@ class Connection(Connectable): """ c = self._clone() c._execution_options = c._execution_options.union(opt) - if 'isolation_level' in opt: - c._set_isolation_level() + if self._has_events: + self.dispatch.set_connection_execution_options(c, opt) + self.dialect.set_connection_execution_options(c, opt) return c - def _set_isolation_level(self): - self.dialect.set_isolation_level(self.connection, - self._execution_options['isolation_level']) - self.connection._connection_record.finalize_callback = \ - self.dialect.reset_isolation_level - @property def closed(self): """Return True if this connection is closed.""" - return not self.__invalid and '_Connection__connection' \ - not in self.__dict__ + return '_Connection__connection' not in self.__dict__ \ + and not self.__can_reconnect @property def invalidated(self): @@ -1038,7 +232,7 @@ class Connection(Connectable): return self._revalidate_connection() def _revalidate_connection(self): - if self.__invalid: + if self.__can_reconnect and self.__invalid: if self.__transaction is not None: raise exc.InvalidRequestError( "Can't reconnect until invalid " @@ -1065,29 +259,47 @@ class Connection(Connectable): @property def info(self): - """A collection of per-DB-API connection instance properties.""" + """Info dictionary associated with the underlying DBAPI connection + referred to by this :class:`.Connection`, allowing user-defined + data to be associated with the connection. + + The data here will follow along with the DBAPI connection including + after it is returned to the connection pool and used again + in subsequent instances of :class:`.Connection`. + + """ return self.connection.info def connect(self): - """Returns self. + """Returns a branched version of this :class:`.Connection`. + + The :meth:`.Connection.close` method on the returned + :class:`.Connection` can be called and this + :class:`.Connection` will remain open. + + This method provides usage symmetry with + :meth:`.Engine.connect`, including for usage + with context managers. - This ``Connectable`` interface method returns self, allowing - Connections to be used interchangeably with Engines in most - situations that require a bind. """ - return self + return self._branch() def contextual_connect(self, **kwargs): - """Returns self. + """Returns a branched version of this :class:`.Connection`. + + The :meth:`.Connection.close` method on the returned + :class:`.Connection` can be called and this + :class:`.Connection` will remain open. + + This method provides usage symmetry with + :meth:`.Engine.contextual_connect`, including for usage + with context managers. - This ``Connectable`` interface method returns self, allowing - Connections to be used interchangeably with Engines in most - situations that require a bind. """ - return self + return self._branch() def invalidate(self, exception=None): """Invalidate the underlying DBAPI connection associated with @@ -1118,7 +330,6 @@ class Connection(Connectable): del self.__connection self.__invalid = True - def detach(self): """Detach the underlying DB-API connection from its connection pool. @@ -1167,7 +378,8 @@ class Connection(Connectable): :meth:`.Connection.begin_twophase` - use a two phase /XID transaction - :meth:`.Engine.begin` - context manager available from :class:`.Engine`. + :meth:`.Engine.begin` - context manager available from + :class:`.Engine`. """ @@ -1204,8 +416,8 @@ class Connection(Connectable): The returned object is an instance of :class:`.TwoPhaseTransaction`, which in addition to the methods provided by - :class:`.Transaction`, also provides a :meth:`~.TwoPhaseTransaction.prepare` - method. + :class:`.Transaction`, also provides a + :meth:`~.TwoPhaseTransaction.prepare` method. :param xid: the two phase transaction id. If not supplied, a random id will be generated. @@ -1220,7 +432,7 @@ class Connection(Connectable): "Cannot start a two phase transaction when a transaction " "is already in progress.") if xid is None: - xid = self.engine.dialect.create_xid(); + xid = self.engine.dialect.create_xid() self.__transaction = TwoPhaseTransaction(self, xid) return self.__transaction @@ -1243,17 +455,16 @@ class Connection(Connectable): self.engine.logger.info("BEGIN (implicit)") if self._has_events: - self.engine.dispatch.begin(self) + self.dispatch.begin(self) try: self.engine.dialect.do_begin(self.connection) - except Exception, e: + except Exception as e: self._handle_dbapi_exception(e, None, None, None, None) - raise def _rollback_impl(self): if self._has_events: - self.engine.dispatch.rollback(self) + self.dispatch.rollback(self) if self._still_open_and_connection_is_valid: if self._echo: @@ -1261,28 +472,26 @@ class Connection(Connectable): try: self.engine.dialect.do_rollback(self.connection) self.__transaction = None - except Exception, e: + except Exception as e: self._handle_dbapi_exception(e, None, None, None, None) - raise else: self.__transaction = None - def _commit_impl(self): + def _commit_impl(self, autocommit=False): if self._has_events: - self.engine.dispatch.commit(self) + self.dispatch.commit(self) if self._echo: self.engine.logger.info("COMMIT") try: self.engine.dialect.do_commit(self.connection) self.__transaction = None - except Exception, e: + except Exception as e: self._handle_dbapi_exception(e, None, None, None, None) - raise def _savepoint_impl(self, name=None): if self._has_events: - self.engine.dispatch.savepoint(self, name) + self.dispatch.savepoint(self, name) if name is None: self.__savepoint_seq += 1 @@ -1293,7 +502,7 @@ class Connection(Connectable): def _rollback_to_savepoint_impl(self, name, context): if self._has_events: - self.engine.dispatch.rollback_savepoint(self, name, context) + self.dispatch.rollback_savepoint(self, name, context) if self._still_open_and_connection_is_valid: self.engine.dialect.do_rollback_to_savepoint(self, name) @@ -1301,22 +510,24 @@ class Connection(Connectable): def _release_savepoint_impl(self, name, context): if self._has_events: - self.engine.dispatch.release_savepoint(self, name, context) + self.dispatch.release_savepoint(self, name, context) if self._still_open_and_connection_is_valid: self.engine.dialect.do_release_savepoint(self, name) self.__transaction = context def _begin_twophase_impl(self, xid): + if self._echo: + self.engine.logger.info("BEGIN TWOPHASE (implicit)") if self._has_events: - self.engine.dispatch.begin_twophase(self, xid) + self.dispatch.begin_twophase(self, xid) if self._still_open_and_connection_is_valid: self.engine.dialect.do_begin_twophase(self, xid) def _prepare_twophase_impl(self, xid): if self._has_events: - self.engine.dispatch.prepare_twophase(self, xid) + self.dispatch.prepare_twophase(self, xid) if self._still_open_and_connection_is_valid: assert isinstance(self.__transaction, TwoPhaseTransaction) @@ -1324,7 +535,7 @@ class Connection(Connectable): def _rollback_twophase_impl(self, xid, is_prepared): if self._has_events: - self.engine.dispatch.rollback_twophase(self, xid, is_prepared) + self.dispatch.rollback_twophase(self, xid, is_prepared) if self._still_open_and_connection_is_valid: assert isinstance(self.__transaction, TwoPhaseTransaction) @@ -1333,7 +544,7 @@ class Connection(Connectable): def _commit_twophase_impl(self, xid, is_prepared): if self._has_events: - self.engine.dispatch.commit_twophase(self, xid, is_prepared) + self.dispatch.commit_twophase(self, xid, is_prepared) if self._still_open_and_connection_is_valid: assert isinstance(self.__transaction, TwoPhaseTransaction) @@ -1363,15 +574,15 @@ class Connection(Connectable): and will allow no further operations. """ - try: conn = self.__connection except AttributeError: - return - if not self.__branch: - conn.close() - self.__invalid = False - del self.__connection + pass + else: + if not self.__branch: + conn.close() + del self.__connection + self.__can_reconnect = False self.__transaction = None def scalar(self, object, *multiparams, **params): @@ -1383,7 +594,8 @@ class Connection(Connectable): return self.execute(object, *multiparams, **params).scalar() def execute(self, object, *multiparams, **params): - """Executes the a SQL statement construct and returns a :class:`.ResultProxy`. + """Executes the a SQL statement construct and returns a + :class:`.ResultProxy`. :param object: The statement to be executed. May be one of: @@ -1440,51 +652,16 @@ class Connection(Connectable): DBAPI-agnostic way, use the :func:`~.expression.text` construct. """ - for c in type(object).__mro__: - if c in Connection.executors: - return Connection.executors[c]( - self, - object, - multiparams, - params) - else: + if isinstance(object, util.string_types[0]): + return self._execute_text(object, multiparams, params) + try: + meth = object._execute_on_connection + except AttributeError: raise exc.InvalidRequestError( "Unexecutable object type: %s" % type(object)) - - def __distill_params(self, multiparams, params): - """Given arguments from the calling form *multiparams, **params, - return a list of bind parameter structures, usually a list of - dictionaries. - - In the case of 'raw' execution which accepts positional parameters, - it may be a list of tuples or lists. - - """ - - if not multiparams: - if params: - return [params] - else: - return [] - elif len(multiparams) == 1: - zero = multiparams[0] - if isinstance(zero, (list, tuple)): - if not zero or hasattr(zero[0], '__iter__') and \ - not hasattr(zero[0], 'strip'): - return zero - else: - return [zero] - elif hasattr(zero, 'keys'): - return [zero] - else: - return [[zero]] else: - if hasattr(multiparams[0], '__iter__') and \ - not hasattr(multiparams[0], 'strip'): - return multiparams - else: - return [multiparams] + return meth(self, multiparams, params) def _execute_function(self, func, multiparams, params): """Execute a sql.FunctionElement object.""" @@ -1496,7 +673,7 @@ class Connection(Connectable): """Execute a schema.ColumnDefault object.""" if self._has_events: - for fn in self.engine.dispatch.before_execute: + for fn in self.dispatch.before_execute: default, multiparams, params = \ fn(self, default, multiparams, params) @@ -1509,16 +686,15 @@ class Connection(Connectable): dialect = self.dialect ctx = dialect.execution_ctx_cls._init_default( dialect, self, conn) - except Exception, e: + except Exception as e: self._handle_dbapi_exception(e, None, None, None, None) - raise ret = ctx._exec_default(default, None) if self.should_close_with_result: self.close() if self._has_events: - self.engine.dispatch.after_execute(self, + self.dispatch.after_execute(self, default, multiparams, params, ret) return ret @@ -1527,7 +703,7 @@ class Connection(Connectable): """Execute a schema.DDL object.""" if self._has_events: - for fn in self.engine.dispatch.before_execute: + for fn in self.dispatch.before_execute: ddl, multiparams, params = \ fn(self, ddl, multiparams, params) @@ -1542,7 +718,7 @@ class Connection(Connectable): compiled ) if self._has_events: - self.engine.dispatch.after_execute(self, + self.dispatch.after_execute(self, ddl, multiparams, params, ret) return ret @@ -1550,12 +726,14 @@ class Connection(Connectable): """Execute a sql.ClauseElement object.""" if self._has_events: - for fn in self.engine.dispatch.before_execute: + for fn in self.dispatch.before_execute: elem, multiparams, params = \ fn(self, elem, multiparams, params) - distilled_params = self.__distill_params(multiparams, params) + distilled_params = _distill_params(multiparams, params) if distilled_params: + # note this is usually dict but we support RowProxy + # as well; but dict.keys() as an iterator is OK keys = distilled_params[0].keys() else: keys = [] @@ -1575,7 +753,6 @@ class Connection(Connectable): dialect=dialect, column_keys=keys, inline=len(distilled_params) > 1) - ret = self._execute_context( dialect, dialect.execution_ctx_cls._init_compiled, @@ -1584,7 +761,7 @@ class Connection(Connectable): compiled_sql, distilled_params ) if self._has_events: - self.engine.dispatch.after_execute(self, + self.dispatch.after_execute(self, elem, multiparams, params, ret) return ret @@ -1592,12 +769,12 @@ class Connection(Connectable): """Execute a sql.Compiled object.""" if self._has_events: - for fn in self.engine.dispatch.before_execute: + for fn in self.dispatch.before_execute: compiled, multiparams, params = \ fn(self, compiled, multiparams, params) dialect = self.dialect - parameters=self.__distill_params(multiparams, params) + parameters = _distill_params(multiparams, params) ret = self._execute_context( dialect, dialect.execution_ctx_cls._init_compiled, @@ -1606,7 +783,7 @@ class Connection(Connectable): compiled, parameters ) if self._has_events: - self.engine.dispatch.after_execute(self, + self.dispatch.after_execute(self, compiled, multiparams, params, ret) return ret @@ -1614,12 +791,12 @@ class Connection(Connectable): """Execute a string SQL statement.""" if self._has_events: - for fn in self.engine.dispatch.before_execute: + for fn in self.dispatch.before_execute: statement, multiparams, params = \ fn(self, statement, multiparams, params) dialect = self.dialect - parameters = self.__distill_params(multiparams, params) + parameters = _distill_params(multiparams, params) ret = self._execute_context( dialect, dialect.execution_ctx_cls._init_statement, @@ -1628,7 +805,7 @@ class Connection(Connectable): statement, parameters ) if self._has_events: - self.engine.dispatch.after_execute(self, + self.dispatch.after_execute(self, statement, multiparams, params, ret) return ret @@ -1645,11 +822,10 @@ class Connection(Connectable): conn = self._revalidate_connection() context = constructor(dialect, self, conn, *args) - except Exception, e: + except Exception as e: self._handle_dbapi_exception(e, - str(statement), parameters, + util.text_type(statement), parameters, None, None) - raise if context.compiled: context.pre_exec() @@ -1662,7 +838,7 @@ class Connection(Connectable): parameters = parameters[0] if self._has_events: - for fn in self.engine.dispatch.before_cursor_execute: + for fn in self.dispatch.before_cursor_execute: statement, parameters = \ fn(self, cursor, statement, parameters, context, context.executemany) @@ -1689,18 +865,16 @@ class Connection(Connectable): statement, parameters, context) - except Exception, e: + except Exception as e: self._handle_dbapi_exception( e, statement, parameters, cursor, context) - raise - if self._has_events: - self.engine.dispatch.after_cursor_execute(self, cursor, + self.dispatch.after_cursor_execute(self, cursor, statement, parameters, context, @@ -1719,8 +893,15 @@ class Connection(Connectable): if context._is_implicit_returning: context._fetch_implicit_returning(result) result.close(_autoclose_connection=False) + result._metadata = None elif not context._is_explicit_returning: result.close(_autoclose_connection=False) + result._metadata = None + elif context.isupdate and context._is_implicit_returning: + context._fetch_implicit_update_returning(result) + result.close(_autoclose_connection=False) + result._metadata = None + elif result._metadata is None: # no results, get rowcount # (which requires open cursor on some drivers @@ -1729,14 +910,14 @@ class Connection(Connectable): result.close(_autoclose_connection=False) if self.__transaction is None and context.should_autocommit: - self._commit_impl() + self._commit_impl(autocommit=True) if result.closed and self.should_close_with_result: self.close() return result - def _cursor_execute(self, cursor, statement, parameters): + def _cursor_execute(self, cursor, statement, parameters, context=None): """Execute a statement + params on the given cursor. Adds appropriate logging and exception handling. @@ -1747,6 +928,14 @@ class Connection(Connectable): terminates at _execute_context(). """ + if self._has_events: + for fn in self.dispatch.before_cursor_execute: + statement, parameters = \ + fn(self, cursor, statement, parameters, + context, + context.executemany + if context is not None else False) + if self._echo: self.engine.logger.info(statement) self.engine.logger.info("%r", parameters) @@ -1755,14 +944,13 @@ class Connection(Connectable): cursor, statement, parameters) - except Exception, e: + except Exception as e: self._handle_dbapi_exception( e, statement, parameters, cursor, None) - raise def _safe_close_cursor(self, cursor): """Close the given cursor, catching exceptions @@ -1771,15 +959,14 @@ class Connection(Connectable): """ try: cursor.close() - except Exception, e: - try: - ex_text = str(e) - except TypeError: - ex_text = repr(e) - self.connection._logger.warn("Error closing cursor: %s", ex_text) + except (SystemExit, KeyboardInterrupt): + raise + except Exception: + self.connection._logger.error( + "Error closing cursor", exc_info=True) - if isinstance(e, (SystemExit, KeyboardInterrupt)): - raise + _reentrant_error = False + _is_disconnect = False def _handle_dbapi_exception(self, e, @@ -1787,17 +974,22 @@ class Connection(Connectable): parameters, cursor, context): - if getattr(self, '_reentrant_error', False): - # Py3K - #raise exc.DBAPIError.instance(statement, parameters, e, - # self.dialect.dbapi.Error) from e - # Py2K - raise exc.DBAPIError.instance(statement, + + exc_info = sys.exc_info() + + if not self._is_disconnect: + self._is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ + not self.closed and \ + self.dialect.is_disconnect(e, self.__connection, cursor) + + if self._reentrant_error: + util.raise_from_cause( + exc.DBAPIError.instance(statement, parameters, e, - self.dialect.dbapi.Error), \ - None, sys.exc_info()[2] - # end Py2K + self.dialect.dbapi.Error), + exc_info + ) self._reentrant_error = True try: # non-DBAPI error - if we already got a context, @@ -1807,7 +999,7 @@ class Connection(Connectable): if should_wrap and context: if self._has_events: - self.engine.dispatch.dbapi_error(self, + self.dispatch.dbapi_error(self, cursor, statement, parameters, @@ -1815,88 +1007,35 @@ class Connection(Connectable): e) context.handle_dbapi_exception(e) - is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ - self.dialect.is_disconnect(e, self.__connection, cursor) - - - if is_disconnect: - self.invalidate(e) - self.engine.dispose() - else: + if not self._is_disconnect: if cursor: self._safe_close_cursor(cursor) self._autorollback() - if self.should_close_with_result: - self.close() - if not should_wrap: - return + if should_wrap: + util.raise_from_cause( + exc.DBAPIError.instance( + statement, + parameters, + e, + self.dialect.dbapi.Error, + connection_invalidated=self._is_disconnect), + exc_info + ) - # Py3K - #raise exc.DBAPIError.instance( - # statement, - # parameters, - # e, - # self.dialect.dbapi.Error, - # connection_invalidated=is_disconnect) \ - # from e - # Py2K - raise exc.DBAPIError.instance( - statement, - parameters, - e, - self.dialect.dbapi.Error, - connection_invalidated=is_disconnect), \ - None, sys.exc_info()[2] - # end Py2K + util.reraise(*exc_info) finally: del self._reentrant_error - - # poor man's multimethod/generic function thingy - executors = { - expression.FunctionElement: _execute_function, - expression.ClauseElement: _execute_clauseelement, - Compiled: _execute_compiled, - schema.SchemaItem: _execute_default, - schema.DDLElement: _execute_ddl, - basestring: _execute_text - } - - @util.deprecated("0.7", "Use the create() method on the given schema " - "object directly, i.e. :meth:`.Table.create`, " - ":meth:`.Index.create`, :meth:`.MetaData.create_all`") - def create(self, entity, **kwargs): - """Emit CREATE statements for the given schema entity.""" - - return self.engine.create(entity, connection=self, **kwargs) - - @util.deprecated("0.7", "Use the drop() method on the given schema " - "object directly, i.e. :meth:`.Table.drop`, " - ":meth:`.Index.drop`, :meth:`.MetaData.drop_all`") - def drop(self, entity, **kwargs): - """Emit DROP statements for the given schema entity.""" - - return self.engine.drop(entity, connection=self, **kwargs) - - @util.deprecated("0.7", "Use autoload=True with :class:`.Table`, " - "or use the :class:`.Inspector` object.") - def reflecttable(self, table, include_columns=None): - """Load table description from the database. - - Given a :class:`.Table` object, reflect its columns and - properties from the database, populating the given :class:`.Table` - object with attributes.. If include_columns (a list or - set) is specified, limit the autoload to the given column - names. - - The default implementation uses the - :class:`.Inspector` interface to - provide the output, building upon the granular table/column/ - constraint etc. methods of :class:`.Dialect`. - - """ - return self.engine.reflecttable(table, self, include_columns) + if self._is_disconnect: + del self._is_disconnect + dbapi_conn_wrapper = self.connection + self.invalidate(e) + if not hasattr(dbapi_conn_wrapper, '_pool') or \ + dbapi_conn_wrapper._pool is self.engine.pool: + self.engine.dispose() + if self.should_close_with_result: + self.close() def default_schema_name(self): return self.engine.dialect.get_default_schema_name(self) @@ -1949,8 +1088,8 @@ class Connection(Connectable): trans.commit() return ret except: - trans.rollback() - raise + with util.safe_reraise(): + trans.rollback() def run_callable(self, callable_, *args, **kwargs): """Given a callable object or function, execute it, passing @@ -2056,11 +1195,12 @@ class Transaction(object): try: self.commit() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() else: self.rollback() + class RootTransaction(Transaction): def __init__(self, connection): super(RootTransaction, self).__init__(connection, None) @@ -2136,15 +1276,15 @@ class TwoPhaseTransaction(Transaction): class Engine(Connectable, log.Identified): """ Connects a :class:`~sqlalchemy.pool.Pool` and - :class:`~sqlalchemy.engine.base.Dialect` together to provide a source - of database connectivity and behavior. + :class:`~sqlalchemy.engine.interfaces.Dialect` together to provide a + source of database connectivity and behavior. An :class:`.Engine` object is instantiated publicly using the :func:`~sqlalchemy.create_engine` function. See also: - :ref:`engines_toplevel` + :doc:`/core/engines` :ref:`connections_toplevel` @@ -2161,6 +1301,7 @@ class Engine(Connectable, log.Identified): self.pool = pool self.url = url self.dialect = dialect + self.pool._dialect = dialect if logging_name: self.logging_name = logging_name self.echo = echo @@ -2169,17 +1310,8 @@ class Engine(Connectable, log.Identified): if proxy: interfaces.ConnectionProxy._adapt_listener(self, proxy) if execution_options: - if 'isolation_level' in execution_options: - raise exc.ArgumentError( - "'isolation_level' execution option may " - "only be specified on Connection.execution_options(). " - "To set engine-wide isolation level, " - "use the isolation_level argument to create_engine()." - ) self.update_execution_options(**execution_options) - dispatch = event.dispatcher(events.ConnectionEvents) - def update_execution_options(self, **opt): """Update the default execution_options dictionary of this :class:`.Engine`. @@ -2190,31 +1322,102 @@ class Engine(Connectable, log.Identified): can be sent via the ``execution_options`` parameter to :func:`.create_engine`. - See :meth:`.Connection.execution_options` for more - details on execution options. + .. seealso:: + + :meth:`.Connection.execution_options` + + :meth:`.Engine.execution_options` """ self._execution_options = \ self._execution_options.union(opt) + self.dispatch.set_engine_execution_options(self, opt) + self.dialect.set_engine_execution_options(self, opt) + + def execution_options(self, **opt): + """Return a new :class:`.Engine` that will provide + :class:`.Connection` objects with the given execution options. + + The returned :class:`.Engine` remains related to the original + :class:`.Engine` in that it shares the same connection pool and + other state: + + * The :class:`.Pool` used by the new :class:`.Engine` is the + same instance. The :meth:`.Engine.dispose` method will replace + the connection pool instance for the parent engine as well + as this one. + * Event listeners are "cascaded" - meaning, the new :class:`.Engine` + inherits the events of the parent, and new events can be associated + with the new :class:`.Engine` individually. + * The logging configuration and logging_name is copied from the parent + :class:`.Engine`. + + The intent of the :meth:`.Engine.execution_options` method is + to implement "sharding" schemes where multiple :class:`.Engine` + objects refer to the same connection pool, but are differentiated + by options that would be consumed by a custom event:: + + primary_engine = create_engine("mysql://") + shard1 = primary_engine.execution_options(shard_id="shard1") + shard2 = primary_engine.execution_options(shard_id="shard2") + + Above, the ``shard1`` engine serves as a factory for + :class:`.Connection` objects that will contain the execution option + ``shard_id=shard1``, and ``shard2`` will produce :class:`.Connection` + objects that contain the execution option ``shard_id=shard2``. + + An event handler can consume the above execution option to perform + a schema switch or other operation, given a connection. Below + we emit a MySQL ``use`` statement to switch databases, at the same + time keeping track of which database we've established using the + :attr:`.Connection.info` dictionary, which gives us a persistent + storage space that follows the DBAPI connection:: + + from sqlalchemy import event + from sqlalchemy.engine import Engine + + shards = {"default": "base", shard_1: "db1", "shard_2": "db2"} + + @event.listens_for(Engine, "before_cursor_execute") + def _switch_shard(conn, cursor, stmt, params, context, executemany): + shard_id = conn._execution_options.get('shard_id', "default") + current_shard = conn.info.get("current_shard", None) + + if current_shard != shard_id: + cursor.execute("use %s" % shards[shard_id]) + conn.info["current_shard"] = shard_id + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.Connection.execution_options` - update execution options + on a :class:`.Connection` object. + + :meth:`.Engine.update_execution_options` - update the execution + options for a given :class:`.Engine` in place. + + """ + return OptionEngine(self, opt) @property def name(self): - """String name of the :class:`~sqlalchemy.engine.Dialect` in use by - this ``Engine``.""" + """String name of the :class:`~sqlalchemy.engine.interfaces.Dialect` + in use by this :class:`Engine`.""" return self.dialect.name @property def driver(self): - """Driver name of the :class:`~sqlalchemy.engine.Dialect` in use by - this ``Engine``.""" + """Driver name of the :class:`~sqlalchemy.engine.interfaces.Dialect` + in use by this :class:`Engine`.""" return self.dialect.driver echo = log.echo_property() def __repr__(self): - return 'Engine(%s)' % str(self.url) + return 'Engine(%r)' % self.url def dispose(self): """Dispose of the connection pool used by this :class:`.Engine`. @@ -2240,69 +1443,24 @@ class Engine(Connectable, log.Identified): the engine are not affected. """ - self.pool.dispose() self.pool = self.pool._replace() - @util.deprecated("0.7", "Use the create() method on the given schema " - "object directly, i.e. :meth:`.Table.create`, " - ":meth:`.Index.create`, :meth:`.MetaData.create_all`") - def create(self, entity, connection=None, **kwargs): - """Emit CREATE statements for the given schema entity.""" - - from sqlalchemy.engine import ddl - - self._run_visitor(ddl.SchemaGenerator, entity, - connection=connection, **kwargs) - - @util.deprecated("0.7", "Use the drop() method on the given schema " - "object directly, i.e. :meth:`.Table.drop`, " - ":meth:`.Index.drop`, :meth:`.MetaData.drop_all`") - def drop(self, entity, connection=None, **kwargs): - """Emit DROP statements for the given schema entity.""" - - from sqlalchemy.engine import ddl - - self._run_visitor(ddl.SchemaDropper, entity, - connection=connection, **kwargs) - def _execute_default(self, default): - connection = self.contextual_connect() - try: - return connection._execute_default(default, (), {}) - finally: - connection.close() + with self.contextual_connect() as conn: + return conn._execute_default(default, (), {}) - @property - @util.deprecated("0.7", - "Use :attr:`~sqlalchemy.sql.expression.func` to create function constructs.") - def func(self): - return expression._FunctionGenerator(bind=self) - - @util.deprecated("0.7", - "Use :func:`.expression.text` to create text constructs.") - def text(self, text, *args, **kwargs): - """Return a :func:`~sqlalchemy.sql.expression.text` construct, - bound to this engine. - - This is equivalent to:: - - text("SELECT * FROM table", bind=engine) - - """ - - return expression.text(text, bind=self, *args, **kwargs) + @contextlib.contextmanager + def _optional_conn_ctx_manager(self, connection=None): + if connection is None: + with self.contextual_connect() as conn: + yield conn + else: + yield connection def _run_visitor(self, visitorcallable, element, connection=None, **kwargs): - if connection is None: - conn = self.contextual_connect(close_with_result=False) - else: - conn = connection - try: + with self._optional_conn_ctx_manager(connection) as conn: conn._run_visitor(visitorcallable, element, **kwargs) - finally: - if connection is None: - conn.close() class _trans_ctx(object): def __init__(self, conn, transaction, close_with_result): @@ -2337,11 +1495,11 @@ class Engine(Connectable, log.Identified): The ``close_with_result`` flag is normally ``False``, and indicates that the :class:`.Connection` will be closed when the operation - is complete. When set to ``True``, it indicates the :class:`.Connection` - is in "single use" mode, where the :class:`.ResultProxy` - returned by the first call to :meth:`.Connection.execute` will - close the :class:`.Connection` when that :class:`.ResultProxy` - has exhausted all result rows. + is complete. When set to ``True``, it indicates the + :class:`.Connection` is in "single use" mode, where the + :class:`.ResultProxy` returned by the first call to + :meth:`.Connection.execute` will close the :class:`.Connection` when + that :class:`.ResultProxy` has exhausted all result rows. .. versionadded:: 0.7.6 @@ -2358,8 +1516,8 @@ class Engine(Connectable, log.Identified): try: trans = conn.begin() except: - conn.close() - raise + with util.safe_reraise(): + conn.close() return Engine._trans_ctx(conn, trans, close_with_result) def transaction(self, callable_, *args, **kwargs): @@ -2401,11 +1559,8 @@ class Engine(Connectable, log.Identified): """ - conn = self.contextual_connect() - try: + with self.contextual_connect() as conn: return conn.transaction(callable_, *args, **kwargs) - finally: - conn.close() def run_callable(self, callable_, *args, **kwargs): """Given a callable object or function, execute it, passing @@ -2420,11 +1575,8 @@ class Engine(Connectable, log.Identified): which one is being dealt with. """ - conn = self.contextual_connect() - try: + with self.contextual_connect() as conn: return conn.run_callable(callable_, *args, **kwargs) - finally: - conn.close() def execute(self, statement, *multiparams, **params): """Executes the given construct and returns a :class:`.ResultProxy`. @@ -2459,29 +1611,33 @@ class Engine(Connectable, log.Identified): def connect(self, **kwargs): """Return a new :class:`.Connection` object. - The :class:`.Connection` object is a facade that uses a DBAPI connection internally - in order to communicate with the database. This connection is procured - from the connection-holding :class:`.Pool` referenced by this :class:`.Engine`. - When the :meth:`~.Connection.close` method of the :class:`.Connection` object is called, - the underlying DBAPI connection is then returned to the connection pool, - where it may be used again in a subsequent call to :meth:`~.Engine.connect`. + The :class:`.Connection` object is a facade that uses a DBAPI + connection internally in order to communicate with the database. This + connection is procured from the connection-holding :class:`.Pool` + referenced by this :class:`.Engine`. When the + :meth:`~.Connection.close` method of the :class:`.Connection` object + is called, the underlying DBAPI connection is then returned to the + connection pool, where it may be used again in a subsequent call to + :meth:`~.Engine.connect`. """ return self._connection_cls(self, **kwargs) def contextual_connect(self, close_with_result=False, **kwargs): - """Return a :class:`.Connection` object which may be part of some ongoing context. + """Return a :class:`.Connection` object which may be part of some + ongoing context. By default, this method does the same thing as :meth:`.Engine.connect`. Subclasses of :class:`.Engine` may override this method to provide contextual behavior. - :param close_with_result: When True, the first :class:`.ResultProxy` created - by the :class:`.Connection` will call the :meth:`.Connection.close` method - of that connection as soon as any pending result rows are exhausted. - This is used to supply the "connectionless execution" behavior provided - by the :meth:`.Engine.execute` method. + :param close_with_result: When True, the first :class:`.ResultProxy` + created by the :class:`.Connection` will call the + :meth:`.Connection.close` method of that connection as soon as any + pending result rows are exhausted. This is used to supply the + "connectionless execution" behavior provided by the + :meth:`.Engine.execute` method. """ @@ -2499,42 +1655,23 @@ class Engine(Connectable, log.Identified): the ``contextual_connect`` for this ``Engine``. """ - if connection is None: - conn = self.contextual_connect() - else: - conn = connection - if not schema: - schema = self.dialect.default_schema_name - try: + with self._optional_conn_ctx_manager(connection) as conn: + if not schema: + schema = self.dialect.default_schema_name return self.dialect.get_table_names(conn, schema) - finally: - if connection is None: - conn.close() - - @util.deprecated("0.7", "Use autoload=True with :class:`.Table`, " - "or use the :class:`.Inspector` object.") - def reflecttable(self, table, connection=None, include_columns=None): - """Load table description from the database. - - Uses the given :class:`.Connection`, or if None produces - its own :class:`.Connection`, and passes the ``table`` - and ``include_columns`` arguments onto that - :class:`.Connection` object's :meth:`.Connection.reflecttable` - method. The :class:`.Table` object is then populated - with new attributes. - - """ - if connection is None: - conn = self.contextual_connect() - else: - conn = connection - try: - self.dialect.reflecttable(conn, table, include_columns) - finally: - if connection is None: - conn.close() def has_table(self, table_name, schema=None): + """Return True if the given backend has a table of the given name. + + .. seealso:: + + :ref:`metadata_reflection_inspector` - detailed schema inspection using + the :class:`.Inspector` interface. + + :class:`.quoted_name` - used to pass quoting information along + with a schema identifier. + + """ return self.run_callable(self.dialect.has_table, table_name, schema) def raw_connection(self): @@ -2557,917 +1694,31 @@ class Engine(Connectable, log.Identified): return self.pool.unique_connection() -# This reconstructor is necessary so that pickles with the C extension or -# without use the same Binary format. -try: - # We need a different reconstructor on the C extension so that we can - # add extra checks that fields have correctly been initialized by - # __setstate__. - from sqlalchemy.cresultproxy import safe_rowproxy_reconstructor - - # The extra function embedding is needed so that the - # reconstructor function has the same signature whether or not - # the extension is present. - def rowproxy_reconstructor(cls, state): - return safe_rowproxy_reconstructor(cls, state) -except ImportError: - def rowproxy_reconstructor(cls, state): - obj = cls.__new__(cls) - obj.__setstate__(state) - return obj - -try: - from sqlalchemy.cresultproxy import BaseRowProxy -except ImportError: - class BaseRowProxy(object): - __slots__ = ('_parent', '_row', '_processors', '_keymap') - - def __init__(self, parent, row, processors, keymap): - """RowProxy objects are constructed by ResultProxy objects.""" - - self._parent = parent - self._row = row - self._processors = processors - self._keymap = keymap - - def __reduce__(self): - return (rowproxy_reconstructor, - (self.__class__, self.__getstate__())) - - def values(self): - """Return the values represented by this RowProxy as a list.""" - return list(self) - - def __iter__(self): - for processor, value in izip(self._processors, self._row): - if processor is None: - yield value - else: - yield processor(value) - - def __len__(self): - return len(self._row) - - def __getitem__(self, key): - try: - processor, obj, index = self._keymap[key] - except KeyError: - processor, obj, index = self._parent._key_fallback(key) - except TypeError: - if isinstance(key, slice): - l = [] - for processor, value in izip(self._processors[key], - self._row[key]): - if processor is None: - l.append(value) - else: - l.append(processor(value)) - return tuple(l) - else: - raise - if index is None: - raise exc.InvalidRequestError( - "Ambiguous column name '%s' in result set! " - "try 'use_labels' option on select statement." % key) - if processor is not None: - return processor(self._row[index]) - else: - return self._row[index] - - def __getattr__(self, name): - try: - return self[name] - except KeyError, e: - raise AttributeError(e.args[0]) - - -class RowProxy(BaseRowProxy): - """Proxy values from a single cursor row. - - Mostly follows "ordered dictionary" behavior, mapping result - values to the string-based column name, the integer position of - the result in the row, as well as Column instances which can be - mapped to the original Columns that produced this result set (for - results that correspond to constructed SQL expressions). - """ - __slots__ = () - - def __contains__(self, key): - return self._parent._has_key(self._row, key) - - def __getstate__(self): - return { - '_parent': self._parent, - '_row': tuple(self) - } - - def __setstate__(self, state): - self._parent = parent = state['_parent'] - self._row = state['_row'] - self._processors = parent._processors - self._keymap = parent._keymap - - __hash__ = None - - def __eq__(self, other): - return other is self or other == tuple(self) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return repr(tuple(self)) - - def has_key(self, key): - """Return True if this RowProxy contains the given key.""" - - return self._parent._has_key(self._row, key) - - def items(self): - """Return a list of tuples, each tuple containing a key/value pair.""" - # TODO: no coverage here - return [(key, self[key]) for key in self.iterkeys()] - - def keys(self): - """Return the list of keys as strings represented by this RowProxy.""" - - return self._parent.keys - - def iterkeys(self): - return iter(self._parent.keys) - - def itervalues(self): - return iter(self) - -try: - # Register RowProxy with Sequence, - # so sequence protocol is implemented - from collections import Sequence - Sequence.register(RowProxy) -except ImportError: - pass - - -class ResultMetaData(object): - """Handle cursor.description, applying additional info from an execution - context.""" - - def __init__(self, parent, metadata): - self._processors = processors = [] - - # We do not strictly need to store the processor in the key mapping, - # though it is faster in the Python version (probably because of the - # saved attribute lookup self._processors) - self._keymap = keymap = {} - self.keys = [] - context = parent.context - dialect = context.dialect - typemap = dialect.dbapi_type_map - translate_colname = context._translate_colname - - # high precedence key values. - primary_keymap = {} - - for i, rec in enumerate(metadata): - colname = rec[0] - coltype = rec[1] - - if dialect.description_encoding: - colname = dialect._description_decoder(colname) - - if translate_colname: - colname, untranslated = translate_colname(colname) - - if context.result_map: - try: - name, obj, type_ = context.result_map[colname.lower()] - except KeyError: - name, obj, type_ = \ - colname, None, typemap.get(coltype, types.NULLTYPE) - else: - name, obj, type_ = \ - colname, None, typemap.get(coltype, types.NULLTYPE) - - processor = type_._cached_result_processor(dialect, coltype) - - processors.append(processor) - rec = (processor, obj, i) - - # indexes as keys. This is only needed for the Python version of - # RowProxy (the C version uses a faster path for integer indexes). - primary_keymap[i] = rec - - # populate primary keymap, looking for conflicts. - if primary_keymap.setdefault(name.lower(), rec) is not rec: - # place a record that doesn't have the "index" - this - # is interpreted later as an AmbiguousColumnError, - # but only when actually accessed. Columns - # colliding by name is not a problem if those names - # aren't used; integer and ColumnElement access is always - # unambiguous. - primary_keymap[name.lower()] = (processor, obj, None) - - if dialect.requires_name_normalize: - colname = dialect.normalize_name(colname) - - self.keys.append(colname) - if obj: - for o in obj: - keymap[o] = rec - - if translate_colname and \ - untranslated: - keymap[untranslated] = rec - - # overwrite keymap values with those of the - # high precedence keymap. - keymap.update(primary_keymap) - - if parent._echo: - context.engine.logger.debug( - "Col %r", tuple(x[0] for x in metadata)) - - @util.pending_deprecation("0.8", "sqlite dialect uses " - "_translate_colname() now") - def _set_keymap_synonym(self, name, origname): - """Set a synonym for the given name. - - Some dialects (SQLite at the moment) may use this to - adjust the column names that are significant within a - row. - - """ - rec = (processor, obj, i) = self._keymap[origname.lower()] - if self._keymap.setdefault(name, rec) is not rec: - self._keymap[name] = (processor, obj, None) - - def _key_fallback(self, key, raiseerr=True): - map = self._keymap - result = None - if isinstance(key, basestring): - result = map.get(key.lower()) - # fallback for targeting a ColumnElement to a textual expression - # this is a rare use case which only occurs when matching text() - # or colummn('name') constructs to ColumnElements, or after a - # pickle/unpickle roundtrip - elif isinstance(key, expression.ColumnElement): - if key._label and key._label.lower() in map: - result = map[key._label.lower()] - elif hasattr(key, 'name') and key.name.lower() in map: - # match is only on name. - result = map[key.name.lower()] - # search extra hard to make sure this - # isn't a column/label name overlap. - # this check isn't currently available if the row - # was unpickled. - if result is not None and \ - result[1] is not None: - for obj in result[1]: - if key._compare_name_for_result(obj): - break - else: - result = None - if result is None: - if raiseerr: - raise exc.NoSuchColumnError( - "Could not locate column in row for column '%s'" % - expression._string_or_unprintable(key)) - else: - return None - else: - map[key] = result - return result - - def _has_key(self, row, key): - if key in self._keymap: - return True - else: - return self._key_fallback(key, False) is not None - - def __getstate__(self): - return { - '_pickled_keymap': dict( - (key, index) - for key, (processor, obj, index) in self._keymap.iteritems() - if isinstance(key, (basestring, int)) - ), - 'keys': self.keys - } - - def __setstate__(self, state): - # the row has been processed at pickling time so we don't need any - # processor anymore - self._processors = [None for _ in xrange(len(state['keys']))] - self._keymap = keymap = {} - for key, index in state['_pickled_keymap'].iteritems(): - # not preserving "obj" here, unfortunately our - # proxy comparison fails with the unpickle - keymap[key] = (None, None, index) - self.keys = state['keys'] - self._echo = False - - -class ResultProxy(object): - """Wraps a DB-API cursor object to provide easier access to row columns. - - Individual columns may be accessed by their integer position, - case-insensitive column name, or by ``schema.Column`` - object. e.g.:: - - row = fetchone() - - col1 = row[0] # access via integer position - - col2 = row['col2'] # access via name - - col3 = row[mytable.c.mycol] # access via Column object. - - ``ResultProxy`` also handles post-processing of result column - data using ``TypeEngine`` objects, which are referenced from - the originating SQL statement that produced this result set. - - """ - - _process_row = RowProxy - out_parameters = None - _can_close_connection = False - - def __init__(self, context): - self.context = context - self.dialect = context.dialect - self.closed = False - self.cursor = self._saved_cursor = context.cursor - self.connection = context.root_connection - self._echo = self.connection._echo and \ - context.engine._should_log_debug() - self._init_metadata() - - def _init_metadata(self): - metadata = self._cursor_description() - if metadata is None: - self._metadata = None - else: - self._metadata = ResultMetaData(self, metadata) - - def keys(self): - """Return the current set of string keys for rows.""" - if self._metadata: - return self._metadata.keys - else: - return [] - - @util.memoized_property - def rowcount(self): - """Return the 'rowcount' for this result. - - The 'rowcount' reports the number of rows *matched* - by the WHERE criterion of an UPDATE or DELETE statement. - - .. note:: - - Notes regarding :attr:`.ResultProxy.rowcount`: - - - * This attribute returns the number of rows *matched*, - which is not necessarily the same as the number of rows - that were actually *modified* - an UPDATE statement, for example, - may have no net change on a given row if the SET values - given are the same as those present in the row already. - Such a row would be matched but not modified. - On backends that feature both styles, such as MySQL, - rowcount is configured by default to return the match - count in all cases. - - * :attr:`.ResultProxy.rowcount` is *only* useful in conjunction - with an UPDATE or DELETE statement. Contrary to what the Python - DBAPI says, it does *not* return the - number of rows available from the results of a SELECT statement - as DBAPIs cannot support this functionality when rows are - unbuffered. - - * :attr:`.ResultProxy.rowcount` may not be fully implemented by - all dialects. In particular, most DBAPIs do not support an - aggregate rowcount result from an executemany call. - The :meth:`.ResultProxy.supports_sane_rowcount` and - :meth:`.ResultProxy.supports_sane_multi_rowcount` methods - will report from the dialect if each usage is known to be - supported. - - * Statements that use RETURNING may not return a correct - rowcount. - - """ - try: - return self.context.rowcount - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, self.cursor, self.context) - raise - - @property - def lastrowid(self): - """return the 'lastrowid' accessor on the DBAPI cursor. - - This is a DBAPI specific method and is only functional - for those backends which support it, for statements - where it is appropriate. It's behavior is not - consistent across backends. - - Usage of this method is normally unnecessary; the - :attr:`~ResultProxy.inserted_primary_key` attribute provides a - tuple of primary key values for a newly inserted row, - regardless of database backend. - - """ - try: - return self._saved_cursor.lastrowid - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, - self._saved_cursor, self.context) - raise - - @property - def returns_rows(self): - """True if this :class:`.ResultProxy` returns rows. - - I.e. if it is legal to call the methods - :meth:`~.ResultProxy.fetchone`, - :meth:`~.ResultProxy.fetchmany` - :meth:`~.ResultProxy.fetchall`. - - """ - return self._metadata is not None - - @property - def is_insert(self): - """True if this :class:`.ResultProxy` is the result - of a executing an expression language compiled - :func:`.expression.insert` construct. - - When True, this implies that the - :attr:`inserted_primary_key` attribute is accessible, - assuming the statement did not include - a user defined "returning" construct. - - """ - return self.context.isinsert - - def _cursor_description(self): - """May be overridden by subclasses.""" - - return self._saved_cursor.description - - def close(self, _autoclose_connection=True): - """Close this ResultProxy. - - Closes the underlying DBAPI cursor corresponding to the execution. - - Note that any data cached within this ResultProxy is still available. - For some types of results, this may include buffered rows. - - If this ResultProxy was generated from an implicit execution, - the underlying Connection will also be closed (returns the - underlying DBAPI connection to the connection pool.) - - This method is called automatically when: - - * all result rows are exhausted using the fetchXXX() methods. - * cursor.description is None. - - """ - - if not self.closed: - self.closed = True - self.connection._safe_close_cursor(self.cursor) - if _autoclose_connection and \ - self.connection.should_close_with_result: - self.connection.close() - # allow consistent errors - self.cursor = None - - def __iter__(self): - while True: - row = self.fetchone() - if row is None: - raise StopIteration - else: - yield row - - @util.memoized_property - def inserted_primary_key(self): - """Return the primary key for the row just inserted. - - The return value is a list of scalar values - corresponding to the list of primary key columns - in the target table. - - This only applies to single row :func:`.insert` - constructs which did not explicitly specify - :meth:`.Insert.returning`. - - Note that primary key columns which specify a - server_default clause, - or otherwise do not qualify as "autoincrement" - columns (see the notes at :class:`.Column`), and were - generated using the database-side default, will - appear in this list as ``None`` unless the backend - supports "returning" and the insert statement executed - with the "implicit returning" enabled. - - """ - - if not self.context.isinsert: - raise exc.InvalidRequestError( - "Statement is not an insert() expression construct.") - elif self.context._is_explicit_returning: - raise exc.InvalidRequestError( - "Can't call inserted_primary_key when returning() " - "is used.") - - return self.context.inserted_primary_key - - @util.deprecated("0.6", "Use :attr:`.ResultProxy.inserted_primary_key`") - def last_inserted_ids(self): - """Return the primary key for the row just inserted.""" - - return self.inserted_primary_key - - def last_updated_params(self): - """Return the collection of updated parameters from this - execution. - - """ - if self.context.executemany: - return self.context.compiled_parameters - else: - return self.context.compiled_parameters[0] - - def last_inserted_params(self): - """Return the collection of inserted parameters from this - execution. - - """ - if self.context.executemany: - return self.context.compiled_parameters - else: - return self.context.compiled_parameters[0] - - def lastrow_has_defaults(self): - """Return ``lastrow_has_defaults()`` from the underlying - ExecutionContext. - - See ExecutionContext for details. - """ - - return self.context.lastrow_has_defaults() - - def postfetch_cols(self): - """Return ``postfetch_cols()`` from the underlying ExecutionContext. - - See ExecutionContext for details. - """ - - return self.context.postfetch_cols - - def prefetch_cols(self): - return self.context.prefetch_cols - - def supports_sane_rowcount(self): - """Return ``supports_sane_rowcount`` from the dialect. - - See :attr:`.ResultProxy.rowcount` for background. - - """ - - return self.dialect.supports_sane_rowcount - - def supports_sane_multi_rowcount(self): - """Return ``supports_sane_multi_rowcount`` from the dialect. - - See :attr:`.ResultProxy.rowcount` for background. - - """ - - return self.dialect.supports_sane_multi_rowcount - - def _fetchone_impl(self): - try: - return self.cursor.fetchone() - except AttributeError: - self._non_result() - - def _fetchmany_impl(self, size=None): - try: - if size is None: - return self.cursor.fetchmany() - else: - return self.cursor.fetchmany(size) - except AttributeError: - self._non_result() - - def _fetchall_impl(self): - try: - return self.cursor.fetchall() - except AttributeError: - self._non_result() - - def _non_result(self): - if self._metadata is None: - raise exc.ResourceClosedError( - "This result object does not return rows. " - "It has been closed automatically.", - ) - else: - raise exc.ResourceClosedError("This result object is closed.") - - def process_rows(self, rows): - process_row = self._process_row - metadata = self._metadata - keymap = metadata._keymap - processors = metadata._processors - if self._echo: - log = self.context.engine.logger.debug - l = [] - for row in rows: - log("Row %r", row) - l.append(process_row(metadata, row, processors, keymap)) - return l - else: - return [process_row(metadata, row, processors, keymap) - for row in rows] - - def fetchall(self): - """Fetch all rows, just like DB-API ``cursor.fetchall()``.""" - - try: - l = self.process_rows(self._fetchall_impl()) - self.close() - return l - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - raise - - def fetchmany(self, size=None): - """Fetch many rows, just like DB-API - ``cursor.fetchmany(size=cursor.arraysize)``. - - If rows are present, the cursor remains open after this is called. - Else the cursor is automatically closed and an empty list is returned. - - """ - - try: - l = self.process_rows(self._fetchmany_impl(size)) - if len(l) == 0: - self.close() - return l - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - raise - - def fetchone(self): - """Fetch one row, just like DB-API ``cursor.fetchone()``. - - If a row is present, the cursor remains open after this is called. - Else the cursor is automatically closed and None is returned. - - """ - try: - row = self._fetchone_impl() - if row is not None: - return self.process_rows([row])[0] - else: - self.close() - return None - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - raise - - def first(self): - """Fetch the first row and then close the result set unconditionally. - - Returns None if no row is present. - - """ - if self._metadata is None: - self._non_result() - - try: - row = self._fetchone_impl() - except Exception, e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - raise - - try: - if row is not None: - return self.process_rows([row])[0] - else: - return None - finally: - self.close() - - def scalar(self): - """Fetch the first column of the first row, and close the result set. - - Returns None if no row is present. - - """ - row = self.first() - if row is not None: - return row[0] - else: - return None - -class BufferedRowResultProxy(ResultProxy): - """A ResultProxy with row buffering behavior. - - ``ResultProxy`` that buffers the contents of a selection of rows - before ``fetchone()`` is called. This is to allow the results of - ``cursor.description`` to be available immediately, when - interfacing with a DB-API that requires rows to be consumed before - this information is available (currently psycopg2, when used with - server-side cursors). - - The pre-fetching behavior fetches only one row initially, and then - grows its buffer size by a fixed amount with each successive need - for additional rows up to a size of 100. - """ - - def _init_metadata(self): - self.__buffer_rows() - super(BufferedRowResultProxy, self)._init_metadata() - - # this is a "growth chart" for the buffering of rows. - # each successive __buffer_rows call will use the next - # value in the list for the buffer size until the max - # is reached - size_growth = { - 1 : 5, - 5 : 10, - 10 : 20, - 20 : 50, - 50 : 100, - 100 : 250, - 250 : 500, - 500 : 1000 - } - - def __buffer_rows(self): - size = getattr(self, '_bufsize', 1) - self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) - self._bufsize = self.size_growth.get(size, size) - - def _fetchone_impl(self): - if self.closed: - return None - if not self.__rowbuffer: - self.__buffer_rows() - if not self.__rowbuffer: - return None - return self.__rowbuffer.popleft() - - def _fetchmany_impl(self, size=None): - if size is None: - return self._fetchall_impl() - result = [] - for x in range(0, size): - row = self._fetchone_impl() - if row is None: - break - result.append(row) - return result - - def _fetchall_impl(self): - self.__rowbuffer.extend(self.cursor.fetchall()) - ret = self.__rowbuffer - self.__rowbuffer = collections.deque() - return ret - -class FullyBufferedResultProxy(ResultProxy): - """A result proxy that buffers rows fully upon creation. - - Used for operations where a result is to be delivered - after the database conversation can not be continued, - such as MSSQL INSERT...OUTPUT after an autocommit. - - """ - def _init_metadata(self): - super(FullyBufferedResultProxy, self)._init_metadata() - self.__rowbuffer = self._buffer_rows() - - def _buffer_rows(self): - return collections.deque(self.cursor.fetchall()) - - def _fetchone_impl(self): - if self.__rowbuffer: - return self.__rowbuffer.popleft() - else: - return None - - def _fetchmany_impl(self, size=None): - if size is None: - return self._fetchall_impl() - result = [] - for x in range(0, size): - row = self._fetchone_impl() - if row is None: - break - result.append(row) - return result - - def _fetchall_impl(self): - ret = self.__rowbuffer - self.__rowbuffer = collections.deque() - return ret - -class BufferedColumnRow(RowProxy): - def __init__(self, parent, row, processors, keymap): - # preprocess row - row = list(row) - # this is a tad faster than using enumerate - index = 0 - for processor in parent._orig_processors: - if processor is not None: - row[index] = processor(row[index]) - index += 1 - row = tuple(row) - super(BufferedColumnRow, self).__init__(parent, row, - processors, keymap) - -class BufferedColumnResultProxy(ResultProxy): - """A ResultProxy with column buffering behavior. - - ``ResultProxy`` that loads all columns into memory each time - fetchone() is called. If fetchmany() or fetchall() are called, - the full grid of results is fetched. This is to operate with - databases where result rows contain "live" results that fall out - of scope unless explicitly fetched. Currently this includes - cx_Oracle LOB objects. - - """ - - _process_row = BufferedColumnRow - - def _init_metadata(self): - super(BufferedColumnResultProxy, self)._init_metadata() - metadata = self._metadata - # orig_processors will be used to preprocess each row when they are - # constructed. - metadata._orig_processors = metadata._processors - # replace the all type processors by None processors. - metadata._processors = [None for _ in xrange(len(metadata.keys))] - keymap = {} - for k, (func, obj, index) in metadata._keymap.iteritems(): - keymap[k] = (None, obj, index) - self._metadata._keymap = keymap - - def fetchall(self): - # can't call cursor.fetchall(), since rows must be - # fully processed before requesting more from the DBAPI. - l = [] - while True: - row = self.fetchone() - if row is None: - break - l.append(row) - return l - - def fetchmany(self, size=None): - # can't call cursor.fetchmany(), since rows must be - # fully processed before requesting more from the DBAPI. - if size is None: - return self.fetchall() - l = [] - for i in xrange(size): - row = self.fetchone() - if row is None: - break - l.append(row) - return l - -def connection_memoize(key): - """Decorator, memoize a function in a connection.info stash. - - Only applicable to functions which take no arguments other than a - connection. The memo will be stored in ``connection.info[key]``. - """ - - @util.decorator - def decorated(fn, self, connection): - connection = connection.connect() - try: - return connection.info[key] - except KeyError: - connection.info[key] = val = fn(self, connection) - return val - - return decorated +class OptionEngine(Engine): + def __init__(self, proxied, execution_options): + self._proxied = proxied + self.url = proxied.url + self.dialect = proxied.dialect + self.logging_name = proxied.logging_name + self.echo = proxied.echo + log.instance_logger(self, echoflag=self.echo) + self.dispatch = self.dispatch._join(proxied.dispatch) + self._execution_options = proxied._execution_options + self.update_execution_options(**execution_options) + + def _get_pool(self): + return self._proxied.pool + + def _set_pool(self, pool): + self._proxied.pool = pool + + pool = property(_get_pool, _set_pool) + + def _get_has_events(self): + return self._proxied._has_events or \ + self.__dict__.get('_has_events', False) + + def _set_has_events(self, value): + self.__dict__['_has_events'] = value + + _has_events = property(_get_has_events, _set_has_events) diff --git a/libs/sqlalchemy/engine/ddl.py b/libs/sqlalchemy/engine/ddl.py deleted file mode 100644 index c3b32505..00000000 --- a/libs/sqlalchemy/engine/ddl.py +++ /dev/null @@ -1,182 +0,0 @@ -# engine/ddl.py -# Copyright (C) 2009-2011 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Routines to handle CREATE/DROP workflow.""" - -from sqlalchemy import engine, schema -from sqlalchemy.sql import util as sql_util - - -class DDLBase(schema.SchemaVisitor): - def __init__(self, connection): - self.connection = connection - -class SchemaGenerator(DDLBase): - def __init__(self, dialect, connection, checkfirst=False, tables=None, **kwargs): - super(SchemaGenerator, self).__init__(connection, **kwargs) - self.checkfirst = checkfirst - self.tables = tables and set(tables) or None - self.preparer = dialect.identifier_preparer - self.dialect = dialect - self.memo = {} - - def _can_create_table(self, table): - self.dialect.validate_identifier(table.name) - if table.schema: - self.dialect.validate_identifier(table.schema) - return not self.checkfirst or \ - not self.dialect.has_table(self.connection, - table.name, schema=table.schema) - - def _can_create_sequence(self, sequence): - return self.dialect.supports_sequences and \ - ( - (not self.dialect.sequences_optional or - not sequence.optional) and - ( - not self.checkfirst or - not self.dialect.has_sequence( - self.connection, - sequence.name, - schema=sequence.schema) - ) - ) - - def visit_metadata(self, metadata): - if self.tables: - tables = self.tables - else: - tables = metadata.tables.values() - collection = [t for t in sql_util.sort_tables(tables) - if self._can_create_table(t)] - seq_coll = [s for s in metadata._sequences.values() - if s.column is None and self._can_create_sequence(s)] - - metadata.dispatch.before_create(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for seq in seq_coll: - self.traverse_single(seq, create_ok=True) - - for table in collection: - self.traverse_single(table, create_ok=True) - - metadata.dispatch.after_create(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_table(self, table, create_ok=False): - if not create_ok and not self._can_create_table(table): - return - - table.dispatch.before_create(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for column in table.columns: - if column.default is not None: - self.traverse_single(column.default) - - self.connection.execute(schema.CreateTable(table)) - - if hasattr(table, 'indexes'): - for index in table.indexes: - self.traverse_single(index) - - table.dispatch.after_create(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_sequence(self, sequence, create_ok=False): - if not create_ok and not self._can_create_sequence(sequence): - return - self.connection.execute(schema.CreateSequence(sequence)) - - def visit_index(self, index): - self.connection.execute(schema.CreateIndex(index)) - - -class SchemaDropper(DDLBase): - def __init__(self, dialect, connection, checkfirst=False, tables=None, **kwargs): - super(SchemaDropper, self).__init__(connection, **kwargs) - self.checkfirst = checkfirst - self.tables = tables - self.preparer = dialect.identifier_preparer - self.dialect = dialect - self.memo = {} - - def visit_metadata(self, metadata): - if self.tables: - tables = self.tables - else: - tables = metadata.tables.values() - collection = [t for t in reversed(sql_util.sort_tables(tables)) - if self._can_drop_table(t)] - seq_coll = [s for s in metadata._sequences.values() - if s.column is None and self._can_drop_sequence(s)] - - metadata.dispatch.before_drop(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for table in collection: - self.traverse_single(table, drop_ok=True) - - for seq in seq_coll: - self.traverse_single(seq, drop_ok=True) - - metadata.dispatch.after_drop(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def _can_drop_table(self, table): - self.dialect.validate_identifier(table.name) - if table.schema: - self.dialect.validate_identifier(table.schema) - return not self.checkfirst or self.dialect.has_table(self.connection, - table.name, schema=table.schema) - - def _can_drop_sequence(self, sequence): - return self.dialect.supports_sequences and \ - ((not self.dialect.sequences_optional or - not sequence.optional) and - (not self.checkfirst or - self.dialect.has_sequence( - self.connection, - sequence.name, - schema=sequence.schema)) - ) - - def visit_index(self, index): - self.connection.execute(schema.DropIndex(index)) - - def visit_table(self, table, drop_ok=False): - if not drop_ok and not self._can_drop_table(table): - return - - table.dispatch.before_drop(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for column in table.columns: - if column.default is not None: - self.traverse_single(column.default) - - self.connection.execute(schema.DropTable(table)) - - table.dispatch.after_drop(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_sequence(self, sequence, drop_ok=False): - if not drop_ok and not self._can_drop_sequence(sequence): - return - self.connection.execute(schema.DropSequence(sequence)) diff --git a/libs/sqlalchemy/engine/default.py b/libs/sqlalchemy/engine/default.py index f3dfd95e..509d772a 100644 --- a/libs/sqlalchemy/engine/default.py +++ b/libs/sqlalchemy/engine/default.py @@ -1,5 +1,5 @@ # engine/default.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -12,19 +12,23 @@ as the base class for their own corresponding classes. """ -import re, random -from sqlalchemy.engine import base, reflection -from sqlalchemy.sql import compiler, expression -from sqlalchemy import exc, types as sqltypes, util, pool, processors +import re +import random +from . import reflection, interfaces, result +from ..sql import compiler, expression +from .. import types as sqltypes +from .. import exc, util, pool, processors import codecs import weakref +from .. import event AUTOCOMMIT_REGEXP = re.compile( r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER)', re.I | re.UNICODE) -class DefaultDialect(base.Dialect): + +class DefaultDialect(interfaces.Dialect): """Default implementation of Dialect""" statement_compiler = compiler.SQLCompiler @@ -33,6 +37,10 @@ class DefaultDialect(base.Dialect): preparer = compiler.IdentifierPreparer supports_alter = True + # the first value we'd get for an autoincrement + # column. + default_sequence_base = 1 + # most DBAPIs happy with this for execute(). # not cx_oracle. execute_sequence_format = tuple @@ -44,27 +52,40 @@ class DefaultDialect(base.Dialect): postfetch_lastrowid = True implicit_returning = False + supports_right_nested_joins = True supports_native_enum = False supports_native_boolean = False + supports_simple_order_by_label = True + + engine_config_types = util.immutabledict([ + ('convert_unicode', util.bool_or_str('force')), + ('pool_timeout', int), + ('echo', util.bool_or_str('debug')), + ('echo_pool', util.bool_or_str('debug')), + ('pool_recycle', int), + ('pool_size', int), + ('max_overflow', int), + ('pool_threadlocal', bool), + ('use_native_unicode', bool), + ]) + # if the NUMERIC type # returns decimal.Decimal. # *not* the FLOAT type however. supports_native_decimal = False - # Py3K - #supports_unicode_statements = True - #supports_unicode_binds = True - #returns_unicode_strings = True - #description_encoding = None - # Py2K - supports_unicode_statements = False - supports_unicode_binds = False - returns_unicode_strings = False - description_encoding = 'use_encoding' - # end Py2K - + if util.py3k: + supports_unicode_statements = True + supports_unicode_binds = True + returns_unicode_strings = True + description_encoding = None + else: + supports_unicode_statements = False + supports_unicode_binds = False + returns_unicode_strings = False + description_encoding = 'use_encoding' name = 'default' @@ -86,6 +107,7 @@ class DefaultDialect(base.Dialect): default_paramstyle = 'named' supports_default_values = False supports_empty_insert = True + supports_multivalues_insert = False server_version_info = None @@ -98,27 +120,20 @@ class DefaultDialect(base.Dialect): reflection_options = () - def __init__(self, convert_unicode=False, assert_unicode=False, + def __init__(self, convert_unicode=False, encoding='utf-8', paramstyle=None, dbapi=None, implicit_returning=None, + supports_right_nested_joins=None, + case_sensitive=True, + supports_native_boolean=None, label_length=None, **kwargs): if not getattr(self, 'ported_sqla_06', True): util.warn( - "The %s dialect is not yet ported to SQLAlchemy 0.6/0.7" % + "The %s dialect is not yet ported to the 0.6 format" % self.name) self.convert_unicode = convert_unicode - if assert_unicode: - util.warn_deprecated( - "assert_unicode is deprecated. " - "SQLAlchemy emits a warning in all cases where it " - "would otherwise like to encode a Python unicode object " - "into a specific encoding but a plain bytestring is " - "received. " - "This does *not* apply to DBAPIs that coerce Unicode " - "natively.") - self.encoding = encoding self.positional = False self._ischema = None @@ -134,6 +149,11 @@ class DefaultDialect(base.Dialect): self.positional = self.paramstyle in ('qmark', 'format', 'numeric') self.identifier_preparer = self.preparer(self) self.type_compiler = self.type_compiler(self) + if supports_right_nested_joins is not None: + self.supports_right_nested_joins = supports_right_nested_joins + if supports_native_boolean is not None: + self.supports_native_boolean = supports_native_boolean + self.case_sensitive = case_sensitive if label_length and label_length > self.max_identifier_length: raise exc.ArgumentError( @@ -143,16 +163,19 @@ class DefaultDialect(base.Dialect): self.label_length = label_length if self.description_encoding == 'use_encoding': - self._description_decoder = processors.to_unicode_processor_factory( + self._description_decoder = \ + processors.to_unicode_processor_factory( encoding ) elif self.description_encoding is not None: - self._description_decoder = processors.to_unicode_processor_factory( + self._description_decoder = \ + processors.to_unicode_processor_factory( self.description_encoding ) self._encoder = codecs.getencoder(self.encoding) self._decoder = processors.to_unicode_processor_factory(self.encoding) + @util.memoized_property def _type_memos(self): return weakref.WeakKeyDictionary() @@ -185,6 +208,10 @@ class DefaultDialect(base.Dialect): self.returns_unicode_strings = self._check_unicode_returns(connection) + if self.description_encoding is not None and \ + self._check_unicode_description(connection): + self._description_decoder = self.description_encoding = None + self.do_rollback(connection.connection) def on_connect(self): @@ -202,14 +229,11 @@ class DefaultDialect(base.Dialect): return None def _check_unicode_returns(self, connection): - # Py2K - if self.supports_unicode_statements: - cast_to = unicode + if util.py2k and not self.supports_unicode_statements: + cast_to = util.binary_type else: - cast_to = str - # end Py2K - # Py3K - #cast_to = str + cast_to = util.text_type + def check_unicode(formatstr, type_): cursor = connection.connection.cursor() try: @@ -217,16 +241,17 @@ class DefaultDialect(base.Dialect): cursor.execute( cast_to( expression.select( - [expression.cast( - expression.literal_column( - "'test %s returns'" % formatstr), type_) + [expression.cast( + expression.literal_column( + "'test %s returns'" % formatstr), + type_) ]).compile(dialect=self) ) ) row = cursor.fetchone() - return isinstance(row[0], unicode) - except self.dbapi.Error, de: + return isinstance(row[0], util.text_type) + except self.dbapi.Error as de: util.warn("Exception attempting to " "detect unicode returns: %r" % de) return False @@ -244,18 +269,41 @@ class DefaultDialect(base.Dialect): else: return unicode_for_varchar + def _check_unicode_description(self, connection): + # all DBAPIs on Py2K return cursor.description as encoded, + # until pypy2.1beta2 with sqlite, so let's just check it - + # it's likely others will start doing this too in Py2k. + + if util.py2k and not self.supports_unicode_statements: + cast_to = util.binary_type + else: + cast_to = util.text_type + + cursor = connection.connection.cursor() + try: + cursor.execute( + cast_to( + expression.select([ + expression.literal_column("'x'").label("some_label") + ]).compile(dialect=self) + ) + ) + return isinstance(cursor.description[0][0], util.text_type) + finally: + cursor.close() + def type_descriptor(self, typeobj): - """Provide a database-specific ``TypeEngine`` object, given + """Provide a database-specific :class:`.TypeEngine` object, given the generic object which comes from the types module. This method looks for a dictionary called ``colspecs`` as a class or instance-level variable, - and passes on to ``types.adapt_type()``. + and passes on to :func:`.types.adapt_type`. """ return sqltypes.adapt_type(typeobj, self.colspecs) - def reflecttable(self, connection, table, include_columns, exclude_columns=None): + def reflecttable(self, connection, table, include_columns, exclude_columns): insp = reflection.Inspector.from_engine(connection) return insp.reflecttable(table, include_columns, exclude_columns) @@ -285,26 +333,35 @@ class DefaultDialect(base.Dialect): opts.update(url.query) return [[], opts] - def do_begin(self, connection): - """Implementations might want to put logic here for turning - autocommit on/off, etc. - """ + def set_engine_execution_options(self, engine, opts): + if 'isolation_level' in opts: + isolation_level = opts['isolation_level'] + @event.listens_for(engine, "engine_connect") + def set_isolation(connection, branch): + if not branch: + self._set_connection_isolation(connection, isolation_level) + def set_connection_execution_options(self, connection, opts): + if 'isolation_level' in opts: + self._set_connection_isolation(connection, opts['isolation_level']) + + def _set_connection_isolation(self, connection, level): + self.set_isolation_level(connection.connection, level) + connection.connection._connection_record.\ + finalize_callback.append(self.reset_isolation_level) + + + def do_begin(self, dbapi_connection): pass - def do_rollback(self, connection): - """Implementations might want to put logic here for turning - autocommit on/off, etc. - """ + def do_rollback(self, dbapi_connection): + dbapi_connection.rollback() - connection.rollback() + def do_commit(self, dbapi_connection): + dbapi_connection.commit() - def do_commit(self, connection): - """Implementations might want to put logic here for turning - autocommit on/off, etc. - """ - - connection.commit() + def do_close(self, dbapi_connection): + dbapi_connection.close() def create_xid(self): """Create a random two-phase transaction ID. @@ -342,7 +399,8 @@ class DefaultDialect(base.Dialect): # the configured default of this dialect. self.set_isolation_level(dbapi_conn, self.default_isolation_level) -class DefaultExecutionContext(base.ExecutionContext): + +class DefaultExecutionContext(interfaces.ExecutionContext): isinsert = False isupdate = False isdelete = False @@ -353,6 +411,7 @@ class DefaultExecutionContext(base.ExecutionContext): statement = None postfetch_cols = None prefetch_cols = None + returning_cols = None _is_implicit_returning = False _is_explicit_returning = False @@ -379,10 +438,10 @@ class DefaultExecutionContext(base.ExecutionContext): self.execution_options.update(connection._execution_options) if not dialect.supports_unicode_statements: - self.unicode_statement = unicode(compiled) + self.unicode_statement = util.text_type(compiled) self.statement = dialect._encoder(self.unicode_statement)[0] else: - self.statement = self.unicode_statement = unicode(compiled) + self.statement = self.unicode_statement = util.text_type(compiled) self.cursor = self.create_cursor() self.compiled_parameters = [] @@ -395,7 +454,8 @@ class DefaultExecutionContext(base.ExecutionContext): return self @classmethod - def _init_compiled(cls, dialect, connection, dbapi_connection, compiled, parameters): + def _init_compiled(cls, dialect, connection, dbapi_connection, + compiled, parameters): """Initialize execution context for a Compiled construct.""" self = cls.__new__(cls) @@ -419,9 +479,10 @@ class DefaultExecutionContext(base.ExecutionContext): self.result_map = compiled.result_map - self.unicode_statement = unicode(compiled) + self.unicode_statement = util.text_type(compiled) if not dialect.supports_unicode_statements: - self.statement = self.unicode_statement.encode(self.dialect.encoding) + self.statement = self.unicode_statement.encode( + self.dialect.encoding) else: self.statement = self.unicode_statement @@ -430,16 +491,16 @@ class DefaultExecutionContext(base.ExecutionContext): self.isdelete = compiled.isdelete if self.isinsert or self.isupdate or self.isdelete: - self._is_explicit_returning = compiled.statement._returning - self._is_implicit_returning = compiled.returning and \ - not compiled.statement._returning + self._is_explicit_returning = bool(compiled.statement._returning) + self._is_implicit_returning = bool(compiled.returning and \ + not compiled.statement._returning) if not parameters: self.compiled_parameters = [compiled.construct_params()] else: self.compiled_parameters = \ [compiled.construct_params(m, _group_number=grp) for - grp,m in enumerate(parameters)] + grp, m in enumerate(parameters)] self.executemany = len(parameters) > 1 @@ -447,6 +508,7 @@ class DefaultExecutionContext(base.ExecutionContext): if self.isinsert or self.isupdate: self.postfetch_cols = self.compiled.postfetch self.prefetch_cols = self.compiled.prefetch + self.returning_cols = self.compiled.returning self.__process_defaults() processors = compiled._bind_processors @@ -474,7 +536,8 @@ class DefaultExecutionContext(base.ExecutionContext): param[dialect._encoder(key)[0]] = \ processors[key](compiled_params[key]) else: - param[dialect._encoder(key)[0]] = compiled_params[key] + param[dialect._encoder(key)[0]] = \ + compiled_params[key] else: for key in compiled_params: if key in processors: @@ -487,7 +550,8 @@ class DefaultExecutionContext(base.ExecutionContext): return self @classmethod - def _init_statement(cls, dialect, connection, dbapi_connection, statement, parameters): + def _init_statement(cls, dialect, connection, dbapi_connection, + statement, parameters): """Initialize execution context for a string SQL statement.""" self = cls.__new__(cls) @@ -510,7 +574,7 @@ class DefaultExecutionContext(base.ExecutionContext): if dialect.supports_unicode_statements: self.parameters = parameters else: - self.parameters= [ + self.parameters = [ dict((dialect._encoder(k)[0], d[k]) for k in d) for d in parameters ] or [{}] @@ -520,7 +584,8 @@ class DefaultExecutionContext(base.ExecutionContext): self.executemany = len(parameters) > 1 - if not dialect.supports_unicode_statements and isinstance(statement, unicode): + if not dialect.supports_unicode_statements and \ + isinstance(statement, util.text_type): self.unicode_statement = statement self.statement = dialect._encoder(statement)[0] else: @@ -574,8 +639,8 @@ class DefaultExecutionContext(base.ExecutionContext): """ conn = self.root_connection - if isinstance(stmt, unicode) and \ - not self.dialect.supports_unicode_statements: + if isinstance(stmt, util.text_type) and \ + not self.dialect.supports_unicode_statements: stmt = self.dialect._encoder(stmt)[0] if self.dialect.positional: @@ -583,7 +648,7 @@ class DefaultExecutionContext(base.ExecutionContext): else: default_params = {} - conn._cursor_execute(self.cursor, stmt, default_params) + conn._cursor_execute(self.cursor, stmt, default_params, context=self) r = self.cursor.fetchone()[0] if type_ is not None: # apply type post processors to the result @@ -611,6 +676,16 @@ class DefaultExecutionContext(base.ExecutionContext): def post_exec(self): pass + def get_result_processor(self, type_, colname, coltype): + """Return a 'result processor' for a given type as present in + cursor.description. + + This has a default implementation that dialects can override + for context-sensitive result type handling. + + """ + return type_._cached_result_processor(self.dialect, coltype) + def get_lastrowid(self): """return self.cursor.lastrowid, or equivalent, after an INSERT. @@ -643,7 +718,7 @@ class DefaultExecutionContext(base.ExecutionContext): pass def get_result_proxy(self): - return base.ResultProxy(self) + return result.ResultProxy(self) @property def rowcount(self): @@ -657,22 +732,24 @@ class DefaultExecutionContext(base.ExecutionContext): def post_insert(self): if not self._is_implicit_returning and \ + not self._is_explicit_returning and \ + not self.compiled.inline and \ self.dialect.postfetch_lastrowid and \ (not self.inserted_primary_key or \ None in self.inserted_primary_key): table = self.compiled.statement.table lastrowid = self.get_lastrowid() - autoinc_col = table._autoincrement_column if autoinc_col is not None: # apply type post processors to the lastrowid - proc = autoinc_col.type._cached_result_processor(self.dialect, None) + proc = autoinc_col.type._cached_result_processor( + self.dialect, None) if proc is not None: lastrowid = proc(lastrowid) self.inserted_primary_key = [ - c is autoinc_col and lastrowid or v + lastrowid if c is autoinc_col else v for c, v in zip( table.primary_key, self.inserted_primary_key) @@ -690,6 +767,11 @@ class DefaultExecutionContext(base.ExecutionContext): ipk.append(row[c]) self.inserted_primary_key = ipk + self.returned_defaults = row + + def _fetch_implicit_update_returning(self, resultproxy): + row = resultproxy.fetchone() + self.returned_defaults = row def lastrow_has_defaults(self): return (self.isinsert or self.isupdate) and \ @@ -716,28 +798,34 @@ class DefaultExecutionContext(base.ExecutionContext): inputsizes = [] for key in self.compiled.positiontup: typeengine = types[key] - dbtype = typeengine.dialect_impl(self.dialect).get_dbapi_type(self.dialect.dbapi) - if dbtype is not None and (not exclude_types or dbtype not in exclude_types): + dbtype = typeengine.dialect_impl(self.dialect).\ + get_dbapi_type(self.dialect.dbapi) + if dbtype is not None and \ + (not exclude_types or dbtype not in exclude_types): inputsizes.append(dbtype) try: self.cursor.setinputsizes(*inputsizes) - except Exception, e: - self.root_connection._handle_dbapi_exception(e, None, None, None, self) - raise + except Exception as e: + self.root_connection._handle_dbapi_exception( + e, None, None, None, self) else: inputsizes = {} for key in self.compiled.bind_names.values(): typeengine = types[key] - dbtype = typeengine.dialect_impl(self.dialect).get_dbapi_type(self.dialect.dbapi) - if dbtype is not None and (not exclude_types or dbtype not in exclude_types): + dbtype = typeengine.dialect_impl(self.dialect).\ + get_dbapi_type(self.dialect.dbapi) + if dbtype is not None and \ + (not exclude_types or dbtype not in exclude_types): if translate: key = translate.get(key, key) - inputsizes[self.dialect._encoder(key)[0]] = dbtype + if not self.dialect.supports_unicode_binds: + key = self.dialect._encoder(key)[0] + inputsizes[key] = dbtype try: self.cursor.setinputsizes(**inputsizes) - except Exception, e: - self.root_connection._handle_dbapi_exception(e, None, None, None, self) - raise + except Exception as e: + self.root_connection._handle_dbapi_exception( + e, None, None, None, self) def _exec_default(self, default, type_): if default.is_sequence: diff --git a/libs/sqlalchemy/engine/interfaces.py b/libs/sqlalchemy/engine/interfaces.py new file mode 100644 index 00000000..c26681cc --- /dev/null +++ b/libs/sqlalchemy/engine/interfaces.py @@ -0,0 +1,846 @@ +# engine/interfaces.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Define core interfaces used by the engine system.""" + +from .. import util, event + +# backwards compat +from ..sql.compiler import Compiled, TypeCompiler + +class Dialect(object): + """Define the behavior of a specific database and DB-API combination. + + Any aspect of metadata definition, SQL query generation, + execution, result-set handling, or anything else which varies + between databases is defined under the general category of the + Dialect. The Dialect acts as a factory for other + database-specific object implementations including + ExecutionContext, Compiled, DefaultGenerator, and TypeEngine. + + All Dialects implement the following attributes: + + name + identifying name for the dialect from a DBAPI-neutral point of view + (i.e. 'sqlite') + + driver + identifying name for the dialect's DBAPI + + positional + True if the paramstyle for this Dialect is positional. + + paramstyle + the paramstyle to be used (some DB-APIs support multiple + paramstyles). + + convert_unicode + True if Unicode conversion should be applied to all ``str`` + types. + + encoding + type of encoding to use for unicode, usually defaults to + 'utf-8'. + + statement_compiler + a :class:`.Compiled` class used to compile SQL statements + + ddl_compiler + a :class:`.Compiled` class used to compile DDL statements + + server_version_info + a tuple containing a version number for the DB backend in use. + This value is only available for supporting dialects, and is + typically populated during the initial connection to the database. + + default_schema_name + the name of the default schema. This value is only available for + supporting dialects, and is typically populated during the + initial connection to the database. + + execution_ctx_cls + a :class:`.ExecutionContext` class used to handle statement execution + + execute_sequence_format + either the 'tuple' or 'list' type, depending on what cursor.execute() + accepts for the second argument (they vary). + + preparer + a :class:`~sqlalchemy.sql.compiler.IdentifierPreparer` class used to + quote identifiers. + + supports_alter + ``True`` if the database supports ``ALTER TABLE``. + + max_identifier_length + The maximum length of identifier names. + + supports_unicode_statements + Indicate whether the DB-API can receive SQL statements as Python + unicode strings + + supports_unicode_binds + Indicate whether the DB-API can receive string bind parameters + as Python unicode strings + + supports_sane_rowcount + Indicate whether the dialect properly implements rowcount for + ``UPDATE`` and ``DELETE`` statements. + + supports_sane_multi_rowcount + Indicate whether the dialect properly implements rowcount for + ``UPDATE`` and ``DELETE`` statements when executed via + executemany. + + preexecute_autoincrement_sequences + True if 'implicit' primary key functions must be executed separately + in order to get their value. This is currently oriented towards + Postgresql. + + implicit_returning + use RETURNING or equivalent during INSERT execution in order to load + newly generated primary keys and other column defaults in one execution, + which are then available via inserted_primary_key. + If an insert statement has returning() specified explicitly, + the "implicit" functionality is not used and inserted_primary_key + will not be available. + + dbapi_type_map + A mapping of DB-API type objects present in this Dialect's + DB-API implementation mapped to TypeEngine implementations used + by the dialect. + + This is used to apply types to result sets based on the DB-API + types present in cursor.description; it only takes effect for + result sets against textual statements where no explicit + typemap was present. + + colspecs + A dictionary of TypeEngine classes from sqlalchemy.types mapped + to subclasses that are specific to the dialect class. This + dictionary is class-level only and is not accessed from the + dialect instance itself. + + supports_default_values + Indicates if the construct ``INSERT INTO tablename DEFAULT + VALUES`` is supported + + supports_sequences + Indicates if the dialect supports CREATE SEQUENCE or similar. + + sequences_optional + If True, indicates if the "optional" flag on the Sequence() construct + should signal to not generate a CREATE SEQUENCE. Applies only to + dialects that support sequences. Currently used only to allow Postgresql + SERIAL to be used on a column that specifies Sequence() for usage on + other backends. + + supports_native_enum + Indicates if the dialect supports a native ENUM construct. + This will prevent types.Enum from generating a CHECK + constraint when that type is used. + + supports_native_boolean + Indicates if the dialect supports a native boolean construct. + This will prevent types.Boolean from generating a CHECK + constraint when that type is used. + + """ + + def create_connect_args(self, url): + """Build DB-API compatible connection arguments. + + Given a :class:`~sqlalchemy.engine.url.URL` object, returns a tuple + consisting of a `*args`/`**kwargs` suitable to send directly + to the dbapi's connect function. + + """ + + raise NotImplementedError() + + @classmethod + def type_descriptor(cls, typeobj): + """Transform a generic type to a dialect-specific type. + + Dialect classes will usually use the + :func:`.types.adapt_type` function in the types module to + accomplish this. + + The returned result is cached *per dialect class* so can + contain no dialect-instance state. + + """ + + raise NotImplementedError() + + def initialize(self, connection): + """Called during strategized creation of the dialect with a + connection. + + Allows dialects to configure options based on server version info or + other properties. + + The connection passed here is a SQLAlchemy Connection object, + with full capabilities. + + The initalize() method of the base dialect should be called via + super(). + + """ + + pass + + def reflecttable(self, connection, table, include_columns, exclude_columns): + """Load table description from the database. + + Given a :class:`.Connection` and a + :class:`~sqlalchemy.schema.Table` object, reflect its columns and + properties from the database. + + The implementation of this method is provided by + :meth:`.DefaultDialect.reflecttable`, which makes use of + :class:`.Inspector` to retrieve column information. + + Dialects should **not** seek to implement this method, and should + instead implement individual schema inspection operations such as + :meth:`.Dialect.get_columns`, :meth:`.Dialect.get_pk_constraint`, + etc. + + """ + + raise NotImplementedError() + + def get_columns(self, connection, table_name, schema=None, **kw): + """Return information about columns in `table_name`. + + Given a :class:`.Connection`, a string + `table_name`, and an optional string `schema`, return column + information as a list of dictionaries with these keys: + + name + the column's name + + type + [sqlalchemy.types#TypeEngine] + + nullable + boolean + + default + the column's default value + + autoincrement + boolean + + sequence + a dictionary of the form + {'name' : str, 'start' :int, 'increment': int} + + Additional column attributes may be present. + """ + + raise NotImplementedError() + + def get_primary_keys(self, connection, table_name, schema=None, **kw): + """Return information about primary keys in `table_name`. + + + Deprecated. This method is only called by the default + implementation of :meth:`.Dialect.get_pk_constraint`. Dialects should + instead implement the :meth:`.Dialect.get_pk_constraint` method directly. + + """ + + raise NotImplementedError() + + def get_pk_constraint(self, connection, table_name, schema=None, **kw): + """Return information about the primary key constraint on + table_name`. + + Given a :class:`.Connection`, a string + `table_name`, and an optional string `schema`, return primary + key information as a dictionary with these keys: + + constrained_columns + a list of column names that make up the primary key + + name + optional name of the primary key constraint. + + """ + raise NotImplementedError() + + def get_foreign_keys(self, connection, table_name, schema=None, **kw): + """Return information about foreign_keys in `table_name`. + + Given a :class:`.Connection`, a string + `table_name`, and an optional string `schema`, return foreign + key information as a list of dicts with these keys: + + name + the constraint's name + + constrained_columns + a list of column names that make up the foreign key + + referred_schema + the name of the referred schema + + referred_table + the name of the referred table + + referred_columns + a list of column names in the referred table that correspond to + constrained_columns + """ + + raise NotImplementedError() + + def get_table_names(self, connection, schema=None, **kw): + """Return a list of table names for `schema`.""" + + raise NotImplementedError + + def get_view_names(self, connection, schema=None, **kw): + """Return a list of all view names available in the database. + + schema: + Optional, retrieve names from a non-default schema. + """ + + raise NotImplementedError() + + def get_view_definition(self, connection, view_name, schema=None, **kw): + """Return view definition. + + Given a :class:`.Connection`, a string + `view_name`, and an optional string `schema`, return the view + definition. + """ + + raise NotImplementedError() + + def get_indexes(self, connection, table_name, schema=None, **kw): + """Return information about indexes in `table_name`. + + Given a :class:`.Connection`, a string + `table_name` and an optional string `schema`, return index + information as a list of dictionaries with these keys: + + name + the index's name + + column_names + list of column names in order + + unique + boolean + """ + + raise NotImplementedError() + + def get_unique_constraints(self, table_name, schema=None, **kw): + """Return information about unique constraints in `table_name`. + + Given a string `table_name` and an optional string `schema`, return + unique constraint information as a list of dicts with these keys: + + name + the unique constraint's name + + column_names + list of column names in order + + \**kw + other options passed to the dialect's get_unique_constraints() method. + + .. versionadded:: 0.9.0 + + """ + + raise NotImplementedError() + + def normalize_name(self, name): + """convert the given name to lowercase if it is detected as + case insensitive. + + this method is only used if the dialect defines + requires_name_normalize=True. + + """ + raise NotImplementedError() + + def denormalize_name(self, name): + """convert the given name to a case insensitive identifier + for the backend if it is an all-lowercase name. + + this method is only used if the dialect defines + requires_name_normalize=True. + + """ + raise NotImplementedError() + + def has_table(self, connection, table_name, schema=None): + """Check the existence of a particular table in the database. + + Given a :class:`.Connection` object and a string + `table_name`, return True if the given table (possibly within + the specified `schema`) exists in the database, False + otherwise. + """ + + raise NotImplementedError() + + def has_sequence(self, connection, sequence_name, schema=None): + """Check the existence of a particular sequence in the database. + + Given a :class:`.Connection` object and a string + `sequence_name`, return True if the given sequence exists in + the database, False otherwise. + """ + + raise NotImplementedError() + + def _get_server_version_info(self, connection): + """Retrieve the server version info from the given connection. + + This is used by the default implementation to populate the + "server_version_info" attribute and is called exactly + once upon first connect. + + """ + + raise NotImplementedError() + + def _get_default_schema_name(self, connection): + """Return the string name of the currently selected schema from + the given connection. + + This is used by the default implementation to populate the + "default_schema_name" attribute and is called exactly + once upon first connect. + + """ + + raise NotImplementedError() + + def do_begin(self, dbapi_connection): + """Provide an implementation of ``connection.begin()``, given a + DB-API connection. + + The DBAPI has no dedicated "begin" method and it is expected + that transactions are implicit. This hook is provided for those + DBAPIs that might need additional help in this area. + + Note that :meth:`.Dialect.do_begin` is not called unless a + :class:`.Transaction` object is in use. The + :meth:`.Dialect.do_autocommit` + hook is provided for DBAPIs that need some extra commands emitted + after a commit in order to enter the next transaction, when the + SQLAlchemy :class:`.Connection` is used in it's default "autocommit" + mode. + + :param dbapi_connection: a DBAPI connection, typically + proxied within a :class:`.ConnectionFairy`. + + """ + + raise NotImplementedError() + + def do_rollback(self, dbapi_connection): + """Provide an implementation of ``connection.rollback()``, given + a DB-API connection. + + :param dbapi_connection: a DBAPI connection, typically + proxied within a :class:`.ConnectionFairy`. + + """ + + raise NotImplementedError() + + + def do_commit(self, dbapi_connection): + """Provide an implementation of ``connection.commit()``, given a + DB-API connection. + + :param dbapi_connection: a DBAPI connection, typically + proxied within a :class:`.ConnectionFairy`. + + """ + + raise NotImplementedError() + + def do_close(self, dbapi_connection): + """Provide an implementation of ``connection.close()``, given a DBAPI + connection. + + This hook is called by the :class:`.Pool` when a connection has been + detached from the pool, or is being returned beyond the normal + capacity of the pool. + + .. versionadded:: 0.8 + + """ + + raise NotImplementedError() + + def create_xid(self): + """Create a two-phase transaction ID. + + This id will be passed to do_begin_twophase(), + do_rollback_twophase(), do_commit_twophase(). Its format is + unspecified. + """ + + raise NotImplementedError() + + def do_savepoint(self, connection, name): + """Create a savepoint with the given name. + + :param connection: a :class:`.Connection`. + :param name: savepoint name. + + """ + + raise NotImplementedError() + + def do_rollback_to_savepoint(self, connection, name): + """Rollback a connection to the named savepoint. + + :param connection: a :class:`.Connection`. + :param name: savepoint name. + + """ + + raise NotImplementedError() + + def do_release_savepoint(self, connection, name): + """Release the named savepoint on a connection. + + :param connection: a :class:`.Connection`. + :param name: savepoint name. + """ + + raise NotImplementedError() + + def do_begin_twophase(self, connection, xid): + """Begin a two phase transaction on the given connection. + + :param connection: a :class:`.Connection`. + :param xid: xid + + """ + + raise NotImplementedError() + + def do_prepare_twophase(self, connection, xid): + """Prepare a two phase transaction on the given connection. + + :param connection: a :class:`.Connection`. + :param xid: xid + + """ + + raise NotImplementedError() + + def do_rollback_twophase(self, connection, xid, is_prepared=True, + recover=False): + """Rollback a two phase transaction on the given connection. + + :param connection: a :class:`.Connection`. + :param xid: xid + :param is_prepared: whether or not + :meth:`.TwoPhaseTransaction.prepare` was called. + :param recover: if the recover flag was passed. + + """ + + raise NotImplementedError() + + def do_commit_twophase(self, connection, xid, is_prepared=True, + recover=False): + """Commit a two phase transaction on the given connection. + + + :param connection: a :class:`.Connection`. + :param xid: xid + :param is_prepared: whether or not + :meth:`.TwoPhaseTransaction.prepare` was called. + :param recover: if the recover flag was passed. + + """ + + raise NotImplementedError() + + def do_recover_twophase(self, connection): + """Recover list of uncommited prepared two phase transaction + identifiers on the given connection. + + :param connection: a :class:`.Connection`. + + """ + + raise NotImplementedError() + + def do_executemany(self, cursor, statement, parameters, context=None): + """Provide an implementation of ``cursor.executemany(statement, + parameters)``.""" + + raise NotImplementedError() + + def do_execute(self, cursor, statement, parameters, context=None): + """Provide an implementation of ``cursor.execute(statement, + parameters)``.""" + + raise NotImplementedError() + + def do_execute_no_params(self, cursor, statement, parameters, + context=None): + """Provide an implementation of ``cursor.execute(statement)``. + + The parameter collection should not be sent. + + """ + + raise NotImplementedError() + + def is_disconnect(self, e, connection, cursor): + """Return True if the given DB-API error indicates an invalid + connection""" + + raise NotImplementedError() + + def connect(self): + """return a callable which sets up a newly created DBAPI connection. + + The callable accepts a single argument "conn" which is the + DBAPI connection itself. It has no return value. + + This is used to set dialect-wide per-connection options such as + isolation modes, unicode modes, etc. + + If a callable is returned, it will be assembled into a pool listener + that receives the direct DBAPI connection, with all wrappers removed. + + If None is returned, no listener will be generated. + + """ + return None + + def reset_isolation_level(self, dbapi_conn): + """Given a DBAPI connection, revert its isolation to the default.""" + + raise NotImplementedError() + + def set_isolation_level(self, dbapi_conn, level): + """Given a DBAPI connection, set its isolation level.""" + + raise NotImplementedError() + + def get_isolation_level(self, dbapi_conn): + """Given a DBAPI connection, return its isolation level.""" + + raise NotImplementedError() + + +class ExecutionContext(object): + """A messenger object for a Dialect that corresponds to a single + execution. + + ExecutionContext should have these data members: + + connection + Connection object which can be freely used by default value + generators to execute SQL. This Connection should reference the + same underlying connection/transactional resources of + root_connection. + + root_connection + Connection object which is the source of this ExecutionContext. This + Connection may have close_with_result=True set, in which case it can + only be used once. + + dialect + dialect which created this ExecutionContext. + + cursor + DB-API cursor procured from the connection, + + compiled + if passed to constructor, sqlalchemy.engine.base.Compiled object + being executed, + + statement + string version of the statement to be executed. Is either + passed to the constructor, or must be created from the + sql.Compiled object by the time pre_exec() has completed. + + parameters + bind parameters passed to the execute() method. For compiled + statements, this is a dictionary or list of dictionaries. For + textual statements, it should be in a format suitable for the + dialect's paramstyle (i.e. dict or list of dicts for non + positional, list or list of lists/tuples for positional). + + isinsert + True if the statement is an INSERT. + + isupdate + True if the statement is an UPDATE. + + should_autocommit + True if the statement is a "committable" statement. + + prefetch_cols + a list of Column objects for which a client-side default + was fired off. Applies to inserts and updates. + + postfetch_cols + a list of Column objects for which a server-side default or + inline SQL expression value was fired off. Applies to inserts + and updates. + """ + + def create_cursor(self): + """Return a new cursor generated from this ExecutionContext's + connection. + + Some dialects may wish to change the behavior of + connection.cursor(), such as postgresql which may return a PG + "server side" cursor. + """ + + raise NotImplementedError() + + def pre_exec(self): + """Called before an execution of a compiled statement. + + If a compiled statement was passed to this ExecutionContext, + the `statement` and `parameters` datamembers must be + initialized after this statement is complete. + """ + + raise NotImplementedError() + + def post_exec(self): + """Called after the execution of a compiled statement. + + If a compiled statement was passed to this ExecutionContext, + the `last_insert_ids`, `last_inserted_params`, etc. + datamembers should be available after this method completes. + """ + + raise NotImplementedError() + + def result(self): + """Return a result object corresponding to this ExecutionContext. + + Returns a ResultProxy. + """ + + raise NotImplementedError() + + def handle_dbapi_exception(self, e): + """Receive a DBAPI exception which occurred upon execute, result + fetch, etc.""" + + raise NotImplementedError() + + def should_autocommit_text(self, statement): + """Parse the given textual statement and return True if it refers to + a "committable" statement""" + + raise NotImplementedError() + + def lastrow_has_defaults(self): + """Return True if the last INSERT or UPDATE row contained + inlined or database-side defaults. + """ + + raise NotImplementedError() + + def get_rowcount(self): + """Return the DBAPI ``cursor.rowcount`` value, or in some + cases an interpreted value. + + See :attr:`.ResultProxy.rowcount` for details on this. + + """ + + raise NotImplementedError() + + +class Connectable(object): + """Interface for an object which supports execution of SQL constructs. + + The two implementations of :class:`.Connectable` are + :class:`.Connection` and :class:`.Engine`. + + Connectable must also implement the 'dialect' member which references a + :class:`.Dialect` instance. + + """ + + def connect(self, **kwargs): + """Return a :class:`.Connection` object. + + Depending on context, this may be ``self`` if this object + is already an instance of :class:`.Connection`, or a newly + procured :class:`.Connection` if this object is an instance + of :class:`.Engine`. + + """ + + def contextual_connect(self): + """Return a :class:`.Connection` object which may be part of an ongoing + context. + + Depending on context, this may be ``self`` if this object + is already an instance of :class:`.Connection`, or a newly + procured :class:`.Connection` if this object is an instance + of :class:`.Engine`. + + """ + + raise NotImplementedError() + + @util.deprecated("0.7", + "Use the create() method on the given schema " + "object directly, i.e. :meth:`.Table.create`, " + ":meth:`.Index.create`, :meth:`.MetaData.create_all`") + def create(self, entity, **kwargs): + """Emit CREATE statements for the given schema entity. + """ + + raise NotImplementedError() + + @util.deprecated("0.7", + "Use the drop() method on the given schema " + "object directly, i.e. :meth:`.Table.drop`, " + ":meth:`.Index.drop`, :meth:`.MetaData.drop_all`") + def drop(self, entity, **kwargs): + """Emit DROP statements for the given schema entity. + """ + + raise NotImplementedError() + + def execute(self, object, *multiparams, **params): + """Executes the given construct and returns a :class:`.ResultProxy`.""" + raise NotImplementedError() + + def scalar(self, object, *multiparams, **params): + """Executes and returns the first column of the first row. + + The underlying cursor is closed after execution. + """ + raise NotImplementedError() + + def _run_visitor(self, visitorcallable, element, + **kwargs): + raise NotImplementedError() + + def _execute_clauseelement(self, elem, multiparams=None, params=None): + raise NotImplementedError() diff --git a/libs/sqlalchemy/engine/reflection.py b/libs/sqlalchemy/engine/reflection.py index 6d34a279..badec84e 100644 --- a/libs/sqlalchemy/engine/reflection.py +++ b/libs/sqlalchemy/engine/reflection.py @@ -1,5 +1,5 @@ # engine/reflection.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -24,12 +24,14 @@ methods such as get_table_names, get_columns, etc. 'name' attribute.. """ -import sqlalchemy -from sqlalchemy import exc, sql -from sqlalchemy import util -from sqlalchemy.util import topological -from sqlalchemy.types import TypeEngine -from sqlalchemy import schema as sa_schema +from .. import exc, sql +from ..sql import schema as sa_schema +from .. import util +from ..sql.type_api import TypeEngine +from ..util import deprecated +from ..util import topological +from .. import inspection +from .base import Connectable @util.decorator @@ -39,8 +41,12 @@ def cache(fn, self, con, *args, **kw): return fn(self, con, *args, **kw) key = ( fn.__name__, - tuple(a for a in args if isinstance(a, basestring)), - tuple((k, v) for k, v in kw.iteritems() if isinstance(v, (basestring, int, float))) + tuple(a for a in args if isinstance(a, util.string_types)), + tuple((k, v) for k, v in kw.items() if + isinstance(v, + util.string_types + util.int_types + (float, ) + ) + ) ) ret = info_cache.get(key) if ret is None: @@ -53,17 +59,24 @@ class Inspector(object): """Performs database schema inspection. The Inspector acts as a proxy to the reflection methods of the - :class:`~sqlalchemy.engine.base.Dialect`, providing a + :class:`~sqlalchemy.engine.interfaces.Dialect`, providing a consistent interface as well as caching support for previously fetched metadata. - The preferred method to construct an :class:`.Inspector` is via the - :meth:`Inspector.from_engine` method. I.e.:: + A :class:`.Inspector` object is usually created via the + :func:`.inspect` function:: + + from sqlalchemy import inspect, create_engine + engine = create_engine('...') + insp = inspect(engine) + + The inspection method above is equivalent to using the + :meth:`.Inspector.from_engine` method, i.e.:: engine = create_engine('...') insp = Inspector.from_engine(engine) - Where above, the :class:`~sqlalchemy.engine.base.Dialect` may opt + Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt to return an :class:`.Inspector` subclass that provides additional methods specific to the dialect's target database. @@ -72,13 +85,13 @@ class Inspector(object): def __init__(self, bind): """Initialize a new :class:`.Inspector`. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + :param bind: a :class:`~sqlalchemy.engine.Connectable`, which is typically an instance of - :class:`~sqlalchemy.engine.base.Engine` or - :class:`~sqlalchemy.engine.base.Connection`. + :class:`~sqlalchemy.engine.Engine` or + :class:`~sqlalchemy.engine.Connection`. For a dialect-specific instance of :class:`.Inspector`, see - :meth:`Inspector.from_engine` + :meth:`.Inspector.from_engine` """ # this might not be a connection, it could be an engine. @@ -99,17 +112,19 @@ class Inspector(object): @classmethod def from_engine(cls, bind): - """Construct a new dialect-specific Inspector object from the given engine or connection. + """Construct a new dialect-specific Inspector object from the given + engine or connection. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + :param bind: a :class:`~sqlalchemy.engine.Connectable`, which is typically an instance of - :class:`~sqlalchemy.engine.base.Engine` or - :class:`~sqlalchemy.engine.base.Connection`. + :class:`~sqlalchemy.engine.Engine` or + :class:`~sqlalchemy.engine.Connection`. - This method differs from direct a direct constructor call of :class:`.Inspector` - in that the :class:`~sqlalchemy.engine.base.Dialect` is given a chance to provide - a dialect-specific :class:`.Inspector` instance, which may provide additional - methods. + This method differs from direct a direct constructor call of + :class:`.Inspector` in that the + :class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to + provide a dialect-specific :class:`.Inspector` instance, which may + provide additional methods. See the example at :class:`.Inspector`. @@ -118,6 +133,10 @@ class Inspector(object): return bind.dialect.inspector(bind) return Inspector(bind) + @inspection._inspects(Connectable) + def _insp(bind): + return Inspector.from_engine(bind) + @property def default_schema_name(self): """Return the default schema name presented by the dialect @@ -139,14 +158,32 @@ class Inspector(object): return [] def get_table_names(self, schema=None, order_by=None): - """Return all table names in `schema`. + """Return all table names in referred to within a particular schema. + + The names are expected to be real tables only, not views. + Views are instead returned using the :meth:`.Inspector.get_view_names` + method. + + + :param schema: Schema name. If ``schema`` is left at ``None``, the + database's default schema is + used, else the named schema is searched. If the database does not + support named schemas, behavior is undefined if ``schema`` is not + passed as ``None``. For special quoting, use :class:`.quoted_name`. - :param schema: Optional, retrieve names from a non-default schema. :param order_by: Optional, may be the string "foreign_key" to sort - the result on foreign key dependencies. + the result on foreign key dependencies. + + .. versionchanged:: 0.8 the "foreign_key" sorting sorts tables + in order of dependee to dependent; that is, in creation + order, rather than in drop order. This is to maintain + consistency with similar features such as + :attr:`.MetaData.sorted_tables` and :func:`.util.sort_tables`. + + .. seealso:: + + :attr:`.MetaData.sorted_tables` - This should probably not return view names or maybe it should return - them with an indicator t or v. """ if hasattr(self.dialect, 'get_table_names'): @@ -155,33 +192,40 @@ class Inspector(object): else: tnames = self.engine.table_names(schema) if order_by == 'foreign_key': - import random - random.shuffle(tnames) - tuples = [] for tname in tnames: for fkey in self.get_foreign_keys(tname, schema): if tname != fkey['referred_table']: - tuples.append((tname, fkey['referred_table'])) + tuples.append((fkey['referred_table'], tname)) tnames = list(topological.sort(tuples, tnames)) return tnames def get_table_options(self, table_name, schema=None, **kw): - """Return a dictionary of options specified when the table of the given name was created. + """Return a dictionary of options specified when the table of the + given name was created. This currently includes some options that apply to MySQL tables. + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. + """ if hasattr(self.dialect, 'get_table_options'): - return self.dialect.get_table_options(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw) + return self.dialect.get_table_options( + self.bind, table_name, schema, + info_cache=self.info_cache, **kw) return {} def get_view_names(self, schema=None): """Return all view names in `schema`. :param schema: Optional, retrieve names from a non-default schema. + For special quoting, use :class:`.quoted_name`. + """ return self.dialect.get_view_names(self.bind, schema, @@ -191,6 +235,8 @@ class Inspector(object): """Return definition for `view_name`. :param schema: Optional, retrieve names from a non-default schema. + For special quoting, use :class:`.quoted_name`. + """ return self.dialect.get_view_definition( @@ -216,6 +262,14 @@ class Inspector(object): attrs dict containing optional column attributes + + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. + """ col_defs = self.dialect.get_columns(self.bind, table_name, schema, @@ -228,6 +282,8 @@ class Inspector(object): col_def['type'] = coltype() return col_defs + @deprecated('0.7', 'Call to deprecated method get_primary_keys.' + ' Use get_pk_constraint instead.') def get_primary_keys(self, table_name, schema=None, **kw): """Return information about primary keys in `table_name`. @@ -235,11 +291,9 @@ class Inspector(object): primary key information as a list of column names. """ - pkeys = self.dialect.get_primary_keys(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw) - - return pkeys + return self.dialect.get_pk_constraint(self.bind, table_name, schema, + info_cache=self.info_cache, + **kw)['constrained_columns'] def get_pk_constraint(self, table_name, schema=None, **kw): """Return information about primary key constraint on `table_name`. @@ -253,14 +307,18 @@ class Inspector(object): name optional name of the primary key constraint. + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. + """ - pkeys = self.dialect.get_pk_constraint(self.bind, table_name, schema, + return self.dialect.get_pk_constraint(self.bind, table_name, schema, info_cache=self.info_cache, **kw) - return pkeys - - def get_foreign_keys(self, table_name, schema=None, **kw): """Return information about foreign_keys in `table_name`. @@ -283,15 +341,18 @@ class Inspector(object): name optional name of the foreign key constraint. - \**kw - other options passed to the dialect's get_foreign_keys() method. + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. """ - fk_defs = self.dialect.get_foreign_keys(self.bind, table_name, schema, + return self.dialect.get_foreign_keys(self.bind, table_name, schema, info_cache=self.info_cache, **kw) - return fk_defs def get_indexes(self, table_name, schema=None, **kw): """Return information about indexes in `table_name`. @@ -308,17 +369,48 @@ class Inspector(object): unique boolean - \**kw - other options passed to the dialect's get_indexes() method. + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. + """ - indexes = self.dialect.get_indexes(self.bind, table_name, + return self.dialect.get_indexes(self.bind, table_name, schema, info_cache=self.info_cache, **kw) - return indexes + + def get_unique_constraints(self, table_name, schema=None, **kw): + """Return information about unique constraints in `table_name`. + + Given a string `table_name` and an optional string `schema`, return + unique constraint information as a list of dicts with these keys: + + name + the unique constraint's name + + column_names + list of column names in order + + :param table_name: string name of the table. For special quoting, + use :class:`.quoted_name`. + + :param schema: string schema name; if omitted, uses the default schema + of the database connection. For special quoting, + use :class:`.quoted_name`. + + .. versionadded:: 0.8.4 + + """ + + return self.dialect.get_unique_constraints( + self.bind, table_name, schema, info_cache=self.info_cache, **kw) def reflecttable(self, table, include_columns, exclude_columns=()): - """Given a Table object, load its internal constructs based on introspection. + """Given a Table object, load its internal constructs based on + introspection. This is the underlying method used by most dialects to produce table reflection. Direct usage is like:: @@ -341,7 +433,8 @@ class Inspector(object): # table attributes we might need. reflection_options = dict( - (k, table.kwargs.get(k)) for k in dialect.reflection_options if k in table.kwargs) + (k, table.kwargs.get(k)) + for k in dialect.reflection_options if k in table.kwargs) schema = table.schema table_name = table.name @@ -354,22 +447,25 @@ class Inspector(object): # table.kwargs will need to be passed to each reflection method. Make # sure keywords are strings. tblkw = table.kwargs.copy() - for (k, v) in tblkw.items(): + for (k, v) in list(tblkw.items()): del tblkw[k] tblkw[str(k)] = v - # Py2K - if isinstance(schema, str): - schema = schema.decode(dialect.encoding) - if isinstance(table_name, str): - table_name = table_name.decode(dialect.encoding) - # end Py2K + if util.py2k: + if isinstance(schema, str): + schema = schema.decode(dialect.encoding) + if isinstance(table_name, str): + table_name = table_name.decode(dialect.encoding) # columns found_table = False + cols_by_orig_name = {} + for col_d in self.get_columns(table_name, schema, **tblkw): found_table = True - table.dispatch.column_reflect(table, col_d) + orig_name = col_d['name'] + + table.dispatch.column_reflect(self, table, col_d) name = col_d['name'] if include_columns and name not in include_columns: @@ -387,8 +483,9 @@ class Inspector(object): colargs = [] if col_d.get('default') is not None: - # the "default" value is assumed to be a literal SQL expression, - # so is wrapped in text() so that no quoting occurs on re-issuance. + # the "default" value is assumed to be a literal SQL + # expression, so is wrapped in text() so that no quoting + # occurs on re-issuance. colargs.append( sa_schema.DefaultClause( sql.text(col_d['default']), _reflected=True @@ -396,7 +493,7 @@ class Inspector(object): ) if 'sequence' in col_d: - # TODO: mssql, maxdb and sybase are using this. + # TODO: mssql and sybase are using this. seq = col_d['sequence'] sequence = sa_schema.Sequence(seq['name'], 1, 1) if 'start' in seq: @@ -405,7 +502,9 @@ class Inspector(object): sequence.increment = seq['increment'] colargs.append(sequence) - col = sa_schema.Column(name, coltype, *colargs, **col_kw) + cols_by_orig_name[orig_name] = col = \ + sa_schema.Column(name, coltype, *colargs, **col_kw) + table.append_column(col) if not found_table: @@ -414,11 +513,18 @@ class Inspector(object): # Primary keys pk_cons = self.get_pk_constraint(table_name, schema, **tblkw) if pk_cons: - pk_cols = [table.c[pk] - for pk in pk_cons['constrained_columns'] - if pk in table.c and pk not in exclude_columns - ] + [pk for pk in table.primary_key if pk.key in exclude_columns] - primary_key_constraint = sa_schema.PrimaryKeyConstraint(name=pk_cons.get('name'), + pk_cols = [ + cols_by_orig_name[pk] + for pk in pk_cons['constrained_columns'] + if pk in cols_by_orig_name and pk not in exclude_columns + ] + pk_cols += [ + pk + for pk in table.primary_key + if pk.key in exclude_columns + ] + primary_key_constraint = sa_schema.PrimaryKeyConstraint( + name=pk_cons.get('name'), *pk_cols ) @@ -428,7 +534,16 @@ class Inspector(object): fkeys = self.get_foreign_keys(table_name, schema, **tblkw) for fkey_d in fkeys: conname = fkey_d['name'] - constrained_columns = fkey_d['constrained_columns'] + # look for columns by orig name in cols_by_orig_name, + # but support columns that are in-Python only as fallback + constrained_columns = [ + cols_by_orig_name[c].key + if c in cols_by_orig_name else c + for c in fkey_d['constrained_columns'] + ] + if exclude_columns and set(constrained_columns).intersection( + exclude_columns): + continue referred_schema = fkey_d['referred_schema'] referred_table = fkey_d['referred_table'] referred_columns = fkey_d['referred_columns'] @@ -449,9 +564,14 @@ class Inspector(object): ) for column in referred_columns: refspec.append(".".join([referred_table, column])) + if 'options' in fkey_d: + options = fkey_d['options'] + else: + options = {} table.append_constraint( sa_schema.ForeignKeyConstraint(constrained_columns, refspec, - conname, link_to_name=True)) + conname, link_to_name=True, + **options)) # Indexes indexes = self.get_indexes(table_name, schema) for index_d in indexes: @@ -465,5 +585,11 @@ class Inspector(object): "Omitting %s KEY for (%s), key covers omitted columns." % (flavor, ', '.join(columns))) continue - sa_schema.Index(name, *[table.columns[c] for c in columns], + # look for columns by orig name in cols_by_orig_name, + # but support columns that are in-Python only as fallback + sa_schema.Index(name, *[ + cols_by_orig_name[c] if c in cols_by_orig_name + else table.c[c] + for c in columns + ], **dict(unique=unique)) diff --git a/libs/sqlalchemy/engine/result.py b/libs/sqlalchemy/engine/result.py new file mode 100644 index 00000000..f9e0ca0d --- /dev/null +++ b/libs/sqlalchemy/engine/result.py @@ -0,0 +1,1018 @@ +# engine/result.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Define result set constructs including :class:`.ResultProxy` +and :class:`.RowProxy.""" + + + +from .. import exc, util +from ..sql import expression, sqltypes +import collections + +# This reconstructor is necessary so that pickles with the C extension or +# without use the same Binary format. +try: + # We need a different reconstructor on the C extension so that we can + # add extra checks that fields have correctly been initialized by + # __setstate__. + from sqlalchemy.cresultproxy import safe_rowproxy_reconstructor + + # The extra function embedding is needed so that the + # reconstructor function has the same signature whether or not + # the extension is present. + def rowproxy_reconstructor(cls, state): + return safe_rowproxy_reconstructor(cls, state) +except ImportError: + def rowproxy_reconstructor(cls, state): + obj = cls.__new__(cls) + obj.__setstate__(state) + return obj + +try: + from sqlalchemy.cresultproxy import BaseRowProxy +except ImportError: + class BaseRowProxy(object): + __slots__ = ('_parent', '_row', '_processors', '_keymap') + + def __init__(self, parent, row, processors, keymap): + """RowProxy objects are constructed by ResultProxy objects.""" + + self._parent = parent + self._row = row + self._processors = processors + self._keymap = keymap + + def __reduce__(self): + return (rowproxy_reconstructor, + (self.__class__, self.__getstate__())) + + def values(self): + """Return the values represented by this RowProxy as a list.""" + return list(self) + + def __iter__(self): + for processor, value in zip(self._processors, self._row): + if processor is None: + yield value + else: + yield processor(value) + + def __len__(self): + return len(self._row) + + def __getitem__(self, key): + try: + processor, obj, index = self._keymap[key] + except KeyError: + processor, obj, index = self._parent._key_fallback(key) + except TypeError: + if isinstance(key, slice): + l = [] + for processor, value in zip(self._processors[key], + self._row[key]): + if processor is None: + l.append(value) + else: + l.append(processor(value)) + return tuple(l) + else: + raise + if index is None: + raise exc.InvalidRequestError( + "Ambiguous column name '%s' in result set! " + "try 'use_labels' option on select statement." % key) + if processor is not None: + return processor(self._row[index]) + else: + return self._row[index] + + def __getattr__(self, name): + try: + return self[name] + except KeyError as e: + raise AttributeError(e.args[0]) + + +class RowProxy(BaseRowProxy): + """Proxy values from a single cursor row. + + Mostly follows "ordered dictionary" behavior, mapping result + values to the string-based column name, the integer position of + the result in the row, as well as Column instances which can be + mapped to the original Columns that produced this result set (for + results that correspond to constructed SQL expressions). + """ + __slots__ = () + + def __contains__(self, key): + return self._parent._has_key(self._row, key) + + def __getstate__(self): + return { + '_parent': self._parent, + '_row': tuple(self) + } + + def __setstate__(self, state): + self._parent = parent = state['_parent'] + self._row = state['_row'] + self._processors = parent._processors + self._keymap = parent._keymap + + __hash__ = None + + def __lt__(self, other): + return tuple(self) < tuple(other) + + def __eq__(self, other): + return other is self or tuple(other) == tuple(self) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return repr(tuple(self)) + + def has_key(self, key): + """Return True if this RowProxy contains the given key.""" + + return self._parent._has_key(self._row, key) + + def items(self): + """Return a list of tuples, each tuple containing a key/value pair.""" + # TODO: no coverage here + return [(key, self[key]) for key in self.keys()] + + def keys(self): + """Return the list of keys as strings represented by this RowProxy.""" + + return self._parent.keys + + def iterkeys(self): + return iter(self._parent.keys) + + def itervalues(self): + return iter(self) + +try: + # Register RowProxy with Sequence, + # so sequence protocol is implemented + from collections import Sequence + Sequence.register(RowProxy) +except ImportError: + pass + + +class ResultMetaData(object): + """Handle cursor.description, applying additional info from an execution + context.""" + + def __init__(self, parent, metadata): + self._processors = processors = [] + + # We do not strictly need to store the processor in the key mapping, + # though it is faster in the Python version (probably because of the + # saved attribute lookup self._processors) + self._keymap = keymap = {} + self.keys = [] + context = parent.context + dialect = context.dialect + typemap = dialect.dbapi_type_map + translate_colname = context._translate_colname + self.case_sensitive = dialect.case_sensitive + + # high precedence key values. + primary_keymap = {} + + for i, rec in enumerate(metadata): + colname = rec[0] + coltype = rec[1] + + if dialect.description_encoding: + colname = dialect._description_decoder(colname) + + if translate_colname: + colname, untranslated = translate_colname(colname) + + if dialect.requires_name_normalize: + colname = dialect.normalize_name(colname) + + if context.result_map: + try: + name, obj, type_ = context.result_map[colname + if self.case_sensitive + else colname.lower()] + except KeyError: + name, obj, type_ = \ + colname, None, typemap.get(coltype, sqltypes.NULLTYPE) + else: + name, obj, type_ = \ + colname, None, typemap.get(coltype, sqltypes.NULLTYPE) + + processor = context.get_result_processor(type_, colname, coltype) + + processors.append(processor) + rec = (processor, obj, i) + + # indexes as keys. This is only needed for the Python version of + # RowProxy (the C version uses a faster path for integer indexes). + primary_keymap[i] = rec + + # populate primary keymap, looking for conflicts. + if primary_keymap.setdefault( + name if self.case_sensitive + else name.lower(), + rec) is not rec: + # place a record that doesn't have the "index" - this + # is interpreted later as an AmbiguousColumnError, + # but only when actually accessed. Columns + # colliding by name is not a problem if those names + # aren't used; integer access is always + # unambiguous. + primary_keymap[name + if self.case_sensitive + else name.lower()] = rec = (None, obj, None) + + self.keys.append(colname) + if obj: + for o in obj: + keymap[o] = rec + # technically we should be doing this but we + # are saving on callcounts by not doing so. + # if keymap.setdefault(o, rec) is not rec: + # keymap[o] = (None, obj, None) + + if translate_colname and \ + untranslated: + keymap[untranslated] = rec + + # overwrite keymap values with those of the + # high precedence keymap. + keymap.update(primary_keymap) + + if parent._echo: + context.engine.logger.debug( + "Col %r", tuple(x[0] for x in metadata)) + + @util.pending_deprecation("0.8", "sqlite dialect uses " + "_translate_colname() now") + def _set_keymap_synonym(self, name, origname): + """Set a synonym for the given name. + + Some dialects (SQLite at the moment) may use this to + adjust the column names that are significant within a + row. + + """ + rec = (processor, obj, i) = self._keymap[origname if + self.case_sensitive + else origname.lower()] + if self._keymap.setdefault(name, rec) is not rec: + self._keymap[name] = (processor, obj, None) + + def _key_fallback(self, key, raiseerr=True): + map = self._keymap + result = None + if isinstance(key, util.string_types): + result = map.get(key if self.case_sensitive else key.lower()) + # fallback for targeting a ColumnElement to a textual expression + # this is a rare use case which only occurs when matching text() + # or colummn('name') constructs to ColumnElements, or after a + # pickle/unpickle roundtrip + elif isinstance(key, expression.ColumnElement): + if key._label and ( + key._label + if self.case_sensitive + else key._label.lower()) in map: + result = map[key._label + if self.case_sensitive + else key._label.lower()] + elif hasattr(key, 'name') and ( + key.name + if self.case_sensitive + else key.name.lower()) in map: + # match is only on name. + result = map[key.name + if self.case_sensitive + else key.name.lower()] + # search extra hard to make sure this + # isn't a column/label name overlap. + # this check isn't currently available if the row + # was unpickled. + if result is not None and \ + result[1] is not None: + for obj in result[1]: + if key._compare_name_for_result(obj): + break + else: + result = None + if result is None: + if raiseerr: + raise exc.NoSuchColumnError( + "Could not locate column in row for column '%s'" % + expression._string_or_unprintable(key)) + else: + return None + else: + map[key] = result + return result + + def _has_key(self, row, key): + if key in self._keymap: + return True + else: + return self._key_fallback(key, False) is not None + + def __getstate__(self): + return { + '_pickled_keymap': dict( + (key, index) + for key, (processor, obj, index) in self._keymap.items() + if isinstance(key, util.string_types + util.int_types) + ), + 'keys': self.keys, + "case_sensitive": self.case_sensitive, + } + + def __setstate__(self, state): + # the row has been processed at pickling time so we don't need any + # processor anymore + self._processors = [None for _ in range(len(state['keys']))] + self._keymap = keymap = {} + for key, index in state['_pickled_keymap'].items(): + # not preserving "obj" here, unfortunately our + # proxy comparison fails with the unpickle + keymap[key] = (None, None, index) + self.keys = state['keys'] + self.case_sensitive = state['case_sensitive'] + self._echo = False + + +class ResultProxy(object): + """Wraps a DB-API cursor object to provide easier access to row columns. + + Individual columns may be accessed by their integer position, + case-insensitive column name, or by ``schema.Column`` + object. e.g.:: + + row = fetchone() + + col1 = row[0] # access via integer position + + col2 = row['col2'] # access via name + + col3 = row[mytable.c.mycol] # access via Column object. + + ``ResultProxy`` also handles post-processing of result column + data using ``TypeEngine`` objects, which are referenced from + the originating SQL statement that produced this result set. + + """ + + _process_row = RowProxy + out_parameters = None + _can_close_connection = False + _metadata = None + + def __init__(self, context): + self.context = context + self.dialect = context.dialect + self.closed = False + self.cursor = self._saved_cursor = context.cursor + self.connection = context.root_connection + self._echo = self.connection._echo and \ + context.engine._should_log_debug() + self._init_metadata() + + def _init_metadata(self): + metadata = self._cursor_description() + if metadata is not None: + self._metadata = ResultMetaData(self, metadata) + + def keys(self): + """Return the current set of string keys for rows.""" + if self._metadata: + return self._metadata.keys + else: + return [] + + @util.memoized_property + def rowcount(self): + """Return the 'rowcount' for this result. + + The 'rowcount' reports the number of rows *matched* + by the WHERE criterion of an UPDATE or DELETE statement. + + .. note:: + + Notes regarding :attr:`.ResultProxy.rowcount`: + + + * This attribute returns the number of rows *matched*, + which is not necessarily the same as the number of rows + that were actually *modified* - an UPDATE statement, for example, + may have no net change on a given row if the SET values + given are the same as those present in the row already. + Such a row would be matched but not modified. + On backends that feature both styles, such as MySQL, + rowcount is configured by default to return the match + count in all cases. + + * :attr:`.ResultProxy.rowcount` is *only* useful in conjunction + with an UPDATE or DELETE statement. Contrary to what the Python + DBAPI says, it does *not* return the + number of rows available from the results of a SELECT statement + as DBAPIs cannot support this functionality when rows are + unbuffered. + + * :attr:`.ResultProxy.rowcount` may not be fully implemented by + all dialects. In particular, most DBAPIs do not support an + aggregate rowcount result from an executemany call. + The :meth:`.ResultProxy.supports_sane_rowcount` and + :meth:`.ResultProxy.supports_sane_multi_rowcount` methods + will report from the dialect if each usage is known to be + supported. + + * Statements that use RETURNING may not return a correct + rowcount. + + """ + try: + return self.context.rowcount + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, self.cursor, self.context) + + @property + def lastrowid(self): + """return the 'lastrowid' accessor on the DBAPI cursor. + + This is a DBAPI specific method and is only functional + for those backends which support it, for statements + where it is appropriate. It's behavior is not + consistent across backends. + + Usage of this method is normally unnecessary when + using insert() expression constructs; the + :attr:`~ResultProxy.inserted_primary_key` attribute provides a + tuple of primary key values for a newly inserted row, + regardless of database backend. + + """ + try: + return self._saved_cursor.lastrowid + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, + self._saved_cursor, self.context) + + @property + def returns_rows(self): + """True if this :class:`.ResultProxy` returns rows. + + I.e. if it is legal to call the methods + :meth:`~.ResultProxy.fetchone`, + :meth:`~.ResultProxy.fetchmany` + :meth:`~.ResultProxy.fetchall`. + + """ + return self._metadata is not None + + @property + def is_insert(self): + """True if this :class:`.ResultProxy` is the result + of a executing an expression language compiled + :func:`.expression.insert` construct. + + When True, this implies that the + :attr:`inserted_primary_key` attribute is accessible, + assuming the statement did not include + a user defined "returning" construct. + + """ + return self.context.isinsert + + def _cursor_description(self): + """May be overridden by subclasses.""" + + return self._saved_cursor.description + + def close(self, _autoclose_connection=True): + """Close this ResultProxy. + + Closes the underlying DBAPI cursor corresponding to the execution. + + Note that any data cached within this ResultProxy is still available. + For some types of results, this may include buffered rows. + + If this ResultProxy was generated from an implicit execution, + the underlying Connection will also be closed (returns the + underlying DBAPI connection to the connection pool.) + + This method is called automatically when: + + * all result rows are exhausted using the fetchXXX() methods. + * cursor.description is None. + + """ + + if not self.closed: + self.closed = True + self.connection._safe_close_cursor(self.cursor) + if _autoclose_connection and \ + self.connection.should_close_with_result: + self.connection.close() + # allow consistent errors + self.cursor = None + + def __iter__(self): + while True: + row = self.fetchone() + if row is None: + raise StopIteration + else: + yield row + + @util.memoized_property + def inserted_primary_key(self): + """Return the primary key for the row just inserted. + + The return value is a list of scalar values + corresponding to the list of primary key columns + in the target table. + + This only applies to single row :func:`.insert` + constructs which did not explicitly specify + :meth:`.Insert.returning`. + + Note that primary key columns which specify a + server_default clause, + or otherwise do not qualify as "autoincrement" + columns (see the notes at :class:`.Column`), and were + generated using the database-side default, will + appear in this list as ``None`` unless the backend + supports "returning" and the insert statement executed + with the "implicit returning" enabled. + + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed + statement is not a compiled expression construct + or is not an insert() construct. + + """ + + if not self.context.compiled: + raise exc.InvalidRequestError( + "Statement is not a compiled " + "expression construct.") + elif not self.context.isinsert: + raise exc.InvalidRequestError( + "Statement is not an insert() " + "expression construct.") + elif self.context._is_explicit_returning: + raise exc.InvalidRequestError( + "Can't call inserted_primary_key " + "when returning() " + "is used.") + + return self.context.inserted_primary_key + + def last_updated_params(self): + """Return the collection of updated parameters from this + execution. + + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed + statement is not a compiled expression construct + or is not an update() construct. + + """ + if not self.context.compiled: + raise exc.InvalidRequestError( + "Statement is not a compiled " + "expression construct.") + elif not self.context.isupdate: + raise exc.InvalidRequestError( + "Statement is not an update() " + "expression construct.") + elif self.context.executemany: + return self.context.compiled_parameters + else: + return self.context.compiled_parameters[0] + + def last_inserted_params(self): + """Return the collection of inserted parameters from this + execution. + + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed + statement is not a compiled expression construct + or is not an insert() construct. + + """ + if not self.context.compiled: + raise exc.InvalidRequestError( + "Statement is not a compiled " + "expression construct.") + elif not self.context.isinsert: + raise exc.InvalidRequestError( + "Statement is not an insert() " + "expression construct.") + elif self.context.executemany: + return self.context.compiled_parameters + else: + return self.context.compiled_parameters[0] + + @property + def returned_defaults(self): + """Return the values of default columns that were fetched using + the :meth:`.ValuesBase.return_defaults` feature. + + The value is an instance of :class:`.RowProxy`, or ``None`` + if :meth:`.ValuesBase.return_defaults` was not used or if the + backend does not support RETURNING. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.ValuesBase.return_defaults` + + """ + return self.context.returned_defaults + + def lastrow_has_defaults(self): + """Return ``lastrow_has_defaults()`` from the underlying + :class:`.ExecutionContext`. + + See :class:`.ExecutionContext` for details. + + """ + + return self.context.lastrow_has_defaults() + + def postfetch_cols(self): + """Return ``postfetch_cols()`` from the underlying + :class:`.ExecutionContext`. + + See :class:`.ExecutionContext` for details. + + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed + statement is not a compiled expression construct + or is not an insert() or update() construct. + + """ + + if not self.context.compiled: + raise exc.InvalidRequestError( + "Statement is not a compiled " + "expression construct.") + elif not self.context.isinsert and not self.context.isupdate: + raise exc.InvalidRequestError( + "Statement is not an insert() or update() " + "expression construct.") + return self.context.postfetch_cols + + def prefetch_cols(self): + """Return ``prefetch_cols()`` from the underlying + :class:`.ExecutionContext`. + + See :class:`.ExecutionContext` for details. + + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed + statement is not a compiled expression construct + or is not an insert() or update() construct. + + """ + + if not self.context.compiled: + raise exc.InvalidRequestError( + "Statement is not a compiled " + "expression construct.") + elif not self.context.isinsert and not self.context.isupdate: + raise exc.InvalidRequestError( + "Statement is not an insert() or update() " + "expression construct.") + return self.context.prefetch_cols + + def supports_sane_rowcount(self): + """Return ``supports_sane_rowcount`` from the dialect. + + See :attr:`.ResultProxy.rowcount` for background. + + """ + + return self.dialect.supports_sane_rowcount + + def supports_sane_multi_rowcount(self): + """Return ``supports_sane_multi_rowcount`` from the dialect. + + See :attr:`.ResultProxy.rowcount` for background. + + """ + + return self.dialect.supports_sane_multi_rowcount + + def _fetchone_impl(self): + try: + return self.cursor.fetchone() + except AttributeError: + self._non_result() + + def _fetchmany_impl(self, size=None): + try: + if size is None: + return self.cursor.fetchmany() + else: + return self.cursor.fetchmany(size) + except AttributeError: + self._non_result() + + def _fetchall_impl(self): + try: + return self.cursor.fetchall() + except AttributeError: + self._non_result() + + def _non_result(self): + if self._metadata is None: + raise exc.ResourceClosedError( + "This result object does not return rows. " + "It has been closed automatically.", + ) + else: + raise exc.ResourceClosedError("This result object is closed.") + + def process_rows(self, rows): + process_row = self._process_row + metadata = self._metadata + keymap = metadata._keymap + processors = metadata._processors + if self._echo: + log = self.context.engine.logger.debug + l = [] + for row in rows: + log("Row %r", row) + l.append(process_row(metadata, row, processors, keymap)) + return l + else: + return [process_row(metadata, row, processors, keymap) + for row in rows] + + def fetchall(self): + """Fetch all rows, just like DB-API ``cursor.fetchall()``.""" + + try: + l = self.process_rows(self._fetchall_impl()) + self.close() + return l + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, + self.cursor, self.context) + + def fetchmany(self, size=None): + """Fetch many rows, just like DB-API + ``cursor.fetchmany(size=cursor.arraysize)``. + + If rows are present, the cursor remains open after this is called. + Else the cursor is automatically closed and an empty list is returned. + + """ + + try: + l = self.process_rows(self._fetchmany_impl(size)) + if len(l) == 0: + self.close() + return l + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, + self.cursor, self.context) + + def fetchone(self): + """Fetch one row, just like DB-API ``cursor.fetchone()``. + + If a row is present, the cursor remains open after this is called. + Else the cursor is automatically closed and None is returned. + + """ + try: + row = self._fetchone_impl() + if row is not None: + return self.process_rows([row])[0] + else: + self.close() + return None + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, + self.cursor, self.context) + + def first(self): + """Fetch the first row and then close the result set unconditionally. + + Returns None if no row is present. + + """ + if self._metadata is None: + self._non_result() + + try: + row = self._fetchone_impl() + except Exception as e: + self.connection._handle_dbapi_exception( + e, None, None, + self.cursor, self.context) + + try: + if row is not None: + return self.process_rows([row])[0] + else: + return None + finally: + self.close() + + def scalar(self): + """Fetch the first column of the first row, and close the result set. + + Returns None if no row is present. + + """ + row = self.first() + if row is not None: + return row[0] + else: + return None + + +class BufferedRowResultProxy(ResultProxy): + """A ResultProxy with row buffering behavior. + + ``ResultProxy`` that buffers the contents of a selection of rows + before ``fetchone()`` is called. This is to allow the results of + ``cursor.description`` to be available immediately, when + interfacing with a DB-API that requires rows to be consumed before + this information is available (currently psycopg2, when used with + server-side cursors). + + The pre-fetching behavior fetches only one row initially, and then + grows its buffer size by a fixed amount with each successive need + for additional rows up to a size of 100. + """ + + def _init_metadata(self): + self.__buffer_rows() + super(BufferedRowResultProxy, self)._init_metadata() + + # this is a "growth chart" for the buffering of rows. + # each successive __buffer_rows call will use the next + # value in the list for the buffer size until the max + # is reached + size_growth = { + 1: 5, + 5: 10, + 10: 20, + 20: 50, + 50: 100, + 100: 250, + 250: 500, + 500: 1000 + } + + def __buffer_rows(self): + size = getattr(self, '_bufsize', 1) + self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) + self._bufsize = self.size_growth.get(size, size) + + def _fetchone_impl(self): + if self.closed: + return None + if not self.__rowbuffer: + self.__buffer_rows() + if not self.__rowbuffer: + return None + return self.__rowbuffer.popleft() + + def _fetchmany_impl(self, size=None): + if size is None: + return self._fetchall_impl() + result = [] + for x in range(0, size): + row = self._fetchone_impl() + if row is None: + break + result.append(row) + return result + + def _fetchall_impl(self): + self.__rowbuffer.extend(self.cursor.fetchall()) + ret = self.__rowbuffer + self.__rowbuffer = collections.deque() + return ret + + +class FullyBufferedResultProxy(ResultProxy): + """A result proxy that buffers rows fully upon creation. + + Used for operations where a result is to be delivered + after the database conversation can not be continued, + such as MSSQL INSERT...OUTPUT after an autocommit. + + """ + def _init_metadata(self): + super(FullyBufferedResultProxy, self)._init_metadata() + self.__rowbuffer = self._buffer_rows() + + def _buffer_rows(self): + return collections.deque(self.cursor.fetchall()) + + def _fetchone_impl(self): + if self.__rowbuffer: + return self.__rowbuffer.popleft() + else: + return None + + def _fetchmany_impl(self, size=None): + if size is None: + return self._fetchall_impl() + result = [] + for x in range(0, size): + row = self._fetchone_impl() + if row is None: + break + result.append(row) + return result + + def _fetchall_impl(self): + ret = self.__rowbuffer + self.__rowbuffer = collections.deque() + return ret + + +class BufferedColumnRow(RowProxy): + def __init__(self, parent, row, processors, keymap): + # preprocess row + row = list(row) + # this is a tad faster than using enumerate + index = 0 + for processor in parent._orig_processors: + if processor is not None: + row[index] = processor(row[index]) + index += 1 + row = tuple(row) + super(BufferedColumnRow, self).__init__(parent, row, + processors, keymap) + + +class BufferedColumnResultProxy(ResultProxy): + """A ResultProxy with column buffering behavior. + + ``ResultProxy`` that loads all columns into memory each time + fetchone() is called. If fetchmany() or fetchall() are called, + the full grid of results is fetched. This is to operate with + databases where result rows contain "live" results that fall out + of scope unless explicitly fetched. Currently this includes + cx_Oracle LOB objects. + + """ + + _process_row = BufferedColumnRow + + def _init_metadata(self): + super(BufferedColumnResultProxy, self)._init_metadata() + metadata = self._metadata + # orig_processors will be used to preprocess each row when they are + # constructed. + metadata._orig_processors = metadata._processors + # replace the all type processors by None processors. + metadata._processors = [None for _ in range(len(metadata.keys))] + keymap = {} + for k, (func, obj, index) in metadata._keymap.items(): + keymap[k] = (None, obj, index) + self._metadata._keymap = keymap + + def fetchall(self): + # can't call cursor.fetchall(), since rows must be + # fully processed before requesting more from the DBAPI. + l = [] + while True: + row = self.fetchone() + if row is None: + break + l.append(row) + return l + + def fetchmany(self, size=None): + # can't call cursor.fetchmany(), since rows must be + # fully processed before requesting more from the DBAPI. + if size is None: + return self.fetchall() + l = [] + for i in range(size): + row = self.fetchone() + if row is None: + break + l.append(row) + return l diff --git a/libs/sqlalchemy/engine/strategies.py b/libs/sqlalchemy/engine/strategies.py index fab97975..f6c06403 100644 --- a/libs/sqlalchemy/engine/strategies.py +++ b/libs/sqlalchemy/engine/strategies.py @@ -1,5 +1,5 @@ # engine/strategies.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -49,18 +49,27 @@ class DefaultEngineStrategy(EngineStrategy): dialect_cls = u.get_dialect() + if kwargs.pop('_coerce_config', False): + def pop_kwarg(key, default=None): + value = kwargs.pop(key, default) + if key in dialect_cls.engine_config_types: + value = dialect_cls.engine_config_types[key](value) + return value + else: + pop_kwarg = kwargs.pop + dialect_args = {} # consume dialect arguments from kwargs for k in util.get_cls_kwargs(dialect_cls): if k in kwargs: - dialect_args[k] = kwargs.pop(k) + dialect_args[k] = pop_kwarg(k) dbapi = kwargs.pop('module', None) if dbapi is None: dbapi_args = {} for k in util.get_func_kwargs(dialect_cls.dbapi): if k in kwargs: - dbapi_args[k] = kwargs.pop(k) + dbapi_args[k] = pop_kwarg(k) dbapi = dialect_cls.dbapi(**dbapi_args) dialect_args['dbapi'] = dbapi @@ -70,33 +79,26 @@ class DefaultEngineStrategy(EngineStrategy): # assemble connection arguments (cargs, cparams) = dialect.create_connect_args(u) - cparams.update(kwargs.pop('connect_args', {})) + cparams.update(pop_kwarg('connect_args', {})) # look for existing pool or create - pool = kwargs.pop('pool', None) + pool = pop_kwarg('pool', None) if pool is None: def connect(): try: return dialect.connect(*cargs, **cparams) - except Exception, e: - # Py3K - #raise exc.DBAPIError.instance(None, None, - # e, dialect.dbapi.Error, - # connection_invalidated= - # dialect.is_disconnect(e, None, None) - # ) from e - # Py2K - import sys - raise exc.DBAPIError.instance( - None, None, e, dialect.dbapi.Error, - connection_invalidated= - dialect.is_disconnect(e, None, None)), \ - None, sys.exc_info()[2] - # end Py2K + except dialect.dbapi.Error as e: + invalidated = dialect.is_disconnect(e, None, None) + util.raise_from_cause( + exc.DBAPIError.instance(None, None, + e, dialect.dbapi.Error, + connection_invalidated=invalidated + ) + ) - creator = kwargs.pop('creator', connect) + creator = pop_kwarg('creator', connect) - poolclass = kwargs.pop('poolclass', None) + poolclass = pop_kwarg('poolclass', None) if poolclass is None: poolclass = dialect_cls.get_pool_class(u) pool_args = {} @@ -107,13 +109,13 @@ class DefaultEngineStrategy(EngineStrategy): 'echo': 'echo_pool', 'timeout': 'pool_timeout', 'recycle': 'pool_recycle', - 'events':'pool_events', - 'use_threadlocal':'pool_threadlocal', - 'reset_on_return':'pool_reset_on_return'} + 'events': 'pool_events', + 'use_threadlocal': 'pool_threadlocal', + 'reset_on_return': 'pool_reset_on_return'} for k in util.get_cls_kwargs(poolclass): tk = translate.get(k, k) if tk in kwargs: - pool_args[k] = kwargs.pop(tk) + pool_args[k] = pop_kwarg(tk) pool = poolclass(creator, **pool_args) else: if isinstance(pool, poollib._DBProxy): @@ -126,7 +128,7 @@ class DefaultEngineStrategy(EngineStrategy): engine_args = {} for k in util.get_cls_kwargs(engineclass): if k in kwargs: - engine_args[k] = kwargs.pop(k) + engine_args[k] = pop_kwarg(k) _initialize = kwargs.pop('_initialize', True) @@ -147,7 +149,8 @@ class DefaultEngineStrategy(EngineStrategy): do_on_connect = dialect.on_connect() if do_on_connect: def on_connect(dbapi_connection, connection_record): - conn = getattr(dbapi_connection, '_sqla_unwrap', dbapi_connection) + conn = getattr( + dbapi_connection, '_sqla_unwrap', dbapi_connection) if conn is None: return do_on_connect(conn) @@ -155,14 +158,10 @@ class DefaultEngineStrategy(EngineStrategy): event.listen(pool, 'first_connect', on_connect) event.listen(pool, 'connect', on_connect) + @util.only_once def first_connect(dbapi_connection, connection_record): - c = base.Connection(engine, connection=dbapi_connection) - - # TODO: removing this allows the on connect activities - # to generate events. tests currently assume these aren't - # sent. do we want users to get all the initial connect - # activities as events ? - c._has_events = False + c = base.Connection(engine, connection=dbapi_connection, + _has_events=False) dialect.initialize(c) event.listen(pool, 'first_connect', first_connect) @@ -238,12 +237,14 @@ class MockEngineStrategy(EngineStrategy): kwargs['checkfirst'] = False from sqlalchemy.engine import ddl - ddl.SchemaGenerator(self.dialect, self, **kwargs).traverse_single(entity) + ddl.SchemaGenerator( + self.dialect, self, **kwargs).traverse_single(entity) def drop(self, entity, **kwargs): kwargs['checkfirst'] = False from sqlalchemy.engine import ddl - ddl.SchemaDropper(self.dialect, self, **kwargs).traverse_single(entity) + ddl.SchemaDropper( + self.dialect, self, **kwargs).traverse_single(entity) def _run_visitor(self, visitorcallable, element, connection=None, diff --git a/libs/sqlalchemy/engine/threadlocal.py b/libs/sqlalchemy/engine/threadlocal.py index c8a16272..ae647a78 100644 --- a/libs/sqlalchemy/engine/threadlocal.py +++ b/libs/sqlalchemy/engine/threadlocal.py @@ -1,21 +1,24 @@ # engine/threadlocal.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Provides a thread-local transactional wrapper around the root Engine class. -The ``threadlocal`` module is invoked when using the ``strategy="threadlocal"`` flag -with :func:`~sqlalchemy.engine.create_engine`. This module is semi-private and is -invoked automatically when the threadlocal engine strategy is used. +The ``threadlocal`` module is invoked when using the +``strategy="threadlocal"`` flag with :func:`~sqlalchemy.engine.create_engine`. +This module is semi-private and is invoked automatically when the threadlocal +engine strategy is used. """ -from sqlalchemy import util, event -from sqlalchemy.engine import base +from .. import util +from . import base import weakref + class TLConnection(base.Connection): + def __init__(self, *arg, **kw): super(TLConnection, self).__init__(*arg, **kw) self.__opencount = 0 @@ -33,16 +36,18 @@ class TLConnection(base.Connection): self.__opencount = 0 base.Connection.close(self) -class TLEngine(base.Engine): - """An Engine that includes support for thread-local managed transactions.""" +class TLEngine(base.Engine): + """An Engine that includes support for thread-local managed + transactions. + + """ _tl_connection_cls = TLConnection def __init__(self, *args, **kwargs): super(TLEngine, self).__init__(*args, **kwargs) self._connections = util.threading.local() - def contextual_connect(self, **kw): if not hasattr(self._connections, 'conn'): connection = None @@ -52,21 +57,24 @@ class TLEngine(base.Engine): if connection is None or connection.closed: # guards against pool-level reapers, if desired. # or not connection.connection.is_valid: - connection = self._tl_connection_cls(self, self.pool.connect(), **kw) - self._connections.conn = conn = weakref.ref(connection) + connection = self._tl_connection_cls( + self, self.pool.connect(), **kw) + self._connections.conn = weakref.ref(connection) return connection._increment_connect() def begin_twophase(self, xid=None): if not hasattr(self._connections, 'trans'): self._connections.trans = [] - self._connections.trans.append(self.contextual_connect().begin_twophase(xid=xid)) + self._connections.trans.append( + self.contextual_connect().begin_twophase(xid=xid)) return self def begin_nested(self): if not hasattr(self._connections, 'trans'): self._connections.trans = [] - self._connections.trans.append(self.contextual_connect().begin_nested()) + self._connections.trans.append( + self.contextual_connect().begin_nested()) return self def begin(self): diff --git a/libs/sqlalchemy/engine/url.py b/libs/sqlalchemy/engine/url.py index 9cabc8dc..78ac0618 100644 --- a/libs/sqlalchemy/engine/url.py +++ b/libs/sqlalchemy/engine/url.py @@ -1,5 +1,5 @@ # engine/url.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,13 +7,16 @@ """Provides the :class:`~sqlalchemy.engine.url.URL` class which encapsulates information about a database connection specification. -The URL object is created automatically when :func:`~sqlalchemy.engine.create_engine` is called -with a string argument; alternatively, the URL is a public-facing construct which can +The URL object is created automatically when +:func:`~sqlalchemy.engine.create_engine` is called with a string +argument; alternatively, the URL is a public-facing construct which can be used directly and is also accepted directly by ``create_engine()``. """ -import re, urllib -from sqlalchemy import exc, util +import re +from .. import exc, util +from . import Dialect +from ..dialects import registry class URL(object): @@ -21,8 +24,8 @@ class URL(object): Represent the components of a URL used to connect to a database. This object is suitable to be passed directly to a - ``create_engine()`` call. The fields of the URL are parsed from a - string by the ``module-level make_url()`` function. the string + :func:`~sqlalchemy.create_engine` call. The fields of the URL are parsed from a + string by the :func:`.make_url` function. the string format of the URL is an RFC-1738-style string. All initialization parameters are available as public attributes. @@ -59,25 +62,35 @@ class URL(object): self.database = database self.query = query or {} - def __str__(self): + def __to_string__(self, hide_password=True): s = self.drivername + "://" if self.username is not None: - s += self.username + s += _rfc_1738_quote(self.username) if self.password is not None: - s += ':' + urllib.quote_plus(self.password) + s += ':' + ('***' if hide_password + else _rfc_1738_quote(self.password)) s += "@" if self.host is not None: - s += self.host + if ':' in self.host: + s += "[%s]" % self.host + else: + s += self.host if self.port is not None: s += ':' + str(self.port) if self.database is not None: s += '/' + self.database if self.query: - keys = self.query.keys() + keys = list(self.query) keys.sort() s += '?' + "&".join("%s=%s" % (k, self.query[k]) for k in keys) return s + def __str__(self): + return self.__to_string__(hide_password=False) + + def __repr__(self): + return self.__to_string__() + def __hash__(self): return hash(str(self)) @@ -96,49 +109,20 @@ class URL(object): to this URL's driver name. """ - try: - if '+' in self.drivername: - dialect, driver = self.drivername.split('+') - else: - dialect, driver = self.drivername, 'base' - - module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects - module = getattr(module, dialect) - if hasattr(module, driver): - module = getattr(module, driver) - else: - module = self._load_entry_point() - if module is None: - raise exc.ArgumentError( - "Could not determine dialect for '%s'." % - self.drivername) - - return module.dialect - except ImportError: - module = self._load_entry_point() - if module is not None: - return module - else: - raise exc.ArgumentError( - "Could not determine dialect for '%s'." % self.drivername) - - def _load_entry_point(self): - """attempt to load this url's dialect from entry points, or return None - if pkg_resources is not installed or there is no matching entry point. - - Raise ImportError if the actual load fails. - - """ - try: - import pkg_resources - except ImportError: - return None - - for res in pkg_resources.iter_entry_points('sqlalchemy.dialects'): - if res.name == self.drivername.replace("+", "."): - return res.load() + if '+' not in self.drivername: + name = self.drivername else: - return None + name = self.drivername.replace('+', '.') + cls = registry.load(name) + # check for legacy dialects that + # would return a module with 'dialect' as the + # actual class + if hasattr(cls, 'dialect') and \ + isinstance(cls.dialect, type) and \ + issubclass(cls.dialect, Dialect): + return cls.dialect + else: + return cls def translate_connect_args(self, names=[], **kw): """Translate url attributes into a dictionary of connection arguments. @@ -150,8 +134,8 @@ class URL(object): :param \**kw: Optional, alternate key names for url attributes. - :param names: Deprecated. Same purpose as the keyword-based alternate names, - but correlates the name to the original positionally. + :param names: Deprecated. Same purpose as the keyword-based alternate + names, but correlates the name to the original positionally. """ translated = {} @@ -167,6 +151,7 @@ class URL(object): translated[name] = getattr(self, sname) return translated + def make_url(name_or_url): """Given a string or unicode instance, produce a new URL instance. @@ -174,25 +159,28 @@ def make_url(name_or_url): existing URL object is passed, just returns the object. """ - if isinstance(name_or_url, basestring): + if isinstance(name_or_url, util.string_types): return _parse_rfc1738_args(name_or_url) else: return name_or_url + def _parse_rfc1738_args(name): pattern = re.compile(r''' (?P[\w\+]+):// (?: (?P[^:/]*) - (?::(?P[^/]*))? + (?::(?P.*))? @)? (?: - (?P[^/:]*) + (?: + \[(?P[^/]+)\] | + (?P[^/:]+) + )? (?::(?P[^/]*))? )? (?:/(?P.*))? - ''' - , re.X) + ''', re.X) m = pattern.match(name) if m is not None: @@ -201,28 +189,39 @@ def _parse_rfc1738_args(name): tokens = components['database'].split('?', 2) components['database'] = tokens[0] query = (len(tokens) > 1 and dict(util.parse_qsl(tokens[1]))) or None - # Py2K - if query is not None: + if util.py2k and query is not None: query = dict((k.encode('ascii'), query[k]) for k in query) - # end Py2K else: query = None components['query'] = query - if components['password'] is not None: - components['password'] = urllib.unquote_plus(components['password']) + if components['username'] is not None: + components['username'] = _rfc_1738_unquote(components['username']) + if components['password'] is not None: + components['password'] = _rfc_1738_unquote(components['password']) + + ipv4host = components.pop('ipv4host') + ipv6host = components.pop('ipv6host') + components['host'] = ipv4host or ipv6host name = components.pop('name') return URL(name, **components) else: raise exc.ArgumentError( "Could not parse rfc1738 URL from string '%s'" % name) + +def _rfc_1738_quote(text): + return re.sub(r'[:@/]', lambda m: "%%%X" % ord(m.group(0)), text) + +def _rfc_1738_unquote(text): + return util.unquote(text) + def _parse_keyvalue_args(name): - m = re.match( r'(\w+)://(.*)', name) + m = re.match(r'(\w+)://(.*)', name) if m is not None: (name, args) = m.group(1, 2) - opts = dict( util.parse_qsl( args ) ) + opts = dict(util.parse_qsl(args)) return URL(name, *opts) else: return None diff --git a/libs/sqlalchemy/engine/util.py b/libs/sqlalchemy/engine/util.py new file mode 100644 index 00000000..6c0644be --- /dev/null +++ b/libs/sqlalchemy/engine/util.py @@ -0,0 +1,72 @@ +# engine/util.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .. import util + +def connection_memoize(key): + """Decorator, memoize a function in a connection.info stash. + + Only applicable to functions which take no arguments other than a + connection. The memo will be stored in ``connection.info[key]``. + """ + + @util.decorator + def decorated(fn, self, connection): + connection = connection.connect() + try: + return connection.info[key] + except KeyError: + connection.info[key] = val = fn(self, connection) + return val + + return decorated + + +def py_fallback(): + def _distill_params(multiparams, params): + """Given arguments from the calling form *multiparams, **params, + return a list of bind parameter structures, usually a list of + dictionaries. + + In the case of 'raw' execution which accepts positional parameters, + it may be a list of tuples or lists. + + """ + + if not multiparams: + if params: + return [params] + else: + return [] + elif len(multiparams) == 1: + zero = multiparams[0] + if isinstance(zero, (list, tuple)): + if not zero or hasattr(zero[0], '__iter__') and \ + not hasattr(zero[0], 'strip'): + # execute(stmt, [{}, {}, {}, ...]) + # execute(stmt, [(), (), (), ...]) + return zero + else: + # execute(stmt, ("value", "value")) + return [zero] + elif hasattr(zero, 'keys'): + # execute(stmt, {"key":"value"}) + return [zero] + else: + # execute(stmt, "value") + return [[zero]] + else: + if hasattr(multiparams[0], '__iter__') and \ + not hasattr(multiparams[0], 'strip'): + return multiparams + else: + return [multiparams] + + return locals() +try: + from sqlalchemy.cutils import _distill_params +except ImportError: + globals().update(py_fallback()) diff --git a/libs/sqlalchemy/event.py b/libs/sqlalchemy/event.py deleted file mode 100644 index dabebb81..00000000 --- a/libs/sqlalchemy/event.py +++ /dev/null @@ -1,460 +0,0 @@ -# sqlalchemy/event.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Base event API.""" - -from sqlalchemy import util, exc -import weakref - -CANCEL = util.symbol('CANCEL') -NO_RETVAL = util.symbol('NO_RETVAL') - -def listen(target, identifier, fn, *args, **kw): - """Register a listener function for the given target. - - e.g.:: - - from sqlalchemy import event - from sqlalchemy.schema import UniqueConstraint - - def unique_constraint_name(const, table): - const.name = "uq_%s_%s" % ( - table.name, - list(const.columns)[0].name - ) - event.listen( - UniqueConstraint, - "after_parent_attach", - unique_constraint_name) - - """ - - for evt_cls in _registrars[identifier]: - tgt = evt_cls._accept_with(target) - if tgt is not None: - tgt.dispatch._listen(tgt, identifier, fn, *args, **kw) - return - raise exc.InvalidRequestError("No such event '%s' for target '%s'" % - (identifier, target)) - -def listens_for(target, identifier, *args, **kw): - """Decorate a function as a listener for the given target + identifier. - - e.g.:: - - from sqlalchemy import event - from sqlalchemy.schema import UniqueConstraint - - @event.listens_for(UniqueConstraint, "after_parent_attach") - def unique_constraint_name(const, table): - const.name = "uq_%s_%s" % ( - table.name, - list(const.columns)[0].name - ) - """ - def decorate(fn): - listen(target, identifier, fn, *args, **kw) - return fn - return decorate - -def remove(target, identifier, fn): - """Remove an event listener. - - Note that some event removals, particularly for those event dispatchers - which create wrapper functions and secondary even listeners, may not yet - be supported. - - """ - for evt_cls in _registrars[identifier]: - for tgt in evt_cls._accept_with(target): - tgt.dispatch._remove(identifier, tgt, fn) - return - -_registrars = util.defaultdict(list) - -def _is_event_name(name): - return not name.startswith('_') and name != 'dispatch' - -class _UnpickleDispatch(object): - """Serializable callable that re-generates an instance of :class:`_Dispatch` - given a particular :class:`.Events` subclass. - - """ - def __call__(self, _parent_cls): - for cls in _parent_cls.__mro__: - if 'dispatch' in cls.__dict__: - return cls.__dict__['dispatch'].dispatch_cls(_parent_cls) - else: - raise AttributeError("No class with a 'dispatch' member present.") - -class _Dispatch(object): - """Mirror the event listening definitions of an Events class with - listener collections. - - Classes which define a "dispatch" member will return a - non-instantiated :class:`._Dispatch` subclass when the member - is accessed at the class level. When the "dispatch" member is - accessed at the instance level of its owner, an instance - of the :class:`._Dispatch` class is returned. - - A :class:`._Dispatch` class is generated for each :class:`.Events` - class defined, by the :func:`._create_dispatcher_class` function. - The original :class:`.Events` classes remain untouched. - This decouples the construction of :class:`.Events` subclasses from - the implementation used by the event internals, and allows - inspecting tools like Sphinx to work in an unsurprising - way against the public API. - - """ - - def __init__(self, _parent_cls): - self._parent_cls = _parent_cls - - def __reduce__(self): - return _UnpickleDispatch(), (self._parent_cls, ) - - def _update(self, other, only_propagate=True): - """Populate from the listeners in another :class:`_Dispatch` - object.""" - - for ls in _event_descriptors(other): - getattr(self, ls.name).\ - for_modify(self)._update(ls, only_propagate=only_propagate) - -def _event_descriptors(target): - return [getattr(target, k) for k in dir(target) if _is_event_name(k)] - -class _EventMeta(type): - """Intercept new Event subclasses and create - associated _Dispatch classes.""" - - def __init__(cls, classname, bases, dict_): - _create_dispatcher_class(cls, classname, bases, dict_) - return type.__init__(cls, classname, bases, dict_) - -def _create_dispatcher_class(cls, classname, bases, dict_): - """Create a :class:`._Dispatch` class corresponding to an - :class:`.Events` class.""" - - # there's all kinds of ways to do this, - # i.e. make a Dispatch class that shares the '_listen' method - # of the Event class, this is the straight monkeypatch. - dispatch_base = getattr(cls, 'dispatch', _Dispatch) - cls.dispatch = dispatch_cls = type("%sDispatch" % classname, - (dispatch_base, ), {}) - dispatch_cls._listen = cls._listen - dispatch_cls._clear = cls._clear - - for k in dict_: - if _is_event_name(k): - setattr(dispatch_cls, k, _DispatchDescriptor(dict_[k])) - _registrars[k].append(cls) - -def _remove_dispatcher(cls): - for k in dir(cls): - if _is_event_name(k): - _registrars[k].remove(cls) - if not _registrars[k]: - del _registrars[k] - -class Events(object): - """Define event listening functions for a particular target type.""" - - - __metaclass__ = _EventMeta - - @classmethod - def _accept_with(cls, target): - # Mapper, ClassManager, Session override this to - # also accept classes, scoped_sessions, sessionmakers, etc. - if hasattr(target, 'dispatch') and ( - isinstance(target.dispatch, cls.dispatch) or \ - isinstance(target.dispatch, type) and \ - issubclass(target.dispatch, cls.dispatch) - ): - return target - else: - return None - - @classmethod - def _listen(cls, target, identifier, fn, propagate=False, insert=False): - if insert: - getattr(target.dispatch, identifier).\ - for_modify(target.dispatch).insert(fn, target, propagate) - else: - getattr(target.dispatch, identifier).\ - for_modify(target.dispatch).append(fn, target, propagate) - - @classmethod - def _remove(cls, target, identifier, fn): - getattr(target.dispatch, identifier).remove(fn, target) - - @classmethod - def _clear(cls): - for attr in dir(cls.dispatch): - if _is_event_name(attr): - getattr(cls.dispatch, attr).clear() - -class _DispatchDescriptor(object): - """Class-level attributes on :class:`._Dispatch` classes.""" - - def __init__(self, fn): - self.__name__ = fn.__name__ - self.__doc__ = fn.__doc__ - self._clslevel = weakref.WeakKeyDictionary() - self._empty_listeners = weakref.WeakKeyDictionary() - - def _contains(self, cls, evt): - return cls in self._clslevel and \ - evt in self._clslevel[cls] - - def insert(self, obj, target, propagate): - assert isinstance(target, type), \ - "Class-level Event targets must be classes." - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls is not target and cls not in self._clslevel: - self.update_subclass(cls) - else: - if cls not in self._clslevel: - self._clslevel[cls] = [] - self._clslevel[cls].insert(0, obj) - - def append(self, obj, target, propagate): - assert isinstance(target, type), \ - "Class-level Event targets must be classes." - - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls is not target and cls not in self._clslevel: - self.update_subclass(cls) - else: - if cls not in self._clslevel: - self._clslevel[cls] = [] - self._clslevel[cls].append(obj) - - def update_subclass(self, target): - if target not in self._clslevel: - self._clslevel[target] = [] - clslevel = self._clslevel[target] - for cls in target.__mro__[1:]: - if cls in self._clslevel: - clslevel.extend([ - fn for fn - in self._clslevel[cls] - if fn not in clslevel - ]) - - def remove(self, obj, target): - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls in self._clslevel: - self._clslevel[cls].remove(obj) - - def clear(self): - """Clear all class level listeners""" - - for dispatcher in self._clslevel.values(): - dispatcher[:] = [] - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _DispatchDescriptor at the class level of - a dispatcher, this returns self. - - """ - return self - - def __get__(self, obj, cls): - if obj is None: - return self - elif obj._parent_cls in self._empty_listeners: - ret = self._empty_listeners[obj._parent_cls] - else: - self._empty_listeners[obj._parent_cls] = ret = \ - _EmptyListener(self, obj._parent_cls) - # assigning it to __dict__ means - # memoized for fast re-access. but more memory. - obj.__dict__[self.__name__] = ret - return ret - -class _EmptyListener(object): - """Serves as a class-level interface to the events - served by a _DispatchDescriptor, when there are no - instance-level events present. - - Is replaced by _ListenerCollection when instance-level - events are added. - - """ - def __init__(self, parent, target_cls): - if target_cls not in parent._clslevel: - parent.update_subclass(target_cls) - self.parent = parent - self.parent_listeners = parent._clslevel[target_cls] - self.name = parent.__name__ - self.propagate = frozenset() - self.listeners = () - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _EmptyListener at the instance level of - a dispatcher, this generates a new - _ListenerCollection, applies it to the instance, - and returns it. - - """ - obj.__dict__[self.name] = result = _ListenerCollection( - self.parent, obj._parent_cls) - return result - - def _needs_modify(self, *args, **kw): - raise NotImplementedError("need to call for_modify()") - - exec_once = insert = append = remove = clear = _needs_modify - - def __call__(self, *args, **kw): - """Execute this event.""" - - for fn in self.parent_listeners: - fn(*args, **kw) - - def __len__(self): - return len(self.parent_listeners) - - def __iter__(self): - return iter(self.parent_listeners) - - def __getitem__(self, index): - return (self.parent_listeners)[index] - - def __nonzero__(self): - return bool(self.parent_listeners) - - -class _ListenerCollection(object): - """Instance-level attributes on instances of :class:`._Dispatch`. - - Represents a collection of listeners. - - As of 0.7.9, _ListenerCollection is only first - created via the _EmptyListener.for_modify() method. - - """ - - _exec_once = False - - def __init__(self, parent, target_cls): - if target_cls not in parent._clslevel: - parent.update_subclass(target_cls) - self.parent_listeners = parent._clslevel[target_cls] - self.name = parent.__name__ - self.listeners = [] - self.propagate = set() - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _ListenerCollection at the instance level of - a dispatcher, this returns self. - - """ - return self - - def exec_once(self, *args, **kw): - """Execute this event, but only if it has not been - executed already for this collection.""" - - if not self._exec_once: - self(*args, **kw) - self._exec_once = True - - def __call__(self, *args, **kw): - """Execute this event.""" - - for fn in self.parent_listeners: - fn(*args, **kw) - for fn in self.listeners: - fn(*args, **kw) - - # I'm not entirely thrilled about the overhead here, - # but this allows class-level listeners to be added - # at any point. - # - # In the absense of instance-level listeners, - # we stay with the _EmptyListener object when called - # at the instance level. - - def __len__(self): - return len(self.parent_listeners + self.listeners) - - def __iter__(self): - return iter(self.parent_listeners + self.listeners) - - def __getitem__(self, index): - return (self.parent_listeners + self.listeners)[index] - - def __nonzero__(self): - return bool(self.listeners or self.parent_listeners) - - def _update(self, other, only_propagate=True): - """Populate from the listeners in another :class:`_Dispatch` - object.""" - - existing_listeners = self.listeners - existing_listener_set = set(existing_listeners) - self.propagate.update(other.propagate) - existing_listeners.extend([l for l - in other.listeners - if l not in existing_listener_set - and not only_propagate or l in self.propagate - ]) - - def insert(self, obj, target, propagate): - if obj not in self.listeners: - self.listeners.insert(0, obj) - if propagate: - self.propagate.add(obj) - - def append(self, obj, target, propagate): - if obj not in self.listeners: - self.listeners.append(obj) - if propagate: - self.propagate.add(obj) - - def remove(self, obj, target): - if obj in self.listeners: - self.listeners.remove(obj) - self.propagate.discard(obj) - - def clear(self): - self.listeners[:] = [] - self.propagate.clear() - -class dispatcher(object): - """Descriptor used by target classes to - deliver the _Dispatch class at the class level - and produce new _Dispatch instances for target - instances. - - """ - def __init__(self, events): - self.dispatch_cls = events.dispatch - self.events = events - - def __get__(self, obj, cls): - if obj is None: - return self.dispatch_cls - obj.__dict__['dispatch'] = disp = self.dispatch_cls(cls) - return disp diff --git a/libs/sqlalchemy/event/__init__.py b/libs/sqlalchemy/event/__init__.py new file mode 100644 index 00000000..b43bf9bf --- /dev/null +++ b/libs/sqlalchemy/event/__init__.py @@ -0,0 +1,10 @@ +# event/__init__.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .api import CANCEL, NO_RETVAL, listen, listens_for, remove, contains +from .base import Events, dispatcher +from .attr import RefCollection +from .legacy import _legacy_signature diff --git a/libs/sqlalchemy/event/api.py b/libs/sqlalchemy/event/api.py new file mode 100644 index 00000000..20e74d90 --- /dev/null +++ b/libs/sqlalchemy/event/api.py @@ -0,0 +1,107 @@ +# event/api.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Public API functions for the event system. + +""" +from __future__ import absolute_import + +from .. import util, exc +from .base import _registrars +from .registry import _EventKey + +CANCEL = util.symbol('CANCEL') +NO_RETVAL = util.symbol('NO_RETVAL') + + +def _event_key(target, identifier, fn): + for evt_cls in _registrars[identifier]: + tgt = evt_cls._accept_with(target) + if tgt is not None: + return _EventKey(target, identifier, fn, tgt) + else: + raise exc.InvalidRequestError("No such event '%s' for target '%s'" % + (identifier, target)) + +def listen(target, identifier, fn, *args, **kw): + """Register a listener function for the given target. + + e.g.:: + + from sqlalchemy import event + from sqlalchemy.schema import UniqueConstraint + + def unique_constraint_name(const, table): + const.name = "uq_%s_%s" % ( + table.name, + list(const.columns)[0].name + ) + event.listen( + UniqueConstraint, + "after_parent_attach", + unique_constraint_name) + + """ + + _event_key(target, identifier, fn).listen(*args, **kw) + + +def listens_for(target, identifier, *args, **kw): + """Decorate a function as a listener for the given target + identifier. + + e.g.:: + + from sqlalchemy import event + from sqlalchemy.schema import UniqueConstraint + + @event.listens_for(UniqueConstraint, "after_parent_attach") + def unique_constraint_name(const, table): + const.name = "uq_%s_%s" % ( + table.name, + list(const.columns)[0].name + ) + """ + def decorate(fn): + listen(target, identifier, fn, *args, **kw) + return fn + return decorate + + +def remove(target, identifier, fn): + """Remove an event listener. + + The arguments here should match exactly those which were sent to + :func:`.listen`; all the event registration which proceeded as a result + of this call will be reverted by calling :func:`.remove` with the same + arguments. + + e.g.:: + + # if a function was registered like this... + @event.listens_for(SomeMappedClass, "before_insert", propagate=True) + def my_listener_function(*arg): + pass + + # ... it's removed like this + event.remove(SomeMappedClass, "before_insert", my_listener_function) + + Above, the listener function associated with ``SomeMappedClass`` was also + propagated to subclasses of ``SomeMappedClass``; the :func:`.remove` function + will revert all of these operations. + + .. versionadded:: 0.9.0 + + """ + _event_key(target, identifier, fn).remove() + +def contains(target, identifier, fn): + """Return True if the given target/ident/fn is set up to listen. + + .. versionadded:: 0.9.0 + + """ + + return _event_key(target, identifier, fn).contains() diff --git a/libs/sqlalchemy/event/attr.py b/libs/sqlalchemy/event/attr.py new file mode 100644 index 00000000..3f894754 --- /dev/null +++ b/libs/sqlalchemy/event/attr.py @@ -0,0 +1,376 @@ +# event/attr.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Attribute implementation for _Dispatch classes. + +The various listener targets for a particular event class are represented +as attributes, which refer to collections of listeners to be fired off. +These collections can exist at the class level as well as at the instance +level. An event is fired off using code like this:: + + some_object.dispatch.first_connect(arg1, arg2) + +Above, ``some_object.dispatch`` would be an instance of ``_Dispatch`` and +``first_connect`` is typically an instance of ``_ListenerCollection`` +if event listeners are present, or ``_EmptyListener`` if none are present. + +The attribute mechanics here spend effort trying to ensure listener functions +are available with a minimum of function call overhead, that unnecessary +objects aren't created (i.e. many empty per-instance listener collections), +as well as that everything is garbage collectable when owning references are +lost. Other features such as "propagation" of listener functions across +many ``_Dispatch`` instances, "joining" of multiple ``_Dispatch`` instances, +as well as support for subclass propagation (e.g. events assigned to +``Pool`` vs. ``QueuePool``) are all implemented here. + +""" + +from __future__ import absolute_import + +from .. import util +from . import registry +from . import legacy +from itertools import chain +import weakref + +class RefCollection(object): + @util.memoized_property + def ref(self): + return weakref.ref(self, registry._collection_gced) + +class _DispatchDescriptor(RefCollection): + """Class-level attributes on :class:`._Dispatch` classes.""" + + def __init__(self, parent_dispatch_cls, fn): + self.__name__ = fn.__name__ + argspec = util.inspect_getargspec(fn) + self.arg_names = argspec.args[1:] + self.has_kw = bool(argspec.keywords) + self.legacy_signatures = list(reversed( + sorted( + getattr(fn, '_legacy_signatures', []), + key=lambda s: s[0] + ) + )) + self.__doc__ = fn.__doc__ = legacy._augment_fn_docs( + self, parent_dispatch_cls, fn) + + self._clslevel = weakref.WeakKeyDictionary() + self._empty_listeners = weakref.WeakKeyDictionary() + + def _adjust_fn_spec(self, fn, named): + if named: + fn = self._wrap_fn_for_kw(fn) + if self.legacy_signatures: + try: + argspec = util.get_callable_argspec(fn, no_self=True) + except ValueError: + pass + else: + fn = legacy._wrap_fn_for_legacy(self, fn, argspec) + return fn + + def _wrap_fn_for_kw(self, fn): + def wrap_kw(*args, **kw): + argdict = dict(zip(self.arg_names, args)) + argdict.update(kw) + return fn(**argdict) + return wrap_kw + + + def insert(self, event_key, propagate): + target = event_key.dispatch_target + assert isinstance(target, type), \ + "Class-level Event targets must be classes." + stack = [target] + while stack: + cls = stack.pop(0) + stack.extend(cls.__subclasses__()) + if cls is not target and cls not in self._clslevel: + self.update_subclass(cls) + else: + if cls not in self._clslevel: + self._clslevel[cls] = [] + self._clslevel[cls].insert(0, event_key._listen_fn) + registry._stored_in_collection(event_key, self) + + def append(self, event_key, propagate): + target = event_key.dispatch_target + assert isinstance(target, type), \ + "Class-level Event targets must be classes." + + stack = [target] + while stack: + cls = stack.pop(0) + stack.extend(cls.__subclasses__()) + if cls is not target and cls not in self._clslevel: + self.update_subclass(cls) + else: + if cls not in self._clslevel: + self._clslevel[cls] = [] + self._clslevel[cls].append(event_key._listen_fn) + registry._stored_in_collection(event_key, self) + + def update_subclass(self, target): + if target not in self._clslevel: + self._clslevel[target] = [] + clslevel = self._clslevel[target] + for cls in target.__mro__[1:]: + if cls in self._clslevel: + clslevel.extend([ + fn for fn + in self._clslevel[cls] + if fn not in clslevel + ]) + + def remove(self, event_key): + target = event_key.dispatch_target + stack = [target] + while stack: + cls = stack.pop(0) + stack.extend(cls.__subclasses__()) + if cls in self._clslevel: + self._clslevel[cls].remove(event_key.fn) + registry._removed_from_collection(event_key, self) + + def clear(self): + """Clear all class level listeners""" + + to_clear = set() + for dispatcher in self._clslevel.values(): + to_clear.update(dispatcher) + dispatcher[:] = [] + registry._clear(self, to_clear) + + def for_modify(self, obj): + """Return an event collection which can be modified. + + For _DispatchDescriptor at the class level of + a dispatcher, this returns self. + + """ + return self + + def __get__(self, obj, cls): + if obj is None: + return self + elif obj._parent_cls in self._empty_listeners: + ret = self._empty_listeners[obj._parent_cls] + else: + self._empty_listeners[obj._parent_cls] = ret = \ + _EmptyListener(self, obj._parent_cls) + # assigning it to __dict__ means + # memoized for fast re-access. but more memory. + obj.__dict__[self.__name__] = ret + return ret + +class _HasParentDispatchDescriptor(object): + def _adjust_fn_spec(self, fn, named): + return self.parent._adjust_fn_spec(fn, named) + +class _EmptyListener(_HasParentDispatchDescriptor): + """Serves as a class-level interface to the events + served by a _DispatchDescriptor, when there are no + instance-level events present. + + Is replaced by _ListenerCollection when instance-level + events are added. + + """ + def __init__(self, parent, target_cls): + if target_cls not in parent._clslevel: + parent.update_subclass(target_cls) + self.parent = parent # _DispatchDescriptor + self.parent_listeners = parent._clslevel[target_cls] + self.name = parent.__name__ + self.propagate = frozenset() + self.listeners = () + + + def for_modify(self, obj): + """Return an event collection which can be modified. + + For _EmptyListener at the instance level of + a dispatcher, this generates a new + _ListenerCollection, applies it to the instance, + and returns it. + + """ + result = _ListenerCollection(self.parent, obj._parent_cls) + if obj.__dict__[self.name] is self: + obj.__dict__[self.name] = result + return result + + def _needs_modify(self, *args, **kw): + raise NotImplementedError("need to call for_modify()") + + exec_once = insert = append = remove = clear = _needs_modify + + def __call__(self, *args, **kw): + """Execute this event.""" + + for fn in self.parent_listeners: + fn(*args, **kw) + + def __len__(self): + return len(self.parent_listeners) + + def __iter__(self): + return iter(self.parent_listeners) + + def __bool__(self): + return bool(self.parent_listeners) + + __nonzero__ = __bool__ + + +class _CompoundListener(_HasParentDispatchDescriptor): + _exec_once = False + + def exec_once(self, *args, **kw): + """Execute this event, but only if it has not been + executed already for this collection.""" + + if not self._exec_once: + self(*args, **kw) + self._exec_once = True + + def __call__(self, *args, **kw): + """Execute this event.""" + + for fn in self.parent_listeners: + fn(*args, **kw) + for fn in self.listeners: + fn(*args, **kw) + + def __len__(self): + return len(self.parent_listeners) + len(self.listeners) + + def __iter__(self): + return chain(self.parent_listeners, self.listeners) + + def __bool__(self): + return bool(self.listeners or self.parent_listeners) + + __nonzero__ = __bool__ + +class _ListenerCollection(RefCollection, _CompoundListener): + """Instance-level attributes on instances of :class:`._Dispatch`. + + Represents a collection of listeners. + + As of 0.7.9, _ListenerCollection is only first + created via the _EmptyListener.for_modify() method. + + """ + + def __init__(self, parent, target_cls): + if target_cls not in parent._clslevel: + parent.update_subclass(target_cls) + self.parent_listeners = parent._clslevel[target_cls] + self.parent = parent + self.name = parent.__name__ + self.listeners = [] + self.propagate = set() + + def for_modify(self, obj): + """Return an event collection which can be modified. + + For _ListenerCollection at the instance level of + a dispatcher, this returns self. + + """ + return self + + def _update(self, other, only_propagate=True): + """Populate from the listeners in another :class:`_Dispatch` + object.""" + + existing_listeners = self.listeners + existing_listener_set = set(existing_listeners) + self.propagate.update(other.propagate) + other_listeners = [l for l + in other.listeners + if l not in existing_listener_set + and not only_propagate or l in self.propagate + ] + + existing_listeners.extend(other_listeners) + + to_associate = other.propagate.union(other_listeners) + registry._stored_in_collection_multi(self, other, to_associate) + + def insert(self, event_key, propagate): + if event_key._listen_fn not in self.listeners: + event_key.prepend_to_list(self, self.listeners) + if propagate: + self.propagate.add(event_key._listen_fn) + + def append(self, event_key, propagate): + if event_key._listen_fn not in self.listeners: + event_key.append_to_list(self, self.listeners) + if propagate: + self.propagate.add(event_key._listen_fn) + + def remove(self, event_key): + self.listeners.remove(event_key._listen_fn) + self.propagate.discard(event_key._listen_fn) + registry._removed_from_collection(event_key, self) + + def clear(self): + registry._clear(self, self.listeners) + self.propagate.clear() + self.listeners[:] = [] + + +class _JoinedDispatchDescriptor(object): + def __init__(self, name): + self.name = name + + def __get__(self, obj, cls): + if obj is None: + return self + else: + obj.__dict__[self.name] = ret = _JoinedListener( + obj.parent, self.name, + getattr(obj.local, self.name) + ) + return ret + + +class _JoinedListener(_CompoundListener): + _exec_once = False + + def __init__(self, parent, name, local): + self.parent = parent + self.name = name + self.local = local + self.parent_listeners = self.local + + @property + def listeners(self): + return getattr(self.parent, self.name) + + def _adjust_fn_spec(self, fn, named): + return self.local._adjust_fn_spec(fn, named) + + def for_modify(self, obj): + self.local = self.parent_listeners = self.local.for_modify(obj) + return self + + def insert(self, event_key, propagate): + self.local.insert(event_key, propagate) + + def append(self, event_key, propagate): + self.local.append(event_key, propagate) + + def remove(self, event_key): + self.local.remove(event_key) + + def clear(self): + raise NotImplementedError() + + diff --git a/libs/sqlalchemy/event/base.py b/libs/sqlalchemy/event/base.py new file mode 100644 index 00000000..5c8d92cb --- /dev/null +++ b/libs/sqlalchemy/event/base.py @@ -0,0 +1,217 @@ +# event/base.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Base implementation classes. + +The public-facing ``Events`` serves as the base class for an event interface; +it's public attributes represent different kinds of events. These attributes +are mirrored onto a ``_Dispatch`` class, which serves as a container for +collections of listener functions. These collections are represented both +at the class level of a particular ``_Dispatch`` class as well as within +instances of ``_Dispatch``. + +""" +from __future__ import absolute_import + +from .. import util +from .attr import _JoinedDispatchDescriptor, _EmptyListener, _DispatchDescriptor + +_registrars = util.defaultdict(list) + + +def _is_event_name(name): + return not name.startswith('_') and name != 'dispatch' + + +class _UnpickleDispatch(object): + """Serializable callable that re-generates an instance of + :class:`_Dispatch` given a particular :class:`.Events` subclass. + + """ + def __call__(self, _parent_cls): + for cls in _parent_cls.__mro__: + if 'dispatch' in cls.__dict__: + return cls.__dict__['dispatch'].dispatch_cls(_parent_cls) + else: + raise AttributeError("No class with a 'dispatch' member present.") + + +class _Dispatch(object): + """Mirror the event listening definitions of an Events class with + listener collections. + + Classes which define a "dispatch" member will return a + non-instantiated :class:`._Dispatch` subclass when the member + is accessed at the class level. When the "dispatch" member is + accessed at the instance level of its owner, an instance + of the :class:`._Dispatch` class is returned. + + A :class:`._Dispatch` class is generated for each :class:`.Events` + class defined, by the :func:`._create_dispatcher_class` function. + The original :class:`.Events` classes remain untouched. + This decouples the construction of :class:`.Events` subclasses from + the implementation used by the event internals, and allows + inspecting tools like Sphinx to work in an unsurprising + way against the public API. + + """ + + _events = None + """reference the :class:`.Events` class which this + :class:`._Dispatch` is created for.""" + + def __init__(self, _parent_cls): + self._parent_cls = _parent_cls + + @util.classproperty + def _listen(cls): + return cls._events._listen + + def _join(self, other): + """Create a 'join' of this :class:`._Dispatch` and another. + + This new dispatcher will dispatch events to both + :class:`._Dispatch` objects. + + """ + if '_joined_dispatch_cls' not in self.__class__.__dict__: + cls = type( + "Joined%s" % self.__class__.__name__, + (_JoinedDispatcher, self.__class__), {} + ) + for ls in _event_descriptors(self): + setattr(cls, ls.name, _JoinedDispatchDescriptor(ls.name)) + + self.__class__._joined_dispatch_cls = cls + return self._joined_dispatch_cls(self, other) + + def __reduce__(self): + return _UnpickleDispatch(), (self._parent_cls, ) + + def _update(self, other, only_propagate=True): + """Populate from the listeners in another :class:`_Dispatch` + object.""" + + for ls in _event_descriptors(other): + if isinstance(ls, _EmptyListener): + continue + getattr(self, ls.name).\ + for_modify(self)._update(ls, only_propagate=only_propagate) + + @util.hybridmethod + def _clear(self): + for attr in dir(self): + if _is_event_name(attr): + getattr(self, attr).for_modify(self).clear() + + +def _event_descriptors(target): + return [getattr(target, k) for k in dir(target) if _is_event_name(k)] + + +class _EventMeta(type): + """Intercept new Event subclasses and create + associated _Dispatch classes.""" + + def __init__(cls, classname, bases, dict_): + _create_dispatcher_class(cls, classname, bases, dict_) + return type.__init__(cls, classname, bases, dict_) + + +def _create_dispatcher_class(cls, classname, bases, dict_): + """Create a :class:`._Dispatch` class corresponding to an + :class:`.Events` class.""" + + # there's all kinds of ways to do this, + # i.e. make a Dispatch class that shares the '_listen' method + # of the Event class, this is the straight monkeypatch. + dispatch_base = getattr(cls, 'dispatch', _Dispatch) + dispatch_cls = type("%sDispatch" % classname, + (dispatch_base, ), {}) + cls._set_dispatch(cls, dispatch_cls) + + for k in dict_: + if _is_event_name(k): + setattr(dispatch_cls, k, _DispatchDescriptor(cls, dict_[k])) + _registrars[k].append(cls) + + if getattr(cls, '_dispatch_target', None): + cls._dispatch_target.dispatch = dispatcher(cls) + + +def _remove_dispatcher(cls): + for k in dir(cls): + if _is_event_name(k): + _registrars[k].remove(cls) + if not _registrars[k]: + del _registrars[k] + +class Events(util.with_metaclass(_EventMeta, object)): + """Define event listening functions for a particular target type.""" + + @staticmethod + def _set_dispatch(cls, dispatch_cls): + # this allows an Events subclass to define additional utility + # methods made available to the target via + # "self.dispatch._events." + # @staticemethod to allow easy "super" calls while in a metaclass + # constructor. + cls.dispatch = dispatch_cls + dispatch_cls._events = cls + + + @classmethod + def _accept_with(cls, target): + # Mapper, ClassManager, Session override this to + # also accept classes, scoped_sessions, sessionmakers, etc. + if hasattr(target, 'dispatch') and ( + isinstance(target.dispatch, cls.dispatch) or \ + isinstance(target.dispatch, type) and \ + issubclass(target.dispatch, cls.dispatch) + ): + return target + else: + return None + + @classmethod + def _listen(cls, event_key, propagate=False, insert=False, named=False): + event_key.base_listen(propagate=propagate, insert=insert, named=named) + + @classmethod + def _remove(cls, event_key): + event_key.remove() + + @classmethod + def _clear(cls): + cls.dispatch._clear() + + +class _JoinedDispatcher(object): + """Represent a connection between two _Dispatch objects.""" + + def __init__(self, local, parent): + self.local = local + self.parent = parent + self._parent_cls = local._parent_cls + + +class dispatcher(object): + """Descriptor used by target classes to + deliver the _Dispatch class at the class level + and produce new _Dispatch instances for target + instances. + + """ + def __init__(self, events): + self.dispatch_cls = events.dispatch + self.events = events + + def __get__(self, obj, cls): + if obj is None: + return self.dispatch_cls + obj.__dict__['dispatch'] = disp = self.dispatch_cls(cls) + return disp + diff --git a/libs/sqlalchemy/event/legacy.py b/libs/sqlalchemy/event/legacy.py new file mode 100644 index 00000000..0b0d290f --- /dev/null +++ b/libs/sqlalchemy/event/legacy.py @@ -0,0 +1,156 @@ +# event/legacy.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Routines to handle adaption of legacy call signatures, +generation of deprecation notes and docstrings. + +""" + +from .. import util + +def _legacy_signature(since, argnames, converter=None): + def leg(fn): + if not hasattr(fn, '_legacy_signatures'): + fn._legacy_signatures = [] + fn._legacy_signatures.append((since, argnames, converter)) + return fn + return leg + +def _wrap_fn_for_legacy(dispatch_descriptor, fn, argspec): + for since, argnames, conv in dispatch_descriptor.legacy_signatures: + if argnames[-1] == "**kw": + has_kw = True + argnames = argnames[0:-1] + else: + has_kw = False + + if len(argnames) == len(argspec.args) \ + and has_kw is bool(argspec.keywords): + + if conv: + assert not has_kw + def wrap_leg(*args): + return fn(*conv(*args)) + else: + def wrap_leg(*args, **kw): + argdict = dict(zip(dispatch_descriptor.arg_names, args)) + args = [argdict[name] for name in argnames] + if has_kw: + return fn(*args, **kw) + else: + return fn(*args) + return wrap_leg + else: + return fn + +def _indent(text, indent): + return "\n".join( + indent + line + for line in text.split("\n") + ) + +def _standard_listen_example(dispatch_descriptor, sample_target, fn): + example_kw_arg = _indent( + "\n".join( + "%(arg)s = kw['%(arg)s']" % {"arg": arg} + for arg in dispatch_descriptor.arg_names[0:2] + ), + " ") + if dispatch_descriptor.legacy_signatures: + current_since = max(since for since, args, conv + in dispatch_descriptor.legacy_signatures) + else: + current_since = None + text = ( + "from sqlalchemy import event\n\n" + "# standard decorator style%(current_since)s\n" + "@event.listens_for(%(sample_target)s, '%(event_name)s')\n" + "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n" + " \"listen for the '%(event_name)s' event\"\n" + "\n # ... (event handling logic) ...\n" + ) + + if len(dispatch_descriptor.arg_names) > 2: + text += ( + + "\n# named argument style (new in 0.9)\n" + "@event.listens_for(%(sample_target)s, '%(event_name)s', named=True)\n" + "def receive_%(event_name)s(**kw):\n" + " \"listen for the '%(event_name)s' event\"\n" + "%(example_kw_arg)s\n" + "\n # ... (event handling logic) ...\n" + ) + + text %= { + "current_since": " (arguments as of %s)" % + current_since if current_since else "", + "event_name": fn.__name__, + "has_kw_arguments": ", **kw" if dispatch_descriptor.has_kw else "", + "named_event_arguments": ", ".join(dispatch_descriptor.arg_names), + "example_kw_arg": example_kw_arg, + "sample_target": sample_target + } + return text + +def _legacy_listen_examples(dispatch_descriptor, sample_target, fn): + text = "" + for since, args, conv in dispatch_descriptor.legacy_signatures: + text += ( + "\n# legacy calling style (pre-%(since)s)\n" + "@event.listens_for(%(sample_target)s, '%(event_name)s')\n" + "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n" + " \"listen for the '%(event_name)s' event\"\n" + "\n # ... (event handling logic) ...\n" % { + "since": since, + "event_name": fn.__name__, + "has_kw_arguments": " **kw" if dispatch_descriptor.has_kw else "", + "named_event_arguments": ", ".join(args), + "sample_target": sample_target + } + ) + return text + +def _version_signature_changes(dispatch_descriptor): + since, args, conv = dispatch_descriptor.legacy_signatures[0] + return ( + "\n.. versionchanged:: %(since)s\n" + " The ``%(event_name)s`` event now accepts the \n" + " arguments ``%(named_event_arguments)s%(has_kw_arguments)s``.\n" + " Listener functions which accept the previous argument \n" + " signature(s) listed above will be automatically \n" + " adapted to the new signature." % { + "since": since, + "event_name": dispatch_descriptor.__name__, + "named_event_arguments": ", ".join(dispatch_descriptor.arg_names), + "has_kw_arguments": ", **kw" if dispatch_descriptor.has_kw else "" + } + ) + +def _augment_fn_docs(dispatch_descriptor, parent_dispatch_cls, fn): + header = ".. container:: event_signatures\n\n"\ + " Example argument forms::\n"\ + "\n" + + sample_target = getattr(parent_dispatch_cls, "_target_class_doc", "obj") + text = ( + header + + _indent( + _standard_listen_example( + dispatch_descriptor, sample_target, fn), + " " * 8) + ) + if dispatch_descriptor.legacy_signatures: + text += _indent( + _legacy_listen_examples( + dispatch_descriptor, sample_target, fn), + " " * 8) + + text += _version_signature_changes(dispatch_descriptor) + + return util.inject_docstring_text(fn.__doc__, + text, + 1 + ) diff --git a/libs/sqlalchemy/event/registry.py b/libs/sqlalchemy/event/registry.py new file mode 100644 index 00000000..7710ff2d --- /dev/null +++ b/libs/sqlalchemy/event/registry.py @@ -0,0 +1,236 @@ +# event/registry.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Provides managed registration services on behalf of :func:`.listen` +arguments. + +By "managed registration", we mean that event listening functions and +other objects can be added to various collections in such a way that their +membership in all those collections can be revoked at once, based on +an equivalent :class:`._EventKey`. + +""" + +from __future__ import absolute_import + +import weakref +import collections +import types +from .. import exc + + +_key_to_collection = collections.defaultdict(dict) +""" +Given an original listen() argument, can locate all +listener collections and the listener fn contained + +(target, identifier, fn) -> { + ref(listenercollection) -> ref(listener_fn) + ref(listenercollection) -> ref(listener_fn) + ref(listenercollection) -> ref(listener_fn) + } +""" + +_collection_to_key = collections.defaultdict(dict) +""" +Given a _ListenerCollection or _DispatchDescriptor, can locate +all the original listen() arguments and the listener fn contained + +ref(listenercollection) -> { + ref(listener_fn) -> (target, identifier, fn), + ref(listener_fn) -> (target, identifier, fn), + ref(listener_fn) -> (target, identifier, fn), + } +""" + +def _collection_gced(ref): + # defaultdict, so can't get a KeyError + if not _collection_to_key or ref not in _collection_to_key: + return + listener_to_key = _collection_to_key.pop(ref) + for key in listener_to_key.values(): + if key in _key_to_collection: + # defaultdict, so can't get a KeyError + dispatch_reg = _key_to_collection[key] + dispatch_reg.pop(ref) + if not dispatch_reg: + _key_to_collection.pop(key) + +def _stored_in_collection(event_key, owner): + key = event_key._key + + dispatch_reg = _key_to_collection[key] + + owner_ref = owner.ref + listen_ref = weakref.ref(event_key._listen_fn) + + if owner_ref in dispatch_reg: + assert dispatch_reg[owner_ref] == listen_ref + else: + dispatch_reg[owner_ref] = listen_ref + + listener_to_key = _collection_to_key[owner_ref] + listener_to_key[listen_ref] = key + +def _removed_from_collection(event_key, owner): + key = event_key._key + + dispatch_reg = _key_to_collection[key] + + listen_ref = weakref.ref(event_key._listen_fn) + + owner_ref = owner.ref + dispatch_reg.pop(owner_ref, None) + if not dispatch_reg: + del _key_to_collection[key] + + if owner_ref in _collection_to_key: + listener_to_key = _collection_to_key[owner_ref] + listener_to_key.pop(listen_ref) + +def _stored_in_collection_multi(newowner, oldowner, elements): + if not elements: + return + + oldowner = oldowner.ref + newowner = newowner.ref + + old_listener_to_key = _collection_to_key[oldowner] + new_listener_to_key = _collection_to_key[newowner] + + for listen_fn in elements: + listen_ref = weakref.ref(listen_fn) + key = old_listener_to_key[listen_ref] + dispatch_reg = _key_to_collection[key] + if newowner in dispatch_reg: + assert dispatch_reg[newowner] == listen_ref + else: + dispatch_reg[newowner] = listen_ref + + new_listener_to_key[listen_ref] = key + +def _clear(owner, elements): + if not elements: + return + + owner = owner.ref + listener_to_key = _collection_to_key[owner] + for listen_fn in elements: + listen_ref = weakref.ref(listen_fn) + key = listener_to_key[listen_ref] + dispatch_reg = _key_to_collection[key] + dispatch_reg.pop(owner, None) + + if not dispatch_reg: + del _key_to_collection[key] + + +class _EventKey(object): + """Represent :func:`.listen` arguments. + """ + + + def __init__(self, target, identifier, fn, dispatch_target, _fn_wrap=None): + self.target = target + self.identifier = identifier + self.fn = fn + if isinstance(fn, types.MethodType): + self.fn_key = id(fn.__func__), id(fn.__self__) + else: + self.fn_key = id(fn) + self.fn_wrap = _fn_wrap + self.dispatch_target = dispatch_target + + @property + def _key(self): + return (id(self.target), self.identifier, self.fn_key) + + def with_wrapper(self, fn_wrap): + if fn_wrap is self._listen_fn: + return self + else: + return _EventKey( + self.target, + self.identifier, + self.fn, + self.dispatch_target, + _fn_wrap=fn_wrap + ) + + def with_dispatch_target(self, dispatch_target): + if dispatch_target is self.dispatch_target: + return self + else: + return _EventKey( + self.target, + self.identifier, + self.fn, + dispatch_target, + _fn_wrap=self.fn_wrap + ) + + def listen(self, *args, **kw): + self.dispatch_target.dispatch._listen(self, *args, **kw) + + def remove(self): + key = self._key + + if key not in _key_to_collection: + raise exc.InvalidRequestError( + "No listeners found for event %s / %r / %s " % + (self.target, self.identifier, self.fn) + ) + dispatch_reg = _key_to_collection.pop(key) + + for collection_ref, listener_ref in dispatch_reg.items(): + collection = collection_ref() + listener_fn = listener_ref() + if collection is not None and listener_fn is not None: + collection.remove(self.with_wrapper(listener_fn)) + + def contains(self): + """Return True if this event key is registered to listen. + """ + return self._key in _key_to_collection + + def base_listen(self, propagate=False, insert=False, + named=False): + + target, identifier, fn = \ + self.dispatch_target, self.identifier, self._listen_fn + + dispatch_descriptor = getattr(target.dispatch, identifier) + + fn = dispatch_descriptor._adjust_fn_spec(fn, named) + self = self.with_wrapper(fn) + + if insert: + dispatch_descriptor.\ + for_modify(target.dispatch).insert(self, propagate) + else: + dispatch_descriptor.\ + for_modify(target.dispatch).append(self, propagate) + + @property + def _listen_fn(self): + return self.fn_wrap or self.fn + + def append_value_to_list(self, owner, list_, value): + _stored_in_collection(self, owner) + list_.append(value) + + def append_to_list(self, owner, list_): + _stored_in_collection(self, owner) + list_.append(self._listen_fn) + + def remove_from_list(self, owner, list_): + _removed_from_collection(self, owner) + list_.remove(self._listen_fn) + + def prepend_to_list(self, owner, list_): + _stored_in_collection(self, owner) + list_.insert(0, self._listen_fn) + diff --git a/libs/sqlalchemy/events.py b/libs/sqlalchemy/events.py index e7aa34f2..cf77bbb7 100644 --- a/libs/sqlalchemy/events.py +++ b/libs/sqlalchemy/events.py @@ -1,20 +1,20 @@ # sqlalchemy/events.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Core event interfaces.""" -from sqlalchemy import event, exc, util -engine = util.importlater('sqlalchemy', 'engine') -pool = util.importlater('sqlalchemy', 'pool') - +from . import event, exc +from .pool import Pool +from .engine import Connectable, Engine +from .sql.base import SchemaEventTarget class DDLEvents(event.Events): """ Define event listeners for schema objects, - that is, :class:`.SchemaItem` and :class:`.SchemaEvent` + that is, :class:`.SchemaItem` and other :class:`.SchemaEventTarget` subclasses, including :class:`.MetaData`, :class:`.Table`, :class:`.Column`. @@ -70,6 +70,9 @@ class DDLEvents(event.Events): """ + _target_class_doc = "SomeSchemaClassOrObject" + _dispatch_target = SchemaEventTarget + def before_create(self, target, connection, **kw): """Called before CREATE statments are emitted. @@ -166,7 +169,7 @@ class DDLEvents(event.Events): """ - def column_reflect(self, table, column_info): + def column_reflect(self, inspector, table, column_info): """Called for each unit of 'column info' retrieved when a :class:`.Table` is being reflected. @@ -188,7 +191,7 @@ class DDLEvents(event.Events): from sqlalchemy.schema import Table from sqlalchemy import event - def listen_for_reflect(table, column_info): + def listen_for_reflect(inspector, table, column_info): "receive a column_reflect event" # ... @@ -200,7 +203,7 @@ class DDLEvents(event.Events): ...or with a specific :class:`.Table` instance using the ``listeners`` argument:: - def listen_for_reflect(table, column_info): + def listen_for_reflect(inspector, table, column_info): "receive a column_reflect event" # ... @@ -216,23 +219,7 @@ class DDLEvents(event.Events): """ -class SchemaEventTarget(object): - """Base class for elements that are the targets of :class:`.DDLEvents` events. - This includes :class:`.SchemaItem` as well as :class:`.SchemaType`. - - """ - dispatch = event.dispatcher(DDLEvents) - - def _set_parent(self, parent): - """Associate with this SchemaEvent's parent object.""" - - raise NotImplementedError() - - def _set_parent_with_dispatch(self, parent): - self.dispatch.before_parent_attach(self, parent) - self._set_parent(parent) - self.dispatch.after_parent_attach(self, parent) class PoolEvents(event.Events): """Available events for :class:`.Pool`. @@ -250,11 +237,11 @@ class PoolEvents(event.Events): event.listen(Pool, 'checkout', my_on_checkout) - In addition to accepting the :class:`.Pool` class and :class:`.Pool` instances, - :class:`.PoolEvents` also accepts :class:`.Engine` objects and - the :class:`.Engine` class as targets, which will be resolved - to the ``.pool`` attribute of the given engine or the :class:`.Pool` - class:: + In addition to accepting the :class:`.Pool` class and + :class:`.Pool` instances, :class:`.PoolEvents` also accepts + :class:`.Engine` objects and the :class:`.Engine` class as + targets, which will be resolved to the ``.pool`` attribute of the + given engine or the :class:`.Pool` class:: engine = create_engine("postgresql://scott:tiger@localhost/test") @@ -263,14 +250,17 @@ class PoolEvents(event.Events): """ + _target_class_doc = "SomeEngineOrPool" + _dispatch_target = Pool + @classmethod def _accept_with(cls, target): if isinstance(target, type): - if issubclass(target, engine.Engine): - return pool.Pool - elif issubclass(target, pool.Pool): + if issubclass(target, Engine): + return Pool + elif issubclass(target, Pool): return target - elif isinstance(target, engine.Engine): + elif isinstance(target, Engine): return target.pool else: return target @@ -316,6 +306,10 @@ class PoolEvents(event.Events): connection will be disposed and a fresh connection retrieved. Processing of all checkout listeners will abort and restart using the new connection. + + .. seealso:: :meth:`.ConnectionEvents.engine_connect` - a similar event + which occurs upon creation of a new :class:`.Connection`. + """ def checkin(self, dbapi_connection, connection_record): @@ -333,22 +327,95 @@ class PoolEvents(event.Events): """ + def reset(self, dbapi_con, con_record): + """Called before the "reset" action occurs for a pooled connection. + + This event represents + when the ``rollback()`` method is called on the DBAPI connection + before it is returned to the pool. The behavior of "reset" can + be controlled, including disabled, using the ``reset_on_return`` + pool argument. + + + The :meth:`.PoolEvents.reset` event is usually followed by the + the :meth:`.PoolEvents.checkin` event is called, except in those + cases where the connection is discarded immediately after reset. + + :param dbapi_con: + A raw DB-API connection + + :param con_record: + The ``_ConnectionRecord`` that persistently manages the connection + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.ConnectionEvents.rollback` + + :meth:`.ConnectionEvents.commit` + + """ + + + class ConnectionEvents(event.Events): - """Available events for :class:`.Connection`. + """Available events for :class:`.Connectable`, which includes + :class:`.Connection` and :class:`.Engine`. - The methods here define the name of an event as well as the names of members that are passed to listener functions. + The methods here define the name of an event as well as the names of + members that are passed to listener functions. - e.g.:: + An event listener can be associated with any :class:`.Connectable` + class or instance, such as an :class:`.Engine`, e.g.:: from sqlalchemy import event, create_engine - def before_execute(conn, clauseelement, multiparams, params): - log.info("Received statement: %s" % clauseelement) + def before_cursor_execute(conn, cursor, statement, parameters, context, + executemany): + log.info("Received statement: %s" % statement) engine = create_engine('postgresql://scott:tiger@localhost/test') - event.listen(engine, "before_execute", before_execute) + event.listen(engine, "before_cursor_execute", before_cursor_execute) - Some events allow modifiers to the listen() function. + or with a specific :class:`.Connection`:: + + with engine.begin() as conn: + @event.listens_for(conn, 'before_cursor_execute') + def before_cursor_execute(conn, cursor, statement, parameters, + context, executemany): + log.info("Received statement: %s" % statement) + + The :meth:`.before_execute` and :meth:`.before_cursor_execute` + events can also be established with the ``retval=True`` flag, which + allows modification of the statement and parameters to be sent + to the database. The :meth:`.before_cursor_execute` event is + particularly useful here to add ad-hoc string transformations, such + as comments, to all executions:: + + from sqlalchemy.engine import Engine + from sqlalchemy import event + + @event.listens_for(Engine, "before_cursor_execute", retval=True) + def comment_sql_calls(conn, cursor, statement, parameters, + context, executemany): + statement = statement + " -- some comment" + return statement, parameters + + .. note:: :class:`.ConnectionEvents` can be established on any + combination of :class:`.Engine`, :class:`.Connection`, as well + as instances of each of those classes. Events across all + four scopes will fire off for a given instance of + :class:`.Connection`. However, for performance reasons, the + :class:`.Connection` object determines at instantiation time + whether or not its parent :class:`.Engine` has event listeners + established. Event listeners added to the :class:`.Engine` + class or to an instance of :class:`.Engine` *after* the instantiation + of a dependent :class:`.Connection` instance will usually + *not* be available on that :class:`.Connection` instance. The newly + added listeners will instead take effect for :class:`.Connection` + instances created subsequent to those event listeners being + established on the parent :class:`.Engine` class or instance. :param retval=False: Applies to the :meth:`.before_execute` and :meth:`.before_cursor_execute` events only. When True, the @@ -357,49 +424,153 @@ class ConnectionEvents(event.Events): and parameters. See those methods for a description of specific return arguments. + .. versionchanged:: 0.8 :class:`.ConnectionEvents` can now be associated + with any :class:`.Connectable` including :class:`.Connection`, + in addition to the existing support for :class:`.Engine`. + """ + _target_class_doc = "SomeEngine" + _dispatch_target = Connectable + + @classmethod - def _listen(cls, target, identifier, fn, retval=False): + def _listen(cls, event_key, retval=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + target._has_events = True if not retval: if identifier == 'before_execute': orig_fn = fn - def wrap(conn, clauseelement, multiparams, params): + + def wrap_before_execute(conn, clauseelement, + multiparams, params): orig_fn(conn, clauseelement, multiparams, params) return clauseelement, multiparams, params - fn = wrap + fn = wrap_before_execute elif identifier == 'before_cursor_execute': orig_fn = fn - def wrap(conn, cursor, statement, + + def wrap_before_cursor_execute(conn, cursor, statement, parameters, context, executemany): orig_fn(conn, cursor, statement, parameters, context, executemany) return statement, parameters - fn = wrap + fn = wrap_before_cursor_execute - elif retval and identifier not in ('before_execute', 'before_cursor_execute'): + elif retval and \ + identifier not in ('before_execute', 'before_cursor_execute'): raise exc.ArgumentError( "Only the 'before_execute' and " "'before_cursor_execute' engine " "event listeners accept the 'retval=True' " "argument.") - event.Events._listen(target, identifier, fn) + event_key.with_wrapper(fn).base_listen() def before_execute(self, conn, clauseelement, multiparams, params): - """Intercept high level execute() events.""" + """Intercept high level execute() events, receiving uncompiled + SQL constructs and other objects prior to rendering into SQL. + + This event is good for debugging SQL compilation issues as well + as early manipulation of the parameters being sent to the database, + as the parameter lists will be in a consistent format here. + + This event can be optionally established with the ``retval=True`` + flag. The ``clauseelement``, ``multiparams``, and ``params`` + arguments should be returned as a three-tuple in this case:: + + @event.listens_for(Engine, "before_execute", retval=True) + def before_execute(conn, conn, clauseelement, multiparams, params): + # do something with clauseelement, multiparams, params + return clauseelement, multiparams, params + + :param conn: :class:`.Connection` object + :param clauseelement: SQL expression construct, :class:`.Compiled` + instance, or string statement passed to :meth:`.Connection.execute`. + :param multiparams: Multiple parameter sets, a list of dictionaries. + :param params: Single parameter set, a single dictionary. + + See also: + + :meth:`.before_cursor_execute` + + """ def after_execute(self, conn, clauseelement, multiparams, params, result): - """Intercept high level execute() events.""" + """Intercept high level execute() events after execute. + + + :param conn: :class:`.Connection` object + :param clauseelement: SQL expression construct, :class:`.Compiled` + instance, or string statement passed to :meth:`.Connection.execute`. + :param multiparams: Multiple parameter sets, a list of dictionaries. + :param params: Single parameter set, a single dictionary. + :param result: :class:`.ResultProxy` generated by the execution. + + """ def before_cursor_execute(self, conn, cursor, statement, parameters, context, executemany): - """Intercept low-level cursor execute() events.""" + """Intercept low-level cursor execute() events before execution, + receiving the string + SQL statement and DBAPI-specific parameter list to be invoked + against a cursor. + + This event is a good choice for logging as well as late modifications + to the SQL string. It's less ideal for parameter modifications except + for those which are specific to a target backend. + + This event can be optionally established with the ``retval=True`` + flag. The ``statement`` and ``parameters`` arguments should be + returned as a two-tuple in this case:: + + @event.listens_for(Engine, "before_cursor_execute", retval=True) + def before_cursor_execute(conn, cursor, statement, + parameters, context, executemany): + # do something with statement, parameters + return statement, parameters + + See the example at :class:`.ConnectionEvents`. + + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object + :param statement: string SQL statement + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param executemany: boolean, if ``True``, this is an ``executemany()`` + call, if ``False``, this is an ``execute()`` call. + + See also: + + :meth:`.before_execute` + + :meth:`.after_cursor_execute` + + """ def after_cursor_execute(self, conn, cursor, statement, parameters, context, executemany): - """Intercept low-level cursor execute() events.""" + """Intercept low-level cursor execute() events after execution. + + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object. Will have results pending + if the statement was a SELECT, but these should not be consumed + as they will be needed by the :class:`.ResultProxy`. + :param statement: string SQL statement + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param executemany: boolean, if ``True``, this is an ``executemany()`` + call, if ``False``, this is an ``execute()`` call. + + """ def dbapi_error(self, conn, cursor, statement, parameters, context, exception): @@ -427,37 +598,212 @@ class ConnectionEvents(event.Events): exception is then wrapped in a SQLAlchemy DBAPI exception wrapper and re-thrown. + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object + :param statement: string SQL statement + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param exception: The **unwrapped** exception emitted directly from the + DBAPI. The class here is specific to the DBAPI module in use. + .. versionadded:: 0.7.7 """ + def engine_connect(self, conn, branch): + """Intercept the creation of a new :class:`.Connection`. + + This event is called typically as the direct result of calling + the :meth:`.Engine.connect` method. + + It differs from the :meth:`.PoolEvents.connect` method, which + refers to the actual connection to a database at the DBAPI level; + a DBAPI connection may be pooled and reused for many operations. + In contrast, this event refers only to the production of a higher level + :class:`.Connection` wrapper around such a DBAPI connection. + + It also differs from the :meth:`.PoolEvents.checkout` event + in that it is specific to the :class:`.Connection` object, not the + DBAPI connection that :meth:`.PoolEvents.checkout` deals with, although + this DBAPI connection is available here via the :attr:`.Connection.connection` + attribute. But note there can in fact + be multiple :meth:`.PoolEvents.checkout` events within the lifespan + of a single :class:`.Connection` object, if that :class:`.Connection` + is invalidated and re-established. There can also be multiple + :class:`.Connection` objects generated for the same already-checked-out + DBAPI connection, in the case that a "branch" of a :class:`.Connection` + is produced. + + :param conn: :class:`.Connection` object. + :param branch: if True, this is a "branch" of an existing + :class:`.Connection`. A branch is generated within the course + of a statement execution to invoke supplemental statements, most + typically to pre-execute a SELECT of a default value for the purposes + of an INSERT statement. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.PoolEvents.checkout` the lower-level pool checkout event + for an individual DBAPI connection + + :meth:`.ConnectionEvents.set_connection_execution_options` - a copy of a + :class:`.Connection` is also made when the + :meth:`.Connection.execution_options` method is called. + + """ + + def set_connection_execution_options(self, conn, opts): + """Intercept when the :meth:`.Connection.execution_options` + method is called. + + This method is called after the new :class:`.Connection` has been + produced, with the newly updated execution options collection, but + before the :class:`.Dialect` has acted upon any of those new options. + + Note that this method is not called when a new :class:`.Connection` + is produced which is inheriting execution options from its parent + :class:`.Engine`; to intercept this condition, use the + :meth:`.ConnectionEvents.engine_connect` event. + + :param conn: The newly copied :class:`.Connection` object + + :param opts: dictionary of options that were passed to the + :meth:`.Connection.execution_options` method. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.ConnectionEvents.set_engine_execution_options` - event + which is called when :meth:`.Engine.execution_options` is called. + + + """ + + def set_engine_execution_options(self, engine, opts): + """Intercept when the :meth:`.Engine.execution_options` + method is called. + + The :meth:`.Engine.execution_options` method produces a shallow + copy of the :class:`.Engine` which stores the new options. That new + :class:`.Engine` is passed here. A particular application of this + method is to add a :meth:`.ConnectionEvents.engine_connect` event + handler to the given :class:`.Engine` which will perform some per- + :class:`.Connection` task specific to these execution options. + + :param conn: The newly copied :class:`.Engine` object + + :param opts: dictionary of options that were passed to the + :meth:`.Connection.execution_options` method. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.ConnectionEvents.set_connection_execution_options` - event + which is called when :meth:`.Connection.execution_options` is called. + + """ + def begin(self, conn): - """Intercept begin() events.""" + """Intercept begin() events. + + :param conn: :class:`.Connection` object + + """ def rollback(self, conn): - """Intercept rollback() events.""" + """Intercept rollback() events, as initiated by a + :class:`.Transaction`. + + Note that the :class:`.Pool` also "auto-rolls back" + a DBAPI connection upon checkin, if the ``reset_on_return`` + flag is set to its default value of ``'rollback'``. + To intercept this + rollback, use the :meth:`.PoolEvents.reset` hook. + + :param conn: :class:`.Connection` object + + .. seealso:: + + :meth:`.PoolEvents.reset` + + """ def commit(self, conn): - """Intercept commit() events.""" + """Intercept commit() events, as initiated by a + :class:`.Transaction`. - def savepoint(self, conn, name=None): - """Intercept savepoint() events.""" + Note that the :class:`.Pool` may also "auto-commit" + a DBAPI connection upon checkin, if the ``reset_on_return`` + flag is set to the value ``'commit'``. To intercept this + commit, use the :meth:`.PoolEvents.reset` hook. + + :param conn: :class:`.Connection` object + """ + + def savepoint(self, conn, name): + """Intercept savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + + """ def rollback_savepoint(self, conn, name, context): - """Intercept rollback_savepoint() events.""" + """Intercept rollback_savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + :param context: :class:`.ExecutionContext` in use. May be ``None``. + + """ def release_savepoint(self, conn, name, context): - """Intercept release_savepoint() events.""" + """Intercept release_savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + :param context: :class:`.ExecutionContext` in use. May be ``None``. + + """ def begin_twophase(self, conn, xid): - """Intercept begin_twophase() events.""" + """Intercept begin_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + + """ def prepare_twophase(self, conn, xid): - """Intercept prepare_twophase() events.""" + """Intercept prepare_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + """ def rollback_twophase(self, conn, xid, is_prepared): - """Intercept rollback_twophase() events.""" + """Intercept rollback_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + :param is_prepared: boolean, indicates if + :meth:`.TwoPhaseTransaction.prepare` was called. + + """ def commit_twophase(self, conn, xid, is_prepared): - """Intercept commit_twophase() events.""" + """Intercept commit_twophase() events. + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + :param is_prepared: boolean, indicates if + :meth:`.TwoPhaseTransaction.prepare` was called. + + """ diff --git a/libs/sqlalchemy/exc.py b/libs/sqlalchemy/exc.py index febee3fe..fe6879b1 100644 --- a/libs/sqlalchemy/exc.py +++ b/libs/sqlalchemy/exc.py @@ -1,19 +1,20 @@ # sqlalchemy/exc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Exceptions used with SQLAlchemy. -The base exception class is :class:`.SQLAlchemyError`. Exceptions which are raised as a -result of DBAPI exceptions are all subclasses of -:class:`.DBAPIError`. +The base exception class is :exc:`.SQLAlchemyError`. Exceptions which are +raised as a result of DBAPI exceptions are all subclasses of +:exc:`.DBAPIError`. """ import traceback + class SQLAlchemyError(Exception): """Generic error class.""" @@ -26,6 +27,16 @@ class ArgumentError(SQLAlchemyError): """ +class NoForeignKeysError(ArgumentError): + """Raised when no foreign keys can be located between two selectables + during a join.""" + + +class AmbiguousForeignKeysError(ArgumentError): + """Raised when more than one foreign key matching can be located + between two selectables during a join.""" + + class CircularDependencyError(SQLAlchemyError): """Raised by topological sorts when a circular dependency is detected. @@ -57,30 +68,39 @@ class CircularDependencyError(SQLAlchemyError): return self.__class__, (None, self.cycles, self.edges, self.args[0]) + class CompileError(SQLAlchemyError): """Raised when an error occurs during SQL compilation""" +class UnsupportedCompilationError(CompileError): + """Raised when an operation is not supported by the given compiler. + + + .. versionadded:: 0.8.3 + + """ + + def __init__(self, compiler, element_type): + super(UnsupportedCompilationError, self).__init__( + "Compiler %r can't render element of type %s" % + (compiler, element_type)) + class IdentifierError(SQLAlchemyError): """Raised when a schema name is beyond the max character limit""" -# Moved to orm.exc; compatibility definition installed by orm import until 0.6 -ConcurrentModificationError = None class DisconnectionError(SQLAlchemyError): """A disconnect is detected on a raw DB-API connection. This error is raised and consumed internally by a connection pool. It can - be raised by the :meth:`.PoolEvents.checkout` event - so that the host pool forces a retry; the exception will be caught - three times in a row before the pool gives up and raises - :class:`~sqlalchemy.exc.InvalidRequestError` regarding the connection attempt. + be raised by the :meth:`.PoolEvents.checkout` event so that the host pool + forces a retry; the exception will be caught three times in a row before + the pool gives up and raises :class:`~sqlalchemy.exc.InvalidRequestError` + regarding the connection attempt. """ -# Moved to orm.exc; compatibility definition installed by orm import until 0.6 -FlushError = None - class TimeoutError(SQLAlchemyError): """Raised when a connection pool times out on getting a connection.""" @@ -92,19 +112,30 @@ class InvalidRequestError(SQLAlchemyError): """ + +class NoInspectionAvailable(InvalidRequestError): + """A subject passed to :func:`sqlalchemy.inspection.inspect` produced + no context for inspection.""" + + class ResourceClosedError(InvalidRequestError): """An operation was requested from a connection, cursor, or other object that's in a closed state.""" + class NoSuchColumnError(KeyError, InvalidRequestError): """A nonexistent column is requested from a ``RowProxy``.""" + class NoReferenceError(InvalidRequestError): """Raised by ``ForeignKey`` to indicate a reference cannot be resolved.""" -class NoReferencedTableError(NoReferenceError): - """Raised by ``ForeignKey`` when the referred ``Table`` cannot be located.""" +class NoReferencedTableError(NoReferenceError): + """Raised by ``ForeignKey`` when the referred ``Table`` cannot be + located. + + """ def __init__(self, message, tname): NoReferenceError.__init__(self, message) self.table_name = tname @@ -112,9 +143,12 @@ class NoReferencedTableError(NoReferenceError): def __reduce__(self): return self.__class__, (self.args[0], self.table_name) -class NoReferencedColumnError(NoReferenceError): - """Raised by ``ForeignKey`` when the referred ``Column`` cannot be located.""" +class NoReferencedColumnError(NoReferenceError): + """Raised by ``ForeignKey`` when the referred ``Column`` cannot be + located. + + """ def __init__(self, message, tname, cname): NoReferenceError.__init__(self, message) self.table_name = tname @@ -124,6 +158,7 @@ class NoReferencedColumnError(NoReferenceError): return self.__class__, (self.args[0], self.table_name, self.column_name) + class NoSuchTableError(InvalidRequestError): """Table does not exist or is not visible to a connection.""" @@ -134,10 +169,11 @@ class UnboundExecutionError(InvalidRequestError): class DontWrapMixin(object): """A mixin class which, when applied to a user-defined Exception class, - will not be wrapped inside of :class:`.StatementError` if the error is + will not be wrapped inside of :exc:`.StatementError` if the error is emitted within the process of executing a statement. E.g.:: + from sqlalchemy.exc import DontWrapMixin class MyCustomException(Exception, DontWrapMixin): @@ -151,14 +187,11 @@ class DontWrapMixin(object): raise MyCustomException("invalid!") """ -import sys -if sys.version_info < (2, 5): - class DontWrapMixin: - pass # Moved to orm.exc; compatibility definition installed by orm import until 0.6 UnmappedColumnError = None + class StatementError(SQLAlchemyError): """An error occurred during execution of a SQL statement. @@ -186,6 +219,10 @@ class StatementError(SQLAlchemyError): self.statement = statement self.params = params self.orig = orig + self.detail = [] + + def add_detail(self, msg): + self.detail.append(msg) def __reduce__(self): return self.__class__, (self.args[0], self.statement, @@ -194,8 +231,16 @@ class StatementError(SQLAlchemyError): def __str__(self): from sqlalchemy.sql import util params_repr = util._repr_params(self.params, 10) - return ' '.join((SQLAlchemyError.__str__(self), - repr(self.statement), repr(params_repr))) + + return ' '.join([ + "(%s)" % det for det in self.detail + ] + [ + SQLAlchemyError.__str__(self), + repr(self.statement), repr(params_repr) + ]) + + def __unicode__(self): + return self.__str__() class DBAPIError(StatementError): @@ -210,13 +255,14 @@ class DBAPIError(StatementError): raise the same exception type for any given error condition. :class:`DBAPIError` features :attr:`~.StatementError.statement` - and :attr:`~.StatementError.params` attributes which supply context regarding - the specifics of the statement which had an issue, for the + and :attr:`~.StatementError.params` attributes which supply context + regarding the specifics of the statement which had an issue, for the typical case when the error was raised within the context of emitting a SQL statement. - The wrapped exception object is available in the :attr:`~.StatementError.orig` attribute. - Its type and properties are DB-API implementation specific. + The wrapped exception object is available in the + :attr:`~.StatementError.orig` attribute. Its type and properties are + DB-API implementation specific. """ @@ -234,11 +280,12 @@ class DBAPIError(StatementError): # not a DBAPI error, statement is present. # raise a StatementError if not isinstance(orig, dbapi_base_err) and statement: + msg = traceback.format_exception_only( + orig.__class__, orig)[-1].strip() return StatementError( - "%s (original cause: %s)" % ( - str(orig), - traceback.format_exception_only(orig.__class__, orig)[-1].strip() - ), statement, params, orig) + "%s (original cause: %s)" % (str(orig), msg), + statement, params, orig + ) name, glob = orig.__class__.__name__, globals() if name in glob and issubclass(glob[name], DBAPIError): @@ -255,7 +302,7 @@ class DBAPIError(StatementError): text = str(orig) except (KeyboardInterrupt, SystemExit): raise - except Exception, e: + except Exception as e: text = 'Error in str() of DB-API-generated exception: ' + str(e) StatementError.__init__( self, diff --git a/libs/sqlalchemy/ext/__init__.py b/libs/sqlalchemy/ext/__init__.py index 4a6e1952..1d77acaa 100644 --- a/libs/sqlalchemy/ext/__init__.py +++ b/libs/sqlalchemy/ext/__init__.py @@ -1,6 +1,5 @@ # ext/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php - diff --git a/libs/sqlalchemy/ext/associationproxy.py b/libs/sqlalchemy/ext/associationproxy.py index d5b0ab69..e62958b4 100644 --- a/libs/sqlalchemy/ext/associationproxy.py +++ b/libs/sqlalchemy/ext/associationproxy.py @@ -1,5 +1,5 @@ # ext/associationproxy.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -15,11 +15,9 @@ See the example ``examples/association/proxied_association.py``. import itertools import operator import weakref -from sqlalchemy import exceptions -from sqlalchemy import orm -from sqlalchemy import util -from sqlalchemy.orm import collections, ColumnProperty -from sqlalchemy.sql import not_ +from .. import exc, orm, util +from ..orm import collections, interfaces +from ..sql import not_, or_ def association_proxy(target_collection, attr, **kw): @@ -29,24 +27,25 @@ def association_proxy(target_collection, attr, **kw): The returned value is an instance of :class:`.AssociationProxy`. - Implements a Python property representing a relationship as a collection of - simpler values, or a scalar value. The proxied property will mimic the collection type of - the target (list, dict or set), or, in the case of a one to one relationship, - a simple scalar value. + Implements a Python property representing a relationship as a collection + of simpler values, or a scalar value. The proxied property will mimic + the collection type of the target (list, dict or set), or, in the case of + a one to one relationship, a simple scalar value. :param target_collection: Name of the attribute we'll proxy to. This attribute is typically mapped by :func:`~sqlalchemy.orm.relationship` to link to a target collection, but can also be a many-to-one or non-scalar relationship. - :param attr: Attribute on the associated instance or instances we'll proxy for. + :param attr: Attribute on the associated instance or instances we'll + proxy for. For example, given a target collection of [obj1, obj2], a list created by this proxy property would look like [getattr(obj1, *attr*), getattr(obj2, *attr*)] - If the relationship is one-to-one or otherwise uselist=False, then simply: - getattr(obj, *attr*) + If the relationship is one-to-one or otherwise uselist=False, then + simply: getattr(obj, *attr*) :param creator: optional. @@ -76,9 +75,22 @@ def association_proxy(target_collection, attr, **kw): return AssociationProxy(target_collection, attr, **kw) -class AssociationProxy(object): +ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY') +"""Symbol indicating an :class:`_InspectionAttr` that's + of type :class:`.AssociationProxy`. + + Is assigned to the :attr:`._InspectionAttr.extension_type` + attibute. + +""" + +class AssociationProxy(interfaces._InspectionAttr): """A descriptor that presents a read/write view of an object attribute.""" + is_attribute = False + extension_type = ASSOCIATION_PROXY + + def __init__(self, target_collection, attr, creator=None, getset_factory=None, proxy_factory=None, proxy_bulk_set=None): @@ -91,34 +103,36 @@ class AssociationProxy(object): :param target_collection: Name of the collection we'll proxy to, usually created with :func:`.relationship`. - :param attr: Attribute on the collected instances we'll proxy for. For example, - given a target collection of [obj1, obj2], a list created by this - proxy property would look like [getattr(obj1, attr), getattr(obj2, - attr)] + :param attr: Attribute on the collected instances we'll proxy + for. For example, given a target collection of [obj1, obj2], a + list created by this proxy property would look like + [getattr(obj1, attr), getattr(obj2, attr)] - :param creator: Optional. When new items are added to this proxied collection, new - instances of the class collected by the target collection will be - created. For list and set collections, the target class constructor - will be called with the 'value' for the new instance. For dict - types, two arguments are passed: key and value. + :param creator: Optional. When new items are added to this proxied + collection, new instances of the class collected by the target + collection will be created. For list and set collections, the + target class constructor will be called with the 'value' for the + new instance. For dict types, two arguments are passed: + key and value. If you want to construct instances differently, supply a 'creator' function that takes arguments as above and returns instances. - :param getset_factory: Optional. Proxied attribute access is automatically handled by - routines that get and set values based on the `attr` argument for - this proxy. + :param getset_factory: Optional. Proxied attribute access is + automatically handled by routines that get and set values based on + the `attr` argument for this proxy. If you would like to customize this behavior, you may supply a `getset_factory` callable that produces a tuple of `getter` and `setter` functions. The factory is called with two arguments, the abstract type of the underlying collection and this proxy instance. - :param proxy_factory: Optional. The type of collection to emulate is determined by - sniffing the target collection. If your collection type can't be - determined by duck typing or you'd like to use a different - collection implementation, you may supply a factory function to - produce those collections. Only applicable to non-scalar relationships. + :param proxy_factory: Optional. The type of collection to emulate is + determined by sniffing the target collection. If your collection + type can't be determined by duck typing or you'd like to use a + different collection implementation, you may supply a factory + function to produce those collections. Only applicable to + non-scalar relationships. :param proxy_bulk_set: Optional, use with proxy_factory. See the _set() method for details. @@ -217,6 +231,10 @@ class AssociationProxy(object): return not self._get_property().\ mapper.get_property(self.value_attr).uselist + @util.memoized_property + def _target_is_object(self): + return getattr(self.target_class, self.value_attr).impl.uses_objects + def __get__(self, obj, class_): if self.owning_class is None: self.owning_class = class_ and class_ or type(obj) @@ -224,7 +242,11 @@ class AssociationProxy(object): return self if self.scalar: - return self._scalar_get(getattr(obj, self.target_collection)) + target = getattr(obj, self.target_collection) + if target is not None: + return self._scalar_get(target) + else: + return None else: try: # If the owning instance is reborn (orm session resurrect, @@ -281,7 +303,8 @@ class AssociationProxy(object): self.collection_class = util.duck_type_collection(lazy_collection()) if self.proxy_factory: - return self.proxy_factory(lazy_collection, creator, self.value_attr, self) + return self.proxy_factory( + lazy_collection, creator, self.value_attr, self) if self.getset_factory: getter, setter = self.getset_factory(self.collection_class, self) @@ -289,13 +312,16 @@ class AssociationProxy(object): getter, setter = self._default_getset(self.collection_class) if self.collection_class is list: - return _AssociationList(lazy_collection, creator, getter, setter, self) + return _AssociationList( + lazy_collection, creator, getter, setter, self) elif self.collection_class is dict: - return _AssociationDict(lazy_collection, creator, getter, setter, self) + return _AssociationDict( + lazy_collection, creator, getter, setter, self) elif self.collection_class is set: - return _AssociationSet(lazy_collection, creator, getter, setter, self) + return _AssociationSet( + lazy_collection, creator, getter, setter, self) else: - raise exceptions.ArgumentError( + raise exc.ArgumentError( 'could not guess which interface to use for ' 'collection_class "%s" backing "%s"; specify a ' 'proxy_factory and proxy_bulk_set manually' % @@ -323,7 +349,7 @@ class AssociationProxy(object): elif self.collection_class is set: proxy.update(values) else: - raise exceptions.ArgumentError( + raise exc.ArgumentError( 'no proxy_bulk_set supplied for custom ' 'collection_class implementation') @@ -342,9 +368,11 @@ class AssociationProxy(object): """ if self._value_is_scalar: - value_expr = getattr(self.target_class, self.value_attr).has(criterion, **kwargs) + value_expr = getattr( + self.target_class, self.value_attr).has(criterion, **kwargs) else: - value_expr = getattr(self.target_class, self.value_attr).any(criterion, **kwargs) + value_expr = getattr( + self.target_class, self.value_attr).any(criterion, **kwargs) # check _value_is_scalar here, otherwise # we're scalar->scalar - call .any() so that @@ -368,10 +396,17 @@ class AssociationProxy(object): """ - return self._comparator.has( + if self._target_is_object: + return self._comparator.has( getattr(self.target_class, self.value_attr).\ has(criterion, **kwargs) ) + else: + if criterion is not None or kwargs: + raise exc.ArgumentError( + "Non-empty has() not allowed for " + "column-targeted association proxy; use ==") + return self._comparator.has() def contains(self, obj): """Produce a proxied 'contains' expression using EXISTS. @@ -391,10 +426,21 @@ class AssociationProxy(object): return self._comparator.any(**{self.value_attr: obj}) def __eq__(self, obj): - return self._comparator.has(**{self.value_attr: obj}) + # note the has() here will fail for collections; eq_() + # is only allowed with a scalar. + if obj is None: + return or_( + self._comparator.has(**{self.value_attr: obj}), + self._comparator == None + ) + else: + return self._comparator.has(**{self.value_attr: obj}) def __ne__(self, obj): - return not_(self.__eq__(obj)) + # note the has() here will fail for collections; eq_() + # is only allowed with a scalar. + return self._comparator.has( + getattr(self.target_class, self.value_attr) != obj) class _lazy_collection(object): @@ -405,18 +451,19 @@ class _lazy_collection(object): def __call__(self): obj = self.ref() if obj is None: - raise exceptions.InvalidRequestError( + raise exc.InvalidRequestError( "stale association proxy, parent object has gone out of " "scope") return getattr(obj, self.target) def __getstate__(self): - return {'obj':self.ref(), 'target':self.target} + return {'obj': self.ref(), 'target': self.target} def __setstate__(self, state): self.ref = weakref.ref(state['obj']) self.target = state['target'] + class _AssociationCollection(object): def __init__(self, lazy_collection, creator, getter, setter, parent): """Constructs an _AssociationCollection. @@ -454,17 +501,20 @@ class _AssociationCollection(object): def __len__(self): return len(self.col) - def __nonzero__(self): + def __bool__(self): return bool(self.col) + __nonzero__ = __bool__ + def __getstate__(self): - return {'parent':self.parent, 'lazy_collection':self.lazy_collection} + return {'parent': self.parent, 'lazy_collection': self.lazy_collection} def __setstate__(self, state): self.parent = state['parent'] self.lazy_collection = state['lazy_collection'] self.parent._inflate(self) + class _AssociationList(_AssociationCollection): """Generic, converting, list-to-list proxy.""" @@ -492,7 +542,7 @@ class _AssociationList(_AssociationCollection): stop = index.stop step = index.step or 1 - rng = range(index.start or 0, stop, step) + rng = list(range(index.start or 0, stop, step)) if step == 1: for i in rng: del self[index.start] @@ -547,7 +597,7 @@ class _AssociationList(_AssociationCollection): def count(self, value): return sum([1 for _ in - itertools.ifilter(lambda v: v == value, iter(self))]) + util.itertools_filter(lambda v: v == value, iter(self))]) def extend(self, values): for v in values: @@ -646,14 +696,16 @@ class _AssociationList(_AssociationCollection): def __hash__(self): raise TypeError("%s objects are unhashable" % type(self).__name__) - for func_name, func in locals().items(): - if (util.callable(func) and func.func_name == func_name and + for func_name, func in list(locals().items()): + if (util.callable(func) and func.__name__ == func_name and not func.__doc__ and hasattr(list, func_name)): func.__doc__ = getattr(list, func_name).__doc__ del func_name, func _NotProvided = util.symbol('_NotProvided') + + class _AssociationDict(_AssociationCollection): """Generic, converting, dict-to-dict proxy.""" @@ -687,7 +739,7 @@ class _AssociationDict(_AssociationCollection): return key in self.col def __iter__(self): - return self.col.iterkeys() + return iter(self.col.keys()) def clear(self): self.col.clear() @@ -732,24 +784,27 @@ class _AssociationDict(_AssociationCollection): def keys(self): return self.col.keys() - def iterkeys(self): - return self.col.iterkeys() + if util.py2k: + def iteritems(self): + return ((key, self._get(self.col[key])) for key in self.col) - def values(self): - return [ self._get(member) for member in self.col.values() ] + def itervalues(self): + return (self._get(self.col[key]) for key in self.col) - def itervalues(self): - for key in self.col: - yield self._get(self.col[key]) - raise StopIteration + def iterkeys(self): + return self.col.iterkeys() - def items(self): - return [(k, self._get(self.col[k])) for k in self] + def values(self): + return [self._get(member) for member in self.col.values()] - def iteritems(self): - for key in self.col: - yield (key, self._get(self.col[key])) - raise StopIteration + def items(self): + return [(k, self._get(self.col[k])) for k in self] + else: + def items(self): + return ((key, self._get(self.col[key])) for key in self.col) + + def values(self): + return (self._get(self.col[key]) for key in self.col) def pop(self, key, default=_NotProvided): if default is _NotProvided: @@ -768,8 +823,8 @@ class _AssociationDict(_AssociationCollection): len(a)) elif len(a) == 1: seq_or_map = a[0] - # discern dict from sequence - took the advice - # from http://www.voidspace.org.uk/python/articles/duck_typing.shtml + # discern dict from sequence - took the advice from + # http://www.voidspace.org.uk/python/articles/duck_typing.shtml # still not perfect :( if hasattr(seq_or_map, 'keys'): for item in seq_or_map: @@ -792,8 +847,8 @@ class _AssociationDict(_AssociationCollection): def __hash__(self): raise TypeError("%s objects are unhashable" % type(self).__name__) - for func_name, func in locals().items(): - if (util.callable(func) and func.func_name == func_name and + for func_name, func in list(locals().items()): + if (util.callable(func) and func.__name__ == func_name and not func.__doc__ and hasattr(dict, func_name)): func.__doc__ = getattr(dict, func_name).__doc__ del func_name, func @@ -814,12 +869,14 @@ class _AssociationSet(_AssociationCollection): def __len__(self): return len(self.col) - def __nonzero__(self): + def __bool__(self): if self.col: return True else: return False + __nonzero__ = __bool__ + def __contains__(self, value): for member in self.col: # testlib.pragma exempt:__eq__ @@ -990,8 +1047,8 @@ class _AssociationSet(_AssociationCollection): def __hash__(self): raise TypeError("%s objects are unhashable" % type(self).__name__) - for func_name, func in locals().items(): - if (util.callable(func) and func.func_name == func_name and + for func_name, func in list(locals().items()): + if (util.callable(func) and func.__name__ == func_name and not func.__doc__ and hasattr(set, func_name)): func.__doc__ = getattr(set, func_name).__doc__ del func_name, func diff --git a/libs/sqlalchemy/ext/automap.py b/libs/sqlalchemy/ext/automap.py new file mode 100644 index 00000000..7a1512f6 --- /dev/null +++ b/libs/sqlalchemy/ext/automap.py @@ -0,0 +1,840 @@ +# ext/automap.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system +which automatically generates mapped classes and relationships from a database +schema, typically though not necessarily one which is reflected. + +.. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`. + +.. note:: + + The :mod:`sqlalchemy.ext.automap` extension should be considered + **experimental** as of 0.9.1. Featureset and API stability is + not guaranteed at this time. + +It is hoped that the :class:`.AutomapBase` system provides a quick +and modernized solution to the problem that the very famous +`SQLSoup `_ +also tries to solve, that of generating a quick and rudimentary object +model from an existing database on the fly. By addressing the issue strictly +at the mapper configuration level, and integrating fully with existing +Declarative class techniques, :class:`.AutomapBase` seeks to provide +a well-integrated approach to the issue of expediently auto-generating ad-hoc +mappings. + + +Basic Use +========= + +The simplest usage is to reflect an existing database into a new model. +We create a new :class:`.AutomapBase` class in a similar manner as to how +we create a declarative base class, using :func:`.automap_base`. +We then call :meth:`.AutomapBase.prepare` on the resulting base class, +asking it to reflect the schema and produce mappings:: + + from sqlalchemy.ext.automap import automap_base + from sqlalchemy.orm import Session + from sqlalchemy import create_engine + + Base = automap_base() + + # engine, suppose it has two tables 'user' and 'address' set up + engine = create_engine("sqlite:///mydatabase.db") + + # reflect the tables + Base.prepare(engine, reflect=True) + + # mapped classes are now created with names by default + # matching that of the table name. + User = Base.classes.user + Address = Base.classes.address + + session = Session(engine) + + # rudimentary relationships are produced + session.add(Address(email_address="foo@bar.com", user=User(name="foo"))) + session.commit() + + # collection-based relationships are by default named "_collection" + print (u1.address_collection) + +Above, calling :meth:`.AutomapBase.prepare` while passing along the +:paramref:`.AutomapBase.prepare.reflect` parameter indicates that the +:meth:`.MetaData.reflect` method will be called on this declarative base +classes' :class:`.MetaData` collection; then, each viable +:class:`.Table` within the :class:`.MetaData` will get a new mapped class +generated automatically. The :class:`.ForeignKeyConstraint` objects which +link the various tables together will be used to produce new, bidirectional +:func:`.relationship` objects between classes. The classes and relationships +follow along a default naming scheme that we can customize. At this point, +our basic mapping consisting of related ``User`` and ``Address`` classes is ready +to use in the traditional way. + +Generating Mappings from an Existing MetaData +============================================= + +We can pass a pre-declared :class:`.MetaData` object to :func:`.automap_base`. +This object can be constructed in any way, including programmatically, from +a serialized file, or from itself being reflected using :meth:`.MetaData.reflect`. +Below we illustrate a combination of reflection and explicit table declaration:: + + from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey + engine = create_engine("sqlite:///mydatabase.db") + + # produce our own MetaData object + metadata = MetaData() + + # we can reflect it ourselves from a database, using options + # such as 'only' to limit what tables we look at... + metadata.reflect(engine, only=['user', 'address']) + + # ... or just define our own Table objects with it (or combine both) + Table('user_order', metadata, + Column('id', Integer, primary_key=True), + Column('user_id', ForeignKey('user.id')) + ) + + # we can then produce a set of mappings from this MetaData. + Base = automap_base(metadata=metadata) + + # calling prepare() just sets up mapped classes and relationships. + Base.prepare() + + # mapped classes are ready + User, Address, Order = Base.classes.user, Base.classes.address, Base.classes.user_order + +Specifying Classes Explcitly +============================ + +The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined +explicitly, in a way similar to that of the :class:`.DeferredReflection` class. +Classes that extend from :class:`.AutomapBase` act like regular declarative +classes, but are not immediately mapped after their construction, and are instead +mapped when we call :meth:`.AutomapBase.prepare`. The :meth:`.AutomapBase.prepare` +method will make use of the classes we've established based on the table name +we use. If our schema contains tables ``user`` and ``address``, we can define +one or both of the classes to be used:: + + from sqlalchemy.ext.automap import automap_base + from sqlalchemy import create_engine + + # automap base + Base = automap_base() + + # pre-declare User for the 'user' table + class User(Base): + __tablename__ = 'user' + + # override schema elements like Columns + user_name = Column('name', String) + + # override relationships too, if desired. + # we must use the same name that automap would use for the relationship, + # and also must refer to the class name that automap will generate + # for "address" + address_collection = relationship("address", collection_class=set) + + # reflect + engine = create_engine("sqlite:///mydatabase.db") + Base.prepare(engine, reflect=True) + + # we still have Address generated from the tablename "address", + # but User is the same as Base.classes.User now + + Address = Base.classes.address + + u1 = session.query(User).first() + print (u1.address_collection) + + # the backref is still there: + a1 = session.query(Address).first() + print (a1.user) + +Above, one of the more intricate details is that we illustrated overriding +one of the :func:`.relationship` objects that automap would have created. +To do this, we needed to make sure the names match up with what automap +would normally generate, in that the relationship name would be ``User.address_collection`` +and the name of the class referred to, from automap's perspective, is called +``address``, even though we are referring to it as ``Address`` within our usage +of this class. + +Overriding Naming Schemes +========================= + +:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and +relationship names based on a schema, which means it has decision points in how +these names are determined. These three decision points are provided using +functions which can be passed to the :meth:`.AutomapBase.prepare` method, and +are known as :func:`.classname_for_table`, +:func:`.name_for_scalar_relationship`, +and :func:`.name_for_collection_relationship`. Any or all of these +functions are provided as in the example below, where we use a "camel case" +scheme for class names and a "pluralizer" for collection names using the +`Inflect `_ package:: + + import re + import inflect + + def camelize_classname(base, tablename, table): + "Produce a 'camelized' class name, e.g. " + "'words_and_underscores' -> 'WordsAndUnderscores'" + + return str(tablename[0].upper() + \\ + re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:])) + + _pluralizer = inflect.engine() + def pluralize_collection(base, local_cls, referred_cls, constraint): + "Produce an 'uncamelized', 'pluralized' class name, e.g. " + "'SomeTerm' -> 'some_terms'" + + referred_name = referred_cls.__name__ + uncamelized = referred_name[0].lower() + \\ + re.sub(r'\W', + lambda m: "_%s" % m.group(0).lower(), + referred_name[1:]) + pluralized = _pluralizer.plural(uncamelized) + return pluralized + + from sqlalchemy.ext.automap import automap_base + + Base = automap_base() + + engine = create_engine("sqlite:///mydatabase.db") + + Base.prepare(engine, reflect=True, + classname_for_table=camelize_classname, + name_for_collection_relationship=pluralize_collection + ) + +From the above mapping, we would now have classes ``User`` and ``Address``, +where the collection from ``User`` to ``Address`` is called ``User.addresses``:: + + User, Address = Base.classes.User, Base.classes.Address + + u1 = User(addresses=[Address(email="foo@bar.com")]) + +Relationship Detection +====================== + +The vast majority of what automap accomplishes is the generation of +:func:`.relationship` structures based on foreign keys. The mechanism +by which this works for many-to-one and one-to-many relationships is as follows: + +1. A given :class:`.Table`, known to be mapped to a particular class, + is examined for :class:`.ForeignKeyConstraint` objects. + +2. From each :class:`.ForeignKeyConstraint`, the remote :class:`.Table` + object present is matched up to the class to which it is to be mapped, + if any, else it is skipped. + +3. As the :class:`.ForeignKeyConstraint` we are examining correponds to a reference + from the immediate mapped class, + the relationship will be set up as a many-to-one referring to the referred class; + a corresponding one-to-many backref will be created on the referred class referring + to this class. + +4. The names of the relationships are determined using the + :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and + :paramref:`.AutomapBase.prepare.name_for_collection_relationship` + callable functions. It is important to note that the default relationship + naming derives the name from the **the actual class name**. If you've + given a particular class an explicit name by declaring it, or specified an + alternate class naming scheme, that's the name from which the relationship + name will be derived. + +5. The classes are inspected for an existing mapped property matching these + names. If one is detected on one side, but none on the other side, :class:`.AutomapBase` + attempts to create a relationship on the missing side, then uses the + :paramref:`.relationship.back_populates` parameter in order to point + the new relationship to the other side. + +6. In the usual case where no relationship is on either side, + :meth:`.AutomapBase.prepare` produces a :func:`.relationship` on the "many-to-one" + side and matches it to the other using the :paramref:`.relationship.backref` + parameter. + +7. Production of the :func:`.relationship` and optionally the :func:`.backref` + is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship` + function, which can be supplied by the end-user in order to augment + the arguments passed to :func:`.relationship` or :func:`.backref` or to + make use of custom implementations of these functions. + +Custom Relationship Arguments +----------------------------- + +The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used +to add parameters to relationships. For most cases, we can make use of the +existing :func:`.automap.generate_relationship` function to return +the object, after augmenting the given keyword dictionary with our own +arguments. + +Below is an illustration of how to send +:paramref:`.relationship.cascade` and +:paramref:`.relationship.passive_deletes` +options along to all one-to-many relationships:: + + from sqlalchemy.ext.automap import generate_relationship + + def _gen_relationship(base, direction, return_fn, + attrname, local_cls, referred_cls, **kw): + if direction is interfaces.ONETOMANY: + kw['cascade'] = 'all, delete-orphan' + kw['passive_deletes'] = True + # make use of the built-in function to actually return + # the result. + return generate_relationship(base, direction, return_fn, + attrname, local_cls, referred_cls, **kw) + + from sqlalchemy.ext.automap import automap_base + from sqlalchemy import create_engine + + # automap base + Base = automap_base() + + engine = create_engine("sqlite:///mydatabase.db") + Base.prepare(engine, reflect=True, + generate_relationship=_gen_relationship) + +Many-to-Many relationships +-------------------------- + +:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g. +those which contain a ``secondary`` argument. The process for producing these +is as follows: + +1. A given :class:`.Table` is examined for :class:`.ForeignKeyConstraint` objects, + before any mapped class has been assigned to it. + +2. If the table contains two and exactly two :class:`.ForeignKeyConstraint` + objects, and all columns within this table are members of these two + :class:`.ForeignKeyConstraint` objects, the table is assumed to be a + "secondary" table, and will **not be mapped directly**. + +3. The two (or one, for self-referential) external tables to which the :class:`.Table` + refers to are matched to the classes to which they will be mapped, if any. + +4. If mapped classes for both sides are located, a many-to-many bi-directional + :func:`.relationship` / :func:`.backref` pair is created between the two + classes. + +5. The override logic for many-to-many works the same as that of one-to-many/ + many-to-one; the :func:`.generate_relationship` function is called upon + to generate the strucures and existing attributes will be maintained. + +Using Automap with Explicit Declarations +======================================== + +As noted previously, automap has no dependency on reflection, and can make +use of any collection of :class:`.Table` objects within a :class:`.MetaData` +collection. From this, it follows that automap can also be used +generate missing relationships given an otherwise complete model that fully defines +table metadata:: + + from sqlalchemy.ext.automap import automap_base + from sqlalchemy import Column, Integer, String, ForeignKey + + Base = automap_base() + + class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + name = Column(String) + + class Address(Base): + __tablename__ = 'address' + + id = Column(Integer, primary_key=True) + email = Column(String) + user_id = Column(ForeignKey('user.id')) + + # produce relationships + Base.prepare() + + # mapping is complete, with "address_collection" and + # "user" relationships + a1 = Address(email='u1') + a2 = Address(email='u2') + u1 = User(address_collection=[a1, a2]) + assert a1.user is u1 + +Above, given mostly complete ``User`` and ``Address`` mappings, the +:class:`.ForeignKey` which we defined on ``Address.user_id`` allowed a +bidirectional relationship pair ``Address.user`` and ``User.address_collection`` +to be generated on the mapped classes. + +Note that when subclassing :class:`.AutomapBase`, the :meth:`.AutomapBase.prepare` +method is required; if not called, the classes we've declared are in an +un-mapped state. + + +""" +from .declarative import declarative_base as _declarative_base +from .declarative.base import _DeferredMapperConfig +from ..sql import and_ +from ..schema import ForeignKeyConstraint +from ..orm import relationship, backref, interfaces +from .. import util + + +def classname_for_table(base, tablename, table): + """Return the class name that should be used, given the name + of a table. + + The default implementation is:: + + return str(tablename) + + Alternate implementations can be specified using the + :paramref:`.AutomapBase.prepare.classname_for_table` + parameter. + + :param base: the :class:`.AutomapBase` class doing the prepare. + + :param tablename: string name of the :class:`.Table`. + + :param table: the :class:`.Table` object itself. + + :return: a string class name. + + .. note:: + + In Python 2, the string used for the class name **must** be a non-Unicode + object, e.g. a ``str()`` object. The ``.name`` attribute of + :class:`.Table` is typically a Python unicode subclass, so the ``str()`` + function should be applied to this name, after accounting for any non-ASCII + characters. + + """ + return str(tablename) + +def name_for_scalar_relationship(base, local_cls, referred_cls, constraint): + """Return the attribute name that should be used to refer from one + class to another, for a scalar object reference. + + The default implementation is:: + + return referred_cls.__name__.lower() + + Alternate implementations can be specified using the + :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` + parameter. + + :param base: the :class:`.AutomapBase` class doing the prepare. + + :param local_cls: the class to be mapped on the local side. + + :param referred_cls: the class to be mapped on the referring side. + + :param constraint: the :class:`.ForeignKeyConstraint` that is being + inspected to produce this relationship. + + """ + return referred_cls.__name__.lower() + +def name_for_collection_relationship(base, local_cls, referred_cls, constraint): + """Return the attribute name that should be used to refer from one + class to another, for a collection reference. + + The default implementation is:: + + return referred_cls.__name__.lower() + "_collection" + + Alternate implementations + can be specified using the :paramref:`.AutomapBase.prepare.name_for_collection_relationship` + parameter. + + :param base: the :class:`.AutomapBase` class doing the prepare. + + :param local_cls: the class to be mapped on the local side. + + :param referred_cls: the class to be mapped on the referring side. + + :param constraint: the :class:`.ForeignKeyConstraint` that is being + inspected to produce this relationship. + + """ + return referred_cls.__name__.lower() + "_collection" + +def generate_relationship(base, direction, return_fn, attrname, local_cls, referred_cls, **kw): + """Generate a :func:`.relationship` or :func:`.backref` on behalf of two + mapped classes. + + An alternate implementation of this function can be specified using the + :paramref:`.AutomapBase.prepare.generate_relationship` parameter. + + The default implementation of this function is as follows:: + + if return_fn is backref: + return return_fn(attrname, **kw) + elif return_fn is relationship: + return return_fn(referred_cls, **kw) + else: + raise TypeError("Unknown relationship function: %s" % return_fn) + + :param base: the :class:`.AutomapBase` class doing the prepare. + + :param direction: indicate the "direction" of the relationship; this will + be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOONE`. + + :param return_fn: the function that is used by default to create the + relationship. This will be either :func:`.relationship` or :func:`.backref`. + The :func:`.backref` function's result will be used to produce a new + :func:`.relationship` in a second step, so it is critical that user-defined + implementations correctly differentiate between the two functions, if + a custom relationship function is being used. + + :attrname: the attribute name to which this relationship is being assigned. + If the value of :paramref:`.generate_relationship.return_fn` is the + :func:`.backref` function, then this name is the name that is being + assigned to the backref. + + :param local_cls: the "local" class to which this relationship or backref + will be locally present. + + :param referred_cls: the "referred" class to which the relationship or backref + refers to. + + :param \**kw: all additional keyword arguments are passed along to the + function. + + :return: a :func:`.relationship` or :func:`.backref` construct, as dictated + by the :paramref:`.generate_relationship.return_fn` parameter. + + """ + if return_fn is backref: + return return_fn(attrname, **kw) + elif return_fn is relationship: + return return_fn(referred_cls, **kw) + else: + raise TypeError("Unknown relationship function: %s" % return_fn) + +class AutomapBase(object): + """Base class for an "automap" schema. + + The :class:`.AutomapBase` class can be compared to the "declarative base" + class that is produced by the :func:`.declarative.declarative_base` + function. In practice, the :class:`.AutomapBase` class is always used + as a mixin along with an actual declarative base. + + A new subclassable :class:`.AutomapBase` is typically instantated + using the :func:`.automap_base` function. + + .. seealso:: + + :ref:`automap_toplevel` + + """ + __abstract__ = True + + classes = None + """An instance of :class:`.util.Properties` containing classes. + + This object behaves much like the ``.c`` collection on a table. Classes + are present under the name they were given, e.g.:: + + Base = automap_base() + Base.prepare(engine=some_engine, reflect=True) + + User, Address = Base.classes.User, Base.classes.Address + + """ + + @classmethod + def prepare(cls, + engine=None, + reflect=False, + classname_for_table=classname_for_table, + collection_class=list, + name_for_scalar_relationship=name_for_scalar_relationship, + name_for_collection_relationship=name_for_collection_relationship, + generate_relationship=generate_relationship): + + """Extract mapped classes and relationships from the :class:`.MetaData` and + perform mappings. + + :param engine: an :class:`.Engine` or :class:`.Connection` with which + to perform schema reflection, if specified. + If the :paramref:`.AutomapBase.prepare.reflect` argument is False, this + object is not used. + + :param reflect: if True, the :meth:`.MetaData.reflect` method is called + on the :class:`.MetaData` associated with this :class:`.AutomapBase`. + The :class:`.Engine` passed via :paramref:`.AutomapBase.prepare.engine` will + be used to perform the reflection if present; else, the :class:`.MetaData` + should already be bound to some engine else the operation will fail. + + :param classname_for_table: callable function which will be used to + produce new class names, given a table name. Defaults to + :func:`.classname_for_table`. + + :param name_for_scalar_relationship: callable function which will be used + to produce relationship names for scalar relationships. Defaults to + :func:`.name_for_scalar_relationship`. + + :param name_for_collection_relationship: callable function which will be used + to produce relationship names for collection-oriented relationships. Defaults to + :func:`.name_for_collection_relationship`. + + :param generate_relationship: callable function which will be used to + actually generate :func:`.relationship` and :func:`.backref` constructs. + Defaults to :func:`.generate_relationship`. + + :param collection_class: the Python collection class that will be used + when a new :func:`.relationship` object is created that represents a + collection. Defaults to ``list``. + + """ + if reflect: + cls.metadata.reflect( + engine, + extend_existing=True, + autoload_replace=False + ) + + table_to_map_config = dict( + (m.local_table, m) + for m in _DeferredMapperConfig.classes_for_base(cls) + ) + + many_to_many = [] + + for table in cls.metadata.tables.values(): + lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table) + if lcl_m2m is not None: + many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table)) + elif not table.primary_key: + continue + elif table not in table_to_map_config: + mapped_cls = type( + classname_for_table(cls, table.name, table), + (cls, ), + {"__table__": table} + ) + map_config = _DeferredMapperConfig.config_for_cls(mapped_cls) + cls.classes[map_config.cls.__name__] = mapped_cls + table_to_map_config[table] = map_config + + for map_config in table_to_map_config.values(): + _relationships_for_fks(cls, + map_config, + table_to_map_config, + collection_class, + name_for_scalar_relationship, + name_for_collection_relationship, + generate_relationship) + + for lcl_m2m, rem_m2m, m2m_const, table in many_to_many: + _m2m_relationship(cls, lcl_m2m, rem_m2m, m2m_const, table, + table_to_map_config, + collection_class, + name_for_scalar_relationship, + name_for_collection_relationship, + generate_relationship) + for map_config in table_to_map_config.values(): + map_config.map() + + + _sa_decl_prepare = True + """Indicate that the mapping of classes should be deferred. + + The presence of this attribute name indicates to declarative + that the call to mapper() should not occur immediately; instead, + information about the table and attributes to be mapped are gathered + into an internal structure called _DeferredMapperConfig. These + objects can be collected later using classes_for_base(), additional + mapping decisions can be made, and then the map() method will actually + apply the mapping. + + The only real reason this deferral of the whole + thing is needed is to support primary key columns that aren't reflected + yet when the class is declared; everything else can theoretically be + added to the mapper later. However, the _DeferredMapperConfig is a + nice interface in any case which exists at that not usually exposed point + at which declarative has the class and the Table but hasn't called + mapper() yet. + + """ + +def automap_base(declarative_base=None, **kw): + """Produce a declarative automap base. + + This function produces a new base class that is a product of the + :class:`.AutomapBase` class as well a declarative base produced by + :func:`.declarative.declarative_base`. + + All parameters other than ``declarative_base`` are keyword arguments + that are passed directly to the :func:`.declarative.declarative_base` + function. + + :param declarative_base: an existing class produced by + :func:`.declarative.declarative_base`. When this is passed, the function + no longer invokes :func:`.declarative.declarative_base` itself, and all other + keyword arguments are ignored. + + :param \**kw: keyword arguments are passed along to + :func:`.declarative.declarative_base`. + + """ + if declarative_base is None: + Base = _declarative_base(**kw) + else: + Base = declarative_base + + return type( + Base.__name__, + (AutomapBase, Base,), + {"__abstract__": True, "classes": util.Properties({})} + ) + +def _is_many_to_many(automap_base, table): + fk_constraints = [const for const in table.constraints + if isinstance(const, ForeignKeyConstraint)] + if len(fk_constraints) != 2: + return None, None, None + + cols = sum( + [[fk.parent for fk in fk_constraint.elements] + for fk_constraint in fk_constraints], []) + + if set(cols) != set(table.c): + return None, None, None + + return ( + fk_constraints[0].elements[0].column.table, + fk_constraints[1].elements[0].column.table, + fk_constraints + ) + +def _relationships_for_fks(automap_base, map_config, table_to_map_config, + collection_class, + name_for_scalar_relationship, + name_for_collection_relationship, + generate_relationship): + local_table = map_config.local_table + local_cls = map_config.cls + + for constraint in local_table.constraints: + if isinstance(constraint, ForeignKeyConstraint): + fks = constraint.elements + referred_table = fks[0].column.table + referred_cfg = table_to_map_config.get(referred_table, None) + if referred_cfg is None: + continue + referred_cls = referred_cfg.cls + + relationship_name = name_for_scalar_relationship( + automap_base, + local_cls, + referred_cls, constraint) + backref_name = name_for_collection_relationship( + automap_base, + referred_cls, + local_cls, + constraint + ) + + create_backref = backref_name not in referred_cfg.properties + + if relationship_name not in map_config.properties: + if create_backref: + backref_obj = generate_relationship(automap_base, + interfaces.ONETOMANY, backref, + backref_name, referred_cls, local_cls, + collection_class=collection_class) + else: + backref_obj = None + map_config.properties[relationship_name] = \ + generate_relationship(automap_base, + interfaces.MANYTOONE, + relationship, + relationship_name, + local_cls, referred_cls, + foreign_keys=[fk.parent for fk in constraint.elements], + backref=backref_obj, + remote_side=[fk.column for fk in constraint.elements] + ) + if not create_backref: + referred_cfg.properties[backref_name].back_populates = relationship_name + elif create_backref: + referred_cfg.properties[backref_name] = \ + generate_relationship(automap_base, + interfaces.ONETOMANY, + relationship, + backref_name, + referred_cls, local_cls, + foreign_keys=[fk.parent for fk in constraint.elements], + back_populates=relationship_name, + collection_class=collection_class) + map_config.properties[relationship_name].back_populates = backref_name + +def _m2m_relationship(automap_base, lcl_m2m, rem_m2m, m2m_const, table, + table_to_map_config, + collection_class, + name_for_scalar_relationship, + name_for_collection_relationship, + generate_relationship): + + map_config = table_to_map_config.get(lcl_m2m, None) + referred_cfg = table_to_map_config.get(rem_m2m, None) + if map_config is None or referred_cfg is None: + return + + local_cls = map_config.cls + referred_cls = referred_cfg.cls + + relationship_name = name_for_collection_relationship( + automap_base, + local_cls, + referred_cls, m2m_const[0]) + backref_name = name_for_collection_relationship( + automap_base, + referred_cls, + local_cls, + m2m_const[1] + ) + + create_backref = backref_name not in referred_cfg.properties + + if relationship_name not in map_config.properties: + if create_backref: + backref_obj = generate_relationship(automap_base, + interfaces.MANYTOMANY, + backref, + backref_name, + referred_cls, local_cls, + collection_class=collection_class + ) + else: + backref_obj = None + map_config.properties[relationship_name] = \ + generate_relationship(automap_base, + interfaces.MANYTOMANY, + relationship, + relationship_name, + local_cls, referred_cls, + secondary=table, + primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements), + secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements), + backref=backref_obj, + collection_class=collection_class + ) + if not create_backref: + referred_cfg.properties[backref_name].back_populates = relationship_name + elif create_backref: + referred_cfg.properties[backref_name] = \ + generate_relationship(automap_base, + interfaces.MANYTOMANY, + relationship, + backref_name, + referred_cls, local_cls, + secondary=table, + primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements), + secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements), + back_populates=relationship_name, + collection_class=collection_class) + map_config.properties[relationship_name].back_populates = backref_name diff --git a/libs/sqlalchemy/ext/compiler.py b/libs/sqlalchemy/ext/compiler.py index 9bd9b42e..5dde74e0 100644 --- a/libs/sqlalchemy/ext/compiler.py +++ b/libs/sqlalchemy/ext/compiler.py @@ -1,5 +1,5 @@ # ext/compiler.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -9,8 +9,9 @@ Synopsis ======== -Usage involves the creation of one or more :class:`~sqlalchemy.sql.expression.ClauseElement` -subclasses and one or more callables defining its compilation:: +Usage involves the creation of one or more +:class:`~sqlalchemy.sql.expression.ClauseElement` subclasses and one or +more callables defining its compilation:: from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.expression import ColumnClause @@ -58,16 +59,18 @@ invoked for the dialect in use:: def visit_alter_column(element, compiler, **kw): return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name) -The second ``visit_alter_table`` will be invoked when any ``postgresql`` dialect is used. +The second ``visit_alter_table`` will be invoked when any ``postgresql`` +dialect is used. Compiling sub-elements of a custom expression construct ======================================================= -The ``compiler`` argument is the :class:`~sqlalchemy.engine.base.Compiled` -object in use. This object can be inspected for any information about the -in-progress compilation, including ``compiler.dialect``, -``compiler.statement`` etc. The :class:`~sqlalchemy.sql.compiler.SQLCompiler` -and :class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()`` +The ``compiler`` argument is the +:class:`~sqlalchemy.engine.interfaces.Compiled` object in use. This object +can be inspected for any information about the in-progress compilation, +including ``compiler.dialect``, ``compiler.statement`` etc. The +:class:`~sqlalchemy.sql.compiler.SQLCompiler` and +:class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()`` method which can be used for compilation of embedded attributes:: from sqlalchemy.sql.expression import Executable, ClauseElement @@ -91,6 +94,12 @@ Produces:: "INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)" +.. note:: + + The above ``InsertFromSelect`` construct is only an example, this actual + functionality is already available using the + :meth:`.Insert.from_select` method. + .. note:: The above ``InsertFromSelect`` construct probably wants to have "autocommit" @@ -99,10 +108,11 @@ Produces:: Cross Compiling between SQL and DDL compilers --------------------------------------------- -SQL and DDL constructs are each compiled using different base compilers - ``SQLCompiler`` -and ``DDLCompiler``. A common need is to access the compilation rules of SQL expressions -from within a DDL expression. The ``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as below where we generate a CHECK -constraint that embeds a SQL expression:: +SQL and DDL constructs are each compiled using different base compilers - +``SQLCompiler`` and ``DDLCompiler``. A common need is to access the +compilation rules of SQL expressions from within a DDL expression. The +``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as +below where we generate a CHECK constraint that embeds a SQL expression:: @compiles(MyConstraint) def compile_my_constraint(constraint, ddlcompiler, **kw): @@ -116,20 +126,22 @@ constraint that embeds a SQL expression:: Enabling Autocommit on a Construct ================================== -Recall from the section :ref:`autocommit` that the :class:`.Engine`, when asked to execute -a construct in the absence of a user-defined transaction, detects if the given -construct represents DML or DDL, that is, a data modification or data definition statement, which -requires (or may require, in the case of DDL) that the transaction generated by the DBAPI be committed -(recall that DBAPI always has a transaction going on regardless of what SQLAlchemy does). Checking -for this is actually accomplished -by checking for the "autocommit" execution option on the construct. When building a construct like -an INSERT derivation, a new DDL type, or perhaps a stored procedure that alters data, the "autocommit" -option needs to be set in order for the statement to function with "connectionless" execution +Recall from the section :ref:`autocommit` that the :class:`.Engine`, when +asked to execute a construct in the absence of a user-defined transaction, +detects if the given construct represents DML or DDL, that is, a data +modification or data definition statement, which requires (or may require, +in the case of DDL) that the transaction generated by the DBAPI be committed +(recall that DBAPI always has a transaction going on regardless of what +SQLAlchemy does). Checking for this is actually accomplished by checking for +the "autocommit" execution option on the construct. When building a +construct like an INSERT derivation, a new DDL type, or perhaps a stored +procedure that alters data, the "autocommit" option needs to be set in order +for the statement to function with "connectionless" execution (as described in :ref:`dbengine_implicit`). -Currently a quick way to do this is to subclass :class:`.Executable`, then add the "autocommit" flag -to the ``_execution_options`` dictionary (note this is a "frozen" dictionary which supplies a generative -``union()`` method):: +Currently a quick way to do this is to subclass :class:`.Executable`, then +add the "autocommit" flag to the ``_execution_options`` dictionary (note this +is a "frozen" dictionary which supplies a generative ``union()`` method):: from sqlalchemy.sql.expression import Executable, ClauseElement @@ -137,8 +149,9 @@ to the ``_execution_options`` dictionary (note this is a "frozen" dictionary whi _execution_options = \\ Executable._execution_options.union({'autocommit': True}) -More succinctly, if the construct is truly similar to an INSERT, UPDATE, or DELETE, :class:`.UpdateBase` -can be used, which already is a subclass of :class:`.Executable`, :class:`.ClauseElement` and includes the +More succinctly, if the construct is truly similar to an INSERT, UPDATE, or +DELETE, :class:`.UpdateBase` can be used, which already is a subclass +of :class:`.Executable`, :class:`.ClauseElement` and includes the ``autocommit`` flag:: from sqlalchemy.sql.expression import UpdateBase @@ -150,7 +163,8 @@ can be used, which already is a subclass of :class:`.Executable`, :class:`.Claus -DDL elements that subclass :class:`.DDLElement` already have the "autocommit" flag turned on. +DDL elements that subclass :class:`.DDLElement` already have the +"autocommit" flag turned on. @@ -158,13 +172,16 @@ DDL elements that subclass :class:`.DDLElement` already have the "autocommit" fl Changing the default compilation of existing constructs ======================================================= -The compiler extension applies just as well to the existing constructs. When overriding -the compilation of a built in SQL construct, the @compiles decorator is invoked upon -the appropriate class (be sure to use the class, i.e. ``Insert`` or ``Select``, instead of the creation function such as ``insert()`` or ``select()``). +The compiler extension applies just as well to the existing constructs. When +overriding the compilation of a built in SQL construct, the @compiles +decorator is invoked upon the appropriate class (be sure to use the class, +i.e. ``Insert`` or ``Select``, instead of the creation function such +as ``insert()`` or ``select()``). -Within the new compilation function, to get at the "original" compilation routine, -use the appropriate visit_XXX method - this because compiler.process() will call upon the -overriding routine and cause an endless loop. Such as, to add "prefix" to all insert statements:: +Within the new compilation function, to get at the "original" compilation +routine, use the appropriate visit_XXX method - this +because compiler.process() will call upon the overriding routine and cause +an endless loop. Such as, to add "prefix" to all insert statements:: from sqlalchemy.sql.expression import Insert @@ -172,14 +189,16 @@ overriding routine and cause an endless loop. Such as, to add "prefix" to all def prefix_inserts(insert, compiler, **kw): return compiler.visit_insert(insert.prefix_with("some prefix"), **kw) -The above compiler will prefix all INSERT statements with "some prefix" when compiled. +The above compiler will prefix all INSERT statements with "some prefix" when +compiled. .. _type_compilation_extension: Changing Compilation of Types ============================= -``compiler`` works for types, too, such as below where we implement the MS-SQL specific 'max' keyword for ``String``/``VARCHAR``:: +``compiler`` works for types, too, such as below where we implement the +MS-SQL specific 'max' keyword for ``String``/``VARCHAR``:: @compiles(String, 'mssql') @compiles(VARCHAR, 'mssql') @@ -219,7 +238,7 @@ A synopsis is as follows: class timestamp(ColumnElement): type = TIMESTAMP() -* :class:`~sqlalchemy.sql.expression.FunctionElement` - This is a hybrid of a +* :class:`~sqlalchemy.sql.functions.FunctionElement` - This is a hybrid of a ``ColumnElement`` and a "from clause" like object, and represents a SQL function or stored procedure type of call. Since most databases support statements along the line of "SELECT FROM " @@ -248,10 +267,10 @@ A synopsis is as follows: ``execute_at()`` method, allowing the construct to be invoked during CREATE TABLE and DROP TABLE sequences. -* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which should be - used with any expression class that represents a "standalone" SQL statement that - can be passed directly to an ``execute()`` method. It is already implicit - within ``DDLElement`` and ``FunctionElement``. +* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which + should be used with any expression class that represents a "standalone" + SQL statement that can be passed directly to an ``execute()`` method. It + is already implicit within ``DDLElement`` and ``FunctionElement``. Further Examples ================ @@ -259,12 +278,13 @@ Further Examples "UTC timestamp" function ------------------------- -A function that works like "CURRENT_TIMESTAMP" except applies the appropriate conversions -so that the time is in UTC time. Timestamps are best stored in relational databases -as UTC, without time zones. UTC so that your database doesn't think time has gone -backwards in the hour when daylight savings ends, without timezones because timezones -are like character encodings - they're best applied only at the endpoints of an -application (i.e. convert to UTC upon user input, re-apply desired timezone upon display). +A function that works like "CURRENT_TIMESTAMP" except applies the +appropriate conversions so that the time is in UTC time. Timestamps are best +stored in relational databases as UTC, without time zones. UTC so that your +database doesn't think time has gone backwards in the hour when daylight +savings ends, without timezones because timezones are like character +encodings - they're best applied only at the endpoints of an application +(i.e. convert to UTC upon user input, re-apply desired timezone upon display). For Postgresql and Microsoft SQL Server:: @@ -298,10 +318,10 @@ Example usage:: "GREATEST" function ------------------- -The "GREATEST" function is given any number of arguments and returns the one that is -of the highest value - it's equivalent to Python's ``max`` function. A SQL -standard version versus a CASE based version which only accommodates two -arguments:: +The "GREATEST" function is given any number of arguments and returns the one +that is of the highest value - it's equivalent to Python's ``max`` +function. A SQL standard version versus a CASE based version which only +accommodates two arguments:: from sqlalchemy.sql import expression from sqlalchemy.ext.compiler import compiles @@ -339,7 +359,8 @@ Example usage:: "false" expression ------------------ -Render a "false" constant expression, rendering as "0" on platforms that don't have a "false" constant:: +Render a "false" constant expression, rendering as "0" on platforms that +don't have a "false" constant:: from sqlalchemy.sql import expression from sqlalchemy.ext.compiler import compiles @@ -367,9 +388,14 @@ Example usage:: ) """ -from sqlalchemy import exc +from .. import exc +from ..sql import visitors + def compiles(class_, *specs): + """Register a function as a compiler for a + given :class:`.ClauseElement` type.""" + def decorate(fn): existing = class_.__dict__.get('_compiler_dispatcher', None) existing_dispatch = class_.__dict__.get('_compiler_dispatch') @@ -380,7 +406,8 @@ def compiles(class_, *specs): existing.specs['default'] = existing_dispatch # TODO: why is the lambda needed ? - setattr(class_, '_compiler_dispatch', lambda *arg, **kw: existing(*arg, **kw)) + setattr(class_, '_compiler_dispatch', + lambda *arg, **kw: existing(*arg, **kw)) setattr(class_, '_compiler_dispatcher', existing) if specs: @@ -392,6 +419,18 @@ def compiles(class_, *specs): return fn return decorate + +def deregister(class_): + """Remove all custom compilers associated with a given + :class:`.ClauseElement` type.""" + + if hasattr(class_, '_compiler_dispatcher'): + # regenerate default _compiler_dispatch + visitors._generate_dispatch(class_) + # remove custom directive + del class_._compiler_dispatcher + + class _dispatcher(object): def __init__(self): self.specs = {} @@ -407,4 +446,3 @@ class _dispatcher(object): "%s construct has no default " "compilation handler." % type(element)) return fn(element, compiler, **kw) - diff --git a/libs/sqlalchemy/ext/declarative.py b/libs/sqlalchemy/ext/declarative/__init__.py old mode 100755 new mode 100644 similarity index 51% rename from libs/sqlalchemy/ext/declarative.py rename to libs/sqlalchemy/ext/declarative/__init__.py index b0876f0d..0ee4e33f --- a/libs/sqlalchemy/ext/declarative.py +++ b/libs/sqlalchemy/ext/declarative/__init__.py @@ -1,5 +1,5 @@ -# ext/declarative.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# ext/declarative/__init__.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -30,8 +30,7 @@ As a simple example:: Above, the :func:`declarative_base` callable returns a new base class from which all mapped classes should inherit. When the class definition is -completed, a new :class:`.Table` and -:func:`.mapper` will have been generated. +completed, a new :class:`.Table` and :func:`.mapper` will have been generated. The resulting table and mapper are accessible via ``__table__`` and ``__mapper__`` attributes on the @@ -52,7 +51,8 @@ assigned. To name columns explicitly with a name distinct from their mapped attribute, just give the column a name. Below, column "some_table_id" is mapped to the -"id" attribute of `SomeClass`, but in SQL will be represented as "some_table_id":: +"id" attribute of `SomeClass`, but in SQL will be represented as +"some_table_id":: class SomeClass(Base): __tablename__ = 'some_table' @@ -60,13 +60,13 @@ just give the column a name. Below, column "some_table_id" is mapped to the Attributes may be added to the class after its construction, and they will be added to the underlying :class:`.Table` and -:func:`.mapper()` definitions as appropriate:: +:func:`.mapper` definitions as appropriate:: SomeClass.data = Column('data', Unicode) SomeClass.related = relationship(RelatedInfo) Classes which are constructed using declarative can interact freely -with classes that are mapped explicitly with :func:`mapper`. +with classes that are mapped explicitly with :func:`.mapper`. It is recommended, though not required, that all tables share the same underlying :class:`~sqlalchemy.schema.MetaData` object, @@ -146,13 +146,52 @@ expression functions like :func:`~sqlalchemy.sql.expression.desc` and order_by="desc(Address.email)", primaryjoin="Address.user_id==User.id") -As an alternative to string-based attributes, attributes may also be -defined after all classes have been created. Just add them to the target -class after the fact:: +For the case where more than one module contains a class of the same name, +string class names can also be specified as module-qualified paths +within any of these string expressions:: + + class User(Base): + # .... + addresses = relationship("myapp.model.address.Address", + order_by="desc(myapp.model.address.Address.email)", + primaryjoin="myapp.model.address.Address.user_id==" + "myapp.model.user.User.id") + +The qualified path can be any partial path that removes ambiguity between +the names. For example, to disambiguate between +``myapp.model.address.Address`` and ``myapp.model.lookup.Address``, +we can specify ``address.Address`` or ``lookup.Address``:: + + class User(Base): + # .... + addresses = relationship("address.Address", + order_by="desc(address.Address.email)", + primaryjoin="address.Address.user_id==" + "User.id") + +.. versionadded:: 0.8 + module-qualified paths can be used when specifying string arguments + with Declarative, in order to specify specific modules. + +Two alternatives also exist to using string-based attributes. A lambda +can also be used, which will be evaluated after all mappers have been +configured:: + + class User(Base): + # ... + addresses = relationship(lambda: Address, + order_by=lambda: desc(Address.email), + primaryjoin=lambda: Address.user_id==User.id) + +Or, the relationship can be added to the class explicitly after the classes +are available:: User.addresses = relationship(Address, primaryjoin=Address.user_id==User.id) + + + Configuring Many-to-Many Relationships ====================================== @@ -175,9 +214,9 @@ the :class:`.MetaData` object used by the declarative base:: id = Column(Integer, primary_key=True) keywords = relationship("Keyword", secondary=keywords) -Like other :func:`.relationship` arguments, a string is accepted as well, -passing the string name of the table as defined in the ``Base.metadata.tables`` -collection:: +Like other :func:`~sqlalchemy.orm.relationship` arguments, a string is accepted +as well, passing the string name of the table as defined in the +``Base.metadata.tables`` collection:: class Author(Base): __tablename__ = 'authors' @@ -186,7 +225,7 @@ collection:: As with traditional mapping, its generally not a good idea to use a :class:`.Table` as the "secondary" argument which is also mapped to -a class, unless the :class:`.relationship` is declared with ``viewonly=True``. +a class, unless the :func:`.relationship` is declared with ``viewonly=True``. Otherwise, the unit-of-work system may attempt duplicate INSERT and DELETE statements against the underlying table. @@ -274,7 +313,8 @@ such as those which already take advantage of the data-driven nature of Note that when the ``__table__`` approach is used, the object is immediately usable as a plain :class:`.Table` within the class declaration body itself, as a Python class is only another syntactical block. Below this is illustrated -by using the ``id`` column in the ``primaryjoin`` condition of a :func:`.relationship`:: +by using the ``id`` column in the ``primaryjoin`` condition of a +:func:`.relationship`:: class MyClass(Base): __table__ = Table('my_table', Base.metadata, @@ -286,8 +326,8 @@ by using the ``id`` column in the ``primaryjoin`` condition of a :func:`.relatio primaryjoin=Widget.myclass_id==__table__.c.id) Similarly, mapped attributes which refer to ``__table__`` can be placed inline, -as below where we assign the ``name`` column to the attribute ``_name``, generating -a synonym for ``name``:: +as below where we assign the ``name`` column to the attribute ``_name``, +generating a synonym for ``name``:: from sqlalchemy.ext.declarative import synonym_for @@ -315,12 +355,13 @@ in conjunction with a mapped class:: However, one improvement that can be made here is to not require the :class:`.Engine` to be available when classes are -being first declared. To achieve this, use the example -described at :ref:`examples_declarative_reflection` to build a -declarative base that sets up mappings only after a special -``prepare(engine)`` step is called:: +being first declared. To achieve this, use the +:class:`.DeferredReflection` mixin, which sets up mappings +only after a special ``prepare(engine)`` step is called:: - Base = declarative_base(cls=DeclarativeReflectedBase) + from sqlalchemy.ext.declarative import declarative_base, DeferredReflection + + Base = declarative_base(cls=DeferredReflection) class Foo(Base): __tablename__ = 'foo' @@ -336,15 +377,17 @@ declarative base that sets up mappings only after a special Base.prepare(e) +.. versionadded:: 0.8 + Added :class:`.DeferredReflection`. Mapper Configuration ==================== Declarative makes use of the :func:`~.orm.mapper` function internally when it creates the mapping to the declared table. The options -for :func:`~.orm.mapper` are passed directly through via the ``__mapper_args__`` -class attribute. As always, arguments which reference locally -mapped columns can reference them directly from within the +for :func:`~.orm.mapper` are passed directly through via the +``__mapper_args__`` class attribute. As always, arguments which reference +locally mapped columns can reference them directly from within the class declaration:: from datetime import datetime @@ -407,6 +450,8 @@ only the ``engineers.id`` column, give it a different attribute name:: column over that of the superclass, such as querying above for ``Engineer.id``. Prior to 0.7 this was the reverse. +.. _declarative_single_table: + Single Table Inheritance ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -445,6 +490,115 @@ The attribute exclusion logic is provided by the behavior can be disabled by passing an explicit ``exclude_properties`` collection (empty or otherwise) to the ``__mapper_args__``. +Resolving Column Conflicts +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Note above that the ``primary_language`` and ``golf_swing`` columns +are "moved up" to be applied to ``Person.__table__``, as a result of their +declaration on a subclass that has no table of its own. A tricky case +comes up when two subclasses want to specify *the same* column, as below:: + + class Person(Base): + __tablename__ = 'people' + id = Column(Integer, primary_key=True) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on': discriminator} + + class Engineer(Person): + __mapper_args__ = {'polymorphic_identity': 'engineer'} + start_date = Column(DateTime) + + class Manager(Person): + __mapper_args__ = {'polymorphic_identity': 'manager'} + start_date = Column(DateTime) + +Above, the ``start_date`` column declared on both ``Engineer`` and ``Manager`` +will result in an error:: + + sqlalchemy.exc.ArgumentError: Column 'start_date' on class + conflicts with existing + column 'people.start_date' + +In a situation like this, Declarative can't be sure +of the intent, especially if the ``start_date`` columns had, for example, +different types. A situation like this can be resolved by using +:class:`.declared_attr` to define the :class:`.Column` conditionally, taking +care to return the **existing column** via the parent ``__table__`` if it +already exists:: + + from sqlalchemy.ext.declarative import declared_attr + + class Person(Base): + __tablename__ = 'people' + id = Column(Integer, primary_key=True) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on': discriminator} + + class Engineer(Person): + __mapper_args__ = {'polymorphic_identity': 'engineer'} + + @declared_attr + def start_date(cls): + "Start date column, if not present already." + return Person.__table__.c.get('start_date', Column(DateTime)) + + class Manager(Person): + __mapper_args__ = {'polymorphic_identity': 'manager'} + + @declared_attr + def start_date(cls): + "Start date column, if not present already." + return Person.__table__.c.get('start_date', Column(DateTime)) + +Above, when ``Manager`` is mapped, the ``start_date`` column is +already present on the ``Person`` class. Declarative lets us return +that :class:`.Column` as a result in this case, where it knows to skip +re-assigning the same column. If the mapping is mis-configured such +that the ``start_date`` column is accidentally re-assigned to a +different table (such as, if we changed ``Manager`` to be joined +inheritance without fixing ``start_date``), an error is raised which +indicates an existing :class:`.Column` is trying to be re-assigned to +a different owning :class:`.Table`. + +.. versionadded:: 0.8 :class:`.declared_attr` can be used on a non-mixin + class, and the returned :class:`.Column` or other mapped attribute + will be applied to the mapping as any other attribute. Previously, + the resulting attribute would be ignored, and also result in a warning + being emitted when a subclass was created. + +.. versionadded:: 0.8 :class:`.declared_attr`, when used either with a + mixin or non-mixin declarative class, can return an existing + :class:`.Column` already assigned to the parent :class:`.Table`, + to indicate that the re-assignment of the :class:`.Column` should be + skipped, however should still be mapped on the target class, + in order to resolve duplicate column conflicts. + +The same concept can be used with mixin classes (see +:ref:`declarative_mixins`):: + + class Person(Base): + __tablename__ = 'people' + id = Column(Integer, primary_key=True) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on': discriminator} + + class HasStartDate(object): + @declared_attr + def start_date(cls): + return cls.__table__.c.get('start_date', Column(DateTime)) + + class Engineer(HasStartDate, Person): + __mapper_args__ = {'polymorphic_identity': 'engineer'} + + class Manager(HasStartDate, Person): + __mapper_args__ = {'polymorphic_identity': 'manager'} + +The above mixin checks the local ``__table__`` attribute for the column. +Because we're using single table inheritance, we're sure that in this case, +``cls.__table__`` refers to ``People.__table__``. If we were mixing joined- +and single-table inheritance, we might want our mixin to check more carefully +if ``cls.__table__`` is really the :class:`.Table` we're looking for. + Concrete Table Inheritance ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -502,12 +656,13 @@ Using the Concrete Helpers ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Helper classes provides a simpler pattern for concrete inheritance. -With these objects, the ``__declare_last__`` helper is used to configure the "polymorphic" -loader for the mapper after all subclasses have been declared. +With these objects, the ``__declare_last__`` helper is used to configure the +"polymorphic" loader for the mapper after all subclasses have been declared. .. versionadded:: 0.7.3 -An abstract base can be declared using the :class:`.AbstractConcreteBase` class:: +An abstract base can be declared using the +:class:`.AbstractConcreteBase` class:: from sqlalchemy.ext.declarative import AbstractConcreteBase @@ -605,8 +760,8 @@ Augmenting the Base In addition to using a pure mixin, most of the techniques in this section can also be applied to the base class itself, for patterns that -should apply to all classes derived from a particular base. This -is achieved using the ``cls`` argument of the :func:`.declarative_base` function:: +should apply to all classes derived from a particular base. This is achieved +using the ``cls`` argument of the :func:`.declarative_base` function:: from sqlalchemy.ext.declarative import declared_attr @@ -626,9 +781,9 @@ is achieved using the ``cls`` argument of the :func:`.declarative_base` function class MyModel(Base): name = Column(String(1000)) -Where above, ``MyModel`` and all other classes that derive from ``Base`` will have -a table name derived from the class name, an ``id`` primary key column, as well as -the "InnoDB" engine for MySQL. +Where above, ``MyModel`` and all other classes that derive from ``Base`` will +have a table name derived from the class name, an ``id`` primary key column, +as well as the "InnoDB" engine for MySQL. Mixing in Columns ~~~~~~~~~~~~~~~~~ @@ -668,7 +823,7 @@ keys, as a :class:`.ForeignKey` itself contains references to columns which can't be properly recreated at this level. For columns that have foreign keys, as well as for the variety of mapper-level constructs that require destination-explicit context, the -:func:`~.declared_attr` decorator is provided so that +:class:`~.declared_attr` decorator is provided so that patterns common to many classes can be defined as callables:: from sqlalchemy.ext.declarative import declared_attr @@ -688,9 +843,10 @@ extension can use the resulting :class:`.Column` object as returned by the method without the need to copy it. .. versionchanged:: > 0.6.5 - Rename 0.6.5 ``sqlalchemy.util.classproperty`` into :func:`~.declared_attr`. + Rename 0.6.5 ``sqlalchemy.util.classproperty`` + into :class:`~.declared_attr`. -Columns generated by :func:`~.declared_attr` can also be +Columns generated by :class:`~.declared_attr` can also be referenced by ``__mapper_args__`` to a limited degree, currently by ``polymorphic_on`` and ``version_id_col``, by specifying the classdecorator itself into the dictionary - the declarative extension @@ -707,6 +863,8 @@ will resolve them at class construction time:: __tablename__='test' id = Column(Integer, primary_key=True) + + Mixing in Relationships ~~~~~~~~~~~~~~~~~~~~~~~ @@ -739,11 +897,57 @@ reference a common target class via many-to-one:: __tablename__ = 'target' id = Column(Integer, primary_key=True) +Using Advanced Relationship Arguments (e.g. ``primaryjoin``, etc.) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + :func:`~sqlalchemy.orm.relationship` definitions which require explicit -primaryjoin, order_by etc. expressions should use the string forms -for these arguments, so that they are evaluated as late as possible. -To reference the mixin class in these expressions, use the given ``cls`` -to get its name:: +primaryjoin, order_by etc. expressions should in all but the most +simplistic cases use **late bound** forms +for these arguments, meaning, using either the string form or a lambda. +The reason for this is that the related :class:`.Column` objects which are to +be configured using ``@declared_attr`` are not available to another +``@declared_attr`` attribute; while the methods will work and return new +:class:`.Column` objects, those are not the :class:`.Column` objects that +Declarative will be using as it calls the methods on its own, thus using +*different* :class:`.Column` objects. + +The canonical example is the primaryjoin condition that depends upon +another mixed-in column:: + + class RefTargetMixin(object): + @declared_attr + def target_id(cls): + return Column('target_id', ForeignKey('target.id')) + + @declared_attr + def target(cls): + return relationship(Target, + primaryjoin=Target.id==cls.target_id # this is *incorrect* + ) + +Mapping a class using the above mixin, we will get an error like:: + + sqlalchemy.exc.InvalidRequestError: this ForeignKey's parent column is not + yet associated with a Table. + +This is because the ``target_id`` :class:`.Column` we've called upon in our ``target()`` +method is not the same :class:`.Column` that declarative is actually going to map +to our table. + +The condition above is resolved using a lambda:: + + class RefTargetMixin(object): + @declared_attr + def target_id(cls): + return Column('target_id', ForeignKey('target.id')) + + @declared_attr + def target(cls): + return relationship(Target, + primaryjoin=lambda: Target.id==cls.target_id + ) + +or alternatively, the string form (which ultmately generates a lambda):: class RefTargetMixin(object): @declared_attr @@ -756,8 +960,8 @@ to get its name:: primaryjoin="Target.id==%s.target_id" % cls.__name__ ) -Mixing in deferred(), column_property(), etc. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Mixing in deferred(), column_property(), and other MapperProperty classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as @@ -775,6 +979,85 @@ requirement so that no reliance on copying is needed:: class Something(SomethingMixin, Base): __tablename__ = "something" +Mixing in Association Proxy and Other Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mixins can specify user-defined attributes as well as other extension +units such as :func:`.association_proxy`. The usage of +:class:`.declared_attr` is required in those cases where the attribute must +be tailored specifically to the target subclass. An example is when +constructing multiple :func:`.association_proxy` attributes which each +target a different type of child object. Below is an +:func:`.association_proxy` / mixin example which provides a scalar list of +string values to an implementing class:: + + from sqlalchemy import Column, Integer, ForeignKey, String + from sqlalchemy.orm import relationship + from sqlalchemy.ext.associationproxy import association_proxy + from sqlalchemy.ext.declarative import declarative_base, declared_attr + + Base = declarative_base() + + class HasStringCollection(object): + @declared_attr + def _strings(cls): + class StringAttribute(Base): + __tablename__ = cls.string_table_name + id = Column(Integer, primary_key=True) + value = Column(String(50), nullable=False) + parent_id = Column(Integer, + ForeignKey('%s.id' % cls.__tablename__), + nullable=False) + def __init__(self, value): + self.value = value + + return relationship(StringAttribute) + + @declared_attr + def strings(cls): + return association_proxy('_strings', 'value') + + class TypeA(HasStringCollection, Base): + __tablename__ = 'type_a' + string_table_name = 'type_a_strings' + id = Column(Integer(), primary_key=True) + + class TypeB(HasStringCollection, Base): + __tablename__ = 'type_b' + string_table_name = 'type_b_strings' + id = Column(Integer(), primary_key=True) + +Above, the ``HasStringCollection`` mixin produces a :func:`.relationship` +which refers to a newly generated class called ``StringAttribute``. The +``StringAttribute`` class is generated with it's own :class:`.Table` +definition which is local to the parent class making usage of the +``HasStringCollection`` mixin. It also produces an :func:`.association_proxy` +object which proxies references to the ``strings`` attribute onto the ``value`` +attribute of each ``StringAttribute`` instance. + +``TypeA`` or ``TypeB`` can be instantiated given the constructor +argument ``strings``, a list of strings:: + + ta = TypeA(strings=['foo', 'bar']) + tb = TypeA(strings=['bat', 'bar']) + +This list will generate a collection +of ``StringAttribute`` objects, which are persisted into a table that's +local to either the ``type_a_strings`` or ``type_b_strings`` table:: + + >>> print ta._strings + [<__main__.StringAttribute object at 0x10151cd90>, + <__main__.StringAttribute object at 0x10151ce10>] + +When constructing the :func:`.association_proxy`, the +:class:`.declared_attr` decorator must be used so that a distinct +:func:`.association_proxy` object is created for each of the ``TypeA`` +and ``TypeB`` classes. + +.. versionadded:: 0.8 :class:`.declared_attr` is usable with non-mapped + attributes, including user-defined attributes as well as + :func:`.association_proxy`. + Controlling table inheritance with mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -906,8 +1189,8 @@ Creating Indexes with Mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To define a named, potentially multicolumn :class:`.Index` that applies to all -tables derived from a mixin, use the "inline" form of :class:`.Index` and establish -it as part of ``__table_args__``:: +tables derived from a mixin, use the "inline" form of :class:`.Index` and +establish it as part of ``__table_args__``:: class MyMixin(object): a = Column(Integer) @@ -928,9 +1211,9 @@ Special Directives ~~~~~~~~~~~~~~~~~~~~~~ The ``__declare_last__()`` hook allows definition of -a class level function that is automatically called by the :meth:`.MapperEvents.after_configured` -event, which occurs after mappings are assumed to be completed and the 'configure' step -has finished:: +a class level function that is automatically called by the +:meth:`.MapperEvents.after_configured` event, which occurs after mappings are +assumed to be completed and the 'configure' step has finished:: class MyClass(Base): @classmethod @@ -946,9 +1229,9 @@ has finished:: ~~~~~~~~~~~~~~~~~~~ ``__abstract__`` causes declarative to skip the production -of a table or mapper for the class entirely. A class can be added within a hierarchy -in the same way as mixin (see :ref:`declarative_mixins`), allowing subclasses to extend -just from the special class:: +of a table or mapper for the class entirely. A class can be added within a +hierarchy in the same way as mixin (see :ref:`declarative_mixins`), allowing +subclasses to extend just from the special class:: class SomeAbstractBase(Base): __abstract__ = True @@ -963,8 +1246,8 @@ just from the special class:: class MyMappedClass(SomeAbstractBase): "" -One possible use of ``__abstract__`` is to use a distinct :class:`.MetaData` for different -bases:: +One possible use of ``__abstract__`` is to use a distinct +:class:`.MetaData` for different bases:: Base = declarative_base() @@ -976,9 +1259,10 @@ bases:: __abstract__ = True metadata = MetaData() -Above, classes which inherit from ``DefaultBase`` will use one :class:`.MetaData` as the -registry of tables, and those which inherit from ``OtherBase`` will use a different one. -The tables themselves can then be created perhaps within distinct databases:: +Above, classes which inherit from ``DefaultBase`` will use one +:class:`.MetaData` as the registry of tables, and those which inherit from +``OtherBase`` will use a different one. The tables themselves can then be +created perhaps within distinct databases:: DefaultBase.metadata.create_all(some_engine) OtherBase.metadata_create_all(some_other_engine) @@ -1000,7 +1284,7 @@ Sessions Note that ``declarative`` does nothing special with sessions, and is only intended as an easier way to configure mappers and :class:`~sqlalchemy.schema.Table` objects. A typical application -setup using :func:`~sqlalchemy.orm.scoped_session` might look like:: +setup using :class:`~sqlalchemy.orm.scoping.scoped_session` might look like:: engine = create_engine('postgresql://scott:tiger@localhost/test') Session = scoped_session(sessionmaker(autocommit=False, @@ -1013,749 +1297,13 @@ Mapped instances then make usage of """ -from sqlalchemy.schema import Table, Column, MetaData, _get_table_key -from sqlalchemy.orm import synonym as _orm_synonym, mapper,\ - comparable_property, class_mapper -from sqlalchemy.orm.interfaces import MapperProperty -from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, CompositeProperty -from sqlalchemy.orm.util import _is_mapped_class -from sqlalchemy import util, exc -from sqlalchemy.sql import util as sql_util, expression -from sqlalchemy import event -from sqlalchemy.orm.util import polymorphic_union, _mapper_or_none +from .api import declarative_base, synonym_for, comparable_using, \ + instrument_declarative, ConcreteBase, AbstractConcreteBase, \ + DeclarativeMeta, DeferredReflection, has_inherited_table,\ + declared_attr, as_declarative -__all__ = 'declarative_base', 'synonym_for', \ - 'comparable_using', 'instrument_declarative' - -def instrument_declarative(cls, registry, metadata): - """Given a class, configure the class declaratively, - using the given registry, which can be any dictionary, and - MetaData object. - - """ - if '_decl_class_registry' in cls.__dict__: - raise exc.InvalidRequestError( - "Class %r already has been " - "instrumented declaratively" % cls) - cls._decl_class_registry = registry - cls.metadata = metadata - _as_declarative(cls, cls.__name__, cls.__dict__) - -def has_inherited_table(cls): - """Given a class, return True if any of the classes it inherits from has a - mapped table, otherwise return False. - """ - for class_ in cls.__mro__: - if getattr(class_,'__table__',None) is not None: - return True - return False - -def _as_declarative(cls, classname, dict_): - - # dict_ will be a dictproxy, which we can't write to, and we need to! - dict_ = dict(dict_) - - column_copies = {} - potential_columns = {} - - mapper_args = {} - table_args = inherited_table_args = None - tablename = None - parent_columns = () - - declarative_props = (declared_attr, util.classproperty) - - for base in cls.__mro__: - _is_declarative_inherits = hasattr(base, '_decl_class_registry') - - if '__declare_last__' in base.__dict__: - @event.listens_for(mapper, "after_configured") - def go(): - cls.__declare_last__() - if '__abstract__' in base.__dict__: - if (base is cls or - (base in cls.__bases__ and not _is_declarative_inherits) - ): - return - - class_mapped = _is_mapped_class(base) - if class_mapped: - parent_columns = base.__table__.c.keys() - - for name,obj in vars(base).items(): - if name == '__mapper_args__': - if not mapper_args and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - mapper_args = cls.__mapper_args__ - elif name == '__tablename__': - if not tablename and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - tablename = cls.__tablename__ - elif name == '__table_args__': - if not table_args and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - table_args = cls.__table_args__ - if not isinstance(table_args, (tuple, dict, type(None))): - raise exc.ArgumentError( - "__table_args__ value must be a tuple, " - "dict, or None") - if base is not cls: - inherited_table_args = True - elif class_mapped: - if isinstance(obj, declarative_props): - util.warn("Regular (i.e. not __special__) " - "attribute '%s.%s' uses @declared_attr, " - "but owning class %s is mapped - " - "not applying to subclass %s." - % (base.__name__, name, base, cls)) - continue - elif base is not cls: - # we're a mixin. - if isinstance(obj, Column): - if obj.foreign_keys: - raise exc.InvalidRequestError( - "Columns with foreign keys to other columns " - "must be declared as @declared_attr callables " - "on declarative mixin classes. ") - if name not in dict_ and not ( - '__table__' in dict_ and - (obj.name or name) in dict_['__table__'].c - ) and name not in potential_columns: - potential_columns[name] = \ - column_copies[obj] = \ - obj.copy() - column_copies[obj]._creation_order = \ - obj._creation_order - elif isinstance(obj, MapperProperty): - raise exc.InvalidRequestError( - "Mapper properties (i.e. deferred," - "column_property(), relationship(), etc.) must " - "be declared as @declared_attr callables " - "on declarative mixin classes.") - elif isinstance(obj, declarative_props): - dict_[name] = ret = \ - column_copies[obj] = getattr(cls, name) - if isinstance(ret, (Column, MapperProperty)) and \ - ret.doc is None: - ret.doc = obj.__doc__ - - # apply inherited columns as we should - for k, v in potential_columns.items(): - if tablename or (v.name or k) not in parent_columns: - dict_[k] = v - - if inherited_table_args and not tablename: - table_args = None - - # make sure that column copies are used rather - # than the original columns from any mixins - for k in ('version_id_col', 'polymorphic_on',): - if k in mapper_args: - v = mapper_args[k] - mapper_args[k] = column_copies.get(v,v) - - if classname in cls._decl_class_registry: - util.warn("The classname %r is already in the registry of this" - " declarative base, mapped to %r" % ( - classname, - cls._decl_class_registry[classname] - )) - cls._decl_class_registry[classname] = cls - our_stuff = util.OrderedDict() - - for k in dict_: - value = dict_[k] - if isinstance(value, declarative_props): - value = getattr(cls, k) - - if (isinstance(value, tuple) and len(value) == 1 and - isinstance(value[0], (Column, MapperProperty))): - util.warn("Ignoring declarative-like tuple value of attribute " - "%s: possibly a copy-and-paste error with a comma " - "left at the end of the line?" % k) - continue - if not isinstance(value, (Column, MapperProperty)): - continue - if k == 'metadata': - raise exc.InvalidRequestError( - "Attribute name 'metadata' is reserved " - "for the MetaData instance when using a " - "declarative base class." - ) - prop = _deferred_relationship(cls, value) - our_stuff[k] = prop - - # set up attributes in the order they were created - our_stuff.sort(key=lambda key: our_stuff[key]._creation_order) - - # extract columns from the class dict - cols = set() - for key, c in our_stuff.iteritems(): - if isinstance(c, (ColumnProperty, CompositeProperty)): - for col in c.columns: - if isinstance(col, Column) and \ - col.table is None: - _undefer_column_name(key, col) - cols.add(col) - elif isinstance(c, Column): - _undefer_column_name(key, c) - cols.add(c) - # if the column is the same name as the key, - # remove it from the explicit properties dict. - # the normal rules for assigning column-based properties - # will take over, including precedence of columns - # in multi-column ColumnProperties. - if key == c.key: - del our_stuff[key] - cols = sorted(cols, key=lambda c:c._creation_order) - table = None - - if hasattr(cls, '__table_cls__'): - table_cls = util.unbound_method_to_callable(cls.__table_cls__) - else: - table_cls = Table - - if '__table__' not in dict_: - if tablename is not None: - - args, table_kw = (), {} - if table_args: - if isinstance(table_args, dict): - table_kw = table_args - elif isinstance(table_args, tuple): - if isinstance(table_args[-1], dict): - args, table_kw = table_args[0:-1], table_args[-1] - else: - args = table_args - - autoload = dict_.get('__autoload__') - if autoload: - table_kw['autoload'] = True - - cls.__table__ = table = table_cls(tablename, cls.metadata, - *(tuple(cols) + tuple(args)), - **table_kw) - else: - table = cls.__table__ - if cols: - for c in cols: - if not table.c.contains_column(c): - raise exc.ArgumentError( - "Can't add additional column %r when " - "specifying __table__" % c.key - ) - - if 'inherits' not in mapper_args: - for c in cls.__bases__: - if _is_mapped_class(c): - mapper_args['inherits'] = c - break - - if hasattr(cls, '__mapper_cls__'): - mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__) - else: - mapper_cls = mapper - - if table is None and 'inherits' not in mapper_args: - raise exc.InvalidRequestError( - "Class %r does not have a __table__ or __tablename__ " - "specified and does not inherit from an existing " - "table-mapped class." % cls - ) - - elif 'inherits' in mapper_args and not mapper_args.get('concrete', False): - inherited_mapper = class_mapper(mapper_args['inherits'], - compile=False) - inherited_table = inherited_mapper.local_table - - if table is None: - # single table inheritance. - # ensure no table args - if table_args: - raise exc.ArgumentError( - "Can't place __table_args__ on an inherited class " - "with no table." - ) - - # add any columns declared here to the inherited table. - for c in cols: - if c.primary_key: - raise exc.ArgumentError( - "Can't place primary key columns on an inherited " - "class with no table." - ) - if c.name in inherited_table.c: - raise exc.ArgumentError( - "Column '%s' on class %s conflicts with " - "existing column '%s'" % - (c, cls, inherited_table.c[c.name]) - ) - inherited_table.append_column(c) - - # single or joined inheritance - # exclude any cols on the inherited table which are not mapped on the - # parent class, to avoid - # mapping columns specific to sibling/nephew classes - inherited_mapper = class_mapper(mapper_args['inherits'], - compile=False) - inherited_table = inherited_mapper.local_table - - if 'exclude_properties' not in mapper_args: - mapper_args['exclude_properties'] = exclude_properties = \ - set([c.key for c in inherited_table.c - if c not in inherited_mapper._columntoproperty]) - exclude_properties.difference_update([c.key for c in cols]) - - # look through columns in the current mapper that - # are keyed to a propname different than the colname - # (if names were the same, we'd have popped it out above, - # in which case the mapper makes this combination). - # See if the superclass has a similar column property. - # If so, join them together. - for k, col in our_stuff.items(): - if not isinstance(col, expression.ColumnElement): - continue - if k in inherited_mapper._props: - p = inherited_mapper._props[k] - if isinstance(p, ColumnProperty): - # note here we place the subclass column - # first. See [ticket:1892] for background. - our_stuff[k] = [col] + p.columns - - - cls.__mapper__ = mapper_cls(cls, - table, - properties=our_stuff, - **mapper_args) - -class DeclarativeMeta(type): - def __init__(cls, classname, bases, dict_): - if '_decl_class_registry' not in cls.__dict__: - _as_declarative(cls, classname, cls.__dict__) - type.__init__(cls, classname, bases, dict_) - - def __setattr__(cls, key, value): - if '__mapper__' in cls.__dict__: - if isinstance(value, Column): - _undefer_column_name(key, value) - cls.__table__.append_column(value) - cls.__mapper__.add_property(key, value) - elif isinstance(value, ColumnProperty): - for col in value.columns: - if isinstance(col, Column) and col.table is None: - _undefer_column_name(key, col) - cls.__table__.append_column(col) - cls.__mapper__.add_property(key, value) - elif isinstance(value, MapperProperty): - cls.__mapper__.add_property( - key, - _deferred_relationship(cls, value) - ) - else: - type.__setattr__(cls, key, value) - else: - type.__setattr__(cls, key, value) - - -class _GetColumns(object): - def __init__(self, cls): - self.cls = cls - - def __getattr__(self, key): - mapper = class_mapper(self.cls, compile=False) - if mapper: - if not mapper.has_property(key): - raise exc.InvalidRequestError( - "Class %r does not have a mapped column named %r" - % (self.cls, key)) - - prop = mapper.get_property(key) - if not isinstance(prop, ColumnProperty): - raise exc.InvalidRequestError( - "Property %r is not an instance of" - " ColumnProperty (i.e. does not correspond" - " directly to a Column)." % key) - return getattr(self.cls, key) - -class _GetTable(object): - def __init__(self, key, metadata): - self.key = key - self.metadata = metadata - - def __getattr__(self, key): - return self.metadata.tables[ - _get_table_key(key, self.key) - ] - -def _deferred_relationship(cls, prop): - def resolve_arg(arg): - import sqlalchemy - - def access_cls(key): - if key in cls._decl_class_registry: - return _GetColumns(cls._decl_class_registry[key]) - elif key in cls.metadata.tables: - return cls.metadata.tables[key] - elif key in cls.metadata._schemas: - return _GetTable(key, cls.metadata) - else: - return sqlalchemy.__dict__[key] - - d = util.PopulateDict(access_cls) - def return_cls(): - try: - x = eval(arg, globals(), d) - - if isinstance(x, _GetColumns): - return x.cls - else: - return x - except NameError, n: - raise exc.InvalidRequestError( - "When initializing mapper %s, expression %r failed to " - "locate a name (%r). If this is a class name, consider " - "adding this relationship() to the %r class after " - "both dependent classes have been defined." % - (prop.parent, arg, n.args[0], cls) - ) - return return_cls - - if isinstance(prop, RelationshipProperty): - for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', - 'secondary', '_user_defined_foreign_keys', 'remote_side'): - v = getattr(prop, attr) - if isinstance(v, basestring): - setattr(prop, attr, resolve_arg(v)) - - if prop.backref and isinstance(prop.backref, tuple): - key, kwargs = prop.backref - for attr in ('primaryjoin', 'secondaryjoin', 'secondary', - 'foreign_keys', 'remote_side', 'order_by'): - if attr in kwargs and isinstance(kwargs[attr], basestring): - kwargs[attr] = resolve_arg(kwargs[attr]) - - - return prop - -def synonym_for(name, map_column=False): - """Decorator, make a Python @property a query synonym for a column. - - A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being - decorated is the 'descriptor', otherwise passes its arguments through to - synonym():: - - @synonym_for('col') - @property - def prop(self): - return 'special sauce' - - The regular ``synonym()`` is also usable directly in a declarative setting - and may be convenient for read/write properties:: - - prop = synonym('col', descriptor=property(_read_prop, _write_prop)) - - """ - def decorate(fn): - return _orm_synonym(name, map_column=map_column, descriptor=fn) - return decorate - -def comparable_using(comparator_factory): - """Decorator, allow a Python @property to be used in query criteria. - - This is a decorator front end to - :func:`~sqlalchemy.orm.comparable_property` that passes - through the comparator_factory and the function being decorated:: - - @comparable_using(MyComparatorType) - @property - def prop(self): - return 'special sauce' - - The regular ``comparable_property()`` is also usable directly in a - declarative setting and may be convenient for read/write properties:: - - prop = comparable_property(MyComparatorType) - - """ - def decorate(fn): - return comparable_property(comparator_factory, fn) - return decorate - -class declared_attr(property): - """Mark a class-level method as representing the definition of - a mapped property or special declarative member name. - - .. versionchanged:: 0.6.{2,3,4} - ``@declared_attr`` is available as - ``sqlalchemy.util.classproperty`` for SQLAlchemy versions - 0.6.2, 0.6.3, 0.6.4. - - @declared_attr turns the attribute into a scalar-like - property that can be invoked from the uninstantiated class. - Declarative treats attributes specifically marked with - @declared_attr as returning a construct that is specific - to mapping or declarative table configuration. The name - of the attribute is that of what the non-dynamic version - of the attribute would be. - - @declared_attr is more often than not applicable to mixins, - to define relationships that are to be applied to different - implementors of the class:: - - class ProvidesUser(object): - "A mixin that adds a 'user' relationship to classes." - - @declared_attr - def user(self): - return relationship("User") - - It also can be applied to mapped classes, such as to provide - a "polymorphic" scheme for inheritance:: - - class Employee(Base): - id = Column(Integer, primary_key=True) - type = Column(String(50), nullable=False) - - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - @declared_attr - def __mapper_args__(cls): - if cls.__name__ == 'Employee': - return { - "polymorphic_on":cls.type, - "polymorphic_identity":"Employee" - } - else: - return {"polymorphic_identity":cls.__name__} - - """ - - def __init__(self, fget, *arg, **kw): - super(declared_attr, self).__init__(fget, *arg, **kw) - self.__doc__ = fget.__doc__ - - def __get__(desc, self, cls): - return desc.fget(cls) - -def _declarative_constructor(self, **kwargs): - """A simple constructor that allows initialization from kwargs. - - Sets attributes on the constructed instance using the names and - values in ``kwargs``. - - Only keys that are present as - attributes of the instance's class are allowed. These could be, - for example, any mapped columns or relationships. - """ - cls_ = type(self) - for k in kwargs: - if not hasattr(cls_, k): - raise TypeError( - "%r is an invalid keyword argument for %s" % - (k, cls_.__name__)) - setattr(self, k, kwargs[k]) -_declarative_constructor.__name__ = '__init__' - -def declarative_base(bind=None, metadata=None, mapper=None, cls=object, - name='Base', constructor=_declarative_constructor, - class_registry=None, - metaclass=DeclarativeMeta): - """Construct a base class for declarative class definitions. - - The new base class will be given a metaclass that produces - appropriate :class:`~sqlalchemy.schema.Table` objects and makes - the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the - information provided declaratively in the class and any subclasses - of the class. - - :param bind: An optional - :class:`~sqlalchemy.engine.base.Connectable`, will be assigned - the ``bind`` attribute on the :class:`~sqlalchemy.MetaData` - instance. - - :param metadata: - An optional :class:`~sqlalchemy.MetaData` instance. All - :class:`~sqlalchemy.schema.Table` objects implicitly declared by - subclasses of the base will share this MetaData. A MetaData instance - will be created if none is provided. The - :class:`~sqlalchemy.MetaData` instance will be available via the - `metadata` attribute of the generated declarative base class. - - :param mapper: - An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will - be used to map subclasses to their Tables. - - :param cls: - Defaults to :class:`object`. A type to use as the base for the generated - declarative base class. May be a class or tuple of classes. - - :param name: - Defaults to ``Base``. The display name for the generated - class. Customizing this is not required, but can improve clarity in - tracebacks and debugging. - - :param constructor: - Defaults to - :func:`~sqlalchemy.ext.declarative._declarative_constructor`, an - __init__ implementation that assigns \**kwargs for declared - fields and relationships to an instance. If ``None`` is supplied, - no __init__ will be provided and construction will fall back to - cls.__init__ by way of the normal Python semantics. - - :param class_registry: optional dictionary that will serve as the - registry of class names-> mapped classes when string names - are used to identify classes inside of :func:`.relationship` - and others. Allows two or more declarative base classes - to share the same registry of class names for simplified - inter-base relationships. - - :param metaclass: - Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ - compatible callable to use as the meta type of the generated - declarative base class. - - """ - lcl_metadata = metadata or MetaData() - if bind: - lcl_metadata.bind = bind - - if class_registry is None: - class_registry = {} - - bases = not isinstance(cls, tuple) and (cls,) or cls - class_dict = dict(_decl_class_registry=class_registry, - metadata=lcl_metadata) - - if constructor: - class_dict['__init__'] = constructor - if mapper: - class_dict['__mapper_cls__'] = mapper - - return metaclass(name, bases, class_dict) - -def _undefer_column_name(key, column): - if column.key is None: - column.key = key - if column.name is None: - column.name = key - -class ConcreteBase(object): - """A helper class for 'concrete' declarative mappings. - - :class:`.ConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :func:`.MapperEvents.after_configured` event. - - :class:`.ConcreteBase` produces a mapped - table for the class itself. Compare to :class:`.AbstractConcreteBase`, - which does not. - - Example:: - - from sqlalchemy.ext.declarative import ConcreteBase - - class Employee(ConcreteBase, Base): - __tablename__ = 'employee' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - __mapper_args__ = { - 'polymorphic_identity':'employee', - 'concrete':True} - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - """ - - @classmethod - def _create_polymorphic_union(cls, mappers): - return polymorphic_union(dict( - (mapper.polymorphic_identity, mapper.local_table) - for mapper in mappers - ), 'type', 'pjoin') - - @classmethod - def __declare_last__(cls): - m = cls.__mapper__ - if m.with_polymorphic: - return - - mappers = list(m.self_and_descendants) - pjoin = cls._create_polymorphic_union(mappers) - m._set_with_polymorphic(("*",pjoin)) - m._set_polymorphic_on(pjoin.c.type) - -class AbstractConcreteBase(ConcreteBase): - """A helper class for 'concrete' declarative mappings. - - :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :func:`.MapperEvents.after_configured` event. - - :class:`.AbstractConcreteBase` does not produce a mapped - table for the class itself. Compare to :class:`.ConcreteBase`, - which does. - - Example:: - - from sqlalchemy.ext.declarative import ConcreteBase - - class Employee(AbstractConcreteBase, Base): - pass - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - """ - - __abstract__ = True - - @classmethod - def __declare_last__(cls): - if hasattr(cls, '__mapper__'): - return - - # can't rely on 'self_and_descendants' here - # since technically an immediate subclass - # might not be mapped, but a subclass - # may be. - mappers = [] - stack = list(cls.__subclasses__()) - while stack: - klass = stack.pop() - stack.extend(klass.__subclasses__()) - mn = _mapper_or_none(klass) - if mn is not None: - mappers.append(mn) - pjoin = cls._create_polymorphic_union(mappers) - cls.__mapper__ = m = mapper(cls, pjoin, polymorphic_on=pjoin.c.type) - - for scls in cls.__subclasses__(): - sm = _mapper_or_none(scls) - if sm.concrete and cls in scls.__bases__: - sm._set_concrete_base(m) +__all__ = ['declarative_base', 'synonym_for', 'has_inherited_table', + 'comparable_using', 'instrument_declarative', 'declared_attr', + 'ConcreteBase', 'AbstractConcreteBase', 'DeclarativeMeta', + 'DeferredReflection'] diff --git a/libs/sqlalchemy/ext/declarative/api.py b/libs/sqlalchemy/ext/declarative/api.py new file mode 100644 index 00000000..2418c6e5 --- /dev/null +++ b/libs/sqlalchemy/ext/declarative/api.py @@ -0,0 +1,512 @@ +# ext/declarative/api.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Public API functions and helpers for declarative.""" + + +from ...schema import Table, MetaData +from ...orm import synonym as _orm_synonym, mapper,\ + comparable_property,\ + interfaces, properties +from ...orm.util import polymorphic_union +from ...orm.base import _mapper_or_none +from ...util import compat +from ... import exc +import weakref + +from .base import _as_declarative, \ + _declarative_constructor,\ + _DeferredMapperConfig, _add_attribute +from .clsregistry import _class_resolver + + +def instrument_declarative(cls, registry, metadata): + """Given a class, configure the class declaratively, + using the given registry, which can be any dictionary, and + MetaData object. + + """ + if '_decl_class_registry' in cls.__dict__: + raise exc.InvalidRequestError( + "Class %r already has been " + "instrumented declaratively" % cls) + cls._decl_class_registry = registry + cls.metadata = metadata + _as_declarative(cls, cls.__name__, cls.__dict__) + + +def has_inherited_table(cls): + """Given a class, return True if any of the classes it inherits from has a + mapped table, otherwise return False. + """ + for class_ in cls.__mro__[1:]: + if getattr(class_, '__table__', None) is not None: + return True + return False + + +class DeclarativeMeta(type): + def __init__(cls, classname, bases, dict_): + if '_decl_class_registry' not in cls.__dict__: + _as_declarative(cls, classname, cls.__dict__) + type.__init__(cls, classname, bases, dict_) + + def __setattr__(cls, key, value): + _add_attribute(cls, key, value) + + +def synonym_for(name, map_column=False): + """Decorator, make a Python @property a query synonym for a column. + + A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being + decorated is the 'descriptor', otherwise passes its arguments through to + synonym():: + + @synonym_for('col') + @property + def prop(self): + return 'special sauce' + + The regular ``synonym()`` is also usable directly in a declarative setting + and may be convenient for read/write properties:: + + prop = synonym('col', descriptor=property(_read_prop, _write_prop)) + + """ + def decorate(fn): + return _orm_synonym(name, map_column=map_column, descriptor=fn) + return decorate + + +def comparable_using(comparator_factory): + """Decorator, allow a Python @property to be used in query criteria. + + This is a decorator front end to + :func:`~sqlalchemy.orm.comparable_property` that passes + through the comparator_factory and the function being decorated:: + + @comparable_using(MyComparatorType) + @property + def prop(self): + return 'special sauce' + + The regular ``comparable_property()`` is also usable directly in a + declarative setting and may be convenient for read/write properties:: + + prop = comparable_property(MyComparatorType) + + """ + def decorate(fn): + return comparable_property(comparator_factory, fn) + return decorate + + +class declared_attr(interfaces._MappedAttribute, property): + """Mark a class-level method as representing the definition of + a mapped property or special declarative member name. + + @declared_attr turns the attribute into a scalar-like + property that can be invoked from the uninstantiated class. + Declarative treats attributes specifically marked with + @declared_attr as returning a construct that is specific + to mapping or declarative table configuration. The name + of the attribute is that of what the non-dynamic version + of the attribute would be. + + @declared_attr is more often than not applicable to mixins, + to define relationships that are to be applied to different + implementors of the class:: + + class ProvidesUser(object): + "A mixin that adds a 'user' relationship to classes." + + @declared_attr + def user(self): + return relationship("User") + + It also can be applied to mapped classes, such as to provide + a "polymorphic" scheme for inheritance:: + + class Employee(Base): + id = Column(Integer, primary_key=True) + type = Column(String(50), nullable=False) + + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @declared_attr + def __mapper_args__(cls): + if cls.__name__ == 'Employee': + return { + "polymorphic_on":cls.type, + "polymorphic_identity":"Employee" + } + else: + return {"polymorphic_identity":cls.__name__} + + .. versionchanged:: 0.8 :class:`.declared_attr` can be used with + non-ORM or extension attributes, such as user-defined attributes + or :func:`.association_proxy` objects, which will be assigned + to the class at class construction time. + + + """ + + def __init__(self, fget, *arg, **kw): + super(declared_attr, self).__init__(fget, *arg, **kw) + self.__doc__ = fget.__doc__ + + def __get__(desc, self, cls): + return desc.fget(cls) + + +def declarative_base(bind=None, metadata=None, mapper=None, cls=object, + name='Base', constructor=_declarative_constructor, + class_registry=None, + metaclass=DeclarativeMeta): + """Construct a base class for declarative class definitions. + + The new base class will be given a metaclass that produces + appropriate :class:`~sqlalchemy.schema.Table` objects and makes + the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the + information provided declaratively in the class and any subclasses + of the class. + + :param bind: An optional + :class:`~sqlalchemy.engine.Connectable`, will be assigned + the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData` + instance. + + :param metadata: + An optional :class:`~sqlalchemy.schema.MetaData` instance. All + :class:`~sqlalchemy.schema.Table` objects implicitly declared by + subclasses of the base will share this MetaData. A MetaData instance + will be created if none is provided. The + :class:`~sqlalchemy.schema.MetaData` instance will be available via the + `metadata` attribute of the generated declarative base class. + + :param mapper: + An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will + be used to map subclasses to their Tables. + + :param cls: + Defaults to :class:`object`. A type to use as the base for the generated + declarative base class. May be a class or tuple of classes. + + :param name: + Defaults to ``Base``. The display name for the generated + class. Customizing this is not required, but can improve clarity in + tracebacks and debugging. + + :param constructor: + Defaults to + :func:`~sqlalchemy.ext.declarative._declarative_constructor`, an + __init__ implementation that assigns \**kwargs for declared + fields and relationships to an instance. If ``None`` is supplied, + no __init__ will be provided and construction will fall back to + cls.__init__ by way of the normal Python semantics. + + :param class_registry: optional dictionary that will serve as the + registry of class names-> mapped classes when string names + are used to identify classes inside of :func:`.relationship` + and others. Allows two or more declarative base classes + to share the same registry of class names for simplified + inter-base relationships. + + :param metaclass: + Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ + compatible callable to use as the meta type of the generated + declarative base class. + + .. seealso:: + + :func:`.as_declarative` + + """ + lcl_metadata = metadata or MetaData() + if bind: + lcl_metadata.bind = bind + + if class_registry is None: + class_registry = weakref.WeakValueDictionary() + + bases = not isinstance(cls, tuple) and (cls,) or cls + class_dict = dict(_decl_class_registry=class_registry, + metadata=lcl_metadata) + + if constructor: + class_dict['__init__'] = constructor + if mapper: + class_dict['__mapper_cls__'] = mapper + + return metaclass(name, bases, class_dict) + +def as_declarative(**kw): + """ + Class decorator for :func:`.declarative_base`. + + Provides a syntactical shortcut to the ``cls`` argument + sent to :func:`.declarative_base`, allowing the base class + to be converted in-place to a "declarative" base:: + + from sqlalchemy.ext.declarative import as_declarative + + @as_declarative() + class Base(object): + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + id = Column(Integer, primary_key=True) + + class MyMappedClass(Base): + # ... + + All keyword arguments passed to :func:`.as_declarative` are passed + along to :func:`.declarative_base`. + + .. versionadded:: 0.8.3 + + .. seealso:: + + :func:`.declarative_base` + + """ + def decorate(cls): + kw['cls'] = cls + kw['name'] = cls.__name__ + return declarative_base(**kw) + + return decorate + +class ConcreteBase(object): + """A helper class for 'concrete' declarative mappings. + + :class:`.ConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :meth:`.after_configured` event. + + :class:`.ConcreteBase` produces a mapped + table for the class itself. Compare to :class:`.AbstractConcreteBase`, + which does not. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + @classmethod + def _create_polymorphic_union(cls, mappers): + return polymorphic_union(dict( + (mp.polymorphic_identity, mp.local_table) + for mp in mappers + ), 'type', 'pjoin') + + @classmethod + def __declare_last__(cls): + m = cls.__mapper__ + if m.with_polymorphic: + return + + mappers = list(m.self_and_descendants) + pjoin = cls._create_polymorphic_union(mappers) + m._set_with_polymorphic(("*", pjoin)) + m._set_polymorphic_on(pjoin.c.type) + + +class AbstractConcreteBase(ConcreteBase): + """A helper class for 'concrete' declarative mappings. + + :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :meth:`.after_configured` event. + + :class:`.AbstractConcreteBase` does not produce a mapped + table for the class itself. Compare to :class:`.ConcreteBase`, + which does. + + Example:: + + from sqlalchemy.ext.declarative import AbstractConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + __abstract__ = True + + @classmethod + def __declare_last__(cls): + if hasattr(cls, '__mapper__'): + return + + # can't rely on 'self_and_descendants' here + # since technically an immediate subclass + # might not be mapped, but a subclass + # may be. + mappers = [] + stack = list(cls.__subclasses__()) + while stack: + klass = stack.pop() + stack.extend(klass.__subclasses__()) + mn = _mapper_or_none(klass) + if mn is not None: + mappers.append(mn) + pjoin = cls._create_polymorphic_union(mappers) + cls.__mapper__ = m = mapper(cls, pjoin, polymorphic_on=pjoin.c.type) + + for scls in cls.__subclasses__(): + sm = _mapper_or_none(scls) + if sm.concrete and cls in scls.__bases__: + sm._set_concrete_base(m) + + +class DeferredReflection(object): + """A helper class for construction of mappings based on + a deferred reflection step. + + Normally, declarative can be used with reflection by + setting a :class:`.Table` object using autoload=True + as the ``__table__`` attribute on a declarative class. + The caveat is that the :class:`.Table` must be fully + reflected, or at the very least have a primary key column, + at the point at which a normal declarative mapping is + constructed, meaning the :class:`.Engine` must be available + at class declaration time. + + The :class:`.DeferredReflection` mixin moves the construction + of mappers to be at a later point, after a specific + method is called which first reflects all :class:`.Table` + objects created so far. Classes can define it as such:: + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.ext.declarative import DeferredReflection + Base = declarative_base() + + class MyClass(DeferredReflection, Base): + __tablename__ = 'mytable' + + Above, ``MyClass`` is not yet mapped. After a series of + classes have been defined in the above fashion, all tables + can be reflected and mappings created using + :meth:`.prepare`:: + + engine = create_engine("someengine://...") + DeferredReflection.prepare(engine) + + The :class:`.DeferredReflection` mixin can be applied to individual + classes, used as the base for the declarative base itself, + or used in a custom abstract class. Using an abstract base + allows that only a subset of classes to be prepared for a + particular prepare step, which is necessary for applications + that use more than one engine. For example, if an application + has two engines, you might use two bases, and prepare each + separately, e.g.:: + + class ReflectedOne(DeferredReflection, Base): + __abstract__ = True + + class ReflectedTwo(DeferredReflection, Base): + __abstract__ = True + + class MyClass(ReflectedOne): + __tablename__ = 'mytable' + + class MyOtherClass(ReflectedOne): + __tablename__ = 'myothertable' + + class YetAnotherClass(ReflectedTwo): + __tablename__ = 'yetanothertable' + + # ... etc. + + Above, the class hierarchies for ``ReflectedOne`` and + ``ReflectedTwo`` can be configured separately:: + + ReflectedOne.prepare(engine_one) + ReflectedTwo.prepare(engine_two) + + .. versionadded:: 0.8 + + """ + @classmethod + def prepare(cls, engine): + """Reflect all :class:`.Table` objects for all current + :class:`.DeferredReflection` subclasses""" + + to_map = _DeferredMapperConfig.classes_for_base(cls) + for thingy in to_map: + cls._sa_decl_prepare(thingy.local_table, engine) + thingy.map() + mapper = thingy.cls.__mapper__ + metadata = mapper.class_.metadata + for rel in mapper._props.values(): + if isinstance(rel, properties.RelationshipProperty) and \ + rel.secondary is not None: + if isinstance(rel.secondary, Table): + cls._reflect_table(rel.secondary, engine) + elif isinstance(rel.secondary, _class_resolver): + rel.secondary._resolvers += ( + cls._sa_deferred_table_resolver(engine, metadata), + ) + + @classmethod + def _sa_deferred_table_resolver(cls, engine, metadata): + def _resolve(key): + t1 = Table(key, metadata) + cls._reflect_table(t1, engine) + return t1 + return _resolve + + @classmethod + def _sa_decl_prepare(cls, local_table, engine): + # autoload Table, which is already + # present in the metadata. This + # will fill in db-loaded columns + # into the existing Table object. + if local_table is not None: + cls._reflect_table(local_table, engine) + + @classmethod + def _reflect_table(cls, table, engine): + Table(table.name, + table.metadata, + extend_existing=True, + autoload_replace=False, + autoload=True, + autoload_with=engine, + schema=table.schema) diff --git a/libs/sqlalchemy/ext/declarative/base.py b/libs/sqlalchemy/ext/declarative/base.py new file mode 100644 index 00000000..a764f126 --- /dev/null +++ b/libs/sqlalchemy/ext/declarative/base.py @@ -0,0 +1,506 @@ +# ext/declarative/base.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Internal implementation for declarative.""" + +from ...schema import Table, Column +from ...orm import mapper, class_mapper, synonym +from ...orm.interfaces import MapperProperty +from ...orm.properties import ColumnProperty, CompositeProperty +from ...orm.attributes import QueryableAttribute +from ...orm.base import _is_mapped_class +from ... import util, exc +from ...sql import expression +from ... import event +from . import clsregistry +import collections +import weakref + +def _declared_mapping_info(cls): + # deferred mapping + if _DeferredMapperConfig.has_cls(cls): + return _DeferredMapperConfig.config_for_cls(cls) + # regular mapping + elif _is_mapped_class(cls): + return class_mapper(cls, configure=False) + else: + return None + + +def _as_declarative(cls, classname, dict_): + from .api import declared_attr + + # dict_ will be a dictproxy, which we can't write to, and we need to! + dict_ = dict(dict_) + + column_copies = {} + potential_columns = {} + + mapper_args_fn = None + table_args = inherited_table_args = None + tablename = None + + declarative_props = (declared_attr, util.classproperty) + + for base in cls.__mro__: + _is_declarative_inherits = hasattr(base, '_decl_class_registry') + + if '__declare_last__' in base.__dict__: + @event.listens_for(mapper, "after_configured") + def go(): + cls.__declare_last__() + if '__abstract__' in base.__dict__: + if (base is cls or + (base in cls.__bases__ and not _is_declarative_inherits) + ): + return + + class_mapped = _declared_mapping_info(base) is not None + + for name, obj in vars(base).items(): + if name == '__mapper_args__': + if not mapper_args_fn and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + # don't even invoke __mapper_args__ until + # after we've determined everything about the + # mapped table. + mapper_args_fn = lambda: cls.__mapper_args__ + elif name == '__tablename__': + if not tablename and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + tablename = cls.__tablename__ + elif name == '__table_args__': + if not table_args and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + table_args = cls.__table_args__ + if not isinstance(table_args, (tuple, dict, type(None))): + raise exc.ArgumentError( + "__table_args__ value must be a tuple, " + "dict, or None") + if base is not cls: + inherited_table_args = True + elif class_mapped: + if isinstance(obj, declarative_props): + util.warn("Regular (i.e. not __special__) " + "attribute '%s.%s' uses @declared_attr, " + "but owning class %s is mapped - " + "not applying to subclass %s." + % (base.__name__, name, base, cls)) + continue + elif base is not cls: + # we're a mixin. + if isinstance(obj, Column): + if getattr(cls, name) is not obj: + # if column has been overridden + # (like by the InstrumentedAttribute of the + # superclass), skip + continue + if obj.foreign_keys: + raise exc.InvalidRequestError( + "Columns with foreign keys to other columns " + "must be declared as @declared_attr callables " + "on declarative mixin classes. ") + if name not in dict_ and not ( + '__table__' in dict_ and + (obj.name or name) in dict_['__table__'].c + ) and name not in potential_columns: + potential_columns[name] = \ + column_copies[obj] = \ + obj.copy() + column_copies[obj]._creation_order = \ + obj._creation_order + elif isinstance(obj, MapperProperty): + raise exc.InvalidRequestError( + "Mapper properties (i.e. deferred," + "column_property(), relationship(), etc.) must " + "be declared as @declared_attr callables " + "on declarative mixin classes.") + elif isinstance(obj, declarative_props): + dict_[name] = ret = \ + column_copies[obj] = getattr(cls, name) + if isinstance(ret, (Column, MapperProperty)) and \ + ret.doc is None: + ret.doc = obj.__doc__ + + # apply inherited columns as we should + for k, v in potential_columns.items(): + dict_[k] = v + + if inherited_table_args and not tablename: + table_args = None + + clsregistry.add_class(classname, cls) + our_stuff = util.OrderedDict() + + for k in list(dict_): + + # TODO: improve this ? all dunders ? + if k in ('__table__', '__tablename__', '__mapper_args__'): + continue + + value = dict_[k] + if isinstance(value, declarative_props): + value = getattr(cls, k) + + elif isinstance(value, QueryableAttribute) and \ + value.class_ is not cls and \ + value.key != k: + # detect a QueryableAttribute that's already mapped being + # assigned elsewhere in userland, turn into a synonym() + value = synonym(value.key) + setattr(cls, k, value) + + + if (isinstance(value, tuple) and len(value) == 1 and + isinstance(value[0], (Column, MapperProperty))): + util.warn("Ignoring declarative-like tuple value of attribute " + "%s: possibly a copy-and-paste error with a comma " + "left at the end of the line?" % k) + continue + if not isinstance(value, (Column, MapperProperty)): + if not k.startswith('__'): + dict_.pop(k) + setattr(cls, k, value) + continue + if k == 'metadata': + raise exc.InvalidRequestError( + "Attribute name 'metadata' is reserved " + "for the MetaData instance when using a " + "declarative base class." + ) + prop = clsregistry._deferred_relationship(cls, value) + our_stuff[k] = prop + + # set up attributes in the order they were created + our_stuff.sort(key=lambda key: our_stuff[key]._creation_order) + + # extract columns from the class dict + declared_columns = set() + name_to_prop_key = collections.defaultdict(set) + for key, c in list(our_stuff.items()): + if isinstance(c, (ColumnProperty, CompositeProperty)): + for col in c.columns: + if isinstance(col, Column) and \ + col.table is None: + _undefer_column_name(key, col) + if not isinstance(c, CompositeProperty): + name_to_prop_key[col.name].add(key) + declared_columns.add(col) + elif isinstance(c, Column): + _undefer_column_name(key, c) + name_to_prop_key[c.name].add(key) + declared_columns.add(c) + # if the column is the same name as the key, + # remove it from the explicit properties dict. + # the normal rules for assigning column-based properties + # will take over, including precedence of columns + # in multi-column ColumnProperties. + if key == c.key: + del our_stuff[key] + + for name, keys in name_to_prop_key.items(): + if len(keys) > 1: + util.warn( + "On class %r, Column object %r named directly multiple times, " + "only one will be used: %s" % + (classname, name, (", ".join(sorted(keys)))) + ) + + declared_columns = sorted( + declared_columns, key=lambda c: c._creation_order) + table = None + + if hasattr(cls, '__table_cls__'): + table_cls = util.unbound_method_to_callable(cls.__table_cls__) + else: + table_cls = Table + + if '__table__' not in dict_: + if tablename is not None: + + args, table_kw = (), {} + if table_args: + if isinstance(table_args, dict): + table_kw = table_args + elif isinstance(table_args, tuple): + if isinstance(table_args[-1], dict): + args, table_kw = table_args[0:-1], table_args[-1] + else: + args = table_args + + autoload = dict_.get('__autoload__') + if autoload: + table_kw['autoload'] = True + + cls.__table__ = table = table_cls( + tablename, cls.metadata, + *(tuple(declared_columns) + tuple(args)), + **table_kw) + else: + table = cls.__table__ + if declared_columns: + for c in declared_columns: + if not table.c.contains_column(c): + raise exc.ArgumentError( + "Can't add additional column %r when " + "specifying __table__" % c.key + ) + + if hasattr(cls, '__mapper_cls__'): + mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__) + else: + mapper_cls = mapper + + for c in cls.__bases__: + if _declared_mapping_info(c) is not None: + inherits = c + break + else: + inherits = None + + if table is None and inherits is None: + raise exc.InvalidRequestError( + "Class %r does not have a __table__ or __tablename__ " + "specified and does not inherit from an existing " + "table-mapped class." % cls + ) + elif inherits: + inherited_mapper = _declared_mapping_info(inherits) + inherited_table = inherited_mapper.local_table + inherited_mapped_table = inherited_mapper.mapped_table + + if table is None: + # single table inheritance. + # ensure no table args + if table_args: + raise exc.ArgumentError( + "Can't place __table_args__ on an inherited class " + "with no table." + ) + # add any columns declared here to the inherited table. + for c in declared_columns: + if c.primary_key: + raise exc.ArgumentError( + "Can't place primary key columns on an inherited " + "class with no table." + ) + if c.name in inherited_table.c: + if inherited_table.c[c.name] is c: + continue + raise exc.ArgumentError( + "Column '%s' on class %s conflicts with " + "existing column '%s'" % + (c, cls, inherited_table.c[c.name]) + ) + inherited_table.append_column(c) + if inherited_mapped_table is not None and \ + inherited_mapped_table is not inherited_table: + inherited_mapped_table._refresh_for_new_column(c) + + defer_map = hasattr(cls, '_sa_decl_prepare') + if defer_map: + cfg_cls = _DeferredMapperConfig + else: + cfg_cls = _MapperConfig + mt = cfg_cls(mapper_cls, + cls, table, + inherits, + declared_columns, + column_copies, + our_stuff, + mapper_args_fn) + if not defer_map: + mt.map() + + +class _MapperConfig(object): + + mapped_table = None + + def __init__(self, mapper_cls, + cls, + table, + inherits, + declared_columns, + column_copies, + properties, mapper_args_fn): + self.mapper_cls = mapper_cls + self.cls = cls + self.local_table = table + self.inherits = inherits + self.properties = properties + self.mapper_args_fn = mapper_args_fn + self.declared_columns = declared_columns + self.column_copies = column_copies + + + def _prepare_mapper_arguments(self): + properties = self.properties + if self.mapper_args_fn: + mapper_args = self.mapper_args_fn() + else: + mapper_args = {} + + # make sure that column copies are used rather + # than the original columns from any mixins + for k in ('version_id_col', 'polymorphic_on',): + if k in mapper_args: + v = mapper_args[k] + mapper_args[k] = self.column_copies.get(v, v) + + assert 'inherits' not in mapper_args, \ + "Can't specify 'inherits' explicitly with declarative mappings" + + if self.inherits: + mapper_args['inherits'] = self.inherits + + if self.inherits and not mapper_args.get('concrete', False): + # single or joined inheritance + # exclude any cols on the inherited table which are + # not mapped on the parent class, to avoid + # mapping columns specific to sibling/nephew classes + inherited_mapper = _declared_mapping_info(self.inherits) + inherited_table = inherited_mapper.local_table + + if 'exclude_properties' not in mapper_args: + mapper_args['exclude_properties'] = exclude_properties = \ + set([c.key for c in inherited_table.c + if c not in inherited_mapper._columntoproperty]) + exclude_properties.difference_update( + [c.key for c in self.declared_columns]) + + # look through columns in the current mapper that + # are keyed to a propname different than the colname + # (if names were the same, we'd have popped it out above, + # in which case the mapper makes this combination). + # See if the superclass has a similar column property. + # If so, join them together. + for k, col in list(properties.items()): + if not isinstance(col, expression.ColumnElement): + continue + if k in inherited_mapper._props: + p = inherited_mapper._props[k] + if isinstance(p, ColumnProperty): + # note here we place the subclass column + # first. See [ticket:1892] for background. + properties[k] = [col] + p.columns + result_mapper_args = mapper_args.copy() + result_mapper_args['properties'] = properties + return result_mapper_args + + def map(self): + mapper_args = self._prepare_mapper_arguments() + self.cls.__mapper__ = self.mapper_cls( + self.cls, + self.local_table, + **mapper_args + ) + +class _DeferredMapperConfig(_MapperConfig): + _configs = util.OrderedDict() + + @property + def cls(self): + return self._cls() + + @cls.setter + def cls(self, class_): + self._cls = weakref.ref(class_, self._remove_config_cls) + self._configs[self._cls] = self + + @classmethod + def _remove_config_cls(cls, ref): + cls._configs.pop(ref, None) + + @classmethod + def has_cls(cls, class_): + # 2.6 fails on weakref if class_ is an old style class + return isinstance(class_, type) and \ + weakref.ref(class_) in cls._configs + + @classmethod + def config_for_cls(cls, class_): + return cls._configs[weakref.ref(class_)] + + + @classmethod + def classes_for_base(cls, base_cls): + return [m for m in cls._configs.values() + if issubclass(m.cls, base_cls)] + + def map(self): + self._configs.pop(self._cls, None) + super(_DeferredMapperConfig, self).map() + + +def _add_attribute(cls, key, value): + """add an attribute to an existing declarative class. + + This runs through the logic to determine MapperProperty, + adds it to the Mapper, adds a column to the mapped Table, etc. + + """ + + if '__mapper__' in cls.__dict__: + if isinstance(value, Column): + _undefer_column_name(key, value) + cls.__table__.append_column(value) + cls.__mapper__.add_property(key, value) + elif isinstance(value, ColumnProperty): + for col in value.columns: + if isinstance(col, Column) and col.table is None: + _undefer_column_name(key, col) + cls.__table__.append_column(col) + cls.__mapper__.add_property(key, value) + elif isinstance(value, MapperProperty): + cls.__mapper__.add_property( + key, + clsregistry._deferred_relationship(cls, value) + ) + elif isinstance(value, QueryableAttribute) and value.key != key: + # detect a QueryableAttribute that's already mapped being + # assigned elsewhere in userland, turn into a synonym() + value = synonym(value.key) + cls.__mapper__.add_property( + key, + clsregistry._deferred_relationship(cls, value) + ) + else: + type.__setattr__(cls, key, value) + else: + type.__setattr__(cls, key, value) + + +def _declarative_constructor(self, **kwargs): + """A simple constructor that allows initialization from kwargs. + + Sets attributes on the constructed instance using the names and + values in ``kwargs``. + + Only keys that are present as + attributes of the instance's class are allowed. These could be, + for example, any mapped columns or relationships. + """ + cls_ = type(self) + for k in kwargs: + if not hasattr(cls_, k): + raise TypeError( + "%r is an invalid keyword argument for %s" % + (k, cls_.__name__)) + setattr(self, k, kwargs[k]) +_declarative_constructor.__name__ = '__init__' + + +def _undefer_column_name(key, column): + if column.key is None: + column.key = key + if column.name is None: + column.name = key diff --git a/libs/sqlalchemy/ext/declarative/clsregistry.py b/libs/sqlalchemy/ext/declarative/clsregistry.py new file mode 100644 index 00000000..fda1cffb --- /dev/null +++ b/libs/sqlalchemy/ext/declarative/clsregistry.py @@ -0,0 +1,305 @@ +# ext/declarative/clsregistry.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Routines to handle the string class registry used by declarative. + +This system allows specification of classes and expressions used in +:func:`.relationship` using strings. + +""" +from ...orm.properties import ColumnProperty, RelationshipProperty, \ + SynonymProperty +from ...schema import _get_table_key +from ...orm import class_mapper, interfaces +from ... import util +from ... import exc +import weakref + +# strong references to registries which we place in +# the _decl_class_registry, which is usually weak referencing. +# the internal registries here link to classes with weakrefs and remove +# themselves when all references to contained classes are removed. +_registries = set() + + +def add_class(classname, cls): + """Add a class to the _decl_class_registry associated with the + given declarative class. + + """ + if classname in cls._decl_class_registry: + # class already exists. + existing = cls._decl_class_registry[classname] + if not isinstance(existing, _MultipleClassMarker): + existing = \ + cls._decl_class_registry[classname] = \ + _MultipleClassMarker([cls, existing]) + else: + cls._decl_class_registry[classname] = cls + + try: + root_module = cls._decl_class_registry['_sa_module_registry'] + except KeyError: + cls._decl_class_registry['_sa_module_registry'] = \ + root_module = _ModuleMarker('_sa_module_registry', None) + + tokens = cls.__module__.split(".") + + # build up a tree like this: + # modulename: myapp.snacks.nuts + # + # myapp->snack->nuts->(classes) + # snack->nuts->(classes) + # nuts->(classes) + # + # this allows partial token paths to be used. + while tokens: + token = tokens.pop(0) + module = root_module.get_module(token) + for token in tokens: + module = module.get_module(token) + module.add_class(classname, cls) + + +class _MultipleClassMarker(object): + """refers to multiple classes of the same name + within _decl_class_registry. + + """ + + def __init__(self, classes, on_remove=None): + self.on_remove = on_remove + self.contents = set([ + weakref.ref(item, self._remove_item) for item in classes]) + _registries.add(self) + + def __iter__(self): + return (ref() for ref in self.contents) + + def attempt_get(self, path, key): + if len(self.contents) > 1: + raise exc.InvalidRequestError( + "Multiple classes found for path \"%s\" " + "in the registry of this declarative " + "base. Please use a fully module-qualified path." % + (".".join(path + [key])) + ) + else: + ref = list(self.contents)[0] + cls = ref() + if cls is None: + raise NameError(key) + return cls + + def _remove_item(self, ref): + self.contents.remove(ref) + if not self.contents: + _registries.discard(self) + if self.on_remove: + self.on_remove() + + def add_item(self, item): + modules = set([cls().__module__ for cls in self.contents]) + if item.__module__ in modules: + util.warn( + "This declarative base already contains a class with the " + "same class name and module name as %s.%s, and will " + "be replaced in the string-lookup table." % ( + item.__module__, + item.__name__ + ) + ) + self.contents.add(weakref.ref(item, self._remove_item)) + + +class _ModuleMarker(object): + """"refers to a module name within + _decl_class_registry. + + """ + def __init__(self, name, parent): + self.parent = parent + self.name = name + self.contents = {} + self.mod_ns = _ModNS(self) + if self.parent: + self.path = self.parent.path + [self.name] + else: + self.path = [] + _registries.add(self) + + def __contains__(self, name): + return name in self.contents + + def __getitem__(self, name): + return self.contents[name] + + def _remove_item(self, name): + self.contents.pop(name, None) + if not self.contents and self.parent is not None: + self.parent._remove_item(self.name) + _registries.discard(self) + + def resolve_attr(self, key): + return getattr(self.mod_ns, key) + + def get_module(self, name): + if name not in self.contents: + marker = _ModuleMarker(name, self) + self.contents[name] = marker + else: + marker = self.contents[name] + return marker + + def add_class(self, name, cls): + if name in self.contents: + existing = self.contents[name] + existing.add_item(cls) + else: + existing = self.contents[name] = \ + _MultipleClassMarker([cls], + on_remove=lambda: self._remove_item(name)) + + +class _ModNS(object): + def __init__(self, parent): + self.__parent = parent + + def __getattr__(self, key): + try: + value = self.__parent.contents[key] + except KeyError: + pass + else: + if value is not None: + if isinstance(value, _ModuleMarker): + return value.mod_ns + else: + assert isinstance(value, _MultipleClassMarker) + return value.attempt_get(self.__parent.path, key) + raise AttributeError("Module %r has no mapped classes " + "registered under the name %r" % (self.__parent.name, key)) + + +class _GetColumns(object): + def __init__(self, cls): + self.cls = cls + + def __getattr__(self, key): + mp = class_mapper(self.cls, configure=False) + if mp: + if key not in mp.all_orm_descriptors: + raise exc.InvalidRequestError( + "Class %r does not have a mapped column named %r" + % (self.cls, key)) + + desc = mp.all_orm_descriptors[key] + if desc.extension_type is interfaces.NOT_EXTENSION: + prop = desc.property + if isinstance(prop, SynonymProperty): + key = prop.name + elif not isinstance(prop, ColumnProperty): + raise exc.InvalidRequestError( + "Property %r is not an instance of" + " ColumnProperty (i.e. does not correspond" + " directly to a Column)." % key) + return getattr(self.cls, key) + + +class _GetTable(object): + def __init__(self, key, metadata): + self.key = key + self.metadata = metadata + + def __getattr__(self, key): + return self.metadata.tables[ + _get_table_key(key, self.key) + ] + + +def _determine_container(key, value): + if isinstance(value, _MultipleClassMarker): + value = value.attempt_get([], key) + return _GetColumns(value) + + +class _class_resolver(object): + def __init__(self, cls, prop, fallback, arg): + self.cls = cls + self.prop = prop + self.arg = self._declarative_arg = arg + self.fallback = fallback + self._dict = util.PopulateDict(self._access_cls) + self._resolvers = () + + def _access_cls(self, key): + cls = self.cls + if key in cls._decl_class_registry: + return _determine_container(key, cls._decl_class_registry[key]) + elif key in cls.metadata.tables: + return cls.metadata.tables[key] + elif key in cls.metadata._schemas: + return _GetTable(key, cls.metadata) + elif '_sa_module_registry' in cls._decl_class_registry and \ + key in cls._decl_class_registry['_sa_module_registry']: + registry = cls._decl_class_registry['_sa_module_registry'] + return registry.resolve_attr(key) + elif self._resolvers: + for resolv in self._resolvers: + value = resolv(key) + if value is not None: + return value + + return self.fallback[key] + + def __call__(self): + try: + x = eval(self.arg, globals(), self._dict) + + if isinstance(x, _GetColumns): + return x.cls + else: + return x + except NameError as n: + raise exc.InvalidRequestError( + "When initializing mapper %s, expression %r failed to " + "locate a name (%r). If this is a class name, consider " + "adding this relationship() to the %r class after " + "both dependent classes have been defined." % + (self.prop.parent, self.arg, n.args[0], self.cls) + ) + + +def _resolver(cls, prop): + import sqlalchemy + from sqlalchemy.orm import foreign, remote + + fallback = sqlalchemy.__dict__.copy() + fallback.update({'foreign': foreign, 'remote': remote}) + + def resolve_arg(arg): + return _class_resolver(cls, prop, fallback, arg) + return resolve_arg + + +def _deferred_relationship(cls, prop): + + if isinstance(prop, RelationshipProperty): + resolve_arg = _resolver(cls, prop) + + for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', + 'secondary', '_user_defined_foreign_keys', 'remote_side'): + v = getattr(prop, attr) + if isinstance(v, util.string_types): + setattr(prop, attr, resolve_arg(v)) + + if prop.backref and isinstance(prop.backref, tuple): + key, kwargs = prop.backref + for attr in ('primaryjoin', 'secondaryjoin', 'secondary', + 'foreign_keys', 'remote_side', 'order_by'): + if attr in kwargs and isinstance(kwargs[attr], str): + kwargs[attr] = resolve_arg(kwargs[attr]) + + return prop diff --git a/libs/sqlalchemy/ext/horizontal_shard.py b/libs/sqlalchemy/ext/horizontal_shard.py index 05b45e03..8b3f968d 100644 --- a/libs/sqlalchemy/ext/horizontal_shard.py +++ b/libs/sqlalchemy/ext/horizontal_shard.py @@ -1,5 +1,5 @@ # ext/horizontal_shard.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -14,13 +14,13 @@ the source distribution. """ -from sqlalchemy import exc as sa_exc -from sqlalchemy import util -from sqlalchemy.orm.session import Session -from sqlalchemy.orm.query import Query +from .. import util +from ..orm.session import Session +from ..orm.query import Query __all__ = ['ShardedSession', 'ShardedQuery'] + class ShardedQuery(Query): def __init__(self, *args, **kwargs): super(ShardedQuery, self).__init__(*args, **kwargs) @@ -72,28 +72,29 @@ class ShardedQuery(Query): else: return None + class ShardedSession(Session): def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None, query_cls=ShardedQuery, **kwargs): """Construct a ShardedSession. - :param shard_chooser: A callable which, passed a Mapper, a mapped instance, and possibly a - SQL clause, returns a shard ID. This id may be based off of the - attributes present within the object, or on some round-robin - scheme. If the scheme is based on a selection, it should set - whatever state on the instance to mark it in the future as + :param shard_chooser: A callable which, passed a Mapper, a mapped + instance, and possibly a SQL clause, returns a shard ID. This id + may be based off of the attributes present within the object, or on + some round-robin scheme. If the scheme is based on a selection, it + should set whatever state on the instance to mark it in the future as participating in that shard. - :param id_chooser: A callable, passed a query and a tuple of identity values, which - should return a list of shard ids where the ID might reside. The - databases will be queried in the order of this listing. + :param id_chooser: A callable, passed a query and a tuple of identity + values, which should return a list of shard ids where the ID might + reside. The databases will be queried in the order of this listing. - :param query_chooser: For a given Query, returns the list of shard_ids where the query - should be issued. Results from all shards returned will be combined - together into a single listing. + :param query_chooser: For a given Query, returns the list of shard_ids + where the query should be issued. Results from all shards returned + will be combined together into a single listing. - :param shards: A dictionary of string shard names to :class:`~sqlalchemy.engine.base.Engine` - objects. + :param shards: A dictionary of string shard names + to :class:`~sqlalchemy.engine.Engine` objects. """ super(ShardedSession, self).__init__(query_cls=query_cls, **kwargs) @@ -117,12 +118,11 @@ class ShardedSession(Session): shard_id=shard_id, instance=instance).contextual_connect(**kwargs) - def get_bind(self, mapper, shard_id=None, instance=None, clause=None, **kw): + def get_bind(self, mapper, shard_id=None, + instance=None, clause=None, **kw): if shard_id is None: shard_id = self.shard_chooser(mapper, instance, clause=clause) return self.__binds[shard_id] def bind_shard(self, shard_id, bind): self.__binds[shard_id] = bind - - diff --git a/libs/sqlalchemy/ext/hybrid.py b/libs/sqlalchemy/ext/hybrid.py index 038898e4..576e0bd4 100644 --- a/libs/sqlalchemy/ext/hybrid.py +++ b/libs/sqlalchemy/ext/hybrid.py @@ -1,5 +1,5 @@ # ext/hybrid.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -9,10 +9,10 @@ "hybrid" means the attribute has distinct behaviors defined at the class level and at the instance level. -The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of method -decorator, is around 50 lines of code and has almost no dependencies on the rest -of SQLAlchemy. It can, in theory, work with any descriptor-based expression -system. +The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of +method decorator, is around 50 lines of code and has almost no +dependencies on the rest of SQLAlchemy. It can, in theory, work with +any descriptor-based expression system. Consider a mapping ``Interval``, representing integer ``start`` and ``end`` values. We can define higher level functions on mapped classes that produce @@ -51,9 +51,10 @@ as the class itself:: def intersects(self, other): return self.contains(other.start) | self.contains(other.end) -Above, the ``length`` property returns the difference between the ``end`` and -``start`` attributes. With an instance of ``Interval``, this subtraction occurs -in Python, using normal Python descriptor mechanics:: +Above, the ``length`` property returns the difference between the +``end`` and ``start`` attributes. With an instance of ``Interval``, +this subtraction occurs in Python, using normal Python descriptor +mechanics:: >>> i1 = Interval(5, 10) >>> i1.length @@ -82,11 +83,12 @@ locate attributes, so can also be used with hybrid attributes:: FROM interval WHERE interval."end" - interval.start = :param_1 -The ``Interval`` class example also illustrates two methods, ``contains()`` and ``intersects()``, -decorated with :class:`.hybrid_method`. -This decorator applies the same idea to methods that :class:`.hybrid_property` applies -to attributes. The methods return boolean values, and take advantage -of the Python ``|`` and ``&`` bitwise operators to produce equivalent instance-level and +The ``Interval`` class example also illustrates two methods, +``contains()`` and ``intersects()``, decorated with +:class:`.hybrid_method`. This decorator applies the same idea to +methods that :class:`.hybrid_property` applies to attributes. The +methods return boolean values, and take advantage of the Python ``|`` +and ``&`` bitwise operators to produce equivalent instance-level and SQL expression-level boolean behavior:: >>> i1.contains(6) @@ -118,12 +120,15 @@ SQL expression-level boolean behavior:: Defining Expression Behavior Distinct from Attribute Behavior -------------------------------------------------------------- -Our usage of the ``&`` and ``|`` bitwise operators above was fortunate, considering -our functions operated on two boolean values to return a new one. In many cases, the construction -of an in-Python function and a SQLAlchemy SQL expression have enough differences that two -separate Python expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorators -define the :meth:`.hybrid_property.expression` modifier for this purpose. As an example we'll -define the radius of the interval, which requires the usage of the absolute value function:: +Our usage of the ``&`` and ``|`` bitwise operators above was +fortunate, considering our functions operated on two boolean values to +return a new one. In many cases, the construction of an in-Python +function and a SQLAlchemy SQL expression have enough differences that +two separate Python expressions should be defined. The +:mod:`~sqlalchemy.ext.hybrid` decorators define the +:meth:`.hybrid_property.expression` modifier for this purpose. As an +example we'll define the radius of the interval, which requires the +usage of the absolute value function:: from sqlalchemy import func @@ -138,8 +143,9 @@ define the radius of the interval, which requires the usage of the absolute valu def radius(cls): return func.abs(cls.length) / 2 -Above the Python function ``abs()`` is used for instance-level operations, the SQL function -``ABS()`` is used via the :attr:`.func` object for class-level expressions:: +Above the Python function ``abs()`` is used for instance-level +operations, the SQL function ``ABS()`` is used via the :attr:`.func` +object for class-level expressions:: >>> i1.radius 2 @@ -153,8 +159,8 @@ Above the Python function ``abs()`` is used for instance-level operations, the S Defining Setters ---------------- -Hybrid properties can also define setter methods. If we wanted ``length`` above, when -set, to modify the endpoint value:: +Hybrid properties can also define setter methods. If we wanted +``length`` above, when set, to modify the endpoint value:: class Interval(object): # ... @@ -223,7 +229,7 @@ mapping which relates a ``User`` to a ``SavingsAccount``:: account = Account(owner=self) else: account = self.accounts[0] - account.balance = balance + account.balance = value @balance.expression def balance(cls): @@ -234,8 +240,8 @@ The above hybrid property ``balance`` works with the first in-Python getter/setter methods can treat ``accounts`` as a Python list available on ``self``. -However, at the expression level, it's expected that the ``User`` class will be used -in an appropriate context such that an appropriate join to +However, at the expression level, it's expected that the ``User`` class will +be used in an appropriate context such that an appropriate join to ``SavingsAccount`` will be present:: >>> print Session().query(User, User.balance).\\ @@ -262,11 +268,10 @@ Correlated Subquery Relationship Hybrid ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We can, of course, forego being dependent on the enclosing query's usage -of joins in favor of the correlated -subquery, which can portably be packed into a single colunn expression. -A correlated subquery is more portable, but often performs more poorly -at the SQL level. -Using the same technique illustrated at :ref:`mapper_column_property_sql_expressions`, +of joins in favor of the correlated subquery, which can portably be packed +into a single column expression. A correlated subquery is more portable, but +often performs more poorly at the SQL level. Using the same technique +illustrated at :ref:`mapper_column_property_sql_expressions`, we can adjust our ``SavingsAccount`` example to aggregate the balances for *all* accounts, and use a correlated subquery for the column expression:: @@ -316,10 +321,11 @@ a correlated SELECT:: Building Custom Comparators --------------------------- -The hybrid property also includes a helper that allows construction of custom comparators. -A comparator object allows one to customize the behavior of each SQLAlchemy expression -operator individually. They are useful when creating custom types that have -some highly idiosyncratic behavior on the SQL side. +The hybrid property also includes a helper that allows construction of +custom comparators. A comparator object allows one to customize the +behavior of each SQLAlchemy expression operator individually. They +are useful when creating custom types that have some highly +idiosyncratic behavior on the SQL side. The example class below allows case-insensitive comparisons on the attribute named ``word_insensitive``:: @@ -356,9 +362,10 @@ SQL function to both sides:: FROM searchword WHERE lower(searchword.word) = lower(:lower_1) -The ``CaseInsensitiveComparator`` above implements part of the :class:`.ColumnOperators` -interface. A "coercion" operation like lowercasing can be applied to all comparison operations -(i.e. ``eq``, ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`:: +The ``CaseInsensitiveComparator`` above implements part of the +:class:`.ColumnOperators` interface. A "coercion" operation like +lowercasing can be applied to all comparison operations (i.e. ``eq``, +``lt``, ``gt``, etc.) using :meth:`.Operators.operate`:: class CaseInsensitiveComparator(Comparator): def operate(self, op, other): @@ -367,17 +374,20 @@ interface. A "coercion" operation like lowercasing can be applied to all compa Hybrid Value Objects -------------------- -Note in our previous example, if we were to compare the ``word_insensitive`` attribute of -a ``SearchWord`` instance to a plain Python string, the plain Python string would not -be coerced to lower case - the ``CaseInsensitiveComparator`` we built, being returned -by ``@word_insensitive.comparator``, only applies to the SQL side. +Note in our previous example, if we were to compare the +``word_insensitive`` attribute of a ``SearchWord`` instance to a plain +Python string, the plain Python string would not be coerced to lower +case - the ``CaseInsensitiveComparator`` we built, being returned by +``@word_insensitive.comparator``, only applies to the SQL side. -A more comprehensive form of the custom comparator is to construct a *Hybrid Value Object*. -This technique applies the target value or expression to a value object which is then -returned by the accessor in all cases. The value object allows control -of all operations upon the value as well as how compared values are treated, both -on the SQL expression side as well as the Python value side. Replacing the -previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord`` class:: +A more comprehensive form of the custom comparator is to construct a +*Hybrid Value Object*. This technique applies the target value or +expression to a value object which is then returned by the accessor in +all cases. The value object allows control of all operations upon +the value as well as how compared values are treated, both on the SQL +expression side as well as the Python value side. Replacing the +previous ``CaseInsensitiveComparator`` class with a new +``CaseInsensitiveWord`` class:: class CaseInsensitiveWord(Comparator): "Hybrid value representing a lower case representation of a word." @@ -404,12 +414,13 @@ previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord`` key = 'word' "Label to apply to Query tuple results" -Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may be a SQL function, -or may be a Python native. By overriding ``operate()`` and ``__clause_element__()`` -to work in terms of ``self.word``, all comparison operations will work against the +Above, the ``CaseInsensitiveWord`` object represents ``self.word``, +which may be a SQL function, or may be a Python native. By +overriding ``operate()`` and ``__clause_element__()`` to work in terms +of ``self.word``, all comparison operations will work against the "converted" form of ``word``, whether it be SQL side or Python side. -Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` object unconditionally -from a single hybrid call:: +Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` +object unconditionally from a single hybrid call:: class SearchWord(Base): __tablename__ = 'searchword' @@ -420,9 +431,10 @@ from a single hybrid call:: def word_insensitive(self): return CaseInsensitiveWord(self.word) -The ``word_insensitive`` attribute now has case-insensitive comparison behavior -universally, including SQL expression vs. Python expression (note the Python value is -converted to lower case on the Python side here):: +The ``word_insensitive`` attribute now has case-insensitive comparison +behavior universally, including SQL expression vs. Python expression +(note the Python value is converted to lower case on the Python side +here):: >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks") SELECT searchword.id AS searchword_id, searchword.word AS searchword_word @@ -439,7 +451,8 @@ SQL expression versus SQL expression:: ... filter( ... sw1.word_insensitive > sw2.word_insensitive ... ) - SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2 + SELECT lower(searchword_1.word) AS lower_1, + lower(searchword_2.word) AS lower_2 FROM searchword AS searchword_1, searchword AS searchword_2 WHERE lower(searchword_1.word) > lower(searchword_2.word) @@ -453,30 +466,36 @@ Python only expression:: >>> print ws1.word_insensitive someword -The Hybrid Value pattern is very useful for any kind of value that may have multiple representations, -such as timestamps, time deltas, units of measurement, currencies and encrypted passwords. +The Hybrid Value pattern is very useful for any kind of value that may +have multiple representations, such as timestamps, time deltas, units +of measurement, currencies and encrypted passwords. -See Also: +.. seealso:: -`Hybrids and Value Agnostic Types `_ - on the techspot.zzzeek.org blog + `Hybrids and Value Agnostic Types + `_ - + on the techspot.zzzeek.org blog -`Value Agnostic Types, Part II `_ - on the techspot.zzzeek.org blog + `Value Agnostic Types, Part II + `_ - + on the techspot.zzzeek.org blog .. _hybrid_transformers: Building Transformers ---------------------- -A *transformer* is an object which can receive a :class:`.Query` object and return a -new one. The :class:`.Query` object includes a method :meth:`.with_transformation` -that simply returns a new :class:`.Query` transformed by the given function. +A *transformer* is an object which can receive a :class:`.Query` +object and return a new one. The :class:`.Query` object includes a +method :meth:`.with_transformation` that returns a new :class:`.Query` +transformed by the given function. We can combine this with the :class:`.Comparator` class to produce one type of recipe which can both set up the FROM clause of a query as well as assign filtering criterion. -Consider a mapped class ``Node``, which assembles using adjacency list into a hierarchical -tree pattern:: +Consider a mapped class ``Node``, which assembles using adjacency list +into a hierarchical tree pattern:: from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship @@ -489,8 +508,9 @@ tree pattern:: parent_id = Column(Integer, ForeignKey('node.id')) parent = relationship("Node", remote_side=id) -Suppose we wanted to add an accessor ``grandparent``. This would return the ``parent`` of -``Node.parent``. When we have an instance of ``Node``, this is simple:: +Suppose we wanted to add an accessor ``grandparent``. This would +return the ``parent`` of ``Node.parent``. When we have an instance of +``Node``, this is simple:: from sqlalchemy.ext.hybrid import hybrid_property @@ -501,11 +521,13 @@ Suppose we wanted to add an accessor ``grandparent``. This would return the ``p def grandparent(self): return self.parent.parent -For the expression, things are not so clear. We'd need to construct a :class:`.Query` where we -:meth:`~.Query.join` twice along ``Node.parent`` to get to the ``grandparent``. We can instead -return a transforming callable that we'll combine with the :class:`.Comparator` class -to receive any :class:`.Query` object, and return a new one that's joined to the ``Node.parent`` -attribute and filtered based on the given criterion:: +For the expression, things are not so clear. We'd need to construct +a :class:`.Query` where we :meth:`~.Query.join` twice along +``Node.parent`` to get to the ``grandparent``. We can instead return +a transforming callable that we'll combine with the +:class:`.Comparator` class to receive any :class:`.Query` object, and +return a new one that's joined to the ``Node.parent`` attribute and +filtered based on the given criterion:: from sqlalchemy.ext.hybrid import Comparator @@ -534,15 +556,17 @@ attribute and filtered based on the given criterion:: def grandparent(cls): return GrandparentTransformer(cls) -The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate` method -at the base of the :class:`.Comparator` hierarchy to return a query-transforming -callable, which then runs the given comparison operation in a particular context. -Such as, in the example above, the ``operate`` method is called, given the -:attr:`.Operators.eq` callable as well as the right side of the comparison -``Node(id=5)``. A function ``transform`` is then returned which will transform -a :class:`.Query` first to join to ``Node.parent``, then to compare ``parent_alias`` -using :attr:`.Operators.eq` against the left and right sides, passing into -:class:`.Query.filter`: +The ``GrandparentTransformer`` overrides the core +:meth:`.Operators.operate` method at the base of the +:class:`.Comparator` hierarchy to return a query-transforming +callable, which then runs the given comparison operation in a +particular context. Such as, in the example above, the ``operate`` +method is called, given the :attr:`.Operators.eq` callable as well as +the right side of the comparison ``Node(id=5)``. A function +``transform`` is then returned which will transform a :class:`.Query` +first to join to ``Node.parent``, then to compare ``parent_alias`` +using :attr:`.Operators.eq` against the left and right sides, passing +into :class:`.Query.filter`: .. sourcecode:: pycon+sql @@ -605,15 +629,43 @@ While it's only recommended for advanced and/or patient developers, there's probably a whole lot of amazing things it can be used for. """ -from sqlalchemy import util -from sqlalchemy.orm import attributes, interfaces +from .. import util +from ..orm import attributes, interfaces -class hybrid_method(object): +HYBRID_METHOD = util.symbol('HYBRID_METHOD') +"""Symbol indicating an :class:`_InspectionAttr` that's + of type :class:`.hybrid_method`. + + Is assigned to the :attr:`._InspectionAttr.extension_type` + attibute. + + .. seealso:: + + :attr:`.Mapper.all_orm_attributes` + +""" + +HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY') +"""Symbol indicating an :class:`_InspectionAttr` that's + of type :class:`.hybrid_method`. + + Is assigned to the :attr:`._InspectionAttr.extension_type` + attibute. + + .. seealso:: + + :attr:`.Mapper.all_orm_attributes` + +""" + +class hybrid_method(interfaces._InspectionAttr): """A decorator which allows definition of a Python object method with both instance-level and class-level behavior. """ + is_attribute = True + extension_type = HYBRID_METHOD def __init__(self, func, expr=None): """Create a new :class:`.hybrid_method`. @@ -642,17 +694,22 @@ class hybrid_method(object): return self.func.__get__(instance, owner) def expression(self, expr): - """Provide a modifying decorator that defines a SQL-expression producing method.""" + """Provide a modifying decorator that defines a + SQL-expression producing method.""" self.expr = expr return self -class hybrid_property(object): + +class hybrid_property(interfaces._InspectionAttr): """A decorator which allows definition of a Python descriptor with both instance-level and class-level behavior. """ + is_attribute = True + extension_type = HYBRID_PROPERTY + def __init__(self, fget, fset=None, fdel=None, expr=None): """Create a new :class:`.hybrid_property`. @@ -699,19 +756,22 @@ class hybrid_property(object): return self def deleter(self, fdel): - """Provide a modifying decorator that defines a value-deletion method.""" + """Provide a modifying decorator that defines a + value-deletion method.""" self.fdel = fdel return self def expression(self, expr): - """Provide a modifying decorator that defines a SQL-expression producing method.""" + """Provide a modifying decorator that defines a SQL-expression + producing method.""" self.expr = expr return self def comparator(self, comparator): - """Provide a modifying decorator that defines a custom comparator producing method. + """Provide a modifying decorator that defines a custom + comparator producing method. The return value of the decorated method should be an instance of :class:`~.hybrid.Comparator`. @@ -720,6 +780,7 @@ class hybrid_property(object): proxy_attr = attributes.\ create_proxied_attribute(self) + def expr(owner): return proxy_attr(owner, self.__name__, self, comparator(owner)) self.expr = expr @@ -727,9 +788,11 @@ class hybrid_property(object): class Comparator(interfaces.PropComparator): - """A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator` + """A helper class that allows easy construction of custom + :class:`~.orm.interfaces.PropComparator` classes for usage with hybrids.""" + property = None def __init__(self, expression): self.expression = expression @@ -740,8 +803,6 @@ class Comparator(interfaces.PropComparator): expr = expr.__clause_element__() return expr - def adapted(self, adapter): + def adapt_to_entity(self, adapt_to_entity): # interesting.... return self - - diff --git a/libs/sqlalchemy/ext/instrumentation.py b/libs/sqlalchemy/ext/instrumentation.py new file mode 100644 index 00000000..2cf36e9b --- /dev/null +++ b/libs/sqlalchemy/ext/instrumentation.py @@ -0,0 +1,407 @@ +"""Extensible class instrumentation. + +The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate +systems of class instrumentation within the ORM. Class instrumentation +refers to how the ORM places attributes on the class which maintain +data and track changes to that data, as well as event hooks installed +on the class. + +.. note:: + The extension package is provided for the benefit of integration + with other object management packages, which already perform + their own instrumentation. It is not intended for general use. + +For examples of how the instrumentation extension is used, +see the example :ref:`examples_instrumentation`. + +.. versionchanged:: 0.8 + The :mod:`sqlalchemy.orm.instrumentation` was split out so + that all functionality having to do with non-standard + instrumentation was moved out to :mod:`sqlalchemy.ext.instrumentation`. + When imported, the module installs itself within + :mod:`sqlalchemy.orm.instrumentation` so that it + takes effect, including recognition of + ``__sa_instrumentation_manager__`` on mapped classes, as + well :data:`.instrumentation_finders` + being used to determine class instrumentation resolution. + +""" +from ..orm import instrumentation as orm_instrumentation +from ..orm.instrumentation import ( + ClassManager, InstrumentationFactory, _default_state_getter, + _default_dict_getter, _default_manager_getter +) +from ..orm import attributes, collections, base as orm_base +from .. import util +from ..orm import exc as orm_exc +import weakref + +INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__' +"""Attribute, elects custom instrumentation when present on a mapped class. + +Allows a class to specify a slightly or wildly different technique for +tracking changes made to mapped attributes and collections. + +Only one instrumentation implementation is allowed in a given object +inheritance hierarchy. + +The value of this attribute must be a callable and will be passed a class +object. The callable must return one of: + + - An instance of an InstrumentationManager or subclass + - An object implementing all or some of InstrumentationManager (TODO) + - A dictionary of callables, implementing all or some of the above (TODO) + - An instance of a ClassManager or subclass + +This attribute is consulted by SQLAlchemy instrumentation +resolution, once the :mod:`sqlalchemy.ext.instrumentation` module +has been imported. If custom finders are installed in the global +instrumentation_finders list, they may or may not choose to honor this +attribute. + +""" + + +def find_native_user_instrumentation_hook(cls): + """Find user-specified instrumentation management for a class.""" + return getattr(cls, INSTRUMENTATION_MANAGER, None) + +instrumentation_finders = [find_native_user_instrumentation_hook] +"""An extensible sequence of callables which return instrumentation +implementations + +When a class is registered, each callable will be passed a class object. +If None is returned, the +next finder in the sequence is consulted. Otherwise the return must be an +instrumentation factory that follows the same guidelines as +sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER. + +By default, the only finder is find_native_user_instrumentation_hook, which +searches for INSTRUMENTATION_MANAGER. If all finders return None, standard +ClassManager instrumentation is used. + +""" + + +class ExtendedInstrumentationRegistry(InstrumentationFactory): + """Extends :class:`.InstrumentationFactory` with additional + bookkeeping, to accommodate multiple types of + class managers. + + """ + _manager_finders = weakref.WeakKeyDictionary() + _state_finders = weakref.WeakKeyDictionary() + _dict_finders = weakref.WeakKeyDictionary() + _extended = False + + def _locate_extended_factory(self, class_): + for finder in instrumentation_finders: + factory = finder(class_) + if factory is not None: + manager = self._extended_class_manager(class_, factory) + return manager, factory + else: + return None, None + + def _check_conflicts(self, class_, factory): + existing_factories = self._collect_management_factories_for(class_).\ + difference([factory]) + if existing_factories: + raise TypeError( + "multiple instrumentation implementations specified " + "in %s inheritance hierarchy: %r" % ( + class_.__name__, list(existing_factories))) + + def _extended_class_manager(self, class_, factory): + manager = factory(class_) + if not isinstance(manager, ClassManager): + manager = _ClassInstrumentationAdapter(class_, manager) + + if factory != ClassManager and not self._extended: + # somebody invoked a custom ClassManager. + # reinstall global "getter" functions with the more + # expensive ones. + self._extended = True + _install_instrumented_lookups() + + self._manager_finders[class_] = manager.manager_getter() + self._state_finders[class_] = manager.state_getter() + self._dict_finders[class_] = manager.dict_getter() + return manager + + def _collect_management_factories_for(self, cls): + """Return a collection of factories in play or specified for a + hierarchy. + + Traverses the entire inheritance graph of a cls and returns a + collection of instrumentation factories for those classes. Factories + are extracted from active ClassManagers, if available, otherwise + instrumentation_finders is consulted. + + """ + hierarchy = util.class_hierarchy(cls) + factories = set() + for member in hierarchy: + manager = self.manager_of_class(member) + if manager is not None: + factories.add(manager.factory) + else: + for finder in instrumentation_finders: + factory = finder(member) + if factory is not None: + break + else: + factory = None + factories.add(factory) + factories.discard(None) + return factories + + def unregister(self, class_): + if class_ in self._manager_finders: + del self._manager_finders[class_] + del self._state_finders[class_] + del self._dict_finders[class_] + super(ExtendedInstrumentationRegistry, self).unregister(class_) + + def manager_of_class(self, cls): + if cls is None: + return None + return self._manager_finders.get(cls, _default_manager_getter)(cls) + + def state_of(self, instance): + if instance is None: + raise AttributeError("None has no persistent state.") + return self._state_finders.get( + instance.__class__, _default_state_getter)(instance) + + def dict_of(self, instance): + if instance is None: + raise AttributeError("None has no persistent state.") + return self._dict_finders.get( + instance.__class__, _default_dict_getter)(instance) + + +orm_instrumentation._instrumentation_factory = \ + _instrumentation_factory = ExtendedInstrumentationRegistry() +orm_instrumentation.instrumentation_finders = instrumentation_finders + + +class InstrumentationManager(object): + """User-defined class instrumentation extension. + + :class:`.InstrumentationManager` can be subclassed in order + to change + how class instrumentation proceeds. This class exists for + the purposes of integration with other object management + frameworks which would like to entirely modify the + instrumentation methodology of the ORM, and is not intended + for regular usage. For interception of class instrumentation + events, see :class:`.InstrumentationEvents`. + + The API for this class should be considered as semi-stable, + and may change slightly with new releases. + + .. versionchanged:: 0.8 + :class:`.InstrumentationManager` was moved from + :mod:`sqlalchemy.orm.instrumentation` to + :mod:`sqlalchemy.ext.instrumentation`. + + """ + + # r4361 added a mandatory (cls) constructor to this interface. + # given that, perhaps class_ should be dropped from all of these + # signatures. + + def __init__(self, class_): + pass + + def manage(self, class_, manager): + setattr(class_, '_default_class_manager', manager) + + def dispose(self, class_, manager): + delattr(class_, '_default_class_manager') + + def manager_getter(self, class_): + def get(cls): + return cls._default_class_manager + return get + + def instrument_attribute(self, class_, key, inst): + pass + + def post_configure_attribute(self, class_, key, inst): + pass + + def install_descriptor(self, class_, key, inst): + setattr(class_, key, inst) + + def uninstall_descriptor(self, class_, key): + delattr(class_, key) + + def install_member(self, class_, key, implementation): + setattr(class_, key, implementation) + + def uninstall_member(self, class_, key): + delattr(class_, key) + + def instrument_collection_class(self, class_, key, collection_class): + return collections.prepare_instrumentation(collection_class) + + def get_instance_dict(self, class_, instance): + return instance.__dict__ + + def initialize_instance_dict(self, class_, instance): + pass + + def install_state(self, class_, instance, state): + setattr(instance, '_default_state', state) + + def remove_state(self, class_, instance): + delattr(instance, '_default_state') + + def state_getter(self, class_): + return lambda instance: getattr(instance, '_default_state') + + def dict_getter(self, class_): + return lambda inst: self.get_instance_dict(class_, inst) + + +class _ClassInstrumentationAdapter(ClassManager): + """Adapts a user-defined InstrumentationManager to a ClassManager.""" + + def __init__(self, class_, override): + self._adapted = override + self._get_state = self._adapted.state_getter(class_) + self._get_dict = self._adapted.dict_getter(class_) + + ClassManager.__init__(self, class_) + + def manage(self): + self._adapted.manage(self.class_, self) + + def dispose(self): + self._adapted.dispose(self.class_) + + def manager_getter(self): + return self._adapted.manager_getter(self.class_) + + def instrument_attribute(self, key, inst, propagated=False): + ClassManager.instrument_attribute(self, key, inst, propagated) + if not propagated: + self._adapted.instrument_attribute(self.class_, key, inst) + + def post_configure_attribute(self, key): + super(_ClassInstrumentationAdapter, self).post_configure_attribute(key) + self._adapted.post_configure_attribute(self.class_, key, self[key]) + + def install_descriptor(self, key, inst): + self._adapted.install_descriptor(self.class_, key, inst) + + def uninstall_descriptor(self, key): + self._adapted.uninstall_descriptor(self.class_, key) + + def install_member(self, key, implementation): + self._adapted.install_member(self.class_, key, implementation) + + def uninstall_member(self, key): + self._adapted.uninstall_member(self.class_, key) + + def instrument_collection_class(self, key, collection_class): + return self._adapted.instrument_collection_class( + self.class_, key, collection_class) + + def initialize_collection(self, key, state, factory): + delegate = getattr(self._adapted, 'initialize_collection', None) + if delegate: + return delegate(key, state, factory) + else: + return ClassManager.initialize_collection(self, key, + state, factory) + + def new_instance(self, state=None): + instance = self.class_.__new__(self.class_) + self.setup_instance(instance, state) + return instance + + def _new_state_if_none(self, instance): + """Install a default InstanceState if none is present. + + A private convenience method used by the __init__ decorator. + """ + if self.has_state(instance): + return False + else: + return self.setup_instance(instance) + + def setup_instance(self, instance, state=None): + self._adapted.initialize_instance_dict(self.class_, instance) + + if state is None: + state = self._state_constructor(instance, self) + + # the given instance is assumed to have no state + self._adapted.install_state(self.class_, instance, state) + return state + + def teardown_instance(self, instance): + self._adapted.remove_state(self.class_, instance) + + def has_state(self, instance): + try: + self._get_state(instance) + except orm_exc.NO_STATE: + return False + else: + return True + + def state_getter(self): + return self._get_state + + def dict_getter(self): + return self._get_dict + + +def _install_instrumented_lookups(): + """Replace global class/object management functions + with ExtendedInstrumentationRegistry implementations, which + allow multiple types of class managers to be present, + at the cost of performance. + + This function is called only by ExtendedInstrumentationRegistry + and unit tests specific to this behavior. + + The _reinstall_default_lookups() function can be called + after this one to re-establish the default functions. + + """ + _install_lookups( + dict( + instance_state=_instrumentation_factory.state_of, + instance_dict=_instrumentation_factory.dict_of, + manager_of_class=_instrumentation_factory.manager_of_class + ) + ) + + +def _reinstall_default_lookups(): + """Restore simplified lookups.""" + _install_lookups( + dict( + instance_state=_default_state_getter, + instance_dict=_default_dict_getter, + manager_of_class=_default_manager_getter + ) + ) + + +def _install_lookups(lookups): + global instance_state, instance_dict, manager_of_class + instance_state = lookups['instance_state'] + instance_dict = lookups['instance_dict'] + manager_of_class = lookups['manager_of_class'] + orm_base.instance_state = attributes.instance_state = \ + orm_instrumentation.instance_state = instance_state + orm_base.instance_dict = attributes.instance_dict = \ + orm_instrumentation.instance_dict = instance_dict + orm_base.manager_of_class = attributes.manager_of_class = \ + orm_instrumentation.manager_of_class = manager_of_class diff --git a/libs/sqlalchemy/ext/mutable.py b/libs/sqlalchemy/ext/mutable.py index 36e8fcaf..82410031 100644 --- a/libs/sqlalchemy/ext/mutable.py +++ b/libs/sqlalchemy/ext/mutable.py @@ -1,5 +1,5 @@ # ext/mutable.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,13 +7,9 @@ """Provide support for tracking of in-place changes to scalar values, which are propagated into ORM change events on owning parent objects. -The :mod:`sqlalchemy.ext.mutable` extension replaces SQLAlchemy's legacy approach to in-place -mutations of scalar values, established by the :class:`.types.MutableType` -class as well as the ``mutable=True`` type flag, with a system that allows -change events to be propagated from the value to the owning parent, thereby -removing the need for the ORM to maintain copies of values as well as the very -expensive requirement of scanning through all "mutable" values on each flush -call, looking for changes. +.. versionadded:: 0.7 :mod:`sqlalchemy.ext.mutable` replaces SQLAlchemy's + legacy approach to in-place mutations of scalar values; see + :ref:`07_migration_mutation_extension`. .. _mutable_scalars: @@ -43,27 +39,27 @@ JSON strings before being persisted:: value = json.loads(value) return value -The usage of ``json`` is only for the purposes of example. The :mod:`sqlalchemy.ext.mutable` -extension can be used +The usage of ``json`` is only for the purposes of example. The +:mod:`sqlalchemy.ext.mutable` extension can be used with any type whose target Python type may be mutable, including :class:`.PickleType`, :class:`.postgresql.ARRAY`, etc. When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself -tracks all parents which reference it. Here we will replace the usage -of plain Python dictionaries with a dict subclass that implements -the :class:`.Mutable` mixin:: +tracks all parents which reference it. Below, we illustrate the a simple +version of the :class:`.MutableDict` dictionary object, which applies +the :class:`.Mutable` mixin to a plain Python dictionary:: import collections from sqlalchemy.ext.mutable import Mutable - class MutationDict(Mutable, dict): + class MutableDict(Mutable, dict): @classmethod def coerce(cls, key, value): - "Convert plain dictionaries to MutationDict." + "Convert plain dictionaries to MutableDict." - if not isinstance(value, MutationDict): + if not isinstance(value, MutableDict): if isinstance(value, dict): - return MutationDict(value) + return MutableDict(value) # this call will raise ValueError return Mutable.coerce(key, value) @@ -84,23 +80,23 @@ the :class:`.Mutable` mixin:: The above dictionary class takes the approach of subclassing the Python built-in ``dict`` to produce a dict -subclass which routes all mutation events through ``__setitem__``. There are -many variants on this approach, such as subclassing ``UserDict.UserDict``, -the newer ``collections.MutableMapping``, etc. The part that's important to this -example is that the :meth:`.Mutable.changed` method is called whenever an in-place change to the -datastructure takes place. +subclass which routes all mutation events through ``__setitem__``. There are +variants on this approach, such as subclassing ``UserDict.UserDict`` or +``collections.MutableMapping``; the part that's important to this example is +that the :meth:`.Mutable.changed` method is called whenever an in-place +change to the datastructure takes place. We also redefine the :meth:`.Mutable.coerce` method which will be used to -convert any values that are not instances of ``MutationDict``, such +convert any values that are not instances of ``MutableDict``, such as the plain dictionaries returned by the ``json`` module, into the -appropriate type. Defining this method is optional; we could just as well created our -``JSONEncodedDict`` such that it always returns an instance of ``MutationDict``, -and additionally ensured that all calling code uses ``MutationDict`` -explicitly. When :meth:`.Mutable.coerce` is not overridden, any values -applied to a parent object which are not instances of the mutable type -will raise a ``ValueError``. +appropriate type. Defining this method is optional; we could just as well +created our ``JSONEncodedDict`` such that it always returns an instance +of ``MutableDict``, and additionally ensured that all calling code +uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not +overridden, any values applied to a parent object which are not instances +of the mutable type will raise a ``ValueError``. -Our new ``MutationDict`` type offers a class method +Our new ``MutableDict`` type offers a class method :meth:`~.Mutable.as_mutable` which we can use within column metadata to associate with types. This method grabs the given type object or class and associates a listener that will detect all future mappings @@ -111,7 +107,7 @@ attribute. Such as, with classical table metadata:: my_data = Table('my_data', metadata, Column('id', Integer, primary_key=True), - Column('data', MutationDict.as_mutable(JSONEncodedDict)) + Column('data', MutableDict.as_mutable(JSONEncodedDict)) ) Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict`` @@ -139,7 +135,7 @@ There's no difference in usage when using declarative:: class MyDataClass(Base): __tablename__ = 'my_data' id = Column(Integer, primary_key=True) - data = Column(MutationDict.as_mutable(JSONEncodedDict)) + data = Column(MutableDict.as_mutable(JSONEncodedDict)) Any in-place changes to the ``MyDataClass.data`` member will flag the attribute as "dirty" on the parent object:: @@ -155,13 +151,14 @@ will flag the attribute as "dirty" on the parent object:: >>> assert m1 in sess.dirty True -The ``MutationDict`` can be associated with all future instances -of ``JSONEncodedDict`` in one step, using :meth:`~.Mutable.associate_with`. This -is similar to :meth:`~.Mutable.as_mutable` except it will intercept -all occurrences of ``MutationDict`` in all mappings unconditionally, without +The ``MutableDict`` can be associated with all future instances +of ``JSONEncodedDict`` in one step, using +:meth:`~.Mutable.associate_with`. This is similar to +:meth:`~.Mutable.as_mutable` except it will intercept all occurrences +of ``MutableDict`` in all mappings unconditionally, without the need to declare it individually:: - MutationDict.associate_with(JSONEncodedDict) + MutableDict.associate_with(JSONEncodedDict) class MyDataClass(Base): __tablename__ = 'my_data' @@ -181,7 +178,7 @@ callbacks. In our case, this is a good thing, since if this dictionary were picklable, it could lead to an excessively large pickle size for our value objects that are pickled by themselves outside of the context of the parent. The developer responsibility here is only to provide a ``__getstate__`` method -that excludes the :meth:`~.MutableBase._parents` collection from the pickle +that excludes the :meth:`~MutableBase._parents` collection from the pickle stream:: class MyMutableType(Mutable): @@ -193,7 +190,7 @@ stream:: With our dictionary example, we need to return the contents of the dict itself (and also restore them on __setstate__):: - class MutationDict(Mutable, dict): + class MutableDict(Mutable, dict): # .... def __getstate__(self): @@ -331,7 +328,7 @@ Supporting Pickling As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper class uses a ``weakref.WeakKeyDictionary`` available via the -:meth:`.MutableBase._parents` attribute which isn't picklable. If we need to +:meth:`MutableBase._parents` attribute which isn't picklable. If we need to pickle instances of ``Point`` or its owning class ``Vertex``, we at least need to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary. Below we define both a ``__getstate__`` and a ``__setstate__`` that package up @@ -348,17 +345,21 @@ the minimal form of our ``Point`` class:: As with :class:`.Mutable`, the :class:`.MutableComposite` augments the pickling process of the parent's object-relational state so that the -:meth:`.MutableBase._parents` collection is restored to all ``Point`` objects. +:meth:`MutableBase._parents` collection is restored to all ``Point`` objects. """ -from sqlalchemy.orm.attributes import flag_modified -from sqlalchemy import event, types -from sqlalchemy.orm import mapper, object_mapper, Mapper -from sqlalchemy.util import memoized_property +from ..orm.attributes import flag_modified +from .. import event, types +from ..orm import mapper, object_mapper, Mapper +from ..util import memoized_property import weakref + class MutableBase(object): - """Common base class to :class:`.Mutable` and :class:`.MutableComposite`.""" + """Common base class to :class:`.Mutable` + and :class:`.MutableComposite`. + + """ @memoized_property def _parents(self): @@ -398,7 +399,8 @@ class MutableBase(object): """ if value is None: return None - raise ValueError("Attribute '%s' does not accept objects of type %s" % (key, type(value))) + msg = "Attribute '%s' does not accept objects of type %s" + raise ValueError(msg % (key, type(value))) @classmethod def _listen_on_attribute(cls, attribute, coerce, parent_cls): @@ -456,12 +458,17 @@ class MutableBase(object): for val in state_dict['ext.mutable.values']: val._parents[state.obj()] = key + event.listen(parent_cls, 'load', load, + raw=True, propagate=True) + event.listen(parent_cls, 'refresh', load, + raw=True, propagate=True) + event.listen(attribute, 'set', set, + raw=True, retval=True, propagate=True) + event.listen(parent_cls, 'pickle', pickle, + raw=True, propagate=True) + event.listen(parent_cls, 'unpickle', unpickle, + raw=True, propagate=True) - event.listen(parent_cls, 'load', load, raw=True, propagate=True) - event.listen(parent_cls, 'refresh', load, raw=True, propagate=True) - event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True) - event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True) - event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True) class Mutable(MutableBase): """Mixin that defines transparent propagation of change @@ -490,23 +497,23 @@ class Mutable(MutableBase): """Associate this wrapper with all future mapped columns of the given type. - This is a convenience method that calls ``associate_with_attribute`` automatically. + This is a convenience method that calls + ``associate_with_attribute`` automatically. .. warning:: The listeners established by this method are *global* to all mappers, and are *not* garbage collected. Only use - :meth:`.associate_with` for types that are permanent to an application, - not with ad-hoc types else this will cause unbounded growth - in memory usage. + :meth:`.associate_with` for types that are permanent to an + application, not with ad-hoc types else this will cause unbounded + growth in memory usage. """ def listen_for_type(mapper, class_): - for prop in mapper.iterate_properties: - if hasattr(prop, 'columns'): - if isinstance(prop.columns[0].type, sqltype): - cls.associate_with_attribute(getattr(class_, prop.key)) + for prop in mapper.column_attrs: + if isinstance(prop.columns[0].type, sqltype): + cls.associate_with_attribute(getattr(class_, prop.key)) event.listen(mapper, 'mapper_configured', listen_for_type) @@ -526,12 +533,12 @@ class Mutable(MutableBase): ) Note that the returned type is always an instance, even if a class - is given, and that only columns which are declared specifically with that - type instance receive additional instrumentation. + is given, and that only columns which are declared specifically with + that type instance receive additional instrumentation. To associate a particular mutable type with all occurrences of a particular type, use the :meth:`.Mutable.associate_with` classmethod - of the particular :meth:`.Mutable` subclass to establish a global + of the particular :class:`.Mutable` subclass to establish a global association. .. warning:: @@ -546,15 +553,16 @@ class Mutable(MutableBase): sqltype = types.to_instance(sqltype) def listen_for_type(mapper, class_): - for prop in mapper.iterate_properties: - if hasattr(prop, 'columns'): - if prop.columns[0].type is sqltype: - cls.associate_with_attribute(getattr(class_, prop.key)) + for prop in mapper.column_attrs: + if prop.columns[0].type is sqltype: + cls.associate_with_attribute(getattr(class_, prop.key)) event.listen(mapper, 'mapper_configured', listen_for_type) return sqltype + + class MutableComposite(MutableBase): """Mixin that defines transparent propagation of change events on a SQLAlchemy "composite" object to its @@ -562,14 +570,6 @@ class MutableComposite(MutableBase): See the example in :ref:`mutable_composites` for usage information. - .. warning:: - - The listeners established by the :class:`.MutableComposite` - class are *global* to all mappers, and are *not* garbage collected. Only use - :class:`.MutableComposite` for types that are permanent to an application, - not with ad-hoc types else this will cause unbounded growth - in memory usage. - """ def changed(self): @@ -583,14 +583,52 @@ class MutableComposite(MutableBase): prop._attribute_keys): setattr(parent, attr_name, value) - def _setup_composite_listener(): def _listen_for_type(mapper, class_): for prop in mapper.iterate_properties: if (hasattr(prop, 'composite_class') and - issubclass(prop.composite_class, MutableComposite)): + isinstance(prop.composite_class, type) and + issubclass(prop.composite_class, MutableComposite)): prop.composite_class._listen_on_attribute( getattr(class_, prop.key), False, class_) - if not Mapper.dispatch.mapper_configured._contains(Mapper, _listen_for_type): + if not event.contains(Mapper, "mapper_configured", _listen_for_type): event.listen(Mapper, 'mapper_configured', _listen_for_type) _setup_composite_listener() + + +class MutableDict(Mutable, dict): + """A dictionary type that implements :class:`.Mutable`. + + .. versionadded:: 0.8 + + """ + + def __setitem__(self, key, value): + """Detect dictionary set events and emit change events.""" + dict.__setitem__(self, key, value) + self.changed() + + def __delitem__(self, key): + """Detect dictionary del events and emit change events.""" + dict.__delitem__(self, key) + self.changed() + + def clear(self): + dict.clear(self) + self.changed() + + @classmethod + def coerce(cls, key, value): + """Convert plain dictionary to MutableDict.""" + if not isinstance(value, MutableDict): + if isinstance(value, dict): + return MutableDict(value) + return Mutable.coerce(key, value) + else: + return value + + def __getstate__(self): + return dict(self) + + def __setstate__(self, state): + self.update(state) diff --git a/libs/sqlalchemy/ext/orderinglist.py b/libs/sqlalchemy/ext/orderinglist.py index 6a9b6c39..9310c607 100644 --- a/libs/sqlalchemy/ext/orderinglist.py +++ b/libs/sqlalchemy/ext/orderinglist.py @@ -1,5 +1,5 @@ # ext/orderinglist.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -107,8 +107,8 @@ or some other integer, provide ``count_from=1``. """ -from sqlalchemy.orm.collections import collection -from sqlalchemy import util +from ..orm.collections import collection +from .. import util __all__ = ['ordering_list'] @@ -324,7 +324,7 @@ class OrderingList(list): if stop < 0: stop += len(self) - for i in xrange(start, stop, step): + for i in range(start, stop, step): self.__setitem__(i, entity[i]) else: self._order_entity(index, entity, True) @@ -334,7 +334,6 @@ class OrderingList(list): super(OrderingList, self).__delitem__(index) self._reorder() - # Py2K def __setslice__(self, start, end, values): super(OrderingList, self).__setslice__(start, end, values) self._reorder() @@ -342,13 +341,12 @@ class OrderingList(list): def __delslice__(self, start, end): super(OrderingList, self).__delslice__(start, end) self._reorder() - # end Py2K def __reduce__(self): return _reconstitute, (self.__class__, self.__dict__, list(self)) - for func_name, func in locals().items(): - if (util.callable(func) and func.func_name == func_name and + for func_name, func in list(locals().items()): + if (util.callable(func) and func.__name__ == func_name and not func.__doc__ and hasattr(list, func_name)): func.__doc__ = getattr(list, func_name).__doc__ del func_name, func diff --git a/libs/sqlalchemy/ext/serializer.py b/libs/sqlalchemy/ext/serializer.py index 47121bca..388cd404 100644 --- a/libs/sqlalchemy/ext/serializer.py +++ b/libs/sqlalchemy/ext/serializer.py @@ -1,5 +1,5 @@ # ext/serializer.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -39,46 +39,32 @@ The serializer module is only appropriate for query structures. It is not needed for: * instances of user-defined classes. These contain no references to engines, - sessions or expression constructs in the typical case and can be serialized directly. + sessions or expression constructs in the typical case and can be serialized + directly. -* Table metadata that is to be loaded entirely from the serialized structure (i.e. is - not already declared in the application). Regular pickle.loads()/dumps() can - be used to fully dump any ``MetaData`` object, typically one which was reflected - from an existing database at some previous point in time. The serializer module - is specifically for the opposite case, where the Table metadata is already present - in memory. +* Table metadata that is to be loaded entirely from the serialized structure + (i.e. is not already declared in the application). Regular + pickle.loads()/dumps() can be used to fully dump any ``MetaData`` object, + typically one which was reflected from an existing database at some previous + point in time. The serializer module is specifically for the opposite case, + where the Table metadata is already present in memory. """ -from sqlalchemy.orm import class_mapper, Query -from sqlalchemy.orm.session import Session -from sqlalchemy.orm.mapper import Mapper -from sqlalchemy.orm.attributes import QueryableAttribute -from sqlalchemy import Table, Column -from sqlalchemy.engine import Engine -from sqlalchemy.util import pickle +from ..orm import class_mapper +from ..orm.session import Session +from ..orm.mapper import Mapper +from ..orm.interfaces import MapperProperty +from ..orm.attributes import QueryableAttribute +from .. import Table, Column +from ..engine import Engine +from ..util import pickle, byte_buffer, b64encode, b64decode, text_type import re -import base64 -# Py3K -#from io import BytesIO as byte_buffer -# Py2K -from cStringIO import StringIO as byte_buffer -# end Py2K -# Py3K -#def b64encode(x): -# return base64.b64encode(x).decode('ascii') -#def b64decode(x): -# return base64.b64decode(x.encode('ascii')) -# Py2K -b64encode = base64.b64encode -b64decode = base64.b64decode -# end Py2K __all__ = ['Serializer', 'Deserializer', 'dumps', 'loads'] - def Serializer(*args, **kw): pickler = pickle.Pickler(*args, **kw) @@ -90,10 +76,13 @@ def Serializer(*args, **kw): id = "attribute:" + key + ":" + b64encode(pickle.dumps(cls)) elif isinstance(obj, Mapper) and not obj.non_primary: id = "mapper:" + b64encode(pickle.dumps(obj.class_)) + elif isinstance(obj, MapperProperty) and not obj.parent.non_primary: + id = "mapperprop:" + b64encode(pickle.dumps(obj.parent.class_)) + \ + ":" + obj.key elif isinstance(obj, Table): - id = "table:" + str(obj) + id = "table:" + text_type(obj.key) elif isinstance(obj, Column) and isinstance(obj.table, Table): - id = "column:" + str(obj.table) + ":" + obj.key + id = "column:" + text_type(obj.table.key) + ":" + text_type(obj.key) elif isinstance(obj, Session): id = "session:" elif isinstance(obj, Engine): @@ -105,7 +94,9 @@ def Serializer(*args, **kw): pickler.persistent_id = persistent_id return pickler -our_ids = re.compile(r'(mapper|table|column|session|attribute|engine):(.*)') +our_ids = re.compile( + r'(mapperprop|mapper|table|column|session|attribute|engine):(.*)') + def Deserializer(file, metadata=None, scoped_session=None, engine=None): unpickler = pickle.Unpickler(file) @@ -121,7 +112,7 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None): return None def persistent_load(id): - m = our_ids.match(id) + m = our_ids.match(text_type(id)) if not m: return None else: @@ -133,6 +124,10 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None): elif type_ == "mapper": cls = pickle.loads(b64decode(args)) return class_mapper(cls) + elif type_ == "mapperprop": + mapper, keyname = args.split(':') + cls = pickle.loads(b64decode(mapper)) + return class_mapper(cls).attrs[keyname] elif type_ == "table": return metadata.tables[args] elif type_ == "column": @@ -147,15 +142,15 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None): unpickler.persistent_load = persistent_load return unpickler + def dumps(obj, protocol=0): buf = byte_buffer() pickler = Serializer(buf, protocol) pickler.dump(obj) return buf.getvalue() + def loads(data, metadata=None, scoped_session=None, engine=None): buf = byte_buffer(data) unpickler = Deserializer(buf, metadata, scoped_session, engine) return unpickler.load() - - diff --git a/libs/sqlalchemy/ext/sqlsoup.py b/libs/sqlalchemy/ext/sqlsoup.py deleted file mode 100644 index 486b09c5..00000000 --- a/libs/sqlalchemy/ext/sqlsoup.py +++ /dev/null @@ -1,811 +0,0 @@ -# ext/sqlsoup.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. versionchanged:: 0.8 - SQLSoup is now its own project. Documentation - and project status are available at: - http://pypi.python.org/pypi/sqlsoup and - http://readthedocs.org/docs/sqlsoup\ . - SQLSoup will no longer be included with SQLAlchemy. - - -Introduction -============ - -SqlSoup provides a convenient way to access existing database -tables without having to declare table or mapper classes ahead -of time. It is built on top of the SQLAlchemy ORM and provides a -super-minimalistic interface to an existing database. - -SqlSoup effectively provides a coarse grained, alternative -interface to working with the SQLAlchemy ORM, providing a "self -configuring" interface for extremely rudimental operations. It's -somewhat akin to a "super novice mode" version of the ORM. While -SqlSoup can be very handy, users are strongly encouraged to use -the full ORM for non-trivial applications. - -Suppose we have a database with users, books, and loans tables -(corresponding to the PyWebOff dataset, if you're curious). - -Creating a SqlSoup gateway is just like creating an SQLAlchemy -engine:: - - >>> from sqlalchemy.ext.sqlsoup import SqlSoup - >>> db = SqlSoup('sqlite:///:memory:') - -or, you can re-use an existing engine:: - - >>> db = SqlSoup(engine) - -You can optionally specify a schema within the database for your -SqlSoup:: - - >>> db.schema = myschemaname - -Loading objects -=============== - -Loading objects is as easy as this:: - - >>> users = db.users.all() - >>> users.sort() - >>> users - [ - MappedUsers(name=u'Joe Student',email=u'student@example.edu', - password=u'student',classname=None,admin=0), - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1) - ] - -Of course, letting the database do the sort is better:: - - >>> db.users.order_by(db.users.name).all() - [ - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1), - MappedUsers(name=u'Joe Student',email=u'student@example.edu', - password=u'student',classname=None,admin=0) - ] - -Field access is intuitive:: - - >>> users[0].email - u'student@example.edu' - -Of course, you don't want to load all users very often. Let's -add a WHERE clause. Let's also switch the order_by to DESC while -we're at it:: - - >>> from sqlalchemy import or_, and_, desc - >>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu') - >>> db.users.filter(where).order_by(desc(db.users.name)).all() - [ - MappedUsers(name=u'Joe Student',email=u'student@example.edu', - password=u'student',classname=None,admin=0), - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1) - ] - -You can also use .first() (to retrieve only the first object -from a query) or .one() (like .first when you expect exactly one -user -- it will raise an exception if more were returned):: - - >>> db.users.filter(db.users.name=='Bhargan Basepair').one() - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1) - -Since name is the primary key, this is equivalent to - - >>> db.users.get('Bhargan Basepair') - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1) - -This is also equivalent to - - >>> db.users.filter_by(name='Bhargan Basepair').one() - MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu', - password=u'basepair',classname=None,admin=1) - -filter_by is like filter, but takes kwargs instead of full -clause expressions. This makes it more concise for simple -queries like this, but you can't do complex queries like the -or\_ above or non-equality based comparisons this way. - -Full query documentation ------------------------- - -Get, filter, filter_by, order_by, limit, and the rest of the -query methods are explained in detail in -:ref:`ormtutorial_querying`. - -Modifying objects -================= - -Modifying objects is intuitive:: - - >>> user = _ - >>> user.email = 'basepair+nospam@example.edu' - >>> db.commit() - -(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work -code, so multiple updates to a single object will be turned into -a single ``UPDATE`` statement when you commit.) - -To finish covering the basics, let's insert a new loan, then -delete it:: - - >>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id - >>> db.loans.insert(book_id=book_id, user_name=user.name) - MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None) - - >>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one() - >>> db.delete(loan) - >>> db.commit() - -You can also delete rows that have not been loaded as objects. -Let's do our insert/delete cycle once more, this time using the -loans table's delete method. (For SQLAlchemy experts: note that -no flush() call is required since this delete acts at the SQL -level, not at the Mapper level.) The same where-clause -construction rules apply here as to the select methods:: - - >>> db.loans.insert(book_id=book_id, user_name=user.name) - MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None) - >>> db.loans.delete(db.loans.book_id==2) - -You can similarly update multiple rows at once. This will change the -book_id to 1 in all loans whose book_id is 2:: - - >>> db.loans.filter_by(db.loans.book_id==2).update({'book_id':1}) - >>> db.loans.filter_by(book_id=1).all() - [MappedLoans(book_id=1,user_name=u'Joe Student', - loan_date=datetime.datetime(2006, 7, 12, 0, 0))] - - -Joins -===== - -Occasionally, you will want to pull out a lot of data from related -tables all at once. In this situation, it is far more efficient to -have the database perform the necessary join. (Here we do not have *a -lot of data* but hopefully the concept is still clear.) SQLAlchemy is -smart enough to recognize that loans has a foreign key to users, and -uses that as the join condition automatically:: - - >>> join1 = db.join(db.users, db.loans, isouter=True) - >>> join1.filter_by(name='Joe Student').all() - [ - MappedJoin(name=u'Joe Student',email=u'student@example.edu', - password=u'student',classname=None,admin=0,book_id=1, - user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0)) - ] - -If you're unfortunate enough to be using MySQL with the default MyISAM -storage engine, you'll have to specify the join condition manually, -since MyISAM does not store foreign keys. Here's the same join again, -with the join condition explicitly specified:: - - >>> db.join(db.users, db.loans, db.users.name==db.loans.user_name, isouter=True) - - -You can compose arbitrarily complex joins by combining Join objects -with tables or other joins. Here we combine our first join with the -books table:: - - >>> join2 = db.join(join1, db.books) - >>> join2.all() - [ - MappedJoin(name=u'Joe Student',email=u'student@example.edu', - password=u'student',classname=None,admin=0,book_id=1, - user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0), - id=1,title=u'Mustards I Have Known',published_year=u'1989', - authors=u'Jones') - ] - -If you join tables that have an identical column name, wrap your join -with `with_labels`, to disambiguate columns with their table name -(.c is short for .columns):: - - >>> db.with_labels(join1).c.keys() - [u'users_name', u'users_email', u'users_password', - u'users_classname', u'users_admin', u'loans_book_id', - u'loans_user_name', u'loans_loan_date'] - -You can also join directly to a labeled object:: - - >>> labeled_loans = db.with_labels(db.loans) - >>> db.join(db.users, labeled_loans, isouter=True).c.keys() - [u'name', u'email', u'password', u'classname', - u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date'] - - -Relationships -============= - -You can define relationships on SqlSoup classes: - - >>> db.users.relate('loans', db.loans) - -These can then be used like a normal SA property: - - >>> db.users.get('Joe Student').loans - [MappedLoans(book_id=1,user_name=u'Joe Student', - loan_date=datetime.datetime(2006, 7, 12, 0, 0))] - - >>> db.users.filter(~db.users.loans.any()).all() - [MappedUsers(name=u'Bhargan Basepair', - email='basepair+nospam@example.edu', - password=u'basepair',classname=None,admin=1)] - -relate can take any options that the relationship function -accepts in normal mapper definition: - - >>> del db._cache['users'] - >>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan') - -Advanced Use -============ - -Sessions, Transactions and Application Integration ---------------------------------------------------- - -.. note:: - - Please read and understand this section thoroughly - before using SqlSoup in any web application. - -SqlSoup uses a ScopedSession to provide thread-local sessions. -You can get a reference to the current one like this:: - - >>> session = db.session - -The default session is available at the module level in SQLSoup, -via:: - - >>> from sqlalchemy.ext.sqlsoup import Session - -The configuration of this session is ``autoflush=True``, -``autocommit=False``. This means when you work with the SqlSoup -object, you need to call ``db.commit()`` in order to have -changes persisted. You may also call ``db.rollback()`` to roll -things back. - -Since the SqlSoup object's Session automatically enters into a -transaction as soon as it's used, it is *essential* that you -call ``commit()`` or ``rollback()`` on it when the work within a -thread completes. This means all the guidelines for web -application integration at :ref:`session_lifespan` must be -followed. - -The SqlSoup object can have any session or scoped session -configured onto it. This is of key importance when integrating -with existing code or frameworks such as Pylons. If your -application already has a ``Session`` configured, pass it to -your SqlSoup object:: - - >>> from myapplication import Session - >>> db = SqlSoup(session=Session) - -If the ``Session`` is configured with ``autocommit=True``, use -``flush()`` instead of ``commit()`` to persist changes - in this -case, the ``Session`` closes out its transaction immediately and -no external management is needed. ``rollback()`` is also not -available. Configuring a new SQLSoup object in "autocommit" mode -looks like:: - - >>> from sqlalchemy.orm import scoped_session, sessionmaker - >>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True))) - - -Mapping arbitrary Selectables ------------------------------ - -SqlSoup can map any SQLAlchemy :class:`.Selectable` with the map -method. Let's map an :func:`.expression.select` object that uses an aggregate -function; we'll use the SQLAlchemy :class:`.Table` that SqlSoup -introspected as the basis. (Since we're not mapping to a simple -table or join, we need to tell SQLAlchemy how to find the -*primary key* which just needs to be unique within the select, -and not necessarily correspond to a *real* PK in the database.):: - - >>> from sqlalchemy import select, func - >>> b = db.books._table - >>> s = select([b.c.published_year, func.count('*').label('n')], from_obj=[b], group_by=[b.c.published_year]) - >>> s = s.alias('years_with_count') - >>> years_with_count = db.map(s, primary_key=[s.c.published_year]) - >>> years_with_count.filter_by(published_year='1989').all() - [MappedBooks(published_year=u'1989',n=1)] - -Obviously if we just wanted to get a list of counts associated with -book years once, raw SQL is going to be less work. The advantage of -mapping a Select is reusability, both standalone and in Joins. (And if -you go to full SQLAlchemy, you can perform mappings like this directly -to your object models.) - -An easy way to save mapped selectables like this is to just hang them on -your db object:: - - >>> db.years_with_count = years_with_count - -Python is flexible like that! - -Raw SQL -------- - -SqlSoup works fine with SQLAlchemy's text construct, described -in :ref:`sqlexpression_text`. You can also execute textual SQL -directly using the `execute()` method, which corresponds to the -`execute()` method on the underlying `Session`. Expressions here -are expressed like ``text()`` constructs, using named parameters -with colons:: - - >>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%') - >>> for name, email in rp.fetchall(): print name, email - Bhargan Basepair basepair+nospam@example.edu - -Or you can get at the current transaction's connection using -`connection()`. This is the raw connection object which can -accept any sort of SQL expression or raw SQL string passed to -the database:: - - >>> conn = db.connection() - >>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%') - -Dynamic table names -------------------- - -You can load a table whose name is specified at runtime with the -entity() method: - - >>> tablename = 'loans' - >>> db.entity(tablename) == db.loans - True - -entity() also takes an optional schema argument. If none is -specified, the default schema is used. - -""" - -from sqlalchemy import Table, MetaData, join -from sqlalchemy import schema, sql, util -from sqlalchemy.engine.base import Engine -from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \ - class_mapper, relationship, session,\ - object_session, attributes -from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE -from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, ArgumentError -from sqlalchemy.sql import expression - - -__all__ = ['PKNotFoundError', 'SqlSoup'] - -Session = scoped_session(sessionmaker(autoflush=True, autocommit=False)) - -class AutoAdd(MapperExtension): - def __init__(self, scoped_session): - self.scoped_session = scoped_session - - def instrument_class(self, mapper, class_): - class_.__init__ = self._default__init__(mapper) - - def _default__init__(ext, mapper): - def __init__(self, **kwargs): - for key, value in kwargs.iteritems(): - setattr(self, key, value) - return __init__ - - def init_instance(self, mapper, class_, oldinit, instance, args, kwargs): - session = self.scoped_session() - state = attributes.instance_state(instance) - session._save_impl(state) - return EXT_CONTINUE - - def init_failed(self, mapper, class_, oldinit, instance, args, kwargs): - sess = object_session(instance) - if sess: - sess.expunge(instance) - return EXT_CONTINUE - -class PKNotFoundError(SQLAlchemyError): - pass - -def _ddl_error(cls): - msg = 'SQLSoup can only modify mapped Tables (found: %s)' \ - % cls._table.__class__.__name__ - raise InvalidRequestError(msg) - -# metaclass is necessary to expose class methods with getattr, e.g. -# we want to pass db.users.select through to users._mapper.select -class SelectableClassType(type): - def insert(cls, **kwargs): - _ddl_error(cls) - - def __clause_element__(cls): - return cls._table - - def __getattr__(cls, attr): - if attr == '_query': - # called during mapper init - raise AttributeError() - return getattr(cls._query, attr) - -class TableClassType(SelectableClassType): - def insert(cls, **kwargs): - o = cls() - o.__dict__.update(kwargs) - return o - - def relate(cls, propname, *args, **kwargs): - class_mapper(cls)._configure_property(propname, relationship(*args, **kwargs)) - -def _is_outer_join(selectable): - if not isinstance(selectable, sql.Join): - return False - if selectable.isouter: - return True - return _is_outer_join(selectable.left) or _is_outer_join(selectable.right) - -def _selectable_name(selectable): - if isinstance(selectable, sql.Alias): - return _selectable_name(selectable.element) - elif isinstance(selectable, sql.Select): - return ''.join(_selectable_name(s) for s in selectable.froms) - elif isinstance(selectable, schema.Table): - return selectable.name.capitalize() - else: - x = selectable.__class__.__name__ - if x[0] == '_': - x = x[1:] - return x - -def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs): - selectable = expression._clause_element_as_expr(selectable) - mapname = 'Mapped' + _selectable_name(selectable) - # Py2K - if isinstance(mapname, unicode): - engine_encoding = engine.dialect.encoding - mapname = mapname.encode(engine_encoding) - # end Py2K - - if isinstance(selectable, Table): - klass = TableClassType(mapname, (base_cls,), {}) - else: - klass = SelectableClassType(mapname, (base_cls,), {}) - - def _compare(self, o): - L = list(self.__class__.c.keys()) - L.sort() - t1 = [getattr(self, k) for k in L] - try: - t2 = [getattr(o, k) for k in L] - except AttributeError: - raise TypeError('unable to compare with %s' % o.__class__) - return t1, t2 - - # python2/python3 compatible system of - # __cmp__ - __lt__ + __eq__ - - def __lt__(self, o): - t1, t2 = _compare(self, o) - return t1 < t2 - - def __eq__(self, o): - t1, t2 = _compare(self, o) - return t1 == t2 - - def __repr__(self): - L = ["%s=%r" % (key, getattr(self, key, '')) - for key in self.__class__.c.keys()] - return '%s(%s)' % (self.__class__.__name__, ','.join(L)) - - for m in ['__eq__', '__repr__', '__lt__']: - setattr(klass, m, eval(m)) - klass._table = selectable - klass.c = expression.ColumnCollection() - mappr = mapper(klass, - selectable, - extension=AutoAdd(session), - **mapper_kwargs) - - for k in mappr.iterate_properties: - klass.c[k.key] = k.columns[0] - - klass._query = session.query_property() - return klass - -class SqlSoup(object): - """Represent an ORM-wrapped database resource.""" - - def __init__(self, engine_or_metadata, base=object, session=None): - """Initialize a new :class:`.SqlSoup`. - - :param engine_or_metadata: a string database URL, :class:`.Engine` - or :class:`.MetaData` object to associate with. If the - argument is a :class:`.MetaData`, it should be *bound* - to an :class:`.Engine`. - :param base: a class which will serve as the default class for - returned mapped classes. Defaults to ``object``. - :param session: a :class:`.ScopedSession` or :class:`.Session` with - which to associate ORM operations for this :class:`.SqlSoup` instance. - If ``None``, a :class:`.ScopedSession` that's local to this - module is used. - - """ - - self.session = session or Session - self.base=base - - if isinstance(engine_or_metadata, MetaData): - self._metadata = engine_or_metadata - elif isinstance(engine_or_metadata, (basestring, Engine)): - self._metadata = MetaData(engine_or_metadata) - else: - raise ArgumentError("invalid engine or metadata argument %r" % - engine_or_metadata) - - self._cache = {} - self.schema = None - - @property - def bind(self): - """The :class:`.Engine` associated with this :class:`.SqlSoup`.""" - return self._metadata.bind - - engine = bind - - def delete(self, instance): - """Mark an instance as deleted.""" - - self.session.delete(instance) - - def execute(self, stmt, **params): - """Execute a SQL statement. - - The statement may be a string SQL string, - an :func:`.expression.select` construct, or an :func:`.expression.text` - construct. - - """ - return self.session.execute(sql.text(stmt, bind=self.bind), **params) - - @property - def _underlying_session(self): - if isinstance(self.session, session.Session): - return self.session - else: - return self.session() - - def connection(self): - """Return the current :class:`.Connection` in use by the current transaction.""" - - return self._underlying_session._connection_for_bind(self.bind) - - def flush(self): - """Flush pending changes to the database. - - See :meth:`.Session.flush`. - - """ - self.session.flush() - - def rollback(self): - """Rollback the current transaction. - - See :meth:`.Session.rollback`. - - """ - self.session.rollback() - - def commit(self): - """Commit the current transaction. - - See :meth:`.Session.commit`. - - """ - self.session.commit() - - def clear(self): - """Synonym for :meth:`.SqlSoup.expunge_all`.""" - - self.session.expunge_all() - - def expunge(self, instance): - """Remove an instance from the :class:`.Session`. - - See :meth:`.Session.expunge`. - - """ - self.session.expunge(instance) - - def expunge_all(self): - """Clear all objects from the current :class:`.Session`. - - See :meth:`.Session.expunge_all`. - - """ - self.session.expunge_all() - - def map_to(self, attrname, tablename=None, selectable=None, - schema=None, base=None, mapper_args=util.immutabledict()): - """Configure a mapping to the given attrname. - - This is the "master" method that can be used to create any - configuration. - - .. versionadded:: 0.6.6 - - :param attrname: String attribute name which will be - established as an attribute on this :class:.`.SqlSoup` - instance. - :param base: a Python class which will be used as the - base for the mapped class. If ``None``, the "base" - argument specified by this :class:`.SqlSoup` - instance's constructor will be used, which defaults to - ``object``. - :param mapper_args: Dictionary of arguments which will - be passed directly to :func:`.orm.mapper`. - :param tablename: String name of a :class:`.Table` to be - reflected. If a :class:`.Table` is already available, - use the ``selectable`` argument. This argument is - mutually exclusive versus the ``selectable`` argument. - :param selectable: a :class:`.Table`, :class:`.Join`, or - :class:`.Select` object which will be mapped. This - argument is mutually exclusive versus the ``tablename`` - argument. - :param schema: String schema name to use if the - ``tablename`` argument is present. - - - """ - if attrname in self._cache: - raise InvalidRequestError( - "Attribute '%s' is already mapped to '%s'" % ( - attrname, - class_mapper(self._cache[attrname]).mapped_table - )) - - if tablename is not None: - if not isinstance(tablename, basestring): - raise ArgumentError("'tablename' argument must be a string." - ) - if selectable is not None: - raise ArgumentError("'tablename' and 'selectable' " - "arguments are mutually exclusive") - - selectable = Table(tablename, - self._metadata, - autoload=True, - autoload_with=self.bind, - schema=schema or self.schema) - elif schema: - raise ArgumentError("'tablename' argument is required when " - "using 'schema'.") - elif selectable is not None: - if not isinstance(selectable, expression.FromClause): - raise ArgumentError("'selectable' argument must be a " - "table, select, join, or other " - "selectable construct.") - else: - raise ArgumentError("'tablename' or 'selectable' argument is " - "required.") - - if not selectable.primary_key.columns: - if tablename: - raise PKNotFoundError( - "table '%s' does not have a primary " - "key defined" % tablename) - else: - raise PKNotFoundError( - "selectable '%s' does not have a primary " - "key defined" % selectable) - - mapped_cls = _class_for_table( - self.session, - self.engine, - selectable, - base or self.base, - mapper_args - ) - self._cache[attrname] = mapped_cls - return mapped_cls - - - def map(self, selectable, base=None, **mapper_args): - """Map a selectable directly. - - .. versionchanged:: 0.6.6 - The class and its mapping are not cached and will - be discarded once dereferenced. - - :param selectable: an :func:`.expression.select` construct. - :param base: a Python class which will be used as the - base for the mapped class. If ``None``, the "base" - argument specified by this :class:`.SqlSoup` - instance's constructor will be used, which defaults to - ``object``. - :param mapper_args: Dictionary of arguments which will - be passed directly to :func:`.orm.mapper`. - - """ - - return _class_for_table( - self.session, - self.engine, - selectable, - base or self.base, - mapper_args - ) - - def with_labels(self, selectable, base=None, **mapper_args): - """Map a selectable directly, wrapping the - selectable in a subquery with labels. - - .. versionchanged:: 0.6.6 - The class and its mapping are not cached and will - be discarded once dereferenced. - - :param selectable: an :func:`.expression.select` construct. - :param base: a Python class which will be used as the - base for the mapped class. If ``None``, the "base" - argument specified by this :class:`.SqlSoup` - instance's constructor will be used, which defaults to - ``object``. - :param mapper_args: Dictionary of arguments which will - be passed directly to :func:`.orm.mapper`. - - """ - - # TODO give meaningful aliases - return self.map( - expression._clause_element_as_expr(selectable). - select(use_labels=True). - alias('foo'), base=base, **mapper_args) - - def join(self, left, right, onclause=None, isouter=False, - base=None, **mapper_args): - """Create an :func:`.expression.join` and map to it. - - .. versionchanged:: 0.6.6 - The class and its mapping are not cached and will - be discarded once dereferenced. - - :param left: a mapped class or table object. - :param right: a mapped class or table object. - :param onclause: optional "ON" clause construct.. - :param isouter: if True, the join will be an OUTER join. - :param base: a Python class which will be used as the - base for the mapped class. If ``None``, the "base" - argument specified by this :class:`.SqlSoup` - instance's constructor will be used, which defaults to - ``object``. - :param mapper_args: Dictionary of arguments which will - be passed directly to :func:`.orm.mapper`. - - """ - - j = join(left, right, onclause=onclause, isouter=isouter) - return self.map(j, base=base, **mapper_args) - - def entity(self, attr, schema=None): - """Return the named entity from this :class:`.SqlSoup`, or - create if not present. - - For more generalized mapping, see :meth:`.map_to`. - - """ - try: - return self._cache[attr] - except KeyError, ke: - return self.map_to(attr, tablename=attr, schema=schema) - - def __getattr__(self, attr): - return self.entity(attr) - - def __repr__(self): - return 'SqlSoup(%r)' % self._metadata - diff --git a/libs/sqlalchemy/inspection.py b/libs/sqlalchemy/inspection.py new file mode 100644 index 00000000..fe9e4055 --- /dev/null +++ b/libs/sqlalchemy/inspection.py @@ -0,0 +1,92 @@ +# sqlalchemy/inspect.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""The inspection module provides the :func:`.inspect` function, +which delivers runtime information about a wide variety +of SQLAlchemy objects, both within the Core as well as the +ORM. + +The :func:`.inspect` function is the entry point to SQLAlchemy's +public API for viewing the configuration and construction +of in-memory objects. Depending on the type of object +passed to :func:`.inspect`, the return value will either be +a related object which provides a known interface, or in many +cases it will return the object itself. + +The rationale for :func:`.inspect` is twofold. One is that +it replaces the need to be aware of a large variety of "information +getting" functions in SQLAlchemy, such as :meth:`.Inspector.from_engine`, +:func:`.orm.attributes.instance_state`, :func:`.orm.class_mapper`, +and others. The other is that the return value of :func:`.inspect` +is guaranteed to obey a documented API, thus allowing third party +tools which build on top of SQLAlchemy configurations to be constructed +in a forwards-compatible way. + +.. versionadded:: 0.8 The :func:`.inspect` system is introduced + as of version 0.8. + +""" + +from . import util, exc +_registrars = util.defaultdict(list) + + +def inspect(subject, raiseerr=True): + """Produce an inspection object for the given target. + + The returned value in some cases may be the + same object as the one given, such as if a + :class:`.Mapper` object is passed. In other + cases, it will be an instance of the registered + inspection type for the given object, such as + if an :class:`.engine.Engine` is passed, an + :class:`.Inspector` object is returned. + + :param subject: the subject to be inspected. + :param raiseerr: When ``True``, if the given subject + does not + correspond to a known SQLAlchemy inspected type, + :class:`sqlalchemy.exc.NoInspectionAvailable` + is raised. If ``False``, ``None`` is returned. + + """ + type_ = type(subject) + for cls in type_.__mro__: + if cls in _registrars: + reg = _registrars[cls] + if reg is True: + return subject + ret = reg(subject) + if ret is not None: + break + else: + reg = ret = None + + if raiseerr and ( + reg is None or ret is None + ): + raise exc.NoInspectionAvailable( + "No inspection system is " + "available for object of type %s" % + type_) + return ret + + +def _inspects(*types): + def decorate(fn_or_cls): + for type_ in types: + if type_ in _registrars: + raise AssertionError( + "Type %s is already " + "registered" % type_) + _registrars[type_] = fn_or_cls + return fn_or_cls + return decorate + + +def _self_inspects(cls): + _inspects(cls)(True) + return cls diff --git a/libs/sqlalchemy/interfaces.py b/libs/sqlalchemy/interfaces.py index 92fb3154..ed50a645 100644 --- a/libs/sqlalchemy/interfaces.py +++ b/libs/sqlalchemy/interfaces.py @@ -1,18 +1,19 @@ # sqlalchemy/interfaces.py -# Copyright (C) 2007-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2007-2014 the SQLAlchemy authors and contributors # Copyright (C) 2007 Jason Kirtland jek@discorporate.us # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Interfaces and abstract types. +"""Deprecated core event interfaces. This module is **deprecated** and is superseded by the event system. """ -from sqlalchemy import event, util +from . import event, util + class PoolListener(object): """Hooks into the lifecycle of connections in a :class:`.Pool`. @@ -89,7 +90,6 @@ class PoolListener(object): if hasattr(listener, 'checkin'): event.listen(self, 'checkin', listener.checkin) - def connect(self, dbapi_con, con_record): """Called once for each new DB-API connection or Pool's ``creator()``. @@ -148,6 +148,7 @@ class PoolListener(object): """ + class ConnectionProxy(object): """Allows interception of statement execution by Connections. @@ -161,11 +162,13 @@ class ConnectionProxy(object): cursor level executions, e.g.:: class MyProxy(ConnectionProxy): - def execute(self, conn, execute, clauseelement, *multiparams, **params): + def execute(self, conn, execute, clauseelement, + *multiparams, **params): print "compiled statement:", clauseelement return execute(clauseelement, *multiparams, **params) - def cursor_execute(self, execute, cursor, statement, parameters, context, executemany): + def cursor_execute(self, execute, cursor, statement, + parameters, context, executemany): print "raw statement:", statement return execute(cursor, statement, parameters, context) @@ -195,7 +198,7 @@ class ConnectionProxy(object): event.listen(self, 'before_execute', adapt_execute) def adapt_cursor_execute(conn, cursor, statement, - parameters,context, executemany, ): + parameters, context, executemany): def execute_wrapper( cursor, @@ -245,14 +248,13 @@ class ConnectionProxy(object): event.listen(self, 'commit_twophase', adapt_listener(listener.commit_twophase)) - def execute(self, conn, execute, clauseelement, *multiparams, **params): """Intercept high level execute() events.""" - return execute(clauseelement, *multiparams, **params) - def cursor_execute(self, execute, cursor, statement, parameters, context, executemany): + def cursor_execute(self, execute, cursor, statement, parameters, + context, executemany): """Intercept low-level cursor execute() events.""" return execute(cursor, statement, parameters, context) @@ -306,4 +308,3 @@ class ConnectionProxy(object): """Intercept commit_twophase() events.""" return commit_twophase(xid, is_prepared) - diff --git a/libs/sqlalchemy/log.py b/libs/sqlalchemy/log.py index 24608fde..935761d5 100644 --- a/libs/sqlalchemy/log.py +++ b/libs/sqlalchemy/log.py @@ -1,5 +1,5 @@ # sqlalchemy/log.py -# Copyright (C) 2006-2011 the SQLAlchemy authors and contributors +# Copyright (C) 2006-2014 the SQLAlchemy authors and contributors # Includes alterations by Vinay Sajip vinay_sajip@yahoo.co.uk # # This module is part of SQLAlchemy and is released under @@ -19,7 +19,6 @@ instance only. import logging import sys -from sqlalchemy import util # set initial level to WARN. This so that # log statements don't occur in the absense of explicit @@ -28,24 +27,24 @@ rootlogger = logging.getLogger('sqlalchemy') if rootlogger.level == logging.NOTSET: rootlogger.setLevel(logging.WARN) + def _add_default_handler(logger): handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s %(name)s %(message)s')) logger.addHandler(handler) + _logged_classes = set() -def class_logger(cls, enable=False): + + +def class_logger(cls): logger = logging.getLogger(cls.__module__ + "." + cls.__name__) - if enable == 'debug': - logger.setLevel(logging.DEBUG) - elif enable == 'info': - logger.setLevel(logging.INFO) cls._should_log_debug = lambda self: logger.isEnabledFor(logging.DEBUG) cls._should_log_info = lambda self: logger.isEnabledFor(logging.INFO) cls.logger = logger _logged_classes.add(cls) - + return cls class Identified(object): logging_name = None @@ -56,6 +55,7 @@ class Identified(object): def _should_log_info(self): return self.logger.isEnabledFor(logging.INFO) + class InstanceLogger(object): """A logger adapter (wrapper) for :class:`.Identified` subclasses. @@ -167,6 +167,7 @@ class InstanceLogger(object): level = self.logger.getEffectiveLevel() return level + def instance_logger(instance, echoflag=None): """create a logger for an instance that implements :class:`.Identified`.""" @@ -191,6 +192,7 @@ def instance_logger(instance, echoflag=None): instance.logger = logger + class echo_property(object): __doc__ = """\ When ``True``, enable log output for this element. diff --git a/libs/sqlalchemy/orm/__init__.py b/libs/sqlalchemy/orm/__init__.py index b4006be4..7825a70a 100644 --- a/libs/sqlalchemy/orm/__init__.py +++ b/libs/sqlalchemy/orm/__init__.py @@ -1,5 +1,5 @@ # orm/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -12,134 +12,60 @@ documentation for an overview of how this module is used. """ -from sqlalchemy.orm import exc -from sqlalchemy.orm.mapper import ( +from . import exc +from .mapper import ( Mapper, _mapper_registry, class_mapper, - configure_mappers + configure_mappers, + reconstructor, + validates ) -from sqlalchemy.orm.interfaces import ( +from .interfaces import ( EXT_CONTINUE, EXT_STOP, - InstrumentationManager, - MapperExtension, PropComparator, + ) +from .deprecated_interfaces import ( + MapperExtension, SessionExtension, AttributeExtension, - ) -from sqlalchemy.orm.util import ( +) +from .util import ( aliased, join, object_mapper, outerjoin, polymorphic_union, + was_deleted, with_parent, + with_polymorphic, ) -from sqlalchemy.orm.properties import ( - ColumnProperty, +from .properties import ColumnProperty +from .relationships import RelationshipProperty +from .descriptor_props import ( ComparableProperty, CompositeProperty, - RelationshipProperty, - PropertyLoader, SynonymProperty, - ) -from sqlalchemy.orm import mapper as mapperlib -from sqlalchemy.orm.mapper import reconstructor, validates -from sqlalchemy.orm import strategies -from sqlalchemy.orm.query import AliasOption, Query -from sqlalchemy.sql import util as sql_util -from sqlalchemy.orm.session import Session -from sqlalchemy.orm.session import object_session, sessionmaker, \ - make_transient -from sqlalchemy.orm.scoping import ScopedSession -from sqlalchemy import util as sa_util - -__all__ = ( - 'EXT_CONTINUE', - 'EXT_STOP', - 'InstrumentationManager', - 'MapperExtension', - 'AttributeExtension', - 'PropComparator', - 'Query', - 'Session', - 'aliased', - 'backref', - 'class_mapper', - 'clear_mappers', - 'column_property', - 'comparable_property', - 'compile_mappers', - 'configure_mappers', - 'composite', - 'contains_alias', - 'contains_eager', - 'create_session', - 'defer', - 'deferred', - 'dynamic_loader', - 'eagerload', - 'eagerload_all', - 'immediateload', - 'join', - 'joinedload', - 'joinedload_all', - 'lazyload', - 'mapper', - 'make_transient', - 'noload', - 'object_mapper', - 'object_session', - 'outerjoin', - 'polymorphic_union', - 'reconstructor', - 'relationship', - 'relation', - 'scoped_session', - 'sessionmaker', - 'subqueryload', - 'subqueryload_all', - 'synonym', - 'undefer', - 'undefer_group', - 'validates' ) - - -def scoped_session(session_factory, scopefunc=None): - """Provides thread-local or scoped management of :class:`.Session` objects. - - This is a front-end function to - :class:`.ScopedSession`:: - - Session = scoped_session(sessionmaker(autoflush=True)) - - To instantiate a Session object which is part of the scoped context, - instantiate normally:: - - session = Session() - - Most session methods are available as classmethods from the scoped - session:: - - Session.commit() - Session.close() - - See also: :ref:`unitofwork_contextual`. - - :param session_factory: a callable function that produces - :class:`.Session` instances, such as :func:`sessionmaker`. - - :param scopefunc: Optional "scope" function which would be - passed to the :class:`.ScopedRegistry`. If None, the - :class:`.ThreadLocalRegistry` is used by default. - - :returns: a :class:`.ScopedSession` instance - - - """ - return ScopedSession(session_factory, scopefunc=scopefunc) +from .relationships import ( + foreign, + remote, +) +from .session import ( + Session, + object_session, + sessionmaker, + make_transient +) +from .scoping import ( + scoped_session +) +from . import mapper as mapperlib +from .query import AliasOption, Query, Bundle +from ..util.langhelpers import public_factory +from .. import util as _sa_util +from . import strategies as _strategies def create_session(bind=None, **kwargs): """Create a new :class:`.Session` @@ -177,458 +103,14 @@ def create_session(bind=None, **kwargs): kwargs.setdefault('expire_on_commit', False) return Session(bind=bind, **kwargs) -def relationship(argument, secondary=None, **kwargs): - """Provide a relationship of a primary Mapper to a secondary Mapper. - - .. versionchanged:: 0.6 - :func:`relationship` is historically known as :func:`relation`. - - This corresponds to a parent-child or associative table relationship. The - constructed class is an instance of :class:`.RelationshipProperty`. - - A typical :func:`.relationship`, used in a classical mapping:: - - mapper(Parent, properties={ - 'children': relationship(Child) - }) - - Some arguments accepted by :func:`.relationship` optionally accept a - callable function, which when called produces the desired value. - The callable is invoked by the parent :class:`.Mapper` at "mapper initialization" - time, which happens only when mappers are first used, and is assumed - to be after all mappings have been constructed. This can be used - to resolve order-of-declaration and other dependency issues, such as - if ``Child`` is declared below ``Parent`` in the same file:: - - mapper(Parent, properties={ - "children":relationship(lambda: Child, - order_by=lambda: Child.id) - }) - - When using the :ref:`declarative_toplevel` extension, the Declarative - initializer allows string arguments to be passed to :func:`.relationship`. - These string arguments are converted into callables that evaluate - the string as Python code, using the Declarative - class-registry as a namespace. This allows the lookup of related - classes to be automatic via their string name, and removes the need to import - related classes at all into the local module space:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class Parent(Base): - __tablename__ = 'parent' - id = Column(Integer, primary_key=True) - children = relationship("Child", order_by="Child.id") - - A full array of examples and reference documentation regarding - :func:`.relationship` is at :ref:`relationship_config_toplevel`. - - :param argument: - a mapped class, or actual :class:`.Mapper` instance, representing the target of - the relationship. - - ``argument`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param secondary: - for a many-to-many relationship, specifies the intermediary - table, and is an instance of :class:`.Table`. The ``secondary`` keyword - argument should generally only - be used for a table that is not otherwise expressed in any class - mapping, unless this relationship is declared as view only, otherwise - conflicting persistence operations can occur. - - ``secondary`` may - also be passed as a callable function which is evaluated at - mapper initialization time. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - many-to-one reference should be loaded when replaced, if - not already loaded. Normally, history tracking logic for - simple many-to-ones only needs to be aware of the "new" - value in order to perform a flush. This flag is available - for applications that make use of - :func:`.attributes.get_history` which also need to know - the "previous" value of the attribute. - - :param backref: - indicates the string name of a property to be placed on the related - mapper's class that will handle this relationship in the other - direction. The other property will be created automatically - when the mappers are configured. Can also be passed as a - :func:`backref` object to control the configuration of the - new relationship. - - :param back_populates: - Takes a string name and has the same meaning as ``backref``, - except the complementing property is **not** created automatically, - and instead must be configured explicitly on the other mapper. The - complementing property should also indicate ``back_populates`` - to this relationship to ensure proper functioning. - - :param cascade: - a comma-separated list of cascade rules which determines how - Session operations should be "cascaded" from parent to child. - This defaults to ``False``, which means the default cascade - should be used. The default value is ``"save-update, merge"``. - - Available cascades are: - - * ``save-update`` - cascade the :meth:`.Session.add` - operation. This cascade applies both to future and - past calls to :meth:`~sqlalchemy.orm.session.Session.add`, - meaning new items added to a collection or scalar relationship - get placed into the same session as that of the parent, and - also applies to items which have been removed from this - relationship but are still part of unflushed history. - - * ``merge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.merge` - operation - - * ``expunge`` - cascade the :meth:`.Session.expunge` - operation - - * ``delete`` - cascade the :meth:`.Session.delete` - operation - - * ``delete-orphan`` - if an item of the child's type is - detached from its parent, mark it for deletion. - - .. versionchanged:: 0.7 - This option does not prevent - a new instance of the child object from being persisted - without a parent to start with; to constrain against - that case, ensure the child's foreign key column(s) - is configured as NOT NULL - - * ``refresh-expire`` - cascade the :meth:`.Session.expire` - and :meth:`~sqlalchemy.orm.session.Session.refresh` operations - - * ``all`` - shorthand for "save-update,merge, refresh-expire, - expunge, delete" - - See the section :ref:`unitofwork_cascades` for more background - on configuring cascades. - - :param cascade_backrefs=True: - a boolean value indicating if the ``save-update`` cascade should - operate along an assignment event intercepted by a backref. - When set to ``False``, - the attribute managed by this relationship will not cascade - an incoming transient object into the session of a - persistent parent, if the event is received via backref. - - That is:: - - mapper(A, a_table, properties={ - 'bs':relationship(B, backref="a", cascade_backrefs=False) - }) - - If an ``A()`` is present in the session, assigning it to - the "a" attribute on a transient ``B()`` will not place - the ``B()`` into the session. To set the flag in the other - direction, i.e. so that ``A().bs.append(B())`` won't add - a transient ``A()`` into the session for a persistent ``B()``:: - - mapper(A, a_table, properties={ - 'bs':relationship(B, - backref=backref("a", cascade_backrefs=False) - ) - }) - - See the section :ref:`unitofwork_cascades` for more background - on configuring cascades. - - :param collection_class: - a class or callable that returns a new list-holding object. will - be used in place of a plain list for storing elements. - Behavior of this attribute is described in detail at - :ref:`custom_collections`. - - :param comparator_factory: - a class which extends :class:`.RelationshipProperty.Comparator` which - provides custom SQL clause generation for comparison operations. - - :param doc: - docstring which will be applied to the resulting descriptor. - - :param extension: - an :class:`.AttributeExtension` instance, or list of extensions, - which will be prepended to the list of attribute listeners for - the resulting descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - :param foreign_keys: - a list of columns which are to be used as "foreign key" columns. - Normally, :func:`relationship` uses the :class:`.ForeignKey` - and :class:`.ForeignKeyConstraint` objects present within the - mapped or secondary :class:`.Table` to determine the "foreign" side of - the join condition. This is used to construct SQL clauses in order - to load objects, as well as to "synchronize" values from - primary key columns to referencing foreign key columns. - The ``foreign_keys`` parameter overrides the notion of what's - "foreign" in the table metadata, allowing the specification - of a list of :class:`.Column` objects that should be considered - part of the foreign key. - - There are only two use cases for ``foreign_keys`` - one, when it is not - convenient for :class:`.Table` metadata to contain its own foreign key - metadata (which should be almost never, unless reflecting a large amount of - tables from a MySQL MyISAM schema, or a schema that doesn't actually - have foreign keys on it). The other is for extremely - rare and exotic composite foreign key setups where some columns - should artificially not be considered as foreign. - - ``foreign_keys`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param innerjoin=False: - when ``True``, joined eager loads will use an inner join to join - against related tables instead of an outer join. The purpose - of this option is generally one of performance, as inner joins - generally perform better than outer joins. Another reason can be - the use of ``with_lockmode``, which does not support outer joins. - - This flag can be set to ``True`` when the relationship references an - object via many-to-one using local foreign keys that are not nullable, - or when the reference is one-to-one or a collection that is guaranteed - to have one or at least one entry. - - :param join_depth: - when non-``None``, an integer value indicating how many levels - deep "eager" loaders should join on a self-referring or cyclical - relationship. The number counts how many times the same Mapper - shall be present in the loading condition along a particular join - branch. When left at its default of ``None``, eager loaders - will stop chaining when they encounter a the same target mapper - which is already higher up in the chain. This option applies - both to joined- and subquery- eager loaders. - - :param lazy='select': specifies - how the related items should be loaded. Default value is - ``select``. Values include: - - * ``select`` - items should be loaded lazily when the property is first - accessed, using a separate SELECT statement, or identity map - fetch for simple many-to-one references. - - * ``immediate`` - items should be loaded as the parents are loaded, - using a separate SELECT statement, or identity map fetch for - simple many-to-one references. - - .. versionadded:: 0.6.5 - - * ``joined`` - items should be loaded "eagerly" in the same query as - that of the parent, using a JOIN or LEFT OUTER JOIN. Whether - the join is "outer" or not is determined by the ``innerjoin`` - parameter. - - * ``subquery`` - items should be loaded "eagerly" within the same - query as that of the parent, using a second SQL statement - which issues a JOIN to a subquery of the original - statement. - - * ``noload`` - no loading should occur at any time. This is to - support "write-only" attributes, or attributes which are - populated in some manner specific to the application. - - * ``dynamic`` - the attribute will return a pre-configured - :class:`~sqlalchemy.orm.query.Query` object for all read - operations, onto which further filtering operations can be - applied before iterating the results. See - the section :ref:`dynamic_relationship` for more details. - - * True - a synonym for 'select' - - * False - a synonym for 'joined' - - * None - a synonym for 'noload' - - Detailed discussion of loader strategies is at :ref:`loading_toplevel`. - - :param load_on_pending=False: - Indicates loading behavior for transient or pending parent objects. - - When set to ``True``, causes the lazy-loader to - issue a query for a parent object that is not persistent, meaning it has - never been flushed. This may take effect for a pending object when - autoflush is disabled, or for a transient object that has been - "attached" to a :class:`.Session` but is not part of its pending - collection. Attachment of transient objects to the session without - moving to the "pending" state is not a supported behavior at this time. - - Note that the load of related objects on a pending or transient object - also does not trigger any attribute change events - no user-defined - events will be emitted for these attributes, and if and when the - object is ultimately flushed, only the user-specific foreign key - attributes will be part of the modified state. - - The load_on_pending flag does not improve behavior - when the ORM is used normally - object references should be constructed - at the object level, not at the foreign key level, so that they - are present in an ordinary way before flush() proceeds. This flag - is not not intended for general use. - - New in 0.6.5. - - :param order_by: - indicates the ordering that should be applied when loading these - items. ``order_by`` is expected to refer to one of the :class:`.Column` - objects to which the target class is mapped, or - the attribute itself bound to the target class which refers - to the column. - - ``order_by`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param passive_deletes=False: - Indicates loading behavior during delete operations. - - A value of True indicates that unloaded child items should not - be loaded during a delete operation on the parent. Normally, - when a parent item is deleted, all child items are loaded so - that they can either be marked as deleted, or have their - foreign key to the parent set to NULL. Marking this flag as - True usually implies an ON DELETE rule is in - place which will handle updating/deleting child rows on the - database side. - - Additionally, setting the flag to the string value 'all' will - disable the "nulling out" of the child foreign keys, when there - is no delete or delete-orphan cascade enabled. This is - typically used when a triggering or error raise scenario is in - place on the database side. Note that the foreign key - attributes on in-session child objects will not be changed - after a flush occurs so this is a very special use-case - setting. - - :param passive_updates=True: - Indicates loading and INSERT/UPDATE/DELETE behavior when the - source of a foreign key value changes (i.e. an "on update" - cascade), which are typically the primary key columns of the - source row. - - When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will - handle propagation of an UPDATE from a source column to - dependent rows. Note that with databases which enforce - referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), - ON UPDATE CASCADE is required for this operation. The - relationship() will update the value of the attribute on related - items which are locally present in the session during a flush. - - When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The relationship() will issue the - appropriate UPDATE statements to the database in response to the - change of a referenced key, and items locally present in the - session during a flush will also be refreshed. - - This flag should probably be set to False if primary key changes - are expected and the database in use doesn't support CASCADE - (i.e. SQLite, MySQL MyISAM tables). - - Also see the passive_updates flag on ``mapper()``. - - A future SQLAlchemy release will provide a "detect" feature for - this flag. - - :param post_update: - this indicates that the relationship should be handled by a - second UPDATE statement after an INSERT or before a - DELETE. Currently, it also will issue an UPDATE after the - instance was UPDATEd as well, although this technically should - be improved. This flag is used to handle saving bi-directional - dependencies between two individual rows (i.e. each row - references the other), where it would otherwise be impossible to - INSERT or DELETE both rows fully since one row exists before the - other. Use this flag when a particular mapping arrangement will - incur two rows that are dependent on each other, such as a table - that has a one-to-many relationship to a set of child rows, and - also has a column that references a single child row within that - list (i.e. both tables contain a foreign key to each other). If - a ``flush()`` operation returns an error that a "cyclical - dependency" was detected, this is a cue that you might want to - use ``post_update`` to "break" the cycle. - - :param primaryjoin: - a SQL expression that will be used as the primary - join of this child object against the parent object, or in a - many-to-many relationship the join of the primary object to the - association table. By default, this value is computed based on the - foreign key relationships of the parent and child tables (or association - table). - - ``primaryjoin`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param remote_side: - used for self-referential relationships, indicates the column or - list of columns that form the "remote side" of the relationship. - - ``remote_side`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param query_class: - a :class:`.Query` subclass that will be used as the base of the - "appender query" returned by a "dynamic" relationship, that - is, a relationship that specifies ``lazy="dynamic"`` or was - otherwise constructed using the :func:`.orm.dynamic_loader` - function. - - :param secondaryjoin: - a SQL expression that will be used as the join of - an association table to the child object. By default, this value is - computed based on the foreign key relationships of the association and - child tables. - - ``secondaryjoin`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param single_parent=(True|False): - when True, installs a validator which will prevent objects - from being associated with more than one parent at a time. - This is used for many-to-one or many-to-many relationships that - should be treated either as one-to-one or one-to-many. Its - usage is optional unless delete-orphan cascade is also - set on this relationship(), in which case its required. - - :param uselist=(True|False): - a boolean that indicates if this property should be loaded as a - list or a scalar. In most cases, this value is determined - automatically by ``relationship()``, based on the type and direction - of the relationship - one to many forms a list, many to one - forms a scalar, many to many is a list. If a scalar is desired - where normally a list would be present, such as a bi-directional - one-to-one relationship, set uselist to False. - - :param viewonly=False: - when set to True, the relationship is used only for loading objects - within the relationship, and has no effect on the unit-of-work - flush process. Relationships with viewonly can specify any kind of - join conditions to provide additional views of related objects - onto a parent object. Note that the functionality of a viewonly - relationship has its limits - complicated join conditions may - not compile into eager or lazy loaders properly. If this is the - case, use an alternative method. - - """ - return RelationshipProperty(argument, secondary=secondary, **kwargs) +relationship = public_factory(RelationshipProperty, ".orm.relationship") def relation(*arg, **kw): """A synonym for :func:`relationship`.""" return relationship(*arg, **kw) + def dynamic_loader(argument, **kw): """Construct a dynamically-loading mapper property. @@ -648,130 +130,14 @@ def dynamic_loader(argument, **kw): kw['lazy'] = 'dynamic' return relationship(argument, **kw) -def column_property(*cols, **kw): - """Provide a column-level property for use with a Mapper. - Column-based properties can normally be applied to the mapper's - ``properties`` dictionary using the :class:`.Column` element directly. - Use this function when the given column is not directly present within the - mapper's selectable; examples include SQL expressions, functions, and - scalar SELECT queries. - - Columns that aren't present in the mapper's selectable won't be persisted - by the mapper and are effectively "read-only" attributes. - - :param \*cols: - list of Column objects to be mapped. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - scalar attribute should be loaded when replaced, if not - already loaded. Normally, history tracking logic for - simple non-primary-key scalar values only needs to be - aware of the "new" value in order to perform a flush. This - flag is available for applications that make use of - :func:`.attributes.get_history` or :meth:`.Session.is_modified` - which also need to know - the "previous" value of the attribute. - - .. versionadded:: 0.6.6 - - :param comparator_factory: a class which extends - :class:`.ColumnProperty.Comparator` which provides custom SQL clause - generation for comparison operations. - - :param group: - a group name for this property when marked as deferred. - - :param deferred: - when True, the column property is "deferred", meaning that - it does not load immediately, and is instead loaded when the - attribute is first accessed on an instance. See also - :func:`~sqlalchemy.orm.deferred`. - - :param doc: - optional string that will be applied as the doc on the - class-bound descriptor. - - :param expire_on_flush=True: - Disable expiry on flush. A column_property() which refers - to a SQL expression (and not a single table-bound column) - is considered to be a "read only" property; populating it - has no effect on the state of data, and it can only return - database state. For this reason a column_property()'s value - is expired whenever the parent object is involved in a - flush, that is, has any kind of "dirty" state within a flush. - Setting this parameter to ``False`` will have the effect of - leaving any existing value present after the flush proceeds. - Note however that the :class:`.Session` with default expiration - settings still expires - all attributes after a :meth:`.Session.commit` call, however. - - .. versionadded:: 0.7.3 - - :param extension: - an - :class:`.AttributeExtension` - instance, or list of extensions, which will be prepended - to the list of attribute listeners for the resulting - descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - - """ - - return ColumnProperty(*cols, **kw) - -def composite(class_, *cols, **kwargs): - """Return a composite column-based property for use with a Mapper. - - See the mapping documentation section :ref:`mapper_composite` for a full - usage example. - - :param class\_: - The "composite type" class. - - :param \*cols: - List of Column objects to be mapped. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - scalar attribute should be loaded when replaced, if not - already loaded. See the same flag on :func:`.column_property`. - - .. versionchanged:: 0.7 - This flag specifically becomes meaningful - - previously it was a placeholder. - - :param group: - A group name for this property when marked as deferred. - - :param deferred: - When True, the column property is "deferred", meaning that it does not - load immediately, and is instead loaded when the attribute is first - accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`. - - :param comparator_factory: a class which extends - :class:`.CompositeProperty.Comparator` which provides custom SQL clause - generation for comparison operations. - - :param doc: - optional string that will be applied as the doc on the - class-bound descriptor. - - :param extension: - an :class:`.AttributeExtension` instance, - or list of extensions, which will be prepended to the list of - attribute listeners for the resulting descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - """ - return CompositeProperty(class_, *cols, **kwargs) +column_property = public_factory(ColumnProperty, ".orm.column_property") +composite = public_factory(CompositeProperty, ".orm.composite") def backref(name, **kwargs): - """Create a back reference with explicit keyword arguments, which are the same - arguments one can send to :func:`relationship`. + """Create a back reference with explicit keyword arguments, which are the + same arguments one can send to :func:`relationship`. Used with the ``backref`` keyword argument to :func:`relationship` in place of a string argument, e.g.:: @@ -781,467 +147,43 @@ def backref(name, **kwargs): """ return (name, kwargs) -def deferred(*columns, **kwargs): - """Return a :class:`.DeferredColumnProperty`, which indicates this - object attributes should only be loaded from its corresponding - table column when first accessed. - Used with the "properties" dictionary sent to :func:`mapper`. +def deferred(*columns, **kw): + """Indicate a column-based mapped attribute that by default will + not load unless accessed. - See also: + :param \*columns: columns to be mapped. This is typically a single + :class:`.Column` object, however a collection is supported in order + to support multiple columns mapped under the same attribute. - :ref:`deferred` + :param \**kw: additional keyword arguments passed to :class:`.ColumnProperty`. + + .. seealso:: + + :ref:`deferred` """ - return ColumnProperty(deferred=True, *columns, **kwargs) + return ColumnProperty(deferred=True, *columns, **kw) -def mapper(class_, local_table=None, *args, **params): - """Return a new :class:`~.Mapper` object. - This function is typically used behind the scenes - via the Declarative extension. When using Declarative, - many of the usual :func:`.mapper` arguments are handled - by the Declarative extension itself, including ``class_``, - ``local_table``, ``properties``, and ``inherits``. - Other options are passed to :func:`.mapper` using - the ``__mapper_args__`` class variable:: +mapper = public_factory(Mapper, ".orm.mapper") - class MyClass(Base): - __tablename__ = 'my_table' - id = Column(Integer, primary_key=True) - type = Column(String(50)) - alt = Column("some_alt", Integer) +synonym = public_factory(SynonymProperty, ".orm.synonym") - __mapper_args__ = { - 'polymorphic_on' : type - } +comparable_property = public_factory(ComparableProperty, + ".orm.comparable_property") - Explicit use of :func:`.mapper` - is often referred to as *classical mapping*. The above - declarative example is equivalent in classical form to:: - - my_table = Table("my_table", metadata, - Column('id', Integer, primary_key=True), - Column('type', String(50)), - Column("some_alt", Integer) - ) - - class MyClass(object): - pass - - mapper(MyClass, my_table, - polymorphic_on=my_table.c.type, - properties={ - 'alt':my_table.c.some_alt - }) - - See also: - - :ref:`classical_mapping` - discussion of direct usage of - :func:`.mapper` - - :param class\_: The class to be mapped. When using Declarative, - this argument is automatically passed as the declared class - itself. - - :param local_table: The :class:`.Table` or other selectable - to which the class is mapped. May be ``None`` if - this mapper inherits from another mapper using single-table - inheritance. When using Declarative, this argument is - automatically passed by the extension, based on what - is configured via the ``__table__`` argument or via the :class:`.Table` - produced as a result of the ``__tablename__`` and :class:`.Column` - arguments present. - - :param always_refresh: If True, all query operations for this mapped - class will overwrite all data within object instances that already - exist within the session, erasing any in-memory changes with - whatever information was loaded from the database. Usage of this - flag is highly discouraged; as an alternative, see the method - :meth:`.Query.populate_existing`. - - :param allow_null_pks: This flag is deprecated - this is stated as - allow_partial_pks which defaults to True. - - :param allow_partial_pks: Defaults to True. Indicates that a - composite primary key with some NULL values should be considered as - possibly existing within the database. This affects whether a - mapper will assign an incoming row to an existing identity, as well - as if :meth:`.Session.merge` will check the database first for a - particular primary key value. A "partial primary key" can occur if - one has mapped to an OUTER JOIN, for example. - - :param batch: Defaults to ``True``, indicating that save operations - of multiple entities can be batched together for efficiency. - Setting to False indicates - that an instance will be fully saved before saving the next - instance. This is used in the extremely rare case that a - :class:`.MapperEvents` listener requires being called - in between individual row persistence operations. - - :param column_prefix: A string which will be prepended - to the mapped attribute name when :class:`.Column` - objects are automatically assigned as attributes to the - mapped class. Does not affect explicitly specified - column-based properties. - - See the section :ref:`column_prefix` for an example. - - :param concrete: If True, indicates this mapper should use concrete - table inheritance with its parent mapper. - - See the section :ref:`concrete_inheritance` for an example. - - :param exclude_properties: A list or set of string column names to - be excluded from mapping. - - See :ref:`include_exclude_cols` for an example. - - :param extension: A :class:`.MapperExtension` instance or - list of :class:`.MapperExtension` - instances which will be applied to all operations by this - :class:`.Mapper`. **Deprecated.** Please see :class:`.MapperEvents`. - - :param include_properties: An inclusive list or set of string column - names to map. - - See :ref:`include_exclude_cols` for an example. - - :param inherits: A mapped class or the corresponding :class:`.Mapper` - of one indicating a superclass to which this :class:`.Mapper` - should *inherit* from. The mapped class here must be a subclass of the - other mapper's class. When using Declarative, this argument - is passed automatically as a result of the natural class - hierarchy of the declared classes. - - See also: - - :ref:`inheritance_toplevel` - - :param inherit_condition: For joined table inheritance, a SQL - expression which will - define how the two tables are joined; defaults to a natural join - between the two tables. - - :param inherit_foreign_keys: When ``inherit_condition`` is used and the - columns present are missing a :class:`.ForeignKey` configuration, - this parameter can be used to specify which columns are "foreign". - In most cases can be left as ``None``. - - :param non_primary: Specify that this :class:`.Mapper` is in addition - to the "primary" mapper, that is, the one used for persistence. - The :class:`.Mapper` created here may be used for ad-hoc - mapping of the class to an alternate selectable, for loading - only. - - The ``non_primary`` feature is rarely needed with modern - usage. - - :param order_by: A single :class:`.Column` or list of :class:`.Column` - objects for which selection operations should use as the default - ordering for entities. By default mappers have no pre-defined - ordering. - - :param passive_updates: Indicates UPDATE behavior of foreign key - columns when a primary key column changes on a joined-table inheritance - mapping. Defaults to ``True``. - - When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will handle - propagation of an UPDATE from a source column to dependent columns - on joined-table rows. - - When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The :class:`.Mapper` here will - emit an UPDATE statement for the dependent columns during a - primary key change. - - See also: - - :ref:`passive_updates` - description of a similar feature as - used with :func:`.relationship` - - :param polymorphic_on: Specifies the column, attribute, or - SQL expression used to determine the target class for an - incoming row, when inheriting classes are present. - - This value is commonly a :class:`.Column` object that's - present in the mapped :class:`.Table`:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - - __mapper_args__ = { - "polymorphic_on":discriminator, - "polymorphic_identity":"employee" - } - - It may also be specified - as a SQL expression, as in this example where we - use the :func:`.case` construct to provide a conditional - approach:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - - __mapper_args__ = { - "polymorphic_on":case([ - (discriminator == "EN", "engineer"), - (discriminator == "MA", "manager"), - ], else_="employee"), - "polymorphic_identity":"employee" - } - - It may also refer to any attribute - configured with :func:`.column_property`, or to the - string name of one:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - employee_type = column_property( - case([ - (discriminator == "EN", "engineer"), - (discriminator == "MA", "manager"), - ], else_="employee") - ) - - __mapper_args__ = { - "polymorphic_on":employee_type, - "polymorphic_identity":"employee" - } - - .. versionchanged:: 0.7.4 - ``polymorphic_on`` may be specified as a SQL expression, - or refer to any attribute configured with - :func:`.column_property`, or to the string name of one. - - When setting ``polymorphic_on`` to reference an - attribute or expression that's not present in the - locally mapped :class:`.Table`, yet the value - of the discriminator should be persisted to the database, - the value of the - discriminator is not automatically set on new - instances; this must be handled by the user, - either through manual means or via event listeners. - A typical approach to establishing such a listener - looks like:: - - from sqlalchemy import event - from sqlalchemy.orm import object_mapper - - @event.listens_for(Employee, "init", propagate=True) - def set_identity(instance, *arg, **kw): - mapper = object_mapper(instance) - instance.discriminator = mapper.polymorphic_identity - - Where above, we assign the value of ``polymorphic_identity`` - for the mapped class to the ``discriminator`` attribute, - thus persisting the value to the ``discriminator`` column - in the database. - - See also: - - :ref:`inheritance_toplevel` - - :param polymorphic_identity: Specifies the value which - identifies this particular class as returned by the - column expression referred to by the ``polymorphic_on`` - setting. As rows are received, the value corresponding - to the ``polymorphic_on`` column expression is compared - to this value, indicating which subclass should - be used for the newly reconstructed object. - - :param properties: A dictionary mapping the string names of object - attributes to :class:`.MapperProperty` instances, which define the - persistence behavior of that attribute. Note that :class:`.Column` - objects present in - the mapped :class:`.Table` are automatically placed into - ``ColumnProperty`` instances upon mapping, unless overridden. - When using Declarative, this argument is passed automatically, - based on all those :class:`.MapperProperty` instances declared - in the declared class body. - - :param primary_key: A list of :class:`.Column` objects which define the - primary key to be used against this mapper's selectable unit. - This is normally simply the primary key of the ``local_table``, but - can be overridden here. - - :param version_id_col: A :class:`.Column` - that will be used to keep a running version id of mapped entities - in the database. This is used during save operations to ensure that - no other thread or process has updated the instance during the - lifetime of the entity, else a :class:`~sqlalchemy.orm.exc.StaleDataError` - exception is - thrown. By default the column must be of :class:`.Integer` type, - unless ``version_id_generator`` specifies a new generation - algorithm. - - :param version_id_generator: A callable which defines the algorithm - used to generate new version ids. Defaults to an integer - generator. Can be replaced with one that generates timestamps, - uuids, etc. e.g.:: - - import uuid - - class MyClass(Base): - __tablename__ = 'mytable' - id = Column(Integer, primary_key=True) - version_uuid = Column(String(32)) - - __mapper_args__ = { - 'version_id_col':version_uuid, - 'version_id_generator':lambda version:uuid.uuid4().hex - } - - The callable receives the current version identifier as its - single argument. - - :param with_polymorphic: A tuple in the form ``(, - )`` indicating the default style of "polymorphic" - loading, that is, which tables are queried at once. is - any single or list of mappers and/or classes indicating the - inherited classes that should be loaded at once. The special value - ``'*'`` may be used to indicate all descending classes should be - loaded immediately. The second tuple argument - indicates a selectable that will be used to query for multiple - classes. - - See also: - - :ref:`concrete_inheritance` - typically uses ``with_polymorphic`` - to specify a UNION statement to select from. - - :ref:`with_polymorphic` - usage example of the related - :meth:`.Query.with_polymorphic` method - - """ - return Mapper(class_, local_table, *args, **params) - -def synonym(name, map_column=False, descriptor=None, - comparator_factory=None, doc=None): - """Denote an attribute name as a synonym to a mapped property. - - .. versionchanged:: 0.7 - :func:`.synonym` is superseded by the :mod:`~sqlalchemy.ext.hybrid` - extension. See the documentation for hybrids - at :ref:`hybrids_toplevel`. - - Used with the ``properties`` dictionary sent to - :func:`~sqlalchemy.orm.mapper`:: - - class MyClass(object): - def _get_status(self): - return self._status - def _set_status(self, value): - self._status = value - status = property(_get_status, _set_status) - - mapper(MyClass, sometable, properties={ - "status":synonym("_status", map_column=True) - }) - - Above, the ``status`` attribute of MyClass will produce - expression behavior against the table column named ``status``, - using the Python attribute ``_status`` on the mapped class - to represent the underlying value. - - :param name: the name of the existing mapped property, which can be - any other ``MapperProperty`` including column-based properties and - relationships. - - :param map_column: if ``True``, an additional ``ColumnProperty`` is created - on the mapper automatically, using the synonym's name as the keyname of - the property, and the keyname of this ``synonym()`` as the name of the - column to map. - - """ - return SynonymProperty(name, map_column=map_column, - descriptor=descriptor, - comparator_factory=comparator_factory, - doc=doc) - -def comparable_property(comparator_factory, descriptor=None): - """Provides a method of applying a :class:`.PropComparator` - to any Python descriptor attribute. - - .. versionchanged:: 0.7 - :func:`.comparable_property` is superseded by - the :mod:`~sqlalchemy.ext.hybrid` extension. See the example - at :ref:`hybrid_custom_comparators`. - - Allows any Python descriptor to behave like a SQL-enabled - attribute when used at the class level in queries, allowing - redefinition of expression operator behavior. - - In the example below we redefine :meth:`.PropComparator.operate` - to wrap both sides of an expression in ``func.lower()`` to produce - case-insensitive comparison:: - - from sqlalchemy.orm import comparable_property - from sqlalchemy.orm.interfaces import PropComparator - from sqlalchemy.sql import func - from sqlalchemy import Integer, String, Column - from sqlalchemy.ext.declarative import declarative_base - - class CaseInsensitiveComparator(PropComparator): - def __clause_element__(self): - return self.prop - - def operate(self, op, other): - return op( - func.lower(self.__clause_element__()), - func.lower(other) - ) - - Base = declarative_base() - - class SearchWord(Base): - __tablename__ = 'search_word' - id = Column(Integer, primary_key=True) - word = Column(String) - word_insensitive = comparable_property(lambda prop, mapper: - CaseInsensitiveComparator(mapper.c.word, mapper) - ) - - - A mapping like the above allows the ``word_insensitive`` attribute - to render an expression like:: - - >>> print SearchWord.word_insensitive == "Trucks" - lower(search_word.word) = lower(:lower_1) - - :param comparator_factory: - A PropComparator subclass or factory that defines operator behavior - for this property. - - :param descriptor: - Optional when used in a ``properties={}`` declaration. The Python - descriptor or property to layer comparison behavior on top of. - - The like-named descriptor will be automatically retrieved from the - mapped class if left blank in a ``properties`` declaration. - - """ - return ComparableProperty(comparator_factory, descriptor) - -@sa_util.deprecated("0.7", message=":func:`.compile_mappers` " +@_sa_util.deprecated("0.7", message=":func:`.compile_mappers` " "is renamed to :func:`.configure_mappers`") def compile_mappers(): - """Initialize the inter-mapper relationships of all mappers that have been defined.""" + """Initialize the inter-mapper relationships of all mappers that have + been defined. + """ configure_mappers() + def clear_mappers(): """Remove all mappers from all classes. @@ -1262,7 +204,7 @@ def clear_mappers(): set of classes. """ - mapperlib._COMPILE_MUTEX.acquire() + mapperlib._CONFIGURE_MUTEX.acquire() try: while _mapper_registry: try: @@ -1272,414 +214,54 @@ def clear_mappers(): except KeyError: pass finally: - mapperlib._COMPILE_MUTEX.release() + mapperlib._CONFIGURE_MUTEX.release() -def joinedload(*keys, **kw): - """Return a ``MapperOption`` that will convert the property of the given - name or series of mapped attributes into an joined eager load. +from . import strategy_options - .. versionchanged:: 0.6beta3 - This function is known as :func:`eagerload` in all versions - of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 - series. :func:`eagerload` will remain available for the foreseeable - future in order to enable cross-compatibility. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - examples:: - - # joined-load the "orders" collection on "User" - query(User).options(joinedload(User.orders)) - - # joined-load the "keywords" collection on each "Item", - # but not the "items" collection on "Order" - those - # remain lazily loaded. - query(Order).options(joinedload(Order.items, Item.keywords)) - - # to joined-load across both, use joinedload_all() - query(Order).options(joinedload_all(Order.items, Item.keywords)) - - # set the default strategy to be 'joined' - query(Order).options(joinedload('*')) - - :func:`joinedload` also accepts a keyword argument `innerjoin=True` which - indicates using an inner join instead of an outer:: - - query(Order).options(joinedload(Order.user, innerjoin=True)) - - .. note:: - - The join created by :func:`joinedload` is anonymously aliased such that - it **does not affect the query results**. An :meth:`.Query.order_by` - or :meth:`.Query.filter` call **cannot** reference these aliased - tables - so-called "user space" joins are constructed using - :meth:`.Query.join`. The rationale for this is that :func:`joinedload` is only - applied in order to affect how related objects or collections are loaded - as an optimizing detail - it can be added or removed with no impact - on actual results. See the section :ref:`zen_of_eager_loading` for - a detailed description of how this is used, including how to use a single - explicit JOIN for filtering/ordering and eager loading simultaneously. - - See also: :func:`subqueryload`, :func:`lazyload` - - """ - innerjoin = kw.pop('innerjoin', None) - if innerjoin is not None: - return ( - strategies.EagerLazyOption(keys, lazy='joined'), - strategies.EagerJoinOption(keys, innerjoin) - ) - else: - return strategies.EagerLazyOption(keys, lazy='joined') - -def joinedload_all(*keys, **kw): - """Return a ``MapperOption`` that will convert all properties along the - given dot-separated path or series of mapped attributes - into an joined eager load. - - .. versionchanged:: 0.6beta3 - This function is known as :func:`eagerload_all` in all versions - of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 - series. :func:`eagerload_all` will remain available for the - foreseeable future in order to enable cross-compatibility. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - For example:: - - query.options(joinedload_all('orders.items.keywords'))... - - will set all of ``orders``, ``orders.items``, and ``orders.items.keywords`` to - load in one joined eager load. - - Individual descriptors are accepted as arguments as well:: - - query.options(joinedload_all(User.orders, Order.items, Item.keywords)) - - The keyword arguments accept a flag `innerjoin=True|False` which will - override the value of the `innerjoin` flag specified on the - relationship(). - - See also: :func:`subqueryload_all`, :func:`lazyload` - - """ - innerjoin = kw.pop('innerjoin', None) - if innerjoin is not None: - return ( - strategies.EagerLazyOption(keys, lazy='joined', chained=True), - strategies.EagerJoinOption(keys, innerjoin, chained=True) - ) - else: - return strategies.EagerLazyOption(keys, lazy='joined', chained=True) +joinedload = strategy_options.joinedload._unbound_fn +joinedload_all = strategy_options.joinedload._unbound_all_fn +contains_eager = strategy_options.contains_eager._unbound_fn +defer = strategy_options.defer._unbound_fn +undefer = strategy_options.undefer._unbound_fn +undefer_group = strategy_options.undefer_group._unbound_fn +load_only = strategy_options.load_only._unbound_fn +lazyload = strategy_options.lazyload._unbound_fn +lazyload_all = strategy_options.lazyload_all._unbound_all_fn +subqueryload = strategy_options.subqueryload._unbound_fn +subqueryload_all = strategy_options.subqueryload_all._unbound_all_fn +immediateload = strategy_options.immediateload._unbound_fn +noload = strategy_options.noload._unbound_fn +defaultload = strategy_options.defaultload._unbound_fn +from .strategy_options import Load def eagerload(*args, **kwargs): """A synonym for :func:`joinedload()`.""" return joinedload(*args, **kwargs) + def eagerload_all(*args, **kwargs): """A synonym for :func:`joinedload_all()`""" return joinedload_all(*args, **kwargs) -def subqueryload(*keys): - """Return a ``MapperOption`` that will convert the property - of the given name or series of mapped attributes - into an subquery eager load. - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - examples:: - # subquery-load the "orders" collection on "User" - query(User).options(subqueryload(User.orders)) +contains_alias = public_factory(AliasOption, ".orm.contains_alias") - # subquery-load the "keywords" collection on each "Item", - # but not the "items" collection on "Order" - those - # remain lazily loaded. - query(Order).options(subqueryload(Order.items, Item.keywords)) - # to subquery-load across both, use subqueryload_all() - query(Order).options(subqueryload_all(Order.items, Item.keywords)) - # set the default strategy to be 'subquery' - query(Order).options(subqueryload('*')) +def __go(lcls): + global __all__ + from .. import util as sa_util + from . import dynamic + from . import events + import inspect as _inspect - See also: :func:`joinedload`, :func:`lazyload` + __all__ = sorted(name for name, obj in lcls.items() + if not (name.startswith('_') or _inspect.ismodule(obj))) - """ - return strategies.EagerLazyOption(keys, lazy="subquery") + _sa_util.dependencies.resolve_all("sqlalchemy.orm") -def subqueryload_all(*keys): - """Return a ``MapperOption`` that will convert all properties along the - given dot-separated path or series of mapped attributes - into a subquery eager load. +__go(locals()) - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - For example:: - - query.options(subqueryload_all('orders.items.keywords'))... - - will set all of ``orders``, ``orders.items``, and ``orders.items.keywords`` to - load in one subquery eager load. - - Individual descriptors are accepted as arguments as well:: - - query.options(subqueryload_all(User.orders, Order.items, - Item.keywords)) - - See also: :func:`joinedload_all`, :func:`lazyload`, :func:`immediateload` - - """ - return strategies.EagerLazyOption(keys, lazy="subquery", chained=True) - -def lazyload(*keys): - """Return a ``MapperOption`` that will convert the property of the given - name or series of mapped attributes into a lazy load. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - See also: :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` - - """ - return strategies.EagerLazyOption(keys, lazy=True) - -def lazyload_all(*keys): - """Return a ``MapperOption`` that will convert all the properties - along the given dot-separated path or series of mapped attributes - into a lazy load. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - See also: :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` - - """ - return strategies.EagerLazyOption(keys, lazy=True, chained=True) - -def noload(*keys): - """Return a ``MapperOption`` that will convert the property of the - given name or series of mapped attributes into a non-load. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - See also: :func:`lazyload`, :func:`eagerload`, - :func:`subqueryload`, :func:`immediateload` - - """ - return strategies.EagerLazyOption(keys, lazy=None) - -def immediateload(*keys): - """Return a ``MapperOption`` that will convert the property of the given - name or series of mapped attributes into an immediate load. - - The "immediate" load means the attribute will be fetched - with a separate SELECT statement per parent in the - same way as lazy loading - except the loader is guaranteed - to be called at load time before the parent object - is returned in the result. - - The normal behavior of lazy loading applies - if - the relationship is a simple many-to-one, and the child - object is already present in the :class:`.Session`, - no SELECT statement will be emitted. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload` - - .. versionadded:: 0.6.5 - - """ - return strategies.EagerLazyOption(keys, lazy='immediate') - -def contains_alias(alias): - """Return a :class:`.MapperOption` that will indicate to the query that - the main table has been aliased. - - This is used in the very rare case that :func:`.contains_eager` - is being used in conjunction with a user-defined SELECT - statement that aliases the parent table. E.g.:: - - # define an aliased UNION called 'ulist' - statement = users.select(users.c.user_id==7).\\ - union(users.select(users.c.user_id>7)).\\ - alias('ulist') - - # add on an eager load of "addresses" - statement = statement.outerjoin(addresses).\\ - select().apply_labels() - - # create query, indicating "ulist" will be an - # alias for the main table, "addresses" - # property should be eager loaded - query = session.query(User).options( - contains_alias('ulist'), - contains_eager('addresses')) - - # then get results via the statement - results = query.from_statement(statement).all() - - :param alias: is the string name of an alias, or a - :class:`~.sql.expression.Alias` object representing - the alias. - - """ - return AliasOption(alias) - -def contains_eager(*keys, **kwargs): - """Return a ``MapperOption`` that will indicate to the query that - the given attribute should be eagerly loaded from columns currently - in the query. - - Used with :meth:`~sqlalchemy.orm.query.Query.options`. - - The option is used in conjunction with an explicit join that loads - the desired rows, i.e.:: - - sess.query(Order).\\ - join(Order.user).\\ - options(contains_eager(Order.user)) - - The above query would join from the ``Order`` entity to its related - ``User`` entity, and the returned ``Order`` objects would have the - ``Order.user`` attribute pre-populated. - - :func:`contains_eager` also accepts an `alias` argument, which is the - string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` - construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when - the eagerly-loaded rows are to come from an aliased table:: - - user_alias = aliased(User) - sess.query(Order).\\ - join((user_alias, Order.user)).\\ - options(contains_eager(Order.user, alias=user_alias)) - - See also :func:`eagerload` for the "automatic" version of this - functionality. - - For additional examples of :func:`contains_eager` see - :ref:`contains_eager`. - - """ - alias = kwargs.pop('alias', None) - if kwargs: - raise exceptions.ArgumentError('Invalid kwargs for contains_eag' - 'er: %r' % kwargs.keys()) - return strategies.EagerLazyOption(keys, lazy='joined', - propagate_to_loaders=False, chained=True), \ - strategies.LoadEagerFromAliasOption(keys, alias=alias, chained=True) - -def defer(*key): - """Return a :class:`.MapperOption` that will convert the column property - of the given name into a deferred load. - - Used with :meth:`.Query.options`. - - e.g.:: - - from sqlalchemy.orm import defer - - query(MyClass).options(defer("attribute_one"), - defer("attribute_two")) - - A class bound descriptor is also accepted:: - - query(MyClass).options( - defer(MyClass.attribute_one), - defer(MyClass.attribute_two)) - - A "path" can be specified onto a related or collection object using a - dotted name. The :func:`.orm.defer` option will be applied to that object - when loaded:: - - query(MyClass).options( - defer("related.attribute_one"), - defer("related.attribute_two")) - - To specify a path via class, send multiple arguments:: - - query(MyClass).options( - defer(MyClass.related, MyOtherClass.attribute_one), - defer(MyClass.related, MyOtherClass.attribute_two)) - - See also: - - :ref:`deferred` - - :param \*key: A key representing an individual path. Multiple entries - are accepted to allow a multiple-token path for a single target, not - multiple targets. - - """ - return strategies.DeferredOption(key, defer=True) - -def undefer(*key): - """Return a :class:`.MapperOption` that will convert the column property - of the given name into a non-deferred (regular column) load. - - Used with :meth:`.Query.options`. - - e.g.:: - - from sqlalchemy.orm import undefer - - query(MyClass).options(undefer("attribute_one"), - undefer("attribute_two")) - - A class bound descriptor is also accepted:: - - query(MyClass).options( - undefer(MyClass.attribute_one), - undefer(MyClass.attribute_two)) - - A "path" can be specified onto a related or collection object using a - dotted name. The :func:`.orm.undefer` option will be applied to that - object when loaded:: - - query(MyClass).options( - undefer("related.attribute_one"), - undefer("related.attribute_two")) - - To specify a path via class, send multiple arguments:: - - query(MyClass).options( - undefer(MyClass.related, MyOtherClass.attribute_one), - undefer(MyClass.related, MyOtherClass.attribute_two)) - - See also: - - :func:`.orm.undefer_group` as a means to "undefer" a group - of attributes at once. - - :ref:`deferred` - - :param \*key: A key representing an individual path. Multiple entries - are accepted to allow a multiple-token path for a single target, not - multiple targets. - - """ - return strategies.DeferredOption(key, defer=False) - -def undefer_group(name): - """Return a :class:`.MapperOption` that will convert the given group of deferred - column properties into a non-deferred (regular column) load. - - Used with :meth:`.Query.options`. - - e.g.:: - - query(MyClass).options(undefer("group_one")) - - See also: - - :ref:`deferred` - - :param name: String name of the deferred group. This name is - established using the "group" name to the :func:`.orm.deferred` - configurational function. - - """ - return strategies.UndeferGroupOption(name) - -from sqlalchemy import util as _sa_util -_sa_util.importlater.resolve_all() diff --git a/libs/sqlalchemy/orm/attributes.py b/libs/sqlalchemy/orm/attributes.py index b40abd57..e5f8550a 100644 --- a/libs/sqlalchemy/orm/attributes.py +++ b/libs/sqlalchemy/orm/attributes.py @@ -1,5 +1,5 @@ # orm/attributes.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -14,80 +14,51 @@ defines a large part of the ORM's interactivity. """ import operator -from operator import itemgetter +from .. import util, event, inspection +from . import interfaces, collections, exc as orm_exc -from sqlalchemy import util, event, exc as sa_exc -from sqlalchemy.orm import interfaces, collections, events, exc as orm_exc +from .base import instance_state, instance_dict, manager_of_class + +from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\ + NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\ + INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\ + PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\ + PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT +from .base import state_str, instance_str + +@inspection._self_inspects +class QueryableAttribute(interfaces._MappedAttribute, + interfaces._InspectionAttr, + interfaces.PropComparator): + """Base class for :term:`descriptor` objects that intercept + attribute events on behalf of a :class:`.MapperProperty` + object. The actual :class:`.MapperProperty` is accessible + via the :attr:`.QueryableAttribute.property` + attribute. -mapperutil = util.importlater("sqlalchemy.orm", "util") + .. seealso:: -PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT') -ATTR_WAS_SET = util.symbol('ATTR_WAS_SET') -ATTR_EMPTY = util.symbol('ATTR_EMPTY') -NO_VALUE = util.symbol('NO_VALUE') -NEVER_SET = util.symbol('NEVER_SET') + :class:`.InstrumentedAttribute` -PASSIVE_RETURN_NEVER_SET = util.symbol('PASSIVE_RETURN_NEVER_SET', -"""Symbol indicating that loader callables can be -fired off, but if no callable is applicable and no value is -present, the attribute should remain non-initialized. -NEVER_SET is returned in this case. -""") + :class:`.MapperProperty` -PASSIVE_NO_INITIALIZE = util.symbol('PASSIVE_NO_INITIALIZE', -"""Symbol indicating that loader callables should - not be fired off, and a non-initialized attribute - should remain that way. -""") + :attr:`.Mapper.all_orm_descriptors` -PASSIVE_NO_FETCH = util.symbol('PASSIVE_NO_FETCH', -"""Symbol indicating that loader callables should not emit SQL, - but a value can be fetched from the current session. + :attr:`.Mapper.attrs` + """ - Non-initialized attributes should be initialized to an empty value. - -""") - -PASSIVE_NO_FETCH_RELATED = util.symbol('PASSIVE_NO_FETCH_RELATED', -"""Symbol indicating that loader callables should not emit SQL for - loading a related object, but can refresh the attributes of the local - instance in order to locate a related object in the current session. - - Non-initialized attributes should be initialized to an empty value. - - The unit of work uses this mode to check if history is present - on many-to-one attributes with minimal SQL emitted. - -""") - -PASSIVE_ONLY_PERSISTENT = util.symbol('PASSIVE_ONLY_PERSISTENT', -"""Symbol indicating that loader callables should only fire off for - parent objects which are persistent (i.e., have a database - identity). - - Load operations for the "previous" value of an attribute make - use of this flag during change events. - -""") - -PASSIVE_OFF = util.symbol('PASSIVE_OFF', -"""Symbol indicating that loader callables should be executed - normally. - -""") - - -class QueryableAttribute(interfaces.PropComparator): - """Base class for class-bound attributes. """ + is_attribute = True def __init__(self, class_, key, impl=None, - comparator=None, parententity=None): + comparator=None, parententity=None, + of_type=None): self.class_ = class_ self.key = key self.impl = impl self.comparator = comparator - self.parententity = parententity + self._parententity = parententity + self._of_type = of_type manager = manager_of_class(class_) # manager is None in the case of AliasedClass @@ -98,9 +69,6 @@ class QueryableAttribute(interfaces.PropComparator): if key in base: self.dispatch._update(base[key].dispatch) - dispatch = event.dispatcher(events.AttributeEvents) - dispatch.dispatch_cls._active_history = False - @util.memoized_property def _supports_population(self): return self.impl.supports_population @@ -113,11 +81,91 @@ class QueryableAttribute(interfaces.PropComparator): # TODO: conditionally attach this method based on clause_element ? return self + + @util.memoized_property + def info(self): + """Return the 'info' dictionary for the underlying SQL element. + + The behavior here is as follows: + + * If the attribute is a column-mapped property, i.e. + :class:`.ColumnProperty`, which is mapped directly + to a schema-level :class:`.Column` object, this attribute + will return the :attr:`.SchemaItem.info` dictionary associated + with the core-level :class:`.Column` object. + + * If the attribute is a :class:`.ColumnProperty` but is mapped to + any other kind of SQL expression other than a :class:`.Column`, + the attribute will refer to the :attr:`.MapperProperty.info` dictionary + associated directly with the :class:`.ColumnProperty`, assuming the SQL + expression itself does not have it's own ``.info`` attribute + (which should be the case, unless a user-defined SQL construct + has defined one). + + * If the attribute refers to any other kind of :class:`.MapperProperty`, + including :class:`.RelationshipProperty`, the attribute will refer + to the :attr:`.MapperProperty.info` dictionary associated with + that :class:`.MapperProperty`. + + * To access the :attr:`.MapperProperty.info` dictionary of the :class:`.MapperProperty` + unconditionally, including for a :class:`.ColumnProperty` that's + associated directly with a :class:`.schema.Column`, the attribute + can be referred to using :attr:`.QueryableAttribute.property` + attribute, as ``MyClass.someattribute.property.info``. + + .. versionadded:: 0.8.0 + + .. seealso:: + + :attr:`.SchemaItem.info` + + :attr:`.MapperProperty.info` + + """ + return self.comparator.info + + @util.memoized_property + def parent(self): + """Return an inspection instance representing the parent. + + This will be either an instance of :class:`.Mapper` + or :class:`.AliasedInsp`, depending upon the nature + of the parent entity which this attribute is associated + with. + + """ + return inspection.inspect(self._parententity) + + @property + def expression(self): + return self.comparator.__clause_element__() + def __clause_element__(self): return self.comparator.__clause_element__() + def _query_clause_element(self): + """like __clause_element__(), but called specifically + by :class:`.Query` to allow special behavior.""" + + return self.comparator._query_clause_element() + + def adapt_to_entity(self, adapt_to_entity): + assert not self._of_type + return self.__class__(adapt_to_entity.entity, self.key, impl=self.impl, + comparator=self.comparator.adapt_to_entity(adapt_to_entity), + parententity=adapt_to_entity) + + def of_type(self, cls): + return QueryableAttribute( + self.class_, + self.key, + self.impl, + self.comparator.of_type(cls), + self._parententity, + of_type=cls) + def label(self, name): - return self.__clause_element__().label(name) + return self._query_clause_element().label(name) def operate(self, op, *other, **kwargs): return op(self.comparator, *other, **kwargs) @@ -133,9 +181,11 @@ class QueryableAttribute(interfaces.PropComparator): return getattr(self.comparator, key) except AttributeError: raise AttributeError( - 'Neither %r object nor %r object has an attribute %r' % ( + 'Neither %r object nor %r object associated with %s ' + 'has an attribute %r' % ( type(self).__name__, type(self.comparator).__name__, + self, key) ) @@ -144,11 +194,26 @@ class QueryableAttribute(interfaces.PropComparator): @util.memoized_property def property(self): + """Return the :class:`.MapperProperty` associated with this + :class:`.QueryableAttribute`. + + + Return values here will commonly be instances of + :class:`.ColumnProperty` or :class:`.RelationshipProperty`. + + + """ return self.comparator.property class InstrumentedAttribute(QueryableAttribute): - """Class bound instrumented attribute which adds descriptor methods.""" + """Class bound instrumented attribute which adds basic + :term:`descriptor` methods. + + See :class:`.QueryableAttribute` for a description of most features. + + + """ def __set__(self, instance, value): self.impl.set(instance_state(instance), @@ -165,7 +230,8 @@ class InstrumentedAttribute(QueryableAttribute): if self._supports_population and self.key in dict_: return dict_[self.key] else: - return self.impl.get(instance_state(instance),dict_) + return self.impl.get(instance_state(instance), dict_) + def create_proxied_attribute(descriptor): """Create an QueryableAttribute / user descriptor hybrid. @@ -184,13 +250,16 @@ def create_proxied_attribute(descriptor): """ - def __init__(self, class_, key, descriptor, comparator, - adapter=None, doc=None): + def __init__(self, class_, key, descriptor, + comparator, + adapt_to_entity=None, doc=None, + original_property=None): self.class_ = class_ self.key = key self.descriptor = descriptor + self.original_property = original_property self._comparator = comparator - self.adapter = adapter + self._adapt_to_entity = adapt_to_entity self.__doc__ = doc @property @@ -201,16 +270,15 @@ def create_proxied_attribute(descriptor): def comparator(self): if util.callable(self._comparator): self._comparator = self._comparator() - if self.adapter: - self._comparator = self._comparator.adapted(self.adapter) + if self._adapt_to_entity: + self._comparator = self._comparator.adapt_to_entity( + self._adapt_to_entity) return self._comparator - def adapted(self, adapter): - """Proxy adapted() for the use case of AliasedClass calling adapted.""" - - return self.__class__(self.class_, self.key, self.descriptor, + def adapt_to_entity(self, adapt_to_entity): + return self.__class__(adapt_to_entity.entity, self.key, self.descriptor, self._comparator, - adapter) + adapt_to_entity) def __get__(self, instance, owner): if instance is None: @@ -219,7 +287,7 @@ def create_proxied_attribute(descriptor): return self.descriptor.__get__(instance, owner) def __str__(self): - return self.key + return "%s.%s" % (self.class_.__name__, self.key) def __getattr__(self, attribute): """Delegate __getattr__ to the original descriptor and/or @@ -232,9 +300,11 @@ def create_proxied_attribute(descriptor): return getattr(self.comparator, attribute) except AttributeError: raise AttributeError( - 'Neither %r object nor %r object has an attribute %r' % ( + 'Neither %r object nor %r object associated with %s ' + 'has an attribute %r' % ( type(descriptor).__name__, type(self.comparator).__name__, + self, attribute) ) @@ -245,6 +315,54 @@ def create_proxied_attribute(descriptor): from_instance=descriptor) return Proxy +OP_REMOVE = util.symbol("REMOVE") +OP_APPEND = util.symbol("APPEND") +OP_REPLACE = util.symbol("REPLACE") + +class Event(object): + """A token propagated throughout the course of a chain of attribute + events. + + Serves as an indicator of the source of the event and also provides + a means of controlling propagation across a chain of attribute + operations. + + The :class:`.Event` object is sent as the ``initiator`` argument + when dealing with the :meth:`.AttributeEvents.append`, + :meth:`.AttributeEvents.set`, + and :meth:`.AttributeEvents.remove` events. + + The :class:`.Event` object is currently interpreted by the backref + event handlers, and is used to control the propagation of operations + across two mutually-dependent attributes. + + .. versionadded:: 0.9.0 + + """ + + impl = None + """The :class:`.AttributeImpl` which is the current event initiator. + """ + + op = None + """The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE` or :attr:`.OP_REPLACE`, + indicating the source operation. + + """ + + def __init__(self, attribute_impl, op): + self.impl = attribute_impl + self.op = op + self.parent_token = self.impl.parent_token + + + @property + def key(self): + return self.impl.key + + def hasparent(self, state): + return self.impl.hasparent(state) + class AttributeImpl(object): """internal implementation for instrumented attributes.""" @@ -252,6 +370,7 @@ class AttributeImpl(object): callable_, dispatch, trackparent=False, extension=None, compare_function=None, active_history=False, parent_token=None, expire_missing=True, + send_modified_events=True, **kwargs): """Construct an AttributeImpl. @@ -295,6 +414,10 @@ class AttributeImpl(object): during state.expire_attributes(None), if no value is present for this key. + send_modified_events + if False, the InstanceState._modified_event method will have no effect; + this means the attribute will never show up as changed in a + history entry. """ self.class_ = class_ self.key = key @@ -302,6 +425,7 @@ class AttributeImpl(object): self.dispatch = dispatch self.trackparent = trackparent self.parent_token = parent_token or self + self.send_modified_events = send_modified_events if compare_function is None: self.is_equal = operator.eq else: @@ -319,6 +443,9 @@ class AttributeImpl(object): self.expire_missing = expire_missing + def __str__(self): + return "%s.%s" % (self.class_.__name__, self.key) + def _get_active_history(self): """Backwards compat for impl.active_history""" @@ -329,7 +456,6 @@ class AttributeImpl(object): active_history = property(_get_active_history, _set_active_history) - def hasparent(self, state, optimistic=False): """Return the boolean value of a `hasparent` flag attached to the given state. @@ -346,7 +472,8 @@ class AttributeImpl(object): will also not have a `hasparent` flag. """ - assert self.trackparent, "This AttributeImpl is not configured to track parents." + msg = "This AttributeImpl is not configured to track parents." + assert self.trackparent, msg return state.parents.get(id(self.parent_token), optimistic) \ is not False @@ -357,7 +484,8 @@ class AttributeImpl(object): attribute represented by this ``InstrumentedAttribute``. """ - assert self.trackparent, "This AttributeImpl is not configured to track parents." + msg = "This AttributeImpl is not configured to track parents." + assert self.trackparent, msg id_ = id(self.parent_token) if value: @@ -376,15 +504,14 @@ class AttributeImpl(object): "but the parent record " "has gone stale, can't be sure this " "is the most recent parent." % - (mapperutil.state_str(state), - mapperutil.state_str(parent_state), + (state_str(state), + state_str(parent_state), self.key)) return state.parents[id_] = False - def set_callable(self, state, callable_): """Set a callable function for this attribute on the given object. @@ -431,7 +558,6 @@ class AttributeImpl(object): def get(self, state, dict_, passive=PASSIVE_OFF): """Retrieve a value from the given object. - If a callable is assembled on this object's attribute, and passive is False, the callable will be executed and the resulting value will be set as the new value for this attribute. @@ -443,12 +569,12 @@ class AttributeImpl(object): key = self.key if key not in state.committed_state or \ state.committed_state[key] is NEVER_SET: - if passive is PASSIVE_NO_INITIALIZE: + if not passive & CALLABLES_OK: return PASSIVE_NO_RESULT if key in state.callables: callable_ = state.callables[key] - value = callable_(passive) + value = callable_(state, passive) elif self.callable_: value = self.callable_(state, passive) else: @@ -468,7 +594,7 @@ class AttributeImpl(object): elif value is not ATTR_EMPTY: return self.set_committed_value(state, dict_, value) - if passive is PASSIVE_RETURN_NEVER_SET: + if not passive & INIT_OK: return NEVER_SET else: # Return a new, empty value @@ -505,15 +631,17 @@ class AttributeImpl(object): """set an attribute value on the given instance and 'commit' it.""" dict_[self.key] = value - state.commit(dict_, [self.key]) + state._commit(dict_, [self.key]) return value + class ScalarAttributeImpl(AttributeImpl): """represents a scalar value-holding InstrumentedAttribute.""" accepts_scalar_loader = True uses_objects = False supports_population = True + collection = False def delete(self, state, dict_): @@ -524,19 +652,24 @@ class ScalarAttributeImpl(AttributeImpl): old = dict_.get(self.key, NO_VALUE) if self.dispatch.remove: - self.fire_remove_event(state, dict_, old, None) - state.modified_event(dict_, self, old) + self.fire_remove_event(state, dict_, old, self._remove_token) + state._modified_event(dict_, self, old) del dict_[self.key] def get_history(self, state, dict_, passive=PASSIVE_OFF): - return History.from_scalar_attribute( - self, state, dict_.get(self.key, NO_VALUE)) + if self.key in dict_: + return History.from_scalar_attribute(self, state, dict_[self.key]) + else: + if passive & INIT_OK: + passive ^= INIT_OK + current = self.get(state, dict_, passive=passive) + if current is PASSIVE_NO_RESULT: + return HISTORY_BLANK + else: + return History.from_scalar_attribute(self, state, current) def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF, check_old=None, pop=False): - if initiator and initiator.parent_token is self.parent_token: - return - if self.dispatch._active_history: old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) else: @@ -545,79 +678,35 @@ class ScalarAttributeImpl(AttributeImpl): if self.dispatch.set: value = self.fire_replace_event(state, dict_, value, old, initiator) - state.modified_event(dict_, self, old) + state._modified_event(dict_, self, old) dict_[self.key] = value + @util.memoized_property + def _replace_token(self): + return Event(self, OP_REPLACE) + + @util.memoized_property + def _append_token(self): + return Event(self, OP_REPLACE) + + @util.memoized_property + def _remove_token(self): + return Event(self, OP_REMOVE) + def fire_replace_event(self, state, dict_, value, previous, initiator): for fn in self.dispatch.set: - value = fn(state, value, previous, initiator or self) + value = fn(state, value, previous, initiator or self._replace_token) return value def fire_remove_event(self, state, dict_, value, initiator): for fn in self.dispatch.remove: - fn(state, value, initiator or self) + fn(state, value, initiator or self._remove_token) @property def type(self): self.property.columns[0].type -class MutableScalarAttributeImpl(ScalarAttributeImpl): - """represents a scalar value-holding InstrumentedAttribute, which can - detect changes within the value itself. - - """ - - uses_objects = False - supports_population = True - - def __init__(self, class_, key, callable_, dispatch, - class_manager, copy_function=None, - compare_function=None, **kwargs): - super(ScalarAttributeImpl, self).__init__( - class_, - key, - callable_, dispatch, - compare_function=compare_function, - **kwargs) - class_manager.mutable_attributes.add(key) - if copy_function is None: - raise sa_exc.ArgumentError( - "MutableScalarAttributeImpl requires a copy function") - self.copy = copy_function - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - if not dict_: - v = state.committed_state.get(self.key, NO_VALUE) - else: - v = dict_.get(self.key, NO_VALUE) - - return History.from_scalar_attribute(self, state, v) - - def check_mutable_modified(self, state, dict_): - a, u, d = self.get_history(state, dict_) - return bool(a or d) - - def get(self, state, dict_, passive=PASSIVE_OFF): - if self.key not in state.mutable_dict: - ret = ScalarAttributeImpl.get(self, state, dict_, passive=passive) - if ret is not PASSIVE_NO_RESULT: - state.mutable_dict[self.key] = ret - return ret - else: - return state.mutable_dict[self.key] - - def delete(self, state, dict_): - ScalarAttributeImpl.delete(self, state, dict_) - state.mutable_dict.pop(self.key) - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, check_old=None, pop=False): - ScalarAttributeImpl.set(self, state, dict_, value, - initiator, passive, check_old=check_old, pop=pop) - state.mutable_dict[self.key] = value - - class ScalarObjectAttributeImpl(ScalarAttributeImpl): """represents a scalar-holding InstrumentedAttribute, where the target object is also instrumented. @@ -629,18 +718,19 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): accepts_scalar_loader = False uses_objects = True supports_population = True + collection = False def delete(self, state, dict_): old = self.get(state, dict_) - self.fire_remove_event(state, dict_, old, self) + self.fire_remove_event(state, dict_, old, self._remove_token) del dict_[self.key] def get_history(self, state, dict_, passive=PASSIVE_OFF): if self.key in dict_: return History.from_object_attribute(self, state, dict_[self.key]) else: - if passive is PASSIVE_OFF: - passive = PASSIVE_RETURN_NEVER_SET + if passive & INIT_OK: + passive ^= INIT_OK current = self.get(state, dict_, passive=passive) if current is PASSIVE_NO_RESULT: return HISTORY_BLANK @@ -669,14 +759,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): passive=PASSIVE_OFF, check_old=None, pop=False): """Set a value on the given InstanceState. - `initiator` is the ``InstrumentedAttribute`` that initiated the - ``set()`` operation and is used to control the depth of a circular - setter operation. - """ - if initiator and initiator.parent_token is self.parent_token: - return - if self.dispatch._active_history: old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) else: @@ -690,21 +773,22 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): else: raise ValueError( "Object %s not associated with %s on attribute '%s'" % ( - mapperutil.instance_str(check_old), - mapperutil.state_str(state), + instance_str(check_old), + state_str(state), self.key )) value = self.fire_replace_event(state, dict_, value, old, initiator) dict_[self.key] = value + def fire_remove_event(self, state, dict_, value, initiator): if self.trackparent and value is not None: self.sethasparent(instance_state(value), state, False) for fn in self.dispatch.remove: - fn(state, value, initiator or self) + fn(state, value, initiator or self._remove_token) - state.modified_event(dict_, self, value) + state._modified_event(dict_, self, value) def fire_replace_event(self, state, dict_, value, previous, initiator): if self.trackparent: @@ -714,9 +798,9 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): self.sethasparent(instance_state(previous), state, False) for fn in self.dispatch.set: - value = fn(state, value, previous, initiator or self) + value = fn(state, value, previous, initiator or self._replace_token) - state.modified_event(dict_, self, previous) + state._modified_event(dict_, self, previous) if self.trackparent: if value is not None: @@ -739,6 +823,7 @@ class CollectionAttributeImpl(AttributeImpl): accepts_scalar_loader = False uses_objects = True supports_population = True + collection = True def __init__(self, class_, key, callable_, dispatch, typecallable=None, trackparent=False, extension=None, @@ -758,7 +843,7 @@ class CollectionAttributeImpl(AttributeImpl): self.collection_factory = typecallable def __copy(self, item): - return [y for y in list(collections.collection_adapter(item))] + return [y for y in collections.collection_adapter(item)] def get_history(self, state, dict_, passive=PASSIVE_OFF): current = self.get(state, dict_, passive=passive) @@ -776,7 +861,7 @@ class CollectionAttributeImpl(AttributeImpl): if self.key in state.committed_state: original = state.committed_state[self.key] - if original is not NO_VALUE: + if original not in (NO_VALUE, NEVER_SET): current_states = [((c is not None) and instance_state(c) or None, c) for c in current] @@ -788,18 +873,28 @@ class CollectionAttributeImpl(AttributeImpl): original_set = dict(original_states) return \ - [(s, o) for s, o in current_states if s not in original_set] + \ - [(s, o) for s, o in current_states if s in original_set] + \ - [(s, o) for s, o in original_states if s not in current_set] + [(s, o) for s, o in current_states + if s not in original_set] + \ + [(s, o) for s, o in current_states + if s in original_set] + \ + [(s, o) for s, o in original_states + if s not in current_set] return [(instance_state(o), o) for o in current] + @util.memoized_property + def _append_token(self): + return Event(self, OP_APPEND) + + @util.memoized_property + def _remove_token(self): + return Event(self, OP_REMOVE) def fire_append_event(self, state, dict_, value, initiator): for fn in self.dispatch.append: - value = fn(state, value, initiator or self) + value = fn(state, value, initiator or self._append_token) - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) if self.trackparent and value is not None: self.sethasparent(instance_state(value), state, True) @@ -807,22 +902,22 @@ class CollectionAttributeImpl(AttributeImpl): return value def fire_pre_remove_event(self, state, dict_, initiator): - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) def fire_remove_event(self, state, dict_, value, initiator): if self.trackparent and value is not None: self.sethasparent(instance_state(value), state, False) for fn in self.dispatch.remove: - fn(state, value, initiator or self) + fn(state, value, initiator or self._remove_token) - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) def delete(self, state, dict_): if self.key not in dict_: return - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) collection = self.get_collection(state, state.dict) collection.clear_with_event() @@ -841,28 +936,22 @@ class CollectionAttributeImpl(AttributeImpl): self.key, state, self.collection_factory) def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator and initiator.parent_token is self.parent_token: - return - collection = self.get_collection(state, dict_, passive=passive) if collection is PASSIVE_NO_RESULT: value = self.fire_append_event(state, dict_, value, initiator) assert self.key not in dict_, \ "Collection was loaded during event handling." - state.get_pending(self.key).append(value) + state._get_pending_mutation(self.key).append(value) else: collection.append_with_event(value, initiator) def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator and initiator.parent_token is self.parent_token: - return - collection = self.get_collection(state, state.dict, passive=passive) if collection is PASSIVE_NO_RESULT: self.fire_remove_event(state, dict_, value, initiator) assert self.key not in dict_, \ "Collection was loaded during event handling." - state.get_pending(self.key).remove(value) + state._get_pending_mutation(self.key).remove(value) else: collection.remove_with_event(value, initiator) @@ -879,14 +968,8 @@ class CollectionAttributeImpl(AttributeImpl): passive=PASSIVE_OFF, pop=False): """Set a value on the given object. - `initiator` is the ``InstrumentedAttribute`` that initiated the - ``set()`` operation and is used to control the depth of a circular - setter operation. """ - if initiator and initiator.parent_token is self.parent_token: - return - self._set_iterable( state, dict_, value, lambda adapter, i: adapter.adapt_like_to_iterable(i)) @@ -918,7 +1001,7 @@ class CollectionAttributeImpl(AttributeImpl): return # place a copy of "old" in state.committed_state - state.modified_event(dict_, self, old, True) + state._modified_event(dict_, self, old, True) old_collection = getattr(old, '_sa_adapter') @@ -927,6 +1010,10 @@ class CollectionAttributeImpl(AttributeImpl): collections.bulk_replace(new_values, old_collection, new_collection) old_collection.unlink(old) + def _invalidate_collection(self, collection): + adapter = getattr(collection, '_sa_adapter') + adapter.invalidated = True + def set_committed_value(self, state, dict_, value): """Set an attribute value on the given instance and 'commit' it.""" @@ -937,14 +1024,14 @@ class CollectionAttributeImpl(AttributeImpl): state.dict[self.key] = user_data - state.commit(dict_, [self.key]) + state._commit(dict_, [self.key]) - if self.key in state.pending: + if self.key in state._pending_mutations: # pending items exist. issue a modified event, # add/remove new items. - state.modified_event(dict_, self, user_data, True) + state._modified_event(dict_, self, user_data, True) - pending = state.pending.pop(self.key) + pending = state._pending_mutations.pop(self.key) added = pending.added_items removed = pending.deleted_items for item in added: @@ -968,57 +1055,77 @@ class CollectionAttributeImpl(AttributeImpl): return getattr(user_data, '_sa_adapter') + def backref_listeners(attribute, key, uselist): """Apply listeners to synchronize a two-way relationship.""" # use easily recognizable names for stack traces parent_token = attribute.impl.parent_token + parent_impl = attribute.impl - def _acceptable_key_err(child_state, initiator): + def _acceptable_key_err(child_state, initiator, child_impl): raise ValueError( - "Object %s not associated with attribute of " - "type %s" % (mapperutil.state_str(child_state), - manager_of_class(initiator.class_)[initiator.key])) + "Bidirectional attribute conflict detected: " + 'Passing object %s to attribute "%s" ' + 'triggers a modify event on attribute "%s" ' + 'via the backref "%s".' % ( + state_str(child_state), + initiator.parent_token, + child_impl.parent_token, + attribute.impl.parent_token + ) + ) def emit_backref_from_scalar_set_event(state, child, oldchild, initiator): if oldchild is child: return child - if oldchild is not None and oldchild is not PASSIVE_NO_RESULT: # With lazy=None, there's no guarantee that the full collection is # present when updating via a backref. old_state, old_dict = instance_state(oldchild),\ instance_dict(oldchild) impl = old_state.manager[key].impl - impl.pop(old_state, - old_dict, - state.obj(), - initiator, passive=PASSIVE_NO_FETCH) + + if initiator.impl is not impl or \ + initiator.op not in (OP_REPLACE, OP_REMOVE): + impl.pop(old_state, + old_dict, + state.obj(), + parent_impl._append_token, + passive=PASSIVE_NO_FETCH) if child is not None: child_state, child_dict = instance_state(child),\ instance_dict(child) child_impl = child_state.manager[key].impl if initiator.parent_token is not parent_token and \ - initiator.parent_token is not child_impl.parent_token: - _acceptable_key_err(state, initiator) - child_impl.append( - child_state, - child_dict, - state.obj(), - initiator, - passive=PASSIVE_NO_FETCH) + initiator.parent_token is not child_impl.parent_token: + _acceptable_key_err(state, initiator, child_impl) + elif initiator.impl is not child_impl or \ + initiator.op not in (OP_APPEND, OP_REPLACE): + child_impl.append( + child_state, + child_dict, + state.obj(), + initiator, + passive=PASSIVE_NO_FETCH) return child def emit_backref_from_collection_append_event(state, child, initiator): + if child is None: + return + child_state, child_dict = instance_state(child), \ instance_dict(child) child_impl = child_state.manager[key].impl + if initiator.parent_token is not parent_token and \ - initiator.parent_token is not child_impl.parent_token: - _acceptable_key_err(state, initiator) - child_impl.append( + initiator.parent_token is not child_impl.parent_token: + _acceptable_key_err(state, initiator, child_impl) + elif initiator.impl is not child_impl or \ + initiator.op not in (OP_APPEND, OP_REPLACE): + child_impl.append( child_state, child_dict, state.obj(), @@ -1031,10 +1138,9 @@ def backref_listeners(attribute, key, uselist): child_state, child_dict = instance_state(child),\ instance_dict(child) child_impl = child_state.manager[key].impl - # can't think of a path that would produce an initiator - # mismatch here, as it would require an existing collection - # mismatch. - child_impl.pop( + if initiator.impl is not child_impl or \ + initiator.op not in (OP_REMOVE, OP_REPLACE): + child_impl.pop( child_state, child_dict, state.obj(), @@ -1059,35 +1165,40 @@ _NO_STATE_SYMBOLS = frozenset([ id(PASSIVE_NO_RESULT), id(NO_VALUE), id(NEVER_SET)]) -class History(tuple): + +History = util.namedtuple("History", [ + "added", "unchanged", "deleted" + ]) + + +class History(History): """A 3-tuple of added, unchanged and deleted values, representing the changes which have occurred on an instrumented attribute. - Each tuple member is an iterable sequence. + The easiest way to get a :class:`.History` object for a particular + attribute on an object is to use the :func:`.inspect` function:: + + from sqlalchemy import inspect + + hist = inspect(myobject).attrs.myattribute.history + + Each tuple member is an iterable sequence: + + * ``added`` - the collection of items added to the attribute (the first + tuple element). + + * ``unchanged`` - the collection of items that have not changed on the + attribute (the second tuple element). + + * ``deleted`` - the collection of items that have been removed from the + attribute (the third tuple element). """ - __slots__ = () - - added = property(itemgetter(0)) - """Return the collection of items added to the attribute (the first tuple - element).""" - - unchanged = property(itemgetter(1)) - """Return the collection of items that have not changed on the attribute - (the second tuple element).""" - - - deleted = property(itemgetter(2)) - """Return the collection of items that have been removed from the - attribute (the third tuple element).""" - - def __new__(cls, added, unchanged, deleted): - return tuple.__new__(cls, (added, unchanged, deleted)) - - def __nonzero__(self): + def __bool__(self): return self != HISTORY_BLANK + __nonzero__ = __bool__ def empty(self): """Return True if this :class:`.History` has no changes @@ -1142,7 +1253,7 @@ class History(tuple): original = state.committed_state.get(attribute.key, _NO_HISTORY) if original is _NO_HISTORY: - if current is NO_VALUE: + if current is NEVER_SET: return cls((), (), ()) else: return cls((), [current], ()) @@ -1159,7 +1270,7 @@ class History(tuple): deleted = () else: deleted = [original] - if current is NO_VALUE: + if current is NEVER_SET: return cls((), (), deleted) else: return cls([current], (), deleted) @@ -1193,18 +1304,23 @@ class History(tuple): @classmethod def from_collection(cls, attribute, state, current): original = state.committed_state.get(attribute.key, _NO_HISTORY) - current = getattr(current, '_sa_adapter') - if original is NO_VALUE: + if current is NO_VALUE or current is NEVER_SET: + return cls((), (), ()) + + current = getattr(current, '_sa_adapter') + if original in (NO_VALUE, NEVER_SET): return cls(list(current), (), ()) elif original is _NO_HISTORY: return cls((), list(current), ()) else: - current_states = [((c is not None) and instance_state(c) or None, c) + current_states = [((c is not None) and instance_state(c) + or None, c) for c in current ] - original_states = [((c is not None) and instance_state(c) or None, c) + original_states = [((c is not None) and instance_state(c) + or None, c) for c in original ] @@ -1219,6 +1335,7 @@ class History(tuple): HISTORY_BLANK = History(None, None, None) + def get_history(obj, key, passive=PASSIVE_OFF): """Return a :class:`.History` record for the given object and attribute key. @@ -1228,10 +1345,11 @@ def get_history(obj, key, passive=PASSIVE_OFF): :param key: string attribute name. - :param passive: indicates if the attribute should be - loaded from the database if not already present (:attr:`.PASSIVE_NO_FETCH`), and - if the attribute should be not initialized to a blank value otherwise - (:attr:`.PASSIVE_NO_INITIALIZE`). Default is :attr:`PASSIVE_OFF`. + :param passive: indicates loading behavior for the attribute + if the value is not already present. This is a + bitflag attribute, which defaults to the symbol + :attr:`.PASSIVE_OFF` indicating all necessary SQL + should be emitted. """ if passive is True: @@ -1245,6 +1363,7 @@ def get_history(obj, key, passive=PASSIVE_OFF): return get_state_history(instance_state(obj), key, passive) + def get_state_history(state, key, passive=PASSIVE_OFF): return state.get_history(key, passive) @@ -1255,6 +1374,7 @@ def has_parent(cls, obj, key, optimistic=False): state = instance_state(obj) return manager.has_parent(state, key, optimistic) + def register_attribute(class_, key, **kw): comparator = kw.pop('comparator', None) parententity = kw.pop('parententity', None) @@ -1264,9 +1384,10 @@ def register_attribute(class_, key, **kw): register_attribute_impl(class_, key, **kw) return desc + def register_attribute_impl(class_, key, uselist=False, callable_=None, - useobject=False, mutable_scalars=False, + useobject=False, impl_class=None, backref=None, **kw): manager = manager_of_class(class_) @@ -1286,10 +1407,7 @@ def register_attribute_impl(class_, key, typecallable=typecallable, **kw) elif useobject: impl = ScalarObjectAttributeImpl(class_, key, callable_, - dispatch,**kw) - elif mutable_scalars: - impl = MutableScalarAttributeImpl(class_, key, callable_, dispatch, - class_manager=manager, **kw) + dispatch, **kw) else: impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) @@ -1301,6 +1419,7 @@ def register_attribute_impl(class_, key, manager.post_configure_attribute(key) return manager[key] + def register_descriptor(class_, key, comparator=None, parententity=None, doc=None): manager = manager_of_class(class_) @@ -1313,9 +1432,11 @@ def register_descriptor(class_, key, comparator=None, manager.instrument_attribute(key, descriptor) return descriptor + def unregister_attribute(class_, key): manager_of_class(class_).uninstrument_attribute(key) + def init_collection(obj, key): """Initialize a collection attribute and return the collection adapter. @@ -1327,7 +1448,7 @@ def init_collection(obj, key): collection_adapter.append_without_event(elem) For an easier way to do the above, see - :func:`~sqlalchemy.orm.attributes.set_committed_value`. + :func:`~sqlalchemy.orm.attributes.set_committed_value`. obj is an instrumented object instance. An InstanceState is accepted directly for backwards compatibility but @@ -1338,6 +1459,7 @@ def init_collection(obj, key): dict_ = state.dict return init_state_collection(state, dict_, key) + def init_state_collection(state, dict_, key): """Initialize a collection attribute and return the collection adapter.""" @@ -1345,6 +1467,7 @@ def init_state_collection(state, dict_, key): user_data = attr.initialize(state, dict_) return attr.get_collection(state, dict_, user_data) + def set_committed_value(instance, key, value): """Set the value of an attribute with no history events. @@ -1363,6 +1486,7 @@ def set_committed_value(instance, key, value): state, dict_ = instance_state(instance), instance_dict(instance) state.manager[key].impl.set_committed_value(state, dict_, value) + def set_attribute(instance, key, value): """Set the value of an attribute, firing history events. @@ -1376,6 +1500,7 @@ def set_attribute(instance, key, value): state, dict_ = instance_state(instance), instance_dict(instance) state.manager[key].impl.set(state, dict_, value, None) + def get_attribute(instance, key): """Get the value of an attribute, firing any callables required. @@ -1389,6 +1514,7 @@ def get_attribute(instance, key): state, dict_ = instance_state(instance), instance_dict(instance) return state.manager[key].impl.get(state, dict_) + def del_attribute(instance, key): """Delete the value of an attribute, firing history events. @@ -1402,6 +1528,7 @@ def del_attribute(instance, key): state, dict_ = instance_state(instance), instance_dict(instance) state.manager[key].impl.delete(state, dict_) + def flag_modified(instance, key): """Mark an attribute on an instance as 'modified'. @@ -1411,5 +1538,4 @@ def flag_modified(instance, key): """ state, dict_ = instance_state(instance), instance_dict(instance) impl = state.manager[key].impl - state.modified_event(dict_, impl, NO_VALUE) - + state._modified_event(dict_, impl, NO_VALUE) diff --git a/libs/sqlalchemy/orm/base.py b/libs/sqlalchemy/orm/base.py new file mode 100644 index 00000000..e3b75eda --- /dev/null +++ b/libs/sqlalchemy/orm/base.py @@ -0,0 +1,453 @@ +# orm/base.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Constants and rudimental functions used throughout the ORM. + +""" + +from .. import util, inspection, exc as sa_exc +from ..sql import expression +from . import exc +import operator + +PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT', +"""Symbol returned by a loader callable or other attribute/history +retrieval operation when a value could not be determined, based +on loader callable flags. +""" +) + +ATTR_WAS_SET = util.symbol('ATTR_WAS_SET', +"""Symbol returned by a loader callable to indicate the +retrieved value, or values, were assigned to their attributes +on the target object. +""") + +ATTR_EMPTY = util.symbol('ATTR_EMPTY', +"""Symbol used internally to indicate an attribute had no callable. +""") + +NO_VALUE = util.symbol('NO_VALUE', +"""Symbol which may be placed as the 'previous' value of an attribute, +indicating no value was loaded for an attribute when it was modified, +and flags indicated we were not to load it. +""" +) + +NEVER_SET = util.symbol('NEVER_SET', +"""Symbol which may be placed as the 'previous' value of an attribute +indicating that the attribute had not been assigned to previously. +""" +) + +NO_CHANGE = util.symbol("NO_CHANGE", +"""No callables or SQL should be emitted on attribute access +and no state should change""", canonical=0 +) + +CALLABLES_OK = util.symbol("CALLABLES_OK", +"""Loader callables can be fired off if a value +is not present.""", canonical=1 +) + +SQL_OK = util.symbol("SQL_OK", +"""Loader callables can emit SQL at least on scalar value +attributes.""", canonical=2) + +RELATED_OBJECT_OK = util.symbol("RELATED_OBJECT_OK", +"""callables can use SQL to load related objects as well +as scalar value attributes. +""", canonical=4 +) + +INIT_OK = util.symbol("INIT_OK", +"""Attributes should be initialized with a blank +value (None or an empty collection) upon get, if no other +value can be obtained. +""", canonical=8 +) + +NON_PERSISTENT_OK = util.symbol("NON_PERSISTENT_OK", +"""callables can be emitted if the parent is not persistent.""", +canonical=16 +) + +LOAD_AGAINST_COMMITTED = util.symbol("LOAD_AGAINST_COMMITTED", +"""callables should use committed values as primary/foreign keys during a load +""", canonical=32 +) + +# pre-packaged sets of flags used as inputs +PASSIVE_OFF = util.symbol("PASSIVE_OFF", + "Callables can be emitted in all cases.", + canonical=(RELATED_OBJECT_OK | NON_PERSISTENT_OK | + INIT_OK | CALLABLES_OK | SQL_OK) +) +PASSIVE_RETURN_NEVER_SET = util.symbol("PASSIVE_RETURN_NEVER_SET", + """PASSIVE_OFF ^ INIT_OK""", + canonical=PASSIVE_OFF ^ INIT_OK +) +PASSIVE_NO_INITIALIZE = util.symbol("PASSIVE_NO_INITIALIZE", + "PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK", + canonical=PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK +) +PASSIVE_NO_FETCH = util.symbol("PASSIVE_NO_FETCH", + "PASSIVE_OFF ^ SQL_OK", + canonical=PASSIVE_OFF ^ SQL_OK +) +PASSIVE_NO_FETCH_RELATED = util.symbol("PASSIVE_NO_FETCH_RELATED", + "PASSIVE_OFF ^ RELATED_OBJECT_OK", + canonical=PASSIVE_OFF ^ RELATED_OBJECT_OK +) +PASSIVE_ONLY_PERSISTENT = util.symbol("PASSIVE_ONLY_PERSISTENT", + "PASSIVE_OFF ^ NON_PERSISTENT_OK", + canonical=PASSIVE_OFF ^ NON_PERSISTENT_OK +) + +DEFAULT_MANAGER_ATTR = '_sa_class_manager' +DEFAULT_STATE_ATTR = '_sa_instance_state' +_INSTRUMENTOR = ('mapper', 'instrumentor') + +EXT_CONTINUE = util.symbol('EXT_CONTINUE') +EXT_STOP = util.symbol('EXT_STOP') + +ONETOMANY = util.symbol('ONETOMANY', +"""Indicates the one-to-many direction for a :func:`.relationship`. + +This symbol is typically used by the internals but may be exposed within +certain API features. + +""") + +MANYTOONE = util.symbol('MANYTOONE', +"""Indicates the many-to-one direction for a :func:`.relationship`. + +This symbol is typically used by the internals but may be exposed within +certain API features. + +""") + +MANYTOMANY = util.symbol('MANYTOMANY', +"""Indicates the many-to-many direction for a :func:`.relationship`. + +This symbol is typically used by the internals but may be exposed within +certain API features. + +""") + +NOT_EXTENSION = util.symbol('NOT_EXTENSION', +"""Symbol indicating an :class:`_InspectionAttr` that's + not part of sqlalchemy.ext. + + Is assigned to the :attr:`._InspectionAttr.extension_type` + attibute. + +""") + +_none_set = frozenset([None]) + + +def _generative(*assertions): + """Mark a method as generative, e.g. method-chained.""" + + @util.decorator + def generate(fn, *args, **kw): + self = args[0]._clone() + for assertion in assertions: + assertion(self, fn.__name__) + fn(self, *args[1:], **kw) + return self + return generate + + +# these can be replaced by sqlalchemy.ext.instrumentation +# if augmented class instrumentation is enabled. +def manager_of_class(cls): + return cls.__dict__.get(DEFAULT_MANAGER_ATTR, None) + +instance_state = operator.attrgetter(DEFAULT_STATE_ATTR) + +instance_dict = operator.attrgetter('__dict__') + +def instance_str(instance): + """Return a string describing an instance.""" + + return state_str(instance_state(instance)) + +def state_str(state): + """Return a string describing an instance via its InstanceState.""" + + if state is None: + return "None" + else: + return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj())) + +def state_class_str(state): + """Return a string describing an instance's class via its InstanceState.""" + + if state is None: + return "None" + else: + return '<%s>' % (state.class_.__name__, ) + + +def attribute_str(instance, attribute): + return instance_str(instance) + "." + attribute + + +def state_attribute_str(state, attribute): + return state_str(state) + "." + attribute + +def object_mapper(instance): + """Given an object, return the primary Mapper associated with the object + instance. + + Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` + if no mapping is configured. + + This function is available via the inspection system as:: + + inspect(instance).mapper + + Using the inspection system will raise + :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is + not part of a mapping. + + """ + return object_state(instance).mapper + + +def object_state(instance): + """Given an object, return the :class:`.InstanceState` + associated with the object. + + Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` + if no mapping is configured. + + Equivalent functionality is available via the :func:`.inspect` + function as:: + + inspect(instance) + + Using the inspection system will raise + :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is + not part of a mapping. + + """ + state = _inspect_mapped_object(instance) + if state is None: + raise exc.UnmappedInstanceError(instance) + else: + return state + + +@inspection._inspects(object) +def _inspect_mapped_object(instance): + try: + return instance_state(instance) + # TODO: whats the py-2/3 syntax to catch two + # different kinds of exceptions at once ? + except exc.UnmappedClassError: + return None + except exc.NO_STATE: + return None + + + +def _class_to_mapper(class_or_mapper): + insp = inspection.inspect(class_or_mapper, False) + if insp is not None: + return insp.mapper + else: + raise exc.UnmappedClassError(class_or_mapper) + + +def _mapper_or_none(entity): + """Return the :class:`.Mapper` for the given class or None if the + class is not mapped.""" + + insp = inspection.inspect(entity, False) + if insp is not None: + return insp.mapper + else: + return None + + +def _is_mapped_class(entity): + """Return True if the given object is a mapped class, + :class:`.Mapper`, or :class:`.AliasedClass`.""" + + insp = inspection.inspect(entity, False) + return insp is not None and \ + hasattr(insp, "mapper") and \ + ( + insp.is_mapper + or insp.is_aliased_class + ) + +def _attr_as_key(attr): + if hasattr(attr, 'key'): + return attr.key + else: + return expression._column_as_key(attr) + + + +def _orm_columns(entity): + insp = inspection.inspect(entity, False) + if hasattr(insp, 'selectable'): + return [c for c in insp.selectable.c] + else: + return [entity] + + + +def _is_aliased_class(entity): + insp = inspection.inspect(entity, False) + return insp is not None and \ + getattr(insp, "is_aliased_class", False) + + +def _entity_descriptor(entity, key): + """Return a class attribute given an entity and string name. + + May return :class:`.InstrumentedAttribute` or user-defined + attribute. + + """ + insp = inspection.inspect(entity) + if insp.is_selectable: + description = entity + entity = insp.c + elif insp.is_aliased_class: + entity = insp.entity + description = entity + elif hasattr(insp, "mapper"): + description = entity = insp.mapper.class_ + else: + description = entity + + try: + return getattr(entity, key) + except AttributeError: + raise sa_exc.InvalidRequestError( + "Entity '%s' has no property '%s'" % + (description, key) + ) + +_state_mapper = util.dottedgetter('manager.mapper') + +@inspection._inspects(type) +def _inspect_mapped_class(class_, configure=False): + try: + class_manager = manager_of_class(class_) + if not class_manager.is_mapped: + return None + mapper = class_manager.mapper + if configure and mapper._new_mappers: + mapper._configure_all() + return mapper + + except exc.NO_STATE: + return None + +def class_mapper(class_, configure=True): + """Given a class, return the primary :class:`.Mapper` associated + with the key. + + Raises :exc:`.UnmappedClassError` if no mapping is configured + on the given class, or :exc:`.ArgumentError` if a non-class + object is passed. + + Equivalent functionality is available via the :func:`.inspect` + function as:: + + inspect(some_mapped_class) + + Using the inspection system will raise + :class:`sqlalchemy.exc.NoInspectionAvailable` if the class is not mapped. + + """ + mapper = _inspect_mapped_class(class_, configure=configure) + if mapper is None: + if not isinstance(class_, type): + raise sa_exc.ArgumentError( + "Class object expected, got '%r'." % class_) + raise exc.UnmappedClassError(class_) + else: + return mapper + + +class _InspectionAttr(object): + """A base class applied to all ORM objects that can be returned + by the :func:`.inspect` function. + + The attributes defined here allow the usage of simple boolean + checks to test basic facts about the object returned. + + While the boolean checks here are basically the same as using + the Python isinstance() function, the flags here can be used without + the need to import all of these classes, and also such that + the SQLAlchemy class system can change while leaving the flags + here intact for forwards-compatibility. + + """ + + is_selectable = False + """Return True if this object is an instance of :class:`.Selectable`.""" + + is_aliased_class = False + """True if this object is an instance of :class:`.AliasedClass`.""" + + is_instance = False + """True if this object is an instance of :class:`.InstanceState`.""" + + is_mapper = False + """True if this object is an instance of :class:`.Mapper`.""" + + is_property = False + """True if this object is an instance of :class:`.MapperProperty`.""" + + is_attribute = False + """True if this object is a Python :term:`descriptor`. + + This can refer to one of many types. Usually a + :class:`.QueryableAttribute` which handles attributes events on behalf + of a :class:`.MapperProperty`. But can also be an extension type + such as :class:`.AssociationProxy` or :class:`.hybrid_property`. + The :attr:`._InspectionAttr.extension_type` will refer to a constant + identifying the specific subtype. + + .. seealso:: + + :attr:`.Mapper.all_orm_descriptors` + + """ + + is_clause_element = False + """True if this object is an instance of :class:`.ClauseElement`.""" + + extension_type = NOT_EXTENSION + """The extension type, if any. + Defaults to :data:`.interfaces.NOT_EXTENSION` + + .. versionadded:: 0.8.0 + + .. seealso:: + + :data:`.HYBRID_METHOD` + + :data:`.HYBRID_PROPERTY` + + :data:`.ASSOCIATION_PROXY` + + """ + +class _MappedAttribute(object): + """Mixin for attributes which should be replaced by mapper-assigned + attributes. + + """ diff --git a/libs/sqlalchemy/orm/collections.py b/libs/sqlalchemy/orm/collections.py index e26a5973..87e351b6 100644 --- a/libs/sqlalchemy/orm/collections.py +++ b/libs/sqlalchemy/orm/collections.py @@ -1,5 +1,5 @@ # orm/collections.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -12,7 +12,7 @@ used, allowing arbitrary types (including built-ins) to be used as entity collections without requiring inheritance from a base class. Instrumentation decoration relays membership change events to the -``InstrumentedCollectionAttribute`` that is currently managing the collection. +:class:`.CollectionAttributeImpl` that is currently managing the collection. The decorators observe function call arguments and return values, tracking entities entering or leaving the collection. Two decorator approaches are provided. One is a bundle of generic decorators that map function arguments @@ -97,19 +97,19 @@ instrumentation may be the answer. Within your method, ``collection_adapter(self)`` will retrieve an object that you can use for explicit control over triggering append and remove events. -The owning object and InstrumentedCollectionAttribute are also reachable +The owning object and :class:`.CollectionAttributeImpl` are also reachable through the adapter, allowing for some very sophisticated behavior. """ -import copy import inspect import operator -import sys import weakref -from sqlalchemy.sql import expression -from sqlalchemy import schema, util, exc as sa_exc +from ..sql import expression +from .. import util, exc as sa_exc +from . import base + __all__ = ['collection', 'collection_adapter', 'mapped_collection', 'column_mapped_collection', @@ -138,8 +138,8 @@ class _PlainColumnGetter(object): return self.cols def __call__(self, value): - state = instance_state(value) - m = _state_mapper(state) + state = base.instance_state(value) + m = base._state_mapper(state) key = [ m._get_state_attr_by_column(state, state.dict, col) @@ -151,6 +151,7 @@ class _PlainColumnGetter(object): else: return key[0] + class _SerializableColumnGetter(object): """Column-based getter used in version 0.7.6 only. @@ -160,11 +161,13 @@ class _SerializableColumnGetter(object): def __init__(self, colkeys): self.colkeys = colkeys self.composite = len(colkeys) > 1 + def __reduce__(self): return _SerializableColumnGetter, (self.colkeys,) + def __call__(self, value): - state = instance_state(value) - m = _state_mapper(state) + state = base.instance_state(value) + m = base._state_mapper(state) key = [m._get_state_attr_by_column( state, state.dict, m.mapped_table.columns[k]) @@ -174,6 +177,7 @@ class _SerializableColumnGetter(object): else: return key[0] + class _SerializableColumnGetterV2(_PlainColumnGetter): """Updated serializable getter which deals with multi-table mapped classes. @@ -219,8 +223,9 @@ class _SerializableColumnGetterV2(_PlainColumnGetter): def column_mapped_collection(mapping_spec): """A dictionary-based collection type with column-based keying. - Returns a :class:`.MappedCollection` factory with a keying function generated - from mapping_spec, which may be a Column or a sequence of Columns. + Returns a :class:`.MappedCollection` factory with a keying function + generated from mapping_spec, which may be a Column or a sequence + of Columns. The key value must be immutable for the lifetime of the object. You can not, for example, map on foreign key values if those key values will @@ -228,16 +233,13 @@ def column_mapped_collection(mapping_spec): after a session flush. """ - global _state_mapper, instance_state - from sqlalchemy.orm.util import _state_mapper - from sqlalchemy.orm.attributes import instance_state - cols = [expression._only_column_elements(q, "mapping_spec") for q in util.to_list(mapping_spec) ] keyfunc = _PlainColumnGetter(cols) return lambda: MappedCollection(keyfunc) + class _SerializableAttrGetter(object): def __init__(self, name): self.name = name @@ -249,6 +251,7 @@ class _SerializableAttrGetter(object): def __reduce__(self): return _SerializableAttrGetter, (self.name, ) + def attribute_mapped_collection(attr_name): """A dictionary-based collection type with attribute-based keying. @@ -269,8 +272,9 @@ def attribute_mapped_collection(attr_name): def mapped_collection(keyfunc): """A dictionary-based collection type with arbitrary keying. - Returns a :class:`.MappedCollection` factory with a keying function generated - from keyfunc, a callable that takes an entity and returns a key value. + Returns a :class:`.MappedCollection` factory with a keying function + generated from keyfunc, a callable that takes an entity and returns a + key value. The key value must be immutable for the lifetime of the object. You can not, for example, map on foreign key values if those key values will @@ -280,13 +284,14 @@ def mapped_collection(keyfunc): """ return lambda: MappedCollection(keyfunc) + class collection(object): """Decorators for entity collection classes. The decorators fall into two groups: annotations and interception recipes. - The annotating decorators (appender, remover, iterator, - internally_instrumented, link) indicate the method's purpose and take no + The annotating decorators (appender, remover, iterator, linker, converter, + internally_instrumented) indicate the method's purpose and take no arguments. They are not written with parens:: @collection.appender @@ -346,7 +351,7 @@ class collection(object): promulgation to collection events. """ - setattr(fn, '_sa_instrument_role', 'appender') + fn._sa_instrument_role = 'appender' return fn @staticmethod @@ -373,7 +378,7 @@ class collection(object): promulgation to collection events. """ - setattr(fn, '_sa_instrument_role', 'remover') + fn._sa_instrument_role = 'remover' return fn @staticmethod @@ -387,17 +392,18 @@ class collection(object): def __iter__(self): ... """ - setattr(fn, '_sa_instrument_role', 'iterator') + fn._sa_instrument_role = 'iterator' return fn @staticmethod def internally_instrumented(fn): """Tag the method as instrumented. - This tag will prevent any decoration from being applied to the method. - Use this if you are orchestrating your own calls to :func:`.collection_adapter` - in one of the basic SQLAlchemy interface methods, or to prevent - an automatic ABC method decoration from wrapping your implementation:: + This tag will prevent any decoration from being applied to the + method. Use this if you are orchestrating your own calls to + :func:`.collection_adapter` in one of the basic SQLAlchemy + interface methods, or to prevent an automatic ABC method + decoration from wrapping your implementation:: # normally an 'extend' method on a list-like class would be # automatically intercepted and re-implemented in terms of @@ -407,12 +413,12 @@ class collection(object): def extend(self, items): ... """ - setattr(fn, '_sa_instrumented', True) + fn._sa_instrumented = True return fn @staticmethod - def link(fn): - """Tag the method as a the "linked to attribute" event handler. + def linker(fn): + """Tag the method as a "linked to attribute" event handler. This optional event handler will be called when the collection class is linked to or unlinked from the InstrumentedAttribute. It is @@ -421,9 +427,12 @@ class collection(object): that has been linked, or None if unlinking. """ - setattr(fn, '_sa_instrument_role', 'link') + fn._sa_instrument_role = 'linker' return fn + link = linker + """deprecated; synonym for :meth:`.collection.linker`.""" + @staticmethod def converter(fn): """Tag the method as the collection converter. @@ -454,7 +463,7 @@ class collection(object): validation on the values about to be assigned. """ - setattr(fn, '_sa_instrument_role', 'converter') + fn._sa_instrument_role = 'converter' return fn @staticmethod @@ -474,7 +483,7 @@ class collection(object): """ def decorator(fn): - setattr(fn, '_sa_instrument_before', ('fire_append_event', arg)) + fn._sa_instrument_before = ('fire_append_event', arg) return fn return decorator @@ -494,8 +503,8 @@ class collection(object): """ def decorator(fn): - setattr(fn, '_sa_instrument_before', ('fire_append_event', arg)) - setattr(fn, '_sa_instrument_after', 'fire_remove_event') + fn._sa_instrument_before = ('fire_append_event', arg) + fn._sa_instrument_after = 'fire_remove_event' return fn return decorator @@ -516,7 +525,7 @@ class collection(object): """ def decorator(fn): - setattr(fn, '_sa_instrument_before', ('fire_remove_event', arg)) + fn._sa_instrument_before = ('fire_remove_event', arg) return fn return decorator @@ -536,31 +545,13 @@ class collection(object): """ def decorator(fn): - setattr(fn, '_sa_instrument_after', 'fire_remove_event') + fn._sa_instrument_after = 'fire_remove_event' return fn return decorator -# public instrumentation interface for 'internally instrumented' -# implementations -def collection_adapter(collection): - """Fetch the :class:`.CollectionAdapter` for a collection.""" - - return getattr(collection, '_sa_adapter', None) - -def collection_iter(collection): - """Iterate over an object supporting the @iterator or __iter__ protocols. - - If the collection is an ORM collection, it need not be attached to an - object to be iterable. - - """ - try: - return getattr(collection, '_sa_iterator', - getattr(collection, '__iter__'))() - except AttributeError: - raise TypeError("'%s' object is not iterable" % - type(collection).__name__) +collection_adapter = operator.attrgetter('_sa_adapter') +"""Fetch the :class:`.CollectionAdapter` for a collection.""" class CollectionAdapter(object): @@ -573,16 +564,19 @@ class CollectionAdapter(object): The ORM uses :class:`.CollectionAdapter` exclusively for interaction with entity collections. - The usage of getattr()/setattr() is currently to allow injection - of custom methods, such as to unwrap Zope security proxies. """ + invalidated = False + def __init__(self, attr, owner_state, data): self._key = attr.key self._data = weakref.ref(data) self.owner_state = owner_state self.link_to_self(data) + def _warn_invalidated(self): + util.warn("This collection has been invalidated.") + @property def data(self): "The entity collection being adapted." @@ -593,16 +587,19 @@ class CollectionAdapter(object): return self.owner_state.manager[self._key].impl def link_to_self(self, data): - """Link a collection to this adapter, and fire a link event.""" - setattr(data, '_sa_adapter', self) - if hasattr(data, '_sa_on_link'): - getattr(data, '_sa_on_link')(self) + """Link a collection to this adapter""" + + data._sa_adapter = self + if data._sa_linker: + data._sa_linker(self) + def unlink(self, data): - """Unlink a collection from any adapter, and fire a link event.""" - setattr(data, '_sa_adapter', None) - if hasattr(data, '_sa_on_link'): - getattr(data, '_sa_on_link')(None) + """Unlink a collection from any adapter""" + + del data._sa_adapter + if data._sa_linker: + data._sa_linker(None) def adapt_like_to_iterable(self, obj): """Converts collection-compatible objects to an iterable of values. @@ -618,7 +615,7 @@ class CollectionAdapter(object): a default duck-typing-based implementation is used. """ - converter = getattr(self._data(), '_sa_converter', None) + converter = self._data()._sa_converter if converter is not None: return converter(obj) @@ -639,75 +636,78 @@ class CollectionAdapter(object): # If the object is an adapted collection, return the (iterable) # adapter. if getattr(obj, '_sa_adapter', None) is not None: - return getattr(obj, '_sa_adapter') + return obj._sa_adapter elif setting_type == dict: - # Py3K - #return obj.values() - # Py2K - return getattr(obj, 'itervalues', getattr(obj, 'values'))() - # end Py2K + if util.py3k: + return obj.values() + else: + return getattr(obj, 'itervalues', obj.values)() else: return iter(obj) def append_with_event(self, item, initiator=None): """Add an entity to the collection, firing mutation events.""" - getattr(self._data(), '_sa_appender')(item, _sa_initiator=initiator) + self._data()._sa_appender(item, _sa_initiator=initiator) def append_without_event(self, item): """Add or restore an entity to the collection, firing no events.""" - getattr(self._data(), '_sa_appender')(item, _sa_initiator=False) + self._data()._sa_appender(item, _sa_initiator=False) def append_multiple_without_event(self, items): """Add or restore an entity to the collection, firing no events.""" - appender = getattr(self._data(), '_sa_appender') + appender = self._data()._sa_appender for item in items: appender(item, _sa_initiator=False) def remove_with_event(self, item, initiator=None): """Remove an entity from the collection, firing mutation events.""" - getattr(self._data(), '_sa_remover')(item, _sa_initiator=initiator) + self._data()._sa_remover(item, _sa_initiator=initiator) def remove_without_event(self, item): """Remove an entity from the collection, firing no events.""" - getattr(self._data(), '_sa_remover')(item, _sa_initiator=False) + self._data()._sa_remover(item, _sa_initiator=False) def clear_with_event(self, initiator=None): """Empty the collection, firing a mutation event for each entity.""" - remover = getattr(self._data(), '_sa_remover') + remover = self._data()._sa_remover for item in list(self): remover(item, _sa_initiator=initiator) def clear_without_event(self): """Empty the collection, firing no events.""" - remover = getattr(self._data(), '_sa_remover') + remover = self._data()._sa_remover for item in list(self): remover(item, _sa_initiator=False) def __iter__(self): """Iterate over entities in the collection.""" - # Py3K requires iter() here - return iter(getattr(self._data(), '_sa_iterator')()) + return iter(self._data()._sa_iterator()) def __len__(self): """Count entities in the collection.""" - return len(list(getattr(self._data(), '_sa_iterator')())) + return len(list(self._data()._sa_iterator())) - def __nonzero__(self): + def __bool__(self): return True + __nonzero__ = __bool__ + def fire_append_event(self, item, initiator=None): """Notify that a entity has entered the collection. - Initiator is a token owned by the InstrumentedAttribute that initiated the membership - mutation, and should be left as None unless you are passing along - an initiator value from a chained operation. + Initiator is a token owned by the InstrumentedAttribute that + initiated the membership mutation, and should be left as None + unless you are passing along an initiator value from a chained + operation. """ - if initiator is not False and item is not None: + if initiator is not False: + if self.invalidated: + self._warn_invalidated() return self.attr.fire_append_event( self.owner_state, self.owner_state.dict, @@ -723,7 +723,9 @@ class CollectionAdapter(object): an initiator value from a chained operation. """ - if initiator is not False and item is not None: + if initiator is not False: + if self.invalidated: + self._warn_invalidated() self.attr.fire_remove_event( self.owner_state, self.owner_state.dict, @@ -736,6 +738,8 @@ class CollectionAdapter(object): fire_remove_event(). """ + if self.invalidated: + self._warn_invalidated() self.attr.fire_pre_remove_event( self.owner_state, self.owner_state.dict, @@ -762,9 +766,11 @@ def bulk_replace(values, existing_adapter, new_adapter): :param values: An iterable of collection member instances - :param existing_adapter: A :class:`.CollectionAdapter` of instances to be replaced + :param existing_adapter: A :class:`.CollectionAdapter` of + instances to be replaced - :param new_adapter: An empty :class:`.CollectionAdapter` to load with ``values`` + :param new_adapter: An empty :class:`.CollectionAdapter` + to load with ``values`` """ @@ -772,9 +778,10 @@ def bulk_replace(values, existing_adapter, new_adapter): values = list(values) idset = util.IdentitySet - constants = idset(existing_adapter or ()).intersection(values or ()) + existing_idset = idset(existing_adapter or ()) + constants = existing_idset.intersection(values or ()) additions = idset(values or ()).difference(constants) - removals = idset(existing_adapter or ()).difference(constants) + removals = existing_idset.difference(constants) for member in values or (): if member in additions: @@ -786,6 +793,7 @@ def bulk_replace(values, existing_adapter, new_adapter): for member in removals: existing_adapter.remove_with_event(member) + def prepare_instrumentation(factory): """Prepare a callable for future use as a collection class factory. @@ -807,7 +815,7 @@ def prepare_instrumentation(factory): # Did factory callable return a builtin? if cls in __canned_instrumentation: # Wrap it so that it returns our 'Instrumented*' - factory = __converting_factory(factory) + factory = __converting_factory(cls, factory) cls = factory() # Instrument the class if needed. @@ -820,51 +828,28 @@ def prepare_instrumentation(factory): return factory -def __converting_factory(original_factory): - """Convert the type returned by collection factories on the fly. - Given a collection factory that returns a builtin type (e.g. a list), - return a wrapped function that converts that type to one of our - instrumented types. +def __converting_factory(specimen_cls, original_factory): + """Return a wrapper that converts a "canned" collection like + set, dict, list into the Instrumented* version. """ + + instrumented_cls = __canned_instrumentation[specimen_cls] + def wrapper(): collection = original_factory() - type_ = type(collection) - if type_ in __canned_instrumentation: - # return an instrumented type initialized from the factory's - # collection - return __canned_instrumentation[type_](collection) - else: - raise sa_exc.InvalidRequestError( - "Collection class factories must produce instances of a " - "single class.") - try: - # often flawed but better than nothing - wrapper.__name__ = "%sWrapper" % original_factory.__name__ - wrapper.__doc__ = original_factory.__doc__ - except: - pass + return instrumented_cls(collection) + + # often flawed but better than nothing + wrapper.__name__ = "%sWrapper" % original_factory.__name__ + wrapper.__doc__ = original_factory.__doc__ + return wrapper def _instrument_class(cls): """Modify methods in a class and install instrumentation.""" - # TODO: more formally document this as a decoratorless/Python 2.3 - # option for specifying instrumentation. (likely doc'd here in code only, - # not in online docs.) Useful for C types too. - # - # __instrumentation__ = { - # 'rolename': 'methodname', # ... - # 'methods': { - # 'methodname': ('fire_{append,remove}_event', argspec, - # 'fire_{append,remove}_event'), - # 'append': ('fire_append_event', 1, None), - # '__setitem__': ('fire_append_event', 1, 'fire_remove_event'), - # 'pop': (None, None, 'fire_remove_event'), - # } - # } - # In the normal call flow, a request for any of the 3 basic collection # types is transformed into one of our trivial subclasses # (e.g. InstrumentedList). Catch anything else that sneaks in here... @@ -873,53 +858,55 @@ def _instrument_class(cls): "Can not instrument a built-in type. Use a " "subclass, even a trivial one.") + roles = {} + methods = {} + + # search for _sa_instrument_role-decorated methods in + # method resolution order, assign to roles + for supercls in cls.__mro__: + for name, method in vars(supercls).items(): + if not util.callable(method): + continue + + # note role declarations + if hasattr(method, '_sa_instrument_role'): + role = method._sa_instrument_role + assert role in ('appender', 'remover', 'iterator', + 'linker', 'converter') + roles.setdefault(role, name) + + # transfer instrumentation requests from decorated function + # to the combined queue + before, after = None, None + if hasattr(method, '_sa_instrument_before'): + op, argument = method._sa_instrument_before + assert op in ('fire_append_event', 'fire_remove_event') + before = op, argument + if hasattr(method, '_sa_instrument_after'): + op = method._sa_instrument_after + assert op in ('fire_append_event', 'fire_remove_event') + after = op + if before: + methods[name] = before[0], before[1], after + elif after: + methods[name] = None, None, after + + # see if this class has "canned" roles based on a known + # collection type (dict, set, list). Apply those roles + # as needed to the "roles" dictionary, and also + # prepare "decorator" methods collection_type = util.duck_type_collection(cls) if collection_type in __interfaces: - roles = __interfaces[collection_type].copy() - decorators = roles.pop('_decorators', {}) - else: - roles, decorators = {}, {} + canned_roles, decorators = __interfaces[collection_type] + for role, name in canned_roles.items(): + roles.setdefault(role, name) - if hasattr(cls, '__instrumentation__'): - roles.update(copy.deepcopy(getattr(cls, '__instrumentation__'))) - - methods = roles.pop('methods', {}) - - for name in dir(cls): - method = getattr(cls, name, None) - if not util.callable(method): - continue - - # note role declarations - if hasattr(method, '_sa_instrument_role'): - role = method._sa_instrument_role - assert role in ('appender', 'remover', 'iterator', - 'link', 'converter') - roles[role] = name - - # transfer instrumentation requests from decorated function - # to the combined queue - before, after = None, None - if hasattr(method, '_sa_instrument_before'): - op, argument = method._sa_instrument_before - assert op in ('fire_append_event', 'fire_remove_event') - before = op, argument - if hasattr(method, '_sa_instrument_after'): - op = method._sa_instrument_after - assert op in ('fire_append_event', 'fire_remove_event') - after = op - if before: - methods[name] = before[0], before[1], after - elif after: - methods[name] = None, None, after - - # apply ABC auto-decoration to methods that need it - - for method, decorator in decorators.items(): - fn = getattr(cls, method, None) - if (fn and method not in methods and - not hasattr(fn, '_sa_instrumented')): - setattr(cls, method, decorator(fn)) + # apply ABC auto-decoration to methods that need it + for method, decorator in decorators.items(): + fn = getattr(cls, method, None) + if (fn and method not in methods and + not hasattr(fn, '_sa_instrumented')): + setattr(cls, method, decorator(fn)) # ensure all roles are present, and apply implicit instrumentation if # needed @@ -946,15 +933,21 @@ def _instrument_class(cls): # apply ad-hoc instrumentation from decorators, class-level defaults # and implicit role declarations - for method, (before, argument, after) in methods.items(): - setattr(cls, method, - _instrument_membership_mutator(getattr(cls, method), + for method_name, (before, argument, after) in methods.items(): + setattr(cls, method_name, + _instrument_membership_mutator(getattr(cls, method_name), before, argument, after)) # intern the role map - for role, method in roles.items(): - setattr(cls, '_sa_%s' % role, getattr(cls, method)) + for role, method_name in roles.items(): + setattr(cls, '_sa_%s' % role, getattr(cls, method_name)) + + cls._sa_adapter = None + if not hasattr(cls, '_sa_linker'): + cls._sa_linker = None + if not hasattr(cls, '_sa_converter'): + cls._sa_converter = None + cls._sa_instrumented = id(cls) - setattr(cls, '_sa_instrumented', id(cls)) def _instrument_membership_mutator(method, before, argument, after): """Route method args and/or return value through the collection adapter.""" @@ -992,7 +985,7 @@ def _instrument_membership_mutator(method, before, argument, after): if initiator is False: executor = None else: - executor = getattr(args[0], '_sa_adapter', None) + executor = args[0]._sa_adapter if before and executor: getattr(executor, before)(value, initiator) @@ -1004,42 +997,46 @@ def _instrument_membership_mutator(method, before, argument, after): if res is not None: getattr(executor, after)(res, initiator) return res - try: - wrapper._sa_instrumented = True - wrapper.__name__ = method.__name__ - wrapper.__doc__ = method.__doc__ - except: - pass + + wrapper._sa_instrumented = True + if hasattr(method, "_sa_instrument_role"): + wrapper._sa_instrument_role = method._sa_instrument_role + wrapper.__name__ = method.__name__ + wrapper.__doc__ = method.__doc__ return wrapper + def __set(collection, item, _sa_initiator=None): """Run set events, may eventually be inlined into decorators.""" - if _sa_initiator is not False and item is not None: - executor = getattr(collection, '_sa_adapter', None) + if _sa_initiator is not False: + executor = collection._sa_adapter if executor: - item = getattr(executor, 'fire_append_event')(item, _sa_initiator) + item = executor.fire_append_event(item, _sa_initiator) return item + def __del(collection, item, _sa_initiator=None): """Run del events, may eventually be inlined into decorators.""" - if _sa_initiator is not False and item is not None: - executor = getattr(collection, '_sa_adapter', None) + if _sa_initiator is not False: + executor = collection._sa_adapter if executor: - getattr(executor, 'fire_remove_event')(item, _sa_initiator) + executor.fire_remove_event(item, _sa_initiator) + def __before_delete(collection, _sa_initiator=None): """Special method to run 'commit existing value' methods""" - executor = getattr(collection, '_sa_adapter', None) + executor = collection._sa_adapter if executor: - getattr(executor, 'fire_pre_remove_event')(_sa_initiator) + executor.fire_pre_remove_event(_sa_initiator) + def _list_decorators(): """Tailored instrumentation wrappers for any list-like class.""" def _tidy(fn): - setattr(fn, '_sa_instrumented', True) - fn.__doc__ = getattr(getattr(list, fn.__name__), '__doc__') + fn._sa_instrumented = True + fn.__doc__ = getattr(list, fn.__name__).__doc__ def append(fn): def append(self, item, _sa_initiator=None): @@ -1078,19 +1075,22 @@ def _list_decorators(): start = index.start or 0 if start < 0: start += len(self) - stop = index.stop or len(self) + if index.stop is not None: + stop = index.stop + else: + stop = len(self) if stop < 0: stop += len(self) if step == 1: - for i in xrange(start, stop, step): + for i in range(start, stop, step): if len(self) > start: del self[start] for i, item in enumerate(value): self.insert(i + start, item) else: - rng = range(start, stop, step) + rng = list(range(start, stop, step)) if len(value) != len(rng): raise ValueError( "attempt to assign sequence of size %s to " @@ -1117,24 +1117,23 @@ def _list_decorators(): _tidy(__delitem__) return __delitem__ - # Py2K - def __setslice__(fn): - def __setslice__(self, start, end, values): - for value in self[start:end]: - __del(self, value) - values = [__set(self, value) for value in values] - fn(self, start, end, values) - _tidy(__setslice__) - return __setslice__ + if util.py2k: + def __setslice__(fn): + def __setslice__(self, start, end, values): + for value in self[start:end]: + __del(self, value) + values = [__set(self, value) for value in values] + fn(self, start, end, values) + _tidy(__setslice__) + return __setslice__ - def __delslice__(fn): - def __delslice__(self, start, end): - for value in self[start:end]: - __del(self, value) - fn(self, start, end) - _tidy(__delslice__) - return __delslice__ - # end Py2K + def __delslice__(fn): + def __delslice__(self, start, end): + for value in self[start:end]: + __del(self, value) + fn(self, start, end) + _tidy(__delslice__) + return __delslice__ def extend(fn): def extend(self, iterable): @@ -1162,6 +1161,15 @@ def _list_decorators(): _tidy(pop) return pop + if not util.py2k: + def clear(fn): + def clear(self, index=-1): + for item in self: + __del(self, item) + fn(self) + _tidy(clear) + return clear + # __imul__ : not wrapping this. all members of the collection are already # present, so no need to fire appends... wrapping it with an explicit # decorator is still possible, so events on *= can be had if they're @@ -1171,12 +1179,13 @@ def _list_decorators(): l.pop('_tidy') return l + def _dict_decorators(): """Tailored instrumentation wrappers for any dict-like mapping class.""" def _tidy(fn): - setattr(fn, '_sa_instrumented', True) - fn.__doc__ = getattr(getattr(dict, fn.__name__), '__doc__') + fn._sa_instrumented = True + fn.__doc__ = getattr(dict, fn.__name__).__doc__ Unspecified = util.symbol('Unspecified') @@ -1235,48 +1244,38 @@ def _dict_decorators(): _tidy(setdefault) return setdefault - if sys.version_info < (2, 4): - def update(fn): - def update(self, other): - for key in other.keys(): - if key not in self or self[key] is not other[key]: - self[key] = other[key] - _tidy(update) - return update - else: - def update(fn): - def update(self, __other=Unspecified, **kw): - if __other is not Unspecified: - if hasattr(__other, 'keys'): - for key in __other.keys(): - if (key not in self or - self[key] is not __other[key]): - self[key] = __other[key] - else: - for key, value in __other: - if key not in self or self[key] is not value: - self[key] = value - for key in kw: - if key not in self or self[key] is not kw[key]: - self[key] = kw[key] - _tidy(update) - return update + def update(fn): + def update(self, __other=Unspecified, **kw): + if __other is not Unspecified: + if hasattr(__other, 'keys'): + for key in list(__other): + if (key not in self or + self[key] is not __other[key]): + self[key] = __other[key] + else: + for key, value in __other: + if key not in self or self[key] is not value: + self[key] = value + for key in kw: + if key not in self or self[key] is not kw[key]: + self[key] = kw[key] + _tidy(update) + return update l = locals().copy() l.pop('_tidy') l.pop('Unspecified') return l -if util.py3k_warning: - _set_binop_bases = (set, frozenset) -else: - import sets - _set_binop_bases = (set, frozenset, sets.BaseSet) +_set_binop_bases = (set, frozenset) + def _set_binops_check_strict(self, obj): - """Allow only set, frozenset and self.__class__-derived objects in binops.""" + """Allow only set, frozenset and self.__class__-derived + objects in binops.""" return isinstance(obj, _set_binop_bases + (self.__class__,)) + def _set_binops_check_loose(self, obj): """Allow anything set-like to participate in set binops.""" return (isinstance(obj, _set_binop_bases + (self.__class__,)) or @@ -1287,8 +1286,8 @@ def _set_decorators(): """Tailored instrumentation wrappers for any set-like class.""" def _tidy(fn): - setattr(fn, '_sa_instrumented', True) - fn.__doc__ = getattr(getattr(set, fn.__name__), '__doc__') + fn._sa_instrumented = True + fn.__doc__ = getattr(set, fn.__name__).__doc__ Unspecified = util.symbol('Unspecified') @@ -1301,23 +1300,15 @@ def _set_decorators(): _tidy(add) return add - if sys.version_info < (2, 4): - def discard(fn): - def discard(self, value, _sa_initiator=None): - if value in self: - self.remove(value, _sa_initiator) - _tidy(discard) - return discard - else: - def discard(fn): - def discard(self, value, _sa_initiator=None): + def discard(fn): + def discard(self, value, _sa_initiator=None): + # testlib.pragma exempt:__hash__ + if value in self: + __del(self, value, _sa_initiator) # testlib.pragma exempt:__hash__ - if value in self: - __del(self, value, _sa_initiator) - # testlib.pragma exempt:__hash__ - fn(self, value) - _tidy(discard) - return discard + fn(self, value) + _tidy(discard) + return discard def remove(fn): def remove(self, value, _sa_initiator=None): @@ -1442,29 +1433,14 @@ def _set_decorators(): class InstrumentedList(list): """An instrumented version of the built-in list.""" - __instrumentation__ = { - 'appender': 'append', - 'remover': 'remove', - 'iterator': '__iter__', } class InstrumentedSet(set): """An instrumented version of the built-in set.""" - __instrumentation__ = { - 'appender': 'add', - 'remover': 'remove', - 'iterator': '__iter__', } class InstrumentedDict(dict): """An instrumented version of the built-in dict.""" - # Py3K - #__instrumentation__ = { - # 'iterator': 'values', } - # Py2K - __instrumentation__ = { - 'iterator': 'itervalues', } - # end Py2K __canned_instrumentation = { list: InstrumentedList, @@ -1473,33 +1449,29 @@ __canned_instrumentation = { } __interfaces = { - list: {'appender': 'append', - 'remover': 'remove', - 'iterator': '__iter__', - '_decorators': _list_decorators(), }, - set: {'appender': 'add', + list: ( + {'appender': 'append', 'remover': 'remove', + 'iterator': '__iter__'}, _list_decorators() + ), + + set: ({'appender': 'add', 'remover': 'remove', - 'iterator': '__iter__', - '_decorators': _set_decorators(), }, + 'iterator': '__iter__'}, _set_decorators() + ), + # decorators are required for dicts and object collections. - # Py3K - #dict: {'iterator': 'values', - # '_decorators': _dict_decorators(), }, - # Py2K - dict: {'iterator': 'itervalues', - '_decorators': _dict_decorators(), }, - # end Py2K - # < 0.4 compatible naming, deprecated- use decorators instead. - None: {} + dict: ({'iterator': 'values'}, _dict_decorators()) if util.py3k + else ({'iterator': 'itervalues'}, _dict_decorators()), } + class MappedCollection(dict): """A basic dictionary-based collection class. - Extends dict with the minimal bag semantics that collection classes require. - ``set`` and ``remove`` are implemented in terms of a keying function: any - callable that takes an object and returns an object for use as a dictionary - key. + Extends dict with the minimal bag semantics that collection + classes require. ``set`` and ``remove`` are implemented in terms + of a keying function: any callable that takes an object and + returns an object for use as a dictionary key. """ @@ -1519,14 +1491,16 @@ class MappedCollection(dict): """ self.keyfunc = keyfunc + @collection.appender + @collection.internally_instrumented def set(self, value, _sa_initiator=None): """Add an item by value, consulting the keyfunc for the key.""" key = self.keyfunc(value) self.__setitem__(key, value, _sa_initiator) - set = collection.internally_instrumented(set) - set = collection.appender(set) + @collection.remover + @collection.internally_instrumented def remove(self, value, _sa_initiator=None): """Remove an item by value, consulting the keyfunc for the key.""" @@ -1541,9 +1515,8 @@ class MappedCollection(dict): "values after flush?" % (value, self[key], key)) self.__delitem__(key, _sa_initiator) - remove = collection.internally_instrumented(remove) - remove = collection.remover(remove) + @collection.converter def _convert(self, dictlike): """Validate and convert a dict-like object into values for set()ing. @@ -1561,11 +1534,11 @@ class MappedCollection(dict): new_key = self.keyfunc(value) if incoming_key != new_key: raise TypeError( - "Found incompatible key %r for value %r; this collection's " + "Found incompatible key %r for value %r; this " + "collection's " "keying function requires a key of %r for this value." % ( incoming_key, value, new_key)) yield value - _convert = collection.converter(_convert) # ensure instrumentation is associated with # these built-in classes; if a user-defined class @@ -1575,4 +1548,3 @@ class MappedCollection(dict): _instrument_class(MappedCollection) _instrument_class(InstrumentedList) _instrument_class(InstrumentedSet) - diff --git a/libs/sqlalchemy/orm/dependency.py b/libs/sqlalchemy/orm/dependency.py index c46969f2..34a2af39 100644 --- a/libs/sqlalchemy/orm/dependency.py +++ b/libs/sqlalchemy/orm/dependency.py @@ -1,5 +1,5 @@ # orm/dependency.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,10 +8,11 @@ """ -from sqlalchemy import sql, util, exc as sa_exc -from sqlalchemy.orm import attributes, exc, sync, unitofwork, \ +from .. import sql, util, exc as sa_exc +from . import attributes, exc, sync, unitofwork, \ util as mapperutil -from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY +from .interfaces import ONETOMANY, MANYTOONE, MANYTOMANY + class DependencyProcessor(object): def __init__(self, prop): @@ -32,7 +33,7 @@ class DependencyProcessor(object): if self.passive_updates: self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE else: - self._passive_update_flag= attributes.PASSIVE_OFF + self._passive_update_flag = attributes.PASSIVE_OFF self.key = prop.key if not self.prop.synchronize_pairs: @@ -63,7 +64,6 @@ class DependencyProcessor(object): """ uow.register_preprocessor(self, True) - def per_property_flush_actions(self, uow): after_save = unitofwork.ProcessAll(uow, self, False, True) before_delete = unitofwork.ProcessAll(uow, self, True, True) @@ -95,7 +95,6 @@ class DependencyProcessor(object): before_delete ) - def per_state_flush_actions(self, uow, states, isdelete): """establish actions and dependencies related to a flush. @@ -159,7 +158,8 @@ class DependencyProcessor(object): # detect if there's anything changed or loaded # by a preprocessor on this state/attribute. if not, # we should be able to skip it entirely. - sum_ = state.manager[self.key].impl.get_all_pending(state, state.dict) + sum_ = state.manager[self.key].impl.get_all_pending( + state, state.dict) if not sum_: continue @@ -210,7 +210,6 @@ class DependencyProcessor(object): after_save, before_delete, isdelete, childisdelete) - def presort_deletes(self, uowcommit, states): return False @@ -247,7 +246,11 @@ class DependencyProcessor(object): self.mapper in uowcommit.mappers def _verify_canload(self, state): - if state is not None and \ + if self.prop.uselist and state is None: + raise exc.FlushError( + "Can't flush None value found in " + "collection %s" % (self.prop, )) + elif state is not None and \ not self.mapper._canload(state, allow_subtypes=not self.enable_typechecks): if self.mapper._canload(state, allow_subtypes=True): @@ -310,6 +313,7 @@ class DependencyProcessor(object): def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.prop) + class OneToManyDP(DependencyProcessor): def per_property_dependencies(self, uow, parent_saves, @@ -433,8 +437,6 @@ class OneToManyDP(DependencyProcessor): uowcommit.register_object(child, operation="delete", prop=self.prop) - - def presort_saves(self, uowcommit, states): children_added = uowcommit.memo(('children_added', self), set) @@ -559,10 +561,10 @@ class OneToManyDP(DependencyProcessor): pks_changed): source = state dest = child + self._verify_canload(child) if dest is None or \ (not self.post_update and uowcommit.is_deleted(dest)): return - self._verify_canload(child) if clearkeys: sync.clear(dest, self.mapper, self.prop.synchronize_pairs) else: @@ -577,6 +579,7 @@ class OneToManyDP(DependencyProcessor): self.parent, self.prop.synchronize_pairs) + class ManyToOneDP(DependencyProcessor): def __init__(self, prop): DependencyProcessor.__init__(self, prop) @@ -690,8 +693,8 @@ class ManyToOneDP(DependencyProcessor): continue uowcommit.register_object(child, isdelete=True, operation="delete", prop=self.prop) - for c, m, st_, dct_ in self.mapper.cascade_iterator( - 'delete', child): + t = self.mapper.cascade_iterator('delete', child) + for c, m, st_, dct_ in t: uowcommit.register_object( st_, isdelete=True) @@ -704,17 +707,14 @@ class ManyToOneDP(DependencyProcessor): self.key, self._passive_delete_flag) if history: - ret = True for child in history.deleted: if self.hasparent(child) is False: uowcommit.register_object(child, isdelete=True, operation="delete", prop=self.prop) - for c, m, st_, dct_ in self.mapper.cascade_iterator( - 'delete', child): - uowcommit.register_object( - st_, - isdelete=True) + t = self.mapper.cascade_iterator('delete', child) + for c, m, st_, dct_ in t: + uowcommit.register_object(st_, isdelete=True) def process_deletes(self, uowcommit, states): if self.post_update and \ @@ -773,6 +773,7 @@ class ManyToOneDP(DependencyProcessor): uowcommit, False) + class DetectKeySwitch(DependencyProcessor): """For many-to-one relationships with no one-to-many backref, searches for parents through the unit of work when a primary @@ -862,7 +863,7 @@ class DetectKeySwitch(DependencyProcessor): related = state.get_impl(self.key).get(state, dict_, passive=self._passive_update_flag) if related is not attributes.PASSIVE_NO_RESULT and \ - related is not None: + related is not None: related_state = attributes.instance_state(dict_[self.key]) if related_state in switchers: uowcommit.register_object(state, @@ -932,12 +933,14 @@ class ManyToManyDP(DependencyProcessor): ]) def presort_deletes(self, uowcommit, states): + # TODO: no tests fail if this whole + # thing is removed !!!! if not self.passive_deletes: # if no passive deletes, load history on # the collection, so that prop_has_changes() # returns True for state in states: - history = uowcommit.get_attribute_history( + uowcommit.get_attribute_history( state, self.key, self._passive_delete_flag) @@ -1031,8 +1034,7 @@ class ManyToManyDP(DependencyProcessor): passive) if history: for child in history.added: - if child is None or \ - (processed is not None and + if (processed is not None and (state, child) in processed): continue associationrow = {} @@ -1043,8 +1045,7 @@ class ManyToManyDP(DependencyProcessor): continue secondary_insert.append(associationrow) for child in history.deleted: - if child is None or \ - (processed is not None and + if (processed is not None and (state, child) in processed): continue associationrow = {} @@ -1097,11 +1098,11 @@ class ManyToManyDP(DependencyProcessor): if result.supports_sane_multi_rowcount() and \ result.rowcount != len(secondary_delete): raise exc.StaleDataError( - "DELETE statement on table '%s' expected to delete %d row(s); " - "Only %d were matched." % - (self.secondary.description, len(secondary_delete), - result.rowcount) - ) + "DELETE statement on table '%s' expected to delete " + "%d row(s); Only %d were matched." % + (self.secondary.description, len(secondary_delete), + result.rowcount) + ) if secondary_update: associationrow = secondary_update[0] @@ -1114,11 +1115,11 @@ class ManyToManyDP(DependencyProcessor): if result.supports_sane_multi_rowcount() and \ result.rowcount != len(secondary_update): raise exc.StaleDataError( - "UPDATE statement on table '%s' expected to update %d row(s); " - "Only %d were matched." % - (self.secondary.description, len(secondary_update), - result.rowcount) - ) + "UPDATE statement on table '%s' expected to update " + "%d row(s); Only %d were matched." % + (self.secondary.description, len(secondary_update), + result.rowcount) + ) if secondary_insert: statement = self.secondary.insert() @@ -1126,8 +1127,14 @@ class ManyToManyDP(DependencyProcessor): def _synchronize(self, state, child, associationrow, clearkeys, uowcommit, operation): - if associationrow is None: - return + + # this checks for None if uselist=True + self._verify_canload(child) + + # but if uselist=False we get here. If child is None, + # no association row can be generated, so return. + if child is None: + return False if child is not None and not uowcommit.session._contains_state(child): if not child.deleted: @@ -1137,8 +1144,6 @@ class ManyToManyDP(DependencyProcessor): (mapperutil.state_class_str(child), operation, self.prop)) return False - self._verify_canload(child) - sync.populate_dict(state, self.parent, associationrow, self.prop.synchronize_pairs) sync.populate_dict(child, self.mapper, associationrow, @@ -1154,8 +1159,7 @@ class ManyToManyDP(DependencyProcessor): self.prop.synchronize_pairs) _direction_to_processor = { - ONETOMANY : OneToManyDP, + ONETOMANY: OneToManyDP, MANYTOONE: ManyToOneDP, - MANYTOMANY : ManyToManyDP, + MANYTOMANY: ManyToManyDP, } - diff --git a/libs/sqlalchemy/orm/deprecated_interfaces.py b/libs/sqlalchemy/orm/deprecated_interfaces.py index d251f52e..020b7c71 100644 --- a/libs/sqlalchemy/orm/deprecated_interfaces.py +++ b/libs/sqlalchemy/orm/deprecated_interfaces.py @@ -1,13 +1,13 @@ # orm/deprecated_interfaces.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import event, util -from interfaces import EXT_CONTINUE - +from .. import event, util +from .interfaces import EXT_CONTINUE +@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") class MapperExtension(object): """Base implementation for :class:`.Mapper` event hooks. @@ -116,7 +116,6 @@ class MapperExtension(object): event.listen(self, "%s" % meth, ls_meth, raw=False, retval=True, propagate=True) - def instrument_class(self, mapper, class_): """Receive a class when the mapper is first constructed, and has applied instrumentation to the mapped class. @@ -374,6 +373,8 @@ class MapperExtension(object): return EXT_CONTINUE + +@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") class SessionExtension(object): """Base implementation for :class:`.Session` event hooks. @@ -385,7 +386,7 @@ class SessionExtension(object): :class:`.SessionEvents`. Subclasses may be installed into a :class:`.Session` (or - :func:`.sessionmaker`) using the ``extension`` keyword + :class:`.sessionmaker`) using the ``extension`` keyword argument:: from sqlalchemy.orm.interfaces import SessionExtension @@ -439,7 +440,7 @@ class SessionExtension(object): Note that this may not be per-flush if a longer running transaction is ongoing.""" - def before_flush( self, session, flush_context, instances): + def before_flush(self, session, flush_context, instances): """Execute before flush process has started. `instances` is an optional list of objects which were passed to @@ -462,7 +463,7 @@ class SessionExtension(object): occurred, depending on whether or not the flush started its own transaction or participated in a larger transaction. """ - def after_begin( self, session, transaction, connection): + def after_begin(self, session, transaction, connection): """Execute after a transaction is begun on a connection `transaction` is the SessionTransaction. This method is called @@ -473,7 +474,7 @@ class SessionExtension(object): This is called after an add, delete or merge. """ - def after_bulk_update( self, session, query, query_context, result): + def after_bulk_update(self, session, query, query_context, result): """Execute after a bulk update operation to the session. This is called after a session.query(...).update() @@ -483,7 +484,7 @@ class SessionExtension(object): `result` is the result object returned from the bulk operation. """ - def after_bulk_delete( self, session, query, query_context, result): + def after_bulk_delete(self, session, query, query_context, result): """Execute after a bulk delete operation to the session. This is called after a session.query(...).delete() @@ -494,6 +495,7 @@ class SessionExtension(object): """ +@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") class AttributeExtension(object): """Base implementation for :class:`.AttributeImpl` event hooks, events that fire upon attribute mutations in user code. @@ -586,5 +588,3 @@ class AttributeExtension(object): """ return value - - diff --git a/libs/sqlalchemy/orm/descriptor_props.py b/libs/sqlalchemy/orm/descriptor_props.py index 70cf0e7c..24b0a15e 100644 --- a/libs/sqlalchemy/orm/descriptor_props.py +++ b/libs/sqlalchemy/orm/descriptor_props.py @@ -1,5 +1,5 @@ # orm/descriptor_props.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -10,13 +10,14 @@ as actively in the load/persist ORM loop. """ -from sqlalchemy.orm.interfaces import \ - MapperProperty, PropComparator, StrategizedProperty -from sqlalchemy.orm.mapper import _none_set -from sqlalchemy.orm import attributes, strategies -from sqlalchemy import util, sql, exc as sa_exc, event, schema -from sqlalchemy.sql import expression -properties = util.importlater('sqlalchemy.orm', 'properties') +from .interfaces import MapperProperty, PropComparator +from .util import _none_set +from . import attributes +from .. import util, sql, exc as sa_exc, event, schema +from ..sql import expression +from . import properties +from . import query + class DescriptorProperty(MapperProperty): """:class:`.MapperProperty` which proxies access to a @@ -30,6 +31,7 @@ class DescriptorProperty(MapperProperty): class _ProxyImpl(object): accepts_scalar_loader = False expire_missing = True + collection = False def __init__(self, key): self.key = key @@ -47,8 +49,10 @@ class DescriptorProperty(MapperProperty): if self.descriptor is None: def fset(obj, value): setattr(obj, self.name, value) + def fdel(obj): delattr(obj, self.name) + def fget(obj): return getattr(obj, self.name) @@ -65,15 +69,79 @@ class DescriptorProperty(MapperProperty): self.key, self.descriptor, lambda: self._comparator_factory(mapper), - doc=self.doc + doc=self.doc, + original_property=self ) proxy_attr.impl = _ProxyImpl(self.key) mapper.class_manager.instrument_attribute(self.key, proxy_attr) +@util.langhelpers.dependency_for("sqlalchemy.orm.properties") class CompositeProperty(DescriptorProperty): + """Defines a "composite" mapped attribute, representing a collection + of columns as one attribute. + :class:`.CompositeProperty` is constructed using the :func:`.composite` + function. + + .. seealso:: + + :ref:`mapper_composite` + + """ def __init__(self, class_, *attrs, **kwargs): + """Return a composite column-based property for use with a Mapper. + + See the mapping documentation section :ref:`mapper_composite` for a full + usage example. + + The :class:`.MapperProperty` returned by :func:`.composite` + is the :class:`.CompositeProperty`. + + :param class\_: + The "composite type" class. + + :param \*cols: + List of Column objects to be mapped. + + :param active_history=False: + When ``True``, indicates that the "previous" value for a + scalar attribute should be loaded when replaced, if not + already loaded. See the same flag on :func:`.column_property`. + + .. versionchanged:: 0.7 + This flag specifically becomes meaningful + - previously it was a placeholder. + + :param group: + A group name for this property when marked as deferred. + + :param deferred: + When True, the column property is "deferred", meaning that it does not + load immediately, and is instead loaded when the attribute is first + accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`. + + :param comparator_factory: a class which extends + :class:`.CompositeProperty.Comparator` which provides custom SQL clause + generation for comparison operations. + + :param doc: + optional string that will be applied as the doc on the + class-bound descriptor. + + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 + + :param extension: + an :class:`.AttributeExtension` instance, + or list of extensions, which will be prepended to the list of + attribute listeners for the resulting descriptor placed on the class. + **Deprecated.** Please see :class:`.AttributeEvents`. + + """ + self.attrs = attrs self.composite_class = class_ self.active_history = kwargs.get('active_history', False) @@ -81,9 +149,13 @@ class CompositeProperty(DescriptorProperty): self.group = kwargs.get('group', None) self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) + if 'info' in kwargs: + self.info = kwargs.pop('info') + util.set_creation_order(self) self._create_descriptor() + def instrument_class(self, mapper): super(CompositeProperty, self).instrument_class(mapper) self._setup_event_handlers() @@ -110,7 +182,10 @@ class CompositeProperty(DescriptorProperty): # key not present. Iterate through related # attributes, retrieve their values. This # ensures they all load. - values = [getattr(instance, key) for key in self._attribute_keys] + values = [ + getattr(instance, key) + for key in self._attribute_keys + ] # current expected behavior here is that the composite is # created on access if the object is persistent or if @@ -164,12 +239,17 @@ class CompositeProperty(DescriptorProperty): def _init_props(self): self.props = props = [] for attr in self.attrs: - if isinstance(attr, basestring): + if isinstance(attr, str): prop = self.parent.get_property(attr) elif isinstance(attr, schema.Column): prop = self.parent._columntoproperty[attr] elif isinstance(attr, attributes.InstrumentedAttribute): prop = attr.property + else: + raise sa_exc.ArgumentError( + "Composite expects Column objects or mapped " + "attributes/attribute names as arguments, got: %r" + % (attr,)) props.append(prop) @property @@ -185,7 +265,9 @@ class CompositeProperty(DescriptorProperty): prop.active_history = self.active_history if self.deferred: prop.deferred = self.deferred - prop.strategy_class = strategies.DeferredColumnLoader + prop.strategy_class = prop._strategy_lookup( + ("deferred", True), + ("instrument", True)) prop.group = self.group def _setup_event_handlers(self): @@ -225,12 +307,15 @@ class CompositeProperty(DescriptorProperty): state.dict.pop(self.key, None) event.listen(self.parent, 'after_insert', - insert_update_handler, raw=True) + insert_update_handler, raw=True) event.listen(self.parent, 'after_update', - insert_update_handler, raw=True) - event.listen(self.parent, 'load', load_handler, raw=True, propagate=True) - event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True) - event.listen(self.parent, "expire", expire_handler, raw=True, propagate=True) + insert_update_handler, raw=True) + event.listen(self.parent, 'load', + load_handler, raw=True, propagate=True) + event.listen(self.parent, 'refresh', + load_handler, raw=True, propagate=True) + event.listen(self.parent, 'expire', + expire_handler, raw=True, propagate=True) # TODO: need a deserialize hook here @@ -271,34 +356,80 @@ class CompositeProperty(DescriptorProperty): ) else: return attributes.History( - (),[self.composite_class(*added)], () + (), [self.composite_class(*added)], () ) def _comparator_factory(self, mapper): - return self.comparator_factory(self) + return self.comparator_factory(self, mapper) + + class CompositeBundle(query.Bundle): + def __init__(self, property, expr): + self.property = property + super(CompositeProperty.CompositeBundle, self).__init__( + property.key, *expr) + + def create_row_processor(self, query, procs, labels): + def proc(row, result): + return self.property.composite_class(*[proc(row, result) for proc in procs]) + return proc + class Comparator(PropComparator): - def __init__(self, prop, adapter=None): - self.prop = self.property = prop - self.adapter = adapter + """Produce boolean, comparison, and other operators for + :class:`.CompositeProperty` attributes. + + See the example in :ref:`composite_operations` for an overview + of usage , as well as the documentation for :class:`.PropComparator`. + + See also: + + :class:`.PropComparator` + + :class:`.ColumnOperators` + + :ref:`types_operators` + + :attr:`.TypeEngine.comparator_factory` + + """ - def __clause_element__(self): - if self.adapter: - # TODO: test coverage for adapted composite comparison - return expression.ClauseList( - *[self.adapter(x) for x in self.prop._comparable_elements]) - else: - return expression.ClauseList(*self.prop._comparable_elements) __hash__ = None + @property + def clauses(self): + return self.__clause_element__() + + def __clause_element__(self): + return expression.ClauseList(group=False, *self._comparable_elements) + + def _query_clause_element(self): + return CompositeProperty.CompositeBundle(self.prop, self.__clause_element__()) + + @util.memoized_property + def _comparable_elements(self): + if self._adapt_to_entity: + return [ + getattr( + self._adapt_to_entity.entity, + prop.key + ) for prop in self.prop._comparable_elements + ] + else: + return self.prop._comparable_elements + def __eq__(self, other): if other is None: values = [None] * len(self.prop._comparable_elements) else: values = other.__composite_values__() - return sql.and_( - *[a==b for a, b in zip(self.prop._comparable_elements, values)]) + comparisons = [ + a == b + for a, b in zip(self.prop._comparable_elements, values) + ] + if self._adapt_to_entity: + comparisons = [self.adapter(x) for x in comparisons] + return sql.and_(*comparisons) def __ne__(self, other): return sql.not_(self.__eq__(other)) @@ -306,6 +437,8 @@ class CompositeProperty(DescriptorProperty): def __str__(self): return str(self.parent.class_.__name__) + "." + self.key + +@util.langhelpers.dependency_for("sqlalchemy.orm.properties") class ConcreteInheritedProperty(DescriptorProperty): """A 'do nothing' :class:`.MapperProperty` that disables an attribute on a concrete subclass that is only present @@ -343,8 +476,10 @@ class ConcreteInheritedProperty(DescriptorProperty): class NoninheritedConcreteProp(object): def __set__(s, obj, value): warn() + def __delete__(s, obj): warn() + def __get__(s, obj, owner): if obj is None: return self.descriptor @@ -352,11 +487,66 @@ class ConcreteInheritedProperty(DescriptorProperty): self.descriptor = NoninheritedConcreteProp() +@util.langhelpers.dependency_for("sqlalchemy.orm.properties") class SynonymProperty(DescriptorProperty): def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None, doc=None): + """Denote an attribute name as a synonym to a mapped property, + in that the attribute will mirror the value and expression behavior + of another attribute. + + :param name: the name of the existing mapped property. This + can refer to the string name of any :class:`.MapperProperty` + configured on the class, including column-bound attributes + and relationships. + + :param descriptor: a Python :term:`descriptor` that will be used + as a getter (and potentially a setter) when this attribute is + accessed at the instance level. + + :param map_column: if ``True``, the :func:`.synonym` construct will + locate the existing named :class:`.MapperProperty` based on the + attribute name of this :func:`.synonym`, and assign it to a new + attribute linked to the name of this :func:`.synonym`. + That is, given a mapping like:: + + class MyClass(Base): + __tablename__ = 'my_table' + + id = Column(Integer, primary_key=True) + job_status = Column(String(50)) + + job_status = synonym("_job_status", map_column=True) + + The above class ``MyClass`` will now have the ``job_status`` + :class:`.Column` object mapped to the attribute named ``_job_status``, + and the attribute named ``job_status`` will refer to the synonym + itself. This feature is typically used in conjunction with the + ``descriptor`` argument in order to link a user-defined descriptor + as a "wrapper" for an existing column. + + :param comparator_factory: A subclass of :class:`.PropComparator` + that will provide custom comparison behavior at the SQL expression + level. + + .. note:: + + For the use case of providing an attribute which redefines both + Python-level and SQL-expression level behavior of an attribute, + please refer to the Hybrid attribute introduced at + :ref:`mapper_hybrids` for a more effective technique. + + .. seealso:: + + :ref:`synonyms` - examples of functionality. + + :ref:`mapper_hybrids` - Hybrids provide a better approach for + more complicated attribute-wrapping schemes than synonyms. + + """ + self.name = name self.map_column = map_column self.descriptor = descriptor @@ -409,10 +599,73 @@ class SynonymProperty(DescriptorProperty): self.parent = parent + +@util.langhelpers.dependency_for("sqlalchemy.orm.properties") class ComparableProperty(DescriptorProperty): """Instruments a Python property for use in query expressions.""" def __init__(self, comparator_factory, descriptor=None, doc=None): + """Provides a method of applying a :class:`.PropComparator` + to any Python descriptor attribute. + + .. versionchanged:: 0.7 + :func:`.comparable_property` is superseded by + the :mod:`~sqlalchemy.ext.hybrid` extension. See the example + at :ref:`hybrid_custom_comparators`. + + Allows any Python descriptor to behave like a SQL-enabled + attribute when used at the class level in queries, allowing + redefinition of expression operator behavior. + + In the example below we redefine :meth:`.PropComparator.operate` + to wrap both sides of an expression in ``func.lower()`` to produce + case-insensitive comparison:: + + from sqlalchemy.orm import comparable_property + from sqlalchemy.orm.interfaces import PropComparator + from sqlalchemy.sql import func + from sqlalchemy import Integer, String, Column + from sqlalchemy.ext.declarative import declarative_base + + class CaseInsensitiveComparator(PropComparator): + def __clause_element__(self): + return self.prop + + def operate(self, op, other): + return op( + func.lower(self.__clause_element__()), + func.lower(other) + ) + + Base = declarative_base() + + class SearchWord(Base): + __tablename__ = 'search_word' + id = Column(Integer, primary_key=True) + word = Column(String) + word_insensitive = comparable_property(lambda prop, mapper: + CaseInsensitiveComparator(mapper.c.word, mapper) + ) + + + A mapping like the above allows the ``word_insensitive`` attribute + to render an expression like:: + + >>> print SearchWord.word_insensitive == "Trucks" + lower(search_word.word) = lower(:lower_1) + + :param comparator_factory: + A PropComparator subclass or factory that defines operator behavior + for this property. + + :param descriptor: + Optional when used in a ``properties={}`` declaration. The Python + descriptor or property to layer comparison behavior on top of. + + The like-named descriptor will be automatically retrieved from the + mapped class if left blank in a ``properties`` declaration. + + """ self.descriptor = descriptor self.comparator_factory = comparator_factory self.doc = doc or (descriptor and descriptor.__doc__) or None @@ -420,3 +673,5 @@ class ComparableProperty(DescriptorProperty): def _comparator_factory(self, mapper): return self.comparator_factory(self, mapper) + + diff --git a/libs/sqlalchemy/orm/dynamic.py b/libs/sqlalchemy/orm/dynamic.py index e3773659..bae09d32 100644 --- a/libs/sqlalchemy/orm/dynamic.py +++ b/libs/sqlalchemy/orm/dynamic.py @@ -1,5 +1,5 @@ # orm/dynamic.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -11,39 +11,40 @@ basic add/delete mutation. """ -from sqlalchemy import log, util -from sqlalchemy.orm import exc as orm_exc -from sqlalchemy.sql import operators -from sqlalchemy.orm import ( - attributes, object_session, util as mapperutil, strategies, object_mapper +from .. import log, util, exc +from ..sql import operators +from . import ( + attributes, object_session, util as orm_util, strategies, + object_mapper, exc as orm_exc, properties ) -from sqlalchemy.orm.query import Query -from sqlalchemy.orm.util import has_identity -from sqlalchemy.orm import collections +from .query import Query +@log.class_logger +@properties.RelationshipProperty.strategy_for(lazy="dynamic") class DynaLoader(strategies.AbstractRelationshipLoader): def init_class_attribute(self, mapper): self.is_class_level = True if not self.uselist: - util.warn( - "On relationship %s, 'dynamic' loaders cannot be used with " - "many-to-one/one-to-one relationships and/or " - "uselist=False." % self.parent_property) + raise exc.InvalidRequestError( + "On relationship %s, 'dynamic' loaders cannot be used with " + "many-to-one/one-to-one relationships and/or " + "uselist=False." % self.parent_property) strategies._register_attribute(self, mapper, useobject=True, + uselist=True, impl_class=DynamicAttributeImpl, target_mapper=self.parent_property.mapper, order_by=self.parent_property.order_by, - query_class=self.parent_property.query_class + query_class=self.parent_property.query_class, + backref=self.parent_property.back_populates, ) -log.class_logger(DynaLoader) - class DynamicAttributeImpl(attributes.AttributeImpl): uses_objects = True accepts_scalar_loader = False supports_population = False + collection = False def __init__(self, class_, key, typecallable, dispatch, @@ -60,7 +61,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): self.query_class = mixin_user_query(query_class) def get(self, state, dict_, passive=attributes.PASSIVE_OFF): - if passive is not attributes.PASSIVE_OFF: + if not passive & attributes.SQL_OK: return self._get_collection_history(state, attributes.PASSIVE_NO_INITIALIZE).added_items else: @@ -68,39 +69,53 @@ class DynamicAttributeImpl(attributes.AttributeImpl): def get_collection(self, state, dict_, user_data=None, passive=attributes.PASSIVE_NO_INITIALIZE): - if passive is not attributes.PASSIVE_OFF: + if not passive & attributes.SQL_OK: return self._get_collection_history(state, passive).added_items else: history = self._get_collection_history(state, passive) - return history.added_items + history.unchanged_items + return history.added_plus_unchanged - def fire_append_event(self, state, dict_, value, initiator): - collection_history = self._modified_event(state, dict_) - collection_history.added_items.append(value) + @util.memoized_property + def _append_token(self): + return attributes.Event(self, attributes.OP_APPEND) + + @util.memoized_property + def _remove_token(self): + return attributes.Event(self, attributes.OP_REMOVE) + + def fire_append_event(self, state, dict_, value, initiator, + collection_history=None): + if collection_history is None: + collection_history = self._modified_event(state, dict_) + + collection_history.add_added(value) for fn in self.dispatch.append: - value = fn(state, value, initiator or self) + value = fn(state, value, initiator or self._append_token) if self.trackparent and value is not None: self.sethasparent(attributes.instance_state(value), state, True) - def fire_remove_event(self, state, dict_, value, initiator): - collection_history = self._modified_event(state, dict_) - collection_history.deleted_items.append(value) + def fire_remove_event(self, state, dict_, value, initiator, + collection_history=None): + if collection_history is None: + collection_history = self._modified_event(state, dict_) + + collection_history.add_removed(value) if self.trackparent and value is not None: self.sethasparent(attributes.instance_state(value), state, False) for fn in self.dispatch.remove: - fn(state, value, initiator or self) + fn(state, value, initiator or self._remove_token) def _modified_event(self, state, dict_): if self.key not in state.committed_state: state.committed_state[self.key] = CollectionHistory(self, state) - state.modified_event(dict_, + state._modified_event(dict_, self, attributes.NEVER_SET) @@ -119,18 +134,31 @@ class DynamicAttributeImpl(attributes.AttributeImpl): return self._set_iterable(state, dict_, value) - def _set_iterable(self, state, dict_, iterable, adapter=None): - collection_history = self._modified_event(state, dict_) new_values = list(iterable) if state.has_identity: - old_collection = list(self.get(state, dict_)) + old_collection = util.IdentitySet(self.get(state, dict_)) + + collection_history = self._modified_event(state, dict_) + if not state.has_identity: + old_collection = collection_history.added_items else: - old_collection = [] - collections.bulk_replace(new_values, DynCollectionAdapter(self, - state, old_collection), - DynCollectionAdapter(self, state, - new_values)) + old_collection = old_collection.union( + collection_history.added_items) + + idset = util.IdentitySet + constants = old_collection.intersection(new_values) + additions = idset(new_values).difference(constants) + removals = old_collection.difference(constants) + + for member in new_values: + if member in additions: + self.fire_append_event(state, dict_, member, None, + collection_history=collection_history) + + for member in removals: + self.fire_remove_event(state, dict_, member, None, + collection_history=collection_history) def delete(self, *args, **kwargs): raise NotImplementedError() @@ -141,15 +169,15 @@ class DynamicAttributeImpl(attributes.AttributeImpl): def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): c = self._get_collection_history(state, passive) - return attributes.History(c.added_items, c.unchanged_items, - c.deleted_items) + return c.as_history() def get_all_pending(self, state, dict_): - c = self._get_collection_history(state, True) + c = self._get_collection_history( + state, attributes.PASSIVE_NO_INITIALIZE) return [ (attributes.instance_state(x), x) for x in - c.added_items + c.unchanged_items + c.deleted_items + c.all_items ] def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF): @@ -158,7 +186,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): else: c = CollectionHistory(self, state) - if passive is attributes.PASSIVE_OFF: + if state.has_identity and (passive & attributes.INIT_OK): return CollectionHistory(self, state, apply_to=c) else: return c @@ -173,34 +201,16 @@ class DynamicAttributeImpl(attributes.AttributeImpl): if initiator is not self: self.fire_remove_event(state, dict_, value, initiator) -class DynCollectionAdapter(object): - """the dynamic analogue to orm.collections.CollectionAdapter""" + def pop(self, state, dict_, value, initiator, + passive=attributes.PASSIVE_OFF): + self.remove(state, dict_, value, initiator, passive=passive) - def __init__(self, attr, owner_state, data): - self.attr = attr - self.state = owner_state - self.data = data - - def __iter__(self): - return iter(self.data) - - def append_with_event(self, item, initiator=None): - self.attr.append(self.state, self.state.dict, item, initiator) - - def remove_with_event(self, item, initiator=None): - self.attr.remove(self.state, self.state.dict, item, initiator) - - def append_without_event(self, item): - pass - - def remove_without_event(self, item): - pass class AppenderMixin(object): query_class = None def __init__(self, attr, state): - Query.__init__(self, attr.target_mapper, None) + super(AppenderMixin, self).__init__(attr.target_mapper, None) self.instance = instance = state.obj() self.attr = attr @@ -215,22 +225,19 @@ class AppenderMixin(object): if self.attr.order_by: self._order_by = self.attr.order_by - def __session(self): + def session(self): sess = object_session(self.instance) if sess is not None and self.autoflush and sess.autoflush \ and self.instance in sess: sess.flush() - if not has_identity(self.instance): + if not orm_util.has_identity(self.instance): return None else: return sess - - def session(self): - return self.__session() - session = property(session, lambda s, x:None) + session = property(session, lambda s, x: None) def __iter__(self): - sess = self.__session() + sess = self.session if sess is None: return iter(self.attr._get_collection_history( attributes.instance_state(self.instance), @@ -239,17 +246,16 @@ class AppenderMixin(object): return iter(self._clone(sess)) def __getitem__(self, index): - sess = self.__session() + sess = self.session if sess is None: return self.attr._get_collection_history( attributes.instance_state(self.instance), - attributes.PASSIVE_NO_INITIALIZE).added_items.\ - __getitem__(index) + attributes.PASSIVE_NO_INITIALIZE).indexed(index) else: return self._clone(sess).__getitem__(index) def count(self): - sess = self.__session() + sess = self.session if sess is None: return len(self.attr._get_collection_history( attributes.instance_state(self.instance), @@ -269,7 +275,7 @@ class AppenderMixin(object): "Parent instance %s is not bound to a Session, and no " "contextual session is established; lazy load operation " "of attribute '%s' cannot proceed" % ( - mapperutil.instance_str(instance), self.attr.key)) + orm_util.instance_str(instance), self.attr.key)) if self.query_class: query = self.query_class(self.attr.target_mapper, session=sess) @@ -281,6 +287,12 @@ class AppenderMixin(object): return query + def extend(self, iterator): + for item in iterator: + self.attr.append( + attributes.instance_state(self.instance), + attributes.instance_dict(self.instance), item, None) + def append(self, item): self.attr.append( attributes.instance_state(self.instance), @@ -301,20 +313,56 @@ def mixin_user_query(cls): name = 'Appender' + cls.__name__ return type(name, (AppenderMixin, cls), {'query_class': cls}) + class CollectionHistory(object): """Overrides AttributeHistory to receive append/remove events directly.""" def __init__(self, attr, state, apply_to=None): if apply_to: - deleted = util.IdentitySet(apply_to.deleted_items) - added = apply_to.added_items coll = AppenderQuery(attr, state).autoflush(False) - self.unchanged_items = [o for o in util.IdentitySet(coll) - if o not in deleted] + self.unchanged_items = util.OrderedIdentitySet(coll) self.added_items = apply_to.added_items self.deleted_items = apply_to.deleted_items + self._reconcile_collection = True else: - self.deleted_items = [] - self.added_items = [] - self.unchanged_items = [] + self.deleted_items = util.OrderedIdentitySet() + self.added_items = util.OrderedIdentitySet() + self.unchanged_items = util.OrderedIdentitySet() + self._reconcile_collection = False + + @property + def added_plus_unchanged(self): + return list(self.added_items.union(self.unchanged_items)) + + @property + def all_items(self): + return list(self.added_items.union( + self.unchanged_items).union(self.deleted_items)) + + def as_history(self): + if self._reconcile_collection: + added = self.added_items.difference(self.unchanged_items) + deleted = self.deleted_items.intersection(self.unchanged_items) + unchanged = self.unchanged_items.difference(deleted) + else: + added, unchanged, deleted = self.added_items,\ + self.unchanged_items,\ + self.deleted_items + return attributes.History( + list(added), + list(unchanged), + list(deleted), + ) + + def indexed(self, index): + return list(self.added_items)[index] + + def add_added(self, value): + self.added_items.add(value) + + def add_removed(self, value): + if value in self.added_items: + self.added_items.remove(value) + else: + self.deleted_items.add(value) diff --git a/libs/sqlalchemy/orm/evaluator.py b/libs/sqlalchemy/orm/evaluator.py index 5eaac6c3..e1dd9606 100644 --- a/libs/sqlalchemy/orm/evaluator.py +++ b/libs/sqlalchemy/orm/evaluator.py @@ -1,12 +1,11 @@ # orm/evaluator.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php import operator -from sqlalchemy.sql import operators, functions -from sqlalchemy.sql import expression as sql +from ..sql import operators class UnevaluatableError(Exception): @@ -14,23 +13,23 @@ class UnevaluatableError(Exception): _straight_ops = set(getattr(operators, op) for op in ('add', 'mul', 'sub', - # Py2K 'div', - # end Py2K 'mod', 'truediv', 'lt', 'le', 'ne', 'gt', 'ge', 'eq')) _notimplemented_ops = set(getattr(operators, op) - for op in ('like_op', 'notlike_op', 'ilike_op', - 'notilike_op', 'between_op', 'in_op', - 'notin_op', 'endswith_op', 'concat_op')) + for op in ('like_op', 'notlike_op', 'ilike_op', + 'notilike_op', 'between_op', 'in_op', + 'notin_op', 'endswith_op', 'concat_op')) + class EvaluatorCompiler(object): def process(self, clause): meth = getattr(self, "visit_%s" % clause.__visit_name__, None) if not meth: - raise UnevaluatableError("Cannot evaluate %s" % type(clause).__name__) + raise UnevaluatableError( + "Cannot evaluate %s" % type(clause).__name__) return meth(clause) def visit_grouping(self, clause): @@ -39,6 +38,12 @@ class EvaluatorCompiler(object): def visit_null(self, clause): return lambda obj: None + def visit_false(self, clause): + return lambda obj: False + + def visit_true(self, clause): + return lambda obj: True + def visit_column(self, clause): if 'parentmapper' in clause._annotations: key = clause._annotations['parentmapper'].\ @@ -49,7 +54,7 @@ class EvaluatorCompiler(object): return lambda obj: get_corresponding_attr(obj) def visit_clauselist(self, clause): - evaluators = map(self.process, clause.clauses) + evaluators = list(map(self.process, clause.clauses)) if clause.operator is operators.or_: def evaluate(obj): has_null = False @@ -71,12 +76,15 @@ class EvaluatorCompiler(object): return False return True else: - raise UnevaluatableError("Cannot evaluate clauselist with operator %s" % clause.operator) + raise UnevaluatableError( + "Cannot evaluate clauselist with operator %s" % + clause.operator) return evaluate def visit_binary(self, clause): - eval_left,eval_right = map(self.process, [clause.left, clause.right]) + eval_left, eval_right = list(map(self.process, + [clause.left, clause.right])) operator = clause.operator if operator is operators.is_: def evaluate(obj): @@ -92,7 +100,9 @@ class EvaluatorCompiler(object): return None return operator(eval_left(obj), eval_right(obj)) else: - raise UnevaluatableError("Cannot evaluate %s with operator %s" % (type(clause).__name__, clause.operator)) + raise UnevaluatableError( + "Cannot evaluate %s with operator %s" % + (type(clause).__name__, clause.operator)) return evaluate def visit_unary(self, clause): @@ -104,7 +114,9 @@ class EvaluatorCompiler(object): return None return not value return evaluate - raise UnevaluatableError("Cannot evaluate %s with operator %s" % (type(clause).__name__, clause.operator)) + raise UnevaluatableError( + "Cannot evaluate %s with operator %s" % + (type(clause).__name__, clause.operator)) def visit_bindparam(self, clause): val = clause.value diff --git a/libs/sqlalchemy/orm/events.py b/libs/sqlalchemy/orm/events.py index 3c868d14..a09154dd 100644 --- a/libs/sqlalchemy/orm/events.py +++ b/libs/sqlalchemy/orm/events.py @@ -1,5 +1,5 @@ # orm/events.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,9 +7,15 @@ """ORM event interfaces. """ -from sqlalchemy import event, exc, util -orm = util.importlater("sqlalchemy", "orm") +from .. import event, exc, util +from .base import _mapper_or_none import inspect +import weakref +from . import interfaces +from . import mapperlib, instrumentation +from .session import Session, sessionmaker +from .scoping import scoped_session +from .attributes import QueryableAttribute class InstrumentationEvents(event.Events): """Events related to class instrumentation events. @@ -17,26 +23,68 @@ class InstrumentationEvents(event.Events): The listeners here support being established against any new style class, that is any object that is a subclass of 'type'. Events will then be fired off for events - against that class as well as all subclasses. - 'type' itself is also accepted as a target - in which case the events fire for all classes. + against that class. If the "propagate=True" flag is passed + to event.listen(), the event will fire off for subclasses + of that class as well. + + The Python ``type`` builtin is also accepted as a target, + which when used has the effect of events being emitted + for all classes. + + Note the "propagate" flag here is defaulted to ``True``, + unlike the other class level events where it defaults + to ``False``. This means that new subclasses will also + be the subject of these events, when a listener + is established on a superclass. + + .. versionchanged:: 0.8 - events here will emit based + on comparing the incoming class to the type of class + passed to :func:`.event.listen`. Previously, the + event would fire for any class unconditionally regardless + of what class was sent for listening, despite + documentation which stated the contrary. """ + _target_class_doc = "SomeBaseClass" + _dispatch_target = instrumentation.InstrumentationFactory + + @classmethod def _accept_with(cls, target): if isinstance(target, type): - return orm.instrumentation.instrumentation_registry + return _InstrumentationEventsHold(target) else: return None @classmethod - def _listen(cls, target, identifier, fn, propagate=False): - event.Events._listen(target, identifier, fn, propagate=propagate) + def _listen(cls, event_key, propagate=True): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + + def listen(target_cls, *arg): + listen_cls = target() + if propagate and issubclass(target_cls, listen_cls): + return fn(target_cls, *arg) + elif not propagate and target_cls is listen_cls: + return fn(target_cls, *arg) + + def remove(ref): + key = event.registry._EventKey(None, identifier, listen, + instrumentation._instrumentation_factory) + getattr(instrumentation._instrumentation_factory.dispatch, + identifier).remove(key) + + target = weakref.ref(target.class_, remove) + + event_key.\ + with_dispatch_target(instrumentation._instrumentation_factory).\ + with_wrapper(listen).base_listen() @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of instrumentation events not yet implemented") + def _clear(cls): + super(InstrumentationEvents, cls)._clear() + instrumentation._instrumentation_factory.dispatch._clear() def class_instrument(self, cls): """Called after the given class is instrumented. @@ -54,10 +102,21 @@ class InstrumentationEvents(event.Events): """ - def attribute_instrument(self, cls, key, inst): """Called when an attribute is instrumented.""" + + +class _InstrumentationEventsHold(object): + """temporary marker object used to transfer from _accept_with() to + _listen() on the InstrumentationEvents class. + + """ + def __init__(self, class_): + self.class_ = class_ + + dispatch = event.dispatcher(InstrumentationEvents) + class InstanceEvents(event.Events): """Define events specific to object lifecycle. @@ -68,21 +127,19 @@ class InstanceEvents(event.Events): def my_load_listener(target, context): print "on load!" - event.listen(SomeMappedClass, 'load', my_load_listener) + event.listen(SomeClass, 'load', my_load_listener) - Available targets include mapped classes, instances of - :class:`.Mapper` (i.e. returned by :func:`.mapper`, - :func:`.class_mapper` and similar), as well as the - :class:`.Mapper` class and :func:`.mapper` function itself - for global event reception:: + Available targets include: - from sqlalchemy.orm import mapper + * mapped classes + * unmapped superclasses of mapped or to-be-mapped classes + (using the ``propagate=True`` flag) + * :class:`.Mapper` objects + * the :class:`.Mapper` class itself and the :func:`.mapper` + function indicate listening for all mappers. - def some_listener(target, context): - log.debug("Instance %s being loaded" % target) - - # attach to all mappers - event.listen(mapper, 'load', some_listener) + .. versionchanged:: 0.8.0 instance events can be associated with + unmapped superclasses of mapped classes. Instance events are closely related to mapper events, but are more specific to the instance and its instrumentation, @@ -92,47 +149,66 @@ class InstanceEvents(event.Events): available to the :func:`.event.listen` function. :param propagate=False: When True, the event listener should - be applied to all inheriting mappers as well as the - mapper which is the target of this listener. + be applied to all inheriting classes as well as the + class which is the target of this listener. :param raw=False: When True, the "target" argument passed to applicable event listener functions will be the instance's :class:`.InstanceState` management object, rather than the mapped instance itself. """ + + _target_class_doc = "SomeClass" + + _dispatch_target = instrumentation.ClassManager + @classmethod - def _accept_with(cls, target): - if isinstance(target, orm.instrumentation.ClassManager): + def _new_classmanager_instance(cls, class_, classmanager): + _InstanceEventsHold.populate(class_, classmanager) + + @classmethod + @util.dependencies("sqlalchemy.orm") + def _accept_with(cls, orm, target): + if isinstance(target, instrumentation.ClassManager): return target - elif isinstance(target, orm.Mapper): + elif isinstance(target, mapperlib.Mapper): return target.class_manager elif target is orm.mapper: - return orm.instrumentation.ClassManager + return instrumentation.ClassManager elif isinstance(target, type): - if issubclass(target, orm.Mapper): - return orm.instrumentation.ClassManager + if issubclass(target, mapperlib.Mapper): + return instrumentation.ClassManager else: - manager = orm.instrumentation.manager_of_class(target) + manager = instrumentation.manager_of_class(target) if manager: return manager + else: + return _InstanceEventsHold(target) return None @classmethod - def _listen(cls, target, identifier, fn, raw=False, propagate=False): + def _listen(cls, event_key, raw=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + if not raw: orig_fn = fn + def wrap(state, *arg, **kw): return orig_fn(state.obj(), *arg, **kw) fn = wrap + event_key = event_key.with_wrapper(fn) + + event_key.base_listen(propagate=propagate) - event.Events._listen(target, identifier, fn, propagate=propagate) if propagate: for mgr in target.subclass_managers(True): - event.Events._listen(mgr, identifier, fn, True) + event_key.with_dispatch_target(mgr).base_listen(propagate=True) @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of instance events not yet implemented") + def _clear(cls): + super(InstanceEvents, cls)._clear() + _InstanceEventsHold._clear() def first_init(self, manager, cls): """Called when the first instance of a particular mapping is called. @@ -256,6 +332,81 @@ class InstanceEvents(event.Events): """ +class _EventsHold(event.RefCollection): + """Hold onto listeners against unmapped, uninstrumented classes. + + Establish _listen() for that class' mapper/instrumentation when + those objects are created for that class. + + """ + def __init__(self, class_): + self.class_ = class_ + + @classmethod + def _clear(cls): + cls.all_holds.clear() + + class HoldEvents(object): + _dispatch_target = None + + @classmethod + def _listen(cls, event_key, raw=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + + if target.class_ in target.all_holds: + collection = target.all_holds[target.class_] + else: + collection = target.all_holds[target.class_] = {} + + event.registry._stored_in_collection(event_key, target) + collection[event_key._key] = (event_key, raw, propagate) + + if propagate: + stack = list(target.class_.__subclasses__()) + while stack: + subclass = stack.pop(0) + stack.extend(subclass.__subclasses__()) + subject = target.resolve(subclass) + if subject is not None: + event_key.with_dispatch_target(subject).\ + listen(raw=raw, propagate=propagate) + + def remove(self, event_key): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + + collection = target.all_holds[target.class_] + del collection[event_key._key] + + @classmethod + def populate(cls, class_, subject): + for subclass in class_.__mro__: + if subclass in cls.all_holds: + collection = cls.all_holds[subclass] + for event_key, raw, propagate in collection.values(): + if propagate or subclass is class_: + # since we can't be sure in what order different classes + # in a hierarchy are triggered with populate(), + # we rely upon _EventsHold for all event + # assignment, instead of using the generic propagate + # flag. + event_key.with_dispatch_target(subject).\ + listen(raw=raw, propagate=False) + + +class _InstanceEventsHold(_EventsHold): + all_holds = weakref.WeakKeyDictionary() + + def resolve(self, class_): + return instrumentation.manager_of_class(class_) + + class HoldInstanceEvents(_EventsHold.HoldEvents, InstanceEvents): + pass + + dispatch = event.dispatcher(HoldInstanceEvents) + + class MapperEvents(event.Events): """Define events specific to mappings. @@ -270,23 +421,22 @@ class MapperEvents(event.Events): "select my_special_function(%d)" % target.special_number) - # associate the listener function with SomeMappedClass, + # associate the listener function with SomeClass, # to execute during the "before_insert" hook - event.listen(SomeMappedClass, 'before_insert', my_before_insert_listener) + event.listen( + SomeClass, 'before_insert', my_before_insert_listener) - Available targets include mapped classes, instances of - :class:`.Mapper` (i.e. returned by :func:`.mapper`, - :func:`.class_mapper` and similar), as well as the - :class:`.Mapper` class and :func:`.mapper` function itself - for global event reception:: + Available targets include: - from sqlalchemy.orm import mapper + * mapped classes + * unmapped superclasses of mapped or to-be-mapped classes + (using the ``propagate=True`` flag) + * :class:`.Mapper` objects + * the :class:`.Mapper` class itself and the :func:`.mapper` + function indicate listening for all mappers. - def some_listener(mapper, connection, target): - log.debug("Instance %s being inserted" % target) - - # attach to all mappers - event.listen(mapper, 'before_insert', some_listener) + .. versionchanged:: 0.8.0 mapper events can be associated with + unmapped superclasses of mapped classes. Mapper events provide hooks into critical sections of the mapper, including those related to object instrumentation, @@ -305,7 +455,8 @@ class MapperEvents(event.Events): available to the :func:`.event.listen` function. :param propagate=False: When True, the event listener should - be applied to all inheriting mappers as well as the + be applied to all inheriting mappers and/or the mappers of + inheriting classes, as well as any mapper which is the target of this listener. :param raw=False: When True, the "target" argument passed to applicable event listener functions will be the @@ -327,47 +478,69 @@ class MapperEvents(event.Events): """ + _target_class_doc = "SomeClass" + _dispatch_target = mapperlib.Mapper + @classmethod - def _accept_with(cls, target): + def _new_mapper_instance(cls, class_, mapper): + _MapperEventsHold.populate(class_, mapper) + + @classmethod + @util.dependencies("sqlalchemy.orm") + def _accept_with(cls, orm, target): if target is orm.mapper: - return orm.Mapper + return mapperlib.Mapper elif isinstance(target, type): - if issubclass(target, orm.Mapper): + if issubclass(target, mapperlib.Mapper): return target else: - return orm.class_mapper(target, compile=False) + mapper = _mapper_or_none(target) + if mapper is not None: + return mapper + else: + return _MapperEventsHold(target) else: return target @classmethod - def _listen(cls, target, identifier, fn, + def _listen(cls, event_key, raw=False, retval=False, propagate=False): + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn if not raw or not retval: if not raw: meth = getattr(cls, identifier) try: - target_index = inspect.getargspec(meth)[0].index('target') - 1 + target_index = \ + inspect.getargspec(meth)[0].index('target') - 1 except ValueError: target_index = None wrapped_fn = fn + def wrap(*arg, **kw): if not raw and target_index is not None: arg = list(arg) arg[target_index] = arg[target_index].obj() if not retval: wrapped_fn(*arg, **kw) - return orm.interfaces.EXT_CONTINUE + return interfaces.EXT_CONTINUE else: return wrapped_fn(*arg, **kw) fn = wrap + event_key = event_key.with_wrapper(wrap) if propagate: for mapper in target.self_and_descendants: - event.Events._listen(mapper, identifier, fn, propagate=True) + event_key.with_dispatch_target(mapper).base_listen(propagate=True) else: - event.Events._listen(target, identifier, fn) + event_key.base_listen() + + @classmethod + def _clear(cls): + super(MapperEvents, cls)._clear() + _MapperEventsHold._clear() def instrument_class(self, mapper, class_): """Receive a class when the mapper is first constructed, @@ -376,8 +549,15 @@ class MapperEvents(event.Events): This event is the earliest phase of mapper construction. Most attributes of the mapper are not yet initialized. - This listener can generally only be applied to the :class:`.Mapper` - class overall. + This listener can either be applied to the :class:`.Mapper` + class overall, or to any un-mapped class which serves as a base + for classes that will be mapped (using the ``propagate=True`` flag):: + + Base = declarative_base() + + @event.listens_for(Base, "instrument_class", propagate=True) + def on_new_class(mapper, cls_): + " ... " :param mapper: the :class:`.Mapper` which is the target of this event. @@ -389,9 +569,9 @@ class MapperEvents(event.Events): """Called when the mapper for the class is fully configured. This event is the latest phase of mapper construction, and - is invoked when the mapped classes are first used, so that relationships - between mappers can be resolved. When the event is called, - the mapper should be in its final state. + is invoked when the mapped classes are first used, so that + relationships between mappers can be resolved. When the event is + called, the mapper should be in its final state. While the configuration event normally occurs automatically, it can be forced to occur ahead of time, in the case where the event @@ -415,9 +595,9 @@ class MapperEvents(event.Events): Theoretically this event is called once per application, but is actually called any time new mappers - have been affected by a :func:`.orm.configure_mappers` call. If new mappings - are constructed after existing ones have already been used, - this event can be called again. + have been affected by a :func:`.orm.configure_mappers` + call. If new mappings are constructed after existing ones have + already been used, this event can be called again. """ @@ -505,7 +685,6 @@ class MapperEvents(event.Events): """ - def populate_instance(self, mapper, context, row, target, **flags): """Receive an instance before that instance has @@ -561,24 +740,26 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: + and via SQL operations with the given** + :class:`.Connection` **only.** Handlers here should **not** make + alterations to the state of the :class:`.Session` overall, and + in general should not affect any :func:`.relationship` -mapped + attributes, as session cascade rules will not function properly, + nor is it always known if the related class has already been + handled. Operations that **are not supported in mapper + events** include: * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, or + another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -617,24 +798,26 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: + and via SQL operations with the given** + :class:`.Connection` **only.** Handlers here should **not** make + alterations to the state of the :class:`.Session` overall, and in + general should not affect any :func:`.relationship` -mapped + attributes, as session cascade rules will not function properly, + nor is it always known if the related class has already been + handled. Operations that **are not supported in mapper + events** include: * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, + or another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -692,9 +875,9 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not + and via SQL operations with the given** :class:`.Connection` + **only.** Handlers here should **not** make alterations to the + state of the :class:`.Session` overall, and in general should not affect any :func:`.relationship` -mapped attributes, as session cascade rules will not function properly, nor is it always known if the related class has already been handled. @@ -703,13 +886,14 @@ class MapperEvents(event.Events): * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, + or another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -765,9 +949,9 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not + and via SQL operations with the given** :class:`.Connection` + **only.** Handlers here should **not** make alterations to the + state of the :class:`.Session` overall, and in general should not affect any :func:`.relationship` -mapped attributes, as session cascade rules will not function properly, nor is it always known if the related class has already been handled. @@ -776,13 +960,14 @@ class MapperEvents(event.Events): * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, + or another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -815,9 +1000,9 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not + and via SQL operations with the given** :class:`.Connection` + **only.** Handlers here should **not** make alterations to the + state of the :class:`.Session` overall, and in general should not affect any :func:`.relationship` -mapped attributes, as session cascade rules will not function properly, nor is it always known if the related class has already been handled. @@ -826,13 +1011,14 @@ class MapperEvents(event.Events): * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, + or another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -865,9 +1051,9 @@ class MapperEvents(event.Events): .. warning:: Mapper-level flush events are designed to operate **on attributes local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` **only.** - Handlers here should **not** make alterations to the state of - the :class:`.Session` overall, and in general should not + and via SQL operations with the given** :class:`.Connection` + **only.** Handlers here should **not** make alterations to the + state of the :class:`.Session` overall, and in general should not affect any :func:`.relationship` -mapped attributes, as session cascade rules will not function properly, nor is it always known if the related class has already been handled. @@ -876,13 +1062,14 @@ class MapperEvents(event.Events): * :meth:`.Session.add` * :meth:`.Session.delete` * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, i.e. ``someobject.related = someotherobject`` + * Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` Operations which manipulate the state of the object relative to other objects are better handled: - * In the ``__init__()`` method of the mapped object itself, or another method - designed to establish some particular state. + * In the ``__init__()`` method of the mapped object itself, + or another method designed to establish some particular state. * In a ``@validates`` handler, see :ref:`simple_validators` * Within the :meth:`.SessionEvents.before_flush` event. @@ -900,9 +1087,17 @@ class MapperEvents(event.Events): """ - @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of mapper events not yet implemented") +class _MapperEventsHold(_EventsHold): + all_holds = weakref.WeakKeyDictionary() + + def resolve(self, class_): + return _mapper_or_none(class_) + + class HoldMapperEvents(_EventsHold.HoldEvents, MapperEvents): + pass + + dispatch = event.dispatcher(HoldMapperEvents) + class SessionEvents(event.Events): """Define events specific to :class:`.Session` lifecycle. @@ -921,7 +1116,7 @@ class SessionEvents(event.Events): The :func:`~.event.listen` function will accept :class:`.Session` objects as well as the return result - of :func:`.sessionmaker` and :func:`.scoped_session`. + of :class:`~.sessionmaker()` and :class:`~.scoped_session()`. Additionally, it accepts the :class:`.Session` class which will apply listeners to all :class:`.Session` instances @@ -929,48 +1124,137 @@ class SessionEvents(event.Events): """ + _target_class_doc = "SomeSessionOrFactory" + + _dispatch_target = Session + @classmethod def _accept_with(cls, target): - if isinstance(target, orm.ScopedSession): - if not isinstance(target.session_factory, type) or \ - not issubclass(target.session_factory, orm.Session): + if isinstance(target, scoped_session): + + target = target.session_factory + if not isinstance(target, sessionmaker) and \ + ( + not isinstance(target, type) or + not issubclass(target, Session) + ): raise exc.ArgumentError( - "Session event listen on a ScopedSession " + "Session event listen on a scoped_session " "requires that its creation callable " - "is a Session subclass.") - return target.session_factory + "is associated with the Session class.") + + if isinstance(target, sessionmaker): + return target.class_ elif isinstance(target, type): - if issubclass(target, orm.ScopedSession): - return orm.Session - elif issubclass(target, orm.Session): + if issubclass(target, scoped_session): + return Session + elif issubclass(target, Session): return target - elif isinstance(target, orm.Session): + elif isinstance(target, Session): return target else: return None - @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of session events not yet implemented") + def after_transaction_create(self, session, transaction): + """Execute when a new :class:`.SessionTransaction` is created. + + This event differs from :meth:`~.SessionEvents.after_begin` + in that it occurs for each :class:`.SessionTransaction` + overall, as opposed to when transactions are begun + on individual database connections. It is also invoked + for nested transactions and subtransactions, and is always + matched by a corresponding + :meth:`~.SessionEvents.after_transaction_end` event + (assuming normal operation of the :class:`.Session`). + + :param session: the target :class:`.Session`. + :param transaction: the target :class:`.SessionTransaction`. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`~.SessionEvents.after_transaction_end` + + """ + + def after_transaction_end(self, session, transaction): + """Execute when the span of a :class:`.SessionTransaction` ends. + + This event differs from :meth:`~.SessionEvents.after_commit` + in that it corresponds to all :class:`.SessionTransaction` + objects in use, including those for nested transactions + and subtransactions, and is always matched by a corresponding + :meth:`~.SessionEvents.after_transaction_create` event. + + :param session: the target :class:`.Session`. + :param transaction: the target :class:`.SessionTransaction`. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`~.SessionEvents.after_transaction_create` + + """ def before_commit(self, session): """Execute before commit is called. - Note that this may not be per-flush if a longer running - transaction is ongoing. + .. note:: + + The :meth:`~.SessionEvents.before_commit` hook is *not* per-flush, + that is, the :class:`.Session` can emit SQL to the database + many times within the scope of a transaction. + For interception of these events, use the :meth:`~.SessionEvents.before_flush`, + :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec` + events. :param session: The target :class:`.Session`. + .. seealso:: + + :meth:`~.SessionEvents.after_commit` + + :meth:`~.SessionEvents.after_begin` + + :meth:`~.SessionEvents.after_transaction_create` + + :meth:`~.SessionEvents.after_transaction_end` + """ def after_commit(self, session): """Execute after a commit has occurred. - Note that this may not be per-flush if a longer running - transaction is ongoing. + .. note:: + + The :meth:`~.SessionEvents.after_commit` hook is *not* per-flush, + that is, the :class:`.Session` can emit SQL to the database + many times within the scope of a transaction. + For interception of these events, use the :meth:`~.SessionEvents.before_flush`, + :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec` + events. + + .. note:: + + The :class:`.Session` is not in an active tranasction + when the :meth:`~.SessionEvents.after_commit` event is invoked, and therefore + can not emit SQL. To emit SQL corresponding to every transaction, + use the :meth:`~.SessionEvents.before_commit` event. :param session: The target :class:`.Session`. + .. seealso:: + + :meth:`~.SessionEvents.before_commit` + + :meth:`~.SessionEvents.after_begin` + + :meth:`~.SessionEvents.after_transaction_create` + + :meth:`~.SessionEvents.after_transaction_end` + """ def after_rollback(self, session): @@ -1010,16 +1294,16 @@ class SessionEvents(event.Events): session.execute("select * from some_table") :param session: The target :class:`.Session`. - :param previous_transaction: The :class:`.SessionTransaction` transactional - marker object which was just closed. The current :class:`.SessionTransaction` - for the given :class:`.Session` is available via the - :attr:`.Session.transaction` attribute. + :param previous_transaction: The :class:`.SessionTransaction` + transactional marker object which was just closed. The current + :class:`.SessionTransaction` for the given :class:`.Session` is + available via the :attr:`.Session.transaction` attribute. .. versionadded:: 0.7.3 """ - def before_flush( self, session, flush_context, instances): + def before_flush(self, session, flush_context, instances): """Execute before flush process has started. :param session: The target :class:`.Session`. @@ -1029,6 +1313,12 @@ class SessionEvents(event.Events): objects which can be passed to the :meth:`.Session.flush` method (note this usage is deprecated). + .. seealso:: + + :meth:`~.SessionEvents.after_flush` + + :meth:`~.SessionEvents.after_flush_postexec` + """ def after_flush(self, session, flush_context): @@ -1043,6 +1333,12 @@ class SessionEvents(event.Events): :param flush_context: Internal :class:`.UOWTransaction` object which handles the details of the flush. + .. seealso:: + + :meth:`~.SessionEvents.before_flush` + + :meth:`~.SessionEvents.after_flush_postexec` + """ def after_flush_postexec(self, session, flush_context): @@ -1057,48 +1353,124 @@ class SessionEvents(event.Events): :param session: The target :class:`.Session`. :param flush_context: Internal :class:`.UOWTransaction` object which handles the details of the flush. + + + .. seealso:: + + :meth:`~.SessionEvents.before_flush` + + :meth:`~.SessionEvents.after_flush` + """ - def after_begin( self, session, transaction, connection): + def after_begin(self, session, transaction, connection): """Execute after a transaction is begun on a connection :param session: The target :class:`.Session`. :param transaction: The :class:`.SessionTransaction`. - :param connection: The :class:`~.engine.base.Connection` object + :param connection: The :class:`~.engine.Connection` object which will be used for SQL statements. + .. seealso:: + + :meth:`~.SessionEvents.before_commit` + + :meth:`~.SessionEvents.after_commit` + + :meth:`~.SessionEvents.after_transaction_create` + + :meth:`~.SessionEvents.after_transaction_end` + + """ + + def before_attach(self, session, instance): + """Execute before an instance is attached to a session. + + This is called before an add, delete or merge causes + the object to be part of the session. + + .. versionadded:: 0.8. Note that :meth:`~.SessionEvents.after_attach` now + fires off after the item is part of the session. + :meth:`.before_attach` is provided for those cases where + the item should not yet be part of the session state. + + .. seealso:: + + :meth:`~.SessionEvents.after_attach` + """ def after_attach(self, session, instance): """Execute after an instance is attached to a session. - This is called after an add, delete or merge. """ + This is called after an add, delete or merge. - def after_bulk_update( self, session, query, query_context, result): + .. note:: + + As of 0.8, this event fires off *after* the item + has been fully associated with the session, which is + different than previous releases. For event + handlers that require the object not yet + be part of session state (such as handlers which + may autoflush while the target object is not + yet complete) consider the + new :meth:`.before_attach` event. + + .. seealso:: + + :meth:`~.SessionEvents.before_attach` + + """ + + @event._legacy_signature("0.9", + ["session", "query", "query_context", "result"], + lambda update_context: ( + update_context.session, + update_context.query, + update_context.context, + update_context.result)) + def after_bulk_update(self, update_context): """Execute after a bulk update operation to the session. This is called as a result of the :meth:`.Query.update` method. - :param query: the :class:`.Query` object that this update operation was - called upon. - :param query_context: The :class:`.QueryContext` object, corresponding - to the invocation of an ORM query. - :param result: the :class:`.ResultProxy` returned as a result of the - bulk UPDATE operation. + :param update_context: an "update context" object which contains + details about the update, including these attributes: + + * ``session`` - the :class:`.Session` involved + * ``query`` -the :class:`.Query` object that this update operation was + called upon. + * ``context`` The :class:`.QueryContext` object, corresponding + to the invocation of an ORM query. + * ``result`` the :class:`.ResultProxy` returned as a result of the + bulk UPDATE operation. + """ - def after_bulk_delete( self, session, query, query_context, result): + @event._legacy_signature("0.9", + ["session", "query", "query_context", "result"], + lambda delete_context: ( + delete_context.session, + delete_context.query, + delete_context.context, + delete_context.result)) + def after_bulk_delete(self, delete_context): """Execute after a bulk delete operation to the session. This is called as a result of the :meth:`.Query.delete` method. - :param query: the :class:`.Query` object that this update operation was - called upon. - :param query_context: The :class:`.QueryContext` object, corresponding - to the invocation of an ORM query. - :param result: the :class:`.ResultProxy` returned as a result of the - bulk DELETE operation. + :param delete_context: a "delete context" object which contains + details about the update, including these attributes: + + * ``session`` - the :class:`.Session` involved + * ``query`` -the :class:`.Query` object that this update operation was + called upon. + * ``context`` The :class:`.QueryContext` object, corresponding + to the invocation of an ORM query. + * ``result`` the :class:`.ResultProxy` returned as a result of the + bulk DELETE operation. + """ @@ -1132,7 +1504,7 @@ class AttributeEvents(event.Events): listen(UserContact.phone, 'set', validate_phone, retval=True) A validation function like the above can also raise an exception - such as :class:`.ValueError` to halt the operation. + such as :exc:`ValueError` to halt the operation. Several modifiers are available to the :func:`~.event.listen` function. @@ -1160,26 +1532,36 @@ class AttributeEvents(event.Events): """ + _target_class_doc = "SomeClass.some_attribute" + _dispatch_target = QueryableAttribute + + @staticmethod + def _set_dispatch(cls, dispatch_cls): + event.Events._set_dispatch(cls, dispatch_cls) + dispatch_cls._active_history = False + @classmethod def _accept_with(cls, target): # TODO: coverage - if isinstance(target, orm.interfaces.MapperProperty): + if isinstance(target, interfaces.MapperProperty): return getattr(target.parent.class_, target.key) else: return target @classmethod - def _listen(cls, target, identifier, fn, active_history=False, + def _listen(cls, event_key, active_history=False, raw=False, retval=False, propagate=False): + + target, identifier, fn = \ + event_key.dispatch_target, event_key.identifier, event_key.fn + if active_history: target.dispatch._active_history = True - # TODO: for removal, need to package the identity - # of the wrapper with the original function. - if not raw or not retval: orig_fn = fn + def wrap(target, value, *arg): if not raw: target = target.obj() @@ -1189,18 +1571,15 @@ class AttributeEvents(event.Events): else: return orig_fn(target, value, *arg) fn = wrap + event_key = event_key.with_wrapper(wrap) - event.Events._listen(target, identifier, fn, propagate) + event_key.base_listen(propagate=propagate) if propagate: - manager = orm.instrumentation.manager_of_class(target.class_) + manager = instrumentation.manager_of_class(target.class_) for mgr in manager.subclass_managers(True): - event.Events._listen(mgr[target.key], identifier, fn, True) - - @classmethod - def _remove(cls, identifier, target, fn): - raise NotImplementedError("Removal of attribute events not yet implemented") + event_key.with_dispatch_target(mgr[target.key]).base_listen(propagate=True) def append(self, target, value, initiator): """Receive a collection append event. @@ -1212,8 +1591,15 @@ class AttributeEvents(event.Events): is registered with ``retval=True``, the listener function must return this value, or a new value which replaces it. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: if the event was registered with ``retval=True``, the given value, or a new effective value, should be returned. @@ -1226,8 +1612,15 @@ class AttributeEvents(event.Events): If the listener is registered with ``raw=True``, this will be the :class:`.InstanceState` object. :param value: the value being removed. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: No return value is defined for this event. """ @@ -1247,8 +1640,15 @@ class AttributeEvents(event.Events): the previous value of the attribute will be loaded from the database if the existing value is currently unloaded or expired. - :param initiator: the attribute implementation object - which initiated this event. + :param initiator: An instance of :class:`.attributes.Event` + representing the initiation of the event. May be modified + from it's original value by backref handlers in order to control + chained event propagation. + + .. versionchanged:: 0.9.0 the ``initiator`` argument is now + passed as a :class:`.attributes.Event` object, and may be modified + by backref handlers within a chain of backref-linked events. + :return: if the event was registered with ``retval=True``, the given value, or a new effective value, should be returned. diff --git a/libs/sqlalchemy/orm/exc.py b/libs/sqlalchemy/orm/exc.py index a116d204..d1ef1ded 100644 --- a/libs/sqlalchemy/orm/exc.py +++ b/libs/sqlalchemy/orm/exc.py @@ -1,18 +1,17 @@ # orm/exc.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """SQLAlchemy ORM exceptions.""" - -import sqlalchemy as sa -orm_util = sa.util.importlater('sqlalchemy.orm', 'util') +from .. import exc as sa_exc, util NO_STATE = (AttributeError, KeyError) """Exception types that may be raised by instrumentation implementations.""" -class StaleDataError(sa.exc.SQLAlchemyError): + +class StaleDataError(sa_exc.SQLAlchemyError): """An operation encountered database state that is unaccounted for. Conditions which cause this to happen include: @@ -41,31 +40,39 @@ class StaleDataError(sa.exc.SQLAlchemyError): ConcurrentModificationError = StaleDataError -class FlushError(sa.exc.SQLAlchemyError): +class FlushError(sa_exc.SQLAlchemyError): """A invalid condition was detected during flush().""" -class UnmappedError(sa.exc.InvalidRequestError): +class UnmappedError(sa_exc.InvalidRequestError): """Base for exceptions that involve expected mappings not present.""" -class ObjectDereferencedError(sa.exc.SQLAlchemyError): - """An operation cannot complete due to an object being garbage collected.""" -class DetachedInstanceError(sa.exc.SQLAlchemyError): +class ObjectDereferencedError(sa_exc.SQLAlchemyError): + """An operation cannot complete due to an object being garbage + collected. + + """ + + +class DetachedInstanceError(sa_exc.SQLAlchemyError): """An attempt to access unloaded attributes on a mapped instance that is detached.""" + class UnmappedInstanceError(UnmappedError): """An mapping operation was requested for an unknown instance.""" - def __init__(self, obj, msg=None): + @util.dependencies("sqlalchemy.orm.base") + def __init__(self, base, obj, msg=None): if not msg: try: - mapper = sa.orm.class_mapper(type(obj)) + base.class_mapper(type(obj)) name = _safe_cls_name(type(obj)) msg = ("Class %r is mapped, but this instance lacks " - "instrumentation. This occurs when the instance is created " - "before sqlalchemy.orm.mapper(%s) was called." % (name, name)) + "instrumentation. This occurs when the instance" + "is created before sqlalchemy.orm.mapper(%s) " + "was called." % (name, name)) except UnmappedClassError: msg = _default_unmapped(type(obj)) if isinstance(obj, type): @@ -77,6 +84,7 @@ class UnmappedInstanceError(UnmappedError): def __reduce__(self): return self.__class__, (None, self.args[0]) + class UnmappedClassError(UnmappedError): """An mapping operation was requested for an unknown class.""" @@ -88,7 +96,8 @@ class UnmappedClassError(UnmappedError): def __reduce__(self): return self.__class__, (None, self.args[0]) -class ObjectDeletedError(sa.exc.InvalidRequestError): + +class ObjectDeletedError(sa_exc.InvalidRequestError): """A refresh operation failed to retrieve the database row corresponding to an object's known primary key identity. @@ -107,33 +116,30 @@ class ObjectDeletedError(sa.exc.InvalidRequestError): object. """ - def __init__(self, state, msg=None): + @util.dependencies("sqlalchemy.orm.base") + def __init__(self, base, state, msg=None): if not msg: msg = "Instance '%s' has been deleted, or its "\ - "row is otherwise not present." % orm_util.state_str(state) + "row is otherwise not present." % base.state_str(state) - sa.exc.InvalidRequestError.__init__(self, msg) + sa_exc.InvalidRequestError.__init__(self, msg) def __reduce__(self): return self.__class__, (None, self.args[0]) -class UnmappedColumnError(sa.exc.InvalidRequestError): + +class UnmappedColumnError(sa_exc.InvalidRequestError): """Mapping operation was requested on an unknown column.""" -class NoResultFound(sa.exc.InvalidRequestError): +class NoResultFound(sa_exc.InvalidRequestError): """A database result was required but none was found.""" -class MultipleResultsFound(sa.exc.InvalidRequestError): +class MultipleResultsFound(sa_exc.InvalidRequestError): """A single database result was required but more than one were found.""" -# Legacy compat until 0.6. -sa.exc.ConcurrentModificationError = ConcurrentModificationError -sa.exc.FlushError = FlushError -sa.exc.UnmappedColumnError - def _safe_cls_name(cls): try: cls_name = '.'.join((cls.__module__, cls.__name__)) @@ -143,9 +149,10 @@ def _safe_cls_name(cls): cls_name = repr(cls) return cls_name -def _default_unmapped(cls): +@util.dependencies("sqlalchemy.orm.base") +def _default_unmapped(base, cls): try: - mappers = sa.orm.attributes.manager_of_class(cls).mappers + mappers = base.manager_of_class(cls).mappers except NO_STATE: mappers = {} except TypeError: diff --git a/libs/sqlalchemy/orm/identity.py b/libs/sqlalchemy/orm/identity.py index 29e13f2c..a91085d2 100644 --- a/libs/sqlalchemy/orm/identity.py +++ b/libs/sqlalchemy/orm/identity.py @@ -1,16 +1,15 @@ # orm/identity.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php import weakref -from sqlalchemy.orm import attributes - +from . import attributes +from .. import util class IdentityMap(dict): def __init__(self): - self._mutable_attrs = set() self._modified = set() self._wr = weakref.ref(self) @@ -31,28 +30,20 @@ class IdentityMap(dict): if state.modified: self._modified.add(state) - if state.manager.mutable_attributes: - self._mutable_attrs.add(state) def _manage_removed_state(self, state): del state._instance_dict - self._mutable_attrs.discard(state) self._modified.discard(state) def _dirty_states(self): - return self._modified.union(s for s in self._mutable_attrs.copy() - if s.modified) + return self._modified def check_modified(self): - """return True if any InstanceStates present have been marked as 'modified'.""" + """return True if any InstanceStates present have been marked + as 'modified'. - if self._modified: - return True - else: - for state in self._mutable_attrs.copy(): - if state.modified: - return True - return False + """ + return bool(self._modified) def has_key(self, key): return key in self @@ -75,6 +66,7 @@ class IdentityMap(dict): def __delitem__(self, key): raise NotImplementedError("IdentityMap uses remove() to remove data") + class WeakInstanceDict(IdentityMap): def __init__(self): IdentityMap.__init__(self) @@ -83,9 +75,7 @@ class WeakInstanceDict(IdentityMap): state = dict.__getitem__(self, key) o = state.obj() if o is None: - o = state._is_really_none() - if o is None: - raise KeyError, key + raise KeyError(key) return o def __contains__(self, key): @@ -93,8 +83,6 @@ class WeakInstanceDict(IdentityMap): if dict.__contains__(self, key): state = dict.__getitem__(self, key) o = state.obj() - if o is None: - o = state._is_really_none() else: return False except KeyError: @@ -124,12 +112,11 @@ class WeakInstanceDict(IdentityMap): existing_state = dict.__getitem__(self, key) if existing_state is not state: o = existing_state.obj() - if o is None: - o = existing_state._is_really_none() if o is not None: - raise AssertionError("A conflicting state is already " - "present in the identity map for key %r" - % (key, )) + raise AssertionError( + "A conflicting state is already " + "present in the identity map for key %r" + % (key, )) else: return except KeyError: @@ -143,9 +130,7 @@ class WeakInstanceDict(IdentityMap): return default o = state.obj() if o is None: - o = state._is_really_none() - if o is None: - return default + return default return o def _items(self): @@ -167,28 +152,27 @@ class WeakInstanceDict(IdentityMap): return result - # Py3K - #def items(self): - # return iter(self._items()) - # - #def values(self): - # return iter(self._values()) - # Py2K - items = _items - def iteritems(self): - return iter(self.items()) + if util.py2k: + items = _items + values = _values - values = _values - def itervalues(self): - return iter(self.values()) - # end Py2K + def iteritems(self): + return iter(self.items()) + + def itervalues(self): + return iter(self.values()) + else: + def items(self): + return iter(self._items()) + + def values(self): + return iter(self._values()) def all_states(self): - # Py3K - # return list(dict.values(self)) - # Py2K - return dict.values(self) - # end Py2K + if util.py2k: + return dict.values(self) + else: + return list(dict.values(self)) def discard(self, state): st = dict.get(self, state.key, None) @@ -199,12 +183,15 @@ class WeakInstanceDict(IdentityMap): def prune(self): return 0 + class StrongInstanceDict(IdentityMap): def all_states(self): - return [attributes.instance_state(o) for o in self.itervalues()] + return [attributes.instance_state(o) for o in self.values()] def contains_state(self, state): - return state.key in self and attributes.instance_state(self[state.key]) is state + return ( + state.key in self and + attributes.instance_state(self[state.key]) is state) def replace(self, state): if dict.__contains__(self, state.key): @@ -251,4 +238,3 @@ class StrongInstanceDict(IdentityMap): dict.update(self, keepers) self.modified = bool(dirty) return ref_count - len(self) - diff --git a/libs/sqlalchemy/orm/instrumentation.py b/libs/sqlalchemy/orm/instrumentation.py index 0006accb..68b4f061 100644 --- a/libs/sqlalchemy/orm/instrumentation.py +++ b/libs/sqlalchemy/orm/instrumentation.py @@ -1,5 +1,5 @@ # orm/instrumentation.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -14,79 +14,41 @@ for state tracking. It interacts closely with state.py and attributes.py which establish per-instance and per-class-attribute instrumentation, respectively. -SQLA's instrumentation system is completely customizable, in which -case an understanding of the general mechanics of this module is helpful. -An example of full customization is in /examples/custom_attributes. +The class instrumentation system can be customized on a per-class +or global basis using the :mod:`sqlalchemy.ext.instrumentation` +module, which provides the means to build and specify +alternate instrumentation forms. + +.. versionchanged: 0.8 + The instrumentation extension system was moved out of the + ORM and into the external :mod:`sqlalchemy.ext.instrumentation` + package. When that package is imported, it installs + itself within sqlalchemy.orm so that its more comprehensive + resolution mechanics take effect. """ -from sqlalchemy.orm import exc, collections, events -from operator import attrgetter, itemgetter -from sqlalchemy import event, util -import weakref -from sqlalchemy.orm import state, attributes - - -INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__' -"""Attribute, elects custom instrumentation when present on a mapped class. - -Allows a class to specify a slightly or wildly different technique for -tracking changes made to mapped attributes and collections. - -Only one instrumentation implementation is allowed in a given object -inheritance hierarchy. - -The value of this attribute must be a callable and will be passed a class -object. The callable must return one of: - - - An instance of an interfaces.InstrumentationManager or subclass - - An object implementing all or some of InstrumentationManager (TODO) - - A dictionary of callables, implementing all or some of the above (TODO) - - An instance of a ClassManager or subclass - -interfaces.InstrumentationManager is public API and will remain stable -between releases. ClassManager is not public and no guarantees are made -about stability. Caveat emptor. - -This attribute is consulted by the default SQLAlchemy instrumentation -resolution code. If custom finders are installed in the global -instrumentation_finders list, they may or may not choose to honor this -attribute. - -""" - -instrumentation_finders = [] -"""An extensible sequence of instrumentation implementation finding callables. - -Finders callables will be passed a class object. If None is returned, the -next finder in the sequence is consulted. Otherwise the return must be an -instrumentation factory that follows the same guidelines as -INSTRUMENTATION_MANAGER. - -By default, the only finder is find_native_user_instrumentation_hook, which -searches for INSTRUMENTATION_MANAGER. If all finders return None, standard -ClassManager instrumentation is used. - -""" - +from . import exc, collections, interfaces, state +from .. import util +from . import base class ClassManager(dict): """tracks state information at the class level.""" - MANAGER_ATTR = '_sa_class_manager' - STATE_ATTR = '_sa_instance_state' + MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR + STATE_ATTR = base.DEFAULT_STATE_ATTR deferred_scalar_loader = None original_init = object.__init__ + factory = None + def __init__(self, class_): self.class_ = class_ - self.factory = None # where we came from, for inheritance bookkeeping self.info = {} self.new_init = None - self.mutable_attributes = set() self.local_attrs = {} self.originals = {} @@ -99,10 +61,28 @@ class ClassManager(dict): for base in self._bases: self.update(base) + self.dispatch._events._new_classmanager_instance(class_, self) + #events._InstanceEventsHold.populate(class_, self) + + for basecls in class_.__mro__: + mgr = manager_of_class(basecls) + if mgr is not None: + self.dispatch._update(mgr.dispatch) self.manage() self._instrument_init() - dispatch = event.dispatcher(events.InstanceEvents) + if '__del__' in class_.__dict__: + util.warn("__del__() method on class %s will " + "cause unreachable cycles and memory leaks, " + "as SQLAlchemy instrumentation often creates " + "reference cycles. Please remove this method." % + class_) + + def __hash__(self): + return id(self) + + def __eq__(self, other): + return other is self @property def is_mapped(self): @@ -113,6 +93,24 @@ class ClassManager(dict): # raises unless self.mapper has been assigned raise exc.UnmappedClassError(self.class_) + def _all_sqla_attributes(self, exclude=None): + """return an iterator of all classbound attributes that are + implement :class:`._InspectionAttr`. + + This includes :class:`.QueryableAttribute` as well as extension + types such as :class:`.hybrid_property` and :class:`.AssociationProxy`. + + """ + if exclude is None: + exclude = set() + for supercls in self.class_.__mro__: + for key in set(supercls.__dict__).difference(exclude): + exclude.add(key) + val = supercls.__dict__[key] + if isinstance(val, interfaces._InspectionAttr): + yield key, val + + def _attr_has_impl(self, key): """Return True if the given attribute is fully initialized. @@ -134,7 +132,7 @@ class ClassManager(dict): """ manager = manager_of_class(cls) if manager is None: - manager = _create_manager_for_cls(cls, _source=self) + manager = _instrumentation_factory.create_manager_for_cls(cls) return manager def _instrument_init(self): @@ -155,10 +153,7 @@ class ClassManager(dict): @util.memoized_property def _state_constructor(self): self.dispatch.first_init(self, self.class_) - if self.mutable_attributes: - return state.MutableAttrInstanceState - else: - return state.InstanceState + return state.InstanceState def manage(self): """Mark this instance as the manager for its class.""" @@ -170,8 +165,25 @@ class ClassManager(dict): delattr(self.class_, self.MANAGER_ATTR) + @util.hybridmethod def manager_getter(self): - return attrgetter(self.MANAGER_ATTR) + return _default_manager_getter + + @util.hybridmethod + def state_getter(self): + """Return a (instance) -> InstanceState callable. + + "state getter" callables should raise either KeyError or + AttributeError if no InstanceState could be found for the + instance. + """ + + return _default_state_getter + + @util.hybridmethod + def dict_getter(self): + return _default_dict_getter + def instrument_attribute(self, key, inst, propagated=False): if propagated: @@ -196,7 +208,7 @@ class ClassManager(dict): yield m def post_configure_attribute(self, key): - instrumentation_registry.dispatch.\ + _instrumentation_factory.dispatch.\ attribute_instrument(self.class_, key, self[key]) def uninstrument_attribute(self, key, propagated=False): @@ -209,8 +221,6 @@ class ClassManager(dict): del self.local_attrs[key] self.uninstall_descriptor(key) del self[key] - if key in self.mutable_attributes: - self.mutable_attributes.remove(key) for cls in self.class_.__subclasses__(): manager = manager_of_class(cls) if manager: @@ -271,7 +281,7 @@ class ClassManager(dict): @property def attributes(self): - return self.itervalues() + return iter(self.values()) ## InstanceState management @@ -288,6 +298,9 @@ class ClassManager(dict): def teardown_instance(self, instance): delattr(instance, self.STATE_ATTR) + def _serialize(self, state, state_dict): + return _SerializeManager(state, state_dict) + def _new_state_if_none(self, instance): """Install a default InstanceState if none is present. @@ -310,19 +323,6 @@ class ClassManager(dict): setattr(instance, self.STATE_ATTR, state) return state - def state_getter(self): - """Return a (instance) -> InstanceState callable. - - "state getter" callables should raise either KeyError or - AttributeError if no InstanceState could be found for the - instance. - """ - - return attrgetter(self.STATE_ATTR) - - def dict_getter(self): - return attrgetter('__dict__') - def has_state(self, instance): return hasattr(instance, self.STATE_ATTR) @@ -330,123 +330,116 @@ class ClassManager(dict): """TODO""" return self.get_impl(key).hasparent(state, optimistic=optimistic) - def __nonzero__(self): + def __bool__(self): """All ClassManagers are non-zero regardless of attribute state.""" return True + __nonzero__ = __bool__ + def __repr__(self): return '<%s of %r at %x>' % ( self.__class__.__name__, self.class_, id(self)) -class _ClassInstrumentationAdapter(ClassManager): - """Adapts a user-defined InstrumentationManager to a ClassManager.""" +class _SerializeManager(object): + """Provide serialization of a :class:`.ClassManager`. - def __init__(self, class_, override, **kw): - self._adapted = override - self._get_state = self._adapted.state_getter(class_) - self._get_dict = self._adapted.dict_getter(class_) + The :class:`.InstanceState` uses ``__init__()`` on serialize + and ``__call__()`` on deserialize. - ClassManager.__init__(self, class_, **kw) + """ + def __init__(self, state, d): + self.class_ = state.class_ + manager = state.manager + manager.dispatch.pickle(state, d) - def manage(self): - self._adapted.manage(self.class_, self) + def __call__(self, state, inst, state_dict): + state.manager = manager = manager_of_class(self.class_) + if manager is None: + raise exc.UnmappedInstanceError( + inst, + "Cannot deserialize object of type %r - " + "no mapper() has " + "been configured for this class within the current " + "Python process!" % + self.class_) + elif manager.is_mapped and not manager.mapper.configured: + manager.mapper._configure_all() - def dispose(self): - self._adapted.dispose(self.class_) + # setup _sa_instance_state ahead of time so that + # unpickle events can access the object normally. + # see [ticket:2362] + if inst is not None: + manager.setup_instance(inst, state) + manager.dispatch.unpickle(state, state_dict) - def manager_getter(self): - return self._adapted.manager_getter(self.class_) +class InstrumentationFactory(object): + """Factory for new ClassManager instances.""" - def instrument_attribute(self, key, inst, propagated=False): - ClassManager.instrument_attribute(self, key, inst, propagated) - if not propagated: - self._adapted.instrument_attribute(self.class_, key, inst) + def create_manager_for_cls(self, class_): + assert class_ is not None + assert manager_of_class(class_) is None - def post_configure_attribute(self, key): - super(_ClassInstrumentationAdapter, self).post_configure_attribute(key) - self._adapted.post_configure_attribute(self.class_, key, self[key]) + # give a more complicated subclass + # a chance to do what it wants here + manager, factory = self._locate_extended_factory(class_) - def install_descriptor(self, key, inst): - self._adapted.install_descriptor(self.class_, key, inst) + if factory is None: + factory = ClassManager + manager = factory(class_) - def uninstall_descriptor(self, key): - self._adapted.uninstall_descriptor(self.class_, key) + self._check_conflicts(class_, factory) - def install_member(self, key, implementation): - self._adapted.install_member(self.class_, key, implementation) + manager.factory = factory - def uninstall_member(self, key): - self._adapted.uninstall_member(self.class_, key) + self.dispatch.class_instrument(class_) + return manager - def instrument_collection_class(self, key, collection_class): - return self._adapted.instrument_collection_class( - self.class_, key, collection_class) + def _locate_extended_factory(self, class_): + """Overridden by a subclass to do an extended lookup.""" + return None, None - def initialize_collection(self, key, state, factory): - delegate = getattr(self._adapted, 'initialize_collection', None) - if delegate: - return delegate(key, state, factory) - else: - return ClassManager.initialize_collection(self, key, - state, factory) + def _check_conflicts(self, class_, factory): + """Overridden by a subclass to test for conflicting factories.""" + return - def new_instance(self, state=None): - instance = self.class_.__new__(self.class_) - self.setup_instance(instance, state) - return instance + def unregister(self, class_): + manager = manager_of_class(class_) + manager.unregister() + manager.dispose() + self.dispatch.class_uninstrument(class_) + if ClassManager.MANAGER_ATTR in class_.__dict__: + delattr(class_, ClassManager.MANAGER_ATTR) - def _new_state_if_none(self, instance): - """Install a default InstanceState if none is present. +# this attribute is replaced by sqlalchemy.ext.instrumentation +# when importred. +_instrumentation_factory = InstrumentationFactory() - A private convenience method used by the __init__ decorator. - """ - if self.has_state(instance): - return False - else: - return self.setup_instance(instance) +# these attributes are replaced by sqlalchemy.ext.instrumentation +# when a non-standard InstrumentationManager class is first +# used to instrument a class. +instance_state = _default_state_getter = base.instance_state - def setup_instance(self, instance, state=None): - self._adapted.initialize_instance_dict(self.class_, instance) +instance_dict = _default_dict_getter = base.instance_dict - if state is None: - state = self._state_constructor(instance, self) +manager_of_class = _default_manager_getter = base.manager_of_class - # the given instance is assumed to have no state - self._adapted.install_state(self.class_, instance, state) - return state - - def teardown_instance(self, instance): - self._adapted.remove_state(self.class_, instance) - - def has_state(self, instance): - try: - state = self._get_state(instance) - except exc.NO_STATE: - return False - else: - return True - - def state_getter(self): - return self._get_state - - def dict_getter(self): - return self._get_dict - -def register_class(class_, **kw): +def register_class(class_): """Register class instrumentation. Returns the existing or newly created class manager. + """ manager = manager_of_class(class_) if manager is None: - manager = _create_manager_for_cls(class_, **kw) + manager = _instrumentation_factory.create_manager_for_cls(class_) return manager + def unregister_class(class_): """Unregister class instrumentation.""" - instrumentation_registry.unregister(class_) + _instrumentation_factory.unregister(class_) def is_instrumented(instance, key): @@ -460,174 +453,6 @@ def is_instrumented(instance, key): return manager_of_class(instance.__class__).\ is_instrumented(key, search=True) -class InstrumentationRegistry(object): - """Private instrumentation registration singleton. - - All classes are routed through this registry - when first instrumented, however the InstrumentationRegistry - is not actually needed unless custom ClassManagers are in use. - - """ - - _manager_finders = weakref.WeakKeyDictionary() - _state_finders = util.WeakIdentityMapping() - _dict_finders = util.WeakIdentityMapping() - _extended = False - - dispatch = event.dispatcher(events.InstrumentationEvents) - - def create_manager_for_cls(self, class_, **kw): - assert class_ is not None - assert manager_of_class(class_) is None - - for finder in instrumentation_finders: - factory = finder(class_) - if factory is not None: - break - else: - factory = ClassManager - - existing_factories = self._collect_management_factories_for(class_).\ - difference([factory]) - if existing_factories: - raise TypeError( - "multiple instrumentation implementations specified " - "in %s inheritance hierarchy: %r" % ( - class_.__name__, list(existing_factories))) - - manager = factory(class_) - if not isinstance(manager, ClassManager): - manager = _ClassInstrumentationAdapter(class_, manager) - - if factory != ClassManager and not self._extended: - # somebody invoked a custom ClassManager. - # reinstall global "getter" functions with the more - # expensive ones. - self._extended = True - _install_lookup_strategy(self) - - manager.factory = factory - self._manager_finders[class_] = manager.manager_getter() - self._state_finders[class_] = manager.state_getter() - self._dict_finders[class_] = manager.dict_getter() - - self.dispatch.class_instrument(class_) - - return manager - - def _collect_management_factories_for(self, cls): - """Return a collection of factories in play or specified for a - hierarchy. - - Traverses the entire inheritance graph of a cls and returns a - collection of instrumentation factories for those classes. Factories - are extracted from active ClassManagers, if available, otherwise - instrumentation_finders is consulted. - - """ - hierarchy = util.class_hierarchy(cls) - factories = set() - for member in hierarchy: - manager = manager_of_class(member) - if manager is not None: - factories.add(manager.factory) - else: - for finder in instrumentation_finders: - factory = finder(member) - if factory is not None: - break - else: - factory = None - factories.add(factory) - factories.discard(None) - return factories - - def manager_of_class(self, cls): - # this is only called when alternate instrumentation - # has been established - if cls is None: - return None - try: - finder = self._manager_finders[cls] - except KeyError: - return None - else: - return finder(cls) - - def state_of(self, instance): - # this is only called when alternate instrumentation - # has been established - if instance is None: - raise AttributeError("None has no persistent state.") - try: - return self._state_finders[instance.__class__](instance) - except KeyError: - raise AttributeError("%r is not instrumented" % - instance.__class__) - - def dict_of(self, instance): - # this is only called when alternate instrumentation - # has been established - if instance is None: - raise AttributeError("None has no persistent state.") - try: - return self._dict_finders[instance.__class__](instance) - except KeyError: - raise AttributeError("%r is not instrumented" % - instance.__class__) - - def unregister(self, class_): - if class_ in self._manager_finders: - manager = self.manager_of_class(class_) - self.dispatch.class_uninstrument(class_) - manager.unregister() - manager.dispose() - del self._manager_finders[class_] - del self._state_finders[class_] - del self._dict_finders[class_] - if ClassManager.MANAGER_ATTR in class_.__dict__: - delattr(class_, ClassManager.MANAGER_ATTR) - -instrumentation_registry = InstrumentationRegistry() - - -def _install_lookup_strategy(implementation): - """Replace global class/object management functions - with either faster or more comprehensive implementations, - based on whether or not extended class instrumentation - has been detected. - - This function is called only by InstrumentationRegistry() - and unit tests specific to this behavior. - - """ - global instance_state, instance_dict, manager_of_class - if implementation is util.symbol('native'): - instance_state = attrgetter(ClassManager.STATE_ATTR) - instance_dict = attrgetter("__dict__") - def manager_of_class(cls): - return cls.__dict__.get(ClassManager.MANAGER_ATTR, None) - else: - instance_state = instrumentation_registry.state_of - instance_dict = instrumentation_registry.dict_of - manager_of_class = instrumentation_registry.manager_of_class - attributes.instance_state = instance_state - attributes.instance_dict = instance_dict - attributes.manager_of_class = manager_of_class - -_create_manager_for_cls = instrumentation_registry.create_manager_for_cls - -# Install default "lookup" strategies. These are basically -# very fast attrgetters for key attributes. -# When a custom ClassManager is installed, more expensive per-class -# strategies are copied over these. -_install_lookup_strategy(util.symbol('native')) - - -def find_native_user_instrumentation_hook(cls): - """Find user-specified instrumentation management for a class.""" - return getattr(cls, INSTRUMENTATION_MANAGER, None) -instrumentation_finders.append(find_native_user_instrumentation_hook) def _generate_init(class_, class_manager): """Build an __init__ decorator that triggers ClassManager events.""" @@ -647,28 +472,28 @@ def _generate_init(class_, class_manager): def __init__(%(apply_pos)s): new_state = class_manager._new_state_if_none(%(self_arg)s) if new_state: - return new_state.initialize_instance(%(apply_kw)s) + return new_state._initialize_instance(%(apply_kw)s) else: return original__init__(%(apply_kw)s) """ func_vars = util.format_argspec_init(original__init__, grouped=False) func_text = func_body % func_vars - # Py3K - #func_defaults = getattr(original__init__, '__defaults__', None) - #func_kw_defaults = getattr(original__init__, '__kwdefaults__', None) - # Py2K - func = getattr(original__init__, 'im_func', original__init__) - func_defaults = getattr(func, 'func_defaults', None) - # end Py2K + if util.py2k: + func = getattr(original__init__, 'im_func', original__init__) + func_defaults = getattr(func, 'func_defaults', None) + else: + func_defaults = getattr(original__init__, '__defaults__', None) + func_kw_defaults = getattr(original__init__, '__kwdefaults__', None) env = locals().copy() - exec func_text in env + exec(func_text, env) __init__ = env['__init__'] __init__.__doc__ = original__init__.__doc__ + if func_defaults: - __init__.func_defaults = func_defaults - # Py3K - #if func_kw_defaults: - # __init__.__kwdefaults__ = func_kw_defaults + __init__.__defaults__ = func_defaults + if not util.py2k and func_kw_defaults: + __init__.__kwdefaults__ = func_kw_defaults + return __init__ diff --git a/libs/sqlalchemy/orm/interfaces.py b/libs/sqlalchemy/orm/interfaces.py index b911ac29..3d5559be 100644 --- a/libs/sqlalchemy/orm/interfaces.py +++ b/libs/sqlalchemy/orm/interfaces.py @@ -1,5 +1,5 @@ # orm/interfaces.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -16,16 +16,16 @@ classes within should be considered mostly private. """ -from itertools import chain +from __future__ import absolute_import -from sqlalchemy import exc as sa_exc -from sqlalchemy import util -from sqlalchemy.sql import operators -deque = __import__('collections').deque +from .. import exc as sa_exc, util, inspect +from ..sql import operators +from collections import deque +from .base import ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION +from .base import _InspectionAttr, _MappedAttribute +from .path_registry import PathRegistry +import collections -mapperutil = util.importlater('sqlalchemy.orm', 'util') - -collections = None __all__ = ( 'AttributeExtension', @@ -42,21 +42,11 @@ __all__ = ( 'SessionExtension', 'StrategizedOption', 'StrategizedProperty', - 'build_path', ) -EXT_CONTINUE = util.symbol('EXT_CONTINUE') -EXT_STOP = util.symbol('EXT_STOP') - -ONETOMANY = util.symbol('ONETOMANY') -MANYTOONE = util.symbol('MANYTOONE') -MANYTOMANY = util.symbol('MANYTOMANY') - -from deprecated_interfaces import AttributeExtension, SessionExtension, \ - MapperExtension -class MapperProperty(object): +class MapperProperty(_MappedAttribute, _InspectionAttr): """Manage the relationship of a ``Mapper`` to a single class attribute, as well as that attribute as it appears on individual instances of the class, including attribute instrumentation, @@ -66,18 +56,21 @@ class MapperProperty(object): mapped :class:`.Column`, which is represented in a mapping as an instance of :class:`.ColumnProperty`, and a reference to another class produced by :func:`.relationship`, - represented in the mapping as an instance of :class:`.RelationshipProperty`. + represented in the mapping as an instance of + :class:`.RelationshipProperty`. """ - cascade = () + cascade = frozenset() """The set of 'cascade' attribute names. This collection is checked before the 'cascade_iterator' method is called. """ - def setup(self, context, entity, path, reduced_path, adapter, **kwargs): + is_property = True + + def setup(self, context, entity, path, adapter, **kwargs): """Called by Query for the purposes of constructing a SQL statement. Each MapperProperty associated with the target mapper processes the @@ -87,7 +80,7 @@ class MapperProperty(object): pass - def create_row_processor(self, context, path, reduced_path, + def create_row_processor(self, context, path, mapper, row, adapter): """Return a 3-tuple consisting of three row processing functions. @@ -112,11 +105,33 @@ class MapperProperty(object): def set_parent(self, parent, init): self.parent = parent - def instrument_class(self, mapper): + def instrument_class(self, mapper): # pragma: no-coverage raise NotImplementedError() - _compile_started = False - _compile_finished = False + @util.memoized_property + def info(self): + """Info dictionary associated with the object, allowing user-defined + data to be associated with this :class:`.MapperProperty`. + + The dictionary is generated when first accessed. Alternatively, + it can be specified as a constructor argument to the + :func:`.column_property`, :func:`.relationship`, or :func:`.composite` + functions. + + .. versionadded:: 0.8 Added support for .info to all + :class:`.MapperProperty` subclasses. + + .. seealso:: + + :attr:`.QueryableAttribute.info` + + :attr:`.SchemaItem.info` + + """ + return {} + + _configure_started = False + _configure_finished = False def init(self): """Called after all mappers are created to assemble @@ -124,14 +139,33 @@ class MapperProperty(object): initialization steps. """ - self._compile_started = True + self._configure_started = True self.do_init() - self._compile_finished = True + self._configure_finished = True @property def class_attribute(self): """Return the class-bound descriptor corresponding to this - MapperProperty.""" + :class:`.MapperProperty`. + + This is basically a ``getattr()`` call:: + + return getattr(self.parent.class_, self.key) + + I.e. if this :class:`.MapperProperty` were named ``addresses``, + and the class to which it is mapped is ``User``, this sequence + is possible:: + + >>> from sqlalchemy import inspect + >>> mapper = inspect(User) + >>> addresses_property = mapper.attrs.addresses + >>> addresses_property.class_attribute is User.addresses + True + >>> User.addresses.property is addresses_property + True + + + """ return getattr(self.parent.class_, self.key) @@ -153,9 +187,6 @@ class MapperProperty(object): """ pass - def per_property_preprocessors(self, uow): - pass - def is_primary(self): """Return True if this ``MapperProperty``'s mapper is the primary mapper for its class. @@ -186,47 +217,131 @@ class MapperProperty(object): return operator(self.comparator, value) + def __repr__(self): + return '<%s at 0x%x; %s>' % ( + self.__class__.__name__, + id(self), getattr(self, 'key', 'no key')) + class PropComparator(operators.ColumnOperators): - """Defines comparison operations for MapperProperty objects. + """Defines boolean, comparison, and other operators for + :class:`.MapperProperty` objects. + + SQLAlchemy allows for operators to + be redefined at both the Core and ORM level. :class:`.PropComparator` + is the base class of operator redefinition for ORM-level operations, + including those of :class:`.ColumnProperty`, + :class:`.RelationshipProperty`, and :class:`.CompositeProperty`. + + .. note:: With the advent of Hybrid properties introduced in SQLAlchemy + 0.7, as well as Core-level operator redefinition in + SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator` + instances is extremely rare. See :ref:`hybrids_toplevel` as well + as :ref:`types_operators`. User-defined subclasses of :class:`.PropComparator` may be created. The built-in Python comparison and math operator methods, such as - ``__eq__()``, ``__lt__()``, ``__add__()``, can be overridden to provide + :meth:`.operators.ColumnOperators.__eq__`, + :meth:`.operators.ColumnOperators.__lt__`, and + :meth:`.operators.ColumnOperators.__add__`, can be overridden to provide new operator behavior. The custom :class:`.PropComparator` is passed to - the mapper property via the ``comparator_factory`` argument. In each case, + the :class:`.MapperProperty` instance via the ``comparator_factory`` + argument. In each case, the appropriate subclass of :class:`.PropComparator` should be used:: + # definition of custom PropComparator subclasses + from sqlalchemy.orm.properties import \\ ColumnProperty,\\ CompositeProperty,\\ RelationshipProperty class MyColumnComparator(ColumnProperty.Comparator): - pass - - class MyCompositeComparator(CompositeProperty.Comparator): - pass + def __eq__(self, other): + return self.__clause_element__() == other class MyRelationshipComparator(RelationshipProperty.Comparator): - pass + def any(self, expression): + "define the 'any' operation" + # ... + + class MyCompositeComparator(CompositeProperty.Comparator): + def __gt__(self, other): + "redefine the 'greater than' operation" + + return sql.and_(*[a>b for a, b in + zip(self.__clause_element__().clauses, + other.__composite_values__())]) + + + # application of custom PropComparator subclasses + + from sqlalchemy.orm import column_property, relationship, composite + from sqlalchemy import Column, String + + class SomeMappedClass(Base): + some_column = column_property(Column("some_column", String), + comparator_factory=MyColumnComparator) + + some_relationship = relationship(SomeOtherClass, + comparator_factory=MyRelationshipComparator) + + some_composite = composite( + Column("a", String), Column("b", String), + comparator_factory=MyCompositeComparator + ) + + Note that for column-level operator redefinition, it's usually + simpler to define the operators at the Core level, using the + :attr:`.TypeEngine.comparator_factory` attribute. See + :ref:`types_operators` for more detail. + + See also: + + :class:`.ColumnProperty.Comparator` + + :class:`.RelationshipProperty.Comparator` + + :class:`.CompositeProperty.Comparator` + + :class:`.ColumnOperators` + + :ref:`types_operators` + + :attr:`.TypeEngine.comparator_factory` """ - def __init__(self, prop, mapper, adapter=None): + def __init__(self, prop, parentmapper, adapt_to_entity=None): self.prop = self.property = prop - self.mapper = mapper - self.adapter = adapter + self._parentmapper = parentmapper + self._adapt_to_entity = adapt_to_entity def __clause_element__(self): raise NotImplementedError("%r" % self) - def adapted(self, adapter): + def _query_clause_element(self): + return self.__clause_element__() + + def adapt_to_entity(self, adapt_to_entity): """Return a copy of this PropComparator which will use the given - adaption function on the local side of generated expressions. + :class:`.AliasedInsp` to produce corresponding expressions. + """ + return self.__class__(self.prop, self._parentmapper, adapt_to_entity) + + @property + def adapter(self): + """Produce a callable that adapts column expressions + to suit an aliased version of this comparator. """ + if self._adapt_to_entity is None: + return None + else: + return self._adapt_to_entity._adapt_element - return self.__class__(self.prop, self.mapper, adapter) + @util.memoized_property + def info(self): + return self.property.info @staticmethod def any_op(a, b, **kwargs): @@ -251,8 +366,8 @@ class PropComparator(operators.ColumnOperators): query.join(Company.employees.of_type(Engineer)).\\ filter(Engineer.name=='foo') - :param \class_: a class or mapper indicating that criterion will be against - this specific subclass. + :param \class_: a class or mapper indicating that criterion will be + against this specific subclass. """ @@ -269,9 +384,9 @@ class PropComparator(operators.ColumnOperators): :param criterion: an optional ClauseElement formulated against the member class' table or attributes. - :param \**kwargs: key/value pairs corresponding to member class attribute - names which will be compared via equality to the corresponding - values. + :param \**kwargs: key/value pairs corresponding to member class + attribute names which will be compared via equality to the + corresponding values. """ @@ -287,9 +402,9 @@ class PropComparator(operators.ColumnOperators): :param criterion: an optional ClauseElement formulated against the member class' table or attributes. - :param \**kwargs: key/value pairs corresponding to member class attribute - names which will be compared via equality to the corresponding - values. + :param \**kwargs: key/value pairs corresponding to member class + attribute names which will be compared via equality to the + corresponding values. """ @@ -308,75 +423,87 @@ class StrategizedProperty(MapperProperty): strategy_wildcard_key = None - def _get_context_strategy(self, context, reduced_path): - key = ('loaderstrategy', reduced_path) - cls = None - if key in context.attributes: - cls = context.attributes[key] - elif self.strategy_wildcard_key: - key = ('loaderstrategy', (self.strategy_wildcard_key,)) - if key in context.attributes: - cls = context.attributes[key] + def _get_context_loader(self, context, path): + load = None - if cls: - try: - return self._strategies[cls] - except KeyError: - return self.__init_strategy(cls) - return self.strategy + # use EntityRegistry.__getitem__()->PropRegistry here so + # that the path is stated in terms of our base + search_path = dict.__getitem__(path, self) - def _get_strategy(self, cls): + # search among: exact match, "attr.*", "default" strategy + # if any. + for path_key in ( + search_path._loader_key, + search_path._wildcard_path_loader_key, + search_path._default_path_loader_key + ): + if path_key in context.attributes: + load = context.attributes[path_key] + break + + return load + + def _get_strategy(self, key): try: - return self._strategies[cls] + return self._strategies[key] except KeyError: - return self.__init_strategy(cls) + cls = self._strategy_lookup(*key) + self._strategies[key] = self._strategies[cls] = strategy = cls(self) + return strategy - def __init_strategy(self, cls): - self._strategies[cls] = strategy = cls(self) - return strategy + def _get_strategy_by_cls(self, cls): + return self._get_strategy(cls._strategy_keys[0]) - def setup(self, context, entity, path, reduced_path, adapter, **kwargs): - self._get_context_strategy(context, reduced_path + (self.key,)).\ - setup_query(context, entity, path, - reduced_path, adapter, **kwargs) + def setup(self, context, entity, path, adapter, **kwargs): + loader = self._get_context_loader(context, path) + if loader and loader.strategy: + strat = self._get_strategy(loader.strategy) + else: + strat = self.strategy + strat.setup_query(context, entity, path, loader, adapter, **kwargs) - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): - return self._get_context_strategy(context, reduced_path + (self.key,)).\ - create_row_processor(context, path, - reduced_path, mapper, row, adapter) + def create_row_processor(self, context, path, mapper, row, adapter): + loader = self._get_context_loader(context, path) + if loader and loader.strategy: + strat = self._get_strategy(loader.strategy) + else: + strat = self.strategy + return strat.create_row_processor(context, path, loader, + mapper, row, adapter) def do_init(self): self._strategies = {} - self.strategy = self.__init_strategy(self.strategy_class) + self.strategy = self._get_strategy_by_cls(self.strategy_class) def post_instrument_class(self, mapper): if self.is_primary() and \ not mapper.class_manager._attr_has_impl(self.key): self.strategy.init_class_attribute(mapper) -def build_path(entity, key, prev=None): - if prev: - return prev + (entity, key) - else: - return (entity, key) -def serialize_path(path): - if path is None: - return None + _strategies = collections.defaultdict(dict) - return zip( - [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], - [path[i] for i in range(1, len(path), 2)] + [None] - ) + @classmethod + def strategy_for(cls, **kw): + def decorate(dec_cls): + dec_cls._strategy_keys = [] + key = tuple(sorted(kw.items())) + cls._strategies[cls][key] = dec_cls + dec_cls._strategy_keys.append(key) + return dec_cls + return decorate -def deserialize_path(path): - if path is None: - return None + @classmethod + def _strategy_lookup(cls, *key): + for prop_cls in cls.__mro__: + if prop_cls in cls._strategies: + strategies = cls._strategies[prop_cls] + try: + return strategies[key] + except KeyError: + pass + raise Exception("can't locate strategy for %s %s" % (cls, key)) - p = tuple(chain(*[(mapperutil.class_mapper(cls), key) for cls, key in path])) - if p and p[-1] is None: - p = p[0:-1] - return p class MapperOption(object): """Describe a modification to a Query.""" @@ -398,242 +525,8 @@ class MapperOption(object): self.process_query(query) -class PropertyOption(MapperOption): - """A MapperOption that is applied to a property off the mapper or - one of its child mappers, identified by a dot-separated key - or list of class-bound attributes. """ - def __init__(self, key, mapper=None): - self.key = key - self.mapper = mapper - def process_query(self, query): - self._process(query, True) - - def process_query_conditionally(self, query): - self._process(query, False) - - def _process(self, query, raiseerr): - paths, mappers = self._get_paths(query, raiseerr) - if paths: - self.process_query_property(query, paths, mappers) - - def process_query_property(self, query, paths, mappers): - pass - - def __getstate__(self): - d = self.__dict__.copy() - d['key'] = ret = [] - for token in util.to_list(self.key): - if isinstance(token, PropComparator): - ret.append((token.mapper.class_, token.key)) - else: - ret.append(token) - return d - - def __setstate__(self, state): - ret = [] - for key in state['key']: - if isinstance(key, tuple): - cls, propkey = key - ret.append(getattr(cls, propkey)) - else: - ret.append(key) - state['key'] = tuple(ret) - self.__dict__ = state - - def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): - if mapperutil._is_aliased_class(mapper): - searchfor = mapper - isa = False - else: - searchfor = mapperutil._class_to_mapper(mapper) - isa = True - for ent in query._mapper_entities: - if searchfor is ent.path_entity or isa \ - and searchfor.common_parent(ent.path_entity): - return ent - else: - if raiseerr: - if not list(query._mapper_entities): - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - raise sa_exc.ArgumentError( - "Can't find property '%s' on any entity " - "specified in this Query. Note the full path " - "from root (%s) to target entity must be specified." - % (token, ",".join(str(x) for - x in query._mapper_entities)) - ) - else: - return None - - def _find_entity_basestring(self, query, token, raiseerr): - for ent in query._mapper_entities: - # return only the first _MapperEntity when searching - # based on string prop name. Ideally object - # attributes are used to specify more exactly. - return ent - else: - if raiseerr: - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - return None - - def _get_paths(self, query, raiseerr): - path = None - entity = None - l = [] - mappers = [] - - # _current_path implies we're in a - # secondary load with an existing path - current_path = list(query._current_path) - - tokens = deque(self.key) - while tokens: - token = tokens.popleft() - if isinstance(token, basestring): - # wildcard token - if token.endswith(':*'): - return [(token,)], [] - sub_tokens = token.split(".", 1) - token = sub_tokens[0] - tokens.extendleft(sub_tokens[1:]) - - # exhaust current_path before - # matching tokens to entities - if current_path: - if current_path[1] == token: - current_path = current_path[2:] - continue - else: - return [], [] - - if not entity: - entity = self._find_entity_basestring( - query, - token, - raiseerr) - if entity is None: - return [], [] - path_element = entity.path_entity - mapper = entity.mapper - mappers.append(mapper) - if hasattr(mapper.class_, token): - prop = getattr(mapper.class_, token).property - else: - if raiseerr: - raise sa_exc.ArgumentError( - "Can't find property named '%s' on the " - "mapped entity %s in this Query. " % ( - token, mapper) - ) - else: - return [], [] - elif isinstance(token, PropComparator): - prop = token.property - - # exhaust current_path before - # matching tokens to entities - if current_path: - if current_path[0:2] == \ - [token.parententity, prop.key]: - current_path = current_path[2:] - continue - else: - return [], [] - - if not entity: - entity = self._find_entity_prop_comparator( - query, - prop.key, - token.parententity, - raiseerr) - if not entity: - return [], [] - path_element = entity.path_entity - mapper = entity.mapper - mappers.append(prop.parent) - else: - raise sa_exc.ArgumentError( - "mapper option expects " - "string key or list of attributes") - assert prop is not None - if raiseerr and not prop.parent.common_parent(mapper): - raise sa_exc.ArgumentError("Attribute '%s' does not " - "link from element '%s'" % (token, path_element)) - - path = build_path(path_element, prop.key, path) - - l.append(path) - if getattr(token, '_of_type', None): - path_element = mapper = token._of_type - else: - path_element = mapper = getattr(prop, 'mapper', None) - if mapper is None and tokens: - raise sa_exc.ArgumentError( - "Attribute '%s' of entity '%s' does not " - "refer to a mapped entity" % - (token, entity) - ) - - if current_path: - # ran out of tokens before - # current_path was exhausted. - assert not tokens - return [], [] - - return l, mappers - -class StrategizedOption(PropertyOption): - """A MapperOption that affects which LoaderStrategy will be used - for an operation by a StrategizedProperty. - """ - - chained = False - - def process_query_property(self, query, paths, mappers): - - # _get_context_strategy may receive the path in terms of a base - # mapper - e.g. options(eagerload_all(Company.employees, - # Engineer.machines)) in the polymorphic tests leads to - # "(Person, 'machines')" in the path due to the mechanics of how - # the eager strategy builds up the path - - if self.chained: - for path in paths: - query._attributes[('loaderstrategy', - _reduce_path(path))] = \ - self.get_strategy_class() - else: - query._attributes[('loaderstrategy', - _reduce_path(paths[-1]))] = \ - self.get_strategy_class() - - def get_strategy_class(self): - raise NotImplementedError() - -def _reduce_path(path): - """Convert a (mapper, path) path to use base mappers. - - This is used to allow more open ended selection of loader strategies, i.e. - Mapper -> prop1 -> Subclass -> prop2, where Subclass is a sub-mapper - of the mapper referenced by Mapper.prop1. - - """ - return tuple([i % 2 != 0 and - element or - getattr(element, 'base_mapper', element) - for i, element in enumerate(path)]) class LoaderStrategy(object): """Describe the loading behavior of a StrategizedProperty object. @@ -663,22 +556,14 @@ class LoaderStrategy(object): self.is_class_level = False self.parent = self.parent_property.parent self.key = self.parent_property.key - # TODO: there's no particular reason we need - # the separate .init() method at this point. - # It's possible someone has written their - # own LS object. - self.init() - - def init(self): - raise NotImplementedError("LoaderStrategy") def init_class_attribute(self, mapper): pass - def setup_query(self, context, entity, path, reduced_path, adapter, **kwargs): + def setup_query(self, context, entity, path, loadopt, adapter, **kwargs): pass - def create_row_processor(self, context, path, reduced_path, mapper, + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): """Return row processing functions which fulfill the contract specified by MapperProperty.create_row_processor. @@ -690,94 +575,3 @@ class LoaderStrategy(object): def __str__(self): return str(self.parent_property) - - def debug_callable(self, fn, logger, announcement, logfn): - if announcement: - logger.debug(announcement) - if logfn: - def call(*args, **kwargs): - logger.debug(logfn(*args, **kwargs)) - return fn(*args, **kwargs) - return call - else: - return fn - -class InstrumentationManager(object): - """User-defined class instrumentation extension. - - :class:`.InstrumentationManager` can be subclassed in order - to change - how class instrumentation proceeds. This class exists for - the purposes of integration with other object management - frameworks which would like to entirely modify the - instrumentation methodology of the ORM, and is not intended - for regular usage. For interception of class instrumentation - events, see :class:`.InstrumentationEvents`. - - For an example of :class:`.InstrumentationManager`, see the - example :ref:`examples_instrumentation`. - - The API for this class should be considered as semi-stable, - and may change slightly with new releases. - - """ - - # r4361 added a mandatory (cls) constructor to this interface. - # given that, perhaps class_ should be dropped from all of these - # signatures. - - def __init__(self, class_): - pass - - def manage(self, class_, manager): - setattr(class_, '_default_class_manager', manager) - - def dispose(self, class_, manager): - delattr(class_, '_default_class_manager') - - def manager_getter(self, class_): - def get(cls): - return cls._default_class_manager - return get - - def instrument_attribute(self, class_, key, inst): - pass - - def post_configure_attribute(self, class_, key, inst): - pass - - def install_descriptor(self, class_, key, inst): - setattr(class_, key, inst) - - def uninstall_descriptor(self, class_, key): - delattr(class_, key) - - def install_member(self, class_, key, implementation): - setattr(class_, key, implementation) - - def uninstall_member(self, class_, key): - delattr(class_, key) - - def instrument_collection_class(self, class_, key, collection_class): - global collections - if collections is None: - from sqlalchemy.orm import collections - return collections.prepare_instrumentation(collection_class) - - def get_instance_dict(self, class_, instance): - return instance.__dict__ - - def initialize_instance_dict(self, class_, instance): - pass - - def install_state(self, class_, instance, state): - setattr(instance, '_default_state', state) - - def remove_state(self, class_, instance): - delattr(instance, '_default_state') - - def state_getter(self, class_): - return lambda instance: getattr(instance, '_default_state') - - def dict_getter(self, class_): - return lambda inst: self.get_instance_dict(class_, inst) diff --git a/libs/sqlalchemy/orm/loading.py b/libs/sqlalchemy/orm/loading.py new file mode 100644 index 00000000..af77fe3e --- /dev/null +++ b/libs/sqlalchemy/orm/loading.py @@ -0,0 +1,610 @@ +# orm/loading.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""private module containing functions used to convert database +rows into object instances and associated state. + +the functions here are called primarily by Query, Mapper, +as well as some of the attribute loading strategies. + +""" + + +from .. import util +from . import attributes, exc as orm_exc, state as statelib +from .interfaces import EXT_CONTINUE +from ..sql import util as sql_util +from .util import _none_set, state_str +from .. import exc as sa_exc + +_new_runid = util.counter() + + +def instances(query, cursor, context): + """Return an ORM result as an iterator.""" + session = query.session + + context.runid = _new_runid() + + filter_fns = [ent.filter_fn + for ent in query._entities] + filtered = id in filter_fns + + single_entity = len(query._entities) == 1 and \ + query._entities[0].supports_single_entity + + if filtered: + if single_entity: + filter_fn = id + else: + def filter_fn(row): + return tuple(fn(x) for x, fn in zip(row, filter_fns)) + + custom_rows = single_entity and \ + query._entities[0].custom_rows + + (process, labels) = \ + list(zip(*[ + query_entity.row_processor(query, + context, custom_rows) + for query_entity in query._entities + ])) + + while True: + context.progress = {} + context.partials = {} + + if query._yield_per: + fetch = cursor.fetchmany(query._yield_per) + if not fetch: + break + else: + fetch = cursor.fetchall() + + if custom_rows: + rows = [] + for row in fetch: + process[0](row, rows) + elif single_entity: + rows = [process[0](row, None) for row in fetch] + else: + rows = [util.KeyedTuple([proc(row, None) for proc in process], + labels) for row in fetch] + + if filtered: + rows = util.unique_list(rows, filter_fn) + + if context.refresh_state and query._only_load_props \ + and context.refresh_state in context.progress: + context.refresh_state._commit( + context.refresh_state.dict, query._only_load_props) + context.progress.pop(context.refresh_state) + + statelib.InstanceState._commit_all_states( + list(context.progress.items()), + session.identity_map + ) + + for state, (dict_, attrs) in context.partials.items(): + state._commit(dict_, attrs) + + for row in rows: + yield row + + if not query._yield_per: + break + + +@util.dependencies("sqlalchemy.orm.query") +def merge_result(querylib, query, iterator, load=True): + """Merge a result into this :class:`.Query` object's Session.""" + + session = query.session + if load: + # flush current contents if we expect to load data + session._autoflush() + + autoflush = session.autoflush + try: + session.autoflush = False + single_entity = len(query._entities) == 1 + if single_entity: + if isinstance(query._entities[0], querylib._MapperEntity): + result = [session._merge( + attributes.instance_state(instance), + attributes.instance_dict(instance), + load=load, _recursive={}) + for instance in iterator] + else: + result = list(iterator) + else: + mapped_entities = [i for i, e in enumerate(query._entities) + if isinstance(e, querylib._MapperEntity)] + result = [] + keys = [ent._label_name for ent in query._entities] + for row in iterator: + newrow = list(row) + for i in mapped_entities: + if newrow[i] is not None: + newrow[i] = session._merge( + attributes.instance_state(newrow[i]), + attributes.instance_dict(newrow[i]), + load=load, _recursive={}) + result.append(util.KeyedTuple(newrow, keys)) + + return iter(result) + finally: + session.autoflush = autoflush + + +def get_from_identity(session, key, passive): + """Look up the given key in the given session's identity map, + check the object for expired state if found. + + """ + instance = session.identity_map.get(key) + if instance is not None: + + state = attributes.instance_state(instance) + + # expired - ensure it still exists + if state.expired: + if not passive & attributes.SQL_OK: + # TODO: no coverage here + return attributes.PASSIVE_NO_RESULT + elif not passive & attributes.RELATED_OBJECT_OK: + # this mode is used within a flush and the instance's + # expired state will be checked soon enough, if necessary + return instance + try: + state(state, passive) + except orm_exc.ObjectDeletedError: + session._remove_newly_deleted([state]) + return None + return instance + else: + return None + + +def load_on_ident(query, key, + refresh_state=None, lockmode=None, + only_load_props=None): + """Load the given identity key from the database.""" + + if key is not None: + ident = key[1] + else: + ident = None + + if refresh_state is None: + q = query._clone() + q._get_condition() + else: + q = query._clone() + + if ident is not None: + mapper = query._mapper_zero() + + (_get_clause, _get_params) = mapper._get_clause + + # None present in ident - turn those comparisons + # into "IS NULL" + if None in ident: + nones = set([ + _get_params[col].key for col, value in + zip(mapper.primary_key, ident) if value is None + ]) + _get_clause = sql_util.adapt_criterion_to_null( + _get_clause, nones) + + _get_clause = q._adapt_clause(_get_clause, True, False) + q._criterion = _get_clause + + params = dict([ + (_get_params[primary_key].key, id_val) + for id_val, primary_key in zip(ident, mapper.primary_key) + ]) + + q._params = params + + if lockmode is not None: + version_check = True + q = q.with_lockmode(lockmode) + elif query._for_update_arg is not None: + version_check = True + q._for_update_arg = query._for_update_arg + else: + version_check = False + + q._get_options( + populate_existing=bool(refresh_state), + version_check=version_check, + only_load_props=only_load_props, + refresh_state=refresh_state) + q._order_by = None + + try: + return q.one() + except orm_exc.NoResultFound: + return None + + +def instance_processor(mapper, context, path, adapter, + polymorphic_from=None, + only_load_props=None, + refresh_state=None, + polymorphic_discriminator=None): + + """Produce a mapper level row processor callable + which processes rows into mapped instances.""" + + # note that this method, most of which exists in a closure + # called _instance(), resists being broken out, as + # attempts to do so tend to add significant function + # call overhead. _instance() is the most + # performance-critical section in the whole ORM. + + pk_cols = mapper.primary_key + + if polymorphic_from or refresh_state: + polymorphic_on = None + else: + if polymorphic_discriminator is not None: + polymorphic_on = polymorphic_discriminator + else: + polymorphic_on = mapper.polymorphic_on + polymorphic_instances = util.PopulateDict( + _configure_subclass_mapper( + mapper, + context, path, adapter) + ) + + version_id_col = mapper.version_id_col + + if adapter: + pk_cols = [adapter.columns[c] for c in pk_cols] + if polymorphic_on is not None: + polymorphic_on = adapter.columns[polymorphic_on] + if version_id_col is not None: + version_id_col = adapter.columns[version_id_col] + + identity_class = mapper._identity_class + + new_populators = [] + existing_populators = [] + eager_populators = [] + + load_path = context.query._current_path + path \ + if context.query._current_path.path \ + else path + + def populate_state(state, dict_, row, isnew, only_load_props): + if isnew: + if context.propagate_options: + state.load_options = context.propagate_options + if state.load_options: + state.load_path = load_path + + if not new_populators: + _populators(mapper, context, path, row, adapter, + new_populators, + existing_populators, + eager_populators + ) + + if isnew: + populators = new_populators + else: + populators = existing_populators + + if only_load_props is None: + for key, populator in populators: + populator(state, dict_, row) + elif only_load_props: + for key, populator in populators: + if key in only_load_props: + populator(state, dict_, row) + + session_identity_map = context.session.identity_map + + listeners = mapper.dispatch + + translate_row = listeners.translate_row or None + create_instance = listeners.create_instance or None + populate_instance = listeners.populate_instance or None + append_result = listeners.append_result or None + populate_existing = context.populate_existing or mapper.always_refresh + invoke_all_eagers = context.invoke_all_eagers + + if mapper.allow_partial_pks: + is_not_primary_key = _none_set.issuperset + else: + is_not_primary_key = _none_set.issubset + + def _instance(row, result): + if not new_populators and invoke_all_eagers: + _populators(mapper, context, path, row, adapter, + new_populators, + existing_populators, + eager_populators + ) + + if translate_row: + for fn in translate_row: + ret = fn(mapper, context, row) + if ret is not EXT_CONTINUE: + row = ret + break + + if polymorphic_on is not None: + discriminator = row[polymorphic_on] + if discriminator is not None: + _instance = polymorphic_instances[discriminator] + if _instance: + return _instance(row, result) + + # determine identity key + if refresh_state: + identitykey = refresh_state.key + if identitykey is None: + # super-rare condition; a refresh is being called + # on a non-instance-key instance; this is meant to only + # occur within a flush() + identitykey = mapper._identity_key_from_state(refresh_state) + else: + identitykey = ( + identity_class, + tuple([row[column] for column in pk_cols]) + ) + + instance = session_identity_map.get(identitykey) + if instance is not None: + state = attributes.instance_state(instance) + dict_ = attributes.instance_dict(instance) + + isnew = state.runid != context.runid + currentload = not isnew + loaded_instance = False + + if not currentload and \ + version_id_col is not None and \ + context.version_check and \ + mapper._get_state_attr_by_column( + state, + dict_, + mapper.version_id_col) != \ + row[version_id_col]: + + raise orm_exc.StaleDataError( + "Instance '%s' has version id '%s' which " + "does not match database-loaded version id '%s'." + % (state_str(state), + mapper._get_state_attr_by_column( + state, dict_, + mapper.version_id_col), + row[version_id_col])) + elif refresh_state: + # out of band refresh_state detected (i.e. its not in the + # session.identity_map) honor it anyway. this can happen + # if a _get() occurs within save_obj(), such as + # when eager_defaults is True. + state = refresh_state + instance = state.obj() + dict_ = attributes.instance_dict(instance) + isnew = state.runid != context.runid + currentload = True + loaded_instance = False + else: + # check for non-NULL values in the primary key columns, + # else no entity is returned for the row + if is_not_primary_key(identitykey[1]): + return None + + isnew = True + currentload = True + loaded_instance = True + + if create_instance: + for fn in create_instance: + instance = fn(mapper, context, + row, mapper.class_) + if instance is not EXT_CONTINUE: + manager = attributes.manager_of_class( + instance.__class__) + # TODO: if manager is None, raise a friendly error + # about returning instances of unmapped types + manager.setup_instance(instance) + break + else: + instance = mapper.class_manager.new_instance() + else: + instance = mapper.class_manager.new_instance() + + dict_ = attributes.instance_dict(instance) + state = attributes.instance_state(instance) + state.key = identitykey + + # attach instance to session. + state.session_id = context.session.hash_key + session_identity_map.add(state) + + if currentload or populate_existing: + # state is being fully loaded, so populate. + # add to the "context.progress" collection. + if isnew: + state.runid = context.runid + context.progress[state] = dict_ + + if populate_instance: + for fn in populate_instance: + ret = fn(mapper, context, row, state, + only_load_props=only_load_props, + instancekey=identitykey, isnew=isnew) + if ret is not EXT_CONTINUE: + break + else: + populate_state(state, dict_, row, isnew, only_load_props) + else: + populate_state(state, dict_, row, isnew, only_load_props) + + if loaded_instance: + state.manager.dispatch.load(state, context) + elif isnew: + state.manager.dispatch.refresh(state, context, only_load_props) + + elif state in context.partials or state.unloaded or eager_populators: + # state is having a partial set of its attributes + # refreshed. Populate those attributes, + # and add to the "context.partials" collection. + if state in context.partials: + isnew = False + (d_, attrs) = context.partials[state] + else: + isnew = True + attrs = state.unloaded + context.partials[state] = (dict_, attrs) + + if populate_instance: + for fn in populate_instance: + ret = fn(mapper, context, row, state, + only_load_props=attrs, + instancekey=identitykey, isnew=isnew) + if ret is not EXT_CONTINUE: + break + else: + populate_state(state, dict_, row, isnew, attrs) + else: + populate_state(state, dict_, row, isnew, attrs) + + for key, pop in eager_populators: + if key not in state.unloaded: + pop(state, dict_, row) + + if isnew: + state.manager.dispatch.refresh(state, context, attrs) + + if result is not None: + if append_result: + for fn in append_result: + if fn(mapper, context, row, state, + result, instancekey=identitykey, + isnew=isnew) is not EXT_CONTINUE: + break + else: + result.append(instance) + else: + result.append(instance) + + return instance + return _instance + + +def _populators(mapper, context, path, row, adapter, + new_populators, existing_populators, eager_populators): + """Produce a collection of attribute level row processor + callables.""" + + delayed_populators = [] + pops = (new_populators, existing_populators, delayed_populators, + eager_populators) + + for prop in mapper._props.values(): + + for i, pop in enumerate(prop.create_row_processor( + context, + path, + mapper, row, adapter)): + if pop is not None: + pops[i].append((prop.key, pop)) + + if delayed_populators: + new_populators.extend(delayed_populators) + + +def _configure_subclass_mapper(mapper, context, path, adapter): + """Produce a mapper level row processor callable factory for mappers + inheriting this one.""" + + def configure_subclass_mapper(discriminator): + try: + sub_mapper = mapper.polymorphic_map[discriminator] + except KeyError: + raise AssertionError( + "No such polymorphic_identity %r is defined" % + discriminator) + if sub_mapper is mapper: + return None + + return instance_processor( + sub_mapper, + context, + path, + adapter, + polymorphic_from=mapper) + return configure_subclass_mapper + + +def load_scalar_attributes(mapper, state, attribute_names): + """initiate a column-based attribute refresh operation.""" + + #assert mapper is _state_mapper(state) + session = state.session + if not session: + raise orm_exc.DetachedInstanceError( + "Instance %s is not bound to a Session; " + "attribute refresh operation cannot proceed" % + (state_str(state))) + + has_key = bool(state.key) + + result = False + + if mapper.inherits and not mapper.concrete: + statement = mapper._optimized_get_statement(state, attribute_names) + if statement is not None: + result = load_on_ident( + session.query(mapper).from_statement(statement), + None, + only_load_props=attribute_names, + refresh_state=state + ) + + if result is False: + if has_key: + identity_key = state.key + else: + # this codepath is rare - only valid when inside a flush, and the + # object is becoming persistent but hasn't yet been assigned + # an identity_key. + # check here to ensure we have the attrs we need. + pk_attrs = [mapper._columntoproperty[col].key + for col in mapper.primary_key] + if state.expired_attributes.intersection(pk_attrs): + raise sa_exc.InvalidRequestError( + "Instance %s cannot be refreshed - it's not " + " persistent and does not " + "contain a full primary key." % state_str(state)) + identity_key = mapper._identity_key_from_state(state) + + if (_none_set.issubset(identity_key) and \ + not mapper.allow_partial_pks) or \ + _none_set.issuperset(identity_key): + util.warn("Instance %s to be refreshed doesn't " + "contain a full primary key - can't be refreshed " + "(and shouldn't be expired, either)." + % state_str(state)) + return + + result = load_on_ident( + session.query(mapper), + identity_key, + refresh_state=state, + only_load_props=attribute_names) + + # if instance is pending, a refresh operation + # may not complete (even if PK attributes are assigned) + if has_key and result is None: + raise orm_exc.ObjectDeletedError(state) diff --git a/libs/sqlalchemy/orm/mapper.py b/libs/sqlalchemy/orm/mapper.py index de4d351b..a853efc3 100644 --- a/libs/sqlalchemy/orm/mapper.py +++ b/libs/sqlalchemy/orm/mapper.py @@ -1,5 +1,5 @@ # orm/mapper.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -13,38 +13,28 @@ This is a semi-private module; the main configurational API of the ORM is available in :class:`~sqlalchemy.orm.`. """ +from __future__ import absolute_import import types import weakref -import operator -from itertools import chain, groupby -deque = __import__('collections').deque +from itertools import chain +from collections import deque -from sqlalchemy import sql, util, log, exc as sa_exc, event, schema -from sqlalchemy.sql import expression, visitors, operators, util as sqlutil -from sqlalchemy.orm import instrumentation, attributes, sync, \ - exc as orm_exc, unitofwork, events -from sqlalchemy.orm.interfaces import MapperProperty, EXT_CONTINUE, \ - PropComparator +from .. import sql, util, log, exc as sa_exc, event, schema, inspection +from ..sql import expression, visitors, operators, util as sql_util +from . import instrumentation, attributes, exc as orm_exc, loading +from . import properties +from .interfaces import MapperProperty, _InspectionAttr, _MappedAttribute -from sqlalchemy.orm.util import _INSTRUMENTOR, _class_to_mapper, \ - _state_mapper, class_mapper, instance_str, state_str +from .base import _class_to_mapper, _state_mapper, class_mapper, \ + state_str, _INSTRUMENTOR +from .path_registry import PathRegistry import sys -sessionlib = util.importlater("sqlalchemy.orm", "session") -properties = util.importlater("sqlalchemy.orm", "properties") -__all__ = ( - 'Mapper', - '_mapper_registry', - 'class_mapper', - 'object_mapper', - ) _mapper_registry = weakref.WeakKeyDictionary() -_new_mappers = False _already_compiling = False -_none_set = frozenset([None]) _memoized_configured_property = util.group_expirable_memoized_property() @@ -54,37 +44,66 @@ _memoized_configured_property = util.group_expirable_memoized_property() # column NO_ATTRIBUTE = util.symbol('NO_ATTRIBUTE') -# lock used to synchronize the "mapper compile" step -_COMPILE_MUTEX = util.threading.RLock() +# lock used to synchronize the "mapper configure" step +_CONFIGURE_MUTEX = util.threading.RLock() -class Mapper(object): + +@inspection._self_inspects +@log.class_logger +class Mapper(_InspectionAttr): """Define the correlation of class attributes to database table columns. - Instances of this class should be constructed via the - :func:`~sqlalchemy.orm.mapper` function. + The :class:`.Mapper` object is instantiated using the + :func:`~sqlalchemy.orm.mapper` function. For information + about instantiating new :class:`.Mapper` objects, see + that function's documentation. + + + When :func:`.mapper` is used + explicitly to link a user defined class with table + metadata, this is referred to as *classical mapping*. + Modern SQLAlchemy usage tends to favor the + :mod:`sqlalchemy.ext.declarative` extension for class + configuration, which + makes usage of :func:`.mapper` behind the scenes. + + Given a particular class known to be mapped by the ORM, + the :class:`.Mapper` which maintains it can be acquired + using the :func:`.inspect` function:: + + from sqlalchemy import inspect + + mapper = inspect(MyClass) + + A class which was mapped by the :mod:`sqlalchemy.ext.declarative` + extension will also have its mapper available via the ``__mapper__`` + attribute. + """ + + _new_mappers = False + def __init__(self, class_, - local_table, - properties = None, - primary_key = None, - non_primary = False, - inherits = None, - inherit_condition = None, - inherit_foreign_keys = None, - extension = None, - order_by = False, - always_refresh = False, - version_id_col = None, - version_id_generator = None, + local_table=None, + properties=None, + primary_key=None, + non_primary=False, + inherits=None, + inherit_condition=None, + inherit_foreign_keys=None, + extension=None, + order_by=False, + always_refresh=False, + version_id_col=None, + version_id_generator=None, polymorphic_on=None, _polymorphic_map=None, polymorphic_identity=None, concrete=False, with_polymorphic=None, - allow_null_pks=None, allow_partial_pks=True, batch=True, column_prefix=None, @@ -92,12 +111,383 @@ class Mapper(object): exclude_properties=None, passive_updates=True, eager_defaults=False, + legacy_is_orphan=False, _compiled_cache_size=100, ): - """Construct a new mapper. + """Return a new :class:`~.Mapper` object. - Mappers are normally constructed via the - :func:`~sqlalchemy.orm.mapper` function. See for details. + This function is typically used behind the scenes + via the Declarative extension. When using Declarative, + many of the usual :func:`.mapper` arguments are handled + by the Declarative extension itself, including ``class_``, + ``local_table``, ``properties``, and ``inherits``. + Other options are passed to :func:`.mapper` using + the ``__mapper_args__`` class variable:: + + class MyClass(Base): + __tablename__ = 'my_table' + id = Column(Integer, primary_key=True) + type = Column(String(50)) + alt = Column("some_alt", Integer) + + __mapper_args__ = { + 'polymorphic_on' : type + } + + + Explicit use of :func:`.mapper` + is often referred to as *classical mapping*. The above + declarative example is equivalent in classical form to:: + + my_table = Table("my_table", metadata, + Column('id', Integer, primary_key=True), + Column('type', String(50)), + Column("some_alt", Integer) + ) + + class MyClass(object): + pass + + mapper(MyClass, my_table, + polymorphic_on=my_table.c.type, + properties={ + 'alt':my_table.c.some_alt + }) + + .. seealso:: + + :ref:`classical_mapping` - discussion of direct usage of + :func:`.mapper` + + :param class\_: The class to be mapped. When using Declarative, + this argument is automatically passed as the declared class + itself. + + :param local_table: The :class:`.Table` or other selectable + to which the class is mapped. May be ``None`` if + this mapper inherits from another mapper using single-table + inheritance. When using Declarative, this argument is + automatically passed by the extension, based on what + is configured via the ``__table__`` argument or via the + :class:`.Table` produced as a result of the ``__tablename__`` + and :class:`.Column` arguments present. + + :param always_refresh: If True, all query operations for this mapped + class will overwrite all data within object instances that already + exist within the session, erasing any in-memory changes with + whatever information was loaded from the database. Usage of this + flag is highly discouraged; as an alternative, see the method + :meth:`.Query.populate_existing`. + + :param allow_partial_pks: Defaults to True. Indicates that a + composite primary key with some NULL values should be considered as + possibly existing within the database. This affects whether a + mapper will assign an incoming row to an existing identity, as well + as if :meth:`.Session.merge` will check the database first for a + particular primary key value. A "partial primary key" can occur if + one has mapped to an OUTER JOIN, for example. + + :param batch: Defaults to ``True``, indicating that save operations + of multiple entities can be batched together for efficiency. + Setting to False indicates + that an instance will be fully saved before saving the next + instance. This is used in the extremely rare case that a + :class:`.MapperEvents` listener requires being called + in between individual row persistence operations. + + :param column_prefix: A string which will be prepended + to the mapped attribute name when :class:`.Column` + objects are automatically assigned as attributes to the + mapped class. Does not affect explicitly specified + column-based properties. + + See the section :ref:`column_prefix` for an example. + + :param concrete: If True, indicates this mapper should use concrete + table inheritance with its parent mapper. + + See the section :ref:`concrete_inheritance` for an example. + + :param eager_defaults: if True, the ORM will immediately fetch the + value of server-generated default values after an INSERT or UPDATE, + rather than leaving them as expired to be fetched on next access. + This can be used for event schemes where the server-generated values + are needed immediately before the flush completes. By default, + this scheme will emit an individual ``SELECT`` statement per row + inserted or updated, which note can add significant performance + overhead. However, if the + target database supports :term:`RETURNING`, the default values will be + returned inline with the INSERT or UPDATE statement, which can + greatly enhance performance for an application that needs frequent + access to just-generated server defaults. + + .. versionchanged:: 0.9.0 The ``eager_defaults`` option can now + make use of :term:`RETURNING` for backends which support it. + + :param exclude_properties: A list or set of string column names to + be excluded from mapping. + + See :ref:`include_exclude_cols` for an example. + + :param extension: A :class:`.MapperExtension` instance or + list of :class:`.MapperExtension` instances which will be applied + to all operations by this :class:`.Mapper`. **Deprecated.** + Please see :class:`.MapperEvents`. + + :param include_properties: An inclusive list or set of string column + names to map. + + See :ref:`include_exclude_cols` for an example. + + :param inherits: A mapped class or the corresponding :class:`.Mapper` + of one indicating a superclass to which this :class:`.Mapper` + should *inherit* from. The mapped class here must be a subclass + of the other mapper's class. When using Declarative, this argument + is passed automatically as a result of the natural class + hierarchy of the declared classes. + + .. seealso:: + + :ref:`inheritance_toplevel` + + :param inherit_condition: For joined table inheritance, a SQL + expression which will + define how the two tables are joined; defaults to a natural join + between the two tables. + + :param inherit_foreign_keys: When ``inherit_condition`` is used and the + columns present are missing a :class:`.ForeignKey` configuration, + this parameter can be used to specify which columns are "foreign". + In most cases can be left as ``None``. + + :param legacy_is_orphan: Boolean, defaults to ``False``. + When ``True``, specifies that "legacy" orphan consideration + is to be applied to objects mapped by this mapper, which means + that a pending (that is, not persistent) object is auto-expunged + from an owning :class:`.Session` only when it is de-associated + from *all* parents that specify a ``delete-orphan`` cascade towards + this mapper. The new default behavior is that the object is auto-expunged + when it is de-associated with *any* of its parents that specify + ``delete-orphan`` cascade. This behavior is more consistent with + that of a persistent object, and allows behavior to be consistent + in more scenarios independently of whether or not an orphanable + object has been flushed yet or not. + + See the change note and example at :ref:`legacy_is_orphan_addition` + for more detail on this change. + + .. versionadded:: 0.8 - the consideration of a pending object as + an "orphan" has been modified to more closely match the + behavior as that of persistent objects, which is that the object + is expunged from the :class:`.Session` as soon as it is + de-associated from any of its orphan-enabled parents. Previously, + the pending object would be expunged only if de-associated + from all of its orphan-enabled parents. The new flag ``legacy_is_orphan`` + is added to :func:`.orm.mapper` which re-establishes the + legacy behavior. + + :param non_primary: Specify that this :class:`.Mapper` is in addition + to the "primary" mapper, that is, the one used for persistence. + The :class:`.Mapper` created here may be used for ad-hoc + mapping of the class to an alternate selectable, for loading + only. + + The ``non_primary`` feature is rarely needed with modern + usage. + + :param order_by: A single :class:`.Column` or list of :class:`.Column` + objects for which selection operations should use as the default + ordering for entities. By default mappers have no pre-defined + ordering. + + :param passive_updates: Indicates UPDATE behavior of foreign key + columns when a primary key column changes on a joined-table + inheritance mapping. Defaults to ``True``. + + When True, it is assumed that ON UPDATE CASCADE is configured on + the foreign key in the database, and that the database will handle + propagation of an UPDATE from a source column to dependent columns + on joined-table rows. + + When False, it is assumed that the database does not enforce + referential integrity and will not be issuing its own CASCADE + operation for an update. The :class:`.Mapper` here will + emit an UPDATE statement for the dependent columns during a + primary key change. + + ..seealso:: + + :ref:`passive_updates` - description of a similar feature as + used with :func:`.relationship` + + :param polymorphic_on: Specifies the column, attribute, or + SQL expression used to determine the target class for an + incoming row, when inheriting classes are present. + + This value is commonly a :class:`.Column` object that's + present in the mapped :class:`.Table`:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + + __mapper_args__ = { + "polymorphic_on":discriminator, + "polymorphic_identity":"employee" + } + + It may also be specified + as a SQL expression, as in this example where we + use the :func:`.case` construct to provide a conditional + approach:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + + __mapper_args__ = { + "polymorphic_on":case([ + (discriminator == "EN", "engineer"), + (discriminator == "MA", "manager"), + ], else_="employee"), + "polymorphic_identity":"employee" + } + + It may also refer to any attribute + configured with :func:`.column_property`, or to the + string name of one:: + + class Employee(Base): + __tablename__ = 'employee' + + id = Column(Integer, primary_key=True) + discriminator = Column(String(50)) + employee_type = column_property( + case([ + (discriminator == "EN", "engineer"), + (discriminator == "MA", "manager"), + ], else_="employee") + ) + + __mapper_args__ = { + "polymorphic_on":employee_type, + "polymorphic_identity":"employee" + } + + .. versionchanged:: 0.7.4 + ``polymorphic_on`` may be specified as a SQL expression, + or refer to any attribute configured with + :func:`.column_property`, or to the string name of one. + + When setting ``polymorphic_on`` to reference an + attribute or expression that's not present in the + locally mapped :class:`.Table`, yet the value + of the discriminator should be persisted to the database, + the value of the + discriminator is not automatically set on new + instances; this must be handled by the user, + either through manual means or via event listeners. + A typical approach to establishing such a listener + looks like:: + + from sqlalchemy import event + from sqlalchemy.orm import object_mapper + + @event.listens_for(Employee, "init", propagate=True) + def set_identity(instance, *arg, **kw): + mapper = object_mapper(instance) + instance.discriminator = mapper.polymorphic_identity + + Where above, we assign the value of ``polymorphic_identity`` + for the mapped class to the ``discriminator`` attribute, + thus persisting the value to the ``discriminator`` column + in the database. + + .. seealso:: + + :ref:`inheritance_toplevel` + + :param polymorphic_identity: Specifies the value which + identifies this particular class as returned by the + column expression referred to by the ``polymorphic_on`` + setting. As rows are received, the value corresponding + to the ``polymorphic_on`` column expression is compared + to this value, indicating which subclass should + be used for the newly reconstructed object. + + :param properties: A dictionary mapping the string names of object + attributes to :class:`.MapperProperty` instances, which define the + persistence behavior of that attribute. Note that :class:`.Column` + objects present in + the mapped :class:`.Table` are automatically placed into + ``ColumnProperty`` instances upon mapping, unless overridden. + When using Declarative, this argument is passed automatically, + based on all those :class:`.MapperProperty` instances declared + in the declared class body. + + :param primary_key: A list of :class:`.Column` objects which define the + primary key to be used against this mapper's selectable unit. + This is normally simply the primary key of the ``local_table``, but + can be overridden here. + + :param version_id_col: A :class:`.Column` + that will be used to keep a running version id of rows + in the table. This is used to detect concurrent updates or + the presence of stale data in a flush. The methodology is to + detect if an UPDATE statement does not match the last known + version id, a + :class:`~sqlalchemy.orm.exc.StaleDataError` exception is + thrown. + By default, the column must be of :class:`.Integer` type, + unless ``version_id_generator`` specifies an alternative version + generator. + + .. seealso:: + + :ref:`mapper_version_counter` - discussion of version counting + and rationale. + + :param version_id_generator: Define how new version ids should + be generated. Defaults to ``None``, which indicates that + a simple integer counting scheme be employed. To provide a custom + versioning scheme, provide a callable function of the form:: + + def generate_version(version): + return next_version + + Alternatively, server-side versioning functions such as triggers, + or programmatic versioning schemes outside of the version id generator + may be used, by specifying the value ``False``. + Please see :ref:`server_side_version_counter` for a discussion + of important points when using this option. + + .. versionadded:: 0.9.0 ``version_id_generator`` supports server-side + version number generation. + + .. seealso:: + + :ref:`custom_version_counter` + + :ref:`server_side_version_counter` + + + :param with_polymorphic: A tuple in the form ``(, + )`` indicating the default style of "polymorphic" + loading, that is, which tables are queried at once. is + any single or list of mappers and/or classes indicating the + inherited classes that should be loaded at once. The special value + ``'*'`` may be used to indicate all descending classes should be + loaded immediately. The second tuple argument + indicates a selectable that will be used to query for multiple + classes. + + .. seealso:: + + :ref:`with_polymorphic` - discussion of polymorphic querying techniques. """ @@ -114,9 +504,19 @@ class Mapper(object): self.order_by = order_by self.always_refresh = always_refresh - self.version_id_col = version_id_col - self.version_id_generator = version_id_generator or \ - (lambda x:(x or 0) + 1) + + if isinstance(version_id_col, MapperProperty): + self.version_id_prop = version_id_col + self.version_id_col = None + else: + self.version_id_col = version_id_col + if version_id_generator is False: + self.version_id_generator = False + elif version_id_generator is None: + self.version_id_generator = lambda x: (x or 0) + 1 + else: + self.version_id_generator = version_id_generator + self.concrete = concrete self.single = False self.inherits = inherits @@ -124,14 +524,16 @@ class Mapper(object): self.inherit_condition = inherit_condition self.inherit_foreign_keys = inherit_foreign_keys self._init_properties = properties or {} - self.delete_orphans = [] + self._delete_orphans = [] self.batch = batch self.eager_defaults = eager_defaults self.column_prefix = column_prefix - self.polymorphic_on = expression._clause_element_as_expr(polymorphic_on) + self.polymorphic_on = expression._clause_element_as_expr( + polymorphic_on) self._dependency_processors = [] self.validators = util.immutabledict() self.passive_updates = passive_updates + self.legacy_is_orphan = legacy_is_orphan self._clause_adapter = None self._requires_row_aliasing = False self._inherits_equated_pairs = None @@ -140,18 +542,11 @@ class Mapper(object): self._reconstructor = None self._deprecated_extensions = util.to_list(extension or []) - if allow_null_pks: - util.warn_deprecated( - "the allow_null_pks option to Mapper() is " - "deprecated. It is now allow_partial_pks=False|True, " - "defaults to True.") - allow_partial_pks = allow_null_pks - self.allow_partial_pks = allow_partial_pks self._set_with_polymorphic(with_polymorphic) - if isinstance(self.local_table, expression._SelectBase): + if isinstance(self.local_table, expression.SelectBase): raise sa_exc.InvalidRequestError( "When mapping against a select() construct, map against " "an alias() of the construct instead." @@ -161,7 +556,7 @@ class Mapper(object): if self.with_polymorphic and \ isinstance(self.with_polymorphic[1], - expression._SelectBase): + expression.SelectBase): self.with_polymorphic = (self.with_polymorphic[0], self.with_polymorphic[1].alias()) @@ -190,10 +585,11 @@ class Mapper(object): self.configured = False # prevent this mapper from being constructed - # while a configure_mappers() is occurring (and defer a configure_mappers() - # until construction succeeds) - _COMPILE_MUTEX.acquire() + # while a configure_mappers() is occurring (and defer a + # configure_mappers() until construction succeeds) + _CONFIGURE_MUTEX.acquire() try: + self.dispatch._events._new_mapper_instance(class_, self) self._configure_inheritance() self._configure_legacy_instrument_class() self._configure_class_instrumentation() @@ -201,16 +597,36 @@ class Mapper(object): self._configure_properties() self._configure_polymorphic_setter() self._configure_pks() - global _new_mappers - _new_mappers = True + Mapper._new_mappers = True self._log("constructed") self._expire_memoizations() finally: - _COMPILE_MUTEX.release() + _CONFIGURE_MUTEX.release() # major attributes initialized at the classlevel so that # they can be Sphinx-documented. + is_mapper = True + """Part of the inspection API.""" + + @property + def mapper(self): + """Part of the inspection API. + + Returns self. + + """ + return self + + @property + def entity(self): + """Part of the inspection API. + + Returns self.class\_. + + """ + return self.class_ + local_table = None """The :class:`.Selectable` which this :class:`.Mapper` manages. @@ -226,7 +642,9 @@ class Mapper(object): this :class:`.Mapper` represents. If this mapper is a single-table inheriting mapper, local_table will be ``None``. - See also :attr:`~.Mapper.mapped_table`. + .. seealso:: + + :attr:`~.Mapper.mapped_table`. """ @@ -244,7 +662,9 @@ class Mapper(object): subclass. For single-table inheritance mappers, mapped_table references the base table. - See also :attr:`~.Mapper.local_table`. + .. seealso:: + + :attr:`~.Mapper.local_table`. """ @@ -263,7 +683,9 @@ class Mapper(object): This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - See also :func:`.configure_mappers`. + .. seealso:: + + :func:`.configure_mappers`. """ @@ -294,11 +716,11 @@ class Mapper(object): which comprise the 'primary key' of the mapped table, from the perspective of this :class:`.Mapper`. - This list is against the selectable in :attr:`~.Mapper.mapped_table`. In the - case of inheriting mappers, some columns may be managed by a superclass - mapper. For example, in the case of a :class:`.Join`, the primary - key is determined by all of the primary key columns across all tables - referenced by the :class:`.Join`. + This list is against the selectable in :attr:`~.Mapper.mapped_table`. In + the case of inheriting mappers, some columns may be managed by a + superclass mapper. For example, in the case of a :class:`.Join`, the + primary key is determined by all of the primary key columns across all + tables referenced by the :class:`.Join`. The list is also not necessarily the same as the primary key column collection associated with the underlying tables; the :class:`.Mapper` @@ -349,11 +771,13 @@ class Mapper(object): """ polymorphic_on = None - """The :class:`.Column` specified as the ``polymorphic_on`` column + """The :class:`.Column` or SQL expression specified as the + ``polymorphic_on`` argument for this :class:`.Mapper`, within an inheritance scenario. - This attribute may also be of other types besides :class:`.Column` - in a future SQLAlchemy release. + This attribute is normally a :class:`.Column` instance but + may also be an expression, such as one derived from + :func:`.cast`. This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. @@ -361,8 +785,8 @@ class Mapper(object): """ polymorphic_map = None - """A mapping of "polymorphic identity" identifiers mapped to :class:`.Mapper` - instances, within an inheritance scenario. + """A mapping of "polymorphic identity" identifiers mapped to + :class:`.Mapper` instances, within an inheritance scenario. The identifiers can be of any type which is comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. @@ -377,11 +801,12 @@ class Mapper(object): """ polymorphic_identity = None - """Represent an identifier which is matched against the :attr:`~.Mapper.polymorphic_on` - column during result row loading. + """Represent an identifier which is matched against the + :attr:`~.Mapper.polymorphic_on` column during result row loading. Used only with inheritance, this object can be of any type which is - comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. + comparable to the type of column represented by + :attr:`~.Mapper.polymorphic_on`. This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. @@ -429,18 +854,20 @@ class Mapper(object): c = None """A synonym for :attr:`~.Mapper.columns`.""" - dispatch = event.dispatcher(events.MapperEvents) + @util.memoized_property + def _path_registry(self): + return PathRegistry.per_mapper(self) def _configure_inheritance(self): """Configure settings related to inherting and/or inherited mappers being present.""" # a set of all mappers which inherit from this one. - self._inheriting_mappers = set() + self._inheriting_mappers = util.WeakSequence() if self.inherits: if isinstance(self.inherits, type): - self.inherits = class_mapper(self.inherits, compile=False) + self.inherits = class_mapper(self.inherits, configure=False) if not issubclass(self.class_, self.inherits.class_): raise sa_exc.ArgumentError( "Class '%s' does not inherit from '%s'" % @@ -468,7 +895,7 @@ class Mapper(object): # immediate table of the inherited mapper, not its # full table which could pull in other stuff we dont # want (allows test/inheritance.InheritTest4 to pass) - self.inherit_condition = sqlutil.join_condition( + self.inherit_condition = sql_util.join_condition( self.inherits.local_table, self.local_table) self.mapped_table = sql.join( @@ -477,7 +904,7 @@ class Mapper(object): self.inherit_condition) fks = util.to_set(self.inherit_foreign_keys) - self._inherits_equated_pairs = sqlutil.criterion_as_pairs( + self._inherits_equated_pairs = sql_util.criterion_as_pairs( self.mapped_table.onclause, consider_as_foreign_keys=fks) else: @@ -510,7 +937,7 @@ class Mapper(object): self.polymorphic_map = self.inherits.polymorphic_map self.batch = self.inherits.batch - self.inherits._inheriting_mappers.add(self) + self.inherits._inheriting_mappers.append(self) self.base_mapper = self.inherits.base_mapper self.passive_updates = self.inherits.passive_updates self._all_tables = self.inherits._all_tables @@ -535,7 +962,7 @@ class Mapper(object): if with_polymorphic == '*': self.with_polymorphic = ('*', None) elif isinstance(with_polymorphic, (tuple, list)): - if isinstance(with_polymorphic[0], (basestring, tuple, list)): + if isinstance(with_polymorphic[0], util.string_types + (tuple, list)): self.with_polymorphic = with_polymorphic else: self.with_polymorphic = (with_polymorphic, None) @@ -544,7 +971,7 @@ class Mapper(object): else: self.with_polymorphic = None - if isinstance(self.local_table, expression._SelectBase): + if isinstance(self.local_table, expression.SelectBase): raise sa_exc.InvalidRequestError( "When mapping against a select() construct, map against " "an alias() of the construct instead." @@ -554,16 +981,16 @@ class Mapper(object): if self.with_polymorphic and \ isinstance(self.with_polymorphic[1], - expression._SelectBase): + expression.SelectBase): self.with_polymorphic = (self.with_polymorphic[0], self.with_polymorphic[1].alias()) if self.configured: self._expire_memoizations() def _set_concrete_base(self, mapper): - """Set the given :class:`.Mapper` as the 'inherits' for this :class:`.Mapper`, - assuming this :class:`.Mapper` is concrete and does not already have - an inherits.""" + """Set the given :class:`.Mapper` as the 'inherits' for this + :class:`.Mapper`, assuming this :class:`.Mapper` is concrete + and does not already have an inherits.""" assert self.concrete assert not self.inherits @@ -577,16 +1004,15 @@ class Mapper(object): self.batch = self.inherits.batch for mp in self.self_and_descendants: mp.base_mapper = self.inherits.base_mapper - self.inherits._inheriting_mappers.add(self) + self.inherits._inheriting_mappers.append(self) self.passive_updates = self.inherits.passive_updates self._all_tables = self.inherits._all_tables - for key, prop in mapper._props.iteritems(): + for key, prop in mapper._props.items(): if key not in self._props and \ not self._should_exclude(key, key, local=False, column=None): self._adapt_inherited_property(key, prop, False) - def _set_polymorphic_on(self, polymorphic_on): self.polymorphic_on = polymorphic_on self._configure_polymorphic_setter(True) @@ -595,8 +1021,9 @@ class Mapper(object): if self.inherits: self.dispatch._update(self.inherits.dispatch) - super_extensions = set(chain(*[m._deprecated_extensions - for m in self.inherits.iterate_to_root()])) + super_extensions = set( + chain(*[m._deprecated_extensions + for m in self.inherits.iterate_to_root()])) else: super_extensions = set() @@ -606,8 +1033,9 @@ class Mapper(object): def _configure_listeners(self): if self.inherits: - super_extensions = set(chain(*[m._deprecated_extensions - for m in self.inherits.iterate_to_root()])) + super_extensions = set( + chain(*[m._deprecated_extensions + for m in self.inherits.iterate_to_root()])) else: super_extensions = set() @@ -615,10 +1043,6 @@ class Mapper(object): if ext not in super_extensions: ext._adapt_listener(self, ext) - if self.inherits: - self.class_manager.dispatch._update( - self.inherits.class_manager.dispatch) - def _configure_class_instrumentation(self): """If this mapper is to be a primary mapper (i.e. the non_primary flag is not set), associate this Mapper with the @@ -667,7 +1091,8 @@ class Mapper(object): self.class_manager = manager manager.mapper = self - manager.deferred_scalar_loader = self._load_scalar_attributes + manager.deferred_scalar_loader = util.partial( + loading.load_scalar_attributes, self) # The remaining members can be added by any mapper, # e_name None or not. @@ -684,30 +1109,20 @@ class Mapper(object): self._reconstructor = method event.listen(manager, 'load', _event_on_load, raw=True) elif hasattr(method, '__sa_validators__'): - include_removes = getattr(method, "__sa_include_removes__", False) + validation_opts = method.__sa_validation_opts__ for name in method.__sa_validators__: self.validators = self.validators.union( - {name : (method, include_removes)} + {name: (method, validation_opts)} ) manager.info[_INSTRUMENTOR] = self - @util.deprecated("0.7", message=":meth:`.Mapper.compile` " - "is replaced by :func:`.configure_mappers`") - def compile(self): - """Initialize the inter-mapper relationships of all mappers that - have been constructed thus far. + @classmethod + def _configure_all(cls): + """Class-level path to the :func:`.configure_mappers` call. """ configure_mappers() - return self - - - @property - @util.deprecated("0.7", message=":attr:`.Mapper.compiled` " - "is replaced by :attr:`.Mapper.configured`") - def compiled(self): - return self.configured def dispose(self): # Disable any attribute-based compilation. @@ -717,13 +1132,14 @@ class Mapper(object): del self._configure_failed if not self.non_primary and \ + self.class_manager is not None and \ self.class_manager.is_mapped and \ - self.class_manager.mapper is self: + self.class_manager.mapper is self: instrumentation.unregister_class(self.class_) def _configure_pks(self): - self.tables = sqlutil.find_tables(self.mapped_table) + self.tables = sql_util.find_tables(self.mapped_table) self._pks_by_table = {} self._cols_by_table = {} @@ -741,7 +1157,7 @@ class Mapper(object): if t.primary_key and pk_cols.issuperset(t.primary_key): # ordering is important since it determines the ordering of # mapper.primary_key (and therefore query.get()) - self._pks_by_table[t] =\ + self._pks_by_table[t] = \ util.ordered_column_set(t.primary_key).\ intersection(pk_cols) self._cols_by_table[t] = \ @@ -788,12 +1204,12 @@ class Mapper(object): # determine primary key from argument or mapped_table pks - # reduce to the minimal set of columns if self._primary_key_argument: - primary_key = sqlutil.reduce_columns( + primary_key = sql_util.reduce_columns( [self.mapped_table.corresponding_column(c) for c in self._primary_key_argument], ignore_nonexistent_tables=True) else: - primary_key = sqlutil.reduce_columns( + primary_key = sql_util.reduce_columns( self._pks_by_table[self.mapped_table], ignore_nonexistent_tables=True) @@ -821,14 +1237,15 @@ class Mapper(object): # load custom properties if self._init_properties: - for key, prop in self._init_properties.iteritems(): + for key, prop in self._init_properties.items(): self._configure_property(key, prop, False) # pull properties from the inherited mapper if any. if self.inherits: - for key, prop in self.inherits._props.iteritems(): + for key, prop in self.inherits._props.items(): if key not in self._props and \ - not self._should_exclude(key, key, local=False, column=None): + not self._should_exclude(key, key, local=False, + column=None): self._adapt_inherited_property(key, prop, False) # create properties for each column in the mapped table, @@ -873,7 +1290,7 @@ class Mapper(object): if self.polymorphic_on is not None: setter = True - if isinstance(self.polymorphic_on, basestring): + if isinstance(self.polymorphic_on, util.string_types): # polymorphic_on specified as as string - link # it to mapped ColumnProperty try: @@ -894,7 +1311,8 @@ class Mapper(object): elif isinstance(self.polymorphic_on, MapperProperty): # polymorphic_on is directly a MapperProperty, # ensure it's a ColumnProperty - if not isinstance(self.polymorphic_on, properties.ColumnProperty): + if not isinstance(self.polymorphic_on, + properties.ColumnProperty): raise sa_exc.ArgumentError( "Only direct column-mapped " "property or SQL expression " @@ -902,7 +1320,7 @@ class Mapper(object): prop = self.polymorphic_on self.polymorphic_on = prop.columns[0] polymorphic_key = prop.key - elif not expression.is_column(self.polymorphic_on): + elif not expression._is_column(self.polymorphic_on): # polymorphic_on is not a Column and not a ColumnProperty; # not supported right now. raise sa_exc.ArgumentError( @@ -911,27 +1329,31 @@ class Mapper(object): "can be passed for polymorphic_on" ) else: - # polymorphic_on is a Column or SQL expression and doesn't - # appear to be mapped. - # this means it can be 1. only present in the with_polymorphic - # selectable or 2. a totally standalone SQL expression which we'd + # polymorphic_on is a Column or SQL expression and + # doesn't appear to be mapped. this means it can be 1. + # only present in the with_polymorphic selectable or + # 2. a totally standalone SQL expression which we'd # hope is compatible with this mapper's mapped_table - col = self.mapped_table.corresponding_column(self.polymorphic_on) + col = self.mapped_table.corresponding_column( + self.polymorphic_on) if col is None: - # polymorphic_on doesn't derive from any column/expression - # isn't present in the mapped table. - # we will make a "hidden" ColumnProperty for it. - # Just check that if it's directly a schema.Column and we - # have with_polymorphic, it's likely a user error if the - # schema.Column isn't represented somehow in either mapped_table or - # with_polymorphic. Otherwise as of 0.7.4 we just go with it - # and assume the user wants it that way (i.e. a CASE statement) + # polymorphic_on doesn't derive from any + # column/expression isn't present in the mapped + # table. we will make a "hidden" ColumnProperty + # for it. Just check that if it's directly a + # schema.Column and we have with_polymorphic, it's + # likely a user error if the schema.Column isn't + # represented somehow in either mapped_table or + # with_polymorphic. Otherwise as of 0.7.4 we + # just go with it and assume the user wants it + # that way (i.e. a CASE statement) setter = False instrument = False col = self.polymorphic_on if isinstance(col, schema.Column) and ( self.with_polymorphic is None or \ - self.with_polymorphic[1].corresponding_column(col) is None + self.with_polymorphic[1].\ + corresponding_column(col) is None ): raise sa_exc.InvalidRequestError( "Could not map polymorphic_on column " @@ -944,22 +1366,26 @@ class Mapper(object): # and is probably mapped, but polymorphic_on itself # is not. This happens when # the polymorphic_on is only directly present in the - # with_polymorphic selectable, as when use polymorphic_union. + # with_polymorphic selectable, as when use + # polymorphic_union. # we'll make a separate ColumnProperty for it. instrument = True key = getattr(col, 'key', None) if key: if self._should_exclude(col.key, col.key, False, col): raise sa_exc.InvalidRequestError( - "Cannot exclude or override the discriminator column %r" % + "Cannot exclude or override the " + "discriminator column %r" % col.key) else: - self.polymorphic_on = col = col.label("_sa_polymorphic_on") + self.polymorphic_on = col = \ + col.label("_sa_polymorphic_on") key = col.key self._configure_property( key, - properties.ColumnProperty(col, _instrument=instrument), + properties.ColumnProperty(col, + _instrument=instrument), init=init, setparent=True) polymorphic_key = key else: @@ -982,7 +1408,10 @@ class Mapper(object): # directly; it ensures the polymorphic_identity of the # instance's mapper is used so is portable to subclasses. if self.polymorphic_on is not None: - self._set_polymorphic_identity = mapper._set_polymorphic_identity + self._set_polymorphic_identity = \ + mapper._set_polymorphic_identity + self._validate_polymorphic_identity = \ + mapper._validate_polymorphic_identity else: self._set_polymorphic_identity = None return @@ -993,11 +1422,46 @@ class Mapper(object): state.get_impl(polymorphic_key).set(state, dict_, state.manager.mapper.polymorphic_identity, None) + def _validate_polymorphic_identity(mapper, state, dict_): + if polymorphic_key in dict_ and \ + dict_[polymorphic_key] not in \ + mapper._acceptable_polymorphic_identities: + util.warn( + "Flushing object %s with " + "incompatible polymorphic identity %r; the " + "object may not refresh and/or load correctly" % ( + state_str(state), + dict_[polymorphic_key] + ) + ) + self._set_polymorphic_identity = _set_polymorphic_identity + self._validate_polymorphic_identity = _validate_polymorphic_identity else: self._set_polymorphic_identity = None + _validate_polymorphic_identity = None + + @_memoized_configured_property + def _version_id_prop(self): + if self.version_id_col is not None: + return self._columntoproperty[self.version_id_col] + else: + return None + + @_memoized_configured_property + def _acceptable_polymorphic_identities(self): + identities = set() + + stack = deque([self]) + while stack: + item = stack.popleft() + if item.mapped_table is self.mapped_table: + identities.add(item.polymorphic_identity) + stack.extend(item._inheriting_mappers) + + return identities def _adapt_inherited_property(self, key, prop, init): if not self.concrete: @@ -1012,67 +1476,7 @@ class Mapper(object): self._log("_configure_property(%s, %s)", key, prop.__class__.__name__) if not isinstance(prop, MapperProperty): - # we were passed a Column or a list of Columns; - # generate a properties.ColumnProperty - columns = util.to_list(prop) - column = columns[0] - if not expression.is_column(column): - raise sa_exc.ArgumentError( - "%s=%r is not an instance of MapperProperty or Column" - % (key, prop)) - - prop = self._props.get(key, None) - - if isinstance(prop, properties.ColumnProperty): - if prop.parent is self: - raise sa_exc.InvalidRequestError( - "Implicitly combining column %s with column " - "%s under attribute '%s'. Please configure one " - "or more attributes for these same-named columns " - "explicitly." - % (prop.columns[-1], column, key)) - - # existing properties.ColumnProperty from an inheriting - # mapper. make a copy and append our column to it - prop = prop.copy() - prop.columns.insert(0, column) - self._log("inserting column to existing list " - "in properties.ColumnProperty %s" % (key)) - - elif prop is None or isinstance(prop, properties.ConcreteInheritedProperty): - mapped_column = [] - for c in columns: - mc = self.mapped_table.corresponding_column(c) - if mc is None: - mc = self.local_table.corresponding_column(c) - if mc is not None: - # if the column is in the local table but not the - # mapped table, this corresponds to adding a - # column after the fact to the local table. - # [ticket:1523] - self.mapped_table._reset_exported() - mc = self.mapped_table.corresponding_column(c) - if mc is None: - raise sa_exc.ArgumentError( - "When configuring property '%s' on %s, " - "column '%s' is not represented in the mapper's " - "table. Use the `column_property()` function to " - "force this column to be mapped as a read-only " - "attribute." % (key, self, c)) - mapped_column.append(mc) - prop = properties.ColumnProperty(*mapped_column) - else: - raise sa_exc.ArgumentError( - "WARNING: when configuring property '%s' on %s, " - "column '%s' conflicts with property '%r'. " - "To resolve this, map the column to the class under a " - "different name in the 'properties' dictionary. Or, " - "to remove all awareness of the column entirely " - "(including its availability as a foreign key), " - "use the 'include_properties' or 'exclude_properties' " - "mapper arguments to control specifically which table " - "columns get mapped." % - (key, self, column.key, prop)) + prop = self._property_from_column(key, prop) if isinstance(prop, properties.ColumnProperty): col = self.mapped_table.corresponding_column(prop.columns[0]) @@ -1101,7 +1505,7 @@ class Mapper(object): # initialized; check for 'readonly' if hasattr(self, '_readonly_props') and \ (not hasattr(col, 'table') or - col.table not in self._cols_by_table): + col.table not in self._cols_by_table): self._readonly_props.add(prop) else: @@ -1139,6 +1543,16 @@ class Mapper(object): "%r for column %r" % (syn, key, key, syn) ) + if key in self._props and \ + not isinstance(prop, properties.ColumnProperty) and \ + not isinstance(self._props[key], properties.ColumnProperty): + util.warn("Property %s on %s being replaced with new " + "property %s; the old property will be discarded" % ( + self._props[key], + self, + prop, + )) + self._props[key] = prop if not self.non_primary: @@ -1154,6 +1568,73 @@ class Mapper(object): if self.configured: self._expire_memoizations() + def _property_from_column(self, key, prop): + """generate/update a :class:`.ColumnProprerty` given a + :class:`.Column` object. """ + + # we were passed a Column or a list of Columns; + # generate a properties.ColumnProperty + columns = util.to_list(prop) + column = columns[0] + if not expression._is_column(column): + raise sa_exc.ArgumentError( + "%s=%r is not an instance of MapperProperty or Column" + % (key, prop)) + + prop = self._props.get(key, None) + + if isinstance(prop, properties.ColumnProperty): + if prop.parent is self: + raise sa_exc.InvalidRequestError( + "Implicitly combining column %s with column " + "%s under attribute '%s'. Please configure one " + "or more attributes for these same-named columns " + "explicitly." + % (prop.columns[-1], column, key)) + + # existing properties.ColumnProperty from an inheriting + # mapper. make a copy and append our column to it + prop = prop.copy() + prop.columns.insert(0, column) + self._log("inserting column to existing list " + "in properties.ColumnProperty %s" % (key)) + return prop + elif prop is None or isinstance(prop, + properties.ConcreteInheritedProperty): + mapped_column = [] + for c in columns: + mc = self.mapped_table.corresponding_column(c) + if mc is None: + mc = self.local_table.corresponding_column(c) + if mc is not None: + # if the column is in the local table but not the + # mapped table, this corresponds to adding a + # column after the fact to the local table. + # [ticket:1523] + self.mapped_table._reset_exported() + mc = self.mapped_table.corresponding_column(c) + if mc is None: + raise sa_exc.ArgumentError( + "When configuring property '%s' on %s, " + "column '%s' is not represented in the mapper's " + "table. Use the `column_property()` function to " + "force this column to be mapped as a read-only " + "attribute." % (key, self, c)) + mapped_column.append(mc) + return properties.ColumnProperty(*mapped_column) + else: + raise sa_exc.ArgumentError( + "WARNING: when configuring property '%s' on %s, " + "column '%s' conflicts with property '%r'. " + "To resolve this, map the column to the class under a " + "different name in the 'properties' dictionary. Or, " + "to remove all awareness of the column entirely " + "(including its availability as a foreign key), " + "use the 'include_properties' or 'exclude_properties' " + "mapper arguments to control specifically which table " + "columns get mapped." % + (key, self, column.key, prop)) + def _post_configure_properties(self): """Call the ``init()`` method on all ``MapperProperties`` attached to this mapper. @@ -1164,14 +1645,14 @@ class Mapper(object): """ self._log("_post_configure_properties() started") - l = [(key, prop) for key, prop in self._props.iteritems()] + l = [(key, prop) for key, prop in self._props.items()] for key, prop in l: self._log("initialize prop %s", key) - if prop.parent is self and not prop._compile_started: + if prop.parent is self and not prop._configure_started: prop.init() - if prop._compile_finished: + if prop._configure_finished: prop.post_instrument_class(self) self._log("_post_configure_properties() complete") @@ -1182,7 +1663,7 @@ class Mapper(object): using `add_property`. """ - for key, value in dict_of_properties.iteritems(): + for key, value in dict_of_properties.items(): self.add_property(key, value) def add_property(self, key, prop): @@ -1212,7 +1693,6 @@ class Mapper(object): "|non-primary" or "") + ")" def _log(self, msg, *args): - self.logger.info( "%s " + msg, *((self._log_desc,) + args) ) @@ -1235,23 +1715,32 @@ class Mapper(object): ) def _is_orphan(self, state): - o = False + orphan_possible = False for mapper in self.iterate_to_root(): - for (key, cls) in mapper.delete_orphans: - if attributes.manager_of_class(cls).has_parent( - state, key, optimistic=bool(state.key)): + for (key, cls) in mapper._delete_orphans: + orphan_possible = True + + has_parent = attributes.manager_of_class(cls).has_parent( + state, key, optimistic=state.has_identity) + + if self.legacy_is_orphan and has_parent: return False - o = o or bool(mapper.delete_orphans) - return o + elif not self.legacy_is_orphan and not has_parent: + return True + + if self.legacy_is_orphan: + return orphan_possible + else: + return False def has_property(self, key): return key in self._props - def get_property(self, key, _compile_mappers=True): + def get_property(self, key, _configure_mappers=True): """return a MapperProperty associated with the given key. """ - if _compile_mappers and _new_mappers: + if _configure_mappers and Mapper._new_mappers: configure_mappers() try: @@ -1260,12 +1749,6 @@ class Mapper(object): raise sa_exc.InvalidRequestError( "Mapper '%s' has no property '%s'" % (self, key)) - @util.deprecated('0.6.4', - 'Call to deprecated function mapper._get_col_to_pr' - 'op(). Use mapper.get_property_by_column()') - def _get_col_to_prop(self, col): - return self._columntoproperty[col] - def get_property_by_column(self, column): """Given a :class:`.Column` object, return the :class:`.MapperProperty` which maps this column.""" @@ -1275,9 +1758,9 @@ class Mapper(object): @property def iterate_properties(self): """return an iterator of all MapperProperty objects.""" - if _new_mappers: + if Mapper._new_mappers: configure_mappers() - return self._props.itervalues() + return iter(self._props.values()) def _mappers_from_spec(self, spec, selectable): """given a with_polymorphic() argument, return the set of mappers it @@ -1290,29 +1773,34 @@ class Mapper(object): if spec == '*': mappers = list(self.self_and_descendants) elif spec: - mappers = [_class_to_mapper(m) for m in util.to_list(spec)] - for m in mappers: + mappers = set() + for m in util.to_list(spec): + m = _class_to_mapper(m) if not m.isa(self): raise sa_exc.InvalidRequestError( - "%r does not inherit from %r" % + "%r does not inherit from %r" % (m, self)) + + if selectable is None: + mappers.update(m.iterate_to_root()) + else: + mappers.add(m) + mappers = [m for m in self.self_and_descendants if m in mappers] else: mappers = [] if selectable is not None: - tables = set(sqlutil.find_tables(selectable, + tables = set(sql_util.find_tables(selectable, include_aliases=True)) mappers = [m for m in mappers if m.local_table in tables] - return mappers - def _selectable_from_mappers(self, mappers): + def _selectable_from_mappers(self, mappers, innerjoin): """given a list of mappers (assumed to be within this mapper's inheritance hierarchy), construct an outerjoin amongst those mapper's mapped tables. """ - from_obj = self.mapped_table for m in mappers: if m is self: @@ -1322,7 +1810,11 @@ class Mapper(object): "'with_polymorphic()' requires 'selectable' argument " "when concrete-inheriting mappers are used.") elif not m.single: - from_obj = from_obj.outerjoin(m.local_table, + if innerjoin: + from_obj = from_obj.join(m.local_table, + m.inherit_condition) + else: + from_obj = from_obj.outerjoin(m.local_table, m.inherit_condition) return from_obj @@ -1340,8 +1832,10 @@ class Mapper(object): @_memoized_configured_property def _with_polymorphic_mappers(self): + if Mapper._new_mappers: + configure_mappers() if not self.with_polymorphic: - return [self] + return [] return self._mappers_from_spec(*self.with_polymorphic) @_memoized_configured_property @@ -1354,30 +1848,52 @@ class Mapper(object): return selectable else: return self._selectable_from_mappers( - self._mappers_from_spec(spec, selectable)) + self._mappers_from_spec(spec, selectable), + False) - def _with_polymorphic_args(self, spec=None, selectable=False): + with_polymorphic_mappers = _with_polymorphic_mappers + """The list of :class:`.Mapper` objects included in the + default "polymorphic" query. + + """ + + @property + def selectable(self): + """The :func:`.select` construct this :class:`.Mapper` selects from + by default. + + Normally, this is equivalent to :attr:`.mapped_table`, unless + the ``with_polymorphic`` feature is in use, in which case the + full "polymorphic" selectable is returned. + + """ + return self._with_polymorphic_selectable + + def _with_polymorphic_args(self, spec=None, selectable=False, + innerjoin=False): if self.with_polymorphic: if not spec: spec = self.with_polymorphic[0] if selectable is False: selectable = self.with_polymorphic[1] - + elif selectable is False: + selectable = None mappers = self._mappers_from_spec(spec, selectable) if selectable is not None: return mappers, selectable else: - return mappers, self._selectable_from_mappers(mappers) + return mappers, self._selectable_from_mappers(mappers, + innerjoin) @_memoized_configured_property def _polymorphic_properties(self): - return tuple(self._iterate_polymorphic_properties( + return list(self._iterate_polymorphic_properties( self._with_polymorphic_mappers)) + def _iterate_polymorphic_properties(self, mappers=None): """Return an iterator of MapperProperty objects which will render into a SELECT.""" - if mappers is None: mappers = self._with_polymorphic_mappers @@ -1389,8 +1905,10 @@ class Mapper(object): # from other mappers, as these are sometimes dependent on that # mapper's polymorphic selectable (which we don't want rendered) for c in util.unique_list( - chain(*[list(mapper.iterate_properties) for mapper in [self] + - mappers]) + chain(*[ + list(mapper.iterate_properties) for mapper in + [self] + mappers + ]) ): if getattr(c, '_is_polymorphic_discriminator', False) and \ (self.polymorphic_on is None or @@ -1398,12 +1916,126 @@ class Mapper(object): continue yield c - @property - def properties(self): - raise NotImplementedError( - "Public collection of MapperProperty objects is " - "provided by the get_property() and iterate_properties " - "accessors.") + @util.memoized_property + def attrs(self): + """A namespace of all :class:`.MapperProperty` objects + associated this mapper. + + This is an object that provides each property based on + its key name. For instance, the mapper for a + ``User`` class which has ``User.name`` attribute would + provide ``mapper.attrs.name``, which would be the + :class:`.ColumnProperty` representing the ``name`` + column. The namespace object can also be iterated, + which would yield each :class:`.MapperProperty`. + + :class:`.Mapper` has several pre-filtered views + of this attribute which limit the types of properties + returned, inclding :attr:`.synonyms`, :attr:`.column_attrs`, + :attr:`.relationships`, and :attr:`.composites`. + + .. seealso:: + + :attr:`.Mapper.all_orm_descriptors` + + """ + if Mapper._new_mappers: + configure_mappers() + return util.ImmutableProperties(self._props) + + @util.memoized_property + def all_orm_descriptors(self): + """A namespace of all :class:`._InspectionAttr` attributes associated + with the mapped class. + + These attributes are in all cases Python :term:`descriptors` associated + with the mapped class or its superclasses. + + This namespace includes attributes that are mapped to the class + as well as attributes declared by extension modules. + It includes any Python descriptor type that inherits from + :class:`._InspectionAttr`. This includes :class:`.QueryableAttribute`, + as well as extension types such as :class:`.hybrid_property`, + :class:`.hybrid_method` and :class:`.AssociationProxy`. + + To distinguish between mapped attributes and extension attributes, + the attribute :attr:`._InspectionAttr.extension_type` will refer + to a constant that distinguishes between different extension types. + + When dealing with a :class:`.QueryableAttribute`, the + :attr:`.QueryableAttribute.property` attribute refers to the + :class:`.MapperProperty` property, which is what you get when referring + to the collection of mapped properties via :attr:`.Mapper.attrs`. + + .. versionadded:: 0.8.0 + + .. seealso:: + + :attr:`.Mapper.attrs` + + """ + return util.ImmutableProperties( + dict(self.class_manager._all_sqla_attributes())) + + @_memoized_configured_property + def synonyms(self): + """Return a namespace of all :class:`.SynonymProperty` + properties maintained by this :class:`.Mapper`. + + .. seealso:: + + :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` + objects. + + """ + return self._filter_properties(properties.SynonymProperty) + + @_memoized_configured_property + def column_attrs(self): + """Return a namespace of all :class:`.ColumnProperty` + properties maintained by this :class:`.Mapper`. + + .. seealso:: + + :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` + objects. + + """ + return self._filter_properties(properties.ColumnProperty) + + @_memoized_configured_property + def relationships(self): + """Return a namespace of all :class:`.RelationshipProperty` + properties maintained by this :class:`.Mapper`. + + .. seealso:: + + :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` + objects. + + """ + return self._filter_properties(properties.RelationshipProperty) + + @_memoized_configured_property + def composites(self): + """Return a namespace of all :class:`.CompositeProperty` + properties maintained by this :class:`.Mapper`. + + .. seealso:: + + :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` + objects. + + """ + return self._filter_properties(properties.CompositeProperty) + + def _filter_properties(self, type_): + if Mapper._new_mappers: + configure_mappers() + return util.ImmutableProperties(util.OrderedDict( + (k, v) for k, v in self._props.items() + if isinstance(v, type_) + )) @_memoized_configured_property def _get_clause(self): @@ -1414,7 +2046,7 @@ class Mapper(object): """ params = [(primary_key, sql.bindparam(None, type_=primary_key.type)) for primary_key in self.primary_key] - return sql.and_(*[k==v for (k, v) in params]), \ + return sql.and_(*[k == v for (k, v) in params]), \ util.column_dict(params) @_memoized_configured_property @@ -1439,6 +2071,7 @@ class Mapper(object): """ result = util.column_dict() + def visit_binary(binary): if binary.operator == operators.eq: if binary.left in result: @@ -1453,24 +2086,17 @@ class Mapper(object): if mapper.inherit_condition is not None: visitors.traverse( mapper.inherit_condition, {}, - {'binary':visit_binary}) + {'binary': visit_binary}) return result def _is_userland_descriptor(self, obj): - if isinstance(obj, (MapperProperty, - attributes.QueryableAttribute)): - return False - elif not hasattr(obj, '__get__'): + if isinstance(obj, (_MappedAttribute, + instrumentation.ClassManager, + expression.ColumnElement)): return False else: - obj = util.unbound_method_to_callable(obj) - if isinstance( - obj.__get__(None, obj), - attributes.QueryableAttribute - ): - return False - return True + return True def _should_exclude(self, name, assigned_name, local, column): """determine whether a particular property should be implicitly @@ -1481,8 +2107,8 @@ class Mapper(object): """ - # check for descriptors, either local or from - # an inherited class + # check for class-bound attributes and/or descriptors, + # either local or from an inherited class if local: if self.class_.__dict__.get(assigned_name, None) is not None \ and self._is_userland_descriptor( @@ -1551,7 +2177,7 @@ class Mapper(object): item = stack.popleft() descendants.append(item) stack.extend(item._inheriting_mappers) - return tuple(descendants) + return util.WeakSequence(descendants) def polymorphic_iterator(self): """Iterate through the collection including this mapper and @@ -1580,10 +2206,11 @@ class Mapper(object): """Return an identity-map key for use in storing/retrieving an item from the identity map. - row - A ``sqlalchemy.engine.base.RowProxy`` instance or a - dictionary corresponding result-set ``ColumnElement`` - instances to their values within a row. + :param row: A :class:`.RowProxy` instance. The columns which are mapped + by this :class:`.Mapper` should be locatable in the row, preferably + via the :class:`.Column` object directly (as is the case when a + :func:`.select` construct is executed), or via string names of the form + ``_``. """ pk_cols = self.primary_key @@ -1597,8 +2224,7 @@ class Mapper(object): """Return an identity-map key for use in storing/retrieving an item from an identity map. - primary_key - A list of values indicating the identifier. + :param primary_key: A list of values indicating the identifier. """ return self._identity_class, tuple(primary_key) @@ -1607,6 +2233,11 @@ class Mapper(object): """Return the identity key for the given instance, based on its primary key attributes. + If the instance's state is expired, calling this method + will result in a database check to see if the object has been deleted. + If the row no longer exists, + :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. + This value is typically also found on the instance state under the attribute name `key`. @@ -1627,6 +2258,11 @@ class Mapper(object): """Return the list of primary key values for the given instance. + If the instance's state is expired, calling this method + will result in a database check to see if the object has been deleted. + If the row no longer exists, + :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. + """ state = attributes.instance_state(instance) return self._primary_key_from_state(state) @@ -1661,63 +2297,6 @@ class Mapper(object): return state.manager[prop.key].impl.\ get_committed_value(state, dict_, passive=passive) - def _load_scalar_attributes(self, state, attribute_names): - """initiate a column-based attribute refresh operation.""" - - #assert mapper is _state_mapper(state) - session = sessionlib._state_session(state) - if not session: - raise orm_exc.DetachedInstanceError( - "Instance %s is not bound to a Session; " - "attribute refresh operation cannot proceed" % - (state_str(state))) - - has_key = bool(state.key) - - result = False - - if self.inherits and not self.concrete: - statement = self._optimized_get_statement(state, attribute_names) - if statement is not None: - result = session.query(self).from_statement(statement).\ - _load_on_ident(None, - only_load_props=attribute_names, - refresh_state=state) - - if result is False: - if has_key: - identity_key = state.key - else: - # this codepath is rare - only valid when inside a flush, and the - # object is becoming persistent but hasn't yet been assigned an identity_key. - # check here to ensure we have the attrs we need. - pk_attrs = [self._columntoproperty[col].key - for col in self.primary_key] - if state.expired_attributes.intersection(pk_attrs): - raise sa_exc.InvalidRequestError("Instance %s cannot be refreshed - it's not " - " persistent and does not " - "contain a full primary key." % state_str(state)) - identity_key = self._identity_key_from_state(state) - - if (_none_set.issubset(identity_key) and \ - not self.allow_partial_pks) or \ - _none_set.issuperset(identity_key): - util.warn("Instance %s to be refreshed doesn't " - "contain a full primary key - can't be refreshed " - "(and shouldn't be expired, either)." - % state_str(state)) - return - - result = session.query(self)._load_on_ident( - identity_key, - refresh_state=state, - only_load_props=attribute_names) - - # if instance is pending, a refresh operation - # may not complete (even if PK attributes are assigned) - if has_key and result is None: - raise orm_exc.ObjectDeletedError(state) - def _optimized_get_statement(self, state, attribute_names): """assemble a WHERE clause which retrieves a given state by primary key, using a minimized set of tables. @@ -1731,7 +2310,7 @@ class Mapper(object): props = self._props tables = set(chain( - *[sqlutil.find_tables(c, check_columns=True) + *[sql_util.find_tables(c, check_columns=True) for key in attribute_names for c in props[key].columns] )) @@ -1762,7 +2341,8 @@ class Mapper(object): state, state.dict, rightcol, passive=attributes.PASSIVE_NO_INITIALIZE) - if rightval is attributes.PASSIVE_NO_RESULT or rightval is None: + if rightval is attributes.PASSIVE_NO_RESULT or \ + rightval is None: raise ColumnsNotAvailable() binary.right = sql.bindparam(None, rightval, type_=binary.right.type) @@ -1774,11 +2354,13 @@ class Mapper(object): for mapper in reversed(list(self.iterate_to_root())): if mapper.local_table in tables: start = True + elif not isinstance(mapper.local_table, expression.TableClause): + return None if start and not mapper.single: allconds.append(visitors.cloned_traverse( mapper.inherit_condition, {}, - {'binary':visit_binary} + {'binary': visit_binary} ) ) except ColumnsNotAvailable: @@ -1826,7 +2408,7 @@ class Mapper(object): queue = deque(prop.cascade_iterator(type_, parent_state, parent_dict, visited_states, halt_on)) if queue: - visitables.append((queue,mpp, None, None)) + visitables.append((queue, mpp, None, None)) elif item_type is mpp: instance, instance_mapper, corresponding_state, \ corresponding_dict = iterator.popleft() @@ -1843,44 +2425,50 @@ class Mapper(object): @_memoized_configured_property def _sorted_tables(self): table_to_mapper = {} + for mapper in self.base_mapper.self_and_descendants: for t in mapper.tables: - table_to_mapper[t] = mapper + table_to_mapper.setdefault(t, mapper) + + extra_dependencies = [] + for table, mapper in table_to_mapper.items(): + super_ = mapper.inherits + if super_: + extra_dependencies.extend([ + (super_table, table) + for super_table in super_.tables + ]) + + def skip(fk): + # attempt to skip dependencies that are not + # significant to the inheritance chain + # for two tables that are related by inheritance. + # while that dependency may be important, it's techinically + # not what we mean to sort on here. + parent = table_to_mapper.get(fk.parent.table) + dep = table_to_mapper.get(fk.column.table) + if parent is not None and \ + dep is not None and \ + dep is not parent and \ + dep.inherit_condition is not None: + cols = set(sql_util._find_columns(dep.inherit_condition)) + if parent.inherit_condition is not None: + cols = cols.union(sql_util._find_columns( + parent.inherit_condition)) + return fk.parent not in cols and fk.column not in cols + else: + return fk.parent not in cols + return False + + sorted_ = sql_util.sort_tables(table_to_mapper, + skip_fn=skip, + extra_dependencies=extra_dependencies) - sorted_ = sqlutil.sort_tables(table_to_mapper.iterkeys()) ret = util.OrderedDict() for t in sorted_: ret[t] = table_to_mapper[t] return ret - def _per_mapper_flush_actions(self, uow): - saves = unitofwork.SaveUpdateAll(uow, self.base_mapper) - deletes = unitofwork.DeleteAll(uow, self.base_mapper) - uow.dependencies.add((saves, deletes)) - - for dep in self._dependency_processors: - dep.per_property_preprocessors(uow) - - for prop in self._props.values(): - prop.per_property_preprocessors(uow) - - def _per_state_flush_actions(self, uow, states, isdelete): - - base_mapper = self.base_mapper - save_all = unitofwork.SaveUpdateAll(uow, base_mapper) - delete_all = unitofwork.DeleteAll(uow, base_mapper) - for state in states: - # keep saves before deletes - - # this ensures 'row switch' operations work - if isdelete: - action = unitofwork.DeleteState(uow, state, base_mapper) - uow.dependencies.add((save_all, action)) - else: - action = unitofwork.SaveUpdateState(uow, state, base_mapper) - uow.dependencies.add((action, delete_all)) - - yield action - def _memo(self, key, callable_): if key in self._memoized_values: return self._memoized_values[key] @@ -1900,325 +2488,14 @@ class Mapper(object): for m in self.iterate_to_root(): if m._inherits_equated_pairs and \ cols.intersection( - [l for l, r in m._inherits_equated_pairs]): + util.reduce(set.union, + [l.proxy_set for l, r in m._inherits_equated_pairs]) + ): result[table].append((m, m._inherits_equated_pairs)) return result - def _instance_processor(self, context, path, reduced_path, adapter, - polymorphic_from=None, - only_load_props=None, refresh_state=None, - polymorphic_discriminator=None): - - """Produce a mapper level row processor callable - which processes rows into mapped instances.""" - - # note that this method, most of which exists in a closure - # called _instance(), resists being broken out, as - # attempts to do so tend to add significant function - # call overhead. _instance() is the most - # performance-critical section in the whole ORM. - - pk_cols = self.primary_key - - if polymorphic_from or refresh_state: - polymorphic_on = None - else: - if polymorphic_discriminator is not None: - polymorphic_on = polymorphic_discriminator - else: - polymorphic_on = self.polymorphic_on - polymorphic_instances = util.PopulateDict( - self._configure_subclass_mapper( - context, path, reduced_path, adapter) - ) - - version_id_col = self.version_id_col - - if adapter: - pk_cols = [adapter.columns[c] for c in pk_cols] - if polymorphic_on is not None: - polymorphic_on = adapter.columns[polymorphic_on] - if version_id_col is not None: - version_id_col = adapter.columns[version_id_col] - - identity_class = self._identity_class - - new_populators = [] - existing_populators = [] - eager_populators = [] - load_path = context.query._current_path + path - - def populate_state(state, dict_, row, isnew, only_load_props): - if isnew: - if context.propagate_options: - state.load_options = context.propagate_options - if state.load_options: - state.load_path = load_path - - if not new_populators: - self._populators(context, path, reduced_path, row, adapter, - new_populators, - existing_populators, - eager_populators - ) - - if isnew: - populators = new_populators - else: - populators = existing_populators - - if only_load_props is None: - for key, populator in populators: - populator(state, dict_, row) - elif only_load_props: - for key, populator in populators: - if key in only_load_props: - populator(state, dict_, row) - - session_identity_map = context.session.identity_map - - listeners = self.dispatch - - translate_row = listeners.translate_row or None - create_instance = listeners.create_instance or None - populate_instance = listeners.populate_instance or None - append_result = listeners.append_result or None - populate_existing = context.populate_existing or self.always_refresh - invoke_all_eagers = context.invoke_all_eagers - - if self.allow_partial_pks: - is_not_primary_key = _none_set.issuperset - else: - is_not_primary_key = _none_set.issubset - - def _instance(row, result): - if not new_populators and invoke_all_eagers: - self._populators(context, path, reduced_path, row, adapter, - new_populators, - existing_populators, - eager_populators - ) - - if translate_row: - for fn in translate_row: - ret = fn(self, context, row) - if ret is not EXT_CONTINUE: - row = ret - break - - if polymorphic_on is not None: - discriminator = row[polymorphic_on] - if discriminator is not None: - _instance = polymorphic_instances[discriminator] - if _instance: - return _instance(row, result) - - # determine identity key - if refresh_state: - identitykey = refresh_state.key - if identitykey is None: - # super-rare condition; a refresh is being called - # on a non-instance-key instance; this is meant to only - # occur within a flush() - identitykey = self._identity_key_from_state(refresh_state) - else: - identitykey = ( - identity_class, - tuple([row[column] for column in pk_cols]) - ) - - instance = session_identity_map.get(identitykey) - if instance is not None: - state = attributes.instance_state(instance) - dict_ = attributes.instance_dict(instance) - - isnew = state.runid != context.runid - currentload = not isnew - loaded_instance = False - - if not currentload and \ - version_id_col is not None and \ - context.version_check and \ - self._get_state_attr_by_column( - state, - dict_, - self.version_id_col) != \ - row[version_id_col]: - - raise orm_exc.StaleDataError( - "Instance '%s' has version id '%s' which " - "does not match database-loaded version id '%s'." - % (state_str(state), - self._get_state_attr_by_column( - state, dict_, - self.version_id_col), - row[version_id_col])) - elif refresh_state: - # out of band refresh_state detected (i.e. its not in the - # session.identity_map) honor it anyway. this can happen - # if a _get() occurs within save_obj(), such as - # when eager_defaults is True. - state = refresh_state - instance = state.obj() - dict_ = attributes.instance_dict(instance) - isnew = state.runid != context.runid - currentload = True - loaded_instance = False - else: - # check for non-NULL values in the primary key columns, - # else no entity is returned for the row - if is_not_primary_key(identitykey[1]): - return None - - isnew = True - currentload = True - loaded_instance = True - - if create_instance: - for fn in create_instance: - instance = fn(self, context, - row, self.class_) - if instance is not EXT_CONTINUE: - manager = attributes.manager_of_class( - instance.__class__) - # TODO: if manager is None, raise a friendly error - # about returning instances of unmapped types - manager.setup_instance(instance) - break - else: - instance = self.class_manager.new_instance() - else: - instance = self.class_manager.new_instance() - - dict_ = attributes.instance_dict(instance) - state = attributes.instance_state(instance) - state.key = identitykey - - # attach instance to session. - state.session_id = context.session.hash_key - session_identity_map.add(state) - - if currentload or populate_existing: - # state is being fully loaded, so populate. - # add to the "context.progress" collection. - if isnew: - state.runid = context.runid - context.progress[state] = dict_ - - if populate_instance: - for fn in populate_instance: - ret = fn(self, context, row, state, - only_load_props=only_load_props, - instancekey=identitykey, isnew=isnew) - if ret is not EXT_CONTINUE: - break - else: - populate_state(state, dict_, row, isnew, only_load_props) - else: - populate_state(state, dict_, row, isnew, only_load_props) - - if loaded_instance: - state.manager.dispatch.load(state, context) - elif isnew: - state.manager.dispatch.refresh(state, context, only_load_props) - - elif state in context.partials or state.unloaded or eager_populators: - # state is having a partial set of its attributes - # refreshed. Populate those attributes, - # and add to the "context.partials" collection. - if state in context.partials: - isnew = False - (d_, attrs) = context.partials[state] - else: - isnew = True - attrs = state.unloaded - context.partials[state] = (dict_, attrs) - - if populate_instance: - for fn in populate_instance: - ret = fn(self, context, row, state, - only_load_props=attrs, - instancekey=identitykey, isnew=isnew) - if ret is not EXT_CONTINUE: - break - else: - populate_state(state, dict_, row, isnew, attrs) - else: - populate_state(state, dict_, row, isnew, attrs) - - for key, pop in eager_populators: - if key not in state.unloaded: - pop(state, dict_, row) - - if isnew: - state.manager.dispatch.refresh(state, context, attrs) - - - if result is not None: - if append_result: - for fn in append_result: - if fn(self, context, row, state, - result, instancekey=identitykey, - isnew=isnew) is not EXT_CONTINUE: - break - else: - result.append(instance) - else: - result.append(instance) - - return instance - return _instance - - def _populators(self, context, path, reduced_path, row, adapter, - new_populators, existing_populators, eager_populators): - """Produce a collection of attribute level row processor callables.""" - - delayed_populators = [] - pops = (new_populators, existing_populators, delayed_populators, eager_populators) - for prop in self._props.itervalues(): - for i, pop in enumerate(prop.create_row_processor( - context, path, - reduced_path, - self, row, adapter)): - if pop is not None: - pops[i].append((prop.key, pop)) - - if delayed_populators: - new_populators.extend(delayed_populators) - - def _configure_subclass_mapper(self, context, path, reduced_path, adapter): - """Produce a mapper level row processor callable factory for mappers - inheriting this one.""" - - def configure_subclass_mapper(discriminator): - try: - mapper = self.polymorphic_map[discriminator] - except KeyError: - raise AssertionError( - "No such polymorphic_identity %r is defined" % - discriminator) - if mapper is self: - return None - - # replace the tip of the path info with the subclass mapper - # being used. that way accurate "load_path" info is available - # for options invoked during deferred loads. - # we lose AliasedClass path elements this way, but currently, - # those are not needed at this stage. - - # this asserts to true - #assert mapper.isa(_class_to_mapper(path[-1])) - - return mapper._instance_processor(context, path[0:-1] + (mapper,), - reduced_path[0:-1] + (mapper.base_mapper,), - adapter, - polymorphic_from=self) - return configure_subclass_mapper - -log.class_logger(Mapper) - def configure_mappers(): """Initialize the inter-mapper relationships of all mappers that have been constructed thus far. @@ -2228,12 +2505,11 @@ def configure_mappers(): """ - global _new_mappers - if not _new_mappers: + if not Mapper._new_mappers: return _call_configured = None - _COMPILE_MUTEX.acquire() + _CONFIGURE_MUTEX.acquire() try: global _already_compiling if _already_compiling: @@ -2242,7 +2518,7 @@ def configure_mappers(): try: # double-check inside mutex - if not _new_mappers: + if not Mapper._new_mappers: return # initialize properties on all mappers @@ -2262,7 +2538,8 @@ def configure_mappers(): try: mapper._post_configure_properties() mapper._expire_memoizations() - mapper.dispatch.mapper_configured(mapper, mapper.class_) + mapper.dispatch.mapper_configured( + mapper, mapper.class_) _call_configured = mapper except: exc = sys.exc_info()[1] @@ -2270,14 +2547,15 @@ def configure_mappers(): mapper._configure_failed = exc raise - _new_mappers = False + Mapper._new_mappers = False finally: _already_compiling = False finally: - _COMPILE_MUTEX.release() + _CONFIGURE_MUTEX.release() if _call_configured is not None: _call_configured.dispatch.after_configured() + def reconstructor(fn): """Decorate a method as the 'reconstructor' hook. @@ -2297,16 +2575,18 @@ def reconstructor(fn): fn.__sa_reconstructor__ = True return fn + def validates(*names, **kw): """Decorate a method as a 'validator' for one or more named properties. Designates a method as a validator, a method which receives the name of the attribute as well as a value to be assigned, or in the - case of a collection, the value to be added to the collection. The function - can then raise validation exceptions to halt the process from continuing - (where Python's built-in ``ValueError`` and ``AssertionError`` exceptions are - reasonable choices), or can modify or replace the value before proceeding. - The function should otherwise return the given value. + case of a collection, the value to be added to the collection. + The function can then raise validation exceptions to halt the + process from continuing (where Python's built-in ``ValueError`` + and ``AssertionError`` exceptions are reasonable choices), or can + modify or replace the value before proceeding. The function should + otherwise return the given value. Note that a validator for a collection **cannot** issue a load of that collection within the validation routine - this usage raises @@ -2319,20 +2599,38 @@ def validates(*names, **kw): argument "is_remove" which will be a boolean. .. versionadded:: 0.7.7 + :param include_backrefs: defaults to ``True``; if ``False``, the + validation function will not emit if the originator is an attribute + event related via a backref. This can be used for bi-directional + :func:`.validates` usage where only one validator should emit per + attribute operation. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :ref:`simple_validators` - usage examples for :func:`.validates` """ include_removes = kw.pop('include_removes', False) + include_backrefs = kw.pop('include_backrefs', True) + def wrap(fn): fn.__sa_validators__ = names - fn.__sa_include_removes__ = include_removes + fn.__sa_validation_opts__ = { + "include_removes": include_removes, + "include_backrefs": include_backrefs + } return fn return wrap + def _event_on_load(state, ctx): instrumenting_mapper = state.manager.info[_INSTRUMENTOR] if instrumenting_mapper._reconstructor: instrumenting_mapper._reconstructor(state.obj()) + def _event_on_first_init(manager, cls): """Initial mapper compilation trigger. @@ -2343,9 +2641,10 @@ def _event_on_first_init(manager, cls): instrumenting_mapper = manager.info.get(_INSTRUMENTOR) if instrumenting_mapper: - if _new_mappers: + if Mapper._new_mappers: configure_mappers() + def _event_on_init(state, args, kwargs): """Run init_instance hooks. @@ -2357,11 +2656,12 @@ def _event_on_init(state, args, kwargs): instrumenting_mapper = state.manager.info.get(_INSTRUMENTOR) if instrumenting_mapper: - if _new_mappers: + if Mapper._new_mappers: configure_mappers() if instrumenting_mapper._set_polymorphic_identity: instrumenting_mapper._set_polymorphic_identity(state) + def _event_on_resurrect(state): # re-populate the primary key elements # of the dict based on the mapping. @@ -2372,7 +2672,7 @@ def _event_on_resurrect(state): state, state.dict, col, val) -class _ColumnMapping(util.py25_dict): +class _ColumnMapping(dict): """Error reporting helper for mapper._columntoproperty.""" def __init__(self, mapper): diff --git a/libs/sqlalchemy/orm/path_registry.py b/libs/sqlalchemy/orm/path_registry.py new file mode 100644 index 00000000..3397626b --- /dev/null +++ b/libs/sqlalchemy/orm/path_registry.py @@ -0,0 +1,261 @@ +# orm/path_registry.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Path tracking utilities, representing mapper graph traversals. + +""" + +from .. import inspection +from .. import util +from .. import exc +from itertools import chain +from .base import class_mapper + +def _unreduce_path(path): + return PathRegistry.deserialize(path) + + +_WILDCARD_TOKEN = "*" +_DEFAULT_TOKEN = "_sa_default" + +class PathRegistry(object): + """Represent query load paths and registry functions. + + Basically represents structures like: + + (, "orders", , "items", ) + + These structures are generated by things like + query options (joinedload(), subqueryload(), etc.) and are + used to compose keys stored in the query._attributes dictionary + for various options. + + They are then re-composed at query compile/result row time as + the query is formed and as rows are fetched, where they again + serve to compose keys to look up options in the context.attributes + dictionary, which is copied from query._attributes. + + The path structure has a limited amount of caching, where each + "root" ultimately pulls from a fixed registry associated with + the first mapper, that also contains elements for each of its + property keys. However paths longer than two elements, which + are the exception rather than the rule, are generated on an + as-needed basis. + + """ + + def __eq__(self, other): + return other is not None and \ + self.path == other.path + + def set(self, attributes, key, value): + attributes[(key, self.path)] = value + + def setdefault(self, attributes, key, value): + attributes.setdefault((key, self.path), value) + + def get(self, attributes, key, value=None): + key = (key, self.path) + if key in attributes: + return attributes[key] + else: + return value + + def __len__(self): + return len(self.path) + + @property + def length(self): + return len(self.path) + + def pairs(self): + path = self.path + for i in range(0, len(path), 2): + yield path[i], path[i + 1] + + def contains_mapper(self, mapper): + for path_mapper in [ + self.path[i] for i in range(0, len(self.path), 2) + ]: + if path_mapper.is_mapper and \ + path_mapper.isa(mapper): + return True + else: + return False + + def contains(self, attributes, key): + return (key, self.path) in attributes + + def __reduce__(self): + return _unreduce_path, (self.serialize(), ) + + def serialize(self): + path = self.path + return list(zip( + [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], + [path[i].key for i in range(1, len(path), 2)] + [None] + )) + + @classmethod + def deserialize(cls, path): + if path is None: + return None + + p = tuple(chain(*[(class_mapper(mcls), + class_mapper(mcls).attrs[key] + if key is not None else None) + for mcls, key in path])) + if p and p[-1] is None: + p = p[0:-1] + return cls.coerce(p) + + @classmethod + def per_mapper(cls, mapper): + return EntityRegistry( + cls.root, mapper + ) + + @classmethod + def coerce(cls, raw): + return util.reduce(lambda prev, next: prev[next], raw, cls.root) + + def token(self, token): + if token.endswith(':' + _WILDCARD_TOKEN): + return TokenRegistry(self, token) + elif token.endswith(":" + _DEFAULT_TOKEN): + return TokenRegistry(self.root, token) + else: + raise exc.ArgumentError("invalid token: %s" % token) + + def __add__(self, other): + return util.reduce( + lambda prev, next: prev[next], + other.path, self) + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.path, ) + + +class RootRegistry(PathRegistry): + """Root registry, defers to mappers so that + paths are maintained per-root-mapper. + + """ + path = () + has_entity = False + def __getitem__(self, entity): + return entity._path_registry + +PathRegistry.root = RootRegistry() + +class TokenRegistry(PathRegistry): + def __init__(self, parent, token): + self.token = token + self.parent = parent + self.path = parent.path + (token,) + + has_entity = False + + def __getitem__(self, entity): + raise NotImplementedError() + +class PropRegistry(PathRegistry): + def __init__(self, parent, prop): + # restate this path in terms of the + # given MapperProperty's parent. + insp = inspection.inspect(parent[-1]) + if not insp.is_aliased_class or insp._use_mapper_path: + parent = parent.parent[prop.parent] + elif insp.is_aliased_class and insp.with_polymorphic_mappers: + if prop.parent is not insp.mapper and \ + prop.parent in insp.with_polymorphic_mappers: + subclass_entity = parent[-1]._entity_for_mapper(prop.parent) + parent = parent.parent[subclass_entity] + + self.prop = prop + self.parent = parent + self.path = parent.path + (prop,) + + @util.memoized_property + def has_entity(self): + return hasattr(self.prop, "mapper") + + @util.memoized_property + def entity(self): + return self.prop.mapper + + @util.memoized_property + def _wildcard_path_loader_key(self): + """Given a path (mapper A, prop X), replace the prop with the wildcard, + e.g. (mapper A, 'relationship:.*') or (mapper A, 'column:.*'), then + return within the ("loader", path) structure. + + """ + return ("loader", + self.parent.token( + "%s:%s" % (self.prop.strategy_wildcard_key, _WILDCARD_TOKEN) + ).path + ) + + @util.memoized_property + def _default_path_loader_key(self): + return ("loader", + self.parent.token( + "%s:%s" % (self.prop.strategy_wildcard_key, _DEFAULT_TOKEN) + ).path + ) + + @util.memoized_property + def _loader_key(self): + return ("loader", self.path) + + @property + def mapper(self): + return self.entity + + @property + def entity_path(self): + return self[self.entity] + + def __getitem__(self, entity): + if isinstance(entity, (int, slice)): + return self.path[entity] + else: + return EntityRegistry( + self, entity + ) + +class EntityRegistry(PathRegistry, dict): + is_aliased_class = False + has_entity = True + + def __init__(self, parent, entity): + self.key = entity + self.parent = parent + self.is_aliased_class = entity.is_aliased_class + self.entity = entity + self.path = parent.path + (entity,) + self.entity_path = self + + @property + def mapper(self): + return inspection.inspect(self.entity).mapper + + def __bool__(self): + return True + __nonzero__ = __bool__ + + def __getitem__(self, entity): + if isinstance(entity, (int, slice)): + return self.path[entity] + else: + return dict.__getitem__(self, entity) + + def __missing__(self, key): + self[key] = item = PropRegistry(self, key) + return item + + + diff --git a/libs/sqlalchemy/orm/persistence.py b/libs/sqlalchemy/orm/persistence.py index 5be57cce..35631988 100644 --- a/libs/sqlalchemy/orm/persistence.py +++ b/libs/sqlalchemy/orm/persistence.py @@ -1,5 +1,5 @@ # orm/persistence.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -15,12 +15,12 @@ in unitofwork.py. import operator from itertools import groupby +from .. import sql, util, exc as sa_exc, schema +from . import attributes, sync, exc as orm_exc, evaluator +from .base import _state_mapper, state_str, _attr_as_key +from ..sql import expression +from . import loading -from sqlalchemy import sql, util, exc as sa_exc -from sqlalchemy.orm import attributes, sync, \ - exc as orm_exc - -from sqlalchemy.orm.util import _state_mapper, state_str def save_obj(base_mapper, states, uowtransaction, single=False): """Issue ``INSERT`` and/or ``UPDATE`` statements for a list @@ -46,7 +46,7 @@ def save_obj(base_mapper, states, uowtransaction, single=False): cached_connections = _cached_connection_dict(base_mapper) - for table, mapper in base_mapper._sorted_tables.iteritems(): + for table, mapper in base_mapper._sorted_tables.items(): insert = _collect_insert_commands(base_mapper, uowtransaction, table, states_to_insert) @@ -61,11 +61,12 @@ def save_obj(base_mapper, states, uowtransaction, single=False): if insert: _emit_insert_statements(base_mapper, uowtransaction, cached_connections, - table, insert) + mapper, table, insert) _finalize_insert_update_commands(base_mapper, uowtransaction, states_to_insert, states_to_update) + def post_update(base_mapper, states, uowtransaction, post_update_cols): """Issue UPDATE statements on behalf of a relationship() which specifies post_update. @@ -77,8 +78,7 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols): base_mapper, states, uowtransaction) - - for table, mapper in base_mapper._sorted_tables.iteritems(): + for table, mapper in base_mapper._sorted_tables.items(): update = _collect_post_update_commands(base_mapper, uowtransaction, table, states_to_update, post_update_cols) @@ -88,6 +88,7 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols): cached_connections, mapper, table, update) + def delete_obj(base_mapper, states, uowtransaction): """Issue ``DELETE`` statements for a list of objects. @@ -105,7 +106,7 @@ def delete_obj(base_mapper, states, uowtransaction): table_to_mapper = base_mapper._sorted_tables - for table in reversed(table_to_mapper.keys()): + for table in reversed(list(table_to_mapper.keys())): delete = _collect_delete_commands(base_mapper, uowtransaction, table, states_to_delete) @@ -118,6 +119,7 @@ def delete_obj(base_mapper, states, uowtransaction): in states_to_delete: mapper.dispatch.after_delete(mapper, connection, state) + def _organize_states_for_save(base_mapper, states, uowtransaction): """Make an initial pass across a set of states for INSERT or UPDATE. @@ -148,12 +150,15 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): else: mapper.dispatch.before_update(mapper, connection, state) + if mapper._validate_polymorphic_identity: + mapper._validate_polymorphic_identity(mapper, state, dict_) + # detect if we have a "pending" instance (i.e. has # no instance_key attached to it), and another instance # with the same identity key already exists as persistent. # convert to an UPDATE if so. if not has_identity and \ - instance_key in uowtransaction.session.identity_map: + instance_key in uowtransaction.session.identity_map: instance = \ uowtransaction.session.identity_map[instance_key] existing = attributes.instance_state(instance) @@ -187,6 +192,7 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): return states_to_insert, states_to_update + def _organize_states_for_post_update(base_mapper, states, uowtransaction): """Make an initial pass across a set of states for UPDATE @@ -200,6 +206,7 @@ def _organize_states_for_post_update(base_mapper, states, return list(_connections_for_states(base_mapper, uowtransaction, states)) + def _organize_states_for_delete(base_mapper, states, uowtransaction): """Make an initial pass across a set of states for DELETE. @@ -220,6 +227,7 @@ def _organize_states_for_delete(base_mapper, states, uowtransaction): bool(state.key), connection)) return states_to_delete + def _collect_insert_commands(base_mapper, uowtransaction, table, states_to_insert): """Identify sets of values to use in INSERT statements for a @@ -238,9 +246,12 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, value_params = {} has_all_pks = True + has_all_defaults = True for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col: - params[col.key] = mapper.version_id_generator(None) + if col is mapper.version_id_col and \ + mapper.version_id_generator is not False: + val = mapper.version_id_generator(None) + params[col.key] = val else: # pull straight from the dict for # pending objects @@ -253,6 +264,9 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, elif col.default is None and \ col.server_default is None: params[col.key] = value + elif col.server_default is not None and \ + mapper.base_mapper.eager_defaults: + has_all_defaults = False elif isinstance(value, sql.ClauseElement): value_params[col] = value @@ -260,9 +274,11 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, params[col.key] = value insert.append((state, state_dict, params, mapper, - connection, value_params, has_all_pks)) + connection, value_params, has_all_pks, + has_all_defaults)) return insert + def _collect_update_commands(base_mapper, uowtransaction, table, states_to_update): """Identify sets of values to use in UPDATE statements for a @@ -306,19 +322,20 @@ def _collect_update_commands(base_mapper, uowtransaction, params[col.key] = history.added[0] hasdata = True else: - params[col.key] = mapper.version_id_generator( - params[col._label]) + if mapper.version_id_generator is not False: + val = mapper.version_id_generator(params[col._label]) + params[col.key] = val - # HACK: check for history, in case the - # history is only - # in a different table than the one - # where the version_id_col is. - for prop in mapper._columntoproperty.itervalues(): - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE) - if history.added: - hasdata = True + # HACK: check for history, in case the + # history is only + # in a different table than the one + # where the version_id_col is. + for prop in mapper._columntoproperty.values(): + history = attributes.get_state_history( + state, prop.key, + attributes.PASSIVE_NO_INITIALIZE) + if history.added: + hasdata = True else: prop = mapper._columntoproperty[col] history = attributes.get_state_history( @@ -370,7 +387,7 @@ def _collect_update_commands(base_mapper, uowtransaction, params[col._label] = value if hasdata: if hasnull: - raise sa_exc.FlushError( + raise orm_exc.FlushError( "Can't update table " "using NULL for primary " "key value") @@ -400,6 +417,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, mapper._get_state_attr_by_column( state, state_dict, col) + elif col in post_update_cols: prop = mapper._columntoproperty[col] history = attributes.get_state_history( @@ -414,6 +432,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, connection)) return update + def _collect_delete_commands(base_mapper, uowtransaction, table, states_to_delete): """Identify values to use in DELETE statements for a list of @@ -434,7 +453,7 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, mapper._get_state_attr_by_column( state, state_dict, col) if value is None: - raise sa_exc.FlushError( + raise orm_exc.FlushError( "Can't delete from table " "using NULL for primary " "key value") @@ -468,7 +487,13 @@ def _emit_update_statements(base_mapper, uowtransaction, sql.bindparam(mapper.version_id_col._label, type_=mapper.version_id_col.type)) - return table.update(clause) + stmt = table.update(clause) + if mapper.base_mapper.eager_defaults: + stmt = stmt.return_defaults() + elif mapper.version_id_col is not None: + stmt = stmt.return_defaults(mapper.version_id_col) + + return stmt statement = base_mapper._memo(('update', table), update_stmt) @@ -490,8 +515,7 @@ def _emit_update_statements(base_mapper, uowtransaction, table, state, state_dict, - c.context.prefetch_cols, - c.context.postfetch_cols, + c, c.context.compiled_parameters[0], value_params) rows += c.rowcount @@ -509,45 +533,57 @@ def _emit_update_statements(base_mapper, uowtransaction, c.dialect.dialect_description, stacklevel=12) + def _emit_insert_statements(base_mapper, uowtransaction, - cached_connections, table, insert): + cached_connections, mapper, table, insert): """Emit INSERT statements corresponding to value lists collected by _collect_insert_commands().""" statement = base_mapper._memo(('insert', table), table.insert) - for (connection, pkeys, hasvalue, has_all_pks), \ + for (connection, pkeys, hasvalue, has_all_pks, has_all_defaults), \ records in groupby(insert, lambda rec: (rec[4], - rec[2].keys(), + list(rec[2].keys()), bool(rec[5]), - rec[6]) + rec[6], rec[7]) ): - if has_all_pks and not hasvalue: + if \ + ( + has_all_defaults + or not base_mapper.eager_defaults + or not connection.dialect.implicit_returning + ) and has_all_pks and not hasvalue: + records = list(records) multiparams = [rec[2] for rec in records] + c = cached_connections[connection].\ execute(statement, multiparams) - for (state, state_dict, params, mapper, - conn, value_params, has_all_pks), \ + for (state, state_dict, params, mapper_rec, + conn, value_params, has_all_pks, has_all_defaults), \ last_inserted_params in \ zip(records, c.context.compiled_parameters): _postfetch( - mapper, + mapper_rec, uowtransaction, table, state, state_dict, - c.context.prefetch_cols, - c.context.postfetch_cols, + c, last_inserted_params, value_params) else: - for state, state_dict, params, mapper, \ + if not has_all_defaults and base_mapper.eager_defaults: + statement = statement.return_defaults() + elif mapper.version_id_col is not None: + statement = statement.return_defaults(mapper.version_id_col) + + for state, state_dict, params, mapper_rec, \ connection, value_params, \ - has_all_pks in records: + has_all_pks, has_all_defaults in records: if value_params: result = connection.execute( @@ -563,28 +599,26 @@ def _emit_insert_statements(base_mapper, uowtransaction, # set primary key attributes for pk, col in zip(primary_key, mapper._pks_by_table[table]): - prop = mapper._columntoproperty[col] + prop = mapper_rec._columntoproperty[col] if state_dict.get(prop.key) is None: # TODO: would rather say: #state_dict[prop.key] = pk - mapper._set_state_attr_by_column( + mapper_rec._set_state_attr_by_column( state, state_dict, col, pk) _postfetch( - mapper, + mapper_rec, uowtransaction, table, state, state_dict, - result.context.prefetch_cols, - result.context.postfetch_cols, + result, result.context.compiled_parameters[0], value_params) - def _emit_post_update_statements(base_mapper, uowtransaction, cached_connections, mapper, table, update): """Emit UPDATE statements corresponding to value lists collected @@ -606,7 +640,7 @@ def _emit_post_update_statements(base_mapper, uowtransaction, # also group them into common (connection, cols) sets # to support executemany(). for key, grouper in groupby( - update, lambda rec: (rec[4], rec[2].keys()) + update, lambda rec: (rec[4], list(rec[2].keys())) ): connection = key[0] multiparams = [params for state, state_dict, @@ -640,7 +674,7 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections, return table.delete(clause) - for connection, del_objects in delete.iteritems(): + for connection, del_objects in delete.items(): statement = base_mapper._memo(('delete', table), delete_stmt) connection = cached_connections[connection] @@ -687,15 +721,27 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction, if p.expire_on_flush or p.key not in state.dict] ) if readonly: - state.expire_attributes(state.dict, readonly) + state._expire_attributes(state.dict, readonly) - # if eager_defaults option is enabled, - # refresh whatever has been expired. - if base_mapper.eager_defaults and state.unloaded: + # if eager_defaults option is enabled, load + # all expired cols. Else if we have a version_id_col, make sure + # it isn't expired. + toload_now = [] + + if base_mapper.eager_defaults: + toload_now.extend(state._unloaded_non_object) + elif mapper.version_id_col is not None and \ + mapper.version_id_generator is False: + prop = mapper._columntoproperty[mapper.version_id_col] + if prop.key in state.unloaded: + toload_now.extend([prop.key]) + + if toload_now: state.key = base_mapper._identity_key_from_state(state) - uowtransaction.session.query(base_mapper)._load_on_ident( + loading.load_on_ident( + uowtransaction.session.query(base_mapper), state.key, refresh_state=state, - only_load_props=state.unloaded) + only_load_props=toload_now) # call after_XXX extensions if not has_identity: @@ -703,22 +749,34 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction, else: mapper.dispatch.after_update(mapper, connection, state) + def _postfetch(mapper, uowtransaction, table, - state, dict_, prefetch_cols, postfetch_cols, - params, value_params): + state, dict_, result, params, value_params): """Expire attributes in need of newly persisted database state, after an INSERT or UPDATE statement has proceeded for that state.""" + prefetch_cols = result.context.prefetch_cols + postfetch_cols = result.context.postfetch_cols + returning_cols = result.context.returning_cols + if mapper.version_id_col is not None: prefetch_cols = list(prefetch_cols) + [mapper.version_id_col] + if returning_cols: + row = result.context.returned_defaults + if row is not None: + for col in returning_cols: + if col.primary_key: + continue + mapper._set_state_attr_by_column(state, dict_, col, row[col]) + for c in prefetch_cols: if c.key in params and c in mapper._columntoproperty: mapper._set_state_attr_by_column(state, dict_, c, params[c.key]) if postfetch_cols: - state.expire_attributes(state.dict, + state._expire_attributes(state.dict, [mapper._columntoproperty[c].key for c in postfetch_cols if c in mapper._columntoproperty] @@ -733,6 +791,7 @@ def _postfetch(mapper, uowtransaction, table, uowtransaction, mapper.passive_updates) + def _connections_for_states(base_mapper, uowtransaction, states): """Return an iterator of (state, state.dict, mapper, connection). @@ -762,18 +821,265 @@ def _connections_for_states(base_mapper, uowtransaction, states): yield state, state.dict, mapper, connection + def _cached_connection_dict(base_mapper): # dictionary of connection->connection_with_cache_options. return util.PopulateDict( - lambda conn:conn.execution_options( + lambda conn: conn.execution_options( compiled_cache=base_mapper._compiled_cache )) + def _sort_states(states): pending = set(states) persistent = set(s for s in pending if s.key is not None) pending.difference_update(persistent) return sorted(pending, key=operator.attrgetter("insert_order")) + \ - sorted(persistent, key=lambda q:q.key[1]) + sorted(persistent, key=lambda q: q.key[1]) +class BulkUD(object): + """Handle bulk update and deletes via a :class:`.Query`.""" + + def __init__(self, query): + self.query = query.enable_eagerloads(False) + + @property + def session(self): + return self.query.session + + @classmethod + def _factory(cls, lookup, synchronize_session, *arg): + try: + klass = lookup[synchronize_session] + except KeyError: + raise sa_exc.ArgumentError( + "Valid strategies for session synchronization " + "are %s" % (", ".join(sorted(repr(x) + for x in lookup)))) + else: + return klass(*arg) + + def exec_(self): + self._do_pre() + self._do_pre_synchronize() + self._do_exec() + self._do_post_synchronize() + self._do_post() + + def _do_pre(self): + query = self.query + self.context = context = query._compile_context() + if len(context.statement.froms) != 1 or \ + not isinstance(context.statement.froms[0], schema.Table): + + self.primary_table = query._only_entity_zero( + "This operation requires only one Table or " + "entity be specified as the target." + ).mapper.local_table + else: + self.primary_table = context.statement.froms[0] + + session = query.session + + if query._autoflush: + session._autoflush() + + def _do_pre_synchronize(self): + pass + + def _do_post_synchronize(self): + pass + + +class BulkEvaluate(BulkUD): + """BulkUD which does the 'evaluate' method of session state resolution.""" + + def _additional_evaluators(self, evaluator_compiler): + pass + + def _do_pre_synchronize(self): + query = self.query + try: + evaluator_compiler = evaluator.EvaluatorCompiler() + if query.whereclause is not None: + eval_condition = evaluator_compiler.process( + query.whereclause) + else: + def eval_condition(obj): + return True + + self._additional_evaluators(evaluator_compiler) + + except evaluator.UnevaluatableError: + raise sa_exc.InvalidRequestError( + "Could not evaluate current criteria in Python. " + "Specify 'fetch' or False for the " + "synchronize_session parameter.") + target_cls = query._mapper_zero().class_ + + #TODO: detect when the where clause is a trivial primary key match + self.matched_objects = [ + obj for (cls, pk), obj in + query.session.identity_map.items() + if issubclass(cls, target_cls) and + eval_condition(obj)] + + +class BulkFetch(BulkUD): + """BulkUD which does the 'fetch' method of session state resolution.""" + + def _do_pre_synchronize(self): + query = self.query + session = query.session + select_stmt = self.context.statement.with_only_columns( + self.primary_table.primary_key) + self.matched_rows = session.execute( + select_stmt, + params=query._params).fetchall() + + +class BulkUpdate(BulkUD): + """BulkUD which handles UPDATEs.""" + + def __init__(self, query, values): + super(BulkUpdate, self).__init__(query) + self.query._no_select_modifiers("update") + self.values = values + + @classmethod + def factory(cls, query, synchronize_session, values): + return BulkUD._factory({ + "evaluate": BulkUpdateEvaluate, + "fetch": BulkUpdateFetch, + False: BulkUpdate + }, synchronize_session, query, values) + + def _do_exec(self): + update_stmt = sql.update(self.primary_table, + self.context.whereclause, self.values) + + self.result = self.query.session.execute( + update_stmt, params=self.query._params) + self.rowcount = self.result.rowcount + + def _do_post(self): + session = self.query.session + session.dispatch.after_bulk_update(self) + + +class BulkDelete(BulkUD): + """BulkUD which handles DELETEs.""" + + def __init__(self, query): + super(BulkDelete, self).__init__(query) + self.query._no_select_modifiers("delete") + + @classmethod + def factory(cls, query, synchronize_session): + return BulkUD._factory({ + "evaluate": BulkDeleteEvaluate, + "fetch": BulkDeleteFetch, + False: BulkDelete + }, synchronize_session, query) + + def _do_exec(self): + delete_stmt = sql.delete(self.primary_table, + self.context.whereclause) + + self.result = self.query.session.execute(delete_stmt, + params=self.query._params) + self.rowcount = self.result.rowcount + + def _do_post(self): + session = self.query.session + session.dispatch.after_bulk_delete(self) + + +class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate): + """BulkUD which handles UPDATEs using the "evaluate" + method of session resolution.""" + + def _additional_evaluators(self, evaluator_compiler): + self.value_evaluators = {} + for key, value in self.values.items(): + key = _attr_as_key(key) + self.value_evaluators[key] = evaluator_compiler.process( + expression._literal_as_binds(value)) + + def _do_post_synchronize(self): + session = self.query.session + states = set() + evaluated_keys = list(self.value_evaluators.keys()) + for obj in self.matched_objects: + state, dict_ = attributes.instance_state(obj),\ + attributes.instance_dict(obj) + + # only evaluate unmodified attributes + to_evaluate = state.unmodified.intersection( + evaluated_keys) + for key in to_evaluate: + dict_[key] = self.value_evaluators[key](obj) + + state._commit(dict_, list(to_evaluate)) + + # expire attributes with pending changes + # (there was no autoflush, so they are overwritten) + state._expire_attributes(dict_, + set(evaluated_keys). + difference(to_evaluate)) + states.add(state) + session._register_altered(states) + + +class BulkDeleteEvaluate(BulkEvaluate, BulkDelete): + """BulkUD which handles DELETEs using the "evaluate" + method of session resolution.""" + + def _do_post_synchronize(self): + self.query.session._remove_newly_deleted( + [attributes.instance_state(obj) + for obj in self.matched_objects]) + + +class BulkUpdateFetch(BulkFetch, BulkUpdate): + """BulkUD which handles UPDATEs using the "fetch" + method of session resolution.""" + + def _do_post_synchronize(self): + session = self.query.session + target_mapper = self.query._mapper_zero() + + states = set([ + attributes.instance_state(session.identity_map[identity_key]) + for identity_key in [ + target_mapper.identity_key_from_primary_key( + list(primary_key)) + for primary_key in self.matched_rows + ] + if identity_key in session.identity_map + ]) + attrib = [_attr_as_key(k) for k in self.values] + for state in states: + session._expire_state(state, attrib) + session._register_altered(states) + + +class BulkDeleteFetch(BulkFetch, BulkDelete): + """BulkUD which handles DELETEs using the "fetch" + method of session resolution.""" + + def _do_post_synchronize(self): + session = self.query.session + target_mapper = self.query._mapper_zero() + for primary_key in self.matched_rows: + # TODO: inline this and call remove_newly_deleted + # once + identity_key = target_mapper.identity_key_from_primary_key( + list(primary_key)) + if identity_key in session.identity_map: + session._remove_newly_deleted( + [attributes.instance_state( + session.identity_map[identity_key] + )] + ) diff --git a/libs/sqlalchemy/orm/properties.py b/libs/sqlalchemy/orm/properties.py index 204232cf..a0def7d3 100644 --- a/libs/sqlalchemy/orm/properties.py +++ b/libs/sqlalchemy/orm/properties.py @@ -1,5 +1,5 @@ # orm/properties.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -10,27 +10,20 @@ This is a private module which defines the behavior of invidual ORM- mapped attributes. """ +from __future__ import absolute_import -from sqlalchemy import sql, util, log, exc as sa_exc -from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \ - join_condition, _shallow_annotate -from sqlalchemy.sql import operators, expression -from sqlalchemy.orm import attributes, dependency, mapper, \ - object_mapper, strategies, configure_mappers -from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, \ - _orm_annotate, _orm_deannotate +from .. import util, log +from ..sql import expression +from . import attributes +from .util import _orm_full_deannotate -from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, \ - MapperProperty, ONETOMANY, PropComparator, StrategizedProperty -mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") -NoneType = type(None) +from .interfaces import PropComparator, StrategizedProperty -__all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty', - 'ComparableProperty', 'RelationshipProperty', 'RelationProperty') +__all__ = ['ColumnProperty', 'CompositeProperty', 'SynonymProperty', + 'ComparableProperty', 'RelationshipProperty'] -from descriptor_props import CompositeProperty, SynonymProperty, \ - ComparableProperty,ConcreteInheritedProperty +@log.class_logger class ColumnProperty(StrategizedProperty): """Describes an object attribute that corresponds to a table column. @@ -38,31 +31,85 @@ class ColumnProperty(StrategizedProperty): """ + strategy_wildcard_key = 'column' + def __init__(self, *columns, **kwargs): - """Construct a ColumnProperty. + """Provide a column-level property for use with a Mapper. - Note the public constructor is the :func:`.orm.column_property` function. + Column-based properties can normally be applied to the mapper's + ``properties`` dictionary using the :class:`.Column` element directly. + Use this function when the given column is not directly present within the + mapper's selectable; examples include SQL expressions, functions, and + scalar SELECT queries. - :param \*columns: The list of `columns` describes a single - object property. If there are multiple tables joined - together for the mapper, this list represents the equivalent - column as it appears across each table. + Columns that aren't present in the mapper's selectable won't be persisted + by the mapper and are effectively "read-only" attributes. + + :param \*cols: + list of Column objects to be mapped. + + :param active_history=False: + When ``True``, indicates that the "previous" value for a + scalar attribute should be loaded when replaced, if not + already loaded. Normally, history tracking logic for + simple non-primary-key scalar values only needs to be + aware of the "new" value in order to perform a flush. This + flag is available for applications that make use of + :func:`.attributes.get_history` or :meth:`.Session.is_modified` + which also need to know + the "previous" value of the attribute. + + .. versionadded:: 0.6.6 + + :param comparator_factory: a class which extends + :class:`.ColumnProperty.Comparator` which provides custom SQL clause + generation for comparison operations. :param group: + a group name for this property when marked as deferred. :param deferred: + when True, the column property is "deferred", meaning that + it does not load immediately, and is instead loaded when the + attribute is first accessed on an instance. See also + :func:`~sqlalchemy.orm.deferred`. - :param comparator_factory: + :param doc: + optional string that will be applied as the doc on the + class-bound descriptor. - :param descriptor: + :param expire_on_flush=True: + Disable expiry on flush. A column_property() which refers + to a SQL expression (and not a single table-bound column) + is considered to be a "read only" property; populating it + has no effect on the state of data, and it can only return + database state. For this reason a column_property()'s value + is expired whenever the parent object is involved in a + flush, that is, has any kind of "dirty" state within a flush. + Setting this parameter to ``False`` will have the effect of + leaving any existing value present after the flush proceeds. + Note however that the :class:`.Session` with default expiration + settings still expires + all attributes after a :meth:`.Session.commit` call, however. - :param expire_on_flush: + .. versionadded:: 0.7.3 + + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 :param extension: + an + :class:`.AttributeExtension` + instance, or list of extensions, which will be prepended + to the list of attribute listeners for the resulting + descriptor placed on the class. + **Deprecated.** Please see :class:`.AttributeEvents`. """ self._orig_columns = [expression._labeled(c) for c in columns] - self.columns = [expression._labeled(_orm_deannotate(c)) + self.columns = [expression._labeled(_orm_full_deannotate(c)) for c in columns] self.group = kwargs.pop('group', None) self.deferred = kwargs.pop('deferred', False) @@ -74,6 +121,9 @@ class ColumnProperty(StrategizedProperty): self.active_history = kwargs.pop('active_history', False) self.expire_on_flush = kwargs.pop('expire_on_flush', True) + if 'info' in kwargs: + self.info = kwargs.pop('info') + if 'doc' in kwargs: self.doc = kwargs.pop('doc') else: @@ -92,12 +142,18 @@ class ColumnProperty(StrategizedProperty): ', '.join(sorted(kwargs.keys())))) util.set_creation_order(self) - if not self.instrument: - self.strategy_class = strategies.UninstrumentedColumnLoader - elif self.deferred: - self.strategy_class = strategies.DeferredColumnLoader - else: - self.strategy_class = strategies.ColumnLoader + + self.strategy_class = self._strategy_lookup( + ("deferred", self.deferred), + ("instrument", self.instrument) + ) + + @property + def expression(self): + """Return the primary column or expression for this ColumnProperty. + + """ + return self.columns[0] def instrument_class(self, mapper): if not self.instrument: @@ -147,17 +203,49 @@ class ColumnProperty(StrategizedProperty): impl = dest_state.get_impl(self.key) impl.set(dest_state, dest_dict, value, None) elif dest_state.has_identity and self.key not in dest_dict: - dest_state.expire_attributes(dest_dict, [self.key]) + dest_state._expire_attributes(dest_dict, [self.key]) class Comparator(PropComparator): + """Produce boolean, comparison, and other operators for + :class:`.ColumnProperty` attributes. + + See the documentation for :class:`.PropComparator` for a brief + overview. + + See also: + + :class:`.PropComparator` + + :class:`.ColumnOperators` + + :ref:`types_operators` + + :attr:`.TypeEngine.comparator_factory` + + """ @util.memoized_instancemethod def __clause_element__(self): if self.adapter: return self.adapter(self.prop.columns[0]) else: return self.prop.columns[0]._annotate({ - "parententity": self.mapper, - "parentmapper":self.mapper}) + "parententity": self._parentmapper, + "parentmapper": self._parentmapper}) + + @util.memoized_property + def info(self): + ce = self.__clause_element__() + try: + return ce.info + except AttributeError: + return self.prop.info + + def __getattr__(self, key): + """proxy attribute access down to the mapped column. + + this allows user-defined comparison methods to be accessed. + """ + return getattr(self.__clause_element__(), key) def operate(self, op, *other, **kwargs): return op(self.__clause_element__(), *other, **kwargs) @@ -166,1400 +254,6 @@ class ColumnProperty(StrategizedProperty): col = self.__clause_element__() return op(col._bind_param(op, other), col, **kwargs) - # TODO: legacy..do we need this ? (0.5) - ColumnComparator = Comparator - def __str__(self): return str(self.parent.class_.__name__) + "." + self.key -log.class_logger(ColumnProperty) - -class RelationshipProperty(StrategizedProperty): - """Describes an object property that holds a single item or list - of items that correspond to a related database table. - - Public constructor is the :func:`.orm.relationship` function. - - Of note here is the :class:`.RelationshipProperty.Comparator` - class, which implements comparison operations for scalar- - and collection-referencing mapped attributes. - - """ - - strategy_wildcard_key = 'relationship:*' - - def __init__(self, argument, - secondary=None, primaryjoin=None, - secondaryjoin=None, - foreign_keys=None, - uselist=None, - order_by=False, - backref=None, - back_populates=None, - post_update=False, - cascade=False, extension=None, - viewonly=False, lazy=True, - collection_class=None, passive_deletes=False, - passive_updates=True, remote_side=None, - enable_typechecks=True, join_depth=None, - comparator_factory=None, - single_parent=False, innerjoin=False, - doc=None, - active_history=False, - cascade_backrefs=True, - load_on_pending=False, - strategy_class=None, _local_remote_pairs=None, - query_class=None): - - self.uselist = uselist - self.argument = argument - self.secondary = secondary - self.primaryjoin = primaryjoin - self.secondaryjoin = secondaryjoin - self.post_update = post_update - self.direction = None - self.viewonly = viewonly - self.lazy = lazy - self.single_parent = single_parent - self._user_defined_foreign_keys = foreign_keys - self.collection_class = collection_class - self.passive_deletes = passive_deletes - self.cascade_backrefs = cascade_backrefs - self.passive_updates = passive_updates - self.remote_side = remote_side - self.enable_typechecks = enable_typechecks - self.query_class = query_class - self.innerjoin = innerjoin - self.doc = doc - self.active_history = active_history - self.join_depth = join_depth - self.local_remote_pairs = _local_remote_pairs - self.extension = extension - self.load_on_pending = load_on_pending - self.comparator_factory = comparator_factory or \ - RelationshipProperty.Comparator - self.comparator = self.comparator_factory(self, None) - util.set_creation_order(self) - - if strategy_class: - self.strategy_class = strategy_class - elif self.lazy== 'dynamic': - from sqlalchemy.orm import dynamic - self.strategy_class = dynamic.DynaLoader - else: - self.strategy_class = strategies.factory(self.lazy) - - self._reverse_property = set() - - if cascade is not False: - self.cascade = CascadeOptions(cascade) - else: - self.cascade = CascadeOptions("save-update, merge") - - if self.passive_deletes == 'all' and \ - ("delete" in self.cascade or - "delete-orphan" in self.cascade): - raise sa_exc.ArgumentError( - "Can't set passive_deletes='all' in conjunction " - "with 'delete' or 'delete-orphan' cascade") - - self.order_by = order_by - - self.back_populates = back_populates - - if self.back_populates: - if backref: - raise sa_exc.ArgumentError( - "backref and back_populates keyword arguments " - "are mutually exclusive") - self.backref = None - else: - self.backref = backref - - - def instrument_class(self, mapper): - attributes.register_descriptor( - mapper.class_, - self.key, - comparator=self.comparator_factory(self, mapper), - parententity=mapper, - doc=self.doc, - ) - - class Comparator(PropComparator): - """Produce comparison operations for :func:`~.orm.relationship`-based - attributes.""" - - def __init__(self, prop, mapper, of_type=None, adapter=None): - """Construction of :class:`.RelationshipProperty.Comparator` - is internal to the ORM's attribute mechanics. - - """ - self.prop = prop - self.mapper = mapper - self.adapter = adapter - if of_type: - self._of_type = _class_to_mapper(of_type) - - def adapted(self, adapter): - """Return a copy of this PropComparator which will use the - given adaption function on the local side of generated - expressions. - - """ - - return self.__class__(self.property, self.mapper, - getattr(self, '_of_type', None), - adapter) - - @property - def parententity(self): - return self.property.parent - - def __clause_element__(self): - elem = self.property.parent._with_polymorphic_selectable - if self.adapter: - return self.adapter(elem) - else: - return elem - - def of_type(self, cls): - """Produce a construct that represents a particular 'subtype' of - attribute for the parent class. - - Currently this is usable in conjunction with :meth:`.Query.join` - and :meth:`.Query.outerjoin`. - - """ - return RelationshipProperty.Comparator( - self.property, - self.mapper, - cls, adapter=self.adapter) - - def in_(self, other): - """Produce an IN clause - this is not implemented - for :func:`~.orm.relationship`-based attributes at this time. - - """ - raise NotImplementedError('in_() not yet supported for ' - 'relationships. For a simple many-to-one, use ' - 'in_() against the set of foreign key values.') - - __hash__ = None - - def __eq__(self, other): - """Implement the ``==`` operator. - - In a many-to-one context, such as:: - - MyClass.some_prop == - - this will typically produce a - clause such as:: - - mytable.related_id == - - Where ```` is the primary key of the given - object. - - The ``==`` operator provides partial functionality for non- - many-to-one comparisons: - - * Comparisons against collections are not supported. - Use :meth:`~.RelationshipProperty.Comparator.contains`. - * Compared to a scalar one-to-many, will produce a - clause that compares the target columns in the parent to - the given target. - * Compared to a scalar many-to-many, an alias - of the association table will be rendered as - well, forming a natural join that is part of the - main body of the query. This will not work for - queries that go beyond simple AND conjunctions of - comparisons, such as those which use OR. Use - explicit joins, outerjoins, or - :meth:`~.RelationshipProperty.Comparator.has` for - more comprehensive non-many-to-one scalar - membership tests. - * Comparisons against ``None`` given in a one-to-many - or many-to-many context produce a NOT EXISTS clause. - - """ - if isinstance(other, (NoneType, expression._Null)): - if self.property.direction in [ONETOMANY, MANYTOMANY]: - return ~self._criterion_exists() - else: - return _orm_annotate(self.property._optimized_compare( - None, adapt_source=self.adapter)) - elif self.property.uselist: - raise sa_exc.InvalidRequestError("Can't compare a colle" - "ction to an object or collection; use " - "contains() to test for membership.") - else: - return _orm_annotate(self.property._optimized_compare(other, - adapt_source=self.adapter)) - - def _criterion_exists(self, criterion=None, **kwargs): - if getattr(self, '_of_type', None): - target_mapper = self._of_type - to_selectable = target_mapper._with_polymorphic_selectable - if self.property._is_self_referential: - to_selectable = to_selectable.alias() - - single_crit = target_mapper._single_table_criterion - if single_crit is not None: - if criterion is not None: - criterion = single_crit & criterion - else: - criterion = single_crit - else: - to_selectable = None - - if self.adapter: - source_selectable = self.__clause_element__() - else: - source_selectable = None - - pj, sj, source, dest, secondary, target_adapter = \ - self.property._create_joins(dest_polymorphic=True, - dest_selectable=to_selectable, - source_selectable=source_selectable) - - for k in kwargs: - crit = getattr(self.property.mapper.class_, k) == kwargs[k] - if criterion is None: - criterion = crit - else: - criterion = criterion & crit - - # annotate the *local* side of the join condition, in the case - # of pj + sj this is the full primaryjoin, in the case of just - # pj its the local side of the primaryjoin. - if sj is not None: - j = _orm_annotate(pj) & sj - else: - j = _orm_annotate(pj, exclude=self.property.remote_side) - - if criterion is not None and target_adapter: - # limit this adapter to annotated only? - criterion = target_adapter.traverse(criterion) - - # only have the "joined left side" of what we - # return be subject to Query adaption. The right - # side of it is used for an exists() subquery and - # should not correlate or otherwise reach out - # to anything in the enclosing query. - if criterion is not None: - criterion = criterion._annotate({'no_replacement_traverse': True}) - - crit = j & criterion - - return sql.exists([1], crit, from_obj=dest).\ - correlate(source._annotate({'_orm_adapt':True})) - - def any(self, criterion=None, **kwargs): - """Produce an expression that tests a collection against - particular criterion, using EXISTS. - - An expression like:: - - session.query(MyClass).filter( - MyClass.somereference.any(SomeRelated.x==2) - ) - - - Will produce a query like:: - - SELECT * FROM my_table WHERE - EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id - AND related.x=2) - - Because :meth:`~.RelationshipProperty.Comparator.any` uses - a correlated subquery, its performance is not nearly as - good when compared against large target tables as that of - using a join. - - :meth:`~.RelationshipProperty.Comparator.any` is particularly - useful for testing for empty collections:: - - session.query(MyClass).filter( - ~MyClass.somereference.any() - ) - - will produce:: - - SELECT * FROM my_table WHERE - NOT EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id) - - :meth:`~.RelationshipProperty.Comparator.any` is only - valid for collections, i.e. a :func:`.relationship` - that has ``uselist=True``. For scalar references, - use :meth:`~.RelationshipProperty.Comparator.has`. - - """ - if not self.property.uselist: - raise sa_exc.InvalidRequestError( - "'any()' not implemented for scalar " - "attributes. Use has()." - ) - - return self._criterion_exists(criterion, **kwargs) - - def has(self, criterion=None, **kwargs): - """Produce an expression that tests a scalar reference against - particular criterion, using EXISTS. - - An expression like:: - - session.query(MyClass).filter( - MyClass.somereference.has(SomeRelated.x==2) - ) - - - Will produce a query like:: - - SELECT * FROM my_table WHERE - EXISTS (SELECT 1 FROM related WHERE related.id==my_table.related_id - AND related.x=2) - - Because :meth:`~.RelationshipProperty.Comparator.has` uses - a correlated subquery, its performance is not nearly as - good when compared against large target tables as that of - using a join. - - :meth:`~.RelationshipProperty.Comparator.has` is only - valid for scalar references, i.e. a :func:`.relationship` - that has ``uselist=False``. For collection references, - use :meth:`~.RelationshipProperty.Comparator.any`. - - """ - if self.property.uselist: - raise sa_exc.InvalidRequestError( - "'has()' not implemented for collections. " - "Use any().") - return self._criterion_exists(criterion, **kwargs) - - def contains(self, other, **kwargs): - """Return a simple expression that tests a collection for - containment of a particular item. - - :meth:`~.RelationshipProperty.Comparator.contains` is - only valid for a collection, i.e. a - :func:`~.orm.relationship` that implements - one-to-many or many-to-many with ``uselist=True``. - - When used in a simple one-to-many context, an - expression like:: - - MyClass.contains(other) - - Produces a clause like:: - - mytable.id == - - Where ```` is the value of the foreign key - attribute on ``other`` which refers to the primary - key of its parent object. From this it follows that - :meth:`~.RelationshipProperty.Comparator.contains` is - very useful when used with simple one-to-many - operations. - - For many-to-many operations, the behavior of - :meth:`~.RelationshipProperty.Comparator.contains` - has more caveats. The association table will be - rendered in the statement, producing an "implicit" - join, that is, includes multiple tables in the FROM - clause which are equated in the WHERE clause:: - - query(MyClass).filter(MyClass.contains(other)) - - Produces a query like:: - - SELECT * FROM my_table, my_association_table AS - my_association_table_1 WHERE - my_table.id = my_association_table_1.parent_id - AND my_association_table_1.child_id = - - Where ```` would be the primary key of - ``other``. From the above, it is clear that - :meth:`~.RelationshipProperty.Comparator.contains` - will **not** work with many-to-many collections when - used in queries that move beyond simple AND - conjunctions, such as multiple - :meth:`~.RelationshipProperty.Comparator.contains` - expressions joined by OR. In such cases subqueries or - explicit "outer joins" will need to be used instead. - See :meth:`~.RelationshipProperty.Comparator.any` for - a less-performant alternative using EXISTS, or refer - to :meth:`.Query.outerjoin` as well as :ref:`ormtutorial_joins` - for more details on constructing outer joins. - - """ - if not self.property.uselist: - raise sa_exc.InvalidRequestError( - "'contains' not implemented for scalar " - "attributes. Use ==") - clause = self.property._optimized_compare(other, - adapt_source=self.adapter) - - if self.property.secondaryjoin is not None: - clause.negation_clause = \ - self.__negated_contains_or_equals(other) - - return clause - - def __negated_contains_or_equals(self, other): - if self.property.direction == MANYTOONE: - state = attributes.instance_state(other) - - def state_bindparam(x, state, col): - o = state.obj() # strong ref - return sql.bindparam(x, unique=True, callable_=lambda : \ - self.property.mapper._get_committed_attr_by_column(o, - col)) - - def adapt(col): - if self.adapter: - return self.adapter(col) - else: - return col - - if self.property._use_get: - return sql.and_(*[ - sql.or_( - adapt(x) != state_bindparam(adapt(x), state, y), - adapt(x) == None) - for (x, y) in self.property.local_remote_pairs]) - - criterion = sql.and_(*[x==y for (x, y) in - zip( - self.property.mapper.primary_key, - self.property.\ - mapper.\ - primary_key_from_instance(other)) - ]) - return ~self._criterion_exists(criterion) - - def __ne__(self, other): - """Implement the ``!=`` operator. - - In a many-to-one context, such as:: - - MyClass.some_prop != - - This will typically produce a clause such as:: - - mytable.related_id != - - Where ```` is the primary key of the - given object. - - The ``!=`` operator provides partial functionality for non- - many-to-one comparisons: - - * Comparisons against collections are not supported. - Use - :meth:`~.RelationshipProperty.Comparator.contains` - in conjunction with :func:`~.expression.not_`. - * Compared to a scalar one-to-many, will produce a - clause that compares the target columns in the parent to - the given target. - * Compared to a scalar many-to-many, an alias - of the association table will be rendered as - well, forming a natural join that is part of the - main body of the query. This will not work for - queries that go beyond simple AND conjunctions of - comparisons, such as those which use OR. Use - explicit joins, outerjoins, or - :meth:`~.RelationshipProperty.Comparator.has` in - conjunction with :func:`~.expression.not_` for - more comprehensive non-many-to-one scalar - membership tests. - * Comparisons against ``None`` given in a one-to-many - or many-to-many context produce an EXISTS clause. - - """ - if isinstance(other, (NoneType, expression._Null)): - if self.property.direction == MANYTOONE: - return sql.or_(*[x != None for x in - self.property._calculated_foreign_keys]) - else: - return self._criterion_exists() - elif self.property.uselist: - raise sa_exc.InvalidRequestError("Can't compare a collection" - " to an object or collection; use " - "contains() to test for membership.") - else: - return self.__negated_contains_or_equals(other) - - @util.memoized_property - def property(self): - if mapperlib.module._new_mappers: - configure_mappers() - return self.prop - - def compare(self, op, value, - value_is_parent=False, - alias_secondary=True): - if op == operators.eq: - if value is None: - if self.uselist: - return ~sql.exists([1], self.primaryjoin) - else: - return self._optimized_compare(None, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) - else: - return self._optimized_compare(value, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) - else: - return op(self.comparator, value) - - def _optimized_compare(self, value, value_is_parent=False, - adapt_source=None, - alias_secondary=True): - if value is not None: - value = attributes.instance_state(value) - return self._get_strategy(strategies.LazyLoader).lazy_clause(value, - reverse_direction=not value_is_parent, - alias_secondary=alias_secondary, - adapt_source=adapt_source) - - def __str__(self): - return str(self.parent.class_.__name__) + "." + self.key - - def merge(self, - session, - source_state, - source_dict, - dest_state, - dest_dict, - load, _recursive): - - if load: - for r in self._reverse_property: - if (source_state, r) in _recursive: - return - - - if not "merge" in self.cascade: - return - - if self.key not in source_dict: - return - - if self.uselist: - instances = source_state.get_impl(self.key).\ - get(source_state, source_dict) - if hasattr(instances, '_sa_adapter'): - # convert collections to adapters to get a true iterator - instances = instances._sa_adapter - - if load: - # for a full merge, pre-load the destination collection, - # so that individual _merge of each item pulls from identity - # map for those already present. - # also assumes CollectionAttrbiuteImpl behavior of loading - # "old" list in any case - dest_state.get_impl(self.key).get(dest_state, dest_dict) - - dest_list = [] - for current in instances: - current_state = attributes.instance_state(current) - current_dict = attributes.instance_dict(current) - _recursive[(current_state, self)] = True - obj = session._merge(current_state, current_dict, - load=load, _recursive=_recursive) - if obj is not None: - dest_list.append(obj) - - if not load: - coll = attributes.init_state_collection(dest_state, - dest_dict, self.key) - for c in dest_list: - coll.append_without_event(c) - else: - dest_state.get_impl(self.key)._set_iterable(dest_state, - dest_dict, dest_list) - else: - current = source_dict[self.key] - if current is not None: - current_state = attributes.instance_state(current) - current_dict = attributes.instance_dict(current) - _recursive[(current_state, self)] = True - obj = session._merge(current_state, current_dict, - load=load, _recursive=_recursive) - else: - obj = None - - if not load: - dest_dict[self.key] = obj - else: - dest_state.get_impl(self.key).set(dest_state, - dest_dict, obj, None) - - def cascade_iterator(self, type_, state, dict_, visited_states, halt_on=None): - #assert type_ in self.cascade - - # only actively lazy load on the 'delete' cascade - if type_ != 'delete' or self.passive_deletes: - passive = attributes.PASSIVE_NO_INITIALIZE - else: - passive = attributes.PASSIVE_OFF - - if type_ == 'save-update': - tuples = state.manager[self.key].impl.\ - get_all_pending(state, dict_) - - else: - tuples = state.value_as_iterable(dict_, self.key, - passive=passive) - - skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \ - not in self.cascade - - for instance_state, c in tuples: - if instance_state in visited_states: - continue - - if c is None: - # would like to emit a warning here, but - # would not be consistent with collection.append(None) - # current behavior of silently skipping. - # see [ticket:2229] - continue - - instance_dict = attributes.instance_dict(c) - - if halt_on and halt_on(instance_state): - continue - - if skip_pending and not instance_state.key: - continue - - instance_mapper = instance_state.manager.mapper - - if not instance_mapper.isa(self.mapper.class_manager.mapper): - raise AssertionError("Attribute '%s' on class '%s' " - "doesn't handle objects " - "of type '%s'" % ( - self.key, - self.parent.class_, - c.__class__ - )) - - visited_states.add(instance_state) - - yield c, instance_mapper, instance_state, instance_dict - - - def _add_reverse_property(self, key): - other = self.mapper.get_property(key, _compile_mappers=False) - self._reverse_property.add(other) - other._reverse_property.add(self) - - if not other.mapper.common_parent(self.parent): - raise sa_exc.ArgumentError('reverse_property %r on ' - 'relationship %s references relationship %s, which ' - 'does not reference mapper %s' % (key, self, other, - self.parent)) - if self.direction in (ONETOMANY, MANYTOONE) and self.direction \ - == other.direction: - raise sa_exc.ArgumentError('%s and back-reference %s are ' - 'both of the same direction %r. Did you mean to ' - 'set remote_side on the many-to-one side ?' - % (other, self, self.direction)) - - @util.memoized_property - def mapper(self): - """Return the targeted :class:`.Mapper` for this - :class:`.RelationshipProperty`. - - This is a lazy-initializing static attribute. - - """ - if isinstance(self.argument, type): - mapper_ = mapper.class_mapper(self.argument, - compile=False) - elif isinstance(self.argument, mapper.Mapper): - mapper_ = self.argument - elif util.callable(self.argument): - - # accept a callable to suit various deferred- - # configurational schemes - - mapper_ = mapper.class_mapper(self.argument(), - compile=False) - else: - raise sa_exc.ArgumentError("relationship '%s' expects " - "a class or a mapper argument (received: %s)" - % (self.key, type(self.argument))) - assert isinstance(mapper_, mapper.Mapper), mapper_ - return mapper_ - - @util.memoized_property - @util.deprecated("0.7", "Use .target") - def table(self): - """Return the selectable linked to this - :class:`.RelationshipProperty` object's target - :class:`.Mapper`.""" - return self.target - - def do_init(self): - self._check_conflicts() - self._process_dependent_arguments() - self._determine_joins() - self._determine_synchronize_pairs() - self._determine_direction() - self._determine_local_remote_pairs() - self._post_init() - self._generate_backref() - super(RelationshipProperty, self).do_init() - - def _check_conflicts(self): - """Test that this relationship is legal, warn about - inheritance conflicts.""" - - if not self.is_primary() \ - and not mapper.class_mapper( - self.parent.class_, - compile=False).has_property(self.key): - raise sa_exc.ArgumentError("Attempting to assign a new " - "relationship '%s' to a non-primary mapper on " - "class '%s'. New relationships can only be added " - "to the primary mapper, i.e. the very first mapper " - "created for class '%s' " % (self.key, - self.parent.class_.__name__, - self.parent.class_.__name__)) - - # check for conflicting relationship() on superclass - if not self.parent.concrete: - for inheriting in self.parent.iterate_to_root(): - if inheriting is not self.parent \ - and inheriting.has_property(self.key): - util.warn("Warning: relationship '%s' on mapper " - "'%s' supersedes the same relationship " - "on inherited mapper '%s'; this can " - "cause dependency issues during flush" - % (self.key, self.parent, inheriting)) - - def _process_dependent_arguments(self): - """Convert incoming configuration arguments to their - proper form. - - Callables are resolved, ORM annotations removed. - - """ - # accept callables for other attributes which may require - # deferred initialization. This technique is used - # by declarative "string configs" and some recipes. - for attr in ( - 'order_by', - 'primaryjoin', - 'secondaryjoin', - 'secondary', - '_user_defined_foreign_keys', - 'remote_side', - ): - attr_value = getattr(self, attr) - if util.callable(attr_value): - setattr(self, attr, attr_value()) - - # remove "annotations" which are present if mapped class - # descriptors are used to create the join expression. - for attr in 'primaryjoin', 'secondaryjoin': - val = getattr(self, attr) - if val is not None: - setattr(self, attr, _orm_deannotate( - expression._only_column_elements(val, attr)) - ) - - # ensure expressions in self.order_by, foreign_keys, - # remote_side are all columns, not strings. - if self.order_by is not False and self.order_by is not None: - self.order_by = [ - expression._only_column_elements(x, "order_by") - for x in - util.to_list(self.order_by)] - - self._user_defined_foreign_keys = \ - util.column_set( - expression._only_column_elements(x, "foreign_keys") - for x in util.to_column_set( - self._user_defined_foreign_keys - )) - - self.remote_side = \ - util.column_set( - expression._only_column_elements(x, "remote_side") - for x in - util.to_column_set(self.remote_side)) - - self.target = self.mapper.mapped_table - - if self.cascade.delete_orphan: - self.mapper.primary_mapper().delete_orphans.append( - (self.key, self.parent.class_) - ) - - def _determine_joins(self): - """Determine the 'primaryjoin' and 'secondaryjoin' attributes, - if not passed to the constructor already. - - This is based on analysis of the foreign key relationships - between the parent and target mapped selectables. - - """ - if self.secondaryjoin is not None and self.secondary is None: - raise sa_exc.ArgumentError("Property '" + self.key - + "' specified with secondary join condition but " - "no secondary argument") - - # if join conditions were not specified, figure them out based - # on foreign keys - - def _search_for_join(mapper, table): - # find a join between the given mapper's mapped table and - # the given table. will try the mapper's local table first - # for more specificity, then if not found will try the more - # general mapped table, which in the case of inheritance is - # a join. - return join_condition(mapper.mapped_table, table, - a_subset=mapper.local_table) - - try: - if self.secondary is not None: - if self.secondaryjoin is None: - self.secondaryjoin = _search_for_join(self.mapper, - self.secondary) - if self.primaryjoin is None: - self.primaryjoin = _search_for_join(self.parent, - self.secondary) - else: - if self.primaryjoin is None: - self.primaryjoin = _search_for_join(self.parent, - self.target) - except sa_exc.ArgumentError, e: - raise sa_exc.ArgumentError("Could not determine join " - "condition between parent/child tables on " - "relationship %s. Specify a 'primaryjoin' " - "expression. If 'secondary' is present, " - "'secondaryjoin' is needed as well." - % self) - - def _columns_are_mapped(self, *cols): - """Return True if all columns in the given collection are - mapped by the tables referenced by this :class:`.Relationship`. - - """ - for c in cols: - if self.secondary is not None \ - and self.secondary.c.contains_column(c): - continue - if not self.parent.mapped_table.c.contains_column(c) and \ - not self.target.c.contains_column(c): - return False - return True - - def _sync_pairs_from_join(self, join_condition, primary): - """Determine a list of "source"/"destination" column pairs - based on the given join condition, as well as the - foreign keys argument. - - "source" would be a column referenced by a foreign key, - and "destination" would be the column who has a foreign key - reference to "source". - - """ - - fks = self._user_defined_foreign_keys - # locate pairs - eq_pairs = criterion_as_pairs(join_condition, - consider_as_foreign_keys=fks, - any_operator=self.viewonly) - - # couldn't find any fks, but we have - # "secondary" - assume the "secondary" columns - # are the fks - if not eq_pairs and \ - self.secondary is not None and \ - not fks: - fks = set(self.secondary.c) - eq_pairs = criterion_as_pairs(join_condition, - consider_as_foreign_keys=fks, - any_operator=self.viewonly) - - if eq_pairs: - util.warn("No ForeignKey objects were present " - "in secondary table '%s'. Assumed referenced " - "foreign key columns %s for join condition '%s' " - "on relationship %s" % ( - self.secondary.description, - ", ".join(sorted(["'%s'" % col for col in fks])), - join_condition, - self - )) - - # Filter out just to columns that are mapped. - # If viewonly, allow pairs where the FK col - # was part of "foreign keys" - the column it references - # may be in an un-mapped table - see - # test.orm.test_relationships.ViewOnlyComplexJoin.test_basic - # for an example of this. - eq_pairs = [(l, r) for (l, r) in eq_pairs - if self._columns_are_mapped(l, r) - or self.viewonly and - r in fks] - - if eq_pairs: - return eq_pairs - - # from here below is just determining the best error message - # to report. Check for a join condition using any operator - # (not just ==), perhaps they need to turn on "viewonly=True". - if not self.viewonly and criterion_as_pairs(join_condition, - consider_as_foreign_keys=self._user_defined_foreign_keys, - any_operator=True): - - err = "Could not locate any "\ - "foreign-key-equated, locally mapped column "\ - "pairs for %s "\ - "condition '%s' on relationship %s." % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self - ) - - if not self._user_defined_foreign_keys: - err += " Ensure that the "\ - "referencing Column objects have a "\ - "ForeignKey present, or are otherwise part "\ - "of a ForeignKeyConstraint on their parent "\ - "Table, or specify the foreign_keys parameter "\ - "to this relationship." - - err += " For more "\ - "relaxed rules on join conditions, the "\ - "relationship may be marked as viewonly=True." - - raise sa_exc.ArgumentError(err) - else: - if self._user_defined_foreign_keys: - raise sa_exc.ArgumentError("Could not determine " - "relationship direction for %s condition " - "'%s', on relationship %s, using manual " - "'foreign_keys' setting. Do the columns " - "in 'foreign_keys' represent all, and " - "only, the 'foreign' columns in this join " - "condition? Does the %s Table already " - "have adequate ForeignKey and/or " - "ForeignKeyConstraint objects established " - "(in which case 'foreign_keys' is usually " - "unnecessary)?" - % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self, - primary and 'mapped' or 'secondary' - )) - else: - raise sa_exc.ArgumentError("Could not determine " - "relationship direction for %s condition " - "'%s', on relationship %s. Ensure that the " - "referencing Column objects have a " - "ForeignKey present, or are otherwise part " - "of a ForeignKeyConstraint on their parent " - "Table, or specify the foreign_keys parameter " - "to this relationship." - % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self - )) - - def _determine_synchronize_pairs(self): - """Resolve 'primary'/foreign' column pairs from the primaryjoin - and secondaryjoin arguments. - - """ - if self.local_remote_pairs: - if not self._user_defined_foreign_keys: - raise sa_exc.ArgumentError( - "foreign_keys argument is " - "required with _local_remote_pairs argument") - self.synchronize_pairs = [] - for l, r in self.local_remote_pairs: - if r in self._user_defined_foreign_keys: - self.synchronize_pairs.append((l, r)) - elif l in self._user_defined_foreign_keys: - self.synchronize_pairs.append((r, l)) - else: - self.synchronize_pairs = self._sync_pairs_from_join( - self.primaryjoin, - True) - - self._calculated_foreign_keys = util.column_set( - r for (l, r) in - self.synchronize_pairs) - - if self.secondaryjoin is not None: - self.secondary_synchronize_pairs = self._sync_pairs_from_join( - self.secondaryjoin, - False) - self._calculated_foreign_keys.update( - r for (l, r) in - self.secondary_synchronize_pairs) - else: - self.secondary_synchronize_pairs = None - - def _determine_direction(self): - """Determine if this relationship is one to many, many to one, - many to many. - - This is derived from the primaryjoin, presence of "secondary", - and in the case of self-referential the "remote side". - - """ - if self.secondaryjoin is not None: - self.direction = MANYTOMANY - elif self._refers_to_parent_table(): - - # self referential defaults to ONETOMANY unless the "remote" - # side is present and does not reference any foreign key - # columns - - if self.local_remote_pairs: - remote = [r for (l, r) in self.local_remote_pairs] - elif self.remote_side: - remote = self.remote_side - else: - remote = None - if not remote or self._calculated_foreign_keys.difference(l for (l, - r) in self.synchronize_pairs).intersection(remote): - self.direction = ONETOMANY - else: - self.direction = MANYTOONE - else: - parentcols = util.column_set(self.parent.mapped_table.c) - targetcols = util.column_set(self.mapper.mapped_table.c) - - # fk collection which suggests ONETOMANY. - onetomany_fk = targetcols.intersection( - self._calculated_foreign_keys) - - # fk collection which suggests MANYTOONE. - - manytoone_fk = parentcols.intersection( - self._calculated_foreign_keys) - - if onetomany_fk and manytoone_fk: - # fks on both sides. do the same test only based on the - # local side. - referents = [c for (c, f) in self.synchronize_pairs] - onetomany_local = parentcols.intersection(referents) - manytoone_local = targetcols.intersection(referents) - - if onetomany_local and not manytoone_local: - self.direction = ONETOMANY - elif manytoone_local and not onetomany_local: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError( - "Can't determine relationship" - " direction for relationship '%s' - foreign " - "key columns are present in both the parent " - "and the child's mapped tables. Specify " - "'foreign_keys' argument." % self) - elif onetomany_fk: - self.direction = ONETOMANY - elif manytoone_fk: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError("Can't determine relationship " - "direction for relationship '%s' - foreign " - "key columns are present in neither the parent " - "nor the child's mapped tables" % self) - - if self.cascade.delete_orphan and not self.single_parent \ - and (self.direction is MANYTOMANY or self.direction - is MANYTOONE): - util.warn('On %s, delete-orphan cascade is not supported ' - 'on a many-to-many or many-to-one relationship ' - 'when single_parent is not set. Set ' - 'single_parent=True on the relationship().' - % self) - if self.direction is MANYTOONE and self.passive_deletes: - util.warn("On %s, 'passive_deletes' is normally configured " - "on one-to-many, one-to-one, many-to-many " - "relationships only." - % self) - - def _determine_local_remote_pairs(self): - """Determine pairs of columns representing "local" to - "remote", where "local" columns are on the parent mapper, - "remote" are on the target mapper. - - These pairs are used on the load side only to generate - lazy loading clauses. - - """ - if not self.local_remote_pairs and not self.remote_side: - # the most common, trivial case. Derive - # local/remote pairs from the synchronize pairs. - eq_pairs = util.unique_list( - self.synchronize_pairs + - (self.secondary_synchronize_pairs or [])) - if self.direction is MANYTOONE: - self.local_remote_pairs = [(r, l) for l, r in eq_pairs] - else: - self.local_remote_pairs = eq_pairs - - # "remote_side" specified, derive from the primaryjoin - # plus remote_side, similarly to how synchronize_pairs - # were determined. - elif self.remote_side: - if self.local_remote_pairs: - raise sa_exc.ArgumentError('remote_side argument is ' - 'redundant against more detailed ' - '_local_remote_side argument.') - if self.direction is MANYTOONE: - self.local_remote_pairs = [(r, l) for (l, r) in - criterion_as_pairs(self.primaryjoin, - consider_as_referenced_keys=self.remote_side, - any_operator=True)] - - else: - self.local_remote_pairs = \ - criterion_as_pairs(self.primaryjoin, - consider_as_foreign_keys=self.remote_side, - any_operator=True) - if not self.local_remote_pairs: - raise sa_exc.ArgumentError('Relationship %s could ' - 'not determine any local/remote column ' - 'pairs from remote side argument %r' - % (self, self.remote_side)) - # else local_remote_pairs were sent explcitly via - # ._local_remote_pairs. - - # create local_side/remote_side accessors - self.local_side = util.ordered_column_set( - l for l, r in self.local_remote_pairs) - self.remote_side = util.ordered_column_set( - r for l, r in self.local_remote_pairs) - - # check that the non-foreign key column in the local/remote - # collection is mapped. The foreign key - # which the individual mapped column references directly may - # itself be in a non-mapped table; see - # test.orm.test_relationships.ViewOnlyComplexJoin.test_basic - # for an example of this. - if self.direction is ONETOMANY: - for col in self.local_side: - if not self._columns_are_mapped(col): - raise sa_exc.ArgumentError( - "Local column '%s' is not " - "part of mapping %s. Specify remote_side " - "argument to indicate which column lazy join " - "condition should compare against." % (col, - self.parent)) - elif self.direction is MANYTOONE: - for col in self.remote_side: - if not self._columns_are_mapped(col): - raise sa_exc.ArgumentError( - "Remote column '%s' is not " - "part of mapping %s. Specify remote_side " - "argument to indicate which column lazy join " - "condition should bind." % (col, self.mapper)) - - def _generate_backref(self): - if not self.is_primary(): - return - if self.backref is not None and not self.back_populates: - if isinstance(self.backref, basestring): - backref_key, kwargs = self.backref, {} - else: - backref_key, kwargs = self.backref - mapper = self.mapper.primary_mapper() - if mapper.has_property(backref_key): - raise sa_exc.ArgumentError("Error creating backref " - "'%s' on relationship '%s': property of that " - "name exists on mapper '%s'" % (backref_key, - self, mapper)) - if self.secondary is not None: - pj = kwargs.pop('primaryjoin', self.secondaryjoin) - sj = kwargs.pop('secondaryjoin', self.primaryjoin) - else: - pj = kwargs.pop('primaryjoin', self.primaryjoin) - sj = kwargs.pop('secondaryjoin', None) - if sj: - raise sa_exc.InvalidRequestError( - "Can't assign 'secondaryjoin' on a backref against " - "a non-secondary relationship." - ) - foreign_keys = kwargs.pop('foreign_keys', - self._user_defined_foreign_keys) - parent = self.parent.primary_mapper() - kwargs.setdefault('viewonly', self.viewonly) - kwargs.setdefault('post_update', self.post_update) - kwargs.setdefault('passive_updates', self.passive_updates) - self.back_populates = backref_key - relationship = RelationshipProperty( - parent, - self.secondary, - pj, - sj, - foreign_keys=foreign_keys, - back_populates=self.key, - **kwargs - ) - mapper._configure_property(backref_key, relationship) - - if self.back_populates: - self._add_reverse_property(self.back_populates) - - def _post_init(self): - self.logger.info('%s setup primary join %s', self, - self.primaryjoin) - self.logger.info('%s setup secondary join %s', self, - self.secondaryjoin) - self.logger.info('%s synchronize pairs [%s]', self, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.synchronize_pairs)) - self.logger.info('%s secondary synchronize pairs [%s]', self, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.secondary_synchronize_pairs or [])) - self.logger.info('%s local/remote pairs [%s]', self, - ','.join('(%s / %s)' % (l, r) for (l, r) in - self.local_remote_pairs)) - self.logger.info('%s relationship direction %s', self, - self.direction) - if self.uselist is None: - self.uselist = self.direction is not MANYTOONE - if not self.viewonly: - self._dependency_processor = \ - dependency.DependencyProcessor.from_relationship(self) - - @util.memoized_property - def _use_get(self): - """memoize the 'use_get' attribute of this RelationshipLoader's - lazyloader.""" - - strategy = self._get_strategy(strategies.LazyLoader) - return strategy.use_get - - def _refers_to_parent_table(self): - pt = self.parent.mapped_table - mt = self.mapper.mapped_table - for c, f in self.synchronize_pairs: - if ( - pt.is_derived_from(c.table) and \ - pt.is_derived_from(f.table) and \ - mt.is_derived_from(c.table) and \ - mt.is_derived_from(f.table) - ): - return True - else: - return False - - @util.memoized_property - def _is_self_referential(self): - return self.mapper.common_parent(self.parent) - - def per_property_preprocessors(self, uow): - if not self.viewonly and self._dependency_processor: - self._dependency_processor.per_property_preprocessors(uow) - - def _create_joins(self, source_polymorphic=False, - source_selectable=None, dest_polymorphic=False, - dest_selectable=None, of_type=None): - if source_selectable is None: - if source_polymorphic and self.parent.with_polymorphic: - source_selectable = self.parent._with_polymorphic_selectable - - aliased = False - if dest_selectable is None: - if dest_polymorphic and self.mapper.with_polymorphic: - dest_selectable = self.mapper._with_polymorphic_selectable - aliased = True - else: - dest_selectable = self.mapper.mapped_table - - if self._is_self_referential and source_selectable is None: - dest_selectable = dest_selectable.alias() - aliased = True - else: - aliased = True - - # place a barrier on the destination such that - # replacement traversals won't ever dig into it. - # its internal structure remains fixed - # regardless of context. - dest_selectable = _shallow_annotate( - dest_selectable, - {'no_replacement_traverse':True}) - - aliased = aliased or (source_selectable is not None) - - primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ - self.secondaryjoin, self.secondary - - # adjust the join condition for single table inheritance, - # in the case that the join is to a subclass - # this is analogous to the "_adjust_for_single_table_inheritance()" - # method in Query. - - dest_mapper = of_type or self.mapper - - single_crit = dest_mapper._single_table_criterion - if single_crit is not None: - if secondaryjoin is not None: - secondaryjoin = secondaryjoin & single_crit - else: - primaryjoin = primaryjoin & single_crit - - if aliased: - if secondary is not None: - secondary = secondary.alias() - primary_aliasizer = ClauseAdapter(secondary) - secondary_aliasizer = \ - ClauseAdapter(dest_selectable, - equivalents=self.mapper._equivalent_columns).\ - chain(primary_aliasizer) - if source_selectable is not None: - primary_aliasizer = \ - ClauseAdapter(secondary).\ - chain(ClauseAdapter(source_selectable, - equivalents=self.parent._equivalent_columns)) - secondaryjoin = \ - secondary_aliasizer.traverse(secondaryjoin) - else: - primary_aliasizer = ClauseAdapter(dest_selectable, - exclude=self.local_side, - equivalents=self.mapper._equivalent_columns) - if source_selectable is not None: - primary_aliasizer.chain( - ClauseAdapter(source_selectable, - exclude=self.remote_side, - equivalents=self.parent._equivalent_columns)) - secondary_aliasizer = None - primaryjoin = primary_aliasizer.traverse(primaryjoin) - target_adapter = secondary_aliasizer or primary_aliasizer - target_adapter.include = target_adapter.exclude = None - else: - target_adapter = None - if source_selectable is None: - source_selectable = self.parent.local_table - if dest_selectable is None: - dest_selectable = self.mapper.local_table - return ( - primaryjoin, - secondaryjoin, - source_selectable, - dest_selectable, - secondary, - target_adapter, - ) - -PropertyLoader = RelationProperty = RelationshipProperty -log.class_logger(RelationshipProperty) - diff --git a/libs/sqlalchemy/orm/query.py b/libs/sqlalchemy/orm/query.py index 286dbf6b..6bd465e9 100644 --- a/libs/sqlalchemy/orm/query.py +++ b/libs/sqlalchemy/orm/query.py @@ -1,5 +1,5 @@ # orm/query.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -19,38 +19,33 @@ database to return iterable result sets. """ from itertools import chain -from operator import itemgetter -from sqlalchemy import sql, util, log, schema -from sqlalchemy import exc as sa_exc -from sqlalchemy.orm import exc as orm_exc -from sqlalchemy.sql import util as sql_util -from sqlalchemy.sql import expression, visitors, operators -from sqlalchemy.orm import ( - attributes, interfaces, mapper, object_mapper, evaluator, +from . import ( + attributes, interfaces, object_mapper, persistence, + exc as orm_exc, loading ) -from sqlalchemy.orm.util import ( - AliasedClass, ORMAdapter, _entity_descriptor, _entity_info, - _is_aliased_class, _is_mapped_class, _orm_columns, _orm_selectable, - join as orm_join,with_parent, _attr_as_key, aliased +from .base import _entity_descriptor, _is_aliased_class, \ + _is_mapped_class, _orm_columns, _generative +from .path_registry import PathRegistry +from .util import ( + AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased ) - +from .. import sql, util, log, exc as sa_exc, inspect, inspection +from ..sql.expression import _interpret_as_from +from ..sql import ( + util as sql_util, + expression, visitors + ) +from ..sql.base import ColumnCollection +from . import properties __all__ = ['Query', 'QueryContext', 'aliased'] -def _generative(*assertions): - """Mark a method as generative.""" - - @util.decorator - def generate(fn, *args, **kw): - self = args[0]._clone() - for assertion in assertions: - assertion(self, fn.func_name) - fn(self, *args[1:], **kw) - return self - return generate +_path_registry = PathRegistry.root +@inspection._self_inspects +@log.class_logger class Query(object): """ORM-level SQL construction object. @@ -62,8 +57,9 @@ class Query(object): criteria and options associated with it. :class:`.Query` objects are normally initially generated using the - :meth:`~.Session.query` method of :class:`.Session`. For a full walkthrough - of :class:`.Query` usage, see the :ref:`ormtutorial_toplevel`. + :meth:`~.Session.query` method of :class:`.Session`. For a full + walkthrough of :class:`.Query` usage, see the + :ref:`ormtutorial_toplevel`. """ @@ -72,7 +68,6 @@ class Query(object): _with_labels = False _criterion = None _yield_per = None - _lockmode = None _order_by = False _group_by = False _having = None @@ -80,18 +75,19 @@ class Query(object): _prefixes = None _offset = None _limit = None + _for_update_arg = None _statement = None _correlate = frozenset() _populate_existing = False _invoke_all_eagers = True _version_check = False _autoflush = True - _current_path = () _only_load_props = None _refresh_state = None _from_obj = () _join_entities = () _select_from_entity = None + _mapper_adapter_map = {} _filter_aliases = None _from_obj_alias = None _joinpath = _joinpoint = util.immutabledict() @@ -102,6 +98,8 @@ class Query(object): _with_hints = () _enable_single_crit = True + _current_path = _path_registry + def __init__(self, entities, session=None): self.session = session self._polymorphic_adapters = {} @@ -111,90 +109,108 @@ class Query(object): if entity_wrapper is None: entity_wrapper = _QueryEntity self._entities = [] + self._primary_entity = None for ent in util.to_list(entities): entity_wrapper(self, ent) - self._setup_aliasizers(self._entities) + self._set_entity_selectables(self._entities) - def _setup_aliasizers(self, entities): - if hasattr(self, '_mapper_adapter_map'): - # usually safe to share a single map, but copying to prevent - # subtle leaks if end-user is reusing base query with arbitrary - # number of aliased() objects - self._mapper_adapter_map = d = self._mapper_adapter_map.copy() - else: - self._mapper_adapter_map = d = {} + def _set_entity_selectables(self, entities): + self._mapper_adapter_map = d = self._mapper_adapter_map.copy() for ent in entities: for entity in ent.entities: if entity not in d: - mapper, selectable, is_aliased_class = \ - _entity_info(entity) - if not is_aliased_class and mapper.with_polymorphic: - with_polymorphic = mapper._with_polymorphic_mappers - if mapper.mapped_table not in \ + ext_info = inspect(entity) + if not ext_info.is_aliased_class and \ + ext_info.mapper.with_polymorphic: + if ext_info.mapper.mapped_table not in \ self._polymorphic_adapters: - self._mapper_loads_polymorphically_with(mapper, + self._mapper_loads_polymorphically_with( + ext_info.mapper, sql_util.ColumnAdapter( - selectable, - mapper._equivalent_columns)) - adapter = None - elif is_aliased_class: - adapter = sql_util.ColumnAdapter( - selectable, - mapper._equivalent_columns) - with_polymorphic = None + ext_info.selectable, + ext_info.mapper._equivalent_columns + ) + ) + aliased_adapter = None + elif ext_info.is_aliased_class: + aliased_adapter = sql_util.ColumnAdapter( + ext_info.selectable, + ext_info.mapper._equivalent_columns + ) else: - with_polymorphic = adapter = None + aliased_adapter = None - d[entity] = (mapper, adapter, selectable, - is_aliased_class, with_polymorphic) - ent.setup_entity(entity, *d[entity]) + d[entity] = ( + ext_info, + aliased_adapter + ) + ent.setup_entity(*d[entity]) def _mapper_loads_polymorphically_with(self, mapper, adapter): - for m2 in mapper._with_polymorphic_mappers: + for m2 in mapper._with_polymorphic_mappers or [mapper]: self._polymorphic_adapters[m2] = adapter for m in m2.iterate_to_root(): - self._polymorphic_adapters[m.mapped_table] = \ - self._polymorphic_adapters[m.local_table] = \ - adapter - - def _set_select_from(self, *obj): + self._polymorphic_adapters[m.local_table] = adapter + def _set_select_from(self, obj, set_base_alias): fa = [] + select_from_alias = None + for from_obj in obj: - if isinstance(from_obj, expression._SelectBase): - from_obj = from_obj.alias() - fa.append(from_obj) + info = inspect(from_obj) + + if hasattr(info, 'mapper') and \ + (info.is_mapper or info.is_aliased_class): + if set_base_alias: + raise sa_exc.ArgumentError( + "A selectable (FromClause) instance is " + "expected when the base alias is being set.") + fa.append(info.selectable) + elif not info.is_selectable: + raise sa_exc.ArgumentError( + "argument is not a mapped class, mapper, " + "aliased(), or FromClause instance.") + else: + if isinstance(from_obj, expression.SelectBase): + from_obj = from_obj.alias() + if set_base_alias: + select_from_alias = from_obj + fa.append(from_obj) self._from_obj = tuple(fa) - if len(self._from_obj) == 1 and \ - isinstance(self._from_obj[0], expression.Alias): + if set_base_alias and \ + len(self._from_obj) == 1 and \ + isinstance(select_from_alias, expression.Alias): equivs = self.__all_equivs() self._from_obj_alias = sql_util.ColumnAdapter( self._from_obj[0], equivs) - def _reset_polymorphic_adapter(self, mapper): for m2 in mapper._with_polymorphic_mappers: self._polymorphic_adapters.pop(m2, None) for m in m2.iterate_to_root(): - self._polymorphic_adapters.pop(m.mapped_table, None) self._polymorphic_adapters.pop(m.local_table, None) - def __adapt_polymorphic_element(self, element): + def _adapt_polymorphic_element(self, element): + if "parententity" in element._annotations: + search = element._annotations['parententity'] + alias = self._polymorphic_adapters.get(search, None) + if alias: + return alias.adapt_clause(element) + if isinstance(element, expression.FromClause): search = element elif hasattr(element, 'table'): search = element.table else: - search = None + return None - if search is not None: - alias = self._polymorphic_adapters.get(search, None) - if alias: - return alias.adapt_clause(element) + alias = self._polymorphic_adapters.get(search, None) + if alias: + return alias.adapt_clause(element) def _adapt_col_list(self, cols): return [ @@ -209,8 +225,8 @@ class Query(object): self._orm_only_adapt = False def _adapt_clause(self, clause, as_filter, orm_only): - """Adapt incoming clauses to transformations which have been applied - within this query.""" + """Adapt incoming clauses to transformations which + have been applied within this query.""" adapters = [] @@ -241,7 +257,7 @@ class Query(object): if self._polymorphic_adapters: adapters.append( ( - orm_only, self.__adapt_polymorphic_element + orm_only, self._adapt_polymorphic_element ) ) @@ -273,14 +289,10 @@ class Query(object): return self._select_from_entity or \ self._entity_zero().entity_zero - @property def _mapper_entities(self): - # TODO: this is wrong, its hardcoded to "primary entity" when - # for the case of __all_equivs() it should not be - # the name of this accessor is wrong too for ent in self._entities: - if hasattr(ent, 'primary_entity'): + if isinstance(ent, _MapperEntity): yield ent def _joinpoint_zero(self): @@ -290,39 +302,36 @@ class Query(object): ) def _mapper_zero_or_none(self): - if not getattr(self._entities[0], 'primary_entity', False): + if self._primary_entity: + return self._primary_entity.mapper + else: return None - return self._entities[0].mapper def _only_mapper_zero(self, rationale=None): if len(self._entities) > 1: raise sa_exc.InvalidRequestError( rationale or - "This operation requires a Query against a single mapper." + "This operation requires a Query " + "against a single mapper." ) return self._mapper_zero() def _only_full_mapper_zero(self, methname): - if len(self._entities) != 1: + if self._entities != [self._primary_entity]: raise sa_exc.InvalidRequestError( "%s() can only be used against " "a single mapped class." % methname) - entity = self._entity_zero() - if not hasattr(entity, 'primary_entity'): - raise sa_exc.InvalidRequestError( - "%s() can only be used against " - "a single mapped class." % methname) - return entity.entity_zero + return self._primary_entity.entity_zero def _only_entity_zero(self, rationale=None): if len(self._entities) > 1: raise sa_exc.InvalidRequestError( rationale or - "This operation requires a Query against a single mapper." + "This operation requires a Query " + "against a single mapper." ) return self._entity_zero() - def __all_equivs(self): equivs = {} for ent in self._mapper_entities: @@ -373,7 +382,8 @@ class Query(object): "Query.%s() being called on a Query which already has LIMIT " "or OFFSET applied. To modify the row-limited results of a " " Query, call from_self() first. " - "Otherwise, call %s() before limit() or offset() are applied." + "Otherwise, call %s() before limit() or offset() " + "are applied." % (meth, meth) ) @@ -427,47 +437,63 @@ class Query(object): statement if self._params: stmt = stmt.params(self._params) + + # TODO: there's no tests covering effects of # the annotation not being there return stmt._annotate({'no_replacement_traverse': True}) - def subquery(self, name=None): - """return the full SELECT statement represented by this :class:`.Query`, - embedded within an :class:`.Alias`. + def subquery(self, name=None, with_labels=False, reduce_columns=False): + """return the full SELECT statement represented by + this :class:`.Query`, embedded within an :class:`.Alias`. Eager JOIN generation within the query is disabled. - The statement will not have disambiguating labels - applied to the list of selected columns unless the - :meth:`.Query.with_labels` method is used to generate a new - :class:`.Query` with the option enabled. - :param name: string name to be assigned as the alias; this is passed through to :meth:`.FromClause.alias`. If ``None``, a name will be deterministically generated at compile time. + :param with_labels: if True, :meth:`.with_labels` will be called + on the :class:`.Query` first to apply table-qualified labels + to all columns. + + :param reduce_columns: if True, :meth:`.Select.reduce_columns` will + be called on the resulting :func:`.select` construct, + to remove same-named columns where one also refers to the other + via foreign key or WHERE clause equivalence. + + .. versionchanged:: 0.8 the ``with_labels`` and ``reduce_columns`` + keyword arguments were added. """ - return self.enable_eagerloads(False).statement.alias(name=name) + q = self.enable_eagerloads(False) + if with_labels: + q = q.with_labels() + q = q.statement + if reduce_columns: + q = q.reduce_columns() + return q.alias(name=name) def cte(self, name=None, recursive=False): - """Return the full SELECT statement represented by this :class:`.Query` - represented as a common table expression (CTE). + """Return the full SELECT statement represented by this + :class:`.Query` represented as a common table expression (CTE). .. versionadded:: 0.7.6 Parameters and usage are the same as those of the - :meth:`._SelectBase.cte` method; see that method for + :meth:`.SelectBase.cte` method; see that method for further details. Here is the `Postgresql WITH - RECURSIVE example `_. - Note that, in this example, the ``included_parts`` cte and the ``incl_alias`` alias - of it are Core selectables, which - means the columns are accessed via the ``.c.`` attribute. The ``parts_alias`` - object is an :func:`.orm.aliased` instance of the ``Part`` entity, so column-mapped - attributes are available directly:: + RECURSIVE example + `_. + Note that, in this example, the ``included_parts`` cte and the + ``incl_alias`` alias of it are Core selectables, which + means the columns are accessed via the ``.c.`` attribute. The + ``parts_alias`` object is an :func:`.orm.aliased` instance of the + ``Part`` entity, so column-mapped attributes are available + directly:: from sqlalchemy.orm import aliased @@ -478,11 +504,11 @@ class Query(object): quantity = Column(Integer) included_parts = session.query( - Part.sub_part, - Part.part, - Part.quantity).\\ - filter(Part.part=="our part").\\ - cte(name="included_parts", recursive=True) + Part.sub_part, + Part.part, + Part.quantity).\\ + filter(Part.part=="our part").\\ + cte(name="included_parts", recursive=True) incl_alias = aliased(included_parts, name="pr") parts_alias = aliased(Part, name="p") @@ -496,22 +522,25 @@ class Query(object): q = session.query( included_parts.c.sub_part, - func.sum(included_parts.c.quantity).label('total_quantity') + func.sum(included_parts.c.quantity). + label('total_quantity') ).\\ group_by(included_parts.c.sub_part) See also: - :meth:`._SelectBase.cte` + :meth:`.SelectBase.cte` """ - return self.enable_eagerloads(False).statement.cte(name=name, recursive=recursive) + return self.enable_eagerloads(False).\ + statement.cte(name=name, recursive=recursive) def label(self, name): - """Return the full SELECT statement represented by this :class:`.Query`, converted + """Return the full SELECT statement represented by this + :class:`.Query`, converted to a scalar subquery with a label of the given name. - Analogous to :meth:`sqlalchemy.sql._SelectBaseMixin.label`. + Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.label`. .. versionadded:: 0.6.5 @@ -519,12 +548,11 @@ class Query(object): return self.enable_eagerloads(False).statement.label(name) - def as_scalar(self): - """Return the full SELECT statement represented by this :class:`.Query`, converted - to a scalar subquery. + """Return the full SELECT statement represented by this + :class:`.Query`, converted to a scalar subquery. - Analogous to :meth:`sqlalchemy.sql._SelectBaseMixin.as_scalar`. + Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.as_scalar`. .. versionadded:: 0.6.5 @@ -532,6 +560,16 @@ class Query(object): return self.enable_eagerloads(False).statement.as_scalar() + @property + def selectable(self): + """Return the :class:`.Select` object emitted by this :class:`.Query`. + + Used for :func:`.inspect` compatibility, this is equivalent to:: + + query.enable_eagerloads(False).with_labels().statement + + """ + return self.__clause_element__() def __clause_element__(self): return self.enable_eagerloads(False).with_labels().statement @@ -594,7 +632,8 @@ class Query(object): @property def whereclause(self): - """A readonly attribute which returns the current WHERE criterion for this Query. + """A readonly attribute which returns the current WHERE criterion for + this Query. This returned value is a SQL expression construct, or ``None`` if no criterion has been established. @@ -617,39 +656,34 @@ class Query(object): @_generative(_no_clauseelement_condition) def with_polymorphic(self, cls_or_mappers, - selectable=None, discriminator=None): - """Load columns for descendant mappers of this Query's mapper. + selectable=None, + polymorphic_on=None): + """Load columns for inheriting classes. - Using this method will ensure that each descendant mapper's - tables are included in the FROM clause, and will allow filter() - criterion to be used against those tables. The resulting - instances will also have those columns already loaded so that - no "post fetch" of those columns will be required. + :meth:`.Query.with_polymorphic` applies transformations + to the "main" mapped class represented by this :class:`.Query`. + The "main" mapped class here means the :class:`.Query` + object's first argument is a full class, i.e. + ``session.query(SomeClass)``. These transformations allow additional + tables to be present in the FROM clause so that columns for a + joined-inheritance subclass are available in the query, both for the + purposes of load-time efficiency as well as the ability to use + these columns at query time. - :param cls_or_mappers: a single class or mapper, or list of - class/mappers, which inherit from this Query's mapper. - Alternatively, it may also be the string ``'*'``, in which case - all descending mappers will be added to the FROM clause. + See the documentation section :ref:`with_polymorphic` for + details on how this method is used. - :param selectable: a table or select() statement that will - be used in place of the generated FROM clause. This argument is - required if any of the desired mappers use concrete table - inheritance, since SQLAlchemy currently cannot generate UNIONs - among tables automatically. If used, the ``selectable`` argument - must represent the full set of tables and columns mapped by every - desired mapper. Otherwise, the unaccounted mapped columns will - result in their table being appended directly to the FROM clause - which will usually lead to incorrect results. - - :param discriminator: a column to be used as the "discriminator" - column for the given selectable. If not given, the polymorphic_on - attribute of the mapper will be used, if any. This is useful for - mappers that don't have polymorphic loading behavior by default, - such as concrete table mappers. + .. versionchanged:: 0.8 + A new and more flexible function + :func:`.orm.with_polymorphic` supersedes + :meth:`.Query.with_polymorphic`, as it can apply the equivalent + functionality to any set of columns or classes in the + :class:`.Query`, not just the "zero mapper". See that + function for a description of arguments. """ - if not getattr(self._entities[0], 'primary_entity', False): + if not self._primary_entity: raise sa_exc.InvalidRequestError( "No primary mapper set up for this Query.") entity = self._entities[0]._clone() @@ -657,7 +691,7 @@ class Query(object): entity.set_with_polymorphic(self, cls_or_mappers, selectable=selectable, - discriminator=discriminator) + polymorphic_on=polymorphic_on) @_generative() def yield_per(self, count): @@ -674,17 +708,17 @@ class Query(object): loading, the full result for all rows is fetched which generally defeats the purpose of :meth:`~sqlalchemy.orm.query.Query.yield_per`. - Also note that many DBAPIs do not "stream" results, pre-buffering - all rows before making them available, including mysql-python and - psycopg2. :meth:`~sqlalchemy.orm.query.Query.yield_per` will also - set the ``stream_results`` execution - option to ``True``, which currently is only understood by psycopg2 - and causes server side cursors to be used. + Also note that while :meth:`~sqlalchemy.orm.query.Query.yield_per` + will set the ``stream_results`` execution option to True, currently + this is only understood by :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect + which will stream results using server side cursors instead of pre-buffer + all rows for this query. Other DBAPIs pre-buffer all rows before + making them available. """ self._yield_per = count - self._execution_options = self._execution_options.copy() - self._execution_options['stream_results'] = True + self._execution_options = self._execution_options.union( + {"stream_results": True}) def get(self, ident): """Return an instance based on the given primary key identifier, @@ -728,7 +762,7 @@ class Query(object): foreign-key-to-primary-key criterion, will also use an operation equivalent to :meth:`~.Query.get` in order to retrieve the target value from the local identity map - before querying the database. See :ref:`loading_toplevel` + before querying the database. See :doc:`/orm/loading` for further details on relationship loading. :param ident: A scalar or tuple value representing @@ -762,9 +796,10 @@ class Query(object): if not self._populate_existing and \ not mapper.always_refresh and \ - self._lockmode is None: + self._for_update_arg is None: - instance = self._get_from_identity(self.session, key, False) + instance = loading.get_from_identity( + self.session, key, attributes.PASSIVE_OFF) if instance is not None: # reject calls for id in identity map but class # mismatch. @@ -772,7 +807,7 @@ class Query(object): return None return instance - return self._load_on_ident(key) + return loading.load_on_ident(self, key) @_generative() def correlate(self, *args): @@ -796,7 +831,8 @@ class Query(object): """ self._correlate = self._correlate.union( - _orm_selectable(s) + _interpret_as_from(s) + if s is not None else None for s in args) @_generative() @@ -852,11 +888,10 @@ class Query(object): """ if property is None: - from sqlalchemy.orm import properties mapper = object_mapper(instance) for prop in mapper.iterate_properties: - if isinstance(prop, properties.PropertyLoader) and \ + if isinstance(prop, properties.RelationshipProperty) and \ prop.mapper is self._mapper_zero(): property = prop break @@ -881,11 +916,11 @@ class Query(object): self._entities = list(self._entities) m = _MapperEntity(self, entity) - self._setup_aliasizers([m]) + self._set_entity_selectables([m]) @_generative() def with_session(self, session): - """Return a :class:`Query` that will use the given :class:`.Session`. + """Return a :class:`.Query` that will use the given :class:`.Session`. """ @@ -922,7 +957,7 @@ class Query(object): '_prefixes', ): self.__dict__.pop(attr, None) - self._set_select_from(fromclause) + self._set_select_from([fromclause], True) # this enables clause adaptation for non-ORM # expressions. @@ -950,18 +985,14 @@ class Query(object): """Return a scalar result corresponding to the given column expression.""" try: - # Py3K - #return self.values(column).__next__()[0] - # Py2K - return self.values(column).next()[0] - # end Py2K + return next(self.values(column))[0] except StopIteration: return None @_generative() def with_entities(self, *entities): - """Return a new :class:`.Query` replacing the SELECT list with the given - entities. + """Return a new :class:`.Query` replacing the SELECT list with the + given entities. e.g.:: @@ -986,7 +1017,6 @@ class Query(object): """ self._set_entities(entities) - @_generative() def add_columns(self, *column): """Add one or more column expressions to the list @@ -998,19 +1028,19 @@ class Query(object): _ColumnEntity(self, c) # _ColumnEntity may add many entities if the # given arg is a FROM clause - self._setup_aliasizers(self._entities[l:]) + self._set_entity_selectables(self._entities[l:]) @util.pending_deprecation("0.7", ":meth:`.add_column` is superseded by :meth:`.add_columns`", False) def add_column(self, column): - """Add a column expression to the list of result columns to be returned. + """Add a column expression to the list of result columns to be + returned. Pending deprecation: :meth:`.add_column` will be superseded by :meth:`.add_columns`. """ - return self.add_columns(column) def options(self, *args): @@ -1019,7 +1049,7 @@ class Query(object): Most supplied options regard changing how column- and relationship-mapped attributes are loaded. See the sections - :ref:`deferred` and :ref:`loading_toplevel` for reference + :ref:`deferred` and :doc:`/orm/loading` for reference documentation. """ @@ -1074,7 +1104,7 @@ class Query(object): :class:`.Table`, :class:`.Alias`, or ORM entity / mapped class /etc. """ - mapper, selectable, is_aliased_class = _entity_info(selectable) + selectable = inspect(selectable).selectable self._with_hints += ((selectable, text, dialect_name),) @@ -1094,32 +1124,63 @@ class Query(object): @_generative() def with_lockmode(self, mode): - """Return a new Query object with the specified locking mode. + """Return a new :class:`.Query` object with the specified "locking mode", + which essentially refers to the ``FOR UPDATE`` clause. - :param mode: a string representing the desired locking mode. A - corresponding value is passed to the ``for_update`` parameter of - :meth:`~sqlalchemy.sql.expression.select` when the query is - executed. Valid values are: + .. deprecated:: 0.9.0 superseded by :meth:`.Query.with_for_update`. - ``'update'`` - passes ``for_update=True``, which translates to - ``FOR UPDATE`` (standard SQL, supported by most dialects) + :param mode: a string representing the desired locking mode. + Valid values are: - ``'update_nowait'`` - passes ``for_update='nowait'``, which - translates to ``FOR UPDATE NOWAIT`` (supported by Oracle, - PostgreSQL 8.1 upwards) + * ``None`` - translates to no lockmode - ``'read'`` - passes ``for_update='read'``, which translates to - ``LOCK IN SHARE MODE`` (for MySQL), and ``FOR SHARE`` (for - PostgreSQL) + * ``'update'`` - translates to ``FOR UPDATE`` + (standard SQL, supported by most dialects) - ``'read_nowait'`` - passes ``for_update='read_nowait'``, which - translates to ``FOR SHARE NOWAIT`` (supported by PostgreSQL). + * ``'update_nowait'`` - translates to ``FOR UPDATE NOWAIT`` + (supported by Oracle, PostgreSQL 8.1 upwards) + + * ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL), + and ``FOR SHARE`` (for PostgreSQL) + + .. seealso:: + + :meth:`.Query.with_for_update` - improved API for + specifying the ``FOR UPDATE`` clause. - .. versionadded:: 0.7.7 - ``FOR SHARE`` and ``FOR SHARE NOWAIT`` (PostgreSQL). """ + self._for_update_arg = LockmodeArg.parse_legacy_query(mode) - self._lockmode = mode + @_generative() + def with_for_update(self, read=False, nowait=False, of=None): + """return a new :class:`.Query` with the specified options for the + ``FOR UPDATE`` clause. + + The behavior of this method is identical to that of + :meth:`.SelectBase.with_for_update`. When called with no arguments, + the resulting ``SELECT`` statement will have a ``FOR UPDATE`` clause + appended. When additional arguments are specified, backend-specific + options such as ``FOR UPDATE NOWAIT`` or ``LOCK IN SHARE MODE`` + can take effect. + + E.g.:: + + q = sess.query(User).with_for_update(nowait=True, of=User) + + The above query on a Postgresql backend will render like:: + + SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT + + .. versionadded:: 0.9.0 :meth:`.Query.with_for_update` supersedes + the :meth:`.Query.with_lockmode` method. + + .. seealso:: + + :meth:`.GenerativeSelect.with_for_update` - Core level method with + full argument and behavioral description. + + """ + self._for_update_arg = LockmodeArg(read=read, nowait=nowait, of=of) @_generative() def params(self, *args, **kwargs): @@ -1168,14 +1229,7 @@ class Query(object): """ for criterion in list(criterion): - if isinstance(criterion, basestring): - criterion = sql.text(criterion) - - if criterion is not None and \ - not isinstance(criterion, sql.ClauseElement): - raise sa_exc.ArgumentError( - "filter() argument must be of type " - "sqlalchemy.sql.ClauseElement or string") + criterion = expression._literal_as_text(criterion) criterion = self._adapt_clause(criterion, True, True) @@ -1208,7 +1262,7 @@ class Query(object): """ clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value - for key, value in kwargs.iteritems()] + for key, value in kwargs.items()] return self.filter(sql.and_(*clauses)) @_generative(_no_statement_condition, _no_limit_offset) @@ -1249,7 +1303,6 @@ class Query(object): the newly resulting :class:`.Query`""" criterion = list(chain(*[_orm_columns(c) for c in criterion])) - criterion = self._adapt_col_list(criterion) if self._group_by is False: @@ -1262,7 +1315,7 @@ class Query(object): """apply a HAVING criterion to the query and return the newly resulting :class:`.Query`. - :meth:`having` is used in conjunction with :meth:`group_by`. + :meth:`~.Query.having` is used in conjunction with :meth:`~.Query.group_by`. HAVING criterion makes it possible to use filters on aggregate functions like COUNT, SUM, AVG, MAX, and MIN, eg.:: @@ -1274,7 +1327,7 @@ class Query(object): """ - if isinstance(criterion, basestring): + if isinstance(criterion, util.string_types): criterion = sql.text(criterion) if criterion is not None and \ @@ -1328,9 +1381,8 @@ class Query(object): """ - return self._from_selectable( - expression.union(*([self]+ list(q)))) + expression.union(*([self] + list(q)))) def union_all(self, *q): """Produce a UNION ALL of this Query against one or more queries. @@ -1340,7 +1392,7 @@ class Query(object): """ return self._from_selectable( - expression.union_all(*([self]+ list(q))) + expression.union_all(*([self] + list(q))) ) def intersect(self, *q): @@ -1351,7 +1403,7 @@ class Query(object): """ return self._from_selectable( - expression.intersect(*([self]+ list(q))) + expression.intersect(*([self] + list(q))) ) def intersect_all(self, *q): @@ -1362,7 +1414,7 @@ class Query(object): """ return self._from_selectable( - expression.intersect_all(*([self]+ list(q))) + expression.intersect_all(*([self] + list(q))) ) def except_(self, *q): @@ -1373,7 +1425,7 @@ class Query(object): """ return self._from_selectable( - expression.except_(*([self]+ list(q))) + expression.except_(*([self] + list(q))) ) def except_all(self, *q): @@ -1384,7 +1436,7 @@ class Query(object): """ return self._from_selectable( - expression.except_all(*([self]+ list(q))) + expression.except_all(*([self] + list(q))) ) def join(self, *props, **kwargs): @@ -1410,9 +1462,9 @@ class Query(object): In the above example we refer to ``User.addresses`` as passed to :meth:`~.Query.join` as the *on clause*, that is, it indicates how the "ON" portion of the JOIN should be constructed. For a - single-entity query such as the one above (i.e. we start by selecting only from - ``User`` and nothing else), the relationship can also be specified by its - string name:: + single-entity query such as the one above (i.e. we start by selecting + only from ``User`` and nothing else), the relationship can also be + specified by its string name:: q = session.query(User).join("addresses") @@ -1422,8 +1474,9 @@ class Query(object): q = session.query(User).join("orders", "items", "keywords") - The above would be shorthand for three separate calls to :meth:`~.Query.join`, - each using an explicit attribute to indicate the source entity:: + The above would be shorthand for three separate calls to + :meth:`~.Query.join`, each using an explicit attribute to indicate + the source entity:: q = session.query(User).\\ join(User.orders).\\ @@ -1440,7 +1493,7 @@ class Query(object): q = session.query(User).join(Address) - The above calling form of :meth:`.join` will raise an error if + The above calling form of :meth:`~.Query.join` will raise an error if either there are no foreign keys between the two entities, or if there are multiple foreign key linkages between them. In the above calling form, :meth:`~.Query.join` is called upon to @@ -1499,25 +1552,26 @@ class Query(object): There is a lot of flexibility in what the "target" can be when using :meth:`~.Query.join`. As noted previously, it also accepts - :class:`.Table` constructs and other selectables such as :func:`.alias` - and :func:`.select` constructs, with either the one or two-argument forms:: + :class:`.Table` constructs and other selectables such as + :func:`.alias` and :func:`.select` constructs, with either the one + or two-argument forms:: addresses_q = select([Address.user_id]).\\ - where(Address.email_address.endswith("@bar.com")).\\ - alias() + where(Address.email_address.endswith("@bar.com")).\\ + alias() q = session.query(User).\\ join(addresses_q, addresses_q.c.user_id==User.id) :meth:`~.Query.join` also features the ability to *adapt* a - :meth:`~sqlalchemy.orm.relationship` -driven ON clause to the target selectable. - Below we construct a JOIN from ``User`` to a subquery against ``Address``, allowing - the relationship denoted by ``User.addresses`` to *adapt* itself - to the altered target:: + :meth:`~sqlalchemy.orm.relationship` -driven ON clause to the target + selectable. Below we construct a JOIN from ``User`` to a subquery + against ``Address``, allowing the relationship denoted by + ``User.addresses`` to *adapt* itself to the altered target:: address_subq = session.query(Address).\\ - filter(Address.email_address == 'ed@foo.com').\\ - subquery() + filter(Address.email_address == 'ed@foo.com').\\ + subquery() q = session.query(User).join(address_subq, User.addresses) @@ -1601,14 +1655,14 @@ class Query(object): example :ref:`examples_xmlpersistence` which illustrates an XPath-like query system using algorithmic joins. - :param *props: A collection of one or more join conditions, + :param \*props: A collection of one or more join conditions, each consisting of a relationship-bound attribute or string relationship name representing an "on clause", or a single target entity, or a tuple in the form of ``(target, onclause)``. A special two-argument calling form of the form ``target, onclause`` is also accepted. :param aliased=False: If True, indicate that the JOIN target should be - anonymously aliased. Subsequent calls to :class:`~.Query.filter` + anonymously aliased. Subsequent calls to :meth:`~.Query.filter` and similar will adapt the incoming criterion to the target alias, until :meth:`~.Query.reset_joinpoint` is called. :param from_joinpoint=False: When using ``aliased=True``, a setting @@ -1632,7 +1686,7 @@ class Query(object): kwargs.pop('from_joinpoint', False) if kwargs: raise TypeError("unknown arguments: %s" % - ','.join(kwargs.iterkeys())) + ','.join(kwargs.keys)) return self._join(props, outerjoin=False, create_aliases=aliased, from_joinpoint=from_joinpoint) @@ -1648,7 +1702,7 @@ class Query(object): kwargs.pop('from_joinpoint', False) if kwargs: raise TypeError("unknown arguments: %s" % - ','.join(kwargs.iterkeys())) + ','.join(kwargs)) return self._join(props, outerjoin=True, create_aliases=aliased, from_joinpoint=from_joinpoint) @@ -1678,7 +1732,7 @@ class Query(object): if len(keys) == 2 and \ isinstance(keys[0], (expression.FromClause, type, AliasedClass)) and \ - isinstance(keys[1], (basestring, expression.ClauseElement, + isinstance(keys[1], (str, expression.ClauseElement, interfaces.PropComparator)): # detect 2-arg form of join and # convert to a tuple. @@ -1698,14 +1752,14 @@ class Query(object): # is a little bit of legacy behavior still at work here # which means they might be in either order. may possibly # lock this down to (right_entity, onclause) in 0.6. - if isinstance(arg1, (interfaces.PropComparator, basestring)): + if isinstance(arg1, (interfaces.PropComparator, util.string_types)): right_entity, onclause = arg2, arg1 else: right_entity, onclause = arg1, arg2 left_entity = prop = None - if isinstance(onclause, basestring): + if isinstance(onclause, util.string_types): left_entity = self._joinpoint_zero() descriptor = _entity_descriptor(left_entity, onclause) @@ -1715,10 +1769,14 @@ class Query(object): # and Class is that of the current joinpoint elif from_joinpoint and \ isinstance(onclause, interfaces.PropComparator): - left_entity = onclause.parententity + left_entity = onclause._parententity + info = inspect(self._joinpoint_zero()) left_mapper, left_selectable, left_is_aliased = \ - _entity_info(self._joinpoint_zero()) + getattr(info, 'mapper', None), \ + info.selectable, \ + getattr(info, 'is_aliased_class', None) + if left_mapper is left_entity: left_entity = self._joinpoint_zero() descriptor = _entity_descriptor(left_entity, @@ -1734,7 +1792,7 @@ class Query(object): else: right_entity = onclause.property.mapper - left_entity = onclause.parententity + left_entity = onclause._parententity prop = onclause.property if not isinstance(onclause, attributes.QueryableAttribute): @@ -1766,6 +1824,7 @@ class Query(object): right_entity, onclause, outerjoin, create_aliases, prop) + def _join_left_to_right(self, left, right, onclause, outerjoin, create_aliases, prop): """append a JOIN to the query's from clause.""" @@ -1783,35 +1842,63 @@ class Query(object): raise sa_exc.InvalidRequestError( "Can't construct a join from %s to %s, they " "are the same entity" % - (left, right)) + (left, right)) - right, right_is_aliased, onclause = self._prepare_right_side( - right, onclause, - outerjoin, create_aliases, - prop) + l_info = inspect(left) + r_info = inspect(right) + + + overlap = False + if not create_aliases: + right_mapper = getattr(r_info, "mapper", None) + # if the target is a joined inheritance mapping, + # be more liberal about auto-aliasing. + if right_mapper and ( + right_mapper.with_polymorphic or + isinstance(right_mapper.mapped_table, expression.Join) + ): + for from_obj in self._from_obj or [l_info.selectable]: + if sql_util.selectables_overlap(l_info.selectable, from_obj) and \ + sql_util.selectables_overlap(from_obj, r_info.selectable): + overlap = True + break + elif sql_util.selectables_overlap(l_info.selectable, r_info.selectable): + overlap = True + + + if overlap and l_info.selectable is r_info.selectable: + raise sa_exc.InvalidRequestError( + "Can't join table/selectable '%s' to itself" % + l_info.selectable) + + right, onclause = self._prepare_right_side( + r_info, right, onclause, + create_aliases, + prop, overlap) # if joining on a MapperProperty path, # track the path to prevent redundant joins if not create_aliases and prop: self._update_joinpoint({ - '_joinpoint_entity':right, - 'prev':((left, right, prop.key), self._joinpoint) + '_joinpoint_entity': right, + 'prev': ((left, right, prop.key), self._joinpoint) }) else: - self._joinpoint = { - '_joinpoint_entity':right - } + self._joinpoint = {'_joinpoint_entity': right} - self._join_to_left(left, right, - right_is_aliased, - onclause, outerjoin) + self._join_to_left(l_info, left, right, onclause, outerjoin) - def _prepare_right_side(self, right, onclause, outerjoin, - create_aliases, prop): - right_mapper, right_selectable, right_is_aliased = _entity_info(right) + def _prepare_right_side(self, r_info, right, onclause, create_aliases, + prop, overlap): + info = r_info + + right_mapper, right_selectable, right_is_aliased = \ + getattr(info, 'mapper', None), \ + info.selectable, \ + getattr(info, 'is_aliased_class', False) if right_mapper: - self._join_entities += (right, ) + self._join_entities += (info, ) if right_mapper and prop and \ not right_mapper.common_parent(prop.mapper): @@ -1833,23 +1920,27 @@ class Query(object): (right_selectable.description, right_mapper.mapped_table.description)) - if not isinstance(right_selectable, expression.Alias): + if isinstance(right_selectable, expression.SelectBase): + # TODO: this isn't even covered now! right_selectable = right_selectable.alias() + need_adapter = True right = aliased(right_mapper, right_selectable) - need_adapter = True aliased_entity = right_mapper and \ not right_is_aliased and \ ( - right_mapper.with_polymorphic or - isinstance( - right_mapper.mapped_table, - expression.Join) + right_mapper.with_polymorphic and isinstance( + right_mapper._with_polymorphic_selectable, + expression.Alias) + or + overlap # test for overlap: + # orm/inheritance/relationships.py + # SelfReferentialM2MTest ) if not need_adapter and (create_aliases or aliased_entity): - right = aliased(right) + right = aliased(right, flat=True) need_adapter = True # if an alias() of the right side was generated here, @@ -1879,44 +1970,23 @@ class Query(object): ) ) - return right, right_is_aliased, onclause + return right, onclause - def _join_to_left(self, left, right, right_is_aliased, onclause, outerjoin): - left_mapper, left_selectable, left_is_aliased = _entity_info(left) + def _join_to_left(self, l_info, left, right, onclause, outerjoin): + info = l_info + left_mapper = getattr(info, 'mapper', None) + left_selectable = info.selectable - # this is an overly broad assumption here, but there's a - # very wide variety of situations where we rely upon orm.join's - # adaption to glue clauses together, with joined-table inheritance's - # wide array of variables taking up most of the space. - # Setting the flag here is still a guess, so it is a bug - # that we don't have definitive criterion to determine when - # adaption should be enabled (or perhaps that we're even doing the - # whole thing the way we are here). - join_to_left = not right_is_aliased and not left_is_aliased - - if self._from_obj and left_selectable is not None: + if self._from_obj: replace_clause_index, clause = sql_util.find_join_source( self._from_obj, left_selectable) if clause is not None: - # the entire query's FROM clause is an alias of itself (i.e. - # from_self(), similar). if the left clause is that one, - # ensure it adapts to the left side. - if self._from_obj_alias and clause is self._from_obj[0]: - join_to_left = True - - # An exception case where adaption to the left edge is not - # desirable. See above note on join_to_left. - if join_to_left and isinstance(clause, expression.Join) and \ - sql_util.clause_is_present(left_selectable, clause): - join_to_left = False - try: clause = orm_join(clause, right, - onclause, isouter=outerjoin, - join_to_left=join_to_left) - except sa_exc.ArgumentError, ae: + onclause, isouter=outerjoin) + except sa_exc.ArgumentError as ae: raise sa_exc.InvalidRequestError( "Could not find a FROM clause to join from. " "Tried joining to %s, but got: %s" % (right, ae)) @@ -1934,23 +2004,16 @@ class Query(object): break else: clause = left - elif left_selectable is not None: - clause = left_selectable else: - clause = None - - if clause is None: - raise sa_exc.InvalidRequestError( - "Could not find a FROM clause to join from") + clause = left_selectable + assert clause is not None try: - clause = orm_join(clause, right, onclause, - isouter=outerjoin, join_to_left=join_to_left) - except sa_exc.ArgumentError, ae: + clause = orm_join(clause, right, onclause, isouter=outerjoin) + except sa_exc.ArgumentError as ae: raise sa_exc.InvalidRequestError( "Could not find a FROM clause to join from. " "Tried joining to %s, but got: %s" % (right, ae)) - self._from_obj = self._from_obj + (clause,) def _reset_joinpoint(self): @@ -1974,32 +2037,134 @@ class Query(object): def select_from(self, *from_obj): """Set the FROM clause of this :class:`.Query` explicitly. - Sending a mapped class or entity here effectively replaces the + :meth:`.Query.select_from` is often used in conjunction with + :meth:`.Query.join` in order to control which entity is selected + from on the "left" side of the join. + + The entity or selectable object here effectively replaces the "left edge" of any calls to :meth:`~.Query.join`, when no joinpoint is otherwise established - usually, the default "join point" is the leftmost entity in the :class:`~.Query` object's list of entities to be selected. - Mapped entities or plain :class:`~.Table` or other selectables - can be sent here which will form the default FROM clause. + A typical example:: - See the example in :meth:`~.Query.join` for a typical - usage of :meth:`~.Query.select_from`. + q = session.query(Address).select_from(User).\\ + join(User.addresses).\\ + filter(User.name == 'ed') + + Which produces SQL equivalent to:: + + SELECT address.* FROM user + JOIN address ON user.id=address.user_id + WHERE user.name = :name_1 + + :param \*from_obj: collection of one or more entities to apply + to the FROM clause. Entities can be mapped classes, + :class:`.AliasedClass` objects, :class:`.Mapper` objects + as well as core :class:`.FromClause` elements like subqueries. + + .. versionchanged:: 0.9 + This method no longer applies the given FROM object + to be the selectable from which matching entities + select from; the :meth:`.select_entity_from` method + now accomplishes this. See that method for a description + of this behavior. + + .. seealso:: + + :meth:`~.Query.join` + + :meth:`.Query.select_entity_from` """ - obj = [] - for fo in from_obj: - if _is_mapped_class(fo): - mapper, selectable, is_aliased_class = _entity_info(fo) - self._select_from_entity = fo - obj.append(selectable) - elif not isinstance(fo, expression.FromClause): - raise sa_exc.ArgumentError( - "select_from() accepts FromClause objects only.") - else: - obj.append(fo) - self._set_select_from(*obj) + self._set_select_from(from_obj, False) + + @_generative(_no_clauseelement_condition) + def select_entity_from(self, from_obj): + """Set the FROM clause of this :class:`.Query` to a + core selectable, applying it as a replacement FROM clause + for corresponding mapped entities. + + This method is similar to the :meth:`.Query.select_from` + method, in that it sets the FROM clause of the query. However, + where :meth:`.Query.select_from` only affects what is placed + in the FROM, this method also applies the given selectable + to replace the FROM which the selected entities would normally + select from. + + The given ``from_obj`` must be an instance of a :class:`.FromClause`, + e.g. a :func:`.select` or :class:`.Alias` construct. + + An example would be a :class:`.Query` that selects ``User`` entities, + but uses :meth:`.Query.select_entity_from` to have the entities + selected from a :func:`.select` construct instead of the + base ``user`` table:: + + select_stmt = select([User]).where(User.id == 7) + + q = session.query(User).\\ + select_entity_from(select_stmt).\\ + filter(User.name == 'ed') + + The query generated will select ``User`` entities directly + from the given :func:`.select` construct, and will be:: + + SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name + FROM (SELECT "user".id AS id, "user".name AS name + FROM "user" + WHERE "user".id = :id_1) AS anon_1 + WHERE anon_1.name = :name_1 + + Notice above that even the WHERE criterion was "adapted" such that + the ``anon_1`` subquery effectively replaces all references to the + ``user`` table, except for the one that it refers to internally. + + Compare this to :meth:`.Query.select_from`, which as of + version 0.9, does not affect existing entities. The + statement below:: + + q = session.query(User).\\ + select_from(select_stmt).\\ + filter(User.name == 'ed') + + Produces SQL where both the ``user`` table as well as the + ``select_stmt`` construct are present as separate elements + in the FROM clause. No "adaptation" of the ``user`` table + is applied:: + + SELECT "user".id AS user_id, "user".name AS user_name + FROM "user", (SELECT "user".id AS id, "user".name AS name + FROM "user" + WHERE "user".id = :id_1) AS anon_1 + WHERE "user".name = :name_1 + + :meth:`.Query.select_entity_from` maintains an older + behavior of :meth:`.Query.select_from`. In modern usage, + similar results can also be achieved using :func:`.aliased`:: + + select_stmt = select([User]).where(User.id == 7) + user_from_select = aliased(User, select_stmt.alias()) + + q = session.query(user_from_select) + + :param from_obj: a :class:`.FromClause` object that will replace + the FROM clause of this :class:`.Query`. + + .. seealso:: + + :meth:`.Query.select_from` + + .. versionadded:: 0.8 + :meth:`.Query.select_entity_from` was added to specify + the specific behavior of entity replacement, however + the :meth:`.Query.select_from` maintains this behavior + as well until 0.9. + + """ + + self._set_select_from([from_obj], True) def __getitem__(self, item): if isinstance(item, slice): @@ -2025,7 +2190,7 @@ class Query(object): if item == -1: return list(self)[-1] else: - return list(self[item:item+1])[0] + return list(self[item:item + 1])[0] @_generative(_no_statement_condition) def slice(self, start, stop): @@ -2085,7 +2250,7 @@ class Query(object): ``Query``. :param \*prefixes: optional prefixes, typically strings, - not using any commas. In particular is useful for MySQL keywords. + not using any commas. In particular is useful for MySQL keywords. e.g.:: @@ -2126,12 +2291,12 @@ class Query(object): appropriate to the entity class represented by this ``Query``. """ - if isinstance(statement, basestring): + if isinstance(statement, util.string_types): statement = sql.text(statement) if not isinstance(statement, - (expression._TextClause, - expression._SelectBase)): + (expression.TextClause, + expression.SelectBase)): raise sa_exc.ArgumentError( "from_statement accepts text(), select(), " "and union() objects only.") @@ -2235,12 +2400,12 @@ class Query(object): def _execute_and_instances(self, querycontext): conn = self._connection_from_session( - mapper = self._mapper_zero_or_none(), - clause = querycontext.statement, + mapper=self._mapper_zero_or_none(), + clause=querycontext.statement, close_with_result=True) result = conn.execute(querycontext.statement, self._params) - return self.instances(result, querycontext) + return loading.instances(self, result, querycontext) @property def column_descriptions(self): @@ -2280,10 +2445,10 @@ class Query(object): """ return [ { - 'name':ent._label_name, - 'type':ent.type, - 'aliased':getattr(ent, 'is_aliased_class', False), - 'expr':ent.expr + 'name': ent._label_name, + 'type': ent.type, + 'aliased': getattr(ent, 'is_aliased_class', False), + 'expr': ent.expr } for ent in self._entities ] @@ -2298,91 +2463,26 @@ class Query(object): for u in session.query(User).instances(result): print u """ - session = self.session - context = __context if context is None: context = QueryContext(self) - context.runid = _new_runid() - - filter_fns = [ent.filter_fn - for ent in self._entities] - filtered = id in filter_fns - - single_entity = filtered and len(self._entities) == 1 - - if filtered: - if single_entity: - filter_fn = id - else: - def filter_fn(row): - return tuple(fn(x) for x, fn in zip(row, filter_fns)) - - custom_rows = single_entity and \ - self._entities[0].mapper.dispatch.append_result - - (process, labels) = \ - zip(*[ - query_entity.row_processor(self, context, custom_rows) - for query_entity in self._entities - ]) - - - while True: - context.progress = {} - context.partials = {} - - if self._yield_per: - fetch = cursor.fetchmany(self._yield_per) - if not fetch: - break - else: - fetch = cursor.fetchall() - - if custom_rows: - rows = [] - for row in fetch: - process[0](row, rows) - elif single_entity: - rows = [process[0](row, None) for row in fetch] - else: - rows = [util.NamedTuple([proc(row, None) for proc in process], - labels) for row in fetch] - - if filtered: - rows = util.unique_list(rows, filter_fn) - - if context.refresh_state and self._only_load_props \ - and context.refresh_state in context.progress: - context.refresh_state.commit( - context.refresh_state.dict, self._only_load_props) - context.progress.pop(context.refresh_state) - - session._finalize_loaded(context.progress) - - for ii, (dict_, attrs) in context.partials.iteritems(): - ii.commit(dict_, attrs) - - for row in rows: - yield row - - if not self._yield_per: - break + return loading.instances(self, cursor, context) def merge_result(self, iterator, load=True): """Merge a result into this :class:`.Query` object's Session. - Given an iterator returned by a :class:`.Query` of the same structure as this - one, return an identical iterator of results, with all mapped - instances merged into the session using :meth:`.Session.merge`. This is an - optimized method which will merge all mapped instances, preserving the - structure of the result rows and unmapped columns with less method - overhead than that of calling :meth:`.Session.merge` explicitly for each - value. + Given an iterator returned by a :class:`.Query` of the same structure + as this one, return an identical iterator of results, with all mapped + instances merged into the session using :meth:`.Session.merge`. This + is an optimized method which will merge all mapped instances, + preserving the structure of the result rows and unmapped columns with + less method overhead than that of calling :meth:`.Session.merge` + explicitly for each value. The structure of the results is determined based on the column list of - this :class:`.Query` - if these do not correspond, unchecked errors will occur. + this :class:`.Query` - if these do not correspond, unchecked errors + will occur. The 'load' argument is the same as that of :meth:`.Session.merge`. @@ -2393,137 +2493,17 @@ class Query(object): """ - session = self.session - if load: - # flush current contents if we expect to load data - session._autoflush() - - autoflush = session.autoflush - try: - session.autoflush = False - single_entity = len(self._entities) == 1 - if single_entity: - if isinstance(self._entities[0], _MapperEntity): - result = [session._merge( - attributes.instance_state(instance), - attributes.instance_dict(instance), - load=load, _recursive={}) - for instance in iterator] - else: - result = list(iterator) - else: - mapped_entities = [i for i, e in enumerate(self._entities) - if isinstance(e, _MapperEntity)] - result = [] - keys = [ent._label_name for ent in self._entities] - for row in iterator: - newrow = list(row) - for i in mapped_entities: - if newrow[i] is not None: - newrow[i] = session._merge( - attributes.instance_state(newrow[i]), - attributes.instance_dict(newrow[i]), - load=load, _recursive={}) - result.append(util.NamedTuple(newrow, keys)) - - return iter(result) - finally: - session.autoflush = autoflush - - @classmethod - def _get_from_identity(cls, session, key, passive): - """Look up the given key in the given session's identity map, - check the object for expired state if found. - - """ - instance = session.identity_map.get(key) - if instance is not None: - - state = attributes.instance_state(instance) - - # expired - ensure it still exists - if state.expired: - if passive is attributes.PASSIVE_NO_FETCH: - # TODO: no coverage here - return attributes.PASSIVE_NO_RESULT - elif passive is attributes.PASSIVE_NO_FETCH_RELATED: - # this mode is used within a flush and the instance's - # expired state will be checked soon enough, if necessary - return instance - try: - state(passive) - except orm_exc.ObjectDeletedError: - session._remove_newly_deleted(state) - return None - return instance - else: - return None - - def _load_on_ident(self, key, refresh_state=None, lockmode=None, - only_load_props=None): - """Load the given identity key from the database.""" - - lockmode = lockmode or self._lockmode - - if key is not None: - ident = key[1] - else: - ident = None - - if refresh_state is None: - q = self._clone() - q._get_condition() - else: - q = self._clone() - - if ident is not None: - mapper = self._mapper_zero() - - (_get_clause, _get_params) = mapper._get_clause - - # None present in ident - turn those comparisons - # into "IS NULL" - if None in ident: - nones = set([ - _get_params[col].key for col, value in - zip(mapper.primary_key, ident) if value is None - ]) - _get_clause = sql_util.adapt_criterion_to_null( - _get_clause, nones) - - _get_clause = q._adapt_clause(_get_clause, True, False) - q._criterion = _get_clause - - params = dict([ - (_get_params[primary_key].key, id_val) - for id_val, primary_key in zip(ident, mapper.primary_key) - ]) - - q._params = params - - if lockmode is not None: - q._lockmode = lockmode - q._get_options( - populate_existing=bool(refresh_state), - version_check=(lockmode is not None), - only_load_props=only_load_props, - refresh_state=refresh_state) - q._order_by = None - - try: - return q.one() - except orm_exc.NoResultFound: - return None + return loading.merge_result(self, iterator, load) @property def _select_args(self): return { - 'limit':self._limit, - 'offset':self._offset, - 'distinct':self._distinct, - 'prefixes':self._prefixes, - 'group_by':self._group_by or None, - 'having':self._having + 'limit': self._limit, + 'offset': self._offset, + 'distinct': self._distinct, + 'prefixes': self._prefixes, + 'group_by': self._group_by or None, + 'having': self._having } @property @@ -2533,6 +2513,26 @@ class Query(object): kwargs.get('offset') is not None or kwargs.get('distinct', False)) + def exists(self): + """A convenience method that turns a query into an EXISTS subquery + of the form EXISTS (SELECT 1 FROM ... WHERE ...). + + e.g.:: + + q = session.query(User).filter(User.name == 'fred') + session.query(q.exists()) + + Producing SQL similar to:: + + SELECT EXISTS ( + SELECT 1 FROM users WHERE users.name = :name_1 + ) AS anon_1 + + .. versionadded:: 0.8.1 + + """ + return sql.exists(self.with_labels().statement.with_only_columns(['1'])) + def count(self): """Return a count of rows this Query would return. @@ -2549,7 +2549,8 @@ class Query(object): to count, to skip the usage of a subquery or otherwise control of the FROM clause, or to use other aggregate functions, - use :attr:`~sqlalchemy.sql.expression.func` expressions in conjunction + use :attr:`~sqlalchemy.sql.expression.func` + expressions in conjunction with :meth:`~.Session.query`, i.e.:: from sqlalchemy import func @@ -2593,109 +2594,53 @@ class Query(object): removed from the session. Matched objects are removed from the session. - ``'evaluate'`` - Evaluate the query's criteria in Python straight on - the objects in the session. If evaluation of the criteria isn't + ``'evaluate'`` - Evaluate the query's criteria in Python straight + on the objects in the session. If evaluation of the criteria isn't implemented, an error is raised. In that case you probably want to use the 'fetch' strategy as a fallback. The expression evaluator currently doesn't account for differing string collations between the database and Python. - Returns the number of rows deleted, excluding any cascades. + :return: the count of rows matched as returned by the database's + "row count" feature. - The method does *not* offer in-Python cascading of relationships - it - is assumed that ON DELETE CASCADE is configured for any foreign key - references which require it. The Session needs to be expired (occurs - automatically after commit(), or call expire_all()) in order for the - state of dependent objects subject to delete or delete-orphan cascade - to be correctly represented. + This method has several key caveats: - Note that the :meth:`.MapperEvents.before_delete` and - :meth:`.MapperEvents.after_delete` - events are **not** invoked from this method. It instead - invokes :meth:`.SessionEvents.after_bulk_delete`. + * The method does **not** offer in-Python cascading of relationships - it + is assumed that ON DELETE CASCADE/SET NULL/etc. is configured for any foreign key + references which require it, otherwise the database may emit an + integrity violation if foreign key references are being enforced. + + After the DELETE, dependent objects in the :class:`.Session` which + were impacted by an ON DELETE may not contain the current + state, or may have been deleted. This issue is resolved once the + :class:`.Session` is expired, + which normally occurs upon :meth:`.Session.commit` or can be forced + by using :meth:`.Session.expire_all`. Accessing an expired object + whose row has been deleted will invoke a SELECT to locate the + row; when the row is not found, an :class:`~sqlalchemy.orm.exc.ObjectDeletedError` + is raised. + + * The :meth:`.MapperEvents.before_delete` and + :meth:`.MapperEvents.after_delete` + events are **not** invoked from this method. Instead, the + :meth:`.SessionEvents.after_bulk_delete` method is provided to act + upon a mass DELETE of entity rows. + + .. seealso:: + + :meth:`.Query.update` + + :ref:`inserts_and_updates` - Core SQL tutorial """ - #TODO: lots of duplication and ifs - probably needs to be - # refactored to strategies #TODO: cascades need handling. - if synchronize_session not in [False, 'evaluate', 'fetch']: - raise sa_exc.ArgumentError( - "Valid strategies for session " - "synchronization are False, 'evaluate' and " - "'fetch'") - self._no_select_modifiers("delete") - - self = self.enable_eagerloads(False) - - context = self._compile_context() - if len(context.statement.froms) != 1 or \ - not isinstance(context.statement.froms[0], schema.Table): - raise sa_exc.ArgumentError("Only deletion via a single table " - "query is currently supported") - primary_table = context.statement.froms[0] - - session = self.session - - if self._autoflush: - session._autoflush() - - if synchronize_session == 'evaluate': - try: - evaluator_compiler = evaluator.EvaluatorCompiler() - if self.whereclause is not None: - eval_condition = evaluator_compiler.process( - self.whereclause) - else: - def eval_condition(obj): - return True - - except evaluator.UnevaluatableError: - raise sa_exc.InvalidRequestError( - "Could not evaluate current criteria in Python. " - "Specify 'fetch' or False for the synchronize_session " - "parameter.") - - target_cls = self._mapper_zero().class_ - - #TODO: detect when the where clause is a trivial primary key match - objs_to_expunge = [ - obj for (cls, pk),obj in - session.identity_map.iteritems() - if issubclass(cls, target_cls) and - eval_condition(obj)] - - elif synchronize_session == 'fetch': - #TODO: use RETURNING when available - select_stmt = context.statement.with_only_columns( - primary_table.primary_key) - matched_rows = session.execute( - select_stmt, - params=self._params).fetchall() - - delete_stmt = sql.delete(primary_table, context.whereclause) - - result = session.execute(delete_stmt, params=self._params) - - if synchronize_session == 'evaluate': - for obj in objs_to_expunge: - session._remove_newly_deleted(attributes.instance_state(obj)) - elif synchronize_session == 'fetch': - target_mapper = self._mapper_zero() - for primary_key in matched_rows: - identity_key = target_mapper.identity_key_from_primary_key( - list(primary_key)) - if identity_key in session.identity_map: - session._remove_newly_deleted( - attributes.instance_state( - session.identity_map[identity_key] - ) - ) - - session.dispatch.after_bulk_delete(session, self, context, result) - - return result.rowcount + delete_op = persistence.BulkDelete.factory( + self, synchronize_session) + delete_op.exec_() + return delete_op.rowcount def update(self, values, synchronize_session='evaluate'): """Perform a bulk update query. @@ -2719,27 +2664,57 @@ class Query(object): objects that are matched by the update query. The updated attributes are expired on matched objects. - ``'evaluate'`` - Evaluate the Query's criteria in Python straight on - the objects in the session. If evaluation of the criteria isn't + ``'evaluate'`` - Evaluate the Query's criteria in Python straight + on the objects in the session. If evaluation of the criteria isn't implemented, an exception is raised. The expression evaluator currently doesn't account for differing string collations between the database and Python. - Returns the number of rows matched by the update. + :return: the count of rows matched as returned by the database's + "row count" feature. - The method does *not* offer in-Python cascading of relationships - it - is assumed that ON UPDATE CASCADE is configured for any foreign key - references which require it. + This method has several key caveats: - The Session needs to be expired (occurs automatically after commit(), - or call expire_all()) in order for the state of dependent objects - subject foreign key cascade to be correctly represented. + * The method does **not** offer in-Python cascading of relationships - it + is assumed that ON UPDATE CASCADE is configured for any foreign key + references which require it, otherwise the database may emit an + integrity violation if foreign key references are being enforced. - Note that the :meth:`.MapperEvents.before_update` and - :meth:`.MapperEvents.after_update` - events are **not** invoked from this method. It instead - invokes :meth:`.SessionEvents.after_bulk_update`. + After the UPDATE, dependent objects in the :class:`.Session` which + were impacted by an ON UPDATE CASCADE may not contain the current + state; this issue is resolved once the :class:`.Session` is expired, + which normally occurs upon :meth:`.Session.commit` or can be forced + by using :meth:`.Session.expire_all`. + + * As of 0.8, this method will support multiple table updates, as detailed + in :ref:`multi_table_updates`, and this behavior does extend to support + updates of joined-inheritance and other multiple table mappings. However, + the **join condition of an inheritance mapper is currently not + automatically rendered**. + Care must be taken in any multiple-table update to explicitly include + the joining condition between those tables, even in mappings where + this is normally automatic. + E.g. if a class ``Engineer`` subclasses ``Employee``, an UPDATE of the + ``Engineer`` local table using criteria against the ``Employee`` + local table might look like:: + + session.query(Engineer).\\ + filter(Engineer.id == Employee.id).\\ + filter(Employee.name == 'dilbert').\\ + update({"engineer_type": "programmer"}) + + * The :meth:`.MapperEvents.before_update` and + :meth:`.MapperEvents.after_update` + events are **not** invoked from this method. Instead, the + :meth:`.SessionEvents.after_bulk_update` method is provided to act + upon a mass UPDATE of entity rows. + + .. seealso:: + + :meth:`.Query.delete` + + :ref:`inserts_and_updates` - Core SQL tutorial """ @@ -2749,108 +2724,11 @@ class Query(object): # fk assignments #TODO: cascades need handling. - if synchronize_session == 'expire': - util.warn_deprecated("The 'expire' value as applied to " - "the synchronize_session argument of " - "query.update() is now called 'fetch'") - synchronize_session = 'fetch' + update_op = persistence.BulkUpdate.factory( + self, synchronize_session, values) + update_op.exec_() + return update_op.rowcount - if synchronize_session not in [False, 'evaluate', 'fetch']: - raise sa_exc.ArgumentError( - "Valid strategies for session synchronization " - "are False, 'evaluate' and 'fetch'") - self._no_select_modifiers("update") - - self = self.enable_eagerloads(False) - - context = self._compile_context() - if len(context.statement.froms) != 1 or \ - not isinstance(context.statement.froms[0], schema.Table): - raise sa_exc.ArgumentError( - "Only update via a single table query is " - "currently supported") - primary_table = context.statement.froms[0] - - session = self.session - - if self._autoflush: - session._autoflush() - - if synchronize_session == 'evaluate': - try: - evaluator_compiler = evaluator.EvaluatorCompiler() - if self.whereclause is not None: - eval_condition = evaluator_compiler.process( - self.whereclause) - else: - def eval_condition(obj): - return True - - value_evaluators = {} - for key,value in values.iteritems(): - key = _attr_as_key(key) - value_evaluators[key] = evaluator_compiler.process( - expression._literal_as_binds(value)) - except evaluator.UnevaluatableError: - raise sa_exc.InvalidRequestError( - "Could not evaluate current criteria in Python. " - "Specify 'fetch' or False for the " - "synchronize_session parameter.") - target_cls = self._mapper_zero().class_ - matched_objects = [] - for (cls, pk),obj in session.identity_map.iteritems(): - evaluated_keys = value_evaluators.keys() - - if issubclass(cls, target_cls) and eval_condition(obj): - matched_objects.append(obj) - - elif synchronize_session == 'fetch': - select_stmt = context.statement.with_only_columns( - primary_table.primary_key) - matched_rows = session.execute( - select_stmt, - params=self._params).fetchall() - - update_stmt = sql.update(primary_table, context.whereclause, values) - - result = session.execute(update_stmt, params=self._params) - - if synchronize_session == 'evaluate': - target_cls = self._mapper_zero().class_ - - for obj in matched_objects: - state, dict_ = attributes.instance_state(obj),\ - attributes.instance_dict(obj) - - # only evaluate unmodified attributes - to_evaluate = state.unmodified.intersection( - evaluated_keys) - for key in to_evaluate: - dict_[key] = value_evaluators[key](obj) - - state.commit(dict_, list(to_evaluate)) - - # expire attributes with pending changes - # (there was no autoflush, so they are overwritten) - state.expire_attributes(dict_, - set(evaluated_keys). - difference(to_evaluate)) - - elif synchronize_session == 'fetch': - target_mapper = self._mapper_zero() - - for primary_key in matched_rows: - identity_key = target_mapper.identity_key_from_primary_key( - list(primary_key)) - if identity_key in session.identity_map: - session.expire( - session.identity_map[identity_key], - [_attr_as_key(k) for k in values] - ) - - session.dispatch.after_bulk_update(session, self, context, result) - - return result.rowcount def _compile_context(self, labels=True): context = QueryContext(self) @@ -2858,18 +2736,9 @@ class Query(object): if context.statement is not None: return context - if self._lockmode: - try: - for_update = {'read': 'read', - 'read_nowait': 'read_nowait', - 'update': True, - 'update_nowait': 'nowait', - None: False}[self._lockmode] - except KeyError: - raise sa_exc.ArgumentError( - "Unknown lockmode %r" % self._lockmode) - else: - for_update = False + context.labels = labels + + context._for_update_arg = self._for_update_arg for entity in self._entities: entity.setup_context(self, context) @@ -2878,16 +2747,19 @@ class Query(object): strategy = rec[0] strategy(*rec[1:]) - eager_joins = context.eager_joins.values() - if context.from_clause: # "load from explicit FROMs" mode, # i.e. when select_from() or join() is used - froms = list(context.from_clause) + context.froms = list(context.from_clause) + # this would fix... + #import pdb + #pdb.set_trace() + #context.froms += tuple(context.from_clause) + else: # "load from discrete FROMs" mode, # i.e. when each _MappedEntity has its own FROM - froms = context.froms + context.froms = context.froms if self._enable_single_crit: self._adjust_for_single_inheritance(context) @@ -2904,134 +2776,160 @@ class Query(object): "SELECT from.") if context.multi_row_eager_loaders and self._should_nest_selectable: - # for eager joins present and LIMIT/OFFSET/DISTINCT, - # wrap the query inside a select, - # then append eager joins onto that + context.statement = self._compound_eager_statement(context) + else: + context.statement = self._simple_statement(context) + return context - if context.order_by: - order_by_col_expr = list( - chain(*[ - sql_util.unwrap_order_by(o) - for o in context.order_by - ]) - ) - else: - context.order_by = None - order_by_col_expr = [] + def _compound_eager_statement(self, context): + # for eager joins present and LIMIT/OFFSET/DISTINCT, + # wrap the query inside a select, + # then append eager joins onto that - inner = sql.select( - context.primary_columns + order_by_col_expr, + if context.order_by: + order_by_col_expr = list( + chain(*[ + sql_util.unwrap_order_by(o) + for o in context.order_by + ]) + ) + else: + context.order_by = None + order_by_col_expr = [] + + inner = sql.select( + context.primary_columns + order_by_col_expr, + context.whereclause, + from_obj=context.froms, + use_labels=context.labels, + # TODO: this order_by is only needed if + # LIMIT/OFFSET is present in self._select_args, + # else the application on the outside is enough + order_by=context.order_by, + **self._select_args + ) + + for hint in self._with_hints: + inner = inner.with_hint(*hint) + + if self._correlate: + inner = inner.correlate(*self._correlate) + + inner = inner.alias() + + equivs = self.__all_equivs() + + context.adapter = sql_util.ColumnAdapter(inner, equivs) + + statement = sql.select( + [inner] + context.secondary_columns, + use_labels=context.labels) + + statement._for_update_arg = context._for_update_arg + + from_clause = inner + for eager_join in context.eager_joins.values(): + # EagerLoader places a 'stop_on' attribute on the join, + # giving us a marker as to where the "splice point" of + # the join should be + from_clause = sql_util.splice_joins( + from_clause, + eager_join, eager_join.stop_on) + + statement.append_from(from_clause) + + if context.order_by: + statement.append_order_by( + *context.adapter.copy_and_process( + context.order_by + ) + ) + + statement.append_order_by(*context.eager_order_by) + return statement + + def _simple_statement(self, context): + if not context.order_by: + context.order_by = None + + if self._distinct and context.order_by: + order_by_col_expr = list( + chain(*[ + sql_util.unwrap_order_by(o) + for o in context.order_by + ]) + ) + context.primary_columns += order_by_col_expr + + context.froms += tuple(context.eager_joins.values()) + + statement = sql.select( + context.primary_columns + + context.secondary_columns, context.whereclause, - from_obj=froms, - use_labels=labels, - correlate=False, - # TODO: this order_by is only needed if - # LIMIT/OFFSET is present in self._select_args, - # else the application on the outside is enough + from_obj=context.froms, + use_labels=context.labels, order_by=context.order_by, **self._select_args ) - for hint in self._with_hints: - inner = inner.with_hint(*hint) + statement._for_update_arg = context._for_update_arg - if self._correlate: - inner = inner.correlate(*self._correlate) + for hint in self._with_hints: + statement = statement.with_hint(*hint) - inner = inner.alias() - - equivs = self.__all_equivs() - - context.adapter = sql_util.ColumnAdapter(inner, equivs) - - statement = sql.select( - [inner] + context.secondary_columns, - for_update=for_update, - use_labels=labels) - - from_clause = inner - for eager_join in eager_joins: - # EagerLoader places a 'stop_on' attribute on the join, - # giving us a marker as to where the "splice point" of - # the join should be - from_clause = sql_util.splice_joins( - from_clause, - eager_join, eager_join.stop_on) - - statement.append_from(from_clause) - - if context.order_by: - statement.append_order_by( - *context.adapter.copy_and_process( - context.order_by - ) - ) + if self._correlate: + statement = statement.correlate(*self._correlate) + if context.eager_order_by: statement.append_order_by(*context.eager_order_by) - else: - if not context.order_by: - context.order_by = None - - if self._distinct and context.order_by: - order_by_col_expr = list( - chain(*[ - sql_util.unwrap_order_by(o) - for o in context.order_by - ]) - ) - context.primary_columns += order_by_col_expr - - froms += tuple(context.eager_joins.values()) - - statement = sql.select( - context.primary_columns + - context.secondary_columns, - context.whereclause, - from_obj=froms, - use_labels=labels, - for_update=for_update, - correlate=False, - order_by=context.order_by, - **self._select_args - ) - - for hint in self._with_hints: - statement = statement.with_hint(*hint) - - if self._correlate: - statement = statement.correlate(*self._correlate) - - if context.eager_order_by: - statement.append_order_by(*context.eager_order_by) - - context.statement = statement - - return context + return statement def _adjust_for_single_inheritance(self, context): """Apply single-table-inheritance filtering. - For all distinct single-table-inheritance mappers represented in the - columns clause of this query, add criterion to the WHERE clause of the - given QueryContext such that only the appropriate subtypes are - selected from the total results. + For all distinct single-table-inheritance mappers represented in + the columns clause of this query, add criterion to the WHERE + clause of the given QueryContext such that only the appropriate + subtypes are selected from the total results. """ - for entity, (mapper, adapter, s, i, w) in \ - self._mapper_adapter_map.iteritems(): - if entity in self._join_entities: + for (ext_info, adapter) in self._mapper_adapter_map.values(): + if ext_info in self._join_entities: continue - single_crit = mapper._single_table_criterion + single_crit = ext_info.mapper._single_table_criterion if single_crit is not None: if adapter: single_crit = adapter.traverse(single_crit) single_crit = self._adapt_clause(single_crit, False, False) context.whereclause = sql.and_( - context.whereclause, single_crit) + sql.True_._ifnone(context.whereclause), + single_crit) def __str__(self): return str(self._compile_context().statement) +from ..sql.selectable import ForUpdateArg + +class LockmodeArg(ForUpdateArg): + @classmethod + def parse_legacy_query(self, mode): + if mode in (None, False): + return None + + if mode == "read": + read = True + nowait = False + elif mode == "update": + read = nowait = False + elif mode == "update_nowait": + nowait = True + read = False + else: + raise sa_exc.ArgumentError( + "Unknown with_lockmode argument: %r" % mode) + + return LockmodeArg(read=read, nowait=nowait) class _QueryEntity(object): """represent an entity column returned within a Query result.""" @@ -3039,9 +2937,11 @@ class _QueryEntity(object): def __new__(cls, *args, **kwargs): if cls is _QueryEntity: entity = args[1] - if not isinstance(entity, basestring) and \ + if not isinstance(entity, util.string_types) and \ _is_mapped_class(entity): cls = _MapperEntity + elif isinstance(entity, Bundle): + cls = _BundleEntity else: cls = _ColumnEntity return object.__new__(cls) @@ -3051,39 +2951,52 @@ class _QueryEntity(object): q.__dict__ = self.__dict__.copy() return q + class _MapperEntity(_QueryEntity): """mapper/class/AliasedClass entity""" def __init__(self, query, entity): - self.primary_entity = not query._entities + if not query._primary_entity: + query._primary_entity = self query._entities.append(self) self.entities = [entity] - self.entity_zero = self.expr = entity + self.expr = entity - def setup_entity(self, entity, mapper, adapter, - from_obj, is_aliased_class, with_polymorphic): - self.mapper = mapper - self.adapter = adapter - self.selectable = from_obj - self._with_polymorphic = with_polymorphic - self._polymorphic_discriminator = None - self.is_aliased_class = is_aliased_class - if is_aliased_class: - self.path_entity = self.entity_zero = entity - self._path = (entity,) - self._label_name = self.entity_zero._sa_label_name - self._reduced_path = (self.path_entity, ) + supports_single_entity = True + + def setup_entity(self, ext_info, aliased_adapter): + self.mapper = ext_info.mapper + self.aliased_adapter = aliased_adapter + self.selectable = ext_info.selectable + self.is_aliased_class = ext_info.is_aliased_class + self._with_polymorphic = ext_info.with_polymorphic_mappers + self._polymorphic_discriminator = \ + ext_info.polymorphic_on + self.entity_zero = ext_info + if ext_info.is_aliased_class: + self._label_name = self.entity_zero.name else: - self.path_entity = mapper - self._path = (mapper,) - self._reduced_path = (mapper.base_mapper, ) - self.entity_zero = mapper self._label_name = self.mapper.class_.__name__ - + self.path = self.entity_zero._path_registry + self.custom_rows = bool(self.mapper.dispatch.append_result) def set_with_polymorphic(self, query, cls_or_mappers, - selectable, discriminator): + selectable, polymorphic_on): + """Receive an update from a call to query.with_polymorphic(). + + Note the newer style of using a free standing with_polymporphic() + construct doesn't make use of this method. + + + """ + if self.is_aliased_class: + # TODO: invalidrequest ? + raise NotImplementedError( + "Can't use with_polymorphic() against " + "an Aliased object" + ) + if cls_or_mappers is None: query._reset_polymorphic_adapter(self.mapper) return @@ -3091,15 +3004,12 @@ class _MapperEntity(_QueryEntity): mappers, from_obj = self.mapper._with_polymorphic_args( cls_or_mappers, selectable) self._with_polymorphic = mappers - self._polymorphic_discriminator = discriminator + self._polymorphic_discriminator = polymorphic_on - # TODO: do the wrapped thing here too so that - # with_polymorphic() can be applied to aliases - if not self.is_aliased_class: - self.selectable = from_obj - query._mapper_loads_polymorphically_with(self.mapper, - sql_util.ColumnAdapter(from_obj, - self.mapper._equivalent_columns)) + self.selectable = from_obj + query._mapper_loads_polymorphically_with(self.mapper, + sql_util.ColumnAdapter(from_obj, + self.mapper._equivalent_columns)) filter_fn = id @@ -3112,10 +3022,18 @@ class _MapperEntity(_QueryEntity): return self.entity_zero def corresponds_to(self, entity): - if _is_aliased_class(entity) or self.is_aliased_class: - return entity is self.path_entity - else: - return entity.common_parent(self.path_entity) + if entity.is_aliased_class: + if self.is_aliased_class: + if entity._base_alias is self.entity_zero._base_alias: + return True + return False + elif self.is_aliased_class: + if self.entity_zero._use_mapper_path: + return entity in self._with_polymorphic + else: + return entity is self.entity_zero + + return entity.common_parent(self.entity_zero) def adapt_to_selectable(self, query, sel): query._entities.append(self) @@ -3123,11 +3041,12 @@ class _MapperEntity(_QueryEntity): def _get_entity_clauses(self, query, context): adapter = None - if not self.is_aliased_class and query._polymorphic_adapters: - adapter = query._polymorphic_adapters.get(self.mapper, None) - if not adapter and self.adapter: - adapter = self.adapter + if not self.is_aliased_class: + if query._polymorphic_adapters: + adapter = query._polymorphic_adapters.get(self.mapper, None) + else: + adapter = self.aliased_adapter if adapter: if query._from_obj_alias: @@ -3152,34 +3071,34 @@ class _MapperEntity(_QueryEntity): # require row aliasing unconditionally. if not adapter and self.mapper._requires_row_aliasing: adapter = sql_util.ColumnAdapter( - self.selectable, - self.mapper._equivalent_columns) + self.selectable, + self.mapper._equivalent_columns) - if self.primary_entity: - _instance = self.mapper._instance_processor( - context, - self._path, - self._reduced_path, - adapter, - only_load_props=query._only_load_props, - refresh_state=context.refresh_state, - polymorphic_discriminator= - self._polymorphic_discriminator + if query._primary_entity is self: + _instance = loading.instance_processor( + self.mapper, + context, + self.path, + adapter, + only_load_props=query._only_load_props, + refresh_state=context.refresh_state, + polymorphic_discriminator=self._polymorphic_discriminator ) else: - _instance = self.mapper._instance_processor( - context, - self._path, - self._reduced_path, - adapter, - polymorphic_discriminator= - self._polymorphic_discriminator) + _instance = loading.instance_processor( + self.mapper, + context, + self.path, + adapter, + polymorphic_discriminator=self._polymorphic_discriminator + ) return _instance, self._label_name def setup_context(self, query, context): adapter = self._get_entity_clauses(query, context) + #if self._adapted_selectable is None: context.froms += (self.selectable,) if context.order_by is False and self.mapper.order_by: @@ -3206,14 +3125,16 @@ class _MapperEntity(_QueryEntity): value.setup( context, self, - self._path, - self._reduced_path, + self.path, adapter, only_load_props=query._only_load_props, column_collection=context.primary_columns ) - if self._polymorphic_discriminator is not None: + if self._polymorphic_discriminator is not None and \ + self._polymorphic_discriminator \ + is not self.mapper.polymorphic_on: + if adapter: pd = adapter.columns[self._polymorphic_discriminator] else: @@ -3223,6 +3144,188 @@ class _MapperEntity(_QueryEntity): def __str__(self): return str(self.mapper) +@inspection._self_inspects +class Bundle(object): + """A grouping of SQL expressions that are returned by a :class:`.Query` + under one namespace. + + The :class:`.Bundle` essentially allows nesting of the tuple-based + results returned by a column-oriented :class:`.Query` object. It also + is extensible via simple subclassing, where the primary capability + to override is that of how the set of expressions should be returned, + allowing post-processing as well as custom return types, without + involving ORM identity-mapped classes. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :ref:`bundles` + + """ + + single_entity = False + """If True, queries for a single Bundle will be returned as a single + entity, rather than an element within a keyed tuple.""" + + def __init__(self, name, *exprs, **kw): + """Construct a new :class:`.Bundle`. + + e.g.:: + + bn = Bundle("mybundle", MyClass.x, MyClass.y) + + for row in session.query(bn).filter(bn.c.x == 5).filter(bn.c.y == 4): + print(row.mybundle.x, row.mybundle.y) + + :param name: name of the bundle. + :param \*exprs: columns or SQL expressions comprising the bundle. + :param single_entity=False: if True, rows for this :class:`.Bundle` + can be returned as a "single entity" outside of any enclosing tuple + in the same manner as a mapped entity. + + """ + self.name = self._label = name + self.exprs = exprs + self.c = self.columns = ColumnCollection() + self.columns.update((getattr(col, "key", col._label), col) + for col in exprs) + self.single_entity = kw.pop('single_entity', self.single_entity) + + columns = None + """A namespace of SQL expressions referred to by this :class:`.Bundle`. + + e.g.:: + + bn = Bundle("mybundle", MyClass.x, MyClass.y) + + q = sess.query(bn).filter(bn.c.x == 5) + + Nesting of bundles is also supported:: + + b1 = Bundle("b1", + Bundle('b2', MyClass.a, MyClass.b), + Bundle('b3', MyClass.x, MyClass.y) + ) + + q = sess.query(b1).filter(b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9) + + .. seealso:: + + :attr:`.Bundle.c` + + """ + + c = None + """An alias for :attr:`.Bundle.columns`.""" + + def _clone(self): + cloned = self.__class__.__new__(self.__class__) + cloned.__dict__.update(self.__dict__) + return cloned + + def __clause_element__(self): + return expression.ClauseList(group=False, *self.c) + + @property + def clauses(self): + return self.__clause_element__().clauses + + def label(self, name): + """Provide a copy of this :class:`.Bundle` passing a new label.""" + + cloned = self._clone() + cloned.name = name + return cloned + + def create_row_processor(self, query, procs, labels): + """Produce the "row processing" function for this :class:`.Bundle`. + + May be overridden by subclasses. + + .. seealso:: + + :ref:`bundles` - includes an example of subclassing. + + """ + def proc(row, result): + return util.KeyedTuple([proc(row, None) for proc in procs], labels) + return proc + + +class _BundleEntity(_QueryEntity): + def __init__(self, query, bundle, setup_entities=True): + query._entities.append(self) + self.bundle = self.expr = bundle + self.type = type(bundle) + self._label_name = bundle.name + self._entities = [] + + if setup_entities: + for expr in bundle.exprs: + if isinstance(expr, Bundle): + _BundleEntity(self, expr) + else: + _ColumnEntity(self, expr, namespace=self) + + self.entities = () + + self.filter_fn = lambda item: item + + self.supports_single_entity = self.bundle.single_entity + + custom_rows = False + + @property + def entity_zero(self): + for ent in self._entities: + ezero = ent.entity_zero + if ezero is not None: + return ezero + else: + return None + + def corresponds_to(self, entity): + # TODO: this seems to have no effect for + # _ColumnEntity either + return False + + @property + def entity_zero_or_selectable(self): + for ent in self._entities: + ezero = ent.entity_zero_or_selectable + if ezero is not None: + return ezero + else: + return None + + def adapt_to_selectable(self, query, sel): + c = _BundleEntity(query, self.bundle, setup_entities=False) + #c._label_name = self._label_name + #c.entity_zero = self.entity_zero + #c.entities = self.entities + + for ent in self._entities: + ent.adapt_to_selectable(c, sel) + + def setup_entity(self, ext_info, aliased_adapter): + for ent in self._entities: + ent.setup_entity(ext_info, aliased_adapter) + + def setup_context(self, query, context): + for ent in self._entities: + ent.setup_context(query, context) + + def row_processor(self, query, context, custom_rows): + procs, labels = zip( + *[ent.row_processor(query, context, custom_rows) + for ent in self._entities] + ) + + proc = self.bundle.create_row_processor(query, procs, labels) + + return proc, self._label_name + class _ColumnEntity(_QueryEntity): """Column/expression based entity.""" @@ -3230,7 +3333,7 @@ class _ColumnEntity(_QueryEntity): self.expr = column self.namespace = namespace - if isinstance(column, basestring): + if isinstance(column, util.string_types): column = sql.literal_column(column) self._label_name = column.name elif isinstance(column, ( @@ -3238,7 +3341,7 @@ class _ColumnEntity(_QueryEntity): interfaces.PropComparator )): self._label_name = column.key - column = column.__clause_element__() + column = column._query_clause_element() else: self._label_name = getattr(column, 'key', None) @@ -3251,6 +3354,9 @@ class _ColumnEntity(_QueryEntity): if c is not column: return + elif isinstance(column, Bundle): + _BundleEntity(query, column) + return if not isinstance(column, sql.ColumnElement): raise sa_exc.InvalidRequestError( @@ -3258,13 +3364,21 @@ class _ColumnEntity(_QueryEntity): "expected - got '%r'" % (column, ) ) + self.type = type_ = column.type + if type_.hashable: + self.filter_fn = lambda item: item + else: + counter = util.counter() + self.filter_fn = lambda item: counter() + # If the Column is unnamed, give it a # label() so that mutable column expressions # can be located in the result even # if the expression's identity has been changed # due to adaption. - if not column._label: - column = column.label(None) + + if not column._label and not getattr(column, 'is_literal', False): + column = column.label(self._label_name) query._entities.append(self) @@ -3293,6 +3407,9 @@ class _ColumnEntity(_QueryEntity): else: self.entity_zero = None + supports_single_entity = False + custom_rows = False + @property def entity_zero_or_selectable(self): if self.entity_zero is not None: @@ -3302,29 +3419,24 @@ class _ColumnEntity(_QueryEntity): else: return None - @property - def type(self): - return self.column.type - - def filter_fn(self, item): - return item - def adapt_to_selectable(self, query, sel): c = _ColumnEntity(query, sel.corresponding_column(self.column)) c._label_name = self._label_name c.entity_zero = self.entity_zero c.entities = self.entities - def setup_entity(self, entity, mapper, adapter, from_obj, - is_aliased_class, with_polymorphic): + def setup_entity(self, ext_info, aliased_adapter): if 'selectable' not in self.__dict__: - self.selectable = from_obj - self.froms.add(from_obj) + self.selectable = ext_info.selectable + self.froms.add(ext_info.selectable) def corresponds_to(self, entity): + # TODO: just returning False here, + # no tests fail if self.entity_zero is None: return False elif _is_aliased_class(entity): + # TODO: polymorphic subclasses ? return entity is self.entity_zero else: return not _is_aliased_class(self.entity_zero) and \ @@ -3354,17 +3466,17 @@ class _ColumnEntity(_QueryEntity): def __str__(self): return str(self.column) -log.class_logger(Query) class QueryContext(object): multi_row_eager_loaders = False adapter = None froms = () + for_update = None def __init__(self, query): if query._statement is not None: - if isinstance(query._statement, expression._SelectBase) and \ + if isinstance(query._statement, expression.SelectBase) and \ not query._statement.use_labels: self.statement = query._statement.apply_labels() else: @@ -3390,17 +3502,49 @@ class QueryContext(object): o.propagate_to_loaders) self.attributes = query._attributes.copy() + class AliasOption(interfaces.MapperOption): def __init__(self, alias): + """Return a :class:`.MapperOption` that will indicate to the :class:`.Query` + that the main table has been aliased. + + This is a seldom-used option to suit the + very rare case that :func:`.contains_eager` + is being used in conjunction with a user-defined SELECT + statement that aliases the parent table. E.g.:: + + # define an aliased UNION called 'ulist' + ulist = users.select(users.c.user_id==7).\\ + union(users.select(users.c.user_id>7)).\\ + alias('ulist') + + # add on an eager load of "addresses" + statement = ulist.outerjoin(addresses).\\ + select().apply_labels() + + # create query, indicating "ulist" will be an + # alias for the main table, "addresses" + # property should be eager loaded + query = session.query(User).options( + contains_alias(ulist), + contains_eager(User.addresses)) + + # then get results via the statement + results = query.from_statement(statement).all() + + :param alias: is the string name of an alias, or a + :class:`~.sql.expression.Alias` object representing + the alias. + + """ self.alias = alias def process_query(self, query): - if isinstance(self.alias, basestring): + if isinstance(self.alias, util.string_types): alias = query._mapper_zero().mapped_table.alias(self.alias) else: alias = self.alias query._from_obj_alias = sql_util.ColumnAdapter(alias) -_new_runid = util.counter() diff --git a/libs/sqlalchemy/orm/relationships.py b/libs/sqlalchemy/orm/relationships.py new file mode 100644 index 00000000..982f10a4 --- /dev/null +++ b/libs/sqlalchemy/orm/relationships.py @@ -0,0 +1,2511 @@ +# orm/relationships.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Heuristics related to join conditions as used in +:func:`.relationship`. + +Provides the :class:`.JoinCondition` object, which encapsulates +SQL annotation and aliasing behavior focused on the `primaryjoin` +and `secondaryjoin` aspects of :func:`.relationship`. + +""" + +from .. import sql, util, exc as sa_exc, schema, log + +from .util import CascadeOptions, _orm_annotate, _orm_deannotate +from . import dependency +from . import attributes +from ..sql.util import ( + ClauseAdapter, + join_condition, _shallow_annotate, visit_binary_product, + _deep_deannotate, selectables_overlap + ) +from ..sql import operators, expression, visitors +from .interfaces import MANYTOMANY, MANYTOONE, ONETOMANY, StrategizedProperty, PropComparator +from ..inspection import inspect +from . import mapper as mapperlib + +def remote(expr): + """Annotate a portion of a primaryjoin expression + with a 'remote' annotation. + + See the section :ref:`relationship_custom_foreign` for a + description of use. + + .. versionadded:: 0.8 + + .. seealso:: + + :ref:`relationship_custom_foreign` + + :func:`.foreign` + + """ + return _annotate_columns(expression._clause_element_as_expr(expr), + {"remote": True}) + + +def foreign(expr): + """Annotate a portion of a primaryjoin expression + with a 'foreign' annotation. + + See the section :ref:`relationship_custom_foreign` for a + description of use. + + .. versionadded:: 0.8 + + .. seealso:: + + :ref:`relationship_custom_foreign` + + :func:`.remote` + + """ + + return _annotate_columns(expression._clause_element_as_expr(expr), + {"foreign": True}) + + +@log.class_logger +@util.langhelpers.dependency_for("sqlalchemy.orm.properties") +class RelationshipProperty(StrategizedProperty): + """Describes an object property that holds a single item or list + of items that correspond to a related database table. + + Public constructor is the :func:`.orm.relationship` function. + + See also: + + :ref:`relationship_config_toplevel` + + """ + + strategy_wildcard_key = 'relationship' + + _dependency_processor = None + + def __init__(self, argument, + secondary=None, primaryjoin=None, + secondaryjoin=None, + foreign_keys=None, + uselist=None, + order_by=False, + backref=None, + back_populates=None, + post_update=False, + cascade=False, extension=None, + viewonly=False, lazy=True, + collection_class=None, passive_deletes=False, + passive_updates=True, remote_side=None, + enable_typechecks=True, join_depth=None, + comparator_factory=None, + single_parent=False, innerjoin=False, + distinct_target_key=None, + doc=None, + active_history=False, + cascade_backrefs=True, + load_on_pending=False, + strategy_class=None, _local_remote_pairs=None, + query_class=None, + info=None): + """Provide a relationship of a primary Mapper to a secondary Mapper. + + This corresponds to a parent-child or associative table relationship. The + constructed class is an instance of :class:`.RelationshipProperty`. + + A typical :func:`.relationship`, used in a classical mapping:: + + mapper(Parent, properties={ + 'children': relationship(Child) + }) + + Some arguments accepted by :func:`.relationship` optionally accept a + callable function, which when called produces the desired value. + The callable is invoked by the parent :class:`.Mapper` at "mapper + initialization" time, which happens only when mappers are first used, and + is assumed to be after all mappings have been constructed. This can be + used to resolve order-of-declaration and other dependency issues, such as + if ``Child`` is declared below ``Parent`` in the same file:: + + mapper(Parent, properties={ + "children":relationship(lambda: Child, + order_by=lambda: Child.id) + }) + + When using the :ref:`declarative_toplevel` extension, the Declarative + initializer allows string arguments to be passed to :func:`.relationship`. + These string arguments are converted into callables that evaluate + the string as Python code, using the Declarative + class-registry as a namespace. This allows the lookup of related + classes to be automatic via their string name, and removes the need to + import related classes at all into the local module space:: + + from sqlalchemy.ext.declarative import declarative_base + + Base = declarative_base() + + class Parent(Base): + __tablename__ = 'parent' + id = Column(Integer, primary_key=True) + children = relationship("Child", order_by="Child.id") + + A full array of examples and reference documentation regarding + :func:`.relationship` is at :ref:`relationship_config_toplevel`. + + :param argument: + a mapped class, or actual :class:`.Mapper` instance, representing the + target of the relationship. + + ``argument`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + :param secondary: + for a many-to-many relationship, specifies the intermediary + table, and is an instance of :class:`.Table`. The ``secondary`` keyword + argument should generally only + be used for a table that is not otherwise expressed in any class + mapping, unless this relationship is declared as view only, otherwise + conflicting persistence operations can occur. + + ``secondary`` may + also be passed as a callable function which is evaluated at + mapper initialization time. + + :param active_history=False: + When ``True``, indicates that the "previous" value for a + many-to-one reference should be loaded when replaced, if + not already loaded. Normally, history tracking logic for + simple many-to-ones only needs to be aware of the "new" + value in order to perform a flush. This flag is available + for applications that make use of + :func:`.attributes.get_history` which also need to know + the "previous" value of the attribute. + + :param backref: + indicates the string name of a property to be placed on the related + mapper's class that will handle this relationship in the other + direction. The other property will be created automatically + when the mappers are configured. Can also be passed as a + :func:`backref` object to control the configuration of the + new relationship. + + :param back_populates: + Takes a string name and has the same meaning as ``backref``, + except the complementing property is **not** created automatically, + and instead must be configured explicitly on the other mapper. The + complementing property should also indicate ``back_populates`` + to this relationship to ensure proper functioning. + + :param cascade: + a comma-separated list of cascade rules which determines how + Session operations should be "cascaded" from parent to child. + This defaults to ``False``, which means the default cascade + should be used. The default value is ``"save-update, merge"``. + + Available cascades are: + + * ``save-update`` - cascade the :meth:`.Session.add` + operation. This cascade applies both to future and + past calls to :meth:`~sqlalchemy.orm.session.Session.add`, + meaning new items added to a collection or scalar relationship + get placed into the same session as that of the parent, and + also applies to items which have been removed from this + relationship but are still part of unflushed history. + + * ``merge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.merge` + operation + + * ``expunge`` - cascade the :meth:`.Session.expunge` + operation + + * ``delete`` - cascade the :meth:`.Session.delete` + operation + + * ``delete-orphan`` - if an item of the child's type is + detached from its parent, mark it for deletion. + + .. versionchanged:: 0.7 + This option does not prevent + a new instance of the child object from being persisted + without a parent to start with; to constrain against + that case, ensure the child's foreign key column(s) + is configured as NOT NULL + + * ``refresh-expire`` - cascade the :meth:`.Session.expire` + and :meth:`~sqlalchemy.orm.session.Session.refresh` operations + + * ``all`` - shorthand for "save-update,merge, refresh-expire, + expunge, delete" + + See the section :ref:`unitofwork_cascades` for more background + on configuring cascades. + + :param cascade_backrefs=True: + a boolean value indicating if the ``save-update`` cascade should + operate along an assignment event intercepted by a backref. + When set to ``False``, + the attribute managed by this relationship will not cascade + an incoming transient object into the session of a + persistent parent, if the event is received via backref. + + That is:: + + mapper(A, a_table, properties={ + 'bs':relationship(B, backref="a", cascade_backrefs=False) + }) + + If an ``A()`` is present in the session, assigning it to + the "a" attribute on a transient ``B()`` will not place + the ``B()`` into the session. To set the flag in the other + direction, i.e. so that ``A().bs.append(B())`` won't add + a transient ``A()`` into the session for a persistent ``B()``:: + + mapper(A, a_table, properties={ + 'bs':relationship(B, + backref=backref("a", cascade_backrefs=False) + ) + }) + + See the section :ref:`unitofwork_cascades` for more background + on configuring cascades. + + :param collection_class: + a class or callable that returns a new list-holding object. will + be used in place of a plain list for storing elements. + Behavior of this attribute is described in detail at + :ref:`custom_collections`. + + :param comparator_factory: + a class which extends :class:`.RelationshipProperty.Comparator` which + provides custom SQL clause generation for comparison operations. + + :param distinct_target_key=None: + Indicate if a "subquery" eager load should apply the DISTINCT + keyword to the innermost SELECT statement. When left as ``None``, + the DISTINCT keyword will be applied in those cases when the target + columns do not comprise the full primary key of the target table. + When set to ``True``, the DISTINCT keyword is applied to the innermost + SELECT unconditionally. + + It may be desirable to set this flag to False when the DISTINCT is + reducing performance of the innermost subquery beyond that of what + duplicate innermost rows may be causing. + + .. versionadded:: 0.8.3 - distinct_target_key allows the + subquery eager loader to apply a DISTINCT modifier to the + innermost SELECT. + + .. versionchanged:: 0.9.0 - distinct_target_key now defaults to + ``None``, so that the feature enables itself automatically for + those cases where the innermost query targets a non-unique + key. + + :param doc: + docstring which will be applied to the resulting descriptor. + + :param extension: + an :class:`.AttributeExtension` instance, or list of extensions, + which will be prepended to the list of attribute listeners for + the resulting descriptor placed on the class. + **Deprecated.** Please see :class:`.AttributeEvents`. + + :param foreign_keys: + a list of columns which are to be used as "foreign key" columns, + or columns which refer to the value in a remote column, within the + context of this :func:`.relationship` object's ``primaryjoin`` + condition. That is, if the ``primaryjoin`` condition of this + :func:`.relationship` is ``a.id == b.a_id``, and the values in ``b.a_id`` + are required to be present in ``a.id``, then the "foreign key" column + of this :func:`.relationship` is ``b.a_id``. + + In normal cases, the ``foreign_keys`` parameter is **not required.** + :func:`.relationship` will **automatically** determine which columns + in the ``primaryjoin`` conditition are to be considered "foreign key" + columns based on those :class:`.Column` objects that specify + :class:`.ForeignKey`, or are otherwise listed as referencing columns + in a :class:`.ForeignKeyConstraint` construct. ``foreign_keys`` is only + needed when: + + 1. There is more than one way to construct a join from the local + table to the remote table, as there are multiple foreign key + references present. Setting ``foreign_keys`` will limit the + :func:`.relationship` to consider just those columns specified + here as "foreign". + + .. versionchanged:: 0.8 + A multiple-foreign key join ambiguity can be resolved by + setting the ``foreign_keys`` parameter alone, without the + need to explicitly set ``primaryjoin`` as well. + + 2. The :class:`.Table` being mapped does not actually have + :class:`.ForeignKey` or :class:`.ForeignKeyConstraint` + constructs present, often because the table + was reflected from a database that does not support foreign key + reflection (MySQL MyISAM). + + 3. The ``primaryjoin`` argument is used to construct a non-standard + join condition, which makes use of columns or expressions that do + not normally refer to their "parent" column, such as a join condition + expressed by a complex comparison using a SQL function. + + The :func:`.relationship` construct will raise informative error messages + that suggest the use of the ``foreign_keys`` parameter when presented + with an ambiguous condition. In typical cases, if :func:`.relationship` + doesn't raise any exceptions, the ``foreign_keys`` parameter is usually + not needed. + + ``foreign_keys`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + .. seealso:: + + :ref:`relationship_foreign_keys` + + :ref:`relationship_custom_foreign` + + :func:`.foreign` - allows direct annotation of the "foreign" columns + within a ``primaryjoin`` condition. + + .. versionadded:: 0.8 + The :func:`.foreign` annotation can also be applied + directly to the ``primaryjoin`` expression, which is an alternate, + more specific system of describing which columns in a particular + ``primaryjoin`` should be considered "foreign". + + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 + + :param innerjoin=False: + when ``True``, joined eager loads will use an inner join to join + against related tables instead of an outer join. The purpose + of this option is generally one of performance, as inner joins + generally perform better than outer joins. Another reason can be + the use of ``with_lockmode``, which does not support outer joins. + + This flag can be set to ``True`` when the relationship references an + object via many-to-one using local foreign keys that are not nullable, + or when the reference is one-to-one or a collection that is guaranteed + to have one or at least one entry. + + :param join_depth: + when non-``None``, an integer value indicating how many levels + deep "eager" loaders should join on a self-referring or cyclical + relationship. The number counts how many times the same Mapper + shall be present in the loading condition along a particular join + branch. When left at its default of ``None``, eager loaders + will stop chaining when they encounter a the same target mapper + which is already higher up in the chain. This option applies + both to joined- and subquery- eager loaders. + + :param lazy='select': specifies + how the related items should be loaded. Default value is + ``select``. Values include: + + * ``select`` - items should be loaded lazily when the property is first + accessed, using a separate SELECT statement, or identity map + fetch for simple many-to-one references. + + * ``immediate`` - items should be loaded as the parents are loaded, + using a separate SELECT statement, or identity map fetch for + simple many-to-one references. + + .. versionadded:: 0.6.5 + + * ``joined`` - items should be loaded "eagerly" in the same query as + that of the parent, using a JOIN or LEFT OUTER JOIN. Whether + the join is "outer" or not is determined by the ``innerjoin`` + parameter. + + * ``subquery`` - items should be loaded "eagerly" as the parents are + loaded, using one additional SQL statement, which issues a JOIN to a + subquery of the original statement, for each collection requested. + + * ``noload`` - no loading should occur at any time. This is to + support "write-only" attributes, or attributes which are + populated in some manner specific to the application. + + * ``dynamic`` - the attribute will return a pre-configured + :class:`~sqlalchemy.orm.query.Query` object for all read + operations, onto which further filtering operations can be + applied before iterating the results. See + the section :ref:`dynamic_relationship` for more details. + + * True - a synonym for 'select' + + * False - a synonym for 'joined' + + * None - a synonym for 'noload' + + Detailed discussion of loader strategies is at :doc:`/orm/loading`. + + :param load_on_pending=False: + Indicates loading behavior for transient or pending parent objects. + + When set to ``True``, causes the lazy-loader to + issue a query for a parent object that is not persistent, meaning it has + never been flushed. This may take effect for a pending object when + autoflush is disabled, or for a transient object that has been + "attached" to a :class:`.Session` but is not part of its pending + collection. + + The load_on_pending flag does not improve behavior + when the ORM is used normally - object references should be constructed + at the object level, not at the foreign key level, so that they + are present in an ordinary way before flush() proceeds. This flag + is not not intended for general use. + + .. versionadded:: 0.6.5 + + .. seealso:: + + :meth:`.Session.enable_relationship_loading` - this method establishes + "load on pending" behavior for the whole object, and also allows + loading on objects that remain transient or detached. + + :param order_by: + indicates the ordering that should be applied when loading these + items. ``order_by`` is expected to refer to one of the :class:`.Column` + objects to which the target class is mapped, or + the attribute itself bound to the target class which refers + to the column. + + ``order_by`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + :param passive_deletes=False: + Indicates loading behavior during delete operations. + + A value of True indicates that unloaded child items should not + be loaded during a delete operation on the parent. Normally, + when a parent item is deleted, all child items are loaded so + that they can either be marked as deleted, or have their + foreign key to the parent set to NULL. Marking this flag as + True usually implies an ON DELETE rule is in + place which will handle updating/deleting child rows on the + database side. + + Additionally, setting the flag to the string value 'all' will + disable the "nulling out" of the child foreign keys, when there + is no delete or delete-orphan cascade enabled. This is + typically used when a triggering or error raise scenario is in + place on the database side. Note that the foreign key + attributes on in-session child objects will not be changed + after a flush occurs so this is a very special use-case + setting. + + :param passive_updates=True: + Indicates loading and INSERT/UPDATE/DELETE behavior when the + source of a foreign key value changes (i.e. an "on update" + cascade), which are typically the primary key columns of the + source row. + + When True, it is assumed that ON UPDATE CASCADE is configured on + the foreign key in the database, and that the database will + handle propagation of an UPDATE from a source column to + dependent rows. Note that with databases which enforce + referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), + ON UPDATE CASCADE is required for this operation. The + relationship() will update the value of the attribute on related + items which are locally present in the session during a flush. + + When False, it is assumed that the database does not enforce + referential integrity and will not be issuing its own CASCADE + operation for an update. The relationship() will issue the + appropriate UPDATE statements to the database in response to the + change of a referenced key, and items locally present in the + session during a flush will also be refreshed. + + This flag should probably be set to False if primary key changes + are expected and the database in use doesn't support CASCADE + (i.e. SQLite, MySQL MyISAM tables). + + Also see the passive_updates flag on ``mapper()``. + + A future SQLAlchemy release will provide a "detect" feature for + this flag. + + :param post_update: + this indicates that the relationship should be handled by a + second UPDATE statement after an INSERT or before a + DELETE. Currently, it also will issue an UPDATE after the + instance was UPDATEd as well, although this technically should + be improved. This flag is used to handle saving bi-directional + dependencies between two individual rows (i.e. each row + references the other), where it would otherwise be impossible to + INSERT or DELETE both rows fully since one row exists before the + other. Use this flag when a particular mapping arrangement will + incur two rows that are dependent on each other, such as a table + that has a one-to-many relationship to a set of child rows, and + also has a column that references a single child row within that + list (i.e. both tables contain a foreign key to each other). If + a ``flush()`` operation returns an error that a "cyclical + dependency" was detected, this is a cue that you might want to + use ``post_update`` to "break" the cycle. + + :param primaryjoin: + a SQL expression that will be used as the primary + join of this child object against the parent object, or in a + many-to-many relationship the join of the primary object to the + association table. By default, this value is computed based on the + foreign key relationships of the parent and child tables (or association + table). + + ``primaryjoin`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + :param remote_side: + used for self-referential relationships, indicates the column or + list of columns that form the "remote side" of the relationship. + + ``remote_side`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + .. versionchanged:: 0.8 + The :func:`.remote` annotation can also be applied + directly to the ``primaryjoin`` expression, which is an alternate, + more specific system of describing which columns in a particular + ``primaryjoin`` should be considered "remote". + + :param query_class: + a :class:`.Query` subclass that will be used as the base of the + "appender query" returned by a "dynamic" relationship, that + is, a relationship that specifies ``lazy="dynamic"`` or was + otherwise constructed using the :func:`.orm.dynamic_loader` + function. + + :param secondaryjoin: + a SQL expression that will be used as the join of + an association table to the child object. By default, this value is + computed based on the foreign key relationships of the association and + child tables. + + ``secondaryjoin`` may also be passed as a callable function + which is evaluated at mapper initialization time, and may be passed as a + Python-evaluable string when using Declarative. + + :param single_parent=(True|False): + when True, installs a validator which will prevent objects + from being associated with more than one parent at a time. + This is used for many-to-one or many-to-many relationships that + should be treated either as one-to-one or one-to-many. Its + usage is optional unless delete-orphan cascade is also + set on this relationship(), in which case its required. + + :param uselist=(True|False): + a boolean that indicates if this property should be loaded as a + list or a scalar. In most cases, this value is determined + automatically by ``relationship()``, based on the type and direction + of the relationship - one to many forms a list, many to one + forms a scalar, many to many is a list. If a scalar is desired + where normally a list would be present, such as a bi-directional + one-to-one relationship, set uselist to False. + + :param viewonly=False: + when set to True, the relationship is used only for loading objects + within the relationship, and has no effect on the unit-of-work + flush process. Relationships with viewonly can specify any kind of + join conditions to provide additional views of related objects + onto a parent object. Note that the functionality of a viewonly + relationship has its limits - complicated join conditions may + not compile into eager or lazy loaders properly. If this is the + case, use an alternative method. + + .. versionchanged:: 0.6 + :func:`relationship` was renamed from its previous name + :func:`relation`. + + """ + + self.uselist = uselist + self.argument = argument + self.secondary = secondary + self.primaryjoin = primaryjoin + self.secondaryjoin = secondaryjoin + self.post_update = post_update + self.direction = None + self.viewonly = viewonly + self.lazy = lazy + self.single_parent = single_parent + self._user_defined_foreign_keys = foreign_keys + self.collection_class = collection_class + self.passive_deletes = passive_deletes + self.cascade_backrefs = cascade_backrefs + self.passive_updates = passive_updates + self.remote_side = remote_side + self.enable_typechecks = enable_typechecks + self.query_class = query_class + self.innerjoin = innerjoin + self.distinct_target_key = distinct_target_key + self.doc = doc + self.active_history = active_history + self.join_depth = join_depth + self.local_remote_pairs = _local_remote_pairs + self.extension = extension + self.load_on_pending = load_on_pending + self.comparator_factory = comparator_factory or \ + RelationshipProperty.Comparator + self.comparator = self.comparator_factory(self, None) + util.set_creation_order(self) + + if info is not None: + self.info = info + + if strategy_class: + self.strategy_class = strategy_class + else: + self.strategy_class = self._strategy_lookup(("lazy", self.lazy)) + + self._reverse_property = set() + + self.cascade = cascade if cascade is not False \ + else "save-update, merge" + + self.order_by = order_by + + self.back_populates = back_populates + + if self.back_populates: + if backref: + raise sa_exc.ArgumentError( + "backref and back_populates keyword arguments " + "are mutually exclusive") + self.backref = None + else: + self.backref = backref + + def instrument_class(self, mapper): + attributes.register_descriptor( + mapper.class_, + self.key, + comparator=self.comparator_factory(self, mapper), + parententity=mapper, + doc=self.doc, + ) + + class Comparator(PropComparator): + """Produce boolean, comparison, and other operators for + :class:`.RelationshipProperty` attributes. + + See the documentation for :class:`.PropComparator` for a brief overview + of ORM level operator definition. + + See also: + + :class:`.PropComparator` + + :class:`.ColumnProperty.Comparator` + + :class:`.ColumnOperators` + + :ref:`types_operators` + + :attr:`.TypeEngine.comparator_factory` + + """ + + _of_type = None + + def __init__(self, prop, parentmapper, adapt_to_entity=None, of_type=None): + """Construction of :class:`.RelationshipProperty.Comparator` + is internal to the ORM's attribute mechanics. + + """ + self.prop = prop + self._parentmapper = parentmapper + self._adapt_to_entity = adapt_to_entity + if of_type: + self._of_type = of_type + + def adapt_to_entity(self, adapt_to_entity): + return self.__class__(self.property, self._parentmapper, + adapt_to_entity=adapt_to_entity, + of_type=self._of_type) + + @util.memoized_property + def mapper(self): + """The target :class:`.Mapper` referred to by this + :class:`.RelationshipProperty.Comparator`. + + This is the "target" or "remote" side of the + :func:`.relationship`. + + """ + return self.property.mapper + + @util.memoized_property + def _parententity(self): + return self.property.parent + + def _source_selectable(self): + elem = self.property.parent._with_polymorphic_selectable + if self.adapter: + return self.adapter(elem) + else: + return elem + + def __clause_element__(self): + adapt_from = self._source_selectable() + if self._of_type: + of_type = inspect(self._of_type).mapper + else: + of_type = None + + pj, sj, source, dest, \ + secondary, target_adapter = self.property._create_joins( + source_selectable=adapt_from, + source_polymorphic=True, + of_type=of_type) + if sj is not None: + return pj & sj + else: + return pj + + def of_type(self, cls): + """Produce a construct that represents a particular 'subtype' of + attribute for the parent class. + + Currently this is usable in conjunction with :meth:`.Query.join` + and :meth:`.Query.outerjoin`. + + """ + return RelationshipProperty.Comparator( + self.property, + self._parentmapper, + adapt_to_entity=self._adapt_to_entity, + of_type=cls) + + def in_(self, other): + """Produce an IN clause - this is not implemented + for :func:`~.orm.relationship`-based attributes at this time. + + """ + raise NotImplementedError('in_() not yet supported for ' + 'relationships. For a simple many-to-one, use ' + 'in_() against the set of foreign key values.') + + __hash__ = None + + def __eq__(self, other): + """Implement the ``==`` operator. + + In a many-to-one context, such as:: + + MyClass.some_prop == + + this will typically produce a + clause such as:: + + mytable.related_id == + + Where ```` is the primary key of the given + object. + + The ``==`` operator provides partial functionality for non- + many-to-one comparisons: + + * Comparisons against collections are not supported. + Use :meth:`~.RelationshipProperty.Comparator.contains`. + * Compared to a scalar one-to-many, will produce a + clause that compares the target columns in the parent to + the given target. + * Compared to a scalar many-to-many, an alias + of the association table will be rendered as + well, forming a natural join that is part of the + main body of the query. This will not work for + queries that go beyond simple AND conjunctions of + comparisons, such as those which use OR. Use + explicit joins, outerjoins, or + :meth:`~.RelationshipProperty.Comparator.has` for + more comprehensive non-many-to-one scalar + membership tests. + * Comparisons against ``None`` given in a one-to-many + or many-to-many context produce a NOT EXISTS clause. + + """ + if isinstance(other, (util.NoneType, expression.Null)): + if self.property.direction in [ONETOMANY, MANYTOMANY]: + return ~self._criterion_exists() + else: + return _orm_annotate(self.property._optimized_compare( + None, adapt_source=self.adapter)) + elif self.property.uselist: + raise sa_exc.InvalidRequestError("Can't compare a colle" + "ction to an object or collection; use " + "contains() to test for membership.") + else: + return _orm_annotate(self.property._optimized_compare(other, + adapt_source=self.adapter)) + + def _criterion_exists(self, criterion=None, **kwargs): + if getattr(self, '_of_type', None): + info = inspect(self._of_type) + target_mapper, to_selectable, is_aliased_class = \ + info.mapper, info.selectable, info.is_aliased_class + if self.property._is_self_referential and not is_aliased_class: + to_selectable = to_selectable.alias() + + single_crit = target_mapper._single_table_criterion + if single_crit is not None: + if criterion is not None: + criterion = single_crit & criterion + else: + criterion = single_crit + else: + is_aliased_class = False + to_selectable = None + + if self.adapter: + source_selectable = self._source_selectable() + else: + source_selectable = None + + pj, sj, source, dest, secondary, target_adapter = \ + self.property._create_joins(dest_polymorphic=True, + dest_selectable=to_selectable, + source_selectable=source_selectable) + + for k in kwargs: + crit = getattr(self.property.mapper.class_, k) == kwargs[k] + if criterion is None: + criterion = crit + else: + criterion = criterion & crit + + # annotate the *local* side of the join condition, in the case + # of pj + sj this is the full primaryjoin, in the case of just + # pj its the local side of the primaryjoin. + if sj is not None: + j = _orm_annotate(pj) & sj + else: + j = _orm_annotate(pj, exclude=self.property.remote_side) + + if criterion is not None and target_adapter and not is_aliased_class: + # limit this adapter to annotated only? + criterion = target_adapter.traverse(criterion) + + # only have the "joined left side" of what we + # return be subject to Query adaption. The right + # side of it is used for an exists() subquery and + # should not correlate or otherwise reach out + # to anything in the enclosing query. + if criterion is not None: + criterion = criterion._annotate( + {'no_replacement_traverse': True}) + + crit = j & sql.True_._ifnone(criterion) + + ex = sql.exists([1], crit, from_obj=dest).correlate_except(dest) + if secondary is not None: + ex = ex.correlate_except(secondary) + return ex + + def any(self, criterion=None, **kwargs): + """Produce an expression that tests a collection against + particular criterion, using EXISTS. + + An expression like:: + + session.query(MyClass).filter( + MyClass.somereference.any(SomeRelated.x==2) + ) + + + Will produce a query like:: + + SELECT * FROM my_table WHERE + EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id + AND related.x=2) + + Because :meth:`~.RelationshipProperty.Comparator.any` uses + a correlated subquery, its performance is not nearly as + good when compared against large target tables as that of + using a join. + + :meth:`~.RelationshipProperty.Comparator.any` is particularly + useful for testing for empty collections:: + + session.query(MyClass).filter( + ~MyClass.somereference.any() + ) + + will produce:: + + SELECT * FROM my_table WHERE + NOT EXISTS (SELECT 1 FROM related WHERE + related.my_id=my_table.id) + + :meth:`~.RelationshipProperty.Comparator.any` is only + valid for collections, i.e. a :func:`.relationship` + that has ``uselist=True``. For scalar references, + use :meth:`~.RelationshipProperty.Comparator.has`. + + """ + if not self.property.uselist: + raise sa_exc.InvalidRequestError( + "'any()' not implemented for scalar " + "attributes. Use has()." + ) + + return self._criterion_exists(criterion, **kwargs) + + def has(self, criterion=None, **kwargs): + """Produce an expression that tests a scalar reference against + particular criterion, using EXISTS. + + An expression like:: + + session.query(MyClass).filter( + MyClass.somereference.has(SomeRelated.x==2) + ) + + + Will produce a query like:: + + SELECT * FROM my_table WHERE + EXISTS (SELECT 1 FROM related WHERE + related.id==my_table.related_id AND related.x=2) + + Because :meth:`~.RelationshipProperty.Comparator.has` uses + a correlated subquery, its performance is not nearly as + good when compared against large target tables as that of + using a join. + + :meth:`~.RelationshipProperty.Comparator.has` is only + valid for scalar references, i.e. a :func:`.relationship` + that has ``uselist=False``. For collection references, + use :meth:`~.RelationshipProperty.Comparator.any`. + + """ + if self.property.uselist: + raise sa_exc.InvalidRequestError( + "'has()' not implemented for collections. " + "Use any().") + return self._criterion_exists(criterion, **kwargs) + + def contains(self, other, **kwargs): + """Return a simple expression that tests a collection for + containment of a particular item. + + :meth:`~.RelationshipProperty.Comparator.contains` is + only valid for a collection, i.e. a + :func:`~.orm.relationship` that implements + one-to-many or many-to-many with ``uselist=True``. + + When used in a simple one-to-many context, an + expression like:: + + MyClass.contains(other) + + Produces a clause like:: + + mytable.id == + + Where ```` is the value of the foreign key + attribute on ``other`` which refers to the primary + key of its parent object. From this it follows that + :meth:`~.RelationshipProperty.Comparator.contains` is + very useful when used with simple one-to-many + operations. + + For many-to-many operations, the behavior of + :meth:`~.RelationshipProperty.Comparator.contains` + has more caveats. The association table will be + rendered in the statement, producing an "implicit" + join, that is, includes multiple tables in the FROM + clause which are equated in the WHERE clause:: + + query(MyClass).filter(MyClass.contains(other)) + + Produces a query like:: + + SELECT * FROM my_table, my_association_table AS + my_association_table_1 WHERE + my_table.id = my_association_table_1.parent_id + AND my_association_table_1.child_id = + + Where ```` would be the primary key of + ``other``. From the above, it is clear that + :meth:`~.RelationshipProperty.Comparator.contains` + will **not** work with many-to-many collections when + used in queries that move beyond simple AND + conjunctions, such as multiple + :meth:`~.RelationshipProperty.Comparator.contains` + expressions joined by OR. In such cases subqueries or + explicit "outer joins" will need to be used instead. + See :meth:`~.RelationshipProperty.Comparator.any` for + a less-performant alternative using EXISTS, or refer + to :meth:`.Query.outerjoin` as well as :ref:`ormtutorial_joins` + for more details on constructing outer joins. + + """ + if not self.property.uselist: + raise sa_exc.InvalidRequestError( + "'contains' not implemented for scalar " + "attributes. Use ==") + clause = self.property._optimized_compare(other, + adapt_source=self.adapter) + + if self.property.secondaryjoin is not None: + clause.negation_clause = \ + self.__negated_contains_or_equals(other) + + return clause + + def __negated_contains_or_equals(self, other): + if self.property.direction == MANYTOONE: + state = attributes.instance_state(other) + + def state_bindparam(x, state, col): + o = state.obj() # strong ref + return sql.bindparam(x, unique=True, callable_=lambda: \ + self.property.mapper._get_committed_attr_by_column(o, col)) + + def adapt(col): + if self.adapter: + return self.adapter(col) + else: + return col + + if self.property._use_get: + return sql.and_(*[ + sql.or_( + adapt(x) != state_bindparam(adapt(x), state, y), + adapt(x) == None) + for (x, y) in self.property.local_remote_pairs]) + + criterion = sql.and_(*[x == y for (x, y) in + zip( + self.property.mapper.primary_key, + self.property.\ + mapper.\ + primary_key_from_instance(other)) + ]) + return ~self._criterion_exists(criterion) + + def __ne__(self, other): + """Implement the ``!=`` operator. + + In a many-to-one context, such as:: + + MyClass.some_prop != + + This will typically produce a clause such as:: + + mytable.related_id != + + Where ```` is the primary key of the + given object. + + The ``!=`` operator provides partial functionality for non- + many-to-one comparisons: + + * Comparisons against collections are not supported. + Use + :meth:`~.RelationshipProperty.Comparator.contains` + in conjunction with :func:`~.expression.not_`. + * Compared to a scalar one-to-many, will produce a + clause that compares the target columns in the parent to + the given target. + * Compared to a scalar many-to-many, an alias + of the association table will be rendered as + well, forming a natural join that is part of the + main body of the query. This will not work for + queries that go beyond simple AND conjunctions of + comparisons, such as those which use OR. Use + explicit joins, outerjoins, or + :meth:`~.RelationshipProperty.Comparator.has` in + conjunction with :func:`~.expression.not_` for + more comprehensive non-many-to-one scalar + membership tests. + * Comparisons against ``None`` given in a one-to-many + or many-to-many context produce an EXISTS clause. + + """ + if isinstance(other, (util.NoneType, expression.Null)): + if self.property.direction == MANYTOONE: + return sql.or_(*[x != None for x in + self.property._calculated_foreign_keys]) + else: + return self._criterion_exists() + elif self.property.uselist: + raise sa_exc.InvalidRequestError("Can't compare a collection" + " to an object or collection; use " + "contains() to test for membership.") + else: + return self.__negated_contains_or_equals(other) + + @util.memoized_property + def property(self): + if mapperlib.Mapper._new_mappers: + mapperlib.Mapper._configure_all() + return self.prop + + def compare(self, op, value, + value_is_parent=False, + alias_secondary=True): + if op == operators.eq: + if value is None: + if self.uselist: + return ~sql.exists([1], self.primaryjoin) + else: + return self._optimized_compare(None, + value_is_parent=value_is_parent, + alias_secondary=alias_secondary) + else: + return self._optimized_compare(value, + value_is_parent=value_is_parent, + alias_secondary=alias_secondary) + else: + return op(self.comparator, value) + + def _optimized_compare(self, value, value_is_parent=False, + adapt_source=None, + alias_secondary=True): + if value is not None: + value = attributes.instance_state(value) + return self._lazy_strategy.lazy_clause(value, + reverse_direction=not value_is_parent, + alias_secondary=alias_secondary, + adapt_source=adapt_source) + + def __str__(self): + return str(self.parent.class_.__name__) + "." + self.key + + def merge(self, + session, + source_state, + source_dict, + dest_state, + dest_dict, + load, _recursive): + + if load: + for r in self._reverse_property: + if (source_state, r) in _recursive: + return + + if not "merge" in self._cascade: + return + + if self.key not in source_dict: + return + + if self.uselist: + instances = source_state.get_impl(self.key).\ + get(source_state, source_dict) + if hasattr(instances, '_sa_adapter'): + # convert collections to adapters to get a true iterator + instances = instances._sa_adapter + + if load: + # for a full merge, pre-load the destination collection, + # so that individual _merge of each item pulls from identity + # map for those already present. + # also assumes CollectionAttrbiuteImpl behavior of loading + # "old" list in any case + dest_state.get_impl(self.key).get(dest_state, dest_dict) + + dest_list = [] + for current in instances: + current_state = attributes.instance_state(current) + current_dict = attributes.instance_dict(current) + _recursive[(current_state, self)] = True + obj = session._merge(current_state, current_dict, + load=load, _recursive=_recursive) + if obj is not None: + dest_list.append(obj) + + if not load: + coll = attributes.init_state_collection(dest_state, + dest_dict, self.key) + for c in dest_list: + coll.append_without_event(c) + else: + dest_state.get_impl(self.key)._set_iterable(dest_state, + dest_dict, dest_list) + else: + current = source_dict[self.key] + if current is not None: + current_state = attributes.instance_state(current) + current_dict = attributes.instance_dict(current) + _recursive[(current_state, self)] = True + obj = session._merge(current_state, current_dict, + load=load, _recursive=_recursive) + else: + obj = None + + if not load: + dest_dict[self.key] = obj + else: + dest_state.get_impl(self.key).set(dest_state, + dest_dict, obj, None) + + def _value_as_iterable(self, state, dict_, key, + passive=attributes.PASSIVE_OFF): + """Return a list of tuples (state, obj) for the given + key. + + returns an empty list if the value is None/empty/PASSIVE_NO_RESULT + """ + + impl = state.manager[key].impl + x = impl.get(state, dict_, passive=passive) + if x is attributes.PASSIVE_NO_RESULT or x is None: + return [] + elif hasattr(impl, 'get_collection'): + return [ + (attributes.instance_state(o), o) for o in + impl.get_collection(state, dict_, x, passive=passive) + ] + else: + return [(attributes.instance_state(x), x)] + + def cascade_iterator(self, type_, state, dict_, + visited_states, halt_on=None): + #assert type_ in self._cascade + + # only actively lazy load on the 'delete' cascade + if type_ != 'delete' or self.passive_deletes: + passive = attributes.PASSIVE_NO_INITIALIZE + else: + passive = attributes.PASSIVE_OFF + + if type_ == 'save-update': + tuples = state.manager[self.key].impl.\ + get_all_pending(state, dict_) + + else: + tuples = self._value_as_iterable(state, dict_, self.key, + passive=passive) + + skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \ + not in self._cascade + + for instance_state, c in tuples: + if instance_state in visited_states: + continue + + if c is None: + # would like to emit a warning here, but + # would not be consistent with collection.append(None) + # current behavior of silently skipping. + # see [ticket:2229] + continue + + instance_dict = attributes.instance_dict(c) + + if halt_on and halt_on(instance_state): + continue + + if skip_pending and not instance_state.key: + continue + + instance_mapper = instance_state.manager.mapper + + if not instance_mapper.isa(self.mapper.class_manager.mapper): + raise AssertionError("Attribute '%s' on class '%s' " + "doesn't handle objects " + "of type '%s'" % ( + self.key, + self.parent.class_, + c.__class__ + )) + + visited_states.add(instance_state) + + yield c, instance_mapper, instance_state, instance_dict + + def _add_reverse_property(self, key): + other = self.mapper.get_property(key, _configure_mappers=False) + self._reverse_property.add(other) + other._reverse_property.add(self) + + if not other.mapper.common_parent(self.parent): + raise sa_exc.ArgumentError('reverse_property %r on ' + 'relationship %s references relationship %s, which ' + 'does not reference mapper %s' % (key, self, other, + self.parent)) + if self.direction in (ONETOMANY, MANYTOONE) and self.direction \ + == other.direction: + raise sa_exc.ArgumentError('%s and back-reference %s are ' + 'both of the same direction %r. Did you mean to ' + 'set remote_side on the many-to-one side ?' + % (other, self, self.direction)) + + @util.memoized_property + def mapper(self): + """Return the targeted :class:`.Mapper` for this + :class:`.RelationshipProperty`. + + This is a lazy-initializing static attribute. + + """ + if util.callable(self.argument) and \ + not isinstance(self.argument, (type, mapperlib.Mapper)): + argument = self.argument() + else: + argument = self.argument + + if isinstance(argument, type): + mapper_ = mapperlib.class_mapper(argument, + configure=False) + elif isinstance(self.argument, mapperlib.Mapper): + mapper_ = argument + else: + raise sa_exc.ArgumentError("relationship '%s' expects " + "a class or a mapper argument (received: %s)" + % (self.key, type(argument))) + return mapper_ + + @util.memoized_property + @util.deprecated("0.7", "Use .target") + def table(self): + """Return the selectable linked to this + :class:`.RelationshipProperty` object's target + :class:`.Mapper`. + """ + return self.target + + def do_init(self): + self._check_conflicts() + self._process_dependent_arguments() + self._setup_join_conditions() + self._check_cascade_settings(self._cascade) + self._post_init() + self._generate_backref() + super(RelationshipProperty, self).do_init() + self._lazy_strategy = self._get_strategy((("lazy", "select"),)) + + + def _process_dependent_arguments(self): + """Convert incoming configuration arguments to their + proper form. + + Callables are resolved, ORM annotations removed. + + """ + # accept callables for other attributes which may require + # deferred initialization. This technique is used + # by declarative "string configs" and some recipes. + for attr in ( + 'order_by', 'primaryjoin', 'secondaryjoin', + 'secondary', '_user_defined_foreign_keys', 'remote_side', + ): + attr_value = getattr(self, attr) + if util.callable(attr_value): + setattr(self, attr, attr_value()) + + # remove "annotations" which are present if mapped class + # descriptors are used to create the join expression. + for attr in 'primaryjoin', 'secondaryjoin': + val = getattr(self, attr) + if val is not None: + setattr(self, attr, _orm_deannotate( + expression._only_column_elements(val, attr)) + ) + + # ensure expressions in self.order_by, foreign_keys, + # remote_side are all columns, not strings. + if self.order_by is not False and self.order_by is not None: + self.order_by = [ + expression._only_column_elements(x, "order_by") + for x in + util.to_list(self.order_by)] + + self._user_defined_foreign_keys = \ + util.column_set( + expression._only_column_elements(x, "foreign_keys") + for x in util.to_column_set( + self._user_defined_foreign_keys + )) + + self.remote_side = \ + util.column_set( + expression._only_column_elements(x, "remote_side") + for x in + util.to_column_set(self.remote_side)) + + self.target = self.mapper.mapped_table + + + def _setup_join_conditions(self): + self._join_condition = jc = JoinCondition( + parent_selectable=self.parent.mapped_table, + child_selectable=self.mapper.mapped_table, + parent_local_selectable=self.parent.local_table, + child_local_selectable=self.mapper.local_table, + primaryjoin=self.primaryjoin, + secondary=self.secondary, + secondaryjoin=self.secondaryjoin, + parent_equivalents=self.parent._equivalent_columns, + child_equivalents=self.mapper._equivalent_columns, + consider_as_foreign_keys=self._user_defined_foreign_keys, + local_remote_pairs=self.local_remote_pairs, + remote_side=self.remote_side, + self_referential=self._is_self_referential, + prop=self, + support_sync=not self.viewonly, + can_be_synced_fn=self._columns_are_mapped + ) + self.primaryjoin = jc.deannotated_primaryjoin + self.secondaryjoin = jc.deannotated_secondaryjoin + self.direction = jc.direction + self.local_remote_pairs = jc.local_remote_pairs + self.remote_side = jc.remote_columns + self.local_columns = jc.local_columns + self.synchronize_pairs = jc.synchronize_pairs + self._calculated_foreign_keys = jc.foreign_key_columns + self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs + + def _check_conflicts(self): + """Test that this relationship is legal, warn about + inheritance conflicts.""" + + if not self.is_primary() \ + and not mapperlib.class_mapper( + self.parent.class_, + configure=False).has_property(self.key): + raise sa_exc.ArgumentError("Attempting to assign a new " + "relationship '%s' to a non-primary mapper on " + "class '%s'. New relationships can only be added " + "to the primary mapper, i.e. the very first mapper " + "created for class '%s' " % (self.key, + self.parent.class_.__name__, + self.parent.class_.__name__)) + + # check for conflicting relationship() on superclass + if not self.parent.concrete: + for inheriting in self.parent.iterate_to_root(): + if inheriting is not self.parent \ + and inheriting.has_property(self.key): + util.warn("Warning: relationship '%s' on mapper " + "'%s' supersedes the same relationship " + "on inherited mapper '%s'; this can " + "cause dependency issues during flush" + % (self.key, self.parent, inheriting)) + + def _get_cascade(self): + """Return the current cascade setting for this + :class:`.RelationshipProperty`. + """ + return self._cascade + + def _set_cascade(self, cascade): + cascade = CascadeOptions(cascade) + if 'mapper' in self.__dict__: + self._check_cascade_settings(cascade) + self._cascade = cascade + + if self._dependency_processor: + self._dependency_processor.cascade = cascade + + cascade = property(_get_cascade, _set_cascade) + + def _check_cascade_settings(self, cascade): + if cascade.delete_orphan and not self.single_parent \ + and (self.direction is MANYTOMANY or self.direction + is MANYTOONE): + raise sa_exc.ArgumentError( + 'On %s, delete-orphan cascade is not supported ' + 'on a many-to-many or many-to-one relationship ' + 'when single_parent is not set. Set ' + 'single_parent=True on the relationship().' + % self) + if self.direction is MANYTOONE and self.passive_deletes: + util.warn("On %s, 'passive_deletes' is normally configured " + "on one-to-many, one-to-one, many-to-many " + "relationships only." + % self) + + if self.passive_deletes == 'all' and \ + ("delete" in cascade or + "delete-orphan" in cascade): + raise sa_exc.ArgumentError( + "On %s, can't set passive_deletes='all' in conjunction " + "with 'delete' or 'delete-orphan' cascade" % self) + + if cascade.delete_orphan: + self.mapper.primary_mapper()._delete_orphans.append( + (self.key, self.parent.class_) + ) + + def _columns_are_mapped(self, *cols): + """Return True if all columns in the given collection are + mapped by the tables referenced by this :class:`.Relationship`. + + """ + for c in cols: + if self.secondary is not None \ + and self.secondary.c.contains_column(c): + continue + if not self.parent.mapped_table.c.contains_column(c) and \ + not self.target.c.contains_column(c): + return False + return True + + def _generate_backref(self): + """Interpret the 'backref' instruction to create a + :func:`.relationship` complementary to this one.""" + + if not self.is_primary(): + return + if self.backref is not None and not self.back_populates: + if isinstance(self.backref, util.string_types): + backref_key, kwargs = self.backref, {} + else: + backref_key, kwargs = self.backref + mapper = self.mapper.primary_mapper() + + check = set(mapper.iterate_to_root()).\ + union(mapper.self_and_descendants) + for m in check: + if m.has_property(backref_key): + raise sa_exc.ArgumentError("Error creating backref " + "'%s' on relationship '%s': property of that " + "name exists on mapper '%s'" % (backref_key, + self, m)) + + # determine primaryjoin/secondaryjoin for the + # backref. Use the one we had, so that + # a custom join doesn't have to be specified in + # both directions. + if self.secondary is not None: + # for many to many, just switch primaryjoin/ + # secondaryjoin. use the annotated + # pj/sj on the _join_condition. + pj = kwargs.pop('primaryjoin', + self._join_condition.secondaryjoin_minus_local) + sj = kwargs.pop('secondaryjoin', + self._join_condition.primaryjoin_minus_local) + else: + pj = kwargs.pop('primaryjoin', + self._join_condition.primaryjoin_reverse_remote) + sj = kwargs.pop('secondaryjoin', None) + if sj: + raise sa_exc.InvalidRequestError( + "Can't assign 'secondaryjoin' on a backref " + "against a non-secondary relationship." + ) + + foreign_keys = kwargs.pop('foreign_keys', + self._user_defined_foreign_keys) + parent = self.parent.primary_mapper() + kwargs.setdefault('viewonly', self.viewonly) + kwargs.setdefault('post_update', self.post_update) + kwargs.setdefault('passive_updates', self.passive_updates) + self.back_populates = backref_key + relationship = RelationshipProperty( + parent, self.secondary, + pj, sj, + foreign_keys=foreign_keys, + back_populates=self.key, + **kwargs) + mapper._configure_property(backref_key, relationship) + + if self.back_populates: + self._add_reverse_property(self.back_populates) + + def _post_init(self): + if self.uselist is None: + self.uselist = self.direction is not MANYTOONE + if not self.viewonly: + self._dependency_processor = \ + dependency.DependencyProcessor.from_relationship(self) + + @util.memoized_property + def _use_get(self): + """memoize the 'use_get' attribute of this RelationshipLoader's + lazyloader.""" + + strategy = self._lazy_strategy + return strategy.use_get + + @util.memoized_property + def _is_self_referential(self): + return self.mapper.common_parent(self.parent) + + def _create_joins(self, source_polymorphic=False, + source_selectable=None, dest_polymorphic=False, + dest_selectable=None, of_type=None): + if source_selectable is None: + if source_polymorphic and self.parent.with_polymorphic: + source_selectable = self.parent._with_polymorphic_selectable + + aliased = False + if dest_selectable is None: + if dest_polymorphic and self.mapper.with_polymorphic: + dest_selectable = self.mapper._with_polymorphic_selectable + aliased = True + else: + dest_selectable = self.mapper.mapped_table + + if self._is_self_referential and source_selectable is None: + dest_selectable = dest_selectable.alias() + aliased = True + else: + aliased = True + + dest_mapper = of_type or self.mapper + + single_crit = dest_mapper._single_table_criterion + aliased = aliased or (source_selectable is not None) + + primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable = \ + self._join_condition.join_targets( + source_selectable, dest_selectable, aliased, single_crit + ) + if source_selectable is None: + source_selectable = self.parent.local_table + if dest_selectable is None: + dest_selectable = self.mapper.local_table + return (primaryjoin, secondaryjoin, source_selectable, + dest_selectable, secondary, target_adapter) + +def _annotate_columns(element, annotations): + def clone(elem): + if isinstance(elem, expression.ColumnClause): + elem = elem._annotate(annotations.copy()) + elem._copy_internals(clone=clone) + return elem + + if element is not None: + element = clone(element) + return element + + +class JoinCondition(object): + def __init__(self, + parent_selectable, + child_selectable, + parent_local_selectable, + child_local_selectable, + primaryjoin=None, + secondary=None, + secondaryjoin=None, + parent_equivalents=None, + child_equivalents=None, + consider_as_foreign_keys=None, + local_remote_pairs=None, + remote_side=None, + self_referential=False, + prop=None, + support_sync=True, + can_be_synced_fn=lambda *c: True + ): + self.parent_selectable = parent_selectable + self.parent_local_selectable = parent_local_selectable + self.child_selectable = child_selectable + self.child_local_selectable = child_local_selectable + self.parent_equivalents = parent_equivalents + self.child_equivalents = child_equivalents + self.primaryjoin = primaryjoin + self.secondaryjoin = secondaryjoin + self.secondary = secondary + self.consider_as_foreign_keys = consider_as_foreign_keys + self._local_remote_pairs = local_remote_pairs + self._remote_side = remote_side + self.prop = prop + self.self_referential = self_referential + self.support_sync = support_sync + self.can_be_synced_fn = can_be_synced_fn + self._determine_joins() + self._annotate_fks() + self._annotate_remote() + self._annotate_local() + self._setup_pairs() + self._check_foreign_cols(self.primaryjoin, True) + if self.secondaryjoin is not None: + self._check_foreign_cols(self.secondaryjoin, False) + self._determine_direction() + self._check_remote_side() + self._log_joins() + + def _log_joins(self): + if self.prop is None: + return + log = self.prop.logger + log.info('%s setup primary join %s', self.prop, + self.primaryjoin) + log.info('%s setup secondary join %s', self.prop, + self.secondaryjoin) + log.info('%s synchronize pairs [%s]', self.prop, + ','.join('(%s => %s)' % (l, r) for (l, r) in + self.synchronize_pairs)) + log.info('%s secondary synchronize pairs [%s]', self.prop, + ','.join('(%s => %s)' % (l, r) for (l, r) in + self.secondary_synchronize_pairs or [])) + log.info('%s local/remote pairs [%s]', self.prop, + ','.join('(%s / %s)' % (l, r) for (l, r) in + self.local_remote_pairs)) + log.info('%s remote columns [%s]', self.prop, + ','.join('%s' % col for col in self.remote_columns) + ) + log.info('%s local columns [%s]', self.prop, + ','.join('%s' % col for col in self.local_columns) + ) + log.info('%s relationship direction %s', self.prop, + self.direction) + + def _determine_joins(self): + """Determine the 'primaryjoin' and 'secondaryjoin' attributes, + if not passed to the constructor already. + + This is based on analysis of the foreign key relationships + between the parent and target mapped selectables. + + """ + if self.secondaryjoin is not None and self.secondary is None: + raise sa_exc.ArgumentError( + "Property %s specified with secondary " + "join condition but " + "no secondary argument" % self.prop) + + # find a join between the given mapper's mapped table and + # the given table. will try the mapper's local table first + # for more specificity, then if not found will try the more + # general mapped table, which in the case of inheritance is + # a join. + try: + consider_as_foreign_keys = self.consider_as_foreign_keys or None + if self.secondary is not None: + if self.secondaryjoin is None: + self.secondaryjoin = \ + join_condition( + self.child_selectable, + self.secondary, + a_subset=self.child_local_selectable, + consider_as_foreign_keys=consider_as_foreign_keys + ) + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.secondary, + a_subset=self.parent_local_selectable, + consider_as_foreign_keys=consider_as_foreign_keys + ) + else: + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.child_selectable, + a_subset=self.parent_local_selectable, + consider_as_foreign_keys=consider_as_foreign_keys + ) + except sa_exc.NoForeignKeysError: + if self.secondary is not None: + raise sa_exc.NoForeignKeysError("Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are no foreign keys " + "linking these tables via secondary table '%s'. " + "Ensure that referencing columns are associated " + "with a ForeignKey or ForeignKeyConstraint, or " + "specify 'primaryjoin' and 'secondaryjoin' " + "expressions." + % (self.prop, self.secondary)) + else: + raise sa_exc.NoForeignKeysError("Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are no foreign keys " + "linking these tables. " + "Ensure that referencing columns are associated " + "with a ForeignKey or ForeignKeyConstraint, or " + "specify a 'primaryjoin' expression." + % self.prop) + except sa_exc.AmbiguousForeignKeysError: + if self.secondary is not None: + raise sa_exc.AmbiguousForeignKeysError( + "Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are multiple foreign key " + "paths linking the tables via secondary table '%s'. " + "Specify the 'foreign_keys' " + "argument, providing a list of those columns which " + "should be counted as containing a foreign key " + "reference from the secondary table to each of the " + "parent and child tables." + % (self.prop, self.secondary)) + else: + raise sa_exc.AmbiguousForeignKeysError( + "Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are multiple foreign key " + "paths linking the tables. Specify the " + "'foreign_keys' argument, providing a list of those " + "columns which should be counted as containing a " + "foreign key reference to the parent table." + % self.prop) + + @property + def primaryjoin_minus_local(self): + return _deep_deannotate(self.primaryjoin, values=("local", "remote")) + + @property + def secondaryjoin_minus_local(self): + return _deep_deannotate(self.secondaryjoin, values=("local", "remote")) + + @util.memoized_property + def primaryjoin_reverse_remote(self): + """Return the primaryjoin condition suitable for the + "reverse" direction. + + If the primaryjoin was delivered here with pre-existing + "remote" annotations, the local/remote annotations + are reversed. Otherwise, the local/remote annotations + are removed. + + """ + if self._has_remote_annotations: + def replace(element): + if "remote" in element._annotations: + v = element._annotations.copy() + del v['remote'] + v['local'] = True + return element._with_annotations(v) + elif "local" in element._annotations: + v = element._annotations.copy() + del v['local'] + v['remote'] = True + return element._with_annotations(v) + return visitors.replacement_traverse( + self.primaryjoin, {}, replace) + else: + if self._has_foreign_annotations: + # TODO: coverage + return _deep_deannotate(self.primaryjoin, + values=("local", "remote")) + else: + return _deep_deannotate(self.primaryjoin) + + def _has_annotation(self, clause, annotation): + for col in visitors.iterate(clause, {}): + if annotation in col._annotations: + return True + else: + return False + + @util.memoized_property + def _has_foreign_annotations(self): + return self._has_annotation(self.primaryjoin, "foreign") + + @util.memoized_property + def _has_remote_annotations(self): + return self._has_annotation(self.primaryjoin, "remote") + + def _annotate_fks(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'foreign' annotations marking columns + considered as foreign. + + """ + if self._has_foreign_annotations: + return + + if self.consider_as_foreign_keys: + self._annotate_from_fk_list() + else: + self._annotate_present_fks() + + def _annotate_from_fk_list(self): + def check_fk(col): + if col in self.consider_as_foreign_keys: + return col._annotate({"foreign": True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, + {}, + check_fk + ) + if self.secondaryjoin is not None: + self.secondaryjoin = visitors.replacement_traverse( + self.secondaryjoin, + {}, + check_fk + ) + + def _annotate_present_fks(self): + if self.secondary is not None: + secondarycols = util.column_set(self.secondary.c) + else: + secondarycols = set() + + def is_foreign(a, b): + if isinstance(a, schema.Column) and \ + isinstance(b, schema.Column): + if a.references(b): + return a + elif b.references(a): + return b + + if secondarycols: + if a in secondarycols and b not in secondarycols: + return a + elif b in secondarycols and a not in secondarycols: + return b + + def visit_binary(binary): + if not isinstance(binary.left, sql.ColumnElement) or \ + not isinstance(binary.right, sql.ColumnElement): + return + + if "foreign" not in binary.left._annotations and \ + "foreign" not in binary.right._annotations: + col = is_foreign(binary.left, binary.right) + if col is not None: + if col.compare(binary.left): + binary.left = binary.left._annotate( + {"foreign": True}) + elif col.compare(binary.right): + binary.right = binary.right._annotate( + {"foreign": True}) + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, + {}, + {"binary": visit_binary} + ) + if self.secondaryjoin is not None: + self.secondaryjoin = visitors.cloned_traverse( + self.secondaryjoin, + {}, + {"binary": visit_binary} + ) + + def _refers_to_parent_table(self): + """Return True if the join condition contains column + comparisons where both columns are in both tables. + + """ + pt = self.parent_selectable + mt = self.child_selectable + result = [False] + + def visit_binary(binary): + c, f = binary.left, binary.right + if ( + isinstance(c, expression.ColumnClause) and \ + isinstance(f, expression.ColumnClause) and \ + pt.is_derived_from(c.table) and \ + pt.is_derived_from(f.table) and \ + mt.is_derived_from(c.table) and \ + mt.is_derived_from(f.table) + ): + result[0] = True + visitors.traverse( + self.primaryjoin, + {}, + {"binary": visit_binary} + ) + return result[0] + + def _tables_overlap(self): + """Return True if parent/child tables have some overlap.""" + + return selectables_overlap(self.parent_selectable, self.child_selectable) + + def _annotate_remote(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'remote' annotations marking columns + considered as part of the 'remote' side. + + """ + if self._has_remote_annotations: + return + + if self.secondary is not None: + self._annotate_remote_secondary() + elif self._local_remote_pairs or self._remote_side: + self._annotate_remote_from_args() + elif self._refers_to_parent_table(): + self._annotate_selfref(lambda col: "foreign" in col._annotations) + elif self._tables_overlap(): + self._annotate_remote_with_overlap() + else: + self._annotate_remote_distinct_selectables() + + def _annotate_remote_secondary(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when 'secondary' is present. + + """ + def repl(element): + if self.secondary.c.contains_column(element): + return element._annotate({"remote": True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + self.secondaryjoin = visitors.replacement_traverse( + self.secondaryjoin, {}, repl) + + def _annotate_selfref(self, fn): + """annotate 'remote' in primaryjoin, secondaryjoin + when the relationship is detected as self-referential. + + """ + def visit_binary(binary): + equated = binary.left.compare(binary.right) + if isinstance(binary.left, expression.ColumnClause) and \ + isinstance(binary.right, expression.ColumnClause): + # assume one to many - FKs are "remote" + if fn(binary.left): + binary.left = binary.left._annotate({"remote": True}) + if fn(binary.right) and not equated: + binary.right = binary.right._annotate( + {"remote": True}) + else: + self._warn_non_column_elements() + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, {}, + {"binary": visit_binary}) + + def _annotate_remote_from_args(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the 'remote_side' or '_local_remote_pairs' + arguments are used. + + """ + if self._local_remote_pairs: + if self._remote_side: + raise sa_exc.ArgumentError( + "remote_side argument is redundant " + "against more detailed _local_remote_side " + "argument.") + + remote_side = [r for (l, r) in self._local_remote_pairs] + else: + remote_side = self._remote_side + + if self._refers_to_parent_table(): + self._annotate_selfref(lambda col: col in remote_side) + else: + def repl(element): + if element in remote_side: + return element._annotate({"remote": True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + + def _annotate_remote_with_overlap(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the parent/child tables have some set of + tables in common, though is not a fully self-referential + relationship. + + """ + def visit_binary(binary): + binary.left, binary.right = proc_left_right(binary.left, + binary.right) + binary.right, binary.left = proc_left_right(binary.right, + binary.left) + + def proc_left_right(left, right): + if isinstance(left, expression.ColumnClause) and \ + isinstance(right, expression.ColumnClause): + if self.child_selectable.c.contains_column(right) and \ + self.parent_selectable.c.contains_column(left): + right = right._annotate({"remote": True}) + else: + self._warn_non_column_elements() + + return left, right + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, {}, + {"binary": visit_binary}) + + def _annotate_remote_distinct_selectables(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the parent/child tables are entirely + separate. + + """ + def repl(element): + if self.child_selectable.c.contains_column(element) and \ + ( + not self.parent_local_selectable.c.\ + contains_column(element) + or self.child_local_selectable.c.\ + contains_column(element)): + return element._annotate({"remote": True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + + def _warn_non_column_elements(self): + util.warn( + "Non-simple column elements in primary " + "join condition for property %s - consider using " + "remote() annotations to mark the remote side." + % self.prop + ) + + def _annotate_local(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'local' annotations. + + This annotates all column elements found + simultaneously in the parent table + and the join condition that don't have a + 'remote' annotation set up from + _annotate_remote() or user-defined. + + """ + if self._has_annotation(self.primaryjoin, "local"): + return + + if self._local_remote_pairs: + local_side = util.column_set([l for (l, r) + in self._local_remote_pairs]) + else: + local_side = util.column_set(self.parent_selectable.c) + + def locals_(elem): + if "remote" not in elem._annotations and \ + elem in local_side: + return elem._annotate({"local": True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, locals_ + ) + + def _check_remote_side(self): + if not self.local_remote_pairs: + raise sa_exc.ArgumentError('Relationship %s could ' + 'not determine any unambiguous local/remote column ' + 'pairs based on join condition and remote_side ' + 'arguments. ' + 'Consider using the remote() annotation to ' + 'accurately mark those elements of the join ' + 'condition that are on the remote side of ' + 'the relationship.' + % (self.prop, )) + + def _check_foreign_cols(self, join_condition, primary): + """Check the foreign key columns collected and emit error + messages.""" + + can_sync = False + + foreign_cols = self._gather_columns_with_annotation( + join_condition, "foreign") + + has_foreign = bool(foreign_cols) + + if primary: + can_sync = bool(self.synchronize_pairs) + else: + can_sync = bool(self.secondary_synchronize_pairs) + + if self.support_sync and can_sync or \ + (not self.support_sync and has_foreign): + return + + # from here below is just determining the best error message + # to report. Check for a join condition using any operator + # (not just ==), perhaps they need to turn on "viewonly=True". + if self.support_sync and has_foreign and not can_sync: + err = "Could not locate any simple equality expressions "\ + "involving locally mapped foreign key columns for "\ + "%s join condition "\ + "'%s' on relationship %s." % ( + primary and 'primary' or 'secondary', + join_condition, + self.prop + ) + err += \ + " Ensure that referencing columns are associated "\ + "with a ForeignKey or ForeignKeyConstraint, or are "\ + "annotated in the join condition with the foreign() "\ + "annotation. To allow comparison operators other than "\ + "'==', the relationship can be marked as viewonly=True." + + raise sa_exc.ArgumentError(err) + else: + err = "Could not locate any relevant foreign key columns "\ + "for %s join condition '%s' on relationship %s." % ( + primary and 'primary' or 'secondary', + join_condition, + self.prop + ) + err += \ + ' Ensure that referencing columns are associated '\ + 'with a ForeignKey or ForeignKeyConstraint, or are '\ + 'annotated in the join condition with the foreign() '\ + 'annotation.' + raise sa_exc.ArgumentError(err) + + def _determine_direction(self): + """Determine if this relationship is one to many, many to one, + many to many. + + """ + if self.secondaryjoin is not None: + self.direction = MANYTOMANY + else: + parentcols = util.column_set(self.parent_selectable.c) + targetcols = util.column_set(self.child_selectable.c) + + # fk collection which suggests ONETOMANY. + onetomany_fk = targetcols.intersection( + self.foreign_key_columns) + + # fk collection which suggests MANYTOONE. + + manytoone_fk = parentcols.intersection( + self.foreign_key_columns) + + if onetomany_fk and manytoone_fk: + # fks on both sides. test for overlap of local/remote + # with foreign key + self_equated = self.remote_columns.intersection( + self.local_columns + ) + onetomany_local = self.remote_columns.\ + intersection(self.foreign_key_columns).\ + difference(self_equated) + manytoone_local = self.local_columns.\ + intersection(self.foreign_key_columns).\ + difference(self_equated) + if onetomany_local and not manytoone_local: + self.direction = ONETOMANY + elif manytoone_local and not onetomany_local: + self.direction = MANYTOONE + else: + raise sa_exc.ArgumentError( + "Can't determine relationship" + " direction for relationship '%s' - foreign " + "key columns within the join condition are present " + "in both the parent and the child's mapped tables. " + "Ensure that only those columns referring " + "to a parent column are marked as foreign, " + "either via the foreign() annotation or " + "via the foreign_keys argument." % self.prop) + elif onetomany_fk: + self.direction = ONETOMANY + elif manytoone_fk: + self.direction = MANYTOONE + else: + raise sa_exc.ArgumentError("Can't determine relationship " + "direction for relationship '%s' - foreign " + "key columns are present in neither the parent " + "nor the child's mapped tables" % self.prop) + + def _deannotate_pairs(self, collection): + """provide deannotation for the various lists of + pairs, so that using them in hashes doesn't incur + high-overhead __eq__() comparisons against + original columns mapped. + + """ + return [(x._deannotate(), y._deannotate()) + for x, y in collection] + + def _setup_pairs(self): + sync_pairs = [] + lrp = util.OrderedSet([]) + secondary_sync_pairs = [] + + def go(joincond, collection): + def visit_binary(binary, left, right): + if "remote" in right._annotations and \ + "remote" not in left._annotations and \ + self.can_be_synced_fn(left): + lrp.add((left, right)) + elif "remote" in left._annotations and \ + "remote" not in right._annotations and \ + self.can_be_synced_fn(right): + lrp.add((right, left)) + if binary.operator is operators.eq and \ + self.can_be_synced_fn(left, right): + if "foreign" in right._annotations: + collection.append((left, right)) + elif "foreign" in left._annotations: + collection.append((right, left)) + visit_binary_product(visit_binary, joincond) + + for joincond, collection in [ + (self.primaryjoin, sync_pairs), + (self.secondaryjoin, secondary_sync_pairs) + ]: + if joincond is None: + continue + go(joincond, collection) + + self.local_remote_pairs = self._deannotate_pairs(lrp) + self.synchronize_pairs = self._deannotate_pairs(sync_pairs) + self.secondary_synchronize_pairs = \ + self._deannotate_pairs(secondary_sync_pairs) + + @util.memoized_property + def remote_columns(self): + return self._gather_join_annotations("remote") + + @util.memoized_property + def local_columns(self): + return self._gather_join_annotations("local") + + @util.memoized_property + def foreign_key_columns(self): + return self._gather_join_annotations("foreign") + + @util.memoized_property + def deannotated_primaryjoin(self): + return _deep_deannotate(self.primaryjoin) + + @util.memoized_property + def deannotated_secondaryjoin(self): + if self.secondaryjoin is not None: + return _deep_deannotate(self.secondaryjoin) + else: + return None + + def _gather_join_annotations(self, annotation): + s = set( + self._gather_columns_with_annotation( + self.primaryjoin, annotation) + ) + if self.secondaryjoin is not None: + s.update( + self._gather_columns_with_annotation( + self.secondaryjoin, annotation) + ) + return set([x._deannotate() for x in s]) + + def _gather_columns_with_annotation(self, clause, *annotation): + annotation = set(annotation) + return set([ + col for col in visitors.iterate(clause, {}) + if annotation.issubset(col._annotations) + ]) + + def join_targets(self, source_selectable, + dest_selectable, + aliased, + single_crit=None): + """Given a source and destination selectable, create a + join between them. + + This takes into account aliasing the join clause + to reference the appropriate corresponding columns + in the target objects, as well as the extra child + criterion, equivalent column sets, etc. + + """ + + # place a barrier on the destination such that + # replacement traversals won't ever dig into it. + # its internal structure remains fixed + # regardless of context. + dest_selectable = _shallow_annotate( + dest_selectable, + {'no_replacement_traverse': True}) + + primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ + self.secondaryjoin, self.secondary + + # adjust the join condition for single table inheritance, + # in the case that the join is to a subclass + # this is analogous to the + # "_adjust_for_single_table_inheritance()" method in Query. + + if single_crit is not None: + if secondaryjoin is not None: + secondaryjoin = secondaryjoin & single_crit + else: + primaryjoin = primaryjoin & single_crit + + if aliased: + if secondary is not None: + secondary = secondary.alias() + primary_aliasizer = ClauseAdapter(secondary) + secondary_aliasizer = \ + ClauseAdapter(dest_selectable, + equivalents=self.child_equivalents).\ + chain(primary_aliasizer) + if source_selectable is not None: + primary_aliasizer = \ + ClauseAdapter(secondary).\ + chain(ClauseAdapter(source_selectable, + equivalents=self.parent_equivalents)) + secondaryjoin = \ + secondary_aliasizer.traverse(secondaryjoin) + else: + primary_aliasizer = ClauseAdapter(dest_selectable, + exclude_fn=_ColInAnnotations("local"), + equivalents=self.child_equivalents) + if source_selectable is not None: + primary_aliasizer.chain( + ClauseAdapter(source_selectable, + exclude_fn=_ColInAnnotations("remote"), + equivalents=self.parent_equivalents)) + secondary_aliasizer = None + + primaryjoin = primary_aliasizer.traverse(primaryjoin) + target_adapter = secondary_aliasizer or primary_aliasizer + target_adapter.exclude_fn = None + else: + target_adapter = None + return primaryjoin, secondaryjoin, secondary, \ + target_adapter, dest_selectable + + def create_lazy_clause(self, reverse_direction=False): + binds = util.column_dict() + lookup = util.column_dict() + equated_columns = util.column_dict() + + if reverse_direction and self.secondaryjoin is None: + for l, r in self.local_remote_pairs: + _list = lookup.setdefault(r, []) + _list.append((r, l)) + equated_columns[l] = r + else: + for l, r in self.local_remote_pairs: + _list = lookup.setdefault(l, []) + _list.append((l, r)) + equated_columns[r] = l + + def col_to_bind(col): + if col in lookup: + for tobind, equated in lookup[col]: + if equated in binds: + return None + if col not in binds: + binds[col] = sql.bindparam( + None, None, type_=col.type, unique=True) + return binds[col] + return None + + lazywhere = self.deannotated_primaryjoin + + if self.deannotated_secondaryjoin is None or not reverse_direction: + lazywhere = visitors.replacement_traverse( + lazywhere, {}, col_to_bind) + + if self.deannotated_secondaryjoin is not None: + secondaryjoin = self.deannotated_secondaryjoin + if reverse_direction: + secondaryjoin = visitors.replacement_traverse( + secondaryjoin, {}, col_to_bind) + lazywhere = sql.and_(lazywhere, secondaryjoin) + + bind_to_col = dict((binds[col].key, col) for col in binds) + + return lazywhere, bind_to_col, equated_columns + +class _ColInAnnotations(object): + """Seralizable equivalent to: + + lambda c: "name" in c._annotations + """ + def __init__(self, name): + self.name = name + + def __call__(self, c): + return self.name in c._annotations diff --git a/libs/sqlalchemy/orm/scoping.py b/libs/sqlalchemy/orm/scoping.py index b5bd65b2..c1f8f319 100644 --- a/libs/sqlalchemy/orm/scoping.py +++ b/libs/sqlalchemy/orm/scoping.py @@ -1,67 +1,98 @@ # orm/scoping.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import exc as sa_exc -from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, warn -from sqlalchemy.orm import class_mapper -from sqlalchemy.orm import exc as orm_exc -from sqlalchemy.orm.session import Session +from .. import exc as sa_exc +from ..util import ScopedRegistry, ThreadLocalRegistry, warn +from . import class_mapper, exc as orm_exc +from .session import Session -__all__ = ['ScopedSession'] +__all__ = ['scoped_session'] -class ScopedSession(object): - """Provides thread-local management of Sessions. +class scoped_session(object): + """Provides scoped management of :class:`.Session` objects. - Typical invocation is via the :func:`.scoped_session` - function:: - - Session = scoped_session(sessionmaker()) - - The internal registry is accessible, - and by default is an instance of :class:`.ThreadLocalRegistry`. - - See also: :ref:`unitofwork_contextual`. + See :ref:`unitofwork_contextual` for a tutorial. """ def __init__(self, session_factory, scopefunc=None): + """Construct a new :class:`.scoped_session`. + + :param session_factory: a factory to create new :class:`.Session` + instances. This is usually, but not necessarily, an instance + of :class:`.sessionmaker`. + :param scopefunc: optional function which defines + the current scope. If not passed, the :class:`.scoped_session` + object assumes "thread-local" scope, and will use + a Python ``threading.local()`` in order to maintain the current + :class:`.Session`. If passed, the function should return + a hashable token; this token will be used as the key in a + dictionary in order to store and retrieve the current + :class:`.Session`. + + """ self.session_factory = session_factory if scopefunc: self.registry = ScopedRegistry(session_factory, scopefunc) else: self.registry = ThreadLocalRegistry(session_factory) - def __call__(self, **kwargs): - if kwargs: - scope = kwargs.pop('scope', False) + def __call__(self, **kw): + """Return the current :class:`.Session`, creating it + using the session factory if not present. + + :param \**kw: Keyword arguments will be passed to the + session factory callable, if an existing :class:`.Session` + is not present. If the :class:`.Session` is present and + keyword arguments have been passed, + :exc:`~sqlalchemy.exc.InvalidRequestError` is raised. + + """ + if kw: + scope = kw.pop('scope', False) if scope is not None: if self.registry.has(): raise sa_exc.InvalidRequestError( "Scoped session is already present; " "no new arguments may be specified.") else: - sess = self.session_factory(**kwargs) + sess = self.session_factory(**kw) self.registry.set(sess) return sess else: - return self.session_factory(**kwargs) + return self.session_factory(**kw) else: return self.registry() def remove(self): - """Dispose of the current contextual session.""" + """Dispose of the current :class:`.Session`, if present. + + This will first call :meth:`.Session.close` method + on the current :class:`.Session`, which releases any existing + transactional/connection resources still being held; transactions + specifically are rolled back. The :class:`.Session` is then + discarded. Upon next usage within the same scope, + the :class:`.scoped_session` will produce a new + :class:`.Session` object. + + """ if self.registry.has(): self.registry().close() self.registry.clear() def configure(self, **kwargs): - """reconfigure the sessionmaker used by this ScopedSession.""" + """reconfigure the :class:`.sessionmaker` used by this + :class:`.scoped_session`. + + See :meth:`.sessionmaker.configure`. + + """ if self.registry.has(): warn('At least one scoped session is already present. ' @@ -71,8 +102,8 @@ class ScopedSession(object): self.session_factory.configure(**kwargs) def query_property(self, query_cls=None): - """return a class property which produces a `Query` object - against the class when called. + """return a class property which produces a :class:`.Query` object + against the class and the current :class:`.Session` when called. e.g.:: @@ -109,27 +140,37 @@ class ScopedSession(object): return None return query() +ScopedSession = scoped_session +"""Old name for backwards compatibility.""" + + def instrument(name): def do(self, *args, **kwargs): return getattr(self.registry(), name)(*args, **kwargs) return do + for meth in Session.public_methods: - setattr(ScopedSession, meth, instrument(meth)) + setattr(scoped_session, meth, instrument(meth)) + def makeprop(name): def set(self, attr): setattr(self.registry(), name, attr) + def get(self): return getattr(self.registry(), name) + return property(get, set) + for prop in ('bind', 'dirty', 'deleted', 'new', 'identity_map', - 'is_active', 'autoflush', 'no_autoflush'): - setattr(ScopedSession, prop, makeprop(prop)) + 'is_active', 'autoflush', 'no_autoflush', 'info'): + setattr(scoped_session, prop, makeprop(prop)) + def clslevel(name): def do(cls, *args, **kwargs): return getattr(Session, name)(*args, **kwargs) return classmethod(do) -for prop in ('close_all', 'object_session', 'identity_key'): - setattr(ScopedSession, prop, clslevel(prop)) +for prop in ('close_all', 'object_session', 'identity_key'): + setattr(scoped_session, prop, clslevel(prop)) diff --git a/libs/sqlalchemy/orm/session.py b/libs/sqlalchemy/orm/session.py index 8994a339..c10a0efc 100644 --- a/libs/sqlalchemy/orm/session.py +++ b/libs/sqlalchemy/orm/session.py @@ -1,106 +1,84 @@ # orm/session.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php - """Provides the Session class and related utilities.""" -import weakref -from itertools import chain -from sqlalchemy import util, sql, engine, log, exc as sa_exc -from sqlalchemy.sql import util as sql_util, expression -from sqlalchemy.orm import ( - SessionExtension, attributes, exc, query, unitofwork, util as mapperutil, state - ) -from sqlalchemy.orm.util import object_mapper as _object_mapper -from sqlalchemy.orm.util import class_mapper as _class_mapper -from sqlalchemy.orm.util import ( - _class_to_mapper, _state_mapper, - ) -from sqlalchemy.orm.mapper import Mapper, _none_set -from sqlalchemy.orm.unitofwork import UOWTransaction -from sqlalchemy.orm import identity -from sqlalchemy import event -from sqlalchemy.orm.events import SessionEvents + +import weakref +from .. import util, sql, engine, exc as sa_exc +from ..sql import util as sql_util, expression +from . import ( + SessionExtension, attributes, exc, query, + loading, identity + ) +from ..inspection import inspect +from .base import ( + object_mapper, class_mapper, + _class_to_mapper, _state_mapper, object_state, + _none_set, state_str, instance_str + ) +from .unitofwork import UOWTransaction +from . import state as statelib import sys -__all__ = ['Session', 'SessionTransaction', 'SessionExtension'] +__all__ = ['Session', 'SessionTransaction', 'SessionExtension', 'sessionmaker'] +_sessions = weakref.WeakValueDictionary() +"""Weak-referencing dictionary of :class:`.Session` objects. +""" -def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, - expire_on_commit=True, **kwargs): - """Generate a custom-configured :class:`.Session` class. - - The returned object is a subclass of :class:`.Session`, which, when instantiated - with no arguments, uses the keyword arguments configured here as its - constructor arguments. - - It is intended that the :func:`.sessionmaker()` function be called within the - global scope of an application, and the returned class be made available - to the rest of the application as the single class used to instantiate - sessions. - - e.g.:: - - # global scope - Session = sessionmaker(autoflush=False) - - # later, in a local scope, create and use a session: - sess = Session() - - Any keyword arguments sent to the constructor itself will override the - "configured" keywords:: - - Session = sessionmaker() - - # bind an individual session to a connection - sess = Session(bind=connection) - - The class also includes a special classmethod ``configure()``, which - allows additional configurational options to take place after the custom - ``Session`` class has been generated. This is useful particularly for - defining the specific ``Engine`` (or engines) to which new instances of - ``Session`` should be bound:: - - Session = sessionmaker() - Session.configure(bind=create_engine('sqlite:///foo.db')) - - sess = Session() - - For options, see the constructor options for :class:`.Session`. - +def _state_session(state): + """Given an :class:`.InstanceState`, return the :class:`.Session` + associated, if any. """ - kwargs['bind'] = bind - kwargs['autoflush'] = autoflush - kwargs['autocommit'] = autocommit - kwargs['expire_on_commit'] = expire_on_commit - - if class_ is None: - class_ = Session - - class Sess(object): - def __init__(self, **local_kwargs): - for k in kwargs: - local_kwargs.setdefault(k, kwargs[k]) - super(Sess, self).__init__(**local_kwargs) - - @classmethod - def configure(self, **new_kwargs): - """(Re)configure the arguments for this sessionmaker. - - e.g.:: - - Session = sessionmaker() - - Session.configure(bind=create_engine('sqlite://')) - """ - kwargs.update(new_kwargs) + if state.session_id: + try: + return _sessions[state.session_id] + except KeyError: + pass + return None - return type("SessionMaker", (Sess, class_), {}) +class _SessionClassMethods(object): + """Class-level methods for :class:`.Session`, :class:`.sessionmaker`.""" + + @classmethod + def close_all(cls): + """Close *all* sessions in memory.""" + + for sess in _sessions.values(): + sess.close() + + @classmethod + @util.dependencies("sqlalchemy.orm.util") + def identity_key(cls, orm_util, *args, **kwargs): + """Return an identity key. + + This is an alias of :func:`.util.identity_key`. + + """ + return orm_util.identity_key(*args, **kwargs) + + @classmethod + def object_session(cls, instance): + """Return the :class:`.Session` to which an object belongs. + + This is an alias of :func:`.object_session`. + + """ + + return object_session(instance) + + +ACTIVE = util.symbol('ACTIVE') +PREPARED = util.symbol('PREPARED') +COMMITTED = util.symbol('COMMITTED') +DEACTIVE = util.symbol('DEACTIVE') +CLOSED = util.symbol('CLOSED') class SessionTransaction(object): """A :class:`.Session`-level transaction. @@ -112,52 +90,55 @@ class SessionTransaction(object): back all at once. It also provides optional two-phase commit behavior which can augment this coordination operation. - The :attr:`.Session.transaction` attribute of :class:`.Session` refers to the - current :class:`.SessionTransaction` object in use, if any. + The :attr:`.Session.transaction` attribute of :class:`.Session` + refers to the current :class:`.SessionTransaction` object in use, if any. A :class:`.SessionTransaction` is associated with a :class:`.Session` in its default mode of ``autocommit=False`` immediately, associated with no database connections. As the :class:`.Session` is called upon to emit SQL on behalf of various :class:`.Engine` or :class:`.Connection` - objects, a corresponding :class:`.Connection` and associated :class:`.Transaction` - is added to a collection within the :class:`.SessionTransaction` object, - becoming one of the connection/transaction pairs maintained by the + objects, a corresponding :class:`.Connection` and associated + :class:`.Transaction` is added to a collection within the + :class:`.SessionTransaction` object, becoming one of the + connection/transaction pairs maintained by the :class:`.SessionTransaction`. The lifespan of the :class:`.SessionTransaction` ends when the - :meth:`.Session.commit`, :meth:`.Session.rollback` or :meth:`.Session.close` - methods are called. At this point, the :class:`.SessionTransaction` removes - its association with its parent :class:`.Session`. A :class:`.Session` - that is in ``autocommit=False`` mode will create a new - :class:`.SessionTransaction` to replace it immediately, whereas a - :class:`.Session` that's in ``autocommit=True`` + :meth:`.Session.commit`, :meth:`.Session.rollback` or + :meth:`.Session.close` methods are called. At this point, the + :class:`.SessionTransaction` removes its association with its parent + :class:`.Session`. A :class:`.Session` that is in ``autocommit=False`` + mode will create a new :class:`.SessionTransaction` to replace it + immediately, whereas a :class:`.Session` that's in ``autocommit=True`` mode will remain without a :class:`.SessionTransaction` until the :meth:`.Session.begin` method is called. Another detail of :class:`.SessionTransaction` behavior is that it is - capable of "nesting". This means that the :meth:`.begin` method can - be called while an existing :class:`.SessionTransaction` is already present, - producing a new :class:`.SessionTransaction` that temporarily replaces - the parent :class:`.SessionTransaction`. When a :class:`.SessionTransaction` - is produced as nested, it assigns itself to the :attr:`.Session.transaction` - attribute. When it is ended via :meth:`.Session.commit` or :meth:`.Session.rollback`, - it restores its parent :class:`.SessionTransaction` back onto the - :attr:`.Session.transaction` attribute. The - behavior is effectively a stack, where :attr:`.Session.transaction` refers - to the current head of the stack. + capable of "nesting". This means that the :meth:`.Session.begin` method + can be called while an existing :class:`.SessionTransaction` is already + present, producing a new :class:`.SessionTransaction` that temporarily + replaces the parent :class:`.SessionTransaction`. When a + :class:`.SessionTransaction` is produced as nested, it assigns itself to + the :attr:`.Session.transaction` attribute. When it is ended via + :meth:`.Session.commit` or :meth:`.Session.rollback`, it restores its + parent :class:`.SessionTransaction` back onto the + :attr:`.Session.transaction` attribute. The behavior is effectively a + stack, where :attr:`.Session.transaction` refers to the current head of + the stack. - The purpose of this stack is to allow nesting of :meth:`.rollback` or - :meth:`.commit` calls in context with various flavors of :meth:`.begin`. - This nesting behavior applies to when :meth:`.Session.begin_nested` - is used to emit a SAVEPOINT transaction, and is also used to produce - a so-called "subtransaction" which allows a block of code to use a + The purpose of this stack is to allow nesting of + :meth:`.Session.rollback` or :meth:`.Session.commit` calls in context + with various flavors of :meth:`.Session.begin`. This nesting behavior + applies to when :meth:`.Session.begin_nested` is used to emit a + SAVEPOINT transaction, and is also used to produce a so-called + "subtransaction" which allows a block of code to use a begin/rollback/commit sequence regardless of whether or not its enclosing - code block has begun a transaction. The :meth:`.flush` method, whether called - explicitly or via autoflush, is the primary consumer of the "subtransaction" - feature, in that it wishes to guarantee that it works within in a transaction block - regardless of whether or not the :class:`.Session` is in transactional mode - when the method is called. + code block has begun a transaction. The :meth:`.flush` method, whether + called explicitly or via autoflush, is the primary consumer of the + "subtransaction" feature, in that it wishes to guarantee that it works + within in a transaction block regardless of whether or not the + :class:`.Session` is in transactional mode when the method is called. See also: @@ -186,8 +167,7 @@ class SessionTransaction(object): self._connections = {} self._parent = parent self.nested = nested - self._active = True - self._prepared = False + self._state = ACTIVE if not parent and nested: raise sa_exc.InvalidRequestError( "Can't start a SAVEPOINT transaction when no existing " @@ -196,44 +176,59 @@ class SessionTransaction(object): if self.session._enable_transaction_accounting: self._take_snapshot() + if self.session.dispatch.after_transaction_create: + self.session.dispatch.after_transaction_create(self.session, self) + @property def is_active(self): - return self.session is not None and self._active + return self.session is not None and self._state is ACTIVE - def _assert_is_active(self): - self._assert_is_open() - if not self._active: - if self._rollback_exception: - raise sa_exc.InvalidRequestError( - "This Session's transaction has been rolled back " - "due to a previous exception during flush." - " To begin a new transaction with this Session, " - "first issue Session.rollback()." - " Original exception was: %s" - % self._rollback_exception + def _assert_active(self, prepared_ok=False, + rollback_ok=False, + deactive_ok=False, + closed_msg="This transaction is closed"): + if self._state is COMMITTED: + raise sa_exc.InvalidRequestError( + "This session is in 'committed' state; no further " + "SQL can be emitted within this transaction." ) - else: + elif self._state is PREPARED: + if not prepared_ok: raise sa_exc.InvalidRequestError( - "This Session's transaction has been rolled back " - "by a nested rollback() call. To begin a new " - "transaction, issue Session.rollback() first." + "This session is in 'prepared' state; no further " + "SQL can be emitted within this transaction." ) - - def _assert_is_open(self, error_msg="The transaction is closed"): - if self.session is None: - raise sa_exc.ResourceClosedError(error_msg) + elif self._state is DEACTIVE: + if not deactive_ok and not rollback_ok: + if self._rollback_exception: + raise sa_exc.InvalidRequestError( + "This Session's transaction has been rolled back " + "due to a previous exception during flush." + " To begin a new transaction with this Session, " + "first issue Session.rollback()." + " Original exception was: %s" + % self._rollback_exception + ) + elif not deactive_ok: + raise sa_exc.InvalidRequestError( + "This Session's transaction has been rolled back " + "by a nested rollback() call. To begin a new " + "transaction, issue Session.rollback() first." + ) + elif self._state is CLOSED: + raise sa_exc.ResourceClosedError(closed_msg) @property def _is_transaction_boundary(self): return self.nested or not self._parent def connection(self, bindkey, **kwargs): - self._assert_is_active() - engine = self.session.get_bind(bindkey, **kwargs) - return self._connection_for_bind(engine) + self._assert_active() + bind = self.session.get_bind(bindkey, **kwargs) + return self._connection_for_bind(bind) def _begin(self, nested=False): - self._assert_is_active() + self._assert_active() return SessionTransaction( self.session, self, nested=nested) @@ -251,6 +246,7 @@ class SessionTransaction(object): if not self._is_transaction_boundary: self._new = self._parent._new self._deleted = self._parent._deleted + self._dirty = self._parent._dirty self._key_switches = self._parent._key_switches return @@ -259,9 +255,10 @@ class SessionTransaction(object): self._new = weakref.WeakKeyDictionary() self._deleted = weakref.WeakKeyDictionary() + self._dirty = weakref.WeakKeyDictionary() self._key_switches = weakref.WeakKeyDictionary() - def _restore_snapshot(self): + def _restore_snapshot(self, dirty_only=False): assert self._is_transaction_boundary for s in set(self._new).union(self.session._new): @@ -283,17 +280,22 @@ class SessionTransaction(object): assert not self.session._deleted for s in self.session.identity_map.all_states(): - s.expire(s.dict, self.session.identity_map._modified) + if not dirty_only or s.modified or s in self._dirty: + s._expire(s.dict, self.session.identity_map._modified) def _remove_snapshot(self): assert self._is_transaction_boundary if not self.nested and self.session.expire_on_commit: for s in self.session.identity_map.all_states(): - s.expire(s.dict, self.session.identity_map._modified) + s._expire(s.dict, self.session.identity_map._modified) + for s in self._deleted: + s.session_id = None + self._deleted.clear() + def _connection_for_bind(self, bind): - self._assert_is_active() + self._assert_active() if bind in self._connections: return self._connections[bind][0] @@ -327,11 +329,12 @@ class SessionTransaction(object): def prepare(self): if self._parent is not None or not self.session.twophase: raise sa_exc.InvalidRequestError( - "Only root two phase transactions of can be prepared") + "'twophase' mode not enabled, or not root transaction; " + "can't prepare.") self._prepare_impl() def _prepare_impl(self): - self._assert_is_active() + self._assert_active() if self._parent is None or self.nested: self.session.dispatch.before_commit(self.session) @@ -341,7 +344,7 @@ class SessionTransaction(object): subtransaction.commit() if not self.session._flushing: - for _flush_guard in xrange(100): + for _flush_guard in range(100): if self.session._is_clean(): break self.session.flush() @@ -356,21 +359,21 @@ class SessionTransaction(object): for t in set(self._connections.values()): t[1].prepare() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() - self._deactivate() - self._prepared = True + self._state = PREPARED def commit(self): - self._assert_is_open() - if not self._prepared: + self._assert_active(prepared_ok=True) + if self._state is not PREPARED: self._prepare_impl() if self._parent is None or self.nested: for t in set(self._connections.values()): t[1].commit() + self._state = COMMITTED self.session.dispatch.after_commit(self.session) if self.session._enable_transaction_accounting: @@ -380,33 +383,33 @@ class SessionTransaction(object): return self._parent def rollback(self, _capture_exception=False): - self._assert_is_open() + self._assert_active(prepared_ok=True, rollback_ok=True) stx = self.session.transaction if stx is not self: for subtransaction in stx._iterate_parents(upto=self): subtransaction.close() - if self.is_active or self._prepared: + if self._state in (ACTIVE, PREPARED): for transaction in self._iterate_parents(): if transaction._parent is None or transaction.nested: transaction._rollback_impl() - transaction._deactivate() + transaction._state = DEACTIVE break else: - transaction._deactivate() + transaction._state = DEACTIVE sess = self.session if self.session._enable_transaction_accounting and \ - not sess._is_clean(): + not sess._is_clean(): # if items were added, deleted, or mutated # here, we need to re-restore the snapshot util.warn( "Session's state has been changed on " "a non-active transaction - this state " "will be discarded.") - self._restore_snapshot() + self._restore_snapshot(dirty_only=self.nested) self.close() if self._parent and _capture_exception: @@ -421,25 +424,27 @@ class SessionTransaction(object): t[1].rollback() if self.session._enable_transaction_accounting: - self._restore_snapshot() + self._restore_snapshot(dirty_only=self.nested) self.session.dispatch.after_rollback(self.session) - def _deactivate(self): - self._active = False - def close(self): self.session.transaction = self._parent if self._parent is None: for connection, transaction, autoclose in \ - set(self._connections.values()): + set(self._connections.values()): if autoclose: connection.close() else: transaction.close() + + self._state = CLOSED + if self.session.dispatch.after_transaction_end: + self.session.dispatch.after_transaction_end(self.session, self) + + if self._parent is None: if not self.session.autocommit: self.session.begin() - self._deactivate() self.session = None self._connections = None @@ -447,23 +452,23 @@ class SessionTransaction(object): return self def __exit__(self, type, value, traceback): - self._assert_is_open("Cannot end transaction context. The transaction " - "was closed from within the context") + self._assert_active(deactive_ok=True, prepared_ok=True) if self.session.transaction is None: return if type is None: try: self.commit() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() else: self.rollback() -class Session(object): + +class Session(_SessionClassMethods): """Manages persistence operations for ORM-mapped objects. - The Session's usage paradigm is described at :ref:`session_toplevel`. + The Session's usage paradigm is described at :doc:`/orm/session`. """ @@ -476,54 +481,65 @@ class Session(object): 'merge', 'query', 'refresh', 'rollback', 'scalar') - def __init__(self, bind=None, autoflush=True, expire_on_commit=True, _enable_transaction_accounting=True, autocommit=False, twophase=False, weak_identity_map=True, binds=None, extension=None, + info=None, query_cls=query.Query): """Construct a new Session. - See also the :func:`.sessionmaker` function which is used to + See also the :class:`.sessionmaker` function which is used to generate a :class:`.Session`-producing callable with a given set of arguments. - :param autocommit: Defaults to ``False``. When ``True``, the ``Session`` - does not keep a persistent transaction running, and will acquire - connections from the engine on an as-needed basis, returning them - immediately after their use. Flushes will begin and commit (or possibly - rollback) their own transaction if no transaction is present. When using - this mode, the `session.begin()` method may be used to begin a - transaction explicitly. + :param autocommit: - Leaving it on its default value of ``False`` means that the ``Session`` - will acquire a connection and begin a transaction the first time it is - used, which it will maintain persistently until ``rollback()``, - ``commit()``, or ``close()`` is called. When the transaction is released - by any of these methods, the ``Session`` is ready for the next usage, - which will again acquire and maintain a new connection/transaction. + .. warning:: + + The autocommit flag is **not for general use**, and if it is used, + queries should only be invoked within the span of a + :meth:`.Session.begin` / :meth:`.Session.commit` pair. Executing + queries outside of a demarcated transaction is a legacy mode + of usage, and can in some cases lead to concurrent connection + checkouts. + + Defaults to ``False``. When ``True``, the + :class:`.Session` does not keep a persistent transaction running, and + will acquire connections from the engine on an as-needed basis, + returning them immediately after their use. Flushes will begin and + commit (or possibly rollback) their own transaction if no + transaction is present. When using this mode, the + :meth:`.Session.begin` method is used to explicitly start + transactions. + + .. seealso:: + + :ref:`session_autocommit` :param autoflush: When ``True``, all query operations will issue a ``flush()`` call to this ``Session`` before proceeding. This is a - convenience feature so that ``flush()`` need not be called repeatedly - in order for database queries to retrieve results. It's typical that - ``autoflush`` is used in conjunction with ``autocommit=False``. In this - scenario, explicit calls to ``flush()`` are rarely needed; you usually - only need to call ``commit()`` (which flushes) to finalize changes. + convenience feature so that ``flush()`` need not be called + repeatedly in order for database queries to retrieve results. It's + typical that ``autoflush`` is used in conjunction with + ``autocommit=False``. In this scenario, explicit calls to + ``flush()`` are rarely needed; you usually only need to call + ``commit()`` (which flushes) to finalize changes. :param bind: An optional ``Engine`` or ``Connection`` to which this ``Session`` should be bound. When specified, all SQL operations performed by this session will execute via this connectable. - :param binds: An optional dictionary which contains more granular "bind" - information than the ``bind`` parameter provides. This dictionary can - map individual ``Table`` instances as well as ``Mapper`` instances to - individual ``Engine`` or ``Connection`` objects. Operations which - proceed relative to a particular ``Mapper`` will consult this - dictionary for the direct ``Mapper`` instance as well as the mapper's - ``mapped_table`` attribute in order to locate an connectable to use. - The full resolution is described in the ``get_bind()`` method of - ``Session``. Usage looks like:: + :param binds: An optional dictionary which contains more granular + "bind" information than the ``bind`` parameter provides. This + dictionary can map individual ``Table`` instances as well as + ``Mapper`` instances to individual ``Engine`` or ``Connection`` + objects. Operations which proceed relative to a particular + ``Mapper`` will consult this dictionary for the direct ``Mapper`` + instance as well as the mapper's ``mapped_table`` attribute in + order to locate an connectable to use. The full resolution is + described in the ``get_bind()`` method of ``Session``. + Usage looks like:: Session = sessionmaker(binds={ SomeMappedClass: create_engine('postgresql://engine1'), @@ -531,42 +547,51 @@ class Session(object): some_table: create_engine('postgresql://engine3'), }) - Also see the :meth:`.Session.bind_mapper` and :meth:`.Session.bind_table` methods. + Also see the :meth:`.Session.bind_mapper` + and :meth:`.Session.bind_table` methods. :param \class_: Specify an alternate class other than - ``sqlalchemy.orm.session.Session`` which should be used by the returned - class. This is the only argument that is local to the + ``sqlalchemy.orm.session.Session`` which should be used by the + returned class. This is the only argument that is local to the ``sessionmaker()`` function, and is not sent directly to the constructor for ``Session``. :param _enable_transaction_accounting: Defaults to ``True``. A - legacy-only flag which when ``False`` disables *all* 0.5-style object - accounting on transaction boundaries, including auto-expiry of - instances on rollback and commit, maintenance of the "new" and + legacy-only flag which when ``False`` disables *all* 0.5-style + object accounting on transaction boundaries, including auto-expiry + of instances on rollback and commit, maintenance of the "new" and "deleted" lists upon rollback, and autoflush of pending changes upon begin(), all of which are interdependent. :param expire_on_commit: Defaults to ``True``. When ``True``, all - instances will be fully expired after each ``commit()``, so that all - attribute/object access subsequent to a completed transaction will load - from the most recent database state. + instances will be fully expired after each ``commit()``, so that + all attribute/object access subsequent to a completed transaction + will load from the most recent database state. :param extension: An optional :class:`~.SessionExtension` instance, or a list - of such instances, which will receive pre- and post- commit and flush - events, as well as a post-rollback event. **Deprecated.** + of such instances, which will receive pre- and post- commit and + flush events, as well as a post-rollback event. **Deprecated.** Please see :class:`.SessionEvents`. - :param query_cls: Class which should be used to create new Query objects, - as returned by the ``query()`` method. Defaults to + :param info: optional dictionary of arbitrary data to be associated + with this :class:`.Session`. Is available via the :attr:`.Session.info` + attribute. Note the dictionary is copied at construction time so + that modifications to the per-:class:`.Session` dictionary will be local + to that :class:`.Session`. + + .. versionadded:: 0.9.0 + + :param query_cls: Class which should be used to create new Query + objects, as returned by the ``query()`` method. Defaults to :class:`~sqlalchemy.orm.query.Query`. :param twophase: When ``True``, all transactions will be started as a "two phase" transaction, i.e. using the "two phase" semantics of the database in use along with an XID. During a ``commit()``, after ``flush()`` has been issued for all attached databases, the - ``prepare()`` method on each database's ``TwoPhaseTransaction`` will - be called. This allows each database to roll back the entire + ``prepare()`` method on each database's ``TwoPhaseTransaction`` + will be called. This allows each database to roll back the entire transaction, before each transaction is committed. :param weak_identity_map: Defaults to ``True`` - when set to @@ -590,6 +615,7 @@ class Session(object): self.bind = bind self.__binds = {} self._flushing = False + self._warn_on_events = False self.transaction = None self.hash_key = _new_sessionid() self.autoflush = autoflush @@ -598,29 +624,48 @@ class Session(object): self._enable_transaction_accounting = _enable_transaction_accounting self.twophase = twophase self._query_cls = query_cls + if info: + self.info.update(info) if extension: for ext in util.to_list(extension): SessionExtension._adapt_listener(self, ext) if binds is not None: - for mapperortable, bind in binds.iteritems(): - if isinstance(mapperortable, (type, Mapper)): + for mapperortable, bind in binds.items(): + insp = inspect(mapperortable) + if insp.is_selectable: + self.bind_table(mapperortable, bind) + elif insp.is_mapper: self.bind_mapper(mapperortable, bind) else: - self.bind_table(mapperortable, bind) + assert False + if not self.autocommit: self.begin() _sessions[self.hash_key] = self - dispatch = event.dispatcher(SessionEvents) - connection_callable = None transaction = None """The current active or inactive :class:`.SessionTransaction`.""" + @util.memoized_property + def info(self): + """A user-modifiable dictionary. + + The initial value of this dictioanry can be populated using the + ``info`` argument to the :class:`.Session` constructor or + :class:`.sessionmaker` constructor or factory methods. The dictionary + here is always local to this :class:`.Session` and can be modified + independently of all other :class:`.Session` objects. + + .. versionadded:: 0.9.0 + + """ + return {} + def begin(self, subtransactions=False, nested=False): """Begin a transaction on this Session. @@ -628,13 +673,14 @@ class Session(object): transaction or nested transaction, an error is raised, unless ``subtransactions=True`` or ``nested=True`` is specified. - The ``subtransactions=True`` flag indicates that this :meth:`~.Session.begin` - can create a subtransaction if a transaction is already in progress. - For documentation on subtransactions, please see :ref:`session_subtransactions`. + The ``subtransactions=True`` flag indicates that this + :meth:`~.Session.begin` can create a subtransaction if a transaction + is already in progress. For documentation on subtransactions, please + see :ref:`session_subtransactions`. The ``nested`` flag begins a SAVEPOINT transaction and is equivalent - to calling :meth:`~.Session.begin_nested`. For documentation on SAVEPOINT - transactions, please see :ref:`session_begin_nested`. + to calling :meth:`~.Session.begin_nested`. For documentation on + SAVEPOINT transactions, please see :ref:`session_begin_nested`. """ if self.transaction is not None: @@ -643,8 +689,8 @@ class Session(object): nested=nested) else: raise sa_exc.InvalidRequestError( - "A transaction is already begun. Use subtransactions=True " - "to allow subtransactions.") + "A transaction is already begun. Use " + "subtransactions=True to allow subtransactions.") else: self.transaction = SessionTransaction( self, nested=nested) @@ -672,6 +718,10 @@ class Session(object): to the first real transaction are closed. Subtransactions occur when begin() is called multiple times. + .. seealso:: + + :ref:`session_rollback` + """ if self.transaction is None: pass @@ -682,24 +732,29 @@ class Session(object): """Flush pending changes and commit the current transaction. If no transaction is in progress, this method raises an - InvalidRequestError. + :exc:`~sqlalchemy.exc.InvalidRequestError`. By default, the :class:`.Session` also expires all database loaded state on all ORM-managed attributes after transaction commit. This so that subsequent operations load the most recent data from the database. This behavior can be disabled using - the ``expire_on_commit=False`` option to :func:`.sessionmaker` or + the ``expire_on_commit=False`` option to :class:`.sessionmaker` or the :class:`.Session` constructor. If a subtransaction is in effect (which occurs when begin() is called multiple times), the subtransaction will be closed, and the next call to ``commit()`` will operate on the enclosing transaction. - For a session configured with autocommit=False, a new transaction will + When using the :class:`.Session` in its default mode of + ``autocommit=False``, a new transaction will be begun immediately after the commit, but note that the newly begun transaction does *not* use any connection resources until the first SQL is actually emitted. + .. seealso:: + + :ref:`session_committing` + """ if self.transaction is None: if not self.autocommit: @@ -713,10 +768,11 @@ class Session(object): """Prepare the current transaction in progress for two phase commit. If no transaction is in progress, this method raises an - InvalidRequestError. + :exc:`~sqlalchemy.exc.InvalidRequestError`. Only root transactions of two phase sessions can be prepared. If the - current transaction is not such, an InvalidRequestError is raised. + current transaction is not such, an + :exc:`~sqlalchemy.exc.InvalidRequestError` is raised. """ if self.transaction is None: @@ -735,18 +791,20 @@ class Session(object): :class:`.Session` object's transactional state. If this :class:`.Session` is configured with ``autocommit=False``, - either the :class:`.Connection` corresponding to the current transaction - is returned, or if no transaction is in progress, a new one is begun - and the :class:`.Connection` returned (note that no transactional state - is established with the DBAPI until the first SQL statement is emitted). + either the :class:`.Connection` corresponding to the current + transaction is returned, or if no transaction is in progress, a new + one is begun and the :class:`.Connection` returned (note that no + transactional state is established with the DBAPI until the first + SQL statement is emitted). - Alternatively, if this :class:`.Session` is configured with ``autocommit=True``, - an ad-hoc :class:`.Connection` is returned using :meth:`.Engine.contextual_connect` - on the underlying :class:`.Engine`. + Alternatively, if this :class:`.Session` is configured with + ``autocommit=True``, an ad-hoc :class:`.Connection` is returned + using :meth:`.Engine.contextual_connect` on the underlying + :class:`.Engine`. - Ambiguity in multi-bind or unbound :class:`.Session` objects can be resolved through - any of the optional keyword arguments. This ultimately makes usage of the - :meth:`.get_bind` method for resolution. + Ambiguity in multi-bind or unbound :class:`.Session` objects can be + resolved through any of the optional keyword arguments. This + ultimately makes usage of the :meth:`.get_bind` method for resolution. :param bind: Optional :class:`.Engine` to be used as the bind. If @@ -765,11 +823,12 @@ class Session(object): etc.) which will be used to locate a bind, if a bind cannot otherwise be identified. - :param close_with_result: Passed to :meth:`Engine.connect`, indicating - the :class:`.Connection` should be considered "single use", automatically - closing when the first result set is closed. This flag only has - an effect if this :class:`.Session` is configured with ``autocommit=True`` - and does not already have a transaction in progress. + :param close_with_result: Passed to :meth:`.Engine.connect`, indicating + the :class:`.Connection` should be considered "single use", + automatically closing when the first result set is closed. This + flag only has an effect if this :class:`.Session` is configured with + ``autocommit=True`` and does not already have a transaction + in progress. :param \**kw: Additional keyword arguments are sent to :meth:`get_bind()`, @@ -811,8 +870,8 @@ class Session(object): :func:`~.sql.expression.delete`, and :func:`~.sql.expression.text`. Plain SQL strings can be passed as well, which in the case of :meth:`.Session.execute` only - will be interpreted the same as if it were passed via a :func:`~.expression.text` - construct. That is, the following usage:: + will be interpreted the same as if it were passed via a + :func:`~.expression.text` construct. That is, the following usage:: result = session.execute( "SELECT * FROM user WHERE id=:param", @@ -828,9 +887,10 @@ class Session(object): ) The second positional argument to :meth:`.Session.execute` is an - optional parameter set. Similar to that of :meth:`.Connection.execute`, whether this - is passed as a single dictionary, or a list of dictionaries, determines - whether the DBAPI cursor's ``execute()`` or ``executemany()`` is used to execute the + optional parameter set. Similar to that of + :meth:`.Connection.execute`, whether this is passed as a single + dictionary, or a list of dictionaries, determines whether the DBAPI + cursor's ``execute()`` or ``executemany()`` is used to execute the statement. An INSERT construct may be invoked for a single row:: result = session.execute(users.insert(), {"id": 7, "name": "somename"}) @@ -859,12 +919,13 @@ class Session(object): The :class:`.ResultProxy` returned by the :meth:`.Session.execute` method is returned with the "close_with_result" flag set to true; the significance of this flag is that if this :class:`.Session` is - autocommitting and does not have a transaction-dedicated :class:`.Connection` - available, a temporary :class:`.Connection` is established for the - statement execution, which is closed (meaning, returned to the connection - pool) when the :class:`.ResultProxy` has consumed all available data. - This applies *only* when the :class:`.Session` is configured with - autocommit=True and no transaction has been started. + autocommitting and does not have a transaction-dedicated + :class:`.Connection` available, a temporary :class:`.Connection` is + established for the statement execution, which is closed (meaning, + returned to the connection pool) when the :class:`.ResultProxy` has + consumed all available data. This applies *only* when the + :class:`.Session` is configured with autocommit=True and no + transaction has been started. :param clause: An executable statement (i.e. an :class:`.Executable` expression @@ -919,7 +980,8 @@ class Session(object): def scalar(self, clause, params=None, mapper=None, bind=None, **kw): """Like :meth:`~.Session.execute` but return a scalar result.""" - return self.execute(clause, params=params, mapper=mapper, bind=bind, **kw).scalar() + return self.execute( + clause, params=params, mapper=mapper, bind=bind, **kw).scalar() def close(self): """Close this Session. @@ -936,13 +998,6 @@ class Session(object): for transaction in self.transaction._iterate_parents(): transaction.close() - @classmethod - def close_all(cls): - """Close *all* sessions in memory.""" - - for sess in _sessions.values(): - sess.close() - def expunge_all(self): """Remove all object instances from this ``Session``. @@ -951,14 +1006,15 @@ class Session(object): """ for state in self.identity_map.all_states() + list(self._new): - state.detach() + state._detach() self.identity_map = self._identity_cls() self._new = {} self._deleted = {} # TODO: need much more test coverage for bind_mapper() and similar ! - # TODO: + crystalize + document resolution order vis. bind_mapper/bind_table + # TODO: + crystalize + document resolution order + # vis. bind_mapper/bind_table def bind_mapper(self, mapper, bind): """Bind operations for a mapper to a Connectable. @@ -974,7 +1030,7 @@ class Session(object): """ if isinstance(mapper, type): - mapper = _class_mapper(mapper) + mapper = class_mapper(mapper) self.__binds[mapper.base_mapper] = bind for t in mapper._all_tables: @@ -1027,7 +1083,7 @@ class Session(object): linked to the :class:`.MetaData` ultimately associated with the :class:`.Table` or other selectable to which the mapper is mapped. - 6. No bind can be found, :class:`.UnboundExecutionError` + 6. No bind can be found, :exc:`~sqlalchemy.exc.UnboundExecutionError` is raised. :param mapper: @@ -1041,10 +1097,10 @@ class Session(object): :param clause: A :class:`.ClauseElement` (i.e. :func:`~.sql.expression.select`, :func:`~.sql.expression.text`, - etc.). If the ``mapper`` argument is not present or could not produce - a bind, the given expression construct will be searched for a bound - element, typically a :class:`.Table` associated with bound - :class:`.MetaData`. + etc.). If the ``mapper`` argument is not present or could not + produce a bind, the given expression construct will be searched + for a bound element, typically a :class:`.Table` associated with + bound :class:`.MetaData`. """ if mapper is clause is None: @@ -1124,11 +1180,18 @@ class Session(object): def _autoflush(self): if self.autoflush and not self._flushing: - self.flush() - - def _finalize_loaded(self, states): - for state, dict_ in states.items(): - state.commit_all(dict_, self.identity_map) + try: + self.flush() + except sa_exc.StatementError as e: + # note we are reraising StatementError as opposed to + # raising FlushError with "chaining" to remain compatible + # with code that catches StatementError, IntegrityError, + # etc. + e.add_detail( + "raised as a result of Query-invoked autoflush; " + "consider using a session.no_autoflush block if this " + "flush is occuring prematurely") + util.raise_from_cause(e) def refresh(self, instance, attribute_names=None, lockmode=None): """Expire and refresh the attributes on the given instance. @@ -1165,13 +1228,14 @@ class Session(object): self._expire_state(state, attribute_names) - if self.query(_object_mapper(instance))._load_on_ident( + if loading.load_on_ident( + self.query(object_mapper(instance)), state.key, refresh_state=state, lockmode=lockmode, only_load_props=attribute_names) is None: raise sa_exc.InvalidRequestError( "Could not refresh instance '%s'" % - mapperutil.instance_str(instance)) + instance_str(instance)) def expire_all(self): """Expires all persistent instances within this Session. @@ -1196,7 +1260,7 @@ class Session(object): """ for state in self.identity_map.all_states(): - state.expire(state.dict, self.identity_map._modified) + state._expire(state.dict, self.identity_map._modified) def expire(self, instance, attribute_names=None): """Expire the attributes on an instance. @@ -1234,7 +1298,7 @@ class Session(object): def _expire_state(self, state, attribute_names): self._validate_persistent(state) if attribute_names: - state.expire_attributes(state.dict, attribute_names) + state._expire_attributes(state.dict, attribute_names) else: # pre-fetch the full cascade since the expire is going to # remove associations @@ -1248,10 +1312,10 @@ class Session(object): """Expire a state if persistent, else expunge if pending""" if state.key: - state.expire(state.dict, self.identity_map._modified) + state._expire(state.dict, self.identity_map._modified) elif state in self._new: self._new.pop(state) - state.detach() + state._detach() @util.deprecated("0.7", "The non-weak-referencing identity map " "feature is no longer needed.") @@ -1282,7 +1346,7 @@ class Session(object): if state.session_id is not self.hash_key: raise sa_exc.InvalidRequestError( "Instance %s is not present in this Session" % - mapperutil.state_str(state)) + state_str(state)) cascaded = list(state.manager.mapper.cascade_iterator( 'expunge', state)) @@ -1293,68 +1357,83 @@ class Session(object): def _expunge_state(self, state): if state in self._new: self._new.pop(state) - state.detach() + state._detach() elif self.identity_map.contains_state(state): self.identity_map.discard(state) self._deleted.pop(state, None) - state.detach() + state._detach() elif self.transaction: self.transaction._deleted.pop(state, None) - def _register_newly_persistent(self, state): - mapper = _state_mapper(state) + def _register_newly_persistent(self, states): + for state in states: + mapper = _state_mapper(state) - # prevent against last minute dereferences of the object - obj = state.obj() - if obj is not None: + # prevent against last minute dereferences of the object + obj = state.obj() + if obj is not None: - instance_key = mapper._identity_key_from_state(state) + instance_key = mapper._identity_key_from_state(state) - if _none_set.issubset(instance_key[1]) and \ - not mapper.allow_partial_pks or \ - _none_set.issuperset(instance_key[1]): - raise exc.FlushError( - "Instance %s has a NULL identity key. If this is an " - "auto-generated value, check that the database table " - "allows generation of new primary key values, and that " - "the mapped Column object is configured to expect these " - "generated values. Ensure also that this flush() is " - "not occurring at an inappropriate time, such as within " - "a load() event." % mapperutil.state_str(state) - ) + if _none_set.issubset(instance_key[1]) and \ + not mapper.allow_partial_pks or \ + _none_set.issuperset(instance_key[1]): + raise exc.FlushError( + "Instance %s has a NULL identity key. If this is an " + "auto-generated value, check that the database table " + "allows generation of new primary key values, and " + "that the mapped Column object is configured to " + "expect these generated values. Ensure also that " + "this flush() is not occurring at an inappropriate " + "time, such aswithin a load() event." + % state_str(state) + ) - if state.key is None: - state.key = instance_key - elif state.key != instance_key: - # primary key switch. use discard() in case another - # state has already replaced this one in the identity - # map (see test/orm/test_naturalpks.py ReversePKsTest) - self.identity_map.discard(state) - if state in self.transaction._key_switches: - orig_key = self.transaction._key_switches[state][0] - else: - orig_key = state.key - self.transaction._key_switches[state] = (orig_key, instance_key) - state.key = instance_key + if state.key is None: + state.key = instance_key + elif state.key != instance_key: + # primary key switch. use discard() in case another + # state has already replaced this one in the identity + # map (see test/orm/test_naturalpks.py ReversePKsTest) + self.identity_map.discard(state) + if state in self.transaction._key_switches: + orig_key = self.transaction._key_switches[state][0] + else: + orig_key = state.key + self.transaction._key_switches[state] = ( + orig_key, instance_key) + state.key = instance_key - self.identity_map.replace(state) - state.commit_all(state.dict, self.identity_map) + self.identity_map.replace(state) + statelib.InstanceState._commit_all_states( + ((state, state.dict) for state in states), + self.identity_map + ) + + self._register_altered(states) # remove from new last, might be the last strong ref - if state in self._new: - if self._enable_transaction_accounting and self.transaction: - self.transaction._new[state] = True + for state in set(states).intersection(self._new): self._new.pop(state) - def _remove_newly_deleted(self, state): + def _register_altered(self, states): if self._enable_transaction_accounting and self.transaction: - self.transaction._deleted[state] = True + for state in states: + if state in self._new: + self.transaction._new[state] = True + else: + self.transaction._dirty[state] = True - self.identity_map.discard(state) - self._deleted.pop(state, None) - state.deleted = True + def _remove_newly_deleted(self, states): + for state in states: + if self._enable_transaction_accounting and self.transaction: + self.transaction._deleted[state] = True - def add(self, instance): + self.identity_map.discard(state) + self._deleted.pop(state, None) + state.deleted = True + + def add(self, instance, _warn=True): """Place an object in the ``Session``. Its state will be persisted to the database on the next flush @@ -1364,6 +1443,9 @@ class Session(object): is ``expunge()``. """ + if _warn and self._warn_on_events: + self._flush_warning("Session.add()") + try: state = attributes.instance_state(instance) except exc.NO_STATE: @@ -1374,8 +1456,11 @@ class Session(object): def add_all(self, instances): """Add the given collection of instances to this ``Session``.""" + if self._warn_on_events: + self._flush_warning("Session.add_all()") + for instance in instances: - self.add(instance) + self.add(instance, _warn=False) def _save_or_update_state(self, state): self._save_or_update_impl(state) @@ -1393,6 +1478,9 @@ class Session(object): The database delete operation occurs upon ``flush()``. """ + if self._warn_on_events: + self._flush_warning("Session.delete()") + try: state = attributes.instance_state(instance) except exc.NO_STATE: @@ -1401,7 +1489,7 @@ class Session(object): if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % - mapperutil.state_str(state)) + state_str(state)) if state in self._deleted: return @@ -1409,7 +1497,7 @@ class Session(object): # ensure object is attached to allow the # cascade operation to load deferred attributes # and collections - self._attach(state) + self._attach(state, include_before=True) # grab the cascades before adding the item to the deleted list # so that autoflush does not delete the item @@ -1423,7 +1511,7 @@ class Session(object): for o, m, st_, dct_ in cascade_states: self._delete_impl(st_) - def merge(self, instance, load=True, **kw): + def merge(self, instance, load=True): """Copy the state of a given instance into a corresponding instance within this :class:`.Session`. @@ -1431,8 +1519,8 @@ class Session(object): source instance, and attempts to reconcile it with an instance of the same primary key in the session. If not found locally, it attempts to load the object from the database based on primary key, and if - none can be located, creates a new instance. The state of each attribute - on the source instance is then copied to the target instance. + none can be located, creates a new instance. The state of each + attribute on the source instance is then copied to the target instance. The resulting target instance is then returned by the method; the original source instance is left unmodified, and un-associated with the :class:`.Session` if not already. @@ -1466,10 +1554,9 @@ class Session(object): should be "clean" as well, else this suggests a mis-use of the method. """ - if 'dont_load' in kw: - load = not kw['dont_load'] - util.warn_deprecated('dont_load=True has been renamed to ' - 'load=False.') + + if self._warn_on_events: + self._flush_warning("Session.merge()") _recursive = {} @@ -1477,7 +1564,7 @@ class Session(object): # flush current contents if we expect to load data self._autoflush() - _object_mapper(instance) # verify mapped + object_mapper(instance) # verify mapped autoflush = self.autoflush try: self.autoflush = False @@ -1566,7 +1653,7 @@ class Session(object): "merging to update the most recent version." % ( existing_version, - mapperutil.state_str(merged_state), + state_str(merged_state), merged_version )) @@ -1580,38 +1667,29 @@ class Session(object): if not load: # remove any history - merged_state.commit_all(merged_dict, self.identity_map) + merged_state._commit_all(merged_dict, self.identity_map) if new_instance: merged_state.manager.dispatch.load(merged_state, None) return merged - @classmethod - def identity_key(cls, *args, **kwargs): - return mapperutil.identity_key(*args, **kwargs) - - @classmethod - def object_session(cls, instance): - """Return the ``Session`` to which an object belongs.""" - - return object_session(instance) - def _validate_persistent(self, state): if not self.identity_map.contains_state(state): raise sa_exc.InvalidRequestError( "Instance '%s' is not persistent within this Session" % - mapperutil.state_str(state)) + state_str(state)) def _save_impl(self, state): if state.key is not None: raise sa_exc.InvalidRequestError( "Object '%s' already has an identity - it can't be registered " - "as pending" % mapperutil.state_str(state)) + "as pending" % state_str(state)) - self._attach(state) + self._before_attach(state) if state not in self._new: self._new[state] = state.obj() state.insert_order = len(self._new) + self._attach(state) def _update_impl(self, state, discard_existing=False): if (self.identity_map.contains_state(state) and @@ -1621,21 +1699,21 @@ class Session(object): if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % - mapperutil.state_str(state)) + state_str(state)) if state.deleted: raise sa_exc.InvalidRequestError( "Instance '%s' has been deleted. Use the make_transient() " "function to send this object back to the transient state." % - mapperutil.state_str(state) + state_str(state) ) - if discard_existing: - existing = self.identity_map.get(state.key) - if existing is not None: - self.identity_map.discard(attributes.instance_state(existing)) - self._attach(state) + self._before_attach(state) self._deleted.pop(state, None) - self.identity_map.add(state) + if discard_existing: + self.identity_map.replace(state) + else: + self.identity_map.add(state) + self._attach(state) def _save_or_update_impl(self, state): if state.key is None: @@ -1650,29 +1728,94 @@ class Session(object): if state.key is None: return - self._attach(state) + self._attach(state, include_before=True) self._deleted[state] = state.obj() self.identity_map.add(state) - def _attach(self, state): + def enable_relationship_loading(self, obj): + """Associate an object with this :class:`.Session` for related + object loading. + + .. warning:: + + :meth:`.enable_relationship_loading` exists to serve special + use cases and is not recommended for general use. + + Accesses of attributes mapped with :func:`.relationship` + will attempt to load a value from the database using this + :class:`.Session` as the source of connectivity. The values + will be loaded based on foreign key values present on this + object - it follows that this functionality + generally only works for many-to-one-relationships. + + The object will be attached to this session, but will + **not** participate in any persistence operations; its state + for almost all purposes will remain either "transient" or + "detached", except for the case of relationship loading. + + Also note that backrefs will often not work as expected. + Altering a relationship-bound attribute on the target object + may not fire off a backref event, if the effective value + is what was already loaded from a foreign-key-holding value. + + The :meth:`.Session.enable_relationship_loading` method is + similar to the ``load_on_pending`` flag on :func:`.relationship`. Unlike + that flag, :meth:`.Session.enable_relationship_loading` allows + an object to remain transient while still being able to load + related items. + + To make a transient object associated with a :class:`.Session` + via :meth:`.Session.enable_relationship_loading` pending, add + it to the :class:`.Session` using :meth:`.Session.add` normally. + + :meth:`.Session.enable_relationship_loading` does not improve + behavior when the ORM is used normally - object references should be + constructed at the object level, not at the foreign key level, so + that they are present in an ordinary way before flush() + proceeds. This method is not intended for general use. + + .. versionadded:: 0.8 + + .. seealso:: + + ``load_on_pending`` at :func:`.relationship` - this flag + allows per-relationship loading of many-to-ones on items that + are pending. + + """ + state = attributes.instance_state(obj) + self._attach(state, include_before=True) + state._load_pending = True + + def _before_attach(self, state): + if state.session_id != self.hash_key and \ + self.dispatch.before_attach: + self.dispatch.before_attach(self, state.obj()) + + def _attach(self, state, include_before=False): if state.key and \ state.key in self.identity_map and \ - not self.identity_map.contains_state(state): + not self.identity_map.contains_state(state): raise sa_exc.InvalidRequestError("Can't attach instance " "%s; another instance with key %s is already " "present in this session." - % (mapperutil.state_str(state), state.key)) + % (state_str(state), state.key)) if state.session_id and \ state.session_id is not self.hash_key and \ state.session_id in _sessions: raise sa_exc.InvalidRequestError( "Object '%s' is already attached to session '%s' " - "(this is '%s')" % (mapperutil.state_str(state), + "(this is '%s')" % (state_str(state), state.session_id, self.hash_key)) if state.session_id != self.hash_key: + if include_before and \ + self.dispatch.before_attach: + self.dispatch.before_attach(self, state.obj()) state.session_id = self.hash_key + if state.modified and state._strong_obj is None: + state._strong_obj = state.obj() if self.dispatch.after_attach: self.dispatch.after_attach(self, state.obj()) @@ -1690,9 +1833,11 @@ class Session(object): return self._contains_state(state) def __iter__(self): - """Iterate over all pending or persistent instances within this Session.""" + """Iterate over all pending or persistent instances within this + Session. - return iter(list(self._new.values()) + self.identity_map.values()) + """ + return iter(list(self._new.values()) + list(self.identity_map.values())) def _contains_state(self, state): return state in self._new or self.identity_map.contains_state(state) @@ -1735,6 +1880,14 @@ class Session(object): finally: self._flushing = False + def _flush_warning(self, method): + util.warn( + "Usage of the '%s' operation is not currently supported " + "within the execution stage of the flush process. " + "Results may not be consistent. Consider using alternative " + "event listeners or connection-level operations instead." + % method) + def _is_clean(self): return not self.identity_map.check_modified() and \ not self._deleted and \ @@ -1784,7 +1937,8 @@ class Session(object): proc = new.union(dirty).difference(deleted) for state in proc: - is_orphan = _state_mapper(state)._is_orphan(state) and state.has_identity + is_orphan = ( + _state_mapper(state)._is_orphan(state) and state.has_identity) flush_context.register_object(state, isdelete=is_orphan) processed.add(state) @@ -1802,12 +1956,31 @@ class Session(object): flush_context.transaction = transaction = self.begin( subtransactions=True) try: - flush_context.execute() + self._warn_on_events = True + try: + flush_context.execute() + finally: + self._warn_on_events = False self.dispatch.after_flush(self, flush_context) flush_context.finalize_flush_changes() + if not objects and self.identity_map._modified: + len_ = len(self.identity_map._modified) + + statelib.InstanceState._commit_all_states( + [(state, state.dict) for state in + self.identity_map._modified], + instance_dict=self.identity_map) + util.warn("Attribute history events accumulated on %d " + "previously clean instances " + "within inner-flush event handlers have been reset, " + "and will not result in database updates. " + "Consider using set_committed_value() within " + "inner-flush event handlers to avoid this warning." + % len_) + # useful assertions: #if not objects: # assert not self.identity_map._modified @@ -1820,12 +1993,11 @@ class Session(object): transaction.commit() except: - transaction.rollback(_capture_exception=True) - raise - + with util.safe_reraise(): + transaction.rollback(_capture_exception=True) def is_modified(self, instance, include_collections=True, - passive=attributes.PASSIVE_OFF): + passive=True): """Return ``True`` if the given instance has locally modified attributes. @@ -1840,18 +2012,16 @@ class Session(object): E.g.:: - return session.is_modified(someobject, passive=True) + return session.is_modified(someobject) .. versionchanged:: 0.8 - In SQLAlchemy 0.7 and earlier, the ``passive`` - flag should **always** be explicitly set to ``True``. - The current default value of :data:`.attributes.PASSIVE_OFF` - for this flag is incorrect, in that it loads unloaded - collections and attributes which by definition - have no modified state, and furthermore trips off - autoflush which then causes all subsequent, possibly - modified attributes to lose their modified state. - The default value of the flag will be changed in 0.8. + When using SQLAlchemy 0.7 and earlier, the ``passive`` + flag should **always** be explicitly set to ``True``, + else SQL loads/autoflushes may proceed which can affect + the modified state itself: + ``session.is_modified(someobject, passive=True)``\ . + In 0.8 and above, the behavior is corrected and + this flag is ignored. A few caveats to this method apply: @@ -1872,47 +2042,32 @@ class Session(object): usually needed, and in those few cases where it isn't, is less expensive on average than issuing a defensive SELECT. - The "old" value is fetched unconditionally only if the attribute - container has the ``active_history`` flag set to ``True``. This flag - is set typically for primary key attributes and scalar object references - that are not a simple many-to-one. To set this flag for - any arbitrary mapped column, use the ``active_history`` argument - with :func:`.column_property`. + The "old" value is fetched unconditionally upon set only if the + attribute container has the ``active_history`` flag set to ``True``. + This flag is set typically for primary key attributes and scalar + object references that are not a simple many-to-one. To set this + flag for any arbitrary mapped column, use the ``active_history`` + argument with :func:`.column_property`. :param instance: mapped instance to be tested for pending changes. - :param include_collections: Indicates if multivalued collections should be - included in the operation. Setting this to ``False`` is a way to detect - only local-column based properties (i.e. scalar columns or many-to-one - foreign keys) that would result in an UPDATE for this instance upon - flush. - :param passive: Indicates if unloaded attributes and - collections should be loaded in the course of performing - this test. If set to ``False``, or left at its default - value of :data:`.PASSIVE_OFF`, unloaded attributes - will be loaded. If set to ``True`` or - :data:`.PASSIVE_NO_INITIALIZE`, unloaded - collections and attributes will remain unloaded. As - noted previously, the existence of this flag here - is a bug, as unloaded attributes by definition have - no changes, and the load operation also triggers an - autoflush which then cancels out subsequent changes. - This flag should **always be set to True**. - + :param include_collections: Indicates if multivalued collections + should be included in the operation. Setting this to ``False`` is a + way to detect only local-column based properties (i.e. scalar columns + or many-to-one foreign keys) that would result in an UPDATE for this + instance upon flush. + :param passive: .. versionchanged:: 0.8 - The flag will be deprecated and the default - set to ``True``. + Ignored for backwards compatibility. + When using SQLAlchemy 0.7 and earlier, this flag should always + be set to ``True``. """ - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - dict_ = state.dict + state = object_state(instance) - if passive is True: - passive = attributes.PASSIVE_NO_INITIALIZE - elif passive is False: - passive = attributes.PASSIVE_OFF + if not state.modified: + return False + + dict_ = state.dict for attr in state.manager.attributes: if \ @@ -1923,11 +2078,13 @@ class Session(object): continue (added, unchanged, deleted) = \ - attr.impl.get_history(state, dict_, passive=passive) + attr.impl.get_history(state, dict_, + passive=attributes.NO_CHANGE) if added or deleted: return True - return False + else: + return False @property def is_active(self): @@ -1953,34 +2110,36 @@ class Session(object): target :class:`.Connection` to a user-defined event listener. The "partial rollback" state refers to when an "inner" transaction, - typically used during a flush, encounters an error and emits - a rollback of the DBAPI connection. At this point, the :class:`.Session` - is in "partial rollback" and awaits for the user to call :meth:`.rollback`, - in order to close out the transaction stack. It is in this "partial - rollback" period that the :attr:`.is_active` flag returns False. After - the call to :meth:`.rollback`, the :class:`.SessionTransaction` is replaced + typically used during a flush, encounters an error and emits a + rollback of the DBAPI connection. At this point, the + :class:`.Session` is in "partial rollback" and awaits for the user to + call :meth:`.Session.rollback`, in order to close out the + transaction stack. It is in this "partial rollback" period that the + :attr:`.is_active` flag returns False. After the call to + :meth:`.Session.rollback`, the :class:`.SessionTransaction` is replaced with a new one and :attr:`.is_active` returns ``True`` again. When a :class:`.Session` is used in ``autocommit=True`` mode, the :class:`.SessionTransaction` is only instantiated within the scope of a flush call, or when :meth:`.Session.begin` is called. So :attr:`.is_active` will always be ``False`` outside of a flush or - :meth:`.begin` block in this mode, and will be ``True`` within the - :meth:`.begin` block as long as it doesn't enter "partial rollback" - state. + :meth:`.Session.begin` block in this mode, and will be ``True`` + within the :meth:`.Session.begin` block as long as it doesn't enter + "partial rollback" state. From all the above, it follows that the only purpose to this flag is for application frameworks that wish to detect is a "rollback" is - necessary within a generic error handling routine, for :class:`.Session` - objects that would otherwise be in "partial rollback" mode. In - a typical integration case, this is also not necessary as it is standard - practice to emit :meth:`.Session.rollback` unconditionally within the - outermost exception catch. + necessary within a generic error handling routine, for + :class:`.Session` objects that would otherwise be in + "partial rollback" mode. In a typical integration case, this is also + not necessary as it is standard practice to emit + :meth:`.Session.rollback` unconditionally within the outermost + exception catch. To track the transactional state of a :class:`.Session` fully, use event listeners, primarily the :meth:`.SessionEvents.after_begin`, - :meth:`.SessionEvents.after_commit`, :meth:`.SessionEvents.after_rollback` - and related events. + :meth:`.SessionEvents.after_commit`, + :meth:`.SessionEvents.after_rollback` and related events. """ return self.transaction and self.transaction.is_active @@ -1992,9 +2151,10 @@ class Session(object): access to the full set of persistent objects (i.e., those that have row identity) currently in the session. - See also: + .. seealso:: - :func:`.identity_key` - operations involving identity keys. + :func:`.identity_key` - helper function to produce the keys used + in this dictionary. """ @@ -2041,15 +2201,142 @@ class Session(object): def deleted(self): "The set of all instances marked as 'deleted' within this ``Session``" - return util.IdentitySet(self._deleted.values()) + return util.IdentitySet(list(self._deleted.values())) @property def new(self): "The set of all instances marked as 'new' within this ``Session``." - return util.IdentitySet(self._new.values()) + return util.IdentitySet(list(self._new.values())) + + +class sessionmaker(_SessionClassMethods): + """A configurable :class:`.Session` factory. + + The :class:`.sessionmaker` factory generates new + :class:`.Session` objects when called, creating them given + the configurational arguments established here. + + e.g.:: + + # global scope + Session = sessionmaker(autoflush=False) + + # later, in a local scope, create and use a session: + sess = Session() + + Any keyword arguments sent to the constructor itself will override the + "configured" keywords:: + + Session = sessionmaker() + + # bind an individual session to a connection + sess = Session(bind=connection) + + The class also includes a method :meth:`.configure`, which can + be used to specify additional keyword arguments to the factory, which + will take effect for subsequent :class:`.Session` objects generated. + This is usually used to associate one or more :class:`.Engine` objects + with an existing :class:`.sessionmaker` factory before it is first + used:: + + # application starts + Session = sessionmaker() + + # ... later + engine = create_engine('sqlite:///foo.db') + Session.configure(bind=engine) + + sess = Session() + + .. seealso: + + :ref:`session_getting` - introductory text on creating + sessions using :class:`.sessionmaker`. + + """ + + def __init__(self, bind=None, class_=Session, autoflush=True, + autocommit=False, + expire_on_commit=True, + info=None, **kw): + """Construct a new :class:`.sessionmaker`. + + All arguments here except for ``class_`` correspond to arguments + accepted by :class:`.Session` directly. See the + :meth:`.Session.__init__` docstring for more details on parameters. + + :param bind: a :class:`.Engine` or other :class:`.Connectable` with + which newly created :class:`.Session` objects will be associated. + :param class_: class to use in order to create new :class:`.Session` + objects. Defaults to :class:`.Session`. + :param autoflush: The autoflush setting to use with newly created + :class:`.Session` objects. + :param autocommit: The autocommit setting to use with newly created + :class:`.Session` objects. + :param expire_on_commit=True: the expire_on_commit setting to use + with newly created :class:`.Session` objects. + :param info: optional dictionary of information that will be available + via :attr:`.Session.info`. Note this dictionary is *updated*, not + replaced, when the ``info`` parameter is specified to the specific + :class:`.Session` construction operation. + + .. versionadded:: 0.9.0 + + :param \**kw: all other keyword arguments are passed to the constructor + of newly created :class:`.Session` objects. + + """ + kw['bind'] = bind + kw['autoflush'] = autoflush + kw['autocommit'] = autocommit + kw['expire_on_commit'] = expire_on_commit + if info is not None: + kw['info'] = info + self.kw = kw + # make our own subclass of the given class, so that + # events can be associated with it specifically. + self.class_ = type(class_.__name__, (class_,), {}) + + def __call__(self, **local_kw): + """Produce a new :class:`.Session` object using the configuration + established in this :class:`.sessionmaker`. + + In Python, the ``__call__`` method is invoked on an object when + it is "called" in the same way as a function:: + + Session = sessionmaker() + session = Session() # invokes sessionmaker.__call__() + + """ + for k, v in self.kw.items(): + if k == 'info' and 'info' in local_kw: + d = v.copy() + d.update(local_kw['info']) + local_kw['info'] = d + else: + local_kw.setdefault(k, v) + return self.class_(**local_kw) + + def configure(self, **new_kw): + """(Re)configure the arguments for this sessionmaker. + + e.g.:: + + Session = sessionmaker() + + Session.configure(bind=create_engine('sqlite://')) + """ + self.kw.update(new_kw) + + def __repr__(self): + return "%s(class_=%r,%s)" % ( + self.__class__.__name__, + self.class_.__name__, + ", ".join("%s=%r" % (k, v) for k, v in self.kw.items()) + ) + -_sessions = weakref.WeakValueDictionary() def make_transient(instance): """Make the given instance 'transient'. @@ -2079,6 +2366,7 @@ def make_transient(instance): if state.deleted: del state.deleted + def object_session(instance): """Return the ``Session`` to which instance belongs. @@ -2092,12 +2380,4 @@ def object_session(instance): raise exc.UnmappedInstanceError(instance) -def _state_session(state): - if state.session_id: - try: - return _sessions[state.session_id] - except KeyError: - pass - return None - _new_sessionid = util.counter() diff --git a/libs/sqlalchemy/orm/shard.py b/libs/sqlalchemy/orm/shard.py deleted file mode 100644 index 93bc7a6b..00000000 --- a/libs/sqlalchemy/orm/shard.py +++ /dev/null @@ -1,15 +0,0 @@ -# orm/shard.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy import util - -util.warn_deprecated( - "Horizontal sharding is now importable via " - "'import sqlalchemy.ext.horizontal_shard" -) - -from sqlalchemy.ext.horizontal_shard import * - diff --git a/libs/sqlalchemy/orm/state.py b/libs/sqlalchemy/orm/state.py index b9a9c463..9712dd05 100644 --- a/libs/sqlalchemy/orm/state.py +++ b/libs/sqlalchemy/orm/state.py @@ -1,5 +1,5 @@ # orm/state.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -11,33 +11,30 @@ defines a large part of the ORM's interactivity. """ -from sqlalchemy.util import EMPTY_SET import weakref -from sqlalchemy import util +from .. import util +from . import exc as orm_exc, interfaces +from .path_registry import PathRegistry +from .base import PASSIVE_NO_RESULT, SQL_OK, NEVER_SET, ATTR_WAS_SET, \ + NO_VALUE, PASSIVE_NO_INITIALIZE, INIT_OK, PASSIVE_OFF +from . import base -from sqlalchemy.orm import exc as orm_exc, attributes, interfaces,\ - util as orm_util -from sqlalchemy.orm.attributes import PASSIVE_OFF, PASSIVE_NO_RESULT, \ - PASSIVE_NO_FETCH, NEVER_SET, ATTR_WAS_SET, NO_VALUE - -mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") - -import sys - -class InstanceState(object): +class InstanceState(interfaces._InspectionAttr): """tracks state information at the instance level.""" session_id = None key = None runid = None - load_options = EMPTY_SET + load_options = util.EMPTY_SET load_path = () insert_order = None - mutable_dict = None _strong_obj = None modified = False expired = False deleted = False + _load_pending = False + + is_instance = True def __init__(self, obj, manager): self.class_ = obj.__class__ @@ -46,23 +43,127 @@ class InstanceState(object): self.callables = {} self.committed_state = {} + @util.memoized_property + def attrs(self): + """Return a namespace representing each attribute on + the mapped object, including its current value + and history. + + The returned object is an instance of :class:`.AttributeState`. + + """ + return util.ImmutableProperties( + dict( + (key, AttributeState(self, key)) + for key in self.manager + ) + ) + + @property + def transient(self): + """Return true if the object is transient.""" + return self.key is None and \ + not self._attached + + @property + def pending(self): + """Return true if the object is pending.""" + return self.key is None and \ + self._attached + + @property + def persistent(self): + """Return true if the object is persistent.""" + return self.key is not None and \ + self._attached + + @property + def detached(self): + """Return true if the object is detached.""" + return self.key is not None and \ + not self._attached + + @property + @util.dependencies("sqlalchemy.orm.session") + def _attached(self, sessionlib): + return self.session_id is not None and \ + self.session_id in sessionlib._sessions + + @property + @util.dependencies("sqlalchemy.orm.session") + def session(self, sessionlib): + """Return the owning :class:`.Session` for this instance, + or ``None`` if none available.""" + return sessionlib._state_session(self) + + @property + def object(self): + """Return the mapped object represented by this + :class:`.InstanceState`.""" + return self.obj() + + @property + def identity(self): + """Return the mapped identity of the mapped object. + This is the primary key identity as persisted by the ORM + which can always be passed directly to + :meth:`.Query.get`. + + Returns ``None`` if the object has no primary key identity. + + .. note:: + An object which is transient or pending + does **not** have a mapped identity until it is flushed, + even if its attributes include primary key values. + + """ + if self.key is None: + return None + else: + return self.key[1] + + @property + def identity_key(self): + """Return the identity key for the mapped object. + + This is the key used to locate the object within + the :attr:`.Session.identity_map` mapping. It contains + the identity as returned by :attr:`.identity` within it. + + + """ + # TODO: just change .key to .identity_key across + # the board ? probably + return self.key + @util.memoized_property def parents(self): return {} @util.memoized_property - def pending(self): + def _pending_mutations(self): return {} + @util.memoized_property + def mapper(self): + """Return the :class:`.Mapper` used for this mapepd object.""" + return self.manager.mapper + @property def has_identity(self): + """Return ``True`` if this object has an identity key. + + This should always have the same value as the + expression ``state.persistent or state.detached``. + + """ return bool(self.key) - def detach(self): - self.session_id = None + def _detach(self): + self.session_id = self._strong_obj = None - def dispose(self): - self.detach() + def _dispose(self): + self._detach() del self.obj def _cleanup(self, ref): @@ -71,7 +172,7 @@ class InstanceState(object): instance_dict.discard(self) self.callables = {} - self.session_id = None + self.session_id = self._strong_obj = None del self.obj def obj(self): @@ -81,19 +182,16 @@ class InstanceState(object): def dict(self): o = self.obj() if o is not None: - return attributes.instance_dict(o) + return base.instance_dict(o) else: return {} - def initialize_instance(*mixed, **kwargs): + def _initialize_instance(*mixed, **kwargs): self, instance, args = mixed[0], mixed[1], mixed[2:] manager = self.manager manager.dispatch.init(self, args, kwargs) - #if manager.mutable_attributes: - # assert self.__class__ is MutableAttrInstanceState - try: return manager.original_init(*mixed[1:], **kwargs) except: @@ -106,49 +204,29 @@ class InstanceState(object): def get_impl(self, key): return self.manager[key].impl - def get_pending(self, key): - if key not in self.pending: - self.pending[key] = PendingCollection() - return self.pending[key] - - def value_as_iterable(self, dict_, key, passive=PASSIVE_OFF): - """Return a list of tuples (state, obj) for the given - key. - - returns an empty list if the value is None/empty/PASSIVE_NO_RESULT - """ - - impl = self.manager[key].impl - x = impl.get(self, dict_, passive=passive) - if x is PASSIVE_NO_RESULT or x is None: - return [] - elif hasattr(impl, 'get_collection'): - return [ - (attributes.instance_state(o), o) for o in - impl.get_collection(self, dict_, x, passive=passive) - ] - else: - return [(attributes.instance_state(x), x)] + def _get_pending_mutation(self, key): + if key not in self._pending_mutations: + self._pending_mutations[key] = PendingCollection() + return self._pending_mutations[key] def __getstate__(self): - d = {'instance':self.obj()} - d.update( + state_dict = {'instance': self.obj()} + state_dict.update( (k, self.__dict__[k]) for k in ( - 'committed_state', 'pending', 'modified', 'expired', - 'callables', 'key', 'parents', 'load_options', 'mutable_dict', + 'committed_state', '_pending_mutations', 'modified', 'expired', + 'callables', 'key', 'parents', 'load_options', 'class_', ) if k in self.__dict__ ) if self.load_path: - d['load_path'] = interfaces.serialize_path(self.load_path) + state_dict['load_path'] = self.load_path.serialize() - self.manager.dispatch.pickle(self, d) + state_dict['manager'] = self.manager._serialize(self, state_dict) - return d + return state_dict - def __setstate__(self, state): - from sqlalchemy.orm import instrumentation - inst = state['instance'] + def __setstate__(self, state_dict): + inst = state_dict['instance'] if inst is not None: self.obj = weakref.ref(inst, self._cleanup) self.class_ = inst.__class__ @@ -157,60 +235,49 @@ class InstanceState(object): # due to storage of state in "parents". "class_" # also new. self.obj = None - self.class_ = state['class_'] - self.manager = manager = instrumentation.manager_of_class(self.class_) - if manager is None: - raise orm_exc.UnmappedInstanceError( - inst, - "Cannot deserialize object of type %r - no mapper() has" - " been configured for this class within the current Python process!" % - self.class_) - elif manager.is_mapped and not manager.mapper.configured: - mapperlib.configure_mappers() + self.class_ = state_dict['class_'] - self.committed_state = state.get('committed_state', {}) - self.pending = state.get('pending', {}) - self.parents = state.get('parents', {}) - self.modified = state.get('modified', False) - self.expired = state.get('expired', False) - self.callables = state.get('callables', {}) - - if self.modified: - self._strong_obj = inst + self.committed_state = state_dict.get('committed_state', {}) + self._pending_mutations = state_dict.get('_pending_mutations', {}) + self.parents = state_dict.get('parents', {}) + self.modified = state_dict.get('modified', False) + self.expired = state_dict.get('expired', False) + self.callables = state_dict.get('callables', {}) self.__dict__.update([ - (k, state[k]) for k in ( - 'key', 'load_options', 'mutable_dict' - ) if k in state + (k, state_dict[k]) for k in ( + 'key', 'load_options', + ) if k in state_dict ]) - if 'load_path' in state: - self.load_path = interfaces.deserialize_path(state['load_path']) + if 'load_path' in state_dict: + self.load_path = PathRegistry.\ + deserialize(state_dict['load_path']) - # setup _sa_instance_state ahead of time so that - # unpickle events can access the object normally. - # see [ticket:2362] - manager.setup_instance(inst, self) - manager.dispatch.unpickle(self, state) + state_dict['manager'](self, inst, state_dict) - def initialize(self, key): + def _initialize(self, key): """Set this attribute to an empty value or collection, based on the AttributeImpl in use.""" self.manager.get_impl(key).initialize(self, self.dict) - def reset(self, dict_, key): + def _reset(self, dict_, key): """Remove the given attribute and any callables associated with it.""" - dict_.pop(key, None) + old = dict_.pop(key, None) + if old is not None and self.manager[key].impl.collection: + self.manager[key].impl._invalidate_collection(old) self.callables.pop(key, None) - def expire_attribute_pre_commit(self, dict_, key): + def _expire_attribute_pre_commit(self, dict_, key): """a fast expire that can be called by column loaders during a load. The additional bookkeeping is finished up in commit_all(). + Should only be called for scalar attributes. + This method is actually called a lot with joined-table loading, when the second table isn't present in the result. @@ -218,65 +285,73 @@ class InstanceState(object): dict_.pop(key, None) self.callables[key] = self - def set_callable(self, dict_, key, callable_): - """Remove the given attribute and set the given callable - as a loader.""" + @classmethod + def _row_processor(cls, manager, fn, key): + impl = manager[key].impl + if impl.collection: + def _set_callable(state, dict_, row): + old = dict_.pop(key, None) + if old is not None: + impl._invalidate_collection(old) + state.callables[key] = fn + else: + def _set_callable(state, dict_, row): + state.callables[key] = fn + return _set_callable - dict_.pop(key, None) - self.callables[key] = callable_ - - def expire(self, dict_, modified_set): + def _expire(self, dict_, modified_set): self.expired = True if self.modified: modified_set.discard(self) self.modified = False + self._strong_obj = None self.committed_state.clear() - self.__dict__.pop('pending', None) - self.__dict__.pop('mutable_dict', None) + InstanceState._pending_mutations._reset(self) # clear out 'parents' collection. not # entirely clear how we can best determine # which to remove, or not. - self.__dict__.pop('parents', None) + InstanceState.parents._reset(self) for key in self.manager: impl = self.manager[key].impl if impl.accepts_scalar_loader and \ - (impl.expire_missing or key in dict_): + (impl.expire_missing or key in dict_): self.callables[key] = self - dict_.pop(key, None) + old = dict_.pop(key, None) + if impl.collection and old is not None: + impl._invalidate_collection(old) self.manager.dispatch.expire(self, None) - def expire_attributes(self, dict_, attribute_names): - pending = self.__dict__.get('pending', None) - mutable_dict = self.mutable_dict + def _expire_attributes(self, dict_, attribute_names): + pending = self.__dict__.get('_pending_mutations', None) for key in attribute_names: impl = self.manager[key].impl if impl.accepts_scalar_loader: self.callables[key] = self - dict_.pop(key, None) + old = dict_.pop(key, None) + if impl.collection and old is not None: + impl._invalidate_collection(old) self.committed_state.pop(key, None) - if mutable_dict: - mutable_dict.pop(key, None) if pending: pending.pop(key, None) self.manager.dispatch.expire(self, attribute_names) - def __call__(self, passive): + def __call__(self, state, passive): """__call__ allows the InstanceState to act as a deferred callable for loading expired attributes, which is also serializable (picklable). """ - if passive is PASSIVE_NO_FETCH: + if not passive & SQL_OK: return PASSIVE_NO_RESULT toload = self.expired_attributes.\ @@ -305,7 +380,6 @@ class InstanceState(object): return set(keys).intersection(self.manager).\ difference(self.committed_state) - @property def unloaded(self): """Return the set of keys which do not have a loaded value. @@ -318,6 +392,13 @@ class InstanceState(object): difference(self.committed_state).\ difference(self.dict) + @property + def _unloaded_non_object(self): + return self.unloaded.intersection( + attr for attr in self.manager + if self.manager[attr].impl.accepts_scalar_loader + ) + @property def expired_attributes(self): """Return the set of keys which are 'expired' to be loaded by @@ -333,10 +414,9 @@ class InstanceState(object): def _instance_dict(self): return None - def _is_really_none(self): - return self.obj() - - def modified_event(self, dict_, attr, previous, collection=False): + def _modified_event(self, dict_, attr, previous, collection=False): + if not attr.send_modified_events: + return if attr.key not in self.committed_state: if collection: if previous is NEVER_SET: @@ -348,29 +428,33 @@ class InstanceState(object): self.committed_state[attr.key] = previous - # the "or not self.modified" is defensive at - # this point. The assertion below is expected - # to be True: # assert self._strong_obj is None or self.modified - if self._strong_obj is None or not self.modified: + if (self.session_id and self._strong_obj is None) \ + or not self.modified: instance_dict = self._instance_dict() if instance_dict: instance_dict._modified.add(self) - self._strong_obj = self.obj() - if self._strong_obj is None: + # only create _strong_obj link if attached + # to a session + + inst = self.obj() + if self.session_id: + self._strong_obj = inst + + if inst is None: raise orm_exc.ObjectDereferencedError( "Can't emit change event for attribute '%s' - " "parent object of type %s has been garbage " "collected." % ( self.manager[attr.key], - orm_util.state_class_str(self) + base.state_class_str(self) )) self.modified = True - def commit(self, dict_, keys): + def _commit(self, dict_, keys): """Commit attributes. This is used by a partial-attribute load operation to mark committed @@ -380,16 +464,8 @@ class InstanceState(object): this step if a value was not populated in state.dict. """ - class_manager = self.manager - if class_manager.mutable_attributes: - for key in keys: - if key in dict_ and key in class_manager.mutable_attributes: - self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) - else: - self.committed_state.pop(key, None) - else: - for key in keys: - self.committed_state.pop(key, None) + for key in keys: + self.committed_state.pop(key, None) self.expired = False @@ -398,7 +474,7 @@ class InstanceState(object): intersection(dict_): del self.callables[key] - def commit_all(self, dict_, instance_dict=None): + def _commit_all(self, dict_, instance_dict=None): """commit all attributes unconditionally. This is used after a flush() or a full load/refresh @@ -409,137 +485,112 @@ class InstanceState(object): - the "modified" flag is set to False - any "expired" markers/callables for attributes loaded are removed. - Attributes marked as "expired" can potentially remain "expired" after this step - if a value was not populated in state.dict. + Attributes marked as "expired" can potentially remain + "expired" after this step if a value was not populated in state.dict. """ + self._commit_all_states([(self, dict_)], instance_dict) - self.committed_state.clear() - self.__dict__.pop('pending', None) + @classmethod + def _commit_all_states(self, iter, instance_dict=None): + """Mass version of commit_all().""" - callables = self.callables - for key in list(callables): - if key in dict_ and callables[key] is self: - del callables[key] + for state, dict_ in iter: + state.committed_state.clear() + InstanceState._pending_mutations._reset(state) - for key in self.manager.mutable_attributes: - if key in dict_: - self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) + callables = state.callables + for key in list(callables): + if key in dict_ and callables[key] is state: + del callables[key] - if instance_dict and self.modified: - instance_dict._modified.discard(self) + if instance_dict and state.modified: + instance_dict._modified.discard(state) - self.modified = self.expired = False - self._strong_obj = None + state.modified = state.expired = False + state._strong_obj = None -class MutableAttrInstanceState(InstanceState): - """InstanceState implementation for objects that reference 'mutable' - attributes. - Has a more involved "cleanup" handler that checks mutable attributes - for changes upon dereference, resurrecting if needed. +class AttributeState(object): + """Provide an inspection interface corresponding + to a particular attribute on a particular mapped object. + + The :class:`.AttributeState` object is accessed + via the :attr:`.InstanceState.attrs` collection + of a particular :class:`.InstanceState`:: + + from sqlalchemy import inspect + + insp = inspect(some_mapped_object) + attr_state = insp.attrs.some_attribute """ - @util.memoized_property - def mutable_dict(self): - return {} - - def _get_modified(self, dict_=None): - if self.__dict__.get('modified', False): - return True - else: - if dict_ is None: - dict_ = self.dict - for key in self.manager.mutable_attributes: - if self.manager[key].impl.check_mutable_modified(self, dict_): - return True - else: - return False - - def _set_modified(self, value): - self.__dict__['modified'] = value - - modified = property(_get_modified, _set_modified) + def __init__(self, state, key): + self.state = state + self.key = key @property - def unmodified(self): - """a set of keys which have no uncommitted changes""" + def loaded_value(self): + """The current value of this attribute as loaded from the database. - dict_ = self.dict - - return set([ - key for key in self.manager - if (key not in self.committed_state or - (key in self.manager.mutable_attributes and - not self.manager[key].impl.check_mutable_modified(self, dict_)))]) - - def unmodified_intersection(self, keys): - """Return self.unmodified.intersection(keys).""" - - dict_ = self.dict - - return set([ - key for key in keys - if (key not in self.committed_state or - (key in self.manager.mutable_attributes and - not self.manager[key].impl.check_mutable_modified(self, dict_)))]) - - - def _is_really_none(self): - """do a check modified/resurrect. - - This would be called in the extremely rare - race condition that the weakref returned None but - the cleanup handler had not yet established the - __resurrect callable as its replacement. + If the value has not been loaded, or is otherwise not present + in the object's dictionary, returns NO_VALUE. """ - if self.modified: - self.obj = self.__resurrect - return self.obj() - else: - return None + return self.state.dict.get(self.key, NO_VALUE) - def reset(self, dict_, key): - self.mutable_dict.pop(key, None) - InstanceState.reset(self, dict_, key) + @property + def value(self): + """Return the value of this attribute. - def _cleanup(self, ref): - """weakref callback. - - This method may be called by an asynchronous - gc. - - If the state shows pending changes, the weakref - is replaced by the __resurrect callable which will - re-establish an object reference on next access, - else removes this InstanceState from the owning - identity map, if any. + This operation is equivalent to accessing the object's + attribute directly or via ``getattr()``, and will fire + off any pending loader callables if needed. """ - if self._get_modified(self.mutable_dict): - self.obj = self.__resurrect - else: - instance_dict = self._instance_dict() - if instance_dict: - instance_dict.discard(self) - self.dispose() + return self.state.manager[self.key].__get__( + self.state.obj(), self.state.class_) - def __resurrect(self): - """A substitute for the obj() weakref function which resurrects.""" + @property + def history(self): + """Return the current pre-flush change history for + this attribute, via the :class:`.History` interface. - # store strong ref'ed version of the object; will revert - # to weakref when changes are persisted - obj = self.manager.new_instance(state=self) - self.obj = weakref.ref(obj, self._cleanup) - self._strong_obj = obj - obj.__dict__.update(self.mutable_dict) + This method will **not** emit loader callables if the value of the + attribute is unloaded. + + .. seealso:: + + :meth:`.AttributeState.load_history` - retrieve history + using loader callables if the value is not locally present. + + :func:`.attributes.get_history` - underlying function + + """ + return self.state.get_history(self.key, + PASSIVE_NO_INITIALIZE) + + def load_history(self): + """Return the current pre-flush change history for + this attribute, via the :class:`.History` interface. + + This method **will** emit loader callables if the value of the + attribute is unloaded. + + .. seealso:: + + :attr:`.AttributeState.history` + + :func:`.attributes.get_history` - underlying function + + .. versionadded:: 0.9.0 + + """ + return self.state.get_history(self.key, + PASSIVE_OFF ^ INIT_OK) - # re-establishes identity attributes from the key - self.manager.dispatch.resurrect(self) - return obj class PendingCollection(object): """A writable placeholder for an unloaded collection. @@ -564,4 +615,3 @@ class PendingCollection(object): self.added_items.remove(value) else: self.deleted_items.add(value) - diff --git a/libs/sqlalchemy/orm/strategies.py b/libs/sqlalchemy/orm/strategies.py index 2cde3f67..033e3d06 100644 --- a/libs/sqlalchemy/orm/strategies.py +++ b/libs/sqlalchemy/orm/strategies.py @@ -1,5 +1,5 @@ # orm/strategies.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,26 +7,25 @@ """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" -from sqlalchemy import exc as sa_exc -from sqlalchemy import sql, util, log, event -from sqlalchemy.sql import util as sql_util -from sqlalchemy.sql import visitors -from sqlalchemy.orm import attributes, interfaces, exc as orm_exc -from sqlalchemy.orm.mapper import _none_set -from sqlalchemy.orm.interfaces import ( - LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, - StrategizedProperty +from .. import exc as sa_exc, inspect +from .. import util, log, event +from ..sql import util as sql_util, visitors +from . import ( + attributes, interfaces, exc as orm_exc, loading, + unitofwork, util as orm_util ) -from sqlalchemy.orm import session as sessionlib, unitofwork -from sqlalchemy.orm import util as mapperutil -from sqlalchemy.orm.query import Query +from .state import InstanceState +from .util import _none_set +from . import properties +from .interfaces import ( + LoaderStrategy, StrategizedProperty + ) +from .session import _state_session import itertools def _register_attribute(strategy, mapper, useobject, compare_function=None, typecallable=None, - copy_function=None, - mutable_scalars=False, uselist=False, callable_=None, proxy_property=None, @@ -45,10 +44,10 @@ def _register_attribute(strategy, mapper, useobject, listen_hooks.append(single_parent_validator) if prop.key in prop.parent.validators: - fn, include_removes = prop.parent.validators[prop.key] + fn, opts = prop.parent.validators[prop.key] listen_hooks.append( - lambda desc, prop: mapperutil._validator_events(desc, - prop.key, fn, include_removes) + lambda desc, prop: orm_util._validator_events(desc, + prop.key, fn, **opts) ) if useobject: @@ -71,17 +70,17 @@ def _register_attribute(strategy, mapper, useobject, m.class_, prop.key, parent_token=prop, - mutable_scalars=mutable_scalars, uselist=uselist, - copy_function=copy_function, compare_function=compare_function, useobject=useobject, extension=attribute_ext, - trackparent=useobject and (prop.single_parent or prop.direction is interfaces.ONETOMANY), + trackparent=useobject and (prop.single_parent + or prop.direction is interfaces.ONETOMANY), typecallable=typecallable, callable_=callable_, active_history=active_history, impl_class=impl_class, + send_modified_events=not useobject or not prop.viewonly, doc=prop.doc, **kw ) @@ -89,6 +88,7 @@ def _register_attribute(strategy, mapper, useobject, for hook in listen_hooks: hook(desc, prop) +@properties.ColumnProperty.strategy_for(instrument=False, deferred=False) class UninstrumentedColumnLoader(LoaderStrategy): """Represent the a non-instrumented MapperProperty. @@ -96,27 +96,32 @@ class UninstrumentedColumnLoader(LoaderStrategy): if the argument is against the with_polymorphic selectable. """ - def init(self): + def __init__(self, parent): + super(UninstrumentedColumnLoader, self).__init__(parent) self.columns = self.parent_property.columns - def setup_query(self, context, entity, path, reduced_path, adapter, + def setup_query(self, context, entity, path, loadopt, adapter, column_collection=None, **kwargs): for c in self.columns: if adapter: c = adapter.columns[c] column_collection.append(c) - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): return None, None, None + +@log.class_logger +@properties.ColumnProperty.strategy_for(instrument=True, deferred=False) class ColumnLoader(LoaderStrategy): """Provide loading behavior for a :class:`.ColumnProperty`.""" - def init(self): + def __init__(self, parent): + super(ColumnLoader, self).__init__(parent) self.columns = self.parent_property.columns self.is_composite = hasattr(self.parent_property, 'composite_class') - def setup_query(self, context, entity, path, reduced_path, + def setup_query(self, context, entity, path, loadopt, adapter, column_collection, **kwargs): for c in self.columns: if adapter: @@ -128,17 +133,16 @@ class ColumnLoader(LoaderStrategy): coltype = self.columns[0].type # TODO: check all columns ? check for foreign key as well? active_history = self.parent_property.active_history or \ - self.columns[0].primary_key + self.columns[0].primary_key or \ + mapper.version_id_col in set(self.columns) _register_attribute(self, mapper, useobject=False, compare_function=coltype.compare_values, - copy_function=coltype.copy_value, - mutable_scalars=self.columns[0].type.is_mutable(), - active_history = active_history + active_history=active_history ) - def create_row_processor(self, context, path, reduced_path, - mapper, row, adapter): + def create_row_processor(self, context, path, + loadopt, mapper, row, adapter): key = self.key # look through list of columns represented here # to see which, if any, is present in the row. @@ -151,69 +155,71 @@ class ColumnLoader(LoaderStrategy): return fetch_col, None, None else: def expire_for_non_present_col(state, dict_, row): - state.expire_attribute_pre_commit(dict_, key) + state._expire_attribute_pre_commit(dict_, key) return expire_for_non_present_col, None, None -log.class_logger(ColumnLoader) + +@log.class_logger +@properties.ColumnProperty.strategy_for(deferred=True, instrument=True) class DeferredColumnLoader(LoaderStrategy): """Provide loading behavior for a deferred :class:`.ColumnProperty`.""" - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): - col = self.columns[0] - if adapter: - col = adapter.columns[col] - - key = self.key - if col in row: - return self.parent_property._get_strategy(ColumnLoader).\ - create_row_processor( - context, path, reduced_path, mapper, row, adapter) - - elif not self.is_class_level: - def set_deferred_for_local_state(state, dict_, row): - state.set_callable(dict_, key, LoadDeferredColumns(state, key)) - return set_deferred_for_local_state, None, None - else: - def reset_col_for_deferred(state, dict_, row): - # reset state on the key so that deferred callables - # fire off on next access. - state.reset(dict_, key) - return reset_col_for_deferred, None, None - - def init(self): + def __init__(self, parent): + super(DeferredColumnLoader, self).__init__(parent) if hasattr(self.parent_property, 'composite_class'): raise NotImplementedError("Deferred loading for composite " "types not implemented yet") self.columns = self.parent_property.columns self.group = self.parent_property.group + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): + col = self.columns[0] + if adapter: + col = adapter.columns[col] + + key = self.key + if col in row: + return self.parent_property._get_strategy_by_cls(ColumnLoader).\ + create_row_processor( + context, path, loadopt, mapper, row, adapter) + + elif not self.is_class_level: + set_deferred_for_local_state = InstanceState._row_processor( + mapper.class_manager, + LoadDeferredColumns(key), key) + return set_deferred_for_local_state, None, None + else: + def reset_col_for_deferred(state, dict_, row): + # reset state on the key so that deferred callables + # fire off on next access. + state._reset(dict_, key) + return reset_col_for_deferred, None, None + def init_class_attribute(self, mapper): self.is_class_level = True _register_attribute(self, mapper, useobject=False, compare_function=self.columns[0].type.compare_values, - copy_function=self.columns[0].type.copy_value, - mutable_scalars=self.columns[0].type.is_mutable(), callable_=self._load_for_state, expire_missing=False ) - def setup_query(self, context, entity, path, reduced_path, adapter, + def setup_query(self, context, entity, path, loadopt, adapter, only_load_props=None, **kwargs): if ( - self.group is not None and - context.attributes.get(('undefer', self.group), False) + loadopt and self.group and + loadopt.local_opts.get('undefer_group', False) == self.group ) or (only_load_props and self.key in only_load_props): - self.parent_property._get_strategy(ColumnLoader).\ + self.parent_property._get_strategy_by_cls(ColumnLoader).\ setup_query(context, entity, - path, reduced_path, adapter, **kwargs) + path, loadopt, adapter, **kwargs) def _load_for_state(self, state, passive): if not state.key: return attributes.ATTR_EMPTY - if passive is attributes.PASSIVE_NO_FETCH: + if not passive & attributes.SQL_OK: return attributes.PASSIVE_NO_RESULT localparent = state.manager.mapper @@ -224,7 +230,7 @@ class DeferredColumnLoader(LoaderStrategy): localparent.iterate_properties if isinstance(p, StrategizedProperty) and isinstance(p.strategy, DeferredColumnLoader) and - p.group==self.group + p.group == self.group ] else: toload = [self.key] @@ -232,68 +238,53 @@ class DeferredColumnLoader(LoaderStrategy): # narrow the keys down to just those which have no history group = [k for k in toload if k in state.unmodified] - session = sessionlib._state_session(state) + session = _state_session(state) if session is None: raise orm_exc.DetachedInstanceError( "Parent instance %s is not bound to a Session; " "deferred load operation of attribute '%s' cannot proceed" % - (mapperutil.state_str(state), self.key) + (orm_util.state_str(state), self.key) ) query = session.query(localparent) - if query._load_on_ident(state.key, + if loading.load_on_ident(query, state.key, only_load_props=group, refresh_state=state) is None: raise orm_exc.ObjectDeletedError(state) return attributes.ATTR_WAS_SET -log.class_logger(DeferredColumnLoader) + class LoadDeferredColumns(object): """serializable loader object used by DeferredColumnLoader""" - def __init__(self, state, key): - self.state = state + def __init__(self, key): self.key = key - def __call__(self, passive=attributes.PASSIVE_OFF): - state, key = self.state, self.key + def __call__(self, state, passive=attributes.PASSIVE_OFF): + key = self.key localparent = state.manager.mapper prop = localparent._props[key] strategy = prop._strategies[DeferredColumnLoader] return strategy._load_for_state(state, passive) -class DeferredOption(StrategizedOption): - propagate_to_loaders = True - def __init__(self, key, defer=False): - super(DeferredOption, self).__init__(key) - self.defer = defer - - def get_strategy_class(self): - if self.defer: - return DeferredColumnLoader - else: - return ColumnLoader - -class UndeferGroupOption(MapperOption): - propagate_to_loaders = True - - def __init__(self, group): - self.group = group - - def process_query(self, query): - query._attributes[('undefer', self.group)] = True class AbstractRelationshipLoader(LoaderStrategy): """LoaderStratgies which deal with related objects.""" - def init(self): + def __init__(self, parent): + super(AbstractRelationshipLoader, self).__init__(parent) self.mapper = self.parent_property.mapper self.target = self.parent_property.target self.uselist = self.parent_property.uselist + + +@log.class_logger +@properties.RelationshipProperty.strategy_for(lazy="noload") +@properties.RelationshipProperty.strategy_for(lazy=None) class NoLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` with "lazy=None". @@ -306,39 +297,41 @@ class NoLoader(AbstractRelationshipLoader): _register_attribute(self, mapper, useobject=True, uselist=self.parent_property.uselist, - typecallable = self.parent_property.collection_class, + typecallable=self.parent_property.collection_class, ) - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): def invoke_no_load(state, dict_, row): - state.initialize(self.key) + state._initialize(self.key) return invoke_no_load, None, None -log.class_logger(NoLoader) + +@log.class_logger +@properties.RelationshipProperty.strategy_for(lazy=True) +@properties.RelationshipProperty.strategy_for(lazy="select") class LazyLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` with "lazy=True", that is loads when first accessed. """ - def init(self): - super(LazyLoader, self).init() + def __init__(self, parent): + super(LazyLoader, self).__init__(parent) + join_condition = self.parent_property._join_condition self._lazywhere, \ self._bind_to_col, \ - self._equated_columns = self._create_lazy_clause(self.parent_property) + self._equated_columns = join_condition.create_lazy_clause() self._rev_lazywhere, \ self._rev_bind_to_col, \ - self._rev_equated_columns = self._create_lazy_clause( - self.parent_property, + self._rev_equated_columns = join_condition.create_lazy_clause( reverse_direction=True) self.logger.info("%s lazy loading clause %s", self, self._lazywhere) # determine if our "lazywhere" clause is the same as the mapper's # get() clause. then we can just use mapper.get() - #from sqlalchemy.orm import query self.use_get = not self.uselist and \ self.mapper._get_clause[0].compare( self._lazywhere, @@ -347,7 +340,7 @@ class LazyLoader(AbstractRelationshipLoader): ) if self.use_get: - for col in self._equated_columns.keys(): + for col in list(self._equated_columns): if col in self.mapper._equivalent_columns: for c in self.mapper._equivalent_columns[col]: self._equated_columns[c] = self._equated_columns[col] @@ -358,6 +351,12 @@ class LazyLoader(AbstractRelationshipLoader): def init_class_attribute(self, mapper): self.is_class_level = True + active_history = ( + self.parent_property.active_history or + self.parent_property.direction is not interfaces.MANYTOONE or + not self.use_get + ) + # MANYTOONE currently only needs the # "old" value for delete-orphan # cascades. the required _SingleParentValidator @@ -365,22 +364,19 @@ class LazyLoader(AbstractRelationshipLoader): # in that case. otherwise we don't need the # "old" value during backref operations. _register_attribute(self, - mapper, - useobject=True, - callable_=self._load_for_state, - uselist = self.parent_property.uselist, - backref = self.parent_property.back_populates, - typecallable = self.parent_property.collection_class, - active_history = \ - self.parent_property.active_history or \ - self.parent_property.direction is not \ - interfaces.MANYTOONE or \ - not self.use_get, - ) + mapper, + useobject=True, + callable_=self._load_for_state, + uselist=self.parent_property.uselist, + backref=self.parent_property.back_populates, + typecallable=self.parent_property.collection_class, + active_history=active_history + ) def lazy_clause(self, state, reverse_direction=False, alias_secondary=False, - adapt_source=None): + adapt_source=None, + passive=None): if state is None: return self._lazy_none_clause( reverse_direction, @@ -402,14 +398,13 @@ class LazyLoader(AbstractRelationshipLoader): else: mapper = self.parent_property.parent - o = state.obj() # strong ref + o = state.obj() # strong ref dict_ = attributes.instance_dict(o) # use the "committed state" only if we're in a flush # for this state. - sess = sessionlib._state_session(state) - if sess is not None and sess._flushing: + if passive and passive & attributes.LOAD_AGAINST_COMMITTED: def visit_bindparam(bindparam): if bindparam._identifying_key in bind_to_col: bindparam.callable = \ @@ -424,14 +419,13 @@ class LazyLoader(AbstractRelationshipLoader): state, dict_, bind_to_col[bindparam._identifying_key]) - if self.parent_property.secondary is not None and alias_secondary: criterion = sql_util.ClauseAdapter( self.parent_property.secondary.alias()).\ traverse(criterion) criterion = visitors.cloned_traverse( - criterion, {}, {'bindparam':visit_bindparam}) + criterion, {}, {'bindparam': visit_bindparam}) if adapt_source: criterion = adapt_source(criterion) @@ -457,28 +451,31 @@ class LazyLoader(AbstractRelationshipLoader): def _load_for_state(self, state, passive): if not state.key and \ - (not self.parent_property.load_on_pending or not state.session_id): + ( + ( + not self.parent_property.load_on_pending + and not state._load_pending + ) + or not state.session_id + ): return attributes.ATTR_EMPTY pending = not state.key ident_key = None if ( - (passive is attributes.PASSIVE_NO_FETCH or \ - passive is attributes.PASSIVE_NO_FETCH_RELATED) and - not self.use_get - ) or ( - passive is attributes.PASSIVE_ONLY_PERSISTENT and - pending - ): + (not passive & attributes.SQL_OK and not self.use_get) + or + (not passive & attributes.NON_PERSISTENT_OK and pending) + ): return attributes.PASSIVE_NO_RESULT - session = sessionlib._state_session(state) + session = _state_session(state) if not session: raise orm_exc.DetachedInstanceError( "Parent instance %s is not bound to a Session; " "lazy load operation of attribute '%s' cannot proceed" % - (mapperutil.state_str(state), self.key) + (orm_util.state_str(state), self.key) ) # if we have a simple primary key load, check the @@ -498,40 +495,36 @@ class LazyLoader(AbstractRelationshipLoader): return None ident_key = self.mapper.identity_key_from_primary_key(ident) - instance = Query._get_from_identity(session, ident_key, passive) + instance = loading.get_from_identity(session, ident_key, passive) if instance is not None: return instance - elif passive is attributes.PASSIVE_NO_FETCH or \ - passive is attributes.PASSIVE_NO_FETCH_RELATED: + elif not passive & attributes.SQL_OK or \ + not passive & attributes.RELATED_OBJECT_OK: return attributes.PASSIVE_NO_RESULT - return self._emit_lazyload(session, state, ident_key) + return self._emit_lazyload(session, state, ident_key, passive) def _get_ident_for_use_get(self, session, state, passive): instance_mapper = state.manager.mapper - if session._flushing: + if passive & attributes.LOAD_AGAINST_COMMITTED: get_attr = instance_mapper._get_committed_state_attr_by_column else: get_attr = instance_mapper._get_state_attr_by_column dict_ = state.dict - if passive is attributes.PASSIVE_NO_FETCH_RELATED: - attr_passive = attributes.PASSIVE_OFF - else: - attr_passive = passive - return [ get_attr( state, dict_, self._equated_columns[pk], - passive=attr_passive) + passive=passive) for pk in self.mapper.primary_key ] - def _emit_lazyload(self, session, state, ident_key): + @util.dependencies("sqlalchemy.orm.strategy_options") + def _emit_lazyload(self, strategy_options, session, state, ident_key, passive): q = session.query(self.mapper)._adapt_all_clauses() q = q._with_invoke_all_eagers(False) @@ -543,13 +536,13 @@ class LazyLoader(AbstractRelationshipLoader): q = q.autoflush(False) if state.load_path: - q = q._with_current_path(state.load_path + (self.key,)) + q = q._with_current_path(state.load_path[self.parent_property]) if state.load_options: q = q._conditional_options(*state.load_options) if self.use_get: - return q._load_on_ident(ident_key) + return loading.load_on_ident(q, ident_key) if self.parent_property.order_by: q = q.order_by(*util.to_list(self.parent_property.order_by)) @@ -560,9 +553,9 @@ class LazyLoader(AbstractRelationshipLoader): if rev.direction is interfaces.MANYTOONE and \ rev._use_get and \ not isinstance(rev.strategy, LazyLoader): - q = q.options(EagerLazyOption((rev.key,), lazy='select')) + q = q.options(strategy_options.Load(rev.parent).lazyload(rev.key)) - lazy_clause = self.lazy_clause(state) + lazy_clause = self.lazy_clause(state, passive=passive) if pending: bind_values = sql_util.bind_values(lazy_clause) @@ -587,21 +580,22 @@ class LazyLoader(AbstractRelationshipLoader): else: return None - - def create_row_processor(self, context, path, reduced_path, + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): key = self.key if not self.is_class_level: - def set_lazy_callable(state, dict_, row): - # we are not the primary manager for this attribute - # on this class - set up a - # per-instance lazyloader, which will override the - # class-level behavior. - # this currently only happens when using a - # "lazyload" option on a "no load" - # attribute - "eager" attributes always have a - # class-level lazyloader installed. - state.set_callable(dict_, key, LoadLazyAttribute(state, key)) + # we are not the primary manager for this attribute + # on this class - set up a + # per-instance lazyloader, which will override the + # class-level behavior. + # this currently only happens when using a + # "lazyload" option on a "no load" + # attribute - "eager" attributes always have a + # class-level lazyloader installed. + set_lazy_callable = InstanceState._row_processor( + mapper.class_manager, + LoadLazyAttribute(key), key) + return set_lazy_callable, None, None else: def reset_for_lazy_callable(state, dict_, row): @@ -613,65 +607,20 @@ class LazyLoader(AbstractRelationshipLoader): # this is needed in # populate_existing() types of scenarios to reset # any existing state. - state.reset(dict_, key) + state._reset(dict_, key) return reset_for_lazy_callable, None, None - @classmethod - def _create_lazy_clause(cls, prop, reverse_direction=False): - binds = util.column_dict() - lookup = util.column_dict() - equated_columns = util.column_dict() - if reverse_direction and prop.secondaryjoin is None: - for l, r in prop.local_remote_pairs: - _list = lookup.setdefault(r, []) - _list.append((r, l)) - equated_columns[l] = r - else: - for l, r in prop.local_remote_pairs: - _list = lookup.setdefault(l, []) - _list.append((l, r)) - equated_columns[r] = l - - def col_to_bind(col): - if col in lookup: - for tobind, equated in lookup[col]: - if equated in binds: - return None - if col not in binds: - binds[col] = sql.bindparam(None, None, type_=col.type, unique=True) - return binds[col] - return None - - lazywhere = prop.primaryjoin - - if prop.secondaryjoin is None or not reverse_direction: - lazywhere = visitors.replacement_traverse( - lazywhere, {}, col_to_bind) - - if prop.secondaryjoin is not None: - secondaryjoin = prop.secondaryjoin - if reverse_direction: - secondaryjoin = visitors.replacement_traverse( - secondaryjoin, {}, col_to_bind) - lazywhere = sql.and_(lazywhere, secondaryjoin) - - bind_to_col = dict((binds[col].key, col) for col in binds) - - return lazywhere, bind_to_col, equated_columns - -log.class_logger(LazyLoader) class LoadLazyAttribute(object): """serializable loader object used by LazyLoader""" - def __init__(self, state, key): - self.state = state + def __init__(self, key): self.key = key - def __call__(self, passive=attributes.PASSIVE_OFF): - state, key = self.state, self.key + def __call__(self, state, passive=attributes.PASSIVE_OFF): + key = self.key instance_mapper = state.manager.mapper prop = instance_mapper._props[key] strategy = prop._strategies[LazyLoader] @@ -679,62 +628,72 @@ class LoadLazyAttribute(object): return strategy._load_for_state(state, passive) +@properties.RelationshipProperty.strategy_for(lazy="immediate") class ImmediateLoader(AbstractRelationshipLoader): def init_class_attribute(self, mapper): self.parent_property.\ - _get_strategy(LazyLoader).\ + _get_strategy_by_cls(LazyLoader).\ init_class_attribute(mapper) def setup_query(self, context, entity, - path, reduced_path, adapter, column_collection=None, + path, loadopt, adapter, column_collection=None, parentmapper=None, **kwargs): pass - def create_row_processor(self, context, path, reduced_path, + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): def load_immediate(state, dict_, row): state.get_impl(self.key).get(state, dict_) return None, None, load_immediate + +@log.class_logger +@properties.RelationshipProperty.strategy_for(lazy="subquery") class SubqueryLoader(AbstractRelationshipLoader): - def init(self): - super(SubqueryLoader, self).init() + def __init__(self, parent): + super(SubqueryLoader, self).__init__(parent) self.join_depth = self.parent_property.join_depth def init_class_attribute(self, mapper): self.parent_property.\ - _get_strategy(LazyLoader).\ + _get_strategy_by_cls(LazyLoader).\ init_class_attribute(mapper) def setup_query(self, context, entity, - path, reduced_path, adapter, + path, loadopt, adapter, column_collection=None, parentmapper=None, **kwargs): if not context.query._enable_eagerloads: return - path = path + (self.key, ) - reduced_path = reduced_path + (self.key, ) + path = path[self.parent_property] # build up a path indicating the path from the leftmost # entity to the thing we're subquery loading. - subq_path = context.attributes.get(('subquery_path', None), ()) + with_poly_info = path.get(context.attributes, + "path_with_polymorphic", None) + if with_poly_info is not None: + effective_entity = with_poly_info.entity + else: + effective_entity = self.mapper + + subq_path = context.attributes.get(('subquery_path', None), + orm_util.PathRegistry.root) subq_path = subq_path + path - # join-depth / recursion check - if ("loaderstrategy", reduced_path) not in context.attributes: + # if not via query option, check for + # a cycle + if not path.contains(context.attributes, "loader"): if self.join_depth: - if len(path) / 2 > self.join_depth: - return - else: - if self.mapper.base_mapper in \ - interfaces._reduce_path(subq_path): + if path.length / 2 > self.join_depth: return + elif subq_path.contains_mapper(self.mapper): + return - subq_mapper, leftmost_mapper, leftmost_attr = \ + subq_mapper, leftmost_mapper, leftmost_attr, leftmost_relationship = \ self._get_leftmost(subq_path) orig_query = context.attributes.get( @@ -745,7 +704,8 @@ class SubqueryLoader(AbstractRelationshipLoader): # produce a subquery from it. left_alias = self._generate_from_original_query( orig_query, leftmost_mapper, - leftmost_attr, subq_path + leftmost_attr, leftmost_relationship, + entity.mapper ) # generate another Query that will join the @@ -753,10 +713,10 @@ class SubqueryLoader(AbstractRelationshipLoader): # basically doing a longhand # "from_self()". (from_self() itself not quite industrial # strength enough for all contingencies...but very close) - q = orig_query.session.query(self.mapper) + q = orig_query.session.query(effective_entity) q._attributes = { ("orig_query", SubqueryLoader): orig_query, - ('subquery_path', None) : subq_path + ('subquery_path', None): subq_path } q = q._enable_single_crit(False) @@ -765,46 +725,67 @@ class SubqueryLoader(AbstractRelationshipLoader): q = q.order_by(*local_attr) q = q.add_columns(*local_attr) - q = self._apply_joins(q, to_join, left_alias, parent_alias) + q = self._apply_joins(q, to_join, left_alias, + parent_alias, effective_entity) - q = self._setup_options(q, subq_path, orig_query) + q = self._setup_options(q, subq_path, orig_query, effective_entity) q = self._setup_outermost_orderby(q) # add new query to attributes to be picked up # by create_row_processor - context.attributes[('subquery', reduced_path)] = q + path.set(context.attributes, "subquery", q) def _get_leftmost(self, subq_path): - subq_mapper = mapperutil._class_to_mapper(subq_path[0]) + subq_path = subq_path.path + subq_mapper = orm_util._class_to_mapper(subq_path[0]) # determine attributes of the leftmost mapper - if self.parent.isa(subq_mapper) and self.key==subq_path[1]: + if self.parent.isa(subq_mapper) and self.parent_property is subq_path[1]: leftmost_mapper, leftmost_prop = \ self.parent, self.parent_property else: leftmost_mapper, leftmost_prop = \ subq_mapper, \ - subq_mapper._props[subq_path[1]] - leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop) + subq_path[1] + + leftmost_cols = leftmost_prop.local_columns leftmost_attr = [ leftmost_mapper._columntoproperty[c].class_attribute for c in leftmost_cols ] - return subq_mapper, leftmost_mapper, leftmost_attr + return subq_mapper, leftmost_mapper, leftmost_attr, leftmost_prop def _generate_from_original_query(self, orig_query, leftmost_mapper, - leftmost_attr, subq_path + leftmost_attr, leftmost_relationship, + entity_mapper ): # reformat the original query # to look only for significant columns - q = orig_query._clone() + q = orig_query._clone().correlate(None) - # TODO: why does polymporphic etc. require hardcoding - # into _adapt_col_list ? Does query.add_columns(...) work - # with polymorphic loading ? - q._set_entities(q._adapt_col_list(leftmost_attr)) + # set a real "from" if not present, as this is more + # accurate than just going off of the column expression + if not q._from_obj and entity_mapper.isa(leftmost_mapper): + q._set_select_from([entity_mapper], False) + + target_cols = q._adapt_col_list(leftmost_attr) + + # select from the identity columns of the outer + q._set_entities(target_cols) + + distinct_target_key = leftmost_relationship.distinct_target_key + + if distinct_target_key is True: + q._distinct = True + elif distinct_target_key is None: + # if target_cols refer to a non-primary key or only + # part of a composite primary key, set the q as distinct + for t in set(c.table for c in target_cols): + if not set(target_cols).issuperset(t.primary_key): + q._distinct = True + break if q._order_by is False: q._order_by = leftmost_mapper.order_by @@ -815,48 +796,66 @@ class SubqueryLoader(AbstractRelationshipLoader): # the original query now becomes a subquery # which we'll join onto. - embed_q = q.with_labels().subquery() - left_alias = mapperutil.AliasedClass(leftmost_mapper, embed_q) - return left_alias + embed_q = q.with_labels().subquery() + left_alias = orm_util.AliasedClass(leftmost_mapper, embed_q, + use_mapper_path=True) + return left_alias def _prep_for_joins(self, left_alias, subq_path): # figure out what's being joined. a.k.a. the fun part - to_join = [ - (subq_path[i], subq_path[i+1]) - for i in xrange(0, len(subq_path), 2) - ] + to_join = [] + pairs = list(subq_path.pairs()) + + for i, (mapper, prop) in enumerate(pairs): + if i > 0: + # look at the previous mapper in the chain - + # if it is as or more specific than this prop's + # mapper, use that instead. + # note we have an assumption here that + # the non-first element is always going to be a mapper, + # not an AliasedClass + + prev_mapper = pairs[i - 1][1].mapper + to_append = prev_mapper if prev_mapper.isa(mapper) else mapper + else: + to_append = mapper + + to_join.append((to_append, prop.key)) # determine the immediate parent class we are joining from, # which needs to be aliased. + if len(to_join) > 1: + info = inspect(to_join[-1][0]) if len(to_join) < 2: # in the case of a one level eager load, this is the # leftmost "left_alias". parent_alias = left_alias - elif subq_path[-2].isa(self.parent): + elif info.mapper.isa(self.parent): # In the case of multiple levels, retrieve # it from subq_path[-2]. This is the same as self.parent # in the vast majority of cases, and [ticket:2014] # illustrates a case where sub_path[-2] is a subclass # of self.parent - parent_alias = mapperutil.AliasedClass(subq_path[-2]) + parent_alias = orm_util.AliasedClass(to_join[-1][0], + use_mapper_path=True) else: # if of_type() were used leading to this relationship, # self.parent is more specific than subq_path[-2] - parent_alias = mapperutil.AliasedClass(self.parent) + parent_alias = orm_util.AliasedClass(self.parent, + use_mapper_path=True) - local_cols, remote_cols = \ - self._local_remote_columns(self.parent_property) + local_cols = self.parent_property.local_columns local_attr = [ getattr(parent_alias, self.parent._columntoproperty[c].key) for c in local_cols ] - return to_join, local_attr, parent_alias - def _apply_joins(self, q, to_join, left_alias, parent_alias): + def _apply_joins(self, q, to_join, left_alias, parent_alias, + effective_entity): for i, (mapper, key) in enumerate(to_join): # we need to use query.join() as opposed to @@ -869,11 +868,18 @@ class SubqueryLoader(AbstractRelationshipLoader): first = i == 0 middle = i < len(to_join) - 1 second_to_last = i == len(to_join) - 2 + last = i == len(to_join) - 1 if first: attr = getattr(left_alias, key) + if last and effective_entity is not self.mapper: + attr = attr.of_type(effective_entity) else: - attr = key + if last and effective_entity is not self.mapper: + attr = getattr(parent_alias, key).\ + of_type(effective_entity) + else: + attr = key if second_to_last: q = q.join(parent_alias, attr, from_joinpoint=True) @@ -881,24 +887,14 @@ class SubqueryLoader(AbstractRelationshipLoader): q = q.join(attr, aliased=middle, from_joinpoint=True) return q - def _local_remote_columns(self, prop): - if prop.secondary is None: - return zip(*prop.local_remote_pairs) - else: - return \ - [p[0] for p in prop.synchronize_pairs],\ - [ - p[0] for p in prop. - secondary_synchronize_pairs - ] - - def _setup_options(self, q, subq_path, orig_query): + def _setup_options(self, q, subq_path, orig_query, effective_entity): # propagate loader options etc. to the new query. # these will fire relative to subq_path. q = q._with_current_path(subq_path) q = q._conditional_options(*orig_query._with_options) if orig_query._populate_existing: q._populate_existing = orig_query._populate_existing + return q def _setup_outermost_orderby(self, q): @@ -919,7 +915,36 @@ class SubqueryLoader(AbstractRelationshipLoader): q = q.order_by(*eager_order_by) return q - def create_row_processor(self, context, path, reduced_path, + class _SubqCollections(object): + """Given a :class:`.Query` used to emit the "subquery load", + provide a load interface that executes the query at the + first moment a value is needed. + + """ + _data = None + + def __init__(self, subq): + self.subq = subq + + def get(self, key, default): + if self._data is None: + self._load() + return self._data.get(key, default) + + def _load(self): + self._data = dict( + (k, [vv[0] for vv in v]) + for k, v in itertools.groupby( + self.subq, + lambda x: x[1:] + ) + ) + + def loader(self, state, dict_, row): + if self._data is None: + self._load() + + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): if not self.parent.class_manager[self.key].impl.supports_population: raise sa_exc.InvalidRequestError( @@ -927,27 +952,22 @@ class SubqueryLoader(AbstractRelationshipLoader): "population - eager loading cannot be applied." % self) - reduced_path = reduced_path + (self.key,) + path = path[self.parent_property] - if ('subquery', reduced_path) not in context.attributes: + subq = path.get(context.attributes, 'subquery') + + if subq is None: return None, None, None - local_cols, remote_cols = self._local_remote_columns(self.parent_property) - - q = context.attributes[('subquery', reduced_path)] + local_cols = self.parent_property.local_columns # cache the loaded collections in the context # so that inheriting mappers don't re-load when they # call upon create_row_processor again - if ('collections', reduced_path) in context.attributes: - collections = context.attributes[('collections', reduced_path)] - else: - collections = context.attributes[('collections', reduced_path)] = dict( - (k, [v[0] for v in v]) - for k, v in itertools.groupby( - q, - lambda x:x[1:] - )) + collections = path.get(context.attributes, "collections") + if collections is None: + collections = self._SubqCollections(subq) + path.set(context.attributes, 'collections', collections) if adapter: local_cols = [adapter.columns[c] for c in local_cols] @@ -966,7 +986,7 @@ class SubqueryLoader(AbstractRelationshipLoader): state.get_impl(self.key).\ set_committed_value(state, dict_, collection) - return load_collection_from_subq, None, None + return load_collection_from_subq, None, None, collections.loader def _create_scalar_loader(self, collections, local_cols): def load_scalar_from_subq(state, dict_, row): @@ -984,108 +1004,191 @@ class SubqueryLoader(AbstractRelationshipLoader): state.get_impl(self.key).\ set_committed_value(state, dict_, scalar) - return load_scalar_from_subq, None, None + return load_scalar_from_subq, None, None, collections.loader -log.class_logger(SubqueryLoader) + +@log.class_logger +@properties.RelationshipProperty.strategy_for(lazy="joined") +@properties.RelationshipProperty.strategy_for(lazy=False) class JoinedLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` using joined eager loading. """ - def init(self): - super(JoinedLoader, self).init() + def __init__(self, parent): + super(JoinedLoader, self).__init__(parent) self.join_depth = self.parent_property.join_depth def init_class_attribute(self, mapper): self.parent_property.\ - _get_strategy(LazyLoader).init_class_attribute(mapper) + _get_strategy_by_cls(LazyLoader).init_class_attribute(mapper) - def setup_query(self, context, entity, path, reduced_path, adapter, \ + def setup_query(self, context, entity, path, loadopt, adapter, \ column_collection=None, parentmapper=None, allow_innerjoin=True, **kwargs): """Add a left outer join to the statement thats being constructed.""" - if not context.query._enable_eagerloads: return - path = path + (self.key,) - reduced_path = reduced_path + (self.key,) + path = path[self.parent_property] - if ("user_defined_eager_row_processor", reduced_path) in\ - context.attributes: + with_polymorphic = None + + user_defined_adapter = self._init_user_defined_eager_proc( + loadopt, context) if loadopt else False + + if user_defined_adapter is not False: clauses, adapter, add_to_collection = \ - self._get_user_defined_adapter( - context, entity, reduced_path, adapter + self._setup_query_on_user_defined_adapter( + context, entity, path, adapter, + user_defined_adapter ) else: - # check for join_depth or basic recursion, - # if the current path was not explicitly stated as - # a desired "loaderstrategy" (i.e. via query.options()) - if ("loaderstrategy", reduced_path) not in context.attributes: + # if not via query option, check for + # a cycle + if not path.contains(context.attributes, "loader"): if self.join_depth: - if len(path) / 2 > self.join_depth: - return - else: - if self.mapper.base_mapper in reduced_path: + if path.length / 2 > self.join_depth: return + elif path.contains_mapper(self.mapper): + return clauses, adapter, add_to_collection, \ allow_innerjoin = self._generate_row_adapter( - context, entity, path, reduced_path, adapter, + context, entity, path, loadopt, adapter, column_collection, parentmapper, allow_innerjoin ) - path += (self.mapper,) - reduced_path += (self.mapper.base_mapper,) + with_poly_info = path.get( + context.attributes, + "path_with_polymorphic", + None + ) + if with_poly_info is not None: + with_polymorphic = with_poly_info.with_polymorphic_mappers + else: + with_polymorphic = None - for value in self.mapper._polymorphic_properties: + path = path[self.mapper] + + for value in self.mapper._iterate_polymorphic_properties( + mappers=with_polymorphic): value.setup( context, entity, path, - reduced_path, clauses, parentmapper=self.mapper, column_collection=add_to_collection, allow_innerjoin=allow_innerjoin) - def _get_user_defined_adapter(self, context, entity, - reduced_path, adapter): - clauses = context.attributes[ - ("user_defined_eager_row_processor", - reduced_path)] + if with_poly_info is not None and \ + None in set(context.secondary_columns): + raise sa_exc.InvalidRequestError( + "Detected unaliased columns when generating joined " + "load. Make sure to use aliased=True or flat=True " + "when using joined loading with with_polymorphic()." + ) - adapter = entity._get_entity_clauses(context.query, context) - if adapter and clauses: - context.attributes[ - ("user_defined_eager_row_processor", - reduced_path)] = clauses = clauses.wrap(adapter) - elif adapter: - context.attributes[ - ("user_defined_eager_row_processor", - reduced_path)] = clauses = adapter + def _init_user_defined_eager_proc(self, loadopt, context): - add_to_collection = context.primary_columns - return clauses, adapter, add_to_collection + # check if the opt applies at all + if "eager_from_alias" not in loadopt.local_opts: + # nope + return False + + path = loadopt.path.parent + + # the option applies. check if the "user_defined_eager_row_processor" + # has been built up. + adapter = path.get(context.attributes, + "user_defined_eager_row_processor", False) + if adapter is not False: + # just return it + return adapter + + # otherwise figure it out. + alias = loadopt.local_opts["eager_from_alias"] + + root_mapper, prop = path[-2:] + + #from .mapper import Mapper + #from .interfaces import MapperProperty + #assert isinstance(root_mapper, Mapper) + #assert isinstance(prop, MapperProperty) + + if alias is not None: + if isinstance(alias, str): + alias = prop.target.alias(alias) + adapter = sql_util.ColumnAdapter(alias, + equivalents=prop.mapper._equivalent_columns) + else: + if path.contains(context.attributes, "path_with_polymorphic"): + with_poly_info = path.get(context.attributes, + "path_with_polymorphic") + adapter = orm_util.ORMAdapter( + with_poly_info.entity, + equivalents=prop.mapper._equivalent_columns) + else: + adapter = context.query._polymorphic_adapters.get(prop.mapper, None) + path.set(context.attributes, + "user_defined_eager_row_processor", + adapter) + + return adapter + + def _setup_query_on_user_defined_adapter(self, context, entity, + path, adapter, user_defined_adapter): + + # apply some more wrapping to the "user defined adapter" + # if we are setting up the query for SQL render. + adapter = entity._get_entity_clauses(context.query, context) + + if adapter and user_defined_adapter: + user_defined_adapter = user_defined_adapter.wrap(adapter) + path.set(context.attributes, "user_defined_eager_row_processor", + user_defined_adapter) + elif adapter: + user_defined_adapter = adapter + path.set(context.attributes, "user_defined_eager_row_processor", + user_defined_adapter) + + add_to_collection = context.primary_columns + return user_defined_adapter, adapter, add_to_collection def _generate_row_adapter(self, - context, entity, path, reduced_path, adapter, + context, entity, path, loadopt, adapter, column_collection, parentmapper, allow_innerjoin ): - clauses = mapperutil.ORMAdapter( - mapperutil.AliasedClass(self.mapper), + with_poly_info = path.get( + context.attributes, + "path_with_polymorphic", + None + ) + if with_poly_info: + to_adapt = with_poly_info.entity + else: + to_adapt = orm_util.AliasedClass(self.mapper, + flat=True, + use_mapper_path=True) + clauses = orm_util.ORMAdapter( + to_adapt, equivalents=self.mapper._equivalent_columns, adapt_required=True) + assert clauses.aliased_class is not None if self.parent_property.direction != interfaces.MANYTOONE: context.multi_row_eager_loaders = True - innerjoin = allow_innerjoin and context.attributes.get( - ("eager_join_type", path), - self.parent_property.innerjoin) + innerjoin = allow_innerjoin and ( + loadopt.local_opts.get( + 'innerjoin', self.parent_property.innerjoin) + if loadopt is not None + else self.parent_property.innerjoin + ) if not innerjoin: # if this is an outer join, all eager joins from # here must also be outer joins @@ -1098,9 +1201,8 @@ class JoinedLoader(AbstractRelationshipLoader): ) add_to_collection = context.secondary_columns - context.attributes[ - ("eager_row_processor", reduced_path) - ] = clauses + path.set(context.attributes, "eager_row_processor", clauses) + return clauses, adapter, add_to_collection, allow_innerjoin def _create_eager_join(self, context, entity, @@ -1119,6 +1221,7 @@ class JoinedLoader(AbstractRelationshipLoader): context.query._should_nest_selectable entity_key = None + if entity not in context.eager_joins and \ not should_nest_selectable and \ context.from_clause: @@ -1137,7 +1240,6 @@ class JoinedLoader(AbstractRelationshipLoader): towrap = context.eager_joins.setdefault(entity_key, default_towrap) - join_to_left = False if adapter: if getattr(adapter, 'aliased_class', None): onclause = getattr( @@ -1145,27 +1247,23 @@ class JoinedLoader(AbstractRelationshipLoader): self.parent_property) else: onclause = getattr( - mapperutil.AliasedClass( + orm_util.AliasedClass( self.parent, - adapter.selectable + adapter.selectable, + use_mapper_path=True ), self.key, self.parent_property ) - if onclause is self.parent_property: - # TODO: this is a temporary hack to - # account for polymorphic eager loads where - # the eagerload is referencing via of_type(). - join_to_left = True else: onclause = self.parent_property + assert clauses.aliased_class is not None context.eager_joins[entity_key] = eagerjoin = \ - mapperutil.join( + orm_util.join( towrap, clauses.aliased_class, onclause, - join_to_left=join_to_left, isouter=not innerjoin ) @@ -1181,7 +1279,7 @@ class JoinedLoader(AbstractRelationshipLoader): # by the Query propagates those columns outward. # This has the effect # of "undefering" those columns. - for col in sql_util.find_columns( + for col in sql_util._find_columns( self.parent_property.primaryjoin): if localparent.mapped_table.c.contains_column(col): if adapter: @@ -1197,13 +1295,12 @@ class JoinedLoader(AbstractRelationshipLoader): ) ) + def _create_eager_adapter(self, context, row, adapter, path, loadopt): + user_defined_adapter = self._init_user_defined_eager_proc( + loadopt, context) if loadopt else False - def _create_eager_adapter(self, context, row, adapter, path, reduced_path): - if ("user_defined_eager_row_processor", reduced_path) in \ - context.attributes: - decorator = context.attributes[ - ("user_defined_eager_row_processor", - reduced_path)] + if user_defined_adapter is not False: + decorator = user_defined_adapter # user defined eagerloads are part of the "primary" # portion of the load. # the adapters applied to the Query should be honored. @@ -1211,11 +1308,10 @@ class JoinedLoader(AbstractRelationshipLoader): decorator = decorator.wrap(context.adapter) elif context.adapter: decorator = context.adapter - elif ("eager_row_processor", reduced_path) in context.attributes: - decorator = context.attributes[ - ("eager_row_processor", reduced_path)] else: - return False + decorator = path.get(context.attributes, "eager_row_processor") + if decorator is None: + return False try: self.mapper.identity_key_from_row(row, decorator) @@ -1225,28 +1321,27 @@ class JoinedLoader(AbstractRelationshipLoader): # processor, will cause a degrade to lazy return False - def create_row_processor(self, context, path, reduced_path, mapper, row, adapter): + def create_row_processor(self, context, path, loadopt, mapper, row, adapter): if not self.parent.class_manager[self.key].impl.supports_population: raise sa_exc.InvalidRequestError( "'%s' does not support object " "population - eager loading cannot be applied." % self) - our_path = path + (self.key,) - our_reduced_path = reduced_path + (self.key,) + our_path = path[self.parent_property] eager_adapter = self._create_eager_adapter( context, row, - adapter, our_path, - our_reduced_path) + adapter, our_path, loadopt) if eager_adapter is not False: key = self.key - _instance = self.mapper._instance_processor( + + _instance = loading.instance_processor( + self.mapper, context, - our_path + (self.mapper,), - our_reduced_path + (self.mapper.base_mapper,), + our_path[self.mapper], eager_adapter) if not self.uselist: @@ -1255,10 +1350,9 @@ class JoinedLoader(AbstractRelationshipLoader): return self._create_collection_loader(context, key, _instance) else: return self.parent_property.\ - _get_strategy(LazyLoader).\ + _get_strategy_by_cls(LazyLoader).\ create_row_processor( - context, path, - reduced_path, + context, path, loadopt, mapper, row, adapter) def _create_collection_loader(self, context, key, _instance): @@ -1318,95 +1412,7 @@ class JoinedLoader(AbstractRelationshipLoader): load_scalar_from_joined_existing_row, \ None, load_scalar_from_joined_exec -EagerLoader = JoinedLoader -"""Deprecated, use JoinedLoader""" -log.class_logger(JoinedLoader) - -class EagerLazyOption(StrategizedOption): - def __init__(self, key, lazy=True, chained=False, - propagate_to_loaders=True - ): - if isinstance(key[0], basestring) and key[0] == '*': - if len(key) != 1: - raise sa_exc.ArgumentError( - "Wildcard identifier '*' must " - "be specified alone.") - key = ("relationship:*",) - propagate_to_loaders = False - super(EagerLazyOption, self).__init__(key) - self.lazy = lazy - self.chained = self.lazy in (False, 'joined', 'subquery') and chained - self.propagate_to_loaders = propagate_to_loaders - self.strategy_cls = factory(lazy) - - def get_strategy_class(self): - return self.strategy_cls - -def factory(identifier): - if identifier is False or identifier == 'joined': - return JoinedLoader - elif identifier is None or identifier == 'noload': - return NoLoader - elif identifier is False or identifier == 'select': - return LazyLoader - elif identifier == 'subquery': - return SubqueryLoader - elif identifier == 'immediate': - return ImmediateLoader - else: - return LazyLoader - -class EagerJoinOption(PropertyOption): - - def __init__(self, key, innerjoin, chained=False): - super(EagerJoinOption, self).__init__(key) - self.innerjoin = innerjoin - self.chained = chained - - def process_query_property(self, query, paths, mappers): - if self.chained: - for path in paths: - query._attributes[("eager_join_type", path)] = self.innerjoin - else: - query._attributes[("eager_join_type", paths[-1])] = self.innerjoin - -class LoadEagerFromAliasOption(PropertyOption): - - def __init__(self, key, alias=None, chained=False): - super(LoadEagerFromAliasOption, self).__init__(key) - if alias is not None: - if not isinstance(alias, basestring): - m, alias, is_aliased_class = mapperutil._entity_info(alias) - self.alias = alias - self.chained = chained - - def process_query_property(self, query, paths, mappers): - if self.chained: - for path in paths[0:-1]: - (root_mapper, propname) = path[-2:] - prop = root_mapper._props[propname] - adapter = query._polymorphic_adapters.get(prop.mapper, None) - query._attributes.setdefault( - ("user_defined_eager_row_processor", - interfaces._reduce_path(path)), adapter) - - if self.alias is not None: - if isinstance(self.alias, basestring): - (root_mapper, propname) = paths[-1][-2:] - prop = root_mapper._props[propname] - self.alias = prop.target.alias(self.alias) - query._attributes[ - ("user_defined_eager_row_processor", - interfaces._reduce_path(paths[-1])) - ] = sql_util.ColumnAdapter(self.alias) - else: - (root_mapper, propname) = paths[-1][-2:] - prop = root_mapper._props[propname] - adapter = query._polymorphic_adapters.get(prop.mapper, None) - query._attributes[ - ("user_defined_eager_row_processor", - interfaces._reduce_path(paths[-1]))] = adapter def single_parent_validator(desc, prop): def _do_check(state, value, oldvalue, initiator): @@ -1417,7 +1423,7 @@ def single_parent_validator(desc, prop): "Instance %s is already associated with an instance " "of %s via its %s attribute, and is only allowed a " "single parent." % - (mapperutil.instance_str(value), state.class_, prop) + (orm_util.instance_str(value), state.class_, prop) ) return value @@ -1427,6 +1433,7 @@ def single_parent_validator(desc, prop): def set_(state, value, oldvalue, initiator): return _do_check(state, value, oldvalue, initiator) - event.listen(desc, 'append', append, raw=True, retval=True, active_history=True) - event.listen(desc, 'set', set_, raw=True, retval=True, active_history=True) - + event.listen(desc, 'append', append, raw=True, retval=True, + active_history=True) + event.listen(desc, 'set', set_, raw=True, retval=True, + active_history=True) diff --git a/libs/sqlalchemy/orm/strategy_options.py b/libs/sqlalchemy/orm/strategy_options.py new file mode 100644 index 00000000..6e838ccb --- /dev/null +++ b/libs/sqlalchemy/orm/strategy_options.py @@ -0,0 +1,924 @@ +# orm/strategy_options.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +""" + +""" + +from .interfaces import MapperOption, PropComparator +from .. import util +from ..sql.base import _generative, Generative +from .. import exc as sa_exc, inspect +from .base import _is_aliased_class, _class_to_mapper +from . import util as orm_util +from .path_registry import PathRegistry, TokenRegistry, \ + _WILDCARD_TOKEN, _DEFAULT_TOKEN + +class Load(Generative, MapperOption): + """Represents loader options which modify the state of a + :class:`.Query` in order to affect how various mapped attributes are loaded. + + .. versionadded:: 0.9.0 The :meth:`.Load` system is a new foundation for + the existing system of loader options, including options such as + :func:`.orm.joinedload`, :func:`.orm.defer`, and others. In particular, + it introduces a new method-chained system that replaces the need for + dot-separated paths as well as "_all()" options such as :func:`.orm.joinedload_all`. + + A :class:`.Load` object can be used directly or indirectly. To use one + directly, instantiate given the parent class. This style of usage is + useful when dealing with a :class:`.Query` that has multiple entities, + or when producing a loader option that can be applied generically to + any style of query:: + + myopt = Load(MyClass).joinedload("widgets") + + The above ``myopt`` can now be used with :meth:`.Query.options`:: + + session.query(MyClass).options(myopt) + + The :class:`.Load` construct is invoked indirectly whenever one makes use + of the various loader options that are present in ``sqlalchemy.orm``, including + options such as :func:`.orm.joinedload`, :func:`.orm.defer`, :func:`.orm.subqueryload`, + and all the rest. These constructs produce an "anonymous" form of the + :class:`.Load` object which tracks attributes and options, but is not linked + to a parent class until it is associated with a parent :class:`.Query`:: + + # produce "unbound" Load object + myopt = joinedload("widgets") + + # when applied using options(), the option is "bound" to the + # class observed in the given query, e.g. MyClass + session.query(MyClass).options(myopt) + + Whether the direct or indirect style is used, the :class:`.Load` object + returned now represents a specific "path" along the entities of a :class:`.Query`. + This path can be traversed using a standard method-chaining approach. + Supposing a class hierarchy such as ``User``, ``User.addresses -> Address``, + ``User.orders -> Order`` and ``Order.items -> Item``, we can specify a variety + of loader options along each element in the "path":: + + session.query(User).options( + joinedload("addresses"), + subqueryload("orders").joinedload("items") + ) + + Where above, the ``addresses`` collection will be joined-loaded, the + ``orders`` collection will be subquery-loaded, and within that subquery load + the ``items`` collection will be joined-loaded. + + + """ + def __init__(self, entity): + insp = inspect(entity) + self.path = insp._path_registry + self.context = {} + self.local_opts = {} + + def _generate(self): + cloned = super(Load, self)._generate() + cloned.local_opts = {} + return cloned + + strategy = None + propagate_to_loaders = False + + def process_query(self, query): + self._process(query, True) + + def process_query_conditionally(self, query): + self._process(query, False) + + def _process(self, query, raiseerr): + current_path = query._current_path + if current_path: + for (token, start_path), loader in self.context.items(): + chopped_start_path = self._chop_path(start_path, current_path) + if chopped_start_path is not None: + query._attributes[(token, chopped_start_path)] = loader + else: + query._attributes.update(self.context) + + def _generate_path(self, path, attr, wildcard_key, raiseerr=True): + if raiseerr and not path.has_entity: + if isinstance(path, TokenRegistry): + raise sa_exc.ArgumentError( + "Wildcard token cannot be followed by another entity") + else: + raise sa_exc.ArgumentError( + "Attribute '%s' of entity '%s' does not " + "refer to a mapped entity" % + (path.prop.key, path.parent.entity) + ) + + if isinstance(attr, util.string_types): + default_token = attr.endswith(_DEFAULT_TOKEN) + if attr.endswith(_WILDCARD_TOKEN) or default_token: + if default_token: + self.propagate_to_loaders = False + if wildcard_key: + attr = "%s:%s" % (wildcard_key, attr) + return path.token(attr) + + try: + # use getattr on the class to work around + # synonyms, hybrids, etc. + attr = getattr(path.entity.class_, attr) + except AttributeError: + if raiseerr: + raise sa_exc.ArgumentError( + "Can't find property named '%s' on the " + "mapped entity %s in this Query. " % ( + attr, path.entity) + ) + else: + return None + else: + attr = attr.property + + path = path[attr] + else: + prop = attr.property + + if not prop.parent.common_parent(path.mapper): + if raiseerr: + raise sa_exc.ArgumentError("Attribute '%s' does not " + "link from element '%s'" % (attr, path.entity)) + else: + return None + + if getattr(attr, '_of_type', None): + ac = attr._of_type + ext_info = inspect(ac) + + path_element = ext_info.mapper + if not ext_info.is_aliased_class: + ac = orm_util.with_polymorphic( + ext_info.mapper.base_mapper, + ext_info.mapper, aliased=True, + _use_mapper_path=True) + path.entity_path[prop].set(self.context, + "path_with_polymorphic", inspect(ac)) + path = path[prop][path_element] + else: + path = path[prop] + + if path.has_entity: + path = path.entity_path + return path + + def _coerce_strat(self, strategy): + if strategy is not None: + strategy = tuple(sorted(strategy.items())) + return strategy + + @_generative + def set_relationship_strategy(self, attr, strategy, propagate_to_loaders=True): + strategy = self._coerce_strat(strategy) + + self.propagate_to_loaders = propagate_to_loaders + # if the path is a wildcard, this will set propagate_to_loaders=False + self.path = self._generate_path(self.path, attr, "relationship") + self.strategy = strategy + if strategy is not None: + self._set_path_strategy() + + @_generative + def set_column_strategy(self, attrs, strategy, opts=None): + strategy = self._coerce_strat(strategy) + + for attr in attrs: + path = self._generate_path(self.path, attr, "column") + cloned = self._generate() + cloned.strategy = strategy + cloned.path = path + cloned.propagate_to_loaders = True + if opts: + cloned.local_opts.update(opts) + cloned._set_path_strategy() + + def _set_path_strategy(self): + if self.path.has_entity: + self.path.parent.set(self.context, "loader", self) + else: + self.path.set(self.context, "loader", self) + + def __getstate__(self): + d = self.__dict__.copy() + d["path"] = self.path.serialize() + return d + + def __setstate__(self, state): + self.__dict__.update(state) + self.path = PathRegistry.deserialize(self.path) + + def _chop_path(self, to_chop, path): + i = -1 + + for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)): + if isinstance(c_token, util.string_types): + # TODO: this is approximated from the _UnboundLoad + # version and probably has issues, not fully covered. + + if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): + return to_chop + elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_token.key: + return None + + if c_token is p_token: + continue + else: + return None + return to_chop[i+1:] + + +class _UnboundLoad(Load): + """Represent a loader option that isn't tied to a root entity. + + The loader option will produce an entity-linked :class:`.Load` + object when it is passed :meth:`.Query.options`. + + This provides compatibility with the traditional system + of freestanding options, e.g. ``joinedload('x.y.z')``. + + """ + def __init__(self): + self.path = () + self._to_bind = set() + self.local_opts = {} + + _is_chain_link = False + + def _set_path_strategy(self): + self._to_bind.add(self) + + def _generate_path(self, path, attr, wildcard_key): + if wildcard_key and isinstance(attr, util.string_types) and \ + attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN): + if attr == _DEFAULT_TOKEN: + self.propagate_to_loaders = False + attr = "%s:%s" % (wildcard_key, attr) + + return path + (attr, ) + + def __getstate__(self): + d = self.__dict__.copy() + d['path'] = ret = [] + for token in util.to_list(self.path): + if isinstance(token, PropComparator): + ret.append((token._parentmapper.class_, token.key)) + else: + ret.append(token) + return d + + def __setstate__(self, state): + ret = [] + for key in state['path']: + if isinstance(key, tuple): + cls, propkey = key + ret.append(getattr(cls, propkey)) + else: + ret.append(key) + state['path'] = tuple(ret) + self.__dict__ = state + + def _process(self, query, raiseerr): + for val in self._to_bind: + val._bind_loader(query, query._attributes, raiseerr) + + @classmethod + def _from_keys(self, meth, keys, chained, kw): + opt = _UnboundLoad() + + def _split_key(key): + if isinstance(key, util.string_types): + # coerce fooload('*') into "default loader strategy" + if key == _WILDCARD_TOKEN: + return (_DEFAULT_TOKEN, ) + # coerce fooload(".*") into "wildcard on default entity" + elif key.startswith("." + _WILDCARD_TOKEN): + key = key[1:] + return key.split(".") + else: + return (key,) + all_tokens = [token for key in keys for token in _split_key(key)] + + for token in all_tokens[0:-1]: + if chained: + opt = meth(opt, token, **kw) + else: + opt = opt.defaultload(token) + opt._is_chain_link = True + + opt = meth(opt, all_tokens[-1], **kw) + opt._is_chain_link = False + + return opt + + + def _chop_path(self, to_chop, path): + i = -1 + for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())): + if isinstance(c_token, util.string_types): + if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): + return to_chop + elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key: + return None + elif isinstance(c_token, PropComparator): + if c_token.property is not p_prop: + return None + else: + i += 1 + + return to_chop[i:] + + + def _bind_loader(self, query, context, raiseerr): + start_path = self.path + # _current_path implies we're in a + # secondary load with an existing path + + current_path = query._current_path + if current_path: + start_path = self._chop_path(start_path, current_path) + + if not start_path: + return None + + token = start_path[0] + if isinstance(token, util.string_types): + entity = self._find_entity_basestring(query, token, raiseerr) + elif isinstance(token, PropComparator): + prop = token.property + entity = self._find_entity_prop_comparator( + query, + prop.key, + token._parententity, + raiseerr) + + else: + raise sa_exc.ArgumentError( + "mapper option expects " + "string key or list of attributes") + + if not entity: + return + + path_element = entity.entity_zero + + # transfer our entity-less state into a Load() object + # with a real entity path. + loader = Load(path_element) + loader.context = context + loader.strategy = self.strategy + + path = loader.path + for token in start_path: + loader.path = path = loader._generate_path( + loader.path, token, None, raiseerr) + if path is None: + return + + loader.local_opts.update(self.local_opts) + + if loader.path.has_entity: + effective_path = loader.path.parent + else: + effective_path = loader.path + + # prioritize "first class" options over those + # that were "links in the chain", e.g. "x" and "y" in someload("x.y.z") + # versus someload("x") / someload("x.y") + if self._is_chain_link: + effective_path.setdefault(context, "loader", loader) + else: + effective_path.set(context, "loader", loader) + + def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): + if _is_aliased_class(mapper): + searchfor = mapper + else: + searchfor = _class_to_mapper(mapper) + for ent in query._mapper_entities: + if ent.corresponds_to(searchfor): + return ent + else: + if raiseerr: + if not list(query._mapper_entities): + raise sa_exc.ArgumentError( + "Query has only expression-based entities - " + "can't find property named '%s'." + % (token, ) + ) + else: + raise sa_exc.ArgumentError( + "Can't find property '%s' on any entity " + "specified in this Query. Note the full path " + "from root (%s) to target entity must be specified." + % (token, ",".join(str(x) for + x in query._mapper_entities)) + ) + else: + return None + + def _find_entity_basestring(self, query, token, raiseerr): + if token.endswith(':' + _WILDCARD_TOKEN): + if len(list(query._mapper_entities)) != 1: + if raiseerr: + raise sa_exc.ArgumentError( + "Wildcard loader can only be used with exactly " + "one entity. Use Load(ent) to specify " + "specific entities.") + + for ent in query._mapper_entities: + # return only the first _MapperEntity when searching + # based on string prop name. Ideally object + # attributes are used to specify more exactly. + return ent + else: + if raiseerr: + raise sa_exc.ArgumentError( + "Query has only expression-based entities - " + "can't find property named '%s'." + % (token, ) + ) + else: + return None + + + +class loader_option(object): + def __init__(self): + pass + + def __call__(self, fn): + self.name = name = fn.__name__ + self.fn = fn + if hasattr(Load, name): + raise TypeError("Load class already has a %s method." % (name)) + setattr(Load, name, fn) + + return self + + def _add_unbound_fn(self, fn): + self._unbound_fn = fn + fn_doc = self.fn.__doc__ + self.fn.__doc__ = """Produce a new :class:`.Load` object with the +:func:`.orm.%(name)s` option applied. + +See :func:`.orm.%(name)s` for usage examples. + +""" % {"name": self.name} + + fn.__doc__ = fn_doc + return self + + def _add_unbound_all_fn(self, fn): + self._unbound_all_fn = fn + fn.__doc__ = """Produce a standalone "all" option for :func:`.orm.%(name)s`. + +.. deprecated:: 0.9.0 + + The "_all()" style is replaced by method chaining, e.g.:: + + session.query(MyClass).options( + %(name)s("someattribute").%(name)s("anotherattribute") + ) + +""" % {"name": self.name} + return self + +@loader_option() +def contains_eager(loadopt, attr, alias=None): + """Indicate that the given attribute should be eagerly loaded from + columns stated manually in the query. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + The option is used in conjunction with an explicit join that loads + the desired rows, i.e.:: + + sess.query(Order).\\ + join(Order.user).\\ + options(contains_eager(Order.user)) + + The above query would join from the ``Order`` entity to its related + ``User`` entity, and the returned ``Order`` objects would have the + ``Order.user`` attribute pre-populated. + + :func:`contains_eager` also accepts an `alias` argument, which is the + string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` + construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when + the eagerly-loaded rows are to come from an aliased table:: + + user_alias = aliased(User) + sess.query(Order).\\ + join((user_alias, Order.user)).\\ + options(contains_eager(Order.user, alias=user_alias)) + + .. seealso:: + + :ref:`contains_eager` + + """ + if alias is not None: + if not isinstance(alias, str): + info = inspect(alias) + alias = info.selectable + + cloned = loadopt.set_relationship_strategy( + attr, + {"lazy": "joined"}, + propagate_to_loaders=False + ) + cloned.local_opts['eager_from_alias'] = alias + return cloned + +@contains_eager._add_unbound_fn +def contains_eager(*keys, **kw): + return _UnboundLoad()._from_keys(_UnboundLoad.contains_eager, keys, True, kw) + +@loader_option() +def load_only(loadopt, *attrs): + """Indicate that for a particular entity, only the given list + of column-based attribute names should be loaded; all others will be + deferred. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + Example - given a class ``User``, load only the ``name`` and ``fullname`` + attributes:: + + session.query(User).options(load_only("name", "fullname")) + + Example - given a relationship ``User.addresses -> Address``, specify + subquery loading for the ``User.addresses`` collection, but on each ``Address`` + object load only the ``email_address`` attribute:: + + session.query(User).options( + subqueryload("addreses").load_only("email_address") + ) + + For a :class:`.Query` that has multiple entities, the lead entity can be + specifically referred to using the :class:`.Load` constructor:: + + session.query(User, Address).join(User.addresses).options( + Load(User).load_only("name", "fullname"), + Load(Address).load_only("email_addres") + ) + + + .. versionadded:: 0.9.0 + + """ + cloned = loadopt.set_column_strategy( + attrs, + {"deferred": False, "instrument": True} + ) + cloned.set_column_strategy("*", + {"deferred": True, "instrument": True}) + return cloned + +@load_only._add_unbound_fn +def load_only(*attrs): + return _UnboundLoad().load_only(*attrs) + +@loader_option() +def joinedload(loadopt, attr, innerjoin=None): + """Indicate that the given attribute should be loaded using joined + eager loading. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + examples:: + + # joined-load the "orders" collection on "User" + query(User).options(joinedload(User.orders)) + + # joined-load Order.items and then Item.keywords + query(Order).options(joinedload(Order.items).joinedload(Item.keywords)) + + # lazily load Order.items, but when Items are loaded, + # joined-load the keywords collection + query(Order).options(lazyload(Order.items).joinedload(Item.keywords)) + + :func:`.orm.joinedload` also accepts a keyword argument `innerjoin=True` which + indicates using an inner join instead of an outer:: + + query(Order).options(joinedload(Order.user, innerjoin=True)) + + .. note:: + + The joins produced by :func:`.orm.joinedload` are **anonymously aliased**. + The criteria by which the join proceeds cannot be modified, nor can the + :class:`.Query` refer to these joins in any way, including ordering. + + To produce a specific SQL JOIN which is explicitly available, use + :meth:`.Query.join`. To combine explicit JOINs with eager loading + of collections, use :func:`.orm.contains_eager`; see :ref:`contains_eager`. + + .. seealso:: + + :ref:`loading_toplevel` + + :ref:`contains_eager` + + :func:`.orm.subqueryload` + + :func:`.orm.lazyload` + + """ + loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"}) + if innerjoin is not None: + loader.local_opts['innerjoin'] = innerjoin + return loader + +@joinedload._add_unbound_fn +def joinedload(*keys, **kw): + return _UnboundLoad._from_keys( + _UnboundLoad.joinedload, keys, False, kw) + +@joinedload._add_unbound_all_fn +def joinedload_all(*keys, **kw): + return _UnboundLoad._from_keys( + _UnboundLoad.joinedload, keys, True, kw) + + +@loader_option() +def subqueryload(loadopt, attr): + """Indicate that the given attribute should be loaded using + subquery eager loading. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + examples:: + + # subquery-load the "orders" collection on "User" + query(User).options(subqueryload(User.orders)) + + # subquery-load Order.items and then Item.keywords + query(Order).options(subqueryload(Order.items).subqueryload(Item.keywords)) + + # lazily load Order.items, but when Items are loaded, + # subquery-load the keywords collection + query(Order).options(lazyload(Order.items).subqueryload(Item.keywords)) + + + .. seealso:: + + :ref:`loading_toplevel` + + :func:`.orm.joinedload` + + :func:`.orm.lazyload` + + """ + return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"}) + +@subqueryload._add_unbound_fn +def subqueryload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {}) + +@subqueryload._add_unbound_all_fn +def subqueryload_all(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, True, {}) + +@loader_option() +def lazyload(loadopt, attr): + """Indicate that the given attribute should be loaded using "lazy" + loading. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + """ + return loadopt.set_relationship_strategy(attr, {"lazy": "select"}) + +@lazyload._add_unbound_fn +def lazyload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {}) + +@lazyload._add_unbound_all_fn +def lazyload_all(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, True, {}) + +@loader_option() +def immediateload(loadopt, attr): + """Indicate that the given attribute should be loaded using + an immediate load with a per-attribute SELECT statement. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + .. seealso:: + + :ref:`loading_toplevel` + + :func:`.orm.joinedload` + + :func:`.orm.lazyload` + + """ + loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"}) + return loader + +@immediateload._add_unbound_fn +def immediateload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {}) + + +@loader_option() +def noload(loadopt, attr): + """Indicate that the given relationship attribute should remain unloaded. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + :func:`.orm.noload` applies to :func:`.relationship` attributes; for + column-based attributes, see :func:`.orm.defer`. + + """ + + return loadopt.set_relationship_strategy(attr, {"lazy": "noload"}) + +@noload._add_unbound_fn +def noload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {}) + +@loader_option() +def defaultload(loadopt, attr): + """Indicate an attribute should load using its default loader style. + + This method is used to link to other loader options, such as + to set the :func:`.orm.defer` option on a class that is linked to + a relationship of the parent class being loaded, :func:`.orm.defaultload` + can be used to navigate this path without changing the loading style + of the relationship:: + + session.query(MyClass).options(defaultload("someattr").defer("some_column")) + + .. seealso:: + + :func:`.orm.defer` + + :func:`.orm.undefer` + + """ + return loadopt.set_relationship_strategy( + attr, + None + ) + +@defaultload._add_unbound_fn +def defaultload(*keys): + return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) + +@loader_option() +def defer(loadopt, key): + """Indicate that the given column-oriented attribute should be deferred, e.g. + not loaded until accessed. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + e.g.:: + + from sqlalchemy.orm import defer + + session.query(MyClass).options( + defer("attribute_one"), + defer("attribute_two")) + + session.query(MyClass).options( + defer(MyClass.attribute_one), + defer(MyClass.attribute_two)) + + To specify a deferred load of an attribute on a related class, + the path can be specified one token at a time, specifying the loading + style for each link along the chain. To leave the loading style + for a link unchanged, use :func:`.orm.defaultload`:: + + session.query(MyClass).options(defaultload("someattr").defer("some_column")) + + A :class:`.Load` object that is present on a certain path can have + :meth:`.Load.defer` called multiple times, each will operate on the same + parent entity:: + + + session.query(MyClass).options( + defaultload("someattr"). + defer("some_column"). + defer("some_other_column"). + defer("another_column") + ) + + :param key: Attribute to be deferred. + + :param \*addl_attrs: Deprecated; this option supports the old 0.8 style + of specifying a path as a series of attributes, which is now superseded + by the method-chained style. + + .. seealso:: + + :ref:`deferred` + + :func:`.orm.undefer` + + """ + return loadopt.set_column_strategy( + (key, ), + {"deferred": True, "instrument": True} + ) + + +@defer._add_unbound_fn +def defer(key, *addl_attrs): + return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {}) + +@loader_option() +def undefer(loadopt, key): + """Indicate that the given column-oriented attribute should be undeferred, e.g. + specified within the SELECT statement of the entity as a whole. + + The column being undeferred is typically set up on the mapping as a + :func:`.deferred` attribute. + + This function is part of the :class:`.Load` interface and supports + both method-chained and standalone operation. + + Examples:: + + # undefer two columns + session.query(MyClass).options(undefer("col1"), undefer("col2")) + + # undefer all columns specific to a single class using Load + * + session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*")) + + :param key: Attribute to be undeferred. + + :param \*addl_attrs: Deprecated; this option supports the old 0.8 style + of specifying a path as a series of attributes, which is now superseded + by the method-chained style. + + .. seealso:: + + :ref:`deferred` + + :func:`.orm.defer` + + :func:`.orm.undefer_group` + + """ + return loadopt.set_column_strategy( + (key, ), + {"deferred": False, "instrument": True} + ) + +@undefer._add_unbound_fn +def undefer(key, *addl_attrs): + return _UnboundLoad._from_keys(_UnboundLoad.undefer, (key, ) + addl_attrs, False, {}) + +@loader_option() +def undefer_group(loadopt, name): + """Indicate that columns within the given deferred group name should be undeferred. + + The columns being undeferred are set up on the mapping as + :func:`.deferred` attributes and include a "group" name. + + E.g:: + + session.query(MyClass).options(undefer_group("large_attrs")) + + To undefer a group of attributes on a related entity, the path can be + spelled out using relationship loader options, such as :func:`.orm.defaultload`:: + + session.query(MyClass).options(defaultload("someattr").undefer_group("large_attrs")) + + .. versionchanged:: 0.9.0 :func:`.orm.undefer_group` is now specific to a + particiular entity load path. + + .. seealso:: + + :ref:`deferred` + + :func:`.orm.defer` + + :func:`.orm.undefer` + + """ + return loadopt.set_column_strategy( + "*", + None, + {"undefer_group": name} + ) + +@undefer_group._add_unbound_fn +def undefer_group(name): + return _UnboundLoad().undefer_group(name) + diff --git a/libs/sqlalchemy/orm/sync.py b/libs/sqlalchemy/orm/sync.py index 3094386b..cf735fc5 100644 --- a/libs/sqlalchemy/orm/sync.py +++ b/libs/sqlalchemy/orm/sync.py @@ -1,5 +1,5 @@ # orm/sync.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -9,7 +9,8 @@ between instances based on join conditions. """ -from sqlalchemy.orm import exc, util as mapperutil, attributes +from . import exc, util as orm_util, attributes + def populate(source, source_mapper, dest, dest_mapper, synchronize_pairs, uowcommit, flag_cascaded_pks): @@ -42,38 +43,45 @@ def populate(source, source_mapper, dest, dest_mapper, r.references(l): uowcommit.attributes[("pk_cascaded", dest, r)] = True + def clear(dest, dest_mapper, synchronize_pairs): for l, r in synchronize_pairs: if r.primary_key: raise AssertionError( - "Dependency rule tried to blank-out primary key " - "column '%s' on instance '%s'" % - (r, mapperutil.state_str(dest)) - ) + "Dependency rule tried to blank-out primary key " + "column '%s' on instance '%s'" % + (r, orm_util.state_str(dest)) + ) try: dest_mapper._set_state_attr_by_column(dest, dest.dict, r, None) except exc.UnmappedColumnError: _raise_col_to_prop(True, None, l, dest_mapper, r) + def update(source, source_mapper, dest, old_prefix, synchronize_pairs): for l, r in synchronize_pairs: try: - oldvalue = source_mapper._get_committed_attr_by_column(source.obj(), l) - value = source_mapper._get_state_attr_by_column(source, source.dict, l) + oldvalue = source_mapper._get_committed_attr_by_column( + source.obj(), l) + value = source_mapper._get_state_attr_by_column( + source, source.dict, l) except exc.UnmappedColumnError: _raise_col_to_prop(False, source_mapper, l, None, r) dest[r.key] = value dest[old_prefix + r.key] = oldvalue + def populate_dict(source, source_mapper, dict_, synchronize_pairs): for l, r in synchronize_pairs: try: - value = source_mapper._get_state_attr_by_column(source, source.dict, l) + value = source_mapper._get_state_attr_by_column( + source, source.dict, l) except exc.UnmappedColumnError: _raise_col_to_prop(False, source_mapper, l, None, r) dict_[r.key] = value + def source_modified(uowcommit, source, source_mapper, synchronize_pairs): """return true if the source object has changes from an old to a new value on the given synchronize pairs @@ -86,23 +94,25 @@ def source_modified(uowcommit, source, source_mapper, synchronize_pairs): _raise_col_to_prop(False, source_mapper, l, None, r) history = uowcommit.get_attribute_history(source, prop.key, attributes.PASSIVE_NO_INITIALIZE) - return bool(history.deleted) + if bool(history.deleted): + return True else: return False -def _raise_col_to_prop(isdest, source_mapper, source_column, dest_mapper, dest_column): + +def _raise_col_to_prop(isdest, source_mapper, source_column, + dest_mapper, dest_column): if isdest: - raise exc.UnmappedColumnError( - "Can't execute sync rule for destination column '%s'; " - "mapper '%s' does not map this column. Try using an explicit" - " `foreign_keys` collection which does not include this column " - "(or use a viewonly=True relation)." % (dest_column, dest_mapper) - ) + raise exc.UnmappedColumnError("Can't execute sync rule for " + "destination column '%s'; mapper '%s' does not map " + "this column. Try using an explicit `foreign_keys` " + "collection which does not include this column (or use " + "a viewonly=True relation)." % (dest_column, + dest_mapper)) else: - raise exc.UnmappedColumnError( - "Can't execute sync rule for source column '%s'; mapper '%s' " - "does not map this column. Try using an explicit `foreign_keys`" - " collection which does not include destination column '%s' (or " - "use a viewonly=True relation)." % - (source_column, source_mapper, dest_column) - ) + raise exc.UnmappedColumnError("Can't execute sync rule for " + "source column '%s'; mapper '%s' does not map this " + "column. Try using an explicit `foreign_keys` " + "collection which does not include destination column " + "'%s' (or use a viewonly=True relation)." + % (source_column, source_mapper, dest_column)) diff --git a/libs/sqlalchemy/orm/unitofwork.py b/libs/sqlalchemy/orm/unitofwork.py index 003d7ae7..2964705a 100644 --- a/libs/sqlalchemy/orm/unitofwork.py +++ b/libs/sqlalchemy/orm/unitofwork.py @@ -1,5 +1,5 @@ # orm/unitofwork.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -12,11 +12,10 @@ organizes them in order of dependency, and executes. """ -from sqlalchemy import util, event -from sqlalchemy.util import topological -from sqlalchemy.orm import attributes, interfaces, persistence -from sqlalchemy.orm import util as mapperutil -session = util.importlater("sqlalchemy.orm", "session") +from .. import util, event +from ..util import topological +from . import attributes, persistence, util as orm_util + def track_cascade_events(descriptor, prop): """Establish event listeners on object attributes which handle @@ -29,25 +28,42 @@ def track_cascade_events(descriptor, prop): # process "save_update" cascade rules for when # an instance is appended to the list of another instance - sess = session._state_session(state) + if item is None: + return + + sess = state.session if sess: + if sess._warn_on_events: + sess._flush_warning("collection append") + prop = state.manager.mapper._props[key] item_state = attributes.instance_state(item) - if prop.cascade.save_update and \ + if prop._cascade.save_update and \ (prop.cascade_backrefs or key == initiator.key) and \ - not sess._contains_state(item_state): + not sess._contains_state(item_state): sess._save_or_update_state(item_state) return item def remove(state, item, initiator): - sess = session._state_session(state) + if item is None: + return + + sess = state.session if sess: + prop = state.manager.mapper._props[key] + + if sess._warn_on_events: + sess._flush_warning( + "collection remove" + if prop.uselist + else "related attribute delete") + # expunge pending orphans item_state = attributes.instance_state(item) - if prop.cascade.delete_orphan and \ + if prop._cascade.delete_orphan and \ item_state in sess._new and \ - prop.mapper._is_orphan(item_state): + prop.mapper._is_orphan(item_state): sess.expunge(item) def set_(state, newvalue, oldvalue, initiator): @@ -56,19 +72,23 @@ def track_cascade_events(descriptor, prop): if oldvalue is newvalue: return newvalue - sess = session._state_session(state) + sess = state.session if sess: + + if sess._warn_on_events: + sess._flush_warning("related attribute set") + prop = state.manager.mapper._props[key] if newvalue is not None: newvalue_state = attributes.instance_state(newvalue) - if prop.cascade.save_update and \ + if prop._cascade.save_update and \ (prop.cascade_backrefs or key == initiator.key) and \ not sess._contains_state(newvalue_state): sess._save_or_update_state(newvalue_state) if oldvalue is not None and \ oldvalue is not attributes.PASSIVE_NO_RESULT and \ - prop.cascade.delete_orphan: + prop._cascade.delete_orphan: # possible to reach here with attributes.NEVER_SET ? oldvalue_state = attributes.instance_state(oldvalue) @@ -154,7 +174,8 @@ class UOWTransaction(object): def get_attribute_history(self, state, key, passive=attributes.PASSIVE_NO_INITIALIZE): - """facade to attributes.get_state_history(), including caching of results.""" + """facade to attributes.get_state_history(), including + caching of results.""" hashkey = ("history", state, key) @@ -166,11 +187,13 @@ class UOWTransaction(object): history, state_history, cached_passive = self.attributes[hashkey] # if the cached lookup was "passive" and now # we want non-passive, do a non-passive lookup and re-cache - if cached_passive is not attributes.PASSIVE_OFF \ - and passive is attributes.PASSIVE_OFF: + + if not cached_passive & attributes.SQL_OK \ + and passive & attributes.SQL_OK: impl = state.manager[key].impl history = impl.get_history(state, state.dict, - attributes.PASSIVE_OFF) + attributes.PASSIVE_OFF | + attributes.LOAD_AGAINST_COMMITTED) if history and impl.uses_objects: state_history = history.as_state() else: @@ -180,12 +203,14 @@ class UOWTransaction(object): impl = state.manager[key].impl # TODO: store the history as (state, object) tuples # so we don't have to keep converting here - history = impl.get_history(state, state.dict, passive) + history = impl.get_history(state, state.dict, passive | + attributes.LOAD_AGAINST_COMMITTED) if history and impl.uses_objects: state_history = history.as_state() else: state_history = history - self.attributes[hashkey] = (history, state_history, passive) + self.attributes[hashkey] = (history, state_history, + passive) return state_history @@ -204,14 +229,14 @@ class UOWTransaction(object): if not state.deleted and operation is not None: util.warn("Object of type %s not in session, %s operation " "along '%s' will not proceed" % - (mapperutil.state_class_str(state), operation, prop)) + (orm_util.state_class_str(state), operation, prop)) return False if state not in self.states: mapper = state.manager.mapper if mapper not in self.mappers: - mapper._per_mapper_flush_actions(self) + self._per_mapper_flush_actions(mapper) self.mappers[mapper].add(state) self.states[state] = (isdelete, listonly) @@ -226,6 +251,20 @@ class UOWTransaction(object): states.add(state) cols.update(post_update_cols) + def _per_mapper_flush_actions(self, mapper): + saves = SaveUpdateAll(self, mapper.base_mapper) + deletes = DeleteAll(self, mapper.base_mapper) + self.dependencies.add((saves, deletes)) + + for dep in mapper._dependency_processors: + dep.per_property_preprocessors(self) + + for prop in mapper.relationships: + if prop.viewonly: + continue + dep = prop._dependency_processor + dep.per_property_preprocessors(self) + @util.memoized_property def _mapper_for_dep(self): """return a dynamic mapping of (Mapper, DependencyProcessor) to @@ -237,7 +276,7 @@ class UOWTransaction(object): """ return util.PopulateDict( - lambda tup:tup[0]._props.get(tup[1].key) is tup[1].prop + lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop ) def filter_states_for_dep(self, dep, states): @@ -274,7 +313,7 @@ class UOWTransaction(object): # see if the graph of mapper dependencies has cycles. self.cycles = cycles = topological.find_cycles( self.dependencies, - self.postsort_actions.values()) + list(self.postsort_actions.values())) if cycles: # if yes, break the per-mapper actions into @@ -330,32 +369,36 @@ class UOWTransaction(object): postsort_actions): rec.execute(self) - def finalize_flush_changes(self): - """mark processed objects as clean / deleted after a successful flush(). + """mark processed objects as clean / deleted after a successful + flush(). this method is called within the flush() method after the execute() method has succeeded and the transaction has been committed. """ - for state, (isdelete, listonly) in self.states.iteritems(): - if isdelete: - self.session._remove_newly_deleted(state) - else: - # if listonly: - # debug... would like to see how many do this - self.session._register_newly_persistent(state) + states = set(self.states) + isdel = set( + s for (s, (isdelete, listonly)) in self.states.items() + if isdelete + ) + other = states.difference(isdel) + self.session._remove_newly_deleted(isdel) + self.session._register_newly_persistent(other) + class IterateMappersMixin(object): def _mappers(self, uow): if self.fromparent: return iter( - m for m in self.dependency_processor.parent.self_and_descendants + m for m in + self.dependency_processor.parent.self_and_descendants if uow._mapper_for_dep[(m, self.dependency_processor)] ) else: return self.dependency_processor.mapper.self_and_descendants + class Preprocess(IterateMappersMixin): def __init__(self, dependency_processor, fromparent): self.dependency_processor = dependency_processor @@ -396,6 +439,7 @@ class Preprocess(IterateMappersMixin): else: return False + class PostSortRec(object): disabled = False @@ -418,12 +462,14 @@ class PostSortRec(object): ",".join(str(x) for x in self.__dict__.values()) ) + class ProcessAll(IterateMappersMixin, PostSortRec): def __init__(self, uow, dependency_processor, delete, fromparent): self.dependency_processor = dependency_processor self.delete = delete self.fromparent = fromparent - uow.deps[dependency_processor.parent.base_mapper].add(dependency_processor) + uow.deps[dependency_processor.parent.base_mapper].\ + add(dependency_processor) def execute(self, uow): states = self._elements(uow) @@ -453,6 +499,7 @@ class ProcessAll(IterateMappersMixin, PostSortRec): if isdelete == self.delete and not listonly: yield state + class IssuePostUpdate(PostSortRec): def __init__(self, uow, mapper, isdelete): self.mapper = mapper @@ -464,6 +511,7 @@ class IssuePostUpdate(PostSortRec): persistence.post_update(self.mapper, states, uow, cols) + class SaveUpdateAll(PostSortRec): def __init__(self, uow, mapper): self.mapper = mapper @@ -476,17 +524,22 @@ class SaveUpdateAll(PostSortRec): ) def per_state_flush_actions(self, uow): - states = list(uow.states_for_mapper_hierarchy(self.mapper, False, False)) - for rec in self.mapper._per_state_flush_actions( - uow, - states, - False): - yield rec + states = list(uow.states_for_mapper_hierarchy( + self.mapper, False, False)) + base_mapper = self.mapper.base_mapper + delete_all = DeleteAll(uow, base_mapper) + for state in states: + # keep saves before deletes - + # this ensures 'row switch' operations work + action = SaveUpdateState(uow, state, base_mapper) + uow.dependencies.add((action, delete_all)) + yield action for dep in uow.deps[self.mapper]: states_for_prop = uow.filter_states_for_dep(dep, states) dep.per_state_flush_actions(uow, states_for_prop, False) + class DeleteAll(PostSortRec): def __init__(self, uow, mapper): self.mapper = mapper @@ -499,17 +552,22 @@ class DeleteAll(PostSortRec): ) def per_state_flush_actions(self, uow): - states = list(uow.states_for_mapper_hierarchy(self.mapper, True, False)) - for rec in self.mapper._per_state_flush_actions( - uow, - states, - True): - yield rec + states = list(uow.states_for_mapper_hierarchy( + self.mapper, True, False)) + base_mapper = self.mapper.base_mapper + save_all = SaveUpdateAll(uow, base_mapper) + for state in states: + # keep saves before deletes - + # this ensures 'row switch' operations work + action = DeleteState(uow, state, base_mapper) + uow.dependencies.add((save_all, action)) + yield action for dep in uow.deps[self.mapper]: states_for_prop = uow.filter_states_for_dep(dep, states) dep.per_state_flush_actions(uow, states_for_prop, True) + class ProcessState(PostSortRec): def __init__(self, uow, dependency_processor, delete, state): self.dependency_processor = dependency_processor @@ -535,10 +593,11 @@ class ProcessState(PostSortRec): return "%s(%s, %s, delete=%s)" % ( self.__class__.__name__, self.dependency_processor, - mapperutil.state_str(self.state), + orm_util.state_str(self.state), self.delete ) + class SaveUpdateState(PostSortRec): def __init__(self, uow, state, mapper): self.state = state @@ -559,9 +618,10 @@ class SaveUpdateState(PostSortRec): def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, - mapperutil.state_str(self.state) + orm_util.state_str(self.state) ) + class DeleteState(PostSortRec): def __init__(self, uow, state, mapper): self.state = state @@ -582,6 +642,5 @@ class DeleteState(PostSortRec): def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, - mapperutil.state_str(self.state) + orm_util.state_str(self.state) ) - diff --git a/libs/sqlalchemy/orm/util.py b/libs/sqlalchemy/orm/util.py index a8cc80ce..dd85f2ef 100644 --- a/libs/sqlalchemy/orm/util.py +++ b/libs/sqlalchemy/orm/util.py @@ -1,25 +1,26 @@ # orm/util.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import sql, util, event, exc as sa_exc -from sqlalchemy.sql import expression, util as sql_util, operators -from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\ - PropComparator, MapperProperty -from sqlalchemy.orm import attributes, exc -import operator +from .. import sql, util, event, exc as sa_exc, inspection +from ..sql import expression, util as sql_util, operators +from .interfaces import PropComparator, MapperProperty +from . import attributes import re -mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") +from .base import instance_str, state_str, state_class_str, attribute_str, \ + state_attribute_str, object_mapper, object_state, _none_set +from .base import class_mapper, _class_to_mapper +from .base import _InspectionAttr +from .path_registry import PathRegistry all_cascades = frozenset(("delete", "delete-orphan", "all", "merge", "expunge", "save-update", "refresh-expire", "none")) -_INSTRUMENTOR = ('mapper', 'instrumentor') class CascadeOptions(frozenset): """Keeps track of the options sent to relationship().cascade""" @@ -68,31 +69,53 @@ class CascadeOptions(frozenset): ",".join([x for x in sorted(self)]) ) -def _validator_events(desc, key, validator, include_removes): + +def _validator_events(desc, key, validator, include_removes, include_backrefs): """Runs a validation method on an attribute value to be set or appended.""" + if not include_backrefs: + def detect_is_backref(state, initiator): + impl = state.manager[key].impl + return initiator.impl is not impl + if include_removes: def append(state, value, initiator): - return validator(state.obj(), key, value, False) + if include_backrefs or not detect_is_backref(state, initiator): + return validator(state.obj(), key, value, False) + else: + return value def set_(state, value, oldvalue, initiator): - return validator(state.obj(), key, value, False) + if include_backrefs or not detect_is_backref(state, initiator): + return validator(state.obj(), key, value, False) + else: + return value def remove(state, value, initiator): - validator(state.obj(), key, value, True) + if include_backrefs or not detect_is_backref(state, initiator): + validator(state.obj(), key, value, True) + else: def append(state, value, initiator): - return validator(state.obj(), key, value) + if include_backrefs or not detect_is_backref(state, initiator): + return validator(state.obj(), key, value) + else: + return value def set_(state, value, oldvalue, initiator): - return validator(state.obj(), key, value) + if include_backrefs or not detect_is_backref(state, initiator): + return validator(state.obj(), key, value) + else: + return value event.listen(desc, 'append', append, raw=True, retval=True) event.listen(desc, 'set', set_, raw=True, retval=True) if include_removes: event.listen(desc, "remove", remove, raw=True, retval=True) -def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=True): + +def polymorphic_union(table_map, typecolname, + aliasname='p_union', cast_nulls=True): """Create a ``UNION`` statement used by a polymorphic mapper. See :ref:`concrete_inheritance` for an example of how @@ -101,20 +124,21 @@ def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=Tr :param table_map: mapping of polymorphic identities to :class:`.Table` objects. :param typecolname: string name of a "discriminator" column, which will be - derived from the query, producing the polymorphic identity for each row. If - ``None``, no polymorphic discriminator is generated. + derived from the query, producing the polymorphic identity for + each row. If ``None``, no polymorphic discriminator is generated. :param aliasname: name of the :func:`~sqlalchemy.sql.expression.alias()` construct generated. - :param cast_nulls: if True, non-existent columns, which are represented as labeled - NULLs, will be passed into CAST. This is a legacy behavior that is problematic - on some backends such as Oracle - in which case it can be set to False. + :param cast_nulls: if True, non-existent columns, which are represented + as labeled NULLs, will be passed into CAST. This is a legacy behavior + that is problematic on some backends such as Oracle - in which case it + can be set to False. """ colnames = util.OrderedSet() colnamemaps = {} types = {} - for key in table_map.keys(): + for key in table_map: table = table_map[key] # mysql doesnt like selecting from a select; @@ -140,7 +164,7 @@ def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=Tr return sql.type_coerce(sql.null(), types[name]).label(name) result = [] - for type, table in table_map.iteritems(): + for type, table in table_map.items(): if typecolname is not None: result.append( sql.select([col(name, table) for name in colnames] + @@ -152,32 +176,61 @@ def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=Tr from_obj=[table])) return sql.union_all(*result).alias(aliasname) -def identity_key(*args, **kwargs): - """Get an identity key. - Valid call signatures: +def identity_key(*args, **kwargs): + """Generate "identity key" tuples, as are used as keys in the + :attr:`.Session.identity_map` dictionary. + + This function has several call styles: * ``identity_key(class, ident)`` - class - mapped class (must be a positional argument) + This form receives a mapped class and a primary key scalar or + tuple as an argument. - ident - primary key, if the key is composite this is a tuple + E.g.:: + + >>> identity_key(MyClass, (1, 2)) + (, (1, 2)) + + :param class: mapped class (must be a positional argument) + :param ident: primary key, may be a scalar or tuple argument. * ``identity_key(instance=instance)`` - instance - object instance (must be given as a keyword arg) + This form will produce the identity key for a given instance. The + instance need not be persistent, only that its primary key attributes + are populated (else the key will contain ``None`` for those missing + values). + + E.g.:: + + >>> instance = MyClass(1, 2) + >>> identity_key(instance=instance) + (, (1, 2)) + + In this form, the given instance is ultimately run though + :meth:`.Mapper.identity_key_from_instance`, which will have the + effect of performing a database check for the corresponding row + if the object is expired. + + :param instance: object instance (must be given as a keyword arg) * ``identity_key(class, row=row)`` - class - mapped class (must be a positional argument) + This form is similar to the class/tuple form, except is passed a + database result row as a :class:`.RowProxy` object. - row - result proxy row (must be given as a keyword arg) + E.g.:: + + >>> row = engine.execute("select * from table where a=1 and b=2").first() + >>> identity_key(MyClass, row=row) + (, (1, 2)) + + :param class: mapped class (must be a positional argument) + :param row: :class:`.RowProxy` row returned by a :class:`.ResultProxy` + (must be given as a keyword arg) """ if args: @@ -196,7 +249,7 @@ def identity_key(*args, **kwargs): "positional arguments, got %s" % len(args)) if kwargs: raise sa_exc.ArgumentError("unknown keyword arguments: %s" - % ", ".join(kwargs.keys())) + % ", ".join(kwargs)) mapper = class_mapper(class_) if "ident" in locals(): return mapper.identity_key_from_primary_key(util.to_list(ident)) @@ -204,10 +257,11 @@ def identity_key(*args, **kwargs): instance = kwargs.pop("instance") if kwargs: raise sa_exc.ArgumentError("unknown keyword arguments: %s" - % ", ".join(kwargs.keys())) + % ", ".join(kwargs.keys)) mapper = object_mapper(instance) return mapper.identity_key_from_instance(instance) + class ORMAdapter(sql_util.ColumnAdapter): """Extends ColumnAdapter to accept ORM entities. @@ -215,9 +269,13 @@ class ORMAdapter(sql_util.ColumnAdapter): and the AliasedClass if any is referenced. """ - def __init__(self, entity, equivalents=None, - chain_to=None, adapt_required=False): - self.mapper, selectable, is_aliased_class = _entity_info(entity) + def __init__(self, entity, equivalents=None, adapt_required=False, + chain_to=None): + info = inspection.inspect(entity) + + self.mapper = info.mapper + selectable = info.selectable + is_aliased_class = info.is_aliased_class if is_aliased_class: self.aliased_class = entity else: @@ -241,7 +299,10 @@ class AliasedClass(object): __getattr__ scheme and maintains a reference to a real :class:`~sqlalchemy.sql.expression.Alias` object. - Usage is via the :class:`~sqlalchemy.orm.aliased()` synonym:: + Usage is via the :func:`.orm.aliased` function, or alternatively + via the :func:`.orm.with_polymorphic` function. + + Usage example:: # find all pairs of users with the same name user_alias = aliased(User) @@ -249,28 +310,286 @@ class AliasedClass(object): join((user_alias, User.id > user_alias.id)).\\ filter(User.name==user_alias.name) - The resulting object is an instance of :class:`.AliasedClass`, however - it implements a ``__getattribute__()`` scheme which will proxy attribute - access to that of the ORM class being aliased. All classmethods - on the mapped entity should also be available here, including - hybrids created with the :ref:`hybrids_toplevel` extension, - which will receive the :class:`.AliasedClass` as the "class" argument - when classmethods are called. + The resulting object is an instance of :class:`.AliasedClass`. + This object implements an attribute scheme which produces the + same attribute and method interface as the original mapped + class, allowing :class:`.AliasedClass` to be compatible + with any attribute technique which works on the original class, + including hybrid attributes (see :ref:`hybrids_toplevel`). + + The :class:`.AliasedClass` can be inspected for its underlying + :class:`.Mapper`, aliased selectable, and other information + using :func:`.inspect`:: + + from sqlalchemy import inspect + my_alias = aliased(MyClass) + insp = inspect(my_alias) + + The resulting inspection object is an instance of :class:`.AliasedInsp`. + + See :func:`.aliased` and :func:`.with_polymorphic` for construction + argument descriptions. + + """ + def __init__(self, cls, alias=None, + name=None, + flat=False, + adapt_on_names=False, + # TODO: None for default here? + with_polymorphic_mappers=(), + with_polymorphic_discriminator=None, + base_alias=None, + use_mapper_path=False): + mapper = _class_to_mapper(cls) + if alias is None: + alias = mapper._with_polymorphic_selectable.alias( + name=name, flat=flat) + self._aliased_insp = AliasedInsp( + self, + mapper, + alias, + name, + with_polymorphic_mappers + if with_polymorphic_mappers + else mapper.with_polymorphic_mappers, + with_polymorphic_discriminator + if with_polymorphic_discriminator is not None + else mapper.polymorphic_on, + base_alias, + use_mapper_path, + adapt_on_names + ) + + self.__name__ = 'AliasedClass_%s' % mapper.class_.__name__ + + def __getattr__(self, key): + try: + _aliased_insp = self.__dict__['_aliased_insp'] + except KeyError: + raise AttributeError() + else: + for base in _aliased_insp._target.__mro__: + try: + attr = object.__getattribute__(base, key) + except AttributeError: + continue + else: + break + else: + raise AttributeError(key) + + if isinstance(attr, PropComparator): + ret = attr.adapt_to_entity(_aliased_insp) + setattr(self, key, ret) + return ret + elif hasattr(attr, 'func_code'): + is_method = getattr(_aliased_insp._target, key, None) + if is_method and is_method.__self__ is not None: + return util.types.MethodType(attr.__func__, self, self) + else: + return None + elif hasattr(attr, '__get__'): + ret = attr.__get__(None, self) + if isinstance(ret, PropComparator): + return ret.adapt_to_entity(_aliased_insp) + else: + return ret + else: + return attr + + def __repr__(self): + return '' % ( + id(self), self._aliased_insp._target.__name__) + + +class AliasedInsp(_InspectionAttr): + """Provide an inspection interface for an + :class:`.AliasedClass` object. + + The :class:`.AliasedInsp` object is returned + given an :class:`.AliasedClass` using the + :func:`.inspect` function:: + + from sqlalchemy import inspect + from sqlalchemy.orm import aliased + + my_alias = aliased(MyMappedClass) + insp = inspect(my_alias) + + Attributes on :class:`.AliasedInsp` + include: + + * ``entity`` - the :class:`.AliasedClass` represented. + * ``mapper`` - the :class:`.Mapper` mapping the underlying class. + * ``selectable`` - the :class:`.Alias` construct which ultimately + represents an aliased :class:`.Table` or :class:`.Select` + construct. + * ``name`` - the name of the alias. Also is used as the attribute + name when returned in a result tuple from :class:`.Query`. + * ``with_polymorphic_mappers`` - collection of :class:`.Mapper` objects + indicating all those mappers expressed in the select construct + for the :class:`.AliasedClass`. + * ``polymorphic_on`` - an alternate column or SQL expression which + will be used as the "discriminator" for a polymorphic load. + + .. seealso:: + + :ref:`inspection_toplevel` + + """ + + def __init__(self, entity, mapper, selectable, name, + with_polymorphic_mappers, polymorphic_on, + _base_alias, _use_mapper_path, adapt_on_names): + self.entity = entity + self.mapper = mapper + self.selectable = selectable + self.name = name + self.with_polymorphic_mappers = with_polymorphic_mappers + self.polymorphic_on = polymorphic_on + self._base_alias = _base_alias or self + self._use_mapper_path = _use_mapper_path + + self._adapter = sql_util.ClauseAdapter(selectable, + equivalents=mapper._equivalent_columns, + adapt_on_names=adapt_on_names) + + self._adapt_on_names = adapt_on_names + self._target = mapper.class_ + + for poly in self.with_polymorphic_mappers: + if poly is not mapper: + setattr(self.entity, poly.class_.__name__, + AliasedClass(poly.class_, selectable, base_alias=self, + adapt_on_names=adapt_on_names, + use_mapper_path=_use_mapper_path)) + + is_aliased_class = True + "always returns True" + + @property + def class_(self): + """Return the mapped class ultimately represented by this + :class:`.AliasedInsp`.""" + return self.mapper.class_ + + @util.memoized_property + def _path_registry(self): + if self._use_mapper_path: + return self.mapper._path_registry + else: + return PathRegistry.per_mapper(self) + + def __getstate__(self): + return { + 'entity': self.entity, + 'mapper': self.mapper, + 'alias': self.selectable, + 'name': self.name, + 'adapt_on_names': self._adapt_on_names, + 'with_polymorphic_mappers': + self.with_polymorphic_mappers, + 'with_polymorphic_discriminator': + self.polymorphic_on, + 'base_alias': self._base_alias, + 'use_mapper_path': self._use_mapper_path + } + + def __setstate__(self, state): + self.__init__( + state['entity'], + state['mapper'], + state['alias'], + state['name'], + state['with_polymorphic_mappers'], + state['with_polymorphic_discriminator'], + state['base_alias'], + state['use_mapper_path'], + state['adapt_on_names'] + ) + + def _adapt_element(self, elem): + return self._adapter.traverse(elem).\ + _annotate({ + 'parententity': self.entity, + 'parentmapper': self.mapper} + ) + + def _entity_for_mapper(self, mapper): + self_poly = self.with_polymorphic_mappers + if mapper in self_poly: + return getattr(self.entity, mapper.class_.__name__)._aliased_insp + elif mapper.isa(self.mapper): + return self + else: + assert False, "mapper %s doesn't correspond to %s" % (mapper, self) + + def __repr__(self): + return '' % ( + id(self), self.class_.__name__) + + +inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) +inspection._inspects(AliasedInsp)(lambda target: target) + + +def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False): + """Produce an alias of the given element, usually an :class:`.AliasedClass` + instance. + + E.g.:: + + my_alias = aliased(MyClass) + + session.query(MyClass, my_alias).filter(MyClass.id > my_alias.id) + + The :func:`.aliased` function is used to create an ad-hoc mapping + of a mapped class to a new selectable. By default, a selectable + is generated from the normally mapped selectable (typically a + :class:`.Table`) using the :meth:`.FromClause.alias` method. + However, :func:`.aliased` can also be used to link the class to + a new :func:`.select` statement. Also, the :func:`.with_polymorphic` + function is a variant of :func:`.aliased` that is intended to specify + a so-called "polymorphic selectable", that corresponds to the union + of several joined-inheritance subclasses at once. + + For convenience, the :func:`.aliased` function also accepts plain + :class:`.FromClause` constructs, such as a :class:`.Table` or + :func:`.select` construct. In those cases, the :meth:`.FromClause.alias` + method is called on the object and the new :class:`.Alias` object + returned. The returned :class:`.Alias` is not ORM-mapped in this case. + + :param element: element to be aliased. Is normally a mapped class, + but for convenience can also be a :class:`.FromClause` element. + + :param alias: Optional selectable unit to map the element to. This should + normally be a :class:`.Alias` object corresponding to the :class:`.Table` + to which the class is mapped, or to a :func:`.select` construct that + is compatible with the mapping. By default, a simple anonymous + alias of the mapped table is generated. + + :param name: optional string name to use for the alias, if not specified + by the ``alias`` parameter. The name, among other things, forms the + attribute name that will be accessible via tuples returned by a + :class:`.Query` object. + + :param flat: Boolean, will be passed through to the :meth:`.FromClause.alias` + call so that aliases of :class:`.Join` objects don't include an enclosing + SELECT. This can lead to more efficient queries in many circumstances. + A JOIN against a nested JOIN will be rewritten as a JOIN against an aliased + SELECT subquery on backends that don't support this syntax. + + .. versionadded:: 0.9.0 + + .. seealso:: :meth:`.Join.alias` - :param cls: ORM mapped entity which will be "wrapped" around an alias. - :param alias: a selectable, such as an :func:`.alias` or :func:`.select` - construct, which will be rendered in place of the mapped table of the - ORM entity. If left as ``None``, an ordinary :class:`.Alias` of the - ORM entity's mapped table will be generated. - :param name: A name which will be applied both to the :class:`.Alias` - if one is generated, as well as the name present in the "named tuple" - returned by the :class:`.Query` object when results are returned. :param adapt_on_names: if True, more liberal "matching" will be used when - mapping the mapped columns of the ORM entity to those of the given selectable - - a name-based match will be performed if the given selectable doesn't - otherwise have a column that corresponds to one on the entity. The - use case for this is when associating an entity with some derived - selectable such as one that uses aggregate functions:: + mapping the mapped columns of the ORM entity to those of the + given selectable - a name-based match will be performed if the + given selectable doesn't otherwise have a column that corresponds + to one on the entity. The use case for this is when associating + an entity with some derived selectable such as one that uses + aggregate functions:: class UnitPrice(Base): __tablename__ = 'unit_price' @@ -282,107 +601,109 @@ class AliasedClass(object): func.sum(UnitPrice.price).label('price') ).group_by(UnitPrice.unit_id).subquery() - aggregated_unit_price = aliased(UnitPrice, alias=aggregated_unit_price, adapt_on_names=True) + aggregated_unit_price = aliased(UnitPrice, + alias=aggregated_unit_price, adapt_on_names=True) - Above, functions on ``aggregated_unit_price`` which - refer to ``.price`` will return the - ``fund.sum(UnitPrice.price).label('price')`` column, - as it is matched on the name "price". Ordinarily, the "price" function wouldn't - have any "column correspondence" to the actual ``UnitPrice.price`` column - as it is not a proxy of the original. + Above, functions on ``aggregated_unit_price`` which refer to + ``.price`` will return the + ``fund.sum(UnitPrice.price).label('price')`` column, as it is + matched on the name "price". Ordinarily, the "price" function + wouldn't have any "column correspondence" to the actual + ``UnitPrice.price`` column as it is not a proxy of the original. .. versionadded:: 0.7.3 + """ - def __init__(self, cls, alias=None, name=None, adapt_on_names=False): - self.__mapper = _class_to_mapper(cls) - self.__target = self.__mapper.class_ - self.__adapt_on_names = adapt_on_names - if alias is None: - alias = self.__mapper._with_polymorphic_selectable.alias(name=name) - self.__adapter = sql_util.ClauseAdapter(alias, - equivalents=self.__mapper._equivalent_columns, - adapt_on_names=self.__adapt_on_names) - self.__alias = alias - # used to assign a name to the RowTuple object - # returned by Query. - self._sa_label_name = name - self.__name__ = 'AliasedClass_' + str(self.__target) - - def __getstate__(self): - return { - 'mapper':self.__mapper, - 'alias':self.__alias, - 'name':self._sa_label_name, - 'adapt_on_names':self.__adapt_on_names, - } - - def __setstate__(self, state): - self.__mapper = state['mapper'] - self.__target = self.__mapper.class_ - self.__adapt_on_names = state['adapt_on_names'] - alias = state['alias'] - self.__adapter = sql_util.ClauseAdapter(alias, - equivalents=self.__mapper._equivalent_columns, - adapt_on_names=self.__adapt_on_names) - self.__alias = alias - name = state['name'] - self._sa_label_name = name - self.__name__ = 'AliasedClass_' + str(self.__target) - - def __adapt_element(self, elem): - return self.__adapter.traverse(elem).\ - _annotate({ - 'parententity': self, - 'parentmapper':self.__mapper} - ) - - def __adapt_prop(self, existing, key): - comparator = existing.comparator.adapted(self.__adapt_element) - - queryattr = attributes.QueryableAttribute(self, key, - impl=existing.impl, parententity=self, comparator=comparator) - setattr(self, key, queryattr) - return queryattr - - def __getattr__(self, key): - for base in self.__target.__mro__: - try: - attr = object.__getattribute__(base, key) - except AttributeError: - continue - else: - break - else: - raise AttributeError(key) - - if isinstance(attr, attributes.QueryableAttribute): - return self.__adapt_prop(attr, key) - elif hasattr(attr, 'func_code'): - is_method = getattr(self.__target, key, None) - if is_method and is_method.im_self is not None: - return util.types.MethodType(attr.im_func, self, self) - else: - return None - elif hasattr(attr, '__get__'): - ret = attr.__get__(None, self) - if isinstance(ret, PropComparator): - return ret.adapted(self.__adapt_element) - return ret - else: - return attr - - def __repr__(self): - return '' % ( - id(self), self.__target.__name__) - -def aliased(element, alias=None, name=None, adapt_on_names=False): if isinstance(element, expression.FromClause): if adapt_on_names: - raise sa_exc.ArgumentError("adapt_on_names only applies to ORM elements") - return element.alias(name) + raise sa_exc.ArgumentError( + "adapt_on_names only applies to ORM elements" + ) + return element.alias(name, flat=flat) else: - return AliasedClass(element, alias=alias, name=name, adapt_on_names=adapt_on_names) + return AliasedClass(element, alias=alias, flat=flat, + name=name, adapt_on_names=adapt_on_names) + + +def with_polymorphic(base, classes, selectable=False, + flat=False, + polymorphic_on=None, aliased=False, + innerjoin=False, _use_mapper_path=False): + """Produce an :class:`.AliasedClass` construct which specifies + columns for descendant mappers of the given base. + + .. versionadded:: 0.8 + :func:`.orm.with_polymorphic` is in addition to the existing + :class:`.Query` method :meth:`.Query.with_polymorphic`, + which has the same purpose but is not as flexible in its usage. + + Using this method will ensure that each descendant mapper's + tables are included in the FROM clause, and will allow filter() + criterion to be used against those tables. The resulting + instances will also have those columns already loaded so that + no "post fetch" of those columns will be required. + + See the examples at :ref:`with_polymorphic`. + + :param base: Base class to be aliased. + + :param classes: a single class or mapper, or list of + class/mappers, which inherit from the base class. + Alternatively, it may also be the string ``'*'``, in which case + all descending mapped classes will be added to the FROM clause. + + :param aliased: when True, the selectable will be wrapped in an + alias, that is ``(SELECT * FROM ) AS anon_1``. + This can be important when using the with_polymorphic() + to create the target of a JOIN on a backend that does not + support parenthesized joins, such as SQLite and older + versions of MySQL. + + :param flat: Boolean, will be passed through to the :meth:`.FromClause.alias` + call so that aliases of :class:`.Join` objects don't include an enclosing + SELECT. This can lead to more efficient queries in many circumstances. + A JOIN against a nested JOIN will be rewritten as a JOIN against an aliased + SELECT subquery on backends that don't support this syntax. + + Setting ``flat`` to ``True`` implies the ``aliased`` flag is + also ``True``. + + .. versionadded:: 0.9.0 + + .. seealso:: :meth:`.Join.alias` + + :param selectable: a table or select() statement that will + be used in place of the generated FROM clause. This argument is + required if any of the desired classes use concrete table + inheritance, since SQLAlchemy currently cannot generate UNIONs + among tables automatically. If used, the ``selectable`` argument + must represent the full set of tables and columns mapped by every + mapped class. Otherwise, the unaccounted mapped columns will + result in their table being appended directly to the FROM clause + which will usually lead to incorrect results. + + :param polymorphic_on: a column to be used as the "discriminator" + column for the given selectable. If not given, the polymorphic_on + attribute of the base classes' mapper will be used, if any. This + is useful for mappings that don't have polymorphic loading + behavior by default. + + :param innerjoin: if True, an INNER JOIN will be used. This should + only be specified if querying for one specific subtype only + """ + primary_mapper = _class_to_mapper(base) + mappers, selectable = primary_mapper.\ + _with_polymorphic_args(classes, selectable, + innerjoin=innerjoin) + if aliased or flat: + selectable = selectable.alias(flat=flat) + return AliasedClass(base, + selectable, + with_polymorphic_mappers=mappers, + with_polymorphic_discriminator=polymorphic_on, + use_mapper_path=_use_mapper_path) + def _orm_annotate(element, exclude=None): """Deep copy the given ClauseElement, annotating each element with the @@ -391,73 +712,90 @@ def _orm_annotate(element, exclude=None): Elements within the exclude collection will be cloned but not annotated. """ - return sql_util._deep_annotate(element, {'_orm_adapt':True}, exclude) + return sql_util._deep_annotate(element, {'_orm_adapt': True}, exclude) + + +def _orm_deannotate(element): + """Remove annotations that link a column to a particular mapping. + + Note this doesn't affect "remote" and "foreign" annotations + passed by the :func:`.orm.foreign` and :func:`.orm.remote` + annotators. + + """ + + return sql_util._deep_deannotate(element, + values=("_orm_adapt", "parententity") + ) + + +def _orm_full_deannotate(element): + return sql_util._deep_deannotate(element) -_orm_deannotate = sql_util._deep_deannotate class _ORMJoin(expression.Join): """Extend Join to support ORM constructs as input.""" __visit_name__ = expression.Join.__visit_name__ - def __init__(self, left, right, onclause=None, - isouter=False, join_to_left=True): - adapt_from = None + def __init__(self, left, right, onclause=None, isouter=False): - if hasattr(left, '_orm_mappers'): - left_mapper = left._orm_mappers[1] - if join_to_left: - adapt_from = left.right + left_info = inspection.inspect(left) + left_orm_info = getattr(left, '_joined_from_info', left_info) + + right_info = inspection.inspect(right) + adapt_to = right_info.selectable + + self._joined_from_info = right_info + + if isinstance(onclause, util.string_types): + onclause = getattr(left_orm_info.entity, onclause) + + if isinstance(onclause, attributes.QueryableAttribute): + on_selectable = onclause.comparator._source_selectable() + prop = onclause.property + elif isinstance(onclause, MapperProperty): + prop = onclause + on_selectable = prop.parent.selectable else: - left_mapper, left, left_is_aliased = _entity_info(left) - if join_to_left and (left_is_aliased or not left_mapper): - adapt_from = left + prop = None - right_mapper, right, right_is_aliased = _entity_info(right) - if right_is_aliased: - adapt_to = right - else: - adapt_to = None - - if left_mapper or right_mapper: - self._orm_mappers = (left_mapper, right_mapper) - - if isinstance(onclause, basestring): - prop = left_mapper.get_property(onclause) - elif isinstance(onclause, attributes.QueryableAttribute): - if adapt_from is None: - adapt_from = onclause.__clause_element__() - prop = onclause.property - elif isinstance(onclause, MapperProperty): - prop = onclause + if prop: + if sql_util.clause_is_present(on_selectable, left_info.selectable): + adapt_from = on_selectable else: - prop = None + adapt_from = left_info.selectable - if prop: - pj, sj, source, dest, \ + pj, sj, source, dest, \ secondary, target_adapter = prop._create_joins( - source_selectable=adapt_from, - dest_selectable=adapt_to, - source_polymorphic=True, - dest_polymorphic=True, - of_type=right_mapper) + source_selectable=adapt_from, + dest_selectable=adapt_to, + source_polymorphic=True, + dest_polymorphic=True, + of_type=right_info.mapper) - if sj is not None: + if sj is not None: + if isouter: + # note this is an inner join from secondary->right + right = sql.join(secondary, right, sj) + onclause = pj + else: left = sql.join(left, secondary, pj, isouter) onclause = sj - else: - onclause = pj - self._target_adapter = target_adapter + else: + onclause = pj + self._target_adapter = target_adapter expression.Join.__init__(self, left, right, onclause, isouter) - def join(self, right, onclause=None, isouter=False, join_to_left=True): - return _ORMJoin(self, right, onclause, isouter, join_to_left) + def join(self, right, onclause=None, isouter=False, join_to_left=None): + return _ORMJoin(self, right, onclause, isouter) - def outerjoin(self, right, onclause=None, join_to_left=True): - return _ORMJoin(self, right, onclause, True, join_to_left) + def outerjoin(self, right, onclause=None, join_to_left=None): + return _ORMJoin(self, right, onclause, True) -def join(left, right, onclause=None, isouter=False, join_to_left=True): + +def join(left, right, onclause=None, isouter=False, join_to_left=None): """Produce an inner join between left and right clauses. :func:`.orm.join` is an extension to the core join interface @@ -468,11 +806,6 @@ def join(left, right, onclause=None, isouter=False, join_to_left=True): be a SQL expression, or an attribute or string name referencing a configured :func:`.relationship`. - ``join_to_left`` indicates to attempt aliasing the ON clause, - in whatever form it is passed, to the selectable - passed as the left side. If False, the onclause - is used as is. - :func:`.orm.join` is not commonly needed in modern usage, as its functionality is encapsulated within that of the :meth:`.Query.join` method, which features a @@ -496,10 +829,14 @@ def join(left, right, onclause=None, isouter=False, join_to_left=True): See :meth:`.Query.join` for information on modern usage of ORM level joins. - """ - return _ORMJoin(left, right, onclause, isouter, join_to_left) + .. versionchanged:: 0.8.1 - the ``join_to_left`` parameter + is no longer used, and is deprecated. -def outerjoin(left, right, onclause=None, join_to_left=True): + """ + return _ORMJoin(left, right, onclause, isouter) + + +def outerjoin(left, right, onclause=None, join_to_left=None): """Produce a left outer join between left and right clauses. This is the "outer join" version of the :func:`.orm.join` function, @@ -507,7 +844,8 @@ def outerjoin(left, right, onclause=None, join_to_left=True): See that function's documentation for other usage details. """ - return _ORMJoin(left, right, onclause, True, join_to_left) + return _ORMJoin(left, right, onclause, True) + def with_parent(instance, prop): """Create filtering criterion that relates this query's primary entity @@ -536,7 +874,7 @@ def with_parent(instance, prop): parent/child relationship. """ - if isinstance(prop, basestring): + if isinstance(prop, util.string_types): mapper = object_mapper(instance) prop = getattr(mapper.class_, prop).property elif isinstance(prop, attributes.QueryableAttribute): @@ -547,204 +885,69 @@ def with_parent(instance, prop): value_is_parent=True) -def _entity_info(entity, compile=True): - """Return mapping information given a class, mapper, or AliasedClass. - - Returns 3-tuple of: mapper, mapped selectable, boolean indicating if this - is an aliased() construct. - - If the given entity is not a mapper, mapped class, or aliased construct, - returns None, the entity, False. This is typically used to allow - unmapped selectables through. - - """ - if isinstance(entity, AliasedClass): - return entity._AliasedClass__mapper, entity._AliasedClass__alias, True - - if isinstance(entity, mapperlib.Mapper): - mapper = entity - - elif isinstance(entity, type): - class_manager = attributes.manager_of_class(entity) - - if class_manager is None: - return None, entity, False - - mapper = class_manager.mapper - else: - return None, entity, False - - if compile and mapperlib.module._new_mappers: - mapperlib.configure_mappers() - return mapper, mapper._with_polymorphic_selectable, False - -def _entity_descriptor(entity, key): - """Return a class attribute given an entity and string name. - - May return :class:`.InstrumentedAttribute` or user-defined - attribute. - - """ - if isinstance(entity, expression.FromClause): - description = entity - entity = entity.c - elif not isinstance(entity, (AliasedClass, type)): - description = entity = entity.class_ - else: - description = entity - - try: - return getattr(entity, key) - except AttributeError: - raise sa_exc.InvalidRequestError( - "Entity '%s' has no property '%s'" % - (description, key) - ) - -def _orm_columns(entity): - mapper, selectable, is_aliased_class = _entity_info(entity) - if isinstance(selectable, expression.Selectable): - return [c for c in selectable.c] - else: - return [selectable] - -def _orm_selectable(entity): - mapper, selectable, is_aliased_class = _entity_info(entity) - return selectable - -def _attr_as_key(attr): - if hasattr(attr, 'key'): - return attr.key - else: - return expression._column_as_key(attr) - -def _is_aliased_class(entity): - return isinstance(entity, AliasedClass) - -_state_mapper = util.dottedgetter('manager.mapper') - -def object_mapper(instance): - """Given an object, return the primary Mapper associated with the object - instance. - - Raises UnmappedInstanceError if no mapping is configured. - - """ - try: - state = attributes.instance_state(instance) - return state.manager.mapper - except exc.UnmappedClassError: - raise exc.UnmappedInstanceError(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - -def class_mapper(class_, compile=True): - """Given a class, return the primary :class:`.Mapper` associated - with the key. - - Raises :class:`.UnmappedClassError` if no mapping is configured - on the given class, or :class:`.ArgumentError` if a non-class - object is passed. - - """ - - try: - class_manager = attributes.manager_of_class(class_) - mapper = class_manager.mapper - - except exc.NO_STATE: - if not isinstance(class_, type): - raise sa_exc.ArgumentError("Class object expected, got '%r'." % class_) - raise exc.UnmappedClassError(class_) - - if compile and mapperlib.module._new_mappers: - mapperlib.configure_mappers() - return mapper - -def _class_to_mapper(class_or_mapper, compile=True): - if _is_aliased_class(class_or_mapper): - return class_or_mapper._AliasedClass__mapper - - elif isinstance(class_or_mapper, type): - try: - class_manager = attributes.manager_of_class(class_or_mapper) - mapper = class_manager.mapper - except exc.NO_STATE: - raise exc.UnmappedClassError(class_or_mapper) - elif isinstance(class_or_mapper, mapperlib.Mapper): - mapper = class_or_mapper - else: - raise exc.UnmappedClassError(class_or_mapper) - - if compile and mapperlib.module._new_mappers: - mapperlib.configure_mappers() - return mapper def has_identity(object): + """Return True if the given object has a database + identity. + + This typically corresponds to the object being + in either the persistent or detached state. + + .. seealso:: + + :func:`.was_deleted` + + """ state = attributes.instance_state(object) return state.has_identity -def _is_mapped_class(cls): - """Return True if the given object is a mapped class, - :class:`.Mapper`, or :class:`.AliasedClass`.""" +def was_deleted(object): + """Return True if the given object was deleted + within a session flush. - if isinstance(cls, (AliasedClass, mapperlib.Mapper)): - return True - if isinstance(cls, expression.ClauseElement): - return False - if isinstance(cls, type): - manager = attributes.manager_of_class(cls) - return manager and _INSTRUMENTOR in manager.info - return False + .. versionadded:: 0.8.0 -def _mapper_or_none(cls): - """Return the :class:`.Mapper` for the given class or None if the - class is not mapped.""" + """ - manager = attributes.manager_of_class(cls) - if manager is not None and _INSTRUMENTOR in manager.info: - return manager.info[_INSTRUMENTOR] - else: - return None + state = attributes.instance_state(object) + return state.deleted -def instance_str(instance): - """Return a string describing an instance.""" - return state_str(attributes.instance_state(instance)) -def state_str(state): - """Return a string describing an instance via its InstanceState.""" - if state is None: - return "None" - else: - return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj())) +def randomize_unitofwork(): + """Use random-ordering sets within the unit of work in order + to detect unit of work sorting issues. -def state_class_str(state): - """Return a string describing an instance's class via its InstanceState.""" + This is a utility function that can be used to help reproduce + inconsistent unit of work sorting issues. For example, + if two kinds of objects A and B are being inserted, and + B has a foreign key reference to A - the A must be inserted first. + However, if there is no relationship between A and B, the unit of work + won't know to perform this sorting, and an operation may or may not + fail, depending on how the ordering works out. Since Python sets + and dictionaries have non-deterministic ordering, such an issue may + occur on some runs and not on others, and in practice it tends to + have a great dependence on the state of the interpreter. This leads + to so-called "heisenbugs" where changing entirely irrelevant aspects + of the test program still cause the failure behavior to change. - if state is None: - return "None" - else: - return '<%s>' % (state.class_.__name__, ) + By calling ``randomize_unitofwork()`` when a script first runs, the + ordering of a key series of sets within the unit of work implementation + are randomized, so that the script can be minimized down to the fundamental + mapping and operation that's failing, while still reproducing the issue + on at least some runs. -def attribute_str(instance, attribute): - return instance_str(instance) + "." + attribute + This utility is also available when running the test suite via the + ``--reversetop`` flag. -def state_attribute_str(state, attribute): - return state_str(state) + "." + attribute + .. versionadded:: 0.8.1 created a standalone version of the + ``--reversetop`` feature. -def identity_equal(a, b): - if a is b: - return True - if a is None or b is None: - return False - try: - state_a = attributes.instance_state(a) - state_b = attributes.instance_state(b) - except exc.NO_STATE: - return False - if state_a.key is None or state_b.key is None: - return False - return state_a.key == state_b.key + """ + from sqlalchemy.orm import unitofwork, session, mapper, dependency + from sqlalchemy.util import topological + from sqlalchemy.testing.util import RandomSet + topological.set = unitofwork.set = session.set = mapper.set = \ + dependency.set = RandomSet diff --git a/libs/sqlalchemy/pool.py b/libs/sqlalchemy/pool.py index 0d04998c..3adfb320 100644 --- a/libs/sqlalchemy/pool.py +++ b/libs/sqlalchemy/pool.py @@ -1,5 +1,5 @@ # sqlalchemy/pool.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -16,16 +16,19 @@ regular DB-API connect() methods to be transparently managed by a SQLAlchemy connection pool. """ -import weakref import time import traceback +import weakref -from sqlalchemy import exc, log, event, events, interfaces, util -from sqlalchemy.util import queue as sqla_queue -from sqlalchemy.util import threading, memoized_property, \ +from . import exc, log, event, interfaces, util +from .util import queue as sqla_queue +from .util import threading, memoized_property, \ chop_traceback + +from collections import deque proxies = {} + def manage(module, **params): """Return a proxy for a DB-API module that automatically pools connections. @@ -48,13 +51,14 @@ def manage(module, **params): except KeyError: return proxies.setdefault(module, _DBProxy(module, **params)) + def clear_managers(): """Remove all current DB-API 2.0 managers. All pools and connections are disposed. """ - for manager in proxies.itervalues(): + for manager in proxies.values(): manager.close() proxies.clear() @@ -62,10 +66,29 @@ reset_rollback = util.symbol('reset_rollback') reset_commit = util.symbol('reset_commit') reset_none = util.symbol('reset_none') +class _ConnDialect(object): + """partial implementation of :class:`.Dialect` + which provides DBAPI connection methods. + + When a :class:`.Pool` is combined with an :class:`.Engine`, + the :class:`.Engine` replaces this with its own + :class:`.Dialect`. + + """ + def do_rollback(self, dbapi_connection): + dbapi_connection.rollback() + + def do_commit(self, dbapi_connection): + dbapi_connection.commit() + + def do_close(self, dbapi_connection): + dbapi_connection.close() class Pool(log.Identified): """Abstract base class for connection pools.""" + _dialect = _ConnDialect() + def __init__(self, creator, recycle=-1, echo=None, use_threadlocal=False, @@ -73,7 +96,8 @@ class Pool(log.Identified): reset_on_return=True, listeners=None, events=None, - _dispatch=None): + _dispatch=None, + _dialect=None): """ Construct a Pool. @@ -106,10 +130,10 @@ class Pool(log.Identified): :meth:`unique_connection` method is provided to bypass the threadlocal behavior installed into :meth:`connect`. - :param reset_on_return: If true, reset the database state of - connections returned to the pool. This is typically a - ROLLBACK to release locks and transaction resources. - Disable at your own peril. Defaults to True. + :param reset_on_return: Configures the action to take + on connections as they are returned to the pool. + See the argument description in :class:`.QueuePool` for + more detail. :param events: a list of 2-tuples, each of the form ``(callable, target)`` which will be passed to event.listen() @@ -149,6 +173,8 @@ class Pool(log.Identified): self.echo = echo if _dispatch: self.dispatch._update(_dispatch, only_propagate=False) + if _dialect: + self._dialect = _dialect if events: for fn, target in events: event.listen(self, target, fn) @@ -159,9 +185,18 @@ class Pool(log.Identified): for l in listeners: self.add_listener(l) - dispatch = event.dispatcher(events.PoolEvents) + def _close_connection(self, connection): + self.logger.debug("Closing connection %r", connection) + try: + self._dialect.do_close(connection) + except (SystemExit, KeyboardInterrupt): + raise + except: + self.logger.error("Exception closing connection %r", + connection, exc_info=True) - @util.deprecated(2.7, "Pool.add_listener is deprecated. Use event.listen()") + @util.deprecated( + 2.7, "Pool.add_listener is deprecated. Use event.listen()") def add_listener(self, listener): """Add a :class:`.PoolListener`-like object to this pool. @@ -181,7 +216,7 @@ class Pool(log.Identified): """ - return _ConnectionFairy(self).checkout() + return _ConnectionFairy.checkout(self) def _create_connection(self): """Called by subclasses to create a new ConnectionRecord.""" @@ -233,18 +268,17 @@ class Pool(log.Identified): """ if not self._use_threadlocal: - return _ConnectionFairy(self).checkout() + return _ConnectionFairy.checkout(self) try: rec = self._threadconns.current() - if rec: - return rec.checkout() except AttributeError: pass + else: + if rec is not None: + return rec.checkout_existing() - agent = _ConnectionFairy(self) - self._threadconns.current = weakref.ref(agent) - return agent.checkout() + return _ConnectionFairy.checkout(self, self._threadconns) def _return_conn(self, record): """Given a _ConnectionRecord, return it to the :class:`.Pool`. @@ -275,28 +309,58 @@ class Pool(log.Identified): class _ConnectionRecord(object): - finalize_callback = None def __init__(self, pool): self.__pool = pool self.connection = self.__connect() - self.info = {} + self.finalize_callback = deque() pool.dispatch.first_connect.\ for_modify(pool.dispatch).\ exec_once(self.connection, self) pool.dispatch.connect(self.connection, self) + @util.memoized_property + def info(self): + return {} + + @classmethod + def checkout(cls, pool): + rec = pool._do_get() + try: + dbapi_connection = rec.get_connection() + except: + rec.checkin() + raise + fairy = _ConnectionFairy(dbapi_connection, rec) + rec.fairy_ref = weakref.ref( + fairy, + lambda ref: _finalize_fairy and \ + _finalize_fairy( + dbapi_connection, + rec, pool, ref, pool._echo) + ) + _refs.add(rec) + if pool._echo: + pool.logger.debug("Connection %r checked out from pool", + dbapi_connection) + return fairy + + def checkin(self): + self.fairy_ref = None + connection = self.connection + pool = self.__pool + while self.finalize_callback: + finalizer = self.finalize_callback.pop() + finalizer(connection) + if pool.dispatch.checkin: + pool.dispatch.checkin(connection, self) + pool._return_conn(self) + + def close(self): if self.connection is not None: - self.__pool.logger.debug("Closing connection %r", self.connection) - try: - self.connection.close() - except (SystemExit, KeyboardInterrupt): - raise - except: - self.__pool.logger.debug("Exception closing connection %r", - self.connection) + self.__pool._close_connection(self.connection) def invalidate(self, e=None): if e is not None: @@ -328,15 +392,7 @@ class _ConnectionRecord(object): return self.connection def __close(self): - try: - self.__pool.logger.debug("Closing connection %r", self.connection) - self.connection.close() - except (SystemExit, KeyboardInterrupt): - raise - except Exception, e: - self.__pool.logger.debug( - "Connection %r threw an error on close: %s", - self.connection, e) + self.__pool._close_connection(self.connection) def __connect(self): try: @@ -344,75 +400,114 @@ class _ConnectionRecord(object): connection = self.__pool._creator() self.__pool.logger.debug("Created new connection %r", connection) return connection - except Exception, e: + except Exception as e: self.__pool.logger.debug("Error on connect(): %s", e) raise -def _finalize_fairy(connection, connection_record, pool, ref, echo): +def _finalize_fairy(connection, connection_record, pool, ref, echo, fairy=None): + """Cleanup for a :class:`._ConnectionFairy` whether or not it's already + been garbage collected. + + """ _refs.discard(connection_record) if ref is not None and \ - connection_record.fairy is not ref: + connection_record.fairy_ref is not ref: return if connection is not None: + if connection_record and echo: + pool.logger.debug("Connection %r being returned to pool", + connection) + try: + fairy = fairy or _ConnectionFairy(connection, connection_record) + if pool.dispatch.reset: + pool.dispatch.reset(fairy, connection_record) if pool._reset_on_return is reset_rollback: - connection.rollback() + if echo: + pool.logger.debug("Connection %s rollback-on-return", + connection) + pool._dialect.do_rollback(fairy) elif pool._reset_on_return is reset_commit: - connection.commit() + if echo: + pool.logger.debug("Connection %s commit-on-return", + connection) + pool._dialect.do_commit(fairy) + # Immediately close detached instances - if connection_record is None: - connection.close() - except Exception, e: - if connection_record is not None: + if not connection_record: + pool._close_connection(connection) + except Exception as e: + if connection_record: connection_record.invalidate(e=e) if isinstance(e, (SystemExit, KeyboardInterrupt)): raise - if connection_record is not None: - connection_record.fairy = None - if echo: - pool.logger.debug("Connection %r being returned to pool", - connection) - if connection_record.finalize_callback: - connection_record.finalize_callback(connection) - del connection_record.finalize_callback - if pool.dispatch.checkin: - pool.dispatch.checkin(connection, connection_record) - pool._return_conn(connection_record) + if connection_record: + connection_record.checkin() + _refs = set() + class _ConnectionFairy(object): """Proxies a DB-API connection and provides return-on-dereference support.""" - __slots__ = '_pool', '__counter', 'connection', \ - '_connection_record', '__weakref__', \ - '_detached_info', '_echo' + def __init__(self, dbapi_connection, connection_record): + self.connection = dbapi_connection + self._connection_record = connection_record - def __init__(self, pool): - self._pool = pool - self.__counter = 0 - self._echo = _echo = pool._should_log_debug() - try: - rec = self._connection_record = pool._do_get() - conn = self.connection = self._connection_record.get_connection() - rec.fairy = weakref.ref( - self, - lambda ref:_finalize_fairy and _finalize_fairy(conn, rec, pool, ref, _echo) - ) - _refs.add(rec) - except: - # helps with endless __getattr__ loops later on - self.connection = None - self._connection_record = None - raise - if self._echo: - self._pool.logger.debug("Connection %r checked out from pool" % - self.connection) + @classmethod + def checkout(cls, pool, threadconns=None, fairy=None): + if not fairy: + fairy = _ConnectionRecord.checkout(pool) + + fairy._pool = pool + fairy._counter = 0 + fairy._echo = pool._should_log_debug() + + if threadconns is not None: + threadconns.current = weakref.ref(fairy) + + if fairy.connection is None: + raise exc.InvalidRequestError("This connection is closed") + fairy._counter += 1 + + if not pool.dispatch.checkout or fairy._counter != 1: + return fairy + + # Pool listeners can trigger a reconnection on checkout + attempts = 2 + while attempts > 0: + try: + pool.dispatch.checkout(fairy.connection, + fairy._connection_record, + fairy) + return fairy + except exc.DisconnectionError as e: + pool.logger.info( + "Disconnection detected on checkout: %s", e) + fairy._connection_record.invalidate(e) + fairy.connection = fairy._connection_record.get_connection() + attempts -= 1 + + pool.logger.info("Reconnection attempts exhausted on checkout") + fairy.invalidate() + raise exc.InvalidRequestError("This connection is closed") + + def checkout_existing(self): + return _ConnectionFairy.checkout(self._pool, fairy=self) + + def checkin(self): + _finalize_fairy(self.connection, self._connection_record, + self._pool, None, self._echo, fairy=self) + self.connection = None + self._connection_record = None + + _close = checkin @property def _logger(self): @@ -422,20 +517,18 @@ class _ConnectionFairy(object): def is_valid(self): return self.connection is not None - @property + @util.memoized_property def info(self): - """An info collection unique to this DB-API connection.""" + """Info dictionary associated with the underlying DBAPI connection + referred to by this :class:`.ConnectionFairy`, allowing user-defined + data to be associated with the connection. - try: - return self._connection_record.info - except AttributeError: - if self.connection is None: - raise exc.InvalidRequestError("This connection is closed") - try: - return self._detached_info - except AttributeError: - self._detached_info = value = {} - return value + The data here will follow along with the DBAPI connection including + after it is returned to the connection pool and used again + in subsequent instances of :class:`.ConnectionFairy`. + + """ + return self._connection_record.info def invalidate(self, e=None): """Mark this connection as invalidated. @@ -446,10 +539,10 @@ class _ConnectionFairy(object): if self.connection is None: raise exc.InvalidRequestError("This connection is closed") - if self._connection_record is not None: + if self._connection_record: self._connection_record.invalidate(e=e) self.connection = None - self._close() + self.checkin() def cursor(self, *args, **kwargs): return self.connection.cursor(*args, **kwargs) @@ -457,32 +550,6 @@ class _ConnectionFairy(object): def __getattr__(self, key): return getattr(self.connection, key) - def checkout(self): - if self.connection is None: - raise exc.InvalidRequestError("This connection is closed") - self.__counter += 1 - - if not self._pool.dispatch.checkout or self.__counter != 1: - return self - - # Pool listeners can trigger a reconnection on checkout - attempts = 2 - while attempts > 0: - try: - self._pool.dispatch.checkout(self.connection, - self._connection_record, - self) - return self - except exc.DisconnectionError, e: - self._pool.logger.info( - "Disconnection detected on checkout: %s", e) - self._connection_record.invalidate(e) - self.connection = self._connection_record.get_connection() - attempts -= 1 - - self._pool.logger.info("Reconnection attempts exhausted on checkout") - self.invalidate() - raise exc.InvalidRequestError("This connection is closed") def detach(self): """Separate this connection from its Pool. @@ -499,23 +566,19 @@ class _ConnectionFairy(object): if self._connection_record is not None: _refs.remove(self._connection_record) - self._connection_record.fairy = None + self._connection_record.fairy_ref = None self._connection_record.connection = None + # TODO: should this be _return_conn? self._pool._do_return_conn(self._connection_record) - self._detached_info = \ - self._connection_record.info.copy() + self.info = self.info.copy() self._connection_record = None def close(self): - self.__counter -= 1 - if self.__counter == 0: - self._close() + self._counter -= 1 + if self._counter == 0: + self.checkin() + - def _close(self): - _finalize_fairy(self.connection, self._connection_record, - self._pool, None, self._echo) - self.connection = None - self._connection_record = None class SingletonThreadPool(Pool): """A Pool that maintains one connection per thread. @@ -549,7 +612,9 @@ class SingletonThreadPool(Pool): echo=self.echo, logging_name=self._orig_logging_name, use_threadlocal=self._use_threadlocal, - _dispatch=self.dispatch) + reset_on_return=self._reset_on_return, + _dispatch=self.dispatch, + _dialect=self._dialect) def dispose(self): """Dispose of this pool.""" @@ -592,11 +657,6 @@ class SingletonThreadPool(Pool): self._cleanup() return c -class DummyLock(object): - def acquire(self, wait=True): - return True - def release(self): - pass class QueuePool(Pool): """A :class:`.Pool` that imposes a limit on the number of open connections. @@ -705,30 +765,27 @@ class QueuePool(Pool): self._overflow = 0 - pool_size self._max_overflow = max_overflow self._timeout = timeout - self._overflow_lock = self._max_overflow > -1 and \ - threading.Lock() or DummyLock() + self._overflow_lock = threading.Lock() def _do_return_conn(self, conn): try: self._pool.put(conn, False) except sqla_queue.Full: - conn.close() - self._overflow_lock.acquire() try: - self._overflow -= 1 + conn.close() finally: - self._overflow_lock.release() + self._dec_overflow() def _do_get(self): + use_overflow = self._max_overflow > -1 + try: - wait = self._max_overflow > -1 and \ - self._overflow >= self._max_overflow + wait = use_overflow and self._overflow >= self._max_overflow return self._pool.get(wait, self._timeout) - except sqla_queue.SAAbort, aborted: + except sqla_queue.SAAbort as aborted: return aborted.context._do_get() except sqla_queue.Empty: - if self._max_overflow > -1 and \ - self._overflow >= self._max_overflow: + if use_overflow and self._overflow >= self._max_overflow: if not wait: return self._do_get() else: @@ -737,17 +794,33 @@ class QueuePool(Pool): "connection timed out, timeout %d" % (self.size(), self.overflow(), self._timeout)) - self._overflow_lock.acquire() - try: - if self._max_overflow > -1 and \ - self._overflow >= self._max_overflow: - return self._do_get() - else: - con = self._create_connection() - self._overflow += 1 - return con - finally: - self._overflow_lock.release() + if self._inc_overflow(): + try: + return self._create_connection() + except: + self._dec_overflow() + raise + else: + return self._do_get() + + def _inc_overflow(self): + if self._max_overflow == -1: + self._overflow += 1 + return True + with self._overflow_lock: + if self._overflow < self._max_overflow: + self._overflow += 1 + return True + else: + return False + + def _dec_overflow(self): + if self._max_overflow == -1: + self._overflow -= 1 + return True + with self._overflow_lock: + self._overflow -= 1 + return True def recreate(self): self.logger.info("Pool recreating") @@ -757,7 +830,9 @@ class QueuePool(Pool): recycle=self._recycle, echo=self.echo, logging_name=self._orig_logging_name, use_threadlocal=self._use_threadlocal, - _dispatch=self.dispatch) + reset_on_return=self._reset_on_return, + _dispatch=self.dispatch, + _dialect=self._dialect) def dispose(self): while True: @@ -796,6 +871,7 @@ class QueuePool(Pool): def checkedout(self): return self._pool.maxsize - self._pool.qsize() + self._overflow + class NullPool(Pool): """A Pool which does not pool connections. @@ -829,7 +905,9 @@ class NullPool(Pool): echo=self.echo, logging_name=self._orig_logging_name, use_threadlocal=self._use_threadlocal, - _dispatch=self.dispatch) + reset_on_return=self._reset_on_return, + _dispatch=self.dispatch, + _dialect=self._dialect) def dispose(self): pass @@ -869,7 +947,8 @@ class StaticPool(Pool): reset_on_return=self._reset_on_return, echo=self.echo, logging_name=self._orig_logging_name, - _dispatch=self.dispatch) + _dispatch=self.dispatch, + _dialect=self._dialect) def _create_connection(self): return self._conn @@ -880,9 +959,10 @@ class StaticPool(Pool): def _do_get(self): return self.connection + class AssertionPool(Pool): - """A :class:`.Pool` that allows at most one checked out connection at any given - time. + """A :class:`.Pool` that allows at most one checked out connection at + any given time. This will raise an exception if more than one connection is checked out at a time. Useful for debugging code that is using more connections @@ -919,7 +999,8 @@ class AssertionPool(Pool): self.logger.info("Pool recreating") return self.__class__(self._creator, echo=self.echo, logging_name=self._orig_logging_name, - _dispatch=self.dispatch) + _dispatch=self.dispatch, + _dialect=self._dialect) def _do_get(self): if self._checked_out: @@ -938,6 +1019,7 @@ class AssertionPool(Pool): self._checkout_traceback = traceback.format_stack() return self._conn + class _DBProxy(object): """Layers connection pooling behavior on top of a standard DB-API module. @@ -966,7 +1048,7 @@ class _DBProxy(object): self._create_pool_mutex = threading.Lock() def close(self): - for key in self.pools.keys(): + for key in list(self.pools): del self.pools[key] def __del__(self): diff --git a/libs/sqlalchemy/processors.py b/libs/sqlalchemy/processors.py index bc5c3909..0abf063b 100644 --- a/libs/sqlalchemy/processors.py +++ b/libs/sqlalchemy/processors.py @@ -1,5 +1,5 @@ # sqlalchemy/processors.py -# Copyright (C) 2010-2011 the SQLAlchemy authors and contributors +# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors # Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com # # This module is part of SQLAlchemy and is released under @@ -16,10 +16,13 @@ import codecs import re import datetime + def str_to_datetime_processor_factory(regexp, type_): rmatch = regexp.match # Even on python2.6 datetime.strptime is both slower than this code # and it does not support microseconds. + has_named_groups = bool(regexp.groupindex) + def process(value): if value is None: return None @@ -28,19 +31,27 @@ def str_to_datetime_processor_factory(regexp, type_): m = rmatch(value) except TypeError: raise ValueError("Couldn't parse %s string '%r' " - "- value is not a string." % (type_.__name__ , value)) + "- value is not a string." % + (type_.__name__, value)) if m is None: raise ValueError("Couldn't parse %s string: " - "'%s'" % (type_.__name__ , value)) - return type_(*map(int, m.groups(0))) + "'%s'" % (type_.__name__, value)) + if has_named_groups: + groups = m.groupdict(0) + return type_(**dict(list(zip(iter(groups.keys()), + list(map(int, iter(groups.values()))))))) + else: + return type_(*list(map(int, m.groups(0)))) return process + def boolean_to_int(value): if value is None: return None else: return int(value) + def py_fallback(): def to_unicode_processor_factory(encoding, errors=None): decoder = codecs.getdecoder(encoding) @@ -55,7 +66,7 @@ def py_fallback(): return decoder(value, errors)[0] return process - def to_decimal_processor_factory(target_class, scale=10): + def to_decimal_processor_factory(target_class, scale): fstring = "%%.%df" % scale def process(value): @@ -108,7 +119,7 @@ try: else: return UnicodeResultProcessor(encoding).process - def to_decimal_processor_factory(target_class, scale=10): + def to_decimal_processor_factory(target_class, scale): # Note that the scale argument is not taken into account for integer # values in the C implementation while it is in the Python one. # For example, the Python implementation might return @@ -118,4 +129,3 @@ try: except ImportError: globals().update(py_fallback()) - diff --git a/libs/sqlalchemy/schema.py b/libs/sqlalchemy/schema.py index 154fb5f7..17214153 100644 --- a/libs/sqlalchemy/schema.py +++ b/libs/sqlalchemy/schema.py @@ -1,3204 +1,54 @@ -# sqlalchemy/schema.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# schema.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""The schema module provides the building blocks for database metadata. - -Each element within this module describes a database entity which can be -created and dropped, or is otherwise part of such an entity. Examples include -tables, columns, sequences, and indexes. - -All entities are subclasses of :class:`~sqlalchemy.schema.SchemaItem`, and as -defined in this module they are intended to be agnostic of any vendor-specific -constructs. - -A collection of entities are grouped into a unit called -:class:`~sqlalchemy.schema.MetaData`. MetaData serves as a logical grouping of -schema elements, and can also be associated with an actual database connection -such that operations involving the contained elements can contact the database -as needed. - -Two of the elements here also build upon their "syntactic" counterparts, which -are defined in :class:`~sqlalchemy.sql.expression.`, specifically -:class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.schema.Column`. -Since these objects are part of the SQL expression language, they are usable -as components in SQL expressions. +"""Compatiblity namespace for sqlalchemy.sql.schema and related. """ -import re, inspect -from sqlalchemy import exc, util, dialects -from sqlalchemy.sql import expression, visitors -from sqlalchemy import event, events -ddl = util.importlater("sqlalchemy.engine", "ddl") -sqlutil = util.importlater("sqlalchemy.sql", "util") -url = util.importlater("sqlalchemy.engine", "url") -sqltypes = util.importlater("sqlalchemy", "types") - -__all__ = ['SchemaItem', 'Table', 'Column', 'ForeignKey', 'Sequence', 'Index', - 'ForeignKeyConstraint', 'PrimaryKeyConstraint', 'CheckConstraint', - 'UniqueConstraint', 'DefaultGenerator', 'Constraint', 'MetaData', - 'ThreadLocalMetaData', 'SchemaVisitor', 'PassiveDefault', - 'DefaultClause', 'FetchedValue', 'ColumnDefault', 'DDL', - 'CreateTable', 'DropTable', 'CreateSequence', 'DropSequence', - 'AddConstraint', 'DropConstraint', - ] -__all__.sort() - -RETAIN_SCHEMA = util.symbol('retain_schema') - -class SchemaItem(events.SchemaEventTarget, visitors.Visitable): - """Base class for items that define a database schema.""" - - __visit_name__ = 'schema_item' - quote = None - - def _init_items(self, *args): - """Initialize the list of child items for this SchemaItem.""" - - for item in args: - if item is not None: - item._set_parent_with_dispatch(self) - - def get_children(self, **kwargs): - """used to allow SchemaVisitor access""" - return [] - - def __repr__(self): - return util.generic_repr(self) - - @util.memoized_property - def info(self): - return {} - -def _get_table_key(name, schema): - if schema is None: - return name - else: - return schema + "." + name - -def _validate_dialect_kwargs(kwargs, name): - # validate remaining kwargs that they all specify DB prefixes - if len([k for k in kwargs - if not re.match( - r'^(?:%s)_' % - '|'.join(dialects.__all__), k - ) - ]): - raise TypeError( - "Invalid argument(s) for %s: %r" % (name, kwargs.keys())) - - -class Table(SchemaItem, expression.TableClause): - """Represent a table in a database. - - e.g.:: - - mytable = Table("mytable", metadata, - Column('mytable_id', Integer, primary_key=True), - Column('value', String(50)) - ) - - The :class:`.Table` object constructs a unique instance of itself based on its - name and optional schema name within the given :class:`.MetaData` object. - Calling the :class:`.Table` - constructor with the same name and same :class:`.MetaData` argument - a second time will return the *same* :class:`.Table` object - in this way - the :class:`.Table` constructor acts as a registry function. - - See also: - - :ref:`metadata_describing` - Introduction to database metadata - - Constructor arguments are as follows: - - :param name: The name of this table as represented in the database. - - This property, along with the *schema*, indicates the *singleton - identity* of this table in relation to its parent :class:`.MetaData`. - Additional calls to :class:`.Table` with the same name, metadata, - and schema name will return the same :class:`.Table` object. - - Names which contain no upper case characters - will be treated as case insensitive names, and will not be quoted - unless they are a reserved word. Names with any number of upper - case characters will be quoted and sent exactly. Note that this - behavior applies even for databases which standardize upper - case names as case insensitive such as Oracle. - - :param metadata: a :class:`.MetaData` object which will contain this - table. The metadata is used as a point of association of this table - with other tables which are referenced via foreign key. It also - may be used to associate this table with a particular - :class:`~sqlalchemy.engine.base.Connectable`. - - :param \*args: Additional positional arguments are used primarily - to add the list of :class:`.Column` objects contained within this - table. Similar to the style of a CREATE TABLE statement, other - :class:`.SchemaItem` constructs may be added here, including - :class:`.PrimaryKeyConstraint`, and :class:`.ForeignKeyConstraint`. - - :param autoload: Defaults to False: the Columns for this table should - be reflected from the database. Usually there will be no Column - objects in the constructor if this property is set. - - :param autoload_replace: If ``True``, when using ``autoload=True`` - and ``extend_existing=True``, - replace ``Column`` objects already present in the ``Table`` that's - in the ``MetaData`` registry with - what's reflected. Otherwise, all existing columns will be - excluded from the reflection process. Note that this does - not impact ``Column`` objects specified in the same call to ``Table`` - which includes ``autoload``, those always take precedence. - Defaults to ``True``. - - .. versionadded:: 0.7.5 - - :param autoload_with: If autoload==True, this is an optional Engine - or Connection instance to be used for the table reflection. If - ``None``, the underlying MetaData's bound connectable will be used. - - :param extend_existing: When ``True``, indicates that if this :class:`.Table` is already - present in the given :class:`.MetaData`, apply further arguments within - the constructor to the existing :class:`.Table`. - - If ``extend_existing`` or ``keep_existing`` are not set, an error is - raised if additional table modifiers are specified when - the given :class:`.Table` is already present in the :class:`.MetaData`. - - .. versionchanged:: 0.7.4 - ``extend_existing`` will work in conjunction - with ``autoload=True`` to run a new reflection operation against - the database; new :class:`.Column` objects will be produced - from database metadata to replace those existing with the same - name, and additional :class:`.Column` objects not present - in the :class:`.Table` will be added. - - As is always the case with ``autoload=True``, :class:`.Column` - objects can be specified in the same :class:`.Table` constructor, - which will take precedence. I.e.:: - - Table("mytable", metadata, - Column('y', Integer), - extend_existing=True, - autoload=True, - autoload_with=engine - ) - - The above will overwrite all columns within ``mytable`` which - are present in the database, except for ``y`` which will be used as is - from the above definition. If the ``autoload_replace`` flag - is set to False, no existing columns will be replaced. - - :param implicit_returning: True by default - indicates that - RETURNING can be used by default to fetch newly inserted primary key - values, for backends which support this. Note that - create_engine() also provides an implicit_returning flag. - - :param include_columns: A list of strings indicating a subset of - columns to be loaded via the ``autoload`` operation; table columns who - aren't present in this list will not be represented on the resulting - ``Table`` object. Defaults to ``None`` which indicates all columns - should be reflected. - - :param info: A dictionary which defaults to ``{}``. A space to store - application specific data. This must be a dictionary. - - :param keep_existing: When ``True``, indicates that if this Table - is already present in the given :class:`.MetaData`, ignore - further arguments within the constructor to the existing - :class:`.Table`, and return the :class:`.Table` object as - originally created. This is to allow a function that wishes - to define a new :class:`.Table` on first call, but on - subsequent calls will return the same :class:`.Table`, - without any of the declarations (particularly constraints) - being applied a second time. Also see extend_existing. - - If extend_existing or keep_existing are not set, an error is - raised if additional table modifiers are specified when - the given :class:`.Table` is already present in the :class:`.MetaData`. - - :param listeners: A list of tuples of the form ``(, )`` - which will be passed to :func:`.event.listen` upon construction. - This alternate hook to :func:`.event.listen` allows the establishment - of a listener function specific to this :class:`.Table` before - the "autoload" process begins. Particularly useful for - the :meth:`.events.column_reflect` event:: - - def listen_for_reflect(table, column_info): - "handle the column reflection event" - # ... - - t = Table( - 'sometable', - autoload=True, - listeners=[ - ('column_reflect', listen_for_reflect) - ]) - - :param mustexist: When ``True``, indicates that this Table must already - be present in the given :class:`.MetaData`` collection, else - an exception is raised. - - :param prefixes: - A list of strings to insert after CREATE in the CREATE TABLE - statement. They will be separated by spaces. - - :param quote: Force quoting of this table's name on or off, corresponding - to ``True`` or ``False``. When left at its default of ``None``, - the column identifier will be quoted according to whether the name is - case sensitive (identifiers with at least one upper case character are - treated as case sensitive), or if it's a reserved word. This flag - is only needed to force quoting of a reserved word which is not known - by the SQLAlchemy dialect. - - :param quote_schema: same as 'quote' but applies to the schema identifier. - - :param schema: The *schema name* for this table, which is required if - the table resides in a schema other than the default selected schema - for the engine's database connection. Defaults to ``None``. - - :param useexisting: Deprecated. Use extend_existing. - - """ - - __visit_name__ = 'table' - - def __new__(cls, *args, **kw): - if not args: - # python3k pickle seems to call this - return object.__new__(cls) - - try: - name, metadata, args = args[0], args[1], args[2:] - except IndexError: - raise TypeError("Table() takes at least two arguments") - - schema = kw.get('schema', None) - if schema is None: - schema = metadata.schema - keep_existing = kw.pop('keep_existing', False) - extend_existing = kw.pop('extend_existing', False) - if 'useexisting' in kw: - util.warn_deprecated("useexisting is deprecated. Use extend_existing.") - if extend_existing: - raise exc.ArgumentError("useexisting is synonymous " - "with extend_existing.") - extend_existing = kw.pop('useexisting', False) - - if keep_existing and extend_existing: - raise exc.ArgumentError("keep_existing and extend_existing " - "are mutually exclusive.") - - mustexist = kw.pop('mustexist', False) - key = _get_table_key(name, schema) - if key in metadata.tables: - if not keep_existing and not extend_existing and bool(args): - raise exc.InvalidRequestError( - "Table '%s' is already defined for this MetaData " - "instance. Specify 'extend_existing=True' " - "to redefine " - "options and columns on an " - "existing Table object." % key) - table = metadata.tables[key] - if extend_existing: - table._init_existing(*args, **kw) - return table - else: - if mustexist: - raise exc.InvalidRequestError( - "Table '%s' not defined" % (key)) - table = object.__new__(cls) - table.dispatch.before_parent_attach(table, metadata) - metadata._add_table(name, schema, table) - try: - table._init(name, metadata, *args, **kw) - table.dispatch.after_parent_attach(table, metadata) - return table - except: - metadata._remove_table(name, schema) - raise - - def __init__(self, *args, **kw): - """Constructor for :class:`~.schema.Table`. - - This method is a no-op. See the top-level - documentation for :class:`~.schema.Table` - for constructor arguments. - - """ - # __init__ is overridden to prevent __new__ from - # calling the superclass constructor. - - def _init(self, name, metadata, *args, **kwargs): - super(Table, self).__init__(name) - self.metadata = metadata - self.schema = kwargs.pop('schema', None) - if self.schema is None: - self.schema = metadata.schema - self.quote_schema = kwargs.pop('quote_schema', metadata.quote_schema) - else: - self.quote_schema = kwargs.pop('quote_schema', None) - - self.indexes = set() - self.constraints = set() - self._columns = expression.ColumnCollection() - PrimaryKeyConstraint()._set_parent_with_dispatch(self) - self.foreign_keys = set() - self._extra_dependencies = set() - self.kwargs = {} - if self.schema is not None: - self.fullname = "%s.%s" % (self.schema, self.name) - else: - self.fullname = self.name - - autoload = kwargs.pop('autoload', False) - autoload_with = kwargs.pop('autoload_with', None) - # this argument is only used with _init_existing() - kwargs.pop('autoload_replace', True) - include_columns = kwargs.pop('include_columns', None) - - self.implicit_returning = kwargs.pop('implicit_returning', True) - self.quote = kwargs.pop('quote', None) - if 'info' in kwargs: - self.info = kwargs.pop('info') - if 'listeners' in kwargs: - listeners = kwargs.pop('listeners') - for evt, fn in listeners: - event.listen(self, evt, fn) - - self._prefixes = kwargs.pop('prefixes', []) - - self._extra_kwargs(**kwargs) - - # load column definitions from the database if 'autoload' is defined - # we do it after the table is in the singleton dictionary to support - # circular foreign keys - if autoload: - self._autoload(metadata, autoload_with, include_columns) - - # initialize all the column, etc. objects. done after reflection to - # allow user-overrides - self._init_items(*args) - - def _autoload(self, metadata, autoload_with, include_columns, exclude_columns=()): - if self.primary_key.columns: - PrimaryKeyConstraint(*[ - c for c in self.primary_key.columns - if c.key in exclude_columns - ])._set_parent_with_dispatch(self) - - if autoload_with: - autoload_with.run_callable( - autoload_with.dialect.reflecttable, - self, include_columns, exclude_columns - ) - else: - bind = _bind_or_error(metadata, - msg="No engine is bound to this Table's MetaData. " - "Pass an engine to the Table via " - "autoload_with=, " - "or associate the MetaData with an engine via " - "metadata.bind=") - bind.run_callable( - bind.dialect.reflecttable, - self, include_columns, exclude_columns - ) - - @property - def _sorted_constraints(self): - """Return the set of constraints as a list, sorted by creation order.""" - - return sorted(self.constraints, key=lambda c:c._creation_order) - - def _init_existing(self, *args, **kwargs): - autoload = kwargs.pop('autoload', False) - autoload_with = kwargs.pop('autoload_with', None) - autoload_replace = kwargs.pop('autoload_replace', True) - schema = kwargs.pop('schema', None) - if schema and schema != self.schema: - raise exc.ArgumentError( - "Can't change schema of existing table from '%s' to '%s'", - (self.schema, schema)) - - include_columns = kwargs.pop('include_columns', None) - - if include_columns is not None: - for c in self.c: - if c.name not in include_columns: - self._columns.remove(c) - - for key in ('quote', 'quote_schema'): - if key in kwargs: - setattr(self, key, kwargs.pop(key)) - - if 'info' in kwargs: - self.info = kwargs.pop('info') - - if autoload: - if not autoload_replace: - exclude_columns = [c.name for c in self.c] - else: - exclude_columns = () - self._autoload(self.metadata, autoload_with, include_columns, exclude_columns) - - self._extra_kwargs(**kwargs) - self._init_items(*args) - - def _extra_kwargs(self, **kwargs): - # validate remaining kwargs that they all specify DB prefixes - _validate_dialect_kwargs(kwargs, "Table") - self.kwargs.update(kwargs) - - def _init_collections(self): - pass - - @util.memoized_property - def _autoincrement_column(self): - for col in self.primary_key: - if col.autoincrement and \ - col.type._type_affinity is not None and \ - issubclass(col.type._type_affinity, sqltypes.Integer) and \ - (not col.foreign_keys or col.autoincrement == 'ignore_fk') and \ - isinstance(col.default, (type(None), Sequence)) and \ - (col.server_default is None or col.server_default.reflected): - return col - - @property - def key(self): - return _get_table_key(self.name, self.schema) - - def __repr__(self): - return "Table(%s)" % ', '.join( - [repr(self.name)] + [repr(self.metadata)] + - [repr(x) for x in self.columns] + - ["%s=%s" % (k, repr(getattr(self, k))) for k in ['schema']]) - - def __str__(self): - return _get_table_key(self.description, self.schema) - - @property - def bind(self): - """Return the connectable associated with this Table.""" - - return self.metadata and self.metadata.bind or None - - def add_is_dependent_on(self, table): - """Add a 'dependency' for this Table. - - This is another Table object which must be created - first before this one can, or dropped after this one. - - Usually, dependencies between tables are determined via - ForeignKey objects. However, for other situations that - create dependencies outside of foreign keys (rules, inheriting), - this method can manually establish such a link. - - """ - self._extra_dependencies.add(table) - - def append_column(self, column): - """Append a :class:`~.schema.Column` to this :class:`~.schema.Table`. - - The "key" of the newly added :class:`~.schema.Column`, i.e. the - value of its ``.key`` attribute, will then be available - in the ``.c`` collection of this :class:`~.schema.Table`, and the - column definition will be included in any CREATE TABLE, SELECT, - UPDATE, etc. statements generated from this :class:`~.schema.Table` - construct. - - Note that this does **not** change the definition of the table - as it exists within any underlying database, assuming that - table has already been created in the database. Relational - databases support the addition of columns to existing tables - using the SQL ALTER command, which would need to be - emitted for an already-existing table that doesn't contain - the newly added column. - - """ - - column._set_parent_with_dispatch(self) - - def append_constraint(self, constraint): - """Append a :class:`~.schema.Constraint` to this :class:`~.schema.Table`. - - This has the effect of the constraint being included in any - future CREATE TABLE statement, assuming specific DDL creation - events have not been associated with the given :class:`~.schema.Constraint` - object. - - Note that this does **not** produce the constraint within the - relational database automatically, for a table that already exists - in the database. To add a constraint to an - existing relational database table, the SQL ALTER command must - be used. SQLAlchemy also provides the :class:`.AddConstraint` construct - which can produce this SQL when invoked as an executable clause. - - """ - - constraint._set_parent_with_dispatch(self) - - def append_ddl_listener(self, event_name, listener): - """Append a DDL event listener to this ``Table``. - - Deprecated. See :class:`.DDLEvents`. - - """ - - def adapt_listener(target, connection, **kw): - listener(event_name, target, connection) - - event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) - - def _set_parent(self, metadata): - metadata._add_table(self.name, self.schema, self) - self.metadata = metadata - - def get_children(self, column_collections=True, - schema_visitor=False, **kw): - if not schema_visitor: - return expression.TableClause.get_children( - self, column_collections=column_collections, **kw) - else: - if column_collections: - return list(self.columns) - else: - return [] - - def exists(self, bind=None): - """Return True if this table exists.""" - - if bind is None: - bind = _bind_or_error(self) - - return bind.run_callable(bind.dialect.has_table, - self.name, schema=self.schema) - - def create(self, bind=None, checkfirst=False): - """Issue a ``CREATE`` statement for this - :class:`.Table`, using the given :class:`.Connectable` - for connectivity. - - See also :meth:`.MetaData.create_all`. - - """ - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst) - - - def drop(self, bind=None, checkfirst=False): - """Issue a ``DROP`` statement for this - :class:`.Table`, using the given :class:`.Connectable` - for connectivity. - - See also :meth:`.MetaData.drop_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst) - - - def tometadata(self, metadata, schema=RETAIN_SCHEMA): - """Return a copy of this :class:`.Table` associated with a different - :class:`.MetaData`. - - E.g.:: - - # create two metadata - meta1 = MetaData('sqlite:///querytest.db') - meta2 = MetaData() - - # load 'users' from the sqlite engine - users_table = Table('users', meta1, autoload=True) - - # create the same Table object for the plain metadata - users_table_2 = users_table.tometadata(meta2) - - """ - - if schema is RETAIN_SCHEMA: - schema = self.schema - elif schema is None: - schema = metadata.schema - key = _get_table_key(self.name, schema) - if key in metadata.tables: - util.warn("Table '%s' already exists within the given " - "MetaData - not copying." % self.description) - return metadata.tables[key] - - args = [] - for c in self.columns: - args.append(c.copy(schema=schema)) - for c in self.constraints: - args.append(c.copy(schema=schema)) - table = Table( - self.name, metadata, schema=schema, - *args, **self.kwargs - ) - for index in self.indexes: - # skip indexes that would be generated - # by the 'index' flag on Column - if len(index.columns) == 1 and \ - list(index.columns)[0].index: - continue - Index(index.name, - unique=index.unique, - *[table.c[col] for col in index.columns.keys()], - **index.kwargs) - table.dispatch._update(self.dispatch) - return table - -class Column(SchemaItem, expression.ColumnClause): - """Represents a column in a database table.""" - - __visit_name__ = 'column' - - def __init__(self, *args, **kwargs): - """ - Construct a new ``Column`` object. - - :param name: The name of this column as represented in the database. - This argument may be the first positional argument, or specified - via keyword. - - Names which contain no upper case characters - will be treated as case insensitive names, and will not be quoted - unless they are a reserved word. Names with any number of upper - case characters will be quoted and sent exactly. Note that this - behavior applies even for databases which standardize upper - case names as case insensitive such as Oracle. - - The name field may be omitted at construction time and applied - later, at any time before the Column is associated with a - :class:`.Table`. This is to support convenient - usage within the :mod:`~sqlalchemy.ext.declarative` extension. - - :param type\_: The column's type, indicated using an instance which - subclasses :class:`~sqlalchemy.types.TypeEngine`. If no arguments - are required for the type, the class of the type can be sent - as well, e.g.:: - - # use a type with arguments - Column('data', String(50)) - - # use no arguments - Column('level', Integer) - - The ``type`` argument may be the second positional argument - or specified by keyword. - - There is partial support for automatic detection of the - type based on that of a :class:`.ForeignKey` associated - with this column, if the type is specified as ``None``. - However, this feature is not fully implemented and - may not function in all cases. - - :param \*args: Additional positional arguments include various - :class:`.SchemaItem` derived constructs which will be applied - as options to the column. These include instances of - :class:`.Constraint`, :class:`.ForeignKey`, :class:`.ColumnDefault`, - and :class:`.Sequence`. In some cases an equivalent keyword - argument is available such as ``server_default``, ``default`` - and ``unique``. - - :param autoincrement: This flag may be set to ``False`` to - indicate an integer primary key column that should not be - considered to be the "autoincrement" column, that is - the integer primary key column which generates values - implicitly upon INSERT and whose value is usually returned - via the DBAPI cursor.lastrowid attribute. It defaults - to ``True`` to satisfy the common use case of a table - with a single integer primary key column. If the table - has a composite primary key consisting of more than one - integer column, set this flag to True only on the - column that should be considered "autoincrement". - - The setting *only* has an effect for columns which are: - - * Integer derived (i.e. INT, SMALLINT, BIGINT). - - * Part of the primary key - - * Are not referenced by any foreign keys, unless - the value is specified as ``'ignore_fk'`` - - .. versionadded:: 0.7.4 - - * have no server side or client side defaults (with the exception - of Postgresql SERIAL). - - The setting has these two effects on columns that meet the - above criteria: - - * DDL issued for the column will include database-specific - keywords intended to signify this column as an - "autoincrement" column, such as AUTO INCREMENT on MySQL, - SERIAL on Postgresql, and IDENTITY on MS-SQL. It does - *not* issue AUTOINCREMENT for SQLite since this is a - special SQLite flag that is not required for autoincrementing - behavior. See the SQLite dialect documentation for - information on SQLite's AUTOINCREMENT. - - * The column will be considered to be available as - cursor.lastrowid or equivalent, for those dialects which - "post fetch" newly inserted identifiers after a row has - been inserted (SQLite, MySQL, MS-SQL). It does not have - any effect in this regard for databases that use sequences - to generate primary key identifiers (i.e. Firebird, Postgresql, - Oracle). - - .. versionchanged:: 0.7.4 - ``autoincrement`` accepts a special value ``'ignore_fk'`` - to indicate that autoincrementing status regardless of foreign key - references. This applies to certain composite foreign key - setups, such as the one demonstrated in the ORM documentation - at :ref:`post_update`. - - :param default: A scalar, Python callable, or - :class:`~sqlalchemy.sql.expression.ClauseElement` representing the - *default value* for this column, which will be invoked upon insert - if this column is otherwise not specified in the VALUES clause of - the insert. This is a shortcut to using :class:`.ColumnDefault` as - a positional argument. - - Contrast this argument to ``server_default`` which creates a - default generator on the database side. - - :param doc: optional String that can be used by the ORM or similar - to document attributes. This attribute does not render SQL - comments (a future attribute 'comment' will achieve that). - - :param key: An optional string identifier which will identify this - ``Column`` object on the :class:`.Table`. When a key is provided, - this is the only identifier referencing the ``Column`` within the - application, including ORM attribute mapping; the ``name`` field - is used only when rendering SQL. - - :param index: When ``True``, indicates that the column is indexed. - This is a shortcut for using a :class:`.Index` construct on the - table. To specify indexes with explicit names or indexes that - contain multiple columns, use the :class:`.Index` construct - instead. - - :param info: A dictionary which defaults to ``{}``. A space to store - application specific data. This must be a dictionary. - - :param nullable: If set to the default of ``True``, indicates the - column will be rendered as allowing NULL, else it's rendered as - NOT NULL. This parameter is only used when issuing CREATE TABLE - statements. - - :param onupdate: A scalar, Python callable, or - :class:`~sqlalchemy.sql.expression.ClauseElement` representing a - default value to be applied to the column within UPDATE - statements, which wil be invoked upon update if this column is not - present in the SET clause of the update. This is a shortcut to - using :class:`.ColumnDefault` as a positional argument with - ``for_update=True``. - - :param primary_key: If ``True``, marks this column as a primary key - column. Multiple columns can have this flag set to specify - composite primary keys. As an alternative, the primary key of a - :class:`.Table` can be specified via an explicit - :class:`.PrimaryKeyConstraint` object. - - :param server_default: A :class:`.FetchedValue` instance, str, Unicode - or :func:`~sqlalchemy.sql.expression.text` construct representing - the DDL DEFAULT value for the column. - - String types will be emitted as-is, surrounded by single quotes:: - - Column('x', Text, server_default="val") - - x TEXT DEFAULT 'val' - - A :func:`~sqlalchemy.sql.expression.text` expression will be - rendered as-is, without quotes:: - - Column('y', DateTime, server_default=text('NOW()'))0 - - y DATETIME DEFAULT NOW() - - Strings and text() will be converted into a :class:`.DefaultClause` - object upon initialization. - - Use :class:`.FetchedValue` to indicate that an already-existing - column will generate a default value on the database side which - will be available to SQLAlchemy for post-fetch after inserts. This - construct does not specify any DDL and the implementation is left - to the database, such as via a trigger. - - :param server_onupdate: A :class:`.FetchedValue` instance - representing a database-side default generation function. This - indicates to SQLAlchemy that a newly generated value will be - available after updates. This construct does not specify any DDL - and the implementation is left to the database, such as via a - trigger. - - :param quote: Force quoting of this column's name on or off, - corresponding to ``True`` or ``False``. When left at its default - of ``None``, the column identifier will be quoted according to - whether the name is case sensitive (identifiers with at least one - upper case character are treated as case sensitive), or if it's a - reserved word. This flag is only needed to force quoting of a - reserved word which is not known by the SQLAlchemy dialect. - - :param unique: When ``True``, indicates that this column contains a - unique constraint, or if ``index`` is ``True`` as well, indicates - that the :class:`.Index` should be created with the unique flag. - To specify multiple columns in the constraint/index or to specify - an explicit name, use the :class:`.UniqueConstraint` or - :class:`.Index` constructs explicitly. - - """ - - name = kwargs.pop('name', None) - type_ = kwargs.pop('type_', None) - args = list(args) - if args: - if isinstance(args[0], basestring): - if name is not None: - raise exc.ArgumentError( - "May not pass name positionally and as a keyword.") - name = args.pop(0) - if args: - coltype = args[0] - - if (isinstance(coltype, sqltypes.TypeEngine) or - (isinstance(coltype, type) and - issubclass(coltype, sqltypes.TypeEngine))): - if type_ is not None: - raise exc.ArgumentError( - "May not pass type_ positionally and as a keyword.") - type_ = args.pop(0) - - no_type = type_ is None - - super(Column, self).__init__(name, None, type_) - self.key = kwargs.pop('key', name) - self.primary_key = kwargs.pop('primary_key', False) - self.nullable = kwargs.pop('nullable', not self.primary_key) - self.default = kwargs.pop('default', None) - self.server_default = kwargs.pop('server_default', None) - self.server_onupdate = kwargs.pop('server_onupdate', None) - self.index = kwargs.pop('index', None) - self.unique = kwargs.pop('unique', None) - self.quote = kwargs.pop('quote', None) - self.doc = kwargs.pop('doc', None) - self.onupdate = kwargs.pop('onupdate', None) - self.autoincrement = kwargs.pop('autoincrement', True) - self.constraints = set() - self.foreign_keys = set() - - # check if this Column is proxying another column - if '_proxies' in kwargs: - self.proxies = kwargs.pop('_proxies') - # otherwise, add DDL-related events - elif isinstance(self.type, sqltypes.SchemaType): - self.type._set_parent_with_dispatch(self) - - if self.default is not None: - if isinstance(self.default, (ColumnDefault, Sequence)): - args.append(self.default) - else: - if getattr(self.type, '_warn_on_bytestring', False): - # Py3K - #if isinstance(self.default, bytes): - # Py2K - if isinstance(self.default, str): - # end Py2K - util.warn("Unicode column received non-unicode " - "default value.") - args.append(ColumnDefault(self.default)) - - if self.server_default is not None: - if isinstance(self.server_default, FetchedValue): - args.append(self.server_default._as_for_update(False)) - else: - args.append(DefaultClause(self.server_default)) - - if self.onupdate is not None: - if isinstance(self.onupdate, (ColumnDefault, Sequence)): - args.append(self.onupdate) - else: - args.append(ColumnDefault(self.onupdate, for_update=True)) - - if self.server_onupdate is not None: - if isinstance(self.server_onupdate, FetchedValue): - args.append(self.server_onupdate._as_for_update(True)) - else: - args.append(DefaultClause(self.server_onupdate, - for_update=True)) - self._init_items(*args) - - if not self.foreign_keys and no_type: - raise exc.ArgumentError("'type' is required on Column objects " - "which have no foreign keys.") - util.set_creation_order(self) - - if 'info' in kwargs: - self.info = kwargs.pop('info') - - if kwargs: - raise exc.ArgumentError( - "Unknown arguments passed to Column: " + repr(kwargs.keys())) - - def __str__(self): - if self.name is None: - return "(no name)" - elif self.table is not None: - if self.table.named_with_column: - return (self.table.description + "." + self.description) - else: - return self.description - else: - return self.description - - def references(self, column): - """Return True if this Column references the given column via foreign - key.""" - - for fk in self.foreign_keys: - if fk.column.proxy_set.intersection(column.proxy_set): - return True - else: - return False - - def append_foreign_key(self, fk): - fk._set_parent_with_dispatch(self) - - def __repr__(self): - kwarg = [] - if self.key != self.name: - kwarg.append('key') - if self.primary_key: - kwarg.append('primary_key') - if not self.nullable: - kwarg.append('nullable') - if self.onupdate: - kwarg.append('onupdate') - if self.default: - kwarg.append('default') - if self.server_default: - kwarg.append('server_default') - return "Column(%s)" % ', '.join( - [repr(self.name)] + [repr(self.type)] + - [repr(x) for x in self.foreign_keys if x is not None] + - [repr(x) for x in self.constraints] + - [(self.table is not None and "table=<%s>" % - self.table.description or "table=None")] + - ["%s=%s" % (k, repr(getattr(self, k))) for k in kwarg]) - - def _set_parent(self, table): - if not self.name: - raise exc.ArgumentError( - "Column must be constructed with a non-blank name or " - "assign a non-blank .name before adding to a Table.") - if self.key is None: - self.key = self.name - - existing = getattr(self, 'table', None) - if existing is not None and existing is not table: - raise exc.ArgumentError( - "Column object already assigned to Table '%s'" % - existing.description) - - if self.key in table._columns: - col = table._columns.get(self.key) - if col is not self: - for fk in list(col.foreign_keys): - table.foreign_keys.remove(fk) - if fk.constraint in table.constraints: - # this might have been removed - # already, if it's a composite constraint - # and more than one col being replaced - table.constraints.remove(fk.constraint) - - table._columns.replace(self) - - if self.primary_key: - table.primary_key._replace(self) - Table._autoincrement_column._reset(table) - elif self.key in table.primary_key: - raise exc.ArgumentError( - "Trying to redefine primary-key column '%s' as a " - "non-primary-key column on table '%s'" % ( - self.key, table.fullname)) - self.table = table - - if self.index: - if isinstance(self.index, basestring): - raise exc.ArgumentError( - "The 'index' keyword argument on Column is boolean only. " - "To create indexes with a specific name, create an " - "explicit Index object external to the Table.") - Index(expression._truncated_label('ix_%s' % self._label), self, unique=self.unique) - elif self.unique: - if isinstance(self.unique, basestring): - raise exc.ArgumentError( - "The 'unique' keyword argument on Column is boolean " - "only. To create unique constraints or indexes with a " - "specific name, append an explicit UniqueConstraint to " - "the Table's list of elements, or create an explicit " - "Index object external to the Table.") - table.append_constraint(UniqueConstraint(self.key)) - - def _on_table_attach(self, fn): - if self.table is not None: - fn(self, self.table) - event.listen(self, 'after_parent_attach', fn) - - def copy(self, **kw): - """Create a copy of this ``Column``, unitialized. - - This is used in ``Table.tometadata``. - - """ - - # Constraint objects plus non-constraint-bound ForeignKey objects - args = \ - [c.copy(**kw) for c in self.constraints] + \ - [c.copy(**kw) for c in self.foreign_keys if not c.constraint] - - c = self._constructor( - name=self.name, - type_=self.type, - key = self.key, - primary_key = self.primary_key, - nullable = self.nullable, - unique = self.unique, - quote=self.quote, - index=self.index, - autoincrement=self.autoincrement, - default=self.default, - server_default=self.server_default, - onupdate=self.onupdate, - server_onupdate=self.server_onupdate, - info=self.info, - doc=self.doc, - *args - ) - c.dispatch._update(self.dispatch) - return c - - def _make_proxy(self, selectable, name=None): - """Create a *proxy* for this column. - - This is a copy of this ``Column`` referenced by a different parent - (such as an alias or select statement). The column should - be used only in select scenarios, as its full DDL/default - information is not transferred. - - """ - fk = [ForeignKey(f.column) for f in self.foreign_keys] - if name is None and self.name is None: - raise exc.InvalidRequestError("Cannot initialize a sub-selectable" - " with this Column object until it's 'name' has " - "been assigned.") - try: - c = self._constructor( - expression._as_truncated(name or self.name), - self.type, - key = name or self.key, - primary_key = self.primary_key, - nullable = self.nullable, - quote=self.quote, _proxies=[self], *fk) - except TypeError, e: - # Py3K - #raise TypeError( - # "Could not create a copy of this %r object. " - # "Ensure the class includes a _constructor() " - # "attribute or method which accepts the " - # "standard Column constructor arguments, or " - # "references the Column class itself." % self.__class__) from e - # Py2K - raise TypeError( - "Could not create a copy of this %r object. " - "Ensure the class includes a _constructor() " - "attribute or method which accepts the " - "standard Column constructor arguments, or " - "references the Column class itself. " - "Original error: %s" % (self.__class__, e)) - # end Py2K - - c.table = selectable - selectable._columns.add(c) - if selectable._is_clone_of is not None: - c._is_clone_of = selectable._is_clone_of.columns[c.name] - if self.primary_key: - selectable.primary_key.add(c) - c.dispatch.after_parent_attach(c, selectable) - return c - - def get_children(self, schema_visitor=False, **kwargs): - if schema_visitor: - return [x for x in (self.default, self.onupdate) - if x is not None] + \ - list(self.foreign_keys) + list(self.constraints) - else: - return expression.ColumnClause.get_children(self, **kwargs) - - -class ForeignKey(SchemaItem): - """Defines a dependency between two columns. - - ``ForeignKey`` is specified as an argument to a :class:`.Column` object, - e.g.:: - - t = Table("remote_table", metadata, - Column("remote_id", ForeignKey("main_table.id")) - ) - - Note that ``ForeignKey`` is only a marker object that defines - a dependency between two columns. The actual constraint - is in all cases represented by the :class:`.ForeignKeyConstraint` - object. This object will be generated automatically when - a ``ForeignKey`` is associated with a :class:`.Column` which - in turn is associated with a :class:`.Table`. Conversely, - when :class:`.ForeignKeyConstraint` is applied to a :class:`.Table`, - ``ForeignKey`` markers are automatically generated to be - present on each associated :class:`.Column`, which are also - associated with the constraint object. - - Note that you cannot define a "composite" foreign key constraint, - that is a constraint between a grouping of multiple parent/child - columns, using ``ForeignKey`` objects. To define this grouping, - the :class:`.ForeignKeyConstraint` object must be used, and applied - to the :class:`.Table`. The associated ``ForeignKey`` objects - are created automatically. - - The ``ForeignKey`` objects associated with an individual - :class:`.Column` object are available in the `foreign_keys` collection - of that column. - - Further examples of foreign key configuration are in - :ref:`metadata_foreignkeys`. - - """ - - __visit_name__ = 'foreign_key' - - def __init__(self, column, _constraint=None, use_alter=False, name=None, - onupdate=None, ondelete=None, deferrable=None, - schema=None, - initially=None, link_to_name=False): - """ - Construct a column-level FOREIGN KEY. - - The :class:`.ForeignKey` object when constructed generates a - :class:`.ForeignKeyConstraint` which is associated with the parent - :class:`.Table` object's collection of constraints. - - :param column: A single target column for the key relationship. A - :class:`.Column` object or a column name as a string: - ``tablename.columnkey`` or ``schema.tablename.columnkey``. - ``columnkey`` is the ``key`` which has been assigned to the column - (defaults to the column name itself), unless ``link_to_name`` is - ``True`` in which case the rendered name of the column is used. - - .. versionadded:: 0.7.4 - Note that if the schema name is not included, and the underlying - :class:`.MetaData` has a "schema", that value will be used. - - :param name: Optional string. An in-database name for the key if - `constraint` is not provided. - - :param onupdate: Optional string. If set, emit ON UPDATE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param ondelete: Optional string. If set, emit ON DELETE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT - DEFERRABLE when issuing DDL for this constraint. - - :param initially: Optional string. If set, emit INITIALLY when - issuing DDL for this constraint. - - :param link_to_name: if True, the string name given in ``column`` is - the rendered name of the referenced column, not its locally - assigned ``key``. - - :param use_alter: passed to the underlying - :class:`.ForeignKeyConstraint` to indicate the constraint should be - generated/dropped externally from the CREATE TABLE/ DROP TABLE - statement. See that classes' constructor for details. - - """ - - self._colspec = column - - # the linked ForeignKeyConstraint. - # ForeignKey will create this when parent Column - # is attached to a Table, *or* ForeignKeyConstraint - # object passes itself in when creating ForeignKey - # markers. - self.constraint = _constraint - - - self.use_alter = use_alter - self.name = name - self.onupdate = onupdate - self.ondelete = ondelete - self.deferrable = deferrable - self.initially = initially - self.link_to_name = link_to_name - - def __repr__(self): - return "ForeignKey(%r)" % self._get_colspec() - - def copy(self, schema=None): - """Produce a copy of this :class:`.ForeignKey` object. - - The new :class:`.ForeignKey` will not be bound - to any :class:`.Column`. - - This method is usually used by the internal - copy procedures of :class:`.Column`, :class:`.Table`, - and :class:`.MetaData`. - - :param schema: The returned :class:`.ForeignKey` will - reference the original table and column name, qualified - by the given string schema name. - - """ - - fk = ForeignKey( - self._get_colspec(schema=schema), - use_alter=self.use_alter, - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - deferrable=self.deferrable, - initially=self.initially, - link_to_name=self.link_to_name - ) - fk.dispatch._update(self.dispatch) - return fk - - def _get_colspec(self, schema=None): - """Return a string based 'column specification' for this :class:`.ForeignKey`. - - This is usually the equivalent of the string-based "tablename.colname" - argument first passed to the object's constructor. - - """ - if schema: - return schema + "." + self.column.table.name + \ - "." + self.column.key - elif isinstance(self._colspec, basestring): - return self._colspec - elif hasattr(self._colspec, '__clause_element__'): - _column = self._colspec.__clause_element__() - else: - _column = self._colspec - - return "%s.%s" % (_column.table.fullname, _column.key) - - target_fullname = property(_get_colspec) - - def references(self, table): - """Return True if the given :class:`.Table` is referenced by this :class:`.ForeignKey`.""" - - return table.corresponding_column(self.column) is not None - - def get_referent(self, table): - """Return the :class:`.Column` in the given :class:`.Table` - referenced by this :class:`.ForeignKey`. - - Returns None if this :class:`.ForeignKey` does not reference the given - :class:`.Table`. - - """ - - return table.corresponding_column(self.column) - - @util.memoized_property - def column(self): - """Return the target :class:`.Column` referenced by this :class:`.ForeignKey`. - - If this :class:`.ForeignKey` was created using a - string-based target column specification, this - attribute will on first access initiate a resolution - process to locate the referenced remote - :class:`.Column`. The resolution process traverses - to the parent :class:`.Column`, :class:`.Table`, and - :class:`.MetaData` to proceed - if any of these aren't - yet present, an error is raised. - - """ - # ForeignKey inits its remote column as late as possible, so tables - # can be defined without dependencies - if isinstance(self._colspec, basestring): - # locate the parent table this foreign key is attached to. we - # use the "original" column which our parent column represents - # (its a list of columns/other ColumnElements if the parent - # table is a UNION) - for c in self.parent.base_columns: - if isinstance(c, Column): - parenttable = c.table - break - else: - raise exc.ArgumentError( - "Parent column '%s' does not descend from a " - "table-attached Column" % str(self.parent)) - - m = self._colspec.split('.') - - if m is None: - raise exc.ArgumentError( - "Invalid foreign key column specification: %s" % - self._colspec) - - # A FK between column 'bar' and table 'foo' can be - # specified as 'foo', 'foo.bar', 'dbo.foo.bar', - # 'otherdb.dbo.foo.bar'. Once we have the column name and - # the table name, treat everything else as the schema - # name. Some databases (e.g. Sybase) support - # inter-database foreign keys. See tickets#1341 and -- - # indirectly related -- Ticket #594. This assumes that '.' - # will never appear *within* any component of the FK. - - (schema, tname, colname) = (None, None, None) - if schema is None and parenttable.metadata.schema is not None: - schema = parenttable.metadata.schema - - if (len(m) == 1): - tname = m.pop() - else: - colname = m.pop() - tname = m.pop() - - if (len(m) > 0): - schema = '.'.join(m) - - if _get_table_key(tname, schema) not in parenttable.metadata: - raise exc.NoReferencedTableError( - "Foreign key associated with column '%s' could not find " - "table '%s' with which to generate a " - "foreign key to target column '%s'" % (self.parent, tname, colname), - tname) - table = Table(tname, parenttable.metadata, - mustexist=True, schema=schema) - - _column = None - if colname is None: - # colname is None in the case that ForeignKey argument - # was specified as table name only, in which case we - # match the column name to the same column on the - # parent. - key = self.parent - _column = table.c.get(self.parent.key, None) - elif self.link_to_name: - key = colname - for c in table.c: - if c.name == colname: - _column = c - else: - key = colname - _column = table.c.get(colname, None) - - if _column is None: - raise exc.NoReferencedColumnError( - "Could not create ForeignKey '%s' on table '%s': " - "table '%s' has no column named '%s'" % ( - self._colspec, parenttable.name, table.name, key), - table.name, key) - - elif hasattr(self._colspec, '__clause_element__'): - _column = self._colspec.__clause_element__() - else: - _column = self._colspec - - # propagate TypeEngine to parent if it didn't have one - if isinstance(self.parent.type, sqltypes.NullType): - self.parent.type = _column.type - return _column - - def _set_parent(self, column): - if hasattr(self, 'parent'): - if self.parent is column: - return - raise exc.InvalidRequestError( - "This ForeignKey already has a parent !") - self.parent = column - self.parent.foreign_keys.add(self) - self.parent._on_table_attach(self._set_table) - - def _set_table(self, column, table): - # standalone ForeignKey - create ForeignKeyConstraint - # on the hosting Table when attached to the Table. - if self.constraint is None and isinstance(table, Table): - self.constraint = ForeignKeyConstraint( - [], [], use_alter=self.use_alter, name=self.name, - onupdate=self.onupdate, ondelete=self.ondelete, - deferrable=self.deferrable, initially=self.initially, - ) - self.constraint._elements[self.parent] = self - self.constraint._set_parent_with_dispatch(table) - table.foreign_keys.add(self) - -class _NotAColumnExpr(object): - def _not_a_column_expr(self): - raise exc.InvalidRequestError( - "This %s cannot be used directly " - "as a column expression." % self.__class__.__name__) - - __clause_element__ = self_group = lambda self: self._not_a_column_expr() - _from_objects = property(lambda self: self._not_a_column_expr()) - -class DefaultGenerator(_NotAColumnExpr, SchemaItem): - """Base class for column *default* values.""" - - __visit_name__ = 'default_generator' - - is_sequence = False - is_server_default = False - column = None - - def __init__(self, for_update=False): - self.for_update = for_update - - def _set_parent(self, column): - self.column = column - if self.for_update: - self.column.onupdate = self - else: - self.column.default = self - - def execute(self, bind=None, **kwargs): - if bind is None: - bind = _bind_or_error(self) - return bind._execute_default(self, **kwargs) - - @property - def bind(self): - """Return the connectable associated with this default.""" - if getattr(self, 'column', None) is not None: - return self.column.table.bind - else: - return None - - -class ColumnDefault(DefaultGenerator): - """A plain default value on a column. - - This could correspond to a constant, a callable function, - or a SQL clause. - - :class:`.ColumnDefault` is generated automatically - whenever the ``default``, ``onupdate`` arguments of - :class:`.Column` are used. A :class:`.ColumnDefault` - can be passed positionally as well. - - For example, the following:: - - Column('foo', Integer, default=50) - - Is equivalent to:: - - Column('foo', Integer, ColumnDefault(50)) - - - """ - - def __init__(self, arg, **kwargs): - super(ColumnDefault, self).__init__(**kwargs) - if isinstance(arg, FetchedValue): - raise exc.ArgumentError( - "ColumnDefault may not be a server-side default type.") - if util.callable(arg): - arg = self._maybe_wrap_callable(arg) - self.arg = arg - - @util.memoized_property - def is_callable(self): - return util.callable(self.arg) - - @util.memoized_property - def is_clause_element(self): - return isinstance(self.arg, expression.ClauseElement) - - @util.memoized_property - def is_scalar(self): - return not self.is_callable and \ - not self.is_clause_element and \ - not self.is_sequence - - def _maybe_wrap_callable(self, fn): - """Backward compat: Wrap callables that don't accept a context.""" - - if inspect.isfunction(fn): - inspectable = fn - elif inspect.isclass(fn): - inspectable = fn.__init__ - elif hasattr(fn, '__call__'): - inspectable = fn.__call__ - else: - # probably not inspectable, try anyways. - inspectable = fn - try: - argspec = inspect.getargspec(inspectable) - except TypeError: - return lambda ctx: fn() - - positionals = len(argspec[0]) - - # Py3K compat - no unbound methods - if inspect.ismethod(inspectable) or inspect.isclass(fn): - positionals -= 1 - - if positionals == 0: - return lambda ctx: fn() - - defaulted = argspec[3] is not None and len(argspec[3]) or 0 - if positionals - defaulted > 1: - raise exc.ArgumentError( - "ColumnDefault Python function takes zero or one " - "positional arguments") - return fn - - def _visit_name(self): - if self.for_update: - return "column_onupdate" - else: - return "column_default" - __visit_name__ = property(_visit_name) - - def __repr__(self): - return "ColumnDefault(%r)" % self.arg - -class Sequence(DefaultGenerator): - """Represents a named database sequence. - - The :class:`.Sequence` object represents the name and configurational - parameters of a database sequence. It also represents - a construct that can be "executed" by a SQLAlchemy :class:`.Engine` - or :class:`.Connection`, rendering the appropriate "next value" function - for the target database and returning a result. - - The :class:`.Sequence` is typically associated with a primary key column:: - - some_table = Table('some_table', metadata, - Column('id', Integer, Sequence('some_table_seq'), primary_key=True) - ) - - When CREATE TABLE is emitted for the above :class:`.Table`, if the - target platform supports sequences, a CREATE SEQUENCE statement will - be emitted as well. For platforms that don't support sequences, - the :class:`.Sequence` construct is ignored. - - See also: :class:`.CreateSequence` :class:`.DropSequence` - - """ - - __visit_name__ = 'sequence' - - is_sequence = True - - def __init__(self, name, start=None, increment=None, schema=None, - optional=False, quote=None, metadata=None, - quote_schema=None, - for_update=False): - """Construct a :class:`.Sequence` object. - - :param name: The name of the sequence. - :param start: the starting index of the sequence. This value is - used when the CREATE SEQUENCE command is emitted to the database - as the value of the "START WITH" clause. If ``None``, the - clause is omitted, which on most platforms indicates a starting - value of 1. - :param increment: the increment value of the sequence. This - value is used when the CREATE SEQUENCE command is emitted to - the database as the value of the "INCREMENT BY" clause. If ``None``, - the clause is omitted, which on most platforms indicates an - increment of 1. - :param schema: Optional schema name for the sequence, if located - in a schema other than the default. - :param optional: boolean value, when ``True``, indicates that this - :class:`.Sequence` object only needs to be explicitly generated - on backends that don't provide another way to generate primary - key identifiers. Currently, it essentially means, "don't create - this sequence on the Postgresql backend, where the SERIAL keyword - creates a sequence for us automatically". - :param quote: boolean value, when ``True`` or ``False``, explicitly - forces quoting of the schema name on or off. When left at its - default of ``None``, normal quoting rules based on casing and reserved - words take place. - :param metadata: optional :class:`.MetaData` object which will be - associated with this :class:`.Sequence`. A :class:`.Sequence` - that is associated with a :class:`.MetaData` gains access to the - ``bind`` of that :class:`.MetaData`, meaning the :meth:`.Sequence.create` - and :meth:`.Sequence.drop` methods will make usage of that engine - automatically. - - .. versionchanged:: 0.7 - Additionally, the appropriate CREATE SEQUENCE/ - DROP SEQUENCE DDL commands will be emitted corresponding to this - :class:`.Sequence` when :meth:`.MetaData.create_all` and - :meth:`.MetaData.drop_all` are invoked. - - Note that when a :class:`.Sequence` is applied to a :class:`.Column`, - the :class:`.Sequence` is automatically associated with the - :class:`.MetaData` object of that column's parent :class:`.Table`, - when that association is made. The :class:`.Sequence` will then - be subject to automatic CREATE SEQUENCE/DROP SEQUENCE corresponding - to when the :class:`.Table` object itself is created or dropped, - rather than that of the :class:`.MetaData` object overall. - :param for_update: Indicates this :class:`.Sequence`, when associated - with a :class:`.Column`, should be invoked for UPDATE statements - on that column's table, rather than for INSERT statements, when - no value is otherwise present for that column in the statement. - - """ - super(Sequence, self).__init__(for_update=for_update) - self.name = name - self.start = start - self.increment = increment - self.optional = optional - self.quote = quote - if metadata is not None and schema is None and metadata.schema: - self.schema = schema = metadata.schema - self.quote_schema = metadata.quote_schema - else: - self.schema = schema - self.quote_schema = quote_schema - self.metadata = metadata - self._key = _get_table_key(name, schema) - if metadata: - self._set_metadata(metadata) - - @util.memoized_property - def is_callable(self): - return False - - @util.memoized_property - def is_clause_element(self): - return False - - def next_value(self): - """Return a :class:`.next_value` function element - which will render the appropriate increment function - for this :class:`.Sequence` within any SQL expression. - - """ - return expression.func.next_value(self, bind=self.bind) - - def _set_parent(self, column): - super(Sequence, self)._set_parent(column) - column._on_table_attach(self._set_table) - - def _set_table(self, column, table): - self._set_metadata(table.metadata) - - def _set_metadata(self, metadata): - self.metadata = metadata - self.metadata._sequences[self._key] = self - - @property - def bind(self): - if self.metadata: - return self.metadata.bind - else: - return None - - def create(self, bind=None, checkfirst=True): - """Creates this sequence in the database.""" - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst) - - def drop(self, bind=None, checkfirst=True): - """Drops this sequence from the database.""" - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst) - - def _not_a_column_expr(self): - raise exc.InvalidRequestError( - "This %s cannot be used directly " - "as a column expression. Use func.next_value(sequence) " - "to produce a 'next value' function that's usable " - "as a column element." - % self.__class__.__name__) - - -class FetchedValue(_NotAColumnExpr, events.SchemaEventTarget): - """A marker for a transparent database-side default. - - Use :class:`.FetchedValue` when the database is configured - to provide some automatic default for a column. - - E.g.:: - - Column('foo', Integer, FetchedValue()) - - Would indicate that some trigger or default generator - will create a new value for the ``foo`` column during an - INSERT. - - """ - is_server_default = True - reflected = False - has_argument = False - - def __init__(self, for_update=False): - self.for_update = for_update - - def _as_for_update(self, for_update): - if for_update == self.for_update: - return self - else: - return self._clone(for_update) - - def _clone(self, for_update): - n = self.__class__.__new__(self.__class__) - n.__dict__.update(self.__dict__) - n.__dict__.pop('column', None) - n.for_update = for_update - return n - - def _set_parent(self, column): - self.column = column - if self.for_update: - self.column.server_onupdate = self - else: - self.column.server_default = self - - def __repr__(self): - return util.generic_repr(self) - -class DefaultClause(FetchedValue): - """A DDL-specified DEFAULT column value. - - :class:`.DefaultClause` is a :class:`.FetchedValue` - that also generates a "DEFAULT" clause when - "CREATE TABLE" is emitted. - - :class:`.DefaultClause` is generated automatically - whenever the ``server_default``, ``server_onupdate`` arguments of - :class:`.Column` are used. A :class:`.DefaultClause` - can be passed positionally as well. - - For example, the following:: - - Column('foo', Integer, server_default="50") - - Is equivalent to:: - - Column('foo', Integer, DefaultClause("50")) - - """ - - has_argument = True - - def __init__(self, arg, for_update=False, _reflected=False): - util.assert_arg_type(arg, (basestring, - expression.ClauseElement, - expression._TextClause), 'arg') - super(DefaultClause, self).__init__(for_update) - self.arg = arg - self.reflected = _reflected - - def __repr__(self): - return "DefaultClause(%r, for_update=%r)" % \ - (self.arg, self.for_update) - -class PassiveDefault(DefaultClause): - """A DDL-specified DEFAULT column value. - - .. deprecated:: 0.6 - :class:`.PassiveDefault` is deprecated. - Use :class:`.DefaultClause`. - """ - @util.deprecated("0.6", - ":class:`.PassiveDefault` is deprecated. " - "Use :class:`.DefaultClause`.", - False) - def __init__(self, *arg, **kw): - DefaultClause.__init__(self, *arg, **kw) - -class Constraint(SchemaItem): - """A table-level SQL constraint.""" - - __visit_name__ = 'constraint' - - def __init__(self, name=None, deferrable=None, initially=None, - _create_rule=None, - **kw): - """Create a SQL constraint. - - :param name: - Optional, the in-database name of this ``Constraint``. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - :param _create_rule: - a callable which is passed the DDLCompiler object during - compilation. Returns True or False to signal inline generation of - this Constraint. - - The AddConstraint and DropConstraint DDL constructs provide - DDLElement's more comprehensive "conditional DDL" approach that is - passed a database connection when DDL is being issued. _create_rule - is instead called during any CREATE TABLE compilation, where there - may not be any transaction/connection in progress. However, it - allows conditional compilation of the constraint even for backends - which do not support addition of constraints through ALTER TABLE, - which currently includes SQLite. - - _create_rule is used by some types to create constraints. - Currently, its call signature is subject to change at any time. - - :param \**kwargs: - Dialect-specific keyword parameters, see the documentation - for various dialects and constraints regarding options here. - - """ - - self.name = name - self.deferrable = deferrable - self.initially = initially - self._create_rule = _create_rule - util.set_creation_order(self) - _validate_dialect_kwargs(kw, self.__class__.__name__) - self.kwargs = kw - - @property - def table(self): - try: - if isinstance(self.parent, Table): - return self.parent - except AttributeError: - pass - raise exc.InvalidRequestError( - "This constraint is not bound to a table. Did you " - "mean to call table.add_constraint(constraint) ?") - - def _set_parent(self, parent): - self.parent = parent - parent.constraints.add(self) - - def copy(self, **kw): - raise NotImplementedError() - -class ColumnCollectionMixin(object): - def __init__(self, *columns): - self.columns = expression.ColumnCollection() - self._pending_colargs = [_to_schema_column_or_string(c) - for c in columns] - if self._pending_colargs and \ - isinstance(self._pending_colargs[0], Column) and \ - self._pending_colargs[0].table is not None: - self._set_parent_with_dispatch(self._pending_colargs[0].table) - - def _set_parent(self, table): - for col in self._pending_colargs: - if isinstance(col, basestring): - col = table.c[col] - self.columns.add(col) - -class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): - """A constraint that proxies a ColumnCollection.""" - - def __init__(self, *columns, **kw): - """ - :param \*columns: - A sequence of column names or Column objects. - - :param name: - Optional, the in-database name of this constraint. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - """ - ColumnCollectionMixin.__init__(self, *columns) - Constraint.__init__(self, **kw) - - def _set_parent(self, table): - ColumnCollectionMixin._set_parent(self, table) - Constraint._set_parent(self, table) - - def __contains__(self, x): - return x in self.columns - - def copy(self, **kw): - c = self.__class__(name=self.name, deferrable=self.deferrable, - initially=self.initially, *self.columns.keys()) - c.dispatch._update(self.dispatch) - return c - - def contains_column(self, col): - return self.columns.contains_column(col) - - def __iter__(self): - # inlining of - # return iter(self.columns) - # ColumnCollection->OrderedProperties->OrderedDict - ordered_dict = self.columns._data - return (ordered_dict[key] for key in ordered_dict._list) - - def __len__(self): - return len(self.columns._data) - - -class CheckConstraint(Constraint): - """A table- or column-level CHECK constraint. - - Can be included in the definition of a Table or Column. - """ - - def __init__(self, sqltext, name=None, deferrable=None, - initially=None, table=None, _create_rule=None): - """Construct a CHECK constraint. - - :param sqltext: - A string containing the constraint definition, which will be used - verbatim, or a SQL expression construct. - - :param name: - Optional, the in-database name of the constraint. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - """ - - super(CheckConstraint, self).\ - __init__(name, deferrable, initially, _create_rule) - self.sqltext = expression._literal_as_text(sqltext) - if table is not None: - self._set_parent_with_dispatch(table) - - def __visit_name__(self): - if isinstance(self.parent, Table): - return "check_constraint" - else: - return "column_check_constraint" - __visit_name__ = property(__visit_name__) - - def copy(self, **kw): - c = CheckConstraint(self.sqltext, - name=self.name, - initially=self.initially, - deferrable=self.deferrable, - _create_rule=self._create_rule) - c.dispatch._update(self.dispatch) - return c - -class ForeignKeyConstraint(Constraint): - """A table-level FOREIGN KEY constraint. - - Defines a single column or composite FOREIGN KEY ... REFERENCES - constraint. For a no-frills, single column foreign key, adding a - :class:`.ForeignKey` to the definition of a :class:`.Column` is a shorthand - equivalent for an unnamed, single column :class:`.ForeignKeyConstraint`. - - Examples of foreign key configuration are in :ref:`metadata_foreignkeys`. - - """ - __visit_name__ = 'foreign_key_constraint' - - def __init__(self, columns, refcolumns, name=None, onupdate=None, - ondelete=None, deferrable=None, initially=None, use_alter=False, - link_to_name=False, table=None): - """Construct a composite-capable FOREIGN KEY. - - :param columns: A sequence of local column names. The named columns - must be defined and present in the parent Table. The names should - match the ``key`` given to each column (defaults to the name) unless - ``link_to_name`` is True. - - :param refcolumns: A sequence of foreign column names or Column - objects. The columns must all be located within the same Table. - - :param name: Optional, the in-database name of the key. - - :param onupdate: Optional string. If set, emit ON UPDATE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param ondelete: Optional string. If set, emit ON DELETE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT - DEFERRABLE when issuing DDL for this constraint. - - :param initially: Optional string. If set, emit INITIALLY when - issuing DDL for this constraint. - - :param link_to_name: if True, the string name given in ``column`` is - the rendered name of the referenced column, not its locally assigned - ``key``. - - :param use_alter: If True, do not emit the DDL for this constraint as - part of the CREATE TABLE definition. Instead, generate it via an - ALTER TABLE statement issued after the full collection of tables - have been created, and drop it via an ALTER TABLE statement before - the full collection of tables are dropped. This is shorthand for the - usage of :class:`.AddConstraint` and :class:`.DropConstraint` applied - as "after-create" and "before-drop" events on the MetaData object. - This is normally used to generate/drop constraints on objects that - are mutually dependent on each other. - - """ - super(ForeignKeyConstraint, self).\ - __init__(name, deferrable, initially) - - self.onupdate = onupdate - self.ondelete = ondelete - self.link_to_name = link_to_name - if self.name is None and use_alter: - raise exc.ArgumentError("Alterable Constraint requires a name") - self.use_alter = use_alter - - self._elements = util.OrderedDict() - - # standalone ForeignKeyConstraint - create - # associated ForeignKey objects which will be applied to hosted - # Column objects (in col.foreign_keys), either now or when attached - # to the Table for string-specified names - for col, refcol in zip(columns, refcolumns): - self._elements[col] = ForeignKey( - refcol, - _constraint=self, - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - use_alter=self.use_alter, - link_to_name=self.link_to_name - ) - - if table is not None: - self._set_parent_with_dispatch(table) - - @property - def columns(self): - return self._elements.keys() - - @property - def elements(self): - return self._elements.values() - - def _set_parent(self, table): - super(ForeignKeyConstraint, self)._set_parent(table) - for col, fk in self._elements.iteritems(): - # string-specified column names now get - # resolved to Column objects - if isinstance(col, basestring): - try: - col = table.c[col] - except KeyError: - raise exc.ArgumentError( - "Can't create ForeignKeyConstraint " - "on table '%s': no column " - "named '%s' is present." % (table.description, col)) - - if not hasattr(fk, 'parent') or \ - fk.parent is not col: - fk._set_parent_with_dispatch(col) - - if self.use_alter: - def supports_alter(ddl, event, schema_item, bind, **kw): - return table in set(kw['tables']) and \ - bind.dialect.supports_alter - - event.listen(table.metadata, "after_create", AddConstraint(self, on=supports_alter)) - event.listen(table.metadata, "before_drop", DropConstraint(self, on=supports_alter)) - - - def copy(self, **kw): - fkc = ForeignKeyConstraint( - [x.parent.key for x in self._elements.values()], - [x._get_colspec(**kw) for x in self._elements.values()], - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - use_alter=self.use_alter, - deferrable=self.deferrable, - initially=self.initially, - link_to_name=self.link_to_name - ) - fkc.dispatch._update(self.dispatch) - return fkc - -class PrimaryKeyConstraint(ColumnCollectionConstraint): - """A table-level PRIMARY KEY constraint. - - Defines a single column or composite PRIMARY KEY constraint. For a - no-frills primary key, adding ``primary_key=True`` to one or more - ``Column`` definitions is a shorthand equivalent for an unnamed single- or - multiple-column PrimaryKeyConstraint. - """ - - __visit_name__ = 'primary_key_constraint' - - def _set_parent(self, table): - super(PrimaryKeyConstraint, self)._set_parent(table) - - if table.primary_key in table.constraints: - table.constraints.remove(table.primary_key) - table.primary_key = self - table.constraints.add(self) - - for c in self.columns: - c.primary_key = True - - def _replace(self, col): - self.columns.replace(col) - -class UniqueConstraint(ColumnCollectionConstraint): - """A table-level UNIQUE constraint. - - Defines a single column or composite UNIQUE constraint. For a no-frills, - single column constraint, adding ``unique=True`` to the ``Column`` - definition is a shorthand equivalent for an unnamed, single column - UniqueConstraint. - """ - - __visit_name__ = 'unique_constraint' - -class Index(ColumnCollectionMixin, SchemaItem): - """A table-level INDEX. - - Defines a composite (one or more column) INDEX. For a no-frills, single - column index, adding ``index=True`` to the ``Column`` definition is - a shorthand equivalent for an unnamed, single column :class:`.Index`. - - See also: - - :ref:`schema_indexes` - General information on :class:`.Index`. - - :ref:`postgresql_indexes` - PostgreSQL-specific options available for the :class:`.Index` construct. - - :ref:`mysql_indexes` - MySQL-specific options available for the :class:`.Index` construct. - """ - - __visit_name__ = 'index' - - def __init__(self, name, *columns, **kw): - """Construct an index object. - - :param name: - The name of the index - - :param \*columns: - Columns to include in the index. All columns must belong to the same - table. - - :param unique: - Defaults to False: create a unique index. - - :param \**kw: - Other keyword arguments may be interpreted by specific dialects. - - """ - self.table = None - # will call _set_parent() if table-bound column - # objects are present - ColumnCollectionMixin.__init__(self, *columns) - self.name = name - self.unique = kw.pop('unique', False) - self.kwargs = kw - - def _set_parent(self, table): - ColumnCollectionMixin._set_parent(self, table) - - if self.table is not None and table is not self.table: - raise exc.ArgumentError( - "Index '%s' is against table '%s', and " - "cannot be associated with table '%s'." % ( - self.name, - self.table.description, - table.description - ) - ) - self.table = table - for c in self.columns: - if c.table != self.table: - raise exc.ArgumentError( - "Column '%s' is not part of table '%s'." % - (c, self.table.description) - ) - table.indexes.add(self) - - @property - def bind(self): - """Return the connectable associated with this Index.""" - - return self.table.bind - - def create(self, bind=None): - """Issue a ``CREATE`` statement for this - :class:`.Index`, using the given :class:`.Connectable` - for connectivity. - - See also :meth:`.MetaData.create_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, self) - return self - - def drop(self, bind=None): - """Issue a ``DROP`` statement for this - :class:`.Index`, using the given :class:`.Connectable` - for connectivity. - - See also :meth:`.MetaData.drop_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, self) - - def __repr__(self): - return 'Index(%s)' % ( - ", ".join( - [repr(self.name)] + - [repr(c) for c in self.columns] + - (self.unique and ["unique=True"] or []) - )) - -class MetaData(SchemaItem): - """A collection of :class:`.Table` objects and their associated schema constructs. - - Holds a collection of :class:`.Table` objects as well as - an optional binding to an :class:`.Engine` or - :class:`.Connection`. If bound, the :class:`.Table` objects - in the collection and their columns may participate in implicit SQL - execution. - - The :class:`.Table` objects themselves are stored in the ``metadata.tables`` - dictionary. - - The ``bind`` property may be assigned to dynamically. A common pattern is - to start unbound and then bind later when an engine is available:: - - metadata = MetaData() - # define tables - Table('mytable', metadata, ...) - # connect to an engine later, perhaps after loading a URL from a - # configuration file - metadata.bind = an_engine - - MetaData is a thread-safe object after tables have been explicitly defined - or loaded via reflection. - - See also: - - :ref:`metadata_describing` - Introduction to database metadata - - .. index:: - single: thread safety; MetaData - - """ - - __visit_name__ = 'metadata' - - def __init__(self, bind=None, reflect=False, schema=None, quote_schema=None): - """Create a new MetaData object. - - :param bind: - An Engine or Connection to bind to. May also be a string or URL - instance, these are passed to create_engine() and this MetaData will - be bound to the resulting engine. - - :param reflect: - Optional, automatically load all tables from the bound database. - Defaults to False. ``bind`` is required when this option is set. - For finer control over loaded tables, use the ``reflect`` method of - ``MetaData``. - - :param schema: - The default schema to use for the :class:`.Table`, :class:`.Sequence`, and other - objects associated with this :class:`.MetaData`. - Defaults to ``None``. - - :param quote_schema: - Sets the ``quote_schema`` flag for those :class:`.Table`, :class:`.Sequence`, - and other objects which make usage of the local ``schema`` name. - - .. versionadded:: 0.7.4 - ``schema`` and ``quote_schema`` parameters. - - """ - self.tables = util.immutabledict() - self.schema = schema - self.quote_schema = quote_schema - self._schemas = set() - self._sequences = {} - self.bind = bind - if reflect: - if not bind: - raise exc.ArgumentError( - "A bind must be supplied in conjunction " - "with reflect=True") - self.reflect() - - def __repr__(self): - return 'MetaData(bind=%r)' % self.bind - - def __contains__(self, table_or_key): - if not isinstance(table_or_key, basestring): - table_or_key = table_or_key.key - return table_or_key in self.tables - - def _add_table(self, name, schema, table): - key = _get_table_key(name, schema) - dict.__setitem__(self.tables, key, table) - if schema: - self._schemas.add(schema) - - def _remove_table(self, name, schema): - key = _get_table_key(name, schema) - dict.pop(self.tables, key, None) - if self._schemas: - self._schemas = set([t.schema - for t in self.tables.values() - if t.schema is not None]) - - def __getstate__(self): - return {'tables': self.tables, 'schema':self.schema, - 'quote_schema':self.quote_schema, - 'schemas':self._schemas, - 'sequences':self._sequences} - - def __setstate__(self, state): - self.tables = state['tables'] - self.schema = state['schema'] - self.quote_schema = state['quote_schema'] - self._bind = None - self._sequences = state['sequences'] - self._schemas = state['schemas'] - - def is_bound(self): - """True if this MetaData is bound to an Engine or Connection.""" - - return self._bind is not None - - def bind(self): - """An :class:`.Engine` or :class:`.Connection` to which this - :class:`.MetaData` is bound. - - Typically, a :class:`.Engine` is assigned to this attribute - so that "implicit execution" may be used, or alternatively - as a means of providing engine binding information to an - ORM :class:`.Session` object:: - - engine = create_engine("someurl://") - metadata.bind = engine - - .. seealso:: - - :ref:`dbengine_implicit` - background on "bound metadata" - - """ - return self._bind - - def _bind_to(self, bind): - """Bind this MetaData to an Engine, Connection, string or URL.""" - - if isinstance(bind, (basestring, url.URL)): - from sqlalchemy import create_engine - self._bind = create_engine(bind) - else: - self._bind = bind - bind = property(bind, _bind_to) - - def clear(self): - """Clear all Table objects from this MetaData.""" - - dict.clear(self.tables) - self._schemas.clear() - - def remove(self, table): - """Remove the given Table object from this MetaData.""" - - self._remove_table(table.name, table.schema) - - @property - def sorted_tables(self): - """Returns a list of ``Table`` objects sorted in order of - dependency. - """ - return sqlutil.sort_tables(self.tables.itervalues()) - - def reflect(self, bind=None, schema=None, views=False, only=None): - """Load all available table definitions from the database. - - Automatically creates ``Table`` entries in this ``MetaData`` for any - table available in the database but not yet present in the - ``MetaData``. May be called multiple times to pick up tables recently - added to the database, however no special action is taken if a table - in this ``MetaData`` no longer exists in the database. - - :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. - - :param schema: - Optional, query and reflect tables from an alterate schema. - If None, the schema associated with this :class:`.MetaData` - is used, if any. - - :param views: - If True, also reflect views. - - :param only: - Optional. Load only a sub-set of available named tables. May be - specified as a sequence of names or a callable. - - If a sequence of names is provided, only those tables will be - reflected. An error is raised if a table is requested but not - available. Named tables already present in this ``MetaData`` are - ignored. - - If a callable is provided, it will be used as a boolean predicate to - filter the list of potential table names. The callable is called - with a table name and this ``MetaData`` instance as positional - arguments and should return a true value for any table to reflect. - - """ - if bind is None: - bind = _bind_or_error(self) - - if bind.engine is not bind: - conn = bind - close = False - else: - conn = bind.contextual_connect() - close = True - - reflect_opts = { - 'autoload': True, - 'autoload_with': bind - } - - if schema is None: - schema = self.schema - - if schema is not None: - reflect_opts['schema'] = schema - - try: - available = util.OrderedSet(bind.engine.table_names(schema, - connection=conn)) - if views: - available.update( - bind.dialect.get_view_names(conn, schema) - ) - - current = set(self.tables.iterkeys()) - - if only is None: - load = [name for name in available if name not in current] - elif util.callable(only): - load = [name for name in available - if name not in current and only(name, self)] - else: - missing = [name for name in only if name not in available] - if missing: - s = schema and (" schema '%s'" % schema) or '' - raise exc.InvalidRequestError( - 'Could not reflect: requested table(s) not available ' - 'in %s%s: (%s)' % - (bind.engine.url, s, ', '.join(missing))) - load = [name for name in only if name not in current] - - for name in load: - Table(name, self, **reflect_opts) - finally: - if close: - conn.close() - - def append_ddl_listener(self, event_name, listener): - """Append a DDL event listener to this ``MetaData``. - - Deprecated. See :class:`.DDLEvents`. - - """ - def adapt_listener(target, connection, **kw): - tables = kw['tables'] - listener(event, target, connection, tables=tables) - - event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) - - def create_all(self, bind=None, tables=None, checkfirst=True): - """Create all tables stored in this metadata. - - Conditional by default, will not attempt to recreate tables already - present in the target database. - - :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. - - :param tables: - Optional list of ``Table`` objects, which is a subset of the total - tables in the ``MetaData`` (others are ignored). - - :param checkfirst: - Defaults to True, don't issue CREATEs for tables already present - in the target database. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst, - tables=tables) - - def drop_all(self, bind=None, tables=None, checkfirst=True): - """Drop all tables stored in this metadata. - - Conditional by default, will not attempt to drop tables not present in - the target database. - - :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. - - :param tables: - Optional list of ``Table`` objects, which is a subset of the - total tables in the ``MetaData`` (others are ignored). - - :param checkfirst: - Defaults to True, only issue DROPs for tables confirmed to be - present in the target database. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst, - tables=tables) - -class ThreadLocalMetaData(MetaData): - """A MetaData variant that presents a different ``bind`` in every thread. - - Makes the ``bind`` property of the MetaData a thread-local value, allowing - this collection of tables to be bound to different ``Engine`` - implementations or connections in each thread. - - The ThreadLocalMetaData starts off bound to None in each thread. Binds - must be made explicitly by assigning to the ``bind`` property or using - ``connect()``. You can also re-bind dynamically multiple times per - thread, just like a regular ``MetaData``. - - """ - - __visit_name__ = 'metadata' - - def __init__(self): - """Construct a ThreadLocalMetaData.""" - - self.context = util.threading.local() - self.__engines = {} - super(ThreadLocalMetaData, self).__init__() - - def bind(self): - """The bound Engine or Connection for this thread. - - This property may be assigned an Engine or Connection, or assigned a - string or URL to automatically create a basic Engine for this bind - with ``create_engine()``.""" - - return getattr(self.context, '_engine', None) - - def _bind_to(self, bind): - """Bind to a Connectable in the caller's thread.""" - - if isinstance(bind, (basestring, url.URL)): - try: - self.context._engine = self.__engines[bind] - except KeyError: - from sqlalchemy import create_engine - e = create_engine(bind) - self.__engines[bind] = e - self.context._engine = e - else: - # TODO: this is squirrely. we shouldnt have to hold onto engines - # in a case like this - if bind not in self.__engines: - self.__engines[bind] = bind - self.context._engine = bind - - bind = property(bind, _bind_to) - - def is_bound(self): - """True if there is a bind for this thread.""" - return (hasattr(self.context, '_engine') and - self.context._engine is not None) - - def dispose(self): - """Dispose all bound engines, in all thread contexts.""" - - for e in self.__engines.itervalues(): - if hasattr(e, 'dispose'): - e.dispose() - -class SchemaVisitor(visitors.ClauseVisitor): - """Define the visiting for ``SchemaItem`` objects.""" - - __traverse_options__ = {'schema_visitor':True} - - -class DDLElement(expression.Executable, expression.ClauseElement): - """Base class for DDL expression constructs. - - This class is the base for the general purpose :class:`.DDL` class, - as well as the various create/drop clause constructs such as - :class:`.CreateTable`, :class:`.DropTable`, :class:`.AddConstraint`, - etc. - - :class:`.DDLElement` integrates closely with SQLAlchemy events, - introduced in :ref:`event_toplevel`. An instance of one is - itself an event receiving callable:: - - event.listen( - users, - 'after_create', - AddConstraint(constraint).execute_if(dialect='postgresql') - ) - - See also: - - :class:`.DDL` - - :class:`.DDLEvents` - - :ref:`event_toplevel` - - :ref:`schema_ddl_sequences` - - """ - - _execution_options = expression.Executable.\ - _execution_options.union({'autocommit':True}) - - target = None - on = None - dialect = None - callable_ = None - - def execute(self, bind=None, target=None): - """Execute this DDL immediately. - - Executes the DDL statement in isolation using the supplied - :class:`~sqlalchemy.engine.base.Connectable` or - :class:`~sqlalchemy.engine.base.Connectable` assigned to the ``.bind`` - property, if not supplied. If the DDL has a conditional ``on`` - criteria, it will be invoked with None as the event. - - :param bind: - Optional, an ``Engine`` or ``Connection``. If not supplied, a valid - :class:`~sqlalchemy.engine.base.Connectable` must be present in the - ``.bind`` property. - - :param target: - Optional, defaults to None. The target SchemaItem for the - execute call. Will be passed to the ``on`` callable if any, - and may also provide string expansion data for the - statement. See ``execute_at`` for more information. - - """ - - if bind is None: - bind = _bind_or_error(self) - - if self._should_execute(target, bind): - return bind.execute(self.against(target)) - else: - bind.engine.logger.info( - "DDL execution skipped, criteria not met.") - - @util.deprecated("0.7", "See :class:`.DDLEvents`, as well as " - ":meth:`.DDLElement.execute_if`.") - def execute_at(self, event_name, target): - """Link execution of this DDL to the DDL lifecycle of a SchemaItem. - - Links this ``DDLElement`` to a ``Table`` or ``MetaData`` instance, - executing it when that schema item is created or dropped. The DDL - statement will be executed using the same Connection and transactional - context as the Table create/drop itself. The ``.bind`` property of - this statement is ignored. - - :param event: - One of the events defined in the schema item's ``.ddl_events``; - e.g. 'before-create', 'after-create', 'before-drop' or 'after-drop' - - :param target: - The Table or MetaData instance for which this DDLElement will - be associated with. - - A DDLElement instance can be linked to any number of schema items. - - ``execute_at`` builds on the ``append_ddl_listener`` interface of - :class:`.MetaData` and :class:`.Table` objects. - - Caveat: Creating or dropping a Table in isolation will also trigger - any DDL set to ``execute_at`` that Table's MetaData. This may change - in a future release. - - """ - - def call_event(target, connection, **kw): - if self._should_execute_deprecated(event_name, - target, connection, **kw): - return connection.execute(self.against(target)) - - event.listen(target, "" + event_name.replace('-', '_'), call_event) - - @expression._generative - def against(self, target): - """Return a copy of this DDL against a specific schema item.""" - - self.target = target - - @expression._generative - def execute_if(self, dialect=None, callable_=None, state=None): - """Return a callable that will execute this - DDLElement conditionally. - - Used to provide a wrapper for event listening:: - - event.listen( - metadata, - 'before_create', - DDL("my_ddl").execute_if(dialect='postgresql') - ) - - :param dialect: May be a string, tuple or a callable - predicate. If a string, it will be compared to the name of the - executing database dialect:: - - DDL('something').execute_if(dialect='postgresql') - - If a tuple, specifies multiple dialect names:: - - DDL('something').execute_if(dialect=('postgresql', 'mysql')) - - :param callable_: A callable, which will be invoked with - four positional arguments as well as optional keyword - arguments: - - :ddl: - This DDL element. - - :target: - The :class:`.Table` or :class:`.MetaData` object which is the target of - this event. May be None if the DDL is executed explicitly. - - :bind: - The :class:`.Connection` being used for DDL execution - - :tables: - Optional keyword argument - a list of Table objects which are to - be created/ dropped within a MetaData.create_all() or drop_all() - method call. - - :state: - Optional keyword argument - will be the ``state`` argument - passed to this function. - - :checkfirst: - Keyword argument, will be True if the 'checkfirst' flag was - set during the call to ``create()``, ``create_all()``, - ``drop()``, ``drop_all()``. - - If the callable returns a true value, the DDL statement will be - executed. - - :param state: any value which will be passed to the callable_ - as the ``state`` keyword argument. - - See also: - - :class:`.DDLEvents` - - :ref:`event_toplevel` - - """ - self.dialect = dialect - self.callable_ = callable_ - self.state = state - - def _should_execute(self, target, bind, **kw): - if self.on is not None and \ - not self._should_execute_deprecated(None, target, bind, **kw): - return False - - if isinstance(self.dialect, basestring): - if self.dialect != bind.engine.name: - return False - elif isinstance(self.dialect, (tuple, list, set)): - if bind.engine.name not in self.dialect: - return False - if self.callable_ is not None and \ - not self.callable_(self, target, bind, state=self.state, **kw): - return False - - return True - - def _should_execute_deprecated(self, event, target, bind, **kw): - if self.on is None: - return True - elif isinstance(self.on, basestring): - return self.on == bind.engine.name - elif isinstance(self.on, (tuple, list, set)): - return bind.engine.name in self.on - else: - return self.on(self, event, target, bind, **kw) - - def __call__(self, target, bind, **kw): - """Execute the DDL as a ddl_listener.""" - - if self._should_execute(target, bind, **kw): - return bind.execute(self.against(target)) - - def _check_ddl_on(self, on): - if (on is not None and - (not isinstance(on, (basestring, tuple, list, set)) and - not util.callable(on))): - raise exc.ArgumentError( - "Expected the name of a database dialect, a tuple " - "of names, or a callable for " - "'on' criteria, got type '%s'." % type(on).__name__) - - def bind(self): - if self._bind: - return self._bind - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - def _generate(self): - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - return s - - def _compiler(self, dialect, **kw): - """Return a compiler appropriate for this ClauseElement, given a - Dialect.""" - - return dialect.ddl_compiler(dialect, self, **kw) - -class DDL(DDLElement): - """A literal DDL statement. - - Specifies literal SQL DDL to be executed by the database. DDL objects - function as DDL event listeners, and can be subscribed to those events - listed in :class:`.DDLEvents`, using either :class:`.Table` or :class:`.MetaData` - objects as targets. Basic templating support allows a single DDL instance - to handle repetitive tasks for multiple tables. - - Examples:: - - from sqlalchemy import event, DDL - - tbl = Table('users', metadata, Column('uid', Integer)) - event.listen(tbl, 'before_create', DDL('DROP TRIGGER users_trigger')) - - spow = DDL('ALTER TABLE %(table)s SET secretpowers TRUE') - event.listen(tbl, 'after_create', spow.execute_if(dialect='somedb')) - - drop_spow = DDL('ALTER TABLE users SET secretpowers FALSE') - connection.execute(drop_spow) - - When operating on Table events, the following ``statement`` - string substitions are available:: - - %(table)s - the Table name, with any required quoting applied - %(schema)s - the schema name, with any required quoting applied - %(fullname)s - the Table name including schema, quoted if needed - - The DDL's "context", if any, will be combined with the standard - substutions noted above. Keys present in the context will override - the standard substitutions. - - """ - - __visit_name__ = "ddl" - - def __init__(self, statement, on=None, context=None, bind=None): - """Create a DDL statement. - - :param statement: - A string or unicode string to be executed. Statements will be - processed with Python's string formatting operator. See the - ``context`` argument and the ``execute_at`` method. - - A literal '%' in a statement must be escaped as '%%'. - - SQL bind parameters are not available in DDL statements. - - :param on: - Deprecated. See :meth:`.DDLElement.execute_if`. - - Optional filtering criteria. May be a string, tuple or a callable - predicate. If a string, it will be compared to the name of the - executing database dialect:: - - DDL('something', on='postgresql') - - If a tuple, specifies multiple dialect names:: - - DDL('something', on=('postgresql', 'mysql')) - - If a callable, it will be invoked with four positional arguments - as well as optional keyword arguments: - - :ddl: - This DDL element. - - :event: - The name of the event that has triggered this DDL, such as - 'after-create' Will be None if the DDL is executed explicitly. - - :target: - The ``Table`` or ``MetaData`` object which is the target of - this event. May be None if the DDL is executed explicitly. - - :connection: - The ``Connection`` being used for DDL execution - - :tables: - Optional keyword argument - a list of Table objects which are to - be created/ dropped within a MetaData.create_all() or drop_all() - method call. - - - If the callable returns a true value, the DDL statement will be - executed. - - :param context: - Optional dictionary, defaults to None. These values will be - available for use in string substitutions on the DDL statement. - - :param bind: - Optional. A :class:`~sqlalchemy.engine.base.Connectable`, used by - default when ``execute()`` is invoked without a bind argument. - - - See also: - - :class:`.DDLEvents` - :mod:`sqlalchemy.event` - - """ - - if not isinstance(statement, basestring): - raise exc.ArgumentError( - "Expected a string or unicode SQL statement, got '%r'" % - statement) - - self.statement = statement - self.context = context or {} - - self._check_ddl_on(on) - self.on = on - self._bind = bind - - - def __repr__(self): - return '<%s@%s; %s>' % ( - type(self).__name__, id(self), - ', '.join([repr(self.statement)] + - ['%s=%r' % (key, getattr(self, key)) - for key in ('on', 'context') - if getattr(self, key)])) - -def _to_schema_column(element): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, Column): - raise exc.ArgumentError("schema.Column object expected") - return element - -def _to_schema_column_or_string(element): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, (basestring, expression.ColumnElement)): - raise exc.ArgumentError("Element %r is not a string name or column element" % element) - return element - -class _CreateDropBase(DDLElement): - """Base class for DDL constucts that represent CREATE and DROP or - equivalents. - - The common theme of _CreateDropBase is a single - ``element`` attribute which refers to the element - to be created or dropped. - - """ - - def __init__(self, element, on=None, bind=None): - self.element = element - self._check_ddl_on(on) - self.on = on - self.bind = bind - - def _create_rule_disable(self, compiler): - """Allow disable of _create_rule using a callable. - - Pass to _create_rule using - util.portable_instancemethod(self._create_rule_disable) - to retain serializability. - - """ - return False - -class CreateSchema(_CreateDropBase): - """Represent a CREATE SCHEMA statement. - - .. versionadded:: 0.7.4 - - The argument here is the string name of the schema. - - """ - - __visit_name__ = "create_schema" - - def __init__(self, name, quote=None, **kw): - """Create a new :class:`.CreateSchema` construct.""" - - self.quote = quote - super(CreateSchema, self).__init__(name, **kw) - -class DropSchema(_CreateDropBase): - """Represent a DROP SCHEMA statement. - - The argument here is the string name of the schema. - - .. versionadded:: 0.7.4 - - """ - - __visit_name__ = "drop_schema" - - def __init__(self, name, quote=None, cascade=False, **kw): - """Create a new :class:`.DropSchema` construct.""" - - self.quote = quote - self.cascade=cascade - super(DropSchema, self).__init__(name, **kw) - - -class CreateTable(_CreateDropBase): - """Represent a CREATE TABLE statement.""" - - __visit_name__ = "create_table" - -class DropTable(_CreateDropBase): - """Represent a DROP TABLE statement.""" - - __visit_name__ = "drop_table" - -class CreateSequence(_CreateDropBase): - """Represent a CREATE SEQUENCE statement.""" - - __visit_name__ = "create_sequence" - -class DropSequence(_CreateDropBase): - """Represent a DROP SEQUENCE statement.""" - - __visit_name__ = "drop_sequence" - -class CreateIndex(_CreateDropBase): - """Represent a CREATE INDEX statement.""" - - __visit_name__ = "create_index" - -class DropIndex(_CreateDropBase): - """Represent a DROP INDEX statement.""" - - __visit_name__ = "drop_index" - -class AddConstraint(_CreateDropBase): - """Represent an ALTER TABLE ADD CONSTRAINT statement.""" - - __visit_name__ = "add_constraint" - - def __init__(self, element, *args, **kw): - super(AddConstraint, self).__init__(element, *args, **kw) - element._create_rule = util.portable_instancemethod( - self._create_rule_disable) - -class DropConstraint(_CreateDropBase): - """Represent an ALTER TABLE DROP CONSTRAINT statement.""" - - __visit_name__ = "drop_constraint" - - def __init__(self, element, cascade=False, **kw): - self.cascade = cascade - super(DropConstraint, self).__init__(element, **kw) - element._create_rule = util.portable_instancemethod( - self._create_rule_disable) - -def _bind_or_error(schemaitem, msg=None): - bind = schemaitem.bind - if not bind: - name = schemaitem.__class__.__name__ - label = getattr(schemaitem, 'fullname', - getattr(schemaitem, 'name', None)) - if label: - item = '%s %r' % (name, label) - else: - item = name - if isinstance(schemaitem, (MetaData, DDL)): - bindable = "the %s's .bind" % name - else: - bindable = "this %s's .metadata.bind" % name - - if msg is None: - msg = "The %s is not bound to an Engine or Connection. "\ - "Execution can not proceed without a database to execute "\ - "against. Either execute with an explicit connection or "\ - "assign %s to enable implicit execution." % \ - (item, bindable) - raise exc.UnboundExecutionError(msg) - return bind - +from .sql.schema import ( + CheckConstraint, + Column, + ColumnDefault, + Constraint, + DefaultClause, + DefaultGenerator, + FetchedValue, + ForeignKey, + ForeignKeyConstraint, + Index, + MetaData, + PassiveDefault, + PrimaryKeyConstraint, + SchemaItem, + Sequence, + Table, + ThreadLocalMetaData, + UniqueConstraint, + _get_table_key, + ColumnCollectionConstraint, + ) + + +from .sql.ddl import ( + DDL, + CreateTable, + DropTable, + CreateSequence, + DropSequence, + CreateIndex, + DropIndex, + CreateSchema, + DropSchema, + _DropView, + CreateColumn, + AddConstraint, + DropConstraint, + DDLBase, + DDLElement, + _CreateDropBase, + _DDLCompiles +) diff --git a/libs/sqlalchemy/sql/__init__.py b/libs/sqlalchemy/sql/__init__.py index 77fbfc84..9ed6049a 100644 --- a/libs/sqlalchemy/sql/__init__.py +++ b/libs/sqlalchemy/sql/__init__.py @@ -1,10 +1,10 @@ # sql/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.sql.expression import ( +from .expression import ( Alias, ClauseElement, ColumnCollection, @@ -35,6 +35,7 @@ from sqlalchemy.sql.expression import ( exists, extract, false, + False_, func, insert, intersect, @@ -55,6 +56,7 @@ from sqlalchemy.sql.expression import ( table, text, true, + True_, tuple_, type_coerce, union, @@ -62,8 +64,26 @@ from sqlalchemy.sql.expression import ( update, ) -from sqlalchemy.sql.visitors import ClauseVisitor +from .visitors import ClauseVisitor -__tmp = locals().keys() -__all__ = sorted([i for i in __tmp if not i.startswith('__')]) + +def __go(lcls): + global __all__ + from .. import util as _sa_util + + import inspect as _inspect + + __all__ = sorted(name for name, obj in lcls.items() + if not (name.startswith('_') or _inspect.ismodule(obj))) + + from .annotation import _prepare_annotations, Annotated + from .elements import AnnotatedColumnElement, ClauseList + from .selectable import AnnotatedFromClause + _prepare_annotations(ColumnElement, AnnotatedColumnElement) + _prepare_annotations(FromClause, AnnotatedFromClause) + _prepare_annotations(ClauseList, Annotated) + + _sa_util.dependencies.resolve_all("sqlalchemy.sql") + +__go(locals()) diff --git a/libs/sqlalchemy/sql/annotation.py b/libs/sqlalchemy/sql/annotation.py new file mode 100644 index 00000000..b5b7849d --- /dev/null +++ b/libs/sqlalchemy/sql/annotation.py @@ -0,0 +1,182 @@ +# sql/annotation.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""The :class:`.Annotated` class and related routines; creates hash-equivalent +copies of SQL constructs which contain context-specific markers and associations. + +""" + +from .. import util +from . import operators + +class Annotated(object): + """clones a ClauseElement and applies an 'annotations' dictionary. + + Unlike regular clones, this clone also mimics __hash__() and + __cmp__() of the original element so that it takes its place + in hashed collections. + + A reference to the original element is maintained, for the important + reason of keeping its hash value current. When GC'ed, the + hash value may be reused, causing conflicts. + + """ + + def __new__(cls, *args): + if not args: + # clone constructor + return object.__new__(cls) + else: + element, values = args + # pull appropriate subclass from registry of annotated + # classes + try: + cls = annotated_classes[element.__class__] + except KeyError: + cls = _new_annotation_type(element.__class__, cls) + return object.__new__(cls) + + def __init__(self, element, values): + self.__dict__ = element.__dict__.copy() + self.__element = element + self._annotations = values + + def _annotate(self, values): + _values = self._annotations.copy() + _values.update(values) + return self._with_annotations(_values) + + def _with_annotations(self, values): + clone = self.__class__.__new__(self.__class__) + clone.__dict__ = self.__dict__.copy() + clone._annotations = values + return clone + + def _deannotate(self, values=None, clone=True): + if values is None: + return self.__element + else: + _values = self._annotations.copy() + for v in values: + _values.pop(v, None) + return self._with_annotations(_values) + + def _compiler_dispatch(self, visitor, **kw): + return self.__element.__class__._compiler_dispatch(self, visitor, **kw) + + @property + def _constructor(self): + return self.__element._constructor + + def _clone(self): + clone = self.__element._clone() + if clone is self.__element: + # detect immutable, don't change anything + return self + else: + # update the clone with any changes that have occurred + # to this object's __dict__. + clone.__dict__.update(self.__dict__) + return self.__class__(clone, self._annotations) + + def __hash__(self): + return hash(self.__element) + + def __eq__(self, other): + if isinstance(self.__element, operators.ColumnOperators): + return self.__element.__class__.__eq__(self, other) + else: + return hash(other) == hash(self) + + + +# hard-generate Annotated subclasses. this technique +# is used instead of on-the-fly types (i.e. type.__new__()) +# so that the resulting objects are pickleable. +annotated_classes = {} + + + +def _deep_annotate(element, annotations, exclude=None): + """Deep copy the given ClauseElement, annotating each element + with the given annotations dictionary. + + Elements within the exclude collection will be cloned but not annotated. + + """ + def clone(elem): + if exclude and \ + hasattr(elem, 'proxy_set') and \ + elem.proxy_set.intersection(exclude): + newelem = elem._clone() + elif annotations != elem._annotations: + newelem = elem._annotate(annotations) + else: + newelem = elem + newelem._copy_internals(clone=clone) + return newelem + + if element is not None: + element = clone(element) + return element + + +def _deep_deannotate(element, values=None): + """Deep copy the given element, removing annotations.""" + + cloned = util.column_dict() + + def clone(elem): + # if a values dict is given, + # the elem must be cloned each time it appears, + # as there may be different annotations in source + # elements that are remaining. if totally + # removing all annotations, can assume the same + # slate... + if values or elem not in cloned: + newelem = elem._deannotate(values=values, clone=True) + newelem._copy_internals(clone=clone) + if not values: + cloned[elem] = newelem + return newelem + else: + return cloned[elem] + + if element is not None: + element = clone(element) + return element + + +def _shallow_annotate(element, annotations): + """Annotate the given ClauseElement and copy its internals so that + internal objects refer to the new annotated object. + + Basically used to apply a "dont traverse" annotation to a + selectable, without digging throughout the whole + structure wasting time. + """ + element = element._annotate(annotations) + element._copy_internals() + return element + +def _new_annotation_type(cls, base_cls): + if issubclass(cls, Annotated): + return cls + elif cls in annotated_classes: + return annotated_classes[cls] + annotated_classes[cls] = anno_cls = type( + "Annotated%s" % cls.__name__, + (base_cls, cls), {}) + globals()["Annotated%s" % cls.__name__] = anno_cls + return anno_cls + +def _prepare_annotations(target_hierarchy, base_cls): + stack = [target_hierarchy] + while stack: + cls = stack.pop() + stack.extend(cls.__subclasses__()) + + _new_annotation_type(cls, base_cls) diff --git a/libs/sqlalchemy/sql/base.py b/libs/sqlalchemy/sql/base.py new file mode 100644 index 00000000..e6c5d6ed --- /dev/null +++ b/libs/sqlalchemy/sql/base.py @@ -0,0 +1,348 @@ +# sql/base.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Foundational utilities common to many sql modules. + +""" + + +from .. import util, exc +import itertools +from .visitors import ClauseVisitor + + +PARSE_AUTOCOMMIT = util.symbol('PARSE_AUTOCOMMIT') +NO_ARG = util.symbol('NO_ARG') + +class Immutable(object): + """mark a ClauseElement as 'immutable' when expressions are cloned.""" + + def unique_params(self, *optionaldict, **kwargs): + raise NotImplementedError("Immutable objects do not support copying") + + def params(self, *optionaldict, **kwargs): + raise NotImplementedError("Immutable objects do not support copying") + + def _clone(self): + return self + + + +def _from_objects(*elements): + return itertools.chain(*[element._from_objects for element in elements]) + +@util.decorator +def _generative(fn, *args, **kw): + """Mark a method as generative.""" + + self = args[0]._generate() + fn(self, *args[1:], **kw) + return self + + +class Generative(object): + """Allow a ClauseElement to generate itself via the + @_generative decorator. + + """ + + def _generate(self): + s = self.__class__.__new__(self.__class__) + s.__dict__ = self.__dict__.copy() + return s + + +class Executable(Generative): + """Mark a ClauseElement as supporting execution. + + :class:`.Executable` is a superclass for all "statement" types + of objects, including :func:`select`, :func:`delete`, :func:`update`, + :func:`insert`, :func:`text`. + + """ + + supports_execution = True + _execution_options = util.immutabledict() + _bind = None + + @_generative + def execution_options(self, **kw): + """ Set non-SQL options for the statement which take effect during + execution. + + Execution options can be set on a per-statement or + per :class:`.Connection` basis. Additionally, the + :class:`.Engine` and ORM :class:`~.orm.query.Query` objects provide + access to execution options which they in turn configure upon + connections. + + The :meth:`execution_options` method is generative. A new + instance of this statement is returned that contains the options:: + + statement = select([table.c.x, table.c.y]) + statement = statement.execution_options(autocommit=True) + + Note that only a subset of possible execution options can be applied + to a statement - these include "autocommit" and "stream_results", + but not "isolation_level" or "compiled_cache". + See :meth:`.Connection.execution_options` for a full list of + possible options. + + .. seealso:: + + :meth:`.Connection.execution_options()` + + :meth:`.Query.execution_options()` + + """ + if 'isolation_level' in kw: + raise exc.ArgumentError( + "'isolation_level' execution option may only be specified " + "on Connection.execution_options(), or " + "per-engine using the isolation_level " + "argument to create_engine()." + ) + if 'compiled_cache' in kw: + raise exc.ArgumentError( + "'compiled_cache' execution option may only be specified " + "on Connection.execution_options(), not per statement." + ) + self._execution_options = self._execution_options.union(kw) + + def execute(self, *multiparams, **params): + """Compile and execute this :class:`.Executable`.""" + e = self.bind + if e is None: + label = getattr(self, 'description', self.__class__.__name__) + msg = ('This %s is not directly bound to a Connection or Engine.' + 'Use the .execute() method of a Connection or Engine ' + 'to execute this construct.' % label) + raise exc.UnboundExecutionError(msg) + return e._execute_clauseelement(self, multiparams, params) + + def scalar(self, *multiparams, **params): + """Compile and execute this :class:`.Executable`, returning the + result's scalar representation. + + """ + return self.execute(*multiparams, **params).scalar() + + @property + def bind(self): + """Returns the :class:`.Engine` or :class:`.Connection` to + which this :class:`.Executable` is bound, or None if none found. + + This is a traversal which checks locally, then + checks among the "from" clauses of associated objects + until a bound engine or connection is found. + + """ + if self._bind is not None: + return self._bind + + for f in _from_objects(self): + if f is self: + continue + engine = f.bind + if engine is not None: + return engine + else: + return None + + +class SchemaEventTarget(object): + """Base class for elements that are the targets of :class:`.DDLEvents` + events. + + This includes :class:`.SchemaItem` as well as :class:`.SchemaType`. + + """ + + def _set_parent(self, parent): + """Associate with this SchemaEvent's parent object.""" + + raise NotImplementedError() + + def _set_parent_with_dispatch(self, parent): + self.dispatch.before_parent_attach(self, parent) + self._set_parent(parent) + self.dispatch.after_parent_attach(self, parent) + +class SchemaVisitor(ClauseVisitor): + """Define the visiting for ``SchemaItem`` objects.""" + + __traverse_options__ = {'schema_visitor': True} + +class ColumnCollection(util.OrderedProperties): + """An ordered dictionary that stores a list of ColumnElement + instances. + + Overrides the ``__eq__()`` method to produce SQL clauses between + sets of correlated columns. + + """ + + def __init__(self, *cols): + super(ColumnCollection, self).__init__() + self._data.update((c.key, c) for c in cols) + self.__dict__['_all_cols'] = util.column_set(self) + + def __str__(self): + return repr([str(c) for c in self]) + + def replace(self, column): + """add the given column to this collection, removing unaliased + versions of this column as well as existing columns with the + same key. + + e.g.:: + + t = Table('sometable', metadata, Column('col1', Integer)) + t.columns.replace(Column('col1', Integer, key='columnone')) + + will remove the original 'col1' from the collection, and add + the new column under the name 'columnname'. + + Used by schema.Column to override columns during table reflection. + + """ + if column.name in self and column.key != column.name: + other = self[column.name] + if other.name == other.key: + del self._data[other.name] + self._all_cols.remove(other) + if column.key in self._data: + self._all_cols.remove(self._data[column.key]) + self._all_cols.add(column) + self._data[column.key] = column + + def add(self, column): + """Add a column to this collection. + + The key attribute of the column will be used as the hash key + for this dictionary. + + """ + self[column.key] = column + + def __delitem__(self, key): + raise NotImplementedError() + + def __setattr__(self, key, object): + raise NotImplementedError() + + def __setitem__(self, key, value): + if key in self: + + # this warning is primarily to catch select() statements + # which have conflicting column names in their exported + # columns collection + + existing = self[key] + if not existing.shares_lineage(value): + util.warn('Column %r on table %r being replaced by ' + '%r, which has the same key. Consider ' + 'use_labels for select() statements.' % (key, + getattr(existing, 'table', None), value)) + self._all_cols.remove(existing) + # pop out memoized proxy_set as this + # operation may very well be occurring + # in a _make_proxy operation + util.memoized_property.reset(value, "proxy_set") + self._all_cols.add(value) + self._data[key] = value + + def clear(self): + self._data.clear() + self._all_cols.clear() + + def remove(self, column): + del self._data[column.key] + self._all_cols.remove(column) + + def update(self, value): + self._data.update(value) + self._all_cols.clear() + self._all_cols.update(self._data.values()) + + def extend(self, iter): + self.update((c.key, c) for c in iter) + + __hash__ = None + + @util.dependencies("sqlalchemy.sql.elements") + def __eq__(self, elements, other): + l = [] + for c in other: + for local in self: + if c.shares_lineage(local): + l.append(c == local) + return elements.and_(*l) + + def __contains__(self, other): + if not isinstance(other, util.string_types): + raise exc.ArgumentError("__contains__ requires a string argument") + return util.OrderedProperties.__contains__(self, other) + + def __setstate__(self, state): + self.__dict__['_data'] = state['_data'] + self.__dict__['_all_cols'] = util.column_set(self._data.values()) + + def contains_column(self, col): + # this has to be done via set() membership + return col in self._all_cols + + def as_immutable(self): + return ImmutableColumnCollection(self._data, self._all_cols) + + +class ImmutableColumnCollection(util.ImmutableProperties, ColumnCollection): + def __init__(self, data, colset): + util.ImmutableProperties.__init__(self, data) + self.__dict__['_all_cols'] = colset + + extend = remove = util.ImmutableProperties._immutable + + +class ColumnSet(util.ordered_column_set): + def contains_column(self, col): + return col in self + + def extend(self, cols): + for col in cols: + self.add(col) + + def __add__(self, other): + return list(self) + list(other) + + @util.dependencies("sqlalchemy.sql.elements") + def __eq__(self, elements, other): + l = [] + for c in other: + for local in self: + if c.shares_lineage(local): + l.append(c == local) + return elements.and_(*l) + + def __hash__(self): + return hash(tuple(x for x in self)) + +def _bind_or_error(schemaitem, msg=None): + bind = schemaitem.bind + if not bind: + name = schemaitem.__class__.__name__ + label = getattr(schemaitem, 'fullname', + getattr(schemaitem, 'name', None)) + if label: + item = '%s object %r' % (name, label) + else: + item = '%s object' % name + if msg is None: + msg = "%s is not bound to an Engine or Connection. "\ + "Execution can not proceed without a database to execute "\ + "against." % item + raise exc.UnboundExecutionError(msg) + return bind diff --git a/libs/sqlalchemy/sql/compiler.py b/libs/sqlalchemy/sql/compiler.py index 9dc56d1f..ade3c623 100644 --- a/libs/sqlalchemy/sql/compiler.py +++ b/libs/sqlalchemy/sql/compiler.py @@ -1,5 +1,5 @@ # sql/compiler.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,26 +8,24 @@ Classes provided include: -:class:`~sqlalchemy.sql.compiler.SQLCompiler` - renders SQL +:class:`.compiler.SQLCompiler` - renders SQL strings -:class:`~sqlalchemy.sql.compiler.DDLCompiler` - renders DDL +:class:`.compiler.DDLCompiler` - renders DDL (data definition language) strings -:class:`~sqlalchemy.sql.compiler.GenericTypeCompiler` - renders +:class:`.compiler.GenericTypeCompiler` - renders type specification strings. To generate user-defined SQL strings, see -:module:`~sqlalchemy.ext.compiler`. +:doc:`/ext/compiler`. """ import re -import sys -from sqlalchemy import schema, engine, util, exc -from sqlalchemy.sql import operators, functions, util as sql_util, \ - visitors -from sqlalchemy.sql import expression as sql +from . import schema, sqltypes, operators, functions, \ + util as sql_util, visitors, elements, selectable, base +from .. import util, exc import decimal import itertools @@ -51,65 +49,75 @@ RESERVED_WORDS = set([ 'using', 'verbose', 'when', 'where']) LEGAL_CHARACTERS = re.compile(r'^[A-Z0-9_$]+$', re.I) -ILLEGAL_INITIAL_CHARACTERS = set([str(x) for x in xrange(0, 10)]).union(['$']) +ILLEGAL_INITIAL_CHARACTERS = set([str(x) for x in range(0, 10)]).union(['$']) BIND_PARAMS = re.compile(r'(? ', - operators.ge : ' >= ', - operators.eq : ' = ', - operators.concat_op : ' || ', - operators.between_op : ' BETWEEN ', - operators.match_op : ' MATCH ', - operators.in_op : ' IN ', - operators.notin_op : ' NOT IN ', - operators.comma_op : ', ', - operators.from_ : ' FROM ', - operators.as_ : ' AS ', - operators.is_ : ' IS ', - operators.isnot : ' IS NOT ', - operators.collate : ' COLLATE ', + operators.and_: ' AND ', + operators.or_: ' OR ', + operators.add: ' + ', + operators.mul: ' * ', + operators.sub: ' - ', + operators.div: ' / ', + operators.mod: ' % ', + operators.truediv: ' / ', + operators.neg: '-', + operators.lt: ' < ', + operators.le: ' <= ', + operators.ne: ' != ', + operators.gt: ' > ', + operators.ge: ' >= ', + operators.eq: ' = ', + operators.concat_op: ' || ', + operators.between_op: ' BETWEEN ', + operators.match_op: ' MATCH ', + operators.in_op: ' IN ', + operators.notin_op: ' NOT IN ', + operators.comma_op: ', ', + operators.from_: ' FROM ', + operators.as_: ' AS ', + operators.is_: ' IS ', + operators.isnot: ' IS NOT ', + operators.collate: ' COLLATE ', # unary - operators.exists : 'EXISTS ', - operators.distinct_op : 'DISTINCT ', - operators.inv : 'NOT ', + operators.exists: 'EXISTS ', + operators.distinct_op: 'DISTINCT ', + operators.inv: 'NOT ', # modifiers - operators.desc_op : ' DESC', - operators.asc_op : ' ASC', - operators.nullsfirst_op : ' NULLS FIRST', - operators.nullslast_op : ' NULLS LAST', + operators.desc_op: ' DESC', + operators.asc_op: ' ASC', + operators.nullsfirst_op: ' NULLS FIRST', + operators.nullslast_op: ' NULLS LAST', + } FUNCTIONS = { - functions.coalesce : 'coalesce%(expr)s', + functions.coalesce: 'coalesce%(expr)s', functions.current_date: 'CURRENT_DATE', functions.current_time: 'CURRENT_TIME', functions.current_timestamp: 'CURRENT_TIMESTAMP', @@ -118,7 +126,7 @@ FUNCTIONS = { functions.localtimestamp: 'LOCALTIMESTAMP', functions.random: 'random%(expr)s', functions.sysdate: 'sysdate', - functions.session_user :'SESSION_USER', + functions.session_user: 'SESSION_USER', functions.user: 'USER' } @@ -141,16 +149,125 @@ EXTRACT_MAP = { } COMPOUND_KEYWORDS = { - sql.CompoundSelect.UNION : 'UNION', - sql.CompoundSelect.UNION_ALL : 'UNION ALL', - sql.CompoundSelect.EXCEPT : 'EXCEPT', - sql.CompoundSelect.EXCEPT_ALL : 'EXCEPT ALL', - sql.CompoundSelect.INTERSECT : 'INTERSECT', - sql.CompoundSelect.INTERSECT_ALL : 'INTERSECT ALL' + selectable.CompoundSelect.UNION: 'UNION', + selectable.CompoundSelect.UNION_ALL: 'UNION ALL', + selectable.CompoundSelect.EXCEPT: 'EXCEPT', + selectable.CompoundSelect.EXCEPT_ALL: 'EXCEPT ALL', + selectable.CompoundSelect.INTERSECT: 'INTERSECT', + selectable.CompoundSelect.INTERSECT_ALL: 'INTERSECT ALL' } +class Compiled(object): + """Represent a compiled SQL or DDL expression. + + The ``__str__`` method of the ``Compiled`` object should produce + the actual text of the statement. ``Compiled`` objects are + specific to their underlying database dialect, and also may + or may not be specific to the columns referenced within a + particular set of bind parameters. In no case should the + ``Compiled`` object be dependent on the actual values of those + bind parameters, even though it may reference those values as + defaults. + """ + + def __init__(self, dialect, statement, bind=None, + compile_kwargs=util.immutabledict()): + """Construct a new ``Compiled`` object. + + :param dialect: ``Dialect`` to compile against. + + :param statement: ``ClauseElement`` to be compiled. + + :param bind: Optional Engine or Connection to compile this + statement against. + + :param compile_kwargs: additional kwargs that will be + passed to the initial call to :meth:`.Compiled.process`. + + .. versionadded:: 0.8 + + """ + + self.dialect = dialect + self.bind = bind + if statement is not None: + self.statement = statement + self.can_execute = statement.supports_execution + self.string = self.process(self.statement, **compile_kwargs) + + @util.deprecated("0.7", ":class:`.Compiled` objects now compile " + "within the constructor.") + def compile(self): + """Produce the internal string representation of this element. + """ + pass + + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_compiled(self, multiparams, params) + + @property + def sql_compiler(self): + """Return a Compiled that is capable of processing SQL expressions. + + If this compiler is one, it would likely just return 'self'. + + """ + + raise NotImplementedError() + + def process(self, obj, **kwargs): + return obj._compiler_dispatch(self, **kwargs) + + def __str__(self): + """Return the string text of the generated SQL or DDL.""" + + return self.string or '' + + def construct_params(self, params=None): + """Return the bind params for this compiled object. + + :param params: a dict of string/object pairs whose values will + override bind values compiled in to the + statement. + """ + + raise NotImplementedError() + + @property + def params(self): + """Return the bind params for this compiled object.""" + return self.construct_params() + + def execute(self, *multiparams, **params): + """Execute this compiled object.""" + + e = self.bind + if e is None: + raise exc.UnboundExecutionError( + "This Compiled object is not bound to any Engine " + "or Connection.") + return e._execute_compiled(self, multiparams, params) + + def scalar(self, *multiparams, **params): + """Execute this compiled object and return the result's + scalar value.""" + + return self.execute(*multiparams, **params).scalar() + + +class TypeCompiler(object): + """Produces DDL specification for TypeEngine objects.""" + + def __init__(self, dialect): + self.dialect = dialect + + def process(self, type_): + return type_._compiler_dispatch(self) + + + class _CompileLabel(visitors.Visitable): - """lightweight label object which acts as an expression._Label.""" + """lightweight label object which acts as an expression.Label.""" __visit_name__ = 'label' __slots__ = 'element', 'name' @@ -158,7 +275,7 @@ class _CompileLabel(visitors.Visitable): def __init__(self, col, name, alt_names=()): self.element = col self.name = name - self._alt_names = alt_names + self._alt_names = (col,) + alt_names @property def proxy_set(self): @@ -168,11 +285,8 @@ class _CompileLabel(visitors.Visitable): def type(self): return self.element.type - @property - def quote(self): - return self.element.quote -class SQLCompiler(engine.Compiled): +class SQLCompiler(Compiled): """Default implementation of Compiled. Compiles ClauseElements into SQL strings. Uses a similar visit @@ -236,11 +350,11 @@ class SQLCompiler(engine.Compiled): # execute) self.inline = inline or getattr(statement, 'inline', False) - # a dictionary of bind parameter keys to _BindParamClause + # a dictionary of bind parameter keys to BindParameter # instances. self.binds = {} - # a dictionary of _BindParamClause instances to "compiled" names + # a dictionary of BindParameter instances to "compiled" names # that are actually present in the generated SQL self.bind_names = util.column_dict() @@ -273,7 +387,7 @@ class SQLCompiler(engine.Compiled): # a map which tracks "truncated" names based on # dialect.label_length or dialect.max_identifier_length self.truncated_names = {} - engine.Compiled.__init__(self, dialect, statement, **kwargs) + Compiled.__init__(self, dialect, statement, **kwargs) if self.positional and dialect.paramstyle == 'numeric': self._apply_numbered_params() @@ -296,16 +410,16 @@ class SQLCompiler(engine.Compiled): poscount = itertools.count(1) self.string = re.sub( r'\[_POSITION\]', - lambda m:str(util.next(poscount)), + lambda m: str(util.next(poscount)), self.string) @util.memoized_property def _bind_processors(self): return dict( (key, value) for key, value in - ( (self.bind_names[bindparam], + ((self.bind_names[bindparam], bindparam.type._cached_bind_processor(self.dialect)) - for bindparam in self.bind_names ) + for bindparam in self.bind_names) if value is not None ) @@ -321,7 +435,7 @@ class SQLCompiler(engine.Compiled): if params: pd = {} - for bindparam, name in self.bind_names.iteritems(): + for bindparam, name in self.bind_names.items(): if bindparam.key in params: pd[name] = params[bindparam.key] elif name in params: @@ -373,24 +487,32 @@ class SQLCompiler(engine.Compiled): def visit_grouping(self, grouping, asfrom=False, **kwargs): return "(" + grouping.element._compiler_dispatch(self, **kwargs) + ")" - def visit_label(self, label, result_map=None, + def visit_label(self, label, + add_to_result_map=None, within_label_clause=False, - within_columns_clause=False, **kw): + within_columns_clause=False, + render_label_as_label=None, + **kw): # only render labels within the columns clause # or ORDER BY clause of a select. dialect-specific compilers # can modify this behavior. - if within_columns_clause and not within_label_clause: - if isinstance(label.name, sql._truncated_label): + render_label_with_as = within_columns_clause and not within_label_clause + render_label_only = render_label_as_label is label + + if render_label_only or render_label_with_as: + if isinstance(label.name, elements._truncated_label): labelname = self._truncated_identifier("colident", label.name) else: labelname = label.name - if result_map is not None: - result_map[labelname.lower()] = ( + if render_label_with_as: + if add_to_result_map is not None: + add_to_result_map( + labelname, label.name, - (label, label.element, labelname, ) + - label._alt_names, - label.type) + (label, labelname, ) + label._alt_names, + label.type + ) return label.element._compiler_dispatch(self, within_columns_clause=True, @@ -398,47 +520,51 @@ class SQLCompiler(engine.Compiled): **kw) + \ OPERATORS[operators.as_] + \ self.preparer.format_label(label, labelname) + elif render_label_only: + return labelname else: return label.element._compiler_dispatch(self, within_columns_clause=False, **kw) - def visit_column(self, column, result_map=None, **kwargs): + def visit_column(self, column, add_to_result_map=None, + include_table=True, **kwargs): name = orig_name = column.name if name is None: raise exc.CompileError("Cannot compile Column object until " - "it's 'name' is assigned.") + "its 'name' is assigned.") is_literal = column.is_literal - if not is_literal and isinstance(name, sql._truncated_label): + if not is_literal and isinstance(name, elements._truncated_label): name = self._truncated_identifier("colident", name) - if result_map is not None: - result_map[name.lower()] = (orig_name, - (column, name, column.key), - column.type) + if add_to_result_map is not None: + add_to_result_map( + name, + orig_name, + (column, name, column.key), + column.type + ) if is_literal: name = self.escape_literal_column(name) else: - name = self.preparer.quote(name, column.quote) + name = self.preparer.quote(name) table = column.table - if table is None or not table.named_with_column: + if table is None or not include_table or not table.named_with_column: return name else: if table.schema: - schema_prefix = self.preparer.quote_schema( - table.schema, - table.quote_schema) + '.' + schema_prefix = self.preparer.quote_schema(table.schema) + '.' else: schema_prefix = '' tablename = table.name - if isinstance(tablename, sql._truncated_label): + if isinstance(tablename, elements._truncated_label): tablename = self._truncated_identifier("alias", tablename) return schema_prefix + \ - self.preparer.quote(tablename, table.quote) + \ + self.preparer.quote(tablename) + \ "." + name def escape_literal_column(self, text): @@ -459,17 +585,13 @@ class SQLCompiler(engine.Compiled): def post_process_text(self, text): return text - def visit_textclause(self, textclause, **kwargs): - if textclause.typemap is not None: - for colname, type_ in textclause.typemap.iteritems(): - self.result_map[colname.lower()] = (colname, None, type_) - + def visit_textclause(self, textclause, **kw): def do_bindparam(m): name = m.group(1) - if name in textclause.bindparams: - return self.process(textclause.bindparams[name]) + if name in textclause._bindparams: + return self.process(textclause._bindparams[name], **kw) else: - return self.bindparam_string(name, **kwargs) + return self.bindparam_string(name, **kw) # un-escape any \:params return BIND_PARAMS_ESC.sub(lambda m: m.group(1), @@ -477,16 +599,53 @@ class SQLCompiler(engine.Compiled): self.post_process_text(textclause.text)) ) + def visit_text_as_from(self, taf, iswrapper=False, + compound_index=0, force_result_map=False, + asfrom=False, + parens=True, **kw): + + toplevel = not self.stack + entry = self._default_stack_entry if toplevel else self.stack[-1] + + populate_result_map = force_result_map or ( + compound_index == 0 and ( + toplevel or \ + entry['iswrapper'] + ) + ) + + if populate_result_map: + for c in taf.c: + self._add_to_result_map( + c.key, c.key, (c,), c.type + ) + + text = self.process(taf.element, **kw) + if asfrom and parens: + text = "(%s)" % text + return text + + def visit_null(self, expr, **kw): return 'NULL' def visit_true(self, expr, **kw): - return 'true' + if self.dialect.supports_native_boolean: + return 'true' + else: + return "1" def visit_false(self, expr, **kw): - return 'false' + if self.dialect.supports_native_boolean: + return 'false' + else: + return "0" + + def visit_clauselist(self, clauselist, order_by_select=None, **kw): + if order_by_select is not None: + return self._order_by_clauselist( + clauselist, order_by_select, **kw) - def visit_clauselist(self, clauselist, **kwargs): sep = clauselist.operator if sep is None: sep = " " @@ -494,8 +653,32 @@ class SQLCompiler(engine.Compiled): sep = OPERATORS[clauselist.operator] return sep.join( s for s in - (c._compiler_dispatch(self, **kwargs) - for c in clauselist.clauses) + ( + c._compiler_dispatch(self, **kw) + for c in clauselist.clauses) + if s) + + def _order_by_clauselist(self, clauselist, order_by_select, **kw): + # look through raw columns collection for labels. + # note that its OK we aren't expanding tables and other selectables + # here; we can only add a label in the ORDER BY for an individual + # label expression in the columns clause. + + raw_col = set(l._order_by_label_element.name + for l in order_by_select._raw_columns + if l._order_by_label_element is not None) + + return ", ".join( + s for s in + ( + c._compiler_dispatch(self, + render_label_as_label= + c._order_by_label_element if + c._order_by_label_element is not None and + c._order_by_label_element.name in raw_col + else None, + **kw) + for c in clauselist.clauses) if s) def visit_case(self, clause, **kwargs): @@ -522,7 +705,7 @@ class SQLCompiler(engine.Compiled): def visit_over(self, over, **kwargs): return "%s OVER (%s)" % ( over.func._compiler_dispatch(self, **kwargs), - ' '.join( + ' '.join( '%s BY %s' % (word, clause._compiler_dispatch(self, **kwargs)) for word, clause in ( ('PARTITION', over.partition_by), @@ -537,9 +720,11 @@ class SQLCompiler(engine.Compiled): return "EXTRACT(%s FROM %s)" % (field, extract.expr._compiler_dispatch(self, **kwargs)) - def visit_function(self, func, result_map=None, **kwargs): - if result_map is not None: - result_map[func.name.lower()] = (func.name, None, func.type) + def visit_function(self, func, add_to_result_map=None, **kwargs): + if add_to_result_map is not None: + add_to_result_map( + func.name, func.name, (), func.type + ) disp = getattr(self, "visit_%s_func" % func.name.lower(), None) if disp: @@ -554,17 +739,25 @@ class SQLCompiler(engine.Compiled): def visit_sequence(self, sequence): raise NotImplementedError( - "Dialect '%s' does not support sequence increments." % self.dialect.name + "Dialect '%s' does not support sequence increments." % + self.dialect.name ) def function_argspec(self, func, **kwargs): return func.clause_expr._compiler_dispatch(self, **kwargs) + def visit_compound_select(self, cs, asfrom=False, parens=True, compound_index=0, **kwargs): - entry = self.stack and self.stack[-1] or {} - self.stack.append({'from': entry.get('from', None), - 'iswrapper': not entry}) + toplevel = not self.stack + entry = self._default_stack_entry if toplevel else self.stack[-1] + + self.stack.append( + { + 'correlate_froms': entry['correlate_froms'], + 'iswrapper': toplevel, + 'asfrom_froms': entry['asfrom_froms'] + }) keyword = self.compound_keywords.get(cs.keyword) @@ -585,7 +778,7 @@ class SQLCompiler(engine.Compiled): self.limit_clause(cs) or "" if self.ctes and \ - compound_index == 0 and not entry: + compound_index == 0 and toplevel: text = self._render_cte_clause() + text self.stack.pop(-1) @@ -595,84 +788,191 @@ class SQLCompiler(engine.Compiled): return text def visit_unary(self, unary, **kw): - s = unary.element._compiler_dispatch(self, **kw) if unary.operator: - s = OPERATORS[unary.operator] + s - if unary.modifier: - s = s + OPERATORS[unary.modifier] - return s + if unary.modifier: + raise exc.CompileError( + "Unary expression does not support operator " + "and modifier simultaneously") + disp = getattr(self, "visit_%s_unary_operator" % + unary.operator.__name__, None) + if disp: + return disp(unary, unary.operator, **kw) + else: + return self._generate_generic_unary_operator(unary, + OPERATORS[unary.operator], **kw) + elif unary.modifier: + disp = getattr(self, "visit_%s_unary_modifier" % + unary.modifier.__name__, None) + if disp: + return disp(unary, unary.modifier, **kw) + else: + return self._generate_generic_unary_modifier(unary, + OPERATORS[unary.modifier], **kw) + else: + raise exc.CompileError( + "Unary expression has no operator or modifier") + + def visit_istrue_unary_operator(self, element, operator, **kw): + if self.dialect.supports_native_boolean: + return self.process(element.element, **kw) + else: + return "%s = 1" % self.process(element.element, **kw) + + def visit_isfalse_unary_operator(self, element, operator, **kw): + if self.dialect.supports_native_boolean: + return "NOT %s" % self.process(element.element, **kw) + else: + return "%s = 0" % self.process(element.element, **kw) def visit_binary(self, binary, **kw): # don't allow "? = ?" to render if self.ansi_bind_rules and \ - isinstance(binary.left, sql._BindParamClause) and \ - isinstance(binary.right, sql._BindParamClause): + isinstance(binary.left, elements.BindParameter) and \ + isinstance(binary.right, elements.BindParameter): kw['literal_binds'] = True - return self._operator_dispatch(binary.operator, - binary, - lambda opstr: binary.left._compiler_dispatch(self, **kw) + - opstr + - binary.right._compiler_dispatch( - self, **kw), - **kw - ) + operator = binary.operator + disp = getattr(self, "visit_%s_binary" % operator.__name__, None) + if disp: + return disp(binary, operator, **kw) + else: + try: + opstring = OPERATORS[operator] + except KeyError: + raise exc.UnsupportedCompilationError(self, operator) + else: + return self._generate_generic_binary(binary, opstring, **kw) - def visit_like_op(self, binary, **kw): + def visit_custom_op_binary(self, element, operator, **kw): + return self._generate_generic_binary(element, + " " + operator.opstring + " ", **kw) + + def visit_custom_op_unary_operator(self, element, operator, **kw): + return self._generate_generic_unary_operator(element, + operator.opstring + " ", **kw) + + def visit_custom_op_unary_modifier(self, element, operator, **kw): + return self._generate_generic_unary_modifier(element, + " " + operator.opstring, **kw) + + def _generate_generic_binary(self, binary, opstring, **kw): + return binary.left._compiler_dispatch(self, **kw) + \ + opstring + \ + binary.right._compiler_dispatch(self, **kw) + + def _generate_generic_unary_operator(self, unary, opstring, **kw): + return opstring + unary.element._compiler_dispatch(self, **kw) + + def _generate_generic_unary_modifier(self, unary, opstring, **kw): + return unary.element._compiler_dispatch(self, **kw) + opstring + + @util.memoized_property + def _like_percent_literal(self): + return elements.literal_column("'%'", type_=sqltypes.STRINGTYPE) + + def visit_contains_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__add__(binary.right).__add__(percent) + return self.visit_like_op_binary(binary, operator, **kw) + + def visit_notcontains_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__add__(binary.right).__add__(percent) + return self.visit_notlike_op_binary(binary, operator, **kw) + + def visit_startswith_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__radd__( + binary.right + ) + return self.visit_like_op_binary(binary, operator, **kw) + + def visit_notstartswith_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__radd__( + binary.right + ) + return self.visit_notlike_op_binary(binary, operator, **kw) + + def visit_endswith_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__add__(binary.right) + return self.visit_like_op_binary(binary, operator, **kw) + + def visit_notendswith_op_binary(self, binary, operator, **kw): + binary = binary._clone() + percent = self._like_percent_literal + binary.right = percent.__add__(binary.right) + return self.visit_notlike_op_binary(binary, operator, **kw) + + def visit_like_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) + + # TODO: use ternary here, not "and"/ "or" return '%s LIKE %s' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) - def visit_notlike_op(self, binary, **kw): + def visit_notlike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return '%s NOT LIKE %s' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) - def visit_ilike_op(self, binary, **kw): + def visit_ilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return 'lower(%s) LIKE lower(%s)' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) - def visit_notilike_op(self, binary, **kw): + def visit_notilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return 'lower(%s) NOT LIKE lower(%s)' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') - - def _operator_dispatch(self, operator, element, fn, **kw): - if util.callable(operator): - disp = getattr(self, "visit_%s" % operator.__name__, None) - if disp: - return disp(element, **kw) - else: - return fn(OPERATORS[operator]) - else: - return fn(" " + operator + " ") + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def visit_bindparam(self, bindparam, within_columns_clause=False, - literal_binds=False, **kwargs): + literal_binds=False, + skip_bind_expression=False, + **kwargs): + + if not skip_bind_expression and bindparam.type._has_bind_expression: + bind_expression = bindparam.type.bind_expression(bindparam) + return self.process(bind_expression, + skip_bind_expression=True) if literal_binds or \ (within_columns_clause and \ self.ansi_bind_rules): if bindparam.value is None: - raise exc.CompileError("Bind parameter without a " - "renderable value not allowed here.") + raise exc.CompileError("Bind parameter '%s' without a " + "renderable value not allowed here." + % bindparam.key) return self.render_literal_bindparam(bindparam, within_columns_clause=True, **kwargs) @@ -682,14 +982,14 @@ class SQLCompiler(engine.Compiled): existing = self.binds[name] if existing is not bindparam: if (existing.unique or bindparam.unique) and \ - not existing.proxy_set.intersection(bindparam.proxy_set): + not existing.proxy_set.intersection( + bindparam.proxy_set): raise exc.CompileError( "Bind parameter '%s' conflicts with " "unique bind parameter of the same name" % bindparam.key ) - elif getattr(existing, '_is_crud', False) or \ - getattr(bindparam, '_is_crud', False): + elif existing._is_crud or bindparam._is_crud: raise exc.CompileError( "bindparam() name '%s' is reserved " "for automatic usage in the VALUES or SET " @@ -706,9 +1006,6 @@ class SQLCompiler(engine.Compiled): def render_literal_bindparam(self, bindparam, **kw): value = bindparam.value - processor = bindparam.type._cached_bind_processor(self.dialect) - if processor: - value = processor(value) return self.render_literal_value(value, bindparam.type) def render_literal_value(self, value, type_): @@ -721,15 +1018,10 @@ class SQLCompiler(engine.Compiled): of the DBAPI. """ - if isinstance(value, basestring): - value = value.replace("'", "''") - return "'%s'" % value - elif value is None: - return "NULL" - elif isinstance(value, (float, int, long)): - return repr(value) - elif isinstance(value, decimal.Decimal): - return str(value) + + processor = type_._cached_literal_processor(self.dialect) + if processor: + return processor(value) else: raise NotImplementedError( "Don't know how to literal-quote value %r" % value) @@ -739,7 +1031,7 @@ class SQLCompiler(engine.Compiled): return self.bind_names[bindparam] bind_name = bindparam.key - if isinstance(bind_name, sql._truncated_label): + if isinstance(bind_name, elements._truncated_label): bind_name = self._truncated_identifier("bindparam", bind_name) # add to bind_names for translation @@ -778,7 +1070,7 @@ class SQLCompiler(engine.Compiled): positional_names.append(name) else: self.positiontup.append(name) - return self.bindtemplate % {'name':name} + return self.bindtemplate % {'name': name} def visit_cte(self, cte, asfrom=False, ashint=False, fromhints=None, @@ -787,7 +1079,7 @@ class SQLCompiler(engine.Compiled): if self.positional: kwargs['positional_names'] = self.cte_positional - if isinstance(cte.name, sql._truncated_label): + if isinstance(cte.name, elements._truncated_label): cte_name = self._truncated_identifier("alias", cte.name) else: cte_name = cte.name @@ -797,7 +1089,7 @@ class SQLCompiler(engine.Compiled): # we've generated a same-named CTE that we are enclosed in, # or this is the same CTE. just return the name. if cte in existing_cte._restates or cte is existing_cte: - return cte_name + return self.preparer.format_alias(cte, cte_name) elif existing_cte in cte._restates: # we've generated a same-named CTE that is # enclosed in us - we take precedence, so @@ -811,19 +1103,24 @@ class SQLCompiler(engine.Compiled): self.ctes_by_name[cte_name] = cte - if cte.cte_alias: - if isinstance(cte.cte_alias, sql._truncated_label): - cte_alias = self._truncated_identifier("alias", cte.cte_alias) - else: - cte_alias = cte.cte_alias - if not cte.cte_alias and cte not in self.ctes: + if cte._cte_alias is not None: + orig_cte = cte._cte_alias + if orig_cte not in self.ctes: + self.visit_cte(orig_cte) + cte_alias_name = cte._cte_alias.name + if isinstance(cte_alias_name, elements._truncated_label): + cte_alias_name = self._truncated_identifier("alias", cte_alias_name) + else: + orig_cte = cte + cte_alias_name = None + if not cte_alias_name and cte not in self.ctes: if cte.recursive: self.ctes_recursive = True text = self.preparer.format_alias(cte, cte_name) if cte.recursive: - if isinstance(cte.original, sql.Select): + if isinstance(cte.original, selectable.Select): col_source = cte.original - elif isinstance(cte.original, sql.CompoundSelect): + elif isinstance(cte.original, selectable.CompoundSelect): col_source = cte.original.selects[0] else: assert False @@ -839,18 +1136,20 @@ class SQLCompiler(engine.Compiled): self, asfrom=True, **kwargs ) self.ctes[cte] = text + if asfrom: - if cte.cte_alias: - text = self.preparer.format_alias(cte, cte_alias) + if cte_alias_name: + text = self.preparer.format_alias(cte, cte_alias_name) text += " AS " + cte_name else: return self.preparer.format_alias(cte, cte_name) return text def visit_alias(self, alias, asfrom=False, ashint=False, + iscrud=False, fromhints=None, **kwargs): if asfrom or ashint: - if isinstance(alias.name, sql._truncated_label): + if isinstance(alias.name, elements._truncated_label): alias_name = self._truncated_identifier("alias", alias.name) else: alias_name = alias.name @@ -864,44 +1163,104 @@ class SQLCompiler(engine.Compiled): self.preparer.format_alias(alias, alias_name) if fromhints and alias in fromhints: - hinttext = self.get_from_hint_text(alias, fromhints[alias]) - if hinttext: - ret += " " + hinttext + ret = self.format_from_hint_text(ret, alias, + fromhints[alias], iscrud) return ret else: return alias.original._compiler_dispatch(self, **kwargs) - def label_select_column(self, select, column, asfrom): - """label columns present in a select().""" + def _add_to_result_map(self, keyname, name, objects, type_): + if not self.dialect.case_sensitive: + keyname = keyname.lower() - if isinstance(column, sql._Label): - return column + if keyname in self.result_map: + # conflicting keyname, just double up the list + # of objects. this will cause an "ambiguous name" + # error if an attempt is made by the result set to + # access. + e_name, e_obj, e_type = self.result_map[keyname] + self.result_map[keyname] = e_name, e_obj + objects, e_type + else: + self.result_map[keyname] = name, objects, type_ - elif select is not None and \ - select.use_labels and \ - column._label: - return _CompileLabel( - column, - column._label, - alt_names=(column._key_label, ) + def _label_select_column(self, select, column, + populate_result_map, + asfrom, column_clause_args, + name=None, + within_columns_clause=True): + """produce labeled columns present in a select().""" + + if column.type._has_column_expression and \ + populate_result_map: + col_expr = column.type.column_expression(column) + add_to_result_map = lambda keyname, name, objects, type_: \ + self._add_to_result_map( + keyname, name, + objects + (column,), type_) + else: + col_expr = column + if populate_result_map: + add_to_result_map = self._add_to_result_map + else: + add_to_result_map = None + + if not within_columns_clause: + result_expr = col_expr + elif isinstance(column, elements.Label): + if col_expr is not column: + result_expr = _CompileLabel( + col_expr, + column.name, + alt_names=(column.element,) + ) + else: + result_expr = col_expr + + elif select is not None and name: + result_expr = _CompileLabel( + col_expr, + name, + alt_names=(column._key_label,) ) elif \ asfrom and \ - isinstance(column, sql.ColumnClause) and \ + isinstance(column, elements.ColumnClause) and \ not column.is_literal and \ column.table is not None and \ - not isinstance(column.table, sql.Select): - return _CompileLabel(column, sql._as_truncated(column.name), - alt_names=(column.key,)) + not isinstance(column.table, selectable.Select): + result_expr = _CompileLabel(col_expr, + elements._as_truncated(column.name), + alt_names=(column.key,)) elif not isinstance(column, - (sql._UnaryExpression, sql._TextClause)) \ + (elements.UnaryExpression, elements.TextClause)) \ and (not hasattr(column, 'name') or \ - isinstance(column, sql.Function)): - return _CompileLabel(column, column.anon_label) + isinstance(column, functions.Function)): + result_expr = _CompileLabel(col_expr, column.anon_label) + elif col_expr is not column: + # TODO: are we sure "column" has a .name and .key here ? + # assert isinstance(column, elements.ColumnClause) + result_expr = _CompileLabel(col_expr, + elements._as_truncated(column.name), + alt_names=(column.key,)) else: - return column + result_expr = col_expr + + column_clause_args.update( + within_columns_clause=within_columns_clause, + add_to_result_map=add_to_result_map + ) + return result_expr._compiler_dispatch( + self, + **column_clause_args + ) + + def format_from_hint_text(self, sqltext, table, hint, iscrud): + hinttext = self.get_from_hint_text(table, hint) + if hinttext: + sqltext += " " + hinttext + return sqltext def get_select_hint_text(self, byfroms): return None @@ -912,46 +1271,206 @@ class SQLCompiler(engine.Compiled): def get_crud_hint_text(self, table, text): return None + def _transform_select_for_nested_joins(self, select): + """Rewrite any "a JOIN (b JOIN c)" expression as + "a JOIN (select * from b JOIN c) AS anon", to support + databases that can't parse a parenthesized join correctly + (i.e. sqlite the main one). + + """ + cloned = {} + column_translate = [{}] + + # TODO: should we be using isinstance() for this, + # as this whole system won't work for custom Join/Select + # subclasses where compilation routines + # call down to compiler.visit_join(), compiler.visit_select() + join_name = selectable.Join.__visit_name__ + select_name = selectable.Select.__visit_name__ + + def visit(element, **kw): + if element in column_translate[-1]: + return column_translate[-1][element] + + elif element in cloned: + return cloned[element] + + newelem = cloned[element] = element._clone() + + if newelem.__visit_name__ is join_name and \ + isinstance(newelem.right, selectable.FromGrouping): + + newelem._reset_exported() + newelem.left = visit(newelem.left, **kw) + + right = visit(newelem.right, **kw) + + selectable_ = selectable.Select( + [right.element], + use_labels=True).alias() + + for c in selectable_.c: + c._key_label = c.key + c._label = c.name + + translate_dict = dict( + zip(newelem.right.element.c, selectable_.c) + ) + + translate_dict[right.element.left] = selectable_ + translate_dict[right.element.right] = selectable_ + + # propagate translations that we've gained + # from nested visit(newelem.right) outwards + # to the enclosing select here. this happens + # only when we have more than one level of right + # join nesting, i.e. "a JOIN (b JOIN (c JOIN d))" + for k, v in list(column_translate[-1].items()): + if v in translate_dict: + # remarkably, no current ORM tests (May 2013) + # hit this condition, only test_join_rewriting + # does. + column_translate[-1][k] = translate_dict[v] + + column_translate[-1].update(translate_dict) + + newelem.right = selectable_ + + newelem.onclause = visit(newelem.onclause, **kw) + elif newelem.__visit_name__ is select_name: + column_translate.append({}) + newelem._copy_internals(clone=visit, **kw) + del column_translate[-1] + else: + newelem._copy_internals(clone=visit, **kw) + + return newelem + + return visit(select) + + def _transform_result_map_for_nested_joins(self, select, transformed_select): + inner_col = dict((c._key_label, c) for + c in transformed_select.inner_columns) + + d = dict( + (inner_col[c._key_label], c) + for c in select.inner_columns + ) + for key, (name, objs, typ) in list(self.result_map.items()): + objs = tuple([d.get(col, col) for col in objs]) + self.result_map[key] = (name, objs, typ) + + + _default_stack_entry = util.immutabledict([ + ('iswrapper', False), + ('correlate_froms', frozenset()), + ('asfrom_froms', frozenset()) + ]) + + def _display_froms_for_select(self, select, asfrom): + # utility method to help external dialects + # get the correct from list for a select. + # specifically the oracle dialect needs this feature + # right now. + toplevel = not self.stack + entry = self._default_stack_entry if toplevel else self.stack[-1] + + correlate_froms = entry['correlate_froms'] + asfrom_froms = entry['asfrom_froms'] + + if asfrom: + froms = select._get_display_froms( + explicit_correlate_froms=\ + correlate_froms.difference(asfrom_froms), + implicit_correlate_froms=()) + else: + froms = select._get_display_froms( + explicit_correlate_froms=correlate_froms, + implicit_correlate_froms=asfrom_froms) + return froms + def visit_select(self, select, asfrom=False, parens=True, iswrapper=False, fromhints=None, compound_index=0, - positional_names=None, **kwargs): + force_result_map=False, + positional_names=None, + nested_join_translation=False, + **kwargs): - entry = self.stack and self.stack[-1] or {} + needs_nested_translation = \ + select.use_labels and \ + not nested_join_translation and \ + not self.stack and \ + not self.dialect.supports_right_nested_joins - existingfroms = entry.get('from', None) + if needs_nested_translation: + transformed_select = self._transform_select_for_nested_joins(select) + text = self.visit_select( + transformed_select, asfrom=asfrom, parens=parens, + iswrapper=iswrapper, fromhints=fromhints, + compound_index=compound_index, + force_result_map=force_result_map, + positional_names=positional_names, + nested_join_translation=True, **kwargs + ) - froms = select._get_display_froms(existingfroms) + toplevel = not self.stack + entry = self._default_stack_entry if toplevel else self.stack[-1] - correlate_froms = set(sql._from_objects(*froms)) - # TODO: might want to propagate existing froms for - # select(select(select)) where innermost select should correlate - # to outermost if existingfroms: correlate_froms = - # correlate_froms.union(existingfroms) + populate_result_map = force_result_map or ( + compound_index == 0 and ( + toplevel or \ + entry['iswrapper'] + ) + ) - populate_result_map = compound_index == 0 and ( - not entry or \ - entry.get('iswrapper', False) - ) + if needs_nested_translation: + if populate_result_map: + self._transform_result_map_for_nested_joins( + select, transformed_select) + return text - self.stack.append({'from': correlate_froms, 'iswrapper': iswrapper}) + correlate_froms = entry['correlate_froms'] + asfrom_froms = entry['asfrom_froms'] - if populate_result_map: - column_clause_args = {'result_map': self.result_map, - 'positional_names': positional_names} + if asfrom: + froms = select._get_display_froms( + explicit_correlate_froms= + correlate_froms.difference(asfrom_froms), + implicit_correlate_froms=()) else: - column_clause_args = {'positional_names': positional_names} + froms = select._get_display_froms( + explicit_correlate_froms=correlate_froms, + implicit_correlate_froms=asfrom_froms) + + new_correlate_froms = set(selectable._from_objects(*froms)) + all_correlate_froms = new_correlate_froms.union(correlate_froms) + + new_entry = { + 'asfrom_froms': new_correlate_froms, + 'iswrapper': iswrapper, + 'correlate_froms': all_correlate_froms + } + self.stack.append(new_entry) + + column_clause_args = kwargs.copy() + column_clause_args.update({ + 'positional_names': positional_names, + 'within_label_clause': False, + 'within_columns_clause': False + }) # the actual list of columns to print in the SELECT column list. inner_columns = [ c for c in [ - self.label_select_column(select, co, asfrom=asfrom).\ - _compiler_dispatch(self, - within_columns_clause=True, - **column_clause_args) - for co in util.unique_list(select.inner_columns) - ] + self._label_select_column(select, + column, + populate_result_map, asfrom, + column_clause_args, + name=name) + for name, column in select._columns_plus_names + ] if c is not None ] @@ -964,7 +1483,7 @@ class SQLCompiler(engine.Compiled): self, ashint=True) }) for (from_, dialect), hinttext in - select._hints.iteritems() + select._hints.items() if dialect in ('*', self.dialect.name) ]) hint_text = self.get_select_hint_text(byfrom) @@ -972,9 +1491,8 @@ class SQLCompiler(engine.Compiled): text += hint_text + " " if select._prefixes: - text += " ".join( - x._compiler_dispatch(self, **kwargs) - for x in select._prefixes) + " " + text += self._generate_prefixes(select, select._prefixes, **kwargs) + text += self.get_select_precolumns(select) text += ', '.join(inner_columns) @@ -1010,15 +1528,23 @@ class SQLCompiler(engine.Compiled): text += " \nHAVING " + t if select._order_by_clause.clauses: - text += self.order_by_clause(select, **kwargs) + if self.dialect.supports_simple_order_by_label: + order_by_select = select + else: + order_by_select = None + + text += self.order_by_clause(select, + order_by_select=order_by_select, **kwargs) + if select._limit is not None or select._offset is not None: text += self.limit_clause(select) - if select.for_update: + + if select._for_update_arg is not None: text += self.for_update_clause(select) if self.ctes and \ - compound_index == 0 and not entry: - text = self._render_cte_clause() + text + compound_index == 0 and toplevel: + text = self._render_cte_clause() + text self.stack.pop(-1) @@ -1027,6 +1553,17 @@ class SQLCompiler(engine.Compiled): else: return text + def _generate_prefixes(self, stmt, prefixes, **kw): + clause = " ".join( + prefix._compiler_dispatch(self, **kw) + for prefix, dialect_name in prefixes + if dialect_name is None or + dialect_name == self.dialect.name + ) + if clause: + clause += " " + return clause + def _render_cte_clause(self): if self.positional: self.positiontup = self.cte_positional + self.positiontup @@ -1058,35 +1595,34 @@ class SQLCompiler(engine.Compiled): return "" def for_update_clause(self, select): - if select.for_update: - return " FOR UPDATE" - else: - return "" + return " FOR UPDATE" + + def returning_clause(self, stmt, returning_cols): + raise exc.CompileError( + "RETURNING is not supported by this " + "dialect's statement compiler.") def limit_clause(self, select): text = "" if select._limit is not None: - text += "\n LIMIT " + self.process(sql.literal(select._limit)) + text += "\n LIMIT " + self.process(elements.literal(select._limit)) if select._offset is not None: if select._limit is None: text += "\n LIMIT -1" - text += " OFFSET " + self.process(sql.literal(select._offset)) + text += " OFFSET " + self.process(elements.literal(select._offset)) return text - def visit_table(self, table, asfrom=False, ashint=False, + def visit_table(self, table, asfrom=False, iscrud=False, ashint=False, fromhints=None, **kwargs): if asfrom or ashint: if getattr(table, "schema", None): - ret = self.preparer.quote_schema(table.schema, - table.quote_schema) + \ - "." + self.preparer.quote(table.name, - table.quote) + ret = self.preparer.quote_schema(table.schema) + \ + "." + self.preparer.quote(table.name) else: - ret = self.preparer.quote(table.name, table.quote) + ret = self.preparer.quote(table.name) if fromhints and table in fromhints: - hinttext = self.get_from_hint_text(table, fromhints[table]) - if hinttext: - ret += " " + hinttext + ret = self.format_from_hint_text(ret, table, + fromhints[table], iscrud) return ret else: return "" @@ -1100,28 +1636,40 @@ class SQLCompiler(engine.Compiled): join.onclause._compiler_dispatch(self, **kwargs) ) - def visit_insert(self, insert_stmt): + def visit_insert(self, insert_stmt, **kw): self.isinsert = True - colparams = self._get_colparams(insert_stmt) + colparams = self._get_colparams(insert_stmt, **kw) if not colparams and \ not self.dialect.supports_default_values and \ not self.dialect.supports_empty_insert: - raise exc.CompileError("The version of %s you are using does " - "not support empty inserts." % + raise exc.CompileError("The '%s' dialect with current database " + "version settings does not support empty " + "inserts." % self.dialect.name) + if insert_stmt._has_multi_parameters: + if not self.dialect.supports_multivalues_insert: + raise exc.CompileError("The '%s' dialect with current database " + "version settings does not support " + "in-place multirow inserts." % + self.dialect.name) + colparams_single = colparams[0] + else: + colparams_single = colparams + + preparer = self.preparer supports_default_values = self.dialect.supports_default_values - text = "INSERT" + text = "INSERT " + if insert_stmt._prefixes: + text += self._generate_prefixes(insert_stmt, + insert_stmt._prefixes, **kw) - prefixes = [self.process(x) for x in insert_stmt._prefixes] - if prefixes: - text += " " + " ".join(prefixes) - - text += " INTO " + preparer.format_table(insert_stmt.table) + text += "INTO " + table_text = preparer.format_table(insert_stmt.table) if insert_stmt._hints: dialect_hints = dict([ @@ -1131,14 +1679,18 @@ class SQLCompiler(engine.Compiled): if dialect in ('*', self.dialect.name) ]) if insert_stmt.table in dialect_hints: - text += " " + self.get_crud_hint_text( + table_text = self.format_from_hint_text( + table_text, insert_stmt.table, - dialect_hints[insert_stmt.table] + dialect_hints[insert_stmt.table], + True ) - if colparams or not supports_default_values: + text += table_text + + if colparams_single or not supports_default_values: text += " (%s)" % ', '.join([preparer.format_column(c[0]) - for c in colparams]) + for c in colparams_single]) if self.returning or insert_stmt._returning: self.returning = self.returning or insert_stmt._returning @@ -1148,8 +1700,19 @@ class SQLCompiler(engine.Compiled): if self.returning_precedes_values: text += " " + returning_clause - if not colparams and supports_default_values: + if insert_stmt.select is not None: + text += " %s" % self.process(insert_stmt.select, **kw) + elif not colparams and supports_default_values: text += " DEFAULT VALUES" + elif insert_stmt._has_multi_parameters: + text += " VALUES %s" % ( + ", ".join( + "(%s)" % ( + ', '.join(c[1] for c in colparam_set) + ) + for colparam_set in colparams + ) + ) else: text += " VALUES (%s)" % \ ', '.join([c[1] for c in colparams]) @@ -1171,7 +1734,8 @@ class SQLCompiler(engine.Compiled): MySQL overrides this. """ - return self.preparer.format_table(from_table) + return from_table._compiler_dispatch(self, asfrom=True, + iscrud=True, **kw) def update_from_clause(self, update_stmt, from_table, extra_froms, @@ -1189,18 +1753,25 @@ class SQLCompiler(engine.Compiled): for t in extra_froms) def visit_update(self, update_stmt, **kw): - self.stack.append({'from': set([update_stmt.table])}) + self.stack.append( + {'correlate_froms': set([update_stmt.table]), + "iswrapper": False, + "asfrom_froms": set([update_stmt.table])}) self.isupdate = True extra_froms = update_stmt._extra_froms - colparams = self._get_colparams(update_stmt, extra_froms) + text = "UPDATE " - text = "UPDATE " + self.update_tables_clause( - update_stmt, - update_stmt.table, - extra_froms, **kw) + if update_stmt._prefixes: + text += self._generate_prefixes(update_stmt, + update_stmt._prefixes, **kw) + + table_text = self.update_tables_clause(update_stmt, update_stmt.table, + extra_froms, **kw) + + colparams = self._get_colparams(update_stmt, extra_froms, **kw) if update_stmt._hints: dialect_hints = dict([ @@ -1210,30 +1781,32 @@ class SQLCompiler(engine.Compiled): if dialect in ('*', self.dialect.name) ]) if update_stmt.table in dialect_hints: - text += " " + self.get_crud_hint_text( + table_text = self.format_from_hint_text( + table_text, update_stmt.table, - dialect_hints[update_stmt.table] + dialect_hints[update_stmt.table], + True ) else: dialect_hints = None - text += ' SET ' - if extra_froms and self.render_table_with_column_in_update_from: - text += ', '.join( - self.visit_column(c[0]) + - '=' + c[1] for c in colparams - ) - else: - text += ', '.join( - self.preparer.quote(c[0].name, c[0].quote) + - '=' + c[1] for c in colparams - ) + text += table_text - if update_stmt._returning: - self.returning = update_stmt._returning + text += ' SET ' + include_table = extra_froms and \ + self.render_table_with_column_in_update_from + text += ', '.join( + c[0]._compiler_dispatch(self, + include_table=include_table) + + '=' + c[1] for c in colparams + ) + + if self.returning or update_stmt._returning: + if not self.returning: + self.returning = update_stmt._returning if self.returning_precedes_values: text += " " + self.returning_clause( - update_stmt, update_stmt._returning) + update_stmt, self.returning) if extra_froms: extra_from_text = self.update_from_clause( @@ -1253,20 +1826,21 @@ class SQLCompiler(engine.Compiled): if self.returning and not self.returning_precedes_values: text += " " + self.returning_clause( - update_stmt, update_stmt._returning) + update_stmt, self.returning) self.stack.pop(-1) return text - def _create_crud_bind_param(self, col, value, required=False): - bindparam = sql.bindparam(col.key, value, + def _create_crud_bind_param(self, col, value, required=False, name=None): + if name is None: + name = col.key + bindparam = elements.BindParameter(name, value, type_=col.type, required=required) bindparam._is_crud = True return bindparam._compiler_dispatch(self) - - def _get_colparams(self, stmt, extra_tables=None): + def _get_colparams(self, stmt, extra_tables=None, **kw): """create a set of tuples representing column/string pairs for use in an INSERT or UPDATE statement. @@ -1290,25 +1864,40 @@ class SQLCompiler(engine.Compiled): for c in stmt.table.columns ] - required = object() + if stmt._has_multi_parameters: + stmt_parameters = stmt.parameters[0] + else: + stmt_parameters = stmt.parameters # if we have statement parameters - set defaults in the # compiled params if self.column_keys is None: parameters = {} else: - parameters = dict((sql._column_as_key(key), required) + parameters = dict((elements._column_as_key(key), REQUIRED) for key in self.column_keys - if not stmt.parameters or - key not in stmt.parameters) - - if stmt.parameters is not None: - for k, v in stmt.parameters.iteritems(): - parameters.setdefault(sql._column_as_key(k), v) + if not stmt_parameters or + key not in stmt_parameters) # create a list of column assignment clauses as tuples values = [] + if stmt_parameters is not None: + for k, v in stmt_parameters.items(): + colkey = elements._column_as_key(k) + if colkey is not None: + parameters.setdefault(colkey, v) + else: + # a non-Column expression on the left side; + # add it to values() in an "as-is" state, + # coercing right side to bound param + if elements._is_literal(v): + v = self.process(elements.BindParameter(None, v, type_=k.type), **kw) + else: + v = self.process(v.self_group(), **kw) + + values.append((k, v)) + need_pks = self.isinsert and \ not self.inline and \ not stmt._returning @@ -1317,38 +1906,55 @@ class SQLCompiler(engine.Compiled): self.dialect.implicit_returning and \ stmt.table.implicit_returning + if self.isinsert: + implicit_return_defaults = implicit_returning and stmt._return_defaults + elif self.isupdate: + implicit_return_defaults = self.dialect.implicit_returning and \ + stmt.table.implicit_returning and \ + stmt._return_defaults + + if implicit_return_defaults: + if stmt._return_defaults is True: + implicit_return_defaults = set(stmt.table.c) + else: + implicit_return_defaults = set(stmt._return_defaults) + postfetch_lastrowid = need_pks and self.dialect.postfetch_lastrowid check_columns = {} # special logic that only occurs for multi-table UPDATE # statements - if extra_tables and stmt.parameters: + if extra_tables and stmt_parameters: + normalized_params = dict( + (elements._clause_element_as_expr(c), param) + for c, param in stmt_parameters.items() + ) assert self.isupdate affected_tables = set() for t in extra_tables: for c in t.c: - if c in stmt.parameters: + if c in normalized_params: affected_tables.add(t) check_columns[c.key] = c - value = stmt.parameters[c] - if sql._is_literal(value): + value = normalized_params[c] + if elements._is_literal(value): value = self._create_crud_bind_param( - c, value, required=value is required) + c, value, required=value is REQUIRED) else: self.postfetch.append(c) - value = self.process(value.self_group()) + value = self.process(value.self_group(), **kw) values.append((c, value)) # determine tables which are actually # to be updated - process onupdate and # server_onupdate for these for t in affected_tables: for c in t.c: - if c in stmt.parameters: + if c in normalized_params: continue elif c.onupdate is not None and not c.onupdate.is_sequence: if c.onupdate.is_clause_element: values.append( - (c, self.process(c.onupdate.arg.self_group())) + (c, self.process(c.onupdate.arg.self_group(), **kw)) ) self.postfetch.append(c) else: @@ -1359,21 +1965,42 @@ class SQLCompiler(engine.Compiled): elif c.server_onupdate is not None: self.postfetch.append(c) - # iterating through columns at the top to maintain ordering. - # otherwise we might iterate through individual sets of - # "defaults", "primary key cols", etc. - for c in stmt.table.columns: + if self.isinsert and stmt.select_names: + # for an insert from select, we can only use names that + # are given, so only select for those names. + cols = (stmt.table.c[elements._column_as_key(name)] + for name in stmt.select_names) + else: + # iterate through all table columns to maintain + # ordering, even for those cols that aren't included + cols = stmt.table.columns + + for c in cols: if c.key in parameters and c.key not in check_columns: value = parameters.pop(c.key) - if sql._is_literal(value): + if elements._is_literal(value): value = self._create_crud_bind_param( - c, value, required=value is required) - elif c.primary_key and implicit_returning: - self.returning.append(c) - value = self.process(value.self_group()) + c, value, required=value is REQUIRED, + name=c.key + if not stmt._has_multi_parameters + else "%s_0" % c.key + ) else: - self.postfetch.append(c) - value = self.process(value.self_group()) + if isinstance(value, elements.BindParameter) and \ + value.type._isnull: + value = value._clone() + value.type = c.type + + if c.primary_key and implicit_returning: + self.returning.append(c) + value = self.process(value.self_group(), **kw) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + value = self.process(value.self_group(), **kw) + else: + self.postfetch.append(c) + value = self.process(value.self_group(), **kw) values.append((c, value)) elif self.isinsert: @@ -1391,13 +2018,13 @@ class SQLCompiler(engine.Compiled): if self.dialect.supports_sequences and \ (not c.default.optional or \ not self.dialect.sequences_optional): - proc = self.process(c.default) + proc = self.process(c.default, **kw) values.append((c, proc)) self.returning.append(c) elif c.default.is_clause_element: values.append( (c, - self.process(c.default.arg.self_group())) + self.process(c.default.arg.self_group(), **kw)) ) self.returning.append(c) else: @@ -1408,7 +2035,13 @@ class SQLCompiler(engine.Compiled): else: self.returning.append(c) else: - if c.default is not None or \ + if ( + c.default is not None and + ( + not c.default.is_sequence or + self.dialect.supports_sequences + ) + ) or \ c is stmt.table._autoincrement_column and ( self.dialect.supports_sequences or self.dialect.preexecute_autoincrement_sequences @@ -1425,16 +2058,22 @@ class SQLCompiler(engine.Compiled): if self.dialect.supports_sequences and \ (not c.default.optional or \ not self.dialect.sequences_optional): - proc = self.process(c.default) + proc = self.process(c.default, **kw) values.append((c, proc)) - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: self.postfetch.append(c) elif c.default.is_clause_element: values.append( - (c, self.process(c.default.arg.self_group())) + (c, self.process(c.default.arg.self_group(), **kw)) ) - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: # dont add primary key column to postfetch self.postfetch.append(c) else: @@ -1443,41 +2082,87 @@ class SQLCompiler(engine.Compiled): ) self.prefetch.append(c) elif c.server_default is not None: - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: self.postfetch.append(c) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) elif self.isupdate: if c.onupdate is not None and not c.onupdate.is_sequence: if c.onupdate.is_clause_element: values.append( - (c, self.process(c.onupdate.arg.self_group())) + (c, self.process(c.onupdate.arg.self_group(), **kw)) ) - self.postfetch.append(c) + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + else: + self.postfetch.append(c) else: values.append( (c, self._create_crud_bind_param(c, None)) ) self.prefetch.append(c) elif c.server_onupdate is not None: - self.postfetch.append(c) + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + else: + self.postfetch.append(c) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) - if parameters and stmt.parameters: + if parameters and stmt_parameters: check = set(parameters).intersection( - sql._column_as_key(k) for k in stmt.parameters + elements._column_as_key(k) for k in stmt.parameters ).difference(check_columns) if check: - util.warn( + raise exc.CompileError( "Unconsumed column names: %s" % (", ".join(check)) ) + if stmt._has_multi_parameters: + values_0 = values + values = [values] + + values.extend( + [ + ( + c, + self._create_crud_bind_param( + c, row[c.key], + name="%s_%d" % (c.key, i + 1) + ) + if c.key in row else param + ) + for (c, param) in values_0 + ] + for i, row in enumerate(stmt.parameters[1:]) + ) + return values - def visit_delete(self, delete_stmt): - self.stack.append({'from': set([delete_stmt.table])}) + def visit_delete(self, delete_stmt, **kw): + self.stack.append({'correlate_froms': set([delete_stmt.table]), + "iswrapper": False, + "asfrom_froms": set([delete_stmt.table])}) self.isdelete = True - text = "DELETE FROM " + self.preparer.format_table(delete_stmt.table) + text = "DELETE " + + if delete_stmt._prefixes: + text += self._generate_prefixes(delete_stmt, + delete_stmt._prefixes, **kw) + + text += "FROM " + table_text = delete_stmt.table._compiler_dispatch(self, + asfrom=True, iscrud=True) if delete_stmt._hints: dialect_hints = dict([ @@ -1487,13 +2172,18 @@ class SQLCompiler(engine.Compiled): if dialect in ('*', self.dialect.name) ]) if delete_stmt.table in dialect_hints: - text += " " + self.get_crud_hint_text( + table_text = self.format_from_hint_text( + table_text, delete_stmt.table, - dialect_hints[delete_stmt.table] + dialect_hints[delete_stmt.table], + True ) + else: dialect_hints = None + text += table_text + if delete_stmt._returning: self.returning = delete_stmt._returning if self.returning_precedes_values: @@ -1501,7 +2191,8 @@ class SQLCompiler(engine.Compiled): delete_stmt, delete_stmt._returning) if delete_stmt._whereclause is not None: - text += " WHERE " + self.process(delete_stmt._whereclause) + text += " WHERE " + text += delete_stmt._whereclause._compiler_dispatch(self) if self.returning and not self.returning_precedes_values: text += " " + self.returning_clause( @@ -1523,12 +2214,16 @@ class SQLCompiler(engine.Compiled): self.preparer.format_savepoint(savepoint_stmt) -class DDLCompiler(engine.Compiled): +class DDLCompiler(Compiled): @util.memoized_property def sql_compiler(self): return self.dialect.statement_compiler(self.dialect, None) + @util.memoized_property + def type_compiler(self): + return self.dialect.type_compiler + @property def preparer(self): return self.dialect.identifier_preparer @@ -1556,10 +2251,12 @@ class DDLCompiler(engine.Compiled): return self.sql_compiler.post_process_text(ddl.statement % context) def visit_create_schema(self, create): - return "CREATE SCHEMA " + self.preparer.format_schema(create.element, create.quote) + schema = self.preparer.format_schema(create.element) + return "CREATE SCHEMA " + schema def visit_drop_schema(self, drop): - text = "DROP SCHEMA " + self.preparer.format_schema(drop.element, drop.quote) + schema = self.preparer.format_schema(drop.element) + text = "DROP SCHEMA " + schema if drop.cascade: text += " CASCADE" return text @@ -1577,37 +2274,25 @@ class DDLCompiler(engine.Compiled): # if only one primary key, specify it along with the column first_pk = False - for column in table.columns: + for create_column in create.columns: + column = create_column.element try: - text += separator - separator = ", \n" - text += "\t" + self.get_column_specification( - column, - first_pk=column.primary_key and \ - not first_pk - ) + processed = self.process(create_column, + first_pk=column.primary_key + and not first_pk) + if processed is not None: + text += separator + separator = ", \n" + text += "\t" + processed if column.primary_key: first_pk = True - const = " ".join(self.process(constraint) \ - for constraint in column.constraints) - if const: - text += " " + const - except exc.CompileError, ce: - # Py3K - #raise exc.CompileError("(in table '%s', column '%s'): %s" - # % ( - # table.description, - # column.name, - # ce.args[0] - # )) from ce - # Py2K - raise exc.CompileError("(in table '%s', column '%s'): %s" - % ( + except exc.CompileError as ce: + util.raise_from_cause( + exc.CompileError(util.u("(in table '%s', column '%s'): %s") % ( table.description, column.name, ce.args[0] - )), None, sys.exc_info()[2] - # end Py2K + ))) const = self.create_table_constraints(table) if const: @@ -1616,6 +2301,23 @@ class DDLCompiler(engine.Compiled): text += "\n)%s\n\n" % self.post_create_table(table) return text + def visit_create_column(self, create, first_pk=False): + column = create.element + + if column.system: + return None + + text = self.get_column_specification( + column, + first_pk=first_pk + ) + const = " ".join(self.process(constraint) \ + for constraint in column.constraints) + if const: + text += " " + const + + return text + def create_table_constraints(self, table): # On some DB order is significant: visit PK first, then the @@ -1642,51 +2344,66 @@ class DDLCompiler(engine.Compiled): def visit_drop_table(self, drop): return "\nDROP TABLE " + self.preparer.format_table(drop.element) - def _index_identifier(self, ident): - if isinstance(ident, sql._truncated_label): - max = self.dialect.max_index_name_length or \ - self.dialect.max_identifier_length - if len(ident) > max: - ident = ident[0:max - 8] + \ - "_" + util.md5_hex(ident)[-4:] - else: - self.dialect.validate_identifier(ident) + def visit_drop_view(self, drop): + return "\nDROP VIEW " + self.preparer.format_table(drop.element) - return ident - def visit_create_index(self, create): + def _verify_index_table(self, index): + if index.table is None: + raise exc.CompileError("Index '%s' is not associated " + "with any table." % index.name) + + + def visit_create_index(self, create, include_schema=False, + include_table_schema=True): index = create.element + self._verify_index_table(index) preparer = self.preparer text = "CREATE " if index.unique: text += "UNIQUE " text += "INDEX %s ON %s (%s)" \ - % (preparer.quote(self._index_identifier(index.name), - index.quote), - preparer.format_table(index.table), - ', '.join(preparer.quote(c.name, c.quote) - for c in index.columns)) + % ( + self._prepared_index_name(index, + include_schema=include_schema), + preparer.format_table(index.table, + use_schema=include_table_schema), + ', '.join( + self.sql_compiler.process(expr, + include_table=False, literal_binds=True) for + expr in index.expressions) + ) return text def visit_drop_index(self, drop): index = drop.element - if index.table is not None and index.table.schema: + return "\nDROP INDEX " + self._prepared_index_name(index, + include_schema=True) + + def _prepared_index_name(self, index, include_schema=False): + if include_schema and index.table is not None and index.table.schema: schema = index.table.schema - schema_name = self.preparer.quote_schema(schema, - index.table.quote_schema) + schema_name = self.preparer.quote_schema(schema) else: schema_name = None - index_name = self.preparer.quote( - self._index_identifier(index.name), - index.quote) + ident = index.name + if isinstance(ident, elements._truncated_label): + max_ = self.dialect.max_index_name_length or \ + self.dialect.max_identifier_length + if len(ident) > max_: + ident = ident[0:max_ - 8] + \ + "_" + util.md5_hex(ident)[-4:] + else: + self.dialect.validate_identifier(ident) + + index_name = self.preparer.quote(ident) if schema_name: index_name = schema_name + "." + index_name - return "\nDROP INDEX " + index_name + return index_name def visit_add_constraint(self, create): - preparer = self.preparer return "ALTER TABLE %s ADD %s" % ( self.preparer.format_table(create.element.table), self.process(create.element) @@ -1706,7 +2423,6 @@ class DDLCompiler(engine.Compiled): self.preparer.format_sequence(drop.element) def visit_drop_constraint(self, drop): - preparer = self.preparer return "ALTER TABLE %s DROP CONSTRAINT %s%s" % ( self.preparer.format_table(drop.element.table), self.preparer.format_constraint(drop.element), @@ -1729,7 +2445,7 @@ class DDLCompiler(engine.Compiled): def get_column_default_string(self, column): if isinstance(column.server_default, schema.DefaultClause): - if isinstance(column.server_default.arg, basestring): + if isinstance(column.server_default.arg, util.string_types): return "'%s'" % column.server_default.arg else: return self.sql_compiler.process(column.server_default.arg) @@ -1741,8 +2457,9 @@ class DDLCompiler(engine.Compiled): if constraint.name is not None: text += "CONSTRAINT %s " % \ self.preparer.format_constraint(constraint) - sqltext = sql_util.expression_as_ddl(constraint.sqltext) - text += "CHECK (%s)" % self.sql_compiler.process(sqltext) + text += "CHECK (%s)" % self.sql_compiler.process(constraint.sqltext, + include_table=False, + literal_binds=True) text += self.define_constraint_deferrability(constraint) return text @@ -1763,7 +2480,7 @@ class DDLCompiler(engine.Compiled): text += "CONSTRAINT %s " % \ self.preparer.format_constraint(constraint) text += "PRIMARY KEY " - text += "(%s)" % ', '.join(self.preparer.quote(c.name, c.quote) + text += "(%s)" % ', '.join(self.preparer.quote(c.name) for c in constraint) text += self.define_constraint_deferrability(constraint) return text @@ -1776,13 +2493,14 @@ class DDLCompiler(engine.Compiled): preparer.format_constraint(constraint) remote_table = list(constraint._elements.values())[0].column.table text += "FOREIGN KEY(%s) REFERENCES %s (%s)" % ( - ', '.join(preparer.quote(f.parent.name, f.parent.quote) + ', '.join(preparer.quote(f.parent.name) for f in constraint._elements.values()), self.define_constraint_remote_table( constraint, remote_table, preparer), - ', '.join(preparer.quote(f.column.name, f.column.quote) + ', '.join(preparer.quote(f.column.name) for f in constraint._elements.values()) ) + text += self.define_constraint_match(constraint) text += self.define_constraint_cascades(constraint) text += self.define_constraint_deferrability(constraint) return text @@ -1798,7 +2516,7 @@ class DDLCompiler(engine.Compiled): text += "CONSTRAINT %s " % \ self.preparer.format_constraint(constraint) text += "UNIQUE (%s)" % ( - ', '.join(self.preparer.quote(c.name, c.quote) + ', '.join(self.preparer.quote(c.name) for c in constraint)) text += self.define_constraint_deferrability(constraint) return text @@ -1822,13 +2540,14 @@ class DDLCompiler(engine.Compiled): text += " INITIALLY %s" % constraint.initially return text + def define_constraint_match(self, constraint): + text = "" + if constraint.match is not None: + text += " MATCH %s" % constraint.match + return text -class GenericTypeCompiler(engine.TypeCompiler): - def visit_CHAR(self, type_): - return "CHAR" + (type_.length and "(%d)" % type_.length or "") - def visit_NCHAR(self, type_): - return "NCHAR" + (type_.length and "(%d)" % type_.length or "") +class GenericTypeCompiler(TypeCompiler): def visit_FLOAT(self, type_): return "FLOAT" @@ -1845,10 +2564,18 @@ class GenericTypeCompiler(engine.TypeCompiler): else: return "NUMERIC(%(precision)s, %(scale)s)" % \ {'precision': type_.precision, - 'scale' : type_.scale} + 'scale': type_.scale} def visit_DECIMAL(self, type_): - return "DECIMAL" + if type_.precision is None: + return "DECIMAL" + elif type_.scale is None: + return "DECIMAL(%(precision)s)" % \ + {'precision': type_.precision} + else: + return "DECIMAL(%(precision)s, %(scale)s)" % \ + {'precision': type_.precision, + 'scale': type_.scale} def visit_INTEGER(self, type_): return "INTEGER" @@ -1877,11 +2604,29 @@ class GenericTypeCompiler(engine.TypeCompiler): def visit_NCLOB(self, type_): return "NCLOB" + def _render_string_type(self, type_, name): + + text = name + if type_.length: + text += "(%d)" % type_.length + if type_.collation: + text += ' COLLATE "%s"' % type_.collation + return text + + def visit_CHAR(self, type_): + return self._render_string_type(type_, "CHAR") + + def visit_NCHAR(self, type_): + return self._render_string_type(type_, "NCHAR") + def visit_VARCHAR(self, type_): - return "VARCHAR" + (type_.length and "(%d)" % type_.length or "") + return self._render_string_type(type_, "VARCHAR") def visit_NVARCHAR(self, type_): - return "NVARCHAR" + (type_.length and "(%d)" % type_.length or "") + return self._render_string_type(type_, "NVARCHAR") + + def visit_TEXT(self, type_): + return self._render_string_type(type_, "TEXT") def visit_BLOB(self, type_): return "BLOB" @@ -1895,9 +2640,6 @@ class GenericTypeCompiler(engine.TypeCompiler): def visit_BOOLEAN(self, type_): return "BOOLEAN" - def visit_TEXT(self, type_): - return "TEXT" - def visit_large_binary(self, type_): return self.visit_BLOB(type_) @@ -1947,7 +2689,9 @@ class GenericTypeCompiler(engine.TypeCompiler): return self.visit_VARCHAR(type_) def visit_null(self, type_): - raise NotImplementedError("Can't generate DDL for the null type") + raise exc.CompileError("Can't generate DDL for %r; " + "did you forget to specify a " + "type on this Column?" % type_) def visit_type_decorator(self, type_): return self.process(type_.type_engine(self.dialect)) @@ -1955,6 +2699,7 @@ class GenericTypeCompiler(engine.TypeCompiler): def visit_user_defined(self, type_): return type_.get_col_spec() + class IdentifierPreparer(object): """Handle quoting and case-folding of identifiers based on options.""" @@ -2022,18 +2767,28 @@ class IdentifierPreparer(object): lc_value = value.lower() return (lc_value in self.reserved_words or value[0] in self.illegal_initial_characters - or not self.legal_characters.match(unicode(value)) + or not self.legal_characters.match(util.text_type(value)) or (lc_value != value)) - def quote_schema(self, schema, force): - """Quote a schema. + def quote_schema(self, schema, force=None): + """Conditionally quote a schema. + + Subclasses can override this to provide database-dependent + quoting behavior for schema names. + + the 'force' flag should be considered deprecated. - Subclasses should override this to provide database-dependent - quoting behavior. """ return self.quote(schema, force) - def quote(self, ident, force): + def quote(self, ident, force=None): + """Conditionally quote an identifier. + + the 'force' flag should be considered deprecated. + """ + + force = getattr(ident, "quote", None) + if force is None: if ident in self._strings: return self._strings[ident] @@ -2049,38 +2804,35 @@ class IdentifierPreparer(object): return ident def format_sequence(self, sequence, use_schema=True): - name = self.quote(sequence.name, sequence.quote) - if not self.omit_schema and use_schema and \ - sequence.schema is not None: - name = self.quote_schema(sequence.schema, sequence.quote) + \ - "." + name + name = self.quote(sequence.name) + if not self.omit_schema and use_schema and sequence.schema is not None: + name = self.quote_schema(sequence.schema) + "." + name return name def format_label(self, label, name=None): - return self.quote(name or label.name, label.quote) + return self.quote(name or label.name) def format_alias(self, alias, name=None): - return self.quote(name or alias.name, alias.quote) + return self.quote(name or alias.name) def format_savepoint(self, savepoint, name=None): - return self.quote(name or savepoint.ident, savepoint.quote) + return self.quote(name or savepoint.ident) def format_constraint(self, constraint): - return self.quote(constraint.name, constraint.quote) + return self.quote(constraint.name) def format_table(self, table, use_schema=True, name=None): """Prepare a quoted table and schema name.""" if name is None: name = table.name - result = self.quote(name, table.quote) + result = self.quote(name) if not self.omit_schema and use_schema \ and getattr(table, "schema", None): - result = self.quote_schema(table.schema, table.quote_schema) + \ - "." + result + result = self.quote_schema(table.schema) + "." + result return result - def format_schema(self, name, quote): + def format_schema(self, name, quote=None): """Prepare a quoted schema name.""" return self.quote(name, quote) @@ -2095,10 +2847,9 @@ class IdentifierPreparer(object): if use_table: return self.format_table( column.table, use_schema=False, - name=table_name) + "." + \ - self.quote(name, column.quote) + name=table_name) + "." + self.quote(name) else: - return self.quote(name, column.quote) + return self.quote(name) else: # literal textual elements get stuck into ColumnClause a lot, # which shouldn't get quoted @@ -2118,7 +2869,7 @@ class IdentifierPreparer(object): if not self.omit_schema and use_schema and \ getattr(table, 'schema', None): - return (self.quote_schema(table.schema, table.quote_schema), + return (self.quote_schema(table.schema), self.format_table(table, use_schema=False)) else: return (self.format_table(table, use_schema=False), ) @@ -2133,9 +2884,9 @@ class IdentifierPreparer(object): r'(?:' r'(?:%(initial)s((?:%(escaped)s|[^%(final)s])+)%(final)s' r'|([^\.]+))(?=\.|$))+' % - { 'initial': initial, - 'final': final, - 'escaped': escaped_final }) + {'initial': initial, + 'final': final, + 'escaped': escaped_final}) return r def unformat_identifiers(self, identifiers): diff --git a/libs/sqlalchemy/sql/ddl.py b/libs/sqlalchemy/sql/ddl.py new file mode 100644 index 00000000..bda87650 --- /dev/null +++ b/libs/sqlalchemy/sql/ddl.py @@ -0,0 +1,864 @@ +# sql/ddl.py +# Copyright (C) 2009-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +""" +Provides the hierarchy of DDL-defining schema items as well as routines +to invoke them for a create/drop call. + +""" + +from .. import util +from .elements import ClauseElement +from .visitors import traverse +from .base import Executable, _generative, SchemaVisitor, _bind_or_error +from ..util import topological +from .. import event +from .. import exc + +class _DDLCompiles(ClauseElement): + def _compiler(self, dialect, **kw): + """Return a compiler appropriate for this ClauseElement, given a + Dialect.""" + + return dialect.ddl_compiler(dialect, self, **kw) + + +class DDLElement(Executable, _DDLCompiles): + """Base class for DDL expression constructs. + + This class is the base for the general purpose :class:`.DDL` class, + as well as the various create/drop clause constructs such as + :class:`.CreateTable`, :class:`.DropTable`, :class:`.AddConstraint`, + etc. + + :class:`.DDLElement` integrates closely with SQLAlchemy events, + introduced in :ref:`event_toplevel`. An instance of one is + itself an event receiving callable:: + + event.listen( + users, + 'after_create', + AddConstraint(constraint).execute_if(dialect='postgresql') + ) + + .. seealso:: + + :class:`.DDL` + + :class:`.DDLEvents` + + :ref:`event_toplevel` + + :ref:`schema_ddl_sequences` + + """ + + _execution_options = Executable.\ + _execution_options.union({'autocommit': True}) + + target = None + on = None + dialect = None + callable_ = None + + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_ddl(self, multiparams, params) + + def execute(self, bind=None, target=None): + """Execute this DDL immediately. + + Executes the DDL statement in isolation using the supplied + :class:`.Connectable` or + :class:`.Connectable` assigned to the ``.bind`` + property, if not supplied. If the DDL has a conditional ``on`` + criteria, it will be invoked with None as the event. + + :param bind: + Optional, an ``Engine`` or ``Connection``. If not supplied, a valid + :class:`.Connectable` must be present in the + ``.bind`` property. + + :param target: + Optional, defaults to None. The target SchemaItem for the + execute call. Will be passed to the ``on`` callable if any, + and may also provide string expansion data for the + statement. See ``execute_at`` for more information. + + """ + + if bind is None: + bind = _bind_or_error(self) + + if self._should_execute(target, bind): + return bind.execute(self.against(target)) + else: + bind.engine.logger.info( + "DDL execution skipped, criteria not met.") + + @util.deprecated("0.7", "See :class:`.DDLEvents`, as well as " + ":meth:`.DDLElement.execute_if`.") + def execute_at(self, event_name, target): + """Link execution of this DDL to the DDL lifecycle of a SchemaItem. + + Links this ``DDLElement`` to a ``Table`` or ``MetaData`` instance, + executing it when that schema item is created or dropped. The DDL + statement will be executed using the same Connection and transactional + context as the Table create/drop itself. The ``.bind`` property of + this statement is ignored. + + :param event: + One of the events defined in the schema item's ``.ddl_events``; + e.g. 'before-create', 'after-create', 'before-drop' or 'after-drop' + + :param target: + The Table or MetaData instance for which this DDLElement will + be associated with. + + A DDLElement instance can be linked to any number of schema items. + + ``execute_at`` builds on the ``append_ddl_listener`` interface of + :class:`.MetaData` and :class:`.Table` objects. + + Caveat: Creating or dropping a Table in isolation will also trigger + any DDL set to ``execute_at`` that Table's MetaData. This may change + in a future release. + + """ + + def call_event(target, connection, **kw): + if self._should_execute_deprecated(event_name, + target, connection, **kw): + return connection.execute(self.against(target)) + + event.listen(target, "" + event_name.replace('-', '_'), call_event) + + @_generative + def against(self, target): + """Return a copy of this DDL against a specific schema item.""" + + self.target = target + + @_generative + def execute_if(self, dialect=None, callable_=None, state=None): + """Return a callable that will execute this + DDLElement conditionally. + + Used to provide a wrapper for event listening:: + + event.listen( + metadata, + 'before_create', + DDL("my_ddl").execute_if(dialect='postgresql') + ) + + :param dialect: May be a string, tuple or a callable + predicate. If a string, it will be compared to the name of the + executing database dialect:: + + DDL('something').execute_if(dialect='postgresql') + + If a tuple, specifies multiple dialect names:: + + DDL('something').execute_if(dialect=('postgresql', 'mysql')) + + :param callable_: A callable, which will be invoked with + four positional arguments as well as optional keyword + arguments: + + :ddl: + This DDL element. + + :target: + The :class:`.Table` or :class:`.MetaData` object which is the + target of this event. May be None if the DDL is executed + explicitly. + + :bind: + The :class:`.Connection` being used for DDL execution + + :tables: + Optional keyword argument - a list of Table objects which are to + be created/ dropped within a MetaData.create_all() or drop_all() + method call. + + :state: + Optional keyword argument - will be the ``state`` argument + passed to this function. + + :checkfirst: + Keyword argument, will be True if the 'checkfirst' flag was + set during the call to ``create()``, ``create_all()``, + ``drop()``, ``drop_all()``. + + If the callable returns a true value, the DDL statement will be + executed. + + :param state: any value which will be passed to the callable\_ + as the ``state`` keyword argument. + + .. seealso:: + + :class:`.DDLEvents` + + :ref:`event_toplevel` + + """ + self.dialect = dialect + self.callable_ = callable_ + self.state = state + + def _should_execute(self, target, bind, **kw): + if self.on is not None and \ + not self._should_execute_deprecated(None, target, bind, **kw): + return False + + if isinstance(self.dialect, util.string_types): + if self.dialect != bind.engine.name: + return False + elif isinstance(self.dialect, (tuple, list, set)): + if bind.engine.name not in self.dialect: + return False + if self.callable_ is not None and \ + not self.callable_(self, target, bind, state=self.state, **kw): + return False + + return True + + def _should_execute_deprecated(self, event, target, bind, **kw): + if self.on is None: + return True + elif isinstance(self.on, util.string_types): + return self.on == bind.engine.name + elif isinstance(self.on, (tuple, list, set)): + return bind.engine.name in self.on + else: + return self.on(self, event, target, bind, **kw) + + def __call__(self, target, bind, **kw): + """Execute the DDL as a ddl_listener.""" + + if self._should_execute(target, bind, **kw): + return bind.execute(self.against(target)) + + def _check_ddl_on(self, on): + if (on is not None and + (not isinstance(on, util.string_types + (tuple, list, set)) and + not util.callable(on))): + raise exc.ArgumentError( + "Expected the name of a database dialect, a tuple " + "of names, or a callable for " + "'on' criteria, got type '%s'." % type(on).__name__) + + def bind(self): + if self._bind: + return self._bind + + def _set_bind(self, bind): + self._bind = bind + bind = property(bind, _set_bind) + + def _generate(self): + s = self.__class__.__new__(self.__class__) + s.__dict__ = self.__dict__.copy() + return s + + +class DDL(DDLElement): + """A literal DDL statement. + + Specifies literal SQL DDL to be executed by the database. DDL objects + function as DDL event listeners, and can be subscribed to those events + listed in :class:`.DDLEvents`, using either :class:`.Table` or + :class:`.MetaData` objects as targets. Basic templating support allows + a single DDL instance to handle repetitive tasks for multiple tables. + + Examples:: + + from sqlalchemy import event, DDL + + tbl = Table('users', metadata, Column('uid', Integer)) + event.listen(tbl, 'before_create', DDL('DROP TRIGGER users_trigger')) + + spow = DDL('ALTER TABLE %(table)s SET secretpowers TRUE') + event.listen(tbl, 'after_create', spow.execute_if(dialect='somedb')) + + drop_spow = DDL('ALTER TABLE users SET secretpowers FALSE') + connection.execute(drop_spow) + + When operating on Table events, the following ``statement`` + string substitions are available:: + + %(table)s - the Table name, with any required quoting applied + %(schema)s - the schema name, with any required quoting applied + %(fullname)s - the Table name including schema, quoted if needed + + The DDL's "context", if any, will be combined with the standard + substutions noted above. Keys present in the context will override + the standard substitutions. + + """ + + __visit_name__ = "ddl" + + def __init__(self, statement, on=None, context=None, bind=None): + """Create a DDL statement. + + :param statement: + A string or unicode string to be executed. Statements will be + processed with Python's string formatting operator. See the + ``context`` argument and the ``execute_at`` method. + + A literal '%' in a statement must be escaped as '%%'. + + SQL bind parameters are not available in DDL statements. + + :param on: + .. deprecated:: 0.7 + See :meth:`.DDLElement.execute_if`. + + Optional filtering criteria. May be a string, tuple or a callable + predicate. If a string, it will be compared to the name of the + executing database dialect:: + + DDL('something', on='postgresql') + + If a tuple, specifies multiple dialect names:: + + DDL('something', on=('postgresql', 'mysql')) + + If a callable, it will be invoked with four positional arguments + as well as optional keyword arguments: + + :ddl: + This DDL element. + + :event: + The name of the event that has triggered this DDL, such as + 'after-create' Will be None if the DDL is executed explicitly. + + :target: + The ``Table`` or ``MetaData`` object which is the target of + this event. May be None if the DDL is executed explicitly. + + :connection: + The ``Connection`` being used for DDL execution + + :tables: + Optional keyword argument - a list of Table objects which are to + be created/ dropped within a MetaData.create_all() or drop_all() + method call. + + + If the callable returns a true value, the DDL statement will be + executed. + + :param context: + Optional dictionary, defaults to None. These values will be + available for use in string substitutions on the DDL statement. + + :param bind: + Optional. A :class:`.Connectable`, used by + default when ``execute()`` is invoked without a bind argument. + + + .. seealso:: + + :class:`.DDLEvents` + + :mod:`sqlalchemy.event` + + """ + + if not isinstance(statement, util.string_types): + raise exc.ArgumentError( + "Expected a string or unicode SQL statement, got '%r'" % + statement) + + self.statement = statement + self.context = context or {} + + self._check_ddl_on(on) + self.on = on + self._bind = bind + + def __repr__(self): + return '<%s@%s; %s>' % ( + type(self).__name__, id(self), + ', '.join([repr(self.statement)] + + ['%s=%r' % (key, getattr(self, key)) + for key in ('on', 'context') + if getattr(self, key)])) + + + +class _CreateDropBase(DDLElement): + """Base class for DDL constucts that represent CREATE and DROP or + equivalents. + + The common theme of _CreateDropBase is a single + ``element`` attribute which refers to the element + to be created or dropped. + + """ + + def __init__(self, element, on=None, bind=None): + self.element = element + self._check_ddl_on(on) + self.on = on + self.bind = bind + + def _create_rule_disable(self, compiler): + """Allow disable of _create_rule using a callable. + + Pass to _create_rule using + util.portable_instancemethod(self._create_rule_disable) + to retain serializability. + + """ + return False + + +class CreateSchema(_CreateDropBase): + """Represent a CREATE SCHEMA statement. + + .. versionadded:: 0.7.4 + + The argument here is the string name of the schema. + + """ + + __visit_name__ = "create_schema" + + def __init__(self, name, quote=None, **kw): + """Create a new :class:`.CreateSchema` construct.""" + + self.quote = quote + super(CreateSchema, self).__init__(name, **kw) + + +class DropSchema(_CreateDropBase): + """Represent a DROP SCHEMA statement. + + The argument here is the string name of the schema. + + .. versionadded:: 0.7.4 + + """ + + __visit_name__ = "drop_schema" + + def __init__(self, name, quote=None, cascade=False, **kw): + """Create a new :class:`.DropSchema` construct.""" + + self.quote = quote + self.cascade = cascade + super(DropSchema, self).__init__(name, **kw) + + +class CreateTable(_CreateDropBase): + """Represent a CREATE TABLE statement.""" + + __visit_name__ = "create_table" + + def __init__(self, element, on=None, bind=None): + """Create a :class:`.CreateTable` construct. + + :param element: a :class:`.Table` that's the subject + of the CREATE + :param on: See the description for 'on' in :class:`.DDL`. + :param bind: See the description for 'bind' in :class:`.DDL`. + + """ + super(CreateTable, self).__init__(element, on=on, bind=bind) + self.columns = [CreateColumn(column) + for column in element.columns + ] + + +class _DropView(_CreateDropBase): + """Semi-public 'DROP VIEW' construct. + + Used by the test suite for dialect-agnostic drops of views. + This object will eventually be part of a public "view" API. + + """ + __visit_name__ = "drop_view" + + +class CreateColumn(_DDLCompiles): + """Represent a :class:`.Column` as rendered in a CREATE TABLE statement, + via the :class:`.CreateTable` construct. + + This is provided to support custom column DDL within the generation + of CREATE TABLE statements, by using the + compiler extension documented in :ref:`sqlalchemy.ext.compiler_toplevel` + to extend :class:`.CreateColumn`. + + Typical integration is to examine the incoming :class:`.Column` + object, and to redirect compilation if a particular flag or condition + is found:: + + from sqlalchemy import schema + from sqlalchemy.ext.compiler import compiles + + @compiles(schema.CreateColumn) + def compile(element, compiler, **kw): + column = element.element + + if "special" not in column.info: + return compiler.visit_create_column(element, **kw) + + text = "%s SPECIAL DIRECTIVE %s" % ( + column.name, + compiler.type_compiler.process(column.type) + ) + default = compiler.get_column_default_string(column) + if default is not None: + text += " DEFAULT " + default + + if not column.nullable: + text += " NOT NULL" + + if column.constraints: + text += " ".join( + compiler.process(const) + for const in column.constraints) + return text + + The above construct can be applied to a :class:`.Table` as follows:: + + from sqlalchemy import Table, Metadata, Column, Integer, String + from sqlalchemy import schema + + metadata = MetaData() + + table = Table('mytable', MetaData(), + Column('x', Integer, info={"special":True}, primary_key=True), + Column('y', String(50)), + Column('z', String(20), info={"special":True}) + ) + + metadata.create_all(conn) + + Above, the directives we've added to the :attr:`.Column.info` collection + will be detected by our custom compilation scheme:: + + CREATE TABLE mytable ( + x SPECIAL DIRECTIVE INTEGER NOT NULL, + y VARCHAR(50), + z SPECIAL DIRECTIVE VARCHAR(20), + PRIMARY KEY (x) + ) + + The :class:`.CreateColumn` construct can also be used to skip certain + columns when producing a ``CREATE TABLE``. This is accomplished by + creating a compilation rule that conditionally returns ``None``. + This is essentially how to produce the same effect as using the + ``system=True`` argument on :class:`.Column`, which marks a column + as an implicitly-present "system" column. + + For example, suppose we wish to produce a :class:`.Table` which skips + rendering of the Postgresql ``xmin`` column against the Postgresql backend, + but on other backends does render it, in anticipation of a triggered rule. + A conditional compilation rule could skip this name only on Postgresql:: + + from sqlalchemy.schema import CreateColumn + + @compiles(CreateColumn, "postgresql") + def skip_xmin(element, compiler, **kw): + if element.element.name == 'xmin': + return None + else: + return compiler.visit_create_column(element, **kw) + + + my_table = Table('mytable', metadata, + Column('id', Integer, primary_key=True), + Column('xmin', Integer) + ) + + Above, a :class:`.CreateTable` construct will generate a ``CREATE TABLE`` + which only includes the ``id`` column in the string; the ``xmin`` column + will be omitted, but only against the Postgresql backend. + + .. versionadded:: 0.8.3 The :class:`.CreateColumn` construct supports + skipping of columns by returning ``None`` from a custom compilation rule. + + .. versionadded:: 0.8 The :class:`.CreateColumn` construct was added + to support custom column creation styles. + + """ + __visit_name__ = 'create_column' + + def __init__(self, element): + self.element = element + + +class DropTable(_CreateDropBase): + """Represent a DROP TABLE statement.""" + + __visit_name__ = "drop_table" + + +class CreateSequence(_CreateDropBase): + """Represent a CREATE SEQUENCE statement.""" + + __visit_name__ = "create_sequence" + + +class DropSequence(_CreateDropBase): + """Represent a DROP SEQUENCE statement.""" + + __visit_name__ = "drop_sequence" + + +class CreateIndex(_CreateDropBase): + """Represent a CREATE INDEX statement.""" + + __visit_name__ = "create_index" + + +class DropIndex(_CreateDropBase): + """Represent a DROP INDEX statement.""" + + __visit_name__ = "drop_index" + + +class AddConstraint(_CreateDropBase): + """Represent an ALTER TABLE ADD CONSTRAINT statement.""" + + __visit_name__ = "add_constraint" + + def __init__(self, element, *args, **kw): + super(AddConstraint, self).__init__(element, *args, **kw) + element._create_rule = util.portable_instancemethod( + self._create_rule_disable) + + +class DropConstraint(_CreateDropBase): + """Represent an ALTER TABLE DROP CONSTRAINT statement.""" + + __visit_name__ = "drop_constraint" + + def __init__(self, element, cascade=False, **kw): + self.cascade = cascade + super(DropConstraint, self).__init__(element, **kw) + element._create_rule = util.portable_instancemethod( + self._create_rule_disable) + + +class DDLBase(SchemaVisitor): + def __init__(self, connection): + self.connection = connection + + +class SchemaGenerator(DDLBase): + + def __init__(self, dialect, connection, checkfirst=False, + tables=None, **kwargs): + super(SchemaGenerator, self).__init__(connection, **kwargs) + self.checkfirst = checkfirst + self.tables = tables + self.preparer = dialect.identifier_preparer + self.dialect = dialect + self.memo = {} + + def _can_create_table(self, table): + self.dialect.validate_identifier(table.name) + if table.schema: + self.dialect.validate_identifier(table.schema) + return not self.checkfirst or \ + not self.dialect.has_table(self.connection, + table.name, schema=table.schema) + + def _can_create_sequence(self, sequence): + return self.dialect.supports_sequences and \ + ( + (not self.dialect.sequences_optional or + not sequence.optional) and + ( + not self.checkfirst or + not self.dialect.has_sequence( + self.connection, + sequence.name, + schema=sequence.schema) + ) + ) + + def visit_metadata(self, metadata): + if self.tables is not None: + tables = self.tables + else: + tables = list(metadata.tables.values()) + collection = [t for t in sort_tables(tables) + if self._can_create_table(t)] + seq_coll = [s for s in metadata._sequences.values() + if s.column is None and self._can_create_sequence(s)] + + metadata.dispatch.before_create(metadata, self.connection, + tables=collection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + for seq in seq_coll: + self.traverse_single(seq, create_ok=True) + + for table in collection: + self.traverse_single(table, create_ok=True) + + metadata.dispatch.after_create(metadata, self.connection, + tables=collection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + def visit_table(self, table, create_ok=False): + if not create_ok and not self._can_create_table(table): + return + + table.dispatch.before_create(table, self.connection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + for column in table.columns: + if column.default is not None: + self.traverse_single(column.default) + + self.connection.execute(CreateTable(table)) + + if hasattr(table, 'indexes'): + for index in table.indexes: + self.traverse_single(index) + + table.dispatch.after_create(table, self.connection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + def visit_sequence(self, sequence, create_ok=False): + if not create_ok and not self._can_create_sequence(sequence): + return + self.connection.execute(CreateSequence(sequence)) + + def visit_index(self, index): + self.connection.execute(CreateIndex(index)) + + +class SchemaDropper(DDLBase): + + def __init__(self, dialect, connection, checkfirst=False, + tables=None, **kwargs): + super(SchemaDropper, self).__init__(connection, **kwargs) + self.checkfirst = checkfirst + self.tables = tables + self.preparer = dialect.identifier_preparer + self.dialect = dialect + self.memo = {} + + def visit_metadata(self, metadata): + if self.tables is not None: + tables = self.tables + else: + tables = list(metadata.tables.values()) + + collection = [ + t + for t in reversed(sort_tables(tables)) + if self._can_drop_table(t) + ] + + seq_coll = [ + s + for s in metadata._sequences.values() + if s.column is None and self._can_drop_sequence(s) + ] + + metadata.dispatch.before_drop( + metadata, self.connection, tables=collection, + checkfirst=self.checkfirst, _ddl_runner=self) + + for table in collection: + self.traverse_single(table, drop_ok=True) + + for seq in seq_coll: + self.traverse_single(seq, drop_ok=True) + + metadata.dispatch.after_drop( + metadata, self.connection, tables=collection, + checkfirst=self.checkfirst, _ddl_runner=self) + + def _can_drop_table(self, table): + self.dialect.validate_identifier(table.name) + if table.schema: + self.dialect.validate_identifier(table.schema) + return not self.checkfirst or self.dialect.has_table(self.connection, + table.name, schema=table.schema) + + def _can_drop_sequence(self, sequence): + return self.dialect.supports_sequences and \ + ((not self.dialect.sequences_optional or + not sequence.optional) and + (not self.checkfirst or + self.dialect.has_sequence( + self.connection, + sequence.name, + schema=sequence.schema)) + ) + + def visit_index(self, index): + self.connection.execute(DropIndex(index)) + + def visit_table(self, table, drop_ok=False): + if not drop_ok and not self._can_drop_table(table): + return + + table.dispatch.before_drop(table, self.connection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + for column in table.columns: + if column.default is not None: + self.traverse_single(column.default) + + self.connection.execute(DropTable(table)) + + table.dispatch.after_drop(table, self.connection, + checkfirst=self.checkfirst, + _ddl_runner=self) + + def visit_sequence(self, sequence, drop_ok=False): + if not drop_ok and not self._can_drop_sequence(sequence): + return + self.connection.execute(DropSequence(sequence)) + +def sort_tables(tables, skip_fn=None, extra_dependencies=None): + """sort a collection of Table objects in order of + their foreign-key dependency.""" + + tables = list(tables) + tuples = [] + if extra_dependencies is not None: + tuples.extend(extra_dependencies) + + def visit_foreign_key(fkey): + if fkey.use_alter: + return + elif skip_fn and skip_fn(fkey): + return + parent_table = fkey.column.table + if parent_table in tables: + child_table = fkey.parent.table + if parent_table is not child_table: + tuples.append((parent_table, child_table)) + + for table in tables: + traverse(table, + {'schema_visitor': True}, + {'foreign_key': visit_foreign_key}) + + tuples.extend( + [parent, table] for parent in table._extra_dependencies + ) + + return list(topological.sort(tuples, tables)) + diff --git a/libs/sqlalchemy/sql/default_comparator.py b/libs/sqlalchemy/sql/default_comparator.py new file mode 100644 index 00000000..c39dce9c --- /dev/null +++ b/libs/sqlalchemy/sql/default_comparator.py @@ -0,0 +1,278 @@ +# sql/default_comparator.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Default implementation of SQL comparison operations. +""" + +from .. import exc, util +from . import operators +from . import type_api +from .elements import BindParameter, True_, False_, BinaryExpression, \ + Null, _const_expr, _clause_element_as_expr, \ + ClauseList, ColumnElement, TextClause, UnaryExpression, \ + collate, _is_literal, _literal_as_text +from .selectable import SelectBase, Alias, Selectable, ScalarSelect + +class _DefaultColumnComparator(operators.ColumnOperators): + """Defines comparison and math operations. + + See :class:`.ColumnOperators` and :class:`.Operators` for descriptions + of all operations. + + """ + + @util.memoized_property + def type(self): + return self.expr.type + + def operate(self, op, *other, **kwargs): + o = self.operators[op.__name__] + return o[0](self, self.expr, op, *(other + o[1:]), **kwargs) + + def reverse_operate(self, op, other, **kwargs): + o = self.operators[op.__name__] + return o[0](self, self.expr, op, other, reverse=True, *o[1:], **kwargs) + + def _adapt_expression(self, op, other_comparator): + """evaluate the return type of , + and apply any adaptations to the given operator. + + This method determines the type of a resulting binary expression + given two source types and an operator. For example, two + :class:`.Column` objects, both of the type :class:`.Integer`, will + produce a :class:`.BinaryExpression` that also has the type + :class:`.Integer` when compared via the addition (``+``) operator. + However, using the addition operator with an :class:`.Integer` + and a :class:`.Date` object will produce a :class:`.Date`, assuming + "days delta" behavior by the database (in reality, most databases + other than Postgresql don't accept this particular operation). + + The method returns a tuple of the form , . + The resulting operator and type will be those applied to the + resulting :class:`.BinaryExpression` as the final operator and the + right-hand side of the expression. + + Note that only a subset of operators make usage of + :meth:`._adapt_expression`, + including math operators and user-defined operators, but not + boolean comparison or special SQL keywords like MATCH or BETWEEN. + + """ + return op, other_comparator.type + + def _boolean_compare(self, expr, op, obj, negate=None, reverse=False, + _python_is_types=(util.NoneType, bool), + **kwargs): + + if isinstance(obj, _python_is_types + (Null, True_, False_)): + + # allow x ==/!= True/False to be treated as a literal. + # this comes out to "== / != true/false" or "1/0" if those + # constants aren't supported and works on all platforms + if op in (operators.eq, operators.ne) and \ + isinstance(obj, (bool, True_, False_)): + return BinaryExpression(expr, + _literal_as_text(obj), + op, + type_=type_api.BOOLEANTYPE, + negate=negate, modifiers=kwargs) + else: + # all other None/True/False uses IS, IS NOT + if op in (operators.eq, operators.is_): + return BinaryExpression(expr, _const_expr(obj), + operators.is_, + negate=operators.isnot) + elif op in (operators.ne, operators.isnot): + return BinaryExpression(expr, _const_expr(obj), + operators.isnot, + negate=operators.is_) + else: + raise exc.ArgumentError( + "Only '=', '!=', 'is_()', 'isnot()' operators can " + "be used with None/True/False") + else: + obj = self._check_literal(expr, op, obj) + + if reverse: + return BinaryExpression(obj, + expr, + op, + type_=type_api.BOOLEANTYPE, + negate=negate, modifiers=kwargs) + else: + return BinaryExpression(expr, + obj, + op, + type_=type_api.BOOLEANTYPE, + negate=negate, modifiers=kwargs) + + def _binary_operate(self, expr, op, obj, reverse=False, result_type=None, + **kw): + obj = self._check_literal(expr, op, obj) + + if reverse: + left, right = obj, expr + else: + left, right = expr, obj + + if result_type is None: + op, result_type = left.comparator._adapt_expression( + op, right.comparator) + + return BinaryExpression(left, right, op, type_=result_type) + + def _scalar(self, expr, op, fn, **kw): + return fn(expr) + + def _in_impl(self, expr, op, seq_or_selectable, negate_op, **kw): + seq_or_selectable = _clause_element_as_expr(seq_or_selectable) + + if isinstance(seq_or_selectable, ScalarSelect): + return self._boolean_compare(expr, op, seq_or_selectable, + negate=negate_op) + elif isinstance(seq_or_selectable, SelectBase): + + # TODO: if we ever want to support (x, y, z) IN (select x, + # y, z from table), we would need a multi-column version of + # as_scalar() to produce a multi- column selectable that + # does not export itself as a FROM clause + + return self._boolean_compare( + expr, op, seq_or_selectable.as_scalar(), + negate=negate_op, **kw) + elif isinstance(seq_or_selectable, (Selectable, TextClause)): + return self._boolean_compare(expr, op, seq_or_selectable, + negate=negate_op, **kw) + + # Handle non selectable arguments as sequences + args = [] + for o in seq_or_selectable: + if not _is_literal(o): + if not isinstance(o, operators.ColumnOperators): + raise exc.InvalidRequestError('in() function accept' + 's either a list of non-selectable values, ' + 'or a selectable: %r' % o) + elif o is None: + o = Null() + else: + o = expr._bind_param(op, o) + args.append(o) + if len(args) == 0: + + # Special case handling for empty IN's, behave like + # comparison against zero row selectable. We use != to + # build the contradiction as it handles NULL values + # appropriately, i.e. "not (x IN ())" should not return NULL + # values for x. + + util.warn('The IN-predicate on "%s" was invoked with an ' + 'empty sequence. This results in a ' + 'contradiction, which nonetheless can be ' + 'expensive to evaluate. Consider alternative ' + 'strategies for improved performance.' % expr) + if op is operators.in_op: + return expr != expr + else: + return expr == expr + + return self._boolean_compare(expr, op, + ClauseList(*args).self_group(against=op), + negate=negate_op) + + def _unsupported_impl(self, expr, op, *arg, **kw): + raise NotImplementedError("Operator '%s' is not supported on " + "this expression" % op.__name__) + + def _neg_impl(self, expr, op, **kw): + """See :meth:`.ColumnOperators.__neg__`.""" + return UnaryExpression(expr, operator=operators.neg) + + def _match_impl(self, expr, op, other, **kw): + """See :meth:`.ColumnOperators.match`.""" + return self._boolean_compare(expr, operators.match_op, + self._check_literal(expr, operators.match_op, + other)) + + def _distinct_impl(self, expr, op, **kw): + """See :meth:`.ColumnOperators.distinct`.""" + return UnaryExpression(expr, operator=operators.distinct_op, + type_=expr.type) + + def _between_impl(self, expr, op, cleft, cright, **kw): + """See :meth:`.ColumnOperators.between`.""" + return BinaryExpression( + expr, + ClauseList( + self._check_literal(expr, operators.and_, cleft), + self._check_literal(expr, operators.and_, cright), + operator=operators.and_, + group=False, group_contents=False), + operators.between_op) + + def _collate_impl(self, expr, op, other, **kw): + return collate(expr, other) + + # a mapping of operators with the method they use, along with + # their negated operator for comparison operators + operators = { + "add": (_binary_operate,), + "mul": (_binary_operate,), + "sub": (_binary_operate,), + "div": (_binary_operate,), + "mod": (_binary_operate,), + "truediv": (_binary_operate,), + "custom_op": (_binary_operate,), + "concat_op": (_binary_operate,), + "lt": (_boolean_compare, operators.ge), + "le": (_boolean_compare, operators.gt), + "ne": (_boolean_compare, operators.eq), + "gt": (_boolean_compare, operators.le), + "ge": (_boolean_compare, operators.lt), + "eq": (_boolean_compare, operators.ne), + "like_op": (_boolean_compare, operators.notlike_op), + "ilike_op": (_boolean_compare, operators.notilike_op), + "notlike_op": (_boolean_compare, operators.like_op), + "notilike_op": (_boolean_compare, operators.ilike_op), + "contains_op": (_boolean_compare, operators.notcontains_op), + "startswith_op": (_boolean_compare, operators.notstartswith_op), + "endswith_op": (_boolean_compare, operators.notendswith_op), + "desc_op": (_scalar, UnaryExpression._create_desc), + "asc_op": (_scalar, UnaryExpression._create_asc), + "nullsfirst_op": (_scalar, UnaryExpression._create_nullsfirst), + "nullslast_op": (_scalar, UnaryExpression._create_nullslast), + "in_op": (_in_impl, operators.notin_op), + "notin_op": (_in_impl, operators.in_op), + "is_": (_boolean_compare, operators.is_), + "isnot": (_boolean_compare, operators.isnot), + "collate": (_collate_impl,), + "match_op": (_match_impl,), + "distinct_op": (_distinct_impl,), + "between_op": (_between_impl, ), + "neg": (_neg_impl,), + "getitem": (_unsupported_impl,), + "lshift": (_unsupported_impl,), + "rshift": (_unsupported_impl,), + } + + def _check_literal(self, expr, operator, other): + if isinstance(other, (ColumnElement, TextClause)): + if isinstance(other, BindParameter) and \ + other.type._isnull: + other = other._clone() + other.type = expr.type + return other + elif hasattr(other, '__clause_element__'): + other = other.__clause_element__() + elif isinstance(other, type_api.TypeEngine.Comparator): + other = other.expr + + if isinstance(other, (SelectBase, Alias)): + return other.as_scalar() + elif not isinstance(other, (ColumnElement, TextClause)): + return expr._bind_param(operator, other) + else: + return other + diff --git a/libs/sqlalchemy/sql/dml.py b/libs/sqlalchemy/sql/dml.py new file mode 100644 index 00000000..22694348 --- /dev/null +++ b/libs/sqlalchemy/sql/dml.py @@ -0,0 +1,770 @@ +# sql/dml.py +# Copyright (C) 2009-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +""" +Provide :class:`.Insert`, :class:`.Update` and :class:`.Delete`. + +""" + +from .base import Executable, _generative, _from_objects +from .elements import ClauseElement, _literal_as_text, Null, and_, _clone +from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes +from .. import util +from .. import exc + +class UpdateBase(HasPrefixes, Executable, ClauseElement): + """Form the base for ``INSERT``, ``UPDATE``, and ``DELETE`` statements. + + """ + + __visit_name__ = 'update_base' + + _execution_options = \ + Executable._execution_options.union({'autocommit': True}) + kwargs = util.immutabledict() + _hints = util.immutabledict() + _prefixes = () + + def _process_colparams(self, parameters): + def process_single(p): + if isinstance(p, (list, tuple)): + return dict( + (c.key, pval) + for c, pval in zip(self.table.c, p) + ) + else: + return p + + if isinstance(parameters, (list, tuple)) and \ + isinstance(parameters[0], (list, tuple, dict)): + + if not self._supports_multi_parameters: + raise exc.InvalidRequestError( + "This construct does not support " + "multiple parameter sets.") + + return [process_single(p) for p in parameters], True + else: + return process_single(parameters), False + + def params(self, *arg, **kw): + """Set the parameters for the statement. + + This method raises ``NotImplementedError`` on the base class, + and is overridden by :class:`.ValuesBase` to provide the + SET/VALUES clause of UPDATE and INSERT. + + """ + raise NotImplementedError( + "params() is not supported for INSERT/UPDATE/DELETE statements." + " To set the values for an INSERT or UPDATE statement, use" + " stmt.values(**parameters).") + + def bind(self): + """Return a 'bind' linked to this :class:`.UpdateBase` + or a :class:`.Table` associated with it. + + """ + return self._bind or self.table.bind + + def _set_bind(self, bind): + self._bind = bind + bind = property(bind, _set_bind) + + @_generative + def returning(self, *cols): + """Add a :term:`RETURNING` or equivalent clause to this statement. + + e.g.:: + + stmt = table.update().\\ + where(table.c.data == 'value').\\ + values(status='X').\\ + returning(table.c.server_flag, table.c.updated_timestamp) + + for server_flag, updated_timestamp in connection.execute(stmt): + print(server_flag, updated_timestamp) + + The given collection of column expressions should be derived from + the table that is + the target of the INSERT, UPDATE, or DELETE. While :class:`.Column` + objects are typical, the elements can also be expressions:: + + stmt = table.insert().returning( + (table.c.first_name + " " + table.c.last_name).label('fullname') + ) + + Upon compilation, a RETURNING clause, or database equivalent, + will be rendered within the statement. For INSERT and UPDATE, + the values are the newly inserted/updated values. For DELETE, + the values are those of the rows which were deleted. + + Upon execution, the values of the columns to be returned + are made available via the result set and can be iterated + using :meth:`.ResultProxy.fetchone` and similar. For DBAPIs which do not + natively support returning values (i.e. cx_oracle), + SQLAlchemy will approximate this behavior at the result level + so that a reasonable amount of behavioral neutrality is + provided. + + Note that not all databases/DBAPIs + support RETURNING. For those backends with no support, + an exception is raised upon compilation and/or execution. + For those who do support it, the functionality across backends + varies greatly, including restrictions on executemany() + and other statements which return multiple rows. Please + read the documentation notes for the database in use in + order to determine the availability of RETURNING. + + .. seealso:: + + :meth:`.ValuesBase.return_defaults` - an alternative method tailored + towards efficient fetching of server-side defaults and triggers + for single-row INSERTs or UPDATEs. + + + """ + self._returning = cols + + + @_generative + def with_hint(self, text, selectable=None, dialect_name="*"): + """Add a table hint for a single table to this + INSERT/UPDATE/DELETE statement. + + .. note:: + + :meth:`.UpdateBase.with_hint` currently applies only to + Microsoft SQL Server. For MySQL INSERT/UPDATE/DELETE hints, use + :meth:`.UpdateBase.prefix_with`. + + The text of the hint is rendered in the appropriate + location for the database backend in use, relative + to the :class:`.Table` that is the subject of this + statement, or optionally to that of the given + :class:`.Table` passed as the ``selectable`` argument. + + The ``dialect_name`` option will limit the rendering of a particular + hint to a particular backend. Such as, to add a hint + that only takes effect for SQL Server:: + + mytable.insert().with_hint("WITH (PAGLOCK)", dialect_name="mssql") + + .. versionadded:: 0.7.6 + + :param text: Text of the hint. + :param selectable: optional :class:`.Table` that specifies + an element of the FROM clause within an UPDATE or DELETE + to be the subject of the hint - applies only to certain backends. + :param dialect_name: defaults to ``*``, if specified as the name + of a particular dialect, will apply these hints only when + that dialect is in use. + """ + if selectable is None: + selectable = self.table + + self._hints = self._hints.union( + {(selectable, dialect_name): text}) + + +class ValuesBase(UpdateBase): + """Supplies support for :meth:`.ValuesBase.values` to + INSERT and UPDATE constructs.""" + + __visit_name__ = 'values_base' + + _supports_multi_parameters = False + _has_multi_parameters = False + select = None + + def __init__(self, table, values, prefixes): + self.table = _interpret_as_from(table) + self.parameters, self._has_multi_parameters = \ + self._process_colparams(values) + if prefixes: + self._setup_prefixes(prefixes) + + @_generative + def values(self, *args, **kwargs): + """specify a fixed VALUES clause for an INSERT statement, or the SET + clause for an UPDATE. + + Note that the :class:`.Insert` and :class:`.Update` constructs support + per-execution time formatting of the VALUES and/or SET clauses, + based on the arguments passed to :meth:`.Connection.execute`. However, + the :meth:`.ValuesBase.values` method can be used to "fix" a particular + set of parameters into the statement. + + Multiple calls to :meth:`.ValuesBase.values` will produce a new + construct, each one with the parameter list modified to include + the new parameters sent. In the typical case of a single + dictionary of parameters, the newly passed keys will replace + the same keys in the previous construct. In the case of a list-based + "multiple values" construct, each new list of values is extended + onto the existing list of values. + + :param \**kwargs: key value pairs representing the string key + of a :class:`.Column` mapped to the value to be rendered into the + VALUES or SET clause:: + + users.insert().values(name="some name") + + users.update().where(users.c.id==5).values(name="some name") + + :param \*args: Alternatively, a dictionary, tuple or list + of dictionaries or tuples can be passed as a single positional + argument in order to form the VALUES or + SET clause of the statement. The single dictionary form + works the same as the kwargs form:: + + users.insert().values({"name": "some name"}) + + If a tuple is passed, the tuple should contain the same number + of columns as the target :class:`.Table`:: + + users.insert().values((5, "some name")) + + The :class:`.Insert` construct also supports multiply-rendered VALUES + construct, for those backends which support this SQL syntax + (SQLite, Postgresql, MySQL). This mode is indicated by passing a list + of one or more dictionaries/tuples:: + + users.insert().values([ + {"name": "some name"}, + {"name": "some other name"}, + {"name": "yet another name"}, + ]) + + In the case of an :class:`.Update` + construct, only the single dictionary/tuple form is accepted, + else an exception is raised. It is also an exception case to + attempt to mix the single-/multiple- value styles together, + either through multiple :meth:`.ValuesBase.values` calls + or by sending a list + kwargs at the same time. + + .. note:: + + Passing a multiple values list is *not* the same + as passing a multiple values list to the :meth:`.Connection.execute` + method. Passing a list of parameter sets to :meth:`.ValuesBase.values` + produces a construct of this form:: + + INSERT INTO table (col1, col2, col3) VALUES + (col1_0, col2_0, col3_0), + (col1_1, col2_1, col3_1), + ... + + whereas a multiple list passed to :meth:`.Connection.execute` + has the effect of using the DBAPI + `executemany() `_ + method, which provides a high-performance system of invoking + a single-row INSERT statement many times against a series + of parameter sets. The "executemany" style is supported by + all database backends, as it does not depend on a special SQL + syntax. + + .. versionadded:: 0.8 + Support for multiple-VALUES INSERT statements. + + + .. seealso:: + + :ref:`inserts_and_updates` - SQL Expression + Language Tutorial + + :func:`~.expression.insert` - produce an ``INSERT`` statement + + :func:`~.expression.update` - produce an ``UPDATE`` statement + + """ + if self.select is not None: + raise exc.InvalidRequestError( + "This construct already inserts from a SELECT") + if self._has_multi_parameters and kwargs: + raise exc.InvalidRequestError( + "This construct already has multiple parameter sets.") + + if args: + if len(args) > 1: + raise exc.ArgumentError( + "Only a single dictionary/tuple or list of " + "dictionaries/tuples is accepted positionally.") + v = args[0] + else: + v = {} + + if self.parameters is None: + self.parameters, self._has_multi_parameters = \ + self._process_colparams(v) + else: + if self._has_multi_parameters: + self.parameters = list(self.parameters) + p, self._has_multi_parameters = self._process_colparams(v) + if not self._has_multi_parameters: + raise exc.ArgumentError( + "Can't mix single-values and multiple values " + "formats in one statement") + + self.parameters.extend(p) + else: + self.parameters = self.parameters.copy() + p, self._has_multi_parameters = self._process_colparams(v) + if self._has_multi_parameters: + raise exc.ArgumentError( + "Can't mix single-values and multiple values " + "formats in one statement") + self.parameters.update(p) + + if kwargs: + if self._has_multi_parameters: + raise exc.ArgumentError( + "Can't pass kwargs and multiple parameter sets " + "simultaenously") + else: + self.parameters.update(kwargs) + + @_generative + def return_defaults(self, *cols): + """Make use of a :term:`RETURNING` clause for the purpose + of fetching server-side expressions and defaults. + + E.g.:: + + stmt = table.insert().values(data='newdata').return_defaults() + + result = connection.execute(stmt) + + server_created_at = result.returned_defaults['created_at'] + + When used against a backend that supports RETURNING, all column + values generated by SQL expression or server-side-default will be added + to any existing RETURNING clause, provided that + :meth:`.UpdateBase.returning` is not used simultaneously. The column values + will then be available on the result using the + :attr:`.ResultProxy.returned_defaults` accessor as a + dictionary, referring to values keyed to the :class:`.Column` object + as well as its ``.key``. + + This method differs from :meth:`.UpdateBase.returning` in these ways: + + 1. :meth:`.ValuesBase.return_defaults` is only intended for use with + an INSERT or an UPDATE statement that matches exactly one row. + While the RETURNING construct in the general sense supports multiple + rows for a multi-row UPDATE or DELETE statement, or for special + cases of INSERT that return multiple rows (e.g. INSERT from SELECT, + multi-valued VALUES clause), :meth:`.ValuesBase.return_defaults` + is intended only + for an "ORM-style" single-row INSERT/UPDATE statement. The row + returned by the statement is also consumed implcitly when + :meth:`.ValuesBase.return_defaults` is used. By contrast, + :meth:`.UpdateBase.returning` leaves the RETURNING result-set intact + with a collection of any number of rows. + + 2. It is compatible with the existing logic to fetch auto-generated + primary key values, also known as "implicit returning". Backends that + support RETURNING will automatically make use of RETURNING in order + to fetch the value of newly generated primary keys; while the + :meth:`.UpdateBase.returning` method circumvents this behavior, + :meth:`.ValuesBase.return_defaults` leaves it intact. + + 3. It can be called against any backend. Backends that don't support + RETURNING will skip the usage of the feature, rather than raising + an exception. The return value of :attr:`.ResultProxy.returned_defaults` + will be ``None`` + + :meth:`.ValuesBase.return_defaults` is used by the ORM to provide + an efficient implementation for the ``eager_defaults`` feature of + :func:`.mapper`. + + :param cols: optional list of column key names or :class:`.Column` + objects. If omitted, all column expressions evaulated on the server + are added to the returning list. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.UpdateBase.returning` + + :attr:`.ResultProxy.returned_defaults` + + """ + self._return_defaults = cols or True + + +class Insert(ValuesBase): + """Represent an INSERT construct. + + The :class:`.Insert` object is created using the + :func:`~.expression.insert()` function. + + .. seealso:: + + :ref:`coretutorial_insert_expressions` + + """ + __visit_name__ = 'insert' + + _supports_multi_parameters = True + + def __init__(self, + table, + values=None, + inline=False, + bind=None, + prefixes=None, + returning=None, + return_defaults=False, + **kwargs): + """Construct an :class:`.Insert` object. + + Similar functionality is available via the + :meth:`~.TableClause.insert` method on + :class:`~.schema.Table`. + + :param table: :class:`.TableClause` which is the subject of the insert. + + :param values: collection of values to be inserted; see + :meth:`.Insert.values` for a description of allowed formats here. + Can be omitted entirely; a :class:`.Insert` construct will also + dynamically render the VALUES clause at execution time based on + the parameters passed to :meth:`.Connection.execute`. + + :param inline: if True, SQL defaults will be compiled 'inline' into the + statement and not pre-executed. + + If both `values` and compile-time bind parameters are present, the + compile-time bind parameters override the information specified + within `values` on a per-key basis. + + The keys within `values` can be either :class:`~sqlalchemy.schema.Column` + objects or their string identifiers. Each key may reference one of: + + * a literal data value (i.e. string, number, etc.); + * a Column object; + * a SELECT statement. + + If a ``SELECT`` statement is specified which references this + ``INSERT`` statement's table, the statement will be correlated + against the ``INSERT`` statement. + + .. seealso:: + + :ref:`coretutorial_insert_expressions` - SQL Expression Tutorial + + :ref:`inserts_and_updates` - SQL Expression Tutorial + + """ + ValuesBase.__init__(self, table, values, prefixes) + self._bind = bind + self.select = self.select_names = None + self.inline = inline + self._returning = returning + self.kwargs = kwargs + self._return_defaults = return_defaults + + def get_children(self, **kwargs): + if self.select is not None: + return self.select, + else: + return () + + @_generative + def from_select(self, names, select): + """Return a new :class:`.Insert` construct which represents + an ``INSERT...FROM SELECT`` statement. + + e.g.:: + + sel = select([table1.c.a, table1.c.b]).where(table1.c.c > 5) + ins = table2.insert().from_select(['a', 'b'], sel) + + :param names: a sequence of string column names or :class:`.Column` + objects representing the target columns. + :param select: a :func:`.select` construct, :class:`.FromClause` + or other construct which resolves into a :class:`.FromClause`, + such as an ORM :class:`.Query` object, etc. The order of + columns returned from this FROM clause should correspond to the + order of columns sent as the ``names`` parameter; while this + is not checked before passing along to the database, the database + would normally raise an exception if these column lists don't + correspond. + + .. note:: + + Depending on backend, it may be necessary for the :class:`.Insert` + statement to be constructed using the ``inline=True`` flag; this + flag will prevent the implicit usage of ``RETURNING`` when the + ``INSERT`` statement is rendered, which isn't supported on a backend + such as Oracle in conjunction with an ``INSERT..SELECT`` combination:: + + sel = select([table1.c.a, table1.c.b]).where(table1.c.c > 5) + ins = table2.insert(inline=True).from_select(['a', 'b'], sel) + + .. note:: + + A SELECT..INSERT construct in SQL has no VALUES clause. Therefore + :class:`.Column` objects which utilize Python-side defaults + (e.g. as described at :ref:`metadata_defaults_toplevel`) + will **not** take effect when using :meth:`.Insert.from_select`. + + .. versionadded:: 0.8.3 + + """ + if self.parameters: + raise exc.InvalidRequestError( + "This construct already inserts value expressions") + + self.parameters, self._has_multi_parameters = \ + self._process_colparams(dict((n, Null()) for n in names)) + + self.select_names = names + self.select = _interpret_as_select(select) + + def _copy_internals(self, clone=_clone, **kw): + # TODO: coverage + self.parameters = self.parameters.copy() + if self.select is not None: + self.select = _clone(self.select) + + +class Update(ValuesBase): + """Represent an Update construct. + + The :class:`.Update` object is created using the :func:`update()` function. + + """ + __visit_name__ = 'update' + + def __init__(self, + table, + whereclause=None, + values=None, + inline=False, + bind=None, + prefixes=None, + returning=None, + return_defaults=False, + **kwargs): + """Construct an :class:`.Update` object. + + E.g.:: + + from sqlalchemy import update + + stmt = update(users).where(users.c.id==5).\\ + values(name='user #5') + + Similar functionality is available via the + :meth:`~.TableClause.update` method on + :class:`.Table`:: + + stmt = users.update().\\ + where(users.c.id==5).\\ + values(name='user #5') + + :param table: A :class:`.Table` object representing the database + table to be updated. + + :param whereclause: Optional SQL expression describing the ``WHERE`` + condition of the ``UPDATE`` statement. Modern applications + may prefer to use the generative :meth:`~Update.where()` + method to specify the ``WHERE`` clause. + + The WHERE clause can refer to multiple tables. + For databases which support this, an ``UPDATE FROM`` clause will + be generated, or on MySQL, a multi-table update. The statement + will fail on databases that don't have support for multi-table + update statements. A SQL-standard method of referring to + additional tables in the WHERE clause is to use a correlated + subquery:: + + users.update().values(name='ed').where( + users.c.name==select([addresses.c.email_address]).\\ + where(addresses.c.user_id==users.c.id).\\ + as_scalar() + ) + + .. versionchanged:: 0.7.4 + The WHERE clause can refer to multiple tables. + + :param values: + Optional dictionary which specifies the ``SET`` conditions of the + ``UPDATE``. If left as ``None``, the ``SET`` + conditions are determined from those parameters passed to the + statement during the execution and/or compilation of the + statement. When compiled standalone without any parameters, + the ``SET`` clause generates for all columns. + + Modern applications may prefer to use the generative + :meth:`.Update.values` method to set the values of the + UPDATE statement. + + :param inline: + if True, SQL defaults present on :class:`.Column` objects via + the ``default`` keyword will be compiled 'inline' into the statement + and not pre-executed. This means that their values will not + be available in the dictionary returned from + :meth:`.ResultProxy.last_updated_params`. + + If both ``values`` and compile-time bind parameters are present, the + compile-time bind parameters override the information specified + within ``values`` on a per-key basis. + + The keys within ``values`` can be either :class:`.Column` + objects or their string identifiers (specifically the "key" of the + :class:`.Column`, normally but not necessarily equivalent to + its "name"). Normally, the + :class:`.Column` objects used here are expected to be + part of the target :class:`.Table` that is the table + to be updated. However when using MySQL, a multiple-table + UPDATE statement can refer to columns from any of + the tables referred to in the WHERE clause. + + The values referred to in ``values`` are typically: + + * a literal data value (i.e. string, number, etc.) + * a SQL expression, such as a related :class:`.Column`, + a scalar-returning :func:`.select` construct, + etc. + + When combining :func:`.select` constructs within the values + clause of an :func:`.update` construct, + the subquery represented by the :func:`.select` should be + *correlated* to the parent table, that is, providing criterion + which links the table inside the subquery to the outer table + being updated:: + + users.update().values( + name=select([addresses.c.email_address]).\\ + where(addresses.c.user_id==users.c.id).\\ + as_scalar() + ) + + .. seealso:: + + :ref:`inserts_and_updates` - SQL Expression + Language Tutorial + + + """ + ValuesBase.__init__(self, table, values, prefixes) + self._bind = bind + self._returning = returning + if whereclause is not None: + self._whereclause = _literal_as_text(whereclause) + else: + self._whereclause = None + self.inline = inline + self.kwargs = kwargs + self._return_defaults = return_defaults + + + def get_children(self, **kwargs): + if self._whereclause is not None: + return self._whereclause, + else: + return () + + def _copy_internals(self, clone=_clone, **kw): + # TODO: coverage + self._whereclause = clone(self._whereclause, **kw) + self.parameters = self.parameters.copy() + + @_generative + def where(self, whereclause): + """return a new update() construct with the given expression added to + its WHERE clause, joined to the existing clause via AND, if any. + + """ + if self._whereclause is not None: + self._whereclause = and_(self._whereclause, + _literal_as_text(whereclause)) + else: + self._whereclause = _literal_as_text(whereclause) + + @property + def _extra_froms(self): + # TODO: this could be made memoized + # if the memoization is reset on each generative call. + froms = [] + seen = set([self.table]) + + if self._whereclause is not None: + for item in _from_objects(self._whereclause): + if not seen.intersection(item._cloned_set): + froms.append(item) + seen.update(item._cloned_set) + + return froms + + +class Delete(UpdateBase): + """Represent a DELETE construct. + + The :class:`.Delete` object is created using the :func:`delete()` function. + + """ + + __visit_name__ = 'delete' + + def __init__(self, + table, + whereclause=None, + bind=None, + returning=None, + prefixes=None, + **kwargs): + """Construct :class:`.Delete` object. + + Similar functionality is available via the + :meth:`~.TableClause.delete` method on + :class:`~.schema.Table`. + + :param table: The table to be updated. + + :param whereclause: A :class:`.ClauseElement` describing the ``WHERE`` + condition of the ``UPDATE`` statement. Note that the + :meth:`~Delete.where()` generative method may be used instead. + + .. seealso:: + + :ref:`deletes` - SQL Expression Tutorial + + """ + self._bind = bind + self.table = _interpret_as_from(table) + self._returning = returning + + if prefixes: + self._setup_prefixes(prefixes) + + if whereclause is not None: + self._whereclause = _literal_as_text(whereclause) + else: + self._whereclause = None + + self.kwargs = kwargs + + def get_children(self, **kwargs): + if self._whereclause is not None: + return self._whereclause, + else: + return () + + @_generative + def where(self, whereclause): + """Add the given WHERE clause to a newly returned delete construct.""" + + if self._whereclause is not None: + self._whereclause = and_(self._whereclause, + _literal_as_text(whereclause)) + else: + self._whereclause = _literal_as_text(whereclause) + + def _copy_internals(self, clone=_clone, **kw): + # TODO: coverage + self._whereclause = clone(self._whereclause, **kw) + diff --git a/libs/sqlalchemy/sql/elements.py b/libs/sqlalchemy/sql/elements.py new file mode 100644 index 00000000..0e888fcf --- /dev/null +++ b/libs/sqlalchemy/sql/elements.py @@ -0,0 +1,2880 @@ +# sql/elements.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Core SQL expression elements, including :class:`.ClauseElement`, +:class:`.ColumnElement`, and derived classes. + +""" + +from __future__ import unicode_literals + +from .. import util, exc, inspection +from . import type_api +from . import operators +from .visitors import Visitable, cloned_traverse, traverse +from .annotation import Annotated +import itertools +from .base import Executable, PARSE_AUTOCOMMIT, Immutable, NO_ARG +from .base import _generative, Generative + +import re +import operator + +def _clone(element, **kw): + return element._clone() + +def collate(expression, collation): + """Return the clause ``expression COLLATE collation``. + + e.g.:: + + collate(mycolumn, 'utf8_bin') + + produces:: + + mycolumn COLLATE utf8_bin + + """ + + expr = _literal_as_binds(expression) + return BinaryExpression( + expr, + _literal_as_text(collation), + operators.collate, type_=expr.type) + +def between(ctest, cleft, cright): + """Return a ``BETWEEN`` predicate clause. + + Equivalent of SQL ``clausetest BETWEEN clauseleft AND clauseright``. + + The :func:`between()` method on all + :class:`.ColumnElement` subclasses provides + similar functionality. + + """ + ctest = _literal_as_binds(ctest) + return ctest.between(cleft, cright) + +def literal(value, type_=None): + """Return a literal clause, bound to a bind parameter. + + Literal clauses are created automatically when non- :class:`.ClauseElement` + objects (such as strings, ints, dates, etc.) are used in a comparison + operation with a :class:`.ColumnElement` + subclass, such as a :class:`~sqlalchemy.schema.Column` object. + Use this function to force the + generation of a literal clause, which will be created as a + :class:`BindParameter` with a bound value. + + :param value: the value to be bound. Can be any Python object supported by + the underlying DB-API, or is translatable via the given type argument. + + :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` which + will provide bind-parameter translation for this literal. + + """ + return BindParameter(None, value, type_=type_, unique=True) + + + +def type_coerce(expression, type_): + """Coerce the given expression into the given type, + on the Python side only. + + :func:`.type_coerce` is roughly similar to :func:`.cast`, except no + "CAST" expression is rendered - the given type is only applied towards + expression typing and against received result values. + + e.g.:: + + from sqlalchemy.types import TypeDecorator + import uuid + + class AsGuid(TypeDecorator): + impl = String + + def process_bind_param(self, value, dialect): + if value is not None: + return str(value) + else: + return None + + def process_result_value(self, value, dialect): + if value is not None: + return uuid.UUID(value) + else: + return None + + conn.execute( + select([type_coerce(mytable.c.ident, AsGuid)]).\\ + where( + type_coerce(mytable.c.ident, AsGuid) == + uuid.uuid3(uuid.NAMESPACE_URL, 'bar') + ) + ) + + :param expression: Column-oriented expression. + :param type_: A :class:`.TypeEngine` class or instance indicating + the type to which the CAST should apply. + + .. seealso:: + + :func:`.cast` + + """ + type_ = type_api.to_instance(type_) + + if hasattr(expression, '__clause_element__'): + return type_coerce(expression.__clause_element__(), type_) + elif isinstance(expression, BindParameter): + bp = expression._clone() + bp.type = type_ + return bp + elif not isinstance(expression, Visitable): + if expression is None: + return Null() + else: + return literal(expression, type_=type_) + else: + return Label(None, expression, type_=type_) + + + + + +def outparam(key, type_=None): + """Create an 'OUT' parameter for usage in functions (stored procedures), + for databases which support them. + + The ``outparam`` can be used like a regular function parameter. + The "output" value will be available from the + :class:`~sqlalchemy.engine.ResultProxy` object via its ``out_parameters`` + attribute, which returns a dictionary containing the values. + + """ + return BindParameter( + key, None, type_=type_, unique=False, isoutparam=True) + + + + +def not_(clause): + """Return a negation of the given clause, i.e. ``NOT(clause)``. + + The ``~`` operator is also overloaded on all + :class:`.ColumnElement` subclasses to produce the + same result. + + """ + return operators.inv(_literal_as_binds(clause)) + + + +@inspection._self_inspects +class ClauseElement(Visitable): + """Base class for elements of a programmatically constructed SQL + expression. + + """ + __visit_name__ = 'clause' + + _annotations = {} + supports_execution = False + _from_objects = [] + bind = None + _is_clone_of = None + is_selectable = False + is_clause_element = True + + _order_by_label_element = None + + def _clone(self): + """Create a shallow copy of this ClauseElement. + + This method may be used by a generative API. Its also used as + part of the "deep" copy afforded by a traversal that combines + the _copy_internals() method. + + """ + c = self.__class__.__new__(self.__class__) + c.__dict__ = self.__dict__.copy() + ClauseElement._cloned_set._reset(c) + ColumnElement.comparator._reset(c) + + # this is a marker that helps to "equate" clauses to each other + # when a Select returns its list of FROM clauses. the cloning + # process leaves around a lot of remnants of the previous clause + # typically in the form of column expressions still attached to the + # old table. + c._is_clone_of = self + + return c + + @property + def _constructor(self): + """return the 'constructor' for this ClauseElement. + + This is for the purposes for creating a new object of + this type. Usually, its just the element's __class__. + However, the "Annotated" version of the object overrides + to return the class of its proxied element. + + """ + return self.__class__ + + @util.memoized_property + def _cloned_set(self): + """Return the set consisting all cloned ancestors of this + ClauseElement. + + Includes this ClauseElement. This accessor tends to be used for + FromClause objects to identify 'equivalent' FROM clauses, regardless + of transformative operations. + + """ + s = util.column_set() + f = self + while f is not None: + s.add(f) + f = f._is_clone_of + return s + + def __getstate__(self): + d = self.__dict__.copy() + d.pop('_is_clone_of', None) + return d + + def _annotate(self, values): + """return a copy of this ClauseElement with annotations + updated by the given dictionary. + + """ + return Annotated(self, values) + + def _with_annotations(self, values): + """return a copy of this ClauseElement with annotations + replaced by the given dictionary. + + """ + return Annotated(self, values) + + def _deannotate(self, values=None, clone=False): + """return a copy of this :class:`.ClauseElement` with annotations + removed. + + :param values: optional tuple of individual values + to remove. + + """ + if clone: + # clone is used when we are also copying + # the expression for a deep deannotation + return self._clone() + else: + # if no clone, since we have no annotations we return + # self + return self + + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_clauseelement(self, multiparams, params) + + def unique_params(self, *optionaldict, **kwargs): + """Return a copy with :func:`bindparam()` elements replaced. + + Same functionality as ``params()``, except adds `unique=True` + to affected bind parameters so that multiple statements can be + used. + + """ + return self._params(True, optionaldict, kwargs) + + def params(self, *optionaldict, **kwargs): + """Return a copy with :func:`bindparam()` elements replaced. + + Returns a copy of this ClauseElement with :func:`bindparam()` + elements replaced with values taken from the given dictionary:: + + >>> clause = column('x') + bindparam('foo') + >>> print clause.compile().params + {'foo':None} + >>> print clause.params({'foo':7}).compile().params + {'foo':7} + + """ + return self._params(False, optionaldict, kwargs) + + def _params(self, unique, optionaldict, kwargs): + if len(optionaldict) == 1: + kwargs.update(optionaldict[0]) + elif len(optionaldict) > 1: + raise exc.ArgumentError( + "params() takes zero or one positional dictionary argument") + + def visit_bindparam(bind): + if bind.key in kwargs: + bind.value = kwargs[bind.key] + bind.required = False + if unique: + bind._convert_to_unique() + return cloned_traverse(self, {}, {'bindparam': visit_bindparam}) + + def compare(self, other, **kw): + """Compare this ClauseElement to the given ClauseElement. + + Subclasses should override the default behavior, which is a + straight identity comparison. + + \**kw are arguments consumed by subclass compare() methods and + may be used to modify the criteria for comparison. + (see :class:`.ColumnElement`) + + """ + return self is other + + def _copy_internals(self, clone=_clone, **kw): + """Reassign internal elements to be clones of themselves. + + Called during a copy-and-traverse operation on newly + shallow-copied elements to create a deep copy. + + The given clone function should be used, which may be applying + additional transformations to the element (i.e. replacement + traversal, cloned traversal, annotations). + + """ + pass + + def get_children(self, **kwargs): + """Return immediate child elements of this :class:`.ClauseElement`. + + This is used for visit traversal. + + \**kwargs may contain flags that change the collection that is + returned, for example to return a subset of items in order to + cut down on larger traversals, or to return child items from a + different context (such as schema-level collections instead of + clause-level). + + """ + return [] + + def self_group(self, against=None): + """Apply a 'grouping' to this :class:`.ClauseElement`. + + This method is overridden by subclasses to return a + "grouping" construct, i.e. parenthesis. In particular + it's used by "binary" expressions to provide a grouping + around themselves when placed into a larger expression, + as well as by :func:`.select` constructs when placed into + the FROM clause of another :func:`.select`. (Note that + subqueries should be normally created using the + :meth:`.Select.alias` method, as many platforms require + nested SELECT statements to be named). + + As expressions are composed together, the application of + :meth:`self_group` is automatic - end-user code should never + need to use this method directly. Note that SQLAlchemy's + clause constructs take operator precedence into account - + so parenthesis might not be needed, for example, in + an expression like ``x OR (y AND z)`` - AND takes precedence + over OR. + + The base :meth:`self_group` method of :class:`.ClauseElement` + just returns self. + """ + return self + + @util.dependencies("sqlalchemy.engine.default") + def compile(self, default, bind=None, dialect=None, **kw): + """Compile this SQL expression. + + The return value is a :class:`~.Compiled` object. + Calling ``str()`` or ``unicode()`` on the returned value will yield a + string representation of the result. The + :class:`~.Compiled` object also can return a + dictionary of bind parameter names and values + using the ``params`` accessor. + + :param bind: An ``Engine`` or ``Connection`` from which a + ``Compiled`` will be acquired. This argument takes precedence over + this :class:`.ClauseElement`'s bound engine, if any. + + :param column_keys: Used for INSERT and UPDATE statements, a list of + column names which should be present in the VALUES clause of the + compiled statement. If ``None``, all columns from the target table + object are rendered. + + :param dialect: A ``Dialect`` instance from which a ``Compiled`` + will be acquired. This argument takes precedence over the `bind` + argument as well as this :class:`.ClauseElement`'s bound engine, if + any. + + :param inline: Used for INSERT statements, for a dialect which does + not support inline retrieval of newly generated primary key + columns, will force the expression used to create the new primary + key value to be rendered inline within the INSERT statement's + VALUES clause. This typically refers to Sequence execution but may + also refer to any server-side default generation function + associated with a primary key `Column`. + + """ + + if not dialect: + if bind: + dialect = bind.dialect + elif self.bind: + dialect = self.bind.dialect + bind = self.bind + else: + dialect = default.DefaultDialect() + return self._compiler(dialect, bind=bind, **kw) + + def _compiler(self, dialect, **kw): + """Return a compiler appropriate for this ClauseElement, given a + Dialect.""" + + return dialect.statement_compiler(dialect, self, **kw) + + def __str__(self): + if util.py3k: + return str(self.compile()) + else: + return unicode(self.compile()).encode('ascii', 'backslashreplace') + + def __and__(self, other): + return and_(self, other) + + def __or__(self, other): + return or_(self, other) + + def __invert__(self): + if hasattr(self, 'negation_clause'): + return self.negation_clause + else: + return self._negate() + + def __bool__(self): + raise TypeError("Boolean value of this clause is not defined") + + __nonzero__ = __bool__ + + def _negate(self): + return UnaryExpression( + self.self_group(against=operators.inv), + operator=operators.inv, + negate=None) + + def __repr__(self): + friendly = getattr(self, 'description', None) + if friendly is None: + return object.__repr__(self) + else: + return '<%s.%s at 0x%x; %s>' % ( + self.__module__, self.__class__.__name__, id(self), friendly) + + + +class ColumnElement(ClauseElement, operators.ColumnOperators): + """Represent a column-oriented SQL expression suitable for usage in the + "columns" clause, WHERE clause etc. of a statement. + + While the most familiar kind of :class:`.ColumnElement` is the + :class:`.Column` object, :class:`.ColumnElement` serves as the basis + for any unit that may be present in a SQL expression, including + the expressions themselves, SQL functions, bound parameters, + literal expressions, keywords such as ``NULL``, etc. + :class:`.ColumnElement` is the ultimate base class for all such elements. + + A :class:`.ColumnElement` provides the ability to generate new + :class:`.ColumnElement` + objects using Python expressions. This means that Python operators + such as ``==``, ``!=`` and ``<`` are overloaded to mimic SQL operations, + and allow the instantiation of further :class:`.ColumnElement` instances + which are composed from other, more fundamental :class:`.ColumnElement` + objects. For example, two :class:`.ColumnClause` objects can be added + together with the addition operator ``+`` to produce + a :class:`.BinaryExpression`. + Both :class:`.ColumnClause` and :class:`.BinaryExpression` are subclasses + of :class:`.ColumnElement`:: + + >>> from sqlalchemy.sql import column + >>> column('a') + column('b') + + >>> print column('a') + column('b') + a + b + + :class:`.ColumnElement` supports the ability to be a *proxy* element, + which indicates that the :class:`.ColumnElement` may be associated with + a :class:`.Selectable` which was derived from another :class:`.Selectable`. + An example of a "derived" :class:`.Selectable` is an :class:`.Alias` of a + :class:`~sqlalchemy.schema.Table`. For the ambitious, an in-depth + discussion of this concept can be found at + `Expression Transformations `_. + + """ + + __visit_name__ = 'column' + primary_key = False + foreign_keys = [] + _label = None + _key_label = None + _alt_names = () + + def self_group(self, against=None): + if against in (operators.and_, operators.or_, operators._asbool) and \ + self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: + return AsBoolean(self, operators.istrue, operators.isfalse) + else: + return self + + def _negate(self): + if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: + return AsBoolean(self, operators.isfalse, operators.istrue) + else: + return super(ColumnElement, self)._negate() + + @util.memoized_property + def type(self): + return type_api.NULLTYPE + + @util.memoized_property + def comparator(self): + return self.type.comparator_factory(self) + + def __getattr__(self, key): + try: + return getattr(self.comparator, key) + except AttributeError: + raise AttributeError( + 'Neither %r object nor %r object has an attribute %r' % ( + type(self).__name__, + type(self.comparator).__name__, + key) + ) + + def operate(self, op, *other, **kwargs): + return op(self.comparator, *other, **kwargs) + + def reverse_operate(self, op, other, **kwargs): + return op(other, self.comparator, **kwargs) + + def _bind_param(self, operator, obj): + return BindParameter(None, obj, + _compared_to_operator=operator, + _compared_to_type=self.type, unique=True) + + @property + def expression(self): + """Return a column expression. + + Part of the inspection interface; returns self. + + """ + return self + + @property + def _select_iterable(self): + return (self, ) + + @util.memoized_property + def base_columns(self): + return util.column_set(c for c in self.proxy_set + if not hasattr(c, '_proxies')) + + @util.memoized_property + def proxy_set(self): + s = util.column_set([self]) + if hasattr(self, '_proxies'): + for c in self._proxies: + s.update(c.proxy_set) + return s + + def shares_lineage(self, othercolumn): + """Return True if the given :class:`.ColumnElement` + has a common ancestor to this :class:`.ColumnElement`.""" + + return bool(self.proxy_set.intersection(othercolumn.proxy_set)) + + def _compare_name_for_result(self, other): + """Return True if the given column element compares to this one + when targeting within a result row.""" + + return hasattr(other, 'name') and hasattr(self, 'name') and \ + other.name == self.name + + def _make_proxy(self, selectable, name=None, name_is_truncatable=False, **kw): + """Create a new :class:`.ColumnElement` representing this + :class:`.ColumnElement` as it appears in the select list of a + descending selectable. + + """ + if name is None: + name = self.anon_label + try: + key = str(self) + except exc.UnsupportedCompilationError: + key = self.anon_label + else: + key = name + co = ColumnClause( + _as_truncated(name) if name_is_truncatable else name, + type_=getattr(self, 'type', None), + _selectable=selectable + ) + co._proxies = [self] + if selectable._is_clone_of is not None: + co._is_clone_of = \ + selectable._is_clone_of.columns.get(key) + selectable._columns[key] = co + return co + + def compare(self, other, use_proxies=False, equivalents=None, **kw): + """Compare this ColumnElement to another. + + Special arguments understood: + + :param use_proxies: when True, consider two columns that + share a common base column as equivalent (i.e. shares_lineage()) + + :param equivalents: a dictionary of columns as keys mapped to sets + of columns. If the given "other" column is present in this + dictionary, if any of the columns in the corresponding set() pass the + comparison test, the result is True. This is used to expand the + comparison to other columns that may be known to be equivalent to + this one via foreign key or other criterion. + + """ + to_compare = (other, ) + if equivalents and other in equivalents: + to_compare = equivalents[other].union(to_compare) + + for oth in to_compare: + if use_proxies and self.shares_lineage(oth): + return True + elif hash(oth) == hash(self): + return True + else: + return False + + def label(self, name): + """Produce a column label, i.e. `` AS ``. + + This is a shortcut to the :func:`~.expression.label` function. + + if 'name' is None, an anonymous label name will be generated. + + """ + return Label(name, self, self.type) + + @util.memoized_property + def anon_label(self): + """provides a constant 'anonymous label' for this ColumnElement. + + This is a label() expression which will be named at compile time. + The same label() is returned each time anon_label is called so + that expressions can reference anon_label multiple times, producing + the same label name at compile time. + + the compiler uses this function automatically at compile time + for expressions that are known to be 'unnamed' like binary + expressions and function calls. + + """ + return _anonymous_label('%%(%d %s)s' % (id(self), getattr(self, + 'name', 'anon'))) + + + +class BindParameter(ColumnElement): + """Represent a bound parameter value. + + """ + + __visit_name__ = 'bindparam' + + _is_crud = False + + def __init__(self, key, value=NO_ARG, type_=None, + unique=False, required=NO_ARG, + quote=None, callable_=None, + isoutparam=False, + _compared_to_operator=None, + _compared_to_type=None): + """Construct a new :class:`.BindParameter`. + + :param key: + the key for this bind param. Will be used in the generated + SQL statement for dialects that use named parameters. This + value may be modified when part of a compilation operation, + if other :class:`BindParameter` objects exist with the same + key, or if its length is too long and truncation is + required. + + :param value: + Initial value for this bind param. This value may be + overridden by the dictionary of parameters sent to statement + compilation/execution. + + Defaults to ``None``, however if neither ``value`` nor + ``callable`` are passed explicitly, the ``required`` flag will be + set to ``True`` which has the effect of requiring a value be present + when the statement is actually executed. + + .. versionchanged:: 0.8 The ``required`` flag is set to ``True`` + automatically if ``value`` or ``callable`` is not passed. + + :param callable\_: + A callable function that takes the place of "value". The function + will be called at statement execution time to determine the + ultimate value. Used for scenarios where the actual bind + value cannot be determined at the point at which the clause + construct is created, but embedded bind values are still desirable. + + :param type\_: + A ``TypeEngine`` object that will be used to pre-process the + value corresponding to this :class:`BindParameter` at + execution time. + + :param unique: + if True, the key name of this BindParamClause will be + modified if another :class:`BindParameter` of the same name + already has been located within the containing + :class:`.ClauseElement`. + + :param required: + If ``True``, a value is required at execution time. If not passed, + is set to ``True`` or ``False`` based on whether or not + one of ``value`` or ``callable`` were passed.. + + .. versionchanged:: 0.8 If the ``required`` flag is not specified, + it will be set automatically to ``True`` or ``False`` depending + on whether or not the ``value`` or ``callable`` parameters + were specified. + + :param quote: + True if this parameter name requires quoting and is not + currently known as a SQLAlchemy reserved word; this currently + only applies to the Oracle backend. + + :param isoutparam: + if True, the parameter should be treated like a stored procedure + "OUT" parameter. + + .. seealso:: + + :func:`.outparam` + + + + """ + if isinstance(key, ColumnClause): + type_ = key.type + key = key.name + if required is NO_ARG: + required = (value is NO_ARG and callable_ is None) + if value is NO_ARG: + value = None + + if quote is not None: + key = quoted_name(key, quote) + + if unique: + self.key = _anonymous_label('%%(%d %s)s' % (id(self), key + or 'param')) + else: + self.key = key or _anonymous_label('%%(%d param)s' + % id(self)) + + # identifying key that won't change across + # clones, used to identify the bind's logical + # identity + self._identifying_key = self.key + + # key that was passed in the first place, used to + # generate new keys + self._orig_key = key or 'param' + + self.unique = unique + self.value = value + self.callable = callable_ + self.isoutparam = isoutparam + self.required = required + if type_ is None: + if _compared_to_type is not None: + self.type = \ + _compared_to_type.coerce_compared_value( + _compared_to_operator, value) + else: + self.type = type_api._type_map.get(type(value), + type_api.NULLTYPE) + elif isinstance(type_, type): + self.type = type_() + else: + self.type = type_ + + def _with_value(self, value): + """Return a copy of this :class:`.BindParameter` with the given value set.""" + cloned = self._clone() + cloned.value = value + cloned.callable = None + cloned.required = False + if cloned.type is type_api.NULLTYPE: + cloned.type = type_api._type_map.get(type(value), + type_api.NULLTYPE) + return cloned + + @property + def effective_value(self): + """Return the value of this bound parameter, + taking into account if the ``callable`` parameter + was set. + + The ``callable`` value will be evaluated + and returned if present, else ``value``. + + """ + if self.callable: + return self.callable() + else: + return self.value + + def _clone(self): + c = ClauseElement._clone(self) + if self.unique: + c.key = _anonymous_label('%%(%d %s)s' % (id(c), c._orig_key + or 'param')) + return c + + def _convert_to_unique(self): + if not self.unique: + self.unique = True + self.key = _anonymous_label('%%(%d %s)s' % (id(self), + self._orig_key or 'param')) + + def compare(self, other, **kw): + """Compare this :class:`BindParameter` to the given + clause.""" + + return isinstance(other, BindParameter) \ + and self.type._compare_type_affinity(other.type) \ + and self.value == other.value + + def __getstate__(self): + """execute a deferred value for serialization purposes.""" + + d = self.__dict__.copy() + v = self.value + if self.callable: + v = self.callable() + d['callable'] = None + d['value'] = v + return d + + def __repr__(self): + return 'BindParameter(%r, %r, type_=%r)' % (self.key, + self.value, self.type) + + +class TypeClause(ClauseElement): + """Handle a type keyword in a SQL statement. + + Used by the ``Case`` statement. + + """ + + __visit_name__ = 'typeclause' + + def __init__(self, type): + self.type = type + + +class TextClause(Executable, ClauseElement): + """Represent a literal SQL text fragment. + + Public constructor is the :func:`text()` function. + + """ + + __visit_name__ = 'textclause' + + _bind_params_regex = re.compile(r'(?`` + to specify bind parameters; they will be compiled to their + engine-specific format. + + :param autocommit: + Deprecated. Use .execution_options(autocommit=) + to set the autocommit option. + + :param bind: + an optional connection or engine to be used for this text query. + + :param bindparams: + Deprecated. A list of :func:`.bindparam` instances used to + provide information about parameters embedded in the statement. + This argument now invokes the :meth:`.TextClause.bindparams` + method on the construct before returning it. E.g.:: + + stmt = text("SELECT * FROM table WHERE id=:id", + bindparams=[bindparam('id', value=5, type_=Integer)]) + + Is equivalent to:: + + stmt = text("SELECT * FROM table WHERE id=:id").\\ + bindparams(bindparam('id', value=5, type_=Integer)) + + .. deprecated:: 0.9.0 the :meth:`.TextClause.bindparams` method + supersedes the ``bindparams`` argument to :func:`.text`. + + :param typemap: + Deprecated. A dictionary mapping the names of columns + represented in the columns clause of a ``SELECT`` statement + to type objects, + which will be used to perform post-processing on columns within + the result set. This parameter now invokes the :meth:`.TextClause.columns` + method, which returns a :class:`.TextAsFrom` construct that gains + a ``.c`` collection and can be embedded in other expressions. E.g.:: + + stmt = text("SELECT * FROM table", + typemap={'id': Integer, 'name': String}, + ) + + Is equivalent to:: + + stmt = text("SELECT * FROM table").columns(id=Integer, name=String) + + Or alternatively:: + + from sqlalchemy.sql import column + stmt = text("SELECT * FROM table").columns( + column('id', Integer), + column('name', String) + ) + + .. deprecated:: 0.9.0 the :meth:`.TextClause.columns` method + supersedes the ``typemap`` argument to :func:`.text`. + + """ + stmt = TextClause(text, bind=bind) + if bindparams: + stmt = stmt.bindparams(*bindparams) + if typemap: + stmt = stmt.columns(**typemap) + if autocommit is not None: + util.warn_deprecated('autocommit on text() is deprecated. ' + 'Use .execution_options(autocommit=True)') + stmt = stmt.execution_options(autocommit=autocommit) + + return stmt + + @_generative + def bindparams(self, *binds, **names_to_values): + """Establish the values and/or types of bound parameters within + this :class:`.TextClause` construct. + + Given a text construct such as:: + + from sqlalchemy import text + stmt = text("SELECT id, name FROM user WHERE name=:name " + "AND timestamp=:timestamp") + + the :meth:`.TextClause.bindparams` method can be used to establish + the initial value of ``:name`` and ``:timestamp``, + using simple keyword arguments:: + + stmt = stmt.bindparams(name='jack', + timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5)) + + Where above, new :class:`.BindParameter` objects + will be generated with the names ``name`` and ``timestamp``, and + values of ``jack`` and ``datetime.datetime(2012, 10, 8, 15, 12, 5)``, + respectively. The types will be + inferred from the values given, in this case :class:`.String` and + :class:`.DateTime`. + + When specific typing behavior is needed, the positional ``*binds`` + argument can be used in which to specify :func:`.bindparam` constructs + directly. These constructs must include at least the ``key`` argument, + then an optional value and type:: + + from sqlalchemy import bindparam + stmt = stmt.bindparams( + bindparam('name', value='jack', type_=String), + bindparam('timestamp', type_=DateTime) + ) + + Above, we specified the type of :class:`.DateTime` for the ``timestamp`` + bind, and the type of :class:`.String` for the ``name`` bind. In + the case of ``name`` we also set the default value of ``"jack"``. + + Additional bound parameters can be supplied at statement execution + time, e.g.:: + + result = connection.execute(stmt, + timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5)) + + The :meth:`.TextClause.bindparams` method can be called repeatedly, where + it will re-use existing :class:`.BindParameter` objects to add new information. + For example, we can call :meth:`.TextClause.bindparams` first with + typing information, and a second time with value information, and it + will be combined:: + + stmt = text("SELECT id, name FROM user WHERE name=:name " + "AND timestamp=:timestamp") + stmt = stmt.bindparams( + bindparam('name', type_=String), + bindparam('timestamp', type_=DateTime) + ) + stmt = stmt.bindparams( + name='jack', + timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5) + ) + + + .. versionadded:: 0.9.0 The :meth:`.TextClause.bindparams` method supersedes + the argument ``bindparams`` passed to :func:`~.expression.text`. + + + """ + self._bindparams = new_params = self._bindparams.copy() + + for bind in binds: + try: + existing = new_params[bind.key] + except KeyError: + raise exc.ArgumentError( + "This text() construct doesn't define a " + "bound parameter named %r" % bind.key) + else: + new_params[existing.key] = bind + + for key, value in names_to_values.items(): + try: + existing = new_params[key] + except KeyError: + raise exc.ArgumentError( + "This text() construct doesn't define a " + "bound parameter named %r" % key) + else: + new_params[key] = existing._with_value(value) + + + + @util.dependencies('sqlalchemy.sql.selectable') + def columns(self, selectable, *cols, **types): + """Turn this :class:`.TextClause` object into a :class:`.TextAsFrom` + object that can be embedded into another statement. + + This function essentially bridges the gap between an entirely + textual SELECT statement and the SQL expression language concept + of a "selectable":: + + from sqlalchemy.sql import column, text + + stmt = text("SELECT id, name FROM some_table") + stmt = stmt.columns(column('id'), column('name')).alias('st') + + stmt = select([mytable]).\\ + select_from( + mytable.join(stmt, mytable.c.name == stmt.c.name) + ).where(stmt.c.id > 5) + + Above, we used untyped :func:`.column` elements. These can also have + types specified, which will impact how the column behaves in expressions + as well as determining result set behavior:: + + stmt = text("SELECT id, name, timestamp FROM some_table") + stmt = stmt.columns( + column('id', Integer), + column('name', Unicode), + column('timestamp', DateTime) + ) + + for id, name, timestamp in connection.execute(stmt): + print(id, name, timestamp) + + Keyword arguments allow just the names and types of columns to be specified, + where the :func:`.column` elements will be generated automatically:: + + stmt = text("SELECT id, name, timestamp FROM some_table") + stmt = stmt.columns( + id=Integer, + name=Unicode, + timestamp=DateTime + ) + + for id, name, timestamp in connection.execute(stmt): + print(id, name, timestamp) + + The :meth:`.TextClause.columns` method provides a direct + route to calling :meth:`.FromClause.alias` as well as :meth:`.SelectBase.cte` + against a textual SELECT statement:: + + stmt = stmt.columns(id=Integer, name=String).cte('st') + + stmt = select([sometable]).where(sometable.c.id == stmt.c.id) + + .. versionadded:: 0.9.0 :func:`.text` can now be converted into a fully + featured "selectable" construct using the :meth:`.TextClause.columns` + method. This method supersedes the ``typemap`` argument to + :func:`.text`. + + """ + + col_by_name = dict( + (col.key, col) for col in cols + ) + for key, type_ in types.items(): + col_by_name[key] = ColumnClause(key, type_) + + return selectable.TextAsFrom(self, list(col_by_name.values())) + + @property + def type(self): + return type_api.NULLTYPE + + @property + def comparator(self): + return self.type.comparator_factory(self) + + def self_group(self, against=None): + if against is operators.in_op: + return Grouping(self) + else: + return self + + def _copy_internals(self, clone=_clone, **kw): + self._bindparams = dict((b.key, clone(b, **kw)) + for b in self._bindparams.values()) + + def get_children(self, **kwargs): + return list(self._bindparams.values()) + + +class Null(ColumnElement): + """Represent the NULL keyword in a SQL statement. + + :class:`.Null` is accessed as a constant via the + :func:`.null` function. + + """ + + __visit_name__ = 'null' + + @util.memoized_property + def type(self): + return type_api.NULLTYPE + + @classmethod + def _singleton(cls): + """Return a constant :class:`.Null` construct.""" + + return NULL + + def compare(self, other): + return isinstance(other, Null) + + +class False_(ColumnElement): + """Represent the ``false`` keyword, or equivalent, in a SQL statement. + + :class:`.False_` is accessed as a constant via the + :func:`.false` function. + + """ + + __visit_name__ = 'false' + + @util.memoized_property + def type(self): + return type_api.BOOLEANTYPE + + def _negate(self): + return TRUE + + @classmethod + def _singleton(cls): + """Return a constant :class:`.False_` construct. + + E.g.:: + + >>> from sqlalchemy import false + >>> print select([t.c.x]).where(false()) + SELECT x FROM t WHERE false + + A backend which does not support true/false constants will render as + an expression against 1 or 0:: + + >>> print select([t.c.x]).where(false()) + SELECT x FROM t WHERE 0 = 1 + + The :func:`.true` and :func:`.false` constants also feature + "short circuit" operation within an :func:`.and_` or :func:`.or_` + conjunction:: + + >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) + SELECT x FROM t WHERE true + + >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) + SELECT x FROM t WHERE false + + .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature + better integrated behavior within conjunctions and on dialects + that don't support true/false constants. + + .. seealso:: + + :func:`.true` + + """ + + return FALSE + + def compare(self, other): + return isinstance(other, False_) + +class True_(ColumnElement): + """Represent the ``true`` keyword, or equivalent, in a SQL statement. + + :class:`.True_` is accessed as a constant via the + :func:`.true` function. + + """ + + __visit_name__ = 'true' + + @util.memoized_property + def type(self): + return type_api.BOOLEANTYPE + + def _negate(self): + return FALSE + + @classmethod + def _ifnone(cls, other): + if other is None: + return cls._singleton() + else: + return other + + @classmethod + def _singleton(cls): + """Return a constant :class:`.True_` construct. + + E.g.:: + + >>> from sqlalchemy import true + >>> print select([t.c.x]).where(true()) + SELECT x FROM t WHERE true + + A backend which does not support true/false constants will render as + an expression against 1 or 0:: + + >>> print select([t.c.x]).where(true()) + SELECT x FROM t WHERE 1 = 1 + + The :func:`.true` and :func:`.false` constants also feature + "short circuit" operation within an :func:`.and_` or :func:`.or_` + conjunction:: + + >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) + SELECT x FROM t WHERE true + + >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) + SELECT x FROM t WHERE false + + .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature + better integrated behavior within conjunctions and on dialects + that don't support true/false constants. + + .. seealso:: + + :func:`.false` + + """ + + return TRUE + + def compare(self, other): + return isinstance(other, True_) + +NULL = Null() +FALSE = False_() +TRUE = True_() + +class ClauseList(ClauseElement): + """Describe a list of clauses, separated by an operator. + + By default, is comma-separated, such as a column listing. + + """ + __visit_name__ = 'clauselist' + + def __init__(self, *clauses, **kwargs): + self.operator = kwargs.pop('operator', operators.comma_op) + self.group = kwargs.pop('group', True) + self.group_contents = kwargs.pop('group_contents', True) + if self.group_contents: + self.clauses = [ + _literal_as_text(clause).self_group(against=self.operator) + for clause in clauses] + else: + self.clauses = [ + _literal_as_text(clause) + for clause in clauses] + + def __iter__(self): + return iter(self.clauses) + + def __len__(self): + return len(self.clauses) + + @property + def _select_iterable(self): + return iter(self) + + def append(self, clause): + if self.group_contents: + self.clauses.append(_literal_as_text(clause).\ + self_group(against=self.operator)) + else: + self.clauses.append(_literal_as_text(clause)) + + def _copy_internals(self, clone=_clone, **kw): + self.clauses = [clone(clause, **kw) for clause in self.clauses] + + def get_children(self, **kwargs): + return self.clauses + + @property + def _from_objects(self): + return list(itertools.chain(*[c._from_objects for c in self.clauses])) + + def self_group(self, against=None): + if self.group and operators.is_precedent(self.operator, against): + return Grouping(self) + else: + return self + + def compare(self, other, **kw): + """Compare this :class:`.ClauseList` to the given :class:`.ClauseList`, + including a comparison of all the clause items. + + """ + if not isinstance(other, ClauseList) and len(self.clauses) == 1: + return self.clauses[0].compare(other, **kw) + elif isinstance(other, ClauseList) and \ + len(self.clauses) == len(other.clauses): + for i in range(0, len(self.clauses)): + if not self.clauses[i].compare(other.clauses[i], **kw): + return False + else: + return self.operator == other.operator + else: + return False + + + +class BooleanClauseList(ClauseList, ColumnElement): + __visit_name__ = 'clauselist' + + def __init__(self, *arg, **kw): + raise NotImplementedError( + "BooleanClauseList has a private constructor") + + @classmethod + def _construct(cls, operator, continue_on, skip_on, *clauses, **kw): + convert_clauses = [] + + clauses = util.coerce_generator_arg(clauses) + for clause in clauses: + clause = _literal_as_text(clause) + + if isinstance(clause, continue_on): + continue + elif isinstance(clause, skip_on): + return clause.self_group(against=operators._asbool) + + convert_clauses.append(clause) + + if len(convert_clauses) == 1: + return convert_clauses[0].self_group(against=operators._asbool) + elif not convert_clauses and clauses: + return clauses[0].self_group(against=operators._asbool) + + convert_clauses = [c.self_group(against=operator) + for c in convert_clauses] + + self = cls.__new__(cls) + self.clauses = convert_clauses + self.group = True + self.operator = operator + self.group_contents = True + self.type = type_api.BOOLEANTYPE + return self + + @classmethod + def and_(cls, *clauses): + """Join a list of clauses together using the ``AND`` operator. + + The ``&`` operator is also overloaded on all :class:`.ColumnElement` + subclasses to produce the + same result. + + """ + return cls._construct(operators.and_, True_, False_, *clauses) + + @classmethod + def or_(cls, *clauses): + """Join a list of clauses together using the ``OR`` operator. + + The ``|`` operator is also overloaded on all + :class:`.ColumnElement` subclasses to produce the + same result. + + """ + return cls._construct(operators.or_, False_, True_, *clauses) + + @property + def _select_iterable(self): + return (self, ) + + def self_group(self, against=None): + if not self.clauses: + return self + else: + return super(BooleanClauseList, self).self_group(against=against) + + def _negate(self): + return ClauseList._negate(self) + + +and_ = BooleanClauseList.and_ +or_ = BooleanClauseList.or_ + +class Tuple(ClauseList, ColumnElement): + """Represent a SQL tuple.""" + + def __init__(self, *clauses, **kw): + """Return a :class:`.Tuple`. + + Main usage is to produce a composite IN construct:: + + from sqlalchemy import tuple_ + + tuple_(table.c.col1, table.c.col2).in_( + [(1, 2), (5, 12), (10, 19)] + ) + + .. warning:: + + The composite IN construct is not supported by all backends, + and is currently known to work on Postgresql and MySQL, + but not SQLite. Unsupported backends will raise + a subclass of :class:`~sqlalchemy.exc.DBAPIError` when such + an expression is invoked. + + """ + + clauses = [_literal_as_binds(c) for c in clauses] + self.type = kw.pop('type_', None) + if self.type is None: + self.type = _type_from_args(clauses) + super(Tuple, self).__init__(*clauses, **kw) + + @property + def _select_iterable(self): + return (self, ) + + def _bind_param(self, operator, obj): + return Tuple(*[ + BindParameter(None, o, _compared_to_operator=operator, + _compared_to_type=self.type, unique=True) + for o in obj + ]).self_group() + + +class Case(ColumnElement): + """Represent a SQL ``CASE`` construct. + + + """ + __visit_name__ = 'case' + + def __init__(self, whens, value=None, else_=None): + """Produce a :class:`.Case` object. + + :param whens: A sequence of pairs, or alternatively a dict, + to be translated into "WHEN / THEN" clauses. + + :param value: Optional for simple case statements, produces + a column expression as in "CASE WHEN ..." + + :param else\_: Optional as well, for case defaults produces + the "ELSE" portion of the "CASE" statement. + + The expressions used for THEN and ELSE, + when specified as strings, will be interpreted + as bound values. To specify textual SQL expressions + for these, use the :func:`literal_column` + construct. + + The expressions used for the WHEN criterion + may only be literal strings when "value" is + present, i.e. CASE table.somecol WHEN "x" THEN "y". + Otherwise, literal strings are not accepted + in this position, and either the text() + or literal() constructs must be used to + interpret raw string values. + + Usage examples:: + + case([(orderline.c.qty > 100, item.c.specialprice), + (orderline.c.qty > 10, item.c.bulkprice) + ], else_=item.c.regularprice) + + case(value=emp.c.type, whens={ + 'engineer': emp.c.salary * 1.1, + 'manager': emp.c.salary * 3, + }) + + Using :func:`.literal_column()`, to allow for databases that + do not support bind parameters in the ``then`` clause. The type + can be specified which determines the type of the :func:`case()` construct + overall:: + + case([(orderline.c.qty > 100, + literal_column("'greaterthan100'", String)), + (orderline.c.qty > 10, literal_column("'greaterthan10'", + String)) + ], else_=literal_column("'lethan10'", String)) + + """ + + try: + whens = util.dictlike_iteritems(whens) + except TypeError: + pass + + if value is not None: + whenlist = [ + (_literal_as_binds(c).self_group(), + _literal_as_binds(r)) for (c, r) in whens + ] + else: + whenlist = [ + (_no_literals(c).self_group(), + _literal_as_binds(r)) for (c, r) in whens + ] + + if whenlist: + type_ = list(whenlist[-1])[-1].type + else: + type_ = None + + if value is None: + self.value = None + else: + self.value = _literal_as_binds(value) + + self.type = type_ + self.whens = whenlist + if else_ is not None: + self.else_ = _literal_as_binds(else_) + else: + self.else_ = None + + def _copy_internals(self, clone=_clone, **kw): + if self.value is not None: + self.value = clone(self.value, **kw) + self.whens = [(clone(x, **kw), clone(y, **kw)) + for x, y in self.whens] + if self.else_ is not None: + self.else_ = clone(self.else_, **kw) + + def get_children(self, **kwargs): + if self.value is not None: + yield self.value + for x, y in self.whens: + yield x + yield y + if self.else_ is not None: + yield self.else_ + + @property + def _from_objects(self): + return list(itertools.chain(*[x._from_objects for x in + self.get_children()])) + + +def literal_column(text, type_=None): + """Return a textual column expression, as would be in the columns + clause of a ``SELECT`` statement. + + The object returned supports further expressions in the same way as any + other column object, including comparison, math and string operations. + The type\_ parameter is important to determine proper expression behavior + (such as, '+' means string concatenation or numerical addition based on + the type). + + :param text: the text of the expression; can be any SQL expression. + Quoting rules will not be applied. To specify a column-name expression + which should be subject to quoting rules, use the :func:`column` + function. + + :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` + object which will + provide result-set translation and additional expression semantics for + this column. If left as None the type will be NullType. + + """ + return ColumnClause(text, type_=type_, is_literal=True) + + + +class Cast(ColumnElement): + """Represent the SQL ``CAST`` construct.""" + + __visit_name__ = 'cast' + + def __init__(self, expression, type_): + """Return a :class:`.Cast` object. + + Equivalent of SQL ``CAST(clause AS totype)``. + + E.g.:: + + cast(table.c.unit_price * table.c.qty, Numeric(10,4)) + + or:: + + cast(table.c.timestamp, DATE) + + :param expression: Column-oriented expression. + :param type_: A :class:`.TypeEngine` class or instance indicating + the type to which the CAST should apply. + + .. seealso:: + + :func:`.type_coerce` - Python-side type coercion without emitting + CAST. + + """ + self.type = type_api.to_instance(type_) + self.clause = _literal_as_binds(expression, type_=self.type) + self.typeclause = TypeClause(self.type) + + def _copy_internals(self, clone=_clone, **kw): + self.clause = clone(self.clause, **kw) + self.typeclause = clone(self.typeclause, **kw) + + def get_children(self, **kwargs): + return self.clause, self.typeclause + + @property + def _from_objects(self): + return self.clause._from_objects + + +class Extract(ColumnElement): + """Represent a SQL EXTRACT clause, ``extract(field FROM expr)``.""" + + __visit_name__ = 'extract' + + def __init__(self, field, expr, **kwargs): + """Return a :class:`.Extract` construct. + + This is typically available as :func:`.extract` + as well as ``func.extract`` from the + :data:`.func` namespace. + + """ + self.type = type_api.INTEGERTYPE + self.field = field + self.expr = _literal_as_binds(expr, None) + + def _copy_internals(self, clone=_clone, **kw): + self.expr = clone(self.expr, **kw) + + def get_children(self, **kwargs): + return self.expr, + + @property + def _from_objects(self): + return self.expr._from_objects + + +class UnaryExpression(ColumnElement): + """Define a 'unary' expression. + + A unary expression has a single column expression + and an operator. The operator can be placed on the left + (where it is called the 'operator') or right (where it is called the + 'modifier') of the column expression. + + """ + __visit_name__ = 'unary' + + def __init__(self, element, operator=None, modifier=None, + type_=None, negate=None): + self.operator = operator + self.modifier = modifier + self.element = element.self_group(against=self.operator or self.modifier) + self.type = type_api.to_instance(type_) + self.negate = negate + + @classmethod + def _create_nullsfirst(cls, column): + """Return a NULLS FIRST ``ORDER BY`` clause element. + + e.g.:: + + someselect.order_by(desc(table1.mycol).nullsfirst()) + + produces:: + + ORDER BY mycol DESC NULLS FIRST + + """ + return UnaryExpression( + _literal_as_text(column), modifier=operators.nullsfirst_op) + + + @classmethod + def _create_nullslast(cls, column): + """Return a NULLS LAST ``ORDER BY`` clause element. + + e.g.:: + + someselect.order_by(desc(table1.mycol).nullslast()) + + produces:: + + ORDER BY mycol DESC NULLS LAST + + """ + return UnaryExpression( + _literal_as_text(column), modifier=operators.nullslast_op) + + + @classmethod + def _create_desc(cls, column): + """Return a descending ``ORDER BY`` clause element. + + e.g.:: + + someselect.order_by(desc(table1.mycol)) + + produces:: + + ORDER BY mycol DESC + + """ + return UnaryExpression( + _literal_as_text(column), modifier=operators.desc_op) + + @classmethod + def _create_asc(cls, column): + """Return an ascending ``ORDER BY`` clause element. + + e.g.:: + + someselect.order_by(asc(table1.mycol)) + + produces:: + + ORDER BY mycol ASC + + """ + return UnaryExpression( + _literal_as_text(column), modifier=operators.asc_op) + + @classmethod + def _create_distinct(cls, expr): + """Return a ``DISTINCT`` clause. + + e.g.:: + + distinct(a) + + renders:: + + DISTINCT a + + """ + expr = _literal_as_binds(expr) + return UnaryExpression(expr, + operator=operators.distinct_op, type_=expr.type) + + @util.memoized_property + def _order_by_label_element(self): + if self.modifier in (operators.desc_op, operators.asc_op): + return self.element._order_by_label_element + else: + return None + + @property + def _from_objects(self): + return self.element._from_objects + + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) + + def get_children(self, **kwargs): + return self.element, + + def compare(self, other, **kw): + """Compare this :class:`UnaryExpression` against the given + :class:`.ClauseElement`.""" + + return ( + isinstance(other, UnaryExpression) and + self.operator == other.operator and + self.modifier == other.modifier and + self.element.compare(other.element, **kw) + ) + + def _negate(self): + if self.negate is not None: + return UnaryExpression( + self.element, + operator=self.negate, + negate=self.operator, + modifier=self.modifier, + type_=self.type) + else: + return ClauseElement._negate(self) + + def self_group(self, against=None): + if self.operator and operators.is_precedent(self.operator, against): + return Grouping(self) + else: + return self + + +class AsBoolean(UnaryExpression): + + def __init__(self, element, operator, negate): + self.element = element + self.type = type_api.BOOLEANTYPE + self.operator = operator + self.negate = negate + self.modifier = None + + def self_group(self, against=None): + return self + + def _negate(self): + return self.element._negate() + + +class BinaryExpression(ColumnElement): + """Represent an expression that is ``LEFT RIGHT``. + + A :class:`.BinaryExpression` is generated automatically + whenever two column expressions are used in a Python binary expresion:: + + >>> from sqlalchemy.sql import column + >>> column('a') + column('b') + + >>> print column('a') + column('b') + a + b + + """ + + __visit_name__ = 'binary' + + def __init__(self, left, right, operator, type_=None, + negate=None, modifiers=None): + # allow compatibility with libraries that + # refer to BinaryExpression directly and pass strings + if isinstance(operator, util.string_types): + operator = operators.custom_op(operator) + self._orig = (left, right) + self.left = left.self_group(against=operator) + self.right = right.self_group(against=operator) + self.operator = operator + self.type = type_api.to_instance(type_) + self.negate = negate + + if modifiers is None: + self.modifiers = {} + else: + self.modifiers = modifiers + + def __bool__(self): + if self.operator in (operator.eq, operator.ne): + return self.operator(hash(self._orig[0]), hash(self._orig[1])) + else: + raise TypeError("Boolean value of this clause is not defined") + + __nonzero__ = __bool__ + + @property + def is_comparison(self): + return operators.is_comparison(self.operator) + + @property + def _from_objects(self): + return self.left._from_objects + self.right._from_objects + + def _copy_internals(self, clone=_clone, **kw): + self.left = clone(self.left, **kw) + self.right = clone(self.right, **kw) + + def get_children(self, **kwargs): + return self.left, self.right + + def compare(self, other, **kw): + """Compare this :class:`BinaryExpression` against the + given :class:`BinaryExpression`.""" + + return ( + isinstance(other, BinaryExpression) and + self.operator == other.operator and + ( + self.left.compare(other.left, **kw) and + self.right.compare(other.right, **kw) or + ( + operators.is_commutative(self.operator) and + self.left.compare(other.right, **kw) and + self.right.compare(other.left, **kw) + ) + ) + ) + + def self_group(self, against=None): + if operators.is_precedent(self.operator, against): + return Grouping(self) + else: + return self + + def _negate(self): + if self.negate is not None: + return BinaryExpression( + self.left, + self.right, + self.negate, + negate=self.operator, + type_=type_api.BOOLEANTYPE, + modifiers=self.modifiers) + else: + return super(BinaryExpression, self)._negate() + + + + +class Grouping(ColumnElement): + """Represent a grouping within a column expression""" + + __visit_name__ = 'grouping' + + def __init__(self, element): + self.element = element + self.type = getattr(element, 'type', type_api.NULLTYPE) + + def self_group(self, against=None): + return self + + @property + def _label(self): + return getattr(self.element, '_label', None) or self.anon_label + + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) + + def get_children(self, **kwargs): + return self.element, + + @property + def _from_objects(self): + return self.element._from_objects + + def __getattr__(self, attr): + return getattr(self.element, attr) + + def __getstate__(self): + return {'element': self.element, 'type': self.type} + + def __setstate__(self, state): + self.element = state['element'] + self.type = state['type'] + + def compare(self, other, **kw): + return isinstance(other, Grouping) and \ + self.element.compare(other.element) + + +class Over(ColumnElement): + """Represent an OVER clause. + + This is a special operator against a so-called + "window" function, as well as any aggregate function, + which produces results relative to the result set + itself. It's supported only by certain database + backends. + + """ + __visit_name__ = 'over' + + order_by = None + partition_by = None + + def __init__(self, func, partition_by=None, order_by=None): + """Produce an :class:`.Over` object against a function. + + Used against aggregate or so-called "window" functions, + for database backends that support window functions. + + E.g.:: + + from sqlalchemy import over + over(func.row_number(), order_by='x') + + Would produce "ROW_NUMBER() OVER(ORDER BY x)". + + :param func: a :class:`.FunctionElement` construct, typically + generated by :data:`~.expression.func`. + :param partition_by: a column element or string, or a list + of such, that will be used as the PARTITION BY clause + of the OVER construct. + :param order_by: a column element or string, or a list + of such, that will be used as the ORDER BY clause + of the OVER construct. + + This function is also available from the :data:`~.expression.func` + construct itself via the :meth:`.FunctionElement.over` method. + + .. versionadded:: 0.7 + + """ + self.func = func + if order_by is not None: + self.order_by = ClauseList(*util.to_list(order_by)) + if partition_by is not None: + self.partition_by = ClauseList(*util.to_list(partition_by)) + + @util.memoized_property + def type(self): + return self.func.type + + def get_children(self, **kwargs): + return [c for c in + (self.func, self.partition_by, self.order_by) + if c is not None] + + def _copy_internals(self, clone=_clone, **kw): + self.func = clone(self.func, **kw) + if self.partition_by is not None: + self.partition_by = clone(self.partition_by, **kw) + if self.order_by is not None: + self.order_by = clone(self.order_by, **kw) + + @property + def _from_objects(self): + return list(itertools.chain( + *[c._from_objects for c in + (self.func, self.partition_by, self.order_by) + if c is not None] + )) + + +class Label(ColumnElement): + """Represents a column label (AS). + + Represent a label, as typically applied to any column-level + element using the ``AS`` sql keyword. + + """ + + __visit_name__ = 'label' + + def __init__(self, name, element, type_=None): + """Return a :class:`Label` object for the + given :class:`.ColumnElement`. + + A label changes the name of an element in the columns clause of a + ``SELECT`` statement, typically via the ``AS`` SQL keyword. + + This functionality is more conveniently available via the + :meth:`.ColumnElement.label` method on :class:`.ColumnElement`. + + :param name: label name + + :param obj: a :class:`.ColumnElement`. + + """ + while isinstance(element, Label): + element = element.element + if name: + self.name = name + else: + self.name = _anonymous_label('%%(%d %s)s' % (id(self), + getattr(element, 'name', 'anon'))) + self.key = self._label = self._key_label = self.name + self._element = element + self._type = type_ + self._proxies = [element] + + def __reduce__(self): + return self.__class__, (self.name, self._element, self._type) + + @util.memoized_property + def _order_by_label_element(self): + return self + + @util.memoized_property + def type(self): + return type_api.to_instance( + self._type or getattr(self._element, 'type', None) + ) + + @util.memoized_property + def element(self): + return self._element.self_group(against=operators.as_) + + def self_group(self, against=None): + sub_element = self._element.self_group(against=against) + if sub_element is not self._element: + return Label(self.name, + sub_element, + type_=self._type) + else: + return self + + @property + def primary_key(self): + return self.element.primary_key + + @property + def foreign_keys(self): + return self.element.foreign_keys + + def get_children(self, **kwargs): + return self.element, + + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) + + @property + def _from_objects(self): + return self.element._from_objects + + def _make_proxy(self, selectable, name=None, **kw): + e = self.element._make_proxy(selectable, + name=name if name else self.name) + e._proxies.append(self) + if self._type is not None: + e.type = self._type + return e + + +class ColumnClause(Immutable, ColumnElement): + """Represents a generic column expression from any textual string. + + This includes columns associated with tables, aliases and select + statements, but also any arbitrary text. May or may not be bound + to an underlying :class:`.Selectable`. + + :class:`.ColumnClause` is constructed by itself typically via + the :func:`~.expression.column` function. It may be placed directly + into constructs such as :func:`.select` constructs:: + + from sqlalchemy.sql import column, select + + c1, c2 = column("c1"), column("c2") + s = select([c1, c2]).where(c1==5) + + There is also a variant on :func:`~.expression.column` known + as :func:`~.expression.literal_column` - the difference is that + in the latter case, the string value is assumed to be an exact + expression, rather than a column name, so that no quoting rules + or similar are applied:: + + from sqlalchemy.sql import literal_column, select + + s = select([literal_column("5 + 7")]) + + :class:`.ColumnClause` can also be used in a table-like + fashion by combining the :func:`~.expression.column` function + with the :func:`~.expression.table` function, to produce + a "lightweight" form of table metadata:: + + from sqlalchemy.sql import table, column + + user = table("user", + column("id"), + column("name"), + column("description"), + ) + + The above construct can be created in an ad-hoc fashion and is + not associated with any :class:`.schema.MetaData`, unlike it's + more full fledged :class:`.schema.Table` counterpart. + + """ + __visit_name__ = 'column' + + onupdate = default = server_default = server_onupdate = None + + _memoized_property = util.group_expirable_memoized_property() + + def __init__(self, text, type_=None, is_literal=False, _selectable=None): + """Construct a :class:`.ColumnClause` object. + + :param text: the text of the element. + + :param type: :class:`.types.TypeEngine` object which can associate + this :class:`.ColumnClause` with a type. + + :param is_literal: if True, the :class:`.ColumnClause` is assumed to + be an exact expression that will be delivered to the output with no + quoting rules applied regardless of case sensitive settings. the + :func:`literal_column()` function is usually used to create such a + :class:`.ColumnClause`. + + :param text: the name of the column. Quoting rules will be applied + to the clause like any other column name. For textual column constructs + that are not to be quoted, use the :func:`literal_column` function. + + :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` object + which will provide result-set translation for this column. + + + """ + + self.key = self.name = text + self.table = _selectable + self.type = type_api.to_instance(type_) + self.is_literal = is_literal + + def _compare_name_for_result(self, other): + if self.is_literal or \ + self.table is None or \ + not hasattr(other, 'proxy_set') or ( + isinstance(other, ColumnClause) and other.is_literal + ): + return super(ColumnClause, self).\ + _compare_name_for_result(other) + else: + return other.proxy_set.intersection(self.proxy_set) + + def _get_table(self): + return self.__dict__['table'] + + def _set_table(self, table): + self._memoized_property.expire_instance(self) + self.__dict__['table'] = table + table = property(_get_table, _set_table) + + @_memoized_property + def _from_objects(self): + t = self.table + if t is not None: + return [t] + else: + return [] + + @util.memoized_property + def description(self): + if util.py3k: + return self.name + else: + return self.name.encode('ascii', 'backslashreplace') + + @_memoized_property + def _key_label(self): + if self.key != self.name: + return self._gen_label(self.key) + else: + return self._label + + @_memoized_property + def _label(self): + return self._gen_label(self.name) + + def _gen_label(self, name): + t = self.table + + if self.is_literal: + return None + + elif t is not None and t.named_with_column: + if getattr(t, 'schema', None): + label = t.schema.replace('.', '_') + "_" + \ + t.name + "_" + name + else: + label = t.name + "_" + name + + # propagate name quoting rules for labels. + if getattr(name, "quote", None) is not None: + if isinstance(label, quoted_name): + label.quote = name.quote + else: + label = quoted_name(label, name.quote) + elif getattr(t.name, "quote", None) is not None: + # can't get this situation to occur, so let's + # assert false on it for now + assert not isinstance(label, quoted_name) + label = quoted_name(label, t.name.quote) + + # ensure the label name doesn't conflict with that + # of an existing column + if label in t.c: + _label = label + counter = 1 + while _label in t.c: + _label = label + "_" + str(counter) + counter += 1 + label = _label + + return _as_truncated(label) + + else: + return name + + def _bind_param(self, operator, obj): + return BindParameter(self.name, obj, + _compared_to_operator=operator, + _compared_to_type=self.type, + unique=True) + + def _make_proxy(self, selectable, name=None, attach=True, + name_is_truncatable=False, **kw): + # propagate the "is_literal" flag only if we are keeping our name, + # otherwise its considered to be a label + is_literal = self.is_literal and (name is None or name == self.name) + c = self._constructor( + _as_truncated(name or self.name) if \ + name_is_truncatable else \ + (name or self.name), + type_=self.type, + _selectable=selectable, + is_literal=is_literal + ) + if name is None: + c.key = self.key + c._proxies = [self] + if selectable._is_clone_of is not None: + c._is_clone_of = \ + selectable._is_clone_of.columns.get(c.key) + + if attach: + selectable._columns[c.key] = c + return c + + +class _IdentifiedClause(Executable, ClauseElement): + + __visit_name__ = 'identified' + _execution_options = \ + Executable._execution_options.union({'autocommit': False}) + + def __init__(self, ident): + self.ident = ident + + +class SavepointClause(_IdentifiedClause): + __visit_name__ = 'savepoint' + + +class RollbackToSavepointClause(_IdentifiedClause): + __visit_name__ = 'rollback_to_savepoint' + + +class ReleaseSavepointClause(_IdentifiedClause): + __visit_name__ = 'release_savepoint' + + +class quoted_name(util.text_type): + """Represent a SQL identifier combined with quoting preferences. + + :class:`.quoted_name` is a Python unicode/str subclass which + represents a particular identifier name along with a + ``quote`` flag. This ``quote`` flag, when set to + ``True`` or ``False``, overrides automatic quoting behavior + for this identifier in order to either unconditionally quote + or to not quote the name. If left at its default of ``None``, + quoting behavior is applied to the identifier on a per-backend basis + based on an examination of the token itself. + + A :class:`.quoted_name` object with ``quote=True`` is also + prevented from being modified in the case of a so-called + "name normalize" option. Certain database backends, such as + Oracle, Firebird, and DB2 "normalize" case-insensitive names + as uppercase. The SQLAlchemy dialects for these backends + convert from SQLAlchemy's lower-case-means-insensitive convention + to the upper-case-means-insensitive conventions of those backends. + The ``quote=True`` flag here will prevent this conversion from occurring + to support an identifier that's quoted as all lower case against + such a backend. + + The :class:`.quoted_name` object is normally created automatically + when specifying the name for key schema constructs such as :class:`.Table`, + :class:`.Column`, and others. The class can also be passed explicitly + as the name to any function that receives a name which can be quoted. + Such as to use the :meth:`.Engine.has_table` method with an unconditionally + quoted name:: + + from sqlaclchemy import create_engine + from sqlalchemy.sql.elements import quoted_name + + engine = create_engine("oracle+cx_oracle://some_dsn") + engine.has_table(quoted_name("some_table", True)) + + The above logic will run the "has table" logic against the Oracle backend, + passing the name exactly as ``"some_table"`` without converting to + upper case. + + .. versionadded:: 0.9.0 + + """ + + def __new__(cls, value, quote): + if value is None: + return None + # experimental - don't bother with quoted_name + # if quote flag is None. doesn't seem to make any dent + # in performance however + # elif not sprcls and quote is None: + # return value + elif isinstance(value, cls) and ( + quote is None or value.quote == quote + ): + return value + self = super(quoted_name, cls).__new__(cls, value) + self.quote = quote + return self + + def __reduce__(self): + return quoted_name, (util.text_type(self), self.quote) + + @util.memoized_instancemethod + def lower(self): + if self.quote: + return self + else: + return util.text_type(self).lower() + + @util.memoized_instancemethod + def upper(self): + if self.quote: + return self + else: + return util.text_type(self).upper() + + def __repr__(self): + backslashed = self.encode('ascii', 'backslashreplace') + if not util.py2k: + backslashed = backslashed.decode('ascii') + return "'%s'" % backslashed + +class _truncated_label(quoted_name): + """A unicode subclass used to identify symbolic " + "names that may require truncation.""" + + def __new__(cls, value, quote=None): + quote = getattr(value, "quote", quote) + #return super(_truncated_label, cls).__new__(cls, value, quote, True) + return super(_truncated_label, cls).__new__(cls, value, quote) + + def __reduce__(self): + return self.__class__, (util.text_type(self), self.quote) + + def apply_map(self, map_): + return self + +# for backwards compatibility in case +# someone is re-implementing the +# _truncated_identifier() sequence in a custom +# compiler +_generated_label = _truncated_label + + +class _anonymous_label(_truncated_label): + """A unicode subclass used to identify anonymously + generated names.""" + + def __add__(self, other): + return _anonymous_label( + quoted_name( + util.text_type.__add__(self, util.text_type(other)), + self.quote) + ) + + def __radd__(self, other): + return _anonymous_label( + quoted_name( + util.text_type.__add__(util.text_type(other), self), + self.quote) + ) + + def apply_map(self, map_): + if self.quote is not None: + # preserve quoting only if necessary + return quoted_name(self % map_, self.quote) + else: + # else skip the constructor call + return self % map_ + + +def _as_truncated(value): + """coerce the given value to :class:`._truncated_label`. + + Existing :class:`._truncated_label` and + :class:`._anonymous_label` objects are passed + unchanged. + """ + + if isinstance(value, _truncated_label): + return value + else: + return _truncated_label(value) + + +def _string_or_unprintable(element): + if isinstance(element, util.string_types): + return element + else: + try: + return str(element) + except: + return "unprintable element %r" % element + + +def _expand_cloned(elements): + """expand the given set of ClauseElements to be the set of all 'cloned' + predecessors. + + """ + return itertools.chain(*[x._cloned_set for x in elements]) + + +def _select_iterables(elements): + """expand tables into individual columns in the + given list of column expressions. + + """ + return itertools.chain(*[c._select_iterable for c in elements]) + + +def _cloned_intersection(a, b): + """return the intersection of sets a and b, counting + any overlap between 'cloned' predecessors. + + The returned set is in terms of the entities present within 'a'. + + """ + all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) + return set(elem for elem in a + if all_overlap.intersection(elem._cloned_set)) + +def _cloned_difference(a, b): + all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) + return set(elem for elem in a + if not all_overlap.intersection(elem._cloned_set)) + + +def _labeled(element): + if not hasattr(element, 'name'): + return element.label(None) + else: + return element + + +def _is_column(col): + """True if ``col`` is an instance of :class:`.ColumnElement`.""" + + return isinstance(col, ColumnElement) + + +def _find_columns(clause): + """locate Column objects within the given expression.""" + + cols = util.column_set() + traverse(clause, {}, {'column': cols.add}) + return cols + + +# there is some inconsistency here between the usage of +# inspect() vs. checking for Visitable and __clause_element__. +# Ideally all functions here would derive from inspect(), +# however the inspect() versions add significant callcount +# overhead for critical functions like _interpret_as_column_or_from(). +# Generally, the column-based functions are more performance critical +# and are fine just checking for __clause_element__(). it's only +# _interpret_as_from() where we'd like to be able to receive ORM entities +# that have no defined namespace, hence inspect() is needed there. + + +def _column_as_key(element): + if isinstance(element, util.string_types): + return element + if hasattr(element, '__clause_element__'): + element = element.__clause_element__() + try: + return element.key + except AttributeError: + return None + + +def _clause_element_as_expr(element): + if hasattr(element, '__clause_element__'): + return element.__clause_element__() + else: + return element + + +def _literal_as_text(element): + if isinstance(element, Visitable): + return element + elif hasattr(element, '__clause_element__'): + return element.__clause_element__() + elif isinstance(element, util.string_types): + return TextClause(util.text_type(element)) + elif isinstance(element, (util.NoneType, bool)): + return _const_expr(element) + else: + raise exc.ArgumentError( + "SQL expression object or string expected." + ) + + +def _no_literals(element): + if hasattr(element, '__clause_element__'): + return element.__clause_element__() + elif not isinstance(element, Visitable): + raise exc.ArgumentError("Ambiguous literal: %r. Use the 'text()' " + "function to indicate a SQL expression " + "literal, or 'literal()' to indicate a " + "bound value." % element) + else: + return element + + +def _is_literal(element): + return not isinstance(element, Visitable) and \ + not hasattr(element, '__clause_element__') + + +def _only_column_elements_or_none(element, name): + if element is None: + return None + else: + return _only_column_elements(element, name) + + +def _only_column_elements(element, name): + if hasattr(element, '__clause_element__'): + element = element.__clause_element__() + if not isinstance(element, ColumnElement): + raise exc.ArgumentError( + "Column-based expression object expected for argument " + "'%s'; got: '%s', type %s" % (name, element, type(element))) + return element + +def _literal_as_binds(element, name=None, type_=None): + if hasattr(element, '__clause_element__'): + return element.__clause_element__() + elif not isinstance(element, Visitable): + if element is None: + return Null() + else: + return BindParameter(name, element, type_=type_, unique=True) + else: + return element + + +def _interpret_as_column_or_from(element): + if isinstance(element, Visitable): + return element + elif hasattr(element, '__clause_element__'): + return element.__clause_element__() + + insp = inspection.inspect(element, raiseerr=False) + if insp is None: + if isinstance(element, (util.NoneType, bool)): + return _const_expr(element) + elif hasattr(insp, "selectable"): + return insp.selectable + + return ColumnClause(str(element), is_literal=True) + + +def _const_expr(element): + if isinstance(element, (Null, False_, True_)): + return element + elif element is None: + return Null() + elif element is False: + return False_() + elif element is True: + return True_() + else: + raise exc.ArgumentError( + "Expected None, False, or True" + ) + + +def _type_from_args(args): + for a in args: + if not a.type._isnull: + return a.type + else: + return type_api.NULLTYPE + + +def _corresponding_column_or_error(fromclause, column, + require_embedded=False): + c = fromclause.corresponding_column(column, + require_embedded=require_embedded) + if c is None: + raise exc.InvalidRequestError( + "Given column '%s', attached to table '%s', " + "failed to locate a corresponding column from table '%s'" + % + (column, + getattr(column, 'table', None), + fromclause.description) + ) + return c + + +class AnnotatedColumnElement(Annotated): + def __init__(self, element, values): + Annotated.__init__(self, element, values) + ColumnElement.comparator._reset(self) + for attr in ('name', 'key', 'table'): + if self.__dict__.get(attr, False) is None: + self.__dict__.pop(attr) + + def _with_annotations(self, values): + clone = super(AnnotatedColumnElement, self)._with_annotations(values) + ColumnElement.comparator._reset(clone) + return clone + + @util.memoized_property + def name(self): + """pull 'name' from parent, if not present""" + return self._Annotated__element.name + + @util.memoized_property + def table(self): + """pull 'table' from parent, if not present""" + return self._Annotated__element.table + + @util.memoized_property + def key(self): + """pull 'key' from parent, if not present""" + return self._Annotated__element.key + + @util.memoized_property + def info(self): + return self._Annotated__element.info + diff --git a/libs/sqlalchemy/sql/expression.py b/libs/sqlalchemy/sql/expression.py index c90a3dcb..c99665b4 100644 --- a/libs/sqlalchemy/sql/expression.py +++ b/libs/sqlalchemy/sql/expression.py @@ -1,45 +1,18 @@ # sql/expression.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Defines the base components of SQL expression trees. +"""Defines the public namespace for SQL expression constructs. -All components are derived from a common base class -:class:`.ClauseElement`. Common behaviors are organized -based on class hierarchies, in some cases via mixins. - -All object construction from this package occurs via functions which -in some cases will construct composite :class:`.ClauseElement` structures -together, and in other cases simply return a single :class:`.ClauseElement` -constructed directly. The function interface affords a more "DSL-ish" -feel to constructing SQL expressions and also allows future class -reorganizations. - -Even though classes are not constructed directly from the outside, -most classes which have additional public methods are considered to be -public (i.e. have no leading underscore). Other classes which are -"semi-public" are marked with a single leading underscore; these -classes usually have few or no public methods and are less guaranteed -to stay the same in future releases. +Prior to version 0.9, this module contained all of "elements", "dml", +"default_comparator" and "selectable". The module was broken up +and most "factory" functions were moved to be grouped with their associated +class. """ -import itertools, re -from operator import attrgetter - -from sqlalchemy import util, exc -from sqlalchemy.sql import operators -from sqlalchemy.sql.operators import Operators, ColumnOperators -from sqlalchemy.sql.visitors import Visitable, cloned_traverse -import operator - -functions = util.importlater("sqlalchemy.sql", "functions") -sqlutil = util.importlater("sqlalchemy.sql", "util") -sqltypes = util.importlater("sqlalchemy", "types") -default = util.importlater("sqlalchemy.engine", "default") - __all__ = [ 'Alias', 'ClauseElement', 'ColumnCollection', 'ColumnElement', 'CompoundSelect', 'Delete', 'FromClause', 'Insert', 'Join', 'Select', @@ -48,5683 +21,107 @@ __all__ = [ 'except_', 'except_all', 'exists', 'extract', 'func', 'modifier', 'collate', 'insert', 'intersect', 'intersect_all', 'join', 'label', 'literal', 'literal_column', 'not_', 'null', 'nullsfirst', 'nullslast', - 'or_', 'outparam', 'outerjoin', 'over', 'select', 'subquery', 'table', 'text', - 'tuple_', 'type_coerce', 'union', 'union_all', 'update', ] - -PARSE_AUTOCOMMIT = util.symbol('PARSE_AUTOCOMMIT') - -def nullsfirst(column): - """Return a NULLS FIRST ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol).nullsfirst()) - - produces:: - - ORDER BY mycol DESC NULLS FIRST - - """ - return _UnaryExpression(column, modifier=operators.nullsfirst_op) - -def nullslast(column): - """Return a NULLS LAST ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol).nullslast()) - - produces:: - - ORDER BY mycol DESC NULLS LAST - - """ - return _UnaryExpression(column, modifier=operators.nullslast_op) - -def desc(column): - """Return a descending ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol)) - - produces:: - - ORDER BY mycol DESC - - """ - return _UnaryExpression(column, modifier=operators.desc_op) - -def asc(column): - """Return an ascending ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(asc(table1.mycol)) - - produces:: - - ORDER BY mycol ASC - - """ - return _UnaryExpression(column, modifier=operators.asc_op) - -def outerjoin(left, right, onclause=None): - """Return an ``OUTER JOIN`` clause element. - - The returned object is an instance of :class:`.Join`. - - Similar functionality is also available via the - :meth:`~.FromClause.outerjoin()` method on any - :class:`.FromClause`. - - :param left: The left side of the join. - - :param right: The right side of the join. - - :param onclause: Optional criterion for the ``ON`` clause, is - derived from foreign key relationships established between - left and right otherwise. - - To chain joins together, use the :meth:`.FromClause.join` or - :meth:`.FromClause.outerjoin` methods on the resulting - :class:`.Join` object. - - """ - return Join(left, right, onclause, isouter=True) - -def join(left, right, onclause=None, isouter=False): - """Return a ``JOIN`` clause element (regular inner join). - - The returned object is an instance of :class:`.Join`. - - Similar functionality is also available via the - :meth:`~.FromClause.join()` method on any - :class:`.FromClause`. - - :param left: The left side of the join. - - :param right: The right side of the join. - - :param onclause: Optional criterion for the ``ON`` clause, is - derived from foreign key relationships established between - left and right otherwise. - - To chain joins together, use the :meth:`.FromClause.join` or - :meth:`.FromClause.outerjoin` methods on the resulting - :class:`.Join` object. - - - """ - return Join(left, right, onclause, isouter) - -def select(columns=None, whereclause=None, from_obj=[], **kwargs): - """Returns a ``SELECT`` clause element. - - Similar functionality is also available via the :func:`select()` - method on any :class:`.FromClause`. - - The returned object is an instance of :class:`.Select`. - - All arguments which accept :class:`.ClauseElement` arguments also accept - string arguments, which will be converted as appropriate into - either :func:`text()` or :func:`literal_column()` constructs. - - See also: - - :ref:`coretutorial_selecting` - Core Tutorial description of :func:`.select`. - - :param columns: - A list of :class:`.ClauseElement` objects, typically - :class:`.ColumnElement` objects or subclasses, which will form the - columns clause of the resulting statement. For all members which are - instances of :class:`.Selectable`, the individual :class:`.ColumnElement` - members of the :class:`.Selectable` will be added individually to the - columns clause. For example, specifying a - :class:`~sqlalchemy.schema.Table` instance will result in all the - contained :class:`~sqlalchemy.schema.Column` objects within to be added - to the columns clause. - - This argument is not present on the form of :func:`select()` - available on :class:`~sqlalchemy.schema.Table`. - - :param whereclause: - A :class:`.ClauseElement` expression which will be used to form the - ``WHERE`` clause. - - :param from_obj: - A list of :class:`.ClauseElement` objects which will be added to the - ``FROM`` clause of the resulting statement. Note that "from" objects are - automatically located within the columns and whereclause ClauseElements. - Use this parameter to explicitly specify "from" objects which are not - automatically locatable. This could include - :class:`~sqlalchemy.schema.Table` objects that aren't otherwise present, - or :class:`.Join` objects whose presence will supercede that of the - :class:`~sqlalchemy.schema.Table` objects already located in the other - clauses. - - :param autocommit: - Deprecated. Use .execution_options(autocommit=) - to set the autocommit option. - - :param bind=None: - an :class:`~.base.Engine` or :class:`~.base.Connection` instance - to which the - resulting :class:`.Select` object will be bound. The :class:`.Select` - object will otherwise automatically bind to whatever - :class:`~.base.Connectable` instances can be located within its contained - :class:`.ClauseElement` members. - - :param correlate=True: - indicates that this :class:`.Select` object should have its - contained :class:`.FromClause` elements "correlated" to an enclosing - :class:`.Select` object. This means that any :class:`.ClauseElement` - instance within the "froms" collection of this :class:`.Select` - which is also present in the "froms" collection of an - enclosing select will not be rendered in the ``FROM`` clause - of this select statement. - - :param distinct=False: - when ``True``, applies a ``DISTINCT`` qualifier to the columns - clause of the resulting statement. - - The boolean argument may also be a column expression or list - of column expressions - this is a special calling form which - is understood by the Postgresql dialect to render the - ``DISTINCT ON ()`` syntax. - - ``distinct`` is also available via the :meth:`~.Select.distinct` - generative method. - - .. note:: - - The ``distinct`` keyword's acceptance of a string - argument for usage with MySQL is deprecated. Use - the ``prefixes`` argument or :meth:`~.Select.prefix_with`. - - :param for_update=False: - when ``True``, applies ``FOR UPDATE`` to the end of the - resulting statement. - - Certain database dialects also support - alternate values for this parameter: - - * With the MySQL dialect, the value ``"read"`` translates to - ``LOCK IN SHARE MODE``. - * With the Oracle and Postgresql dialects, the value ``"nowait"`` - translates to ``FOR UPDATE NOWAIT``. - * With the Postgresql dialect, the values "read" and ``"read_nowait"`` - translate to ``FOR SHARE`` and ``FOR SHARE NOWAIT``, respectively. - - .. versionadded:: 0.7.7 - - :param group_by: - a list of :class:`.ClauseElement` objects which will comprise the - ``GROUP BY`` clause of the resulting select. - - :param having: - a :class:`.ClauseElement` that will comprise the ``HAVING`` clause - of the resulting select when ``GROUP BY`` is used. - - :param limit=None: - a numerical value which usually compiles to a ``LIMIT`` - expression in the resulting select. Databases that don't - support ``LIMIT`` will attempt to provide similar - functionality. - - :param offset=None: - a numeric value which usually compiles to an ``OFFSET`` - expression in the resulting select. Databases that don't - support ``OFFSET`` will attempt to provide similar - functionality. - - :param order_by: - a scalar or list of :class:`.ClauseElement` objects which will - comprise the ``ORDER BY`` clause of the resulting select. - - :param prefixes: - a list of strings or :class:`.ClauseElement` objects to include - directly after the SELECT keyword in the generated statement, - for dialect-specific query features. ``prefixes`` is - also available via the :meth:`~.Select.prefix_with` - generative method. - - :param use_labels=False: - when ``True``, the statement will be generated using labels - for each column in the columns clause, which qualify each - column with its parent table's (or aliases) name so that name - conflicts between columns in different tables don't occur. - The format of the label is _. The "c" - collection of the resulting :class:`.Select` object will use these - names as well for targeting column members. - - use_labels is also available via the :meth:`~._SelectBase.apply_labels` - generative method. - - """ - return Select(columns, whereclause=whereclause, from_obj=from_obj, - **kwargs) - -def subquery(alias, *args, **kwargs): - """Return an :class:`.Alias` object derived - from a :class:`.Select`. - - name - alias name - - \*args, \**kwargs - - all other arguments are delivered to the - :func:`select` function. - - """ - return Select(*args, **kwargs).alias(alias) - -def insert(table, values=None, inline=False, **kwargs): - """Represent an ``INSERT`` statement via the :class:`.Insert` SQL - construct. - - Similar functionality is available via the :meth:`~.TableClause.insert` method on - :class:`~.schema.Table`. - - - :param table: The table to be inserted into. - - :param values: A dictionary which specifies the column specifications of - the ``INSERT``, and is optional. If left as None, the column - specifications are determined from the bind parameters used during the - compile phase of the ``INSERT`` statement. If the bind parameters also - are None during the compile phase, then the column specifications will be - generated from the full list of table columns. Note that the - :meth:`~Insert.values()` generative method may also be used for this. - - :param prefixes: A list of modifier keywords to be inserted between INSERT - and INTO. Alternatively, the :meth:`~Insert.prefix_with` generative - method may be used. - - :param inline: if True, SQL defaults will be compiled 'inline' into the - statement and not pre-executed. - - If both `values` and compile-time bind parameters are present, the - compile-time bind parameters override the information specified - within `values` on a per-key basis. - - The keys within `values` can be either :class:`~sqlalchemy.schema.Column` - objects or their string identifiers. Each key may reference one of: - - * a literal data value (i.e. string, number, etc.); - * a Column object; - * a SELECT statement. - - If a ``SELECT`` statement is specified which references this - ``INSERT`` statement's table, the statement will be correlated - against the ``INSERT`` statement. - - See also: - - :ref:`coretutorial_insert_expressions` - SQL Expression Tutorial - - :ref:`inserts_and_updates` - SQL Expression Tutorial - - """ - return Insert(table, values, inline=inline, **kwargs) - -def update(table, whereclause=None, values=None, inline=False, **kwargs): - """Represent an ``UPDATE`` statement via the :class:`.Update` SQL - construct. - - E.g.:: - - from sqlalchemy import update - - stmt = update(users).where(users.c.id==5).\\ - values(name='user #5') - - Similar functionality is available via the :meth:`~.TableClause.update` method on - :class:`.Table`:: - - - stmt = users.update().\\ - where(users.c.id==5).\\ - values(name='user #5') - - :param table: A :class:`.Table` object representing the database - table to be updated. - - :param whereclause: Optional SQL expression describing the ``WHERE`` - condition of the ``UPDATE`` statement. Modern applications - may prefer to use the generative :meth:`~Update.where()` - method to specify the ``WHERE`` clause. - - The WHERE clause can refer to multiple tables. - For databases which support this, an ``UPDATE FROM`` clause will - be generated, or on MySQL, a multi-table update. The statement - will fail on databases that don't have support for multi-table - update statements. A SQL-standard method of referring to - additional tables in the WHERE clause is to use a correlated - subquery:: - - users.update().values(name='ed').where( - users.c.name==select([addresses.c.email_address]).\\ - where(addresses.c.user_id==users.c.id).\\ - as_scalar() - ) - - .. versionchanged:: 0.7.4 - The WHERE clause can refer to multiple tables. - - :param values: - Optional dictionary which specifies the ``SET`` conditions of the - ``UPDATE``. If left as ``None``, the ``SET`` - conditions are determined from those parameters passed to the - statement during the execution and/or compilation of the - statement. When compiled standalone without any parameters, - the ``SET`` clause generates for all columns. - - Modern applications may prefer to use the generative - :meth:`.Update.values` method to set the values of the - UPDATE statement. - - :param inline: - if True, SQL defaults present on :class:`.Column` objects via - the ``default`` keyword will be compiled 'inline' into the statement - and not pre-executed. This means that their values will not - be available in the dictionary returned from - :meth:`.ResultProxy.last_updated_params`. - - If both ``values`` and compile-time bind parameters are present, the - compile-time bind parameters override the information specified - within ``values`` on a per-key basis. - - The keys within ``values`` can be either :class:`.Column` - objects or their string identifiers (specifically the "key" of the - :class:`.Column`, normally but not necessarily equivalent to - its "name"). Normally, the - :class:`.Column` objects used here are expected to be - part of the target :class:`.Table` that is the table - to be updated. However when using MySQL, a multiple-table - UPDATE statement can refer to columns from any of - the tables referred to in the WHERE clause. - - The values referred to in ``values`` are typically: - - * a literal data value (i.e. string, number, etc.) - * a SQL expression, such as a related :class:`.Column`, - a scalar-returning :func:`.select` construct, - etc. - - When combining :func:`.select` constructs within the values - clause of an :func:`.update` construct, - the subquery represented by the :func:`.select` should be - *correlated* to the parent table, that is, providing criterion - which links the table inside the subquery to the outer table - being updated:: - - users.update().values( - name=select([addresses.c.email_address]).\\ - where(addresses.c.user_id==users.c.id).\\ - as_scalar() - ) - - See also: - - :ref:`inserts_and_updates` - SQL Expression - Language Tutorial - - - """ - return Update( - table, - whereclause=whereclause, - values=values, - inline=inline, - **kwargs) - -def delete(table, whereclause = None, **kwargs): - """Represent a ``DELETE`` statement via the :class:`.Delete` SQL - construct. - - Similar functionality is available via the :meth:`~.TableClause.delete` method on - :class:`~.schema.Table`. - - :param table: The table to be updated. - - :param whereclause: A :class:`.ClauseElement` describing the ``WHERE`` - condition of the ``UPDATE`` statement. Note that the - :meth:`~Delete.where()` generative method may be used instead. - - See also: - - :ref:`deletes` - SQL Expression Tutorial - - """ - return Delete(table, whereclause, **kwargs) - -def and_(*clauses): - """Join a list of clauses together using the ``AND`` operator. - - The ``&`` operator is also overloaded on all - :class:`_CompareMixin` subclasses to produce the - same result. - - """ - if len(clauses) == 1: - return clauses[0] - return BooleanClauseList(operator=operators.and_, *clauses) - -def or_(*clauses): - """Join a list of clauses together using the ``OR`` operator. - - The ``|`` operator is also overloaded on all - :class:`_CompareMixin` subclasses to produce the - same result. - - """ - if len(clauses) == 1: - return clauses[0] - return BooleanClauseList(operator=operators.or_, *clauses) - -def not_(clause): - """Return a negation of the given clause, i.e. ``NOT(clause)``. - - The ``~`` operator is also overloaded on all - :class:`_CompareMixin` subclasses to produce the - same result. - - """ - return operators.inv(_literal_as_binds(clause)) - -def distinct(expr): - """Return a ``DISTINCT`` clause. - - e.g.:: - - distinct(a) - - renders:: - - DISTINCT a - - """ - expr = _literal_as_binds(expr) - return _UnaryExpression(expr, operator=operators.distinct_op, type_=expr.type) - -def between(ctest, cleft, cright): - """Return a ``BETWEEN`` predicate clause. - - Equivalent of SQL ``clausetest BETWEEN clauseleft AND clauseright``. - - The :func:`between()` method on all - :class:`_CompareMixin` subclasses provides - similar functionality. - - """ - ctest = _literal_as_binds(ctest) - return ctest.between(cleft, cright) - - -def case(whens, value=None, else_=None): - """Produce a ``CASE`` statement. - - whens - A sequence of pairs, or alternatively a dict, - to be translated into "WHEN / THEN" clauses. - - value - Optional for simple case statements, produces - a column expression as in "CASE WHEN ..." - - else\_ - Optional as well, for case defaults produces - the "ELSE" portion of the "CASE" statement. - - The expressions used for THEN and ELSE, - when specified as strings, will be interpreted - as bound values. To specify textual SQL expressions - for these, use the :func:`literal_column` - construct. - - The expressions used for the WHEN criterion - may only be literal strings when "value" is - present, i.e. CASE table.somecol WHEN "x" THEN "y". - Otherwise, literal strings are not accepted - in this position, and either the text() - or literal() constructs must be used to - interpret raw string values. - - Usage examples:: - - case([(orderline.c.qty > 100, item.c.specialprice), - (orderline.c.qty > 10, item.c.bulkprice) - ], else_=item.c.regularprice) - case(value=emp.c.type, whens={ - 'engineer': emp.c.salary * 1.1, - 'manager': emp.c.salary * 3, - }) - - Using :func:`literal_column()`, to allow for databases that - do not support bind parameters in the ``then`` clause. The type - can be specified which determines the type of the :func:`case()` construct - overall:: - - case([(orderline.c.qty > 100, - literal_column("'greaterthan100'", String)), - (orderline.c.qty > 10, literal_column("'greaterthan10'", - String)) - ], else_=literal_column("'lethan10'", String)) - - """ - - return _Case(whens, value=value, else_=else_) - -def cast(clause, totype, **kwargs): - """Return a ``CAST`` function. - - Equivalent of SQL ``CAST(clause AS totype)``. - - Use with a :class:`~sqlalchemy.types.TypeEngine` subclass, i.e:: - - cast(table.c.unit_price * table.c.qty, Numeric(10,4)) - - or:: - - cast(table.c.timestamp, DATE) - - """ - return _Cast(clause, totype, **kwargs) - -def extract(field, expr): - """Return the clause ``extract(field FROM expr)``.""" - - return _Extract(field, expr) - -def collate(expression, collation): - """Return the clause ``expression COLLATE collation``. - - e.g.:: - - collate(mycolumn, 'utf8_bin') - - produces:: - - mycolumn COLLATE utf8_bin - - """ - - expr = _literal_as_binds(expression) - return _BinaryExpression( - expr, - _literal_as_text(collation), - operators.collate, type_=expr.type) - -def exists(*args, **kwargs): - """Return an ``EXISTS`` clause as applied to a :class:`.Select` object. - - Calling styles are of the following forms:: - - # use on an existing select() - s = select([table.c.col1]).where(table.c.col2==5) - s = exists(s) - - # construct a select() at once - exists(['*'], **select_arguments).where(criterion) - - # columns argument is optional, generates "EXISTS (SELECT *)" - # by default. - exists().where(table.c.col2==5) - - """ - return _Exists(*args, **kwargs) - -def union(*selects, **kwargs): - """Return a ``UNION`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - A similar :func:`union()` method is available on all - :class:`.FromClause` subclasses. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.UNION, *selects, **kwargs) - -def union_all(*selects, **kwargs): - """Return a ``UNION ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - A similar :func:`union_all()` method is available on all - :class:`.FromClause` subclasses. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.UNION_ALL, *selects, **kwargs) - -def except_(*selects, **kwargs): - """Return an ``EXCEPT`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.EXCEPT, *selects, **kwargs) - -def except_all(*selects, **kwargs): - """Return an ``EXCEPT ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.EXCEPT_ALL, *selects, **kwargs) - -def intersect(*selects, **kwargs): - """Return an ``INTERSECT`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.INTERSECT, *selects, **kwargs) - -def intersect_all(*selects, **kwargs): - """Return an ``INTERSECT ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.INTERSECT_ALL, *selects, **kwargs) - -def alias(selectable, name=None): - """Return an :class:`.Alias` object. - - An :class:`.Alias` represents any :class:`.FromClause` - with an alternate name assigned within SQL, typically using the ``AS`` - clause when generated, e.g. ``SELECT * FROM table AS aliasname``. - - Similar functionality is available via the - :meth:`~.FromClause.alias` method - available on all :class:`.FromClause` subclasses. - - When an :class:`.Alias` is created from a :class:`.Table` object, - this has the effect of the table being rendered - as ``tablename AS aliasname`` in a SELECT statement. - - For :func:`.select` objects, the effect is that of creating a named - subquery, i.e. ``(select ...) AS aliasname``. - - The ``name`` parameter is optional, and provides the name - to use in the rendered SQL. If blank, an "anonymous" name - will be deterministically generated at compile time. - Deterministic means the name is guaranteed to be unique against - other constructs used in the same statement, and will also be the - same name for each successive compilation of the same statement - object. - - :param selectable: any :class:`.FromClause` subclass, - such as a table, select statement, etc. - - :param name: string name to be assigned as the alias. - If ``None``, a name will be deterministically generated - at compile time. - - """ - return Alias(selectable, name=name) - - -def literal(value, type_=None): - """Return a literal clause, bound to a bind parameter. - - Literal clauses are created automatically when non- :class:`.ClauseElement` - objects (such as strings, ints, dates, etc.) are used in a comparison - operation with a :class:`_CompareMixin` - subclass, such as a :class:`~sqlalchemy.schema.Column` object. Use this function to force the - generation of a literal clause, which will be created as a - :class:`_BindParamClause` with a bound value. - - :param value: the value to be bound. Can be any Python object supported by - the underlying DB-API, or is translatable via the given type argument. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` which - will provide bind-parameter translation for this literal. - - """ - return _BindParamClause(None, value, type_=type_, unique=True) - -def tuple_(*expr): - """Return a SQL tuple. - - Main usage is to produce a composite IN construct:: - - tuple_(table.c.col1, table.c.col2).in_( - [(1, 2), (5, 12), (10, 19)] - ) - - .. warning:: - - The composite IN construct is not supported by all backends, - and is currently known to work on Postgresql and MySQL, - but not SQLite. Unsupported backends will raise - a subclass of :class:`~sqlalchemy.exc.DBAPIError` when such - an expression is invoked. - - """ - return _Tuple(*expr) - -def type_coerce(expr, type_): - """Coerce the given expression into the given type, on the Python side only. - - :func:`.type_coerce` is roughly similar to :func:`.cast`, except no - "CAST" expression is rendered - the given type is only applied towards - expression typing and against received result values. - - e.g.:: - - from sqlalchemy.types import TypeDecorator - import uuid - - class AsGuid(TypeDecorator): - impl = String - - def process_bind_param(self, value, dialect): - if value is not None: - return str(value) - else: - return None - - def process_result_value(self, value, dialect): - if value is not None: - return uuid.UUID(value) - else: - return None - - conn.execute( - select([type_coerce(mytable.c.ident, AsGuid)]).\\ - where( - type_coerce(mytable.c.ident, AsGuid) == - uuid.uuid3(uuid.NAMESPACE_URL, 'bar') - ) - ) - - """ - if hasattr(expr, '__clause_expr__'): - return type_coerce(expr.__clause_expr__()) - - elif not isinstance(expr, Visitable): - if expr is None: - return null() - else: - return literal(expr, type_=type_) - else: - return _Label(None, expr, type_=type_) - - -def label(name, obj): - """Return a :class:`_Label` object for the - given :class:`.ColumnElement`. - - A label changes the name of an element in the columns clause of a - ``SELECT`` statement, typically via the ``AS`` SQL keyword. - - This functionality is more conveniently available via the - :func:`label()` method on :class:`.ColumnElement`. - - name - label name - - obj - a :class:`.ColumnElement`. - - """ - return _Label(name, obj) - -def column(text, type_=None): - """Return a textual column clause, as would be in the columns clause of a - ``SELECT`` statement. - - The object returned is an instance of :class:`.ColumnClause`, which - represents the "syntactical" portion of the schema-level - :class:`~sqlalchemy.schema.Column` object. It is often used directly - within :func:`~.expression.select` constructs or with lightweight :func:`~.expression.table` - constructs. - - Note that the :func:`~.expression.column` function is not part of - the ``sqlalchemy`` namespace. It must be imported from the ``sql`` package:: - - from sqlalchemy.sql import table, column - - :param text: the name of the column. Quoting rules will be applied - to the clause like any other column name. For textual column constructs - that are not to be quoted, use the :func:`literal_column` function. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` object - which will provide result-set translation for this column. - - See :class:`.ColumnClause` for further examples. - - """ - return ColumnClause(text, type_=type_) - -def literal_column(text, type_=None): - """Return a textual column expression, as would be in the columns - clause of a ``SELECT`` statement. - - The object returned supports further expressions in the same way as any - other column object, including comparison, math and string operations. - The type\_ parameter is important to determine proper expression behavior - (such as, '+' means string concatenation or numerical addition based on - the type). - - :param text: the text of the expression; can be any SQL expression. - Quoting rules will not be applied. To specify a column-name expression - which should be subject to quoting rules, use the :func:`column` - function. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` object which will - provide result-set translation and additional expression semantics for - this column. If left as None the type will be NullType. - - """ - return ColumnClause(text, type_=type_, is_literal=True) - -def table(name, *columns): - """Represent a textual table clause. - - The object returned is an instance of :class:`.TableClause`, which represents the - "syntactical" portion of the schema-level :class:`~.schema.Table` object. - It may be used to construct lightweight table constructs. - - Note that the :func:`~.expression.table` function is not part of - the ``sqlalchemy`` namespace. It must be imported from the ``sql`` package:: - - from sqlalchemy.sql import table, column - - :param name: Name of the table. - - :param columns: A collection of :func:`~.expression.column` constructs. - - See :class:`.TableClause` for further examples. - - """ - return TableClause(name, *columns) - -def bindparam(key, value=None, type_=None, unique=False, required=False, callable_=None): - """Create a bind parameter clause with the given key. - - :param key: - the key for this bind param. Will be used in the generated - SQL statement for dialects that use named parameters. This - value may be modified when part of a compilation operation, - if other :class:`_BindParamClause` objects exist with the same - key, or if its length is too long and truncation is - required. - - :param value: - Initial value for this bind param. This value may be - overridden by the dictionary of parameters sent to statement - compilation/execution. - - :param callable\_: - A callable function that takes the place of "value". The function - will be called at statement execution time to determine the - ultimate value. Used for scenarios where the actual bind - value cannot be determined at the point at which the clause - construct is created, but embedded bind values are still desirable. - - :param type\_: - A ``TypeEngine`` object that will be used to pre-process the - value corresponding to this :class:`_BindParamClause` at - execution time. - - :param unique: - if True, the key name of this BindParamClause will be - modified if another :class:`_BindParamClause` of the same name - already has been located within the containing - :class:`.ClauseElement`. - - :param required: - a value is required at execution time. - - """ - if isinstance(key, ColumnClause): - return _BindParamClause(key.name, value, type_=key.type, - callable_=callable_, - unique=unique, required=required) - else: - return _BindParamClause(key, value, type_=type_, - callable_=callable_, - unique=unique, required=required) - -def outparam(key, type_=None): - """Create an 'OUT' parameter for usage in functions (stored procedures), - for databases which support them. - - The ``outparam`` can be used like a regular function parameter. - The "output" value will be available from the - :class:`~sqlalchemy.engine.ResultProxy` object via its ``out_parameters`` - attribute, which returns a dictionary containing the values. - - """ - return _BindParamClause( - key, None, type_=type_, unique=False, isoutparam=True) - -def text(text, bind=None, *args, **kwargs): - """Create a SQL construct that is represented by a literal string. - - E.g.:: - - t = text("SELECT * FROM users") - result = connection.execute(t) - - The advantages :func:`text` provides over a plain string are - backend-neutral support for bind parameters, per-statement - execution options, as well as - bind parameter and result-column typing behavior, allowing - SQLAlchemy type constructs to play a role when executing - a statement that is specified literally. - - Bind parameters are specified by name, using the format ``:name``. - E.g.:: - - t = text("SELECT * FROM users WHERE id=:user_id") - result = connection.execute(t, user_id=12) - - To invoke SQLAlchemy typing logic for bind parameters, the - ``bindparams`` list allows specification of :func:`bindparam` - constructs which specify the type for a given name:: - - t = text("SELECT id FROM users WHERE updated_at>:updated", - bindparams=[bindparam('updated', DateTime())] - ) - - Typing during result row processing is also an important concern. - Result column types - are specified using the ``typemap`` dictionary, where the keys - match the names of columns. These names are taken from what - the DBAPI returns as ``cursor.description``:: - - t = text("SELECT id, name FROM users", - typemap={ - 'id':Integer, - 'name':Unicode - } - ) - - The :func:`text` construct is used internally for most cases when - a literal string is specified for part of a larger query, such as - within :func:`select()`, :func:`update()`, - :func:`insert()` or :func:`delete()`. In those cases, the same - bind parameter syntax is applied:: - - s = select([users.c.id, users.c.name]).where("id=:user_id") - result = connection.execute(s, user_id=12) - - Using :func:`text` explicitly usually implies the construction - of a full, standalone statement. As such, SQLAlchemy refers - to it as an :class:`.Executable` object, and it supports - the :meth:`Executable.execution_options` method. For example, - a :func:`text` construct that should be subject to "autocommit" - can be set explicitly so using the ``autocommit`` option:: - - t = text("EXEC my_procedural_thing()").\\ - execution_options(autocommit=True) - - Note that SQLAlchemy's usual "autocommit" behavior applies to - :func:`text` constructs - that is, statements which begin - with a phrase such as ``INSERT``, ``UPDATE``, ``DELETE``, - or a variety of other phrases specific to certain backends, will - be eligible for autocommit if no transaction is in progress. - - :param text: - the text of the SQL statement to be created. use ``:`` - to specify bind parameters; they will be compiled to their - engine-specific format. - - :param autocommit: - Deprecated. Use .execution_options(autocommit=) - to set the autocommit option. - - :param bind: - an optional connection or engine to be used for this text query. - - :param bindparams: - a list of :func:`bindparam()` instances which can be used to define - the types and/or initial values for the bind parameters within - the textual statement; the keynames of the bindparams must match - those within the text of the statement. The types will be used - for pre-processing on bind values. - - :param typemap: - a dictionary mapping the names of columns represented in the - columns clause of a ``SELECT`` statement to type objects, - which will be used to perform post-processing on columns within - the result set. This argument applies to any expression - that returns result sets. - - """ - return _TextClause(text, bind=bind, *args, **kwargs) - -def over(func, partition_by=None, order_by=None): - """Produce an OVER clause against a function. - - Used against aggregate or so-called "window" functions, - for database backends that support window functions. - - E.g.:: - - from sqlalchemy import over - over(func.row_number(), order_by='x') - - Would produce "ROW_NUMBER() OVER(ORDER BY x)". - - :param func: a :class:`.FunctionElement` construct, typically - generated by :attr:`~.expression.func`. - :param partition_by: a column element or string, or a list - of such, that will be used as the PARTITION BY clause - of the OVER construct. - :param order_by: a column element or string, or a list - of such, that will be used as the ORDER BY clause - of the OVER construct. - - This function is also available from the :attr:`~.expression.func` - construct itself via the :meth:`.FunctionElement.over` method. - - .. versionadded:: 0.7 - - """ - return _Over(func, partition_by=partition_by, order_by=order_by) - -def null(): - """Return a :class:`_Null` object, which compiles to ``NULL``. - - """ - return _Null() - -def true(): - """Return a :class:`_True` object, which compiles to ``true``, or the - boolean equivalent for the target dialect. - - """ - return _True() - -def false(): - """Return a :class:`_False` object, which compiles to ``false``, or the - boolean equivalent for the target dialect. - - """ - return _False() - -class _FunctionGenerator(object): - """Generate :class:`.Function` objects based on getattr calls.""" - - def __init__(self, **opts): - self.__names = [] - self.opts = opts - - def __getattr__(self, name): - # passthru __ attributes; fixes pydoc - if name.startswith('__'): - try: - return self.__dict__[name] - except KeyError: - raise AttributeError(name) - - elif name.endswith('_'): - name = name[0:-1] - f = _FunctionGenerator(**self.opts) - f.__names = list(self.__names) + [name] - return f - - def __call__(self, *c, **kwargs): - o = self.opts.copy() - o.update(kwargs) - if len(self.__names) == 1: - func = getattr(functions, self.__names[-1].lower(), None) - if func is not None and \ - isinstance(func, type) and \ - issubclass(func, Function): - return func(*c, **o) - - return Function(self.__names[-1], - packagenames=self.__names[0:-1], *c, **o) - -# "func" global - i.e. func.count() -func = _FunctionGenerator() -"""Generate SQL function expressions. - - ``func`` is a special object instance which generates SQL functions based on name-based attributes, e.g.:: - - >>> print func.count(1) - count(:param_1) - - The element is a column-oriented SQL element like any other, and is - used in that way:: - - >>> print select([func.count(table.c.id)]) - SELECT count(sometable.id) FROM sometable - - Any name can be given to ``func``. If the function name is unknown to - SQLAlchemy, it will be rendered exactly as is. For common SQL functions - which SQLAlchemy is aware of, the name may be interpreted as a *generic - function* which will be compiled appropriately to the target database:: - - >>> print func.current_timestamp() - CURRENT_TIMESTAMP - - To call functions which are present in dot-separated packages, specify them in the same manner:: - - >>> print func.stats.yield_curve(5, 10) - stats.yield_curve(:yield_curve_1, :yield_curve_2) - - SQLAlchemy can be made aware of the return type of functions to enable - type-specific lexical and result-based behavior. For example, to ensure - that a string-based function returns a Unicode value and is similarly - treated as a string in expressions, specify - :class:`~sqlalchemy.types.Unicode` as the type: - - >>> print func.my_string(u'hi', type_=Unicode) + ' ' + \ - ... func.my_string(u'there', type_=Unicode) - my_string(:my_string_1) || :my_string_2 || my_string(:my_string_3) - - The object returned by a ``func`` call is an instance of :class:`.Function`. - This object meets the "column" interface, including comparison and labeling - functions. The object can also be passed the :meth:`~.Connectable.execute` - method of a :class:`.Connection` or :class:`.Engine`, where it will be - wrapped inside of a SELECT statement first:: - - print connection.execute(func.current_timestamp()).scalar() - - A function can also be "bound" to a :class:`.Engine` or :class:`.Connection` - using the ``bind`` keyword argument, providing an execute() as well - as a scalar() method:: - - myfunc = func.current_timestamp(bind=some_engine) - print myfunc.scalar() - - Functions which are interpreted as "generic" functions know how to - calculate their return type automatically. For a listing of known generic - functions, see :ref:`generic_functions`. - -""" - -# "modifier" global - i.e. modifier.distinct -# TODO: use UnaryExpression for this instead ? -modifier = _FunctionGenerator(group=False) - -class _truncated_label(unicode): - """A unicode subclass used to identify symbolic " - "names that may require truncation.""" - - def apply_map(self, map_): - return self - -# for backwards compatibility in case -# someone is re-implementing the -# _truncated_identifier() sequence in a custom -# compiler -_generated_label = _truncated_label - -class _anonymous_label(_truncated_label): - """A unicode subclass used to identify anonymously - generated names.""" - - def __add__(self, other): - return _anonymous_label( - unicode(self) + - unicode(other)) - - def __radd__(self, other): - return _anonymous_label( - unicode(other) + - unicode(self)) - - def apply_map(self, map_): - return self % map_ - -def _as_truncated(value): - """coerce the given value to :class:`._truncated_label`. - - Existing :class:`._truncated_label` and - :class:`._anonymous_label` objects are passed - unchanged. - """ - - if isinstance(value, _truncated_label): - return value - else: - return _truncated_label(value) - -def _string_or_unprintable(element): - if isinstance(element, basestring): - return element - else: - try: - return str(element) - except: - return "unprintable element %r" % element - -def _clone(element, **kw): - return element._clone() - -def _expand_cloned(elements): - """expand the given set of ClauseElements to be the set of all 'cloned' - predecessors. - - """ - return itertools.chain(*[x._cloned_set for x in elements]) - -def _select_iterables(elements): - """expand tables into individual columns in the - given list of column expressions. - - """ - return itertools.chain(*[c._select_iterable for c in elements]) - -def _cloned_intersection(a, b): - """return the intersection of sets a and b, counting - any overlap between 'cloned' predecessors. - - The returned set is in terms of the entities present within 'a'. - - """ - all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) - return set(elem for elem in a - if all_overlap.intersection(elem._cloned_set)) - - -def _is_literal(element): - return not isinstance(element, Visitable) and \ - not hasattr(element, '__clause_element__') - -def _from_objects(*elements): - return itertools.chain(*[element._from_objects for element in elements]) - -def _labeled(element): - if not hasattr(element, 'name'): - return element.label(None) - else: - return element - -def _column_as_key(element): - if isinstance(element, basestring): - return element - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - return element.key - -def _literal_as_text(element): - if isinstance(element, Visitable): - return element - elif hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif isinstance(element, basestring): - return _TextClause(unicode(element)) - elif isinstance(element, (util.NoneType, bool)): - return _const_expr(element) - else: - raise exc.ArgumentError( - "SQL expression object or string expected." - ) - -def _const_expr(element): - if element is None: - return null() - elif element is False: - return false() - elif element is True: - return true() - else: - raise exc.ArgumentError( - "Expected None, False, or True" - ) - -def _clause_element_as_expr(element): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - else: - return element - -def _literal_as_column(element): - if isinstance(element, Visitable): - return element - elif hasattr(element, '__clause_element__'): - return element.__clause_element__() - else: - return literal_column(str(element)) - -def _literal_as_binds(element, name=None, type_=None): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif not isinstance(element, Visitable): - if element is None: - return null() - else: - return _BindParamClause(name, element, type_=type_, unique=True) - else: - return element - -def _type_from_args(args): - for a in args: - if not isinstance(a.type, sqltypes.NullType): - return a.type - else: - return sqltypes.NullType - -def _no_literals(element): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif not isinstance(element, Visitable): - raise exc.ArgumentError("Ambiguous literal: %r. Use the 'text()' " - "function to indicate a SQL expression " - "literal, or 'literal()' to indicate a " - "bound value." % element) - else: - return element - -def _only_column_elements_or_none(element, name): - if element is None: - return None - else: - return _only_column_elements(element, name) - -def _only_column_elements(element, name): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, ColumnElement): - raise exc.ArgumentError( - "Column-based expression object expected for argument " - "'%s'; got: '%s', type %s" % (name, element, type(element))) - return element - -def _corresponding_column_or_error(fromclause, column, - require_embedded=False): - c = fromclause.corresponding_column(column, - require_embedded=require_embedded) - if c is None: - raise exc.InvalidRequestError( - "Given column '%s', attached to table '%s', " - "failed to locate a corresponding column from table '%s'" - % - (column, - getattr(column, 'table', None),fromclause.description) - ) - return c - -@util.decorator -def _generative(fn, *args, **kw): - """Mark a method as generative.""" - - self = args[0]._generate() - fn(self, *args[1:], **kw) - return self - - -def is_column(col): - """True if ``col`` is an instance of :class:`.ColumnElement`.""" - - return isinstance(col, ColumnElement) - - -class ClauseElement(Visitable): - """Base class for elements of a programmatically constructed SQL - expression. - - """ - __visit_name__ = 'clause' - - _annotations = {} - supports_execution = False - _from_objects = [] - bind = None - _is_clone_of = None - - def _clone(self): - """Create a shallow copy of this ClauseElement. - - This method may be used by a generative API. Its also used as - part of the "deep" copy afforded by a traversal that combines - the _copy_internals() method. - - """ - c = self.__class__.__new__(self.__class__) - c.__dict__ = self.__dict__.copy() - c.__dict__.pop('_cloned_set', None) - - # this is a marker that helps to "equate" clauses to each other - # when a Select returns its list of FROM clauses. the cloning - # process leaves around a lot of remnants of the previous clause - # typically in the form of column expressions still attached to the - # old table. - c._is_clone_of = self - - return c - - @property - def _constructor(self): - """return the 'constructor' for this ClauseElement. - - This is for the purposes for creating a new object of - this type. Usually, its just the element's __class__. - However, the "Annotated" version of the object overrides - to return the class of its proxied element. - - """ - return self.__class__ - - @util.memoized_property - def _cloned_set(self): - """Return the set consisting all cloned ancestors of this - ClauseElement. - - Includes this ClauseElement. This accessor tends to be used for - FromClause objects to identify 'equivalent' FROM clauses, regardless - of transformative operations. - - """ - s = util.column_set() - f = self - while f is not None: - s.add(f) - f = f._is_clone_of - return s - - def __getstate__(self): - d = self.__dict__.copy() - d.pop('_is_clone_of', None) - return d - - if util.jython: - def __hash__(self): - """Return a distinct hash code. - - ClauseElements may have special equality comparisons which - makes us rely on them having unique hash codes for use in - hash-based collections. Stock __hash__ doesn't guarantee - unique values on platforms with moving GCs. - """ - return id(self) - - def _annotate(self, values): - """return a copy of this ClauseElement with the given annotations - dictionary. - - """ - return sqlutil.Annotated(self, values) - - def _deannotate(self): - """return a copy of this ClauseElement with an empty annotations - dictionary. - - """ - return self._clone() - - def unique_params(self, *optionaldict, **kwargs): - """Return a copy with :func:`bindparam()` elements replaced. - - Same functionality as ``params()``, except adds `unique=True` - to affected bind parameters so that multiple statements can be - used. - - """ - return self._params(True, optionaldict, kwargs) - - def params(self, *optionaldict, **kwargs): - """Return a copy with :func:`bindparam()` elements replaced. - - Returns a copy of this ClauseElement with :func:`bindparam()` - elements replaced with values taken from the given dictionary:: - - >>> clause = column('x') + bindparam('foo') - >>> print clause.compile().params - {'foo':None} - >>> print clause.params({'foo':7}).compile().params - {'foo':7} - - """ - return self._params(False, optionaldict, kwargs) - - def _params(self, unique, optionaldict, kwargs): - if len(optionaldict) == 1: - kwargs.update(optionaldict[0]) - elif len(optionaldict) > 1: - raise exc.ArgumentError( - "params() takes zero or one positional dictionary argument") - - def visit_bindparam(bind): - if bind.key in kwargs: - bind.value = kwargs[bind.key] - if unique: - bind._convert_to_unique() - return cloned_traverse(self, {}, {'bindparam':visit_bindparam}) - - def compare(self, other, **kw): - """Compare this ClauseElement to the given ClauseElement. - - Subclasses should override the default behavior, which is a - straight identity comparison. - - \**kw are arguments consumed by subclass compare() methods and - may be used to modify the criteria for comparison. - (see :class:`.ColumnElement`) - - """ - return self is other - - def _copy_internals(self, clone=_clone, **kw): - """Reassign internal elements to be clones of themselves. - - Called during a copy-and-traverse operation on newly - shallow-copied elements to create a deep copy. - - The given clone function should be used, which may be applying - additional transformations to the element (i.e. replacement - traversal, cloned traversal, annotations). - - """ - pass - - def get_children(self, **kwargs): - """Return immediate child elements of this :class:`.ClauseElement`. - - This is used for visit traversal. - - \**kwargs may contain flags that change the collection that is - returned, for example to return a subset of items in order to - cut down on larger traversals, or to return child items from a - different context (such as schema-level collections instead of - clause-level). - - """ - return [] - - def self_group(self, against=None): - """Apply a 'grouping' to this :class:`.ClauseElement`. - - This method is overridden by subclasses to return a - "grouping" construct, i.e. parenthesis. In particular - it's used by "binary" expressions to provide a grouping - around themselves when placed into a larger expression, - as well as by :func:`.select` constructs when placed into - the FROM clause of another :func:`.select`. (Note that - subqueries should be normally created using the - :func:`.Select.alias` method, as many platforms require - nested SELECT statements to be named). - - As expressions are composed together, the application of - :meth:`self_group` is automatic - end-user code should never - need to use this method directly. Note that SQLAlchemy's - clause constructs take operator precedence into account - - so parenthesis might not be needed, for example, in - an expression like ``x OR (y AND z)`` - AND takes precedence - over OR. - - The base :meth:`self_group` method of :class:`.ClauseElement` - just returns self. - """ - return self - - - @util.deprecated('0.7', - 'Only SQL expressions which subclass ' - ':class:`.Executable` may provide the ' - ':func:`.execute` method.') - def execute(self, *multiparams, **params): - """Compile and execute this :class:`.ClauseElement`. - - """ - e = self.bind - if e is None: - label = getattr(self, 'description', self.__class__.__name__) - msg = ('This %s does not support direct execution.' % label) - raise exc.UnboundExecutionError(msg) - return e._execute_clauseelement(self, multiparams, params) - - @util.deprecated('0.7', - 'Only SQL expressions which subclass ' - ':class:`.Executable` may provide the ' - ':func:`.scalar` method.') - def scalar(self, *multiparams, **params): - """Compile and execute this :class:`.ClauseElement`, returning - the result's scalar representation. - - """ - return self.execute(*multiparams, **params).scalar() - - def compile(self, bind=None, dialect=None, **kw): - """Compile this SQL expression. - - The return value is a :class:`~sqlalchemy.engine.Compiled` object. - Calling ``str()`` or ``unicode()`` on the returned value will yield a - string representation of the result. The - :class:`~sqlalchemy.engine.Compiled` object also can return a - dictionary of bind parameter names and values - using the ``params`` accessor. - - :param bind: An ``Engine`` or ``Connection`` from which a - ``Compiled`` will be acquired. This argument takes precedence over - this :class:`.ClauseElement`'s bound engine, if any. - - :param column_keys: Used for INSERT and UPDATE statements, a list of - column names which should be present in the VALUES clause of the - compiled statement. If ``None``, all columns from the target table - object are rendered. - - :param dialect: A ``Dialect`` instance from which a ``Compiled`` - will be acquired. This argument takes precedence over the `bind` - argument as well as this :class:`.ClauseElement`'s bound engine, if - any. - - :param inline: Used for INSERT statements, for a dialect which does - not support inline retrieval of newly generated primary key - columns, will force the expression used to create the new primary - key value to be rendered inline within the INSERT statement's - VALUES clause. This typically refers to Sequence execution but may - also refer to any server-side default generation function - associated with a primary key `Column`. - - """ - - if not dialect: - if bind: - dialect = bind.dialect - elif self.bind: - dialect = self.bind.dialect - bind = self.bind - else: - dialect = default.DefaultDialect() - return self._compiler(dialect, bind=bind, **kw) - - def _compiler(self, dialect, **kw): - """Return a compiler appropriate for this ClauseElement, given a - Dialect.""" - - return dialect.statement_compiler(dialect, self, **kw) - - def __str__(self): - # Py3K - #return unicode(self.compile()) - # Py2K - return unicode(self.compile()).encode('ascii', 'backslashreplace') - # end Py2K - - def __and__(self, other): - return and_(self, other) - - def __or__(self, other): - return or_(self, other) - - def __invert__(self): - return self._negate() - - def __nonzero__(self): - raise TypeError("Boolean value of this clause is not defined") - - def _negate(self): - if hasattr(self, 'negation_clause'): - return self.negation_clause - else: - return _UnaryExpression( - self.self_group(against=operators.inv), - operator=operators.inv, - negate=None) - - def __repr__(self): - friendly = getattr(self, 'description', None) - if friendly is None: - return object.__repr__(self) - else: - return '<%s.%s at 0x%x; %s>' % ( - self.__module__, self.__class__.__name__, id(self), friendly) - - -class _Immutable(object): - """mark a ClauseElement as 'immutable' when expressions are cloned.""" - - def unique_params(self, *optionaldict, **kwargs): - raise NotImplementedError("Immutable objects do not support copying") - - def params(self, *optionaldict, **kwargs): - raise NotImplementedError("Immutable objects do not support copying") - - def _clone(self): - return self - - -class _CompareMixin(ColumnOperators): - """Defines comparison and math operations for :class:`.ClauseElement` - instances. - - See :class:`.ColumnOperators` and :class:`.Operators` for descriptions - of all operations. - - """ - - def __compare(self, op, obj, negate=None, reverse=False, - **kwargs - ): - if obj is None or isinstance(obj, _Null): - if op in (operators.eq, operators.is_): - return _BinaryExpression(self, null(), operators.is_, - negate=operators.isnot) - elif op in (operators.ne, operators.isnot): - return _BinaryExpression(self, null(), operators.isnot, - negate=operators.is_) - else: - raise exc.ArgumentError("Only '='/'!=' operators can " - "be used with NULL") - else: - obj = self._check_literal(op, obj) - - if reverse: - return _BinaryExpression(obj, - self, - op, - type_=sqltypes.BOOLEANTYPE, - negate=negate, modifiers=kwargs) - else: - return _BinaryExpression(self, - obj, - op, - type_=sqltypes.BOOLEANTYPE, - negate=negate, modifiers=kwargs) - - def __operate(self, op, obj, reverse=False): - obj = self._check_literal(op, obj) - - if reverse: - left, right = obj, self - else: - left, right = self, obj - - if left.type is None: - op, result_type = sqltypes.NULLTYPE._adapt_expression(op, - right.type) - elif right.type is None: - op, result_type = left.type._adapt_expression(op, - sqltypes.NULLTYPE) - else: - op, result_type = left.type._adapt_expression(op, - right.type) - return _BinaryExpression(left, right, op, type_=result_type) - - - # a mapping of operators with the method they use, along with their negated - # operator for comparison operators - operators = { - operators.add : (__operate,), - operators.mul : (__operate,), - operators.sub : (__operate,), - # Py2K - operators.div : (__operate,), - # end Py2K - operators.mod : (__operate,), - operators.truediv : (__operate,), - operators.lt : (__compare, operators.ge), - operators.le : (__compare, operators.gt), - operators.ne : (__compare, operators.eq), - operators.gt : (__compare, operators.le), - operators.ge : (__compare, operators.lt), - operators.eq : (__compare, operators.ne), - operators.like_op : (__compare, operators.notlike_op), - operators.ilike_op : (__compare, operators.notilike_op), - operators.is_ : (__compare, operators.is_), - operators.isnot : (__compare, operators.isnot), - } - - def operate(self, op, *other, **kwargs): - o = _CompareMixin.operators[op] - return o[0](self, op, other[0], *o[1:], **kwargs) - - def reverse_operate(self, op, other, **kwargs): - o = _CompareMixin.operators[op] - return o[0](self, op, other, reverse=True, *o[1:], **kwargs) - - def in_(self, other): - """See :meth:`.ColumnOperators.in_`.""" - return self._in_impl(operators.in_op, operators.notin_op, other) - - def _in_impl(self, op, negate_op, seq_or_selectable): - seq_or_selectable = _clause_element_as_expr(seq_or_selectable) - - if isinstance(seq_or_selectable, _ScalarSelect): - return self.__compare(op, seq_or_selectable, - negate=negate_op) - elif isinstance(seq_or_selectable, _SelectBase): - - # TODO: if we ever want to support (x, y, z) IN (select x, - # y, z from table), we would need a multi-column version of - # as_scalar() to produce a multi- column selectable that - # does not export itself as a FROM clause - - return self.__compare(op, seq_or_selectable.as_scalar(), - negate=negate_op) - elif isinstance(seq_or_selectable, (Selectable, _TextClause)): - return self.__compare(op, seq_or_selectable, - negate=negate_op) - - - # Handle non selectable arguments as sequences - - args = [] - for o in seq_or_selectable: - if not _is_literal(o): - if not isinstance(o, _CompareMixin): - raise exc.InvalidRequestError('in() function accept' - 's either a list of non-selectable values, ' - 'or a selectable: %r' % o) - else: - o = self._bind_param(op, o) - args.append(o) - if len(args) == 0: - - # Special case handling for empty IN's, behave like - # comparison against zero row selectable. We use != to - # build the contradiction as it handles NULL values - # appropriately, i.e. "not (x IN ())" should not return NULL - # values for x. - - util.warn('The IN-predicate on "%s" was invoked with an ' - 'empty sequence. This results in a ' - 'contradiction, which nonetheless can be ' - 'expensive to evaluate. Consider alternative ' - 'strategies for improved performance.' % self) - return self != self - - return self.__compare(op, - ClauseList(*args).self_group(against=op), - negate=negate_op) - - def __neg__(self): - """See :meth:`.ColumnOperators.__neg__`.""" - return _UnaryExpression(self, operator=operators.neg) - - def startswith(self, other, escape=None): - """See :meth:`.ColumnOperators.startswith`.""" - # use __radd__ to force string concat behavior - return self.__compare( - operators.like_op, - literal_column("'%'", type_=sqltypes.String).__radd__( - self._check_literal(operators.like_op, other) - ), - escape=escape) - - def endswith(self, other, escape=None): - """See :meth:`.ColumnOperators.endswith`.""" - return self.__compare( - operators.like_op, - literal_column("'%'", type_=sqltypes.String) + - self._check_literal(operators.like_op, other), - escape=escape) - - def contains(self, other, escape=None): - """See :meth:`.ColumnOperators.contains`.""" - return self.__compare( - operators.like_op, - literal_column("'%'", type_=sqltypes.String) + - self._check_literal(operators.like_op, other) + - literal_column("'%'", type_=sqltypes.String), - escape=escape) - - def match(self, other): - """See :meth:`.ColumnOperators.match`.""" - return self.__compare(operators.match_op, - self._check_literal(operators.match_op, - other)) - - def label(self, name): - """Produce a column label, i.e. `` AS ``. - - This is a shortcut to the :func:`~.expression.label` function. - - if 'name' is None, an anonymous label name will be generated. - - """ - return _Label(name, self, self.type) - - def desc(self): - """See :meth:`.ColumnOperators.desc`.""" - return desc(self) - - def asc(self): - """See :meth:`.ColumnOperators.asc`.""" - return asc(self) - - def nullsfirst(self): - """See :meth:`.ColumnOperators.nullsfirst`.""" - return nullsfirst(self) - - def nullslast(self): - """See :meth:`.ColumnOperators.nullslast`.""" - return nullslast(self) - - def distinct(self): - """See :meth:`.ColumnOperators.distinct`.""" - return _UnaryExpression(self, operator=operators.distinct_op, - type_=self.type) - - def between(self, cleft, cright): - """See :meth:`.ColumnOperators.between`.""" - return _BinaryExpression( - self, - ClauseList( - self._check_literal(operators.and_, cleft), - self._check_literal(operators.and_, cright), - operator=operators.and_, - group=False), - operators.between_op) - - def collate(self, collation): - """See :meth:`.ColumnOperators.collate`.""" - - return collate(self, collation) - - def op(self, operator): - """See :meth:`.ColumnOperators.op`.""" - - return lambda other: self.__operate(operator, other) - - def _bind_param(self, operator, obj): - return _BindParamClause(None, obj, - _compared_to_operator=operator, - _compared_to_type=self.type, unique=True) - - def _check_literal(self, operator, other): - if isinstance(other, _BindParamClause) and \ - isinstance(other.type, sqltypes.NullType): - # TODO: perhaps we should not mutate the incoming bindparam() - # here and instead make a copy of it. this might - # be the only place that we're mutating an incoming construct. - other.type = self.type - return other - elif hasattr(other, '__clause_element__'): - other = other.__clause_element__() - if isinstance(other, (_SelectBase, Alias)): - other = other.as_scalar() - return other - elif not isinstance(other, ClauseElement): - return self._bind_param(operator, other) - elif isinstance(other, (_SelectBase, Alias)): - return other.as_scalar() - else: - return other - - -class ColumnElement(ClauseElement, _CompareMixin): - """Represent an element that is usable within the "column clause" portion - of a ``SELECT`` statement. - - This includes columns associated with tables, aliases, and - subqueries, expressions, function calls, SQL keywords such as - ``NULL``, literals, etc. :class:`.ColumnElement` is the ultimate base - class for all such elements. - - :class:`.ColumnElement` supports the ability to be a *proxy* element, - which indicates that the :class:`.ColumnElement` may be associated with - a :class:`.Selectable` which was derived from another :class:`.Selectable`. - An example of a "derived" :class:`.Selectable` is an :class:`.Alias` of a - :class:`~sqlalchemy.schema.Table`. - - A :class:`.ColumnElement`, by subclassing the :class:`_CompareMixin` mixin - class, provides the ability to generate new :class:`.ClauseElement` - objects using Python expressions. See the :class:`_CompareMixin` - docstring for more details. - - """ - - __visit_name__ = 'column' - primary_key = False - foreign_keys = [] - quote = None - _label = None - _key_label = None - _alt_names = () - - @property - def _select_iterable(self): - return (self, ) - - @util.memoized_property - def base_columns(self): - return util.column_set(c for c in self.proxy_set - if not hasattr(c, 'proxies')) - - @util.memoized_property - def proxy_set(self): - s = util.column_set([self]) - if hasattr(self, 'proxies'): - for c in self.proxies: - s.update(c.proxy_set) - return s - - def shares_lineage(self, othercolumn): - """Return True if the given :class:`.ColumnElement` - has a common ancestor to this :class:`.ColumnElement`.""" - - return bool(self.proxy_set.intersection(othercolumn.proxy_set)) - - def _compare_name_for_result(self, other): - """Return True if the given column element compares to this one - when targeting within a result row.""" - - return hasattr(other, 'name') and hasattr(self, 'name') and \ - other.name == self.name - - def _make_proxy(self, selectable, name=None): - """Create a new :class:`.ColumnElement` representing this - :class:`.ColumnElement` as it appears in the select list of a - descending selectable. - - """ - if name is None: - name = self.anon_label - # TODO: may want to change this to anon_label, - # or some value that is more useful than the - # compiled form of the expression - key = str(self) - else: - key = name - - co = ColumnClause(_as_truncated(name), - selectable, - type_=getattr(self, - 'type', None)) - co.proxies = [self] - if selectable._is_clone_of is not None: - co._is_clone_of = \ - selectable._is_clone_of.columns.get(key) - selectable._columns[key] = co - return co - - def compare(self, other, use_proxies=False, equivalents=None, **kw): - """Compare this ColumnElement to another. - - Special arguments understood: - - :param use_proxies: when True, consider two columns that - share a common base column as equivalent (i.e. shares_lineage()) - - :param equivalents: a dictionary of columns as keys mapped to sets - of columns. If the given "other" column is present in this - dictionary, if any of the columns in the corresponding set() pass the - comparison test, the result is True. This is used to expand the - comparison to other columns that may be known to be equivalent to - this one via foreign key or other criterion. - - """ - to_compare = (other, ) - if equivalents and other in equivalents: - to_compare = equivalents[other].union(to_compare) - - for oth in to_compare: - if use_proxies and self.shares_lineage(oth): - return True - elif oth is self: - return True - else: - return False - - @util.memoized_property - def anon_label(self): - """provides a constant 'anonymous label' for this ColumnElement. - - This is a label() expression which will be named at compile time. - The same label() is returned each time anon_label is called so - that expressions can reference anon_label multiple times, producing - the same label name at compile time. - - the compiler uses this function automatically at compile time - for expressions that are known to be 'unnamed' like binary - expressions and function calls. - - """ - return _anonymous_label('%%(%d %s)s' % (id(self), getattr(self, - 'name', 'anon'))) - -class ColumnCollection(util.OrderedProperties): - """An ordered dictionary that stores a list of ColumnElement - instances. - - Overrides the ``__eq__()`` method to produce SQL clauses between - sets of correlated columns. - - """ - - def __init__(self, *cols): - super(ColumnCollection, self).__init__() - self._data.update((c.key, c) for c in cols) - self.__dict__['_all_cols'] = util.column_set(self) - - def __str__(self): - return repr([str(c) for c in self]) - - def replace(self, column): - """add the given column to this collection, removing unaliased - versions of this column as well as existing columns with the - same key. - - e.g.:: - - t = Table('sometable', metadata, Column('col1', Integer)) - t.columns.replace(Column('col1', Integer, key='columnone')) - - will remove the original 'col1' from the collection, and add - the new column under the name 'columnname'. - - Used by schema.Column to override columns during table reflection. - - """ - if column.name in self and column.key != column.name: - other = self[column.name] - if other.name == other.key: - del self._data[other.name] - self._all_cols.remove(other) - if column.key in self._data: - self._all_cols.remove(self._data[column.key]) - self._all_cols.add(column) - self._data[column.key] = column - - def add(self, column): - """Add a column to this collection. - - The key attribute of the column will be used as the hash key - for this dictionary. - - """ - self[column.key] = column - - def __delitem__(self, key): - raise NotImplementedError() - - def __setattr__(self, key, object): - raise NotImplementedError() - - def __setitem__(self, key, value): - if key in self: - - # this warning is primarily to catch select() statements - # which have conflicting column names in their exported - # columns collection - - existing = self[key] - if not existing.shares_lineage(value): - util.warn('Column %r on table %r being replaced by ' - 'another column with the same key. Consider ' - 'use_labels for select() statements.' % (key, - getattr(existing, 'table', None))) - self._all_cols.remove(existing) - # pop out memoized proxy_set as this - # operation may very well be occurring - # in a _make_proxy operation - value.__dict__.pop('proxy_set', None) - self._all_cols.add(value) - self._data[key] = value - - def clear(self): - self._data.clear() - self._all_cols.clear() - - def remove(self, column): - del self._data[column.key] - self._all_cols.remove(column) - - def update(self, value): - self._data.update(value) - self._all_cols.clear() - self._all_cols.update(self._data.values()) - - def extend(self, iter): - self.update((c.key, c) for c in iter) - - __hash__ = None - - def __eq__(self, other): - l = [] - for c in other: - for local in self: - if c.shares_lineage(local): - l.append(c==local) - return and_(*l) - - def __contains__(self, other): - if not isinstance(other, basestring): - raise exc.ArgumentError("__contains__ requires a string argument") - return util.OrderedProperties.__contains__(self, other) - - def __setstate__(self, state): - self.__dict__['_data'] = state['_data'] - self.__dict__['_all_cols'] = util.column_set(self._data.values()) - - def contains_column(self, col): - # this has to be done via set() membership - return col in self._all_cols - - def as_immutable(self): - return ImmutableColumnCollection(self._data, self._all_cols) - -class ImmutableColumnCollection(util.ImmutableProperties, ColumnCollection): - def __init__(self, data, colset): - util.ImmutableProperties.__init__(self, data) - self.__dict__['_all_cols'] = colset - - extend = remove = util.ImmutableProperties._immutable - - -class ColumnSet(util.ordered_column_set): - def contains_column(self, col): - return col in self - - def extend(self, cols): - for col in cols: - self.add(col) - - def __add__(self, other): - return list(self) + list(other) - - def __eq__(self, other): - l = [] - for c in other: - for local in self: - if c.shares_lineage(local): - l.append(c==local) - return and_(*l) - - def __hash__(self): - return hash(tuple(x for x in self)) - -class Selectable(ClauseElement): - """mark a class as being selectable""" - __visit_name__ = 'selectable' - -class FromClause(Selectable): - """Represent an element that can be used within the ``FROM`` - clause of a ``SELECT`` statement. - - """ - __visit_name__ = 'fromclause' - named_with_column = False - _hide_froms = [] - quote = None - schema = None - _memoized_property = util.group_expirable_memoized_property(["_columns"]) - - def count(self, whereclause=None, **params): - """return a SELECT COUNT generated against this - :class:`.FromClause`.""" - - if self.primary_key: - col = list(self.primary_key)[0] - else: - col = list(self.columns)[0] - return select( - [func.count(col).label('tbl_row_count')], - whereclause, - from_obj=[self], - **params) - - def select(self, whereclause=None, **params): - """return a SELECT of this :class:`.FromClause`.""" - - return select([self], whereclause, **params) - - def join(self, right, onclause=None, isouter=False): - """return a join of this :class:`.FromClause` against another - :class:`.FromClause`.""" - - return Join(self, right, onclause, isouter) - - def outerjoin(self, right, onclause=None): - """return an outer join of this :class:`.FromClause` against another - :class:`.FromClause`.""" - - return Join(self, right, onclause, True) - - def alias(self, name=None): - """return an alias of this :class:`.FromClause`. - - This is shorthand for calling:: - - from sqlalchemy import alias - a = alias(self, name=name) - - See :func:`~.expression.alias` for details. - - """ - - return Alias(self, name) - - def is_derived_from(self, fromclause): - """Return True if this FromClause is 'derived' from the given - FromClause. - - An example would be an Alias of a Table is derived from that Table. - - """ - # this is essentially an "identity" check in the base class. - # Other constructs override this to traverse through - # contained elements. - return fromclause in self._cloned_set - - def _is_lexical_equivalent(self, other): - """Return True if this FromClause and the other represent - the same lexical identity. - - This tests if either one is a copy of the other, or - if they are the same via annotation identity. - - """ - return self._cloned_set.intersection(other._cloned_set) - - def replace_selectable(self, old, alias): - """replace all occurrences of FromClause 'old' with the given Alias - object, returning a copy of this :class:`.FromClause`. - - """ - - return sqlutil.ClauseAdapter(alias).traverse(self) - - def correspond_on_equivalents(self, column, equivalents): - """Return corresponding_column for the given column, or if None - search for a match in the given dictionary. - - """ - col = self.corresponding_column(column, require_embedded=True) - if col is None and col in equivalents: - for equiv in equivalents[col]: - nc = self.corresponding_column(equiv, require_embedded=True) - if nc: - return nc - return col - - def corresponding_column(self, column, require_embedded=False): - """Given a :class:`.ColumnElement`, return the exported - :class:`.ColumnElement` object from this :class:`.Selectable` - which corresponds to that original - :class:`~sqlalchemy.schema.Column` via a common ancestor - column. - - :param column: the target :class:`.ColumnElement` to be matched - - :param require_embedded: only return corresponding columns for - the given :class:`.ColumnElement`, if the given - :class:`.ColumnElement` is actually present within a sub-element - of this :class:`.FromClause`. Normally the column will match if - it merely shares a common ancestor with one of the exported - columns of this :class:`.FromClause`. - - """ - - def embedded(expanded_proxy_set, target_set): - for t in target_set.difference(expanded_proxy_set): - if not set(_expand_cloned([t]) - ).intersection(expanded_proxy_set): - return False - return True - - # don't dig around if the column is locally present - if self.c.contains_column(column): - return column - col, intersect = None, None - target_set = column.proxy_set - cols = self.c - for c in cols: - expanded_proxy_set = set(_expand_cloned(c.proxy_set)) - i = target_set.intersection(expanded_proxy_set) - if i and (not require_embedded - or embedded(expanded_proxy_set, target_set)): - if col is None: - - # no corresponding column yet, pick this one. - - col, intersect = c, i - elif len(i) > len(intersect): - - # 'c' has a larger field of correspondence than - # 'col'. i.e. selectable.c.a1_x->a1.c.x->table.c.x - # matches a1.c.x->table.c.x better than - # selectable.c.x->table.c.x does. - - col, intersect = c, i - elif i == intersect: - - # they have the same field of correspondence. see - # which proxy_set has fewer columns in it, which - # indicates a closer relationship with the root - # column. Also take into account the "weight" - # attribute which CompoundSelect() uses to give - # higher precedence to columns based on vertical - # position in the compound statement, and discard - # columns that have no reference to the target - # column (also occurs with CompoundSelect) - - col_distance = util.reduce(operator.add, - [sc._annotations.get('weight', 1) for sc in - col.proxy_set if sc.shares_lineage(column)]) - c_distance = util.reduce(operator.add, - [sc._annotations.get('weight', 1) for sc in - c.proxy_set if sc.shares_lineage(column)]) - if c_distance < col_distance: - col, intersect = c, i - return col - - @property - def description(self): - """a brief description of this FromClause. - - Used primarily for error message formatting. - - """ - return getattr(self, 'name', self.__class__.__name__ + " object") - - def _reset_exported(self): - """delete memoized collections when a FromClause is cloned.""" - - self._memoized_property.expire_instance(self) - - @_memoized_property - def columns(self): - """Return the collection of Column objects contained by this - FromClause.""" - - if '_columns' not in self.__dict__: - self._init_collections() - self._populate_column_collection() - return self._columns.as_immutable() - - @_memoized_property - def primary_key(self): - """Return the collection of Column objects which comprise the - primary key of this FromClause.""" - - self._init_collections() - self._populate_column_collection() - return self.primary_key - - @_memoized_property - def foreign_keys(self): - """Return the collection of ForeignKey objects which this - FromClause references.""" - - self._init_collections() - self._populate_column_collection() - return self.foreign_keys - - c = property(attrgetter('columns')) - _select_iterable = property(attrgetter('columns')) - - def _init_collections(self): - assert '_columns' not in self.__dict__ - assert 'primary_key' not in self.__dict__ - assert 'foreign_keys' not in self.__dict__ - - self._columns = ColumnCollection() - self.primary_key = ColumnSet() - self.foreign_keys = set() - - def _populate_column_collection(self): - pass - -class _BindParamClause(ColumnElement): - """Represent a bind parameter. - - Public constructor is the :func:`bindparam()` function. - - """ - - __visit_name__ = 'bindparam' - quote = None - - def __init__(self, key, value, type_=None, unique=False, - callable_=None, - isoutparam=False, required=False, - _compared_to_operator=None, - _compared_to_type=None): - """Construct a _BindParamClause. - - :param key: - the key for this bind param. Will be used in the generated - SQL statement for dialects that use named parameters. This - value may be modified when part of a compilation operation, - if other :class:`_BindParamClause` objects exist with the same - key, or if its length is too long and truncation is - required. - - :param value: - Initial value for this bind param. This value may be - overridden by the dictionary of parameters sent to statement - compilation/execution. - - :param callable\_: - A callable function that takes the place of "value". The function - will be called at statement execution time to determine the - ultimate value. Used for scenarios where the actual bind - value cannot be determined at the point at which the clause - construct is created, but embedded bind values are still desirable. - - :param type\_: - A ``TypeEngine`` object that will be used to pre-process the - value corresponding to this :class:`_BindParamClause` at - execution time. - - :param unique: - if True, the key name of this BindParamClause will be - modified if another :class:`_BindParamClause` of the same name - already has been located within the containing - :class:`.ClauseElement`. - - :param required: - a value is required at execution time. - - :param isoutparam: - if True, the parameter should be treated like a stored procedure - "OUT" parameter. - - """ - if unique: - self.key = _anonymous_label('%%(%d %s)s' % (id(self), key - or 'param')) - else: - self.key = key or _anonymous_label('%%(%d param)s' - % id(self)) - - # identifying key that won't change across - # clones, used to identify the bind's logical - # identity - self._identifying_key = self.key - - # key that was passed in the first place, used to - # generate new keys - self._orig_key = key or 'param' - - self.unique = unique - self.value = value - self.callable = callable_ - self.isoutparam = isoutparam - self.required = required - if type_ is None: - if _compared_to_type is not None: - self.type = \ - _compared_to_type._coerce_compared_value( - _compared_to_operator, value) - else: - self.type = sqltypes._type_map.get(type(value), - sqltypes.NULLTYPE) - elif isinstance(type_, type): - self.type = type_() - else: - self.type = type_ - - @property - def effective_value(self): - """Return the value of this bound parameter, - taking into account if the ``callable`` parameter - was set. - - The ``callable`` value will be evaluated - and returned if present, else ``value``. - - """ - if self.callable: - return self.callable() - else: - return self.value - - def _clone(self): - c = ClauseElement._clone(self) - if self.unique: - c.key = _anonymous_label('%%(%d %s)s' % (id(c), c._orig_key - or 'param')) - return c - - def _convert_to_unique(self): - if not self.unique: - self.unique = True - self.key = _anonymous_label('%%(%d %s)s' % (id(self), - self._orig_key or 'param')) - - def compare(self, other, **kw): - """Compare this :class:`_BindParamClause` to the given - clause.""" - - return isinstance(other, _BindParamClause) \ - and self.type._compare_type_affinity(other.type) \ - and self.value == other.value - - def __getstate__(self): - """execute a deferred value for serialization purposes.""" - - d = self.__dict__.copy() - v = self.value - if self.callable: - v = self.callable() - d['callable'] = None - d['value'] = v - return d - - def __repr__(self): - return '_BindParamClause(%r, %r, type_=%r)' % (self.key, - self.value, self.type) - -class _TypeClause(ClauseElement): - """Handle a type keyword in a SQL statement. - - Used by the ``Case`` statement. - - """ - - __visit_name__ = 'typeclause' - - def __init__(self, type): - self.type = type - - -class _Generative(object): - """Allow a ClauseElement to generate itself via the - @_generative decorator. - - """ - - def _generate(self): - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - return s - - -class Executable(_Generative): - """Mark a ClauseElement as supporting execution. - - :class:`.Executable` is a superclass for all "statement" types - of objects, including :func:`select`, :func:`delete`, :func:`update`, - :func:`insert`, :func:`text`. - - """ - - supports_execution = True - _execution_options = util.immutabledict() - _bind = None - - @_generative - def execution_options(self, **kw): - """ Set non-SQL options for the statement which take effect during - execution. - - Execution options can be set on a per-statement or - per :class:`.Connection` basis. Additionally, the - :class:`.Engine` and ORM :class:`~.orm.query.Query` objects provide access - to execution options which they in turn configure upon connections. - - The :meth:`execution_options` method is generative. A new - instance of this statement is returned that contains the options:: - - statement = select([table.c.x, table.c.y]) - statement = statement.execution_options(autocommit=True) - - Note that only a subset of possible execution options can be applied - to a statement - these include "autocommit" and "stream_results", - but not "isolation_level" or "compiled_cache". - See :meth:`.Connection.execution_options` for a full list of - possible options. - - See also: - - :meth:`.Connection.execution_options()` - - :meth:`.Query.execution_options()` - - """ - if 'isolation_level' in kw: - raise exc.ArgumentError( - "'isolation_level' execution option may only be specified " - "on Connection.execution_options(), or " - "per-engine using the isolation_level " - "argument to create_engine()." - ) - if 'compiled_cache' in kw: - raise exc.ArgumentError( - "'compiled_cache' execution option may only be specified " - "on Connection.execution_options(), not per statement." - ) - self._execution_options = self._execution_options.union(kw) - - def execute(self, *multiparams, **params): - """Compile and execute this :class:`.Executable`.""" - - e = self.bind - if e is None: - label = getattr(self, 'description', self.__class__.__name__) - msg = ('This %s is not directly bound to a Connection or Engine.' - 'Use the .execute() method of a Connection or Engine ' - 'to execute this construct.' % label) - raise exc.UnboundExecutionError(msg) - return e._execute_clauseelement(self, multiparams, params) - - def scalar(self, *multiparams, **params): - """Compile and execute this :class:`.Executable`, returning the - result's scalar representation. - - """ - return self.execute(*multiparams, **params).scalar() - - @property - def bind(self): - """Returns the :class:`.Engine` or :class:`.Connection` to - which this :class:`.Executable` is bound, or None if none found. - - This is a traversal which checks locally, then - checks among the "from" clauses of associated objects - until a bound engine or connection is found. - - """ - if self._bind is not None: - return self._bind - - for f in _from_objects(self): - if f is self: - continue - engine = f.bind - if engine is not None: - return engine - else: - return None - - -# legacy, some outside users may be calling this + 'or_', 'outparam', 'outerjoin', 'over', 'select', 'subquery', + 'table', 'text', + 'tuple_', 'type_coerce', 'union', 'union_all', 'update'] + + +from .visitors import Visitable +from .functions import func, modifier, FunctionElement +from ..util.langhelpers import public_factory +from .elements import ClauseElement, ColumnElement,\ + BindParameter, UnaryExpression, BooleanClauseList, \ + Label, Cast, Case, ColumnClause, TextClause, Over, Null, \ + True_, False_, BinaryExpression, Tuple, TypeClause, Extract, \ + Grouping, not_, \ + collate, literal_column, between,\ + literal, outparam, type_coerce, ClauseList + +from .elements import SavepointClause, RollbackToSavepointClause, \ + ReleaseSavepointClause + +from .base import ColumnCollection, Generative, Executable, \ + PARSE_AUTOCOMMIT + +from .selectable import Alias, Join, Select, Selectable, TableClause, \ + CompoundSelect, CTE, FromClause, FromGrouping, SelectBase, \ + alias, GenerativeSelect, \ + subquery, HasPrefixes, Exists, ScalarSelect, TextAsFrom + + +from .dml import Insert, Update, Delete, UpdateBase, ValuesBase + +# factory functions - these pull class-bound constructors and classmethods +# from SQL elements and selectables into public functions. This allows +# the functions to be available in the sqlalchemy.sql.* namespace and +# to be auto-cross-documenting from the function to the class itself. + +and_ = public_factory(BooleanClauseList.and_, ".expression.and_") +or_ = public_factory(BooleanClauseList.or_, ".expression.or_") +bindparam = public_factory(BindParameter, ".expression.bindparam") +select = public_factory(Select, ".expression.select") +text = public_factory(TextClause._create_text, ".expression.text") +table = public_factory(TableClause, ".expression.table") +column = public_factory(ColumnClause, ".expression.column") +over = public_factory(Over, ".expression.over") +label = public_factory(Label, ".expression.label") +case = public_factory(Case, ".expression.case") +cast = public_factory(Cast, ".expression.cast") +extract = public_factory(Extract, ".expression.extract") +tuple_ = public_factory(Tuple, ".expression.tuple_") +except_ = public_factory(CompoundSelect._create_except, ".expression.except_") +except_all = public_factory(CompoundSelect._create_except_all, ".expression.except_all") +intersect = public_factory(CompoundSelect._create_intersect, ".expression.intersect") +intersect_all = public_factory(CompoundSelect._create_intersect_all, ".expression.intersect_all") +union = public_factory(CompoundSelect._create_union, ".expression.union") +union_all = public_factory(CompoundSelect._create_union_all, ".expression.union_all") +exists = public_factory(Exists, ".expression.exists") +nullsfirst = public_factory(UnaryExpression._create_nullsfirst, ".expression.nullsfirst") +nullslast = public_factory(UnaryExpression._create_nullslast, ".expression.nullslast") +asc = public_factory(UnaryExpression._create_asc, ".expression.asc") +desc = public_factory(UnaryExpression._create_desc, ".expression.desc") +distinct = public_factory(UnaryExpression._create_distinct, ".expression.distinct") +true = public_factory(True_._singleton, ".expression.true") +false = public_factory(False_._singleton, ".expression.false") +null = public_factory(Null._singleton, ".expression.null") +join = public_factory(Join._create_join, ".expression.join") +outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin") +insert = public_factory(Insert, ".expression.insert") +update = public_factory(Update, ".expression.update") +delete = public_factory(Delete, ".expression.delete") + + +# internal functions still being called from tests and the ORM, +# these might be better off in some other namespace +from .base import _from_objects +from .elements import _literal_as_text, _clause_element_as_expr,\ + _is_column, _labeled, _only_column_elements, _string_or_unprintable, \ + _truncated_label, _clone, _cloned_difference, _cloned_intersection,\ + _column_as_key, _literal_as_binds, _select_iterables, \ + _corresponding_column_or_error +from .selectable import _interpret_as_from + + + +# old names for compatibility _Executable = Executable - -class _TextClause(Executable, ClauseElement): - """Represent a literal SQL text fragment. - - Public constructor is the :func:`text()` function. - - """ - - __visit_name__ = 'textclause' - - _bind_params_regex = re.compile(r'(? RIGHT``.""" - - __visit_name__ = 'binary' - - def __init__(self, left, right, operator, type_=None, - negate=None, modifiers=None): - self.left = _literal_as_text(left).self_group(against=operator) - self.right = _literal_as_text(right).self_group(against=operator) - self.operator = operator - self.type = sqltypes.to_instance(type_) - self.negate = negate - if modifiers is None: - self.modifiers = {} - else: - self.modifiers = modifiers - - def __nonzero__(self): - try: - return self.operator(hash(self.left), hash(self.right)) - except: - raise TypeError("Boolean value of this clause is not defined") - - @property - def _from_objects(self): - return self.left._from_objects + self.right._from_objects - - def _copy_internals(self, clone=_clone, **kw): - self.left = clone(self.left, **kw) - self.right = clone(self.right, **kw) - - def get_children(self, **kwargs): - return self.left, self.right - - def compare(self, other, **kw): - """Compare this :class:`_BinaryExpression` against the - given :class:`_BinaryExpression`.""" - - return ( - isinstance(other, _BinaryExpression) and - self.operator == other.operator and - ( - self.left.compare(other.left, **kw) and - self.right.compare(other.right, **kw) or - ( - operators.is_commutative(self.operator) and - self.left.compare(other.right, **kw) and - self.right.compare(other.left, **kw) - ) - ) - ) - - def self_group(self, against=None): - if operators.is_precedent(self.operator, against): - return _Grouping(self) - else: - return self - - def _negate(self): - if self.negate is not None: - return _BinaryExpression( - self.left, - self.right, - self.negate, - negate=self.operator, - type_=sqltypes.BOOLEANTYPE, - modifiers=self.modifiers) - else: - return super(_BinaryExpression, self)._negate() - -class _Exists(_UnaryExpression): - __visit_name__ = _UnaryExpression.__visit_name__ - _from_objects = [] - - def __init__(self, *args, **kwargs): - if args and isinstance(args[0], (_SelectBase, _ScalarSelect)): - s = args[0] - else: - if not args: - args = ([literal_column('*')],) - s = select(*args, **kwargs).as_scalar().self_group() - - _UnaryExpression.__init__(self, s, operator=operators.exists, - type_=sqltypes.Boolean) - - def select(self, whereclause=None, **params): - return select([self], whereclause, **params) - - def correlate(self, fromclause): - e = self._clone() - e.element = self.element.correlate(fromclause).self_group() - return e - - def select_from(self, clause): - """return a new :class:`._Exists` construct, applying the given expression - to the :meth:`.Select.select_from` method of the select statement - contained. - - """ - e = self._clone() - e.element = self.element.select_from(clause).self_group() - return e - - def where(self, clause): - """return a new exists() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - e = self._clone() - e.element = self.element.where(clause).self_group() - return e - -class Join(FromClause): - """represent a ``JOIN`` construct between two :class:`.FromClause` - elements. - - The public constructor function for :class:`.Join` is the module-level - :func:`join()` function, as well as the :func:`join()` method available - off all :class:`.FromClause` subclasses. - - """ - __visit_name__ = 'join' - - def __init__(self, left, right, onclause=None, isouter=False): - """Construct a new :class:`.Join`. - - The usual entrypoint here is the :func:`~.expression.join` - function or the :meth:`.FromClause.join` method of any - :class:`.FromClause` object. - - """ - self.left = _literal_as_text(left) - self.right = _literal_as_text(right).self_group() - - if onclause is None: - self.onclause = self._match_primaries(self.left, self.right) - else: - self.onclause = onclause - - self.isouter = isouter - self.__folded_equivalents = None - - @property - def description(self): - return "Join object on %s(%d) and %s(%d)" % ( - self.left.description, - id(self.left), - self.right.description, - id(self.right)) - - def is_derived_from(self, fromclause): - return fromclause is self or \ - self.left.is_derived_from(fromclause) or\ - self.right.is_derived_from(fromclause) - - def self_group(self, against=None): - return _FromGrouping(self) - - def _populate_column_collection(self): - columns = [c for c in self.left.columns] + \ - [c for c in self.right.columns] - - self.primary_key.extend(sqlutil.reduce_columns( - (c for c in columns if c.primary_key), self.onclause)) - self._columns.update((col._label, col) for col in columns) - self.foreign_keys.update(itertools.chain( - *[col.foreign_keys for col in columns])) - - def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() - self.left = clone(self.left, **kw) - self.right = clone(self.right, **kw) - self.onclause = clone(self.onclause, **kw) - self.__folded_equivalents = None - - def get_children(self, **kwargs): - return self.left, self.right, self.onclause - - def _match_primaries(self, left, right): - if isinstance(left, Join): - left_right = left.right - else: - left_right = None - return sqlutil.join_condition(left, right, a_subset=left_right) - - def select(self, whereclause=None, fold_equivalents=False, **kwargs): - """Create a :class:`.Select` from this :class:`.Join`. - - The equivalent long-hand form, given a :class:`.Join` object - ``j``, is:: - - from sqlalchemy import select - j = select([j.left, j.right], **kw).\\ - where(whereclause).\\ - select_from(j) - - :param whereclause: the WHERE criterion that will be sent to - the :func:`select()` function - - :param fold_equivalents: based on the join criterion of this - :class:`.Join`, do not include - repeat column names in the column list of the resulting - select, for columns that are calculated to be "equivalent" - based on the join criterion of this :class:`.Join`. This will - recursively apply to any joins directly nested by this one - as well. - - :param \**kwargs: all other kwargs are sent to the - underlying :func:`select()` function. - - """ - if fold_equivalents: - collist = sqlutil.folded_equivalents(self) - else: - collist = [self.left, self.right] - - return select(collist, whereclause, from_obj=[self], **kwargs) - - @property - def bind(self): - return self.left.bind or self.right.bind - - def alias(self, name=None): - """return an alias of this :class:`.Join`. - - Used against a :class:`.Join` object, - :meth:`~.Join.alias` calls the :meth:`~.Join.select` - method first so that a subquery against a - :func:`.select` construct is generated. - the :func:`~expression.select` construct also has the - ``correlate`` flag set to ``False`` and will not - auto-correlate inside an enclosing :func:`~expression.select` - construct. - - The equivalent long-hand form, given a :class:`.Join` object - ``j``, is:: - - from sqlalchemy import select, alias - j = alias( - select([j.left, j.right]).\\ - select_from(j).\\ - with_labels(True).\\ - correlate(False), - name=name - ) - - See :func:`~.expression.alias` for further details on - aliases. - - """ - return self.select(use_labels=True, correlate=False).alias(name) - - @property - def _hide_froms(self): - return itertools.chain(*[_from_objects(x.left, x.right) - for x in self._cloned_set]) - - @property - def _from_objects(self): - return [self] + \ - self.onclause._from_objects + \ - self.left._from_objects + \ - self.right._from_objects - -class Alias(FromClause): - """Represents an table or selectable alias (AS). - - Represents an alias, as typically applied to any table or - sub-select within a SQL statement using the ``AS`` keyword (or - without the keyword on certain databases such as Oracle). - - This object is constructed from the :func:`~.expression.alias` module level - function as well as the :meth:`.FromClause.alias` method available on all - :class:`.FromClause` subclasses. - - """ - - __visit_name__ = 'alias' - named_with_column = True - - def __init__(self, selectable, name=None): - baseselectable = selectable - while isinstance(baseselectable, Alias): - baseselectable = baseselectable.element - self.original = baseselectable - self.supports_execution = baseselectable.supports_execution - if self.supports_execution: - self._execution_options = baseselectable._execution_options - self.element = selectable - if name is None: - if self.original.named_with_column: - name = getattr(self.original, 'name', None) - name = _anonymous_label('%%(%d %s)s' % (id(self), name - or 'anon')) - self.name = name - - @property - def description(self): - # Py3K - #return self.name - # Py2K - return self.name.encode('ascii', 'backslashreplace') - # end Py2K - - def as_scalar(self): - try: - return self.element.as_scalar() - except AttributeError: - raise AttributeError("Element %s does not support " - "'as_scalar()'" % self.element) - - def is_derived_from(self, fromclause): - if fromclause in self._cloned_set: - return True - return self.element.is_derived_from(fromclause) - - def _populate_column_collection(self): - for col in self.element.columns: - col._make_proxy(self) - - def _copy_internals(self, clone=_clone, **kw): - # don't apply anything to an aliased Table - # for now. May want to drive this from - # the given **kw. - if isinstance(self.element, TableClause): - return - self._reset_exported() - self.element = clone(self.element, **kw) - baseselectable = self.element - while isinstance(baseselectable, Alias): - baseselectable = baseselectable.element - self.original = baseselectable - - def get_children(self, column_collections=True, **kw): - if column_collections: - for c in self.c: - yield c - yield self.element - - @property - def _from_objects(self): - return [self] - - @property - def bind(self): - return self.element.bind - -class CTE(Alias): - """Represent a Common Table Expression. - - The :class:`.CTE` object is obtained using the - :meth:`._SelectBase.cte` method from any selectable. - See that method for complete examples. - - .. versionadded:: 0.7.6 - - """ - __visit_name__ = 'cte' - - def __init__(self, selectable, - name=None, - recursive=False, - cte_alias=False, - _restates=frozenset()): - self.recursive = recursive - self.cte_alias = cte_alias - self._restates = _restates - super(CTE, self).__init__(selectable, name=name) - - def alias(self, name=None): - return CTE( - self.original, - name=name, - recursive=self.recursive, - cte_alias = self.name - ) - - def union(self, other): - return CTE( - self.original.union(other), - name=self.name, - recursive=self.recursive, - _restates=self._restates.union([self]) - ) - - def union_all(self, other): - return CTE( - self.original.union_all(other), - name=self.name, - recursive=self.recursive, - _restates=self._restates.union([self]) - ) - - -class _Grouping(ColumnElement): - """Represent a grouping within a column expression""" - - __visit_name__ = 'grouping' - - def __init__(self, element): - self.element = element - self.type = getattr(element, 'type', None) - - @property - def _label(self): - return getattr(self.element, '_label', None) or self.anon_label - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - def get_children(self, **kwargs): - return self.element, - - @property - def _from_objects(self): - return self.element._from_objects - - def __getattr__(self, attr): - return getattr(self.element, attr) - - def __getstate__(self): - return {'element':self.element, 'type':self.type} - - def __setstate__(self, state): - self.element = state['element'] - self.type = state['type'] - -class _FromGrouping(FromClause): - """Represent a grouping of a FROM clause""" - __visit_name__ = 'grouping' - - def __init__(self, element): - self.element = element - - def _init_collections(self): - pass - - @property - def columns(self): - return self.element.columns - - @property - def primary_key(self): - return self.element.primary_key - - @property - def foreign_keys(self): - # this could be - # self.element.foreign_keys - # see SelectableTest.test_join_condition - return set() - - @property - def _hide_froms(self): - return self.element._hide_froms - - def get_children(self, **kwargs): - return self.element, - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - @property - def _from_objects(self): - return self.element._from_objects - - def __getattr__(self, attr): - return getattr(self.element, attr) - - def __getstate__(self): - return {'element':self.element} - - def __setstate__(self, state): - self.element = state['element'] - -class _Over(ColumnElement): - """Represent an OVER clause. - - This is a special operator against a so-called - "window" function, as well as any aggregate function, - which produces results relative to the result set - itself. It's supported only by certain database - backends. - - """ - __visit_name__ = 'over' - - order_by = None - partition_by = None - - def __init__(self, func, partition_by=None, order_by=None): - self.func = func - if order_by is not None: - self.order_by = ClauseList(*util.to_list(order_by)) - if partition_by is not None: - self.partition_by = ClauseList(*util.to_list(partition_by)) - - @util.memoized_property - def type(self): - return self.func.type - - def get_children(self, **kwargs): - return [c for c in - (self.func, self.partition_by, self.order_by) - if c is not None] - - def _copy_internals(self, clone=_clone, **kw): - self.func = clone(self.func, **kw) - if self.partition_by is not None: - self.partition_by = clone(self.partition_by, **kw) - if self.order_by is not None: - self.order_by = clone(self.order_by, **kw) - - @property - def _from_objects(self): - return list(itertools.chain( - *[c._from_objects for c in - (self.func, self.partition_by, self.order_by) - if c is not None] - )) - -class _Label(ColumnElement): - """Represents a column label (AS). - - Represent a label, as typically applied to any column-level - element using the ``AS`` sql keyword. - - This object is constructed from the :func:`label()` module level - function as well as the :func:`label()` method available on all - :class:`.ColumnElement` subclasses. - - """ - - __visit_name__ = 'label' - - def __init__(self, name, element, type_=None): - while isinstance(element, _Label): - element = element.element - if name: - self.name = name - else: - self.name = _anonymous_label('%%(%d %s)s' % (id(self), - getattr(element, 'name', 'anon'))) - self.key = self._label = self._key_label = self.name - self._element = element - self._type = type_ - self.quote = element.quote - self.proxies = [element] - - @util.memoized_property - def type(self): - return sqltypes.to_instance( - self._type or getattr(self._element, 'type', None) - ) - - @util.memoized_property - def element(self): - return self._element.self_group(against=operators.as_) - - def self_group(self, against=None): - sub_element = self._element.self_group(against=against) - if sub_element is not self._element: - return _Label(self.name, - sub_element, - type_=self._type) - else: - return self - - @property - def primary_key(self): - return self.element.primary_key - - @property - def foreign_keys(self): - return self.element.foreign_keys - - def get_children(self, **kwargs): - return self.element, - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - @property - def _from_objects(self): - return self.element._from_objects - - def _make_proxy(self, selectable, name = None): - e = self.element._make_proxy(selectable, name=name or self.name) - e.proxies.append(self) - return e - -class ColumnClause(_Immutable, ColumnElement): - """Represents a generic column expression from any textual string. - - This includes columns associated with tables, aliases and select - statements, but also any arbitrary text. May or may not be bound - to an underlying :class:`.Selectable`. - - :class:`.ColumnClause` is constructed by itself typically via - the :func:`~.expression.column` function. It may be placed directly - into constructs such as :func:`.select` constructs:: - - from sqlalchemy.sql import column, select - - c1, c2 = column("c1"), column("c2") - s = select([c1, c2]).where(c1==5) - - There is also a variant on :func:`~.expression.column` known - as :func:`~.expression.literal_column` - the difference is that - in the latter case, the string value is assumed to be an exact - expression, rather than a column name, so that no quoting rules - or similar are applied:: - - from sqlalchemy.sql import literal_column, select - - s = select([literal_column("5 + 7")]) - - :class:`.ColumnClause` can also be used in a table-like - fashion by combining the :func:`~.expression.column` function - with the :func:`~.expression.table` function, to produce - a "lightweight" form of table metadata:: - - from sqlalchemy.sql import table, column - - user = table("user", - column("id"), - column("name"), - column("description"), - ) - - The above construct can be created in an ad-hoc fashion and is - not associated with any :class:`.schema.MetaData`, unlike it's - more full fledged :class:`.schema.Table` counterpart. - - :param text: the text of the element. - - :param selectable: parent selectable. - - :param type: :class:`.types.TypeEngine` object which can associate - this :class:`.ColumnClause` with a type. - - :param is_literal: if True, the :class:`.ColumnClause` is assumed to - be an exact expression that will be delivered to the output with no - quoting rules applied regardless of case sensitive settings. the - :func:`literal_column()` function is usually used to create such a - :class:`.ColumnClause`. - - """ - __visit_name__ = 'column' - - onupdate = default = server_default = server_onupdate = None - - _memoized_property = util.group_expirable_memoized_property() - - def __init__(self, text, selectable=None, type_=None, is_literal=False): - self.key = self.name = text - self.table = selectable - self.type = sqltypes.to_instance(type_) - self.is_literal = is_literal - - def _compare_name_for_result(self, other): - if self.is_literal or \ - self.table is None or \ - not hasattr(other, 'proxy_set') or ( - isinstance(other, ColumnClause) and other.is_literal - ): - return super(ColumnClause, self).\ - _compare_name_for_result(other) - else: - return other.proxy_set.intersection(self.proxy_set) - - def _get_table(self): - return self.__dict__['table'] - def _set_table(self, table): - self._memoized_property.expire_instance(self) - self.__dict__['table'] = table - table = property(_get_table, _set_table) - - @_memoized_property - def _from_objects(self): - t = self.table - if t is not None: - return [t] - else: - return [] - - @util.memoized_property - def description(self): - # Py3K - #return self.name - # Py2K - return self.name.encode('ascii', 'backslashreplace') - # end Py2K - - @_memoized_property - def _key_label(self): - if self.key != self.name: - return self._gen_label(self.key) - else: - return self._label - - @_memoized_property - def _label(self): - return self._gen_label(self.name) - - def _gen_label(self, name): - t = self.table - if self.is_literal: - return None - - elif t is not None and t.named_with_column: - if getattr(t, 'schema', None): - label = t.schema.replace('.', '_') + "_" + \ - t.name + "_" + name - else: - label = t.name + "_" + name - - # ensure the label name doesn't conflict with that - # of an existing column - if label in t.c: - _label = label - counter = 1 - while _label in t.c: - _label = label + "_" + str(counter) - counter += 1 - label = _label - - return _as_truncated(label) - - else: - return name - - def label(self, name): - # currently, anonymous labels don't occur for - # ColumnClause. The use at the moment - # is that they do not generate nicely for - # is_literal clauses. We would like to change - # this so that label(None) acts as would be expected. - # See [ticket:2168]. - if name is None: - return self - else: - return super(ColumnClause, self).label(name) - - - def _bind_param(self, operator, obj): - return _BindParamClause(self.name, obj, - _compared_to_operator=operator, - _compared_to_type=self.type, - unique=True) - - def _make_proxy(self, selectable, name=None, attach=True): - # propagate the "is_literal" flag only if we are keeping our name, - # otherwise its considered to be a label - is_literal = self.is_literal and (name is None or name == self.name) - c = self._constructor( - _as_truncated(name or self.name), - selectable=selectable, - type_=self.type, - is_literal=is_literal - ) - c.proxies = [self] - if selectable._is_clone_of is not None: - c._is_clone_of = \ - selectable._is_clone_of.columns.get(c.name) - - if attach: - selectable._columns[c.name] = c - return c - -class TableClause(_Immutable, FromClause): - """Represents a minimal "table" construct. - - The constructor for :class:`.TableClause` is the - :func:`~.expression.table` function. This produces - a lightweight table object that has only a name and a - collection of columns, which are typically produced - by the :func:`~.expression.column` function:: - - from sqlalchemy.sql import table, column - - user = table("user", - column("id"), - column("name"), - column("description"), - ) - - The :class:`.TableClause` construct serves as the base for - the more commonly used :class:`~.schema.Table` object, providing - the usual set of :class:`~.expression.FromClause` services including - the ``.c.`` collection and statement generation methods. - - It does **not** provide all the additional schema-level services - of :class:`~.schema.Table`, including constraints, references to other - tables, or support for :class:`.MetaData`-level services. It's useful - on its own as an ad-hoc construct used to generate quick SQL - statements when a more fully fledged :class:`~.schema.Table` is not on hand. - - """ - - __visit_name__ = 'table' - - named_with_column = True - - def __init__(self, name, *columns): - super(TableClause, self).__init__() - self.name = self.fullname = name - self._columns = ColumnCollection() - self.primary_key = ColumnSet() - self.foreign_keys = set() - for c in columns: - self.append_column(c) - - def _init_collections(self): - pass - - @util.memoized_property - def description(self): - # Py3K - #return self.name - # Py2K - return self.name.encode('ascii', 'backslashreplace') - # end Py2K - - def append_column(self, c): - self._columns[c.name] = c - c.table = self - - def get_children(self, column_collections=True, **kwargs): - if column_collections: - return [c for c in self.c] - else: - return [] - - def count(self, whereclause=None, **params): - """return a SELECT COUNT generated against this - :class:`.TableClause`.""" - - if self.primary_key: - col = list(self.primary_key)[0] - else: - col = list(self.columns)[0] - return select( - [func.count(col).label('tbl_row_count')], - whereclause, - from_obj=[self], - **params) - - def insert(self, values=None, inline=False, **kwargs): - """Generate an :func:`.insert` construct against this - :class:`.TableClause`. - - E.g.:: - - table.insert().values(name='foo') - - See :func:`.insert` for argument and usage information. - - """ - - return insert(self, values=values, inline=inline, **kwargs) - - def update(self, whereclause=None, values=None, inline=False, **kwargs): - """Generate an :func:`.update` construct against this - :class:`.TableClause`. - - E.g.:: - - table.update().where(table.c.id==7).values(name='foo') - - See :func:`.update` for argument and usage information. - - """ - - return update(self, whereclause=whereclause, - values=values, inline=inline, **kwargs) - - def delete(self, whereclause=None, **kwargs): - """Generate a :func:`.delete` construct against this - :class:`.TableClause`. - - E.g.:: - - table.delete().where(table.c.id==7) - - See :func:`.delete` for argument and usage information. - - """ - - return delete(self, whereclause, **kwargs) - - @property - def _from_objects(self): - return [self] - -class _SelectBase(Executable, FromClause): - """Base class for :class:`.Select` and ``CompoundSelects``.""" - - _order_by_clause = ClauseList() - _group_by_clause = ClauseList() - _limit = None - _offset = None - - def __init__(self, - use_labels=False, - for_update=False, - limit=None, - offset=None, - order_by=None, - group_by=None, - bind=None, - autocommit=None): - self.use_labels = use_labels - self.for_update = for_update - if autocommit is not None: - util.warn_deprecated('autocommit on select() is ' - 'deprecated. Use .execution_options(a' - 'utocommit=True)') - self._execution_options = \ - self._execution_options.union({'autocommit' - : autocommit}) - if limit is not None: - self._limit = util.asint(limit) - if offset is not None: - self._offset = util.asint(offset) - self._bind = bind - - if order_by is not None: - self._order_by_clause = ClauseList(*util.to_list(order_by)) - if group_by is not None: - self._group_by_clause = ClauseList(*util.to_list(group_by)) - - def as_scalar(self): - """return a 'scalar' representation of this selectable, which can be - used as a column expression. - - Typically, a select statement which has only one column in its columns - clause is eligible to be used as a scalar expression. - - The returned object is an instance of - :class:`_ScalarSelect`. - - """ - return _ScalarSelect(self) - - @_generative - def apply_labels(self): - """return a new selectable with the 'use_labels' flag set to True. - - This will result in column expressions being generated using labels - against their table name, such as "SELECT somecolumn AS - tablename_somecolumn". This allows selectables which contain multiple - FROM clauses to produce a unique set of column names regardless of - name conflicts among the individual FROM clauses. - - """ - self.use_labels = True - - def label(self, name): - """return a 'scalar' representation of this selectable, embedded as a - subquery with a label. - - See also :meth:`~._SelectBase.as_scalar`. - - """ - return self.as_scalar().label(name) - - def cte(self, name=None, recursive=False): - """Return a new :class:`.CTE`, or Common Table Expression instance. - - Common table expressions are a SQL standard whereby SELECT - statements can draw upon secondary statements specified along - with the primary statement, using a clause called "WITH". - Special semantics regarding UNION can also be employed to - allow "recursive" queries, where a SELECT statement can draw - upon the set of rows that have previously been selected. - - SQLAlchemy detects :class:`.CTE` objects, which are treated - similarly to :class:`.Alias` objects, as special elements - to be delivered to the FROM clause of the statement as well - as to a WITH clause at the top of the statement. - - .. versionadded:: 0.7.6 - - :param name: name given to the common table expression. Like - :meth:`._FromClause.alias`, the name can be left as ``None`` - in which case an anonymous symbol will be used at query - compile time. - :param recursive: if ``True``, will render ``WITH RECURSIVE``. - A recursive common table expression is intended to be used in - conjunction with UNION ALL in order to derive rows - from those already selected. - - The following examples illustrate two examples from - Postgresql's documentation at - http://www.postgresql.org/docs/8.4/static/queries-with.html. - - Example 1, non recursive:: - - from sqlalchemy import Table, Column, String, Integer, MetaData, \\ - select, func - - metadata = MetaData() - - orders = Table('orders', metadata, - Column('region', String), - Column('amount', Integer), - Column('product', String), - Column('quantity', Integer) - ) - - regional_sales = select([ - orders.c.region, - func.sum(orders.c.amount).label('total_sales') - ]).group_by(orders.c.region).cte("regional_sales") - - - top_regions = select([regional_sales.c.region]).\\ - where( - regional_sales.c.total_sales > - select([ - func.sum(regional_sales.c.total_sales)/10 - ]) - ).cte("top_regions") - - statement = select([ - orders.c.region, - orders.c.product, - func.sum(orders.c.quantity).label("product_units"), - func.sum(orders.c.amount).label("product_sales") - ]).where(orders.c.region.in_( - select([top_regions.c.region]) - )).group_by(orders.c.region, orders.c.product) - - result = conn.execute(statement).fetchall() - - Example 2, WITH RECURSIVE:: - - from sqlalchemy import Table, Column, String, Integer, MetaData, \\ - select, func - - metadata = MetaData() - - parts = Table('parts', metadata, - Column('part', String), - Column('sub_part', String), - Column('quantity', Integer), - ) - - included_parts = select([ - parts.c.sub_part, - parts.c.part, - parts.c.quantity]).\\ - where(parts.c.part=='our part').\\ - cte(recursive=True) - - - incl_alias = included_parts.alias() - parts_alias = parts.alias() - included_parts = included_parts.union_all( - select([ - parts_alias.c.part, - parts_alias.c.sub_part, - parts_alias.c.quantity - ]). - where(parts_alias.c.part==incl_alias.c.sub_part) - ) - - statement = select([ - included_parts.c.sub_part, - func.sum(included_parts.c.quantity).label('total_quantity') - ]).\ - select_from(included_parts.join(parts, - included_parts.c.part==parts.c.part)).\\ - group_by(included_parts.c.sub_part) - - result = conn.execute(statement).fetchall() - - - See also: - - :meth:`.orm.query.Query.cte` - ORM version of :meth:`._SelectBase.cte`. - - """ - return CTE(self, name=name, recursive=recursive) - - @_generative - @util.deprecated('0.6', - message=":func:`.autocommit` is deprecated. Use " - ":func:`.Executable.execution_options` with the " - "'autocommit' flag.") - def autocommit(self): - """return a new selectable with the 'autocommit' flag set to - True.""" - - self._execution_options = \ - self._execution_options.union({'autocommit': True}) - - def _generate(self): - """Override the default _generate() method to also clear out - exported collections.""" - - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - s._reset_exported() - return s - - @_generative - def limit(self, limit): - """return a new selectable with the given LIMIT criterion - applied.""" - - self._limit = util.asint(limit) - - @_generative - def offset(self, offset): - """return a new selectable with the given OFFSET criterion - applied.""" - - self._offset = util.asint(offset) - - @_generative - def order_by(self, *clauses): - """return a new selectable with the given list of ORDER BY - criterion applied. - - The criterion will be appended to any pre-existing ORDER BY - criterion. - - """ - - self.append_order_by(*clauses) - - @_generative - def group_by(self, *clauses): - """return a new selectable with the given list of GROUP BY - criterion applied. - - The criterion will be appended to any pre-existing GROUP BY - criterion. - - """ - - self.append_group_by(*clauses) - - def append_order_by(self, *clauses): - """Append the given ORDER BY criterion applied to this selectable. - - The criterion will be appended to any pre-existing ORDER BY criterion. - - """ - if len(clauses) == 1 and clauses[0] is None: - self._order_by_clause = ClauseList() - else: - if getattr(self, '_order_by_clause', None) is not None: - clauses = list(self._order_by_clause) + list(clauses) - self._order_by_clause = ClauseList(*clauses) - - def append_group_by(self, *clauses): - """Append the given GROUP BY criterion applied to this selectable. - - The criterion will be appended to any pre-existing GROUP BY criterion. - - """ - if len(clauses) == 1 and clauses[0] is None: - self._group_by_clause = ClauseList() - else: - if getattr(self, '_group_by_clause', None) is not None: - clauses = list(self._group_by_clause) + list(clauses) - self._group_by_clause = ClauseList(*clauses) - - @property - def _from_objects(self): - return [self] - - -class _ScalarSelect(_Grouping): - _from_objects = [] - - def __init__(self, element): - self.element = element - self.type = element._scalar_type() - - @property - def columns(self): - raise exc.InvalidRequestError('Scalar Select expression has no ' - 'columns; use this object directly within a ' - 'column-level expression.') - c = columns - - def self_group(self, **kwargs): - return self - - def _make_proxy(self, selectable, name): - return list(self.inner_columns)[0]._make_proxy(selectable, name) - -class CompoundSelect(_SelectBase): - """Forms the basis of ``UNION``, ``UNION ALL``, and other - SELECT-based set operations.""" - - __visit_name__ = 'compound_select' - - UNION = util.symbol('UNION') - UNION_ALL = util.symbol('UNION ALL') - EXCEPT = util.symbol('EXCEPT') - EXCEPT_ALL = util.symbol('EXCEPT ALL') - INTERSECT = util.symbol('INTERSECT') - INTERSECT_ALL = util.symbol('INTERSECT ALL') - - def __init__(self, keyword, *selects, **kwargs): - self._should_correlate = kwargs.pop('correlate', False) - self.keyword = keyword - self.selects = [] - - numcols = None - - # some DBs do not like ORDER BY in the inner queries of a UNION, etc. - for n, s in enumerate(selects): - s = _clause_element_as_expr(s) - - if not numcols: - numcols = len(s.c) - elif len(s.c) != numcols: - raise exc.ArgumentError('All selectables passed to ' - 'CompoundSelect must have identical numbers of ' - 'columns; select #%d has %d columns, select ' - '#%d has %d' % (1, len(self.selects[0].c), n - + 1, len(s.c))) - - self.selects.append(s.self_group(self)) - - _SelectBase.__init__(self, **kwargs) - - def _scalar_type(self): - return self.selects[0]._scalar_type() - - def self_group(self, against=None): - return _FromGrouping(self) - - def is_derived_from(self, fromclause): - for s in self.selects: - if s.is_derived_from(fromclause): - return True - return False - - def _populate_column_collection(self): - for cols in zip(*[s.c for s in self.selects]): - - # this is a slightly hacky thing - the union exports a - # column that resembles just that of the *first* selectable. - # to get at a "composite" column, particularly foreign keys, - # you have to dig through the proxies collection which we - # generate below. We may want to improve upon this, such as - # perhaps _make_proxy can accept a list of other columns - # that are "shared" - schema.column can then copy all the - # ForeignKeys in. this would allow the union() to have all - # those fks too. - - proxy = cols[0]._make_proxy(self, name=self.use_labels - and cols[0]._label or None) - - # hand-construct the "proxies" collection to include all - # derived columns place a 'weight' annotation corresponding - # to how low in the list of select()s the column occurs, so - # that the corresponding_column() operation can resolve - # conflicts - - proxy.proxies = [c._annotate({'weight': i + 1}) for (i, - c) in enumerate(cols)] - - def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() - self.selects = [clone(s, **kw) for s in self.selects] - if hasattr(self, '_col_map'): - del self._col_map - for attr in ('_order_by_clause', '_group_by_clause'): - if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr), **kw)) - - def get_children(self, column_collections=True, **kwargs): - return (column_collections and list(self.c) or []) \ - + [self._order_by_clause, self._group_by_clause] \ - + list(self.selects) - - def bind(self): - if self._bind: - return self._bind - for s in self.selects: - e = s.bind - if e: - return e - else: - return None - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - -class Select(_SelectBase): - """Represents a ``SELECT`` statement. - - See also: - - :func:`~.expression.select` - the function which creates a :class:`.Select` object. - - :ref:`coretutorial_selecting` - Core Tutorial description of :func:`.select`. - - """ - - __visit_name__ = 'select' - - _prefixes = () - _hints = util.immutabledict() - _distinct = False - _from_cloned = None - - _memoized_property = _SelectBase._memoized_property - - def __init__(self, - columns, - whereclause=None, - from_obj=None, - distinct=False, - having=None, - correlate=True, - prefixes=None, - **kwargs): - """Construct a Select object. - - The public constructor for Select is the - :func:`select` function; see that function for - argument descriptions. - - Additional generative and mutator methods are available on the - :class:`_SelectBase` superclass. - - """ - self._should_correlate = correlate - if distinct is not False: - if isinstance(distinct, basestring): - util.warn_deprecated( - "A string argument passed to the 'distinct' " - "keyword argument of 'select()' is deprecated " - "- please use 'prefixes' or 'prefix_with()' " - "to specify additional prefixes") - if prefixes: - prefixes = util.to_list(prefixes) + [distinct] - else: - prefixes = [distinct] - elif distinct is True: - self._distinct = True - else: - self._distinct = [ - _literal_as_text(e) - for e in util.to_list(distinct) - ] - - self._correlate = set() - if from_obj is not None: - self._from_obj = util.OrderedSet( - _literal_as_text(f) - for f in util.to_list(from_obj)) - else: - self._from_obj = util.OrderedSet() - - try: - cols_present = bool(columns) - except TypeError: - raise exc.ArgumentError("columns argument to select() must " - "be a Python list or other iterable") - - if cols_present: - self._raw_columns = [] - for c in columns: - c = _literal_as_column(c) - if isinstance(c, _ScalarSelect): - c = c.self_group(against=operators.comma_op) - self._raw_columns.append(c) - else: - self._raw_columns = [] - - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - - if having is not None: - self._having = _literal_as_text(having) - else: - self._having = None - - if prefixes: - self._prefixes = tuple([_literal_as_text(p) for p in prefixes]) - - _SelectBase.__init__(self, **kwargs) - - @property - def _froms(self): - # would love to cache this, - # but there's just enough edge cases, particularly now that - # declarative encourages construction of SQL expressions - # without tables present, to just regen this each time. - froms = [] - seen = set() - translate = self._from_cloned - def add(items): - for item in items: - if translate and item in translate: - item = translate[item] - if not seen.intersection(item._cloned_set): - froms.append(item) - seen.update(item._cloned_set) - - add(_from_objects(*self._raw_columns)) - if self._whereclause is not None: - add(_from_objects(self._whereclause)) - add(self._from_obj) - - return froms - - def _get_display_froms(self, existing_froms=None): - """Return the full list of 'from' clauses to be displayed. - - Takes into account a set of existing froms which may be - rendered in the FROM clause of enclosing selects; this Select - may want to leave those absent if it is automatically - correlating. - - """ - froms = self._froms - - toremove = set(itertools.chain(*[f._hide_froms for f in froms])) - if toremove: - # if we're maintaining clones of froms, - # add the copies out to the toremove list. only include - # clones that are lexical equivalents. - if self._from_cloned: - toremove.update( - self._from_cloned[f] for f in - toremove.intersection(self._from_cloned) - if self._from_cloned[f]._is_lexical_equivalent(f) - ) - # filter out to FROM clauses not in the list, - # using a list to maintain ordering - froms = [f for f in froms if f not in toremove] - - if len(froms) > 1 or self._correlate: - if self._correlate: - froms = [f for f in froms if f not in _cloned_intersection(froms, - self._correlate)] - if self._should_correlate and existing_froms: - froms = [f for f in froms if f not in _cloned_intersection(froms, - existing_froms)] - - if not len(froms): - raise exc.InvalidRequestError("Select statement '%s" - "' returned no FROM clauses due to " - "auto-correlation; specify " - "correlate() to control " - "correlation manually." % self) - - return froms - - def _scalar_type(self): - elem = self._raw_columns[0] - cols = list(elem._select_iterable) - return cols[0].type - - @property - def froms(self): - """Return the displayed list of FromClause elements.""" - - return self._get_display_froms() - - @_generative - def with_hint(self, selectable, text, dialect_name='*'): - """Add an indexing hint for the given selectable to this - :class:`.Select`. - - The text of the hint is rendered in the appropriate - location for the database backend in use, relative - to the given :class:`.Table` or :class:`.Alias` passed as the - ``selectable`` argument. The dialect implementation - typically uses Python string substitution syntax - with the token ``%(name)s`` to render the name of - the table or alias. E.g. when using Oracle, the - following:: - - select([mytable]).\\ - with_hint(mytable, "+ index(%(name)s ix_mytable)") - - Would render SQL as:: - - select /*+ index(mytable ix_mytable) */ ... from mytable - - The ``dialect_name`` option will limit the rendering of a particular - hint to a particular backend. Such as, to add hints for both Oracle - and Sybase simultaneously:: - - select([mytable]).\\ - with_hint(mytable, "+ index(%(name)s ix_mytable)", 'oracle').\\ - with_hint(mytable, "WITH INDEX ix_mytable", 'sybase') - - """ - self._hints = self._hints.union({(selectable, dialect_name):text}) - - @property - def type(self): - raise exc.InvalidRequestError("Select objects don't have a type. " - "Call as_scalar() on this Select object " - "to return a 'scalar' version of this Select.") - - @_memoized_property.method - def locate_all_froms(self): - """return a Set of all FromClause elements referenced by this Select. - - This set is a superset of that returned by the ``froms`` property, - which is specifically for those FromClause elements that would - actually be rendered. - - """ - froms = self._froms - return froms + list(_from_objects(*froms)) - - @property - def inner_columns(self): - """an iterator of all ColumnElement expressions which would - be rendered into the columns clause of the resulting SELECT statement. - - """ - return _select_iterables(self._raw_columns) - - def is_derived_from(self, fromclause): - if self in fromclause._cloned_set: - return True - - for f in self.locate_all_froms(): - if f.is_derived_from(fromclause): - return True - return False - - def _copy_internals(self, clone=_clone, **kw): - - # Select() object has been cloned and probably adapted by the - # given clone function. Apply the cloning function to internal - # objects - - # 1. keep a dictionary of the froms we've cloned, and what - # they've become. This is consulted later when we derive - # additional froms from "whereclause" and the columns clause, - # which may still reference the uncloned parent table. - # as of 0.7.4 we also put the current version of _froms, which - # gets cleared on each generation. previously we were "baking" - # _froms into self._from_obj. - self._from_cloned = from_cloned = dict((f, clone(f, **kw)) - for f in self._from_obj.union(self._froms)) - - # 3. update persistent _from_obj with the cloned versions. - self._from_obj = util.OrderedSet(from_cloned[f] for f in - self._from_obj) - - # the _correlate collection is done separately, what can happen - # here is the same item is _correlate as in _from_obj but the - # _correlate version has an annotation on it - (specifically - # RelationshipProperty.Comparator._criterion_exists() does - # this). Also keep _correlate liberally open with it's previous - # contents, as this set is used for matching, not rendering. - self._correlate = set(clone(f) for f in - self._correlate).union(self._correlate) - - # 4. clone other things. The difficulty here is that Column - # objects are not actually cloned, and refer to their original - # .table, resulting in the wrong "from" parent after a clone - # operation. Hence _from_cloned and _from_obj supercede what is - # present here. - self._raw_columns = [clone(c, **kw) for c in self._raw_columns] - for attr in '_whereclause', '_having', '_order_by_clause', \ - '_group_by_clause': - if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr), **kw)) - - # erase exported column list, _froms collection, - # etc. - self._reset_exported() - - def get_children(self, column_collections=True, **kwargs): - """return child elements as per the ClauseElement specification.""" - - return (column_collections and list(self.columns) or []) + \ - self._raw_columns + list(self._froms) + \ - [x for x in - (self._whereclause, self._having, - self._order_by_clause, self._group_by_clause) - if x is not None] - - @_generative - def column(self, column): - """return a new select() construct with the given column expression - added to its columns clause. - - """ - self.append_column(column) - - @_generative - def with_only_columns(self, columns): - """Return a new :func:`.select` construct with its columns - clause replaced with the given columns. - - .. versionchanged:: 0.7.3 - Due to a bug fix, this method has a slight - behavioral change as of version 0.7.3. - Prior to version 0.7.3, the FROM clause of - a :func:`.select` was calculated upfront and as new columns - were added; in 0.7.3 and later it's calculated - at compile time, fixing an issue regarding late binding - of columns to parent tables. This changes the behavior of - :meth:`.Select.with_only_columns` in that FROM clauses no - longer represented in the new list are dropped, - but this behavior is more consistent in - that the FROM clauses are consistently derived from the - current columns clause. The original intent of this method - is to allow trimming of the existing columns list to be fewer - columns than originally present; the use case of replacing - the columns list with an entirely different one hadn't - been anticipated until 0.7.3 was released; the usage - guidelines below illustrate how this should be done. - - This method is exactly equivalent to as if the original - :func:`.select` had been called with the given columns - clause. I.e. a statement:: - - s = select([table1.c.a, table1.c.b]) - s = s.with_only_columns([table1.c.b]) - - should be exactly equivalent to:: - - s = select([table1.c.b]) - - This means that FROM clauses which are only derived - from the column list will be discarded if the new column - list no longer contains that FROM:: - - >>> table1 = table('t1', column('a'), column('b')) - >>> table2 = table('t2', column('a'), column('b')) - >>> s1 = select([table1.c.a, table2.c.b]) - >>> print s1 - SELECT t1.a, t2.b FROM t1, t2 - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print s2 - SELECT t2.b FROM t1 - - The preferred way to maintain a specific FROM clause - in the construct, assuming it won't be represented anywhere - else (i.e. not in the WHERE clause, etc.) is to set it using - :meth:`.Select.select_from`:: - - >>> s1 = select([table1.c.a, table2.c.b]).\\ - ... select_from(table1.join(table2, table1.c.a==table2.c.a)) - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print s2 - SELECT t2.b FROM t1 JOIN t2 ON t1.a=t2.a - - Care should also be taken to use the correct - set of column objects passed to :meth:`.Select.with_only_columns`. - Since the method is essentially equivalent to calling the - :func:`.select` construct in the first place with the given - columns, the columns passed to :meth:`.Select.with_only_columns` - should usually be a subset of those which were passed - to the :func:`.select` construct, not those which are available - from the ``.c`` collection of that :func:`.select`. That - is:: - - s = select([table1.c.a, table1.c.b]).select_from(table1) - s = s.with_only_columns([table1.c.b]) - - and **not**:: - - # usually incorrect - s = s.with_only_columns([s.c.b]) - - The latter would produce the SQL:: - - SELECT b - FROM (SELECT t1.a AS a, t1.b AS b - FROM t1), t1 - - Since the :func:`.select` construct is essentially being - asked to select both from ``table1`` as well as itself. - - """ - self._reset_exported() - rc = [] - for c in columns: - c = _literal_as_column(c) - if isinstance(c, _ScalarSelect): - c = c.self_group(against=operators.comma_op) - rc.append(c) - self._raw_columns = rc - - @_generative - def where(self, whereclause): - """return a new select() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - - self.append_whereclause(whereclause) - - @_generative - def having(self, having): - """return a new select() construct with the given expression added to - its HAVING clause, joined to the existing clause via AND, if any. - - """ - self.append_having(having) - - @_generative - def distinct(self, *expr): - """Return a new select() construct which will apply DISTINCT to its - columns clause. - - :param \*expr: optional column expressions. When present, - the Postgresql dialect will render a ``DISTINCT ON (>)`` - construct. - - """ - if expr: - expr = [_literal_as_text(e) for e in expr] - if isinstance(self._distinct, list): - self._distinct = self._distinct + expr - else: - self._distinct = expr - else: - self._distinct = True - - @_generative - def prefix_with(self, *expr): - """return a new select() construct which will apply the given - expressions, typically strings, to the start of its columns clause, - not using any commas. In particular is useful for MySQL - keywords. - - e.g.:: - - select(['a', 'b']).prefix_with('HIGH_PRIORITY', - 'SQL_SMALL_RESULT', - 'ALL') - - Would render:: - - SELECT HIGH_PRIORITY SQL_SMALL_RESULT ALL a, b - - """ - expr = tuple(_literal_as_text(e) for e in expr) - self._prefixes = self._prefixes + expr - - @_generative - def select_from(self, fromclause): - """return a new :func:`.select` construct with the given FROM expression - merged into its list of FROM objects. - - E.g.:: - - table1 = table('t1', column('a')) - table2 = table('t2', column('b')) - s = select([table1.c.a]).\\ - select_from( - table1.join(table2, table1.c.a==table2.c.b) - ) - - The "from" list is a unique set on the identity of each element, - so adding an already present :class:`.Table` or other selectable - will have no effect. Passing a :class:`.Join` that refers - to an already present :class:`.Table` or other selectable will have - the effect of concealing the presence of that selectable as - an individual element in the rendered FROM list, instead rendering it into a - JOIN clause. - - While the typical purpose of :meth:`.Select.select_from` is to replace - the default, derived FROM clause with a join, it can also be called with - individual table elements, multiple times if desired, in the case that the - FROM clause cannot be fully derived from the columns clause:: - - select([func.count('*')]).select_from(table1) - - """ - self.append_from(fromclause) - - @_generative - def correlate(self, *fromclauses): - """return a new select() construct which will correlate the given FROM - clauses to that of an enclosing select(), if a match is found. - - By "match", the given fromclause must be present in this select's - list of FROM objects and also present in an enclosing select's list of - FROM objects. - - Calling this method turns off the select's default behavior of - "auto-correlation". Normally, select() auto-correlates all of its FROM - clauses to those of an embedded select when compiled. - - If the fromclause is None, correlation is disabled for the returned - select(). - - """ - self._should_correlate = False - if fromclauses and fromclauses[0] is None: - self._correlate = set() - else: - self._correlate = self._correlate.union(fromclauses) - - def append_correlation(self, fromclause): - """append the given correlation expression to this select() - construct.""" - - self._should_correlate = False - self._correlate = self._correlate.union([fromclause]) - - def append_column(self, column): - """append the given column expression to the columns clause of this - select() construct. - - """ - self._reset_exported() - column = _literal_as_column(column) - - if isinstance(column, _ScalarSelect): - column = column.self_group(against=operators.comma_op) - - self._raw_columns = self._raw_columns + [column] - - def append_prefix(self, clause): - """append the given columns clause prefix expression to this select() - construct. - - """ - clause = _literal_as_text(clause) - self._prefixes = self._prefixes + (clause,) - - def append_whereclause(self, whereclause): - """append the given expression to this select() construct's WHERE - criterion. - - The expression will be joined to existing WHERE criterion via AND. - - """ - self._reset_exported() - whereclause = _literal_as_text(whereclause) - - if self._whereclause is not None: - self._whereclause = and_(self._whereclause, whereclause) - else: - self._whereclause = whereclause - - def append_having(self, having): - """append the given expression to this select() construct's HAVING - criterion. - - The expression will be joined to existing HAVING criterion via AND. - - """ - if self._having is not None: - self._having = and_(self._having, _literal_as_text(having)) - else: - self._having = _literal_as_text(having) - - def append_from(self, fromclause): - """append the given FromClause expression to this select() construct's - FROM clause. - - """ - self._reset_exported() - fromclause = _literal_as_text(fromclause) - self._from_obj = self._from_obj.union([fromclause]) - - def _populate_column_collection(self): - for c in self.inner_columns: - if hasattr(c, '_make_proxy'): - c._make_proxy(self, - name=self.use_labels - and c._label or None) - - def self_group(self, against=None): - """return a 'grouping' construct as per the ClauseElement - specification. - - This produces an element that can be embedded in an expression. Note - that this method is called automatically as needed when constructing - expressions and should not require explicit use. - - """ - if isinstance(against, CompoundSelect): - return self - return _FromGrouping(self) - - def union(self, other, **kwargs): - """return a SQL UNION of this select() construct against the given - selectable.""" - - return union(self, other, **kwargs) - - def union_all(self, other, **kwargs): - """return a SQL UNION ALL of this select() construct against the given - selectable. - - """ - return union_all(self, other, **kwargs) - - def except_(self, other, **kwargs): - """return a SQL EXCEPT of this select() construct against the given - selectable.""" - - return except_(self, other, **kwargs) - - def except_all(self, other, **kwargs): - """return a SQL EXCEPT ALL of this select() construct against the - given selectable. - - """ - return except_all(self, other, **kwargs) - - def intersect(self, other, **kwargs): - """return a SQL INTERSECT of this select() construct against the given - selectable. - - """ - return intersect(self, other, **kwargs) - - def intersect_all(self, other, **kwargs): - """return a SQL INTERSECT ALL of this select() construct against the - given selectable. - - """ - return intersect_all(self, other, **kwargs) - - def bind(self): - if self._bind: - return self._bind - froms = self._froms - if not froms: - for c in self._raw_columns: - e = c.bind - if e: - self._bind = e - return e - else: - e = list(froms)[0].bind - if e: - self._bind = e - return e - - return None - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - -class UpdateBase(Executable, ClauseElement): - """Form the base for ``INSERT``, ``UPDATE``, and ``DELETE`` statements. - - """ - - __visit_name__ = 'update_base' - - _execution_options = \ - Executable._execution_options.union({'autocommit': True}) - kwargs = util.immutabledict() - _hints = util.immutabledict() - - def _process_colparams(self, parameters): - if isinstance(parameters, (list, tuple)): - pp = {} - for i, c in enumerate(self.table.c): - pp[c.key] = parameters[i] - return pp - else: - return parameters - - def params(self, *arg, **kw): - """Set the parameters for the statement. - - This method raises ``NotImplementedError`` on the base class, - and is overridden by :class:`.ValuesBase` to provide the - SET/VALUES clause of UPDATE and INSERT. - - """ - raise NotImplementedError( - "params() is not supported for INSERT/UPDATE/DELETE statements." - " To set the values for an INSERT or UPDATE statement, use" - " stmt.values(**parameters).") - - def bind(self): - """Return a 'bind' linked to this :class:`.UpdateBase` - or a :class:`.Table` associated with it. - - """ - return self._bind or self.table.bind - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - _returning_re = re.compile(r'(?:firebird|postgres(?:ql)?)_returning') - def _process_deprecated_kw(self, kwargs): - for k in list(kwargs): - m = self._returning_re.match(k) - if m: - self._returning = kwargs.pop(k) - util.warn_deprecated( - "The %r argument is deprecated. Please " - "use statement.returning(col1, col2, ...)" % k - ) - return kwargs - - @_generative - def returning(self, *cols): - """Add a RETURNING or equivalent clause to this statement. - - The given list of columns represent columns within the table that is - the target of the INSERT, UPDATE, or DELETE. Each element can be any - column expression. :class:`~sqlalchemy.schema.Table` objects will be - expanded into their individual columns. - - Upon compilation, a RETURNING clause, or database equivalent, - will be rendered within the statement. For INSERT and UPDATE, - the values are the newly inserted/updated values. For DELETE, - the values are those of the rows which were deleted. - - Upon execution, the values of the columns to be returned - are made available via the result set and can be iterated - using ``fetchone()`` and similar. For DBAPIs which do not - natively support returning values (i.e. cx_oracle), - SQLAlchemy will approximate this behavior at the result level - so that a reasonable amount of behavioral neutrality is - provided. - - Note that not all databases/DBAPIs - support RETURNING. For those backends with no support, - an exception is raised upon compilation and/or execution. - For those who do support it, the functionality across backends - varies greatly, including restrictions on executemany() - and other statements which return multiple rows. Please - read the documentation notes for the database in use in - order to determine the availability of RETURNING. - - """ - self._returning = cols - - @_generative - def with_hint(self, text, selectable=None, dialect_name="*"): - """Add a table hint for a single table to this - INSERT/UPDATE/DELETE statement. - - .. note:: - - :meth:`.UpdateBase.with_hint` currently applies only to - Microsoft SQL Server. For MySQL INSERT hints, use - :meth:`.Insert.prefix_with`. UPDATE/DELETE hints for - MySQL will be added in a future release. - - The text of the hint is rendered in the appropriate - location for the database backend in use, relative - to the :class:`.Table` that is the subject of this - statement, or optionally to that of the given - :class:`.Table` passed as the ``selectable`` argument. - - The ``dialect_name`` option will limit the rendering of a particular - hint to a particular backend. Such as, to add a hint - that only takes effect for SQL Server:: - - mytable.insert().with_hint("WITH (PAGLOCK)", dialect_name="mssql") - - .. versionadded:: 0.7.6 - - :param text: Text of the hint. - :param selectable: optional :class:`.Table` that specifies - an element of the FROM clause within an UPDATE or DELETE - to be the subject of the hint - applies only to certain backends. - :param dialect_name: defaults to ``*``, if specified as the name - of a particular dialect, will apply these hints only when - that dialect is in use. - """ - if selectable is None: - selectable = self.table - - self._hints = self._hints.union({(selectable, dialect_name):text}) - -class ValuesBase(UpdateBase): - """Supplies support for :meth:`.ValuesBase.values` to INSERT and UPDATE constructs.""" - - __visit_name__ = 'values_base' - - def __init__(self, table, values): - self.table = table - self.parameters = self._process_colparams(values) - - @_generative - def values(self, *args, **kwargs): - """specify the VALUES clause for an INSERT statement, or the SET - clause for an UPDATE. - - :param \**kwargs: key value pairs representing the string key - of a :class:`.Column` mapped to the value to be rendered into the - VALUES or SET clause:: - - users.insert().values(name="some name") - - users.update().where(users.c.id==5).values(name="some name") - - :param \*args: A single dictionary can be sent as the first positional - argument. This allows non-string based keys, such as Column - objects, to be used:: - - users.insert().values({users.c.name : "some name"}) - - users.update().where(users.c.id==5).values({users.c.name : "some name"}) - - See also: - - :ref:`inserts_and_updates` - SQL Expression - Language Tutorial - - :func:`~.expression.insert` - produce an ``INSERT`` statement - - :func:`~.expression.update` - produce an ``UPDATE`` statement - - """ - if args: - v = args[0] - else: - v = {} - - if self.parameters is None: - self.parameters = self._process_colparams(v) - self.parameters.update(kwargs) - else: - self.parameters = self.parameters.copy() - self.parameters.update(self._process_colparams(v)) - self.parameters.update(kwargs) - -class Insert(ValuesBase): - """Represent an INSERT construct. - - The :class:`.Insert` object is created using the :func:`~.expression.insert()` function. - - See also: - - :ref:`coretutorial_insert_expressions` - - """ - __visit_name__ = 'insert' - - _prefixes = () - - def __init__(self, - table, - values=None, - inline=False, - bind=None, - prefixes=None, - returning=None, - **kwargs): - ValuesBase.__init__(self, table, values) - self._bind = bind - self.select = None - self.inline = inline - self._returning = returning - if prefixes: - self._prefixes = tuple([_literal_as_text(p) for p in prefixes]) - - if kwargs: - self.kwargs = self._process_deprecated_kw(kwargs) - - def get_children(self, **kwargs): - if self.select is not None: - return self.select, - else: - return () - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self.parameters = self.parameters.copy() - - @_generative - def prefix_with(self, clause): - """Add a word or expression between INSERT and INTO. Generative. - - If multiple prefixes are supplied, they will be separated with - spaces. - - """ - clause = _literal_as_text(clause) - self._prefixes = self._prefixes + (clause,) - -class Update(ValuesBase): - """Represent an Update construct. - - The :class:`.Update` object is created using the :func:`update()` function. - - """ - __visit_name__ = 'update' - - def __init__(self, - table, - whereclause, - values=None, - inline=False, - bind=None, - returning=None, - **kwargs): - ValuesBase.__init__(self, table, values) - self._bind = bind - self._returning = returning - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - self.inline = inline - - if kwargs: - self.kwargs = self._process_deprecated_kw(kwargs) - - def get_children(self, **kwargs): - if self._whereclause is not None: - return self._whereclause, - else: - return () - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self._whereclause = clone(self._whereclause, **kw) - self.parameters = self.parameters.copy() - - @_generative - def where(self, whereclause): - """return a new update() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - if self._whereclause is not None: - self._whereclause = and_(self._whereclause, - _literal_as_text(whereclause)) - else: - self._whereclause = _literal_as_text(whereclause) - - @property - def _extra_froms(self): - # TODO: this could be made memoized - # if the memoization is reset on each generative call. - froms = [] - seen = set([self.table]) - - if self._whereclause is not None: - for item in _from_objects(self._whereclause): - if not seen.intersection(item._cloned_set): - froms.append(item) - seen.update(item._cloned_set) - - return froms - -class Delete(UpdateBase): - """Represent a DELETE construct. - - The :class:`.Delete` object is created using the :func:`delete()` function. - - """ - - __visit_name__ = 'delete' - - def __init__(self, - table, - whereclause, - bind=None, - returning =None, - **kwargs): - self._bind = bind - self.table = table - self._returning = returning - - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - - if kwargs: - self.kwargs = self._process_deprecated_kw(kwargs) - - def get_children(self, **kwargs): - if self._whereclause is not None: - return self._whereclause, - else: - return () - - @_generative - def where(self, whereclause): - """Add the given WHERE clause to a newly returned delete construct.""" - - if self._whereclause is not None: - self._whereclause = and_(self._whereclause, - _literal_as_text(whereclause)) - else: - self._whereclause = _literal_as_text(whereclause) - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self._whereclause = clone(self._whereclause, **kw) - -class _IdentifiedClause(Executable, ClauseElement): - - __visit_name__ = 'identified' - _execution_options = \ - Executable._execution_options.union({'autocommit': False}) - quote = None - - def __init__(self, ident): - self.ident = ident - -class SavepointClause(_IdentifiedClause): - __visit_name__ = 'savepoint' - -class RollbackToSavepointClause(_IdentifiedClause): - __visit_name__ = 'rollback_to_savepoint' - -class ReleaseSavepointClause(_IdentifiedClause): - __visit_name__ = 'release_savepoint' - - +_BindParamClause = BindParameter +_Label = Label +_SelectBase = SelectBase +_BinaryExpression = BinaryExpression +_Cast = Cast +_Null = Null +_False = False_ +_True = True_ +_TextClause = TextClause +_UnaryExpression = UnaryExpression +_Case = Case +_Tuple = Tuple +_Over = Over +_Generative = Generative +_TypeClause = TypeClause +_Extract = Extract +_Exists = Exists +_Grouping = Grouping +_FromGrouping = FromGrouping +_ScalarSelect = ScalarSelect diff --git a/libs/sqlalchemy/sql/functions.py b/libs/sqlalchemy/sql/functions.py index 95781d70..f300e241 100644 --- a/libs/sqlalchemy/sql/functions.py +++ b/libs/sqlalchemy/sql/functions.py @@ -1,36 +1,418 @@ # sql/functions.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import types as sqltypes, schema -from sqlalchemy.sql.expression import ( - ClauseList, Function, _literal_as_binds, text, _type_from_args - ) -from sqlalchemy.sql import operators -from sqlalchemy.sql.visitors import VisitableType +"""SQL function API, factories, and built-in functions. + +""" +from . import sqltypes, schema +from .base import Executable +from .elements import ClauseList, Cast, Extract, _literal_as_binds, \ + literal_column, _type_from_args, ColumnElement, _clone,\ + Over, BindParameter +from .selectable import FromClause, Select + +from . import operators +from .visitors import VisitableType +from .. import util + +_registry = util.defaultdict(dict) + + +def register_function(identifier, fn, package="_default"): + """Associate a callable with a particular func. name. + + This is normally called by _GenericMeta, but is also + available by itself so that a non-Function construct + can be associated with the :data:`.func` accessor (i.e. + CAST, EXTRACT). + + """ + reg = _registry[package] + reg[identifier] = fn + + +class FunctionElement(Executable, ColumnElement, FromClause): + """Base for SQL function-oriented constructs. + + .. seealso:: + + :class:`.Function` - named SQL function. + + :data:`.func` - namespace which produces registered or ad-hoc + :class:`.Function` instances. + + :class:`.GenericFunction` - allows creation of registered function + types. + + """ + + packagenames = () + + def __init__(self, *clauses, **kwargs): + """Construct a :class:`.FunctionElement`. + """ + args = [_literal_as_binds(c, self.name) for c in clauses] + self.clause_expr = ClauseList( + operator=operators.comma_op, + group_contents=True, *args).\ + self_group() + + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_function(self, multiparams, params) + + @property + def columns(self): + """Fulfill the 'columns' contract of :class:`.ColumnElement`. + + Returns a single-element list consisting of this object. + + """ + return [self] + + @util.memoized_property + def clauses(self): + """Return the underlying :class:`.ClauseList` which contains + the arguments for this :class:`.FunctionElement`. + + """ + return self.clause_expr.element + + def over(self, partition_by=None, order_by=None): + """Produce an OVER clause against this function. + + Used against aggregate or so-called "window" functions, + for database backends that support window functions. + + The expression:: + + func.row_number().over(order_by='x') + + is shorthand for:: + + from sqlalchemy import over + over(func.row_number(), order_by='x') + + See :func:`~.expression.over` for a full description. + + .. versionadded:: 0.7 + + """ + return Over(self, partition_by=partition_by, order_by=order_by) + + @property + def _from_objects(self): + return self.clauses._from_objects + + def get_children(self, **kwargs): + return self.clause_expr, + + def _copy_internals(self, clone=_clone, **kw): + self.clause_expr = clone(self.clause_expr, **kw) + self._reset_exported() + FunctionElement.clauses._reset(self) + + def select(self): + """Produce a :func:`~.expression.select` construct + against this :class:`.FunctionElement`. + + This is shorthand for:: + + s = select([function_element]) + + """ + s = Select([self]) + if self._execution_options: + s = s.execution_options(**self._execution_options) + return s + + def scalar(self): + """Execute this :class:`.FunctionElement` against an embedded + 'bind' and return a scalar value. + + This first calls :meth:`~.FunctionElement.select` to + produce a SELECT construct. + + Note that :class:`.FunctionElement` can be passed to + the :meth:`.Connectable.scalar` method of :class:`.Connection` + or :class:`.Engine`. + + """ + return self.select().execute().scalar() + + def execute(self): + """Execute this :class:`.FunctionElement` against an embedded + 'bind'. + + This first calls :meth:`~.FunctionElement.select` to + produce a SELECT construct. + + Note that :class:`.FunctionElement` can be passed to + the :meth:`.Connectable.execute` method of :class:`.Connection` + or :class:`.Engine`. + + """ + return self.select().execute() + + def _bind_param(self, operator, obj): + return BindParameter(None, obj, _compared_to_operator=operator, + _compared_to_type=self.type, unique=True) + + +class _FunctionGenerator(object): + """Generate :class:`.Function` objects based on getattr calls.""" + + def __init__(self, **opts): + self.__names = [] + self.opts = opts + + def __getattr__(self, name): + # passthru __ attributes; fixes pydoc + if name.startswith('__'): + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) + + elif name.endswith('_'): + name = name[0:-1] + f = _FunctionGenerator(**self.opts) + f.__names = list(self.__names) + [name] + return f + + def __call__(self, *c, **kwargs): + o = self.opts.copy() + o.update(kwargs) + + tokens = len(self.__names) + + if tokens == 2: + package, fname = self.__names + elif tokens == 1: + package, fname = "_default", self.__names[0] + else: + package = None + + if package is not None: + func = _registry[package].get(fname) + if func is not None: + return func(*c, **o) + + return Function(self.__names[-1], + packagenames=self.__names[0:-1], *c, **o) + + +func = _FunctionGenerator() +"""Generate SQL function expressions. + + :data:`.func` is a special object instance which generates SQL + functions based on name-based attributes, e.g.:: + + >>> print func.count(1) + count(:param_1) + + The element is a column-oriented SQL element like any other, and is + used in that way:: + + >>> print select([func.count(table.c.id)]) + SELECT count(sometable.id) FROM sometable + + Any name can be given to :data:`.func`. If the function name is unknown to + SQLAlchemy, it will be rendered exactly as is. For common SQL functions + which SQLAlchemy is aware of, the name may be interpreted as a *generic + function* which will be compiled appropriately to the target database:: + + >>> print func.current_timestamp() + CURRENT_TIMESTAMP + + To call functions which are present in dot-separated packages, + specify them in the same manner:: + + >>> print func.stats.yield_curve(5, 10) + stats.yield_curve(:yield_curve_1, :yield_curve_2) + + SQLAlchemy can be made aware of the return type of functions to enable + type-specific lexical and result-based behavior. For example, to ensure + that a string-based function returns a Unicode value and is similarly + treated as a string in expressions, specify + :class:`~sqlalchemy.types.Unicode` as the type: + + >>> print func.my_string(u'hi', type_=Unicode) + ' ' + \ + ... func.my_string(u'there', type_=Unicode) + my_string(:my_string_1) || :my_string_2 || my_string(:my_string_3) + + The object returned by a :data:`.func` call is usually an instance of + :class:`.Function`. + This object meets the "column" interface, including comparison and labeling + functions. The object can also be passed the :meth:`~.Connectable.execute` + method of a :class:`.Connection` or :class:`.Engine`, where it will be + wrapped inside of a SELECT statement first:: + + print connection.execute(func.current_timestamp()).scalar() + + In a few exception cases, the :data:`.func` accessor + will redirect a name to a built-in expression such as :func:`.cast` + or :func:`.extract`, as these names have well-known meaning + but are not exactly the same as "functions" from a SQLAlchemy + perspective. + + .. versionadded:: 0.8 :data:`.func` can return non-function expression + constructs for common quasi-functional names like :func:`.cast` + and :func:`.extract`. + + Functions which are interpreted as "generic" functions know how to + calculate their return type automatically. For a listing of known generic + functions, see :ref:`generic_functions`. + +""" + +modifier = _FunctionGenerator(group=False) + +class Function(FunctionElement): + """Describe a named SQL function. + + See the superclass :class:`.FunctionElement` for a description + of public methods. + + .. seealso:: + + :data:`.func` - namespace which produces registered or ad-hoc + :class:`.Function` instances. + + :class:`.GenericFunction` - allows creation of registered function + types. + + """ + + __visit_name__ = 'function' + + def __init__(self, name, *clauses, **kw): + """Construct a :class:`.Function`. + + The :data:`.func` construct is normally used to construct + new :class:`.Function` instances. + + """ + self.packagenames = kw.pop('packagenames', None) or [] + self.name = name + self._bind = kw.get('bind', None) + self.type = sqltypes.to_instance(kw.get('type_', None)) + + FunctionElement.__init__(self, *clauses, **kw) + + def _bind_param(self, operator, obj): + return BindParameter(self.name, obj, + _compared_to_operator=operator, + _compared_to_type=self.type, + unique=True) + class _GenericMeta(VisitableType): - def __call__(self, *args, **kwargs): - args = [_literal_as_binds(c) for c in args] - return type.__call__(self, *args, **kwargs) + def __init__(cls, clsname, bases, clsdict): + cls.name = name = clsdict.get('name', clsname) + cls.identifier = identifier = clsdict.get('identifier', name) + package = clsdict.pop('package', '_default') + # legacy + if '__return_type__' in clsdict: + cls.type = clsdict['__return_type__'] + register_function(identifier, cls, package) + super(_GenericMeta, cls).__init__(clsname, bases, clsdict) -class GenericFunction(Function): - __metaclass__ = _GenericMeta - def __init__(self, type_=None, args=(), **kwargs): +class GenericFunction(util.with_metaclass(_GenericMeta, Function)): + """Define a 'generic' function. + + A generic function is a pre-established :class:`.Function` + class that is instantiated automatically when called + by name from the :data:`.func` attribute. Note that + calling any name from :data:`.func` has the effect that + a new :class:`.Function` instance is created automatically, + given that name. The primary use case for defining + a :class:`.GenericFunction` class is so that a function + of a particular name may be given a fixed return type. + It can also include custom argument parsing schemes as well + as additional methods. + + Subclasses of :class:`.GenericFunction` are automatically + registered under the name of the class. For + example, a user-defined function ``as_utc()`` would + be available immediately:: + + from sqlalchemy.sql.functions import GenericFunction + from sqlalchemy.types import DateTime + + class as_utc(GenericFunction): + type = DateTime + + print select([func.as_utc()]) + + User-defined generic functions can be organized into + packages by specifying the "package" attribute when defining + :class:`.GenericFunction`. Third party libraries + containing many functions may want to use this in order + to avoid name conflicts with other systems. For example, + if our ``as_utc()`` function were part of a package + "time":: + + class as_utc(GenericFunction): + type = DateTime + package = "time" + + The above function would be available from :data:`.func` + using the package name ``time``:: + + print select([func.time.as_utc()]) + + A final option is to allow the function to be accessed + from one name in :data:`.func` but to render as a different name. + The ``identifier`` attribute will override the name used to + access the function as loaded from :data:`.func`, but will retain + the usage of ``name`` as the rendered name:: + + class GeoBuffer(GenericFunction): + type = Geometry + package = "geo" + name = "ST_Buffer" + identifier = "buffer" + + The above function will render as follows:: + + >>> print func.geo.buffer() + ST_Buffer() + + .. versionadded:: 0.8 :class:`.GenericFunction` now supports + automatic registration of new functions as well as package + and custom naming support. + + .. versionchanged:: 0.8 The attribute name ``type`` is used + to specify the function's return type at the class level. + Previously, the name ``__return_type__`` was used. This + name is still recognized for backwards-compatibility. + + """ + + coerce_arguments = True + + def __init__(self, *args, **kwargs): + parsed_args = kwargs.pop('_parsed_args', None) + if parsed_args is None: + parsed_args = [_literal_as_binds(c) for c in args] self.packagenames = [] - self.name = self.__class__.__name__ self._bind = kwargs.get('bind', None) self.clause_expr = ClauseList( operator=operators.comma_op, - group_contents=True, *args).self_group() + group_contents=True, *parsed_args).self_group() self.type = sqltypes.to_instance( - type_ or getattr(self, '__return_type__', None)) + kwargs.pop("type_", None) or getattr(self, 'type', None)) -class next_value(Function): +register_function("cast", Cast) +register_function("extract", Extract) + + +class next_value(GenericFunction): """Represent the 'next value', given a :class:`.Sequence` as it's single argument. @@ -52,83 +434,101 @@ class next_value(Function): def _from_objects(self): return [] + class AnsiFunction(GenericFunction): def __init__(self, **kwargs): GenericFunction.__init__(self, **kwargs) + class ReturnTypeFromArgs(GenericFunction): """Define a function whose return type is the same as its arguments.""" def __init__(self, *args, **kwargs): + args = [_literal_as_binds(c) for c in args] kwargs.setdefault('type_', _type_from_args(args)) - GenericFunction.__init__(self, args=args, **kwargs) + kwargs['_parsed_args'] = args + GenericFunction.__init__(self, *args, **kwargs) + class coalesce(ReturnTypeFromArgs): pass + class max(ReturnTypeFromArgs): pass + class min(ReturnTypeFromArgs): pass + class sum(ReturnTypeFromArgs): pass class now(GenericFunction): - __return_type__ = sqltypes.DateTime + type = sqltypes.DateTime + class concat(GenericFunction): - __return_type__ = sqltypes.String - def __init__(self, *args, **kwargs): - GenericFunction.__init__(self, args=args, **kwargs) + type = sqltypes.String + class char_length(GenericFunction): - __return_type__ = sqltypes.Integer + type = sqltypes.Integer def __init__(self, arg, **kwargs): - GenericFunction.__init__(self, args=[arg], **kwargs) + GenericFunction.__init__(self, arg, **kwargs) + class random(GenericFunction): - def __init__(self, *args, **kwargs): - kwargs.setdefault('type_', None) - GenericFunction.__init__(self, args=args, **kwargs) + pass + class count(GenericFunction): - """The ANSI COUNT aggregate function. With no arguments, emits COUNT \*.""" + """The ANSI COUNT aggregate function. With no arguments, + emits COUNT \*. - __return_type__ = sqltypes.Integer + """ + type = sqltypes.Integer def __init__(self, expression=None, **kwargs): if expression is None: - expression = text('*') - GenericFunction.__init__(self, args=(expression,), **kwargs) + expression = literal_column('*') + GenericFunction.__init__(self, expression, **kwargs) + class current_date(AnsiFunction): - __return_type__ = sqltypes.Date + type = sqltypes.Date + class current_time(AnsiFunction): - __return_type__ = sqltypes.Time + type = sqltypes.Time + class current_timestamp(AnsiFunction): - __return_type__ = sqltypes.DateTime + type = sqltypes.DateTime + class current_user(AnsiFunction): - __return_type__ = sqltypes.String + type = sqltypes.String + class localtime(AnsiFunction): - __return_type__ = sqltypes.DateTime + type = sqltypes.DateTime + class localtimestamp(AnsiFunction): - __return_type__ = sqltypes.DateTime + type = sqltypes.DateTime + class session_user(AnsiFunction): - __return_type__ = sqltypes.String + type = sqltypes.String + class sysdate(AnsiFunction): - __return_type__ = sqltypes.DateTime + type = sqltypes.DateTime + class user(AnsiFunction): - __return_type__ = sqltypes.String - + type = sqltypes.String diff --git a/libs/sqlalchemy/sql/operators.py b/libs/sqlalchemy/sql/operators.py index 9e796506..d7ec977a 100644 --- a/libs/sqlalchemy/sql/operators.py +++ b/libs/sqlalchemy/sql/operators.py @@ -1,5 +1,5 @@ # sql/operators.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -9,21 +9,29 @@ """Defines operators used in SQL expressions.""" +from .. import util + + from operator import ( - and_, or_, inv, add, mul, sub, mod, truediv, lt, le, ne, gt, ge, eq, neg + and_, or_, inv, add, mul, sub, mod, truediv, lt, le, ne, gt, ge, eq, neg, + getitem, lshift, rshift ) -# Py2K -from operator import (div,) -# end Py2K +if util.py2k: + from operator import div +else: + div = truediv + -from sqlalchemy.util import symbol class Operators(object): """Base of comparison and logical operators. - Implements base methods :meth:`operate` and :meth:`reverse_operate`, - as well as :meth:`__and__`, :meth:`__or__`, :meth:`__invert__`. + Implements base methods :meth:`~sqlalchemy.sql.operators.Operators.operate` and + :meth:`~sqlalchemy.sql.operators.Operators.reverse_operate`, as well as + :meth:`~sqlalchemy.sql.operators.Operators.__and__`, + :meth:`~sqlalchemy.sql.operators.Operators.__or__`, + :meth:`~sqlalchemy.sql.operators.Operators.__invert__`. Usually is used via its most common subclass :class:`.ColumnOperators`. @@ -94,7 +102,7 @@ class Operators(object): """ return self.operate(inv) - def op(self, opstring): + def op(self, opstring, precedence=0): """produce a generic operator function. e.g.:: @@ -105,21 +113,37 @@ class Operators(object): somecolumn * 5 - :param operator: a string which will be output as the infix operator - between this :class:`.ClauseElement` and the expression passed to the - generated function. - This function can also be used to make bitwise operators explicit. For example:: somecolumn.op('&')(0xff) - is a bitwise AND of the value in somecolumn. + is a bitwise AND of the value in ``somecolumn``. + + :param operator: a string which will be output as the infix operator + between this element and the expression passed to the + generated function. + + :param precedence: precedence to apply to the operator, when + parenthesizing expressions. A lower number will cause the expression + to be parenthesized when applied against another operator with + higher precedence. The default value of ``0`` is lower than all + operators except for the comma (``,``) and ``AS`` operators. + A value of 100 will be higher or equal to all operators, and -100 + will be lower than or equal to all operators. + + .. versionadded:: 0.8 - added the 'precedence' argument. + + .. seealso:: + + :ref:`types_operators` """ - def _op(b): - return self.operate(op, opstring, b) - return _op + operator = custom_op(opstring, precedence) + + def against(other): + return operator(self, other) + return against def operate(self, op, *other, **kwargs): """Operate on an argument. @@ -155,11 +179,48 @@ class Operators(object): """ raise NotImplementedError(str(op)) -class ColumnOperators(Operators): - """Defines comparison and math operations. - By default all methods call down to - :meth:`Operators.operate` or :meth:`Operators.reverse_operate` +class custom_op(object): + """Represent a 'custom' operator. + + :class:`.custom_op` is normally instantitated when the + :meth:`.ColumnOperators.op` method is used to create a + custom operator callable. The class can also be used directly + when programmatically constructing expressions. E.g. + to represent the "factorial" operation:: + + from sqlalchemy.sql import UnaryExpression + from sqlalchemy.sql import operators + from sqlalchemy import Numeric + + unary = UnaryExpression(table.c.somecolumn, + modifier=operators.custom_op("!"), + type_=Numeric) + + """ + __name__ = 'custom_op' + + def __init__(self, opstring, precedence=0): + self.opstring = opstring + self.precedence = precedence + + def __eq__(self, other): + return isinstance(other, custom_op) and \ + other.opstring == self.opstring + + def __hash__(self): + return id(self) + + def __call__(self, left, right, **kw): + return left.operate(self, right, **kw) + + +class ColumnOperators(Operators): + """Defines boolean, comparison, and other operators for + :class:`.ColumnElement` expressions. + + By default, all methods call down to + :meth:`.operate` or :meth:`.reverse_operate`, passing in the appropriate operator function from the Python builtin ``operator`` module or a SQLAlchemy-specific operator function from @@ -174,15 +235,21 @@ class ColumnOperators(Operators): def eq(a, b): return a == b - A SQLAlchemy construct like :class:`.ColumnElement` ultimately + The core column expression unit :class:`.ColumnElement` overrides :meth:`.Operators.operate` and others - to return further :class:`.ClauseElement` constructs, + to return further :class:`.ColumnElement` constructs, so that the ``==`` operation above is replaced by a clause construct. - The docstrings here will describe column-oriented - behavior of each operator. For ORM-based operators - on related objects and collections, see :class:`.RelationshipProperty.Comparator`. + See also: + + :ref:`types_operators` + + :attr:`.TypeEngine.comparator_factory` + + :class:`.ColumnOperators` + + :class:`.PropComparator` """ @@ -249,6 +316,33 @@ class ColumnOperators(Operators): """ return self.operate(neg) + def __getitem__(self, index): + """Implement the [] operator. + + This can be used by some database-specific types + such as Postgresql ARRAY and HSTORE. + + """ + return self.operate(getitem, index) + + def __lshift__(self, other): + """implement the << operator. + + Not used by SQLAlchemy core, this is provided + for custom operator systems which want to use + << as an extension point. + """ + return self.operate(lshift, other) + + def __rshift__(self, other): + """implement the >> operator. + + Not used by SQLAlchemy core, this is provided + for custom operator systems which want to use + >> as an extension point. + """ + return self.operate(rshift, other) + def concat(self, other): """Implement the 'concat' operator. @@ -263,6 +357,20 @@ class ColumnOperators(Operators): In a column context, produces the clause ``a LIKE other``. + E.g.:: + + select([sometable]).where(sometable.c.column.like("%foobar%")) + + :param other: expression to be compared + :param escape: optional escape character, renders the ``ESCAPE`` + keyword, e.g.:: + + somecolumn.like("foo/%bar", escape="/") + + .. seealso:: + + :meth:`.ColumnOperators.ilike` + """ return self.operate(like_op, other, escape=escape) @@ -271,6 +379,20 @@ class ColumnOperators(Operators): In a column context, produces the clause ``a ILIKE other``. + E.g.:: + + select([sometable]).where(sometable.c.column.ilike("%foobar%")) + + :param other: expression to be compared + :param escape: optional escape character, renders the ``ESCAPE`` + keyword, e.g.:: + + somecolumn.ilike("foo/%bar", escape="/") + + .. seealso:: + + :meth:`.ColumnOperators.like` + """ return self.operate(ilike_op, other, escape=escape) @@ -284,6 +406,51 @@ class ColumnOperators(Operators): """ return self.operate(in_op, other) + def notin_(self, other): + """implement the ``NOT IN`` operator. + + This is equivalent to using negation with :meth:`.ColumnOperators.in_`, + i.e. ``~x.in_(y)``. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.ColumnOperators.in_` + + """ + return self.operate(notin_op, other) + + def notlike(self, other, escape=None): + """implement the ``NOT LIKE`` operator. + + This is equivalent to using negation with + :meth:`.ColumnOperators.like`, i.e. ``~x.like(y)``. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.ColumnOperators.like` + + """ + return self.operate(notlike_op, other, escape=escape) + + def notilike(self, other, escape=None): + """implement the ``NOT ILIKE`` operator. + + This is equivalent to using negation with + :meth:`.ColumnOperators.ilike`, i.e. ``~x.ilike(y)``. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.ColumnOperators.ilike` + + """ + return self.operate(notilike_op, other, escape=escape) + def is_(self, other): """Implement the ``IS`` operator. @@ -376,7 +543,7 @@ class ColumnOperators(Operators): def __radd__(self, other): """Implement the ``+`` operator in reverse. - See :meth:`__add__`. + See :meth:`.ColumnOperators.__add__`. """ return self.reverse_operate(add, other) @@ -384,7 +551,7 @@ class ColumnOperators(Operators): def __rsub__(self, other): """Implement the ``-`` operator in reverse. - See :meth:`__sub__`. + See :meth:`.ColumnOperators.__sub__`. """ return self.reverse_operate(sub, other) @@ -392,7 +559,7 @@ class ColumnOperators(Operators): def __rmul__(self, other): """Implement the ``*`` operator in reverse. - See :meth:`__mul__`. + See :meth:`.ColumnOperators.__mul__`. """ return self.reverse_operate(mul, other) @@ -400,7 +567,7 @@ class ColumnOperators(Operators): def __rdiv__(self, other): """Implement the ``/`` operator in reverse. - See :meth:`__div__`. + See :meth:`.ColumnOperators.__div__`. """ return self.reverse_operate(div, other) @@ -411,7 +578,10 @@ class ColumnOperators(Operators): return self.operate(between_op, cleft, cright) def distinct(self): - """Produce a :func:`~.expression.distinct` clause against the parent object.""" + """Produce a :func:`~.expression.distinct` clause against the + parent object. + + """ return self.operate(distinct_op) def __add__(self, other): @@ -421,7 +591,7 @@ class ColumnOperators(Operators): if the parent object has non-string affinity. If the parent object has a string affinity, produces the concatenation operator, ``a || b`` - - see :meth:`concat`. + see :meth:`.ColumnOperators.concat`. """ return self.operate(add, other) @@ -469,144 +639,216 @@ class ColumnOperators(Operators): def __rtruediv__(self, other): """Implement the ``//`` operator in reverse. - See :meth:`__truediv__`. + See :meth:`.ColumnOperators.__truediv__`. """ return self.reverse_operate(truediv, other) + def from_(): raise NotImplementedError() + def as_(): raise NotImplementedError() + def exists(): raise NotImplementedError() + +def istrue(a): + raise NotImplementedError() + +def isfalse(a): + raise NotImplementedError() + def is_(a, b): return a.is_(b) + def isnot(a, b): return a.isnot(b) + def collate(a, b): return a.collate(b) + def op(a, opstring, b): return a.op(opstring)(b) + def like_op(a, b, escape=None): return a.like(b, escape=escape) + def notlike_op(a, b, escape=None): - return ~a.like(b, escape=escape) + return a.notlike(b, escape=escape) + def ilike_op(a, b, escape=None): return a.ilike(b, escape=escape) + def notilike_op(a, b, escape=None): - return ~a.ilike(b, escape=escape) + return a.notilike(b, escape=escape) + def between_op(a, b, c): return a.between(b, c) + def in_op(a, b): return a.in_(b) + def notin_op(a, b): - return ~a.in_(b) + return a.notin_(b) + def distinct_op(a): return a.distinct() + def startswith_op(a, b, escape=None): return a.startswith(b, escape=escape) + +def notstartswith_op(a, b, escape=None): + return ~a.startswith(b, escape=escape) + + def endswith_op(a, b, escape=None): return a.endswith(b, escape=escape) + +def notendswith_op(a, b, escape=None): + return ~a.endswith(b, escape=escape) + + def contains_op(a, b, escape=None): return a.contains(b, escape=escape) + +def notcontains_op(a, b, escape=None): + return ~a.contains(b, escape=escape) + + def match_op(a, b): return a.match(b) + def comma_op(a, b): raise NotImplementedError() + def concat_op(a, b): return a.concat(b) + def desc_op(a): return a.desc() + def asc_op(a): return a.asc() + def nullsfirst_op(a): return a.nullsfirst() + def nullslast_op(a): return a.nullslast() + _commutative = set([eq, ne, add, mul]) +_comparison = set([eq, ne, lt, gt, ge, le, between_op]) + + +def is_comparison(op): + return op in _comparison + + def is_commutative(op): return op in _commutative + def is_ordering_modifier(op): return op in (asc_op, desc_op, nullsfirst_op, nullslast_op) _associative = _commutative.union([concat_op, and_, or_]) +_natural_self_precedent = _associative.union([getitem]) +"""Operators where if we have (a op b) op c, we don't want to +parenthesize (a op b). -_smallest = symbol('_smallest') -_largest = symbol('_largest') +""" + +_asbool = util.symbol('_asbool', canonical=-10) +_smallest = util.symbol('_smallest', canonical=-100) +_largest = util.symbol('_largest', canonical=100) _PRECEDENCE = { from_: 15, - mul: 7, - truediv: 7, - # Py2K - div: 7, - # end Py2K - mod: 7, - neg: 7, - add: 6, - sub: 6, + getitem: 15, + mul: 8, + truediv: 8, + div: 8, + mod: 8, + neg: 8, + add: 7, + sub: 7, + concat_op: 6, match_op: 6, - ilike_op: 5, - notilike_op: 5, - like_op: 5, - notlike_op: 5, - in_op: 5, - notin_op: 5, - is_: 5, - isnot: 5, + + ilike_op: 6, + notilike_op: 6, + like_op: 6, + notlike_op: 6, + in_op: 6, + notin_op: 6, + + is_: 6, + isnot: 6, + eq: 5, ne: 5, gt: 5, lt: 5, ge: 5, le: 5, + between_op: 5, distinct_op: 5, inv: 5, + istrue: 5, + isfalse: 5, and_: 3, or_: 2, comma_op: -1, - collate: 7, + + desc_op: 3, + asc_op: 3, + collate: 4, + as_: -1, exists: 0, - _smallest: -1000, - _largest: 1000 + _asbool: -10, + _smallest: _smallest, + _largest: _largest } + def is_precedent(operator, against): - if operator is against and operator in _associative: + if operator is against and operator in _natural_self_precedent: return False else: - return (_PRECEDENCE.get(operator, _PRECEDENCE[_smallest]) <= - _PRECEDENCE.get(against, _PRECEDENCE[_largest])) + return (_PRECEDENCE.get(operator, + getattr(operator, 'precedence', _smallest)) <= + _PRECEDENCE.get(against, + getattr(against, 'precedence', _largest))) diff --git a/libs/sqlalchemy/sql/schema.py b/libs/sqlalchemy/sql/schema.py new file mode 100644 index 00000000..6ee92871 --- /dev/null +++ b/libs/sqlalchemy/sql/schema.py @@ -0,0 +1,3150 @@ +# sql/schema.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""The schema module provides the building blocks for database metadata. + +Each element within this module describes a database entity which can be +created and dropped, or is otherwise part of such an entity. Examples include +tables, columns, sequences, and indexes. + +All entities are subclasses of :class:`~sqlalchemy.schema.SchemaItem`, and as +defined in this module they are intended to be agnostic of any vendor-specific +constructs. + +A collection of entities are grouped into a unit called +:class:`~sqlalchemy.schema.MetaData`. MetaData serves as a logical grouping of +schema elements, and can also be associated with an actual database connection +such that operations involving the contained elements can contact the database +as needed. + +Two of the elements here also build upon their "syntactic" counterparts, which +are defined in :class:`~sqlalchemy.sql.expression.`, specifically +:class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.schema.Column`. +Since these objects are part of the SQL expression language, they are usable +as components in SQL expressions. + +""" + +import re +import inspect +from .. import exc, util, event, inspection +from .base import SchemaEventTarget +from . import visitors +from . import type_api +from .base import _bind_or_error, ColumnCollection +from .elements import ClauseElement, ColumnClause, _truncated_label, \ + _as_truncated, TextClause, _literal_as_text,\ + ColumnElement, _find_columns, quoted_name +from .selectable import TableClause +import collections +import sqlalchemy +from . import ddl + +RETAIN_SCHEMA = util.symbol('retain_schema') + + +def _get_table_key(name, schema): + if schema is None: + return name + else: + return schema + "." + name + + +def _validate_dialect_kwargs(kwargs, name): + # validate remaining kwargs that they all specify DB prefixes + + for k in kwargs: + m = re.match('^(.+?)_.*', k) + if m is None: + raise TypeError("Additional arguments should be " + "named _, got '%s'" % k) + +@inspection._self_inspects +class SchemaItem(SchemaEventTarget, visitors.Visitable): + """Base class for items that define a database schema.""" + + __visit_name__ = 'schema_item' + + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_default(self, multiparams, params) + + def _init_items(self, *args): + """Initialize the list of child items for this SchemaItem.""" + + for item in args: + if item is not None: + item._set_parent_with_dispatch(self) + + def get_children(self, **kwargs): + """used to allow SchemaVisitor access""" + return [] + + def __repr__(self): + return util.generic_repr(self) + + @property + @util.deprecated('0.9', 'Use ``.name.quote``') + def quote(self): + """Return the value of the ``quote`` flag passed + to this schema object, for those schema items which + have a ``name`` field. + + """ + + return self.name.quote + + @util.memoized_property + def info(self): + """Info dictionary associated with the object, allowing user-defined + data to be associated with this :class:`.SchemaItem`. + + The dictionary is automatically generated when first accessed. + It can also be specified in the constructor of some objects, + such as :class:`.Table` and :class:`.Column`. + + """ + return {} + + def _schema_item_copy(self, schema_item): + if 'info' in self.__dict__: + schema_item.info = self.info.copy() + schema_item.dispatch._update(self.dispatch) + return schema_item + + +class Table(SchemaItem, TableClause): + """Represent a table in a database. + + e.g.:: + + mytable = Table("mytable", metadata, + Column('mytable_id', Integer, primary_key=True), + Column('value', String(50)) + ) + + The :class:`.Table` object constructs a unique instance of itself based + on its name and optional schema name within the given + :class:`.MetaData` object. Calling the :class:`.Table` + constructor with the same name and same :class:`.MetaData` argument + a second time will return the *same* :class:`.Table` object - in this way + the :class:`.Table` constructor acts as a registry function. + + .. seealso:: + + :ref:`metadata_describing` - Introduction to database metadata + + Constructor arguments are as follows: + + :param name: The name of this table as represented in the database. + + The table name, along with the value of the ``schema`` parameter, + forms a key which uniquely identifies this :class:`.Table` within + the owning :class:`.MetaData` collection. + Additional calls to :class:`.Table` with the same name, metadata, + and schema name will return the same :class:`.Table` object. + + Names which contain no upper case characters + will be treated as case insensitive names, and will not be quoted + unless they are a reserved word or contain special characters. + A name with any number of upper case characters is considered + to be case sensitive, and will be sent as quoted. + + To enable unconditional quoting for the table name, specify the flag + ``quote=True`` to the constructor, or use the :class:`.quoted_name` + construct to specify the name. + + :param metadata: a :class:`.MetaData` object which will contain this + table. The metadata is used as a point of association of this table + with other tables which are referenced via foreign key. It also + may be used to associate this table with a particular + :class:`.Connectable`. + + :param \*args: Additional positional arguments are used primarily + to add the list of :class:`.Column` objects contained within this + table. Similar to the style of a CREATE TABLE statement, other + :class:`.SchemaItem` constructs may be added here, including + :class:`.PrimaryKeyConstraint`, and :class:`.ForeignKeyConstraint`. + + :param autoload: Defaults to False: the Columns for this table should + be reflected from the database. Usually there will be no Column + objects in the constructor if this property is set. + + :param autoload_replace: If ``True``, when using ``autoload=True`` + and ``extend_existing=True``, + replace ``Column`` objects already present in the ``Table`` that's + in the ``MetaData`` registry with + what's reflected. Otherwise, all existing columns will be + excluded from the reflection process. Note that this does + not impact ``Column`` objects specified in the same call to ``Table`` + which includes ``autoload``, those always take precedence. + Defaults to ``True``. + + .. versionadded:: 0.7.5 + + :param autoload_with: If autoload==True, this is an optional Engine + or Connection instance to be used for the table reflection. If + ``None``, the underlying MetaData's bound connectable will be used. + + :param extend_existing: When ``True``, indicates that if this + :class:`.Table` is already present in the given :class:`.MetaData`, + apply further arguments within the constructor to the existing + :class:`.Table`. + + If ``extend_existing`` or ``keep_existing`` are not set, an error is + raised if additional table modifiers are specified when + the given :class:`.Table` is already present in the :class:`.MetaData`. + + .. versionchanged:: 0.7.4 + ``extend_existing`` will work in conjunction + with ``autoload=True`` to run a new reflection operation against + the database; new :class:`.Column` objects will be produced + from database metadata to replace those existing with the same + name, and additional :class:`.Column` objects not present + in the :class:`.Table` will be added. + + As is always the case with ``autoload=True``, :class:`.Column` + objects can be specified in the same :class:`.Table` constructor, + which will take precedence. I.e.:: + + Table("mytable", metadata, + Column('y', Integer), + extend_existing=True, + autoload=True, + autoload_with=engine + ) + + The above will overwrite all columns within ``mytable`` which + are present in the database, except for ``y`` which will be used as is + from the above definition. If the ``autoload_replace`` flag + is set to False, no existing columns will be replaced. + + :param implicit_returning: True by default - indicates that + RETURNING can be used by default to fetch newly inserted primary key + values, for backends which support this. Note that + create_engine() also provides an implicit_returning flag. + + :param include_columns: A list of strings indicating a subset of + columns to be loaded via the ``autoload`` operation; table columns who + aren't present in this list will not be represented on the resulting + ``Table`` object. Defaults to ``None`` which indicates all columns + should be reflected. + + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + :param keep_existing: When ``True``, indicates that if this Table + is already present in the given :class:`.MetaData`, ignore + further arguments within the constructor to the existing + :class:`.Table`, and return the :class:`.Table` object as + originally created. This is to allow a function that wishes + to define a new :class:`.Table` on first call, but on + subsequent calls will return the same :class:`.Table`, + without any of the declarations (particularly constraints) + being applied a second time. Also see extend_existing. + + If extend_existing or keep_existing are not set, an error is + raised if additional table modifiers are specified when + the given :class:`.Table` is already present in the :class:`.MetaData`. + + :param listeners: A list of tuples of the form ``(, )`` + which will be passed to :func:`.event.listen` upon construction. + This alternate hook to :func:`.event.listen` allows the establishment + of a listener function specific to this :class:`.Table` before + the "autoload" process begins. Particularly useful for + the :meth:`.DDLEvents.column_reflect` event:: + + def listen_for_reflect(table, column_info): + "handle the column reflection event" + # ... + + t = Table( + 'sometable', + autoload=True, + listeners=[ + ('column_reflect', listen_for_reflect) + ]) + + :param mustexist: When ``True``, indicates that this Table must already + be present in the given :class:`.MetaData` collection, else + an exception is raised. + + :param prefixes: + A list of strings to insert after CREATE in the CREATE TABLE + statement. They will be separated by spaces. + + :param quote: Force quoting of this table's name on or off, corresponding + to ``True`` or ``False``. When left at its default of ``None``, + the column identifier will be quoted according to whether the name is + case sensitive (identifiers with at least one upper case character are + treated as case sensitive), or if it's a reserved word. This flag + is only needed to force quoting of a reserved word which is not known + by the SQLAlchemy dialect. + + :param quote_schema: same as 'quote' but applies to the schema identifier. + + :param schema: The schema name for this table, which is required if + the table resides in a schema other than the default selected schema + for the engine's database connection. Defaults to ``None``. + + The quoting rules for the schema name are the same as those for the + ``name`` parameter, in that quoting is applied for reserved words or + case-sensitive names; to enable unconditional quoting for the + schema name, specify the flag + ``quote_schema=True`` to the constructor, or use the :class:`.quoted_name` + construct to specify the name. + + + :param useexisting: Deprecated. Use extend_existing. + + """ + + __visit_name__ = 'table' + + def __new__(cls, *args, **kw): + if not args: + # python3k pickle seems to call this + return object.__new__(cls) + + try: + name, metadata, args = args[0], args[1], args[2:] + except IndexError: + raise TypeError("Table() takes at least two arguments") + + schema = kw.get('schema', None) + if schema is None: + schema = metadata.schema + keep_existing = kw.pop('keep_existing', False) + extend_existing = kw.pop('extend_existing', False) + if 'useexisting' in kw: + msg = "useexisting is deprecated. Use extend_existing." + util.warn_deprecated(msg) + if extend_existing: + msg = "useexisting is synonymous with extend_existing." + raise exc.ArgumentError(msg) + extend_existing = kw.pop('useexisting', False) + + if keep_existing and extend_existing: + msg = "keep_existing and extend_existing are mutually exclusive." + raise exc.ArgumentError(msg) + + mustexist = kw.pop('mustexist', False) + key = _get_table_key(name, schema) + if key in metadata.tables: + if not keep_existing and not extend_existing and bool(args): + raise exc.InvalidRequestError( + "Table '%s' is already defined for this MetaData " + "instance. Specify 'extend_existing=True' " + "to redefine " + "options and columns on an " + "existing Table object." % key) + table = metadata.tables[key] + if extend_existing: + table._init_existing(*args, **kw) + return table + else: + if mustexist: + raise exc.InvalidRequestError( + "Table '%s' not defined" % (key)) + table = object.__new__(cls) + table.dispatch.before_parent_attach(table, metadata) + metadata._add_table(name, schema, table) + try: + table._init(name, metadata, *args, **kw) + table.dispatch.after_parent_attach(table, metadata) + return table + except: + #metadata._remove_table(name, schema) + raise + + + @property + @util.deprecated('0.9', 'Use ``table.schema.quote``') + def quote_schema(self): + """Return the value of the ``quote_schema`` flag passed + to this :class:`.Table`. + """ + + return self.schema.quote + + def __init__(self, *args, **kw): + """Constructor for :class:`~.schema.Table`. + + This method is a no-op. See the top-level + documentation for :class:`~.schema.Table` + for constructor arguments. + + """ + # __init__ is overridden to prevent __new__ from + # calling the superclass constructor. + + def _init(self, name, metadata, *args, **kwargs): + super(Table, self).__init__(quoted_name(name, kwargs.pop('quote', None))) + self.metadata = metadata + + self.schema = kwargs.pop('schema', None) + if self.schema is None: + self.schema = metadata.schema + else: + quote_schema = kwargs.pop('quote_schema', None) + self.schema = quoted_name(self.schema, quote_schema) + + self.indexes = set() + self.constraints = set() + self._columns = ColumnCollection() + PrimaryKeyConstraint()._set_parent_with_dispatch(self) + self.foreign_keys = set() + self._extra_dependencies = set() + self.kwargs = {} + if self.schema is not None: + self.fullname = "%s.%s" % (self.schema, self.name) + else: + self.fullname = self.name + + autoload = kwargs.pop('autoload', False) + autoload_with = kwargs.pop('autoload_with', None) + # this argument is only used with _init_existing() + kwargs.pop('autoload_replace', True) + include_columns = kwargs.pop('include_columns', None) + + self.implicit_returning = kwargs.pop('implicit_returning', True) + + if 'info' in kwargs: + self.info = kwargs.pop('info') + if 'listeners' in kwargs: + listeners = kwargs.pop('listeners') + for evt, fn in listeners: + event.listen(self, evt, fn) + + self._prefixes = kwargs.pop('prefixes', []) + + self._extra_kwargs(**kwargs) + + # load column definitions from the database if 'autoload' is defined + # we do it after the table is in the singleton dictionary to support + # circular foreign keys + if autoload: + self._autoload(metadata, autoload_with, include_columns) + + # initialize all the column, etc. objects. done after reflection to + # allow user-overrides + self._init_items(*args) + + def _autoload(self, metadata, autoload_with, include_columns, + exclude_columns=()): + if self.primary_key.columns: + PrimaryKeyConstraint(*[ + c for c in self.primary_key.columns + if c.key in exclude_columns + ])._set_parent_with_dispatch(self) + + if autoload_with: + autoload_with.run_callable( + autoload_with.dialect.reflecttable, + self, include_columns, exclude_columns + ) + else: + bind = _bind_or_error(metadata, + msg="No engine is bound to this Table's MetaData. " + "Pass an engine to the Table via " + "autoload_with=, " + "or associate the MetaData with an engine via " + "metadata.bind=") + bind.run_callable( + bind.dialect.reflecttable, + self, include_columns, exclude_columns + ) + + @property + def _sorted_constraints(self): + """Return the set of constraints as a list, sorted by creation + order. + + """ + return sorted(self.constraints, key=lambda c: c._creation_order) + + def _init_existing(self, *args, **kwargs): + autoload = kwargs.pop('autoload', False) + autoload_with = kwargs.pop('autoload_with', None) + autoload_replace = kwargs.pop('autoload_replace', True) + schema = kwargs.pop('schema', None) + if schema and schema != self.schema: + raise exc.ArgumentError( + "Can't change schema of existing table from '%s' to '%s'", + (self.schema, schema)) + + include_columns = kwargs.pop('include_columns', None) + + if include_columns is not None: + for c in self.c: + if c.name not in include_columns: + self._columns.remove(c) + + for key in ('quote', 'quote_schema'): + if key in kwargs: + raise exc.ArgumentError( + "Can't redefine 'quote' or 'quote_schema' arguments") + + if 'info' in kwargs: + self.info = kwargs.pop('info') + + if autoload: + if not autoload_replace: + exclude_columns = [c.name for c in self.c] + else: + exclude_columns = () + self._autoload( + self.metadata, autoload_with, include_columns, exclude_columns) + + self._extra_kwargs(**kwargs) + self._init_items(*args) + + def _extra_kwargs(self, **kwargs): + # validate remaining kwargs that they all specify DB prefixes + _validate_dialect_kwargs(kwargs, "Table") + self.kwargs.update(kwargs) + + def _init_collections(self): + pass + + @util.memoized_property + def _autoincrement_column(self): + for col in self.primary_key: + if col.autoincrement and \ + col.type._type_affinity is not None and \ + issubclass(col.type._type_affinity, type_api.INTEGERTYPE._type_affinity) and \ + (not col.foreign_keys or col.autoincrement == 'ignore_fk') and \ + isinstance(col.default, (type(None), Sequence)) and \ + (col.server_default is None or col.server_default.reflected): + return col + + @property + def key(self): + """Return the 'key' for this :class:`.Table`. + + This value is used as the dictionary key within the + :attr:`.MetaData.tables` collection. It is typically the same + as that of :attr:`.Table.name` for a table with no :attr:`.Table.schema` + set; otherwise it is typically of the form ``schemaname.tablename``. + + """ + return _get_table_key(self.name, self.schema) + + def __repr__(self): + return "Table(%s)" % ', '.join( + [repr(self.name)] + [repr(self.metadata)] + + [repr(x) for x in self.columns] + + ["%s=%s" % (k, repr(getattr(self, k))) for k in ['schema']]) + + def __str__(self): + return _get_table_key(self.description, self.schema) + + @property + def bind(self): + """Return the connectable associated with this Table.""" + + return self.metadata and self.metadata.bind or None + + def add_is_dependent_on(self, table): + """Add a 'dependency' for this Table. + + This is another Table object which must be created + first before this one can, or dropped after this one. + + Usually, dependencies between tables are determined via + ForeignKey objects. However, for other situations that + create dependencies outside of foreign keys (rules, inheriting), + this method can manually establish such a link. + + """ + self._extra_dependencies.add(table) + + def append_column(self, column): + """Append a :class:`~.schema.Column` to this :class:`~.schema.Table`. + + The "key" of the newly added :class:`~.schema.Column`, i.e. the + value of its ``.key`` attribute, will then be available + in the ``.c`` collection of this :class:`~.schema.Table`, and the + column definition will be included in any CREATE TABLE, SELECT, + UPDATE, etc. statements generated from this :class:`~.schema.Table` + construct. + + Note that this does **not** change the definition of the table + as it exists within any underlying database, assuming that + table has already been created in the database. Relational + databases support the addition of columns to existing tables + using the SQL ALTER command, which would need to be + emitted for an already-existing table that doesn't contain + the newly added column. + + """ + + column._set_parent_with_dispatch(self) + + def append_constraint(self, constraint): + """Append a :class:`~.schema.Constraint` to this + :class:`~.schema.Table`. + + This has the effect of the constraint being included in any + future CREATE TABLE statement, assuming specific DDL creation + events have not been associated with the given + :class:`~.schema.Constraint` object. + + Note that this does **not** produce the constraint within the + relational database automatically, for a table that already exists + in the database. To add a constraint to an + existing relational database table, the SQL ALTER command must + be used. SQLAlchemy also provides the + :class:`.AddConstraint` construct which can produce this SQL when + invoked as an executable clause. + + """ + + constraint._set_parent_with_dispatch(self) + + def append_ddl_listener(self, event_name, listener): + """Append a DDL event listener to this ``Table``. + + .. deprecated:: 0.7 + See :class:`.DDLEvents`. + + """ + + def adapt_listener(target, connection, **kw): + listener(event_name, target, connection) + + event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) + + def _set_parent(self, metadata): + metadata._add_table(self.name, self.schema, self) + self.metadata = metadata + + def get_children(self, column_collections=True, + schema_visitor=False, **kw): + if not schema_visitor: + return TableClause.get_children( + self, column_collections=column_collections, **kw) + else: + if column_collections: + return list(self.columns) + else: + return [] + + def exists(self, bind=None): + """Return True if this table exists.""" + + if bind is None: + bind = _bind_or_error(self) + + return bind.run_callable(bind.dialect.has_table, + self.name, schema=self.schema) + + def create(self, bind=None, checkfirst=False): + """Issue a ``CREATE`` statement for this + :class:`.Table`, using the given :class:`.Connectable` + for connectivity. + + .. seealso:: + + :meth:`.MetaData.create_all`. + + """ + + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaGenerator, + self, + checkfirst=checkfirst) + + def drop(self, bind=None, checkfirst=False): + """Issue a ``DROP`` statement for this + :class:`.Table`, using the given :class:`.Connectable` + for connectivity. + + .. seealso:: + + :meth:`.MetaData.drop_all`. + + """ + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaDropper, + self, + checkfirst=checkfirst) + + def tometadata(self, metadata, schema=RETAIN_SCHEMA): + """Return a copy of this :class:`.Table` associated with a different + :class:`.MetaData`. + + E.g.:: + + some_engine = create_engine("sqlite:///some.db") + + # create two metadata + meta1 = MetaData() + meta2 = MetaData() + + # load 'users' from the sqlite engine + users_table = Table('users', meta1, autoload=True, + autoload_with=some_engine) + + # create the same Table object for the plain metadata + users_table_2 = users_table.tometadata(meta2) + + :param metadata: Target :class:`.MetaData` object. + :param schema: Optional string name of a target schema, or + ``None`` for no schema. The :class:`.Table` object will be + given this schema name upon copy. Defaults to the special + symbol :attr:`.RETAIN_SCHEMA` which indicates no change should be + made to the schema name of the resulting :class:`.Table`. + + """ + + if schema is RETAIN_SCHEMA: + schema = self.schema + elif schema is None: + schema = metadata.schema + key = _get_table_key(self.name, schema) + if key in metadata.tables: + util.warn("Table '%s' already exists within the given " + "MetaData - not copying." % self.description) + return metadata.tables[key] + + args = [] + for c in self.columns: + args.append(c.copy(schema=schema)) + table = Table( + self.name, metadata, schema=schema, + *args, **self.kwargs + ) + for c in self.constraints: + table.append_constraint(c.copy(schema=schema, target_table=table)) + + for index in self.indexes: + # skip indexes that would be generated + # by the 'index' flag on Column + if len(index.columns) == 1 and \ + list(index.columns)[0].index: + continue + Index(index.name, + unique=index.unique, + *[table.c[col] for col in index.columns.keys()], + **index.kwargs) + return self._schema_item_copy(table) + + +class Column(SchemaItem, ColumnClause): + """Represents a column in a database table.""" + + __visit_name__ = 'column' + + def __init__(self, *args, **kwargs): + """ + Construct a new ``Column`` object. + + :param name: The name of this column as represented in the database. + This argument may be the first positional argument, or specified + via keyword. + + Names which contain no upper case characters + will be treated as case insensitive names, and will not be quoted + unless they are a reserved word. Names with any number of upper + case characters will be quoted and sent exactly. Note that this + behavior applies even for databases which standardize upper + case names as case insensitive such as Oracle. + + The name field may be omitted at construction time and applied + later, at any time before the Column is associated with a + :class:`.Table`. This is to support convenient + usage within the :mod:`~sqlalchemy.ext.declarative` extension. + + :param type\_: The column's type, indicated using an instance which + subclasses :class:`~sqlalchemy.types.TypeEngine`. If no arguments + are required for the type, the class of the type can be sent + as well, e.g.:: + + # use a type with arguments + Column('data', String(50)) + + # use no arguments + Column('level', Integer) + + The ``type`` argument may be the second positional argument + or specified by keyword. + + If the ``type`` is ``None`` or is omitted, it will first default to the special + type :class:`.NullType`. If and when this :class:`.Column` is + made to refer to another column using :class:`.ForeignKey` + and/or :class:`.ForeignKeyConstraint`, the type of the remote-referenced + column will be copied to this column as well, at the moment that + the foreign key is resolved against that remote :class:`.Column` + object. + + .. versionchanged:: 0.9.0 + Support for propagation of type to a :class:`.Column` from its + :class:`.ForeignKey` object has been improved and should be + more reliable and timely. + + :param \*args: Additional positional arguments include various + :class:`.SchemaItem` derived constructs which will be applied + as options to the column. These include instances of + :class:`.Constraint`, :class:`.ForeignKey`, :class:`.ColumnDefault`, + and :class:`.Sequence`. In some cases an equivalent keyword + argument is available such as ``server_default``, ``default`` + and ``unique``. + + :param autoincrement: This flag may be set to ``False`` to + indicate an integer primary key column that should not be + considered to be the "autoincrement" column, that is + the integer primary key column which generates values + implicitly upon INSERT and whose value is usually returned + via the DBAPI cursor.lastrowid attribute. It defaults + to ``True`` to satisfy the common use case of a table + with a single integer primary key column. If the table + has a composite primary key consisting of more than one + integer column, set this flag to True only on the + column that should be considered "autoincrement". + + The setting *only* has an effect for columns which are: + + * Integer derived (i.e. INT, SMALLINT, BIGINT). + + * Part of the primary key + + * Are not referenced by any foreign keys, unless + the value is specified as ``'ignore_fk'`` + + .. versionadded:: 0.7.4 + + * have no server side or client side defaults (with the exception + of Postgresql SERIAL). + + The setting has these two effects on columns that meet the + above criteria: + + * DDL issued for the column will include database-specific + keywords intended to signify this column as an + "autoincrement" column, such as AUTO INCREMENT on MySQL, + SERIAL on Postgresql, and IDENTITY on MS-SQL. It does + *not* issue AUTOINCREMENT for SQLite since this is a + special SQLite flag that is not required for autoincrementing + behavior. See the SQLite dialect documentation for + information on SQLite's AUTOINCREMENT. + + * The column will be considered to be available as + cursor.lastrowid or equivalent, for those dialects which + "post fetch" newly inserted identifiers after a row has + been inserted (SQLite, MySQL, MS-SQL). It does not have + any effect in this regard for databases that use sequences + to generate primary key identifiers (i.e. Firebird, Postgresql, + Oracle). + + .. versionchanged:: 0.7.4 + ``autoincrement`` accepts a special value ``'ignore_fk'`` + to indicate that autoincrementing status regardless of foreign + key references. This applies to certain composite foreign key + setups, such as the one demonstrated in the ORM documentation + at :ref:`post_update`. + + :param default: A scalar, Python callable, or + :class:`.ColumnElement` expression representing the + *default value* for this column, which will be invoked upon insert + if this column is otherwise not specified in the VALUES clause of + the insert. This is a shortcut to using :class:`.ColumnDefault` as + a positional argument; see that class for full detail on the + structure of the argument. + + Contrast this argument to ``server_default`` which creates a + default generator on the database side. + + :param doc: optional String that can be used by the ORM or similar + to document attributes. This attribute does not render SQL + comments (a future attribute 'comment' will achieve that). + + :param key: An optional string identifier which will identify this + ``Column`` object on the :class:`.Table`. When a key is provided, + this is the only identifier referencing the ``Column`` within the + application, including ORM attribute mapping; the ``name`` field + is used only when rendering SQL. + + :param index: When ``True``, indicates that the column is indexed. + This is a shortcut for using a :class:`.Index` construct on the + table. To specify indexes with explicit names or indexes that + contain multiple columns, use the :class:`.Index` construct + instead. + + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + :param nullable: If set to the default of ``True``, indicates the + column will be rendered as allowing NULL, else it's rendered as + NOT NULL. This parameter is only used when issuing CREATE TABLE + statements. + + :param onupdate: A scalar, Python callable, or + :class:`~sqlalchemy.sql.expression.ClauseElement` representing a + default value to be applied to the column within UPDATE + statements, which wil be invoked upon update if this column is not + present in the SET clause of the update. This is a shortcut to + using :class:`.ColumnDefault` as a positional argument with + ``for_update=True``. + + :param primary_key: If ``True``, marks this column as a primary key + column. Multiple columns can have this flag set to specify + composite primary keys. As an alternative, the primary key of a + :class:`.Table` can be specified via an explicit + :class:`.PrimaryKeyConstraint` object. + + :param server_default: A :class:`.FetchedValue` instance, str, Unicode + or :func:`~sqlalchemy.sql.expression.text` construct representing + the DDL DEFAULT value for the column. + + String types will be emitted as-is, surrounded by single quotes:: + + Column('x', Text, server_default="val") + + x TEXT DEFAULT 'val' + + A :func:`~sqlalchemy.sql.expression.text` expression will be + rendered as-is, without quotes:: + + Column('y', DateTime, server_default=text('NOW()')) + + y DATETIME DEFAULT NOW() + + Strings and text() will be converted into a :class:`.DefaultClause` + object upon initialization. + + Use :class:`.FetchedValue` to indicate that an already-existing + column will generate a default value on the database side which + will be available to SQLAlchemy for post-fetch after inserts. This + construct does not specify any DDL and the implementation is left + to the database, such as via a trigger. + + :param server_onupdate: A :class:`.FetchedValue` instance + representing a database-side default generation function. This + indicates to SQLAlchemy that a newly generated value will be + available after updates. This construct does not specify any DDL + and the implementation is left to the database, such as via a + trigger. + + :param quote: Force quoting of this column's name on or off, + corresponding to ``True`` or ``False``. When left at its default + of ``None``, the column identifier will be quoted according to + whether the name is case sensitive (identifiers with at least one + upper case character are treated as case sensitive), or if it's a + reserved word. This flag is only needed to force quoting of a + reserved word which is not known by the SQLAlchemy dialect. + + :param unique: When ``True``, indicates that this column contains a + unique constraint, or if ``index`` is ``True`` as well, indicates + that the :class:`.Index` should be created with the unique flag. + To specify multiple columns in the constraint/index or to specify + an explicit name, use the :class:`.UniqueConstraint` or + :class:`.Index` constructs explicitly. + + :param system: When ``True``, indicates this is a "system" column, + that is a column which is automatically made available by the + database, and should not be included in the columns list for a + ``CREATE TABLE`` statement. + + For more elaborate scenarios where columns should be conditionally + rendered differently on different backends, consider custom + compilation rules for :class:`.CreateColumn`. + + ..versionadded:: 0.8.3 Added the ``system=True`` parameter to + :class:`.Column`. + + """ + + name = kwargs.pop('name', None) + type_ = kwargs.pop('type_', None) + args = list(args) + if args: + if isinstance(args[0], util.string_types): + if name is not None: + raise exc.ArgumentError( + "May not pass name positionally and as a keyword.") + name = args.pop(0) + if args: + coltype = args[0] + + if hasattr(coltype, "_sqla_type"): + if type_ is not None: + raise exc.ArgumentError( + "May not pass type_ positionally and as a keyword.") + type_ = args.pop(0) + + if name is not None: + name = quoted_name(name, kwargs.pop('quote', None)) + elif "quote" in kwargs: + raise exc.ArgumentError("Explicit 'name' is required when " + "sending 'quote' argument") + + super(Column, self).__init__(name, type_) + self.key = kwargs.pop('key', name) + self.primary_key = kwargs.pop('primary_key', False) + self.nullable = kwargs.pop('nullable', not self.primary_key) + self.default = kwargs.pop('default', None) + self.server_default = kwargs.pop('server_default', None) + self.server_onupdate = kwargs.pop('server_onupdate', None) + + # these default to None because .index and .unique is *not* + # an informational flag about Column - there can still be an + # Index or UniqueConstraint referring to this Column. + self.index = kwargs.pop('index', None) + self.unique = kwargs.pop('unique', None) + + self.system = kwargs.pop('system', False) + self.doc = kwargs.pop('doc', None) + self.onupdate = kwargs.pop('onupdate', None) + self.autoincrement = kwargs.pop('autoincrement', True) + self.constraints = set() + self.foreign_keys = set() + + # check if this Column is proxying another column + if '_proxies' in kwargs: + self._proxies = kwargs.pop('_proxies') + # otherwise, add DDL-related events + elif isinstance(self.type, SchemaEventTarget): + self.type._set_parent_with_dispatch(self) + + if self.default is not None: + if isinstance(self.default, (ColumnDefault, Sequence)): + args.append(self.default) + else: + if getattr(self.type, '_warn_on_bytestring', False): + if isinstance(self.default, util.binary_type): + util.warn("Unicode column received non-unicode " + "default value.") + args.append(ColumnDefault(self.default)) + + if self.server_default is not None: + if isinstance(self.server_default, FetchedValue): + args.append(self.server_default._as_for_update(False)) + else: + args.append(DefaultClause(self.server_default)) + + if self.onupdate is not None: + if isinstance(self.onupdate, (ColumnDefault, Sequence)): + args.append(self.onupdate) + else: + args.append(ColumnDefault(self.onupdate, for_update=True)) + + if self.server_onupdate is not None: + if isinstance(self.server_onupdate, FetchedValue): + args.append(self.server_onupdate._as_for_update(True)) + else: + args.append(DefaultClause(self.server_onupdate, + for_update=True)) + self._init_items(*args) + + util.set_creation_order(self) + + if 'info' in kwargs: + self.info = kwargs.pop('info') + + if kwargs: + raise exc.ArgumentError( + "Unknown arguments passed to Column: " + repr(list(kwargs))) + +# @property +# def quote(self): +# return getattr(self.name, "quote", None) + + def __str__(self): + if self.name is None: + return "(no name)" + elif self.table is not None: + if self.table.named_with_column: + return (self.table.description + "." + self.description) + else: + return self.description + else: + return self.description + + def references(self, column): + """Return True if this Column references the given column via foreign + key.""" + + for fk in self.foreign_keys: + if fk.column.proxy_set.intersection(column.proxy_set): + return True + else: + return False + + def append_foreign_key(self, fk): + fk._set_parent_with_dispatch(self) + + def __repr__(self): + kwarg = [] + if self.key != self.name: + kwarg.append('key') + if self.primary_key: + kwarg.append('primary_key') + if not self.nullable: + kwarg.append('nullable') + if self.onupdate: + kwarg.append('onupdate') + if self.default: + kwarg.append('default') + if self.server_default: + kwarg.append('server_default') + return "Column(%s)" % ', '.join( + [repr(self.name)] + [repr(self.type)] + + [repr(x) for x in self.foreign_keys if x is not None] + + [repr(x) for x in self.constraints] + + [(self.table is not None and "table=<%s>" % + self.table.description or "table=None")] + + ["%s=%s" % (k, repr(getattr(self, k))) for k in kwarg]) + + def _set_parent(self, table): + if not self.name: + raise exc.ArgumentError( + "Column must be constructed with a non-blank name or " + "assign a non-blank .name before adding to a Table.") + if self.key is None: + self.key = self.name + + existing = getattr(self, 'table', None) + if existing is not None and existing is not table: + raise exc.ArgumentError( + "Column object already assigned to Table '%s'" % + existing.description) + + if self.key in table._columns: + col = table._columns.get(self.key) + if col is not self: + for fk in col.foreign_keys: + table.foreign_keys.remove(fk) + if fk.constraint in table.constraints: + # this might have been removed + # already, if it's a composite constraint + # and more than one col being replaced + table.constraints.remove(fk.constraint) + + table._columns.replace(self) + + if self.primary_key: + table.primary_key._replace(self) + Table._autoincrement_column._reset(table) + elif self.key in table.primary_key: + raise exc.ArgumentError( + "Trying to redefine primary-key column '%s' as a " + "non-primary-key column on table '%s'" % ( + self.key, table.fullname)) + self.table = table + + if self.index: + if isinstance(self.index, util.string_types): + raise exc.ArgumentError( + "The 'index' keyword argument on Column is boolean only. " + "To create indexes with a specific name, create an " + "explicit Index object external to the Table.") + Index(_truncated_label('ix_%s' % self._label), + self, unique=bool(self.unique)) + elif self.unique: + if isinstance(self.unique, util.string_types): + raise exc.ArgumentError( + "The 'unique' keyword argument on Column is boolean " + "only. To create unique constraints or indexes with a " + "specific name, append an explicit UniqueConstraint to " + "the Table's list of elements, or create an explicit " + "Index object external to the Table.") + table.append_constraint(UniqueConstraint(self.key)) + + fk_key = (table.key, self.key) + if fk_key in self.table.metadata._fk_memos: + for fk in self.table.metadata._fk_memos[fk_key]: + fk._set_remote_table(table) + + def _on_table_attach(self, fn): + if self.table is not None: + fn(self, self.table) + event.listen(self, 'after_parent_attach', fn) + + def copy(self, **kw): + """Create a copy of this ``Column``, unitialized. + + This is used in ``Table.tometadata``. + + """ + + # Constraint objects plus non-constraint-bound ForeignKey objects + args = \ + [c.copy(**kw) for c in self.constraints] + \ + [c.copy(**kw) for c in self.foreign_keys if not c.constraint] + + type_ = self.type + if isinstance(type_, SchemaEventTarget): + type_ = type_.copy(**kw) + + c = self._constructor( + name=self.name, + type_=type_, + key=self.key, + primary_key=self.primary_key, + nullable=self.nullable, + unique=self.unique, + system=self.system, + #quote=self.quote, + index=self.index, + autoincrement=self.autoincrement, + default=self.default, + server_default=self.server_default, + onupdate=self.onupdate, + server_onupdate=self.server_onupdate, + doc=self.doc, + *args + ) + return self._schema_item_copy(c) + + def _make_proxy(self, selectable, name=None, key=None, + name_is_truncatable=False, **kw): + """Create a *proxy* for this column. + + This is a copy of this ``Column`` referenced by a different parent + (such as an alias or select statement). The column should + be used only in select scenarios, as its full DDL/default + information is not transferred. + + """ + fk = [ForeignKey(f.column, _constraint=f.constraint) + for f in self.foreign_keys] + if name is None and self.name is None: + raise exc.InvalidRequestError("Cannot initialize a sub-selectable" + " with this Column object until it's 'name' has " + "been assigned.") + try: + c = self._constructor( + _as_truncated(name or self.name) if \ + name_is_truncatable else (name or self.name), + self.type, + key=key if key else name if name else self.key, + primary_key=self.primary_key, + nullable=self.nullable, + _proxies=[self], *fk) + except TypeError: + util.raise_from_cause( + TypeError( + "Could not create a copy of this %r object. " + "Ensure the class includes a _constructor() " + "attribute or method which accepts the " + "standard Column constructor arguments, or " + "references the Column class itself." % self.__class__) + ) + + c.table = selectable + selectable._columns.add(c) + if selectable._is_clone_of is not None: + c._is_clone_of = selectable._is_clone_of.columns[c.key] + if self.primary_key: + selectable.primary_key.add(c) + c.dispatch.after_parent_attach(c, selectable) + return c + + def get_children(self, schema_visitor=False, **kwargs): + if schema_visitor: + return [x for x in (self.default, self.onupdate) + if x is not None] + \ + list(self.foreign_keys) + list(self.constraints) + else: + return ColumnClause.get_children(self, **kwargs) + + +class ForeignKey(SchemaItem): + """Defines a dependency between two columns. + + ``ForeignKey`` is specified as an argument to a :class:`.Column` object, + e.g.:: + + t = Table("remote_table", metadata, + Column("remote_id", ForeignKey("main_table.id")) + ) + + Note that ``ForeignKey`` is only a marker object that defines + a dependency between two columns. The actual constraint + is in all cases represented by the :class:`.ForeignKeyConstraint` + object. This object will be generated automatically when + a ``ForeignKey`` is associated with a :class:`.Column` which + in turn is associated with a :class:`.Table`. Conversely, + when :class:`.ForeignKeyConstraint` is applied to a :class:`.Table`, + ``ForeignKey`` markers are automatically generated to be + present on each associated :class:`.Column`, which are also + associated with the constraint object. + + Note that you cannot define a "composite" foreign key constraint, + that is a constraint between a grouping of multiple parent/child + columns, using ``ForeignKey`` objects. To define this grouping, + the :class:`.ForeignKeyConstraint` object must be used, and applied + to the :class:`.Table`. The associated ``ForeignKey`` objects + are created automatically. + + The ``ForeignKey`` objects associated with an individual + :class:`.Column` object are available in the `foreign_keys` collection + of that column. + + Further examples of foreign key configuration are in + :ref:`metadata_foreignkeys`. + + """ + + __visit_name__ = 'foreign_key' + + def __init__(self, column, _constraint=None, use_alter=False, name=None, + onupdate=None, ondelete=None, deferrable=None, + initially=None, link_to_name=False, match=None): + """ + Construct a column-level FOREIGN KEY. + + The :class:`.ForeignKey` object when constructed generates a + :class:`.ForeignKeyConstraint` which is associated with the parent + :class:`.Table` object's collection of constraints. + + :param column: A single target column for the key relationship. A + :class:`.Column` object or a column name as a string: + ``tablename.columnkey`` or ``schema.tablename.columnkey``. + ``columnkey`` is the ``key`` which has been assigned to the column + (defaults to the column name itself), unless ``link_to_name`` is + ``True`` in which case the rendered name of the column is used. + + .. versionadded:: 0.7.4 + Note that if the schema name is not included, and the + underlying :class:`.MetaData` has a "schema", that value will + be used. + + :param name: Optional string. An in-database name for the key if + `constraint` is not provided. + + :param onupdate: Optional string. If set, emit ON UPDATE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + + :param ondelete: Optional string. If set, emit ON DELETE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + + :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT + DEFERRABLE when issuing DDL for this constraint. + + :param initially: Optional string. If set, emit INITIALLY when + issuing DDL for this constraint. + + :param link_to_name: if True, the string name given in ``column`` is + the rendered name of the referenced column, not its locally + assigned ``key``. + + :param use_alter: passed to the underlying + :class:`.ForeignKeyConstraint` to indicate the constraint should be + generated/dropped externally from the CREATE TABLE/ DROP TABLE + statement. See that classes' constructor for details. + + :param match: Optional string. If set, emit MATCH when issuing + DDL for this constraint. Typical values include SIMPLE, PARTIAL + and FULL. + + """ + + self._colspec = column + if isinstance(self._colspec, util.string_types): + self._table_column = None + else: + if hasattr(self._colspec, '__clause_element__'): + self._table_column = self._colspec.__clause_element__() + else: + self._table_column = self._colspec + + if not isinstance(self._table_column, ColumnClause): + raise exc.ArgumentError( + "String, Column, or Column-bound argument " + "expected, got %r" % self._table_column) + elif not isinstance(self._table_column.table, (util.NoneType, TableClause)): + raise exc.ArgumentError( + "ForeignKey received Column not bound " + "to a Table, got: %r" % self._table_column.table + ) + + # the linked ForeignKeyConstraint. + # ForeignKey will create this when parent Column + # is attached to a Table, *or* ForeignKeyConstraint + # object passes itself in when creating ForeignKey + # markers. + self.constraint = _constraint + self.parent = None + self.use_alter = use_alter + self.name = name + self.onupdate = onupdate + self.ondelete = ondelete + self.deferrable = deferrable + self.initially = initially + self.link_to_name = link_to_name + self.match = match + + def __repr__(self): + return "ForeignKey(%r)" % self._get_colspec() + + def copy(self, schema=None): + """Produce a copy of this :class:`.ForeignKey` object. + + The new :class:`.ForeignKey` will not be bound + to any :class:`.Column`. + + This method is usually used by the internal + copy procedures of :class:`.Column`, :class:`.Table`, + and :class:`.MetaData`. + + :param schema: The returned :class:`.ForeignKey` will + reference the original table and column name, qualified + by the given string schema name. + + """ + + fk = ForeignKey( + self._get_colspec(schema=schema), + use_alter=self.use_alter, + name=self.name, + onupdate=self.onupdate, + ondelete=self.ondelete, + deferrable=self.deferrable, + initially=self.initially, + link_to_name=self.link_to_name, + match=self.match + ) + return self._schema_item_copy(fk) + + + def _get_colspec(self, schema=None): + """Return a string based 'column specification' for this + :class:`.ForeignKey`. + + This is usually the equivalent of the string-based "tablename.colname" + argument first passed to the object's constructor. + + """ + if schema: + _schema, tname, colname = self._column_tokens + return "%s.%s.%s" % (schema, tname, colname) + elif self._table_column is not None: + return "%s.%s" % ( + self._table_column.table.fullname, self._table_column.key) + else: + return self._colspec + + + def _table_key(self): + if self._table_column is not None: + if self._table_column.table is None: + return None + else: + return self._table_column.table.key + else: + schema, tname, colname = self._column_tokens + return _get_table_key(tname, schema) + + + + target_fullname = property(_get_colspec) + + def references(self, table): + """Return True if the given :class:`.Table` is referenced by this + :class:`.ForeignKey`.""" + + return table.corresponding_column(self.column) is not None + + def get_referent(self, table): + """Return the :class:`.Column` in the given :class:`.Table` + referenced by this :class:`.ForeignKey`. + + Returns None if this :class:`.ForeignKey` does not reference the given + :class:`.Table`. + + """ + + return table.corresponding_column(self.column) + + @util.memoized_property + def _column_tokens(self): + """parse a string-based _colspec into its component parts.""" + + m = self._colspec.split('.') + if m is None: + raise exc.ArgumentError( + "Invalid foreign key column specification: %s" % + self._colspec) + if (len(m) == 1): + tname = m.pop() + colname = None + else: + colname = m.pop() + tname = m.pop() + + # A FK between column 'bar' and table 'foo' can be + # specified as 'foo', 'foo.bar', 'dbo.foo.bar', + # 'otherdb.dbo.foo.bar'. Once we have the column name and + # the table name, treat everything else as the schema + # name. Some databases (e.g. Sybase) support + # inter-database foreign keys. See tickets#1341 and -- + # indirectly related -- Ticket #594. This assumes that '.' + # will never appear *within* any component of the FK. + + if (len(m) > 0): + schema = '.'.join(m) + else: + schema = None + return schema, tname, colname + + def _resolve_col_tokens(self): + if self.parent is None: + raise exc.InvalidRequestError( + "this ForeignKey object does not yet have a " + "parent Column associated with it.") + + elif self.parent.table is None: + raise exc.InvalidRequestError( + "this ForeignKey's parent column is not yet associated " + "with a Table.") + + parenttable = self.parent.table + + # assertion, can be commented out. + # basically Column._make_proxy() sends the actual + # target Column to the ForeignKey object, so the + # string resolution here is never called. + for c in self.parent.base_columns: + if isinstance(c, Column): + assert c.table is parenttable + break + else: + assert False + ###################### + + schema, tname, colname = self._column_tokens + + if schema is None and parenttable.metadata.schema is not None: + schema = parenttable.metadata.schema + + tablekey = _get_table_key(tname, schema) + return parenttable, tablekey, colname + + + def _link_to_col_by_colstring(self, parenttable, table, colname): + if not hasattr(self.constraint, '_referred_table'): + self.constraint._referred_table = table + else: + assert self.constraint._referred_table is table + + _column = None + if colname is None: + # colname is None in the case that ForeignKey argument + # was specified as table name only, in which case we + # match the column name to the same column on the + # parent. + key = self.parent + _column = table.c.get(self.parent.key, None) + elif self.link_to_name: + key = colname + for c in table.c: + if c.name == colname: + _column = c + else: + key = colname + _column = table.c.get(colname, None) + + if _column is None: + raise exc.NoReferencedColumnError( + "Could not initialize target column for ForeignKey '%s' on table '%s': " + "table '%s' has no column named '%s'" % ( + self._colspec, parenttable.name, table.name, key), + table.name, key) + + self._set_target_column(_column) + + def _set_target_column(self, column): + # propagate TypeEngine to parent if it didn't have one + if self.parent.type._isnull: + self.parent.type = column.type + + # super-edgy case, if other FKs point to our column, + # they'd get the type propagated out also. + if isinstance(self.parent.table, Table): + fk_key = (self.parent.table.key, self.parent.key) + if fk_key in self.parent.table.metadata._fk_memos: + for fk in self.parent.table.metadata._fk_memos[fk_key]: + if fk.parent.type._isnull: + fk.parent.type = column.type + + self.column = column + + @util.memoized_property + def column(self): + """Return the target :class:`.Column` referenced by this + :class:`.ForeignKey`. + + If no target column has been established, an exception + is raised. + + .. versionchanged:: 0.9.0 + Foreign key target column resolution now occurs as soon as both + the ForeignKey object and the remote Column to which it refers + are both associated with the same MetaData object. + + """ + + if isinstance(self._colspec, util.string_types): + + parenttable, tablekey, colname = self._resolve_col_tokens() + + if tablekey not in parenttable.metadata: + raise exc.NoReferencedTableError( + "Foreign key associated with column '%s' could not find " + "table '%s' with which to generate a " + "foreign key to target column '%s'" % + (self.parent, tablekey, colname), + tablekey) + elif parenttable.key not in parenttable.metadata: + raise exc.InvalidRequestError( + "Table %s is no longer associated with its " + "parent MetaData" % parenttable) + else: + raise exc.NoReferencedColumnError( + "Could not initialize target column for " + "ForeignKey '%s' on table '%s': " + "table '%s' has no column named '%s'" % ( + self._colspec, parenttable.name, tablekey, colname), + tablekey, colname) + elif hasattr(self._colspec, '__clause_element__'): + _column = self._colspec.__clause_element__() + return _column + else: + _column = self._colspec + return _column + + def _set_parent(self, column): + if self.parent is not None and self.parent is not column: + raise exc.InvalidRequestError( + "This ForeignKey already has a parent !") + self.parent = column + self.parent.foreign_keys.add(self) + self.parent._on_table_attach(self._set_table) + + def _set_remote_table(self, table): + parenttable, tablekey, colname = self._resolve_col_tokens() + self._link_to_col_by_colstring(parenttable, table, colname) + self.constraint._validate_dest_table(table) + + def _remove_from_metadata(self, metadata): + parenttable, table_key, colname = self._resolve_col_tokens() + fk_key = (table_key, colname) + + if self in metadata._fk_memos[fk_key]: + # TODO: no test coverage for self not in memos + metadata._fk_memos[fk_key].remove(self) + + def _set_table(self, column, table): + # standalone ForeignKey - create ForeignKeyConstraint + # on the hosting Table when attached to the Table. + if self.constraint is None and isinstance(table, Table): + self.constraint = ForeignKeyConstraint( + [], [], use_alter=self.use_alter, name=self.name, + onupdate=self.onupdate, ondelete=self.ondelete, + deferrable=self.deferrable, initially=self.initially, + match=self.match, + ) + self.constraint._elements[self.parent] = self + self.constraint._set_parent_with_dispatch(table) + table.foreign_keys.add(self) + + # set up remote ".column" attribute, or a note to pick it + # up when the other Table/Column shows up + if isinstance(self._colspec, util.string_types): + parenttable, table_key, colname = self._resolve_col_tokens() + fk_key = (table_key, colname) + if table_key in parenttable.metadata.tables: + table = parenttable.metadata.tables[table_key] + try: + self._link_to_col_by_colstring(parenttable, table, colname) + except exc.NoReferencedColumnError: + # this is OK, we'll try later + pass + parenttable.metadata._fk_memos[fk_key].append(self) + elif hasattr(self._colspec, '__clause_element__'): + _column = self._colspec.__clause_element__() + self._set_target_column(_column) + else: + _column = self._colspec + self._set_target_column(_column) + + + +class _NotAColumnExpr(object): + def _not_a_column_expr(self): + raise exc.InvalidRequestError( + "This %s cannot be used directly " + "as a column expression." % self.__class__.__name__) + + __clause_element__ = self_group = lambda self: self._not_a_column_expr() + _from_objects = property(lambda self: self._not_a_column_expr()) + + +class DefaultGenerator(_NotAColumnExpr, SchemaItem): + """Base class for column *default* values.""" + + __visit_name__ = 'default_generator' + + is_sequence = False + is_server_default = False + column = None + + def __init__(self, for_update=False): + self.for_update = for_update + + def _set_parent(self, column): + self.column = column + if self.for_update: + self.column.onupdate = self + else: + self.column.default = self + + def execute(self, bind=None, **kwargs): + if bind is None: + bind = _bind_or_error(self) + return bind._execute_default(self, **kwargs) + + @property + def bind(self): + """Return the connectable associated with this default.""" + if getattr(self, 'column', None) is not None: + return self.column.table.bind + else: + return None + + +class ColumnDefault(DefaultGenerator): + """A plain default value on a column. + + This could correspond to a constant, a callable function, + or a SQL clause. + + :class:`.ColumnDefault` is generated automatically + whenever the ``default``, ``onupdate`` arguments of + :class:`.Column` are used. A :class:`.ColumnDefault` + can be passed positionally as well. + + For example, the following:: + + Column('foo', Integer, default=50) + + Is equivalent to:: + + Column('foo', Integer, ColumnDefault(50)) + + + """ + + def __init__(self, arg, **kwargs): + """"Construct a new :class:`.ColumnDefault`. + + + :param arg: argument representing the default value. + May be one of the following: + + * a plain non-callable Python value, such as a + string, integer, boolean, or other simple type. + The default value will be used as is each time. + * a SQL expression, that is one which derives from + :class:`.ColumnElement`. The SQL expression will + be rendered into the INSERT or UPDATE statement, + or in the case of a primary key column when + RETURNING is not used may be + pre-executed before an INSERT within a SELECT. + * A Python callable. The function will be invoked for each + new row subject to an INSERT or UPDATE. + The callable must accept exactly + zero or one positional arguments. The one-argument form + will receive an instance of the :class:`.ExecutionContext`, + which provides contextual information as to the current + :class:`.Connection` in use as well as the current + statement and parameters. + + """ + super(ColumnDefault, self).__init__(**kwargs) + if isinstance(arg, FetchedValue): + raise exc.ArgumentError( + "ColumnDefault may not be a server-side default type.") + if util.callable(arg): + arg = self._maybe_wrap_callable(arg) + self.arg = arg + + @util.memoized_property + def is_callable(self): + return util.callable(self.arg) + + @util.memoized_property + def is_clause_element(self): + return isinstance(self.arg, ClauseElement) + + @util.memoized_property + def is_scalar(self): + return not self.is_callable and \ + not self.is_clause_element and \ + not self.is_sequence + + def _maybe_wrap_callable(self, fn): + """Wrap callables that don't accept a context. + + The alternative here is to require that + a simple callable passed to "default" would need + to be of the form "default=lambda ctx: datetime.now". + That is the more "correct" way to go, but the case + of using a zero-arg callable for "default" is so + much more prominent than the context-specific one + I'm having trouble justifying putting that inconvenience + on everyone. + + """ + if inspect.isfunction(fn) or inspect.ismethod(fn): + inspectable = fn + elif inspect.isclass(fn): + inspectable = fn.__init__ + elif hasattr(fn, '__call__'): + inspectable = fn.__call__ + else: + # probably not inspectable, try anyways. + inspectable = fn + try: + argspec = inspect.getargspec(inspectable) + except TypeError: + return lambda ctx: fn() + + defaulted = argspec[3] is not None and len(argspec[3]) or 0 + positionals = len(argspec[0]) - defaulted + + # Py3K compat - no unbound methods + if inspect.ismethod(inspectable) or inspect.isclass(fn): + positionals -= 1 + + if positionals == 0: + return lambda ctx: fn() + elif positionals == 1: + return fn + else: + raise exc.ArgumentError( + "ColumnDefault Python function takes zero or one " + "positional arguments") + + def _visit_name(self): + if self.for_update: + return "column_onupdate" + else: + return "column_default" + __visit_name__ = property(_visit_name) + + def __repr__(self): + return "ColumnDefault(%r)" % self.arg + + +class Sequence(DefaultGenerator): + """Represents a named database sequence. + + The :class:`.Sequence` object represents the name and configurational + parameters of a database sequence. It also represents + a construct that can be "executed" by a SQLAlchemy :class:`.Engine` + or :class:`.Connection`, rendering the appropriate "next value" function + for the target database and returning a result. + + The :class:`.Sequence` is typically associated with a primary key column:: + + some_table = Table('some_table', metadata, + Column('id', Integer, Sequence('some_table_seq'), primary_key=True) + ) + + When CREATE TABLE is emitted for the above :class:`.Table`, if the + target platform supports sequences, a CREATE SEQUENCE statement will + be emitted as well. For platforms that don't support sequences, + the :class:`.Sequence` construct is ignored. + + .. seealso:: + + :class:`.CreateSequence` + + :class:`.DropSequence` + + """ + + __visit_name__ = 'sequence' + + is_sequence = True + + def __init__(self, name, start=None, increment=None, schema=None, + optional=False, quote=None, metadata=None, + quote_schema=None, + for_update=False): + """Construct a :class:`.Sequence` object. + + :param name: The name of the sequence. + :param start: the starting index of the sequence. This value is + used when the CREATE SEQUENCE command is emitted to the database + as the value of the "START WITH" clause. If ``None``, the + clause is omitted, which on most platforms indicates a starting + value of 1. + :param increment: the increment value of the sequence. This + value is used when the CREATE SEQUENCE command is emitted to + the database as the value of the "INCREMENT BY" clause. If ``None``, + the clause is omitted, which on most platforms indicates an + increment of 1. + :param schema: Optional schema name for the sequence, if located + in a schema other than the default. + :param optional: boolean value, when ``True``, indicates that this + :class:`.Sequence` object only needs to be explicitly generated + on backends that don't provide another way to generate primary + key identifiers. Currently, it essentially means, "don't create + this sequence on the Postgresql backend, where the SERIAL keyword + creates a sequence for us automatically". + :param quote: boolean value, when ``True`` or ``False``, explicitly + forces quoting of the schema name on or off. When left at its + default of ``None``, normal quoting rules based on casing and reserved + words take place. + :param quote_schema: set the quoting preferences for the ``schema`` + name. + :param metadata: optional :class:`.MetaData` object which will be + associated with this :class:`.Sequence`. A :class:`.Sequence` + that is associated with a :class:`.MetaData` gains access to the + ``bind`` of that :class:`.MetaData`, meaning the + :meth:`.Sequence.create` and :meth:`.Sequence.drop` methods will + make usage of that engine automatically. + + .. versionchanged:: 0.7 + Additionally, the appropriate CREATE SEQUENCE/ + DROP SEQUENCE DDL commands will be emitted corresponding to this + :class:`.Sequence` when :meth:`.MetaData.create_all` and + :meth:`.MetaData.drop_all` are invoked. + + Note that when a :class:`.Sequence` is applied to a :class:`.Column`, + the :class:`.Sequence` is automatically associated with the + :class:`.MetaData` object of that column's parent :class:`.Table`, + when that association is made. The :class:`.Sequence` will then + be subject to automatic CREATE SEQUENCE/DROP SEQUENCE corresponding + to when the :class:`.Table` object itself is created or dropped, + rather than that of the :class:`.MetaData` object overall. + :param for_update: Indicates this :class:`.Sequence`, when associated + with a :class:`.Column`, should be invoked for UPDATE statements + on that column's table, rather than for INSERT statements, when + no value is otherwise present for that column in the statement. + + """ + super(Sequence, self).__init__(for_update=for_update) + self.name = quoted_name(name, quote) + self.start = start + self.increment = increment + self.optional = optional + if metadata is not None and schema is None and metadata.schema: + self.schema = schema = metadata.schema + else: + self.schema = quoted_name(schema, quote_schema) + self.metadata = metadata + self._key = _get_table_key(name, schema) + if metadata: + self._set_metadata(metadata) + + @util.memoized_property + def is_callable(self): + return False + + @util.memoized_property + def is_clause_element(self): + return False + + @util.dependencies("sqlalchemy.sql.functions.func") + def next_value(self, func): + """Return a :class:`.next_value` function element + which will render the appropriate increment function + for this :class:`.Sequence` within any SQL expression. + + """ + return func.next_value(self, bind=self.bind) + + def _set_parent(self, column): + super(Sequence, self)._set_parent(column) + column._on_table_attach(self._set_table) + + def _set_table(self, column, table): + self._set_metadata(table.metadata) + + def _set_metadata(self, metadata): + self.metadata = metadata + self.metadata._sequences[self._key] = self + + @property + def bind(self): + if self.metadata: + return self.metadata.bind + else: + return None + + def create(self, bind=None, checkfirst=True): + """Creates this sequence in the database.""" + + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaGenerator, + self, + checkfirst=checkfirst) + + def drop(self, bind=None, checkfirst=True): + """Drops this sequence from the database.""" + + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaDropper, + self, + checkfirst=checkfirst) + + def _not_a_column_expr(self): + raise exc.InvalidRequestError( + "This %s cannot be used directly " + "as a column expression. Use func.next_value(sequence) " + "to produce a 'next value' function that's usable " + "as a column element." + % self.__class__.__name__) + + +@inspection._self_inspects +class FetchedValue(_NotAColumnExpr, SchemaEventTarget): + """A marker for a transparent database-side default. + + Use :class:`.FetchedValue` when the database is configured + to provide some automatic default for a column. + + E.g.:: + + Column('foo', Integer, FetchedValue()) + + Would indicate that some trigger or default generator + will create a new value for the ``foo`` column during an + INSERT. + + .. seealso:: + + :ref:`triggered_columns` + + """ + is_server_default = True + reflected = False + has_argument = False + + def __init__(self, for_update=False): + self.for_update = for_update + + def _as_for_update(self, for_update): + if for_update == self.for_update: + return self + else: + return self._clone(for_update) + + def _clone(self, for_update): + n = self.__class__.__new__(self.__class__) + n.__dict__.update(self.__dict__) + n.__dict__.pop('column', None) + n.for_update = for_update + return n + + def _set_parent(self, column): + self.column = column + if self.for_update: + self.column.server_onupdate = self + else: + self.column.server_default = self + + def __repr__(self): + return util.generic_repr(self) + + +class DefaultClause(FetchedValue): + """A DDL-specified DEFAULT column value. + + :class:`.DefaultClause` is a :class:`.FetchedValue` + that also generates a "DEFAULT" clause when + "CREATE TABLE" is emitted. + + :class:`.DefaultClause` is generated automatically + whenever the ``server_default``, ``server_onupdate`` arguments of + :class:`.Column` are used. A :class:`.DefaultClause` + can be passed positionally as well. + + For example, the following:: + + Column('foo', Integer, server_default="50") + + Is equivalent to:: + + Column('foo', Integer, DefaultClause("50")) + + """ + + has_argument = True + + def __init__(self, arg, for_update=False, _reflected=False): + util.assert_arg_type(arg, (util.string_types[0], + ClauseElement, + TextClause), 'arg') + super(DefaultClause, self).__init__(for_update) + self.arg = arg + self.reflected = _reflected + + def __repr__(self): + return "DefaultClause(%r, for_update=%r)" % \ + (self.arg, self.for_update) + + +class PassiveDefault(DefaultClause): + """A DDL-specified DEFAULT column value. + + .. deprecated:: 0.6 + :class:`.PassiveDefault` is deprecated. + Use :class:`.DefaultClause`. + """ + @util.deprecated("0.6", + ":class:`.PassiveDefault` is deprecated. " + "Use :class:`.DefaultClause`.", + False) + def __init__(self, *arg, **kw): + DefaultClause.__init__(self, *arg, **kw) + + +class Constraint(SchemaItem): + """A table-level SQL constraint.""" + + __visit_name__ = 'constraint' + + def __init__(self, name=None, deferrable=None, initially=None, + _create_rule=None, + **kw): + """Create a SQL constraint. + + :param name: + Optional, the in-database name of this ``Constraint``. + + :param deferrable: + Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when + issuing DDL for this constraint. + + :param initially: + Optional string. If set, emit INITIALLY when issuing DDL + for this constraint. + + :param _create_rule: + a callable which is passed the DDLCompiler object during + compilation. Returns True or False to signal inline generation of + this Constraint. + + The AddConstraint and DropConstraint DDL constructs provide + DDLElement's more comprehensive "conditional DDL" approach that is + passed a database connection when DDL is being issued. _create_rule + is instead called during any CREATE TABLE compilation, where there + may not be any transaction/connection in progress. However, it + allows conditional compilation of the constraint even for backends + which do not support addition of constraints through ALTER TABLE, + which currently includes SQLite. + + _create_rule is used by some types to create constraints. + Currently, its call signature is subject to change at any time. + + :param \**kwargs: + Dialect-specific keyword parameters, see the documentation + for various dialects and constraints regarding options here. + + """ + + self.name = name + self.deferrable = deferrable + self.initially = initially + self._create_rule = _create_rule + util.set_creation_order(self) + _validate_dialect_kwargs(kw, self.__class__.__name__) + self.kwargs = kw + + @property + def table(self): + try: + if isinstance(self.parent, Table): + return self.parent + except AttributeError: + pass + raise exc.InvalidRequestError( + "This constraint is not bound to a table. Did you " + "mean to call table.append_constraint(constraint) ?") + + def _set_parent(self, parent): + self.parent = parent + parent.constraints.add(self) + + def copy(self, **kw): + raise NotImplementedError() + + +def _to_schema_column(element): + if hasattr(element, '__clause_element__'): + element = element.__clause_element__() + if not isinstance(element, Column): + raise exc.ArgumentError("schema.Column object expected") + return element + + +def _to_schema_column_or_string(element): + if hasattr(element, '__clause_element__'): + element = element.__clause_element__() + if not isinstance(element, util.string_types + (ColumnElement, )): + msg = "Element %r is not a string name or column element" + raise exc.ArgumentError(msg % element) + return element + + +class ColumnCollectionMixin(object): + def __init__(self, *columns): + self.columns = ColumnCollection() + self._pending_colargs = [_to_schema_column_or_string(c) + for c in columns] + if self._pending_colargs and \ + isinstance(self._pending_colargs[0], Column) and \ + isinstance(self._pending_colargs[0].table, Table): + self._set_parent_with_dispatch(self._pending_colargs[0].table) + + def _set_parent(self, table): + for col in self._pending_colargs: + if isinstance(col, util.string_types): + col = table.c[col] + self.columns.add(col) + + +class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): + """A constraint that proxies a ColumnCollection.""" + + def __init__(self, *columns, **kw): + """ + :param \*columns: + A sequence of column names or Column objects. + + :param name: + Optional, the in-database name of this constraint. + + :param deferrable: + Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when + issuing DDL for this constraint. + + :param initially: + Optional string. If set, emit INITIALLY when issuing DDL + for this constraint. + + """ + ColumnCollectionMixin.__init__(self, *columns) + Constraint.__init__(self, **kw) + + def _set_parent(self, table): + ColumnCollectionMixin._set_parent(self, table) + Constraint._set_parent(self, table) + + def __contains__(self, x): + return x in self.columns + + def copy(self, **kw): + c = self.__class__(name=self.name, deferrable=self.deferrable, + initially=self.initially, *self.columns.keys()) + return self._schema_item_copy(c) + + def contains_column(self, col): + return self.columns.contains_column(col) + + def __iter__(self): + # inlining of + # return iter(self.columns) + # ColumnCollection->OrderedProperties->OrderedDict + ordered_dict = self.columns._data + return (ordered_dict[key] for key in ordered_dict._list) + + def __len__(self): + return len(self.columns._data) + + +class CheckConstraint(Constraint): + """A table- or column-level CHECK constraint. + + Can be included in the definition of a Table or Column. + """ + + def __init__(self, sqltext, name=None, deferrable=None, + initially=None, table=None, _create_rule=None, + _autoattach=True): + """Construct a CHECK constraint. + + :param sqltext: + A string containing the constraint definition, which will be used + verbatim, or a SQL expression construct. If given as a string, + the object is converted to a :class:`.Text` object. If the textual + string includes a colon character, escape this using a backslash:: + + CheckConstraint(r"foo ~ E'a(?\:b|c)d") + + :param name: + Optional, the in-database name of the constraint. + + :param deferrable: + Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when + issuing DDL for this constraint. + + :param initially: + Optional string. If set, emit INITIALLY when issuing DDL + for this constraint. + + """ + + super(CheckConstraint, self).\ + __init__(name, deferrable, initially, _create_rule) + self.sqltext = _literal_as_text(sqltext) + if table is not None: + self._set_parent_with_dispatch(table) + elif _autoattach: + cols = _find_columns(self.sqltext) + tables = set([c.table for c in cols + if isinstance(c.table, Table)]) + if len(tables) == 1: + self._set_parent_with_dispatch( + tables.pop()) + + def __visit_name__(self): + if isinstance(self.parent, Table): + return "check_constraint" + else: + return "column_check_constraint" + __visit_name__ = property(__visit_name__) + + def copy(self, target_table=None, **kw): + if target_table is not None: + def replace(col): + if self.table.c.contains_column(col): + return target_table.c[col.key] + else: + return None + sqltext = visitors.replacement_traverse(self.sqltext, {}, replace) + else: + sqltext = self.sqltext + c = CheckConstraint(sqltext, + name=self.name, + initially=self.initially, + deferrable=self.deferrable, + _create_rule=self._create_rule, + table=target_table, + _autoattach=False) + return self._schema_item_copy(c) + + +class ForeignKeyConstraint(Constraint): + """A table-level FOREIGN KEY constraint. + + Defines a single column or composite FOREIGN KEY ... REFERENCES + constraint. For a no-frills, single column foreign key, adding a + :class:`.ForeignKey` to the definition of a :class:`.Column` is a shorthand + equivalent for an unnamed, single column :class:`.ForeignKeyConstraint`. + + Examples of foreign key configuration are in :ref:`metadata_foreignkeys`. + + """ + __visit_name__ = 'foreign_key_constraint' + + def __init__(self, columns, refcolumns, name=None, onupdate=None, + ondelete=None, deferrable=None, initially=None, use_alter=False, + link_to_name=False, match=None, table=None): + """Construct a composite-capable FOREIGN KEY. + + :param columns: A sequence of local column names. The named columns + must be defined and present in the parent Table. The names should + match the ``key`` given to each column (defaults to the name) unless + ``link_to_name`` is True. + + :param refcolumns: A sequence of foreign column names or Column + objects. The columns must all be located within the same Table. + + :param name: Optional, the in-database name of the key. + + :param onupdate: Optional string. If set, emit ON UPDATE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + + :param ondelete: Optional string. If set, emit ON DELETE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + + :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT + DEFERRABLE when issuing DDL for this constraint. + + :param initially: Optional string. If set, emit INITIALLY when + issuing DDL for this constraint. + + :param link_to_name: if True, the string name given in ``column`` is + the rendered name of the referenced column, not its locally assigned + ``key``. + + :param use_alter: If True, do not emit the DDL for this constraint as + part of the CREATE TABLE definition. Instead, generate it via an + ALTER TABLE statement issued after the full collection of tables + have been created, and drop it via an ALTER TABLE statement before + the full collection of tables are dropped. This is shorthand for the + usage of :class:`.AddConstraint` and :class:`.DropConstraint` applied + as "after-create" and "before-drop" events on the MetaData object. + This is normally used to generate/drop constraints on objects that + are mutually dependent on each other. + + :param match: Optional string. If set, emit MATCH when issuing + DDL for this constraint. Typical values include SIMPLE, PARTIAL + and FULL. + + """ + super(ForeignKeyConstraint, self).\ + __init__(name, deferrable, initially) + + self.onupdate = onupdate + self.ondelete = ondelete + self.link_to_name = link_to_name + if self.name is None and use_alter: + raise exc.ArgumentError("Alterable Constraint requires a name") + self.use_alter = use_alter + self.match = match + + self._elements = util.OrderedDict() + + # standalone ForeignKeyConstraint - create + # associated ForeignKey objects which will be applied to hosted + # Column objects (in col.foreign_keys), either now or when attached + # to the Table for string-specified names + for col, refcol in zip(columns, refcolumns): + self._elements[col] = ForeignKey( + refcol, + _constraint=self, + name=self.name, + onupdate=self.onupdate, + ondelete=self.ondelete, + use_alter=self.use_alter, + link_to_name=self.link_to_name, + match=self.match, + deferrable=self.deferrable, + initially=self.initially + ) + + if table is not None: + self._set_parent_with_dispatch(table) + elif columns and \ + isinstance(columns[0], Column) and \ + columns[0].table is not None: + self._set_parent_with_dispatch(columns[0].table) + + def _validate_dest_table(self, table): + table_keys = set([elem._table_key() for elem in self._elements.values()]) + if None not in table_keys and len(table_keys) > 1: + elem0, elem1 = sorted(table_keys)[0:2] + raise exc.ArgumentError( + 'ForeignKeyConstraint on %s(%s) refers to ' + 'multiple remote tables: %s and %s' % ( + table.fullname, + self._col_description, + elem0, + elem1 + )) + + @property + def _col_description(self): + return ", ".join(self._elements) + + @property + def columns(self): + return list(self._elements) + + @property + def elements(self): + return list(self._elements.values()) + + def _set_parent(self, table): + super(ForeignKeyConstraint, self)._set_parent(table) + + self._validate_dest_table(table) + + for col, fk in self._elements.items(): + # string-specified column names now get + # resolved to Column objects + if isinstance(col, util.string_types): + try: + col = table.c[col] + except KeyError: + raise exc.ArgumentError( + "Can't create ForeignKeyConstraint " + "on table '%s': no column " + "named '%s' is present." % (table.description, col)) + + if not hasattr(fk, 'parent') or \ + fk.parent is not col: + fk._set_parent_with_dispatch(col) + + if self.use_alter: + def supports_alter(ddl, event, schema_item, bind, **kw): + return table in set(kw['tables']) and \ + bind.dialect.supports_alter + + event.listen(table.metadata, "after_create", + ddl.AddConstraint(self, on=supports_alter)) + event.listen(table.metadata, "before_drop", + ddl.DropConstraint(self, on=supports_alter)) + + def copy(self, schema=None, **kw): + fkc = ForeignKeyConstraint( + [x.parent.key for x in self._elements.values()], + [x._get_colspec(schema=schema) for x in self._elements.values()], + name=self.name, + onupdate=self.onupdate, + ondelete=self.ondelete, + use_alter=self.use_alter, + deferrable=self.deferrable, + initially=self.initially, + link_to_name=self.link_to_name, + match=self.match + ) + for self_fk, other_fk in zip( + self._elements.values(), + fkc._elements.values()): + self_fk._schema_item_copy(other_fk) + return self._schema_item_copy(fkc) + + +class PrimaryKeyConstraint(ColumnCollectionConstraint): + """A table-level PRIMARY KEY constraint. + + Defines a single column or composite PRIMARY KEY constraint. For a + no-frills primary key, adding ``primary_key=True`` to one or more + ``Column`` definitions is a shorthand equivalent for an unnamed single- or + multiple-column PrimaryKeyConstraint. + """ + + __visit_name__ = 'primary_key_constraint' + + def _set_parent(self, table): + super(PrimaryKeyConstraint, self)._set_parent(table) + + if table.primary_key in table.constraints: + table.constraints.remove(table.primary_key) + table.primary_key = self + table.constraints.add(self) + + for c in self.columns: + c.primary_key = True + + def _replace(self, col): + self.columns.replace(col) + + +class UniqueConstraint(ColumnCollectionConstraint): + """A table-level UNIQUE constraint. + + Defines a single column or composite UNIQUE constraint. For a no-frills, + single column constraint, adding ``unique=True`` to the ``Column`` + definition is a shorthand equivalent for an unnamed, single column + UniqueConstraint. + """ + + __visit_name__ = 'unique_constraint' + + +class Index(ColumnCollectionMixin, SchemaItem): + """A table-level INDEX. + + Defines a composite (one or more column) INDEX. + + E.g.:: + + sometable = Table("sometable", metadata, + Column("name", String(50)), + Column("address", String(100)) + ) + + Index("some_index", sometable.c.name) + + For a no-frills, single column index, adding + :class:`.Column` also supports ``index=True``:: + + sometable = Table("sometable", metadata, + Column("name", String(50), index=True) + ) + + For a composite index, multiple columns can be specified:: + + Index("some_index", sometable.c.name, sometable.c.address) + + Functional indexes are supported as well, keeping in mind that at least + one :class:`.Column` must be present:: + + Index("some_index", func.lower(sometable.c.name)) + + .. versionadded:: 0.8 support for functional and expression-based indexes. + + .. seealso:: + + :ref:`schema_indexes` - General information on :class:`.Index`. + + :ref:`postgresql_indexes` - PostgreSQL-specific options available for the + :class:`.Index` construct. + + :ref:`mysql_indexes` - MySQL-specific options available for the + :class:`.Index` construct. + + :ref:`mssql_indexes` - MSSQL-specific options available for the + :class:`.Index` construct. + + """ + + __visit_name__ = 'index' + + def __init__(self, name, *expressions, **kw): + """Construct an index object. + + :param name: + The name of the index + + :param \*expressions: + Column expressions to include in the index. The expressions + are normally instances of :class:`.Column`, but may also + be arbitrary SQL expressions which ultmately refer to a + :class:`.Column`. + + :param unique: + Defaults to False: create a unique index. + + :param \**kw: + Other keyword arguments may be interpreted by specific dialects. + + """ + self.table = None + + columns = [] + for expr in expressions: + if not isinstance(expr, ClauseElement): + columns.append(expr) + else: + cols = [] + visitors.traverse(expr, {}, {'column': cols.append}) + if cols: + columns.append(cols[0]) + else: + columns.append(expr) + + self.expressions = expressions + self.name = quoted_name(name, kw.pop("quote", None)) + self.unique = kw.pop('unique', False) + self.kwargs = kw + + # will call _set_parent() if table-bound column + # objects are present + ColumnCollectionMixin.__init__(self, *columns) + + + + def _set_parent(self, table): + ColumnCollectionMixin._set_parent(self, table) + + if self.table is not None and table is not self.table: + raise exc.ArgumentError( + "Index '%s' is against table '%s', and " + "cannot be associated with table '%s'." % ( + self.name, + self.table.description, + table.description + ) + ) + self.table = table + for c in self.columns: + if c.table != self.table: + raise exc.ArgumentError( + "Column '%s' is not part of table '%s'." % + (c, self.table.description) + ) + table.indexes.add(self) + + self.expressions = [ + expr if isinstance(expr, ClauseElement) + else colexpr + for expr, colexpr in zip(self.expressions, self.columns) + ] + + @property + def bind(self): + """Return the connectable associated with this Index.""" + + return self.table.bind + + def create(self, bind=None): + """Issue a ``CREATE`` statement for this + :class:`.Index`, using the given :class:`.Connectable` + for connectivity. + + .. seealso:: + + :meth:`.MetaData.create_all`. + + """ + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaGenerator, self) + return self + + def drop(self, bind=None): + """Issue a ``DROP`` statement for this + :class:`.Index`, using the given :class:`.Connectable` + for connectivity. + + .. seealso:: + + :meth:`.MetaData.drop_all`. + + """ + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaDropper, self) + + def __repr__(self): + return 'Index(%s)' % ( + ", ".join( + [repr(self.name)] + + [repr(c) for c in self.columns] + + (self.unique and ["unique=True"] or []) + )) + + +class MetaData(SchemaItem): + """A collection of :class:`.Table` objects and their associated schema + constructs. + + Holds a collection of :class:`.Table` objects as well as + an optional binding to an :class:`.Engine` or + :class:`.Connection`. If bound, the :class:`.Table` objects + in the collection and their columns may participate in implicit SQL + execution. + + The :class:`.Table` objects themselves are stored in the + :attr:`.MetaData.tables` dictionary. + + :class:`.MetaData` is a thread-safe object for read operations. Construction + of new tables within a single :class:`.MetaData` object, either explicitly + or via reflection, may not be completely thread-safe. + + .. seealso:: + + :ref:`metadata_describing` - Introduction to database metadata + + """ + + __visit_name__ = 'metadata' + + def __init__(self, bind=None, reflect=False, schema=None, + quote_schema=None): + """Create a new MetaData object. + + :param bind: + An Engine or Connection to bind to. May also be a string or URL + instance, these are passed to create_engine() and this MetaData will + be bound to the resulting engine. + + :param reflect: + Optional, automatically load all tables from the bound database. + Defaults to False. ``bind`` is required when this option is set. + + .. deprecated:: 0.8 + Please use the :meth:`.MetaData.reflect` method. + + :param schema: + The default schema to use for the :class:`.Table`, + :class:`.Sequence`, and other objects associated with this + :class:`.MetaData`. Defaults to ``None``. + + :param quote_schema: + Sets the ``quote_schema`` flag for those :class:`.Table`, + :class:`.Sequence`, and other objects which make usage of the + local ``schema`` name. + + .. versionadded:: 0.7.4 + ``schema`` and ``quote_schema`` parameters. + + """ + self.tables = util.immutabledict() + self.schema = quoted_name(schema, quote_schema) + self._schemas = set() + self._sequences = {} + self._fk_memos = collections.defaultdict(list) + + self.bind = bind + if reflect: + util.warn("reflect=True is deprecate; please " + "use the reflect() method.") + if not bind: + raise exc.ArgumentError( + "A bind must be supplied in conjunction " + "with reflect=True") + self.reflect() + + tables = None + """A dictionary of :class:`.Table` objects keyed to their name or "table key". + + The exact key is that determined by the :attr:`.Table.key` attribute; + for a table with no :attr:`.Table.schema` attribute, this is the same + as :attr:`.Table.name`. For a table with a schema, it is typically of the + form ``schemaname.tablename``. + + .. seealso:: + + :attr:`.MetaData.sorted_tables` + + """ + + def __repr__(self): + return 'MetaData(bind=%r)' % self.bind + + def __contains__(self, table_or_key): + if not isinstance(table_or_key, util.string_types): + table_or_key = table_or_key.key + return table_or_key in self.tables + + def _add_table(self, name, schema, table): + key = _get_table_key(name, schema) + dict.__setitem__(self.tables, key, table) + if schema: + self._schemas.add(schema) + + + + def _remove_table(self, name, schema): + key = _get_table_key(name, schema) + removed = dict.pop(self.tables, key, None) + if removed is not None: + for fk in removed.foreign_keys: + fk._remove_from_metadata(self) + if self._schemas: + self._schemas = set([t.schema + for t in self.tables.values() + if t.schema is not None]) + + + def __getstate__(self): + return {'tables': self.tables, + 'schema': self.schema, + 'schemas': self._schemas, + 'sequences': self._sequences, + 'fk_memos': self._fk_memos} + + def __setstate__(self, state): + self.tables = state['tables'] + self.schema = state['schema'] + self._bind = None + self._sequences = state['sequences'] + self._schemas = state['schemas'] + self._fk_memos = state['fk_memos'] + + def is_bound(self): + """True if this MetaData is bound to an Engine or Connection.""" + + return self._bind is not None + + def bind(self): + """An :class:`.Engine` or :class:`.Connection` to which this + :class:`.MetaData` is bound. + + Typically, a :class:`.Engine` is assigned to this attribute + so that "implicit execution" may be used, or alternatively + as a means of providing engine binding information to an + ORM :class:`.Session` object:: + + engine = create_engine("someurl://") + metadata.bind = engine + + .. seealso:: + + :ref:`dbengine_implicit` - background on "bound metadata" + + """ + return self._bind + + @util.dependencies("sqlalchemy.engine.url") + def _bind_to(self, url, bind): + """Bind this MetaData to an Engine, Connection, string or URL.""" + + if isinstance(bind, util.string_types + (url.URL, )): + self._bind = sqlalchemy.create_engine(bind) + else: + self._bind = bind + bind = property(bind, _bind_to) + + def clear(self): + """Clear all Table objects from this MetaData.""" + + dict.clear(self.tables) + self._schemas.clear() + self._fk_memos.clear() + + def remove(self, table): + """Remove the given Table object from this MetaData.""" + + self._remove_table(table.name, table.schema) + + @property + def sorted_tables(self): + """Returns a list of :class:`.Table` objects sorted in order of + foreign key dependency. + + The sorting will place :class:`.Table` objects that have dependencies + first, before the dependencies themselves, representing the + order in which they can be created. To get the order in which + the tables would be dropped, use the ``reversed()`` Python built-in. + + .. seealso:: + + :attr:`.MetaData.tables` + + :meth:`.Inspector.get_table_names` + + """ + return ddl.sort_tables(self.tables.values()) + + def reflect(self, bind=None, schema=None, views=False, only=None, + extend_existing=False, + autoload_replace=True): + """Load all available table definitions from the database. + + Automatically creates ``Table`` entries in this ``MetaData`` for any + table available in the database but not yet present in the + ``MetaData``. May be called multiple times to pick up tables recently + added to the database, however no special action is taken if a table + in this ``MetaData`` no longer exists in the database. + + :param bind: + A :class:`.Connectable` used to access the database; if None, uses + the existing bind on this ``MetaData``, if any. + + :param schema: + Optional, query and reflect tables from an alterate schema. + If None, the schema associated with this :class:`.MetaData` + is used, if any. + + :param views: + If True, also reflect views. + + :param only: + Optional. Load only a sub-set of available named tables. May be + specified as a sequence of names or a callable. + + If a sequence of names is provided, only those tables will be + reflected. An error is raised if a table is requested but not + available. Named tables already present in this ``MetaData`` are + ignored. + + If a callable is provided, it will be used as a boolean predicate to + filter the list of potential table names. The callable is called + with a table name and this ``MetaData`` instance as positional + arguments and should return a true value for any table to reflect. + + :param extend_existing: Passed along to each :class:`.Table` as + :paramref:`.Table.extend_existing`. + + .. versionadded:: 0.9.1 + + :param autoload_replace: Passed along to each :class:`.Table` as + :paramref:`.Table.autoload_replace`. + + .. versionadded:: 0.9.1 + + + """ + if bind is None: + bind = _bind_or_error(self) + + with bind.connect() as conn: + + reflect_opts = { + 'autoload': True, + 'autoload_with': conn, + 'extend_existing': extend_existing, + 'autoload_replace': autoload_replace + } + + if schema is None: + schema = self.schema + + if schema is not None: + reflect_opts['schema'] = schema + + available = util.OrderedSet(bind.engine.table_names(schema, + connection=conn)) + if views: + available.update( + bind.dialect.get_view_names(conn, schema) + ) + + if schema is not None: + available_w_schema = util.OrderedSet(["%s.%s" % (schema, name) + for name in available]) + else: + available_w_schema = available + + current = set(self.tables) + + if only is None: + load = [name for name, schname in + zip(available, available_w_schema) + if extend_existing or schname not in current] + elif util.callable(only): + load = [name for name, schname in + zip(available, available_w_schema) + if (extend_existing or schname not in current) + and only(name, self)] + else: + missing = [name for name in only if name not in available] + if missing: + s = schema and (" schema '%s'" % schema) or '' + raise exc.InvalidRequestError( + 'Could not reflect: requested table(s) not available ' + 'in %s%s: (%s)' % + (bind.engine.url, s, ', '.join(missing))) + load = [name for name in only if extend_existing or + name not in current] + + for name in load: + Table(name, self, **reflect_opts) + + def append_ddl_listener(self, event_name, listener): + """Append a DDL event listener to this ``MetaData``. + + .. deprecated:: 0.7 + See :class:`.DDLEvents`. + + """ + def adapt_listener(target, connection, **kw): + tables = kw['tables'] + listener(event, target, connection, tables=tables) + + event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) + + def create_all(self, bind=None, tables=None, checkfirst=True): + """Create all tables stored in this metadata. + + Conditional by default, will not attempt to recreate tables already + present in the target database. + + :param bind: + A :class:`.Connectable` used to access the + database; if None, uses the existing bind on this ``MetaData``, if + any. + + :param tables: + Optional list of ``Table`` objects, which is a subset of the total + tables in the ``MetaData`` (others are ignored). + + :param checkfirst: + Defaults to True, don't issue CREATEs for tables already present + in the target database. + + """ + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaGenerator, + self, + checkfirst=checkfirst, + tables=tables) + + def drop_all(self, bind=None, tables=None, checkfirst=True): + """Drop all tables stored in this metadata. + + Conditional by default, will not attempt to drop tables not present in + the target database. + + :param bind: + A :class:`.Connectable` used to access the + database; if None, uses the existing bind on this ``MetaData``, if + any. + + :param tables: + Optional list of ``Table`` objects, which is a subset of the + total tables in the ``MetaData`` (others are ignored). + + :param checkfirst: + Defaults to True, only issue DROPs for tables confirmed to be + present in the target database. + + """ + if bind is None: + bind = _bind_or_error(self) + bind._run_visitor(ddl.SchemaDropper, + self, + checkfirst=checkfirst, + tables=tables) + + +class ThreadLocalMetaData(MetaData): + """A MetaData variant that presents a different ``bind`` in every thread. + + Makes the ``bind`` property of the MetaData a thread-local value, allowing + this collection of tables to be bound to different ``Engine`` + implementations or connections in each thread. + + The ThreadLocalMetaData starts off bound to None in each thread. Binds + must be made explicitly by assigning to the ``bind`` property or using + ``connect()``. You can also re-bind dynamically multiple times per + thread, just like a regular ``MetaData``. + + """ + + __visit_name__ = 'metadata' + + def __init__(self): + """Construct a ThreadLocalMetaData.""" + + self.context = util.threading.local() + self.__engines = {} + super(ThreadLocalMetaData, self).__init__() + + def bind(self): + """The bound Engine or Connection for this thread. + + This property may be assigned an Engine or Connection, or assigned a + string or URL to automatically create a basic Engine for this bind + with ``create_engine()``.""" + + return getattr(self.context, '_engine', None) + + @util.dependencies("sqlalchemy.engine.url") + def _bind_to(self, url, bind): + """Bind to a Connectable in the caller's thread.""" + + if isinstance(bind, util.string_types + (url.URL, )): + try: + self.context._engine = self.__engines[bind] + except KeyError: + e = sqlalchemy.create_engine(bind) + self.__engines[bind] = e + self.context._engine = e + else: + # TODO: this is squirrely. we shouldnt have to hold onto engines + # in a case like this + if bind not in self.__engines: + self.__engines[bind] = bind + self.context._engine = bind + + bind = property(bind, _bind_to) + + def is_bound(self): + """True if there is a bind for this thread.""" + return (hasattr(self.context, '_engine') and + self.context._engine is not None) + + def dispose(self): + """Dispose all bound engines, in all thread contexts.""" + + for e in self.__engines.values(): + if hasattr(e, 'dispose'): + e.dispose() + + + diff --git a/libs/sqlalchemy/sql/selectable.py b/libs/sqlalchemy/sql/selectable.py new file mode 100644 index 00000000..951268b2 --- /dev/null +++ b/libs/sqlalchemy/sql/selectable.py @@ -0,0 +1,3001 @@ +# sql/selectable.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""The :class:`.FromClause` class of SQL expression elements, representing +SQL tables and derived rowsets. + +""" + +from .elements import ClauseElement, TextClause, ClauseList, \ + and_, Grouping, UnaryExpression, literal_column +from .elements import _clone, \ + _literal_as_text, _interpret_as_column_or_from, _expand_cloned,\ + _select_iterables, _anonymous_label, _clause_element_as_expr,\ + _cloned_intersection, _cloned_difference, True_, _only_column_elements +from .base import Immutable, Executable, _generative, \ + ColumnCollection, ColumnSet, _from_objects, Generative +from . import type_api +from .. import inspection +from .. import util +from .. import exc +from operator import attrgetter +from . import operators +import operator +from .annotation import Annotated +import itertools + +def _interpret_as_from(element): + insp = inspection.inspect(element, raiseerr=False) + if insp is None: + if isinstance(element, util.string_types): + return TextClause(util.text_type(element)) + elif hasattr(insp, "selectable"): + return insp.selectable + raise exc.ArgumentError("FROM expression expected") + +def _interpret_as_select(element): + element = _interpret_as_from(element) + if isinstance(element, Alias): + element = element.original + if not isinstance(element, Select): + element = element.select() + return element + +def subquery(alias, *args, **kwargs): + """Return an :class:`.Alias` object derived + from a :class:`.Select`. + + name + alias name + + \*args, \**kwargs + + all other arguments are delivered to the + :func:`select` function. + + """ + return Select(*args, **kwargs).alias(alias) + + + +def alias(selectable, name=None, flat=False): + """Return an :class:`.Alias` object. + + An :class:`.Alias` represents any :class:`.FromClause` + with an alternate name assigned within SQL, typically using the ``AS`` + clause when generated, e.g. ``SELECT * FROM table AS aliasname``. + + Similar functionality is available via the + :meth:`~.FromClause.alias` method + available on all :class:`.FromClause` subclasses. + + When an :class:`.Alias` is created from a :class:`.Table` object, + this has the effect of the table being rendered + as ``tablename AS aliasname`` in a SELECT statement. + + For :func:`.select` objects, the effect is that of creating a named + subquery, i.e. ``(select ...) AS aliasname``. + + The ``name`` parameter is optional, and provides the name + to use in the rendered SQL. If blank, an "anonymous" name + will be deterministically generated at compile time. + Deterministic means the name is guaranteed to be unique against + other constructs used in the same statement, and will also be the + same name for each successive compilation of the same statement + object. + + :param selectable: any :class:`.FromClause` subclass, + such as a table, select statement, etc. + + :param name: string name to be assigned as the alias. + If ``None``, a name will be deterministically generated + at compile time. + + :param flat: Will be passed through to if the given selectable + is an instance of :class:`.Join` - see :meth:`.Join.alias` + for details. + + .. versionadded:: 0.9.0 + + """ + return selectable.alias(name=name, flat=flat) + + +class Selectable(ClauseElement): + """mark a class as being selectable""" + __visit_name__ = 'selectable' + + is_selectable = True + + @property + def selectable(self): + return self + + +class FromClause(Selectable): + """Represent an element that can be used within the ``FROM`` + clause of a ``SELECT`` statement. + + The most common forms of :class:`.FromClause` are the + :class:`.Table` and the :func:`.select` constructs. Key + features common to all :class:`.FromClause` objects include: + + * a :attr:`.c` collection, which provides per-name access to a collection + of :class:`.ColumnElement` objects. + * a :attr:`.primary_key` attribute, which is a collection of all those + :class:`.ColumnElement` objects that indicate the ``primary_key`` flag. + * Methods to generate various derivations of a "from" clause, including + :meth:`.FromClause.alias`, :meth:`.FromClause.join`, + :meth:`.FromClause.select`. + + + """ + __visit_name__ = 'fromclause' + named_with_column = False + _hide_froms = [] + + schema = None + """Define the 'schema' attribute for this :class:`.FromClause`. + + This is typically ``None`` for most objects except that of :class:`.Table`, + where it is taken as the value of the :paramref:`.Table.schema` argument. + + """ + + _memoized_property = util.group_expirable_memoized_property(["_columns"]) + + @util.dependencies("sqlalchemy.sql.functions") + def count(self, functions, whereclause=None, **params): + """return a SELECT COUNT generated against this + :class:`.FromClause`.""" + + if self.primary_key: + col = list(self.primary_key)[0] + else: + col = list(self.columns)[0] + return Select( + [functions.func.count(col).label('tbl_row_count')], + whereclause, + from_obj=[self], + **params) + + def select(self, whereclause=None, **params): + """return a SELECT of this :class:`.FromClause`. + + .. seealso:: + + :func:`~.sql.expression.select` - general purpose + method which allows for arbitrary column lists. + + """ + + return Select([self], whereclause, **params) + + def join(self, right, onclause=None, isouter=False): + """return a join of this :class:`.FromClause` against another + :class:`.FromClause`.""" + + return Join(self, right, onclause, isouter) + + def outerjoin(self, right, onclause=None): + """return an outer join of this :class:`.FromClause` against another + :class:`.FromClause`.""" + + return Join(self, right, onclause, True) + + def alias(self, name=None, flat=False): + """return an alias of this :class:`.FromClause`. + + This is shorthand for calling:: + + from sqlalchemy import alias + a = alias(self, name=name) + + See :func:`~.expression.alias` for details. + + """ + + return Alias(self, name) + + def is_derived_from(self, fromclause): + """Return True if this FromClause is 'derived' from the given + FromClause. + + An example would be an Alias of a Table is derived from that Table. + + """ + # this is essentially an "identity" check in the base class. + # Other constructs override this to traverse through + # contained elements. + return fromclause in self._cloned_set + + def _is_lexical_equivalent(self, other): + """Return True if this FromClause and the other represent + the same lexical identity. + + This tests if either one is a copy of the other, or + if they are the same via annotation identity. + + """ + return self._cloned_set.intersection(other._cloned_set) + + @util.dependencies("sqlalchemy.sql.util") + def replace_selectable(self, sqlutil, old, alias): + """replace all occurrences of FromClause 'old' with the given Alias + object, returning a copy of this :class:`.FromClause`. + + """ + + return sqlutil.ClauseAdapter(alias).traverse(self) + + def correspond_on_equivalents(self, column, equivalents): + """Return corresponding_column for the given column, or if None + search for a match in the given dictionary. + + """ + col = self.corresponding_column(column, require_embedded=True) + if col is None and col in equivalents: + for equiv in equivalents[col]: + nc = self.corresponding_column(equiv, require_embedded=True) + if nc: + return nc + return col + + def corresponding_column(self, column, require_embedded=False): + """Given a :class:`.ColumnElement`, return the exported + :class:`.ColumnElement` object from this :class:`.Selectable` + which corresponds to that original + :class:`~sqlalchemy.schema.Column` via a common ancestor + column. + + :param column: the target :class:`.ColumnElement` to be matched + + :param require_embedded: only return corresponding columns for + the given :class:`.ColumnElement`, if the given :class:`.ColumnElement` + is actually present within a sub-element + of this :class:`.FromClause`. Normally the column will match if + it merely shares a common ancestor with one of the exported + columns of this :class:`.FromClause`. + + """ + + def embedded(expanded_proxy_set, target_set): + for t in target_set.difference(expanded_proxy_set): + if not set(_expand_cloned([t]) + ).intersection(expanded_proxy_set): + return False + return True + + # don't dig around if the column is locally present + if self.c.contains_column(column): + return column + col, intersect = None, None + target_set = column.proxy_set + cols = self.c + for c in cols: + expanded_proxy_set = set(_expand_cloned(c.proxy_set)) + i = target_set.intersection(expanded_proxy_set) + if i and (not require_embedded + or embedded(expanded_proxy_set, target_set)): + if col is None: + + # no corresponding column yet, pick this one. + + col, intersect = c, i + elif len(i) > len(intersect): + + # 'c' has a larger field of correspondence than + # 'col'. i.e. selectable.c.a1_x->a1.c.x->table.c.x + # matches a1.c.x->table.c.x better than + # selectable.c.x->table.c.x does. + + col, intersect = c, i + elif i == intersect: + + # they have the same field of correspondence. see + # which proxy_set has fewer columns in it, which + # indicates a closer relationship with the root + # column. Also take into account the "weight" + # attribute which CompoundSelect() uses to give + # higher precedence to columns based on vertical + # position in the compound statement, and discard + # columns that have no reference to the target + # column (also occurs with CompoundSelect) + + col_distance = util.reduce(operator.add, + [sc._annotations.get('weight', 1) for sc in + col.proxy_set if sc.shares_lineage(column)]) + c_distance = util.reduce(operator.add, + [sc._annotations.get('weight', 1) for sc in + c.proxy_set if sc.shares_lineage(column)]) + if c_distance < col_distance: + col, intersect = c, i + return col + + @property + def description(self): + """a brief description of this FromClause. + + Used primarily for error message formatting. + + """ + return getattr(self, 'name', self.__class__.__name__ + " object") + + def _reset_exported(self): + """delete memoized collections when a FromClause is cloned.""" + + self._memoized_property.expire_instance(self) + + @_memoized_property + def columns(self): + """A named-based collection of :class:`.ColumnElement` objects + maintained by this :class:`.FromClause`. + + The :attr:`.columns`, or :attr:`.c` collection, is the gateway + to the construction of SQL expressions using table-bound or + other selectable-bound columns:: + + select([mytable]).where(mytable.c.somecolumn == 5) + + """ + + if '_columns' not in self.__dict__: + self._init_collections() + self._populate_column_collection() + return self._columns.as_immutable() + + @_memoized_property + def primary_key(self): + """Return the collection of Column objects which comprise the + primary key of this FromClause.""" + + self._init_collections() + self._populate_column_collection() + return self.primary_key + + @_memoized_property + def foreign_keys(self): + """Return the collection of ForeignKey objects which this + FromClause references.""" + + self._init_collections() + self._populate_column_collection() + return self.foreign_keys + + c = property(attrgetter('columns'), + doc="An alias for the :attr:`.columns` attribute.") + _select_iterable = property(attrgetter('columns')) + + def _init_collections(self): + assert '_columns' not in self.__dict__ + assert 'primary_key' not in self.__dict__ + assert 'foreign_keys' not in self.__dict__ + + self._columns = ColumnCollection() + self.primary_key = ColumnSet() + self.foreign_keys = set() + + @property + def _cols_populated(self): + return '_columns' in self.__dict__ + + def _populate_column_collection(self): + """Called on subclasses to establish the .c collection. + + Each implementation has a different way of establishing + this collection. + + """ + + def _refresh_for_new_column(self, column): + """Given a column added to the .c collection of an underlying + selectable, produce the local version of that column, assuming this + selectable ultimately should proxy this column. + + this is used to "ping" a derived selectable to add a new column + to its .c. collection when a Column has been added to one of the + Table objects it ultimtely derives from. + + If the given selectable hasn't populated it's .c. collection yet, + it should at least pass on the message to the contained selectables, + but it will return None. + + This method is currently used by Declarative to allow Table + columns to be added to a partially constructed inheritance + mapping that may have already produced joins. The method + isn't public right now, as the full span of implications + and/or caveats aren't yet clear. + + It's also possible that this functionality could be invoked by + default via an event, which would require that + selectables maintain a weak referencing collection of all + derivations. + + """ + if not self._cols_populated: + return None + elif column.key in self.columns and self.columns[column.key] is column: + return column + else: + return None + + +class Join(FromClause): + """represent a ``JOIN`` construct between two :class:`.FromClause` + elements. + + The public constructor function for :class:`.Join` is the module-level + :func:`join()` function, as well as the :func:`join()` method available + off all :class:`.FromClause` subclasses. + + """ + __visit_name__ = 'join' + + def __init__(self, left, right, onclause=None, isouter=False): + """Construct a new :class:`.Join`. + + The usual entrypoint here is the :func:`~.expression.join` + function or the :meth:`.FromClause.join` method of any + :class:`.FromClause` object. + + """ + self.left = _interpret_as_from(left) + self.right = _interpret_as_from(right).self_group() + + if onclause is None: + self.onclause = self._match_primaries(self.left, self.right) + else: + self.onclause = onclause + + self.isouter = isouter + + @classmethod + def _create_outerjoin(cls, left, right, onclause=None): + """Return an ``OUTER JOIN`` clause element. + + The returned object is an instance of :class:`.Join`. + + Similar functionality is also available via the + :meth:`~.FromClause.outerjoin()` method on any + :class:`.FromClause`. + + :param left: The left side of the join. + + :param right: The right side of the join. + + :param onclause: Optional criterion for the ``ON`` clause, is + derived from foreign key relationships established between + left and right otherwise. + + To chain joins together, use the :meth:`.FromClause.join` or + :meth:`.FromClause.outerjoin` methods on the resulting + :class:`.Join` object. + + """ + return cls(left, right, onclause, isouter=True) + + + @classmethod + def _create_join(cls, left, right, onclause=None, isouter=False): + """Return a ``JOIN`` clause element (regular inner join). + + The returned object is an instance of :class:`.Join`. + + Similar functionality is also available via the + :meth:`~.FromClause.join()` method on any + :class:`.FromClause`. + + :param left: The left side of the join. + + :param right: The right side of the join. + + :param onclause: Optional criterion for the ``ON`` clause, is + derived from foreign key relationships established between + left and right otherwise. + + :param isouter: if True, produce an outer join; synonymous + with :func:`.outerjoin`. + + To chain joins together, use the :meth:`.FromClause.join` or + :meth:`.FromClause.outerjoin` methods on the resulting + :class:`.Join` object. + + + """ + return cls(left, right, onclause, isouter) + + + @property + def description(self): + return "Join object on %s(%d) and %s(%d)" % ( + self.left.description, + id(self.left), + self.right.description, + id(self.right)) + + def is_derived_from(self, fromclause): + return fromclause is self or \ + self.left.is_derived_from(fromclause) or \ + self.right.is_derived_from(fromclause) + + def self_group(self, against=None): + return FromGrouping(self) + + @util.dependencies("sqlalchemy.sql.util") + def _populate_column_collection(self, sqlutil): + columns = [c for c in self.left.columns] + \ + [c for c in self.right.columns] + + self.primary_key.extend(sqlutil.reduce_columns( + (c for c in columns if c.primary_key), self.onclause)) + self._columns.update((col._label, col) for col in columns) + self.foreign_keys.update(itertools.chain( + *[col.foreign_keys for col in columns])) + + def _refresh_for_new_column(self, column): + col = self.left._refresh_for_new_column(column) + if col is None: + col = self.right._refresh_for_new_column(column) + if col is not None: + if self._cols_populated: + self._columns[col._label] = col + self.foreign_keys.add(col) + if col.primary_key: + self.primary_key.add(col) + return col + return None + + def _copy_internals(self, clone=_clone, **kw): + self._reset_exported() + self.left = clone(self.left, **kw) + self.right = clone(self.right, **kw) + self.onclause = clone(self.onclause, **kw) + + def get_children(self, **kwargs): + return self.left, self.right, self.onclause + + def _match_primaries(self, left, right): + if isinstance(left, Join): + left_right = left.right + else: + left_right = None + return self._join_condition(left, right, a_subset=left_right) + + @classmethod + def _join_condition(cls, a, b, ignore_nonexistent_tables=False, + a_subset=None, + consider_as_foreign_keys=None): + """create a join condition between two tables or selectables. + + e.g.:: + + join_condition(tablea, tableb) + + would produce an expression along the lines of:: + + tablea.c.id==tableb.c.tablea_id + + The join is determined based on the foreign key relationships + between the two selectables. If there are multiple ways + to join, or no way to join, an error is raised. + + :param ignore_nonexistent_tables: Deprecated - this + flag is no longer used. Only resolution errors regarding + the two given tables are propagated. + + :param a_subset: An optional expression that is a sub-component + of ``a``. An attempt will be made to join to just this sub-component + first before looking at the full ``a`` construct, and if found + will be successful even if there are other ways to join to ``a``. + This allows the "right side" of a join to be passed thereby + providing a "natural join". + + """ + crit = [] + constraints = set() + + for left in (a_subset, a): + if left is None: + continue + for fk in sorted( + b.foreign_keys, + key=lambda fk: fk.parent._creation_order): + if consider_as_foreign_keys is not None and \ + fk.parent not in consider_as_foreign_keys: + continue + try: + col = fk.get_referent(left) + except exc.NoReferenceError as nrte: + if nrte.table_name == left.name: + raise + else: + continue + + if col is not None: + crit.append(col == fk.parent) + constraints.add(fk.constraint) + if left is not b: + for fk in sorted( + left.foreign_keys, + key=lambda fk: fk.parent._creation_order): + if consider_as_foreign_keys is not None and \ + fk.parent not in consider_as_foreign_keys: + continue + try: + col = fk.get_referent(b) + except exc.NoReferenceError as nrte: + if nrte.table_name == b.name: + raise + else: + # this is totally covered. can't get + # coverage to mark it. + continue + + if col is not None: + crit.append(col == fk.parent) + constraints.add(fk.constraint) + if crit: + break + + if len(crit) == 0: + if isinstance(b, FromGrouping): + hint = " Perhaps you meant to convert the right side to a "\ + "subquery using alias()?" + else: + hint = "" + raise exc.NoForeignKeysError( + "Can't find any foreign key relationships " + "between '%s' and '%s'.%s" % (a.description, b.description, hint)) + elif len(constraints) > 1: + raise exc.AmbiguousForeignKeysError( + "Can't determine join between '%s' and '%s'; " + "tables have more than one foreign key " + "constraint relationship between them. " + "Please specify the 'onclause' of this " + "join explicitly." % (a.description, b.description)) + elif len(crit) == 1: + return (crit[0]) + else: + return and_(*crit) + + + def select(self, whereclause=None, **kwargs): + """Create a :class:`.Select` from this :class:`.Join`. + + The equivalent long-hand form, given a :class:`.Join` object + ``j``, is:: + + from sqlalchemy import select + j = select([j.left, j.right], **kw).\\ + where(whereclause).\\ + select_from(j) + + :param whereclause: the WHERE criterion that will be sent to + the :func:`select()` function + + :param \**kwargs: all other kwargs are sent to the + underlying :func:`select()` function. + + """ + collist = [self.left, self.right] + + return Select(collist, whereclause, from_obj=[self], **kwargs) + + @property + def bind(self): + return self.left.bind or self.right.bind + + @util.dependencies("sqlalchemy.sql.util") + def alias(self, sqlutil, name=None, flat=False): + """return an alias of this :class:`.Join`. + + The default behavior here is to first produce a SELECT + construct from this :class:`.Join`, then to produce a + :class:`.Alias` from that. So given a join of the form:: + + j = table_a.join(table_b, table_a.c.id == table_b.c.a_id) + + The JOIN by itself would look like:: + + table_a JOIN table_b ON table_a.id = table_b.a_id + + Whereas the alias of the above, ``j.alias()``, would in a + SELECT context look like:: + + (SELECT table_a.id AS table_a_id, table_b.id AS table_b_id, + table_b.a_id AS table_b_a_id + FROM table_a + JOIN table_b ON table_a.id = table_b.a_id) AS anon_1 + + The equivalent long-hand form, given a :class:`.Join` object + ``j``, is:: + + from sqlalchemy import select, alias + j = alias( + select([j.left, j.right]).\\ + select_from(j).\\ + with_labels(True).\\ + correlate(False), + name=name + ) + + The selectable produced by :meth:`.Join.alias` features the same + columns as that of the two individual selectables presented under + a single name - the individual columns are "auto-labeled", meaning + the ``.c.`` collection of the resulting :class:`.Alias` represents + the names of the individual columns using a ``_`` + scheme:: + + j.c.table_a_id + j.c.table_b_a_id + + :meth:`.Join.alias` also features an alternate + option for aliasing joins which produces no enclosing SELECT and + does not normally apply labels to the column names. The + ``flat=True`` option will call :meth:`.FromClause.alias` + against the left and right sides individually. + Using this option, no new ``SELECT`` is produced; + we instead, from a construct as below:: + + j = table_a.join(table_b, table_a.c.id == table_b.c.a_id) + j = j.alias(flat=True) + + we get a result like this:: + + table_a AS table_a_1 JOIN table_b AS table_b_1 ON + table_a_1.id = table_b_1.a_id + + The ``flat=True`` argument is also propagated to the contained + selectables, so that a composite join such as:: + + j = table_a.join( + table_b.join(table_c, + table_b.c.id == table_c.c.b_id), + table_b.c.a_id == table_a.c.id + ).alias(flat=True) + + Will produce an expression like:: + + table_a AS table_a_1 JOIN ( + table_b AS table_b_1 JOIN table_c AS table_c_1 + ON table_b_1.id = table_c_1.b_id + ) ON table_a_1.id = table_b_1.a_id + + The standalone :func:`~.expression.alias` function as well as the + base :meth:`.FromClause.alias` method also support the ``flat=True`` + argument as a no-op, so that the argument can be passed to the + ``alias()`` method of any selectable. + + .. versionadded:: 0.9.0 Added the ``flat=True`` option to create + "aliases" of joins without enclosing inside of a SELECT + subquery. + + :param name: name given to the alias. + + :param flat: if True, produce an alias of the left and right + sides of this :class:`.Join` and return the join of those + two selectables. This produces join expression that does not + include an enclosing SELECT. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :func:`~.expression.alias` + + """ + if flat: + assert name is None, "Can't send name argument with flat" + left_a, right_a = self.left.alias(flat=True), \ + self.right.alias(flat=True) + adapter = sqlutil.ClauseAdapter(left_a).\ + chain(sqlutil.ClauseAdapter(right_a)) + + return left_a.join(right_a, + adapter.traverse(self.onclause), isouter=self.isouter) + else: + return self.select(use_labels=True, correlate=False).alias(name) + + @property + def _hide_froms(self): + return itertools.chain(*[_from_objects(x.left, x.right) + for x in self._cloned_set]) + + @property + def _from_objects(self): + return [self] + \ + self.onclause._from_objects + \ + self.left._from_objects + \ + self.right._from_objects + + +class Alias(FromClause): + """Represents an table or selectable alias (AS). + + Represents an alias, as typically applied to any table or + sub-select within a SQL statement using the ``AS`` keyword (or + without the keyword on certain databases such as Oracle). + + This object is constructed from the :func:`~.expression.alias` module level + function as well as the :meth:`.FromClause.alias` method available on all + :class:`.FromClause` subclasses. + + """ + + __visit_name__ = 'alias' + named_with_column = True + + def __init__(self, selectable, name=None): + baseselectable = selectable + while isinstance(baseselectable, Alias): + baseselectable = baseselectable.element + self.original = baseselectable + self.supports_execution = baseselectable.supports_execution + if self.supports_execution: + self._execution_options = baseselectable._execution_options + self.element = selectable + if name is None: + if self.original.named_with_column: + name = getattr(self.original, 'name', None) + name = _anonymous_label('%%(%d %s)s' % (id(self), name + or 'anon')) + self.name = name + + @property + def description(self): + if util.py3k: + return self.name + else: + return self.name.encode('ascii', 'backslashreplace') + + def as_scalar(self): + try: + return self.element.as_scalar() + except AttributeError: + raise AttributeError("Element %s does not support " + "'as_scalar()'" % self.element) + + def is_derived_from(self, fromclause): + if fromclause in self._cloned_set: + return True + return self.element.is_derived_from(fromclause) + + def _populate_column_collection(self): + for col in self.element.columns: + col._make_proxy(self) + + def _refresh_for_new_column(self, column): + col = self.element._refresh_for_new_column(column) + if col is not None: + if not self._cols_populated: + return None + else: + return col._make_proxy(self) + else: + return None + + def _copy_internals(self, clone=_clone, **kw): + # don't apply anything to an aliased Table + # for now. May want to drive this from + # the given **kw. + if isinstance(self.element, TableClause): + return + self._reset_exported() + self.element = clone(self.element, **kw) + baseselectable = self.element + while isinstance(baseselectable, Alias): + baseselectable = baseselectable.element + self.original = baseselectable + + def get_children(self, column_collections=True, **kw): + if column_collections: + for c in self.c: + yield c + yield self.element + + @property + def _from_objects(self): + return [self] + + @property + def bind(self): + return self.element.bind + + +class CTE(Alias): + """Represent a Common Table Expression. + + The :class:`.CTE` object is obtained using the + :meth:`.SelectBase.cte` method from any selectable. + See that method for complete examples. + + .. versionadded:: 0.7.6 + + """ + __visit_name__ = 'cte' + + def __init__(self, selectable, + name=None, + recursive=False, + _cte_alias=None, + _restates=frozenset()): + self.recursive = recursive + self._cte_alias = _cte_alias + self._restates = _restates + super(CTE, self).__init__(selectable, name=name) + + def alias(self, name=None, flat=False): + return CTE( + self.original, + name=name, + recursive=self.recursive, + _cte_alias=self, + ) + + def union(self, other): + return CTE( + self.original.union(other), + name=self.name, + recursive=self.recursive, + _restates=self._restates.union([self]) + ) + + def union_all(self, other): + return CTE( + self.original.union_all(other), + name=self.name, + recursive=self.recursive, + _restates=self._restates.union([self]) + ) + + + + +class FromGrouping(FromClause): + """Represent a grouping of a FROM clause""" + __visit_name__ = 'grouping' + + def __init__(self, element): + self.element = element + + def _init_collections(self): + pass + + @property + def columns(self): + return self.element.columns + + @property + def primary_key(self): + return self.element.primary_key + + @property + def foreign_keys(self): + return self.element.foreign_keys + + def is_derived_from(self, element): + return self.element.is_derived_from(element) + + def alias(self, **kw): + return FromGrouping(self.element.alias(**kw)) + + @property + def _hide_froms(self): + return self.element._hide_froms + + def get_children(self, **kwargs): + return self.element, + + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) + + @property + def _from_objects(self): + return self.element._from_objects + + def __getattr__(self, attr): + return getattr(self.element, attr) + + def __getstate__(self): + return {'element': self.element} + + def __setstate__(self, state): + self.element = state['element'] + +class TableClause(Immutable, FromClause): + """Represents a minimal "table" construct. + + This is a lightweight table object that has only a name and a + collection of columns, which are typically produced + by the :func:`.expression.column` function:: + + from sqlalchemy.sql import table, column + + user = table("user", + column("id"), + column("name"), + column("description"), + ) + + The :class:`.TableClause` construct serves as the base for + the more commonly used :class:`~.schema.Table` object, providing + the usual set of :class:`~.expression.FromClause` services including + the ``.c.`` collection and statement generation methods. + + It does **not** provide all the additional schema-level services + of :class:`~.schema.Table`, including constraints, references to other + tables, or support for :class:`.MetaData`-level services. It's useful + on its own as an ad-hoc construct used to generate quick SQL + statements when a more fully fledged :class:`~.schema.Table` + is not on hand. + + """ + + __visit_name__ = 'table' + + named_with_column = True + + implicit_returning = False + """:class:`.TableClause` doesn't support having a primary key or column + -level defaults, so implicit returning doesn't apply.""" + + _autoincrement_column = None + """No PK or default support so no autoincrement column.""" + + def __init__(self, name, *columns): + """Produce a new :class:`.TableClause`. + + The object returned is an instance of :class:`.TableClause`, which + represents the "syntactical" portion of the schema-level + :class:`~.schema.Table` object. + It may be used to construct lightweight table constructs. + + Note that the :func:`.expression.table` function is not part of + the ``sqlalchemy`` namespace. It must be imported from the + ``sql`` package:: + + from sqlalchemy.sql import table, column + + :param name: Name of the table. + + :param columns: A collection of :func:`.expression.column` constructs. + + """ + + super(TableClause, self).__init__() + self.name = self.fullname = name + self._columns = ColumnCollection() + self.primary_key = ColumnSet() + self.foreign_keys = set() + for c in columns: + self.append_column(c) + + def _init_collections(self): + pass + + @util.memoized_property + def description(self): + if util.py3k: + return self.name + else: + return self.name.encode('ascii', 'backslashreplace') + + def append_column(self, c): + self._columns[c.key] = c + c.table = self + + def get_children(self, column_collections=True, **kwargs): + if column_collections: + return [c for c in self.c] + else: + return [] + + @util.dependencies("sqlalchemy.sql.functions") + def count(self, functions, whereclause=None, **params): + """return a SELECT COUNT generated against this + :class:`.TableClause`.""" + + if self.primary_key: + col = list(self.primary_key)[0] + else: + col = list(self.columns)[0] + return Select( + [functions.func.count(col).label('tbl_row_count')], + whereclause, + from_obj=[self], + **params) + + @util.dependencies("sqlalchemy.sql.dml") + def insert(self, dml, values=None, inline=False, **kwargs): + """Generate an :func:`.insert` construct against this + :class:`.TableClause`. + + E.g.:: + + table.insert().values(name='foo') + + See :func:`.insert` for argument and usage information. + + """ + + return dml.Insert(self, values=values, inline=inline, **kwargs) + + @util.dependencies("sqlalchemy.sql.dml") + def update(self, dml, whereclause=None, values=None, inline=False, **kwargs): + """Generate an :func:`.update` construct against this + :class:`.TableClause`. + + E.g.:: + + table.update().where(table.c.id==7).values(name='foo') + + See :func:`.update` for argument and usage information. + + """ + + return dml.Update(self, whereclause=whereclause, + values=values, inline=inline, **kwargs) + + @util.dependencies("sqlalchemy.sql.dml") + def delete(self, dml, whereclause=None, **kwargs): + """Generate a :func:`.delete` construct against this + :class:`.TableClause`. + + E.g.:: + + table.delete().where(table.c.id==7) + + See :func:`.delete` for argument and usage information. + + """ + + return dml.Delete(self, whereclause, **kwargs) + + @property + def _from_objects(self): + return [self] + + +class ForUpdateArg(ClauseElement): + + @classmethod + def parse_legacy_select(self, arg): + """Parse the for_update arugment of :func:`.select`. + + :param mode: Defines the lockmode to use. + + ``None`` - translates to no lockmode + + ``'update'`` - translates to ``FOR UPDATE`` + (standard SQL, supported by most dialects) + + ``'nowait'`` - translates to ``FOR UPDATE NOWAIT`` + (supported by Oracle, PostgreSQL 8.1 upwards) + + ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL), + and ``FOR SHARE`` (for PostgreSQL) + + ``'read_nowait'`` - translates to ``FOR SHARE NOWAIT`` + (supported by PostgreSQL). ``FOR SHARE`` and + ``FOR SHARE NOWAIT`` (PostgreSQL). + + """ + if arg in (None, False): + return None + + nowait = read = False + if arg == 'nowait': + nowait = True + elif arg == 'read': + read = True + elif arg == 'read_nowait': + read = nowait = True + elif arg is not True: + raise exc.ArgumentError("Unknown for_update argument: %r" % arg) + + return ForUpdateArg(read=read, nowait=nowait) + + @property + def legacy_for_update_value(self): + if self.read and not self.nowait: + return "read" + elif self.read and self.nowait: + return "read_nowait" + elif self.nowait: + return "nowait" + else: + return True + + def _copy_internals(self, clone=_clone, **kw): + if self.of is not None: + self.of = [clone(col, **kw) for col in self.of] + + def __init__(self, nowait=False, read=False, of=None): + """Represents arguments specified to :meth:`.Select.for_update`. + + .. versionadded:: 0.9.0 + """ + + self.nowait = nowait + self.read = read + if of is not None: + self.of = [_interpret_as_column_or_from(elem) + for elem in util.to_list(of)] + else: + self.of = None + + +class SelectBase(Executable, FromClause): + """Base class for SELECT statements. + + + This includes :class:`.Select`, :class:`.CompoundSelect` and + :class:`.TextAsFrom`. + + + """ + + def as_scalar(self): + """return a 'scalar' representation of this selectable, which can be + used as a column expression. + + Typically, a select statement which has only one column in its columns + clause is eligible to be used as a scalar expression. + + The returned object is an instance of + :class:`ScalarSelect`. + + """ + return ScalarSelect(self) + + + def label(self, name): + """return a 'scalar' representation of this selectable, embedded as a + subquery with a label. + + .. seealso:: + + :meth:`~.SelectBase.as_scalar`. + + """ + return self.as_scalar().label(name) + + def cte(self, name=None, recursive=False): + """Return a new :class:`.CTE`, or Common Table Expression instance. + + Common table expressions are a SQL standard whereby SELECT + statements can draw upon secondary statements specified along + with the primary statement, using a clause called "WITH". + Special semantics regarding UNION can also be employed to + allow "recursive" queries, where a SELECT statement can draw + upon the set of rows that have previously been selected. + + SQLAlchemy detects :class:`.CTE` objects, which are treated + similarly to :class:`.Alias` objects, as special elements + to be delivered to the FROM clause of the statement as well + as to a WITH clause at the top of the statement. + + .. versionadded:: 0.7.6 + + :param name: name given to the common table expression. Like + :meth:`._FromClause.alias`, the name can be left as ``None`` + in which case an anonymous symbol will be used at query + compile time. + :param recursive: if ``True``, will render ``WITH RECURSIVE``. + A recursive common table expression is intended to be used in + conjunction with UNION ALL in order to derive rows + from those already selected. + + The following examples illustrate two examples from + Postgresql's documentation at + http://www.postgresql.org/docs/8.4/static/queries-with.html. + + Example 1, non recursive:: + + from sqlalchemy import Table, Column, String, Integer, MetaData, \\ + select, func + + metadata = MetaData() + + orders = Table('orders', metadata, + Column('region', String), + Column('amount', Integer), + Column('product', String), + Column('quantity', Integer) + ) + + regional_sales = select([ + orders.c.region, + func.sum(orders.c.amount).label('total_sales') + ]).group_by(orders.c.region).cte("regional_sales") + + + top_regions = select([regional_sales.c.region]).\\ + where( + regional_sales.c.total_sales > + select([ + func.sum(regional_sales.c.total_sales)/10 + ]) + ).cte("top_regions") + + statement = select([ + orders.c.region, + orders.c.product, + func.sum(orders.c.quantity).label("product_units"), + func.sum(orders.c.amount).label("product_sales") + ]).where(orders.c.region.in_( + select([top_regions.c.region]) + )).group_by(orders.c.region, orders.c.product) + + result = conn.execute(statement).fetchall() + + Example 2, WITH RECURSIVE:: + + from sqlalchemy import Table, Column, String, Integer, MetaData, \\ + select, func + + metadata = MetaData() + + parts = Table('parts', metadata, + Column('part', String), + Column('sub_part', String), + Column('quantity', Integer), + ) + + included_parts = select([ + parts.c.sub_part, + parts.c.part, + parts.c.quantity]).\\ + where(parts.c.part=='our part').\\ + cte(recursive=True) + + + incl_alias = included_parts.alias() + parts_alias = parts.alias() + included_parts = included_parts.union_all( + select([ + parts_alias.c.part, + parts_alias.c.sub_part, + parts_alias.c.quantity + ]). + where(parts_alias.c.part==incl_alias.c.sub_part) + ) + + statement = select([ + included_parts.c.sub_part, + func.sum(included_parts.c.quantity). + label('total_quantity') + ]).\ + select_from(included_parts.join(parts, + included_parts.c.part==parts.c.part)).\\ + group_by(included_parts.c.sub_part) + + result = conn.execute(statement).fetchall() + + + .. seealso:: + + :meth:`.orm.query.Query.cte` - ORM version of :meth:`.SelectBase.cte`. + + """ + return CTE(self, name=name, recursive=recursive) + + @_generative + @util.deprecated('0.6', + message="``autocommit()`` is deprecated. Use " + ":meth:`.Executable.execution_options` with the " + "'autocommit' flag.") + def autocommit(self): + """return a new selectable with the 'autocommit' flag set to + True. + """ + + self._execution_options = \ + self._execution_options.union({'autocommit': True}) + + def _generate(self): + """Override the default _generate() method to also clear out + exported collections.""" + + s = self.__class__.__new__(self.__class__) + s.__dict__ = self.__dict__.copy() + s._reset_exported() + return s + + @property + def _from_objects(self): + return [self] + +class GenerativeSelect(SelectBase): + """Base class for SELECT statements where additional elements can be + added. + + This serves as the base for :class:`.Select` and :class:`.CompoundSelect` + where elements such as ORDER BY, GROUP BY can be added and column rendering + can be controlled. Compare to :class:`.TextAsFrom`, which, while it + subclasses :class:`.SelectBase` and is also a SELECT construct, represents + a fixed textual string which cannot be altered at this level, only + wrapped as a subquery. + + .. versionadded:: 0.9.0 :class:`.GenerativeSelect` was added to + provide functionality specific to :class:`.Select` and :class:`.CompoundSelect` + while allowing :class:`.SelectBase` to be used for other SELECT-like + objects, e.g. :class:`.TextAsFrom`. + + """ + _order_by_clause = ClauseList() + _group_by_clause = ClauseList() + _limit = None + _offset = None + _for_update_arg = None + + def __init__(self, + use_labels=False, + for_update=False, + limit=None, + offset=None, + order_by=None, + group_by=None, + bind=None, + autocommit=None): + self.use_labels = use_labels + + if for_update is not False: + self._for_update_arg = ForUpdateArg.parse_legacy_select(for_update) + + if autocommit is not None: + util.warn_deprecated('autocommit on select() is ' + 'deprecated. Use .execution_options(a' + 'utocommit=True)') + self._execution_options = \ + self._execution_options.union( + {'autocommit': autocommit}) + if limit is not None: + self._limit = util.asint(limit) + if offset is not None: + self._offset = util.asint(offset) + self._bind = bind + + if order_by is not None: + self._order_by_clause = ClauseList(*util.to_list(order_by)) + if group_by is not None: + self._group_by_clause = ClauseList(*util.to_list(group_by)) + + @property + def for_update(self): + """Provide legacy dialect support for the ``for_update`` attribute. + """ + if self._for_update_arg is not None: + return self._for_update_arg.legacy_for_update_value + else: + return None + + @for_update.setter + def for_update(self, value): + self._for_update_arg = ForUpdateArg.parse_legacy_select(value) + + @_generative + def with_for_update(self, nowait=False, read=False, of=None): + """Specify a ``FOR UPDATE`` clause for this :class:`.GenerativeSelect`. + + E.g.:: + + stmt = select([table]).with_for_update(nowait=True) + + On a database like Postgresql or Oracle, the above would render a + statement like:: + + SELECT table.a, table.b FROM table FOR UPDATE NOWAIT + + on other backends, the ``nowait`` option is ignored and instead + would produce:: + + SELECT table.a, table.b FROM table FOR UPDATE + + When called with no arguments, the statement will render with + the suffix ``FOR UPDATE``. Additional arguments can then be + provided which allow for common database-specific + variants. + + :param nowait: boolean; will render ``FOR UPDATE NOWAIT`` on Oracle and + Postgresql dialects. + + :param read: boolean; will render ``LOCK IN SHARE MODE`` on MySQL, + ``FOR SHARE`` on Postgresql. On Postgresql, when combined with + ``nowait``, will render ``FOR SHARE NOWAIT``. + + :param of: SQL expression or list of SQL expression elements + (typically :class:`.Column` objects or a compatible expression) which + will render into a ``FOR UPDATE OF`` clause; supported by PostgreSQL + and Oracle. May render as a table or as a column depending on + backend. + + .. versionadded:: 0.9.0 + + """ + self._for_update_arg = ForUpdateArg(nowait=nowait, read=read, of=of) + + @_generative + def apply_labels(self): + """return a new selectable with the 'use_labels' flag set to True. + + This will result in column expressions being generated using labels + against their table name, such as "SELECT somecolumn AS + tablename_somecolumn". This allows selectables which contain multiple + FROM clauses to produce a unique set of column names regardless of + name conflicts among the individual FROM clauses. + + """ + self.use_labels = True + + @_generative + def limit(self, limit): + """return a new selectable with the given LIMIT criterion + applied.""" + + self._limit = util.asint(limit) + + @_generative + def offset(self, offset): + """return a new selectable with the given OFFSET criterion + applied.""" + + self._offset = util.asint(offset) + + @_generative + def order_by(self, *clauses): + """return a new selectable with the given list of ORDER BY + criterion applied. + + The criterion will be appended to any pre-existing ORDER BY + criterion. + + """ + + self.append_order_by(*clauses) + + @_generative + def group_by(self, *clauses): + """return a new selectable with the given list of GROUP BY + criterion applied. + + The criterion will be appended to any pre-existing GROUP BY + criterion. + + """ + + self.append_group_by(*clauses) + + def append_order_by(self, *clauses): + """Append the given ORDER BY criterion applied to this selectable. + + The criterion will be appended to any pre-existing ORDER BY criterion. + + This is an **in-place** mutation method; the + :meth:`~.GenerativeSelect.order_by` method is preferred, as it provides standard + :term:`method chaining`. + + """ + if len(clauses) == 1 and clauses[0] is None: + self._order_by_clause = ClauseList() + else: + if getattr(self, '_order_by_clause', None) is not None: + clauses = list(self._order_by_clause) + list(clauses) + self._order_by_clause = ClauseList(*clauses) + + def append_group_by(self, *clauses): + """Append the given GROUP BY criterion applied to this selectable. + + The criterion will be appended to any pre-existing GROUP BY criterion. + + This is an **in-place** mutation method; the + :meth:`~.GenerativeSelect.group_by` method is preferred, as it provides standard + :term:`method chaining`. + + """ + if len(clauses) == 1 and clauses[0] is None: + self._group_by_clause = ClauseList() + else: + if getattr(self, '_group_by_clause', None) is not None: + clauses = list(self._group_by_clause) + list(clauses) + self._group_by_clause = ClauseList(*clauses) + + +class CompoundSelect(GenerativeSelect): + """Forms the basis of ``UNION``, ``UNION ALL``, and other + SELECT-based set operations. + + + .. seealso:: + + :func:`.union` + + :func:`.union_all` + + :func:`.intersect` + + :func:`.intersect_all` + + :func:`.except` + + :func:`.except_all` + + """ + + __visit_name__ = 'compound_select' + + UNION = util.symbol('UNION') + UNION_ALL = util.symbol('UNION ALL') + EXCEPT = util.symbol('EXCEPT') + EXCEPT_ALL = util.symbol('EXCEPT ALL') + INTERSECT = util.symbol('INTERSECT') + INTERSECT_ALL = util.symbol('INTERSECT ALL') + + def __init__(self, keyword, *selects, **kwargs): + self._auto_correlate = kwargs.pop('correlate', False) + self.keyword = keyword + self.selects = [] + + numcols = None + + # some DBs do not like ORDER BY in the inner queries of a UNION, etc. + for n, s in enumerate(selects): + s = _clause_element_as_expr(s) + + if not numcols: + numcols = len(s.c) + elif len(s.c) != numcols: + raise exc.ArgumentError('All selectables passed to ' + 'CompoundSelect must have identical numbers of ' + 'columns; select #%d has %d columns, select ' + '#%d has %d' % (1, len(self.selects[0].c), n + + 1, len(s.c))) + + self.selects.append(s.self_group(self)) + + GenerativeSelect.__init__(self, **kwargs) + + @classmethod + def _create_union(cls, *selects, **kwargs): + """Return a ``UNION`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + A similar :func:`union()` method is available on all + :class:`.FromClause` subclasses. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.UNION, *selects, **kwargs) + + @classmethod + def _create_union_all(cls, *selects, **kwargs): + """Return a ``UNION ALL`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + A similar :func:`union_all()` method is available on all + :class:`.FromClause` subclasses. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.UNION_ALL, *selects, **kwargs) + + + @classmethod + def _create_except(cls, *selects, **kwargs): + """Return an ``EXCEPT`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.EXCEPT, *selects, **kwargs) + + + @classmethod + def _create_except_all(cls, *selects, **kwargs): + """Return an ``EXCEPT ALL`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.EXCEPT_ALL, *selects, **kwargs) + + + @classmethod + def _create_intersect(cls, *selects, **kwargs): + """Return an ``INTERSECT`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.INTERSECT, *selects, **kwargs) + + + @classmethod + def _create_intersect_all(cls, *selects, **kwargs): + """Return an ``INTERSECT ALL`` of multiple selectables. + + The returned object is an instance of + :class:`.CompoundSelect`. + + \*selects + a list of :class:`.Select` instances. + + \**kwargs + available keyword arguments are the same as those of + :func:`select`. + + """ + return CompoundSelect(CompoundSelect.INTERSECT_ALL, *selects, **kwargs) + + + def _scalar_type(self): + return self.selects[0]._scalar_type() + + def self_group(self, against=None): + return FromGrouping(self) + + def is_derived_from(self, fromclause): + for s in self.selects: + if s.is_derived_from(fromclause): + return True + return False + + def _populate_column_collection(self): + for cols in zip(*[s.c for s in self.selects]): + + # this is a slightly hacky thing - the union exports a + # column that resembles just that of the *first* selectable. + # to get at a "composite" column, particularly foreign keys, + # you have to dig through the proxies collection which we + # generate below. We may want to improve upon this, such as + # perhaps _make_proxy can accept a list of other columns + # that are "shared" - schema.column can then copy all the + # ForeignKeys in. this would allow the union() to have all + # those fks too. + + proxy = cols[0]._make_proxy(self, + name=cols[0]._label if self.use_labels else None, + key=cols[0]._key_label if self.use_labels else None) + + # hand-construct the "_proxies" collection to include all + # derived columns place a 'weight' annotation corresponding + # to how low in the list of select()s the column occurs, so + # that the corresponding_column() operation can resolve + # conflicts + + proxy._proxies = [c._annotate({'weight': i + 1}) for (i, + c) in enumerate(cols)] + + def _refresh_for_new_column(self, column): + for s in self.selects: + s._refresh_for_new_column(column) + + if not self._cols_populated: + return None + + raise NotImplementedError("CompoundSelect constructs don't support " + "addition of columns to underlying selectables") + + def _copy_internals(self, clone=_clone, **kw): + self._reset_exported() + self.selects = [clone(s, **kw) for s in self.selects] + if hasattr(self, '_col_map'): + del self._col_map + for attr in ('_order_by_clause', '_group_by_clause', '_for_update_arg'): + if getattr(self, attr) is not None: + setattr(self, attr, clone(getattr(self, attr), **kw)) + + def get_children(self, column_collections=True, **kwargs): + return (column_collections and list(self.c) or []) \ + + [self._order_by_clause, self._group_by_clause] \ + + list(self.selects) + + def bind(self): + if self._bind: + return self._bind + for s in self.selects: + e = s.bind + if e: + return e + else: + return None + + def _set_bind(self, bind): + self._bind = bind + bind = property(bind, _set_bind) + + +class HasPrefixes(object): + _prefixes = () + + @_generative + def prefix_with(self, *expr, **kw): + """Add one or more expressions following the statement keyword, i.e. + SELECT, INSERT, UPDATE, or DELETE. Generative. + + This is used to support backend-specific prefix keywords such as those + provided by MySQL. + + E.g.:: + + stmt = table.insert().prefix_with("LOW_PRIORITY", dialect="mysql") + + Multiple prefixes can be specified by multiple calls + to :meth:`.prefix_with`. + + :param \*expr: textual or :class:`.ClauseElement` construct which + will be rendered following the INSERT, UPDATE, or DELETE + keyword. + :param \**kw: A single keyword 'dialect' is accepted. This is an + optional string dialect name which will + limit rendering of this prefix to only that dialect. + + """ + dialect = kw.pop('dialect', None) + if kw: + raise exc.ArgumentError("Unsupported argument(s): %s" % + ",".join(kw)) + self._setup_prefixes(expr, dialect) + + def _setup_prefixes(self, prefixes, dialect=None): + self._prefixes = self._prefixes + tuple( + [(_literal_as_text(p), dialect) for p in prefixes]) + + + +class Select(HasPrefixes, GenerativeSelect): + """Represents a ``SELECT`` statement. + + """ + + __visit_name__ = 'select' + + _prefixes = () + _hints = util.immutabledict() + _distinct = False + _from_cloned = None + _correlate = () + _correlate_except = None + _memoized_property = SelectBase._memoized_property + + def __init__(self, + columns=None, + whereclause=None, + from_obj=None, + distinct=False, + having=None, + correlate=True, + prefixes=None, + **kwargs): + """Construct a new :class:`.Select`. + + Similar functionality is also available via the :meth:`.FromClause.select` + method on any :class:`.FromClause`. + + All arguments which accept :class:`.ClauseElement` arguments also accept + string arguments, which will be converted as appropriate into + either :func:`text()` or :func:`literal_column()` constructs. + + .. seealso:: + + :ref:`coretutorial_selecting` - Core Tutorial description of + :func:`.select`. + + :param columns: + A list of :class:`.ClauseElement` objects, typically + :class:`.ColumnElement` objects or subclasses, which will form the + columns clause of the resulting statement. For all members which are + instances of :class:`.Selectable`, the individual :class:`.ColumnElement` + members of the :class:`.Selectable` will be added individually to the + columns clause. For example, specifying a + :class:`~sqlalchemy.schema.Table` instance will result in all the + contained :class:`~sqlalchemy.schema.Column` objects within to be added + to the columns clause. + + This argument is not present on the form of :func:`select()` + available on :class:`~sqlalchemy.schema.Table`. + + :param whereclause: + A :class:`.ClauseElement` expression which will be used to form the + ``WHERE`` clause. + + :param from_obj: + A list of :class:`.ClauseElement` objects which will be added to the + ``FROM`` clause of the resulting statement. Note that "from" objects are + automatically located within the columns and whereclause ClauseElements. + Use this parameter to explicitly specify "from" objects which are not + automatically locatable. This could include + :class:`~sqlalchemy.schema.Table` objects that aren't otherwise present, + or :class:`.Join` objects whose presence will supercede that of the + :class:`~sqlalchemy.schema.Table` objects already located in the other + clauses. + + :param autocommit: + Deprecated. Use .execution_options(autocommit=) + to set the autocommit option. + + :param bind=None: + an :class:`~.Engine` or :class:`~.Connection` instance + to which the + resulting :class:`.Select` object will be bound. The :class:`.Select` + object will otherwise automatically bind to whatever + :class:`~.base.Connectable` instances can be located within its contained + :class:`.ClauseElement` members. + + :param correlate=True: + indicates that this :class:`.Select` object should have its + contained :class:`.FromClause` elements "correlated" to an enclosing + :class:`.Select` object. This means that any :class:`.ClauseElement` + instance within the "froms" collection of this :class:`.Select` + which is also present in the "froms" collection of an + enclosing select will not be rendered in the ``FROM`` clause + of this select statement. + + :param distinct=False: + when ``True``, applies a ``DISTINCT`` qualifier to the columns + clause of the resulting statement. + + The boolean argument may also be a column expression or list + of column expressions - this is a special calling form which + is understood by the Postgresql dialect to render the + ``DISTINCT ON ()`` syntax. + + ``distinct`` is also available via the :meth:`~.Select.distinct` + generative method. + + :param for_update=False: + when ``True``, applies ``FOR UPDATE`` to the end of the + resulting statement. + + .. deprecated:: 0.9.0 - use :meth:`.GenerativeSelect.with_for_update` + to specify the structure of the ``FOR UPDATE`` clause. + + ``for_update`` accepts various string values interpreted by + specific backends, including: + + * ``"read"`` - on MySQL, translates to ``LOCK IN SHARE MODE``; + on Postgresql, translates to ``FOR SHARE``. + * ``"nowait"`` - on Postgresql and Oracle, translates to + ``FOR UPDATE NOWAIT``. + * ``"read_nowait"`` - on Postgresql, translates to + ``FOR SHARE NOWAIT``. + + .. seealso:: + + :meth:`.GenerativeSelect.with_for_update` - improved API for + specifying the ``FOR UPDATE`` clause. + + :param group_by: + a list of :class:`.ClauseElement` objects which will comprise the + ``GROUP BY`` clause of the resulting select. + + :param having: + a :class:`.ClauseElement` that will comprise the ``HAVING`` clause + of the resulting select when ``GROUP BY`` is used. + + :param limit=None: + a numerical value which usually compiles to a ``LIMIT`` + expression in the resulting select. Databases that don't + support ``LIMIT`` will attempt to provide similar + functionality. + + :param offset=None: + a numeric value which usually compiles to an ``OFFSET`` + expression in the resulting select. Databases that don't + support ``OFFSET`` will attempt to provide similar + functionality. + + :param order_by: + a scalar or list of :class:`.ClauseElement` objects which will + comprise the ``ORDER BY`` clause of the resulting select. + + :param use_labels=False: + when ``True``, the statement will be generated using labels + for each column in the columns clause, which qualify each + column with its parent table's (or aliases) name so that name + conflicts between columns in different tables don't occur. + The format of the label is _. The "c" + collection of the resulting :class:`.Select` object will use these + names as well for targeting column members. + + use_labels is also available via the :meth:`~.GenerativeSelect.apply_labels` + generative method. + + """ + self._auto_correlate = correlate + if distinct is not False: + if distinct is True: + self._distinct = True + else: + self._distinct = [ + _literal_as_text(e) + for e in util.to_list(distinct) + ] + + if from_obj is not None: + self._from_obj = util.OrderedSet( + _interpret_as_from(f) + for f in util.to_list(from_obj)) + else: + self._from_obj = util.OrderedSet() + + try: + cols_present = bool(columns) + except TypeError: + raise exc.ArgumentError("columns argument to select() must " + "be a Python list or other iterable") + + if cols_present: + self._raw_columns = [] + for c in columns: + c = _interpret_as_column_or_from(c) + if isinstance(c, ScalarSelect): + c = c.self_group(against=operators.comma_op) + self._raw_columns.append(c) + else: + self._raw_columns = [] + + if whereclause is not None: + self._whereclause = _literal_as_text(whereclause) + else: + self._whereclause = None + + if having is not None: + self._having = _literal_as_text(having) + else: + self._having = None + + if prefixes: + self._setup_prefixes(prefixes) + + GenerativeSelect.__init__(self, **kwargs) + + @property + def _froms(self): + # would love to cache this, + # but there's just enough edge cases, particularly now that + # declarative encourages construction of SQL expressions + # without tables present, to just regen this each time. + froms = [] + seen = set() + translate = self._from_cloned + + def add(items): + for item in items: + if item is self: + raise exc.InvalidRequestError( + "select() construct refers to itself as a FROM") + if translate and item in translate: + item = translate[item] + if not seen.intersection(item._cloned_set): + froms.append(item) + seen.update(item._cloned_set) + + add(_from_objects(*self._raw_columns)) + if self._whereclause is not None: + add(_from_objects(self._whereclause)) + add(self._from_obj) + + return froms + + def _get_display_froms(self, explicit_correlate_froms=None, + implicit_correlate_froms=None): + """Return the full list of 'from' clauses to be displayed. + + Takes into account a set of existing froms which may be + rendered in the FROM clause of enclosing selects; this Select + may want to leave those absent if it is automatically + correlating. + + """ + froms = self._froms + + toremove = set(itertools.chain(*[ + _expand_cloned(f._hide_froms) + for f in froms])) + if toremove: + # if we're maintaining clones of froms, + # add the copies out to the toremove list. only include + # clones that are lexical equivalents. + if self._from_cloned: + toremove.update( + self._from_cloned[f] for f in + toremove.intersection(self._from_cloned) + if self._from_cloned[f]._is_lexical_equivalent(f) + ) + # filter out to FROM clauses not in the list, + # using a list to maintain ordering + froms = [f for f in froms if f not in toremove] + + if self._correlate: + to_correlate = self._correlate + if to_correlate: + froms = [ + f for f in froms if f not in + _cloned_intersection( + _cloned_intersection(froms, explicit_correlate_froms or ()), + to_correlate + ) + ] + + if self._correlate_except is not None: + + froms = [ + f for f in froms if f not in + _cloned_difference( + _cloned_intersection(froms, explicit_correlate_froms or ()), + self._correlate_except + ) + ] + + if self._auto_correlate and \ + implicit_correlate_froms and \ + len(froms) > 1: + + froms = [ + f for f in froms if f not in + _cloned_intersection(froms, implicit_correlate_froms) + ] + + if not len(froms): + raise exc.InvalidRequestError("Select statement '%s" + "' returned no FROM clauses due to " + "auto-correlation; specify " + "correlate() to control " + "correlation manually." % self) + + return froms + + def _scalar_type(self): + elem = self._raw_columns[0] + cols = list(elem._select_iterable) + return cols[0].type + + @property + def froms(self): + """Return the displayed list of FromClause elements.""" + + return self._get_display_froms() + + @_generative + def with_hint(self, selectable, text, dialect_name='*'): + """Add an indexing hint for the given selectable to this + :class:`.Select`. + + The text of the hint is rendered in the appropriate + location for the database backend in use, relative + to the given :class:`.Table` or :class:`.Alias` passed as the + ``selectable`` argument. The dialect implementation + typically uses Python string substitution syntax + with the token ``%(name)s`` to render the name of + the table or alias. E.g. when using Oracle, the + following:: + + select([mytable]).\\ + with_hint(mytable, "+ index(%(name)s ix_mytable)") + + Would render SQL as:: + + select /*+ index(mytable ix_mytable) */ ... from mytable + + The ``dialect_name`` option will limit the rendering of a particular + hint to a particular backend. Such as, to add hints for both Oracle + and Sybase simultaneously:: + + select([mytable]).\\ + with_hint(mytable, "+ index(%(name)s ix_mytable)", 'oracle').\\ + with_hint(mytable, "WITH INDEX ix_mytable", 'sybase') + + """ + self._hints = self._hints.union( + {(selectable, dialect_name): text}) + + @property + def type(self): + raise exc.InvalidRequestError("Select objects don't have a type. " + "Call as_scalar() on this Select object " + "to return a 'scalar' version of this Select.") + + @_memoized_property.method + def locate_all_froms(self): + """return a Set of all FromClause elements referenced by this Select. + + This set is a superset of that returned by the ``froms`` property, + which is specifically for those FromClause elements that would + actually be rendered. + + """ + froms = self._froms + return froms + list(_from_objects(*froms)) + + @property + def inner_columns(self): + """an iterator of all ColumnElement expressions which would + be rendered into the columns clause of the resulting SELECT statement. + + """ + return _select_iterables(self._raw_columns) + + def is_derived_from(self, fromclause): + if self in fromclause._cloned_set: + return True + + for f in self.locate_all_froms(): + if f.is_derived_from(fromclause): + return True + return False + + def _copy_internals(self, clone=_clone, **kw): + + # Select() object has been cloned and probably adapted by the + # given clone function. Apply the cloning function to internal + # objects + + # 1. keep a dictionary of the froms we've cloned, and what + # they've become. This is consulted later when we derive + # additional froms from "whereclause" and the columns clause, + # which may still reference the uncloned parent table. + # as of 0.7.4 we also put the current version of _froms, which + # gets cleared on each generation. previously we were "baking" + # _froms into self._from_obj. + self._from_cloned = from_cloned = dict((f, clone(f, **kw)) + for f in self._from_obj.union(self._froms)) + + # 3. update persistent _from_obj with the cloned versions. + self._from_obj = util.OrderedSet(from_cloned[f] for f in + self._from_obj) + + # the _correlate collection is done separately, what can happen + # here is the same item is _correlate as in _from_obj but the + # _correlate version has an annotation on it - (specifically + # RelationshipProperty.Comparator._criterion_exists() does + # this). Also keep _correlate liberally open with it's previous + # contents, as this set is used for matching, not rendering. + self._correlate = set(clone(f) for f in + self._correlate).union(self._correlate) + + # 4. clone other things. The difficulty here is that Column + # objects are not actually cloned, and refer to their original + # .table, resulting in the wrong "from" parent after a clone + # operation. Hence _from_cloned and _from_obj supercede what is + # present here. + self._raw_columns = [clone(c, **kw) for c in self._raw_columns] + for attr in '_whereclause', '_having', '_order_by_clause', \ + '_group_by_clause', '_for_update_arg': + if getattr(self, attr) is not None: + setattr(self, attr, clone(getattr(self, attr), **kw)) + + # erase exported column list, _froms collection, + # etc. + self._reset_exported() + + def get_children(self, column_collections=True, **kwargs): + """return child elements as per the ClauseElement specification.""" + + return (column_collections and list(self.columns) or []) + \ + self._raw_columns + list(self._froms) + \ + [x for x in + (self._whereclause, self._having, + self._order_by_clause, self._group_by_clause) + if x is not None] + + @_generative + def column(self, column): + """return a new select() construct with the given column expression + added to its columns clause. + + """ + self.append_column(column) + + @util.dependencies("sqlalchemy.sql.util") + def reduce_columns(self, sqlutil, only_synonyms=True): + """Return a new :func`.select` construct with redundantly + named, equivalently-valued columns removed from the columns clause. + + "Redundant" here means two columns where one refers to the + other either based on foreign key, or via a simple equality + comparison in the WHERE clause of the statement. The primary purpose + of this method is to automatically construct a select statement + with all uniquely-named columns, without the need to use + table-qualified labels as :meth:`.apply_labels` does. + + When columns are omitted based on foreign key, the referred-to + column is the one that's kept. When columns are omitted based on + WHERE eqivalence, the first column in the columns clause is the + one that's kept. + + :param only_synonyms: when True, limit the removal of columns + to those which have the same name as the equivalent. Otherwise, + all columns that are equivalent to another are removed. + + .. versionadded:: 0.8 + + """ + return self.with_only_columns( + sqlutil.reduce_columns( + self.inner_columns, + only_synonyms=only_synonyms, + *(self._whereclause, ) + tuple(self._from_obj) + ) + ) + + @_generative + def with_only_columns(self, columns): + """Return a new :func:`.select` construct with its columns + clause replaced with the given columns. + + .. versionchanged:: 0.7.3 + Due to a bug fix, this method has a slight + behavioral change as of version 0.7.3. + Prior to version 0.7.3, the FROM clause of + a :func:`.select` was calculated upfront and as new columns + were added; in 0.7.3 and later it's calculated + at compile time, fixing an issue regarding late binding + of columns to parent tables. This changes the behavior of + :meth:`.Select.with_only_columns` in that FROM clauses no + longer represented in the new list are dropped, + but this behavior is more consistent in + that the FROM clauses are consistently derived from the + current columns clause. The original intent of this method + is to allow trimming of the existing columns list to be fewer + columns than originally present; the use case of replacing + the columns list with an entirely different one hadn't + been anticipated until 0.7.3 was released; the usage + guidelines below illustrate how this should be done. + + This method is exactly equivalent to as if the original + :func:`.select` had been called with the given columns + clause. I.e. a statement:: + + s = select([table1.c.a, table1.c.b]) + s = s.with_only_columns([table1.c.b]) + + should be exactly equivalent to:: + + s = select([table1.c.b]) + + This means that FROM clauses which are only derived + from the column list will be discarded if the new column + list no longer contains that FROM:: + + >>> table1 = table('t1', column('a'), column('b')) + >>> table2 = table('t2', column('a'), column('b')) + >>> s1 = select([table1.c.a, table2.c.b]) + >>> print s1 + SELECT t1.a, t2.b FROM t1, t2 + >>> s2 = s1.with_only_columns([table2.c.b]) + >>> print s2 + SELECT t2.b FROM t1 + + The preferred way to maintain a specific FROM clause + in the construct, assuming it won't be represented anywhere + else (i.e. not in the WHERE clause, etc.) is to set it using + :meth:`.Select.select_from`:: + + >>> s1 = select([table1.c.a, table2.c.b]).\\ + ... select_from(table1.join(table2, + ... table1.c.a==table2.c.a)) + >>> s2 = s1.with_only_columns([table2.c.b]) + >>> print s2 + SELECT t2.b FROM t1 JOIN t2 ON t1.a=t2.a + + Care should also be taken to use the correct + set of column objects passed to :meth:`.Select.with_only_columns`. + Since the method is essentially equivalent to calling the + :func:`.select` construct in the first place with the given + columns, the columns passed to :meth:`.Select.with_only_columns` + should usually be a subset of those which were passed + to the :func:`.select` construct, not those which are available + from the ``.c`` collection of that :func:`.select`. That + is:: + + s = select([table1.c.a, table1.c.b]).select_from(table1) + s = s.with_only_columns([table1.c.b]) + + and **not**:: + + # usually incorrect + s = s.with_only_columns([s.c.b]) + + The latter would produce the SQL:: + + SELECT b + FROM (SELECT t1.a AS a, t1.b AS b + FROM t1), t1 + + Since the :func:`.select` construct is essentially being + asked to select both from ``table1`` as well as itself. + + """ + self._reset_exported() + rc = [] + for c in columns: + c = _interpret_as_column_or_from(c) + if isinstance(c, ScalarSelect): + c = c.self_group(against=operators.comma_op) + rc.append(c) + self._raw_columns = rc + + @_generative + def where(self, whereclause): + """return a new select() construct with the given expression added to + its WHERE clause, joined to the existing clause via AND, if any. + + """ + + self.append_whereclause(whereclause) + + @_generative + def having(self, having): + """return a new select() construct with the given expression added to + its HAVING clause, joined to the existing clause via AND, if any. + + """ + self.append_having(having) + + @_generative + def distinct(self, *expr): + """Return a new select() construct which will apply DISTINCT to its + columns clause. + + :param \*expr: optional column expressions. When present, + the Postgresql dialect will render a ``DISTINCT ON (>)`` + construct. + + """ + if expr: + expr = [_literal_as_text(e) for e in expr] + if isinstance(self._distinct, list): + self._distinct = self._distinct + expr + else: + self._distinct = expr + else: + self._distinct = True + + @_generative + def select_from(self, fromclause): + """return a new :func:`.select` construct with the + given FROM expression + merged into its list of FROM objects. + + E.g.:: + + table1 = table('t1', column('a')) + table2 = table('t2', column('b')) + s = select([table1.c.a]).\\ + select_from( + table1.join(table2, table1.c.a==table2.c.b) + ) + + The "from" list is a unique set on the identity of each element, + so adding an already present :class:`.Table` or other selectable + will have no effect. Passing a :class:`.Join` that refers + to an already present :class:`.Table` or other selectable will have + the effect of concealing the presence of that selectable as + an individual element in the rendered FROM list, instead + rendering it into a JOIN clause. + + While the typical purpose of :meth:`.Select.select_from` is to + replace the default, derived FROM clause with a join, it can + also be called with individual table elements, multiple times + if desired, in the case that the FROM clause cannot be fully + derived from the columns clause:: + + select([func.count('*')]).select_from(table1) + + """ + self.append_from(fromclause) + + @_generative + def correlate(self, *fromclauses): + """return a new :class:`.Select` which will correlate the given FROM + clauses to that of an enclosing :class:`.Select`. + + Calling this method turns off the :class:`.Select` object's + default behavior of "auto-correlation". Normally, FROM elements + which appear in a :class:`.Select` that encloses this one via + its :term:`WHERE clause`, ORDER BY, HAVING or + :term:`columns clause` will be omitted from this :class:`.Select` + object's :term:`FROM clause`. + Setting an explicit correlation collection using the + :meth:`.Select.correlate` method provides a fixed list of FROM objects + that can potentially take place in this process. + + When :meth:`.Select.correlate` is used to apply specific FROM clauses + for correlation, the FROM elements become candidates for + correlation regardless of how deeply nested this :class:`.Select` + object is, relative to an enclosing :class:`.Select` which refers to + the same FROM object. This is in contrast to the behavior of + "auto-correlation" which only correlates to an immediate enclosing + :class:`.Select`. Multi-level correlation ensures that the link + between enclosed and enclosing :class:`.Select` is always via + at least one WHERE/ORDER BY/HAVING/columns clause in order for + correlation to take place. + + If ``None`` is passed, the :class:`.Select` object will correlate + none of its FROM entries, and all will render unconditionally + in the local FROM clause. + + :param \*fromclauses: a list of one or more :class:`.FromClause` + constructs, or other compatible constructs (i.e. ORM-mapped + classes) to become part of the correlate collection. + + .. versionchanged:: 0.8.0 ORM-mapped classes are accepted by + :meth:`.Select.correlate`. + + .. versionchanged:: 0.8.0 The :meth:`.Select.correlate` method no + longer unconditionally removes entries from the FROM clause; instead, + the candidate FROM entries must also be matched by a FROM entry + located in an enclosing :class:`.Select`, which ultimately encloses + this one as present in the WHERE clause, ORDER BY clause, HAVING + clause, or columns clause of an enclosing :meth:`.Select`. + + .. versionchanged:: 0.8.2 explicit correlation takes place + via any level of nesting of :class:`.Select` objects; in previous + 0.8 versions, correlation would only occur relative to the immediate + enclosing :class:`.Select` construct. + + .. seealso:: + + :meth:`.Select.correlate_except` + + :ref:`correlated_subqueries` + + """ + self._auto_correlate = False + if fromclauses and fromclauses[0] is None: + self._correlate = () + else: + self._correlate = set(self._correlate).union( + _interpret_as_from(f) for f in fromclauses) + + @_generative + def correlate_except(self, *fromclauses): + """return a new :class:`.Select` which will omit the given FROM + clauses from the auto-correlation process. + + Calling :meth:`.Select.correlate_except` turns off the + :class:`.Select` object's default behavior of + "auto-correlation" for the given FROM elements. An element + specified here will unconditionally appear in the FROM list, while + all other FROM elements remain subject to normal auto-correlation + behaviors. + + .. versionchanged:: 0.8.2 The :meth:`.Select.correlate_except` + method was improved to fully prevent FROM clauses specified here + from being omitted from the immediate FROM clause of this + :class:`.Select`. + + If ``None`` is passed, the :class:`.Select` object will correlate + all of its FROM entries. + + .. versionchanged:: 0.8.2 calling ``correlate_except(None)`` will + correctly auto-correlate all FROM clauses. + + :param \*fromclauses: a list of one or more :class:`.FromClause` + constructs, or other compatible constructs (i.e. ORM-mapped + classes) to become part of the correlate-exception collection. + + .. seealso:: + + :meth:`.Select.correlate` + + :ref:`correlated_subqueries` + + """ + + self._auto_correlate = False + if fromclauses and fromclauses[0] is None: + self._correlate_except = () + else: + self._correlate_except = set(self._correlate_except or ()).union( + _interpret_as_from(f) for f in fromclauses) + + def append_correlation(self, fromclause): + """append the given correlation expression to this select() + construct. + + This is an **in-place** mutation method; the + :meth:`~.Select.correlate` method is preferred, as it provides standard + :term:`method chaining`. + + """ + + self._auto_correlate = False + self._correlate = set(self._correlate).union( + _interpret_as_from(f) for f in fromclause) + + def append_column(self, column): + """append the given column expression to the columns clause of this + select() construct. + + This is an **in-place** mutation method; the + :meth:`~.Select.column` method is preferred, as it provides standard + :term:`method chaining`. + + """ + self._reset_exported() + column = _interpret_as_column_or_from(column) + + if isinstance(column, ScalarSelect): + column = column.self_group(against=operators.comma_op) + + self._raw_columns = self._raw_columns + [column] + + def append_prefix(self, clause): + """append the given columns clause prefix expression to this select() + construct. + + This is an **in-place** mutation method; the + :meth:`~.Select.prefix_with` method is preferred, as it provides standard + :term:`method chaining`. + + """ + clause = _literal_as_text(clause) + self._prefixes = self._prefixes + (clause,) + + def append_whereclause(self, whereclause): + """append the given expression to this select() construct's WHERE + criterion. + + The expression will be joined to existing WHERE criterion via AND. + + This is an **in-place** mutation method; the + :meth:`~.Select.where` method is preferred, as it provides standard + :term:`method chaining`. + + """ + + self._reset_exported() + self._whereclause = and_(True_._ifnone(self._whereclause), whereclause) + + def append_having(self, having): + """append the given expression to this select() construct's HAVING + criterion. + + The expression will be joined to existing HAVING criterion via AND. + + This is an **in-place** mutation method; the + :meth:`~.Select.having` method is preferred, as it provides standard + :term:`method chaining`. + + """ + self._reset_exported() + self._having = and_(True_._ifnone(self._having), having) + + def append_from(self, fromclause): + """append the given FromClause expression to this select() construct's + FROM clause. + + This is an **in-place** mutation method; the + :meth:`~.Select.select_from` method is preferred, as it provides standard + :term:`method chaining`. + + """ + self._reset_exported() + fromclause = _interpret_as_from(fromclause) + self._from_obj = self._from_obj.union([fromclause]) + + + @_memoized_property + def _columns_plus_names(self): + if self.use_labels: + names = set() + def name_for_col(c): + if c._label is None: + return (None, c) + name = c._label + if name in names: + name = c.anon_label + else: + names.add(name) + return name, c + + return [ + name_for_col(c) + for c in util.unique_list(_select_iterables(self._raw_columns)) + ] + else: + return [ + (None, c) + for c in util.unique_list(_select_iterables(self._raw_columns)) + ] + + def _populate_column_collection(self): + for name, c in self._columns_plus_names: + if not hasattr(c, '_make_proxy'): + continue + if name is None: + key = None + elif self.use_labels: + key = c._key_label + if key is not None and key in self.c: + key = c.anon_label + else: + key = None + + c._make_proxy(self, key=key, + name=name, + name_is_truncatable=True) + + def _refresh_for_new_column(self, column): + for fromclause in self._froms: + col = fromclause._refresh_for_new_column(column) + if col is not None: + if col in self.inner_columns and self._cols_populated: + our_label = col._key_label if self.use_labels else col.key + if our_label not in self.c: + return col._make_proxy(self, + name=col._label if self.use_labels else None, + key=col._key_label if self.use_labels else None, + name_is_truncatable=True) + return None + return None + + def self_group(self, against=None): + """return a 'grouping' construct as per the ClauseElement + specification. + + This produces an element that can be embedded in an expression. Note + that this method is called automatically as needed when constructing + expressions and should not require explicit use. + + """ + if isinstance(against, CompoundSelect): + return self + return FromGrouping(self) + + def union(self, other, **kwargs): + """return a SQL UNION of this select() construct against the given + selectable.""" + + return CompoundSelect._create_union(self, other, **kwargs) + + def union_all(self, other, **kwargs): + """return a SQL UNION ALL of this select() construct against the given + selectable. + + """ + return CompoundSelect._create_union_all(self, other, **kwargs) + + def except_(self, other, **kwargs): + """return a SQL EXCEPT of this select() construct against the given + selectable.""" + + return CompoundSelect._create_except(self, other, **kwargs) + + def except_all(self, other, **kwargs): + """return a SQL EXCEPT ALL of this select() construct against the + given selectable. + + """ + return CompoundSelect._create_except_all(self, other, **kwargs) + + def intersect(self, other, **kwargs): + """return a SQL INTERSECT of this select() construct against the given + selectable. + + """ + return CompoundSelect._create_intersect(self, other, **kwargs) + + def intersect_all(self, other, **kwargs): + """return a SQL INTERSECT ALL of this select() construct against the + given selectable. + + """ + return CompoundSelect._create_intersect_all(self, other, **kwargs) + + def bind(self): + if self._bind: + return self._bind + froms = self._froms + if not froms: + for c in self._raw_columns: + e = c.bind + if e: + self._bind = e + return e + else: + e = list(froms)[0].bind + if e: + self._bind = e + return e + + return None + + def _set_bind(self, bind): + self._bind = bind + bind = property(bind, _set_bind) + + +class ScalarSelect(Generative, Grouping): + _from_objects = [] + + def __init__(self, element): + self.element = element + self.type = element._scalar_type() + + @property + def columns(self): + raise exc.InvalidRequestError('Scalar Select expression has no ' + 'columns; use this object directly within a ' + 'column-level expression.') + c = columns + + @_generative + def where(self, crit): + """Apply a WHERE clause to the SELECT statement referred to + by this :class:`.ScalarSelect`. + + """ + self.element = self.element.where(crit) + + def self_group(self, **kwargs): + return self + + +class Exists(UnaryExpression): + """Represent an ``EXISTS`` clause. + + """ + __visit_name__ = UnaryExpression.__visit_name__ + _from_objects = [] + + + def __init__(self, *args, **kwargs): + """Construct a new :class:`.Exists` against an existing + :class:`.Select` object. + + Calling styles are of the following forms:: + + # use on an existing select() + s = select([table.c.col1]).where(table.c.col2==5) + s = exists(s) + + # construct a select() at once + exists(['*'], **select_arguments).where(criterion) + + # columns argument is optional, generates "EXISTS (SELECT *)" + # by default. + exists().where(table.c.col2==5) + + """ + if args and isinstance(args[0], (SelectBase, ScalarSelect)): + s = args[0] + else: + if not args: + args = ([literal_column('*')],) + s = Select(*args, **kwargs).as_scalar().self_group() + + UnaryExpression.__init__(self, s, operator=operators.exists, + type_=type_api.BOOLEANTYPE) + + def select(self, whereclause=None, **params): + return Select([self], whereclause, **params) + + def correlate(self, *fromclause): + e = self._clone() + e.element = self.element.correlate(*fromclause).self_group() + return e + + def correlate_except(self, *fromclause): + e = self._clone() + e.element = self.element.correlate_except(*fromclause).self_group() + return e + + def select_from(self, clause): + """return a new :class:`.Exists` construct, applying the given + expression to the :meth:`.Select.select_from` method of the select + statement contained. + + """ + e = self._clone() + e.element = self.element.select_from(clause).self_group() + return e + + def where(self, clause): + """return a new exists() construct with the given expression added to + its WHERE clause, joined to the existing clause via AND, if any. + + """ + e = self._clone() + e.element = self.element.where(clause).self_group() + return e + + +class TextAsFrom(SelectBase): + """Wrap a :class:`.TextClause` construct within a :class:`.SelectBase` + interface. + + This allows the :class:`.TextClause` object to gain a ``.c`` collection and + other FROM-like capabilities such as :meth:`.FromClause.alias`, + :meth:`.SelectBase.cte`, etc. + + The :class:`.TextAsFrom` construct is produced via the + :meth:`.TextClause.columns` method - see that method for details. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :func:`.text` + + :meth:`.TextClause.columns` + + """ + __visit_name__ = "text_as_from" + + def __init__(self, text, columns): + self.element = text + self.column_args = columns + + @property + def _bind(self): + return self.element._bind + + @_generative + def bindparams(self, *binds, **bind_as_values): + self.element = self.element.bindparams(*binds, **bind_as_values) + + def _populate_column_collection(self): + for c in self.column_args: + c._make_proxy(self) + + def _copy_internals(self, clone=_clone, **kw): + self._reset_exported() + self.element = clone(self.element, **kw) + + def _scalar_type(self): + return self.column_args[0].type + +class AnnotatedFromClause(Annotated): + def __init__(self, element, values): + # force FromClause to generate their internal + # collections into __dict__ + element.c + Annotated.__init__(self, element, values) + + diff --git a/libs/sqlalchemy/sql/sqltypes.py b/libs/sqlalchemy/sql/sqltypes.py new file mode 100644 index 00000000..702e7736 --- /dev/null +++ b/libs/sqlalchemy/sql/sqltypes.py @@ -0,0 +1,1628 @@ +# sql/sqltypes.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""SQL specific types. + +""" + +import datetime as dt +import codecs + +from .type_api import TypeEngine, TypeDecorator, to_instance +from .elements import quoted_name, type_coerce +from .default_comparator import _DefaultColumnComparator +from .. import exc, util, processors +from .base import _bind_or_error, SchemaEventTarget +from . import operators +from .. import event +from ..util import pickle +import decimal + +if util.jython: + import array + +class _DateAffinity(object): + """Mixin date/time specific expression adaptations. + + Rules are implemented within Date,Time,Interval,DateTime, Numeric, + Integer. Based on http://www.postgresql.org/docs/current/static + /functions-datetime.html. + + """ + + @property + def _expression_adaptations(self): + raise NotImplementedError() + + class Comparator(TypeEngine.Comparator): + _blank_dict = util.immutabledict() + + def _adapt_expression(self, op, other_comparator): + othertype = other_comparator.type._type_affinity + return op, \ + to_instance(self.type._expression_adaptations.get(op, self._blank_dict).\ + get(othertype, NULLTYPE)) + comparator_factory = Comparator + +class Concatenable(object): + """A mixin that marks a type as supporting 'concatenation', + typically strings.""" + + class Comparator(TypeEngine.Comparator): + def _adapt_expression(self, op, other_comparator): + if op is operators.add and isinstance(other_comparator, + (Concatenable.Comparator, NullType.Comparator)): + return operators.concat_op, self.expr.type + else: + return op, self.expr.type + + comparator_factory = Comparator + + +class String(Concatenable, TypeEngine): + """The base for all string and character types. + + In SQL, corresponds to VARCHAR. Can also take Python unicode objects + and encode to the database's encoding in bind params (and the reverse for + result sets.) + + The `length` field is usually required when the `String` type is + used within a CREATE TABLE statement, as VARCHAR requires a length + on most databases. + + """ + + __visit_name__ = 'string' + + def __init__(self, length=None, collation=None, + convert_unicode=False, + unicode_error=None, + _warn_on_bytestring=False + ): + """ + Create a string-holding type. + + :param length: optional, a length for the column for use in + DDL and CAST expressions. May be safely omitted if no ``CREATE + TABLE`` will be issued. Certain databases may require a + ``length`` for use in DDL, and will raise an exception when + the ``CREATE TABLE`` DDL is issued if a ``VARCHAR`` + with no length is included. Whether the value is + interpreted as bytes or characters is database specific. + + :param collation: Optional, a column-level collation for + use in DDL and CAST expressions. Renders using the + COLLATE keyword supported by SQLite, MySQL, and Postgresql. + E.g.:: + + >>> from sqlalchemy import cast, select, String + >>> print select([cast('some string', String(collation='utf8'))]) + SELECT CAST(:param_1 AS VARCHAR COLLATE utf8) AS anon_1 + + .. versionadded:: 0.8 Added support for COLLATE to all + string types. + + :param convert_unicode: When set to ``True``, the + :class:`.String` type will assume that + input is to be passed as Python ``unicode`` objects, + and results returned as Python ``unicode`` objects. + If the DBAPI in use does not support Python unicode + (which is fewer and fewer these days), SQLAlchemy + will encode/decode the value, using the + value of the ``encoding`` parameter passed to + :func:`.create_engine` as the encoding. + + When using a DBAPI that natively supports Python + unicode objects, this flag generally does not + need to be set. For columns that are explicitly + intended to store non-ASCII data, the :class:`.Unicode` + or :class:`.UnicodeText` + types should be used regardless, which feature + the same behavior of ``convert_unicode`` but + also indicate an underlying column type that + directly supports unicode, such as ``NVARCHAR``. + + For the extremely rare case that Python ``unicode`` + is to be encoded/decoded by SQLAlchemy on a backend + that does natively support Python ``unicode``, + the value ``force`` can be passed here which will + cause SQLAlchemy's encode/decode services to be + used unconditionally. + + :param unicode_error: Optional, a method to use to handle Unicode + conversion errors. Behaves like the ``errors`` keyword argument to + the standard library's ``string.decode()`` functions. This flag + requires that ``convert_unicode`` is set to ``force`` - otherwise, + SQLAlchemy is not guaranteed to handle the task of unicode + conversion. Note that this flag adds significant performance + overhead to row-fetching operations for backends that already + return unicode objects natively (which most DBAPIs do). This + flag should only be used as a last resort for reading + strings from a column with varied or corrupted encodings. + + """ + if unicode_error is not None and convert_unicode != 'force': + raise exc.ArgumentError("convert_unicode must be 'force' " + "when unicode_error is set.") + + self.length = length + self.collation = collation + self.convert_unicode = convert_unicode + self.unicode_error = unicode_error + self._warn_on_bytestring = _warn_on_bytestring + + def literal_processor(self, dialect): + def process(value): + value = value.replace("'", "''") + return "'%s'" % value + return process + + def bind_processor(self, dialect): + if self.convert_unicode or dialect.convert_unicode: + if dialect.supports_unicode_binds and \ + self.convert_unicode != 'force': + if self._warn_on_bytestring: + def process(value): + if isinstance(value, util.binary_type): + util.warn("Unicode type received non-unicode bind " + "param value.") + return value + return process + else: + return None + else: + encoder = codecs.getencoder(dialect.encoding) + warn_on_bytestring = self._warn_on_bytestring + + def process(value): + if isinstance(value, util.text_type): + return encoder(value, self.unicode_error)[0] + elif warn_on_bytestring and value is not None: + util.warn("Unicode type received non-unicode bind " + "param value") + return value + return process + else: + return None + + def result_processor(self, dialect, coltype): + wants_unicode = self.convert_unicode or dialect.convert_unicode + needs_convert = wants_unicode and \ + (dialect.returns_unicode_strings is not True or + self.convert_unicode in ('force', 'force_nocheck')) + needs_isinstance = ( + needs_convert and + dialect.returns_unicode_strings and + self.convert_unicode != 'force_nocheck' + ) + + if needs_convert: + to_unicode = processors.to_unicode_processor_factory( + dialect.encoding, self.unicode_error) + + if needs_isinstance: + # we wouldn't be here unless convert_unicode='force' + # was specified, or the driver has erratic unicode-returning + # habits. since we will be getting back unicode + # in most cases, we check for it (decode will fail). + def process(value): + if isinstance(value, util.text_type): + return value + else: + return to_unicode(value) + return process + else: + # here, we assume that the object is not unicode, + # avoiding expensive isinstance() check. + return to_unicode + else: + return None + + @property + def python_type(self): + if self.convert_unicode: + return util.text_type + else: + return str + + def get_dbapi_type(self, dbapi): + return dbapi.STRING + + +class Text(String): + """A variably sized string type. + + In SQL, usually corresponds to CLOB or TEXT. Can also take Python + unicode objects and encode to the database's encoding in bind + params (and the reverse for result sets.) In general, TEXT objects + do not have a length; while some databases will accept a length + argument here, it will be rejected by others. + + """ + __visit_name__ = 'text' + + +class Unicode(String): + """A variable length Unicode string type. + + The :class:`.Unicode` type is a :class:`.String` subclass + that assumes input and output as Python ``unicode`` data, + and in that regard is equivalent to the usage of the + ``convert_unicode`` flag with the :class:`.String` type. + However, unlike plain :class:`.String`, it also implies an + underlying column type that is explicitly supporting of non-ASCII + data, such as ``NVARCHAR`` on Oracle and SQL Server. + This can impact the output of ``CREATE TABLE`` statements + and ``CAST`` functions at the dialect level, and can + also affect the handling of bound parameters in some + specific DBAPI scenarios. + + The encoding used by the :class:`.Unicode` type is usually + determined by the DBAPI itself; most modern DBAPIs + feature support for Python ``unicode`` objects as bound + values and result set values, and the encoding should + be configured as detailed in the notes for the target + DBAPI in the :ref:`dialect_toplevel` section. + + For those DBAPIs which do not support, or are not configured + to accommodate Python ``unicode`` objects + directly, SQLAlchemy does the encoding and decoding + outside of the DBAPI. The encoding in this scenario + is determined by the ``encoding`` flag passed to + :func:`.create_engine`. + + When using the :class:`.Unicode` type, it is only appropriate + to pass Python ``unicode`` objects, and not plain ``str``. + If a plain ``str`` is passed under Python 2, a warning + is emitted. If you notice your application emitting these warnings but + you're not sure of the source of them, the Python + ``warnings`` filter, documented at + http://docs.python.org/library/warnings.html, + can be used to turn these warnings into exceptions + which will illustrate a stack trace:: + + import warnings + warnings.simplefilter('error') + + For an application that wishes to pass plain bytestrings + and Python ``unicode`` objects to the ``Unicode`` type + equally, the bytestrings must first be decoded into + unicode. The recipe at :ref:`coerce_to_unicode` illustrates + how this is done. + + See also: + + :class:`.UnicodeText` - unlengthed textual counterpart + to :class:`.Unicode`. + + """ + + __visit_name__ = 'unicode' + + def __init__(self, length=None, **kwargs): + """ + Create a :class:`.Unicode` object. + + Parameters are the same as that of :class:`.String`, + with the exception that ``convert_unicode`` + defaults to ``True``. + + """ + kwargs.setdefault('convert_unicode', True) + kwargs.setdefault('_warn_on_bytestring', True) + super(Unicode, self).__init__(length=length, **kwargs) + + +class UnicodeText(Text): + """An unbounded-length Unicode string type. + + See :class:`.Unicode` for details on the unicode + behavior of this object. + + Like :class:`.Unicode`, usage the :class:`.UnicodeText` type implies a + unicode-capable type being used on the backend, such as + ``NCLOB``, ``NTEXT``. + + """ + + __visit_name__ = 'unicode_text' + + def __init__(self, length=None, **kwargs): + """ + Create a Unicode-converting Text type. + + Parameters are the same as that of :class:`.Text`, + with the exception that ``convert_unicode`` + defaults to ``True``. + + """ + kwargs.setdefault('convert_unicode', True) + kwargs.setdefault('_warn_on_bytestring', True) + super(UnicodeText, self).__init__(length=length, **kwargs) + + +class Integer(_DateAffinity, TypeEngine): + """A type for ``int`` integers.""" + + __visit_name__ = 'integer' + + def get_dbapi_type(self, dbapi): + return dbapi.NUMBER + + @property + def python_type(self): + return int + + def literal_processor(self, dialect): + def process(value): + return str(value) + return process + + @util.memoized_property + def _expression_adaptations(self): + # TODO: need a dictionary object that will + # handle operators generically here, this is incomplete + return { + operators.add: { + Date: Date, + Integer: self.__class__, + Numeric: Numeric, + }, + operators.mul: { + Interval: Interval, + Integer: self.__class__, + Numeric: Numeric, + }, + operators.div: { + Integer: self.__class__, + Numeric: Numeric, + }, + operators.truediv: { + Integer: self.__class__, + Numeric: Numeric, + }, + operators.sub: { + Integer: self.__class__, + Numeric: Numeric, + }, + } + + + +class SmallInteger(Integer): + """A type for smaller ``int`` integers. + + Typically generates a ``SMALLINT`` in DDL, and otherwise acts like + a normal :class:`.Integer` on the Python side. + + """ + + __visit_name__ = 'small_integer' + + +class BigInteger(Integer): + """A type for bigger ``int`` integers. + + Typically generates a ``BIGINT`` in DDL, and otherwise acts like + a normal :class:`.Integer` on the Python side. + + """ + + __visit_name__ = 'big_integer' + + + +class Numeric(_DateAffinity, TypeEngine): + """A type for fixed precision numbers. + + Typically generates DECIMAL or NUMERIC. Returns + ``decimal.Decimal`` objects by default, applying + conversion as needed. + + .. note:: + + The `cdecimal `_ library + is a high performing alternative to Python's built-in + ``decimal.Decimal`` type, which performs very poorly in high volume + situations. SQLAlchemy 0.7 is tested against ``cdecimal`` and supports + it fully. The type is not necessarily supported by DBAPI + implementations however, most of which contain an import for plain + ``decimal`` in their source code, even though some such as psycopg2 + provide hooks for alternate adapters. SQLAlchemy imports ``decimal`` + globally as well. The most straightforward and + foolproof way to use "cdecimal" given current DBAPI and Python support + is to patch it directly into sys.modules before anything else is + imported:: + + import sys + import cdecimal + sys.modules["decimal"] = cdecimal + + While the global patch is a little ugly, it's particularly + important to use just one decimal library at a time since + Python Decimal and cdecimal Decimal objects + are not currently compatible *with each other*:: + + >>> import cdecimal + >>> import decimal + >>> decimal.Decimal("10") == cdecimal.Decimal("10") + False + + SQLAlchemy will provide more natural support of + cdecimal if and when it becomes a standard part of Python + installations and is supported by all DBAPIs. + + """ + + __visit_name__ = 'numeric' + + _default_decimal_return_scale = 10 + + def __init__(self, precision=None, scale=None, + decimal_return_scale=None, asdecimal=True): + """ + Construct a Numeric. + + :param precision: the numeric precision for use in DDL ``CREATE + TABLE``. + + :param scale: the numeric scale for use in DDL ``CREATE TABLE``. + + :param asdecimal: default True. Return whether or not + values should be sent as Python Decimal objects, or + as floats. Different DBAPIs send one or the other based on + datatypes - the Numeric type will ensure that return values + are one or the other across DBAPIs consistently. + + :param decimal_return_scale: Default scale to use when converting + from floats to Python decimals. Floating point values will typically + be much longer due to decimal inaccuracy, and most floating point + database types don't have a notion of "scale", so by default the + float type looks for the first ten decimal places when converting. + Specfiying this value will override that length. Types which + do include an explicit ".scale" value, such as the base :class:`.Numeric` + as well as the MySQL float types, will use the value of ".scale" + as the default for decimal_return_scale, if not otherwise specified. + + .. versionadded:: 0.9.0 + + When using the ``Numeric`` type, care should be taken to ensure + that the asdecimal setting is apppropriate for the DBAPI in use - + when Numeric applies a conversion from Decimal->float or float-> + Decimal, this conversion incurs an additional performance overhead + for all result columns received. + + DBAPIs that return Decimal natively (e.g. psycopg2) will have + better accuracy and higher performance with a setting of ``True``, + as the native translation to Decimal reduces the amount of floating- + point issues at play, and the Numeric type itself doesn't need + to apply any further conversions. However, another DBAPI which + returns floats natively *will* incur an additional conversion + overhead, and is still subject to floating point data loss - in + which case ``asdecimal=False`` will at least remove the extra + conversion overhead. + + """ + self.precision = precision + self.scale = scale + self.decimal_return_scale = decimal_return_scale + self.asdecimal = asdecimal + + @property + def _effective_decimal_return_scale(self): + if self.decimal_return_scale is not None: + return self.decimal_return_scale + elif getattr(self, "scale", None) is not None: + return self.scale + else: + return self._default_decimal_return_scale + + def get_dbapi_type(self, dbapi): + return dbapi.NUMBER + + def literal_processor(self, dialect): + def process(value): + return str(value) + return process + + @property + def python_type(self): + if self.asdecimal: + return decimal.Decimal + else: + return float + + def bind_processor(self, dialect): + if dialect.supports_native_decimal: + return None + else: + return processors.to_float + + def result_processor(self, dialect, coltype): + if self.asdecimal: + if dialect.supports_native_decimal: + # we're a "numeric", DBAPI will give us Decimal directly + return None + else: + util.warn('Dialect %s+%s does *not* support Decimal ' + 'objects natively, and SQLAlchemy must ' + 'convert from floating point - rounding ' + 'errors and other issues may occur. Please ' + 'consider storing Decimal numbers as strings ' + 'or integers on this platform for lossless ' + 'storage.' % (dialect.name, dialect.driver)) + + # we're a "numeric", DBAPI returns floats, convert. + return processors.to_decimal_processor_factory( + decimal.Decimal, + self.scale if self.scale is not None + else self._default_decimal_return_scale) + else: + if dialect.supports_native_decimal: + return processors.to_float + else: + return None + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.mul: { + Interval: Interval, + Numeric: self.__class__, + Integer: self.__class__, + }, + operators.div: { + Numeric: self.__class__, + Integer: self.__class__, + }, + operators.truediv: { + Numeric: self.__class__, + Integer: self.__class__, + }, + operators.add: { + Numeric: self.__class__, + Integer: self.__class__, + }, + operators.sub: { + Numeric: self.__class__, + Integer: self.__class__, + } + } + + +class Float(Numeric): + """A type for ``float`` numbers. + + Returns Python ``float`` objects by default, applying + conversion as needed. + + """ + + __visit_name__ = 'float' + + scale = None + + def __init__(self, precision=None, asdecimal=False, + decimal_return_scale=None, **kwargs): + """ + Construct a Float. + + :param precision: the numeric precision for use in DDL ``CREATE + TABLE``. + + :param asdecimal: the same flag as that of :class:`.Numeric`, but + defaults to ``False``. Note that setting this flag to ``True`` + results in floating point conversion. + + :param decimal_return_scale: Default scale to use when converting + from floats to Python decimals. Floating point values will typically + be much longer due to decimal inaccuracy, and most floating point + database types don't have a notion of "scale", so by default the + float type looks for the first ten decimal places when converting. + Specfiying this value will override that length. Note that the + MySQL float types, which do include "scale", will use "scale" + as the default for decimal_return_scale, if not otherwise specified. + + .. versionadded:: 0.9.0 + + :param \**kwargs: deprecated. Additional arguments here are ignored + by the default :class:`.Float` type. For database specific + floats that support additional arguments, see that dialect's + documentation for details, such as + :class:`sqlalchemy.dialects.mysql.FLOAT`. + + """ + self.precision = precision + self.asdecimal = asdecimal + self.decimal_return_scale = decimal_return_scale + if kwargs: + util.warn_deprecated("Additional keyword arguments " + "passed to Float ignored.") + + def result_processor(self, dialect, coltype): + if self.asdecimal: + return processors.to_decimal_processor_factory( + decimal.Decimal, + self._effective_decimal_return_scale) + else: + return None + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.mul: { + Interval: Interval, + Numeric: self.__class__, + }, + operators.div: { + Numeric: self.__class__, + }, + operators.truediv: { + Numeric: self.__class__, + }, + operators.add: { + Numeric: self.__class__, + }, + operators.sub: { + Numeric: self.__class__, + } + } + + +class DateTime(_DateAffinity, TypeEngine): + """A type for ``datetime.datetime()`` objects. + + Date and time types return objects from the Python ``datetime`` + module. Most DBAPIs have built in support for the datetime + module, with the noted exception of SQLite. In the case of + SQLite, date and time types are stored as strings which are then + converted back to datetime objects when rows are returned. + + """ + + __visit_name__ = 'datetime' + + def __init__(self, timezone=False): + """Construct a new :class:`.DateTime`. + + :param timezone: boolean. If True, and supported by the + backend, will produce 'TIMESTAMP WITH TIMEZONE'. For backends + that don't support timezone aware timestamps, has no + effect. + + """ + self.timezone = timezone + + def get_dbapi_type(self, dbapi): + return dbapi.DATETIME + + @property + def python_type(self): + return dt.datetime + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.add: { + Interval: self.__class__, + }, + operators.sub: { + Interval: self.__class__, + DateTime: Interval, + }, + } + + +class Date(_DateAffinity, TypeEngine): + """A type for ``datetime.date()`` objects.""" + + __visit_name__ = 'date' + + def get_dbapi_type(self, dbapi): + return dbapi.DATETIME + + @property + def python_type(self): + return dt.date + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.add: { + Integer: self.__class__, + Interval: DateTime, + Time: DateTime, + }, + operators.sub: { + # date - integer = date + Integer: self.__class__, + + # date - date = integer. + Date: Integer, + + Interval: DateTime, + + # date - datetime = interval, + # this one is not in the PG docs + # but works + DateTime: Interval, + }, + } + + +class Time(_DateAffinity, TypeEngine): + """A type for ``datetime.time()`` objects.""" + + __visit_name__ = 'time' + + def __init__(self, timezone=False): + self.timezone = timezone + + def get_dbapi_type(self, dbapi): + return dbapi.DATETIME + + @property + def python_type(self): + return dt.time + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.add: { + Date: DateTime, + Interval: self.__class__ + }, + operators.sub: { + Time: Interval, + Interval: self.__class__, + }, + } + + +class _Binary(TypeEngine): + """Define base behavior for binary types.""" + + def __init__(self, length=None): + self.length = length + + def literal_processor(self, dialect): + def process(value): + value = value.decode(self.dialect.encoding).replace("'", "''") + return "'%s'" % value + return process + + @property + def python_type(self): + return util.binary_type + + # Python 3 - sqlite3 doesn't need the `Binary` conversion + # here, though pg8000 does to indicate "bytea" + def bind_processor(self, dialect): + DBAPIBinary = dialect.dbapi.Binary + + def process(value): + if value is not None: + return DBAPIBinary(value) + else: + return None + return process + + # Python 3 has native bytes() type + # both sqlite3 and pg8000 seem to return it, + # psycopg2 as of 2.5 returns 'memoryview' + if util.py2k: + def result_processor(self, dialect, coltype): + if util.jython: + def process(value): + if value is not None: + if isinstance(value, array.array): + return value.tostring() + return str(value) + else: + return None + else: + process = processors.to_str + return process + else: + def result_processor(self, dialect, coltype): + def process(value): + if value is not None: + value = bytes(value) + return value + return process + + def coerce_compared_value(self, op, value): + """See :meth:`.TypeEngine.coerce_compared_value` for a description.""" + + if isinstance(value, util.string_types): + return self + else: + return super(_Binary, self).coerce_compared_value(op, value) + + def get_dbapi_type(self, dbapi): + return dbapi.BINARY + + +class LargeBinary(_Binary): + """A type for large binary byte data. + + The Binary type generates BLOB or BYTEA when tables are created, + and also converts incoming values using the ``Binary`` callable + provided by each DB-API. + + """ + + __visit_name__ = 'large_binary' + + def __init__(self, length=None): + """ + Construct a LargeBinary type. + + :param length: optional, a length for the column for use in + DDL statements, for those BLOB types that accept a length + (i.e. MySQL). It does *not* produce a small BINARY/VARBINARY + type - use the BINARY/VARBINARY types specifically for those. + May be safely omitted if no ``CREATE + TABLE`` will be issued. Certain databases may require a + *length* for use in DDL, and will raise an exception when + the ``CREATE TABLE`` DDL is issued. + + """ + _Binary.__init__(self, length=length) + + +class Binary(LargeBinary): + """Deprecated. Renamed to LargeBinary.""" + + def __init__(self, *arg, **kw): + util.warn_deprecated('The Binary type has been renamed to ' + 'LargeBinary.') + LargeBinary.__init__(self, *arg, **kw) + + + +class SchemaType(SchemaEventTarget): + """Mark a type as possibly requiring schema-level DDL for usage. + + Supports types that must be explicitly created/dropped (i.e. PG ENUM type) + as well as types that are complimented by table or schema level + constraints, triggers, and other rules. + + :class:`.SchemaType` classes can also be targets for the + :meth:`.DDLEvents.before_parent_attach` and + :meth:`.DDLEvents.after_parent_attach` events, where the events fire off + surrounding the association of the type object with a parent + :class:`.Column`. + + .. seealso:: + + :class:`.Enum` + + :class:`.Boolean` + + + """ + + def __init__(self, name=None, schema=None, metadata=None, + inherit_schema=False, quote=None): + if name is not None: + self.name = quoted_name(name, quote) + else: + self.name = None + self.schema = schema + self.metadata = metadata + self.inherit_schema = inherit_schema + if self.metadata: + event.listen( + self.metadata, + "before_create", + util.portable_instancemethod(self._on_metadata_create) + ) + event.listen( + self.metadata, + "after_drop", + util.portable_instancemethod(self._on_metadata_drop) + ) + + def _set_parent(self, column): + column._on_table_attach(util.portable_instancemethod(self._set_table)) + + def _set_table(self, column, table): + if self.inherit_schema: + self.schema = table.schema + + event.listen( + table, + "before_create", + util.portable_instancemethod( + self._on_table_create) + ) + event.listen( + table, + "after_drop", + util.portable_instancemethod(self._on_table_drop) + ) + if self.metadata is None: + # TODO: what's the difference between self.metadata + # and table.metadata here ? + event.listen( + table.metadata, + "before_create", + util.portable_instancemethod(self._on_metadata_create) + ) + event.listen( + table.metadata, + "after_drop", + util.portable_instancemethod(self._on_metadata_drop) + ) + + def copy(self, **kw): + return self.adapt(self.__class__) + + def adapt(self, impltype, **kw): + schema = kw.pop('schema', self.schema) + metadata = kw.pop('metadata', self.metadata) + return impltype(name=self.name, + schema=schema, + metadata=metadata, + inherit_schema=self.inherit_schema, + **kw + ) + + @property + def bind(self): + return self.metadata and self.metadata.bind or None + + def create(self, bind=None, checkfirst=False): + """Issue CREATE ddl for this type, if applicable.""" + + if bind is None: + bind = _bind_or_error(self) + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t.create(bind=bind, checkfirst=checkfirst) + + def drop(self, bind=None, checkfirst=False): + """Issue DROP ddl for this type, if applicable.""" + + if bind is None: + bind = _bind_or_error(self) + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t.drop(bind=bind, checkfirst=checkfirst) + + def _on_table_create(self, target, bind, **kw): + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t._on_table_create(target, bind, **kw) + + def _on_table_drop(self, target, bind, **kw): + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t._on_table_drop(target, bind, **kw) + + def _on_metadata_create(self, target, bind, **kw): + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t._on_metadata_create(target, bind, **kw) + + def _on_metadata_drop(self, target, bind, **kw): + t = self.dialect_impl(bind.dialect) + if t.__class__ is not self.__class__ and isinstance(t, SchemaType): + t._on_metadata_drop(target, bind, **kw) + +class Enum(String, SchemaType): + """Generic Enum Type. + + The Enum type provides a set of possible string values which the + column is constrained towards. + + By default, uses the backend's native ENUM type if available, + else uses VARCHAR + a CHECK constraint. + + .. seealso:: + + :class:`~.postgresql.ENUM` - PostgreSQL-specific type, + which has additional functionality. + + """ + + __visit_name__ = 'enum' + + def __init__(self, *enums, **kw): + """Construct an enum. + + Keyword arguments which don't apply to a specific backend are ignored + by that backend. + + :param \*enums: string or unicode enumeration labels. If unicode + labels are present, the `convert_unicode` flag is auto-enabled. + + :param convert_unicode: Enable unicode-aware bind parameter and + result-set processing for this Enum's data. This is set + automatically based on the presence of unicode label strings. + + :param metadata: Associate this type directly with a ``MetaData`` + object. For types that exist on the target database as an + independent schema construct (Postgresql), this type will be + created and dropped within ``create_all()`` and ``drop_all()`` + operations. If the type is not associated with any ``MetaData`` + object, it will associate itself with each ``Table`` in which it is + used, and will be created when any of those individual tables are + created, after a check is performed for it's existence. The type is + only dropped when ``drop_all()`` is called for that ``Table`` + object's metadata, however. + + :param name: The name of this type. This is required for Postgresql + and any future supported database which requires an explicitly + named type, or an explicitly named constraint in order to generate + the type and/or a table that uses it. + + :param native_enum: Use the database's native ENUM type when + available. Defaults to True. When False, uses VARCHAR + check + constraint for all backends. + + :param schema: Schema name of this type. For types that exist on the + target database as an independent schema construct (Postgresql), + this parameter specifies the named schema in which the type is + present. + + .. note:: + + The ``schema`` of the :class:`.Enum` type does not + by default make use of the ``schema`` established on the + owning :class:`.Table`. If this behavior is desired, + set the ``inherit_schema`` flag to ``True``. + + :param quote: Set explicit quoting preferences for the type's name. + + :param inherit_schema: When ``True``, the "schema" from the owning + :class:`.Table` will be copied to the "schema" attribute of this + :class:`.Enum`, replacing whatever value was passed for the + ``schema`` attribute. This also takes effect when using the + :meth:`.Table.tometadata` operation. + + .. versionadded:: 0.8 + + """ + self.enums = enums + self.native_enum = kw.pop('native_enum', True) + convert_unicode = kw.pop('convert_unicode', None) + if convert_unicode is None: + for e in enums: + if isinstance(e, util.text_type): + convert_unicode = True + break + else: + convert_unicode = False + + if self.enums: + length = max(len(x) for x in self.enums) + else: + length = 0 + String.__init__(self, + length=length, + convert_unicode=convert_unicode, + ) + SchemaType.__init__(self, **kw) + + def __repr__(self): + return util.generic_repr(self, + to_inspect=[Enum, SchemaType], + ) + + def _should_create_constraint(self, compiler): + return not self.native_enum or \ + not compiler.dialect.supports_native_enum + + @util.dependencies("sqlalchemy.sql.schema") + def _set_table(self, schema, column, table): + if self.native_enum: + SchemaType._set_table(self, column, table) + + e = schema.CheckConstraint( + type_coerce(column, self).in_(self.enums), + name=self.name, + _create_rule=util.portable_instancemethod( + self._should_create_constraint) + ) + table.append_constraint(e) + + def adapt(self, impltype, **kw): + schema = kw.pop('schema', self.schema) + metadata = kw.pop('metadata', self.metadata) + if issubclass(impltype, Enum): + return impltype(name=self.name, + schema=schema, + metadata=metadata, + convert_unicode=self.convert_unicode, + native_enum=self.native_enum, + inherit_schema=self.inherit_schema, + *self.enums, + **kw + ) + else: + return super(Enum, self).adapt(impltype, **kw) + + +class PickleType(TypeDecorator): + """Holds Python objects, which are serialized using pickle. + + PickleType builds upon the Binary type to apply Python's + ``pickle.dumps()`` to incoming objects, and ``pickle.loads()`` on + the way out, allowing any pickleable Python object to be stored as + a serialized binary field. + + To allow ORM change events to propagate for elements associated + with :class:`.PickleType`, see :ref:`mutable_toplevel`. + + """ + + impl = LargeBinary + + def __init__(self, protocol=pickle.HIGHEST_PROTOCOL, + pickler=None, comparator=None): + """ + Construct a PickleType. + + :param protocol: defaults to ``pickle.HIGHEST_PROTOCOL``. + + :param pickler: defaults to cPickle.pickle or pickle.pickle if + cPickle is not available. May be any object with + pickle-compatible ``dumps` and ``loads`` methods. + + :param comparator: a 2-arg callable predicate used + to compare values of this type. If left as ``None``, + the Python "equals" operator is used to compare values. + + """ + self.protocol = protocol + self.pickler = pickler or pickle + self.comparator = comparator + super(PickleType, self).__init__() + + def __reduce__(self): + return PickleType, (self.protocol, + None, + self.comparator) + + def bind_processor(self, dialect): + impl_processor = self.impl.bind_processor(dialect) + dumps = self.pickler.dumps + protocol = self.protocol + if impl_processor: + def process(value): + if value is not None: + value = dumps(value, protocol) + return impl_processor(value) + else: + def process(value): + if value is not None: + value = dumps(value, protocol) + return value + return process + + def result_processor(self, dialect, coltype): + impl_processor = self.impl.result_processor(dialect, coltype) + loads = self.pickler.loads + if impl_processor: + def process(value): + value = impl_processor(value) + if value is None: + return None + return loads(value) + else: + def process(value): + if value is None: + return None + return loads(value) + return process + + def compare_values(self, x, y): + if self.comparator: + return self.comparator(x, y) + else: + return x == y + + +class Boolean(TypeEngine, SchemaType): + """A bool datatype. + + Boolean typically uses BOOLEAN or SMALLINT on the DDL side, and on + the Python side deals in ``True`` or ``False``. + + """ + + __visit_name__ = 'boolean' + + def __init__(self, create_constraint=True, name=None): + """Construct a Boolean. + + :param create_constraint: defaults to True. If the boolean + is generated as an int/smallint, also create a CHECK constraint + on the table that ensures 1 or 0 as a value. + + :param name: if a CHECK constraint is generated, specify + the name of the constraint. + + """ + self.create_constraint = create_constraint + self.name = name + + def _should_create_constraint(self, compiler): + return not compiler.dialect.supports_native_boolean + + @util.dependencies("sqlalchemy.sql.schema") + def _set_table(self, schema, column, table): + if not self.create_constraint: + return + + e = schema.CheckConstraint( + type_coerce(column, self).in_([0, 1]), + name=self.name, + _create_rule=util.portable_instancemethod( + self._should_create_constraint) + ) + table.append_constraint(e) + + @property + def python_type(self): + return bool + + def bind_processor(self, dialect): + if dialect.supports_native_boolean: + return None + else: + return processors.boolean_to_int + + def result_processor(self, dialect, coltype): + if dialect.supports_native_boolean: + return None + else: + return processors.int_to_boolean + + +class Interval(_DateAffinity, TypeDecorator): + """A type for ``datetime.timedelta()`` objects. + + The Interval type deals with ``datetime.timedelta`` objects. In + PostgreSQL, the native ``INTERVAL`` type is used; for others, the + value is stored as a date which is relative to the "epoch" + (Jan. 1, 1970). + + Note that the ``Interval`` type does not currently provide date arithmetic + operations on platforms which do not support interval types natively. Such + operations usually require transformation of both sides of the expression + (such as, conversion of both sides into integer epoch values first) which + currently is a manual procedure (such as via + :attr:`~sqlalchemy.sql.expression.func`). + + """ + + impl = DateTime + epoch = dt.datetime.utcfromtimestamp(0) + + def __init__(self, native=True, + second_precision=None, + day_precision=None): + """Construct an Interval object. + + :param native: when True, use the actual + INTERVAL type provided by the database, if + supported (currently Postgresql, Oracle). + Otherwise, represent the interval data as + an epoch value regardless. + + :param second_precision: For native interval types + which support a "fractional seconds precision" parameter, + i.e. Oracle and Postgresql + + :param day_precision: for native interval types which + support a "day precision" parameter, i.e. Oracle. + + """ + super(Interval, self).__init__() + self.native = native + self.second_precision = second_precision + self.day_precision = day_precision + + def adapt(self, cls, **kw): + if self.native and hasattr(cls, '_adapt_from_generic_interval'): + return cls._adapt_from_generic_interval(self, **kw) + else: + return self.__class__( + native=self.native, + second_precision=self.second_precision, + day_precision=self.day_precision, + **kw) + + @property + def python_type(self): + return dt.timedelta + + def bind_processor(self, dialect): + impl_processor = self.impl.bind_processor(dialect) + epoch = self.epoch + if impl_processor: + def process(value): + if value is not None: + value = epoch + value + return impl_processor(value) + else: + def process(value): + if value is not None: + value = epoch + value + return value + return process + + def result_processor(self, dialect, coltype): + impl_processor = self.impl.result_processor(dialect, coltype) + epoch = self.epoch + if impl_processor: + def process(value): + value = impl_processor(value) + if value is None: + return None + return value - epoch + else: + def process(value): + if value is None: + return None + return value - epoch + return process + + @util.memoized_property + def _expression_adaptations(self): + return { + operators.add: { + Date: DateTime, + Interval: self.__class__, + DateTime: DateTime, + Time: Time, + }, + operators.sub: { + Interval: self.__class__ + }, + operators.mul: { + Numeric: self.__class__ + }, + operators.truediv: { + Numeric: self.__class__ + }, + operators.div: { + Numeric: self.__class__ + } + } + + @property + def _type_affinity(self): + return Interval + + def coerce_compared_value(self, op, value): + """See :meth:`.TypeEngine.coerce_compared_value` for a description.""" + + return self.impl.coerce_compared_value(op, value) + + +class REAL(Float): + """The SQL REAL type.""" + + __visit_name__ = 'REAL' + + +class FLOAT(Float): + """The SQL FLOAT type.""" + + __visit_name__ = 'FLOAT' + + +class NUMERIC(Numeric): + """The SQL NUMERIC type.""" + + __visit_name__ = 'NUMERIC' + + +class DECIMAL(Numeric): + """The SQL DECIMAL type.""" + + __visit_name__ = 'DECIMAL' + + +class INTEGER(Integer): + """The SQL INT or INTEGER type.""" + + __visit_name__ = 'INTEGER' +INT = INTEGER + + +class SMALLINT(SmallInteger): + """The SQL SMALLINT type.""" + + __visit_name__ = 'SMALLINT' + + +class BIGINT(BigInteger): + """The SQL BIGINT type.""" + + __visit_name__ = 'BIGINT' + + +class TIMESTAMP(DateTime): + """The SQL TIMESTAMP type.""" + + __visit_name__ = 'TIMESTAMP' + + def get_dbapi_type(self, dbapi): + return dbapi.TIMESTAMP + + +class DATETIME(DateTime): + """The SQL DATETIME type.""" + + __visit_name__ = 'DATETIME' + + +class DATE(Date): + """The SQL DATE type.""" + + __visit_name__ = 'DATE' + + +class TIME(Time): + """The SQL TIME type.""" + + __visit_name__ = 'TIME' + + +class TEXT(Text): + """The SQL TEXT type.""" + + __visit_name__ = 'TEXT' + + +class CLOB(Text): + """The CLOB type. + + This type is found in Oracle and Informix. + """ + + __visit_name__ = 'CLOB' + + +class VARCHAR(String): + """The SQL VARCHAR type.""" + + __visit_name__ = 'VARCHAR' + + +class NVARCHAR(Unicode): + """The SQL NVARCHAR type.""" + + __visit_name__ = 'NVARCHAR' + + +class CHAR(String): + """The SQL CHAR type.""" + + __visit_name__ = 'CHAR' + + +class NCHAR(Unicode): + """The SQL NCHAR type.""" + + __visit_name__ = 'NCHAR' + + +class BLOB(LargeBinary): + """The SQL BLOB type.""" + + __visit_name__ = 'BLOB' + + +class BINARY(_Binary): + """The SQL BINARY type.""" + + __visit_name__ = 'BINARY' + + +class VARBINARY(_Binary): + """The SQL VARBINARY type.""" + + __visit_name__ = 'VARBINARY' + + +class BOOLEAN(Boolean): + """The SQL BOOLEAN type.""" + + __visit_name__ = 'BOOLEAN' + +class NullType(TypeEngine): + """An unknown type. + + :class:`.NullType` is used as a default type for those cases where + a type cannot be determined, including: + + * During table reflection, when the type of a column is not recognized + by the :class:`.Dialect` + * When constructing SQL expressions using plain Python objects of + unknown types (e.g. ``somecolumn == my_special_object``) + * When a new :class:`.Column` is created, and the given type is passed + as ``None`` or is not passed at all. + + The :class:`.NullType` can be used within SQL expression invocation + without issue, it just has no behavior either at the expression construction + level or at the bind-parameter/result processing level. :class:`.NullType` + will result in a :exc:`.CompileError` if the compiler is asked to render + the type itself, such as if it is used in a :func:`.cast` operation + or within a schema creation operation such as that invoked by + :meth:`.MetaData.create_all` or the :class:`.CreateTable` construct. + + """ + __visit_name__ = 'null' + + _isnull = True + + def literal_processor(self, dialect): + def process(value): + return "NULL" + return process + + class Comparator(TypeEngine.Comparator): + def _adapt_expression(self, op, other_comparator): + if isinstance(other_comparator, NullType.Comparator) or \ + not operators.is_commutative(op): + return op, self.expr.type + else: + return other_comparator._adapt_expression(op, self) + comparator_factory = Comparator + + +NULLTYPE = NullType() +BOOLEANTYPE = Boolean() +STRINGTYPE = String() +INTEGERTYPE = Integer() + +_type_map = { + int: Integer(), + float: Numeric(), + bool: BOOLEANTYPE, + decimal.Decimal: Numeric(), + dt.date: Date(), + dt.datetime: DateTime(), + dt.time: Time(), + dt.timedelta: Interval(), + util.NoneType: NULLTYPE +} + +if util.py3k: + _type_map[bytes] = LargeBinary() + _type_map[str] = Unicode() +else: + _type_map[unicode] = Unicode() + _type_map[str] = String() + + +# back-assign to type_api +from . import type_api +type_api.BOOLEANTYPE = BOOLEANTYPE +type_api.STRINGTYPE = STRINGTYPE +type_api.INTEGERTYPE = INTEGERTYPE +type_api.NULLTYPE = NULLTYPE +type_api._type_map = _type_map + +# this one, there's all kinds of ways to play it, but at the EOD +# there's just a giant dependency cycle between the typing system and +# the expression element system, as you might expect. We can use +# importlaters or whatnot, but the typing system just necessarily has +# to have some kind of connection like this. right now we're injecting the +# _DefaultColumnComparator implementation into the TypeEngine.Comparator interface. +# Alternatively TypeEngine.Comparator could have an "impl" injected, though +# just injecting the base is simpler, error free, and more performant. +class Comparator(_DefaultColumnComparator): + BOOLEANTYPE = BOOLEANTYPE + +TypeEngine.Comparator.__bases__ = (Comparator, ) + TypeEngine.Comparator.__bases__ + diff --git a/libs/sqlalchemy/sql/type_api.py b/libs/sqlalchemy/sql/type_api.py new file mode 100644 index 00000000..c6aad92b --- /dev/null +++ b/libs/sqlalchemy/sql/type_api.py @@ -0,0 +1,1053 @@ +# sql/types_api.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Base types API. + +""" + + +from .. import exc, util +from . import operators +from .visitors import Visitable + +# these are back-assigned by sqltypes. +BOOLEANTYPE = None +INTEGERTYPE = None +NULLTYPE = None +STRINGTYPE = None + +class TypeEngine(Visitable): + """Base for built-in types.""" + + _sqla_type = True + _isnull = False + + class Comparator(operators.ColumnOperators): + """Base class for custom comparison operations defined at the + type level. See :attr:`.TypeEngine.comparator_factory`. + + + """ + + def __init__(self, expr): + self.expr = expr + + def __reduce__(self): + return _reconstitute_comparator, (self.expr, ) + + + hashable = True + """Flag, if False, means values from this type aren't hashable. + + Used by the ORM when uniquing result lists. + + """ + + comparator_factory = Comparator + """A :class:`.TypeEngine.Comparator` class which will apply + to operations performed by owning :class:`.ColumnElement` objects. + + The :attr:`.comparator_factory` attribute is a hook consulted by + the core expression system when column and SQL expression operations + are performed. When a :class:`.TypeEngine.Comparator` class is + associated with this attribute, it allows custom re-definition of + all existing operators, as well as definition of new operators. + Existing operators include those provided by Python operator overloading + such as :meth:`.operators.ColumnOperators.__add__` and + :meth:`.operators.ColumnOperators.__eq__`, + those provided as standard + attributes of :class:`.operators.ColumnOperators` such as + :meth:`.operators.ColumnOperators.like` + and :meth:`.operators.ColumnOperators.in_`. + + Rudimentary usage of this hook is allowed through simple subclassing + of existing types, or alternatively by using :class:`.TypeDecorator`. + See the documentation section :ref:`types_operators` for examples. + + .. versionadded:: 0.8 The expression system was enhanced to support + customization of operators on a per-type level. + + """ + + def copy_value(self, value): + return value + + def literal_processor(self, dialect): + """Return a conversion function for processing literal values that are + to be rendered directly without using binds. + + This function is used when the compiler makes use of the + "literal_binds" flag, typically used in DDL generation as well + as in certain scenarios where backends don't accept bound parameters. + + .. versionadded:: 0.9.0 + + """ + return None + + def bind_processor(self, dialect): + """Return a conversion function for processing bind values. + + Returns a callable which will receive a bind parameter value + as the sole positional argument and will return a value to + send to the DB-API. + + If processing is not necessary, the method should return ``None``. + + :param dialect: Dialect instance in use. + + """ + return None + + def result_processor(self, dialect, coltype): + """Return a conversion function for processing result row values. + + Returns a callable which will receive a result row column + value as the sole positional argument and will return a value + to return to the user. + + If processing is not necessary, the method should return ``None``. + + :param dialect: Dialect instance in use. + + :param coltype: DBAPI coltype argument received in cursor.description. + + """ + return None + + def column_expression(self, colexpr): + """Given a SELECT column expression, return a wrapping SQL expression. + + This is typically a SQL function that wraps a column expression + as rendered in the columns clause of a SELECT statement. + It is used for special data types that require + columns to be wrapped in some special database function in order + to coerce the value before being sent back to the application. + It is the SQL analogue of the :meth:`.TypeEngine.result_processor` + method. + + The method is evaluated at statement compile time, as opposed + to statement construction time. + + See also: + + :ref:`types_sql_value_processing` + + """ + + return None + + @util.memoized_property + def _has_column_expression(self): + """memoized boolean, check if column_expression is implemented. + + Allows the method to be skipped for the vast majority of expression + types that don't use this feature. + + """ + + return self.__class__.column_expression.__code__ \ + is not TypeEngine.column_expression.__code__ + + def bind_expression(self, bindvalue): + """"Given a bind value (i.e. a :class:`.BindParameter` instance), + return a SQL expression in its place. + + This is typically a SQL function that wraps the existing bound + parameter within the statement. It is used for special data types + that require literals being wrapped in some special database function + in order to coerce an application-level value into a database-specific + format. It is the SQL analogue of the + :meth:`.TypeEngine.bind_processor` method. + + The method is evaluated at statement compile time, as opposed + to statement construction time. + + Note that this method, when implemented, should always return + the exact same structure, without any conditional logic, as it + may be used in an executemany() call against an arbitrary number + of bound parameter sets. + + See also: + + :ref:`types_sql_value_processing` + + """ + return None + + @util.memoized_property + def _has_bind_expression(self): + """memoized boolean, check if bind_expression is implemented. + + Allows the method to be skipped for the vast majority of expression + types that don't use this feature. + + """ + + return self.__class__.bind_expression.__code__ \ + is not TypeEngine.bind_expression.__code__ + + def compare_values(self, x, y): + """Compare two values for equality.""" + + return x == y + + def get_dbapi_type(self, dbapi): + """Return the corresponding type object from the underlying DB-API, if + any. + + This can be useful for calling ``setinputsizes()``, for example. + + """ + return None + + @property + def python_type(self): + """Return the Python type object expected to be returned + by instances of this type, if known. + + Basically, for those types which enforce a return type, + or are known across the board to do such for all common + DBAPIs (like ``int`` for example), will return that type. + + If a return type is not defined, raises + ``NotImplementedError``. + + Note that any type also accommodates NULL in SQL which + means you can also get back ``None`` from any type + in practice. + + """ + raise NotImplementedError() + + def with_variant(self, type_, dialect_name): + """Produce a new type object that will utilize the given + type when applied to the dialect of the given name. + + e.g.:: + + from sqlalchemy.types import String + from sqlalchemy.dialects import mysql + + s = String() + + s = s.with_variant(mysql.VARCHAR(collation='foo'), 'mysql') + + The construction of :meth:`.TypeEngine.with_variant` is always + from the "fallback" type to that which is dialect specific. + The returned type is an instance of :class:`.Variant`, which + itself provides a :meth:`~sqlalchemy.types.Variant.with_variant` + that can be called repeatedly. + + :param type_: a :class:`.TypeEngine` that will be selected + as a variant from the originating type, when a dialect + of the given name is in use. + :param dialect_name: base name of the dialect which uses + this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) + + .. versionadded:: 0.7.2 + + """ + return Variant(self, {dialect_name: type_}) + + + @util.memoized_property + def _type_affinity(self): + """Return a rudimental 'affinity' value expressing the general class + of type.""" + + typ = None + for t in self.__class__.__mro__: + if t in (TypeEngine, UserDefinedType): + return typ + elif issubclass(t, (TypeEngine, UserDefinedType)): + typ = t + else: + return self.__class__ + + def dialect_impl(self, dialect): + """Return a dialect-specific implementation for this + :class:`.TypeEngine`. + + """ + try: + return dialect._type_memos[self]['impl'] + except KeyError: + return self._dialect_info(dialect)['impl'] + + + def _cached_literal_processor(self, dialect): + """Return a dialect-specific literal processor for this type.""" + try: + return dialect._type_memos[self]['literal'] + except KeyError: + d = self._dialect_info(dialect) + d['literal'] = lp = d['impl'].literal_processor(dialect) + return lp + + def _cached_bind_processor(self, dialect): + """Return a dialect-specific bind processor for this type.""" + + try: + return dialect._type_memos[self]['bind'] + except KeyError: + d = self._dialect_info(dialect) + d['bind'] = bp = d['impl'].bind_processor(dialect) + return bp + + def _cached_result_processor(self, dialect, coltype): + """Return a dialect-specific result processor for this type.""" + + try: + return dialect._type_memos[self][coltype] + except KeyError: + d = self._dialect_info(dialect) + # key assumption: DBAPI type codes are + # constants. Else this dictionary would + # grow unbounded. + d[coltype] = rp = d['impl'].result_processor(dialect, coltype) + return rp + + def _dialect_info(self, dialect): + """Return a dialect-specific registry which + caches a dialect-specific implementation, bind processing + function, and one or more result processing functions.""" + + if self in dialect._type_memos: + return dialect._type_memos[self] + else: + impl = self._gen_dialect_impl(dialect) + if impl is self: + impl = self.adapt(type(self)) + # this can't be self, else we create a cycle + assert impl is not self + dialect._type_memos[self] = d = {'impl': impl} + return d + + def _gen_dialect_impl(self, dialect): + return dialect.type_descriptor(self) + + def adapt(self, cls, **kw): + """Produce an "adapted" form of this type, given an "impl" class + to work with. + + This method is used internally to associate generic + types with "implementation" types that are specific to a particular + dialect. + """ + return util.constructor_copy(self, cls, **kw) + + + def coerce_compared_value(self, op, value): + """Suggest a type for a 'coerced' Python value in an expression. + + Given an operator and value, gives the type a chance + to return a type which the value should be coerced into. + + The default behavior here is conservative; if the right-hand + side is already coerced into a SQL type based on its + Python type, it is usually left alone. + + End-user functionality extension here should generally be via + :class:`.TypeDecorator`, which provides more liberal behavior in that + it defaults to coercing the other side of the expression into this + type, thus applying special Python conversions above and beyond those + needed by the DBAPI to both ides. It also provides the public method + :meth:`.TypeDecorator.coerce_compared_value` which is intended for + end-user customization of this behavior. + + """ + _coerced_type = _type_map.get(type(value), NULLTYPE) + if _coerced_type is NULLTYPE or _coerced_type._type_affinity \ + is self._type_affinity: + return self + else: + return _coerced_type + + def _compare_type_affinity(self, other): + return self._type_affinity is other._type_affinity + + def compile(self, dialect=None): + """Produce a string-compiled form of this :class:`.TypeEngine`. + + When called with no arguments, uses a "default" dialect + to produce a string result. + + :param dialect: a :class:`.Dialect` instance. + + """ + # arg, return value is inconsistent with + # ClauseElement.compile()....this is a mistake. + + if not dialect: + dialect = self._default_dialect() + + return dialect.type_compiler.process(self) + + @util.dependencies("sqlalchemy.engine.default") + def _default_dialect(self, default): + if self.__class__.__module__.startswith("sqlalchemy.dialects"): + tokens = self.__class__.__module__.split(".")[0:3] + mod = ".".join(tokens) + return getattr(__import__(mod).dialects, tokens[-1]).dialect() + else: + return default.DefaultDialect() + + def __str__(self): + if util.py2k: + return unicode(self.compile()).\ + encode('ascii', 'backslashreplace') + else: + return str(self.compile()) + + def __repr__(self): + return util.generic_repr(self) + +class UserDefinedType(TypeEngine): + """Base for user defined types. + + This should be the base of new types. Note that + for most cases, :class:`.TypeDecorator` is probably + more appropriate:: + + import sqlalchemy.types as types + + class MyType(types.UserDefinedType): + def __init__(self, precision = 8): + self.precision = precision + + def get_col_spec(self): + return "MYTYPE(%s)" % self.precision + + def bind_processor(self, dialect): + def process(value): + return value + return process + + def result_processor(self, dialect, coltype): + def process(value): + return value + return process + + Once the type is made, it's immediately usable:: + + table = Table('foo', meta, + Column('id', Integer, primary_key=True), + Column('data', MyType(16)) + ) + + """ + __visit_name__ = "user_defined" + + + class Comparator(TypeEngine.Comparator): + def _adapt_expression(self, op, other_comparator): + if hasattr(self.type, 'adapt_operator'): + util.warn_deprecated( + "UserDefinedType.adapt_operator is deprecated. Create " + "a UserDefinedType.Comparator subclass instead which " + "generates the desired expression constructs, given a " + "particular operator." + ) + return self.type.adapt_operator(op), self.type + else: + return op, self.type + + comparator_factory = Comparator + + def coerce_compared_value(self, op, value): + """Suggest a type for a 'coerced' Python value in an expression. + + Default behavior for :class:`.UserDefinedType` is the + same as that of :class:`.TypeDecorator`; by default it returns + ``self``, assuming the compared value should be coerced into + the same type as this one. See + :meth:`.TypeDecorator.coerce_compared_value` for more detail. + + .. versionchanged:: 0.8 :meth:`.UserDefinedType.coerce_compared_value` + now returns ``self`` by default, rather than falling onto the + more fundamental behavior of + :meth:`.TypeEngine.coerce_compared_value`. + + """ + + return self + + +class TypeDecorator(TypeEngine): + """Allows the creation of types which add additional functionality + to an existing type. + + This method is preferred to direct subclassing of SQLAlchemy's + built-in types as it ensures that all required functionality of + the underlying type is kept in place. + + Typical usage:: + + import sqlalchemy.types as types + + class MyType(types.TypeDecorator): + '''Prefixes Unicode values with "PREFIX:" on the way in and + strips it off on the way out. + ''' + + impl = types.Unicode + + def process_bind_param(self, value, dialect): + return "PREFIX:" + value + + def process_result_value(self, value, dialect): + return value[7:] + + def copy(self): + return MyType(self.impl.length) + + The class-level "impl" attribute is required, and can reference any + TypeEngine class. Alternatively, the load_dialect_impl() method + can be used to provide different type classes based on the dialect + given; in this case, the "impl" variable can reference + ``TypeEngine`` as a placeholder. + + Types that receive a Python type that isn't similar to the ultimate type + used may want to define the :meth:`TypeDecorator.coerce_compared_value` + method. This is used to give the expression system a hint when coercing + Python objects into bind parameters within expressions. Consider this + expression:: + + mytable.c.somecol + datetime.date(2009, 5, 15) + + Above, if "somecol" is an ``Integer`` variant, it makes sense that + we're doing date arithmetic, where above is usually interpreted + by databases as adding a number of days to the given date. + The expression system does the right thing by not attempting to + coerce the "date()" value into an integer-oriented bind parameter. + + However, in the case of ``TypeDecorator``, we are usually changing an + incoming Python type to something new - ``TypeDecorator`` by default will + "coerce" the non-typed side to be the same type as itself. Such as below, + we define an "epoch" type that stores a date value as an integer:: + + class MyEpochType(types.TypeDecorator): + impl = types.Integer + + epoch = datetime.date(1970, 1, 1) + + def process_bind_param(self, value, dialect): + return (value - self.epoch).days + + def process_result_value(self, value, dialect): + return self.epoch + timedelta(days=value) + + Our expression of ``somecol + date`` with the above type will coerce the + "date" on the right side to also be treated as ``MyEpochType``. + + This behavior can be overridden via the + :meth:`~TypeDecorator.coerce_compared_value` method, which returns a type + that should be used for the value of the expression. Below we set it such + that an integer value will be treated as an ``Integer``, and any other + value is assumed to be a date and will be treated as a ``MyEpochType``:: + + def coerce_compared_value(self, op, value): + if isinstance(value, int): + return Integer() + else: + return self + + """ + + __visit_name__ = "type_decorator" + + def __init__(self, *args, **kwargs): + """Construct a :class:`.TypeDecorator`. + + Arguments sent here are passed to the constructor + of the class assigned to the ``impl`` class level attribute, + assuming the ``impl`` is a callable, and the resulting + object is assigned to the ``self.impl`` instance attribute + (thus overriding the class attribute of the same name). + + If the class level ``impl`` is not a callable (the unusual case), + it will be assigned to the same instance attribute 'as-is', + ignoring those arguments passed to the constructor. + + Subclasses can override this to customize the generation + of ``self.impl`` entirely. + + """ + + if not hasattr(self.__class__, 'impl'): + raise AssertionError("TypeDecorator implementations " + "require a class-level variable " + "'impl' which refers to the class of " + "type being decorated") + self.impl = to_instance(self.__class__.impl, *args, **kwargs) + + coerce_to_is_types = (util.NoneType, ) + """Specify those Python types which should be coerced at the expression + level to "IS " when compared using ``==`` (and same for + ``IS NOT`` in conjunction with ``!=``. + + For most SQLAlchemy types, this includes ``NoneType``, as well as ``bool``. + + :class:`.TypeDecorator` modifies this list to only include ``NoneType``, + as typedecorator implementations that deal with boolean types are common. + + Custom :class:`.TypeDecorator` classes can override this attribute to + return an empty tuple, in which case no values will be coerced to + constants. + + ..versionadded:: 0.8.2 + Added :attr:`.TypeDecorator.coerce_to_is_types` to allow for easier + control of ``__eq__()`` ``__ne__()`` operations. + + """ + + class Comparator(TypeEngine.Comparator): + + def operate(self, op, *other, **kwargs): + kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types + return super(TypeDecorator.Comparator, self).operate( + op, *other, **kwargs) + + def reverse_operate(self, op, other, **kwargs): + kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types + return super(TypeDecorator.Comparator, self).reverse_operate( + op, other, **kwargs) + + @property + def comparator_factory(self): + return type("TDComparator", + (TypeDecorator.Comparator, self.impl.comparator_factory), + {}) + + def _gen_dialect_impl(self, dialect): + """ + #todo + """ + adapted = dialect.type_descriptor(self) + if adapted is not self: + return adapted + + # otherwise adapt the impl type, link + # to a copy of this TypeDecorator and return + # that. + typedesc = self.load_dialect_impl(dialect).dialect_impl(dialect) + tt = self.copy() + if not isinstance(tt, self.__class__): + raise AssertionError('Type object %s does not properly ' + 'implement the copy() method, it must ' + 'return an object of type %s' % (self, + self.__class__)) + tt.impl = typedesc + return tt + + @property + def _type_affinity(self): + """ + #todo + """ + return self.impl._type_affinity + + def type_engine(self, dialect): + """Return a dialect-specific :class:`.TypeEngine` instance + for this :class:`.TypeDecorator`. + + In most cases this returns a dialect-adapted form of + the :class:`.TypeEngine` type represented by ``self.impl``. + Makes usage of :meth:`dialect_impl` but also traverses + into wrapped :class:`.TypeDecorator` instances. + Behavior can be customized here by overriding + :meth:`load_dialect_impl`. + + """ + adapted = dialect.type_descriptor(self) + if type(adapted) is not type(self): + return adapted + elif isinstance(self.impl, TypeDecorator): + return self.impl.type_engine(dialect) + else: + return self.load_dialect_impl(dialect) + + def load_dialect_impl(self, dialect): + """Return a :class:`.TypeEngine` object corresponding to a dialect. + + This is an end-user override hook that can be used to provide + differing types depending on the given dialect. It is used + by the :class:`.TypeDecorator` implementation of :meth:`type_engine` + to help determine what type should ultimately be returned + for a given :class:`.TypeDecorator`. + + By default returns ``self.impl``. + + """ + return self.impl + + def __getattr__(self, key): + """Proxy all other undefined accessors to the underlying + implementation.""" + return getattr(self.impl, key) + + def process_literal_param(self, value, dialect): + """Receive a literal parameter value to be rendered inline within + a statement. + + This method is used when the compiler renders a + literal value without using binds, typically within DDL + such as in the "server default" of a column or an expression + within a CHECK constraint. + + The returned string will be rendered into the output string. + + .. versionadded:: 0.9.0 + + """ + raise NotImplementedError() + + def process_bind_param(self, value, dialect): + """Receive a bound parameter value to be converted. + + Subclasses override this method to return the + value that should be passed along to the underlying + :class:`.TypeEngine` object, and from there to the + DBAPI ``execute()`` method. + + The operation could be anything desired to perform custom + behavior, such as transforming or serializing data. + This could also be used as a hook for validating logic. + + This operation should be designed with the reverse operation + in mind, which would be the process_result_value method of + this class. + + :param value: Data to operate upon, of any type expected by + this method in the subclass. Can be ``None``. + :param dialect: the :class:`.Dialect` in use. + + """ + + raise NotImplementedError() + + def process_result_value(self, value, dialect): + """Receive a result-row column value to be converted. + + Subclasses should implement this method to operate on data + fetched from the database. + + Subclasses override this method to return the + value that should be passed back to the application, + given a value that is already processed by + the underlying :class:`.TypeEngine` object, originally + from the DBAPI cursor method ``fetchone()`` or similar. + + The operation could be anything desired to perform custom + behavior, such as transforming or serializing data. + This could also be used as a hook for validating logic. + + :param value: Data to operate upon, of any type expected by + this method in the subclass. Can be ``None``. + :param dialect: the :class:`.Dialect` in use. + + This operation should be designed to be reversible by + the "process_bind_param" method of this class. + + """ + + raise NotImplementedError() + + @util.memoized_property + def _has_bind_processor(self): + """memoized boolean, check if process_bind_param is implemented. + + Allows the base process_bind_param to raise + NotImplementedError without needing to test an expensive + exception throw. + + """ + + return self.__class__.process_bind_param.__code__ \ + is not TypeDecorator.process_bind_param.__code__ + + @util.memoized_property + def _has_literal_processor(self): + """memoized boolean, check if process_literal_param is implemented. + + + """ + + return self.__class__.process_literal_param.__code__ \ + is not TypeDecorator.process_literal_param.__code__ + + def literal_processor(self, dialect): + """Provide a literal processing function for the given + :class:`.Dialect`. + + Subclasses here will typically override :meth:`.TypeDecorator.process_literal_param` + instead of this method directly. + + By default, this method makes use of :meth:`.TypeDecorator.process_bind_param` + if that method is implemented, where :meth:`.TypeDecorator.process_literal_param` + is not. The rationale here is that :class:`.TypeDecorator` typically deals + with Python conversions of data that are above the layer of database + presentation. With the value converted by :meth:`.TypeDecorator.process_bind_param`, + the underlying type will then handle whether it needs to be presented to the + DBAPI as a bound parameter or to the database as an inline SQL value. + + .. versionadded:: 0.9.0 + + """ + if self._has_literal_processor: + process_param = self.process_literal_param + elif self._has_bind_processor: + # the bind processor should normally be OK + # for TypeDecorator since it isn't doing DB-level + # handling, the handling here won't be different for bound vs. + # literals. + process_param = self.process_bind_param + else: + process_param = None + + if process_param: + impl_processor = self.impl.literal_processor(dialect) + if impl_processor: + def process(value): + return impl_processor(process_param(value, dialect)) + else: + def process(value): + return process_param(value, dialect) + + return process + else: + return self.impl.literal_processor(dialect) + + def bind_processor(self, dialect): + """Provide a bound value processing function for the + given :class:`.Dialect`. + + This is the method that fulfills the :class:`.TypeEngine` + contract for bound value conversion. :class:`.TypeDecorator` + will wrap a user-defined implementation of + :meth:`process_bind_param` here. + + User-defined code can override this method directly, + though its likely best to use :meth:`process_bind_param` so that + the processing provided by ``self.impl`` is maintained. + + :param dialect: Dialect instance in use. + + This method is the reverse counterpart to the + :meth:`result_processor` method of this class. + + """ + if self._has_bind_processor: + process_param = self.process_bind_param + impl_processor = self.impl.bind_processor(dialect) + if impl_processor: + def process(value): + return impl_processor(process_param(value, dialect)) + + else: + def process(value): + return process_param(value, dialect) + + return process + else: + return self.impl.bind_processor(dialect) + + @util.memoized_property + def _has_result_processor(self): + """memoized boolean, check if process_result_value is implemented. + + Allows the base process_result_value to raise + NotImplementedError without needing to test an expensive + exception throw. + + """ + return self.__class__.process_result_value.__code__ \ + is not TypeDecorator.process_result_value.__code__ + + def result_processor(self, dialect, coltype): + """Provide a result value processing function for the given + :class:`.Dialect`. + + This is the method that fulfills the :class:`.TypeEngine` + contract for result value conversion. :class:`.TypeDecorator` + will wrap a user-defined implementation of + :meth:`process_result_value` here. + + User-defined code can override this method directly, + though its likely best to use :meth:`process_result_value` so that + the processing provided by ``self.impl`` is maintained. + + :param dialect: Dialect instance in use. + :param coltype: An SQLAlchemy data type + + This method is the reverse counterpart to the + :meth:`bind_processor` method of this class. + + """ + if self._has_result_processor: + process_value = self.process_result_value + impl_processor = self.impl.result_processor(dialect, + coltype) + if impl_processor: + def process(value): + return process_value(impl_processor(value), dialect) + + else: + def process(value): + return process_value(value, dialect) + + return process + else: + return self.impl.result_processor(dialect, coltype) + + def coerce_compared_value(self, op, value): + """Suggest a type for a 'coerced' Python value in an expression. + + By default, returns self. This method is called by + the expression system when an object using this type is + on the left or right side of an expression against a plain Python + object which does not yet have a SQLAlchemy type assigned:: + + expr = table.c.somecolumn + 35 + + Where above, if ``somecolumn`` uses this type, this method will + be called with the value ``operator.add`` + and ``35``. The return value is whatever SQLAlchemy type should + be used for ``35`` for this particular operation. + + """ + return self + + def copy(self): + """Produce a copy of this :class:`.TypeDecorator` instance. + + This is a shallow copy and is provided to fulfill part of + the :class:`.TypeEngine` contract. It usually does not + need to be overridden unless the user-defined :class:`.TypeDecorator` + has local state that should be deep-copied. + + """ + + instance = self.__class__.__new__(self.__class__) + instance.__dict__.update(self.__dict__) + return instance + + def get_dbapi_type(self, dbapi): + """Return the DBAPI type object represented by this + :class:`.TypeDecorator`. + + By default this calls upon :meth:`.TypeEngine.get_dbapi_type` of the + underlying "impl". + """ + return self.impl.get_dbapi_type(dbapi) + + def compare_values(self, x, y): + """Given two values, compare them for equality. + + By default this calls upon :meth:`.TypeEngine.compare_values` + of the underlying "impl", which in turn usually + uses the Python equals operator ``==``. + + This function is used by the ORM to compare + an original-loaded value with an intercepted + "changed" value, to determine if a net change + has occurred. + + """ + return self.impl.compare_values(x, y) + + def __repr__(self): + return util.generic_repr(self, to_inspect=self.impl) + + +class Variant(TypeDecorator): + """A wrapping type that selects among a variety of + implementations based on dialect in use. + + The :class:`.Variant` type is typically constructed + using the :meth:`.TypeEngine.with_variant` method. + + .. versionadded:: 0.7.2 + + .. seealso:: :meth:`.TypeEngine.with_variant` for an example of use. + + """ + + def __init__(self, base, mapping): + """Construct a new :class:`.Variant`. + + :param base: the base 'fallback' type + :param mapping: dictionary of string dialect names to + :class:`.TypeEngine` instances. + + """ + self.impl = base + self.mapping = mapping + + def load_dialect_impl(self, dialect): + if dialect.name in self.mapping: + return self.mapping[dialect.name] + else: + return self.impl + + def with_variant(self, type_, dialect_name): + """Return a new :class:`.Variant` which adds the given + type + dialect name to the mapping, in addition to the + mapping present in this :class:`.Variant`. + + :param type_: a :class:`.TypeEngine` that will be selected + as a variant from the originating type, when a dialect + of the given name is in use. + :param dialect_name: base name of the dialect which uses + this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) + + """ + + if dialect_name in self.mapping: + raise exc.ArgumentError( + "Dialect '%s' is already present in " + "the mapping for this Variant" % dialect_name) + mapping = self.mapping.copy() + mapping[dialect_name] = type_ + return Variant(self.impl, mapping) + +def _reconstitute_comparator(expression): + return expression.comparator + + +def to_instance(typeobj, *arg, **kw): + if typeobj is None: + return NULLTYPE + + if util.callable(typeobj): + return typeobj(*arg, **kw) + else: + return typeobj + + +def adapt_type(typeobj, colspecs): + if isinstance(typeobj, type): + typeobj = typeobj() + for t in typeobj.__class__.__mro__[0:-1]: + try: + impltype = colspecs[t] + break + except KeyError: + pass + else: + # couldnt adapt - so just return the type itself + # (it may be a user-defined type) + return typeobj + # if we adapted the given generic type to a database-specific type, + # but it turns out the originally given "generic" type + # is actually a subclass of our resulting type, then we were already + # given a more specific type than that required; so use that. + if (issubclass(typeobj.__class__, impltype)): + return typeobj + return typeobj.adapt(impltype) + + diff --git a/libs/sqlalchemy/sql/util.py b/libs/sqlalchemy/sql/util.py index 0a00674c..50ce30aa 100644 --- a/libs/sqlalchemy/sql/util.py +++ b/libs/sqlalchemy/sql/util.py @@ -1,41 +1,33 @@ # sql/util.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import exc, schema, util, sql, types as sqltypes -from sqlalchemy.util import topological -from sqlalchemy.sql import expression, operators, visitors +"""High level utilities which build upon other modules here. + +""" + +from .. import exc, util +from .base import _from_objects, ColumnSet +from . import operators, visitors from itertools import chain from collections import deque -"""Utility functions that build upon SQL and Schema constructs.""" +from .elements import BindParameter, ColumnClause, ColumnElement, \ + Null, UnaryExpression, literal_column, Label +from .selectable import ScalarSelect, Join, FromClause, FromGrouping +from .schema import Column -def sort_tables(tables): - """sort a collection of Table objects in order of their foreign-key dependency.""" +join_condition = util.langhelpers.public_factory( + Join._join_condition, + ".sql.util.join_condition") - tables = list(tables) - tuples = [] - def visit_foreign_key(fkey): - if fkey.use_alter: - return - parent_table = fkey.column.table - if parent_table in tables: - child_table = fkey.parent.table - if parent_table is not child_table: - tuples.append((parent_table, child_table)) +# names that are still being imported from the outside +from .annotation import _shallow_annotate, _deep_annotate, _deep_deannotate +from .elements import _find_columns +from .ddl import sort_tables - for table in tables: - visitors.traverse(table, - {'schema_visitor':True}, - {'foreign_key':visit_foreign_key}) - - tuples.extend( - [parent, table] for parent in table._extra_dependencies - ) - - return list(topological.sort(tuples, tables)) def find_join_source(clauses, join_to): """Given a list of FROM clauses and a selectable, @@ -54,7 +46,7 @@ def find_join_source(clauses, join_to): """ - selectables = list(expression._from_objects(join_to)) + selectables = list(_from_objects(join_to)) for i, f in enumerate(clauses): for s in selectables: if f.is_derived_from(s): @@ -62,6 +54,67 @@ def find_join_source(clauses, join_to): else: return None, None + +def visit_binary_product(fn, expr): + """Produce a traversal of the given expression, delivering + column comparisons to the given function. + + The function is of the form:: + + def my_fn(binary, left, right) + + For each binary expression located which has a + comparison operator, the product of "left" and + "right" will be delivered to that function, + in terms of that binary. + + Hence an expression like:: + + and_( + (a + b) == q + func.sum(e + f), + j == r + ) + + would have the traversal:: + + a q + a e + a f + b q + b e + b f + j r + + That is, every combination of "left" and + "right" that doesn't further contain + a binary comparison is passed as pairs. + + """ + stack = [] + + def visit(element): + if isinstance(element, ScalarSelect): + # we dont want to dig into correlated subqueries, + # those are just column elements by themselves + yield element + elif element.__visit_name__ == 'binary' and \ + operators.is_comparison(element.operator): + stack.insert(0, element) + for l in visit(element.left): + for r in visit(element.right): + fn(stack[0], l, r) + stack.pop(0) + for elem in element.get_children(): + visit(elem) + else: + if isinstance(element, ColumnClause): + yield element + for elem in element.get_children(): + for e in visit(elem): + yield e + list(visit(expr)) + + def find_tables(clause, check_columns=False, include_aliases=False, include_joins=False, include_selects=False, include_crud=False): @@ -77,7 +130,7 @@ def find_tables(clause, check_columns=False, _visitors['join'] = tables.append if include_aliases: - _visitors['alias'] = tables.append + _visitors['alias'] = tables.append if include_crud: _visitors['insert'] = _visitors['update'] = \ @@ -90,15 +143,10 @@ def find_tables(clause, check_columns=False, _visitors['table'] = tables.append - visitors.traverse(clause, {'column_collections':False}, _visitors) + visitors.traverse(clause, {'column_collections': False}, _visitors) return tables -def find_columns(clause): - """locate Column objects within the given expression.""" - cols = util.column_set() - visitors.traverse(clause, {}, {'column':cols.add}) - return cols def unwrap_order_by(clause): """Break up an 'order by' expression into individual column-expressions, @@ -108,9 +156,9 @@ def unwrap_order_by(clause): stack = deque([clause]) while stack: t = stack.popleft() - if isinstance(t, expression.ColumnElement) and \ + if isinstance(t, ColumnElement) and \ ( - not isinstance(t, expression._UnaryExpression) or \ + not isinstance(t, UnaryExpression) or \ not operators.is_ordering_modifier(t.modifier) ): cols.add(t) @@ -119,6 +167,7 @@ def unwrap_order_by(clause): stack.append(c) return cols + def clause_is_present(clause, search): """Given a target clause and a second to search within, return True if the target is plainly present in the search without any @@ -128,15 +177,30 @@ def clause_is_present(clause, search): """ - stack = [search] + for elem in surface_selectables(search): + if clause == elem: # use == here so that Annotated's compare + return True + else: + return False + +def surface_selectables(clause): + stack = [clause] while stack: elem = stack.pop() - if clause is elem: - return True - elif isinstance(elem, expression.Join): + yield elem + if isinstance(elem, Join): stack.extend((elem.left, elem.right)) - return False + elif isinstance(elem, FromGrouping): + stack.append(elem.element) +def selectables_overlap(left, right): + """Return True if left/right have some overlapping selectable""" + + return bool( + set(surface_selectables(left)).intersection( + surface_selectables(right) + ) + ) def bind_values(clause): """Return an ordered list of "bound" values in the given clause. @@ -151,19 +215,22 @@ def bind_values(clause): """ v = [] + def visit_bindparam(bind): v.append(bind.effective_value) - visitors.traverse(clause, {}, {'bindparam':visit_bindparam}) + visitors.traverse(clause, {}, {'bindparam': visit_bindparam}) return v + def _quote_ddl_expr(element): - if isinstance(element, basestring): + if isinstance(element, util.string_types): element = element.replace("'", "''") return "'%s'" % element else: return repr(element) + class _repr_params(object): """A string view of bound parameters, truncating display to the given number of 'multi' parameter sets. @@ -177,289 +244,40 @@ class _repr_params(object): if isinstance(self.params, (list, tuple)) and \ len(self.params) > self.batches and \ isinstance(self.params[0], (list, dict, tuple)): + msg = " ... displaying %i of %i total bound parameter sets ... " return ' '.join(( repr(self.params[:self.batches - 2])[0:-1], - " ... displaying %i of %i total bound parameter sets ... " % (self.batches, len(self.params)), + msg % (self.batches, len(self.params)), repr(self.params[-2:])[1:] )) else: return repr(self.params) -def expression_as_ddl(clause): - """Given a SQL expression, convert for usage in DDL, such as - CREATE INDEX and CHECK CONSTRAINT. - Converts bind params into quoted literals, column identifiers - into detached column constructs so that the parent table - identifier is not included. - - """ - def repl(element): - if isinstance(element, expression._BindParamClause): - return expression.literal_column(_quote_ddl_expr(element.value)) - elif isinstance(element, expression.ColumnClause) and \ - element.table is not None: - return expression.column(element.name) - else: - return None - - return visitors.replacement_traverse(clause, {}, repl) def adapt_criterion_to_null(crit, nulls): - """given criterion containing bind params, convert selected elements to IS NULL.""" + """given criterion containing bind params, convert selected elements + to IS NULL. + + """ def visit_binary(binary): - if isinstance(binary.left, expression._BindParamClause) \ + if isinstance(binary.left, BindParameter) \ and binary.left._identifying_key in nulls: # reverse order if the NULL is on the left side binary.left = binary.right - binary.right = expression.null() + binary.right = Null() binary.operator = operators.is_ binary.negate = operators.isnot - elif isinstance(binary.right, expression._BindParamClause) \ + elif isinstance(binary.right, BindParameter) \ and binary.right._identifying_key in nulls: - binary.right = expression.null() + binary.right = Null() binary.operator = operators.is_ binary.negate = operators.isnot - return visitors.cloned_traverse(crit, {}, {'binary':visit_binary}) + return visitors.cloned_traverse(crit, {}, {'binary': visit_binary}) -def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None): - """create a join condition between two tables or selectables. - - e.g.:: - - join_condition(tablea, tableb) - - would produce an expression along the lines of:: - - tablea.c.id==tableb.c.tablea_id - - The join is determined based on the foreign key relationships - between the two selectables. If there are multiple ways - to join, or no way to join, an error is raised. - - :param ignore_nonexistent_tables: Deprecated - this - flag is no longer used. Only resolution errors regarding - the two given tables are propagated. - - :param a_subset: An optional expression that is a sub-component - of ``a``. An attempt will be made to join to just this sub-component - first before looking at the full ``a`` construct, and if found - will be successful even if there are other ways to join to ``a``. - This allows the "right side" of a join to be passed thereby - providing a "natural join". - - """ - crit = [] - constraints = set() - - for left in (a_subset, a): - if left is None: - continue - for fk in sorted( - b.foreign_keys, - key=lambda fk:fk.parent._creation_order): - try: - col = fk.get_referent(left) - except exc.NoReferenceError, nrte: - if nrte.table_name == left.name: - raise - else: - continue - - if col is not None: - crit.append(col == fk.parent) - constraints.add(fk.constraint) - if left is not b: - for fk in sorted( - left.foreign_keys, - key=lambda fk:fk.parent._creation_order): - try: - col = fk.get_referent(b) - except exc.NoReferenceError, nrte: - if nrte.table_name == b.name: - raise - else: - # this is totally covered. can't get - # coverage to mark it. - continue - - if col is not None: - crit.append(col == fk.parent) - constraints.add(fk.constraint) - if crit: - break - - if len(crit) == 0: - if isinstance(b, expression._FromGrouping): - hint = " Perhaps you meant to convert the right side to a "\ - "subquery using alias()?" - else: - hint = "" - raise exc.ArgumentError( - "Can't find any foreign key relationships " - "between '%s' and '%s'.%s" % (a.description, b.description, hint)) - elif len(constraints) > 1: - raise exc.ArgumentError( - "Can't determine join between '%s' and '%s'; " - "tables have more than one foreign key " - "constraint relationship between them. " - "Please specify the 'onclause' of this " - "join explicitly." % (a.description, b.description)) - elif len(crit) == 1: - return (crit[0]) - else: - return sql.and_(*crit) - - -class Annotated(object): - """clones a ClauseElement and applies an 'annotations' dictionary. - - Unlike regular clones, this clone also mimics __hash__() and - __cmp__() of the original element so that it takes its place - in hashed collections. - - A reference to the original element is maintained, for the important - reason of keeping its hash value current. When GC'ed, the - hash value may be reused, causing conflicts. - - """ - - def __new__(cls, *args): - if not args: - # clone constructor - return object.__new__(cls) - else: - element, values = args - # pull appropriate subclass from registry of annotated - # classes - try: - cls = annotated_classes[element.__class__] - except KeyError: - cls = annotated_classes[element.__class__] = type.__new__(type, - "Annotated%s" % element.__class__.__name__, - (Annotated, element.__class__), {}) - return object.__new__(cls) - - def __init__(self, element, values): - # force FromClause to generate their internal - # collections into __dict__ - if isinstance(element, expression.FromClause): - element.c - - self.__dict__ = element.__dict__.copy() - self.__element = element - self._annotations = values - - def _annotate(self, values): - _values = self._annotations.copy() - _values.update(values) - clone = self.__class__.__new__(self.__class__) - clone.__dict__ = self.__dict__.copy() - clone._annotations = _values - return clone - - def _deannotate(self): - return self.__element - - def _compiler_dispatch(self, visitor, **kw): - return self.__element.__class__._compiler_dispatch(self, visitor, **kw) - - @property - def _constructor(self): - return self.__element._constructor - - def _clone(self): - clone = self.__element._clone() - if clone is self.__element: - # detect immutable, don't change anything - return self - else: - # update the clone with any changes that have occurred - # to this object's __dict__. - clone.__dict__.update(self.__dict__) - return Annotated(clone, self._annotations) - - def __hash__(self): - return hash(self.__element) - - def __eq__(self, other): - if isinstance(self.__element, expression.ColumnOperators): - return self.__element.__class__.__eq__(self, other) - else: - return hash(other) == hash(self) - - -# hard-generate Annotated subclasses. this technique -# is used instead of on-the-fly types (i.e. type.__new__()) -# so that the resulting objects are pickleable. -annotated_classes = {} - -for cls in expression.__dict__.values() + [schema.Column, schema.Table]: - if isinstance(cls, type) and issubclass(cls, expression.ClauseElement): - exec "class Annotated%s(Annotated, cls):\n" \ - " pass" % (cls.__name__, ) in locals() - exec "annotated_classes[cls] = Annotated%s" % (cls.__name__) - -def _deep_annotate(element, annotations, exclude=None): - """Deep copy the given ClauseElement, annotating each element - with the given annotations dictionary. - - Elements within the exclude collection will be cloned but not annotated. - - """ - cloned = util.column_dict() - - def clone(elem): - # check if element is present in the exclude list. - # take into account proxying relationships. - if elem in cloned: - return cloned[elem] - elif exclude and \ - hasattr(elem, 'proxy_set') and \ - elem.proxy_set.intersection(exclude): - newelem = elem._clone() - elif annotations != elem._annotations: - newelem = elem._annotate(annotations) - else: - newelem = elem - newelem._copy_internals(clone=clone) - cloned[elem] = newelem - return newelem - - if element is not None: - element = clone(element) - return element - -def _deep_deannotate(element): - """Deep copy the given element, removing all annotations.""" - - cloned = util.column_dict() - - def clone(elem): - if elem not in cloned: - newelem = elem._deannotate() - newelem._copy_internals(clone=clone) - cloned[elem] = newelem - return cloned[elem] - - if element is not None: - element = clone(element) - return element - -def _shallow_annotate(element, annotations): - """Annotate the given ClauseElement and copy its internals so that - internal objects refer to the new annotated object. - - Basically used to apply a "dont traverse" annotation to a - selectable, without digging throughout the whole - structure wasting time. - """ - element = element._annotate(annotations) - element._copy_internals() - return element def splice_joins(left, right, stop_on=None): if left is None: @@ -471,7 +289,7 @@ def splice_joins(left, right, stop_on=None): ret = None while stack: (right, prevright) = stack.pop() - if isinstance(right, expression.Join) and right is not stop_on: + if isinstance(right, Join) and right is not stop_on: right = right._clone() right._reset_exported() right.onclause = adapter.traverse(right.onclause) @@ -485,18 +303,21 @@ def splice_joins(left, right, stop_on=None): return ret + def reduce_columns(columns, *clauses, **kw): - """given a list of columns, return a 'reduced' set based on natural equivalents. + """given a list of columns, return a 'reduced' set based on natural + equivalents. the set is reduced to the smallest list of columns which have no natural - equivalent present in the list. A "natural equivalent" means that two columns - will ultimately represent the same value because they are related by a foreign key. + equivalent present in the list. A "natural equivalent" means that two + columns will ultimately represent the same value because they are related + by a foreign key. \*clauses is an optional list of join clauses which will be traversed to further identify columns that are "equivalent". \**kw may specify 'ignore_nonexistent_tables' to ignore foreign keys - whose tables are not yet configured. + whose tables are not yet configured, or columns that aren't yet present. This function is primarily used to determine the most minimal "primary key" from a selectable, by reducing the set of primary key columns present @@ -504,6 +325,7 @@ def reduce_columns(columns, *clauses, **kw): """ ignore_nonexistent_tables = kw.pop('ignore_nonexistent_tables', False) + only_synonyms = kw.pop('only_synonyms', False) columns = util.ordered_column_set(columns) @@ -515,28 +337,44 @@ def reduce_columns(columns, *clauses, **kw): continue try: fk_col = fk.column - except exc.NoReferencedTableError: + except exc.NoReferencedColumnError: + # TODO: add specific coverage here + # to test/sql/test_selectable ReduceTest if ignore_nonexistent_tables: continue else: raise - if fk_col.shares_lineage(c): + except exc.NoReferencedTableError: + # TODO: add specific coverage here + # to test/sql/test_selectable ReduceTest + if ignore_nonexistent_tables: + continue + else: + raise + if fk_col.shares_lineage(c) and \ + (not only_synonyms or \ + c.name == col.name): omit.add(col) break if clauses: def visit_binary(binary): if binary.operator == operators.eq: - cols = util.column_set(chain(*[c.proxy_set for c in columns.difference(omit)])) + cols = util.column_set(chain(*[c.proxy_set + for c in columns.difference(omit)])) if binary.left in cols and binary.right in cols: - for c in columns: - if c.shares_lineage(binary.right): + for c in reversed(columns): + if c.shares_lineage(binary.right) and \ + (not only_synonyms or \ + c.name == binary.left.name): omit.add(c) break for clause in clauses: - visitors.traverse(clause, {}, {'binary':visit_binary}) + if clause is not None: + visitors.traverse(clause, {}, {'binary': visit_binary}) + + return ColumnSet(columns.difference(omit)) - return expression.ColumnSet(columns.difference(omit)) def criterion_as_pairs(expression, consider_as_foreign_keys=None, consider_as_referenced_keys=None, any_operator=False): @@ -547,84 +385,47 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, "'consider_as_foreign_keys' or " "'consider_as_referenced_keys'") + def col_is(a, b): + #return a is b + return a.compare(b) + def visit_binary(binary): if not any_operator and binary.operator is not operators.eq: return - if not isinstance(binary.left, sql.ColumnElement) or \ - not isinstance(binary.right, sql.ColumnElement): + if not isinstance(binary.left, ColumnElement) or \ + not isinstance(binary.right, ColumnElement): return if consider_as_foreign_keys: if binary.left in consider_as_foreign_keys and \ - (binary.right is binary.left or + (col_is(binary.right, binary.left) or binary.right not in consider_as_foreign_keys): pairs.append((binary.right, binary.left)) elif binary.right in consider_as_foreign_keys and \ - (binary.left is binary.right or + (col_is(binary.left, binary.right) or binary.left not in consider_as_foreign_keys): pairs.append((binary.left, binary.right)) elif consider_as_referenced_keys: if binary.left in consider_as_referenced_keys and \ - (binary.right is binary.left or + (col_is(binary.right, binary.left) or binary.right not in consider_as_referenced_keys): pairs.append((binary.left, binary.right)) elif binary.right in consider_as_referenced_keys and \ - (binary.left is binary.right or + (col_is(binary.left, binary.right) or binary.left not in consider_as_referenced_keys): pairs.append((binary.right, binary.left)) else: - if isinstance(binary.left, schema.Column) and \ - isinstance(binary.right, schema.Column): + if isinstance(binary.left, Column) and \ + isinstance(binary.right, Column): if binary.left.references(binary.right): pairs.append((binary.right, binary.left)) elif binary.right.references(binary.left): pairs.append((binary.left, binary.right)) pairs = [] - visitors.traverse(expression, {}, {'binary':visit_binary}) + visitors.traverse(expression, {}, {'binary': visit_binary}) return pairs -def folded_equivalents(join, equivs=None): - """Return a list of uniquely named columns. - The column list of the given Join will be narrowed - down to a list of all equivalently-named, - equated columns folded into one column, where 'equated' means they are - equated to each other in the ON clause of this join. - - This function is used by Join.select(fold_equivalents=True). - - Deprecated. This function is used for a certain kind of - "polymorphic_union" which is designed to achieve joined - table inheritance where the base table has no "discriminator" - column; [ticket:1131] will provide a better way to - achieve this. - - """ - if equivs is None: - equivs = set() - def visit_binary(binary): - if binary.operator == operators.eq and binary.left.name == binary.right.name: - equivs.add(binary.right) - equivs.add(binary.left) - visitors.traverse(join.onclause, {}, {'binary':visit_binary}) - collist = [] - if isinstance(join.left, expression.Join): - left = folded_equivalents(join.left, equivs) - else: - left = list(join.left.columns) - if isinstance(join.right, expression.Join): - right = folded_equivalents(join.right, equivs) - else: - right = list(join.right.columns) - used = set() - for c in left + right: - if c in equivs: - if c.name not in used: - collist.append(c) - used.add(c.name) - else: - collist.append(c) - return collist class AliasedRow(object): """Wrap a RowProxy with a translation map. @@ -681,15 +482,27 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): s.c.col1 == table2.c.col1 """ - def __init__(self, selectable, equivalents=None, include=None, exclude=None, adapt_on_names=False): - self.__traverse_options__ = {'stop_on':[selectable]} + def __init__(self, selectable, equivalents=None, + include=None, exclude=None, + include_fn=None, exclude_fn=None, + adapt_on_names=False): + self.__traverse_options__ = {'stop_on': [selectable]} self.selectable = selectable - self.include = include - self.exclude = exclude + if include: + assert not include_fn + self.include_fn = lambda e: e in include + else: + self.include_fn = include_fn + if exclude: + assert not exclude_fn + self.exclude_fn = lambda e: e in exclude + else: + self.exclude_fn = exclude_fn self.equivalents = util.column_dict(equivalents or {}) self.adapt_on_names = adapt_on_names - def _corresponding_column(self, col, require_embedded, _seen=util.EMPTY_SET): + def _corresponding_column(self, col, require_embedded, + _seen=util.EMPTY_SET): newcol = self.selectable.corresponding_column( col, require_embedded=require_embedded) @@ -704,20 +517,20 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): newcol = self.selectable.c.get(col.name) return newcol + magic_flag = False def replace(self, col): - if isinstance(col, expression.FromClause): - if self.selectable.is_derived_from(col): - return self.selectable - - if not isinstance(col, expression.ColumnElement): + if not self.magic_flag and isinstance(col, FromClause) and \ + self.selectable.is_derived_from(col): + return self.selectable + elif not isinstance(col, ColumnElement): return None - - if self.include and col not in self.include: + elif self.include_fn and not self.include_fn(col): return None - elif self.exclude and col in self.exclude: + elif self.exclude_fn and self.exclude_fn(col): return None + else: + return self._corresponding_column(col, True) - return self._corresponding_column(col, True) class ColumnAdapter(ClauseAdapter): """Extends ClauseAdapter with extra utility functions. @@ -761,15 +574,14 @@ class ColumnAdapter(ClauseAdapter): c = self.adapt_clause(col) # anonymize labels in case they have a hardcoded name - if isinstance(c, expression._Label): + if isinstance(c, Label): c = c.label(None) - # adapt_required indicates that if we got the same column - # back which we put in (i.e. it passed through), - # it's not correct. this is used by eagerloading which - # knows that all columns and expressions need to be adapted - # to a result row, and a "passthrough" is definitely targeting - # the wrong column. + # adapt_required used by eager loading to indicate that + # we don't trust a result row column that is not translated. + # this is to prevent a column from being interpreted as that + # of the child row in a self-referential scenario, see + # inheritance/test_basic.py->EagerTargetingTest.test_adapt_stringency if self.adapt_required and c is col: return None @@ -786,3 +598,4 @@ class ColumnAdapter(ClauseAdapter): def __setstate__(self, state): self.__dict__.update(state) self.columns = util.PopulateDict(self._locate_col) + diff --git a/libs/sqlalchemy/sql/visitors.py b/libs/sqlalchemy/sql/visitors.py index d236063d..d9ad04fc 100644 --- a/libs/sqlalchemy/sql/visitors.py +++ b/libs/sqlalchemy/sql/visitors.py @@ -1,5 +1,5 @@ # sql/visitors.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -24,15 +24,17 @@ http://techspot.zzzeek.org/2008/01/23/expression-transformations/ """ from collections import deque -import re -from sqlalchemy import util +from .. import util import operator +from .. import exc __all__ = ['VisitableType', 'Visitable', 'ClauseVisitor', 'CloningVisitor', 'ReplacingCloningVisitor', 'iterate', 'iterate_depthfirst', 'traverse_using', 'traverse', + 'traverse_depthfirst', 'cloned_traverse', 'replacement_traverse'] + class VisitableType(type): """Metaclass which assigns a `_compiler_dispatch` method to classes having a `__visit_name__` attribute. @@ -43,16 +45,15 @@ class VisitableType(type): def _compiler_dispatch (self, visitor, **kw): '''Look for an attribute named "visit_" + self.__visit_name__ on the visitor, and call it with the same kw params.''' - return getattr(visitor, 'visit_%s' % self.__visit_name__)(self, **kw) + visit_attr = 'visit_%s' % self.__visit_name__ + return getattr(visitor, visit_attr)(self, **kw) Classes having no __visit_name__ attribute will remain unaffected. """ def __init__(cls, clsname, bases, clsdict): - if cls.__name__ == 'Visitable' or not hasattr(cls, '__visit_name__'): - super(VisitableType, cls).__init__(clsname, bases, clsdict) - return - - _generate_dispatch(cls) + if clsname != 'Visitable' and \ + hasattr(cls, '__visit_name__'): + _generate_dispatch(cls) super(VisitableType, cls).__init__(clsname, bases, clsdict) @@ -68,28 +69,40 @@ def _generate_dispatch(cls): # the string name of the class's __visit_name__ is known at # this early stage (import time) so it can be pre-constructed. getter = operator.attrgetter("visit_%s" % visit_name) + def _compiler_dispatch(self, visitor, **kw): - return getter(visitor)(self, **kw) + try: + meth = getter(visitor) + except AttributeError: + raise exc.UnsupportedCompilationError(visitor, cls) + else: + return meth(self, **kw) else: # The optimization opportunity is lost for this case because the # __visit_name__ is not yet a string. As a result, the visit # string has to be recalculated with each compilation. def _compiler_dispatch(self, visitor, **kw): - return getattr(visitor, 'visit_%s' % self.__visit_name__)(self, **kw) + visit_attr = 'visit_%s' % self.__visit_name__ + try: + meth = getattr(visitor, visit_attr) + except AttributeError: + raise exc.UnsupportedCompilationError(visitor, cls) + else: + return meth(self, **kw) - _compiler_dispatch.__doc__ = \ + _compiler_dispatch.__doc__ = \ """Look for an attribute named "visit_" + self.__visit_name__ on the visitor, and call it with the same kw params. """ cls._compiler_dispatch = _compiler_dispatch -class Visitable(object): + +class Visitable(util.with_metaclass(VisitableType, object)): """Base class for visitable objects, applies the ``VisitableType`` metaclass. """ - __metaclass__ = VisitableType class ClauseVisitor(object): """Base class for visitor objects which can traverse using @@ -106,8 +119,10 @@ class ClauseVisitor(object): return meth(obj, **kw) def iterate(self, obj): - """traverse the given expression structure, returning an iterator of all elements.""" + """traverse the given expression structure, returning an iterator + of all elements. + """ return iterate(obj, self.__traverse_options__) def traverse(self, obj): @@ -143,6 +158,7 @@ class ClauseVisitor(object): tail._next = visitor return self + class CloningVisitor(ClauseVisitor): """Base class for visitor objects which can traverse using the cloned_traverse() function. @@ -150,14 +166,18 @@ class CloningVisitor(ClauseVisitor): """ def copy_and_process(self, list_): - """Apply cloned traversal to the given list of elements, and return the new list.""" + """Apply cloned traversal to the given list of elements, and return + the new list. + """ return [self.traverse(x) for x in list_] def traverse(self, obj): """traverse and visit the given expression structure.""" - return cloned_traverse(obj, self.__traverse_options__, self._visitor_dict) + return cloned_traverse( + obj, self.__traverse_options__, self._visitor_dict) + class ReplacingCloningVisitor(CloningVisitor): """Base class for visitor objects which can traverse using @@ -184,6 +204,7 @@ class ReplacingCloningVisitor(CloningVisitor): return e return replacement_traverse(obj, self.__traverse_options__, replace) + def iterate(obj, opts): """traverse the given expression structure, returning an iterator. @@ -197,6 +218,7 @@ def iterate(obj, opts): for c in t.get_children(**opts): stack.append(c) + def iterate_depthfirst(obj, opts): """traverse the given expression structure, returning an iterator. @@ -212,43 +234,53 @@ def iterate_depthfirst(obj, opts): stack.append(c) return iter(traversal) -def traverse_using(iterator, obj, visitors): - """visit the given expression structure using the given iterator of objects.""" +def traverse_using(iterator, obj, visitors): + """visit the given expression structure using the given iterator of + objects. + + """ for target in iterator: meth = visitors.get(target.__visit_name__, None) if meth: meth(target) return obj -def traverse(obj, opts, visitors): - """traverse and visit the given expression structure using the default iterator.""" +def traverse(obj, opts, visitors): + """traverse and visit the given expression structure using the default + iterator. + + """ return traverse_using(iterate(obj, opts), obj, visitors) -def traverse_depthfirst(obj, opts, visitors): - """traverse and visit the given expression structure using the depth-first iterator.""" +def traverse_depthfirst(obj, opts, visitors): + """traverse and visit the given expression structure using the + depth-first iterator. + + """ return traverse_using(iterate_depthfirst(obj, opts), obj, visitors) + def cloned_traverse(obj, opts, visitors): """clone the given expression structure, allowing modifications by visitors.""" - cloned = util.column_dict() - stop_on = util.column_set(opts.get('stop_on', [])) + cloned = {} + stop_on = set(opts.get('stop_on', [])) def clone(elem): if elem in stop_on: return elem else: - if elem not in cloned: - cloned[elem] = newelem = elem._clone() + if id(elem) not in cloned: + cloned[id(elem)] = newelem = elem._clone() newelem._copy_internals(clone=clone) meth = visitors.get(newelem.__visit_name__, None) if meth: meth(newelem) - return cloned[elem] + return cloned[id(elem)] if obj is not None: obj = clone(obj) @@ -259,17 +291,17 @@ def replacement_traverse(obj, opts, replace): """clone the given expression structure, allowing element replacement by a given replacement function.""" - cloned = util.column_dict() - stop_on = util.column_set(opts.get('stop_on', [])) + cloned = {} + stop_on = set([id(x) for x in opts.get('stop_on', [])]) def clone(elem, **kw): - if elem in stop_on or \ + if id(elem) in stop_on or \ 'no_replacement_traverse' in elem._annotations: return elem else: newelem = replace(elem) if newelem is not None: - stop_on.add(newelem) + stop_on.add(id(newelem)) return newelem else: if elem not in cloned: diff --git a/libs/sqlalchemy/types.py b/libs/sqlalchemy/types.py index 5fe2ba20..3994bd4a 100644 --- a/libs/sqlalchemy/types.py +++ b/libs/sqlalchemy/types.py @@ -1,2406 +1,77 @@ -# sqlalchemy/types.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# types.py +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""defines genericized SQL types, each represented by a subclass of -:class:`~sqlalchemy.types.AbstractType`. Dialects define further subclasses of these -types. - -For more information see the SQLAlchemy documentation on types. +"""Compatiblity namespace for sqlalchemy.sql.types. """ -__all__ = [ 'TypeEngine', 'TypeDecorator', 'AbstractType', 'UserDefinedType', - 'INT', 'CHAR', 'VARCHAR', 'NCHAR', 'NVARCHAR','TEXT', 'Text', + +__all__ = ['TypeEngine', 'TypeDecorator', 'UserDefinedType', + 'INT', 'CHAR', 'VARCHAR', 'NCHAR', 'NVARCHAR', 'TEXT', 'Text', 'FLOAT', 'NUMERIC', 'REAL', 'DECIMAL', 'TIMESTAMP', 'DATETIME', - 'CLOB', 'BLOB', 'BINARY', 'VARBINARY', 'BOOLEAN', 'BIGINT', 'SMALLINT', - 'INTEGER', 'DATE', 'TIME', 'String', 'Integer', 'SmallInteger', - 'BigInteger', 'Numeric', 'Float', 'DateTime', 'Date', 'Time', - 'LargeBinary', 'Binary', 'Boolean', 'Unicode', 'Concatenable', - 'UnicodeText','PickleType', 'Interval', 'Enum', 'MutableType' ] - -import inspect -import datetime as dt -import codecs - -from sqlalchemy import exc, schema -from sqlalchemy.sql import expression, operators -from sqlalchemy.util import pickle -from sqlalchemy.util.compat import decimal -from sqlalchemy.sql.visitors import Visitable -from sqlalchemy import util -from sqlalchemy import processors, events, event -import collections -default = util.importlater("sqlalchemy.engine", "default") - -NoneType = type(None) -if util.jython: - import array - -class AbstractType(Visitable): - """Base for all types - not needed except for backwards - compatibility.""" - -class TypeEngine(AbstractType): - """Base for built-in types.""" - - def copy_value(self, value): - return value - - def bind_processor(self, dialect): - """Return a conversion function for processing bind values. - - Returns a callable which will receive a bind parameter value - as the sole positional argument and will return a value to - send to the DB-API. - - If processing is not necessary, the method should return ``None``. - - :param dialect: Dialect instance in use. - - """ - return None - - def result_processor(self, dialect, coltype): - """Return a conversion function for processing result row values. - - Returns a callable which will receive a result row column - value as the sole positional argument and will return a value - to return to the user. - - If processing is not necessary, the method should return ``None``. - - :param dialect: Dialect instance in use. - - :param coltype: DBAPI coltype argument received in cursor.description. - - """ - return None - - def compare_values(self, x, y): - """Compare two values for equality.""" - - return x == y - - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - This allows systems like the ORM to know if a column value can - be considered 'not changed' by comparing the identity of - objects alone. Values such as dicts, lists which - are serialized into strings are examples of "mutable" - column structures. - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - - When this method is overridden, :meth:`copy_value` should - also be supplied. The :class:`.MutableType` mixin - is recommended as a helper. - - """ - return False - - def get_dbapi_type(self, dbapi): - """Return the corresponding type object from the underlying DB-API, if - any. - - This can be useful for calling ``setinputsizes()``, for example. - - """ - return None - - @property - def python_type(self): - """Return the Python type object expected to be returned - by instances of this type, if known. - - Basically, for those types which enforce a return type, - or are known across the board to do such for all common - DBAPIs (like ``int`` for example), will return that type. - - If a return type is not defined, raises - ``NotImplementedError``. - - Note that any type also accommodates NULL in SQL which - means you can also get back ``None`` from any type - in practice. - - """ - raise NotImplementedError() - - def with_variant(self, type_, dialect_name): - """Produce a new type object that will utilize the given - type when applied to the dialect of the given name. - - e.g.:: - - from sqlalchemy.types import String - from sqlalchemy.dialects import mysql - - s = String() - - s = s.with_variant(mysql.VARCHAR(collation='foo'), 'mysql') - - The construction of :meth:`.TypeEngine.with_variant` is always - from the "fallback" type to that which is dialect specific. - The returned type is an instance of :class:`.Variant`, which - itself provides a :meth:`~sqlalchemy.types.Variant.with_variant` that can - be called repeatedly. - - :param type_: a :class:`.TypeEngine` that will be selected - as a variant from the originating type, when a dialect - of the given name is in use. - :param dialect_name: base name of the dialect which uses - this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) - - .. versionadded:: 0.7.2 - - """ - return Variant(self, {dialect_name:type_}) - - def _adapt_expression(self, op, othertype): - """evaluate the return type of , - and apply any adaptations to the given operator. - - """ - return op, self - - @util.memoized_property - def _type_affinity(self): - """Return a rudimental 'affinity' value expressing the general class - of type.""" - - typ = None - for t in self.__class__.__mro__: - if t is TypeEngine or t is UserDefinedType: - return typ - elif issubclass(t, TypeEngine): - typ = t - else: - return self.__class__ - - def dialect_impl(self, dialect): - """Return a dialect-specific implementation for this :class:`.TypeEngine`.""" - - try: - return dialect._type_memos[self]['impl'] - except KeyError: - return self._dialect_info(dialect)['impl'] - - def _cached_bind_processor(self, dialect): - """Return a dialect-specific bind processor for this type.""" - - try: - return dialect._type_memos[self]['bind'] - except KeyError: - d = self._dialect_info(dialect) - d['bind'] = bp = d['impl'].bind_processor(dialect) - return bp - - def _cached_result_processor(self, dialect, coltype): - """Return a dialect-specific result processor for this type.""" - - try: - return dialect._type_memos[self][coltype] - except KeyError: - d = self._dialect_info(dialect) - # key assumption: DBAPI type codes are - # constants. Else this dictionary would - # grow unbounded. - d[coltype] = rp = d['impl'].result_processor(dialect, coltype) - return rp - - def _dialect_info(self, dialect): - """Return a dialect-specific registry which - caches a dialect-specific implementation, bind processing - function, and one or more result processing functions.""" - - if self in dialect._type_memos: - return dialect._type_memos[self] - else: - impl = self._gen_dialect_impl(dialect) - if impl is self: - impl = self.adapt(type(self)) - # this can't be self, else we create a cycle - assert impl is not self - dialect._type_memos[self] = d = {'impl':impl} - return d - - def _gen_dialect_impl(self, dialect): - return dialect.type_descriptor(self) - - def adapt(self, cls, **kw): - """Produce an "adapted" form of this type, given an "impl" class - to work with. - - This method is used internally to associate generic - types with "implementation" types that are specific to a particular - dialect. - """ - return util.constructor_copy(self, cls, **kw) - - def _coerce_compared_value(self, op, value): - """Suggest a type for a 'coerced' Python value in an expression. - - Given an operator and value, gives the type a chance - to return a type which the value should be coerced into. - - The default behavior here is conservative; if the right-hand - side is already coerced into a SQL type based on its - Python type, it is usually left alone. - - End-user functionality extension here should generally be via - :class:`.TypeDecorator`, which provides more liberal behavior in that - it defaults to coercing the other side of the expression into this - type, thus applying special Python conversions above and beyond those - needed by the DBAPI to both ides. It also provides the public method - :meth:`.TypeDecorator.coerce_compared_value` which is intended for - end-user customization of this behavior. - - """ - _coerced_type = _type_map.get(type(value), NULLTYPE) - if _coerced_type is NULLTYPE or _coerced_type._type_affinity \ - is self._type_affinity: - return self - else: - return _coerced_type - - def _compare_type_affinity(self, other): - return self._type_affinity is other._type_affinity - - def compile(self, dialect=None): - """Produce a string-compiled form of this :class:`.TypeEngine`. - - When called with no arguments, uses a "default" dialect - to produce a string result. - - :param dialect: a :class:`.Dialect` instance. - - """ - # arg, return value is inconsistent with - # ClauseElement.compile()....this is a mistake. - - if not dialect: - dialect = self._default_dialect - - return dialect.type_compiler.process(self) - - @property - def _default_dialect(self): - if self.__class__.__module__.startswith("sqlalchemy.dialects"): - tokens = self.__class__.__module__.split(".")[0:3] - mod = ".".join(tokens) - return getattr(__import__(mod).dialects, tokens[-1]).dialect() - else: - return default.DefaultDialect() - - def __str__(self): - # Py3K - #return unicode(self.compile()) - # Py2K - return unicode(self.compile()).\ - encode('ascii', 'backslashreplace') - # end Py2K - - def __init__(self, *args, **kwargs): - """Support implementations that were passing arguments""" - if args or kwargs: - util.warn_deprecated("Passing arguments to type object " - "constructor %s is deprecated" % self.__class__) - - def __repr__(self): - return util.generic_repr(self) - -class UserDefinedType(TypeEngine): - """Base for user defined types. - - This should be the base of new types. Note that - for most cases, :class:`.TypeDecorator` is probably - more appropriate:: - - import sqlalchemy.types as types - - class MyType(types.UserDefinedType): - def __init__(self, precision = 8): - self.precision = precision - - def get_col_spec(self): - return "MYTYPE(%s)" % self.precision - - def bind_processor(self, dialect): - def process(value): - return value - return process - - def result_processor(self, dialect, coltype): - def process(value): - return value - return process - - Once the type is made, it's immediately usable:: - - table = Table('foo', meta, - Column('id', Integer, primary_key=True), - Column('data', MyType(16)) - ) - - """ - __visit_name__ = "user_defined" - - def _adapt_expression(self, op, othertype): - """evaluate the return type of , - and apply any adaptations to the given operator. - - """ - return self.adapt_operator(op), self - - def adapt_operator(self, op): - """A hook which allows the given operator to be adapted - to something new. - - See also UserDefinedType._adapt_expression(), an as-yet- - semi-public method with greater capability in this regard. - - """ - return op - -class TypeDecorator(TypeEngine): - """Allows the creation of types which add additional functionality - to an existing type. - - This method is preferred to direct subclassing of SQLAlchemy's - built-in types as it ensures that all required functionality of - the underlying type is kept in place. - - Typical usage:: - - import sqlalchemy.types as types - - class MyType(types.TypeDecorator): - '''Prefixes Unicode values with "PREFIX:" on the way in and - strips it off on the way out. - ''' - - impl = types.Unicode - - def process_bind_param(self, value, dialect): - return "PREFIX:" + value - - def process_result_value(self, value, dialect): - return value[7:] - - def copy(self): - return MyType(self.impl.length) - - The class-level "impl" attribute is required, and can reference any - TypeEngine class. Alternatively, the load_dialect_impl() method - can be used to provide different type classes based on the dialect - given; in this case, the "impl" variable can reference - ``TypeEngine`` as a placeholder. - - Types that receive a Python type that isn't similar to the ultimate type - used may want to define the :meth:`TypeDecorator.coerce_compared_value` - method. This is used to give the expression system a hint when coercing - Python objects into bind parameters within expressions. Consider this - expression:: - - mytable.c.somecol + datetime.date(2009, 5, 15) - - Above, if "somecol" is an ``Integer`` variant, it makes sense that - we're doing date arithmetic, where above is usually interpreted - by databases as adding a number of days to the given date. - The expression system does the right thing by not attempting to - coerce the "date()" value into an integer-oriented bind parameter. - - However, in the case of ``TypeDecorator``, we are usually changing an - incoming Python type to something new - ``TypeDecorator`` by default will - "coerce" the non-typed side to be the same type as itself. Such as below, - we define an "epoch" type that stores a date value as an integer:: - - class MyEpochType(types.TypeDecorator): - impl = types.Integer - - epoch = datetime.date(1970, 1, 1) - - def process_bind_param(self, value, dialect): - return (value - self.epoch).days - - def process_result_value(self, value, dialect): - return self.epoch + timedelta(days=value) - - Our expression of ``somecol + date`` with the above type will coerce the - "date" on the right side to also be treated as ``MyEpochType``. - - This behavior can be overridden via the - :meth:`~TypeDecorator.coerce_compared_value` method, which returns a type - that should be used for the value of the expression. Below we set it such - that an integer value will be treated as an ``Integer``, and any other - value is assumed to be a date and will be treated as a ``MyEpochType``:: - - def coerce_compared_value(self, op, value): - if isinstance(value, int): - return Integer() - else: - return self - - """ - - __visit_name__ = "type_decorator" - - def __init__(self, *args, **kwargs): - """Construct a :class:`.TypeDecorator`. - - Arguments sent here are passed to the constructor - of the class assigned to the ``impl`` class level attribute, - assuming the ``impl`` is a callable, and the resulting - object is assigned to the ``self.impl`` instance attribute - (thus overriding the class attribute of the same name). - - If the class level ``impl`` is not a callable (the unusual case), - it will be assigned to the same instance attribute 'as-is', - ignoring those arguments passed to the constructor. - - Subclasses can override this to customize the generation - of ``self.impl`` entirely. - - """ - - if not hasattr(self.__class__, 'impl'): - raise AssertionError("TypeDecorator implementations " - "require a class-level variable " - "'impl' which refers to the class of " - "type being decorated") - self.impl = to_instance(self.__class__.impl, *args, **kwargs) - - - def _gen_dialect_impl(self, dialect): - """ - #todo - """ - adapted = dialect.type_descriptor(self) - if adapted is not self: - return adapted - - # otherwise adapt the impl type, link - # to a copy of this TypeDecorator and return - # that. - typedesc = self.load_dialect_impl(dialect).dialect_impl(dialect) - tt = self.copy() - if not isinstance(tt, self.__class__): - raise AssertionError('Type object %s does not properly ' - 'implement the copy() method, it must ' - 'return an object of type %s' % (self, - self.__class__)) - tt.impl = typedesc - return tt - - @property - def _type_affinity(self): - """ - #todo - """ - return self.impl._type_affinity - - def type_engine(self, dialect): - """Return a dialect-specific :class:`.TypeEngine` instance for this :class:`.TypeDecorator`. - - In most cases this returns a dialect-adapted form of - the :class:`.TypeEngine` type represented by ``self.impl``. - Makes usage of :meth:`dialect_impl` but also traverses - into wrapped :class:`.TypeDecorator` instances. - Behavior can be customized here by overriding :meth:`load_dialect_impl`. - - """ - adapted = dialect.type_descriptor(self) - if type(adapted) is not type(self): - return adapted - elif isinstance(self.impl, TypeDecorator): - return self.impl.type_engine(dialect) - else: - return self.load_dialect_impl(dialect) - - def load_dialect_impl(self, dialect): - """Return a :class:`.TypeEngine` object corresponding to a dialect. - - This is an end-user override hook that can be used to provide - differing types depending on the given dialect. It is used - by the :class:`.TypeDecorator` implementation of :meth:`type_engine` - to help determine what type should ultimately be returned - for a given :class:`.TypeDecorator`. - - By default returns ``self.impl``. - - """ - return self.impl - - def __getattr__(self, key): - """Proxy all other undefined accessors to the underlying - implementation.""" - return getattr(self.impl, key) - - def process_bind_param(self, value, dialect): - """Receive a bound parameter value to be converted. - - Subclasses override this method to return the - value that should be passed along to the underlying - :class:`.TypeEngine` object, and from there to the - DBAPI ``execute()`` method. - - The operation could be anything desired to perform custom - behavior, such as transforming or serializing data. - This could also be used as a hook for validating logic. - - This operation should be designed with the reverse operation - in mind, which would be the process_result_value method of - this class. - - :param value: Data to operate upon, of any type expected by - this method in the subclass. Can be ``None``. - :param dialect: the :class:`.Dialect` in use. - - """ - - raise NotImplementedError() - - def process_result_value(self, value, dialect): - """Receive a result-row column value to be converted. - - Subclasses should implement this method to operate on data - fetched from the database. - - Subclasses override this method to return the - value that should be passed back to the application, - given a value that is already processed by - the underlying :class:`.TypeEngine` object, originally - from the DBAPI cursor method ``fetchone()`` or similar. - - The operation could be anything desired to perform custom - behavior, such as transforming or serializing data. - This could also be used as a hook for validating logic. - - :param value: Data to operate upon, of any type expected by - this method in the subclass. Can be ``None``. - :param dialect: the :class:`.Dialect` in use. - - This operation should be designed to be reversible by - the "process_bind_param" method of this class. - - """ - - raise NotImplementedError() - - def bind_processor(self, dialect): - """Provide a bound value processing function for the - given :class:`.Dialect`. - - This is the method that fulfills the :class:`.TypeEngine` - contract for bound value conversion. :class:`.TypeDecorator` - will wrap a user-defined implementation of - :meth:`process_bind_param` here. - - User-defined code can override this method directly, - though its likely best to use :meth:`process_bind_param` so that - the processing provided by ``self.impl`` is maintained. - - :param dialect: Dialect instance in use. - - This method is the reverse counterpart to the - :meth:`result_processor` method of this class. - - """ - if self.__class__.process_bind_param.func_code \ - is not TypeDecorator.process_bind_param.func_code: - process_param = self.process_bind_param - impl_processor = self.impl.bind_processor(dialect) - if impl_processor: - def process(value): - return impl_processor(process_param(value, dialect)) - - else: - def process(value): - return process_param(value, dialect) - - return process - else: - return self.impl.bind_processor(dialect) - - def result_processor(self, dialect, coltype): - """Provide a result value processing function for the given :class:`.Dialect`. - - This is the method that fulfills the :class:`.TypeEngine` - contract for result value conversion. :class:`.TypeDecorator` - will wrap a user-defined implementation of - :meth:`process_result_value` here. - - User-defined code can override this method directly, - though its likely best to use :meth:`process_result_value` so that - the processing provided by ``self.impl`` is maintained. - - :param dialect: Dialect instance in use. - :param coltype: An SQLAlchemy data type - - This method is the reverse counterpart to the - :meth:`bind_processor` method of this class. - - """ - if self.__class__.process_result_value.func_code \ - is not TypeDecorator.process_result_value.func_code: - process_value = self.process_result_value - impl_processor = self.impl.result_processor(dialect, - coltype) - if impl_processor: - def process(value): - return process_value(impl_processor(value), dialect) - - else: - def process(value): - return process_value(value, dialect) - - return process - else: - return self.impl.result_processor(dialect, coltype) - - def coerce_compared_value(self, op, value): - """Suggest a type for a 'coerced' Python value in an expression. - - By default, returns self. This method is called by - the expression system when an object using this type is - on the left or right side of an expression against a plain Python - object which does not yet have a SQLAlchemy type assigned:: - - expr = table.c.somecolumn + 35 - - Where above, if ``somecolumn`` uses this type, this method will - be called with the value ``operator.add`` - and ``35``. The return value is whatever SQLAlchemy type should - be used for ``35`` for this particular operation. - - """ - return self - - def _coerce_compared_value(self, op, value): - """See :meth:`.TypeEngine._coerce_compared_value` for a description.""" - - return self.coerce_compared_value(op, value) - - def copy(self): - """Produce a copy of this :class:`.TypeDecorator` instance. - - This is a shallow copy and is provided to fulfill part of - the :class:`.TypeEngine` contract. It usually does not - need to be overridden unless the user-defined :class:`.TypeDecorator` - has local state that should be deep-copied. - - """ - - instance = self.__class__.__new__(self.__class__) - instance.__dict__.update(self.__dict__) - return instance - - def get_dbapi_type(self, dbapi): - """Return the DBAPI type object represented by this :class:`.TypeDecorator`. - - By default this calls upon :meth:`.TypeEngine.get_dbapi_type` of the - underlying "impl". - """ - return self.impl.get_dbapi_type(dbapi) - - def copy_value(self, value): - """Given a value, produce a copy of it. - - By default this calls upon :meth:`.TypeEngine.copy_value` - of the underlying "impl". - - :meth:`.copy_value` will return the object - itself, assuming "mutability" is not enabled. - Only the :class:`.MutableType` mixin provides a copy - function that actually produces a new object. - The copying function is used by the ORM when - "mutable" types are used, to memoize the original - version of an object as loaded from the database, - which is then compared to the possibly mutated - version to check for changes. - - Modern implementations should use the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel` for intercepting in-place - changes to values. - - """ - return self.impl.copy_value(value) - - def compare_values(self, x, y): - """Given two values, compare them for equality. - - By default this calls upon :meth:`.TypeEngine.compare_values` - of the underlying "impl", which in turn usually - uses the Python equals operator ``==``. - - This function is used by the ORM to compare - an original-loaded value with an intercepted - "changed" value, to determine if a net change - has occurred. - - """ - return self.impl.compare_values(x, y) - - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - This allows systems like the ORM to know if a column value can - be considered 'not changed' by comparing the identity of - objects alone. Values such as dicts, lists which - are serialized into strings are examples of "mutable" - column structures. - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - - """ - return self.impl.is_mutable() - - def _adapt_expression(self, op, othertype): - """ - #todo - """ - op, typ =self.impl._adapt_expression(op, othertype) - if typ is self.impl: - return op, self - else: - return op, typ - - def __repr__(self): - return util.generic_repr(self, to_inspect=self.impl) - -class Variant(TypeDecorator): - """A wrapping type that selects among a variety of - implementations based on dialect in use. - - The :class:`.Variant` type is typically constructed - using the :meth:`.TypeEngine.with_variant` method. - - .. versionadded:: 0.7.2 - - """ - - def __init__(self, base, mapping): - """Construct a new :class:`.Variant`. - - :param base: the base 'fallback' type - :param mapping: dictionary of string dialect names to :class:`.TypeEngine` - instances. - - """ - self.impl = base - self.mapping = mapping - - def load_dialect_impl(self, dialect): - if dialect.name in self.mapping: - return self.mapping[dialect.name] - else: - return self.impl - - def with_variant(self, type_, dialect_name): - """Return a new :class:`.Variant` which adds the given - type + dialect name to the mapping, in addition to the - mapping present in this :class:`.Variant`. - - :param type_: a :class:`.TypeEngine` that will be selected - as a variant from the originating type, when a dialect - of the given name is in use. - :param dialect_name: base name of the dialect which uses - this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) - - """ - - if dialect_name in self.mapping: - raise exc.ArgumentError( - "Dialect '%s' is already present in " - "the mapping for this Variant" % dialect_name) - mapping = self.mapping.copy() - mapping[dialect_name] = type_ - return Variant(self.impl, mapping) - -class MutableType(object): - """A mixin that marks a :class:`.TypeEngine` as representing - a mutable Python object type. This functionality is used - only by the ORM. - - .. versionchanged:: 0.7 - :class:`.MutableType` is superseded - by the ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. This extension provides an event - driven approach to in-place mutation detection that does not - incur the severe performance penalty of the :class:`.MutableType` - approach. - - "mutable" means that changes can occur in place to a value - of this type. Examples includes Python lists, dictionaries, - and sets, as well as user-defined objects. The primary - need for identification of "mutable" types is by the ORM, - which applies special rules to such values in order to guarantee - that changes are detected. These rules may have a significant - performance impact, described below. - - A :class:`.MutableType` usually allows a flag called - ``mutable=False`` to enable/disable the "mutability" flag, - represented on this class by :meth:`is_mutable`. Examples - include :class:`.PickleType` and - :class:`~sqlalchemy.dialects.postgresql.base.ARRAY`. Setting - this flag to ``True`` enables mutability-specific behavior - by the ORM. - - The :meth:`copy_value` and :meth:`compare_values` functions - represent a copy and compare function for values of this - type - implementing subclasses should override these - appropriately. - - .. warning:: - - The usage of mutable types has significant performance - implications when using the ORM. In order to detect changes, the - ORM must create a copy of the value when it is first - accessed, so that changes to the current value can be compared - against the "clean" database-loaded value. Additionally, when the - ORM checks to see if any data requires flushing, it must scan - through all instances in the session which are known to have - "mutable" attributes and compare the current value of each - one to its "clean" - value. So for example, if the Session contains 6000 objects (a - fairly large amount) and autoflush is enabled, every individual - execution of :class:`.Query` will require a full scan of that subset of - the 6000 objects that have mutable attributes, possibly resulting - in tens of thousands of additional method calls for every query. - - .. versionchanged:: 0.7 - As of SQLAlchemy 0.7, the ``sqlalchemy.ext.mutable`` is provided - which allows an event driven approach to in-place - mutation detection. This approach should now be favored over - the usage of :class:`.MutableType` with ``mutable=True``. - ``sqlalchemy.ext.mutable`` is described in :ref:`mutable_toplevel`. - - """ - - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - For :class:`.MutableType`, this method is set to - return ``True``. - - """ - return True - - def copy_value(self, value): - """Unimplemented.""" - raise NotImplementedError() - - def compare_values(self, x, y): - """Compare *x* == *y*.""" - return x == y - -def to_instance(typeobj, *arg, **kw): - if typeobj is None: - return NULLTYPE - - if util.callable(typeobj): - return typeobj(*arg, **kw) - else: - return typeobj - -def adapt_type(typeobj, colspecs): - if isinstance(typeobj, type): - typeobj = typeobj() - for t in typeobj.__class__.__mro__[0:-1]: - try: - impltype = colspecs[t] - break - except KeyError: - pass - else: - # couldnt adapt - so just return the type itself - # (it may be a user-defined type) - return typeobj - # if we adapted the given generic type to a database-specific type, - # but it turns out the originally given "generic" type - # is actually a subclass of our resulting type, then we were already - # given a more specific type than that required; so use that. - if (issubclass(typeobj.__class__, impltype)): - return typeobj - return typeobj.adapt(impltype) - - - - - -class NullType(TypeEngine): - """An unknown type. - - NullTypes will stand in if :class:`~sqlalchemy.Table` reflection - encounters a column data type unknown to SQLAlchemy. The - resulting columns are nearly fully usable: the DB-API adapter will - handle all translation to and from the database data type. - - NullType does not have sufficient information to particpate in a - ``CREATE TABLE`` statement and will raise an exception if - encountered during a :meth:`~sqlalchemy.Table.create` operation. - - """ - __visit_name__ = 'null' - - def _adapt_expression(self, op, othertype): - if isinstance(othertype, NullType) or not operators.is_commutative(op): - return op, self - else: - return othertype._adapt_expression(op, self) - -NullTypeEngine = NullType - -class Concatenable(object): - """A mixin that marks a type as supporting 'concatenation', - typically strings.""" - - def _adapt_expression(self, op, othertype): - if op is operators.add and issubclass(othertype._type_affinity, - (Concatenable, NullType)): - return operators.concat_op, self - else: - return op, self - -class _DateAffinity(object): - """Mixin date/time specific expression adaptations. - - Rules are implemented within Date,Time,Interval,DateTime, Numeric, - Integer. Based on http://www.postgresql.org/docs/current/static - /functions-datetime.html. - - """ - - @property - def _expression_adaptations(self): - raise NotImplementedError() - - _blank_dict = util.immutabledict() - def _adapt_expression(self, op, othertype): - othertype = othertype._type_affinity - return op, \ - self._expression_adaptations.get(op, self._blank_dict).\ - get(othertype, NULLTYPE) - -class String(Concatenable, TypeEngine): - """The base for all string and character types. - - In SQL, corresponds to VARCHAR. Can also take Python unicode objects - and encode to the database's encoding in bind params (and the reverse for - result sets.) - - The `length` field is usually required when the `String` type is - used within a CREATE TABLE statement, as VARCHAR requires a length - on most databases. - - """ - - __visit_name__ = 'string' - - def __init__(self, length=None, convert_unicode=False, - assert_unicode=None, unicode_error=None, - _warn_on_bytestring=False - ): - """ - Create a string-holding type. - - :param length: optional, a length for the column for use in - DDL statements. May be safely omitted if no ``CREATE - TABLE`` will be issued. Certain databases may require a - ``length`` for use in DDL, and will raise an exception when - the ``CREATE TABLE`` DDL is issued if a ``VARCHAR`` - with no length is included. Whether the value is - interpreted as bytes or characters is database specific. - - :param convert_unicode: When set to ``True``, the - :class:`.String` type will assume that - input is to be passed as Python ``unicode`` objects, - and results returned as Python ``unicode`` objects. - If the DBAPI in use does not support Python unicode - (which is fewer and fewer these days), SQLAlchemy - will encode/decode the value, using the - value of the ``encoding`` parameter passed to - :func:`.create_engine` as the encoding. - - When using a DBAPI that natively supports Python - unicode objects, this flag generally does not - need to be set. For columns that are explicitly - intended to store non-ASCII data, the :class:`.Unicode` - or :class:`UnicodeText` - types should be used regardless, which feature - the same behavior of ``convert_unicode`` but - also indicate an underlying column type that - directly supports unicode, such as ``NVARCHAR``. - - For the extremely rare case that Python ``unicode`` - is to be encoded/decoded by SQLAlchemy on a backend - that does natively support Python ``unicode``, - the value ``force`` can be passed here which will - cause SQLAlchemy's encode/decode services to be - used unconditionally. - - :param assert_unicode: Deprecated. A warning is emitted - when a non-``unicode`` object is passed to the - :class:`.Unicode` subtype of :class:`.String`, - or the :class:`.UnicodeText` subtype of :class:`.Text`. - See :class:`.Unicode` for information on how to - control this warning. - - :param unicode_error: Optional, a method to use to handle Unicode - conversion errors. Behaves like the ``errors`` keyword argument to - the standard library's ``string.decode()`` functions. This flag - requires that ``convert_unicode`` is set to ``force`` - otherwise, - SQLAlchemy is not guaranteed to handle the task of unicode - conversion. Note that this flag adds significant performance - overhead to row-fetching operations for backends that already - return unicode objects natively (which most DBAPIs do). This - flag should only be used as a last resort for reading - strings from a column with varied or corrupted encodings. - - """ - if unicode_error is not None and convert_unicode != 'force': - raise exc.ArgumentError("convert_unicode must be 'force' " - "when unicode_error is set.") - - if assert_unicode: - util.warn_deprecated('assert_unicode is deprecated. ' - 'SQLAlchemy emits a warning in all ' - 'cases where it would otherwise like ' - 'to encode a Python unicode object ' - 'into a specific encoding but a plain ' - 'bytestring is received. This does ' - '*not* apply to DBAPIs that coerce ' - 'Unicode natively.') - self.length = length - self.convert_unicode = convert_unicode - self.unicode_error = unicode_error - self._warn_on_bytestring = _warn_on_bytestring - - def bind_processor(self, dialect): - if self.convert_unicode or dialect.convert_unicode: - if dialect.supports_unicode_binds and \ - self.convert_unicode != 'force': - if self._warn_on_bytestring: - def process(value): - # Py3K - #if isinstance(value, bytes): - # Py2K - if isinstance(value, str): - # end Py2K - util.warn("Unicode type received non-unicode bind " - "param value.") - return value - return process - else: - return None - else: - encoder = codecs.getencoder(dialect.encoding) - warn_on_bytestring = self._warn_on_bytestring - def process(value): - if isinstance(value, unicode): - return encoder(value, self.unicode_error)[0] - elif warn_on_bytestring and value is not None: - util.warn("Unicode type received non-unicode bind " - "param value") - return value - return process - else: - return None - - def result_processor(self, dialect, coltype): - wants_unicode = self.convert_unicode or dialect.convert_unicode - needs_convert = wants_unicode and \ - (dialect.returns_unicode_strings is not True or - self.convert_unicode == 'force') - - if needs_convert: - to_unicode = processors.to_unicode_processor_factory( - dialect.encoding, self.unicode_error) - - if dialect.returns_unicode_strings: - # we wouldn't be here unless convert_unicode='force' - # was specified, or the driver has erratic unicode-returning - # habits. since we will be getting back unicode - # in most cases, we check for it (decode will fail). - def process(value): - if isinstance(value, unicode): - return value - else: - return to_unicode(value) - return process - else: - # here, we assume that the object is not unicode, - # avoiding expensive isinstance() check. - return to_unicode - else: - return None - - @property - def python_type(self): - if self.convert_unicode: - return unicode - else: - return str - - def get_dbapi_type(self, dbapi): - return dbapi.STRING - -class Text(String): - """A variably sized string type. - - In SQL, usually corresponds to CLOB or TEXT. Can also take Python - unicode objects and encode to the database's encoding in bind - params (and the reverse for result sets.) - - """ - __visit_name__ = 'text' - -class Unicode(String): - """A variable length Unicode string type. - - The :class:`.Unicode` type is a :class:`.String` subclass - that assumes input and output as Python ``unicode`` data, - and in that regard is equivalent to the usage of the - ``convert_unicode`` flag with the :class:`.String` type. - However, unlike plain :class:`.String`, it also implies an - underlying column type that is explicitly supporting of non-ASCII - data, such as ``NVARCHAR`` on Oracle and SQL Server. - This can impact the output of ``CREATE TABLE`` statements - and ``CAST`` functions at the dialect level, and can - also affect the handling of bound parameters in some - specific DBAPI scenarios. - - The encoding used by the :class:`.Unicode` type is usually - determined by the DBAPI itself; most modern DBAPIs - feature support for Python ``unicode`` objects as bound - values and result set values, and the encoding should - be configured as detailed in the notes for the target - DBAPI in the :ref:`dialect_toplevel` section. - - For those DBAPIs which do not support, or are not configured - to accommodate Python ``unicode`` objects - directly, SQLAlchemy does the encoding and decoding - outside of the DBAPI. The encoding in this scenario - is determined by the ``encoding`` flag passed to - :func:`.create_engine`. - - When using the :class:`.Unicode` type, it is only appropriate - to pass Python ``unicode`` objects, and not plain ``str``. - If a plain ``str`` is passed under Python 2, a warning - is emitted. If you notice your application emitting these warnings but - you're not sure of the source of them, the Python - ``warnings`` filter, documented at - http://docs.python.org/library/warnings.html, - can be used to turn these warnings into exceptions - which will illustrate a stack trace:: - - import warnings - warnings.simplefilter('error') - - For an application that wishes to pass plain bytestrings - and Python ``unicode`` objects to the ``Unicode`` type - equally, the bytestrings must first be decoded into - unicode. The recipe at :ref:`coerce_to_unicode` illustrates - how this is done. - - See also: - - :class:`.UnicodeText` - unlengthed textual counterpart - to :class:`.Unicode`. - - """ - - __visit_name__ = 'unicode' - - def __init__(self, length=None, **kwargs): - """ - Create a :class:`.Unicode` object. - - Parameters are the same as that of :class:`.String`, - with the exception that ``convert_unicode`` - defaults to ``True``. - - """ - kwargs.setdefault('convert_unicode', True) - kwargs.setdefault('_warn_on_bytestring', True) - super(Unicode, self).__init__(length=length, **kwargs) - -class UnicodeText(Text): - """An unbounded-length Unicode string type. - - See :class:`.Unicode` for details on the unicode - behavior of this object. - - Like :class:`.Unicode`, usage the :class:`.UnicodeText` type implies a - unicode-capable type being used on the backend, such as - ``NCLOB``, ``NTEXT``. - - """ - - __visit_name__ = 'unicode_text' - - def __init__(self, length=None, **kwargs): - """ - Create a Unicode-converting Text type. - - Parameters are the same as that of :class:`.Text`, - with the exception that ``convert_unicode`` - defaults to ``True``. - - """ - kwargs.setdefault('convert_unicode', True) - kwargs.setdefault('_warn_on_bytestring', True) - super(UnicodeText, self).__init__(length=length, **kwargs) - - -class Integer(_DateAffinity, TypeEngine): - """A type for ``int`` integers.""" - - __visit_name__ = 'integer' - - def get_dbapi_type(self, dbapi): - return dbapi.NUMBER - - @property - def python_type(self): - return int - - @util.memoized_property - def _expression_adaptations(self): - # TODO: need a dictionary object that will - # handle operators generically here, this is incomplete - return { - operators.add:{ - Date:Date, - Integer:Integer, - Numeric:Numeric, - }, - operators.mul:{ - Interval:Interval, - Integer:Integer, - Numeric:Numeric, - }, - # Py2K - operators.div:{ - Integer:Integer, - Numeric:Numeric, - }, - # end Py2K - operators.truediv:{ - Integer:Integer, - Numeric:Numeric, - }, - operators.sub:{ - Integer:Integer, - Numeric:Numeric, - }, - } - -class SmallInteger(Integer): - """A type for smaller ``int`` integers. - - Typically generates a ``SMALLINT`` in DDL, and otherwise acts like - a normal :class:`.Integer` on the Python side. - - """ - - __visit_name__ = 'small_integer' - - -class BigInteger(Integer): - """A type for bigger ``int`` integers. - - Typically generates a ``BIGINT`` in DDL, and otherwise acts like - a normal :class:`.Integer` on the Python side. - - """ - - __visit_name__ = 'big_integer' - - -class Numeric(_DateAffinity, TypeEngine): - """A type for fixed precision numbers. - - Typically generates DECIMAL or NUMERIC. Returns - ``decimal.Decimal`` objects by default, applying - conversion as needed. - - .. note:: - - The `cdecimal `_ library - is a high performing alternative to Python's built-in - ``decimal.Decimal`` type, which performs very poorly in high volume - situations. SQLAlchemy 0.7 is tested against ``cdecimal`` and supports - it fully. The type is not necessarily supported by DBAPI - implementations however, most of which contain an import for plain - ``decimal`` in their source code, even though some such as psycopg2 - provide hooks for alternate adapters. SQLAlchemy imports ``decimal`` - globally as well. While the alternate ``Decimal`` class can be patched - into SQLA's ``decimal`` module, overall the most straightforward and - foolproof way to use "cdecimal" given current DBAPI and Python support - is to patch it directly into sys.modules before anything else is - imported:: - - import sys - import cdecimal - sys.modules["decimal"] = cdecimal - - While the global patch is a little ugly, it's particularly - important to use just one decimal library at a time since - Python Decimal and cdecimal Decimal objects - are not currently compatible *with each other*:: - - >>> import cdecimal - >>> import decimal - >>> decimal.Decimal("10") == cdecimal.Decimal("10") - False - - SQLAlchemy will provide more natural support of - cdecimal if and when it becomes a standard part of Python - installations and is supported by all DBAPIs. - - """ - - __visit_name__ = 'numeric' - - def __init__(self, precision=None, scale=None, asdecimal=True): - """ - Construct a Numeric. - - :param precision: the numeric precision for use in DDL ``CREATE - TABLE``. - - :param scale: the numeric scale for use in DDL ``CREATE TABLE``. - - :param asdecimal: default True. Return whether or not - values should be sent as Python Decimal objects, or - as floats. Different DBAPIs send one or the other based on - datatypes - the Numeric type will ensure that return values - are one or the other across DBAPIs consistently. - - When using the ``Numeric`` type, care should be taken to ensure - that the asdecimal setting is apppropriate for the DBAPI in use - - when Numeric applies a conversion from Decimal->float or float-> - Decimal, this conversion incurs an additional performance overhead - for all result columns received. - - DBAPIs that return Decimal natively (e.g. psycopg2) will have - better accuracy and higher performance with a setting of ``True``, - as the native translation to Decimal reduces the amount of floating- - point issues at play, and the Numeric type itself doesn't need - to apply any further conversions. However, another DBAPI which - returns floats natively *will* incur an additional conversion - overhead, and is still subject to floating point data loss - in - which case ``asdecimal=False`` will at least remove the extra - conversion overhead. - - """ - self.precision = precision - self.scale = scale - self.asdecimal = asdecimal - - def get_dbapi_type(self, dbapi): - return dbapi.NUMBER - - @property - def python_type(self): - if self.asdecimal: - return decimal.Decimal - else: - return float - - def bind_processor(self, dialect): - if dialect.supports_native_decimal: - return None - else: - return processors.to_float - - def result_processor(self, dialect, coltype): - if self.asdecimal: - if dialect.supports_native_decimal: - # we're a "numeric", DBAPI will give us Decimal directly - return None - else: - util.warn('Dialect %s+%s does *not* support Decimal ' - 'objects natively, and SQLAlchemy must ' - 'convert from floating point - rounding ' - 'errors and other issues may occur. Please ' - 'consider storing Decimal numbers as strings ' - 'or integers on this platform for lossless ' - 'storage.' % (dialect.name, dialect.driver)) - - # we're a "numeric", DBAPI returns floats, convert. - if self.scale is not None: - return processors.to_decimal_processor_factory( - decimal.Decimal, self.scale) - else: - return processors.to_decimal_processor_factory( - decimal.Decimal) - else: - if dialect.supports_native_decimal: - return processors.to_float - else: - return None - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.mul:{ - Interval:Interval, - Numeric:Numeric, - Integer:Numeric, - }, - # Py2K - operators.div:{ - Numeric:Numeric, - Integer:Numeric, - }, - # end Py2K - operators.truediv:{ - Numeric:Numeric, - Integer:Numeric, - }, - operators.add:{ - Numeric:Numeric, - Integer:Numeric, - }, - operators.sub:{ - Numeric:Numeric, - Integer:Numeric, - } - } - -class Float(Numeric): - """A type for ``float`` numbers. - - Returns Python ``float`` objects by default, applying - conversion as needed. - - """ - - __visit_name__ = 'float' - - scale = None - - def __init__(self, precision=None, asdecimal=False, **kwargs): - """ - Construct a Float. - - :param precision: the numeric precision for use in DDL ``CREATE - TABLE``. - - :param asdecimal: the same flag as that of :class:`.Numeric`, but - defaults to ``False``. Note that setting this flag to ``True`` - results in floating point conversion. - - :param \**kwargs: deprecated. Additional arguments here are ignored - by the default :class:`.Float` type. For database specific - floats that support additional arguments, see that dialect's - documentation for details, such as :class:`sqlalchemy.dialects.mysql.FLOAT`. - - """ - self.precision = precision - self.asdecimal = asdecimal - if kwargs: - util.warn_deprecated("Additional keyword arguments " - "passed to Float ignored.") - - def result_processor(self, dialect, coltype): - if self.asdecimal: - return processors.to_decimal_processor_factory(decimal.Decimal) - else: - return None - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.mul:{ - Interval:Interval, - Numeric:Float, - }, - # Py2K - operators.div:{ - Numeric:Float, - }, - # end Py2K - operators.truediv:{ - Numeric:Float, - }, - operators.add:{ - Numeric:Float, - }, - operators.sub:{ - Numeric:Float, - } - } - - -class DateTime(_DateAffinity, TypeEngine): - """A type for ``datetime.datetime()`` objects. - - Date and time types return objects from the Python ``datetime`` - module. Most DBAPIs have built in support for the datetime - module, with the noted exception of SQLite. In the case of - SQLite, date and time types are stored as strings which are then - converted back to datetime objects when rows are returned. - - """ - - __visit_name__ = 'datetime' - - def __init__(self, timezone=False): - """Construct a new :class:`.DateTime`. - - :param timezone: boolean. If True, and supported by the - backend, will produce 'TIMESTAMP WITH TIMEZONE'. For backends - that don't support timezone aware timestamps, has no - effect. - - """ - self.timezone = timezone - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.datetime - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add:{ - Interval:DateTime, - }, - operators.sub:{ - Interval:DateTime, - DateTime:Interval, - }, - } - - -class Date(_DateAffinity,TypeEngine): - """A type for ``datetime.date()`` objects.""" - - __visit_name__ = 'date' - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.date - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add:{ - Integer:Date, - Interval:DateTime, - Time:DateTime, - }, - operators.sub:{ - # date - integer = date - Integer:Date, - - # date - date = integer. - Date:Integer, - - Interval:DateTime, - - # date - datetime = interval, - # this one is not in the PG docs - # but works - DateTime:Interval, - }, - } - - -class Time(_DateAffinity,TypeEngine): - """A type for ``datetime.time()`` objects.""" - - __visit_name__ = 'time' - - def __init__(self, timezone=False): - self.timezone = timezone - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.time - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add:{ - Date:DateTime, - Interval:Time - }, - operators.sub:{ - Time:Interval, - Interval:Time, - }, - } - - -class _Binary(TypeEngine): - """Define base behavior for binary types.""" - - def __init__(self, length=None): - self.length = length - - @property - def python_type(self): - # Py3K - #return bytes - # Py2K - return str - # end Py2K - - # Python 3 - sqlite3 doesn't need the `Binary` conversion - # here, though pg8000 does to indicate "bytea" - def bind_processor(self, dialect): - DBAPIBinary = dialect.dbapi.Binary - def process(value): - x = self - if value is not None: - return DBAPIBinary(value) - else: - return None - return process - - # Python 3 has native bytes() type - # both sqlite3 and pg8000 seem to return it - # (i.e. and not 'memoryview') - # Py2K - def result_processor(self, dialect, coltype): - if util.jython: - def process(value): - if value is not None: - if isinstance(value, array.array): - return value.tostring() - return str(value) - else: - return None - else: - process = processors.to_str - return process - # end Py2K - - def _coerce_compared_value(self, op, value): - """See :meth:`.TypeEngine._coerce_compared_value` for a description.""" - - if isinstance(value, basestring): - return self - else: - return super(_Binary, self)._coerce_compared_value(op, value) - - def get_dbapi_type(self, dbapi): - return dbapi.BINARY - -class LargeBinary(_Binary): - """A type for large binary byte data. - - The Binary type generates BLOB or BYTEA when tables are created, - and also converts incoming values using the ``Binary`` callable - provided by each DB-API. - - """ - - __visit_name__ = 'large_binary' - - def __init__(self, length=None): - """ - Construct a LargeBinary type. - - :param length: optional, a length for the column for use in - DDL statements, for those BLOB types that accept a length - (i.e. MySQL). It does *not* produce a small BINARY/VARBINARY - type - use the BINARY/VARBINARY types specifically for those. - May be safely omitted if no ``CREATE - TABLE`` will be issued. Certain databases may require a - *length* for use in DDL, and will raise an exception when - the ``CREATE TABLE`` DDL is issued. - - """ - _Binary.__init__(self, length=length) - -class Binary(LargeBinary): - """Deprecated. Renamed to LargeBinary.""" - - def __init__(self, *arg, **kw): - util.warn_deprecated('The Binary type has been renamed to ' - 'LargeBinary.') - LargeBinary.__init__(self, *arg, **kw) - -class SchemaType(events.SchemaEventTarget): - """Mark a type as possibly requiring schema-level DDL for usage. - - Supports types that must be explicitly created/dropped (i.e. PG ENUM type) - as well as types that are complimented by table or schema level - constraints, triggers, and other rules. - - :class:`.SchemaType` classes can also be targets for the - :meth:`.DDLEvents.before_parent_attach` and :meth:`.DDLEvents.after_parent_attach` - events, where the events fire off surrounding the association of - the type object with a parent :class:`.Column`. - - """ - - def __init__(self, **kw): - self.name = kw.pop('name', None) - self.quote = kw.pop('quote', None) - self.schema = kw.pop('schema', None) - self.metadata = kw.pop('metadata', None) - if self.metadata: - event.listen( - self.metadata, - "before_create", - util.portable_instancemethod(self._on_metadata_create) - ) - event.listen( - self.metadata, - "after_drop", - util.portable_instancemethod(self._on_metadata_drop) - ) - - def _set_parent(self, column): - column._on_table_attach(util.portable_instancemethod(self._set_table)) - - def _set_table(self, column, table): - event.listen( - table, - "before_create", - util.portable_instancemethod( - self._on_table_create) - ) - event.listen( - table, - "after_drop", - util.portable_instancemethod(self._on_table_drop) - ) - if self.metadata is None: - # TODO: what's the difference between self.metadata - # and table.metadata here ? - event.listen( - table.metadata, - "before_create", - util.portable_instancemethod(self._on_metadata_create) - ) - event.listen( - table.metadata, - "after_drop", - util.portable_instancemethod(self._on_metadata_drop) - ) - - @property - def bind(self): - return self.metadata and self.metadata.bind or None - - def create(self, bind=None, checkfirst=False): - """Issue CREATE ddl for this type, if applicable.""" - - if bind is None: - bind = schema._bind_or_error(self) - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t.create(bind=bind, checkfirst=checkfirst) - - def drop(self, bind=None, checkfirst=False): - """Issue DROP ddl for this type, if applicable.""" - - if bind is None: - bind = schema._bind_or_error(self) - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t.drop(bind=bind, checkfirst=checkfirst) - - def _on_table_create(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_table_create(target, bind, **kw) - - def _on_table_drop(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_table_drop(target, bind, **kw) - - def _on_metadata_create(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_metadata_create(target, bind, **kw) - - def _on_metadata_drop(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_metadata_drop(target, bind, **kw) - -class Enum(String, SchemaType): - """Generic Enum Type. - - The Enum type provides a set of possible string values which the - column is constrained towards. - - By default, uses the backend's native ENUM type if available, - else uses VARCHAR + a CHECK constraint. - - See also: - - :class:`~.postgresql.ENUM` - PostgreSQL-specific type, - which has additional functionality. - - """ - - __visit_name__ = 'enum' - - def __init__(self, *enums, **kw): - """Construct an enum. - - Keyword arguments which don't apply to a specific backend are ignored - by that backend. - - :param \*enums: string or unicode enumeration labels. If unicode - labels are present, the `convert_unicode` flag is auto-enabled. - - :param convert_unicode: Enable unicode-aware bind parameter and - result-set processing for this Enum's data. This is set - automatically based on the presence of unicode label strings. - - :param metadata: Associate this type directly with a ``MetaData`` - object. For types that exist on the target database as an - independent schema construct (Postgresql), this type will be - created and dropped within ``create_all()`` and ``drop_all()`` - operations. If the type is not associated with any ``MetaData`` - object, it will associate itself with each ``Table`` in which it is - used, and will be created when any of those individual tables are - created, after a check is performed for it's existence. The type is - only dropped when ``drop_all()`` is called for that ``Table`` - object's metadata, however. - - :param name: The name of this type. This is required for Postgresql - and any future supported database which requires an explicitly - named type, or an explicitly named constraint in order to generate - the type and/or a table that uses it. - - :param native_enum: Use the database's native ENUM type when - available. Defaults to True. When False, uses VARCHAR + check - constraint for all backends. - - :param schema: Schemaname of this type. For types that exist on the - target database as an independent schema construct (Postgresql), - this parameter specifies the named schema in which the type is - present. - - :param quote: Force quoting to be on or off on the type's name. If - left as the default of `None`, the usual schema-level "case - sensitive"/"reserved name" rules are used to determine if this - type's name should be quoted. - - """ - self.enums = enums - self.native_enum = kw.pop('native_enum', True) - convert_unicode = kw.pop('convert_unicode', None) - if convert_unicode is None: - for e in enums: - if isinstance(e, unicode): - convert_unicode = True - break - else: - convert_unicode = False - - if self.enums: - length = max(len(x) for x in self.enums) - else: - length = 0 - String.__init__(self, - length=length, - convert_unicode=convert_unicode, - ) - SchemaType.__init__(self, **kw) - - def __repr__(self): - return util.generic_repr(self, [ - ("native_enum", True), - ("name", None) - ]) - - def _should_create_constraint(self, compiler): - return not self.native_enum or \ - not compiler.dialect.supports_native_enum - - def _set_table(self, column, table): - if self.native_enum: - SchemaType._set_table(self, column, table) - - - e = schema.CheckConstraint( - column.in_(self.enums), - name=self.name, - _create_rule=util.portable_instancemethod( - self._should_create_constraint) - ) - table.append_constraint(e) - - def adapt(self, impltype, **kw): - if issubclass(impltype, Enum): - return impltype(name=self.name, - quote=self.quote, - schema=self.schema, - metadata=self.metadata, - convert_unicode=self.convert_unicode, - native_enum=self.native_enum, - *self.enums, - **kw - ) - else: - return super(Enum, self).adapt(impltype, **kw) - -class PickleType(MutableType, TypeDecorator): - """Holds Python objects, which are serialized using pickle. - - PickleType builds upon the Binary type to apply Python's - ``pickle.dumps()`` to incoming objects, and ``pickle.loads()`` on - the way out, allowing any pickleable Python object to be stored as - a serialized binary field. - - """ - - impl = LargeBinary - - def __init__(self, protocol=pickle.HIGHEST_PROTOCOL, - pickler=None, mutable=False, comparator=None): - """ - Construct a PickleType. - - :param protocol: defaults to ``pickle.HIGHEST_PROTOCOL``. - - :param pickler: defaults to cPickle.pickle or pickle.pickle if - cPickle is not available. May be any object with - pickle-compatible ``dumps` and ``loads`` methods. - - :param mutable: defaults to False; implements - :meth:`AbstractType.is_mutable`. When ``True``, incoming - objects will be compared against copies of themselves - using the Python "equals" operator, unless the - ``comparator`` argument is present. See - :class:`.MutableType` for details on "mutable" type - behavior. - - .. versionchanged:: 0.7.0 - Default changed from ``True``. - - .. note:: - - This functionality is now superseded by the - ``sqlalchemy.ext.mutable`` extension described in - :ref:`mutable_toplevel`. - - :param comparator: a 2-arg callable predicate used - to compare values of this type. If left as ``None``, - the Python "equals" operator is used to compare values. - - """ - self.protocol = protocol - self.pickler = pickler or pickle - self.mutable = mutable - self.comparator = comparator - super(PickleType, self).__init__() - - def __reduce__(self): - return PickleType, (self.protocol, - None, - self.mutable, - self.comparator) - - def bind_processor(self, dialect): - impl_processor = self.impl.bind_processor(dialect) - dumps = self.pickler.dumps - protocol = self.protocol - if impl_processor: - def process(value): - if value is not None: - value = dumps(value, protocol) - return impl_processor(value) - else: - def process(value): - if value is not None: - value = dumps(value, protocol) - return value - return process - - def result_processor(self, dialect, coltype): - impl_processor = self.impl.result_processor(dialect, coltype) - loads = self.pickler.loads - if impl_processor: - def process(value): - value = impl_processor(value) - if value is None: - return None - return loads(value) - else: - def process(value): - if value is None: - return None - return loads(value) - return process - - def copy_value(self, value): - if self.mutable: - return self.pickler.loads( - self.pickler.dumps(value, self.protocol)) - else: - return value - - def compare_values(self, x, y): - if self.comparator: - return self.comparator(x, y) - else: - return x == y - - def is_mutable(self): - """Return True if the target Python type is 'mutable'. - - When this method is overridden, :meth:`copy_value` should - also be supplied. The :class:`.MutableType` mixin - is recommended as a helper. - - """ - return self.mutable - - -class Boolean(TypeEngine, SchemaType): - """A bool datatype. - - Boolean typically uses BOOLEAN or SMALLINT on the DDL side, and on - the Python side deals in ``True`` or ``False``. - - """ - - __visit_name__ = 'boolean' - - def __init__(self, create_constraint=True, name=None): - """Construct a Boolean. - - :param create_constraint: defaults to True. If the boolean - is generated as an int/smallint, also create a CHECK constraint - on the table that ensures 1 or 0 as a value. - - :param name: if a CHECK constraint is generated, specify - the name of the constraint. - - """ - self.create_constraint = create_constraint - self.name = name - - def _should_create_constraint(self, compiler): - return not compiler.dialect.supports_native_boolean - - def _set_table(self, column, table): - if not self.create_constraint: - return - - e = schema.CheckConstraint( - column.in_([0, 1]), - name=self.name, - _create_rule=util.portable_instancemethod( - self._should_create_constraint) - ) - table.append_constraint(e) - - @property - def python_type(self): - return bool - - def bind_processor(self, dialect): - if dialect.supports_native_boolean: - return None - else: - return processors.boolean_to_int - - def result_processor(self, dialect, coltype): - if dialect.supports_native_boolean: - return None - else: - return processors.int_to_boolean - -class Interval(_DateAffinity, TypeDecorator): - """A type for ``datetime.timedelta()`` objects. - - The Interval type deals with ``datetime.timedelta`` objects. In - PostgreSQL, the native ``INTERVAL`` type is used; for others, the - value is stored as a date which is relative to the "epoch" - (Jan. 1, 1970). - - Note that the ``Interval`` type does not currently provide date arithmetic - operations on platforms which do not support interval types natively. Such - operations usually require transformation of both sides of the expression - (such as, conversion of both sides into integer epoch values first) which - currently is a manual procedure (such as via - :attr:`~sqlalchemy.sql.expression.func`). - - """ - - impl = DateTime - epoch = dt.datetime.utcfromtimestamp(0) - - def __init__(self, native=True, - second_precision=None, - day_precision=None): - """Construct an Interval object. - - :param native: when True, use the actual - INTERVAL type provided by the database, if - supported (currently Postgresql, Oracle). - Otherwise, represent the interval data as - an epoch value regardless. - - :param second_precision: For native interval types - which support a "fractional seconds precision" parameter, - i.e. Oracle and Postgresql - - :param day_precision: for native interval types which - support a "day precision" parameter, i.e. Oracle. - - """ - super(Interval, self).__init__() - self.native = native - self.second_precision = second_precision - self.day_precision = day_precision - - def adapt(self, cls, **kw): - if self.native and hasattr(cls, '_adapt_from_generic_interval'): - return cls._adapt_from_generic_interval(self, **kw) - else: - return self.__class__( - native=self.native, - second_precision=self.second_precision, - day_precision=self.day_precision, - **kw) - - @property - def python_type(self): - return dt.timedelta - - def bind_processor(self, dialect): - impl_processor = self.impl.bind_processor(dialect) - epoch = self.epoch - if impl_processor: - def process(value): - if value is not None: - value = epoch + value - return impl_processor(value) - else: - def process(value): - if value is not None: - value = epoch + value - return value - return process - - def result_processor(self, dialect, coltype): - impl_processor = self.impl.result_processor(dialect, coltype) - epoch = self.epoch - if impl_processor: - def process(value): - value = impl_processor(value) - if value is None: - return None - return value - epoch - else: - def process(value): - if value is None: - return None - return value - epoch - return process - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add:{ - Date:DateTime, - Interval:Interval, - DateTime:DateTime, - Time:Time, - }, - operators.sub:{ - Interval:Interval - }, - operators.mul:{ - Numeric:Interval - }, - operators.truediv: { - Numeric:Interval - }, - # Py2K - operators.div: { - Numeric:Interval - } - # end Py2K - } - - @property - def _type_affinity(self): - return Interval - - def _coerce_compared_value(self, op, value): - """See :meth:`.TypeEngine._coerce_compared_value` for a description.""" - - return self.impl._coerce_compared_value(op, value) - - -class REAL(Float): - """The SQL REAL type.""" - - __visit_name__ = 'REAL' - -class FLOAT(Float): - """The SQL FLOAT type.""" - - __visit_name__ = 'FLOAT' - -class NUMERIC(Numeric): - """The SQL NUMERIC type.""" - - __visit_name__ = 'NUMERIC' - - -class DECIMAL(Numeric): - """The SQL DECIMAL type.""" - - __visit_name__ = 'DECIMAL' - - -class INTEGER(Integer): - """The SQL INT or INTEGER type.""" - - __visit_name__ = 'INTEGER' -INT = INTEGER - - -class SMALLINT(SmallInteger): - """The SQL SMALLINT type.""" - - __visit_name__ = 'SMALLINT' - - -class BIGINT(BigInteger): - """The SQL BIGINT type.""" - - __visit_name__ = 'BIGINT' - -class TIMESTAMP(DateTime): - """The SQL TIMESTAMP type.""" - - __visit_name__ = 'TIMESTAMP' - - def get_dbapi_type(self, dbapi): - return dbapi.TIMESTAMP - -class DATETIME(DateTime): - """The SQL DATETIME type.""" - - __visit_name__ = 'DATETIME' - - -class DATE(Date): - """The SQL DATE type.""" - - __visit_name__ = 'DATE' - - -class TIME(Time): - """The SQL TIME type.""" - - __visit_name__ = 'TIME' - -class TEXT(Text): - """The SQL TEXT type.""" - - __visit_name__ = 'TEXT' - -class CLOB(Text): - """The CLOB type. - - This type is found in Oracle and Informix. - """ - - __visit_name__ = 'CLOB' - -class VARCHAR(String): - """The SQL VARCHAR type.""" - - __visit_name__ = 'VARCHAR' - -class NVARCHAR(Unicode): - """The SQL NVARCHAR type.""" - - __visit_name__ = 'NVARCHAR' - -class CHAR(String): - """The SQL CHAR type.""" - - __visit_name__ = 'CHAR' - - -class NCHAR(Unicode): - """The SQL NCHAR type.""" - - __visit_name__ = 'NCHAR' - - -class BLOB(LargeBinary): - """The SQL BLOB type.""" - - __visit_name__ = 'BLOB' - -class BINARY(_Binary): - """The SQL BINARY type.""" - - __visit_name__ = 'BINARY' - -class VARBINARY(_Binary): - """The SQL VARBINARY type.""" - - __visit_name__ = 'VARBINARY' - - -class BOOLEAN(Boolean): - """The SQL BOOLEAN type.""" - - __visit_name__ = 'BOOLEAN' - -NULLTYPE = NullType() -BOOLEANTYPE = Boolean() -STRINGTYPE = String() - -_type_map = { - str: String(), - # Py3K - #bytes : LargeBinary(), - # Py2K - unicode : Unicode(), - # end Py2K - int : Integer(), - float : Numeric(), - bool: BOOLEANTYPE, - decimal.Decimal : Numeric(), - dt.date : Date(), - dt.datetime : DateTime(), - dt.time : Time(), - dt.timedelta : Interval(), - NoneType: NULLTYPE -} + 'CLOB', 'BLOB', 'BINARY', 'VARBINARY', 'BOOLEAN', 'BIGINT', + 'SMALLINT', 'INTEGER', 'DATE', 'TIME', 'String', 'Integer', + 'SmallInteger', 'BigInteger', 'Numeric', 'Float', 'DateTime', + 'Date', 'Time', 'LargeBinary', 'Binary', 'Boolean', 'Unicode', + 'Concatenable', 'UnicodeText', 'PickleType', 'Interval', 'Enum'] + +from .sql.type_api import ( + adapt_type, + TypeEngine, + TypeDecorator, + Variant, + to_instance, + UserDefinedType +) +from .sql.sqltypes import ( + BIGINT, + BINARY, + BLOB, + BOOLEAN, + BigInteger, + Binary, + _Binary, + Boolean, + CHAR, + CLOB, + Concatenable, + DATE, + DATETIME, + DECIMAL, + Date, + DateTime, + Enum, + FLOAT, + Float, + INT, + INTEGER, + Integer, + Interval, + LargeBinary, + NCHAR, + NVARCHAR, + NullType, + NULLTYPE, + NUMERIC, + Numeric, + PickleType, + REAL, + SchemaType, + SMALLINT, + SmallInteger, + String, + STRINGTYPE, + TEXT, + TIME, + TIMESTAMP, + Text, + Time, + Unicode, + UnicodeText, + VARBINARY, + VARCHAR, + _type_map + ) diff --git a/libs/sqlalchemy/util/__init__.py b/libs/sqlalchemy/util/__init__.py index 8cb4c65b..eba64ed1 100644 --- a/libs/sqlalchemy/util/__init__.py +++ b/libs/sqlalchemy/util/__init__.py @@ -1,34 +1,45 @@ # util/__init__.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from compat import callable, cmp, reduce, defaultdict, py25_dict, \ - threading, py3k_warning, jython, pypy, cpython, win32, set_types, buffer, \ - pickle, update_wrapper, partial, md5_hex, decode_slice, dottedgetter,\ - parse_qsl, any, contextmanager, next +from .compat import callable, cmp, reduce, \ + threading, py3k, py33, py2k, jython, pypy, cpython, win32, \ + pickle, dottedgetter, parse_qsl, namedtuple, next, reraise, \ + raise_from_cause, text_type, string_types, int_types, binary_type, \ + quote_plus, with_metaclass, print_, itertools_filterfalse, u, ue, b,\ + unquote_plus, unquote, b64decode, b64encode, byte_buffer, itertools_filter,\ + iterbytes, StringIO, inspect_getargspec -from _collections import NamedTuple, ImmutableContainer, immutabledict, \ +from ._collections import KeyedTuple, ImmutableContainer, immutabledict, \ Properties, OrderedProperties, ImmutableProperties, OrderedDict, \ OrderedSet, IdentitySet, OrderedIdentitySet, column_set, \ column_dict, ordered_column_set, populate_column_dict, unique_list, \ UniqueAppender, PopulateDict, EMPTY_SET, to_list, to_set, \ - to_column_set, update_copy, flatten_iterator, WeakIdentityMapping, \ - LRUCache, ScopedRegistry, ThreadLocalRegistry + to_column_set, update_copy, flatten_iterator, \ + LRUCache, ScopedRegistry, ThreadLocalRegistry, WeakSequence, \ + coerce_generator_arg -from langhelpers import iterate_attributes, class_hierarchy, \ +from .langhelpers import iterate_attributes, class_hierarchy, \ portable_instancemethod, unbound_method_to_callable, \ getargspec_init, format_argspec_init, format_argspec_plus, \ get_func_kwargs, get_cls_kwargs, decorator, as_interface, \ - memoized_property, memoized_instancemethod, \ - reset_memoized, group_expirable_memoized_property, importlater, \ + memoized_property, memoized_instancemethod, md5_hex, \ + group_expirable_memoized_property, dependencies, decode_slice, \ monkeypatch_proxied_specials, asbool, bool_or_str, coerce_kw_type,\ duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\ classproperty, set_creation_order, warn_exception, warn, NoneType,\ constructor_copy, methods_equivalent, chop_traceback, asint,\ - generic_repr, counter + generic_repr, counter, PluginLoader, hybridmethod, safe_reraise,\ + get_callable_argspec, only_once -from deprecations import warn_deprecated, warn_pending_deprecation, \ - deprecated, pending_deprecation +from .deprecations import warn_deprecated, warn_pending_deprecation, \ + deprecated, pending_deprecation, inject_docstring_text +# things that used to be not always available, +# but are now as of current support Python versions +from collections import defaultdict +from functools import partial +from functools import update_wrapper +from contextlib import contextmanager diff --git a/libs/sqlalchemy/util/_collections.py b/libs/sqlalchemy/util/_collections.py index 5a09dca6..c0a24ba4 100644 --- a/libs/sqlalchemy/util/_collections.py +++ b/libs/sqlalchemy/util/_collections.py @@ -1,44 +1,111 @@ # util/_collections.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Collection classes and helpers.""" -import sys -import itertools +from __future__ import absolute_import import weakref import operator -from langhelpers import symbol -from compat import time_func, threading +from .compat import threading, itertools_filterfalse +from . import py2k +import types EMPTY_SET = frozenset() -class NamedTuple(tuple): - """tuple() subclass that adds labeled names. +class KeyedTuple(tuple): + """``tuple`` subclass that adds labeled names. - Is also pickleable. + E.g.:: + + >>> k = KeyedTuple([1, 2, 3], labels=["one", "two", "three"]) + >>> k.one + 1 + >>> k.two + 2 + + Result rows returned by :class:`.Query` that contain multiple + ORM entities and/or column expressions make use of this + class to return rows. + + The :class:`.KeyedTuple` exhibits similar behavior to the + ``collections.namedtuple()`` construct provided in the Python + standard library, however is architected very differently. + Unlike ``collections.namedtuple()``, :class:`.KeyedTuple` is + does not rely on creation of custom subtypes in order to represent + a new series of keys, instead each :class:`.KeyedTuple` instance + receives its list of keys in place. The subtype approach + of ``collections.namedtuple()`` introduces significant complexity + and performance overhead, which is not necessary for the + :class:`.Query` object's use case. + + .. versionchanged:: 0.8 + Compatibility methods with ``collections.namedtuple()`` have been + added including :attr:`.KeyedTuple._fields` and + :meth:`.KeyedTuple._asdict`. + + .. seealso:: + + :ref:`ormtutorial_querying` """ def __new__(cls, vals, labels=None): t = tuple.__new__(cls, vals) + t._labels = [] if labels: t.__dict__.update(zip(labels, vals)) t._labels = labels return t def keys(self): + """Return a list of string key names for this :class:`.KeyedTuple`. + + .. seealso:: + + :attr:`.KeyedTuple._fields` + + """ + return [l for l in self._labels if l is not None] + @property + def _fields(self): + """Return a tuple of string key names for this :class:`.KeyedTuple`. + + This method provides compatibility with ``collections.namedtuple()``. + + .. versionadded:: 0.8 + + .. seealso:: + + :meth:`.KeyedTuple.keys` + + """ + return tuple(self.keys()) + + def _asdict(self): + """Return the contents of this :class:`.KeyedTuple` as a dictionary. + + This method provides compatibility with ``collections.namedtuple()``, + with the exception that the dictionary returned is **not** ordered. + + .. versionadded:: 0.8 + + """ + return dict((key, self.__dict__[key]) for key in self.keys()) + + class ImmutableContainer(object): def _immutable(self, *arg, **kw): raise TypeError("%s object is immutable" % self.__class__.__name__) __delitem__ = __setitem__ = __setattr__ = _immutable + class immutabledict(ImmutableContainer, dict): clear = pop = popitem = setdefault = \ @@ -66,6 +133,7 @@ class immutabledict(ImmutableContainer, dict): def __repr__(self): return "immutabledict(%s)" % dict.__repr__(self) + class Properties(object): """Provide a __getattr__/__setattr__ interface over a dict.""" @@ -76,7 +144,7 @@ class Properties(object): return len(self._data) def __iter__(self): - return self._data.itervalues() + return iter(list(self._data.values())) def __add__(self, other): return list(self) + list(other) @@ -123,7 +191,13 @@ class Properties(object): return default def keys(self): - return self._data.keys() + return list(self._data) + + def values(self): + return list(self._data.values()) + + def items(self): + return list(self._data.items()) def has_key(self, key): return key in self._data @@ -131,6 +205,7 @@ class Properties(object): def clear(self): self._data.clear() + class OrderedProperties(Properties): """Provide a __getattr__/__setattr__ interface with an OrderedDict as backing store.""" @@ -187,23 +262,55 @@ class OrderedDict(dict): def __iter__(self): return iter(self._list) - def values(self): - return [self[key] for key in self._list] - def itervalues(self): - return iter([self[key] for key in self._list]) + if py2k: + def values(self): + return [self[key] for key in self._list] - def keys(self): - return list(self._list) + def keys(self): + return self._list - def iterkeys(self): - return iter(self.keys()) + def itervalues(self): + return iter([self[key] for key in self._list]) - def items(self): - return [(key, self[key]) for key in self.keys()] + def iterkeys(self): + return iter(self) + + def iteritems(self): + return iter(self.items()) + + def items(self): + return [(key, self[key]) for key in self._list] + else: + def values(self): + #return (self[key] for key in self) + return (self[key] for key in self._list) + + def keys(self): + #return iter(self) + return iter(self._list) + + def items(self): + #return ((key, self[key]) for key in self) + return ((key, self[key]) for key in self._list) + + _debug_iter = False + if _debug_iter: + # normally disabled to reduce function call + # overhead + def __iter__(self): + len_ = len(self._list) + for item in self._list: + yield item + assert len_ == len(self._list), \ + "Dictionary changed size during iteration" + def values(self): + return (self[key] for key in self) + def keys(self): + return iter(self) + def items(self): + return ((key, self[key]) for key in self) - def iteritems(self): - return iter(self.items()) def __setitem__(self, key, object): if key not in self: @@ -231,6 +338,7 @@ class OrderedDict(dict): self._list.remove(item[0]) return item + class OrderedSet(set): def __init__(self, d=None): set.__init__(self) @@ -314,22 +422,22 @@ class OrderedSet(set): def intersection_update(self, other): other = set(other) set.intersection_update(self, other) - self._list = [ a for a in self._list if a in other] + self._list = [a for a in self._list if a in other] return self __iand__ = intersection_update def symmetric_difference_update(self, other): set.symmetric_difference_update(self, other) - self._list = [ a for a in self._list if a in self] - self._list += [ a for a in other._list if a in self] + self._list = [a for a in self._list if a in self] + self._list += [a for a in other._list if a in self] return self __ixor__ = symmetric_difference_update def difference_update(self, other): set.difference_update(self, other) - self._list = [ a for a in self._list if a in self] + self._list = [a for a in self._list if a in self] return self __isub__ = difference_update @@ -376,9 +484,6 @@ class IdentitySet(object): def clear(self): self._members.clear() - def __sub__(self, other): - return self.difference(other) - def __cmp__(self, other): raise TypeError('cannot compare sets using cmp()') @@ -399,8 +504,8 @@ class IdentitySet(object): if len(self) > len(other): return False - for m in itertools.ifilterfalse(other._members.__contains__, - self._members.iterkeys()): + for m in itertools_filterfalse(other._members.__contains__, + iter(self._members.keys())): return False return True @@ -420,8 +525,8 @@ class IdentitySet(object): if len(self) < len(other): return False - for m in itertools.ifilterfalse(self._members.__contains__, - other._members.iterkeys()): + for m in itertools_filterfalse(self._members.__contains__, + iter(other._members.keys())): return False return True @@ -438,8 +543,9 @@ class IdentitySet(object): def union(self, iterable): result = type(self)() # testlib.pragma exempt:__hash__ - result._members.update( - self._working_set(self._member_id_tuples()).union(_iter_id(iterable))) + members = self._member_id_tuples() + other = _iter_id(iterable) + result._members.update(self._working_set(members).union(other)) return result def __or__(self, other): @@ -459,8 +565,9 @@ class IdentitySet(object): def difference(self, iterable): result = type(self)() # testlib.pragma exempt:__hash__ - result._members.update( - self._working_set(self._member_id_tuples()).difference(_iter_id(iterable))) + members = self._member_id_tuples() + other = _iter_id(iterable) + result._members.update(self._working_set(members).difference(other)) return result def __sub__(self, other): @@ -480,8 +587,9 @@ class IdentitySet(object): def intersection(self, iterable): result = type(self)() # testlib.pragma exempt:__hash__ - result._members.update( - self._working_set(self._member_id_tuples()).intersection(_iter_id(iterable))) + members = self._member_id_tuples() + other = _iter_id(iterable) + result._members.update(self._working_set(members).intersection(other)) return result def __and__(self, other): @@ -501,12 +609,14 @@ class IdentitySet(object): def symmetric_difference(self, iterable): result = type(self)() # testlib.pragma exempt:__hash__ + members = self._member_id_tuples() + other = _iter_id(iterable) result._members.update( - self._working_set(self._member_id_tuples()).symmetric_difference(_iter_id(iterable))) + self._working_set(members).symmetric_difference(other)) return result def _member_id_tuples(self): - return ((id(v), v) for v in self._members.itervalues()) + return ((id(v), v) for v in self._members.values()) def __xor__(self, other): if not isinstance(other, IdentitySet): @@ -523,7 +633,7 @@ class IdentitySet(object): return self def copy(self): - return type(self)(self._members.itervalues()) + return type(self)(iter(self._members.values())) __copy__ = copy @@ -531,13 +641,41 @@ class IdentitySet(object): return len(self._members) def __iter__(self): - return self._members.itervalues() + return iter(self._members.values()) def __hash__(self): raise TypeError('set objects are unhashable') def __repr__(self): - return '%s(%r)' % (type(self).__name__, self._members.values()) + return '%s(%r)' % (type(self).__name__, list(self._members.values())) + + +class WeakSequence(object): + def __init__(self, __elements=()): + self._storage = [ + weakref.ref(element, self._remove) for element in __elements + ] + + def append(self, item): + self._storage.append(weakref.ref(item, self._remove)) + + def _remove(self, ref): + self._storage.remove(ref) + + def __len__(self): + return len(self._storage) + + def __iter__(self): + return (obj for obj in + (ref() for ref in self._storage) if obj is not None) + + def __getitem__(self, index): + try: + obj = self._storage[index] + except KeyError: + raise IndexError("Index %s out of range" % index) + else: + return obj() class OrderedIdentitySet(IdentitySet): @@ -556,37 +694,25 @@ class OrderedIdentitySet(IdentitySet): self.add(o) -if sys.version_info >= (2, 5): - class PopulateDict(dict): - """A dict which populates missing values via a creation function. +class PopulateDict(dict): + """A dict which populates missing values via a creation function. - Note the creation function takes a key, unlike - collections.defaultdict. + Note the creation function takes a key, unlike + collections.defaultdict. - """ + """ - def __init__(self, creator): - self.creator = creator + def __init__(self, creator): + self.creator = creator - def __missing__(self, key): - self[key] = val = self.creator(key) - return val -else: - class PopulateDict(dict): - """A dict which populates missing values via a creation function.""" + def __missing__(self, key): + self[key] = val = self.creator(key) + return val - def __init__(self, creator): - self.creator = creator - - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - self[key] = value = self.creator(key) - return value - -# define collections that are capable of storing +# Define collections that are capable of storing # ColumnElement objects as hashable keys/elements. +# At this point, these are mostly historical, things +# used to be more complicated. column_set = set column_dict = dict ordered_column_set = OrderedSet @@ -603,6 +729,7 @@ def unique_list(seq, hashfunc=None): if hashfunc(x) not in seen and not seen.__setitem__(hashfunc(x), True)] + class UniqueAppender(object): """Appends items to a collection ensuring uniqueness. @@ -629,6 +756,12 @@ class UniqueAppender(object): def __iter__(self): return iter(self.data) +def coerce_generator_arg(arg): + if len(arg) == 1 and isinstance(arg[0], types.GeneratorType): + return list(arg[0]) + else: + return arg + def to_list(x, default=None): if x is None: return default @@ -637,6 +770,7 @@ def to_list(x, default=None): else: return x + def to_set(x): if x is None: return set() @@ -645,6 +779,7 @@ def to_set(x): else: return x + def to_column_set(x): if x is None: return column_set() @@ -653,6 +788,7 @@ def to_column_set(x): else: return x + def update_copy(d, _new=None, **kw): """Copy the given dict and update with the given values.""" @@ -662,104 +798,19 @@ def update_copy(d, _new=None, **kw): d.update(**kw) return d + def flatten_iterator(x): """Given an iterator of which further sub-elements may also be iterators, flatten the sub-elements into a single iterator. """ for elem in x: - if not isinstance(elem, basestring) and hasattr(elem, '__iter__'): + if not isinstance(elem, str) and hasattr(elem, '__iter__'): for y in flatten_iterator(elem): yield y else: yield elem -class WeakIdentityMapping(weakref.WeakKeyDictionary): - """A WeakKeyDictionary with an object identity index. - - Adds a .by_id dictionary to a regular WeakKeyDictionary. Trades - performance during mutation operations for accelerated lookups by id(). - - The usual cautions about weak dictionaries and iteration also apply to - this subclass. - - """ - _none = symbol('none') - - def __init__(self): - weakref.WeakKeyDictionary.__init__(self) - self.by_id = {} - self._weakrefs = {} - - def __setitem__(self, object, value): - oid = id(object) - self.by_id[oid] = value - if oid not in self._weakrefs: - self._weakrefs[oid] = self._ref(object) - weakref.WeakKeyDictionary.__setitem__(self, object, value) - - def __delitem__(self, object): - del self._weakrefs[id(object)] - del self.by_id[id(object)] - weakref.WeakKeyDictionary.__delitem__(self, object) - - def setdefault(self, object, default=None): - value = weakref.WeakKeyDictionary.setdefault(self, object, default) - oid = id(object) - if value is default: - self.by_id[oid] = default - if oid not in self._weakrefs: - self._weakrefs[oid] = self._ref(object) - return value - - def pop(self, object, default=_none): - if default is self._none: - value = weakref.WeakKeyDictionary.pop(self, object) - else: - value = weakref.WeakKeyDictionary.pop(self, object, default) - if id(object) in self.by_id: - del self._weakrefs[id(object)] - del self.by_id[id(object)] - return value - - def popitem(self): - item = weakref.WeakKeyDictionary.popitem(self) - oid = id(item[0]) - del self._weakrefs[oid] - del self.by_id[oid] - return item - - def clear(self): - # Py2K - # in 3k, MutableMapping calls popitem() - self._weakrefs.clear() - self.by_id.clear() - # end Py2K - weakref.WeakKeyDictionary.clear(self) - - def update(self, *a, **kw): - raise NotImplementedError - - def _cleanup(self, wr, key=None): - if key is None: - key = wr.key - try: - del self._weakrefs[key] - except (KeyError, AttributeError): # pragma: no cover - pass # pragma: no cover - try: - del self.by_id[key] - except (KeyError, AttributeError): # pragma: no cover - pass # pragma: no cover - - class _keyed_weakref(weakref.ref): - def __init__(self, object, callback): - weakref.ref.__init__(self, object, callback) - self.key = id(object) - - def _ref(self, object): - return self._keyed_weakref(object, self._cleanup) - class LRUCache(dict): """Dictionary with 'squishy' removal of least @@ -869,6 +920,7 @@ class ScopedRegistry(object): except KeyError: pass + class ThreadLocalRegistry(ScopedRegistry): """A :class:`.ScopedRegistry` that uses a ``threading.local()`` variable for storage. @@ -897,9 +949,9 @@ class ThreadLocalRegistry(ScopedRegistry): except AttributeError: pass + def _iter_id(iterable): """Generator: ((id(o), o) for o in iterable).""" for item in iterable: yield id(item), item - diff --git a/libs/sqlalchemy/util/compat.py b/libs/sqlalchemy/util/compat.py index 18ea2815..f1346406 100644 --- a/libs/sqlalchemy/util/compat.py +++ b/libs/sqlalchemy/util/compat.py @@ -1,5 +1,5 @@ # util/compat.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,45 +8,24 @@ import sys - try: import threading except ImportError: import dummy_threading as threading +py33 = sys.version_info >= (3, 3) py32 = sys.version_info >= (3, 2) -py3k_warning = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0) +py3k = sys.version_info >= (3, 0) +py2k = sys.version_info < (3, 0) jython = sys.platform.startswith('java') pypy = hasattr(sys, 'pypy_version_info') win32 = sys.platform.startswith('win') cpython = not pypy and not jython # TODO: something better for this ? -if py3k_warning: - set_types = set -elif sys.version_info < (2, 6): - import sets - set_types = set, sets.Set -else: - # 2.6 deprecates sets.Set, but we still need to be able to detect them - # in user code and as return values from DB-APIs - ignore = ('ignore', None, DeprecationWarning, None, 0) - import warnings - try: - warnings.filters.insert(0, ignore) - except Exception: - import sets - else: - import sets - warnings.filters.remove(ignore) +import collections +next = next - set_types = set, sets.Set - -if sys.version_info < (2, 6): - def next(iter): - return iter.next() -else: - next = next -if py3k_warning: +if py3k: import pickle else: try: @@ -54,146 +33,121 @@ else: except ImportError: import pickle -# a controversial feature, required by MySQLdb currently -def buffer(x): - return x +ArgSpec = collections.namedtuple("ArgSpec", + ["args", "varargs", "keywords", "defaults"]) -# Py2K -buffer = buffer -# end Py2K +if py3k: + import builtins -try: - from contextlib import contextmanager -except ImportError: - def contextmanager(fn): - return fn + from inspect import getfullargspec as inspect_getfullargspec + from urllib.parse import quote_plus, unquote_plus, parse_qsl, quote, unquote + import configparser + from io import StringIO -try: - from functools import update_wrapper -except ImportError: - def update_wrapper(wrapper, wrapped, - assigned=('__doc__', '__module__', '__name__'), - updated=('__dict__',)): - for attr in assigned: - setattr(wrapper, attr, getattr(wrapped, attr)) - for attr in updated: - getattr(wrapper, attr).update(getattr(wrapped, attr, ())) - return wrapper + from io import BytesIO as byte_buffer -try: - from functools import partial -except ImportError: - def partial(func, *args, **keywords): - def newfunc(*fargs, **fkeywords): - newkeywords = keywords.copy() - newkeywords.update(fkeywords) - return func(*(args + fargs), **newkeywords) - return newfunc + def inspect_getargspec(func): + return ArgSpec( + *inspect_getfullargspec(func)[0:4] + ) + string_types = str, + binary_type = bytes + text_type = str + int_types = int, + iterbytes = iter -if sys.version_info < (2, 6): - # emits a nasty deprecation warning - # in newer pythons - from cgi import parse_qsl -else: - from urlparse import parse_qsl + def u(s): + return s -# Py3K -#from inspect import getfullargspec as inspect_getfullargspec -# Py2K -from inspect import getargspec as inspect_getfullargspec -# end Py2K + def ue(s): + return s + + def b(s): + return s.encode("latin-1") + + if py32: + callable = callable + else: + def callable(fn): + return hasattr(fn, '__call__') -if py3k_warning: - # they're bringing it back in 3.2. brilliant ! - def callable(fn): - return hasattr(fn, '__call__') def cmp(a, b): return (a > b) - (a < b) from functools import reduce + + print_ = getattr(builtins, "print") + + import_ = getattr(builtins, '__import__') + + import itertools + itertools_filterfalse = itertools.filterfalse + itertools_filter = filter + itertools_imap = map + + import base64 + def b64encode(x): + return base64.b64encode(x).decode('ascii') + def b64decode(x): + return base64.b64decode(x.encode('ascii')) + else: + from inspect import getargspec as inspect_getfullargspec + inspect_getargspec = inspect_getfullargspec + from urllib import quote_plus, unquote_plus, quote, unquote + from urlparse import parse_qsl + import ConfigParser as configparser + from StringIO import StringIO + from cStringIO import StringIO as byte_buffer + + string_types = basestring, + binary_type = str + text_type = unicode + int_types = int, long + def iterbytes(buf): + return (ord(byte) for byte in buf) + + def u(s): + # this differs from what six does, which doesn't support non-ASCII + # strings - we only use u() with + # literal source strings, and all our source files with non-ascii + # in them (all are tests) are utf-8 encoded. + return unicode(s, "utf-8") + + def ue(s): + return unicode(s, "unicode_escape") + + def b(s): + return s + + def import_(*args): + if len(args) == 4: + args = args[0:3] + ([str(arg) for arg in args[3]],) + return __import__(*args) + callable = callable cmp = cmp reduce = reduce -try: - from collections import defaultdict -except ImportError: - class defaultdict(dict): - def __init__(self, default_factory=None, *a, **kw): - if (default_factory is not None and - not hasattr(default_factory, '__call__')): - raise TypeError('first argument must be callable') - dict.__init__(self, *a, **kw) - self.default_factory = default_factory - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - return self.__missing__(key) - def __missing__(self, key): - if self.default_factory is None: - raise KeyError(key) - self[key] = value = self.default_factory() - return value - def __reduce__(self): - if self.default_factory is None: - args = tuple() - else: - args = self.default_factory, - return type(self), args, None, None, self.iteritems() - def copy(self): - return self.__copy__() - def __copy__(self): - return type(self)(self.default_factory, self) - def __deepcopy__(self, memo): - import copy - return type(self)(self.default_factory, - copy.deepcopy(self.items())) - def __repr__(self): - return 'defaultdict(%s, %s)' % (self.default_factory, - dict.__repr__(self)) + import base64 + b64encode = base64.b64encode + b64decode = base64.b64decode + def print_(*args, **kwargs): + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + for arg in enumerate(args): + if not isinstance(arg, basestring): + arg = str(arg) + fp.write(arg) -# find or create a dict implementation that supports __missing__ -class _probe(dict): - def __missing__(self, key): - return 1 + import itertools + itertools_filterfalse = itertools.ifilterfalse + itertools_filter = itertools.ifilter + itertools_imap = itertools.imap -try: - try: - _probe()['missing'] - py25_dict = dict - except KeyError: - class py25_dict(dict): - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - try: - missing = self.__missing__ - except AttributeError: - raise KeyError(key) - else: - return missing(key) -finally: - del _probe - - -try: - import hashlib - _md5 = hashlib.md5 -except ImportError: - import md5 - _md5 = md5.new - -def md5_hex(x): - # Py3K - #x = x.encode('utf-8') - m = _md5() - m.update(x) - return m.hexdigest() import time if win32 or jython: @@ -201,43 +155,61 @@ if win32 or jython: else: time_func = time.time -if sys.version_info >= (2, 5): - any = any +from collections import namedtuple +from operator import attrgetter as dottedgetter + + +if py3k: + def reraise(tp, value, tb=None, cause=None): + if cause is not None: + value.__cause__ = cause + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + def raise_from_cause(exception, exc_info=None): + if exc_info is None: + exc_info = sys.exc_info() + exc_type, exc_value, exc_tb = exc_info + reraise(type(exception), exception, tb=exc_tb, cause=exc_value) else: - def any(iterator): - for item in iterator: - if bool(item): - return True + exec("def reraise(tp, value, tb=None, cause=None):\n" + " raise tp, value, tb\n") + + def raise_from_cause(exception, exc_info=None): + # not as nice as that of Py3K, but at least preserves + # the code line where the issue occurred + if exc_info is None: + exc_info = sys.exc_info() + exc_type, exc_value, exc_tb = exc_info + reraise(type(exception), exception, tb=exc_tb) + +if py3k: + exec_ = getattr(builtins, 'exec') +else: + def exec_(func_text, globals_, lcl=None): + if lcl is None: + exec('exec func_text in globals_') else: - return False - -if sys.version_info >= (2, 5): - def decode_slice(slc): - """decode a slice object as sent to __getitem__. - - takes into account the 2.5 __index__() method, basically. - - """ - ret = [] - for x in slc.start, slc.stop, slc.step: - if hasattr(x, '__index__'): - x = x.__index__() - ret.append(x) - return tuple(ret) -else: - def decode_slice(slc): - return (slc.start, slc.stop, slc.step) - -if sys.version_info >= (2, 6): - from operator import attrgetter as dottedgetter -else: - def dottedgetter(attr): - def g(obj): - for name in attr.split("."): - obj = getattr(obj, name) - return obj - return g + exec('exec func_text in globals_, lcl') -import decimal +def with_metaclass(meta, *bases): + """Create a base class with a metaclass. + + Drops the middle class upon creation. + + Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ + + """ + + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + diff --git a/libs/sqlalchemy/util/deprecations.py b/libs/sqlalchemy/util/deprecations.py index 330d35db..c8854dc3 100644 --- a/libs/sqlalchemy/util/deprecations.py +++ b/libs/sqlalchemy/util/deprecations.py @@ -1,5 +1,5 @@ # util/deprecations.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -7,17 +7,20 @@ """Helpers related to deprecation of functions, methods, classes, other functionality.""" -from sqlalchemy import exc +from .. import exc import warnings import re -from langhelpers import decorator +from .langhelpers import decorator + def warn_deprecated(msg, stacklevel=3): warnings.warn(msg, exc.SADeprecationWarning, stacklevel=stacklevel) + def warn_pending_deprecation(msg, stacklevel=3): warnings.warn(msg, exc.SAPendingDeprecationWarning, stacklevel=stacklevel) + def deprecated(version, message=None, add_deprecation_to_docstring=True): """Decorates a function and issues a deprecation warning on use. @@ -47,6 +50,7 @@ def deprecated(version, message=None, add_deprecation_to_docstring=True): message % dict(func=fn.__name__), header) return decorate + def pending_deprecation(version, message=None, add_deprecation_to_docstring=True): """Decorates a function and issues a pending deprecation warning on use. @@ -80,6 +84,7 @@ def pending_deprecation(version, message=None, message % dict(func=fn.__name__), header) return decorate + def _sanitize_restructured_text(text): def repl(m): type_, name = m.group(1, 2) @@ -102,17 +107,37 @@ def _decorate_with_warning(func, wtype, message, docstring_header=None): doc = func.__doc__ is not None and func.__doc__ or '' if docstring_header is not None: docstring_header %= dict(func=func.__name__) - docs = doc and doc.expandtabs().split('\n') or [] - indent = '' - for line in docs[1:]: - text = line.lstrip() - if text: - indent = line[0:len(line) - len(text)] - break - point = min(len(docs), 1) - docs.insert(point, '\n' + indent + docstring_header.rstrip()) - doc = '\n'.join(docs) + + doc = inject_docstring_text(doc, docstring_header, 1) decorated = warned(func) decorated.__doc__ = doc return decorated + +import textwrap + +def _dedent_docstring(text): + split_text = text.split("\n", 1) + if len(split_text) == 1: + return text + else: + firstline, remaining = split_text + if not firstline.startswith(" "): + return firstline + "\n" + textwrap.dedent(remaining) + else: + return textwrap.dedent(text) + +def inject_docstring_text(doctext, injecttext, pos): + doctext = _dedent_docstring(doctext or "") + lines = doctext.split('\n') + injectlines = textwrap.dedent(injecttext).split("\n") + if injectlines[0]: + injectlines.insert(0, "") + + blanks = [num for num, line in enumerate(lines) if not line.strip()] + blanks.insert(0, 0) + + inject_pos = blanks[min(pos, len(blanks) - 1)] + + lines = lines[0:inject_pos] + injectlines + lines[inject_pos:] + return "\n".join(lines) diff --git a/libs/sqlalchemy/util/langhelpers.py b/libs/sqlalchemy/util/langhelpers.py index b7c5132d..b6ca7eb2 100644 --- a/libs/sqlalchemy/util/langhelpers.py +++ b/libs/sqlalchemy/util/langhelpers.py @@ -1,5 +1,5 @@ # util/langhelpers.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -15,15 +15,69 @@ import re import sys import types import warnings -from compat import update_wrapper, set_types, threading, callable, inspect_getfullargspec, py3k_warning -from sqlalchemy import exc +from functools import update_wrapper +from .. import exc +import hashlib +from . import compat +from . import _collections + +def md5_hex(x): + if compat.py3k: + x = x.encode('utf-8') + m = hashlib.md5() + m.update(x) + return m.hexdigest() + +class safe_reraise(object): + """Reraise an exception after invoking some + handler code. + + Stores the existing exception info before + invoking so that it is maintained across a potential + coroutine context switch. + + e.g.:: + + try: + sess.commit() + except: + with safe_reraise(): + sess.rollback() + + """ + + def __enter__(self): + self._exc_info = sys.exc_info() + + def __exit__(self, type_, value, traceback): + # see #2703 for notes + if type_ is None: + exc_type, exc_value, exc_tb = self._exc_info + self._exc_info = None # remove potential circular references + compat.reraise(exc_type, exc_value, exc_tb) + else: + self._exc_info = None # remove potential circular references + compat.reraise(type_, value, traceback) + +def decode_slice(slc): + """decode a slice object as sent to __getitem__. + + takes into account the 2.5 __index__() method, basically. + + """ + ret = [] + for x in slc.start, slc.stop, slc.step: + if hasattr(x, '__index__'): + x = x.__index__() + ret.append(x) + return tuple(ret) def _unique_symbols(used, *bases): used = set(used) for base in bases: pool = itertools.chain((base,), - itertools.imap(lambda i: base + str(i), - xrange(1000))) + compat.itertools_imap(lambda i: base + str(i), + range(1000))) for sym in pool: if sym not in used: used.add(sym) @@ -32,28 +86,115 @@ def _unique_symbols(used, *bases): else: raise NameError("exhausted namespace for symbol base %s" % base) + def decorator(target): """A signature-matching decorator factory.""" def decorate(fn): if not inspect.isfunction(fn): raise Exception("not a decoratable function") - spec = inspect_getfullargspec(fn) - names = tuple(spec[0]) + spec[1:3] + (fn.func_name,) + spec = compat.inspect_getfullargspec(fn) + names = tuple(spec[0]) + spec[1:3] + (fn.__name__,) targ_name, fn_name = _unique_symbols(names, 'target', 'fn') metadata = dict(target=targ_name, fn=fn_name) metadata.update(format_argspec_plus(spec, grouped=False)) - - code = 'lambda %(args)s: %(target)s(%(fn)s, %(apply_kw)s)' % ( - metadata) - decorated = eval(code, {targ_name:target, fn_name:fn}) - decorated.func_defaults = getattr(fn, 'im_func', fn).func_defaults + metadata['name'] = fn.__name__ + code = """\ +def %(name)s(%(args)s): + return %(target)s(%(fn)s, %(apply_kw)s) +""" % metadata + decorated = _exec_code_in_env(code, + {targ_name: target, fn_name: fn}, + fn.__name__) + decorated.__defaults__ = getattr(fn, 'im_func', fn).__defaults__ return update_wrapper(decorated, fn) return update_wrapper(decorate, target) +def _exec_code_in_env(code, env, fn_name): + exec(code, env) + return env[fn_name] -def get_cls_kwargs(cls): +def public_factory(target, location): + """Produce a wrapping function for the given cls or classmethod. + + Rationale here is so that the __init__ method of the + class can serve as documentation for the function. + + """ + if isinstance(target, type): + fn = target.__init__ + callable_ = target + doc = "Construct a new :class:`.%s` object. \n\n"\ + "This constructor is mirrored as a public API function; see :func:`~%s` "\ + "for a full usage and argument description." % ( + target.__name__, location, ) + else: + fn = callable_ = target + doc = "This function is mirrored; see :func:`~%s` "\ + "for a description of arguments." % location + + location_name = location.split(".")[-1] + spec = compat.inspect_getfullargspec(fn) + del spec[0][0] + metadata = format_argspec_plus(spec, grouped=False) + metadata['name'] = location_name + code = """\ +def %(name)s(%(args)s): + return cls(%(apply_kw)s) +""" % metadata + env = {'cls': callable_, 'symbol': symbol} + exec(code, env) + decorated = env[location_name] + decorated.__doc__ = fn.__doc__ + if compat.py2k or hasattr(fn, '__func__'): + fn.__func__.__doc__ = doc + else: + fn.__doc__ = doc + return decorated + + +class PluginLoader(object): + + def __init__(self, group, auto_fn=None): + self.group = group + self.impls = {} + self.auto_fn = auto_fn + + def load(self, name): + if name in self.impls: + return self.impls[name]() + + if self.auto_fn: + loader = self.auto_fn(name) + if loader: + self.impls[name] = loader + return loader() + + try: + import pkg_resources + except ImportError: + pass + else: + for impl in pkg_resources.iter_entry_points( + self.group, name): + self.impls[name] = impl.load + return impl.load() + + raise exc.ArgumentError( + "Can't load plugin: %s:%s" % + (self.group, name)) + + def register(self, name, modulepath, objname): + def load(): + mod = compat.import_(modulepath) + for token in modulepath.split(".")[1:]: + mod = getattr(mod, token) + return getattr(mod, objname) + self.impls[name] = load + + +def get_cls_kwargs(cls, _set=None): """Return the full set of inherited kwargs for the given `cls`. Probes a class's __init__ method, collecting all named arguments. If the @@ -65,48 +206,50 @@ def get_cls_kwargs(cls): No anonymous tuple arguments please ! """ + toplevel = _set == None + if toplevel: + _set = set() - for c in cls.__mro__: - if '__init__' in c.__dict__: - stack = set([c]) - break - else: - return [] + ctr = cls.__dict__.get('__init__', False) - args = set() - while stack: - class_ = stack.pop() - ctr = class_.__dict__.get('__init__', False) - if (not ctr or - not isinstance(ctr, types.FunctionType) or - not isinstance(ctr.func_code, types.CodeType)): - stack.update(class_.__bases__) - continue - - # this is shorthand for - # names, _, has_kw, _ = inspect.getargspec(ctr) + has_init = ctr and isinstance(ctr, types.FunctionType) and \ + isinstance(ctr.__code__, types.CodeType) + if has_init: names, has_kw = inspect_func_args(ctr) - args.update(names) - if has_kw: - stack.update(class_.__bases__) - args.discard('self') - return args + _set.update(names) + + if not has_kw and not toplevel: + return None + + if not has_init or has_kw: + for c in cls.__bases__: + if get_cls_kwargs(c, _set) is None: + break + + _set.discard('self') + return _set + + try: + # TODO: who doesn't have this constant? from inspect import CO_VARKEYWORDS + def inspect_func_args(fn): - co = fn.func_code + co = fn.__code__ nargs = co.co_argcount names = co.co_varnames args = list(names[:nargs]) has_kw = bool(co.co_flags & CO_VARKEYWORDS) return args, has_kw + except ImportError: def inspect_func_args(fn): names, _, has_kw, _ = inspect.getargspec(fn) return names, bool(has_kw) + def get_func_kwargs(func): """Return the set of legal kwargs for the given `func`. @@ -115,7 +258,22 @@ def get_func_kwargs(func): """ - return inspect.getargspec(func)[0] + return compat.inspect_getargspec(func)[0] + +def get_callable_argspec(fn, no_self=False): + if isinstance(fn, types.FunctionType): + return compat.inspect_getargspec(fn) + elif isinstance(fn, types.MethodType) and no_self: + spec = compat.inspect_getargspec(fn.__func__) + return compat.ArgSpec(spec.args[1:], spec.varargs, spec.keywords, spec.defaults) + elif hasattr(fn, '__func__'): + return compat.inspect_getargspec(fn.__func__) + elif hasattr(fn, '__call__') and \ + not hasattr(fn.__call__, '__call__'): # functools.partial does this; + # not much we can do + return get_callable_argspec(fn.__call__) + else: + raise ValueError("Can't inspect function: %s" % fn) def format_argspec_plus(fn, grouped=True): """Returns a dictionary of formatted, introspected function arguments. @@ -149,8 +307,8 @@ def format_argspec_plus(fn, grouped=True): 'apply_pos': '(self, a, b, c, **d)'} """ - if callable(fn): - spec = inspect_getfullargspec(fn) + if compat.callable(fn): + spec = compat.inspect_getfullargspec(fn) else: # we accept an existing argspec... spec = fn @@ -162,28 +320,29 @@ def format_argspec_plus(fn, grouped=True): else: self_arg = None - # Py3K - #apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2], None, spec[4]) - #num_defaults = 0 - #if spec[3]: - # num_defaults += len(spec[3]) - #if spec[4]: - # num_defaults += len(spec[4]) - #name_args = spec[0] + spec[4] - # Py2K - apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2]) - num_defaults = 0 - if spec[3]: - num_defaults += len(spec[3]) - name_args = spec[0] - # end Py2K + if compat.py3k: + apply_pos = inspect.formatargspec(spec[0], spec[1], + spec[2], None, spec[4]) + num_defaults = 0 + if spec[3]: + num_defaults += len(spec[3]) + if spec[4]: + num_defaults += len(spec[4]) + name_args = spec[0] + spec[4] + else: + apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2]) + num_defaults = 0 + if spec[3]: + num_defaults += len(spec[3]) + name_args = spec[0] if num_defaults: - defaulted_vals = name_args[0-num_defaults:] + defaulted_vals = name_args[0 - num_defaults:] else: defaulted_vals = () - apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], defaulted_vals, + apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], + defaulted_vals, formatvalue=lambda x: '=' + x) if grouped: return dict(args=args, self_arg=self_arg, @@ -192,6 +351,7 @@ def format_argspec_plus(fn, grouped=True): return dict(args=args[1:-1], self_arg=self_arg, apply_pos=apply_pos[1:-1], apply_kw=apply_kw[1:-1]) + def format_argspec_init(method, grouped=True): """format_argspec_plus with considerations for typical __init__ methods @@ -205,7 +365,6 @@ def format_argspec_init(method, grouped=True): try: return format_argspec_plus(method, grouped=grouped) except TypeError: - self_arg = 'self' if method is object.__init__: args = grouped and '(self)' or 'self' else: @@ -213,6 +372,7 @@ def format_argspec_init(method, grouped=True): or 'self, *args, **kwargs') return dict(self_arg='self', args=args, apply_pos=args, apply_kw=args) + def getargspec_init(method): """inspect.getargspec with considerations for typical __init__ methods @@ -232,53 +392,84 @@ def getargspec_init(method): def unbound_method_to_callable(func_or_cls): - """Adjust the incoming callable such that a 'self' argument is not required.""" + """Adjust the incoming callable such that a 'self' argument is not + required. - if isinstance(func_or_cls, types.MethodType) and not func_or_cls.im_self: - return func_or_cls.im_func + """ + + if isinstance(func_or_cls, types.MethodType) and not func_or_cls.__self__: + return func_or_cls.__func__ else: return func_or_cls + def generic_repr(obj, additional_kw=(), to_inspect=None): """Produce a __repr__() based on direct association of the __init__() specification vs. same-named attributes present. """ if to_inspect is None: - to_inspect = obj - def genargs(): + to_inspect = [obj] + else: + to_inspect = _collections.to_list(to_inspect) + + missing = object() + + pos_args = [] + kw_args = _collections.OrderedDict() + vargs = None + for i, insp in enumerate(to_inspect): try: - (args, vargs, vkw, defaults) = inspect.getargspec(to_inspect.__init__) + (_args, _vargs, vkw, defaults) = \ + inspect.getargspec(insp.__init__) except TypeError: - return - - default_len = defaults and len(defaults) or 0 - - if not default_len: - for arg in args[1:]: - yield repr(getattr(obj, arg, None)) - if vargs is not None and hasattr(obj, vargs): - yield ', '.join(repr(val) for val in getattr(obj, vargs)) + continue else: - for arg in args[1:-default_len]: - yield repr(getattr(obj, arg, None)) - for (arg, defval) in zip(args[-default_len:], defaults): - try: - val = getattr(obj, arg, None) - if val != defval: - yield '%s=%r' % (arg, val) - except: - pass - if additional_kw: - for arg, defval in additional_kw: - try: - val = getattr(obj, arg, None) - if val != defval: - yield '%s=%r' % (arg, val) - except: - pass + default_len = defaults and len(defaults) or 0 + if i == 0: + if _vargs: + vargs = _vargs + if default_len: + pos_args.extend(_args[1:-default_len]) + else: + pos_args.extend(_args[1:]) + else: + kw_args.update([ + (arg, missing) for arg in _args[1:-default_len] + ]) + + if default_len: + kw_args.update([ + (arg, default) + for arg, default + in zip(_args[-default_len:], defaults) + ]) + output = [] + + output.extend(repr(getattr(obj, arg, None)) for arg in pos_args) + + if vargs is not None and hasattr(obj, vargs): + output.extend([repr(val) for val in getattr(obj, vargs)]) + + for arg, defval in kw_args.items(): + try: + val = getattr(obj, arg, missing) + if val is not missing and val != defval: + output.append('%s=%r' % (arg, val)) + except: + pass + + if additional_kw: + for arg, defval in additional_kw: + try: + val = getattr(obj, arg, missing) + if val is not missing and val != defval: + output.append('%s=%r' % (arg, val)) + except: + pass + + return "%s(%s)" % (obj.__class__.__name__, ", ".join(output)) - return "%s(%s)" % (obj.__class__.__name__, ", ".join(genargs())) class portable_instancemethod(object): """Turn an instancemethod into a (parent, name) pair @@ -286,12 +477,13 @@ class portable_instancemethod(object): """ def __init__(self, meth): - self.target = meth.im_self + self.target = meth.__self__ self.name = meth.__name__ def __call__(self, *arg, **kw): return getattr(self.target, self.name)(*arg, **kw) + def class_hierarchy(cls): """Return an unordered sequence of all classes related to cls. @@ -305,37 +497,39 @@ def class_hierarchy(cls): will not be descended. """ - # Py2K - if isinstance(cls, types.ClassType): - return list() - # end Py2K + if compat.py2k: + if isinstance(cls, types.ClassType): + return list() + hier = set([cls]) process = list(cls.__mro__) while process: c = process.pop() - # Py2K - if isinstance(c, types.ClassType): - continue - for b in (_ for _ in c.__bases__ - if _ not in hier and not isinstance(_, types.ClassType)): - # end Py2K - # Py3K - #for b in (_ for _ in c.__bases__ - # if _ not in hier): + if compat.py2k: + if isinstance(c, types.ClassType): + continue + bases = (_ for _ in c.__bases__ + if _ not in hier and not isinstance(_, types.ClassType)) + else: + bases = (_ for _ in c.__bases__ if _ not in hier) + + for b in bases: process.append(b) hier.add(b) - # Py3K - #if c.__module__ == 'builtins' or not hasattr(c, '__subclasses__'): - # continue - # Py2K - if c.__module__ == '__builtin__' or not hasattr(c, '__subclasses__'): - continue - # end Py2K + + if compat.py3k: + if c.__module__ == 'builtins' or not hasattr(c, '__subclasses__'): + continue + else: + if c.__module__ == '__builtin__' or not hasattr(c, '__subclasses__'): + continue + for s in [_ for _ in c.__subclasses__() if _ not in hier]: process.append(s) hier.add(s) return list(hier) + def iterate_attributes(cls): """iterate all the keys and attributes associated with a class, without using getattr(). @@ -351,6 +545,7 @@ def iterate_attributes(cls): yield (key, c.__dict__[key]) break + def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, name='self.proxy', from_instance=None): """Automates delegation of __specials__ for a proxying type.""" @@ -364,6 +559,7 @@ def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, dunders = [m for m in dir(from_cls) if (m.startswith('__') and m.endswith('__') and not hasattr(into_cls, m) and m not in skip)] + for method in dunders: try: fn = getattr(from_cls, method) @@ -384,9 +580,9 @@ def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, "return %(name)s.%(method)s%(d_args)s" % locals()) env = from_instance is not None and {name: from_instance} or {} - exec py in env + compat.exec_(py, env) try: - env[method].func_defaults = fn.func_defaults + env[method].__defaults__ = fn.__defaults__ except AttributeError: pass setattr(into_cls, method, env[method]) @@ -395,11 +591,8 @@ def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, def methods_equivalent(meth1, meth2): """Return True if the two methods are the same implementation.""" - # Py3K - #return getattr(meth1, '__func__', meth1) is getattr(meth2, '__func__', meth2) - # Py2K - return getattr(meth1, 'im_func', meth1) is getattr(meth2, 'im_func', meth2) - # end Py2K + return getattr(meth1, '__func__', meth1) is getattr(meth2, '__func__', meth2) + def as_interface(obj, cls=None, methods=None, required=None): """Ensure basic interface compliance for an instance or dict of callables. @@ -471,7 +664,7 @@ def as_interface(obj, cls=None, methods=None, required=None): for method, impl in dictlike_iteritems(obj): if method not in interface: raise TypeError("%r: unknown in this interface" % method) - if not callable(impl): + if not compat.callable(impl): raise TypeError("%r=%r is not callable" % (method, impl)) setattr(AnonymousInterface, method, staticmethod(impl)) found.add(method) @@ -497,7 +690,12 @@ class memoized_property(object): return result def _reset(self, obj): - obj.__dict__.pop(self.__name__, None) + memoized_property.reset(obj, self.__name__) + + @classmethod + def reset(cls, obj, name): + obj.__dict__.pop(name, None) + class memoized_instancemethod(object): """Decorate a method memoize its return value. @@ -515,6 +713,7 @@ class memoized_instancemethod(object): def __get__(self, obj, cls): if obj is None: return self + def oneshot(*args, **kw): result = self.fget(obj, *args, **kw) memo = lambda *a, **kw: result @@ -522,13 +721,11 @@ class memoized_instancemethod(object): memo.__doc__ = self.__doc__ obj.__dict__[self.__name__] = memo return result + oneshot.__name__ = self.__name__ oneshot.__doc__ = self.__doc__ return oneshot -def reset_memoized(instance, name): - instance.__dict__.pop(name, None) - class group_expirable_memoized_property(object): """A family of @memoized_properties that can be expired in tandem.""" @@ -552,86 +749,141 @@ class group_expirable_memoized_property(object): self.attributes.append(fn.__name__) return memoized_instancemethod(fn) -class importlater(object): - """Deferred import object. - e.g.:: - somesubmod = importlater("mypackage.somemodule", "somesubmod") +def dependency_for(modulename): + def decorate(obj): + # TODO: would be nice to improve on this import silliness, + # unfortunately importlib doesn't work that great either + tokens = modulename.split(".") + mod = compat.import_(".".join(tokens[0:-1]), globals(), locals(), tokens[-1]) + mod = getattr(mod, tokens[-1]) + setattr(mod, obj.__name__, obj) + return obj + return decorate - is equivalent to:: +class dependencies(object): + """Apply imported dependencies as arguments to a function. - from mypackage.somemodule import somesubmod + E.g.:: - except evaluted upon attribute access to "somesubmod". + @util.dependencies( + "sqlalchemy.sql.widget", + "sqlalchemy.engine.default" + ); + def some_func(self, widget, default, arg1, arg2, **kw): + # ... - importlater() currently requires that resolve_all() be - called, typically at the bottom of a package's __init__.py. - This is so that __import__ still called only at - module import time, and not potentially within - a non-main thread later on. + Rationale is so that the impact of a dependency cycle can be + associated directly with the few functions that cause the cycle, + and not pollute the module-level namespace. """ - _unresolved = set() + def __init__(self, *deps): + self.import_deps = [] + for dep in deps: + tokens = dep.split(".") + self.import_deps.append( + dependencies._importlater( + ".".join(tokens[0:-1]), + tokens[-1] + ) + ) - def __init__(self, path, addtl=None): - self._il_path = path - self._il_addtl = addtl - importlater._unresolved.add(self) + def __call__(self, fn): + import_deps = self.import_deps + spec = compat.inspect_getfullargspec(fn) + + spec_zero = list(spec[0]) + hasself = spec_zero[0] in ('self', 'cls') + + for i in range(len(import_deps)): + spec[0][i + (1 if hasself else 0)] = "import_deps[%r]" % i + + inner_spec = format_argspec_plus(spec, grouped=False) + + for impname in import_deps: + del spec_zero[1 if hasself else 0] + spec[0][:] = spec_zero + + outer_spec = format_argspec_plus(spec, grouped=False) + + code = 'lambda %(args)s: fn(%(apply_kw)s)' % { + "args": outer_spec['args'], + "apply_kw": inner_spec['apply_kw'] + } + + decorated = eval(code, locals()) + decorated.__defaults__ = getattr(fn, 'im_func', fn).__defaults__ + return update_wrapper(decorated, fn) @classmethod - def resolve_all(cls): - for m in list(importlater._unresolved): - m._resolve() + def resolve_all(cls, path): + for m in list(dependencies._unresolved): + if m._full_path.startswith(path): + m._resolve() - @property - def _full_path(self): - if self._il_addtl: + _unresolved = set() + _by_key = {} + + class _importlater(object): + _unresolved = set() + + _by_key = {} + + def __new__(cls, path, addtl): + key = path + "." + addtl + if key in dependencies._by_key: + return dependencies._by_key[key] + else: + dependencies._by_key[key] = imp = object.__new__(cls) + return imp + + def __init__(self, path, addtl): + self._il_path = path + self._il_addtl = addtl + dependencies._unresolved.add(self) + + + @property + def _full_path(self): return self._il_path + "." + self._il_addtl - else: - return self._il_path - @memoized_property - def module(self): - if self in importlater._unresolved: - raise ImportError( - "importlater.resolve_all() hasn't been called") + @memoized_property + def module(self): + if self in dependencies._unresolved: + raise ImportError( + "importlater.resolve_all() hasn't " + "been called (this is %s %s)" + % (self._il_path, self._il_addtl)) - m = self._initial_import - if self._il_addtl: - m = getattr(m, self._il_addtl) - else: - for token in self._il_path.split(".")[1:]: - m = getattr(m, token) - return m + return getattr(self._initial_import, self._il_addtl) - def _resolve(self): - importlater._unresolved.discard(self) - if self._il_addtl: - self._initial_import = __import__( + def _resolve(self): + dependencies._unresolved.discard(self) + self._initial_import = compat.import_( self._il_path, globals(), locals(), [self._il_addtl]) - else: - self._initial_import = __import__(self._il_path) - def __getattr__(self, key): - if key == 'module': - raise ImportError("Could not resolve module %s" - % self._full_path) - try: - attr = getattr(self.module, key) - except AttributeError: - raise AttributeError( - "Module %s has no attribute '%s'" % - (self._full_path, key) - ) - self.__dict__[key] = attr - return attr + def __getattr__(self, key): + if key == 'module': + raise ImportError("Could not resolve module %s" + % self._full_path) + try: + attr = getattr(self.module, key) + except AttributeError: + raise AttributeError( + "Module %s has no attribute '%s'" % + (self._full_path, key) + ) + self.__dict__[key] = attr + return attr + # from paste.deploy.converters def asbool(obj): - if isinstance(obj, (str, unicode)): + if isinstance(obj, compat.string_types): obj = obj.strip().lower() if obj in ['true', 'yes', 'on', 'y', 't', '1']: return True @@ -641,6 +893,7 @@ def asbool(obj): raise ValueError("String is not true/false: %r" % obj) return bool(obj) + def bool_or_str(*text): """Return a callable that will evaulate a string as boolean, or one of a set of "alternate" string values. @@ -653,6 +906,7 @@ def bool_or_str(*text): return asbool(obj) return bool_or_value + def asint(value): """Coerce to integer.""" @@ -689,19 +943,20 @@ def constructor_copy(obj, cls, **kw): def counter(): """Return a threadsafe counter function.""" - lock = threading.Lock() - counter = itertools.count(1L) + lock = compat.threading.Lock() + counter = itertools.count(1) # avoid the 2to3 "next" transformation... def _next(): lock.acquire() try: - return counter.next() + return next(counter) finally: lock.release() return _next + def duck_type_collection(specimen, default=None): """Given an instance or class, guess if it is or is acting as one of the basic collection types: list, set and dict. If the __emulates__ @@ -711,7 +966,7 @@ def duck_type_collection(specimen, default=None): if hasattr(specimen, '__emulates__'): # canonicalize set vs sets.Set to a standard: the builtin set if (specimen.__emulates__ is not None and - issubclass(specimen.__emulates__, set_types)): + issubclass(specimen.__emulates__, set)): return set else: return specimen.__emulates__ @@ -719,7 +974,7 @@ def duck_type_collection(specimen, default=None): isa = isinstance(specimen, type) and issubclass or isinstance if isa(specimen, list): return list - elif isa(specimen, set_types): + elif isa(specimen, set): return set elif isa(specimen, dict): return dict @@ -733,32 +988,32 @@ def duck_type_collection(specimen, default=None): else: return default + def assert_arg_type(arg, argtype, name): if isinstance(arg, argtype): return arg else: if isinstance(argtype, tuple): raise exc.ArgumentError( - "Argument '%s' is expected to be one of type %s, got '%s'" % - (name, ' or '.join("'%s'" % a for a in argtype), type(arg))) + "Argument '%s' is expected to be one of type %s, got '%s'" % + (name, ' or '.join("'%s'" % a for a in argtype), type(arg))) else: raise exc.ArgumentError( - "Argument '%s' is expected to be of type '%s', got '%s'" % - (name, argtype, type(arg))) + "Argument '%s' is expected to be of type '%s', got '%s'" % + (name, argtype, type(arg))) def dictlike_iteritems(dictlike): """Return a (key, value) iterator for almost any dict-like object.""" - # Py3K - #if hasattr(dictlike, 'items'): - # return dictlike.items() - # Py2K - if hasattr(dictlike, 'iteritems'): - return dictlike.iteritems() - elif hasattr(dictlike, 'items'): - return iter(dictlike.items()) - # end Py2K + if compat.py3k: + if hasattr(dictlike, 'items'): + return list(dictlike.items()) + else: + if hasattr(dictlike, 'iteritems'): + return dictlike.iteritems() + elif hasattr(dictlike, 'items'): + return iter(dictlike.items()) getter = getattr(dictlike, '__getitem__', getattr(dictlike, 'get', None)) if getter is None: @@ -796,17 +1051,38 @@ class classproperty(property): return desc.fget(cls) -class _symbol(object): - def __init__(self, name, doc=None): +class hybridmethod(object): + """Decorate a function as cls- or instance- level.""" + def __init__(self, func, expr=None): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self.func.__get__(owner, owner.__class__) + else: + return self.func.__get__(instance, owner) + + +class _symbol(int): + def __new__(self, name, doc=None, canonical=None): """Construct a new named symbol.""" - assert isinstance(name, str) - self.name = name + assert isinstance(name, compat.string_types) + if canonical is None: + canonical = hash(name) + v = int.__new__(_symbol, canonical) + v.name = name if doc: - self.__doc__ = doc + v.__doc__ = doc + return v + def __reduce__(self): - return symbol, (self.name,) + return symbol, (self.name, "x", int(self)) + + def __str__(self): + return repr(self) + def __repr__(self): - return "" % self.name + return "symbol(%r)" % self.name _symbol.__name__ = 'symbol' @@ -833,20 +1109,22 @@ class symbol(object): """ symbols = {} - _lock = threading.Lock() + _lock = compat.threading.Lock() - def __new__(cls, name, doc=None): + def __new__(cls, name, doc=None, canonical=None): cls._lock.acquire() try: sym = cls.symbols.get(name) if sym is None: - cls.symbols[name] = sym = _symbol(name, doc) + cls.symbols[name] = sym = _symbol(name, doc, canonical) return sym finally: symbol._lock.release() _creation_order = 1 + + def set_creation_order(instance): """Assign a '_creation_order' sequence to the given instance. @@ -857,10 +1135,14 @@ def set_creation_order(instance): """ global _creation_order instance._creation_order = _creation_order - _creation_order +=1 + _creation_order += 1 + def warn_exception(func, *args, **kwargs): - """executes the given function, catches all exceptions and converts to a warning.""" + """executes the given function, catches all exceptions and converts to + a warning. + + """ try: return func(*args, **kwargs) except: @@ -881,13 +1163,28 @@ def warn(msg, stacklevel=3): be controlled. """ - if isinstance(msg, basestring): + if isinstance(msg, compat.string_types): warnings.warn(msg, exc.SAWarning, stacklevel=stacklevel) else: warnings.warn(msg, stacklevel=stacklevel) + +def only_once(fn): + """Decorate the given function to be a no-op after it is called exactly + once.""" + + once = [fn] + def go(*arg, **kw): + if once: + once_fn = once.pop() + return once_fn(*arg, **kw) + + return update_wrapper(go, fn) + + _SQLA_RE = re.compile(r'sqlalchemy/([a-z_]+/){0,2}[a-z_]+\.py') _UNITTEST_RE = re.compile(r'unit(?:2|test2?/)') + def chop_traceback(tb, exclude_prefix=_UNITTEST_RE, exclude_suffix=_SQLA_RE): """Chop extraneous lines off beginning and end of a traceback. @@ -906,6 +1203,6 @@ def chop_traceback(tb, exclude_prefix=_UNITTEST_RE, exclude_suffix=_SQLA_RE): start += 1 while start <= end and exclude_suffix.search(tb[end]): end -= 1 - return tb[start:end+1] + return tb[start:end + 1] NoneType = type(None) diff --git a/libs/sqlalchemy/util/queue.py b/libs/sqlalchemy/util/queue.py index acccf3c5..82ff55a5 100644 --- a/libs/sqlalchemy/util/queue.py +++ b/libs/sqlalchemy/util/queue.py @@ -1,48 +1,53 @@ # util/queue.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """An adaptation of Py2.3/2.4's Queue module which supports reentrant -behavior, using RLock instead of Lock for its mutex object. +behavior, using RLock instead of Lock for its mutex object. The +Queue object is used exclusively by the sqlalchemy.pool.QueuePool +class. This is to support the connection pool's usage of weakref callbacks to return connections to the underlying Queue, which can in extremely rare cases be invoked within the ``get()`` method of the Queue itself, producing a ``put()`` inside the ``get()`` and therefore a reentrant -condition.""" +condition. + +An additional change includes a special "abort" method which can be used +to immediately raise a special exception for threads that are blocking +on get(). This is to accommodate a rare race condition that can occur +within QueuePool. + +""" from collections import deque from time import time as _time -from sqlalchemy.util import threading -import sys - -if sys.version_info < (2, 6): - def notify_all(condition): - condition.notify() -else: - def notify_all(condition): - condition.notify_all() +from .compat import threading -__all__ = ['Empty', 'Full', 'Queue'] +__all__ = ['Empty', 'Full', 'Queue', 'SAAbort'] + class Empty(Exception): "Exception raised by Queue.get(block=0)/get_nowait()." pass + class Full(Exception): "Exception raised by Queue.put(block=0)/put_nowait()." pass + class SAAbort(Exception): "Special SQLA exception to abort waiting" def __init__(self, context): self.context = context + class Queue: def __init__(self, maxsize=0): """Initialize a queue object with a given maximum size. @@ -146,7 +151,6 @@ class Queue: return an item if one is immediately available, else raise the ``Empty`` exception (`timeout` is ignored in that case). """ - self.not_empty.acquire() try: if not block: @@ -154,7 +158,11 @@ class Queue: raise Empty elif timeout is None: while self._empty(): - self.not_empty.wait() + # wait for only half a second, then + # loop around, so that we can see a change in + # _sqla_abort_context in case we missed the notify_all() + # called by abort() + self.not_empty.wait(.5) if self._sqla_abort_context: raise SAAbort(self._sqla_abort_context) else: @@ -183,7 +191,10 @@ class Queue: if not self.not_full.acquire(False): return try: - notify_all(self.not_empty) + # note that this is now optional + # as the waiters in get() both loop around + # to check the _sqla_abort_context flag periodically + self.not_empty.notify_all() finally: self.not_full.release() diff --git a/libs/sqlalchemy/util/topological.py b/libs/sqlalchemy/util/topological.py index 86e42c1f..fe7e7689 100644 --- a/libs/sqlalchemy/util/topological.py +++ b/libs/sqlalchemy/util/topological.py @@ -1,17 +1,17 @@ # util/topological.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Topological sorting algorithms.""" -from sqlalchemy.exc import CircularDependencyError -from sqlalchemy import util - +from ..exc import CircularDependencyError +from .. import util __all__ = ['sort', 'sort_as_subsets', 'find_cycles'] + def sort_as_subsets(tuples, allitems): edges = util.defaultdict(set) @@ -36,6 +36,7 @@ def sort_as_subsets(tuples, allitems): todo.difference_update(output) yield output + def sort(tuples, allitems): """sort the given list of items by dependency. @@ -46,8 +47,10 @@ def sort(tuples, allitems): for s in set_: yield s + def find_cycles(tuples, allitems): - # straight from gvr with some mods + # adapted from: + # http://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html edges = util.defaultdict(set) for parent, child in tuples: @@ -84,6 +87,7 @@ def find_cycles(tuples, allitems): node = stack.pop() return output + def _gen_edges(edges): return set([ (right, left) From fe24322f7c9c753d938c9ac4df51e7a186ed0f64 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Jan 2014 21:56:27 +0100 Subject: [PATCH 002/468] Add kwargs to to_dict --- couchpotato/core/settings/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 09d46afa..8b235dd1 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -292,7 +292,7 @@ class Release(Base, TableHelper): files = relationship('File', secondary = release_files) info = relationship('ReleaseInfo') #, cascade = 'all, delete-orphan') - def to_dict(self, deep = None, exclude = None): + def to_dict(self, deep = None, exclude = None, **kwargs): if not exclude: exclude = [] if not deep: deep = {} @@ -373,7 +373,7 @@ class Profile(Base, TableHelper): movie = relationship('Movie') types = relationship('ProfileType', order_by="asc(ProfileType.order)") #, cascade = 'all, delete-orphan') - def to_dict(self, deep = None, exclude = None): + def to_dict(self, deep = None, exclude = None, **kwargs): if not exclude: exclude = [] if not deep: deep = {} From 00b613d2e0fb88dde4501947cec60ffe0885332f Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Jan 2014 21:56:47 +0100 Subject: [PATCH 003/468] Scoped session --- couchpotato/environment.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 1c5863d1..42f8fded 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -2,6 +2,7 @@ from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings from sqlalchemy.engine import create_engine +from sqlalchemy.orm import scoped_session from sqlalchemy.orm.session import sessionmaker import os @@ -24,6 +25,7 @@ class Env(object): _daemonized = False _desktop = None _engine = None + _session = None ''' Data paths and directories ''' _app_dir = "" @@ -54,7 +56,13 @@ class Env(object): @staticmethod def getSession(): - session = sessionmaker(bind = Env.getEngine()) + existing_session = Env.get('session') + if existing_session: + return existing_session() + + session = scoped_session(sessionmaker(bind = Env.getEngine())) + Env.set('session', session) + return session() @staticmethod From aab10fb59911dd0af6b1f8654649757966f574f9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Jan 2014 08:22:06 +0100 Subject: [PATCH 004/468] Close all --- couchpotato/core/media/_base/media/main.py | 5 + couchpotato/core/media/movie/searcher/main.py | 2 + .../core/media/movie/suggestion/main.py | 2 + couchpotato/core/notifications/core/main.py | 4 + couchpotato/core/plugins/category/main.py | 4 + couchpotato/core/plugins/dashboard/main.py | 1 + couchpotato/core/plugins/file/main.py | 1 + couchpotato/core/plugins/profile/main.py | 2 + couchpotato/core/plugins/quality/main.py | 3 + couchpotato/core/plugins/release/main.py | 9 +- couchpotato/core/plugins/renamer/main.py | 338 +++++++++--------- couchpotato/core/plugins/scanner/main.py | 2 + couchpotato/core/plugins/status/main.py | 3 +- couchpotato/core/plugins/subtitle/main.py | 2 + .../core/providers/info/_modifier/main.py | 1 + couchpotato/core/settings/__init__.py | 1 + 16 files changed, 210 insertions(+), 170 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index cbcd4245..5345b07c 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -101,6 +101,8 @@ class MediaPlugin(MediaBase): def handler(): fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id)) + db.close() + if handler: return handler @@ -124,6 +126,7 @@ class MediaPlugin(MediaBase): if m: results = m.to_dict(self.default_dict) + db.close() return results def getView(self, id = None, **kwargs): @@ -259,6 +262,7 @@ class MediaPlugin(MediaBase): 'releases_count': releases_count.get(media_id), })) + db.close() return total_count, movies def listView(self, **kwargs): @@ -352,6 +356,7 @@ class MediaPlugin(MediaBase): if len(chars) == 25: break + db.close() return ''.join(sorted(chars)) def charView(self, **kwargs): diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 7ae76a4e..bde5b44b 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -117,6 +117,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): pass self.in_progress = False + db.close() def single(self, movie, search_protocols = None, manual = False): @@ -207,6 +208,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']}) + db.close() return ret def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index 22e23fe2..d1d64089 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -33,6 +33,7 @@ class Suggestion(Plugin): .options(joinedload_all('library')) \ .filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all() movies = [x.library.identifier for x in active_movies] + db.close() if not ignored or len(ignored) == 0: ignored = splitString(Env.prop('suggest_ignore', default = '')) @@ -97,6 +98,7 @@ class Suggestion(Plugin): .filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all() movies = [x[0] for x in active_movies] movies.extend(seen) + db.close() ignored.extend([x.get('imdb') for x in cached_suggestion]) suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True) diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 93f94d6a..d77758b6 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -126,6 +126,7 @@ class CoreNotifier(Notification): ndict['type'] = 'notification' notifications.append(ndict) + db.close() return { 'success': True, 'empty': len(notifications) == 0, @@ -172,6 +173,7 @@ class CoreNotifier(Notification): self.frontend(type = listener, data = data) + db.close() return True except: log.error('Failed notify: %s', traceback.format_exc()) @@ -287,6 +289,8 @@ class CoreNotifier(Notification): ndict['type'] = 'notification' messages.append(ndict) + db.close() + return { 'success': True, 'result': messages, diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index c7abaee4..41965b03 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -42,6 +42,7 @@ class CategoryPlugin(Plugin): for category in categories: temp.append(category.to_dict()) + db.close() return temp def save(self, **kwargs): @@ -65,6 +66,7 @@ class CategoryPlugin(Plugin): category_dict = c.to_dict() + db.close() return { 'success': True, 'category': category_dict @@ -94,6 +96,7 @@ class CategoryPlugin(Plugin): db.commit() + db.close() return { 'success': True } @@ -126,6 +129,7 @@ class CategoryPlugin(Plugin): except Exception as e: message = log.error('Failed deleting category: %s', e) + db.close() return { 'success': success, 'message': message diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 949d8f50..73866411 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -119,6 +119,7 @@ class Dashboard(Plugin): 'files': {}, })) + db.close() return { 'success': True, 'empty': len(movies) == 0, diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index c52a9801..18071547 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -166,6 +166,7 @@ class FileManager(Plugin): for type_object in results: types.append(type_object.to_dict()) + db.close() return types def getTypesView(self, **kwargs): diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 914d46f3..cc048733 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -71,6 +71,7 @@ class ProfilePlugin(Plugin): for profile in profiles: temp.append(profile.to_dict(self.to_dict)) + db.close() return temp def save(self, **kwargs): @@ -127,6 +128,7 @@ class ProfilePlugin(Plugin): .options(joinedload_all('types')) \ .first() default_dict = default.to_dict(self.to_dict) + db.close() return default_dict diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index d56ff89f..cc6215d3 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -78,6 +78,8 @@ class QualityPlugin(Plugin): temp.append(q) self.cached_qualities = temp + + db.close() return temp def single(self, identifier = ''): @@ -89,6 +91,7 @@ class QualityPlugin(Plugin): if quality: quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) + db.close() return quality_dict def getQuality(self, identifier): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index a478b64d..1e1352c5 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -88,6 +88,8 @@ class Release(Plugin): elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]: self.updateStatus(id = rel.id, status = ignored_status) + db.close() + def add(self, group): @@ -176,7 +178,7 @@ class Release(Plugin): rel = db.query(Relea).filter_by(id = id).first() if rel: - rel.delete() + db.delete(rel) db.commit() return True except: @@ -220,6 +222,7 @@ class Release(Plugin): ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True) self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status) + db.close() return { 'success': True } @@ -259,11 +262,10 @@ class Release(Plugin): 'files': {} }), manual = True) - db.expunge_all() - if success: fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) + db.close() return { 'success': success == True } @@ -456,6 +458,7 @@ class Release(Plugin): releases = [r.to_dict({'info': {}, 'files': {}}) for r in releases_raw] releases = sorted(releases, key = lambda k: k['info'].get('score', 0), reverse = True) + db.close() return releases def forMovieView(self, id = None, **kwargs): diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index f86bebad..c4c67deb 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -598,6 +598,7 @@ class Renamer(Plugin): break self.renaming_started = False + db.close() def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False): if not group: group = {} @@ -851,197 +852,200 @@ Remove it if you want it to be renamed (again, or at least let it try again) snatched_status, ignored_status, failed_status, seeding_status, downloaded_status, missing_status = \ fireEvent('status.get', ['snatched', 'ignored', 'failed', 'seeding', 'downloaded', 'missing'], single = True) - db = get_session() - rels = db.query(Release).filter( - Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')]) - ).all() - - if not rels: - #No releases found that need status checking - self.checking_snatched = False - return True - - # Collect all download information with the download IDs from the releases - download_ids = [] - no_status_support = [] try: - for rel in rels: - rel_dict = rel.to_dict({'info': {}}) - if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'): - download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']}) + db = get_session() + rels = db.query(Release).filter( + Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')]) + ).all() - ds = rel_dict['info'].get('download_status_support') - if ds == False or ds == 'False': - no_status_support.append(ss(rel_dict['info'].get('download_downloader'))) - except: - log.error('Error getting download IDs from database') - self.checking_snatched = False - return False + if not rels: + #No releases found that need status checking + self.checking_snatched = False + return True - release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else [] + # Collect all download information with the download IDs from the releases + download_ids = [] + no_status_support = [] + try: + for rel in rels: + rel_dict = rel.to_dict({'info': {}}) + if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'): + download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']}) - if len(no_status_support) > 0: - log.debug('Download status functionality is not implemented for one of the active downloaders: %s', no_status_support) + ds = rel_dict['info'].get('download_status_support') + if ds == False or ds == 'False': + no_status_support.append(ss(rel_dict['info'].get('download_downloader'))) + except: + log.error('Error getting download IDs from database') + self.checking_snatched = False + return False - if not release_downloads: - if fire_scan: - self.scan() + release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else [] - self.checking_snatched = False - return True + if len(no_status_support) > 0: + log.debug('Download status functionality is not implemented for one of the active downloaders: %s', no_status_support) - scan_releases = [] - scan_required = False + if not release_downloads: + if fire_scan: + self.scan() - log.debug('Checking status snatched releases...') + self.checking_snatched = False + return True - try: - for rel in rels: - rel_dict = rel.to_dict({'info': {}}) - movie_dict = fireEvent('media.get', media_id = rel.movie_id, single = True) + scan_releases = [] + scan_required = False - if not isinstance(rel_dict['info'], dict): - log.error('Faulty release found without any info, ignoring.') - fireEvent('release.update_status', rel.id, status = ignored_status, single = True) - continue + log.debug('Checking status snatched releases...') - # Check if download ID is available - if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'): - log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name'])) - scan_required = True + try: + for rel in rels: + rel_dict = rel.to_dict({'info': {}}) + movie_dict = fireEvent('media.get', media_id = rel.movie_id, single = True) - # Continue with next release - continue + if not isinstance(rel_dict['info'], dict): + log.error('Faulty release found without any info, ignoring.') + fireEvent('release.update_status', rel.id, status = ignored_status, single = True) + continue - # Find release in downloaders - nzbname = self.createNzbName(rel_dict['info'], movie_dict) + # Check if download ID is available + if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'): + log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name'])) + scan_required = True + + # Continue with next release + continue + + # Find release in downloaders + nzbname = self.createNzbName(rel_dict['info'], movie_dict) - found_release = False - for release_download in release_downloads: found_release = False - if rel_dict['info'].get('download_id'): - if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']: - log.debug('Found release by id: %s', release_download['id']) - found_release = True - break - else: - if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: - log.debug('Found release by release name or imdb ID: %s', release_download['name']) - found_release = True - break - - if not found_release: - log.info('%s not found in downloaders', nzbname) - - #Check status if already missing and for how long, if > 1 week, set to ignored else to missing - if rel.status_id == missing_status.get('id'): - if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: - fireEvent('release.update_status', rel.id, status = ignored_status, single = True) - else: - # Set the release to missing - fireEvent('release.update_status', rel.id, status = missing_status, single = True) - - # Continue with next release - continue - - # Log that we found the release - timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft'] - log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft)) - - # Check status of release - if release_download['status'] == 'busy': - # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) - - # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading - if self.movieInFromFolder(release_download['folder']): - self.tagRelease(release_download = release_download, tag = 'downloading') - - elif release_download['status'] == 'seeding': - #If linking setting is enabled, process release - if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download): - log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) - - # Remove the downloading tag - self.untagRelease(release_download = release_download, tag = 'downloading') - - # Scan and set the torrent to paused if required - release_download.update({'pause': True, 'scan': True, 'process_complete': False}) - scan_releases.append(release_download) - else: - #let it seed - log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio'])) - - # Set the release to seeding - fireEvent('release.update_status', rel.id, status = seeding_status, single = True) - - elif release_download['status'] == 'failed': - # Set the release to failed - fireEvent('release.update_status', rel.id, status = failed_status, single = True) - - fireEvent('download.remove_failed', release_download, single = True) - - if self.conf('next_on_failed'): - fireEvent('movie.searcher.try_next_release', media_id = rel.movie_id) - - elif release_download['status'] == 'completed': - log.info('Download of %s completed!', release_download['name']) - - #Make sure the downloader sent over a path to look in - if self.statusInfoComplete(release_download): - - # If the release has been seeding, process now the seeding is done - if rel.status_id == seeding_status.get('id'): - if self.conf('file_action') != 'move': - # Set the release to done as the movie has already been renamed - fireEvent('release.update_status', rel.id, status = downloaded_status, single = True) - - # Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': False, 'process_complete': True}) - scan_releases.append(release_download) - else: - # Scan and Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': True, 'process_complete': True}) - scan_releases.append(release_download) - + for release_download in release_downloads: + found_release = False + if rel_dict['info'].get('download_id'): + if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']: + log.debug('Found release by id: %s', release_download['id']) + found_release = True + break else: - # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: + log.debug('Found release by release name or imdb ID: %s', release_download['name']) + found_release = True + break + + if not found_release: + log.info('%s not found in downloaders', nzbname) + + #Check status if already missing and for how long, if > 1 week, set to ignored else to missing + if rel.status_id == missing_status.get('id'): + if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: + fireEvent('release.update_status', rel.id, status = ignored_status, single = True) + else: + # Set the release to missing + fireEvent('release.update_status', rel.id, status = missing_status, single = True) + + # Continue with next release + continue + + # Log that we found the release + timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft'] + log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft)) + + # Check status of release + if release_download['status'] == 'busy': + # Set the release to snatched if it was missing before + fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + + # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading + if self.movieInFromFolder(release_download['folder']): + self.tagRelease(release_download = release_download, tag = 'downloading') + + elif release_download['status'] == 'seeding': + #If linking setting is enabled, process release + if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download): + log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) # Remove the downloading tag self.untagRelease(release_download = release_download, tag = 'downloading') - # Scan and Allow the downloader to clean-up - release_download.update({'pause': False, 'scan': True, 'process_complete': True}) + # Scan and set the torrent to paused if required + release_download.update({'pause': True, 'scan': True, 'process_complete': False}) scan_releases.append(release_download) - else: - scan_required = True + else: + #let it seed + log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio'])) - except: - log.error('Failed checking for release in downloader: %s', traceback.format_exc()) + # Set the release to seeding + fireEvent('release.update_status', rel.id, status = seeding_status, single = True) - # The following can either be done here, or inside the scanner if we pass it scan_items in one go - for release_download in scan_releases: - # Ask the renamer to scan the item - if release_download['scan']: - if release_download['pause'] and self.conf('file_action') == 'link': - fireEvent('download.pause', release_download = release_download, pause = True, single = True) - self.scan(release_download = release_download) - if release_download['pause'] and self.conf('file_action') == 'link': - fireEvent('download.pause', release_download = release_download, pause = False, single = True) - if release_download['process_complete']: - #First make sure the files were succesfully processed - if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'): - # Remove the seeding tag if it exists - self.untagRelease(release_download = release_download, tag = 'renamed_already') - # Ask the downloader to process the item - fireEvent('download.process_complete', release_download = release_download, single = True) + elif release_download['status'] == 'failed': + # Set the release to failed + fireEvent('release.update_status', rel.id, status = failed_status, single = True) - if fire_scan and (scan_required or len(no_status_support) > 0): - self.scan() + fireEvent('download.remove_failed', release_download, single = True) - self.checking_snatched = False - return True + if self.conf('next_on_failed'): + fireEvent('movie.searcher.try_next_release', media_id = rel.movie_id) + + elif release_download['status'] == 'completed': + log.info('Download of %s completed!', release_download['name']) + + #Make sure the downloader sent over a path to look in + if self.statusInfoComplete(release_download): + + # If the release has been seeding, process now the seeding is done + if rel.status_id == seeding_status.get('id'): + if self.conf('file_action') != 'move': + # Set the release to done as the movie has already been renamed + fireEvent('release.update_status', rel.id, status = downloaded_status, single = True) + + # Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': False, 'process_complete': True}) + scan_releases.append(release_download) + else: + # Scan and Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_releases.append(release_download) + + else: + # Set the release to snatched if it was missing before + fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + + # Remove the downloading tag + self.untagRelease(release_download = release_download, tag = 'downloading') + + # Scan and Allow the downloader to clean-up + release_download.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_releases.append(release_download) + else: + scan_required = True + + except: + log.error('Failed checking for release in downloader: %s', traceback.format_exc()) + + # The following can either be done here, or inside the scanner if we pass it scan_items in one go + for release_download in scan_releases: + # Ask the renamer to scan the item + if release_download['scan']: + if release_download['pause'] and self.conf('file_action') == 'link': + fireEvent('download.pause', release_download = release_download, pause = True, single = True) + self.scan(release_download = release_download) + if release_download['pause'] and self.conf('file_action') == 'link': + fireEvent('download.pause', release_download = release_download, pause = False, single = True) + if release_download['process_complete']: + #First make sure the files were succesfully processed + if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'): + # Remove the seeding tag if it exists + self.untagRelease(release_download = release_download, tag = 'renamed_already') + # Ask the downloader to process the item + fireEvent('download.process_complete', release_download = release_download, single = True) + + if fire_scan and (scan_required or len(no_status_support) > 0): + self.scan() + + self.checking_snatched = False + return True + finally: + db.close() def extendReleaseDownload(self, release_download): @@ -1064,6 +1068,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) if not rls: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) + db.close() + if rls: rls_dict = rls.to_dict({'info':{}}) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index daff0919..00a5b752 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -441,6 +441,7 @@ class Scanner(Plugin): else: log.debug('Found no movies in the folder %s', folder) + db.close() return processed_movies def getMetaData(self, group, folder = '', release_download = None): @@ -608,6 +609,7 @@ class Scanner(Plugin): break except: pass + db.close() # Search based on identifiers if not imdb_id: diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index 08f46984..55852610 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -55,7 +55,7 @@ class StatusPlugin(Plugin): db = get_session() status = db.query(Status).filter_by(id = id).first() status_dict = status.to_dict() - #db.close() + db.close() return status_dict @@ -73,6 +73,7 @@ class StatusPlugin(Plugin): # Update cache self.status_cached[status.identifier] = s + db.close() return temp def get(self, identifiers): diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index 56056c0a..e51df35e 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -41,6 +41,8 @@ class Subtitle(Plugin): # get subtitles for those files subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) + db.close() + def searchSingle(self, group): if self.isDisabled(): return diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index 88d4381c..598a01d7 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -104,6 +104,7 @@ class MovieResultModifier(Plugin): except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) + db.close() return temp def checkLibrary(self, result): diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 3b575176..e6565d07 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -216,6 +216,7 @@ class Settings(object): except: pass + db.close() return prop def setProperty(self, identifier, value = ''): From a254886badbffc08f3cccb9136a90d3a266427df Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Jan 2014 17:49:54 +0100 Subject: [PATCH 005/468] Try NoSQL --- couchpotato/__init__.py | 6 +- couchpotato/core/_base/_core/main.py | 4 +- couchpotato/core/_base/clientscript/main.py | 2 +- couchpotato/core/_base/desktop/main.py | 2 +- couchpotato/core/_base/updater/main.py | 4 +- couchpotato/core/downloaders/rtorrent/main.py | 2 +- couchpotato/core/media/__init__.py | 28 +- couchpotato/core/media/_base/media/index.py | 60 + couchpotato/core/media/_base/media/main.py | 75 +- couchpotato/core/media/_base/search/main.py | 2 +- couchpotato/core/media/_base/searcher/base.py | 2 +- couchpotato/core/media/movie/_base/main.py | 126 +- .../core/media/movie/library/movie/main.py | 23 +- couchpotato/core/media/movie/searcher/main.py | 90 +- .../core/media/movie/suggestion/main.py | 4 +- couchpotato/core/notifications/core/index.py | 42 + couchpotato/core/notifications/core/main.py | 108 +- couchpotato/core/notifications/growl/main.py | 2 +- couchpotato/core/plugins/automation/main.py | 4 +- couchpotato/core/plugins/category/main.py | 16 +- couchpotato/core/plugins/custom/main.py | 2 +- couchpotato/core/plugins/dashboard/main.py | 2 +- couchpotato/core/plugins/file/main.py | 108 +- couchpotato/core/plugins/manage/main.py | 2 +- couchpotato/core/plugins/nosql/__init__.py | 7 + couchpotato/core/plugins/nosql/index.py | 20 + couchpotato/core/plugins/nosql/main.py | 72 + couchpotato/core/plugins/profile/main.py | 48 +- couchpotato/core/plugins/quality/main.py | 8 +- couchpotato/core/plugins/release/main.py | 133 +- couchpotato/core/plugins/renamer/main.py | 6 +- couchpotato/core/plugins/scanner/main.py | 4 +- couchpotato/core/plugins/status/__init__.py | 7 - couchpotato/core/plugins/status/main.py | 137 -- .../core/plugins/status/static/status.js | 17 - couchpotato/core/plugins/subtitle/main.py | 2 +- .../core/providers/info/_modifier/main.py | 2 +- couchpotato/core/providers/metadata/base.py | 1 + couchpotato/core/settings/__init__.py | 56 +- couchpotato/core/settings/index.py | 16 + couchpotato/environment.py | 24 +- couchpotato/runner.py | 81 +- couchpotato/templates/index.html | 4 +- libs/CodernityDB/__init__.py | 20 + libs/CodernityDB/database.py | 1214 ++++++++++ libs/CodernityDB/database_gevent.py | 31 + libs/CodernityDB/database_safe_shared.py | 229 ++ .../CodernityDB/database_super_thread_safe.py | 112 + libs/CodernityDB/database_thread_safe.py | 35 + libs/CodernityDB/debug_stuff.py | 211 ++ libs/CodernityDB/env.py | 25 + libs/CodernityDB/hash_index.py | 880 +++++++ libs/CodernityDB/index.py | 195 ++ libs/CodernityDB/indexcreator.py | 645 ++++++ libs/CodernityDB/lfu_cache.py | 150 ++ libs/CodernityDB/lfu_cache_with_lock.py | 158 ++ libs/CodernityDB/migrate.py | 45 + libs/CodernityDB/misc.py | 35 + libs/CodernityDB/patch.py | 99 + libs/CodernityDB/rr_cache.py | 112 + libs/CodernityDB/rr_cache_with_lock.py | 117 + libs/CodernityDB/sharded_hash.py | 146 ++ libs/CodernityDB/sharded_index.py | 112 + libs/CodernityDB/storage.py | 153 ++ libs/CodernityDB/tree_index.py | 2048 +++++++++++++++++ 65 files changed, 7372 insertions(+), 761 deletions(-) create mode 100644 couchpotato/core/media/_base/media/index.py create mode 100644 couchpotato/core/notifications/core/index.py create mode 100644 couchpotato/core/plugins/nosql/__init__.py create mode 100644 couchpotato/core/plugins/nosql/index.py create mode 100644 couchpotato/core/plugins/nosql/main.py delete mode 100644 couchpotato/core/plugins/status/__init__.py delete mode 100644 couchpotato/core/plugins/status/main.py delete mode 100644 couchpotato/core/plugins/status/static/status.js create mode 100644 couchpotato/core/settings/index.py create mode 100644 libs/CodernityDB/__init__.py create mode 100644 libs/CodernityDB/database.py create mode 100644 libs/CodernityDB/database_gevent.py create mode 100644 libs/CodernityDB/database_safe_shared.py create mode 100644 libs/CodernityDB/database_super_thread_safe.py create mode 100644 libs/CodernityDB/database_thread_safe.py create mode 100644 libs/CodernityDB/debug_stuff.py create mode 100644 libs/CodernityDB/env.py create mode 100644 libs/CodernityDB/hash_index.py create mode 100644 libs/CodernityDB/index.py create mode 100644 libs/CodernityDB/indexcreator.py create mode 100644 libs/CodernityDB/lfu_cache.py create mode 100644 libs/CodernityDB/lfu_cache_with_lock.py create mode 100644 libs/CodernityDB/migrate.py create mode 100644 libs/CodernityDB/misc.py create mode 100644 libs/CodernityDB/patch.py create mode 100644 libs/CodernityDB/rr_cache.py create mode 100644 libs/CodernityDB/rr_cache_with_lock.py create mode 100644 libs/CodernityDB/sharded_hash.py create mode 100644 libs/CodernityDB/sharded_index.py create mode 100644 libs/CodernityDB/storage.py create mode 100644 libs/CodernityDB/tree_index.py diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 6b8cfd36..dba45861 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -49,7 +49,11 @@ def addView(route, func, static = False): def get_session(): - return Env.getSession() + return None + + +def get_db(): + return Env.get('db') # Web view diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 02e21f2d..22b5a91b 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -42,11 +42,11 @@ class Core(Plugin): addEvent('app.shutdown', self.shutdown) addEvent('app.restart', self.restart) - addEvent('app.load', self.launchBrowser, priority = 1) + addEvent('app.load2', self.launchBrowser, priority = 1) addEvent('app.base_url', self.createBaseUrl) addEvent('app.api_url', self.createApiUrl) addEvent('app.version', self.version) - addEvent('app.load', self.checkDataDir) + addEvent('app.load2', self.checkDataDir) addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.api_key', self.checkApikey) diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 248d2bc5..1c9d82a6 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -74,7 +74,7 @@ class ClientScript(Plugin): addEvent('clientscript.get_scripts', self.getScripts) if not Env.get('dev'): - addEvent('app.load', self.minify) + addEvent('app.load2', self.minify) self.addCore() diff --git a/couchpotato/core/_base/desktop/main.py b/couchpotato/core/_base/desktop/main.py index c3beff17..21499159 100644 --- a/couchpotato/core/_base/desktop/main.py +++ b/couchpotato/core/_base/desktop/main.py @@ -25,7 +25,7 @@ if Env.get('desktop'): # Events to desktop addEvent('app.after_shutdown', desktop.afterShutdown) - addEvent('app.load', desktop.onAppLoad, priority = 110) + addEvent('app.load2', desktop.onAppLoad, priority = 110) def onClose(self, event): return fireEvent('app.shutdown', single = True) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index ef595ad7..aad4eee2 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -33,8 +33,8 @@ class Updater(Plugin): else: self.updater = SourceUpdater() - addEvent('app.load', self.logVersion, priority = 10000) - addEvent('app.load', self.setCrons) + addEvent('app.load2', self.logVersion, priority = 10000) + addEvent('app.load2', self.setCrons) addEvent('updater.info', self.info) addApiView('updater.info', self.info, docs = { diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 27177756..23b7a25c 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -23,7 +23,7 @@ class rTorrent(Downloader): def __init__(self): super(rTorrent, self).__init__() - addEvent('app.load', self.migrate) + addEvent('app.load2', self.migrate) def migrate(self): diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 512c52c2..db87aa75 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,9 +1,11 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, get_db, CPLog from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Media +log = CPLog(__name__) + class MediaBase(Plugin): @@ -24,20 +26,19 @@ class MediaBase(Plugin): def getType(self): return self._type - def createOnComplete(self, id): + def createOnComplete(self, media_id): def onComplete(): try: - db = get_session() - media = db.query(Media).filter_by(id = id).first() - media_dict = media.to_dict(self.default_dict) - event_name = '%s.searcher.single' % media.type + db = get_db() + media = db.get('id', media_id) + event_name = '%s.searcher.single' % media.get('type') - fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id)) + fireEvent(event_name, media, on_complete = self.createNotifyFront(media_id)) except: log.error('Failed creating onComplete: %s', traceback.format_exc()) finally: - db.close() + pass #db.close() return onComplete @@ -45,15 +46,14 @@ class MediaBase(Plugin): def notifyFront(): try: - db = get_session() - media = db.query(Media).filter_by(id = media_id).first() - media_dict = media.to_dict(self.default_dict) - event_name = '%s.update' % media.type + db = get_db() + media = db.get('id', media_id) + event_name = '%s.update' % media.get('type') - fireEvent('notify.frontend', type = event_name, data = media_dict) + fireEvent('notify.frontend', type = event_name, data = media) except: log.error('Failed creating onComplete: %s', traceback.format_exc()) finally: - db.close() + pass #db.close() return notifyFront diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py new file mode 100644 index 00000000..bd90fcf4 --- /dev/null +++ b/couchpotato/core/media/_base/media/index.py @@ -0,0 +1,60 @@ +from CodernityDB.hash_index import HashIndex +from CodernityDB.tree_index import MultiTreeBasedIndex + + +class MediaIMDBIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = 'I' + super(MediaIMDBIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return int(key.strip('t')) + + def make_key_value(self, data): + if data.get('type') == 'media' and data.get('identifier'): + return int(data['identifier'].strip('t')), None + + +class MediaStatusIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = 's' + super(MediaStatusIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return int(key.strip('t')) + + def make_key_value(self, data): + if data.get('type') == 'media' and data.get('identifier'): + return int(data['identifier'].strip('t')), None + + +class TitleIndex(MultiTreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex +from itertools import izip""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(TitleIndex, self).__init__(*args, **kwargs) + self.__l = kwargs.get('w_len', 2) + + def make_key_value(self, data): + + if data.get('type') == 'title' and len(data.get('title', '')) > 0: + + out = set() + title = data.get('title').lower() + l = self.__l + max_l = len(title) + for x in xrange(l - 1, max_l): + m = (title, ) + for y in xrange(0, x): + m += (title[y + 1:],) + out.update(set(''.join(x).rjust(32, '_').lower() for x in izip(*m))) #ignore import error + + return out, {'media_id': data.get('media_id')} + + def make_key(self, key): + return key.rjust(32, '_').lower() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5345b07c..9a50ffe8 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,11 +1,12 @@ import traceback -from couchpotato import get_session, tryInt +from couchpotato import get_session, tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase +from .index import MediaIMDBIndex, TitleIndex, MediaStatusIndex from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ Media from sqlalchemy.orm import joinedload_all @@ -60,16 +61,40 @@ class MediaPlugin(MediaBase): addApiView('media.available_chars', self.charView) - addEvent('app.load', self.addSingleRefreshView) - addEvent('app.load', self.addSingleListView) - addEvent('app.load', self.addSingleCharView) - addEvent('app.load', self.addSingleDeleteView) + addEvent('database.setup', self.databaseSetup) + + addEvent('app.load2', self.addSingleRefreshView) + addEvent('app.load2', self.addSingleListView) + addEvent('app.load2', self.addSingleCharView) + addEvent('app.load2', self.addSingleDeleteView) addEvent('media.get', self.get) addEvent('media.list', self.list) addEvent('media.delete', self.delete) addEvent('media.restatus', self.restatus) + def databaseSetup(self): + + db = get_db() + + try: + db.add_index(MediaIMDBIndex(db.path, 'media')) + except: + log.debug('Index already exists') + db.update_index(MediaIMDBIndex(db.path, 'media')) + + try: + db.add_index(TitleIndex(db.path, 'media_title')) + except: + log.debug('Index already exists') + db.update_index(TitleIndex(db.path, 'media_title')) + + try: + db.add_index(MediaStatusIndex(db.path, 'media_status')) + except: + log.debug('Index already exists') + db.update_index(MediaStatusIndex(db.path, 'media_status')) + def refresh(self, id = '', **kwargs): handlers = [] ids = splitString(id) @@ -87,24 +112,23 @@ class MediaPlugin(MediaBase): 'success': True, } - def createRefreshHandler(self, id): - db = get_session() + def createRefreshHandler(self, media_id): - media = db.query(Media).filter_by(id = id).first() + try: + media = get_db().get('id', media_id) - if media: - - default_title = getTitle(media.library) - identifier = media.library.identifier - event = 'library.update.%s' % media.type + default_title = getTitle(media_id) + event = 'library.update.%s' % media.get('type') def handler(): - fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id)) + fireEvent(event, identifier = media.get('identifier'), default_title = default_title, on_complete = self.createOnComplete(media_id)) - db.close() + if handler: + return handler + + except: + log.error('Refresh handler for non existing media: %s', traceback.format_exc()) - if handler: - return handler def addSingleRefreshView(self): @@ -113,20 +137,19 @@ class MediaPlugin(MediaBase): def get(self, media_id): - db = get_session() + db = get_db() imdb_id = getImdb(str(media_id)) if imdb_id: - m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first() + m = db.get('media', imdb_id, with_doc = True)['doc'] else: - m = db.query(Media).filter_by(id = media_id).first() + m = db.get('id', media_id) results = None if m: - results = m.to_dict(self.default_dict) + results = db.run('media', 'to_dict', m, self.default_dict) - db.close() return results def getView(self, id = None, **kwargs): @@ -262,7 +285,7 @@ class MediaPlugin(MediaBase): 'releases_count': releases_count.get(media_id), })) - db.close() + pass #db.close() return total_count, movies def listView(self, **kwargs): @@ -356,7 +379,7 @@ class MediaPlugin(MediaBase): if len(chars) == 25: break - db.close() + pass #db.close() return ''.join(sorted(chars)) def charView(self, **kwargs): @@ -428,7 +451,7 @@ class MediaPlugin(MediaBase): log.error('Failed deleting media: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return True @@ -481,5 +504,5 @@ class MediaPlugin(MediaBase): log.error('Failed restatus: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() diff --git a/couchpotato/core/media/_base/search/main.py b/couchpotato/core/media/_base/search/main.py index 81897b5f..6f0f5172 100644 --- a/couchpotato/core/media/_base/search/main.py +++ b/couchpotato/core/media/_base/search/main.py @@ -25,7 +25,7 @@ class Search(Plugin): }"""} }) - addEvent('app.load', self.addSingleSearches) + addEvent('app.load2', self.addSingleSearches) def search(self, q = '', types = None, **kwargs): diff --git a/couchpotato/core/media/_base/searcher/base.py b/couchpotato/core/media/_base/searcher/base.py index 5322d850..60799bf0 100644 --- a/couchpotato/core/media/_base/searcher/base.py +++ b/couchpotato/core/media/_base/searcher/base.py @@ -28,7 +28,7 @@ class SearcherBase(Plugin): fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) - addEvent('app.load', setCrons) + addEvent('app.load2', setCrons) addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons) addEvent('setting.save.%s_searcher.cron_hour.after' % _type, setCrons) addEvent('setting.save.%s_searcher.cron_minute.after' % _type, setCrons) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index a7ecf2d2..306766c4 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -1,5 +1,5 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode @@ -62,78 +62,78 @@ class MovieBase(MovieTypeBase): except: pass - library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library) - - # Status - status_active, snatched_status, ignored_status, done_status, downloaded_status = \ - fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) + # library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library) + info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) default_profile = fireEvent('profile.default', single = True) cat_id = params.get('category_id') try: - db = get_session() - m = db.query(Media).filter_by(library_id = library.get('id')).first() + db = get_db() + + new = False + try: + m = db.get('movie', params.get('identifier'), with_doc = True)['doc'] + except: + new = True + m = db.insert({ + 'type': 'movie', + 'identifier': params.get('identifier'), + 'status': status_id if status_id else 'active', + 'profile_id': params.get('profile_id', default_profile.get('id')), + 'category_id': tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, + }) + added = True do_search = False search_after = search_after and self.conf('search_on_add', section = 'moviesearcher') - if not m: - m = Media( - library_id = library.get('id'), - profile_id = params.get('profile_id', default_profile.get('id')), - status_id = status_id if status_id else status_active.get('id'), - category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, - ) - db.add(m) - db.commit() - + if new: onComplete = None if search_after: - onComplete = self.createOnComplete(m.id) + onComplete = self.createOnComplete(m['_id']) - fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) + # fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) search_after = False elif force_readd: # Clean snatched history - for release in m.releases: - if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]: + for release in db.run('release', 'for_media', m['_id']): + if release.get('status') in ['downloaded', 'snatched', 'done']: if params.get('ignore_previous', False): - release.status_id = ignored_status.get('id') + release['status'] = 'ignored' + db.update(release) else: - fireEvent('release.delete', release.id, single = True) + fireEvent('release.delete', release['_id'], single = True) - m.profile_id = params.get('profile_id', default_profile.get('id')) - m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None) + m['profile_id'] = params.get('profile_id', default_profile.get('id')) + m['category_id'] = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m['category_id'] or None) else: log.debug('Movie already exists, not updating: %s', params) added = False if force_readd: - m.status_id = status_id if status_id else status_active.get('id') - m.last_edit = int(time.time()) + m['status'] = status_id if status_id else 'active' + m['last_edit'] = int(time.time()) do_search = True - db.commit() + db.update(m) # Remove releases - available_status = fireEvent('status.get', 'available', single = True) - for rel in m.releases: - if rel.status_id is available_status.get('id'): + for rel in db.run('release', 'for_media', m['_id']): + if rel['status'] is 'available': db.delete(rel) - db.commit() - movie_dict = m.to_dict(self.default_dict) + movie_dict = db.run('movie', 'to_dict', m['_id']) if do_search and search_after: - onComplete = self.createOnComplete(m.id) + onComplete = self.createOnComplete(m['_id']) onComplete() if added: if params.get('title'): message = 'Successfully added "%s" to your wanted list.' % params.get('title', '') else: - title = getTitle(m.library) + title = getTitle(m) if title: message = 'Successfully added "%s" to your wanted list.' % title else: @@ -142,10 +142,7 @@ class MovieBase(MovieTypeBase): return movie_dict except: - log.error('Failed deleting media: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() + log.error('Failed adding media: %s', traceback.format_exc()) def addView(self, **kwargs): add_dict = self.add(params = kwargs) @@ -158,49 +155,44 @@ class MovieBase(MovieTypeBase): def edit(self, id = '', **kwargs): try: - db = get_session() - - available_status = fireEvent('status.get', 'available', single = True) + db = get_db() ids = splitString(id) for media_id in ids: - m = db.query(Media).filter_by(id = media_id).first() - if not m: - continue + try: + m = db.get('media', media_id) + m['profile_id'] = kwargs.get('profile_id') - m.profile_id = kwargs.get('profile_id') + cat_id = kwargs.get('category_id') + if cat_id is not None: + m['category_id'] = tryInt(cat_id) if tryInt(cat_id) > 0 else None - cat_id = kwargs.get('category_id') - if cat_id is not None: - m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None + # Remove releases + for rel in db.run('release', 'for_media', m['_id']): + if rel['status'] is 'available': + db.delete(rel) - # Remove releases - for rel in m.releases: - if rel.status_id is available_status.get('id'): - db.delete(rel) - db.commit() + # Default title + if kwargs.get('default_title'): + for title in m['titles']: + title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() - # Default title - if kwargs.get('default_title'): - for title in m.library.titles: - title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() + db.update(m) - db.commit() + fireEvent('media.restatus', m['_id']) - fireEvent('media.restatus', m.id) + movie_dict = db.run('media', 'to_dict', m['_id']) + fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) - movie_dict = m.to_dict(self.default_dict) - fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) + except: + log.error('Can\'t edit non-existing media') return { 'success': True, } except: - log.error('Failed deleting media: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() + log.error('Failed editing media: %s', traceback.format_exc()) return { 'success': False, diff --git a/couchpotato/core/media/movie/library/movie/main.py b/couchpotato/core/media/movie/library/movie/main.py index 034a8fb0..663c6d04 100644 --- a/couchpotato/core/media/movie/library/movie/main.py +++ b/couchpotato/core/media/movie/library/movie/main.py @@ -62,7 +62,7 @@ class MovieLibraryPlugin(LibraryBase): log.error('Failed adding media: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return {} @@ -130,31 +130,24 @@ class MovieLibraryPlugin(LibraryBase): # Files images = info.get('images', []) + library['files'] = [] for image_type in ['poster']: for image in images.get(image_type, []): if not isinstance(image, (str, unicode)): continue file_path = fireEvent('file.download', url = image, single = True) - if file_path: - file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True) - try: - file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() - library.files.append(file_obj) - db.commit() - break - except: - log.debug('Failed to attach to library: %s', traceback.format_exc()) - db.rollback() + # TODO: save in movie doc + library['files'].append({ + 'type': 'image_%s' % image_type, + 'path': file_path + }) library_dict = library.to_dict(self.default_dict) return library_dict except: log.error('Failed update media: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() return {} @@ -180,7 +173,7 @@ class MovieLibraryPlugin(LibraryBase): log.error('Failed updating release dates: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return {} diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index bde5b44b..b4665deb 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -1,4 +1,4 @@ -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString @@ -51,7 +51,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): }) if self.conf('run_on_launch'): - addEvent('app.load', self.searchAll) + addEvent('app.load2', self.searchAll) def searchAllView(self, **kwargs): @@ -71,27 +71,16 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.in_progress = True fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started') - db = get_session() + db = get_db() - movies_raw = db.query(Media).filter( - Media.status.has(identifier = 'active') - ).all() + movies = db.get_many('movie_status', 'active') - random.shuffle(movies_raw) - - movies = [] - for m in movies_raw: - movies.append(m.to_dict({ - 'category': {}, - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}}, - 'library': {'titles': {}, 'files': {}}, - 'files': {}, - })) + #TODO: random.shuffle(movies_raw) + total = db.count(db.get_many, 'movie_status', 'active') self.in_progress = { - 'total': len(movies), - 'to_go': len(movies), + 'total': total, + 'to_go': total, } try: @@ -99,13 +88,15 @@ class MovieSearcher(SearcherBase, MovieTypeBase): for movie in movies: + movie_dict = db.run('media', 'to_dict', movie['_id']) + try: - self.single(movie, search_protocols) + self.single(movie_dict, search_protocols) except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['library']['identifier'], traceback.format_exc())) - fireEvent('library.update.movie', movie['library']['identifier']) + log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['identifier'], traceback.format_exc())) + fireEvent('library.update.movie', movie['identifier']) except: - log.error('Search failed for %s: %s', (movie['library']['identifier'], traceback.format_exc())) + log.error('Search failed for %s: %s', (movie['identifier'], traceback.format_exc())) self.in_progress['to_go'] -= 1 @@ -117,7 +108,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): pass self.in_progress = False - db.close() def single(self, movie, search_protocols = None, manual = False): @@ -132,32 +122,30 @@ class MovieSearcher(SearcherBase, MovieTypeBase): except SearchSetupError: return - done_status = fireEvent('status.get', 'done', single = True) - - if not movie['profile'] or (movie['status_id'] == done_status.get('id') and not manual): + if not movie['profile_id'] or (movie['status'] == 'done' and not manual): log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') return pre_releases = fireEvent('quality.pre_releases', single = True) release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True) - available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) found_releases = [] too_early_to_search = [] - default_title = getTitle(movie['library']) + default_title = getTitle(movie) if not default_title: log.error('No proper info found for movie, removing it from library to cause it from having more issues.') - fireEvent('media.delete', movie['id'], single = True) + fireEvent('media.delete', movie['_id'], single = True) return - fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title) + fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['_id']}, message = 'Searching for "%s"' % default_title) - db = get_session() + db = get_db() ret = False + for quality_type in movie['profile']['types']: - if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): + if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['year']): too_early_to_search.append(quality_type['quality']['identifier']) continue @@ -165,7 +153,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # See if better quality is available for release in movie['releases']: - if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]: + if release['quality']['order'] <= quality_type['quality']['order'] and release['status'] not in ['available', 'ignored', 'failed']: has_better_quality += 1 # Don't search for quality lower then already available. @@ -179,7 +167,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): 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(Media).filter_by(id = movie.get('id')).first(): + if not fireEvent('media.get', movie.get('_id'), single = True): break # Add them to this movie releases list @@ -191,12 +179,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # Remove releases that aren't found anymore for release in movie.get('releases', []): - if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('id'), single = True) + if release.get('status') == 'available' and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('_id'), single = True) else: log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) - fireEvent('media.restatus', movie['id']) + fireEvent('media.restatus', movie['_id']) break # Break if CP wants to shut down @@ -206,9 +194,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if len(too_early_to_search) > 0: log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']}) + fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['_id']}) - db.close() + pass #db.close() return ret def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): @@ -329,31 +317,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase): def tryNextRelease(self, media_id, manual = False): - snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True) - try: - db = get_session() - rels = db.query(Release) \ - .filter_by(movie_id = media_id) \ - .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \ - .all() + db = get_db() + rels = db.run('media', 'with_status', media_id, status = ['snatched', 'done']) for rel in rels: - rel.status_id = ignored_status.get('id') - db.commit() + rel['status'] = 'ignored' + db.update(rel) - movie_dict = fireEvent('media.get', media_id = media_id, single = True) - log.info('Trying next release for: %s', getTitle(movie_dict['library'])) - fireEvent('movie.searcher.single', movie_dict, manual = manual) + movie_dict = db.run('media', 'to_dict', media_id) + log.info('Trying next release for: %s', getTitle(movie_dict)) + self.single(movie_dict, manual = manual) return True except: log.error('Failed searching for next release: %s', traceback.format_exc()) - db.rollback() return False - finally: - db.close() def getSearchTitle(self, media): if media['type'] == 'movie': diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index d1d64089..eaccbe64 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -33,7 +33,7 @@ class Suggestion(Plugin): .options(joinedload_all('library')) \ .filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all() movies = [x.library.identifier for x in active_movies] - db.close() + pass #db.close() if not ignored or len(ignored) == 0: ignored = splitString(Env.prop('suggest_ignore', default = '')) @@ -98,7 +98,7 @@ class Suggestion(Plugin): .filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all() movies = [x[0] for x in active_movies] movies.extend(seen) - db.close() + pass #db.close() ignored.extend([x.get('imdb') for x in cached_suggestion]) suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True) diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py new file mode 100644 index 00000000..e51124d6 --- /dev/null +++ b/couchpotato/core/notifications/core/index.py @@ -0,0 +1,42 @@ +import time +from CodernityDB.tree_index import TreeBasedIndex + + +class NotificationIndex(TreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import TreeBasedIndex +import time""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = 'I' + super(NotificationIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return key + + def make_key_value(self, data): + if data.get('type') == 'notification': + added = data.get('added', time.time()) + data['added'] = added + + return added, None + + +class NotificationUnreadIndex(TreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import TreeBasedIndex +import time""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = 'I' + super(NotificationUnreadIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return key + + def make_key_value(self, data): + if data.get('type') == 'notification' and not data.get('read'): + added = data.get('added', time.time()) + data['added'] = added + + return added, None diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index d77758b6..4a245cdd 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -1,14 +1,13 @@ -from couchpotato import get_session +from couchpotato import get_db from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -from couchpotato.core.settings.model import Notification as Notif +from .index import NotificationIndex, NotificationUnreadIndex from couchpotato.environment import Env from operator import itemgetter -from sqlalchemy.sql.expression import or_ import threading import time import traceback @@ -58,48 +57,55 @@ class CoreNotifier(Notification): fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True) fireEvent('schedule.interval', 'core.clean_messages', self.cleanMessages, seconds = 15, single = True) - addEvent('app.load', self.clean) - addEvent('app.load', self.checkMessages) + addEvent('app.load2', self.clean) + addEvent('app.load2', self.checkMessages) + + addEvent('database.setup', self.databaseSetup) self.messages = [] self.listeners = [] self.m_lock = threading.Lock() - def clean(self): + def databaseSetup(self): + + db = get_db() try: - db = get_session() - db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete() - db.commit() + db.add_index(NotificationIndex(db.path, 'notification')) + except: + log.debug('Index already exists') + db.edit_index(NotificationIndex(db.path, 'notification')) + + try: + db.add_index(NotificationUnreadIndex(db.path, 'notification_unread')) + except: + log.debug('Index already exists') + db.edit_index(NotificationUnreadIndex(db.path, 'notification_unread')) + + def clean(self): + try: + db = get_db() + for n in db.all('notification', with_doc = True): + if n['doc']['added'] <= (int(time.time()) - 2419200): + db.delete(n['doc']) except: log.error('Failed cleaning notification: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() def markAsRead(self, ids = None, **kwargs): ids = splitString(ids) if ids else None try: - db = get_session() - - if ids: - q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids])) - else: - q = db.query(Notif).filter_by(read = False) - - q.update({Notif.read: True}) - db.commit() - + db = get_db() + for x in db.all('notification_unread', with_doc = True): + if not ids or x['_id'] in ids: + x['doc']['read'] = True + db.update(x['doc']) return { 'success': True } except: log.error('Failed mark as read: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() return { 'success': False @@ -107,26 +113,20 @@ class CoreNotifier(Notification): def listView(self, limit_offset = None, **kwargs): - db = get_session() - - q = db.query(Notif) + db = get_db() if limit_offset: splt = splitString(limit_offset) limit = splt[0] offset = 0 if len(splt) is 1 else splt[1] - q = q.limit(limit).offset(offset) + results = db.get_many('notification', limit = limit, offset = offset, with_doc = True) else: - q = q.limit(200) + results = db.get_many('notification', limit = 200, with_doc = True) - results = q.all() notifications = [] for n in results: - ndict = n.to_dict() - ndict['type'] = 'notification' - notifications.append(ndict) + notifications.append(n['doc']) - db.close() return { 'success': True, 'empty': len(notifications) == 0, @@ -156,30 +156,23 @@ class CoreNotifier(Notification): if not data: data = {} try: - db = get_session() + db = get_db() data['notification_type'] = listener if listener else 'unknown' - n = Notif( - message = toUnicode(message), - data = data - ) - db.add(n) - db.commit() + n = { + 'type': 'notification', + 'time': time.time(), + 'message': toUnicode(message), + 'data': data + } + db.insert(n) - ndict = n.to_dict() - ndict['type'] = 'notification' - ndict['time'] = time.time() + self.frontend(type = listener, data = n) - self.frontend(type = listener, data = data) - - db.close() return True except: log.error('Failed notify: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() def frontend(self, type = 'notification', data = None, message = None): if not data: data = {} @@ -278,18 +271,13 @@ class CoreNotifier(Notification): # Get unread if init: - db = get_session() + db = get_db() - notifications = db.query(Notif) \ - .filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \ - .all() + notifications = db.all('notification_unread', with_doc = True) for n in notifications: - ndict = n.to_dict() - ndict['type'] = 'notification' - messages.append(ndict) - - db.close() + if n['doc'].get('added') > (time.time() - 259200): + messages.append(n['doc']) return { 'success': True, diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index a3927ed2..68d73cb1 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -16,7 +16,7 @@ class Growl(Notification): super(Growl, self).__init__() if self.isEnabled(): - addEvent('app.load', self.register) + addEvent('app.load2', self.register) def register(self): if self.registered: return diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 2edcd3be..44d7673f 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -10,10 +10,10 @@ class Automation(Plugin): def __init__(self): - addEvent('app.load', self.setCrons) + addEvent('app.load2', self.setCrons) if not Env.get('dev'): - addEvent('app.load', self.addMovies) + addEvent('app.load2', self.addMovies) addEvent('setting.save.automation.hour.after', self.setCrons) diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index 41965b03..b26a8293 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -42,7 +42,7 @@ class CategoryPlugin(Plugin): for category in categories: temp.append(category.to_dict()) - db.close() + pass #db.close() return temp def save(self, **kwargs): @@ -66,7 +66,7 @@ class CategoryPlugin(Plugin): category_dict = c.to_dict() - db.close() + pass #db.close() return { 'success': True, 'category': category_dict @@ -75,7 +75,7 @@ class CategoryPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False, @@ -96,7 +96,7 @@ class CategoryPlugin(Plugin): db.commit() - db.close() + pass #db.close() return { 'success': True } @@ -104,7 +104,7 @@ class CategoryPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -129,7 +129,7 @@ class CategoryPlugin(Plugin): except Exception as e: message = log.error('Failed deleting category: %s', e) - db.close() + pass #db.close() return { 'success': success, 'message': message @@ -138,7 +138,7 @@ class CategoryPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -158,4 +158,4 @@ class CategoryPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() diff --git a/couchpotato/core/plugins/custom/main.py b/couchpotato/core/plugins/custom/main.py index a15c915c..42400994 100644 --- a/couchpotato/core/plugins/custom/main.py +++ b/couchpotato/core/plugins/custom/main.py @@ -10,7 +10,7 @@ log = CPLog(__name__) class Custom(Plugin): def __init__(self): - addEvent('app.load', self.createStructure) + addEvent('app.load2', self.createStructure) def createStructure(self): diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 73866411..bc14f4b5 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -119,7 +119,7 @@ class Dashboard(Plugin): 'files': {}, })) - db.close() + pass #db.close() return { 'success': True, 'empty': len(movies) == 0, diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 18071547..c24d0d58 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -19,9 +19,7 @@ log = CPLog(__name__) class FileManager(Plugin): def __init__(self): - addEvent('file.add', self.add) addEvent('file.download', self.download) - addEvent('file.types', self.getTypes) addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = { 'desc': 'Return a file from the cp_data/cache directory', @@ -31,36 +29,8 @@ class FileManager(Plugin): 'return': {'type': 'file'} }) - addApiView('file.types', self.getTypesView, docs = { - 'desc': 'Return a list of all the file types and their ids.', - 'return': {'type': 'object', 'example': """{ - 'types': [ - { - "identifier": "poster_original", - "type": "image", - "id": 1, - "name": "Poster_original" - }, - { - "identifier": "poster", - "type": "image", - "id": 2, - "name": "Poster" - }, - etc - ] -}"""} - }) - - addEvent('app.load', self.cleanup) - addEvent('app.load', self.init) - - def init(self): - - for type_tuple in Scanner.file_types.values(): - self.getType(type_tuple) - def cleanup(self): + # TODO: unused # Wait a bit after starting before cleanup time.sleep(3) @@ -77,9 +47,6 @@ class FileManager(Plugin): os.remove(file_path) except: log.error('Failed removing unused file: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() def showCacheFile(self, route, **kwargs): Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})]) @@ -101,76 +68,3 @@ class FileManager(Plugin): self.createFile(dest, filedata, binary = True) return dest - - def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = None): - if not properties: properties = {} - - try: - db = get_session() - type_id = self.getType(type_tuple).get('id') - - f = db.query(File).filter(File.path == toUnicode(path)).first() - if not f: - f = File() - db.add(f) - - f.path = toUnicode(path) - f.part = part - f.available = available - f.type_id = type_id - - db.commit() - - file_dict = f.to_dict() - - return file_dict - except: - log.error('Failed adding file: %s, %s', (path, traceback.format_exc())) - db.rollback() - finally: - db.close() - - def getType(self, type_tuple): - - try: - db = get_session() - type_type, type_identifier = type_tuple - - ft = db.query(FileType).filter_by(identifier = type_identifier).first() - if not ft: - ft = FileType( - type = toUnicode(type_type), - identifier = type_identifier, - name = toUnicode(type_identifier[0].capitalize() + type_identifier[1:]) - ) - db.add(ft) - db.commit() - - type_dict = ft.to_dict() - - return type_dict - except: - log.error('Failed getting type: %s, %s', (type_tuple, traceback.format_exc())) - db.rollback() - finally: - db.close() - - - def getTypes(self): - - db = get_session() - - results = db.query(FileType).all() - - types = [] - for type_object in results: - types.append(type_object.to_dict()) - - db.close() - return types - - def getTypesView(self, **kwargs): - - return { - 'types': self.getTypes() - } diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 2f297491..47081692 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -47,7 +47,7 @@ class Manage(Plugin): }) if not Env.get('dev') and self.conf('startup_scan'): - addEvent('app.load', self.updateLibraryQuick) + addEvent('app.load2', self.updateLibraryQuick) def getProgress(self, **kwargs): return { diff --git a/couchpotato/core/plugins/nosql/__init__.py b/couchpotato/core/plugins/nosql/__init__.py new file mode 100644 index 00000000..7f0e952e --- /dev/null +++ b/couchpotato/core/plugins/nosql/__init__.py @@ -0,0 +1,7 @@ +from .main import NoSQL + + +def start(): + return NoSQL() + +config = [] diff --git a/couchpotato/core/plugins/nosql/index.py b/couchpotato/core/plugins/nosql/index.py new file mode 100644 index 00000000..e1361799 --- /dev/null +++ b/couchpotato/core/plugins/nosql/index.py @@ -0,0 +1,20 @@ +from hashlib import md5 +from CodernityDB.tree_index import TreeBasedIndex + + +class ReleaseIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(ReleaseIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'release': + return md5(data['media_id']).digest(), None + + def run_for_media(self, db, media_id): + for release in db.get_many('release', media_id, with_doc=True): + yield release['doc'] diff --git a/couchpotato/core/plugins/nosql/main.py b/couchpotato/core/plugins/nosql/main.py new file mode 100644 index 00000000..11f15cb9 --- /dev/null +++ b/couchpotato/core/plugins/nosql/main.py @@ -0,0 +1,72 @@ +import time +from couchpotato import CPLog +from couchpotato.core.plugins.base import Plugin +from .index import ReleaseIndex, MediaIMDBIndex, TitleIndex + +log = CPLog(__name__) + + +class NoSQL(Plugin): + + db = None + + def test(self): + + db = self.db + + try: db.add_index(ReleaseIndex(db.path, 'release')) + except: log.debug('Index already exists') + + for id in range(10): + media = db.insert({ + 'type': 'media', + 'tmdb': id, + 'imdb': 'tt%s' % id, + 'last_edit': 0, + 'status': 'active', + 'title': 'Lord of the Rings: The Return of the King', + 'year': 2011, + 'profile_id': 0, + 'category_id': 0, + }) + + db.insert({ + 'media_id': media['_id'], + 'type': 'title', + 'title': 'Lord of the Rings: The Return of the King', + }) + + for x in range(40): + db.insert({ + 'media_id': media['_id'], + 'type': 'release', + 'name': 'Release %s' % x + }) + + print db.count(db.all, 'media') + + m = db.get('media', 'tt0') + db.get('id', m['_id']) + + start = time.time() + print list(db.get_many('media_title', 'lord of')) + print time.time() - start + + return + + return + + for media in db.all('media', with_doc = True): + doc = media['doc'] + for r in db.run('release', 'for_media', media['_id']): + db.delete(r) + + db.delete(doc) + break + + start = time.time() + db.reindex() + print time.time() - start + + print db.count(db.all, 'media') + print db.count(db.all, 'release') diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index cc048733..9d721852 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -1,5 +1,5 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode @@ -31,27 +31,24 @@ class ProfilePlugin(Plugin): }) addEvent('app.initialize', self.fill, priority = 90) - addEvent('app.load', self.forceDefaults) + addEvent('app.load2', self.forceDefaults) def forceDefaults(self): # Get all active movies without profile - active_status = fireEvent('status.get', 'active', single = True) - try: - db = get_session() - movies = db.query(Media).filter(Media.status_id == active_status.get('id'), Media.profile == None).all() + db = get_db() + medias = db.run('media', 'with_status', ['active']) - if len(movies) > 0: - default_profile = self.default() - for movie in movies: - movie.profile_id = default_profile.get('id') - db.commit() + profile_ids = [x.get('_id') for x in self.all()] + + for media in medias: + if media['profile_id'] not in profile_ids: + default_profile = self.default() + media['profile_id'] = default_profile.get('id') + db.update(media) except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() def allView(self, **kwargs): @@ -62,17 +59,10 @@ class ProfilePlugin(Plugin): def all(self): - db = get_session() - profiles = db.query(Profile) \ - .options(joinedload_all('types')) \ - .all() + db = get_db() + profiles = db.all('profile', with_doc = True) - temp = [] - for profile in profiles: - temp.append(profile.to_dict(self.to_dict)) - - db.close() - return temp + return list(profiles) def save(self, **kwargs): @@ -115,7 +105,7 @@ class ProfilePlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -128,7 +118,7 @@ class ProfilePlugin(Plugin): .options(joinedload_all('types')) \ .first() default_dict = default.to_dict(self.to_dict) - db.close() + pass #db.close() return default_dict @@ -154,7 +144,7 @@ class ProfilePlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -188,7 +178,7 @@ class ProfilePlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -243,6 +233,6 @@ class ProfilePlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return False diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index cc6215d3..e5070093 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -79,7 +79,7 @@ class QualityPlugin(Plugin): self.cached_qualities = temp - db.close() + pass #db.close() return temp def single(self, identifier = ''): @@ -91,7 +91,7 @@ class QualityPlugin(Plugin): if quality: quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) - db.close() + pass #db.close() return quality_dict def getQuality(self, identifier): @@ -119,7 +119,7 @@ class QualityPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return { 'success': False @@ -181,7 +181,7 @@ class QualityPlugin(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return False diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 1e1352c5..9066fa18 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -1,4 +1,4 @@ -from couchpotato import get_session, md5 +from couchpotato import get_session, md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toUnicode @@ -72,34 +72,32 @@ class Release(Plugin): done_status, available_status, snatched_status, downloaded_status, ignored_status = \ fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True) - db = get_session() + db = get_db() # get movies last_edit more than a week ago - media = db.query(Media) \ - .filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \ - .all() + medias = db.run('media', 'with_status', ['done']) - for item in media: - for rel in item.releases: + for media in medias: + if media['last_edit'] > (now - week): + continue + + for rel in db.run('release', 'for_media', media['_id']): # Remove all available releases - if rel.status_id in [available_status.get('id')]: - fireEvent('release.delete', id = rel.id, single = True) - # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move - elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]: - self.updateStatus(id = rel.id, status = ignored_status) + if rel['status'] in ['available']: + fireEvent('release.delete', id = rel['_id'], single = True) - db.close() + # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move + elif rel['status'] in ['snatched', 'downloaded']: + self.updateStatus(id = rel['id'], status = ignored_status) def add(self, group): try: - db = get_session() + db = get_db() identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) - done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True) - # Add movie media = db.query(Media).filter_by(library_id = group['library'].get('id')).first() if not media: @@ -129,41 +127,28 @@ class Release(Plugin): db.commit() # Add each file type - added_files = [] + rel['files'] = [] for type in group['files']: for cur_file in group['files'][type]: - added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie') - added_files.append(added_file.get('id')) + added_file = self.saveFile(cur_file, type = type) + rel['files'].append(added_file.get('id')) - # Add the release files in batch - try: - added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all() - rel.files.extend(added_files) - db.commit() - except: - log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc())) - - fireEvent('media.restatus', media.id) + fireEvent('media.restatus', media['_id']) return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() return False def saveFile(self, filepath, type = 'unknown', include_media_info = False): - properties = {} - - # Get media info for files - if include_media_info: - properties = {} - # Check database and update/insert if necessary - return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True) + return { + 'type': '%s_%s' % Scanner.file_types.get(type), + 'path': filepath, + 'part': fireEvent('scanner.partnumber', file, single = True), + } def deleteView(self, id = None, **kwargs): @@ -171,60 +156,58 @@ class Release(Plugin): 'success': self.delete(id) } - def delete(self, id): + def delete(self, release_id): try: - db = get_session() + db = get_db() - rel = db.query(Relea).filter_by(id = id).first() - if rel: - db.delete(rel) - db.commit() - return True + rel = db.get('release', release_id, with_doc = True) + db.delete(rel['doc']) + return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() return False - def clean(self, id): + def clean(self, release_id): try: - db = get_session() + db = get_db() - rel = db.query(Relea).filter_by(id = id).first() - if rel: - for release_file in rel.files: - if not os.path.isfile(ss(release_file.path)): - db.delete(release_file) - db.commit() + rel = db.get('release', release_id, with_doc = True) + files = [] + for release_file in rel['files']: + if os.path.isfile(ss(release_file['path'])): + files.append(release_file) - if len(rel.files) == 0: - self.delete(id) + if len(rel['files']) == 0: + self.delete(rel['_id']) + else: + rel['files'] = files + db.update(rel) - return True + return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() return False - def ignore(self, id = None, **kwargs): + def ignore(self, release_id = None, **kwargs): - db = get_session() + db = get_db() - rel = db.query(Relea).filter_by(id = id).first() - if rel: - ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True) - self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status) + try: + rel = db.get('release', release_id, with_doc = True)['doc'] + self.updateStatus(release_id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored') + + return { + 'success': True + } + except: + log.error('Failed: %s', traceback.format_exc()) - db.close() return { - 'success': True + 'success': False } def manualDownload(self, id = None, **kwargs): @@ -265,7 +248,7 @@ class Release(Plugin): if success: fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) - db.close() + pass #db.close() return { 'success': success == True } @@ -361,7 +344,7 @@ class Release(Plugin): db.rollback() return False finally: - db.close() + pass #db.close() return True @@ -441,7 +424,7 @@ class Release(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return [] @@ -458,7 +441,7 @@ class Release(Plugin): releases = [r.to_dict({'info': {}, 'files': {}}) for r in releases_raw] releases = sorted(releases, key = lambda k: k['info'].get('score', 0), reverse = True) - db.close() + pass #db.close() return releases def forMovieView(self, id = None, **kwargs): @@ -506,6 +489,6 @@ class Release(Plugin): log.error('Failed: %s', traceback.format_exc()) db.rollback() finally: - db.close() + pass #db.close() return False diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index c4c67deb..868dbc59 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -598,7 +598,7 @@ class Renamer(Plugin): break self.renaming_started = False - db.close() + pass #db.close() def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False): if not group: group = {} @@ -1045,7 +1045,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.checking_snatched = False return True finally: - db.close() + pass #db.close() def extendReleaseDownload(self, release_download): @@ -1068,7 +1068,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) if not rls: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) - db.close() + pass #db.close() if rls: diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 00a5b752..7e8fb9bb 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -441,7 +441,7 @@ class Scanner(Plugin): else: log.debug('Found no movies in the folder %s', folder) - db.close() + pass #db.close() return processed_movies def getMetaData(self, group, folder = '', release_download = None): @@ -609,7 +609,7 @@ class Scanner(Plugin): break except: pass - db.close() + pass #db.close() # Search based on identifiers if not imdb_id: diff --git a/couchpotato/core/plugins/status/__init__.py b/couchpotato/core/plugins/status/__init__.py deleted file mode 100644 index 204fbee7..00000000 --- a/couchpotato/core/plugins/status/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import StatusPlugin - - -def start(): - return StatusPlugin() - -config = [] diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py deleted file mode 100644 index 55852610..00000000 --- a/couchpotato/core/plugins/status/main.py +++ /dev/null @@ -1,137 +0,0 @@ -import traceback -from couchpotato import get_session -from couchpotato.api import addApiView -from couchpotato.core.event import addEvent -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Status - -log = CPLog(__name__) - - -class StatusPlugin(Plugin): - - statuses = { - 'needs_update': 'Needs update', - 'active': 'Active', - 'done': 'Done', - 'downloaded': 'Downloaded', - 'wanted': 'Wanted', - 'snatched': 'Snatched', - 'failed': 'Failed', - 'deleted': 'Deleted', - 'ignored': 'Ignored', - 'available': 'Available', - 'suggest': 'Suggest', - 'seeding': 'Seeding', - 'missing': 'Missing', - } - status_cached = {} - - def __init__(self): - addEvent('status.get', self.get) - addEvent('status.get_by_id', self.getById) - addEvent('status.all', self.all) - addEvent('app.initialize', self.fill) - addEvent('app.load', self.all) # Cache all statuses - - addApiView('status.list', self.list, docs = { - 'desc': 'Check for available update', - 'return': {'type': 'object', 'example': """{ - 'success': True, - 'list': array, statuses -}"""} - }) - - def list(self, **kwargs): - - return { - 'success': True, - 'list': self.all() - } - - def getById(self, id): - db = get_session() - status = db.query(Status).filter_by(id = id).first() - status_dict = status.to_dict() - db.close() - - return status_dict - - def all(self): - - db = get_session() - - statuses = db.query(Status).all() - - temp = [] - for status in statuses: - s = status.to_dict() - temp.append(s) - - # Update cache - self.status_cached[status.identifier] = s - - db.close() - return temp - - def get(self, identifiers): - - if not isinstance(identifiers, list): - identifiers = [identifiers] - - try: - db = get_session() - return_list = [] - - for identifier in identifiers: - - if self.status_cached.get(identifier): - return_list.append(self.status_cached.get(identifier)) - continue - - s = db.query(Status).filter_by(identifier = identifier).first() - if not s: - s = Status( - identifier = identifier, - label = toUnicode(identifier.capitalize()) - ) - db.add(s) - db.commit() - - status_dict = s.to_dict() - - self.status_cached[identifier] = status_dict - return_list.append(status_dict) - - return return_list if len(identifiers) > 1 else return_list[0] - except: - log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() - - def fill(self): - - try: - db = get_session() - - for identifier, label in self.statuses.items(): - s = db.query(Status).filter_by(identifier = identifier).first() - if not s: - log.info('Creating status: %s', label) - s = Status( - identifier = identifier, - label = toUnicode(label) - ) - db.add(s) - - s.label = toUnicode(label) - db.commit() - except: - log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() - diff --git a/couchpotato/core/plugins/status/static/status.js b/couchpotato/core/plugins/status/static/status.js deleted file mode 100644 index 2b8d30f3..00000000 --- a/couchpotato/core/plugins/status/static/status.js +++ /dev/null @@ -1,17 +0,0 @@ -var StatusBase = new Class({ - - setup: function(statuses){ - var self = this; - - self.statuses = statuses; - - }, - - get: function(id){ - return this.statuses.filter(function(status){ - return status.id == id - }).pick() - }, - -}); -window.Status = new StatusBase(); diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index e51df35e..b8d9a06b 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -41,7 +41,7 @@ class Subtitle(Plugin): # get subtitles for those files subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) - db.close() + pass #db.close() def searchSingle(self, group): if self.isDisabled(): return diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index 598a01d7..cefecb74 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -104,7 +104,7 @@ class MovieResultModifier(Plugin): except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) - db.close() + pass #db.close() return temp def checkLibrary(self, result): diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 72d07609..3f336be4 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -90,6 +90,7 @@ class MetaDataBase(Plugin): file_type = {} for ft in file_types: + # TODO: change type to "image_"+wanted_file_type if ft.get('identifier') == wanted_file_type: file_type = ft break diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index e6565d07..bbbc9381 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -4,6 +4,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat +from couchpotato.core.settings.index import PropertyIndex from couchpotato.core.settings.model import Properties import ConfigParser @@ -42,6 +43,7 @@ class Settings(object): } }"""} }) + addApiView('settings.save', self.saveView, docs = { 'desc': 'Save setting to config file (settings.conf)', 'params': { @@ -51,6 +53,8 @@ class Settings(object): } }) + addEvent('database.setup', self.databaseSetup) + def setFile(self, config_file): self.file = config_file @@ -62,6 +66,17 @@ class Settings(object): self.connectEvents() + def databaseSetup(self): + from couchpotato import get_db + + db = get_db() + + try: + db.add_index(PropertyIndex(db.path, 'property')) + except: + self.log.debug('Index already exists') + db.edit_index(PropertyIndex(db.path, 'property')) + def parser(self): return self.p @@ -206,36 +221,33 @@ class Settings(object): } def getProperty(self, identifier): - from couchpotato import get_session + from couchpotato import get_db - db = get_session() + db = get_db() prop = None try: - propert = db.query(Properties).filter_by(identifier = identifier).first() - prop = propert.value + propert = db.get('property', identifier, with_doc = True) + prop = propert['doc']['value'] except: - pass + self.log.debug('Property doesn\'t exist: %s', traceback.format_exc(0)) - db.close() return prop def setProperty(self, identifier, value = ''): - from couchpotato import get_session + from couchpotato import get_db + + db = get_db() try: - db = get_session() - - p = db.query(Properties).filter_by(identifier = identifier).first() - if not p: - p = Properties() - db.add(p) - - p.identifier = identifier - p.value = toUnicode(value) - - db.commit() + p = db.get('property', identifier, with_doc = True) + p['doc'].update({ + 'identifier': identifier, + 'value': toUnicode(value), + }) + db.update(p['doc']) except: - self.log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - db.close() + db.insert({ + 'type': 'property', + 'identifier': identifier, + 'value': toUnicode(value), + }) diff --git a/couchpotato/core/settings/index.py b/couchpotato/core/settings/index.py new file mode 100644 index 00000000..45d366eb --- /dev/null +++ b/couchpotato/core/settings/index.py @@ -0,0 +1,16 @@ +from CodernityDB.hash_index import UniqueHashIndex, HashIndex +from hashlib import md5 + + +class PropertyIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(PropertyIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'property': + return md5(data['identifier']).digest(), None diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 42f8fded..5eb1272c 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -31,7 +31,7 @@ class Env(object): _app_dir = "" _data_dir = "" _cache_dir = "" - _db_path = "" + _db = "" _log_path = "" @staticmethod @@ -54,28 +54,6 @@ class Env(object): def set(attr, value): return setattr(Env, '_' + attr, value) - @staticmethod - def getSession(): - existing_session = Env.get('session') - if existing_session: - return existing_session() - - session = scoped_session(sessionmaker(bind = Env.getEngine())) - Env.set('session', session) - - return session() - - @staticmethod - def getEngine(): - existing_engine = Env.get('engine') - if existing_engine: - return existing_engine - - engine = create_engine(Env.get('db_path'), echo = False) - Env.set('engine', engine) - - return engine - @staticmethod def setting(attr, section = 'core', value = None, default = '', type = None): diff --git a/couchpotato/runner.py b/couchpotato/runner.py index bd033983..744d425b 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -1,3 +1,4 @@ +from CodernityDB.database_thread_safe import ThreadSafeDatabase from argparse import ArgumentParser from cache import FileSystemCache from couchpotato import KeyHandler, LoginHandler, LogoutHandler @@ -82,52 +83,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En Env.set('encoding', encoding) # Do db stuff - db_path = toUnicode(os.path.join(data_dir, 'couchpotato.db')) + db_path = toUnicode(os.path.join(data_dir, 'database')) - # Backup before start and cleanup old databases - new_backup = toUnicode(os.path.join(data_dir, 'db_backup', str(int(time.time())))) - if not os.path.isdir(new_backup): os.makedirs(new_backup) + # Check if database exists + print db_path - # Remove older backups, keep backups 3 days or at least 3 - backups = [] - for directory in os.listdir(os.path.dirname(new_backup)): - backup = toUnicode(os.path.join(os.path.dirname(new_backup), directory)) - if os.path.isdir(backup): - backups.append(backup) + db = ThreadSafeDatabase(db_path) + db_exists = db.exists() + if db_exists: + db.open() + else: + db.create() - latest_backup = tryInt(os.path.basename(sorted(backups)[-1])) if len(backups) > 0 else 0 - if latest_backup < time.time() - 3600: - # Create path and copy - src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal'] - for src_file in src_files: - if os.path.isfile(src_file): - dst_file = toUnicode(os.path.join(new_backup, os.path.basename(src_file))) - shutil.copyfile(src_file, dst_file) - - # Try and copy stats seperately - try: shutil.copystat(src_file, dst_file) - except: pass - - total_backups = len(backups) - for backup in backups: - if total_backups > 3: - if tryInt(os.path.basename(backup)) < time.time() - 259200: - for the_file in os.listdir(backup): - file_path = os.path.join(backup, the_file) - try: - if os.path.isfile(file_path): - os.remove(file_path) - except: - raise - - os.rmdir(backup) - total_backups -= 1 + # TODO:Backup before start and cleanup old databases # Register environment settings Env.set('app_dir', toUnicode(base_path)) Env.set('data_dir', toUnicode(data_dir)) Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log'))) - Env.set('db_path', toUnicode('sqlite:///' + db_path)) + Env.set('db', db) Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache'))) Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python')))) Env.set('console_log', options.console_log) @@ -183,34 +157,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En log.warning('%s %s %s line:%s', (category, message, filename, lineno)) warnings.showwarning = customwarn - # Check if database exists - db = Env.get('db_path') - db_exists = os.path.isfile(toUnicode(db_path)) - - # Load migrations - if False and db_exists: - - from migrate.versioning.api import version_control, db_version, version, upgrade - repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') - - latest_db_version = version(repo) - try: - current_db_version = db_version(db, repo) - except: - version_control(db, repo, version = latest_db_version) - current_db_version = db_version(db, repo) - - if current_db_version < latest_db_version: - if development: - log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.') - else: - log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version)) - upgrade(db, repo) - - # Configure Database - from couchpotato.core.settings.model import setup - setup() - # Create app from couchpotato import WebHandler web_base = ('/' + Env.setting('url_base').lstrip('/') + '/') if Env.setting('url_base') else '/' @@ -279,6 +225,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Fill database with needed stuff if not db_exists: fireEvent('app.initialize', in_order = True) + fireEvent('database.setup') # Go go go! from tornado.ioloop import IOLoop @@ -287,7 +234,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Some logging and fire load event try: log.info('Starting server on port %(port)s', config) except: pass - fireEventAsync('app.load') + fireEventAsync('app.load2') if config['ssl_cert'] and config['ssl_key']: server = HTTPServer(application, no_keep_alive = True, ssl_options = { diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index 52a4491b..af257454 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -71,8 +71,6 @@ Status.setup({{ json_encode(fireEvent('status.all', single = True)) }}); - File.Type.setup({{ json_encode(fireEvent('file.types', single = True)) }}); - CategoryList.setup({{ json_encode(fireEvent('category.all', single = True)) }}); App.setup({ @@ -95,4 +93,4 @@ CouchPotato - \ No newline at end of file + diff --git a/libs/CodernityDB/__init__.py b/libs/CodernityDB/__init__.py new file mode 100644 index 00000000..8399a60f --- /dev/null +++ b/libs/CodernityDB/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +__version__ = '0.4.2' +__license__ = "Apache 2.0" diff --git a/libs/CodernityDB/database.py b/libs/CodernityDB/database.py new file mode 100644 index 00000000..064836f1 --- /dev/null +++ b/libs/CodernityDB/database.py @@ -0,0 +1,1214 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import io +from inspect import getsource + +# for custom indexes +from CodernityDB.storage import Storage, IU_Storage +from CodernityDB.hash_index import (IU_UniqueHashIndex, + IU_HashIndex, + HashIndex, + UniqueHashIndex) +# normal imports + +from CodernityDB.index import (ElemNotFound, + DocIdNotFound, + IndexException, + Index, + TryReindexException, + ReindexException, + IndexNotFoundException, + IndexConflict) + +from CodernityDB.misc import NONE + +from CodernityDB.env import cdb_environment + +from random import randrange + +import warnings + + +def header_for_indexes(index_name, index_class, db_custom="", ind_custom="", classes_code=""): + return """# %s +# %s + +# inserted automatically +import os +import marshal + +import struct +import shutil + +from hashlib import md5 + +# custom db code start +# db_custom +%s + +# custom index code start +# ind_custom +%s + +# source of classes in index.classes_code +# classes_code +%s + +# index code start + +""" % (index_name, index_class, db_custom, ind_custom, classes_code) + + +class DatabaseException(Exception): + pass + + +class PreconditionsException(DatabaseException): + pass + + +class RecordDeleted(DatabaseException): + pass + + +class RecordNotFound(DatabaseException): + pass + + +class RevConflict(DatabaseException): + pass + + +class DatabaseConflict(DatabaseException): + pass + + +class DatabasePathException(DatabaseException): + pass + + +class DatabaseIsNotOpened(PreconditionsException): + pass + + +class Database(object): + """ + A default single thread database object. + """ + + custom_header = "" # : use it for imports required by your database + + def __init__(self, path): + self.path = path + self.storage = None + self.indexes = [] + self.id_ind = None + self.indexes_names = {} + self.opened = False + + def create_new_rev(self, old_rev=None): + """ + Creates new revision number based on previous one. + Increments it + random bytes. On overflow starts from 0 again. + """ + if old_rev: + try: + rev_num = int(old_rev[:4], 16) + except: + raise RevConflict() + rev_num += 1 + if rev_num > 65025: + # starting the counter from 0 again + rev_num = 0 + rnd = randrange(65536) + return "%04x%04x" % (rev_num, rnd) + else: + # new rev + rnd = randrange(256 ** 2) + return '0001%04x' % rnd + + def __not_opened(self): + if not self.opened: + raise DatabaseIsNotOpened("Database is not opened") + + def set_indexes(self, indexes=[]): + """ + Set indexes using ``indexes`` param + + :param indexes: indexes to set in db + :type indexes: iterable of :py:class:`CodernityDB.index.Index` objects. + + """ + for ind in indexes: + self.add_index(ind, create=False) + + def _add_single_index(self, p, i, index): + """ + Adds single index to a database. + It will use :py:meth:`inspect.getsource` to get class source. + Then it will build real index file, save it in ``_indexes`` directory. + """ + code = getsource(index.__class__) + if not code.startswith('c'): # fix for indented index codes + import textwrap + code = textwrap.dedent(code) + index._order = i + cls_code = getattr(index, 'classes_code', []) + classes_code = "" + for curr in cls_code: + classes_code += getsource(curr) + '\n\n' + with io.FileIO(os.path.join(p, "%.2d%s" % (i, index.name) + '.py'), 'w') as f: + f.write(header_for_indexes(index.name, + index.__class__.__name__, + getattr(self, 'custom_header', ''), + getattr(index, 'custom_header', ''), + classes_code)) + f.write(code) + return True + + def _read_index_single(self, p, ind, ind_kwargs={}): + """ + It will read single index from index file (ie. generated in :py:meth:`._add_single_index`). + Then it will perform ``exec`` on that code + + If error will occur the index file will be saved with ``_broken`` suffix + + :param p: path + :param ind: index name (will be joined with *p*) + :returns: new index object + """ + with io.FileIO(os.path.join(p, ind), 'r') as f: + name = f.readline()[2:].strip() + _class = f.readline()[2:].strip() + code = f.read() + try: + obj = compile(code, '', f.__name__, repr(args[1:]) + res = f(*args, **kwargs) +# if db.opened: +# db.flush() +# print '<=', f.__name__, repr(args[1:]) + return res + return _inner + + def __new__(cls, classname, bases, attr): + new_attr = {} + for base in bases: + for b_attr in dir(base): + a = getattr(base, b_attr, None) + if isinstance(a, MethodType) and not b_attr.startswith('_'): + if b_attr == 'flush' or b_attr == 'flush_indexes': + pass + else: + # setattr(base, b_attr, SuperLock.wrapper(a)) + new_attr[b_attr] = SuperLock.wrapper(a) + for attr_name, attr_value in attr.iteritems(): + if isinstance(attr_value, FunctionType) and not attr_name.startswith('_'): + attr_value = SuperLock.wrapper(attr_value) + new_attr[attr_name] = attr_value + new_attr['super_lock'] = RLock() + return type.__new__(cls, classname, bases, new_attr) + + +class SuperThreadSafeDatabase(Database): + """ + Thread safe version that always allows single thread to use db. + It adds the same lock for all methods, so only one operation can be + performed in given time. Completely different implementation + than ThreadSafe version (without super word) + """ + + __metaclass__ = SuperLock + + def __init__(self, *args, **kwargs): + super(SuperThreadSafeDatabase, self).__init__(*args, **kwargs) + + def __patch_index_gens(self, name): + ind = self.indexes_names[name] + for c in ('all', 'get_many'): + m = getattr(ind, c) + if getattr(ind, c + "_orig", None): + return + m_fixed = th_safe_gen.wrapper(m, name, c, self.super_lock) + setattr(ind, c, m_fixed) + setattr(ind, c + '_orig', m) + + def open(self, *args, **kwargs): + res = super(SuperThreadSafeDatabase, self).open(*args, **kwargs) + for name in self.indexes_names.iterkeys(): + self.__patch_index_gens(name) + return res + + def create(self, *args, **kwargs): + res = super(SuperThreadSafeDatabase, self).create(*args, **kwargs) + for name in self.indexes_names.iterkeys(): + self.__patch_index_gens(name) + return res + + def add_index(self, *args, **kwargs): + res = super(SuperThreadSafeDatabase, self).add_index(*args, **kwargs) + self.__patch_index_gens(res) + return res + + def edit_index(self, *args, **kwargs): + res = super(SuperThreadSafeDatabase, self).edit_index(*args, **kwargs) + self.__patch_index_gens(res) + return res diff --git a/libs/CodernityDB/database_thread_safe.py b/libs/CodernityDB/database_thread_safe.py new file mode 100644 index 00000000..5349e09b --- /dev/null +++ b/libs/CodernityDB/database_thread_safe.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import RLock + +from CodernityDB.env import cdb_environment + +cdb_environment['mode'] = "threads" +cdb_environment['rlock_obj'] = RLock + + +from database_safe_shared import SafeDatabase + + +class ThreadSafeDatabase(SafeDatabase): + """ + Thread safe version of CodernityDB that uses several lock objects, + on different methods / different indexes etc. It's completely different + implementation of locking than SuperThreadSafe one. + """ + pass diff --git a/libs/CodernityDB/debug_stuff.py b/libs/CodernityDB/debug_stuff.py new file mode 100644 index 00000000..76cdedf9 --- /dev/null +++ b/libs/CodernityDB/debug_stuff.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from CodernityDB.tree_index import TreeBasedIndex +import struct +import os + +import inspect +from functools import wraps +import json + + +class DebugTreeBasedIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + super(DebugTreeBasedIndex, self).__init__(*args, **kwargs) + + def print_tree(self): + print '-----CURRENT TREE-----' + print self.root_flag + + if self.root_flag == 'l': + print '---ROOT---' + self._print_leaf_data(self.data_start) + return + else: + print '---ROOT---' + self._print_node_data(self.data_start) + nr_of_el, children_flag = self._read_node_nr_of_elements_and_children_flag( + self.data_start) + nodes = [] + for index in range(nr_of_el): + l_pointer, key, r_pointer = self._read_single_node_key( + self.data_start, index) + nodes.append(l_pointer) + nodes.append(r_pointer) + print 'ROOT NODES', nodes + while children_flag == 'n': + self._print_level(nodes, 'n') + new_nodes = [] + for node in nodes: + nr_of_el, children_flag = \ + self._read_node_nr_of_elements_and_children_flag(node) + for index in range(nr_of_el): + l_pointer, key, r_pointer = self._read_single_node_key( + node, index) + new_nodes.append(l_pointer) + new_nodes.append(r_pointer) + nodes = new_nodes + self._print_level(nodes, 'l') + + def _print_level(self, nodes, flag): + print '---NEXT LVL---' + if flag == 'n': + for node in nodes: + self._print_node_data(node) + elif flag == 'l': + for node in nodes: + self._print_leaf_data(node) + + def _print_leaf_data(self, leaf_start_position): + print 'printing data of leaf at', leaf_start_position + nr_of_elements = self._read_leaf_nr_of_elements(leaf_start_position) + self.buckets.seek(leaf_start_position) + data = self.buckets.read(self.leaf_heading_size + + nr_of_elements * self.single_leaf_record_size) + leaf = struct.unpack('<' + self.leaf_heading_format + + nr_of_elements * self.single_leaf_record_format, data) + print leaf + print + + def _print_node_data(self, node_start_position): + print 'printing data of node at', node_start_position + nr_of_elements = self._read_node_nr_of_elements_and_children_flag( + node_start_position)[0] + self.buckets.seek(node_start_position) + data = self.buckets.read(self.node_heading_size + self.pointer_size + + nr_of_elements * (self.key_size + self.pointer_size)) + node = struct.unpack('<' + self.node_heading_format + self.pointer_format + + nr_of_elements * ( + self.key_format + self.pointer_format), + data) + print node + print +# ------------------> + + +def database_step_by_step(db_obj, path=None): + + if not path: + # ugly for multiplatform support.... + p = db_obj.path + p1 = os.path.split(p) + p2 = os.path.split(p1[0]) + p3 = '_'.join([p2[1], 'operation_logger.log']) + path = os.path.join(os.path.split(p2[0])[0], p3) + f_obj = open(path, 'wb') + + __stack = [] # inspect.stack() is not working on pytest etc + + def remove_from_stack(name): + for i in range(len(__stack)): + if __stack[-i] == name: + __stack.pop(-i) + + def __dumper(f): + @wraps(f) + def __inner(*args, **kwargs): + funct_name = f.__name__ + if funct_name == 'count': + name = args[0].__name__ + meth_args = (name,) + args[1:] + elif funct_name in ('reindex_index', 'compact_index'): + name = args[0].name + meth_args = (name,) + args[1:] + else: + meth_args = args + kwargs_copy = kwargs.copy() + res = None + __stack.append(funct_name) + if funct_name == 'insert': + try: + res = f(*args, **kwargs) + except: + packed = json.dumps((funct_name, + meth_args, kwargs_copy, None)) + f_obj.write('%s\n' % packed) + f_obj.flush() + raise + else: + packed = json.dumps((funct_name, + meth_args, kwargs_copy, res)) + f_obj.write('%s\n' % packed) + f_obj.flush() + else: + if funct_name == 'get': + for curr in __stack: + if ('delete' in curr or 'update' in curr) and not curr.startswith('test'): + remove_from_stack(funct_name) + return f(*args, **kwargs) + packed = json.dumps((funct_name, meth_args, kwargs_copy)) + f_obj.write('%s\n' % packed) + f_obj.flush() + res = f(*args, **kwargs) + remove_from_stack(funct_name) + return res + return __inner + + for meth_name, meth_f in inspect.getmembers(db_obj, predicate=inspect.ismethod): + if not meth_name.startswith('_'): + setattr(db_obj, meth_name, __dumper(meth_f)) + + setattr(db_obj, 'operation_logger', f_obj) + + +def database_from_steps(db_obj, path): + # db_obj.insert=lambda data : insert_for_debug(db_obj, data) + with open(path, 'rb') as f_obj: + for current in f_obj: + line = json.loads(current[:-1]) + if line[0] == 'count': + obj = getattr(db_obj, line[1][0]) + line[1] = [obj] + line[1][1:] + name = line[0] + if name == 'insert': + try: + line[1][0].pop('_rev') + except: + pass + elif name in ('delete', 'update'): + el = db_obj.get('id', line[1][0]['_id']) + line[1][0]['_rev'] = el['_rev'] +# print 'FROM STEPS doing', line + meth = getattr(db_obj, line[0], None) + if not meth: + raise Exception("Method = `%s` not found" % line[0]) + + meth(*line[1], **line[2]) + + +# def insert_for_debug(self, data): +# +# _rev = data['_rev'] +# +# if not '_id' in data: +# _id = uuid4().hex +# else: +# _id = data['_id'] +# data['_id'] = _id +# try: +# _id = bytes(_id) +# except: +# raise DatabaseException("`_id` must be valid bytes object") +# self._insert_indexes(_id, _rev, data) +# ret = {'_id': _id, '_rev': _rev} +# data.update(ret) +# return ret diff --git a/libs/CodernityDB/env.py b/libs/CodernityDB/env.py new file mode 100644 index 00000000..69ca8cdd --- /dev/null +++ b/libs/CodernityDB/env.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +It's CodernityDB environment. +Handles internal informations.' +""" + +cdb_environment = { + 'mode': 'normal' +} diff --git a/libs/CodernityDB/hash_index.py b/libs/CodernityDB/hash_index.py new file mode 100644 index 00000000..cd160fd0 --- /dev/null +++ b/libs/CodernityDB/hash_index.py @@ -0,0 +1,880 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from CodernityDB.index import (Index, + IndexException, + DocIdNotFound, + ElemNotFound, + TryReindexException, + IndexPreconditionsException) + +import os +import marshal +import io +import struct +import shutil + +from CodernityDB.storage import IU_Storage, DummyStorage + +from CodernityDB.env import cdb_environment + +if cdb_environment.get('rlock_obj'): + from CodernityDB import patch + patch.patch_cache_rr(cdb_environment['rlock_obj']) + +from CodernityDB.rr_cache import cache1lvl + + +from CodernityDB.misc import random_hex_32 + +try: + from CodernityDB import __version__ +except ImportError: + from __init__ import __version__ + + +class IU_HashIndex(Index): + """ + That class is for Internal Use only, if you want to use HashIndex just subclass the :py:class:`HashIndex` instead this one. + + That design is because main index logic should be always in database not in custom user indexes. + """ + + def __init__(self, db_path, name, entry_line_format='<32s{key}IIcI', hash_lim=0xfffff, storage_class=None, key_format='c'): + """ + The index is capable to solve conflicts by `Separate chaining` + :param db_path: database path + :type db_path: string + :param name: index name + :type name: ascii string + :param line_format: line format, `key_format` parameter value will replace `{key}` if present. + :type line_format: string (32s{key}IIcI by default) {doc_id}{hash_key}{start}{size}{status}{next} + :param hash_lim: maximum hash functon results (remember about birthday problem) count from 0 + :type hash_lim: integer + :param storage_class: Storage class by default it will open standard :py:class:`CodernityDB.storage.Storage` (if string has to be accesible by globals()[storage_class]) + :type storage_class: class name which will be instance of CodernityDB.storage.Storage instance or None + :param key_format: a index key format + """ + if key_format and '{key}' in entry_line_format: + entry_line_format = entry_line_format.replace('{key}', key_format) + super(IU_HashIndex, self).__init__(db_path, name) + self.hash_lim = hash_lim + if not storage_class: + storage_class = IU_Storage + if storage_class and not isinstance(storage_class, basestring): + storage_class = storage_class.__name__ + self.storage_class = storage_class + self.storage = None + + self.bucket_line_format = "= self.data_start: + self.buckets.seek(pos_prev) + data = self.buckets.read(self.entry_line_size) + if data: + doc_id, l_key, start, size, status, _next = self.entry_struct.unpack(data) + self.buckets.seek(pos_prev) + self.buckets.write(self.entry_struct.pack(doc_id, + l_key, + start, + size, + status, + pos_next)) + self.flush() + if pos_next: + self.buckets.seek(pos_next) + data = self.buckets.read(self.entry_line_size) + if data: + doc_id, l_key, start, size, status, _next = self.entry_struct.unpack(data) + self.buckets.seek(pos_next) + self.buckets.write(self.entry_struct.pack(doc_id, + l_key, + start, + size, + status, + _next)) + self.flush() + return + + def delete(self, doc_id, key, start=0, size=0): + start_position = self._calculate_position(key) + self.buckets.seek(start_position) + curr_data = self.buckets.read(self.bucket_line_size) + if curr_data: + location = self.bucket_struct.unpack(curr_data)[0] + else: + # case happens when trying to delete element with new index key in data + # after adding new index to database without reindex + raise TryReindexException() + found_at, _doc_id, _key, start, size, status, _next = self._locate_doc_id(doc_id, key, location) + self.buckets.seek(found_at) + self.buckets.write(self.entry_struct.pack(doc_id, + key, + start, + size, + 'd', + _next)) + self.flush() + # self._fix_link(_key, _prev, _next) + self._find_key.delete(key) + self._locate_doc_id.delete(doc_id) + return True + + def compact(self, hash_lim=None): + + if not hash_lim: + hash_lim = self.hash_lim + + compact_ind = self.__class__( + self.db_path, self.name + '_compact', hash_lim=hash_lim) + compact_ind.create_index() + + gen = self.all() + while True: + try: + doc_id, key, start, size, status = gen.next() + except StopIteration: + break + self.storage._f.seek(start) + value = self.storage._f.read(size) + start_ = compact_ind.storage._f.tell() + compact_ind.storage._f.write(value) + compact_ind.insert(doc_id, key, start_, size, status) + + compact_ind.close_index() + original_name = self.name + # os.unlink(os.path.join(self.db_path, self.name + "_buck")) + self.close_index() + shutil.move(os.path.join(compact_ind.db_path, compact_ind. + name + "_buck"), os.path.join(self.db_path, self.name + "_buck")) + shutil.move(os.path.join(compact_ind.db_path, compact_ind. + name + "_stor"), os.path.join(self.db_path, self.name + "_stor")) + # self.name = original_name + self.open_index() # reload... + self.name = original_name + self._save_params(dict(name=original_name)) + self._fix_params() + self._clear_cache() + return True + + def make_key(self, key): + return key + + def make_key_value(self, data): + return '1', data + + def _clear_cache(self): + self._find_key.clear() + self._locate_doc_id.clear() + + def close_index(self): + super(IU_HashIndex, self).close_index() + self._clear_cache() + + +class IU_UniqueHashIndex(IU_HashIndex): + """ + Index for *unique* keys! Designed to be a **id** index. + + That class is for Internal Use only, if you want to use UniqueHashIndex just subclass the :py:class:`UniqueHashIndex` instead this one. + + That design is because main index logic should be always in database not in custom user indexes. + """ + + def __init__(self, db_path, name, entry_line_format="<32s8sIIcI", *args, **kwargs): + if 'key' in kwargs: + raise IndexPreconditionsException( + "UniqueHashIndex doesn't accept key parameter'") + super(IU_UniqueHashIndex, self).__init__(db_path, name, + entry_line_format, *args, **kwargs) + self.create_key = random_hex_32 # : set the function to create random key when no _id given + # self.entry_struct=struct.Struct(entry_line_format) + +# @lfu_cache(100) + def _find_key(self, key): + """ + Find the key position + + :param key: the key to find + """ + start_position = self._calculate_position(key) + self.buckets.seek(start_position) + curr_data = self.buckets.read(self.bucket_line_size) + if curr_data: + location = self.bucket_struct.unpack(curr_data)[0] + found_at, l_key, rev, start, size, status, _next = self._locate_key( + key, location) + return l_key, rev, start, size, status + else: + return None, None, 0, 0, 'u' + + def _find_key_many(self, *args, **kwargs): + raise NotImplementedError() + + def _find_place(self, start, key): + """ + Find a place to where put the key. It will iterate using `next` field in record, until + empty `next` found + + :param start: position to start from + """ + location = start + while True: + self.buckets.seek(location) + data = self.buckets.read(self.entry_line_size) + # todo, maybe partial read there... + l_key, rev, start, size, status, _next = self.entry_struct.unpack( + data) + if l_key == key: + raise IndexException("The '%s' key already exists" % key) + if not _next or status == 'd': + return self.buckets.tell() - self.entry_line_size, l_key, rev, start, size, status, _next + else: + location = _next # go to next record + + # @lfu_cache(100) + def _locate_key(self, key, start): + """ + Locate position of the key, it will iterate using `next` field in record + until required key will be find. + + :param key: the key to locate + :param start: position to start from + """ + location = start + while True: + self.buckets.seek(location) + data = self.buckets.read(self.entry_line_size) + # todo, maybe partial read there... + try: + l_key, rev, start, size, status, _next = self.entry_struct.unpack(data) + except struct.error: + raise ElemNotFound("Location '%s' not found" % key) + if l_key == key: + break + else: + if not _next: + # not found + raise ElemNotFound("Location '%s' not found" % key) + else: + location = _next # go to next record + return self.buckets.tell() - self.entry_line_size, l_key, rev, start, size, status, _next + + def update(self, key, rev, u_start=0, u_size=0, u_status='o'): + start_position = self._calculate_position(key) + self.buckets.seek(start_position) + curr_data = self.buckets.read(self.bucket_line_size) + # test if it's unique or not really unique hash + + if curr_data: + location = self.bucket_struct.unpack(curr_data)[0] + else: + raise ElemNotFound("Location '%s' not found" % key) + found_at, _key, _rev, start, size, status, _next = self._locate_key( + key, location) + if u_start == 0: + u_start = start + if u_size == 0: + u_size = size + self.buckets.seek(found_at) + self.buckets.write(self.entry_struct.pack(key, + rev, + u_start, + u_size, + u_status, + _next)) + self.flush() + self._find_key.delete(key) + return True + + def insert(self, key, rev, start, size, status='o'): + start_position = self._calculate_position(key) + self.buckets.seek(start_position) + curr_data = self.buckets.read(self.bucket_line_size) + + # conflict occurs? + if curr_data: + location = self.bucket_struct.unpack(curr_data)[0] + else: + location = 0 + if location: + # last key with that hash + found_at, _key, _rev, _start, _size, _status, _next = self._find_place( + location, key) + self.buckets.seek(0, 2) + wrote_at = self.buckets.tell() + + # check if position is bigger than all hash entries... + if wrote_at < self.data_start: + self.buckets.seek(self.data_start) + wrote_at = self.buckets.tell() + + self.buckets.write(self.entry_struct.pack(key, + rev, + start, + size, + status, + _next)) + +# self.flush() + self.buckets.seek(found_at) + self.buckets.write(self.entry_struct.pack(_key, + _rev, + _start, + _size, + _status, + wrote_at)) + self.flush() + self._find_key.delete(_key) + # self._locate_key.delete(_key) + return True + # raise NotImplementedError + else: + self.buckets.seek(0, 2) + wrote_at = self.buckets.tell() + + # check if position is bigger than all hash entries... + if wrote_at < self.data_start: + self.buckets.seek(self.data_start) + wrote_at = self.buckets.tell() + + self.buckets.write(self.entry_struct.pack(key, + rev, + start, + size, + status, + 0)) +# self.flush() + self.buckets.seek(start_position) + self.buckets.write(self.bucket_struct.pack(wrote_at)) + self.flush() + self._find_key.delete(key) + return True + + def all(self, limit=-1, offset=0): + self.buckets.seek(self.data_start) + while offset: + curr_data = self.buckets.read(self.entry_line_size) + if not curr_data: + break + try: + doc_id, rev, start, size, status, next = self.entry_struct.unpack(curr_data) + except IndexException: + break + else: + if status != 'd': + offset -= 1 + + while limit: + curr_data = self.buckets.read(self.entry_line_size) + if not curr_data: + break + try: + doc_id, rev, start, size, status, next = self.entry_struct.unpack(curr_data) + except IndexException: + break + else: + if status != 'd': + yield doc_id, rev, start, size, status + limit -= 1 + + def get_many(self, *args, **kwargs): + raise NotImplementedError() + + def delete(self, key, start=0, size=0): + self.update(key, '00000000', start, size, 'd') + + def make_key_value(self, data): + _id = data['_id'] + try: + _id = bytes(data['_id']) + except: + raise IndexPreconditionsException( + "_id must be valid string/bytes object") + if len(_id) != 32: + raise IndexPreconditionsException("Invalid _id lenght") + del data['_id'] + del data['_rev'] + return _id, data + + def destroy(self): + Index.destroy(self) + self._clear_cache() + + def _clear_cache(self): + self._find_key.clear() + + def insert_with_storage(self, _id, _rev, value): + if value: + start, size = self.storage.insert(value) + else: + start = 1 + size = 0 + return self.insert(_id, _rev, start, size) + + def update_with_storage(self, _id, _rev, value): + if value: + start, size = self.storage.insert(value) + else: + start = 1 + size = 0 + return self.update(_id, _rev, start, size) + + +class DummyHashIndex(IU_HashIndex): + def __init__(self, db_path, name, entry_line_format="<32s4sIIcI", *args, **kwargs): + super(DummyHashIndex, self).__init__(db_path, name, + entry_line_format, *args, **kwargs) + self.create_key = random_hex_32 # : set the function to create random key when no _id given + # self.entry_struct=struct.Struct(entry_line_format) + + def update(self, *args, **kwargs): + return True + + def insert(self, *args, **kwargs): + return True + + def all(self, *args, **kwargs): + raise StopIteration + + def get(self, *args, **kwargs): + raise ElemNotFound + + def get_many(self, *args, **kwargs): + raise StopIteration + + def delete(self, *args, **kwargs): + pass + + def make_key_value(self, data): + return '1', {'_': 1} + + def destroy(self): + pass + + def _clear_cache(self): + pass + + def _open_storage(self): + if not self.storage: + self.storage = DummyStorage() + self.storage.open() + + def _create_storage(self): + if not self.storage: + self.storage = DummyStorage() + self.storage.create() + + +class IU_MultiHashIndex(IU_HashIndex): + """ + Class that allows to index more than one key per database record. + + It operates very well on GET/INSERT. It's not optimized for + UPDATE operations (will always readd everything) + """ + + def __init__(self, *args, **kwargs): + super(IU_MultiHashIndex, self).__init__(*args, **kwargs) + + def insert(self, doc_id, key, start, size, status='o'): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + ins = super(IU_MultiHashIndex, self).insert + for curr_key in key: + ins(doc_id, curr_key, start, size, status) + return True + + def update(self, doc_id, key, u_start, u_size, u_status='o'): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + upd = super(IU_MultiHashIndex, self).update + for curr_key in key: + upd(doc_id, curr_key, u_start, u_size, u_status) + + def delete(self, doc_id, key, start=0, size=0): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + delete = super(IU_MultiHashIndex, self).delete + for curr_key in key: + delete(doc_id, curr_key, start, size) + + def get(self, key): + return super(IU_MultiHashIndex, self).get(key) + + def make_key_value(self, data): + raise NotImplementedError() + + +# classes for public use, done in this way because of +# generation static files with indexes (_index directory) + + +class HashIndex(IU_HashIndex): + """ + That class is designed to be used in custom indexes. + """ + pass + + +class UniqueHashIndex(IU_UniqueHashIndex): + """ + That class is designed to be used in custom indexes. It's designed to be **id** index. + """ + pass + + +class MultiHashIndex(IU_MultiHashIndex): + """ + That class is designed to be used in custom indexes. + """ diff --git a/libs/CodernityDB/index.py b/libs/CodernityDB/index.py new file mode 100644 index 00000000..48db2a4a --- /dev/null +++ b/libs/CodernityDB/index.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import marshal + +import struct +import shutil + +from CodernityDB.storage import IU_Storage, DummyStorage + +try: + from CodernityDB import __version__ +except ImportError: + from __init__ import __version__ + + +import io + + +class IndexException(Exception): + pass + + +class IndexNotFoundException(IndexException): + pass + + +class ReindexException(IndexException): + pass + + +class TryReindexException(ReindexException): + pass + + +class ElemNotFound(IndexException): + pass + + +class DocIdNotFound(ElemNotFound): + pass + + +class IndexConflict(IndexException): + pass + + +class IndexPreconditionsException(IndexException): + pass + + +class Index(object): + + __version__ = __version__ + + custom_header = "" # : use it for imports required by your index + + def __init__(self, + db_path, + name): + self.name = name + self._start_ind = 500 + self.db_path = db_path + + def open_index(self): + if not os.path.isfile(os.path.join(self.db_path, self.name + '_buck')): + raise IndexException("Doesn't exists") + self.buckets = io.open( + os.path.join(self.db_path, self.name + "_buck"), 'r+b', buffering=0) + self._fix_params() + self._open_storage() + + def _close(self): + self.buckets.close() + self.storage.close() + + def close_index(self): + self.flush() + self.fsync() + self._close() + + def create_index(self): + raise NotImplementedError() + + def _fix_params(self): + self.buckets.seek(0) + props = marshal.loads(self.buckets.read(self._start_ind)) + for k, v in props.iteritems(): + self.__dict__[k] = v + self.buckets.seek(0, 2) + + def _save_params(self, in_params={}): + self.buckets.seek(0) + props = marshal.loads(self.buckets.read(self._start_ind)) + props.update(in_params) + self.buckets.seek(0) + data = marshal.dumps(props) + if len(data) > self._start_ind: + raise IndexException("To big props") + self.buckets.write(data) + self.flush() + self.buckets.seek(0, 2) + self.__dict__.update(props) + + def _open_storage(self, *args, **kwargs): + pass + + def _create_storage(self, *args, **kwargs): + pass + + def _destroy_storage(self, *args, **kwargs): + self.storage.destroy() + + def _find_key(self, key): + raise NotImplementedError() + + def update(self, doc_id, key, start, size): + raise NotImplementedError() + + def insert(self, doc_id, key, start, size): + raise NotImplementedError() + + def get(self, key): + raise NotImplementedError() + + def get_many(self, key, start_from=None, limit=0): + raise NotImplementedError() + + def all(self, start_pos): + raise NotImplementedError() + + def delete(self, key, start, size): + raise NotImplementedError() + + def make_key_value(self, data): + raise NotImplementedError() + + def make_key(self, data): + raise NotImplementedError() + + def compact(self, *args, **kwargs): + raise NotImplementedError() + + def destroy(self, *args, **kwargs): + self._close() + bucket_file = os.path.join(self.db_path, self.name + '_buck') + os.unlink(bucket_file) + self._destroy_storage() + self._find_key.clear() + + def flush(self): + try: + self.buckets.flush() + self.storage.flush() + except: + pass + + def fsync(self): + try: + os.fsync(self.buckets.fileno()) + self.storage.fsync() + except: + pass + + def update_with_storage(self, doc_id, key, value): + if value: + start, size = self.storage.insert(value) + else: + start = 1 + size = 0 + return self.update(doc_id, key, start, size) + + def insert_with_storage(self, doc_id, key, value): + if value: + start, size = self.storage.insert(value) + else: + start = 1 + size = 0 + return self.insert(doc_id, key, start, size) diff --git a/libs/CodernityDB/indexcreator.py b/libs/CodernityDB/indexcreator.py new file mode 100644 index 00000000..1e09a22b --- /dev/null +++ b/libs/CodernityDB/indexcreator.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import re +import tokenize +import token +import uuid + + +class IndexCreatorException(Exception): + def __init__(self, ex, line=None): + self.ex = ex + self.line = line + + def __str__(self): + if self.line: + return repr(self.ex + "(in line: %d)" % self.line) + return repr(self.ex) + + +class IndexCreatorFunctionException(IndexCreatorException): + pass + + +class IndexCreatorValueException(IndexCreatorException): + pass + + +class Parser(object): + def __init__(self): + pass + + def parse(self, data, name=None): + if not name: + self.name = "_" + uuid.uuid4().hex + else: + self.name = name + + self.ind = 0 + self.stage = 0 + self.logic = ['and', 'or', 'in'] + self.logic2 = ['&', '|'] + self.allowed_props = {'TreeBasedIndex': ['type', 'name', 'key_format', 'node_capacity', 'pointer_format', 'meta_format'], + 'HashIndex': ['type', 'name', 'key_format', 'hash_lim', 'entry_line_format'], + 'MultiHashIndex': ['type', 'name', 'key_format', 'hash_lim', 'entry_line_format'], + 'MultiTreeBasedIndex': ['type', 'name', 'key_format', 'node_capacity', 'pointer_format', 'meta_format'] + } + self.funcs = {'md5': (['md5'], ['.digest()']), + 'len': (['len'], []), + 'str': (['str'], []), + 'fix_r': (['self.fix_r'], []), + 'prefix': (['self.prefix'], []), + 'infix': (['self.infix'], []), + 'suffix': (['self.suffix'], []) + } + self.handle_int_imports = {'infix': "from itertools import izip\n"} + + self.funcs_with_body = {'fix_r': + (""" def fix_r(self,s,l): + e = len(s) + if e == l: + return s + elif e > l: + return s[:l] + else: + return s.rjust(l,'_')\n""", False), + 'prefix': + (""" def prefix(self,s,m,l,f): + t = len(s) + if m < 1: + m = 1 + o = set() + if t > l: + s = s[:l] + t = l + while m <= t: + o.add(s.rjust(f,'_')) + s = s[:-1] + t -= 1 + return o\n""", False), + 'suffix': + (""" def suffix(self,s,m,l,f): + t = len(s) + if m < 1: + m = 1 + o = set() + if t > l: + s = s[t-l:] + t = len(s) + while m <= t: + o.add(s.rjust(f,'_')) + s = s[1:] + t -= 1 + return o\n""", False), + 'infix': + (""" def infix(self,s,m,l,f): + t = len(s) + o = set() + for x in xrange(m - 1, l): + t = (s, ) + for y in xrange(0, x): + t += (s[y + 1:],) + o.update(set(''.join(x).rjust(f, '_').lower() for x in izip(*t))) + return o\n""", False)} + self.none = ['None', 'none', 'null'] + self.props_assign = ['=', ':'] + self.all_adj_num_comp = {token.NUMBER: ( + token.NUMBER, token.NAME, '-', '('), + token.NAME: (token.NUMBER, token.NAME, '-', '('), + ')': (token.NUMBER, token.NAME, '-', '(') + } + + self.all_adj_num_op = {token.NUMBER: (token.NUMBER, token.NAME, '('), + token.NAME: (token.NUMBER, token.NAME, '('), + ')': (token.NUMBER, token.NAME, '(') + } + self.allowed_adjacent = { + "<=": self.all_adj_num_comp, + ">=": self.all_adj_num_comp, + ">": self.all_adj_num_comp, + "<": self.all_adj_num_comp, + + "==": {token.NUMBER: (token.NUMBER, token.NAME, '('), + token.NAME: (token.NUMBER, token.NAME, token.STRING, '('), + token.STRING: (token.NAME, token.STRING, '('), + ')': (token.NUMBER, token.NAME, token.STRING, '('), + ']': (token.NUMBER, token.NAME, token.STRING, '(') + }, + + "+": {token.NUMBER: (token.NUMBER, token.NAME, '('), + token.NAME: (token.NUMBER, token.NAME, token.STRING, '('), + token.STRING: (token.NAME, token.STRING, '('), + ')': (token.NUMBER, token.NAME, token.STRING, '('), + ']': (token.NUMBER, token.NAME, token.STRING, '(') + }, + + "-": {token.NUMBER: (token.NUMBER, token.NAME, '('), + token.NAME: (token.NUMBER, token.NAME, '('), + ')': (token.NUMBER, token.NAME, '('), + '<': (token.NUMBER, token.NAME, '('), + '>': (token.NUMBER, token.NAME, '('), + '<=': (token.NUMBER, token.NAME, '('), + '>=': (token.NUMBER, token.NAME, '('), + '==': (token.NUMBER, token.NAME, '('), + ']': (token.NUMBER, token.NAME, '(') + }, + "*": self.all_adj_num_op, + "/": self.all_adj_num_op, + "%": self.all_adj_num_op, + ",": {token.NUMBER: (token.NUMBER, token.NAME, token.STRING, '{', '[', '('), + token.NAME: (token.NUMBER, token.NAME, token.STRING, '(', '{', '['), + token.STRING: (token.NAME, token.STRING, token.NUMBER, '(', '{', '['), + ')': (token.NUMBER, token.NAME, token.STRING, '(', '{', '['), + ']': (token.NUMBER, token.NAME, token.STRING, '(', '{', '['), + '}': (token.NUMBER, token.NAME, token.STRING, '(', '{', '[') + } + } + + def is_num(s): + m = re.search('[^0-9*()+\-\s/]+', s) + return not m + + def is_string(s): + m = re.search('\s*(?P[\'\"]+).*?(?P=a)\s*', s) + return m + data = re.split('make_key_value\:', data) + + if len(data) < 2: + raise IndexCreatorFunctionException( + "Couldn't find a definition of make_key_value function!\n") + + spl1 = re.split('make_key\:', data[0]) + spl2 = re.split('make_key\:', data[1]) + + self.funcs_rev = False + + if len(spl1) > 1: + data = [spl1[0]] + [data[1]] + [spl1[1]] + self.funcs_rev = True + elif len(spl2) > 1: + data = [data[0]] + spl2 + else: + data.append("key") + + if data[1] == re.search('\s*', data[1], re.S | re.M).group(0): + raise IndexCreatorFunctionException("Empty function body ", + len(re.split('\n', data[0])) + (len(re.split('\n', data[2])) if self.funcs_rev else 1) - 1) + if data[2] == re.search('\s*', data[2], re.S | re.M).group(0): + raise IndexCreatorFunctionException("Empty function body ", + len(re.split('\n', data[0])) + (1 if self.funcs_rev else len(re.split('\n', data[1]))) - 1) + if data[0] == re.search('\s*', data[0], re.S | re.M).group(0): + raise IndexCreatorValueException("You didn't set any properity or you set them not at the begining of the code\n") + + data = [re.split( + '\n', data[0]), re.split('\n', data[1]), re.split('\n', data[2])] + self.cnt_lines = (len(data[0]), len(data[1]), len(data[2])) + ind = 0 + self.predata = data + self.data = [[], [], []] + for i, v in enumerate(self.predata[0]): + for k, w in enumerate(self.predata[0][i]): + if self.predata[0][i][k] in self.props_assign: + if not is_num(self.predata[0][i][k + 1:]) and self.predata[0][i].strip()[:4] != 'type' and self.predata[0][i].strip()[:4] != 'name': + s = self.predata[0][i][k + 1:] + self.predata[0][i] = self.predata[0][i][:k + 1] + + m = re.search('\s+', s.strip()) + if not is_string(s) and not m: + s = "'" + s.strip() + "'" + self.predata[0][i] += s + break + + for n, i in enumerate(self.predata): + for k in i: + k = k.strip() + if k: + self.data[ind].append(k) + self.check_enclosures(k, n) + ind += 1 + + return self.parse_ex() + + def readline(self, stage): + def foo(): + if len(self.data[stage]) <= self.ind: + self.ind = 0 + return "" + else: + self.ind += 1 + return self.data[stage][self.ind - 1] + return foo + + def add(self, l, i): + def add_aux(*args): + # print args,self.ind + if len(l[i]) < self.ind: + l[i].append([]) + l[i][self.ind - 1].append(args) + return add_aux + + def parse_ex(self): + self.index_name = "" + self.index_type = "" + self.curLine = -1 + self.con = -1 + self.brackets = -1 + self.curFunc = None + self.colons = 0 + self.line_cons = ([], [], []) + self.pre_tokens = ([], [], []) + self.known_dicts_in_mkv = [] + self.prop_name = True + self.prop_assign = False + self.is_one_arg_enough = False + self.funcs_stack = [] + self.last_line = [-1, -1, -1] + self.props_set = [] + self.custom_header = set() + + self.tokens = [] + self.tokens_head = ['# %s\n' % self.name, 'class %s(' % self.name, '):\n', ' def __init__(self, *args, **kwargs): '] + + for i in xrange(3): + tokenize.tokenize(self.readline(i), self.add(self.pre_tokens, i)) + # tokenize treats some keyword not in the right way, thats why we + # have to change some of them + for nk, k in enumerate(self.pre_tokens[i]): + for na, a in enumerate(k): + if a[0] == token.NAME and a[1] in self.logic: + self.pre_tokens[i][nk][ + na] = (token.OP, a[1], a[2], a[3], a[4]) + + for i in self.pre_tokens[1]: + self.line_cons[1].append(self.check_colons(i, 1)) + self.check_adjacents(i, 1) + if self.check_for_2nd_arg(i) == -1 and not self.is_one_arg_enough: + raise IndexCreatorValueException("No 2nd value to return (did u forget about ',None'?", self.cnt_line_nr(i[0][4], 1)) + self.is_one_arg_enough = False + + for i in self.pre_tokens[2]: + self.line_cons[2].append(self.check_colons(i, 2)) + self.check_adjacents(i, 2) + + for i in self.pre_tokens[0]: + self.handle_prop_line(i) + + self.cur_brackets = 0 + self.tokens += ['\n super(%s, self).__init__(*args, **kwargs)\n def make_key_value(self, data): ' % self.name] + + for i in self.pre_tokens[1]: + for k in i: + self.handle_make_value(*k) + + self.curLine = -1 + self.con = -1 + self.cur_brackets = 0 + self.tokens += ['\n def make_key(self, key):'] + + for i in self.pre_tokens[2]: + for k in i: + self.handle_make_key(*k) + + if self.index_type == "": + raise IndexCreatorValueException("Missing index type definition\n") + if self.index_name == "": + raise IndexCreatorValueException("Missing index name\n") + + self.tokens_head[0] = "# " + self.index_name + "\n" + \ + self.tokens_head[0] + + for i in self.funcs_with_body: + if self.funcs_with_body[i][1]: + self.tokens_head.insert(4, self.funcs_with_body[i][0]) + + if None in self.custom_header: + self.custom_header.remove(None) + if self.custom_header: + s = ' custom_header = """' + for i in self.custom_header: + s += i + s += '"""\n' + self.tokens_head.insert(4, s) + + if self.index_type in self.allowed_props: + for i in self.props_set: + if i not in self.allowed_props[self.index_type]: + raise IndexCreatorValueException("Properity %s is not allowed for index type: %s" % (i, self.index_type)) + + # print "".join(self.tokens_head) + # print "----------" + # print (" ".join(self.tokens)) + return "".join(self.custom_header), "".join(self.tokens_head) + (" ".join(self.tokens)) + + # has to be run BEFORE tokenize + def check_enclosures(self, d, st): + encs = [] + contr = {'(': ')', '{': '}', '[': ']', "'": "'", '"': '"'} + ends = [')', '}', ']', "'", '"'] + for i in d: + if len(encs) > 0 and encs[-1] in ['"', "'"]: + if encs[-1] == i: + del encs[-1] + elif i in contr: + encs += [i] + elif i in ends: + if len(encs) < 1 or contr[encs[-1]] != i: + raise IndexCreatorValueException("Missing opening enclosure for \'%s\'" % i, self.cnt_line_nr(d, st)) + del encs[-1] + + if len(encs) > 0: + raise IndexCreatorValueException("Missing closing enclosure for \'%s\'" % encs[0], self.cnt_line_nr(d, st)) + + def check_adjacents(self, d, st): + def std_check(d, n): + if n == 0: + prev = -1 + else: + prev = d[n - 1][1] if d[n - 1][0] == token.OP else d[n - 1][0] + + cur = d[n][1] if d[n][0] == token.OP else d[n][0] + + # there always is an endmarker at the end, but this is a precaution + if n + 2 > len(d): + nex = -1 + else: + nex = d[n + 1][1] if d[n + 1][0] == token.OP else d[n + 1][0] + + if prev not in self.allowed_adjacent[cur]: + raise IndexCreatorValueException("Wrong left value of the %s" % cur, self.cnt_line_nr(line, st)) + + # there is an assumption that whole data always ends with 0 marker, the idea prolly needs a rewritting to allow more whitespaces + # between tokens, so it will be handled anyway + elif nex not in self.allowed_adjacent[cur][prev]: + raise IndexCreatorValueException("Wrong right value of the %s" % cur, self.cnt_line_nr(line, st)) + + for n, (t, i, _, _, line) in enumerate(d): + if t == token.NAME or t == token.STRING: + if n + 1 < len(d) and d[n + 1][0] in [token.NAME, token.STRING]: + raise IndexCreatorValueException("Did you forget about an operator in between?", self.cnt_line_nr(line, st)) + elif i in self.allowed_adjacent: + std_check(d, n) + + def check_colons(self, d, st): + cnt = 0 + br = 0 + + def check_ret_args_nr(a, s): + c_b_cnt = 0 + s_b_cnt = 0 + n_b_cnt = 0 + comas_cnt = 0 + for _, i, _, _, line in a: + + if c_b_cnt == n_b_cnt == s_b_cnt == 0: + if i == ',': + comas_cnt += 1 + if (s == 1 and comas_cnt > 1) or (s == 2 and comas_cnt > 0): + raise IndexCreatorFunctionException("Too much arguments to return", self.cnt_line_nr(line, st)) + if s == 0 and comas_cnt > 0: + raise IndexCreatorValueException("A coma here doesn't make any sense", self.cnt_line_nr(line, st)) + + elif i == ':': + if s == 0: + raise IndexCreatorValueException("A colon here doesn't make any sense", self.cnt_line_nr(line, st)) + raise IndexCreatorFunctionException("Two colons don't make any sense", self.cnt_line_nr(line, st)) + + if i == '{': + c_b_cnt += 1 + elif i == '}': + c_b_cnt -= 1 + elif i == '(': + n_b_cnt += 1 + elif i == ')': + n_b_cnt -= 1 + elif i == '[': + s_b_cnt += 1 + elif i == ']': + s_b_cnt -= 1 + + def check_if_empty(a): + for i in a: + if i not in [token.NEWLINE, token.INDENT, token.ENDMARKER]: + return False + return True + if st == 0: + check_ret_args_nr(d, st) + return + + for n, i in enumerate(d): + if i[1] == ':': + if br == 0: + if len(d) < n or check_if_empty(d[n + 1:]): + raise IndexCreatorValueException( + "Empty return value", self.cnt_line_nr(i[4], st)) + elif len(d) >= n: + check_ret_args_nr(d[n + 1:], st) + return cnt + else: + cnt += 1 + elif i[1] == '{': + br += 1 + elif i[1] == '}': + br -= 1 + check_ret_args_nr(d, st) + return -1 + + def check_for_2nd_arg(self, d): + c_b_cnt = 0 # curly brackets counter '{}' + s_b_cnt = 0 # square brackets counter '[]' + n_b_cnt = 0 # normal brackets counter '()' + + def check_2nd_arg(d, ind): + d = d[ind[0]:] + for t, i, (n, r), _, line in d: + if i == '{' or i is None: + return 0 + elif t == token.NAME: + self.known_dicts_in_mkv.append((i, (n, r))) + return 0 + elif t == token.STRING or t == token.NUMBER: + raise IndexCreatorValueException("Second return value of make_key_value function has to be a dictionary!", self.cnt_line_nr(line, 1)) + + for ind in enumerate(d): + t, i, _, _, _ = ind[1] + if s_b_cnt == n_b_cnt == c_b_cnt == 0: + if i == ',': + return check_2nd_arg(d, ind) + elif (t == token.NAME and i not in self.funcs) or i == '{': + self.is_one_arg_enough = True + + if i == '{': + c_b_cnt += 1 + self.is_one_arg_enough = True + elif i == '}': + c_b_cnt -= 1 + elif i == '(': + n_b_cnt += 1 + elif i == ')': + n_b_cnt -= 1 + elif i == '[': + s_b_cnt += 1 + elif i == ']': + s_b_cnt -= 1 + return -1 + + def cnt_line_nr(self, l, stage): + nr = -1 + for n, i in enumerate(self.predata[stage]): + # print i,"|||",i.strip(),"|||",l + if l == i.strip(): + nr = n + if nr == -1: + return -1 + + if stage == 0: + return nr + 1 + elif stage == 1: + return nr + self.cnt_lines[0] + (self.cnt_lines[2] - 1 if self.funcs_rev else 0) + elif stage == 2: + return nr + self.cnt_lines[0] + (self.cnt_lines[1] - 1 if not self.funcs_rev else 0) + + return -1 + + def handle_prop_line(self, d): + d_len = len(d) + if d[d_len - 1][0] == token.ENDMARKER: + d_len -= 1 + + if d_len < 3: + raise IndexCreatorValueException("Can't handle properity assingment ", self.cnt_line_nr(d[0][4], 0)) + + if not d[1][1] in self.props_assign: + raise IndexCreatorValueException( + "Did you forget : or =?", self.cnt_line_nr(d[0][4], 0)) + + if d[0][0] == token.NAME or d[0][0] == token.STRING: + if d[0][1] in self.props_set: + raise IndexCreatorValueException("Properity %s is set more than once" % d[0][1], self.cnt_line_nr(d[0][4], 0)) + self.props_set += [d[0][1]] + if d[0][1] == "type" or d[0][1] == "name": + t, tk, _, _, line = d[2] + + if d_len > 3: + raise IndexCreatorValueException( + "Wrong value to assign", self.cnt_line_nr(line, 0)) + + if t == token.STRING: + m = re.search('\s*(?P[\'\"]+)(.*?)(?P=a)\s*', tk) + if m: + tk = m.groups()[1] + elif t != token.NAME: + raise IndexCreatorValueException( + "Wrong value to assign", self.cnt_line_nr(line, 0)) + + if d[0][1] == "type": + if d[2][1] == "TreeBasedIndex": + self.custom_header.add("from CodernityDB.tree_index import TreeBasedIndex\n") + elif d[2][1] == "MultiTreeBasedIndex": + self.custom_header.add("from CodernityDB.tree_index import MultiTreeBasedIndex\n") + elif d[2][1] == "MultiHashIndex": + self.custom_header.add("from CodernityDB.hash_index import MultiHashIndex\n") + self.tokens_head.insert(2, tk) + self.index_type = tk + else: + self.index_name = tk + return + else: + self.tokens += ['\n kwargs["' + d[0][1] + '"]'] + else: + raise IndexCreatorValueException("Can't handle properity assingment ", self.cnt_line_nr(d[0][4], 0)) + + self.tokens += ['='] + + self.check_adjacents(d[2:], 0) + self.check_colons(d[2:], 0) + + for i in d[2:]: + self.tokens += [i[1]] + + def generate_func(self, t, tk, pos_start, pos_end, line, hdata, stage): + if self.last_line[stage] != -1 and pos_start[0] > self.last_line[stage] and line != '': + raise IndexCreatorFunctionException("This line will never be executed!", self.cnt_line_nr(line, stage)) + if t == 0: + return + + if pos_start[1] == 0: + if self.line_cons[stage][pos_start[0] - 1] == -1: + self.tokens += ['\n return'] + self.last_line[stage] = pos_start[0] + else: + self.tokens += ['\n if'] + elif tk == ':' and self.line_cons[stage][pos_start[0] - 1] > -1: + if self.line_cons[stage][pos_start[0] - 1] == 0: + self.tokens += [':\n return'] + return + self.line_cons[stage][pos_start[0] - 1] -= 1 + + if tk in self.logic2: + # print tk + if line[pos_start[1] - 1] != tk and line[pos_start[1] + 1] != tk: + self.tokens += [tk] + if line[pos_start[1] - 1] != tk and line[pos_start[1] + 1] == tk: + if tk == '&': + self.tokens += ['and'] + else: + self.tokens += ['or'] + return + + if self.brackets != 0: + def search_through_known_dicts(a): + for i, (n, r) in self.known_dicts_in_mkv: + if i == tk and r > pos_start[1] and n == pos_start[0] and hdata == 'data': + return True + return False + + if t == token.NAME and len(self.funcs_stack) > 0 and self.funcs_stack[-1][0] == 'md5' and search_through_known_dicts(tk): + raise IndexCreatorValueException("Second value returned by make_key_value for sure isn't a dictionary ", self.cnt_line_nr(line, 1)) + + if tk == ')': + self.cur_brackets -= 1 + if len(self.funcs_stack) > 0 and self.cur_brackets == self.funcs_stack[-1][1]: + self.tokens += [tk] + self.tokens += self.funcs[self.funcs_stack[-1][0]][1] + del self.funcs_stack[-1] + return + if tk == '(': + self.cur_brackets += 1 + + if tk in self.none: + self.tokens += ['None'] + return + + if t == token.NAME and tk not in self.logic and tk != hdata: + if tk not in self.funcs: + self.tokens += [hdata + '["' + tk + '"]'] + else: + self.tokens += self.funcs[tk][0] + if tk in self.funcs_with_body: + self.funcs_with_body[tk] = ( + self.funcs_with_body[tk][0], True) + self.custom_header.add(self.handle_int_imports.get(tk)) + self.funcs_stack += [(tk, self.cur_brackets)] + else: + self.tokens += [tk] + + def handle_make_value(self, t, tk, pos_start, pos_end, line): + self.generate_func(t, tk, pos_start, pos_end, line, 'data', 1) + + def handle_make_key(self, t, tk, pos_start, pos_end, line): + self.generate_func(t, tk, pos_start, pos_end, line, 'key', 2) diff --git a/libs/CodernityDB/lfu_cache.py b/libs/CodernityDB/lfu_cache.py new file mode 100644 index 00000000..e11ffc95 --- /dev/null +++ b/libs/CodernityDB/lfu_cache.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +from heapq import nsmallest +from operator import itemgetter +from collections import defaultdict + +try: + from collections import Counter +except ImportError: + class Counter(dict): + 'Mapping where default values are zero' + def __missing__(self, key): + return 0 + + +def cache1lvl(maxsize=100): + """ + modified version of http://code.activestate.com/recipes/498245/ + """ + def decorating_function(user_function): + cache = {} + use_count = Counter() + + @functools.wraps(user_function) + def wrapper(key, *args, **kwargs): + try: + result = cache[key] + except KeyError: + if len(cache) == maxsize: + for k, _ in nsmallest(maxsize // 10 or 1, + use_count.iteritems(), + key=itemgetter(1)): + del cache[k], use_count[k] + cache[key] = user_function(key, *args, **kwargs) + result = cache[key] + # result = user_function(obj, key, *args, **kwargs) + finally: + use_count[key] += 1 + return result + + def clear(): + cache.clear() + use_count.clear() + + def delete(key): + try: + del cache[key] + del use_count[key] + except KeyError: + return False + else: + return True + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + return wrapper + return decorating_function + + +def twolvl_iterator(dict): + for k, v in dict.iteritems(): + for kk, vv in v.iteritems(): + yield k, kk, vv + + +def cache2lvl(maxsize=100): + """ + modified version of http://code.activestate.com/recipes/498245/ + """ + def decorating_function(user_function): + cache = {} + use_count = defaultdict(Counter) + + @functools.wraps(user_function) + def wrapper(*args, **kwargs): +# return user_function(*args, **kwargs) + try: + result = cache[args[0]][args[1]] + except KeyError: + if wrapper.cache_size == maxsize: + to_delete = maxsize // 10 or 1 + for k1, k2, v in nsmallest(to_delete, + twolvl_iterator(use_count), + key=itemgetter(2)): + del cache[k1][k2], use_count[k1][k2] + if not cache[k1]: + del cache[k1] + del use_count[k1] + wrapper.cache_size -= to_delete + result = user_function(*args, **kwargs) + try: + cache[args[0]][args[1]] = result + except KeyError: + cache[args[0]] = {args[1]: result} + wrapper.cache_size += 1 + finally: + use_count[args[0]][args[1]] += 1 + return result + + def clear(): + cache.clear() + use_count.clear() + + def delete(key, inner_key=None): + if inner_key is not None: + try: + del cache[key][inner_key] + del use_count[key][inner_key] + if not cache[key]: + del cache[key] + del use_count[key] + wrapper.cache_size -= 1 + except KeyError: + return False + else: + return True + else: + try: + wrapper.cache_size -= len(cache[key]) + del cache[key] + del use_count[key] + except KeyError: + return False + else: + return True + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + wrapper.cache_size = 0 + return wrapper + return decorating_function diff --git a/libs/CodernityDB/lfu_cache_with_lock.py b/libs/CodernityDB/lfu_cache_with_lock.py new file mode 100644 index 00000000..39f43cc6 --- /dev/null +++ b/libs/CodernityDB/lfu_cache_with_lock.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +from heapq import nsmallest +from operator import itemgetter +from collections import defaultdict + + +try: + from collections import Counter +except ImportError: + class Counter(dict): + 'Mapping where default values are zero' + def __missing__(self, key): + return 0 + + +def twolvl_iterator(dict): + for k, v in dict.iteritems(): + for kk, vv in v.iteritems(): + yield k, kk, vv + + +def create_cache1lvl(lock_obj): + def cache1lvl(maxsize=100): + """ + modified version of http://code.activestate.com/recipes/498245/ + """ + def decorating_function(user_function): + cache = {} + use_count = Counter() + lock = lock_obj() + + @functools.wraps(user_function) + def wrapper(key, *args, **kwargs): + try: + result = cache[key] + except KeyError: + with lock: + if len(cache) == maxsize: + for k, _ in nsmallest(maxsize // 10 or 1, + use_count.iteritems(), + key=itemgetter(1)): + del cache[k], use_count[k] + cache[key] = user_function(key, *args, **kwargs) + result = cache[key] + use_count[key] += 1 + else: + with lock: + use_count[key] += 1 + return result + + def clear(): + cache.clear() + use_count.clear() + + def delete(key): + try: + del cache[key] + del use_count[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + return wrapper + return decorating_function + return cache1lvl + + +def create_cache2lvl(lock_obj): + def cache2lvl(maxsize=100): + """ + modified version of http://code.activestate.com/recipes/498245/ + """ + def decorating_function(user_function): + cache = {} + use_count = defaultdict(Counter) + lock = lock_obj() + + @functools.wraps(user_function) + def wrapper(*args, **kwargs): + try: + result = cache[args[0]][args[1]] + except KeyError: + with lock: + if wrapper.cache_size == maxsize: + to_delete = maxsize / 10 or 1 + for k1, k2, v in nsmallest(to_delete, + twolvl_iterator( + use_count), + key=itemgetter(2)): + del cache[k1][k2], use_count[k1][k2] + if not cache[k1]: + del cache[k1] + del use_count[k1] + wrapper.cache_size -= to_delete + result = user_function(*args, **kwargs) + try: + cache[args[0]][args[1]] = result + except KeyError: + cache[args[0]] = {args[1]: result} + use_count[args[0]][args[1]] += 1 + wrapper.cache_size += 1 + else: + use_count[args[0]][args[1]] += 1 + return result + + def clear(): + cache.clear() + use_count.clear() + + def delete(key, *args): + if args: + try: + del cache[key][args[0]] + del use_count[key][args[0]] + if not cache[key]: + del cache[key] + del use_count[key] + wrapper.cache_size -= 1 + return True + except KeyError: + return False + else: + try: + wrapper.cache_size -= len(cache[key]) + del cache[key] + del use_count[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + wrapper.cache_size = 0 + return wrapper + return decorating_function + return cache2lvl diff --git a/libs/CodernityDB/migrate.py b/libs/CodernityDB/migrate.py new file mode 100644 index 00000000..4d0b4005 --- /dev/null +++ b/libs/CodernityDB/migrate.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from CodernityDB.database import Database +import shutil +import os + + +def migrate(source, destination): + """ + Very basic for now + """ + dbs = Database(source) + dbt = Database(destination) + dbs.open() + dbt.create() + dbt.close() + for curr in os.listdir(os.path.join(dbs.path, '_indexes')): + if curr != '00id.py': + shutil.copyfile(os.path.join(dbs.path, '_indexes', curr), + os.path.join(dbt.path, '_indexes', curr)) + dbt.open() + for c in dbs.all('id'): + del c['_rev'] + dbt.insert(c) + return True + + +if __name__ == '__main__': + import sys + migrate(sys.argv[1], sys.argv[2]) diff --git a/libs/CodernityDB/misc.py b/libs/CodernityDB/misc.py new file mode 100644 index 00000000..54c94812 --- /dev/null +++ b/libs/CodernityDB/misc.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from random import getrandbits, randrange +import uuid + + +class NONE: + """ + It's inteded to be None but different, + for internal use only! + """ + pass + + +def random_hex_32(): + return uuid.UUID(int=getrandbits(128), version=4).hex + + +def random_hex_4(*args, **kwargs): + return '%04x' % randrange(256 ** 2) diff --git a/libs/CodernityDB/patch.py b/libs/CodernityDB/patch.py new file mode 100644 index 00000000..4c074f43 --- /dev/null +++ b/libs/CodernityDB/patch.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from CodernityDB.misc import NONE + + +def __patch(obj, name, new): + n = NONE() + orig = getattr(obj, name, n) + if orig is not n: + if orig == new: + raise Exception("Shouldn't happen, new and orig are the same") + setattr(obj, name, new) + return + + +def patch_cache_lfu(lock_obj): + """ + Patnches cache mechanizm to be thread safe (gevent ones also) + + .. note:: + + It's internal CodernityDB mechanizm, it will be called when needed + + """ + import lfu_cache + import lfu_cache_with_lock + lfu_lock1lvl = lfu_cache_with_lock.create_cache1lvl(lock_obj) + lfu_lock2lvl = lfu_cache_with_lock.create_cache2lvl(lock_obj) + __patch(lfu_cache, 'cache1lvl', lfu_lock1lvl) + __patch(lfu_cache, 'cache2lvl', lfu_lock2lvl) + + +def patch_cache_rr(lock_obj): + """ + Patches cache mechanizm to be thread safe (gevent ones also) + + .. note:: + + It's internal CodernityDB mechanizm, it will be called when needed + + """ + import rr_cache + import rr_cache_with_lock + rr_lock1lvl = rr_cache_with_lock.create_cache1lvl(lock_obj) + rr_lock2lvl = rr_cache_with_lock.create_cache2lvl(lock_obj) + __patch(rr_cache, 'cache1lvl', rr_lock1lvl) + __patch(rr_cache, 'cache2lvl', rr_lock2lvl) + + +def patch_flush_fsync(db_obj): + """ + Will always execute index.fsync after index.flush. + + .. note:: + + It's for advanced users, use when you understand difference between `flush` and `fsync`, and when you definitely need that. + + It's important to call it **AFTER** database has all indexes etc (after db.create or db.open) + + Example usage:: + + ... + db = Database('/tmp/patch_demo') + db.create() + patch_flush_fsync(db) + ... + + """ + + def always_fsync(ind_obj): + def _inner(): + ind_obj.orig_flush() + ind_obj.fsync() + return _inner + + for index in db_obj.indexes: + setattr(index, 'orig_flush', index.flush) + setattr(index, 'flush', always_fsync(index)) + + setattr(db_obj, 'orig_flush', db_obj.flush) + setattr(db_obj, 'flush', always_fsync(db_obj)) + + return diff --git a/libs/CodernityDB/rr_cache.py b/libs/CodernityDB/rr_cache.py new file mode 100644 index 00000000..5801b7cc --- /dev/null +++ b/libs/CodernityDB/rr_cache.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from random import choice + + +def cache1lvl(maxsize=100): + def decorating_function(user_function): + cache1lvl = {} + + @functools.wraps(user_function) + def wrapper(key, *args, **kwargs): + try: + result = cache1lvl[key] + except KeyError: + if len(cache1lvl) == maxsize: + for i in xrange(maxsize // 10 or 1): + del cache1lvl[choice(cache1lvl.keys())] + cache1lvl[key] = user_function(key, *args, **kwargs) + result = cache1lvl[key] +# result = user_function(obj, key, *args, **kwargs) + return result + + def clear(): + cache1lvl.clear() + + def delete(key): + try: + del cache1lvl[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache1lvl + wrapper.delete = delete + return wrapper + return decorating_function + + +def cache2lvl(maxsize=100): + def decorating_function(user_function): + cache = {} + + @functools.wraps(user_function) + def wrapper(*args, **kwargs): +# return user_function(*args, **kwargs) + try: + result = cache[args[0]][args[1]] + except KeyError: +# print wrapper.cache_size + if wrapper.cache_size == maxsize: + to_delete = maxsize // 10 or 1 + for i in xrange(to_delete): + key1 = choice(cache.keys()) + key2 = choice(cache[key1].keys()) + del cache[key1][key2] + if not cache[key1]: + del cache[key1] + wrapper.cache_size -= to_delete +# print wrapper.cache_size + result = user_function(*args, **kwargs) + try: + cache[args[0]][args[1]] = result + except KeyError: + cache[args[0]] = {args[1]: result} + wrapper.cache_size += 1 + return result + + def clear(): + cache.clear() + wrapper.cache_size = 0 + + def delete(key, inner_key=None): + if inner_key: + try: + del cache[key][inner_key] + if not cache[key]: + del cache[key] + wrapper.cache_size -= 1 + return True + except KeyError: + return False + else: + try: + wrapper.cache_size -= len(cache[key]) + del cache[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + wrapper.cache_size = 0 + return wrapper + return decorating_function diff --git a/libs/CodernityDB/rr_cache_with_lock.py b/libs/CodernityDB/rr_cache_with_lock.py new file mode 100644 index 00000000..66298c59 --- /dev/null +++ b/libs/CodernityDB/rr_cache_with_lock.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +from random import choice + + +def create_cache1lvl(lock_obj): + def cache1lvl(maxsize=100): + def decorating_function(user_function): + cache = {} + lock = lock_obj() + + @functools.wraps(user_function) + def wrapper(key, *args, **kwargs): + try: + result = cache[key] + except KeyError: + with lock: + if len(cache) == maxsize: + for i in xrange(maxsize // 10 or 1): + del cache[choice(cache.keys())] + cache[key] = user_function(key, *args, **kwargs) + result = cache[key] + return result + + def clear(): + cache.clear() + + def delete(key): + try: + del cache[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + return wrapper + return decorating_function + return cache1lvl + + +def create_cache2lvl(lock_obj): + def cache2lvl(maxsize=100): + def decorating_function(user_function): + cache = {} + lock = lock_obj() + + @functools.wraps(user_function) + def wrapper(*args, **kwargs): + try: + result = cache[args[0]][args[1]] + except KeyError: + with lock: + if wrapper.cache_size == maxsize: + to_delete = maxsize // 10 or 1 + for i in xrange(to_delete): + key1 = choice(cache.keys()) + key2 = choice(cache[key1].keys()) + del cache[key1][key2] + if not cache[key1]: + del cache[key1] + wrapper.cache_size -= to_delete + result = user_function(*args, **kwargs) + try: + cache[args[0]][args[1]] = result + except KeyError: + cache[args[0]] = {args[1]: result} + wrapper.cache_size += 1 + return result + + def clear(): + cache.clear() + wrapper.cache_size = 0 + + def delete(key, *args): + if args: + try: + del cache[key][args[0]] + if not cache[key]: + del cache[key] + wrapper.cache_size -= 1 + return True + except KeyError: + return False + else: + try: + wrapper.cache_size -= len(cache[key]) + del cache[key] + return True + except KeyError: + return False + + wrapper.clear = clear + wrapper.cache = cache + wrapper.delete = delete + wrapper.cache_size = 0 + return wrapper + return decorating_function + return cache2lvl diff --git a/libs/CodernityDB/sharded_hash.py b/libs/CodernityDB/sharded_hash.py new file mode 100644 index 00000000..08a8c2f0 --- /dev/null +++ b/libs/CodernityDB/sharded_hash.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from CodernityDB.hash_index import UniqueHashIndex, HashIndex +from CodernityDB.sharded_index import ShardedIndex +from CodernityDB.index import IndexPreconditionsException + +from random import getrandbits +import uuid + + +class IU_ShardedUniqueHashIndex(ShardedIndex): + + custom_header = """import uuid +from random import getrandbits +from CodernityDB.sharded_index import ShardedIndex +""" + + def __init__(self, db_path, name, *args, **kwargs): + if kwargs.get('sh_nums', 0) > 255: + raise IndexPreconditionsException("Too many shards") + kwargs['ind_class'] = UniqueHashIndex + super(IU_ShardedUniqueHashIndex, self).__init__(db_path, + name, *args, **kwargs) + self.patchers.append(self.wrap_insert_id_index) + + @staticmethod + def wrap_insert_id_index(db_obj, clean=False): + def _insert_id_index(_rev, data): + """ + Performs insert on **id** index. + """ + _id, value = db_obj.id_ind.make_key_value(data) # may be improved + trg_shard = _id[:2] + storage = db_obj.id_ind.shards_r[trg_shard].storage + start, size = storage.insert(value) + db_obj.id_ind.insert(_id, _rev, start, size) + return _id + if not clean: + if hasattr(db_obj, '_insert_id_index_orig'): + raise IndexPreconditionsException( + "Already patched, something went wrong") + setattr(db_obj, "_insert_id_index_orig", db_obj._insert_id_index) + setattr(db_obj, "_insert_id_index", _insert_id_index) + else: + setattr(db_obj, "_insert_id_index", db_obj._insert_id_index_orig) + delattr(db_obj, "_insert_id_index_orig") + + def create_key(self): + h = uuid.UUID(int=getrandbits(128), version=4).hex + trg = self.last_used + 1 + if trg >= self.sh_nums: + trg = 0 + self.last_used = trg + h = '%02x%30s' % (trg, h[2:]) + return h + + def delete(self, key, *args, **kwargs): + trg_shard = key[:2] + op = self.shards_r[trg_shard] + return op.delete(key, *args, **kwargs) + + def update(self, key, *args, **kwargs): + trg_shard = key[:2] + self.last_used = int(trg_shard, 16) + op = self.shards_r[trg_shard] + return op.update(key, *args, **kwargs) + + def insert(self, key, *args, **kwargs): + trg_shard = key[:2] # in most cases it's in create_key BUT not always + self.last_used = int(key[:2], 16) + op = self.shards_r[trg_shard] + return op.insert(key, *args, **kwargs) + + def get(self, key, *args, **kwargs): + trg_shard = key[:2] + self.last_used = int(trg_shard, 16) + op = self.shards_r[trg_shard] + return op.get(key, *args, **kwargs) + + +class ShardedUniqueHashIndex(IU_ShardedUniqueHashIndex): + + # allow unique hash to be used directly + custom_header = 'from CodernityDB.sharded_hash import IU_ShardedUniqueHashIndex' + + pass + + +class IU_ShardedHashIndex(ShardedIndex): + + custom_header = """from CodernityDB.sharded_index import ShardedIndex""" + + def __init__(self, db_path, name, *args, **kwargs): + kwargs['ind_class'] = HashIndex + super(IU_ShardedHashIndex, self).__init__(db_path, name, * + args, **kwargs) + + def calculate_shard(self, key): + """ + Must be implemented. It has to return shard to be used by key + + :param key: key + :returns: target shard + :rtype: int + """ + raise NotImplementedError() + + def delete(self, doc_id, key, *args, **kwargs): + trg_shard = self.calculate_shard(key) + op = self.shards_r[trg_shard] + return op.delete(doc_id, key, *args, **kwargs) + + def insert(self, doc_id, key, *args, **kwargs): + trg_shard = self.calculate_shard(key) + op = self.shards_r[trg_shard] + return op.insert(doc_id, key, *args, **kwargs) + + def update(self, doc_id, key, *args, **kwargs): + trg_shard = self.calculate_shard(key) + op = self.shards_r[trg_shard] + return op.insert(doc_id, key, *args, **kwargs) + + def get(self, key, *args, **kwargs): + trg_shard = self.calculate_shard(key) + op = self.shards_r[trg_shard] + return op.get(key, *args, **kwargs) + + +class ShardedHashIndex(IU_ShardedHashIndex): + pass diff --git a/libs/CodernityDB/sharded_index.py b/libs/CodernityDB/sharded_index.py new file mode 100644 index 00000000..2bdf9d75 --- /dev/null +++ b/libs/CodernityDB/sharded_index.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from CodernityDB.index import Index +# from CodernityDB.env import cdb_environment +# import warnings + + +class ShardedIndex(Index): + + def __init__(self, db_path, name, *args, **kwargs): + """ + There are 3 additional parameters. You have to hardcode them in your custom class. **NEVER** use directly + + :param int sh_nums: how many shards should be + :param class ind_class: Index class to use (HashIndex or your custom one) + :param bool use_make_keys: if True, `make_key`, and `make_key_value` will be overriden with those from first shard + + The rest parameters are passed straight to `ind_class` shards. + + """ + super(ShardedIndex, self).__init__(db_path, name) + try: + self.sh_nums = kwargs.pop('sh_nums') + except KeyError: + self.sh_nums = 5 + try: + ind_class = kwargs.pop('ind_class') + except KeyError: + raise Exception("ind_class must be given") + else: + # if not isinstance(ind_class, basestring): + # ind_class = ind_class.__name__ + self.ind_class = ind_class + if 'use_make_keys' in kwargs: + self.use_make_keys = kwargs.pop('use_make_keys') + else: + self.use_make_keys = False + self._set_shard_datas(*args, **kwargs) + self.patchers = [] # database object patchers + + def _set_shard_datas(self, *args, **kwargs): + self.shards = {} + self.shards_r = {} +# ind_class = globals()[self.ind_class] + ind_class = self.ind_class + i = 0 + for sh_name in [self.name + str(x) for x in xrange(self.sh_nums)]: + # dict is better than list in that case + self.shards[i] = ind_class(self.db_path, sh_name, *args, **kwargs) + self.shards_r['%02x' % i] = self.shards[i] + self.shards_r[i] = self.shards[i] + i += 1 + + if not self.use_make_keys: + self.make_key = self.shards[0].make_key + self.make_key_value = self.shards[0].make_key_value + + self.last_used = 0 + + @property + def storage(self): + st = self.shards[self.last_used].storage + return st + + def __getattr__(self, name): + return getattr(self.shards[self.last_used], name) + + def open_index(self): + for curr in self.shards.itervalues(): + curr.open_index() + + def create_index(self): + for curr in self.shards.itervalues(): + curr.create_index() + + def destroy(self): + for curr in self.shards.itervalues(): + curr.destroy() + + def compact(self): + for curr in self.shards.itervalues(): + curr.compact() + + def reindex(self): + for curr in self.shards.itervalues(): + curr.reindex() + + def all(self, *args, **kwargs): + for curr in self.shards.itervalues(): + for now in curr.all(*args, **kwargs): + yield now + + def get_many(self, *args, **kwargs): + for curr in self.shards.itervalues(): + for now in curr.get_many(*args, **kwargs): + yield now diff --git a/libs/CodernityDB/storage.py b/libs/CodernityDB/storage.py new file mode 100644 index 00000000..30be1f3b --- /dev/null +++ b/libs/CodernityDB/storage.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import struct +import shutil +import marshal +import io + + +try: + from CodernityDB import __version__ +except ImportError: + from __init__ import __version__ + + +class StorageException(Exception): + pass + + +class DummyStorage(object): + """ + Storage mostly used to fake real storage + """ + + def create(self, *args, **kwargs): + pass + + def open(self, *args, **kwargs): + pass + + def close(self, *args, **kwargs): + pass + + def data_from(self, *args, **kwargs): + pass + + def data_to(self, *args, **kwargs): + pass + + def save(self, *args, **kwargs): + return 0, 0 + + def insert(self, *args, **kwargs): + return self.save(*args, **kwargs) + + def update(self, *args, **kwargs): + return 0, 0 + + def get(self, *args, **kwargs): + return None + + # def compact(self, *args, **kwargs): + # pass + + def fsync(self, *args, **kwargs): + pass + + def flush(self, *args, **kwargs): + pass + + +class IU_Storage(object): + + __version__ = __version__ + + def __init__(self, db_path, name='main'): + self.db_path = db_path + self.name = name + self._header_size = 100 + + def create(self): + if os.path.exists(os.path.join(self.db_path, self.name + "_stor")): + raise IOError("Storage already exists!") + with io.open(os.path.join(self.db_path, self.name + "_stor"), 'wb') as f: + f.write(struct.pack("10s90s", self.__version__, '|||||')) + f.close() + self._f = io.open(os.path.join( + self.db_path, self.name + "_stor"), 'r+b', buffering=0) + self.flush() + self._f.seek(0, 2) + + def open(self): + if not os.path.exists(os.path.join(self.db_path, self.name + "_stor")): + raise IOError("Storage doesn't exists!") + self._f = io.open(os.path.join( + self.db_path, self.name + "_stor"), 'r+b', buffering=0) + self.flush() + self._f.seek(0, 2) + + def destroy(self): + os.unlink(os.path.join(self.db_path, self.name + '_stor')) + + def close(self): + self._f.close() + # self.flush() + # self.fsync() + + def data_from(self, data): + return marshal.loads(data) + + def data_to(self, data): + return marshal.dumps(data) + + def save(self, data): + s_data = self.data_to(data) + self._f.seek(0, 2) + start = self._f.tell() + size = len(s_data) + self._f.write(s_data) + self.flush() + return start, size + + def insert(self, data): + return self.save(data) + + def update(self, data): + return self.save(data) + + def get(self, start, size, status='c'): + if status == 'd': + return None + else: + self._f.seek(start) + return self.data_from(self._f.read(size)) + + def flush(self): + self._f.flush() + + def fsync(self): + os.fsync(self._f.fileno()) + + +# classes for public use, done in this way because of +# generation static files with indexes (_index directory) + + +class Storage(IU_Storage): + pass diff --git a/libs/CodernityDB/tree_index.py b/libs/CodernityDB/tree_index.py new file mode 100644 index 00000000..b79805db --- /dev/null +++ b/libs/CodernityDB/tree_index.py @@ -0,0 +1,2048 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011-2013 Codernity (http://codernity.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from index import Index, IndexException, DocIdNotFound, ElemNotFound +import struct +import marshal +import os +import io +import shutil +from storage import IU_Storage +# from ipdb import set_trace + +from CodernityDB.env import cdb_environment +from CodernityDB.index import TryReindexException + +if cdb_environment.get('rlock_obj'): + from CodernityDB import patch + patch.patch_cache_rr(cdb_environment['rlock_obj']) + +from CodernityDB.rr_cache import cache1lvl, cache2lvl + +tree_buffer_size = io.DEFAULT_BUFFER_SIZE + +cdb_environment['tree_buffer_size'] = tree_buffer_size + + +MODE_FIRST = 0 +MODE_LAST = 1 + +MOVE_BUFFER_PREV = 0 +MOVE_BUFFER_NEXT = 1 + + +class NodeCapacityException(IndexException): + pass + + +class IU_TreeBasedIndex(Index): + + custom_header = 'from CodernityDB.tree_index import TreeBasedIndex' + + def __init__(self, db_path, name, key_format='32s', pointer_format='I', + meta_format='32sIIc', node_capacity=10, storage_class=None): + if node_capacity < 3: + raise NodeCapacityException + super(IU_TreeBasedIndex, self).__init__(db_path, name) + self.data_start = self._start_ind + 1 + self.node_capacity = node_capacity + self.flag_format = 'c' + self.elements_counter_format = 'h' + self.pointer_format = pointer_format + self.key_format = key_format + self.meta_format = meta_format + self._count_props() + if not storage_class: + storage_class = IU_Storage + if storage_class and not isinstance(storage_class, basestring): + storage_class = storage_class.__name__ + self.storage_class = storage_class + self.storage = None + cache = cache1lvl(100) + twolvl_cache = cache2lvl(150) + self._find_key = cache(self._find_key) + self._match_doc_id = cache(self._match_doc_id) +# self._read_single_leaf_record = +# twolvl_cache(self._read_single_leaf_record) + self._find_key_in_leaf = twolvl_cache(self._find_key_in_leaf) + self._read_single_node_key = twolvl_cache(self._read_single_node_key) + self._find_first_key_occurence_in_node = twolvl_cache( + self._find_first_key_occurence_in_node) + self._find_last_key_occurence_in_node = twolvl_cache( + self._find_last_key_occurence_in_node) + self._read_leaf_nr_of_elements = cache(self._read_leaf_nr_of_elements) + self._read_leaf_neighbours = cache(self._read_leaf_neighbours) + self._read_leaf_nr_of_elements_and_neighbours = cache( + self._read_leaf_nr_of_elements_and_neighbours) + self._read_node_nr_of_elements_and_children_flag = cache( + self._read_node_nr_of_elements_and_children_flag) + + def _count_props(self): + """ + Counts dynamic properties for tree, such as all complex formats + """ + self.single_leaf_record_format = self.key_format + self.meta_format + self.single_node_record_format = self.pointer_format + \ + self.key_format + self.pointer_format + self.node_format = self.elements_counter_format + self.flag_format\ + + self.pointer_format + (self.key_format + + self.pointer_format) * self.node_capacity + self.leaf_format = self.elements_counter_format + self.pointer_format * 2\ + + (self.single_leaf_record_format) * self.node_capacity + self.leaf_heading_format = self.elements_counter_format + \ + self.pointer_format * 2 + self.node_heading_format = self.elements_counter_format + \ + self.flag_format + self.key_size = struct.calcsize('<' + self.key_format) + self.meta_size = struct.calcsize('<' + self.meta_format) + self.single_leaf_record_size = struct.calcsize('<' + self. + single_leaf_record_format) + self.single_node_record_size = struct.calcsize('<' + self. + single_node_record_format) + self.node_size = struct.calcsize('<' + self.node_format) + self.leaf_size = struct.calcsize('<' + self.leaf_format) + self.flag_size = struct.calcsize('<' + self.flag_format) + self.elements_counter_size = struct.calcsize('<' + self. + elements_counter_format) + self.pointer_size = struct.calcsize('<' + self.pointer_format) + self.leaf_heading_size = struct.calcsize( + '<' + self.leaf_heading_format) + self.node_heading_size = struct.calcsize( + '<' + self.node_heading_format) + + def create_index(self): + if os.path.isfile(os.path.join(self.db_path, self.name + '_buck')): + raise IndexException('Already exists') + with io.open(os.path.join(self.db_path, self.name + "_buck"), 'w+b') as f: + props = dict(name=self.name, + flag_format=self.flag_format, + pointer_format=self.pointer_format, + elements_counter_format=self.elements_counter_format, + node_capacity=self.node_capacity, + key_format=self.key_format, + meta_format=self.meta_format, + version=self.__version__, + storage_class=self.storage_class) + f.write(marshal.dumps(props)) + self.buckets = io.open(os.path.join(self.db_path, self.name + + "_buck"), 'r+b', buffering=0) + self._create_storage() + self.buckets.seek(self._start_ind) + self.buckets.write(struct.pack(' candidate_start: + move_buffer = MOVE_BUFFER_PREV + elif buffer_end < candidate_start + self.single_leaf_record_size: + move_buffer = MOVE_BUFFER_NEXT + else: + move_buffer = None + return self._calculate_key_position(leaf_start, (imin + imax) / 2, 'l'), (imin + imax) / 2, move_buffer + + def _choose_next_candidate_index_in_node(self, node_start, candidate_start, buffer_start, buffer_end, imin, imax): + if buffer_start > candidate_start: + move_buffer = MOVE_BUFFER_PREV + elif buffer_end < candidate_start + self.single_node_record_size: + (self.pointer_size + self.key_size) - 1 + move_buffer = MOVE_BUFFER_NEXT + else: + move_buffer = None + return self._calculate_key_position(node_start, (imin + imax) / 2, 'n'), (imin + imax) / 2, move_buffer + + def _find_key_in_leaf(self, leaf_start, key, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_leaf_with_one_element(key, leaf_start)[-5:] + else: + return self._find_key_in_leaf_using_binary_search(key, leaf_start, nr_of_elements)[-5:] + + def _find_key_in_leaf_for_update(self, key, doc_id, leaf_start, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_leaf_with_one_element(key, leaf_start, doc_id=doc_id) + else: + return self._find_key_in_leaf_using_binary_search(key, leaf_start, nr_of_elements, mode=MODE_FIRST, doc_id=doc_id) + + def _find_index_of_first_key_equal_or_smaller_key(self, key, leaf_start, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_leaf_with_one_element(key, leaf_start, mode=MODE_FIRST, return_closest=True)[:2] + else: + return self._find_key_in_leaf_using_binary_search(key, leaf_start, nr_of_elements, mode=MODE_FIRST, return_closest=True)[:2] + + def _find_index_of_last_key_equal_or_smaller_key(self, key, leaf_start, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_leaf_with_one_element(key, leaf_start, mode=MODE_LAST, return_closest=True)[:2] + else: + return self._find_key_in_leaf_using_binary_search(key, leaf_start, nr_of_elements, mode=MODE_LAST, return_closest=True)[:2] + + def _find_index_of_first_key_equal(self, key, leaf_start, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_leaf_with_one_element(key, leaf_start, mode=MODE_FIRST)[:2] + else: + return self._find_key_in_leaf_using_binary_search(key, leaf_start, nr_of_elements, mode=MODE_FIRST)[:2] + + def _find_key_in_leaf_with_one_element(self, key, leaf_start, doc_id=None, mode=None, return_closest=False): + curr_key, curr_doc_id, curr_start, curr_size,\ + curr_status = self._read_single_leaf_record(leaf_start, 0) + if key != curr_key: + if return_closest and curr_status != 'd': + return leaf_start, 0 + else: + raise ElemNotFound + else: + if curr_status == 'd': + raise ElemNotFound + elif doc_id is not None and doc_id != curr_doc_id: +# should't happen, crashes earlier on id index + raise DocIdNotFound + else: + return leaf_start, 0, curr_doc_id, curr_key, curr_start, curr_size, curr_status + + def _find_key_in_leaf_using_binary_search(self, key, leaf_start, nr_of_elements, doc_id=None, mode=None, return_closest=False): + """ + Binary search implementation used in all get functions + """ + imin, imax = 0, nr_of_elements - 1 + buffer_start, buffer_end = self._set_buffer_limits() + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + self._calculate_key_position(leaf_start, + (imin + imax) / 2, + 'l'), + buffer_start, + buffer_end, + imin, imax) + while imax != imin and imax > imin: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + candidate_index) + candidate_start = self._calculate_key_position( + leaf_start, candidate_index, 'l') + if key < curr_key: + if move_buffer == MOVE_BUFFER_PREV: + buffer_start, buffer_end = self._prev_buffer( + buffer_start, buffer_end) + else: # if next chosen element is in current buffer, abort moving to other + move_buffer is None + imax = candidate_index - 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + elif key == curr_key: + if mode == MODE_LAST: + if move_buffer == MOVE_BUFFER_NEXT: + buffer_start, buffer_end = self._next_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imin = candidate_index + 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + else: + if curr_status == 'o': + break + else: + if move_buffer == MOVE_BUFFER_PREV: + buffer_start, buffer_end = self._prev_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imax = candidate_index + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + else: + if move_buffer == MOVE_BUFFER_NEXT: + buffer_start, buffer_end = self._next_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imin = candidate_index + 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + + if imax > imin: + chosen_key_position = candidate_index + else: + chosen_key_position = imax + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + chosen_key_position) + if key != curr_key: + if return_closest: # useful for find all bigger/smaller methods + return leaf_start, chosen_key_position + else: + raise ElemNotFound + if doc_id and doc_id == curr_doc_id and curr_status == 'o': + return leaf_start, chosen_key_position, curr_doc_id, curr_key, curr_start, curr_size, curr_status + else: + if mode == MODE_FIRST and imin < chosen_key_position: # check if there isn't any element with equal key before chosen one + matching_record_index = self._leaf_linear_key_search(key, + self._calculate_key_position(leaf_start, + imin, + 'l'), + imin, + chosen_key_position) + else: + matching_record_index = chosen_key_position + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + matching_record_index) + if curr_status == 'd' and not return_closest: + leaf_start, nr_of_elements, matching_record_index = self._find_existing(key, + matching_record_index, + leaf_start, + nr_of_elements) + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + matching_record_index) + if doc_id is not None and doc_id != curr_doc_id: + leaf_start, nr_of_elements, matching_record_index = self._match_doc_id(doc_id, + key, + matching_record_index, + leaf_start, + nr_of_elements) + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + matching_record_index) + return leaf_start, matching_record_index, curr_doc_id, curr_key, curr_start, curr_size, curr_status + + def _find_place_in_leaf(self, key, leaf_start, nr_of_elements): + if nr_of_elements == 1: + return self._find_place_in_leaf_with_one_element(key, leaf_start) + else: + return self._find_place_in_leaf_using_binary_search(key, leaf_start, nr_of_elements) + + def _find_place_in_leaf_with_one_element(self, key, leaf_start): + curr_key, curr_doc_id, curr_start, curr_size,\ + curr_status = self._read_single_leaf_record(leaf_start, 0) + if curr_status == 'd': + return leaf_start, 0, 0, False, True # leaf start, index of new key position, nr of rec to rewrite, full_leaf flag, on_deleted flag + else: + if key < curr_key: + return leaf_start, 0, 1, False, False + else: + return leaf_start, 1, 0, False, False + + def _find_place_in_leaf_using_binary_search(self, key, leaf_start, nr_of_elements): + """ + Binary search implementation used in insert function + """ + imin, imax = 0, nr_of_elements - 1 + buffer_start, buffer_end = self._set_buffer_limits() + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + self._calculate_key_position(leaf_start, + (imin + imax) / 2, + 'l'), + buffer_start, + buffer_end, + imin, imax) + while imax != imin and imax > imin: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + candidate_index) + candidate_start = self._calculate_key_position( + leaf_start, candidate_index, 'l') + if key < curr_key: + if move_buffer == MOVE_BUFFER_PREV: + buffer_start, buffer_end = self._prev_buffer( + buffer_start, buffer_end) + else: # if next chosen element is in current buffer, abort moving to other + move_buffer is None + imax = candidate_index - 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + else: + if move_buffer == MOVE_BUFFER_NEXT: + buffer_start, buffer_end = self._next_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imin = candidate_index + 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_leaf(leaf_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + if imax < imin and imin < nr_of_elements: + chosen_key_position = imin + else: + chosen_key_position = imax + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + chosen_key_position) + if curr_status == 'd': + return leaf_start, chosen_key_position, 0, False, True + elif key < curr_key: + if chosen_key_position > 0: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + chosen_key_position - 1) + if curr_start == 'd': + return leaf_start, chosen_key_position - 1, 0, False, True + else: + return leaf_start, chosen_key_position, nr_of_elements - chosen_key_position, (nr_of_elements == self.node_capacity), False + else: + return leaf_start, chosen_key_position, nr_of_elements - chosen_key_position, (nr_of_elements == self.node_capacity), False + else: + if chosen_key_position < nr_of_elements - 1: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_start, + chosen_key_position + 1) + if curr_start == 'd': + return leaf_start, chosen_key_position + 1, 0, False, True + else: + return leaf_start, chosen_key_position + 1, nr_of_elements - chosen_key_position - 1, (nr_of_elements == self.node_capacity), False + else: + return leaf_start, chosen_key_position + 1, nr_of_elements - chosen_key_position - 1, (nr_of_elements == self.node_capacity), False + + def _set_buffer_limits(self): + pos = self.buckets.tell() + buffer_start = pos - (pos % tree_buffer_size) + return buffer_start, (buffer_start + tree_buffer_size) + + def _find_first_key_occurence_in_node(self, node_start, key, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_node_with_one_element(key, node_start, mode=MODE_FIRST) + else: + return self._find_key_in_node_using_binary_search(key, node_start, nr_of_elements, mode=MODE_FIRST) + + def _find_last_key_occurence_in_node(self, node_start, key, nr_of_elements): + if nr_of_elements == 1: + return self._find_key_in_node_with_one_element(key, node_start, mode=MODE_LAST) + else: + return self._find_key_in_node_using_binary_search(key, node_start, nr_of_elements, mode=MODE_LAST) + + def _find_key_in_node_with_one_element(self, key, node_start, mode=None): + l_pointer, curr_key, r_pointer = self._read_single_node_key( + node_start, 0) + if key < curr_key: + return 0, l_pointer + elif key > curr_key: + return 0, r_pointer + else: + if mode == MODE_FIRST: + return 0, l_pointer + elif mode == MODE_LAST: + return 0, r_pointer + else: + raise Exception('Invalid mode declared: set first/last') + + def _find_key_in_node_using_binary_search(self, key, node_start, nr_of_elements, mode=None): + imin, imax = 0, nr_of_elements - 1 + buffer_start, buffer_end = self._set_buffer_limits() + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_node(node_start, + self._calculate_key_position(node_start, + (imin + imax) / 2, + 'n'), + buffer_start, + buffer_end, + imin, imax) + while imax != imin and imax > imin: + l_pointer, curr_key, r_pointer = self._read_single_node_key( + node_start, candidate_index) + candidate_start = self._calculate_key_position( + node_start, candidate_index, 'n') + if key < curr_key: + if move_buffer == MOVE_BUFFER_PREV: + buffer_start, buffer_end = self._prev_buffer( + buffer_start, buffer_end) + else: # if next chosen element is in current buffer, abort moving to other + move_buffer is None + imax = candidate_index - 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_node(node_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + elif key == curr_key: + if mode == MODE_LAST: + if move_buffer == MOVE_BUFFER_NEXT: + buffer_start, buffer_end = self._next_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imin = candidate_index + 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_node(node_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + else: + break + else: + if move_buffer == MOVE_BUFFER_NEXT: + buffer_start, buffer_end = self._next_buffer( + buffer_start, buffer_end) + else: + move_buffer is None + imin = candidate_index + 1 + candidate_start, candidate_index, move_buffer = self._choose_next_candidate_index_in_node(node_start, + candidate_start, + buffer_start, + buffer_end, + imin, imax) + + if imax > imin: + chosen_key_position = candidate_index + elif imax < imin and imin < nr_of_elements: + chosen_key_position = imin + else: + chosen_key_position = imax + l_pointer, curr_key, r_pointer = self._read_single_node_key( + node_start, chosen_key_position) + if mode == MODE_FIRST and imin < chosen_key_position: # check if there is no elements with equal key before chosen one + matching_record_index = self._node_linear_key_search(key, + self._calculate_key_position(node_start, + imin, + 'n'), + imin, + chosen_key_position) + else: + matching_record_index = chosen_key_position + l_pointer, curr_key, r_pointer = self._read_single_node_key( + node_start, matching_record_index) + if key < curr_key: + return matching_record_index, l_pointer + elif key > curr_key: + return matching_record_index, r_pointer + else: + if mode == MODE_FIRST: + return matching_record_index, l_pointer + elif mode == MODE_LAST: + return matching_record_index, r_pointer + else: + raise Exception('Invalid mode declared: first/last') + + def _update_leaf_ready_data(self, leaf_start, start_index, new_nr_of_elements, records_to_rewrite): + self.buckets.seek(leaf_start) + self.buckets.write(struct.pack(' new_leaf_size - 1: + key_moved_to_parent_node = leaf_data[(old_leaf_size - 1) * 5] + elif nr_of_records_to_rewrite == new_leaf_size - 1: + key_moved_to_parent_node = new_data[0] + else: + key_moved_to_parent_node = leaf_data[old_leaf_size * 5] + data_to_write = self._prepare_new_root_data(key_moved_to_parent_node, + left_leaf_start_position, + right_leaf_start_position, + 'l') + if nr_of_records_to_rewrite > half_size: + # key goes to first half + # prepare left leaf data + left_leaf_data = struct.pack('<' + self.leaf_heading_format + self.single_leaf_record_format + * (self.node_capacity - nr_of_records_to_rewrite), + old_leaf_size, + 0, + right_leaf_start_position, + *leaf_data[:-nr_of_records_to_rewrite * 5]) + left_leaf_data += struct.pack( + '<' + self.single_leaf_record_format * ( + nr_of_records_to_rewrite - new_leaf_size + 1), + new_data[0], + new_data[1], + new_data[2], + new_data[3], + new_data[4], + *leaf_data[-nr_of_records_to_rewrite * 5:(old_leaf_size - 1) * 5]) + # prepare right leaf_data + right_leaf_data = struct.pack('<' + self.elements_counter_format + 2 * self.pointer_format + + self.single_leaf_record_format * + new_leaf_size, + new_leaf_size, + left_leaf_start_position, + 0, + *leaf_data[-new_leaf_size * 5:]) + else: + # key goes to second half + if nr_of_records_to_rewrite: + records_before = leaf_data[old_leaf_size * + 5:-nr_of_records_to_rewrite * 5] + records_after = leaf_data[-nr_of_records_to_rewrite * 5:] + else: + records_before = leaf_data[old_leaf_size * 5:] + records_after = [] + + left_leaf_data = struct.pack( + '<' + self.leaf_heading_format + + self.single_leaf_record_format * old_leaf_size, + old_leaf_size, + 0, + right_leaf_start_position, + *leaf_data[:old_leaf_size * 5]) + # prepare right leaf_data + right_leaf_data = struct.pack('<' + self.elements_counter_format + 2 * self.pointer_format + + self.single_leaf_record_format * (new_leaf_size - + nr_of_records_to_rewrite - 1), + new_leaf_size, + left_leaf_start_position, + 0, + *records_before) + right_leaf_data += struct.pack( + '<' + self.single_leaf_record_format * ( + nr_of_records_to_rewrite + 1), + new_data[0], + new_data[1], + new_data[2], + new_data[3], + new_data[4], + *records_after) + left_leaf_data += (self.node_capacity - + old_leaf_size) * self.single_leaf_record_size * '\x00' + right_leaf_data += blanks + data_to_write += left_leaf_data + data_to_write += right_leaf_data + self.buckets.seek(self._start_ind) + self.buckets.write(struct.pack(' half_size: # insert key into first half of leaf + self.buckets.seek(self._calculate_key_position(leaf_start, + self.node_capacity - nr_of_records_to_rewrite, + 'l')) + # read all records with key>new_key + data = self.buckets.read( + nr_of_records_to_rewrite * self.single_leaf_record_size) + records_to_rewrite = struct.unpack( + '<' + nr_of_records_to_rewrite * self.single_leaf_record_format, data) + # remove deleted records, if succeded abort spliting + if self._update_if_has_deleted(leaf_start, + records_to_rewrite, + self.node_capacity - + nr_of_records_to_rewrite, + [new_key, new_doc_id, new_start, new_size, new_status]): + return None + key_moved_to_parent_node = records_to_rewrite[ + -new_leaf_size * 5] + # write new leaf at end of file + self.buckets.seek(0, 2) # end of file + new_leaf_start = self.buckets.tell() + # prepare new leaf_data + new_leaf = struct.pack('<' + self.elements_counter_format + 2 * self.pointer_format + + self.single_leaf_record_format * + new_leaf_size, + new_leaf_size, + leaf_start, + next_l, + *records_to_rewrite[-new_leaf_size * 5:]) + new_leaf += blanks + # write new leaf + self.buckets.write(new_leaf) + # update old leaf heading + self._update_leaf_size_and_pointers(leaf_start, + old_leaf_size, + prev_l, + new_leaf_start) + # seek position of new key in first half + self.buckets.seek(self._calculate_key_position(leaf_start, + self.node_capacity - nr_of_records_to_rewrite, + 'l')) + # write new key and keys after + self.buckets.write( + struct.pack( + '<' + self.single_leaf_record_format * + (nr_of_records_to_rewrite - new_leaf_size + 1), + new_key, + new_doc_id, + new_start, + new_size, + 'o', + *records_to_rewrite[:-new_leaf_size * 5])) + + if next_l: # when next_l is 0 there is no next leaf to update, avoids writing data at 0 position of file + self._update_leaf_prev_pointer( + next_l, new_leaf_start) + +# self._read_single_leaf_record.delete(leaf_start) + self._find_key_in_leaf.delete(leaf_start) + + return new_leaf_start, key_moved_to_parent_node + else: # key goes into second half of leaf ' + # seek half of the leaf + self.buckets.seek(self._calculate_key_position( + leaf_start, old_leaf_size, 'l')) + data = self.buckets.read( + self.single_leaf_record_size * (new_leaf_size - 1)) + records_to_rewrite = struct.unpack('<' + (new_leaf_size - 1) * + self.single_leaf_record_format, data) + # remove deleted records, if succeded abort spliting + if self._update_if_has_deleted(leaf_start, + records_to_rewrite, + old_leaf_size, + [new_key, new_doc_id, new_start, new_size, new_status]): + return None + key_moved_to_parent_node = records_to_rewrite[ + -(new_leaf_size - 1) * 5] + if key_moved_to_parent_node > new_key: + key_moved_to_parent_node = new_key + self.buckets.seek(0, 2) # end of file + new_leaf_start = self.buckets.tell() + # prepare new leaf data + index_of_records_split = nr_of_records_to_rewrite * 5 + if index_of_records_split: + records_before = records_to_rewrite[ + :-index_of_records_split] + records_after = records_to_rewrite[ + -index_of_records_split:] + else: + records_before = records_to_rewrite + records_after = [] + new_leaf = struct.pack('<' + self.elements_counter_format + 2 * self.pointer_format + + self.single_leaf_record_format * (new_leaf_size - + nr_of_records_to_rewrite - 1), + new_leaf_size, + leaf_start, + next_l, + *records_before) + new_leaf += struct.pack( + '<' + self.single_leaf_record_format * + (nr_of_records_to_rewrite + 1), + new_key, + new_doc_id, + new_start, + new_size, + 'o', + *records_after) + new_leaf += blanks + self.buckets.write(new_leaf) + self._update_leaf_size_and_pointers(leaf_start, + old_leaf_size, + prev_l, + new_leaf_start) + if next_l: # pren next_l is 0 there is no next leaf to update, avoids writing data at 0 position of file + self._update_leaf_prev_pointer( + next_l, new_leaf_start) + +# self._read_single_leaf_record.delete(leaf_start) + self._find_key_in_leaf.delete(leaf_start) + + return new_leaf_start, key_moved_to_parent_node + + def _update_if_has_deleted(self, leaf_start, records_to_rewrite, start_position, new_record_data): + """ + Checks if there are any deleted elements in data to rewrite and prevent from writing then back. + """ + curr_index = 0 + nr_of_elements = self.node_capacity + records_to_rewrite = list(records_to_rewrite) + for status in records_to_rewrite[4::5]: # remove deleted from list + if status != 'o': + del records_to_rewrite[curr_index * 5:curr_index * 5 + 5] + nr_of_elements -= 1 + else: + curr_index += 1 + # if were deleted dont have to split, just update leaf + if nr_of_elements < self.node_capacity: + data_split_index = 0 + for key in records_to_rewrite[0::5]: + if key > new_record_data[0]: + break + else: + data_split_index += 1 + records_to_rewrite = records_to_rewrite[:data_split_index * 5]\ + + new_record_data\ + + records_to_rewrite[data_split_index * 5:] + self._update_leaf_ready_data(leaf_start, + start_position, + nr_of_elements + 1, + records_to_rewrite), + return True + else: # did not found any deleted records in leaf + return False + + def _prepare_new_root_data(self, root_key, left_pointer, right_pointer, children_flag='n'): + new_root = struct.pack( + '<' + self.node_heading_format + self.single_node_record_format, + 1, + children_flag, + left_pointer, + root_key, + right_pointer) + new_root += (self.key_size + self.pointer_size) * (self. + node_capacity - 1) * '\x00' + return new_root + + def _create_new_root_from_node(self, node_start, children_flag, nr_of_keys_to_rewrite, new_node_size, old_node_size, new_key, new_pointer): + # reading second half of node + self.buckets.seek(self.data_start + self.node_heading_size) + # read all keys with key>new_key + data = self.buckets.read(self.pointer_size + self. + node_capacity * (self.key_size + self.pointer_size)) + old_node_data = struct.unpack('<' + self.pointer_format + self.node_capacity * + (self.key_format + self.pointer_format), data) + self.buckets.seek(0, 2) # end of file + new_node_start = self.buckets.tell() + if nr_of_keys_to_rewrite == new_node_size: + key_moved_to_root = new_key + # prepare new nodes data + left_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + old_node_size * (self. + key_format + self.pointer_format), + old_node_size, + children_flag, + *old_node_data[:old_node_size * 2 + 1]) + + right_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + new_node_size * (self. + key_format + self.pointer_format), + new_node_size, + children_flag, + new_pointer, + *old_node_data[old_node_size * 2 + 1:]) + elif nr_of_keys_to_rewrite > new_node_size: + key_moved_to_root = old_node_data[old_node_size * 2 - 1] + # prepare new nodes data + if nr_of_keys_to_rewrite == self.node_capacity: + keys_before = old_node_data[:1] + keys_after = old_node_data[1:old_node_size * 2 - 1] + else: + keys_before = old_node_data[:-nr_of_keys_to_rewrite * 2] + keys_after = old_node_data[-( + nr_of_keys_to_rewrite) * 2:old_node_size * 2 - 1] + left_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + (self.node_capacity - nr_of_keys_to_rewrite) * (self. + key_format + self.pointer_format), + old_node_size, + children_flag, + *keys_before) + left_node += struct.pack( + '<' + (self.key_format + self.pointer_format) * + (nr_of_keys_to_rewrite - new_node_size), + new_key, + new_pointer, + *keys_after) + + right_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + new_node_size * (self. + key_format + self.pointer_format), + new_node_size, + children_flag, + *old_node_data[old_node_size * 2:]) + else: +# 'inserting key into second half of node and creating new root' + key_moved_to_root = old_node_data[old_node_size * 2 + 1] + # prepare new nodes data + left_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + old_node_size * (self. + key_format + self.pointer_format), + old_node_size, + children_flag, + *old_node_data[:old_node_size * 2 + 1]) + if nr_of_keys_to_rewrite: + keys_before = old_node_data[(old_node_size + + 1) * 2:-nr_of_keys_to_rewrite * 2] + keys_after = old_node_data[-nr_of_keys_to_rewrite * 2:] + else: + keys_before = old_node_data[(old_node_size + 1) * 2:] + keys_after = [] + right_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + (new_node_size - nr_of_keys_to_rewrite - 1) * (self. + key_format + self.pointer_format), + new_node_size, + children_flag, + *keys_before) + right_node += struct.pack( + '<' + (nr_of_keys_to_rewrite + 1) * + (self.key_format + self.pointer_format), + new_key, + new_pointer, + *keys_after) + new_root = self._prepare_new_root_data(key_moved_to_root, + new_node_start, + new_node_start + self.node_size) + left_node += (self.node_capacity - old_node_size) * \ + (self.key_size + self.pointer_size) * '\x00' + # adding blanks after new node + right_node += (self.node_capacity - new_node_size) * \ + (self.key_size + self.pointer_size) * '\x00' + self.buckets.seek(0, 2) + self.buckets.write(left_node + right_node) + self.buckets.seek(self.data_start) + self.buckets.write(new_root) + + self._read_single_node_key.delete(node_start) + self._read_node_nr_of_elements_and_children_flag.delete(node_start) + return None + + def _split_node(self, node_start, nr_of_keys_to_rewrite, new_key, new_pointer, children_flag, create_new_root=False): + """ + Splits full node in two separate ones, first half of records stays on old position, + second half is written as new leaf at the end of file. + """ + half_size = self.node_capacity / 2 + if self.node_capacity % 2 == 0: + old_node_size = new_node_size = half_size + else: + old_node_size = half_size + new_node_size = half_size + 1 + if create_new_root: + self._create_new_root_from_node(node_start, children_flag, nr_of_keys_to_rewrite, new_node_size, old_node_size, new_key, new_pointer) + else: + blanks = (self.node_capacity - new_node_size) * ( + self.key_size + self.pointer_size) * '\x00' + if nr_of_keys_to_rewrite == new_node_size: # insert key into first half of node + # reading second half of node + self.buckets.seek(self._calculate_key_position(node_start, + old_node_size, + 'n') + self.pointer_size) + # read all keys with key>new_key + data = self.buckets.read(nr_of_keys_to_rewrite * + (self.key_size + self.pointer_size)) + old_node_data = struct.unpack('<' + nr_of_keys_to_rewrite * + (self.key_format + self.pointer_format), data) + # write new node at end of file + self.buckets.seek(0, 2) + new_node_start = self.buckets.tell() + # prepare new node_data + new_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + (self.key_format + + self.pointer_format) * new_node_size, + new_node_size, + children_flag, + new_pointer, + *old_node_data) + new_node += blanks + # write new node + self.buckets.write(new_node) + # update old node data + self._update_size( + node_start, old_node_size) + + self._read_single_node_key.delete(node_start) + self._read_node_nr_of_elements_and_children_flag.delete( + node_start) + + return new_node_start, new_key + elif nr_of_keys_to_rewrite > half_size: # insert key into first half of node + # seek for first key to rewrite + self.buckets.seek(self._calculate_key_position(node_start, self.node_capacity - nr_of_keys_to_rewrite, 'n') + + self.pointer_size) + # read all keys with key>new_key + data = self.buckets.read( + nr_of_keys_to_rewrite * (self.key_size + self.pointer_size)) + old_node_data = struct.unpack( + '<' + nr_of_keys_to_rewrite * (self.key_format + self.pointer_format), data) + key_moved_to_parent_node = old_node_data[-( + new_node_size + 1) * 2] + self.buckets.seek(0, 2) + new_node_start = self.buckets.tell() + # prepare new node_data + new_node = struct.pack('<' + self.node_heading_format + + self.pointer_format + (self.key_format + + self.pointer_format) * new_node_size, + new_node_size, + children_flag, + old_node_data[-new_node_size * 2 - 1], + *old_node_data[-new_node_size * 2:]) + new_node += blanks + # write new node + self.buckets.write(new_node) + self._update_size( + node_start, old_node_size) + # seek position of new key in first half + self.buckets.seek(self._calculate_key_position(node_start, self.node_capacity - nr_of_keys_to_rewrite, 'n') + + self.pointer_size) + # write new key and keys after + self.buckets.write( + struct.pack( + '<' + (self.key_format + self.pointer_format) * + (nr_of_keys_to_rewrite - new_node_size), + new_key, + new_pointer, + *old_node_data[:-(new_node_size + 1) * 2])) + + self._read_single_node_key.delete(node_start) + self._read_node_nr_of_elements_and_children_flag.delete( + node_start) + + return new_node_start, key_moved_to_parent_node + else: # key goes into second half + # reading second half of node + self.buckets.seek(self._calculate_key_position(node_start, + old_node_size, + 'n') + + self.pointer_size) + data = self.buckets.read( + new_node_size * (self.key_size + self.pointer_size)) + old_node_data = struct.unpack('<' + new_node_size * + (self.key_format + self.pointer_format), data) + # find key which goes to parent node + key_moved_to_parent_node = old_node_data[0] + self.buckets.seek(0, 2) # end of file + new_node_start = self.buckets.tell() + index_of_records_split = nr_of_keys_to_rewrite * 2 + # prepare new node_data + first_leaf_pointer = old_node_data[1] + old_node_data = old_node_data[2:] + if index_of_records_split: + keys_before = old_node_data[:-index_of_records_split] + keys_after = old_node_data[-index_of_records_split:] + else: + keys_before = old_node_data + keys_after = [] + new_node = struct.pack('<' + self.node_heading_format + self.pointer_format + + (self.key_format + self.pointer_format) * + (new_node_size - + nr_of_keys_to_rewrite - 1), + new_node_size, + children_flag, + first_leaf_pointer, + *keys_before) + new_node += struct.pack('<' + (self.key_format + self.pointer_format) * + (nr_of_keys_to_rewrite + 1), + new_key, + new_pointer, + *keys_after) + new_node += blanks + # write new node + self.buckets.write(new_node) + self._update_size(node_start, old_node_size) + + self._read_single_node_key.delete(node_start) + self._read_node_nr_of_elements_and_children_flag.delete( + node_start) + + return new_node_start, key_moved_to_parent_node + + def insert_first_record_into_leaf(self, leaf_start, key, doc_id, start, size, status): + self.buckets.seek(leaf_start) + self.buckets.write(struct.pack('<' + self.elements_counter_format, + 1)) + self.buckets.seek(leaf_start + self.leaf_heading_size) + self.buckets.write(struct.pack('<' + self.single_leaf_record_format, + key, + doc_id, + start, + size, + status)) + +# self._read_single_leaf_record.delete(leaf_start) + self._find_key_in_leaf.delete(leaf_start) + self._read_leaf_nr_of_elements.delete(leaf_start) + self._read_leaf_nr_of_elements_and_neighbours.delete(leaf_start) + + def _insert_new_record_into_leaf(self, leaf_start, key, doc_id, start, size, status, nodes_stack, indexes): + nr_of_elements = self._read_leaf_nr_of_elements(leaf_start) + if nr_of_elements == 0: + self.insert_first_record_into_leaf( + leaf_start, key, doc_id, start, size, status) + return + leaf_start, new_record_position, nr_of_records_to_rewrite, full_leaf, on_deleted\ + = self._find_place_in_leaf(key, leaf_start, nr_of_elements) + if full_leaf: + try: # check if leaf has parent node + leaf_parent_pointer = nodes_stack.pop() + except IndexError: # leaf is a root + leaf_parent_pointer = 0 + split_data = self._split_leaf(leaf_start, + nr_of_records_to_rewrite, + key, + doc_id, + start, + size, + status, + create_new_root=(False if leaf_parent_pointer else True)) + if split_data is not None: # means that split created new root or replaced split with update_if_has_deleted + new_leaf_start_position, key_moved_to_parent_node = split_data + self._insert_new_key_into_node(leaf_parent_pointer, + key_moved_to_parent_node, + leaf_start, + new_leaf_start_position, + nodes_stack, + indexes) + else: # there is a place for record in leaf + self.buckets.seek(leaf_start) + self._update_leaf( + leaf_start, new_record_position, nr_of_elements, nr_of_records_to_rewrite, + on_deleted, key, doc_id, start, size, status) + + def _update_node(self, new_key_position, nr_of_keys_to_rewrite, new_key, new_pointer): + if nr_of_keys_to_rewrite == 0: + self.buckets.seek(new_key_position) + self.buckets.write( + struct.pack('<' + self.key_format + self.pointer_format, + new_key, + new_pointer)) + self.flush() + else: + self.buckets.seek(new_key_position) + data = self.buckets.read(nr_of_keys_to_rewrite * ( + self.key_size + self.pointer_size)) + keys_to_rewrite = struct.unpack( + '<' + nr_of_keys_to_rewrite * (self.key_format + self.pointer_format), data) + self.buckets.seek(new_key_position) + self.buckets.write( + struct.pack( + '<' + (nr_of_keys_to_rewrite + 1) * + (self.key_format + self.pointer_format), + new_key, + new_pointer, + *keys_to_rewrite)) + self.flush() + + def _insert_new_key_into_node(self, node_start, new_key, old_half_start, new_half_start, nodes_stack, indexes): + parent_key_index = indexes.pop() + nr_of_elements, children_flag = self._read_node_nr_of_elements_and_children_flag(node_start) + parent_prev_pointer = self._read_single_node_key( + node_start, parent_key_index)[0] + if parent_prev_pointer == old_half_start: # splited child was on the left side of his parent key, must write new key before it + new_key_position = self.pointer_size + self._calculate_key_position(node_start, parent_key_index, 'n') + nr_of_keys_to_rewrite = nr_of_elements - parent_key_index + else: # splited child was on the right side of his parent key, must write new key after it + new_key_position = self.pointer_size + self._calculate_key_position(node_start, parent_key_index + 1, 'n') + nr_of_keys_to_rewrite = nr_of_elements - (parent_key_index + 1) + if nr_of_elements == self.node_capacity: + try: # check if node has parent + node_parent_pointer = nodes_stack.pop() + except IndexError: # node is a root + node_parent_pointer = 0 + new_data = self._split_node(node_start, + nr_of_keys_to_rewrite, + new_key, + new_half_start, + children_flag, + create_new_root=(False if node_parent_pointer else True)) + if new_data: # if not new_data, new root has been created + new_node_start_position, key_moved_to_parent_node = new_data + self._insert_new_key_into_node(node_parent_pointer, + key_moved_to_parent_node, + node_start, + new_node_start_position, + nodes_stack, + indexes) + + self._find_first_key_occurence_in_node.delete(node_start) + self._find_last_key_occurence_in_node.delete(node_start) + else: # there is a empty slot for new key in node + self._update_size(node_start, nr_of_elements + 1) + self._update_node(new_key_position, + nr_of_keys_to_rewrite, + new_key, + new_half_start) + + self._find_first_key_occurence_in_node.delete(node_start) + self._find_last_key_occurence_in_node.delete(node_start) + self._read_single_node_key.delete(node_start) + self._read_node_nr_of_elements_and_children_flag.delete(node_start) + + def _find_leaf_to_insert(self, key): + """ + Traverses tree in search for leaf for insert, remembering parent nodes in path, + looks for last occurence of key if already in tree. + """ + nodes_stack = [self.data_start] + if self.root_flag == 'l': + return nodes_stack, [] + else: + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(self.data_start) + curr_index, curr_pointer = self._find_last_key_occurence_in_node( + self.data_start, key, nr_of_elements) + nodes_stack.append(curr_pointer) + indexes = [curr_index] + while(curr_child_flag == 'n'): + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(curr_pointer) + curr_index, curr_pointer = self._find_last_key_occurence_in_node(curr_pointer, key, nr_of_elements) + nodes_stack.append(curr_pointer) + indexes.append(curr_index) + return nodes_stack, indexes + # nodes stack contains start addreses of nodes directly above leaf with key, indexes match keys adjacent nodes_stack values (as pointers) + # required when inserting new keys in upper tree levels + + def _find_leaf_with_last_key_occurence(self, key): + if self.root_flag == 'l': + return self.data_start + else: + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(self.data_start) + curr_position = self._find_last_key_occurence_in_node( + self.data_start, key, nr_of_elements)[1] + while(curr_child_flag == 'n'): + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(curr_position) + curr_position = self._find_last_key_occurence_in_node( + curr_position, key, nr_of_elements)[1] + return curr_position + + def _find_leaf_with_first_key_occurence(self, key): + if self.root_flag == 'l': + return self.data_start + else: + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(self.data_start) + curr_position = self._find_first_key_occurence_in_node( + self.data_start, key, nr_of_elements)[1] + while(curr_child_flag == 'n'): + nr_of_elements, curr_child_flag = self._read_node_nr_of_elements_and_children_flag(curr_position) + curr_position = self._find_first_key_occurence_in_node( + curr_position, key, nr_of_elements)[1] + return curr_position + + def _find_key(self, key): + containing_leaf_start = self._find_leaf_with_first_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(containing_leaf_start) + try: + doc_id, l_key, start, size, status = self._find_key_in_leaf( + containing_leaf_start, key, nr_of_elements) + except ElemNotFound: + if next_leaf: + nr_of_elements = self._read_leaf_nr_of_elements(next_leaf) + else: + raise ElemNotFound + doc_id, l_key, start, size, status = self._find_key_in_leaf( + next_leaf, key, nr_of_elements) + return doc_id, l_key, start, size, status + + def _find_key_to_update(self, key, doc_id): + """ + Search tree for key that matches not only given key but also doc_id. + """ + containing_leaf_start = self._find_leaf_with_first_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(containing_leaf_start) + try: + leaf_start, record_index, doc_id, l_key, start, size, status = self._find_key_in_leaf_for_update(key, + doc_id, + containing_leaf_start, + nr_of_elements) + except ElemNotFound: + if next_leaf: + nr_of_elements = self._read_leaf_nr_of_elements(next_leaf) + else: + raise TryReindexException() + try: + leaf_start, record_index, doc_id, l_key, start, size, status = self._find_key_in_leaf_for_update(key, + doc_id, + next_leaf, + nr_of_elements) + except ElemNotFound: + raise TryReindexException() + return leaf_start, record_index, doc_id, l_key, start, size, status + + def update(self, doc_id, key, u_start=0, u_size=0, u_status='o'): + containing_leaf_start, element_index, old_doc_id, old_key, old_start, old_size, old_status = self._find_key_to_update(key, doc_id) + new_data = (old_doc_id, old_start, old_size, old_status) + if not u_start: + new_data[1] = u_start + if not u_size: + new_data[2] = u_size + if not u_status: + new_data[3] = u_status + self._update_element(containing_leaf_start, element_index, new_data) + + self._find_key.delete(key) + self._match_doc_id.delete(doc_id) + self._find_key_in_leaf.delete(containing_leaf_start, key) + return True + + def delete(self, doc_id, key, start=0, size=0): + containing_leaf_start, element_index = self._find_key_to_update( + key, doc_id)[:2] + self._delete_element(containing_leaf_start, element_index) + + self._find_key.delete(key) + self._match_doc_id.delete(doc_id) + self._find_key_in_leaf.delete(containing_leaf_start, key) + return True + + def _find_key_many(self, key, limit=1, offset=0): + leaf_with_key = self._find_leaf_with_first_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + try: + leaf_with_key, key_index = self._find_index_of_first_key_equal( + key, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + except ElemNotFound: + leaf_with_key = next_leaf + key_index = 0 + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + while offset: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if key == curr_key: + if status != 'd': + offset -= 1 + key_index += 1 + else: + return + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + while limit: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if key == curr_key: + if status != 'd': + yield doc_id, start, size, status + limit -= 1 + key_index += 1 + else: + return + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + + def _find_key_smaller(self, key, limit=1, offset=0): + leaf_with_key = self._find_leaf_with_first_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + leaf_with_key, key_index = self._find_index_of_first_key_equal_or_smaller_key(key, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + curr_key = self._read_single_leaf_record(leaf_with_key, key_index)[0] + if curr_key >= key: + key_index -= 1 + while offset: + if key_index >= 0: + key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + offset -= 1 + key_index -= 1 + else: + if prev_leaf: + leaf_with_key = prev_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(prev_leaf) + key_index = nr_of_elements - 1 + else: + return + while limit: + if key_index >= 0: + key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + yield doc_id, key, start, size, status + limit -= 1 + key_index -= 1 + else: + if prev_leaf: + leaf_with_key = prev_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(prev_leaf) + key_index = nr_of_elements - 1 + else: + return + + def _find_key_equal_and_smaller(self, key, limit=1, offset=0): + leaf_with_key = self._find_leaf_with_last_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + try: + leaf_with_key, key_index = self._find_index_of_last_key_equal_or_smaller_key(key, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + except ElemNotFound: + leaf_with_key = prev_leaf + key_index = self._read_leaf_nr_of_elements_and_neighbours( + leaf_with_key)[0] + curr_key = self._read_single_leaf_record(leaf_with_key, key_index)[0] + if curr_key > key: + key_index -= 1 + while offset: + if key_index >= 0: + key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + offset -= 1 + key_index -= 1 + else: + if prev_leaf: + leaf_with_key = prev_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(prev_leaf) + key_index = nr_of_elements - 1 + else: + return + while limit: + if key_index >= 0: + key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + yield doc_id, key, start, size, status + limit -= 1 + key_index -= 1 + else: + if prev_leaf: + leaf_with_key = prev_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(prev_leaf) + key_index = nr_of_elements - 1 + else: + return + + def _find_key_bigger(self, key, limit=1, offset=0): + leaf_with_key = self._find_leaf_with_last_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + try: + leaf_with_key, key_index = self._find_index_of_last_key_equal_or_smaller_key(key, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + except ElemNotFound: + key_index = 0 + curr_key = self._read_single_leaf_record(leaf_with_key, key_index)[0] + if curr_key <= key: + key_index += 1 + while offset: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + offset -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + while limit: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + yield doc_id, curr_key, start, size, status + limit -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + + def _find_key_equal_and_bigger(self, key, limit=1, offset=0): + leaf_with_key = self._find_leaf_with_first_key_occurence(key) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + leaf_with_key, key_index = self._find_index_of_first_key_equal_or_smaller_key(key, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + curr_key = self._read_single_leaf_record(leaf_with_key, key_index)[0] + if curr_key < key: + key_index += 1 + while offset: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + offset -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + while limit: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_with_key, key_index) + if status != 'd': + yield doc_id, curr_key, start, size, status + limit -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + + def _find_key_between(self, start, end, limit, offset, inclusive_start, inclusive_end): + """ + Returns generator containing all keys withing given interval. + """ + if inclusive_start: + leaf_with_key = self._find_leaf_with_first_key_occurence(start) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + leaf_with_key, key_index = self._find_index_of_first_key_equal_or_smaller_key(start, leaf_with_key, nr_of_elements) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + curr_key = self._read_single_leaf_record( + leaf_with_key, key_index)[0] + if curr_key < start: + key_index += 1 + else: + leaf_with_key = self._find_leaf_with_last_key_occurence(start) + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_with_key) + leaf_with_key, key_index = self._find_index_of_last_key_equal_or_smaller_key(start, leaf_with_key, nr_of_elements) + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_with_key, key_index) + if curr_key <= start: + key_index += 1 + while offset: + if key_index < nr_of_elements: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_with_key, key_index) + if curr_status != 'd': + offset -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + while limit: + if key_index < nr_of_elements: + curr_key, curr_doc_id, curr_start, curr_size, curr_status = self._read_single_leaf_record(leaf_with_key, key_index) + if curr_key > end or (curr_key == end and not inclusive_end): + return + elif curr_status != 'd': + yield curr_doc_id, curr_key, curr_start, curr_size, curr_status + limit -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_with_key = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + + def get(self, key): + return self._find_key(self.make_key(key)) + + def get_many(self, key, limit=1, offset=0): + return self._find_key_many(self.make_key(key), limit, offset) + + def get_between(self, start, end, limit=1, offset=0, inclusive_start=True, inclusive_end=True): + if start is None: + end = self.make_key(end) + if inclusive_end: + return self._find_key_equal_and_smaller(end, limit, offset) + else: + return self._find_key_smaller(end, limit, offset) + elif end is None: + start = self.make_key(start) + if inclusive_start: + return self._find_key_equal_and_bigger(start, limit, offset) + else: + return self._find_key_bigger(start, limit, offset) + else: + start = self.make_key(start) + end = self.make_key(end) + return self._find_key_between(start, end, limit, offset, inclusive_start, inclusive_end) + + def all(self, limit=-1, offset=0): + """ + Traverses linked list of all tree leaves and returns generator containing all elements stored in index. + """ + if self.root_flag == 'n': + leaf_start = self.data_start + self.node_size + else: + leaf_start = self.data_start + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(leaf_start) + key_index = 0 + while offset: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_start, key_index) + if status != 'd': + offset -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_start = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + while limit: + if key_index < nr_of_elements: + curr_key, doc_id, start, size, status = self._read_single_leaf_record( + leaf_start, key_index) + if status != 'd': + yield doc_id, curr_key, start, size, status + limit -= 1 + key_index += 1 + else: + key_index = 0 + if next_leaf: + leaf_start = next_leaf + nr_of_elements, prev_leaf, next_leaf = self._read_leaf_nr_of_elements_and_neighbours(next_leaf) + else: + return + + def make_key(self, key): + raise NotImplementedError() + + def make_key_value(self, data): + raise NotImplementedError() + + def _open_storage(self): + s = globals()[self.storage_class] + if not self.storage: + self.storage = s(self.db_path, self.name) + self.storage.open() + + def _create_storage(self): + s = globals()[self.storage_class] + if not self.storage: + self.storage = s(self.db_path, self.name) + self.storage.create() + + def compact(self, node_capacity=0): + if not node_capacity: + node_capacity = self.node_capacity + + compact_ind = self.__class__( + self.db_path, self.name + '_compact', node_capacity=node_capacity) + compact_ind.create_index() + + gen = self.all() + while True: + try: + doc_id, key, start, size, status = gen.next() + except StopIteration: + break + self.storage._f.seek(start) + value = self.storage._f.read(size) + start_ = compact_ind.storage._f.tell() + compact_ind.storage._f.write(value) + compact_ind.insert(doc_id, key, start_, size, status) + + compact_ind.close_index() + original_name = self.name + # os.unlink(os.path.join(self.db_path, self.name + "_buck")) + self.close_index() + shutil.move(os.path.join(compact_ind.db_path, compact_ind. + name + "_buck"), os.path.join(self.db_path, self.name + "_buck")) + shutil.move(os.path.join(compact_ind.db_path, compact_ind. + name + "_stor"), os.path.join(self.db_path, self.name + "_stor")) + # self.name = original_name + self.open_index() # reload... + self.name = original_name + self._save_params(dict(name=original_name)) + self._fix_params() + self._clear_cache() + return True + + def _fix_params(self): + super(IU_TreeBasedIndex, self)._fix_params() + self._count_props() + + def _clear_cache(self): + self._find_key.clear() + self._match_doc_id.clear() +# self._read_single_leaf_record.clear() + self._find_key_in_leaf.clear() + self._read_single_node_key.clear() + self._find_first_key_occurence_in_node.clear() + self._find_last_key_occurence_in_node.clear() + self._read_leaf_nr_of_elements.clear() + self._read_leaf_neighbours.clear() + self._read_leaf_nr_of_elements_and_neighbours.clear() + self._read_node_nr_of_elements_and_children_flag.clear() + + def close_index(self): + super(IU_TreeBasedIndex, self).close_index() + self._clear_cache() + + +class IU_MultiTreeBasedIndex(IU_TreeBasedIndex): + """ + Class that allows to index more than one key per database record. + + It operates very well on GET/INSERT. It's not optimized for + UPDATE operations (will always readd everything) + """ + + def __init__(self, *args, **kwargs): + super(IU_MultiTreeBasedIndex, self).__init__(*args, **kwargs) + + def insert(self, doc_id, key, start, size, status='o'): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + ins = super(IU_MultiTreeBasedIndex, self).insert + for curr_key in key: + ins(doc_id, curr_key, start, size, status) + return True + + def update(self, doc_id, key, u_start, u_size, u_status='o'): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + upd = super(IU_MultiTreeBasedIndex, self).update + for curr_key in key: + upd(doc_id, curr_key, u_start, u_size, u_status) + + def delete(self, doc_id, key, start=0, size=0): + if isinstance(key, (list, tuple)): + key = set(key) + elif not isinstance(key, set): + key = set([key]) + delete = super(IU_MultiTreeBasedIndex, self).delete + for curr_key in key: + delete(doc_id, curr_key, start, size) + + def get(self, key): + return super(IU_MultiTreeBasedIndex, self).get(key) + + def make_key_value(self, data): + raise NotImplementedError() + + +# classes for public use, done in this way because of +# generation static files with indexes (_index directory) + + +class TreeBasedIndex(IU_TreeBasedIndex): + pass + + +class MultiTreeBasedIndex(IU_MultiTreeBasedIndex): + """ + It allows to index more than one key for record. (ie. prefix/infix/suffix search mechanizms) + That class is designed to be used in custom indexes. + """ + pass From 63743dd2b688e011140250440c84d4221555687e Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 31 Jan 2014 00:38:37 +0100 Subject: [PATCH 006/468] More NoSQL --- couchpotato/core/media/_base/media/index.py | 36 +++++-- couchpotato/core/media/_base/media/main.py | 17 +++- couchpotato/core/notifications/core/main.py | 6 +- couchpotato/core/plugins/category/index.py | 16 +++ couchpotato/core/plugins/category/main.py | 63 +++++++----- couchpotato/core/plugins/nosql/index.py | 15 ++- couchpotato/core/plugins/nosql/main.py | 20 +++- couchpotato/core/plugins/profile/index.py | 16 +++ couchpotato/core/plugins/profile/main.py | 65 +++++++------ couchpotato/core/plugins/release/index.py | 42 ++++++++ couchpotato/core/plugins/release/main.py | 102 +++++++++++--------- couchpotato/core/plugins/renamer/main.py | 79 +++++++-------- 12 files changed, 302 insertions(+), 175 deletions(-) create mode 100644 couchpotato/core/plugins/category/index.py create mode 100644 couchpotato/core/plugins/profile/index.py create mode 100644 couchpotato/core/plugins/release/index.py diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index bd90fcf4..e930dd3c 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -1,5 +1,7 @@ +from itertools import izip from CodernityDB.hash_index import HashIndex -from CodernityDB.tree_index import MultiTreeBasedIndex +from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex +from hashlib import md5 class MediaIMDBIndex(HashIndex): @@ -15,19 +17,27 @@ class MediaIMDBIndex(HashIndex): if data.get('type') == 'media' and data.get('identifier'): return int(data['identifier'].strip('t')), None + def run_with_status(self, db, status = []): -class MediaStatusIndex(HashIndex): + status = list(status if isinstance(status, (list, tuple)) else [status]) + + for s in status: + for ms in db.get_many('media_status', s, with_doc = True): + yield ms['doc'] + + +class MediaStatusIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = 's' + kwargs['key_format'] = '16s' super(MediaStatusIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return int(key.strip('t')) + return md5(key).digest() def make_key_value(self, data): - if data.get('type') == 'media' and data.get('identifier'): - return int(data['identifier'].strip('t')), None + if data.get('type') == 'media' and data.get('status'): + return md5(data.get('status')).digest(), None class TitleIndex(MultiTreeBasedIndex): @@ -58,3 +68,17 @@ from itertools import izip""" def make_key(self, key): return key.rjust(32, '_').lower() + + +class YearIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = 'i' + super(YearIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return key + + def make_key_value(self, data): + if data.get('type') == 'media' and data.get('year') is not None: + return data['year'], None diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 9a50ffe8..2b56ac79 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -6,7 +6,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIMDBIndex, TitleIndex, MediaStatusIndex +from .index import MediaIMDBIndex, TitleIndex, MediaStatusIndex, YearIndex from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ Media from sqlalchemy.orm import joinedload_all @@ -77,23 +77,32 @@ class MediaPlugin(MediaBase): db = get_db() + # IMDB index try: db.add_index(MediaIMDBIndex(db.path, 'media')) except: log.debug('Index already exists') - db.update_index(MediaIMDBIndex(db.path, 'media')) + db.edit_index(MediaIMDBIndex(db.path, 'media')) + # Title index try: db.add_index(TitleIndex(db.path, 'media_title')) except: log.debug('Index already exists') - db.update_index(TitleIndex(db.path, 'media_title')) + db.edit_index(TitleIndex(db.path, 'media_title')) + # Status index try: db.add_index(MediaStatusIndex(db.path, 'media_status')) except: log.debug('Index already exists') - db.update_index(MediaStatusIndex(db.path, 'media_status')) + db.edit_index(MediaStatusIndex(db.path, 'media_status')) + + # Year index + try: db.add_index(YearIndex(db.path, 'year')) + except: + log.debug('Index already exists') + db.edit_index(YearIndex(db.path, 'year')) def refresh(self, id = '', **kwargs): handlers = [] diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 4a245cdd..2eb5a302 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -86,7 +86,7 @@ class CoreNotifier(Notification): try: db = get_db() for n in db.all('notification', with_doc = True): - if n['doc']['added'] <= (int(time.time()) - 2419200): + if n['doc'].get('time', 0) <= (int(time.time()) - 2419200): db.delete(n['doc']) except: log.error('Failed cleaning notification: %s', traceback.format_exc()) @@ -162,7 +162,7 @@ class CoreNotifier(Notification): n = { 'type': 'notification', - 'time': time.time(), + 'time': int(time.time()), 'message': toUnicode(message), 'data': data } @@ -276,7 +276,7 @@ class CoreNotifier(Notification): notifications = db.all('notification_unread', with_doc = True) for n in notifications: - if n['doc'].get('added') > (time.time() - 259200): + if n['doc'].get('time') > (time.time() - 259200): messages.append(n['doc']) return { diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py new file mode 100644 index 00000000..fa579dd9 --- /dev/null +++ b/couchpotato/core/plugins/category/index.py @@ -0,0 +1,16 @@ +from hashlib import md5 +from CodernityDB.tree_index import TreeBasedIndex + + +class CategoryIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(CategoryIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'category': + return md5(data['media_id']).digest(), None diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index b26a8293..52161c6a 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -1,10 +1,11 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin +from .index import CategoryIndex from couchpotato.core.settings.model import Media, Category log = CPLog(__name__) @@ -13,8 +14,6 @@ log = CPLog(__name__) class CategoryPlugin(Plugin): def __init__(self): - addEvent('category.all', self.all) - addApiView('category.save', self.save) addApiView('category.save_order', self.saveOrder) addApiView('category.delete', self.delete) @@ -26,6 +25,20 @@ class CategoryPlugin(Plugin): }"""} }) + addEvent('category.all', self.all) + addEvent('database.setup', self.databaseSetup) + + def databaseSetup(self): + + db = get_db() + + # Release media_id index + try: + db.add_index(CategoryIndex(db.path, 'category')) + except: + log.debug('Index already exists') + db.edit_index(CategoryIndex(db.path, 'category')) + def allView(self, **kwargs): return { @@ -35,47 +48,45 @@ class CategoryPlugin(Plugin): def all(self): - db = get_session() - categories = db.query(Category).all() + db = get_db() + categories = db.all('category', with_doc = True) temp = [] for category in categories: - temp.append(category.to_dict()) + temp.append(category['doc']) - pass #db.close() return temp def save(self, **kwargs): try: - db = get_session() + db = get_db() - c = db.query(Category).filter_by(id = kwargs.get('id')).first() - if not c: - c = Category() - db.add(c) + category = { + 'order': kwargs.get('order', 0), + 'label': toUnicode(kwargs.get('label', '')), + 'ignored': toUnicode(kwargs.get('ignored', '')), + 'preferred': toUnicode(kwargs.get('preferred', '')), + 'required': toUnicode(kwargs.get('required', '')), + 'destination': toUnicode(kwargs.get('destination', '')), + } - c.order = kwargs.get('order', c.order if c.order else 0) - c.label = toUnicode(kwargs.get('label', '')) - c.ignored = toUnicode(kwargs.get('ignored', '')) - c.preferred = toUnicode(kwargs.get('preferred', '')) - c.required = toUnicode(kwargs.get('required', '')) - c.destination = toUnicode(kwargs.get('destination', '')) + try: + c = db.get('id', kwargs.get('id')) + category['order'] = c.get('order', category['order']) + c.update(category) - db.commit() + db.update(c) + except: + c = db.insert(category) + c.update(category) - category_dict = c.to_dict() - - pass #db.close() return { 'success': True, - 'category': category_dict + 'category': c } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False, diff --git a/couchpotato/core/plugins/nosql/index.py b/couchpotato/core/plugins/nosql/index.py index e1361799..e6b0389e 100644 --- a/couchpotato/core/plugins/nosql/index.py +++ b/couchpotato/core/plugins/nosql/index.py @@ -1,20 +1,17 @@ from hashlib import md5 -from CodernityDB.tree_index import TreeBasedIndex +from CodernityDB.tree_index import TreeBasedIndex, MultiTreeBasedIndex -class ReleaseIndex(TreeBasedIndex): +class NameIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): kwargs['key_format'] = '16s' - super(ReleaseIndex, self).__init__(*args, **kwargs) + super(NameIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return key def make_key_value(self, data): - if data.get('type') == 'release': - return md5(data['media_id']).digest(), None + if data.get('type') == 'media' and data.get('title') is not None: + return data.get('title'), None - def run_for_media(self, db, media_id): - for release in db.get_many('release', media_id, with_doc=True): - yield release['doc'] diff --git a/couchpotato/core/plugins/nosql/main.py b/couchpotato/core/plugins/nosql/main.py index 11f15cb9..511c8f1b 100644 --- a/couchpotato/core/plugins/nosql/main.py +++ b/couchpotato/core/plugins/nosql/main.py @@ -1,7 +1,8 @@ import time -from couchpotato import CPLog +from couchpotato import CPLog, get_db +from couchpotato.core.event import addEvent from couchpotato.core.plugins.base import Plugin -from .index import ReleaseIndex, MediaIMDBIndex, TitleIndex +from .index import ReleaseIndex, NameIndex log = CPLog(__name__) @@ -10,9 +11,20 @@ class NoSQL(Plugin): db = None + def __init__(self): + + pass #addEvent('app.load2', self.test) + def test(self): - db = self.db + db = get_db() + + try: db.add_index(YearIndex(db.path, 'year')) + except: + log.debug('Index already exists') + db.edit_index(YearIndex(db.path, 'year')) + + return try: db.add_index(ReleaseIndex(db.path, 'release')) except: log.debug('Index already exists') @@ -54,8 +66,6 @@ class NoSQL(Plugin): return - return - for media in db.all('media', with_doc = True): doc = media['doc'] for r in db.run('release', 'for_media', media['_id']): diff --git a/couchpotato/core/plugins/profile/index.py b/couchpotato/core/plugins/profile/index.py new file mode 100644 index 00000000..2457699b --- /dev/null +++ b/couchpotato/core/plugins/profile/index.py @@ -0,0 +1,16 @@ +from CodernityDB.hash_index import HashIndex +from hashlib import md5 + + +class ProfileIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(ProfileIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'profile' and data.get('identifier'): + return md5(data.get('identifier')).digest(), None diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 9d721852..8939f414 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -5,6 +5,7 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin +from .index import ProfileIndex from couchpotato.core.settings.model import Profile, ProfileType, Media from sqlalchemy.orm import joinedload_all @@ -30,9 +31,22 @@ class ProfilePlugin(Plugin): }"""} }) + addEvent('database.setup', self.databaseSetup) + addEvent('app.initialize', self.fill, priority = 90) addEvent('app.load2', self.forceDefaults) + def databaseSetup(self): + + db = get_db() + + try: + db.add_index(ProfileIndex(db.path, 'profile')) + except: + log.debug('Index already exists') + db.edit_index(ProfileIndex(db.path, 'profile')) + + def forceDefaults(self): # Get all active movies without profile @@ -112,15 +126,8 @@ class ProfilePlugin(Plugin): } def default(self): - - db = get_session() - default = db.query(Profile) \ - .options(joinedload_all('types')) \ - .first() - default_dict = default.to_dict(self.to_dict) - pass #db.close() - - return default_dict + db = get_db() + return db.get_many('profile', limit = 1, with_doc = True)[0] def saveOrder(self, **kwargs): @@ -187,7 +194,7 @@ class ProfilePlugin(Plugin): def fill(self): try: - db = get_session() + db = get_db() profiles = [{ 'label': 'Best', @@ -201,38 +208,32 @@ class ProfilePlugin(Plugin): }] # Create default quality profile - order = -2 + order = 0 for profile in profiles: log.info('Creating default profile: %s', profile.get('label')) - p = Profile( - label = toUnicode(profile.get('label')), - order = order - ) - db.add(p) - quality_order = 0 - for quality in profile.get('qualities'): - quality = fireEvent('quality.single', identifier = quality, single = True) - profile_type = ProfileType( - quality_id = quality.get('id'), - profile = p, - finish = True, - wait_for = 0, - order = quality_order - ) - p.types.append(profile_type) + pro = { + 'type': 'profile', + 'identifier': profile.get('label').lower(), + 'label': toUnicode(profile.get('label')), + 'order': order, + 'qualities': profile.get('qualities'), + 'finish': [], + 'wait_for': [] + } - quality_order += 1 + for q in profile.get('qualities'): + pro['finish'].append(True) + pro['wait_for'].append(0) + db.insert(pro) order += 1 - db.commit() + for x in db.all('profile', with_doc = True): + log.info(x) return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return False diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py new file mode 100644 index 00000000..3db2cba5 --- /dev/null +++ b/couchpotato/core/plugins/release/index.py @@ -0,0 +1,42 @@ +from hashlib import md5 +from CodernityDB.tree_index import TreeBasedIndex + + +class ReleaseIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(ReleaseIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'release' and data.get('media_id'): + return md5(data['media_id']).digest(), {'media_id': data.get('media_id')} + + def run_for_media(self, db, media_id): + for release in db.get_many('release', media_id, with_doc = True): + yield release['doc'] + + def run_with_status(self, db, status = []): + + status = list(status if isinstance(status, (list, tuple)) else [status]) + + for s in status: + for ms in db.get_many('release_status', s, with_doc = True): + yield ms['doc'] + + +class ReleaseStatusIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '16s' + super(ReleaseStatusIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).digest() + + def make_key_value(self, data): + if data.get('type') == 'release' and data.get('status'): + return md5(data.get('status')).digest(), None diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 9066fa18..8faf6a5d 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -5,8 +5,9 @@ from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin +from .index import ReleaseIndex, ReleaseStatusIndex from couchpotato.core.plugins.scanner.main import Scanner -from couchpotato.core.settings.model import File, Release as Relea, Media, \ +from couchpotato.core.settings.model import Release as Relea, Media, \ ReleaseInfo from couchpotato.environment import Env from inspect import ismethod, isfunction @@ -23,8 +24,6 @@ log = CPLog(__name__) class Release(Plugin): def __init__(self): - addEvent('release.add', self.add) - addApiView('release.manual_download', self.manualDownload, docs = { 'desc': 'Send a release manually to the downloaders', 'params': { @@ -50,6 +49,7 @@ class Release(Plugin): } }) + addEvent('release.add', self.add) addEvent('release.download', self.download) addEvent('release.try_download_result', self.tryDownloadResult) addEvent('release.create_from_search', self.createFromSearch) @@ -58,43 +58,59 @@ class Release(Plugin): addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) + addEvent('database.setup', self.databaseSetup) + # Clean releases that didn't have activity in the last week - addEvent('app.load', self.cleanDone) + addEvent('app.load2', self.cleanDone) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4) - def cleanDone(self): + def databaseSetup(self): + db = get_db() + + # Release media_id index + try: + db.add_index(ReleaseIndex(db.path, 'release')) + except: + log.debug('Index already exists') + db.edit_index(ReleaseIndex(db.path, 'release')) + + # Release status index + try: + db.add_index(ReleaseStatusIndex(db.path, 'release_status')) + except: + log.debug('Index already exists') + db.edit_index(ReleaseStatusIndex(db.path, 'release_status')) + + def cleanDone(self): log.debug('Removing releases from dashboard') now = time.time() week = 262080 - done_status, available_status, snatched_status, downloaded_status, ignored_status = \ - fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True) - db = get_db() # get movies last_edit more than a week ago medias = db.run('media', 'with_status', ['done']) for media in medias: - if media['last_edit'] > (now - week): + if media.get('last_edit', 0) > (now - week): continue for rel in db.run('release', 'for_media', media['_id']): + # Remove all available releases if rel['status'] in ['available']: - fireEvent('release.delete', id = rel['_id'], single = True) + self.delete(rel['_id']) # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move elif rel['status'] in ['snatched', 'downloaded']: - self.updateStatus(id = rel['id'], status = ignored_status) - + self.updateStatus(rel['_id'], status = 'ignore') def add(self, group): try: - db = get_db() + db = get_session() identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) @@ -160,9 +176,8 @@ class Release(Plugin): try: db = get_db() - - rel = db.get('release', release_id, with_doc = True) - db.delete(rel['doc']) + rel = db.get('id', release_id) + db.delete(rel) return True except: log.error('Failed: %s', traceback.format_exc()) @@ -173,16 +188,17 @@ class Release(Plugin): try: db = get_db() + rel = db.get('id', release_id) - rel = db.get('release', release_id, with_doc = True) - files = [] - for release_file in rel['files']: - if os.path.isfile(ss(release_file['path'])): - files.append(release_file) - - if len(rel['files']) == 0: + if len(rel.get('files')) == 0: self.delete(rel['_id']) else: + + files = [] + for release_file in rel.get('files'): + if os.path.isfile(ss(release_file['path'])): + files.append(release_file) + rel['files'] = files db.update(rel) @@ -197,7 +213,7 @@ class Release(Plugin): db = get_db() try: - rel = db.get('release', release_id, with_doc = True)['doc'] + rel = db.get('id', release_id, with_doc = True) self.updateStatus(release_id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored') return { @@ -453,42 +469,34 @@ class Release(Plugin): 'success': True } - def updateStatus(self, id, status = None): + def updateStatus(self, release_id, status = None): if not status: return False try: - db = get_session() + db = get_db() - rel = db.query(Relea).filter_by(id = id).first() - if rel and status and rel.status_id != status.get('id'): + rel = db.get('id', release_id) + if rel and rel.get('status') != status: - item = {} - for info in rel.info: - item[info.identifier] = info.value - - release_name = None - if rel.files: - for file_item in rel.files: - if file_item.type.identifier == 'movie': - release_name = os.path.basename(file_item.path) + release_name = rel.get('name') + if rel.get('files'): + for file_item in rel.get('files', []): + if file_item.get('type') == 'movie': + release_name = os.path.basename(file_item.get('path')) break - else: - release_name = item['name'] #update status in Db - log.debug('Marking release %s as %s', (release_name, status.get("label"))) - rel.status_id = status.get('id') - rel.last_edit = int(time.time()) - db.commit() + log.debug('Marking release %s as %s', (release_name, status)) + rel['status'] = status + rel['last_edit'] = int(time.time()) + + db.update(rel) #Update all movie info as there is no release update function - fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict()) + fireEvent('notify.frontend', type = 'release.update_status', data = rel) return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return False diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 868dbc59..d00eee6b 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -1,4 +1,4 @@ -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss, sp @@ -6,7 +6,7 @@ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile, Release, \ +from couchpotato.core.settings.model import Library, File, Profile, \ ReleaseInfo from couchpotato.environment import Env from unrar2 import RarFile @@ -45,7 +45,7 @@ class Renamer(Plugin): addEvent('renamer.scan', self.scan) addEvent('renamer.check_snatched', self.checkSnatched) - addEvent('app.load', self.scan) + addEvent('app.load2', self.scan) addEvent('app.load', self.setCrons) # Enable / disable interval @@ -211,10 +211,6 @@ class Renamer(Plugin): nfo_name = self.conf('nfo_name') separator = self.conf('separator') - # Statuses - done_status, active_status, downloaded_status, snatched_status, seeding_status = \ - fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched', 'seeding'], single = True) - # Tag release folder as failed_rename in case no groups were found. This prevents check_snatched from removing the release from the downloader. if not groups and self.statusInfoComplete(release_download): self.tagRelease(release_download = release_download, tag = 'failed_rename') @@ -849,14 +845,10 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.checking_snatched = True - snatched_status, ignored_status, failed_status, seeding_status, downloaded_status, missing_status = \ - fireEvent('status.get', ['snatched', 'ignored', 'failed', 'seeding', 'downloaded', 'missing'], single = True) - try: - db = get_session() - rels = db.query(Release).filter( - Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')]) - ).all() + db = get_db() + + rels = list(db.get_many('release', 'with_status', ['snatched', 'seeding', 'missing'])) if not rels: #No releases found that need status checking @@ -868,13 +860,12 @@ Remove it if you want it to be renamed (again, or at least let it try again) no_status_support = [] try: for rel in rels: - rel_dict = rel.to_dict({'info': {}}) - if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'): - download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']}) + if rel['info'].get('download_id') and rel['info'].get('download_downloader'): + download_ids.append({'id': rel['info']['download_id'], 'downloader': rel['info']['download_downloader']}) - ds = rel_dict['info'].get('download_status_support') - if ds == False or ds == 'False': - no_status_support.append(ss(rel_dict['info'].get('download_downloader'))) + ds = rel['info'].get('download_status_support') + if ds is False or ds == 'False': + no_status_support.append(ss(rel['info'].get('download_downloader'))) except: log.error('Error getting download IDs from database') self.checking_snatched = False @@ -899,35 +890,34 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: for rel in rels: - rel_dict = rel.to_dict({'info': {}}) - movie_dict = fireEvent('media.get', media_id = rel.movie_id, single = True) + movie_dict = db.get('id', rel.get('media_id')) - if not isinstance(rel_dict['info'], dict): + if not isinstance(rel['info'], dict): log.error('Faulty release found without any info, ignoring.') - fireEvent('release.update_status', rel.id, status = ignored_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True) continue # Check if download ID is available - if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'): - log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name'])) + if not rel['info'].get('download_id') or not rel['info'].get('download_downloader'): + log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel['info'].get('download_downloader', 'unknown'), rel['info']['name'])) scan_required = True # Continue with next release continue # Find release in downloaders - nzbname = self.createNzbName(rel_dict['info'], movie_dict) + nzbname = self.createNzbName(rel['info'], movie_dict) found_release = False for release_download in release_downloads: found_release = False - if rel_dict['info'].get('download_id'): - if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']: + if rel['info'].get('download_id'): + if release_download['id'] == rel['info']['download_id'] and release_download['downloader'] == rel['info']['download_downloader']: log.debug('Found release by id: %s', release_download['id']) found_release = True break else: - if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: + if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: log.debug('Found release by release name or imdb ID: %s', release_download['name']) found_release = True break @@ -936,12 +926,12 @@ Remove it if you want it to be renamed (again, or at least let it try again) log.info('%s not found in downloaders', nzbname) #Check status if already missing and for how long, if > 1 week, set to ignored else to missing - if rel.status_id == missing_status.get('id'): + if rel.get('status') == 'missing': if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: - fireEvent('release.update_status', rel.id, status = ignored_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True) else: # Set the release to missing - fireEvent('release.update_status', rel.id, status = missing_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'missing', single = True) # Continue with next release continue @@ -953,7 +943,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Check status of release if release_download['status'] == 'busy': # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True) # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading if self.movieInFromFolder(release_download['folder']): @@ -961,7 +951,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif release_download['status'] == 'seeding': #If linking setting is enabled, process release - if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download): + if self.conf('file_action') != 'move' and not rel.status_id == 'seeding' and self.statusInfoComplete(release_download): log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) # Remove the downloading tag @@ -975,16 +965,16 @@ Remove it if you want it to be renamed (again, or at least let it try again) log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio'])) # Set the release to seeding - fireEvent('release.update_status', rel.id, status = seeding_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'seeding', single = True) elif release_download['status'] == 'failed': # Set the release to failed - fireEvent('release.update_status', rel.id, status = failed_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'failed', single = True) fireEvent('download.remove_failed', release_download, single = True) if self.conf('next_on_failed'): - fireEvent('movie.searcher.try_next_release', media_id = rel.movie_id) + fireEvent('movie.searcher.try_next_release', media_id = rel.get('media_id')) elif release_download['status'] == 'completed': log.info('Download of %s completed!', release_download['name']) @@ -993,10 +983,10 @@ Remove it if you want it to be renamed (again, or at least let it try again) if self.statusInfoComplete(release_download): # If the release has been seeding, process now the seeding is done - if rel.status_id == seeding_status.get('id'): + if rel.get('status') == 'seeding': if self.conf('file_action') != 'move': # Set the release to done as the movie has already been renamed - fireEvent('release.update_status', rel.id, status = downloaded_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'downloaded', single = True) # Allow the downloader to clean-up release_download.update({'pause': False, 'scan': False, 'process_complete': True}) @@ -1008,7 +998,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) else: # Set the release to snatched if it was missing before - fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True) # Remove the downloading tag self.untagRelease(release_download = release_download, tag = 'downloading') @@ -1044,8 +1034,11 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.checking_snatched = False return True - finally: - pass #db.close() + except: + log.error('Failed checking snatched: %s', traceback.format_exc()) + + self.checking_snatched = False + return False def extendReleaseDownload(self, release_download): From 99252074be0643dfa8dde3dd0f4d5e94b5ebd7a1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 2 Feb 2014 20:41:14 +0100 Subject: [PATCH 007/468] More nosql --- couchpotato/core/_base/_core/main.py | 4 +- couchpotato/core/_base/clientscript/main.py | 2 +- couchpotato/core/_base/desktop/main.py | 2 +- couchpotato/core/_base/updater/main.py | 4 +- couchpotato/core/downloaders/rtorrent/main.py | 2 +- couchpotato/core/helpers/variable.py | 16 +- couchpotato/core/media/__init__.py | 11 +- .../core/media/_base/library/__init__.py | 13 -- couchpotato/core/media/_base/media/index.py | 19 +- couchpotato/core/media/_base/media/main.py | 199 +++++++++--------- couchpotato/core/media/_base/search/main.py | 2 +- couchpotato/core/media/_base/searcher/base.py | 2 +- couchpotato/core/media/_base/searcher/main.py | 2 +- couchpotato/core/media/movie/_base/main.py | 193 +++++++++++++++-- .../media/movie/_base/static/movie.actions.js | 34 ++- .../core/media/movie/_base/static/movie.js | 37 ++-- .../core/media/movie/_base/static/search.js | 4 +- .../core/media/movie/library/__init__.py | 0 .../media/movie/library/movie/__init__.py | 7 - .../core/media/movie/library/movie/main.py | 193 ----------------- couchpotato/core/media/movie/searcher/main.py | 62 +++--- .../core/media/movie/suggestion/main.py | 2 - couchpotato/core/notifications/core/index.py | 4 +- couchpotato/core/notifications/core/main.py | 6 +- couchpotato/core/notifications/growl/main.py | 2 +- .../core/notifications/pushover/main.py | 12 +- couchpotato/core/notifications/trakt/main.py | 7 +- couchpotato/core/notifications/xbmc/main.py | 4 +- couchpotato/core/plugins/automation/main.py | 4 +- couchpotato/core/plugins/base.py | 2 +- couchpotato/core/plugins/category/index.py | 8 +- couchpotato/core/plugins/custom/main.py | 2 +- couchpotato/core/plugins/dashboard/main.py | 1 - couchpotato/core/plugins/file/main.py | 3 +- couchpotato/core/plugins/manage/main.py | 14 +- couchpotato/core/plugins/nosql/index.py | 2 +- couchpotato/core/plugins/nosql/main.py | 6 +- couchpotato/core/plugins/profile/index.py | 13 +- couchpotato/core/plugins/profile/main.py | 20 +- .../core/plugins/profile/static/profile.js | 2 +- couchpotato/core/plugins/quality/index.py | 16 ++ couchpotato/core/plugins/quality/main.py | 106 +++++----- couchpotato/core/plugins/release/index.py | 30 ++- couchpotato/core/plugins/release/main.py | 95 ++++----- couchpotato/core/plugins/renamer/main.py | 33 ++- couchpotato/core/plugins/scanner/main.py | 14 +- couchpotato/core/plugins/score/main.py | 12 +- couchpotato/core/plugins/subtitle/main.py | 3 +- couchpotato/core/plugins/trailer/main.py | 2 +- couchpotato/core/providers/base.py | 2 +- .../core/providers/info/_modifier/main.py | 31 +-- .../providers/info/couchpotatoapi/main.py | 4 +- .../core/providers/info/themoviedb/main.py | 2 - couchpotato/core/providers/metadata/base.py | 9 +- .../core/providers/metadata/xbmc/main.py | 6 +- .../core/providers/nzb/binsearch/main.py | 2 +- .../core/providers/nzb/newznab/main.py | 2 +- .../core/providers/nzb/nzbclub/main.py | 2 +- .../core/providers/nzb/nzbindex/main.py | 2 +- .../core/providers/nzb/omgwtfnzbs/main.py | 2 +- .../core/providers/torrent/awesomehd/main.py | 2 +- .../core/providers/torrent/bithdtv/main.py | 2 +- .../core/providers/torrent/bitsoup/main.py | 2 +- .../core/providers/torrent/hdbits/main.py | 2 +- .../providers/torrent/ilovetorrents/main.py | 2 +- .../core/providers/torrent/iptorrents/main.py | 2 +- .../providers/torrent/kickasstorrents/main.py | 2 +- .../providers/torrent/passthepopcorn/main.py | 4 +- .../core/providers/torrent/publichd/main.py | 2 +- .../providers/torrent/sceneaccess/main.py | 2 +- .../providers/torrent/thepiratebay/main.py | 2 +- .../providers/torrent/torrentbytes/main.py | 2 +- .../core/providers/torrent/torrentday/main.py | 2 +- .../providers/torrent/torrentleech/main.py | 2 +- .../providers/torrent/torrentpotato/main.py | 2 +- .../providers/torrent/torrentshack/main.py | 2 +- .../core/providers/torrent/yify/main.py | 2 +- .../core/providers/trailer/hdtrailers/main.py | 8 +- couchpotato/core/settings/__init__.py | 3 +- couchpotato/core/settings/index.py | 8 +- couchpotato/runner.py | 4 +- couchpotato/templates/index.html | 2 - 82 files changed, 643 insertions(+), 715 deletions(-) delete mode 100644 couchpotato/core/media/_base/library/__init__.py delete mode 100644 couchpotato/core/media/movie/library/__init__.py delete mode 100644 couchpotato/core/media/movie/library/movie/__init__.py delete mode 100644 couchpotato/core/media/movie/library/movie/main.py create mode 100644 couchpotato/core/plugins/quality/index.py diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 22b5a91b..02e21f2d 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -42,11 +42,11 @@ class Core(Plugin): addEvent('app.shutdown', self.shutdown) addEvent('app.restart', self.restart) - addEvent('app.load2', self.launchBrowser, priority = 1) + addEvent('app.load', self.launchBrowser, priority = 1) addEvent('app.base_url', self.createBaseUrl) addEvent('app.api_url', self.createApiUrl) addEvent('app.version', self.version) - addEvent('app.load2', self.checkDataDir) + addEvent('app.load', self.checkDataDir) addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.api_key', self.checkApikey) diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 1c9d82a6..248d2bc5 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -74,7 +74,7 @@ class ClientScript(Plugin): addEvent('clientscript.get_scripts', self.getScripts) if not Env.get('dev'): - addEvent('app.load2', self.minify) + addEvent('app.load', self.minify) self.addCore() diff --git a/couchpotato/core/_base/desktop/main.py b/couchpotato/core/_base/desktop/main.py index 21499159..c3beff17 100644 --- a/couchpotato/core/_base/desktop/main.py +++ b/couchpotato/core/_base/desktop/main.py @@ -25,7 +25,7 @@ if Env.get('desktop'): # Events to desktop addEvent('app.after_shutdown', desktop.afterShutdown) - addEvent('app.load2', desktop.onAppLoad, priority = 110) + addEvent('app.load', desktop.onAppLoad, priority = 110) def onClose(self, event): return fireEvent('app.shutdown', single = True) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index aad4eee2..ef595ad7 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -33,8 +33,8 @@ class Updater(Plugin): else: self.updater = SourceUpdater() - addEvent('app.load2', self.logVersion, priority = 10000) - addEvent('app.load2', self.setCrons) + addEvent('app.load', self.logVersion, priority = 10000) + addEvent('app.load', self.setCrons) addEvent('updater.info', self.info) addApiView('updater.info', self.info, docs = { diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 23b7a25c..27177756 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -23,7 +23,7 @@ class rTorrent(Downloader): def __init__(self): super(rTorrent, self).__init__() - addEvent('app.load2', self.migrate) + addEvent('app.load', self.migrate) def migrate(self): diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 47810a66..fc910dff 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -216,26 +216,24 @@ def toIterable(value): return [value] -def getTitle(library_dict): +def getTitle(media_dict): try: try: - return library_dict['titles'][0]['title'] + return media_dict['title'] except: try: - for title in library_dict.titles: - if title.default: - return title.title + return media_dict['titles'][0] except: try: - return library_dict['info']['titles'][0] + return media_dict['info']['titles'][0] except: - log.error('Could not get title for %s', library_dict.identifier) + log.error('Could not get title for %s', media_dict.get('identifier')) return None - log.error('Could not get title for %s', library_dict['identifier']) + log.error('Could not get title for %s', media_dict['identifier']) return None except: - log.error('Could not get title for library item: %s', library_dict) + log.error('Could not get title for library item: %s', media_dict) return None diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index db87aa75..c7ae732a 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,8 +1,7 @@ import traceback -from couchpotato import get_session, get_db, CPLog -from couchpotato.core.event import addEvent, fireEventAsync, fireEvent +from couchpotato import get_db, CPLog +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Media log = CPLog(__name__) @@ -34,11 +33,9 @@ class MediaBase(Plugin): media = db.get('id', media_id) event_name = '%s.searcher.single' % media.get('type') - fireEvent(event_name, media, on_complete = self.createNotifyFront(media_id)) + fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id)) except: log.error('Failed creating onComplete: %s', traceback.format_exc()) - finally: - pass #db.close() return onComplete @@ -53,7 +50,5 @@ class MediaBase(Plugin): fireEvent('notify.frontend', type = event_name, data = media) except: log.error('Failed creating onComplete: %s', traceback.format_exc()) - finally: - pass #db.close() return notifyFront diff --git a/couchpotato/core/media/_base/library/__init__.py b/couchpotato/core/media/_base/library/__init__.py deleted file mode 100644 index 553eff5a..00000000 --- a/couchpotato/core/media/_base/library/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from couchpotato.core.event import addEvent -from couchpotato.core.plugins.base import Plugin - - -class LibraryBase(Plugin): - - _type = None - - def initType(self): - addEvent('library.types', self.getType) - - def getType(self): - return self._type diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index e930dd3c..97b16066 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -14,9 +14,14 @@ class MediaIMDBIndex(HashIndex): return int(key.strip('t')) def make_key_value(self, data): - if data.get('type') == 'media' and data.get('identifier'): + if data.get('_t') == 'media' and data.get('identifier'): return int(data['identifier'].strip('t')), None + def run_to_dict(self, db, media_id, dict = None): + if not dict: dict = {} + + return db.get('id', media_id) + def run_with_status(self, db, status = []): status = list(status if isinstance(status, (list, tuple)) else [status]) @@ -29,15 +34,15 @@ class MediaIMDBIndex(HashIndex): class MediaStatusIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = '32s' super(MediaStatusIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('type') == 'media' and data.get('status'): - return md5(data.get('status')).digest(), None + if data.get('_t') == 'media' and data.get('status'): + return md5(data.get('status')).hexdigest(), None class TitleIndex(MultiTreeBasedIndex): @@ -52,7 +57,7 @@ from itertools import izip""" def make_key_value(self, data): - if data.get('type') == 'title' and len(data.get('title', '')) > 0: + if data.get('_t') == 'title' and len(data.get('title', '')) > 0: out = set() title = data.get('title').lower() @@ -80,5 +85,5 @@ class YearIndex(TreeBasedIndex): return key def make_key_value(self, data): - if data.get('type') == 'media' and data.get('year') is not None: + if data.get('_t') == 'media' and data.get('year') is not None: return data['year'], None diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 2b56ac79..b4b0cc18 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,16 +1,14 @@ import traceback +import time from couchpotato import get_session, tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase from .index import MediaIMDBIndex, TitleIndex, MediaStatusIndex, YearIndex from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ Media -from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql.expression import or_, asc, not_, desc from string import ascii_lowercase log = CPLog(__name__) @@ -63,10 +61,10 @@ class MediaPlugin(MediaBase): addEvent('database.setup', self.databaseSetup) - addEvent('app.load2', self.addSingleRefreshView) - addEvent('app.load2', self.addSingleListView) - addEvent('app.load2', self.addSingleCharView) - addEvent('app.load2', self.addSingleDeleteView) + addEvent('app.load', self.addSingleRefreshView) + addEvent('app.load', self.addSingleListView) + addEvent('app.load', self.addSingleCharView) + addEvent('app.load', self.addSingleDeleteView) addEvent('media.get', self.get) addEvent('media.list', self.list) @@ -126,7 +124,7 @@ class MediaPlugin(MediaBase): try: media = get_db().get('id', media_id) - default_title = getTitle(media_id) + default_title = getTitle(media) event = 'library.update.%s' % media.get('type') def handler(): @@ -157,7 +155,7 @@ class MediaPlugin(MediaBase): results = None if m: - results = db.run('media', 'to_dict', m, self.default_dict) + results = db.run('media', 'to_dict', m['_id']) return results @@ -172,7 +170,9 @@ class MediaPlugin(MediaBase): def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): - db = get_session() + db = get_db() + + start = time.time() # Make a list from string if status and not isinstance(status, (list, tuple)): @@ -182,120 +182,128 @@ class MediaPlugin(MediaBase): if types and not isinstance(types, (list, tuple)): types = [types] - # query movie ids - q = db.query(Media) \ - .with_entities(Media.id) \ - .group_by(Media.id) + # query media ids + all_media_ids = set([x['_id'] for x in db.all('media')]) + media_ids = all_media_ids + filter_by = {} # Filter on movie status if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) + filter_by['media_status'] = set() + for media_status in db.run('media', 'with_status', status): + filter_by['media_status'].add(media_status.get('_id')) # Filter on release status if release_status and len(release_status) > 0: - q = q.join(Media.releases) + filter_by['release_status'] = set() + for release_status in db.run('release', 'with_status', release_status): + filter_by['release_status'].add(release_status.get('media_id')) - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Release.status_id.in_(statuses)) + # Filter by combining ids + for x in filter_by: + media_ids = media_ids & filter_by[x] # Filter on type - if types and len(types) > 0: - try: q = q.filter(Media.type.in_(types)) - except: pass + # if types and len(types) > 0: + # try: q = q.filter(Media.type.in_(types)) + # except: pass # Only join when searching / ordering - if starts_with or search or order != 'release_order': - q = q.join(Media.library, Library.titles) \ - .filter(LibraryTitle.default == True) + # if starts_with or search or order != 'release_order': + # q = q.join(Media.library, Library.titles) \ + # .filter(LibraryTitle.default == True) # Add search filters - filter_or = [] - if starts_with: - starts_with = toUnicode(starts_with.lower()) - if starts_with in ascii_lowercase: - filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) - else: - ignore = [] - for letter in ascii_lowercase: - ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) - filter_or.append(not_(or_(*ignore))) + # filter_or = [] + # if starts_with: + # starts_with = toUnicode(starts_with.lower()) + # if starts_with in ascii_lowercase: + # filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) + # else: + # ignore = [] + # for letter in ascii_lowercase: + # ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) + # filter_or.append(not_(or_(*ignore))) if search: - filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) + search_ids = db.get_many('media_title', search) + for search_id in search_ids: + print search_id - if len(filter_or) > 0: - q = q.filter(or_(*filter_or)) + #if len(filter_or) > 0: + # pass #q = q.filter(or_(*filter_or)) - total_count = q.count() + total_count = len(media_ids) if total_count == 0: return 0, [] - if order == 'release_order': - q = q.order_by(desc(Release.last_edit)) - else: - q = q.order_by(asc(LibraryTitle.simple_title)) + # if order == 'release_order': + # q = q.order_by(desc(Release.last_edit)) + # else: + # q = q.order_by(asc(LibraryTitle.simple_title)) - if limit_offset: - splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset - limit = splt[0] - offset = 0 if len(splt) is 1 else splt[1] - q = q.limit(limit).offset(offset) + # if limit_offset: + # splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset + # limit = splt[0] + # offset = 0 if len(splt) is 1 else splt[1] + # q = q.limit(limit).offset(offset) # Get all media_ids in sorted order - media_ids = [m.id for m in q.all()] + # media_ids = [m.id for m in q.all()] # List release statuses - releases = db.query(Release) \ - .filter(Release.movie_id.in_(media_ids)) \ - .all() + # releases = db.query(Release) \ + # .filter(Release.movie_id.in_(media_ids)) \ + # .all() - release_statuses = dict((m, set()) for m in media_ids) - releases_count = dict((m, 0) for m in media_ids) - for release in releases: - release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) - releases_count[release.movie_id] += 1 + # release_statuses = dict((m, set()) for m in media_ids) + # releases_count = dict((m, 0) for m in media_ids) + # for release in releases: + # release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) + # releases_count[release.movie_id] += 1 # Get main movie data - q2 = db.query(Media) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) + # q2 = db.query(Media) \ + # .options(joinedload_all('library.titles')) \ + # .options(joinedload_all('library.files')) \ + # .options(joinedload_all('status')) \ + # .options(joinedload_all('files')) - q2 = q2.filter(Media.id.in_(media_ids)) + # q2 = q2.filter(Media.id.in_(media_ids)) - results = q2.all() + # results = q2.all() # Create dict by movie id - movie_dict = {} - for movie in results: - movie_dict[movie.id] = movie + # medias = {} + # for media in media_ids: + # movie_dict[movie.id] = movie # List movies based on media_ids order - movies = [] + medias = [] for media_id in media_ids: - releases = [] - for r in release_statuses.get(media_id): - x = splitString(r) - releases.append({'status_id': x[0], 'quality_id': x[1]}) - - # Merge releases with movie dict - movies.append(mergeDicts(movie_dict[media_id].to_dict({ + media = db.run('media', 'to_dict', media_id, { 'library': {'titles': {}, 'files': {}}, 'files': {}, - }), { - 'releases': releases, - 'releases_count': releases_count.get(media_id), - })) + }) - pass #db.close() - return total_count, movies + media['releases'] = [] + for r in db.get_many('release', media_id, with_doc = True): + media['releases'].append(r) + + # Merge releases with movie dict + medias.append(media) + # medias.append(mergeDicts(movie_dict[media_id].to_dict({ + # 'library': {'titles': {}, 'files': {}}, + # 'files': {}, + # }), { + # 'releases': releases, + # 'releases_count': releases_count.get(media_id), + # })) + + print time.time() - start + + return total_count, medias def listView(self, **kwargs): @@ -424,19 +432,18 @@ class MediaPlugin(MediaBase): db.commit() deleted = True else: - done_status = fireEvent('status.get', 'done', single = True) total_releases = len(media.releases) total_deleted = 0 new_movie_status = None for release in media.releases: if delete_from in ['wanted', 'snatched', 'late']: - if release.status_id != done_status.get('id'): + if release.get('status') != 'done': db.delete(release) total_deleted += 1 new_movie_status = 'done' elif delete_from == 'manage': - if release.status_id == done_status.get('id'): + if release.get('status') == 'done': db.delete(release) total_deleted += 1 new_movie_status = 'active' @@ -447,9 +454,8 @@ class MediaPlugin(MediaBase): db.commit() deleted = True elif new_movie_status: - new_status = fireEvent('status.get', new_movie_status, single = True) media.profile_id = None - media.status_id = new_status.get('id') + media['status'] = new_status db.commit() else: fireEvent('media.restatus', media.id, single = True) @@ -458,9 +464,6 @@ class MediaPlugin(MediaBase): fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) except: log.error('Failed deleting media: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return True @@ -483,8 +486,6 @@ class MediaPlugin(MediaBase): def restatus(self, media_id): - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) - try: db = get_session() @@ -494,17 +495,17 @@ class MediaPlugin(MediaBase): return False log.debug('Changing status for %s', m.library.titles[0].title) - if not m.profile: - m.status_id = done_status.get('id') + if not m['profile_id']: + m['status'] = 'done' else: move_to_wanted = True for t in m.profile.types: for release in m.releases: - if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): + if t.quality.identifier is release.quality.identifier and (release.get('status') == 'done' and t.finish): move_to_wanted = False - m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') + m['status'] = 'active' if move_to_wanted else 'done' db.commit() diff --git a/couchpotato/core/media/_base/search/main.py b/couchpotato/core/media/_base/search/main.py index 6f0f5172..81897b5f 100644 --- a/couchpotato/core/media/_base/search/main.py +++ b/couchpotato/core/media/_base/search/main.py @@ -25,7 +25,7 @@ class Search(Plugin): }"""} }) - addEvent('app.load2', self.addSingleSearches) + addEvent('app.load', self.addSingleSearches) def search(self, q = '', types = None, **kwargs): diff --git a/couchpotato/core/media/_base/searcher/base.py b/couchpotato/core/media/_base/searcher/base.py index 60799bf0..5322d850 100644 --- a/couchpotato/core/media/_base/searcher/base.py +++ b/couchpotato/core/media/_base/searcher/base.py @@ -28,7 +28,7 @@ class SearcherBase(Plugin): fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) - addEvent('app.load2', setCrons) + addEvent('app.load', setCrons) addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons) addEvent('setting.save.%s_searcher.cron_hour.after' % _type, setCrons) addEvent('setting.save.%s_searcher.cron_minute.after' % _type, setCrons) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index e7209b60..8c0eaa0f 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -48,7 +48,7 @@ class Searcher(SearcherBase): results = [] for search_protocol in protocols: - protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media['type']), media, quality, merge = True) + protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media.get('type')), media, quality, merge = True) if protocol_results: results += protocol_results diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 306766c4..f35d4a8b 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -1,13 +1,13 @@ import traceback -from couchpotato import get_session, get_db +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, tryInt, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase -from couchpotato.core.settings.model import Media import time +import six log = CPLog(__name__) @@ -42,8 +42,10 @@ class MovieBase(MovieTypeBase): }) addEvent('movie.add', self.add) + addEvent('movie.update_info', self.updateInfo) + addEvent('movie.update_release_dates', self.updateReleaseDate) - def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): + def add(self, params = None, force_readd = True, search_after = True, update_library = False, status = None): if not params: params = {} if not params.get('identifier'): @@ -62,27 +64,55 @@ class MovieBase(MovieTypeBase): except: pass - # library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library) info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) + # Set default title + default_title = toUnicode(info.get('title')) + titles = info.get('titles', []) + counter = 0 + def_title = None + for title in titles: + if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): + def_title = toUnicode(title) + break + counter += 1 + + if not def_title: + def_title = toUnicode(titles[0]) + + # Default profile and category default_profile = fireEvent('profile.default', single = True) cat_id = params.get('category_id') try: db = get_db() + media = { + '_t': 'media', + 'type': 'movie', + 'title': def_title, + 'identifier': params.get('identifier'), + 'status': status if status else 'active', + 'profile_id': params.get('profile_id', default_profile.get('_id')), + 'category_id': tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, + } + new = False try: - m = db.get('movie', params.get('identifier'), with_doc = True)['doc'] + m = db.get('media', params.get('identifier'), with_doc = True)['doc'] except: new = True - m = db.insert({ - 'type': 'movie', - 'identifier': params.get('identifier'), - 'status': status_id if status_id else 'active', - 'profile_id': params.get('profile_id', default_profile.get('id')), - 'category_id': tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, - }) + m = db.insert(media) + + # Update dict to be usable + m.update(media) + + # Update movie info + try: del info['in_wanted'] + except: pass + try: del info['in_library'] + except: pass + m['info'] = info added = True do_search = False @@ -92,7 +122,7 @@ class MovieBase(MovieTypeBase): if search_after: onComplete = self.createOnComplete(m['_id']) - # fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) + fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title', ''), on_complete = onComplete) search_after = False elif force_readd: @@ -106,24 +136,24 @@ class MovieBase(MovieTypeBase): fireEvent('release.delete', release['_id'], single = True) m['profile_id'] = params.get('profile_id', default_profile.get('id')) - m['category_id'] = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m['category_id'] or None) + m['category_id'] = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.get('category_id') or None) else: log.debug('Movie already exists, not updating: %s', params) added = False if force_readd: - m['status'] = status_id if status_id else 'active' m['last_edit'] = int(time.time()) do_search = True - db.update(m) + if added: + db.update(m) # Remove releases for rel in db.run('release', 'for_media', m['_id']): if rel['status'] is 'available': db.delete(rel) - movie_dict = db.run('movie', 'to_dict', m['_id']) + movie_dict = db.run('media', 'to_dict', m['_id']) if do_search and search_after: onComplete = self.createOnComplete(m['_id']) @@ -137,7 +167,7 @@ class MovieBase(MovieTypeBase): if title: message = 'Successfully added "%s" to your wanted list.' % title else: - message = 'Succesfully added to your wanted list.' + message = 'Successfully added to your wanted list.' fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message) return movie_dict @@ -161,7 +191,7 @@ class MovieBase(MovieTypeBase): for media_id in ids: try: - m = db.get('media', media_id) + m = db.get('id', media_id) m['profile_id'] = kwargs.get('profile_id') cat_id = kwargs.get('category_id') @@ -197,3 +227,128 @@ class MovieBase(MovieTypeBase): return { 'success': False, } + + def updateInfo(self, media_id = None, identifier = None, default_title = None, extended = False): + """ + Update movie information inside media['doc']['info'] + + @param media_id: document id + @param default_title: default title, if empty, use first one or existing one + @param extended: update with extended info (parses more info, actors, images from some info providers) + @return: dict, with media + """ + + if self.shuttingDown(): + return + + try: + db = get_db() + + if media_id: + media = db.get('id', media_id) + else: + media = db.get('media', identifier, with_doc = True)['doc'] + + info = fireEvent('movie.info', merge = True, extended = extended, identifier = media.get('identifier')) + + # Don't need those here + try: del info['in_wanted'] + except: pass + try: del info['in_library'] + except: pass + + if not info or len(info) == 0: + log.error('Could not update, no movie info to work with: %s', media.get('identifier')) + return False + + # Update basic info + media['info'] = info + + titles = info.get('titles', []) + log.debug('Adding titles: %s', titles) + counter = 0 + + def_title = None + for title in titles: + if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): + def_title = toUnicode(title) + break + counter += 1 + + if not def_title: + def_title = toUnicode(titles[0]) + + media = { + 'title': def_title, + } + + # Files + images = info.get('images', []) + media['files'] = media.get('filed', []) + for image_type in ['poster']: + for image in images.get(image_type, []): + if not isinstance(image, (str, unicode)): + continue + + file_path = fireEvent('file.download', url = image, single = True) + media['files'].append({ + 'type': 'image_%s' % image_type, + 'path': file_path + }) + + db.update(media) + + return media + except: + log.error('Failed update media: %s', traceback.format_exc()) + + return {} + + def updateReleaseDate(self, media_id): + """ + Update releasedate (eta) info only + + @param media_id: document id + @return: dict, with dates dvd, theater, bluray, expires + """ + + try: + db = get_db() + + media = db.get('id', media_id) + + if not media.get('info'): + info_dict = self.updateInfo(media_id) + dates = info_dict.get('info', {}).get('release_date') + else: + dates = media.get('info').get('release_date') + + if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: + dates = fireEvent('movie.info.release_date', identifier = media['identifier'], merge = True) + media['info'].update({'release_date': dates}) + db.update(media) + + return dates + except: + log.error('Failed updating release dates: %s', traceback.format_exc()) + + return {} + + def simplifyTitle(self, title): + """ + Removes all special chars from a title so it's easier to make sortable + @param title: media title + @return: string, simplified title + """ + + title = toUnicode(title) + + nr_prefix = '' if title[0] in ascii_letters else '#' + title = simplifyString(title) + + for prefix in ['the ']: + if prefix == title[:len(prefix)]: + title = title[len(prefix):] + break + + return nr_prefix + title diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 66c84c68..c5186935 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -191,11 +191,9 @@ MA.Release = new Class({ self.releases.each(function(release){ - var status = Status.get(release.status_id), - quality = Quality.getProfile(release.quality_id) || {}, + var quality = Quality.getProfile(release.quality_id) || {}, info = release.info, provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : ''); - release.status = status; var release_name = self.get(release, 'name'); if(release.files && release.files.length > 0){ @@ -215,7 +213,7 @@ MA.Release = new Class({ 'id': 'release_'+release.id }).adopt( new Element('span.name', {'text': release_name, 'title': release_name}), - new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}), + new Element('span.status', {'text': status.identifier, 'class': 'release_status '+release.status}), new Element('span.quality', {'text': quality.get('label') || 'n/a'}), new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.age', {'text': self.get(release, 'age')}), @@ -257,22 +255,20 @@ MA.Release = new Class({ if(notification.data.id != release.id) return; var q = self.movie.quality.getElement('.q_id' + release.quality_id), - status = Status.get(release.status_id), - new_status = Status.get(notification.data.status_id); + new_status = notification.data.status; - release.status_id = new_status.id - release.el.set('class', 'item ' + new_status.identifier); + release.el.set('class', 'item ' + new_status); var status_el = release.el.getElement('.release_status'); - status_el.set('class', 'release_status ' + new_status.identifier); + status_el.set('class', 'release_status ' + new_status); status_el.set('text', new_status.identifier); - if(!q && (new_status.identifier == 'snatched' || new_status.identifier == 'seeding' || new_status.identifier == 'done')) + if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) var q = self.addQuality(release.quality_id); - if(new_status && q && !q.hasClass(new_status.identifier)) { - q.removeClass(status.identifier).addClass(new_status.identifier); - q.set('title', q.get('title').replace(status.label, new_status.label)); + if(q && !q.hasClass(new_status)) { + q.removeClass(release.status).addClass(new_status); + q.set('title', q.get('title').replace(status, new_status)); } } @@ -344,12 +340,10 @@ MA.Release = new Class({ self.movie.data.releases.each(function(release){ if(has_available && has_snatched) return; - var status = Status.get(release.status_id); - - if(['snatched', 'downloaded', 'seeding'].contains(status.identifier)) + if(['snatched', 'downloaded', 'seeding'].contains(release.status)) has_snatched = true; - if(['available'].contains(status.identifier)) + if(['available'].contains(release.status)) has_available = true; }); @@ -726,10 +720,10 @@ MA.Readd = new Class({ create: function(){ var self = this; - var movie_done = Status.get(self.movie.data.status_id).identifier == 'done'; + var movie_done = self.movie.data.status == 'done'; if(!movie_done) var snatched = self.movie.data.releases.filter(function(release){ - return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done'); + return release.status && (release.status == 'snatched' || release.status == 'downloaded' || release.status == 'done'); }).length; if(movie_done || snatched && snatched > 0) @@ -924,4 +918,4 @@ MA.Files = new Class({ self.movie.slide('in', self.options_container); }, -}); \ No newline at end of file +}); diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 3ca1912c..bd290dcd 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -59,7 +59,7 @@ var Movie = new Class({ if(!self.data.releases) self.data.releases = []; - self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id}); + self.data.releases.push({'quality_id': data.quality_id, 'status': data.status}); self.updateReleases(); } } @@ -146,8 +146,7 @@ var Movie = new Class({ create: function(){ var self = this; - var s = Status.get(self.get('status_id')); - self.el.addClass('status_'+s.identifier); + self.el.addClass('status_'+self.get('status')); self.el.adopt( self.select_checkbox = new Element('input[type=checkbox].inlay', { @@ -157,7 +156,7 @@ var Movie = new Class({ } } }), - self.thumbnail = File.Select.single('poster', self.data.library.files), + self.thumbnail = File.Select.single('poster', self.data.files || []), self.data_container = new Element('div.data.inlay.light').adopt( self.info_container = new Element('div.info').adopt( new Element('div.title').adopt( @@ -165,11 +164,11 @@ var Movie = new Class({ 'text': self.getTitle() || 'n/a' }), self.year = new Element('div.year', { - 'text': self.data.library.year || 'n/a' + 'text': self.data.info.year || 'n/a' }) ), self.description = new Element('div.description', { - 'text': self.data.library.plot + 'text': self.data.info.plot }), self.quality = new Element('div.quality', { 'events': { @@ -221,14 +220,14 @@ var Movie = new Class({ self.data.releases.each(function(release){ var q = self.quality.getElement('.q_id'+ release.quality_id), - status = Status.get(release.status_id); + status = release.status; - if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done')) + if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) var q = self.addQuality(release.quality_id) - if (status && q && !q.hasClass(status.identifier)){ - q.addClass(status.identifier); - q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) + if (q && !q.hasClass(status)){ + q.addClass(status); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status) } }); @@ -249,16 +248,10 @@ var Movie = new Class({ getTitle: function(){ var self = this; - var titles = self.data.library.titles; - - var title = titles.filter(function(title){ - return title['default'] - }).pop() - - if(title) - return self.getUnprefixedTitle(title.title) - else if(titles.length > 0) - return self.getUnprefixedTitle(titles[0].title) + if(self.data.title) + return self.getUnprefixedTitle(self.data.title) + else if(self.data.info.titles.length > 0) + return self.getUnprefixedTitle(self.data.info.titles[0]) return 'Unknown movie' }, @@ -313,7 +306,7 @@ var Movie = new Class({ }, get: function(attr){ - return this.data[attr] || this.data.library[attr] + return this.data[attr] || this.data.info[attr] }, select: function(bool){ diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index e04167f0..61842149 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -197,8 +197,8 @@ Block.Search.MovieItem = new Class({ profiles.each(function(profile){ new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, - 'text': profile.label ? profile.label : profile.data.label + 'value': profile.get('_id'), + 'text': profile.get('label') }).inject(self.profile_select) }); diff --git a/couchpotato/core/media/movie/library/__init__.py b/couchpotato/core/media/movie/library/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/couchpotato/core/media/movie/library/movie/__init__.py b/couchpotato/core/media/movie/library/movie/__init__.py deleted file mode 100644 index 98ed54c0..00000000 --- a/couchpotato/core/media/movie/library/movie/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import MovieLibraryPlugin - - -def start(): - return MovieLibraryPlugin() - -config = [] diff --git a/couchpotato/core/media/movie/library/movie/main.py b/couchpotato/core/media/movie/library/movie/main.py deleted file mode 100644 index 663c6d04..00000000 --- a/couchpotato/core/media/movie/library/movie/main.py +++ /dev/null @@ -1,193 +0,0 @@ -from couchpotato import get_session -from couchpotato.core.event import addEvent, fireEventAsync, fireEvent -from couchpotato.core.helpers.encoding import toUnicode, simplifyString -from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.library import LibraryBase -from couchpotato.core.settings.model import Library, LibraryTitle, File -from string import ascii_letters -import time -import traceback -import six - -log = CPLog(__name__) - - -class MovieLibraryPlugin(LibraryBase): - - default_dict = {'titles': {}, 'files': {}} - - def __init__(self): - addEvent('library.add.movie', self.add) - addEvent('library.update.movie', self.update) - addEvent('library.update.movie.release_date', self.updateReleaseDate) - - def add(self, attrs = None, update_after = True): - if not attrs: attrs = {} - - primary_provider = attrs.get('primary_provider', 'imdb') - - try: - db = get_session() - - l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first() - if not l: - status = fireEvent('status.get', 'needs_update', single = True) - l = Library( - year = attrs.get('year'), - identifier = attrs.get('identifier'), - plot = toUnicode(attrs.get('plot')), - tagline = toUnicode(attrs.get('tagline')), - status_id = status.get('id'), - info = {} - ) - - title = LibraryTitle( - title = toUnicode(attrs.get('title')), - simple_title = self.simplifyTitle(attrs.get('title')), - ) - - l.titles.append(title) - - db.add(l) - db.commit() - - # Update library info - if update_after is not False: - handle = fireEventAsync if update_after is 'async' else fireEvent - handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', ''))) - - library_dict = l.to_dict(self.default_dict) - return library_dict - except: - log.error('Failed adding media: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() - - return {} - - def update(self, identifier, default_title = '', extended = False): - - if self.shuttingDown(): - return - - try: - db = get_session() - - library = db.query(Library).filter_by(identifier = identifier).first() - done_status = fireEvent('status.get', 'done', single = True) - - info = fireEvent('movie.info', merge = True, extended = extended, identifier = identifier) - - # Don't need those here - try: del info['in_wanted'] - except: pass - try: del info['in_library'] - except: pass - - if not info or len(info) == 0: - log.error('Could not update, no movie info to work with: %s', identifier) - return False - - # Main info - library.plot = toUnicode(info.get('plot', '')) - library.tagline = toUnicode(info.get('tagline', '')) - library.year = info.get('year', 0) - library.status_id = done_status.get('id') - library.info.update(info) - db.commit() - - # Titles - [db.delete(title) for title in library.titles] - db.commit() - - titles = info.get('titles', []) - log.debug('Adding titles: %s', titles) - counter = 0 - - def_title = None - for title in titles: - if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): - def_title = toUnicode(title) - break - counter += 1 - - if not def_title: - def_title = toUnicode(titles[0]) - - for title in titles: - if not title: - continue - title = toUnicode(title) - t = LibraryTitle( - title = title, - simple_title = self.simplifyTitle(title), - default = title == def_title - ) - library.titles.append(t) - - db.commit() - - # Files - images = info.get('images', []) - library['files'] = [] - for image_type in ['poster']: - for image in images.get(image_type, []): - if not isinstance(image, (str, unicode)): - continue - - file_path = fireEvent('file.download', url = image, single = True) - - # TODO: save in movie doc - library['files'].append({ - 'type': 'image_%s' % image_type, - 'path': file_path - }) - - library_dict = library.to_dict(self.default_dict) - return library_dict - except: - log.error('Failed update media: %s', traceback.format_exc()) - - return {} - - def updateReleaseDate(self, identifier): - - try: - db = get_session() - library = db.query(Library).filter_by(identifier = identifier).first() - - if not library.info: - library_dict = self.update(identifier) - dates = library_dict.get('info', {}).get('release_date') - else: - dates = library.info.get('release_date') - - if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: - dates = fireEvent('movie.release_date', identifier = identifier, merge = True) - library.info.update({'release_date': dates}) - db.commit() - - return dates - except: - log.error('Failed updating release dates: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() - - return {} - - - def simplifyTitle(self, title): - - title = toUnicode(title) - - nr_prefix = '' if title[0] in ascii_letters else '#' - title = simplifyString(title) - - for prefix in ['the ']: - if prefix == title[:len(prefix)]: - title = title[len(prefix):] - break - - return nr_prefix + title diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index b4665deb..fdff2fd9 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -1,4 +1,4 @@ -from couchpotato import get_session, get_db +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString @@ -6,7 +6,6 @@ from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media.movie import MovieTypeBase -from couchpotato.core.settings.model import Media, Release from couchpotato.environment import Env from datetime import date import random @@ -51,7 +50,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): }) if self.conf('run_on_launch'): - addEvent('app.load2', self.searchAll) + addEvent('app.load', self.searchAll) def searchAllView(self, **kwargs): @@ -94,7 +93,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.single(movie_dict, search_protocols) except IndexError: log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['identifier'], traceback.format_exc())) - fireEvent('library.update.movie', movie['identifier']) + fireEvent('movie.update_info', movie['_id']) except: log.error('Search failed for %s: %s', (movie['identifier'], traceback.format_exc())) @@ -111,10 +110,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): def single(self, movie, search_protocols = None, manual = False): - # movies don't contain 'type' yet, so just set to default here - if 'type' not in movie: - movie['type'] = 'movie' - # Find out search type try: if not search_protocols: @@ -127,7 +122,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return pre_releases = fireEvent('quality.pre_releases', single = True) - release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True) + release_dates = fireEvent('movie.update_release_date', media_id = movie['_id'], merge = True) found_releases = [] too_early_to_search = [] @@ -142,39 +137,50 @@ class MovieSearcher(SearcherBase, MovieTypeBase): db = get_db() + profile = db.get('id', movie['profile_id']) + quality_order = fireEvent('quality.order', single = True) + media_releases = db.get_many('release', movie['_id']) + ret = False - for quality_type in movie['profile']['types']: - if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['year']): - too_early_to_search.append(quality_type['quality']['identifier']) + for q_identifier in profile.get('qualities'): + index = profile['qualities'].index(q_identifier) + quality_custom = { + 'quality': q_identifier, + 'finish': profile['finish'][index], + 'wait_for': profile['wait_for'][index] + } + + if not self.conf('always_search') and not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']): + too_early_to_search.append(q_identifier) continue has_better_quality = 0 # See if better quality is available - for release in movie['releases']: - if release['quality']['order'] <= quality_type['quality']['order'] and release['status'] not in ['available', 'ignored', 'failed']: + for release in media_releases: + if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: has_better_quality += 1 # Don't search for quality lower then already available. if has_better_quality is 0: - log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) - quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) + quality = fireEvent('quality.single', identifier = q_identifier, single = True) + log.info('Search for %s in %s', (default_title, quality['label'])) results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] if len(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['label'])) # Check if movie isn't deleted while searching if not fireEvent('media.get', movie.get('_id'), single = True): break # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, movie, quality_type, single = True) + found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True) # Try find a valid result and download it - if fireEvent('release.try_download_result', results, movie, quality_type, manual, single = True): + if fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): ret = True # Remove releases that aren't found anymore @@ -183,7 +189,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('release.delete', release.get('_id'), single = True) else: - log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) + log.info('Better quality (%s) already available or snatched for %s', (quality['label'], default_title)) fireEvent('media.restatus', movie['_id']) break @@ -219,7 +225,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) # Contains lower quality string - if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True): + if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True): log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) return False @@ -249,23 +255,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return True # Check if nzb contains imdb link - if getImdb(nzb.get('description', '')) == media['library']['identifier']: + if getImdb(nzb.get('description', '')) == media['identifier']: return True - for raw_title in media['library']['titles']: - for movie_title in possibleTitles(raw_title['title']): + for raw_title in media['info']['titles']: + for movie_title in possibleTitles(raw_title): movie_words = re.split('\W+', simplifyString(movie_title)) if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True): # if no IMDB link, at least check year range 1 - if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True): + if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 1, single = True): return True # if no IMDB link, at least check year - if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True): + if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 0, single = True): return True - log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year'])) + log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['info']['year'])) return False def couldBeReleased(self, is_pre_release, dates, year = None): @@ -337,7 +343,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): def getSearchTitle(self, media): if media['type'] == 'movie': - return getTitle(media['library']) + return getTitle(media) class SearchSetupError(Exception): pass diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index eaccbe64..55cf7062 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -89,8 +89,6 @@ class Suggestion(Plugin): # Get new results and add them if len(new_suggestions) - 1 < limit: - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) - db = get_session() active_movies = db.query(Media) \ .join(Library) \ diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py index e51124d6..a6fb13d4 100644 --- a/couchpotato/core/notifications/core/index.py +++ b/couchpotato/core/notifications/core/index.py @@ -15,7 +15,7 @@ import time""" return key def make_key_value(self, data): - if data.get('type') == 'notification': + if data.get('_t') == 'notification': added = data.get('added', time.time()) data['added'] = added @@ -35,7 +35,7 @@ import time""" return key def make_key_value(self, data): - if data.get('type') == 'notification' and not data.get('read'): + if data.get('_t') == 'notification' and not data.get('read'): added = data.get('added', time.time()) data['added'] = added diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 2eb5a302..5610ef28 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -57,8 +57,8 @@ class CoreNotifier(Notification): fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True) fireEvent('schedule.interval', 'core.clean_messages', self.cleanMessages, seconds = 15, single = True) - addEvent('app.load2', self.clean) - addEvent('app.load2', self.checkMessages) + addEvent('app.load', self.clean) + addEvent('app.load', self.checkMessages) addEvent('database.setup', self.databaseSetup) @@ -161,7 +161,7 @@ class CoreNotifier(Notification): data['notification_type'] = listener if listener else 'unknown' n = { - 'type': 'notification', + '_t': 'notification', 'time': int(time.time()), 'message': toUnicode(message), 'data': data diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 68d73cb1..a3927ed2 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -16,7 +16,7 @@ class Growl(Notification): super(Growl, self).__init__() if self.isEnabled(): - addEvent('app.load2', self.register) + addEvent('app.load', self.register) def register(self): if self.registered: return diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py index ba954a54..b90c2118 100644 --- a/couchpotato/core/notifications/pushover/main.py +++ b/couchpotato/core/notifications/pushover/main.py @@ -23,16 +23,16 @@ class Pushover(Notification): 'priority': self.conf('priority'), } - if data and data.get('library'): + if data and data.get('identifier'): api_data.update({ - 'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']), - 'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])), + 'url': toUnicode('http://www.imdb.com/title/%s/' % data['identifier']), + 'url_title': toUnicode('%s on IMDb' % getTitle(data)), }) http_handler.request('POST', - "/1/messages.json", - headers = {'Content-type': 'application/x-www-form-urlencoded'}, - body = tryUrlencode(api_data) + "/1/messages.json", + headers = {'Content-type': 'application/x-www-form-urlencoded'}, + body = tryUrlencode(api_data) ) response = http_handler.getresponse() diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py index c759c6db..55eee425 100644 --- a/couchpotato/core/notifications/trakt/main.py +++ b/couchpotato/core/notifications/trakt/main.py @@ -1,3 +1,4 @@ +from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification @@ -21,9 +22,9 @@ class Trakt(Notification): 'username': self.conf('automation_username'), 'password' : self.conf('automation_password'), 'movies': [{ - 'imdb_id': data['library']['identifier'], - 'title': data['library']['titles'][0]['title'], - 'year': data['library']['year'] + 'imdb_id': data['identifier'], + 'title': getTitle(data), + 'year': data['info']['year'] }] if data else [] } diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index bfda85e1..31a776c5 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.helpers.variable import splitString +from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification import base64 @@ -131,7 +131,7 @@ class XBMC(Notification): server = 'http://%s/xbmcCmds/' % host # Notification(title, message [, timeout , image]) - cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium'))) + cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(getTitle(data)), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium'))) server += cmd # I have no idea what to set to, just tried text/plain and seems to be working :) diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 44d7673f..2edcd3be 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -10,10 +10,10 @@ class Automation(Plugin): def __init__(self): - addEvent('app.load2', self.setCrons) + addEvent('app.load', self.setCrons) if not Env.get('dev'): - addEvent('app.load2', self.addMovies) + addEvent('app.load', self.addMovies) addEvent('setting.save.automation.hour.after', self.setCrons) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 0625535e..5a805361 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -291,7 +291,7 @@ class Plugin(object): def cpTag(self, media): if Env.setting('enabled', 'renamer'): - return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else '' + return '.cp(' + media.get('identifier') + ')' if media.get('identifier') else '' return '' diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py index fa579dd9..1eb3651e 100644 --- a/couchpotato/core/plugins/category/index.py +++ b/couchpotato/core/plugins/category/index.py @@ -5,12 +5,12 @@ from CodernityDB.tree_index import TreeBasedIndex class CategoryIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = '32s' super(CategoryIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('type') == 'category': - return md5(data['media_id']).digest(), None + if data.get('_t') == 'category': + return md5(data['media_id']).hexdigest(), None diff --git a/couchpotato/core/plugins/custom/main.py b/couchpotato/core/plugins/custom/main.py index 42400994..a15c915c 100644 --- a/couchpotato/core/plugins/custom/main.py +++ b/couchpotato/core/plugins/custom/main.py @@ -10,7 +10,7 @@ log = CPLog(__name__) class Custom(Plugin): def __init__(self): - addEvent('app.load2', self.createStructure) + addEvent('app.load', self.createStructure) def createStructure(self): diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index bc14f4b5..10c1c61b 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -49,7 +49,6 @@ class Dashboard(Plugin): limit = tryInt(splt[0]) # Get all active movies - active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True) q = db.query(Media) \ .join(Library) \ .outerjoin(Media.releases) \ diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index c24d0d58..37aa7282 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -5,8 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.plugins.scanner.main import Scanner -from couchpotato.core.settings.model import FileType, File +from couchpotato.core.settings.model import File from couchpotato.environment import Env from tornado.web import StaticFileHandler import os.path diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 47081692..86d00557 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -47,7 +47,7 @@ class Manage(Plugin): }) if not Env.get('dev') and self.conf('startup_scan'): - addEvent('app.load2', self.updateLibraryQuick) + addEvent('app.load', self.updateLibraryQuick) def getProgress(self, **kwargs): return { @@ -120,7 +120,7 @@ class Manage(Plugin): total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) for done_movie in done_movies: - if done_movie['library']['identifier'] not in added_identifiers: + if done_movie['identifier'] not in added_identifiers: fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') else: @@ -184,13 +184,13 @@ class Manage(Plugin): 'to_go': total_found, }) - if group['library'] and group['library'].get('identifier'): - identifier = group['library'].get('identifier') + if group['media'] and group['media'].get('identifier'): + identifier = group['media'].get('identifier') added_identifiers.append(identifier) # Add it to release and update the info fireEvent('release.add', group = group) - fireEvent('library.update.movie', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) + fireEvent('movie.update_info', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) else: self.updateProgress(folder) @@ -207,7 +207,7 @@ class Manage(Plugin): total = self.in_progress[folder]['total'] movie_dict = fireEvent('media.get', identifier, single = True) - fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library'])) + fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict)) return afterUpdate @@ -237,7 +237,7 @@ class Manage(Plugin): if groups: for group in groups.values(): - if group['library'] and group['library'].get('identifier'): + if group.get('info'): fireEvent('release.add', group = group) def getDiskSpace(self): diff --git a/couchpotato/core/plugins/nosql/index.py b/couchpotato/core/plugins/nosql/index.py index e6b0389e..f88738fb 100644 --- a/couchpotato/core/plugins/nosql/index.py +++ b/couchpotato/core/plugins/nosql/index.py @@ -12,6 +12,6 @@ class NameIndex(TreeBasedIndex): return key def make_key_value(self, data): - if data.get('type') == 'media' and data.get('title') is not None: + if data.get('_t') == 'media' and data.get('title') is not None: return data.get('title'), None diff --git a/couchpotato/core/plugins/nosql/main.py b/couchpotato/core/plugins/nosql/main.py index 511c8f1b..b5243d9f 100644 --- a/couchpotato/core/plugins/nosql/main.py +++ b/couchpotato/core/plugins/nosql/main.py @@ -2,7 +2,6 @@ import time from couchpotato import CPLog, get_db from couchpotato.core.event import addEvent from couchpotato.core.plugins.base import Plugin -from .index import ReleaseIndex, NameIndex log = CPLog(__name__) @@ -13,7 +12,7 @@ class NoSQL(Plugin): def __init__(self): - pass #addEvent('app.load2', self.test) + pass #addEvent('app.load', self.test) def test(self): @@ -31,7 +30,8 @@ class NoSQL(Plugin): for id in range(10): media = db.insert({ - 'type': 'media', + '_t': 'media', + 'type': 'movie', 'tmdb': id, 'imdb': 'tt%s' % id, 'last_edit': 0, diff --git a/couchpotato/core/plugins/profile/index.py b/couchpotato/core/plugins/profile/index.py index 2457699b..dbba1632 100644 --- a/couchpotato/core/plugins/profile/index.py +++ b/couchpotato/core/plugins/profile/index.py @@ -1,16 +1,15 @@ -from CodernityDB.hash_index import HashIndex -from hashlib import md5 +from CodernityDB.tree_index import TreeBasedIndex -class ProfileIndex(HashIndex): +class ProfileIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = 'i' super(ProfileIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return key def make_key_value(self, data): - if data.get('type') == 'profile' and data.get('identifier'): - return md5(data.get('identifier')).digest(), None + if data.get('_t') == 'profile': + return data.get('order', 99), None diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 8939f414..8f04fe3b 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -1,13 +1,12 @@ import traceback from couchpotato import get_session, get_db from couchpotato.api import addApiView -from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ProfileIndex -from couchpotato.core.settings.model import Profile, ProfileType, Media -from sqlalchemy.orm import joinedload_all +from couchpotato.core.settings.model import Profile, ProfileType log = CPLog(__name__) @@ -34,7 +33,7 @@ class ProfilePlugin(Plugin): addEvent('database.setup', self.databaseSetup) addEvent('app.initialize', self.fill, priority = 90) - addEvent('app.load2', self.forceDefaults) + addEvent('app.load', self.forceDefaults) def databaseSetup(self): @@ -46,7 +45,6 @@ class ProfilePlugin(Plugin): log.debug('Index already exists') db.edit_index(ProfileIndex(db.path, 'profile')) - def forceDefaults(self): # Get all active movies without profile @@ -57,7 +55,7 @@ class ProfilePlugin(Plugin): profile_ids = [x.get('_id') for x in self.all()] for media in medias: - if media['profile_id'] not in profile_ids: + if media.get('profile_id') not in profile_ids: default_profile = self.default() media['profile_id'] = default_profile.get('id') db.update(media) @@ -76,7 +74,7 @@ class ProfilePlugin(Plugin): db = get_db() profiles = db.all('profile', with_doc = True) - return list(profiles) + return [x['doc'] for x in profiles] def save(self, **kwargs): @@ -127,7 +125,7 @@ class ProfilePlugin(Plugin): def default(self): db = get_db() - return db.get_many('profile', limit = 1, with_doc = True)[0] + return list(db.all('profile', limit = 1, with_doc = True))[0]['doc'] def saveOrder(self, **kwargs): @@ -213,8 +211,7 @@ class ProfilePlugin(Plugin): log.info('Creating default profile: %s', profile.get('label')) pro = { - 'type': 'profile', - 'identifier': profile.get('label').lower(), + '_t': 'profile', 'label': toUnicode(profile.get('label')), 'order': order, 'qualities': profile.get('qualities'), @@ -229,9 +226,6 @@ class ProfilePlugin(Plugin): db.insert(pro) order += 1 - for x in db.all('profile', with_doc = True): - log.info(x) - return True except: log.error('Failed: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index a93ca1bc..bb0504b2 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -324,4 +324,4 @@ Profile.Type = new Class({ return this.el; } -}) \ No newline at end of file +}) diff --git a/couchpotato/core/plugins/quality/index.py b/couchpotato/core/plugins/quality/index.py new file mode 100644 index 00000000..cf2d5c77 --- /dev/null +++ b/couchpotato/core/plugins/quality/index.py @@ -0,0 +1,16 @@ +from CodernityDB.hash_index import HashIndex +from hashlib import md5 + + +class QualityIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(QualityIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).hexdigest() + + def make_key_value(self, data): + if data.get('_t') == 'quality' and data.get('identifier'): + return md5(data.get('identifier')).hexdigest(), None diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index e5070093..e75eb465 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -1,15 +1,14 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.variable import mergeDicts, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Quality, Profile, ProfileType -from sqlalchemy.sql.expression import or_ +from couchpotato.core.plugins.quality.index import QualityIndex +from couchpotato.core.settings.model import Quality import re -import time log = CPLog(__name__) @@ -39,6 +38,7 @@ class QualityPlugin(Plugin): addEvent('quality.single', self.single) addEvent('quality.guess', self.guess) addEvent('quality.pre_releases', self.preReleases) + addEvent('quality.order', self.getOrder) addApiView('quality.size.save', self.saveSize) addApiView('quality.list', self.allView, docs = { @@ -50,9 +50,31 @@ class QualityPlugin(Plugin): }) addEvent('app.initialize', self.fill, priority = 10) + addEvent('database.setup', self.databaseSetup) addEvent('app.test', self.doTest) + self.addOrder() + + def addOrder(self): + self.order = [] + for q in self.qualities: + self.order.append(q.get('identifier')) + + def getOrder(self): + return self.order + + def databaseSetup(self): + + db = get_db() + + # Quality index + try: + db.add_index(QualityIndex(db.path, 'quality')) + except: + log.debug('Index already exists') + db.edit_index(QualityIndex(db.path, 'quality')) + def preReleases(self): return self.pre_releases @@ -68,30 +90,29 @@ class QualityPlugin(Plugin): if self.cached_qualities: return self.cached_qualities - db = get_session() + db = get_db() - qualities = db.query(Quality).all() + qualities = db.all('quality', with_doc = True) temp = [] for quality in qualities: - q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict()) + quality = quality['doc'] + q = mergeDicts(self.getQuality(quality.get('identifier')), quality) temp.append(q) self.cached_qualities = temp - pass #db.close() return temp def single(self, identifier = ''): - db = get_session() + db = get_db() quality_dict = {} - quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first() + quality = db.get('quality', identifier, with_doc = True)['doc'] if quality: - quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) + quality_dict = mergeDicts(self.getQuality(quality['identifier']), quality) - pass #db.close() return quality_dict def getQuality(self, identifier): @@ -128,60 +149,35 @@ class QualityPlugin(Plugin): def fill(self): try: - db = get_session() + db = get_db() order = 0 for q in self.qualities: - # Create quality - qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first() + db.insert({ + '_t': 'quality', + 'order': order, + 'identifier': q.get('identifier'), + 'size_min': q.get('size')[0], + 'size_max': q.get('size')[1] + }) - if not qual: - log.info('Creating quality: %s', q.get('label')) - qual = Quality() - qual.order = order - qual.identifier = q.get('identifier') - qual.label = toUnicode(q.get('label')) - qual.size_min, qual.size_max = q.get('size') - - db.add(qual) - - # Create single quality profile - prof = db.query(Profile).filter( - Profile.core == True - ).filter( - Profile.types.any(quality = qual) - ).all() - - if not prof: - log.info('Creating profile: %s', q.get('label')) - prof = Profile( - core = True, - label = toUnicode(qual.label), - order = order - ) - db.add(prof) - - profile_type = ProfileType( - quality = qual, - profile = prof, - finish = True, - order = 0 - ) - prof.types.append(profile_type) + log.info('Creating profile: %s', q.get('label')) + db.insert({ + '_t': 'profile', + 'order': order + 20, # Make sure it goes behind other profiles + 'core': True, + 'qualities': [q.get('identifier')], + 'label': toUnicode(q.get('label')), + 'finish': [True], + 'wait_for': [0], + }) order += 1 - db.commit() - - time.sleep(0.3) # Wait a moment - return True except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return False diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 3db2cba5..c68edf1c 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -5,15 +5,15 @@ from CodernityDB.tree_index import TreeBasedIndex class ReleaseIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = '32s' super(ReleaseIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('type') == 'release' and data.get('media_id'): - return md5(data['media_id']).digest(), {'media_id': data.get('media_id')} + if data.get('_t') == 'release' and data.get('media_id'): + return md5(data['media_id']).hexdigest(), {'media_id': data.get('media_id')} def run_for_media(self, db, media_id): for release in db.get_many('release', media_id, with_doc = True): @@ -31,12 +31,26 @@ class ReleaseIndex(TreeBasedIndex): class ReleaseStatusIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = '32s' super(ReleaseStatusIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('type') == 'release' and data.get('status'): - return md5(data.get('status')).digest(), None + if data.get('_t') == 'release' and data.get('status'): + return md5(data.get('status')).hexdigest(), None + + +class ReleaseIDIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(ReleaseIDIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).hexdigest() + + def make_key_value(self, data): + if data.get('_t') == 'release' and data.get('identifier'): + return md5(data.get('identifier')).hexdigest(), None diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 8faf6a5d..7cb634bd 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from .index import ReleaseIndex, ReleaseStatusIndex +from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.settings.model import Release as Relea, Media, \ ReleaseInfo @@ -61,7 +61,7 @@ class Release(Plugin): addEvent('database.setup', self.databaseSetup) # Clean releases that didn't have activity in the last week - addEvent('app.load2', self.cleanDone) + addEvent('app.load', self.cleanDone) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4) def databaseSetup(self): @@ -82,6 +82,13 @@ class Release(Plugin): log.debug('Index already exists') db.edit_index(ReleaseStatusIndex(db.path, 'release_status')) + # Release identifier index + try: + db.add_index(ReleaseIDIndex(db.path, 'release_identifier')) + except: + log.debug('Index already exists') + db.edit_index(ReleaseIDIndex(db.path, 'release_identifier')) + def cleanDone(self): log.debug('Removing releases from dashboard') @@ -112,7 +119,7 @@ class Release(Plugin): try: db = get_session() - identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) + identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) # Add movie media = db.query(Media).filter_by(library_id = group['library'].get('id')).first() @@ -120,7 +127,7 @@ class Release(Plugin): media = Media( library_id = group['library'].get('id'), profile_id = 0, - status_id = done_status.get('id') + status = 'done' ) db.add(media) db.commit() @@ -129,7 +136,7 @@ class Release(Plugin): rel = db.query(Relea).filter( or_( Relea.identifier == identifier, - and_(Relea.identifier.startswith(group['library']['identifier']), Relea.status_id == snatched_status.get('id')) + and_(Relea.identifier.startswith(group['identifier']), Relea.status == 'snatched') ) ).first() if not rel: @@ -137,7 +144,7 @@ class Release(Plugin): identifier = identifier, movie = media, quality_id = group['meta_data']['quality'].get('id'), - status_id = done_status.get('id') + status = 'done' ) db.add(rel) db.commit() @@ -246,11 +253,6 @@ class Release(Plugin): # Get matching provider provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) - # Backwards compatibility code - if not item.get('protocol'): - item['protocol'] = item['type'] - item['type'] = 'movie' - if item.get('protocol') != 'torrent_magnet': item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download @@ -271,11 +273,6 @@ class Release(Plugin): def download(self, data, media, manual = False): - # Backwards compatibility code - if not data.get('protocol'): - data['protocol'] = data['type'] - data['type'] = 'movie' - # Test to see if any downloaders are enabled for this type downloader_enabled = fireEvent('download.enabled', manual, data, single = True) if not downloader_enabled: @@ -303,8 +300,6 @@ class Release(Plugin): return False log.debug('Downloader result: %s', download_result) - snatched_status, done_status, downloaded_status, active_status = fireEvent('status.get', ['snatched', 'done', 'downloaded', 'active'], single = True) - try: db = get_session() rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first() @@ -324,7 +319,7 @@ class Release(Plugin): rls.info.append(rls_info) db.commit() - log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label) + log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls.quality.label) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) log.info(snatch_message) fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict()) @@ -335,7 +330,7 @@ class Release(Plugin): # If renamer isn't used, mark media done if finished or release downloaded else: - if media['status_id'] == active_status.get('id'): + if media['status'] == 'active': finished = next((True for profile_type in media['profile']['types'] if profile_type['quality_id'] == rls.quality.id and profile_type['finish']), False) if finished: @@ -346,7 +341,7 @@ class Release(Plugin): # Mark media done mdia = db.query(Media).filter_by(id = media['id']).first() - mdia.status_id = done_status.get('id') + mdia.status = 'done' mdia.last_edit = int(time.time()) db.commit() @@ -364,15 +359,14 @@ class Release(Plugin): return True - def tryDownloadResult(self, results, media, quality_type, manual = False): - ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True) + def tryDownloadResult(self, results, media, quality_custom, manual = False): for rel in results: - if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and rel.get('age') <= quality_type.get('wait_for', 0): - log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), rel['name'])) + if not quality_custom.get('finish', False) and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): + log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for'), rel['name'])) continue - if rel['status_id'] in [ignored_status.get('id'), failed_status.get('id')]: + if rel['status'] in ['ignored', 'failed']: log.info('Ignored: %s', rel['name']) continue @@ -388,12 +382,10 @@ class Release(Plugin): return False - def createFromSearch(self, search_results, media, quality_type): - - available_status = fireEvent('status.get', ['available'], single = True) + def createFromSearch(self, search_results, media, quality): try: - db = get_session() + db = get_db() found_releases = [] @@ -402,45 +394,40 @@ class Release(Plugin): rel_identifier = md5(rel['url']) found_releases.append(rel_identifier) - rls = db.query(Relea).filter_by(identifier = rel_identifier).first() - if not rls: - rls = Relea( - identifier = rel_identifier, - movie_id = media.get('id'), - #media_id = media.get('id'), - quality_id = quality_type.get('quality_id'), - status_id = available_status.get('id') - ) - db.add(rls) - else: - [db.delete(old_info) for old_info in rls.info] - rls.last_edit = int(time.time()) + release = { + '_t': 'release', + 'identifier': rel_identifier, + 'media_id': media.get('_id'), + 'quality': quality.get('identifier'), + 'status': 'available', + 'last_edit': int(time.time()), + 'info': {} + } - db.commit() + try: + rls = db.get('release_identifier', rel_identifier, with_doc = True)['doc'] + except: + rls = db.insert(release) + rls.update(release) + # Update info, but filter out functions for info in rel: try: if not isinstance(rel[info], (str, unicode, int, long, float)): continue - rls_info = ReleaseInfo( - identifier = info, - value = toUnicode(rel[info]) - ) - rls.info.append(rls_info) + rls['info'][info] = toUnicode(rel[info]) except InterfaceError: log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) - db.commit() + db.update(rls) - rel['status_id'] = rls.status_id + # Update release in search_results + rel['status'] = rls.get('status') return found_releases except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return [] diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index d00eee6b..0e06a751 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -45,7 +45,7 @@ class Renamer(Plugin): addEvent('renamer.scan', self.scan) addEvent('renamer.check_snatched', self.checkSnatched) - addEvent('app.load2', self.scan) + addEvent('app.load', self.scan) addEvent('app.load', self.setCrons) # Enable / disable interval @@ -222,23 +222,20 @@ class Renamer(Plugin): remove_files = [] remove_releases = [] - movie_title = getTitle(group['library']) + movie_title = getTitle(group) # Add _UNKNOWN_ if no library item is connected - if not group['library'] or not movie_title: + if not group.get('info') or not movie_title: self.tagRelease(group = group, tag = 'unknown') continue # Rename the files using the library data else: - group['library'] = fireEvent('library.update.movie', identifier = group['library']['identifier'], single = True) - if not group['library']: + group['media'] = fireEvent('movie.update_info', identifier = group['media']['identifier'], single = True) + if not group['media']: log.error('Could not rename, no library item to work with: %s', group_identifier) continue - library = group['library'] - library_ent = db.query(Library).filter_by(identifier = group['library']['identifier']).first() - - movie_title = getTitle(library) + movie_title = getTitle(group['media']) # Overwrite destination when set in category destination = to_folder @@ -419,19 +416,19 @@ class Renamer(Plugin): # Add it to the wanted list before we continue if len(library_ent.movies) == 0: profile = db.query(Profile).filter_by(core = True, label = group['meta_data']['quality']['label']).first() - fireEvent('movie.add', params = {'identifier': group['library']['identifier'], 'profile_id': profile.id}, search_after = False) + fireEvent('movie.add', params = {'identifier': group['identifier'], 'profile_id': profile.id}, search_after = False) db.expire_all() - library_ent = db.query(Library).filter_by(identifier = group['library']['identifier']).first() + library_ent = db.query(Library).filter_by(identifier = group['identifier']).first() for movie in library_ent.movies: # Mark movie "done" once it's found the quality with the finish check try: - if movie.status_id == active_status.get('id') and movie.profile: + if movie.get('status') == 'active' and movie.get('profile_id'): for profile_type in movie.profile.types: if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: - movie.status_id = done_status.get('id') - movie.last_edit = int(time.time()) + movie['status'] = 'done' + movie['last_edit'] = int(time.time()) db.commit() except Exception as e: log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc())) @@ -441,7 +438,7 @@ class Renamer(Plugin): for release in movie.releases: # When a release already exists - if release.status_id is done_status.get('id'): + if release.get('status') == 'done': # This is where CP removes older, lesser quality releases if release.quality.order > group['meta_data']['quality']['order']: @@ -470,7 +467,7 @@ class Renamer(Plugin): break - elif release.status_id in [snatched_status.get('id'), seeding_status.get('id')]: + elif release.get('status') in ['snatched', 'seeding']: if release_download and release_download.get('rls_id'): if release_download['rls_id'] == release.id: if release_download['status'] == 'completed': @@ -917,7 +914,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) found_release = True break else: - if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']: + if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['identifier']: log.debug('Found release by release name or imdb ID: %s', release_download['name']) found_release = True break @@ -951,7 +948,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif release_download['status'] == 'seeding': #If linking setting is enabled, process release - if self.conf('file_action') != 'move' and not rel.status_id == 'seeding' and self.statusInfoComplete(release_download): + if self.conf('file_action') != 'move' and not rel.get('status') == 'seeding' and self.statusInfoComplete(release_download): log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio'])) # Remove the downloading tag diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 7e8fb9bb..f9ccea78 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \ splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File, Media +from couchpotato.core.settings.model import File from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info from subliminal.videos import Video @@ -417,13 +417,11 @@ class Scanner(Plugin): del group['unsorted_files'] # Determine movie - group['library'] = self.determineMovie(group, release_download = release_download) - if not group['library']: - log.error('Unable to determine movie: %s', group['identifiers']) + group['media'] = self.determineMedia(group, release_download = release_download) + if not group['media']: + log.error('Unable to determine media: %s', group['identifiers']) else: - movie = db.query(Media).filter_by(library_id = group['library']['id']).first() - group['movie_id'] = None if not movie else movie.id - db.expire_all() + group['identifier'] = group['media']['identifier'] processed_movies[identifier] = group @@ -554,7 +552,7 @@ class Scanner(Plugin): return detected_languages - def determineMovie(self, group, release_download = None): + def determineMedia(self, group, release_download = None): # Get imdb id from downloader imdb_id = release_download and release_download.get('imdb_id') diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index 30e7baca..e6fef253 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -24,11 +24,11 @@ class Score(Plugin): try: preferred_words = removeDuplicate(preferred_words + splitString(movie['category']['preferred'].lower())) except: pass - score = nameScore(toUnicode(nzb['name']), movie['library']['year'], preferred_words) + score = nameScore(toUnicode(nzb['name']), movie['info']['year'], preferred_words) - for movie_title in movie['library']['titles']: - score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title'])) - score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title['title'])) + for movie_title in movie['info']['titles']: + score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title)) + score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title)) score += sizeScore(nzb['size']) @@ -44,7 +44,7 @@ class Score(Plugin): score += providerScore(nzb['provider']) # Duplicates in name - score += duplicateScore(nzb['name'], getTitle(movie['library'])) + score += duplicateScore(nzb['name'], getTitle(movie)) # Merge global and category ignored_words = splitString(Env.setting('ignored_words', section = 'searcher').lower()) @@ -52,7 +52,7 @@ class Score(Plugin): except: pass # Partial ignored words - score += partialIgnoredScore(nzb['name'], getTitle(movie['library']), ignored_words) + score += partialIgnoredScore(nzb['name'], getTitle(movie), ignored_words) # Ignore single downloads from multipart score += halfMultipartScore(nzb['name']) diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index b8d9a06b..a3b1d13c 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -25,14 +25,13 @@ class Subtitle(Plugin): db = get_session() library = db.query(Library).all() - done_status = fireEvent('status.get', 'done', single = True) for movie in library.movies: for release in movie.releases: # get releases and their movie files - if release.status_id is done_status.get('id'): + if release.get('status') == 'done': files = [] for file in release.files.filter(FileType.status.has(identifier = 'movie')).all(): diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py index ba040058..36084066 100644 --- a/couchpotato/core/plugins/trailer/main.py +++ b/couchpotato/core/plugins/trailer/main.py @@ -18,7 +18,7 @@ class Trailer(Plugin): trailers = fireEvent('trailer.search', group = group, merge = True) if not trailers or trailers == []: - log.info('No trailers found for: %s', getTitle(group['library'])) + log.info('No trailers found for: %s', getTitle(group)) return False for trailer in trailers.get(self.conf('quality'), []): diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 93e0900f..c772be47 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -199,7 +199,7 @@ class YarrProvider(Provider): self._search(movie, quality, results) # Search possible titles else: - for title in possibleTitles(getTitle(movie['library'])): + for title in possibleTitles(getTitle(movie)): self._searchOnTitle(title, movie, quality, results) return results diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index cefecb74..09d19258 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -1,9 +1,9 @@ -from couchpotato import get_session -from couchpotato.core.event import addEvent, fireEvent +from CodernityDB.database import RecordNotFound +from couchpotato import get_db +from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library import copy import traceback @@ -86,25 +86,26 @@ class MovieResultModifier(Plugin): } # Add release info from current library - db = get_session() + db = get_db() try: - l = db.query(Library).filter_by(identifier = imdb).first() - if l: - # Statuses - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) + media = None + try: + media = db.get('media', imdb, with_doc = True)['doc'] + except RecordNotFound: + pass - for movie in l.movies: - if movie.status_id == active_status['id']: - temp['in_wanted'] = fireEvent('media.get', movie.id, single = True) + if media: - for release in movie.releases: - if release.status_id == done_status['id']: - temp['in_library'] = fireEvent('media.get', movie.id, single = True) + if media.get('status') == 'active': + temp['in_wanted'] = media + + for release in db.get_many('release', media.get('_id'), with_doc = True): + if release.get('status') == 'done': + temp['in_library'] = media except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) - pass #db.close() return temp def checkLibrary(self, result): diff --git a/couchpotato/core/providers/info/couchpotatoapi/main.py b/couchpotato/core/providers/info/couchpotatoapi/main.py index 848cbf05..a07eb135 100644 --- a/couchpotato/core/providers/info/couchpotatoapi/main.py +++ b/couchpotato/core/providers/info/couchpotatoapi/main.py @@ -26,9 +26,11 @@ class CouchPotatoApi(MovieProvider): def __init__(self): addEvent('movie.info', self.getInfo, priority = 1) + addEvent('movie.info.release_date', self.getReleaseDate) + addEvent('info.search', self.search, priority = 1) addEvent('movie.search', self.search, priority = 1) - addEvent('movie.release_date', self.getReleaseDate) + addEvent('movie.suggest', self.getSuggestions) addEvent('movie.is_movie', self.isMovie) diff --git a/couchpotato/core/providers/info/themoviedb/main.py b/couchpotato/core/providers/info/themoviedb/main.py index d301db2b..1b706b52 100644 --- a/couchpotato/core/providers/info/themoviedb/main.py +++ b/couchpotato/core/providers/info/themoviedb/main.py @@ -12,8 +12,6 @@ log = CPLog(__name__) class TheMovieDb(MovieProvider): def __init__(self): - #addEvent('info.search', self.search, priority = 2) - #addEvent('movie.search', self.search, priority = 2) addEvent('movie.info', self.getInfo, priority = 2) addEvent('movie.info_by_tmdb', self.getInfo) diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 3f336be4..0d8f08ef 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -25,8 +25,7 @@ class MetaDataBase(Plugin): # Update library to get latest info try: - updated_library = fireEvent('library.update.movie', group['library']['identifier'], extended = True, single = True) - group['library'] = mergeDicts(group['library'], updated_library) + group['media'] = fireEvent('movie.update_info', group['media']['identifier'], extended = True, single = True) except: log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) @@ -34,7 +33,7 @@ class MetaDataBase(Plugin): meta_name = os.path.basename(root_name) root = os.path.dirname(root_name) - movie_info = group['library'].get('info') + movie_info = group['media'].get('info') for file_type in ['nfo', 'thumbnail', 'fanart']: try: @@ -96,13 +95,13 @@ class MetaDataBase(Plugin): break # See if it is in current files - for cur_file in data['library'].get('files', []): + for cur_file in data.get('files', []): if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')): return cur_file.get('path') # Download using existing info try: - images = data['library']['info']['images'][wanted_file_type] + images = data['info']['images'][wanted_file_type] file_path = fireEvent('file.download', url = images[0], single = True) return file_path except: diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py index 267b2822..90cab8f3 100644 --- a/couchpotato/core/providers/metadata/xbmc/main.py +++ b/couchpotato/core/providers/metadata/xbmc/main.py @@ -30,21 +30,21 @@ class XBMC(MetaDataBase): # return imdb url only if self.conf('meta_url_only'): - return 'http://www.imdb.com/title/%s/' % toUnicode(data['library']['identifier']) + return 'http://www.imdb.com/title/%s/' % toUnicode(data['identifier']) nfoxml = Element('movie') # Title try: el = SubElement(nfoxml, 'title') - el.text = toUnicode(getTitle(data['library'])) + el.text = toUnicode(getTitle(data)) except: pass # IMDB id try: el = SubElement(nfoxml, 'id') - el.text = toUnicode(data['library']['identifier']) + el.text = toUnicode(data['identifier']) except: pass diff --git a/couchpotato/core/providers/nzb/binsearch/main.py b/couchpotato/core/providers/nzb/binsearch/main.py index c54dd435..60007d5a 100644 --- a/couchpotato/core/providers/nzb/binsearch/main.py +++ b/couchpotato/core/providers/nzb/binsearch/main.py @@ -23,7 +23,7 @@ class BinSearch(NZBProvider): def _search(self, movie, quality, results): arguments = tryUrlencode({ - 'q': movie['library']['identifier'], + 'q': movie['identifier'], 'm': 'n', 'max': 400, 'adv_age': Env.setting('retention', 'nzb'), diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index deadaa1c..c6830b79 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -43,7 +43,7 @@ class Newznab(NZBProvider, RSS): def _searchOnHost(self, host, movie, quality, results): arguments = tryUrlencode({ - 'imdbid': movie['library']['identifier'].replace('tt', ''), + 'imdbid': movie['identifier'].replace('tt', ''), 'apikey': host['api_key'], 'extended': 1 }) + ('&%s' % host['custom_tag'] if host.get('custom_tag') else '') diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index 778cdbcd..5d027a5a 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -20,7 +20,7 @@ class NZBClub(NZBProvider, RSS): def _searchOnTitle(self, title, movie, quality, results): - q = '"%s %s"' % (title, movie['library']['year']) + q = '"%s %s"' % (title, movie['info']['year']) params = tryUrlencode({ 'q': q, diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py index a143c199..d29d94ff 100644 --- a/couchpotato/core/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/providers/nzb/nzbindex/main.py @@ -23,7 +23,7 @@ class NzbIndex(NZBProvider, RSS): def _searchOnTitle(self, title, movie, quality, results): - q = '"%s %s" | "%s (%s)"' % (title, movie['library']['year'], title, movie['library']['year']) + q = '"%s %s" | "%s (%s)"' % (title, movie['info']['year'], title, movie['info']['year']) arguments = tryUrlencode({ 'q': q, 'age': Env.setting('retention', 'nzb'), diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/providers/nzb/omgwtfnzbs/main.py index 93925752..73254632 100644 --- a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py +++ b/couchpotato/core/providers/nzb/omgwtfnzbs/main.py @@ -37,7 +37,7 @@ class OMGWTFNZBs(NZBProvider, RSS): def _searchOnTitle(self, title, movie, quality, results): - q = '%s %s' % (title, movie['library']['year']) + q = '%s %s' % (title, movie['info']['year']) params = tryUrlencode({ 'search': q, 'catid': ','.join([str(x) for x in self.getCatId(quality['identifier'])]), diff --git a/couchpotato/core/providers/torrent/awesomehd/main.py b/couchpotato/core/providers/torrent/awesomehd/main.py index ca6a30df..cc26030c 100644 --- a/couchpotato/core/providers/torrent/awesomehd/main.py +++ b/couchpotato/core/providers/torrent/awesomehd/main.py @@ -20,7 +20,7 @@ class AwesomeHD(TorrentProvider): def _search(self, movie, quality, results): - data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal'))) + data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['identifier'], self.conf('only_internal'))) if data: try: diff --git a/couchpotato/core/providers/torrent/bithdtv/main.py b/couchpotato/core/providers/torrent/bithdtv/main.py index 90117de4..259de6bc 100644 --- a/couchpotato/core/providers/torrent/bithdtv/main.py +++ b/couchpotato/core/providers/torrent/bithdtv/main.py @@ -26,7 +26,7 @@ class BiTHDTV(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): arguments = tryUrlencode({ - 'search': '%s %s' % (title.replace(':', ''), movie['library']['year']), + 'search': '%s %s' % (title.replace(':', ''), movie['info']['year']), 'cat': self.cat_id_movies }) diff --git a/couchpotato/core/providers/torrent/bitsoup/main.py b/couchpotato/core/providers/torrent/bitsoup/main.py index a709c5c1..c4035635 100644 --- a/couchpotato/core/providers/torrent/bitsoup/main.py +++ b/couchpotato/core/providers/torrent/bitsoup/main.py @@ -22,7 +22,7 @@ class Bitsoup(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - q = '"%s" %s' % (simplifyString(title), movie['library']['year']) + q = '"%s" %s' % (simplifyString(title), movie['info']['year']) arguments = tryUrlencode({ 'search': q, }) diff --git a/couchpotato/core/providers/torrent/hdbits/main.py b/couchpotato/core/providers/torrent/hdbits/main.py index ce17bbac..461745b9 100644 --- a/couchpotato/core/providers/torrent/hdbits/main.py +++ b/couchpotato/core/providers/torrent/hdbits/main.py @@ -43,7 +43,7 @@ class HDBits(TorrentProvider): def _search(self, movie, quality, results): - match = re.match(r'tt(\d{7})', movie['library']['identifier']) + match = re.match(r'tt(\d{7})', movie['identifier']) data = self._post_query(imdb = {'id': match.group(1)}) diff --git a/couchpotato/core/providers/torrent/ilovetorrents/main.py b/couchpotato/core/providers/torrent/ilovetorrents/main.py index 6d56ea48..74bf0c40 100644 --- a/couchpotato/core/providers/torrent/ilovetorrents/main.py +++ b/couchpotato/core/providers/torrent/ilovetorrents/main.py @@ -38,7 +38,7 @@ class ILoveTorrents(TorrentProvider): while page < total_pages: - movieTitle = tryUrlencode('"%s" %s' % (title, movie['library']['year'])) + movieTitle = tryUrlencode('"%s" %s' % (title, movie['info']['year'])) search_url = self.urls['search'] % (movieTitle, page, cats[0]) page += 1 diff --git a/couchpotato/core/providers/torrent/iptorrents/main.py b/couchpotato/core/providers/torrent/iptorrents/main.py index 35ce25c0..b4e038ca 100644 --- a/couchpotato/core/providers/torrent/iptorrents/main.py +++ b/couchpotato/core/providers/torrent/iptorrents/main.py @@ -36,7 +36,7 @@ class IPTorrents(TorrentProvider): current_page = 1 while current_page <= pages and not self.shuttingDown(): - url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page) + url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), current_page) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py index f96e9812..8752348e 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py @@ -36,7 +36,7 @@ class KickAssTorrents(TorrentMagnetProvider): def _search(self, movie, quality, results): - data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', movie['library']['identifier'].replace('tt', ''))) + data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', movie['identifier'].replace('tt', ''))) if data: diff --git a/couchpotato/core/providers/torrent/passthepopcorn/main.py b/couchpotato/core/providers/torrent/passthepopcorn/main.py index 66cad33c..21bfa72e 100644 --- a/couchpotato/core/providers/torrent/passthepopcorn/main.py +++ b/couchpotato/core/providers/torrent/passthepopcorn/main.py @@ -56,13 +56,13 @@ class PassThePopcorn(TorrentProvider): def _search(self, movie, quality, results): - movie_title = getTitle(movie['library']) + movie_title = getTitle(movie) quality_id = quality['identifier'] params = mergeDicts(self.quality_search_params[quality_id].copy(), { 'order_by': 'relevance', 'order_way': 'descending', - 'searchstr': movie['library']['identifier'] + 'searchstr': movie['identifier'] }) url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) diff --git a/couchpotato/core/providers/torrent/publichd/main.py b/couchpotato/core/providers/torrent/publichd/main.py index b7c32fba..b200b1fd 100644 --- a/couchpotato/core/providers/torrent/publichd/main.py +++ b/couchpotato/core/providers/torrent/publichd/main.py @@ -30,7 +30,7 @@ class PublicHD(TorrentMagnetProvider): params = tryUrlencode({ 'page':'torrents', - 'search': '%s %s' % (title, movie['library']['year']), + 'search': '%s %s' % (title, movie['info']['year']), 'active': 1, }) diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/providers/torrent/sceneaccess/main.py index a43c49bb..dcf0b351 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/providers/torrent/sceneaccess/main.py @@ -39,7 +39,7 @@ class SceneAccess(TorrentProvider): ) arguments = tryUrlencode({ - 'search': movie['library']['identifier'], + 'search': movie['identifier'], 'method': 1, }) url = "%s&%s" % (url, arguments) diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py index f8b77787..8ccefeb1 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py @@ -50,7 +50,7 @@ class ThePirateBay(TorrentMagnetProvider): while page < total_pages: - search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['library']['year'])), page, ','.join(str(x) for x in cats)) + search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['info']['year'])), page, ','.join(str(x) for x in cats)) page += 1 data = self.getHTMLData(search_url) diff --git a/couchpotato/core/providers/torrent/torrentbytes/main.py b/couchpotato/core/providers/torrent/torrentbytes/main.py index 91939e78..109e50f7 100644 --- a/couchpotato/core/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/providers/torrent/torrentbytes/main.py @@ -34,7 +34,7 @@ class TorrentBytes(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0]) + url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality['identifier'])[0]) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/providers/torrent/torrentday/main.py b/couchpotato/core/providers/torrent/torrentday/main.py index 6d343234..83177a22 100644 --- a/couchpotato/core/providers/torrent/torrentday/main.py +++ b/couchpotato/core/providers/torrent/torrentday/main.py @@ -27,7 +27,7 @@ class TorrentDay(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - q = '"%s %s"' % (title, movie['library']['year']) + q = '"%s %s"' % (title, movie['info']['year']) data = { '/browse.php?': None, diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py index ea6158df..80c426f8 100644 --- a/couchpotato/core/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/providers/torrent/torrentleech/main.py @@ -35,7 +35,7 @@ class TorrentLeech(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0]) + url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality['identifier'])[0]) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/providers/torrent/torrentpotato/main.py index eaaf8d2c..a58fd61c 100644 --- a/couchpotato/core/providers/torrent/torrentpotato/main.py +++ b/couchpotato/core/providers/torrent/torrentpotato/main.py @@ -35,7 +35,7 @@ class TorrentPotato(TorrentProvider): arguments = tryUrlencode({ 'user': host['name'], 'passkey': host['pass_key'], - 'imdbid': movie['library']['identifier'] + 'imdbid': movie['identifier'] }) url = '%s?%s' % (host['host'], arguments) diff --git a/couchpotato/core/providers/torrent/torrentshack/main.py b/couchpotato/core/providers/torrent/torrentshack/main.py index f0cd5997..6c6dc3b8 100644 --- a/couchpotato/core/providers/torrent/torrentshack/main.py +++ b/couchpotato/core/providers/torrent/torrentshack/main.py @@ -34,7 +34,7 @@ class TorrentShack(TorrentProvider): scene_only = '1' if self.conf('scene_only') else '' - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), scene_only, self.getCatId(quality['identifier'])[0]) + url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), scene_only, self.getCatId(quality['identifier'])[0]) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py index 6deaf7bb..00d67eb0 100644 --- a/couchpotato/core/providers/torrent/yify/main.py +++ b/couchpotato/core/providers/torrent/yify/main.py @@ -33,7 +33,7 @@ class Yify(TorrentMagnetProvider): def _search(self, movie, quality, results): - search_url = self.urls['search'] % (self.getDomain(), movie['library']['identifier'], quality['identifier']) + search_url = self.urls['search'] % (self.getDomain(), movie['identifier'], quality['identifier']) data = self.getJsonData(search_url) diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py index cba7609f..6af83ac7 100644 --- a/couchpotato/core/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/providers/trailer/hdtrailers/main.py @@ -20,11 +20,11 @@ class HDTrailers(TrailerProvider): def search(self, group): - movie_name = getTitle(group['library']) + movie_name = getTitle(group) url = self.urls['api'] % self.movieUrlName(movie_name) try: - data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url, show_error = False) + data = self.getCache('hdtrailers.%s' % group['identifier'], url, show_error = False) except HTTPError: log.debug('No page found for: %s', movie_name) data = None @@ -50,11 +50,11 @@ class HDTrailers(TrailerProvider): def findViaAlternative(self, group): results = {'480p':[], '720p':[], '1080p':[]} - movie_name = getTitle(group['library']) + movie_name = getTitle(group) url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name})) try: - data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url, show_error = False) + data = self.getCache('hdtrailers.alt.%s' % group['identifier'], url, show_error = False) except HTTPError: log.debug('No alternative page found for: %s', movie_name) data = None diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index bbbc9381..fcea04f5 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -5,7 +5,6 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat from couchpotato.core.settings.index import PropertyIndex -from couchpotato.core.settings.model import Properties import ConfigParser @@ -247,7 +246,7 @@ class Settings(object): db.update(p['doc']) except: db.insert({ - 'type': 'property', + '_t': 'property', 'identifier': identifier, 'value': toUnicode(value), }) diff --git a/couchpotato/core/settings/index.py b/couchpotato/core/settings/index.py index 45d366eb..68ec52d8 100644 --- a/couchpotato/core/settings/index.py +++ b/couchpotato/core/settings/index.py @@ -5,12 +5,12 @@ from hashlib import md5 class PropertyIndex(HashIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' + kwargs['key_format'] = '32s' super(PropertyIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).digest() + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('type') == 'property': - return md5(data['identifier']).digest(), None + if data.get('_t') == 'property': + return md5(data['identifier']).hexdigest(), None diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 744d425b..b2ea606b 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -223,9 +223,9 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En loader.run() # Fill database with needed stuff + fireEvent('database.setup') if not db_exists: fireEvent('app.initialize', in_order = True) - fireEvent('database.setup') # Go go go! from tornado.ioloop import IOLoop @@ -234,7 +234,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Some logging and fire load event try: log.info('Starting server on port %(port)s', config) except: pass - fireEventAsync('app.load2') + fireEventAsync('app.load') if config['ssl_cert'] and config['ssl_key']: server = HTTPServer(application, no_keep_alive = True, ssl_options = { diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index af257454..b4328001 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -69,8 +69,6 @@ 'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }} }); - Status.setup({{ json_encode(fireEvent('status.all', single = True)) }}); - CategoryList.setup({{ json_encode(fireEvent('category.all', single = True)) }}); App.setup({ From a609b401c421b4c2d9aece2a414443a531002d33 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Feb 2014 15:11:06 +0100 Subject: [PATCH 008/468] nosql --- README.md | 6 +- couchpotato/core/media/_base/media/index.py | 102 ++++++- couchpotato/core/media/_base/media/main.py | 267 ++++++++---------- couchpotato/core/media/movie/_base/main.py | 50 ++-- .../core/media/movie/_base/static/list.js | 12 +- .../media/movie/_base/static/movie.actions.js | 20 +- .../core/media/movie/_base/static/movie.js | 15 +- couchpotato/core/media/movie/searcher/main.py | 2 +- .../notifications/core/static/notification.js | 2 +- couchpotato/core/plugins/dashboard/main.py | 92 ++---- couchpotato/core/plugins/file/static/file.js | 86 ------ couchpotato/core/plugins/manage/main.py | 2 +- couchpotato/core/plugins/profile/main.py | 3 +- .../core/plugins/profile/static/profile.js | 32 ++- .../core/plugins/quality/static/quality.js | 6 +- couchpotato/core/plugins/release/index.py | 10 +- couchpotato/core/plugins/renamer/main.py | 2 +- couchpotato/runner.py | 2 +- 18 files changed, 316 insertions(+), 395 deletions(-) delete mode 100644 couchpotato/core/plugins/file/static/file.js diff --git a/README.md b/README.md index e38ea0e8..19616889 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -CouchPotato Server +CouchPotato ===== CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours. @@ -7,7 +7,7 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s ## Running from Source -CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also. +CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed. Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details: @@ -40,7 +40,7 @@ Linux (ubuntu / debian): * Make it executable. `sudo chmod +x /etc/init.d/couchpotato` * Add it to defaults. `sudo update-rc.d couchpotato defaults` * Open your browser and go to: `http://localhost:5050/` - + FreeBSD : diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 97b16066..d83c347c 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -1,7 +1,9 @@ from itertools import izip +from string import ascii_letters from CodernityDB.hash_index import HashIndex from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex from hashlib import md5 +from couchpotato.core.helpers.encoding import toUnicode, simplifyString class MediaIMDBIndex(HashIndex): @@ -22,13 +24,13 @@ class MediaIMDBIndex(HashIndex): return db.get('id', media_id) - def run_with_status(self, db, status = []): + def run_with_status(self, db, status = [], with_doc = True): status = list(status if isinstance(status, (list, tuple)) else [status]) for s in status: - for ms in db.get_many('media_status', s, with_doc = True): - yield ms['doc'] + for ms in db.get_many('media_status', s, with_doc = with_doc): + yield ms['doc'] if with_doc else ms class MediaStatusIndex(TreeBasedIndex): @@ -45,31 +47,46 @@ class MediaStatusIndex(TreeBasedIndex): return md5(data.get('status')).hexdigest(), None -class TitleIndex(MultiTreeBasedIndex): - - custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex -from itertools import izip""" +class MediaTypeIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' - super(TitleIndex, self).__init__(*args, **kwargs) + super(MediaTypeIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).hexdigest() + + def make_key_value(self, data): + if data.get('_t') == 'media' and data.get('type'): + return md5(data.get('type')).hexdigest(), None + + +class TitleSearchIndex(MultiTreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex +from itertools import izip +from couchpotato.core.helpers.encoding import simplifyString""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(TitleSearchIndex, self).__init__(*args, **kwargs) self.__l = kwargs.get('w_len', 2) def make_key_value(self, data): - if data.get('_t') == 'title' and len(data.get('title', '')) > 0: + if data.get('_t') == 'media' and len(data.get('title', '')) > 0: out = set() - title = data.get('title').lower() + title = str(simplifyString(data.get('title').lower())) l = self.__l max_l = len(title) for x in xrange(l - 1, max_l): m = (title, ) for y in xrange(0, x): m += (title[y + 1:],) - out.update(set(''.join(x).rjust(32, '_').lower() for x in izip(*m))) #ignore import error + out.update(set(''.join(x).rjust(32, '_').lower() for x in izip(*m))) - return out, {'media_id': data.get('media_id')} + return out, None def make_key(self, key): return key.rjust(32, '_').lower() @@ -87,3 +104,64 @@ class YearIndex(TreeBasedIndex): def make_key_value(self, data): if data.get('_t') == 'media' and data.get('year') is not None: return data['year'], None + + +class TitleIndex(TreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import TreeBasedIndex +from string import ascii_letters +from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(TitleIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return self.simplify(key) + + def make_key_value(self, data): + if data.get('_t') == 'media' and data.get('title') is not None: + return self.simplify(data['title']), None + + def simplify(self, title): + + title = toUnicode(title) + + nr_prefix = '' if title[0] in ascii_letters else '#' + title = simplifyString(title) + + for prefix in ['the ']: + if prefix == title[:len(prefix)]: + title = title[len(prefix):] + break + + return str(nr_prefix + title).ljust(32, '_')[:32] + + +class StartsWithIndex(TreeBasedIndex): + + custom_header = """from CodernityDB.tree_index import TreeBasedIndex +from string import ascii_letters +from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '1s' + super(StartsWithIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return self.first(key) + + def make_key_value(self, data): + if data.get('_t') == 'media' and data.get('title') is not None: + return self.first(data['title']), None + + def first(self, title): + title = toUnicode(title) + title = simplifyString(title) + + for prefix in ['the ']: + if prefix == title[:len(prefix)]: + title = title[len(prefix):] + break + + return str(title[0] if title[0] in ascii_letters else '#').lower() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index b4b0cc18..17e443b3 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,14 +1,13 @@ import traceback -import time from couchpotato import get_session, tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIMDBIndex, TitleIndex, MediaStatusIndex, YearIndex -from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ - Media +from .index import MediaIMDBIndex, MediaStatusIndex, YearIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex +from couchpotato.core.settings.model import Media from string import ascii_lowercase log = CPLog(__name__) @@ -61,10 +60,10 @@ class MediaPlugin(MediaBase): addEvent('database.setup', self.databaseSetup) - addEvent('app.load', self.addSingleRefreshView) - addEvent('app.load', self.addSingleListView) - addEvent('app.load', self.addSingleCharView) - addEvent('app.load', self.addSingleDeleteView) + addEvent('app.load', self.addSingleRefreshView, priority = 100) + addEvent('app.load', self.addSingleListView, priority = 100) + addEvent('app.load', self.addSingleCharView, priority = 100) + addEvent('app.load', self.addSingleDeleteView, priority = 100) addEvent('media.get', self.get) addEvent('media.list', self.list) @@ -84,10 +83,10 @@ class MediaPlugin(MediaBase): # Title index try: - db.add_index(TitleIndex(db.path, 'media_title')) + db.add_index(TitleSearchIndex(db.path, 'media_search_title')) except: log.debug('Index already exists') - db.edit_index(TitleIndex(db.path, 'media_title')) + db.edit_index(TitleSearchIndex(db.path, 'media_search_title')) # Status index try: @@ -96,11 +95,31 @@ class MediaPlugin(MediaBase): log.debug('Index already exists') db.edit_index(MediaStatusIndex(db.path, 'media_status')) - # Year index - try: db.add_index(YearIndex(db.path, 'year')) + # Type index + try: + db.add_index(MediaTypeIndex(db.path, 'media_by_type')) except: log.debug('Index already exists') - db.edit_index(YearIndex(db.path, 'year')) + db.edit_index(MediaTypeIndex(db.path, 'media_by_type')) + + # Year index + try: db.add_index(YearIndex(db.path, 'media_year')) + except: + log.debug('Index already exists') + db.edit_index(YearIndex(db.path, 'media_year')) + + # Title index + try: db.add_index(TitleIndex(db.path, 'media_title')) + except: + log.debug('Index already exists') + db.edit_index(TitleIndex(db.path, 'media_title')) + + # Startswith index + try: db.add_index(StartsWithIndex(db.path, 'media_startswith')) + except: + log.debug('Index already exists') + db.edit_index(StartsWithIndex(db.path, 'media_startswith')) + def refresh(self, id = '', **kwargs): handlers = [] @@ -112,7 +131,7 @@ class MediaPlugin(MediaBase): if refresh_handler: handlers.append(refresh_handler) - fireEvent('notify.frontend', type = 'media.busy', data = {'id': [tryInt(x) for x in ids]}) + fireEvent('notify.frontend', type = 'media.busy', data = {'id': ids}) fireEventAsync('schedule.queue', handlers = handlers) return { @@ -124,8 +143,9 @@ class MediaPlugin(MediaBase): try: media = get_db().get('id', media_id) + default_title = getTitle(media) - event = 'library.update.%s' % media.get('type') + event = '%s.update_info' % media.get('type') def handler(): fireEvent(event, identifier = media.get('identifier'), default_title = default_title, on_complete = self.createOnComplete(media_id)) @@ -172,8 +192,6 @@ class MediaPlugin(MediaBase): db = get_db() - start = time.time() - # Make a list from string if status and not isinstance(status, (list, tuple)): status = [status] @@ -183,125 +201,73 @@ class MediaPlugin(MediaBase): types = [types] # query media ids - all_media_ids = set([x['_id'] for x in db.all('media')]) + if types: + all_media_ids = set() + for media_type in types: + all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)])) + else: + all_media_ids = set([x['_id'] for x in db.all('media')]) + media_ids = all_media_ids filter_by = {} # Filter on movie status if status and len(status) > 0: filter_by['media_status'] = set() - for media_status in db.run('media', 'with_status', status): + for media_status in db.run('media', 'with_status', status, with_doc = False): filter_by['media_status'].add(media_status.get('_id')) # Filter on release status if release_status and len(release_status) > 0: filter_by['release_status'] = set() - for release_status in db.run('release', 'with_status', release_status): + for release_status in db.run('release', 'with_status', release_status, with_doc = False): filter_by['release_status'].add(release_status.get('media_id')) + # Add search filters + if starts_with: + filter_by['starts_with'] = set() + starts_with = toUnicode(starts_with.lower())[0] + starts_with = starts_with if starts_with in ascii_lowercase else '#' + filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)] + + # Filter with search query + if search: + filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)] + # Filter by combining ids for x in filter_by: - media_ids = media_ids & filter_by[x] - - # Filter on type - # if types and len(types) > 0: - # try: q = q.filter(Media.type.in_(types)) - # except: pass - - # Only join when searching / ordering - # if starts_with or search or order != 'release_order': - # q = q.join(Media.library, Library.titles) \ - # .filter(LibraryTitle.default == True) - - # Add search filters - # filter_or = [] - # if starts_with: - # starts_with = toUnicode(starts_with.lower()) - # if starts_with in ascii_lowercase: - # filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) - # else: - # ignore = [] - # for letter in ascii_lowercase: - # ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) - # filter_or.append(not_(or_(*ignore))) - - if search: - search_ids = db.get_many('media_title', search) - for search_id in search_ids: - print search_id - - #if len(filter_or) > 0: - # pass #q = q.filter(or_(*filter_or)) + media_ids = [n for n in media_ids if n in filter_by[x]] total_count = len(media_ids) if total_count == 0: return 0, [] - # if order == 'release_order': - # q = q.order_by(desc(Release.last_edit)) - # else: - # q = q.order_by(asc(LibraryTitle.simple_title)) + if limit_offset: + splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset + limit = tryInt(splt[0]) + offset = tryInt(0 if len(splt) is 1 else splt[1]) + start = offset * limit + end = start + limit + media_ids = media_ids[start:end] - # if limit_offset: - # splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset - # limit = splt[0] - # offset = 0 if len(splt) is 1 else splt[1] - # q = q.limit(limit).offset(offset) - - # Get all media_ids in sorted order - # media_ids = [m.id for m in q.all()] - - # List release statuses - # releases = db.query(Release) \ - # .filter(Release.movie_id.in_(media_ids)) \ - # .all() - - # release_statuses = dict((m, set()) for m in media_ids) - # releases_count = dict((m, 0) for m in media_ids) - # for release in releases: - # release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) - # releases_count[release.movie_id] += 1 - - # Get main movie data - # q2 = db.query(Media) \ - # .options(joinedload_all('library.titles')) \ - # .options(joinedload_all('library.files')) \ - # .options(joinedload_all('status')) \ - # .options(joinedload_all('files')) - - # q2 = q2.filter(Media.id.in_(media_ids)) - - # results = q2.all() - - # Create dict by movie id - # medias = {} - # for media in media_ids: - # movie_dict[movie.id] = movie - - # List movies based on media_ids order + # List movies based on title order medias = [] - for media_id in media_ids: + for m in db.all('media_title'): + media_id = m['_id'] + if media_id not in media_ids: continue - media = db.run('media', 'to_dict', media_id, { - 'library': {'titles': {}, 'files': {}}, - 'files': {}, - }) + media = db.run('media', 'to_dict', media_id) media['releases'] = [] for r in db.get_many('release', media_id, with_doc = True): - media['releases'].append(r) + media['releases'].append(r['doc']) # Merge releases with movie dict medias.append(media) - # medias.append(mergeDicts(movie_dict[media_id].to_dict({ - # 'library': {'titles': {}, 'files': {}}, - # 'files': {}, - # }), { - # 'releases': releases, - # 'releases_count': releases_count.get(media_id), - # })) - print time.time() - start + # remove from media ids + media_ids.remove(media_id) + if len(media_ids) == 0: break return total_count, medias @@ -341,63 +307,52 @@ class MediaPlugin(MediaBase): def availableChars(self, types = None, status = None, release_status = None): - types = types or [] - status = status or [] - release_status = release_status or [] - - db = get_session() + db = get_db() # Make a list from string - if not isinstance(status, (list, tuple)): + if status and not isinstance(status, (list, tuple)): status = [status] if release_status and not isinstance(release_status, (list, tuple)): release_status = [release_status] if types and not isinstance(types, (list, tuple)): types = [types] - q = db.query(Media) + # query media ids + if types: + all_media_ids = set() + for media_type in types: + all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)])) + else: + all_media_ids = set([x['_id'] for x in db.all('media')]) + + media_ids = all_media_ids + filter_by = {} # Filter on movie status if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) + filter_by['media_status'] = set() + for media_status in db.run('media', 'with_status', status, with_doc = False): + filter_by['media_status'].add(media_status.get('_id')) # Filter on release status if release_status and len(release_status) > 0: + filter_by['release_status'] = set() + for release_status in db.run('release', 'with_status', release_status, with_doc = False): + filter_by['release_status'].add(release_status.get('media_id')) - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.join(Media.releases) \ - .filter(Release.status_id.in_(statuses)) - - # Filter on type - if types and len(types) > 0: - try: q = q.filter(Media.type.in_(types)) - except: pass - - q = q.join(Library, LibraryTitle) \ - .with_entities(LibraryTitle.simple_title) \ - .filter(LibraryTitle.default == True) - - titles = q.all() + # Filter by combining ids + for x in filter_by: + media_ids = [n for n in media_ids if n in filter_by[x]] chars = set() - for title in titles: - try: - char = title[0][0] - char = char if char in ascii_lowercase else '#' - chars.add(str(char)) - except: - log.error('Failed getting title for %s', title.libraries_id) + for x in db.all('media_startswith'): + if x['_id'] in media_ids: + chars.add(x['key']) if len(chars) == 25: break - pass #db.close() - return ''.join(sorted(chars)) + return list(chars) def charView(self, **kwargs): @@ -487,32 +442,30 @@ class MediaPlugin(MediaBase): def restatus(self, media_id): try: - db = get_session() + db = get_db() - m = db.query(Media).filter_by(id = media_id).first() - if not m or len(m.library.titles) == 0: - log.debug('Can\'t restatus movie, doesn\'t seem to exist.') - return False + m = db.get('id', media_id) - log.debug('Changing status for %s', m.library.titles[0].title) + log.debug('Changing status for %s', getTitle(m)) if not m['profile_id']: m['status'] = 'done' else: move_to_wanted = True - for t in m.profile.types: - for release in m.releases: - if t.quality.identifier is release.quality.identifier and (release.get('status') == 'done' and t.finish): + profile = db.get('id', m['profile_id']) + media_releases = db.get_many('release', m['_id']) + + for q_identifier in profile['qualities']: + index = profile['qualities'].index(q_identifier) + + for release in media_releases: + if q_identifier is release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): move_to_wanted = False m['status'] = 'active' if move_to_wanted else 'done' - db.commit() + db.update(m) return True except: log.error('Failed restatus: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() - diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index f35d4a8b..bb04b392 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -117,12 +117,11 @@ class MovieBase(MovieTypeBase): added = True do_search = False search_after = search_after and self.conf('search_on_add', section = 'moviesearcher') + onComplete = None + if new: - onComplete = None if search_after: onComplete = self.createOnComplete(m['_id']) - - fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title', ''), on_complete = onComplete) search_after = False elif force_readd: @@ -148,6 +147,9 @@ class MovieBase(MovieTypeBase): if added: db.update(m) + # Do full update to get images etc + fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) + # Remove releases for rel in db.run('release', 'for_media', m['_id']): if rel['status'] is 'available': @@ -205,8 +207,9 @@ class MovieBase(MovieTypeBase): # Default title if kwargs.get('default_title'): - for title in m['titles']: - title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() + m['title'] = kwargs.get('default_title') + + print m db.update(m) @@ -266,35 +269,36 @@ class MovieBase(MovieTypeBase): titles = info.get('titles', []) log.debug('Adding titles: %s', titles) - counter = 0 + # Define default title def_title = None - for title in titles: - if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): - def_title = toUnicode(title) - break - counter += 1 + if default_title: + counter = 0 + for title in titles: + if title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): + def_title = toUnicode(title) + break + counter += 1 if not def_title: def_title = toUnicode(titles[0]) - media = { - 'title': def_title, - } + media['title'] = def_title # Files images = info.get('images', []) - media['files'] = media.get('filed', []) + media['files'] = media.get('files', {}) for image_type in ['poster']: for image in images.get(image_type, []): if not isinstance(image, (str, unicode)): continue - file_path = fireEvent('file.download', url = image, single = True) - media['files'].append({ - 'type': 'image_%s' % image_type, - 'path': file_path - }) + file_type = 'image_%s' % image_type + if file_type not in media['files']: + file_path = fireEvent('file.download', url = image, single = True) + media['files']['image_%s' % image_type] = [file_path]; + + break db.update(media) @@ -306,7 +310,7 @@ class MovieBase(MovieTypeBase): def updateReleaseDate(self, media_id): """ - Update releasedate (eta) info only + Update release_date (eta) info only @param media_id: document id @return: dict, with dates dvd, theater, bluray, expires @@ -318,8 +322,8 @@ class MovieBase(MovieTypeBase): media = db.get('id', media_id) if not media.get('info'): - info_dict = self.updateInfo(media_id) - dates = info_dict.get('info', {}).get('release_date') + media = self.updateInfo(media_id) + dates = media.get('info', {}).get('release_date') else: dates = media.get('info').get('release_date') diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index 85dee2e5..a193daf5 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -59,9 +59,9 @@ var MovieList = new Class({ movieDeleted: function(notification){ var self = this; - if(self.movies_added[notification.data.id]){ + if(self.movies_added[notification.data._id]){ self.movies.each(function(movie){ - if(movie.get('id') == notification.data.id){ + if(movie.get('_id') == notification.data._id){ movie.destroy(); delete self.movies_added[notification.data.id]; self.setCounter(self.counter_count-1); @@ -288,7 +288,7 @@ var MovieList = new Class({ 'onSuccess': function(json){ available_chars = json.chars - json.chars.split('').each(function(c){ + available_chars.each(function(c){ self.letters[c.capitalize()].addClass('available') }) @@ -438,7 +438,7 @@ var MovieList = new Class({ var ids = [] self.movies.each(function(movie){ if (movie.isSelected()) - ids.include(movie.get('id')) + ids.include(movie.get('_id')) }); return ids @@ -467,7 +467,7 @@ var MovieList = new Class({ self.offset = 0; if(self.scrollspy){ - self.load_more.show(); + //self.load_more.show(); self.scrollspy.start(); } }, @@ -629,4 +629,4 @@ var MovieList = new Class({ return this.el; } -}); \ No newline at end of file +}); diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index c5186935..b52a5923 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -427,7 +427,7 @@ MA.Release = new Class({ Api.request('media.delete', { 'data': { - 'id': self.movie.get('id'), + 'id': self.movie.get('_id'), 'delete_from': 'wanted' }, 'onComplete': function(){ @@ -449,7 +449,7 @@ MA.Release = new Class({ Api.request('movie.searcher.try_next', { 'data': { - 'id': self.movie.get('id') + 'id': self.movie.get('_id') } }); @@ -604,13 +604,13 @@ MA.Edit = new Class({ ) ).inject(self.movie, 'top'); - Array.each(self.movie.data.library.titles, function(alt){ + Array.each(self.movie.data.info.titles, function(title){ new Element('option', { - 'text': alt.title + 'text': title }).inject(self.title_select); - if(alt['default']) - self.title_select.set('value', alt.title); + if(title == self.movie.data.title) + self.title_select.set('value', title); }); @@ -643,7 +643,7 @@ MA.Edit = new Class({ profiles.each(function(profile){ - var profile_id = profile.id ? profile.id : profile.data.id; + var profile_id = profile.get('_id'); new Element('option', { 'value': profile_id, @@ -666,7 +666,7 @@ MA.Edit = new Class({ Api.request('movie.edit', { 'data': { - 'id': self.movie.get('id'), + 'id': self.movie.get('_id'), 'default_title': self.title_select.get('value'), 'profile_id': self.profile_select.get('value'), 'category_id': self.category_select.get('value') @@ -706,7 +706,7 @@ MA.Refresh = new Class({ Api.request('media.refresh', { 'data': { - 'id': self.movie.get('id') + 'id': self.movie.get('_id') } }); } @@ -817,7 +817,7 @@ MA.Delete = new Class({ function(){ Api.request('media.delete', { 'data': { - 'id': self.movie.get('id'), + 'id': self.movie.get('_id'), 'delete_from': self.movie.list.options.identifier }, 'onComplete': function(){ diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index bd290dcd..7e07cd4a 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -59,7 +59,7 @@ var Movie = new Class({ if(!self.data.releases) self.data.releases = []; - self.data.releases.push({'quality_id': data.quality_id, 'status': data.status}); + self.data.releases.push({'quality_identifier': data.quality_identifier, 'status': data.status}); self.updateReleases(); } } @@ -156,7 +156,10 @@ var Movie = new Class({ } } }), - self.thumbnail = File.Select.single('poster', self.data.files || []), + self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', { + 'class': 'type_image poster', + 'src': Api.createUrl('file.cache') + (self.data.files || {})['image_poster'][0] + }): null, self.data_container = new Element('div.data.inlay.light').adopt( self.info_container = new Element('div.info').adopt( new Element('div.title').adopt( @@ -194,7 +197,7 @@ var Movie = new Class({ if(self.profile.data) self.profile.getTypes().each(function(type){ - var q = self.addQuality(type.quality_id || type.get('quality_id')); + var q = self.addQuality(type.quality_identifier || type.get('quality_identifier')); if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ q.addClass('finish'); q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') @@ -223,7 +226,7 @@ var Movie = new Class({ status = release.status; if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) - var q = self.addQuality(release.quality_id) + var q = self.addQuality(release.quality_identifier) if (q && !q.hasClass(status)){ q.addClass(status); @@ -233,10 +236,10 @@ var Movie = new Class({ }); }, - addQuality: function(quality_id){ + addQuality: function(quality_identifier){ var self = this; - var q = Quality.getQuality(quality_id); + var q = Quality.getQuality(quality_identifier); return new Element('span', { 'text': q.label, 'class': 'q_'+q.identifier + ' q_id' + q.id, diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index fdff2fd9..69615a16 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -122,7 +122,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return pre_releases = fireEvent('quality.pre_releases', single = True) - release_dates = fireEvent('movie.update_release_date', media_id = movie['_id'], merge = True) + release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True) found_releases = [] too_early_to_search = [] diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 18d09e76..f8d46645 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -147,7 +147,7 @@ var NotificationBase = new Class({ // Process data if(json){ Array.each(json.result, function(result){ - App.trigger(result.type, [result]); + App.trigger(result._t, [result]); if(result.message && result.read === undefined) self.showMessage(result.message); }) diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 10c1c61b..e2e4c792 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -1,13 +1,9 @@ -from couchpotato import get_session +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Media, Library, LibraryTitle, \ - Release -from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql.expression import asc, or_ import random as rndm import time @@ -21,26 +17,21 @@ class Dashboard(Plugin): def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs): - db = get_session() + db = get_db() now = time.time() # Get profiles first, determine pre or post theater profiles = fireEvent('profile.all', single = True) - qualities = fireEvent('quality.all', single = True) pre_releases = fireEvent('quality.pre_releases', single = True) - id_pre = {} - for quality in qualities: - id_pre[quality.get('id')] = quality.get('identifier') in pre_releases - # See what the profile contain and cache it profile_pre = {} for profile in profiles: contains = {} - for profile_type in profile.get('types', []): - contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True + for q_identifier in profile.get('qualities', []): + contains['theater' if q_identifier in pre_releases else 'dvd'] = True - profile_pre[profile.get('id')] = contains + profile_pre[profile.get('_id')] = contains # Add limit limit = 12 @@ -48,43 +39,32 @@ class Dashboard(Plugin): splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset limit = tryInt(splt[0]) - # Get all active movies - q = db.query(Media) \ - .join(Library) \ - .outerjoin(Media.releases) \ - .filter(Media.status_id == active_status.get('id')) \ - .with_entities(Media.id, Media.profile_id, Library.info, Library.year) \ - .group_by(Media.id) \ - .filter(or_(Release.id == None, Release.status_id == ignored_status.get('id'))) + # Get all active medias + active_ids = [x['_id'] for x in db.get_many('media_status', 'active')] - if not random: - q = q.join(LibraryTitle) \ - .filter(LibraryTitle.default == True) \ - .order_by(asc(LibraryTitle.simple_title)) + medias = [] + if len(active_ids) > 0: - active = q.all() - movies = [] + # Order by title or randomize + if not random: + orders_ids = db.all('media_title') + active_ids = [x['_id'] for x in orders_ids if x['_id'] in active_ids] + else: + rndm.shuffle(active_ids) - if len(active) > 0: + for media_id in active_ids: + media = db.get('id', media_id) - # Do the shuffle - if random: - rndm.shuffle(active) - - movie_ids = [] - for movie in active: - movie_id, profile_id, info, year = movie - - pp = profile_pre.get(profile_id) + pp = profile_pre.get(media['profile_id']) if not pp: continue - eta = info.get('release_date', {}) or {} + eta = media['info'].get('release_date', {}) or {} coming_soon = False # Theater quality - if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, year, single = True): + if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True): coming_soon = True - elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, year, single = True): + elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True): coming_soon = True if coming_soon: @@ -92,37 +72,15 @@ class Dashboard(Plugin): # Don't list older movies if ((not late and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or (late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))): - movie_ids.append(movie_id) + medias.append(media) - if len(movie_ids) >= limit: + if len(medias) >= limit: break - if len(movie_ids) > 0: - - # Get all movie information - movies_raw = db.query(Media) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('files')) \ - .filter(Media.id.in_(movie_ids)) \ - .all() - - # Create dict by movie id - movie_dict = {} - for movie in movies_raw: - movie_dict[movie.id] = movie - - for movie_id in movie_ids: - movies.append(movie_dict[movie_id].to_dict({ - 'library': {'titles': {}, 'files': {}}, - 'files': {}, - })) - - pass #db.close() return { 'success': True, - 'empty': len(movies) == 0, - 'movies': movies, + 'empty': len(medias) == 0, + 'movies': medias, } getLateView = getSoonView diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js deleted file mode 100644 index 6659c882..00000000 --- a/couchpotato/core/plugins/file/static/file.js +++ /dev/null @@ -1,86 +0,0 @@ -var File = new Class({ - - initialize: function(type, file){ - var self = this; - - if(!file){ - self.empty = true; - self.el = new Element('div.empty_file.'+type); - return - } - - self.data = file; - self.type = File.Type.get(file.type_id); - - self['create'+(self.type.type).capitalize()]() - - }, - - createImage: function(){ - var self = this; - - var file_name = self.data.path.replace(/^.*[\\\/]/, ''); - - self.el = new Element('div', { - 'class': 'type_image ' + self.type.identifier, - 'styles': { - 'background-image': 'url('+Api.createUrl('file.cache') + file_name+')' - } - }).adopt( - new Element('img', { - 'src': Api.createUrl('file.cache') + file_name - }) - ) - }, - - toElement: function(){ - return this.el; - } - -}); - -var FileSelect = new Class({ - - multiple: function(type, files, single){ - - var results = files.filter(function(file){ - return file.type_id == File.Type.get(type).id; - }); - - if(single) - return new File(type, results.pop()); - - return results; - - }, - - single: function(type, files){ - return this.multiple(type, files, true); - } - -}); -window.File.Select = new FileSelect(); - -var FileTypeBase = new Class({ - - setup: function(types){ - var self = this; - - self.typesById = {}; - self.typesByKey = {}; - Object.each(types, function(type){ - self.typesByKey[type.identifier] = type; - self.typesById[type.id] = type; - }); - - }, - - get: function(identifier){ - if(typeOf(identifier) == 'number') - return this.typesById[identifier] - else - return this.typesByKey[identifier] - } - -}); -window.File.Type = new FileTypeBase(); diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 86d00557..1c734b13 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -124,7 +124,7 @@ class Manage(Plugin): fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') else: - releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True) + releases = fireEvent('release.for_movie', id = done_movie.get('_id'), single = True) for release in releases: if len(release.get('files', [])) > 0: diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 8f04fe3b..90335953 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -56,8 +56,7 @@ class ProfilePlugin(Plugin): for media in medias: if media.get('profile_id') not in profile_ids: - default_profile = self.default() - media['profile_id'] = default_profile.get('id') + media['profile_id'] = profile_ids[0].get('_id') db.update(media) except: log.error('Failed: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index bb0504b2..2789e98f 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -54,10 +54,20 @@ var Profile = new Class({ ) ); - self.makeSortable() + self.makeSortable(); + + // Combine qualities and properties into types + data.types = []; + data.qualities.each(function(quality, nr){ + data.types.include({ + 'quality_identifier': quality, + 'finish': data.finish[nr] || false, + 'wait_for': data.wait_for[nr] || 0 + }) + }); if(data.types) - Object.each(data.types, self.addType.bind(self)) + data.types.each(self.addType.bind(self)); else self.delete_button.hide(); @@ -111,8 +121,9 @@ var Profile = new Class({ Array.each(self.type_container.getElements('.type'), function(type){ if(!type.hasClass('deleted') && type.getElement('select').get('value') > 0) data.types.include({ - 'quality_id': type.getElement('select').get('value'), - 'finish': +type.getElement('input[type=checkbox]').checked + 'quality_identifier': type.getElement('select').get('value'), + 'finish': +type.getElement('input[type=checkbox]').checked, + 'wait_for': 0 }); }) @@ -145,7 +156,7 @@ var Profile = new Class({ var self = this; return self.types.filter(function(type){ - return type.get('quality_id') + return type.get('quality_identifier') }); }, @@ -264,7 +275,7 @@ Profile.Type = new Class({ new Element('span.handle') ); - self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty'); + self.el[self.data.quality_identifier ? 'removeClass' : 'addClass']('is_empty'); self.finish_class = new Form.Check(self.finish); @@ -287,11 +298,11 @@ Profile.Type = new Class({ Object.each(Quality.qualities, function(q){ new Element('option', { 'text': q.label, - 'value': q.id + 'value': q.identifier }).inject(self.qualities) }); - self.qualities.set('value', self.data.quality_id); + self.qualities.set('value', self.data.quality_identifier); return self.qualities; @@ -301,8 +312,9 @@ Profile.Type = new Class({ var self = this; return { - 'quality_id': self.qualities.get('value'), - 'finish': +self.finish.checked + 'quality_identifier': self.qualities.get('value'), + 'finish': +self.finish.checked, + 'wait_for': 0 } }, diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index ead9a904..af017755 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -17,7 +17,7 @@ var QualityBase = new Class({ getProfile: function(id){ return this.profiles.filter(function(profile){ - return profile.data.id == id + return profile.data._id == id }).pick() }, @@ -28,9 +28,9 @@ var QualityBase = new Class({ }); }, - getQuality: function(id){ + getQuality: function(identifier){ return this.qualities.filter(function(q){ - return q.id == id; + return q.identifier == identifier; }).pick(); }, diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index c68edf1c..11c12563 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -19,13 +19,13 @@ class ReleaseIndex(TreeBasedIndex): for release in db.get_many('release', media_id, with_doc = True): yield release['doc'] - def run_with_status(self, db, status = []): + def run_with_status(self, db, status = [], with_doc = True): status = list(status if isinstance(status, (list, tuple)) else [status]) for s in status: - for ms in db.get_many('release_status', s, with_doc = True): - yield ms['doc'] + for ms in db.get_many('release_status', s, with_doc = with_doc): + yield ms['doc'] if with_doc else ms class ReleaseStatusIndex(TreeBasedIndex): @@ -39,7 +39,7 @@ class ReleaseStatusIndex(TreeBasedIndex): def make_key_value(self, data): if data.get('_t') == 'release' and data.get('status'): - return md5(data.get('status')).hexdigest(), None + return md5(data.get('status')).hexdigest(), {'media_id': data.get('media_id')} class ReleaseIDIndex(TreeBasedIndex): @@ -53,4 +53,4 @@ class ReleaseIDIndex(TreeBasedIndex): def make_key_value(self, data): if data.get('_t') == 'release' and data.get('identifier'): - return md5(data.get('identifier')).hexdigest(), None + return md5(data.get('identifier')).hexdigest(), {'media_id': data.get('media_id')} diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 0e06a751..799b1774 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -461,7 +461,7 @@ class Renamer(Plugin): self.tagRelease(group = group, tag = 'exists') # Notify on rename fail - download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label) + download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (getTitle(movie), group['meta_data']['quality']['label'], release.quality.label) fireEvent('movie.renaming.canceled', message = download_message, data = group) remove_leftovers = False diff --git a/couchpotato/runner.py b/couchpotato/runner.py index b2ea606b..e3f07855 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -223,7 +223,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En loader.run() # Fill database with needed stuff - fireEvent('database.setup') + fireEvent('database.setup') if not db_exists: fireEvent('app.initialize', in_order = True) From c3bc9c859157109042d9c8f75ec7e23811c2b69d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Feb 2014 17:58:45 +0100 Subject: [PATCH 009/468] Nosql --- couchpotato/core/helpers/request.py | 6 +- couchpotato/core/media/_base/media/index.py | 14 --- couchpotato/core/media/_base/media/main.py | 44 ++++----- couchpotato/core/media/movie/_base/main.py | 10 +- .../core/media/movie/_base/static/list.js | 10 +- .../media/movie/_base/static/movie.actions.js | 93 +++++-------------- .../core/media/movie/_base/static/movie.js | 22 ++--- .../core/media/movie/_base/static/search.js | 2 +- couchpotato/core/media/movie/searcher/main.py | 3 +- .../core/media/movie/suggestion/main.py | 25 ++--- .../notifications/core/static/notification.js | 4 +- couchpotato/core/plugins/category/index.py | 21 ++++- couchpotato/core/plugins/category/main.py | 62 ++++++------- .../core/plugins/category/static/category.js | 12 +-- couchpotato/core/plugins/profile/main.py | 71 +++++++------- .../core/plugins/profile/static/profile.js | 34 +++---- couchpotato/core/plugins/quality/main.py | 3 - .../core/plugins/quality/static/quality.js | 2 +- couchpotato/core/plugins/release/main.py | 36 ------- couchpotato/core/plugins/renamer/main.py | 5 +- couchpotato/core/plugins/scanner/main.py | 21 +---- couchpotato/core/plugins/subtitle/main.py | 27 +----- .../core/providers/info/_modifier/main.py | 2 +- .../core/providers/nzb/newznab/__init__.py | 2 +- couchpotato/core/settings/__init__.py | 2 +- couchpotato/runner.py | 2 - couchpotato/static/scripts/page/home.js | 2 +- 27 files changed, 185 insertions(+), 352 deletions(-) diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 8a10f078..523e46ec 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -9,6 +9,7 @@ def getParams(params): reg = re.compile('^[a-z0-9_\.]+$') temp = {} + for param, value in sorted(params.items()): nest = re.split("([\[\]]+)", param) @@ -44,7 +45,10 @@ def dictToList(params): new = {} for x, value in params.items(): try: - new_value = [dictToList(value[k]) for k in sorted(value.keys(), cmp = natcmp)] + convert = lambda text: int(text) if text.isdigit() else text.lower() + alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] + sorted_keys = sorted(value.keys(), key = alphanum_key) + new_value = [dictToList(value[k]) for k in sorted_keys] except: new_value = value diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index d83c347c..68e6610c 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -92,20 +92,6 @@ from couchpotato.core.helpers.encoding import simplifyString""" return key.rjust(32, '_').lower() -class YearIndex(TreeBasedIndex): - - def __init__(self, *args, **kwargs): - kwargs['key_format'] = 'i' - super(YearIndex, self).__init__(*args, **kwargs) - - def make_key(self, key): - return key - - def make_key_value(self, data): - if data.get('_t') == 'media' and data.get('year') is not None: - return data['year'], None - - class TitleIndex(TreeBasedIndex): custom_header = """from CodernityDB.tree_index import TreeBasedIndex diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 17e443b3..480b571f 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,13 +1,12 @@ import traceback -from couchpotato import get_session, tryInt, get_db +from couchpotato import tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIMDBIndex, MediaStatusIndex, YearIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex -from couchpotato.core.settings.model import Media +from .index import MediaIMDBIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex from string import ascii_lowercase log = CPLog(__name__) @@ -102,12 +101,6 @@ class MediaPlugin(MediaBase): log.debug('Index already exists') db.edit_index(MediaTypeIndex(db.path, 'media_by_type')) - # Year index - try: db.add_index(YearIndex(db.path, 'media_year')) - except: - log.debug('Index already exists') - db.edit_index(YearIndex(db.path, 'media_year')) - # Title index try: db.add_index(TitleIndex(db.path, 'media_title')) except: @@ -377,46 +370,45 @@ class MediaPlugin(MediaBase): def delete(self, media_id, delete_from = None): try: - db = get_session() + db = get_db() - media = db.query(Media).filter_by(id = media_id).first() + media = db.get('id', media_id) if media: deleted = False if delete_from == 'all': db.delete(media) - db.commit() deleted = True else: - total_releases = len(media.releases) + media_releases = list(db.run('release', 'for_media', media['_id'])) + + total_releases = len(media_releases) total_deleted = 0 - new_movie_status = None - for release in media.releases: + new_media_status = None + + for release in media_releases: if delete_from in ['wanted', 'snatched', 'late']: if release.get('status') != 'done': db.delete(release) total_deleted += 1 - new_movie_status = 'done' + new_media_status = 'done' elif delete_from == 'manage': if release.get('status') == 'done': db.delete(release) total_deleted += 1 - new_movie_status = 'active' - db.commit() + new_media_status = 'active' if total_releases == total_deleted: db.delete(media) - db.commit() deleted = True - elif new_movie_status: - media.profile_id = None - media['status'] = new_status - db.commit() + elif new_media_status: + media['status'] = new_media_status + db.update(media) else: - fireEvent('media.restatus', media.id, single = True) + fireEvent('media.restatus', media.get('_id'), single = True) if deleted: - fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) + fireEvent('notify.frontend', type = 'media.deleted', data = media) except: log.error('Failed deleting media: %s', traceback.format_exc()) @@ -453,7 +445,7 @@ class MediaPlugin(MediaBase): move_to_wanted = True profile = db.get('id', m['profile_id']) - media_releases = db.get_many('release', m['_id']) + media_releases = db.run('release', 'for_media', m['_id']) for q_identifier in profile['qualities']: index = profile['qualities'].index(q_identifier) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index bb04b392..79ab97e6 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -94,7 +94,7 @@ class MovieBase(MovieTypeBase): 'identifier': params.get('identifier'), 'status': status if status else 'active', 'profile_id': params.get('profile_id', default_profile.get('_id')), - 'category_id': tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, + 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 else None, } new = False @@ -135,7 +135,7 @@ class MovieBase(MovieTypeBase): fireEvent('release.delete', release['_id'], single = True) m['profile_id'] = params.get('profile_id', default_profile.get('id')) - m['category_id'] = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.get('category_id') or None) + m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) else: log.debug('Movie already exists, not updating: %s', params) added = False @@ -198,7 +198,7 @@ class MovieBase(MovieTypeBase): cat_id = kwargs.get('category_id') if cat_id is not None: - m['category_id'] = tryInt(cat_id) if tryInt(cat_id) > 0 else None + m['category_id'] = cat_id if len(cat_id) > 0 else None # Remove releases for rel in db.run('release', 'for_media', m['_id']): @@ -209,12 +209,12 @@ class MovieBase(MovieTypeBase): if kwargs.get('default_title'): m['title'] = kwargs.get('default_title') - print m - db.update(m) fireEvent('media.restatus', m['_id']) + m = db.get('id', media_id) + movie_dict = db.run('media', 'to_dict', m['_id']) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index a193daf5..ce405957 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -63,7 +63,7 @@ var MovieList = new Class({ self.movies.each(function(movie){ if(movie.get('_id') == notification.data._id){ movie.destroy(); - delete self.movies_added[notification.data.id]; + delete self.movies_added[notification.data._id]; self.setCounter(self.counter_count-1); self.total_movies--; } @@ -77,7 +77,7 @@ var MovieList = new Class({ var self = this; self.fireEvent('movieAdded', notification); - if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ + if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){ window.scroll(0,0); self.createMovie(notification.data, 'top'); self.setCounter(self.counter_count+1); @@ -180,7 +180,7 @@ var MovieList = new Class({ m.fireEvent('injected'); self.movies.include(m) - self.movies_added[movie.id] = true; + self.movies_added[movie._id] = true; }, createNavigation: function(){ @@ -259,8 +259,8 @@ var MovieList = new Class({ self.mass_edit_select_class = new Form.Check(self.mass_edit_select); Quality.getActiveProfiles().each(function(profile){ new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, - 'text': profile.label ? profile.label : profile.data.label + 'value': profile.get('_id'), + 'text': profile.get('label') }).inject(self.mass_edit_quality) }); diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index b52a5923..50548ccb 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -127,7 +127,7 @@ MA.Release = new Class({ self.showHelper(); App.on('movie.searcher.ended', function(notification){ - if(self.movie.data.id != notification.data.id) return; + if(self.movie.data._id != notification.data._id) return; self.releases = null; if(self.options_container){ @@ -143,30 +143,7 @@ MA.Release = new Class({ if(e) (e).preventDefault(); - if(self.releases) - self.createReleases(); - else { - - self.movie.busy(true); - - Api.request('release.for_movie', { - 'data': { - 'id': self.movie.data.id - }, - 'onComplete': function(json){ - self.movie.busy(false, 1); - - if(json && json.releases){ - self.releases = json.releases; - self.createReleases(); - } - else - alert('Something went wrong, check the logs.'); - } - }); - - } - + self.createReleases(); }, @@ -189,9 +166,9 @@ MA.Release = new Class({ new Element('span.provider', {'text': 'Provider'}) ).inject(self.release_container) - self.releases.each(function(release){ + self.movie.data.releases.each(function(release){ - var quality = Quality.getProfile(release.quality_id) || {}, + var quality = Quality.getQuality(release.quality) || {}, info = release.info, provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : ''); @@ -209,12 +186,12 @@ MA.Release = new Class({ // Create release var item = new Element('div', { - 'class': 'item '+status.identifier, - 'id': 'release_'+release.id + 'class': 'item '+release.status, + 'id': 'release_'+release._id }).adopt( new Element('span.name', {'text': release_name, 'title': release_name}), - new Element('span.status', {'text': status.identifier, 'class': 'release_status '+release.status}), - new Element('span.quality', {'text': quality.get('label') || 'n/a'}), + new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), + new Element('span.quality', {'text': quality.label || 'n/a'}), new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.score', {'text': self.get(release, 'score')}), @@ -243,32 +220,32 @@ MA.Release = new Class({ ).inject(self.release_container); release['el'] = item; - if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){ - if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched')) + if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ + if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) self.last_release = release; } - else if(!self.next_release && status.identifier == 'available'){ + else if(!self.next_release && release.status == 'available'){ self.next_release = release; } var update_handle = function(notification) { - if(notification.data.id != release.id) return; + if(notification.data._id != release._id) return; - var q = self.movie.quality.getElement('.q_id' + release.quality_id), + var q = self.movie.quality.getElement('.q_' + release.quality), new_status = notification.data.status; release.el.set('class', 'item ' + new_status); var status_el = release.el.getElement('.release_status'); status_el.set('class', 'release_status ' + new_status); - status_el.set('text', new_status.identifier); + status_el.set('text', new_status); if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) var q = self.addQuality(release.quality_id); if(q && !q.hasClass(new_status)) { q.removeClass(release.status).addClass(new_status); - q.set('title', q.get('title').replace(status, new_status)); + q.set('title', q.get('title').replace(release.status, new_status)); } } @@ -277,12 +254,12 @@ MA.Release = new Class({ }); if(self.last_release) - self.release_container.getElements('#release_'+self.last_release.id).addClass('last_release'); + self.release_container.getElements('#release_'+self.last_release._id).addClass('last_release'); if(self.next_release) - self.release_container.getElements('#release_'+self.next_release.id).addClass('next_release'); + self.release_container.getElements('#release_'+self.next_release._id).addClass('next_release'); - if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ + if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){ self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); @@ -385,7 +362,7 @@ MA.Release = new Class({ download: function(release){ var self = this; - var release_el = self.release_container.getElement('#release_'+release.id), + var release_el = self.release_container.getElement('#release_'+release._id), icon = release_el.getElement('.download.icon2'); if(icon) @@ -393,7 +370,7 @@ MA.Release = new Class({ Api.request('release.manual_download', { 'data': { - 'id': release.id + 'id': release._id }, 'onComplete': function(json){ if(icon) @@ -416,7 +393,7 @@ MA.Release = new Class({ Api.request('release.ignore', { 'data': { - 'id': release.id + 'id': release._id }, }) @@ -623,14 +600,14 @@ MA.Edit = new Class({ self.category_select.show(); categories.each(function(category){ - var category_id = category.data.id; + var category_id = category.data._id; new Element('option', { 'value': category_id, 'text': category.data.label }).inject(self.category_select); - if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id) + if(self.movie.category && self.movie.category.data && self.movie.category.data._id == category_id) self.category_select.set('value', category_id); }); @@ -859,29 +836,7 @@ MA.Files = new Class({ var self = this; (e).preventDefault(); - if(self.releases) - self.showFiles(); - else { - - self.movie.busy(true); - - Api.request('release.for_movie', { - 'data': { - 'id': self.movie.data.id - }, - 'onComplete': function(json){ - self.movie.busy(false, 1); - - if(json && json.releases){ - self.releases = json.releases; - self.showFiles(); - } - else - alert('Something went wrong, check the logs.'); - } - }); - - } + self.showFiles(); }, diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 7e07cd4a..417f065d 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -27,7 +27,7 @@ var Movie = new Class({ // Do refresh with new data self.global_events['movie.update'] = function(notification){ - if(self.data.id != notification.data.id) return; + if(self.data._id != notification.data._id) return; self.busy(false); self.removeView(); @@ -38,7 +38,7 @@ var Movie = new Class({ // Add spinner on load / search ['media.busy', 'movie.searcher.started'].each(function(listener){ self.global_events[listener] = function(notification){ - if(notification.data && (self.data.id == notification.data.id || (typeOf(notification.data.id) == 'array' && notification.data.id.indexOf(self.data.id) > -1))) + if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1))) self.busy(true); } App.on(listener, self.global_events[listener]); @@ -46,7 +46,7 @@ var Movie = new Class({ // Remove spinner self.global_events['movie.searcher.ended'] = function(notification){ - if(notification.data && self.data.id == notification.data.id) + if(notification.data && self.data._id == notification.data._id) self.busy(false) } App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); @@ -54,12 +54,12 @@ var Movie = new Class({ // Reload when releases have updated self.global_events['release.update_status'] = function(notification){ var data = notification.data - if(data && self.data.id == data.movie_id){ + if(data && self.data._id == data.movie_id){ if(!self.data.releases) self.data.releases = []; - self.data.releases.push({'quality_identifier': data.quality_identifier, 'status': data.status}); + self.data.releases.push({'quality': data.quality, 'status': data.status}); self.updateReleases(); } } @@ -197,7 +197,7 @@ var Movie = new Class({ if(self.profile.data) self.profile.getTypes().each(function(type){ - var q = self.addQuality(type.quality_identifier || type.get('quality_identifier')); + var q = self.addQuality(type.get('quality')); if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ q.addClass('finish'); q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') @@ -222,11 +222,11 @@ var Movie = new Class({ self.data.releases.each(function(release){ - var q = self.quality.getElement('.q_id'+ release.quality_id), + var q = self.quality.getElement('.q_'+ release.quality), status = release.status; if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) - var q = self.addQuality(release.quality_identifier) + var q = self.addQuality(release.quality) if (q && !q.hasClass(status)){ q.addClass(status); @@ -236,13 +236,13 @@ var Movie = new Class({ }); }, - addQuality: function(quality_identifier){ + addQuality: function(quality){ var self = this; - var q = Quality.getQuality(quality_identifier); + var q = Quality.getQuality(quality); return new Element('span', { 'text': q.label, - 'class': 'q_'+q.identifier + ' q_id' + q.id, + 'class': 'q_'+q.identifier, 'title': '' }).inject(self.quality); diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index 61842149..edd1d23e 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -184,7 +184,7 @@ Block.Search.MovieItem = new Class({ self.category_select.show(); categories.each(function(category){ new Element('option', { - 'value': category.data.id, + 'value': category.data._id, 'text': category.data.label }).inject(self.category_select); }); diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 69615a16..cecfd720 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -139,7 +139,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): profile = db.get('id', movie['profile_id']) quality_order = fireEvent('quality.order', single = True) - media_releases = db.get_many('release', movie['_id']) + media_releases = db.run('release', 'for_media', movie['_id']) ret = False @@ -202,7 +202,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['_id']}) - pass #db.close() return ret def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index 55cf7062..bc42e456 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -1,12 +1,9 @@ -from couchpotato import get_session +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import splitString, removeDuplicate from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Media, Library from couchpotato.environment import Env -from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql.expression import or_ class Suggestion(Plugin): @@ -28,12 +25,9 @@ class Suggestion(Plugin): else: if not movies or len(movies) == 0: - db = get_session() - active_movies = db.query(Media) \ - .options(joinedload_all('library')) \ - .filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all() - movies = [x.library.identifier for x in active_movies] - pass #db.close() + db = get_db() + active_movies = db.run('media', 'with_status', ['active', 'done']) + movies = [x['identifier'] for x in active_movies] if not ignored or len(ignored) == 0: ignored = splitString(Env.prop('suggest_ignore', default = '')) @@ -88,15 +82,10 @@ class Suggestion(Plugin): # Get new results and add them if len(new_suggestions) - 1 < limit: - - db = get_session() - active_movies = db.query(Media) \ - .join(Library) \ - .with_entities(Library.identifier) \ - .filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all() - movies = [x[0] for x in active_movies] + db = get_db() + active_movies = db.run('media', 'with_status', ['active', 'done']) + movies = [x['identifier'] for x in active_movies] movies.extend(seen) - pass #db.close() ignored.extend([x.get('imdb') for x in cached_suggestion]) suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index f8d46645..756c41eb 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -77,7 +77,7 @@ var NotificationBase = new Class({ var ids = [] rn.each(function(n){ - ids.include(n.id) + ids.include(n._id) }) } @@ -187,7 +187,7 @@ var NotificationBase = new Class({ new Element('a.close.icon2', { 'events': { 'click': function(){ - self.markAsRead([data.id]); + self.markAsRead([data._id]); hide_message(); } } diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py index 1eb3651e..107f927d 100644 --- a/couchpotato/core/plugins/category/index.py +++ b/couchpotato/core/plugins/category/index.py @@ -1,16 +1,29 @@ -from hashlib import md5 from CodernityDB.tree_index import TreeBasedIndex class CategoryIndex(TreeBasedIndex): def __init__(self, *args, **kwargs): - kwargs['key_format'] = '32s' + kwargs['key_format'] = 'i' super(CategoryIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).hexdigest() + return key def make_key_value(self, data): if data.get('_t') == 'category': - return md5(data['media_id']).hexdigest(), None + return data.get('order', -99), None + + +class CategoryMediaIndex(TreeBasedIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(CategoryMediaIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return str(key) + + def make_key_value(self, data): + if data.get('_t') == 'media' and data.get('category_id'): + return str(data.get('category_id')), None diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index 52161c6a..4761c11c 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -1,12 +1,11 @@ import traceback -from couchpotato import get_session, get_db +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from .index import CategoryIndex -from couchpotato.core.settings.model import Media, Category +from .index import CategoryIndex, CategoryMediaIndex log = CPLog(__name__) @@ -32,18 +31,25 @@ class CategoryPlugin(Plugin): db = get_db() - # Release media_id index + # Category index try: db.add_index(CategoryIndex(db.path, 'category')) except: log.debug('Index already exists') db.edit_index(CategoryIndex(db.path, 'category')) + # Category media_id index + try: + db.add_index(CategoryMediaIndex(db.path, 'category_media')) + except: + log.debug('Index already exists') + db.edit_index(CategoryMediaIndex(db.path, 'category_media')) + def allView(self, **kwargs): return { 'success': True, - 'list': self.all() + 'categories': self.all() } def all(self): @@ -51,11 +57,7 @@ class CategoryPlugin(Plugin): db = get_db() categories = db.all('category', with_doc = True) - temp = [] - for category in categories: - temp.append(category['doc']) - - return temp + return [x['doc'] for x in categories] def save(self, **kwargs): @@ -63,7 +65,8 @@ class CategoryPlugin(Plugin): db = get_db() category = { - 'order': kwargs.get('order', 0), + '_t': 'category', + 'order': kwargs.get('order', 999), 'label': toUnicode(kwargs.get('label', '')), 'ignored': toUnicode(kwargs.get('ignored', '')), 'preferred': toUnicode(kwargs.get('preferred', '')), @@ -96,26 +99,21 @@ class CategoryPlugin(Plugin): def saveOrder(self, **kwargs): try: - db = get_session() + db = get_db() order = 0 for category_id in kwargs.get('ids', []): - c = db.query(Category).filter_by(id = category_id).first() - c.order = order + c = db.get('id', category_id) + c['order'] = order + db.update(c) order += 1 - db.commit() - - pass #db.close() return { 'success': True } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False @@ -124,32 +122,27 @@ class CategoryPlugin(Plugin): def delete(self, id = None, **kwargs): try: - db = get_session() + db = get_db() success = False message = '' try: - c = db.query(Category).filter_by(id = id).first() + c = db.get('id', id) db.delete(c) - db.commit() # Force defaults on all empty category movies self.removeFromMovie(id) success = True - except Exception as e: - message = log.error('Failed deleting category: %s', e) + except: + message = log.error('Failed deleting category: %s', traceback.format_exc()) - pass #db.close() return { 'success': success, 'message': message } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False @@ -158,15 +151,12 @@ class CategoryPlugin(Plugin): def removeFromMovie(self, category_id): try: - db = get_session() - movies = db.query(Media).filter(Media.category_id == category_id).all() + db = get_db() + movies = [x['doc'] for x in db.get_many('category_media', category_id, with_doc = True)] if len(movies) > 0: for movie in movies: - movie.category_id = None - db.commit() + movie['category_id'] = None + db.update(movie) except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js index 168b70de..fedd2849 100644 --- a/couchpotato/core/plugins/category/static/category.js +++ b/couchpotato/core/plugins/category/static/category.js @@ -86,7 +86,7 @@ var CategoryListBase = new Class({ getCategory: function(id){ return this.categories.filter(function(category){ - return category.data.id == id + return category.data._id == id }).pick() }, @@ -121,7 +121,7 @@ var CategoryListBase = new Class({ ).inject(self.content) Array.each(self.categories, function(category){ - new Element('li', {'data-id': category.data.id}).adopt( + new Element('li', {'data-id': category.data._id}).adopt( new Element('span.category_label', { 'text': category.data.label }), @@ -258,7 +258,7 @@ var Category = new Class({ var self = this; var data = { - 'id' : self.data.id, + 'id' : self.data._id, 'label' : self.el.getElement('.category_label input').get('value'), 'required' : self.el.getElement('.category_required input').get('value'), 'preferred' : self.el.getElement('.category_preferred input').get('value'), @@ -271,7 +271,7 @@ var Category = new Class({ del: function(){ var self = this; - + if(self.data.label == undefined){ self.el.destroy(); return; @@ -286,7 +286,7 @@ var Category = new Class({ (e).preventDefault(); Api.request('category.delete', { 'data': { - 'id': self.data.id + 'id': self.data._id }, 'useSpinner': true, 'spinnerOptions': { @@ -329,4 +329,4 @@ var Category = new Class({ return this.el } -}); \ No newline at end of file +}); diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 90335953..1c3ec630 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -1,8 +1,9 @@ import traceback -from couchpotato import get_session, get_db +from couchpotato import get_session, get_db, tryInt from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ProfileIndex @@ -78,45 +79,41 @@ class ProfilePlugin(Plugin): def save(self, **kwargs): try: - db = get_session() + db = get_db() - p = db.query(Profile).filter_by(id = kwargs.get('id')).first() - if not p: - p = Profile() - db.add(p) - - p.label = toUnicode(kwargs.get('label')) - p.order = kwargs.get('order', p.order if p.order else 0) - p.core = kwargs.get('core', False) - - #delete old types - [db.delete(t) for t in p.types] + profile = { + 'label': toUnicode(kwargs.get('label')), + 'order': tryInt(kwargs.get('order', 999)), + 'core': kwargs.get('core', False), + 'qualities': [], + 'wait_for': [], + 'finish': [] + } + # Update types order = 0 for type in kwargs.get('types', []): - t = ProfileType( - order = order, - finish = type.get('finish') if order > 0 else 1, - wait_for = kwargs.get('wait_for'), - quality_id = type.get('quality_id') - ) - p.types.append(t) - + profile['qualities'].append(type.get('quality')) + profile['wait_for'].append(tryInt(type.get('wait_for'))) + profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True) order += 1 - db.commit() + id = kwargs.get('id') + try: + p = db.get('id', id) + except: + p = db.insert(profile) + p.update(profile) - profile_dict = p.to_dict(self.to_dict) + p['order'] = tryInt(kwargs.get('order', p.get('order', 999))) + db.update(p) return { 'success': True, - 'profile': profile_dict + 'profile': p } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False @@ -129,26 +126,23 @@ class ProfilePlugin(Plugin): def saveOrder(self, **kwargs): try: - db = get_session() + db = get_db() order = 0 - for profile in kwargs.get('ids', []): - p = db.query(Profile).filter_by(id = profile).first() - p.hide = kwargs.get('hidden')[order] - p.order = order + + for profile_id in kwargs.get('ids', []): + p = db.get('id', profile_id) + p['hide'] = tryInt(kwargs.get('hidden')[order]) == 1 + p['order'] = order + db.update(p) order += 1 - db.commit() - return { 'success': True } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False @@ -180,9 +174,6 @@ class ProfilePlugin(Plugin): } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 2789e98f..1bd2edce 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -57,14 +57,16 @@ var Profile = new Class({ self.makeSortable(); // Combine qualities and properties into types - data.types = []; - data.qualities.each(function(quality, nr){ - data.types.include({ - 'quality_identifier': quality, - 'finish': data.finish[nr] || false, - 'wait_for': data.wait_for[nr] || 0 - }) - }); + if(data.qualities){ + data.types = []; + data.qualities.each(function(quality, nr){ + data.types.include({ + 'quality': quality, + 'finish': data.finish[nr] || false, + 'wait_for': data.wait_for[nr] || 0 + }) + }); + } if(data.types) data.types.each(self.addType.bind(self)); @@ -112,16 +114,16 @@ var Profile = new Class({ var self = this; var data = { - 'id' : self.data.id, + 'id' : self.data._id, 'label' : self.el.getElement('.quality_label input').get('value'), 'wait_for' : self.el.getElement('.wait_for input').get('value'), 'types': [] } Array.each(self.type_container.getElements('.type'), function(type){ - if(!type.hasClass('deleted') && type.getElement('select').get('value') > 0) + if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1) data.types.include({ - 'quality_identifier': type.getElement('select').get('value'), + 'quality': type.getElement('select').get('value'), 'finish': +type.getElement('input[type=checkbox]').checked, 'wait_for': 0 }); @@ -156,7 +158,7 @@ var Profile = new Class({ var self = this; return self.types.filter(function(type){ - return type.get('quality_identifier') + return type.get('quality') }); }, @@ -173,7 +175,7 @@ var Profile = new Class({ (e).preventDefault(); Api.request('profile.delete', { 'data': { - 'id': self.data.id + 'id': self.data._id }, 'useSpinner': true, 'spinnerOptions': { @@ -275,7 +277,7 @@ Profile.Type = new Class({ new Element('span.handle') ); - self.el[self.data.quality_identifier ? 'removeClass' : 'addClass']('is_empty'); + self.el[self.data.quality ? 'removeClass' : 'addClass']('is_empty'); self.finish_class = new Form.Check(self.finish); @@ -302,7 +304,7 @@ Profile.Type = new Class({ }).inject(self.qualities) }); - self.qualities.set('value', self.data.quality_identifier); + self.qualities.set('value', self.data.quality); return self.qualities; @@ -312,7 +314,7 @@ Profile.Type = new Class({ var self = this; return { - 'quality_identifier': self.qualities.get('value'), + 'quality': self.qualities.get('value'), 'finish': +self.finish.checked, 'wait_for': 0 } diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index e75eb465..265cdf2d 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -138,9 +138,6 @@ class QualityPlugin(Plugin): } except: log.error('Failed: %s', traceback.format_exc()) - db.rollback() - finally: - pass #db.close() return { 'success': False diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index af017755..613096fc 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -117,7 +117,7 @@ var QualityBase = new Class({ Array.each(self.profiles, function(profile){ var check; - new Element('li', {'data-id': profile.data.id}).adopt( + new Element('li', {'data-id': profile.data._id}).adopt( check = new Element('input.inlay[type=checkbox]', { 'checked': !profile.data.hide, 'events': { diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 7cb634bd..bd805fd4 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -42,18 +42,11 @@ class Release(Plugin): 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} } }) - addApiView('release.for_movie', self.forMovieView, docs = { - 'desc': 'Returns all releases for a movie. Ordered by score(desc)', - 'params': { - 'id': {'type': 'id', 'desc': 'ID of the movie'} - } - }) addEvent('release.add', self.add) addEvent('release.download', self.download) addEvent('release.try_download_result', self.tryDownloadResult) addEvent('release.create_from_search', self.createFromSearch) - addEvent('release.for_movie', self.forMovie) addEvent('release.delete', self.delete) addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) @@ -266,7 +259,6 @@ class Release(Plugin): if success: fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) - pass #db.close() return { 'success': success == True } @@ -352,10 +344,7 @@ class Release(Plugin): except: log.error('Failed storing download status: %s', traceback.format_exc()) - db.rollback() return False - finally: - pass #db.close() return True @@ -431,31 +420,6 @@ class Release(Plugin): return [] - def forMovie(self, id = None): - - db = get_session() - - releases_raw = db.query(Relea) \ - .options(joinedload_all('info')) \ - .options(joinedload_all('files')) \ - .filter(Relea.movie_id == id) \ - .all() - - releases = [r.to_dict({'info': {}, 'files': {}}) for r in releases_raw] - releases = sorted(releases, key = lambda k: k['info'].get('score', 0), reverse = True) - - pass #db.close() - return releases - - def forMovieView(self, id = None, **kwargs): - - releases = self.forMovie(id) - - return { - 'releases': releases, - 'success': True - } - def updateStatus(self, release_id, status = None): if not status: return False diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 799b1774..c26de9e9 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -591,7 +591,6 @@ class Renamer(Plugin): break self.renaming_started = False - pass #db.close() def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False): if not group: group = {} @@ -845,7 +844,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: db = get_db() - rels = list(db.get_many('release', 'with_status', ['snatched', 'seeding', 'missing'])) + rels = list(db.run('release', 'with_status', ['snatched', 'seeding', 'missing'])) if not rels: #No releases found that need status checking @@ -1058,8 +1057,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) if not rls: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) - pass #db.close() - if rls: rls_dict = rls.to_dict({'info':{}}) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index f9ccea78..744c7bd6 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -1,11 +1,9 @@ -from couchpotato import get_session from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss, sp +from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \ splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info from subliminal.videos import Video @@ -352,7 +350,6 @@ class Scanner(Plugin): release_download = None # Determine file types - db = get_session() processed_movies = {} while True and not self.shuttingDown(): try: @@ -439,7 +436,6 @@ class Scanner(Plugin): else: log.debug('Found no movies in the folder %s', folder) - pass #db.close() return processed_movies def getMetaData(self, group, folder = '', release_download = None): @@ -594,21 +590,6 @@ class Scanner(Plugin): except: pass - # Check if path is already in db - if not imdb_id: - - db = get_session() - for cf in files['movie']: - f = db.query(File).filter_by(path = toUnicode(cf)).first() - try: - imdb_id = f.library[0].identifier - log.debug('Found movie via database: %s', cf) - cur_file = cf - break - except: - pass - pass #db.close() - # Search based on identifiers if not imdb_id: for identifier in group['identifiers']: diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index a3b1d13c..c00c757e 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -1,10 +1,8 @@ -from couchpotato import get_session -from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, sp from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, FileType from couchpotato.environment import Env import subliminal import traceback @@ -19,29 +17,6 @@ class Subtitle(Plugin): def __init__(self): addEvent('renamer.before', self.searchSingle) - def searchLibrary(self): - - # Get all active and online movies - db = get_session() - - library = db.query(Library).all() - - for movie in library.movies: - - for release in movie.releases: - - # get releases and their movie files - if release.get('status') == 'done': - - files = [] - for file in release.files.filter(FileType.status.has(identifier = 'movie')).all(): - files.append(file.path) - - # get subtitles for those files - subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) - - pass #db.close() - def searchSingle(self, group): if self.isDisabled(): return diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index 09d19258..010f1f5b 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -100,7 +100,7 @@ class MovieResultModifier(Plugin): if media.get('status') == 'active': temp['in_wanted'] = media - for release in db.get_many('release', media.get('_id'), with_doc = True): + for release in db.run('release', 'for_media', media.get('_id')): if release.get('status') == 'done': temp['in_library'] = media except: diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index 97f1cfad..86a25a4f 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -29,7 +29,7 @@ config = [{ }, { 'name': 'host', - 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', + 'default': 'api.nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', 'description': 'The hostname of your newznab provider', }, { diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index fcea04f5..23f48b13 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -228,7 +228,7 @@ class Settings(object): propert = db.get('property', identifier, with_doc = True) prop = propert['doc']['value'] except: - self.log.debug('Property doesn\'t exist: %s', traceback.format_exc(0)) + self.log.debug('Property "%s" doesn\'t exist: %s', (identifier, traceback.format_exc(0))) return prop diff --git a/couchpotato/runner.py b/couchpotato/runner.py index e3f07855..d6a967c7 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -86,8 +86,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db_path = toUnicode(os.path.join(data_dir, 'database')) # Check if database exists - print db_path - db = ThreadSafeDatabase(db_path) db_exists = db.exists() if db_exists: diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 818ce0ed..dbf8d7b8 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -62,7 +62,7 @@ Page.Home = new Class({ // Track movie added var after_search = function(data){ - if(notification.data.id != data.data.id) return; + if(notification.data._id != data.data._id) return; // Force update after search self.available_list.update(); From f7e1a2a5eba318ba49abb556f86a1a0f63ee0142 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Feb 2014 15:57:08 +0100 Subject: [PATCH 010/468] nosql --- couchpotato/core/helpers/request.py | 1 - couchpotato/core/media/movie/_base/main.py | 2 +- couchpotato/core/plugins/file/main.py | 32 +- couchpotato/core/plugins/manage/main.py | 10 +- couchpotato/core/plugins/profile/main.py | 16 +- couchpotato/core/plugins/quality/main.py | 11 +- couchpotato/core/plugins/release/index.py | 5 +- couchpotato/core/plugins/release/main.py | 71 ++-- couchpotato/core/plugins/scanner/main.py | 16 +- libs/scandir/.gitattributes | 1 + libs/scandir/.gitignore | 4 + libs/scandir/__init__.py | 0 libs/scandir/_scandir.c | 373 +++++++++++++++++ libs/scandir/scandir.py | 456 +++++++++++++++++++++ 14 files changed, 915 insertions(+), 83 deletions(-) create mode 100644 libs/scandir/.gitattributes create mode 100644 libs/scandir/.gitignore create mode 100644 libs/scandir/__init__.py create mode 100644 libs/scandir/_scandir.c create mode 100644 libs/scandir/scandir.py diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 523e46ec..b59673f9 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -1,5 +1,4 @@ from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import natcmp from urllib import unquote import re diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 79ab97e6..54efd677 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -45,7 +45,7 @@ class MovieBase(MovieTypeBase): addEvent('movie.update_info', self.updateInfo) addEvent('movie.update_release_dates', self.updateReleaseDate) - def add(self, params = None, force_readd = True, search_after = True, update_library = False, status = None): + def add(self, params = None, force_readd = True, search_after = True, status = None): if not params: params = {} if not params.get('identifier'): diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 37aa7282..9c862fcf 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -1,12 +1,12 @@ -from couchpotato import get_session +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File from couchpotato.environment import Env +from scandir import scandir from tornado.web import StaticFileHandler import os.path import time @@ -28,22 +28,30 @@ class FileManager(Plugin): 'return': {'type': 'file'} }) + addEvent('app.load', self.cleanup) + def cleanup(self): - # TODO: unused # Wait a bit after starting before cleanup - time.sleep(3) + time.sleep(2) log.debug('Cleaning up unused files') try: - db = get_session() - for root, dirs, walk_files in os.walk(Env.get('cache_dir')): - for filename in walk_files: - if os.path.splitext(filename)[1] in ['.png', '.jpg', '.jpeg']: - file_path = os.path.join(root, filename) - f = db.query(File).filter(File.path == toUnicode(file_path)).first() - if not f: - os.remove(file_path) + db = get_db() + cache_dir = Env.get('cache_dir') + medias = db.all('media', with_doc = True) + + files = [] + for media in medias: + file_dict = media['doc'].get('files', {}) + for x in file_dict.keys(): + files.extend(file_dict[x]) + + for file in scandir.scandir(cache_dir): + if os.path.splitext(file.name)[1] in ['.png', '.jpg', '.jpeg']: + file_path = os.path.join(cache_dir, file.name) + if toUnicode(file_path) not in files: + os.remove(file_path) except: log.error('Failed removing unused file: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 1c734b13..26065005 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -48,6 +48,7 @@ class Manage(Plugin): if not Env.get('dev') and self.conf('startup_scan'): addEvent('app.load', self.updateLibraryQuick) + addEvent('app.load', self.updateLibrary) def getProgress(self, **kwargs): return { @@ -67,7 +68,7 @@ class Manage(Plugin): return self.updateLibrary(full = False) def updateLibrary(self, full = True): - last_update = float(Env.prop('manage.last_update', default = 0)) + last_update = 0 #float(Env.prop('manage.last_update', default = 0)) if self.in_progress: log.info('Already updating library: %s', self.in_progress) @@ -184,13 +185,12 @@ class Manage(Plugin): 'to_go': total_found, }) - if group['media'] and group['media'].get('identifier'): - identifier = group['media'].get('identifier') - added_identifiers.append(identifier) + if group['media'] and group['identifier']: + added_identifiers.append(group['identifier']) # Add it to release and update the info fireEvent('release.add', group = group) - fireEvent('movie.update_info', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) + fireEvent('movie.update_info', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier'])) else: self.updateProgress(folder) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 1c3ec630..53a20d58 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -1,13 +1,11 @@ import traceback -from couchpotato import get_session, get_db, tryInt +from couchpotato import get_db, tryInt from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ProfileIndex -from couchpotato.core.settings.model import Profile, ProfileType log = CPLog(__name__) @@ -82,6 +80,7 @@ class ProfilePlugin(Plugin): db = get_db() profile = { + '_t': 'profile', 'label': toUnicode(kwargs.get('label')), 'order': tryInt(kwargs.get('order', 999)), 'core': kwargs.get('core', False), @@ -101,11 +100,11 @@ class ProfilePlugin(Plugin): id = kwargs.get('id') try: p = db.get('id', id) + profile['order'] = tryInt(kwargs.get('order', p.get('order', 999))) except: p = db.insert(profile) - p.update(profile) - p['order'] = tryInt(kwargs.get('order', p.get('order', 999))) + p.update(profile) db.update(p) return { @@ -151,15 +150,14 @@ class ProfilePlugin(Plugin): def delete(self, id = None, **kwargs): try: - db = get_session() + db = get_db() success = False message = '' - try: - p = db.query(Profile).filter_by(id = id).first() + try: + p = db.get('id', id) db.delete(p) - db.commit() # Force defaults on all empty profile movies self.forceDefaults() diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 265cdf2d..9bd76ebe 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -1,5 +1,5 @@ import traceback -from couchpotato import get_session, get_db +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, ss @@ -7,7 +7,6 @@ from couchpotato.core.helpers.variable import mergeDicts, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.quality.index import QualityIndex -from couchpotato.core.settings.model import Quality import re log = CPLog(__name__) @@ -124,12 +123,12 @@ class QualityPlugin(Plugin): def saveSize(self, **kwargs): try: - db = get_session() - quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first() + db = get_db() + quality = db.get('quality', kwargs.get('identifier'), with_doc = True) if quality: - setattr(quality, kwargs.get('value_type'), kwargs.get('value')) - db.commit() + quality['doc'][kwargs.get('value_type')] = kwargs.get('value') + db.update(quality['doc']) self.cached_qualities = None diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 11c12563..2629fc64 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -9,11 +9,11 @@ class ReleaseIndex(TreeBasedIndex): super(ReleaseIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).hexdigest() + return key def make_key_value(self, data): if data.get('_t') == 'release' and data.get('media_id'): - return md5(data['media_id']).hexdigest(), {'media_id': data.get('media_id')} + return data['media_id'], None def run_for_media(self, db, media_id): for release in db.get_many('release', media_id, with_doc = True): @@ -49,6 +49,7 @@ class ReleaseIDIndex(TreeBasedIndex): super(ReleaseIDIndex, self).__init__(*args, **kwargs) def make_key(self, key): + print key return md5(key).hexdigest() def make_key_value(self, data): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index bd805fd4..3623a1a0 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -12,7 +12,6 @@ from couchpotato.core.settings.model import Release as Relea, Media, \ from couchpotato.environment import Env from inspect import ismethod, isfunction from sqlalchemy.exc import InterfaceError -from sqlalchemy.orm import joinedload_all from sqlalchemy.sql.expression import and_, or_ import os import time @@ -110,44 +109,43 @@ class Release(Plugin): def add(self, group): try: - db = get_session() + db = get_db() identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) - # Add movie - media = db.query(Media).filter_by(library_id = group['library'].get('id')).first() - if not media: - media = Media( - library_id = group['library'].get('id'), - profile_id = 0, - status = 'done' - ) - db.add(media) - db.commit() + # Add movie if it doesn't exist + try: + media = db.get('media', group['identifier'], with_doc = True)['doc'] + media['status'] = 'done' + db.update(media) + except: + media = { + '_t': 'media', + 'identifier': group['identifier'], + 'profile_id': None, + 'status': 'done' + } + m = db.insert(media) + media.update(m) # Add Release - rel = db.query(Relea).filter( - or_( - Relea.identifier == identifier, - and_(Relea.identifier.startswith(group['identifier']), Relea.status == 'snatched') - ) - ).first() - if not rel: - rel = Relea( - identifier = identifier, - movie = media, - quality_id = group['meta_data']['quality'].get('id'), - status = 'done' - ) - db.add(rel) - db.commit() + release = { + '_t': 'release', + 'media_id': media['_id'], + 'identifier': identifier, + 'quality': group['meta_data']['quality'].get('identifier'), + 'status': 'done' + } + try: + r = db.get('release_identifier', identifier, with_doc = True)['doc'] + release.update(r) + db.update(release) + except: + r = db.insert(release) + release.update(r) # Add each file type - rel['files'] = [] - for type in group['files']: - for cur_file in group['files'][type]: - added_file = self.saveFile(cur_file, type = type) - rel['files'].append(added_file.get('id')) + release['files'] = dict((k, v) for k, v in group['files'].items() if v) fireEvent('media.restatus', media['_id']) @@ -157,15 +155,6 @@ class Release(Plugin): return False - def saveFile(self, filepath, type = 'unknown', include_media_info = False): - - # Check database and update/insert if necessary - return { - 'type': '%s_%s' % Scanner.file_types.get(type), - 'path': filepath, - 'part': fireEvent('scanner.partnumber', file, single = True), - } - def deleteView(self, id = None, **kwargs): return { diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 744c7bd6..7d384246 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -1,3 +1,4 @@ +from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \ @@ -6,6 +7,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info +from scandir import scandir from subliminal.videos import Video import enzyme import os @@ -123,7 +125,7 @@ class Scanner(Plugin): check_file_date = True try: files = [] - for root, dirs, walk_files in os.walk(folder): + for root, dirs, walk_files in scandir.walk(folder): files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) # Break if CP wants to shut down @@ -138,7 +140,6 @@ class Scanner(Plugin): check_file_date = False files = [sp(x) for x in files] - for file_path in files: if not os.path.exists(file_path): @@ -418,7 +419,7 @@ class Scanner(Plugin): if not group['media']: log.error('Unable to determine media: %s', group['identifiers']) else: - group['identifier'] = group['media']['identifier'] + group['identifier'] = group['media']['imdb'] processed_movies[identifier] = group @@ -610,9 +611,12 @@ class Scanner(Plugin): log.debug('Identifier to short to use for search: %s', identifier) if imdb_id: - return fireEvent('library.add.movie', attrs = { - 'identifier': imdb_id - }, update_after = False, single = True) + try: + db = get_db() + return db.get('media', imdb_id, with_doc = True)['doc']['info'] + except: + log.debug('Movie "%s" not in library, just getting info', imdb_id) + return fireEvent('movie.info', identifier = imdb_id, merge = True, extended = False) log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers']) return {} diff --git a/libs/scandir/.gitattributes b/libs/scandir/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/libs/scandir/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/libs/scandir/.gitignore b/libs/scandir/.gitignore new file mode 100644 index 00000000..48878c73 --- /dev/null +++ b/libs/scandir/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*.pyd +benchtree +build diff --git a/libs/scandir/__init__.py b/libs/scandir/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/scandir/_scandir.c b/libs/scandir/_scandir.c new file mode 100644 index 00000000..26c81eda --- /dev/null +++ b/libs/scandir/_scandir.c @@ -0,0 +1,373 @@ +// scandir C speedups +// +// TODO: this is a work in progress! +// +// There's a fair bit of PY_MAJOR_VERSION boilerplate to support both Python 2 +// and Python 3 -- the structure of this is taken from here: +// http://docs.python.org/3.3/howto/cporting.html + +#include +#include + +#ifdef MS_WINDOWS +#include +#endif + +#if PY_MAJOR_VERSION >= 3 +#define INITERROR return NULL +#define FROM_LONG PyLong_FromLong +#define FROM_STRING PyUnicode_FromStringAndSize +#else +#define INITERROR return +#define FROM_LONG PyInt_FromLong +#define FROM_STRING PyString_FromStringAndSize +#endif + +#ifdef MS_WINDOWS + +static PyObject * +win32_error_unicode(char* function, Py_UNICODE* filename) +{ + errno = GetLastError(); + if (filename) + return PyErr_SetFromWindowsErrWithUnicodeFilename(errno, filename); + else + return PyErr_SetFromWindowsErr(errno); +} + +/* Below, we *know* that ugo+r is 0444 */ +#if _S_IREAD != 0400 +#error Unsupported C library +#endif +static int +attributes_to_mode(DWORD attr) +{ + int m = 0; + if (attr & FILE_ATTRIBUTE_DIRECTORY) + m |= _S_IFDIR | 0111; /* IFEXEC for user,group,other */ + else + m |= _S_IFREG; + if (attr & FILE_ATTRIBUTE_READONLY) + m |= 0444; + else + m |= 0666; + if (attr & FILE_ATTRIBUTE_REPARSE_POINT) + m |= 0120000; // S_IFLNK + return m; +} + +double +filetime_to_time(FILETIME *filetime) +{ + const double SECONDS_BETWEEN_EPOCHS = 11644473600.0; + + unsigned long long total = (unsigned long long)filetime->dwHighDateTime << 32 | + (unsigned long long)filetime->dwLowDateTime; + return (double)total / 10000000.0 - SECONDS_BETWEEN_EPOCHS; +} + +static PyTypeObject StatResultType; + +static PyObject * +find_data_to_statresult(WIN32_FIND_DATAW *data) +{ + PY_LONG_LONG size; + PyObject *v = PyStructSequence_New(&StatResultType); + if (v == NULL) + return NULL; + + size = (PY_LONG_LONG)data->nFileSizeHigh << 32 | + (PY_LONG_LONG)data->nFileSizeLow; + + PyStructSequence_SET_ITEM(v, 0, FROM_LONG(attributes_to_mode(data->dwFileAttributes))); + PyStructSequence_SET_ITEM(v, 1, FROM_LONG(0)); + PyStructSequence_SET_ITEM(v, 2, FROM_LONG(0)); + PyStructSequence_SET_ITEM(v, 3, FROM_LONG(0)); + PyStructSequence_SET_ITEM(v, 4, FROM_LONG(0)); + PyStructSequence_SET_ITEM(v, 5, FROM_LONG(0)); + PyStructSequence_SET_ITEM(v, 6, PyLong_FromLongLong((PY_LONG_LONG)size)); + PyStructSequence_SET_ITEM(v, 7, PyFloat_FromDouble(filetime_to_time(&data->ftLastAccessTime))); + PyStructSequence_SET_ITEM(v, 8, PyFloat_FromDouble(filetime_to_time(&data->ftLastWriteTime))); + PyStructSequence_SET_ITEM(v, 9, PyFloat_FromDouble(filetime_to_time(&data->ftCreationTime))); + + if (PyErr_Occurred()) { + Py_DECREF(v); + return NULL; + } + + return v; +} + +static PyStructSequence_Field stat_result_fields[] = { + {"st_mode", "protection bits"}, + {"st_ino", "inode"}, + {"st_dev", "device"}, + {"st_nlink", "number of hard links"}, + {"st_uid", "user ID of owner"}, + {"st_gid", "group ID of owner"}, + {"st_size", "total size, in bytes"}, + {"st_atime", "time of last access"}, + {"st_mtime", "time of last modification"}, + {"st_ctime", "time of last change"}, + {0} +}; + +static PyStructSequence_Desc stat_result_desc = { + "stat_result", /* name */ + NULL, /* doc */ + stat_result_fields, + 10 +}; + +static PyObject * +scandir_helper(PyObject *self, PyObject *args) +{ + PyObject *d, *v; + HANDLE hFindFile; + BOOL result; + WIN32_FIND_DATAW wFileData; + Py_UNICODE *wnamebuf; + Py_ssize_t len; + PyObject *po; + PyObject *name_stat; + + if (!PyArg_ParseTuple(args, "U:scandir_helper", &po)) + return NULL; + + /* Overallocate for \\*.*\0 */ + len = PyUnicode_GET_SIZE(po); + wnamebuf = malloc((len + 5) * sizeof(wchar_t)); + if (!wnamebuf) { + PyErr_NoMemory(); + return NULL; + } + wcscpy(wnamebuf, PyUnicode_AS_UNICODE(po)); + if (len > 0) { + Py_UNICODE wch = wnamebuf[len-1]; + if (wch != L'/' && wch != L'\\' && wch != L':') + wnamebuf[len++] = L'\\'; + wcscpy(wnamebuf + len, L"*.*"); + } + if ((d = PyList_New(0)) == NULL) { + free(wnamebuf); + return NULL; + } + Py_BEGIN_ALLOW_THREADS + hFindFile = FindFirstFileW(wnamebuf, &wFileData); + Py_END_ALLOW_THREADS + if (hFindFile == INVALID_HANDLE_VALUE) { + int error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) { + free(wnamebuf); + return d; + } + Py_DECREF(d); + win32_error_unicode("FindFirstFileW", wnamebuf); + free(wnamebuf); + return NULL; + } + do { + /* Skip over . and .. */ + if (wcscmp(wFileData.cFileName, L".") != 0 && + wcscmp(wFileData.cFileName, L"..") != 0) { + v = PyUnicode_FromUnicode(wFileData.cFileName, wcslen(wFileData.cFileName)); + if (v == NULL) { + Py_DECREF(d); + d = NULL; + break; + } + name_stat = Py_BuildValue("ON", v, find_data_to_statresult(&wFileData)); + if (name_stat == NULL) { + Py_DECREF(v); + Py_DECREF(d); + d = NULL; + break; + } + if (PyList_Append(d, name_stat) != 0) { + Py_DECREF(v); + Py_DECREF(d); + Py_DECREF(name_stat); + d = NULL; + break; + } + Py_DECREF(name_stat); + Py_DECREF(v); + } + Py_BEGIN_ALLOW_THREADS + result = FindNextFileW(hFindFile, &wFileData); + Py_END_ALLOW_THREADS + /* FindNextFile sets error to ERROR_NO_MORE_FILES if + it got to the end of the directory. */ + if (!result && GetLastError() != ERROR_NO_MORE_FILES) { + Py_DECREF(d); + win32_error_unicode("FindNextFileW", wnamebuf); + FindClose(hFindFile); + free(wnamebuf); + return NULL; + } + } while (result == TRUE); + + if (FindClose(hFindFile) == FALSE) { + Py_DECREF(d); + win32_error_unicode("FindClose", wnamebuf); + free(wnamebuf); + return NULL; + } + free(wnamebuf); + return d; +} + +#else // Linux / OS X + +#include +#define NAMLEN(dirent) strlen((dirent)->d_name) + +static PyObject * +posix_error_with_allocated_filename(char* name) +{ + PyObject *rc = PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); + PyMem_Free(name); + return rc; +} + +static PyObject * +scandir_helper(PyObject *self, PyObject *args) +{ + char *name = NULL; + PyObject *d, *v, *name_type; + DIR *dirp; + struct dirent *ep; + int arg_is_unicode = 1; + + errno = 0; + if (!PyArg_ParseTuple(args, "U:scandir_helper", &v)) { + arg_is_unicode = 0; + PyErr_Clear(); + } + if (!PyArg_ParseTuple(args, "et:scandir_helper", Py_FileSystemDefaultEncoding, &name)) + return NULL; + Py_BEGIN_ALLOW_THREADS + dirp = opendir(name); + Py_END_ALLOW_THREADS + if (dirp == NULL) { + return posix_error_with_allocated_filename(name); + } + if ((d = PyList_New(0)) == NULL) { + Py_BEGIN_ALLOW_THREADS + closedir(dirp); + Py_END_ALLOW_THREADS + PyMem_Free(name); + return NULL; + } + for (;;) { + errno = 0; + Py_BEGIN_ALLOW_THREADS + ep = readdir(dirp); + Py_END_ALLOW_THREADS + if (ep == NULL) { + if (errno == 0) { + break; + } else { + Py_BEGIN_ALLOW_THREADS + closedir(dirp); + Py_END_ALLOW_THREADS + Py_DECREF(d); + return posix_error_with_allocated_filename(name); + } + } + if (ep->d_name[0] == '.' && + (NAMLEN(ep) == 1 || + (ep->d_name[1] == '.' && NAMLEN(ep) == 2))) + continue; + v = FROM_STRING(ep->d_name, NAMLEN(ep)); + if (v == NULL) { + Py_DECREF(d); + d = NULL; + break; + } + if (arg_is_unicode) { + PyObject *w; + + w = PyUnicode_FromEncodedObject(v, + Py_FileSystemDefaultEncoding, + "strict"); + if (w != NULL) { + Py_DECREF(v); + v = w; + } + else { + /* fall back to the original byte string, as + discussed in patch #683592 */ + PyErr_Clear(); + } + } + name_type = Py_BuildValue("ON", v, FROM_LONG(ep->d_type)); + if (name_type == NULL) { + Py_DECREF(v); + Py_DECREF(d); + d = NULL; + break; + } + if (PyList_Append(d, name_type) != 0) { + Py_DECREF(v); + Py_DECREF(d); + Py_DECREF(name_type); + d = NULL; + break; + } + Py_DECREF(name_type); + Py_DECREF(v); + } + Py_BEGIN_ALLOW_THREADS + closedir(dirp); + Py_END_ALLOW_THREADS + PyMem_Free(name); + + return d; +} + +#endif + +static PyMethodDef scandir_methods[] = { + {"scandir_helper", (PyCFunction)scandir_helper, METH_VARARGS, NULL}, + {NULL, NULL}, +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_scandir", + NULL, + 0, + scandir_methods, + NULL, + NULL, + NULL, + NULL, +}; +#endif + +#if PY_MAJOR_VERSION >= 3 +PyObject * +PyInit__scandir(void) +{ + PyObject *module = PyModule_Create(&moduledef); +#else +void +init_scandir(void) +{ + PyObject *module = Py_InitModule("_scandir", scandir_methods); +#endif + if (module == NULL) { + INITERROR; + } + +#ifdef MS_WINDOWS + stat_result_desc.name = "scandir.stat_result"; + PyStructSequence_InitType(&StatResultType, &stat_result_desc); +#endif + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} diff --git a/libs/scandir/scandir.py b/libs/scandir/scandir.py new file mode 100644 index 00000000..1e34f8e5 --- /dev/null +++ b/libs/scandir/scandir.py @@ -0,0 +1,456 @@ +"""scandir, a better directory iterator that exposes all file info OS provides + +scandir is a generator version of os.listdir() that returns an iterator over +files in a directory, and also exposes the extra information most OSes provide +while iterating files in a directory. + +See README.md or https://github.com/benhoyt/scandir for rationale and docs. + +scandir is released under the new BSD 3-clause license. See LICENSE.txt for +the full license text. +""" + +from __future__ import division + +import ctypes +import os +import stat +import sys + +__version__ = '0.3' +__all__ = ['scandir', 'walk'] + +# Shortcuts to these functions for speed and ease +join = os.path.join +lstat = os.lstat + +S_IFDIR = stat.S_IFDIR +S_IFREG = stat.S_IFREG +S_IFLNK = stat.S_IFLNK + +# 'unicode' isn't defined on Python 3 +try: + unicode +except NameError: + unicode = str + +_scandir = None + + +class GenericDirEntry(object): + __slots__ = ('name', '_lstat', '_path') + + def __init__(self, path, name): + self._path = path + self.name = name + self._lstat = None + + def lstat(self): + if self._lstat is None: + self._lstat = lstat(join(self._path, self.name)) + return self._lstat + + def is_dir(self): + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFDIR + + def is_file(self): + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFREG + + def is_symlink(self): + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFLNK + + def __str__(self): + return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) + + __repr__ = __str__ + + +if sys.platform == 'win32': + from ctypes import wintypes + + # Various constants from windows.h + INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value + ERROR_FILE_NOT_FOUND = 2 + ERROR_NO_MORE_FILES = 18 + FILE_ATTRIBUTE_READONLY = 1 + FILE_ATTRIBUTE_DIRECTORY = 16 + FILE_ATTRIBUTE_REPARSE_POINT = 1024 + + # Numer of seconds between 1601-01-01 and 1970-01-01 + SECONDS_BETWEEN_EPOCHS = 11644473600 + + kernel32 = ctypes.windll.kernel32 + + # ctypes wrappers for (wide string versions of) FindFirstFile, + # FindNextFile, and FindClose + FindFirstFile = kernel32.FindFirstFileW + FindFirstFile.argtypes = [ + wintypes.LPCWSTR, + ctypes.POINTER(wintypes.WIN32_FIND_DATAW), + ] + FindFirstFile.restype = wintypes.HANDLE + + FindNextFile = kernel32.FindNextFileW + FindNextFile.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(wintypes.WIN32_FIND_DATAW), + ] + FindNextFile.restype = wintypes.BOOL + + FindClose = kernel32.FindClose + FindClose.argtypes = [wintypes.HANDLE] + FindClose.restype = wintypes.BOOL + + def filetime_to_time(filetime): + """Convert Win32 FILETIME to time since Unix epoch in seconds.""" + total = filetime.dwHighDateTime << 32 | filetime.dwLowDateTime + return total / 10000000 - SECONDS_BETWEEN_EPOCHS + + def find_data_to_stat(data): + """Convert Win32 FIND_DATA struct to stat_result.""" + # First convert Win32 dwFileAttributes to st_mode + attributes = data.dwFileAttributes + st_mode = 0 + if attributes & FILE_ATTRIBUTE_DIRECTORY: + st_mode |= S_IFDIR | 0o111 + else: + st_mode |= S_IFREG + if attributes & FILE_ATTRIBUTE_READONLY: + st_mode |= 0o444 + else: + st_mode |= 0o666 + if attributes & FILE_ATTRIBUTE_REPARSE_POINT: + st_mode |= S_IFLNK + + st_size = data.nFileSizeHigh << 32 | data.nFileSizeLow + st_atime = filetime_to_time(data.ftLastAccessTime) + st_mtime = filetime_to_time(data.ftLastWriteTime) + st_ctime = filetime_to_time(data.ftCreationTime) + + # Some fields set to zero per CPython's posixmodule.c: st_ino, st_dev, + # st_nlink, st_uid, st_gid + return os.stat_result((st_mode, 0, 0, 0, 0, 0, st_size, st_atime, + st_mtime, st_ctime)) + + class Win32DirEntry(object): + __slots__ = ('name', '_lstat', '_find_data') + + def __init__(self, name, find_data): + self.name = name + self._lstat = None + self._find_data = find_data + + def lstat(self): + if self._lstat is None: + # Lazily convert to stat object, because it's slow, and often + # we only need is_dir() etc + self._lstat = find_data_to_stat(self._find_data) + return self._lstat + + def is_dir(self): + return (self._find_data.dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY != 0) + + def is_file(self): + return (self._find_data.dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY == 0) + + def is_symlink(self): + return (self._find_data.dwFileAttributes & + FILE_ATTRIBUTE_REPARSE_POINT != 0) + + def __str__(self): + return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) + + __repr__ = __str__ + + def win_error(error, filename): + exc = WindowsError(error, ctypes.FormatError(error)) + exc.filename = filename + return exc + + def scandir(path='.', windows_wildcard='*.*'): + """Like os.listdir(), but yield DirEntry objects instead of returning + a list of names. + """ + # Call FindFirstFile and handle errors + data = wintypes.WIN32_FIND_DATAW() + data_p = ctypes.byref(data) + filename = join(path, windows_wildcard) + handle = FindFirstFile(filename, data_p) + if handle == INVALID_HANDLE_VALUE: + error = ctypes.GetLastError() + if error == ERROR_FILE_NOT_FOUND: + # No files, don't yield anything + return + raise win_error(error, path) + + # Call FindNextFile in a loop, stopping when no more files + try: + while True: + # Skip '.' and '..' (current and parent directory), but + # otherwise yield (filename, stat_result) tuple + name = data.cFileName + if name not in ('.', '..'): + yield Win32DirEntry(name, data) + + data = wintypes.WIN32_FIND_DATAW() + data_p = ctypes.byref(data) + success = FindNextFile(handle, data_p) + if not success: + error = ctypes.GetLastError() + if error == ERROR_NO_MORE_FILES: + break + raise win_error(error, path) + finally: + if not FindClose(handle): + raise win_error(ctypes.GetLastError(), path) + + try: + import _scandir + + scandir_helper = _scandir.scandir_helper + + class Win32DirEntry(object): + __slots__ = ('name', '_lstat') + + def __init__(self, name, lstat): + self.name = name + self._lstat = lstat + + def lstat(self): + return self._lstat + + def is_dir(self): + return self._lstat.st_mode & 0o170000 == S_IFDIR + + def is_file(self): + return self._lstat.st_mode & 0o170000 == S_IFREG + + def is_symlink(self): + return self._lstat.st_mode & 0o170000 == S_IFLNK + + def __str__(self): + return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) + + __repr__ = __str__ + + def scandir(path='.'): + for name, stat in scandir_helper(unicode(path)): + yield Win32DirEntry(name, stat) + + except ImportError: + pass + + +# Linux, OS X, and BSD implementation +elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: + import ctypes.util + + DIR_p = ctypes.c_void_p + + # Rather annoying how the dirent struct is slightly different on each + # platform. The only fields we care about are d_name and d_type. + class Dirent(ctypes.Structure): + if sys.platform.startswith('linux'): + _fields_ = ( + ('d_ino', ctypes.c_ulong), + ('d_off', ctypes.c_long), + ('d_reclen', ctypes.c_ushort), + ('d_type', ctypes.c_byte), + ('d_name', ctypes.c_char * 256), + ) + else: + _fields_ = ( + ('d_ino', ctypes.c_uint32), # must be uint32, not ulong + ('d_reclen', ctypes.c_ushort), + ('d_type', ctypes.c_byte), + ('d_namlen', ctypes.c_byte), + ('d_name', ctypes.c_char * 256), + ) + + DT_UNKNOWN = 0 + DT_DIR = 4 + DT_REG = 8 + DT_LNK = 10 + + Dirent_p = ctypes.POINTER(Dirent) + Dirent_pp = ctypes.POINTER(Dirent_p) + + libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) + opendir = libc.opendir + opendir.argtypes = [ctypes.c_char_p] + opendir.restype = DIR_p + + readdir_r = libc.readdir_r + readdir_r.argtypes = [DIR_p, Dirent_p, Dirent_pp] + readdir_r.restype = ctypes.c_int + + closedir = libc.closedir + closedir.argtypes = [DIR_p] + closedir.restype = ctypes.c_int + + file_system_encoding = sys.getfilesystemencoding() + + class PosixDirEntry(object): + __slots__ = ('name', '_d_type', '_lstat', '_path') + + def __init__(self, path, name, d_type): + self._path = path + self.name = name + self._d_type = d_type + self._lstat = None + + def lstat(self): + if self._lstat is None: + self._lstat = lstat(join(self._path, self.name)) + return self._lstat + + # Ridiculous duplication between these is* functions -- helps a little + # bit with os.walk() performance compared to calling another function. + def is_dir(self): + d_type = self._d_type + if d_type != DT_UNKNOWN: + return d_type == DT_DIR + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFDIR + + def is_file(self): + d_type = self._d_type + if d_type != DT_UNKNOWN: + return d_type == DT_REG + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFREG + + def is_symlink(self): + d_type = self._d_type + if d_type != DT_UNKNOWN: + return d_type == DT_LNK + try: + self.lstat() + except OSError: + return False + return self._lstat.st_mode & 0o170000 == S_IFLNK + + def __str__(self): + return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) + + __repr__ = __str__ + + def posix_error(filename): + errno = ctypes.get_errno() + exc = OSError(errno, os.strerror(errno)) + exc.filename = filename + return exc + + def scandir(path='.'): + """Like os.listdir(), but yield DirEntry objects instead of returning + a list of names. + """ + dir_p = opendir(path.encode(file_system_encoding)) + if not dir_p: + raise posix_error(path) + try: + result = Dirent_p() + while True: + entry = Dirent() + if readdir_r(dir_p, entry, result): + raise posix_error(path) + if not result: + break + name = entry.d_name.decode(file_system_encoding) + if name not in ('.', '..'): + yield PosixDirEntry(path, name, entry.d_type) + finally: + if closedir(dir_p): + raise posix_error(path) + + try: + import _scandir + + scandir_helper = _scandir.scandir_helper + + def scandir(path='.'): + for name, d_type in scandir_helper(unicode(path)): + yield PosixDirEntry(path, name, d_type) + + except ImportError: + pass + + +# Some other system -- no d_type or stat information +else: + def scandir(path='.'): + """Like os.listdir(), but yield DirEntry objects instead of returning + a list of names. + """ + for name in os.listdir(path): + yield GenericDirEntry(path, name) + + +def walk(top, topdown=True, onerror=None, followlinks=False): + """Like os.walk(), but faster, as it uses scandir() internally.""" + # Determine which are files and which are directories + dirs = [] + nondirs = [] + try: + for entry in scandir(top): + if entry.is_dir(): + dirs.append(entry) + else: + nondirs.append(entry) + except OSError as error: + if onerror is not None: + onerror(error) + return + + # Yield before recursion if going top down + if topdown: + # Need to do some fancy footwork here as caller is allowed to modify + # dir_names, and we really want them to modify dirs (list of DirEntry + # objects) instead. Keep a mapping of entries keyed by name. + dir_names = [] + entries_by_name = {} + for entry in dirs: + dir_names.append(entry.name) + entries_by_name[entry.name] = entry + + yield top, dir_names, [e.name for e in nondirs] + + dirs = [] + for dir_name in dir_names: + entry = entries_by_name.get(dir_name) + if entry is None: + # Only happens when caller creates a new directory and adds it + # to dir_names + entry = GenericDirEntry(top, dir_name) + dirs.append(entry) + + # Recurse into sub-directories, following symbolic links if "followlinks" + for entry in dirs: + if followlinks or not entry.is_symlink(): + new_path = join(top, entry.name) + for x in walk(new_path, topdown, onerror, followlinks): + yield x + + # Yield before recursion if going bottom up + if not topdown: + yield top, [e.name for e in dirs], [e.name for e in nondirs] From b9a8ca14c3065b2db118b910d6aa7bc95b8b26c6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Feb 2014 18:21:47 +0100 Subject: [PATCH 011/468] nosql --- couchpotato/core/media/_base/media/main.py | 4 +- .../media/movie/_base/static/movie.actions.js | 216 +++++++++--------- .../core/media/movie/_base/static/movie.js | 2 +- couchpotato/core/media/movie/searcher/main.py | 4 +- .../notifications/core/static/notification.js | 2 +- couchpotato/core/plugins/manage/main.py | 36 +-- couchpotato/core/plugins/release/index.py | 4 +- couchpotato/core/plugins/release/main.py | 36 +-- couchpotato/core/plugins/renamer/main.py | 6 +- couchpotato/runner.py | 1 - 10 files changed, 153 insertions(+), 158 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 480b571f..af12bfb4 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -124,7 +124,7 @@ class MediaPlugin(MediaBase): if refresh_handler: handlers.append(refresh_handler) - fireEvent('notify.frontend', type = 'media.busy', data = {'id': ids}) + fireEvent('notify.frontend', type = 'media.busy', data = {'_id': ids}) fireEventAsync('schedule.queue', handlers = handlers) return { @@ -266,7 +266,7 @@ class MediaPlugin(MediaBase): def listView(self, **kwargs): - types = splitString(kwargs.get('types')) + types = splitString(kwargs.get('type')) status = splitString(kwargs.get('status')) release_status = splitString(kwargs.get('release_status')) limit_offset = kwargs.get('limit_offset') diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 50548ccb..4152d824 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -121,7 +121,7 @@ MA.Release = new Class({ } }); - if(self.movie.data.releases.length == 0) + if(!self.movie.data.releases || self.movie.data.releases.length == 0) self.el.hide() else self.showHelper(); @@ -166,92 +166,93 @@ MA.Release = new Class({ new Element('span.provider', {'text': 'Provider'}) ).inject(self.release_container) - self.movie.data.releases.each(function(release){ + if(self.movie.data.releases) + self.movie.data.releases.each(function(release){ - var quality = Quality.getQuality(release.quality) || {}, - info = release.info, - provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : ''); + var quality = Quality.getQuality(release.quality) || {}, + info = release.info, + provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : ''); - var release_name = self.get(release, 'name'); - if(release.files && release.files.length > 0){ - try { - var movie_file = release.files.filter(function(file){ - var type = File.Type.get(file.type_id); - return type && type.identifier == 'movie' - }).pick(); - release_name = movie_file.path.split(Api.getOption('path_sep')).getLast(); - } - catch(e){} - } - - // Create release - var item = new Element('div', { - 'class': 'item '+release.status, - 'id': 'release_'+release._id - }).adopt( - new Element('span.name', {'text': release_name, 'title': release_name}), - new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), - new Element('span.quality', {'text': quality.label || 'n/a'}), - new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), - new Element('span.age', {'text': self.get(release, 'age')}), - new Element('span.score', {'text': self.get(release, 'score')}), - new Element('span.provider', { 'text': provider, 'title': provider }), - release.info['detail_url'] ? new Element('a.info.icon2', { - 'href': release.info['detail_url'], - 'target': '_blank' - }) : new Element('a'), - new Element('a.download.icon2', { - 'events': { - 'click': function(e){ - (e).preventDefault(); - if(!this.hasClass('completed')) - self.download(release); - } + var release_name = self.get(release, 'name'); + if(release.files && release.files.length > 0){ + try { + var movie_file = release.files.filter(function(file){ + var type = File.Type.get(file.type_id); + return type && type.identifier == 'movie' + }).pick(); + release_name = movie_file.path.split(Api.getOption('path_sep')).getLast(); } - }), - new Element('a.delete.icon2', { - 'events': { - 'click': function(e){ - (e).preventDefault(); - self.ignore(release); - } - } - }) - ).inject(self.release_container); - release['el'] = item; - - if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ - if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) - self.last_release = release; - } - else if(!self.next_release && release.status == 'available'){ - self.next_release = release; - } - - var update_handle = function(notification) { - if(notification.data._id != release._id) return; - - var q = self.movie.quality.getElement('.q_' + release.quality), - new_status = notification.data.status; - - release.el.set('class', 'item ' + new_status); - - var status_el = release.el.getElement('.release_status'); - status_el.set('class', 'release_status ' + new_status); - status_el.set('text', new_status); - - if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) - var q = self.addQuality(release.quality_id); - - if(q && !q.hasClass(new_status)) { - q.removeClass(release.status).addClass(new_status); - q.set('title', q.get('title').replace(release.status, new_status)); + catch(e){} } - } - App.on('release.update_status', update_handle); + // Create release + var item = new Element('div', { + 'class': 'item '+release.status, + 'id': 'release_'+release._id + }).adopt( + new Element('span.name', {'text': release_name, 'title': release_name}), + new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), + new Element('span.quality', {'text': quality.label || 'n/a'}), + new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), + new Element('span.age', {'text': self.get(release, 'age')}), + new Element('span.score', {'text': self.get(release, 'score')}), + new Element('span.provider', { 'text': provider, 'title': provider }), + release.info['detail_url'] ? new Element('a.info.icon2', { + 'href': release.info['detail_url'], + 'target': '_blank' + }) : new Element('a'), + new Element('a.download.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + if(!this.hasClass('completed')) + self.download(release); + } + } + }), + new Element('a.delete.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + self.ignore(release); + } + } + }) + ).inject(self.release_container); + release['el'] = item; - }); + if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ + if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) + self.last_release = release; + } + else if(!self.next_release && release.status == 'available'){ + self.next_release = release; + } + + var update_handle = function(notification) { + if(notification.data._id != release._id) return; + + var q = self.movie.quality.getElement('.q_' + release.quality), + new_status = notification.data.status; + + release.el.set('class', 'item ' + new_status); + + var status_el = release.el.getElement('.release_status'); + status_el.set('class', 'release_status ' + new_status); + status_el.set('text', new_status); + + if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) + var q = self.addQuality(release.quality_id); + + if(q && !q.hasClass(new_status)) { + q.removeClass(release.status).addClass(new_status); + q.set('title', q.get('title').replace(release.status, new_status)); + } + } + + App.on('release.update_status', update_handle); + + }); if(self.last_release) self.release_container.getElements('#release_'+self.last_release._id).addClass('last_release'); @@ -314,16 +315,17 @@ MA.Release = new Class({ var has_available = false, has_snatched = false; - self.movie.data.releases.each(function(release){ - if(has_available && has_snatched) return; + if(self.movie.data.releases) + self.movie.data.releases.each(function(release){ + if(has_available && has_snatched) return; - if(['snatched', 'downloaded', 'seeding'].contains(release.status)) - has_snatched = true; + if(['snatched', 'downloaded', 'seeding'].contains(release.status)) + has_snatched = true; - if(['available'].contains(release.status)) - has_available = true; + if(['available'].contains(release.status)) + has_available = true; - }); + }); if(has_available || has_snatched){ @@ -698,7 +700,7 @@ MA.Readd = new Class({ var self = this; var movie_done = self.movie.data.status == 'done'; - if(!movie_done) + if(self.movie.data.releases && !movie_done) var snatched = self.movie.data.releases.filter(function(release){ return release.status && (release.status == 'snatched' || release.status == 'downloaded' || release.status == 'done'); }).length; @@ -832,15 +834,7 @@ MA.Files = new Class({ }, - show: function(e){ - var self = this; - (e).preventDefault(); - - self.showFiles(); - - }, - - showFiles: function(){ + show: function(){ var self = this; if(!self.options_container){ @@ -851,26 +845,26 @@ MA.Files = new Class({ // Header new Element('div.item.head').adopt( new Element('span.name', {'text': 'File'}), - new Element('span.type', {'text': 'Type'}), - new Element('span.is_available', {'text': 'Available'}) + new Element('span.type', {'text': 'Type'}) ).inject(self.files_container) - Array.each(self.releases, function(release){ + if(self.movie.data.releases) + Array.each(self.movie.data.releases, function(release){ + var rel = new Element('div.release').inject(self.files_container); - var rel = new Element('div.release').inject(self.files_container); - - Array.each(release.files, function(file){ - new Element('div.file.item').adopt( - new Element('span.name', {'text': file.path}), - new Element('span.type', {'text': File.Type.get(file.type_id).name}), - new Element('span.available', {'text': file.available}) - ).inject(rel) + Object.each(release.files, function(files, type){ + Array.each(files, function(file){ + new Element('div.file.item').adopt( + new Element('span.name', {'text': file}), + new Element('span.type', {'text': type}) + ).inject(rel) + }); + }); }); - }); } self.movie.slide('in', self.options_container); - }, + } }); diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 417f065d..835847e3 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -187,7 +187,7 @@ var Movie = new Class({ ) ); - if(self.thumbnail.empty) + if(!self.thumbnail || self.thumbnail.empty) self.el.addClass('no_thumbnail'); //self.changeView(self.view); diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index cecfd720..155390de 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -133,7 +133,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('media.delete', movie['_id'], single = True) return - fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['_id']}, message = 'Searching for "%s"' % default_title) + fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) db = get_db() @@ -200,7 +200,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if len(too_early_to_search) > 0: log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['_id']}) + fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) return ret diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 756c41eb..717fbf11 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -147,7 +147,7 @@ var NotificationBase = new Class({ // Process data if(json){ Array.each(json.result, function(result){ - App.trigger(result._t, [result]); + App.trigger(result._t || result.type, [result]); if(result.message && result.read === undefined) self.showMessage(result.message); }) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 26065005..65618b5b 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -1,3 +1,4 @@ +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.helpers.encoding import sp @@ -48,7 +49,6 @@ class Manage(Plugin): if not Env.get('dev') and self.conf('startup_scan'): addEvent('app.load', self.updateLibraryQuick) - addEvent('app.load', self.updateLibrary) def getProgress(self, **kwargs): return { @@ -125,32 +125,34 @@ class Manage(Plugin): fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') else: - releases = fireEvent('release.for_movie', id = done_movie.get('_id'), single = True) + db = get_db() + releases = list(db.run('release', 'for_media', done_movie.get('_id'))) for release in releases: if len(release.get('files', [])) > 0: - for release_file in release.get('files', []): - # Remove release not available anymore - if not os.path.isfile(sp(release_file['path'])): - fireEvent('release.clean', release['id']) - break + for file_type in release.get('files', {}): + for release_file in release['files'][file_type]: + # Remove release not available anymore + if not os.path.isfile(sp(release_file)): + fireEvent('release.clean', release['_id']) + break # Check if there are duplicate releases (different quality) use the last one, delete the rest if len(releases) > 1: used_files = {} for release in releases: + for file_type in release.get('files', {}): + for release_file in release['files'][file_type]: + already_used = used_files.get(release_file) - for release_file in release.get('files', []): - already_used = used_files.get(release_file['path']) - - if already_used: - if already_used < release['id']: - fireEvent('release.delete', release['id'], single = True) # delete this one + if already_used: + if already_used.get('last_edit', 0) < release.get('last_edit', 0): + fireEvent('release.delete', release['_id'], single = True) # delete current one + else: + fireEvent('release.delete', already_used['_id'], single = True) # delete previous one + break else: - fireEvent('release.delete', already_used, single = True) # delete previous one - break - else: - used_files[release_file['path']] = release.get('id') + used_files[release_file] = release del used_files Env.prop('manage.last_update', time.time()) diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 2629fc64..9317cc27 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -1,4 +1,5 @@ from hashlib import md5 +from CodernityDB.hash_index import HashIndex from CodernityDB.tree_index import TreeBasedIndex @@ -42,14 +43,13 @@ class ReleaseStatusIndex(TreeBasedIndex): return md5(data.get('status')).hexdigest(), {'media_id': data.get('media_id')} -class ReleaseIDIndex(TreeBasedIndex): +class ReleaseIDIndex(HashIndex): def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' super(ReleaseIDIndex, self).__init__(*args, **kwargs) def make_key(self, key): - print key return md5(key).hexdigest() def make_key_value(self, data): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 3623a1a0..28ae8c3f 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -111,41 +111,41 @@ class Release(Plugin): try: db = get_db() - identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) + release_identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) # Add movie if it doesn't exist try: media = db.get('media', group['identifier'], with_doc = True)['doc'] - media['status'] = 'done' - db.update(media) except: - media = { - '_t': 'media', + media = fireEvent('movie.add', params = { 'identifier': group['identifier'], 'profile_id': None, - 'status': 'done' - } - m = db.insert(media) - media.update(m) + }, search_after = False, status = 'done', single = True) # Add Release release = { '_t': 'release', 'media_id': media['_id'], - 'identifier': identifier, + 'identifier': release_identifier, 'quality': group['meta_data']['quality'].get('identifier'), + 'last_edit': int(time.time()), 'status': 'done' } try: - r = db.get('release_identifier', identifier, with_doc = True)['doc'] - release.update(r) - db.update(release) + r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] + r['media_id'] = media['_id'] except: r = db.insert(release) - release.update(r) - # Add each file type + # Update with ref and _id + release.update({ + '_id': r['_id'], + '_rev': r['_rev'], + }) + + # Empty out empty file groups release['files'] = dict((k, v) for k, v in group['files'].items() if v) + db.update(release) fireEvent('media.restatus', media['_id']) @@ -307,7 +307,7 @@ class Release(Plugin): # Mark release as snatched if renamer_enabled: - self.updateStatus(rls.id, status = snatched_status) + self.updateStatus(rls.id, status = 'snatched') # If renamer isn't used, mark media done if finished or release downloaded else: @@ -318,7 +318,7 @@ class Release(Plugin): log.info('Renamer disabled, marking media as finished: %s', log_movie) # Mark release done - self.updateStatus(rls.id, status = done_status) + self.updateStatus(rls.id, status = 'done') # Mark media done mdia = db.query(Media).filter_by(id = media['id']).first() @@ -329,7 +329,7 @@ class Release(Plugin): return True # Assume release downloaded - self.updateStatus(rls.id, status = downloaded_status) + self.updateStatus(rls.id, status = 'downloaded') except: log.error('Failed storing download status: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index c26de9e9..8cc14c97 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -472,14 +472,14 @@ class Renamer(Plugin): if release_download['rls_id'] == release.id: if release_download['status'] == 'completed': # Set the release to downloaded - fireEvent('release.update_status', release.id, status = downloaded_status, single = True) + fireEvent('release.update_status', release.id, status = 'downloaded', single = True) elif release_download['status'] == 'seeding': # Set the release to seeding - fireEvent('release.update_status', release.id, status = seeding_status, single = True) + fireEvent('release.update_status', release.id, status = 'seeding', single = True) elif release.quality.id is group['meta_data']['quality']['id']: # Set the release to downloaded - fireEvent('release.update_status', release.id, status = downloaded_status, single = True) + fireEvent('release.update_status', release.id, status = 'downloaded', single = True) # Remove leftover files if not remove_leftovers: # Don't remove anything diff --git a/couchpotato/runner.py b/couchpotato/runner.py index d6a967c7..c79317ae 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -13,7 +13,6 @@ from uuid import uuid4 import locale import logging import os.path -import shutil import sys import time import traceback From 0d4d0f3126fa451aadb8097f13617eccfeb79ac2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Feb 2014 18:29:17 +0100 Subject: [PATCH 012/468] scandir --- couchpotato/core/_base/updater/main.py | 7 ++++--- couchpotato/core/downloaders/rtorrent/main.py | 3 ++- couchpotato/core/plugins/renamer/main.py | 17 +++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index ef595ad7..4f96e929 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -13,6 +13,7 @@ import shutil import tarfile import time import traceback +from scandir import scandir import version import zipfile from six.moves import filter @@ -160,7 +161,7 @@ class BaseUpdater(Plugin): def deletePyc(self, only_excess = True): - for root, dirs, files in os.walk(ss(Env.get('app_dir'))): + for root, dirs, files in scandir.walk(ss(Env.get('app_dir'))): pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) py_files = set(filter(lambda filename: filename.endswith('.py'), files)) @@ -308,11 +309,11 @@ class SourceUpdater(BaseUpdater): # Get list of files we want to overwrite self.deletePyc() existing_files = [] - for root, subfiles, filenames in os.walk(app_dir): + for root, subfiles, filenames in scandir.walk(app_dir): for filename in filenames: existing_files.append(os.path.join(root, filename)) - for root, subfiles, filenames in os.walk(path): + for root, subfiles, filenames in scandir.walk(path): for filename in filenames: fromfile = os.path.join(root, filename) tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, '')) diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 27177756..994980e8 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -10,6 +10,7 @@ from hashlib import sha1 from rtorrent import RTorrent from rtorrent.err import MethodError import os +from scandir import scandir log = CPLog(__name__) @@ -242,7 +243,7 @@ class rTorrent(Downloader): if torrent.is_multi_file() and torrent.directory.endswith(torrent.name): # Remove empty directories bottom up try: - for path, _, _ in os.walk(torrent.directory, topdown = False): + for path, _, _ in scandir.walk(torrent.directory, topdown = False): os.rmdir(path) except OSError: log.info('Directory "%s" contains extra files, unable to remove', torrent.directory) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 8cc14c97..42ec78fc 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -9,6 +9,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, \ ReleaseInfo from couchpotato.environment import Env +from scandir import scandir from unrar2 import RarFile import errno import fnmatch @@ -186,7 +187,7 @@ class Renamer(Plugin): else: # Get all files from the specified folder try: - for root, folders, names in os.walk(media_folder): + for root, folders, names in scandir.walk(media_folder): files.extend([sp(os.path.join(root, name)) for name in names]) except: log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc())) @@ -635,7 +636,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Tag all files in release folder else: - for root, folders, names in os.walk(release_download['folder']): + for root, folders, names in scandir.walk(release_download['folder']): tag_files.extend([os.path.join(root, name) for name in names]) for filename in tag_files: @@ -670,7 +671,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Untag all files in release folder else: - for root, folders, names in os.walk(release_download['folder']): + for root, folders, names in scandir.walk(release_download['folder']): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) folder = release_download['folder'] @@ -682,7 +683,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find all .ignore files in folder ignore_files = [] - for root, dirnames, filenames in os.walk(folder): + for root, dirnames, filenames in scandir.walk(folder): ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and delete if found @@ -711,11 +712,11 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find tag on all files in release folder else: - for root, folders, names in os.walk(release_download['folder']): + for root, folders, names in scandir.walk(release_download['folder']): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) # Find all .ignore files in folder - for root, dirnames, filenames in os.walk(folder): + for root, dirnames, filenames in scandir.walk(folder): ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and return True found @@ -818,7 +819,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) folder = sp(folder) loge = log.error if show_error else log.debug - for root, dirs, files in os.walk(folder): + for root, dirs, files in scandir.walk(folder): for dir_name in dirs: full_path = os.path.join(root, dir_name) @@ -1102,7 +1103,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) check_file_date = False if not files: - for root, folders, names in os.walk(folder): + for root, folders, names in scandir.walk(folder): files.extend([sp(os.path.join(root, name)) for name in names]) # Find all archive files From ecf91d616b7af8590935274883e579383e8e43c2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Feb 2014 23:07:42 +0100 Subject: [PATCH 013/468] nosql --- couchpotato/core/media/_base/media/main.py | 4 +- couchpotato/core/media/movie/searcher/main.py | 2 +- couchpotato/core/plugins/manage/main.py | 2 +- couchpotato/core/plugins/release/index.py | 14 ++ couchpotato/core/plugins/release/main.py | 123 ++++++----- couchpotato/core/plugins/renamer/main.py | 208 +++++++++--------- couchpotato/core/plugins/scanner/main.py | 9 +- 7 files changed, 188 insertions(+), 174 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index af12bfb4..e2c9a310 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -251,9 +251,7 @@ class MediaPlugin(MediaBase): media = db.run('media', 'to_dict', media_id) - media['releases'] = [] - for r in db.get_many('release', media_id, with_doc = True): - media['releases'].append(r['doc']) + media['releases'] = list(db.run('release', 'for_media', media_id)) # Merge releases with movie dict medias.append(media) diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 155390de..58d0d837 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -184,7 +184,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): ret = True # Remove releases that aren't found anymore - for release in movie.get('releases', []): + for release in db.run('release', 'for_media', movie['_id']): if release.get('status') == 'available' and release.get('identifier') not in found_releases: fireEvent('release.delete', release.get('_id'), single = True) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 65618b5b..d29321ce 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -129,7 +129,7 @@ class Manage(Plugin): releases = list(db.run('release', 'for_media', done_movie.get('_id'))) for release in releases: - if len(release.get('files', [])) > 0: + if release.get('files'): for file_type in release.get('files', {}): for release_file in release['files'][file_type]: # Remove release not available anymore diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 9317cc27..2c343716 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -55,3 +55,17 @@ class ReleaseIDIndex(HashIndex): def make_key_value(self, data): if data.get('_t') == 'release' and data.get('identifier'): return md5(data.get('identifier')).hexdigest(), {'media_id': data.get('media_id')} + + +class ReleaseDownloadIndex(HashIndex): + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(ReleaseDownloadIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).hexdigest() + + def make_key_value(self, data): + if data.get('_t') == 'release' and data.get('download_info') and data['download_info']['id'] and data['download_info']['downloader']: + return md5('%s-%s' % (data['download_info']['downloader'], data['download_info']['id'])).hexdigest(), None diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 28ae8c3f..185c2d2e 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -1,18 +1,14 @@ -from couchpotato import get_session, md5, get_db +from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex -from couchpotato.core.plugins.scanner.main import Scanner -from couchpotato.core.settings.model import Release as Relea, Media, \ - ReleaseInfo +from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex from couchpotato.environment import Env from inspect import ismethod, isfunction from sqlalchemy.exc import InterfaceError -from sqlalchemy.sql.expression import and_, or_ import os import time import traceback @@ -81,6 +77,13 @@ class Release(Plugin): log.debug('Index already exists') db.edit_index(ReleaseIDIndex(db.path, 'release_identifier')) + # Release identifier index + try: + db.add_index(ReleaseDownloadIndex(db.path, 'release_download')) + except: + log.debug('Index already exists') + db.edit_index(ReleaseDownloadIndex(db.path, 'release_download')) + def cleanDone(self): log.debug('Removing releases from dashboard') @@ -217,41 +220,36 @@ class Release(Plugin): def manualDownload(self, id = None, **kwargs): - db = get_session() + db = get_db() - rel = db.query(Relea).filter_by(id = id).first() - if not rel: - log.error('Couldn\'t find release with id: %s', id) + try: + release = db.get('id', id) + item = release['info'] + movie = db.get('id', release['media_id']) + + fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name']) + + # Get matching provider + provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) + + if item.get('protocol') != 'torrent_magnet': + item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download + + success = self.download(data = item, media = movie, manual = True) + + if success: + fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) + + return { + 'success': success == True + } + + except: + log.error('Couldn\'t find release with id: %s: %s', (id, traceback.format_exc())) return { 'success': False } - item = {} - for info in rel.info: - item[info.identifier] = info.value - - fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name']) - - # Get matching provider - provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) - - if item.get('protocol') != 'torrent_magnet': - item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download - - success = self.download(data = item, media = rel.movie.to_dict({ - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}}, - 'library': {'titles': {}, 'files': {}}, - 'files': {} - }), manual = True) - - if success: - fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) - - return { - 'success': success == True - } - def download(self, data, media, manual = False): # Test to see if any downloaders are enabled for this type @@ -282,9 +280,11 @@ class Release(Plugin): log.debug('Downloader result: %s', download_result) try: - db = get_session() - rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first() - if not rls: + db = get_db() + + try: + rls = db.get('release_identifier', md5(data['url']), with_doc = True)['doc'] + except: log.error('No release found to store download information in') return False @@ -292,44 +292,44 @@ class Release(Plugin): # Save download-id info if returned if isinstance(download_result, dict): - for key in download_result: - rls_info = ReleaseInfo( - identifier = 'download_%s' % key, - value = toUnicode(download_result.get(key)) - ) - rls.info.append(rls_info) - db.commit() + rls['download_info'] = download_result + db.update(rls) - log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls.quality.label) + log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls['quality']) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) log.info(snatch_message) - fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict()) + fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls) # Mark release as snatched if renamer_enabled: - self.updateStatus(rls.id, status = 'snatched') + self.updateStatus(rls['_id'], status = 'snatched') # If renamer isn't used, mark media done if finished or release downloaded else: + if media['status'] == 'active': - finished = next((True for profile_type in media['profile']['types'] - if profile_type['quality_id'] == rls.quality.id and profile_type['finish']), False) + profile = db.get('id', media['profile_id']) + finished = False + if rls['quality'] in profile['qualities']: + nr = profile['qualities'].index(rls['quality']) + finished = profile['finish'][nr] + if finished: log.info('Renamer disabled, marking media as finished: %s', log_movie) # Mark release done - self.updateStatus(rls.id, status = 'done') + self.updateStatus(rls['_id'], status = 'done') # Mark media done - mdia = db.query(Media).filter_by(id = media['id']).first() - mdia.status = 'done' - mdia.last_edit = int(time.time()) - db.commit() + mdia = db.get('id', media['_id']) + mdia['status'] = 'done' + mdia['last_edit'] = int(time.time()) + db.update(mdia) return True # Assume release downloaded - self.updateStatus(rls.id, status = 'downloaded') + self.updateStatus(rls['_id'], status = 'downloaded') except: log.error('Failed storing download status: %s', traceback.format_exc()) @@ -420,10 +420,11 @@ class Release(Plugin): release_name = rel.get('name') if rel.get('files'): - for file_item in rel.get('files', []): - if file_item.get('type') == 'movie': - release_name = os.path.basename(file_item.get('path')) - break + for file_type in rel.get('files', {}): + if file_type == 'movie': + for release_file in rel['files'][file_type]: + release_name = os.path.basename(release_file) + break #update status in Db log.debug('Marking release %s as %s', (release_name, status)) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 42ec78fc..cee92f6c 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -1,4 +1,4 @@ -from couchpotato import get_session, get_db +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss, sp @@ -6,8 +6,6 @@ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile, \ - ReleaseInfo from couchpotato.environment import Env from scandir import scandir from unrar2 import RarFile @@ -113,6 +111,9 @@ class Renamer(Plugin): # Get media folder to process media_folder = release_download.get('folder') + # Quality order for calculation quality priority + quality_order = fireEvent('quality.order', single = True) + # Get all folders that should not be processed no_process = [to_folder] cat_list = fireEvent('category.all', single = True) or [] @@ -192,7 +193,7 @@ class Renamer(Plugin): except: log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc())) - db = get_session() + db = get_db() # Extend the download info with info stored in the downloaded release release_download = self.extendReleaseDownload(release_download) @@ -223,36 +224,47 @@ class Renamer(Plugin): remove_files = [] remove_releases = [] - movie_title = getTitle(group) + media_title = getTitle(group) # Add _UNKNOWN_ if no library item is connected - if not group.get('info') or not movie_title: + if not group.get('info') or not media_title: self.tagRelease(group = group, tag = 'unknown') continue # Rename the files using the library data else: - group['media'] = fireEvent('movie.update_info', identifier = group['media']['identifier'], single = True) - if not group['media']: + + # Media not in library, add it first + if not group['media'].get('_id'): + group['media'] = fireEvent('movie.add', params = { + 'identifier': group['identifier'], + 'profile_id': None + }, search_after = False, status = 'done') + else: + group['media'] = fireEvent('movie.update_info', identifier = group['media']['identifier'], single = True) + + if not group['media'] or not group['media'].get('_id'): log.error('Could not rename, no library item to work with: %s', group_identifier) continue - movie_title = getTitle(group['media']) + media = group['media'] + media_title = getTitle(media) # Overwrite destination when set in category destination = to_folder category_label = '' - for movie in library_ent.movies: - if movie.category and movie.category.label: - category_label = movie.category.label + if media.get('category_id'): + try: + category = db.get('id', media['category_id']) + category_label = category['label'] - if movie.category and movie.category.destination and len(movie.category.destination) > 0 and movie.category.destination != 'None': - destination = movie.category.destination - log.debug('Setting category destination for "%s": %s' % (movie_title, destination)) - else: - log.debug('No category destination found for "%s"' % movie_title) - - break + if category['destination'] and len(category['destination']) > 0 and category['destination'] != 'None': + destination = category['destination'] + log.debug('Setting category destination for "%s": %s' % (media_title, destination)) + else: + log.debug('No category destination found for "%s"' % media_title) + except: + log.error('Failed getting category label: %s', traceback.format_exc()) # Find subtitle for renaming group['before_rename'] = [] @@ -263,7 +275,7 @@ class Renamer(Plugin): group['before_rename'].extend(extr_files) # Remove weird chars from movie name - movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title) + movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', media_title) # Put 'The' at the end name_the = movie_name @@ -274,7 +286,7 @@ class Renamer(Plugin): 'ext': 'mkv', 'namethe': name_the.strip(), 'thename': movie_name.strip(), - 'year': library['year'], + 'year': media['info']['year'], 'first': name_the[0].upper(), 'quality': group['meta_data']['quality']['label'], 'quality_type': group['meta_data']['quality_type'], @@ -285,10 +297,10 @@ class Renamer(Plugin): 'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_height': group['meta_data'].get('resolution_height'), 'audio_channels': group['meta_data'].get('audio_channels'), - 'imdb_id': library['identifier'], + 'imdb_id': group['identifier'], 'cd': '', 'cd_nr': '', - 'mpaa': library['info'].get('mpaa', ''), + 'mpaa': media['info'].get('mpaa', ''), 'category': category_label, } @@ -414,73 +426,73 @@ class Renamer(Plugin): # Before renaming, remove the lower quality files remove_leftovers = True - # Add it to the wanted list before we continue - if len(library_ent.movies) == 0: - profile = db.query(Profile).filter_by(core = True, label = group['meta_data']['quality']['label']).first() - fireEvent('movie.add', params = {'identifier': group['identifier'], 'profile_id': profile.id}, search_after = False) - db.expire_all() - library_ent = db.query(Library).filter_by(identifier = group['identifier']).first() + # Mark movie "done" once it's found the quality with the finish check + try: + if media.get('status') == 'active' and media.get('profile_id'): + profile = db.get('id', media['profile_id']) + if group['meta_data']['quality']['identifier'] in profile.get('qualities', []): + nr = profile['qualities'].index(group['meta_data']['quality']['identifier']) + finish = profile['finish'][nr] + if finish: + mdia = db.get('id', media['_id']) + mdia['status'] = 'done' + mdia['last_edit'] = int(time.time()) + db.update(mdia) - for movie in library_ent.movies: + except Exception as e: + log.error('Failed marking movie finished: %s', (traceback.format_exc())) - # Mark movie "done" once it's found the quality with the finish check - try: - if movie.get('status') == 'active' and movie.get('profile_id'): - for profile_type in movie.profile.types: - if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: - movie['status'] = 'done' - movie['last_edit'] = int(time.time()) - db.commit() - except Exception as e: - log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc())) - db.rollback() + # Go over current movie releases + for release in db.run('release', 'for_media', media['_id']): - # Go over current movie releases - for release in movie.releases: + # When a release already exists + if release.get('status') == 'done': - # When a release already exists - if release.get('status') == 'done': + release_order = quality_order.index(release['quality']) + group_quality_order = quality_order.index(group['meta_data']['quality']['identifier']) - # This is where CP removes older, lesser quality releases - if release.quality.order > group['meta_data']['quality']['order']: - log.info('Removing lesser quality %s for %s.', (movie.library.titles[0].title, release.quality.label)) - for current_file in release.files: - remove_files.append(current_file) - remove_releases.append(release) - # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc - elif release.quality.order is group['meta_data']['quality']['order']: - log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (movie.library.titles[0].title, release.quality.label)) - for current_file in release.files: - remove_files.append(current_file) - remove_releases.append(release) + # This is where CP removes older, lesser quality releases + if release_order > group_quality_order: + log.info('Removing lesser quality %s for %s.', (media_title, release.get('quality'))) + for file_type in release.get('files', {}): + for release_file in release['files'][file_type]: + remove_files.append(release_file) + remove_releases.append(release) + # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc + elif release_order == group_quality_order: + log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (media_title, release.get('quality'))) + for file_type in release.get('files', {}): + for release_file in release['files'][file_type]: + remove_files.append(release_file) + remove_releases.append(release) - # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan - else: - log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label)) + # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan + else: + log.info('Better quality release already exists for %s, with quality %s', (media_title, release.get('quality'))) - # Add exists tag to the .ignore file - self.tagRelease(group = group, tag = 'exists') + # Add exists tag to the .ignore file + self.tagRelease(group = group, tag = 'exists') - # Notify on rename fail - download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (getTitle(movie), group['meta_data']['quality']['label'], release.quality.label) - fireEvent('movie.renaming.canceled', message = download_message, data = group) - remove_leftovers = False + # Notify on rename fail + download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('identifier')) + fireEvent('movie.renaming.canceled', message = download_message, data = group) + remove_leftovers = False - break + break - elif release.get('status') in ['snatched', 'seeding']: - if release_download and release_download.get('rls_id'): - if release_download['rls_id'] == release.id: - if release_download['status'] == 'completed': - # Set the release to downloaded - fireEvent('release.update_status', release.id, status = 'downloaded', single = True) - elif release_download['status'] == 'seeding': - # Set the release to seeding - fireEvent('release.update_status', release.id, status = 'seeding', single = True) - - elif release.quality.id is group['meta_data']['quality']['id']: + elif release.get('status') in ['snatched', 'seeding']: + if release_download and release_download.get('release_id'): + if release_download['release_id'] == release['_id']: + if release_download['status'] == 'completed': # Set the release to downloaded - fireEvent('release.update_status', release.id, status = 'downloaded', single = True) + fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) + elif release_download['status'] == 'seeding': + # Set the release to seeding + fireEvent('release.update_status', release['_id'], status = 'seeding', single = True) + + elif release.get('identifier') == group['meta_data']['quality']['identifier']: + # Set the release to downloaded + fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) # Remove leftover files if not remove_leftovers: # Don't remove anything @@ -496,9 +508,6 @@ class Renamer(Plugin): delete_folders = [] for src in remove_files: - if isinstance(src, File): - src = src.path - if rename_files.get(src): log.debug('Not removing file that will be renamed: %s', src) continue @@ -542,7 +551,7 @@ class Renamer(Plugin): self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(release_download) or self.fileIsAdded(src, group)) group['renamed_files'].append(dst) except: - log.error('Failed ranaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) + log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) failed_rename = True break @@ -581,7 +590,7 @@ class Renamer(Plugin): log.error('Failed removing %s: %s', (group_folder, traceback.format_exc())) # Notify on download, search for trailers etc - download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality']) + download_message = 'Downloaded %s (%s)' % (media_title, replacements['quality']) try: fireEvent('renamer.after', message = download_message, group = group, in_order = True) except: @@ -1040,32 +1049,21 @@ Remove it if you want it to be renamed (again, or at least let it try again) def extendReleaseDownload(self, release_download): rls = None + db = get_db() - if release_download and release_download.get('id') and release_download.get('downloader'): - - db = get_session() - - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = release_download.get('downloader')).all() - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = release_download.get('id')).all() - - for rlsnfo_dwnld in rlsnfo_dwnlds: - for rlsnfo_id in rlsnfo_ids: - if rlsnfo_id.release == rlsnfo_dwnld.release: - rls = rlsnfo_id.release - break - if rls: break - - if not rls: + if release_download and release_download.get('id'): + try: + rls = db.get('release_download', '%s_%s' % (release_download.get('downloader'), release_download.get('id')), with_doc = True)['doc'] + except: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) if rls: - - rls_dict = rls.to_dict({'info':{}}) + media = db.get('id', rls['media_id']) release_download.update({ - 'imdb_id': rls.movie.library.identifier, - 'quality': rls.quality.identifier, - 'protocol': rls_dict.get('info', {}).get('protocol') or rls_dict.get('info', {}).get('type'), - 'rls_id': rls.id, + 'imdb_id': media['identifier'], + 'quality': rls['quality'], + 'protocol': rls.get('info', {}).get('protocol') or rls.get('info', {}).get('type'), + 'release_id': rls['_id'], }) return release_download diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 7d384246..e7f9b69d 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -419,7 +419,7 @@ class Scanner(Plugin): if not group['media']: log.error('Unable to determine media: %s', group['identifiers']) else: - group['identifier'] = group['media']['imdb'] + group['identifier'] = group['media'].get('identifier') or group['media']['info'].get('imdb') processed_movies[identifier] = group @@ -613,10 +613,13 @@ class Scanner(Plugin): if imdb_id: try: db = get_db() - return db.get('media', imdb_id, with_doc = True)['doc']['info'] + return db.get('media', imdb_id, with_doc = True)['doc'] except: log.debug('Movie "%s" not in library, just getting info', imdb_id) - return fireEvent('movie.info', identifier = imdb_id, merge = True, extended = False) + return { + 'identifier': imdb_id, + 'info': fireEvent('movie.info', identifier = imdb_id, merge = True, extended = False) + } log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers']) return {} From a13e0a75e8cb296f794df208a1e5a6d4f99f4af4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Feb 2014 23:08:14 +0100 Subject: [PATCH 014/468] Remove test --- couchpotato/core/plugins/nosql/__init__.py | 7 -- couchpotato/core/plugins/nosql/index.py | 17 ----- couchpotato/core/plugins/nosql/main.py | 82 ---------------------- 3 files changed, 106 deletions(-) delete mode 100644 couchpotato/core/plugins/nosql/__init__.py delete mode 100644 couchpotato/core/plugins/nosql/index.py delete mode 100644 couchpotato/core/plugins/nosql/main.py diff --git a/couchpotato/core/plugins/nosql/__init__.py b/couchpotato/core/plugins/nosql/__init__.py deleted file mode 100644 index 7f0e952e..00000000 --- a/couchpotato/core/plugins/nosql/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import NoSQL - - -def start(): - return NoSQL() - -config = [] diff --git a/couchpotato/core/plugins/nosql/index.py b/couchpotato/core/plugins/nosql/index.py deleted file mode 100644 index f88738fb..00000000 --- a/couchpotato/core/plugins/nosql/index.py +++ /dev/null @@ -1,17 +0,0 @@ -from hashlib import md5 -from CodernityDB.tree_index import TreeBasedIndex, MultiTreeBasedIndex - - -class NameIndex(TreeBasedIndex): - - def __init__(self, *args, **kwargs): - kwargs['key_format'] = '16s' - super(NameIndex, self).__init__(*args, **kwargs) - - def make_key(self, key): - return key - - def make_key_value(self, data): - if data.get('_t') == 'media' and data.get('title') is not None: - return data.get('title'), None - diff --git a/couchpotato/core/plugins/nosql/main.py b/couchpotato/core/plugins/nosql/main.py deleted file mode 100644 index b5243d9f..00000000 --- a/couchpotato/core/plugins/nosql/main.py +++ /dev/null @@ -1,82 +0,0 @@ -import time -from couchpotato import CPLog, get_db -from couchpotato.core.event import addEvent -from couchpotato.core.plugins.base import Plugin - -log = CPLog(__name__) - - -class NoSQL(Plugin): - - db = None - - def __init__(self): - - pass #addEvent('app.load', self.test) - - def test(self): - - db = get_db() - - try: db.add_index(YearIndex(db.path, 'year')) - except: - log.debug('Index already exists') - db.edit_index(YearIndex(db.path, 'year')) - - return - - try: db.add_index(ReleaseIndex(db.path, 'release')) - except: log.debug('Index already exists') - - for id in range(10): - media = db.insert({ - '_t': 'media', - 'type': 'movie', - 'tmdb': id, - 'imdb': 'tt%s' % id, - 'last_edit': 0, - 'status': 'active', - 'title': 'Lord of the Rings: The Return of the King', - 'year': 2011, - 'profile_id': 0, - 'category_id': 0, - }) - - db.insert({ - 'media_id': media['_id'], - 'type': 'title', - 'title': 'Lord of the Rings: The Return of the King', - }) - - for x in range(40): - db.insert({ - 'media_id': media['_id'], - 'type': 'release', - 'name': 'Release %s' % x - }) - - print db.count(db.all, 'media') - - m = db.get('media', 'tt0') - db.get('id', m['_id']) - - start = time.time() - print list(db.get_many('media_title', 'lord of')) - print time.time() - start - - return - - for media in db.all('media', with_doc = True): - doc = media['doc'] - for r in db.run('release', 'for_media', media['_id']): - db.delete(r) - - db.delete(doc) - break - - start = time.time() - db.reindex() - print time.time() - start - - print db.count(db.all, 'media') - print db.count(db.all, 'release') From 4b356aba3ea3b754b11749c5e7c7162f6b3ce390 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Feb 2014 23:22:11 +0100 Subject: [PATCH 015/468] Nosql --- couchpotato/core/media/movie/searcher/main.py | 19 +++++++++---------- couchpotato/core/plugins/dashboard/main.py | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 58d0d837..8a13cb43 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -72,11 +72,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): db = get_db() - movies = db.get_many('movie_status', 'active') + medias = [x['_id'] for x in db.run('media', 'with_status', 'active', with_doc = False)] + random.shuffle(medias) - #TODO: random.shuffle(movies_raw) - - total = db.count(db.get_many, 'movie_status', 'active') + total = len(medias) self.in_progress = { 'total': total, 'to_go': total, @@ -85,17 +84,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase): try: search_protocols = fireEvent('searcher.protocols', single = True) - for movie in movies: + for media_id in medias: - movie_dict = db.run('media', 'to_dict', movie['_id']) + media = db.run('media', 'to_dict', media_id) try: - self.single(movie_dict, search_protocols) + self.single(media, search_protocols) except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['identifier'], traceback.format_exc())) - fireEvent('movie.update_info', movie['_id']) + log.error('Forcing library update for %s, if you see this often, please report: %s', (media['identifier'], traceback.format_exc())) + fireEvent('movie.update_info', media_id) except: - log.error('Search failed for %s: %s', (movie['identifier'], traceback.format_exc())) + log.error('Search failed for %s: %s', (media['identifier'], traceback.format_exc())) self.in_progress['to_go'] -= 1 diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index e2e4c792..7f7c4fe9 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -40,7 +40,7 @@ class Dashboard(Plugin): limit = tryInt(splt[0]) # Get all active medias - active_ids = [x['_id'] for x in db.get_many('media_status', 'active')] + active_ids = [x['_id'] for x in db.run('media', 'with_status', 'active', with_doc = False)] medias = [] if len(active_ids) > 0: From 8724076601e4861f8a117347934def3d843b949c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Feb 2014 23:41:54 +0100 Subject: [PATCH 016/468] Remove sqlalchemy and elixir --- couchpotato/core/media/_base/media/main.py | 6 +- couchpotato/core/migration/__init__.py | 0 couchpotato/core/migration/migrate.cfg | 4 - .../versions/001_Releases_last_edit.py | 25 - .../migration/versions/002_Movie_category.py | 18 - .../core/migration/versions/__init__.py | 30 - couchpotato/core/notifications/xbmc/main.py | 16 +- couchpotato/core/plugins/base.py | 2 +- couchpotato/core/plugins/quality/main.py | 5 +- couchpotato/core/plugins/release/main.py | 3 +- couchpotato/core/settings/model.py | 505 --- couchpotato/environment.py | 3 - libs/decorator.py | 210 - libs/migrate/__init__.py | 11 - libs/migrate/changeset/__init__.py | 28 - libs/migrate/changeset/ansisql.py | 292 -- libs/migrate/changeset/constraint.py | 199 - libs/migrate/changeset/databases/__init__.py | 10 - libs/migrate/changeset/databases/firebird.py | 93 - libs/migrate/changeset/databases/mysql.py | 65 - libs/migrate/changeset/databases/oracle.py | 108 - libs/migrate/changeset/databases/postgres.py | 42 - libs/migrate/changeset/databases/sqlite.py | 153 - libs/migrate/changeset/databases/visitor.py | 78 - libs/migrate/changeset/schema.py | 655 --- libs/migrate/exceptions.py | 87 - libs/migrate/versioning/__init__.py | 5 - libs/migrate/versioning/api.py | 384 -- libs/migrate/versioning/cfgparse.py | 27 - libs/migrate/versioning/config.py | 14 - libs/migrate/versioning/genmodel.py | 285 -- libs/migrate/versioning/migrate_repository.py | 100 - libs/migrate/versioning/pathed.py | 75 - libs/migrate/versioning/repository.py | 242 -- libs/migrate/versioning/schema.py | 220 - libs/migrate/versioning/schemadiff.py | 292 -- libs/migrate/versioning/script/__init__.py | 6 - libs/migrate/versioning/script/base.py | 57 - libs/migrate/versioning/script/py.py | 160 - libs/migrate/versioning/script/sql.py | 49 - libs/migrate/versioning/shell.py | 214 - libs/migrate/versioning/template.py | 93 - libs/migrate/versioning/templates/__init__.py | 0 .../templates/manage/default.py_tmpl | 12 - .../templates/manage/pylons.py_tmpl | 30 - .../templates/repository/__init__.py | 0 .../templates/repository/default/README | 4 - .../templates/repository/default/__init__.py | 0 .../templates/repository/default/migrate.cfg | 25 - .../repository/default/versions/__init__.py | 0 .../templates/repository/pylons/README | 4 - .../templates/repository/pylons/__init__.py | 0 .../templates/repository/pylons/migrate.cfg | 25 - .../repository/pylons/versions/__init__.py | 0 .../versioning/templates/script/__init__.py | 0 .../templates/script/default.py_tmpl | 13 - .../templates/script/pylons.py_tmpl | 13 - .../templates/sql_script/default.py_tmpl | 0 .../templates/sql_script/pylons.py_tmpl | 0 libs/migrate/versioning/util/__init__.py | 179 - libs/migrate/versioning/util/importpath.py | 16 - libs/migrate/versioning/util/keyedinstance.py | 36 - libs/migrate/versioning/version.py | 238 -- libs/sqlalchemy/__init__.py | 133 - libs/sqlalchemy/cextension/processors.c | 665 --- libs/sqlalchemy/cextension/resultproxy.c | 718 ---- libs/sqlalchemy/cextension/utils.c | 225 -- libs/sqlalchemy/connectors/__init__.py | 9 - libs/sqlalchemy/connectors/mxodbc.py | 149 - libs/sqlalchemy/connectors/mysqldb.py | 162 - libs/sqlalchemy/connectors/pyodbc.py | 170 - libs/sqlalchemy/connectors/zxJDBC.py | 59 - libs/sqlalchemy/databases/__init__.py | 31 - libs/sqlalchemy/dialects/__init__.py | 44 - libs/sqlalchemy/dialects/drizzle/__init__.py | 22 - libs/sqlalchemy/dialects/drizzle/base.py | 498 --- libs/sqlalchemy/dialects/drizzle/mysqldb.py | 48 - libs/sqlalchemy/dialects/firebird/__init__.py | 20 - libs/sqlalchemy/dialects/firebird/base.py | 736 ---- libs/sqlalchemy/dialects/firebird/fdb.py | 115 - .../dialects/firebird/kinterbasdb.py | 179 - libs/sqlalchemy/dialects/mssql/__init__.py | 26 - libs/sqlalchemy/dialects/mssql/adodbapi.py | 79 - libs/sqlalchemy/dialects/mssql/base.py | 1476 ------- .../dialects/mssql/information_schema.py | 114 - libs/sqlalchemy/dialects/mssql/mxodbc.py | 111 - libs/sqlalchemy/dialects/mssql/pymssql.py | 100 - libs/sqlalchemy/dialects/mssql/pyodbc.py | 260 -- libs/sqlalchemy/dialects/mssql/zxjdbc.py | 65 - libs/sqlalchemy/dialects/mysql/__init__.py | 28 - libs/sqlalchemy/dialects/mysql/base.py | 2955 -------------- libs/sqlalchemy/dialects/mysql/cymysql.py | 69 - libs/sqlalchemy/dialects/mysql/gaerdbms.py | 84 - .../dialects/mysql/mysqlconnector.py | 128 - libs/sqlalchemy/dialects/mysql/mysqldb.py | 78 - libs/sqlalchemy/dialects/mysql/oursql.py | 261 -- libs/sqlalchemy/dialects/mysql/pymysql.py | 44 - libs/sqlalchemy/dialects/mysql/pyodbc.py | 80 - libs/sqlalchemy/dialects/mysql/zxjdbc.py | 111 - libs/sqlalchemy/dialects/oracle/__init__.py | 23 - libs/sqlalchemy/dialects/oracle/base.py | 1249 ------ libs/sqlalchemy/dialects/oracle/cx_oracle.py | 869 ---- libs/sqlalchemy/dialects/oracle/zxjdbc.py | 218 - libs/sqlalchemy/dialects/postgres.py | 16 - .../dialects/postgresql/__init__.py | 29 - libs/sqlalchemy/dialects/postgresql/base.py | 2224 ----------- .../dialects/postgresql/constraints.py | 73 - libs/sqlalchemy/dialects/postgresql/hstore.py | 369 -- libs/sqlalchemy/dialects/postgresql/json.py | 199 - libs/sqlalchemy/dialects/postgresql/pg8000.py | 126 - .../dialects/postgresql/psycopg2.py | 484 --- .../dialects/postgresql/pypostgresql.py | 78 - libs/sqlalchemy/dialects/postgresql/ranges.py | 160 - libs/sqlalchemy/dialects/postgresql/zxjdbc.py | 45 - libs/sqlalchemy/dialects/sqlite/__init__.py | 21 - libs/sqlalchemy/dialects/sqlite/base.py | 965 ----- libs/sqlalchemy/dialects/sqlite/pysqlite.py | 335 -- libs/sqlalchemy/dialects/sybase/__init__.py | 27 - libs/sqlalchemy/dialects/sybase/base.py | 814 ---- libs/sqlalchemy/dialects/sybase/mxodbc.py | 32 - libs/sqlalchemy/dialects/sybase/pyodbc.py | 84 - libs/sqlalchemy/dialects/sybase/pysybase.py | 100 - .../dialects/type_migration_guidelines.txt | 145 - libs/sqlalchemy/engine/__init__.py | 363 -- libs/sqlalchemy/engine/base.py | 1724 -------- libs/sqlalchemy/engine/default.py | 908 ----- libs/sqlalchemy/engine/interfaces.py | 846 ---- libs/sqlalchemy/engine/reflection.py | 595 --- libs/sqlalchemy/engine/result.py | 1018 ----- libs/sqlalchemy/engine/strategies.py | 259 -- libs/sqlalchemy/engine/threadlocal.py | 134 - libs/sqlalchemy/engine/url.py | 227 -- libs/sqlalchemy/engine/util.py | 72 - libs/sqlalchemy/event/__init__.py | 10 - libs/sqlalchemy/event/api.py | 107 - libs/sqlalchemy/event/attr.py | 376 -- libs/sqlalchemy/event/base.py | 217 - libs/sqlalchemy/event/legacy.py | 156 - libs/sqlalchemy/event/registry.py | 236 -- libs/sqlalchemy/events.py | 809 ---- libs/sqlalchemy/exc.py | 360 -- libs/sqlalchemy/ext/__init__.py | 5 - libs/sqlalchemy/ext/associationproxy.py | 1054 ----- libs/sqlalchemy/ext/automap.py | 840 ---- libs/sqlalchemy/ext/compiler.py | 448 --- libs/sqlalchemy/ext/declarative/__init__.py | 1309 ------ libs/sqlalchemy/ext/declarative/api.py | 512 --- libs/sqlalchemy/ext/declarative/base.py | 506 --- .../sqlalchemy/ext/declarative/clsregistry.py | 305 -- libs/sqlalchemy/ext/horizontal_shard.py | 128 - libs/sqlalchemy/ext/hybrid.py | 808 ---- libs/sqlalchemy/ext/instrumentation.py | 407 -- libs/sqlalchemy/ext/mutable.py | 634 --- libs/sqlalchemy/ext/orderinglist.py | 365 -- libs/sqlalchemy/ext/serializer.py | 156 - libs/sqlalchemy/inspection.py | 92 - libs/sqlalchemy/interfaces.py | 310 -- libs/sqlalchemy/log.py | 214 - libs/sqlalchemy/orm/__init__.py | 267 -- libs/sqlalchemy/orm/attributes.py | 1541 ------- libs/sqlalchemy/orm/base.py | 453 --- libs/sqlalchemy/orm/collections.py | 1550 ------- libs/sqlalchemy/orm/dependency.py | 1165 ------ libs/sqlalchemy/orm/deprecated_interfaces.py | 590 --- libs/sqlalchemy/orm/descriptor_props.py | 677 ---- libs/sqlalchemy/orm/dynamic.py | 368 -- libs/sqlalchemy/orm/evaluator.py | 123 - libs/sqlalchemy/orm/events.py | 1656 -------- libs/sqlalchemy/orm/exc.py | 163 - libs/sqlalchemy/orm/identity.py | 240 -- libs/sqlalchemy/orm/instrumentation.py | 499 --- libs/sqlalchemy/orm/interfaces.py | 577 --- libs/sqlalchemy/orm/loading.py | 610 --- libs/sqlalchemy/orm/mapper.py | 2690 ------------- libs/sqlalchemy/orm/path_registry.py | 261 -- libs/sqlalchemy/orm/persistence.py | 1085 ----- libs/sqlalchemy/orm/properties.py | 259 -- libs/sqlalchemy/orm/query.py | 3550 ----------------- libs/sqlalchemy/orm/relationships.py | 2511 ------------ libs/sqlalchemy/orm/scoping.py | 176 - libs/sqlalchemy/orm/session.py | 2383 ----------- libs/sqlalchemy/orm/state.py | 617 --- libs/sqlalchemy/orm/strategies.py | 1439 ------- libs/sqlalchemy/orm/strategy_options.py | 924 ----- libs/sqlalchemy/orm/sync.py | 118 - libs/sqlalchemy/orm/unitofwork.py | 646 --- libs/sqlalchemy/orm/util.py | 953 ----- libs/sqlalchemy/pool.py | 1110 ------ libs/sqlalchemy/processors.py | 131 - libs/sqlalchemy/schema.py | 54 - libs/sqlalchemy/sql/__init__.py | 89 - libs/sqlalchemy/sql/annotation.py | 182 - libs/sqlalchemy/sql/base.py | 348 -- libs/sqlalchemy/sql/compiler.py | 2897 -------------- libs/sqlalchemy/sql/ddl.py | 864 ---- libs/sqlalchemy/sql/default_comparator.py | 278 -- libs/sqlalchemy/sql/dml.py | 770 ---- libs/sqlalchemy/sql/elements.py | 2880 ------------- libs/sqlalchemy/sql/expression.py | 127 - libs/sqlalchemy/sql/functions.py | 534 --- libs/sqlalchemy/sql/operators.py | 854 ---- libs/sqlalchemy/sql/schema.py | 3150 --------------- libs/sqlalchemy/sql/selectable.py | 3001 -------------- libs/sqlalchemy/sql/sqltypes.py | 1628 -------- libs/sqlalchemy/sql/type_api.py | 1053 ----- libs/sqlalchemy/sql/util.py | 601 --- libs/sqlalchemy/sql/visitors.py | 314 -- libs/sqlalchemy/types.py | 77 - libs/sqlalchemy/util/__init__.py | 45 - libs/sqlalchemy/util/_collections.py | 957 ----- libs/sqlalchemy/util/compat.py | 215 - libs/sqlalchemy/util/deprecations.py | 143 - libs/sqlalchemy/util/langhelpers.py | 1208 ------ libs/sqlalchemy/util/queue.py | 236 -- libs/sqlalchemy/util/topological.py | 96 - libs/tempita/__init__.py | 1160 ------ libs/tempita/_looper.py | 163 - libs/tempita/compat3.py | 45 - 218 files changed, 14 insertions(+), 94000 deletions(-) delete mode 100755 couchpotato/core/migration/__init__.py delete mode 100644 couchpotato/core/migration/migrate.cfg delete mode 100644 couchpotato/core/migration/versions/001_Releases_last_edit.py delete mode 100644 couchpotato/core/migration/versions/002_Movie_category.py delete mode 100755 couchpotato/core/migration/versions/__init__.py delete mode 100644 couchpotato/core/settings/model.py delete mode 100644 libs/decorator.py delete mode 100644 libs/migrate/__init__.py delete mode 100644 libs/migrate/changeset/__init__.py delete mode 100644 libs/migrate/changeset/ansisql.py delete mode 100644 libs/migrate/changeset/constraint.py delete mode 100644 libs/migrate/changeset/databases/__init__.py delete mode 100644 libs/migrate/changeset/databases/firebird.py delete mode 100644 libs/migrate/changeset/databases/mysql.py delete mode 100644 libs/migrate/changeset/databases/oracle.py delete mode 100644 libs/migrate/changeset/databases/postgres.py delete mode 100644 libs/migrate/changeset/databases/sqlite.py delete mode 100644 libs/migrate/changeset/databases/visitor.py delete mode 100644 libs/migrate/changeset/schema.py delete mode 100644 libs/migrate/exceptions.py delete mode 100644 libs/migrate/versioning/__init__.py delete mode 100644 libs/migrate/versioning/api.py delete mode 100644 libs/migrate/versioning/cfgparse.py delete mode 100644 libs/migrate/versioning/config.py delete mode 100644 libs/migrate/versioning/genmodel.py delete mode 100644 libs/migrate/versioning/migrate_repository.py delete mode 100644 libs/migrate/versioning/pathed.py delete mode 100644 libs/migrate/versioning/repository.py delete mode 100644 libs/migrate/versioning/schema.py delete mode 100644 libs/migrate/versioning/schemadiff.py delete mode 100644 libs/migrate/versioning/script/__init__.py delete mode 100644 libs/migrate/versioning/script/base.py delete mode 100644 libs/migrate/versioning/script/py.py delete mode 100644 libs/migrate/versioning/script/sql.py delete mode 100644 libs/migrate/versioning/shell.py delete mode 100644 libs/migrate/versioning/template.py delete mode 100644 libs/migrate/versioning/templates/__init__.py delete mode 100644 libs/migrate/versioning/templates/manage/default.py_tmpl delete mode 100644 libs/migrate/versioning/templates/manage/pylons.py_tmpl delete mode 100644 libs/migrate/versioning/templates/repository/__init__.py delete mode 100644 libs/migrate/versioning/templates/repository/default/README delete mode 100644 libs/migrate/versioning/templates/repository/default/__init__.py delete mode 100644 libs/migrate/versioning/templates/repository/default/migrate.cfg delete mode 100644 libs/migrate/versioning/templates/repository/default/versions/__init__.py delete mode 100644 libs/migrate/versioning/templates/repository/pylons/README delete mode 100644 libs/migrate/versioning/templates/repository/pylons/__init__.py delete mode 100644 libs/migrate/versioning/templates/repository/pylons/migrate.cfg delete mode 100644 libs/migrate/versioning/templates/repository/pylons/versions/__init__.py delete mode 100644 libs/migrate/versioning/templates/script/__init__.py delete mode 100644 libs/migrate/versioning/templates/script/default.py_tmpl delete mode 100644 libs/migrate/versioning/templates/script/pylons.py_tmpl delete mode 100644 libs/migrate/versioning/templates/sql_script/default.py_tmpl delete mode 100644 libs/migrate/versioning/templates/sql_script/pylons.py_tmpl delete mode 100644 libs/migrate/versioning/util/__init__.py delete mode 100644 libs/migrate/versioning/util/importpath.py delete mode 100644 libs/migrate/versioning/util/keyedinstance.py delete mode 100644 libs/migrate/versioning/version.py delete mode 100644 libs/sqlalchemy/__init__.py delete mode 100644 libs/sqlalchemy/cextension/processors.c delete mode 100644 libs/sqlalchemy/cextension/resultproxy.c delete mode 100644 libs/sqlalchemy/cextension/utils.c delete mode 100644 libs/sqlalchemy/connectors/__init__.py delete mode 100644 libs/sqlalchemy/connectors/mxodbc.py delete mode 100644 libs/sqlalchemy/connectors/mysqldb.py delete mode 100644 libs/sqlalchemy/connectors/pyodbc.py delete mode 100644 libs/sqlalchemy/connectors/zxJDBC.py delete mode 100644 libs/sqlalchemy/databases/__init__.py delete mode 100644 libs/sqlalchemy/dialects/__init__.py delete mode 100644 libs/sqlalchemy/dialects/drizzle/__init__.py delete mode 100644 libs/sqlalchemy/dialects/drizzle/base.py delete mode 100644 libs/sqlalchemy/dialects/drizzle/mysqldb.py delete mode 100644 libs/sqlalchemy/dialects/firebird/__init__.py delete mode 100644 libs/sqlalchemy/dialects/firebird/base.py delete mode 100644 libs/sqlalchemy/dialects/firebird/fdb.py delete mode 100644 libs/sqlalchemy/dialects/firebird/kinterbasdb.py delete mode 100644 libs/sqlalchemy/dialects/mssql/__init__.py delete mode 100644 libs/sqlalchemy/dialects/mssql/adodbapi.py delete mode 100644 libs/sqlalchemy/dialects/mssql/base.py delete mode 100644 libs/sqlalchemy/dialects/mssql/information_schema.py delete mode 100644 libs/sqlalchemy/dialects/mssql/mxodbc.py delete mode 100644 libs/sqlalchemy/dialects/mssql/pymssql.py delete mode 100644 libs/sqlalchemy/dialects/mssql/pyodbc.py delete mode 100644 libs/sqlalchemy/dialects/mssql/zxjdbc.py delete mode 100644 libs/sqlalchemy/dialects/mysql/__init__.py delete mode 100644 libs/sqlalchemy/dialects/mysql/base.py delete mode 100644 libs/sqlalchemy/dialects/mysql/cymysql.py delete mode 100644 libs/sqlalchemy/dialects/mysql/gaerdbms.py delete mode 100644 libs/sqlalchemy/dialects/mysql/mysqlconnector.py delete mode 100644 libs/sqlalchemy/dialects/mysql/mysqldb.py delete mode 100644 libs/sqlalchemy/dialects/mysql/oursql.py delete mode 100644 libs/sqlalchemy/dialects/mysql/pymysql.py delete mode 100644 libs/sqlalchemy/dialects/mysql/pyodbc.py delete mode 100644 libs/sqlalchemy/dialects/mysql/zxjdbc.py delete mode 100644 libs/sqlalchemy/dialects/oracle/__init__.py delete mode 100644 libs/sqlalchemy/dialects/oracle/base.py delete mode 100644 libs/sqlalchemy/dialects/oracle/cx_oracle.py delete mode 100644 libs/sqlalchemy/dialects/oracle/zxjdbc.py delete mode 100644 libs/sqlalchemy/dialects/postgres.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/__init__.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/base.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/constraints.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/hstore.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/json.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/pg8000.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/psycopg2.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/pypostgresql.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/ranges.py delete mode 100644 libs/sqlalchemy/dialects/postgresql/zxjdbc.py delete mode 100644 libs/sqlalchemy/dialects/sqlite/__init__.py delete mode 100644 libs/sqlalchemy/dialects/sqlite/base.py delete mode 100644 libs/sqlalchemy/dialects/sqlite/pysqlite.py delete mode 100644 libs/sqlalchemy/dialects/sybase/__init__.py delete mode 100644 libs/sqlalchemy/dialects/sybase/base.py delete mode 100644 libs/sqlalchemy/dialects/sybase/mxodbc.py delete mode 100644 libs/sqlalchemy/dialects/sybase/pyodbc.py delete mode 100644 libs/sqlalchemy/dialects/sybase/pysybase.py delete mode 100644 libs/sqlalchemy/dialects/type_migration_guidelines.txt delete mode 100644 libs/sqlalchemy/engine/__init__.py delete mode 100644 libs/sqlalchemy/engine/base.py delete mode 100644 libs/sqlalchemy/engine/default.py delete mode 100644 libs/sqlalchemy/engine/interfaces.py delete mode 100644 libs/sqlalchemy/engine/reflection.py delete mode 100644 libs/sqlalchemy/engine/result.py delete mode 100644 libs/sqlalchemy/engine/strategies.py delete mode 100644 libs/sqlalchemy/engine/threadlocal.py delete mode 100644 libs/sqlalchemy/engine/url.py delete mode 100644 libs/sqlalchemy/engine/util.py delete mode 100644 libs/sqlalchemy/event/__init__.py delete mode 100644 libs/sqlalchemy/event/api.py delete mode 100644 libs/sqlalchemy/event/attr.py delete mode 100644 libs/sqlalchemy/event/base.py delete mode 100644 libs/sqlalchemy/event/legacy.py delete mode 100644 libs/sqlalchemy/event/registry.py delete mode 100644 libs/sqlalchemy/events.py delete mode 100644 libs/sqlalchemy/exc.py delete mode 100644 libs/sqlalchemy/ext/__init__.py delete mode 100644 libs/sqlalchemy/ext/associationproxy.py delete mode 100644 libs/sqlalchemy/ext/automap.py delete mode 100644 libs/sqlalchemy/ext/compiler.py delete mode 100644 libs/sqlalchemy/ext/declarative/__init__.py delete mode 100644 libs/sqlalchemy/ext/declarative/api.py delete mode 100644 libs/sqlalchemy/ext/declarative/base.py delete mode 100644 libs/sqlalchemy/ext/declarative/clsregistry.py delete mode 100644 libs/sqlalchemy/ext/horizontal_shard.py delete mode 100644 libs/sqlalchemy/ext/hybrid.py delete mode 100644 libs/sqlalchemy/ext/instrumentation.py delete mode 100644 libs/sqlalchemy/ext/mutable.py delete mode 100644 libs/sqlalchemy/ext/orderinglist.py delete mode 100644 libs/sqlalchemy/ext/serializer.py delete mode 100644 libs/sqlalchemy/inspection.py delete mode 100644 libs/sqlalchemy/interfaces.py delete mode 100644 libs/sqlalchemy/log.py delete mode 100644 libs/sqlalchemy/orm/__init__.py delete mode 100644 libs/sqlalchemy/orm/attributes.py delete mode 100644 libs/sqlalchemy/orm/base.py delete mode 100644 libs/sqlalchemy/orm/collections.py delete mode 100644 libs/sqlalchemy/orm/dependency.py delete mode 100644 libs/sqlalchemy/orm/deprecated_interfaces.py delete mode 100644 libs/sqlalchemy/orm/descriptor_props.py delete mode 100644 libs/sqlalchemy/orm/dynamic.py delete mode 100644 libs/sqlalchemy/orm/evaluator.py delete mode 100644 libs/sqlalchemy/orm/events.py delete mode 100644 libs/sqlalchemy/orm/exc.py delete mode 100644 libs/sqlalchemy/orm/identity.py delete mode 100644 libs/sqlalchemy/orm/instrumentation.py delete mode 100644 libs/sqlalchemy/orm/interfaces.py delete mode 100644 libs/sqlalchemy/orm/loading.py delete mode 100644 libs/sqlalchemy/orm/mapper.py delete mode 100644 libs/sqlalchemy/orm/path_registry.py delete mode 100644 libs/sqlalchemy/orm/persistence.py delete mode 100644 libs/sqlalchemy/orm/properties.py delete mode 100644 libs/sqlalchemy/orm/query.py delete mode 100644 libs/sqlalchemy/orm/relationships.py delete mode 100644 libs/sqlalchemy/orm/scoping.py delete mode 100644 libs/sqlalchemy/orm/session.py delete mode 100644 libs/sqlalchemy/orm/state.py delete mode 100644 libs/sqlalchemy/orm/strategies.py delete mode 100644 libs/sqlalchemy/orm/strategy_options.py delete mode 100644 libs/sqlalchemy/orm/sync.py delete mode 100644 libs/sqlalchemy/orm/unitofwork.py delete mode 100644 libs/sqlalchemy/orm/util.py delete mode 100644 libs/sqlalchemy/pool.py delete mode 100644 libs/sqlalchemy/processors.py delete mode 100644 libs/sqlalchemy/schema.py delete mode 100644 libs/sqlalchemy/sql/__init__.py delete mode 100644 libs/sqlalchemy/sql/annotation.py delete mode 100644 libs/sqlalchemy/sql/base.py delete mode 100644 libs/sqlalchemy/sql/compiler.py delete mode 100644 libs/sqlalchemy/sql/ddl.py delete mode 100644 libs/sqlalchemy/sql/default_comparator.py delete mode 100644 libs/sqlalchemy/sql/dml.py delete mode 100644 libs/sqlalchemy/sql/elements.py delete mode 100644 libs/sqlalchemy/sql/expression.py delete mode 100644 libs/sqlalchemy/sql/functions.py delete mode 100644 libs/sqlalchemy/sql/operators.py delete mode 100644 libs/sqlalchemy/sql/schema.py delete mode 100644 libs/sqlalchemy/sql/selectable.py delete mode 100644 libs/sqlalchemy/sql/sqltypes.py delete mode 100644 libs/sqlalchemy/sql/type_api.py delete mode 100644 libs/sqlalchemy/sql/util.py delete mode 100644 libs/sqlalchemy/sql/visitors.py delete mode 100644 libs/sqlalchemy/types.py delete mode 100644 libs/sqlalchemy/util/__init__.py delete mode 100644 libs/sqlalchemy/util/_collections.py delete mode 100644 libs/sqlalchemy/util/compat.py delete mode 100644 libs/sqlalchemy/util/deprecations.py delete mode 100644 libs/sqlalchemy/util/langhelpers.py delete mode 100644 libs/sqlalchemy/util/queue.py delete mode 100644 libs/sqlalchemy/util/topological.py delete mode 100644 libs/tempita/__init__.py delete mode 100644 libs/tempita/_looper.py delete mode 100644 libs/tempita/compat3.py diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index e2c9a310..721c1d00 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -181,7 +181,7 @@ class MediaPlugin(MediaBase): 'media': media, } - def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): + def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None): db = get_db() @@ -270,7 +270,6 @@ class MediaPlugin(MediaBase): limit_offset = kwargs.get('limit_offset') starts_with = kwargs.get('starts_with') search = kwargs.get('search') - order = kwargs.get('order') total_movies, movies = self.list( types = types, @@ -278,8 +277,7 @@ class MediaPlugin(MediaBase): release_status = release_status, limit_offset = limit_offset, starts_with = starts_with, - search = search, - order = order + search = search ) return { diff --git a/couchpotato/core/migration/__init__.py b/couchpotato/core/migration/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/couchpotato/core/migration/migrate.cfg b/couchpotato/core/migration/migrate.cfg deleted file mode 100644 index f17e967a..00000000 --- a/couchpotato/core/migration/migrate.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[db_settings] -repository_id = CouchPotato -version_table = migrate_version -required_dbs = ['sqlite'] diff --git a/couchpotato/core/migration/versions/001_Releases_last_edit.py b/couchpotato/core/migration/versions/001_Releases_last_edit.py deleted file mode 100644 index d4b12080..00000000 --- a/couchpotato/core/migration/versions/001_Releases_last_edit.py +++ /dev/null @@ -1,25 +0,0 @@ -from migrate.changeset.schema import create_column -from sqlalchemy.schema import MetaData, Column, Table, Index -from sqlalchemy.types import Integer - -meta = MetaData() - - -def upgrade(migrate_engine): - meta.bind = migrate_engine - - # Change release, add last_edit and index - last_edit_column = Column('last_edit', Integer) - release = Table('release', meta, last_edit_column) - - create_column(last_edit_column, release) - Index('ix_release_last_edit', release.c.last_edit).create() - - # Change movie last_edit - last_edit_column = Column('last_edit', Integer) - movie = Table('movie', meta, last_edit_column) - Index('ix_movie_last_edit', movie.c.last_edit).create() - - -def downgrade(migrate_engine): - pass diff --git a/couchpotato/core/migration/versions/002_Movie_category.py b/couchpotato/core/migration/versions/002_Movie_category.py deleted file mode 100644 index 023e47c6..00000000 --- a/couchpotato/core/migration/versions/002_Movie_category.py +++ /dev/null @@ -1,18 +0,0 @@ -from migrate.changeset.schema import create_column -from sqlalchemy.schema import MetaData, Column, Table, Index -from sqlalchemy.types import Integer - -meta = MetaData() - - -def upgrade(migrate_engine): - meta.bind = migrate_engine - - category_column = Column('category_id', Integer) - movie = Table('movie', meta, category_column) - create_column(category_column, movie) - Index('ix_movie_category_id', movie.c.category_id).create() - - -def downgrade(migrate_engine): - pass diff --git a/couchpotato/core/migration/versions/__init__.py b/couchpotato/core/migration/versions/__init__.py deleted file mode 100755 index 7e6e44bf..00000000 --- a/couchpotato/core/migration/versions/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -""" - Examples - - Adding a column: - - from migrate import * - from migrate.changeset.schema import create_column - from sqlalchemy import * - - meta = MetaData() - - def upgrade(migrate_engine): - meta.bind = migrate_engine - - #print changeset.schema - path_column = Column('path', String) - resource = Table('resource', meta, path_column) - - create_column(path_column, resource) - - - - Adding Relation table: http://www.mail-archive.com/sqlelixir@googlegroups.com/msg02061.html - - person = Table('person', metadata, Column('id', Integer)) - person_column = Column('person_id', Integer, ForeignKey('person.id'), nullable=False) - movie = Table('movie', metadata, person_column) - person_constraint = ForeignKeyConstraint(['person_id'], ['person.id'], ondelete="restrict", table=movie) - -""" diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 31a776c5..44e3573c 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -86,13 +86,13 @@ class XBMC(Notification): # send the text message resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) - for result in resp: - if result.get('result') and result['result'] == 'OK': + for r in resp: + if r.get('result') and r['result'] == 'OK': log.debug('Message delivered successfully!') success = True break - elif result.get('error'): - log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + elif r.get('error'): + log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code'])) break elif result.get('result') and type(result['result']['version']).__name__ == 'dict': @@ -109,13 +109,13 @@ class XBMC(Notification): # send the text message resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) - for result in resp: - if result.get('result') and result['result'] == 'OK': + for r in resp: + if r.get('result') and r['result'] == 'OK': log.debug('Message delivered successfully!') success = True break - elif result.get('error'): - log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + elif r.get('error'): + log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code'])) break # error getting version info (we do have contact with XBMC though) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 5a805361..a315cf81 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -110,7 +110,7 @@ class Plugin(object): f.write(content) f.close() os.chmod(path, Env.getPermission('file')) - except Exception as e: + except: log.error('Unable writing to file "%s": %s', (path, traceback.format_exc())) if os.path.isfile(path): os.remove(path) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 9bd76ebe..4e23ea31 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -202,10 +202,9 @@ class QualityPlugin(Plugin): # Try again with loose testing for quality in qualities: - loose_score = self.guessLooseScore(quality, files = files, extra = extra) + loose_score = self.guessLooseScore(quality, extra = extra) self.calcScore(score, quality, loose_score) - # Return nothing if all scores are 0 has_non_zero = 0 for s in score: @@ -262,7 +261,7 @@ class QualityPlugin(Plugin): return score - def guessLooseScore(self, quality, files = None, extra = None): + def guessLooseScore(self, quality, extra = None): score = 0 diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 185c2d2e..20d67bb9 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -8,7 +8,6 @@ from couchpotato.core.plugins.base import Plugin from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex from couchpotato.environment import Env from inspect import ismethod, isfunction -from sqlalchemy.exc import InterfaceError import os import time import traceback @@ -395,7 +394,7 @@ class Release(Plugin): continue rls['info'][info] = toUnicode(rel[info]) - except InterfaceError: + except: log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) db.update(rls) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py deleted file mode 100644 index 8b235dd1..00000000 --- a/couchpotato/core/settings/model.py +++ /dev/null @@ -1,505 +0,0 @@ -import uuid -import datetime - -from sqlalchemy import Column, ForeignKey, Table, Index -from sqlalchemy.ext.associationproxy import AssociationProxy -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import relationship, object_mapper, ColumnProperty, class_mapper -from sqlalchemy.orm.exc import UnmappedInstanceError -from sqlalchemy.orm.query import Query -from sqlalchemy.ext.declarative import declarative_base -from couchpotato.core.helpers.encoding import toUnicode -from sqlalchemy.ext.mutable import Mutable -from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \ - TypeDecorator -import json -import time - - -class SetEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, set): - return list(obj) - return json.JSONEncoder.default(self, obj) - - -class JsonType(TypeDecorator): - impl = UnicodeText - - def process_bind_param(self, value, dialect): - try: - return toUnicode(json.dumps(value, cls = SetEncoder)) - except: - try: - return toUnicode(json.dumps(value, cls = SetEncoder, encoding = 'latin-1')) - except: - raise - - def process_result_value(self, value, dialect): - return json.loads(value if value else '{}') - - -class MutableDict(Mutable, dict): - - @classmethod - def coerce(cls, key, value): - if not isinstance(value, MutableDict): - if isinstance(value, dict): - return MutableDict(value) - return Mutable.coerce(key, value) - else: - return value - - def __delitem(self, key): - dict.__delitem__(self, key) - self.changed() - - def __setitem__(self, key, value): - dict.__setitem__(self, key, value) - self.changed() - - def __getstate__(self): - return dict(self) - - def __setstate__(self, state): - self.update(self) - - def update(self, *args, **kwargs): - super(MutableDict, self).update(*args, **kwargs) - self.changed() - -MutableDict.associate_with(JsonType) - -Base = declarative_base() - -COLUMN_BLACKLIST = ('_sa_polymorphic_on', ) - -def is_mapped_class(cls): - try: - class_mapper(cls) - return True - except: - return False - -def is_like_list(instance, relation): - """Returns ``True`` if and only if the relation of `instance` whose name is -`relation` is list-like. - -A relation may be like a list if, for example, it is a non-lazy one-to-many -relation, or it is a dynamically loaded one-to-many. - -""" - if relation in instance._sa_class_manager: - return instance._sa_class_manager[relation].property.uselist - related_value = getattr(type(instance), relation, None) - return isinstance(related_value, AssociationProxy) - -class TableHelper(): - def to_dict(self, deep = None, exclude = None, include = None, - exclude_relations = None, include_relations = None, - include_methods = None): - instance = self - - if (exclude is not None or exclude_relations is not None) and \ - (include is not None or include_relations is not None): - raise ValueError('Cannot specify both include and exclude.') - # create a list of names of columns, including hybrid properties - try: - columns = [p.key for p in object_mapper(instance).iterate_properties - if isinstance(p, ColumnProperty)] - except UnmappedInstanceError: - return instance - for parent in type(instance).mro(): - columns += [key for key, value in parent.__dict__.items() - if isinstance(value, hybrid_property)] - # filter the columns based on exclude and include values - if exclude is not None: - columns = (c for c in columns if c not in exclude) - elif include is not None: - columns = (c for c in columns if c in include) - # create a dictionary mapping column name to value - result = dict((col, getattr(instance, col)) for col in columns - if not (col.startswith('__') or col in COLUMN_BLACKLIST)) - # add any included methods - if include_methods is not None: - result.update(dict((method, getattr(instance, method)()) for method in include_methods if not '.' in method)) - # Check for objects in the dictionary that may not be serializable by - # default. Specifically, convert datetime and date objects to ISO 8601 - # format, and convert UUID objects to hexadecimal strings. - for key, value in result.items(): - # TODO We can get rid of this when issue #33 is resolved. - if isinstance(value, datetime.date): - result[key] = value.isoformat() - elif isinstance(value, uuid.UUID): - result[key] = str(value) - elif is_mapped_class(type(value)): - result[key] = value.to_dict() - # recursively call _to_dict on each of the `deep` relations - deep = deep or {} - for relation, rdeep in deep.items(): - # Get the related value so we can see if it is None, a list, a query - # (as specified by a dynamic relationship loader), or an actual - # instance of a model. - relatedvalue = getattr(instance, relation) - if relatedvalue is None: - result[relation] = None - continue - # Determine the included and excluded fields for the related model. - newexclude = None - newinclude = None - if exclude_relations is not None and relation in exclude_relations: - newexclude = exclude_relations[relation] - elif (include_relations is not None and - relation in include_relations): - newinclude = include_relations[relation] - # Determine the included methods for the related model. - newmethods = None - if include_methods is not None: - newmethods = [method.split('.', 1)[1] for method in include_methods - if method.split('.', 1)[0] == relation] - if is_like_list(instance, relation): - result[relation] = [inst.to_dict(rdeep, exclude = newexclude, - include = newinclude, - include_methods = newmethods) - for inst in relatedvalue] - continue - # If the related value is dynamically loaded, resolve the query to get - # the single instance. - if isinstance(relatedvalue, Query): - relatedvalue = relatedvalue.one() - result[relation] = relatedvalue.to_dict(rdeep, exclude = newexclude, - include = newinclude, - include_methods = newmethods) - - return result - - -movie_files = Table('movie_files__file_movie', Base.metadata, - Column('movie_id', Integer, ForeignKey('movie.id'), nullable = False), - Column('file_id', Integer, ForeignKey('file.id'), nullable = False), - Index('movie_files_idx', 'movie_id', 'file_id', unique = True) -) - -release_files = Table('release_files__file_release', Base.metadata, - Column('release_id', Integer, ForeignKey('release.id'), nullable = False), - Column('file_id', Integer, ForeignKey('file.id'), nullable = False), - Index('release_files_idx', 'release_id', 'file_id', unique = True) -) - -library_files = Table('library_files__file_library', Base.metadata, - Column('library_id', Integer, ForeignKey('library.id'), nullable = False), - Column('file_id', Integer, ForeignKey('file.id'), nullable = False), - Index('library_files_idx', 'library_id', 'file_id', unique = True) -) - -class Movie(Base, TableHelper): - __tablename__ = 'movie' - id = Column(Integer, primary_key = True) - - """Movie Resource a movie could have multiple releases - The files belonging to the movie object are global for the whole movie - such as trailers, nfo, thumbnails""" - - last_edit = Column(Integer, default = lambda: int(time.time()), index = True) - type = 'movie' # Compat tv branch - - library_id = Column(Integer, ForeignKey('library.id'), index = True) - status_id = Column(Integer, ForeignKey('status.id'), index = True) - profile_id = Column(Integer, ForeignKey('profile.id'), index = True) - category_id = Column(Integer, ForeignKey('category.id'), index = True) - - library = relationship('Library') #cascade = 'delete, delete-orphan', single_parent = True) - status = relationship('Status') - profile = relationship('Profile') - category = relationship('Category') - releases = relationship('Release') #, cascade = 'all, delete-orphan') - files = relationship('File', secondary = movie_files) #, cascade = 'all, delete-orphan', single_parent = True) - -Media = Movie # Compat tv branch - - -class Library(Base, TableHelper): - __tablename__ = 'library' - id = Column(Integer, primary_key = True) - - """""" - - year = Column(Integer) - identifier = Column(String(20), index = True) - - plot = Column(UnicodeText) - tagline = Column(UnicodeText(255)) - info = Column(JsonType) - - status_id = Column(Integer, ForeignKey('status.id'), index = True) - status = relationship('Status') - - movies = relationship('Movie') #, cascade = 'all, delete-orphan') - titles = relationship('LibraryTitle', order_by="desc(LibraryTitle.default)") #, cascade = 'all, delete-orphan') - files = relationship('File', secondary = library_files) #, cascade = 'all, delete-orphan', single_parent = True) - - -class LibraryTitle(Base, TableHelper): - __tablename__ = 'librarytitle' - id = Column(Integer, primary_key = True) - - """""" - - #using_options(order_by = '-default') - - title = Column(Unicode) - simple_title = Column(Unicode, index = True) - default = Column(Boolean, default = False, index = True) - - language = relationship('Language') - - libraries_id = Column(Integer, ForeignKey('library.id'), index = True) - libraries = relationship('Library') - - -class Language(Base, TableHelper): - __tablename__ = 'language' - id = Column(Integer, primary_key = True) - - """""" - - identifier = Column(String(20), index = True) - label = Column(Unicode) - - titles_id = Column(Integer, ForeignKey('librarytitle.id'), index = True) - titles = relationship('LibraryTitle') - - -class Release(Base, TableHelper): - __tablename__ = 'release' - id = Column(Integer, primary_key = True) - - """Logically groups all files that belong to a certain release, such as - parts of a movie, subtitles.""" - - last_edit = Column(Integer, default = lambda: int(time.time()), index = True) - identifier = Column(String(100), index = True) - - movie_id = Column(Integer, ForeignKey('movie.id'), index = True) - movie = relationship('Movie') - - status_id = Column(Integer, ForeignKey('status.id'), index = True) - status = relationship('Status') - - quality_id = Column(Integer, ForeignKey('quality.id'), index = True) - quality = relationship('Quality') - - files = relationship('File', secondary = release_files) - info = relationship('ReleaseInfo') #, cascade = 'all, delete-orphan') - - def to_dict(self, deep = None, exclude = None, **kwargs): - if not exclude: exclude = [] - if not deep: deep = {} - - orig_dict = super(Release, self).to_dict(deep = deep, exclude = exclude) - - new_info = {} - for info in orig_dict.get('info', []): - - value = info['value'] - try: value = int(info['value']) - except: pass - - new_info[info['identifier']] = value - - orig_dict['info'] = new_info - - return orig_dict - - -class ReleaseInfo(Base, TableHelper): - __tablename__ = 'releaseinfo' - id = Column(Integer, primary_key = True) - - """Properties that can be bound to a file for off-line usage""" - - identifier = Column(String(50), index = True) - value = Column(Unicode(255), nullable = False) - - release_id = Column(Integer, ForeignKey('release.id'), index = True) - release = relationship('Release') - - -class Status(Base, TableHelper): - __tablename__ = 'status' - id = Column(Integer, primary_key = True) - - """The status of a release, such as Downloaded, Deleted, Wanted etc""" - - identifier = Column(String(20), unique = True) - label = Column(Unicode(20)) - - releases = relationship('Release') - movies = relationship('Movie') - - -class Quality(Base, TableHelper): - __tablename__ = 'quality' - id = Column(Integer, primary_key = True) - - """Quality name of a release, DVD, 720p, DVD-Rip etc""" - - #using_options(order_by = 'order') - - identifier = Column(String(20), unique = True) - label = Column(Unicode(20)) - order = Column(Integer, default = 0, index = True) - - size_min = Column(Integer) - size_max = Column(Integer) - - releases = relationship('Release') - profile_types = relationship('ProfileType', order_by="asc(ProfileType.order)") - - -class Profile(Base, TableHelper): - __tablename__ = 'profile' - id = Column(Integer, primary_key = True) - - """""" - - #using_options(order_by = 'order') - - label = Column(Unicode(50)) - order = Column(Integer, default = 0, index = True) - core = Column(Boolean, default = False) - hide = Column(Boolean, default = False) - - movie = relationship('Movie') - types = relationship('ProfileType', order_by="asc(ProfileType.order)") #, cascade = 'all, delete-orphan') - - def to_dict(self, deep = None, exclude = None, **kwargs): - if not exclude: exclude = [] - if not deep: deep = {} - - orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude) - orig_dict['core'] = orig_dict.get('core') or False - orig_dict['hide'] = orig_dict.get('hide') or False - - return orig_dict - - -class Category(Base, TableHelper): - __tablename__ = 'category' - id = Column(Integer, primary_key = True) - - """""" - - #using_options(order_by = 'order') - - label = Column(Unicode(50)) - order = Column(Integer, default = 0, index = True) - required = Column(Unicode(255)) - preferred = Column(Unicode(255)) - ignored = Column(Unicode(255)) - destination = Column(Unicode(255)) - - movie = relationship('Movie') - - -class ProfileType(Base, TableHelper): - __tablename__ = 'profiletype' - id = Column(Integer, primary_key = True) - - """""" - - #using_options(order_by = 'order') - - order = Column(Integer, default = 0, index = True) - finish = Column(Boolean, default = True) - wait_for = Column(Integer, default = 0) - - quality_id = Column(Integer, ForeignKey('quality.id'), index = True) - quality = relationship('Quality') - - profile_id = Column(Integer, ForeignKey('profile.id'), index = True) - profile = relationship('Profile') - - -class File(Base, TableHelper): - __tablename__ = 'file' - id = Column(Integer, primary_key = True) - - """File that belongs to a release.""" - - path = Column(Unicode(255), nullable = False, unique = True) - part = Column(Integer, default = 1) - available = Column(Boolean, default = True) - - type_id = Column(Integer, ForeignKey('filetype.id'), index = True) - type = relationship('FileType') - - properties = relationship('FileProperty') - - movie = relationship('Movie', secondary = movie_files) - release = relationship('Release', secondary = release_files) - library = relationship('Library', secondary = library_files) - - -class FileType(Base, TableHelper): - __tablename__ = 'filetype' - id = Column(Integer, primary_key = True) - - """Types could be trailer, subtitle, movie, partial movie etc.""" - - identifier = Column(String(20), unique = True) - type = Column(Unicode(20)) - name = Column(Unicode(50), nullable = False) - - files = relationship('File') - - -class FileProperty(Base, TableHelper): - __tablename__ = 'fileproperty' - id = Column(Integer, primary_key = True) - - """Properties that can be bound to a file for off-line usage""" - - identifier = Column(String(20), index = True) - value = Column(Unicode(255), nullable = False) - - file_id = Column(Integer, ForeignKey('file.id'), index = True) - file = relationship('File') - - -class Notification(Base, TableHelper): - __tablename__ = 'notification' - id = Column(Integer, primary_key = True) - - """""" - - #using_options(order_by = 'added') - - added = Column(Integer, default = lambda: int(time.time()), index = True) - read = Column(Boolean, default = False, index = True) - message = Column(Unicode(255)) - data = Column(JsonType) - - -class Properties(Base, TableHelper): - __tablename__ = 'properties' - id = Column(Integer, primary_key = True) - - """""" - - identifier = Column(String(50), index = True) - value = Column(Unicode(255), nullable = False) - - -def setup(): - """Setup the database and create the tables that don't exists yet""" - from couchpotato.environment import Env - - engine = Env.getEngine() - Base.metadata.create_all(engine) - - try: - engine.execute("PRAGMA journal_mode = WAL") - engine.execute("PRAGMA temp_store = MEMORY") - except: - pass diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 5eb1272c..40d1d7af 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -1,9 +1,6 @@ from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings -from sqlalchemy.engine import create_engine -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm.session import sessionmaker import os diff --git a/libs/decorator.py b/libs/decorator.py deleted file mode 100644 index ea7e9909..00000000 --- a/libs/decorator.py +++ /dev/null @@ -1,210 +0,0 @@ -########################## LICENCE ############################### -## -## Copyright (c) 2005-2011, Michele Simionato -## All rights reserved. -## -## Redistributions of source code must retain the above copyright -## notice, this list of conditions and the following disclaimer. -## Redistributions in bytecode form must reproduce the above copyright -## notice, this list of conditions and the following disclaimer in -## the documentation and/or other materials provided with the -## distribution. - -## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -## DAMAGE. - -""" -Decorator module, see http://pypi.python.org/pypi/decorator -for the documentation. -""" - -__version__ = '3.3.2' - -__all__ = ["decorator", "FunctionMaker", "partial"] - -import sys, re, inspect - -try: - from functools import partial -except ImportError: # for Python version < 2.5 - class partial(object): - "A simple replacement of functools.partial" - def __init__(self, func, *args, **kw): - self.func = func - self.args = args - self.keywords = kw - def __call__(self, *otherargs, **otherkw): - kw = self.keywords.copy() - kw.update(otherkw) - return self.func(*(self.args + otherargs), **kw) - -if sys.version >= '3': - from inspect import getfullargspec -else: - class getfullargspec(object): - "A quick and dirty replacement for getfullargspec for Python 2.X" - def __init__(self, f): - self.args, self.varargs, self.varkw, self.defaults = \ - inspect.getargspec(f) - self.kwonlyargs = [] - self.kwonlydefaults = None - self.annotations = getattr(f, '__annotations__', {}) - def __iter__(self): - yield self.args - yield self.varargs - yield self.varkw - yield self.defaults - -DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(') - -# basic functionality -class FunctionMaker(object): - """ - An object with the ability to create functions with a given signature. - It has attributes name, doc, module, signature, defaults, dict and - methods update and make. - """ - def __init__(self, func=None, name=None, signature=None, - defaults=None, doc=None, module=None, funcdict=None): - self.shortsignature = signature - if func: - # func can be a class or a callable, but not an instance method - self.name = func.__name__ - if self.name == '': # small hack for lambda functions - self.name = '_lambda_' - self.doc = func.__doc__ - self.module = func.__module__ - if inspect.isfunction(func): - argspec = getfullargspec(func) - for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', - 'kwonlydefaults', 'annotations'): - setattr(self, a, getattr(argspec, a)) - for i, arg in enumerate(self.args): - setattr(self, 'arg%d' % i, arg) - self.signature = inspect.formatargspec( - formatvalue=lambda val: "", *argspec)[1:-1] - allargs = list(self.args) - if self.varargs: - allargs.append('*' + self.varargs) - if self.varkw: - allargs.append('**' + self.varkw) - try: - self.shortsignature = ', '.join(allargs) - except TypeError: # exotic signature, valid only in Python 2.X - self.shortsignature = self.signature - self.dict = func.__dict__.copy() - # func=None happens when decorating a caller - if name: - self.name = name - if signature is not None: - self.signature = signature - if defaults: - self.defaults = defaults - if doc: - self.doc = doc - if module: - self.module = module - if funcdict: - self.dict = funcdict - # check existence required attributes - assert hasattr(self, 'name') - if not hasattr(self, 'signature'): - raise TypeError('You are decorating a non function: %s' % func) - - def update(self, func, **kw): - "Update the signature of func with the data in self" - func.__name__ = self.name - func.__doc__ = getattr(self, 'doc', None) - func.__dict__ = getattr(self, 'dict', {}) - func.func_defaults = getattr(self, 'defaults', ()) - func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None) - callermodule = sys._getframe(3).f_globals.get('__name__', '?') - func.__module__ = getattr(self, 'module', callermodule) - func.__dict__.update(kw) - - def make(self, src_templ, evaldict=None, addsource=False, **attrs): - "Make a new function from a given template and update the signature" - src = src_templ % vars(self) # expand name and signature - evaldict = evaldict or {} - mo = DEF.match(src) - if mo is None: - raise SyntaxError('not a valid function template\n%s' % src) - name = mo.group(1) # extract the function name - names = set([name] + [arg.strip(' *') for arg in - self.shortsignature.split(',')]) - for n in names: - if n in ('_func_', '_call_'): - raise NameError('%s is overridden in\n%s' % (n, src)) - if not src.endswith('\n'): # add a newline just for safety - src += '\n' # this is needed in old versions of Python - try: - code = compile(src, '', 'single') - # print >> sys.stderr, 'Compiling %s' % src - exec code in evaldict - except: - print >> sys.stderr, 'Error in generated code:' - print >> sys.stderr, src - raise - func = evaldict[name] - if addsource: - attrs['__source__'] = src - self.update(func, **attrs) - return func - - @classmethod - def create(cls, obj, body, evaldict, defaults=None, - doc=None, module=None, addsource=True,**attrs): - """ - Create a function from the strings name, signature and body. - evaldict is the evaluation dictionary. If addsource is true an attribute - __source__ is added to the result. The attributes attrs are added, - if any. - """ - if isinstance(obj, str): # "name(signature)" - name, rest = obj.strip().split('(', 1) - signature = rest[:-1] #strip a right parens - func = None - else: # a function - name = None - signature = None - func = obj - self = cls(func, name, signature, defaults, doc, module) - ibody = '\n'.join(' ' + line for line in body.splitlines()) - return self.make('def %(name)s(%(signature)s):\n' + ibody, - evaldict, addsource, **attrs) - -def decorator(caller, func=None): - """ - decorator(caller) converts a caller function into a decorator; - decorator(caller, func) decorates a function using a caller. - """ - if func is not None: # returns a decorated function - evaldict = func.func_globals.copy() - evaldict['_call_'] = caller - evaldict['_func_'] = func - return FunctionMaker.create( - func, "return _call_(_func_, %(shortsignature)s)", - evaldict, undecorated=func, __wrapped__=func) - else: # returns a decorator - if isinstance(caller, partial): - return partial(decorator, caller) - # otherwise assume caller is a function - first = inspect.getargspec(caller)[0][0] # first arg - evaldict = caller.func_globals.copy() - evaldict['_call_'] = caller - evaldict['decorator'] = decorator - return FunctionMaker.create( - '%s(%s)' % (caller.__name__, first), - 'return decorator(_call_, %s)' % first, - evaldict, undecorated=caller, __wrapped__=caller, - doc=caller.__doc__, module=caller.__module__) diff --git a/libs/migrate/__init__.py b/libs/migrate/__init__.py deleted file mode 100644 index 0cfdb726..00000000 --- a/libs/migrate/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" - SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for - database schema version and repository management and - :mod:`migrate.changeset` that allows to define database schema changes - using Python. -""" - -from migrate.versioning import * -from migrate.changeset import * - -__version__ = '0.7.2' diff --git a/libs/migrate/changeset/__init__.py b/libs/migrate/changeset/__init__.py deleted file mode 100644 index 80ea152d..00000000 --- a/libs/migrate/changeset/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - This module extends SQLAlchemy and provides additional DDL [#]_ - support. - - .. [#] SQL Data Definition Language -""" -import re -import warnings - -import sqlalchemy -from sqlalchemy import __version__ as _sa_version - -warnings.simplefilter('always', DeprecationWarning) - -_sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split(".")) -SQLA_07 = _sa_version >= (0, 7) - -del re -del _sa_version - -from migrate.changeset.schema import * -from migrate.changeset.constraint import * - -sqlalchemy.schema.Table.__bases__ += (ChangesetTable, ) -sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, ) -sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, ) - -sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, ) diff --git a/libs/migrate/changeset/ansisql.py b/libs/migrate/changeset/ansisql.py deleted file mode 100644 index 9ded5605..00000000 --- a/libs/migrate/changeset/ansisql.py +++ /dev/null @@ -1,292 +0,0 @@ -""" - Extensions to SQLAlchemy for altering existing tables. - - At the moment, this isn't so much based off of ANSI as much as - things that just happen to work with multiple databases. -""" -import StringIO - -import sqlalchemy as sa -from sqlalchemy.schema import SchemaVisitor -from sqlalchemy.engine.default import DefaultDialect -from sqlalchemy.sql import ClauseElement -from sqlalchemy.schema import (ForeignKeyConstraint, - PrimaryKeyConstraint, - CheckConstraint, - UniqueConstraint, - Index) - -from migrate import exceptions -from migrate.changeset import constraint - -from sqlalchemy.schema import AddConstraint, DropConstraint -from sqlalchemy.sql.compiler import DDLCompiler -SchemaGenerator = SchemaDropper = DDLCompiler - - -class AlterTableVisitor(SchemaVisitor): - """Common operations for ``ALTER TABLE`` statements.""" - - # engine.Compiler looks for .statement - # when it spawns off a new compiler - statement = ClauseElement() - - def append(self, s): - """Append content to the SchemaIterator's query buffer.""" - - self.buffer.write(s) - - def execute(self): - """Execute the contents of the SchemaIterator's buffer.""" - try: - return self.connection.execute(self.buffer.getvalue()) - finally: - self.buffer.truncate(0) - - def __init__(self, dialect, connection, **kw): - self.connection = connection - self.buffer = StringIO.StringIO() - self.preparer = dialect.identifier_preparer - self.dialect = dialect - - def traverse_single(self, elem): - ret = super(AlterTableVisitor, self).traverse_single(elem) - if ret: - # adapt to 0.6 which uses a string-returning - # object - self.append(" %s" % ret) - - def _to_table(self, param): - """Returns the table object for the given param object.""" - if isinstance(param, (sa.Column, sa.Index, sa.schema.Constraint)): - ret = param.table - else: - ret = param - return ret - - def start_alter_table(self, param): - """Returns the start of an ``ALTER TABLE`` SQL-Statement. - - Use the param object to determine the table name and use it - for building the SQL statement. - - :param param: object to determine the table from - :type param: :class:`sqlalchemy.Column`, :class:`sqlalchemy.Index`, - :class:`sqlalchemy.schema.Constraint`, :class:`sqlalchemy.Table`, - or string (table name) - """ - table = self._to_table(param) - self.append('\nALTER TABLE %s ' % self.preparer.format_table(table)) - return table - - -class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): - """Extends ansisql generator for column creation (alter table add col)""" - - def visit_column(self, column): - """Create a column (table already exists). - - :param column: column object - :type column: :class:`sqlalchemy.Column` instance - """ - if column.default is not None: - self.traverse_single(column.default) - - table = self.start_alter_table(column) - self.append("ADD ") - self.append(self.get_column_specification(column)) - - for cons in column.constraints: - self.traverse_single(cons) - self.execute() - - # ALTER TABLE STATEMENTS - - # add indexes and unique constraints - if column.index_name: - Index(column.index_name,column).create() - elif column.unique_name: - constraint.UniqueConstraint(column, - name=column.unique_name).create() - - # SA bounds FK constraints to table, add manually - for fk in column.foreign_keys: - self.add_foreignkey(fk.constraint) - - # add primary key constraint if needed - if column.primary_key_name: - cons = constraint.PrimaryKeyConstraint(column, - name=column.primary_key_name) - cons.create() - - def add_foreignkey(self, fk): - self.connection.execute(AddConstraint(fk)) - -class ANSIColumnDropper(AlterTableVisitor, SchemaDropper): - """Extends ANSI SQL dropper for column dropping (``ALTER TABLE - DROP COLUMN``). - """ - - def visit_column(self, column): - """Drop a column from its table. - - :param column: the column object - :type column: :class:`sqlalchemy.Column` - """ - table = self.start_alter_table(column) - self.append('DROP COLUMN %s' % self.preparer.format_column(column)) - self.execute() - - -class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): - """Manages changes to existing schema elements. - - Note that columns are schema elements; ``ALTER TABLE ADD COLUMN`` - is in SchemaGenerator. - - All items may be renamed. Columns can also have many of their properties - - type, for example - changed. - - Each function is passed a tuple, containing (object, name); where - object is a type of object you'd expect for that function - (ie. table for visit_table) and name is the object's new - name. NONE means the name is unchanged. - """ - - def visit_table(self, table): - """Rename a table. Other ops aren't supported.""" - self.start_alter_table(table) - self.append("RENAME TO %s" % self.preparer.quote(table.new_name, - table.quote)) - self.execute() - - def visit_index(self, index): - """Rename an index""" - if hasattr(self, '_validate_identifier'): - # SA <= 0.6.3 - self.append("ALTER INDEX %s RENAME TO %s" % ( - self.preparer.quote( - self._validate_identifier( - index.name, True), index.quote), - self.preparer.quote( - self._validate_identifier( - index.new_name, True), index.quote))) - else: - # SA >= 0.6.5 - self.append("ALTER INDEX %s RENAME TO %s" % ( - self.preparer.quote( - self._index_identifier( - index.name), index.quote), - self.preparer.quote( - self._index_identifier( - index.new_name), index.quote))) - self.execute() - - def visit_column(self, delta): - """Rename/change a column.""" - # ALTER COLUMN is implemented as several ALTER statements - keys = delta.keys() - if 'type' in keys: - self._run_subvisit(delta, self._visit_column_type) - if 'nullable' in keys: - self._run_subvisit(delta, self._visit_column_nullable) - if 'server_default' in keys: - # Skip 'default': only handle server-side defaults, others - # are managed by the app, not the db. - self._run_subvisit(delta, self._visit_column_default) - if 'name' in keys: - self._run_subvisit(delta, self._visit_column_name, start_alter=False) - - def _run_subvisit(self, delta, func, start_alter=True): - """Runs visit method based on what needs to be changed on column""" - table = self._to_table(delta.table) - col_name = delta.current_name - if start_alter: - self.start_alter_column(table, col_name) - ret = func(table, delta.result_column, delta) - self.execute() - - def start_alter_column(self, table, col_name): - """Starts ALTER COLUMN""" - self.start_alter_table(table) - self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote)) - - def _visit_column_nullable(self, table, column, delta): - nullable = delta['nullable'] - if nullable: - self.append("DROP NOT NULL") - else: - self.append("SET NOT NULL") - - def _visit_column_default(self, table, column, delta): - default_text = self.get_column_default_string(column) - if default_text is not None: - self.append("SET DEFAULT %s" % default_text) - else: - self.append("DROP DEFAULT") - - def _visit_column_type(self, table, column, delta): - type_ = delta['type'] - type_text = str(type_.compile(dialect=self.dialect)) - self.append("TYPE %s" % type_text) - - def _visit_column_name(self, table, column, delta): - self.start_alter_table(table) - col_name = self.preparer.quote(delta.current_name, table.quote) - new_name = self.preparer.format_column(delta.result_column) - self.append('RENAME COLUMN %s TO %s' % (col_name, new_name)) - - -class ANSIConstraintCommon(AlterTableVisitor): - """ - Migrate's constraints require a separate creation function from - SA's: Migrate's constraints are created independently of a table; - SA's are created at the same time as the table. - """ - - def get_constraint_name(self, cons): - """Gets a name for the given constraint. - - If the name is already set it will be used otherwise the - constraint's :meth:`autoname ` - method is used. - - :param cons: constraint object - """ - if cons.name is not None: - ret = cons.name - else: - ret = cons.name = cons.autoname() - return self.preparer.quote(ret, cons.quote) - - def visit_migrate_primary_key_constraint(self, *p, **k): - self._visit_constraint(*p, **k) - - def visit_migrate_foreign_key_constraint(self, *p, **k): - self._visit_constraint(*p, **k) - - def visit_migrate_check_constraint(self, *p, **k): - self._visit_constraint(*p, **k) - - def visit_migrate_unique_constraint(self, *p, **k): - self._visit_constraint(*p, **k) - -class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator): - def _visit_constraint(self, constraint): - constraint.name = self.get_constraint_name(constraint) - self.append(self.process(AddConstraint(constraint))) - self.execute() - -class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper): - def _visit_constraint(self, constraint): - constraint.name = self.get_constraint_name(constraint) - self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade))) - self.execute() - - -class ANSIDialect(DefaultDialect): - columngenerator = ANSIColumnGenerator - columndropper = ANSIColumnDropper - schemachanger = ANSISchemaChanger - constraintgenerator = ANSIConstraintGenerator - constraintdropper = ANSIConstraintDropper diff --git a/libs/migrate/changeset/constraint.py b/libs/migrate/changeset/constraint.py deleted file mode 100644 index 96407bd7..00000000 --- a/libs/migrate/changeset/constraint.py +++ /dev/null @@ -1,199 +0,0 @@ -""" - This module defines standalone schema constraint classes. -""" -from sqlalchemy import schema - -from migrate.exceptions import * - -class ConstraintChangeset(object): - """Base class for Constraint classes.""" - - def _normalize_columns(self, cols, table_name=False): - """Given: column objects or names; return col names and - (maybe) a table""" - colnames = [] - table = None - for col in cols: - if isinstance(col, schema.Column): - if col.table is not None and table is None: - table = col.table - if table_name: - col = '.'.join((col.table.name, col.name)) - else: - col = col.name - colnames.append(col) - return colnames, table - - def __do_imports(self, visitor_name, *a, **kw): - engine = kw.pop('engine', self.table.bind) - from migrate.changeset.databases.visitor import (get_engine_visitor, - run_single_visitor) - visitorcallable = get_engine_visitor(engine, visitor_name) - run_single_visitor(engine, visitorcallable, self, *a, **kw) - - def create(self, *a, **kw): - """Create the constraint in the database. - - :param engine: the database engine to use. If this is \ - :keyword:`None` the instance's engine will be used - :type engine: :class:`sqlalchemy.engine.base.Engine` - :param connection: reuse connection istead of creating new one. - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - """ - # TODO: set the parent here instead of in __init__ - self.__do_imports('constraintgenerator', *a, **kw) - - def drop(self, *a, **kw): - """Drop the constraint from the database. - - :param engine: the database engine to use. If this is - :keyword:`None` the instance's engine will be used - :param cascade: Issue CASCADE drop if database supports it - :type engine: :class:`sqlalchemy.engine.base.Engine` - :type cascade: bool - :param connection: reuse connection istead of creating new one. - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - :returns: Instance with cleared columns - """ - self.cascade = kw.pop('cascade', False) - self.__do_imports('constraintdropper', *a, **kw) - # the spirit of Constraint objects is that they - # are immutable (just like in a DB. they're only ADDed - # or DROPped). - #self.columns.clear() - return self - - -class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): - """Construct PrimaryKeyConstraint - - Migrate's additional parameters: - - :param cols: Columns in constraint. - :param table: If columns are passed as strings, this kw is required - :type table: Table instance - :type cols: strings or Column instances - """ - - __migrate_visit_name__ = 'migrate_primary_key_constraint' - - def __init__(self, *cols, **kwargs): - colnames, table = self._normalize_columns(cols) - table = kwargs.pop('table', table) - super(PrimaryKeyConstraint, self).__init__(*colnames, **kwargs) - if table is not None: - self._set_parent(table) - - - def autoname(self): - """Mimic the database's automatic constraint names""" - return "%s_pkey" % self.table.name - - -class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint): - """Construct ForeignKeyConstraint - - Migrate's additional parameters: - - :param columns: Columns in constraint - :param refcolumns: Columns that this FK reffers to in another table. - :param table: If columns are passed as strings, this kw is required - :type table: Table instance - :type columns: list of strings or Column instances - :type refcolumns: list of strings or Column instances - """ - - __migrate_visit_name__ = 'migrate_foreign_key_constraint' - - def __init__(self, columns, refcolumns, *args, **kwargs): - colnames, table = self._normalize_columns(columns) - table = kwargs.pop('table', table) - refcolnames, reftable = self._normalize_columns(refcolumns, - table_name=True) - super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args, - **kwargs) - if table is not None: - self._set_parent(table) - - @property - def referenced(self): - return [e.column for e in self.elements] - - @property - def reftable(self): - return self.referenced[0].table - - def autoname(self): - """Mimic the database's automatic constraint names""" - if hasattr(self.columns, 'keys'): - # SA <= 0.5 - firstcol = self.columns[self.columns.keys()[0]] - ret = "%(table)s_%(firstcolumn)s_fkey" % dict( - table=firstcol.table.name, - firstcolumn=firstcol.name,) - else: - # SA >= 0.6 - ret = "%(table)s_%(firstcolumn)s_fkey" % dict( - table=self.table.name, - firstcolumn=self.columns[0],) - return ret - - -class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): - """Construct CheckConstraint - - Migrate's additional parameters: - - :param sqltext: Plain SQL text to check condition - :param columns: If not name is applied, you must supply this kw\ - to autoname constraint - :param table: If columns are passed as strings, this kw is required - :type table: Table instance - :type columns: list of Columns instances - :type sqltext: string - """ - - __migrate_visit_name__ = 'migrate_check_constraint' - - def __init__(self, sqltext, *args, **kwargs): - cols = kwargs.pop('columns', []) - if not cols and not kwargs.get('name', False): - raise InvalidConstraintError('You must either set "name"' - 'parameter or "columns" to autogenarate it.') - colnames, table = self._normalize_columns(cols) - table = kwargs.pop('table', table) - schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs) - if table is not None: - self._set_parent(table) - self.colnames = colnames - - def autoname(self): - return "%(table)s_%(cols)s_check" % \ - dict(table=self.table.name, cols="_".join(self.colnames)) - - -class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint): - """Construct UniqueConstraint - - Migrate's additional parameters: - - :param cols: Columns in constraint. - :param table: If columns are passed as strings, this kw is required - :type table: Table instance - :type cols: strings or Column instances - - .. versionadded:: 0.6.0 - """ - - __migrate_visit_name__ = 'migrate_unique_constraint' - - def __init__(self, *cols, **kwargs): - self.colnames, table = self._normalize_columns(cols) - table = kwargs.pop('table', table) - super(UniqueConstraint, self).__init__(*self.colnames, **kwargs) - if table is not None: - self._set_parent(table) - - def autoname(self): - """Mimic the database's automatic constraint names""" - return "%s_%s_key" % (self.table.name, self.colnames[0]) diff --git a/libs/migrate/changeset/databases/__init__.py b/libs/migrate/changeset/databases/__init__.py deleted file mode 100644 index 85469183..00000000 --- a/libs/migrate/changeset/databases/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" - This module contains database dialect specific changeset - implementations. -""" -__all__ = [ - 'postgres', - 'sqlite', - 'mysql', - 'oracle', -] diff --git a/libs/migrate/changeset/databases/firebird.py b/libs/migrate/changeset/databases/firebird.py deleted file mode 100644 index 226728b9..00000000 --- a/libs/migrate/changeset/databases/firebird.py +++ /dev/null @@ -1,93 +0,0 @@ -""" - Firebird database specific implementations of changeset classes. -""" -from sqlalchemy.databases import firebird as sa_base -from sqlalchemy.schema import PrimaryKeyConstraint -from migrate import exceptions -from migrate.changeset import ansisql - - -FBSchemaGenerator = sa_base.FBDDLCompiler - -class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator): - """Firebird column generator implementation.""" - - -class FBColumnDropper(ansisql.ANSIColumnDropper): - """Firebird column dropper implementation.""" - - def visit_column(self, column): - """Firebird supports 'DROP col' instead of 'DROP COLUMN col' syntax - - Drop primary key and unique constraints if dropped column is referencing it.""" - if column.primary_key: - if column.table.primary_key.columns.contains_column(column): - column.table.primary_key.drop() - # TODO: recreate primary key if it references more than this column - - for index in column.table.indexes: - # "column in index.columns" causes problems as all - # column objects compare equal and return a SQL expression - if column.name in [col.name for col in index.columns]: - index.drop() - # TODO: recreate index if it references more than this column - - for cons in column.table.constraints: - if isinstance(cons,PrimaryKeyConstraint): - # will be deleted only when the column its on - # is deleted! - continue - - should_drop = column.name in cons.columns - if should_drop: - self.start_alter_table(column) - self.append("DROP CONSTRAINT ") - self.append(self.preparer.format_constraint(cons)) - self.execute() - # TODO: recreate unique constraint if it refenrences more than this column - - self.start_alter_table(column) - self.append('DROP %s' % self.preparer.format_column(column)) - self.execute() - - -class FBSchemaChanger(ansisql.ANSISchemaChanger): - """Firebird schema changer implementation.""" - - def visit_table(self, table): - """Rename table not supported""" - raise exceptions.NotSupportedError( - "Firebird does not support renaming tables.") - - def _visit_column_name(self, table, column, delta): - self.start_alter_table(table) - col_name = self.preparer.quote(delta.current_name, table.quote) - new_name = self.preparer.format_column(delta.result_column) - self.append('ALTER COLUMN %s TO %s' % (col_name, new_name)) - - def _visit_column_nullable(self, table, column, delta): - """Changing NULL is not supported""" - # TODO: http://www.firebirdfaq.org/faq103/ - raise exceptions.NotSupportedError( - "Firebird does not support altering NULL bevahior.") - - -class FBConstraintGenerator(ansisql.ANSIConstraintGenerator): - """Firebird constraint generator implementation.""" - - -class FBConstraintDropper(ansisql.ANSIConstraintDropper): - """Firebird constaint dropper implementation.""" - - def cascade_constraint(self, constraint): - """Cascading constraints is not supported""" - raise exceptions.NotSupportedError( - "Firebird does not support cascading constraints") - - -class FBDialect(ansisql.ANSIDialect): - columngenerator = FBColumnGenerator - columndropper = FBColumnDropper - schemachanger = FBSchemaChanger - constraintgenerator = FBConstraintGenerator - constraintdropper = FBConstraintDropper diff --git a/libs/migrate/changeset/databases/mysql.py b/libs/migrate/changeset/databases/mysql.py deleted file mode 100644 index 6987b4bb..00000000 --- a/libs/migrate/changeset/databases/mysql.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - MySQL database specific implementations of changeset classes. -""" - -from sqlalchemy.databases import mysql as sa_base -from sqlalchemy import types as sqltypes - -from migrate import exceptions -from migrate.changeset import ansisql - - -MySQLSchemaGenerator = sa_base.MySQLDDLCompiler - -class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator): - pass - - -class MySQLColumnDropper(ansisql.ANSIColumnDropper): - pass - - -class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger): - - def visit_column(self, delta): - table = delta.table - colspec = self.get_column_specification(delta.result_column) - if delta.result_column.autoincrement: - primary_keys = [c for c in table.primary_key.columns - if (c.autoincrement and - isinstance(c.type, sqltypes.Integer) and - not c.foreign_keys)] - - if primary_keys: - first = primary_keys.pop(0) - if first.name == delta.current_name: - colspec += " AUTO_INCREMENT" - old_col_name = self.preparer.quote(delta.current_name, table.quote) - - self.start_alter_table(table) - - self.append("CHANGE COLUMN %s " % old_col_name) - self.append(colspec) - self.execute() - - def visit_index(self, param): - # If MySQL can do this, I can't find how - raise exceptions.NotSupportedError("MySQL cannot rename indexes") - - -class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator): - pass - - -class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper): - def visit_migrate_check_constraint(self, *p, **k): - raise exceptions.NotSupportedError("MySQL does not support CHECK" - " constraints, use triggers instead.") - - -class MySQLDialect(ansisql.ANSIDialect): - columngenerator = MySQLColumnGenerator - columndropper = MySQLColumnDropper - schemachanger = MySQLSchemaChanger - constraintgenerator = MySQLConstraintGenerator - constraintdropper = MySQLConstraintDropper diff --git a/libs/migrate/changeset/databases/oracle.py b/libs/migrate/changeset/databases/oracle.py deleted file mode 100644 index 2f16b5b5..00000000 --- a/libs/migrate/changeset/databases/oracle.py +++ /dev/null @@ -1,108 +0,0 @@ -""" - Oracle database specific implementations of changeset classes. -""" -import sqlalchemy as sa -from sqlalchemy.databases import oracle as sa_base - -from migrate import exceptions -from migrate.changeset import ansisql - - -OracleSchemaGenerator = sa_base.OracleDDLCompiler - - -class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator): - pass - - -class OracleColumnDropper(ansisql.ANSIColumnDropper): - pass - - -class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger): - - def get_column_specification(self, column, **kwargs): - # Ignore the NOT NULL generated - override_nullable = kwargs.pop('override_nullable', None) - if override_nullable: - orig = column.nullable - column.nullable = True - ret = super(OracleSchemaChanger, self).get_column_specification( - column, **kwargs) - if override_nullable: - column.nullable = orig - return ret - - def visit_column(self, delta): - keys = delta.keys() - - if 'name' in keys: - self._run_subvisit(delta, - self._visit_column_name, - start_alter=False) - - if len(set(('type', 'nullable', 'server_default')).intersection(keys)): - self._run_subvisit(delta, - self._visit_column_change, - start_alter=False) - - def _visit_column_change(self, table, column, delta): - # Oracle cannot drop a default once created, but it can set it - # to null. We'll do that if default=None - # http://forums.oracle.com/forums/message.jspa?messageID=1273234#1273234 - dropdefault_hack = (column.server_default is None \ - and 'server_default' in delta.keys()) - # Oracle apparently doesn't like it when we say "not null" if - # the column's already not null. Fudge it, so we don't need a - # new function - notnull_hack = ((not column.nullable) \ - and ('nullable' not in delta.keys())) - # We need to specify NULL if we're removing a NOT NULL - # constraint - null_hack = (column.nullable and ('nullable' in delta.keys())) - - if dropdefault_hack: - column.server_default = sa.PassiveDefault(sa.sql.null()) - if notnull_hack: - column.nullable = True - colspec = self.get_column_specification(column, - override_nullable=null_hack) - if null_hack: - colspec += ' NULL' - if notnull_hack: - column.nullable = False - if dropdefault_hack: - column.server_default = None - - self.start_alter_table(table) - self.append("MODIFY (") - self.append(colspec) - self.append(")") - - -class OracleConstraintCommon(object): - - def get_constraint_name(self, cons): - # Oracle constraints can't guess their name like other DBs - if not cons.name: - raise exceptions.NotSupportedError( - "Oracle constraint names must be explicitly stated") - return cons.name - - -class OracleConstraintGenerator(OracleConstraintCommon, - ansisql.ANSIConstraintGenerator): - pass - - -class OracleConstraintDropper(OracleConstraintCommon, - ansisql.ANSIConstraintDropper): - pass - - -class OracleDialect(ansisql.ANSIDialect): - columngenerator = OracleColumnGenerator - columndropper = OracleColumnDropper - schemachanger = OracleSchemaChanger - constraintgenerator = OracleConstraintGenerator - constraintdropper = OracleConstraintDropper diff --git a/libs/migrate/changeset/databases/postgres.py b/libs/migrate/changeset/databases/postgres.py deleted file mode 100644 index 10ea094c..00000000 --- a/libs/migrate/changeset/databases/postgres.py +++ /dev/null @@ -1,42 +0,0 @@ -""" - `PostgreSQL`_ database specific implementations of changeset classes. - - .. _`PostgreSQL`: http://www.postgresql.org/ -""" -from migrate.changeset import ansisql - -from sqlalchemy.databases import postgresql as sa_base -PGSchemaGenerator = sa_base.PGDDLCompiler - - -class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator): - """PostgreSQL column generator implementation.""" - pass - - -class PGColumnDropper(ansisql.ANSIColumnDropper): - """PostgreSQL column dropper implementation.""" - pass - - -class PGSchemaChanger(ansisql.ANSISchemaChanger): - """PostgreSQL schema changer implementation.""" - pass - - -class PGConstraintGenerator(ansisql.ANSIConstraintGenerator): - """PostgreSQL constraint generator implementation.""" - pass - - -class PGConstraintDropper(ansisql.ANSIConstraintDropper): - """PostgreSQL constaint dropper implementation.""" - pass - - -class PGDialect(ansisql.ANSIDialect): - columngenerator = PGColumnGenerator - columndropper = PGColumnDropper - schemachanger = PGSchemaChanger - constraintgenerator = PGConstraintGenerator - constraintdropper = PGConstraintDropper diff --git a/libs/migrate/changeset/databases/sqlite.py b/libs/migrate/changeset/databases/sqlite.py deleted file mode 100644 index 5ddd3f17..00000000 --- a/libs/migrate/changeset/databases/sqlite.py +++ /dev/null @@ -1,153 +0,0 @@ -""" - `SQLite`_ database specific implementations of changeset classes. - - .. _`SQLite`: http://www.sqlite.org/ -""" -from UserDict import DictMixin -from copy import copy - -from sqlalchemy.databases import sqlite as sa_base - -from migrate import exceptions -from migrate.changeset import ansisql - - -SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler - - -class SQLiteCommon(object): - - def _not_supported(self, op): - raise exceptions.NotSupportedError("SQLite does not support " - "%s; see http://www.sqlite.org/lang_altertable.html" % op) - - -class SQLiteHelper(SQLiteCommon): - - def recreate_table(self,table,column=None,delta=None): - table_name = self.preparer.format_table(table) - - # we remove all indexes so as not to have - # problems during copy and re-create - for index in table.indexes: - index.drop() - - self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name) - self.execute() - - insertion_string = self._modify_table(table, column, delta) - - table.create(bind=self.connection) - self.append(insertion_string % {'table_name': table_name}) - self.execute() - self.append('DROP TABLE migration_tmp') - self.execute() - - def visit_column(self, delta): - if isinstance(delta, DictMixin): - column = delta.result_column - table = self._to_table(delta.table) - else: - column = delta - table = self._to_table(column.table) - self.recreate_table(table,column,delta) - -class SQLiteColumnGenerator(SQLiteSchemaGenerator, - ansisql.ANSIColumnGenerator, - # at the end so we get the normal - # visit_column by default - SQLiteHelper, - SQLiteCommon - ): - """SQLite ColumnGenerator""" - - def _modify_table(self, table, column, delta): - columns = ' ,'.join(map( - self.preparer.format_column, - [c for c in table.columns if c.name!=column.name])) - return ('INSERT INTO %%(table_name)s (%(cols)s) ' - 'SELECT %(cols)s from migration_tmp')%{'cols':columns} - - def visit_column(self,column): - if column.foreign_keys: - SQLiteHelper.visit_column(self,column) - else: - super(SQLiteColumnGenerator,self).visit_column(column) - -class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper): - """SQLite ColumnDropper""" - - def _modify_table(self, table, column, delta): - - columns = ' ,'.join(map(self.preparer.format_column, table.columns)) - return 'INSERT INTO %(table_name)s SELECT ' + columns + \ - ' from migration_tmp' - - def visit_column(self,column): - # For SQLite, we *have* to remove the column here so the table - # is re-created properly. - column.remove_from_table(column.table,unset_table=False) - super(SQLiteColumnDropper,self).visit_column(column) - - -class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger): - """SQLite SchemaChanger""" - - def _modify_table(self, table, column, delta): - return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' - - def visit_index(self, index): - """Does not support ALTER INDEX""" - self._not_supported('ALTER INDEX') - - -class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon): - - def visit_migrate_primary_key_constraint(self, constraint): - tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )" - cols = ', '.join(map(self.preparer.format_column, constraint.columns)) - tname = self.preparer.format_table(constraint.table) - name = self.get_constraint_name(constraint) - msg = tmpl % (name, tname, cols) - self.append(msg) - self.execute() - - def _modify_table(self, table, column, delta): - return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' - - def visit_migrate_foreign_key_constraint(self, *p, **k): - self.recreate_table(p[0].table) - - def visit_migrate_unique_constraint(self, *p, **k): - self.recreate_table(p[0].table) - - -class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, - SQLiteCommon, - ansisql.ANSIConstraintCommon): - - def visit_migrate_primary_key_constraint(self, constraint): - tmpl = "DROP INDEX %s " - name = self.get_constraint_name(constraint) - msg = tmpl % (name) - self.append(msg) - self.execute() - - def visit_migrate_foreign_key_constraint(self, *p, **k): - self._not_supported('ALTER TABLE DROP CONSTRAINT') - - def visit_migrate_check_constraint(self, *p, **k): - self._not_supported('ALTER TABLE DROP CONSTRAINT') - - def visit_migrate_unique_constraint(self, *p, **k): - self._not_supported('ALTER TABLE DROP CONSTRAINT') - - -# TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index - -class SQLiteDialect(ansisql.ANSIDialect): - columngenerator = SQLiteColumnGenerator - columndropper = SQLiteColumnDropper - schemachanger = SQLiteSchemaChanger - constraintgenerator = SQLiteConstraintGenerator - constraintdropper = SQLiteConstraintDropper diff --git a/libs/migrate/changeset/databases/visitor.py b/libs/migrate/changeset/databases/visitor.py deleted file mode 100644 index 228b4d3f..00000000 --- a/libs/migrate/changeset/databases/visitor.py +++ /dev/null @@ -1,78 +0,0 @@ -""" - Module for visitor class mapping. -""" -import sqlalchemy as sa - -from migrate.changeset import ansisql -from migrate.changeset.databases import (sqlite, - postgres, - mysql, - oracle, - firebird) - - -# Map SA dialects to the corresponding Migrate extensions -DIALECTS = { - "default": ansisql.ANSIDialect, - "sqlite": sqlite.SQLiteDialect, - "postgres": postgres.PGDialect, - "postgresql": postgres.PGDialect, - "mysql": mysql.MySQLDialect, - "oracle": oracle.OracleDialect, - "firebird": firebird.FBDialect, -} - - -def get_engine_visitor(engine, name): - """ - Get the visitor implementation for the given database engine. - - :param engine: SQLAlchemy Engine - :param name: Name of the visitor - :type name: string - :type engine: Engine - :returns: visitor - """ - # TODO: link to supported visitors - return get_dialect_visitor(engine.dialect, name) - - -def get_dialect_visitor(sa_dialect, name): - """ - Get the visitor implementation for the given dialect. - - Finds the visitor implementation based on the dialect class and - returns and instance initialized with the given name. - - Binds dialect specific preparer to visitor. - """ - - # map sa dialect to migrate dialect and return visitor - sa_dialect_name = getattr(sa_dialect, 'name', 'default') - migrate_dialect_cls = DIALECTS[sa_dialect_name] - visitor = getattr(migrate_dialect_cls, name) - - # bind preparer - visitor.preparer = sa_dialect.preparer(sa_dialect) - - return visitor - -def run_single_visitor(engine, visitorcallable, element, - connection=None, **kwargs): - """Taken from :meth:`sqlalchemy.engine.base.Engine._run_single_visitor` - with support for migrate visitors. - """ - if connection is None: - conn = engine.contextual_connect(close_with_result=False) - else: - conn = connection - visitor = visitorcallable(engine.dialect, conn) - try: - if hasattr(element, '__migrate_visit_name__'): - fn = getattr(visitor, 'visit_' + element.__migrate_visit_name__) - else: - fn = getattr(visitor, 'visit_' + element.__visit_name__) - fn(element, **kwargs) - finally: - if connection is None: - conn.close() diff --git a/libs/migrate/changeset/schema.py b/libs/migrate/changeset/schema.py deleted file mode 100644 index c467cc53..00000000 --- a/libs/migrate/changeset/schema.py +++ /dev/null @@ -1,655 +0,0 @@ -""" - Schema module providing common schema operations. -""" -import warnings - -from UserDict import DictMixin - -import sqlalchemy - -from sqlalchemy.schema import ForeignKeyConstraint -from sqlalchemy.schema import UniqueConstraint - -from migrate.exceptions import * -from migrate.changeset import SQLA_07 -from migrate.changeset.databases.visitor import (get_engine_visitor, - run_single_visitor) - - -__all__ = [ - 'create_column', - 'drop_column', - 'alter_column', - 'rename_table', - 'rename_index', - 'ChangesetTable', - 'ChangesetColumn', - 'ChangesetIndex', - 'ChangesetDefaultClause', - 'ColumnDelta', -] - -def create_column(column, table=None, *p, **kw): - """Create a column, given the table. - - API to :meth:`ChangesetColumn.create`. - """ - if table is not None: - return table.create_column(column, *p, **kw) - return column.create(*p, **kw) - - -def drop_column(column, table=None, *p, **kw): - """Drop a column, given the table. - - API to :meth:`ChangesetColumn.drop`. - """ - if table is not None: - return table.drop_column(column, *p, **kw) - return column.drop(*p, **kw) - - -def rename_table(table, name, engine=None, **kw): - """Rename a table. - - If Table instance is given, engine is not used. - - API to :meth:`ChangesetTable.rename`. - - :param table: Table to be renamed. - :param name: New name for Table. - :param engine: Engine instance. - :type table: string or Table instance - :type name: string - :type engine: obj - """ - table = _to_table(table, engine) - table.rename(name, **kw) - - -def rename_index(index, name, table=None, engine=None, **kw): - """Rename an index. - - If Index instance is given, - table and engine are not used. - - API to :meth:`ChangesetIndex.rename`. - - :param index: Index to be renamed. - :param name: New name for index. - :param table: Table to which Index is reffered. - :param engine: Engine instance. - :type index: string or Index instance - :type name: string - :type table: string or Table instance - :type engine: obj - """ - index = _to_index(index, table, engine) - index.rename(name, **kw) - - -def alter_column(*p, **k): - """Alter a column. - - This is a helper function that creates a :class:`ColumnDelta` and - runs it. - - :argument column: - The name of the column to be altered or a - :class:`ChangesetColumn` column representing it. - - :param table: - A :class:`~sqlalchemy.schema.Table` or table name to - for the table where the column will be changed. - - :param engine: - The :class:`~sqlalchemy.engine.base.Engine` to use for table - reflection and schema alterations. - - :returns: A :class:`ColumnDelta` instance representing the change. - - - """ - - if 'table' not in k and isinstance(p[0], sqlalchemy.Column): - k['table'] = p[0].table - if 'engine' not in k: - k['engine'] = k['table'].bind - - # deprecation - if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): - warnings.warn( - "Passing a Column object to alter_column is deprecated." - " Just pass in keyword parameters instead.", - MigrateDeprecationWarning - ) - engine = k['engine'] - - # enough tests seem to break when metadata is always altered - # that this crutch has to be left in until they can be sorted - # out - k['alter_metadata']=True - - delta = ColumnDelta(*p, **k) - - visitorcallable = get_engine_visitor(engine, 'schemachanger') - engine._run_visitor(visitorcallable, delta) - - return delta - - -def _to_table(table, engine=None): - """Return if instance of Table, else construct new with metadata""" - if isinstance(table, sqlalchemy.Table): - return table - - # Given: table name, maybe an engine - meta = sqlalchemy.MetaData() - if engine is not None: - meta.bind = engine - return sqlalchemy.Table(table, meta) - - -def _to_index(index, table=None, engine=None): - """Return if instance of Index, else construct new with metadata""" - if isinstance(index, sqlalchemy.Index): - return index - - # Given: index name; table name required - table = _to_table(table, engine) - ret = sqlalchemy.Index(index) - ret.table = table - return ret - - -class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem): - """Extracts the differences between two columns/column-parameters - - May receive parameters arranged in several different ways: - - * **current_column, new_column, \*p, \*\*kw** - Additional parameters can be specified to override column - differences. - - * **current_column, \*p, \*\*kw** - Additional parameters alter current_column. Table name is extracted - from current_column object. - Name is changed to current_column.name from current_name, - if current_name is specified. - - * **current_col_name, \*p, \*\*kw** - Table kw must specified. - - :param table: Table at which current Column should be bound to.\ - If table name is given, reflection will be used. - :type table: string or Table instance - - :param metadata: A :class:`MetaData` instance to store - reflected table names - - :param engine: When reflecting tables, either engine or metadata must \ - be specified to acquire engine object. - :type engine: :class:`Engine` instance - :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \ - `result_column` through :func:`dict` alike object. - - * :class:`ColumnDelta`.result_column is altered column with new attributes - - * :class:`ColumnDelta`.current_name is current name of column in db - - - """ - - # Column attributes that can be altered - diff_keys = ('name', 'type', 'primary_key', 'nullable', - 'server_onupdate', 'server_default', 'autoincrement') - diffs = dict() - __visit_name__ = 'column' - - def __init__(self, *p, **kw): - # 'alter_metadata' is not a public api. It exists purely - # as a crutch until the tests that fail when 'alter_metadata' - # behaviour always happens can be sorted out - self.alter_metadata = kw.pop("alter_metadata", False) - - self.meta = kw.pop("metadata", None) - self.engine = kw.pop("engine", None) - - # Things are initialized differently depending on how many column - # parameters are given. Figure out how many and call the appropriate - # method. - if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column): - # At least one column specified - if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): - # Two columns specified - diffs = self.compare_2_columns(*p, **kw) - else: - # Exactly one column specified - diffs = self.compare_1_column(*p, **kw) - else: - # Zero columns specified - if not len(p) or not isinstance(p[0], basestring): - raise ValueError("First argument must be column name") - diffs = self.compare_parameters(*p, **kw) - - self.apply_diffs(diffs) - - def __repr__(self): - return '' % ( - self.alter_metadata, - super(ColumnDelta, self).__repr__() - ) - - def __getitem__(self, key): - if key not in self.keys(): - raise KeyError("No such diff key, available: %s" % self.diffs ) - return getattr(self.result_column, key) - - def __setitem__(self, key, value): - if key not in self.keys(): - raise KeyError("No such diff key, available: %s" % self.diffs ) - setattr(self.result_column, key, value) - - def __delitem__(self, key): - raise NotImplementedError - - def keys(self): - return self.diffs.keys() - - def compare_parameters(self, current_name, *p, **k): - """Compares Column objects with reflection""" - self.table = k.pop('table') - self.result_column = self._table.c.get(current_name) - if len(p): - k = self._extract_parameters(p, k, self.result_column) - return k - - def compare_1_column(self, col, *p, **k): - """Compares one Column object""" - self.table = k.pop('table', None) - if self.table is None: - self.table = col.table - self.result_column = col - if len(p): - k = self._extract_parameters(p, k, self.result_column) - return k - - def compare_2_columns(self, old_col, new_col, *p, **k): - """Compares two Column objects""" - self.process_column(new_col) - self.table = k.pop('table', None) - # we cannot use bool() on table in SA06 - if self.table is None: - self.table = old_col.table - if self.table is None: - new_col.table - self.result_column = old_col - - # set differences - # leave out some stuff for later comp - for key in (set(self.diff_keys) - set(('type',))): - val = getattr(new_col, key, None) - if getattr(self.result_column, key, None) != val: - k.setdefault(key, val) - - # inspect types - if not self.are_column_types_eq(self.result_column.type, new_col.type): - k.setdefault('type', new_col.type) - - if len(p): - k = self._extract_parameters(p, k, self.result_column) - return k - - def apply_diffs(self, diffs): - """Populate dict and column object with new values""" - self.diffs = diffs - for key in self.diff_keys: - if key in diffs: - setattr(self.result_column, key, diffs[key]) - - self.process_column(self.result_column) - - # create an instance of class type if not yet - if 'type' in diffs and callable(self.result_column.type): - self.result_column.type = self.result_column.type() - - # add column to the table - if self.table is not None and self.alter_metadata: - self.result_column.add_to_table(self.table) - - def are_column_types_eq(self, old_type, new_type): - """Compares two types to be equal""" - ret = old_type.__class__ == new_type.__class__ - - # String length is a special case - if ret and isinstance(new_type, sqlalchemy.types.String): - ret = (getattr(old_type, 'length', None) == \ - getattr(new_type, 'length', None)) - return ret - - def _extract_parameters(self, p, k, column): - """Extracts data from p and modifies diffs""" - p = list(p) - while len(p): - if isinstance(p[0], basestring): - k.setdefault('name', p.pop(0)) - elif isinstance(p[0], sqlalchemy.types.AbstractType): - k.setdefault('type', p.pop(0)) - elif callable(p[0]): - p[0] = p[0]() - else: - break - - if len(p): - new_col = column.copy_fixed() - new_col._init_items(*p) - k = self.compare_2_columns(column, new_col, **k) - return k - - def process_column(self, column): - """Processes default values for column""" - # XXX: this is a snippet from SA processing of positional parameters - toinit = list() - - if column.server_default is not None: - if isinstance(column.server_default, sqlalchemy.FetchedValue): - toinit.append(column.server_default) - else: - toinit.append(sqlalchemy.DefaultClause(column.server_default)) - if column.server_onupdate is not None: - if isinstance(column.server_onupdate, FetchedValue): - toinit.append(column.server_default) - else: - toinit.append(sqlalchemy.DefaultClause(column.server_onupdate, - for_update=True)) - if toinit: - column._init_items(*toinit) - - def _get_table(self): - return getattr(self, '_table', None) - - def _set_table(self, table): - if isinstance(table, basestring): - if self.alter_metadata: - if not self.meta: - raise ValueError("metadata must be specified for table" - " reflection when using alter_metadata") - meta = self.meta - if self.engine: - meta.bind = self.engine - else: - if not self.engine and not self.meta: - raise ValueError("engine or metadata must be specified" - " to reflect tables") - if not self.engine: - self.engine = self.meta.bind - meta = sqlalchemy.MetaData(bind=self.engine) - self._table = sqlalchemy.Table(table, meta, autoload=True) - elif isinstance(table, sqlalchemy.Table): - self._table = table - if not self.alter_metadata: - self._table.meta = sqlalchemy.MetaData(bind=self._table.bind) - def _get_result_column(self): - return getattr(self, '_result_column', None) - - def _set_result_column(self, column): - """Set Column to Table based on alter_metadata evaluation.""" - self.process_column(column) - if not hasattr(self, 'current_name'): - self.current_name = column.name - if self.alter_metadata: - self._result_column = column - else: - self._result_column = column.copy_fixed() - - table = property(_get_table, _set_table) - result_column = property(_get_result_column, _set_result_column) - - -class ChangesetTable(object): - """Changeset extensions to SQLAlchemy tables.""" - - def create_column(self, column, *p, **kw): - """Creates a column. - - The column parameter may be a column definition or the name of - a column in this table. - - API to :meth:`ChangesetColumn.create` - - :param column: Column to be created - :type column: Column instance or string - """ - if not isinstance(column, sqlalchemy.Column): - # It's a column name - column = getattr(self.c, str(column)) - column.create(table=self, *p, **kw) - - def drop_column(self, column, *p, **kw): - """Drop a column, given its name or definition. - - API to :meth:`ChangesetColumn.drop` - - :param column: Column to be droped - :type column: Column instance or string - """ - if not isinstance(column, sqlalchemy.Column): - # It's a column name - try: - column = getattr(self.c, str(column)) - except AttributeError: - # That column isn't part of the table. We don't need - # its entire definition to drop the column, just its - # name, so create a dummy column with the same name. - column = sqlalchemy.Column(str(column), sqlalchemy.Integer()) - column.drop(table=self, *p, **kw) - - def rename(self, name, connection=None, **kwargs): - """Rename this table. - - :param name: New name of the table. - :type name: string - :param connection: reuse connection istead of creating new one. - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - """ - engine = self.bind - self.new_name = name - visitorcallable = get_engine_visitor(engine, 'schemachanger') - run_single_visitor(engine, visitorcallable, self, connection, **kwargs) - - # Fix metadata registration - self.name = name - self.deregister() - self._set_parent(self.metadata) - - def _meta_key(self): - """Get the meta key for this table.""" - return sqlalchemy.schema._get_table_key(self.name, self.schema) - - def deregister(self): - """Remove this table from its metadata""" - if SQLA_07: - self.metadata._remove_table(self.name, self.schema) - else: - key = self._meta_key() - meta = self.metadata - if key in meta.tables: - del meta.tables[key] - - -class ChangesetColumn(object): - """Changeset extensions to SQLAlchemy columns.""" - - def alter(self, *p, **k): - """Makes a call to :func:`alter_column` for the column this - method is called on. - """ - if 'table' not in k: - k['table'] = self.table - if 'engine' not in k: - k['engine'] = k['table'].bind - return alter_column(self, *p, **k) - - def create(self, table=None, index_name=None, unique_name=None, - primary_key_name=None, populate_default=True, connection=None, **kwargs): - """Create this column in the database. - - Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, - for most databases. - - :param table: Table instance to create on. - :param index_name: Creates :class:`ChangesetIndex` on this column. - :param unique_name: Creates :class:\ -`~migrate.changeset.constraint.UniqueConstraint` on this column. - :param primary_key_name: Creates :class:\ -`~migrate.changeset.constraint.PrimaryKeyConstraint` on this column. - :param populate_default: If True, created column will be \ -populated with defaults - :param connection: reuse connection istead of creating new one. - :type table: Table instance - :type index_name: string - :type unique_name: string - :type primary_key_name: string - :type populate_default: bool - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - - :returns: self - """ - self.populate_default = populate_default - self.index_name = index_name - self.unique_name = unique_name - self.primary_key_name = primary_key_name - for cons in ('index_name', 'unique_name', 'primary_key_name'): - self._check_sanity_constraints(cons) - - self.add_to_table(table) - engine = self.table.bind - visitorcallable = get_engine_visitor(engine, 'columngenerator') - engine._run_visitor(visitorcallable, self, connection, **kwargs) - - # TODO: reuse existing connection - if self.populate_default and self.default is not None: - stmt = table.update().values({self: engine._execute_default(self.default)}) - engine.execute(stmt) - - return self - - def drop(self, table=None, connection=None, **kwargs): - """Drop this column from the database, leaving its table intact. - - ``ALTER TABLE DROP COLUMN``, for most databases. - - :param connection: reuse connection istead of creating new one. - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - """ - if table is not None: - self.table = table - engine = self.table.bind - visitorcallable = get_engine_visitor(engine, 'columndropper') - engine._run_visitor(visitorcallable, self, connection, **kwargs) - self.remove_from_table(self.table, unset_table=False) - self.table = None - return self - - def add_to_table(self, table): - if table is not None and self.table is None: - if SQLA_07: - table.append_column(self) - else: - self._set_parent(table) - - def _col_name_in_constraint(self,cons,name): - return False - - def remove_from_table(self, table, unset_table=True): - # TODO: remove primary keys, constraints, etc - if unset_table: - self.table = None - - to_drop = set() - for index in table.indexes: - columns = [] - for col in index.columns: - if col.name!=self.name: - columns.append(col) - if columns: - index.columns=columns - else: - to_drop.add(index) - table.indexes = table.indexes - to_drop - - to_drop = set() - for cons in table.constraints: - # TODO: deal with other types of constraint - if isinstance(cons,(ForeignKeyConstraint, - UniqueConstraint)): - for col_name in cons.columns: - if not isinstance(col_name,basestring): - col_name = col_name.name - if self.name==col_name: - to_drop.add(cons) - table.constraints = table.constraints - to_drop - - if table.c.contains_column(self): - if SQLA_07: - table._columns.remove(self) - else: - table.c.remove(self) - - # TODO: this is fixed in 0.6 - def copy_fixed(self, **kw): - """Create a copy of this ``Column``, with all attributes.""" - return sqlalchemy.Column(self.name, self.type, self.default, - key=self.key, - primary_key=self.primary_key, - nullable=self.nullable, - quote=self.quote, - index=self.index, - unique=self.unique, - onupdate=self.onupdate, - autoincrement=self.autoincrement, - server_default=self.server_default, - server_onupdate=self.server_onupdate, - *[c.copy(**kw) for c in self.constraints]) - - def _check_sanity_constraints(self, name): - """Check if constraints names are correct""" - obj = getattr(self, name) - if (getattr(self, name[:-5]) and not obj): - raise InvalidConstraintError("Column.create() accepts index_name," - " primary_key_name and unique_name to generate constraints") - if not isinstance(obj, basestring) and obj is not None: - raise InvalidConstraintError( - "%s argument for column must be constraint name" % name) - - -class ChangesetIndex(object): - """Changeset extensions to SQLAlchemy Indexes.""" - - __visit_name__ = 'index' - - def rename(self, name, connection=None, **kwargs): - """Change the name of an index. - - :param name: New name of the Index. - :type name: string - :param connection: reuse connection istead of creating new one. - :type connection: :class:`sqlalchemy.engine.base.Connection` instance - """ - engine = self.table.bind - self.new_name = name - visitorcallable = get_engine_visitor(engine, 'schemachanger') - engine._run_visitor(visitorcallable, self, connection, **kwargs) - self.name = name - - -class ChangesetDefaultClause(object): - """Implements comparison between :class:`DefaultClause` instances""" - - def __eq__(self, other): - if isinstance(other, self.__class__): - if self.arg == other.arg: - return True - - def __ne__(self, other): - return not self.__eq__(other) diff --git a/libs/migrate/exceptions.py b/libs/migrate/exceptions.py deleted file mode 100644 index cb8c4094..00000000 --- a/libs/migrate/exceptions.py +++ /dev/null @@ -1,87 +0,0 @@ -""" - Provide exception classes for :mod:`migrate` -""" - - -class Error(Exception): - """Error base class.""" - - -class ApiError(Error): - """Base class for API errors.""" - - -class KnownError(ApiError): - """A known error condition.""" - - -class UsageError(ApiError): - """A known error condition where help should be displayed.""" - - -class ControlledSchemaError(Error): - """Base class for controlled schema errors.""" - - -class InvalidVersionError(ControlledSchemaError): - """Invalid version number.""" - - -class DatabaseNotControlledError(ControlledSchemaError): - """Database should be under version control, but it's not.""" - - -class DatabaseAlreadyControlledError(ControlledSchemaError): - """Database shouldn't be under version control, but it is""" - - -class WrongRepositoryError(ControlledSchemaError): - """This database is under version control by another repository.""" - - -class NoSuchTableError(ControlledSchemaError): - """The table does not exist.""" - - -class PathError(Error): - """Base class for path errors.""" - - -class PathNotFoundError(PathError): - """A path with no file was required; found a file.""" - - -class PathFoundError(PathError): - """A path with a file was required; found no file.""" - - -class RepositoryError(Error): - """Base class for repository errors.""" - - -class InvalidRepositoryError(RepositoryError): - """Invalid repository error.""" - - -class ScriptError(Error): - """Base class for script errors.""" - - -class InvalidScriptError(ScriptError): - """Invalid script error.""" - - -class InvalidVersionError(Error): - """Invalid version error.""" - -# migrate.changeset - -class NotSupportedError(Error): - """Not supported error""" - - -class InvalidConstraintError(Error): - """Invalid constraint error""" - -class MigrateDeprecationWarning(DeprecationWarning): - """Warning for deprecated features in Migrate""" diff --git a/libs/migrate/versioning/__init__.py b/libs/migrate/versioning/__init__.py deleted file mode 100644 index 8b5a7363..00000000 --- a/libs/migrate/versioning/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - This package provides functionality to create and manage - repositories of database schema changesets and to apply these - changesets to databases. -""" diff --git a/libs/migrate/versioning/api.py b/libs/migrate/versioning/api.py deleted file mode 100644 index 570dc086..00000000 --- a/libs/migrate/versioning/api.py +++ /dev/null @@ -1,384 +0,0 @@ -""" - This module provides an external API to the versioning system. - - .. versionchanged:: 0.6.0 - :func:`migrate.versioning.api.test` and schema diff functions - changed order of positional arguments so all accept `url` and `repository` - as first arguments. - - .. versionchanged:: 0.5.4 - ``--preview_sql`` displays source file when using SQL scripts. - If Python script is used, it runs the action with mocked engine and - returns captured SQL statements. - - .. versionchanged:: 0.5.4 - Deprecated ``--echo`` parameter in favour of new - :func:`migrate.versioning.util.construct_engine` behavior. -""" - -# Dear migrate developers, -# -# please do not comment this module using sphinx syntax because its -# docstrings are presented as user help and most users cannot -# interpret sphinx annotated ReStructuredText. -# -# Thanks, -# Jan Dittberner - -import sys -import inspect -import logging - -from migrate import exceptions -from migrate.versioning import (repository, schema, version, - script as script_) # command name conflict -from migrate.versioning.util import catch_known_errors, with_engine - - -log = logging.getLogger(__name__) -command_desc = { - 'help': 'displays help on a given command', - 'create': 'create an empty repository at the specified path', - 'script': 'create an empty change Python script', - 'script_sql': 'create empty change SQL scripts for given database', - 'version': 'display the latest version available in a repository', - 'db_version': 'show the current version of the repository under version control', - 'source': 'display the Python code for a particular version in this repository', - 'version_control': 'mark a database as under this repository\'s version control', - 'upgrade': 'upgrade a database to a later version', - 'downgrade': 'downgrade a database to an earlier version', - 'drop_version_control': 'removes version control from a database', - 'manage': 'creates a Python script that runs Migrate with a set of default values', - 'test': 'performs the upgrade and downgrade command on the given database', - 'compare_model_to_db': 'compare MetaData against the current database state', - 'create_model': 'dump the current database as a Python model to stdout', - 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData', - 'update_db_from_model': 'modify the database to match the structure of the current MetaData', -} -__all__ = command_desc.keys() - -Repository = repository.Repository -ControlledSchema = schema.ControlledSchema -VerNum = version.VerNum -PythonScript = script_.PythonScript -SqlScript = script_.SqlScript - - -# deprecated -def help(cmd=None, **opts): - """%prog help COMMAND - - Displays help on a given command. - """ - if cmd is None: - raise exceptions.UsageError(None) - try: - func = globals()[cmd] - except: - raise exceptions.UsageError( - "'%s' isn't a valid command. Try 'help COMMAND'" % cmd) - ret = func.__doc__ - if sys.argv[0]: - ret = ret.replace('%prog', sys.argv[0]) - return ret - -@catch_known_errors -def create(repository, name, **opts): - """%prog create REPOSITORY_PATH NAME [--table=TABLE] - - Create an empty repository at the specified path. - - You can specify the version_table to be used; by default, it is - 'migrate_version'. This table is created in all version-controlled - databases. - """ - repo_path = Repository.create(repository, name, **opts) - - -@catch_known_errors -def script(description, repository, **opts): - """%prog script DESCRIPTION REPOSITORY_PATH - - Create an empty change script using the next unused version number - appended with the given description. - - For instance, manage.py script "Add initial tables" creates: - repository/versions/001_Add_initial_tables.py - """ - repo = Repository(repository) - repo.create_script(description, **opts) - - -@catch_known_errors -def script_sql(database, description, repository, **opts): - """%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH - - Create empty change SQL scripts for given DATABASE, where DATABASE - is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.) - or generic ('default'). - - For instance, manage.py script_sql postgresql description creates: - repository/versions/001_description_postgresql_upgrade.sql and - repository/versions/001_description_postgresql_downgrade.sql - """ - repo = Repository(repository) - repo.create_script_sql(database, description, **opts) - - -def version(repository, **opts): - """%prog version REPOSITORY_PATH - - Display the latest version available in a repository. - """ - repo = Repository(repository) - return repo.latest - - -@with_engine -def db_version(url, repository, **opts): - """%prog db_version URL REPOSITORY_PATH - - Show the current version of the repository with the given - connection string, under version control of the specified - repository. - - The url should be any valid SQLAlchemy connection string. - """ - engine = opts.pop('engine') - schema = ControlledSchema(engine, repository) - return schema.version - - -def source(version, dest=None, repository=None, **opts): - """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH - - Display the Python code for a particular version in this - repository. Save it to the file at DESTINATION or, if omitted, - send to stdout. - """ - if repository is None: - raise exceptions.UsageError("A repository must be specified") - repo = Repository(repository) - ret = repo.version(version).script().source() - if dest is not None: - dest = open(dest, 'w') - dest.write(ret) - dest.close() - ret = None - return ret - - -def upgrade(url, repository, version=None, **opts): - """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql] - - Upgrade a database to a later version. - - This runs the upgrade() function defined in your change scripts. - - By default, the database is updated to the latest available - version. You may specify a version instead, if you wish. - - You may preview the Python or SQL code to be executed, rather than - actually executing it, using the appropriate 'preview' option. - """ - err = "Cannot upgrade a database of version %s to version %s. "\ - "Try 'downgrade' instead." - return _migrate(url, repository, version, upgrade=True, err=err, **opts) - - -def downgrade(url, repository, version, **opts): - """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql] - - Downgrade a database to an earlier version. - - This is the reverse of upgrade; this runs the downgrade() function - defined in your change scripts. - - You may preview the Python or SQL code to be executed, rather than - actually executing it, using the appropriate 'preview' option. - """ - err = "Cannot downgrade a database of version %s to version %s. "\ - "Try 'upgrade' instead." - return _migrate(url, repository, version, upgrade=False, err=err, **opts) - -@with_engine -def test(url, repository, **opts): - """%prog test URL REPOSITORY_PATH [VERSION] - - Performs the upgrade and downgrade option on the given - database. This is not a real test and may leave the database in a - bad state. You should therefore better run the test on a copy of - your database. - """ - engine = opts.pop('engine') - repos = Repository(repository) - - # Upgrade - log.info("Upgrading...") - script = repos.version(None).script(engine.name, 'upgrade') - script.run(engine, 1) - log.info("done") - - log.info("Downgrading...") - script = repos.version(None).script(engine.name, 'downgrade') - script.run(engine, -1) - log.info("done") - log.info("Success") - - -@with_engine -def version_control(url, repository, version=None, **opts): - """%prog version_control URL REPOSITORY_PATH [VERSION] - - Mark a database as under this repository's version control. - - Once a database is under version control, schema changes should - only be done via change scripts in this repository. - - This creates the table version_table in the database. - - The url should be any valid SQLAlchemy connection string. - - By default, the database begins at version 0 and is assumed to be - empty. If the database is not empty, you may specify a version at - which to begin instead. No attempt is made to verify this - version's correctness - the database schema is expected to be - identical to what it would be if the database were created from - scratch. - """ - engine = opts.pop('engine') - ControlledSchema.create(engine, repository, version) - - -@with_engine -def drop_version_control(url, repository, **opts): - """%prog drop_version_control URL REPOSITORY_PATH - - Removes version control from a database. - """ - engine = opts.pop('engine') - schema = ControlledSchema(engine, repository) - schema.drop() - - -def manage(file, **opts): - """%prog manage FILENAME [VARIABLES...] - - Creates a script that runs Migrate with a set of default values. - - For example:: - - %prog manage manage.py --repository=/path/to/repository \ ---url=sqlite:///project.db - - would create the script manage.py. The following two commands - would then have exactly the same results:: - - python manage.py version - %prog version --repository=/path/to/repository - """ - Repository.create_manage_file(file, **opts) - - -@with_engine -def compare_model_to_db(url, repository, model, **opts): - """%prog compare_model_to_db URL REPOSITORY_PATH MODEL - - Compare the current model (assumed to be a module level variable - of type sqlalchemy.MetaData) against the current database. - - NOTE: This is EXPERIMENTAL. - """ # TODO: get rid of EXPERIMENTAL label - engine = opts.pop('engine') - return ControlledSchema.compare_model_to_db(engine, model, repository) - - -@with_engine -def create_model(url, repository, **opts): - """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True] - - Dump the current database as a Python model to stdout. - - NOTE: This is EXPERIMENTAL. - """ # TODO: get rid of EXPERIMENTAL label - engine = opts.pop('engine') - declarative = opts.get('declarative', False) - return ControlledSchema.create_model(engine, repository, declarative) - - -@catch_known_errors -@with_engine -def make_update_script_for_model(url, repository, oldmodel, model, **opts): - """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH - - Create a script changing the old Python model to the new (current) - Python model, sending to stdout. - - NOTE: This is EXPERIMENTAL. - """ # TODO: get rid of EXPERIMENTAL label - engine = opts.pop('engine') - return PythonScript.make_update_script_for_model( - engine, oldmodel, model, repository, **opts) - - -@with_engine -def update_db_from_model(url, repository, model, **opts): - """%prog update_db_from_model URL REPOSITORY_PATH MODEL - - Modify the database to match the structure of the current Python - model. This also sets the db_version number to the latest in the - repository. - - NOTE: This is EXPERIMENTAL. - """ # TODO: get rid of EXPERIMENTAL label - engine = opts.pop('engine') - schema = ControlledSchema(engine, repository) - schema.update_db_from_model(model) - -@with_engine -def _migrate(url, repository, version, upgrade, err, **opts): - engine = opts.pop('engine') - url = str(engine.url) - schema = ControlledSchema(engine, repository) - version = _migrate_version(schema, version, upgrade, err) - - changeset = schema.changeset(version) - for ver, change in changeset: - nextver = ver + changeset.step - log.info('%s -> %s... ', ver, nextver) - - if opts.get('preview_sql'): - if isinstance(change, PythonScript): - log.info(change.preview_sql(url, changeset.step, **opts)) - elif isinstance(change, SqlScript): - log.info(change.source()) - - elif opts.get('preview_py'): - if not isinstance(change, PythonScript): - raise exceptions.UsageError("Python source can be only displayed" - " for python migration files") - source_ver = max(ver, nextver) - module = schema.repository.version(source_ver).script().module - funcname = upgrade and "upgrade" or "downgrade" - func = getattr(module, funcname) - log.info(inspect.getsource(func)) - else: - schema.runchange(ver, change, changeset.step) - log.info('done') - - -def _migrate_version(schema, version, upgrade, err): - if version is None: - return version - # Version is specified: ensure we're upgrading in the right direction - # (current version < target version for upgrading; reverse for down) - version = VerNum(version) - cur = schema.version - if upgrade is not None: - if upgrade: - direction = cur <= version - else: - direction = cur >= version - if not direction: - raise exceptions.KnownError(err % (cur, version)) - return version diff --git a/libs/migrate/versioning/cfgparse.py b/libs/migrate/versioning/cfgparse.py deleted file mode 100644 index ff27d672..00000000 --- a/libs/migrate/versioning/cfgparse.py +++ /dev/null @@ -1,27 +0,0 @@ -""" - Configuration parser module. -""" - -from ConfigParser import ConfigParser - -from migrate.versioning.config import * -from migrate.versioning import pathed - - -class Parser(ConfigParser): - """A project configuration file.""" - - def to_dict(self, sections=None): - """It's easier to access config values like dictionaries""" - return self._sections - - -class Config(pathed.Pathed, Parser): - """Configuration class.""" - - def __init__(self, path, *p, **k): - """Confirm the config file exists; read it.""" - self.require_found(path) - pathed.Pathed.__init__(self, path) - Parser.__init__(self, *p, **k) - self.read(path) diff --git a/libs/migrate/versioning/config.py b/libs/migrate/versioning/config.py deleted file mode 100644 index 2429fd8b..00000000 --- a/libs/migrate/versioning/config.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -from sqlalchemy.util import OrderedDict - - -__all__ = ['databases', 'operations'] - -databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql', 'firebird') - -# Map operation names to function names -operations = OrderedDict() -operations['upgrade'] = 'upgrade' -operations['downgrade'] = 'downgrade' diff --git a/libs/migrate/versioning/genmodel.py b/libs/migrate/versioning/genmodel.py deleted file mode 100644 index 85df6276..00000000 --- a/libs/migrate/versioning/genmodel.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -Code to generate a Python model from a database or differences -between a model and database. - -Some of this is borrowed heavily from the AutoCode project at: -http://code.google.com/p/sqlautocode/ -""" - -import sys -import logging - -import sqlalchemy - -import migrate -import migrate.changeset - - -log = logging.getLogger(__name__) -HEADER = """ -## File autogenerated by genmodel.py - -from sqlalchemy import * -meta = MetaData() -""" - -DECLARATIVE_HEADER = """ -## File autogenerated by genmodel.py - -from sqlalchemy import * -from sqlalchemy.ext import declarative - -Base = declarative.declarative_base() -""" - - -class ModelGenerator(object): - """Various transformations from an A, B diff. - - In the implementation, A tends to be called the model and B - the database (although this is not true of all diffs). - The diff is directionless, but transformations apply the diff - in a particular direction, described in the method name. - """ - - def __init__(self, diff, engine, declarative=False): - self.diff = diff - self.engine = engine - self.declarative = declarative - - def column_repr(self, col): - kwarg = [] - if col.key != col.name: - kwarg.append('key') - if col.primary_key: - col.primary_key = True # otherwise it dumps it as 1 - kwarg.append('primary_key') - if not col.nullable: - kwarg.append('nullable') - if col.onupdate: - kwarg.append('onupdate') - if col.default: - if col.primary_key: - # I found that PostgreSQL automatically creates a - # default value for the sequence, but let's not show - # that. - pass - else: - kwarg.append('default') - args = ['%s=%r' % (k, getattr(col, k)) for k in kwarg] - - # crs: not sure if this is good idea, but it gets rid of extra - # u'' - name = col.name.encode('utf8') - - type_ = col.type - for cls in col.type.__class__.__mro__: - if cls.__module__ == 'sqlalchemy.types' and \ - not cls.__name__.isupper(): - if cls is not type_.__class__: - type_ = cls() - break - - type_repr = repr(type_) - if type_repr.endswith('()'): - type_repr = type_repr[:-2] - - constraints = [repr(cn) for cn in col.constraints] - - data = { - 'name': name, - 'commonStuff': ', '.join([type_repr] + constraints + args), - } - - if self.declarative: - return """%(name)s = Column(%(commonStuff)s)""" % data - else: - return """Column(%(name)r, %(commonStuff)s)""" % data - - def _getTableDefn(self, table, metaName='meta'): - out = [] - tableName = table.name - if self.declarative: - out.append("class %(table)s(Base):" % {'table': tableName}) - out.append(" __tablename__ = '%(table)s'\n" % - {'table': tableName}) - for col in table.columns: - out.append(" %s" % self.column_repr(col)) - out.append('\n') - else: - out.append("%(table)s = Table('%(table)s', %(meta)s," % - {'table': tableName, 'meta': metaName}) - for col in table.columns: - out.append(" %s," % self.column_repr(col)) - out.append(")\n") - return out - - def _get_tables(self,missingA=False,missingB=False,modified=False): - to_process = [] - for bool_,names,metadata in ( - (missingA,self.diff.tables_missing_from_A,self.diff.metadataB), - (missingB,self.diff.tables_missing_from_B,self.diff.metadataA), - (modified,self.diff.tables_different,self.diff.metadataA), - ): - if bool_: - for name in names: - yield metadata.tables.get(name) - - def genBDefinition(self): - """Generates the source code for a definition of B. - - Assumes a diff where A is empty. - - Was: toPython. Assume database (B) is current and model (A) is empty. - """ - - out = [] - if self.declarative: - out.append(DECLARATIVE_HEADER) - else: - out.append(HEADER) - out.append("") - for table in self._get_tables(missingA=True): - out.extend(self._getTableDefn(table)) - return '\n'.join(out) - - def genB2AMigration(self, indent=' '): - '''Generate a migration from B to A. - - Was: toUpgradeDowngradePython - Assume model (A) is most current and database (B) is out-of-date. - ''' - - decls = ['from migrate.changeset import schema', - 'pre_meta = MetaData()', - 'post_meta = MetaData()', - ] - upgradeCommands = ['pre_meta.bind = migrate_engine', - 'post_meta.bind = migrate_engine'] - downgradeCommands = list(upgradeCommands) - - for tn in self.diff.tables_missing_from_A: - pre_table = self.diff.metadataB.tables[tn] - decls.extend(self._getTableDefn(pre_table, metaName='pre_meta')) - upgradeCommands.append( - "pre_meta.tables[%(table)r].drop()" % {'table': tn}) - downgradeCommands.append( - "pre_meta.tables[%(table)r].create()" % {'table': tn}) - - for tn in self.diff.tables_missing_from_B: - post_table = self.diff.metadataA.tables[tn] - decls.extend(self._getTableDefn(post_table, metaName='post_meta')) - upgradeCommands.append( - "post_meta.tables[%(table)r].create()" % {'table': tn}) - downgradeCommands.append( - "post_meta.tables[%(table)r].drop()" % {'table': tn}) - - for (tn, td) in self.diff.tables_different.iteritems(): - if td.columns_missing_from_A or td.columns_different: - pre_table = self.diff.metadataB.tables[tn] - decls.extend(self._getTableDefn( - pre_table, metaName='pre_meta')) - if td.columns_missing_from_B or td.columns_different: - post_table = self.diff.metadataA.tables[tn] - decls.extend(self._getTableDefn( - post_table, metaName='post_meta')) - - for col in td.columns_missing_from_A: - upgradeCommands.append( - 'pre_meta.tables[%r].columns[%r].drop()' % (tn, col)) - downgradeCommands.append( - 'pre_meta.tables[%r].columns[%r].create()' % (tn, col)) - for col in td.columns_missing_from_B: - upgradeCommands.append( - 'post_meta.tables[%r].columns[%r].create()' % (tn, col)) - downgradeCommands.append( - 'post_meta.tables[%r].columns[%r].drop()' % (tn, col)) - for modelCol, databaseCol, modelDecl, databaseDecl in td.columns_different: - upgradeCommands.append( - 'assert False, "Can\'t alter columns: %s:%s=>%s"' % ( - tn, modelCol.name, databaseCol.name)) - downgradeCommands.append( - 'assert False, "Can\'t alter columns: %s:%s=>%s"' % ( - tn, modelCol.name, databaseCol.name)) - - return ( - '\n'.join(decls), - '\n'.join('%s%s' % (indent, line) for line in upgradeCommands), - '\n'.join('%s%s' % (indent, line) for line in downgradeCommands)) - - def _db_can_handle_this_change(self,td): - """Check if the database can handle going from B to A.""" - - if (td.columns_missing_from_B - and not td.columns_missing_from_A - and not td.columns_different): - # Even sqlite can handle column additions. - return True - else: - return not self.engine.url.drivername.startswith('sqlite') - - def runB2A(self): - """Goes from B to A. - - Was: applyModel. Apply model (A) to current database (B). - """ - - meta = sqlalchemy.MetaData(self.engine) - - for table in self._get_tables(missingA=True): - table = table.tometadata(meta) - table.drop() - for table in self._get_tables(missingB=True): - table = table.tometadata(meta) - table.create() - for modelTable in self._get_tables(modified=True): - tableName = modelTable.name - modelTable = modelTable.tometadata(meta) - dbTable = self.diff.metadataB.tables[tableName] - - td = self.diff.tables_different[tableName] - - if self._db_can_handle_this_change(td): - - for col in td.columns_missing_from_B: - modelTable.columns[col].create() - for col in td.columns_missing_from_A: - dbTable.columns[col].drop() - # XXX handle column changes here. - else: - # Sqlite doesn't support drop column, so you have to - # do more: create temp table, copy data to it, drop - # old table, create new table, copy data back. - # - # I wonder if this is guaranteed to be unique? - tempName = '_temp_%s' % modelTable.name - - def getCopyStatement(): - preparer = self.engine.dialect.preparer - commonCols = [] - for modelCol in modelTable.columns: - if modelCol.name in dbTable.columns: - commonCols.append(modelCol.name) - commonColsStr = ', '.join(commonCols) - return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \ - (tableName, commonColsStr, commonColsStr, tempName) - - # Move the data in one transaction, so that we don't - # leave the database in a nasty state. - connection = self.engine.connect() - trans = connection.begin() - try: - connection.execute( - 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \ - (tempName, modelTable.name)) - # make sure the drop takes place inside our - # transaction with the bind parameter - modelTable.drop(bind=connection) - modelTable.create(bind=connection) - connection.execute(getCopyStatement()) - connection.execute('DROP TABLE %s' % tempName) - trans.commit() - except: - trans.rollback() - raise - diff --git a/libs/migrate/versioning/migrate_repository.py b/libs/migrate/versioning/migrate_repository.py deleted file mode 100644 index 53833bbc..00000000 --- a/libs/migrate/versioning/migrate_repository.py +++ /dev/null @@ -1,100 +0,0 @@ -""" - Script to migrate repository from sqlalchemy <= 0.4.4 to the new - repository schema. This shouldn't use any other migrate modules, so - that it can work in any version. -""" - -import os -import sys -import logging - -log = logging.getLogger(__name__) - - -def usage(): - """Gives usage information.""" - print """Usage: %(prog)s repository-to-migrate - - Upgrade your repository to the new flat format. - - NOTE: You should probably make a backup before running this. - """ % {'prog': sys.argv[0]} - - sys.exit(1) - - -def delete_file(filepath): - """Deletes a file and prints a message.""" - log.info('Deleting file: %s' % filepath) - os.remove(filepath) - - -def move_file(src, tgt): - """Moves a file and prints a message.""" - log.info('Moving file %s to %s' % (src, tgt)) - if os.path.exists(tgt): - raise Exception( - 'Cannot move file %s because target %s already exists' % \ - (src, tgt)) - os.rename(src, tgt) - - -def delete_directory(dirpath): - """Delete a directory and print a message.""" - log.info('Deleting directory: %s' % dirpath) - os.rmdir(dirpath) - - -def migrate_repository(repos): - """Does the actual migration to the new repository format.""" - log.info('Migrating repository at: %s to new format' % repos) - versions = '%s/versions' % repos - dirs = os.listdir(versions) - # Only use int's in list. - numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()] - numdirs.sort() # Sort list. - for dirname in numdirs: - origdir = '%s/%s' % (versions, dirname) - log.info('Working on directory: %s' % origdir) - files = os.listdir(origdir) - files.sort() - for filename in files: - # Delete compiled Python files. - if filename.endswith('.pyc') or filename.endswith('.pyo'): - delete_file('%s/%s' % (origdir, filename)) - - # Delete empty __init__.py files. - origfile = '%s/__init__.py' % origdir - if os.path.exists(origfile) and len(open(origfile).read()) == 0: - delete_file(origfile) - - # Move sql upgrade scripts. - if filename.endswith('.sql'): - version, dbms, operation = filename.split('.', 3)[0:3] - origfile = '%s/%s' % (origdir, filename) - # For instance: 2.postgres.upgrade.sql -> - # 002_postgres_upgrade.sql - tgtfile = '%s/%03d_%s_%s.sql' % ( - versions, int(version), dbms, operation) - move_file(origfile, tgtfile) - - # Move Python upgrade script. - pyfile = '%s.py' % dirname - pyfilepath = '%s/%s' % (origdir, pyfile) - if os.path.exists(pyfilepath): - tgtfile = '%s/%03d.py' % (versions, int(dirname)) - move_file(pyfilepath, tgtfile) - - # Try to remove directory. Will fail if it's not empty. - delete_directory(origdir) - - -def main(): - """Main function to be called when using this script.""" - if len(sys.argv) != 2: - usage() - migrate_repository(sys.argv[1]) - - -if __name__ == '__main__': - main() diff --git a/libs/migrate/versioning/pathed.py b/libs/migrate/versioning/pathed.py deleted file mode 100644 index fbee0e46..00000000 --- a/libs/migrate/versioning/pathed.py +++ /dev/null @@ -1,75 +0,0 @@ -""" - A path/directory class. -""" - -import os -import shutil -import logging - -from migrate import exceptions -from migrate.versioning.config import * -from migrate.versioning.util import KeyedInstance - - -log = logging.getLogger(__name__) - -class Pathed(KeyedInstance): - """ - A class associated with a path/directory tree. - - Only one instance of this class may exist for a particular file; - __new__ will return an existing instance if possible - """ - parent = None - - @classmethod - def _key(cls, path): - return str(path) - - def __init__(self, path): - self.path = path - if self.__class__.parent is not None: - self._init_parent(path) - - def _init_parent(self, path): - """Try to initialize this object's parent, if it has one""" - parent_path = self.__class__._parent_path(path) - self.parent = self.__class__.parent(parent_path) - log.debug("Getting parent %r:%r" % (self.__class__.parent, parent_path)) - self.parent._init_child(path, self) - - def _init_child(self, child, path): - """Run when a child of this object is initialized. - - Parameters: the child object; the path to this object (its - parent) - """ - - @classmethod - def _parent_path(cls, path): - """ - Fetch the path of this object's parent from this object's path. - """ - # os.path.dirname(), but strip directories like files (like - # unix basename) - # - # Treat directories like files... - if path[-1] == '/': - path = path[:-1] - ret = os.path.dirname(path) - return ret - - @classmethod - def require_notfound(cls, path): - """Ensures a given path does not already exist""" - if os.path.exists(path): - raise exceptions.PathFoundError(path) - - @classmethod - def require_found(cls, path): - """Ensures a given path already exists""" - if not os.path.exists(path): - raise exceptions.PathNotFoundError(path) - - def __str__(self): - return self.path diff --git a/libs/migrate/versioning/repository.py b/libs/migrate/versioning/repository.py deleted file mode 100644 index 6e2f678f..00000000 --- a/libs/migrate/versioning/repository.py +++ /dev/null @@ -1,242 +0,0 @@ -""" - SQLAlchemy migrate repository management. -""" -import os -import shutil -import string -import logging - -from pkg_resources import resource_filename -from tempita import Template as TempitaTemplate - -from migrate import exceptions -from migrate.versioning import version, pathed, cfgparse -from migrate.versioning.template import Template -from migrate.versioning.config import * - - -log = logging.getLogger(__name__) - -class Changeset(dict): - """A collection of changes to be applied to a database. - - Changesets are bound to a repository and manage a set of - scripts from that repository. - - Behaves like a dict, for the most part. Keys are ordered based on step value. - """ - - def __init__(self, start, *changes, **k): - """ - Give a start version; step must be explicitly stated. - """ - self.step = k.pop('step', 1) - self.start = version.VerNum(start) - self.end = self.start - for change in changes: - self.add(change) - - def __iter__(self): - return iter(self.items()) - - def keys(self): - """ - In a series of upgrades x -> y, keys are version x. Sorted. - """ - ret = super(Changeset, self).keys() - # Reverse order if downgrading - ret.sort(reverse=(self.step < 1)) - return ret - - def values(self): - return [self[k] for k in self.keys()] - - def items(self): - return zip(self.keys(), self.values()) - - def add(self, change): - """Add new change to changeset""" - key = self.end - self.end += self.step - self[key] = change - - def run(self, *p, **k): - """Run the changeset scripts""" - for version, script in self: - script.run(*p, **k) - - -class Repository(pathed.Pathed): - """A project's change script repository""" - - _config = 'migrate.cfg' - _versions = 'versions' - - def __init__(self, path): - log.debug('Loading repository %s...' % path) - self.verify(path) - super(Repository, self).__init__(path) - self.config = cfgparse.Config(os.path.join(self.path, self._config)) - self.versions = version.Collection(os.path.join(self.path, - self._versions)) - log.debug('Repository %s loaded successfully' % path) - log.debug('Config: %r' % self.config.to_dict()) - - @classmethod - def verify(cls, path): - """ - Ensure the target path is a valid repository. - - :raises: :exc:`InvalidRepositoryError ` - """ - # Ensure the existence of required files - try: - cls.require_found(path) - cls.require_found(os.path.join(path, cls._config)) - cls.require_found(os.path.join(path, cls._versions)) - except exceptions.PathNotFoundError, e: - raise exceptions.InvalidRepositoryError(path) - - @classmethod - def prepare_config(cls, tmpl_dir, name, options=None): - """ - Prepare a project configuration file for a new project. - - :param tmpl_dir: Path to Repository template - :param config_file: Name of the config file in Repository template - :param name: Repository name - :type tmpl_dir: string - :type config_file: string - :type name: string - :returns: Populated config file - """ - if options is None: - options = {} - options.setdefault('version_table', 'migrate_version') - options.setdefault('repository_id', name) - options.setdefault('required_dbs', []) - options.setdefault('use_timestamp_numbering', False) - - tmpl = open(os.path.join(tmpl_dir, cls._config)).read() - ret = TempitaTemplate(tmpl).substitute(options) - - # cleanup - del options['__template_name__'] - - return ret - - @classmethod - def create(cls, path, name, **opts): - """Create a repository at a specified path""" - cls.require_notfound(path) - theme = opts.pop('templates_theme', None) - t_path = opts.pop('templates_path', None) - - # Create repository - tmpl_dir = Template(t_path).get_repository(theme=theme) - shutil.copytree(tmpl_dir, path) - - # Edit config defaults - config_text = cls.prepare_config(tmpl_dir, name, options=opts) - fd = open(os.path.join(path, cls._config), 'w') - fd.write(config_text) - fd.close() - - opts['repository_name'] = name - - # Create a management script - manager = os.path.join(path, 'manage.py') - Repository.create_manage_file(manager, templates_theme=theme, - templates_path=t_path, **opts) - - return cls(path) - - def create_script(self, description, **k): - """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`""" - - k['use_timestamp_numbering'] = self.use_timestamp_numbering - self.versions.create_new_python_version(description, **k) - - def create_script_sql(self, database, description, **k): - """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`""" - k['use_timestamp_numbering'] = self.use_timestamp_numbering - self.versions.create_new_sql_version(database, description, **k) - - @property - def latest(self): - """API to :attr:`migrate.versioning.version.Collection.latest`""" - return self.versions.latest - - @property - def version_table(self): - """Returns version_table name specified in config""" - return self.config.get('db_settings', 'version_table') - - @property - def id(self): - """Returns repository id specified in config""" - return self.config.get('db_settings', 'repository_id') - - @property - def use_timestamp_numbering(self): - """Returns use_timestamp_numbering specified in config""" - if self.config.has_option('db_settings', 'use_timestamp_numbering'): - return self.config.getboolean('db_settings', 'use_timestamp_numbering') - return False - - def version(self, *p, **k): - """API to :attr:`migrate.versioning.version.Collection.version`""" - return self.versions.version(*p, **k) - - @classmethod - def clear(cls): - # TODO: deletes repo - super(Repository, cls).clear() - version.Collection.clear() - - def changeset(self, database, start, end=None): - """Create a changeset to migrate this database from ver. start to end/latest. - - :param database: name of database to generate changeset - :param start: version to start at - :param end: version to end at (latest if None given) - :type database: string - :type start: int - :type end: int - :returns: :class:`Changeset instance ` - """ - start = version.VerNum(start) - - if end is None: - end = self.latest - else: - end = version.VerNum(end) - - if start <= end: - step = 1 - range_mod = 1 - op = 'upgrade' - else: - step = -1 - range_mod = 0 - op = 'downgrade' - - versions = range(start + range_mod, end + range_mod, step) - changes = [self.version(v).script(database, op) for v in versions] - ret = Changeset(start, step=step, *changes) - return ret - - @classmethod - def create_manage_file(cls, file_, **opts): - """Create a project management script (manage.py) - - :param file_: Destination file to be written - :param opts: Options that are passed to :func:`migrate.versioning.shell.main` - """ - mng_file = Template(opts.pop('templates_path', None))\ - .get_manage(theme=opts.pop('templates_theme', None)) - - tmpl = open(mng_file).read() - fd = open(file_, 'w') - fd.write(TempitaTemplate(tmpl).substitute(opts)) - fd.close() diff --git a/libs/migrate/versioning/schema.py b/libs/migrate/versioning/schema.py deleted file mode 100644 index e4d93653..00000000 --- a/libs/migrate/versioning/schema.py +++ /dev/null @@ -1,220 +0,0 @@ -""" - Database schema version management. -""" -import sys -import logging - -from sqlalchemy import (Table, Column, MetaData, String, Text, Integer, - create_engine) -from sqlalchemy.sql import and_ -from sqlalchemy import exceptions as sa_exceptions -from sqlalchemy.sql import bindparam - -from migrate import exceptions -from migrate.changeset import SQLA_07 -from migrate.versioning import genmodel, schemadiff -from migrate.versioning.repository import Repository -from migrate.versioning.util import load_model -from migrate.versioning.version import VerNum - - -log = logging.getLogger(__name__) - -class ControlledSchema(object): - """A database under version control""" - - def __init__(self, engine, repository): - if isinstance(repository, basestring): - repository = Repository(repository) - self.engine = engine - self.repository = repository - self.meta = MetaData(engine) - self.load() - - def __eq__(self, other): - """Compare two schemas by repositories and versions""" - return (self.repository is other.repository \ - and self.version == other.version) - - def load(self): - """Load controlled schema version info from DB""" - tname = self.repository.version_table - try: - if not hasattr(self, 'table') or self.table is None: - self.table = Table(tname, self.meta, autoload=True) - - result = self.engine.execute(self.table.select( - self.table.c.repository_id == str(self.repository.id))) - - data = list(result)[0] - except: - cls, exc, tb = sys.exc_info() - raise exceptions.DatabaseNotControlledError, exc.__str__(), tb - - self.version = data['version'] - return data - - def drop(self): - """ - Remove version control from a database. - """ - if SQLA_07: - try: - self.table.drop() - except sa_exceptions.DatabaseError: - raise exceptions.DatabaseNotControlledError(str(self.table)) - else: - try: - self.table.drop() - except (sa_exceptions.SQLError): - raise exceptions.DatabaseNotControlledError(str(self.table)) - - def changeset(self, version=None): - """API to Changeset creation. - - Uses self.version for start version and engine.name - to get database name. - """ - database = self.engine.name - start_ver = self.version - changeset = self.repository.changeset(database, start_ver, version) - return changeset - - def runchange(self, ver, change, step): - startver = ver - endver = ver + step - # Current database version must be correct! Don't run if corrupt! - if self.version != startver: - raise exceptions.InvalidVersionError("%s is not %s" % \ - (self.version, startver)) - # Run the change - change.run(self.engine, step) - - # Update/refresh database version - self.update_repository_table(startver, endver) - self.load() - - def update_repository_table(self, startver, endver): - """Update version_table with new information""" - update = self.table.update(and_(self.table.c.version == int(startver), - self.table.c.repository_id == str(self.repository.id))) - self.engine.execute(update, version=int(endver)) - - def upgrade(self, version=None): - """ - Upgrade (or downgrade) to a specified version, or latest version. - """ - changeset = self.changeset(version) - for ver, change in changeset: - self.runchange(ver, change, changeset.step) - - def update_db_from_model(self, model): - """ - Modify the database to match the structure of the current Python model. - """ - model = load_model(model) - - diff = schemadiff.getDiffOfModelAgainstDatabase( - model, self.engine, excludeTables=[self.repository.version_table] - ) - genmodel.ModelGenerator(diff,self.engine).runB2A() - - self.update_repository_table(self.version, int(self.repository.latest)) - - self.load() - - @classmethod - def create(cls, engine, repository, version=None): - """ - Declare a database to be under a repository's version control. - - :raises: :exc:`DatabaseAlreadyControlledError` - :returns: :class:`ControlledSchema` - """ - # Confirm that the version # is valid: positive, integer, - # exists in repos - if isinstance(repository, basestring): - repository = Repository(repository) - version = cls._validate_version(repository, version) - table = cls._create_table_version(engine, repository, version) - # TODO: history table - # Load repository information and return - return cls(engine, repository) - - @classmethod - def _validate_version(cls, repository, version): - """ - Ensures this is a valid version number for this repository. - - :raises: :exc:`InvalidVersionError` if invalid - :return: valid version number - """ - if version is None: - version = 0 - try: - version = VerNum(version) # raises valueerror - if version < 0 or version > repository.latest: - raise ValueError() - except ValueError: - raise exceptions.InvalidVersionError(version) - return version - - @classmethod - def _create_table_version(cls, engine, repository, version): - """ - Creates the versioning table in a database. - - :raises: :exc:`DatabaseAlreadyControlledError` - """ - # Create tables - tname = repository.version_table - meta = MetaData(engine) - - table = Table( - tname, meta, - Column('repository_id', String(250), primary_key=True), - Column('repository_path', Text), - Column('version', Integer), ) - - # there can be multiple repositories/schemas in the same db - if not table.exists(): - table.create() - - # test for existing repository_id - s = table.select(table.c.repository_id == bindparam("repository_id")) - result = engine.execute(s, repository_id=repository.id) - if result.fetchone(): - raise exceptions.DatabaseAlreadyControlledError - - # Insert data - engine.execute(table.insert().values( - repository_id=repository.id, - repository_path=repository.path, - version=int(version))) - return table - - @classmethod - def compare_model_to_db(cls, engine, model, repository): - """ - Compare the current model against the current database. - """ - if isinstance(repository, basestring): - repository = Repository(repository) - model = load_model(model) - - diff = schemadiff.getDiffOfModelAgainstDatabase( - model, engine, excludeTables=[repository.version_table]) - return diff - - @classmethod - def create_model(cls, engine, repository, declarative=False): - """ - Dump the current database as a Python model. - """ - if isinstance(repository, basestring): - repository = Repository(repository) - - diff = schemadiff.getDiffOfModelAgainstDatabase( - MetaData(), engine, excludeTables=[repository.version_table] - ) - return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition() diff --git a/libs/migrate/versioning/schemadiff.py b/libs/migrate/versioning/schemadiff.py deleted file mode 100644 index 04cf83e6..00000000 --- a/libs/migrate/versioning/schemadiff.py +++ /dev/null @@ -1,292 +0,0 @@ -""" - Schema differencing support. -""" - -import logging -import sqlalchemy - -from sqlalchemy.types import Float - -log = logging.getLogger(__name__) - -def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None): - """ - Return differences of model against database. - - :return: object which will evaluate to :keyword:`True` if there \ - are differences else :keyword:`False`. - """ - db_metadata = sqlalchemy.MetaData(engine, reflect=True) - - # sqlite will include a dynamically generated 'sqlite_sequence' table if - # there are autoincrement sequences in the database; this should not be - # compared. - if engine.dialect.name == 'sqlite': - if 'sqlite_sequence' in db_metadata.tables: - db_metadata.remove(db_metadata.tables['sqlite_sequence']) - - return SchemaDiff(metadata, db_metadata, - labelA='model', - labelB='database', - excludeTables=excludeTables) - - -def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None): - """ - Return differences of model against another model. - - :return: object which will evaluate to :keyword:`True` if there \ - are differences else :keyword:`False`. - """ - return SchemaDiff(metadataA, metadataB, excludeTables) - - -class ColDiff(object): - """ - Container for differences in one :class:`~sqlalchemy.schema.Column` - between two :class:`~sqlalchemy.schema.Table` instances, ``A`` - and ``B``. - - .. attribute:: col_A - - The :class:`~sqlalchemy.schema.Column` object for A. - - .. attribute:: col_B - - The :class:`~sqlalchemy.schema.Column` object for B. - - .. attribute:: type_A - - The most generic type of the :class:`~sqlalchemy.schema.Column` - object in A. - - .. attribute:: type_B - - The most generic type of the :class:`~sqlalchemy.schema.Column` - object in A. - - """ - - diff = False - - def __init__(self,col_A,col_B): - self.col_A = col_A - self.col_B = col_B - - self.type_A = col_A.type - self.type_B = col_B.type - - self.affinity_A = self.type_A._type_affinity - self.affinity_B = self.type_B._type_affinity - - if self.affinity_A is not self.affinity_B: - self.diff = True - return - - if isinstance(self.type_A,Float) or isinstance(self.type_B,Float): - if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)): - self.diff=True - return - - for attr in ('precision','scale','length'): - A = getattr(self.type_A,attr,None) - B = getattr(self.type_B,attr,None) - if not (A is None or B is None) and A!=B: - self.diff=True - return - - def __nonzero__(self): - return self.diff - -class TableDiff(object): - """ - Container for differences in one :class:`~sqlalchemy.schema.Table` - between two :class:`~sqlalchemy.schema.MetaData` instances, ``A`` - and ``B``. - - .. attribute:: columns_missing_from_A - - A sequence of column names that were found in B but weren't in - A. - - .. attribute:: columns_missing_from_B - - A sequence of column names that were found in A but weren't in - B. - - .. attribute:: columns_different - - A dictionary containing information about columns that were - found to be different. - It maps column names to a :class:`ColDiff` objects describing the - differences found. - """ - __slots__ = ( - 'columns_missing_from_A', - 'columns_missing_from_B', - 'columns_different', - ) - - def __nonzero__(self): - return bool( - self.columns_missing_from_A or - self.columns_missing_from_B or - self.columns_different - ) - -class SchemaDiff(object): - """ - Compute the difference between two :class:`~sqlalchemy.schema.MetaData` - objects. - - The string representation of a :class:`SchemaDiff` will summarise - the changes found between the two - :class:`~sqlalchemy.schema.MetaData` objects. - - The length of a :class:`SchemaDiff` will give the number of - changes found, enabling it to be used much like a boolean in - expressions. - - :param metadataA: - First :class:`~sqlalchemy.schema.MetaData` to compare. - - :param metadataB: - Second :class:`~sqlalchemy.schema.MetaData` to compare. - - :param labelA: - The label to use in messages about the first - :class:`~sqlalchemy.schema.MetaData`. - - :param labelB: - The label to use in messages about the second - :class:`~sqlalchemy.schema.MetaData`. - - :param excludeTables: - A sequence of table names to exclude. - - .. attribute:: tables_missing_from_A - - A sequence of table names that were found in B but weren't in - A. - - .. attribute:: tables_missing_from_B - - A sequence of table names that were found in A but weren't in - B. - - .. attribute:: tables_different - - A dictionary containing information about tables that were found - to be different. - It maps table names to a :class:`TableDiff` objects describing the - differences found. - """ - - def __init__(self, - metadataA, metadataB, - labelA='metadataA', - labelB='metadataB', - excludeTables=None): - - self.metadataA, self.metadataB = metadataA, metadataB - self.labelA, self.labelB = labelA, labelB - self.label_width = max(len(labelA),len(labelB)) - excludeTables = set(excludeTables or []) - - A_table_names = set(metadataA.tables.keys()) - B_table_names = set(metadataB.tables.keys()) - - self.tables_missing_from_A = sorted( - B_table_names - A_table_names - excludeTables - ) - self.tables_missing_from_B = sorted( - A_table_names - B_table_names - excludeTables - ) - - self.tables_different = {} - for table_name in A_table_names.intersection(B_table_names): - - td = TableDiff() - - A_table = metadataA.tables[table_name] - B_table = metadataB.tables[table_name] - - A_column_names = set(A_table.columns.keys()) - B_column_names = set(B_table.columns.keys()) - - td.columns_missing_from_A = sorted( - B_column_names - A_column_names - ) - - td.columns_missing_from_B = sorted( - A_column_names - B_column_names - ) - - td.columns_different = {} - - for col_name in A_column_names.intersection(B_column_names): - - cd = ColDiff( - A_table.columns.get(col_name), - B_table.columns.get(col_name) - ) - - if cd: - td.columns_different[col_name]=cd - - # XXX - index and constraint differences should - # be checked for here - - if td: - self.tables_different[table_name]=td - - def __str__(self): - ''' Summarize differences. ''' - out = [] - column_template =' %%%is: %%r' % self.label_width - - for names,label in ( - (self.tables_missing_from_A,self.labelA), - (self.tables_missing_from_B,self.labelB), - ): - if names: - out.append( - ' tables missing from %s: %s' % ( - label,', '.join(sorted(names)) - ) - ) - - for name,td in sorted(self.tables_different.items()): - out.append( - ' table with differences: %s' % name - ) - for names,label in ( - (td.columns_missing_from_A,self.labelA), - (td.columns_missing_from_B,self.labelB), - ): - if names: - out.append( - ' %s missing these columns: %s' % ( - label,', '.join(sorted(names)) - ) - ) - for name,cd in td.columns_different.items(): - out.append(' column with differences: %s' % name) - out.append(column_template % (self.labelA,cd.col_A)) - out.append(column_template % (self.labelB,cd.col_B)) - - if out: - out.insert(0, 'Schema diffs:') - return '\n'.join(out) - else: - return 'No schema diffs' - - def __len__(self): - """ - Used in bool evaluation, return of 0 means no diffs. - """ - return ( - len(self.tables_missing_from_A) + - len(self.tables_missing_from_B) + - len(self.tables_different) - ) diff --git a/libs/migrate/versioning/script/__init__.py b/libs/migrate/versioning/script/__init__.py deleted file mode 100644 index c788edaa..00000000 --- a/libs/migrate/versioning/script/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from migrate.versioning.script.base import BaseScript -from migrate.versioning.script.py import PythonScript -from migrate.versioning.script.sql import SqlScript diff --git a/libs/migrate/versioning/script/base.py b/libs/migrate/versioning/script/base.py deleted file mode 100644 index 42872352..00000000 --- a/libs/migrate/versioning/script/base.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import logging - -from migrate import exceptions -from migrate.versioning.config import operations -from migrate.versioning import pathed - - -log = logging.getLogger(__name__) - -class BaseScript(pathed.Pathed): - """Base class for other types of scripts. - All scripts have the following properties: - - source (script.source()) - The source code of the script - version (script.version()) - The version number of the script - operations (script.operations()) - The operations defined by the script: upgrade(), downgrade() or both. - Returns a tuple of operations. - Can also check for an operation with ex. script.operation(Script.ops.up) - """ # TODO: sphinxfy this and implement it correctly - - def __init__(self, path): - log.debug('Loading script %s...' % path) - self.verify(path) - super(BaseScript, self).__init__(path) - log.debug('Script %s loaded successfully' % path) - - @classmethod - def verify(cls, path): - """Ensure this is a valid script - This version simply ensures the script file's existence - - :raises: :exc:`InvalidScriptError ` - """ - try: - cls.require_found(path) - except: - raise exceptions.InvalidScriptError(path) - - def source(self): - """:returns: source code of the script. - :rtype: string - """ - fd = open(self.path) - ret = fd.read() - fd.close() - return ret - - def run(self, engine): - """Core of each BaseScript subclass. - This method executes the script. - """ - raise NotImplementedError() diff --git a/libs/migrate/versioning/script/py.py b/libs/migrate/versioning/script/py.py deleted file mode 100644 index 3a090d49..00000000 --- a/libs/migrate/versioning/script/py.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import shutil -import warnings -import logging -import inspect -from StringIO import StringIO - -import migrate -from migrate.versioning import genmodel, schemadiff -from migrate.versioning.config import operations -from migrate.versioning.template import Template -from migrate.versioning.script import base -from migrate.versioning.util import import_path, load_model, with_engine -from migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError - -log = logging.getLogger(__name__) -__all__ = ['PythonScript'] - - -class PythonScript(base.BaseScript): - """Base for Python scripts""" - - @classmethod - def create(cls, path, **opts): - """Create an empty migration script at specified path - - :returns: :class:`PythonScript instance `""" - cls.require_notfound(path) - - src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None)) - shutil.copy(src, path) - - return cls(path) - - @classmethod - def make_update_script_for_model(cls, engine, oldmodel, - model, repository, **opts): - """Create a migration script based on difference between two SA models. - - :param repository: path to migrate repository - :param oldmodel: dotted.module.name:SAClass or SAClass object - :param model: dotted.module.name:SAClass or SAClass object - :param engine: SQLAlchemy engine - :type repository: string or :class:`Repository instance ` - :type oldmodel: string or Class - :type model: string or Class - :type engine: Engine instance - :returns: Upgrade / Downgrade script - :rtype: string - """ - - if isinstance(repository, basestring): - # oh dear, an import cycle! - from migrate.versioning.repository import Repository - repository = Repository(repository) - - oldmodel = load_model(oldmodel) - model = load_model(model) - - # Compute differences. - diff = schemadiff.getDiffOfModelAgainstModel( - model, - oldmodel, - excludeTables=[repository.version_table]) - # TODO: diff can be False (there is no difference?) - decls, upgradeCommands, downgradeCommands = \ - genmodel.ModelGenerator(diff,engine).genB2AMigration() - - # Store differences into file. - src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None)) - f = open(src) - contents = f.read() - f.close() - - # generate source - search = 'def upgrade(migrate_engine):' - contents = contents.replace(search, '\n\n'.join((decls, search)), 1) - if upgradeCommands: - contents = contents.replace(' pass', upgradeCommands, 1) - if downgradeCommands: - contents = contents.replace(' pass', downgradeCommands, 1) - return contents - - @classmethod - def verify_module(cls, path): - """Ensure path is a valid script - - :param path: Script location - :type path: string - :raises: :exc:`InvalidScriptError ` - :returns: Python module - """ - # Try to import and get the upgrade() func - module = import_path(path) - try: - assert callable(module.upgrade) - except Exception, e: - raise InvalidScriptError(path + ': %s' % str(e)) - return module - - def preview_sql(self, url, step, **args): - """Mocks SQLAlchemy Engine to store all executed calls in a string - and runs :meth:`PythonScript.run ` - - :returns: SQL file - """ - buf = StringIO() - args['engine_arg_strategy'] = 'mock' - args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p) - - @with_engine - def go(url, step, **kw): - engine = kw.pop('engine') - self.run(engine, step) - return buf.getvalue() - - return go(url, step, **args) - - def run(self, engine, step): - """Core method of Script file. - Exectues :func:`update` or :func:`downgrade` functions - - :param engine: SQLAlchemy Engine - :param step: Operation to run - :type engine: string - :type step: int - """ - if step > 0: - op = 'upgrade' - elif step < 0: - op = 'downgrade' - else: - raise ScriptError("%d is not a valid step" % step) - - funcname = base.operations[op] - script_func = self._func(funcname) - - # check for old way of using engine - if not inspect.getargspec(script_func)[0]: - raise TypeError("upgrade/downgrade functions must accept engine" - " parameter (since version 0.5.4)") - - script_func(engine) - - @property - def module(self): - """Calls :meth:`migrate.versioning.script.py.verify_module` - and returns it. - """ - if not hasattr(self, '_module'): - self._module = self.verify_module(self.path) - return self._module - - def _func(self, funcname): - if not hasattr(self.module, funcname): - msg = "Function '%s' is not defined in this script" - raise ScriptError(msg % funcname) - return getattr(self.module, funcname) diff --git a/libs/migrate/versioning/script/sql.py b/libs/migrate/versioning/script/sql.py deleted file mode 100644 index ed807641..00000000 --- a/libs/migrate/versioning/script/sql.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import logging -import shutil - -from migrate.versioning.script import base -from migrate.versioning.template import Template - - -log = logging.getLogger(__name__) - -class SqlScript(base.BaseScript): - """A file containing plain SQL statements.""" - - @classmethod - def create(cls, path, **opts): - """Create an empty migration script at specified path - - :returns: :class:`SqlScript instance `""" - cls.require_notfound(path) - - src = Template(opts.pop('templates_path', None)).get_sql_script(theme=opts.pop('templates_theme', None)) - shutil.copy(src, path) - return cls(path) - - # TODO: why is step parameter even here? - def run(self, engine, step=None, executemany=True): - """Runs SQL script through raw dbapi execute call""" - text = self.source() - # Don't rely on SA's autocommit here - # (SA uses .startswith to check if a commit is needed. What if script - # starts with a comment?) - conn = engine.connect() - try: - trans = conn.begin() - try: - # HACK: SQLite doesn't allow multiple statements through - # its execute() method, but it provides executescript() instead - dbapi = conn.engine.raw_connection() - if executemany and getattr(dbapi, 'executescript', None): - dbapi.executescript(text) - else: - conn.execute(text) - trans.commit() - except: - trans.rollback() - raise - finally: - conn.close() diff --git a/libs/migrate/versioning/shell.py b/libs/migrate/versioning/shell.py deleted file mode 100644 index ad7b6798..00000000 --- a/libs/migrate/versioning/shell.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The migrate command-line tool.""" - -import sys -import inspect -import logging -from optparse import OptionParser, BadOptionError - -from migrate import exceptions -from migrate.versioning import api -from migrate.versioning.config import * -from migrate.versioning.util import asbool - - -alias = dict( - s=api.script, - vc=api.version_control, - dbv=api.db_version, - v=api.version, -) - -def alias_setup(): - global alias - for key, val in alias.iteritems(): - setattr(api, key, val) -alias_setup() - - -class PassiveOptionParser(OptionParser): - - def _process_args(self, largs, rargs, values): - """little hack to support all --some_option=value parameters""" - - while rargs: - arg = rargs[0] - if arg == "--": - del rargs[0] - return - elif arg[0:2] == "--": - # if parser does not know about the option - # pass it along (make it anonymous) - try: - opt = arg.split('=', 1)[0] - self._match_long_opt(opt) - except BadOptionError: - largs.append(arg) - del rargs[0] - else: - self._process_long_opt(rargs, values) - elif arg[:1] == "-" and len(arg) > 1: - self._process_short_opts(rargs, values) - elif self.allow_interspersed_args: - largs.append(arg) - del rargs[0] - -def main(argv=None, **kwargs): - """Shell interface to :mod:`migrate.versioning.api`. - - kwargs are default options that can be overriden with passing - --some_option as command line option - - :param disable_logging: Let migrate configure logging - :type disable_logging: bool - """ - if argv is not None: - argv = argv - else: - argv = list(sys.argv[1:]) - commands = list(api.__all__) - commands.sort() - - usage = """%%prog COMMAND ... - - Available commands: - %s - - Enter "%%prog help COMMAND" for information on a particular command. - """ % '\n\t'.join(["%s - %s" % (command.ljust(28), api.command_desc.get(command)) for command in commands]) - - parser = PassiveOptionParser(usage=usage) - parser.add_option("-d", "--debug", - action="store_true", - dest="debug", - default=False, - help="Shortcut to turn on DEBUG mode for logging") - parser.add_option("-q", "--disable_logging", - action="store_true", - dest="disable_logging", - default=False, - help="Use this option to disable logging configuration") - help_commands = ['help', '-h', '--help'] - HELP = False - - try: - command = argv.pop(0) - if command in help_commands: - HELP = True - command = argv.pop(0) - except IndexError: - parser.print_help() - return - - command_func = getattr(api, command, None) - if command_func is None or command.startswith('_'): - parser.error("Invalid command %s" % command) - - parser.set_usage(inspect.getdoc(command_func)) - f_args, f_varargs, f_kwargs, f_defaults = inspect.getargspec(command_func) - for arg in f_args: - parser.add_option( - "--%s" % arg, - dest=arg, - action='store', - type="string") - - # display help of the current command - if HELP: - parser.print_help() - return - - options, args = parser.parse_args(argv) - - # override kwargs with anonymous parameters - override_kwargs = dict() - for arg in list(args): - if arg.startswith('--'): - args.remove(arg) - if '=' in arg: - opt, value = arg[2:].split('=', 1) - else: - opt = arg[2:] - value = True - override_kwargs[opt] = value - - # override kwargs with options if user is overwriting - for key, value in options.__dict__.iteritems(): - if value is not None: - override_kwargs[key] = value - - # arguments that function accepts without passed kwargs - f_required = list(f_args) - candidates = dict(kwargs) - candidates.update(override_kwargs) - for key, value in candidates.iteritems(): - if key in f_args: - f_required.remove(key) - - # map function arguments to parsed arguments - for arg in args: - try: - kw = f_required.pop(0) - except IndexError: - parser.error("Too many arguments for command %s: %s" % (command, - arg)) - kwargs[kw] = arg - - # apply overrides - kwargs.update(override_kwargs) - - # configure options - for key, value in options.__dict__.iteritems(): - kwargs.setdefault(key, value) - - # configure logging - if not asbool(kwargs.pop('disable_logging', False)): - # filter to log =< INFO into stdout and rest to stderr - class SingleLevelFilter(logging.Filter): - def __init__(self, min=None, max=None): - self.min = min or 0 - self.max = max or 100 - - def filter(self, record): - return self.min <= record.levelno <= self.max - - logger = logging.getLogger() - h1 = logging.StreamHandler(sys.stdout) - f1 = SingleLevelFilter(max=logging.INFO) - h1.addFilter(f1) - h2 = logging.StreamHandler(sys.stderr) - f2 = SingleLevelFilter(min=logging.WARN) - h2.addFilter(f2) - logger.addHandler(h1) - logger.addHandler(h2) - - if options.debug: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - log = logging.getLogger(__name__) - - # check if all args are given - try: - num_defaults = len(f_defaults) - except TypeError: - num_defaults = 0 - f_args_default = f_args[len(f_args) - num_defaults:] - required = list(set(f_required) - set(f_args_default)) - if required: - parser.error("Not enough arguments for command %s: %s not specified" \ - % (command, ', '.join(required))) - - # handle command - try: - ret = command_func(**kwargs) - if ret is not None: - log.info(ret) - except (exceptions.UsageError, exceptions.KnownError), e: - parser.error(e.args[0]) - -if __name__ == "__main__": - main() diff --git a/libs/migrate/versioning/template.py b/libs/migrate/versioning/template.py deleted file mode 100644 index 182898a6..00000000 --- a/libs/migrate/versioning/template.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os -import shutil -import sys - -from pkg_resources import resource_filename - -from migrate.versioning.config import * -from migrate.versioning import pathed - - -class Collection(pathed.Pathed): - """A collection of templates of a specific type""" - _mask = None - - def get_path(self, file): - return os.path.join(self.path, str(file)) - - -class RepositoryCollection(Collection): - _mask = '%s' - -class ScriptCollection(Collection): - _mask = '%s.py_tmpl' - -class ManageCollection(Collection): - _mask = '%s.py_tmpl' - -class SQLScriptCollection(Collection): - _mask = '%s.py_tmpl' - -class Template(pathed.Pathed): - """Finds the paths/packages of various Migrate templates. - - :param path: Templates are loaded from migrate package - if `path` is not provided. - """ - pkg = 'migrate.versioning.templates' - - def __new__(cls, path=None): - if path is None: - path = cls._find_path(cls.pkg) - return super(Template, cls).__new__(cls, path) - - def __init__(self, path=None): - if path is None: - path = Template._find_path(self.pkg) - super(Template, self).__init__(path) - self.repository = RepositoryCollection(os.path.join(path, 'repository')) - self.script = ScriptCollection(os.path.join(path, 'script')) - self.manage = ManageCollection(os.path.join(path, 'manage')) - self.sql_script = SQLScriptCollection(os.path.join(path, 'sql_script')) - - @classmethod - def _find_path(cls, pkg): - """Returns absolute path to dotted python package.""" - tmp_pkg = pkg.rsplit('.', 1) - - if len(tmp_pkg) != 1: - return resource_filename(tmp_pkg[0], tmp_pkg[1]) - else: - return resource_filename(tmp_pkg[0], '') - - def _get_item(self, collection, theme=None): - """Locates and returns collection. - - :param collection: name of collection to locate - :param type_: type of subfolder in collection (defaults to "_default") - :returns: (package, source) - :rtype: str, str - """ - item = getattr(self, collection) - theme_mask = getattr(item, '_mask') - theme = theme_mask % (theme or 'default') - return item.get_path(theme) - - def get_repository(self, *a, **kw): - """Calls self._get_item('repository', *a, **kw)""" - return self._get_item('repository', *a, **kw) - - def get_script(self, *a, **kw): - """Calls self._get_item('script', *a, **kw)""" - return self._get_item('script', *a, **kw) - - def get_sql_script(self, *a, **kw): - """Calls self._get_item('sql_script', *a, **kw)""" - return self._get_item('sql_script', *a, **kw) - - def get_manage(self, *a, **kw): - """Calls self._get_item('manage', *a, **kw)""" - return self._get_item('manage', *a, **kw) diff --git a/libs/migrate/versioning/templates/__init__.py b/libs/migrate/versioning/templates/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/manage/default.py_tmpl b/libs/migrate/versioning/templates/manage/default.py_tmpl deleted file mode 100644 index f6d75c50..00000000 --- a/libs/migrate/versioning/templates/manage/default.py_tmpl +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python -from migrate.versioning.shell import main - -{{py: -_vars = locals().copy() -del _vars['__template_name__'] -_vars.pop('repository_name', None) -defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()]) -}} - -if __name__ == '__main__': - main({{ defaults }}) diff --git a/libs/migrate/versioning/templates/manage/pylons.py_tmpl b/libs/migrate/versioning/templates/manage/pylons.py_tmpl deleted file mode 100644 index cc2f7885..00000000 --- a/libs/migrate/versioning/templates/manage/pylons.py_tmpl +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -import sys - -from sqlalchemy import engine_from_config -from paste.deploy.loadwsgi import ConfigLoader - -from migrate.versioning.shell import main -from {{ locals().pop('repository_name') }}.model import migrations - - -if '-c' in sys.argv: - pos = sys.argv.index('-c') - conf_path = sys.argv[pos + 1] - del sys.argv[pos:pos + 2] -else: - conf_path = 'development.ini' - -{{py: -_vars = locals().copy() -del _vars['__template_name__'] -defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()]) -}} - -conf_dict = ConfigLoader(conf_path).parser._sections['app:main'] - -# migrate supports passing url as an existing Engine instance (since 0.6.0) -# usage: migrate -c path/to/config.ini COMMANDS -if __name__ == '__main__': - main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }}) diff --git a/libs/migrate/versioning/templates/repository/__init__.py b/libs/migrate/versioning/templates/repository/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/repository/default/README b/libs/migrate/versioning/templates/repository/default/README deleted file mode 100644 index 6218f8ca..00000000 --- a/libs/migrate/versioning/templates/repository/default/README +++ /dev/null @@ -1,4 +0,0 @@ -This is a database migration repository. - -More information at -http://code.google.com/p/sqlalchemy-migrate/ diff --git a/libs/migrate/versioning/templates/repository/default/__init__.py b/libs/migrate/versioning/templates/repository/default/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/repository/default/migrate.cfg b/libs/migrate/versioning/templates/repository/default/migrate.cfg deleted file mode 100644 index dae06123..00000000 --- a/libs/migrate/versioning/templates/repository/default/migrate.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id={{ locals().pop('repository_id') }} - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table={{ locals().pop('version_table') }} - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs={{ locals().pop('required_dbs') }} - -# When creating new change scripts, Migrate will stamp the new script with -# a version number. By default this is latest_version + 1. You can set this -# to 'true' to tell Migrate to use the UTC timestamp instead. -use_timestamp_numbering={{ locals().pop('use_timestamp_numbering') }} diff --git a/libs/migrate/versioning/templates/repository/default/versions/__init__.py b/libs/migrate/versioning/templates/repository/default/versions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/repository/pylons/README b/libs/migrate/versioning/templates/repository/pylons/README deleted file mode 100644 index 6218f8ca..00000000 --- a/libs/migrate/versioning/templates/repository/pylons/README +++ /dev/null @@ -1,4 +0,0 @@ -This is a database migration repository. - -More information at -http://code.google.com/p/sqlalchemy-migrate/ diff --git a/libs/migrate/versioning/templates/repository/pylons/__init__.py b/libs/migrate/versioning/templates/repository/pylons/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/repository/pylons/migrate.cfg b/libs/migrate/versioning/templates/repository/pylons/migrate.cfg deleted file mode 100644 index dae06123..00000000 --- a/libs/migrate/versioning/templates/repository/pylons/migrate.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id={{ locals().pop('repository_id') }} - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table={{ locals().pop('version_table') }} - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs={{ locals().pop('required_dbs') }} - -# When creating new change scripts, Migrate will stamp the new script with -# a version number. By default this is latest_version + 1. You can set this -# to 'true' to tell Migrate to use the UTC timestamp instead. -use_timestamp_numbering={{ locals().pop('use_timestamp_numbering') }} diff --git a/libs/migrate/versioning/templates/repository/pylons/versions/__init__.py b/libs/migrate/versioning/templates/repository/pylons/versions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/script/__init__.py b/libs/migrate/versioning/templates/script/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/script/default.py_tmpl b/libs/migrate/versioning/templates/script/default.py_tmpl deleted file mode 100644 index 58d874bf..00000000 --- a/libs/migrate/versioning/templates/script/default.py_tmpl +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import * -from migrate import * - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; bind - # migrate_engine to your metadata - pass - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - pass diff --git a/libs/migrate/versioning/templates/script/pylons.py_tmpl b/libs/migrate/versioning/templates/script/pylons.py_tmpl deleted file mode 100644 index 58d874bf..00000000 --- a/libs/migrate/versioning/templates/script/pylons.py_tmpl +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import * -from migrate import * - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; bind - # migrate_engine to your metadata - pass - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - pass diff --git a/libs/migrate/versioning/templates/sql_script/default.py_tmpl b/libs/migrate/versioning/templates/sql_script/default.py_tmpl deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/templates/sql_script/pylons.py_tmpl b/libs/migrate/versioning/templates/sql_script/pylons.py_tmpl deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/migrate/versioning/util/__init__.py b/libs/migrate/versioning/util/__init__.py deleted file mode 100644 index 9b79f409..00000000 --- a/libs/migrate/versioning/util/__init__.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""".. currentmodule:: migrate.versioning.util""" - -import warnings -import logging -from decorator import decorator -from pkg_resources import EntryPoint - -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine -from sqlalchemy.pool import StaticPool - -from migrate import exceptions -from migrate.versioning.util.keyedinstance import KeyedInstance -from migrate.versioning.util.importpath import import_path - - -log = logging.getLogger(__name__) - -def load_model(dotted_name): - """Import module and use module-level variable". - - :param dotted_name: path to model in form of string: ``some.python.module:Class`` - - .. versionchanged:: 0.5.4 - - """ - if isinstance(dotted_name, basestring): - if ':' not in dotted_name: - # backwards compatibility - warnings.warn('model should be in form of module.model:User ' - 'and not module.model.User', exceptions.MigrateDeprecationWarning) - dotted_name = ':'.join(dotted_name.rsplit('.', 1)) - return EntryPoint.parse('x=%s' % dotted_name).load(False) - else: - # Assume it's already loaded. - return dotted_name - -def asbool(obj): - """Do everything to use object as bool""" - if isinstance(obj, basestring): - obj = obj.strip().lower() - if obj in ['true', 'yes', 'on', 'y', 't', '1']: - return True - elif obj in ['false', 'no', 'off', 'n', 'f', '0']: - return False - else: - raise ValueError("String is not true/false: %r" % obj) - if obj in (True, False): - return bool(obj) - else: - raise ValueError("String is not true/false: %r" % obj) - -def guess_obj_type(obj): - """Do everything to guess object type from string - - Tries to convert to `int`, `bool` and finally returns if not succeded. - - .. versionadded: 0.5.4 - """ - - result = None - - try: - result = int(obj) - except: - pass - - if result is None: - try: - result = asbool(obj) - except: - pass - - if result is not None: - return result - else: - return obj - -@decorator -def catch_known_errors(f, *a, **kw): - """Decorator that catches known api errors - - .. versionadded: 0.5.4 - """ - - try: - return f(*a, **kw) - except exceptions.PathFoundError, e: - raise exceptions.KnownError("The path %s already exists" % e.args[0]) - -def construct_engine(engine, **opts): - """.. versionadded:: 0.5.4 - - Constructs and returns SQLAlchemy engine. - - Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions: - - :param engine: connection string or a existing engine - :param engine_dict: python dictionary of options to pass to `create_engine` - :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`) - :type engine_dict: dict - :type engine: string or Engine instance - :type engine_arg_*: string - :returns: SQLAlchemy Engine - - .. note:: - - keyword parameters override ``engine_dict`` values. - - """ - if isinstance(engine, Engine): - return engine - elif not isinstance(engine, basestring): - raise ValueError("you need to pass either an existing engine or a database uri") - - # get options for create_engine - if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict): - kwargs = opts['engine_dict'] - else: - kwargs = dict() - - # DEPRECATED: handle echo the old way - echo = asbool(opts.get('echo', False)) - if echo: - warnings.warn('echo=True parameter is deprecated, pass ' - 'engine_arg_echo=True or engine_dict={"echo": True}', - exceptions.MigrateDeprecationWarning) - kwargs['echo'] = echo - - # parse keyword arguments - for key, value in opts.iteritems(): - if key.startswith('engine_arg_'): - kwargs[key[11:]] = guess_obj_type(value) - - log.debug('Constructing engine') - # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs) - # seems like 0.5.x branch does not work with engine.dispose and staticpool - return create_engine(engine, **kwargs) - -@decorator -def with_engine(f, *a, **kw): - """Decorator for :mod:`migrate.versioning.api` functions - to safely close resources after function usage. - - Passes engine parameters to :func:`construct_engine` and - resulting parameter is available as kw['engine']. - - Engine is disposed after wrapped function is executed. - - .. versionadded: 0.6.0 - """ - url = a[0] - engine = construct_engine(url, **kw) - - try: - kw['engine'] = engine - return f(*a, **kw) - finally: - if isinstance(engine, Engine): - log.debug('Disposing SQLAlchemy engine %s', engine) - engine.dispose() - - -class Memoize: - """Memoize(fn) - an instance which acts like fn but memoizes its arguments - Will only work on functions with non-mutable arguments - - ActiveState Code 52201 - """ - def __init__(self, fn): - self.fn = fn - self.memo = {} - - def __call__(self, *args): - if not self.memo.has_key(args): - self.memo[args] = self.fn(*args) - return self.memo[args] diff --git a/libs/migrate/versioning/util/importpath.py b/libs/migrate/versioning/util/importpath.py deleted file mode 100644 index 59b57f14..00000000 --- a/libs/migrate/versioning/util/importpath.py +++ /dev/null @@ -1,16 +0,0 @@ -import os -import sys - -def import_path(fullpath): - """ Import a file with full path specification. Allows one to - import from anywhere, something __import__ does not do. - """ - # http://zephyrfalcon.org/weblog/arch_d7_2002_08_31.html - path, filename = os.path.split(fullpath) - filename, ext = os.path.splitext(filename) - sys.path.append(path) - module = __import__(filename) - reload(module) # Might be out of date during tests - del sys.path[-1] - return module - diff --git a/libs/migrate/versioning/util/keyedinstance.py b/libs/migrate/versioning/util/keyedinstance.py deleted file mode 100644 index 3f6cb916..00000000 --- a/libs/migrate/versioning/util/keyedinstance.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -class KeyedInstance(object): - """A class whose instances have a unique identifier of some sort - No two instances with the same unique ID should exist - if we try to create - a second instance, the first should be returned. - """ - - _instances = dict() - - def __new__(cls, *p, **k): - instances = cls._instances - clskey = str(cls) - if clskey not in instances: - instances[clskey] = dict() - instances = instances[clskey] - - key = cls._key(*p, **k) - if key not in instances: - instances[key] = super(KeyedInstance, cls).__new__(cls) - return instances[key] - - @classmethod - def _key(cls, *p, **k): - """Given a unique identifier, return a dictionary key - This should be overridden by child classes, to specify which parameters - should determine an object's uniqueness - """ - raise NotImplementedError() - - @classmethod - def clear(cls): - # Allow cls.clear() as well as uniqueInstance.clear(cls) - if str(cls) in cls._instances: - del cls._instances[str(cls)] diff --git a/libs/migrate/versioning/version.py b/libs/migrate/versioning/version.py deleted file mode 100644 index d5a5be98..00000000 --- a/libs/migrate/versioning/version.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os -import re -import shutil -import logging - -from migrate import exceptions -from migrate.versioning import pathed, script -from datetime import datetime - - -log = logging.getLogger(__name__) - -class VerNum(object): - """A version number that behaves like a string and int at the same time""" - - _instances = dict() - - def __new__(cls, value): - val = str(value) - if val not in cls._instances: - cls._instances[val] = super(VerNum, cls).__new__(cls) - ret = cls._instances[val] - return ret - - def __init__(self,value): - self.value = str(int(value)) - if self < 0: - raise ValueError("Version number cannot be negative") - - def __add__(self, value): - ret = int(self) + int(value) - return VerNum(ret) - - def __sub__(self, value): - return self + (int(value) * -1) - - def __cmp__(self, value): - return int(self) - int(value) - - def __repr__(self): - return "" % self.value - - def __str__(self): - return str(self.value) - - def __int__(self): - return int(self.value) - - -class Collection(pathed.Pathed): - """A collection of versioning scripts in a repository""" - - FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*') - - def __init__(self, path): - """Collect current version scripts in repository - and store them in self.versions - """ - super(Collection, self).__init__(path) - - # Create temporary list of files, allowing skipped version numbers. - files = os.listdir(path) - if '1' in files: - # deprecation - raise Exception('It looks like you have a repository in the old ' - 'format (with directories for each version). ' - 'Please convert repository before proceeding.') - - tempVersions = dict() - for filename in files: - match = self.FILENAME_WITH_VERSION.match(filename) - if match: - num = int(match.group(1)) - tempVersions.setdefault(num, []).append(filename) - else: - pass # Must be a helper file or something, let's ignore it. - - # Create the versions member where the keys - # are VerNum's and the values are Version's. - self.versions = dict() - for num, files in tempVersions.items(): - self.versions[VerNum(num)] = Version(num, path, files) - - @property - def latest(self): - """:returns: Latest version in Collection""" - return max([VerNum(0)] + self.versions.keys()) - - def _next_ver_num(self, use_timestamp_numbering): - if use_timestamp_numbering == True: - return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S'))) - else: - return self.latest + 1 - - def create_new_python_version(self, description, **k): - """Create Python files for new version""" - ver = self._next_ver_num(k.pop('use_timestamp_numbering', False)) - extra = str_to_filename(description) - - if extra: - if extra == '_': - extra = '' - elif not extra.startswith('_'): - extra = '_%s' % extra - - filename = '%03d%s.py' % (ver, extra) - filepath = self._version_path(filename) - - script.PythonScript.create(filepath, **k) - self.versions[ver] = Version(ver, self.path, [filename]) - - def create_new_sql_version(self, database, description, **k): - """Create SQL files for new version""" - ver = self._next_ver_num(k.pop('use_timestamp_numbering', False)) - self.versions[ver] = Version(ver, self.path, []) - - extra = str_to_filename(description) - - if extra: - if extra == '_': - extra = '' - elif not extra.startswith('_'): - extra = '_%s' % extra - - # Create new files. - for op in ('upgrade', 'downgrade'): - filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op) - filepath = self._version_path(filename) - script.SqlScript.create(filepath, **k) - self.versions[ver].add_script(filepath) - - def version(self, vernum=None): - """Returns latest Version if vernum is not given. - Otherwise, returns wanted version""" - if vernum is None: - vernum = self.latest - return self.versions[VerNum(vernum)] - - @classmethod - def clear(cls): - super(Collection, cls).clear() - - def _version_path(self, ver): - """Returns path of file in versions repository""" - return os.path.join(self.path, str(ver)) - - -class Version(object): - """A single version in a collection - :param vernum: Version Number - :param path: Path to script files - :param filelist: List of scripts - :type vernum: int, VerNum - :type path: string - :type filelist: list - """ - - def __init__(self, vernum, path, filelist): - self.version = VerNum(vernum) - - # Collect scripts in this folder - self.sql = dict() - self.python = None - - for script in filelist: - self.add_script(os.path.join(path, script)) - - def script(self, database=None, operation=None): - """Returns SQL or Python Script""" - for db in (database, 'default'): - # Try to return a .sql script first - try: - return self.sql[db][operation] - except KeyError: - continue # No .sql script exists - - # TODO: maybe add force Python parameter? - ret = self.python - - assert ret is not None, \ - "There is no script for %d version" % self.version - return ret - - def add_script(self, path): - """Add script to Collection/Version""" - if path.endswith(Extensions.py): - self._add_script_py(path) - elif path.endswith(Extensions.sql): - self._add_script_sql(path) - - SQL_FILENAME = re.compile(r'^.*\.sql') - - def _add_script_sql(self, path): - basename = os.path.basename(path) - match = self.SQL_FILENAME.match(basename) - - if match: - basename = basename.replace('.sql', '') - parts = basename.split('_') - if len(parts) < 3: - raise exceptions.ScriptError( - "Invalid SQL script name %s " % basename + \ - "(needs to be ###_description_database_operation.sql)") - version = parts[0] - op = parts[-1] - dbms = parts[-2] - else: - raise exceptions.ScriptError( - "Invalid SQL script name %s " % basename + \ - "(needs to be ###_description_database_operation.sql)") - - # File the script into a dictionary - self.sql.setdefault(dbms, {})[op] = script.SqlScript(path) - - def _add_script_py(self, path): - if self.python is not None: - raise exceptions.ScriptError('You can only have one Python script ' - 'per version, but you have: %s and %s' % (self.python, path)) - self.python = script.PythonScript(path) - - -class Extensions: - """A namespace for file extensions""" - py = 'py' - sql = 'sql' - -def str_to_filename(s): - """Replaces spaces, (double and single) quotes - and double underscores to underscores - """ - - s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_") - while '__' in s: - s = s.replace('__', '_') - return s diff --git a/libs/sqlalchemy/__init__.py b/libs/sqlalchemy/__init__.py deleted file mode 100644 index f6667f7d..00000000 --- a/libs/sqlalchemy/__init__.py +++ /dev/null @@ -1,133 +0,0 @@ -# sqlalchemy/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -from .sql import ( - alias, - and_, - asc, - between, - bindparam, - case, - cast, - collate, - delete, - desc, - distinct, - except_, - except_all, - exists, - extract, - false, - func, - insert, - intersect, - intersect_all, - join, - literal, - literal_column, - modifier, - not_, - null, - or_, - outerjoin, - outparam, - over, - select, - subquery, - text, - true, - tuple_, - type_coerce, - union, - union_all, - update, - ) - -from .types import ( - BIGINT, - BINARY, - BLOB, - BOOLEAN, - BigInteger, - Binary, - Boolean, - CHAR, - CLOB, - DATE, - DATETIME, - DECIMAL, - Date, - DateTime, - Enum, - FLOAT, - Float, - INT, - INTEGER, - Integer, - Interval, - LargeBinary, - NCHAR, - NVARCHAR, - NUMERIC, - Numeric, - PickleType, - REAL, - SMALLINT, - SmallInteger, - String, - TEXT, - TIME, - TIMESTAMP, - Text, - Time, - TypeDecorator, - Unicode, - UnicodeText, - VARBINARY, - VARCHAR, - ) - - -from .schema import ( - CheckConstraint, - Column, - ColumnDefault, - Constraint, - DefaultClause, - FetchedValue, - ForeignKey, - ForeignKeyConstraint, - Index, - MetaData, - PassiveDefault, - PrimaryKeyConstraint, - Sequence, - Table, - ThreadLocalMetaData, - UniqueConstraint, - DDL, -) - - -from .inspection import inspect -from .engine import create_engine, engine_from_config - -__version__ = '0.9.1' - -def __go(lcls): - global __all__ - - from . import events - from . import util as _sa_util - - import inspect as _inspect - - __all__ = sorted(name for name, obj in lcls.items() - if not (name.startswith('_') or _inspect.ismodule(obj))) - - _sa_util.dependencies.resolve_all("sqlalchemy") -__go(locals()) \ No newline at end of file diff --git a/libs/sqlalchemy/cextension/processors.c b/libs/sqlalchemy/cextension/processors.c deleted file mode 100644 index 003c2e32..00000000 --- a/libs/sqlalchemy/cextension/processors.c +++ /dev/null @@ -1,665 +0,0 @@ -/* -processors.c -Copyright (C) 2010-2014 the SQLAlchemy authors and contributors -Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com - -This module is part of SQLAlchemy and is released under -the MIT License: http://www.opensource.org/licenses/mit-license.php -*/ - -#include -#include - -#define MODULE_NAME "cprocessors" -#define MODULE_DOC "Module containing C versions of data processing functions." - -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#endif - -static PyObject * -int_to_boolean(PyObject *self, PyObject *arg) -{ - long l = 0; - PyObject *res; - - if (arg == Py_None) - Py_RETURN_NONE; - - -#if PY_MAJOR_VERSION >= 3 - l = PyLong_AsLong(arg); -#else - l = PyInt_AsLong(arg); -#endif - if (l == 0) { - res = Py_False; - } else if (l == 1) { - res = Py_True; - } else if ((l == -1) && PyErr_Occurred()) { - /* -1 can be either the actual value, or an error flag. */ - return NULL; - } else { - PyErr_SetString(PyExc_ValueError, - "int_to_boolean only accepts None, 0 or 1"); - return NULL; - } - - Py_INCREF(res); - return res; -} - -static PyObject * -to_str(PyObject *self, PyObject *arg) -{ - if (arg == Py_None) - Py_RETURN_NONE; - - return PyObject_Str(arg); -} - -static PyObject * -to_float(PyObject *self, PyObject *arg) -{ - if (arg == Py_None) - Py_RETURN_NONE; - - return PyNumber_Float(arg); -} - -static PyObject * -str_to_datetime(PyObject *self, PyObject *arg) -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *bytes; - PyObject *err_bytes; -#endif - const char *str; - int numparsed; - unsigned int year, month, day, hour, minute, second, microsecond = 0; - PyObject *err_repr; - - if (arg == Py_None) - Py_RETURN_NONE; - -#if PY_MAJOR_VERSION >= 3 - bytes = PyUnicode_AsASCIIString(arg); - if (bytes == NULL) - str = NULL; - else - str = PyBytes_AS_STRING(bytes); -#else - str = PyString_AsString(arg); -#endif - if (str == NULL) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse datetime string '%.200s' " - "- value is not a string.", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse datetime string '%.200s' " - "- value is not a string.", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - - /* microseconds are optional */ - /* - TODO: this is slightly less picky than the Python version which would - not accept "2000-01-01 00:00:00.". I don't know which is better, but they - should be coherent. - */ - numparsed = sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day, - &hour, &minute, &second, µsecond); -#if PY_MAJOR_VERSION >= 3 - Py_DECREF(bytes); -#endif - if (numparsed < 6) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse datetime string: %.200s", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse datetime string: %.200s", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - return PyDateTime_FromDateAndTime(year, month, day, - hour, minute, second, microsecond); -} - -static PyObject * -str_to_time(PyObject *self, PyObject *arg) -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *bytes; - PyObject *err_bytes; -#endif - const char *str; - int numparsed; - unsigned int hour, minute, second, microsecond = 0; - PyObject *err_repr; - - if (arg == Py_None) - Py_RETURN_NONE; - -#if PY_MAJOR_VERSION >= 3 - bytes = PyUnicode_AsASCIIString(arg); - if (bytes == NULL) - str = NULL; - else - str = PyBytes_AS_STRING(bytes); -#else - str = PyString_AsString(arg); -#endif - if (str == NULL) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; - -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse time string '%.200s' - value is not a string.", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse time string '%.200s' - value is not a string.", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - - /* microseconds are optional */ - /* - TODO: this is slightly less picky than the Python version which would - not accept "00:00:00.". I don't know which is better, but they should be - coherent. - */ - numparsed = sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second, - µsecond); -#if PY_MAJOR_VERSION >= 3 - Py_DECREF(bytes); -#endif - if (numparsed < 3) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse time string: %.200s", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse time string: %.200s", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - return PyTime_FromTime(hour, minute, second, microsecond); -} - -static PyObject * -str_to_date(PyObject *self, PyObject *arg) -{ -#if PY_MAJOR_VERSION >= 3 - PyObject *bytes; - PyObject *err_bytes; -#endif - const char *str; - int numparsed; - unsigned int year, month, day; - PyObject *err_repr; - - if (arg == Py_None) - Py_RETURN_NONE; - -#if PY_MAJOR_VERSION >= 3 - bytes = PyUnicode_AsASCIIString(arg); - if (bytes == NULL) - str = NULL; - else - str = PyBytes_AS_STRING(bytes); -#else - str = PyString_AsString(arg); -#endif - if (str == NULL) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse date string '%.200s' - value is not a string.", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse date string '%.200s' - value is not a string.", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - - numparsed = sscanf(str, "%4u-%2u-%2u", &year, &month, &day); -#if PY_MAJOR_VERSION >= 3 - Py_DECREF(bytes); -#endif - if (numparsed != 3) { - err_repr = PyObject_Repr(arg); - if (err_repr == NULL) - return NULL; -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(err_repr); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_ValueError, - "Couldn't parse date string: %.200s", - PyBytes_AS_STRING(err_bytes)); - Py_DECREF(err_bytes); -#else - PyErr_Format( - PyExc_ValueError, - "Couldn't parse date string: %.200s", - PyString_AsString(err_repr)); -#endif - Py_DECREF(err_repr); - return NULL; - } - return PyDate_FromDate(year, month, day); -} - - -/*********** - * Structs * - ***********/ - -typedef struct { - PyObject_HEAD - PyObject *encoding; - PyObject *errors; -} UnicodeResultProcessor; - -typedef struct { - PyObject_HEAD - PyObject *type; - PyObject *format; -} DecimalResultProcessor; - - - -/************************** - * UnicodeResultProcessor * - **************************/ - -static int -UnicodeResultProcessor_init(UnicodeResultProcessor *self, PyObject *args, - PyObject *kwds) -{ - PyObject *encoding, *errors = NULL; - static char *kwlist[] = {"encoding", "errors", NULL}; - -#if PY_MAJOR_VERSION >= 3 - if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|U:__init__", kwlist, - &encoding, &errors)) - return -1; -#else - if (!PyArg_ParseTupleAndKeywords(args, kwds, "S|S:__init__", kwlist, - &encoding, &errors)) - return -1; -#endif - -#if PY_MAJOR_VERSION >= 3 - encoding = PyUnicode_AsASCIIString(encoding); -#else - Py_INCREF(encoding); -#endif - self->encoding = encoding; - - if (errors) { -#if PY_MAJOR_VERSION >= 3 - errors = PyUnicode_AsASCIIString(errors); -#else - Py_INCREF(errors); -#endif - } else { -#if PY_MAJOR_VERSION >= 3 - errors = PyBytes_FromString("strict"); -#else - errors = PyString_FromString("strict"); -#endif - if (errors == NULL) - return -1; - } - self->errors = errors; - - return 0; -} - -static PyObject * -UnicodeResultProcessor_process(UnicodeResultProcessor *self, PyObject *value) -{ - const char *encoding, *errors; - char *str; - Py_ssize_t len; - - if (value == Py_None) - Py_RETURN_NONE; - -#if PY_MAJOR_VERSION >= 3 - if (PyBytes_AsStringAndSize(value, &str, &len)) - return NULL; - - encoding = PyBytes_AS_STRING(self->encoding); - errors = PyBytes_AS_STRING(self->errors); -#else - if (PyString_AsStringAndSize(value, &str, &len)) - return NULL; - - encoding = PyString_AS_STRING(self->encoding); - errors = PyString_AS_STRING(self->errors); -#endif - - return PyUnicode_Decode(str, len, encoding, errors); -} - -static void -UnicodeResultProcessor_dealloc(UnicodeResultProcessor *self) -{ - Py_XDECREF(self->encoding); - Py_XDECREF(self->errors); -#if PY_MAJOR_VERSION >= 3 - Py_TYPE(self)->tp_free((PyObject*)self); -#else - self->ob_type->tp_free((PyObject*)self); -#endif -} - -static PyMethodDef UnicodeResultProcessor_methods[] = { - {"process", (PyCFunction)UnicodeResultProcessor_process, METH_O, - "The value processor itself."}, - {NULL} /* Sentinel */ -}; - -static PyTypeObject UnicodeResultProcessorType = { - PyVarObject_HEAD_INIT(NULL, 0) - "sqlalchemy.cprocessors.UnicodeResultProcessor", /* tp_name */ - sizeof(UnicodeResultProcessor), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)UnicodeResultProcessor_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - "UnicodeResultProcessor objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - UnicodeResultProcessor_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)UnicodeResultProcessor_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -/************************** - * DecimalResultProcessor * - **************************/ - -static int -DecimalResultProcessor_init(DecimalResultProcessor *self, PyObject *args, - PyObject *kwds) -{ - PyObject *type, *format; - -#if PY_MAJOR_VERSION >= 3 - if (!PyArg_ParseTuple(args, "OU", &type, &format)) -#else - if (!PyArg_ParseTuple(args, "OS", &type, &format)) -#endif - return -1; - - Py_INCREF(type); - self->type = type; - - Py_INCREF(format); - self->format = format; - - return 0; -} - -static PyObject * -DecimalResultProcessor_process(DecimalResultProcessor *self, PyObject *value) -{ - PyObject *str, *result, *args; - - if (value == Py_None) - Py_RETURN_NONE; - - /* Decimal does not accept float values directly */ - /* SQLite can also give us an integer here (see [ticket:2432]) */ - /* XXX: starting with Python 3.1, we could use Decimal.from_float(f), - but the result wouldn't be the same */ - - args = PyTuple_Pack(1, value); - if (args == NULL) - return NULL; - -#if PY_MAJOR_VERSION >= 3 - str = PyUnicode_Format(self->format, args); -#else - str = PyString_Format(self->format, args); -#endif - - Py_DECREF(args); - if (str == NULL) - return NULL; - - result = PyObject_CallFunctionObjArgs(self->type, str, NULL); - Py_DECREF(str); - return result; -} - -static void -DecimalResultProcessor_dealloc(DecimalResultProcessor *self) -{ - Py_XDECREF(self->type); - Py_XDECREF(self->format); -#if PY_MAJOR_VERSION >= 3 - Py_TYPE(self)->tp_free((PyObject*)self); -#else - self->ob_type->tp_free((PyObject*)self); -#endif -} - -static PyMethodDef DecimalResultProcessor_methods[] = { - {"process", (PyCFunction)DecimalResultProcessor_process, METH_O, - "The value processor itself."}, - {NULL} /* Sentinel */ -}; - -static PyTypeObject DecimalResultProcessorType = { - PyVarObject_HEAD_INIT(NULL, 0) - "sqlalchemy.DecimalResultProcessor", /* tp_name */ - sizeof(DecimalResultProcessor), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)DecimalResultProcessor_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - "DecimalResultProcessor objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - DecimalResultProcessor_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)DecimalResultProcessor_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - -static PyMethodDef module_methods[] = { - {"int_to_boolean", int_to_boolean, METH_O, - "Convert an integer to a boolean."}, - {"to_str", to_str, METH_O, - "Convert any value to its string representation."}, - {"to_float", to_float, METH_O, - "Convert any value to its floating point representation."}, - {"str_to_datetime", str_to_datetime, METH_O, - "Convert an ISO string to a datetime.datetime object."}, - {"str_to_time", str_to_time, METH_O, - "Convert an ISO string to a datetime.time object."}, - {"str_to_date", str_to_date, METH_O, - "Convert an ISO string to a datetime.date object."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - - -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef module_def = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_DOC, - -1, - module_methods -}; - -#define INITERROR return NULL - -PyObject * -PyInit_cprocessors(void) - -#else - -#define INITERROR return - -PyMODINIT_FUNC -initcprocessors(void) - -#endif - -{ - PyObject *m; - - UnicodeResultProcessorType.tp_new = PyType_GenericNew; - if (PyType_Ready(&UnicodeResultProcessorType) < 0) - INITERROR; - - DecimalResultProcessorType.tp_new = PyType_GenericNew; - if (PyType_Ready(&DecimalResultProcessorType) < 0) - INITERROR; - -#if PY_MAJOR_VERSION >= 3 - m = PyModule_Create(&module_def); -#else - m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); -#endif - if (m == NULL) - INITERROR; - - PyDateTime_IMPORT; - - Py_INCREF(&UnicodeResultProcessorType); - PyModule_AddObject(m, "UnicodeResultProcessor", - (PyObject *)&UnicodeResultProcessorType); - - Py_INCREF(&DecimalResultProcessorType); - PyModule_AddObject(m, "DecimalResultProcessor", - (PyObject *)&DecimalResultProcessorType); - -#if PY_MAJOR_VERSION >= 3 - return m; -#endif -} diff --git a/libs/sqlalchemy/cextension/resultproxy.c b/libs/sqlalchemy/cextension/resultproxy.c deleted file mode 100644 index 481352a4..00000000 --- a/libs/sqlalchemy/cextension/resultproxy.c +++ /dev/null @@ -1,718 +0,0 @@ -/* -resultproxy.c -Copyright (C) 2010-2014 the SQLAlchemy authors and contributors -Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com - -This module is part of SQLAlchemy and is released under -the MIT License: http://www.opensource.org/licenses/mit-license.php -*/ - -#include - -#define MODULE_NAME "cresultproxy" -#define MODULE_DOC "Module containing C versions of core ResultProxy classes." - -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -typedef Py_ssize_t (*lenfunc)(PyObject *); -#define PyInt_FromSsize_t(x) PyInt_FromLong(x) -typedef intargfunc ssizeargfunc; -#endif - - -/*********** - * Structs * - ***********/ - -typedef struct { - PyObject_HEAD - PyObject *parent; - PyObject *row; - PyObject *processors; - PyObject *keymap; -} BaseRowProxy; - -/**************** - * BaseRowProxy * - ****************/ - -static PyObject * -safe_rowproxy_reconstructor(PyObject *self, PyObject *args) -{ - PyObject *cls, *state, *tmp; - BaseRowProxy *obj; - - if (!PyArg_ParseTuple(args, "OO", &cls, &state)) - return NULL; - - obj = (BaseRowProxy *)PyObject_CallMethod(cls, "__new__", "O", cls); - if (obj == NULL) - return NULL; - - tmp = PyObject_CallMethod((PyObject *)obj, "__setstate__", "O", state); - if (tmp == NULL) { - Py_DECREF(obj); - return NULL; - } - Py_DECREF(tmp); - - if (obj->parent == NULL || obj->row == NULL || - obj->processors == NULL || obj->keymap == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "__setstate__ for BaseRowProxy subclasses must set values " - "for parent, row, processors and keymap"); - Py_DECREF(obj); - return NULL; - } - - return (PyObject *)obj; -} - -static int -BaseRowProxy_init(BaseRowProxy *self, PyObject *args, PyObject *kwds) -{ - PyObject *parent, *row, *processors, *keymap; - - if (!PyArg_UnpackTuple(args, "BaseRowProxy", 4, 4, - &parent, &row, &processors, &keymap)) - return -1; - - Py_INCREF(parent); - self->parent = parent; - - if (!PySequence_Check(row)) { - PyErr_SetString(PyExc_TypeError, "row must be a sequence"); - return -1; - } - Py_INCREF(row); - self->row = row; - - if (!PyList_CheckExact(processors)) { - PyErr_SetString(PyExc_TypeError, "processors must be a list"); - return -1; - } - Py_INCREF(processors); - self->processors = processors; - - if (!PyDict_CheckExact(keymap)) { - PyErr_SetString(PyExc_TypeError, "keymap must be a dict"); - return -1; - } - Py_INCREF(keymap); - self->keymap = keymap; - - return 0; -} - -/* We need the reduce method because otherwise the default implementation - * does very weird stuff for pickle protocol 0 and 1. It calls - * BaseRowProxy.__new__(RowProxy_instance) upon *pickling*. - */ -static PyObject * -BaseRowProxy_reduce(PyObject *self) -{ - PyObject *method, *state; - PyObject *module, *reconstructor, *cls; - - method = PyObject_GetAttrString(self, "__getstate__"); - if (method == NULL) - return NULL; - - state = PyObject_CallObject(method, NULL); - Py_DECREF(method); - if (state == NULL) - return NULL; - - module = PyImport_ImportModule("sqlalchemy.engine.result"); - if (module == NULL) - return NULL; - - reconstructor = PyObject_GetAttrString(module, "rowproxy_reconstructor"); - Py_DECREF(module); - if (reconstructor == NULL) { - Py_DECREF(state); - return NULL; - } - - cls = PyObject_GetAttrString(self, "__class__"); - if (cls == NULL) { - Py_DECREF(reconstructor); - Py_DECREF(state); - return NULL; - } - - return Py_BuildValue("(N(NN))", reconstructor, cls, state); -} - -static void -BaseRowProxy_dealloc(BaseRowProxy *self) -{ - Py_XDECREF(self->parent); - Py_XDECREF(self->row); - Py_XDECREF(self->processors); - Py_XDECREF(self->keymap); -#if PY_MAJOR_VERSION >= 3 - Py_TYPE(self)->tp_free((PyObject *)self); -#else - self->ob_type->tp_free((PyObject *)self); -#endif -} - -static PyObject * -BaseRowProxy_processvalues(PyObject *values, PyObject *processors, int astuple) -{ - Py_ssize_t num_values, num_processors; - PyObject **valueptr, **funcptr, **resultptr; - PyObject *func, *result, *processed_value, *values_fastseq; - - num_values = PySequence_Length(values); - num_processors = PyList_Size(processors); - if (num_values != num_processors) { - PyErr_Format(PyExc_RuntimeError, - "number of values in row (%d) differ from number of column " - "processors (%d)", - (int)num_values, (int)num_processors); - return NULL; - } - - if (astuple) { - result = PyTuple_New(num_values); - } else { - result = PyList_New(num_values); - } - if (result == NULL) - return NULL; - - values_fastseq = PySequence_Fast(values, "row must be a sequence"); - if (values_fastseq == NULL) - return NULL; - - valueptr = PySequence_Fast_ITEMS(values_fastseq); - funcptr = PySequence_Fast_ITEMS(processors); - resultptr = PySequence_Fast_ITEMS(result); - while (--num_values >= 0) { - func = *funcptr; - if (func != Py_None) { - processed_value = PyObject_CallFunctionObjArgs(func, *valueptr, - NULL); - if (processed_value == NULL) { - Py_DECREF(values_fastseq); - Py_DECREF(result); - return NULL; - } - *resultptr = processed_value; - } else { - Py_INCREF(*valueptr); - *resultptr = *valueptr; - } - valueptr++; - funcptr++; - resultptr++; - } - Py_DECREF(values_fastseq); - return result; -} - -static PyListObject * -BaseRowProxy_values(BaseRowProxy *self) -{ - return (PyListObject *)BaseRowProxy_processvalues(self->row, - self->processors, 0); -} - -static PyObject * -BaseRowProxy_iter(BaseRowProxy *self) -{ - PyObject *values, *result; - - values = BaseRowProxy_processvalues(self->row, self->processors, 1); - if (values == NULL) - return NULL; - - result = PyObject_GetIter(values); - Py_DECREF(values); - if (result == NULL) - return NULL; - - return result; -} - -static Py_ssize_t -BaseRowProxy_length(BaseRowProxy *self) -{ - return PySequence_Length(self->row); -} - -static PyObject * -BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) -{ - PyObject *processors, *values; - PyObject *processor, *value, *processed_value; - PyObject *row, *record, *result, *indexobject; - PyObject *exc_module, *exception, *cstr_obj; -#if PY_MAJOR_VERSION >= 3 - PyObject *bytes; -#endif - char *cstr_key; - long index; - int key_fallback = 0; - int tuple_check = 0; - -#if PY_MAJOR_VERSION < 3 - if (PyInt_CheckExact(key)) { - index = PyInt_AS_LONG(key); - } -#endif - - if (PyLong_CheckExact(key)) { - index = PyLong_AsLong(key); - if ((index == -1) && PyErr_Occurred()) - /* -1 can be either the actual value, or an error flag. */ - return NULL; - } else if (PySlice_Check(key)) { - values = PyObject_GetItem(self->row, key); - if (values == NULL) - return NULL; - - processors = PyObject_GetItem(self->processors, key); - if (processors == NULL) { - Py_DECREF(values); - return NULL; - } - - result = BaseRowProxy_processvalues(values, processors, 1); - Py_DECREF(values); - Py_DECREF(processors); - return result; - } else { - record = PyDict_GetItem((PyObject *)self->keymap, key); - if (record == NULL) { - record = PyObject_CallMethod(self->parent, "_key_fallback", - "O", key); - if (record == NULL) - return NULL; - key_fallback = 1; - } - - indexobject = PyTuple_GetItem(record, 2); - if (indexobject == NULL) - return NULL; - - if (key_fallback) { - Py_DECREF(record); - } - - if (indexobject == Py_None) { - exc_module = PyImport_ImportModule("sqlalchemy.exc"); - if (exc_module == NULL) - return NULL; - - exception = PyObject_GetAttrString(exc_module, - "InvalidRequestError"); - Py_DECREF(exc_module); - if (exception == NULL) - return NULL; - - // wow. this seems quite excessive. - cstr_obj = PyObject_Str(key); - if (cstr_obj == NULL) - return NULL; - -/* - FIXME: raise encoding error exception (in both versions below) - if the key contains non-ascii chars, instead of an - InvalidRequestError without any message like in the - python version. -*/ -#if PY_MAJOR_VERSION >= 3 - bytes = PyUnicode_AsASCIIString(cstr_obj); - if (bytes == NULL) - return NULL; - cstr_key = PyBytes_AS_STRING(bytes); -#else - cstr_key = PyString_AsString(cstr_obj); -#endif - if (cstr_key == NULL) { - Py_DECREF(cstr_obj); - return NULL; - } - Py_DECREF(cstr_obj); - - PyErr_Format(exception, - "Ambiguous column name '%.200s' in result set! " - "try 'use_labels' option on select statement.", cstr_key); - return NULL; - } - -#if PY_MAJOR_VERSION >= 3 - index = PyLong_AsLong(indexobject); -#else - index = PyInt_AsLong(indexobject); -#endif - if ((index == -1) && PyErr_Occurred()) - /* -1 can be either the actual value, or an error flag. */ - return NULL; - } - processor = PyList_GetItem(self->processors, index); - if (processor == NULL) - return NULL; - - row = self->row; - if (PyTuple_CheckExact(row)) { - value = PyTuple_GetItem(row, index); - tuple_check = 1; - } - else { - value = PySequence_GetItem(row, index); - tuple_check = 0; - } - - if (value == NULL) - return NULL; - - if (processor != Py_None) { - processed_value = PyObject_CallFunctionObjArgs(processor, value, NULL); - if (!tuple_check) { - Py_DECREF(value); - } - return processed_value; - } else { - if (tuple_check) { - Py_INCREF(value); - } - return value; - } -} - -static PyObject * -BaseRowProxy_getitem(PyObject *self, Py_ssize_t i) -{ - PyObject *index; - -#if PY_MAJOR_VERSION >= 3 - index = PyLong_FromSsize_t(i); -#else - index = PyInt_FromSsize_t(i); -#endif - return BaseRowProxy_subscript((BaseRowProxy*)self, index); -} - -static PyObject * -BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name) -{ - PyObject *tmp; -#if PY_MAJOR_VERSION >= 3 - PyObject *err_bytes; -#endif - - if (!(tmp = PyObject_GenericGetAttr((PyObject *)self, name))) { - if (!PyErr_ExceptionMatches(PyExc_AttributeError)) - return NULL; - PyErr_Clear(); - } - else - return tmp; - - tmp = BaseRowProxy_subscript(self, name); - if (tmp == NULL && PyErr_ExceptionMatches(PyExc_KeyError)) { - -#if PY_MAJOR_VERSION >= 3 - err_bytes = PyUnicode_AsASCIIString(name); - if (err_bytes == NULL) - return NULL; - PyErr_Format( - PyExc_AttributeError, - "Could not locate column in row for column '%.200s'", - PyBytes_AS_STRING(err_bytes) - ); -#else - PyErr_Format( - PyExc_AttributeError, - "Could not locate column in row for column '%.200s'", - PyString_AsString(name) - ); -#endif - return NULL; - } - return tmp; -} - -/*********************** - * getters and setters * - ***********************/ - -static PyObject * -BaseRowProxy_getparent(BaseRowProxy *self, void *closure) -{ - Py_INCREF(self->parent); - return self->parent; -} - -static int -BaseRowProxy_setparent(BaseRowProxy *self, PyObject *value, void *closure) -{ - PyObject *module, *cls; - - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "Cannot delete the 'parent' attribute"); - return -1; - } - - module = PyImport_ImportModule("sqlalchemy.engine.result"); - if (module == NULL) - return -1; - - cls = PyObject_GetAttrString(module, "ResultMetaData"); - Py_DECREF(module); - if (cls == NULL) - return -1; - - if (PyObject_IsInstance(value, cls) != 1) { - PyErr_SetString(PyExc_TypeError, - "The 'parent' attribute value must be an instance of " - "ResultMetaData"); - return -1; - } - Py_DECREF(cls); - Py_XDECREF(self->parent); - Py_INCREF(value); - self->parent = value; - - return 0; -} - -static PyObject * -BaseRowProxy_getrow(BaseRowProxy *self, void *closure) -{ - Py_INCREF(self->row); - return self->row; -} - -static int -BaseRowProxy_setrow(BaseRowProxy *self, PyObject *value, void *closure) -{ - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "Cannot delete the 'row' attribute"); - return -1; - } - - if (!PySequence_Check(value)) { - PyErr_SetString(PyExc_TypeError, - "The 'row' attribute value must be a sequence"); - return -1; - } - - Py_XDECREF(self->row); - Py_INCREF(value); - self->row = value; - - return 0; -} - -static PyObject * -BaseRowProxy_getprocessors(BaseRowProxy *self, void *closure) -{ - Py_INCREF(self->processors); - return self->processors; -} - -static int -BaseRowProxy_setprocessors(BaseRowProxy *self, PyObject *value, void *closure) -{ - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "Cannot delete the 'processors' attribute"); - return -1; - } - - if (!PyList_CheckExact(value)) { - PyErr_SetString(PyExc_TypeError, - "The 'processors' attribute value must be a list"); - return -1; - } - - Py_XDECREF(self->processors); - Py_INCREF(value); - self->processors = value; - - return 0; -} - -static PyObject * -BaseRowProxy_getkeymap(BaseRowProxy *self, void *closure) -{ - Py_INCREF(self->keymap); - return self->keymap; -} - -static int -BaseRowProxy_setkeymap(BaseRowProxy *self, PyObject *value, void *closure) -{ - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "Cannot delete the 'keymap' attribute"); - return -1; - } - - if (!PyDict_CheckExact(value)) { - PyErr_SetString(PyExc_TypeError, - "The 'keymap' attribute value must be a dict"); - return -1; - } - - Py_XDECREF(self->keymap); - Py_INCREF(value); - self->keymap = value; - - return 0; -} - -static PyGetSetDef BaseRowProxy_getseters[] = { - {"_parent", - (getter)BaseRowProxy_getparent, (setter)BaseRowProxy_setparent, - "ResultMetaData", - NULL}, - {"_row", - (getter)BaseRowProxy_getrow, (setter)BaseRowProxy_setrow, - "Original row tuple", - NULL}, - {"_processors", - (getter)BaseRowProxy_getprocessors, (setter)BaseRowProxy_setprocessors, - "list of type processors", - NULL}, - {"_keymap", - (getter)BaseRowProxy_getkeymap, (setter)BaseRowProxy_setkeymap, - "Key to (processor, index) dict", - NULL}, - {NULL} -}; - -static PyMethodDef BaseRowProxy_methods[] = { - {"values", (PyCFunction)BaseRowProxy_values, METH_NOARGS, - "Return the values represented by this BaseRowProxy as a list."}, - {"__reduce__", (PyCFunction)BaseRowProxy_reduce, METH_NOARGS, - "Pickle support method."}, - {NULL} /* Sentinel */ -}; - -static PySequenceMethods BaseRowProxy_as_sequence = { - (lenfunc)BaseRowProxy_length, /* sq_length */ - 0, /* sq_concat */ - 0, /* sq_repeat */ - (ssizeargfunc)BaseRowProxy_getitem, /* sq_item */ - 0, /* sq_slice */ - 0, /* sq_ass_item */ - 0, /* sq_ass_slice */ - 0, /* sq_contains */ - 0, /* sq_inplace_concat */ - 0, /* sq_inplace_repeat */ -}; - -static PyMappingMethods BaseRowProxy_as_mapping = { - (lenfunc)BaseRowProxy_length, /* mp_length */ - (binaryfunc)BaseRowProxy_subscript, /* mp_subscript */ - 0 /* mp_ass_subscript */ -}; - -static PyTypeObject BaseRowProxyType = { - PyVarObject_HEAD_INIT(NULL, 0) - "sqlalchemy.cresultproxy.BaseRowProxy", /* tp_name */ - sizeof(BaseRowProxy), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)BaseRowProxy_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - &BaseRowProxy_as_sequence, /* tp_as_sequence */ - &BaseRowProxy_as_mapping, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - (getattrofunc)BaseRowProxy_getattro,/* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - "BaseRowProxy is a abstract base class for RowProxy", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - (getiterfunc)BaseRowProxy_iter, /* tp_iter */ - 0, /* tp_iternext */ - BaseRowProxy_methods, /* tp_methods */ - 0, /* tp_members */ - BaseRowProxy_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)BaseRowProxy_init, /* tp_init */ - 0, /* tp_alloc */ - 0 /* tp_new */ -}; - -static PyMethodDef module_methods[] = { - {"safe_rowproxy_reconstructor", safe_rowproxy_reconstructor, METH_VARARGS, - "reconstruct a RowProxy instance from its pickled form."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - - -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef module_def = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_DOC, - -1, - module_methods -}; - -#define INITERROR return NULL - -PyObject * -PyInit_cresultproxy(void) - -#else - -#define INITERROR return - -PyMODINIT_FUNC -initcresultproxy(void) - -#endif - -{ - PyObject *m; - - BaseRowProxyType.tp_new = PyType_GenericNew; - if (PyType_Ready(&BaseRowProxyType) < 0) - INITERROR; - -#if PY_MAJOR_VERSION >= 3 - m = PyModule_Create(&module_def); -#else - m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); -#endif - if (m == NULL) - INITERROR; - - Py_INCREF(&BaseRowProxyType); - PyModule_AddObject(m, "BaseRowProxy", (PyObject *)&BaseRowProxyType); - -#if PY_MAJOR_VERSION >= 3 - return m; -#endif -} diff --git a/libs/sqlalchemy/cextension/utils.c b/libs/sqlalchemy/cextension/utils.c deleted file mode 100644 index 3c556e1c..00000000 --- a/libs/sqlalchemy/cextension/utils.c +++ /dev/null @@ -1,225 +0,0 @@ -/* -utils.c -Copyright (C) 2012-2014 the SQLAlchemy authors and contributors - -This module is part of SQLAlchemy and is released under -the MIT License: http://www.opensource.org/licenses/mit-license.php -*/ - -#include - -#define MODULE_NAME "cutils" -#define MODULE_DOC "Module containing C versions of utility functions." - -/* - Given arguments from the calling form *multiparams, **params, - return a list of bind parameter structures, usually a list of - dictionaries. - - In the case of 'raw' execution which accepts positional parameters, - it may be a list of tuples or lists. - - */ -static PyObject * -distill_params(PyObject *self, PyObject *args) -{ - PyObject *multiparams, *params; - PyObject *enclosing_list, *double_enclosing_list; - PyObject *zero_element, *zero_element_item; - Py_ssize_t multiparam_size, zero_element_length; - - if (!PyArg_UnpackTuple(args, "_distill_params", 2, 2, &multiparams, ¶ms)) { - return NULL; - } - - if (multiparams != Py_None) { - multiparam_size = PyTuple_Size(multiparams); - if (multiparam_size < 0) { - return NULL; - } - } - else { - multiparam_size = 0; - } - - if (multiparam_size == 0) { - if (params != Py_None && PyDict_Size(params) != 0) { - enclosing_list = PyList_New(1); - if (enclosing_list == NULL) { - return NULL; - } - Py_INCREF(params); - if (PyList_SetItem(enclosing_list, 0, params) == -1) { - Py_DECREF(params); - Py_DECREF(enclosing_list); - return NULL; - } - } - else { - enclosing_list = PyList_New(0); - if (enclosing_list == NULL) { - return NULL; - } - } - return enclosing_list; - } - else if (multiparam_size == 1) { - zero_element = PyTuple_GetItem(multiparams, 0); - if (PyTuple_Check(zero_element) || PyList_Check(zero_element)) { - zero_element_length = PySequence_Length(zero_element); - - if (zero_element_length != 0) { - zero_element_item = PySequence_GetItem(zero_element, 0); - if (zero_element_item == NULL) { - return NULL; - } - } - else { - zero_element_item = NULL; - } - - if (zero_element_length == 0 || - ( - PyObject_HasAttrString(zero_element_item, "__iter__") && - !PyObject_HasAttrString(zero_element_item, "strip") - ) - ) { - /* - * execute(stmt, [{}, {}, {}, ...]) - * execute(stmt, [(), (), (), ...]) - */ - Py_XDECREF(zero_element_item); - Py_INCREF(zero_element); - return zero_element; - } - else { - /* - * execute(stmt, ("value", "value")) - */ - Py_XDECREF(zero_element_item); - enclosing_list = PyList_New(1); - if (enclosing_list == NULL) { - return NULL; - } - Py_INCREF(zero_element); - if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { - Py_DECREF(zero_element); - Py_DECREF(enclosing_list); - return NULL; - } - return enclosing_list; - } - } - else if (PyObject_HasAttrString(zero_element, "keys")) { - /* - * execute(stmt, {"key":"value"}) - */ - enclosing_list = PyList_New(1); - if (enclosing_list == NULL) { - return NULL; - } - Py_INCREF(zero_element); - if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { - Py_DECREF(zero_element); - Py_DECREF(enclosing_list); - return NULL; - } - return enclosing_list; - } else { - enclosing_list = PyList_New(1); - if (enclosing_list == NULL) { - return NULL; - } - double_enclosing_list = PyList_New(1); - if (double_enclosing_list == NULL) { - Py_DECREF(enclosing_list); - return NULL; - } - Py_INCREF(zero_element); - if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) { - Py_DECREF(zero_element); - Py_DECREF(enclosing_list); - Py_DECREF(double_enclosing_list); - return NULL; - } - if (PyList_SetItem(double_enclosing_list, 0, enclosing_list) == -1) { - Py_DECREF(zero_element); - Py_DECREF(enclosing_list); - Py_DECREF(double_enclosing_list); - return NULL; - } - return double_enclosing_list; - } - } - else { - zero_element = PyTuple_GetItem(multiparams, 0); - if (PyObject_HasAttrString(zero_element, "__iter__") && - !PyObject_HasAttrString(zero_element, "strip") - ) { - Py_INCREF(multiparams); - return multiparams; - } - else { - enclosing_list = PyList_New(1); - if (enclosing_list == NULL) { - return NULL; - } - Py_INCREF(multiparams); - if (PyList_SetItem(enclosing_list, 0, multiparams) == -1) { - Py_DECREF(multiparams); - Py_DECREF(enclosing_list); - return NULL; - } - return enclosing_list; - } - } -} - -static PyMethodDef module_methods[] = { - {"_distill_params", distill_params, METH_VARARGS, - "Distill an execute() parameter structure."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - -#if PY_MAJOR_VERSION >= 3 - -static struct PyModuleDef module_def = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_DOC, - -1, - module_methods - }; -#endif - - -#if PY_MAJOR_VERSION >= 3 -PyObject * -PyInit_cutils(void) -#else -PyMODINIT_FUNC -initcutils(void) -#endif -{ - PyObject *m; - -#if PY_MAJOR_VERSION >= 3 - m = PyModule_Create(&module_def); -#else - m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC); -#endif - -#if PY_MAJOR_VERSION >= 3 - if (m == NULL) - return NULL; - return m; -#else - if (m == NULL) - return; -#endif -} - diff --git a/libs/sqlalchemy/connectors/__init__.py b/libs/sqlalchemy/connectors/__init__.py deleted file mode 100644 index 761024fe..00000000 --- a/libs/sqlalchemy/connectors/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# connectors/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -class Connector(object): - pass diff --git a/libs/sqlalchemy/connectors/mxodbc.py b/libs/sqlalchemy/connectors/mxodbc.py deleted file mode 100644 index e5562a25..00000000 --- a/libs/sqlalchemy/connectors/mxodbc.py +++ /dev/null @@ -1,149 +0,0 @@ -# connectors/mxodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -Provide an SQLALchemy connector for the eGenix mxODBC commercial -Python adapter for ODBC. This is not a free product, but eGenix -provides SQLAlchemy with a license for use in continuous integration -testing. - -This has been tested for use with mxODBC 3.1.2 on SQL Server 2005 -and 2008, using the SQL Server Native driver. However, it is -possible for this to be used on other database platforms. - -For more info on mxODBC, see http://www.egenix.com/ - -""" - -import sys -import re -import warnings - -from . import Connector - - -class MxODBCConnector(Connector): - driver = 'mxodbc' - - supports_sane_multi_rowcount = False - supports_unicode_statements = True - supports_unicode_binds = True - - supports_native_decimal = True - - @classmethod - def dbapi(cls): - # this classmethod will normally be replaced by an instance - # attribute of the same name, so this is normally only called once. - cls._load_mx_exceptions() - platform = sys.platform - if platform == 'win32': - from mx.ODBC import Windows as module - # this can be the string "linux2", and possibly others - elif 'linux' in platform: - from mx.ODBC import unixODBC as module - elif platform == 'darwin': - from mx.ODBC import iODBC as module - else: - raise ImportError("Unrecognized platform for mxODBC import") - return module - - @classmethod - def _load_mx_exceptions(cls): - """ Import mxODBC exception classes into the module namespace, - as if they had been imported normally. This is done here - to avoid requiring all SQLAlchemy users to install mxODBC. - """ - global InterfaceError, ProgrammingError - from mx.ODBC import InterfaceError - from mx.ODBC import ProgrammingError - - def on_connect(self): - def connect(conn): - conn.stringformat = self.dbapi.MIXED_STRINGFORMAT - conn.datetimeformat = self.dbapi.PYDATETIME_DATETIMEFORMAT - conn.decimalformat = self.dbapi.DECIMAL_DECIMALFORMAT - conn.errorhandler = self._error_handler() - return connect - - def _error_handler(self): - """ Return a handler that adjusts mxODBC's raised Warnings to - emit Python standard warnings. - """ - from mx.ODBC.Error import Warning as MxOdbcWarning - - def error_handler(connection, cursor, errorclass, errorvalue): - if issubclass(errorclass, MxOdbcWarning): - errorclass.__bases__ = (Warning,) - warnings.warn(message=str(errorvalue), - category=errorclass, - stacklevel=2) - else: - raise errorclass(errorvalue) - return error_handler - - def create_connect_args(self, url): - """ Return a tuple of *args,**kwargs for creating a connection. - - The mxODBC 3.x connection constructor looks like this: - - connect(dsn, user='', password='', - clear_auto_commit=1, errorhandler=None) - - This method translates the values in the provided uri - into args and kwargs needed to instantiate an mxODBC Connection. - - The arg 'errorhandler' is not used by SQLAlchemy and will - not be populated. - - """ - opts = url.translate_connect_args(username='user') - opts.update(url.query) - args = opts.pop('host') - opts.pop('port', None) - opts.pop('database', None) - return (args,), opts - - def is_disconnect(self, e, connection, cursor): - # TODO: eGenix recommends checking connection.closed here - # Does that detect dropped connections ? - if isinstance(e, self.dbapi.ProgrammingError): - return "connection already closed" in str(e) - elif isinstance(e, self.dbapi.Error): - return '[08S01]' in str(e) - else: - return False - - def _get_server_version_info(self, connection): - # eGenix suggests using conn.dbms_version instead - # of what we're doing here - dbapi_con = connection.connection - version = [] - r = re.compile('[.\-]') - # 18 == pyodbc.SQL_DBMS_VER - for n in r.split(dbapi_con.getinfo(18)[1]): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) - - def _get_direct(self, context): - if context: - native_odbc_execute = context.execution_options.\ - get('native_odbc_execute', 'auto') - # default to direct=True in all cases, is more generally - # compatible especially with SQL Server - return False if native_odbc_execute is True else True - else: - return True - - def do_executemany(self, cursor, statement, parameters, context=None): - cursor.executemany( - statement, parameters, direct=self._get_direct(context)) - - def do_execute(self, cursor, statement, parameters, context=None): - cursor.execute(statement, parameters, direct=self._get_direct(context)) diff --git a/libs/sqlalchemy/connectors/mysqldb.py b/libs/sqlalchemy/connectors/mysqldb.py deleted file mode 100644 index 0f250dfd..00000000 --- a/libs/sqlalchemy/connectors/mysqldb.py +++ /dev/null @@ -1,162 +0,0 @@ -# connectors/mysqldb.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Define behaviors common to MySQLdb dialects. - -Currently includes MySQL and Drizzle. - -""" - -from . import Connector -from ..engine import base as engine_base, default -from ..sql import operators as sql_operators -from .. import exc, log, schema, sql, types as sqltypes, util, processors -import re - - -# the subclassing of Connector by all classes -# here is not strictly necessary - - -class MySQLDBExecutionContext(Connector): - - @property - def rowcount(self): - if hasattr(self, '_rowcount'): - return self._rowcount - else: - return self.cursor.rowcount - - -class MySQLDBCompiler(Connector): - def visit_mod_binary(self, binary, operator, **kw): - return self.process(binary.left, **kw) + " %% " + \ - self.process(binary.right, **kw) - - def post_process_text(self, text): - return text.replace('%', '%%') - - -class MySQLDBIdentifierPreparer(Connector): - - def _escape_identifier(self, value): - value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace("%", "%%") - - -class MySQLDBConnector(Connector): - driver = 'mysqldb' - supports_unicode_statements = False - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - - supports_native_decimal = True - - default_paramstyle = 'format' - - @classmethod - def dbapi(cls): - # is overridden when pymysql is used - return __import__('MySQLdb') - - def do_executemany(self, cursor, statement, parameters, context=None): - rowcount = cursor.executemany(statement, parameters) - if context is not None: - context._rowcount = rowcount - - def create_connect_args(self, url): - opts = url.translate_connect_args(database='db', username='user', - password='passwd') - opts.update(url.query) - - util.coerce_kw_type(opts, 'compress', bool) - util.coerce_kw_type(opts, 'connect_timeout', int) - util.coerce_kw_type(opts, 'read_timeout', int) - util.coerce_kw_type(opts, 'client_flag', int) - util.coerce_kw_type(opts, 'local_infile', int) - # Note: using either of the below will cause all strings to be returned - # as Unicode, both in raw SQL operations and with column types like - # String and MSString. - util.coerce_kw_type(opts, 'use_unicode', bool) - util.coerce_kw_type(opts, 'charset', str) - - # Rich values 'cursorclass' and 'conv' are not supported via - # query string. - - ssl = {} - keys = ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher'] - for key in keys: - if key in opts: - ssl[key[4:]] = opts[key] - util.coerce_kw_type(ssl, key[4:], str) - del opts[key] - if ssl: - opts['ssl'] = ssl - - # FOUND_ROWS must be set in CLIENT_FLAGS to enable - # supports_sane_rowcount. - client_flag = opts.get('client_flag', 0) - if self.dbapi is not None: - try: - CLIENT_FLAGS = __import__( - self.dbapi.__name__ + '.constants.CLIENT' - ).constants.CLIENT - client_flag |= CLIENT_FLAGS.FOUND_ROWS - except (AttributeError, ImportError): - self.supports_sane_rowcount = False - opts['client_flag'] = client_flag - return [[], opts] - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - version = [] - r = re.compile('[.\-]') - for n in r.split(dbapi_con.get_server_info()): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) - - def _extract_error_code(self, exception): - return exception.args[0] - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - # Note: MySQL-python 1.2.1c7 seems to ignore changes made - # on a connection via set_character_set() - if self.server_version_info < (4, 1, 0): - try: - return connection.connection.character_set_name() - except AttributeError: - # < 1.2.1 final MySQL-python drivers have no charset support. - # a query is needed. - pass - - # Prefer 'character_set_results' for the current connection over the - # value in the driver. SET NAMES or individual variable SETs will - # change the charset without updating the driver's view of the world. - # - # If it's decided that issuing that sort of SQL leaves you SOL, then - # this can prefer the driver value. - rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'") - opts = dict([(row[0], row[1]) for row in self._compat_fetchall(rs)]) - - if 'character_set_results' in opts: - return opts['character_set_results'] - try: - return connection.connection.character_set_name() - except AttributeError: - # Still no charset on < 1.2.1 final... - if 'character_set' in opts: - return opts['character_set'] - else: - util.warn( - "Could not detect the connection character set with this " - "combination of MySQL server and MySQL-python. " - "MySQL-python >= 1.2.2 is recommended. Assuming latin1.") - return 'latin1' diff --git a/libs/sqlalchemy/connectors/pyodbc.py b/libs/sqlalchemy/connectors/pyodbc.py deleted file mode 100644 index 284de288..00000000 --- a/libs/sqlalchemy/connectors/pyodbc.py +++ /dev/null @@ -1,170 +0,0 @@ -# connectors/pyodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from . import Connector -from .. import util - - -import sys -import re - - -class PyODBCConnector(Connector): - driver = 'pyodbc' - - supports_sane_multi_rowcount = False - - if util.py2k: - # PyODBC unicode is broken on UCS-4 builds - supports_unicode = sys.maxunicode == 65535 - supports_unicode_statements = supports_unicode - - supports_native_decimal = True - default_paramstyle = 'named' - - # for non-DSN connections, this should - # hold the desired driver name - pyodbc_driver_name = None - - # will be set to True after initialize() - # if the freetds.so is detected - freetds = False - - # will be set to the string version of - # the FreeTDS driver if freetds is detected - freetds_driver_version = None - - # will be set to True after initialize() - # if the libessqlsrv.so is detected - easysoft = False - - def __init__(self, supports_unicode_binds=None, **kw): - super(PyODBCConnector, self).__init__(**kw) - self._user_supports_unicode_binds = supports_unicode_binds - - @classmethod - def dbapi(cls): - return __import__('pyodbc') - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - opts.update(url.query) - - keys = opts - query = url.query - - connect_args = {} - for param in ('ansi', 'unicode_results', 'autocommit'): - if param in keys: - connect_args[param] = util.asbool(keys.pop(param)) - - if 'odbc_connect' in keys: - connectors = [util.unquote_plus(keys.pop('odbc_connect'))] - else: - dsn_connection = 'dsn' in keys or \ - ('host' in keys and 'database' not in keys) - if dsn_connection: - connectors = ['dsn=%s' % (keys.pop('host', '') or \ - keys.pop('dsn', ''))] - else: - port = '' - if 'port' in keys and not 'port' in query: - port = ',%d' % int(keys.pop('port')) - - connectors = ["DRIVER={%s}" % - keys.pop('driver', self.pyodbc_driver_name), - 'Server=%s%s' % (keys.pop('host', ''), port), - 'Database=%s' % keys.pop('database', '')] - - user = keys.pop("user", None) - if user: - connectors.append("UID=%s" % user) - connectors.append("PWD=%s" % keys.pop('password', '')) - else: - connectors.append("Trusted_Connection=Yes") - - # if set to 'Yes', the ODBC layer will try to automagically - # convert textual data from your database encoding to your - # client encoding. This should obviously be set to 'No' if - # you query a cp1253 encoded database from a latin1 client... - if 'odbc_autotranslate' in keys: - connectors.append("AutoTranslate=%s" % - keys.pop("odbc_autotranslate")) - - connectors.extend(['%s=%s' % (k, v) for k, v in keys.items()]) - return [[";".join(connectors)], connect_args] - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.ProgrammingError): - return "The cursor's connection has been closed." in str(e) or \ - 'Attempt to use a closed connection.' in str(e) - elif isinstance(e, self.dbapi.Error): - return '[08S01]' in str(e) - else: - return False - - def initialize(self, connection): - # determine FreeTDS first. can't issue SQL easily - # without getting unicode_statements/binds set up. - - pyodbc = self.dbapi - - dbapi_con = connection.connection - - _sql_driver_name = dbapi_con.getinfo(pyodbc.SQL_DRIVER_NAME) - self.freetds = bool(re.match(r".*libtdsodbc.*\.so", _sql_driver_name - )) - self.easysoft = bool(re.match(r".*libessqlsrv.*\.so", _sql_driver_name - )) - - if self.freetds: - self.freetds_driver_version = dbapi_con.getinfo( - pyodbc.SQL_DRIVER_VER) - - self.supports_unicode_statements = ( - not util.py2k or - (not self.freetds and not self.easysoft) - ) - - if self._user_supports_unicode_binds is not None: - self.supports_unicode_binds = self._user_supports_unicode_binds - elif util.py2k: - self.supports_unicode_binds = ( - not self.freetds or self.freetds_driver_version >= '0.91' - ) and not self.easysoft - else: - self.supports_unicode_binds = True - - # run other initialization which asks for user name, etc. - super(PyODBCConnector, self).initialize(connection) - - def _dbapi_version(self): - if not self.dbapi: - return () - return self._parse_dbapi_version(self.dbapi.version) - - def _parse_dbapi_version(self, vers): - m = re.match( - r'(?:py.*-)?([\d\.]+)(?:-(\w+))?', - vers - ) - if not m: - return () - vers = tuple([int(x) for x in m.group(1).split(".")]) - if m.group(2): - vers += (m.group(2),) - return vers - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - version = [] - r = re.compile('[.\-]') - for n in r.split(dbapi_con.getinfo(self.dbapi.SQL_DBMS_VER)): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) diff --git a/libs/sqlalchemy/connectors/zxJDBC.py b/libs/sqlalchemy/connectors/zxJDBC.py deleted file mode 100644 index e0bbc573..00000000 --- a/libs/sqlalchemy/connectors/zxJDBC.py +++ /dev/null @@ -1,59 +0,0 @@ -# connectors/zxJDBC.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -import sys -from . import Connector - - -class ZxJDBCConnector(Connector): - driver = 'zxjdbc' - - supports_sane_rowcount = False - supports_sane_multi_rowcount = False - - supports_unicode_binds = True - supports_unicode_statements = sys.version > '2.5.0+' - description_encoding = None - default_paramstyle = 'qmark' - - jdbc_db_name = None - jdbc_driver_name = None - - @classmethod - def dbapi(cls): - from com.ziclix.python.sql import zxJDBC - return zxJDBC - - def _driver_kwargs(self): - """Return kw arg dict to be sent to connect().""" - return {} - - def _create_jdbc_url(self, url): - """Create a JDBC url from a :class:`~sqlalchemy.engine.url.URL`""" - return 'jdbc:%s://%s%s/%s' % (self.jdbc_db_name, url.host, - url.port is not None - and ':%s' % url.port or '', - url.database) - - def create_connect_args(self, url): - opts = self._driver_kwargs() - opts.update(url.query) - return [ - [self._create_jdbc_url(url), - url.username, url.password, - self.jdbc_driver_name], - opts] - - def is_disconnect(self, e, connection, cursor): - if not isinstance(e, self.dbapi.ProgrammingError): - return False - e = str(e) - return 'connection is closed' in e or 'cursor is closed' in e - - def _get_server_version_info(self, connection): - # use connection.connection.dbversion, and parse appropriately - # to get a tuple - raise NotImplementedError() diff --git a/libs/sqlalchemy/databases/__init__.py b/libs/sqlalchemy/databases/__init__.py deleted file mode 100644 index 915eefa4..00000000 --- a/libs/sqlalchemy/databases/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# databases/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Include imports from the sqlalchemy.dialects package for backwards -compatibility with pre 0.6 versions. - -""" -from ..dialects.sqlite import base as sqlite -from ..dialects.postgresql import base as postgresql -postgres = postgresql -from ..dialects.mysql import base as mysql -from ..dialects.drizzle import base as drizzle -from ..dialects.oracle import base as oracle -from ..dialects.firebird import base as firebird -from ..dialects.mssql import base as mssql -from ..dialects.sybase import base as sybase - - -__all__ = ( - 'drizzle', - 'firebird', - 'mssql', - 'mysql', - 'postgresql', - 'sqlite', - 'oracle', - 'sybase', - ) diff --git a/libs/sqlalchemy/dialects/__init__.py b/libs/sqlalchemy/dialects/__init__.py deleted file mode 100644 index 974d4f78..00000000 --- a/libs/sqlalchemy/dialects/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# dialects/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -__all__ = ( - 'drizzle', - 'firebird', - 'mssql', - 'mysql', - 'oracle', - 'postgresql', - 'sqlite', - 'sybase', - ) - -from .. import util - -def _auto_fn(name): - """default dialect importer. - - plugs into the :class:`.PluginLoader` - as a first-hit system. - - """ - if "." in name: - dialect, driver = name.split(".") - else: - dialect = name - driver = "base" - try: - module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects - except ImportError: - return None - - module = getattr(module, dialect) - if hasattr(module, driver): - module = getattr(module, driver) - return lambda: module.dialect - else: - return None - -registry = util.PluginLoader("sqlalchemy.dialects", auto_fn=_auto_fn) diff --git a/libs/sqlalchemy/dialects/drizzle/__init__.py b/libs/sqlalchemy/dialects/drizzle/__init__.py deleted file mode 100644 index 1392b8e2..00000000 --- a/libs/sqlalchemy/dialects/drizzle/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from sqlalchemy.dialects.drizzle import base, mysqldb - -base.dialect = mysqldb.dialect - -from sqlalchemy.dialects.drizzle.base import \ - BIGINT, BINARY, BLOB, \ - BOOLEAN, CHAR, DATE, \ - DATETIME, DECIMAL, DOUBLE, \ - ENUM, FLOAT, INTEGER, \ - NUMERIC, REAL, TEXT, \ - TIME, TIMESTAMP, VARBINARY, \ - VARCHAR, dialect - -__all__ = ( - 'BIGINT', 'BINARY', 'BLOB', - 'BOOLEAN', 'CHAR', 'DATE', - 'DATETIME', 'DECIMAL', 'DOUBLE', - 'ENUM', 'FLOAT', 'INTEGER', - 'NUMERIC', 'REAL', 'TEXT', - 'TIME', 'TIMESTAMP', 'VARBINARY', - 'VARCHAR', 'dialect' -) diff --git a/libs/sqlalchemy/dialects/drizzle/base.py b/libs/sqlalchemy/dialects/drizzle/base.py deleted file mode 100644 index b5addb42..00000000 --- a/libs/sqlalchemy/dialects/drizzle/base.py +++ /dev/null @@ -1,498 +0,0 @@ -# drizzle/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# Copyright (C) 2010-2011 Monty Taylor -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -""" - -.. dialect:: drizzle - :name: Drizzle - -Drizzle is a variant of MySQL. Unlike MySQL, Drizzle's default storage engine -is InnoDB (transactions, foreign-keys) rather than MyISAM. For more -`Notable Differences `_, visit -the `Drizzle Documentation `_. - -The SQLAlchemy Drizzle dialect leans heavily on the MySQL dialect, so much of -the :doc:`SQLAlchemy MySQL ` documentation is also relevant. - - -""" - -from sqlalchemy import exc -from sqlalchemy import log -from sqlalchemy import types as sqltypes -from sqlalchemy.engine import reflection -from sqlalchemy.dialects.mysql import base as mysql_dialect -from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \ - BLOB, BINARY, VARBINARY - - -class _NumericType(object): - """Base for Drizzle numeric types.""" - - def __init__(self, **kw): - super(_NumericType, self).__init__(**kw) - - -class _FloatType(_NumericType, sqltypes.Float): - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - if isinstance(self, (REAL, DOUBLE)) and \ - ( - (precision is None and scale is not None) or - (precision is not None and scale is None) - ): - raise exc.ArgumentError( - "You must specify both precision and scale or omit " - "both altogether.") - - super(_FloatType, self).__init__(precision=precision, - asdecimal=asdecimal, **kw) - self.scale = scale - - -class _StringType(mysql_dialect._StringType): - """Base for Drizzle string types.""" - - def __init__(self, collation=None, binary=False, **kw): - kw['national'] = False - super(_StringType, self).__init__(collation=collation, binary=binary, - **kw) - - -class NUMERIC(_NumericType, sqltypes.NUMERIC): - """Drizzle NUMERIC type.""" - - __visit_name__ = 'NUMERIC' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a NUMERIC. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - """ - - super(NUMERIC, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class DECIMAL(_NumericType, sqltypes.DECIMAL): - """Drizzle DECIMAL type.""" - - __visit_name__ = 'DECIMAL' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a DECIMAL. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - """ - super(DECIMAL, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class DOUBLE(_FloatType): - """Drizzle DOUBLE type.""" - - __visit_name__ = 'DOUBLE' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a DOUBLE. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - """ - - super(DOUBLE, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class REAL(_FloatType, sqltypes.REAL): - """Drizzle REAL type.""" - - __visit_name__ = 'REAL' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a REAL. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - """ - - super(REAL, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class FLOAT(_FloatType, sqltypes.FLOAT): - """Drizzle FLOAT type.""" - - __visit_name__ = 'FLOAT' - - def __init__(self, precision=None, scale=None, asdecimal=False, **kw): - """Construct a FLOAT. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - """ - - super(FLOAT, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - def bind_processor(self, dialect): - return None - - -class INTEGER(sqltypes.INTEGER): - """Drizzle INTEGER type.""" - - __visit_name__ = 'INTEGER' - - def __init__(self, **kw): - """Construct an INTEGER.""" - - super(INTEGER, self).__init__(**kw) - - -class BIGINT(sqltypes.BIGINT): - """Drizzle BIGINTEGER type.""" - - __visit_name__ = 'BIGINT' - - def __init__(self, **kw): - """Construct a BIGINTEGER.""" - - super(BIGINT, self).__init__(**kw) - - -class TIME(mysql_dialect.TIME): - """Drizzle TIME type.""" - - -class TIMESTAMP(sqltypes.TIMESTAMP): - """Drizzle TIMESTAMP type.""" - - __visit_name__ = 'TIMESTAMP' - - -class TEXT(_StringType, sqltypes.TEXT): - """Drizzle TEXT type, for text up to 2^16 characters.""" - - __visit_name__ = 'TEXT' - - def __init__(self, length=None, **kw): - """Construct a TEXT. - - :param length: Optional, if provided the server may optimize storage - by substituting the smallest TEXT type sufficient to store - ``length`` characters. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - - super(TEXT, self).__init__(length=length, **kw) - - -class VARCHAR(_StringType, sqltypes.VARCHAR): - """Drizzle VARCHAR type, for variable-length character data.""" - - __visit_name__ = 'VARCHAR' - - def __init__(self, length=None, **kwargs): - """Construct a VARCHAR. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - - super(VARCHAR, self).__init__(length=length, **kwargs) - - -class CHAR(_StringType, sqltypes.CHAR): - """Drizzle CHAR type, for fixed-length character data.""" - - __visit_name__ = 'CHAR' - - def __init__(self, length=None, **kwargs): - """Construct a CHAR. - - :param length: Maximum data length, in characters. - - :param binary: Optional, use the default binary collation for the - national character set. This does not affect the type of data - stored, use a BINARY type for binary data. - - :param collation: Optional, request a particular collation. Must be - compatible with the national character set. - - """ - - super(CHAR, self).__init__(length=length, **kwargs) - - -class ENUM(mysql_dialect.ENUM): - """Drizzle ENUM type.""" - - def __init__(self, *enums, **kw): - """Construct an ENUM. - - Example: - - Column('myenum', ENUM("foo", "bar", "baz")) - - :param enums: The range of valid values for this ENUM. Values will be - quoted when generating the schema according to the quoting flag (see - below). - - :param strict: Defaults to False: ensure that a given value is in this - ENUM's range of permissible values when inserting or updating rows. - Note that Drizzle will not raise a fatal error if you attempt to - store an out of range value- an alternate value will be stored - instead. - (See Drizzle ENUM documentation.) - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - :param quoting: Defaults to 'auto': automatically determine enum value - quoting. If all enum values are surrounded by the same quoting - character, then use 'quoted' mode. Otherwise, use 'unquoted' mode. - - 'quoted': values in enums are already quoted, they will be used - directly when generating the schema - this usage is deprecated. - - 'unquoted': values in enums are not quoted, they will be escaped and - surrounded by single quotes when generating the schema. - - Previous versions of this type always required manually quoted - values to be supplied; future versions will always quote the string - literals for you. This is a transitional option. - - """ - - super(ENUM, self).__init__(*enums, **kw) - - -class _DrizzleBoolean(sqltypes.Boolean): - def get_dbapi_type(self, dbapi): - return dbapi.NUMERIC - - -colspecs = { - sqltypes.Numeric: NUMERIC, - sqltypes.Float: FLOAT, - sqltypes.Time: TIME, - sqltypes.Enum: ENUM, - sqltypes.Boolean: _DrizzleBoolean, -} - - -# All the types we have in Drizzle -ischema_names = { - 'BIGINT': BIGINT, - 'BINARY': BINARY, - 'BLOB': BLOB, - 'BOOLEAN': BOOLEAN, - 'CHAR': CHAR, - 'DATE': DATE, - 'DATETIME': DATETIME, - 'DECIMAL': DECIMAL, - 'DOUBLE': DOUBLE, - 'ENUM': ENUM, - 'FLOAT': FLOAT, - 'INT': INTEGER, - 'INTEGER': INTEGER, - 'NUMERIC': NUMERIC, - 'TEXT': TEXT, - 'TIME': TIME, - 'TIMESTAMP': TIMESTAMP, - 'VARBINARY': VARBINARY, - 'VARCHAR': VARCHAR, -} - - -class DrizzleCompiler(mysql_dialect.MySQLCompiler): - - def visit_typeclause(self, typeclause): - type_ = typeclause.type.dialect_impl(self.dialect) - if isinstance(type_, sqltypes.Integer): - return 'INTEGER' - else: - return super(DrizzleCompiler, self).visit_typeclause(typeclause) - - def visit_cast(self, cast, **kwargs): - type_ = self.process(cast.typeclause) - if type_ is None: - return self.process(cast.clause) - - return 'CAST(%s AS %s)' % (self.process(cast.clause), type_) - - -class DrizzleDDLCompiler(mysql_dialect.MySQLDDLCompiler): - pass - - -class DrizzleTypeCompiler(mysql_dialect.MySQLTypeCompiler): - def _extend_numeric(self, type_, spec): - return spec - - def _extend_string(self, type_, defaults, spec): - """Extend a string-type declaration with standard SQL - COLLATE annotations and Drizzle specific extensions. - - """ - - def attr(name): - return getattr(type_, name, defaults.get(name)) - - if attr('collation'): - collation = 'COLLATE %s' % type_.collation - elif attr('binary'): - collation = 'BINARY' - else: - collation = None - - return ' '.join([c for c in (spec, collation) - if c is not None]) - - def visit_NCHAR(self, type): - raise NotImplementedError("Drizzle does not support NCHAR") - - def visit_NVARCHAR(self, type): - raise NotImplementedError("Drizzle does not support NVARCHAR") - - def visit_FLOAT(self, type_): - if type_.scale is not None and type_.precision is not None: - return "FLOAT(%s, %s)" % (type_.precision, type_.scale) - else: - return "FLOAT" - - def visit_BOOLEAN(self, type_): - return "BOOLEAN" - - def visit_BLOB(self, type_): - return "BLOB" - - -class DrizzleExecutionContext(mysql_dialect.MySQLExecutionContext): - pass - - -class DrizzleIdentifierPreparer(mysql_dialect.MySQLIdentifierPreparer): - pass - - -@log.class_logger -class DrizzleDialect(mysql_dialect.MySQLDialect): - """Details of the Drizzle dialect. - - Not used directly in application code. - """ - - name = 'drizzle' - - _supports_cast = True - supports_sequences = False - supports_native_boolean = True - supports_views = False - - default_paramstyle = 'format' - colspecs = colspecs - - statement_compiler = DrizzleCompiler - ddl_compiler = DrizzleDDLCompiler - type_compiler = DrizzleTypeCompiler - ischema_names = ischema_names - preparer = DrizzleIdentifierPreparer - - def on_connect(self): - """Force autocommit - Drizzle Bug#707842 doesn't set this properly""" - - def connect(conn): - conn.autocommit(False) - return connect - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - """Return a Unicode SHOW TABLES from a given schema.""" - - if schema is not None: - current_schema = schema - else: - current_schema = self.default_schema_name - - charset = 'utf8' - rp = connection.execute("SHOW TABLES FROM %s" % - self.identifier_preparer.quote_identifier(current_schema)) - return [row[0] for row in self._compat_fetchall(rp, charset=charset)] - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - raise NotImplementedError - - def _detect_casing(self, connection): - """Sniff out identifier case sensitivity. - - Cached per-connection. This value can not change without a server - restart. - """ - - return 0 - - def _detect_collations(self, connection): - """Pull the active COLLATIONS list from the server. - - Cached per-connection. - """ - - collations = {} - charset = self._connection_charset - rs = connection.execute( - 'SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM' - ' data_dictionary.COLLATIONS') - for row in self._compat_fetchall(rs, charset): - collations[row[0]] = row[1] - return collations - - def _detect_ansiquotes(self, connection): - """Detect and adjust for the ANSI_QUOTES sql mode.""" - - self._server_ansiquotes = False - self._backslash_escapes = False - - diff --git a/libs/sqlalchemy/dialects/drizzle/mysqldb.py b/libs/sqlalchemy/dialects/drizzle/mysqldb.py deleted file mode 100644 index 7d91cc36..00000000 --- a/libs/sqlalchemy/dialects/drizzle/mysqldb.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -.. dialect:: drizzle+mysqldb - :name: MySQL-Python - :dbapi: mysqldb - :connectstring: drizzle+mysqldb://:@[:]/ - :url: http://sourceforge.net/projects/mysql-python - - -""" - -from sqlalchemy.dialects.drizzle.base import ( - DrizzleDialect, - DrizzleExecutionContext, - DrizzleCompiler, - DrizzleIdentifierPreparer) -from sqlalchemy.connectors.mysqldb import ( - MySQLDBExecutionContext, - MySQLDBCompiler, - MySQLDBIdentifierPreparer, - MySQLDBConnector) - - -class DrizzleExecutionContext_mysqldb(MySQLDBExecutionContext, - DrizzleExecutionContext): - pass - - -class DrizzleCompiler_mysqldb(MySQLDBCompiler, DrizzleCompiler): - pass - - -class DrizzleIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, - DrizzleIdentifierPreparer): - pass - - -class DrizzleDialect_mysqldb(MySQLDBConnector, DrizzleDialect): - execution_ctx_cls = DrizzleExecutionContext_mysqldb - statement_compiler = DrizzleCompiler_mysqldb - preparer = DrizzleIdentifierPreparer_mysqldb - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - return 'utf8' - - -dialect = DrizzleDialect_mysqldb diff --git a/libs/sqlalchemy/dialects/firebird/__init__.py b/libs/sqlalchemy/dialects/firebird/__init__.py deleted file mode 100644 index 094ac3e8..00000000 --- a/libs/sqlalchemy/dialects/firebird/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# firebird/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.firebird import base, kinterbasdb, fdb - -base.dialect = fdb.dialect - -from sqlalchemy.dialects.firebird.base import \ - SMALLINT, BIGINT, FLOAT, FLOAT, DATE, TIME, \ - TEXT, NUMERIC, FLOAT, TIMESTAMP, VARCHAR, CHAR, BLOB,\ - dialect - -__all__ = ( - 'SMALLINT', 'BIGINT', 'FLOAT', 'FLOAT', 'DATE', 'TIME', - 'TEXT', 'NUMERIC', 'FLOAT', 'TIMESTAMP', 'VARCHAR', 'CHAR', 'BLOB', - 'dialect' -) diff --git a/libs/sqlalchemy/dialects/firebird/base.py b/libs/sqlalchemy/dialects/firebird/base.py deleted file mode 100644 index b9af6e58..00000000 --- a/libs/sqlalchemy/dialects/firebird/base.py +++ /dev/null @@ -1,736 +0,0 @@ -# firebird/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: firebird - :name: Firebird - -Firebird Dialects ------------------ - -Firebird offers two distinct dialects_ (not to be confused with a -SQLAlchemy ``Dialect``): - -dialect 1 - This is the old syntax and behaviour, inherited from Interbase pre-6.0. - -dialect 3 - This is the newer and supported syntax, introduced in Interbase 6.0. - -The SQLAlchemy Firebird dialect detects these versions and -adjusts its representation of SQL accordingly. However, -support for dialect 1 is not well tested and probably has -incompatibilities. - -Locking Behavior ----------------- - -Firebird locks tables aggressively. For this reason, a DROP TABLE may -hang until other transactions are released. SQLAlchemy does its best -to release transactions as quickly as possible. The most common cause -of hanging transactions is a non-fully consumed result set, i.e.:: - - result = engine.execute("select * from table") - row = result.fetchone() - return - -Where above, the ``ResultProxy`` has not been fully consumed. The -connection will be returned to the pool and the transactional state -rolled back once the Python garbage collector reclaims the objects -which hold onto the connection, which often occurs asynchronously. -The above use case can be alleviated by calling ``first()`` on the -``ResultProxy`` which will fetch the first row and immediately close -all remaining cursor/connection resources. - -RETURNING support ------------------ - -Firebird 2.0 supports returning a result set from inserts, and 2.1 -extends that to deletes and updates. This is generically exposed by -the SQLAlchemy ``returning()`` method, such as:: - - # INSERT..RETURNING - result = table.insert().returning(table.c.col1, table.c.col2).\\ - values(name='foo') - print result.fetchall() - - # UPDATE..RETURNING - raises = empl.update().returning(empl.c.id, empl.c.salary).\\ - where(empl.c.sales>100).\\ - values(dict(salary=empl.c.salary * 1.1)) - print raises.fetchall() - - -.. _dialects: http://mc-computing.com/Databases/Firebird/SQL_Dialect.html - -""" - -import datetime - -from sqlalchemy import schema as sa_schema -from sqlalchemy import exc, types as sqltypes, sql, util -from sqlalchemy.sql import expression -from sqlalchemy.engine import base, default, reflection -from sqlalchemy.sql import compiler - - -from sqlalchemy.types import (BIGINT, BLOB, DATE, FLOAT, INTEGER, NUMERIC, - SMALLINT, TEXT, TIME, TIMESTAMP, Integer) - - -RESERVED_WORDS = set([ - "active", "add", "admin", "after", "all", "alter", "and", "any", "as", - "asc", "ascending", "at", "auto", "avg", "before", "begin", "between", - "bigint", "bit_length", "blob", "both", "by", "case", "cast", "char", - "character", "character_length", "char_length", "check", "close", - "collate", "column", "commit", "committed", "computed", "conditional", - "connect", "constraint", "containing", "count", "create", "cross", - "cstring", "current", "current_connection", "current_date", - "current_role", "current_time", "current_timestamp", - "current_transaction", "current_user", "cursor", "database", "date", - "day", "dec", "decimal", "declare", "default", "delete", "desc", - "descending", "disconnect", "distinct", "do", "domain", "double", - "drop", "else", "end", "entry_point", "escape", "exception", - "execute", "exists", "exit", "external", "extract", "fetch", "file", - "filter", "float", "for", "foreign", "from", "full", "function", - "gdscode", "generator", "gen_id", "global", "grant", "group", - "having", "hour", "if", "in", "inactive", "index", "inner", - "input_type", "insensitive", "insert", "int", "integer", "into", "is", - "isolation", "join", "key", "leading", "left", "length", "level", - "like", "long", "lower", "manual", "max", "maximum_segment", "merge", - "min", "minute", "module_name", "month", "names", "national", - "natural", "nchar", "no", "not", "null", "numeric", "octet_length", - "of", "on", "only", "open", "option", "or", "order", "outer", - "output_type", "overflow", "page", "pages", "page_size", "parameter", - "password", "plan", "position", "post_event", "precision", "primary", - "privileges", "procedure", "protected", "rdb$db_key", "read", "real", - "record_version", "recreate", "recursive", "references", "release", - "reserv", "reserving", "retain", "returning_values", "returns", - "revoke", "right", "rollback", "rows", "row_count", "savepoint", - "schema", "second", "segment", "select", "sensitive", "set", "shadow", - "shared", "singular", "size", "smallint", "snapshot", "some", "sort", - "sqlcode", "stability", "start", "starting", "starts", "statistics", - "sub_type", "sum", "suspend", "table", "then", "time", "timestamp", - "to", "trailing", "transaction", "trigger", "trim", "uncommitted", - "union", "unique", "update", "upper", "user", "using", "value", - "values", "varchar", "variable", "varying", "view", "wait", "when", - "where", "while", "with", "work", "write", "year", - ]) - - -class _StringType(sqltypes.String): - """Base for Firebird string types.""" - - def __init__(self, charset=None, **kw): - self.charset = charset - super(_StringType, self).__init__(**kw) - - -class VARCHAR(_StringType, sqltypes.VARCHAR): - """Firebird VARCHAR type""" - __visit_name__ = 'VARCHAR' - - def __init__(self, length=None, **kwargs): - super(VARCHAR, self).__init__(length=length, **kwargs) - - -class CHAR(_StringType, sqltypes.CHAR): - """Firebird CHAR type""" - __visit_name__ = 'CHAR' - - def __init__(self, length=None, **kwargs): - super(CHAR, self).__init__(length=length, **kwargs) - - -class _FBDateTime(sqltypes.DateTime): - def bind_processor(self, dialect): - def process(value): - if type(value) == datetime.date: - return datetime.datetime(value.year, value.month, value.day) - else: - return value - return process - -colspecs = { - sqltypes.DateTime: _FBDateTime -} - -ischema_names = { - 'SHORT': SMALLINT, - 'LONG': INTEGER, - 'QUAD': FLOAT, - 'FLOAT': FLOAT, - 'DATE': DATE, - 'TIME': TIME, - 'TEXT': TEXT, - 'INT64': BIGINT, - 'DOUBLE': FLOAT, - 'TIMESTAMP': TIMESTAMP, - 'VARYING': VARCHAR, - 'CSTRING': CHAR, - 'BLOB': BLOB, - } - - -# TODO: date conversion types (should be implemented as _FBDateTime, -# _FBDate, etc. as bind/result functionality is required) - -class FBTypeCompiler(compiler.GenericTypeCompiler): - def visit_boolean(self, type_): - return self.visit_SMALLINT(type_) - - def visit_datetime(self, type_): - return self.visit_TIMESTAMP(type_) - - def visit_TEXT(self, type_): - return "BLOB SUB_TYPE 1" - - def visit_BLOB(self, type_): - return "BLOB SUB_TYPE 0" - - def _extend_string(self, type_, basic): - charset = getattr(type_, 'charset', None) - if charset is None: - return basic - else: - return '%s CHARACTER SET %s' % (basic, charset) - - def visit_CHAR(self, type_): - basic = super(FBTypeCompiler, self).visit_CHAR(type_) - return self._extend_string(type_, basic) - - def visit_VARCHAR(self, type_): - if not type_.length: - raise exc.CompileError( - "VARCHAR requires a length on dialect %s" % - self.dialect.name) - basic = super(FBTypeCompiler, self).visit_VARCHAR(type_) - return self._extend_string(type_, basic) - - -class FBCompiler(sql.compiler.SQLCompiler): - """Firebird specific idiosyncrasies""" - - ansi_bind_rules = True - - #def visit_contains_op_binary(self, binary, operator, **kw): - # cant use CONTAINING b.c. it's case insensitive. - - #def visit_notcontains_op_binary(self, binary, operator, **kw): - # cant use NOT CONTAINING b.c. it's case insensitive. - - def visit_now_func(self, fn, **kw): - return "CURRENT_TIMESTAMP" - - def visit_startswith_op_binary(self, binary, operator, **kw): - return '%s STARTING WITH %s' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) - - def visit_notstartswith_op_binary(self, binary, operator, **kw): - return '%s NOT STARTING WITH %s' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) - - def visit_mod_binary(self, binary, operator, **kw): - return "mod(%s, %s)" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw)) - - def visit_alias(self, alias, asfrom=False, **kwargs): - if self.dialect._version_two: - return super(FBCompiler, self).\ - visit_alias(alias, asfrom=asfrom, **kwargs) - else: - # Override to not use the AS keyword which FB 1.5 does not like - if asfrom: - alias_name = isinstance(alias.name, - expression._truncated_label) and \ - self._truncated_identifier("alias", - alias.name) or alias.name - - return self.process( - alias.original, asfrom=asfrom, **kwargs) + \ - " " + \ - self.preparer.format_alias(alias, alias_name) - else: - return self.process(alias.original, **kwargs) - - def visit_substring_func(self, func, **kw): - s = self.process(func.clauses.clauses[0]) - start = self.process(func.clauses.clauses[1]) - if len(func.clauses.clauses) > 2: - length = self.process(func.clauses.clauses[2]) - return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length) - else: - return "SUBSTRING(%s FROM %s)" % (s, start) - - def visit_length_func(self, function, **kw): - if self.dialect._version_two: - return "char_length" + self.function_argspec(function) - else: - return "strlen" + self.function_argspec(function) - - visit_char_length_func = visit_length_func - - def function_argspec(self, func, **kw): - # TODO: this probably will need to be - # narrowed to a fixed list, some no-arg functions - # may require parens - see similar example in the oracle - # dialect - if func.clauses is not None and len(func.clauses): - return self.process(func.clause_expr, **kw) - else: - return "" - - def default_from(self): - return " FROM rdb$database" - - def visit_sequence(self, seq): - return "gen_id(%s, 1)" % self.preparer.format_sequence(seq) - - def get_select_precolumns(self, select): - """Called when building a ``SELECT`` statement, position is just - before column list Firebird puts the limit and offset right - after the ``SELECT``... - """ - - result = "" - if select._limit: - result += "FIRST %s " % self.process(sql.literal(select._limit)) - if select._offset: - result += "SKIP %s " % self.process(sql.literal(select._offset)) - if select._distinct: - result += "DISTINCT " - return result - - def limit_clause(self, select): - """Already taken care of in the `get_select_precolumns` method.""" - - return "" - - def returning_clause(self, stmt, returning_cols): - columns = [ - self._label_select_column(None, c, True, False, {}) - for c in expression._select_iterables(returning_cols) - ] - - return 'RETURNING ' + ', '.join(columns) - - -class FBDDLCompiler(sql.compiler.DDLCompiler): - """Firebird syntactic idiosyncrasies""" - - def visit_create_sequence(self, create): - """Generate a ``CREATE GENERATOR`` statement for the sequence.""" - - # no syntax for these - # http://www.firebirdsql.org/manual/generatorguide-sqlsyntax.html - if create.element.start is not None: - raise NotImplemented( - "Firebird SEQUENCE doesn't support START WITH") - if create.element.increment is not None: - raise NotImplemented( - "Firebird SEQUENCE doesn't support INCREMENT BY") - - if self.dialect._version_two: - return "CREATE SEQUENCE %s" % \ - self.preparer.format_sequence(create.element) - else: - return "CREATE GENERATOR %s" % \ - self.preparer.format_sequence(create.element) - - def visit_drop_sequence(self, drop): - """Generate a ``DROP GENERATOR`` statement for the sequence.""" - - if self.dialect._version_two: - return "DROP SEQUENCE %s" % \ - self.preparer.format_sequence(drop.element) - else: - return "DROP GENERATOR %s" % \ - self.preparer.format_sequence(drop.element) - - -class FBIdentifierPreparer(sql.compiler.IdentifierPreparer): - """Install Firebird specific reserved words.""" - - reserved_words = RESERVED_WORDS - illegal_initial_characters = compiler.ILLEGAL_INITIAL_CHARACTERS.union(['_']) - - def __init__(self, dialect): - super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True) - - -class FBExecutionContext(default.DefaultExecutionContext): - def fire_sequence(self, seq, type_): - """Get the next value from the sequence using ``gen_id()``.""" - - return self._execute_scalar( - "SELECT gen_id(%s, 1) FROM rdb$database" % - self.dialect.identifier_preparer.format_sequence(seq), - type_ - ) - - -class FBDialect(default.DefaultDialect): - """Firebird dialect""" - - name = 'firebird' - - max_identifier_length = 31 - - supports_sequences = True - sequences_optional = False - supports_default_values = True - postfetch_lastrowid = False - - supports_native_boolean = False - - requires_name_normalize = True - supports_empty_insert = False - - statement_compiler = FBCompiler - ddl_compiler = FBDDLCompiler - preparer = FBIdentifierPreparer - type_compiler = FBTypeCompiler - execution_ctx_cls = FBExecutionContext - - colspecs = colspecs - ischema_names = ischema_names - - # defaults to dialect ver. 3, - # will be autodetected off upon - # first connect - _version_two = True - - def initialize(self, connection): - super(FBDialect, self).initialize(connection) - self._version_two = ('firebird' in self.server_version_info and \ - self.server_version_info >= (2, ) - ) or \ - ('interbase' in self.server_version_info and \ - self.server_version_info >= (6, ) - ) - - if not self._version_two: - # TODO: whatever other pre < 2.0 stuff goes here - self.ischema_names = ischema_names.copy() - self.ischema_names['TIMESTAMP'] = sqltypes.DATE - self.colspecs = { - sqltypes.DateTime: sqltypes.DATE - } - - self.implicit_returning = self._version_two and \ - self.__dict__.get('implicit_returning', True) - - def normalize_name(self, name): - # Remove trailing spaces: FB uses a CHAR() type, - # that is padded with spaces - name = name and name.rstrip() - if name is None: - return None - elif name.upper() == name and \ - not self.identifier_preparer._requires_quotes(name.lower()): - return name.lower() - else: - return name - - def denormalize_name(self, name): - if name is None: - return None - elif name.lower() == name and \ - not self.identifier_preparer._requires_quotes(name.lower()): - return name.upper() - else: - return name - - def has_table(self, connection, table_name, schema=None): - """Return ``True`` if the given table exists, ignoring - the `schema`.""" - - tblqry = """ - SELECT 1 AS has_table FROM rdb$database - WHERE EXISTS (SELECT rdb$relation_name - FROM rdb$relations - WHERE rdb$relation_name=?) - """ - c = connection.execute(tblqry, [self.denormalize_name(table_name)]) - return c.first() is not None - - def has_sequence(self, connection, sequence_name, schema=None): - """Return ``True`` if the given sequence (generator) exists.""" - - genqry = """ - SELECT 1 AS has_sequence FROM rdb$database - WHERE EXISTS (SELECT rdb$generator_name - FROM rdb$generators - WHERE rdb$generator_name=?) - """ - c = connection.execute(genqry, [self.denormalize_name(sequence_name)]) - return c.first() is not None - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - # there are two queries commonly mentioned for this. - # this one, using view_blr, is at the Firebird FAQ among other places: - # http://www.firebirdfaq.org/faq174/ - s = """ - select rdb$relation_name - from rdb$relations - where rdb$view_blr is null - and (rdb$system_flag is null or rdb$system_flag = 0); - """ - - # the other query is this one. It's not clear if there's really - # any difference between these two. This link: - # http://www.alberton.info/firebird_sql_meta_info.html#.Ur3vXfZGni8 - # states them as interchangeable. Some discussion at [ticket:2898] - # SELECT DISTINCT rdb$relation_name - # FROM rdb$relation_fields - # WHERE rdb$system_flag=0 AND rdb$view_context IS NULL - - return [self.normalize_name(row[0]) for row in connection.execute(s)] - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - # see http://www.firebirdfaq.org/faq174/ - s = """ - select rdb$relation_name - from rdb$relations - where rdb$view_blr is not null - and (rdb$system_flag is null or rdb$system_flag = 0); - """ - return [self.normalize_name(row[0]) for row in connection.execute(s)] - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - qry = """ - SELECT rdb$view_source AS view_source - FROM rdb$relations - WHERE rdb$relation_name=? - """ - rp = connection.execute(qry, [self.denormalize_name(view_name)]) - row = rp.first() - if row: - return row['view_source'] - else: - return None - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - # Query to extract the PK/FK constrained fields of the given table - keyqry = """ - SELECT se.rdb$field_name AS fname - FROM rdb$relation_constraints rc - JOIN rdb$index_segments se ON rc.rdb$index_name=se.rdb$index_name - WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=? - """ - tablename = self.denormalize_name(table_name) - # get primary key fields - c = connection.execute(keyqry, ["PRIMARY KEY", tablename]) - pkfields = [self.normalize_name(r['fname']) for r in c.fetchall()] - return {'constrained_columns': pkfields, 'name': None} - - @reflection.cache - def get_column_sequence(self, connection, - table_name, column_name, - schema=None, **kw): - tablename = self.denormalize_name(table_name) - colname = self.denormalize_name(column_name) - # Heuristic-query to determine the generator associated to a PK field - genqry = """ - SELECT trigdep.rdb$depended_on_name AS fgenerator - FROM rdb$dependencies tabdep - JOIN rdb$dependencies trigdep - ON tabdep.rdb$dependent_name=trigdep.rdb$dependent_name - AND trigdep.rdb$depended_on_type=14 - AND trigdep.rdb$dependent_type=2 - JOIN rdb$triggers trig ON - trig.rdb$trigger_name=tabdep.rdb$dependent_name - WHERE tabdep.rdb$depended_on_name=? - AND tabdep.rdb$depended_on_type=0 - AND trig.rdb$trigger_type=1 - AND tabdep.rdb$field_name=? - AND (SELECT count(*) - FROM rdb$dependencies trigdep2 - WHERE trigdep2.rdb$dependent_name = trigdep.rdb$dependent_name) = 2 - """ - genr = connection.execute(genqry, [tablename, colname]).first() - if genr is not None: - return dict(name=self.normalize_name(genr['fgenerator'])) - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - # Query to extract the details of all the fields of the given table - tblqry = """ - SELECT r.rdb$field_name AS fname, - r.rdb$null_flag AS null_flag, - t.rdb$type_name AS ftype, - f.rdb$field_sub_type AS stype, - f.rdb$field_length/ - COALESCE(cs.rdb$bytes_per_character,1) AS flen, - f.rdb$field_precision AS fprec, - f.rdb$field_scale AS fscale, - COALESCE(r.rdb$default_source, - f.rdb$default_source) AS fdefault - FROM rdb$relation_fields r - JOIN rdb$fields f ON r.rdb$field_source=f.rdb$field_name - JOIN rdb$types t - ON t.rdb$type=f.rdb$field_type AND - t.rdb$field_name='RDB$FIELD_TYPE' - LEFT JOIN rdb$character_sets cs ON - f.rdb$character_set_id=cs.rdb$character_set_id - WHERE f.rdb$system_flag=0 AND r.rdb$relation_name=? - ORDER BY r.rdb$field_position - """ - # get the PK, used to determine the eventual associated sequence - pk_constraint = self.get_pk_constraint(connection, table_name) - pkey_cols = pk_constraint['constrained_columns'] - - tablename = self.denormalize_name(table_name) - # get all of the fields for this table - c = connection.execute(tblqry, [tablename]) - cols = [] - while True: - row = c.fetchone() - if row is None: - break - name = self.normalize_name(row['fname']) - orig_colname = row['fname'] - - # get the data type - colspec = row['ftype'].rstrip() - coltype = self.ischema_names.get(colspec) - if coltype is None: - util.warn("Did not recognize type '%s' of column '%s'" % - (colspec, name)) - coltype = sqltypes.NULLTYPE - elif issubclass(coltype, Integer) and row['fprec'] != 0: - coltype = NUMERIC( - precision=row['fprec'], - scale=row['fscale'] * -1) - elif colspec in ('VARYING', 'CSTRING'): - coltype = coltype(row['flen']) - elif colspec == 'TEXT': - coltype = TEXT(row['flen']) - elif colspec == 'BLOB': - if row['stype'] == 1: - coltype = TEXT() - else: - coltype = BLOB() - else: - coltype = coltype() - - # does it have a default value? - defvalue = None - if row['fdefault'] is not None: - # the value comes down as "DEFAULT 'value'": there may be - # more than one whitespace around the "DEFAULT" keyword - # and it may also be lower case - # (see also http://tracker.firebirdsql.org/browse/CORE-356) - defexpr = row['fdefault'].lstrip() - assert defexpr[:8].rstrip().upper() == \ - 'DEFAULT', "Unrecognized default value: %s" % \ - defexpr - defvalue = defexpr[8:].strip() - if defvalue == 'NULL': - # Redundant - defvalue = None - col_d = { - 'name': name, - 'type': coltype, - 'nullable': not bool(row['null_flag']), - 'default': defvalue, - 'autoincrement': defvalue is None - } - - if orig_colname.lower() == orig_colname: - col_d['quote'] = True - - # if the PK is a single field, try to see if its linked to - # a sequence thru a trigger - if len(pkey_cols) == 1 and name == pkey_cols[0]: - seq_d = self.get_column_sequence(connection, tablename, name) - if seq_d is not None: - col_d['sequence'] = seq_d - - cols.append(col_d) - return cols - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - # Query to extract the details of each UK/FK of the given table - fkqry = """ - SELECT rc.rdb$constraint_name AS cname, - cse.rdb$field_name AS fname, - ix2.rdb$relation_name AS targetrname, - se.rdb$field_name AS targetfname - FROM rdb$relation_constraints rc - JOIN rdb$indices ix1 ON ix1.rdb$index_name=rc.rdb$index_name - JOIN rdb$indices ix2 ON ix2.rdb$index_name=ix1.rdb$foreign_key - JOIN rdb$index_segments cse ON - cse.rdb$index_name=ix1.rdb$index_name - JOIN rdb$index_segments se - ON se.rdb$index_name=ix2.rdb$index_name - AND se.rdb$field_position=cse.rdb$field_position - WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=? - ORDER BY se.rdb$index_name, se.rdb$field_position - """ - tablename = self.denormalize_name(table_name) - - c = connection.execute(fkqry, ["FOREIGN KEY", tablename]) - fks = util.defaultdict(lambda: { - 'name': None, - 'constrained_columns': [], - 'referred_schema': None, - 'referred_table': None, - 'referred_columns': [] - }) - - for row in c: - cname = self.normalize_name(row['cname']) - fk = fks[cname] - if not fk['name']: - fk['name'] = cname - fk['referred_table'] = self.normalize_name(row['targetrname']) - fk['constrained_columns'].append( - self.normalize_name(row['fname'])) - fk['referred_columns'].append( - self.normalize_name(row['targetfname'])) - return list(fks.values()) - - @reflection.cache - def get_indexes(self, connection, table_name, schema=None, **kw): - qry = """ - SELECT ix.rdb$index_name AS index_name, - ix.rdb$unique_flag AS unique_flag, - ic.rdb$field_name AS field_name - FROM rdb$indices ix - JOIN rdb$index_segments ic - ON ix.rdb$index_name=ic.rdb$index_name - LEFT OUTER JOIN rdb$relation_constraints - ON rdb$relation_constraints.rdb$index_name = - ic.rdb$index_name - WHERE ix.rdb$relation_name=? AND ix.rdb$foreign_key IS NULL - AND rdb$relation_constraints.rdb$constraint_type IS NULL - ORDER BY index_name, ic.rdb$field_position - """ - c = connection.execute(qry, [self.denormalize_name(table_name)]) - - indexes = util.defaultdict(dict) - for row in c: - indexrec = indexes[row['index_name']] - if 'name' not in indexrec: - indexrec['name'] = self.normalize_name(row['index_name']) - indexrec['column_names'] = [] - indexrec['unique'] = bool(row['unique_flag']) - - indexrec['column_names'].append( - self.normalize_name(row['field_name'])) - - return list(indexes.values()) - diff --git a/libs/sqlalchemy/dialects/firebird/fdb.py b/libs/sqlalchemy/dialects/firebird/fdb.py deleted file mode 100644 index 4d94ef0d..00000000 --- a/libs/sqlalchemy/dialects/firebird/fdb.py +++ /dev/null @@ -1,115 +0,0 @@ -# firebird/fdb.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: firebird+fdb - :name: fdb - :dbapi: pyodbc - :connectstring: firebird+fdb://user:password@host:port/path/to/db[?key=value&key=value...] - :url: http://pypi.python.org/pypi/fdb/ - - fdb is a kinterbasdb compatible DBAPI for Firebird. - - .. versionadded:: 0.8 - Support for the fdb Firebird driver. - - .. versionchanged:: 0.9 - The fdb dialect is now the default dialect - under the ``firebird://`` URL space, as ``fdb`` is now the official - Python driver for Firebird. - -Arguments ----------- - -The ``fdb`` dialect is based on the :mod:`sqlalchemy.dialects.firebird.kinterbasdb` -dialect, however does not accept every argument that Kinterbasdb does. - -* ``enable_rowcount`` - True by default, setting this to False disables - the usage of "cursor.rowcount" with the - Kinterbasdb dialect, which SQLAlchemy ordinarily calls upon automatically - after any UPDATE or DELETE statement. When disabled, SQLAlchemy's - ResultProxy will return -1 for result.rowcount. The rationale here is - that Kinterbasdb requires a second round trip to the database when - .rowcount is called - since SQLA's resultproxy automatically closes - the cursor after a non-result-returning statement, rowcount must be - called, if at all, before the result object is returned. Additionally, - cursor.rowcount may not return correct results with older versions - of Firebird, and setting this flag to False will also cause the - SQLAlchemy ORM to ignore its usage. The behavior can also be controlled on a - per-execution basis using the ``enable_rowcount`` option with - :meth:`.Connection.execution_options`:: - - conn = engine.connect().execution_options(enable_rowcount=True) - r = conn.execute(stmt) - print r.rowcount - -* ``retaining`` - False by default. Setting this to True will pass the - ``retaining=True`` keyword argument to the ``.commit()`` and ``.rollback()`` - methods of the DBAPI connection, which can improve performance in some - situations, but apparently with significant caveats. - Please read the fdb and/or kinterbasdb DBAPI documentation in order to - understand the implications of this flag. - - .. versionadded:: 0.8.2 - ``retaining`` keyword argument specifying - transaction retaining behavior - in 0.8 it defaults to ``True`` - for backwards compatibility. - - .. versionchanged:: 0.9.0 - the ``retaining`` flag defaults to ``False``. - In 0.8 it defaulted to ``True``. - - .. seealso:: - - http://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - information - on the "retaining" flag. - -""" - -from .kinterbasdb import FBDialect_kinterbasdb -from ... import util - - -class FBDialect_fdb(FBDialect_kinterbasdb): - - def __init__(self, enable_rowcount=True, - retaining=False, **kwargs): - super(FBDialect_fdb, self).__init__( - enable_rowcount=enable_rowcount, - retaining=retaining, **kwargs) - - @classmethod - def dbapi(cls): - return __import__('fdb') - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - if opts.get('port'): - opts['host'] = "%s/%s" % (opts['host'], opts['port']) - del opts['port'] - opts.update(url.query) - - util.coerce_kw_type(opts, 'type_conv', int) - - return ([], opts) - - def _get_server_version_info(self, connection): - """Get the version of the Firebird server used by a connection. - - Returns a tuple of (`major`, `minor`, `build`), three integers - representing the version of the attached server. - """ - - # This is the simpler approach (the other uses the services api), - # that for backward compatibility reasons returns a string like - # LI-V6.3.3.12981 Firebird 2.0 - # where the first version is a fake one resembling the old - # Interbase signature. - - isc_info_firebird_version = 103 - fbconn = connection.connection - - version = fbconn.db_info(isc_info_firebird_version) - - return self._parse_version_info(version) - -dialect = FBDialect_fdb diff --git a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py b/libs/sqlalchemy/dialects/firebird/kinterbasdb.py deleted file mode 100644 index b8a83a07..00000000 --- a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py +++ /dev/null @@ -1,179 +0,0 @@ -# firebird/kinterbasdb.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: firebird+kinterbasdb - :name: kinterbasdb - :dbapi: kinterbasdb - :connectstring: firebird+kinterbasdb://user:password@host:port/path/to/db[?key=value&key=value...] - :url: http://firebirdsql.org/index.php?op=devel&sub=python - -Arguments ----------- - -The Kinterbasdb backend accepts the ``enable_rowcount`` and ``retaining`` -arguments accepted by the :mod:`sqlalchemy.dialects.firebird.fdb` dialect. In addition, it -also accepts the following: - -* ``type_conv`` - select the kind of mapping done on the types: by default - SQLAlchemy uses 200 with Unicode, datetime and decimal support. See - the linked documents below for further information. - -* ``concurrency_level`` - set the backend policy with regards to threading - issues: by default SQLAlchemy uses policy 1. See the linked documents - below for futher information. - -.. seealso:: - - http://sourceforge.net/projects/kinterbasdb - - http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_param_conv_dynamic_type_translation - - http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurrency - -""" - -from .base import FBDialect, FBExecutionContext -from ... import util, types as sqltypes -from re import match -import decimal - - -class _kinterbasdb_numeric(object): - def bind_processor(self, dialect): - def process(value): - if isinstance(value, decimal.Decimal): - return str(value) - else: - return value - return process - -class _FBNumeric_kinterbasdb(_kinterbasdb_numeric, sqltypes.Numeric): - pass - -class _FBFloat_kinterbasdb(_kinterbasdb_numeric, sqltypes.Float): - pass - - -class FBExecutionContext_kinterbasdb(FBExecutionContext): - @property - def rowcount(self): - if self.execution_options.get('enable_rowcount', - self.dialect.enable_rowcount): - return self.cursor.rowcount - else: - return -1 - - -class FBDialect_kinterbasdb(FBDialect): - driver = 'kinterbasdb' - supports_sane_rowcount = False - supports_sane_multi_rowcount = False - execution_ctx_cls = FBExecutionContext_kinterbasdb - - supports_native_decimal = True - - colspecs = util.update_copy( - FBDialect.colspecs, - { - sqltypes.Numeric: _FBNumeric_kinterbasdb, - sqltypes.Float: _FBFloat_kinterbasdb, - } - - ) - - def __init__(self, type_conv=200, concurrency_level=1, - enable_rowcount=True, - retaining=False, **kwargs): - super(FBDialect_kinterbasdb, self).__init__(**kwargs) - self.enable_rowcount = enable_rowcount - self.type_conv = type_conv - self.concurrency_level = concurrency_level - self.retaining = retaining - if enable_rowcount: - self.supports_sane_rowcount = True - - @classmethod - def dbapi(cls): - return __import__('kinterbasdb') - - def do_execute(self, cursor, statement, parameters, context=None): - # kinterbase does not accept a None, but wants an empty list - # when there are no arguments. - cursor.execute(statement, parameters or []) - - def do_rollback(self, dbapi_connection): - dbapi_connection.rollback(self.retaining) - - def do_commit(self, dbapi_connection): - dbapi_connection.commit(self.retaining) - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - if opts.get('port'): - opts['host'] = "%s/%s" % (opts['host'], opts['port']) - del opts['port'] - opts.update(url.query) - - util.coerce_kw_type(opts, 'type_conv', int) - - type_conv = opts.pop('type_conv', self.type_conv) - concurrency_level = opts.pop('concurrency_level', - self.concurrency_level) - - if self.dbapi is not None: - initialized = getattr(self.dbapi, 'initialized', None) - if initialized is None: - # CVS rev 1.96 changed the name of the attribute: - # http://kinterbasdb.cvs.sourceforge.net/viewvc/kinterbasdb/ - # Kinterbasdb-3.0/__init__.py?r1=1.95&r2=1.96 - initialized = getattr(self.dbapi, '_initialized', False) - if not initialized: - self.dbapi.init(type_conv=type_conv, - concurrency_level=concurrency_level) - return ([], opts) - - def _get_server_version_info(self, connection): - """Get the version of the Firebird server used by a connection. - - Returns a tuple of (`major`, `minor`, `build`), three integers - representing the version of the attached server. - """ - - # This is the simpler approach (the other uses the services api), - # that for backward compatibility reasons returns a string like - # LI-V6.3.3.12981 Firebird 2.0 - # where the first version is a fake one resembling the old - # Interbase signature. - - fbconn = connection.connection - version = fbconn.server_version - - return self._parse_version_info(version) - - def _parse_version_info(self, version): - m = match('\w+-V(\d+)\.(\d+)\.(\d+)\.(\d+)( \w+ (\d+)\.(\d+))?', version) - if not m: - raise AssertionError( - "Could not determine version from string '%s'" % version) - - if m.group(5) != None: - return tuple([int(x) for x in m.group(6, 7, 4)] + ['firebird']) - else: - return tuple([int(x) for x in m.group(1, 2, 3)] + ['interbase']) - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, (self.dbapi.OperationalError, - self.dbapi.ProgrammingError)): - msg = str(e) - return ('Unable to complete network request to host' in msg or - 'Invalid connection state' in msg or - 'Invalid cursor state' in msg or - 'connection shutdown' in msg) - else: - return False - -dialect = FBDialect_kinterbasdb diff --git a/libs/sqlalchemy/dialects/mssql/__init__.py b/libs/sqlalchemy/dialects/mssql/__init__.py deleted file mode 100644 index 7a2dfa60..00000000 --- a/libs/sqlalchemy/dialects/mssql/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# mssql/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.mssql import base, pyodbc, adodbapi, \ - pymssql, zxjdbc, mxodbc - -base.dialect = pyodbc.dialect - -from sqlalchemy.dialects.mssql.base import \ - INTEGER, BIGINT, SMALLINT, TINYINT, VARCHAR, NVARCHAR, CHAR, \ - NCHAR, TEXT, NTEXT, DECIMAL, NUMERIC, FLOAT, DATETIME,\ - DATETIME2, DATETIMEOFFSET, DATE, TIME, SMALLDATETIME, \ - BINARY, VARBINARY, BIT, REAL, IMAGE, TIMESTAMP,\ - MONEY, SMALLMONEY, UNIQUEIDENTIFIER, SQL_VARIANT, dialect - - -__all__ = ( - 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'VARCHAR', 'NVARCHAR', 'CHAR', - 'NCHAR', 'TEXT', 'NTEXT', 'DECIMAL', 'NUMERIC', 'FLOAT', 'DATETIME', - 'DATETIME2', 'DATETIMEOFFSET', 'DATE', 'TIME', 'SMALLDATETIME', - 'BINARY', 'VARBINARY', 'BIT', 'REAL', 'IMAGE', 'TIMESTAMP', - 'MONEY', 'SMALLMONEY', 'UNIQUEIDENTIFIER', 'SQL_VARIANT', 'dialect' -) diff --git a/libs/sqlalchemy/dialects/mssql/adodbapi.py b/libs/sqlalchemy/dialects/mssql/adodbapi.py deleted file mode 100644 index 95cf4242..00000000 --- a/libs/sqlalchemy/dialects/mssql/adodbapi.py +++ /dev/null @@ -1,79 +0,0 @@ -# mssql/adodbapi.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql+adodbapi - :name: adodbapi - :dbapi: adodbapi - :connectstring: mssql+adodbapi://:@ - :url: http://adodbapi.sourceforge.net/ - -.. note:: - - The adodbapi dialect is not implemented SQLAlchemy versions 0.6 and - above at this time. - -""" -import datetime -from sqlalchemy import types as sqltypes, util -from sqlalchemy.dialects.mssql.base import MSDateTime, MSDialect -import sys - - -class MSDateTime_adodbapi(MSDateTime): - def result_processor(self, dialect, coltype): - def process(value): - # adodbapi will return datetimes with empty time - # values as datetime.date() objects. - # Promote them back to full datetime.datetime() - if type(value) is datetime.date: - return datetime.datetime(value.year, value.month, value.day) - return value - return process - - -class MSDialect_adodbapi(MSDialect): - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - supports_unicode = sys.maxunicode == 65535 - supports_unicode_statements = True - driver = 'adodbapi' - - @classmethod - def import_dbapi(cls): - import adodbapi as module - return module - - colspecs = util.update_copy( - MSDialect.colspecs, - { - sqltypes.DateTime: MSDateTime_adodbapi - } - ) - - def create_connect_args(self, url): - keys = url.query - - connectors = ["Provider=SQLOLEDB"] - if 'port' in keys: - connectors.append("Data Source=%s, %s" % - (keys.get("host"), keys.get("port"))) - else: - connectors.append("Data Source=%s" % keys.get("host")) - connectors.append("Initial Catalog=%s" % keys.get("database")) - user = keys.get("user") - if user: - connectors.append("User Id=%s" % user) - connectors.append("Password=%s" % keys.get("password", "")) - else: - connectors.append("Integrated Security=SSPI") - return [[";".join(connectors)], {}] - - def is_disconnect(self, e, connection, cursor): - return isinstance(e, self.dbapi.adodbapi.DatabaseError) and \ - "'connection failure'" in str(e) - -dialect = MSDialect_adodbapi diff --git a/libs/sqlalchemy/dialects/mssql/base.py b/libs/sqlalchemy/dialects/mssql/base.py deleted file mode 100644 index 90fd1f38..00000000 --- a/libs/sqlalchemy/dialects/mssql/base.py +++ /dev/null @@ -1,1476 +0,0 @@ -# mssql/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql - :name: Microsoft SQL Server - - -Auto Increment Behavior ------------------------ - -``IDENTITY`` columns are supported by using SQLAlchemy -``schema.Sequence()`` objects. In other words:: - - from sqlalchemy import Table, Integer, Sequence, Column - - Table('test', metadata, - Column('id', Integer, - Sequence('blah',100,10), primary_key=True), - Column('name', String(20)) - ).create(some_engine) - -would yield:: - - CREATE TABLE test ( - id INTEGER NOT NULL IDENTITY(100,10) PRIMARY KEY, - name VARCHAR(20) NULL, - ) - -Note that the ``start`` and ``increment`` values for sequences are -optional and will default to 1,1. - -Implicit ``autoincrement`` behavior works the same in MSSQL as it -does in other dialects and results in an ``IDENTITY`` column. - -* Support for ``SET IDENTITY_INSERT ON`` mode (automagic on / off for - ``INSERT`` s) - -* Support for auto-fetching of ``@@IDENTITY/@@SCOPE_IDENTITY()`` on - ``INSERT`` - -Collation Support ------------------ - -Character collations are supported by the base string types, -specified by the string argument "collation":: - - from sqlalchemy import VARCHAR - Column('login', VARCHAR(32, collation='Latin1_General_CI_AS')) - -When such a column is associated with a :class:`.Table`, the -CREATE TABLE statement for this column will yield:: - - login VARCHAR(32) COLLATE Latin1_General_CI_AS NULL - -.. versionadded:: 0.8 Character collations are now part of the base string - types. - -LIMIT/OFFSET Support --------------------- - -MSSQL has no support for the LIMIT or OFFSET keysowrds. LIMIT is -supported directly through the ``TOP`` Transact SQL keyword:: - - select.limit - -will yield:: - - SELECT TOP n - -If using SQL Server 2005 or above, LIMIT with OFFSET -support is available through the ``ROW_NUMBER OVER`` construct. -For versions below 2005, LIMIT with OFFSET usage will fail. - -Nullability ------------ -MSSQL has support for three levels of column nullability. The default -nullability allows nulls and is explicit in the CREATE TABLE -construct:: - - name VARCHAR(20) NULL - -If ``nullable=None`` is specified then no specification is made. In -other words the database's configured default is used. This will -render:: - - name VARCHAR(20) - -If ``nullable`` is ``True`` or ``False`` then the column will be -``NULL` or ``NOT NULL`` respectively. - -Date / Time Handling --------------------- -DATE and TIME are supported. Bind parameters are converted -to datetime.datetime() objects as required by most MSSQL drivers, -and results are processed from strings if needed. -The DATE and TIME types are not available for MSSQL 2005 and -previous - if a server version below 2008 is detected, DDL -for these types will be issued as DATETIME. - -.. _mssql_indexes: - -MSSQL-Specific Index Options ------------------------------ - -The MSSQL dialect supports special options for :class:`.Index`. - -CLUSTERED -^^^^^^^^^^ - -The ``mssql_clustered`` option adds the CLUSTERED keyword to the index:: - - Index("my_index", table.c.x, mssql_clustered=True) - -would render the index as ``CREATE CLUSTERED INDEX my_index ON table (x)`` - -.. versionadded:: 0.8 - -INCLUDE -^^^^^^^ - -The ``mssql_include`` option renders INCLUDE(colname) for the given string names:: - - Index("my_index", table.c.x, mssql_include=['y']) - -would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)`` - -.. versionadded:: 0.8 - -Index ordering -^^^^^^^^^^^^^^ - -Index ordering is available via functional expressions, such as:: - - Index("my_index", table.c.x.desc()) - -would render the index as ``CREATE INDEX my_index ON table (x DESC)`` - -.. versionadded:: 0.8 - -.. seealso:: - - :ref:`schema_indexes_functional` - -Compatibility Levels --------------------- -MSSQL supports the notion of setting compatibility levels at the -database level. This allows, for instance, to run a database that -is compatible with SQL2000 while running on a SQL2005 database -server. ``server_version_info`` will always return the database -server version information (in this case SQL2005) and not the -compatibility level information. Because of this, if running under -a backwards compatibility mode SQAlchemy may attempt to use T-SQL -statements that are unable to be parsed by the database server. - -Triggers --------- - -SQLAlchemy by default uses OUTPUT INSERTED to get at newly -generated primary key values via IDENTITY columns or other -server side defaults. MS-SQL does not -allow the usage of OUTPUT INSERTED on tables that have triggers. -To disable the usage of OUTPUT INSERTED on a per-table basis, -specify ``implicit_returning=False`` for each :class:`.Table` -which has triggers:: - - Table('mytable', metadata, - Column('id', Integer, primary_key=True), - # ..., - implicit_returning=False - ) - -Declarative form:: - - class MyClass(Base): - # ... - __table_args__ = {'implicit_returning':False} - - -This option can also be specified engine-wide using the -``implicit_returning=False`` argument on :func:`.create_engine`. - -Enabling Snapshot Isolation ---------------------------- - -Not necessarily specific to SQLAlchemy, SQL Server has a default transaction -isolation mode that locks entire tables, and causes even mildly concurrent -applications to have long held locks and frequent deadlocks. -Enabling snapshot isolation for the database as a whole is recommended -for modern levels of concurrency support. This is accomplished via the -following ALTER DATABASE commands executed at the SQL prompt:: - - ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ON - - ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON - -Background on SQL Server snapshot isolation is available at -http://msdn.microsoft.com/en-us/library/ms175095.aspx. - -Known Issues ------------- - -* No support for more than one ``IDENTITY`` column per table -* reflection of indexes does not work with versions older than - SQL Server 2005 - -""" -import datetime -import operator -import re - -from ... import sql, schema as sa_schema, exc, util -from ...sql import compiler, expression, \ - util as sql_util, cast -from ... import engine -from ...engine import reflection, default -from ... import types as sqltypes -from ...types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \ - FLOAT, TIMESTAMP, DATETIME, DATE, BINARY,\ - VARBINARY, TEXT, VARCHAR, NVARCHAR, CHAR, NCHAR - - -from ...util import update_wrapper -from . import information_schema as ischema - -MS_2008_VERSION = (10,) -MS_2005_VERSION = (9,) -MS_2000_VERSION = (8,) - -RESERVED_WORDS = set( - ['add', 'all', 'alter', 'and', 'any', 'as', 'asc', 'authorization', - 'backup', 'begin', 'between', 'break', 'browse', 'bulk', 'by', 'cascade', - 'case', 'check', 'checkpoint', 'close', 'clustered', 'coalesce', - 'collate', 'column', 'commit', 'compute', 'constraint', 'contains', - 'containstable', 'continue', 'convert', 'create', 'cross', 'current', - 'current_date', 'current_time', 'current_timestamp', 'current_user', - 'cursor', 'database', 'dbcc', 'deallocate', 'declare', 'default', - 'delete', 'deny', 'desc', 'disk', 'distinct', 'distributed', 'double', - 'drop', 'dump', 'else', 'end', 'errlvl', 'escape', 'except', 'exec', - 'execute', 'exists', 'exit', 'external', 'fetch', 'file', 'fillfactor', - 'for', 'foreign', 'freetext', 'freetexttable', 'from', 'full', - 'function', 'goto', 'grant', 'group', 'having', 'holdlock', 'identity', - 'identity_insert', 'identitycol', 'if', 'in', 'index', 'inner', 'insert', - 'intersect', 'into', 'is', 'join', 'key', 'kill', 'left', 'like', - 'lineno', 'load', 'merge', 'national', 'nocheck', 'nonclustered', 'not', - 'null', 'nullif', 'of', 'off', 'offsets', 'on', 'open', 'opendatasource', - 'openquery', 'openrowset', 'openxml', 'option', 'or', 'order', 'outer', - 'over', 'percent', 'pivot', 'plan', 'precision', 'primary', 'print', - 'proc', 'procedure', 'public', 'raiserror', 'read', 'readtext', - 'reconfigure', 'references', 'replication', 'restore', 'restrict', - 'return', 'revert', 'revoke', 'right', 'rollback', 'rowcount', - 'rowguidcol', 'rule', 'save', 'schema', 'securityaudit', 'select', - 'session_user', 'set', 'setuser', 'shutdown', 'some', 'statistics', - 'system_user', 'table', 'tablesample', 'textsize', 'then', 'to', 'top', - 'tran', 'transaction', 'trigger', 'truncate', 'tsequal', 'union', - 'unique', 'unpivot', 'update', 'updatetext', 'use', 'user', 'values', - 'varying', 'view', 'waitfor', 'when', 'where', 'while', 'with', - 'writetext', - ]) - - -class REAL(sqltypes.REAL): - __visit_name__ = 'REAL' - - def __init__(self, **kw): - # REAL is a synonym for FLOAT(24) on SQL server - kw['precision'] = 24 - super(REAL, self).__init__(**kw) - - -class TINYINT(sqltypes.Integer): - __visit_name__ = 'TINYINT' - - -# MSSQL DATE/TIME types have varied behavior, sometimes returning -# strings. MSDate/TIME check for everything, and always -# filter bind parameters into datetime objects (required by pyodbc, -# not sure about other dialects). - -class _MSDate(sqltypes.Date): - def bind_processor(self, dialect): - def process(value): - if type(value) == datetime.date: - return datetime.datetime(value.year, value.month, value.day) - else: - return value - return process - - _reg = re.compile(r"(\d+)-(\d+)-(\d+)") - - def result_processor(self, dialect, coltype): - def process(value): - if isinstance(value, datetime.datetime): - return value.date() - elif isinstance(value, util.string_types): - return datetime.date(*[ - int(x or 0) - for x in self._reg.match(value).groups() - ]) - else: - return value - return process - - -class TIME(sqltypes.TIME): - def __init__(self, precision=None, **kwargs): - self.precision = precision - super(TIME, self).__init__() - - __zero_date = datetime.date(1900, 1, 1) - - def bind_processor(self, dialect): - def process(value): - if isinstance(value, datetime.datetime): - value = datetime.datetime.combine( - self.__zero_date, value.time()) - elif isinstance(value, datetime.time): - value = datetime.datetime.combine(self.__zero_date, value) - return value - return process - - _reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d{0,6}))?") - - def result_processor(self, dialect, coltype): - def process(value): - if isinstance(value, datetime.datetime): - return value.time() - elif isinstance(value, util.string_types): - return datetime.time(*[ - int(x or 0) - for x in self._reg.match(value).groups()]) - else: - return value - return process -_MSTime = TIME - - -class _DateTimeBase(object): - def bind_processor(self, dialect): - def process(value): - if type(value) == datetime.date: - return datetime.datetime(value.year, value.month, value.day) - else: - return value - return process - - -class _MSDateTime(_DateTimeBase, sqltypes.DateTime): - pass - - -class SMALLDATETIME(_DateTimeBase, sqltypes.DateTime): - __visit_name__ = 'SMALLDATETIME' - - -class DATETIME2(_DateTimeBase, sqltypes.DateTime): - __visit_name__ = 'DATETIME2' - - def __init__(self, precision=None, **kw): - super(DATETIME2, self).__init__(**kw) - self.precision = precision - - -# TODO: is this not an Interval ? -class DATETIMEOFFSET(sqltypes.TypeEngine): - __visit_name__ = 'DATETIMEOFFSET' - - def __init__(self, precision=None, **kwargs): - self.precision = precision - - -class _StringType(object): - """Base for MSSQL string types.""" - - def __init__(self, collation=None): - super(_StringType, self).__init__(collation=collation) - - - - -class NTEXT(sqltypes.UnicodeText): - """MSSQL NTEXT type, for variable-length unicode text up to 2^30 - characters.""" - - __visit_name__ = 'NTEXT' - - - -class IMAGE(sqltypes.LargeBinary): - __visit_name__ = 'IMAGE' - - -class BIT(sqltypes.TypeEngine): - __visit_name__ = 'BIT' - - -class MONEY(sqltypes.TypeEngine): - __visit_name__ = 'MONEY' - - -class SMALLMONEY(sqltypes.TypeEngine): - __visit_name__ = 'SMALLMONEY' - - -class UNIQUEIDENTIFIER(sqltypes.TypeEngine): - __visit_name__ = "UNIQUEIDENTIFIER" - - -class SQL_VARIANT(sqltypes.TypeEngine): - __visit_name__ = 'SQL_VARIANT' - -# old names. -MSDateTime = _MSDateTime -MSDate = _MSDate -MSReal = REAL -MSTinyInteger = TINYINT -MSTime = TIME -MSSmallDateTime = SMALLDATETIME -MSDateTime2 = DATETIME2 -MSDateTimeOffset = DATETIMEOFFSET -MSText = TEXT -MSNText = NTEXT -MSString = VARCHAR -MSNVarchar = NVARCHAR -MSChar = CHAR -MSNChar = NCHAR -MSBinary = BINARY -MSVarBinary = VARBINARY -MSImage = IMAGE -MSBit = BIT -MSMoney = MONEY -MSSmallMoney = SMALLMONEY -MSUniqueIdentifier = UNIQUEIDENTIFIER -MSVariant = SQL_VARIANT - -ischema_names = { - 'int': INTEGER, - 'bigint': BIGINT, - 'smallint': SMALLINT, - 'tinyint': TINYINT, - 'varchar': VARCHAR, - 'nvarchar': NVARCHAR, - 'char': CHAR, - 'nchar': NCHAR, - 'text': TEXT, - 'ntext': NTEXT, - 'decimal': DECIMAL, - 'numeric': NUMERIC, - 'float': FLOAT, - 'datetime': DATETIME, - 'datetime2': DATETIME2, - 'datetimeoffset': DATETIMEOFFSET, - 'date': DATE, - 'time': TIME, - 'smalldatetime': SMALLDATETIME, - 'binary': BINARY, - 'varbinary': VARBINARY, - 'bit': BIT, - 'real': REAL, - 'image': IMAGE, - 'timestamp': TIMESTAMP, - 'money': MONEY, - 'smallmoney': SMALLMONEY, - 'uniqueidentifier': UNIQUEIDENTIFIER, - 'sql_variant': SQL_VARIANT, -} - - -class MSTypeCompiler(compiler.GenericTypeCompiler): - def _extend(self, spec, type_, length=None): - """Extend a string-type declaration with standard SQL - COLLATE annotations. - - """ - - if getattr(type_, 'collation', None): - collation = 'COLLATE %s' % type_.collation - else: - collation = None - - if not length: - length = type_.length - - if length: - spec = spec + "(%s)" % length - - return ' '.join([c for c in (spec, collation) - if c is not None]) - - def visit_FLOAT(self, type_): - precision = getattr(type_, 'precision', None) - if precision is None: - return "FLOAT" - else: - return "FLOAT(%(precision)s)" % {'precision': precision} - - def visit_TINYINT(self, type_): - return "TINYINT" - - def visit_DATETIMEOFFSET(self, type_): - if type_.precision: - return "DATETIMEOFFSET(%s)" % type_.precision - else: - return "DATETIMEOFFSET" - - def visit_TIME(self, type_): - precision = getattr(type_, 'precision', None) - if precision: - return "TIME(%s)" % precision - else: - return "TIME" - - def visit_DATETIME2(self, type_): - precision = getattr(type_, 'precision', None) - if precision: - return "DATETIME2(%s)" % precision - else: - return "DATETIME2" - - def visit_SMALLDATETIME(self, type_): - return "SMALLDATETIME" - - def visit_unicode(self, type_): - return self.visit_NVARCHAR(type_) - - def visit_unicode_text(self, type_): - return self.visit_NTEXT(type_) - - def visit_NTEXT(self, type_): - return self._extend("NTEXT", type_) - - def visit_TEXT(self, type_): - return self._extend("TEXT", type_) - - def visit_VARCHAR(self, type_): - return self._extend("VARCHAR", type_, length=type_.length or 'max') - - def visit_CHAR(self, type_): - return self._extend("CHAR", type_) - - def visit_NCHAR(self, type_): - return self._extend("NCHAR", type_) - - def visit_NVARCHAR(self, type_): - return self._extend("NVARCHAR", type_, length=type_.length or 'max') - - def visit_date(self, type_): - if self.dialect.server_version_info < MS_2008_VERSION: - return self.visit_DATETIME(type_) - else: - return self.visit_DATE(type_) - - def visit_time(self, type_): - if self.dialect.server_version_info < MS_2008_VERSION: - return self.visit_DATETIME(type_) - else: - return self.visit_TIME(type_) - - def visit_large_binary(self, type_): - return self.visit_IMAGE(type_) - - def visit_IMAGE(self, type_): - return "IMAGE" - - def visit_VARBINARY(self, type_): - return self._extend( - "VARBINARY", - type_, - length=type_.length or 'max') - - def visit_boolean(self, type_): - return self.visit_BIT(type_) - - def visit_BIT(self, type_): - return "BIT" - - def visit_MONEY(self, type_): - return "MONEY" - - def visit_SMALLMONEY(self, type_): - return 'SMALLMONEY' - - def visit_UNIQUEIDENTIFIER(self, type_): - return "UNIQUEIDENTIFIER" - - def visit_SQL_VARIANT(self, type_): - return 'SQL_VARIANT' - - -class MSExecutionContext(default.DefaultExecutionContext): - _enable_identity_insert = False - _select_lastrowid = False - _result_proxy = None - _lastrowid = None - - def pre_exec(self): - """Activate IDENTITY_INSERT if needed.""" - - if self.isinsert: - tbl = self.compiled.statement.table - seq_column = tbl._autoincrement_column - insert_has_sequence = seq_column is not None - - if insert_has_sequence: - self._enable_identity_insert = \ - seq_column.key in self.compiled_parameters[0] - else: - self._enable_identity_insert = False - - self._select_lastrowid = insert_has_sequence and \ - not self.compiled.returning and \ - not self._enable_identity_insert and \ - not self.executemany - - if self._enable_identity_insert: - self.root_connection._cursor_execute(self.cursor, - "SET IDENTITY_INSERT %s ON" % - self.dialect.identifier_preparer.format_table(tbl), - (), self) - - def post_exec(self): - """Disable IDENTITY_INSERT if enabled.""" - - conn = self.root_connection - if self._select_lastrowid: - if self.dialect.use_scope_identity: - conn._cursor_execute(self.cursor, - "SELECT scope_identity() AS lastrowid", (), self) - else: - conn._cursor_execute(self.cursor, - "SELECT @@identity AS lastrowid", (), self) - # fetchall() ensures the cursor is consumed without closing it - row = self.cursor.fetchall()[0] - self._lastrowid = int(row[0]) - - if (self.isinsert or self.isupdate or self.isdelete) and \ - self.compiled.returning: - self._result_proxy = engine.FullyBufferedResultProxy(self) - - if self._enable_identity_insert: - conn._cursor_execute(self.cursor, - "SET IDENTITY_INSERT %s OFF" % - self.dialect.identifier_preparer. - format_table(self.compiled.statement.table), - (), self) - - def get_lastrowid(self): - return self._lastrowid - - def handle_dbapi_exception(self, e): - if self._enable_identity_insert: - try: - self.cursor.execute( - "SET IDENTITY_INSERT %s OFF" % - self.dialect.identifier_preparer.\ - format_table(self.compiled.statement.table) - ) - except: - pass - - def get_result_proxy(self): - if self._result_proxy: - return self._result_proxy - else: - return engine.ResultProxy(self) - - -class MSSQLCompiler(compiler.SQLCompiler): - returning_precedes_values = True - - extract_map = util.update_copy( - compiler.SQLCompiler.extract_map, - { - 'doy': 'dayofyear', - 'dow': 'weekday', - 'milliseconds': 'millisecond', - 'microseconds': 'microsecond' - }) - - def __init__(self, *args, **kwargs): - self.tablealiases = {} - super(MSSQLCompiler, self).__init__(*args, **kwargs) - - def visit_now_func(self, fn, **kw): - return "CURRENT_TIMESTAMP" - - def visit_current_date_func(self, fn, **kw): - return "GETDATE()" - - def visit_length_func(self, fn, **kw): - return "LEN%s" % self.function_argspec(fn, **kw) - - def visit_char_length_func(self, fn, **kw): - return "LEN%s" % self.function_argspec(fn, **kw) - - def visit_concat_op_binary(self, binary, operator, **kw): - return "%s + %s" % \ - (self.process(binary.left, **kw), - self.process(binary.right, **kw)) - - def visit_true(self, expr, **kw): - return '1' - - def visit_false(self, expr, **kw): - return '0' - - def visit_match_op_binary(self, binary, operator, **kw): - return "CONTAINS (%s, %s)" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw)) - - def get_select_precolumns(self, select): - """ MS-SQL puts TOP, it's version of LIMIT here """ - if select._distinct or select._limit is not None: - s = select._distinct and "DISTINCT " or "" - - # ODBC drivers and possibly others - # don't support bind params in the SELECT clause on SQL Server. - # so have to use literal here. - if select._limit is not None: - if not select._offset: - s += "TOP %d " % select._limit - return s - return compiler.SQLCompiler.get_select_precolumns(self, select) - - def get_from_hint_text(self, table, text): - return text - - def get_crud_hint_text(self, table, text): - return text - - def limit_clause(self, select): - # Limit in mssql is after the select keyword - return "" - - def visit_select(self, select, **kwargs): - """Look for ``LIMIT`` and OFFSET in a select statement, and if - so tries to wrap it in a subquery with ``row_number()`` criterion. - - """ - if select._offset and not getattr(select, '_mssql_visit', None): - # to use ROW_NUMBER(), an ORDER BY is required. - if not select._order_by_clause.clauses: - raise exc.CompileError('MSSQL requires an order_by when ' - 'using an offset.') - - _offset = select._offset - _limit = select._limit - _order_by_clauses = select._order_by_clause.clauses - select = select._generate() - select._mssql_visit = True - select = select.column( - sql.func.ROW_NUMBER().over(order_by=_order_by_clauses) - .label("mssql_rn") - ).order_by(None).alias() - - mssql_rn = sql.column('mssql_rn') - limitselect = sql.select([c for c in select.c if - c.key != 'mssql_rn']) - limitselect.append_whereclause(mssql_rn > _offset) - if _limit is not None: - limitselect.append_whereclause(mssql_rn <= (_limit + _offset)) - return self.process(limitselect, iswrapper=True, **kwargs) - else: - return compiler.SQLCompiler.visit_select(self, select, **kwargs) - - def _schema_aliased_table(self, table): - if getattr(table, 'schema', None) is not None: - if table not in self.tablealiases: - self.tablealiases[table] = table.alias() - return self.tablealiases[table] - else: - return None - - def visit_table(self, table, mssql_aliased=False, iscrud=False, **kwargs): - if mssql_aliased is table or iscrud: - return super(MSSQLCompiler, self).visit_table(table, **kwargs) - - # alias schema-qualified tables - alias = self._schema_aliased_table(table) - if alias is not None: - return self.process(alias, mssql_aliased=table, **kwargs) - else: - return super(MSSQLCompiler, self).visit_table(table, **kwargs) - - def visit_alias(self, alias, **kwargs): - # translate for schema-qualified table aliases - kwargs['mssql_aliased'] = alias.original - return super(MSSQLCompiler, self).visit_alias(alias, **kwargs) - - def visit_extract(self, extract, **kw): - field = self.extract_map.get(extract.field, extract.field) - return 'DATEPART("%s", %s)' % \ - (field, self.process(extract.expr, **kw)) - - def visit_savepoint(self, savepoint_stmt): - return "SAVE TRANSACTION %s" % self.preparer.format_savepoint(savepoint_stmt) - - def visit_rollback_to_savepoint(self, savepoint_stmt): - return ("ROLLBACK TRANSACTION %s" - % self.preparer.format_savepoint(savepoint_stmt)) - - def visit_column(self, column, add_to_result_map=None, **kwargs): - if column.table is not None and \ - (not self.isupdate and not self.isdelete) or self.is_subquery(): - # translate for schema-qualified table aliases - t = self._schema_aliased_table(column.table) - if t is not None: - converted = expression._corresponding_column_or_error( - t, column) - if add_to_result_map is not None: - add_to_result_map( - column.name, - column.name, - (column, column.name, column.key), - column.type - ) - - return super(MSSQLCompiler, self).\ - visit_column(converted, **kwargs) - - return super(MSSQLCompiler, self).visit_column( - column, add_to_result_map=add_to_result_map, **kwargs) - - def visit_binary(self, binary, **kwargs): - """Move bind parameters to the right-hand side of an operator, where - possible. - - """ - if ( - isinstance(binary.left, expression.BindParameter) - and binary.operator == operator.eq - and not isinstance(binary.right, expression.BindParameter) - ): - return self.process( - expression.BinaryExpression(binary.right, - binary.left, - binary.operator), - **kwargs) - return super(MSSQLCompiler, self).visit_binary(binary, **kwargs) - - def returning_clause(self, stmt, returning_cols): - - if self.isinsert or self.isupdate: - target = stmt.table.alias("inserted") - else: - target = stmt.table.alias("deleted") - - adapter = sql_util.ClauseAdapter(target) - - columns = [ - self._label_select_column(None, adapter.traverse(c), - True, False, {}) - for c in expression._select_iterables(returning_cols) - ] - - return 'OUTPUT ' + ', '.join(columns) - - def get_cte_preamble(self, recursive): - # SQL Server finds it too inconvenient to accept - # an entirely optional, SQL standard specified, - # "RECURSIVE" word with their "WITH", - # so here we go - return "WITH" - - def label_select_column(self, select, column, asfrom): - if isinstance(column, expression.Function): - return column.label(None) - else: - return super(MSSQLCompiler, self).\ - label_select_column(select, column, asfrom) - - def for_update_clause(self, select): - # "FOR UPDATE" is only allowed on "DECLARE CURSOR" which - # SQLAlchemy doesn't use - return '' - - def order_by_clause(self, select, **kw): - order_by = self.process(select._order_by_clause, **kw) - - # MSSQL only allows ORDER BY in subqueries if there is a LIMIT - if order_by and (not self.is_subquery() or select._limit): - return " ORDER BY " + order_by - else: - return "" - - def update_from_clause(self, update_stmt, - from_table, extra_froms, - from_hints, - **kw): - """Render the UPDATE..FROM clause specific to MSSQL. - - In MSSQL, if the UPDATE statement involves an alias of the table to - be updated, then the table itself must be added to the FROM list as - well. Otherwise, it is optional. Here, we add it regardless. - - """ - return "FROM " + ', '.join( - t._compiler_dispatch(self, asfrom=True, - fromhints=from_hints, **kw) - for t in [from_table] + extra_froms) - - -class MSSQLStrictCompiler(MSSQLCompiler): - """A subclass of MSSQLCompiler which disables the usage of bind - parameters where not allowed natively by MS-SQL. - - A dialect may use this compiler on a platform where native - binds are used. - - """ - ansi_bind_rules = True - - def visit_in_op_binary(self, binary, operator, **kw): - kw['literal_binds'] = True - return "%s IN %s" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw) - ) - - def visit_notin_op_binary(self, binary, operator, **kw): - kw['literal_binds'] = True - return "%s NOT IN %s" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw) - ) - - def render_literal_value(self, value, type_): - """ - For date and datetime values, convert to a string - format acceptable to MSSQL. That seems to be the - so-called ODBC canonical date format which looks - like this: - - yyyy-mm-dd hh:mi:ss.mmm(24h) - - For other data types, call the base class implementation. - """ - # datetime and date are both subclasses of datetime.date - if issubclass(type(value), datetime.date): - # SQL Server wants single quotes around the date string. - return "'" + str(value) + "'" - else: - return super(MSSQLStrictCompiler, self).\ - render_literal_value(value, type_) - - -class MSDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, **kwargs): - colspec = (self.preparer.format_column(column) + " " - + self.dialect.type_compiler.process(column.type)) - - if column.nullable is not None: - if not column.nullable or column.primary_key or \ - isinstance(column.default, sa_schema.Sequence): - colspec += " NOT NULL" - else: - colspec += " NULL" - - if column.table is None: - raise exc.CompileError( - "mssql requires Table-bound columns " - "in order to generate DDL") - - # install an IDENTITY Sequence if we either a sequence or an implicit IDENTITY column - if isinstance(column.default, sa_schema.Sequence): - if column.default.start == 0: - start = 0 - else: - start = column.default.start or 1 - - colspec += " IDENTITY(%s,%s)" % (start, column.default.increment or 1) - elif column is column.table._autoincrement_column: - colspec += " IDENTITY(1,1)" - else: - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - return colspec - - def visit_create_index(self, create, include_schema=False): - index = create.element - self._verify_index_table(index) - preparer = self.preparer - text = "CREATE " - if index.unique: - text += "UNIQUE " - - # handle clustering option - if index.kwargs.get("mssql_clustered"): - text += "CLUSTERED " - - text += "INDEX %s ON %s (%s)" \ - % ( - self._prepared_index_name(index, - include_schema=include_schema), - preparer.format_table(index.table), - ', '.join( - self.sql_compiler.process(expr, - include_table=False, literal_binds=True) for - expr in index.expressions) - ) - - # handle other included columns - if index.kwargs.get("mssql_include"): - inclusions = [index.table.c[col] - if isinstance(col, util.string_types) else col - for col in index.kwargs["mssql_include"]] - - text += " INCLUDE (%s)" \ - % ', '.join([preparer.quote(c.name) - for c in inclusions]) - - return text - - def visit_drop_index(self, drop): - return "\nDROP INDEX %s ON %s" % ( - self._prepared_index_name(drop.element, include_schema=False), - self.preparer.format_table(drop.element.table) - ) - -class MSIdentifierPreparer(compiler.IdentifierPreparer): - reserved_words = RESERVED_WORDS - - def __init__(self, dialect): - super(MSIdentifierPreparer, self).__init__(dialect, initial_quote='[', - final_quote=']') - - def _escape_identifier(self, value): - return value - - def quote_schema(self, schema, force=None): - """Prepare a quoted table and schema name.""" - result = '.'.join([self.quote(x, force) for x in schema.split('.')]) - return result - - -def _db_plus_owner_listing(fn): - def wrap(dialect, connection, schema=None, **kw): - dbname, owner = _owner_plus_db(dialect, schema) - return _switch_db(dbname, connection, fn, dialect, connection, - dbname, owner, schema, **kw) - return update_wrapper(wrap, fn) - - -def _db_plus_owner(fn): - def wrap(dialect, connection, tablename, schema=None, **kw): - dbname, owner = _owner_plus_db(dialect, schema) - return _switch_db(dbname, connection, fn, dialect, connection, - tablename, dbname, owner, schema, **kw) - return update_wrapper(wrap, fn) - - -def _switch_db(dbname, connection, fn, *arg, **kw): - if dbname: - current_db = connection.scalar("select db_name()") - connection.execute("use %s" % dbname) - try: - return fn(*arg, **kw) - finally: - if dbname: - connection.execute("use %s" % current_db) - - -def _owner_plus_db(dialect, schema): - if not schema: - return None, dialect.default_schema_name - elif "." in schema: - return schema.split(".", 1) - else: - return None, schema - - -class MSDialect(default.DefaultDialect): - name = 'mssql' - supports_default_values = True - supports_empty_insert = False - execution_ctx_cls = MSExecutionContext - use_scope_identity = True - max_identifier_length = 128 - schema_name = "dbo" - - colspecs = { - sqltypes.DateTime: _MSDateTime, - sqltypes.Date: _MSDate, - sqltypes.Time: TIME, - } - - ischema_names = ischema_names - - supports_native_boolean = False - supports_unicode_binds = True - postfetch_lastrowid = True - - server_version_info = () - - statement_compiler = MSSQLCompiler - ddl_compiler = MSDDLCompiler - type_compiler = MSTypeCompiler - preparer = MSIdentifierPreparer - - def __init__(self, - query_timeout=None, - use_scope_identity=True, - max_identifier_length=None, - schema_name="dbo", **opts): - self.query_timeout = int(query_timeout or 0) - self.schema_name = schema_name - - self.use_scope_identity = use_scope_identity - self.max_identifier_length = int(max_identifier_length or 0) or \ - self.max_identifier_length - super(MSDialect, self).__init__(**opts) - - def do_savepoint(self, connection, name): - # give the DBAPI a push - connection.execute("IF @@TRANCOUNT = 0 BEGIN TRANSACTION") - super(MSDialect, self).do_savepoint(connection, name) - - def do_release_savepoint(self, connection, name): - # SQL Server does not support RELEASE SAVEPOINT - pass - - def initialize(self, connection): - super(MSDialect, self).initialize(connection) - if self.server_version_info[0] not in list(range(8, 17)): - # FreeTDS with version 4.2 seems to report here - # a number like "95.10.255". Don't know what - # that is. So emit warning. - util.warn( - "Unrecognized server version info '%s'. Version specific " - "behaviors may not function properly. If using ODBC " - "with FreeTDS, ensure server version 7.0 or 8.0, not 4.2, " - "is configured in the FreeTDS configuration." % - ".".join(str(x) for x in self.server_version_info)) - if self.server_version_info >= MS_2005_VERSION and \ - 'implicit_returning' not in self.__dict__: - self.implicit_returning = True - - def _get_default_schema_name(self, connection): - user_name = connection.scalar("SELECT user_name()") - if user_name is not None: - # now, get the default schema - query = sql.text(""" - SELECT default_schema_name FROM - sys.database_principals - WHERE name = :name - AND type = 'S' - """) - try: - default_schema_name = connection.scalar(query, name=user_name) - if default_schema_name is not None: - return util.text_type(default_schema_name) - except: - pass - return self.schema_name - - @_db_plus_owner - def has_table(self, connection, tablename, dbname, owner, schema): - columns = ischema.columns - - whereclause = columns.c.table_name == tablename - - if owner: - whereclause = sql.and_(whereclause, - columns.c.table_schema == owner) - s = sql.select([columns], whereclause) - c = connection.execute(s) - return c.first() is not None - - @reflection.cache - def get_schema_names(self, connection, **kw): - s = sql.select([ischema.schemata.c.schema_name], - order_by=[ischema.schemata.c.schema_name] - ) - schema_names = [r[0] for r in connection.execute(s)] - return schema_names - - @reflection.cache - @_db_plus_owner_listing - def get_table_names(self, connection, dbname, owner, schema, **kw): - tables = ischema.tables - s = sql.select([tables.c.table_name], - sql.and_( - tables.c.table_schema == owner, - tables.c.table_type == 'BASE TABLE' - ), - order_by=[tables.c.table_name] - ) - table_names = [r[0] for r in connection.execute(s)] - return table_names - - @reflection.cache - @_db_plus_owner_listing - def get_view_names(self, connection, dbname, owner, schema, **kw): - tables = ischema.tables - s = sql.select([tables.c.table_name], - sql.and_( - tables.c.table_schema == owner, - tables.c.table_type == 'VIEW' - ), - order_by=[tables.c.table_name] - ) - view_names = [r[0] for r in connection.execute(s)] - return view_names - - @reflection.cache - @_db_plus_owner - def get_indexes(self, connection, tablename, dbname, owner, schema, **kw): - # using system catalogs, don't support index reflection - # below MS 2005 - if self.server_version_info < MS_2005_VERSION: - return [] - - rp = connection.execute( - sql.text("select ind.index_id, ind.is_unique, ind.name " - "from sys.indexes as ind join sys.tables as tab on " - "ind.object_id=tab.object_id " - "join sys.schemas as sch on sch.schema_id=tab.schema_id " - "where tab.name = :tabname " - "and sch.name=:schname " - "and ind.is_primary_key=0", - bindparams=[ - sql.bindparam('tabname', tablename, - sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', owner, - sqltypes.String(convert_unicode=True)) - ], - typemap={ - 'name': sqltypes.Unicode() - } - ) - ) - indexes = {} - for row in rp: - indexes[row['index_id']] = { - 'name': row['name'], - 'unique': row['is_unique'] == 1, - 'column_names': [] - } - rp = connection.execute( - sql.text( - "select ind_col.index_id, ind_col.object_id, col.name " - "from sys.columns as col " - "join sys.tables as tab on tab.object_id=col.object_id " - "join sys.index_columns as ind_col on " - "(ind_col.column_id=col.column_id and " - "ind_col.object_id=tab.object_id) " - "join sys.schemas as sch on sch.schema_id=tab.schema_id " - "where tab.name=:tabname " - "and sch.name=:schname", - bindparams=[ - sql.bindparam('tabname', tablename, - sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', owner, - sqltypes.String(convert_unicode=True)) - ], - typemap={'name': sqltypes.Unicode()} - ), - ) - for row in rp: - if row['index_id'] in indexes: - indexes[row['index_id']]['column_names'].append(row['name']) - - return list(indexes.values()) - - @reflection.cache - @_db_plus_owner - def get_view_definition(self, connection, viewname, dbname, owner, schema, **kw): - rp = connection.execute( - sql.text( - "select definition from sys.sql_modules as mod, " - "sys.views as views, " - "sys.schemas as sch" - " where " - "mod.object_id=views.object_id and " - "views.schema_id=sch.schema_id and " - "views.name=:viewname and sch.name=:schname", - bindparams=[ - sql.bindparam('viewname', viewname, - sqltypes.String(convert_unicode=True)), - sql.bindparam('schname', owner, - sqltypes.String(convert_unicode=True)) - ] - ) - ) - - if rp: - view_def = rp.scalar() - return view_def - - @reflection.cache - @_db_plus_owner - def get_columns(self, connection, tablename, dbname, owner, schema, **kw): - # Get base columns - columns = ischema.columns - if owner: - whereclause = sql.and_(columns.c.table_name == tablename, - columns.c.table_schema == owner) - else: - whereclause = columns.c.table_name == tablename - s = sql.select([columns], whereclause, - order_by=[columns.c.ordinal_position]) - - c = connection.execute(s) - cols = [] - while True: - row = c.fetchone() - if row is None: - break - (name, type, nullable, charlen, - numericprec, numericscale, default, collation) = ( - row[columns.c.column_name], - row[columns.c.data_type], - row[columns.c.is_nullable] == 'YES', - row[columns.c.character_maximum_length], - row[columns.c.numeric_precision], - row[columns.c.numeric_scale], - row[columns.c.column_default], - row[columns.c.collation_name] - ) - coltype = self.ischema_names.get(type, None) - - kwargs = {} - if coltype in (MSString, MSChar, MSNVarchar, MSNChar, MSText, - MSNText, MSBinary, MSVarBinary, - sqltypes.LargeBinary): - kwargs['length'] = charlen - if collation: - kwargs['collation'] = collation - if coltype == MSText or \ - (coltype in (MSString, MSNVarchar) and charlen == -1): - kwargs.pop('length') - - if coltype is None: - util.warn( - "Did not recognize type '%s' of column '%s'" % - (type, name)) - coltype = sqltypes.NULLTYPE - else: - if issubclass(coltype, sqltypes.Numeric) and \ - coltype is not MSReal: - kwargs['scale'] = numericscale - kwargs['precision'] = numericprec - - coltype = coltype(**kwargs) - cdict = { - 'name': name, - 'type': coltype, - 'nullable': nullable, - 'default': default, - 'autoincrement': False, - } - cols.append(cdict) - # autoincrement and identity - colmap = {} - for col in cols: - colmap[col['name']] = col - # We also run an sp_columns to check for identity columns: - cursor = connection.execute("sp_columns @table_name = '%s', " - "@table_owner = '%s'" - % (tablename, owner)) - ic = None - while True: - row = cursor.fetchone() - if row is None: - break - (col_name, type_name) = row[3], row[5] - if type_name.endswith("identity") and col_name in colmap: - ic = col_name - colmap[col_name]['autoincrement'] = True - colmap[col_name]['sequence'] = dict( - name='%s_identity' % col_name) - break - cursor.close() - - if ic is not None and self.server_version_info >= MS_2005_VERSION: - table_fullname = "%s.%s" % (owner, tablename) - cursor = connection.execute( - "select ident_seed('%s'), ident_incr('%s')" - % (table_fullname, table_fullname) - ) - - row = cursor.first() - if row is not None and row[0] is not None: - colmap[ic]['sequence'].update({ - 'start': int(row[0]), - 'increment': int(row[1]) - }) - return cols - - @reflection.cache - @_db_plus_owner - def get_pk_constraint(self, connection, tablename, dbname, owner, schema, **kw): - pkeys = [] - TC = ischema.constraints - C = ischema.key_constraints.alias('C') - - # Primary key constraints - s = sql.select([C.c.column_name, TC.c.constraint_type, C.c.constraint_name], - sql.and_(TC.c.constraint_name == C.c.constraint_name, - TC.c.table_schema == C.c.table_schema, - C.c.table_name == tablename, - C.c.table_schema == owner) - ) - c = connection.execute(s) - constraint_name = None - for row in c: - if 'PRIMARY' in row[TC.c.constraint_type.name]: - pkeys.append(row[0]) - if constraint_name is None: - constraint_name = row[C.c.constraint_name.name] - return {'constrained_columns': pkeys, 'name': constraint_name} - - @reflection.cache - @_db_plus_owner - def get_foreign_keys(self, connection, tablename, dbname, owner, schema, **kw): - RR = ischema.ref_constraints - C = ischema.key_constraints.alias('C') - R = ischema.key_constraints.alias('R') - - # Foreign key constraints - s = sql.select([C.c.column_name, - R.c.table_schema, R.c.table_name, R.c.column_name, - RR.c.constraint_name, RR.c.match_option, - RR.c.update_rule, - RR.c.delete_rule], - sql.and_(C.c.table_name == tablename, - C.c.table_schema == owner, - C.c.constraint_name == RR.c.constraint_name, - R.c.constraint_name == - RR.c.unique_constraint_name, - C.c.ordinal_position == R.c.ordinal_position - ), - order_by=[RR.c.constraint_name, R.c.ordinal_position] - ) - - # group rows by constraint ID, to handle multi-column FKs - fkeys = [] - fknm, scols, rcols = (None, [], []) - - def fkey_rec(): - return { - 'name': None, - 'constrained_columns': [], - 'referred_schema': None, - 'referred_table': None, - 'referred_columns': [] - } - - fkeys = util.defaultdict(fkey_rec) - - for r in connection.execute(s).fetchall(): - scol, rschema, rtbl, rcol, rfknm, fkmatch, fkuprule, fkdelrule = r - - rec = fkeys[rfknm] - rec['name'] = rfknm - if not rec['referred_table']: - rec['referred_table'] = rtbl - if schema is not None or owner != rschema: - if dbname: - rschema = dbname + "." + rschema - rec['referred_schema'] = rschema - - local_cols, remote_cols = \ - rec['constrained_columns'],\ - rec['referred_columns'] - - local_cols.append(scol) - remote_cols.append(rcol) - - return list(fkeys.values()) diff --git a/libs/sqlalchemy/dialects/mssql/information_schema.py b/libs/sqlalchemy/dialects/mssql/information_schema.py deleted file mode 100644 index 26e70f7f..00000000 --- a/libs/sqlalchemy/dialects/mssql/information_schema.py +++ /dev/null @@ -1,114 +0,0 @@ -# mssql/information_schema.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -# TODO: should be using the sys. catalog with SQL Server, not information schema - -from ... import Table, MetaData, Column -from ...types import String, Unicode, UnicodeText, Integer, TypeDecorator -from ... import cast -from ... import util -from ...sql import expression -from ...ext.compiler import compiles - -ischema = MetaData() - -class CoerceUnicode(TypeDecorator): - impl = Unicode - - def process_bind_param(self, value, dialect): - if util.py2k and isinstance(value, util.binary_type): - value = value.decode(dialect.encoding) - return value - - def bind_expression(self, bindvalue): - return _cast_on_2005(bindvalue) - -class _cast_on_2005(expression.ColumnElement): - def __init__(self, bindvalue): - self.bindvalue = bindvalue - -@compiles(_cast_on_2005) -def _compile(element, compiler, **kw): - from . import base - if compiler.dialect.server_version_info < base.MS_2005_VERSION: - return compiler.process(element.bindvalue, **kw) - else: - return compiler.process(cast(element.bindvalue, Unicode), **kw) - -schemata = Table("SCHEMATA", ischema, - Column("CATALOG_NAME", CoerceUnicode, key="catalog_name"), - Column("SCHEMA_NAME", CoerceUnicode, key="schema_name"), - Column("SCHEMA_OWNER", CoerceUnicode, key="schema_owner"), - schema="INFORMATION_SCHEMA") - -tables = Table("TABLES", ischema, - Column("TABLE_CATALOG", CoerceUnicode, key="table_catalog"), - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("TABLE_TYPE", String(convert_unicode=True), key="table_type"), - schema="INFORMATION_SCHEMA") - -columns = Table("COLUMNS", ischema, - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("COLUMN_NAME", CoerceUnicode, key="column_name"), - Column("IS_NULLABLE", Integer, key="is_nullable"), - Column("DATA_TYPE", String, key="data_type"), - Column("ORDINAL_POSITION", Integer, key="ordinal_position"), - Column("CHARACTER_MAXIMUM_LENGTH", Integer, key="character_maximum_length"), - Column("NUMERIC_PRECISION", Integer, key="numeric_precision"), - Column("NUMERIC_SCALE", Integer, key="numeric_scale"), - Column("COLUMN_DEFAULT", Integer, key="column_default"), - Column("COLLATION_NAME", String, key="collation_name"), - schema="INFORMATION_SCHEMA") - -constraints = Table("TABLE_CONSTRAINTS", ischema, - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("CONSTRAINT_NAME", CoerceUnicode, key="constraint_name"), - Column("CONSTRAINT_TYPE", String(convert_unicode=True), key="constraint_type"), - schema="INFORMATION_SCHEMA") - -column_constraints = Table("CONSTRAINT_COLUMN_USAGE", ischema, - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("COLUMN_NAME", CoerceUnicode, key="column_name"), - Column("CONSTRAINT_NAME", CoerceUnicode, key="constraint_name"), - schema="INFORMATION_SCHEMA") - -key_constraints = Table("KEY_COLUMN_USAGE", ischema, - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("COLUMN_NAME", CoerceUnicode, key="column_name"), - Column("CONSTRAINT_NAME", CoerceUnicode, key="constraint_name"), - Column("ORDINAL_POSITION", Integer, key="ordinal_position"), - schema="INFORMATION_SCHEMA") - -ref_constraints = Table("REFERENTIAL_CONSTRAINTS", ischema, - Column("CONSTRAINT_CATALOG", CoerceUnicode, key="constraint_catalog"), - Column("CONSTRAINT_SCHEMA", CoerceUnicode, key="constraint_schema"), - Column("CONSTRAINT_NAME", CoerceUnicode, key="constraint_name"), - # TODO: is CATLOG misspelled ? - Column("UNIQUE_CONSTRAINT_CATLOG", CoerceUnicode, - key="unique_constraint_catalog"), - - Column("UNIQUE_CONSTRAINT_SCHEMA", CoerceUnicode, - key="unique_constraint_schema"), - Column("UNIQUE_CONSTRAINT_NAME", CoerceUnicode, - key="unique_constraint_name"), - Column("MATCH_OPTION", String, key="match_option"), - Column("UPDATE_RULE", String, key="update_rule"), - Column("DELETE_RULE", String, key="delete_rule"), - schema="INFORMATION_SCHEMA") - -views = Table("VIEWS", ischema, - Column("TABLE_CATALOG", CoerceUnicode, key="table_catalog"), - Column("TABLE_SCHEMA", CoerceUnicode, key="table_schema"), - Column("TABLE_NAME", CoerceUnicode, key="table_name"), - Column("VIEW_DEFINITION", CoerceUnicode, key="view_definition"), - Column("CHECK_OPTION", String, key="check_option"), - Column("IS_UPDATABLE", String, key="is_updatable"), - schema="INFORMATION_SCHEMA") diff --git a/libs/sqlalchemy/dialects/mssql/mxodbc.py b/libs/sqlalchemy/dialects/mssql/mxodbc.py deleted file mode 100644 index 5b686c47..00000000 --- a/libs/sqlalchemy/dialects/mssql/mxodbc.py +++ /dev/null @@ -1,111 +0,0 @@ -# mssql/mxodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql+mxodbc - :name: mxODBC - :dbapi: mxodbc - :connectstring: mssql+mxodbc://:@ - :url: http://www.egenix.com/ - -Execution Modes ---------------- - -mxODBC features two styles of statement execution, using the -``cursor.execute()`` and ``cursor.executedirect()`` methods (the second being -an extension to the DBAPI specification). The former makes use of a particular -API call specific to the SQL Server Native Client ODBC driver known -SQLDescribeParam, while the latter does not. - -mxODBC apparently only makes repeated use of a single prepared statement -when SQLDescribeParam is used. The advantage to prepared statement reuse is -one of performance. The disadvantage is that SQLDescribeParam has a limited -set of scenarios in which bind parameters are understood, including that they -cannot be placed within the argument lists of function calls, anywhere outside -the FROM, or even within subqueries within the FROM clause - making the usage -of bind parameters within SELECT statements impossible for all but the most -simplistic statements. - -For this reason, the mxODBC dialect uses the "native" mode by default only for -INSERT, UPDATE, and DELETE statements, and uses the escaped string mode for -all other statements. - -This behavior can be controlled via -:meth:`~sqlalchemy.sql.expression.Executable.execution_options` using the -``native_odbc_execute`` flag with a value of ``True`` or ``False``, where a -value of ``True`` will unconditionally use native bind parameters and a value -of ``False`` will unconditionally use string-escaped parameters. - -""" - - -from ... import types as sqltypes -from ...connectors.mxodbc import MxODBCConnector -from .pyodbc import MSExecutionContext_pyodbc, _MSNumeric_pyodbc -from .base import (MSDialect, - MSSQLStrictCompiler, - _MSDateTime, _MSDate, _MSTime) - - -class _MSNumeric_mxodbc(_MSNumeric_pyodbc): - """Include pyodbc's numeric processor. - """ - - -class _MSDate_mxodbc(_MSDate): - def bind_processor(self, dialect): - def process(value): - if value is not None: - return "%s-%s-%s" % (value.year, value.month, value.day) - else: - return None - return process - - -class _MSTime_mxodbc(_MSTime): - def bind_processor(self, dialect): - def process(value): - if value is not None: - return "%s:%s:%s" % (value.hour, value.minute, value.second) - else: - return None - return process - - -class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc): - """ - The pyodbc execution context is useful for enabling - SELECT SCOPE_IDENTITY in cases where OUTPUT clause - does not work (tables with insert triggers). - """ - #todo - investigate whether the pyodbc execution context - # is really only being used in cases where OUTPUT - # won't work. - - -class MSDialect_mxodbc(MxODBCConnector, MSDialect): - - # this is only needed if "native ODBC" mode is used, - # which is now disabled by default. - #statement_compiler = MSSQLStrictCompiler - - execution_ctx_cls = MSExecutionContext_mxodbc - - # flag used by _MSNumeric_mxodbc - _need_decimal_fix = True - - colspecs = { - sqltypes.Numeric: _MSNumeric_mxodbc, - sqltypes.DateTime: _MSDateTime, - sqltypes.Date: _MSDate_mxodbc, - sqltypes.Time: _MSTime_mxodbc, - } - - def __init__(self, description_encoding=None, **params): - super(MSDialect_mxodbc, self).__init__(**params) - self.description_encoding = description_encoding - -dialect = MSDialect_mxodbc diff --git a/libs/sqlalchemy/dialects/mssql/pymssql.py b/libs/sqlalchemy/dialects/mssql/pymssql.py deleted file mode 100644 index 021219cb..00000000 --- a/libs/sqlalchemy/dialects/mssql/pymssql.py +++ /dev/null @@ -1,100 +0,0 @@ -# mssql/pymssql.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql+pymssql - :name: pymssql - :dbapi: pymssql - :connectstring: mssql+pymssql://:@?charset=utf8 - :url: http://pymssql.sourceforge.net/ - -Limitations ------------ - -pymssql inherits a lot of limitations from FreeTDS, including: - -* no support for multibyte schema identifiers -* poor support for large decimals -* poor support for binary fields -* poor support for VARCHAR/CHAR fields over 255 characters - -Please consult the pymssql documentation for further information. - -""" -from .base import MSDialect -from ... import types as sqltypes, util, processors -import re - - -class _MSNumeric_pymssql(sqltypes.Numeric): - def result_processor(self, dialect, type_): - if not self.asdecimal: - return processors.to_float - else: - return sqltypes.Numeric.result_processor(self, dialect, type_) - - -class MSDialect_pymssql(MSDialect): - supports_sane_rowcount = False - driver = 'pymssql' - - colspecs = util.update_copy( - MSDialect.colspecs, - { - sqltypes.Numeric: _MSNumeric_pymssql, - sqltypes.Float: sqltypes.Float, - } - ) - - @classmethod - def dbapi(cls): - module = __import__('pymssql') - # pymmsql doesn't have a Binary method. we use string - # TODO: monkeypatching here is less than ideal - module.Binary = lambda x: x if hasattr(x, 'decode') else str(x) - - client_ver = tuple(int(x) for x in module.__version__.split(".")) - if client_ver < (1, ): - util.warn("The pymssql dialect expects at least " - "the 1.0 series of the pymssql DBAPI.") - return module - - def __init__(self, **params): - super(MSDialect_pymssql, self).__init__(**params) - self.use_scope_identity = True - - def _get_server_version_info(self, connection): - vers = connection.scalar("select @@version") - m = re.match( - r"Microsoft SQL Server.*? - (\d+).(\d+).(\d+).(\d+)", vers) - if m: - return tuple(int(x) for x in m.group(1, 2, 3, 4)) - else: - return None - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - opts.update(url.query) - port = opts.pop('port', None) - if port and 'host' in opts: - opts['host'] = "%s:%s" % (opts['host'], port) - return [[], opts] - - def is_disconnect(self, e, connection, cursor): - for msg in ( - "Adaptive Server connection timed out", - "Net-Lib error during Connection reset by peer", - "message 20003", # connection timeout - "Error 10054", - "Not connected to any MS SQL server", - "Connection is closed" - ): - if msg in str(e): - return True - else: - return False - -dialect = MSDialect_pymssql diff --git a/libs/sqlalchemy/dialects/mssql/pyodbc.py b/libs/sqlalchemy/dialects/mssql/pyodbc.py deleted file mode 100644 index 8c43eb8a..00000000 --- a/libs/sqlalchemy/dialects/mssql/pyodbc.py +++ /dev/null @@ -1,260 +0,0 @@ -# mssql/pyodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql+pyodbc - :name: PyODBC - :dbapi: pyodbc - :connectstring: mssql+pyodbc://:@ - :url: http://pypi.python.org/pypi/pyodbc/ - -Additional Connection Examples -------------------------------- - -Examples of pyodbc connection string URLs: - -* ``mssql+pyodbc://mydsn`` - connects using the specified DSN named ``mydsn``. - The connection string that is created will appear like:: - - dsn=mydsn;Trusted_Connection=Yes - -* ``mssql+pyodbc://user:pass@mydsn`` - connects using the DSN named - ``mydsn`` passing in the ``UID`` and ``PWD`` information. The - connection string that is created will appear like:: - - dsn=mydsn;UID=user;PWD=pass - -* ``mssql+pyodbc://user:pass@mydsn/?LANGUAGE=us_english`` - connects - using the DSN named ``mydsn`` passing in the ``UID`` and ``PWD`` - information, plus the additional connection configuration option - ``LANGUAGE``. The connection string that is created will appear - like:: - - dsn=mydsn;UID=user;PWD=pass;LANGUAGE=us_english - -* ``mssql+pyodbc://user:pass@host/db`` - connects using a connection - that would appear like:: - - DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass - -* ``mssql+pyodbc://user:pass@host:123/db`` - connects using a connection - string which includes the port - information using the comma syntax. This will create the following - connection string:: - - DRIVER={SQL Server};Server=host,123;Database=db;UID=user;PWD=pass - -* ``mssql+pyodbc://user:pass@host/db?port=123`` - connects using a connection - string that includes the port - information as a separate ``port`` keyword. This will create the - following connection string:: - - DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass;port=123 - -* ``mssql+pyodbc://user:pass@host/db?driver=MyDriver`` - connects using a connection - string that includes a custom - ODBC driver name. This will create the following connection string:: - - DRIVER={MyDriver};Server=host;Database=db;UID=user;PWD=pass - -If you require a connection string that is outside the options -presented above, use the ``odbc_connect`` keyword to pass in a -urlencoded connection string. What gets passed in will be urldecoded -and passed directly. - -For example:: - - mssql+pyodbc:///?odbc_connect=dsn%3Dmydsn%3BDatabase%3Ddb - -would create the following connection string:: - - dsn=mydsn;Database=db - -Encoding your connection string can be easily accomplished through -the python shell. For example:: - - >>> import urllib - >>> urllib.quote_plus('dsn=mydsn;Database=db') - 'dsn%3Dmydsn%3BDatabase%3Ddb' - -Unicode Binds -------------- - -The current state of PyODBC on a unix backend with FreeTDS and/or -EasySoft is poor regarding unicode; different OS platforms and versions of UnixODBC -versus IODBC versus FreeTDS/EasySoft versus PyODBC itself dramatically -alter how strings are received. The PyODBC dialect attempts to use all the information -it knows to determine whether or not a Python unicode literal can be -passed directly to the PyODBC driver or not; while SQLAlchemy can encode -these to bytestrings first, some users have reported that PyODBC mis-handles -bytestrings for certain encodings and requires a Python unicode object, -while the author has observed widespread cases where a Python unicode -is completely misinterpreted by PyODBC, particularly when dealing with -the information schema tables used in table reflection, and the value -must first be encoded to a bytestring. - -It is for this reason that whether or not unicode literals for bound -parameters be sent to PyODBC can be controlled using the -``supports_unicode_binds`` parameter to ``create_engine()``. When -left at its default of ``None``, the PyODBC dialect will use its -best guess as to whether or not the driver deals with unicode literals -well. When ``False``, unicode literals will be encoded first, and when -``True`` unicode literals will be passed straight through. This is an interim -flag that hopefully should not be needed when the unicode situation stabilizes -for unix + PyODBC. - -.. versionadded:: 0.7.7 - ``supports_unicode_binds`` parameter to ``create_engine()``\ . - -""" - -from .base import MSExecutionContext, MSDialect -from ...connectors.pyodbc import PyODBCConnector -from ... import types as sqltypes, util -import decimal - -class _ms_numeric_pyodbc(object): - - """Turns Decimals with adjusted() < 0 or > 7 into strings. - - The routines here are needed for older pyodbc versions - as well as current mxODBC versions. - - """ - - def bind_processor(self, dialect): - - super_process = super(_ms_numeric_pyodbc, self).\ - bind_processor(dialect) - - if not dialect._need_decimal_fix: - return super_process - - def process(value): - if self.asdecimal and \ - isinstance(value, decimal.Decimal): - - adjusted = value.adjusted() - if adjusted < 0: - return self._small_dec_to_string(value) - elif adjusted > 7: - return self._large_dec_to_string(value) - - if super_process: - return super_process(value) - else: - return value - return process - - # these routines needed for older versions of pyodbc. - # as of 2.1.8 this logic is integrated. - - def _small_dec_to_string(self, value): - return "%s0.%s%s" % ( - (value < 0 and '-' or ''), - '0' * (abs(value.adjusted()) - 1), - "".join([str(nint) for nint in value.as_tuple()[1]])) - - def _large_dec_to_string(self, value): - _int = value.as_tuple()[1] - if 'E' in str(value): - result = "%s%s%s" % ( - (value < 0 and '-' or ''), - "".join([str(s) for s in _int]), - "0" * (value.adjusted() - (len(_int) - 1))) - else: - if (len(_int) - 1) > value.adjusted(): - result = "%s%s.%s" % ( - (value < 0 and '-' or ''), - "".join( - [str(s) for s in _int][0:value.adjusted() + 1]), - "".join( - [str(s) for s in _int][value.adjusted() + 1:])) - else: - result = "%s%s" % ( - (value < 0 and '-' or ''), - "".join( - [str(s) for s in _int][0:value.adjusted() + 1])) - return result - -class _MSNumeric_pyodbc(_ms_numeric_pyodbc, sqltypes.Numeric): - pass - -class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float): - pass - -class MSExecutionContext_pyodbc(MSExecutionContext): - _embedded_scope_identity = False - - def pre_exec(self): - """where appropriate, issue "select scope_identity()" in the same - statement. - - Background on why "scope_identity()" is preferable to "@@identity": - http://msdn.microsoft.com/en-us/library/ms190315.aspx - - Background on why we attempt to embed "scope_identity()" into the same - statement as the INSERT: - http://code.google.com/p/pyodbc/wiki/FAQs#How_do_I_retrieve_autogenerated/identity_values? - - """ - - super(MSExecutionContext_pyodbc, self).pre_exec() - - # don't embed the scope_identity select into an - # "INSERT .. DEFAULT VALUES" - if self._select_lastrowid and \ - self.dialect.use_scope_identity and \ - len(self.parameters[0]): - self._embedded_scope_identity = True - - self.statement += "; select scope_identity()" - - def post_exec(self): - if self._embedded_scope_identity: - # Fetch the last inserted id from the manipulated statement - # We may have to skip over a number of result sets with - # no data (due to triggers, etc.) - while True: - try: - # fetchall() ensures the cursor is consumed - # without closing it (FreeTDS particularly) - row = self.cursor.fetchall()[0] - break - except self.dialect.dbapi.Error as e: - # no way around this - nextset() consumes the previous set - # so we need to just keep flipping - self.cursor.nextset() - - self._lastrowid = int(row[0]) - else: - super(MSExecutionContext_pyodbc, self).post_exec() - - -class MSDialect_pyodbc(PyODBCConnector, MSDialect): - - execution_ctx_cls = MSExecutionContext_pyodbc - - pyodbc_driver_name = 'SQL Server' - - colspecs = util.update_copy( - MSDialect.colspecs, - { - sqltypes.Numeric: _MSNumeric_pyodbc, - sqltypes.Float: _MSFloat_pyodbc - } - ) - - def __init__(self, description_encoding=None, **params): - super(MSDialect_pyodbc, self).__init__(**params) - self.description_encoding = description_encoding - self.use_scope_identity = self.use_scope_identity and \ - self.dbapi and \ - hasattr(self.dbapi.Cursor, 'nextset') - self._need_decimal_fix = self.dbapi and \ - self._dbapi_version() < (2, 1, 8) - -dialect = MSDialect_pyodbc diff --git a/libs/sqlalchemy/dialects/mssql/zxjdbc.py b/libs/sqlalchemy/dialects/mssql/zxjdbc.py deleted file mode 100644 index 706eef3a..00000000 --- a/libs/sqlalchemy/dialects/mssql/zxjdbc.py +++ /dev/null @@ -1,65 +0,0 @@ -# mssql/zxjdbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mssql+zxjdbc - :name: zxJDBC for Jython - :dbapi: zxjdbc - :connectstring: mssql+zxjdbc://user:pass@host:port/dbname[?key=value&key=value...] - :driverurl: http://jtds.sourceforge.net/ - - -""" -from ...connectors.zxJDBC import ZxJDBCConnector -from .base import MSDialect, MSExecutionContext -from ... import engine - - -class MSExecutionContext_zxjdbc(MSExecutionContext): - - _embedded_scope_identity = False - - def pre_exec(self): - super(MSExecutionContext_zxjdbc, self).pre_exec() - # scope_identity after the fact returns null in jTDS so we must - # embed it - if self._select_lastrowid and self.dialect.use_scope_identity: - self._embedded_scope_identity = True - self.statement += "; SELECT scope_identity()" - - def post_exec(self): - if self._embedded_scope_identity: - while True: - try: - row = self.cursor.fetchall()[0] - break - except self.dialect.dbapi.Error: - self.cursor.nextset() - self._lastrowid = int(row[0]) - - if (self.isinsert or self.isupdate or self.isdelete) and \ - self.compiled.returning: - self._result_proxy = engine.FullyBufferedResultProxy(self) - - if self._enable_identity_insert: - table = self.dialect.identifier_preparer.format_table( - self.compiled.statement.table) - self.cursor.execute("SET IDENTITY_INSERT %s OFF" % table) - - -class MSDialect_zxjdbc(ZxJDBCConnector, MSDialect): - jdbc_db_name = 'jtds:sqlserver' - jdbc_driver_name = 'net.sourceforge.jtds.jdbc.Driver' - - execution_ctx_cls = MSExecutionContext_zxjdbc - - def _get_server_version_info(self, connection): - return tuple( - int(x) - for x in connection.connection.dbversion.split('.') - ) - -dialect = MSDialect_zxjdbc diff --git a/libs/sqlalchemy/dialects/mysql/__init__.py b/libs/sqlalchemy/dialects/mysql/__init__.py deleted file mode 100644 index 4eb8cc6d..00000000 --- a/libs/sqlalchemy/dialects/mysql/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# mysql/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from . import base, mysqldb, oursql, \ - pyodbc, zxjdbc, mysqlconnector, pymysql,\ - gaerdbms, cymysql - -# default dialect -base.dialect = mysqldb.dialect - -from .base import \ - BIGINT, BINARY, BIT, BLOB, BOOLEAN, CHAR, DATE, DATETIME, \ - DECIMAL, DOUBLE, ENUM, DECIMAL,\ - FLOAT, INTEGER, INTEGER, LONGBLOB, LONGTEXT, MEDIUMBLOB, \ - MEDIUMINT, MEDIUMTEXT, NCHAR, \ - NVARCHAR, NUMERIC, SET, SMALLINT, REAL, TEXT, TIME, TIMESTAMP, \ - TINYBLOB, TINYINT, TINYTEXT,\ - VARBINARY, VARCHAR, YEAR, dialect - -__all__ = ( -'BIGINT', 'BINARY', 'BIT', 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'DOUBLE', -'ENUM', 'DECIMAL', 'FLOAT', 'INTEGER', 'INTEGER', 'LONGBLOB', 'LONGTEXT', 'MEDIUMBLOB', 'MEDIUMINT', -'MEDIUMTEXT', 'NCHAR', 'NVARCHAR', 'NUMERIC', 'SET', 'SMALLINT', 'REAL', 'TEXT', 'TIME', 'TIMESTAMP', -'TINYBLOB', 'TINYINT', 'TINYTEXT', 'VARBINARY', 'VARCHAR', 'YEAR', 'dialect' -) diff --git a/libs/sqlalchemy/dialects/mysql/base.py b/libs/sqlalchemy/dialects/mysql/base.py deleted file mode 100644 index a3942e89..00000000 --- a/libs/sqlalchemy/dialects/mysql/base.py +++ /dev/null @@ -1,2955 +0,0 @@ -# mysql/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql - :name: MySQL - -Supported Versions and Features -------------------------------- - -SQLAlchemy supports MySQL starting with version 4.1 through modern releases. -However, no heroic measures are taken to work around major missing -SQL features - if your server version does not support sub-selects, for -example, they won't work in SQLAlchemy either. - -See the official MySQL documentation for detailed information about features -supported in any given server release. - -.. _mysql_connection_timeouts: - -Connection Timeouts -------------------- - -MySQL features an automatic connection close behavior, for connections that have -been idle for eight hours or more. To circumvent having this issue, use the -``pool_recycle`` option which controls the maximum age of any connection:: - - engine = create_engine('mysql+mysqldb://...', pool_recycle=3600) - -.. _mysql_storage_engines: - -Storage Engines ---------------- - -Most MySQL server installations have a default table type of ``MyISAM``, a -non-transactional table type. During a transaction, non-transactional storage -engines do not participate and continue to store table changes in autocommit -mode. For fully atomic transactions as well as support for foreign key -constraints, all participating tables must use a -transactional engine such as ``InnoDB``, ``Falcon``, ``SolidDB``, `PBXT`, etc. - -Storage engines can be elected when creating tables in SQLAlchemy by supplying -a ``mysql_engine='whatever'`` to the ``Table`` constructor. Any MySQL table -creation option can be specified in this syntax:: - - Table('mytable', metadata, - Column('data', String(32)), - mysql_engine='InnoDB', - mysql_charset='utf8' - ) - -.. seealso:: - - `The InnoDB Storage Engine - `_ - - on the MySQL website. - -Case Sensitivity and Table Reflection -------------------------------------- - -MySQL has inconsistent support for case-sensitive identifier -names, basing support on specific details of the underlying -operating system. However, it has been observed that no matter -what case sensitivity behavior is present, the names of tables in -foreign key declarations are *always* received from the database -as all-lower case, making it impossible to accurately reflect a -schema where inter-related tables use mixed-case identifier names. - -Therefore it is strongly advised that table names be declared as -all lower case both within SQLAlchemy as well as on the MySQL -database itself, especially if database reflection features are -to be used. - -Transaction Isolation Level ---------------------------- - -:func:`.create_engine` accepts an ``isolation_level`` -parameter which results in the command ``SET SESSION -TRANSACTION ISOLATION LEVEL `` being invoked for -every new connection. Valid values for this parameter are -``READ COMMITTED``, ``READ UNCOMMITTED``, -``REPEATABLE READ``, and ``SERIALIZABLE``:: - - engine = create_engine( - "mysql://scott:tiger@localhost/test", - isolation_level="READ UNCOMMITTED" - ) - -.. versionadded:: 0.7.6 - -Keys ----- - -Not all MySQL storage engines support foreign keys. For ``MyISAM`` and -similar engines, the information loaded by table reflection will not include -foreign keys. For these tables, you may supply a -:class:`~sqlalchemy.ForeignKeyConstraint` at reflection time:: - - Table('mytable', metadata, - ForeignKeyConstraint(['other_id'], ['othertable.other_id']), - autoload=True - ) - -When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on -an integer primary key column:: - - >>> t = Table('mytable', metadata, - ... Column('mytable_id', Integer, primary_key=True) - ... ) - >>> t.create() - CREATE TABLE mytable ( - id INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (id) - ) - -You can disable this behavior by supplying ``autoincrement=False`` to the -:class:`~sqlalchemy.Column`. This flag can also be used to enable -auto-increment on a secondary column in a multi-column key for some storage -engines:: - - Table('mytable', metadata, - Column('gid', Integer, primary_key=True, autoincrement=False), - Column('id', Integer, primary_key=True) - ) - -Ansi Quoting Style ------------------- - -MySQL features two varieties of identifier "quoting style", one using -backticks and the other using quotes, e.g. ```some_identifier``` vs. -``"some_identifier"``. All MySQL dialects detect which version -is in use by checking the value of ``sql_mode`` when a connection is first -established with a particular :class:`.Engine`. This quoting style comes -into play when rendering table and column names as well as when reflecting -existing database structures. The detection is entirely automatic and -no special configuration is needed to use either quoting style. - -.. versionchanged:: 0.6 detection of ANSI quoting style is entirely automatic, - there's no longer any end-user ``create_engine()`` options in this regard. - -MySQL SQL Extensions --------------------- - -Many of the MySQL SQL extensions are handled through SQLAlchemy's generic -function and operator support:: - - table.select(table.c.password==func.md5('plaintext')) - table.select(table.c.username.op('regexp')('^[a-d]')) - -And of course any valid MySQL statement can be executed as a string as well. - -Some limited direct support for MySQL extensions to SQL is currently -available. - -* SELECT pragma:: - - select(..., prefixes=['HIGH_PRIORITY', 'SQL_SMALL_RESULT']) - -* UPDATE with LIMIT:: - - update(..., mysql_limit=10) - -rowcount Support ----------------- - -SQLAlchemy standardizes the DBAPI ``cursor.rowcount`` attribute to be the -usual definition of "number of rows matched by an UPDATE or DELETE" statement. -This is in contradiction to the default setting on most MySQL DBAPI drivers, -which is "number of rows actually modified/deleted". For this reason, the -SQLAlchemy MySQL dialects always set the ``constants.CLIENT.FOUND_ROWS`` flag, -or whatever is equivalent for the DBAPI in use, on connect, unless the flag value -is overridden using DBAPI-specific options -(such as ``client_flag`` for the MySQL-Python driver, ``found_rows`` for the -OurSQL driver). - -See also: - -:attr:`.ResultProxy.rowcount` - - -CAST Support ------------- - -MySQL documents the CAST operator as available in version 4.0.2. When using the -SQLAlchemy :func:`.cast` function, SQLAlchemy -will not render the CAST token on MySQL before this version, based on server version -detection, instead rendering the internal expression directly. - -CAST may still not be desirable on an early MySQL version post-4.0.2, as it didn't -add all datatype support until 4.1.1. If your application falls into this -narrow area, the behavior of CAST can be controlled using the -:ref:`sqlalchemy.ext.compiler_toplevel` system, as per the recipe below:: - - from sqlalchemy.sql.expression import Cast - from sqlalchemy.ext.compiler import compiles - - @compiles(Cast, 'mysql') - def _check_mysql_version(element, compiler, **kw): - if compiler.dialect.server_version_info < (4, 1, 0): - return compiler.process(element.clause, **kw) - else: - return compiler.visit_cast(element, **kw) - -The above function, which only needs to be declared once -within an application, overrides the compilation of the -:func:`.cast` construct to check for version 4.1.0 before -fully rendering CAST; else the internal element of the -construct is rendered directly. - - -.. _mysql_indexes: - -MySQL Specific Index Options ----------------------------- - -MySQL-specific extensions to the :class:`.Index` construct are available. - -Index Length -~~~~~~~~~~~~~ - -MySQL provides an option to create index entries with a certain length, where -"length" refers to the number of characters or bytes in each value which will -become part of the index. SQLAlchemy provides this feature via the -``mysql_length`` parameter:: - - Index('my_index', my_table.c.data, mysql_length=10) - - Index('a_b_idx', my_table.c.a, my_table.c.b, mysql_length={'a': 4, 'b': 9}) - -Prefix lengths are given in characters for nonbinary string types and in bytes -for binary string types. The value passed to the keyword argument *must* be -either an integer (and, thus, specify the same prefix length value for all -columns of the index) or a dict in which keys are column names and values are -prefix length values for corresponding columns. MySQL only allows a length for -a column of an index if it is for a CHAR, VARCHAR, TEXT, BINARY, VARBINARY and -BLOB. - -.. versionadded:: 0.8.2 ``mysql_length`` may now be specified as a dictionary - for use with composite indexes. - -Index Types -~~~~~~~~~~~~~ - -Some MySQL storage engines permit you to specify an index type when creating -an index or primary key constraint. SQLAlchemy provides this feature via the -``mysql_using`` parameter on :class:`.Index`:: - - Index('my_index', my_table.c.data, mysql_using='hash') - -As well as the ``mysql_using`` parameter on :class:`.PrimaryKeyConstraint`:: - - PrimaryKeyConstraint("data", mysql_using='hash') - -The value passed to the keyword argument will be simply passed through to the -underlying CREATE INDEX or PRIMARY KEY clause, so it *must* be a valid index -type for your MySQL storage engine. - -More information can be found at: - -http://dev.mysql.com/doc/refman/5.0/en/create-index.html - -http://dev.mysql.com/doc/refman/5.0/en/create-table.html - -.. _mysql_foreign_keys: - -MySQL Foreign Key Options -------------------------- - -MySQL does not support the foreign key arguments "DEFERRABLE", "INITIALLY", -or "MATCH". Using the ``deferrable`` or ``initially`` keyword argument with -:class:`.ForeignKeyConstraint` or :class:`.ForeignKey` will have the effect of these keywords being -rendered in a DDL expression, which will then raise an error on MySQL. -In order to use these keywords on a foreign key while having them ignored -on a MySQL backend, use a custom compile rule:: - - from sqlalchemy.ext.compiler import compiles - from sqlalchemy.schema import ForeignKeyConstraint - - @compiles(ForeignKeyConstraint, "mysql") - def process(element, compiler, **kw): - element.deferrable = element.initially = None - return compiler.visit_foreign_key_constraint(element, **kw) - -.. versionchanged:: 0.9.0 - the MySQL backend no longer silently ignores - the ``deferrable`` or ``initially`` keyword arguments of :class:`.ForeignKeyConstraint` - and :class:`.ForeignKey`. - -The "MATCH" keyword is in fact more insidious, and is explicitly disallowed -by SQLAlchemy in conjunction with the MySQL backend. This argument is silently -ignored by MySQL, but in addition has the effect of ON UPDATE and ON DELETE options -also being ignored by the backend. Therefore MATCH should never be used with the -MySQL backend; as is the case with DEFERRABLE and INITIALLY, custom compilation -rules can be used to correct a MySQL ForeignKeyConstraint at DDL definition time. - -.. versionadded:: 0.9.0 - the MySQL backend will raise a :class:`.CompileError` - when the ``match`` keyword is used with :class:`.ForeignKeyConstraint` - or :class:`.ForeignKey`. - -""" - -import datetime -import inspect -import re -import sys - -from ... import schema as sa_schema -from ... import exc, log, sql, util -from ...sql import compiler -from array import array as _array - -from ...engine import reflection -from ...engine import default -from ... import types as sqltypes -from ...util import topological -from ...types import DATE, DATETIME, BOOLEAN, TIME, \ - BLOB, BINARY, VARBINARY - -RESERVED_WORDS = set( - ['accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc', - 'asensitive', 'before', 'between', 'bigint', 'binary', 'blob', 'both', - 'by', 'call', 'cascade', 'case', 'change', 'char', 'character', 'check', - 'collate', 'column', 'condition', 'constraint', 'continue', 'convert', - 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', - 'current_user', 'cursor', 'database', 'databases', 'day_hour', - 'day_microsecond', 'day_minute', 'day_second', 'dec', 'decimal', - 'declare', 'default', 'delayed', 'delete', 'desc', 'describe', - 'deterministic', 'distinct', 'distinctrow', 'div', 'double', 'drop', - 'dual', 'each', 'else', 'elseif', 'enclosed', 'escaped', 'exists', - 'exit', 'explain', 'false', 'fetch', 'float', 'float4', 'float8', - 'for', 'force', 'foreign', 'from', 'fulltext', 'grant', 'group', 'having', - 'high_priority', 'hour_microsecond', 'hour_minute', 'hour_second', 'if', - 'ignore', 'in', 'index', 'infile', 'inner', 'inout', 'insensitive', - 'insert', 'int', 'int1', 'int2', 'int3', 'int4', 'int8', 'integer', - 'interval', 'into', 'is', 'iterate', 'join', 'key', 'keys', 'kill', - 'leading', 'leave', 'left', 'like', 'limit', 'linear', 'lines', 'load', - 'localtime', 'localtimestamp', 'lock', 'long', 'longblob', 'longtext', - 'loop', 'low_priority', 'master_ssl_verify_server_cert', 'match', - 'mediumblob', 'mediumint', 'mediumtext', 'middleint', - 'minute_microsecond', 'minute_second', 'mod', 'modifies', 'natural', - 'not', 'no_write_to_binlog', 'null', 'numeric', 'on', 'optimize', - 'option', 'optionally', 'or', 'order', 'out', 'outer', 'outfile', - 'precision', 'primary', 'procedure', 'purge', 'range', 'read', 'reads', - 'read_only', 'read_write', 'real', 'references', 'regexp', 'release', - 'rename', 'repeat', 'replace', 'require', 'restrict', 'return', - 'revoke', 'right', 'rlike', 'schema', 'schemas', 'second_microsecond', - 'select', 'sensitive', 'separator', 'set', 'show', 'smallint', 'spatial', - 'specific', 'sql', 'sqlexception', 'sqlstate', 'sqlwarning', - 'sql_big_result', 'sql_calc_found_rows', 'sql_small_result', 'ssl', - 'starting', 'straight_join', 'table', 'terminated', 'then', 'tinyblob', - 'tinyint', 'tinytext', 'to', 'trailing', 'trigger', 'true', 'undo', - 'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use', - 'using', 'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary', - 'varchar', 'varcharacter', 'varying', 'when', 'where', 'while', 'with', - - 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0 - - 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1 - - 'accessible', 'linear', 'master_ssl_verify_server_cert', 'range', - 'read_only', 'read_write', # 5.1 - - 'general', 'ignore_server_ids', 'master_heartbeat_period', 'maxvalue', - 'resignal', 'signal', 'slow', # 5.5 - - 'get', 'io_after_gtids', 'io_before_gtids', 'master_bind', 'one_shot', - 'partition', 'sql_after_gtids', 'sql_before_gtids', # 5.6 - - ]) - -AUTOCOMMIT_RE = re.compile( - r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|LOAD +DATA|REPLACE)', - re.I | re.UNICODE) -SET_RE = re.compile( - r'\s*SET\s+(?:(?:GLOBAL|SESSION)\s+)?\w', - re.I | re.UNICODE) - - -class _NumericType(object): - """Base for MySQL numeric types. - - This is the base both for NUMERIC as well as INTEGER, hence - it's a mixin. - - """ - - def __init__(self, unsigned=False, zerofill=False, **kw): - self.unsigned = unsigned - self.zerofill = zerofill - super(_NumericType, self).__init__(**kw) - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[_NumericType, sqltypes.Numeric]) - -class _FloatType(_NumericType, sqltypes.Float): - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - if isinstance(self, (REAL, DOUBLE)) and \ - ( - (precision is None and scale is not None) or - (precision is not None and scale is None) - ): - raise exc.ArgumentError( - "You must specify both precision and scale or omit " - "both altogether.") - super(_FloatType, self).__init__(precision=precision, asdecimal=asdecimal, **kw) - self.scale = scale - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[_FloatType, _NumericType, sqltypes.Float]) - -class _IntegerType(_NumericType, sqltypes.Integer): - def __init__(self, display_width=None, **kw): - self.display_width = display_width - super(_IntegerType, self).__init__(**kw) - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[_IntegerType, _NumericType, sqltypes.Integer]) - -class _StringType(sqltypes.String): - """Base for MySQL string types.""" - - def __init__(self, charset=None, collation=None, - ascii=False, binary=False, unicode=False, - national=False, **kw): - self.charset = charset - - # allow collate= or collation= - kw.setdefault('collation', kw.pop('collate', collation)) - - self.ascii = ascii - self.unicode = unicode - self.binary = binary - self.national = national - super(_StringType, self).__init__(**kw) - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[_StringType, sqltypes.String]) - -class NUMERIC(_NumericType, sqltypes.NUMERIC): - """MySQL NUMERIC type.""" - - __visit_name__ = 'NUMERIC' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a NUMERIC. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(NUMERIC, self).__init__(precision=precision, - scale=scale, asdecimal=asdecimal, **kw) - - -class DECIMAL(_NumericType, sqltypes.DECIMAL): - """MySQL DECIMAL type.""" - - __visit_name__ = 'DECIMAL' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a DECIMAL. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(DECIMAL, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class DOUBLE(_FloatType): - """MySQL DOUBLE type.""" - - __visit_name__ = 'DOUBLE' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a DOUBLE. - - .. note:: - - The :class:`.DOUBLE` type by default converts from float - to Decimal, using a truncation that defaults to 10 digits. Specify - either ``scale=n`` or ``decimal_return_scale=n`` in order to change - this scale, or ``asdecimal=False`` to return values directly as - Python floating points. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(DOUBLE, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class REAL(_FloatType, sqltypes.REAL): - """MySQL REAL type.""" - - __visit_name__ = 'REAL' - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a REAL. - - .. note:: - - The :class:`.REAL` type by default converts from float - to Decimal, using a truncation that defaults to 10 digits. Specify - either ``scale=n`` or ``decimal_return_scale=n`` in order to change - this scale, or ``asdecimal=False`` to return values directly as - Python floating points. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(REAL, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - -class FLOAT(_FloatType, sqltypes.FLOAT): - """MySQL FLOAT type.""" - - __visit_name__ = 'FLOAT' - - def __init__(self, precision=None, scale=None, asdecimal=False, **kw): - """Construct a FLOAT. - - :param precision: Total digits in this number. If scale and precision - are both None, values are stored to limits allowed by the server. - - :param scale: The number of digits after the decimal point. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(FLOAT, self).__init__(precision=precision, scale=scale, - asdecimal=asdecimal, **kw) - - def bind_processor(self, dialect): - return None - - -class INTEGER(_IntegerType, sqltypes.INTEGER): - """MySQL INTEGER type.""" - - __visit_name__ = 'INTEGER' - - def __init__(self, display_width=None, **kw): - """Construct an INTEGER. - - :param display_width: Optional, maximum display width for this number. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(INTEGER, self).__init__(display_width=display_width, **kw) - - -class BIGINT(_IntegerType, sqltypes.BIGINT): - """MySQL BIGINTEGER type.""" - - __visit_name__ = 'BIGINT' - - def __init__(self, display_width=None, **kw): - """Construct a BIGINTEGER. - - :param display_width: Optional, maximum display width for this number. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(BIGINT, self).__init__(display_width=display_width, **kw) - - -class MEDIUMINT(_IntegerType): - """MySQL MEDIUMINTEGER type.""" - - __visit_name__ = 'MEDIUMINT' - - def __init__(self, display_width=None, **kw): - """Construct a MEDIUMINTEGER - - :param display_width: Optional, maximum display width for this number. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(MEDIUMINT, self).__init__(display_width=display_width, **kw) - - -class TINYINT(_IntegerType): - """MySQL TINYINT type.""" - - __visit_name__ = 'TINYINT' - - def __init__(self, display_width=None, **kw): - """Construct a TINYINT. - - :param display_width: Optional, maximum display width for this number. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(TINYINT, self).__init__(display_width=display_width, **kw) - - -class SMALLINT(_IntegerType, sqltypes.SMALLINT): - """MySQL SMALLINTEGER type.""" - - __visit_name__ = 'SMALLINT' - - def __init__(self, display_width=None, **kw): - """Construct a SMALLINTEGER. - - :param display_width: Optional, maximum display width for this number. - - :param unsigned: a boolean, optional. - - :param zerofill: Optional. If true, values will be stored as strings - left-padded with zeros. Note that this does not effect the values - returned by the underlying database API, which continue to be - numeric. - - """ - super(SMALLINT, self).__init__(display_width=display_width, **kw) - - -class BIT(sqltypes.TypeEngine): - """MySQL BIT type. - - This type is for MySQL 5.0.3 or greater for MyISAM, and 5.0.5 or greater for - MyISAM, MEMORY, InnoDB and BDB. For older versions, use a MSTinyInteger() - type. - - """ - - __visit_name__ = 'BIT' - - def __init__(self, length=None): - """Construct a BIT. - - :param length: Optional, number of bits. - - """ - self.length = length - - def result_processor(self, dialect, coltype): - """Convert a MySQL's 64 bit, variable length binary string to a long. - - TODO: this is MySQL-db, pyodbc specific. OurSQL and mysqlconnector - already do this, so this logic should be moved to those dialects. - - """ - - def process(value): - if value is not None: - v = 0 - for i in map(ord, value): - v = v << 8 | i - return v - return value - return process - - -class TIME(sqltypes.TIME): - """MySQL TIME type. - - Recent versions of MySQL add support for - fractional seconds precision. While the - :class:`.mysql.TIME` type now supports this, - note that many DBAPI drivers may not yet - include support. - - """ - - __visit_name__ = 'TIME' - - def __init__(self, timezone=False, fsp=None): - """Construct a MySQL TIME type. - - :param timezone: not used by the MySQL dialect. - :param fsp: fractional seconds precision value. - MySQL 5.6 supports storage of fractional seconds; - this parameter will be used when emitting DDL - for the TIME type. Note that many DBAPI drivers - may not yet have support for fractional seconds, - however. - - .. versionadded:: 0.8 The MySQL-specific TIME - type as well as fractional seconds support. - - """ - super(TIME, self).__init__(timezone=timezone) - self.fsp = fsp - - def result_processor(self, dialect, coltype): - time = datetime.time - - def process(value): - # convert from a timedelta value - if value is not None: - microseconds = value.microseconds - seconds = value.seconds - minutes = seconds // 60 - return time(minutes // 60, - minutes % 60, - seconds - minutes * 60, - microsecond=microseconds) - else: - return None - return process - - -class TIMESTAMP(sqltypes.TIMESTAMP): - """MySQL TIMESTAMP type.""" - __visit_name__ = 'TIMESTAMP' - - -class YEAR(sqltypes.TypeEngine): - """MySQL YEAR type, for single byte storage of years 1901-2155.""" - - __visit_name__ = 'YEAR' - - def __init__(self, display_width=None): - self.display_width = display_width - - -class TEXT(_StringType, sqltypes.TEXT): - """MySQL TEXT type, for text up to 2^16 characters.""" - - __visit_name__ = 'TEXT' - - def __init__(self, length=None, **kw): - """Construct a TEXT. - - :param length: Optional, if provided the server may optimize storage - by substituting the smallest TEXT type sufficient to store - ``length`` characters. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param national: Optional. If true, use the server's configured - national character set. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - super(TEXT, self).__init__(length=length, **kw) - - -class TINYTEXT(_StringType): - """MySQL TINYTEXT type, for text up to 2^8 characters.""" - - __visit_name__ = 'TINYTEXT' - - def __init__(self, **kwargs): - """Construct a TINYTEXT. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param national: Optional. If true, use the server's configured - national character set. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - super(TINYTEXT, self).__init__(**kwargs) - - -class MEDIUMTEXT(_StringType): - """MySQL MEDIUMTEXT type, for text up to 2^24 characters.""" - - __visit_name__ = 'MEDIUMTEXT' - - def __init__(self, **kwargs): - """Construct a MEDIUMTEXT. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param national: Optional. If true, use the server's configured - national character set. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - super(MEDIUMTEXT, self).__init__(**kwargs) - - -class LONGTEXT(_StringType): - """MySQL LONGTEXT type, for text up to 2^32 characters.""" - - __visit_name__ = 'LONGTEXT' - - def __init__(self, **kwargs): - """Construct a LONGTEXT. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param national: Optional. If true, use the server's configured - national character set. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - super(LONGTEXT, self).__init__(**kwargs) - - -class VARCHAR(_StringType, sqltypes.VARCHAR): - """MySQL VARCHAR type, for variable-length character data.""" - - __visit_name__ = 'VARCHAR' - - def __init__(self, length=None, **kwargs): - """Construct a VARCHAR. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param national: Optional. If true, use the server's configured - national character set. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - """ - super(VARCHAR, self).__init__(length=length, **kwargs) - - -class CHAR(_StringType, sqltypes.CHAR): - """MySQL CHAR type, for fixed-length character data.""" - - __visit_name__ = 'CHAR' - - def __init__(self, length=None, **kwargs): - """Construct a CHAR. - - :param length: Maximum data length, in characters. - - :param binary: Optional, use the default binary collation for the - national character set. This does not affect the type of data - stored, use a BINARY type for binary data. - - :param collation: Optional, request a particular collation. Must be - compatible with the national character set. - - """ - super(CHAR, self).__init__(length=length, **kwargs) - - -class NVARCHAR(_StringType, sqltypes.NVARCHAR): - """MySQL NVARCHAR type. - - For variable-length character data in the server's configured national - character set. - """ - - __visit_name__ = 'NVARCHAR' - - def __init__(self, length=None, **kwargs): - """Construct an NVARCHAR. - - :param length: Maximum data length, in characters. - - :param binary: Optional, use the default binary collation for the - national character set. This does not affect the type of data - stored, use a BINARY type for binary data. - - :param collation: Optional, request a particular collation. Must be - compatible with the national character set. - - """ - kwargs['national'] = True - super(NVARCHAR, self).__init__(length=length, **kwargs) - - -class NCHAR(_StringType, sqltypes.NCHAR): - """MySQL NCHAR type. - - For fixed-length character data in the server's configured national - character set. - """ - - __visit_name__ = 'NCHAR' - - def __init__(self, length=None, **kwargs): - """Construct an NCHAR. - - :param length: Maximum data length, in characters. - - :param binary: Optional, use the default binary collation for the - national character set. This does not affect the type of data - stored, use a BINARY type for binary data. - - :param collation: Optional, request a particular collation. Must be - compatible with the national character set. - - """ - kwargs['national'] = True - super(NCHAR, self).__init__(length=length, **kwargs) - - -class TINYBLOB(sqltypes._Binary): - """MySQL TINYBLOB type, for binary data up to 2^8 bytes.""" - - __visit_name__ = 'TINYBLOB' - - -class MEDIUMBLOB(sqltypes._Binary): - """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes.""" - - __visit_name__ = 'MEDIUMBLOB' - - -class LONGBLOB(sqltypes._Binary): - """MySQL LONGBLOB type, for binary data up to 2^32 bytes.""" - - __visit_name__ = 'LONGBLOB' - -class _EnumeratedValues(_StringType): - def _init_values(self, values, kw): - self.quoting = kw.pop('quoting', 'auto') - - if self.quoting == 'auto' and len(values): - # What quoting character are we using? - q = None - for e in values: - if len(e) == 0: - self.quoting = 'unquoted' - break - elif q is None: - q = e[0] - - if len(e) == 1 or e[0] != q or e[-1] != q: - self.quoting = 'unquoted' - break - else: - self.quoting = 'quoted' - - if self.quoting == 'quoted': - util.warn_deprecated( - 'Manually quoting %s value literals is deprecated. Supply ' - 'unquoted values and use the quoting= option in cases of ' - 'ambiguity.' % self.__class__.__name__) - - values = self._strip_values(values) - - self._enumerated_values = values - length = max([len(v) for v in values] + [0]) - return values, length - - @classmethod - def _strip_values(cls, values): - strip_values = [] - for a in values: - if a[0:1] == '"' or a[0:1] == "'": - # strip enclosing quotes and unquote interior - a = a[1:-1].replace(a[0] * 2, a[0]) - strip_values.append(a) - return strip_values - -class ENUM(sqltypes.Enum, _EnumeratedValues): - """MySQL ENUM type.""" - - __visit_name__ = 'ENUM' - - def __init__(self, *enums, **kw): - """Construct an ENUM. - - E.g.:: - - Column('myenum', ENUM("foo", "bar", "baz")) - - :param enums: The range of valid values for this ENUM. Values will be - quoted when generating the schema according to the quoting flag (see - below). - - :param strict: Defaults to False: ensure that a given value is in this - ENUM's range of permissible values when inserting or updating rows. - Note that MySQL will not raise a fatal error if you attempt to store - an out of range value- an alternate value will be stored instead. - (See MySQL ENUM documentation.) - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - :param quoting: Defaults to 'auto': automatically determine enum value - quoting. If all enum values are surrounded by the same quoting - character, then use 'quoted' mode. Otherwise, use 'unquoted' mode. - - 'quoted': values in enums are already quoted, they will be used - directly when generating the schema - this usage is deprecated. - - 'unquoted': values in enums are not quoted, they will be escaped and - surrounded by single quotes when generating the schema. - - Previous versions of this type always required manually quoted - values to be supplied; future versions will always quote the string - literals for you. This is a transitional option. - - """ - values, length = self._init_values(enums, kw) - self.strict = kw.pop('strict', False) - kw.pop('metadata', None) - kw.pop('schema', None) - kw.pop('name', None) - kw.pop('quote', None) - kw.pop('native_enum', None) - kw.pop('inherit_schema', None) - _StringType.__init__(self, length=length, **kw) - sqltypes.Enum.__init__(self, *values) - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[ENUM, _StringType, sqltypes.Enum]) - - def bind_processor(self, dialect): - super_convert = super(ENUM, self).bind_processor(dialect) - - def process(value): - if self.strict and value is not None and value not in self.enums: - raise exc.InvalidRequestError('"%s" not a valid value for ' - 'this enum' % value) - if super_convert: - return super_convert(value) - else: - return value - return process - - def adapt(self, impltype, **kw): - kw['strict'] = self.strict - return sqltypes.Enum.adapt(self, impltype, **kw) - - -class SET(_EnumeratedValues): - """MySQL SET type.""" - - __visit_name__ = 'SET' - - def __init__(self, *values, **kw): - """Construct a SET. - - E.g.:: - - Column('myset', SET("foo", "bar", "baz")) - - :param values: The range of valid values for this SET. Values will be - quoted when generating the schema according to the quoting flag (see - below). - - .. versionchanged:: 0.9.0 quoting is applied automatically to - :class:`.mysql.SET` in the same way as for :class:`.mysql.ENUM`. - - :param charset: Optional, a column-level character set for this string - value. Takes precedence to 'ascii' or 'unicode' short-hand. - - :param collation: Optional, a column-level collation for this string - value. Takes precedence to 'binary' short-hand. - - :param ascii: Defaults to False: short-hand for the ``latin1`` - character set, generates ASCII in schema. - - :param unicode: Defaults to False: short-hand for the ``ucs2`` - character set, generates UNICODE in schema. - - :param binary: Defaults to False: short-hand, pick the binary - collation type that matches the column's character set. Generates - BINARY in schema. This does not affect the type of data stored, - only the collation of character data. - - :param quoting: Defaults to 'auto': automatically determine enum value - quoting. If all enum values are surrounded by the same quoting - character, then use 'quoted' mode. Otherwise, use 'unquoted' mode. - - 'quoted': values in enums are already quoted, they will be used - directly when generating the schema - this usage is deprecated. - - 'unquoted': values in enums are not quoted, they will be escaped and - surrounded by single quotes when generating the schema. - - Previous versions of this type always required manually quoted - values to be supplied; future versions will always quote the string - literals for you. This is a transitional option. - - .. versionadded:: 0.9.0 - - """ - values, length = self._init_values(values, kw) - self.values = tuple(values) - - kw.setdefault('length', length) - super(SET, self).__init__(**kw) - - def result_processor(self, dialect, coltype): - def process(value): - # The good news: - # No ',' quoting issues- commas aren't allowed in SET values - # The bad news: - # Plenty of driver inconsistencies here. - if isinstance(value, set): - # ..some versions convert '' to an empty set - if not value: - value.add('') - return value - # ...and some versions return strings - if value is not None: - return set(value.split(',')) - else: - return value - return process - - def bind_processor(self, dialect): - super_convert = super(SET, self).bind_processor(dialect) - - def process(value): - if value is None or isinstance(value, util.int_types + util.string_types): - pass - else: - if None in value: - value = set(value) - value.remove(None) - value.add('') - value = ','.join(value) - if super_convert: - return super_convert(value) - else: - return value - return process - -# old names -MSTime = TIME -MSSet = SET -MSEnum = ENUM -MSLongBlob = LONGBLOB -MSMediumBlob = MEDIUMBLOB -MSTinyBlob = TINYBLOB -MSBlob = BLOB -MSBinary = BINARY -MSVarBinary = VARBINARY -MSNChar = NCHAR -MSNVarChar = NVARCHAR -MSChar = CHAR -MSString = VARCHAR -MSLongText = LONGTEXT -MSMediumText = MEDIUMTEXT -MSTinyText = TINYTEXT -MSText = TEXT -MSYear = YEAR -MSTimeStamp = TIMESTAMP -MSBit = BIT -MSSmallInteger = SMALLINT -MSTinyInteger = TINYINT -MSMediumInteger = MEDIUMINT -MSBigInteger = BIGINT -MSNumeric = NUMERIC -MSDecimal = DECIMAL -MSDouble = DOUBLE -MSReal = REAL -MSFloat = FLOAT -MSInteger = INTEGER - -colspecs = { - _IntegerType: _IntegerType, - _NumericType: _NumericType, - _FloatType: _FloatType, - sqltypes.Numeric: NUMERIC, - sqltypes.Float: FLOAT, - sqltypes.Time: TIME, - sqltypes.Enum: ENUM, -} - -# Everything 3.23 through 5.1 excepting OpenGIS types. -ischema_names = { - 'bigint': BIGINT, - 'binary': BINARY, - 'bit': BIT, - 'blob': BLOB, - 'boolean': BOOLEAN, - 'char': CHAR, - 'date': DATE, - 'datetime': DATETIME, - 'decimal': DECIMAL, - 'double': DOUBLE, - 'enum': ENUM, - 'fixed': DECIMAL, - 'float': FLOAT, - 'int': INTEGER, - 'integer': INTEGER, - 'longblob': LONGBLOB, - 'longtext': LONGTEXT, - 'mediumblob': MEDIUMBLOB, - 'mediumint': MEDIUMINT, - 'mediumtext': MEDIUMTEXT, - 'nchar': NCHAR, - 'nvarchar': NVARCHAR, - 'numeric': NUMERIC, - 'set': SET, - 'smallint': SMALLINT, - 'text': TEXT, - 'time': TIME, - 'timestamp': TIMESTAMP, - 'tinyblob': TINYBLOB, - 'tinyint': TINYINT, - 'tinytext': TINYTEXT, - 'varbinary': VARBINARY, - 'varchar': VARCHAR, - 'year': YEAR, -} - - -class MySQLExecutionContext(default.DefaultExecutionContext): - - def should_autocommit_text(self, statement): - return AUTOCOMMIT_RE.match(statement) - - -class MySQLCompiler(compiler.SQLCompiler): - - render_table_with_column_in_update_from = True - """Overridden from base SQLCompiler value""" - - extract_map = compiler.SQLCompiler.extract_map.copy() - extract_map.update({'milliseconds': 'millisecond'}) - - def visit_random_func(self, fn, **kw): - return "rand%s" % self.function_argspec(fn) - - def visit_utc_timestamp_func(self, fn, **kw): - return "UTC_TIMESTAMP" - - def visit_sysdate_func(self, fn, **kw): - return "SYSDATE()" - - def visit_concat_op_binary(self, binary, operator, **kw): - return "concat(%s, %s)" % (self.process(binary.left), - self.process(binary.right)) - - def visit_match_op_binary(self, binary, operator, **kw): - return "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % \ - (self.process(binary.left), self.process(binary.right)) - - def get_from_hint_text(self, table, text): - return text - - def visit_typeclause(self, typeclause): - type_ = typeclause.type.dialect_impl(self.dialect) - if isinstance(type_, sqltypes.Integer): - if getattr(type_, 'unsigned', False): - return 'UNSIGNED INTEGER' - else: - return 'SIGNED INTEGER' - elif isinstance(type_, sqltypes.TIMESTAMP): - return 'DATETIME' - elif isinstance(type_, (sqltypes.DECIMAL, sqltypes.DateTime, - sqltypes.Date, sqltypes.Time)): - return self.dialect.type_compiler.process(type_) - elif isinstance(type_, sqltypes.Text): - return 'CHAR' - elif (isinstance(type_, sqltypes.String) and not - isinstance(type_, (ENUM, SET))): - if getattr(type_, 'length'): - return 'CHAR(%s)' % type_.length - else: - return 'CHAR' - elif isinstance(type_, sqltypes._Binary): - return 'BINARY' - elif isinstance(type_, sqltypes.NUMERIC): - return self.dialect.type_compiler.process( - type_).replace('NUMERIC', 'DECIMAL') - else: - return None - - def visit_cast(self, cast, **kwargs): - # No cast until 4, no decimals until 5. - if not self.dialect._supports_cast: - return self.process(cast.clause.self_group()) - - type_ = self.process(cast.typeclause) - if type_ is None: - return self.process(cast.clause.self_group()) - - return 'CAST(%s AS %s)' % (self.process(cast.clause), type_) - - def render_literal_value(self, value, type_): - value = super(MySQLCompiler, self).render_literal_value(value, type_) - if self.dialect._backslash_escapes: - value = value.replace('\\', '\\\\') - return value - - def get_select_precolumns(self, select): - """Add special MySQL keywords in place of DISTINCT. - - .. note:: - - this usage is deprecated. :meth:`.Select.prefix_with` - should be used for special keywords at the start - of a SELECT. - - """ - if isinstance(select._distinct, util.string_types): - return select._distinct.upper() + " " - elif select._distinct: - return "DISTINCT " - else: - return "" - - def visit_join(self, join, asfrom=False, **kwargs): - return ''.join( - (self.process(join.left, asfrom=True, **kwargs), - (join.isouter and " LEFT OUTER JOIN " or " INNER JOIN "), - self.process(join.right, asfrom=True, **kwargs), - " ON ", - self.process(join.onclause, **kwargs))) - - def for_update_clause(self, select): - if select._for_update_arg.read: - return " LOCK IN SHARE MODE" - else: - return " FOR UPDATE" - - def limit_clause(self, select): - # MySQL supports: - # LIMIT - # LIMIT , - # and in server versions > 3.3: - # LIMIT OFFSET - # The latter is more readable for offsets but we're stuck with the - # former until we can refine dialects by server revision. - - limit, offset = select._limit, select._offset - - if (limit, offset) == (None, None): - return '' - elif offset is not None: - # As suggested by the MySQL docs, need to apply an - # artificial limit if one wasn't provided - # http://dev.mysql.com/doc/refman/5.0/en/select.html - if limit is None: - # hardwire the upper limit. Currently - # needed by OurSQL with Python 3 - # (https://bugs.launchpad.net/oursql/+bug/686232), - # but also is consistent with the usage of the upper - # bound as part of MySQL's "syntax" for OFFSET with - # no LIMIT - return ' \n LIMIT %s, %s' % ( - self.process(sql.literal(offset)), - "18446744073709551615") - else: - return ' \n LIMIT %s, %s' % ( - self.process(sql.literal(offset)), - self.process(sql.literal(limit))) - else: - # No offset provided, so just use the limit - return ' \n LIMIT %s' % (self.process(sql.literal(limit)),) - - def update_limit_clause(self, update_stmt): - limit = update_stmt.kwargs.get('%s_limit' % self.dialect.name, None) - if limit: - return "LIMIT %s" % limit - else: - return None - - def update_tables_clause(self, update_stmt, from_table, extra_froms, **kw): - return ', '.join(t._compiler_dispatch(self, asfrom=True, **kw) - for t in [from_table] + list(extra_froms)) - - def update_from_clause(self, update_stmt, from_table, - extra_froms, from_hints, **kw): - return None - - -# ug. "InnoDB needs indexes on foreign keys and referenced keys [...]. -# Starting with MySQL 4.1.2, these indexes are created automatically. -# In older versions, the indexes must be created explicitly or the -# creation of foreign key constraints fails." - -class MySQLDDLCompiler(compiler.DDLCompiler): - def create_table_constraints(self, table): - """Get table constraints.""" - constraint_string = super( - MySQLDDLCompiler, self).create_table_constraints(table) - - engine_key = '%s_engine' % self.dialect.name - is_innodb = engine_key in table.kwargs and \ - table.kwargs[engine_key].lower() == 'innodb' - - auto_inc_column = table._autoincrement_column - - if is_innodb and \ - auto_inc_column is not None and \ - auto_inc_column is not list(table.primary_key)[0]: - if constraint_string: - constraint_string += ", \n\t" - constraint_string += "KEY %s (%s)" % ( - self.preparer.quote( - "idx_autoinc_%s" % auto_inc_column.name - ), - self.preparer.format_column(auto_inc_column) - ) - - return constraint_string - - def get_column_specification(self, column, **kw): - """Builds column DDL.""" - - colspec = [self.preparer.format_column(column), - self.dialect.type_compiler.process(column.type) - ] - - default = self.get_column_default_string(column) - if default is not None: - colspec.append('DEFAULT ' + default) - - is_timestamp = isinstance(column.type, sqltypes.TIMESTAMP) - if not column.nullable and not is_timestamp: - colspec.append('NOT NULL') - - elif column.nullable and is_timestamp and default is None: - colspec.append('NULL') - - if column is column.table._autoincrement_column and \ - column.server_default is None: - colspec.append('AUTO_INCREMENT') - - return ' '.join(colspec) - - def post_create_table(self, table): - """Build table-level CREATE options like ENGINE and COLLATE.""" - - table_opts = [] - - opts = dict( - ( - k[len(self.dialect.name) + 1:].upper(), - v - ) - for k, v in table.kwargs.items() - if k.startswith('%s_' % self.dialect.name) - ) - - for opt in topological.sort([ - ('DEFAULT_CHARSET', 'COLLATE'), - ('DEFAULT_CHARACTER_SET', 'COLLATE') - ], opts): - arg = opts[opt] - if opt in _options_of_type_string: - arg = "'%s'" % arg.replace("\\", "\\\\").replace("'", "''") - - if opt in ('DATA_DIRECTORY', 'INDEX_DIRECTORY', - 'DEFAULT_CHARACTER_SET', 'CHARACTER_SET', - 'DEFAULT_CHARSET', - 'DEFAULT_COLLATE'): - opt = opt.replace('_', ' ') - - joiner = '=' - if opt in ('TABLESPACE', 'DEFAULT CHARACTER SET', - 'CHARACTER SET', 'COLLATE'): - joiner = ' ' - - table_opts.append(joiner.join((opt, arg))) - return ' '.join(table_opts) - - def visit_create_index(self, create): - index = create.element - self._verify_index_table(index) - preparer = self.preparer - table = preparer.format_table(index.table) - columns = [self.sql_compiler.process(expr, include_table=False, - literal_binds=True) - for expr in index.expressions] - - name = self._prepared_index_name(index) - - text = "CREATE " - if index.unique: - text += "UNIQUE " - text += "INDEX %s ON %s " % (name, table) - - if 'mysql_length' in index.kwargs: - length = index.kwargs['mysql_length'] - - if isinstance(length, dict): - # length value can be a (column_name --> integer value) mapping - # specifying the prefix length for each column of the index - columns = ', '.join( - ('%s(%d)' % (col, length[col]) - if col in length else '%s' % col) - for col in columns - ) - else: - # or can be an integer value specifying the same - # prefix length for all columns of the index - columns = ', '.join( - '%s(%d)' % (col, length) - for col in columns - ) - else: - columns = ', '.join(columns) - text += '(%s)' % columns - - if 'mysql_using' in index.kwargs: - using = index.kwargs['mysql_using'] - text += " USING %s" % (preparer.quote(using)) - - return text - - def visit_primary_key_constraint(self, constraint): - text = super(MySQLDDLCompiler, self).\ - visit_primary_key_constraint(constraint) - if "mysql_using" in constraint.kwargs: - using = constraint.kwargs['mysql_using'] - text += " USING %s" % (self.preparer.quote(using)) - return text - - def visit_drop_index(self, drop): - index = drop.element - - return "\nDROP INDEX %s ON %s" % ( - self._prepared_index_name(index, - include_schema=False), - self.preparer.format_table(index.table)) - - def visit_drop_constraint(self, drop): - constraint = drop.element - if isinstance(constraint, sa_schema.ForeignKeyConstraint): - qual = "FOREIGN KEY " - const = self.preparer.format_constraint(constraint) - elif isinstance(constraint, sa_schema.PrimaryKeyConstraint): - qual = "PRIMARY KEY " - const = "" - elif isinstance(constraint, sa_schema.UniqueConstraint): - qual = "INDEX " - const = self.preparer.format_constraint(constraint) - else: - qual = "" - const = self.preparer.format_constraint(constraint) - return "ALTER TABLE %s DROP %s%s" % \ - (self.preparer.format_table(constraint.table), - qual, const) - - def define_constraint_match(self, constraint): - if constraint.match is not None: - raise exc.CompileError( - "MySQL ignores the 'MATCH' keyword while at the same time " - "causes ON UPDATE/ON DELETE clauses to be ignored.") - return "" - -class MySQLTypeCompiler(compiler.GenericTypeCompiler): - def _extend_numeric(self, type_, spec): - "Extend a numeric-type declaration with MySQL specific extensions." - - if not self._mysql_type(type_): - return spec - - if type_.unsigned: - spec += ' UNSIGNED' - if type_.zerofill: - spec += ' ZEROFILL' - return spec - - def _extend_string(self, type_, defaults, spec): - """Extend a string-type declaration with standard SQL CHARACTER SET / - COLLATE annotations and MySQL specific extensions. - - """ - - def attr(name): - return getattr(type_, name, defaults.get(name)) - - if attr('charset'): - charset = 'CHARACTER SET %s' % attr('charset') - elif attr('ascii'): - charset = 'ASCII' - elif attr('unicode'): - charset = 'UNICODE' - else: - charset = None - - if attr('collation'): - collation = 'COLLATE %s' % type_.collation - elif attr('binary'): - collation = 'BINARY' - else: - collation = None - - if attr('national'): - # NATIONAL (aka NCHAR/NVARCHAR) trumps charsets. - return ' '.join([c for c in ('NATIONAL', spec, collation) - if c is not None]) - return ' '.join([c for c in (spec, charset, collation) - if c is not None]) - - def _mysql_type(self, type_): - return isinstance(type_, (_StringType, _NumericType)) - - def visit_NUMERIC(self, type_): - if type_.precision is None: - return self._extend_numeric(type_, "NUMERIC") - elif type_.scale is None: - return self._extend_numeric(type_, - "NUMERIC(%(precision)s)" % - {'precision': type_.precision}) - else: - return self._extend_numeric(type_, - "NUMERIC(%(precision)s, %(scale)s)" % - {'precision': type_.precision, - 'scale': type_.scale}) - - def visit_DECIMAL(self, type_): - if type_.precision is None: - return self._extend_numeric(type_, "DECIMAL") - elif type_.scale is None: - return self._extend_numeric(type_, - "DECIMAL(%(precision)s)" % - {'precision': type_.precision}) - else: - return self._extend_numeric(type_, - "DECIMAL(%(precision)s, %(scale)s)" % - {'precision': type_.precision, - 'scale': type_.scale}) - - def visit_DOUBLE(self, type_): - if type_.precision is not None and type_.scale is not None: - return self._extend_numeric(type_, - "DOUBLE(%(precision)s, %(scale)s)" % - {'precision': type_.precision, - 'scale': type_.scale}) - else: - return self._extend_numeric(type_, 'DOUBLE') - - def visit_REAL(self, type_): - if type_.precision is not None and type_.scale is not None: - return self._extend_numeric(type_, - "REAL(%(precision)s, %(scale)s)" % - {'precision': type_.precision, - 'scale': type_.scale}) - else: - return self._extend_numeric(type_, 'REAL') - - def visit_FLOAT(self, type_): - if self._mysql_type(type_) and \ - type_.scale is not None and \ - type_.precision is not None: - return self._extend_numeric(type_, - "FLOAT(%s, %s)" % (type_.precision, type_.scale)) - elif type_.precision is not None: - return self._extend_numeric(type_, - "FLOAT(%s)" % (type_.precision,)) - else: - return self._extend_numeric(type_, "FLOAT") - - def visit_INTEGER(self, type_): - if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, - "INTEGER(%(display_width)s)" % - {'display_width': type_.display_width}) - else: - return self._extend_numeric(type_, "INTEGER") - - def visit_BIGINT(self, type_): - if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, - "BIGINT(%(display_width)s)" % - {'display_width': type_.display_width}) - else: - return self._extend_numeric(type_, "BIGINT") - - def visit_MEDIUMINT(self, type_): - if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, - "MEDIUMINT(%(display_width)s)" % - {'display_width': type_.display_width}) - else: - return self._extend_numeric(type_, "MEDIUMINT") - - def visit_TINYINT(self, type_): - if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, - "TINYINT(%s)" % type_.display_width) - else: - return self._extend_numeric(type_, "TINYINT") - - def visit_SMALLINT(self, type_): - if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, - "SMALLINT(%(display_width)s)" % - {'display_width': type_.display_width} - ) - else: - return self._extend_numeric(type_, "SMALLINT") - - def visit_BIT(self, type_): - if type_.length is not None: - return "BIT(%s)" % type_.length - else: - return "BIT" - - def visit_DATETIME(self, type_): - return "DATETIME" - - def visit_DATE(self, type_): - return "DATE" - - def visit_TIME(self, type_): - if getattr(type_, 'fsp', None): - return "TIME(%d)" % type_.fsp - else: - return "TIME" - - def visit_TIMESTAMP(self, type_): - return 'TIMESTAMP' - - def visit_YEAR(self, type_): - if type_.display_width is None: - return "YEAR" - else: - return "YEAR(%s)" % type_.display_width - - def visit_TEXT(self, type_): - if type_.length: - return self._extend_string(type_, {}, "TEXT(%d)" % type_.length) - else: - return self._extend_string(type_, {}, "TEXT") - - def visit_TINYTEXT(self, type_): - return self._extend_string(type_, {}, "TINYTEXT") - - def visit_MEDIUMTEXT(self, type_): - return self._extend_string(type_, {}, "MEDIUMTEXT") - - def visit_LONGTEXT(self, type_): - return self._extend_string(type_, {}, "LONGTEXT") - - def visit_VARCHAR(self, type_): - if type_.length: - return self._extend_string(type_, {}, "VARCHAR(%d)" % type_.length) - else: - raise exc.CompileError( - "VARCHAR requires a length on dialect %s" % - self.dialect.name) - - def visit_CHAR(self, type_): - if type_.length: - return self._extend_string(type_, {}, "CHAR(%(length)s)" % - {'length': type_.length}) - else: - return self._extend_string(type_, {}, "CHAR") - - def visit_NVARCHAR(self, type_): - # We'll actually generate the equiv. "NATIONAL VARCHAR" instead - # of "NVARCHAR". - if type_.length: - return self._extend_string(type_, {'national': True}, - "VARCHAR(%(length)s)" % {'length': type_.length}) - else: - raise exc.CompileError( - "NVARCHAR requires a length on dialect %s" % - self.dialect.name) - - def visit_NCHAR(self, type_): - # We'll actually generate the equiv. - # "NATIONAL CHAR" instead of "NCHAR". - if type_.length: - return self._extend_string(type_, {'national': True}, - "CHAR(%(length)s)" % {'length': type_.length}) - else: - return self._extend_string(type_, {'national': True}, "CHAR") - - def visit_VARBINARY(self, type_): - return "VARBINARY(%d)" % type_.length - - def visit_large_binary(self, type_): - return self.visit_BLOB(type_) - - def visit_enum(self, type_): - if not type_.native_enum: - return super(MySQLTypeCompiler, self).visit_enum(type_) - else: - return self._visit_enumerated_values("ENUM", type_, type_.enums) - - def visit_BLOB(self, type_): - if type_.length: - return "BLOB(%d)" % type_.length - else: - return "BLOB" - - def visit_TINYBLOB(self, type_): - return "TINYBLOB" - - def visit_MEDIUMBLOB(self, type_): - return "MEDIUMBLOB" - - def visit_LONGBLOB(self, type_): - return "LONGBLOB" - - def _visit_enumerated_values(self, name, type_, enumerated_values): - quoted_enums = [] - for e in enumerated_values: - quoted_enums.append("'%s'" % e.replace("'", "''")) - return self._extend_string(type_, {}, "%s(%s)" % ( - name, ",".join(quoted_enums)) - ) - - def visit_ENUM(self, type_): - return self._visit_enumerated_values("ENUM", type_, - type_._enumerated_values) - - def visit_SET(self, type_): - return self._visit_enumerated_values("SET", type_, - type_._enumerated_values) - - def visit_BOOLEAN(self, type): - return "BOOL" - - -class MySQLIdentifierPreparer(compiler.IdentifierPreparer): - - reserved_words = RESERVED_WORDS - - def __init__(self, dialect, server_ansiquotes=False, **kw): - if not server_ansiquotes: - quote = "`" - else: - quote = '"' - - super(MySQLIdentifierPreparer, self).__init__( - dialect, - initial_quote=quote, - escape_quote=quote) - - def _quote_free_identifiers(self, *ids): - """Unilaterally identifier-quote any number of strings.""" - - return tuple([self.quote_identifier(i) for i in ids if i is not None]) - - -@log.class_logger -class MySQLDialect(default.DefaultDialect): - """Details of the MySQL dialect. Not used directly in application code.""" - - name = 'mysql' - supports_alter = True - - # identifiers are 64, however aliases can be 255... - max_identifier_length = 255 - max_index_name_length = 64 - - supports_native_enum = True - - supports_sane_rowcount = True - supports_sane_multi_rowcount = False - supports_multivalues_insert = True - - default_paramstyle = 'format' - colspecs = colspecs - - statement_compiler = MySQLCompiler - ddl_compiler = MySQLDDLCompiler - type_compiler = MySQLTypeCompiler - ischema_names = ischema_names - preparer = MySQLIdentifierPreparer - - # default SQL compilation settings - - # these are modified upon initialize(), - # i.e. first connect - _backslash_escapes = True - _server_ansiquotes = False - - def __init__(self, isolation_level=None, **kwargs): - kwargs.pop('use_ansiquotes', None) # legacy - default.DefaultDialect.__init__(self, **kwargs) - self.isolation_level = isolation_level - - def on_connect(self): - if self.isolation_level is not None: - def connect(conn): - self.set_isolation_level(conn, self.isolation_level) - return connect - else: - return None - - _isolation_lookup = set(['SERIALIZABLE', - 'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ']) - - def set_isolation_level(self, connection, level): - level = level.replace('_', ' ') - if level not in self._isolation_lookup: - raise exc.ArgumentError( - "Invalid value '%s' for isolation_level. " - "Valid isolation levels for %s are %s" % - (level, self.name, ", ".join(self._isolation_lookup)) - ) - cursor = connection.cursor() - cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL %s" % level) - cursor.execute("COMMIT") - cursor.close() - - def get_isolation_level(self, connection): - cursor = connection.cursor() - cursor.execute('SELECT @@tx_isolation') - val = cursor.fetchone()[0] - cursor.close() - if util.py3k and isinstance(val, bytes): - val = val.decode() - return val.upper().replace("-", " ") - - def do_commit(self, dbapi_connection): - """Execute a COMMIT.""" - - # COMMIT/ROLLBACK were introduced in 3.23.15. - # Yes, we have at least one user who has to talk to these old versions! - # - # Ignore commit/rollback if support isn't present, otherwise even basic - # operations via autocommit fail. - try: - dbapi_connection.commit() - except: - if self.server_version_info < (3, 23, 15): - args = sys.exc_info()[1].args - if args and args[0] == 1064: - return - raise - - def do_rollback(self, dbapi_connection): - """Execute a ROLLBACK.""" - - try: - dbapi_connection.rollback() - except: - if self.server_version_info < (3, 23, 15): - args = sys.exc_info()[1].args - if args and args[0] == 1064: - return - raise - - def do_begin_twophase(self, connection, xid): - connection.execute(sql.text("XA BEGIN :xid"), xid=xid) - - def do_prepare_twophase(self, connection, xid): - connection.execute(sql.text("XA END :xid"), xid=xid) - connection.execute(sql.text("XA PREPARE :xid"), xid=xid) - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - connection.execute(sql.text("XA END :xid"), xid=xid) - connection.execute(sql.text("XA ROLLBACK :xid"), xid=xid) - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - self.do_prepare_twophase(connection, xid) - connection.execute(sql.text("XA COMMIT :xid"), xid=xid) - - def do_recover_twophase(self, connection): - resultset = connection.execute("XA RECOVER") - return [row['data'][0:row['gtrid_length']] for row in resultset] - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.OperationalError): - return self._extract_error_code(e) in \ - (2006, 2013, 2014, 2045, 2055) - elif isinstance(e, self.dbapi.InterfaceError): - # if underlying connection is closed, - # this is the error you get - return "(0, '')" in str(e) - else: - return False - - def _compat_fetchall(self, rp, charset=None): - """Proxy result rows to smooth over MySQL-Python driver - inconsistencies.""" - - return [_DecodingRowProxy(row, charset) for row in rp.fetchall()] - - def _compat_fetchone(self, rp, charset=None): - """Proxy a result row to smooth over MySQL-Python driver - inconsistencies.""" - - return _DecodingRowProxy(rp.fetchone(), charset) - - def _compat_first(self, rp, charset=None): - """Proxy a result row to smooth over MySQL-Python driver - inconsistencies.""" - - return _DecodingRowProxy(rp.first(), charset) - - def _extract_error_code(self, exception): - raise NotImplementedError() - - def _get_default_schema_name(self, connection): - return connection.execute('SELECT DATABASE()').scalar() - - def has_table(self, connection, table_name, schema=None): - # SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly - # on macosx (and maybe win?) with multibyte table names. - # - # TODO: if this is not a problem on win, make the strategy swappable - # based on platform. DESCRIBE is slower. - - # [ticket:726] - # full_name = self.identifier_preparer.format_table(table, - # use_schema=True) - - full_name = '.'.join(self.identifier_preparer._quote_free_identifiers( - schema, table_name)) - - st = "DESCRIBE %s" % full_name - rs = None - try: - try: - rs = connection.execute(st) - have = rs.fetchone() is not None - rs.close() - return have - except exc.DBAPIError as e: - if self._extract_error_code(e.orig) == 1146: - return False - raise - finally: - if rs: - rs.close() - - def initialize(self, connection): - default.DefaultDialect.initialize(self, connection) - self._connection_charset = self._detect_charset(connection) - self._detect_ansiquotes(connection) - if self._server_ansiquotes: - # if ansiquotes == True, build a new IdentifierPreparer - # with the new setting - self.identifier_preparer = self.preparer(self, - server_ansiquotes=self._server_ansiquotes) - - @property - def _supports_cast(self): - return self.server_version_info is None or \ - self.server_version_info >= (4, 0, 2) - - @reflection.cache - def get_schema_names(self, connection, **kw): - rp = connection.execute("SHOW schemas") - return [r[0] for r in rp] - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - """Return a Unicode SHOW TABLES from a given schema.""" - if schema is not None: - current_schema = schema - else: - current_schema = self.default_schema_name - - charset = self._connection_charset - if self.server_version_info < (5, 0, 2): - rp = connection.execute("SHOW TABLES FROM %s" % - self.identifier_preparer.quote_identifier(current_schema)) - return [row[0] for - row in self._compat_fetchall(rp, charset=charset)] - else: - rp = connection.execute("SHOW FULL TABLES FROM %s" % - self.identifier_preparer.quote_identifier(current_schema)) - - return [row[0] - for row in self._compat_fetchall(rp, charset=charset) - if row[1] == 'BASE TABLE'] - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - if self.server_version_info < (5, 0, 2): - raise NotImplementedError - if schema is None: - schema = self.default_schema_name - if self.server_version_info < (5, 0, 2): - return self.get_table_names(connection, schema) - charset = self._connection_charset - rp = connection.execute("SHOW FULL TABLES FROM %s" % - self.identifier_preparer.quote_identifier(schema)) - return [row[0] - for row in self._compat_fetchall(rp, charset=charset) - if row[1] in ('VIEW', 'SYSTEM VIEW')] - - @reflection.cache - def get_table_options(self, connection, table_name, schema=None, **kw): - - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - return parsed_state.table_options - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - return parsed_state.columns - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - for key in parsed_state.keys: - if key['type'] == 'PRIMARY': - # There can be only one. - cols = [s[0] for s in key['columns']] - return {'constrained_columns': cols, 'name': None} - return {'constrained_columns': [], 'name': None} - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - default_schema = None - - fkeys = [] - - for spec in parsed_state.constraints: - # only FOREIGN KEYs - ref_name = spec['table'][-1] - ref_schema = len(spec['table']) > 1 and spec['table'][-2] or schema - - if not ref_schema: - if default_schema is None: - default_schema = \ - connection.dialect.default_schema_name - if schema == default_schema: - ref_schema = schema - - loc_names = spec['local'] - ref_names = spec['foreign'] - - con_kw = {} - for opt in ('onupdate', 'ondelete'): - if spec.get(opt, False): - con_kw[opt] = spec[opt] - - fkey_d = { - 'name': spec['name'], - 'constrained_columns': loc_names, - 'referred_schema': ref_schema, - 'referred_table': ref_name, - 'referred_columns': ref_names, - 'options': con_kw - } - fkeys.append(fkey_d) - return fkeys - - @reflection.cache - def get_indexes(self, connection, table_name, schema=None, **kw): - - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - - indexes = [] - for spec in parsed_state.keys: - unique = False - flavor = spec['type'] - if flavor == 'PRIMARY': - continue - if flavor == 'UNIQUE': - unique = True - elif flavor in (None, 'FULLTEXT', 'SPATIAL'): - pass - else: - self.logger.info( - "Converting unknown KEY type %s to a plain KEY" % flavor) - pass - index_d = {} - index_d['name'] = spec['name'] - index_d['column_names'] = [s[0] for s in spec['columns']] - index_d['unique'] = unique - index_d['type'] = flavor - indexes.append(index_d) - return indexes - - @reflection.cache - def get_unique_constraints(self, connection, table_name, - schema=None, **kw): - parsed_state = self._parsed_state_or_create( - connection, table_name, schema, **kw) - - return [ - { - 'name': key['name'], - 'column_names': [col[0] for col in key['columns']] - } - for key in parsed_state.keys - if key['type'] == 'UNIQUE' - ] - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - - charset = self._connection_charset - full_name = '.'.join(self.identifier_preparer._quote_free_identifiers( - schema, view_name)) - sql = self._show_create_table(connection, None, charset, - full_name=full_name) - return sql - - def _parsed_state_or_create(self, connection, table_name, - schema=None, **kw): - return self._setup_parser( - connection, - table_name, - schema, - info_cache=kw.get('info_cache', None) - ) - - @util.memoized_property - def _tabledef_parser(self): - """return the MySQLTableDefinitionParser, generate if needed. - - The deferred creation ensures that the dialect has - retrieved server version information first. - - """ - if (self.server_version_info < (4, 1) and self._server_ansiquotes): - # ANSI_QUOTES doesn't affect SHOW CREATE TABLE on < 4.1 - preparer = self.preparer(self, server_ansiquotes=False) - else: - preparer = self.identifier_preparer - return MySQLTableDefinitionParser(self, preparer) - - @reflection.cache - def _setup_parser(self, connection, table_name, schema=None, **kw): - charset = self._connection_charset - parser = self._tabledef_parser - full_name = '.'.join(self.identifier_preparer._quote_free_identifiers( - schema, table_name)) - sql = self._show_create_table(connection, None, charset, - full_name=full_name) - if sql.startswith('CREATE ALGORITHM'): - # Adapt views to something table-like. - columns = self._describe_table(connection, None, charset, - full_name=full_name) - sql = parser._describe_to_create(table_name, columns) - return parser.parse(sql, charset) - - def _detect_charset(self, connection): - raise NotImplementedError() - - def _detect_casing(self, connection): - """Sniff out identifier case sensitivity. - - Cached per-connection. This value can not change without a server - restart. - - """ - # http://dev.mysql.com/doc/refman/5.0/en/name-case-sensitivity.html - - charset = self._connection_charset - row = self._compat_first(connection.execute( - "SHOW VARIABLES LIKE 'lower_case_table_names'"), - charset=charset) - if not row: - cs = 0 - else: - # 4.0.15 returns OFF or ON according to [ticket:489] - # 3.23 doesn't, 4.0.27 doesn't.. - if row[1] == 'OFF': - cs = 0 - elif row[1] == 'ON': - cs = 1 - else: - cs = int(row[1]) - return cs - - def _detect_collations(self, connection): - """Pull the active COLLATIONS list from the server. - - Cached per-connection. - """ - - collations = {} - if self.server_version_info < (4, 1, 0): - pass - else: - charset = self._connection_charset - rs = connection.execute('SHOW COLLATION') - for row in self._compat_fetchall(rs, charset): - collations[row[0]] = row[1] - return collations - - def _detect_ansiquotes(self, connection): - """Detect and adjust for the ANSI_QUOTES sql mode.""" - - row = self._compat_first( - connection.execute("SHOW VARIABLES LIKE 'sql_mode'"), - charset=self._connection_charset) - - if not row: - mode = '' - else: - mode = row[1] or '' - # 4.0 - if mode.isdigit(): - mode_no = int(mode) - mode = (mode_no | 4 == mode_no) and 'ANSI_QUOTES' or '' - - self._server_ansiquotes = 'ANSI_QUOTES' in mode - - # as of MySQL 5.0.1 - self._backslash_escapes = 'NO_BACKSLASH_ESCAPES' not in mode - - def _show_create_table(self, connection, table, charset=None, - full_name=None): - """Run SHOW CREATE TABLE for a ``Table``.""" - - if full_name is None: - full_name = self.identifier_preparer.format_table(table) - st = "SHOW CREATE TABLE %s" % full_name - - rp = None - try: - rp = connection.execute(st) - except exc.DBAPIError as e: - if self._extract_error_code(e.orig) == 1146: - raise exc.NoSuchTableError(full_name) - else: - raise - row = self._compat_first(rp, charset=charset) - if not row: - raise exc.NoSuchTableError(full_name) - return row[1].strip() - - return sql - - def _describe_table(self, connection, table, charset=None, - full_name=None): - """Run DESCRIBE for a ``Table`` and return processed rows.""" - - if full_name is None: - full_name = self.identifier_preparer.format_table(table) - st = "DESCRIBE %s" % full_name - - rp, rows = None, None - try: - try: - rp = connection.execute(st) - except exc.DBAPIError as e: - if self._extract_error_code(e.orig) == 1146: - raise exc.NoSuchTableError(full_name) - else: - raise - rows = self._compat_fetchall(rp, charset=charset) - finally: - if rp: - rp.close() - return rows - - -class ReflectedState(object): - """Stores raw information about a SHOW CREATE TABLE statement.""" - - def __init__(self): - self.columns = [] - self.table_options = {} - self.table_name = None - self.keys = [] - self.constraints = [] - - -@log.class_logger -class MySQLTableDefinitionParser(object): - """Parses the results of a SHOW CREATE TABLE statement.""" - - def __init__(self, dialect, preparer): - self.dialect = dialect - self.preparer = preparer - self._prep_regexes() - - def parse(self, show_create, charset): - state = ReflectedState() - state.charset = charset - for line in re.split(r'\r?\n', show_create): - if line.startswith(' ' + self.preparer.initial_quote): - self._parse_column(line, state) - # a regular table options line - elif line.startswith(') '): - self._parse_table_options(line, state) - # an ANSI-mode table options line - elif line == ')': - pass - elif line.startswith('CREATE '): - self._parse_table_name(line, state) - # Not present in real reflection, but may be if - # loading from a file. - elif not line: - pass - else: - type_, spec = self._parse_constraints(line) - if type_ is None: - util.warn("Unknown schema content: %r" % line) - elif type_ == 'key': - state.keys.append(spec) - elif type_ == 'constraint': - state.constraints.append(spec) - else: - pass - return state - - def _parse_constraints(self, line): - """Parse a KEY or CONSTRAINT line. - - :param line: A line of SHOW CREATE TABLE output - """ - - # KEY - m = self._re_key.match(line) - if m: - spec = m.groupdict() - # convert columns into name, length pairs - spec['columns'] = self._parse_keyexprs(spec['columns']) - return 'key', spec - - # CONSTRAINT - m = self._re_constraint.match(line) - if m: - spec = m.groupdict() - spec['table'] = \ - self.preparer.unformat_identifiers(spec['table']) - spec['local'] = [c[0] - for c in self._parse_keyexprs(spec['local'])] - spec['foreign'] = [c[0] - for c in self._parse_keyexprs(spec['foreign'])] - return 'constraint', spec - - # PARTITION and SUBPARTITION - m = self._re_partition.match(line) - if m: - # Punt! - return 'partition', line - - # No match. - return (None, line) - - def _parse_table_name(self, line, state): - """Extract the table name. - - :param line: The first line of SHOW CREATE TABLE - """ - - regex, cleanup = self._pr_name - m = regex.match(line) - if m: - state.table_name = cleanup(m.group('name')) - - def _parse_table_options(self, line, state): - """Build a dictionary of all reflected table-level options. - - :param line: The final line of SHOW CREATE TABLE output. - """ - - options = {} - - if not line or line == ')': - pass - - else: - rest_of_line = line[:] - for regex, cleanup in self._pr_options: - m = regex.search(rest_of_line) - if not m: - continue - directive, value = m.group('directive'), m.group('val') - if cleanup: - value = cleanup(value) - options[directive.lower()] = value - rest_of_line = regex.sub('', rest_of_line) - - for nope in ('auto_increment', 'data directory', 'index directory'): - options.pop(nope, None) - - for opt, val in options.items(): - state.table_options['%s_%s' % (self.dialect.name, opt)] = val - - def _parse_column(self, line, state): - """Extract column details. - - Falls back to a 'minimal support' variant if full parse fails. - - :param line: Any column-bearing line from SHOW CREATE TABLE - """ - - spec = None - m = self._re_column.match(line) - if m: - spec = m.groupdict() - spec['full'] = True - else: - m = self._re_column_loose.match(line) - if m: - spec = m.groupdict() - spec['full'] = False - if not spec: - util.warn("Unknown column definition %r" % line) - return - if not spec['full']: - util.warn("Incomplete reflection of column definition %r" % line) - - name, type_, args, notnull = \ - spec['name'], spec['coltype'], spec['arg'], spec['notnull'] - - try: - col_type = self.dialect.ischema_names[type_] - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (type_, name)) - col_type = sqltypes.NullType - - # Column type positional arguments eg. varchar(32) - if args is None or args == '': - type_args = [] - elif args[0] == "'" and args[-1] == "'": - type_args = self._re_csv_str.findall(args) - else: - type_args = [int(v) for v in self._re_csv_int.findall(args)] - - # Column type keyword options - type_kw = {} - for kw in ('unsigned', 'zerofill'): - if spec.get(kw, False): - type_kw[kw] = True - for kw in ('charset', 'collate'): - if spec.get(kw, False): - type_kw[kw] = spec[kw] - - if issubclass(col_type, _EnumeratedValues): - type_args = _EnumeratedValues._strip_values(type_args) - - type_instance = col_type(*type_args, **type_kw) - - col_args, col_kw = [], {} - - # NOT NULL - col_kw['nullable'] = True - if spec.get('notnull', False): - col_kw['nullable'] = False - - # AUTO_INCREMENT - if spec.get('autoincr', False): - col_kw['autoincrement'] = True - elif issubclass(col_type, sqltypes.Integer): - col_kw['autoincrement'] = False - - # DEFAULT - default = spec.get('default', None) - - if default == 'NULL': - # eliminates the need to deal with this later. - default = None - - col_d = dict(name=name, type=type_instance, default=default) - col_d.update(col_kw) - state.columns.append(col_d) - - def _describe_to_create(self, table_name, columns): - """Re-format DESCRIBE output as a SHOW CREATE TABLE string. - - DESCRIBE is a much simpler reflection and is sufficient for - reflecting views for runtime use. This method formats DDL - for columns only- keys are omitted. - - :param columns: A sequence of DESCRIBE or SHOW COLUMNS 6-tuples. - SHOW FULL COLUMNS FROM rows must be rearranged for use with - this function. - """ - - buffer = [] - for row in columns: - (name, col_type, nullable, default, extra) = \ - [row[i] for i in (0, 1, 2, 4, 5)] - - line = [' '] - line.append(self.preparer.quote_identifier(name)) - line.append(col_type) - if not nullable: - line.append('NOT NULL') - if default: - if 'auto_increment' in default: - pass - elif (col_type.startswith('timestamp') and - default.startswith('C')): - line.append('DEFAULT') - line.append(default) - elif default == 'NULL': - line.append('DEFAULT') - line.append(default) - else: - line.append('DEFAULT') - line.append("'%s'" % default.replace("'", "''")) - if extra: - line.append(extra) - - buffer.append(' '.join(line)) - - return ''.join([('CREATE TABLE %s (\n' % - self.preparer.quote_identifier(table_name)), - ',\n'.join(buffer), - '\n) ']) - - def _parse_keyexprs(self, identifiers): - """Unpack '"col"(2),"col" ASC'-ish strings into components.""" - - return self._re_keyexprs.findall(identifiers) - - def _prep_regexes(self): - """Pre-compile regular expressions.""" - - self._re_columns = [] - self._pr_options = [] - - _final = self.preparer.final_quote - - quotes = dict(zip(('iq', 'fq', 'esc_fq'), - [re.escape(s) for s in - (self.preparer.initial_quote, - _final, - self.preparer._escape_identifier(_final))])) - - self._pr_name = _pr_compile( - r'^CREATE (?:\w+ +)?TABLE +' - r'%(iq)s(?P(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($' % quotes, - self.preparer._unescape_identifier) - - # `col`,`col2`(32),`col3`(15) DESC - # - # Note: ASC and DESC aren't reflected, so we'll punt... - self._re_keyexprs = _re_compile( - r'(?:' - r'(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)' - r'(?:\((\d+)\))?(?=\,|$))+' % quotes) - - # 'foo' or 'foo','bar' or 'fo,o','ba''a''r' - self._re_csv_str = _re_compile(r'\x27(?:\x27\x27|[^\x27])*\x27') - - # 123 or 123,456 - self._re_csv_int = _re_compile(r'\d+') - - # `colname` [type opts] - # (NOT NULL | NULL) - # DEFAULT ('value' | CURRENT_TIMESTAMP...) - # COMMENT 'comment' - # COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT) - # STORAGE (DISK|MEMORY) - self._re_column = _re_compile( - r' ' - r'%(iq)s(?P(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'(?P\w+)' - r'(?:\((?P(?:\d+|\d+,\d+|' - r'(?:\x27(?:\x27\x27|[^\x27])*\x27,?)+))\))?' - r'(?: +(?PUNSIGNED))?' - r'(?: +(?PZEROFILL))?' - r'(?: +CHARACTER SET +(?P[\w_]+))?' - r'(?: +COLLATE +(?P[\w_]+))?' - r'(?: +(?PNOT NULL))?' - r'(?: +DEFAULT +(?P' - r'(?:NULL|\x27(?:\x27\x27|[^\x27])*\x27|\w+' - r'(?: +ON UPDATE \w+)?)' - r'))?' - r'(?: +(?PAUTO_INCREMENT))?' - r'(?: +COMMENT +(P(?:\x27\x27|[^\x27])+))?' - r'(?: +COLUMN_FORMAT +(?P\w+))?' - r'(?: +STORAGE +(?P\w+))?' - r'(?: +(?P.*))?' - r',?$' - % quotes - ) - - # Fallback, try to parse as little as possible - self._re_column_loose = _re_compile( - r' ' - r'%(iq)s(?P(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'(?P\w+)' - r'(?:\((?P(?:\d+|\d+,\d+|\x27(?:\x27\x27|[^\x27])+\x27))\))?' - r'.*?(?PNOT NULL)?' - % quotes - ) - - # (PRIMARY|UNIQUE|FULLTEXT|SPATIAL) INDEX `name` (USING (BTREE|HASH))? - # (`col` (ASC|DESC)?, `col` (ASC|DESC)?) - # KEY_BLOCK_SIZE size | WITH PARSER name - self._re_key = _re_compile( - r' ' - r'(?:(?P\S+) )?KEY' - r'(?: +%(iq)s(?P(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)?' - r'(?: +USING +(?P\S+))?' - r' +\((?P.+?)\)' - r'(?: +USING +(?P\S+))?' - r'(?: +KEY_BLOCK_SIZE +(?P\S+))?' - r'(?: +WITH PARSER +(?P\S+))?' - r',?$' - % quotes - ) - - # CONSTRAINT `name` FOREIGN KEY (`local_col`) - # REFERENCES `remote` (`remote_col`) - # MATCH FULL | MATCH PARTIAL | MATCH SIMPLE - # ON DELETE CASCADE ON UPDATE RESTRICT - # - # unique constraints come back as KEYs - kw = quotes.copy() - kw['on'] = 'RESTRICT|CASCADE|SET NULL|NOACTION' - self._re_constraint = _re_compile( - r' ' - r'CONSTRAINT +' - r'%(iq)s(?P(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'FOREIGN KEY +' - r'\((?P[^\)]+?)\) REFERENCES +' - r'(?P%(iq)s[^%(fq)s]+%(fq)s(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +' - r'\((?P[^\)]+?)\)' - r'(?: +(?PMATCH \w+))?' - r'(?: +ON DELETE (?P%(on)s))?' - r'(?: +ON UPDATE (?P%(on)s))?' - % kw - ) - - # PARTITION - # - # punt! - self._re_partition = _re_compile(r'(?:.*)(?:SUB)?PARTITION(?:.*)') - - # Table-level options (COLLATE, ENGINE, etc.) - # Do the string options first, since they have quoted - # strings we need to get rid of. - for option in _options_of_type_string: - self._add_option_string(option) - - for option in ('ENGINE', 'TYPE', 'AUTO_INCREMENT', - 'AVG_ROW_LENGTH', 'CHARACTER SET', - 'DEFAULT CHARSET', 'CHECKSUM', - 'COLLATE', 'DELAY_KEY_WRITE', 'INSERT_METHOD', - 'MAX_ROWS', 'MIN_ROWS', 'PACK_KEYS', 'ROW_FORMAT', - 'KEY_BLOCK_SIZE'): - self._add_option_word(option) - - self._add_option_regex('UNION', r'\([^\)]+\)') - self._add_option_regex('TABLESPACE', r'.*? STORAGE DISK') - self._add_option_regex('RAID_TYPE', - r'\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+') - - _optional_equals = r'(?:\s*(?:=\s*)|\s+)' - - def _add_option_string(self, directive): - regex = (r'(?P%s)%s' - r"'(?P(?:[^']|'')*?)'(?!')" % - (re.escape(directive), self._optional_equals)) - self._pr_options.append(_pr_compile(regex, lambda v: - v.replace("\\\\", "\\").replace("''", "'"))) - - def _add_option_word(self, directive): - regex = (r'(?P%s)%s' - r'(?P\w+)' % - (re.escape(directive), self._optional_equals)) - self._pr_options.append(_pr_compile(regex)) - - def _add_option_regex(self, directive, regex): - regex = (r'(?P%s)%s' - r'(?P%s)' % - (re.escape(directive), self._optional_equals, regex)) - self._pr_options.append(_pr_compile(regex)) - -_options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY', - 'PASSWORD', 'CONNECTION') - - - -class _DecodingRowProxy(object): - """Return unicode-decoded values based on type inspection. - - Smooth over data type issues (esp. with alpha driver versions) and - normalize strings as Unicode regardless of user-configured driver - encoding settings. - - """ - - # Some MySQL-python versions can return some columns as - # sets.Set(['value']) (seriously) but thankfully that doesn't - # seem to come up in DDL queries. - - def __init__(self, rowproxy, charset): - self.rowproxy = rowproxy - self.charset = charset - - def __getitem__(self, index): - item = self.rowproxy[index] - if isinstance(item, _array): - item = item.tostring() - - if self.charset and isinstance(item, util.binary_type): - return item.decode(self.charset) - else: - return item - - def __getattr__(self, attr): - item = getattr(self.rowproxy, attr) - if isinstance(item, _array): - item = item.tostring() - if self.charset and isinstance(item, util.binary_type): - return item.decode(self.charset) - else: - return item - - -def _pr_compile(regex, cleanup=None): - """Prepare a 2-tuple of compiled regex and callable.""" - - return (_re_compile(regex), cleanup) - - -def _re_compile(regex): - """Compile a string to regex, I and UNICODE.""" - - return re.compile(regex, re.I | re.UNICODE) diff --git a/libs/sqlalchemy/dialects/mysql/cymysql.py b/libs/sqlalchemy/dialects/mysql/cymysql.py deleted file mode 100644 index e81a79b8..00000000 --- a/libs/sqlalchemy/dialects/mysql/cymysql.py +++ /dev/null @@ -1,69 +0,0 @@ -# mysql/cymysql.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql+cymysql - :name: CyMySQL - :dbapi: cymysql - :connectstring: mysql+cymysql://:@/[?] - :url: https://github.com/nakagami/CyMySQL - -""" - -from .mysqldb import MySQLDialect_mysqldb -from .base import (BIT, MySQLDialect) -from ... import util - -class _cymysqlBIT(BIT): - def result_processor(self, dialect, coltype): - """Convert a MySQL's 64 bit, variable length binary string to a long. - """ - - def process(value): - if value is not None: - v = 0 - for i in util.iterbytes(value): - v = v << 8 | i - return v - return value - return process - - -class MySQLDialect_cymysql(MySQLDialect_mysqldb): - driver = 'cymysql' - - description_encoding = None - supports_sane_rowcount = True - supports_sane_multi_rowcount = False - supports_unicode_statements = True - - colspecs = util.update_copy( - MySQLDialect.colspecs, - { - BIT: _cymysqlBIT, - } - ) - - @classmethod - def dbapi(cls): - return __import__('cymysql') - - def _extract_error_code(self, exception): - return exception.errno - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.OperationalError): - return self._extract_error_code(e) in \ - (2006, 2013, 2014, 2045, 2055) - elif isinstance(e, self.dbapi.InterfaceError): - # if underlying connection is closed, - # this is the error you get - return True - else: - return False - -dialect = MySQLDialect_cymysql diff --git a/libs/sqlalchemy/dialects/mysql/gaerdbms.py b/libs/sqlalchemy/dialects/mysql/gaerdbms.py deleted file mode 100644 index 13203fce..00000000 --- a/libs/sqlalchemy/dialects/mysql/gaerdbms.py +++ /dev/null @@ -1,84 +0,0 @@ -# mysql/gaerdbms.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -""" -.. dialect:: mysql+gaerdbms - :name: Google Cloud SQL - :dbapi: rdbms - :connectstring: mysql+gaerdbms:///?instance= - :url: https://developers.google.com/appengine/docs/python/cloud-sql/developers-guide - - This dialect is based primarily on the :mod:`.mysql.mysqldb` dialect with minimal - changes. - - .. versionadded:: 0.7.8 - - -Pooling -------- - -Google App Engine connections appear to be randomly recycled, -so the dialect does not pool connections. The :class:`.NullPool` -implementation is installed within the :class:`.Engine` by -default. - -""" - -import os - -from .mysqldb import MySQLDialect_mysqldb -from ...pool import NullPool -import re - - -def _is_dev_environment(): - return os.environ.get('SERVER_SOFTWARE', '').startswith('Development/') - - -class MySQLDialect_gaerdbms(MySQLDialect_mysqldb): - - @classmethod - def dbapi(cls): - # from django: - # http://code.google.com/p/googleappengine/source/ - # browse/trunk/python/google/storage/speckle/ - # python/django/backend/base.py#118 - # see also [ticket:2649] - # see also http://stackoverflow.com/q/14224679/34549 - from google.appengine.api import apiproxy_stub_map - - if _is_dev_environment(): - from google.appengine.api import rdbms_mysqldb - return rdbms_mysqldb - elif apiproxy_stub_map.apiproxy.GetStub('rdbms'): - from google.storage.speckle.python.api import rdbms_apiproxy - return rdbms_apiproxy - else: - from google.storage.speckle.python.api import rdbms_googleapi - return rdbms_googleapi - - @classmethod - def get_pool_class(cls, url): - # Cloud SQL connections die at any moment - return NullPool - - def create_connect_args(self, url): - opts = url.translate_connect_args() - if not _is_dev_environment(): - # 'dsn' and 'instance' are because we are skipping - # the traditional google.api.rdbms wrapper - opts['dsn'] = '' - opts['instance'] = url.query['instance'] - return [], opts - - def _extract_error_code(self, exception): - match = re.compile(r"^(\d+)L?:|^\((\d+)L?,").match(str(exception)) - # The rdbms api will wrap then re-raise some types of errors - # making this regex return no matches. - code = match.group(1) or match.group(2) if match else None - if code: - return int(code) - -dialect = MySQLDialect_gaerdbms diff --git a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py deleted file mode 100644 index b6e7c75f..00000000 --- a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py +++ /dev/null @@ -1,128 +0,0 @@ -# mysql/mysqlconnector.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: mysql+mysqlconnector - :name: MySQL Connector/Python - :dbapi: myconnpy - :connectstring: mysql+mysqlconnector://:@[:]/ - :url: https://launchpad.net/myconnpy - - -""" - -from .base import (MySQLDialect, - MySQLExecutionContext, MySQLCompiler, MySQLIdentifierPreparer, - BIT) - -from ... import util - - -class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext): - - def get_lastrowid(self): - return self.cursor.lastrowid - - -class MySQLCompiler_mysqlconnector(MySQLCompiler): - def visit_mod_binary(self, binary, operator, **kw): - return self.process(binary.left, **kw) + " %% " + \ - self.process(binary.right, **kw) - - def post_process_text(self, text): - return text.replace('%', '%%') - - -class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer): - - def _escape_identifier(self, value): - value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace("%", "%%") - - -class _myconnpyBIT(BIT): - def result_processor(self, dialect, coltype): - """MySQL-connector already converts mysql bits, so.""" - - return None - - -class MySQLDialect_mysqlconnector(MySQLDialect): - driver = 'mysqlconnector' - supports_unicode_statements = True - supports_unicode_binds = True - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - - supports_native_decimal = True - - default_paramstyle = 'format' - execution_ctx_cls = MySQLExecutionContext_mysqlconnector - statement_compiler = MySQLCompiler_mysqlconnector - - preparer = MySQLIdentifierPreparer_mysqlconnector - - colspecs = util.update_copy( - MySQLDialect.colspecs, - { - BIT: _myconnpyBIT, - } - ) - - @classmethod - def dbapi(cls): - from mysql import connector - return connector - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - - opts.update(url.query) - - util.coerce_kw_type(opts, 'buffered', bool) - util.coerce_kw_type(opts, 'raise_on_warnings', bool) - opts.setdefault('buffered', True) - opts.setdefault('raise_on_warnings', True) - - # FOUND_ROWS must be set in ClientFlag to enable - # supports_sane_rowcount. - if self.dbapi is not None: - try: - from mysql.connector.constants import ClientFlag - client_flags = opts.get('client_flags', ClientFlag.get_default()) - client_flags |= ClientFlag.FOUND_ROWS - opts['client_flags'] = client_flags - except: - pass - return [[], opts] - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - version = dbapi_con.get_server_version() - return tuple(version) - - def _detect_charset(self, connection): - return connection.connection.charset - - def _extract_error_code(self, exception): - return exception.errno - - def is_disconnect(self, e, connection, cursor): - errnos = (2006, 2013, 2014, 2045, 2055, 2048) - exceptions = (self.dbapi.OperationalError, self.dbapi.InterfaceError) - if isinstance(e, exceptions): - return e.errno in errnos or \ - "MySQL Connection not available." in str(e) - else: - return False - - def _compat_fetchall(self, rp, charset=None): - return rp.fetchall() - - def _compat_fetchone(self, rp, charset=None): - return rp.fetchone() - -dialect = MySQLDialect_mysqlconnector diff --git a/libs/sqlalchemy/dialects/mysql/mysqldb.py b/libs/sqlalchemy/dialects/mysql/mysqldb.py deleted file mode 100644 index c6942ae2..00000000 --- a/libs/sqlalchemy/dialects/mysql/mysqldb.py +++ /dev/null @@ -1,78 +0,0 @@ -# mysql/mysqldb.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql+mysqldb - :name: MySQL-Python - :dbapi: mysqldb - :connectstring: mysql+mysqldb://:@[:]/ - :url: http://sourceforge.net/projects/mysql-python - - -Unicode -------- - -MySQLdb will accommodate Python ``unicode`` objects if the -``use_unicode=1`` parameter, or the ``charset`` parameter, -is passed as a connection argument. - -Without this setting, many MySQL server installations default to -a ``latin1`` encoding for client connections, which has the effect -of all data being converted into ``latin1``, even if you have ``utf8`` -or another character set configured on your tables -and columns. With versions 4.1 and higher, you can change the connection -character set either through server configuration or by including the -``charset`` parameter. The ``charset`` -parameter as received by MySQL-Python also has the side-effect of -enabling ``use_unicode=1``:: - - # set client encoding to utf8; all strings come back as unicode - create_engine('mysql+mysqldb:///mydb?charset=utf8') - -Manually configuring ``use_unicode=0`` will cause MySQL-python to -return encoded strings:: - - # set client encoding to utf8; all strings come back as utf8 str - create_engine('mysql+mysqldb:///mydb?charset=utf8&use_unicode=0') - -Known Issues -------------- - -MySQL-python version 1.2.2 has a serious memory leak related -to unicode conversion, a feature which is disabled via ``use_unicode=0``. -It is strongly advised to use the latest version of MySQL-Python. - -""" - -from .base import (MySQLDialect, MySQLExecutionContext, - MySQLCompiler, MySQLIdentifierPreparer) -from ...connectors.mysqldb import ( - MySQLDBExecutionContext, - MySQLDBCompiler, - MySQLDBIdentifierPreparer, - MySQLDBConnector - ) - - -class MySQLExecutionContext_mysqldb(MySQLDBExecutionContext, MySQLExecutionContext): - pass - - -class MySQLCompiler_mysqldb(MySQLDBCompiler, MySQLCompiler): - pass - - -class MySQLIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, MySQLIdentifierPreparer): - pass - - -class MySQLDialect_mysqldb(MySQLDBConnector, MySQLDialect): - execution_ctx_cls = MySQLExecutionContext_mysqldb - statement_compiler = MySQLCompiler_mysqldb - preparer = MySQLIdentifierPreparer_mysqldb - -dialect = MySQLDialect_mysqldb diff --git a/libs/sqlalchemy/dialects/mysql/oursql.py b/libs/sqlalchemy/dialects/mysql/oursql.py deleted file mode 100644 index e6b50f33..00000000 --- a/libs/sqlalchemy/dialects/mysql/oursql.py +++ /dev/null @@ -1,261 +0,0 @@ -# mysql/oursql.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql+oursql - :name: OurSQL - :dbapi: oursql - :connectstring: mysql+oursql://:@[:]/ - :url: http://packages.python.org/oursql/ - -Unicode -------- - -oursql defaults to using ``utf8`` as the connection charset, but other -encodings may be used instead. Like the MySQL-Python driver, unicode support -can be completely disabled:: - - # oursql sets the connection charset to utf8 automatically; all strings come - # back as utf8 str - create_engine('mysql+oursql:///mydb?use_unicode=0') - -To not automatically use ``utf8`` and instead use whatever the connection -defaults to, there is a separate parameter:: - - # use the default connection charset; all strings come back as unicode - create_engine('mysql+oursql:///mydb?default_charset=1') - - # use latin1 as the connection charset; all strings come back as unicode - create_engine('mysql+oursql:///mydb?charset=latin1') -""" - -import re - -from .base import (BIT, MySQLDialect, MySQLExecutionContext) -from ... import types as sqltypes, util - - -class _oursqlBIT(BIT): - def result_processor(self, dialect, coltype): - """oursql already converts mysql bits, so.""" - - return None - - -class MySQLExecutionContext_oursql(MySQLExecutionContext): - - @property - def plain_query(self): - return self.execution_options.get('_oursql_plain_query', False) - - -class MySQLDialect_oursql(MySQLDialect): - driver = 'oursql' - - if util.py2k: - supports_unicode_binds = True - supports_unicode_statements = True - - supports_native_decimal = True - - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - execution_ctx_cls = MySQLExecutionContext_oursql - - colspecs = util.update_copy( - MySQLDialect.colspecs, - { - sqltypes.Time: sqltypes.Time, - BIT: _oursqlBIT, - } - ) - - @classmethod - def dbapi(cls): - return __import__('oursql') - - def do_execute(self, cursor, statement, parameters, context=None): - """Provide an implementation of *cursor.execute(statement, parameters)*.""" - - if context and context.plain_query: - cursor.execute(statement, plain_query=True) - else: - cursor.execute(statement, parameters) - - def do_begin(self, connection): - connection.cursor().execute('BEGIN', plain_query=True) - - def _xa_query(self, connection, query, xid): - if util.py2k: - arg = connection.connection._escape_string(xid) - else: - charset = self._connection_charset - arg = connection.connection._escape_string(xid.encode(charset)).decode(charset) - arg = "'%s'" % arg - connection.execution_options(_oursql_plain_query=True).execute(query % arg) - - # Because mysql is bad, these methods have to be - # reimplemented to use _PlainQuery. Basically, some queries - # refuse to return any data if they're run through - # the parameterized query API, or refuse to be parameterized - # in the first place. - def do_begin_twophase(self, connection, xid): - self._xa_query(connection, 'XA BEGIN %s', xid) - - def do_prepare_twophase(self, connection, xid): - self._xa_query(connection, 'XA END %s', xid) - self._xa_query(connection, 'XA PREPARE %s', xid) - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - self._xa_query(connection, 'XA END %s', xid) - self._xa_query(connection, 'XA ROLLBACK %s', xid) - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - self.do_prepare_twophase(connection, xid) - self._xa_query(connection, 'XA COMMIT %s', xid) - - # Q: why didn't we need all these "plain_query" overrides earlier ? - # am i on a newer/older version of OurSQL ? - def has_table(self, connection, table_name, schema=None): - return MySQLDialect.has_table( - self, - connection.connect().execution_options(_oursql_plain_query=True), - table_name, - schema - ) - - def get_table_options(self, connection, table_name, schema=None, **kw): - return MySQLDialect.get_table_options( - self, - connection.connect().execution_options(_oursql_plain_query=True), - table_name, - schema=schema, - **kw - ) - - def get_columns(self, connection, table_name, schema=None, **kw): - return MySQLDialect.get_columns( - self, - connection.connect().execution_options(_oursql_plain_query=True), - table_name, - schema=schema, - **kw - ) - - def get_view_names(self, connection, schema=None, **kw): - return MySQLDialect.get_view_names( - self, - connection.connect().execution_options(_oursql_plain_query=True), - schema=schema, - **kw - ) - - def get_table_names(self, connection, schema=None, **kw): - return MySQLDialect.get_table_names( - self, - connection.connect().execution_options(_oursql_plain_query=True), - schema - ) - - def get_schema_names(self, connection, **kw): - return MySQLDialect.get_schema_names( - self, - connection.connect().execution_options(_oursql_plain_query=True), - **kw - ) - - def initialize(self, connection): - return MySQLDialect.initialize( - self, - connection.execution_options(_oursql_plain_query=True) - ) - - def _show_create_table(self, connection, table, charset=None, - full_name=None): - return MySQLDialect._show_create_table( - self, - connection.contextual_connect(close_with_result=True). - execution_options(_oursql_plain_query=True), - table, charset, full_name - ) - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.ProgrammingError): - return e.errno is None and 'cursor' not in e.args[1] and e.args[1].endswith('closed') - else: - return e.errno in (2006, 2013, 2014, 2045, 2055) - - def create_connect_args(self, url): - opts = url.translate_connect_args(database='db', username='user', - password='passwd') - opts.update(url.query) - - util.coerce_kw_type(opts, 'port', int) - util.coerce_kw_type(opts, 'compress', bool) - util.coerce_kw_type(opts, 'autoping', bool) - util.coerce_kw_type(opts, 'raise_on_warnings', bool) - - util.coerce_kw_type(opts, 'default_charset', bool) - if opts.pop('default_charset', False): - opts['charset'] = None - else: - util.coerce_kw_type(opts, 'charset', str) - opts['use_unicode'] = opts.get('use_unicode', True) - util.coerce_kw_type(opts, 'use_unicode', bool) - - # FOUND_ROWS must be set in CLIENT_FLAGS to enable - # supports_sane_rowcount. - opts.setdefault('found_rows', True) - - ssl = {} - for key in ['ssl_ca', 'ssl_key', 'ssl_cert', - 'ssl_capath', 'ssl_cipher']: - if key in opts: - ssl[key[4:]] = opts[key] - util.coerce_kw_type(ssl, key[4:], str) - del opts[key] - if ssl: - opts['ssl'] = ssl - - return [[], opts] - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - version = [] - r = re.compile('[.\-]') - for n in r.split(dbapi_con.server_info): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) - - def _extract_error_code(self, exception): - return exception.errno - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - return connection.connection.charset - - def _compat_fetchall(self, rp, charset=None): - """oursql isn't super-broken like MySQLdb, yaaay.""" - return rp.fetchall() - - def _compat_fetchone(self, rp, charset=None): - """oursql isn't super-broken like MySQLdb, yaaay.""" - return rp.fetchone() - - def _compat_first(self, rp, charset=None): - return rp.first() - - -dialect = MySQLDialect_oursql diff --git a/libs/sqlalchemy/dialects/mysql/pymysql.py b/libs/sqlalchemy/dialects/mysql/pymysql.py deleted file mode 100644 index 74de09c4..00000000 --- a/libs/sqlalchemy/dialects/mysql/pymysql.py +++ /dev/null @@ -1,44 +0,0 @@ -# mysql/pymysql.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql+pymysql - :name: PyMySQL - :dbapi: pymysql - :connectstring: mysql+pymysql://:@/[?] - :url: http://code.google.com/p/pymysql/ - -MySQL-Python Compatibility --------------------------- - -The pymysql DBAPI is a pure Python port of the MySQL-python (MySQLdb) driver, -and targets 100% compatibility. Most behavioral notes for MySQL-python apply to -the pymysql driver as well. - -""" - -from .mysqldb import MySQLDialect_mysqldb -from ...util import py3k - -class MySQLDialect_pymysql(MySQLDialect_mysqldb): - driver = 'pymysql' - - description_encoding = None - if py3k: - supports_unicode_statements = True - - @classmethod - def dbapi(cls): - return __import__('pymysql') - - if py3k: - def _extract_error_code(self, exception): - if isinstance(exception.args[0], Exception): - exception = exception.args[0] - return exception.args[0] - -dialect = MySQLDialect_pymysql diff --git a/libs/sqlalchemy/dialects/mysql/pyodbc.py b/libs/sqlalchemy/dialects/mysql/pyodbc.py deleted file mode 100644 index e60e39ce..00000000 --- a/libs/sqlalchemy/dialects/mysql/pyodbc.py +++ /dev/null @@ -1,80 +0,0 @@ -# mysql/pyodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - - -.. dialect:: mysql+pyodbc - :name: PyODBC - :dbapi: pyodbc - :connectstring: mysql+pyodbc://:@ - :url: http://pypi.python.org/pypi/pyodbc/ - - -Limitations ------------ - -The mysql-pyodbc dialect is subject to unresolved character encoding issues -which exist within the current ODBC drivers available. -(see http://code.google.com/p/pyodbc/issues/detail?id=25). Consider usage -of OurSQL, MySQLdb, or MySQL-connector/Python. - -""" - -from .base import MySQLDialect, MySQLExecutionContext -from ...connectors.pyodbc import PyODBCConnector -from ... import util -import re - - -class MySQLExecutionContext_pyodbc(MySQLExecutionContext): - - def get_lastrowid(self): - cursor = self.create_cursor() - cursor.execute("SELECT LAST_INSERT_ID()") - lastrowid = cursor.fetchone()[0] - cursor.close() - return lastrowid - - -class MySQLDialect_pyodbc(PyODBCConnector, MySQLDialect): - supports_unicode_statements = False - execution_ctx_cls = MySQLExecutionContext_pyodbc - - pyodbc_driver_name = "MySQL" - - def __init__(self, **kw): - # deal with http://code.google.com/p/pyodbc/issues/detail?id=25 - kw.setdefault('convert_unicode', True) - super(MySQLDialect_pyodbc, self).__init__(**kw) - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - # Prefer 'character_set_results' for the current connection over the - # value in the driver. SET NAMES or individual variable SETs will - # change the charset without updating the driver's view of the world. - # - # If it's decided that issuing that sort of SQL leaves you SOL, then - # this can prefer the driver value. - rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'") - opts = dict([(row[0], row[1]) for row in self._compat_fetchall(rs)]) - for key in ('character_set_connection', 'character_set'): - if opts.get(key, None): - return opts[key] - - util.warn("Could not detect the connection character set. Assuming latin1.") - return 'latin1' - - def _extract_error_code(self, exception): - m = re.compile(r"\((\d+)\)").search(str(exception.args)) - c = m.group(1) - if c: - return int(c) - else: - return None - -dialect = MySQLDialect_pyodbc diff --git a/libs/sqlalchemy/dialects/mysql/zxjdbc.py b/libs/sqlalchemy/dialects/mysql/zxjdbc.py deleted file mode 100644 index b5fcfbda..00000000 --- a/libs/sqlalchemy/dialects/mysql/zxjdbc.py +++ /dev/null @@ -1,111 +0,0 @@ -# mysql/zxjdbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: mysql+zxjdbc - :name: zxjdbc for Jython - :dbapi: zxjdbc - :connectstring: mysql+zxjdbc://:@[:]/ - :driverurl: http://dev.mysql.com/downloads/connector/j/ - -Character Sets --------------- - -SQLAlchemy zxjdbc dialects pass unicode straight through to the -zxjdbc/JDBC layer. To allow multiple character sets to be sent from the -MySQL Connector/J JDBC driver, by default SQLAlchemy sets its -``characterEncoding`` connection property to ``UTF-8``. It may be -overriden via a ``create_engine`` URL parameter. - -""" -import re - -from ... import types as sqltypes, util -from ...connectors.zxJDBC import ZxJDBCConnector -from .base import BIT, MySQLDialect, MySQLExecutionContext - - -class _ZxJDBCBit(BIT): - def result_processor(self, dialect, coltype): - """Converts boolean or byte arrays from MySQL Connector/J to longs.""" - def process(value): - if value is None: - return value - if isinstance(value, bool): - return int(value) - v = 0 - for i in value: - v = v << 8 | (i & 0xff) - value = v - return value - return process - - -class MySQLExecutionContext_zxjdbc(MySQLExecutionContext): - def get_lastrowid(self): - cursor = self.create_cursor() - cursor.execute("SELECT LAST_INSERT_ID()") - lastrowid = cursor.fetchone()[0] - cursor.close() - return lastrowid - - -class MySQLDialect_zxjdbc(ZxJDBCConnector, MySQLDialect): - jdbc_db_name = 'mysql' - jdbc_driver_name = 'com.mysql.jdbc.Driver' - - execution_ctx_cls = MySQLExecutionContext_zxjdbc - - colspecs = util.update_copy( - MySQLDialect.colspecs, - { - sqltypes.Time: sqltypes.Time, - BIT: _ZxJDBCBit - } - ) - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - # Prefer 'character_set_results' for the current connection over the - # value in the driver. SET NAMES or individual variable SETs will - # change the charset without updating the driver's view of the world. - # - # If it's decided that issuing that sort of SQL leaves you SOL, then - # this can prefer the driver value. - rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'") - opts = dict((row[0], row[1]) for row in self._compat_fetchall(rs)) - for key in ('character_set_connection', 'character_set'): - if opts.get(key, None): - return opts[key] - - util.warn("Could not detect the connection character set. Assuming latin1.") - return 'latin1' - - def _driver_kwargs(self): - """return kw arg dict to be sent to connect().""" - return dict(characterEncoding='UTF-8', yearIsDateType='false') - - def _extract_error_code(self, exception): - # e.g.: DBAPIError: (Error) Table 'test.u2' doesn't exist - # [SQLCode: 1146], [SQLState: 42S02] 'DESCRIBE `u2`' () - m = re.compile(r"\[SQLCode\: (\d+)\]").search(str(exception.args)) - c = m.group(1) - if c: - return int(c) - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - version = [] - r = re.compile('[.\-]') - for n in r.split(dbapi_con.dbversion): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) - -dialect = MySQLDialect_zxjdbc diff --git a/libs/sqlalchemy/dialects/oracle/__init__.py b/libs/sqlalchemy/dialects/oracle/__init__.py deleted file mode 100644 index 070e387d..00000000 --- a/libs/sqlalchemy/dialects/oracle/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# oracle/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.oracle import base, cx_oracle, zxjdbc - -base.dialect = cx_oracle.dialect - -from sqlalchemy.dialects.oracle.base import \ - VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, NUMBER,\ - BLOB, BFILE, CLOB, NCLOB, TIMESTAMP, RAW,\ - FLOAT, DOUBLE_PRECISION, LONG, dialect, INTERVAL,\ - VARCHAR2, NVARCHAR2, ROWID, dialect - - -__all__ = ( -'VARCHAR', 'NVARCHAR', 'CHAR', 'DATE', 'DATETIME', 'NUMBER', -'BLOB', 'BFILE', 'CLOB', 'NCLOB', 'TIMESTAMP', 'RAW', -'FLOAT', 'DOUBLE_PRECISION', 'LONG', 'dialect', 'INTERVAL', -'VARCHAR2', 'NVARCHAR2', 'ROWID' -) diff --git a/libs/sqlalchemy/dialects/oracle/base.py b/libs/sqlalchemy/dialects/oracle/base.py deleted file mode 100644 index e5a16044..00000000 --- a/libs/sqlalchemy/dialects/oracle/base.py +++ /dev/null @@ -1,1249 +0,0 @@ -# oracle/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: oracle - :name: Oracle - - Oracle version 8 through current (11g at the time of this writing) are supported. - -Connect Arguments ------------------ - -The dialect supports several :func:`~sqlalchemy.create_engine()` arguments which -affect the behavior of the dialect regardless of driver in use. - -* ``use_ansi`` - Use ANSI JOIN constructs (see the section on Oracle 8). Defaults - to ``True``. If ``False``, Oracle-8 compatible constructs are used for joins. - -* ``optimize_limits`` - defaults to ``False``. see the section on LIMIT/OFFSET. - -* ``use_binds_for_limits`` - defaults to ``True``. see the section on LIMIT/OFFSET. - -Auto Increment Behavior ------------------------ - -SQLAlchemy Table objects which include integer primary keys are usually assumed to have -"autoincrementing" behavior, meaning they can generate their own primary key values upon -INSERT. Since Oracle has no "autoincrement" feature, SQLAlchemy relies upon sequences -to produce these values. With the Oracle dialect, *a sequence must always be explicitly -specified to enable autoincrement*. This is divergent with the majority of documentation -examples which assume the usage of an autoincrement-capable database. To specify sequences, -use the sqlalchemy.schema.Sequence object which is passed to a Column construct:: - - t = Table('mytable', metadata, - Column('id', Integer, Sequence('id_seq'), primary_key=True), - Column(...), ... - ) - -This step is also required when using table reflection, i.e. autoload=True:: - - t = Table('mytable', metadata, - Column('id', Integer, Sequence('id_seq'), primary_key=True), - autoload=True - ) - -Identifier Casing ------------------ - -In Oracle, the data dictionary represents all case insensitive identifier names -using UPPERCASE text. SQLAlchemy on the other hand considers an all-lower case identifier -name to be case insensitive. The Oracle dialect converts all case insensitive identifiers -to and from those two formats during schema level communication, such as reflection of -tables and indexes. Using an UPPERCASE name on the SQLAlchemy side indicates a -case sensitive identifier, and SQLAlchemy will quote the name - this will cause mismatches -against data dictionary data received from Oracle, so unless identifier names have been -truly created as case sensitive (i.e. using quoted names), all lowercase names should be -used on the SQLAlchemy side. - -Unicode -------- - -.. versionchanged:: 0.6 - SQLAlchemy uses the "native unicode" mode provided as of cx_oracle 5. - cx_oracle 5.0.2 or greater is recommended for support of NCLOB. - If not using cx_oracle 5, the NLS_LANG environment variable needs - to be set in order for the oracle client library to use proper encoding, - such as "AMERICAN_AMERICA.UTF8". - -Also note that Oracle supports unicode data through the NVARCHAR and NCLOB data types. -When using the SQLAlchemy Unicode and UnicodeText types, these DDL types will be used -within CREATE TABLE statements. Usage of VARCHAR2 and CLOB with unicode text still -requires NLS_LANG to be set. - -LIMIT/OFFSET Support --------------------- - -Oracle has no support for the LIMIT or OFFSET keywords. SQLAlchemy uses -a wrapped subquery approach in conjunction with ROWNUM. The exact methodology -is taken from -http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html . - -There are two options which affect its behavior: - -* the "FIRST ROWS()" optimization keyword is not used by default. To enable the usage of this - optimization directive, specify ``optimize_limits=True`` to :func:`.create_engine`. -* the values passed for the limit/offset are sent as bound parameters. Some users have observed - that Oracle produces a poor query plan when the values are sent as binds and not - rendered literally. To render the limit/offset values literally within the SQL - statement, specify ``use_binds_for_limits=False`` to :func:`.create_engine`. - -Some users have reported better performance when the entirely different approach of a -window query is used, i.e. ROW_NUMBER() OVER (ORDER BY), to provide LIMIT/OFFSET (note -that the majority of users don't observe this). To suit this case the -method used for LIMIT/OFFSET can be replaced entirely. See the recipe at -http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowFunctionsByDefault -which installs a select compiler that overrides the generation of limit/offset with -a window function. - -RETURNING Support ------------------ - -The Oracle database supports a limited form of RETURNING, in order to retrieve result -sets of matched rows from INSERT, UPDATE and DELETE statements. Oracle's -RETURNING..INTO syntax only supports one row being returned, as it relies upon -OUT parameters in order to function. In addition, supported DBAPIs have further -limitations (see :ref:`cx_oracle_returning`). - -SQLAlchemy's "implicit returning" feature, which employs RETURNING within an INSERT -and sometimes an UPDATE statement in order to fetch newly generated primary key values -and other SQL defaults and expressions, is normally enabled on the Oracle -backend. By default, "implicit returning" typically only fetches the value of a -single ``nextval(some_seq)`` expression embedded into an INSERT in order to increment -a sequence within an INSERT statement and get the value back at the same time. -To disable this feature across the board, specify ``implicit_returning=False`` to -:func:`.create_engine`:: - - engine = create_engine("oracle://scott:tiger@dsn", implicit_returning=False) - -Implicit returning can also be disabled on a table-by-table basis as a table option:: - - # Core Table - my_table = Table("my_table", metadata, ..., implicit_returning=False) - - - # declarative - class MyClass(Base): - __tablename__ = 'my_table' - __table_args__ = {"implicit_returning": False} - -.. seealso:: - - :ref:`cx_oracle_returning` - additional cx_oracle-specific restrictions on implicit returning. - -ON UPDATE CASCADE ------------------ - -Oracle doesn't have native ON UPDATE CASCADE functionality. A trigger based solution -is available at http://asktom.oracle.com/tkyte/update_cascade/index.html . - -When using the SQLAlchemy ORM, the ORM has limited ability to manually issue -cascading updates - specify ForeignKey objects using the -"deferrable=True, initially='deferred'" keyword arguments, -and specify "passive_updates=False" on each relationship(). - -Oracle 8 Compatibility ----------------------- - -When Oracle 8 is detected, the dialect internally configures itself to the following -behaviors: - -* the use_ansi flag is set to False. This has the effect of converting all - JOIN phrases into the WHERE clause, and in the case of LEFT OUTER JOIN - makes use of Oracle's (+) operator. - -* the NVARCHAR2 and NCLOB datatypes are no longer generated as DDL when - the :class:`~sqlalchemy.types.Unicode` is used - VARCHAR2 and CLOB are issued - instead. This because these types don't seem to work correctly on Oracle 8 - even though they are available. The :class:`~sqlalchemy.types.NVARCHAR` - and :class:`~sqlalchemy.dialects.oracle.NCLOB` types will always generate NVARCHAR2 and NCLOB. - -* the "native unicode" mode is disabled when using cx_oracle, i.e. SQLAlchemy - encodes all Python unicode objects to "string" before passing in as bind parameters. - -Synonym/DBLINK Reflection -------------------------- - -When using reflection with Table objects, the dialect can optionally search for tables -indicated by synonyms, either in local or remote schemas or accessed over DBLINK, -by passing the flag oracle_resolve_synonyms=True as a -keyword argument to the Table construct. If synonyms are not in use -this flag should be left off. - -""" - -import re - -from sqlalchemy import util, sql -from sqlalchemy.engine import default, base, reflection -from sqlalchemy.sql import compiler, visitors, expression -from sqlalchemy.sql import operators as sql_operators, functions as sql_functions -from sqlalchemy import types as sqltypes -from sqlalchemy.types import VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, \ - BLOB, CLOB, TIMESTAMP, FLOAT - -RESERVED_WORDS = \ - set('SHARE RAW DROP BETWEEN FROM DESC OPTION PRIOR LONG THEN '\ - 'DEFAULT ALTER IS INTO MINUS INTEGER NUMBER GRANT IDENTIFIED '\ - 'ALL TO ORDER ON FLOAT DATE HAVING CLUSTER NOWAIT RESOURCE '\ - 'ANY TABLE INDEX FOR UPDATE WHERE CHECK SMALLINT WITH DELETE '\ - 'BY ASC REVOKE LIKE SIZE RENAME NOCOMPRESS NULL GROUP VALUES '\ - 'AS IN VIEW EXCLUSIVE COMPRESS SYNONYM SELECT INSERT EXISTS '\ - 'NOT TRIGGER ELSE CREATE INTERSECT PCTFREE DISTINCT USER '\ - 'CONNECT SET MODE OF UNIQUE VARCHAR2 VARCHAR LOCK OR CHAR '\ - 'DECIMAL UNION PUBLIC AND START UID COMMENT CURRENT LEVEL'.split()) - -NO_ARG_FNS = set('UID CURRENT_DATE SYSDATE USER ' - 'CURRENT_TIME CURRENT_TIMESTAMP'.split()) - - -class RAW(sqltypes._Binary): - __visit_name__ = 'RAW' -OracleRaw = RAW - - -class NCLOB(sqltypes.Text): - __visit_name__ = 'NCLOB' - - -class VARCHAR2(VARCHAR): - __visit_name__ = 'VARCHAR2' - -NVARCHAR2 = NVARCHAR - - -class NUMBER(sqltypes.Numeric, sqltypes.Integer): - __visit_name__ = 'NUMBER' - - def __init__(self, precision=None, scale=None, asdecimal=None): - if asdecimal is None: - asdecimal = bool(scale and scale > 0) - - super(NUMBER, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal) - - def adapt(self, impltype): - ret = super(NUMBER, self).adapt(impltype) - # leave a hint for the DBAPI handler - ret._is_oracle_number = True - return ret - - @property - def _type_affinity(self): - if bool(self.scale and self.scale > 0): - return sqltypes.Numeric - else: - return sqltypes.Integer - - -class DOUBLE_PRECISION(sqltypes.Numeric): - __visit_name__ = 'DOUBLE_PRECISION' - - def __init__(self, precision=None, scale=None, asdecimal=None): - if asdecimal is None: - asdecimal = False - - super(DOUBLE_PRECISION, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal) - - -class BFILE(sqltypes.LargeBinary): - __visit_name__ = 'BFILE' - - -class LONG(sqltypes.Text): - __visit_name__ = 'LONG' - - -class INTERVAL(sqltypes.TypeEngine): - __visit_name__ = 'INTERVAL' - - def __init__(self, - day_precision=None, - second_precision=None): - """Construct an INTERVAL. - - Note that only DAY TO SECOND intervals are currently supported. - This is due to a lack of support for YEAR TO MONTH intervals - within available DBAPIs (cx_oracle and zxjdbc). - - :param day_precision: the day precision value. this is the number of digits - to store for the day field. Defaults to "2" - :param second_precision: the second precision value. this is the number of digits - to store for the fractional seconds field. Defaults to "6". - - """ - self.day_precision = day_precision - self.second_precision = second_precision - - @classmethod - def _adapt_from_generic_interval(cls, interval): - return INTERVAL(day_precision=interval.day_precision, - second_precision=interval.second_precision) - - @property - def _type_affinity(self): - return sqltypes.Interval - - -class ROWID(sqltypes.TypeEngine): - """Oracle ROWID type. - - When used in a cast() or similar, generates ROWID. - - """ - __visit_name__ = 'ROWID' - - -class _OracleBoolean(sqltypes.Boolean): - def get_dbapi_type(self, dbapi): - return dbapi.NUMBER - -colspecs = { - sqltypes.Boolean: _OracleBoolean, - sqltypes.Interval: INTERVAL, -} - -ischema_names = { - 'VARCHAR2': VARCHAR, - 'NVARCHAR2': NVARCHAR, - 'CHAR': CHAR, - 'DATE': DATE, - 'NUMBER': NUMBER, - 'BLOB': BLOB, - 'BFILE': BFILE, - 'CLOB': CLOB, - 'NCLOB': NCLOB, - 'TIMESTAMP': TIMESTAMP, - 'TIMESTAMP WITH TIME ZONE': TIMESTAMP, - 'INTERVAL DAY TO SECOND': INTERVAL, - 'RAW': RAW, - 'FLOAT': FLOAT, - 'DOUBLE PRECISION': DOUBLE_PRECISION, - 'LONG': LONG, -} - - -class OracleTypeCompiler(compiler.GenericTypeCompiler): - # Note: - # Oracle DATE == DATETIME - # Oracle does not allow milliseconds in DATE - # Oracle does not support TIME columns - - def visit_datetime(self, type_): - return self.visit_DATE(type_) - - def visit_float(self, type_): - return self.visit_FLOAT(type_) - - def visit_unicode(self, type_): - if self.dialect._supports_nchar: - return self.visit_NVARCHAR2(type_) - else: - return self.visit_VARCHAR2(type_) - - def visit_INTERVAL(self, type_): - return "INTERVAL DAY%s TO SECOND%s" % ( - type_.day_precision is not None and - "(%d)" % type_.day_precision or - "", - type_.second_precision is not None and - "(%d)" % type_.second_precision or - "", - ) - - def visit_LONG(self, type_): - return "LONG" - - def visit_TIMESTAMP(self, type_): - if type_.timezone: - return "TIMESTAMP WITH TIME ZONE" - else: - return "TIMESTAMP" - - def visit_DOUBLE_PRECISION(self, type_): - return self._generate_numeric(type_, "DOUBLE PRECISION") - - def visit_NUMBER(self, type_, **kw): - return self._generate_numeric(type_, "NUMBER", **kw) - - def _generate_numeric(self, type_, name, precision=None, scale=None): - if precision is None: - precision = type_.precision - - if scale is None: - scale = getattr(type_, 'scale', None) - - if precision is None: - return name - elif scale is None: - n = "%(name)s(%(precision)s)" - return n % {'name': name, 'precision': precision} - else: - n = "%(name)s(%(precision)s, %(scale)s)" - return n % {'name': name, 'precision': precision, 'scale': scale} - - def visit_string(self, type_): - return self.visit_VARCHAR2(type_) - - def visit_VARCHAR2(self, type_): - return self._visit_varchar(type_, '', '2') - - def visit_NVARCHAR2(self, type_): - return self._visit_varchar(type_, 'N', '2') - visit_NVARCHAR = visit_NVARCHAR2 - - def visit_VARCHAR(self, type_): - return self._visit_varchar(type_, '', '') - - def _visit_varchar(self, type_, n, num): - if not type_.length: - return "%(n)sVARCHAR%(two)s" % {'two': num, 'n': n} - elif not n and self.dialect._supports_char_length: - varchar = "VARCHAR%(two)s(%(length)s CHAR)" - return varchar % {'length': type_.length, 'two': num} - else: - varchar = "%(n)sVARCHAR%(two)s(%(length)s)" - return varchar % {'length': type_.length, 'two': num, 'n': n} - - def visit_text(self, type_): - return self.visit_CLOB(type_) - - def visit_unicode_text(self, type_): - if self.dialect._supports_nchar: - return self.visit_NCLOB(type_) - else: - return self.visit_CLOB(type_) - - def visit_large_binary(self, type_): - return self.visit_BLOB(type_) - - def visit_big_integer(self, type_): - return self.visit_NUMBER(type_, precision=19) - - def visit_boolean(self, type_): - return self.visit_SMALLINT(type_) - - def visit_RAW(self, type_): - if type_.length: - return "RAW(%(length)s)" % {'length': type_.length} - else: - return "RAW" - - def visit_ROWID(self, type_): - return "ROWID" - - -class OracleCompiler(compiler.SQLCompiler): - """Oracle compiler modifies the lexical structure of Select - statements to work under non-ANSI configured Oracle databases, if - the use_ansi flag is False. - """ - - compound_keywords = util.update_copy( - compiler.SQLCompiler.compound_keywords, - { - expression.CompoundSelect.EXCEPT: 'MINUS' - } - ) - - def __init__(self, *args, **kwargs): - self.__wheres = {} - self._quoted_bind_names = {} - super(OracleCompiler, self).__init__(*args, **kwargs) - - def visit_mod_binary(self, binary, operator, **kw): - return "mod(%s, %s)" % (self.process(binary.left, **kw), - self.process(binary.right, **kw)) - - def visit_now_func(self, fn, **kw): - return "CURRENT_TIMESTAMP" - - def visit_char_length_func(self, fn, **kw): - return "LENGTH" + self.function_argspec(fn, **kw) - - def visit_match_op_binary(self, binary, operator, **kw): - return "CONTAINS (%s, %s)" % (self.process(binary.left), - self.process(binary.right)) - - def visit_true(self, expr, **kw): - return '1' - - def visit_false(self, expr, **kw): - return '0' - - def get_select_hint_text(self, byfroms): - return " ".join( - "/*+ %s */" % text for table, text in byfroms.items() - ) - - def function_argspec(self, fn, **kw): - if len(fn.clauses) > 0 or fn.name.upper() not in NO_ARG_FNS: - return compiler.SQLCompiler.function_argspec(self, fn, **kw) - else: - return "" - - def default_from(self): - """Called when a ``SELECT`` statement has no froms, - and no ``FROM`` clause is to be appended. - - The Oracle compiler tacks a "FROM DUAL" to the statement. - """ - - return " FROM DUAL" - - def visit_join(self, join, **kwargs): - if self.dialect.use_ansi: - return compiler.SQLCompiler.visit_join(self, join, **kwargs) - else: - kwargs['asfrom'] = True - if isinstance(join.right, expression.FromGrouping): - right = join.right.element - else: - right = join.right - return self.process(join.left, **kwargs) + \ - ", " + self.process(right, **kwargs) - - - def _get_nonansi_join_whereclause(self, froms): - clauses = [] - - def visit_join(join): - if join.isouter: - def visit_binary(binary): - if binary.operator == sql_operators.eq: - if join.right.is_derived_from(binary.left.table): - binary.left = _OuterJoinColumn(binary.left) - elif join.right.is_derived_from(binary.right.table): - binary.right = _OuterJoinColumn(binary.right) - clauses.append(visitors.cloned_traverse(join.onclause, {}, - {'binary': visit_binary})) - else: - clauses.append(join.onclause) - - for j in join.left, join.right: - if isinstance(j, expression.Join): - visit_join(j) - elif isinstance(j, expression.FromGrouping): - visit_join(j.element) - - for f in froms: - if isinstance(f, expression.Join): - visit_join(f) - - if not clauses: - return None - else: - return sql.and_(*clauses) - - def visit_outer_join_column(self, vc): - return self.process(vc.column) + "(+)" - - def visit_sequence(self, seq): - return self.dialect.identifier_preparer.format_sequence(seq) + ".nextval" - - def visit_alias(self, alias, asfrom=False, ashint=False, **kwargs): - """Oracle doesn't like ``FROM table AS alias``. Is the AS standard SQL??""" - - if asfrom or ashint: - alias_name = isinstance(alias.name, expression._truncated_label) and \ - self._truncated_identifier("alias", alias.name) or alias.name - - if ashint: - return alias_name - elif asfrom: - return self.process(alias.original, asfrom=asfrom, **kwargs) + \ - " " + self.preparer.format_alias(alias, alias_name) - else: - return self.process(alias.original, **kwargs) - - def returning_clause(self, stmt, returning_cols): - columns = [] - binds = [] - for i, column in enumerate(expression._select_iterables(returning_cols)): - if column.type._has_column_expression: - col_expr = column.type.column_expression(column) - else: - col_expr = column - outparam = sql.outparam("ret_%d" % i, type_=column.type) - self.binds[outparam.key] = outparam - binds.append(self.bindparam_string(self._truncate_bindparam(outparam))) - columns.append(self.process(col_expr, within_columns_clause=False)) - self.result_map[outparam.key] = ( - outparam.key, - (column, getattr(column, 'name', None), - getattr(column, 'key', None)), - column.type - ) - - return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) - - def _TODO_visit_compound_select(self, select): - """Need to determine how to get ``LIMIT``/``OFFSET`` into a ``UNION`` for Oracle.""" - pass - - def visit_select(self, select, **kwargs): - """Look for ``LIMIT`` and OFFSET in a select statement, and if - so tries to wrap it in a subquery with ``rownum`` criterion. - """ - - if not getattr(select, '_oracle_visit', None): - if not self.dialect.use_ansi: - froms = self._display_froms_for_select( - select, kwargs.get('asfrom', False)) - whereclause = self._get_nonansi_join_whereclause(froms) - if whereclause is not None: - select = select.where(whereclause) - select._oracle_visit = True - - if select._limit is not None or select._offset is not None: - # See http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html - # - # Generalized form of an Oracle pagination query: - # select ... from ( - # select /*+ FIRST_ROWS(N) */ ...., rownum as ora_rn from ( - # select distinct ... where ... order by ... - # ) where ROWNUM <= :limit+:offset - # ) where ora_rn > :offset - # Outer select and "ROWNUM as ora_rn" can be dropped if limit=0 - - # TODO: use annotations instead of clone + attr set ? - select = select._generate() - select._oracle_visit = True - - # Wrap the middle select and add the hint - limitselect = sql.select([c for c in select.c]) - if select._limit and self.dialect.optimize_limits: - limitselect = limitselect.prefix_with("/*+ FIRST_ROWS(%d) */" % select._limit) - - limitselect._oracle_visit = True - limitselect._is_wrapper = True - - # If needed, add the limiting clause - if select._limit is not None: - max_row = select._limit - if select._offset is not None: - max_row += select._offset - if not self.dialect.use_binds_for_limits: - max_row = sql.literal_column("%d" % max_row) - limitselect.append_whereclause( - sql.literal_column("ROWNUM") <= max_row) - - # If needed, add the ora_rn, and wrap again with offset. - if select._offset is None: - limitselect._for_update_arg = select._for_update_arg - select = limitselect - else: - limitselect = limitselect.column( - sql.literal_column("ROWNUM").label("ora_rn")) - limitselect._oracle_visit = True - limitselect._is_wrapper = True - - offsetselect = sql.select( - [c for c in limitselect.c if c.key != 'ora_rn']) - offsetselect._oracle_visit = True - offsetselect._is_wrapper = True - - offset_value = select._offset - if not self.dialect.use_binds_for_limits: - offset_value = sql.literal_column("%d" % offset_value) - offsetselect.append_whereclause( - sql.literal_column("ora_rn") > offset_value) - - offsetselect._for_update_arg = select._for_update_arg - select = offsetselect - - kwargs['iswrapper'] = getattr(select, '_is_wrapper', False) - return compiler.SQLCompiler.visit_select(self, select, **kwargs) - - def limit_clause(self, select): - return "" - - def for_update_clause(self, select): - if self.is_subquery(): - return "" - - tmp = ' FOR UPDATE' - - if select._for_update_arg.of: - tmp += ' OF ' + ', '.join( - self.process(elem) for elem in - select._for_update_arg.of - ) - - if select._for_update_arg.nowait: - tmp += " NOWAIT" - - return tmp - - -class OracleDDLCompiler(compiler.DDLCompiler): - - def define_constraint_cascades(self, constraint): - text = "" - if constraint.ondelete is not None: - text += " ON DELETE %s" % constraint.ondelete - - # oracle has no ON UPDATE CASCADE - - # its only available via triggers http://asktom.oracle.com/tkyte/update_cascade/index.html - if constraint.onupdate is not None: - util.warn( - "Oracle does not contain native UPDATE CASCADE " - "functionality - onupdates will not be rendered for foreign keys. " - "Consider using deferrable=True, initially='deferred' or triggers.") - - return text - - def visit_create_index(self, create, **kw): - return super(OracleDDLCompiler, self).\ - visit_create_index(create, include_schema=True) - - -class OracleIdentifierPreparer(compiler.IdentifierPreparer): - - reserved_words = set([x.lower() for x in RESERVED_WORDS]) - illegal_initial_characters = set(range(0, 10)).union(["_", "$"]) - - def _bindparam_requires_quotes(self, value): - """Return True if the given identifier requires quoting.""" - lc_value = value.lower() - return (lc_value in self.reserved_words - or value[0] in self.illegal_initial_characters - or not self.legal_characters.match(util.text_type(value)) - ) - - def format_savepoint(self, savepoint): - name = re.sub(r'^_+', '', savepoint.ident) - return super(OracleIdentifierPreparer, self).format_savepoint(savepoint, name) - - -class OracleExecutionContext(default.DefaultExecutionContext): - def fire_sequence(self, seq, type_): - return self._execute_scalar("SELECT " + - self.dialect.identifier_preparer.format_sequence(seq) + - ".nextval FROM DUAL", type_) - - -class OracleDialect(default.DefaultDialect): - name = 'oracle' - supports_alter = True - supports_unicode_statements = False - supports_unicode_binds = False - max_identifier_length = 30 - supports_sane_rowcount = True - supports_sane_multi_rowcount = False - - supports_sequences = True - sequences_optional = False - postfetch_lastrowid = False - - default_paramstyle = 'named' - colspecs = colspecs - ischema_names = ischema_names - requires_name_normalize = True - - supports_default_values = False - supports_empty_insert = False - - statement_compiler = OracleCompiler - ddl_compiler = OracleDDLCompiler - type_compiler = OracleTypeCompiler - preparer = OracleIdentifierPreparer - execution_ctx_cls = OracleExecutionContext - - reflection_options = ('oracle_resolve_synonyms', ) - - def __init__(self, - use_ansi=True, - optimize_limits=False, - use_binds_for_limits=True, - **kwargs): - default.DefaultDialect.__init__(self, **kwargs) - self.use_ansi = use_ansi - self.optimize_limits = optimize_limits - self.use_binds_for_limits = use_binds_for_limits - - def initialize(self, connection): - super(OracleDialect, self).initialize(connection) - self.implicit_returning = self.__dict__.get( - 'implicit_returning', - self.server_version_info > (10, ) - ) - - if self._is_oracle_8: - self.colspecs = self.colspecs.copy() - self.colspecs.pop(sqltypes.Interval) - self.use_ansi = False - - @property - def _is_oracle_8(self): - return self.server_version_info and \ - self.server_version_info < (9, ) - - @property - def _supports_char_length(self): - return not self._is_oracle_8 - - @property - def _supports_nchar(self): - return not self._is_oracle_8 - - def do_release_savepoint(self, connection, name): - # Oracle does not support RELEASE SAVEPOINT - pass - - def has_table(self, connection, table_name, schema=None): - if not schema: - schema = self.default_schema_name - cursor = connection.execute( - sql.text("SELECT table_name FROM all_tables " - "WHERE table_name = :name AND owner = :schema_name"), - name=self.denormalize_name(table_name), schema_name=self.denormalize_name(schema)) - return cursor.first() is not None - - def has_sequence(self, connection, sequence_name, schema=None): - if not schema: - schema = self.default_schema_name - cursor = connection.execute( - sql.text("SELECT sequence_name FROM all_sequences " - "WHERE sequence_name = :name AND sequence_owner = :schema_name"), - name=self.denormalize_name(sequence_name), schema_name=self.denormalize_name(schema)) - return cursor.first() is not None - - def normalize_name(self, name): - if name is None: - return None - if util.py2k: - if isinstance(name, str): - name = name.decode(self.encoding) - if name.upper() == name and \ - not self.identifier_preparer._requires_quotes(name.lower()): - return name.lower() - else: - return name - - def denormalize_name(self, name): - if name is None: - return None - elif name.lower() == name and not self.identifier_preparer._requires_quotes(name.lower()): - name = name.upper() - if util.py2k: - if not self.supports_unicode_binds: - name = name.encode(self.encoding) - else: - name = unicode(name) - return name - - def _get_default_schema_name(self, connection): - return self.normalize_name(connection.execute('SELECT USER FROM DUAL').scalar()) - - def _resolve_synonym(self, connection, desired_owner=None, desired_synonym=None, desired_table=None): - """search for a local synonym matching the given desired owner/name. - - if desired_owner is None, attempts to locate a distinct owner. - - returns the actual name, owner, dblink name, and synonym name if found. - """ - - q = "SELECT owner, table_owner, table_name, db_link, "\ - "synonym_name FROM all_synonyms WHERE " - clauses = [] - params = {} - if desired_synonym: - clauses.append("synonym_name = :synonym_name") - params['synonym_name'] = desired_synonym - if desired_owner: - clauses.append("owner = :desired_owner") - params['desired_owner'] = desired_owner - if desired_table: - clauses.append("table_name = :tname") - params['tname'] = desired_table - - q += " AND ".join(clauses) - - result = connection.execute(sql.text(q), **params) - if desired_owner: - row = result.first() - if row: - return row['table_name'], row['table_owner'], row['db_link'], row['synonym_name'] - else: - return None, None, None, None - else: - rows = result.fetchall() - if len(rows) > 1: - raise AssertionError("There are multiple tables visible to the schema, you must specify owner") - elif len(rows) == 1: - row = rows[0] - return row['table_name'], row['table_owner'], row['db_link'], row['synonym_name'] - else: - return None, None, None, None - - @reflection.cache - def _prepare_reflection_args(self, connection, table_name, schema=None, - resolve_synonyms=False, dblink='', **kw): - - if resolve_synonyms: - actual_name, owner, dblink, synonym = self._resolve_synonym( - connection, - desired_owner=self.denormalize_name(schema), - desired_synonym=self.denormalize_name(table_name) - ) - else: - actual_name, owner, dblink, synonym = None, None, None, None - if not actual_name: - actual_name = self.denormalize_name(table_name) - - if dblink: - # using user_db_links here since all_db_links appears - # to have more restricted permissions. - # http://docs.oracle.com/cd/B28359_01/server.111/b28310/ds_admin005.htm - # will need to hear from more users if we are doing - # the right thing here. See [ticket:2619] - owner = connection.scalar( - sql.text("SELECT username FROM user_db_links " - "WHERE db_link=:link"), link=dblink) - dblink = "@" + dblink - elif not owner: - owner = self.denormalize_name(schema or self.default_schema_name) - - return (actual_name, owner, dblink or '', synonym) - - @reflection.cache - def get_schema_names(self, connection, **kw): - s = "SELECT username FROM all_users ORDER BY username" - cursor = connection.execute(s,) - return [self.normalize_name(row[0]) for row in cursor] - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - schema = self.denormalize_name(schema or self.default_schema_name) - - # note that table_names() isnt loading DBLINKed or synonym'ed tables - if schema is None: - schema = self.default_schema_name - s = sql.text( - "SELECT table_name FROM all_tables " - "WHERE nvl(tablespace_name, 'no tablespace') NOT IN ('SYSTEM', 'SYSAUX') " - "AND OWNER = :owner " - "AND IOT_NAME IS NULL") - cursor = connection.execute(s, owner=schema) - return [self.normalize_name(row[0]) for row in cursor] - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - schema = self.denormalize_name(schema or self.default_schema_name) - s = sql.text("SELECT view_name FROM all_views WHERE owner = :owner") - cursor = connection.execute(s, owner=self.denormalize_name(schema)) - return [self.normalize_name(row[0]) for row in cursor] - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - """ - - kw arguments can be: - - oracle_resolve_synonyms - - dblink - - """ - - resolve_synonyms = kw.get('oracle_resolve_synonyms', False) - dblink = kw.get('dblink', '') - info_cache = kw.get('info_cache') - - (table_name, schema, dblink, synonym) = \ - self._prepare_reflection_args(connection, table_name, schema, - resolve_synonyms, dblink, - info_cache=info_cache) - columns = [] - if self._supports_char_length: - char_length_col = 'char_length' - else: - char_length_col = 'data_length' - - params = {"table_name": table_name} - text = "SELECT column_name, data_type, %(char_length_col)s, "\ - "data_precision, data_scale, "\ - "nullable, data_default FROM ALL_TAB_COLUMNS%(dblink)s "\ - "WHERE table_name = :table_name" - if schema is not None: - params['owner'] = schema - text += " AND owner = :owner " - text += " ORDER BY column_id" - text = text % {'dblink': dblink, 'char_length_col': char_length_col} - - c = connection.execute(sql.text(text), **params) - - for row in c: - (colname, orig_colname, coltype, length, precision, scale, nullable, default) = \ - (self.normalize_name(row[0]), row[0], row[1], row[2], row[3], row[4], row[5] == 'Y', row[6]) - - if coltype == 'NUMBER': - coltype = NUMBER(precision, scale) - elif coltype in ('VARCHAR2', 'NVARCHAR2', 'CHAR'): - coltype = self.ischema_names.get(coltype)(length) - elif 'WITH TIME ZONE' in coltype: - coltype = TIMESTAMP(timezone=True) - else: - coltype = re.sub(r'\(\d+\)', '', coltype) - try: - coltype = self.ischema_names[coltype] - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (coltype, colname)) - coltype = sqltypes.NULLTYPE - - cdict = { - 'name': colname, - 'type': coltype, - 'nullable': nullable, - 'default': default, - 'autoincrement': default is None - } - if orig_colname.lower() == orig_colname: - cdict['quote'] = True - - columns.append(cdict) - return columns - - @reflection.cache - def get_indexes(self, connection, table_name, schema=None, - resolve_synonyms=False, dblink='', **kw): - - info_cache = kw.get('info_cache') - (table_name, schema, dblink, synonym) = \ - self._prepare_reflection_args(connection, table_name, schema, - resolve_synonyms, dblink, - info_cache=info_cache) - indexes = [] - - params = {'table_name': table_name} - text = \ - "SELECT a.index_name, a.column_name, b.uniqueness "\ - "\nFROM ALL_IND_COLUMNS%(dblink)s a, "\ - "\nALL_INDEXES%(dblink)s b "\ - "\nWHERE "\ - "\na.index_name = b.index_name "\ - "\nAND a.table_owner = b.table_owner "\ - "\nAND a.table_name = b.table_name "\ - "\nAND a.table_name = :table_name " - - if schema is not None: - params['schema'] = schema - text += "AND a.table_owner = :schema " - - text += "ORDER BY a.index_name, a.column_position" - - text = text % {'dblink': dblink} - - q = sql.text(text) - rp = connection.execute(q, **params) - indexes = [] - last_index_name = None - pk_constraint = self.get_pk_constraint( - connection, table_name, schema, resolve_synonyms=resolve_synonyms, - dblink=dblink, info_cache=kw.get('info_cache')) - pkeys = pk_constraint['constrained_columns'] - uniqueness = dict(NONUNIQUE=False, UNIQUE=True) - - oracle_sys_col = re.compile(r'SYS_NC\d+\$', re.IGNORECASE) - - def upper_name_set(names): - return set([i.upper() for i in names]) - - pk_names = upper_name_set(pkeys) - - def remove_if_primary_key(index): - # don't include the primary key index - if index is not None and \ - upper_name_set(index['column_names']) == pk_names: - indexes.pop() - - index = None - for rset in rp: - if rset.index_name != last_index_name: - remove_if_primary_key(index) - index = dict(name=self.normalize_name(rset.index_name), column_names=[]) - indexes.append(index) - index['unique'] = uniqueness.get(rset.uniqueness, False) - - # filter out Oracle SYS_NC names. could also do an outer join - # to the all_tab_columns table and check for real col names there. - if not oracle_sys_col.match(rset.column_name): - index['column_names'].append(self.normalize_name(rset.column_name)) - last_index_name = rset.index_name - remove_if_primary_key(index) - return indexes - - @reflection.cache - def _get_constraint_data(self, connection, table_name, schema=None, - dblink='', **kw): - - params = {'table_name': table_name} - - text = \ - "SELECT"\ - "\nac.constraint_name,"\ - "\nac.constraint_type,"\ - "\nloc.column_name AS local_column,"\ - "\nrem.table_name AS remote_table,"\ - "\nrem.column_name AS remote_column,"\ - "\nrem.owner AS remote_owner,"\ - "\nloc.position as loc_pos,"\ - "\nrem.position as rem_pos"\ - "\nFROM all_constraints%(dblink)s ac,"\ - "\nall_cons_columns%(dblink)s loc,"\ - "\nall_cons_columns%(dblink)s rem"\ - "\nWHERE ac.table_name = :table_name"\ - "\nAND ac.constraint_type IN ('R','P')" - - if schema is not None: - params['owner'] = schema - text += "\nAND ac.owner = :owner" - - text += \ - "\nAND ac.owner = loc.owner"\ - "\nAND ac.constraint_name = loc.constraint_name"\ - "\nAND ac.r_owner = rem.owner(+)"\ - "\nAND ac.r_constraint_name = rem.constraint_name(+)"\ - "\nAND (rem.position IS NULL or loc.position=rem.position)"\ - "\nORDER BY ac.constraint_name, loc.position" - - text = text % {'dblink': dblink} - rp = connection.execute(sql.text(text), **params) - constraint_data = rp.fetchall() - return constraint_data - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - resolve_synonyms = kw.get('oracle_resolve_synonyms', False) - dblink = kw.get('dblink', '') - info_cache = kw.get('info_cache') - - (table_name, schema, dblink, synonym) = \ - self._prepare_reflection_args(connection, table_name, schema, - resolve_synonyms, dblink, - info_cache=info_cache) - pkeys = [] - constraint_name = None - constraint_data = self._get_constraint_data(connection, table_name, - schema, dblink, - info_cache=kw.get('info_cache')) - - for row in constraint_data: - (cons_name, cons_type, local_column, remote_table, remote_column, remote_owner) = \ - row[0:2] + tuple([self.normalize_name(x) for x in row[2:6]]) - if cons_type == 'P': - if constraint_name is None: - constraint_name = self.normalize_name(cons_name) - pkeys.append(local_column) - return {'constrained_columns': pkeys, 'name': constraint_name} - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - """ - - kw arguments can be: - - oracle_resolve_synonyms - - dblink - - """ - - requested_schema = schema # to check later on - resolve_synonyms = kw.get('oracle_resolve_synonyms', False) - dblink = kw.get('dblink', '') - info_cache = kw.get('info_cache') - - (table_name, schema, dblink, synonym) = \ - self._prepare_reflection_args(connection, table_name, schema, - resolve_synonyms, dblink, - info_cache=info_cache) - - constraint_data = self._get_constraint_data(connection, table_name, - schema, dblink, - info_cache=kw.get('info_cache')) - - def fkey_rec(): - return { - 'name': None, - 'constrained_columns': [], - 'referred_schema': None, - 'referred_table': None, - 'referred_columns': [] - } - - fkeys = util.defaultdict(fkey_rec) - - for row in constraint_data: - (cons_name, cons_type, local_column, remote_table, remote_column, remote_owner) = \ - row[0:2] + tuple([self.normalize_name(x) for x in row[2:6]]) - - if cons_type == 'R': - if remote_table is None: - # ticket 363 - util.warn( - ("Got 'None' querying 'table_name' from " - "all_cons_columns%(dblink)s - does the user have " - "proper rights to the table?") % {'dblink': dblink}) - continue - - rec = fkeys[cons_name] - rec['name'] = cons_name - local_cols, remote_cols = rec['constrained_columns'], rec['referred_columns'] - - if not rec['referred_table']: - if resolve_synonyms: - ref_remote_name, ref_remote_owner, ref_dblink, ref_synonym = \ - self._resolve_synonym( - connection, - desired_owner=self.denormalize_name(remote_owner), - desired_table=self.denormalize_name(remote_table) - ) - if ref_synonym: - remote_table = self.normalize_name(ref_synonym) - remote_owner = self.normalize_name(ref_remote_owner) - - rec['referred_table'] = remote_table - - if requested_schema is not None or self.denormalize_name(remote_owner) != schema: - rec['referred_schema'] = remote_owner - - local_cols.append(local_column) - remote_cols.append(remote_column) - - return list(fkeys.values()) - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, - resolve_synonyms=False, dblink='', **kw): - info_cache = kw.get('info_cache') - (view_name, schema, dblink, synonym) = \ - self._prepare_reflection_args(connection, view_name, schema, - resolve_synonyms, dblink, - info_cache=info_cache) - - params = {'view_name': view_name} - text = "SELECT text FROM all_views WHERE view_name=:view_name" - - if schema is not None: - text += " AND owner = :schema" - params['schema'] = schema - - rp = connection.execute(sql.text(text), **params).scalar() - if rp: - if util.py2k: - rp = rp.decode(self.encoding) - return rp - else: - return None - - -class _OuterJoinColumn(sql.ClauseElement): - __visit_name__ = 'outer_join_column' - - def __init__(self, column): - self.column = column diff --git a/libs/sqlalchemy/dialects/oracle/cx_oracle.py b/libs/sqlalchemy/dialects/oracle/cx_oracle.py deleted file mode 100644 index c427e4bc..00000000 --- a/libs/sqlalchemy/dialects/oracle/cx_oracle.py +++ /dev/null @@ -1,869 +0,0 @@ -# oracle/cx_oracle.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: oracle+cx_oracle - :name: cx-Oracle - :dbapi: cx_oracle - :connectstring: oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...] - :url: http://cx-oracle.sourceforge.net/ - -Additional Connect Arguments ----------------------------- - -When connecting with ``dbname`` present, the host, port, and dbname tokens are -converted to a TNS name using -the cx_oracle ``makedsn()`` function. Otherwise, the host token is taken -directly as a TNS name. - -Additional arguments which may be specified either as query string arguments -on the URL, or as keyword arguments to :func:`.create_engine()` are: - -* allow_twophase - enable two-phase transactions. Defaults to ``True``. - -* arraysize - set the cx_oracle.arraysize value on cursors, in SQLAlchemy - it defaults to 50. See the section on "LOB Objects" below. - -* auto_convert_lobs - defaults to True, see the section on LOB objects. - -* auto_setinputsizes - the cx_oracle.setinputsizes() call is issued for - all bind parameters. This is required for LOB datatypes but can be - disabled to reduce overhead. Defaults to ``True``. Specific types - can be excluded from this process using the ``exclude_setinputsizes`` - parameter. - -* exclude_setinputsizes - a tuple or list of string DBAPI type names to - be excluded from the "auto setinputsizes" feature. The type names here - must match DBAPI types that are found in the "cx_Oracle" module namespace, - such as cx_Oracle.UNICODE, cx_Oracle.NCLOB, etc. Defaults to - ``(STRING, UNICODE)``. - - .. versionadded:: 0.8 specific DBAPI types can be excluded from the - auto_setinputsizes feature via the exclude_setinputsizes attribute. - -* mode - This is given the string value of SYSDBA or SYSOPER, or alternatively - an integer value. This value is only available as a URL query string - argument. - -* threaded - enable multithreaded access to cx_oracle connections. Defaults - to ``True``. Note that this is the opposite default of the cx_Oracle DBAPI - itself. - -Unicode -------- - -cx_oracle 5 fully supports Python unicode objects. SQLAlchemy will pass -all unicode strings directly to cx_oracle, and additionally uses an output -handler so that all string based result values are returned as unicode as well. -Generally, the ``NLS_LANG`` environment variable determines the nature -of the encoding to be used. - -Note that this behavior is disabled when Oracle 8 is detected, as it has been -observed that issues remain when passing Python unicodes to cx_oracle with Oracle 8. - -.. _cx_oracle_returning: - -RETURNING Support ------------------ - -cx_oracle supports a limited subset of Oracle's already limited RETURNING support. -Typically, results can only be guaranteed for at most one column being returned; -this is the typical case when SQLAlchemy uses RETURNING to get just the value of a -primary-key-associated sequence value. Additional column expressions will -cause problems in a non-determinative way, due to cx_oracle's lack of support for -the OCI_DATA_AT_EXEC API which is required for more complex RETURNING scenarios. - -.. seealso:: - - http://docs.oracle.com/cd/B10501_01/appdev.920/a96584/oci05bnd.htm#420693 - OCI documentation for RETURNING - - http://sourceforge.net/mailarchive/message.php?msg_id=31338136 - cx_oracle developer commentary - - - - -LOB Objects ------------ - -cx_oracle returns oracle LOBs using the cx_oracle.LOB object. SQLAlchemy converts -these to strings so that the interface of the Binary type is consistent with that of -other backends, and so that the linkage to a live cursor is not needed in scenarios -like result.fetchmany() and result.fetchall(). This means that by default, LOB -objects are fully fetched unconditionally by SQLAlchemy, and the linkage to a live -cursor is broken. - -To disable this processing, pass ``auto_convert_lobs=False`` to :func:`.create_engine()`. - -Two Phase Transaction Support ------------------------------ - -Two Phase transactions are implemented using XA transactions, and are known -to work in a rudimental fashion with recent versions of cx_Oracle -as of SQLAlchemy 0.8.0b2, 0.7.10. However, the mechanism is not yet -considered to be robust and should still be regarded as experimental. - -In particular, the cx_Oracle DBAPI as recently as 5.1.2 has a bug regarding -two phase which prevents -a particular DBAPI connection from being consistently usable in both -prepared transactions as well as traditional DBAPI usage patterns; therefore -once a particular connection is used via :meth:`.Connection.begin_prepared`, -all subsequent usages of the underlying DBAPI connection must be within -the context of prepared transactions. - -The default behavior of :class:`.Engine` is to maintain a pool of DBAPI -connections. Therefore, due to the above glitch, a DBAPI connection that has -been used in a two-phase operation, and is then returned to the pool, will -not be usable in a non-two-phase context. To avoid this situation, -the application can make one of several choices: - -* Disable connection pooling using :class:`.NullPool` - -* Ensure that the particular :class:`.Engine` in use is only used - for two-phase operations. A :class:`.Engine` bound to an ORM - :class:`.Session` which includes ``twophase=True`` will consistently - use the two-phase transaction style. - -* For ad-hoc two-phase operations without disabling pooling, the DBAPI - connection in use can be evicted from the connection pool using the - :meth:`.Connection.detach` method. - -.. versionchanged:: 0.8.0b2,0.7.10 - Support for cx_oracle prepared transactions has been implemented - and tested. - - -Precision Numerics ------------------- - -The SQLAlchemy dialect goes through a lot of steps to ensure -that decimal numbers are sent and received with full accuracy. -An "outputtypehandler" callable is associated with each -cx_oracle connection object which detects numeric types and -receives them as string values, instead of receiving a Python -``float`` directly, which is then passed to the Python -``Decimal`` constructor. The :class:`.Numeric` and -:class:`.Float` types under the cx_oracle dialect are aware of -this behavior, and will coerce the ``Decimal`` to ``float`` if -the ``asdecimal`` flag is ``False`` (default on :class:`.Float`, -optional on :class:`.Numeric`). - -Because the handler coerces to ``Decimal`` in all cases first, -the feature can detract significantly from performance. -If precision numerics aren't required, the decimal handling -can be disabled by passing the flag ``coerce_to_decimal=False`` -to :func:`.create_engine`:: - - engine = create_engine("oracle+cx_oracle://dsn", - coerce_to_decimal=False) - -.. versionadded:: 0.7.6 - Add the ``coerce_to_decimal`` flag. - -Another alternative to performance is to use the -`cdecimal `_ library; -see :class:`.Numeric` for additional notes. - -The handler attempts to use the "precision" and "scale" -attributes of the result set column to best determine if -subsequent incoming values should be received as ``Decimal`` as -opposed to int (in which case no processing is added). There are -several scenarios where OCI_ does not provide unambiguous data -as to the numeric type, including some situations where -individual rows may return a combination of floating point and -integer values. Certain values for "precision" and "scale" have -been observed to determine this scenario. When it occurs, the -outputtypehandler receives as string and then passes off to a -processing function which detects, for each returned value, if a -decimal point is present, and if so converts to ``Decimal``, -otherwise to int. The intention is that simple int-based -statements like "SELECT my_seq.nextval() FROM DUAL" continue to -return ints and not ``Decimal`` objects, and that any kind of -floating point value is received as a string so that there is no -floating point loss of precision. - -The "decimal point is present" logic itself is also sensitive to -locale. Under OCI_, this is controlled by the NLS_LANG -environment variable. Upon first connection, the dialect runs a -test to determine the current "decimal" character, which can be -a comma "," for european locales. From that point forward the -outputtypehandler uses that character to represent a decimal -point. Note that cx_oracle 5.0.3 or greater is required -when dealing with numerics with locale settings that don't use -a period "." as the decimal character. - -.. versionchanged:: 0.6.6 - The outputtypehandler uses a comma "," character to represent - a decimal point. - -.. _OCI: http://www.oracle.com/technetwork/database/features/oci/index.html - -""" - -from __future__ import absolute_import - -from .base import OracleCompiler, OracleDialect, OracleExecutionContext -from . import base as oracle -from ...engine import result as _result -from sqlalchemy import types as sqltypes, util, exc, processors -import random -import collections -import decimal -import re - - -class _OracleNumeric(sqltypes.Numeric): - def bind_processor(self, dialect): - # cx_oracle accepts Decimal objects and floats - return None - - def result_processor(self, dialect, coltype): - # we apply a cx_oracle type handler to all connections - # that converts floating point strings to Decimal(). - # However, in some subquery situations, Oracle doesn't - # give us enough information to determine int or Decimal. - # It could even be int/Decimal differently on each row, - # regardless of the scale given for the originating type. - # So we still need an old school isinstance() handler - # here for decimals. - - if dialect.supports_native_decimal: - if self.asdecimal: - fstring = "%%.%df" % self._effective_decimal_return_scale - - def to_decimal(value): - if value is None: - return None - elif isinstance(value, decimal.Decimal): - return value - else: - return decimal.Decimal(fstring % value) - - return to_decimal - else: - if self.precision is None and self.scale is None: - return processors.to_float - elif not getattr(self, '_is_oracle_number', False) \ - and self.scale is not None: - return processors.to_float - else: - return None - else: - # cx_oracle 4 behavior, will assume - # floats - return super(_OracleNumeric, self).\ - result_processor(dialect, coltype) - - -class _OracleDate(sqltypes.Date): - def bind_processor(self, dialect): - return None - - def result_processor(self, dialect, coltype): - def process(value): - if value is not None: - return value.date() - else: - return value - return process - - -class _LOBMixin(object): - def result_processor(self, dialect, coltype): - if not dialect.auto_convert_lobs: - # return the cx_oracle.LOB directly. - return None - - def process(value): - if value is not None: - return value.read() - else: - return value - return process - - -class _NativeUnicodeMixin(object): - if util.py2k: - def bind_processor(self, dialect): - if dialect._cx_oracle_with_unicode: - def process(value): - if value is None: - return value - else: - return unicode(value) - return process - else: - return super(_NativeUnicodeMixin, self).bind_processor(dialect) - - # we apply a connection output handler that returns - # unicode in all cases, so the "native_unicode" flag - # will be set for the default String.result_processor. - - -class _OracleChar(_NativeUnicodeMixin, sqltypes.CHAR): - def get_dbapi_type(self, dbapi): - return dbapi.FIXED_CHAR - - -class _OracleNVarChar(_NativeUnicodeMixin, sqltypes.NVARCHAR): - def get_dbapi_type(self, dbapi): - return getattr(dbapi, 'UNICODE', dbapi.STRING) - - -class _OracleText(_LOBMixin, sqltypes.Text): - def get_dbapi_type(self, dbapi): - return dbapi.CLOB - - -class _OracleLong(oracle.LONG): - # a raw LONG is a text type, but does *not* - # get the LobMixin with cx_oracle. - - def get_dbapi_type(self, dbapi): - return dbapi.LONG_STRING - -class _OracleString(_NativeUnicodeMixin, sqltypes.String): - pass - - -class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText): - def get_dbapi_type(self, dbapi): - return dbapi.NCLOB - - def result_processor(self, dialect, coltype): - lob_processor = _LOBMixin.result_processor(self, dialect, coltype) - if lob_processor is None: - return None - - string_processor = sqltypes.UnicodeText.result_processor(self, dialect, coltype) - - if string_processor is None: - return lob_processor - else: - def process(value): - return string_processor(lob_processor(value)) - return process - - -class _OracleInteger(sqltypes.Integer): - def result_processor(self, dialect, coltype): - def to_int(val): - if val is not None: - val = int(val) - return val - return to_int - - -class _OracleBinary(_LOBMixin, sqltypes.LargeBinary): - def get_dbapi_type(self, dbapi): - return dbapi.BLOB - - def bind_processor(self, dialect): - return None - - -class _OracleInterval(oracle.INTERVAL): - def get_dbapi_type(self, dbapi): - return dbapi.INTERVAL - - -class _OracleRaw(oracle.RAW): - pass - - -class _OracleRowid(oracle.ROWID): - def get_dbapi_type(self, dbapi): - return dbapi.ROWID - - -class OracleCompiler_cx_oracle(OracleCompiler): - def bindparam_string(self, name, **kw): - quote = getattr(name, 'quote', None) - if quote is True or quote is not False and \ - self.preparer._bindparam_requires_quotes(name): - quoted_name = '"%s"' % name - self._quoted_bind_names[name] = quoted_name - return OracleCompiler.bindparam_string(self, quoted_name, **kw) - else: - return OracleCompiler.bindparam_string(self, name, **kw) - - -class OracleExecutionContext_cx_oracle(OracleExecutionContext): - - def pre_exec(self): - quoted_bind_names = \ - getattr(self.compiled, '_quoted_bind_names', None) - if quoted_bind_names: - if not self.dialect.supports_unicode_statements: - # if DBAPI doesn't accept unicode statements, - # keys in self.parameters would have been encoded - # here. so convert names in quoted_bind_names - # to encoded as well. - quoted_bind_names = \ - dict( - (fromname.encode(self.dialect.encoding), - toname.encode(self.dialect.encoding)) - for fromname, toname in - quoted_bind_names.items() - ) - for param in self.parameters: - for fromname, toname in quoted_bind_names.items(): - param[toname] = param[fromname] - del param[fromname] - - if self.dialect.auto_setinputsizes: - # cx_oracle really has issues when you setinputsizes - # on String, including that outparams/RETURNING - # breaks for varchars - self.set_input_sizes(quoted_bind_names, - exclude_types=self.dialect.exclude_setinputsizes - ) - - # if a single execute, check for outparams - if len(self.compiled_parameters) == 1: - for bindparam in self.compiled.binds.values(): - if bindparam.isoutparam: - dbtype = bindparam.type.dialect_impl(self.dialect).\ - get_dbapi_type(self.dialect.dbapi) - if not hasattr(self, 'out_parameters'): - self.out_parameters = {} - if dbtype is None: - raise exc.InvalidRequestError( - "Cannot create out parameter for parameter " - "%r - it's type %r is not supported by" - " cx_oracle" % - (bindparam.key, bindparam.type) - ) - name = self.compiled.bind_names[bindparam] - self.out_parameters[name] = self.cursor.var(dbtype) - self.parameters[0][quoted_bind_names.get(name, name)] = \ - self.out_parameters[name] - - def create_cursor(self): - c = self._dbapi_connection.cursor() - if self.dialect.arraysize: - c.arraysize = self.dialect.arraysize - - return c - - def get_result_proxy(self): - if hasattr(self, 'out_parameters') and self.compiled.returning: - returning_params = dict( - (k, v.getvalue()) - for k, v in self.out_parameters.items() - ) - return ReturningResultProxy(self, returning_params) - - result = None - if self.cursor.description is not None: - for column in self.cursor.description: - type_code = column[1] - if type_code in self.dialect._cx_oracle_binary_types: - result = _result.BufferedColumnResultProxy(self) - - if result is None: - result = _result.ResultProxy(self) - - if hasattr(self, 'out_parameters'): - if self.compiled_parameters is not None and \ - len(self.compiled_parameters) == 1: - result.out_parameters = out_parameters = {} - - for bind, name in self.compiled.bind_names.items(): - if name in self.out_parameters: - type = bind.type - impl_type = type.dialect_impl(self.dialect) - dbapi_type = impl_type.get_dbapi_type(self.dialect.dbapi) - result_processor = impl_type.\ - result_processor(self.dialect, - dbapi_type) - if result_processor is not None: - out_parameters[name] = \ - result_processor(self.out_parameters[name].getvalue()) - else: - out_parameters[name] = self.out_parameters[name].getvalue() - else: - result.out_parameters = dict( - (k, v.getvalue()) - for k, v in self.out_parameters.items() - ) - - return result - - -class OracleExecutionContext_cx_oracle_with_unicode(OracleExecutionContext_cx_oracle): - """Support WITH_UNICODE in Python 2.xx. - - WITH_UNICODE allows cx_Oracle's Python 3 unicode handling - behavior under Python 2.x. This mode in some cases disallows - and in other cases silently passes corrupted data when - non-Python-unicode strings (a.k.a. plain old Python strings) - are passed as arguments to connect(), the statement sent to execute(), - or any of the bind parameter keys or values sent to execute(). - This optional context therefore ensures that all statements are - passed as Python unicode objects. - - """ - def __init__(self, *arg, **kw): - OracleExecutionContext_cx_oracle.__init__(self, *arg, **kw) - self.statement = util.text_type(self.statement) - - def _execute_scalar(self, stmt): - return super(OracleExecutionContext_cx_oracle_with_unicode, self).\ - _execute_scalar(util.text_type(stmt)) - - -class ReturningResultProxy(_result.FullyBufferedResultProxy): - """Result proxy which stuffs the _returning clause + outparams into the fetch.""" - - def __init__(self, context, returning_params): - self._returning_params = returning_params - super(ReturningResultProxy, self).__init__(context) - - def _cursor_description(self): - returning = self.context.compiled.returning - return [ - ("ret_%d" % i, None) - for i, col in enumerate(returning) - ] - - def _buffer_rows(self): - return collections.deque([tuple(self._returning_params["ret_%d" % i] - for i, c in enumerate(self._returning_params))]) - - -class OracleDialect_cx_oracle(OracleDialect): - execution_ctx_cls = OracleExecutionContext_cx_oracle - statement_compiler = OracleCompiler_cx_oracle - - driver = "cx_oracle" - - colspecs = colspecs = { - sqltypes.Numeric: _OracleNumeric, - sqltypes.Date: _OracleDate, # generic type, assume datetime.date is desired - oracle.DATE: oracle.DATE, # non generic type - passthru - sqltypes.LargeBinary: _OracleBinary, - sqltypes.Boolean: oracle._OracleBoolean, - sqltypes.Interval: _OracleInterval, - oracle.INTERVAL: _OracleInterval, - sqltypes.Text: _OracleText, - sqltypes.String: _OracleString, - sqltypes.UnicodeText: _OracleUnicodeText, - sqltypes.CHAR: _OracleChar, - - # a raw LONG is a text type, but does *not* - # get the LobMixin with cx_oracle. - oracle.LONG: _OracleLong, - - # this is only needed for OUT parameters. - # it would be nice if we could not use it otherwise. - sqltypes.Integer: _OracleInteger, - - oracle.RAW: _OracleRaw, - sqltypes.Unicode: _OracleNVarChar, - sqltypes.NVARCHAR: _OracleNVarChar, - oracle.ROWID: _OracleRowid, - } - - execute_sequence_format = list - - def __init__(self, - auto_setinputsizes=True, - exclude_setinputsizes=("STRING", "UNICODE"), - auto_convert_lobs=True, - threaded=True, - allow_twophase=True, - coerce_to_decimal=True, - arraysize=50, **kwargs): - OracleDialect.__init__(self, **kwargs) - self.threaded = threaded - self.arraysize = arraysize - self.allow_twophase = allow_twophase - self.supports_timestamp = self.dbapi is None or \ - hasattr(self.dbapi, 'TIMESTAMP') - self.auto_setinputsizes = auto_setinputsizes - self.auto_convert_lobs = auto_convert_lobs - - if hasattr(self.dbapi, 'version'): - self.cx_oracle_ver = tuple([int(x) for x in - self.dbapi.version.split('.')]) - else: - self.cx_oracle_ver = (0, 0, 0) - - def types(*names): - return set( - getattr(self.dbapi, name, None) for name in names - ).difference([None]) - - self.exclude_setinputsizes = types(*(exclude_setinputsizes or ())) - self._cx_oracle_string_types = types("STRING", "UNICODE", - "NCLOB", "CLOB") - self._cx_oracle_unicode_types = types("UNICODE", "NCLOB") - self._cx_oracle_binary_types = types("BFILE", "CLOB", "NCLOB", "BLOB") - self.supports_unicode_binds = self.cx_oracle_ver >= (5, 0) - - self.supports_native_decimal = ( - self.cx_oracle_ver >= (5, 0) and - coerce_to_decimal - ) - - self._cx_oracle_native_nvarchar = self.cx_oracle_ver >= (5, 0) - - if self.cx_oracle_ver is None: - # this occurs in tests with mock DBAPIs - self._cx_oracle_string_types = set() - self._cx_oracle_with_unicode = False - elif self.cx_oracle_ver >= (5,) and not hasattr(self.dbapi, 'UNICODE'): - # cx_Oracle WITH_UNICODE mode. *only* python - # unicode objects accepted for anything - self.supports_unicode_statements = True - self.supports_unicode_binds = True - self._cx_oracle_with_unicode = True - - if util.py2k: - # There's really no reason to run with WITH_UNICODE under Python 2.x. - # Give the user a hint. - util.warn( - "cx_Oracle is compiled under Python 2.xx using the " - "WITH_UNICODE flag. Consider recompiling cx_Oracle " - "without this flag, which is in no way necessary for full " - "support of Unicode. Otherwise, all string-holding bind " - "parameters must be explicitly typed using SQLAlchemy's " - "String type or one of its subtypes," - "or otherwise be passed as Python unicode. " - "Plain Python strings passed as bind parameters will be " - "silently corrupted by cx_Oracle." - ) - self.execution_ctx_cls = \ - OracleExecutionContext_cx_oracle_with_unicode - else: - self._cx_oracle_with_unicode = False - - if self.cx_oracle_ver is None or \ - not self.auto_convert_lobs or \ - not hasattr(self.dbapi, 'CLOB'): - self.dbapi_type_map = {} - else: - # only use this for LOB objects. using it for strings, dates - # etc. leads to a little too much magic, reflection doesn't know if it should - # expect encoded strings or unicodes, etc. - self.dbapi_type_map = { - self.dbapi.CLOB: oracle.CLOB(), - self.dbapi.NCLOB: oracle.NCLOB(), - self.dbapi.BLOB: oracle.BLOB(), - self.dbapi.BINARY: oracle.RAW(), - } - - @classmethod - def dbapi(cls): - import cx_Oracle - return cx_Oracle - - def initialize(self, connection): - super(OracleDialect_cx_oracle, self).initialize(connection) - if self._is_oracle_8: - self.supports_unicode_binds = False - self._detect_decimal_char(connection) - - def _detect_decimal_char(self, connection): - """detect if the decimal separator character is not '.', as - is the case with european locale settings for NLS_LANG. - - cx_oracle itself uses similar logic when it formats Python - Decimal objects to strings on the bind side (as of 5.0.3), - as Oracle sends/receives string numerics only in the - current locale. - - """ - if self.cx_oracle_ver < (5,): - # no output type handlers before version 5 - return - - cx_Oracle = self.dbapi - conn = connection.connection - - # override the output_type_handler that's - # on the cx_oracle connection with a plain - # one on the cursor - - def output_type_handler(cursor, name, defaultType, - size, precision, scale): - return cursor.var( - cx_Oracle.STRING, - 255, arraysize=cursor.arraysize) - - cursor = conn.cursor() - cursor.outputtypehandler = output_type_handler - cursor.execute("SELECT 0.1 FROM DUAL") - val = cursor.fetchone()[0] - cursor.close() - char = re.match(r"([\.,])", val).group(1) - if char != '.': - _detect_decimal = self._detect_decimal - self._detect_decimal = \ - lambda value: _detect_decimal(value.replace(char, '.')) - self._to_decimal = \ - lambda value: decimal.Decimal(value.replace(char, '.')) - - def _detect_decimal(self, value): - if "." in value: - return decimal.Decimal(value) - else: - return int(value) - - _to_decimal = decimal.Decimal - - def on_connect(self): - if self.cx_oracle_ver < (5,): - # no output type handlers before version 5 - return - - cx_Oracle = self.dbapi - - def output_type_handler(cursor, name, defaultType, - size, precision, scale): - # convert all NUMBER with precision + positive scale to Decimal - # this almost allows "native decimal" mode. - if self.supports_native_decimal and \ - defaultType == cx_Oracle.NUMBER and \ - precision and scale > 0: - return cursor.var( - cx_Oracle.STRING, - 255, - outconverter=self._to_decimal, - arraysize=cursor.arraysize) - # if NUMBER with zero precision and 0 or neg scale, this appears - # to indicate "ambiguous". Use a slower converter that will - # make a decision based on each value received - the type - # may change from row to row (!). This kills - # off "native decimal" mode, handlers still needed. - elif self.supports_native_decimal and \ - defaultType == cx_Oracle.NUMBER \ - and not precision and scale <= 0: - return cursor.var( - cx_Oracle.STRING, - 255, - outconverter=self._detect_decimal, - arraysize=cursor.arraysize) - # allow all strings to come back natively as Unicode - elif defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR): - return cursor.var(util.text_type, size, cursor.arraysize) - - def on_connect(conn): - conn.outputtypehandler = output_type_handler - - return on_connect - - def create_connect_args(self, url): - dialect_opts = dict(url.query) - for opt in ('use_ansi', 'auto_setinputsizes', 'auto_convert_lobs', - 'threaded', 'allow_twophase'): - if opt in dialect_opts: - util.coerce_kw_type(dialect_opts, opt, bool) - setattr(self, opt, dialect_opts[opt]) - - if url.database: - # if we have a database, then we have a remote host - port = url.port - if port: - port = int(port) - else: - port = 1521 - dsn = self.dbapi.makedsn(url.host, port, url.database) - else: - # we have a local tnsname - dsn = url.host - - opts = dict( - user=url.username, - password=url.password, - dsn=dsn, - threaded=self.threaded, - twophase=self.allow_twophase, - ) - - if util.py2k: - if self._cx_oracle_with_unicode: - for k, v in opts.items(): - if isinstance(v, str): - opts[k] = unicode(v) - else: - for k, v in opts.items(): - if isinstance(v, unicode): - opts[k] = str(v) - - if 'mode' in url.query: - opts['mode'] = url.query['mode'] - if isinstance(opts['mode'], util.string_types): - mode = opts['mode'].upper() - if mode == 'SYSDBA': - opts['mode'] = self.dbapi.SYSDBA - elif mode == 'SYSOPER': - opts['mode'] = self.dbapi.SYSOPER - else: - util.coerce_kw_type(opts, 'mode', int) - return ([], opts) - - def _get_server_version_info(self, connection): - return tuple( - int(x) - for x in connection.connection.version.split('.') - ) - - def is_disconnect(self, e, connection, cursor): - error, = e.args - if isinstance(e, self.dbapi.InterfaceError): - return "not connected" in str(e) - elif hasattr(error, 'code'): - # ORA-00028: your session has been killed - # ORA-03114: not connected to ORACLE - # ORA-03113: end-of-file on communication channel - # ORA-03135: connection lost contact - # ORA-01033: ORACLE initialization or shutdown in progress - # ORA-02396: exceeded maximum idle time, please connect again - # TODO: Others ? - return error.code in (28, 3114, 3113, 3135, 1033, 2396) - else: - return False - - def create_xid(self): - """create a two-phase transaction ID. - - this id will be passed to do_begin_twophase(), do_rollback_twophase(), - do_commit_twophase(). its format is unspecified.""" - - id = random.randint(0, 2 ** 128) - return (0x1234, "%032x" % id, "%032x" % 9) - - def do_executemany(self, cursor, statement, parameters, context=None): - if isinstance(parameters, tuple): - parameters = list(parameters) - cursor.executemany(statement, parameters) - - def do_begin_twophase(self, connection, xid): - connection.connection.begin(*xid) - - def do_prepare_twophase(self, connection, xid): - result = connection.connection.prepare() - connection.info['cx_oracle_prepared'] = result - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - self.do_rollback(connection.connection) - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - self.do_commit(connection.connection) - else: - oci_prepared = connection.info['cx_oracle_prepared'] - if oci_prepared: - self.do_commit(connection.connection) - - def do_recover_twophase(self, connection): - connection.info.pop('cx_oracle_prepared', None) - -dialect = OracleDialect_cx_oracle diff --git a/libs/sqlalchemy/dialects/oracle/zxjdbc.py b/libs/sqlalchemy/dialects/oracle/zxjdbc.py deleted file mode 100644 index 710645b2..00000000 --- a/libs/sqlalchemy/dialects/oracle/zxjdbc.py +++ /dev/null @@ -1,218 +0,0 @@ -# oracle/zxjdbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: oracle+zxjdbc - :name: zxJDBC for Jython - :dbapi: zxjdbc - :connectstring: oracle+zxjdbc://user:pass@host/dbname - :driverurl: http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html. - -""" -import decimal -import re - -from sqlalchemy import sql, types as sqltypes, util -from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector -from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, OracleExecutionContext -from sqlalchemy.engine import result as _result -from sqlalchemy.sql import expression -import collections - -SQLException = zxJDBC = None - - -class _ZxJDBCDate(sqltypes.Date): - - def result_processor(self, dialect, coltype): - def process(value): - if value is None: - return None - else: - return value.date() - return process - - -class _ZxJDBCNumeric(sqltypes.Numeric): - - def result_processor(self, dialect, coltype): - #XXX: does the dialect return Decimal or not??? - # if it does (in all cases), we could use a None processor as well as - # the to_float generic processor - if self.asdecimal: - def process(value): - if isinstance(value, decimal.Decimal): - return value - else: - return decimal.Decimal(str(value)) - else: - def process(value): - if isinstance(value, decimal.Decimal): - return float(value) - else: - return value - return process - - -class OracleCompiler_zxjdbc(OracleCompiler): - - def returning_clause(self, stmt, returning_cols): - self.returning_cols = list(expression._select_iterables(returning_cols)) - - # within_columns_clause=False so that labels (foo AS bar) don't render - columns = [self.process(c, within_columns_clause=False, result_map=self.result_map) - for c in self.returning_cols] - - if not hasattr(self, 'returning_parameters'): - self.returning_parameters = [] - - binds = [] - for i, col in enumerate(self.returning_cols): - dbtype = col.type.dialect_impl(self.dialect).get_dbapi_type(self.dialect.dbapi) - self.returning_parameters.append((i + 1, dbtype)) - - bindparam = sql.bindparam("ret_%d" % i, value=ReturningParam(dbtype)) - self.binds[bindparam.key] = bindparam - binds.append(self.bindparam_string(self._truncate_bindparam(bindparam))) - - return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds) - - -class OracleExecutionContext_zxjdbc(OracleExecutionContext): - - def pre_exec(self): - if hasattr(self.compiled, 'returning_parameters'): - # prepare a zxJDBC statement so we can grab its underlying - # OraclePreparedStatement's getReturnResultSet later - self.statement = self.cursor.prepare(self.statement) - - def get_result_proxy(self): - if hasattr(self.compiled, 'returning_parameters'): - rrs = None - try: - try: - rrs = self.statement.__statement__.getReturnResultSet() - next(rrs) - except SQLException as sqle: - msg = '%s [SQLCode: %d]' % (sqle.getMessage(), sqle.getErrorCode()) - if sqle.getSQLState() is not None: - msg += ' [SQLState: %s]' % sqle.getSQLState() - raise zxJDBC.Error(msg) - else: - row = tuple(self.cursor.datahandler.getPyObject(rrs, index, dbtype) - for index, dbtype in self.compiled.returning_parameters) - return ReturningResultProxy(self, row) - finally: - if rrs is not None: - try: - rrs.close() - except SQLException: - pass - self.statement.close() - - return _result.ResultProxy(self) - - def create_cursor(self): - cursor = self._dbapi_connection.cursor() - cursor.datahandler = self.dialect.DataHandler(cursor.datahandler) - return cursor - - -class ReturningResultProxy(_result.FullyBufferedResultProxy): - - """ResultProxy backed by the RETURNING ResultSet results.""" - - def __init__(self, context, returning_row): - self._returning_row = returning_row - super(ReturningResultProxy, self).__init__(context) - - def _cursor_description(self): - ret = [] - for c in self.context.compiled.returning_cols: - if hasattr(c, 'name'): - ret.append((c.name, c.type)) - else: - ret.append((c.anon_label, c.type)) - return ret - - def _buffer_rows(self): - return collections.deque([self._returning_row]) - - -class ReturningParam(object): - - """A bindparam value representing a RETURNING parameter. - - Specially handled by OracleReturningDataHandler. - """ - - def __init__(self, type): - self.type = type - - def __eq__(self, other): - if isinstance(other, ReturningParam): - return self.type == other.type - return NotImplemented - - def __ne__(self, other): - if isinstance(other, ReturningParam): - return self.type != other.type - return NotImplemented - - def __repr__(self): - kls = self.__class__ - return '<%s.%s object at 0x%x type=%s>' % (kls.__module__, kls.__name__, id(self), - self.type) - - -class OracleDialect_zxjdbc(ZxJDBCConnector, OracleDialect): - jdbc_db_name = 'oracle' - jdbc_driver_name = 'oracle.jdbc.OracleDriver' - - statement_compiler = OracleCompiler_zxjdbc - execution_ctx_cls = OracleExecutionContext_zxjdbc - - colspecs = util.update_copy( - OracleDialect.colspecs, - { - sqltypes.Date: _ZxJDBCDate, - sqltypes.Numeric: _ZxJDBCNumeric - } - ) - - def __init__(self, *args, **kwargs): - super(OracleDialect_zxjdbc, self).__init__(*args, **kwargs) - global SQLException, zxJDBC - from java.sql import SQLException - from com.ziclix.python.sql import zxJDBC - from com.ziclix.python.sql.handler import OracleDataHandler - - class OracleReturningDataHandler(OracleDataHandler): - """zxJDBC DataHandler that specially handles ReturningParam.""" - - def setJDBCObject(self, statement, index, object, dbtype=None): - if type(object) is ReturningParam: - statement.registerReturnParameter(index, object.type) - elif dbtype is None: - OracleDataHandler.setJDBCObject( - self, statement, index, object) - else: - OracleDataHandler.setJDBCObject( - self, statement, index, object, dbtype) - self.DataHandler = OracleReturningDataHandler - - def initialize(self, connection): - super(OracleDialect_zxjdbc, self).initialize(connection) - self.implicit_returning = connection.connection.driverversion >= '10.2' - - def _create_jdbc_url(self, url): - return 'jdbc:oracle:thin:@%s:%s:%s' % (url.host, url.port or 1521, url.database) - - def _get_server_version_info(self, connection): - version = re.search(r'Release ([\d\.]+)', connection.connection.dbversion).group(1) - return tuple(int(x) for x in version.split('.')) - -dialect = OracleDialect_zxjdbc diff --git a/libs/sqlalchemy/dialects/postgres.py b/libs/sqlalchemy/dialects/postgres.py deleted file mode 100644 index 6ed7e18b..00000000 --- a/libs/sqlalchemy/dialects/postgres.py +++ /dev/null @@ -1,16 +0,0 @@ -# dialects/postgres.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -# backwards compat with the old name -from sqlalchemy.util import warn_deprecated - -warn_deprecated( - "The SQLAlchemy PostgreSQL dialect has been renamed from 'postgres' to 'postgresql'. " - "The new URL format is postgresql[+driver]://:@/" - ) - -from sqlalchemy.dialects.postgresql import * -from sqlalchemy.dialects.postgresql import base diff --git a/libs/sqlalchemy/dialects/postgresql/__init__.py b/libs/sqlalchemy/dialects/postgresql/__init__.py deleted file mode 100644 index 180e9fc7..00000000 --- a/libs/sqlalchemy/dialects/postgresql/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# postgresql/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from . import base, psycopg2, pg8000, pypostgresql, zxjdbc - -base.dialect = psycopg2.dialect - -from .base import \ - INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, \ - INET, CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME, \ - DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect, array, Any, All, \ - TSVECTOR -from .constraints import ExcludeConstraint -from .hstore import HSTORE, hstore -from .json import JSON, JSONElement -from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \ - TSTZRANGE - -__all__ = ( - 'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC', - 'FLOAT', 'REAL', 'INET', 'CIDR', 'UUID', 'BIT', 'MACADDR', - 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN', - 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE', - 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE', - 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONElement' -) diff --git a/libs/sqlalchemy/dialects/postgresql/base.py b/libs/sqlalchemy/dialects/postgresql/base.py deleted file mode 100644 index b7979a3e..00000000 --- a/libs/sqlalchemy/dialects/postgresql/base.py +++ /dev/null @@ -1,2224 +0,0 @@ -# postgresql/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: postgresql - :name: PostgreSQL - - -Sequences/SERIAL ----------------- - -PostgreSQL supports sequences, and SQLAlchemy uses these as the default means -of creating new primary key values for integer-based primary key columns. When -creating tables, SQLAlchemy will issue the ``SERIAL`` datatype for -integer-based primary key columns, which generates a sequence and server side -default corresponding to the column. - -To specify a specific named sequence to be used for primary key generation, -use the :func:`~sqlalchemy.schema.Sequence` construct:: - - Table('sometable', metadata, - Column('id', Integer, Sequence('some_id_seq'), primary_key=True) - ) - -When SQLAlchemy issues a single INSERT statement, to fulfill the contract of -having the "last insert identifier" available, a RETURNING clause is added to -the INSERT statement which specifies the primary key columns should be -returned after the statement completes. The RETURNING functionality only takes -place if Postgresql 8.2 or later is in use. As a fallback approach, the -sequence, whether specified explicitly or implicitly via ``SERIAL``, is -executed independently beforehand, the returned value to be used in the -subsequent insert. Note that when an -:func:`~sqlalchemy.sql.expression.insert()` construct is executed using -"executemany" semantics, the "last inserted identifier" functionality does not -apply; no RETURNING clause is emitted nor is the sequence pre-executed in this -case. - -To force the usage of RETURNING by default off, specify the flag -``implicit_returning=False`` to :func:`.create_engine`. - -.. _postgresql_isolation_level: - -Transaction Isolation Level ---------------------------- - -All Postgresql dialects support setting of transaction isolation level -both via a dialect-specific parameter ``isolation_level`` -accepted by :func:`.create_engine`, -as well as the ``isolation_level`` argument as passed to :meth:`.Connection.execution_options`. -When using a non-psycopg2 dialect, this feature works by issuing the -command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL -`` for each new connection. - -To set isolation level using :func:`.create_engine`:: - - engine = create_engine( - "postgresql+pg8000://scott:tiger@localhost/test", - isolation_level="READ UNCOMMITTED" - ) - -To set using per-connection execution options:: - - connection = engine.connect() - connection = connection.execution_options(isolation_level="READ COMMITTED") - -Valid values for ``isolation_level`` include: - -* ``READ COMMITTED`` -* ``READ UNCOMMITTED`` -* ``REPEATABLE READ`` -* ``SERIALIZABLE`` - -The :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect also offers the special level ``AUTOCOMMIT``. See -:ref:`psycopg2_isolation_level` for details. - - -Remote / Cross-Schema Table Introspection ------------------------------------------ - -Tables can be introspected from any accessible schema, including -inter-schema foreign key relationships. However, care must be taken -when specifying the "schema" argument for a given :class:`.Table`, when -the given schema is also present in PostgreSQL's ``search_path`` variable -for the current connection. - -If a FOREIGN KEY constraint reports that the remote table's schema is within -the current ``search_path``, the "schema" attribute of the resulting -:class:`.Table` will be set to ``None``, unless the actual schema of the -remote table matches that of the referencing table, and the "schema" argument -was explicitly stated on the referencing table. - -The best practice here is to not use the ``schema`` argument -on :class:`.Table` for any schemas that are present in ``search_path``. -``search_path`` defaults to "public", but care should be taken -to inspect the actual value using:: - - SHOW search_path; - -.. versionchanged:: 0.7.3 - Prior to this version, cross-schema foreign keys when the schemas - were also in the ``search_path`` could make an incorrect assumption - if the schemas were explicitly stated on each :class:`.Table`. - -Background on PG's ``search_path`` is at: -http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH - -INSERT/UPDATE...RETURNING -------------------------- - -The dialect supports PG 8.2's ``INSERT..RETURNING``, ``UPDATE..RETURNING`` and -``DELETE..RETURNING`` syntaxes. ``INSERT..RETURNING`` is used by default -for single-row INSERT statements in order to fetch newly generated -primary key identifiers. To specify an explicit ``RETURNING`` clause, -use the :meth:`._UpdateBase.returning` method on a per-statement basis:: - - # INSERT..RETURNING - result = table.insert().returning(table.c.col1, table.c.col2).\\ - values(name='foo') - print result.fetchall() - - # UPDATE..RETURNING - result = table.update().returning(table.c.col1, table.c.col2).\\ - where(table.c.name=='foo').values(name='bar') - print result.fetchall() - - # DELETE..RETURNING - result = table.delete().returning(table.c.col1, table.c.col2).\\ - where(table.c.name=='foo') - print result.fetchall() - -.. _postgresql_match: - -Full Text Search ----------------- - -SQLAlchemy makes available the Postgresql ``@@`` operator via the -:meth:`.ColumnElement.match` method on any textual column expression. -On a Postgresql dialect, an expression like the following:: - - select([sometable.c.text.match("search string")]) - -will emit to the database:: - - SELECT text @@ to_tsquery('search string') FROM table - -The Postgresql text search functions such as ``to_tsquery()`` -and ``to_tsvector()`` are available -explicitly using the standard :attr:`.func` construct. For example:: - - select([ - func.to_tsvector('fat cats ate rats').match('cat & rat') - ]) - -Emits the equivalent of:: - - SELECT to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat') - -The :class:`.postgresql.TSVECTOR` type can provide for explicit CAST:: - - from sqlalchemy.dialects.postgresql import TSVECTOR - from sqlalchemy import select, cast - select([cast("some text", TSVECTOR)]) - -produces a statement equivalent to:: - - SELECT CAST('some text' AS TSVECTOR) AS anon_1 - - -FROM ONLY ... ------------------------- - -The dialect supports PostgreSQL's ONLY keyword for targeting only a particular -table in an inheritance hierarchy. This can be used to produce the -``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...`` -syntaxes. It uses SQLAlchemy's hints mechanism:: - - # SELECT ... FROM ONLY ... - result = table.select().with_hint(table, 'ONLY', 'postgresql') - print result.fetchall() - - # UPDATE ONLY ... - table.update(values=dict(foo='bar')).with_hint('ONLY', - dialect_name='postgresql') - - # DELETE FROM ONLY ... - table.delete().with_hint('ONLY', dialect_name='postgresql') - -.. _postgresql_indexes: - -Postgresql-Specific Index Options ---------------------------------- - -Several extensions to the :class:`.Index` construct are available, specific -to the PostgreSQL dialect. - -Partial Indexes -^^^^^^^^^^^^^^^^ - -Partial indexes add criterion to the index definition so that the index is -applied to a subset of rows. These can be specified on :class:`.Index` -using the ``postgresql_where`` keyword argument:: - - Index('my_index', my_table.c.id, postgresql_where=tbl.c.value > 10) - -Operator Classes -^^^^^^^^^^^^^^^^^ - -PostgreSQL allows the specification of an *operator class* for each column of -an index (see -http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html). -The :class:`.Index` construct allows these to be specified via the -``postgresql_ops`` keyword argument:: - - Index('my_index', my_table.c.id, my_table.c.data, - postgresql_ops={ - 'data': 'text_pattern_ops', - 'id': 'int4_ops' - }) - -.. versionadded:: 0.7.2 - ``postgresql_ops`` keyword argument to :class:`.Index` construct. - -Note that the keys in the ``postgresql_ops`` dictionary are the "key" name of -the :class:`.Column`, i.e. the name used to access it from the ``.c`` -collection of :class:`.Table`, which can be configured to be different than -the actual name of the column as expressed in the database. - -Index Types -^^^^^^^^^^^^ - -PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well -as the ability for users to create their own (see -http://www.postgresql.org/docs/8.3/static/indexes-types.html). These can be -specified on :class:`.Index` using the ``postgresql_using`` keyword argument:: - - Index('my_index', my_table.c.data, postgresql_using='gin') - -The value passed to the keyword argument will be simply passed through to the -underlying CREATE INDEX command, so it *must* be a valid index type for your -version of PostgreSQL. - -""" -from collections import defaultdict -import re - -from ... import sql, schema, exc, util -from ...engine import default, reflection -from ...sql import compiler, expression, operators -from ... import types as sqltypes - -try: - from uuid import UUID as _python_UUID -except ImportError: - _python_UUID = None - -from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, VARCHAR, \ - CHAR, TEXT, FLOAT, NUMERIC, \ - DATE, BOOLEAN, REAL - -RESERVED_WORDS = set( - ["all", "analyse", "analyze", "and", "any", "array", "as", "asc", - "asymmetric", "both", "case", "cast", "check", "collate", "column", - "constraint", "create", "current_catalog", "current_date", - "current_role", "current_time", "current_timestamp", "current_user", - "default", "deferrable", "desc", "distinct", "do", "else", "end", - "except", "false", "fetch", "for", "foreign", "from", "grant", "group", - "having", "in", "initially", "intersect", "into", "leading", "limit", - "localtime", "localtimestamp", "new", "not", "null", "of", "off", "offset", - "old", "on", "only", "or", "order", "placing", "primary", "references", - "returning", "select", "session_user", "some", "symmetric", "table", - "then", "to", "trailing", "true", "union", "unique", "user", "using", - "variadic", "when", "where", "window", "with", "authorization", - "between", "binary", "cross", "current_schema", "freeze", "full", - "ilike", "inner", "is", "isnull", "join", "left", "like", "natural", - "notnull", "outer", "over", "overlaps", "right", "similar", "verbose" - ]) - -_DECIMAL_TYPES = (1231, 1700) -_FLOAT_TYPES = (700, 701, 1021, 1022) -_INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016) - - -class BYTEA(sqltypes.LargeBinary): - __visit_name__ = 'BYTEA' - - -class DOUBLE_PRECISION(sqltypes.Float): - __visit_name__ = 'DOUBLE_PRECISION' - - -class INET(sqltypes.TypeEngine): - __visit_name__ = "INET" -PGInet = INET - - -class CIDR(sqltypes.TypeEngine): - __visit_name__ = "CIDR" -PGCidr = CIDR - - -class MACADDR(sqltypes.TypeEngine): - __visit_name__ = "MACADDR" -PGMacAddr = MACADDR - - -class TIMESTAMP(sqltypes.TIMESTAMP): - def __init__(self, timezone=False, precision=None): - super(TIMESTAMP, self).__init__(timezone=timezone) - self.precision = precision - - -class TIME(sqltypes.TIME): - def __init__(self, timezone=False, precision=None): - super(TIME, self).__init__(timezone=timezone) - self.precision = precision - - -class INTERVAL(sqltypes.TypeEngine): - """Postgresql INTERVAL type. - - The INTERVAL type may not be supported on all DBAPIs. - It is known to work on psycopg2 and not pg8000 or zxjdbc. - - """ - __visit_name__ = 'INTERVAL' - - def __init__(self, precision=None): - self.precision = precision - - @classmethod - def _adapt_from_generic_interval(cls, interval): - return INTERVAL(precision=interval.second_precision) - - @property - def _type_affinity(self): - return sqltypes.Interval - -PGInterval = INTERVAL - - -class BIT(sqltypes.TypeEngine): - __visit_name__ = 'BIT' - - def __init__(self, length=None, varying=False): - if not varying: - # BIT without VARYING defaults to length 1 - self.length = length or 1 - else: - # but BIT VARYING can be unlimited-length, so no default - self.length = length - self.varying = varying - -PGBit = BIT - - -class UUID(sqltypes.TypeEngine): - """Postgresql UUID type. - - Represents the UUID column type, interpreting - data either as natively returned by the DBAPI - or as Python uuid objects. - - The UUID type may not be supported on all DBAPIs. - It is known to work on psycopg2 and not pg8000. - - """ - __visit_name__ = 'UUID' - - def __init__(self, as_uuid=False): - """Construct a UUID type. - - - :param as_uuid=False: if True, values will be interpreted - as Python uuid objects, converting to/from string via the - DBAPI. - - """ - if as_uuid and _python_UUID is None: - raise NotImplementedError( - "This version of Python does not support the native UUID type." - ) - self.as_uuid = as_uuid - - def bind_processor(self, dialect): - if self.as_uuid: - def process(value): - if value is not None: - value = util.text_type(value) - return value - return process - else: - return None - - def result_processor(self, dialect, coltype): - if self.as_uuid: - def process(value): - if value is not None: - value = _python_UUID(value) - return value - return process - else: - return None - -PGUuid = UUID - -class TSVECTOR(sqltypes.TypeEngine): - """The :class:`.postgresql.TSVECTOR` type implements the Postgresql - text search type TSVECTOR. - - It can be used to do full text queries on natural language - documents. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :ref:`postgresql_match` - - """ - __visit_name__ = 'TSVECTOR' - - - -class _Slice(expression.ColumnElement): - __visit_name__ = 'slice' - type = sqltypes.NULLTYPE - - def __init__(self, slice_, source_comparator): - self.start = source_comparator._check_literal( - source_comparator.expr, - operators.getitem, slice_.start) - self.stop = source_comparator._check_literal( - source_comparator.expr, - operators.getitem, slice_.stop) - - -class Any(expression.ColumnElement): - """Represent the clause ``left operator ANY (right)``. ``right`` must be - an array expression. - - .. seealso:: - - :class:`.postgresql.ARRAY` - - :meth:`.postgresql.ARRAY.Comparator.any` - ARRAY-bound method - - """ - __visit_name__ = 'any' - - def __init__(self, left, right, operator=operators.eq): - self.type = sqltypes.Boolean() - self.left = expression._literal_as_binds(left) - self.right = right - self.operator = operator - - -class All(expression.ColumnElement): - """Represent the clause ``left operator ALL (right)``. ``right`` must be - an array expression. - - .. seealso:: - - :class:`.postgresql.ARRAY` - - :meth:`.postgresql.ARRAY.Comparator.all` - ARRAY-bound method - - """ - __visit_name__ = 'all' - - def __init__(self, left, right, operator=operators.eq): - self.type = sqltypes.Boolean() - self.left = expression._literal_as_binds(left) - self.right = right - self.operator = operator - - -class array(expression.Tuple): - """A Postgresql ARRAY literal. - - This is used to produce ARRAY literals in SQL expressions, e.g.:: - - from sqlalchemy.dialects.postgresql import array - from sqlalchemy.dialects import postgresql - from sqlalchemy import select, func - - stmt = select([ - array([1,2]) + array([3,4,5]) - ]) - - print stmt.compile(dialect=postgresql.dialect()) - - Produces the SQL:: - - SELECT ARRAY[%(param_1)s, %(param_2)s] || - ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1 - - An instance of :class:`.array` will always have the datatype - :class:`.ARRAY`. The "inner" type of the array is inferred from - the values present, unless the ``type_`` keyword argument is passed:: - - array(['foo', 'bar'], type_=CHAR) - - .. versionadded:: 0.8 Added the :class:`~.postgresql.array` literal type. - - See also: - - :class:`.postgresql.ARRAY` - - """ - __visit_name__ = 'array' - - def __init__(self, clauses, **kw): - super(array, self).__init__(*clauses, **kw) - self.type = ARRAY(self.type) - - def _bind_param(self, operator, obj): - return array(*[ - expression.BindParameter(None, o, _compared_to_operator=operator, - _compared_to_type=self.type, unique=True) - for o in obj - ]) - - def self_group(self, against=None): - return self - - -class ARRAY(sqltypes.Concatenable, sqltypes.TypeEngine): - """Postgresql ARRAY type. - - Represents values as Python lists. - - An :class:`.ARRAY` type is constructed given the "type" - of element:: - - mytable = Table("mytable", metadata, - Column("data", ARRAY(Integer)) - ) - - The above type represents an N-dimensional array, - meaning Postgresql will interpret values with any number - of dimensions automatically. To produce an INSERT - construct that passes in a 1-dimensional array of integers:: - - connection.execute( - mytable.insert(), - data=[1,2,3] - ) - - The :class:`.ARRAY` type can be constructed given a fixed number - of dimensions:: - - mytable = Table("mytable", metadata, - Column("data", ARRAY(Integer, dimensions=2)) - ) - - This has the effect of the :class:`.ARRAY` type - specifying that number of bracketed blocks when a :class:`.Table` - is used in a CREATE TABLE statement, or when the type is used - within a :func:`.expression.cast` construct; it also causes - the bind parameter and result set processing of the type - to optimize itself to expect exactly that number of dimensions. - Note that Postgresql itself still allows N dimensions with such a type. - - SQL expressions of type :class:`.ARRAY` have support for "index" and - "slice" behavior. The Python ``[]`` operator works normally here, given - integer indexes or slices. Note that Postgresql arrays default - to 1-based indexing. The operator produces binary expression - constructs which will produce the appropriate SQL, both for - SELECT statements:: - - select([mytable.c.data[5], mytable.c.data[2:7]]) - - as well as UPDATE statements when the :meth:`.Update.values` method - is used:: - - mytable.update().values({ - mytable.c.data[5]: 7, - mytable.c.data[2:7]: [1, 2, 3] - }) - - :class:`.ARRAY` provides special methods for containment operations, - e.g.:: - - mytable.c.data.contains([1, 2]) - - For a full list of special methods see :class:`.ARRAY.Comparator`. - - .. versionadded:: 0.8 Added support for index and slice operations - to the :class:`.ARRAY` type, including support for UPDATE - statements, and special array containment operations. - - The :class:`.ARRAY` type may not be supported on all DBAPIs. - It is known to work on psycopg2 and not pg8000. - - See also: - - :class:`.postgresql.array` - produce a literal array value. - - """ - __visit_name__ = 'ARRAY' - - class Comparator(sqltypes.Concatenable.Comparator): - """Define comparison operations for :class:`.ARRAY`.""" - - def __getitem__(self, index): - if isinstance(index, slice): - index = _Slice(index, self) - return_type = self.type - else: - return_type = self.type.item_type - return self._binary_operate(self.expr, operators.getitem, index, - result_type=return_type) - - def any(self, other, operator=operators.eq): - """Return ``other operator ANY (array)`` clause. - - Argument places are switched, because ANY requires array - expression to be on the right hand-side. - - E.g.:: - - from sqlalchemy.sql import operators - - conn.execute( - select([table.c.data]).where( - table.c.data.any(7, operator=operators.lt) - ) - ) - - :param other: expression to be compared - :param operator: an operator object from the - :mod:`sqlalchemy.sql.operators` - package, defaults to :func:`.operators.eq`. - - .. seealso:: - - :class:`.postgresql.Any` - - :meth:`.postgresql.ARRAY.Comparator.all` - - """ - return Any(other, self.expr, operator=operator) - - def all(self, other, operator=operators.eq): - """Return ``other operator ALL (array)`` clause. - - Argument places are switched, because ALL requires array - expression to be on the right hand-side. - - E.g.:: - - from sqlalchemy.sql import operators - - conn.execute( - select([table.c.data]).where( - table.c.data.all(7, operator=operators.lt) - ) - ) - - :param other: expression to be compared - :param operator: an operator object from the - :mod:`sqlalchemy.sql.operators` - package, defaults to :func:`.operators.eq`. - - .. seealso:: - - :class:`.postgresql.All` - - :meth:`.postgresql.ARRAY.Comparator.any` - - """ - return All(other, self.expr, operator=operator) - - def contains(self, other, **kwargs): - """Boolean expression. Test if elements are a superset of the - elements of the argument array expression. - """ - return self.expr.op('@>')(other) - - def contained_by(self, other): - """Boolean expression. Test if elements are a proper subset of the - elements of the argument array expression. - """ - return self.expr.op('<@')(other) - - def overlap(self, other): - """Boolean expression. Test if array has elements in common with - an argument array expression. - """ - return self.expr.op('&&')(other) - - def _adapt_expression(self, op, other_comparator): - if isinstance(op, operators.custom_op): - if op.opstring in ['@>', '<@', '&&']: - return op, sqltypes.Boolean - return sqltypes.Concatenable.Comparator.\ - _adapt_expression(self, op, other_comparator) - - comparator_factory = Comparator - - def __init__(self, item_type, as_tuple=False, dimensions=None): - """Construct an ARRAY. - - E.g.:: - - Column('myarray', ARRAY(Integer)) - - Arguments are: - - :param item_type: The data type of items of this array. Note that - dimensionality is irrelevant here, so multi-dimensional arrays like - ``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as - ``ARRAY(ARRAY(Integer))`` or such. - - :param as_tuple=False: Specify whether return results - should be converted to tuples from lists. DBAPIs such - as psycopg2 return lists by default. When tuples are - returned, the results are hashable. - - :param dimensions: if non-None, the ARRAY will assume a fixed - number of dimensions. This will cause the DDL emitted for this - ARRAY to include the exact number of bracket clauses ``[]``, - and will also optimize the performance of the type overall. - Note that PG arrays are always implicitly "non-dimensioned", - meaning they can store any number of dimensions no matter how - they were declared. - - """ - if isinstance(item_type, ARRAY): - raise ValueError("Do not nest ARRAY types; ARRAY(basetype) " - "handles multi-dimensional arrays of basetype") - if isinstance(item_type, type): - item_type = item_type() - self.item_type = item_type - self.as_tuple = as_tuple - self.dimensions = dimensions - - def compare_values(self, x, y): - return x == y - - def _proc_array(self, arr, itemproc, dim, collection): - if dim is None: - arr = list(arr) - if dim == 1 or dim is None and ( - # this has to be (list, tuple), or at least - # not hasattr('__iter__'), since Py3K strings - # etc. have __iter__ - not arr or not isinstance(arr[0], (list, tuple))): - if itemproc: - return collection(itemproc(x) for x in arr) - else: - return collection(arr) - else: - return collection( - self._proc_array( - x, itemproc, - dim - 1 if dim is not None else None, - collection) - for x in arr - ) - - def bind_processor(self, dialect): - item_proc = self.item_type.\ - dialect_impl(dialect).\ - bind_processor(dialect) - - def process(value): - if value is None: - return value - else: - return self._proc_array( - value, - item_proc, - self.dimensions, - list) - return process - - def result_processor(self, dialect, coltype): - item_proc = self.item_type.\ - dialect_impl(dialect).\ - result_processor(dialect, coltype) - - def process(value): - if value is None: - return value - else: - return self._proc_array( - value, - item_proc, - self.dimensions, - tuple if self.as_tuple else list) - return process - -PGArray = ARRAY - - -class ENUM(sqltypes.Enum): - """Postgresql ENUM type. - - This is a subclass of :class:`.types.Enum` which includes - support for PG's ``CREATE TYPE``. - - :class:`~.postgresql.ENUM` is used automatically when - using the :class:`.types.Enum` type on PG assuming - the ``native_enum`` is left as ``True``. However, the - :class:`~.postgresql.ENUM` class can also be instantiated - directly in order to access some additional Postgresql-specific - options, namely finer control over whether or not - ``CREATE TYPE`` should be emitted. - - Note that both :class:`.types.Enum` as well as - :class:`~.postgresql.ENUM` feature create/drop - methods; the base :class:`.types.Enum` type ultimately - delegates to the :meth:`~.postgresql.ENUM.create` and - :meth:`~.postgresql.ENUM.drop` methods present here. - - """ - - def __init__(self, *enums, **kw): - """Construct an :class:`~.postgresql.ENUM`. - - Arguments are the same as that of - :class:`.types.Enum`, but also including - the following parameters. - - :param create_type: Defaults to True. - Indicates that ``CREATE TYPE`` should be - emitted, after optionally checking for the - presence of the type, when the parent - table is being created; and additionally - that ``DROP TYPE`` is called when the table - is dropped. When ``False``, no check - will be performed and no ``CREATE TYPE`` - or ``DROP TYPE`` is emitted, unless - :meth:`~.postgresql.ENUM.create` - or :meth:`~.postgresql.ENUM.drop` - are called directly. - Setting to ``False`` is helpful - when invoking a creation scheme to a SQL file - without access to the actual database - - the :meth:`~.postgresql.ENUM.create` and - :meth:`~.postgresql.ENUM.drop` methods can - be used to emit SQL to a target bind. - - .. versionadded:: 0.7.4 - - """ - self.create_type = kw.pop("create_type", True) - super(ENUM, self).__init__(*enums, **kw) - - def create(self, bind=None, checkfirst=True): - """Emit ``CREATE TYPE`` for this - :class:`~.postgresql.ENUM`. - - If the underlying dialect does not support - Postgresql CREATE TYPE, no action is taken. - - :param bind: a connectable :class:`.Engine`, - :class:`.Connection`, or similar object to emit - SQL. - :param checkfirst: if ``True``, a query against - the PG catalog will be first performed to see - if the type does not exist already before - creating. - - """ - if not bind.dialect.supports_native_enum: - return - - if not checkfirst or \ - not bind.dialect.has_type(bind, self.name, schema=self.schema): - bind.execute(CreateEnumType(self)) - - def drop(self, bind=None, checkfirst=True): - """Emit ``DROP TYPE`` for this - :class:`~.postgresql.ENUM`. - - If the underlying dialect does not support - Postgresql DROP TYPE, no action is taken. - - :param bind: a connectable :class:`.Engine`, - :class:`.Connection`, or similar object to emit - SQL. - :param checkfirst: if ``True``, a query against - the PG catalog will be first performed to see - if the type actually exists before dropping. - - """ - if not bind.dialect.supports_native_enum: - return - - if not checkfirst or \ - bind.dialect.has_type(bind, self.name, schema=self.schema): - bind.execute(DropEnumType(self)) - - def _check_for_name_in_memos(self, checkfirst, kw): - """Look in the 'ddl runner' for 'memos', then - note our name in that collection. - - This to ensure a particular named enum is operated - upon only once within any kind of create/drop - sequence without relying upon "checkfirst". - - """ - if not self.create_type: - return True - if '_ddl_runner' in kw: - ddl_runner = kw['_ddl_runner'] - if '_pg_enums' in ddl_runner.memo: - pg_enums = ddl_runner.memo['_pg_enums'] - else: - pg_enums = ddl_runner.memo['_pg_enums'] = set() - present = self.name in pg_enums - pg_enums.add(self.name) - return present - else: - return False - - def _on_table_create(self, target, bind, checkfirst, **kw): - if not self._check_for_name_in_memos(checkfirst, kw): - self.create(bind=bind, checkfirst=checkfirst) - - def _on_metadata_create(self, target, bind, checkfirst, **kw): - if self.metadata is not None and \ - not self._check_for_name_in_memos(checkfirst, kw): - self.create(bind=bind, checkfirst=checkfirst) - - def _on_metadata_drop(self, target, bind, checkfirst, **kw): - if not self._check_for_name_in_memos(checkfirst, kw): - self.drop(bind=bind, checkfirst=checkfirst) - -colspecs = { - sqltypes.Interval: INTERVAL, - sqltypes.Enum: ENUM, -} - -ischema_names = { - 'integer': INTEGER, - 'bigint': BIGINT, - 'smallint': SMALLINT, - 'character varying': VARCHAR, - 'character': CHAR, - '"char"': sqltypes.String, - 'name': sqltypes.String, - 'text': TEXT, - 'numeric': NUMERIC, - 'float': FLOAT, - 'real': REAL, - 'inet': INET, - 'cidr': CIDR, - 'uuid': UUID, - 'bit': BIT, - 'bit varying': BIT, - 'macaddr': MACADDR, - 'double precision': DOUBLE_PRECISION, - 'timestamp': TIMESTAMP, - 'timestamp with time zone': TIMESTAMP, - 'timestamp without time zone': TIMESTAMP, - 'time with time zone': TIME, - 'time without time zone': TIME, - 'date': DATE, - 'time': TIME, - 'bytea': BYTEA, - 'boolean': BOOLEAN, - 'interval': INTERVAL, - 'interval year to month': INTERVAL, - 'interval day to second': INTERVAL, - 'tsvector' : TSVECTOR -} - - -class PGCompiler(compiler.SQLCompiler): - - def visit_array(self, element, **kw): - return "ARRAY[%s]" % self.visit_clauselist(element, **kw) - - def visit_slice(self, element, **kw): - return "%s:%s" % ( - self.process(element.start, **kw), - self.process(element.stop, **kw), - ) - - def visit_any(self, element, **kw): - return "%s%sANY (%s)" % ( - self.process(element.left, **kw), - compiler.OPERATORS[element.operator], - self.process(element.right, **kw) - ) - - def visit_all(self, element, **kw): - return "%s%sALL (%s)" % ( - self.process(element.left, **kw), - compiler.OPERATORS[element.operator], - self.process(element.right, **kw) - ) - - def visit_getitem_binary(self, binary, operator, **kw): - return "%s[%s]" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw) - ) - - def visit_match_op_binary(self, binary, operator, **kw): - return "%s @@ to_tsquery(%s)" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw)) - - def visit_ilike_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - - return '%s ILIKE %s' % \ - (self.process(binary.left, **kw), - self.process(binary.right, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def visit_notilike_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - return '%s NOT ILIKE %s' % \ - (self.process(binary.left, **kw), - self.process(binary.right, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def render_literal_value(self, value, type_): - value = super(PGCompiler, self).render_literal_value(value, type_) - - if self.dialect._backslash_escapes: - value = value.replace('\\', '\\\\') - return value - - def visit_sequence(self, seq): - return "nextval('%s')" % self.preparer.format_sequence(seq) - - def limit_clause(self, select): - text = "" - if select._limit is not None: - text += " \n LIMIT " + self.process(sql.literal(select._limit)) - if select._offset is not None: - if select._limit is None: - text += " \n LIMIT ALL" - text += " OFFSET " + self.process(sql.literal(select._offset)) - return text - - def format_from_hint_text(self, sqltext, table, hint, iscrud): - if hint.upper() != 'ONLY': - raise exc.CompileError("Unrecognized hint: %r" % hint) - return "ONLY " + sqltext - - def get_select_precolumns(self, select): - if select._distinct is not False: - if select._distinct is True: - return "DISTINCT " - elif isinstance(select._distinct, (list, tuple)): - return "DISTINCT ON (" + ', '.join( - [self.process(col) for col in select._distinct] - ) + ") " - else: - return "DISTINCT ON (" + self.process(select._distinct) + ") " - else: - return "" - - def for_update_clause(self, select): - - if select._for_update_arg.read: - tmp = " FOR SHARE" - else: - tmp = " FOR UPDATE" - - if select._for_update_arg.of: - tables = util.OrderedSet( - c.table if isinstance(c, expression.ColumnClause) - else c for c in select._for_update_arg.of) - tmp += " OF " + ", ".join( - self.process(table, ashint=True) - for table in tables - ) - - if select._for_update_arg.nowait: - tmp += " NOWAIT" - - return tmp - - def returning_clause(self, stmt, returning_cols): - - columns = [ - self._label_select_column(None, c, True, False, {}) - for c in expression._select_iterables(returning_cols) - ] - - return 'RETURNING ' + ', '.join(columns) - - - def visit_substring_func(self, func, **kw): - s = self.process(func.clauses.clauses[0], **kw) - start = self.process(func.clauses.clauses[1], **kw) - if len(func.clauses.clauses) > 2: - length = self.process(func.clauses.clauses[2], **kw) - return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length) - else: - return "SUBSTRING(%s FROM %s)" % (s, start) - -class PGDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, **kwargs): - - colspec = self.preparer.format_column(column) - impl_type = column.type.dialect_impl(self.dialect) - if column.primary_key and \ - column is column.table._autoincrement_column and \ - ( - self.dialect.supports_smallserial or - not isinstance(impl_type, sqltypes.SmallInteger) - ) and ( - column.default is None or - ( - isinstance(column.default, schema.Sequence) and - column.default.optional - )): - if isinstance(impl_type, sqltypes.BigInteger): - colspec += " BIGSERIAL" - elif isinstance(impl_type, sqltypes.SmallInteger): - colspec += " SMALLSERIAL" - else: - colspec += " SERIAL" - else: - colspec += " " + self.dialect.type_compiler.process(column.type) - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - if not column.nullable: - colspec += " NOT NULL" - return colspec - - def visit_create_enum_type(self, create): - type_ = create.element - - return "CREATE TYPE %s AS ENUM (%s)" % ( - self.preparer.format_type(type_), - ", ".join( - self.sql_compiler.process(sql.literal(e), literal_binds=True) - for e in type_.enums) - ) - - def visit_drop_enum_type(self, drop): - type_ = drop.element - - return "DROP TYPE %s" % ( - self.preparer.format_type(type_) - ) - - def visit_create_index(self, create): - preparer = self.preparer - index = create.element - self._verify_index_table(index) - text = "CREATE " - if index.unique: - text += "UNIQUE " - text += "INDEX %s ON %s " % ( - self._prepared_index_name(index, - include_schema=False), - preparer.format_table(index.table) - ) - - if 'postgresql_using' in index.kwargs: - using = index.kwargs['postgresql_using'] - text += "USING %s " % preparer.quote(using) - - ops = index.kwargs.get('postgresql_ops', {}) - text += "(%s)" \ - % ( - ', '.join([ - self.sql_compiler.process( - expr.self_group() - if not isinstance(expr, expression.ColumnClause) - else expr, - include_table=False, literal_binds=True) + - (c.key in ops and (' ' + ops[c.key]) or '') - for expr, c in zip(index.expressions, index.columns)]) - ) - - if 'postgresql_where' in index.kwargs: - whereclause = index.kwargs['postgresql_where'] - else: - whereclause = None - - if whereclause is not None: - where_compiled = self.sql_compiler.process( - whereclause, include_table=False, - literal_binds=True) - text += " WHERE " + where_compiled - return text - - def visit_exclude_constraint(self, constraint): - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - self.preparer.format_constraint(constraint) - elements = [] - for c in constraint.columns: - op = constraint.operators[c.name] - elements.append(self.preparer.quote(c.name) + ' WITH '+op) - text += "EXCLUDE USING %s (%s)" % (constraint.using, ', '.join(elements)) - if constraint.where is not None: - text += ' WHERE (%s)' % self.sql_compiler.process( - constraint.where, - literal_binds=True) - text += self.define_constraint_deferrability(constraint) - return text - - -class PGTypeCompiler(compiler.GenericTypeCompiler): - def visit_TSVECTOR(self, type): - return "TSVECTOR" - - def visit_INET(self, type_): - return "INET" - - def visit_CIDR(self, type_): - return "CIDR" - - def visit_MACADDR(self, type_): - return "MACADDR" - - def visit_FLOAT(self, type_): - if not type_.precision: - return "FLOAT" - else: - return "FLOAT(%(precision)s)" % {'precision': type_.precision} - - def visit_DOUBLE_PRECISION(self, type_): - return "DOUBLE PRECISION" - - def visit_BIGINT(self, type_): - return "BIGINT" - - def visit_HSTORE(self, type_): - return "HSTORE" - - def visit_JSON(self, type_): - return "JSON" - - def visit_INT4RANGE(self, type_): - return "INT4RANGE" - - def visit_INT8RANGE(self, type_): - return "INT8RANGE" - - def visit_NUMRANGE(self, type_): - return "NUMRANGE" - - def visit_DATERANGE(self, type_): - return "DATERANGE" - - def visit_TSRANGE(self, type_): - return "TSRANGE" - - def visit_TSTZRANGE(self, type_): - return "TSTZRANGE" - - def visit_datetime(self, type_): - return self.visit_TIMESTAMP(type_) - - def visit_enum(self, type_): - if not type_.native_enum or not self.dialect.supports_native_enum: - return super(PGTypeCompiler, self).visit_enum(type_) - else: - return self.visit_ENUM(type_) - - def visit_ENUM(self, type_): - return self.dialect.identifier_preparer.format_type(type_) - - def visit_TIMESTAMP(self, type_): - return "TIMESTAMP%s %s" % ( - getattr(type_, 'precision', None) and "(%d)" % - type_.precision or "", - (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE" - ) - - def visit_TIME(self, type_): - return "TIME%s %s" % ( - getattr(type_, 'precision', None) and "(%d)" % - type_.precision or "", - (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE" - ) - - def visit_INTERVAL(self, type_): - if type_.precision is not None: - return "INTERVAL(%d)" % type_.precision - else: - return "INTERVAL" - - def visit_BIT(self, type_): - if type_.varying: - compiled = "BIT VARYING" - if type_.length is not None: - compiled += "(%d)" % type_.length - else: - compiled = "BIT(%d)" % type_.length - return compiled - - def visit_UUID(self, type_): - return "UUID" - - def visit_large_binary(self, type_): - return self.visit_BYTEA(type_) - - def visit_BYTEA(self, type_): - return "BYTEA" - - def visit_ARRAY(self, type_): - return self.process(type_.item_type) + ('[]' * (type_.dimensions - if type_.dimensions - is not None else 1)) - - -class PGIdentifierPreparer(compiler.IdentifierPreparer): - - reserved_words = RESERVED_WORDS - - def _unquote_identifier(self, value): - if value[0] == self.initial_quote: - value = value[1:-1].\ - replace(self.escape_to_quote, self.escape_quote) - return value - - def format_type(self, type_, use_schema=True): - if not type_.name: - raise exc.CompileError("Postgresql ENUM type requires a name.") - - name = self.quote(type_.name) - if not self.omit_schema and use_schema and type_.schema is not None: - name = self.quote_schema(type_.schema) + "." + name - return name - - -class PGInspector(reflection.Inspector): - - def __init__(self, conn): - reflection.Inspector.__init__(self, conn) - - def get_table_oid(self, table_name, schema=None): - """Return the oid from `table_name` and `schema`.""" - - return self.dialect.get_table_oid(self.bind, table_name, schema, - info_cache=self.info_cache) - - -class CreateEnumType(schema._CreateDropBase): - __visit_name__ = "create_enum_type" - - -class DropEnumType(schema._CreateDropBase): - __visit_name__ = "drop_enum_type" - - -class PGExecutionContext(default.DefaultExecutionContext): - def fire_sequence(self, seq, type_): - return self._execute_scalar(("select nextval('%s')" % \ - self.dialect.identifier_preparer.format_sequence(seq)), type_) - - def get_insert_default(self, column): - if column.primary_key and column is column.table._autoincrement_column: - if column.server_default and column.server_default.has_argument: - - # pre-execute passive defaults on primary key columns - return self._execute_scalar("select %s" % - column.server_default.arg, column.type) - - elif (column.default is None or - (column.default.is_sequence and - column.default.optional)): - - # execute the sequence associated with a SERIAL primary - # key column. for non-primary-key SERIAL, the ID just - # generates server side. - - try: - seq_name = column._postgresql_seq_name - except AttributeError: - tab = column.table.name - col = column.name - tab = tab[0:29 + max(0, (29 - len(col)))] - col = col[0:29 + max(0, (29 - len(tab)))] - name = "%s_%s_seq" % (tab, col) - column._postgresql_seq_name = seq_name = name - - sch = column.table.schema - if sch is not None: - exc = "select nextval('\"%s\".\"%s\"')" % \ - (sch, seq_name) - else: - exc = "select nextval('\"%s\"')" % \ - (seq_name, ) - - return self._execute_scalar(exc, column.type) - - return super(PGExecutionContext, self).get_insert_default(column) - - -class PGDialect(default.DefaultDialect): - name = 'postgresql' - supports_alter = True - max_identifier_length = 63 - supports_sane_rowcount = True - - supports_native_enum = True - supports_native_boolean = True - supports_smallserial = True - - supports_sequences = True - sequences_optional = True - preexecute_autoincrement_sequences = True - postfetch_lastrowid = False - - supports_default_values = True - supports_empty_insert = False - supports_multivalues_insert = True - default_paramstyle = 'pyformat' - ischema_names = ischema_names - colspecs = colspecs - - statement_compiler = PGCompiler - ddl_compiler = PGDDLCompiler - type_compiler = PGTypeCompiler - preparer = PGIdentifierPreparer - execution_ctx_cls = PGExecutionContext - inspector = PGInspector - isolation_level = None - - _backslash_escapes = True - - def __init__(self, isolation_level=None, json_serializer=None, - json_deserializer=None, **kwargs): - default.DefaultDialect.__init__(self, **kwargs) - self.isolation_level = isolation_level - self._json_deserializer = json_deserializer - self._json_serializer = json_serializer - - def initialize(self, connection): - super(PGDialect, self).initialize(connection) - self.implicit_returning = self.server_version_info > (8, 2) and \ - self.__dict__.get('implicit_returning', True) - self.supports_native_enum = self.server_version_info >= (8, 3) - if not self.supports_native_enum: - self.colspecs = self.colspecs.copy() - # pop base Enum type - self.colspecs.pop(sqltypes.Enum, None) - # psycopg2, others may have placed ENUM here as well - self.colspecs.pop(ENUM, None) - - # http://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689 - self.supports_smallserial = self.server_version_info >= (9, 2) - - self._backslash_escapes = connection.scalar( - "show standard_conforming_strings" - ) == 'off' - - def on_connect(self): - if self.isolation_level is not None: - def connect(conn): - self.set_isolation_level(conn, self.isolation_level) - return connect - else: - return None - - _isolation_lookup = set(['SERIALIZABLE', - 'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ']) - - def set_isolation_level(self, connection, level): - level = level.replace('_', ' ') - if level not in self._isolation_lookup: - raise exc.ArgumentError( - "Invalid value '%s' for isolation_level. " - "Valid isolation levels for %s are %s" % - (level, self.name, ", ".join(self._isolation_lookup)) - ) - cursor = connection.cursor() - cursor.execute( - "SET SESSION CHARACTERISTICS AS TRANSACTION " - "ISOLATION LEVEL %s" % level) - cursor.execute("COMMIT") - cursor.close() - - def get_isolation_level(self, connection): - cursor = connection.cursor() - cursor.execute('show transaction isolation level') - val = cursor.fetchone()[0] - cursor.close() - return val.upper() - - def do_begin_twophase(self, connection, xid): - self.do_begin(connection.connection) - - def do_prepare_twophase(self, connection, xid): - connection.execute("PREPARE TRANSACTION '%s'" % xid) - - def do_rollback_twophase(self, connection, xid, - is_prepared=True, recover=False): - if is_prepared: - if recover: - #FIXME: ugly hack to get out of transaction - # context when committing recoverable transactions - # Must find out a way how to make the dbapi not - # open a transaction. - connection.execute("ROLLBACK") - connection.execute("ROLLBACK PREPARED '%s'" % xid) - connection.execute("BEGIN") - self.do_rollback(connection.connection) - else: - self.do_rollback(connection.connection) - - def do_commit_twophase(self, connection, xid, - is_prepared=True, recover=False): - if is_prepared: - if recover: - connection.execute("ROLLBACK") - connection.execute("COMMIT PREPARED '%s'" % xid) - connection.execute("BEGIN") - self.do_rollback(connection.connection) - else: - self.do_commit(connection.connection) - - def do_recover_twophase(self, connection): - resultset = connection.execute( - sql.text("SELECT gid FROM pg_prepared_xacts")) - return [row[0] for row in resultset] - - def _get_default_schema_name(self, connection): - return connection.scalar("select current_schema()") - - def has_schema(self, connection, schema): - query = "select nspname from pg_namespace where lower(nspname)=:schema" - cursor = connection.execute( - sql.text( - query, - bindparams=[ - sql.bindparam( - 'schema', util.text_type(schema.lower()), - type_=sqltypes.Unicode)] - ) - ) - - return bool(cursor.first()) - - def has_table(self, connection, table_name, schema=None): - # seems like case gets folded in pg_class... - if schema is None: - cursor = connection.execute( - sql.text( - "select relname from pg_class c join pg_namespace n on " - "n.oid=c.relnamespace where n.nspname=current_schema() and " - "relname=:name", - bindparams=[ - sql.bindparam('name', util.text_type(table_name), - type_=sqltypes.Unicode)] - ) - ) - else: - cursor = connection.execute( - sql.text( - "select relname from pg_class c join pg_namespace n on " - "n.oid=c.relnamespace where n.nspname=:schema and " - "relname=:name", - bindparams=[ - sql.bindparam('name', - util.text_type(table_name), type_=sqltypes.Unicode), - sql.bindparam('schema', - util.text_type(schema), type_=sqltypes.Unicode)] - ) - ) - return bool(cursor.first()) - - def has_sequence(self, connection, sequence_name, schema=None): - if schema is None: - cursor = connection.execute( - sql.text( - "SELECT relname FROM pg_class c join pg_namespace n on " - "n.oid=c.relnamespace where relkind='S' and " - "n.nspname=current_schema() " - "and relname=:name", - bindparams=[ - sql.bindparam('name', util.text_type(sequence_name), - type_=sqltypes.Unicode) - ] - ) - ) - else: - cursor = connection.execute( - sql.text( - "SELECT relname FROM pg_class c join pg_namespace n on " - "n.oid=c.relnamespace where relkind='S' and " - "n.nspname=:schema and relname=:name", - bindparams=[ - sql.bindparam('name', util.text_type(sequence_name), - type_=sqltypes.Unicode), - sql.bindparam('schema', - util.text_type(schema), type_=sqltypes.Unicode) - ] - ) - ) - - return bool(cursor.first()) - - def has_type(self, connection, type_name, schema=None): - if schema is not None: - query = """ - SELECT EXISTS ( - SELECT * FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n - WHERE t.typnamespace = n.oid - AND t.typname = :typname - AND n.nspname = :nspname - ) - """ - query = sql.text(query) - else: - query = """ - SELECT EXISTS ( - SELECT * FROM pg_catalog.pg_type t - WHERE t.typname = :typname - AND pg_type_is_visible(t.oid) - ) - """ - query = sql.text(query) - query = query.bindparams( - sql.bindparam('typname', - util.text_type(type_name), type_=sqltypes.Unicode), - ) - if schema is not None: - query = query.bindparams( - sql.bindparam('nspname', - util.text_type(schema), type_=sqltypes.Unicode), - ) - cursor = connection.execute(query) - return bool(cursor.scalar()) - - def _get_server_version_info(self, connection): - v = connection.execute("select version()").scalar() - m = re.match( - '.*(?:PostgreSQL|EnterpriseDB) ' - '(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?(?:devel)?', - v) - if not m: - raise AssertionError( - "Could not determine version from string '%s'" % v) - return tuple([int(x) for x in m.group(1, 2, 3) if x is not None]) - - @reflection.cache - def get_table_oid(self, connection, table_name, schema=None, **kw): - """Fetch the oid for schema.table_name. - - Several reflection methods require the table oid. The idea for using - this method is that it can be fetched one time and cached for - subsequent calls. - - """ - table_oid = None - if schema is not None: - schema_where_clause = "n.nspname = :schema" - else: - schema_where_clause = "pg_catalog.pg_table_is_visible(c.oid)" - query = """ - SELECT c.oid - FROM pg_catalog.pg_class c - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE (%s) - AND c.relname = :table_name AND c.relkind in ('r','v') - """ % schema_where_clause - # Since we're binding to unicode, table_name and schema_name must be - # unicode. - table_name = util.text_type(table_name) - if schema is not None: - schema = util.text_type(schema) - s = sql.text(query).bindparams(table_name=sqltypes.Unicode) - s = s.columns(oid=sqltypes.Integer) - if schema: - s = s.bindparams(sql.bindparam('schema', type_=sqltypes.Unicode)) - c = connection.execute(s, table_name=table_name, schema=schema) - table_oid = c.scalar() - if table_oid is None: - raise exc.NoSuchTableError(table_name) - return table_oid - - @reflection.cache - def get_schema_names(self, connection, **kw): - s = """ - SELECT nspname - FROM pg_namespace - ORDER BY nspname - """ - rp = connection.execute(s) - # what about system tables? - - if util.py2k: - schema_names = [row[0].decode(self.encoding) for row in rp \ - if not row[0].startswith('pg_')] - else: - schema_names = [row[0] for row in rp \ - if not row[0].startswith('pg_')] - return schema_names - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - if schema is not None: - current_schema = schema - else: - current_schema = self.default_schema_name - - result = connection.execute( - sql.text("SELECT relname FROM pg_class c " - "WHERE relkind = 'r' " - "AND '%s' = (select nspname from pg_namespace n " - "where n.oid = c.relnamespace) " % - current_schema, - typemap={'relname': sqltypes.Unicode} - ) - ) - return [row[0] for row in result] - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - if schema is not None: - current_schema = schema - else: - current_schema = self.default_schema_name - s = """ - SELECT relname - FROM pg_class c - WHERE relkind = 'v' - AND '%(schema)s' = (select nspname from pg_namespace n - where n.oid = c.relnamespace) - """ % dict(schema=current_schema) - - if util.py2k: - view_names = [row[0].decode(self.encoding) - for row in connection.execute(s)] - else: - view_names = [row[0] for row in connection.execute(s)] - return view_names - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - if schema is not None: - current_schema = schema - else: - current_schema = self.default_schema_name - s = """ - SELECT definition FROM pg_views - WHERE schemaname = :schema - AND viewname = :view_name - """ - rp = connection.execute(sql.text(s), - view_name=view_name, schema=current_schema) - if rp: - if util.py2k: - view_def = rp.scalar().decode(self.encoding) - else: - view_def = rp.scalar() - return view_def - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) - SQL_COLS = """ - SELECT a.attname, - pg_catalog.format_type(a.atttypid, a.atttypmod), - (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) - FROM pg_catalog.pg_attrdef d - WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum - AND a.atthasdef) - AS DEFAULT, - a.attnotnull, a.attnum, a.attrelid as table_oid - FROM pg_catalog.pg_attribute a - WHERE a.attrelid = :table_oid - AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum - """ - s = sql.text(SQL_COLS, - bindparams=[sql.bindparam('table_oid', type_=sqltypes.Integer)], - typemap={'attname': sqltypes.Unicode, 'default': sqltypes.Unicode} - ) - c = connection.execute(s, table_oid=table_oid) - rows = c.fetchall() - domains = self._load_domains(connection) - enums = self._load_enums(connection) - - # format columns - columns = [] - for name, format_type, default, notnull, attnum, table_oid in rows: - column_info = self._get_column_info( - name, format_type, default, notnull, domains, enums, schema) - columns.append(column_info) - return columns - - def _get_column_info(self, name, format_type, default, - notnull, domains, enums, schema): - ## strip (*) from character varying(5), timestamp(5) - # with time zone, geometry(POLYGON), etc. - attype = re.sub(r'\(.*\)', '', format_type) - - # strip '[]' from integer[], etc. - attype = re.sub(r'\[\]', '', attype) - - nullable = not notnull - is_array = format_type.endswith('[]') - charlen = re.search('\(([\d,]+)\)', format_type) - if charlen: - charlen = charlen.group(1) - args = re.search('\((.*)\)', format_type) - if args and args.group(1): - args = tuple(re.split('\s*,\s*', args.group(1))) - else: - args = () - kwargs = {} - - if attype == 'numeric': - if charlen: - prec, scale = charlen.split(',') - args = (int(prec), int(scale)) - else: - args = () - elif attype == 'double precision': - args = (53, ) - elif attype == 'integer': - args = () - elif attype in ('timestamp with time zone', - 'time with time zone'): - kwargs['timezone'] = True - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif attype in ('timestamp without time zone', - 'time without time zone', 'time'): - kwargs['timezone'] = False - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif attype == 'bit varying': - kwargs['varying'] = True - if charlen: - args = (int(charlen),) - else: - args = () - elif attype in ('interval', 'interval year to month', - 'interval day to second'): - if charlen: - kwargs['precision'] = int(charlen) - args = () - elif charlen: - args = (int(charlen),) - - while True: - if attype in self.ischema_names: - coltype = self.ischema_names[attype] - break - elif attype in enums: - enum = enums[attype] - coltype = ENUM - if "." in attype: - kwargs['schema'], kwargs['name'] = attype.split('.') - else: - kwargs['name'] = attype - args = tuple(enum['labels']) - break - elif attype in domains: - domain = domains[attype] - attype = domain['attype'] - # A table can't override whether the domain is nullable. - nullable = domain['nullable'] - if domain['default'] and not default: - # It can, however, override the default - # value, but can't set it to null. - default = domain['default'] - continue - else: - coltype = None - break - - if coltype: - coltype = coltype(*args, **kwargs) - if is_array: - coltype = ARRAY(coltype) - else: - util.warn("Did not recognize type '%s' of column '%s'" % - (attype, name)) - coltype = sqltypes.NULLTYPE - # adjust the default value - autoincrement = False - if default is not None: - match = re.search(r"""(nextval\(')([^']+)('.*$)""", default) - if match is not None: - autoincrement = True - # the default is related to a Sequence - sch = schema - if '.' not in match.group(2) and sch is not None: - # unconditionally quote the schema name. this could - # later be enhanced to obey quoting rules / - # "quote schema" - default = match.group(1) + \ - ('"%s"' % sch) + '.' + \ - match.group(2) + match.group(3) - - column_info = dict(name=name, type=coltype, nullable=nullable, - default=default, autoincrement=autoincrement) - return column_info - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) - - if self.server_version_info < (8, 4): - # unnest() and generate_subscripts() both introduced in - # version 8.4 - PK_SQL = """ - SELECT a.attname - FROM - pg_class t - join pg_index ix on t.oid = ix.indrelid - join pg_attribute a - on t.oid=a.attrelid and a.attnum=ANY(ix.indkey) - WHERE - t.oid = :table_oid and ix.indisprimary = 't' - ORDER BY a.attnum - """ - else: - PK_SQL = """ - SELECT a.attname - FROM pg_attribute a JOIN ( - SELECT unnest(ix.indkey) attnum, - generate_subscripts(ix.indkey, 1) ord - FROM pg_index ix - WHERE ix.indrelid = :table_oid AND ix.indisprimary - ) k ON a.attnum=k.attnum - WHERE a.attrelid = :table_oid - ORDER BY k.ord - """ - t = sql.text(PK_SQL, typemap={'attname': sqltypes.Unicode}) - c = connection.execute(t, table_oid=table_oid) - cols = [r[0] for r in c.fetchall()] - - PK_CONS_SQL = """ - SELECT conname - FROM pg_catalog.pg_constraint r - WHERE r.conrelid = :table_oid AND r.contype = 'p' - ORDER BY 1 - """ - t = sql.text(PK_CONS_SQL, typemap={'conname': sqltypes.Unicode}) - c = connection.execute(t, table_oid=table_oid) - name = c.scalar() - - return {'constrained_columns': cols, 'name': name} - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - preparer = self.identifier_preparer - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) - - FK_SQL = """ - SELECT r.conname, - pg_catalog.pg_get_constraintdef(r.oid, true) as condef, - n.nspname as conschema - FROM pg_catalog.pg_constraint r, - pg_namespace n, - pg_class c - - WHERE r.conrelid = :table AND - r.contype = 'f' AND - c.oid = confrelid AND - n.oid = c.relnamespace - ORDER BY 1 - """ - # http://www.postgresql.org/docs/9.0/static/sql-createtable.html - FK_REGEX = re.compile( - r'FOREIGN KEY \((.*?)\) REFERENCES (?:(.*?)\.)?(.*?)\((.*?)\)' - r'[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?' - r'[\s]?(ON UPDATE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?' - r'[\s]?(ON DELETE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?' - r'[\s]?(DEFERRABLE|NOT DEFERRABLE)?' - r'[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?' - ) - - t = sql.text(FK_SQL, typemap={ - 'conname': sqltypes.Unicode, - 'condef': sqltypes.Unicode}) - c = connection.execute(t, table=table_oid) - fkeys = [] - for conname, condef, conschema in c.fetchall(): - m = re.search(FK_REGEX, condef).groups() - constrained_columns, referred_schema, \ - referred_table, referred_columns, \ - _, match, _, onupdate, _, ondelete, \ - deferrable, _, initially = m - if deferrable is not None: - deferrable = True if deferrable == 'DEFERRABLE' else False - constrained_columns = [preparer._unquote_identifier(x) - for x in re.split(r'\s*,\s*', constrained_columns)] - - if referred_schema: - referred_schema = \ - preparer._unquote_identifier(referred_schema) - elif schema is not None and schema == conschema: - # no schema was returned by pg_get_constraintdef(). This - # means the schema is in the search path. We will leave - # it as None, unless the actual schema, which we pull out - # from pg_namespace even though pg_get_constraintdef() doesn't - # want to give it to us, matches that of the referencing table, - # and an explicit schema was given for the referencing table. - referred_schema = schema - referred_table = preparer._unquote_identifier(referred_table) - referred_columns = [preparer._unquote_identifier(x) - for x in re.split(r'\s*,\s', referred_columns)] - fkey_d = { - 'name': conname, - 'constrained_columns': constrained_columns, - 'referred_schema': referred_schema, - 'referred_table': referred_table, - 'referred_columns': referred_columns, - 'options': { - 'onupdate': onupdate, - 'ondelete': ondelete, - 'deferrable': deferrable, - 'initially': initially, - 'match': match - } - } - fkeys.append(fkey_d) - return fkeys - - @reflection.cache - def get_indexes(self, connection, table_name, schema, **kw): - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) - - # cast indkey as varchar since it's an int2vector, - # returned as a list by some drivers such as pypostgresql - - IDX_SQL = """ - SELECT - i.relname as relname, - ix.indisunique, ix.indexprs, ix.indpred, - a.attname, a.attnum, ix.indkey::varchar - FROM - pg_class t - join pg_index ix on t.oid = ix.indrelid - join pg_class i on i.oid=ix.indexrelid - left outer join - pg_attribute a - on t.oid=a.attrelid and a.attnum=ANY(ix.indkey) - WHERE - t.relkind = 'r' - and t.oid = :table_oid - and ix.indisprimary = 'f' - ORDER BY - t.relname, - i.relname - """ - - t = sql.text(IDX_SQL, typemap={'attname': sqltypes.Unicode}) - c = connection.execute(t, table_oid=table_oid) - - indexes = defaultdict(lambda: defaultdict(dict)) - - sv_idx_name = None - for row in c.fetchall(): - idx_name, unique, expr, prd, col, col_num, idx_key = row - - if expr: - if idx_name != sv_idx_name: - util.warn( - "Skipped unsupported reflection of " - "expression-based index %s" - % idx_name) - sv_idx_name = idx_name - continue - - if prd and not idx_name == sv_idx_name: - util.warn( - "Predicate of partial index %s ignored during reflection" - % idx_name) - sv_idx_name = idx_name - - index = indexes[idx_name] - if col is not None: - index['cols'][col_num] = col - index['key'] = [int(k.strip()) for k in idx_key.split()] - index['unique'] = unique - - return [ - {'name': name, - 'unique': idx['unique'], - 'column_names': [idx['cols'][i] for i in idx['key']]} - for name, idx in indexes.items() - ] - - @reflection.cache - def get_unique_constraints(self, connection, table_name, - schema=None, **kw): - table_oid = self.get_table_oid(connection, table_name, schema, - info_cache=kw.get('info_cache')) - - UNIQUE_SQL = """ - SELECT - cons.conname as name, - cons.conkey as key, - a.attnum as col_num, - a.attname as col_name - FROM - pg_catalog.pg_constraint cons - join pg_attribute a - on cons.conrelid = a.attrelid AND a.attnum = ANY(cons.conkey) - WHERE - cons.conrelid = :table_oid AND - cons.contype = 'u' - """ - - t = sql.text(UNIQUE_SQL, typemap={'col_name': sqltypes.Unicode}) - c = connection.execute(t, table_oid=table_oid) - - uniques = defaultdict(lambda: defaultdict(dict)) - for row in c.fetchall(): - uc = uniques[row.name] - uc["key"] = row.key - uc["cols"][row.col_num] = row.col_name - - return [ - {'name': name, - 'column_names': [uc["cols"][i] for i in uc["key"]]} - for name, uc in uniques.items() - ] - - def _load_enums(self, connection): - if not self.supports_native_enum: - return {} - - ## Load data types for enums: - SQL_ENUMS = """ - SELECT t.typname as "name", - -- no enum defaults in 8.4 at least - -- t.typdefault as "default", - pg_catalog.pg_type_is_visible(t.oid) as "visible", - n.nspname as "schema", - e.enumlabel as "label" - FROM pg_catalog.pg_type t - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - LEFT JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid - WHERE t.typtype = 'e' - ORDER BY "name", e.oid -- e.oid gives us label order - """ - - s = sql.text(SQL_ENUMS, typemap={ - 'attname': sqltypes.Unicode, - 'label': sqltypes.Unicode}) - c = connection.execute(s) - - enums = {} - for enum in c.fetchall(): - if enum['visible']: - # 'visible' just means whether or not the enum is in a - # schema that's on the search path -- or not overridden by - # a schema with higher precedence. If it's not visible, - # it will be prefixed with the schema-name when it's used. - name = enum['name'] - else: - name = "%s.%s" % (enum['schema'], enum['name']) - - if name in enums: - enums[name]['labels'].append(enum['label']) - else: - enums[name] = { - 'labels': [enum['label']], - } - - return enums - - def _load_domains(self, connection): - ## Load data types for domains: - SQL_DOMAINS = """ - SELECT t.typname as "name", - pg_catalog.format_type(t.typbasetype, t.typtypmod) as "attype", - not t.typnotnull as "nullable", - t.typdefault as "default", - pg_catalog.pg_type_is_visible(t.oid) as "visible", - n.nspname as "schema" - FROM pg_catalog.pg_type t - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - WHERE t.typtype = 'd' - """ - - s = sql.text(SQL_DOMAINS, typemap={'attname': sqltypes.Unicode}) - c = connection.execute(s) - - domains = {} - for domain in c.fetchall(): - ## strip (30) from character varying(30) - attype = re.search('([^\(]+)', domain['attype']).group(1) - if domain['visible']: - # 'visible' just means whether or not the domain is in a - # schema that's on the search path -- or not overridden by - # a schema with higher precedence. If it's not visible, - # it will be prefixed with the schema-name when it's used. - name = domain['name'] - else: - name = "%s.%s" % (domain['schema'], domain['name']) - - domains[name] = { - 'attype': attype, - 'nullable': domain['nullable'], - 'default': domain['default'] - } - - return domains diff --git a/libs/sqlalchemy/dialects/postgresql/constraints.py b/libs/sqlalchemy/dialects/postgresql/constraints.py deleted file mode 100644 index f45cef1a..00000000 --- a/libs/sqlalchemy/dialects/postgresql/constraints.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy.schema import ColumnCollectionConstraint -from sqlalchemy.sql import expression - -class ExcludeConstraint(ColumnCollectionConstraint): - """A table-level EXCLUDE constraint. - - Defines an EXCLUDE constraint as described in the `postgres - documentation`__. - - __ http://www.postgresql.org/docs/9.0/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE - """ - - __visit_name__ = 'exclude_constraint' - - where = None - - def __init__(self, *elements, **kw): - """ - :param \*elements: - A sequence of two tuples of the form ``(column, operator)`` where - column must be a column name or Column object and operator must - be a string containing the operator to use. - - :param name: - Optional, the in-database name of this constraint. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - :param using: - Optional string. If set, emit USING when issuing DDL - for this constraint. Defaults to 'gist'. - - :param where: - Optional string. If set, emit WHERE when issuing DDL - for this constraint. - - """ - ColumnCollectionConstraint.__init__( - self, - *[col for col, op in elements], - name=kw.get('name'), - deferrable=kw.get('deferrable'), - initially=kw.get('initially') - ) - self.operators = {} - for col_or_string, op in elements: - name = getattr(col_or_string, 'name', col_or_string) - self.operators[name] = op - self.using = kw.get('using', 'gist') - where = kw.get('where') - if where: - self.where = expression._literal_as_text(where) - - def copy(self, **kw): - elements = [(col, self.operators[col]) - for col in self.columns.keys()] - c = self.__class__(*elements, - name=self.name, - deferrable=self.deferrable, - initially=self.initially) - c.dispatch._update(self.dispatch) - return c - diff --git a/libs/sqlalchemy/dialects/postgresql/hstore.py b/libs/sqlalchemy/dialects/postgresql/hstore.py deleted file mode 100644 index 76562088..00000000 --- a/libs/sqlalchemy/dialects/postgresql/hstore.py +++ /dev/null @@ -1,369 +0,0 @@ -# postgresql/hstore.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -import re - -from .base import ARRAY, ischema_names -from ... import types as sqltypes -from ...sql import functions as sqlfunc -from ...sql.operators import custom_op -from ... import util - -__all__ = ('HSTORE', 'hstore') - -# My best guess at the parsing rules of hstore literals, since no formal -# grammar is given. This is mostly reverse engineered from PG's input parser -# behavior. -HSTORE_PAIR_RE = re.compile(r""" -( - "(?P (\\ . | [^"])* )" # Quoted key -) -[ ]* => [ ]* # Pair operator, optional adjoining whitespace -( - (?P NULL ) # NULL value - | "(?P (\\ . | [^"])* )" # Quoted value -) -""", re.VERBOSE) - -HSTORE_DELIMITER_RE = re.compile(r""" -[ ]* , [ ]* -""", re.VERBOSE) - - -def _parse_error(hstore_str, pos): - """format an unmarshalling error.""" - - ctx = 20 - hslen = len(hstore_str) - - parsed_tail = hstore_str[max(pos - ctx - 1, 0):min(pos, hslen)] - residual = hstore_str[min(pos, hslen):min(pos + ctx + 1, hslen)] - - if len(parsed_tail) > ctx: - parsed_tail = '[...]' + parsed_tail[1:] - if len(residual) > ctx: - residual = residual[:-1] + '[...]' - - return "After %r, could not parse residual at position %d: %r" % ( - parsed_tail, pos, residual) - - -def _parse_hstore(hstore_str): - """Parse an hstore from it's literal string representation. - - Attempts to approximate PG's hstore input parsing rules as closely as - possible. Although currently this is not strictly necessary, since the - current implementation of hstore's output syntax is stricter than what it - accepts as input, the documentation makes no guarantees that will always - be the case. - - - - """ - result = {} - pos = 0 - pair_match = HSTORE_PAIR_RE.match(hstore_str) - - while pair_match is not None: - key = pair_match.group('key').replace(r'\"', '"').replace("\\\\", "\\") - if pair_match.group('value_null'): - value = None - else: - value = pair_match.group('value').replace(r'\"', '"').replace("\\\\", "\\") - result[key] = value - - pos += pair_match.end() - - delim_match = HSTORE_DELIMITER_RE.match(hstore_str[pos:]) - if delim_match is not None: - pos += delim_match.end() - - pair_match = HSTORE_PAIR_RE.match(hstore_str[pos:]) - - if pos != len(hstore_str): - raise ValueError(_parse_error(hstore_str, pos)) - - return result - - -def _serialize_hstore(val): - """Serialize a dictionary into an hstore literal. Keys and values must - both be strings (except None for values). - - """ - def esc(s, position): - if position == 'value' and s is None: - return 'NULL' - elif isinstance(s, util.string_types): - return '"%s"' % s.replace("\\", "\\\\").replace('"', r'\"') - else: - raise ValueError("%r in %s position is not a string." % - (s, position)) - - return ', '.join('%s=>%s' % (esc(k, 'key'), esc(v, 'value')) - for k, v in val.items()) - - -class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine): - """Represent the Postgresql HSTORE type. - - The :class:`.HSTORE` type stores dictionaries containing strings, e.g.:: - - data_table = Table('data_table', metadata, - Column('id', Integer, primary_key=True), - Column('data', HSTORE) - ) - - with engine.connect() as conn: - conn.execute( - data_table.insert(), - data = {"key1": "value1", "key2": "value2"} - ) - - :class:`.HSTORE` provides for a wide range of operations, including: - - * Index operations:: - - data_table.c.data['some key'] == 'some value' - - * Containment operations:: - - data_table.c.data.has_key('some key') - - data_table.c.data.has_all(['one', 'two', 'three']) - - * Concatenation:: - - data_table.c.data + {"k1": "v1"} - - For a full list of special methods see :class:`.HSTORE.comparator_factory`. - - For usage with the SQLAlchemy ORM, it may be desirable to combine - the usage of :class:`.HSTORE` with :class:`.MutableDict` dictionary - now part of the :mod:`sqlalchemy.ext.mutable` - extension. This extension will allow "in-place" changes to the - dictionary, e.g. addition of new keys or replacement/removal of existing - keys to/from the current dictionary, to produce events which will be detected - by the unit of work:: - - from sqlalchemy.ext.mutable import MutableDict - - class MyClass(Base): - __tablename__ = 'data_table' - - id = Column(Integer, primary_key=True) - data = Column(MutableDict.as_mutable(HSTORE)) - - my_object = session.query(MyClass).one() - - # in-place mutation, requires Mutable extension - # in order for the ORM to detect - my_object.data['some_key'] = 'some value' - - session.commit() - - When the :mod:`sqlalchemy.ext.mutable` extension is not used, the ORM - will not be alerted to any changes to the contents of an existing dictionary, - unless that dictionary value is re-assigned to the HSTORE-attribute itself, - thus generating a change event. - - .. versionadded:: 0.8 - - .. seealso:: - - :class:`.hstore` - render the Postgresql ``hstore()`` function. - - - """ - - __visit_name__ = 'HSTORE' - - class comparator_factory(sqltypes.Concatenable.Comparator): - """Define comparison operations for :class:`.HSTORE`.""" - - def has_key(self, other): - """Boolean expression. Test for presence of a key. Note that the - key may be a SQLA expression. - """ - return self.expr.op('?')(other) - - def has_all(self, other): - """Boolean expression. Test for presence of all keys in the PG - array. - """ - return self.expr.op('?&')(other) - - def has_any(self, other): - """Boolean expression. Test for presence of any key in the PG - array. - """ - return self.expr.op('?|')(other) - - def defined(self, key): - """Boolean expression. Test for presence of a non-NULL value for - the key. Note that the key may be a SQLA expression. - """ - return _HStoreDefinedFunction(self.expr, key) - - def contains(self, other, **kwargs): - """Boolean expression. Test if keys are a superset of the keys of - the argument hstore expression. - """ - return self.expr.op('@>')(other) - - def contained_by(self, other): - """Boolean expression. Test if keys are a proper subset of the - keys of the argument hstore expression. - """ - return self.expr.op('<@')(other) - - def __getitem__(self, other): - """Text expression. Get the value at a given key. Note that the - key may be a SQLA expression. - """ - return self.expr.op('->', precedence=5)(other) - - def delete(self, key): - """HStore expression. Returns the contents of this hstore with the - given key deleted. Note that the key may be a SQLA expression. - """ - if isinstance(key, dict): - key = _serialize_hstore(key) - return _HStoreDeleteFunction(self.expr, key) - - def slice(self, array): - """HStore expression. Returns a subset of an hstore defined by - array of keys. - """ - return _HStoreSliceFunction(self.expr, array) - - def keys(self): - """Text array expression. Returns array of keys.""" - return _HStoreKeysFunction(self.expr) - - def vals(self): - """Text array expression. Returns array of values.""" - return _HStoreValsFunction(self.expr) - - def array(self): - """Text array expression. Returns array of alternating keys and - values. - """ - return _HStoreArrayFunction(self.expr) - - def matrix(self): - """Text array expression. Returns array of [key, value] pairs.""" - return _HStoreMatrixFunction(self.expr) - - def _adapt_expression(self, op, other_comparator): - if isinstance(op, custom_op): - if op.opstring in ['?', '?&', '?|', '@>', '<@']: - return op, sqltypes.Boolean - elif op.opstring == '->': - return op, sqltypes.Text - return sqltypes.Concatenable.Comparator.\ - _adapt_expression(self, op, other_comparator) - - def bind_processor(self, dialect): - if util.py2k: - encoding = dialect.encoding - def process(value): - if isinstance(value, dict): - return _serialize_hstore(value).encode(encoding) - else: - return value - else: - def process(value): - if isinstance(value, dict): - return _serialize_hstore(value) - else: - return value - return process - - def result_processor(self, dialect, coltype): - if util.py2k: - encoding = dialect.encoding - def process(value): - if value is not None: - return _parse_hstore(value.decode(encoding)) - else: - return value - else: - def process(value): - if value is not None: - return _parse_hstore(value) - else: - return value - return process - - -ischema_names['hstore'] = HSTORE - - -class hstore(sqlfunc.GenericFunction): - """Construct an hstore value within a SQL expression using the - Postgresql ``hstore()`` function. - - The :class:`.hstore` function accepts one or two arguments as described - in the Postgresql documentation. - - E.g.:: - - from sqlalchemy.dialects.postgresql import array, hstore - - select([hstore('key1', 'value1')]) - - select([ - hstore( - array(['key1', 'key2', 'key3']), - array(['value1', 'value2', 'value3']) - ) - ]) - - .. versionadded:: 0.8 - - .. seealso:: - - :class:`.HSTORE` - the Postgresql ``HSTORE`` datatype. - - """ - type = HSTORE - name = 'hstore' - - -class _HStoreDefinedFunction(sqlfunc.GenericFunction): - type = sqltypes.Boolean - name = 'defined' - - -class _HStoreDeleteFunction(sqlfunc.GenericFunction): - type = HSTORE - name = 'delete' - - -class _HStoreSliceFunction(sqlfunc.GenericFunction): - type = HSTORE - name = 'slice' - - -class _HStoreKeysFunction(sqlfunc.GenericFunction): - type = ARRAY(sqltypes.Text) - name = 'akeys' - - -class _HStoreValsFunction(sqlfunc.GenericFunction): - type = ARRAY(sqltypes.Text) - name = 'avals' - - -class _HStoreArrayFunction(sqlfunc.GenericFunction): - type = ARRAY(sqltypes.Text) - name = 'hstore_to_array' - - -class _HStoreMatrixFunction(sqlfunc.GenericFunction): - type = ARRAY(sqltypes.Text) - name = 'hstore_to_matrix' diff --git a/libs/sqlalchemy/dialects/postgresql/json.py b/libs/sqlalchemy/dialects/postgresql/json.py deleted file mode 100644 index 2e29185e..00000000 --- a/libs/sqlalchemy/dialects/postgresql/json.py +++ /dev/null @@ -1,199 +0,0 @@ -# postgresql/json.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -from __future__ import absolute_import - -import json - -from .base import ischema_names -from ... import types as sqltypes -from ...sql.operators import custom_op -from ... import sql -from ...sql import elements -from ... import util - -__all__ = ('JSON', 'JSONElement') - - -class JSONElement(elements.BinaryExpression): - """Represents accessing an element of a :class:`.JSON` value. - - The :class:`.JSONElement` is produced whenever using the Python index - operator on an expression that has the type :class:`.JSON`:: - - expr = mytable.c.json_data['some_key'] - - The expression typically compiles to a JSON access such as ``col -> key``. - Modifiers are then available for typing behavior, including :meth:`.JSONElement.cast` - and :attr:`.JSONElement.astext`. - - """ - def __init__(self, left, right, astext=False, opstring=None, result_type=None): - self._astext = astext - if opstring is None: - if hasattr(right, '__iter__') and \ - not isinstance(right, util.string_types): - opstring = "#>" - right = "{%s}" % (", ".join(util.text_type(elem) for elem in right)) - else: - opstring = "->" - - self._json_opstring = opstring - operator = custom_op(opstring, precedence=5) - right = left._check_literal(left, operator, right) - super(JSONElement, self).__init__(left, right, operator, type_=result_type) - - @property - def astext(self): - """Convert this :class:`.JSONElement` to use the 'astext' operator - when evaluated. - - E.g.:: - - select([data_table.c.data['some key'].astext]) - - .. seealso:: - - :meth:`.JSONElement.cast` - - """ - if self._astext: - return self - else: - return JSONElement( - self.left, - self.right, - astext=True, - opstring=self._json_opstring + ">", - result_type=sqltypes.String(convert_unicode=True) - ) - - def cast(self, type_): - """Convert this :class:`.JSONElement` to apply both the 'astext' operator - as well as an explicit type cast when evaulated. - - E.g.:: - - select([data_table.c.data['some key'].cast(Integer)]) - - .. seealso:: - - :attr:`.JSONElement.astext` - - """ - if not self._astext: - return self.astext.cast(type_) - else: - return sql.cast(self, type_) - - -class JSON(sqltypes.TypeEngine): - """Represent the Postgresql JSON type. - - The :class:`.JSON` type stores arbitrary JSON format data, e.g.:: - - data_table = Table('data_table', metadata, - Column('id', Integer, primary_key=True), - Column('data', JSON) - ) - - with engine.connect() as conn: - conn.execute( - data_table.insert(), - data = {"key1": "value1", "key2": "value2"} - ) - - :class:`.JSON` provides several operations: - - * Index operations:: - - data_table.c.data['some key'] - - * Index operations returning text (required for text comparison):: - - data_table.c.data['some key'].astext == 'some value' - - * Index operations with a built-in CAST call:: - - data_table.c.data['some key'].cast(Integer) == 5 - - * Path index operations:: - - data_table.c.data[('key_1', 'key_2', ..., 'key_n')] - - * Path index operations returning text (required for text comparison):: - - data_table.c.data[('key_1', 'key_2', ..., 'key_n')].astext == 'some value' - - Index operations return an instance of :class:`.JSONElement`, which represents - an expression such as ``column -> index``. This element then defines - methods such as :attr:`.JSONElement.astext` and :meth:`.JSONElement.cast` - for setting up type behavior. - - The :class:`.JSON` type, when used with the SQLAlchemy ORM, does not detect - in-place mutations to the structure. In order to detect these, the - :mod:`sqlalchemy.ext.mutable` extension must be used. This extension will - allow "in-place" changes to the datastructure to produce events which - will be detected by the unit of work. See the example at :class:`.HSTORE` - for a simple example involving a dictionary. - - Custom serializers and deserializers are specified at the dialect level, - that is using :func:`.create_engine`. The reason for this is that when - using psycopg2, the DBAPI only allows serializers at the per-cursor - or per-connection level. E.g.:: - - engine = create_engine("postgresql://scott:tiger@localhost/test", - json_serializer=my_serialize_fn, - json_deserializer=my_deserialize_fn - ) - - When using the psycopg2 dialect, the json_deserializer is registered - against the database using ``psycopg2.extras.register_default_json``. - - .. versionadded:: 0.9 - - """ - - __visit_name__ = 'JSON' - - class comparator_factory(sqltypes.Concatenable.Comparator): - """Define comparison operations for :class:`.JSON`.""" - - def __getitem__(self, other): - """Get the value at a given key.""" - - return JSONElement(self.expr, other) - - def _adapt_expression(self, op, other_comparator): - if isinstance(op, custom_op): - if op.opstring == '->': - return op, sqltypes.Text - return sqltypes.Concatenable.Comparator.\ - _adapt_expression(self, op, other_comparator) - - def bind_processor(self, dialect): - json_serializer = dialect._json_serializer or json.dumps - if util.py2k: - encoding = dialect.encoding - def process(value): - return json_serializer(value).encode(encoding) - else: - def process(value): - return json_serializer(value) - return process - - def result_processor(self, dialect, coltype): - json_deserializer = dialect._json_deserializer or json.loads - if util.py2k: - encoding = dialect.encoding - def process(value): - return json_deserializer(value.decode(encoding)) - else: - def process(value): - return json_deserializer(value) - return process - - -ischema_names['json'] = JSON diff --git a/libs/sqlalchemy/dialects/postgresql/pg8000.py b/libs/sqlalchemy/dialects/postgresql/pg8000.py deleted file mode 100644 index bc73f975..00000000 --- a/libs/sqlalchemy/dialects/postgresql/pg8000.py +++ /dev/null @@ -1,126 +0,0 @@ -# postgresql/pg8000.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: postgresql+pg8000 - :name: pg8000 - :dbapi: pg8000 - :connectstring: postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...] - :url: http://pybrary.net/pg8000/ - -Unicode -------- - -pg8000 requires that the postgresql client encoding be -configured in the postgresql.conf file in order to use encodings -other than ascii. Set this value to the same value as the -"encoding" parameter on create_engine(), usually "utf-8". - -Interval --------- - -Passing data from/to the Interval type is not supported as of -yet. - -""" -from ... import util, exc -import decimal -from ... import processors -from ... import types as sqltypes -from .base import PGDialect, \ - PGCompiler, PGIdentifierPreparer, PGExecutionContext,\ - _DECIMAL_TYPES, _FLOAT_TYPES, _INT_TYPES - - -class _PGNumeric(sqltypes.Numeric): - def result_processor(self, dialect, coltype): - if self.asdecimal: - if coltype in _FLOAT_TYPES: - return processors.to_decimal_processor_factory( - decimal.Decimal, - self._effective_decimal_return_scale) - elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: - # pg8000 returns Decimal natively for 1700 - return None - else: - raise exc.InvalidRequestError( - "Unknown PG numeric type: %d" % coltype) - else: - if coltype in _FLOAT_TYPES: - # pg8000 returns float natively for 701 - return None - elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: - return processors.to_float - else: - raise exc.InvalidRequestError( - "Unknown PG numeric type: %d" % coltype) - - -class _PGNumericNoBind(_PGNumeric): - def bind_processor(self, dialect): - return None - - -class PGExecutionContext_pg8000(PGExecutionContext): - pass - - -class PGCompiler_pg8000(PGCompiler): - def visit_mod_binary(self, binary, operator, **kw): - return self.process(binary.left, **kw) + " %% " + \ - self.process(binary.right, **kw) - - def post_process_text(self, text): - if '%%' in text: - util.warn("The SQLAlchemy postgresql dialect " - "now automatically escapes '%' in text() " - "expressions to '%%'.") - return text.replace('%', '%%') - - -class PGIdentifierPreparer_pg8000(PGIdentifierPreparer): - def _escape_identifier(self, value): - value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace('%', '%%') - - -class PGDialect_pg8000(PGDialect): - driver = 'pg8000' - - supports_unicode_statements = True - - supports_unicode_binds = True - - default_paramstyle = 'format' - supports_sane_multi_rowcount = False - execution_ctx_cls = PGExecutionContext_pg8000 - statement_compiler = PGCompiler_pg8000 - preparer = PGIdentifierPreparer_pg8000 - description_encoding = 'use_encoding' - - colspecs = util.update_copy( - PGDialect.colspecs, - { - sqltypes.Numeric: _PGNumericNoBind, - sqltypes.Float: _PGNumeric - } - ) - - @classmethod - def dbapi(cls): - return __import__('pg8000').dbapi - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - if 'port' in opts: - opts['port'] = int(opts['port']) - opts.update(url.query) - return ([], opts) - - def is_disconnect(self, e, connection, cursor): - return "connection is closed" in str(e) - -dialect = PGDialect_pg8000 diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg2.py b/libs/sqlalchemy/dialects/postgresql/psycopg2.py deleted file mode 100644 index e9f64f82..00000000 --- a/libs/sqlalchemy/dialects/postgresql/psycopg2.py +++ /dev/null @@ -1,484 +0,0 @@ -# postgresql/psycopg2.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: postgresql+psycopg2 - :name: psycopg2 - :dbapi: psycopg2 - :connectstring: postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...] - :url: http://pypi.python.org/pypi/psycopg2/ - -psycopg2 Connect Arguments ------------------------------------ - -psycopg2-specific keyword arguments which are accepted by -:func:`.create_engine()` are: - -* ``server_side_cursors``: Enable the usage of "server side cursors" for SQL - statements which support this feature. What this essentially means from a - psycopg2 point of view is that the cursor is created using a name, e.g. - ``connection.cursor('some name')``, which has the effect that result rows are - not immediately pre-fetched and buffered after statement execution, but are - instead left on the server and only retrieved as needed. SQLAlchemy's - :class:`~sqlalchemy.engine.ResultProxy` uses special row-buffering - behavior when this feature is enabled, such that groups of 100 rows at a - time are fetched over the wire to reduce conversational overhead. - Note that the ``stream_results=True`` execution option is a more targeted - way of enabling this mode on a per-execution basis. -* ``use_native_unicode``: Enable the usage of Psycopg2 "native unicode" mode - per connection. True by default. -* ``isolation_level``: This option, available for all Posgtresql dialects, - includes the ``AUTOCOMMIT`` isolation level when using the psycopg2 - dialect. See :ref:`psycopg2_isolation_level`. - - -Unix Domain Connections ------------------------- - -psycopg2 supports connecting via Unix domain connections. When the ``host`` -portion of the URL is omitted, SQLAlchemy passes ``None`` to psycopg2, -which specifies Unix-domain communication rather than TCP/IP communication:: - - create_engine("postgresql+psycopg2://user:password@/dbname") - -By default, the socket file used is to connect to a Unix-domain socket -in ``/tmp``, or whatever socket directory was specified when PostgreSQL -was built. This value can be overridden by passing a pathname to psycopg2, -using ``host`` as an additional keyword argument:: - - create_engine("postgresql+psycopg2://user:password@/dbname?host=/var/lib/postgresql") - -See also: - -`PQconnectdbParams `_ - -Per-Statement/Connection Execution Options -------------------------------------------- - -The following DBAPI-specific options are respected when used with -:meth:`.Connection.execution_options`, :meth:`.Executable.execution_options`, -:meth:`.Query.execution_options`, in addition to those not specific to DBAPIs: - -* isolation_level - Set the transaction isolation level for the lifespan of a - :class:`.Connection` (can only be set on a connection, not a statement - or query). See :ref:`psycopg2_isolation_level`. - -* stream_results - Enable or disable usage of psycopg2 server side cursors - - this feature makes use of "named" cursors in combination with special - result handling methods so that result rows are not fully buffered. - If ``None`` or not set, the ``server_side_cursors`` option of the - :class:`.Engine` is used. - -Unicode -------- - -By default, the psycopg2 driver uses the ``psycopg2.extensions.UNICODE`` -extension, such that the DBAPI receives and returns all strings as Python -Unicode objects directly - SQLAlchemy passes these values through without -change. Psycopg2 here will encode/decode string values based on the -current "client encoding" setting; by default this is the value in -the ``postgresql.conf`` file, which often defaults to ``SQL_ASCII``. -Typically, this can be changed to ``utf-8``, as a more useful default:: - - #client_encoding = sql_ascii # actually, defaults to database - # encoding - client_encoding = utf8 - -A second way to affect the client encoding is to set it within Psycopg2 -locally. SQLAlchemy will call psycopg2's ``set_client_encoding()`` -method (see: http://initd.org/psycopg/docs/connection.html#connection.set_client_encoding) -on all new connections based on the value passed to -:func:`.create_engine` using the ``client_encoding`` parameter:: - - engine = create_engine("postgresql://user:pass@host/dbname", client_encoding='utf8') - -This overrides the encoding specified in the Postgresql client configuration. - -.. versionadded:: 0.7.3 - The psycopg2-specific ``client_encoding`` parameter to - :func:`.create_engine`. - -SQLAlchemy can also be instructed to skip the usage of the psycopg2 -``UNICODE`` extension and to instead utilize it's own unicode encode/decode -services, which are normally reserved only for those DBAPIs that don't -fully support unicode directly. Passing ``use_native_unicode=False`` to -:func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``. -SQLAlchemy will instead encode data itself into Python bytestrings on the way -in and coerce from bytes on the way back, -using the value of the :func:`.create_engine` ``encoding`` parameter, which -defaults to ``utf-8``. -SQLAlchemy's own unicode encode/decode functionality is steadily becoming -obsolete as more DBAPIs support unicode fully along with the approach of -Python 3; in modern usage psycopg2 should be relied upon to handle unicode. - -Transactions ------------- - -The psycopg2 dialect fully supports SAVEPOINT and two-phase commit operations. - -.. _psycopg2_isolation_level: - -Psycopg2 Transaction Isolation Level -------------------------------------- - -As discussed in :ref:`postgresql_isolation_level`, -all Postgresql dialects support setting of transaction isolation level -both via the ``isolation_level`` parameter passed to :func:`.create_engine`, -as well as the ``isolation_level`` argument used by :meth:`.Connection.execution_options`. -When using the psycopg2 dialect, these options make use of -psycopg2's ``set_isolation_level()`` connection method, rather than -emitting a Postgresql directive; this is because psycopg2's API-level -setting is always emitted at the start of each transaction in any case. - -The psycopg2 dialect supports these constants for isolation level: - -* ``READ COMMITTED`` -* ``READ UNCOMMITTED`` -* ``REPEATABLE READ`` -* ``SERIALIZABLE`` -* ``AUTOCOMMIT`` - -.. versionadded:: 0.8.2 support for AUTOCOMMIT isolation level when using - psycopg2. - - -NOTICE logging ---------------- - -The psycopg2 dialect will log Postgresql NOTICE messages via the -``sqlalchemy.dialects.postgresql`` logger:: - - import logging - logging.getLogger('sqlalchemy.dialects.postgresql').setLevel(logging.INFO) - -HSTORE type ------------- - -The psycopg2 dialect will make use of the -``psycopg2.extensions.register_hstore()`` extension when using the HSTORE -type. This replaces SQLAlchemy's pure-Python HSTORE coercion which takes -effect for other DBAPIs. - -""" -from __future__ import absolute_import - -import re -import logging - -from ... import util, exc -import decimal -from ... import processors -from ...engine import result as _result -from ...sql import expression -from ... import types as sqltypes -from .base import PGDialect, PGCompiler, \ - PGIdentifierPreparer, PGExecutionContext, \ - ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES,\ - _INT_TYPES -from .hstore import HSTORE -from .json import JSON - - -logger = logging.getLogger('sqlalchemy.dialects.postgresql') - - -class _PGNumeric(sqltypes.Numeric): - def bind_processor(self, dialect): - return None - - def result_processor(self, dialect, coltype): - if self.asdecimal: - if coltype in _FLOAT_TYPES: - return processors.to_decimal_processor_factory( - decimal.Decimal, - self._effective_decimal_return_scale) - elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: - # pg8000 returns Decimal natively for 1700 - return None - else: - raise exc.InvalidRequestError( - "Unknown PG numeric type: %d" % coltype) - else: - if coltype in _FLOAT_TYPES: - # pg8000 returns float natively for 701 - return None - elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: - return processors.to_float - else: - raise exc.InvalidRequestError( - "Unknown PG numeric type: %d" % coltype) - - -class _PGEnum(ENUM): - def result_processor(self, dialect, coltype): - if util.py2k and self.convert_unicode is True: - # we can't easily use PG's extensions here because - # the OID is on the fly, and we need to give it a python - # function anyway - not really worth it. - self.convert_unicode = "force_nocheck" - return super(_PGEnum, self).result_processor(dialect, coltype) - -class _PGHStore(HSTORE): - def bind_processor(self, dialect): - if dialect._has_native_hstore: - return None - else: - return super(_PGHStore, self).bind_processor(dialect) - - def result_processor(self, dialect, coltype): - if dialect._has_native_hstore: - return None - else: - return super(_PGHStore, self).result_processor(dialect, coltype) - - -class _PGJSON(JSON): - - def result_processor(self, dialect, coltype): - if dialect._has_native_json: - return None - else: - return super(_PGJSON, self).result_processor(dialect, coltype) - -# When we're handed literal SQL, ensure it's a SELECT-query. Since -# 8.3, combining cursors and "FOR UPDATE" has been fine. -SERVER_SIDE_CURSOR_RE = re.compile( - r'\s*SELECT', - re.I | re.UNICODE) - -_server_side_id = util.counter() - - -class PGExecutionContext_psycopg2(PGExecutionContext): - def create_cursor(self): - # TODO: coverage for server side cursors + select.for_update() - - if self.dialect.server_side_cursors: - is_server_side = \ - self.execution_options.get('stream_results', True) and ( - (self.compiled and isinstance(self.compiled.statement, expression.Selectable) \ - or \ - ( - (not self.compiled or - isinstance(self.compiled.statement, expression.TextClause)) - and self.statement and SERVER_SIDE_CURSOR_RE.match(self.statement)) - ) - ) - else: - is_server_side = \ - self.execution_options.get('stream_results', False) - - self.__is_server_side = is_server_side - if is_server_side: - # use server-side cursors: - # http://lists.initd.org/pipermail/psycopg/2007-January/005251.html - ident = "c_%s_%s" % (hex(id(self))[2:], hex(_server_side_id())[2:]) - return self._dbapi_connection.cursor(ident) - else: - return self._dbapi_connection.cursor() - - def get_result_proxy(self): - # TODO: ouch - if logger.isEnabledFor(logging.INFO): - self._log_notices(self.cursor) - - if self.__is_server_side: - return _result.BufferedRowResultProxy(self) - else: - return _result.ResultProxy(self) - - def _log_notices(self, cursor): - for notice in cursor.connection.notices: - # NOTICE messages have a - # newline character at the end - logger.info(notice.rstrip()) - - cursor.connection.notices[:] = [] - - -class PGCompiler_psycopg2(PGCompiler): - def visit_mod_binary(self, binary, operator, **kw): - return self.process(binary.left, **kw) + " %% " + \ - self.process(binary.right, **kw) - - def post_process_text(self, text): - return text.replace('%', '%%') - - -class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer): - def _escape_identifier(self, value): - value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace('%', '%%') - - -class PGDialect_psycopg2(PGDialect): - driver = 'psycopg2' - if util.py2k: - supports_unicode_statements = False - - default_paramstyle = 'pyformat' - supports_sane_multi_rowcount = False - execution_ctx_cls = PGExecutionContext_psycopg2 - statement_compiler = PGCompiler_psycopg2 - preparer = PGIdentifierPreparer_psycopg2 - psycopg2_version = (0, 0) - - _has_native_hstore = False - _has_native_json = False - - colspecs = util.update_copy( - PGDialect.colspecs, - { - sqltypes.Numeric: _PGNumeric, - ENUM: _PGEnum, # needs force_unicode - sqltypes.Enum: _PGEnum, # needs force_unicode - HSTORE: _PGHStore, - JSON: _PGJSON - } - ) - - def __init__(self, server_side_cursors=False, use_native_unicode=True, - client_encoding=None, - use_native_hstore=True, - **kwargs): - PGDialect.__init__(self, **kwargs) - self.server_side_cursors = server_side_cursors - self.use_native_unicode = use_native_unicode - self.use_native_hstore = use_native_hstore - self.supports_unicode_binds = use_native_unicode - self.client_encoding = client_encoding - if self.dbapi and hasattr(self.dbapi, '__version__'): - m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?', - self.dbapi.__version__) - if m: - self.psycopg2_version = tuple( - int(x) - for x in m.group(1, 2, 3) - if x is not None) - - def initialize(self, connection): - super(PGDialect_psycopg2, self).initialize(connection) - self._has_native_hstore = self.use_native_hstore and \ - self._hstore_oids(connection.connection) \ - is not None - self._has_native_json = self.psycopg2_version >= (2, 5) - - @classmethod - def dbapi(cls): - import psycopg2 - return psycopg2 - - @util.memoized_property - def _isolation_lookup(self): - from psycopg2 import extensions - return { - 'AUTOCOMMIT': extensions.ISOLATION_LEVEL_AUTOCOMMIT, - 'READ COMMITTED': extensions.ISOLATION_LEVEL_READ_COMMITTED, - 'READ UNCOMMITTED': extensions.ISOLATION_LEVEL_READ_UNCOMMITTED, - 'REPEATABLE READ': extensions.ISOLATION_LEVEL_REPEATABLE_READ, - 'SERIALIZABLE': extensions.ISOLATION_LEVEL_SERIALIZABLE - } - - def set_isolation_level(self, connection, level): - try: - level = self._isolation_lookup[level.replace('_', ' ')] - except KeyError: - raise exc.ArgumentError( - "Invalid value '%s' for isolation_level. " - "Valid isolation levels for %s are %s" % - (level, self.name, ", ".join(self._isolation_lookup)) - ) - - connection.set_isolation_level(level) - - def on_connect(self): - from psycopg2 import extras, extensions - - fns = [] - if self.client_encoding is not None: - def on_connect(conn): - conn.set_client_encoding(self.client_encoding) - fns.append(on_connect) - - if self.isolation_level is not None: - def on_connect(conn): - self.set_isolation_level(conn, self.isolation_level) - fns.append(on_connect) - - if self.dbapi and self.use_native_unicode: - def on_connect(conn): - extensions.register_type(extensions.UNICODE, conn) - extensions.register_type(extensions.UNICODEARRAY, conn) - fns.append(on_connect) - - if self.dbapi and self.use_native_hstore: - def on_connect(conn): - hstore_oids = self._hstore_oids(conn) - if hstore_oids is not None: - oid, array_oid = hstore_oids - if util.py2k: - extras.register_hstore(conn, oid=oid, - array_oid=array_oid, - unicode=True) - else: - extras.register_hstore(conn, oid=oid, - array_oid=array_oid) - fns.append(on_connect) - - if self.dbapi and self._json_deserializer: - def on_connect(conn): - extras.register_default_json(conn, loads=self._json_deserializer) - fns.append(on_connect) - - if fns: - def on_connect(conn): - for fn in fns: - fn(conn) - return on_connect - else: - return None - - @util.memoized_instancemethod - def _hstore_oids(self, conn): - if self.psycopg2_version >= (2, 4): - from psycopg2 import extras - oids = extras.HstoreAdapter.get_oids(conn) - if oids is not None and oids[0]: - return oids[0:2] - return None - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - if 'port' in opts: - opts['port'] = int(opts['port']) - opts.update(url.query) - return ([], opts) - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.Error): - str_e = str(e).partition("\n")[0] - for msg in [ - # these error messages from libpq: interfaces/libpq/fe-misc.c - # and interfaces/libpq/fe-secure.c. - # TODO: these are sent through gettext in libpq and we can't - # check within other locales - consider using connection.closed - 'terminating connection', - 'closed the connection', - 'connection not open', - 'could not receive data from server', - # psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h - 'connection already closed', - 'cursor already closed', - # not sure where this path is originally from, it may - # be obsolete. It really says "losed", not "closed". - 'losed the connection unexpectedly' - ]: - idx = str_e.find(msg) - if idx >= 0 and '"' not in str_e[:idx]: - return True - return False - -dialect = PGDialect_psycopg2 diff --git a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py b/libs/sqlalchemy/dialects/postgresql/pypostgresql.py deleted file mode 100644 index f030d2c1..00000000 --- a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py +++ /dev/null @@ -1,78 +0,0 @@ -# postgresql/pypostgresql.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: postgresql+pypostgresql - :name: py-postgresql - :dbapi: pypostgresql - :connectstring: postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...] - :url: http://python.projects.pgfoundry.org/ - - -""" -from ... import util -from ... import types as sqltypes -from .base import PGDialect, PGExecutionContext -from ... import processors - - -class PGNumeric(sqltypes.Numeric): - def bind_processor(self, dialect): - return processors.to_str - - def result_processor(self, dialect, coltype): - if self.asdecimal: - return None - else: - return processors.to_float - - -class PGExecutionContext_pypostgresql(PGExecutionContext): - pass - - -class PGDialect_pypostgresql(PGDialect): - driver = 'pypostgresql' - - supports_unicode_statements = True - supports_unicode_binds = True - description_encoding = None - default_paramstyle = 'pyformat' - - # requires trunk version to support sane rowcounts - # TODO: use dbapi version information to set this flag appropriately - supports_sane_rowcount = True - supports_sane_multi_rowcount = False - - execution_ctx_cls = PGExecutionContext_pypostgresql - colspecs = util.update_copy( - PGDialect.colspecs, - { - sqltypes.Numeric: PGNumeric, - - # prevents PGNumeric from being used - sqltypes.Float: sqltypes.Float, - } - ) - - @classmethod - def dbapi(cls): - from postgresql.driver import dbapi20 - return dbapi20 - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user') - if 'port' in opts: - opts['port'] = int(opts['port']) - else: - opts['port'] = 5432 - opts.update(url.query) - return ([], opts) - - def is_disconnect(self, e, connection, cursor): - return "connection is closed" in str(e) - -dialect = PGDialect_pypostgresql diff --git a/libs/sqlalchemy/dialects/postgresql/ranges.py b/libs/sqlalchemy/dialects/postgresql/ranges.py deleted file mode 100644 index 57b0c4c3..00000000 --- a/libs/sqlalchemy/dialects/postgresql/ranges.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .base import ischema_names -from ... import types as sqltypes - -__all__ = ('INT4RANGE', 'INT8RANGE', 'NUMRANGE') - -class RangeOperators(object): - """ - This mixin provides functionality for the Range Operators - listed in Table 9-44 of the `postgres documentation`__ for Range - Functions and Operators. It is used by all the range types - provided in the ``postgres`` dialect and can likely be used for - any range types you create yourself. - - __ http://www.postgresql.org/docs/devel/static/functions-range.html - - No extra support is provided for the Range Functions listed in - Table 9-45 of the postgres documentation. For these, the normal - :func:`~sqlalchemy.sql.expression.func` object should be used. - - .. versionadded:: 0.8.2 Support for Postgresql RANGE operations. - - """ - - class comparator_factory(sqltypes.Concatenable.Comparator): - """Define comparison operations for range types.""" - - def __ne__(self, other): - "Boolean expression. Returns true if two ranges are not equal" - return self.expr.op('<>')(other) - - def contains(self, other, **kw): - """Boolean expression. Returns true if the right hand operand, - which can be an element or a range, is contained within the - column. - """ - return self.expr.op('@>')(other) - - def contained_by(self, other): - """Boolean expression. Returns true if the column is contained - within the right hand operand. - """ - return self.expr.op('<@')(other) - - def overlaps(self, other): - """Boolean expression. Returns true if the column overlaps - (has points in common with) the right hand operand. - """ - return self.expr.op('&&')(other) - - def strictly_left_of(self, other): - """Boolean expression. Returns true if the column is strictly - left of the right hand operand. - """ - return self.expr.op('<<')(other) - - __lshift__ = strictly_left_of - - def strictly_right_of(self, other): - """Boolean expression. Returns true if the column is strictly - right of the right hand operand. - """ - return self.expr.op('>>')(other) - - __rshift__ = strictly_right_of - - def not_extend_right_of(self, other): - """Boolean expression. Returns true if the range in the column - does not extend right of the range in the operand. - """ - return self.expr.op('&<')(other) - - def not_extend_left_of(self, other): - """Boolean expression. Returns true if the range in the column - does not extend left of the range in the operand. - """ - return self.expr.op('&>')(other) - - def adjacent_to(self, other): - """Boolean expression. Returns true if the range in the column - is adjacent to the range in the operand. - """ - return self.expr.op('-|-')(other) - - def __add__(self, other): - """Range expression. Returns the union of the two ranges. - Will raise an exception if the resulting range is not - contigous. - """ - return self.expr.op('+')(other) - -class INT4RANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql INT4RANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'INT4RANGE' - -ischema_names['int4range'] = INT4RANGE - -class INT8RANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql INT8RANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'INT8RANGE' - -ischema_names['int8range'] = INT8RANGE - -class NUMRANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql NUMRANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'NUMRANGE' - -ischema_names['numrange'] = NUMRANGE - -class DATERANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql DATERANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'DATERANGE' - -ischema_names['daterange'] = DATERANGE - -class TSRANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql TSRANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'TSRANGE' - -ischema_names['tsrange'] = TSRANGE - -class TSTZRANGE(RangeOperators, sqltypes.TypeEngine): - """Represent the Postgresql TSTZRANGE type. - - .. versionadded:: 0.8.2 - - """ - - __visit_name__ = 'TSTZRANGE' - -ischema_names['tstzrange'] = TSTZRANGE diff --git a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py b/libs/sqlalchemy/dialects/postgresql/zxjdbc.py deleted file mode 100644 index 67e7d53e..00000000 --- a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py +++ /dev/null @@ -1,45 +0,0 @@ -# postgresql/zxjdbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: postgresql+zxjdbc - :name: zxJDBC for Jython - :dbapi: zxjdbc - :connectstring: postgresql+zxjdbc://scott:tiger@localhost/db - :driverurl: http://jdbc.postgresql.org/ - - -""" -from ...connectors.zxJDBC import ZxJDBCConnector -from .base import PGDialect, PGExecutionContext - - -class PGExecutionContext_zxjdbc(PGExecutionContext): - - def create_cursor(self): - cursor = self._dbapi_connection.cursor() - cursor.datahandler = self.dialect.DataHandler(cursor.datahandler) - return cursor - - -class PGDialect_zxjdbc(ZxJDBCConnector, PGDialect): - jdbc_db_name = 'postgresql' - jdbc_driver_name = 'org.postgresql.Driver' - - execution_ctx_cls = PGExecutionContext_zxjdbc - - supports_native_decimal = True - - def __init__(self, *args, **kwargs): - super(PGDialect_zxjdbc, self).__init__(*args, **kwargs) - from com.ziclix.python.sql.handler import PostgresqlDataHandler - self.DataHandler = PostgresqlDataHandler - - def _get_server_version_info(self, connection): - parts = connection.connection.dbversion.split('.') - return tuple(int(x) for x in parts) - -dialect = PGDialect_zxjdbc diff --git a/libs/sqlalchemy/dialects/sqlite/__init__.py b/libs/sqlalchemy/dialects/sqlite/__init__.py deleted file mode 100644 index a9b23575..00000000 --- a/libs/sqlalchemy/dialects/sqlite/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# sqlite/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.sqlite import base, pysqlite - -# default dialect -base.dialect = pysqlite.dialect - - -from sqlalchemy.dialects.sqlite.base import \ - BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL, FLOAT, INTEGER, REAL,\ - NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect - -__all__ = ( - 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', - 'INTEGER', 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', - 'REAL', 'dialect' -) diff --git a/libs/sqlalchemy/dialects/sqlite/base.py b/libs/sqlalchemy/dialects/sqlite/base.py deleted file mode 100644 index ac644f8d..00000000 --- a/libs/sqlalchemy/dialects/sqlite/base.py +++ /dev/null @@ -1,965 +0,0 @@ -# sqlite/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: sqlite - :name: SQLite - - -Date and Time Types -------------------- - -SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite -does not provide out of the box functionality for translating values between -Python `datetime` objects and a SQLite-supported format. SQLAlchemy's own -:class:`~sqlalchemy.types.DateTime` and related types provide date formatting -and parsing functionality when SQlite is used. The implementation classes are -:class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`. -These types represent dates and times as ISO formatted strings, which also -nicely support ordering. There's no reliance on typical "libc" internals -for these functions so historical dates are fully supported. - -Auto Incrementing Behavior --------------------------- - -Background on SQLite's autoincrement is at: http://sqlite.org/autoinc.html - -Two things to note: - -* The AUTOINCREMENT keyword is **not** required for SQLite tables to - generate primary key values automatically. AUTOINCREMENT only means that - the algorithm used to generate ROWID values should be slightly different. -* SQLite does **not** generate primary key (i.e. ROWID) values, even for - one column, if the table has a composite (i.e. multi-column) primary key. - This is regardless of the AUTOINCREMENT keyword being present or not. - -To specifically render the AUTOINCREMENT keyword on the primary key -column when rendering DDL, add the flag ``sqlite_autoincrement=True`` -to the Table construct:: - - Table('sometable', metadata, - Column('id', Integer, primary_key=True), - sqlite_autoincrement=True) - -Transaction Isolation Level ---------------------------- - -:func:`.create_engine` accepts an ``isolation_level`` parameter which -results in the command ``PRAGMA read_uncommitted `` being invoked for -every new connection. Valid values for this parameter are ``SERIALIZABLE`` -and ``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively. -See the section :ref:`pysqlite_serializable` for an important workaround -when using serializable isolation with Pysqlite. - -Database Locking Behavior / Concurrency ---------------------------------------- - -Note that SQLite is not designed for a high level of concurrency. The -database itself, being a file, is locked completely during write operations -and within transactions, meaning exactly one connection has exclusive access -to the database during this period - all other connections will be blocked -during this time. - -The Python DBAPI specification also calls for a connection model that is always -in a transaction; there is no BEGIN method, only commit and rollback. This -implies that a SQLite DBAPI driver would technically allow only serialized -access to a particular database file at all times. The pysqlite driver -attempts to ameliorate this by deferring the actual BEGIN statement until -the first DML (INSERT, UPDATE, or DELETE) is received within a -transaction. While this breaks serializable isolation, it at least delays -the exclusive locking inherent in SQLite's design. - -SQLAlchemy's default mode of usage with the ORM is known -as "autocommit=False", which means the moment the :class:`.Session` begins to -be used, a transaction is begun. As the :class:`.Session` is used, the -autoflush feature, also on by default, will flush out pending changes to the -database before each query. The effect of this is that a :class:`.Session` -used in its default mode will often emit DML early on, long before the -transaction is actually committed. This again will have the effect of -serializing access to the SQLite database. If highly concurrent reads are -desired against the SQLite database, it is advised that the autoflush feature -be disabled, and potentially even that autocommit be re-enabled, which has -the effect of each SQL statement and flush committing changes immediately. - -For more information on SQLite's lack of concurrency by design, please -see `Situations Where Another RDBMS May Work Better - High -Concurrency `_ near the bottom of -the page. - -.. _sqlite_foreign_keys: - -Foreign Key Support -------------------- - -SQLite supports FOREIGN KEY syntax when emitting CREATE statements for tables, -however by default these constraints have no effect on the operation -of the table. - -Constraint checking on SQLite has three prerequisites: - -* At least version 3.6.19 of SQLite must be in use -* The SQLite libary must be compiled *without* the SQLITE_OMIT_FOREIGN_KEY - or SQLITE_OMIT_TRIGGER symbols enabled. -* The ``PRAGMA foreign_keys = ON`` statement must be emitted on all connections - before use. - -SQLAlchemy allows for the ``PRAGMA`` statement to be emitted automatically -for new connections through the usage of events:: - - from sqlalchemy.engine import Engine - from sqlalchemy import event - - @event.listens_for(Engine, "connect") - def set_sqlite_pragma(dbapi_connection, connection_record): - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA foreign_keys=ON") - cursor.close() - -.. seealso:: - - `SQLite Foreign Key Support `_ - - on the SQLite web site. - - :ref:`event_toplevel` - SQLAlchemy event API. - -""" - -import datetime -import re - -from sqlalchemy import sql, exc -from sqlalchemy.engine import default, base, reflection -from sqlalchemy import types as sqltypes -from sqlalchemy import util -from sqlalchemy.sql import compiler -from sqlalchemy import processors - -from sqlalchemy.types import BIGINT, BLOB, BOOLEAN, CHAR,\ - DECIMAL, FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT,\ - TIMESTAMP, VARCHAR - - -class _DateTimeMixin(object): - _reg = None - _storage_format = None - - def __init__(self, storage_format=None, regexp=None, **kw): - super(_DateTimeMixin, self).__init__(**kw) - if regexp is not None: - self._reg = re.compile(regexp) - if storage_format is not None: - self._storage_format = storage_format - - def adapt(self, cls, **kw): - if self._storage_format: - kw["storage_format"] = self._storage_format - if self._reg: - kw["regexp"] = self._reg - return util.constructor_copy(self, cls, **kw) - - def literal_processor(self, dialect): - bp = self.bind_processor(dialect) - def process(value): - return "'%s'" % bp(value) - return process - - -class DATETIME(_DateTimeMixin, sqltypes.DateTime): - """Represent a Python datetime object in SQLite using a string. - - The default string storage format is:: - - "%(year)04d-%(month)02d-%(day)02d %(hour)02d:%(min)02d:%(second)02d.%(microsecond)06d" - - e.g.:: - - 2011-03-15 12:05:57.10558 - - The storage format can be customized to some degree using the - ``storage_format`` and ``regexp`` parameters, such as:: - - import re - from sqlalchemy.dialects.sqlite import DATETIME - - dt = DATETIME( - storage_format="%(year)04d/%(month)02d/%(day)02d %(hour)02d:%(min)02d:%(second)02d", - regexp=r"(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)" - ) - - :param storage_format: format string which will be applied to the - dict with keys year, month, day, hour, minute, second, and microsecond. - - :param regexp: regular expression which will be applied to - incoming result rows. If the regexp contains named groups, the - resulting match dict is applied to the Python datetime() constructor - as keyword arguments. Otherwise, if positional groups are used, the - the datetime() constructor is called with positional arguments via - ``*map(int, match_obj.groups(0))``. - """ - - _storage_format = ( - "%(year)04d-%(month)02d-%(day)02d " - "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" - ) - - def __init__(self, *args, **kwargs): - truncate_microseconds = kwargs.pop('truncate_microseconds', False) - super(DATETIME, self).__init__(*args, **kwargs) - if truncate_microseconds: - assert 'storage_format' not in kwargs, "You can specify only "\ - "one of truncate_microseconds or storage_format." - assert 'regexp' not in kwargs, "You can specify only one of "\ - "truncate_microseconds or regexp." - self._storage_format = ( - "%(year)04d-%(month)02d-%(day)02d " - "%(hour)02d:%(minute)02d:%(second)02d" - ) - - - def bind_processor(self, dialect): - datetime_datetime = datetime.datetime - datetime_date = datetime.date - format = self._storage_format - - def process(value): - if value is None: - return None - elif isinstance(value, datetime_datetime): - return format % { - 'year': value.year, - 'month': value.month, - 'day': value.day, - 'hour': value.hour, - 'minute': value.minute, - 'second': value.second, - 'microsecond': value.microsecond, - } - elif isinstance(value, datetime_date): - return format % { - 'year': value.year, - 'month': value.month, - 'day': value.day, - 'hour': 0, - 'minute': 0, - 'second': 0, - 'microsecond': 0, - } - else: - raise TypeError("SQLite DateTime type only accepts Python " - "datetime and date objects as input.") - return process - - def result_processor(self, dialect, coltype): - if self._reg: - return processors.str_to_datetime_processor_factory( - self._reg, datetime.datetime) - else: - return processors.str_to_datetime - - -class DATE(_DateTimeMixin, sqltypes.Date): - """Represent a Python date object in SQLite using a string. - - The default string storage format is:: - - "%(year)04d-%(month)02d-%(day)02d" - - e.g.:: - - 2011-03-15 - - The storage format can be customized to some degree using the - ``storage_format`` and ``regexp`` parameters, such as:: - - import re - from sqlalchemy.dialects.sqlite import DATE - - d = DATE( - storage_format="%(month)02d/%(day)02d/%(year)04d", - regexp=re.compile("(?P\d+)/(?P\d+)/(?P\d+)") - ) - - :param storage_format: format string which will be applied to the - dict with keys year, month, and day. - - :param regexp: regular expression which will be applied to - incoming result rows. If the regexp contains named groups, the - resulting match dict is applied to the Python date() constructor - as keyword arguments. Otherwise, if positional groups are used, the - the date() constructor is called with positional arguments via - ``*map(int, match_obj.groups(0))``. - """ - - _storage_format = "%(year)04d-%(month)02d-%(day)02d" - - def bind_processor(self, dialect): - datetime_date = datetime.date - format = self._storage_format - - def process(value): - if value is None: - return None - elif isinstance(value, datetime_date): - return format % { - 'year': value.year, - 'month': value.month, - 'day': value.day, - } - else: - raise TypeError("SQLite Date type only accepts Python " - "date objects as input.") - return process - - def result_processor(self, dialect, coltype): - if self._reg: - return processors.str_to_datetime_processor_factory( - self._reg, datetime.date) - else: - return processors.str_to_date - - -class TIME(_DateTimeMixin, sqltypes.Time): - """Represent a Python time object in SQLite using a string. - - The default string storage format is:: - - "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" - - e.g.:: - - 12:05:57.10558 - - The storage format can be customized to some degree using the - ``storage_format`` and ``regexp`` parameters, such as:: - - import re - from sqlalchemy.dialects.sqlite import TIME - - t = TIME( - storage_format="%(hour)02d-%(minute)02d-%(second)02d-%(microsecond)06d", - regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?") - ) - - :param storage_format: format string which will be applied to the - dict with keys hour, minute, second, and microsecond. - - :param regexp: regular expression which will be applied to - incoming result rows. If the regexp contains named groups, the - resulting match dict is applied to the Python time() constructor - as keyword arguments. Otherwise, if positional groups are used, the - the time() constructor is called with positional arguments via - ``*map(int, match_obj.groups(0))``. - """ - - _storage_format = "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" - - def __init__(self, *args, **kwargs): - truncate_microseconds = kwargs.pop('truncate_microseconds', False) - super(TIME, self).__init__(*args, **kwargs) - if truncate_microseconds: - assert 'storage_format' not in kwargs, "You can specify only "\ - "one of truncate_microseconds or storage_format." - assert 'regexp' not in kwargs, "You can specify only one of "\ - "truncate_microseconds or regexp." - self._storage_format = "%(hour)02d:%(minute)02d:%(second)02d" - - def bind_processor(self, dialect): - datetime_time = datetime.time - format = self._storage_format - - def process(value): - if value is None: - return None - elif isinstance(value, datetime_time): - return format % { - 'hour': value.hour, - 'minute': value.minute, - 'second': value.second, - 'microsecond': value.microsecond, - } - else: - raise TypeError("SQLite Time type only accepts Python " - "time objects as input.") - return process - - def result_processor(self, dialect, coltype): - if self._reg: - return processors.str_to_datetime_processor_factory( - self._reg, datetime.time) - else: - return processors.str_to_time - -colspecs = { - sqltypes.Date: DATE, - sqltypes.DateTime: DATETIME, - sqltypes.Time: TIME, -} - -ischema_names = { - 'BIGINT': sqltypes.BIGINT, - 'BLOB': sqltypes.BLOB, - 'BOOL': sqltypes.BOOLEAN, - 'BOOLEAN': sqltypes.BOOLEAN, - 'CHAR': sqltypes.CHAR, - 'DATE': sqltypes.DATE, - 'DATETIME': sqltypes.DATETIME, - 'DECIMAL': sqltypes.DECIMAL, - 'FLOAT': sqltypes.FLOAT, - 'INT': sqltypes.INTEGER, - 'INTEGER': sqltypes.INTEGER, - 'NUMERIC': sqltypes.NUMERIC, - 'REAL': sqltypes.REAL, - 'SMALLINT': sqltypes.SMALLINT, - 'TEXT': sqltypes.TEXT, - 'TIME': sqltypes.TIME, - 'TIMESTAMP': sqltypes.TIMESTAMP, - 'VARCHAR': sqltypes.VARCHAR, - 'NVARCHAR': sqltypes.NVARCHAR, - 'NCHAR': sqltypes.NCHAR, -} - - -class SQLiteCompiler(compiler.SQLCompiler): - extract_map = util.update_copy( - compiler.SQLCompiler.extract_map, - { - 'month': '%m', - 'day': '%d', - 'year': '%Y', - 'second': '%S', - 'hour': '%H', - 'doy': '%j', - 'minute': '%M', - 'epoch': '%s', - 'dow': '%w', - 'week': '%W' - }) - - def visit_now_func(self, fn, **kw): - return "CURRENT_TIMESTAMP" - - def visit_localtimestamp_func(self, func, **kw): - return 'DATETIME(CURRENT_TIMESTAMP, "localtime")' - - def visit_true(self, expr, **kw): - return '1' - - def visit_false(self, expr, **kw): - return '0' - - def visit_char_length_func(self, fn, **kw): - return "length%s" % self.function_argspec(fn) - - def visit_cast(self, cast, **kwargs): - if self.dialect.supports_cast: - return super(SQLiteCompiler, self).visit_cast(cast) - else: - return self.process(cast.clause) - - def visit_extract(self, extract, **kw): - try: - return "CAST(STRFTIME('%s', %s) AS INTEGER)" % ( - self.extract_map[extract.field], - self.process(extract.expr, **kw) - ) - except KeyError: - raise exc.CompileError( - "%s is not a valid extract argument." % extract.field) - - def limit_clause(self, select): - text = "" - if select._limit is not None: - text += "\n LIMIT " + self.process(sql.literal(select._limit)) - if select._offset is not None: - if select._limit is None: - text += "\n LIMIT " + self.process(sql.literal(-1)) - text += " OFFSET " + self.process(sql.literal(select._offset)) - else: - text += " OFFSET " + self.process(sql.literal(0)) - return text - - def for_update_clause(self, select): - # sqlite has no "FOR UPDATE" AFAICT - return '' - - -class SQLiteDDLCompiler(compiler.DDLCompiler): - - def get_column_specification(self, column, **kwargs): - coltype = self.dialect.type_compiler.process(column.type) - colspec = self.preparer.format_column(column) + " " + coltype - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - if not column.nullable: - colspec += " NOT NULL" - - if (column.primary_key and - column.table.kwargs.get('sqlite_autoincrement', False) and - len(column.table.primary_key.columns) == 1 and - issubclass(column.type._type_affinity, sqltypes.Integer) and - not column.foreign_keys): - colspec += " PRIMARY KEY AUTOINCREMENT" - - return colspec - - def visit_primary_key_constraint(self, constraint): - # for columns with sqlite_autoincrement=True, - # the PRIMARY KEY constraint can only be inline - # with the column itself. - if len(constraint.columns) == 1: - c = list(constraint)[0] - if c.primary_key and \ - c.table.kwargs.get('sqlite_autoincrement', False) and \ - issubclass(c.type._type_affinity, sqltypes.Integer) and \ - not c.foreign_keys: - return None - - return super(SQLiteDDLCompiler, self).\ - visit_primary_key_constraint(constraint) - - def visit_foreign_key_constraint(self, constraint): - - local_table = list(constraint._elements.values())[0].parent.table - remote_table = list(constraint._elements.values())[0].column.table - - if local_table.schema != remote_table.schema: - return None - else: - return super(SQLiteDDLCompiler, self).visit_foreign_key_constraint(constraint) - - def define_constraint_remote_table(self, constraint, table, preparer): - """Format the remote table clause of a CREATE CONSTRAINT clause.""" - - return preparer.format_table(table, use_schema=False) - - def visit_create_index(self, create): - return super(SQLiteDDLCompiler, self).\ - visit_create_index(create, include_table_schema=False) - - -class SQLiteTypeCompiler(compiler.GenericTypeCompiler): - def visit_large_binary(self, type_): - return self.visit_BLOB(type_) - - -class SQLiteIdentifierPreparer(compiler.IdentifierPreparer): - reserved_words = set([ - 'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc', - 'attach', 'autoincrement', 'before', 'begin', 'between', 'by', - 'cascade', 'case', 'cast', 'check', 'collate', 'column', 'commit', - 'conflict', 'constraint', 'create', 'cross', 'current_date', - 'current_time', 'current_timestamp', 'database', 'default', - 'deferrable', 'deferred', 'delete', 'desc', 'detach', 'distinct', - 'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive', - 'explain', 'false', 'fail', 'for', 'foreign', 'from', 'full', 'glob', - 'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index', - 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', - 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit', - 'match', 'natural', 'not', 'notnull', 'null', 'of', 'offset', 'on', - 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query', - 'raise', 'references', 'reindex', 'rename', 'replace', 'restrict', - 'right', 'rollback', 'row', 'select', 'set', 'table', 'temp', - 'temporary', 'then', 'to', 'transaction', 'trigger', 'true', 'union', - 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual', - 'when', 'where', - ]) - - def format_index(self, index, use_schema=True, name=None): - """Prepare a quoted index and schema name.""" - - if name is None: - name = index.name - result = self.quote(name, index.quote) - if (not self.omit_schema and - use_schema and - getattr(index.table, "schema", None)): - result = self.quote_schema( - index.table.schema, index.table.quote_schema) + "." + result - return result - - -class SQLiteExecutionContext(default.DefaultExecutionContext): - @util.memoized_property - def _preserve_raw_colnames(self): - return self.execution_options.get("sqlite_raw_colnames", False) - - def _translate_colname(self, colname): - # adjust for dotted column names. SQLite - # in the case of UNION may store col names as - # "tablename.colname" - # in cursor.description - if not self._preserve_raw_colnames and "." in colname: - return colname.split(".")[1], colname - else: - return colname, None - - -class SQLiteDialect(default.DefaultDialect): - name = 'sqlite' - supports_alter = False - supports_unicode_statements = True - supports_unicode_binds = True - supports_default_values = True - supports_empty_insert = False - supports_cast = True - supports_multivalues_insert = True - supports_right_nested_joins = False - - default_paramstyle = 'qmark' - execution_ctx_cls = SQLiteExecutionContext - statement_compiler = SQLiteCompiler - ddl_compiler = SQLiteDDLCompiler - type_compiler = SQLiteTypeCompiler - preparer = SQLiteIdentifierPreparer - ischema_names = ischema_names - colspecs = colspecs - isolation_level = None - - supports_cast = True - supports_default_values = True - - _broken_fk_pragma_quotes = False - - def __init__(self, isolation_level=None, native_datetime=False, **kwargs): - default.DefaultDialect.__init__(self, **kwargs) - self.isolation_level = isolation_level - - # this flag used by pysqlite dialect, and perhaps others in the - # future, to indicate the driver is handling date/timestamp - # conversions (and perhaps datetime/time as well on some - # hypothetical driver ?) - self.native_datetime = native_datetime - - if self.dbapi is not None: - self.supports_default_values = \ - self.dbapi.sqlite_version_info >= (3, 3, 8) - self.supports_cast = \ - self.dbapi.sqlite_version_info >= (3, 2, 3) - self.supports_multivalues_insert = \ - self.dbapi.sqlite_version_info >= (3, 7, 11) - # http://www.sqlite.org/releaselog/3_7_11.html - - # see http://www.sqlalchemy.org/trac/ticket/2568 - # as well as http://www.sqlite.org/src/info/600482d161 - self._broken_fk_pragma_quotes = \ - self.dbapi.sqlite_version_info < (3, 6, 14) - - - _isolation_lookup = { - 'READ UNCOMMITTED': 1, - 'SERIALIZABLE': 0 - } - - def set_isolation_level(self, connection, level): - try: - isolation_level = self._isolation_lookup[level.replace('_', ' ')] - except KeyError: - raise exc.ArgumentError( - "Invalid value '%s' for isolation_level. " - "Valid isolation levels for %s are %s" % - (level, self.name, ", ".join(self._isolation_lookup)) - ) - cursor = connection.cursor() - cursor.execute("PRAGMA read_uncommitted = %d" % isolation_level) - cursor.close() - - def get_isolation_level(self, connection): - cursor = connection.cursor() - cursor.execute('PRAGMA read_uncommitted') - res = cursor.fetchone() - if res: - value = res[0] - else: - # http://www.sqlite.org/changes.html#version_3_3_3 - # "Optional READ UNCOMMITTED isolation (instead of the - # default isolation level of SERIALIZABLE) and - # table level locking when database connections - # share a common cache."" - # pre-SQLite 3.3.0 default to 0 - value = 0 - cursor.close() - if value == 0: - return "SERIALIZABLE" - elif value == 1: - return "READ UNCOMMITTED" - else: - assert False, "Unknown isolation level %s" % value - - def on_connect(self): - if self.isolation_level is not None: - def connect(conn): - self.set_isolation_level(conn, self.isolation_level) - return connect - else: - return None - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - if schema is not None: - qschema = self.identifier_preparer.quote_identifier(schema) - master = '%s.sqlite_master' % qschema - s = ("SELECT name FROM %s " - "WHERE type='table' ORDER BY name") % (master,) - rs = connection.execute(s) - else: - try: - s = ("SELECT name FROM " - " (SELECT * FROM sqlite_master UNION ALL " - " SELECT * FROM sqlite_temp_master) " - "WHERE type='table' ORDER BY name") - rs = connection.execute(s) - except exc.DBAPIError: - s = ("SELECT name FROM sqlite_master " - "WHERE type='table' ORDER BY name") - rs = connection.execute(s) - - return [row[0] for row in rs] - - def has_table(self, connection, table_name, schema=None): - quote = self.identifier_preparer.quote_identifier - if schema is not None: - pragma = "PRAGMA %s." % quote(schema) - else: - pragma = "PRAGMA " - qtable = quote(table_name) - statement = "%stable_info(%s)" % (pragma, qtable) - cursor = _pragma_cursor(connection.execute(statement)) - row = cursor.fetchone() - - # consume remaining rows, to work around - # http://www.sqlite.org/cvstrac/tktview?tn=1884 - while not cursor.closed and cursor.fetchone() is not None: - pass - - return (row is not None) - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - if schema is not None: - qschema = self.identifier_preparer.quote_identifier(schema) - master = '%s.sqlite_master' % qschema - s = ("SELECT name FROM %s " - "WHERE type='view' ORDER BY name") % (master,) - rs = connection.execute(s) - else: - try: - s = ("SELECT name FROM " - " (SELECT * FROM sqlite_master UNION ALL " - " SELECT * FROM sqlite_temp_master) " - "WHERE type='view' ORDER BY name") - rs = connection.execute(s) - except exc.DBAPIError: - s = ("SELECT name FROM sqlite_master " - "WHERE type='view' ORDER BY name") - rs = connection.execute(s) - - return [row[0] for row in rs] - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - quote = self.identifier_preparer.quote_identifier - if schema is not None: - qschema = self.identifier_preparer.quote_identifier(schema) - master = '%s.sqlite_master' % qschema - s = ("SELECT sql FROM %s WHERE name = '%s'" - "AND type='view'") % (master, view_name) - rs = connection.execute(s) - else: - try: - s = ("SELECT sql FROM " - " (SELECT * FROM sqlite_master UNION ALL " - " SELECT * FROM sqlite_temp_master) " - "WHERE name = '%s' " - "AND type='view'") % view_name - rs = connection.execute(s) - except exc.DBAPIError: - s = ("SELECT sql FROM sqlite_master WHERE name = '%s' " - "AND type='view'") % view_name - rs = connection.execute(s) - - result = rs.fetchall() - if result: - return result[0].sql - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - quote = self.identifier_preparer.quote_identifier - if schema is not None: - pragma = "PRAGMA %s." % quote(schema) - else: - pragma = "PRAGMA " - qtable = quote(table_name) - statement = "%stable_info(%s)" % (pragma, qtable) - c = _pragma_cursor(connection.execute(statement)) - - rows = c.fetchall() - columns = [] - for row in rows: - (name, type_, nullable, default, primary_key) = \ - (row[1], row[2].upper(), not row[3], - row[4], row[5]) - - columns.append(self._get_column_info(name, type_, nullable, - default, primary_key)) - return columns - - def _get_column_info(self, name, type_, nullable, - default, primary_key): - - match = re.match(r'(\w+)(\(.*?\))?', type_) - if match: - coltype = match.group(1) - args = match.group(2) - else: - coltype = "VARCHAR" - args = '' - try: - coltype = self.ischema_names[coltype] - if args is not None: - args = re.findall(r'(\d+)', args) - coltype = coltype(*[int(a) for a in args]) - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (coltype, name)) - coltype = sqltypes.NullType() - - if default is not None: - default = util.text_type(default) - - return { - 'name': name, - 'type': coltype, - 'nullable': nullable, - 'default': default, - 'autoincrement': default is None, - 'primary_key': primary_key - } - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - cols = self.get_columns(connection, table_name, schema, **kw) - pkeys = [] - for col in cols: - if col['primary_key']: - pkeys.append(col['name']) - return {'constrained_columns': pkeys, 'name': None} - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - quote = self.identifier_preparer.quote_identifier - if schema is not None: - pragma = "PRAGMA %s." % quote(schema) - else: - pragma = "PRAGMA " - qtable = quote(table_name) - statement = "%sforeign_key_list(%s)" % (pragma, qtable) - c = _pragma_cursor(connection.execute(statement)) - fkeys = [] - fks = {} - while True: - row = c.fetchone() - if row is None: - break - (numerical_id, rtbl, lcol, rcol) = (row[0], row[2], row[3], row[4]) - - self._parse_fk(fks, fkeys, numerical_id, rtbl, lcol, rcol) - return fkeys - - def _parse_fk(self, fks, fkeys, numerical_id, rtbl, lcol, rcol): - # sqlite won't return rcol if the table - # was created with REFERENCES , no col - if rcol is None: - rcol = lcol - - if self._broken_fk_pragma_quotes: - rtbl = re.sub(r'^[\"\[`\']|[\"\]`\']$', '', rtbl) - - try: - fk = fks[numerical_id] - except KeyError: - fk = { - 'name': None, - 'constrained_columns': [], - 'referred_schema': None, - 'referred_table': rtbl, - 'referred_columns': [] - } - fkeys.append(fk) - fks[numerical_id] = fk - - if lcol not in fk['constrained_columns']: - fk['constrained_columns'].append(lcol) - if rcol not in fk['referred_columns']: - fk['referred_columns'].append(rcol) - return fk - - @reflection.cache - def get_indexes(self, connection, table_name, schema=None, **kw): - quote = self.identifier_preparer.quote_identifier - if schema is not None: - pragma = "PRAGMA %s." % quote(schema) - else: - pragma = "PRAGMA " - include_auto_indexes = kw.pop('include_auto_indexes', False) - qtable = quote(table_name) - statement = "%sindex_list(%s)" % (pragma, qtable) - c = _pragma_cursor(connection.execute(statement)) - indexes = [] - while True: - row = c.fetchone() - if row is None: - break - # ignore implicit primary key index. - # http://www.mail-archive.com/sqlite-users@sqlite.org/msg30517.html - elif (not include_auto_indexes and - row[1].startswith('sqlite_autoindex')): - continue - - indexes.append(dict(name=row[1], column_names=[], unique=row[2])) - # loop thru unique indexes to get the column names. - for idx in indexes: - statement = "%sindex_info(%s)" % (pragma, quote(idx['name'])) - c = connection.execute(statement) - cols = idx['column_names'] - while True: - row = c.fetchone() - if row is None: - break - cols.append(row[2]) - return indexes - - @reflection.cache - def get_unique_constraints(self, connection, table_name, - schema=None, **kw): - UNIQUE_SQL = """ - SELECT sql - FROM - sqlite_master - WHERE - type='table' AND - name=:table_name - """ - c = connection.execute(UNIQUE_SQL, table_name=table_name) - table_data = c.fetchone()[0] - - UNIQUE_PATTERN = 'CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)' - return [ - {'name': name, - 'column_names': [col.strip(' "') for col in cols.split(',')]} - for name, cols in re.findall(UNIQUE_PATTERN, table_data) - ] - - -def _pragma_cursor(cursor): - """work around SQLite issue whereby cursor.description - is blank when PRAGMA returns no rows.""" - - if cursor.closed: - cursor.fetchone = lambda: None - cursor.fetchall = lambda: [] - return cursor diff --git a/libs/sqlalchemy/dialects/sqlite/pysqlite.py b/libs/sqlalchemy/dialects/sqlite/pysqlite.py deleted file mode 100644 index b53f4d4a..00000000 --- a/libs/sqlalchemy/dialects/sqlite/pysqlite.py +++ /dev/null @@ -1,335 +0,0 @@ -# sqlite/pysqlite.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: sqlite+pysqlite - :name: pysqlite - :dbapi: sqlite3 - :connectstring: sqlite+pysqlite:///file_path - :url: http://docs.python.org/library/sqlite3.html - - Note that ``pysqlite`` is the same driver as the ``sqlite3`` - module included with the Python distribution. - -Driver ------- - -When using Python 2.5 and above, the built in ``sqlite3`` driver is -already installed and no additional installation is needed. Otherwise, -the ``pysqlite2`` driver needs to be present. This is the same driver as -``sqlite3``, just with a different name. - -The ``pysqlite2`` driver will be loaded first, and if not found, ``sqlite3`` -is loaded. This allows an explicitly installed pysqlite driver to take -precedence over the built in one. As with all dialects, a specific -DBAPI module may be provided to :func:`~sqlalchemy.create_engine()` to control -this explicitly:: - - from sqlite3 import dbapi2 as sqlite - e = create_engine('sqlite+pysqlite:///file.db', module=sqlite) - - -Connect Strings ---------------- - -The file specification for the SQLite database is taken as the "database" -portion of the URL. Note that the format of a SQLAlchemy url is:: - - driver://user:pass@host/database - -This means that the actual filename to be used starts with the characters to -the **right** of the third slash. So connecting to a relative filepath -looks like:: - - # relative path - e = create_engine('sqlite:///path/to/database.db') - -An absolute path, which is denoted by starting with a slash, means you -need **four** slashes:: - - # absolute path - e = create_engine('sqlite:////path/to/database.db') - -To use a Windows path, regular drive specifications and backslashes can be -used. Double backslashes are probably needed:: - - # absolute path on Windows - e = create_engine('sqlite:///C:\\\\path\\\\to\\\\database.db') - -The sqlite ``:memory:`` identifier is the default if no filepath is -present. Specify ``sqlite://`` and nothing else:: - - # in-memory database - e = create_engine('sqlite://') - -Compatibility with sqlite3 "native" date and datetime types ------------------------------------------------------------ - -The pysqlite driver includes the sqlite3.PARSE_DECLTYPES and -sqlite3.PARSE_COLNAMES options, which have the effect of any column -or expression explicitly cast as "date" or "timestamp" will be converted -to a Python date or datetime object. The date and datetime types provided -with the pysqlite dialect are not currently compatible with these options, -since they render the ISO date/datetime including microseconds, which -pysqlite's driver does not. Additionally, SQLAlchemy does not at -this time automatically render the "cast" syntax required for the -freestanding functions "current_timestamp" and "current_date" to return -datetime/date types natively. Unfortunately, pysqlite -does not provide the standard DBAPI types in ``cursor.description``, -leaving SQLAlchemy with no way to detect these types on the fly -without expensive per-row type checks. - -Keeping in mind that pysqlite's parsing option is not recommended, -nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES -can be forced if one configures "native_datetime=True" on create_engine():: - - engine = create_engine('sqlite://', - connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES}, - native_datetime=True - ) - -With this flag enabled, the DATE and TIMESTAMP types (but note - not the -DATETIME or TIME types...confused yet ?) will not perform any bind parameter -or result processing. Execution of "func.current_date()" will return a string. -"func.current_timestamp()" is registered as returning a DATETIME type in -SQLAlchemy, so this function still receives SQLAlchemy-level result processing. - -.. _pysqlite_threading_pooling: - -Threading/Pooling Behavior ---------------------------- - -Pysqlite's default behavior is to prohibit the usage of a single connection -in more than one thread. This is originally intended to work with older -versions of SQLite that did not support multithreaded operation under -various circumstances. In particular, older SQLite versions -did not allow a ``:memory:`` database to be used in multiple threads -under any circumstances. - -Pysqlite does include a now-undocumented flag known as -``check_same_thread`` which will disable this check, however note that pysqlite -connections are still not safe to use in concurrently in multiple threads. -In particular, any statement execution calls would need to be externally -mutexed, as Pysqlite does not provide for thread-safe propagation of error -messages among other things. So while even ``:memory:`` databases can be -shared among threads in modern SQLite, Pysqlite doesn't provide enough -thread-safety to make this usage worth it. - -SQLAlchemy sets up pooling to work with Pysqlite's default behavior: - -* When a ``:memory:`` SQLite database is specified, the dialect by default - will use :class:`.SingletonThreadPool`. This pool maintains a single - connection per thread, so that all access to the engine within the current - thread use the same ``:memory:`` database - other threads would access a - different ``:memory:`` database. -* When a file-based database is specified, the dialect will use - :class:`.NullPool` as the source of connections. This pool closes and - discards connections which are returned to the pool immediately. SQLite - file-based connections have extremely low overhead, so pooling is not - necessary. The scheme also prevents a connection from being used again in - a different thread and works best with SQLite's coarse-grained file locking. - - .. versionchanged:: 0.7 - Default selection of :class:`.NullPool` for SQLite file-based databases. - Previous versions select :class:`.SingletonThreadPool` by - default for all SQLite databases. - - -Using a Memory Database in Multiple Threads -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To use a ``:memory:`` database in a multithreaded scenario, the same connection -object must be shared among threads, since the database exists -only within the scope of that connection. The -:class:`.StaticPool` implementation will maintain a single connection -globally, and the ``check_same_thread`` flag can be passed to Pysqlite -as ``False``:: - - from sqlalchemy.pool import StaticPool - engine = create_engine('sqlite://', - connect_args={'check_same_thread':False}, - poolclass=StaticPool) - -Note that using a ``:memory:`` database in multiple threads requires a recent -version of SQLite. - -Using Temporary Tables with SQLite -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Due to the way SQLite deals with temporary tables, if you wish to use a -temporary table in a file-based SQLite database across multiple checkouts -from the connection pool, such as when using an ORM :class:`.Session` where -the temporary table should continue to remain after :meth:`.Session.commit` or -:meth:`.Session.rollback` is called, a pool which maintains a single connection must -be used. Use :class:`.SingletonThreadPool` if the scope is only needed -within the current thread, or :class:`.StaticPool` is scope is needed within -multiple threads for this case:: - - # maintain the same connection per thread - from sqlalchemy.pool import SingletonThreadPool - engine = create_engine('sqlite:///mydb.db', - poolclass=SingletonThreadPool) - - - # maintain the same connection across all threads - from sqlalchemy.pool import StaticPool - engine = create_engine('sqlite:///mydb.db', - poolclass=StaticPool) - -Note that :class:`.SingletonThreadPool` should be configured for the number -of threads that are to be used; beyond that number, connections will be -closed out in a non deterministic way. - -Unicode -------- - -The pysqlite driver only returns Python ``unicode`` objects in result sets, -never plain strings, and accommodates ``unicode`` objects within bound -parameter values in all cases. Regardless of the SQLAlchemy string type in -use, string-based result values will by Python ``unicode`` in Python 2. -The :class:`.Unicode` type should still be used to indicate those columns that -require unicode, however, so that non-``unicode`` values passed inadvertently -will emit a warning. Pysqlite will emit an error if a non-``unicode`` string -is passed containing non-ASCII characters. - -.. _pysqlite_serializable: - -Serializable Transaction Isolation ----------------------------------- - -The pysqlite DBAPI driver has a long-standing bug in which transactional -state is not begun until the first DML statement, that is INSERT, UPDATE -or DELETE, is emitted. A SELECT statement will not cause transactional -state to begin. While this mode of usage is fine for typical situations -and has the advantage that the SQLite database file is not prematurely -locked, it breaks serializable transaction isolation, which requires -that the database file be locked upon any SQL being emitted. - -To work around this issue, the ``BEGIN`` keyword can be emitted -at the start of each transaction. The following recipe establishes -a :meth:`.ConnectionEvents.begin` handler to achieve this:: - - from sqlalchemy import create_engine, event - - engine = create_engine("sqlite:///myfile.db", isolation_level='SERIALIZABLE') - - @event.listens_for(engine, "begin") - def do_begin(conn): - conn.execute("BEGIN") - -""" - -from sqlalchemy.dialects.sqlite.base import SQLiteDialect, DATETIME, DATE -from sqlalchemy import exc, pool -from sqlalchemy import types as sqltypes -from sqlalchemy import util - -import os - - -class _SQLite_pysqliteTimeStamp(DATETIME): - def bind_processor(self, dialect): - if dialect.native_datetime: - return None - else: - return DATETIME.bind_processor(self, dialect) - - def result_processor(self, dialect, coltype): - if dialect.native_datetime: - return None - else: - return DATETIME.result_processor(self, dialect, coltype) - - -class _SQLite_pysqliteDate(DATE): - def bind_processor(self, dialect): - if dialect.native_datetime: - return None - else: - return DATE.bind_processor(self, dialect) - - def result_processor(self, dialect, coltype): - if dialect.native_datetime: - return None - else: - return DATE.result_processor(self, dialect, coltype) - - -class SQLiteDialect_pysqlite(SQLiteDialect): - default_paramstyle = 'qmark' - - colspecs = util.update_copy( - SQLiteDialect.colspecs, - { - sqltypes.Date: _SQLite_pysqliteDate, - sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp, - } - ) - - if not util.py2k: - description_encoding = None - - driver = 'pysqlite' - - def __init__(self, **kwargs): - SQLiteDialect.__init__(self, **kwargs) - - if self.dbapi is not None: - sqlite_ver = self.dbapi.version_info - if sqlite_ver < (2, 1, 3): - util.warn( - ("The installed version of pysqlite2 (%s) is out-dated " - "and will cause errors in some cases. Version 2.1.3 " - "or greater is recommended.") % - '.'.join([str(subver) for subver in sqlite_ver])) - - @classmethod - def dbapi(cls): - try: - from pysqlite2 import dbapi2 as sqlite - except ImportError as e: - try: - from sqlite3 import dbapi2 as sqlite # try 2.5+ stdlib name. - except ImportError: - raise e - return sqlite - - @classmethod - def get_pool_class(cls, url): - if url.database and url.database != ':memory:': - return pool.NullPool - else: - return pool.SingletonThreadPool - - def _get_server_version_info(self, connection): - return self.dbapi.sqlite_version_info - - def create_connect_args(self, url): - if url.username or url.password or url.host or url.port: - raise exc.ArgumentError( - "Invalid SQLite URL: %s\n" - "Valid SQLite URL forms are:\n" - " sqlite:///:memory: (or, sqlite://)\n" - " sqlite:///relative/path/to/file.db\n" - " sqlite:////absolute/path/to/file.db" % (url,)) - filename = url.database or ':memory:' - if filename != ':memory:': - filename = os.path.abspath(filename) - - opts = url.query.copy() - util.coerce_kw_type(opts, 'timeout', float) - util.coerce_kw_type(opts, 'isolation_level', str) - util.coerce_kw_type(opts, 'detect_types', int) - util.coerce_kw_type(opts, 'check_same_thread', bool) - util.coerce_kw_type(opts, 'cached_statements', int) - - return ([filename], opts) - - def is_disconnect(self, e, connection, cursor): - return isinstance(e, self.dbapi.ProgrammingError) and \ - "Cannot operate on a closed database." in str(e) - -dialect = SQLiteDialect_pysqlite diff --git a/libs/sqlalchemy/dialects/sybase/__init__.py b/libs/sqlalchemy/dialects/sybase/__init__.py deleted file mode 100644 index 85f9dd9c..00000000 --- a/libs/sqlalchemy/dialects/sybase/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# sybase/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from sqlalchemy.dialects.sybase import base, pysybase, pyodbc - -# default dialect -base.dialect = pyodbc.dialect - -from .base import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\ - TEXT, DATE, DATETIME, FLOAT, NUMERIC,\ - BIGINT, INT, INTEGER, SMALLINT, BINARY,\ - VARBINARY, UNITEXT, UNICHAR, UNIVARCHAR,\ - IMAGE, BIT, MONEY, SMALLMONEY, TINYINT,\ - dialect - - -__all__ = ( - 'CHAR', 'VARCHAR', 'TIME', 'NCHAR', 'NVARCHAR', - 'TEXT', 'DATE', 'DATETIME', 'FLOAT', 'NUMERIC', - 'BIGINT', 'INT', 'INTEGER', 'SMALLINT', 'BINARY', - 'VARBINARY', 'UNITEXT', 'UNICHAR', 'UNIVARCHAR', - 'IMAGE', 'BIT', 'MONEY', 'SMALLMONEY', 'TINYINT', - 'dialect' -) diff --git a/libs/sqlalchemy/dialects/sybase/base.py b/libs/sqlalchemy/dialects/sybase/base.py deleted file mode 100644 index 2f58aed9..00000000 --- a/libs/sqlalchemy/dialects/sybase/base.py +++ /dev/null @@ -1,814 +0,0 @@ -# sybase/base.py -# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors -# get_select_precolumns(), limit_clause() implementation -# copyright (C) 2007 Fisch Asset Management -# AG http://www.fam.ch, with coding by Alexander Houben -# alexander.houben@thor-solutions.ch -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -.. dialect:: sybase - :name: Sybase - -.. note:: - - The Sybase dialect functions on current SQLAlchemy versions - but is not regularly tested, and may have many issues and - caveats not currently handled. - -""" -import operator -import re - -from sqlalchemy.sql import compiler, expression, text, bindparam -from sqlalchemy.engine import default, base, reflection -from sqlalchemy import types as sqltypes -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy import schema as sa_schema -from sqlalchemy import util, sql, exc - -from sqlalchemy.types import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\ - TEXT, DATE, DATETIME, FLOAT, NUMERIC,\ - BIGINT, INT, INTEGER, SMALLINT, BINARY,\ - VARBINARY, DECIMAL, TIMESTAMP, Unicode,\ - UnicodeText, REAL - -RESERVED_WORDS = set([ - "add", "all", "alter", "and", - "any", "as", "asc", "backup", - "begin", "between", "bigint", "binary", - "bit", "bottom", "break", "by", - "call", "capability", "cascade", "case", - "cast", "char", "char_convert", "character", - "check", "checkpoint", "close", "comment", - "commit", "connect", "constraint", "contains", - "continue", "convert", "create", "cross", - "cube", "current", "current_timestamp", "current_user", - "cursor", "date", "dbspace", "deallocate", - "dec", "decimal", "declare", "default", - "delete", "deleting", "desc", "distinct", - "do", "double", "drop", "dynamic", - "else", "elseif", "encrypted", "end", - "endif", "escape", "except", "exception", - "exec", "execute", "existing", "exists", - "externlogin", "fetch", "first", "float", - "for", "force", "foreign", "forward", - "from", "full", "goto", "grant", - "group", "having", "holdlock", "identified", - "if", "in", "index", "index_lparen", - "inner", "inout", "insensitive", "insert", - "inserting", "install", "instead", "int", - "integer", "integrated", "intersect", "into", - "iq", "is", "isolation", "join", - "key", "lateral", "left", "like", - "lock", "login", "long", "match", - "membership", "message", "mode", "modify", - "natural", "new", "no", "noholdlock", - "not", "notify", "null", "numeric", - "of", "off", "on", "open", - "option", "options", "or", "order", - "others", "out", "outer", "over", - "passthrough", "precision", "prepare", "primary", - "print", "privileges", "proc", "procedure", - "publication", "raiserror", "readtext", "real", - "reference", "references", "release", "remote", - "remove", "rename", "reorganize", "resource", - "restore", "restrict", "return", "revoke", - "right", "rollback", "rollup", "save", - "savepoint", "scroll", "select", "sensitive", - "session", "set", "setuser", "share", - "smallint", "some", "sqlcode", "sqlstate", - "start", "stop", "subtrans", "subtransaction", - "synchronize", "syntax_error", "table", "temporary", - "then", "time", "timestamp", "tinyint", - "to", "top", "tran", "trigger", - "truncate", "tsequal", "unbounded", "union", - "unique", "unknown", "unsigned", "update", - "updating", "user", "using", "validate", - "values", "varbinary", "varchar", "variable", - "varying", "view", "wait", "waitfor", - "when", "where", "while", "window", - "with", "with_cube", "with_lparen", "with_rollup", - "within", "work", "writetext", - ]) - - -class _SybaseUnitypeMixin(object): - """these types appear to return a buffer object.""" - - def result_processor(self, dialect, coltype): - def process(value): - if value is not None: - return str(value) # decode("ucs-2") - else: - return None - return process - - -class UNICHAR(_SybaseUnitypeMixin, sqltypes.Unicode): - __visit_name__ = 'UNICHAR' - - -class UNIVARCHAR(_SybaseUnitypeMixin, sqltypes.Unicode): - __visit_name__ = 'UNIVARCHAR' - - -class UNITEXT(_SybaseUnitypeMixin, sqltypes.UnicodeText): - __visit_name__ = 'UNITEXT' - - -class TINYINT(sqltypes.Integer): - __visit_name__ = 'TINYINT' - - -class BIT(sqltypes.TypeEngine): - __visit_name__ = 'BIT' - - -class MONEY(sqltypes.TypeEngine): - __visit_name__ = "MONEY" - - -class SMALLMONEY(sqltypes.TypeEngine): - __visit_name__ = "SMALLMONEY" - - -class UNIQUEIDENTIFIER(sqltypes.TypeEngine): - __visit_name__ = "UNIQUEIDENTIFIER" - - -class IMAGE(sqltypes.LargeBinary): - __visit_name__ = 'IMAGE' - - -class SybaseTypeCompiler(compiler.GenericTypeCompiler): - def visit_large_binary(self, type_): - return self.visit_IMAGE(type_) - - def visit_boolean(self, type_): - return self.visit_BIT(type_) - - def visit_unicode(self, type_): - return self.visit_NVARCHAR(type_) - - def visit_UNICHAR(self, type_): - return "UNICHAR(%d)" % type_.length - - def visit_UNIVARCHAR(self, type_): - return "UNIVARCHAR(%d)" % type_.length - - def visit_UNITEXT(self, type_): - return "UNITEXT" - - def visit_TINYINT(self, type_): - return "TINYINT" - - def visit_IMAGE(self, type_): - return "IMAGE" - - def visit_BIT(self, type_): - return "BIT" - - def visit_MONEY(self, type_): - return "MONEY" - - def visit_SMALLMONEY(self, type_): - return "SMALLMONEY" - - def visit_UNIQUEIDENTIFIER(self, type_): - return "UNIQUEIDENTIFIER" - -ischema_names = { - 'bigint': BIGINT, - 'int': INTEGER, - 'integer': INTEGER, - 'smallint': SMALLINT, - 'tinyint': TINYINT, - 'unsigned bigint': BIGINT, # TODO: unsigned flags - 'unsigned int': INTEGER, # TODO: unsigned flags - 'unsigned smallint': SMALLINT, # TODO: unsigned flags - 'numeric': NUMERIC, - 'decimal': DECIMAL, - 'dec': DECIMAL, - 'float': FLOAT, - 'double': NUMERIC, # TODO - 'double precision': NUMERIC, # TODO - 'real': REAL, - 'smallmoney': SMALLMONEY, - 'money': MONEY, - 'smalldatetime': DATETIME, - 'datetime': DATETIME, - 'date': DATE, - 'time': TIME, - 'char': CHAR, - 'character': CHAR, - 'varchar': VARCHAR, - 'character varying': VARCHAR, - 'char varying': VARCHAR, - 'unichar': UNICHAR, - 'unicode character': UNIVARCHAR, - 'nchar': NCHAR, - 'national char': NCHAR, - 'national character': NCHAR, - 'nvarchar': NVARCHAR, - 'nchar varying': NVARCHAR, - 'national char varying': NVARCHAR, - 'national character varying': NVARCHAR, - 'text': TEXT, - 'unitext': UNITEXT, - 'binary': BINARY, - 'varbinary': VARBINARY, - 'image': IMAGE, - 'bit': BIT, - -# not in documentation for ASE 15.7 - 'long varchar': TEXT, # TODO - 'timestamp': TIMESTAMP, - 'uniqueidentifier': UNIQUEIDENTIFIER, - -} - - -class SybaseInspector(reflection.Inspector): - - def __init__(self, conn): - reflection.Inspector.__init__(self, conn) - - def get_table_id(self, table_name, schema=None): - """Return the table id from `table_name` and `schema`.""" - - return self.dialect.get_table_id(self.bind, table_name, schema, - info_cache=self.info_cache) - - -class SybaseExecutionContext(default.DefaultExecutionContext): - _enable_identity_insert = False - - def set_ddl_autocommit(self, connection, value): - """Must be implemented by subclasses to accommodate DDL executions. - - "connection" is the raw unwrapped DBAPI connection. "value" - is True or False. when True, the connection should be configured - such that a DDL can take place subsequently. when False, - a DDL has taken place and the connection should be resumed - into non-autocommit mode. - - """ - raise NotImplementedError() - - def pre_exec(self): - if self.isinsert: - tbl = self.compiled.statement.table - seq_column = tbl._autoincrement_column - insert_has_sequence = seq_column is not None - - if insert_has_sequence: - self._enable_identity_insert = \ - seq_column.key in self.compiled_parameters[0] - else: - self._enable_identity_insert = False - - if self._enable_identity_insert: - self.cursor.execute("SET IDENTITY_INSERT %s ON" % - self.dialect.identifier_preparer.format_table(tbl)) - - if self.isddl: - # TODO: to enhance this, we can detect "ddl in tran" on the - # database settings. this error message should be improved to - # include a note about that. - if not self.should_autocommit: - raise exc.InvalidRequestError( - "The Sybase dialect only supports " - "DDL in 'autocommit' mode at this time.") - - self.root_connection.engine.logger.info( - "AUTOCOMMIT (Assuming no Sybase 'ddl in tran')") - - self.set_ddl_autocommit( - self.root_connection.connection.connection, - True) - - def post_exec(self): - if self.isddl: - self.set_ddl_autocommit(self.root_connection, False) - - if self._enable_identity_insert: - self.cursor.execute( - "SET IDENTITY_INSERT %s OFF" % - self.dialect.identifier_preparer. - format_table(self.compiled.statement.table) - ) - - def get_lastrowid(self): - cursor = self.create_cursor() - cursor.execute("SELECT @@identity AS lastrowid") - lastrowid = cursor.fetchone()[0] - cursor.close() - return lastrowid - - -class SybaseSQLCompiler(compiler.SQLCompiler): - ansi_bind_rules = True - - extract_map = util.update_copy( - compiler.SQLCompiler.extract_map, - { - 'doy': 'dayofyear', - 'dow': 'weekday', - 'milliseconds': 'millisecond' - }) - - def get_select_precolumns(self, select): - s = select._distinct and "DISTINCT " or "" - # TODO: don't think Sybase supports - # bind params for FIRST / TOP - if select._limit: - #if select._limit == 1: - #s += "FIRST " - #else: - #s += "TOP %s " % (select._limit,) - s += "TOP %s " % (select._limit,) - if select._offset: - if not select._limit: - # FIXME: sybase doesn't allow an offset without a limit - # so use a huge value for TOP here - s += "TOP 1000000 " - s += "START AT %s " % (select._offset + 1,) - return s - - def get_from_hint_text(self, table, text): - return text - - def limit_clause(self, select): - # Limit in sybase is after the select keyword - return "" - - def visit_extract(self, extract, **kw): - field = self.extract_map.get(extract.field, extract.field) - return 'DATEPART("%s", %s)' % ( - field, self.process(extract.expr, **kw)) - - def visit_now_func(self, fn, **kw): - return "GETDATE()" - - def for_update_clause(self, select): - # "FOR UPDATE" is only allowed on "DECLARE CURSOR" - # which SQLAlchemy doesn't use - return '' - - def order_by_clause(self, select, **kw): - kw['literal_binds'] = True - order_by = self.process(select._order_by_clause, **kw) - - # SybaseSQL only allows ORDER BY in subqueries if there is a LIMIT - if order_by and (not self.is_subquery() or select._limit): - return " ORDER BY " + order_by - else: - return "" - - -class SybaseDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, **kwargs): - colspec = self.preparer.format_column(column) + " " + \ - self.dialect.type_compiler.process(column.type) - - if column.table is None: - raise exc.CompileError( - "The Sybase dialect requires Table-bound " - "columns in order to generate DDL") - seq_col = column.table._autoincrement_column - - # install a IDENTITY Sequence if we have an implicit IDENTITY column - if seq_col is column: - sequence = isinstance(column.default, sa_schema.Sequence) \ - and column.default - if sequence: - start, increment = sequence.start or 1, \ - sequence.increment or 1 - else: - start, increment = 1, 1 - if (start, increment) == (1, 1): - colspec += " IDENTITY" - else: - # TODO: need correct syntax for this - colspec += " IDENTITY(%s,%s)" % (start, increment) - else: - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - if column.nullable is not None: - if not column.nullable or column.primary_key: - colspec += " NOT NULL" - else: - colspec += " NULL" - - return colspec - - def visit_drop_index(self, drop): - index = drop.element - return "\nDROP INDEX %s.%s" % ( - self.preparer.quote_identifier(index.table.name), - self._prepared_index_name(drop.element, - include_schema=False) - ) - - -class SybaseIdentifierPreparer(compiler.IdentifierPreparer): - reserved_words = RESERVED_WORDS - - -class SybaseDialect(default.DefaultDialect): - name = 'sybase' - supports_unicode_statements = False - supports_sane_rowcount = False - supports_sane_multi_rowcount = False - - supports_native_boolean = False - supports_unicode_binds = False - postfetch_lastrowid = True - - colspecs = {} - ischema_names = ischema_names - - type_compiler = SybaseTypeCompiler - statement_compiler = SybaseSQLCompiler - ddl_compiler = SybaseDDLCompiler - preparer = SybaseIdentifierPreparer - inspector = SybaseInspector - - def _get_default_schema_name(self, connection): - return connection.scalar( - text("SELECT user_name() as user_name", - typemap={'user_name': Unicode}) - ) - - def initialize(self, connection): - super(SybaseDialect, self).initialize(connection) - if self.server_version_info is not None and\ - self.server_version_info < (15, ): - self.max_identifier_length = 30 - else: - self.max_identifier_length = 255 - - def get_table_id(self, connection, table_name, schema=None, **kw): - """Fetch the id for schema.table_name. - - Several reflection methods require the table id. The idea for using - this method is that it can be fetched one time and cached for - subsequent calls. - - """ - - table_id = None - if schema is None: - schema = self.default_schema_name - - TABLEID_SQL = text(""" - SELECT o.id AS id - FROM sysobjects o JOIN sysusers u ON o.uid=u.uid - WHERE u.name = :schema_name - AND o.name = :table_name - AND o.type in ('U', 'V') - """) - - if util.py2k: - if isinstance(schema, unicode): - schema = schema.encode("ascii") - if isinstance(table_name, unicode): - table_name = table_name.encode("ascii") - result = connection.execute(TABLEID_SQL, - schema_name=schema, - table_name=table_name) - table_id = result.scalar() - if table_id is None: - raise exc.NoSuchTableError(table_name) - return table_id - - @reflection.cache - def get_columns(self, connection, table_name, schema=None, **kw): - table_id = self.get_table_id(connection, table_name, schema, - info_cache=kw.get("info_cache")) - - COLUMN_SQL = text(""" - SELECT col.name AS name, - t.name AS type, - (col.status & 8) AS nullable, - (col.status & 128) AS autoincrement, - com.text AS 'default', - col.prec AS precision, - col.scale AS scale, - col.length AS length - FROM systypes t, syscolumns col LEFT OUTER JOIN syscomments com ON - col.cdefault = com.id - WHERE col.usertype = t.usertype - AND col.id = :table_id - ORDER BY col.colid - """) - - results = connection.execute(COLUMN_SQL, table_id=table_id) - - columns = [] - for (name, type_, nullable, autoincrement, default, precision, scale, - length) in results: - col_info = self._get_column_info(name, type_, bool(nullable), - bool(autoincrement), default, precision, scale, - length) - columns.append(col_info) - - return columns - - def _get_column_info(self, name, type_, nullable, autoincrement, default, - precision, scale, length): - - coltype = self.ischema_names.get(type_, None) - - kwargs = {} - - if coltype in (NUMERIC, DECIMAL): - args = (precision, scale) - elif coltype == FLOAT: - args = (precision,) - elif coltype in (CHAR, VARCHAR, UNICHAR, UNIVARCHAR, NCHAR, NVARCHAR): - args = (length,) - else: - args = () - - if coltype: - coltype = coltype(*args, **kwargs) - #is this necessary - #if is_array: - # coltype = ARRAY(coltype) - else: - util.warn("Did not recognize type '%s' of column '%s'" % - (type_, name)) - coltype = sqltypes.NULLTYPE - - if default: - default = re.sub("DEFAULT", "", default).strip() - default = re.sub("^'(.*)'$", lambda m: m.group(1), default) - else: - default = None - - column_info = dict(name=name, type=coltype, nullable=nullable, - default=default, autoincrement=autoincrement) - return column_info - - @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - - table_id = self.get_table_id(connection, table_name, schema, - info_cache=kw.get("info_cache")) - - table_cache = {} - column_cache = {} - foreign_keys = [] - - table_cache[table_id] = {"name": table_name, "schema": schema} - - COLUMN_SQL = text(""" - SELECT c.colid AS id, c.name AS name - FROM syscolumns c - WHERE c.id = :table_id - """) - - results = connection.execute(COLUMN_SQL, table_id=table_id) - columns = {} - for col in results: - columns[col["id"]] = col["name"] - column_cache[table_id] = columns - - REFCONSTRAINT_SQL = text(""" - SELECT o.name AS name, r.reftabid AS reftable_id, - r.keycnt AS 'count', - r.fokey1 AS fokey1, r.fokey2 AS fokey2, r.fokey3 AS fokey3, - r.fokey4 AS fokey4, r.fokey5 AS fokey5, r.fokey6 AS fokey6, - r.fokey7 AS fokey7, r.fokey1 AS fokey8, r.fokey9 AS fokey9, - r.fokey10 AS fokey10, r.fokey11 AS fokey11, r.fokey12 AS fokey12, - r.fokey13 AS fokey13, r.fokey14 AS fokey14, r.fokey15 AS fokey15, - r.fokey16 AS fokey16, - r.refkey1 AS refkey1, r.refkey2 AS refkey2, r.refkey3 AS refkey3, - r.refkey4 AS refkey4, r.refkey5 AS refkey5, r.refkey6 AS refkey6, - r.refkey7 AS refkey7, r.refkey1 AS refkey8, r.refkey9 AS refkey9, - r.refkey10 AS refkey10, r.refkey11 AS refkey11, - r.refkey12 AS refkey12, r.refkey13 AS refkey13, - r.refkey14 AS refkey14, r.refkey15 AS refkey15, - r.refkey16 AS refkey16 - FROM sysreferences r JOIN sysobjects o on r.tableid = o.id - WHERE r.tableid = :table_id - """) - referential_constraints = connection.execute(REFCONSTRAINT_SQL, - table_id=table_id) - - REFTABLE_SQL = text(""" - SELECT o.name AS name, u.name AS 'schema' - FROM sysobjects o JOIN sysusers u ON o.uid = u.uid - WHERE o.id = :table_id - """) - - for r in referential_constraints: - reftable_id = r["reftable_id"] - - if reftable_id not in table_cache: - c = connection.execute(REFTABLE_SQL, table_id=reftable_id) - reftable = c.fetchone() - c.close() - table_info = {"name": reftable["name"], "schema": None} - if (schema is not None or - reftable["schema"] != self.default_schema_name): - table_info["schema"] = reftable["schema"] - - table_cache[reftable_id] = table_info - results = connection.execute(COLUMN_SQL, table_id=reftable_id) - reftable_columns = {} - for col in results: - reftable_columns[col["id"]] = col["name"] - column_cache[reftable_id] = reftable_columns - - reftable = table_cache[reftable_id] - reftable_columns = column_cache[reftable_id] - - constrained_columns = [] - referred_columns = [] - for i in range(1, r["count"] + 1): - constrained_columns.append(columns[r["fokey%i" % i]]) - referred_columns.append(reftable_columns[r["refkey%i" % i]]) - - fk_info = { - "constrained_columns": constrained_columns, - "referred_schema": reftable["schema"], - "referred_table": reftable["name"], - "referred_columns": referred_columns, - "name": r["name"] - } - - foreign_keys.append(fk_info) - - return foreign_keys - - @reflection.cache - def get_indexes(self, connection, table_name, schema=None, **kw): - table_id = self.get_table_id(connection, table_name, schema, - info_cache=kw.get("info_cache")) - - INDEX_SQL = text(""" - SELECT object_name(i.id) AS table_name, - i.keycnt AS 'count', - i.name AS name, - (i.status & 0x2) AS 'unique', - index_col(object_name(i.id), i.indid, 1) AS col_1, - index_col(object_name(i.id), i.indid, 2) AS col_2, - index_col(object_name(i.id), i.indid, 3) AS col_3, - index_col(object_name(i.id), i.indid, 4) AS col_4, - index_col(object_name(i.id), i.indid, 5) AS col_5, - index_col(object_name(i.id), i.indid, 6) AS col_6, - index_col(object_name(i.id), i.indid, 7) AS col_7, - index_col(object_name(i.id), i.indid, 8) AS col_8, - index_col(object_name(i.id), i.indid, 9) AS col_9, - index_col(object_name(i.id), i.indid, 10) AS col_10, - index_col(object_name(i.id), i.indid, 11) AS col_11, - index_col(object_name(i.id), i.indid, 12) AS col_12, - index_col(object_name(i.id), i.indid, 13) AS col_13, - index_col(object_name(i.id), i.indid, 14) AS col_14, - index_col(object_name(i.id), i.indid, 15) AS col_15, - index_col(object_name(i.id), i.indid, 16) AS col_16 - FROM sysindexes i, sysobjects o - WHERE o.id = i.id - AND o.id = :table_id - AND (i.status & 2048) = 0 - AND i.indid BETWEEN 1 AND 254 - """) - - results = connection.execute(INDEX_SQL, table_id=table_id) - indexes = [] - for r in results: - column_names = [] - for i in range(1, r["count"]): - column_names.append(r["col_%i" % (i,)]) - index_info = {"name": r["name"], - "unique": bool(r["unique"]), - "column_names": column_names} - indexes.append(index_info) - - return indexes - - @reflection.cache - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - table_id = self.get_table_id(connection, table_name, schema, - info_cache=kw.get("info_cache")) - - PK_SQL = text(""" - SELECT object_name(i.id) AS table_name, - i.keycnt AS 'count', - i.name AS name, - index_col(object_name(i.id), i.indid, 1) AS pk_1, - index_col(object_name(i.id), i.indid, 2) AS pk_2, - index_col(object_name(i.id), i.indid, 3) AS pk_3, - index_col(object_name(i.id), i.indid, 4) AS pk_4, - index_col(object_name(i.id), i.indid, 5) AS pk_5, - index_col(object_name(i.id), i.indid, 6) AS pk_6, - index_col(object_name(i.id), i.indid, 7) AS pk_7, - index_col(object_name(i.id), i.indid, 8) AS pk_8, - index_col(object_name(i.id), i.indid, 9) AS pk_9, - index_col(object_name(i.id), i.indid, 10) AS pk_10, - index_col(object_name(i.id), i.indid, 11) AS pk_11, - index_col(object_name(i.id), i.indid, 12) AS pk_12, - index_col(object_name(i.id), i.indid, 13) AS pk_13, - index_col(object_name(i.id), i.indid, 14) AS pk_14, - index_col(object_name(i.id), i.indid, 15) AS pk_15, - index_col(object_name(i.id), i.indid, 16) AS pk_16 - FROM sysindexes i, sysobjects o - WHERE o.id = i.id - AND o.id = :table_id - AND (i.status & 2048) = 2048 - AND i.indid BETWEEN 1 AND 254 - """) - - results = connection.execute(PK_SQL, table_id=table_id) - pks = results.fetchone() - results.close() - - constrained_columns = [] - for i in range(1, pks["count"] + 1): - constrained_columns.append(pks["pk_%i" % (i,)]) - return {"constrained_columns": constrained_columns, - "name": pks["name"]} - - @reflection.cache - def get_schema_names(self, connection, **kw): - - SCHEMA_SQL = text("SELECT u.name AS name FROM sysusers u") - - schemas = connection.execute(SCHEMA_SQL) - - return [s["name"] for s in schemas] - - @reflection.cache - def get_table_names(self, connection, schema=None, **kw): - if schema is None: - schema = self.default_schema_name - - TABLE_SQL = text(""" - SELECT o.name AS name - FROM sysobjects o JOIN sysusers u ON o.uid = u.uid - WHERE u.name = :schema_name - AND o.type = 'U' - """) - - if util.py2k: - if isinstance(schema, unicode): - schema = schema.encode("ascii") - - tables = connection.execute(TABLE_SQL, schema_name=schema) - - return [t["name"] for t in tables] - - @reflection.cache - def get_view_definition(self, connection, view_name, schema=None, **kw): - if schema is None: - schema = self.default_schema_name - - VIEW_DEF_SQL = text(""" - SELECT c.text - FROM syscomments c JOIN sysobjects o ON c.id = o.id - WHERE o.name = :view_name - AND o.type = 'V' - """) - - if util.py2k: - if isinstance(view_name, unicode): - view_name = view_name.encode("ascii") - - view = connection.execute(VIEW_DEF_SQL, view_name=view_name) - - return view.scalar() - - @reflection.cache - def get_view_names(self, connection, schema=None, **kw): - if schema is None: - schema = self.default_schema_name - - VIEW_SQL = text(""" - SELECT o.name AS name - FROM sysobjects o JOIN sysusers u ON o.uid = u.uid - WHERE u.name = :schema_name - AND o.type = 'V' - """) - - if util.py2k: - if isinstance(schema, unicode): - schema = schema.encode("ascii") - views = connection.execute(VIEW_SQL, schema_name=schema) - - return [v["name"] for v in views] - - def has_table(self, connection, table_name, schema=None): - try: - self.get_table_id(connection, table_name, schema) - except exc.NoSuchTableError: - return False - else: - return True diff --git a/libs/sqlalchemy/dialects/sybase/mxodbc.py b/libs/sqlalchemy/dialects/sybase/mxodbc.py deleted file mode 100644 index f14d1c42..00000000 --- a/libs/sqlalchemy/dialects/sybase/mxodbc.py +++ /dev/null @@ -1,32 +0,0 @@ -# sybase/mxodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -""" - -.. dialect:: sybase+mxodbc - :name: mxODBC - :dbapi: mxodbc - :connectstring: sybase+mxodbc://:@ - :url: http://www.egenix.com/ - -.. note:: - - This dialect is a stub only and is likely non functional at this time. - - -""" -from sqlalchemy.dialects.sybase.base import SybaseDialect -from sqlalchemy.dialects.sybase.base import SybaseExecutionContext -from sqlalchemy.connectors.mxodbc import MxODBCConnector - - -class SybaseExecutionContext_mxodbc(SybaseExecutionContext): - pass - - -class SybaseDialect_mxodbc(MxODBCConnector, SybaseDialect): - execution_ctx_cls = SybaseExecutionContext_mxodbc - -dialect = SybaseDialect_mxodbc diff --git a/libs/sqlalchemy/dialects/sybase/pyodbc.py b/libs/sqlalchemy/dialects/sybase/pyodbc.py deleted file mode 100644 index f773e5a6..00000000 --- a/libs/sqlalchemy/dialects/sybase/pyodbc.py +++ /dev/null @@ -1,84 +0,0 @@ -# sybase/pyodbc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: sybase+pyodbc - :name: PyODBC - :dbapi: pyodbc - :connectstring: sybase+pyodbc://:@[/] - :url: http://pypi.python.org/pypi/pyodbc/ - - -Unicode Support ---------------- - -The pyodbc driver currently supports usage of these Sybase types with -Unicode or multibyte strings:: - - CHAR - NCHAR - NVARCHAR - TEXT - VARCHAR - -Currently *not* supported are:: - - UNICHAR - UNITEXT - UNIVARCHAR - -""" - -from sqlalchemy.dialects.sybase.base import SybaseDialect,\ - SybaseExecutionContext -from sqlalchemy.connectors.pyodbc import PyODBCConnector -from sqlalchemy import types as sqltypes, processors -import decimal - - -class _SybNumeric_pyodbc(sqltypes.Numeric): - """Turns Decimals with adjusted() < -6 into floats. - - It's not yet known how to get decimals with many - significant digits or very large adjusted() into Sybase - via pyodbc. - - """ - - def bind_processor(self, dialect): - super_process = super(_SybNumeric_pyodbc, self).\ - bind_processor(dialect) - - def process(value): - if self.asdecimal and \ - isinstance(value, decimal.Decimal): - - if value.adjusted() < -6: - return processors.to_float(value) - - if super_process: - return super_process(value) - else: - return value - return process - - -class SybaseExecutionContext_pyodbc(SybaseExecutionContext): - def set_ddl_autocommit(self, connection, value): - if value: - connection.autocommit = True - else: - connection.autocommit = False - - -class SybaseDialect_pyodbc(PyODBCConnector, SybaseDialect): - execution_ctx_cls = SybaseExecutionContext_pyodbc - - colspecs = { - sqltypes.Numeric: _SybNumeric_pyodbc, - } - -dialect = SybaseDialect_pyodbc diff --git a/libs/sqlalchemy/dialects/sybase/pysybase.py b/libs/sqlalchemy/dialects/sybase/pysybase.py deleted file mode 100644 index 664bd9ac..00000000 --- a/libs/sqlalchemy/dialects/sybase/pysybase.py +++ /dev/null @@ -1,100 +0,0 @@ -# sybase/pysybase.py -# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -.. dialect:: sybase+pysybase - :name: Python-Sybase - :dbapi: Sybase - :connectstring: sybase+pysybase://:@/[database name] - :url: http://python-sybase.sourceforge.net/ - -Unicode Support ---------------- - -The python-sybase driver does not appear to support non-ASCII strings of any -kind at this time. - -""" - -from sqlalchemy import types as sqltypes, processors -from sqlalchemy.dialects.sybase.base import SybaseDialect, \ - SybaseExecutionContext, SybaseSQLCompiler - - -class _SybNumeric(sqltypes.Numeric): - def result_processor(self, dialect, type_): - if not self.asdecimal: - return processors.to_float - else: - return sqltypes.Numeric.result_processor(self, dialect, type_) - - -class SybaseExecutionContext_pysybase(SybaseExecutionContext): - - def set_ddl_autocommit(self, dbapi_connection, value): - if value: - # call commit() on the Sybase connection directly, - # to avoid any side effects of calling a Connection - # transactional method inside of pre_exec() - dbapi_connection.commit() - - def pre_exec(self): - SybaseExecutionContext.pre_exec(self) - - for param in self.parameters: - for key in list(param): - param["@" + key] = param[key] - del param[key] - - -class SybaseSQLCompiler_pysybase(SybaseSQLCompiler): - def bindparam_string(self, name, **kw): - return "@" + name - - -class SybaseDialect_pysybase(SybaseDialect): - driver = 'pysybase' - execution_ctx_cls = SybaseExecutionContext_pysybase - statement_compiler = SybaseSQLCompiler_pysybase - - colspecs = { - sqltypes.Numeric: _SybNumeric, - sqltypes.Float: sqltypes.Float - } - - @classmethod - def dbapi(cls): - import Sybase - return Sybase - - def create_connect_args(self, url): - opts = url.translate_connect_args(username='user', password='passwd') - - return ([opts.pop('host')], opts) - - def do_executemany(self, cursor, statement, parameters, context=None): - # calling python-sybase executemany yields: - # TypeError: string too long for buffer - for param in parameters: - cursor.execute(statement, param) - - def _get_server_version_info(self, connection): - vers = connection.scalar("select @@version_number") - # i.e. 15500, 15000, 12500 == (15, 5, 0, 0), (15, 0, 0, 0), - # (12, 5, 0, 0) - return (vers / 1000, vers % 1000 / 100, vers % 100 / 10, vers % 10) - - def is_disconnect(self, e, connection, cursor): - if isinstance(e, (self.dbapi.OperationalError, - self.dbapi.ProgrammingError)): - msg = str(e) - return ('Unable to complete network request to host' in msg or - 'Invalid connection state' in msg or - 'Invalid cursor state' in msg) - else: - return False - -dialect = SybaseDialect_pysybase diff --git a/libs/sqlalchemy/dialects/type_migration_guidelines.txt b/libs/sqlalchemy/dialects/type_migration_guidelines.txt deleted file mode 100644 index 1ca15f7f..00000000 --- a/libs/sqlalchemy/dialects/type_migration_guidelines.txt +++ /dev/null @@ -1,145 +0,0 @@ -Rules for Migrating TypeEngine classes to 0.6 ---------------------------------------------- - -1. the TypeEngine classes are used for: - - a. Specifying behavior which needs to occur for bind parameters - or result row columns. - - b. Specifying types that are entirely specific to the database - in use and have no analogue in the sqlalchemy.types package. - - c. Specifying types where there is an analogue in sqlalchemy.types, - but the database in use takes vendor-specific flags for those - types. - - d. If a TypeEngine class doesn't provide any of this, it should be - *removed* from the dialect. - -2. the TypeEngine classes are *no longer* used for generating DDL. Dialects -now have a TypeCompiler subclass which uses the same visit_XXX model as -other compilers. - -3. the "ischema_names" and "colspecs" dictionaries are now required members on -the Dialect class. - -4. The names of types within dialects are now important. If a dialect-specific type -is a subclass of an existing generic type and is only provided for bind/result behavior, -the current mixed case naming can remain, i.e. _PGNumeric for Numeric - in this case, -end users would never need to use _PGNumeric directly. However, if a dialect-specific -type is specifying a type *or* arguments that are not present generically, it should -match the real name of the type on that backend, in uppercase. E.g. postgresql.INET, -mysql.ENUM, postgresql.ARRAY. - -Or follow this handy flowchart: - - is the type meant to provide bind/result is the type the same name as an - behavior to a generic type (i.e. MixedCase) ---- no ---> UPPERCASE type in types.py ? - type in types.py ? | | - | no yes - yes | | - | | does your type need special - | +<--- yes --- behavior or arguments ? - | | | - | | no - name the type using | | - _MixedCase, i.e. v V - _OracleBoolean. it name the type don't make a - stays private to the dialect identically as that type, make sure the dialect's - and is invoked *only* via within the DB, base.py imports the types.py - the colspecs dict. using UPPERCASE UPPERCASE name into its namespace - | (i.e. BIT, NCHAR, INTERVAL). - | Users can import it. - | | - v v - subclass the closest is the name of this type - MixedCase type types.py, identical to an UPPERCASE - i.e. <--- no ------- name in types.py ? - class _DateTime(types.DateTime), - class DATETIME2(types.DateTime), | - class BIT(types.TypeEngine). yes - | - v - the type should - subclass the - UPPERCASE - type in types.py - (i.e. class BLOB(types.BLOB)) - - -Example 1. pysqlite needs bind/result processing for the DateTime type in types.py, -which applies to all DateTimes and subclasses. It's named _SLDateTime and -subclasses types.DateTime. - -Example 2. MS-SQL has a TIME type which takes a non-standard "precision" argument -that is rendered within DDL. So it's named TIME in the MS-SQL dialect's base.py, -and subclasses types.TIME. Users can then say mssql.TIME(precision=10). - -Example 3. MS-SQL dialects also need special bind/result processing for date -But its DATE type doesn't render DDL differently than that of a plain -DATE, i.e. it takes no special arguments. Therefore we are just adding behavior -to types.Date, so it's named _MSDate in the MS-SQL dialect's base.py, and subclasses -types.Date. - -Example 4. MySQL has a SET type, there's no analogue for this in types.py. So -MySQL names it SET in the dialect's base.py, and it subclasses types.String, since -it ultimately deals with strings. - -Example 5. Postgresql has a DATETIME type. The DBAPIs handle dates correctly, -and no special arguments are used in PG's DDL beyond what types.py provides. -Postgresql dialect therefore imports types.DATETIME into its base.py. - -Ideally one should be able to specify a schema using names imported completely from a -dialect, all matching the real name on that backend: - - from sqlalchemy.dialects.postgresql import base as pg - - t = Table('mytable', metadata, - Column('id', pg.INTEGER, primary_key=True), - Column('name', pg.VARCHAR(300)), - Column('inetaddr', pg.INET) - ) - -where above, the INTEGER and VARCHAR types are ultimately from sqlalchemy.types, -but the PG dialect makes them available in its own namespace. - -5. "colspecs" now is a dictionary of generic or uppercased types from sqlalchemy.types -linked to types specified in the dialect. Again, if a type in the dialect does not -specify any special behavior for bind_processor() or result_processor() and does not -indicate a special type only available in this database, it must be *removed* from the -module and from this dictionary. - -6. "ischema_names" indicates string descriptions of types as returned from the database -linked to TypeEngine classes. - - a. The string name should be matched to the most specific type possible within - sqlalchemy.types, unless there is no matching type within sqlalchemy.types in which - case it points to a dialect type. *It doesn't matter* if the dialect has it's - own subclass of that type with special bind/result behavior - reflect to the types.py - UPPERCASE type as much as possible. With very few exceptions, all types - should reflect to an UPPERCASE type. - - b. If the dialect contains a matching dialect-specific type that takes extra arguments - which the generic one does not, then point to the dialect-specific type. E.g. - mssql.VARCHAR takes a "collation" parameter which should be preserved. - -5. DDL, or what was formerly issued by "get_col_spec()", is now handled exclusively by -a subclass of compiler.GenericTypeCompiler. - - a. your TypeCompiler class will receive generic and uppercase types from - sqlalchemy.types. Do not assume the presence of dialect-specific attributes on - these types. - - b. the visit_UPPERCASE methods on GenericTypeCompiler should *not* be overridden with - methods that produce a different DDL name. Uppercase types don't do any kind of - "guessing" - if visit_TIMESTAMP is called, the DDL should render as TIMESTAMP in - all cases, regardless of whether or not that type is legal on the backend database. - - c. the visit_UPPERCASE methods *should* be overridden with methods that add additional - arguments and flags to those types. - - d. the visit_lowercase methods are overridden to provide an interpretation of a generic - type. E.g. visit_large_binary() might be overridden to say "return self.visit_BIT(type_)". - - e. visit_lowercase methods should *never* render strings directly - it should always - be via calling a visit_UPPERCASE() method. diff --git a/libs/sqlalchemy/engine/__init__.py b/libs/sqlalchemy/engine/__init__.py deleted file mode 100644 index 890c7664..00000000 --- a/libs/sqlalchemy/engine/__init__.py +++ /dev/null @@ -1,363 +0,0 @@ -# engine/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""SQL connections, SQL execution and high-level DB-API interface. - -The engine package defines the basic components used to interface -DB-API modules with higher-level statement construction, -connection-management, execution and result contexts. The primary -"entry point" class into this package is the Engine and it's public -constructor ``create_engine()``. - -This package includes: - -base.py - Defines interface classes and some implementation classes which - comprise the basic components used to interface between a DB-API, - constructed and plain-text statements, connections, transactions, - and results. - -default.py - Contains default implementations of some of the components defined - in base.py. All current database dialects use the classes in - default.py as base classes for their own database-specific - implementations. - -strategies.py - The mechanics of constructing ``Engine`` objects are represented - here. Defines the ``EngineStrategy`` class which represents how - to go from arguments specified to the ``create_engine()`` - function, to a fully constructed ``Engine``, including - initialization of connection pooling, dialects, and specific - subclasses of ``Engine``. - -threadlocal.py - The ``TLEngine`` class is defined here, which is a subclass of - the generic ``Engine`` and tracks ``Connection`` and - ``Transaction`` objects against the identity of the current - thread. This allows certain programming patterns based around - the concept of a "thread-local connection" to be possible. - The ``TLEngine`` is created by using the "threadlocal" engine - strategy in conjunction with the ``create_engine()`` function. - -url.py - Defines the ``URL`` class which represents the individual - components of a string URL passed to ``create_engine()``. Also - defines a basic module-loading strategy for the dialect specifier - within a URL. -""" - -from .interfaces import ( - Connectable, - Dialect, - ExecutionContext, - - # backwards compat - Compiled, - TypeCompiler -) - -from .base import ( - Connection, - Engine, - NestedTransaction, - RootTransaction, - Transaction, - TwoPhaseTransaction, - ) - -from .result import ( - BufferedColumnResultProxy, - BufferedColumnRow, - BufferedRowResultProxy, - FullyBufferedResultProxy, - ResultProxy, - RowProxy, - ) - -from .util import ( - connection_memoize - ) - - -from . import util, strategies - -# backwards compat -from ..sql import ddl - -default_strategy = 'plain' - - -def create_engine(*args, **kwargs): - """Create a new :class:`.Engine` instance. - - The standard calling form is to send the URL as the - first positional argument, usually a string - that indicates database dialect and connection arguments. - Additional keyword arguments may then follow it which - establish various options on the resulting :class:`.Engine` - and its underlying :class:`.Dialect` and :class:`.Pool` - constructs. - - The string form of the URL is - ``dialect+driver://user:password@host/dbname[?key=value..]``, where - ``dialect`` is a database name such as ``mysql``, ``oracle``, - ``postgresql``, etc., and ``driver`` the name of a DBAPI, such as - ``psycopg2``, ``pyodbc``, ``cx_oracle``, etc. Alternatively, - the URL can be an instance of :class:`~sqlalchemy.engine.url.URL`. - - ``**kwargs`` takes a wide variety of options which are routed - towards their appropriate components. Arguments may be specific - to the :class:`.Engine`, the underlying :class:`.Dialect`, as well as - the :class:`.Pool`. Specific dialects also accept keyword - arguments that are unique to that dialect. Here, we describe the - parameters that are common to most :func:`.create_engine()` usage. - - Once established, the newly resulting :class:`.Engine` will - request a connection from the underlying :class:`.Pool` once - :meth:`.Engine.connect` is called, or a method which depends on it - such as :meth:`.Engine.execute` is invoked. The :class:`.Pool` in turn - will establish the first actual DBAPI connection when this request - is received. The :func:`.create_engine` call itself does **not** - establish any actual DBAPI connections directly. - - See also: - - :doc:`/core/engines` - - :ref:`connections_toplevel` - - :param case_sensitive=True: if False, result column names - will match in a case-insensitive fashion, that is, - ``row['SomeColumn']``. - - .. versionchanged:: 0.8 - By default, result row names match case-sensitively. - In version 0.7 and prior, all matches were case-insensitive. - - :param connect_args: a dictionary of options which will be - passed directly to the DBAPI's ``connect()`` method as - additional keyword arguments. See the example - at :ref:`custom_dbapi_args`. - - :param convert_unicode=False: if set to True, sets - the default behavior of ``convert_unicode`` on the - :class:`.String` type to ``True``, regardless - of a setting of ``False`` on an individual - :class:`.String` type, thus causing all :class:`.String` - -based columns - to accommodate Python ``unicode`` objects. This flag - is useful as an engine-wide setting when using a - DBAPI that does not natively support Python - ``unicode`` objects and raises an error when - one is received (such as pyodbc with FreeTDS). - - See :class:`.String` for further details on - what this flag indicates. - - :param creator: a callable which returns a DBAPI connection. - This creation function will be passed to the underlying - connection pool and will be used to create all new database - connections. Usage of this function causes connection - parameters specified in the URL argument to be bypassed. - - :param echo=False: if True, the Engine will log all statements - as well as a repr() of their parameter lists to the engines - logger, which defaults to sys.stdout. The ``echo`` attribute of - ``Engine`` can be modified at any time to turn logging on and - off. If set to the string ``"debug"``, result rows will be - printed to the standard output as well. This flag ultimately - controls a Python logger; see :ref:`dbengine_logging` for - information on how to configure logging directly. - - :param echo_pool=False: if True, the connection pool will log - all checkouts/checkins to the logging stream, which defaults to - sys.stdout. This flag ultimately controls a Python logger; see - :ref:`dbengine_logging` for information on how to configure logging - directly. - - :param encoding: Defaults to ``utf-8``. This is the string - encoding used by SQLAlchemy for string encode/decode - operations which occur within SQLAlchemy, **outside of - the DBAPI.** Most modern DBAPIs feature some degree of - direct support for Python ``unicode`` objects, - what you see in Python 2 as a string of the form - ``u'some string'``. For those scenarios where the - DBAPI is detected as not supporting a Python ``unicode`` - object, this encoding is used to determine the - source/destination encoding. It is **not used** - for those cases where the DBAPI handles unicode - directly. - - To properly configure a system to accommodate Python - ``unicode`` objects, the DBAPI should be - configured to handle unicode to the greatest - degree as is appropriate - see - the notes on unicode pertaining to the specific - target database in use at :ref:`dialect_toplevel`. - - Areas where string encoding may need to be accommodated - outside of the DBAPI include zero or more of: - - * the values passed to bound parameters, corresponding to - the :class:`.Unicode` type or the :class:`.String` type - when ``convert_unicode`` is ``True``; - * the values returned in result set columns corresponding - to the :class:`.Unicode` type or the :class:`.String` - type when ``convert_unicode`` is ``True``; - * the string SQL statement passed to the DBAPI's - ``cursor.execute()`` method; - * the string names of the keys in the bound parameter - dictionary passed to the DBAPI's ``cursor.execute()`` - as well as ``cursor.setinputsizes()`` methods; - * the string column names retrieved from the DBAPI's - ``cursor.description`` attribute. - - When using Python 3, the DBAPI is required to support - *all* of the above values as Python ``unicode`` objects, - which in Python 3 are just known as ``str``. In Python 2, - the DBAPI does not specify unicode behavior at all, - so SQLAlchemy must make decisions for each of the above - values on a per-DBAPI basis - implementations are - completely inconsistent in their behavior. - - :param execution_options: Dictionary execution options which will - be applied to all connections. See - :meth:`~sqlalchemy.engine.Connection.execution_options` - - :param implicit_returning=True: When ``True``, a RETURNING- - compatible construct, if available, will be used to - fetch newly generated primary key values when a single row - INSERT statement is emitted with no existing returning() - clause. This applies to those backends which support RETURNING - or a compatible construct, including Postgresql, Firebird, Oracle, - Microsoft SQL Server. Set this to ``False`` to disable - the automatic usage of RETURNING. - - :param label_length=None: optional integer value which limits - the size of dynamically generated column labels to that many - characters. If less than 6, labels are generated as - "_(counter)". If ``None``, the value of - ``dialect.max_identifier_length`` is used instead. - - :param listeners: A list of one or more - :class:`~sqlalchemy.interfaces.PoolListener` objects which will - receive connection pool events. - - :param logging_name: String identifier which will be used within - the "name" field of logging records generated within the - "sqlalchemy.engine" logger. Defaults to a hexstring of the - object's id. - - :param max_overflow=10: the number of connections to allow in - connection pool "overflow", that is connections that can be - opened above and beyond the pool_size setting, which defaults - to five. this is only used with :class:`~sqlalchemy.pool.QueuePool`. - - :param module=None: reference to a Python module object (the module - itself, not its string name). Specifies an alternate DBAPI module to - be used by the engine's dialect. Each sub-dialect references a - specific DBAPI which will be imported before first connect. This - parameter causes the import to be bypassed, and the given module to - be used instead. Can be used for testing of DBAPIs as well as to - inject "mock" DBAPI implementations into the :class:`.Engine`. - - :param pool=None: an already-constructed instance of - :class:`~sqlalchemy.pool.Pool`, such as a - :class:`~sqlalchemy.pool.QueuePool` instance. If non-None, this - pool will be used directly as the underlying connection pool - for the engine, bypassing whatever connection parameters are - present in the URL argument. For information on constructing - connection pools manually, see :ref:`pooling_toplevel`. - - :param poolclass=None: a :class:`~sqlalchemy.pool.Pool` - subclass, which will be used to create a connection pool - instance using the connection parameters given in the URL. Note - this differs from ``pool`` in that you don't actually - instantiate the pool in this case, you just indicate what type - of pool to be used. - - :param pool_logging_name: String identifier which will be used within - the "name" field of logging records generated within the - "sqlalchemy.pool" logger. Defaults to a hexstring of the object's - id. - - :param pool_size=5: the number of connections to keep open - inside the connection pool. This used with - :class:`~sqlalchemy.pool.QueuePool` as - well as :class:`~sqlalchemy.pool.SingletonThreadPool`. With - :class:`~sqlalchemy.pool.QueuePool`, a ``pool_size`` setting - of 0 indicates no limit; to disable pooling, set ``poolclass`` to - :class:`~sqlalchemy.pool.NullPool` instead. - - :param pool_recycle=-1: this setting causes the pool to recycle - connections after the given number of seconds has passed. It - defaults to -1, or no timeout. For example, setting to 3600 - means connections will be recycled after one hour. Note that - MySQL in particular will disconnect automatically if no - activity is detected on a connection for eight hours (although - this is configurable with the MySQLDB connection itself and the - server configuration as well). - - :param pool_reset_on_return='rollback': set the "reset on return" - behavior of the pool, which is whether ``rollback()``, - ``commit()``, or nothing is called upon connections - being returned to the pool. See the docstring for - ``reset_on_return`` at :class:`.Pool`. - - .. versionadded:: 0.7.6 - - :param pool_timeout=30: number of seconds to wait before giving - up on getting a connection from the pool. This is only used - with :class:`~sqlalchemy.pool.QueuePool`. - - :param strategy='plain': selects alternate engine implementations. - Currently available are: - - * the ``threadlocal`` strategy, which is described in - :ref:`threadlocal_strategy`; - * the ``mock`` strategy, which dispatches all statement - execution to a function passed as the argument ``executor``. - See `example in the FAQ - `_. - - :param executor=None: a function taking arguments - ``(sql, *multiparams, **params)``, to which the ``mock`` strategy will - dispatch all statement execution. Used only by ``strategy='mock'``. - - """ - - strategy = kwargs.pop('strategy', default_strategy) - strategy = strategies.strategies[strategy] - return strategy.create(*args, **kwargs) - - -def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): - """Create a new Engine instance using a configuration dictionary. - - The dictionary is typically produced from a config file where keys - are prefixed, such as sqlalchemy.url, sqlalchemy.echo, etc. The - 'prefix' argument indicates the prefix to be searched for. - - A select set of keyword arguments will be "coerced" to their - expected type based on string values. In a future release, this - functionality will be expanded and include dialect-specific - arguments. - """ - - options = dict((key[len(prefix):], configuration[key]) - for key in configuration - if key.startswith(prefix)) - options['_coerce_config'] = True - options.update(kwargs) - url = options.pop('url') - return create_engine(url, **options) - - -__all__ = ( - 'create_engine', - 'engine_from_config', - ) diff --git a/libs/sqlalchemy/engine/base.py b/libs/sqlalchemy/engine/base.py deleted file mode 100644 index 0e888ca4..00000000 --- a/libs/sqlalchemy/engine/base.py +++ /dev/null @@ -1,1724 +0,0 @@ -# engine/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -from __future__ import with_statement - -"""Defines :class:`.Connection` and :class:`.Engine`. - -""" - - -import sys -from .. import exc, util, log, interfaces -from ..sql import expression, util as sql_util, schema, ddl -from .interfaces import Connectable, Compiled -from .util import _distill_params -import contextlib - - -class Connection(Connectable): - """Provides high-level functionality for a wrapped DB-API connection. - - Provides execution support for string-based SQL statements as well as - :class:`.ClauseElement`, :class:`.Compiled` and :class:`.DefaultGenerator` - objects. Provides a :meth:`begin` method to return :class:`.Transaction` - objects. - - The Connection object is **not** thread-safe. While a Connection can be - shared among threads using properly synchronized access, it is still - possible that the underlying DBAPI connection may not support shared - access between threads. Check the DBAPI documentation for details. - - The Connection object represents a single dbapi connection checked out - from the connection pool. In this state, the connection pool has no affect - upon the connection, including its expiration or timeout state. For the - connection pool to properly manage connections, connections should be - returned to the connection pool (i.e. ``connection.close()``) whenever the - connection is not in use. - - .. index:: - single: thread safety; Connection - - """ - - def __init__(self, engine, connection=None, close_with_result=False, - _branch=False, _execution_options=None, - _dispatch=None, - _has_events=None): - """Construct a new Connection. - - The constructor here is not public and is only called only by an - :class:`.Engine`. See :meth:`.Engine.connect` and - :meth:`.Engine.contextual_connect` methods. - - """ - self.engine = engine - self.dialect = engine.dialect - self.__connection = connection or engine.raw_connection() - self.__transaction = None - self.should_close_with_result = close_with_result - self.__savepoint_seq = 0 - self.__branch = _branch - self.__invalid = False - self.__can_reconnect = True - if _dispatch: - self.dispatch = _dispatch - elif engine._has_events: - self.dispatch = self.dispatch._join(engine.dispatch) - self._has_events = _has_events or ( - _has_events is None and engine._has_events) - - self._echo = self.engine._should_log_info() - if _execution_options: - self._execution_options =\ - engine._execution_options.union(_execution_options) - else: - self._execution_options = engine._execution_options - - if self._has_events: - self.dispatch.engine_connect(self, _branch) - - def _branch(self): - """Return a new Connection which references this Connection's - engine and connection; but does not have close_with_result enabled, - and also whose close() method does nothing. - - This is used to execute "sub" statements within a single execution, - usually an INSERT statement. - """ - - return self.engine._connection_cls( - self.engine, - self.__connection, - _branch=True, - _has_events=self._has_events, - _dispatch=self.dispatch) - - def _clone(self): - """Create a shallow copy of this Connection. - - """ - c = self.__class__.__new__(self.__class__) - c.__dict__ = self.__dict__.copy() - return c - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def execution_options(self, **opt): - """ Set non-SQL options for the connection which take effect - during execution. - - The method returns a copy of this :class:`.Connection` which references - the same underlying DBAPI connection, but also defines the given - execution options which will take effect for a call to - :meth:`execute`. As the new :class:`.Connection` references the same - underlying resource, it's usually a good idea to ensure that the copies - would be discarded immediately, which is implicit if used as in:: - - result = connection.execution_options(stream_results=True).\\ - execute(stmt) - - Note that any key/value can be passed to - :meth:`.Connection.execution_options`, and it will be stored in the - ``_execution_options`` dictionary of the :class:`.Connection`. It - is suitable for usage by end-user schemes to communicate with - event listeners, for example. - - The keywords that are currently recognized by SQLAlchemy itself - include all those listed under :meth:`.Executable.execution_options`, - as well as others that are specific to :class:`.Connection`. - - :param autocommit: Available on: Connection, statement. - When True, a COMMIT will be invoked after execution - when executed in 'autocommit' mode, i.e. when an explicit - transaction is not begun on the connection. Note that DBAPI - connections by default are always in a transaction - SQLAlchemy uses - rules applied to different kinds of statements to determine if - COMMIT will be invoked in order to provide its "autocommit" feature. - Typically, all INSERT/UPDATE/DELETE statements as well as - CREATE/DROP statements have autocommit behavior enabled; SELECT - constructs do not. Use this option when invoking a SELECT or other - specific SQL construct where COMMIT is desired (typically when - calling stored procedures and such), and an explicit - transaction is not in progress. - - :param compiled_cache: Available on: Connection. - A dictionary where :class:`.Compiled` objects - will be cached when the :class:`.Connection` compiles a clause - expression into a :class:`.Compiled` object. - It is the user's responsibility to - manage the size of this dictionary, which will have keys - corresponding to the dialect, clause element, the column - names within the VALUES or SET clause of an INSERT or UPDATE, - as well as the "batch" mode for an INSERT or UPDATE statement. - The format of this dictionary is not guaranteed to stay the - same in future releases. - - Note that the ORM makes use of its own "compiled" caches for - some operations, including flush operations. The caching - used by the ORM internally supersedes a cache dictionary - specified here. - - :param isolation_level: Available on: Connection. - Set the transaction isolation level for - the lifespan of this connection. Valid values include - those string values accepted by the ``isolation_level`` - parameter passed to :func:`.create_engine`, and are - database specific, including those for :ref:`sqlite_toplevel`, - :ref:`postgresql_toplevel` - see those dialect's documentation - for further info. - - Note that this option necessarily affects the underlying - DBAPI connection for the lifespan of the originating - :class:`.Connection`, and is not per-execution. This - setting is not removed until the underlying DBAPI connection - is returned to the connection pool, i.e. - the :meth:`.Connection.close` method is called. - - :param no_parameters: When ``True``, if the final parameter - list or dictionary is totally empty, will invoke the - statement on the cursor as ``cursor.execute(statement)``, - not passing the parameter collection at all. - Some DBAPIs such as psycopg2 and mysql-python consider - percent signs as significant only when parameters are - present; this option allows code to generate SQL - containing percent signs (and possibly other characters) - that is neutral regarding whether it's executed by the DBAPI - or piped into a script that's later invoked by - command line tools. - - .. versionadded:: 0.7.6 - - :param stream_results: Available on: Connection, statement. - Indicate to the dialect that results should be - "streamed" and not pre-buffered, if possible. This is a limitation - of many DBAPIs. The flag is currently understood only by the - psycopg2 dialect. - - """ - c = self._clone() - c._execution_options = c._execution_options.union(opt) - if self._has_events: - self.dispatch.set_connection_execution_options(c, opt) - self.dialect.set_connection_execution_options(c, opt) - return c - - @property - def closed(self): - """Return True if this connection is closed.""" - - return '_Connection__connection' not in self.__dict__ \ - and not self.__can_reconnect - - @property - def invalidated(self): - """Return True if this connection was invalidated.""" - - return self.__invalid - - @property - def connection(self): - "The underlying DB-API connection managed by this Connection." - - try: - return self.__connection - except AttributeError: - return self._revalidate_connection() - - def _revalidate_connection(self): - if self.__can_reconnect and self.__invalid: - if self.__transaction is not None: - raise exc.InvalidRequestError( - "Can't reconnect until invalid " - "transaction is rolled back") - self.__connection = self.engine.raw_connection() - self.__invalid = False - return self.__connection - raise exc.ResourceClosedError("This Connection is closed") - - @property - def _connection_is_valid(self): - # use getattr() for is_valid to support exceptions raised in - # dialect initializer, where the connection is not wrapped in - # _ConnectionFairy - - return getattr(self.__connection, 'is_valid', False) - - @property - def _still_open_and_connection_is_valid(self): - return \ - not self.closed and \ - not self.invalidated and \ - getattr(self.__connection, 'is_valid', False) - - @property - def info(self): - """Info dictionary associated with the underlying DBAPI connection - referred to by this :class:`.Connection`, allowing user-defined - data to be associated with the connection. - - The data here will follow along with the DBAPI connection including - after it is returned to the connection pool and used again - in subsequent instances of :class:`.Connection`. - - """ - - return self.connection.info - - def connect(self): - """Returns a branched version of this :class:`.Connection`. - - The :meth:`.Connection.close` method on the returned - :class:`.Connection` can be called and this - :class:`.Connection` will remain open. - - This method provides usage symmetry with - :meth:`.Engine.connect`, including for usage - with context managers. - - """ - - return self._branch() - - def contextual_connect(self, **kwargs): - """Returns a branched version of this :class:`.Connection`. - - The :meth:`.Connection.close` method on the returned - :class:`.Connection` can be called and this - :class:`.Connection` will remain open. - - This method provides usage symmetry with - :meth:`.Engine.contextual_connect`, including for usage - with context managers. - - """ - - return self._branch() - - def invalidate(self, exception=None): - """Invalidate the underlying DBAPI connection associated with - this Connection. - - The underlying DB-API connection is literally closed (if - possible), and is discarded. Its source connection pool will - typically lazily create a new connection to replace it. - - Upon the next usage, this Connection will attempt to reconnect - to the pool with a new connection. - - Transactions in progress remain in an "opened" state (even though the - actual transaction is gone); these must be explicitly rolled back - before a reconnect on this Connection can proceed. This is to prevent - applications from accidentally continuing their transactional - operations in a non-transactional state. - - """ - if self.invalidated: - return - - if self.closed: - raise exc.ResourceClosedError("This Connection is closed") - - if self._connection_is_valid: - self.__connection.invalidate(exception) - del self.__connection - self.__invalid = True - - def detach(self): - """Detach the underlying DB-API connection from its connection pool. - - This Connection instance will remain usable. When closed, - the DB-API connection will be literally closed and not - returned to its pool. The pool will typically lazily create a - new connection to replace the detached connection. - - This method can be used to insulate the rest of an application - from a modified state on a connection (such as a transaction - isolation level or similar). Also see - :class:`~sqlalchemy.interfaces.PoolListener` for a mechanism to modify - connection state when connections leave and return to their - connection pool. - """ - - self.__connection.detach() - - def begin(self): - """Begin a transaction and return a transaction handle. - - The returned object is an instance of :class:`.Transaction`. - This object represents the "scope" of the transaction, - which completes when either the :meth:`.Transaction.rollback` - or :meth:`.Transaction.commit` method is called. - - Nested calls to :meth:`.begin` on the same :class:`.Connection` - will return new :class:`.Transaction` objects that represent - an emulated transaction within the scope of the enclosing - transaction, that is:: - - trans = conn.begin() # outermost transaction - trans2 = conn.begin() # "nested" - trans2.commit() # does nothing - trans.commit() # actually commits - - Calls to :meth:`.Transaction.commit` only have an effect - when invoked via the outermost :class:`.Transaction` object, though the - :meth:`.Transaction.rollback` method of any of the - :class:`.Transaction` objects will roll back the - transaction. - - See also: - - :meth:`.Connection.begin_nested` - use a SAVEPOINT - - :meth:`.Connection.begin_twophase` - use a two phase /XID transaction - - :meth:`.Engine.begin` - context manager available from - :class:`.Engine`. - - """ - - if self.__transaction is None: - self.__transaction = RootTransaction(self) - return self.__transaction - else: - return Transaction(self, self.__transaction) - - def begin_nested(self): - """Begin a nested transaction and return a transaction handle. - - The returned object is an instance of :class:`.NestedTransaction`. - - Nested transactions require SAVEPOINT support in the - underlying database. Any transaction in the hierarchy may - ``commit`` and ``rollback``, however the outermost transaction - still controls the overall ``commit`` or ``rollback`` of the - transaction of a whole. - - See also :meth:`.Connection.begin`, - :meth:`.Connection.begin_twophase`. - """ - - if self.__transaction is None: - self.__transaction = RootTransaction(self) - else: - self.__transaction = NestedTransaction(self, self.__transaction) - return self.__transaction - - def begin_twophase(self, xid=None): - """Begin a two-phase or XA transaction and return a transaction - handle. - - The returned object is an instance of :class:`.TwoPhaseTransaction`, - which in addition to the methods provided by - :class:`.Transaction`, also provides a - :meth:`~.TwoPhaseTransaction.prepare` method. - - :param xid: the two phase transaction id. If not supplied, a - random id will be generated. - - See also :meth:`.Connection.begin`, - :meth:`.Connection.begin_twophase`. - - """ - - if self.__transaction is not None: - raise exc.InvalidRequestError( - "Cannot start a two phase transaction when a transaction " - "is already in progress.") - if xid is None: - xid = self.engine.dialect.create_xid() - self.__transaction = TwoPhaseTransaction(self, xid) - return self.__transaction - - def recover_twophase(self): - return self.engine.dialect.do_recover_twophase(self) - - def rollback_prepared(self, xid, recover=False): - self.engine.dialect.do_rollback_twophase(self, xid, recover=recover) - - def commit_prepared(self, xid, recover=False): - self.engine.dialect.do_commit_twophase(self, xid, recover=recover) - - def in_transaction(self): - """Return True if a transaction is in progress.""" - - return self.__transaction is not None - - def _begin_impl(self): - if self._echo: - self.engine.logger.info("BEGIN (implicit)") - - if self._has_events: - self.dispatch.begin(self) - - try: - self.engine.dialect.do_begin(self.connection) - except Exception as e: - self._handle_dbapi_exception(e, None, None, None, None) - - def _rollback_impl(self): - if self._has_events: - self.dispatch.rollback(self) - - if self._still_open_and_connection_is_valid: - if self._echo: - self.engine.logger.info("ROLLBACK") - try: - self.engine.dialect.do_rollback(self.connection) - self.__transaction = None - except Exception as e: - self._handle_dbapi_exception(e, None, None, None, None) - else: - self.__transaction = None - - def _commit_impl(self, autocommit=False): - if self._has_events: - self.dispatch.commit(self) - - if self._echo: - self.engine.logger.info("COMMIT") - try: - self.engine.dialect.do_commit(self.connection) - self.__transaction = None - except Exception as e: - self._handle_dbapi_exception(e, None, None, None, None) - - def _savepoint_impl(self, name=None): - if self._has_events: - self.dispatch.savepoint(self, name) - - if name is None: - self.__savepoint_seq += 1 - name = 'sa_savepoint_%s' % self.__savepoint_seq - if self._still_open_and_connection_is_valid: - self.engine.dialect.do_savepoint(self, name) - return name - - def _rollback_to_savepoint_impl(self, name, context): - if self._has_events: - self.dispatch.rollback_savepoint(self, name, context) - - if self._still_open_and_connection_is_valid: - self.engine.dialect.do_rollback_to_savepoint(self, name) - self.__transaction = context - - def _release_savepoint_impl(self, name, context): - if self._has_events: - self.dispatch.release_savepoint(self, name, context) - - if self._still_open_and_connection_is_valid: - self.engine.dialect.do_release_savepoint(self, name) - self.__transaction = context - - def _begin_twophase_impl(self, xid): - if self._echo: - self.engine.logger.info("BEGIN TWOPHASE (implicit)") - if self._has_events: - self.dispatch.begin_twophase(self, xid) - - if self._still_open_and_connection_is_valid: - self.engine.dialect.do_begin_twophase(self, xid) - - def _prepare_twophase_impl(self, xid): - if self._has_events: - self.dispatch.prepare_twophase(self, xid) - - if self._still_open_and_connection_is_valid: - assert isinstance(self.__transaction, TwoPhaseTransaction) - self.engine.dialect.do_prepare_twophase(self, xid) - - def _rollback_twophase_impl(self, xid, is_prepared): - if self._has_events: - self.dispatch.rollback_twophase(self, xid, is_prepared) - - if self._still_open_and_connection_is_valid: - assert isinstance(self.__transaction, TwoPhaseTransaction) - self.engine.dialect.do_rollback_twophase(self, xid, is_prepared) - self.__transaction = None - - def _commit_twophase_impl(self, xid, is_prepared): - if self._has_events: - self.dispatch.commit_twophase(self, xid, is_prepared) - - if self._still_open_and_connection_is_valid: - assert isinstance(self.__transaction, TwoPhaseTransaction) - self.engine.dialect.do_commit_twophase(self, xid, is_prepared) - self.__transaction = None - - def _autorollback(self): - if not self.in_transaction(): - self._rollback_impl() - - def close(self): - """Close this :class:`.Connection`. - - This results in a release of the underlying database - resources, that is, the DBAPI connection referenced - internally. The DBAPI connection is typically restored - back to the connection-holding :class:`.Pool` referenced - by the :class:`.Engine` that produced this - :class:`.Connection`. Any transactional state present on - the DBAPI connection is also unconditionally released via - the DBAPI connection's ``rollback()`` method, regardless - of any :class:`.Transaction` object that may be - outstanding with regards to this :class:`.Connection`. - - After :meth:`~.Connection.close` is called, the - :class:`.Connection` is permanently in a closed state, - and will allow no further operations. - - """ - try: - conn = self.__connection - except AttributeError: - pass - else: - if not self.__branch: - conn.close() - del self.__connection - self.__can_reconnect = False - self.__transaction = None - - def scalar(self, object, *multiparams, **params): - """Executes and returns the first column of the first row. - - The underlying result/cursor is closed after execution. - """ - - return self.execute(object, *multiparams, **params).scalar() - - def execute(self, object, *multiparams, **params): - """Executes the a SQL statement construct and returns a - :class:`.ResultProxy`. - - :param object: The statement to be executed. May be - one of: - - * a plain string - * any :class:`.ClauseElement` construct that is also - a subclass of :class:`.Executable`, such as a - :func:`~.expression.select` construct - * a :class:`.FunctionElement`, such as that generated - by :attr:`.func`, will be automatically wrapped in - a SELECT statement, which is then executed. - * a :class:`.DDLElement` object - * a :class:`.DefaultGenerator` object - * a :class:`.Compiled` object - - :param \*multiparams/\**params: represent bound parameter - values to be used in the execution. Typically, - the format is either a collection of one or more - dictionaries passed to \*multiparams:: - - conn.execute( - table.insert(), - {"id":1, "value":"v1"}, - {"id":2, "value":"v2"} - ) - - ...or individual key/values interpreted by \**params:: - - conn.execute( - table.insert(), id=1, value="v1" - ) - - In the case that a plain SQL string is passed, and the underlying - DBAPI accepts positional bind parameters, a collection of tuples - or individual values in \*multiparams may be passed:: - - conn.execute( - "INSERT INTO table (id, value) VALUES (?, ?)", - (1, "v1"), (2, "v2") - ) - - conn.execute( - "INSERT INTO table (id, value) VALUES (?, ?)", - 1, "v1" - ) - - Note above, the usage of a question mark "?" or other - symbol is contingent upon the "paramstyle" accepted by the DBAPI - in use, which may be any of "qmark", "named", "pyformat", "format", - "numeric". See `pep-249 `_ - for details on paramstyle. - - To execute a textual SQL statement which uses bound parameters in a - DBAPI-agnostic way, use the :func:`~.expression.text` construct. - - """ - if isinstance(object, util.string_types[0]): - return self._execute_text(object, multiparams, params) - try: - meth = object._execute_on_connection - except AttributeError: - raise exc.InvalidRequestError( - "Unexecutable object type: %s" % - type(object)) - else: - return meth(self, multiparams, params) - - def _execute_function(self, func, multiparams, params): - """Execute a sql.FunctionElement object.""" - - return self._execute_clauseelement(func.select(), - multiparams, params) - - def _execute_default(self, default, multiparams, params): - """Execute a schema.ColumnDefault object.""" - - if self._has_events: - for fn in self.dispatch.before_execute: - default, multiparams, params = \ - fn(self, default, multiparams, params) - - try: - try: - conn = self.__connection - except AttributeError: - conn = self._revalidate_connection() - - dialect = self.dialect - ctx = dialect.execution_ctx_cls._init_default( - dialect, self, conn) - except Exception as e: - self._handle_dbapi_exception(e, None, None, None, None) - - ret = ctx._exec_default(default, None) - if self.should_close_with_result: - self.close() - - if self._has_events: - self.dispatch.after_execute(self, - default, multiparams, params, ret) - - return ret - - def _execute_ddl(self, ddl, multiparams, params): - """Execute a schema.DDL object.""" - - if self._has_events: - for fn in self.dispatch.before_execute: - ddl, multiparams, params = \ - fn(self, ddl, multiparams, params) - - dialect = self.dialect - - compiled = ddl.compile(dialect=dialect) - ret = self._execute_context( - dialect, - dialect.execution_ctx_cls._init_ddl, - compiled, - None, - compiled - ) - if self._has_events: - self.dispatch.after_execute(self, - ddl, multiparams, params, ret) - return ret - - def _execute_clauseelement(self, elem, multiparams, params): - """Execute a sql.ClauseElement object.""" - - if self._has_events: - for fn in self.dispatch.before_execute: - elem, multiparams, params = \ - fn(self, elem, multiparams, params) - - distilled_params = _distill_params(multiparams, params) - if distilled_params: - # note this is usually dict but we support RowProxy - # as well; but dict.keys() as an iterator is OK - keys = distilled_params[0].keys() - else: - keys = [] - - dialect = self.dialect - if 'compiled_cache' in self._execution_options: - key = dialect, elem, tuple(keys), len(distilled_params) > 1 - if key in self._execution_options['compiled_cache']: - compiled_sql = self._execution_options['compiled_cache'][key] - else: - compiled_sql = elem.compile( - dialect=dialect, column_keys=keys, - inline=len(distilled_params) > 1) - self._execution_options['compiled_cache'][key] = compiled_sql - else: - compiled_sql = elem.compile( - dialect=dialect, column_keys=keys, - inline=len(distilled_params) > 1) - - ret = self._execute_context( - dialect, - dialect.execution_ctx_cls._init_compiled, - compiled_sql, - distilled_params, - compiled_sql, distilled_params - ) - if self._has_events: - self.dispatch.after_execute(self, - elem, multiparams, params, ret) - return ret - - def _execute_compiled(self, compiled, multiparams, params): - """Execute a sql.Compiled object.""" - - if self._has_events: - for fn in self.dispatch.before_execute: - compiled, multiparams, params = \ - fn(self, compiled, multiparams, params) - - dialect = self.dialect - parameters = _distill_params(multiparams, params) - ret = self._execute_context( - dialect, - dialect.execution_ctx_cls._init_compiled, - compiled, - parameters, - compiled, parameters - ) - if self._has_events: - self.dispatch.after_execute(self, - compiled, multiparams, params, ret) - return ret - - def _execute_text(self, statement, multiparams, params): - """Execute a string SQL statement.""" - - if self._has_events: - for fn in self.dispatch.before_execute: - statement, multiparams, params = \ - fn(self, statement, multiparams, params) - - dialect = self.dialect - parameters = _distill_params(multiparams, params) - ret = self._execute_context( - dialect, - dialect.execution_ctx_cls._init_statement, - statement, - parameters, - statement, parameters - ) - if self._has_events: - self.dispatch.after_execute(self, - statement, multiparams, params, ret) - return ret - - def _execute_context(self, dialect, constructor, - statement, parameters, - *args): - """Create an :class:`.ExecutionContext` and execute, returning - a :class:`.ResultProxy`.""" - - try: - try: - conn = self.__connection - except AttributeError: - conn = self._revalidate_connection() - - context = constructor(dialect, self, conn, *args) - except Exception as e: - self._handle_dbapi_exception(e, - util.text_type(statement), parameters, - None, None) - - if context.compiled: - context.pre_exec() - - cursor, statement, parameters = context.cursor, \ - context.statement, \ - context.parameters - - if not context.executemany: - parameters = parameters[0] - - if self._has_events: - for fn in self.dispatch.before_cursor_execute: - statement, parameters = \ - fn(self, cursor, statement, parameters, - context, context.executemany) - - if self._echo: - self.engine.logger.info(statement) - self.engine.logger.info("%r", - sql_util._repr_params(parameters, batches=10)) - try: - if context.executemany: - self.dialect.do_executemany( - cursor, - statement, - parameters, - context) - elif not parameters and context.no_parameters: - self.dialect.do_execute_no_params( - cursor, - statement, - context) - else: - self.dialect.do_execute( - cursor, - statement, - parameters, - context) - except Exception as e: - self._handle_dbapi_exception( - e, - statement, - parameters, - cursor, - context) - - if self._has_events: - self.dispatch.after_cursor_execute(self, cursor, - statement, - parameters, - context, - context.executemany) - - if context.compiled: - context.post_exec() - - if context.isinsert and not context.executemany: - context.post_insert() - - # create a resultproxy, get rowcount/implicit RETURNING - # rows, close cursor if no further results pending - result = context.get_result_proxy() - if context.isinsert: - if context._is_implicit_returning: - context._fetch_implicit_returning(result) - result.close(_autoclose_connection=False) - result._metadata = None - elif not context._is_explicit_returning: - result.close(_autoclose_connection=False) - result._metadata = None - elif context.isupdate and context._is_implicit_returning: - context._fetch_implicit_update_returning(result) - result.close(_autoclose_connection=False) - result._metadata = None - - elif result._metadata is None: - # no results, get rowcount - # (which requires open cursor on some drivers - # such as kintersbasdb, mxodbc), - result.rowcount - result.close(_autoclose_connection=False) - - if self.__transaction is None and context.should_autocommit: - self._commit_impl(autocommit=True) - - if result.closed and self.should_close_with_result: - self.close() - - return result - - def _cursor_execute(self, cursor, statement, parameters, context=None): - """Execute a statement + params on the given cursor. - - Adds appropriate logging and exception handling. - - This method is used by DefaultDialect for special-case - executions, such as for sequences and column defaults. - The path of statement execution in the majority of cases - terminates at _execute_context(). - - """ - if self._has_events: - for fn in self.dispatch.before_cursor_execute: - statement, parameters = \ - fn(self, cursor, statement, parameters, - context, - context.executemany - if context is not None else False) - - if self._echo: - self.engine.logger.info(statement) - self.engine.logger.info("%r", parameters) - try: - self.dialect.do_execute( - cursor, - statement, - parameters) - except Exception as e: - self._handle_dbapi_exception( - e, - statement, - parameters, - cursor, - None) - - def _safe_close_cursor(self, cursor): - """Close the given cursor, catching exceptions - and turning into log warnings. - - """ - try: - cursor.close() - except (SystemExit, KeyboardInterrupt): - raise - except Exception: - self.connection._logger.error( - "Error closing cursor", exc_info=True) - - _reentrant_error = False - _is_disconnect = False - - def _handle_dbapi_exception(self, - e, - statement, - parameters, - cursor, - context): - - exc_info = sys.exc_info() - - if not self._is_disconnect: - self._is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ - not self.closed and \ - self.dialect.is_disconnect(e, self.__connection, cursor) - - if self._reentrant_error: - util.raise_from_cause( - exc.DBAPIError.instance(statement, - parameters, - e, - self.dialect.dbapi.Error), - exc_info - ) - self._reentrant_error = True - try: - # non-DBAPI error - if we already got a context, - # or theres no string statement, don't wrap it - should_wrap = isinstance(e, self.dialect.dbapi.Error) or \ - (statement is not None and context is None) - - if should_wrap and context: - if self._has_events: - self.dispatch.dbapi_error(self, - cursor, - statement, - parameters, - context, - e) - context.handle_dbapi_exception(e) - - if not self._is_disconnect: - if cursor: - self._safe_close_cursor(cursor) - self._autorollback() - - if should_wrap: - util.raise_from_cause( - exc.DBAPIError.instance( - statement, - parameters, - e, - self.dialect.dbapi.Error, - connection_invalidated=self._is_disconnect), - exc_info - ) - - util.reraise(*exc_info) - - finally: - del self._reentrant_error - if self._is_disconnect: - del self._is_disconnect - dbapi_conn_wrapper = self.connection - self.invalidate(e) - if not hasattr(dbapi_conn_wrapper, '_pool') or \ - dbapi_conn_wrapper._pool is self.engine.pool: - self.engine.dispose() - if self.should_close_with_result: - self.close() - - def default_schema_name(self): - return self.engine.dialect.get_default_schema_name(self) - - def transaction(self, callable_, *args, **kwargs): - """Execute the given function within a transaction boundary. - - The function is passed this :class:`.Connection` - as the first argument, followed by the given \*args and \**kwargs, - e.g.:: - - def do_something(conn, x, y): - conn.execute("some statement", {'x':x, 'y':y}) - - conn.transaction(do_something, 5, 10) - - The operations inside the function are all invoked within the - context of a single :class:`.Transaction`. - Upon success, the transaction is committed. If an - exception is raised, the transaction is rolled back - before propagating the exception. - - .. note:: - - The :meth:`.transaction` method is superseded by - the usage of the Python ``with:`` statement, which can - be used with :meth:`.Connection.begin`:: - - with conn.begin(): - conn.execute("some statement", {'x':5, 'y':10}) - - As well as with :meth:`.Engine.begin`:: - - with engine.begin() as conn: - conn.execute("some statement", {'x':5, 'y':10}) - - See also: - - :meth:`.Engine.begin` - engine-level transactional - context - - :meth:`.Engine.transaction` - engine-level version of - :meth:`.Connection.transaction` - - """ - - trans = self.begin() - try: - ret = self.run_callable(callable_, *args, **kwargs) - trans.commit() - return ret - except: - with util.safe_reraise(): - trans.rollback() - - def run_callable(self, callable_, *args, **kwargs): - """Given a callable object or function, execute it, passing - a :class:`.Connection` as the first argument. - - The given \*args and \**kwargs are passed subsequent - to the :class:`.Connection` argument. - - This function, along with :meth:`.Engine.run_callable`, - allows a function to be run with a :class:`.Connection` - or :class:`.Engine` object without the need to know - which one is being dealt with. - - """ - return callable_(self, *args, **kwargs) - - def _run_visitor(self, visitorcallable, element, **kwargs): - visitorcallable(self.dialect, self, - **kwargs).traverse_single(element) - - -class Transaction(object): - """Represent a database transaction in progress. - - The :class:`.Transaction` object is procured by - calling the :meth:`~.Connection.begin` method of - :class:`.Connection`:: - - from sqlalchemy import create_engine - engine = create_engine("postgresql://scott:tiger@localhost/test") - connection = engine.connect() - trans = connection.begin() - connection.execute("insert into x (a, b) values (1, 2)") - trans.commit() - - The object provides :meth:`.rollback` and :meth:`.commit` - methods in order to control transaction boundaries. It - also implements a context manager interface so that - the Python ``with`` statement can be used with the - :meth:`.Connection.begin` method:: - - with connection.begin(): - connection.execute("insert into x (a, b) values (1, 2)") - - The Transaction object is **not** threadsafe. - - See also: :meth:`.Connection.begin`, :meth:`.Connection.begin_twophase`, - :meth:`.Connection.begin_nested`. - - .. index:: - single: thread safety; Transaction - """ - - def __init__(self, connection, parent): - self.connection = connection - self._parent = parent or self - self.is_active = True - - def close(self): - """Close this :class:`.Transaction`. - - If this transaction is the base transaction in a begin/commit - nesting, the transaction will rollback(). Otherwise, the - method returns. - - This is used to cancel a Transaction without affecting the scope of - an enclosing transaction. - - """ - if not self._parent.is_active: - return - if self._parent is self: - self.rollback() - - def rollback(self): - """Roll back this :class:`.Transaction`. - - """ - if not self._parent.is_active: - return - self._do_rollback() - self.is_active = False - - def _do_rollback(self): - self._parent.rollback() - - def commit(self): - """Commit this :class:`.Transaction`.""" - - if not self._parent.is_active: - raise exc.InvalidRequestError("This transaction is inactive") - self._do_commit() - self.is_active = False - - def _do_commit(self): - pass - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - if type is None and self.is_active: - try: - self.commit() - except: - with util.safe_reraise(): - self.rollback() - else: - self.rollback() - - -class RootTransaction(Transaction): - def __init__(self, connection): - super(RootTransaction, self).__init__(connection, None) - self.connection._begin_impl() - - def _do_rollback(self): - if self.is_active: - self.connection._rollback_impl() - - def _do_commit(self): - if self.is_active: - self.connection._commit_impl() - - -class NestedTransaction(Transaction): - """Represent a 'nested', or SAVEPOINT transaction. - - A new :class:`.NestedTransaction` object may be procured - using the :meth:`.Connection.begin_nested` method. - - The interface is the same as that of :class:`.Transaction`. - - """ - def __init__(self, connection, parent): - super(NestedTransaction, self).__init__(connection, parent) - self._savepoint = self.connection._savepoint_impl() - - def _do_rollback(self): - if self.is_active: - self.connection._rollback_to_savepoint_impl( - self._savepoint, self._parent) - - def _do_commit(self): - if self.is_active: - self.connection._release_savepoint_impl( - self._savepoint, self._parent) - - -class TwoPhaseTransaction(Transaction): - """Represent a two-phase transaction. - - A new :class:`.TwoPhaseTransaction` object may be procured - using the :meth:`.Connection.begin_twophase` method. - - The interface is the same as that of :class:`.Transaction` - with the addition of the :meth:`prepare` method. - - """ - def __init__(self, connection, xid): - super(TwoPhaseTransaction, self).__init__(connection, None) - self._is_prepared = False - self.xid = xid - self.connection._begin_twophase_impl(self.xid) - - def prepare(self): - """Prepare this :class:`.TwoPhaseTransaction`. - - After a PREPARE, the transaction can be committed. - - """ - if not self._parent.is_active: - raise exc.InvalidRequestError("This transaction is inactive") - self.connection._prepare_twophase_impl(self.xid) - self._is_prepared = True - - def _do_rollback(self): - self.connection._rollback_twophase_impl(self.xid, self._is_prepared) - - def _do_commit(self): - self.connection._commit_twophase_impl(self.xid, self._is_prepared) - - -class Engine(Connectable, log.Identified): - """ - Connects a :class:`~sqlalchemy.pool.Pool` and - :class:`~sqlalchemy.engine.interfaces.Dialect` together to provide a - source of database connectivity and behavior. - - An :class:`.Engine` object is instantiated publicly using the - :func:`~sqlalchemy.create_engine` function. - - See also: - - :doc:`/core/engines` - - :ref:`connections_toplevel` - - """ - - _execution_options = util.immutabledict() - _has_events = False - _connection_cls = Connection - - def __init__(self, pool, dialect, url, - logging_name=None, echo=None, proxy=None, - execution_options=None - ): - self.pool = pool - self.url = url - self.dialect = dialect - self.pool._dialect = dialect - if logging_name: - self.logging_name = logging_name - self.echo = echo - self.engine = self - log.instance_logger(self, echoflag=echo) - if proxy: - interfaces.ConnectionProxy._adapt_listener(self, proxy) - if execution_options: - self.update_execution_options(**execution_options) - - def update_execution_options(self, **opt): - """Update the default execution_options dictionary - of this :class:`.Engine`. - - The given keys/values in \**opt are added to the - default execution options that will be used for - all connections. The initial contents of this dictionary - can be sent via the ``execution_options`` parameter - to :func:`.create_engine`. - - .. seealso:: - - :meth:`.Connection.execution_options` - - :meth:`.Engine.execution_options` - - """ - self._execution_options = \ - self._execution_options.union(opt) - self.dispatch.set_engine_execution_options(self, opt) - self.dialect.set_engine_execution_options(self, opt) - - def execution_options(self, **opt): - """Return a new :class:`.Engine` that will provide - :class:`.Connection` objects with the given execution options. - - The returned :class:`.Engine` remains related to the original - :class:`.Engine` in that it shares the same connection pool and - other state: - - * The :class:`.Pool` used by the new :class:`.Engine` is the - same instance. The :meth:`.Engine.dispose` method will replace - the connection pool instance for the parent engine as well - as this one. - * Event listeners are "cascaded" - meaning, the new :class:`.Engine` - inherits the events of the parent, and new events can be associated - with the new :class:`.Engine` individually. - * The logging configuration and logging_name is copied from the parent - :class:`.Engine`. - - The intent of the :meth:`.Engine.execution_options` method is - to implement "sharding" schemes where multiple :class:`.Engine` - objects refer to the same connection pool, but are differentiated - by options that would be consumed by a custom event:: - - primary_engine = create_engine("mysql://") - shard1 = primary_engine.execution_options(shard_id="shard1") - shard2 = primary_engine.execution_options(shard_id="shard2") - - Above, the ``shard1`` engine serves as a factory for - :class:`.Connection` objects that will contain the execution option - ``shard_id=shard1``, and ``shard2`` will produce :class:`.Connection` - objects that contain the execution option ``shard_id=shard2``. - - An event handler can consume the above execution option to perform - a schema switch or other operation, given a connection. Below - we emit a MySQL ``use`` statement to switch databases, at the same - time keeping track of which database we've established using the - :attr:`.Connection.info` dictionary, which gives us a persistent - storage space that follows the DBAPI connection:: - - from sqlalchemy import event - from sqlalchemy.engine import Engine - - shards = {"default": "base", shard_1: "db1", "shard_2": "db2"} - - @event.listens_for(Engine, "before_cursor_execute") - def _switch_shard(conn, cursor, stmt, params, context, executemany): - shard_id = conn._execution_options.get('shard_id', "default") - current_shard = conn.info.get("current_shard", None) - - if current_shard != shard_id: - cursor.execute("use %s" % shards[shard_id]) - conn.info["current_shard"] = shard_id - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.Connection.execution_options` - update execution options - on a :class:`.Connection` object. - - :meth:`.Engine.update_execution_options` - update the execution - options for a given :class:`.Engine` in place. - - """ - return OptionEngine(self, opt) - - @property - def name(self): - """String name of the :class:`~sqlalchemy.engine.interfaces.Dialect` - in use by this :class:`Engine`.""" - - return self.dialect.name - - @property - def driver(self): - """Driver name of the :class:`~sqlalchemy.engine.interfaces.Dialect` - in use by this :class:`Engine`.""" - - return self.dialect.driver - - echo = log.echo_property() - - def __repr__(self): - return 'Engine(%r)' % self.url - - def dispose(self): - """Dispose of the connection pool used by this :class:`.Engine`. - - A new connection pool is created immediately after the old one has - been disposed. This new pool, like all SQLAlchemy connection pools, - does not make any actual connections to the database until one is - first requested. - - This method has two general use cases: - - * When a dropped connection is detected, it is assumed that all - connections held by the pool are potentially dropped, and - the entire pool is replaced. - - * An application may want to use :meth:`dispose` within a test - suite that is creating multiple engines. - - It is critical to note that :meth:`dispose` does **not** guarantee - that the application will release all open database connections - only - those connections that are checked into the pool are closed. - Connections which remain checked out or have been detached from - the engine are not affected. - - """ - self.pool = self.pool._replace() - - def _execute_default(self, default): - with self.contextual_connect() as conn: - return conn._execute_default(default, (), {}) - - @contextlib.contextmanager - def _optional_conn_ctx_manager(self, connection=None): - if connection is None: - with self.contextual_connect() as conn: - yield conn - else: - yield connection - - def _run_visitor(self, visitorcallable, element, - connection=None, **kwargs): - with self._optional_conn_ctx_manager(connection) as conn: - conn._run_visitor(visitorcallable, element, **kwargs) - - class _trans_ctx(object): - def __init__(self, conn, transaction, close_with_result): - self.conn = conn - self.transaction = transaction - self.close_with_result = close_with_result - - def __enter__(self): - return self.conn - - def __exit__(self, type, value, traceback): - if type is not None: - self.transaction.rollback() - else: - self.transaction.commit() - if not self.close_with_result: - self.conn.close() - - def begin(self, close_with_result=False): - """Return a context manager delivering a :class:`.Connection` - with a :class:`.Transaction` established. - - E.g.:: - - with engine.begin() as conn: - conn.execute("insert into table (x, y, z) values (1, 2, 3)") - conn.execute("my_special_procedure(5)") - - Upon successful operation, the :class:`.Transaction` - is committed. If an error is raised, the :class:`.Transaction` - is rolled back. - - The ``close_with_result`` flag is normally ``False``, and indicates - that the :class:`.Connection` will be closed when the operation - is complete. When set to ``True``, it indicates the - :class:`.Connection` is in "single use" mode, where the - :class:`.ResultProxy` returned by the first call to - :meth:`.Connection.execute` will close the :class:`.Connection` when - that :class:`.ResultProxy` has exhausted all result rows. - - .. versionadded:: 0.7.6 - - See also: - - :meth:`.Engine.connect` - procure a :class:`.Connection` from - an :class:`.Engine`. - - :meth:`.Connection.begin` - start a :class:`.Transaction` - for a particular :class:`.Connection`. - - """ - conn = self.contextual_connect(close_with_result=close_with_result) - try: - trans = conn.begin() - except: - with util.safe_reraise(): - conn.close() - return Engine._trans_ctx(conn, trans, close_with_result) - - def transaction(self, callable_, *args, **kwargs): - """Execute the given function within a transaction boundary. - - The function is passed a :class:`.Connection` newly procured - from :meth:`.Engine.contextual_connect` as the first argument, - followed by the given \*args and \**kwargs. - - e.g.:: - - def do_something(conn, x, y): - conn.execute("some statement", {'x':x, 'y':y}) - - engine.transaction(do_something, 5, 10) - - The operations inside the function are all invoked within the - context of a single :class:`.Transaction`. - Upon success, the transaction is committed. If an - exception is raised, the transaction is rolled back - before propagating the exception. - - .. note:: - - The :meth:`.transaction` method is superseded by - the usage of the Python ``with:`` statement, which can - be used with :meth:`.Engine.begin`:: - - with engine.begin() as conn: - conn.execute("some statement", {'x':5, 'y':10}) - - See also: - - :meth:`.Engine.begin` - engine-level transactional - context - - :meth:`.Connection.transaction` - connection-level version of - :meth:`.Engine.transaction` - - """ - - with self.contextual_connect() as conn: - return conn.transaction(callable_, *args, **kwargs) - - def run_callable(self, callable_, *args, **kwargs): - """Given a callable object or function, execute it, passing - a :class:`.Connection` as the first argument. - - The given \*args and \**kwargs are passed subsequent - to the :class:`.Connection` argument. - - This function, along with :meth:`.Connection.run_callable`, - allows a function to be run with a :class:`.Connection` - or :class:`.Engine` object without the need to know - which one is being dealt with. - - """ - with self.contextual_connect() as conn: - return conn.run_callable(callable_, *args, **kwargs) - - def execute(self, statement, *multiparams, **params): - """Executes the given construct and returns a :class:`.ResultProxy`. - - The arguments are the same as those used by - :meth:`.Connection.execute`. - - Here, a :class:`.Connection` is acquired using the - :meth:`~.Engine.contextual_connect` method, and the statement executed - with that connection. The returned :class:`.ResultProxy` is flagged - such that when the :class:`.ResultProxy` is exhausted and its - underlying cursor is closed, the :class:`.Connection` created here - will also be closed, which allows its associated DBAPI connection - resource to be returned to the connection pool. - - """ - - connection = self.contextual_connect(close_with_result=True) - return connection.execute(statement, *multiparams, **params) - - def scalar(self, statement, *multiparams, **params): - return self.execute(statement, *multiparams, **params).scalar() - - def _execute_clauseelement(self, elem, multiparams=None, params=None): - connection = self.contextual_connect(close_with_result=True) - return connection._execute_clauseelement(elem, multiparams, params) - - def _execute_compiled(self, compiled, multiparams, params): - connection = self.contextual_connect(close_with_result=True) - return connection._execute_compiled(compiled, multiparams, params) - - def connect(self, **kwargs): - """Return a new :class:`.Connection` object. - - The :class:`.Connection` object is a facade that uses a DBAPI - connection internally in order to communicate with the database. This - connection is procured from the connection-holding :class:`.Pool` - referenced by this :class:`.Engine`. When the - :meth:`~.Connection.close` method of the :class:`.Connection` object - is called, the underlying DBAPI connection is then returned to the - connection pool, where it may be used again in a subsequent call to - :meth:`~.Engine.connect`. - - """ - - return self._connection_cls(self, **kwargs) - - def contextual_connect(self, close_with_result=False, **kwargs): - """Return a :class:`.Connection` object which may be part of some - ongoing context. - - By default, this method does the same thing as :meth:`.Engine.connect`. - Subclasses of :class:`.Engine` may override this method - to provide contextual behavior. - - :param close_with_result: When True, the first :class:`.ResultProxy` - created by the :class:`.Connection` will call the - :meth:`.Connection.close` method of that connection as soon as any - pending result rows are exhausted. This is used to supply the - "connectionless execution" behavior provided by the - :meth:`.Engine.execute` method. - - """ - - return self._connection_cls(self, - self.pool.connect(), - close_with_result=close_with_result, - **kwargs) - - def table_names(self, schema=None, connection=None): - """Return a list of all table names available in the database. - - :param schema: Optional, retrieve names from a non-default schema. - - :param connection: Optional, use a specified connection. Default is - the ``contextual_connect`` for this ``Engine``. - """ - - with self._optional_conn_ctx_manager(connection) as conn: - if not schema: - schema = self.dialect.default_schema_name - return self.dialect.get_table_names(conn, schema) - - def has_table(self, table_name, schema=None): - """Return True if the given backend has a table of the given name. - - .. seealso:: - - :ref:`metadata_reflection_inspector` - detailed schema inspection using - the :class:`.Inspector` interface. - - :class:`.quoted_name` - used to pass quoting information along - with a schema identifier. - - """ - return self.run_callable(self.dialect.has_table, table_name, schema) - - def raw_connection(self): - """Return a "raw" DBAPI connection from the connection pool. - - The returned object is a proxied version of the DBAPI - connection object used by the underlying driver in use. - The object will have all the same behavior as the real DBAPI - connection, except that its ``close()`` method will result in the - connection being returned to the pool, rather than being closed - for real. - - This method provides direct DBAPI connection access for - special situations. In most situations, the :class:`.Connection` - object should be used, which is procured using the - :meth:`.Engine.connect` method. - - """ - - return self.pool.unique_connection() - - -class OptionEngine(Engine): - def __init__(self, proxied, execution_options): - self._proxied = proxied - self.url = proxied.url - self.dialect = proxied.dialect - self.logging_name = proxied.logging_name - self.echo = proxied.echo - log.instance_logger(self, echoflag=self.echo) - self.dispatch = self.dispatch._join(proxied.dispatch) - self._execution_options = proxied._execution_options - self.update_execution_options(**execution_options) - - def _get_pool(self): - return self._proxied.pool - - def _set_pool(self, pool): - self._proxied.pool = pool - - pool = property(_get_pool, _set_pool) - - def _get_has_events(self): - return self._proxied._has_events or \ - self.__dict__.get('_has_events', False) - - def _set_has_events(self, value): - self.__dict__['_has_events'] = value - - _has_events = property(_get_has_events, _set_has_events) diff --git a/libs/sqlalchemy/engine/default.py b/libs/sqlalchemy/engine/default.py deleted file mode 100644 index 509d772a..00000000 --- a/libs/sqlalchemy/engine/default.py +++ /dev/null @@ -1,908 +0,0 @@ -# engine/default.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Default implementations of per-dialect sqlalchemy.engine classes. - -These are semi-private implementation classes which are only of importance -to database dialect authors; dialects will usually use the classes here -as the base class for their own corresponding classes. - -""" - -import re -import random -from . import reflection, interfaces, result -from ..sql import compiler, expression -from .. import types as sqltypes -from .. import exc, util, pool, processors -import codecs -import weakref -from .. import event - -AUTOCOMMIT_REGEXP = re.compile( - r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER)', - re.I | re.UNICODE) - - - -class DefaultDialect(interfaces.Dialect): - """Default implementation of Dialect""" - - statement_compiler = compiler.SQLCompiler - ddl_compiler = compiler.DDLCompiler - type_compiler = compiler.GenericTypeCompiler - preparer = compiler.IdentifierPreparer - supports_alter = True - - # the first value we'd get for an autoincrement - # column. - default_sequence_base = 1 - - # most DBAPIs happy with this for execute(). - # not cx_oracle. - execute_sequence_format = tuple - - supports_views = True - supports_sequences = False - sequences_optional = False - preexecute_autoincrement_sequences = False - postfetch_lastrowid = True - implicit_returning = False - - supports_right_nested_joins = True - - supports_native_enum = False - supports_native_boolean = False - - supports_simple_order_by_label = True - - engine_config_types = util.immutabledict([ - ('convert_unicode', util.bool_or_str('force')), - ('pool_timeout', int), - ('echo', util.bool_or_str('debug')), - ('echo_pool', util.bool_or_str('debug')), - ('pool_recycle', int), - ('pool_size', int), - ('max_overflow', int), - ('pool_threadlocal', bool), - ('use_native_unicode', bool), - ]) - - # if the NUMERIC type - # returns decimal.Decimal. - # *not* the FLOAT type however. - supports_native_decimal = False - - if util.py3k: - supports_unicode_statements = True - supports_unicode_binds = True - returns_unicode_strings = True - description_encoding = None - else: - supports_unicode_statements = False - supports_unicode_binds = False - returns_unicode_strings = False - description_encoding = 'use_encoding' - - name = 'default' - - # length at which to truncate - # any identifier. - max_identifier_length = 9999 - - # length at which to truncate - # the name of an index. - # Usually None to indicate - # 'use max_identifier_length'. - # thanks to MySQL, sigh - max_index_name_length = None - - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - dbapi_type_map = {} - colspecs = {} - default_paramstyle = 'named' - supports_default_values = False - supports_empty_insert = True - supports_multivalues_insert = False - - server_version_info = None - - # indicates symbol names are - # UPPERCASEd if they are case insensitive - # within the database. - # if this is True, the methods normalize_name() - # and denormalize_name() must be provided. - requires_name_normalize = False - - reflection_options = () - - def __init__(self, convert_unicode=False, - encoding='utf-8', paramstyle=None, dbapi=None, - implicit_returning=None, - supports_right_nested_joins=None, - case_sensitive=True, - supports_native_boolean=None, - label_length=None, **kwargs): - - if not getattr(self, 'ported_sqla_06', True): - util.warn( - "The %s dialect is not yet ported to the 0.6 format" % - self.name) - - self.convert_unicode = convert_unicode - self.encoding = encoding - self.positional = False - self._ischema = None - self.dbapi = dbapi - if paramstyle is not None: - self.paramstyle = paramstyle - elif self.dbapi is not None: - self.paramstyle = self.dbapi.paramstyle - else: - self.paramstyle = self.default_paramstyle - if implicit_returning is not None: - self.implicit_returning = implicit_returning - self.positional = self.paramstyle in ('qmark', 'format', 'numeric') - self.identifier_preparer = self.preparer(self) - self.type_compiler = self.type_compiler(self) - if supports_right_nested_joins is not None: - self.supports_right_nested_joins = supports_right_nested_joins - if supports_native_boolean is not None: - self.supports_native_boolean = supports_native_boolean - self.case_sensitive = case_sensitive - - if label_length and label_length > self.max_identifier_length: - raise exc.ArgumentError( - "Label length of %d is greater than this dialect's" - " maximum identifier length of %d" % - (label_length, self.max_identifier_length)) - self.label_length = label_length - - if self.description_encoding == 'use_encoding': - self._description_decoder = \ - processors.to_unicode_processor_factory( - encoding - ) - elif self.description_encoding is not None: - self._description_decoder = \ - processors.to_unicode_processor_factory( - self.description_encoding - ) - self._encoder = codecs.getencoder(self.encoding) - self._decoder = processors.to_unicode_processor_factory(self.encoding) - - - @util.memoized_property - def _type_memos(self): - return weakref.WeakKeyDictionary() - - @property - def dialect_description(self): - return self.name + "+" + self.driver - - @classmethod - def get_pool_class(cls, url): - return getattr(cls, 'poolclass', pool.QueuePool) - - def initialize(self, connection): - try: - self.server_version_info = \ - self._get_server_version_info(connection) - except NotImplementedError: - self.server_version_info = None - try: - self.default_schema_name = \ - self._get_default_schema_name(connection) - except NotImplementedError: - self.default_schema_name = None - - try: - self.default_isolation_level = \ - self.get_isolation_level(connection.connection) - except NotImplementedError: - self.default_isolation_level = None - - self.returns_unicode_strings = self._check_unicode_returns(connection) - - if self.description_encoding is not None and \ - self._check_unicode_description(connection): - self._description_decoder = self.description_encoding = None - - self.do_rollback(connection.connection) - - def on_connect(self): - """return a callable which sets up a newly created DBAPI connection. - - This is used to set dialect-wide per-connection options such as - isolation modes, unicode modes, etc. - - If a callable is returned, it will be assembled into a pool listener - that receives the direct DBAPI connection, with all wrappers removed. - - If None is returned, no listener will be generated. - - """ - return None - - def _check_unicode_returns(self, connection): - if util.py2k and not self.supports_unicode_statements: - cast_to = util.binary_type - else: - cast_to = util.text_type - - def check_unicode(formatstr, type_): - cursor = connection.connection.cursor() - try: - try: - cursor.execute( - cast_to( - expression.select( - [expression.cast( - expression.literal_column( - "'test %s returns'" % formatstr), - type_) - ]).compile(dialect=self) - ) - ) - row = cursor.fetchone() - - return isinstance(row[0], util.text_type) - except self.dbapi.Error as de: - util.warn("Exception attempting to " - "detect unicode returns: %r" % de) - return False - finally: - cursor.close() - - # detect plain VARCHAR - unicode_for_varchar = check_unicode("plain", sqltypes.VARCHAR(60)) - - # detect if there's an NVARCHAR type with different behavior available - unicode_for_unicode = check_unicode("unicode", sqltypes.Unicode(60)) - - if unicode_for_unicode and not unicode_for_varchar: - return "conditional" - else: - return unicode_for_varchar - - def _check_unicode_description(self, connection): - # all DBAPIs on Py2K return cursor.description as encoded, - # until pypy2.1beta2 with sqlite, so let's just check it - - # it's likely others will start doing this too in Py2k. - - if util.py2k and not self.supports_unicode_statements: - cast_to = util.binary_type - else: - cast_to = util.text_type - - cursor = connection.connection.cursor() - try: - cursor.execute( - cast_to( - expression.select([ - expression.literal_column("'x'").label("some_label") - ]).compile(dialect=self) - ) - ) - return isinstance(cursor.description[0][0], util.text_type) - finally: - cursor.close() - - def type_descriptor(self, typeobj): - """Provide a database-specific :class:`.TypeEngine` object, given - the generic object which comes from the types module. - - This method looks for a dictionary called - ``colspecs`` as a class or instance-level variable, - and passes on to :func:`.types.adapt_type`. - - """ - return sqltypes.adapt_type(typeobj, self.colspecs) - - def reflecttable(self, connection, table, include_columns, exclude_columns): - insp = reflection.Inspector.from_engine(connection) - return insp.reflecttable(table, include_columns, exclude_columns) - - def get_pk_constraint(self, conn, table_name, schema=None, **kw): - """Compatibility method, adapts the result of get_primary_keys() - for those dialects which don't implement get_pk_constraint(). - - """ - return { - 'constrained_columns': - self.get_primary_keys(conn, table_name, - schema=schema, **kw) - } - - def validate_identifier(self, ident): - if len(ident) > self.max_identifier_length: - raise exc.IdentifierError( - "Identifier '%s' exceeds maximum length of %d characters" % - (ident, self.max_identifier_length) - ) - - def connect(self, *cargs, **cparams): - return self.dbapi.connect(*cargs, **cparams) - - def create_connect_args(self, url): - opts = url.translate_connect_args() - opts.update(url.query) - return [[], opts] - - def set_engine_execution_options(self, engine, opts): - if 'isolation_level' in opts: - isolation_level = opts['isolation_level'] - @event.listens_for(engine, "engine_connect") - def set_isolation(connection, branch): - if not branch: - self._set_connection_isolation(connection, isolation_level) - - def set_connection_execution_options(self, connection, opts): - if 'isolation_level' in opts: - self._set_connection_isolation(connection, opts['isolation_level']) - - def _set_connection_isolation(self, connection, level): - self.set_isolation_level(connection.connection, level) - connection.connection._connection_record.\ - finalize_callback.append(self.reset_isolation_level) - - - def do_begin(self, dbapi_connection): - pass - - def do_rollback(self, dbapi_connection): - dbapi_connection.rollback() - - def do_commit(self, dbapi_connection): - dbapi_connection.commit() - - def do_close(self, dbapi_connection): - dbapi_connection.close() - - def create_xid(self): - """Create a random two-phase transaction ID. - - This id will be passed to do_begin_twophase(), do_rollback_twophase(), - do_commit_twophase(). Its format is unspecified. - """ - - return "_sa_%032x" % random.randint(0, 2 ** 128) - - def do_savepoint(self, connection, name): - connection.execute(expression.SavepointClause(name)) - - def do_rollback_to_savepoint(self, connection, name): - connection.execute(expression.RollbackToSavepointClause(name)) - - def do_release_savepoint(self, connection, name): - connection.execute(expression.ReleaseSavepointClause(name)) - - def do_executemany(self, cursor, statement, parameters, context=None): - cursor.executemany(statement, parameters) - - def do_execute(self, cursor, statement, parameters, context=None): - cursor.execute(statement, parameters) - - def do_execute_no_params(self, cursor, statement, context=None): - cursor.execute(statement) - - def is_disconnect(self, e, connection, cursor): - return False - - def reset_isolation_level(self, dbapi_conn): - # default_isolation_level is read from the first connection - # after the initial set of 'isolation_level', if any, so is - # the configured default of this dialect. - self.set_isolation_level(dbapi_conn, self.default_isolation_level) - - -class DefaultExecutionContext(interfaces.ExecutionContext): - isinsert = False - isupdate = False - isdelete = False - isddl = False - executemany = False - result_map = None - compiled = None - statement = None - postfetch_cols = None - prefetch_cols = None - returning_cols = None - _is_implicit_returning = False - _is_explicit_returning = False - - # a hook for SQLite's translation of - # result column names - _translate_colname = None - - @classmethod - def _init_ddl(cls, dialect, connection, dbapi_connection, compiled_ddl): - """Initialize execution context for a DDLElement construct.""" - - self = cls.__new__(cls) - self.dialect = dialect - self.root_connection = connection - self._dbapi_connection = dbapi_connection - self.engine = connection.engine - - self.compiled = compiled = compiled_ddl - self.isddl = True - - self.execution_options = compiled.statement._execution_options - if connection._execution_options: - self.execution_options = dict(self.execution_options) - self.execution_options.update(connection._execution_options) - - if not dialect.supports_unicode_statements: - self.unicode_statement = util.text_type(compiled) - self.statement = dialect._encoder(self.unicode_statement)[0] - else: - self.statement = self.unicode_statement = util.text_type(compiled) - - self.cursor = self.create_cursor() - self.compiled_parameters = [] - - if dialect.positional: - self.parameters = [dialect.execute_sequence_format()] - else: - self.parameters = [{}] - - return self - - @classmethod - def _init_compiled(cls, dialect, connection, dbapi_connection, - compiled, parameters): - """Initialize execution context for a Compiled construct.""" - - self = cls.__new__(cls) - self.dialect = dialect - self.root_connection = connection - self._dbapi_connection = dbapi_connection - self.engine = connection.engine - - self.compiled = compiled - - if not compiled.can_execute: - raise exc.ArgumentError("Not an executable clause") - - self.execution_options = compiled.statement._execution_options - if connection._execution_options: - self.execution_options = dict(self.execution_options) - self.execution_options.update(connection._execution_options) - - # compiled clauseelement. process bind params, process table defaults, - # track collections used by ResultProxy to target and process results - - self.result_map = compiled.result_map - - self.unicode_statement = util.text_type(compiled) - if not dialect.supports_unicode_statements: - self.statement = self.unicode_statement.encode( - self.dialect.encoding) - else: - self.statement = self.unicode_statement - - self.isinsert = compiled.isinsert - self.isupdate = compiled.isupdate - self.isdelete = compiled.isdelete - - if self.isinsert or self.isupdate or self.isdelete: - self._is_explicit_returning = bool(compiled.statement._returning) - self._is_implicit_returning = bool(compiled.returning and \ - not compiled.statement._returning) - - if not parameters: - self.compiled_parameters = [compiled.construct_params()] - else: - self.compiled_parameters = \ - [compiled.construct_params(m, _group_number=grp) for - grp, m in enumerate(parameters)] - - self.executemany = len(parameters) > 1 - - self.cursor = self.create_cursor() - if self.isinsert or self.isupdate: - self.postfetch_cols = self.compiled.postfetch - self.prefetch_cols = self.compiled.prefetch - self.returning_cols = self.compiled.returning - self.__process_defaults() - - processors = compiled._bind_processors - - # Convert the dictionary of bind parameter values - # into a dict or list to be sent to the DBAPI's - # execute() or executemany() method. - parameters = [] - if dialect.positional: - for compiled_params in self.compiled_parameters: - param = [] - for key in self.compiled.positiontup: - if key in processors: - param.append(processors[key](compiled_params[key])) - else: - param.append(compiled_params[key]) - parameters.append(dialect.execute_sequence_format(param)) - else: - encode = not dialect.supports_unicode_statements - for compiled_params in self.compiled_parameters: - param = {} - if encode: - for key in compiled_params: - if key in processors: - param[dialect._encoder(key)[0]] = \ - processors[key](compiled_params[key]) - else: - param[dialect._encoder(key)[0]] = \ - compiled_params[key] - else: - for key in compiled_params: - if key in processors: - param[key] = processors[key](compiled_params[key]) - else: - param[key] = compiled_params[key] - parameters.append(param) - self.parameters = dialect.execute_sequence_format(parameters) - - return self - - @classmethod - def _init_statement(cls, dialect, connection, dbapi_connection, - statement, parameters): - """Initialize execution context for a string SQL statement.""" - - self = cls.__new__(cls) - self.dialect = dialect - self.root_connection = connection - self._dbapi_connection = dbapi_connection - self.engine = connection.engine - - # plain text statement - self.execution_options = connection._execution_options - - if not parameters: - if self.dialect.positional: - self.parameters = [dialect.execute_sequence_format()] - else: - self.parameters = [{}] - elif isinstance(parameters[0], dialect.execute_sequence_format): - self.parameters = parameters - elif isinstance(parameters[0], dict): - if dialect.supports_unicode_statements: - self.parameters = parameters - else: - self.parameters = [ - dict((dialect._encoder(k)[0], d[k]) for k in d) - for d in parameters - ] or [{}] - else: - self.parameters = [dialect.execute_sequence_format(p) - for p in parameters] - - self.executemany = len(parameters) > 1 - - if not dialect.supports_unicode_statements and \ - isinstance(statement, util.text_type): - self.unicode_statement = statement - self.statement = dialect._encoder(statement)[0] - else: - self.statement = self.unicode_statement = statement - - self.cursor = self.create_cursor() - return self - - @classmethod - def _init_default(cls, dialect, connection, dbapi_connection): - """Initialize execution context for a ColumnDefault construct.""" - - self = cls.__new__(cls) - self.dialect = dialect - self.root_connection = connection - self._dbapi_connection = dbapi_connection - self.engine = connection.engine - self.execution_options = connection._execution_options - self.cursor = self.create_cursor() - return self - - @util.memoized_property - def no_parameters(self): - return self.execution_options.get("no_parameters", False) - - @util.memoized_property - def is_crud(self): - return self.isinsert or self.isupdate or self.isdelete - - @util.memoized_property - def should_autocommit(self): - autocommit = self.execution_options.get('autocommit', - not self.compiled and - self.statement and - expression.PARSE_AUTOCOMMIT - or False) - - if autocommit is expression.PARSE_AUTOCOMMIT: - return self.should_autocommit_text(self.unicode_statement) - else: - return autocommit - - def _execute_scalar(self, stmt, type_): - """Execute a string statement on the current cursor, returning a - scalar result. - - Used to fire off sequences, default phrases, and "select lastrowid" - types of statements individually or in the context of a parent INSERT - or UPDATE statement. - - """ - - conn = self.root_connection - if isinstance(stmt, util.text_type) and \ - not self.dialect.supports_unicode_statements: - stmt = self.dialect._encoder(stmt)[0] - - if self.dialect.positional: - default_params = self.dialect.execute_sequence_format() - else: - default_params = {} - - conn._cursor_execute(self.cursor, stmt, default_params, context=self) - r = self.cursor.fetchone()[0] - if type_ is not None: - # apply type post processors to the result - proc = type_._cached_result_processor( - self.dialect, - self.cursor.description[0][1] - ) - if proc: - return proc(r) - return r - - @property - def connection(self): - return self.root_connection._branch() - - def should_autocommit_text(self, statement): - return AUTOCOMMIT_REGEXP.match(statement) - - def create_cursor(self): - return self._dbapi_connection.cursor() - - def pre_exec(self): - pass - - def post_exec(self): - pass - - def get_result_processor(self, type_, colname, coltype): - """Return a 'result processor' for a given type as present in - cursor.description. - - This has a default implementation that dialects can override - for context-sensitive result type handling. - - """ - return type_._cached_result_processor(self.dialect, coltype) - - def get_lastrowid(self): - """return self.cursor.lastrowid, or equivalent, after an INSERT. - - This may involve calling special cursor functions, - issuing a new SELECT on the cursor (or a new one), - or returning a stored value that was - calculated within post_exec(). - - This function will only be called for dialects - which support "implicit" primary key generation, - keep preexecute_autoincrement_sequences set to False, - and when no explicit id value was bound to the - statement. - - The function is called once, directly after - post_exec() and before the transaction is committed - or ResultProxy is generated. If the post_exec() - method assigns a value to `self._lastrowid`, the - value is used in place of calling get_lastrowid(). - - Note that this method is *not* equivalent to the - ``lastrowid`` method on ``ResultProxy``, which is a - direct proxy to the DBAPI ``lastrowid`` accessor - in all cases. - - """ - return self.cursor.lastrowid - - def handle_dbapi_exception(self, e): - pass - - def get_result_proxy(self): - return result.ResultProxy(self) - - @property - def rowcount(self): - return self.cursor.rowcount - - def supports_sane_rowcount(self): - return self.dialect.supports_sane_rowcount - - def supports_sane_multi_rowcount(self): - return self.dialect.supports_sane_multi_rowcount - - def post_insert(self): - if not self._is_implicit_returning and \ - not self._is_explicit_returning and \ - not self.compiled.inline and \ - self.dialect.postfetch_lastrowid and \ - (not self.inserted_primary_key or \ - None in self.inserted_primary_key): - - table = self.compiled.statement.table - lastrowid = self.get_lastrowid() - autoinc_col = table._autoincrement_column - if autoinc_col is not None: - # apply type post processors to the lastrowid - proc = autoinc_col.type._cached_result_processor( - self.dialect, None) - if proc is not None: - lastrowid = proc(lastrowid) - - self.inserted_primary_key = [ - lastrowid if c is autoinc_col else v - for c, v in zip( - table.primary_key, - self.inserted_primary_key) - ] - - def _fetch_implicit_returning(self, resultproxy): - table = self.compiled.statement.table - row = resultproxy.fetchone() - - ipk = [] - for c, v in zip(table.primary_key, self.inserted_primary_key): - if v is not None: - ipk.append(v) - else: - ipk.append(row[c]) - - self.inserted_primary_key = ipk - self.returned_defaults = row - - def _fetch_implicit_update_returning(self, resultproxy): - row = resultproxy.fetchone() - self.returned_defaults = row - - def lastrow_has_defaults(self): - return (self.isinsert or self.isupdate) and \ - bool(self.postfetch_cols) - - def set_input_sizes(self, translate=None, exclude_types=None): - """Given a cursor and ClauseParameters, call the appropriate - style of ``setinputsizes()`` on the cursor, using DB-API types - from the bind parameter's ``TypeEngine`` objects. - - This method only called by those dialects which require it, - currently cx_oracle. - - """ - - if not hasattr(self.compiled, 'bind_names'): - return - - types = dict( - (self.compiled.bind_names[bindparam], bindparam.type) - for bindparam in self.compiled.bind_names) - - if self.dialect.positional: - inputsizes = [] - for key in self.compiled.positiontup: - typeengine = types[key] - dbtype = typeengine.dialect_impl(self.dialect).\ - get_dbapi_type(self.dialect.dbapi) - if dbtype is not None and \ - (not exclude_types or dbtype not in exclude_types): - inputsizes.append(dbtype) - try: - self.cursor.setinputsizes(*inputsizes) - except Exception as e: - self.root_connection._handle_dbapi_exception( - e, None, None, None, self) - else: - inputsizes = {} - for key in self.compiled.bind_names.values(): - typeengine = types[key] - dbtype = typeengine.dialect_impl(self.dialect).\ - get_dbapi_type(self.dialect.dbapi) - if dbtype is not None and \ - (not exclude_types or dbtype not in exclude_types): - if translate: - key = translate.get(key, key) - if not self.dialect.supports_unicode_binds: - key = self.dialect._encoder(key)[0] - inputsizes[key] = dbtype - try: - self.cursor.setinputsizes(**inputsizes) - except Exception as e: - self.root_connection._handle_dbapi_exception( - e, None, None, None, self) - - def _exec_default(self, default, type_): - if default.is_sequence: - return self.fire_sequence(default, type_) - elif default.is_callable: - return default.arg(self) - elif default.is_clause_element: - # TODO: expensive branching here should be - # pulled into _exec_scalar() - conn = self.connection - c = expression.select([default.arg]).compile(bind=conn) - return conn._execute_compiled(c, (), {}).scalar() - else: - return default.arg - - def get_insert_default(self, column): - if column.default is None: - return None - else: - return self._exec_default(column.default, column.type) - - def get_update_default(self, column): - if column.onupdate is None: - return None - else: - return self._exec_default(column.onupdate, column.type) - - def __process_defaults(self): - """Generate default values for compiled insert/update statements, - and generate inserted_primary_key collection. - """ - - if self.executemany: - if len(self.compiled.prefetch): - scalar_defaults = {} - - # pre-determine scalar Python-side defaults - # to avoid many calls of get_insert_default()/ - # get_update_default() - for c in self.prefetch_cols: - if self.isinsert and c.default and c.default.is_scalar: - scalar_defaults[c] = c.default.arg - elif self.isupdate and c.onupdate and c.onupdate.is_scalar: - scalar_defaults[c] = c.onupdate.arg - - for param in self.compiled_parameters: - self.current_parameters = param - for c in self.prefetch_cols: - if c in scalar_defaults: - val = scalar_defaults[c] - elif self.isinsert: - val = self.get_insert_default(c) - else: - val = self.get_update_default(c) - if val is not None: - param[c.key] = val - del self.current_parameters - else: - self.current_parameters = compiled_parameters = \ - self.compiled_parameters[0] - - for c in self.compiled.prefetch: - if self.isinsert: - val = self.get_insert_default(c) - else: - val = self.get_update_default(c) - - if val is not None: - compiled_parameters[c.key] = val - del self.current_parameters - - if self.isinsert: - self.inserted_primary_key = [ - self.compiled_parameters[0].get(c.key, None) - for c in self.compiled.\ - statement.table.primary_key - ] - - -DefaultDialect.execution_ctx_cls = DefaultExecutionContext diff --git a/libs/sqlalchemy/engine/interfaces.py b/libs/sqlalchemy/engine/interfaces.py deleted file mode 100644 index c26681cc..00000000 --- a/libs/sqlalchemy/engine/interfaces.py +++ /dev/null @@ -1,846 +0,0 @@ -# engine/interfaces.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Define core interfaces used by the engine system.""" - -from .. import util, event - -# backwards compat -from ..sql.compiler import Compiled, TypeCompiler - -class Dialect(object): - """Define the behavior of a specific database and DB-API combination. - - Any aspect of metadata definition, SQL query generation, - execution, result-set handling, or anything else which varies - between databases is defined under the general category of the - Dialect. The Dialect acts as a factory for other - database-specific object implementations including - ExecutionContext, Compiled, DefaultGenerator, and TypeEngine. - - All Dialects implement the following attributes: - - name - identifying name for the dialect from a DBAPI-neutral point of view - (i.e. 'sqlite') - - driver - identifying name for the dialect's DBAPI - - positional - True if the paramstyle for this Dialect is positional. - - paramstyle - the paramstyle to be used (some DB-APIs support multiple - paramstyles). - - convert_unicode - True if Unicode conversion should be applied to all ``str`` - types. - - encoding - type of encoding to use for unicode, usually defaults to - 'utf-8'. - - statement_compiler - a :class:`.Compiled` class used to compile SQL statements - - ddl_compiler - a :class:`.Compiled` class used to compile DDL statements - - server_version_info - a tuple containing a version number for the DB backend in use. - This value is only available for supporting dialects, and is - typically populated during the initial connection to the database. - - default_schema_name - the name of the default schema. This value is only available for - supporting dialects, and is typically populated during the - initial connection to the database. - - execution_ctx_cls - a :class:`.ExecutionContext` class used to handle statement execution - - execute_sequence_format - either the 'tuple' or 'list' type, depending on what cursor.execute() - accepts for the second argument (they vary). - - preparer - a :class:`~sqlalchemy.sql.compiler.IdentifierPreparer` class used to - quote identifiers. - - supports_alter - ``True`` if the database supports ``ALTER TABLE``. - - max_identifier_length - The maximum length of identifier names. - - supports_unicode_statements - Indicate whether the DB-API can receive SQL statements as Python - unicode strings - - supports_unicode_binds - Indicate whether the DB-API can receive string bind parameters - as Python unicode strings - - supports_sane_rowcount - Indicate whether the dialect properly implements rowcount for - ``UPDATE`` and ``DELETE`` statements. - - supports_sane_multi_rowcount - Indicate whether the dialect properly implements rowcount for - ``UPDATE`` and ``DELETE`` statements when executed via - executemany. - - preexecute_autoincrement_sequences - True if 'implicit' primary key functions must be executed separately - in order to get their value. This is currently oriented towards - Postgresql. - - implicit_returning - use RETURNING or equivalent during INSERT execution in order to load - newly generated primary keys and other column defaults in one execution, - which are then available via inserted_primary_key. - If an insert statement has returning() specified explicitly, - the "implicit" functionality is not used and inserted_primary_key - will not be available. - - dbapi_type_map - A mapping of DB-API type objects present in this Dialect's - DB-API implementation mapped to TypeEngine implementations used - by the dialect. - - This is used to apply types to result sets based on the DB-API - types present in cursor.description; it only takes effect for - result sets against textual statements where no explicit - typemap was present. - - colspecs - A dictionary of TypeEngine classes from sqlalchemy.types mapped - to subclasses that are specific to the dialect class. This - dictionary is class-level only and is not accessed from the - dialect instance itself. - - supports_default_values - Indicates if the construct ``INSERT INTO tablename DEFAULT - VALUES`` is supported - - supports_sequences - Indicates if the dialect supports CREATE SEQUENCE or similar. - - sequences_optional - If True, indicates if the "optional" flag on the Sequence() construct - should signal to not generate a CREATE SEQUENCE. Applies only to - dialects that support sequences. Currently used only to allow Postgresql - SERIAL to be used on a column that specifies Sequence() for usage on - other backends. - - supports_native_enum - Indicates if the dialect supports a native ENUM construct. - This will prevent types.Enum from generating a CHECK - constraint when that type is used. - - supports_native_boolean - Indicates if the dialect supports a native boolean construct. - This will prevent types.Boolean from generating a CHECK - constraint when that type is used. - - """ - - def create_connect_args(self, url): - """Build DB-API compatible connection arguments. - - Given a :class:`~sqlalchemy.engine.url.URL` object, returns a tuple - consisting of a `*args`/`**kwargs` suitable to send directly - to the dbapi's connect function. - - """ - - raise NotImplementedError() - - @classmethod - def type_descriptor(cls, typeobj): - """Transform a generic type to a dialect-specific type. - - Dialect classes will usually use the - :func:`.types.adapt_type` function in the types module to - accomplish this. - - The returned result is cached *per dialect class* so can - contain no dialect-instance state. - - """ - - raise NotImplementedError() - - def initialize(self, connection): - """Called during strategized creation of the dialect with a - connection. - - Allows dialects to configure options based on server version info or - other properties. - - The connection passed here is a SQLAlchemy Connection object, - with full capabilities. - - The initalize() method of the base dialect should be called via - super(). - - """ - - pass - - def reflecttable(self, connection, table, include_columns, exclude_columns): - """Load table description from the database. - - Given a :class:`.Connection` and a - :class:`~sqlalchemy.schema.Table` object, reflect its columns and - properties from the database. - - The implementation of this method is provided by - :meth:`.DefaultDialect.reflecttable`, which makes use of - :class:`.Inspector` to retrieve column information. - - Dialects should **not** seek to implement this method, and should - instead implement individual schema inspection operations such as - :meth:`.Dialect.get_columns`, :meth:`.Dialect.get_pk_constraint`, - etc. - - """ - - raise NotImplementedError() - - def get_columns(self, connection, table_name, schema=None, **kw): - """Return information about columns in `table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return column - information as a list of dictionaries with these keys: - - name - the column's name - - type - [sqlalchemy.types#TypeEngine] - - nullable - boolean - - default - the column's default value - - autoincrement - boolean - - sequence - a dictionary of the form - {'name' : str, 'start' :int, 'increment': int} - - Additional column attributes may be present. - """ - - raise NotImplementedError() - - def get_primary_keys(self, connection, table_name, schema=None, **kw): - """Return information about primary keys in `table_name`. - - - Deprecated. This method is only called by the default - implementation of :meth:`.Dialect.get_pk_constraint`. Dialects should - instead implement the :meth:`.Dialect.get_pk_constraint` method directly. - - """ - - raise NotImplementedError() - - def get_pk_constraint(self, connection, table_name, schema=None, **kw): - """Return information about the primary key constraint on - table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return primary - key information as a dictionary with these keys: - - constrained_columns - a list of column names that make up the primary key - - name - optional name of the primary key constraint. - - """ - raise NotImplementedError() - - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - """Return information about foreign_keys in `table_name`. - - Given a :class:`.Connection`, a string - `table_name`, and an optional string `schema`, return foreign - key information as a list of dicts with these keys: - - name - the constraint's name - - constrained_columns - a list of column names that make up the foreign key - - referred_schema - the name of the referred schema - - referred_table - the name of the referred table - - referred_columns - a list of column names in the referred table that correspond to - constrained_columns - """ - - raise NotImplementedError() - - def get_table_names(self, connection, schema=None, **kw): - """Return a list of table names for `schema`.""" - - raise NotImplementedError - - def get_view_names(self, connection, schema=None, **kw): - """Return a list of all view names available in the database. - - schema: - Optional, retrieve names from a non-default schema. - """ - - raise NotImplementedError() - - def get_view_definition(self, connection, view_name, schema=None, **kw): - """Return view definition. - - Given a :class:`.Connection`, a string - `view_name`, and an optional string `schema`, return the view - definition. - """ - - raise NotImplementedError() - - def get_indexes(self, connection, table_name, schema=None, **kw): - """Return information about indexes in `table_name`. - - Given a :class:`.Connection`, a string - `table_name` and an optional string `schema`, return index - information as a list of dictionaries with these keys: - - name - the index's name - - column_names - list of column names in order - - unique - boolean - """ - - raise NotImplementedError() - - def get_unique_constraints(self, table_name, schema=None, **kw): - """Return information about unique constraints in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - unique constraint information as a list of dicts with these keys: - - name - the unique constraint's name - - column_names - list of column names in order - - \**kw - other options passed to the dialect's get_unique_constraints() method. - - .. versionadded:: 0.9.0 - - """ - - raise NotImplementedError() - - def normalize_name(self, name): - """convert the given name to lowercase if it is detected as - case insensitive. - - this method is only used if the dialect defines - requires_name_normalize=True. - - """ - raise NotImplementedError() - - def denormalize_name(self, name): - """convert the given name to a case insensitive identifier - for the backend if it is an all-lowercase name. - - this method is only used if the dialect defines - requires_name_normalize=True. - - """ - raise NotImplementedError() - - def has_table(self, connection, table_name, schema=None): - """Check the existence of a particular table in the database. - - Given a :class:`.Connection` object and a string - `table_name`, return True if the given table (possibly within - the specified `schema`) exists in the database, False - otherwise. - """ - - raise NotImplementedError() - - def has_sequence(self, connection, sequence_name, schema=None): - """Check the existence of a particular sequence in the database. - - Given a :class:`.Connection` object and a string - `sequence_name`, return True if the given sequence exists in - the database, False otherwise. - """ - - raise NotImplementedError() - - def _get_server_version_info(self, connection): - """Retrieve the server version info from the given connection. - - This is used by the default implementation to populate the - "server_version_info" attribute and is called exactly - once upon first connect. - - """ - - raise NotImplementedError() - - def _get_default_schema_name(self, connection): - """Return the string name of the currently selected schema from - the given connection. - - This is used by the default implementation to populate the - "default_schema_name" attribute and is called exactly - once upon first connect. - - """ - - raise NotImplementedError() - - def do_begin(self, dbapi_connection): - """Provide an implementation of ``connection.begin()``, given a - DB-API connection. - - The DBAPI has no dedicated "begin" method and it is expected - that transactions are implicit. This hook is provided for those - DBAPIs that might need additional help in this area. - - Note that :meth:`.Dialect.do_begin` is not called unless a - :class:`.Transaction` object is in use. The - :meth:`.Dialect.do_autocommit` - hook is provided for DBAPIs that need some extra commands emitted - after a commit in order to enter the next transaction, when the - SQLAlchemy :class:`.Connection` is used in it's default "autocommit" - mode. - - :param dbapi_connection: a DBAPI connection, typically - proxied within a :class:`.ConnectionFairy`. - - """ - - raise NotImplementedError() - - def do_rollback(self, dbapi_connection): - """Provide an implementation of ``connection.rollback()``, given - a DB-API connection. - - :param dbapi_connection: a DBAPI connection, typically - proxied within a :class:`.ConnectionFairy`. - - """ - - raise NotImplementedError() - - - def do_commit(self, dbapi_connection): - """Provide an implementation of ``connection.commit()``, given a - DB-API connection. - - :param dbapi_connection: a DBAPI connection, typically - proxied within a :class:`.ConnectionFairy`. - - """ - - raise NotImplementedError() - - def do_close(self, dbapi_connection): - """Provide an implementation of ``connection.close()``, given a DBAPI - connection. - - This hook is called by the :class:`.Pool` when a connection has been - detached from the pool, or is being returned beyond the normal - capacity of the pool. - - .. versionadded:: 0.8 - - """ - - raise NotImplementedError() - - def create_xid(self): - """Create a two-phase transaction ID. - - This id will be passed to do_begin_twophase(), - do_rollback_twophase(), do_commit_twophase(). Its format is - unspecified. - """ - - raise NotImplementedError() - - def do_savepoint(self, connection, name): - """Create a savepoint with the given name. - - :param connection: a :class:`.Connection`. - :param name: savepoint name. - - """ - - raise NotImplementedError() - - def do_rollback_to_savepoint(self, connection, name): - """Rollback a connection to the named savepoint. - - :param connection: a :class:`.Connection`. - :param name: savepoint name. - - """ - - raise NotImplementedError() - - def do_release_savepoint(self, connection, name): - """Release the named savepoint on a connection. - - :param connection: a :class:`.Connection`. - :param name: savepoint name. - """ - - raise NotImplementedError() - - def do_begin_twophase(self, connection, xid): - """Begin a two phase transaction on the given connection. - - :param connection: a :class:`.Connection`. - :param xid: xid - - """ - - raise NotImplementedError() - - def do_prepare_twophase(self, connection, xid): - """Prepare a two phase transaction on the given connection. - - :param connection: a :class:`.Connection`. - :param xid: xid - - """ - - raise NotImplementedError() - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - """Rollback a two phase transaction on the given connection. - - :param connection: a :class:`.Connection`. - :param xid: xid - :param is_prepared: whether or not - :meth:`.TwoPhaseTransaction.prepare` was called. - :param recover: if the recover flag was passed. - - """ - - raise NotImplementedError() - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - """Commit a two phase transaction on the given connection. - - - :param connection: a :class:`.Connection`. - :param xid: xid - :param is_prepared: whether or not - :meth:`.TwoPhaseTransaction.prepare` was called. - :param recover: if the recover flag was passed. - - """ - - raise NotImplementedError() - - def do_recover_twophase(self, connection): - """Recover list of uncommited prepared two phase transaction - identifiers on the given connection. - - :param connection: a :class:`.Connection`. - - """ - - raise NotImplementedError() - - def do_executemany(self, cursor, statement, parameters, context=None): - """Provide an implementation of ``cursor.executemany(statement, - parameters)``.""" - - raise NotImplementedError() - - def do_execute(self, cursor, statement, parameters, context=None): - """Provide an implementation of ``cursor.execute(statement, - parameters)``.""" - - raise NotImplementedError() - - def do_execute_no_params(self, cursor, statement, parameters, - context=None): - """Provide an implementation of ``cursor.execute(statement)``. - - The parameter collection should not be sent. - - """ - - raise NotImplementedError() - - def is_disconnect(self, e, connection, cursor): - """Return True if the given DB-API error indicates an invalid - connection""" - - raise NotImplementedError() - - def connect(self): - """return a callable which sets up a newly created DBAPI connection. - - The callable accepts a single argument "conn" which is the - DBAPI connection itself. It has no return value. - - This is used to set dialect-wide per-connection options such as - isolation modes, unicode modes, etc. - - If a callable is returned, it will be assembled into a pool listener - that receives the direct DBAPI connection, with all wrappers removed. - - If None is returned, no listener will be generated. - - """ - return None - - def reset_isolation_level(self, dbapi_conn): - """Given a DBAPI connection, revert its isolation to the default.""" - - raise NotImplementedError() - - def set_isolation_level(self, dbapi_conn, level): - """Given a DBAPI connection, set its isolation level.""" - - raise NotImplementedError() - - def get_isolation_level(self, dbapi_conn): - """Given a DBAPI connection, return its isolation level.""" - - raise NotImplementedError() - - -class ExecutionContext(object): - """A messenger object for a Dialect that corresponds to a single - execution. - - ExecutionContext should have these data members: - - connection - Connection object which can be freely used by default value - generators to execute SQL. This Connection should reference the - same underlying connection/transactional resources of - root_connection. - - root_connection - Connection object which is the source of this ExecutionContext. This - Connection may have close_with_result=True set, in which case it can - only be used once. - - dialect - dialect which created this ExecutionContext. - - cursor - DB-API cursor procured from the connection, - - compiled - if passed to constructor, sqlalchemy.engine.base.Compiled object - being executed, - - statement - string version of the statement to be executed. Is either - passed to the constructor, or must be created from the - sql.Compiled object by the time pre_exec() has completed. - - parameters - bind parameters passed to the execute() method. For compiled - statements, this is a dictionary or list of dictionaries. For - textual statements, it should be in a format suitable for the - dialect's paramstyle (i.e. dict or list of dicts for non - positional, list or list of lists/tuples for positional). - - isinsert - True if the statement is an INSERT. - - isupdate - True if the statement is an UPDATE. - - should_autocommit - True if the statement is a "committable" statement. - - prefetch_cols - a list of Column objects for which a client-side default - was fired off. Applies to inserts and updates. - - postfetch_cols - a list of Column objects for which a server-side default or - inline SQL expression value was fired off. Applies to inserts - and updates. - """ - - def create_cursor(self): - """Return a new cursor generated from this ExecutionContext's - connection. - - Some dialects may wish to change the behavior of - connection.cursor(), such as postgresql which may return a PG - "server side" cursor. - """ - - raise NotImplementedError() - - def pre_exec(self): - """Called before an execution of a compiled statement. - - If a compiled statement was passed to this ExecutionContext, - the `statement` and `parameters` datamembers must be - initialized after this statement is complete. - """ - - raise NotImplementedError() - - def post_exec(self): - """Called after the execution of a compiled statement. - - If a compiled statement was passed to this ExecutionContext, - the `last_insert_ids`, `last_inserted_params`, etc. - datamembers should be available after this method completes. - """ - - raise NotImplementedError() - - def result(self): - """Return a result object corresponding to this ExecutionContext. - - Returns a ResultProxy. - """ - - raise NotImplementedError() - - def handle_dbapi_exception(self, e): - """Receive a DBAPI exception which occurred upon execute, result - fetch, etc.""" - - raise NotImplementedError() - - def should_autocommit_text(self, statement): - """Parse the given textual statement and return True if it refers to - a "committable" statement""" - - raise NotImplementedError() - - def lastrow_has_defaults(self): - """Return True if the last INSERT or UPDATE row contained - inlined or database-side defaults. - """ - - raise NotImplementedError() - - def get_rowcount(self): - """Return the DBAPI ``cursor.rowcount`` value, or in some - cases an interpreted value. - - See :attr:`.ResultProxy.rowcount` for details on this. - - """ - - raise NotImplementedError() - - -class Connectable(object): - """Interface for an object which supports execution of SQL constructs. - - The two implementations of :class:`.Connectable` are - :class:`.Connection` and :class:`.Engine`. - - Connectable must also implement the 'dialect' member which references a - :class:`.Dialect` instance. - - """ - - def connect(self, **kwargs): - """Return a :class:`.Connection` object. - - Depending on context, this may be ``self`` if this object - is already an instance of :class:`.Connection`, or a newly - procured :class:`.Connection` if this object is an instance - of :class:`.Engine`. - - """ - - def contextual_connect(self): - """Return a :class:`.Connection` object which may be part of an ongoing - context. - - Depending on context, this may be ``self`` if this object - is already an instance of :class:`.Connection`, or a newly - procured :class:`.Connection` if this object is an instance - of :class:`.Engine`. - - """ - - raise NotImplementedError() - - @util.deprecated("0.7", - "Use the create() method on the given schema " - "object directly, i.e. :meth:`.Table.create`, " - ":meth:`.Index.create`, :meth:`.MetaData.create_all`") - def create(self, entity, **kwargs): - """Emit CREATE statements for the given schema entity. - """ - - raise NotImplementedError() - - @util.deprecated("0.7", - "Use the drop() method on the given schema " - "object directly, i.e. :meth:`.Table.drop`, " - ":meth:`.Index.drop`, :meth:`.MetaData.drop_all`") - def drop(self, entity, **kwargs): - """Emit DROP statements for the given schema entity. - """ - - raise NotImplementedError() - - def execute(self, object, *multiparams, **params): - """Executes the given construct and returns a :class:`.ResultProxy`.""" - raise NotImplementedError() - - def scalar(self, object, *multiparams, **params): - """Executes and returns the first column of the first row. - - The underlying cursor is closed after execution. - """ - raise NotImplementedError() - - def _run_visitor(self, visitorcallable, element, - **kwargs): - raise NotImplementedError() - - def _execute_clauseelement(self, elem, multiparams=None, params=None): - raise NotImplementedError() diff --git a/libs/sqlalchemy/engine/reflection.py b/libs/sqlalchemy/engine/reflection.py deleted file mode 100644 index badec84e..00000000 --- a/libs/sqlalchemy/engine/reflection.py +++ /dev/null @@ -1,595 +0,0 @@ -# engine/reflection.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provides an abstraction for obtaining database schema information. - -Usage Notes: - -Here are some general conventions when accessing the low level inspector -methods such as get_table_names, get_columns, etc. - -1. Inspector methods return lists of dicts in most cases for the following - reasons: - - * They're both standard types that can be serialized. - * Using a dict instead of a tuple allows easy expansion of attributes. - * Using a list for the outer structure maintains order and is easy to work - with (e.g. list comprehension [d['name'] for d in cols]). - -2. Records that contain a name, such as the column name in a column record - use the key 'name'. So for most return values, each record will have a - 'name' attribute.. -""" - -from .. import exc, sql -from ..sql import schema as sa_schema -from .. import util -from ..sql.type_api import TypeEngine -from ..util import deprecated -from ..util import topological -from .. import inspection -from .base import Connectable - - -@util.decorator -def cache(fn, self, con, *args, **kw): - info_cache = kw.get('info_cache', None) - if info_cache is None: - return fn(self, con, *args, **kw) - key = ( - fn.__name__, - tuple(a for a in args if isinstance(a, util.string_types)), - tuple((k, v) for k, v in kw.items() if - isinstance(v, - util.string_types + util.int_types + (float, ) - ) - ) - ) - ret = info_cache.get(key) - if ret is None: - ret = fn(self, con, *args, **kw) - info_cache[key] = ret - return ret - - -class Inspector(object): - """Performs database schema inspection. - - The Inspector acts as a proxy to the reflection methods of the - :class:`~sqlalchemy.engine.interfaces.Dialect`, providing a - consistent interface as well as caching support for previously - fetched metadata. - - A :class:`.Inspector` object is usually created via the - :func:`.inspect` function:: - - from sqlalchemy import inspect, create_engine - engine = create_engine('...') - insp = inspect(engine) - - The inspection method above is equivalent to using the - :meth:`.Inspector.from_engine` method, i.e.:: - - engine = create_engine('...') - insp = Inspector.from_engine(engine) - - Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt - to return an :class:`.Inspector` subclass that provides additional - methods specific to the dialect's target database. - - """ - - def __init__(self, bind): - """Initialize a new :class:`.Inspector`. - - :param bind: a :class:`~sqlalchemy.engine.Connectable`, - which is typically an instance of - :class:`~sqlalchemy.engine.Engine` or - :class:`~sqlalchemy.engine.Connection`. - - For a dialect-specific instance of :class:`.Inspector`, see - :meth:`.Inspector.from_engine` - - """ - # this might not be a connection, it could be an engine. - self.bind = bind - - # set the engine - if hasattr(bind, 'engine'): - self.engine = bind.engine - else: - self.engine = bind - - if self.engine is bind: - # if engine, ensure initialized - bind.connect().close() - - self.dialect = self.engine.dialect - self.info_cache = {} - - @classmethod - def from_engine(cls, bind): - """Construct a new dialect-specific Inspector object from the given - engine or connection. - - :param bind: a :class:`~sqlalchemy.engine.Connectable`, - which is typically an instance of - :class:`~sqlalchemy.engine.Engine` or - :class:`~sqlalchemy.engine.Connection`. - - This method differs from direct a direct constructor call of - :class:`.Inspector` in that the - :class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to - provide a dialect-specific :class:`.Inspector` instance, which may - provide additional methods. - - See the example at :class:`.Inspector`. - - """ - if hasattr(bind.dialect, 'inspector'): - return bind.dialect.inspector(bind) - return Inspector(bind) - - @inspection._inspects(Connectable) - def _insp(bind): - return Inspector.from_engine(bind) - - @property - def default_schema_name(self): - """Return the default schema name presented by the dialect - for the current engine's database user. - - E.g. this is typically ``public`` for Postgresql and ``dbo`` - for SQL Server. - - """ - return self.dialect.default_schema_name - - def get_schema_names(self): - """Return all schema names. - """ - - if hasattr(self.dialect, 'get_schema_names'): - return self.dialect.get_schema_names(self.bind, - info_cache=self.info_cache) - return [] - - def get_table_names(self, schema=None, order_by=None): - """Return all table names in referred to within a particular schema. - - The names are expected to be real tables only, not views. - Views are instead returned using the :meth:`.Inspector.get_view_names` - method. - - - :param schema: Schema name. If ``schema`` is left at ``None``, the - database's default schema is - used, else the named schema is searched. If the database does not - support named schemas, behavior is undefined if ``schema`` is not - passed as ``None``. For special quoting, use :class:`.quoted_name`. - - :param order_by: Optional, may be the string "foreign_key" to sort - the result on foreign key dependencies. - - .. versionchanged:: 0.8 the "foreign_key" sorting sorts tables - in order of dependee to dependent; that is, in creation - order, rather than in drop order. This is to maintain - consistency with similar features such as - :attr:`.MetaData.sorted_tables` and :func:`.util.sort_tables`. - - .. seealso:: - - :attr:`.MetaData.sorted_tables` - - """ - - if hasattr(self.dialect, 'get_table_names'): - tnames = self.dialect.get_table_names(self.bind, - schema, info_cache=self.info_cache) - else: - tnames = self.engine.table_names(schema) - if order_by == 'foreign_key': - tuples = [] - for tname in tnames: - for fkey in self.get_foreign_keys(tname, schema): - if tname != fkey['referred_table']: - tuples.append((fkey['referred_table'], tname)) - tnames = list(topological.sort(tuples, tnames)) - return tnames - - def get_table_options(self, table_name, schema=None, **kw): - """Return a dictionary of options specified when the table of the - given name was created. - - This currently includes some options that apply to MySQL tables. - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - """ - if hasattr(self.dialect, 'get_table_options'): - return self.dialect.get_table_options( - self.bind, table_name, schema, - info_cache=self.info_cache, **kw) - return {} - - def get_view_names(self, schema=None): - """Return all view names in `schema`. - - :param schema: Optional, retrieve names from a non-default schema. - For special quoting, use :class:`.quoted_name`. - - """ - - return self.dialect.get_view_names(self.bind, schema, - info_cache=self.info_cache) - - def get_view_definition(self, view_name, schema=None): - """Return definition for `view_name`. - - :param schema: Optional, retrieve names from a non-default schema. - For special quoting, use :class:`.quoted_name`. - - """ - - return self.dialect.get_view_definition( - self.bind, view_name, schema, info_cache=self.info_cache) - - def get_columns(self, table_name, schema=None, **kw): - """Return information about columns in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - column information as a list of dicts with these keys: - - name - the column's name - - type - :class:`~sqlalchemy.types.TypeEngine` - - nullable - boolean - - default - the column's default value - - attrs - dict containing optional column attributes - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - """ - - col_defs = self.dialect.get_columns(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw) - for col_def in col_defs: - # make this easy and only return instances for coltype - coltype = col_def['type'] - if not isinstance(coltype, TypeEngine): - col_def['type'] = coltype() - return col_defs - - @deprecated('0.7', 'Call to deprecated method get_primary_keys.' - ' Use get_pk_constraint instead.') - def get_primary_keys(self, table_name, schema=None, **kw): - """Return information about primary keys in `table_name`. - - Given a string `table_name`, and an optional string `schema`, return - primary key information as a list of column names. - """ - - return self.dialect.get_pk_constraint(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw)['constrained_columns'] - - def get_pk_constraint(self, table_name, schema=None, **kw): - """Return information about primary key constraint on `table_name`. - - Given a string `table_name`, and an optional string `schema`, return - primary key information as a dictionary with these keys: - - constrained_columns - a list of column names that make up the primary key - - name - optional name of the primary key constraint. - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - """ - return self.dialect.get_pk_constraint(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw) - - def get_foreign_keys(self, table_name, schema=None, **kw): - """Return information about foreign_keys in `table_name`. - - Given a string `table_name`, and an optional string `schema`, return - foreign key information as a list of dicts with these keys: - - constrained_columns - a list of column names that make up the foreign key - - referred_schema - the name of the referred schema - - referred_table - the name of the referred table - - referred_columns - a list of column names in the referred table that correspond to - constrained_columns - - name - optional name of the foreign key constraint. - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - """ - - return self.dialect.get_foreign_keys(self.bind, table_name, schema, - info_cache=self.info_cache, - **kw) - - def get_indexes(self, table_name, schema=None, **kw): - """Return information about indexes in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - index information as a list of dicts with these keys: - - name - the index's name - - column_names - list of column names in order - - unique - boolean - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - """ - - return self.dialect.get_indexes(self.bind, table_name, - schema, - info_cache=self.info_cache, **kw) - - def get_unique_constraints(self, table_name, schema=None, **kw): - """Return information about unique constraints in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - unique constraint information as a list of dicts with these keys: - - name - the unique constraint's name - - column_names - list of column names in order - - :param table_name: string name of the table. For special quoting, - use :class:`.quoted_name`. - - :param schema: string schema name; if omitted, uses the default schema - of the database connection. For special quoting, - use :class:`.quoted_name`. - - .. versionadded:: 0.8.4 - - """ - - return self.dialect.get_unique_constraints( - self.bind, table_name, schema, info_cache=self.info_cache, **kw) - - def reflecttable(self, table, include_columns, exclude_columns=()): - """Given a Table object, load its internal constructs based on - introspection. - - This is the underlying method used by most dialects to produce - table reflection. Direct usage is like:: - - from sqlalchemy import create_engine, MetaData, Table - from sqlalchemy.engine import reflection - - engine = create_engine('...') - meta = MetaData() - user_table = Table('user', meta) - insp = Inspector.from_engine(engine) - insp.reflecttable(user_table, None) - - :param table: a :class:`~sqlalchemy.schema.Table` instance. - :param include_columns: a list of string column names to include - in the reflection process. If ``None``, all columns are reflected. - - """ - dialect = self.bind.dialect - - # table attributes we might need. - reflection_options = dict( - (k, table.kwargs.get(k)) - for k in dialect.reflection_options if k in table.kwargs) - - schema = table.schema - table_name = table.name - - # apply table options - tbl_opts = self.get_table_options(table_name, schema, **table.kwargs) - if tbl_opts: - table.kwargs.update(tbl_opts) - - # table.kwargs will need to be passed to each reflection method. Make - # sure keywords are strings. - tblkw = table.kwargs.copy() - for (k, v) in list(tblkw.items()): - del tblkw[k] - tblkw[str(k)] = v - - if util.py2k: - if isinstance(schema, str): - schema = schema.decode(dialect.encoding) - if isinstance(table_name, str): - table_name = table_name.decode(dialect.encoding) - - # columns - found_table = False - cols_by_orig_name = {} - - for col_d in self.get_columns(table_name, schema, **tblkw): - found_table = True - orig_name = col_d['name'] - - table.dispatch.column_reflect(self, table, col_d) - - name = col_d['name'] - if include_columns and name not in include_columns: - continue - if exclude_columns and name in exclude_columns: - continue - - coltype = col_d['type'] - col_kw = { - 'nullable': col_d['nullable'], - } - for k in ('autoincrement', 'quote', 'info', 'key'): - if k in col_d: - col_kw[k] = col_d[k] - - colargs = [] - if col_d.get('default') is not None: - # the "default" value is assumed to be a literal SQL - # expression, so is wrapped in text() so that no quoting - # occurs on re-issuance. - colargs.append( - sa_schema.DefaultClause( - sql.text(col_d['default']), _reflected=True - ) - ) - - if 'sequence' in col_d: - # TODO: mssql and sybase are using this. - seq = col_d['sequence'] - sequence = sa_schema.Sequence(seq['name'], 1, 1) - if 'start' in seq: - sequence.start = seq['start'] - if 'increment' in seq: - sequence.increment = seq['increment'] - colargs.append(sequence) - - cols_by_orig_name[orig_name] = col = \ - sa_schema.Column(name, coltype, *colargs, **col_kw) - - table.append_column(col) - - if not found_table: - raise exc.NoSuchTableError(table.name) - - # Primary keys - pk_cons = self.get_pk_constraint(table_name, schema, **tblkw) - if pk_cons: - pk_cols = [ - cols_by_orig_name[pk] - for pk in pk_cons['constrained_columns'] - if pk in cols_by_orig_name and pk not in exclude_columns - ] - pk_cols += [ - pk - for pk in table.primary_key - if pk.key in exclude_columns - ] - primary_key_constraint = sa_schema.PrimaryKeyConstraint( - name=pk_cons.get('name'), - *pk_cols - ) - - table.append_constraint(primary_key_constraint) - - # Foreign keys - fkeys = self.get_foreign_keys(table_name, schema, **tblkw) - for fkey_d in fkeys: - conname = fkey_d['name'] - # look for columns by orig name in cols_by_orig_name, - # but support columns that are in-Python only as fallback - constrained_columns = [ - cols_by_orig_name[c].key - if c in cols_by_orig_name else c - for c in fkey_d['constrained_columns'] - ] - if exclude_columns and set(constrained_columns).intersection( - exclude_columns): - continue - referred_schema = fkey_d['referred_schema'] - referred_table = fkey_d['referred_table'] - referred_columns = fkey_d['referred_columns'] - refspec = [] - if referred_schema is not None: - sa_schema.Table(referred_table, table.metadata, - autoload=True, schema=referred_schema, - autoload_with=self.bind, - **reflection_options - ) - for column in referred_columns: - refspec.append(".".join( - [referred_schema, referred_table, column])) - else: - sa_schema.Table(referred_table, table.metadata, autoload=True, - autoload_with=self.bind, - **reflection_options - ) - for column in referred_columns: - refspec.append(".".join([referred_table, column])) - if 'options' in fkey_d: - options = fkey_d['options'] - else: - options = {} - table.append_constraint( - sa_schema.ForeignKeyConstraint(constrained_columns, refspec, - conname, link_to_name=True, - **options)) - # Indexes - indexes = self.get_indexes(table_name, schema) - for index_d in indexes: - name = index_d['name'] - columns = index_d['column_names'] - unique = index_d['unique'] - flavor = index_d.get('type', 'unknown type') - if include_columns and \ - not set(columns).issubset(include_columns): - util.warn( - "Omitting %s KEY for (%s), key covers omitted columns." % - (flavor, ', '.join(columns))) - continue - # look for columns by orig name in cols_by_orig_name, - # but support columns that are in-Python only as fallback - sa_schema.Index(name, *[ - cols_by_orig_name[c] if c in cols_by_orig_name - else table.c[c] - for c in columns - ], - **dict(unique=unique)) diff --git a/libs/sqlalchemy/engine/result.py b/libs/sqlalchemy/engine/result.py deleted file mode 100644 index f9e0ca0d..00000000 --- a/libs/sqlalchemy/engine/result.py +++ /dev/null @@ -1,1018 +0,0 @@ -# engine/result.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Define result set constructs including :class:`.ResultProxy` -and :class:`.RowProxy.""" - - - -from .. import exc, util -from ..sql import expression, sqltypes -import collections - -# This reconstructor is necessary so that pickles with the C extension or -# without use the same Binary format. -try: - # We need a different reconstructor on the C extension so that we can - # add extra checks that fields have correctly been initialized by - # __setstate__. - from sqlalchemy.cresultproxy import safe_rowproxy_reconstructor - - # The extra function embedding is needed so that the - # reconstructor function has the same signature whether or not - # the extension is present. - def rowproxy_reconstructor(cls, state): - return safe_rowproxy_reconstructor(cls, state) -except ImportError: - def rowproxy_reconstructor(cls, state): - obj = cls.__new__(cls) - obj.__setstate__(state) - return obj - -try: - from sqlalchemy.cresultproxy import BaseRowProxy -except ImportError: - class BaseRowProxy(object): - __slots__ = ('_parent', '_row', '_processors', '_keymap') - - def __init__(self, parent, row, processors, keymap): - """RowProxy objects are constructed by ResultProxy objects.""" - - self._parent = parent - self._row = row - self._processors = processors - self._keymap = keymap - - def __reduce__(self): - return (rowproxy_reconstructor, - (self.__class__, self.__getstate__())) - - def values(self): - """Return the values represented by this RowProxy as a list.""" - return list(self) - - def __iter__(self): - for processor, value in zip(self._processors, self._row): - if processor is None: - yield value - else: - yield processor(value) - - def __len__(self): - return len(self._row) - - def __getitem__(self, key): - try: - processor, obj, index = self._keymap[key] - except KeyError: - processor, obj, index = self._parent._key_fallback(key) - except TypeError: - if isinstance(key, slice): - l = [] - for processor, value in zip(self._processors[key], - self._row[key]): - if processor is None: - l.append(value) - else: - l.append(processor(value)) - return tuple(l) - else: - raise - if index is None: - raise exc.InvalidRequestError( - "Ambiguous column name '%s' in result set! " - "try 'use_labels' option on select statement." % key) - if processor is not None: - return processor(self._row[index]) - else: - return self._row[index] - - def __getattr__(self, name): - try: - return self[name] - except KeyError as e: - raise AttributeError(e.args[0]) - - -class RowProxy(BaseRowProxy): - """Proxy values from a single cursor row. - - Mostly follows "ordered dictionary" behavior, mapping result - values to the string-based column name, the integer position of - the result in the row, as well as Column instances which can be - mapped to the original Columns that produced this result set (for - results that correspond to constructed SQL expressions). - """ - __slots__ = () - - def __contains__(self, key): - return self._parent._has_key(self._row, key) - - def __getstate__(self): - return { - '_parent': self._parent, - '_row': tuple(self) - } - - def __setstate__(self, state): - self._parent = parent = state['_parent'] - self._row = state['_row'] - self._processors = parent._processors - self._keymap = parent._keymap - - __hash__ = None - - def __lt__(self, other): - return tuple(self) < tuple(other) - - def __eq__(self, other): - return other is self or tuple(other) == tuple(self) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return repr(tuple(self)) - - def has_key(self, key): - """Return True if this RowProxy contains the given key.""" - - return self._parent._has_key(self._row, key) - - def items(self): - """Return a list of tuples, each tuple containing a key/value pair.""" - # TODO: no coverage here - return [(key, self[key]) for key in self.keys()] - - def keys(self): - """Return the list of keys as strings represented by this RowProxy.""" - - return self._parent.keys - - def iterkeys(self): - return iter(self._parent.keys) - - def itervalues(self): - return iter(self) - -try: - # Register RowProxy with Sequence, - # so sequence protocol is implemented - from collections import Sequence - Sequence.register(RowProxy) -except ImportError: - pass - - -class ResultMetaData(object): - """Handle cursor.description, applying additional info from an execution - context.""" - - def __init__(self, parent, metadata): - self._processors = processors = [] - - # We do not strictly need to store the processor in the key mapping, - # though it is faster in the Python version (probably because of the - # saved attribute lookup self._processors) - self._keymap = keymap = {} - self.keys = [] - context = parent.context - dialect = context.dialect - typemap = dialect.dbapi_type_map - translate_colname = context._translate_colname - self.case_sensitive = dialect.case_sensitive - - # high precedence key values. - primary_keymap = {} - - for i, rec in enumerate(metadata): - colname = rec[0] - coltype = rec[1] - - if dialect.description_encoding: - colname = dialect._description_decoder(colname) - - if translate_colname: - colname, untranslated = translate_colname(colname) - - if dialect.requires_name_normalize: - colname = dialect.normalize_name(colname) - - if context.result_map: - try: - name, obj, type_ = context.result_map[colname - if self.case_sensitive - else colname.lower()] - except KeyError: - name, obj, type_ = \ - colname, None, typemap.get(coltype, sqltypes.NULLTYPE) - else: - name, obj, type_ = \ - colname, None, typemap.get(coltype, sqltypes.NULLTYPE) - - processor = context.get_result_processor(type_, colname, coltype) - - processors.append(processor) - rec = (processor, obj, i) - - # indexes as keys. This is only needed for the Python version of - # RowProxy (the C version uses a faster path for integer indexes). - primary_keymap[i] = rec - - # populate primary keymap, looking for conflicts. - if primary_keymap.setdefault( - name if self.case_sensitive - else name.lower(), - rec) is not rec: - # place a record that doesn't have the "index" - this - # is interpreted later as an AmbiguousColumnError, - # but only when actually accessed. Columns - # colliding by name is not a problem if those names - # aren't used; integer access is always - # unambiguous. - primary_keymap[name - if self.case_sensitive - else name.lower()] = rec = (None, obj, None) - - self.keys.append(colname) - if obj: - for o in obj: - keymap[o] = rec - # technically we should be doing this but we - # are saving on callcounts by not doing so. - # if keymap.setdefault(o, rec) is not rec: - # keymap[o] = (None, obj, None) - - if translate_colname and \ - untranslated: - keymap[untranslated] = rec - - # overwrite keymap values with those of the - # high precedence keymap. - keymap.update(primary_keymap) - - if parent._echo: - context.engine.logger.debug( - "Col %r", tuple(x[0] for x in metadata)) - - @util.pending_deprecation("0.8", "sqlite dialect uses " - "_translate_colname() now") - def _set_keymap_synonym(self, name, origname): - """Set a synonym for the given name. - - Some dialects (SQLite at the moment) may use this to - adjust the column names that are significant within a - row. - - """ - rec = (processor, obj, i) = self._keymap[origname if - self.case_sensitive - else origname.lower()] - if self._keymap.setdefault(name, rec) is not rec: - self._keymap[name] = (processor, obj, None) - - def _key_fallback(self, key, raiseerr=True): - map = self._keymap - result = None - if isinstance(key, util.string_types): - result = map.get(key if self.case_sensitive else key.lower()) - # fallback for targeting a ColumnElement to a textual expression - # this is a rare use case which only occurs when matching text() - # or colummn('name') constructs to ColumnElements, or after a - # pickle/unpickle roundtrip - elif isinstance(key, expression.ColumnElement): - if key._label and ( - key._label - if self.case_sensitive - else key._label.lower()) in map: - result = map[key._label - if self.case_sensitive - else key._label.lower()] - elif hasattr(key, 'name') and ( - key.name - if self.case_sensitive - else key.name.lower()) in map: - # match is only on name. - result = map[key.name - if self.case_sensitive - else key.name.lower()] - # search extra hard to make sure this - # isn't a column/label name overlap. - # this check isn't currently available if the row - # was unpickled. - if result is not None and \ - result[1] is not None: - for obj in result[1]: - if key._compare_name_for_result(obj): - break - else: - result = None - if result is None: - if raiseerr: - raise exc.NoSuchColumnError( - "Could not locate column in row for column '%s'" % - expression._string_or_unprintable(key)) - else: - return None - else: - map[key] = result - return result - - def _has_key(self, row, key): - if key in self._keymap: - return True - else: - return self._key_fallback(key, False) is not None - - def __getstate__(self): - return { - '_pickled_keymap': dict( - (key, index) - for key, (processor, obj, index) in self._keymap.items() - if isinstance(key, util.string_types + util.int_types) - ), - 'keys': self.keys, - "case_sensitive": self.case_sensitive, - } - - def __setstate__(self, state): - # the row has been processed at pickling time so we don't need any - # processor anymore - self._processors = [None for _ in range(len(state['keys']))] - self._keymap = keymap = {} - for key, index in state['_pickled_keymap'].items(): - # not preserving "obj" here, unfortunately our - # proxy comparison fails with the unpickle - keymap[key] = (None, None, index) - self.keys = state['keys'] - self.case_sensitive = state['case_sensitive'] - self._echo = False - - -class ResultProxy(object): - """Wraps a DB-API cursor object to provide easier access to row columns. - - Individual columns may be accessed by their integer position, - case-insensitive column name, or by ``schema.Column`` - object. e.g.:: - - row = fetchone() - - col1 = row[0] # access via integer position - - col2 = row['col2'] # access via name - - col3 = row[mytable.c.mycol] # access via Column object. - - ``ResultProxy`` also handles post-processing of result column - data using ``TypeEngine`` objects, which are referenced from - the originating SQL statement that produced this result set. - - """ - - _process_row = RowProxy - out_parameters = None - _can_close_connection = False - _metadata = None - - def __init__(self, context): - self.context = context - self.dialect = context.dialect - self.closed = False - self.cursor = self._saved_cursor = context.cursor - self.connection = context.root_connection - self._echo = self.connection._echo and \ - context.engine._should_log_debug() - self._init_metadata() - - def _init_metadata(self): - metadata = self._cursor_description() - if metadata is not None: - self._metadata = ResultMetaData(self, metadata) - - def keys(self): - """Return the current set of string keys for rows.""" - if self._metadata: - return self._metadata.keys - else: - return [] - - @util.memoized_property - def rowcount(self): - """Return the 'rowcount' for this result. - - The 'rowcount' reports the number of rows *matched* - by the WHERE criterion of an UPDATE or DELETE statement. - - .. note:: - - Notes regarding :attr:`.ResultProxy.rowcount`: - - - * This attribute returns the number of rows *matched*, - which is not necessarily the same as the number of rows - that were actually *modified* - an UPDATE statement, for example, - may have no net change on a given row if the SET values - given are the same as those present in the row already. - Such a row would be matched but not modified. - On backends that feature both styles, such as MySQL, - rowcount is configured by default to return the match - count in all cases. - - * :attr:`.ResultProxy.rowcount` is *only* useful in conjunction - with an UPDATE or DELETE statement. Contrary to what the Python - DBAPI says, it does *not* return the - number of rows available from the results of a SELECT statement - as DBAPIs cannot support this functionality when rows are - unbuffered. - - * :attr:`.ResultProxy.rowcount` may not be fully implemented by - all dialects. In particular, most DBAPIs do not support an - aggregate rowcount result from an executemany call. - The :meth:`.ResultProxy.supports_sane_rowcount` and - :meth:`.ResultProxy.supports_sane_multi_rowcount` methods - will report from the dialect if each usage is known to be - supported. - - * Statements that use RETURNING may not return a correct - rowcount. - - """ - try: - return self.context.rowcount - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, self.cursor, self.context) - - @property - def lastrowid(self): - """return the 'lastrowid' accessor on the DBAPI cursor. - - This is a DBAPI specific method and is only functional - for those backends which support it, for statements - where it is appropriate. It's behavior is not - consistent across backends. - - Usage of this method is normally unnecessary when - using insert() expression constructs; the - :attr:`~ResultProxy.inserted_primary_key` attribute provides a - tuple of primary key values for a newly inserted row, - regardless of database backend. - - """ - try: - return self._saved_cursor.lastrowid - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, - self._saved_cursor, self.context) - - @property - def returns_rows(self): - """True if this :class:`.ResultProxy` returns rows. - - I.e. if it is legal to call the methods - :meth:`~.ResultProxy.fetchone`, - :meth:`~.ResultProxy.fetchmany` - :meth:`~.ResultProxy.fetchall`. - - """ - return self._metadata is not None - - @property - def is_insert(self): - """True if this :class:`.ResultProxy` is the result - of a executing an expression language compiled - :func:`.expression.insert` construct. - - When True, this implies that the - :attr:`inserted_primary_key` attribute is accessible, - assuming the statement did not include - a user defined "returning" construct. - - """ - return self.context.isinsert - - def _cursor_description(self): - """May be overridden by subclasses.""" - - return self._saved_cursor.description - - def close(self, _autoclose_connection=True): - """Close this ResultProxy. - - Closes the underlying DBAPI cursor corresponding to the execution. - - Note that any data cached within this ResultProxy is still available. - For some types of results, this may include buffered rows. - - If this ResultProxy was generated from an implicit execution, - the underlying Connection will also be closed (returns the - underlying DBAPI connection to the connection pool.) - - This method is called automatically when: - - * all result rows are exhausted using the fetchXXX() methods. - * cursor.description is None. - - """ - - if not self.closed: - self.closed = True - self.connection._safe_close_cursor(self.cursor) - if _autoclose_connection and \ - self.connection.should_close_with_result: - self.connection.close() - # allow consistent errors - self.cursor = None - - def __iter__(self): - while True: - row = self.fetchone() - if row is None: - raise StopIteration - else: - yield row - - @util.memoized_property - def inserted_primary_key(self): - """Return the primary key for the row just inserted. - - The return value is a list of scalar values - corresponding to the list of primary key columns - in the target table. - - This only applies to single row :func:`.insert` - constructs which did not explicitly specify - :meth:`.Insert.returning`. - - Note that primary key columns which specify a - server_default clause, - or otherwise do not qualify as "autoincrement" - columns (see the notes at :class:`.Column`), and were - generated using the database-side default, will - appear in this list as ``None`` unless the backend - supports "returning" and the insert statement executed - with the "implicit returning" enabled. - - Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed - statement is not a compiled expression construct - or is not an insert() construct. - - """ - - if not self.context.compiled: - raise exc.InvalidRequestError( - "Statement is not a compiled " - "expression construct.") - elif not self.context.isinsert: - raise exc.InvalidRequestError( - "Statement is not an insert() " - "expression construct.") - elif self.context._is_explicit_returning: - raise exc.InvalidRequestError( - "Can't call inserted_primary_key " - "when returning() " - "is used.") - - return self.context.inserted_primary_key - - def last_updated_params(self): - """Return the collection of updated parameters from this - execution. - - Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed - statement is not a compiled expression construct - or is not an update() construct. - - """ - if not self.context.compiled: - raise exc.InvalidRequestError( - "Statement is not a compiled " - "expression construct.") - elif not self.context.isupdate: - raise exc.InvalidRequestError( - "Statement is not an update() " - "expression construct.") - elif self.context.executemany: - return self.context.compiled_parameters - else: - return self.context.compiled_parameters[0] - - def last_inserted_params(self): - """Return the collection of inserted parameters from this - execution. - - Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed - statement is not a compiled expression construct - or is not an insert() construct. - - """ - if not self.context.compiled: - raise exc.InvalidRequestError( - "Statement is not a compiled " - "expression construct.") - elif not self.context.isinsert: - raise exc.InvalidRequestError( - "Statement is not an insert() " - "expression construct.") - elif self.context.executemany: - return self.context.compiled_parameters - else: - return self.context.compiled_parameters[0] - - @property - def returned_defaults(self): - """Return the values of default columns that were fetched using - the :meth:`.ValuesBase.return_defaults` feature. - - The value is an instance of :class:`.RowProxy`, or ``None`` - if :meth:`.ValuesBase.return_defaults` was not used or if the - backend does not support RETURNING. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.ValuesBase.return_defaults` - - """ - return self.context.returned_defaults - - def lastrow_has_defaults(self): - """Return ``lastrow_has_defaults()`` from the underlying - :class:`.ExecutionContext`. - - See :class:`.ExecutionContext` for details. - - """ - - return self.context.lastrow_has_defaults() - - def postfetch_cols(self): - """Return ``postfetch_cols()`` from the underlying - :class:`.ExecutionContext`. - - See :class:`.ExecutionContext` for details. - - Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed - statement is not a compiled expression construct - or is not an insert() or update() construct. - - """ - - if not self.context.compiled: - raise exc.InvalidRequestError( - "Statement is not a compiled " - "expression construct.") - elif not self.context.isinsert and not self.context.isupdate: - raise exc.InvalidRequestError( - "Statement is not an insert() or update() " - "expression construct.") - return self.context.postfetch_cols - - def prefetch_cols(self): - """Return ``prefetch_cols()`` from the underlying - :class:`.ExecutionContext`. - - See :class:`.ExecutionContext` for details. - - Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed - statement is not a compiled expression construct - or is not an insert() or update() construct. - - """ - - if not self.context.compiled: - raise exc.InvalidRequestError( - "Statement is not a compiled " - "expression construct.") - elif not self.context.isinsert and not self.context.isupdate: - raise exc.InvalidRequestError( - "Statement is not an insert() or update() " - "expression construct.") - return self.context.prefetch_cols - - def supports_sane_rowcount(self): - """Return ``supports_sane_rowcount`` from the dialect. - - See :attr:`.ResultProxy.rowcount` for background. - - """ - - return self.dialect.supports_sane_rowcount - - def supports_sane_multi_rowcount(self): - """Return ``supports_sane_multi_rowcount`` from the dialect. - - See :attr:`.ResultProxy.rowcount` for background. - - """ - - return self.dialect.supports_sane_multi_rowcount - - def _fetchone_impl(self): - try: - return self.cursor.fetchone() - except AttributeError: - self._non_result() - - def _fetchmany_impl(self, size=None): - try: - if size is None: - return self.cursor.fetchmany() - else: - return self.cursor.fetchmany(size) - except AttributeError: - self._non_result() - - def _fetchall_impl(self): - try: - return self.cursor.fetchall() - except AttributeError: - self._non_result() - - def _non_result(self): - if self._metadata is None: - raise exc.ResourceClosedError( - "This result object does not return rows. " - "It has been closed automatically.", - ) - else: - raise exc.ResourceClosedError("This result object is closed.") - - def process_rows(self, rows): - process_row = self._process_row - metadata = self._metadata - keymap = metadata._keymap - processors = metadata._processors - if self._echo: - log = self.context.engine.logger.debug - l = [] - for row in rows: - log("Row %r", row) - l.append(process_row(metadata, row, processors, keymap)) - return l - else: - return [process_row(metadata, row, processors, keymap) - for row in rows] - - def fetchall(self): - """Fetch all rows, just like DB-API ``cursor.fetchall()``.""" - - try: - l = self.process_rows(self._fetchall_impl()) - self.close() - return l - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - - def fetchmany(self, size=None): - """Fetch many rows, just like DB-API - ``cursor.fetchmany(size=cursor.arraysize)``. - - If rows are present, the cursor remains open after this is called. - Else the cursor is automatically closed and an empty list is returned. - - """ - - try: - l = self.process_rows(self._fetchmany_impl(size)) - if len(l) == 0: - self.close() - return l - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - - def fetchone(self): - """Fetch one row, just like DB-API ``cursor.fetchone()``. - - If a row is present, the cursor remains open after this is called. - Else the cursor is automatically closed and None is returned. - - """ - try: - row = self._fetchone_impl() - if row is not None: - return self.process_rows([row])[0] - else: - self.close() - return None - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - - def first(self): - """Fetch the first row and then close the result set unconditionally. - - Returns None if no row is present. - - """ - if self._metadata is None: - self._non_result() - - try: - row = self._fetchone_impl() - except Exception as e: - self.connection._handle_dbapi_exception( - e, None, None, - self.cursor, self.context) - - try: - if row is not None: - return self.process_rows([row])[0] - else: - return None - finally: - self.close() - - def scalar(self): - """Fetch the first column of the first row, and close the result set. - - Returns None if no row is present. - - """ - row = self.first() - if row is not None: - return row[0] - else: - return None - - -class BufferedRowResultProxy(ResultProxy): - """A ResultProxy with row buffering behavior. - - ``ResultProxy`` that buffers the contents of a selection of rows - before ``fetchone()`` is called. This is to allow the results of - ``cursor.description`` to be available immediately, when - interfacing with a DB-API that requires rows to be consumed before - this information is available (currently psycopg2, when used with - server-side cursors). - - The pre-fetching behavior fetches only one row initially, and then - grows its buffer size by a fixed amount with each successive need - for additional rows up to a size of 100. - """ - - def _init_metadata(self): - self.__buffer_rows() - super(BufferedRowResultProxy, self)._init_metadata() - - # this is a "growth chart" for the buffering of rows. - # each successive __buffer_rows call will use the next - # value in the list for the buffer size until the max - # is reached - size_growth = { - 1: 5, - 5: 10, - 10: 20, - 20: 50, - 50: 100, - 100: 250, - 250: 500, - 500: 1000 - } - - def __buffer_rows(self): - size = getattr(self, '_bufsize', 1) - self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) - self._bufsize = self.size_growth.get(size, size) - - def _fetchone_impl(self): - if self.closed: - return None - if not self.__rowbuffer: - self.__buffer_rows() - if not self.__rowbuffer: - return None - return self.__rowbuffer.popleft() - - def _fetchmany_impl(self, size=None): - if size is None: - return self._fetchall_impl() - result = [] - for x in range(0, size): - row = self._fetchone_impl() - if row is None: - break - result.append(row) - return result - - def _fetchall_impl(self): - self.__rowbuffer.extend(self.cursor.fetchall()) - ret = self.__rowbuffer - self.__rowbuffer = collections.deque() - return ret - - -class FullyBufferedResultProxy(ResultProxy): - """A result proxy that buffers rows fully upon creation. - - Used for operations where a result is to be delivered - after the database conversation can not be continued, - such as MSSQL INSERT...OUTPUT after an autocommit. - - """ - def _init_metadata(self): - super(FullyBufferedResultProxy, self)._init_metadata() - self.__rowbuffer = self._buffer_rows() - - def _buffer_rows(self): - return collections.deque(self.cursor.fetchall()) - - def _fetchone_impl(self): - if self.__rowbuffer: - return self.__rowbuffer.popleft() - else: - return None - - def _fetchmany_impl(self, size=None): - if size is None: - return self._fetchall_impl() - result = [] - for x in range(0, size): - row = self._fetchone_impl() - if row is None: - break - result.append(row) - return result - - def _fetchall_impl(self): - ret = self.__rowbuffer - self.__rowbuffer = collections.deque() - return ret - - -class BufferedColumnRow(RowProxy): - def __init__(self, parent, row, processors, keymap): - # preprocess row - row = list(row) - # this is a tad faster than using enumerate - index = 0 - for processor in parent._orig_processors: - if processor is not None: - row[index] = processor(row[index]) - index += 1 - row = tuple(row) - super(BufferedColumnRow, self).__init__(parent, row, - processors, keymap) - - -class BufferedColumnResultProxy(ResultProxy): - """A ResultProxy with column buffering behavior. - - ``ResultProxy`` that loads all columns into memory each time - fetchone() is called. If fetchmany() or fetchall() are called, - the full grid of results is fetched. This is to operate with - databases where result rows contain "live" results that fall out - of scope unless explicitly fetched. Currently this includes - cx_Oracle LOB objects. - - """ - - _process_row = BufferedColumnRow - - def _init_metadata(self): - super(BufferedColumnResultProxy, self)._init_metadata() - metadata = self._metadata - # orig_processors will be used to preprocess each row when they are - # constructed. - metadata._orig_processors = metadata._processors - # replace the all type processors by None processors. - metadata._processors = [None for _ in range(len(metadata.keys))] - keymap = {} - for k, (func, obj, index) in metadata._keymap.items(): - keymap[k] = (None, obj, index) - self._metadata._keymap = keymap - - def fetchall(self): - # can't call cursor.fetchall(), since rows must be - # fully processed before requesting more from the DBAPI. - l = [] - while True: - row = self.fetchone() - if row is None: - break - l.append(row) - return l - - def fetchmany(self, size=None): - # can't call cursor.fetchmany(), since rows must be - # fully processed before requesting more from the DBAPI. - if size is None: - return self.fetchall() - l = [] - for i in range(size): - row = self.fetchone() - if row is None: - break - l.append(row) - return l diff --git a/libs/sqlalchemy/engine/strategies.py b/libs/sqlalchemy/engine/strategies.py deleted file mode 100644 index f6c06403..00000000 --- a/libs/sqlalchemy/engine/strategies.py +++ /dev/null @@ -1,259 +0,0 @@ -# engine/strategies.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Strategies for creating new instances of Engine types. - -These are semi-private implementation classes which provide the -underlying behavior for the "strategy" keyword argument available on -:func:`~sqlalchemy.engine.create_engine`. Current available options are -``plain``, ``threadlocal``, and ``mock``. - -New strategies can be added via new ``EngineStrategy`` classes. -""" - -from operator import attrgetter - -from sqlalchemy.engine import base, threadlocal, url -from sqlalchemy import util, exc, event -from sqlalchemy import pool as poollib - -strategies = {} - - -class EngineStrategy(object): - """An adaptor that processes input arguments and produces an Engine. - - Provides a ``create`` method that receives input arguments and - produces an instance of base.Engine or a subclass. - - """ - - def __init__(self): - strategies[self.name] = self - - def create(self, *args, **kwargs): - """Given arguments, returns a new Engine instance.""" - - raise NotImplementedError() - - -class DefaultEngineStrategy(EngineStrategy): - """Base class for built-in strategies.""" - - def create(self, name_or_url, **kwargs): - # create url.URL object - u = url.make_url(name_or_url) - - dialect_cls = u.get_dialect() - - if kwargs.pop('_coerce_config', False): - def pop_kwarg(key, default=None): - value = kwargs.pop(key, default) - if key in dialect_cls.engine_config_types: - value = dialect_cls.engine_config_types[key](value) - return value - else: - pop_kwarg = kwargs.pop - - dialect_args = {} - # consume dialect arguments from kwargs - for k in util.get_cls_kwargs(dialect_cls): - if k in kwargs: - dialect_args[k] = pop_kwarg(k) - - dbapi = kwargs.pop('module', None) - if dbapi is None: - dbapi_args = {} - for k in util.get_func_kwargs(dialect_cls.dbapi): - if k in kwargs: - dbapi_args[k] = pop_kwarg(k) - dbapi = dialect_cls.dbapi(**dbapi_args) - - dialect_args['dbapi'] = dbapi - - # create dialect - dialect = dialect_cls(**dialect_args) - - # assemble connection arguments - (cargs, cparams) = dialect.create_connect_args(u) - cparams.update(pop_kwarg('connect_args', {})) - - # look for existing pool or create - pool = pop_kwarg('pool', None) - if pool is None: - def connect(): - try: - return dialect.connect(*cargs, **cparams) - except dialect.dbapi.Error as e: - invalidated = dialect.is_disconnect(e, None, None) - util.raise_from_cause( - exc.DBAPIError.instance(None, None, - e, dialect.dbapi.Error, - connection_invalidated=invalidated - ) - ) - - creator = pop_kwarg('creator', connect) - - poolclass = pop_kwarg('poolclass', None) - if poolclass is None: - poolclass = dialect_cls.get_pool_class(u) - pool_args = {} - - # consume pool arguments from kwargs, translating a few of - # the arguments - translate = {'logging_name': 'pool_logging_name', - 'echo': 'echo_pool', - 'timeout': 'pool_timeout', - 'recycle': 'pool_recycle', - 'events': 'pool_events', - 'use_threadlocal': 'pool_threadlocal', - 'reset_on_return': 'pool_reset_on_return'} - for k in util.get_cls_kwargs(poolclass): - tk = translate.get(k, k) - if tk in kwargs: - pool_args[k] = pop_kwarg(tk) - pool = poolclass(creator, **pool_args) - else: - if isinstance(pool, poollib._DBProxy): - pool = pool.get_pool(*cargs, **cparams) - else: - pool = pool - - # create engine. - engineclass = self.engine_cls - engine_args = {} - for k in util.get_cls_kwargs(engineclass): - if k in kwargs: - engine_args[k] = pop_kwarg(k) - - _initialize = kwargs.pop('_initialize', True) - - # all kwargs should be consumed - if kwargs: - raise TypeError( - "Invalid argument(s) %s sent to create_engine(), " - "using configuration %s/%s/%s. Please check that the " - "keyword arguments are appropriate for this combination " - "of components." % (','.join("'%s'" % k for k in kwargs), - dialect.__class__.__name__, - pool.__class__.__name__, - engineclass.__name__)) - - engine = engineclass(pool, dialect, u, **engine_args) - - if _initialize: - do_on_connect = dialect.on_connect() - if do_on_connect: - def on_connect(dbapi_connection, connection_record): - conn = getattr( - dbapi_connection, '_sqla_unwrap', dbapi_connection) - if conn is None: - return - do_on_connect(conn) - - event.listen(pool, 'first_connect', on_connect) - event.listen(pool, 'connect', on_connect) - - @util.only_once - def first_connect(dbapi_connection, connection_record): - c = base.Connection(engine, connection=dbapi_connection, - _has_events=False) - - dialect.initialize(c) - event.listen(pool, 'first_connect', first_connect) - - return engine - - -class PlainEngineStrategy(DefaultEngineStrategy): - """Strategy for configuring a regular Engine.""" - - name = 'plain' - engine_cls = base.Engine - -PlainEngineStrategy() - - -class ThreadLocalEngineStrategy(DefaultEngineStrategy): - """Strategy for configuring an Engine with threadlocal behavior.""" - - name = 'threadlocal' - engine_cls = threadlocal.TLEngine - -ThreadLocalEngineStrategy() - - -class MockEngineStrategy(EngineStrategy): - """Strategy for configuring an Engine-like object with mocked execution. - - Produces a single mock Connectable object which dispatches - statement execution to a passed-in function. - - """ - - name = 'mock' - - def create(self, name_or_url, executor, **kwargs): - # create url.URL object - u = url.make_url(name_or_url) - - dialect_cls = u.get_dialect() - - dialect_args = {} - # consume dialect arguments from kwargs - for k in util.get_cls_kwargs(dialect_cls): - if k in kwargs: - dialect_args[k] = kwargs.pop(k) - - # create dialect - dialect = dialect_cls(**dialect_args) - - return MockEngineStrategy.MockConnection(dialect, executor) - - class MockConnection(base.Connectable): - def __init__(self, dialect, execute): - self._dialect = dialect - self.execute = execute - - engine = property(lambda s: s) - dialect = property(attrgetter('_dialect')) - name = property(lambda s: s._dialect.name) - - def contextual_connect(self, **kwargs): - return self - - def execution_options(self, **kw): - return self - - def compiler(self, statement, parameters, **kwargs): - return self._dialect.compiler( - statement, parameters, engine=self, **kwargs) - - def create(self, entity, **kwargs): - kwargs['checkfirst'] = False - from sqlalchemy.engine import ddl - - ddl.SchemaGenerator( - self.dialect, self, **kwargs).traverse_single(entity) - - def drop(self, entity, **kwargs): - kwargs['checkfirst'] = False - from sqlalchemy.engine import ddl - ddl.SchemaDropper( - self.dialect, self, **kwargs).traverse_single(entity) - - def _run_visitor(self, visitorcallable, element, - connection=None, - **kwargs): - kwargs['checkfirst'] = False - visitorcallable(self.dialect, self, - **kwargs).traverse_single(element) - - def execute(self, object, *multiparams, **params): - raise NotImplementedError() - -MockEngineStrategy() diff --git a/libs/sqlalchemy/engine/threadlocal.py b/libs/sqlalchemy/engine/threadlocal.py deleted file mode 100644 index ae647a78..00000000 --- a/libs/sqlalchemy/engine/threadlocal.py +++ /dev/null @@ -1,134 +0,0 @@ -# engine/threadlocal.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provides a thread-local transactional wrapper around the root Engine class. - -The ``threadlocal`` module is invoked when using the -``strategy="threadlocal"`` flag with :func:`~sqlalchemy.engine.create_engine`. -This module is semi-private and is invoked automatically when the threadlocal -engine strategy is used. -""" - -from .. import util -from . import base -import weakref - - -class TLConnection(base.Connection): - - def __init__(self, *arg, **kw): - super(TLConnection, self).__init__(*arg, **kw) - self.__opencount = 0 - - def _increment_connect(self): - self.__opencount += 1 - return self - - def close(self): - if self.__opencount == 1: - base.Connection.close(self) - self.__opencount -= 1 - - def _force_close(self): - self.__opencount = 0 - base.Connection.close(self) - - -class TLEngine(base.Engine): - """An Engine that includes support for thread-local managed - transactions. - - """ - _tl_connection_cls = TLConnection - - def __init__(self, *args, **kwargs): - super(TLEngine, self).__init__(*args, **kwargs) - self._connections = util.threading.local() - - def contextual_connect(self, **kw): - if not hasattr(self._connections, 'conn'): - connection = None - else: - connection = self._connections.conn() - - if connection is None or connection.closed: - # guards against pool-level reapers, if desired. - # or not connection.connection.is_valid: - connection = self._tl_connection_cls( - self, self.pool.connect(), **kw) - self._connections.conn = weakref.ref(connection) - - return connection._increment_connect() - - def begin_twophase(self, xid=None): - if not hasattr(self._connections, 'trans'): - self._connections.trans = [] - self._connections.trans.append( - self.contextual_connect().begin_twophase(xid=xid)) - return self - - def begin_nested(self): - if not hasattr(self._connections, 'trans'): - self._connections.trans = [] - self._connections.trans.append( - self.contextual_connect().begin_nested()) - return self - - def begin(self): - if not hasattr(self._connections, 'trans'): - self._connections.trans = [] - self._connections.trans.append(self.contextual_connect().begin()) - return self - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - if type is None: - self.commit() - else: - self.rollback() - - def prepare(self): - if not hasattr(self._connections, 'trans') or \ - not self._connections.trans: - return - self._connections.trans[-1].prepare() - - def commit(self): - if not hasattr(self._connections, 'trans') or \ - not self._connections.trans: - return - trans = self._connections.trans.pop(-1) - trans.commit() - - def rollback(self): - if not hasattr(self._connections, 'trans') or \ - not self._connections.trans: - return - trans = self._connections.trans.pop(-1) - trans.rollback() - - def dispose(self): - self._connections = util.threading.local() - super(TLEngine, self).dispose() - - @property - def closed(self): - return not hasattr(self._connections, 'conn') or \ - self._connections.conn() is None or \ - self._connections.conn().closed - - def close(self): - if not self.closed: - self.contextual_connect().close() - connection = self._connections.conn() - connection._force_close() - del self._connections.conn - self._connections.trans = [] - - def __repr__(self): - return 'TLEngine(%s)' % str(self.url) diff --git a/libs/sqlalchemy/engine/url.py b/libs/sqlalchemy/engine/url.py deleted file mode 100644 index 78ac0618..00000000 --- a/libs/sqlalchemy/engine/url.py +++ /dev/null @@ -1,227 +0,0 @@ -# engine/url.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provides the :class:`~sqlalchemy.engine.url.URL` class which encapsulates -information about a database connection specification. - -The URL object is created automatically when -:func:`~sqlalchemy.engine.create_engine` is called with a string -argument; alternatively, the URL is a public-facing construct which can -be used directly and is also accepted directly by ``create_engine()``. -""" - -import re -from .. import exc, util -from . import Dialect -from ..dialects import registry - - -class URL(object): - """ - Represent the components of a URL used to connect to a database. - - This object is suitable to be passed directly to a - :func:`~sqlalchemy.create_engine` call. The fields of the URL are parsed from a - string by the :func:`.make_url` function. the string - format of the URL is an RFC-1738-style string. - - All initialization parameters are available as public attributes. - - :param drivername: the name of the database backend. - This name will correspond to a module in sqlalchemy/databases - or a third party plug-in. - - :param username: The user name. - - :param password: database password. - - :param host: The name of the host. - - :param port: The port number. - - :param database: The database name. - - :param query: A dictionary of options to be passed to the - dialect and/or the DBAPI upon connect. - - """ - - def __init__(self, drivername, username=None, password=None, - host=None, port=None, database=None, query=None): - self.drivername = drivername - self.username = username - self.password = password - self.host = host - if port is not None: - self.port = int(port) - else: - self.port = None - self.database = database - self.query = query or {} - - def __to_string__(self, hide_password=True): - s = self.drivername + "://" - if self.username is not None: - s += _rfc_1738_quote(self.username) - if self.password is not None: - s += ':' + ('***' if hide_password - else _rfc_1738_quote(self.password)) - s += "@" - if self.host is not None: - if ':' in self.host: - s += "[%s]" % self.host - else: - s += self.host - if self.port is not None: - s += ':' + str(self.port) - if self.database is not None: - s += '/' + self.database - if self.query: - keys = list(self.query) - keys.sort() - s += '?' + "&".join("%s=%s" % (k, self.query[k]) for k in keys) - return s - - def __str__(self): - return self.__to_string__(hide_password=False) - - def __repr__(self): - return self.__to_string__() - - def __hash__(self): - return hash(str(self)) - - def __eq__(self, other): - return \ - isinstance(other, URL) and \ - self.drivername == other.drivername and \ - self.username == other.username and \ - self.password == other.password and \ - self.host == other.host and \ - self.database == other.database and \ - self.query == other.query - - def get_dialect(self): - """Return the SQLAlchemy database dialect class corresponding - to this URL's driver name. - """ - - if '+' not in self.drivername: - name = self.drivername - else: - name = self.drivername.replace('+', '.') - cls = registry.load(name) - # check for legacy dialects that - # would return a module with 'dialect' as the - # actual class - if hasattr(cls, 'dialect') and \ - isinstance(cls.dialect, type) and \ - issubclass(cls.dialect, Dialect): - return cls.dialect - else: - return cls - - def translate_connect_args(self, names=[], **kw): - """Translate url attributes into a dictionary of connection arguments. - - Returns attributes of this url (`host`, `database`, `username`, - `password`, `port`) as a plain dictionary. The attribute names are - used as the keys by default. Unset or false attributes are omitted - from the final dictionary. - - :param \**kw: Optional, alternate key names for url attributes. - - :param names: Deprecated. Same purpose as the keyword-based alternate - names, but correlates the name to the original positionally. - """ - - translated = {} - attribute_names = ['host', 'database', 'username', 'password', 'port'] - for sname in attribute_names: - if names: - name = names.pop(0) - elif sname in kw: - name = kw[sname] - else: - name = sname - if name is not None and getattr(self, sname, False): - translated[name] = getattr(self, sname) - return translated - - -def make_url(name_or_url): - """Given a string or unicode instance, produce a new URL instance. - - The given string is parsed according to the RFC 1738 spec. If an - existing URL object is passed, just returns the object. - """ - - if isinstance(name_or_url, util.string_types): - return _parse_rfc1738_args(name_or_url) - else: - return name_or_url - - -def _parse_rfc1738_args(name): - pattern = re.compile(r''' - (?P[\w\+]+):// - (?: - (?P[^:/]*) - (?::(?P.*))? - @)? - (?: - (?: - \[(?P[^/]+)\] | - (?P[^/:]+) - )? - (?::(?P[^/]*))? - )? - (?:/(?P.*))? - ''', re.X) - - m = pattern.match(name) - if m is not None: - components = m.groupdict() - if components['database'] is not None: - tokens = components['database'].split('?', 2) - components['database'] = tokens[0] - query = (len(tokens) > 1 and dict(util.parse_qsl(tokens[1]))) or None - if util.py2k and query is not None: - query = dict((k.encode('ascii'), query[k]) for k in query) - else: - query = None - components['query'] = query - - if components['username'] is not None: - components['username'] = _rfc_1738_unquote(components['username']) - - if components['password'] is not None: - components['password'] = _rfc_1738_unquote(components['password']) - - ipv4host = components.pop('ipv4host') - ipv6host = components.pop('ipv6host') - components['host'] = ipv4host or ipv6host - name = components.pop('name') - return URL(name, **components) - else: - raise exc.ArgumentError( - "Could not parse rfc1738 URL from string '%s'" % name) - - -def _rfc_1738_quote(text): - return re.sub(r'[:@/]', lambda m: "%%%X" % ord(m.group(0)), text) - -def _rfc_1738_unquote(text): - return util.unquote(text) - -def _parse_keyvalue_args(name): - m = re.match(r'(\w+)://(.*)', name) - if m is not None: - (name, args) = m.group(1, 2) - opts = dict(util.parse_qsl(args)) - return URL(name, *opts) - else: - return None diff --git a/libs/sqlalchemy/engine/util.py b/libs/sqlalchemy/engine/util.py deleted file mode 100644 index 6c0644be..00000000 --- a/libs/sqlalchemy/engine/util.py +++ /dev/null @@ -1,72 +0,0 @@ -# engine/util.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .. import util - -def connection_memoize(key): - """Decorator, memoize a function in a connection.info stash. - - Only applicable to functions which take no arguments other than a - connection. The memo will be stored in ``connection.info[key]``. - """ - - @util.decorator - def decorated(fn, self, connection): - connection = connection.connect() - try: - return connection.info[key] - except KeyError: - connection.info[key] = val = fn(self, connection) - return val - - return decorated - - -def py_fallback(): - def _distill_params(multiparams, params): - """Given arguments from the calling form *multiparams, **params, - return a list of bind parameter structures, usually a list of - dictionaries. - - In the case of 'raw' execution which accepts positional parameters, - it may be a list of tuples or lists. - - """ - - if not multiparams: - if params: - return [params] - else: - return [] - elif len(multiparams) == 1: - zero = multiparams[0] - if isinstance(zero, (list, tuple)): - if not zero or hasattr(zero[0], '__iter__') and \ - not hasattr(zero[0], 'strip'): - # execute(stmt, [{}, {}, {}, ...]) - # execute(stmt, [(), (), (), ...]) - return zero - else: - # execute(stmt, ("value", "value")) - return [zero] - elif hasattr(zero, 'keys'): - # execute(stmt, {"key":"value"}) - return [zero] - else: - # execute(stmt, "value") - return [[zero]] - else: - if hasattr(multiparams[0], '__iter__') and \ - not hasattr(multiparams[0], 'strip'): - return multiparams - else: - return [multiparams] - - return locals() -try: - from sqlalchemy.cutils import _distill_params -except ImportError: - globals().update(py_fallback()) diff --git a/libs/sqlalchemy/event/__init__.py b/libs/sqlalchemy/event/__init__.py deleted file mode 100644 index b43bf9bf..00000000 --- a/libs/sqlalchemy/event/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# event/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .api import CANCEL, NO_RETVAL, listen, listens_for, remove, contains -from .base import Events, dispatcher -from .attr import RefCollection -from .legacy import _legacy_signature diff --git a/libs/sqlalchemy/event/api.py b/libs/sqlalchemy/event/api.py deleted file mode 100644 index 20e74d90..00000000 --- a/libs/sqlalchemy/event/api.py +++ /dev/null @@ -1,107 +0,0 @@ -# event/api.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Public API functions for the event system. - -""" -from __future__ import absolute_import - -from .. import util, exc -from .base import _registrars -from .registry import _EventKey - -CANCEL = util.symbol('CANCEL') -NO_RETVAL = util.symbol('NO_RETVAL') - - -def _event_key(target, identifier, fn): - for evt_cls in _registrars[identifier]: - tgt = evt_cls._accept_with(target) - if tgt is not None: - return _EventKey(target, identifier, fn, tgt) - else: - raise exc.InvalidRequestError("No such event '%s' for target '%s'" % - (identifier, target)) - -def listen(target, identifier, fn, *args, **kw): - """Register a listener function for the given target. - - e.g.:: - - from sqlalchemy import event - from sqlalchemy.schema import UniqueConstraint - - def unique_constraint_name(const, table): - const.name = "uq_%s_%s" % ( - table.name, - list(const.columns)[0].name - ) - event.listen( - UniqueConstraint, - "after_parent_attach", - unique_constraint_name) - - """ - - _event_key(target, identifier, fn).listen(*args, **kw) - - -def listens_for(target, identifier, *args, **kw): - """Decorate a function as a listener for the given target + identifier. - - e.g.:: - - from sqlalchemy import event - from sqlalchemy.schema import UniqueConstraint - - @event.listens_for(UniqueConstraint, "after_parent_attach") - def unique_constraint_name(const, table): - const.name = "uq_%s_%s" % ( - table.name, - list(const.columns)[0].name - ) - """ - def decorate(fn): - listen(target, identifier, fn, *args, **kw) - return fn - return decorate - - -def remove(target, identifier, fn): - """Remove an event listener. - - The arguments here should match exactly those which were sent to - :func:`.listen`; all the event registration which proceeded as a result - of this call will be reverted by calling :func:`.remove` with the same - arguments. - - e.g.:: - - # if a function was registered like this... - @event.listens_for(SomeMappedClass, "before_insert", propagate=True) - def my_listener_function(*arg): - pass - - # ... it's removed like this - event.remove(SomeMappedClass, "before_insert", my_listener_function) - - Above, the listener function associated with ``SomeMappedClass`` was also - propagated to subclasses of ``SomeMappedClass``; the :func:`.remove` function - will revert all of these operations. - - .. versionadded:: 0.9.0 - - """ - _event_key(target, identifier, fn).remove() - -def contains(target, identifier, fn): - """Return True if the given target/ident/fn is set up to listen. - - .. versionadded:: 0.9.0 - - """ - - return _event_key(target, identifier, fn).contains() diff --git a/libs/sqlalchemy/event/attr.py b/libs/sqlalchemy/event/attr.py deleted file mode 100644 index 3f894754..00000000 --- a/libs/sqlalchemy/event/attr.py +++ /dev/null @@ -1,376 +0,0 @@ -# event/attr.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Attribute implementation for _Dispatch classes. - -The various listener targets for a particular event class are represented -as attributes, which refer to collections of listeners to be fired off. -These collections can exist at the class level as well as at the instance -level. An event is fired off using code like this:: - - some_object.dispatch.first_connect(arg1, arg2) - -Above, ``some_object.dispatch`` would be an instance of ``_Dispatch`` and -``first_connect`` is typically an instance of ``_ListenerCollection`` -if event listeners are present, or ``_EmptyListener`` if none are present. - -The attribute mechanics here spend effort trying to ensure listener functions -are available with a minimum of function call overhead, that unnecessary -objects aren't created (i.e. many empty per-instance listener collections), -as well as that everything is garbage collectable when owning references are -lost. Other features such as "propagation" of listener functions across -many ``_Dispatch`` instances, "joining" of multiple ``_Dispatch`` instances, -as well as support for subclass propagation (e.g. events assigned to -``Pool`` vs. ``QueuePool``) are all implemented here. - -""" - -from __future__ import absolute_import - -from .. import util -from . import registry -from . import legacy -from itertools import chain -import weakref - -class RefCollection(object): - @util.memoized_property - def ref(self): - return weakref.ref(self, registry._collection_gced) - -class _DispatchDescriptor(RefCollection): - """Class-level attributes on :class:`._Dispatch` classes.""" - - def __init__(self, parent_dispatch_cls, fn): - self.__name__ = fn.__name__ - argspec = util.inspect_getargspec(fn) - self.arg_names = argspec.args[1:] - self.has_kw = bool(argspec.keywords) - self.legacy_signatures = list(reversed( - sorted( - getattr(fn, '_legacy_signatures', []), - key=lambda s: s[0] - ) - )) - self.__doc__ = fn.__doc__ = legacy._augment_fn_docs( - self, parent_dispatch_cls, fn) - - self._clslevel = weakref.WeakKeyDictionary() - self._empty_listeners = weakref.WeakKeyDictionary() - - def _adjust_fn_spec(self, fn, named): - if named: - fn = self._wrap_fn_for_kw(fn) - if self.legacy_signatures: - try: - argspec = util.get_callable_argspec(fn, no_self=True) - except ValueError: - pass - else: - fn = legacy._wrap_fn_for_legacy(self, fn, argspec) - return fn - - def _wrap_fn_for_kw(self, fn): - def wrap_kw(*args, **kw): - argdict = dict(zip(self.arg_names, args)) - argdict.update(kw) - return fn(**argdict) - return wrap_kw - - - def insert(self, event_key, propagate): - target = event_key.dispatch_target - assert isinstance(target, type), \ - "Class-level Event targets must be classes." - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls is not target and cls not in self._clslevel: - self.update_subclass(cls) - else: - if cls not in self._clslevel: - self._clslevel[cls] = [] - self._clslevel[cls].insert(0, event_key._listen_fn) - registry._stored_in_collection(event_key, self) - - def append(self, event_key, propagate): - target = event_key.dispatch_target - assert isinstance(target, type), \ - "Class-level Event targets must be classes." - - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls is not target and cls not in self._clslevel: - self.update_subclass(cls) - else: - if cls not in self._clslevel: - self._clslevel[cls] = [] - self._clslevel[cls].append(event_key._listen_fn) - registry._stored_in_collection(event_key, self) - - def update_subclass(self, target): - if target not in self._clslevel: - self._clslevel[target] = [] - clslevel = self._clslevel[target] - for cls in target.__mro__[1:]: - if cls in self._clslevel: - clslevel.extend([ - fn for fn - in self._clslevel[cls] - if fn not in clslevel - ]) - - def remove(self, event_key): - target = event_key.dispatch_target - stack = [target] - while stack: - cls = stack.pop(0) - stack.extend(cls.__subclasses__()) - if cls in self._clslevel: - self._clslevel[cls].remove(event_key.fn) - registry._removed_from_collection(event_key, self) - - def clear(self): - """Clear all class level listeners""" - - to_clear = set() - for dispatcher in self._clslevel.values(): - to_clear.update(dispatcher) - dispatcher[:] = [] - registry._clear(self, to_clear) - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _DispatchDescriptor at the class level of - a dispatcher, this returns self. - - """ - return self - - def __get__(self, obj, cls): - if obj is None: - return self - elif obj._parent_cls in self._empty_listeners: - ret = self._empty_listeners[obj._parent_cls] - else: - self._empty_listeners[obj._parent_cls] = ret = \ - _EmptyListener(self, obj._parent_cls) - # assigning it to __dict__ means - # memoized for fast re-access. but more memory. - obj.__dict__[self.__name__] = ret - return ret - -class _HasParentDispatchDescriptor(object): - def _adjust_fn_spec(self, fn, named): - return self.parent._adjust_fn_spec(fn, named) - -class _EmptyListener(_HasParentDispatchDescriptor): - """Serves as a class-level interface to the events - served by a _DispatchDescriptor, when there are no - instance-level events present. - - Is replaced by _ListenerCollection when instance-level - events are added. - - """ - def __init__(self, parent, target_cls): - if target_cls not in parent._clslevel: - parent.update_subclass(target_cls) - self.parent = parent # _DispatchDescriptor - self.parent_listeners = parent._clslevel[target_cls] - self.name = parent.__name__ - self.propagate = frozenset() - self.listeners = () - - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _EmptyListener at the instance level of - a dispatcher, this generates a new - _ListenerCollection, applies it to the instance, - and returns it. - - """ - result = _ListenerCollection(self.parent, obj._parent_cls) - if obj.__dict__[self.name] is self: - obj.__dict__[self.name] = result - return result - - def _needs_modify(self, *args, **kw): - raise NotImplementedError("need to call for_modify()") - - exec_once = insert = append = remove = clear = _needs_modify - - def __call__(self, *args, **kw): - """Execute this event.""" - - for fn in self.parent_listeners: - fn(*args, **kw) - - def __len__(self): - return len(self.parent_listeners) - - def __iter__(self): - return iter(self.parent_listeners) - - def __bool__(self): - return bool(self.parent_listeners) - - __nonzero__ = __bool__ - - -class _CompoundListener(_HasParentDispatchDescriptor): - _exec_once = False - - def exec_once(self, *args, **kw): - """Execute this event, but only if it has not been - executed already for this collection.""" - - if not self._exec_once: - self(*args, **kw) - self._exec_once = True - - def __call__(self, *args, **kw): - """Execute this event.""" - - for fn in self.parent_listeners: - fn(*args, **kw) - for fn in self.listeners: - fn(*args, **kw) - - def __len__(self): - return len(self.parent_listeners) + len(self.listeners) - - def __iter__(self): - return chain(self.parent_listeners, self.listeners) - - def __bool__(self): - return bool(self.listeners or self.parent_listeners) - - __nonzero__ = __bool__ - -class _ListenerCollection(RefCollection, _CompoundListener): - """Instance-level attributes on instances of :class:`._Dispatch`. - - Represents a collection of listeners. - - As of 0.7.9, _ListenerCollection is only first - created via the _EmptyListener.for_modify() method. - - """ - - def __init__(self, parent, target_cls): - if target_cls not in parent._clslevel: - parent.update_subclass(target_cls) - self.parent_listeners = parent._clslevel[target_cls] - self.parent = parent - self.name = parent.__name__ - self.listeners = [] - self.propagate = set() - - def for_modify(self, obj): - """Return an event collection which can be modified. - - For _ListenerCollection at the instance level of - a dispatcher, this returns self. - - """ - return self - - def _update(self, other, only_propagate=True): - """Populate from the listeners in another :class:`_Dispatch` - object.""" - - existing_listeners = self.listeners - existing_listener_set = set(existing_listeners) - self.propagate.update(other.propagate) - other_listeners = [l for l - in other.listeners - if l not in existing_listener_set - and not only_propagate or l in self.propagate - ] - - existing_listeners.extend(other_listeners) - - to_associate = other.propagate.union(other_listeners) - registry._stored_in_collection_multi(self, other, to_associate) - - def insert(self, event_key, propagate): - if event_key._listen_fn not in self.listeners: - event_key.prepend_to_list(self, self.listeners) - if propagate: - self.propagate.add(event_key._listen_fn) - - def append(self, event_key, propagate): - if event_key._listen_fn not in self.listeners: - event_key.append_to_list(self, self.listeners) - if propagate: - self.propagate.add(event_key._listen_fn) - - def remove(self, event_key): - self.listeners.remove(event_key._listen_fn) - self.propagate.discard(event_key._listen_fn) - registry._removed_from_collection(event_key, self) - - def clear(self): - registry._clear(self, self.listeners) - self.propagate.clear() - self.listeners[:] = [] - - -class _JoinedDispatchDescriptor(object): - def __init__(self, name): - self.name = name - - def __get__(self, obj, cls): - if obj is None: - return self - else: - obj.__dict__[self.name] = ret = _JoinedListener( - obj.parent, self.name, - getattr(obj.local, self.name) - ) - return ret - - -class _JoinedListener(_CompoundListener): - _exec_once = False - - def __init__(self, parent, name, local): - self.parent = parent - self.name = name - self.local = local - self.parent_listeners = self.local - - @property - def listeners(self): - return getattr(self.parent, self.name) - - def _adjust_fn_spec(self, fn, named): - return self.local._adjust_fn_spec(fn, named) - - def for_modify(self, obj): - self.local = self.parent_listeners = self.local.for_modify(obj) - return self - - def insert(self, event_key, propagate): - self.local.insert(event_key, propagate) - - def append(self, event_key, propagate): - self.local.append(event_key, propagate) - - def remove(self, event_key): - self.local.remove(event_key) - - def clear(self): - raise NotImplementedError() - - diff --git a/libs/sqlalchemy/event/base.py b/libs/sqlalchemy/event/base.py deleted file mode 100644 index 5c8d92cb..00000000 --- a/libs/sqlalchemy/event/base.py +++ /dev/null @@ -1,217 +0,0 @@ -# event/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Base implementation classes. - -The public-facing ``Events`` serves as the base class for an event interface; -it's public attributes represent different kinds of events. These attributes -are mirrored onto a ``_Dispatch`` class, which serves as a container for -collections of listener functions. These collections are represented both -at the class level of a particular ``_Dispatch`` class as well as within -instances of ``_Dispatch``. - -""" -from __future__ import absolute_import - -from .. import util -from .attr import _JoinedDispatchDescriptor, _EmptyListener, _DispatchDescriptor - -_registrars = util.defaultdict(list) - - -def _is_event_name(name): - return not name.startswith('_') and name != 'dispatch' - - -class _UnpickleDispatch(object): - """Serializable callable that re-generates an instance of - :class:`_Dispatch` given a particular :class:`.Events` subclass. - - """ - def __call__(self, _parent_cls): - for cls in _parent_cls.__mro__: - if 'dispatch' in cls.__dict__: - return cls.__dict__['dispatch'].dispatch_cls(_parent_cls) - else: - raise AttributeError("No class with a 'dispatch' member present.") - - -class _Dispatch(object): - """Mirror the event listening definitions of an Events class with - listener collections. - - Classes which define a "dispatch" member will return a - non-instantiated :class:`._Dispatch` subclass when the member - is accessed at the class level. When the "dispatch" member is - accessed at the instance level of its owner, an instance - of the :class:`._Dispatch` class is returned. - - A :class:`._Dispatch` class is generated for each :class:`.Events` - class defined, by the :func:`._create_dispatcher_class` function. - The original :class:`.Events` classes remain untouched. - This decouples the construction of :class:`.Events` subclasses from - the implementation used by the event internals, and allows - inspecting tools like Sphinx to work in an unsurprising - way against the public API. - - """ - - _events = None - """reference the :class:`.Events` class which this - :class:`._Dispatch` is created for.""" - - def __init__(self, _parent_cls): - self._parent_cls = _parent_cls - - @util.classproperty - def _listen(cls): - return cls._events._listen - - def _join(self, other): - """Create a 'join' of this :class:`._Dispatch` and another. - - This new dispatcher will dispatch events to both - :class:`._Dispatch` objects. - - """ - if '_joined_dispatch_cls' not in self.__class__.__dict__: - cls = type( - "Joined%s" % self.__class__.__name__, - (_JoinedDispatcher, self.__class__), {} - ) - for ls in _event_descriptors(self): - setattr(cls, ls.name, _JoinedDispatchDescriptor(ls.name)) - - self.__class__._joined_dispatch_cls = cls - return self._joined_dispatch_cls(self, other) - - def __reduce__(self): - return _UnpickleDispatch(), (self._parent_cls, ) - - def _update(self, other, only_propagate=True): - """Populate from the listeners in another :class:`_Dispatch` - object.""" - - for ls in _event_descriptors(other): - if isinstance(ls, _EmptyListener): - continue - getattr(self, ls.name).\ - for_modify(self)._update(ls, only_propagate=only_propagate) - - @util.hybridmethod - def _clear(self): - for attr in dir(self): - if _is_event_name(attr): - getattr(self, attr).for_modify(self).clear() - - -def _event_descriptors(target): - return [getattr(target, k) for k in dir(target) if _is_event_name(k)] - - -class _EventMeta(type): - """Intercept new Event subclasses and create - associated _Dispatch classes.""" - - def __init__(cls, classname, bases, dict_): - _create_dispatcher_class(cls, classname, bases, dict_) - return type.__init__(cls, classname, bases, dict_) - - -def _create_dispatcher_class(cls, classname, bases, dict_): - """Create a :class:`._Dispatch` class corresponding to an - :class:`.Events` class.""" - - # there's all kinds of ways to do this, - # i.e. make a Dispatch class that shares the '_listen' method - # of the Event class, this is the straight monkeypatch. - dispatch_base = getattr(cls, 'dispatch', _Dispatch) - dispatch_cls = type("%sDispatch" % classname, - (dispatch_base, ), {}) - cls._set_dispatch(cls, dispatch_cls) - - for k in dict_: - if _is_event_name(k): - setattr(dispatch_cls, k, _DispatchDescriptor(cls, dict_[k])) - _registrars[k].append(cls) - - if getattr(cls, '_dispatch_target', None): - cls._dispatch_target.dispatch = dispatcher(cls) - - -def _remove_dispatcher(cls): - for k in dir(cls): - if _is_event_name(k): - _registrars[k].remove(cls) - if not _registrars[k]: - del _registrars[k] - -class Events(util.with_metaclass(_EventMeta, object)): - """Define event listening functions for a particular target type.""" - - @staticmethod - def _set_dispatch(cls, dispatch_cls): - # this allows an Events subclass to define additional utility - # methods made available to the target via - # "self.dispatch._events." - # @staticemethod to allow easy "super" calls while in a metaclass - # constructor. - cls.dispatch = dispatch_cls - dispatch_cls._events = cls - - - @classmethod - def _accept_with(cls, target): - # Mapper, ClassManager, Session override this to - # also accept classes, scoped_sessions, sessionmakers, etc. - if hasattr(target, 'dispatch') and ( - isinstance(target.dispatch, cls.dispatch) or \ - isinstance(target.dispatch, type) and \ - issubclass(target.dispatch, cls.dispatch) - ): - return target - else: - return None - - @classmethod - def _listen(cls, event_key, propagate=False, insert=False, named=False): - event_key.base_listen(propagate=propagate, insert=insert, named=named) - - @classmethod - def _remove(cls, event_key): - event_key.remove() - - @classmethod - def _clear(cls): - cls.dispatch._clear() - - -class _JoinedDispatcher(object): - """Represent a connection between two _Dispatch objects.""" - - def __init__(self, local, parent): - self.local = local - self.parent = parent - self._parent_cls = local._parent_cls - - -class dispatcher(object): - """Descriptor used by target classes to - deliver the _Dispatch class at the class level - and produce new _Dispatch instances for target - instances. - - """ - def __init__(self, events): - self.dispatch_cls = events.dispatch - self.events = events - - def __get__(self, obj, cls): - if obj is None: - return self.dispatch_cls - obj.__dict__['dispatch'] = disp = self.dispatch_cls(cls) - return disp - diff --git a/libs/sqlalchemy/event/legacy.py b/libs/sqlalchemy/event/legacy.py deleted file mode 100644 index 0b0d290f..00000000 --- a/libs/sqlalchemy/event/legacy.py +++ /dev/null @@ -1,156 +0,0 @@ -# event/legacy.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Routines to handle adaption of legacy call signatures, -generation of deprecation notes and docstrings. - -""" - -from .. import util - -def _legacy_signature(since, argnames, converter=None): - def leg(fn): - if not hasattr(fn, '_legacy_signatures'): - fn._legacy_signatures = [] - fn._legacy_signatures.append((since, argnames, converter)) - return fn - return leg - -def _wrap_fn_for_legacy(dispatch_descriptor, fn, argspec): - for since, argnames, conv in dispatch_descriptor.legacy_signatures: - if argnames[-1] == "**kw": - has_kw = True - argnames = argnames[0:-1] - else: - has_kw = False - - if len(argnames) == len(argspec.args) \ - and has_kw is bool(argspec.keywords): - - if conv: - assert not has_kw - def wrap_leg(*args): - return fn(*conv(*args)) - else: - def wrap_leg(*args, **kw): - argdict = dict(zip(dispatch_descriptor.arg_names, args)) - args = [argdict[name] for name in argnames] - if has_kw: - return fn(*args, **kw) - else: - return fn(*args) - return wrap_leg - else: - return fn - -def _indent(text, indent): - return "\n".join( - indent + line - for line in text.split("\n") - ) - -def _standard_listen_example(dispatch_descriptor, sample_target, fn): - example_kw_arg = _indent( - "\n".join( - "%(arg)s = kw['%(arg)s']" % {"arg": arg} - for arg in dispatch_descriptor.arg_names[0:2] - ), - " ") - if dispatch_descriptor.legacy_signatures: - current_since = max(since for since, args, conv - in dispatch_descriptor.legacy_signatures) - else: - current_since = None - text = ( - "from sqlalchemy import event\n\n" - "# standard decorator style%(current_since)s\n" - "@event.listens_for(%(sample_target)s, '%(event_name)s')\n" - "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n" - " \"listen for the '%(event_name)s' event\"\n" - "\n # ... (event handling logic) ...\n" - ) - - if len(dispatch_descriptor.arg_names) > 2: - text += ( - - "\n# named argument style (new in 0.9)\n" - "@event.listens_for(%(sample_target)s, '%(event_name)s', named=True)\n" - "def receive_%(event_name)s(**kw):\n" - " \"listen for the '%(event_name)s' event\"\n" - "%(example_kw_arg)s\n" - "\n # ... (event handling logic) ...\n" - ) - - text %= { - "current_since": " (arguments as of %s)" % - current_since if current_since else "", - "event_name": fn.__name__, - "has_kw_arguments": ", **kw" if dispatch_descriptor.has_kw else "", - "named_event_arguments": ", ".join(dispatch_descriptor.arg_names), - "example_kw_arg": example_kw_arg, - "sample_target": sample_target - } - return text - -def _legacy_listen_examples(dispatch_descriptor, sample_target, fn): - text = "" - for since, args, conv in dispatch_descriptor.legacy_signatures: - text += ( - "\n# legacy calling style (pre-%(since)s)\n" - "@event.listens_for(%(sample_target)s, '%(event_name)s')\n" - "def receive_%(event_name)s(%(named_event_arguments)s%(has_kw_arguments)s):\n" - " \"listen for the '%(event_name)s' event\"\n" - "\n # ... (event handling logic) ...\n" % { - "since": since, - "event_name": fn.__name__, - "has_kw_arguments": " **kw" if dispatch_descriptor.has_kw else "", - "named_event_arguments": ", ".join(args), - "sample_target": sample_target - } - ) - return text - -def _version_signature_changes(dispatch_descriptor): - since, args, conv = dispatch_descriptor.legacy_signatures[0] - return ( - "\n.. versionchanged:: %(since)s\n" - " The ``%(event_name)s`` event now accepts the \n" - " arguments ``%(named_event_arguments)s%(has_kw_arguments)s``.\n" - " Listener functions which accept the previous argument \n" - " signature(s) listed above will be automatically \n" - " adapted to the new signature." % { - "since": since, - "event_name": dispatch_descriptor.__name__, - "named_event_arguments": ", ".join(dispatch_descriptor.arg_names), - "has_kw_arguments": ", **kw" if dispatch_descriptor.has_kw else "" - } - ) - -def _augment_fn_docs(dispatch_descriptor, parent_dispatch_cls, fn): - header = ".. container:: event_signatures\n\n"\ - " Example argument forms::\n"\ - "\n" - - sample_target = getattr(parent_dispatch_cls, "_target_class_doc", "obj") - text = ( - header + - _indent( - _standard_listen_example( - dispatch_descriptor, sample_target, fn), - " " * 8) - ) - if dispatch_descriptor.legacy_signatures: - text += _indent( - _legacy_listen_examples( - dispatch_descriptor, sample_target, fn), - " " * 8) - - text += _version_signature_changes(dispatch_descriptor) - - return util.inject_docstring_text(fn.__doc__, - text, - 1 - ) diff --git a/libs/sqlalchemy/event/registry.py b/libs/sqlalchemy/event/registry.py deleted file mode 100644 index 7710ff2d..00000000 --- a/libs/sqlalchemy/event/registry.py +++ /dev/null @@ -1,236 +0,0 @@ -# event/registry.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provides managed registration services on behalf of :func:`.listen` -arguments. - -By "managed registration", we mean that event listening functions and -other objects can be added to various collections in such a way that their -membership in all those collections can be revoked at once, based on -an equivalent :class:`._EventKey`. - -""" - -from __future__ import absolute_import - -import weakref -import collections -import types -from .. import exc - - -_key_to_collection = collections.defaultdict(dict) -""" -Given an original listen() argument, can locate all -listener collections and the listener fn contained - -(target, identifier, fn) -> { - ref(listenercollection) -> ref(listener_fn) - ref(listenercollection) -> ref(listener_fn) - ref(listenercollection) -> ref(listener_fn) - } -""" - -_collection_to_key = collections.defaultdict(dict) -""" -Given a _ListenerCollection or _DispatchDescriptor, can locate -all the original listen() arguments and the listener fn contained - -ref(listenercollection) -> { - ref(listener_fn) -> (target, identifier, fn), - ref(listener_fn) -> (target, identifier, fn), - ref(listener_fn) -> (target, identifier, fn), - } -""" - -def _collection_gced(ref): - # defaultdict, so can't get a KeyError - if not _collection_to_key or ref not in _collection_to_key: - return - listener_to_key = _collection_to_key.pop(ref) - for key in listener_to_key.values(): - if key in _key_to_collection: - # defaultdict, so can't get a KeyError - dispatch_reg = _key_to_collection[key] - dispatch_reg.pop(ref) - if not dispatch_reg: - _key_to_collection.pop(key) - -def _stored_in_collection(event_key, owner): - key = event_key._key - - dispatch_reg = _key_to_collection[key] - - owner_ref = owner.ref - listen_ref = weakref.ref(event_key._listen_fn) - - if owner_ref in dispatch_reg: - assert dispatch_reg[owner_ref] == listen_ref - else: - dispatch_reg[owner_ref] = listen_ref - - listener_to_key = _collection_to_key[owner_ref] - listener_to_key[listen_ref] = key - -def _removed_from_collection(event_key, owner): - key = event_key._key - - dispatch_reg = _key_to_collection[key] - - listen_ref = weakref.ref(event_key._listen_fn) - - owner_ref = owner.ref - dispatch_reg.pop(owner_ref, None) - if not dispatch_reg: - del _key_to_collection[key] - - if owner_ref in _collection_to_key: - listener_to_key = _collection_to_key[owner_ref] - listener_to_key.pop(listen_ref) - -def _stored_in_collection_multi(newowner, oldowner, elements): - if not elements: - return - - oldowner = oldowner.ref - newowner = newowner.ref - - old_listener_to_key = _collection_to_key[oldowner] - new_listener_to_key = _collection_to_key[newowner] - - for listen_fn in elements: - listen_ref = weakref.ref(listen_fn) - key = old_listener_to_key[listen_ref] - dispatch_reg = _key_to_collection[key] - if newowner in dispatch_reg: - assert dispatch_reg[newowner] == listen_ref - else: - dispatch_reg[newowner] = listen_ref - - new_listener_to_key[listen_ref] = key - -def _clear(owner, elements): - if not elements: - return - - owner = owner.ref - listener_to_key = _collection_to_key[owner] - for listen_fn in elements: - listen_ref = weakref.ref(listen_fn) - key = listener_to_key[listen_ref] - dispatch_reg = _key_to_collection[key] - dispatch_reg.pop(owner, None) - - if not dispatch_reg: - del _key_to_collection[key] - - -class _EventKey(object): - """Represent :func:`.listen` arguments. - """ - - - def __init__(self, target, identifier, fn, dispatch_target, _fn_wrap=None): - self.target = target - self.identifier = identifier - self.fn = fn - if isinstance(fn, types.MethodType): - self.fn_key = id(fn.__func__), id(fn.__self__) - else: - self.fn_key = id(fn) - self.fn_wrap = _fn_wrap - self.dispatch_target = dispatch_target - - @property - def _key(self): - return (id(self.target), self.identifier, self.fn_key) - - def with_wrapper(self, fn_wrap): - if fn_wrap is self._listen_fn: - return self - else: - return _EventKey( - self.target, - self.identifier, - self.fn, - self.dispatch_target, - _fn_wrap=fn_wrap - ) - - def with_dispatch_target(self, dispatch_target): - if dispatch_target is self.dispatch_target: - return self - else: - return _EventKey( - self.target, - self.identifier, - self.fn, - dispatch_target, - _fn_wrap=self.fn_wrap - ) - - def listen(self, *args, **kw): - self.dispatch_target.dispatch._listen(self, *args, **kw) - - def remove(self): - key = self._key - - if key not in _key_to_collection: - raise exc.InvalidRequestError( - "No listeners found for event %s / %r / %s " % - (self.target, self.identifier, self.fn) - ) - dispatch_reg = _key_to_collection.pop(key) - - for collection_ref, listener_ref in dispatch_reg.items(): - collection = collection_ref() - listener_fn = listener_ref() - if collection is not None and listener_fn is not None: - collection.remove(self.with_wrapper(listener_fn)) - - def contains(self): - """Return True if this event key is registered to listen. - """ - return self._key in _key_to_collection - - def base_listen(self, propagate=False, insert=False, - named=False): - - target, identifier, fn = \ - self.dispatch_target, self.identifier, self._listen_fn - - dispatch_descriptor = getattr(target.dispatch, identifier) - - fn = dispatch_descriptor._adjust_fn_spec(fn, named) - self = self.with_wrapper(fn) - - if insert: - dispatch_descriptor.\ - for_modify(target.dispatch).insert(self, propagate) - else: - dispatch_descriptor.\ - for_modify(target.dispatch).append(self, propagate) - - @property - def _listen_fn(self): - return self.fn_wrap or self.fn - - def append_value_to_list(self, owner, list_, value): - _stored_in_collection(self, owner) - list_.append(value) - - def append_to_list(self, owner, list_): - _stored_in_collection(self, owner) - list_.append(self._listen_fn) - - def remove_from_list(self, owner, list_): - _removed_from_collection(self, owner) - list_.remove(self._listen_fn) - - def prepend_to_list(self, owner, list_): - _stored_in_collection(self, owner) - list_.insert(0, self._listen_fn) - diff --git a/libs/sqlalchemy/events.py b/libs/sqlalchemy/events.py deleted file mode 100644 index cf77bbb7..00000000 --- a/libs/sqlalchemy/events.py +++ /dev/null @@ -1,809 +0,0 @@ -# sqlalchemy/events.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Core event interfaces.""" - -from . import event, exc -from .pool import Pool -from .engine import Connectable, Engine -from .sql.base import SchemaEventTarget - -class DDLEvents(event.Events): - """ - Define event listeners for schema objects, - that is, :class:`.SchemaItem` and other :class:`.SchemaEventTarget` - subclasses, including :class:`.MetaData`, :class:`.Table`, - :class:`.Column`. - - :class:`.MetaData` and :class:`.Table` support events - specifically regarding when CREATE and DROP - DDL is emitted to the database. - - Attachment events are also provided to customize - behavior whenever a child schema element is associated - with a parent, such as, when a :class:`.Column` is associated - with its :class:`.Table`, when a :class:`.ForeignKeyConstraint` - is associated with a :class:`.Table`, etc. - - Example using the ``after_create`` event:: - - from sqlalchemy import event - from sqlalchemy import Table, Column, Metadata, Integer - - m = MetaData() - some_table = Table('some_table', m, Column('data', Integer)) - - def after_create(target, connection, **kw): - connection.execute("ALTER TABLE %s SET name=foo_%s" % - (target.name, target.name)) - - event.listen(some_table, "after_create", after_create) - - DDL events integrate closely with the - :class:`.DDL` class and the :class:`.DDLElement` hierarchy - of DDL clause constructs, which are themselves appropriate - as listener callables:: - - from sqlalchemy import DDL - event.listen( - some_table, - "after_create", - DDL("ALTER TABLE %(table)s SET name=foo_%(table)s") - ) - - The methods here define the name of an event as well - as the names of members that are passed to listener - functions. - - See also: - - :ref:`event_toplevel` - - :class:`.DDLElement` - - :class:`.DDL` - - :ref:`schema_ddl_sequences` - - """ - - _target_class_doc = "SomeSchemaClassOrObject" - _dispatch_target = SchemaEventTarget - - def before_create(self, target, connection, **kw): - """Called before CREATE statments are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - CREATE statement or statements will be emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - """ - - def after_create(self, target, connection, **kw): - """Called after CREATE statments are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - CREATE statement or statements have been emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - """ - - def before_drop(self, target, connection, **kw): - """Called before DROP statments are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - DROP statement or statements will be emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - """ - - def after_drop(self, target, connection, **kw): - """Called after DROP statments are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - DROP statement or statements have been emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - """ - - def before_parent_attach(self, target, parent): - """Called before a :class:`.SchemaItem` is associated with - a parent :class:`.SchemaItem`. - - :param target: the target object - :param parent: the parent to which the target is being attached. - - :func:`.event.listen` also accepts a modifier for this event: - - :param propagate=False: When True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def after_parent_attach(self, target, parent): - """Called after a :class:`.SchemaItem` is associated with - a parent :class:`.SchemaItem`. - - :param target: the target object - :param parent: the parent to which the target is being attached. - - :func:`.event.listen` also accepts a modifier for this event: - - :param propagate=False: When True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def column_reflect(self, inspector, table, column_info): - """Called for each unit of 'column info' retrieved when - a :class:`.Table` is being reflected. - - The dictionary of column information as returned by the - dialect is passed, and can be modified. The dictionary - is that returned in each element of the list returned - by :meth:`.reflection.Inspector.get_columns`. - - The event is called before any action is taken against - this dictionary, and the contents can be modified. - The :class:`.Column` specific arguments ``info``, ``key``, - and ``quote`` can also be added to the dictionary and - will be passed to the constructor of :class:`.Column`. - - Note that this event is only meaningful if either - associated with the :class:`.Table` class across the - board, e.g.:: - - from sqlalchemy.schema import Table - from sqlalchemy import event - - def listen_for_reflect(inspector, table, column_info): - "receive a column_reflect event" - # ... - - event.listen( - Table, - 'column_reflect', - listen_for_reflect) - - ...or with a specific :class:`.Table` instance using - the ``listeners`` argument:: - - def listen_for_reflect(inspector, table, column_info): - "receive a column_reflect event" - # ... - - t = Table( - 'sometable', - autoload=True, - listeners=[ - ('column_reflect', listen_for_reflect) - ]) - - This because the reflection process initiated by ``autoload=True`` - completes within the scope of the constructor for :class:`.Table`. - - """ - - - -class PoolEvents(event.Events): - """Available events for :class:`.Pool`. - - The methods here define the name of an event as well - as the names of members that are passed to listener - functions. - - e.g.:: - - from sqlalchemy import event - - def my_on_checkout(dbapi_conn, connection_rec, connection_proxy): - "handle an on checkout event" - - event.listen(Pool, 'checkout', my_on_checkout) - - In addition to accepting the :class:`.Pool` class and - :class:`.Pool` instances, :class:`.PoolEvents` also accepts - :class:`.Engine` objects and the :class:`.Engine` class as - targets, which will be resolved to the ``.pool`` attribute of the - given engine or the :class:`.Pool` class:: - - engine = create_engine("postgresql://scott:tiger@localhost/test") - - # will associate with engine.pool - event.listen(engine, 'checkout', my_on_checkout) - - """ - - _target_class_doc = "SomeEngineOrPool" - _dispatch_target = Pool - - @classmethod - def _accept_with(cls, target): - if isinstance(target, type): - if issubclass(target, Engine): - return Pool - elif issubclass(target, Pool): - return target - elif isinstance(target, Engine): - return target.pool - else: - return target - - def connect(self, dbapi_connection, connection_record): - """Called once for each new DB-API connection or Pool's ``creator()``. - - :param dbapi_con: - A newly connected raw DB-API connection (not a SQLAlchemy - ``Connection`` wrapper). - - :param con_record: - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - def first_connect(self, dbapi_connection, connection_record): - """Called exactly once for the first DB-API connection. - - :param dbapi_con: - A newly connected raw DB-API connection (not a SQLAlchemy - ``Connection`` wrapper). - - :param con_record: - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - def checkout(self, dbapi_connection, connection_record, connection_proxy): - """Called when a connection is retrieved from the Pool. - - :param dbapi_con: - A raw DB-API connection - - :param con_record: - The ``_ConnectionRecord`` that persistently manages the connection - - :param con_proxy: - The ``_ConnectionFairy`` which manages the connection for the span of - the current checkout. - - If you raise a :class:`~sqlalchemy.exc.DisconnectionError`, the current - connection will be disposed and a fresh connection retrieved. - Processing of all checkout listeners will abort and restart - using the new connection. - - .. seealso:: :meth:`.ConnectionEvents.engine_connect` - a similar event - which occurs upon creation of a new :class:`.Connection`. - - """ - - def checkin(self, dbapi_connection, connection_record): - """Called when a connection returns to the pool. - - Note that the connection may be closed, and may be None if the - connection has been invalidated. ``checkin`` will not be called - for detached connections. (They do not return to the pool.) - - :param dbapi_con: - A raw DB-API connection - - :param con_record: - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - def reset(self, dbapi_con, con_record): - """Called before the "reset" action occurs for a pooled connection. - - This event represents - when the ``rollback()`` method is called on the DBAPI connection - before it is returned to the pool. The behavior of "reset" can - be controlled, including disabled, using the ``reset_on_return`` - pool argument. - - - The :meth:`.PoolEvents.reset` event is usually followed by the - the :meth:`.PoolEvents.checkin` event is called, except in those - cases where the connection is discarded immediately after reset. - - :param dbapi_con: - A raw DB-API connection - - :param con_record: - The ``_ConnectionRecord`` that persistently manages the connection - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.ConnectionEvents.rollback` - - :meth:`.ConnectionEvents.commit` - - """ - - - -class ConnectionEvents(event.Events): - """Available events for :class:`.Connectable`, which includes - :class:`.Connection` and :class:`.Engine`. - - The methods here define the name of an event as well as the names of - members that are passed to listener functions. - - An event listener can be associated with any :class:`.Connectable` - class or instance, such as an :class:`.Engine`, e.g.:: - - from sqlalchemy import event, create_engine - - def before_cursor_execute(conn, cursor, statement, parameters, context, - executemany): - log.info("Received statement: %s" % statement) - - engine = create_engine('postgresql://scott:tiger@localhost/test') - event.listen(engine, "before_cursor_execute", before_cursor_execute) - - or with a specific :class:`.Connection`:: - - with engine.begin() as conn: - @event.listens_for(conn, 'before_cursor_execute') - def before_cursor_execute(conn, cursor, statement, parameters, - context, executemany): - log.info("Received statement: %s" % statement) - - The :meth:`.before_execute` and :meth:`.before_cursor_execute` - events can also be established with the ``retval=True`` flag, which - allows modification of the statement and parameters to be sent - to the database. The :meth:`.before_cursor_execute` event is - particularly useful here to add ad-hoc string transformations, such - as comments, to all executions:: - - from sqlalchemy.engine import Engine - from sqlalchemy import event - - @event.listens_for(Engine, "before_cursor_execute", retval=True) - def comment_sql_calls(conn, cursor, statement, parameters, - context, executemany): - statement = statement + " -- some comment" - return statement, parameters - - .. note:: :class:`.ConnectionEvents` can be established on any - combination of :class:`.Engine`, :class:`.Connection`, as well - as instances of each of those classes. Events across all - four scopes will fire off for a given instance of - :class:`.Connection`. However, for performance reasons, the - :class:`.Connection` object determines at instantiation time - whether or not its parent :class:`.Engine` has event listeners - established. Event listeners added to the :class:`.Engine` - class or to an instance of :class:`.Engine` *after* the instantiation - of a dependent :class:`.Connection` instance will usually - *not* be available on that :class:`.Connection` instance. The newly - added listeners will instead take effect for :class:`.Connection` - instances created subsequent to those event listeners being - established on the parent :class:`.Engine` class or instance. - - :param retval=False: Applies to the :meth:`.before_execute` and - :meth:`.before_cursor_execute` events only. When True, the - user-defined event function must have a return value, which - is a tuple of parameters that replace the given statement - and parameters. See those methods for a description of - specific return arguments. - - .. versionchanged:: 0.8 :class:`.ConnectionEvents` can now be associated - with any :class:`.Connectable` including :class:`.Connection`, - in addition to the existing support for :class:`.Engine`. - - """ - - _target_class_doc = "SomeEngine" - _dispatch_target = Connectable - - - @classmethod - def _listen(cls, event_key, retval=False): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - target._has_events = True - - if not retval: - if identifier == 'before_execute': - orig_fn = fn - - def wrap_before_execute(conn, clauseelement, - multiparams, params): - orig_fn(conn, clauseelement, multiparams, params) - return clauseelement, multiparams, params - fn = wrap_before_execute - elif identifier == 'before_cursor_execute': - orig_fn = fn - - def wrap_before_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - orig_fn(conn, cursor, statement, - parameters, context, executemany) - return statement, parameters - fn = wrap_before_cursor_execute - - elif retval and \ - identifier not in ('before_execute', 'before_cursor_execute'): - raise exc.ArgumentError( - "Only the 'before_execute' and " - "'before_cursor_execute' engine " - "event listeners accept the 'retval=True' " - "argument.") - event_key.with_wrapper(fn).base_listen() - - def before_execute(self, conn, clauseelement, multiparams, params): - """Intercept high level execute() events, receiving uncompiled - SQL constructs and other objects prior to rendering into SQL. - - This event is good for debugging SQL compilation issues as well - as early manipulation of the parameters being sent to the database, - as the parameter lists will be in a consistent format here. - - This event can be optionally established with the ``retval=True`` - flag. The ``clauseelement``, ``multiparams``, and ``params`` - arguments should be returned as a three-tuple in this case:: - - @event.listens_for(Engine, "before_execute", retval=True) - def before_execute(conn, conn, clauseelement, multiparams, params): - # do something with clauseelement, multiparams, params - return clauseelement, multiparams, params - - :param conn: :class:`.Connection` object - :param clauseelement: SQL expression construct, :class:`.Compiled` - instance, or string statement passed to :meth:`.Connection.execute`. - :param multiparams: Multiple parameter sets, a list of dictionaries. - :param params: Single parameter set, a single dictionary. - - See also: - - :meth:`.before_cursor_execute` - - """ - - def after_execute(self, conn, clauseelement, multiparams, params, result): - """Intercept high level execute() events after execute. - - - :param conn: :class:`.Connection` object - :param clauseelement: SQL expression construct, :class:`.Compiled` - instance, or string statement passed to :meth:`.Connection.execute`. - :param multiparams: Multiple parameter sets, a list of dictionaries. - :param params: Single parameter set, a single dictionary. - :param result: :class:`.ResultProxy` generated by the execution. - - """ - - def before_cursor_execute(self, conn, cursor, statement, - parameters, context, executemany): - """Intercept low-level cursor execute() events before execution, - receiving the string - SQL statement and DBAPI-specific parameter list to be invoked - against a cursor. - - This event is a good choice for logging as well as late modifications - to the SQL string. It's less ideal for parameter modifications except - for those which are specific to a target backend. - - This event can be optionally established with the ``retval=True`` - flag. The ``statement`` and ``parameters`` arguments should be - returned as a two-tuple in this case:: - - @event.listens_for(Engine, "before_cursor_execute", retval=True) - def before_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - # do something with statement, parameters - return statement, parameters - - See the example at :class:`.ConnectionEvents`. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object - :param statement: string SQL statement - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param executemany: boolean, if ``True``, this is an ``executemany()`` - call, if ``False``, this is an ``execute()`` call. - - See also: - - :meth:`.before_execute` - - :meth:`.after_cursor_execute` - - """ - - def after_cursor_execute(self, conn, cursor, statement, - parameters, context, executemany): - """Intercept low-level cursor execute() events after execution. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object. Will have results pending - if the statement was a SELECT, but these should not be consumed - as they will be needed by the :class:`.ResultProxy`. - :param statement: string SQL statement - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param executemany: boolean, if ``True``, this is an ``executemany()`` - call, if ``False``, this is an ``execute()`` call. - - """ - - def dbapi_error(self, conn, cursor, statement, parameters, - context, exception): - """Intercept a raw DBAPI error. - - This event is called with the DBAPI exception instance - received from the DBAPI itself, *before* SQLAlchemy wraps the - exception with it's own exception wrappers, and before any - other operations are performed on the DBAPI cursor; the - existing transaction remains in effect as well as any state - on the cursor. - - The use case here is to inject low-level exception handling - into an :class:`.Engine`, typically for logging and - debugging purposes. In general, user code should **not** modify - any state or throw any exceptions here as this will - interfere with SQLAlchemy's cleanup and error handling - routines. - - Subsequent to this hook, SQLAlchemy may attempt any - number of operations on the connection/cursor, including - closing the cursor, rolling back of the transaction in the - case of connectionless execution, and disposing of the entire - connection pool if a "disconnect" was detected. The - exception is then wrapped in a SQLAlchemy DBAPI exception - wrapper and re-thrown. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object - :param statement: string SQL statement - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param exception: The **unwrapped** exception emitted directly from the - DBAPI. The class here is specific to the DBAPI module in use. - - .. versionadded:: 0.7.7 - - """ - - def engine_connect(self, conn, branch): - """Intercept the creation of a new :class:`.Connection`. - - This event is called typically as the direct result of calling - the :meth:`.Engine.connect` method. - - It differs from the :meth:`.PoolEvents.connect` method, which - refers to the actual connection to a database at the DBAPI level; - a DBAPI connection may be pooled and reused for many operations. - In contrast, this event refers only to the production of a higher level - :class:`.Connection` wrapper around such a DBAPI connection. - - It also differs from the :meth:`.PoolEvents.checkout` event - in that it is specific to the :class:`.Connection` object, not the - DBAPI connection that :meth:`.PoolEvents.checkout` deals with, although - this DBAPI connection is available here via the :attr:`.Connection.connection` - attribute. But note there can in fact - be multiple :meth:`.PoolEvents.checkout` events within the lifespan - of a single :class:`.Connection` object, if that :class:`.Connection` - is invalidated and re-established. There can also be multiple - :class:`.Connection` objects generated for the same already-checked-out - DBAPI connection, in the case that a "branch" of a :class:`.Connection` - is produced. - - :param conn: :class:`.Connection` object. - :param branch: if True, this is a "branch" of an existing - :class:`.Connection`. A branch is generated within the course - of a statement execution to invoke supplemental statements, most - typically to pre-execute a SELECT of a default value for the purposes - of an INSERT statement. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.PoolEvents.checkout` the lower-level pool checkout event - for an individual DBAPI connection - - :meth:`.ConnectionEvents.set_connection_execution_options` - a copy of a - :class:`.Connection` is also made when the - :meth:`.Connection.execution_options` method is called. - - """ - - def set_connection_execution_options(self, conn, opts): - """Intercept when the :meth:`.Connection.execution_options` - method is called. - - This method is called after the new :class:`.Connection` has been - produced, with the newly updated execution options collection, but - before the :class:`.Dialect` has acted upon any of those new options. - - Note that this method is not called when a new :class:`.Connection` - is produced which is inheriting execution options from its parent - :class:`.Engine`; to intercept this condition, use the - :meth:`.ConnectionEvents.engine_connect` event. - - :param conn: The newly copied :class:`.Connection` object - - :param opts: dictionary of options that were passed to the - :meth:`.Connection.execution_options` method. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.ConnectionEvents.set_engine_execution_options` - event - which is called when :meth:`.Engine.execution_options` is called. - - - """ - - def set_engine_execution_options(self, engine, opts): - """Intercept when the :meth:`.Engine.execution_options` - method is called. - - The :meth:`.Engine.execution_options` method produces a shallow - copy of the :class:`.Engine` which stores the new options. That new - :class:`.Engine` is passed here. A particular application of this - method is to add a :meth:`.ConnectionEvents.engine_connect` event - handler to the given :class:`.Engine` which will perform some per- - :class:`.Connection` task specific to these execution options. - - :param conn: The newly copied :class:`.Engine` object - - :param opts: dictionary of options that were passed to the - :meth:`.Connection.execution_options` method. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.ConnectionEvents.set_connection_execution_options` - event - which is called when :meth:`.Connection.execution_options` is called. - - """ - - def begin(self, conn): - """Intercept begin() events. - - :param conn: :class:`.Connection` object - - """ - - def rollback(self, conn): - """Intercept rollback() events, as initiated by a - :class:`.Transaction`. - - Note that the :class:`.Pool` also "auto-rolls back" - a DBAPI connection upon checkin, if the ``reset_on_return`` - flag is set to its default value of ``'rollback'``. - To intercept this - rollback, use the :meth:`.PoolEvents.reset` hook. - - :param conn: :class:`.Connection` object - - .. seealso:: - - :meth:`.PoolEvents.reset` - - """ - - def commit(self, conn): - """Intercept commit() events, as initiated by a - :class:`.Transaction`. - - Note that the :class:`.Pool` may also "auto-commit" - a DBAPI connection upon checkin, if the ``reset_on_return`` - flag is set to the value ``'commit'``. To intercept this - commit, use the :meth:`.PoolEvents.reset` hook. - - :param conn: :class:`.Connection` object - """ - - def savepoint(self, conn, name): - """Intercept savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - - """ - - def rollback_savepoint(self, conn, name, context): - """Intercept rollback_savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - :param context: :class:`.ExecutionContext` in use. May be ``None``. - - """ - - def release_savepoint(self, conn, name, context): - """Intercept release_savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - :param context: :class:`.ExecutionContext` in use. May be ``None``. - - """ - - def begin_twophase(self, conn, xid): - """Intercept begin_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - - """ - - def prepare_twophase(self, conn, xid): - """Intercept prepare_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - """ - - def rollback_twophase(self, conn, xid, is_prepared): - """Intercept rollback_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - :param is_prepared: boolean, indicates if - :meth:`.TwoPhaseTransaction.prepare` was called. - - """ - - def commit_twophase(self, conn, xid, is_prepared): - """Intercept commit_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - :param is_prepared: boolean, indicates if - :meth:`.TwoPhaseTransaction.prepare` was called. - - """ diff --git a/libs/sqlalchemy/exc.py b/libs/sqlalchemy/exc.py deleted file mode 100644 index fe6879b1..00000000 --- a/libs/sqlalchemy/exc.py +++ /dev/null @@ -1,360 +0,0 @@ -# sqlalchemy/exc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Exceptions used with SQLAlchemy. - -The base exception class is :exc:`.SQLAlchemyError`. Exceptions which are -raised as a result of DBAPI exceptions are all subclasses of -:exc:`.DBAPIError`. - -""" - -import traceback - - -class SQLAlchemyError(Exception): - """Generic error class.""" - - -class ArgumentError(SQLAlchemyError): - """Raised when an invalid or conflicting function argument is supplied. - - This error generally corresponds to construction time state errors. - - """ - - -class NoForeignKeysError(ArgumentError): - """Raised when no foreign keys can be located between two selectables - during a join.""" - - -class AmbiguousForeignKeysError(ArgumentError): - """Raised when more than one foreign key matching can be located - between two selectables during a join.""" - - -class CircularDependencyError(SQLAlchemyError): - """Raised by topological sorts when a circular dependency is detected. - - There are two scenarios where this error occurs: - - * In a Session flush operation, if two objects are mutually dependent - on each other, they can not be inserted or deleted via INSERT or - DELETE statements alone; an UPDATE will be needed to post-associate - or pre-deassociate one of the foreign key constrained values. - The ``post_update`` flag described at :ref:`post_update` can resolve - this cycle. - * In a :meth:`.MetaData.create_all`, :meth:`.MetaData.drop_all`, - :attr:`.MetaData.sorted_tables` operation, two :class:`.ForeignKey` - or :class:`.ForeignKeyConstraint` objects mutually refer to each - other. Apply the ``use_alter=True`` flag to one or both, - see :ref:`use_alter`. - - """ - def __init__(self, message, cycles, edges, msg=None): - if msg is None: - message += " Cycles: %r all edges: %r" % (cycles, edges) - else: - message = msg - SQLAlchemyError.__init__(self, message) - self.cycles = cycles - self.edges = edges - - def __reduce__(self): - return self.__class__, (None, self.cycles, - self.edges, self.args[0]) - - -class CompileError(SQLAlchemyError): - """Raised when an error occurs during SQL compilation""" - -class UnsupportedCompilationError(CompileError): - """Raised when an operation is not supported by the given compiler. - - - .. versionadded:: 0.8.3 - - """ - - def __init__(self, compiler, element_type): - super(UnsupportedCompilationError, self).__init__( - "Compiler %r can't render element of type %s" % - (compiler, element_type)) - -class IdentifierError(SQLAlchemyError): - """Raised when a schema name is beyond the max character limit""" - - -class DisconnectionError(SQLAlchemyError): - """A disconnect is detected on a raw DB-API connection. - - This error is raised and consumed internally by a connection pool. It can - be raised by the :meth:`.PoolEvents.checkout` event so that the host pool - forces a retry; the exception will be caught three times in a row before - the pool gives up and raises :class:`~sqlalchemy.exc.InvalidRequestError` - regarding the connection attempt. - - """ - - -class TimeoutError(SQLAlchemyError): - """Raised when a connection pool times out on getting a connection.""" - - -class InvalidRequestError(SQLAlchemyError): - """SQLAlchemy was asked to do something it can't do. - - This error generally corresponds to runtime state errors. - - """ - - -class NoInspectionAvailable(InvalidRequestError): - """A subject passed to :func:`sqlalchemy.inspection.inspect` produced - no context for inspection.""" - - -class ResourceClosedError(InvalidRequestError): - """An operation was requested from a connection, cursor, or other - object that's in a closed state.""" - - -class NoSuchColumnError(KeyError, InvalidRequestError): - """A nonexistent column is requested from a ``RowProxy``.""" - - -class NoReferenceError(InvalidRequestError): - """Raised by ``ForeignKey`` to indicate a reference cannot be resolved.""" - - -class NoReferencedTableError(NoReferenceError): - """Raised by ``ForeignKey`` when the referred ``Table`` cannot be - located. - - """ - def __init__(self, message, tname): - NoReferenceError.__init__(self, message) - self.table_name = tname - - def __reduce__(self): - return self.__class__, (self.args[0], self.table_name) - - -class NoReferencedColumnError(NoReferenceError): - """Raised by ``ForeignKey`` when the referred ``Column`` cannot be - located. - - """ - def __init__(self, message, tname, cname): - NoReferenceError.__init__(self, message) - self.table_name = tname - self.column_name = cname - - def __reduce__(self): - return self.__class__, (self.args[0], self.table_name, - self.column_name) - - -class NoSuchTableError(InvalidRequestError): - """Table does not exist or is not visible to a connection.""" - - -class UnboundExecutionError(InvalidRequestError): - """SQL was attempted without a database connection to execute it on.""" - - -class DontWrapMixin(object): - """A mixin class which, when applied to a user-defined Exception class, - will not be wrapped inside of :exc:`.StatementError` if the error is - emitted within the process of executing a statement. - - E.g.:: - - from sqlalchemy.exc import DontWrapMixin - - class MyCustomException(Exception, DontWrapMixin): - pass - - class MySpecialType(TypeDecorator): - impl = String - - def process_bind_param(self, value, dialect): - if value == 'invalid': - raise MyCustomException("invalid!") - - """ - -# Moved to orm.exc; compatibility definition installed by orm import until 0.6 -UnmappedColumnError = None - - -class StatementError(SQLAlchemyError): - """An error occurred during execution of a SQL statement. - - :class:`StatementError` wraps the exception raised - during execution, and features :attr:`.statement` - and :attr:`.params` attributes which supply context regarding - the specifics of the statement which had an issue. - - The wrapped exception object is available in - the :attr:`.orig` attribute. - - """ - - statement = None - """The string SQL statement being invoked when this exception occurred.""" - - params = None - """The parameter list being used when this exception occurred.""" - - orig = None - """The DBAPI exception object.""" - - def __init__(self, message, statement, params, orig): - SQLAlchemyError.__init__(self, message) - self.statement = statement - self.params = params - self.orig = orig - self.detail = [] - - def add_detail(self, msg): - self.detail.append(msg) - - def __reduce__(self): - return self.__class__, (self.args[0], self.statement, - self.params, self.orig) - - def __str__(self): - from sqlalchemy.sql import util - params_repr = util._repr_params(self.params, 10) - - return ' '.join([ - "(%s)" % det for det in self.detail - ] + [ - SQLAlchemyError.__str__(self), - repr(self.statement), repr(params_repr) - ]) - - def __unicode__(self): - return self.__str__() - - -class DBAPIError(StatementError): - """Raised when the execution of a database operation fails. - - Wraps exceptions raised by the DB-API underlying the - database operation. Driver-specific implementations of the standard - DB-API exception types are wrapped by matching sub-types of SQLAlchemy's - :class:`DBAPIError` when possible. DB-API's ``Error`` type maps to - :class:`DBAPIError` in SQLAlchemy, otherwise the names are identical. Note - that there is no guarantee that different DB-API implementations will - raise the same exception type for any given error condition. - - :class:`DBAPIError` features :attr:`~.StatementError.statement` - and :attr:`~.StatementError.params` attributes which supply context - regarding the specifics of the statement which had an issue, for the - typical case when the error was raised within the context of - emitting a SQL statement. - - The wrapped exception object is available in the - :attr:`~.StatementError.orig` attribute. Its type and properties are - DB-API implementation specific. - - """ - - @classmethod - def instance(cls, statement, params, - orig, - dbapi_base_err, - connection_invalidated=False): - # Don't ever wrap these, just return them directly as if - # DBAPIError didn't exist. - if isinstance(orig, (KeyboardInterrupt, SystemExit, DontWrapMixin)): - return orig - - if orig is not None: - # not a DBAPI error, statement is present. - # raise a StatementError - if not isinstance(orig, dbapi_base_err) and statement: - msg = traceback.format_exception_only( - orig.__class__, orig)[-1].strip() - return StatementError( - "%s (original cause: %s)" % (str(orig), msg), - statement, params, orig - ) - - name, glob = orig.__class__.__name__, globals() - if name in glob and issubclass(glob[name], DBAPIError): - cls = glob[name] - - return cls(statement, params, orig, connection_invalidated) - - def __reduce__(self): - return self.__class__, (self.statement, self.params, - self.orig, self.connection_invalidated) - - def __init__(self, statement, params, orig, connection_invalidated=False): - try: - text = str(orig) - except (KeyboardInterrupt, SystemExit): - raise - except Exception as e: - text = 'Error in str() of DB-API-generated exception: ' + str(e) - StatementError.__init__( - self, - '(%s) %s' % (orig.__class__.__name__, text), - statement, - params, - orig - ) - self.connection_invalidated = connection_invalidated - - -class InterfaceError(DBAPIError): - """Wraps a DB-API InterfaceError.""" - - -class DatabaseError(DBAPIError): - """Wraps a DB-API DatabaseError.""" - - -class DataError(DatabaseError): - """Wraps a DB-API DataError.""" - - -class OperationalError(DatabaseError): - """Wraps a DB-API OperationalError.""" - - -class IntegrityError(DatabaseError): - """Wraps a DB-API IntegrityError.""" - - -class InternalError(DatabaseError): - """Wraps a DB-API InternalError.""" - - -class ProgrammingError(DatabaseError): - """Wraps a DB-API ProgrammingError.""" - - -class NotSupportedError(DatabaseError): - """Wraps a DB-API NotSupportedError.""" - - -# Warnings - -class SADeprecationWarning(DeprecationWarning): - """Issued once per usage of a deprecated API.""" - - -class SAPendingDeprecationWarning(PendingDeprecationWarning): - """Issued once per usage of a deprecated API.""" - - -class SAWarning(RuntimeWarning): - """Issued at runtime.""" diff --git a/libs/sqlalchemy/ext/__init__.py b/libs/sqlalchemy/ext/__init__.py deleted file mode 100644 index 1d77acaa..00000000 --- a/libs/sqlalchemy/ext/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# ext/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php diff --git a/libs/sqlalchemy/ext/associationproxy.py b/libs/sqlalchemy/ext/associationproxy.py deleted file mode 100644 index e62958b4..00000000 --- a/libs/sqlalchemy/ext/associationproxy.py +++ /dev/null @@ -1,1054 +0,0 @@ -# ext/associationproxy.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Contain the ``AssociationProxy`` class. - -The ``AssociationProxy`` is a Python property object which provides -transparent proxied access to the endpoint of an association object. - -See the example ``examples/association/proxied_association.py``. - -""" -import itertools -import operator -import weakref -from .. import exc, orm, util -from ..orm import collections, interfaces -from ..sql import not_, or_ - - -def association_proxy(target_collection, attr, **kw): - """Return a Python property implementing a view of a target - attribute which references an attribute on members of the - target. - - The returned value is an instance of :class:`.AssociationProxy`. - - Implements a Python property representing a relationship as a collection - of simpler values, or a scalar value. The proxied property will mimic - the collection type of the target (list, dict or set), or, in the case of - a one to one relationship, a simple scalar value. - - :param target_collection: Name of the attribute we'll proxy to. - This attribute is typically mapped by - :func:`~sqlalchemy.orm.relationship` to link to a target collection, but - can also be a many-to-one or non-scalar relationship. - - :param attr: Attribute on the associated instance or instances we'll - proxy for. - - For example, given a target collection of [obj1, obj2], a list created - by this proxy property would look like [getattr(obj1, *attr*), - getattr(obj2, *attr*)] - - If the relationship is one-to-one or otherwise uselist=False, then - simply: getattr(obj, *attr*) - - :param creator: optional. - - When new items are added to this proxied collection, new instances of - the class collected by the target collection will be created. For list - and set collections, the target class constructor will be called with - the 'value' for the new instance. For dict types, two arguments are - passed: key and value. - - If you want to construct instances differently, supply a *creator* - function that takes arguments as above and returns instances. - - For scalar relationships, creator() will be called if the target is None. - If the target is present, set operations are proxied to setattr() on the - associated object. - - If you have an associated object with multiple attributes, you may set - up multiple association proxies mapping to different attributes. See - the unit tests for examples, and for examples of how creator() functions - can be used to construct the scalar relationship on-demand in this - situation. - - :param \*\*kw: Passes along any other keyword arguments to - :class:`.AssociationProxy`. - - """ - return AssociationProxy(target_collection, attr, **kw) - - -ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY') -"""Symbol indicating an :class:`_InspectionAttr` that's - of type :class:`.AssociationProxy`. - - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. - -""" - -class AssociationProxy(interfaces._InspectionAttr): - """A descriptor that presents a read/write view of an object attribute.""" - - is_attribute = False - extension_type = ASSOCIATION_PROXY - - - def __init__(self, target_collection, attr, creator=None, - getset_factory=None, proxy_factory=None, - proxy_bulk_set=None): - """Construct a new :class:`.AssociationProxy`. - - The :func:`.association_proxy` function is provided as the usual - entrypoint here, though :class:`.AssociationProxy` can be instantiated - and/or subclassed directly. - - :param target_collection: Name of the collection we'll proxy to, - usually created with :func:`.relationship`. - - :param attr: Attribute on the collected instances we'll proxy - for. For example, given a target collection of [obj1, obj2], a - list created by this proxy property would look like - [getattr(obj1, attr), getattr(obj2, attr)] - - :param creator: Optional. When new items are added to this proxied - collection, new instances of the class collected by the target - collection will be created. For list and set collections, the - target class constructor will be called with the 'value' for the - new instance. For dict types, two arguments are passed: - key and value. - - If you want to construct instances differently, supply a 'creator' - function that takes arguments as above and returns instances. - - :param getset_factory: Optional. Proxied attribute access is - automatically handled by routines that get and set values based on - the `attr` argument for this proxy. - - If you would like to customize this behavior, you may supply a - `getset_factory` callable that produces a tuple of `getter` and - `setter` functions. The factory is called with two arguments, the - abstract type of the underlying collection and this proxy instance. - - :param proxy_factory: Optional. The type of collection to emulate is - determined by sniffing the target collection. If your collection - type can't be determined by duck typing or you'd like to use a - different collection implementation, you may supply a factory - function to produce those collections. Only applicable to - non-scalar relationships. - - :param proxy_bulk_set: Optional, use with proxy_factory. See - the _set() method for details. - - """ - self.target_collection = target_collection - self.value_attr = attr - self.creator = creator - self.getset_factory = getset_factory - self.proxy_factory = proxy_factory - self.proxy_bulk_set = proxy_bulk_set - - self.owning_class = None - self.key = '_%s_%s_%s' % ( - type(self).__name__, target_collection, id(self)) - self.collection_class = None - - @property - def remote_attr(self): - """The 'remote' :class:`.MapperProperty` referenced by this - :class:`.AssociationProxy`. - - .. versionadded:: 0.7.3 - - See also: - - :attr:`.AssociationProxy.attr` - - :attr:`.AssociationProxy.local_attr` - - """ - return getattr(self.target_class, self.value_attr) - - @property - def local_attr(self): - """The 'local' :class:`.MapperProperty` referenced by this - :class:`.AssociationProxy`. - - .. versionadded:: 0.7.3 - - See also: - - :attr:`.AssociationProxy.attr` - - :attr:`.AssociationProxy.remote_attr` - - """ - return getattr(self.owning_class, self.target_collection) - - @property - def attr(self): - """Return a tuple of ``(local_attr, remote_attr)``. - - This attribute is convenient when specifying a join - using :meth:`.Query.join` across two relationships:: - - sess.query(Parent).join(*Parent.proxied.attr) - - .. versionadded:: 0.7.3 - - See also: - - :attr:`.AssociationProxy.local_attr` - - :attr:`.AssociationProxy.remote_attr` - - """ - return (self.local_attr, self.remote_attr) - - def _get_property(self): - return (orm.class_mapper(self.owning_class). - get_property(self.target_collection)) - - @util.memoized_property - def target_class(self): - """The intermediary class handled by this :class:`.AssociationProxy`. - - Intercepted append/set/assignment events will result - in the generation of new instances of this class. - - """ - return self._get_property().mapper.class_ - - @util.memoized_property - def scalar(self): - """Return ``True`` if this :class:`.AssociationProxy` proxies a scalar - relationship on the local side.""" - - scalar = not self._get_property().uselist - if scalar: - self._initialize_scalar_accessors() - return scalar - - @util.memoized_property - def _value_is_scalar(self): - return not self._get_property().\ - mapper.get_property(self.value_attr).uselist - - @util.memoized_property - def _target_is_object(self): - return getattr(self.target_class, self.value_attr).impl.uses_objects - - def __get__(self, obj, class_): - if self.owning_class is None: - self.owning_class = class_ and class_ or type(obj) - if obj is None: - return self - - if self.scalar: - target = getattr(obj, self.target_collection) - if target is not None: - return self._scalar_get(target) - else: - return None - else: - try: - # If the owning instance is reborn (orm session resurrect, - # etc.), refresh the proxy cache. - creator_id, proxy = getattr(obj, self.key) - if id(obj) == creator_id: - return proxy - except AttributeError: - pass - proxy = self._new(_lazy_collection(obj, self.target_collection)) - setattr(obj, self.key, (id(obj), proxy)) - return proxy - - def __set__(self, obj, values): - if self.owning_class is None: - self.owning_class = type(obj) - - if self.scalar: - creator = self.creator and self.creator or self.target_class - target = getattr(obj, self.target_collection) - if target is None: - setattr(obj, self.target_collection, creator(values)) - else: - self._scalar_set(target, values) - else: - proxy = self.__get__(obj, None) - if proxy is not values: - proxy.clear() - self._set(proxy, values) - - def __delete__(self, obj): - if self.owning_class is None: - self.owning_class = type(obj) - delattr(obj, self.key) - - def _initialize_scalar_accessors(self): - if self.getset_factory: - get, set = self.getset_factory(None, self) - else: - get, set = self._default_getset(None) - self._scalar_get, self._scalar_set = get, set - - def _default_getset(self, collection_class): - attr = self.value_attr - getter = operator.attrgetter(attr) - if collection_class is dict: - setter = lambda o, k, v: setattr(o, attr, v) - else: - setter = lambda o, v: setattr(o, attr, v) - return getter, setter - - def _new(self, lazy_collection): - creator = self.creator and self.creator or self.target_class - self.collection_class = util.duck_type_collection(lazy_collection()) - - if self.proxy_factory: - return self.proxy_factory( - lazy_collection, creator, self.value_attr, self) - - if self.getset_factory: - getter, setter = self.getset_factory(self.collection_class, self) - else: - getter, setter = self._default_getset(self.collection_class) - - if self.collection_class is list: - return _AssociationList( - lazy_collection, creator, getter, setter, self) - elif self.collection_class is dict: - return _AssociationDict( - lazy_collection, creator, getter, setter, self) - elif self.collection_class is set: - return _AssociationSet( - lazy_collection, creator, getter, setter, self) - else: - raise exc.ArgumentError( - 'could not guess which interface to use for ' - 'collection_class "%s" backing "%s"; specify a ' - 'proxy_factory and proxy_bulk_set manually' % - (self.collection_class.__name__, self.target_collection)) - - def _inflate(self, proxy): - creator = self.creator and self.creator or self.target_class - - if self.getset_factory: - getter, setter = self.getset_factory(self.collection_class, self) - else: - getter, setter = self._default_getset(self.collection_class) - - proxy.creator = creator - proxy.getter = getter - proxy.setter = setter - - def _set(self, proxy, values): - if self.proxy_bulk_set: - self.proxy_bulk_set(proxy, values) - elif self.collection_class is list: - proxy.extend(values) - elif self.collection_class is dict: - proxy.update(values) - elif self.collection_class is set: - proxy.update(values) - else: - raise exc.ArgumentError( - 'no proxy_bulk_set supplied for custom ' - 'collection_class implementation') - - @property - def _comparator(self): - return self._get_property().comparator - - def any(self, criterion=None, **kwargs): - """Produce a proxied 'any' expression using EXISTS. - - This expression will be a composed product - using the :meth:`.RelationshipProperty.Comparator.any` - and/or :meth:`.RelationshipProperty.Comparator.has` - operators of the underlying proxied attributes. - - """ - - if self._value_is_scalar: - value_expr = getattr( - self.target_class, self.value_attr).has(criterion, **kwargs) - else: - value_expr = getattr( - self.target_class, self.value_attr).any(criterion, **kwargs) - - # check _value_is_scalar here, otherwise - # we're scalar->scalar - call .any() so that - # the "can't call any() on a scalar" msg is raised. - if self.scalar and not self._value_is_scalar: - return self._comparator.has( - value_expr - ) - else: - return self._comparator.any( - value_expr - ) - - def has(self, criterion=None, **kwargs): - """Produce a proxied 'has' expression using EXISTS. - - This expression will be a composed product - using the :meth:`.RelationshipProperty.Comparator.any` - and/or :meth:`.RelationshipProperty.Comparator.has` - operators of the underlying proxied attributes. - - """ - - if self._target_is_object: - return self._comparator.has( - getattr(self.target_class, self.value_attr).\ - has(criterion, **kwargs) - ) - else: - if criterion is not None or kwargs: - raise exc.ArgumentError( - "Non-empty has() not allowed for " - "column-targeted association proxy; use ==") - return self._comparator.has() - - def contains(self, obj): - """Produce a proxied 'contains' expression using EXISTS. - - This expression will be a composed product - using the :meth:`.RelationshipProperty.Comparator.any` - , :meth:`.RelationshipProperty.Comparator.has`, - and/or :meth:`.RelationshipProperty.Comparator.contains` - operators of the underlying proxied attributes. - """ - - if self.scalar and not self._value_is_scalar: - return self._comparator.has( - getattr(self.target_class, self.value_attr).contains(obj) - ) - else: - return self._comparator.any(**{self.value_attr: obj}) - - def __eq__(self, obj): - # note the has() here will fail for collections; eq_() - # is only allowed with a scalar. - if obj is None: - return or_( - self._comparator.has(**{self.value_attr: obj}), - self._comparator == None - ) - else: - return self._comparator.has(**{self.value_attr: obj}) - - def __ne__(self, obj): - # note the has() here will fail for collections; eq_() - # is only allowed with a scalar. - return self._comparator.has( - getattr(self.target_class, self.value_attr) != obj) - - -class _lazy_collection(object): - def __init__(self, obj, target): - self.ref = weakref.ref(obj) - self.target = target - - def __call__(self): - obj = self.ref() - if obj is None: - raise exc.InvalidRequestError( - "stale association proxy, parent object has gone out of " - "scope") - return getattr(obj, self.target) - - def __getstate__(self): - return {'obj': self.ref(), 'target': self.target} - - def __setstate__(self, state): - self.ref = weakref.ref(state['obj']) - self.target = state['target'] - - -class _AssociationCollection(object): - def __init__(self, lazy_collection, creator, getter, setter, parent): - """Constructs an _AssociationCollection. - - This will always be a subclass of either _AssociationList, - _AssociationSet, or _AssociationDict. - - lazy_collection - A callable returning a list-based collection of entities (usually an - object attribute managed by a SQLAlchemy relationship()) - - creator - A function that creates new target entities. Given one parameter: - value. This assertion is assumed:: - - obj = creator(somevalue) - assert getter(obj) == somevalue - - getter - A function. Given an associated object, return the 'value'. - - setter - A function. Given an associated object and a value, store that - value on the object. - - """ - self.lazy_collection = lazy_collection - self.creator = creator - self.getter = getter - self.setter = setter - self.parent = parent - - col = property(lambda self: self.lazy_collection()) - - def __len__(self): - return len(self.col) - - def __bool__(self): - return bool(self.col) - - __nonzero__ = __bool__ - - def __getstate__(self): - return {'parent': self.parent, 'lazy_collection': self.lazy_collection} - - def __setstate__(self, state): - self.parent = state['parent'] - self.lazy_collection = state['lazy_collection'] - self.parent._inflate(self) - - -class _AssociationList(_AssociationCollection): - """Generic, converting, list-to-list proxy.""" - - def _create(self, value): - return self.creator(value) - - def _get(self, object): - return self.getter(object) - - def _set(self, object, value): - return self.setter(object, value) - - def __getitem__(self, index): - return self._get(self.col[index]) - - def __setitem__(self, index, value): - if not isinstance(index, slice): - self._set(self.col[index], value) - else: - if index.stop is None: - stop = len(self) - elif index.stop < 0: - stop = len(self) + index.stop - else: - stop = index.stop - step = index.step or 1 - - rng = list(range(index.start or 0, stop, step)) - if step == 1: - for i in rng: - del self[index.start] - i = index.start - for item in value: - self.insert(i, item) - i += 1 - else: - if len(value) != len(rng): - raise ValueError( - "attempt to assign sequence of size %s to " - "extended slice of size %s" % (len(value), - len(rng))) - for i, item in zip(rng, value): - self._set(self.col[i], item) - - def __delitem__(self, index): - del self.col[index] - - def __contains__(self, value): - for member in self.col: - # testlib.pragma exempt:__eq__ - if self._get(member) == value: - return True - return False - - def __getslice__(self, start, end): - return [self._get(member) for member in self.col[start:end]] - - def __setslice__(self, start, end, values): - members = [self._create(v) for v in values] - self.col[start:end] = members - - def __delslice__(self, start, end): - del self.col[start:end] - - def __iter__(self): - """Iterate over proxied values. - - For the actual domain objects, iterate over .col instead or - just use the underlying collection directly from its property - on the parent. - """ - - for member in self.col: - yield self._get(member) - raise StopIteration - - def append(self, value): - item = self._create(value) - self.col.append(item) - - def count(self, value): - return sum([1 for _ in - util.itertools_filter(lambda v: v == value, iter(self))]) - - def extend(self, values): - for v in values: - self.append(v) - - def insert(self, index, value): - self.col[index:index] = [self._create(value)] - - def pop(self, index=-1): - return self.getter(self.col.pop(index)) - - def remove(self, value): - for i, val in enumerate(self): - if val == value: - del self.col[i] - return - raise ValueError("value not in list") - - def reverse(self): - """Not supported, use reversed(mylist)""" - - raise NotImplementedError - - def sort(self): - """Not supported, use sorted(mylist)""" - - raise NotImplementedError - - def clear(self): - del self.col[0:len(self.col)] - - def __eq__(self, other): - return list(self) == other - - def __ne__(self, other): - return list(self) != other - - def __lt__(self, other): - return list(self) < other - - def __le__(self, other): - return list(self) <= other - - def __gt__(self, other): - return list(self) > other - - def __ge__(self, other): - return list(self) >= other - - def __cmp__(self, other): - return cmp(list(self), other) - - def __add__(self, iterable): - try: - other = list(iterable) - except TypeError: - return NotImplemented - return list(self) + other - - def __radd__(self, iterable): - try: - other = list(iterable) - except TypeError: - return NotImplemented - return other + list(self) - - def __mul__(self, n): - if not isinstance(n, int): - return NotImplemented - return list(self) * n - __rmul__ = __mul__ - - def __iadd__(self, iterable): - self.extend(iterable) - return self - - def __imul__(self, n): - # unlike a regular list *=, proxied __imul__ will generate unique - # backing objects for each copy. *= on proxied lists is a bit of - # a stretch anyhow, and this interpretation of the __imul__ contract - # is more plausibly useful than copying the backing objects. - if not isinstance(n, int): - return NotImplemented - if n == 0: - self.clear() - elif n > 1: - self.extend(list(self) * (n - 1)) - return self - - def copy(self): - return list(self) - - def __repr__(self): - return repr(list(self)) - - def __hash__(self): - raise TypeError("%s objects are unhashable" % type(self).__name__) - - for func_name, func in list(locals().items()): - if (util.callable(func) and func.__name__ == func_name and - not func.__doc__ and hasattr(list, func_name)): - func.__doc__ = getattr(list, func_name).__doc__ - del func_name, func - - -_NotProvided = util.symbol('_NotProvided') - - -class _AssociationDict(_AssociationCollection): - """Generic, converting, dict-to-dict proxy.""" - - def _create(self, key, value): - return self.creator(key, value) - - def _get(self, object): - return self.getter(object) - - def _set(self, object, key, value): - return self.setter(object, key, value) - - def __getitem__(self, key): - return self._get(self.col[key]) - - def __setitem__(self, key, value): - if key in self.col: - self._set(self.col[key], key, value) - else: - self.col[key] = self._create(key, value) - - def __delitem__(self, key): - del self.col[key] - - def __contains__(self, key): - # testlib.pragma exempt:__hash__ - return key in self.col - - def has_key(self, key): - # testlib.pragma exempt:__hash__ - return key in self.col - - def __iter__(self): - return iter(self.col.keys()) - - def clear(self): - self.col.clear() - - def __eq__(self, other): - return dict(self) == other - - def __ne__(self, other): - return dict(self) != other - - def __lt__(self, other): - return dict(self) < other - - def __le__(self, other): - return dict(self) <= other - - def __gt__(self, other): - return dict(self) > other - - def __ge__(self, other): - return dict(self) >= other - - def __cmp__(self, other): - return cmp(dict(self), other) - - def __repr__(self): - return repr(dict(self.items())) - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - def setdefault(self, key, default=None): - if key not in self.col: - self.col[key] = self._create(key, default) - return default - else: - return self[key] - - def keys(self): - return self.col.keys() - - if util.py2k: - def iteritems(self): - return ((key, self._get(self.col[key])) for key in self.col) - - def itervalues(self): - return (self._get(self.col[key]) for key in self.col) - - def iterkeys(self): - return self.col.iterkeys() - - def values(self): - return [self._get(member) for member in self.col.values()] - - def items(self): - return [(k, self._get(self.col[k])) for k in self] - else: - def items(self): - return ((key, self._get(self.col[key])) for key in self.col) - - def values(self): - return (self._get(self.col[key]) for key in self.col) - - def pop(self, key, default=_NotProvided): - if default is _NotProvided: - member = self.col.pop(key) - else: - member = self.col.pop(key, default) - return self._get(member) - - def popitem(self): - item = self.col.popitem() - return (item[0], self._get(item[1])) - - def update(self, *a, **kw): - if len(a) > 1: - raise TypeError('update expected at most 1 arguments, got %i' % - len(a)) - elif len(a) == 1: - seq_or_map = a[0] - # discern dict from sequence - took the advice from - # http://www.voidspace.org.uk/python/articles/duck_typing.shtml - # still not perfect :( - if hasattr(seq_or_map, 'keys'): - for item in seq_or_map: - self[item] = seq_or_map[item] - else: - try: - for k, v in seq_or_map: - self[k] = v - except ValueError: - raise ValueError( - "dictionary update sequence " - "requires 2-element tuples") - - for key, value in kw: - self[key] = value - - def copy(self): - return dict(self.items()) - - def __hash__(self): - raise TypeError("%s objects are unhashable" % type(self).__name__) - - for func_name, func in list(locals().items()): - if (util.callable(func) and func.__name__ == func_name and - not func.__doc__ and hasattr(dict, func_name)): - func.__doc__ = getattr(dict, func_name).__doc__ - del func_name, func - - -class _AssociationSet(_AssociationCollection): - """Generic, converting, set-to-set proxy.""" - - def _create(self, value): - return self.creator(value) - - def _get(self, object): - return self.getter(object) - - def _set(self, object, value): - return self.setter(object, value) - - def __len__(self): - return len(self.col) - - def __bool__(self): - if self.col: - return True - else: - return False - - __nonzero__ = __bool__ - - def __contains__(self, value): - for member in self.col: - # testlib.pragma exempt:__eq__ - if self._get(member) == value: - return True - return False - - def __iter__(self): - """Iterate over proxied values. - - For the actual domain objects, iterate over .col instead or just use - the underlying collection directly from its property on the parent. - - """ - for member in self.col: - yield self._get(member) - raise StopIteration - - def add(self, value): - if value not in self: - self.col.add(self._create(value)) - - # for discard and remove, choosing a more expensive check strategy rather - # than call self.creator() - def discard(self, value): - for member in self.col: - if self._get(member) == value: - self.col.discard(member) - break - - def remove(self, value): - for member in self.col: - if self._get(member) == value: - self.col.discard(member) - return - raise KeyError(value) - - def pop(self): - if not self.col: - raise KeyError('pop from an empty set') - member = self.col.pop() - return self._get(member) - - def update(self, other): - for value in other: - self.add(value) - - def __ior__(self, other): - if not collections._set_binops_check_strict(self, other): - return NotImplemented - for value in other: - self.add(value) - return self - - def _set(self): - return set(iter(self)) - - def union(self, other): - return set(self).union(other) - - __or__ = union - - def difference(self, other): - return set(self).difference(other) - - __sub__ = difference - - def difference_update(self, other): - for value in other: - self.discard(value) - - def __isub__(self, other): - if not collections._set_binops_check_strict(self, other): - return NotImplemented - for value in other: - self.discard(value) - return self - - def intersection(self, other): - return set(self).intersection(other) - - __and__ = intersection - - def intersection_update(self, other): - want, have = self.intersection(other), set(self) - - remove, add = have - want, want - have - - for value in remove: - self.remove(value) - for value in add: - self.add(value) - - def __iand__(self, other): - if not collections._set_binops_check_strict(self, other): - return NotImplemented - want, have = self.intersection(other), set(self) - - remove, add = have - want, want - have - - for value in remove: - self.remove(value) - for value in add: - self.add(value) - return self - - def symmetric_difference(self, other): - return set(self).symmetric_difference(other) - - __xor__ = symmetric_difference - - def symmetric_difference_update(self, other): - want, have = self.symmetric_difference(other), set(self) - - remove, add = have - want, want - have - - for value in remove: - self.remove(value) - for value in add: - self.add(value) - - def __ixor__(self, other): - if not collections._set_binops_check_strict(self, other): - return NotImplemented - want, have = self.symmetric_difference(other), set(self) - - remove, add = have - want, want - have - - for value in remove: - self.remove(value) - for value in add: - self.add(value) - return self - - def issubset(self, other): - return set(self).issubset(other) - - def issuperset(self, other): - return set(self).issuperset(other) - - def clear(self): - self.col.clear() - - def copy(self): - return set(self) - - def __eq__(self, other): - return set(self) == other - - def __ne__(self, other): - return set(self) != other - - def __lt__(self, other): - return set(self) < other - - def __le__(self, other): - return set(self) <= other - - def __gt__(self, other): - return set(self) > other - - def __ge__(self, other): - return set(self) >= other - - def __repr__(self): - return repr(set(self)) - - def __hash__(self): - raise TypeError("%s objects are unhashable" % type(self).__name__) - - for func_name, func in list(locals().items()): - if (util.callable(func) and func.__name__ == func_name and - not func.__doc__ and hasattr(set, func_name)): - func.__doc__ = getattr(set, func_name).__doc__ - del func_name, func diff --git a/libs/sqlalchemy/ext/automap.py b/libs/sqlalchemy/ext/automap.py deleted file mode 100644 index 7a1512f6..00000000 --- a/libs/sqlalchemy/ext/automap.py +++ /dev/null @@ -1,840 +0,0 @@ -# ext/automap.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system -which automatically generates mapped classes and relationships from a database -schema, typically though not necessarily one which is reflected. - -.. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`. - -.. note:: - - The :mod:`sqlalchemy.ext.automap` extension should be considered - **experimental** as of 0.9.1. Featureset and API stability is - not guaranteed at this time. - -It is hoped that the :class:`.AutomapBase` system provides a quick -and modernized solution to the problem that the very famous -`SQLSoup `_ -also tries to solve, that of generating a quick and rudimentary object -model from an existing database on the fly. By addressing the issue strictly -at the mapper configuration level, and integrating fully with existing -Declarative class techniques, :class:`.AutomapBase` seeks to provide -a well-integrated approach to the issue of expediently auto-generating ad-hoc -mappings. - - -Basic Use -========= - -The simplest usage is to reflect an existing database into a new model. -We create a new :class:`.AutomapBase` class in a similar manner as to how -we create a declarative base class, using :func:`.automap_base`. -We then call :meth:`.AutomapBase.prepare` on the resulting base class, -asking it to reflect the schema and produce mappings:: - - from sqlalchemy.ext.automap import automap_base - from sqlalchemy.orm import Session - from sqlalchemy import create_engine - - Base = automap_base() - - # engine, suppose it has two tables 'user' and 'address' set up - engine = create_engine("sqlite:///mydatabase.db") - - # reflect the tables - Base.prepare(engine, reflect=True) - - # mapped classes are now created with names by default - # matching that of the table name. - User = Base.classes.user - Address = Base.classes.address - - session = Session(engine) - - # rudimentary relationships are produced - session.add(Address(email_address="foo@bar.com", user=User(name="foo"))) - session.commit() - - # collection-based relationships are by default named "_collection" - print (u1.address_collection) - -Above, calling :meth:`.AutomapBase.prepare` while passing along the -:paramref:`.AutomapBase.prepare.reflect` parameter indicates that the -:meth:`.MetaData.reflect` method will be called on this declarative base -classes' :class:`.MetaData` collection; then, each viable -:class:`.Table` within the :class:`.MetaData` will get a new mapped class -generated automatically. The :class:`.ForeignKeyConstraint` objects which -link the various tables together will be used to produce new, bidirectional -:func:`.relationship` objects between classes. The classes and relationships -follow along a default naming scheme that we can customize. At this point, -our basic mapping consisting of related ``User`` and ``Address`` classes is ready -to use in the traditional way. - -Generating Mappings from an Existing MetaData -============================================= - -We can pass a pre-declared :class:`.MetaData` object to :func:`.automap_base`. -This object can be constructed in any way, including programmatically, from -a serialized file, or from itself being reflected using :meth:`.MetaData.reflect`. -Below we illustrate a combination of reflection and explicit table declaration:: - - from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey - engine = create_engine("sqlite:///mydatabase.db") - - # produce our own MetaData object - metadata = MetaData() - - # we can reflect it ourselves from a database, using options - # such as 'only' to limit what tables we look at... - metadata.reflect(engine, only=['user', 'address']) - - # ... or just define our own Table objects with it (or combine both) - Table('user_order', metadata, - Column('id', Integer, primary_key=True), - Column('user_id', ForeignKey('user.id')) - ) - - # we can then produce a set of mappings from this MetaData. - Base = automap_base(metadata=metadata) - - # calling prepare() just sets up mapped classes and relationships. - Base.prepare() - - # mapped classes are ready - User, Address, Order = Base.classes.user, Base.classes.address, Base.classes.user_order - -Specifying Classes Explcitly -============================ - -The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined -explicitly, in a way similar to that of the :class:`.DeferredReflection` class. -Classes that extend from :class:`.AutomapBase` act like regular declarative -classes, but are not immediately mapped after their construction, and are instead -mapped when we call :meth:`.AutomapBase.prepare`. The :meth:`.AutomapBase.prepare` -method will make use of the classes we've established based on the table name -we use. If our schema contains tables ``user`` and ``address``, we can define -one or both of the classes to be used:: - - from sqlalchemy.ext.automap import automap_base - from sqlalchemy import create_engine - - # automap base - Base = automap_base() - - # pre-declare User for the 'user' table - class User(Base): - __tablename__ = 'user' - - # override schema elements like Columns - user_name = Column('name', String) - - # override relationships too, if desired. - # we must use the same name that automap would use for the relationship, - # and also must refer to the class name that automap will generate - # for "address" - address_collection = relationship("address", collection_class=set) - - # reflect - engine = create_engine("sqlite:///mydatabase.db") - Base.prepare(engine, reflect=True) - - # we still have Address generated from the tablename "address", - # but User is the same as Base.classes.User now - - Address = Base.classes.address - - u1 = session.query(User).first() - print (u1.address_collection) - - # the backref is still there: - a1 = session.query(Address).first() - print (a1.user) - -Above, one of the more intricate details is that we illustrated overriding -one of the :func:`.relationship` objects that automap would have created. -To do this, we needed to make sure the names match up with what automap -would normally generate, in that the relationship name would be ``User.address_collection`` -and the name of the class referred to, from automap's perspective, is called -``address``, even though we are referring to it as ``Address`` within our usage -of this class. - -Overriding Naming Schemes -========================= - -:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and -relationship names based on a schema, which means it has decision points in how -these names are determined. These three decision points are provided using -functions which can be passed to the :meth:`.AutomapBase.prepare` method, and -are known as :func:`.classname_for_table`, -:func:`.name_for_scalar_relationship`, -and :func:`.name_for_collection_relationship`. Any or all of these -functions are provided as in the example below, where we use a "camel case" -scheme for class names and a "pluralizer" for collection names using the -`Inflect `_ package:: - - import re - import inflect - - def camelize_classname(base, tablename, table): - "Produce a 'camelized' class name, e.g. " - "'words_and_underscores' -> 'WordsAndUnderscores'" - - return str(tablename[0].upper() + \\ - re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:])) - - _pluralizer = inflect.engine() - def pluralize_collection(base, local_cls, referred_cls, constraint): - "Produce an 'uncamelized', 'pluralized' class name, e.g. " - "'SomeTerm' -> 'some_terms'" - - referred_name = referred_cls.__name__ - uncamelized = referred_name[0].lower() + \\ - re.sub(r'\W', - lambda m: "_%s" % m.group(0).lower(), - referred_name[1:]) - pluralized = _pluralizer.plural(uncamelized) - return pluralized - - from sqlalchemy.ext.automap import automap_base - - Base = automap_base() - - engine = create_engine("sqlite:///mydatabase.db") - - Base.prepare(engine, reflect=True, - classname_for_table=camelize_classname, - name_for_collection_relationship=pluralize_collection - ) - -From the above mapping, we would now have classes ``User`` and ``Address``, -where the collection from ``User`` to ``Address`` is called ``User.addresses``:: - - User, Address = Base.classes.User, Base.classes.Address - - u1 = User(addresses=[Address(email="foo@bar.com")]) - -Relationship Detection -====================== - -The vast majority of what automap accomplishes is the generation of -:func:`.relationship` structures based on foreign keys. The mechanism -by which this works for many-to-one and one-to-many relationships is as follows: - -1. A given :class:`.Table`, known to be mapped to a particular class, - is examined for :class:`.ForeignKeyConstraint` objects. - -2. From each :class:`.ForeignKeyConstraint`, the remote :class:`.Table` - object present is matched up to the class to which it is to be mapped, - if any, else it is skipped. - -3. As the :class:`.ForeignKeyConstraint` we are examining correponds to a reference - from the immediate mapped class, - the relationship will be set up as a many-to-one referring to the referred class; - a corresponding one-to-many backref will be created on the referred class referring - to this class. - -4. The names of the relationships are determined using the - :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and - :paramref:`.AutomapBase.prepare.name_for_collection_relationship` - callable functions. It is important to note that the default relationship - naming derives the name from the **the actual class name**. If you've - given a particular class an explicit name by declaring it, or specified an - alternate class naming scheme, that's the name from which the relationship - name will be derived. - -5. The classes are inspected for an existing mapped property matching these - names. If one is detected on one side, but none on the other side, :class:`.AutomapBase` - attempts to create a relationship on the missing side, then uses the - :paramref:`.relationship.back_populates` parameter in order to point - the new relationship to the other side. - -6. In the usual case where no relationship is on either side, - :meth:`.AutomapBase.prepare` produces a :func:`.relationship` on the "many-to-one" - side and matches it to the other using the :paramref:`.relationship.backref` - parameter. - -7. Production of the :func:`.relationship` and optionally the :func:`.backref` - is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship` - function, which can be supplied by the end-user in order to augment - the arguments passed to :func:`.relationship` or :func:`.backref` or to - make use of custom implementations of these functions. - -Custom Relationship Arguments ------------------------------ - -The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used -to add parameters to relationships. For most cases, we can make use of the -existing :func:`.automap.generate_relationship` function to return -the object, after augmenting the given keyword dictionary with our own -arguments. - -Below is an illustration of how to send -:paramref:`.relationship.cascade` and -:paramref:`.relationship.passive_deletes` -options along to all one-to-many relationships:: - - from sqlalchemy.ext.automap import generate_relationship - - def _gen_relationship(base, direction, return_fn, - attrname, local_cls, referred_cls, **kw): - if direction is interfaces.ONETOMANY: - kw['cascade'] = 'all, delete-orphan' - kw['passive_deletes'] = True - # make use of the built-in function to actually return - # the result. - return generate_relationship(base, direction, return_fn, - attrname, local_cls, referred_cls, **kw) - - from sqlalchemy.ext.automap import automap_base - from sqlalchemy import create_engine - - # automap base - Base = automap_base() - - engine = create_engine("sqlite:///mydatabase.db") - Base.prepare(engine, reflect=True, - generate_relationship=_gen_relationship) - -Many-to-Many relationships --------------------------- - -:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g. -those which contain a ``secondary`` argument. The process for producing these -is as follows: - -1. A given :class:`.Table` is examined for :class:`.ForeignKeyConstraint` objects, - before any mapped class has been assigned to it. - -2. If the table contains two and exactly two :class:`.ForeignKeyConstraint` - objects, and all columns within this table are members of these two - :class:`.ForeignKeyConstraint` objects, the table is assumed to be a - "secondary" table, and will **not be mapped directly**. - -3. The two (or one, for self-referential) external tables to which the :class:`.Table` - refers to are matched to the classes to which they will be mapped, if any. - -4. If mapped classes for both sides are located, a many-to-many bi-directional - :func:`.relationship` / :func:`.backref` pair is created between the two - classes. - -5. The override logic for many-to-many works the same as that of one-to-many/ - many-to-one; the :func:`.generate_relationship` function is called upon - to generate the strucures and existing attributes will be maintained. - -Using Automap with Explicit Declarations -======================================== - -As noted previously, automap has no dependency on reflection, and can make -use of any collection of :class:`.Table` objects within a :class:`.MetaData` -collection. From this, it follows that automap can also be used -generate missing relationships given an otherwise complete model that fully defines -table metadata:: - - from sqlalchemy.ext.automap import automap_base - from sqlalchemy import Column, Integer, String, ForeignKey - - Base = automap_base() - - class User(Base): - __tablename__ = 'user' - - id = Column(Integer, primary_key=True) - name = Column(String) - - class Address(Base): - __tablename__ = 'address' - - id = Column(Integer, primary_key=True) - email = Column(String) - user_id = Column(ForeignKey('user.id')) - - # produce relationships - Base.prepare() - - # mapping is complete, with "address_collection" and - # "user" relationships - a1 = Address(email='u1') - a2 = Address(email='u2') - u1 = User(address_collection=[a1, a2]) - assert a1.user is u1 - -Above, given mostly complete ``User`` and ``Address`` mappings, the -:class:`.ForeignKey` which we defined on ``Address.user_id`` allowed a -bidirectional relationship pair ``Address.user`` and ``User.address_collection`` -to be generated on the mapped classes. - -Note that when subclassing :class:`.AutomapBase`, the :meth:`.AutomapBase.prepare` -method is required; if not called, the classes we've declared are in an -un-mapped state. - - -""" -from .declarative import declarative_base as _declarative_base -from .declarative.base import _DeferredMapperConfig -from ..sql import and_ -from ..schema import ForeignKeyConstraint -from ..orm import relationship, backref, interfaces -from .. import util - - -def classname_for_table(base, tablename, table): - """Return the class name that should be used, given the name - of a table. - - The default implementation is:: - - return str(tablename) - - Alternate implementations can be specified using the - :paramref:`.AutomapBase.prepare.classname_for_table` - parameter. - - :param base: the :class:`.AutomapBase` class doing the prepare. - - :param tablename: string name of the :class:`.Table`. - - :param table: the :class:`.Table` object itself. - - :return: a string class name. - - .. note:: - - In Python 2, the string used for the class name **must** be a non-Unicode - object, e.g. a ``str()`` object. The ``.name`` attribute of - :class:`.Table` is typically a Python unicode subclass, so the ``str()`` - function should be applied to this name, after accounting for any non-ASCII - characters. - - """ - return str(tablename) - -def name_for_scalar_relationship(base, local_cls, referred_cls, constraint): - """Return the attribute name that should be used to refer from one - class to another, for a scalar object reference. - - The default implementation is:: - - return referred_cls.__name__.lower() - - Alternate implementations can be specified using the - :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` - parameter. - - :param base: the :class:`.AutomapBase` class doing the prepare. - - :param local_cls: the class to be mapped on the local side. - - :param referred_cls: the class to be mapped on the referring side. - - :param constraint: the :class:`.ForeignKeyConstraint` that is being - inspected to produce this relationship. - - """ - return referred_cls.__name__.lower() - -def name_for_collection_relationship(base, local_cls, referred_cls, constraint): - """Return the attribute name that should be used to refer from one - class to another, for a collection reference. - - The default implementation is:: - - return referred_cls.__name__.lower() + "_collection" - - Alternate implementations - can be specified using the :paramref:`.AutomapBase.prepare.name_for_collection_relationship` - parameter. - - :param base: the :class:`.AutomapBase` class doing the prepare. - - :param local_cls: the class to be mapped on the local side. - - :param referred_cls: the class to be mapped on the referring side. - - :param constraint: the :class:`.ForeignKeyConstraint` that is being - inspected to produce this relationship. - - """ - return referred_cls.__name__.lower() + "_collection" - -def generate_relationship(base, direction, return_fn, attrname, local_cls, referred_cls, **kw): - """Generate a :func:`.relationship` or :func:`.backref` on behalf of two - mapped classes. - - An alternate implementation of this function can be specified using the - :paramref:`.AutomapBase.prepare.generate_relationship` parameter. - - The default implementation of this function is as follows:: - - if return_fn is backref: - return return_fn(attrname, **kw) - elif return_fn is relationship: - return return_fn(referred_cls, **kw) - else: - raise TypeError("Unknown relationship function: %s" % return_fn) - - :param base: the :class:`.AutomapBase` class doing the prepare. - - :param direction: indicate the "direction" of the relationship; this will - be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOONE`. - - :param return_fn: the function that is used by default to create the - relationship. This will be either :func:`.relationship` or :func:`.backref`. - The :func:`.backref` function's result will be used to produce a new - :func:`.relationship` in a second step, so it is critical that user-defined - implementations correctly differentiate between the two functions, if - a custom relationship function is being used. - - :attrname: the attribute name to which this relationship is being assigned. - If the value of :paramref:`.generate_relationship.return_fn` is the - :func:`.backref` function, then this name is the name that is being - assigned to the backref. - - :param local_cls: the "local" class to which this relationship or backref - will be locally present. - - :param referred_cls: the "referred" class to which the relationship or backref - refers to. - - :param \**kw: all additional keyword arguments are passed along to the - function. - - :return: a :func:`.relationship` or :func:`.backref` construct, as dictated - by the :paramref:`.generate_relationship.return_fn` parameter. - - """ - if return_fn is backref: - return return_fn(attrname, **kw) - elif return_fn is relationship: - return return_fn(referred_cls, **kw) - else: - raise TypeError("Unknown relationship function: %s" % return_fn) - -class AutomapBase(object): - """Base class for an "automap" schema. - - The :class:`.AutomapBase` class can be compared to the "declarative base" - class that is produced by the :func:`.declarative.declarative_base` - function. In practice, the :class:`.AutomapBase` class is always used - as a mixin along with an actual declarative base. - - A new subclassable :class:`.AutomapBase` is typically instantated - using the :func:`.automap_base` function. - - .. seealso:: - - :ref:`automap_toplevel` - - """ - __abstract__ = True - - classes = None - """An instance of :class:`.util.Properties` containing classes. - - This object behaves much like the ``.c`` collection on a table. Classes - are present under the name they were given, e.g.:: - - Base = automap_base() - Base.prepare(engine=some_engine, reflect=True) - - User, Address = Base.classes.User, Base.classes.Address - - """ - - @classmethod - def prepare(cls, - engine=None, - reflect=False, - classname_for_table=classname_for_table, - collection_class=list, - name_for_scalar_relationship=name_for_scalar_relationship, - name_for_collection_relationship=name_for_collection_relationship, - generate_relationship=generate_relationship): - - """Extract mapped classes and relationships from the :class:`.MetaData` and - perform mappings. - - :param engine: an :class:`.Engine` or :class:`.Connection` with which - to perform schema reflection, if specified. - If the :paramref:`.AutomapBase.prepare.reflect` argument is False, this - object is not used. - - :param reflect: if True, the :meth:`.MetaData.reflect` method is called - on the :class:`.MetaData` associated with this :class:`.AutomapBase`. - The :class:`.Engine` passed via :paramref:`.AutomapBase.prepare.engine` will - be used to perform the reflection if present; else, the :class:`.MetaData` - should already be bound to some engine else the operation will fail. - - :param classname_for_table: callable function which will be used to - produce new class names, given a table name. Defaults to - :func:`.classname_for_table`. - - :param name_for_scalar_relationship: callable function which will be used - to produce relationship names for scalar relationships. Defaults to - :func:`.name_for_scalar_relationship`. - - :param name_for_collection_relationship: callable function which will be used - to produce relationship names for collection-oriented relationships. Defaults to - :func:`.name_for_collection_relationship`. - - :param generate_relationship: callable function which will be used to - actually generate :func:`.relationship` and :func:`.backref` constructs. - Defaults to :func:`.generate_relationship`. - - :param collection_class: the Python collection class that will be used - when a new :func:`.relationship` object is created that represents a - collection. Defaults to ``list``. - - """ - if reflect: - cls.metadata.reflect( - engine, - extend_existing=True, - autoload_replace=False - ) - - table_to_map_config = dict( - (m.local_table, m) - for m in _DeferredMapperConfig.classes_for_base(cls) - ) - - many_to_many = [] - - for table in cls.metadata.tables.values(): - lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table) - if lcl_m2m is not None: - many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table)) - elif not table.primary_key: - continue - elif table not in table_to_map_config: - mapped_cls = type( - classname_for_table(cls, table.name, table), - (cls, ), - {"__table__": table} - ) - map_config = _DeferredMapperConfig.config_for_cls(mapped_cls) - cls.classes[map_config.cls.__name__] = mapped_cls - table_to_map_config[table] = map_config - - for map_config in table_to_map_config.values(): - _relationships_for_fks(cls, - map_config, - table_to_map_config, - collection_class, - name_for_scalar_relationship, - name_for_collection_relationship, - generate_relationship) - - for lcl_m2m, rem_m2m, m2m_const, table in many_to_many: - _m2m_relationship(cls, lcl_m2m, rem_m2m, m2m_const, table, - table_to_map_config, - collection_class, - name_for_scalar_relationship, - name_for_collection_relationship, - generate_relationship) - for map_config in table_to_map_config.values(): - map_config.map() - - - _sa_decl_prepare = True - """Indicate that the mapping of classes should be deferred. - - The presence of this attribute name indicates to declarative - that the call to mapper() should not occur immediately; instead, - information about the table and attributes to be mapped are gathered - into an internal structure called _DeferredMapperConfig. These - objects can be collected later using classes_for_base(), additional - mapping decisions can be made, and then the map() method will actually - apply the mapping. - - The only real reason this deferral of the whole - thing is needed is to support primary key columns that aren't reflected - yet when the class is declared; everything else can theoretically be - added to the mapper later. However, the _DeferredMapperConfig is a - nice interface in any case which exists at that not usually exposed point - at which declarative has the class and the Table but hasn't called - mapper() yet. - - """ - -def automap_base(declarative_base=None, **kw): - """Produce a declarative automap base. - - This function produces a new base class that is a product of the - :class:`.AutomapBase` class as well a declarative base produced by - :func:`.declarative.declarative_base`. - - All parameters other than ``declarative_base`` are keyword arguments - that are passed directly to the :func:`.declarative.declarative_base` - function. - - :param declarative_base: an existing class produced by - :func:`.declarative.declarative_base`. When this is passed, the function - no longer invokes :func:`.declarative.declarative_base` itself, and all other - keyword arguments are ignored. - - :param \**kw: keyword arguments are passed along to - :func:`.declarative.declarative_base`. - - """ - if declarative_base is None: - Base = _declarative_base(**kw) - else: - Base = declarative_base - - return type( - Base.__name__, - (AutomapBase, Base,), - {"__abstract__": True, "classes": util.Properties({})} - ) - -def _is_many_to_many(automap_base, table): - fk_constraints = [const for const in table.constraints - if isinstance(const, ForeignKeyConstraint)] - if len(fk_constraints) != 2: - return None, None, None - - cols = sum( - [[fk.parent for fk in fk_constraint.elements] - for fk_constraint in fk_constraints], []) - - if set(cols) != set(table.c): - return None, None, None - - return ( - fk_constraints[0].elements[0].column.table, - fk_constraints[1].elements[0].column.table, - fk_constraints - ) - -def _relationships_for_fks(automap_base, map_config, table_to_map_config, - collection_class, - name_for_scalar_relationship, - name_for_collection_relationship, - generate_relationship): - local_table = map_config.local_table - local_cls = map_config.cls - - for constraint in local_table.constraints: - if isinstance(constraint, ForeignKeyConstraint): - fks = constraint.elements - referred_table = fks[0].column.table - referred_cfg = table_to_map_config.get(referred_table, None) - if referred_cfg is None: - continue - referred_cls = referred_cfg.cls - - relationship_name = name_for_scalar_relationship( - automap_base, - local_cls, - referred_cls, constraint) - backref_name = name_for_collection_relationship( - automap_base, - referred_cls, - local_cls, - constraint - ) - - create_backref = backref_name not in referred_cfg.properties - - if relationship_name not in map_config.properties: - if create_backref: - backref_obj = generate_relationship(automap_base, - interfaces.ONETOMANY, backref, - backref_name, referred_cls, local_cls, - collection_class=collection_class) - else: - backref_obj = None - map_config.properties[relationship_name] = \ - generate_relationship(automap_base, - interfaces.MANYTOONE, - relationship, - relationship_name, - local_cls, referred_cls, - foreign_keys=[fk.parent for fk in constraint.elements], - backref=backref_obj, - remote_side=[fk.column for fk in constraint.elements] - ) - if not create_backref: - referred_cfg.properties[backref_name].back_populates = relationship_name - elif create_backref: - referred_cfg.properties[backref_name] = \ - generate_relationship(automap_base, - interfaces.ONETOMANY, - relationship, - backref_name, - referred_cls, local_cls, - foreign_keys=[fk.parent for fk in constraint.elements], - back_populates=relationship_name, - collection_class=collection_class) - map_config.properties[relationship_name].back_populates = backref_name - -def _m2m_relationship(automap_base, lcl_m2m, rem_m2m, m2m_const, table, - table_to_map_config, - collection_class, - name_for_scalar_relationship, - name_for_collection_relationship, - generate_relationship): - - map_config = table_to_map_config.get(lcl_m2m, None) - referred_cfg = table_to_map_config.get(rem_m2m, None) - if map_config is None or referred_cfg is None: - return - - local_cls = map_config.cls - referred_cls = referred_cfg.cls - - relationship_name = name_for_collection_relationship( - automap_base, - local_cls, - referred_cls, m2m_const[0]) - backref_name = name_for_collection_relationship( - automap_base, - referred_cls, - local_cls, - m2m_const[1] - ) - - create_backref = backref_name not in referred_cfg.properties - - if relationship_name not in map_config.properties: - if create_backref: - backref_obj = generate_relationship(automap_base, - interfaces.MANYTOMANY, - backref, - backref_name, - referred_cls, local_cls, - collection_class=collection_class - ) - else: - backref_obj = None - map_config.properties[relationship_name] = \ - generate_relationship(automap_base, - interfaces.MANYTOMANY, - relationship, - relationship_name, - local_cls, referred_cls, - secondary=table, - primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements), - secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements), - backref=backref_obj, - collection_class=collection_class - ) - if not create_backref: - referred_cfg.properties[backref_name].back_populates = relationship_name - elif create_backref: - referred_cfg.properties[backref_name] = \ - generate_relationship(automap_base, - interfaces.MANYTOMANY, - relationship, - backref_name, - referred_cls, local_cls, - secondary=table, - primaryjoin=and_(fk.column == fk.parent for fk in m2m_const[1].elements), - secondaryjoin=and_(fk.column == fk.parent for fk in m2m_const[0].elements), - back_populates=relationship_name, - collection_class=collection_class) - map_config.properties[relationship_name].back_populates = backref_name diff --git a/libs/sqlalchemy/ext/compiler.py b/libs/sqlalchemy/ext/compiler.py deleted file mode 100644 index 5dde74e0..00000000 --- a/libs/sqlalchemy/ext/compiler.py +++ /dev/null @@ -1,448 +0,0 @@ -# ext/compiler.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provides an API for creation of custom ClauseElements and compilers. - -Synopsis -======== - -Usage involves the creation of one or more -:class:`~sqlalchemy.sql.expression.ClauseElement` subclasses and one or -more callables defining its compilation:: - - from sqlalchemy.ext.compiler import compiles - from sqlalchemy.sql.expression import ColumnClause - - class MyColumn(ColumnClause): - pass - - @compiles(MyColumn) - def compile_mycolumn(element, compiler, **kw): - return "[%s]" % element.name - -Above, ``MyColumn`` extends :class:`~sqlalchemy.sql.expression.ColumnClause`, -the base expression element for named column objects. The ``compiles`` -decorator registers itself with the ``MyColumn`` class so that it is invoked -when the object is compiled to a string:: - - from sqlalchemy import select - - s = select([MyColumn('x'), MyColumn('y')]) - print str(s) - -Produces:: - - SELECT [x], [y] - -Dialect-specific compilation rules -================================== - -Compilers can also be made dialect-specific. The appropriate compiler will be -invoked for the dialect in use:: - - from sqlalchemy.schema import DDLElement - - class AlterColumn(DDLElement): - - def __init__(self, column, cmd): - self.column = column - self.cmd = cmd - - @compiles(AlterColumn) - def visit_alter_column(element, compiler, **kw): - return "ALTER COLUMN %s ..." % element.column.name - - @compiles(AlterColumn, 'postgresql') - def visit_alter_column(element, compiler, **kw): - return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name) - -The second ``visit_alter_table`` will be invoked when any ``postgresql`` -dialect is used. - -Compiling sub-elements of a custom expression construct -======================================================= - -The ``compiler`` argument is the -:class:`~sqlalchemy.engine.interfaces.Compiled` object in use. This object -can be inspected for any information about the in-progress compilation, -including ``compiler.dialect``, ``compiler.statement`` etc. The -:class:`~sqlalchemy.sql.compiler.SQLCompiler` and -:class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()`` -method which can be used for compilation of embedded attributes:: - - from sqlalchemy.sql.expression import Executable, ClauseElement - - class InsertFromSelect(Executable, ClauseElement): - def __init__(self, table, select): - self.table = table - self.select = select - - @compiles(InsertFromSelect) - def visit_insert_from_select(element, compiler, **kw): - return "INSERT INTO %s (%s)" % ( - compiler.process(element.table, asfrom=True), - compiler.process(element.select) - ) - - insert = InsertFromSelect(t1, select([t1]).where(t1.c.x>5)) - print insert - -Produces:: - - "INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)" - -.. note:: - - The above ``InsertFromSelect`` construct is only an example, this actual - functionality is already available using the - :meth:`.Insert.from_select` method. - -.. note:: - - The above ``InsertFromSelect`` construct probably wants to have "autocommit" - enabled. See :ref:`enabling_compiled_autocommit` for this step. - -Cross Compiling between SQL and DDL compilers ---------------------------------------------- - -SQL and DDL constructs are each compiled using different base compilers - -``SQLCompiler`` and ``DDLCompiler``. A common need is to access the -compilation rules of SQL expressions from within a DDL expression. The -``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as -below where we generate a CHECK constraint that embeds a SQL expression:: - - @compiles(MyConstraint) - def compile_my_constraint(constraint, ddlcompiler, **kw): - return "CONSTRAINT %s CHECK (%s)" % ( - constraint.name, - ddlcompiler.sql_compiler.process(constraint.expression) - ) - -.. _enabling_compiled_autocommit: - -Enabling Autocommit on a Construct -================================== - -Recall from the section :ref:`autocommit` that the :class:`.Engine`, when -asked to execute a construct in the absence of a user-defined transaction, -detects if the given construct represents DML or DDL, that is, a data -modification or data definition statement, which requires (or may require, -in the case of DDL) that the transaction generated by the DBAPI be committed -(recall that DBAPI always has a transaction going on regardless of what -SQLAlchemy does). Checking for this is actually accomplished by checking for -the "autocommit" execution option on the construct. When building a -construct like an INSERT derivation, a new DDL type, or perhaps a stored -procedure that alters data, the "autocommit" option needs to be set in order -for the statement to function with "connectionless" execution -(as described in :ref:`dbengine_implicit`). - -Currently a quick way to do this is to subclass :class:`.Executable`, then -add the "autocommit" flag to the ``_execution_options`` dictionary (note this -is a "frozen" dictionary which supplies a generative ``union()`` method):: - - from sqlalchemy.sql.expression import Executable, ClauseElement - - class MyInsertThing(Executable, ClauseElement): - _execution_options = \\ - Executable._execution_options.union({'autocommit': True}) - -More succinctly, if the construct is truly similar to an INSERT, UPDATE, or -DELETE, :class:`.UpdateBase` can be used, which already is a subclass -of :class:`.Executable`, :class:`.ClauseElement` and includes the -``autocommit`` flag:: - - from sqlalchemy.sql.expression import UpdateBase - - class MyInsertThing(UpdateBase): - def __init__(self, ...): - ... - - - - -DDL elements that subclass :class:`.DDLElement` already have the -"autocommit" flag turned on. - - - - -Changing the default compilation of existing constructs -======================================================= - -The compiler extension applies just as well to the existing constructs. When -overriding the compilation of a built in SQL construct, the @compiles -decorator is invoked upon the appropriate class (be sure to use the class, -i.e. ``Insert`` or ``Select``, instead of the creation function such -as ``insert()`` or ``select()``). - -Within the new compilation function, to get at the "original" compilation -routine, use the appropriate visit_XXX method - this -because compiler.process() will call upon the overriding routine and cause -an endless loop. Such as, to add "prefix" to all insert statements:: - - from sqlalchemy.sql.expression import Insert - - @compiles(Insert) - def prefix_inserts(insert, compiler, **kw): - return compiler.visit_insert(insert.prefix_with("some prefix"), **kw) - -The above compiler will prefix all INSERT statements with "some prefix" when -compiled. - -.. _type_compilation_extension: - -Changing Compilation of Types -============================= - -``compiler`` works for types, too, such as below where we implement the -MS-SQL specific 'max' keyword for ``String``/``VARCHAR``:: - - @compiles(String, 'mssql') - @compiles(VARCHAR, 'mssql') - def compile_varchar(element, compiler, **kw): - if element.length == 'max': - return "VARCHAR('max')" - else: - return compiler.visit_VARCHAR(element, **kw) - - foo = Table('foo', metadata, - Column('data', VARCHAR('max')) - ) - -Subclassing Guidelines -====================== - -A big part of using the compiler extension is subclassing SQLAlchemy -expression constructs. To make this easier, the expression and -schema packages feature a set of "bases" intended for common tasks. -A synopsis is as follows: - -* :class:`~sqlalchemy.sql.expression.ClauseElement` - This is the root - expression class. Any SQL expression can be derived from this base, and is - probably the best choice for longer constructs such as specialized INSERT - statements. - -* :class:`~sqlalchemy.sql.expression.ColumnElement` - The root of all - "column-like" elements. Anything that you'd place in the "columns" clause of - a SELECT statement (as well as order by and group by) can derive from this - - the object will automatically have Python "comparison" behavior. - - :class:`~sqlalchemy.sql.expression.ColumnElement` classes want to have a - ``type`` member which is expression's return type. This can be established - at the instance level in the constructor, or at the class level if its - generally constant:: - - class timestamp(ColumnElement): - type = TIMESTAMP() - -* :class:`~sqlalchemy.sql.functions.FunctionElement` - This is a hybrid of a - ``ColumnElement`` and a "from clause" like object, and represents a SQL - function or stored procedure type of call. Since most databases support - statements along the line of "SELECT FROM " - ``FunctionElement`` adds in the ability to be used in the FROM clause of a - ``select()`` construct:: - - from sqlalchemy.sql.expression import FunctionElement - - class coalesce(FunctionElement): - name = 'coalesce' - - @compiles(coalesce) - def compile(element, compiler, **kw): - return "coalesce(%s)" % compiler.process(element.clauses) - - @compiles(coalesce, 'oracle') - def compile(element, compiler, **kw): - if len(element.clauses) > 2: - raise TypeError("coalesce only supports two arguments on Oracle") - return "nvl(%s)" % compiler.process(element.clauses) - -* :class:`~sqlalchemy.schema.DDLElement` - The root of all DDL expressions, - like CREATE TABLE, ALTER TABLE, etc. Compilation of ``DDLElement`` - subclasses is issued by a ``DDLCompiler`` instead of a ``SQLCompiler``. - ``DDLElement`` also features ``Table`` and ``MetaData`` event hooks via the - ``execute_at()`` method, allowing the construct to be invoked during CREATE - TABLE and DROP TABLE sequences. - -* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which - should be used with any expression class that represents a "standalone" - SQL statement that can be passed directly to an ``execute()`` method. It - is already implicit within ``DDLElement`` and ``FunctionElement``. - -Further Examples -================ - -"UTC timestamp" function -------------------------- - -A function that works like "CURRENT_TIMESTAMP" except applies the -appropriate conversions so that the time is in UTC time. Timestamps are best -stored in relational databases as UTC, without time zones. UTC so that your -database doesn't think time has gone backwards in the hour when daylight -savings ends, without timezones because timezones are like character -encodings - they're best applied only at the endpoints of an application -(i.e. convert to UTC upon user input, re-apply desired timezone upon display). - -For Postgresql and Microsoft SQL Server:: - - from sqlalchemy.sql import expression - from sqlalchemy.ext.compiler import compiles - from sqlalchemy.types import DateTime - - class utcnow(expression.FunctionElement): - type = DateTime() - - @compiles(utcnow, 'postgresql') - def pg_utcnow(element, compiler, **kw): - return "TIMEZONE('utc', CURRENT_TIMESTAMP)" - - @compiles(utcnow, 'mssql') - def ms_utcnow(element, compiler, **kw): - return "GETUTCDATE()" - -Example usage:: - - from sqlalchemy import ( - Table, Column, Integer, String, DateTime, MetaData - ) - metadata = MetaData() - event = Table("event", metadata, - Column("id", Integer, primary_key=True), - Column("description", String(50), nullable=False), - Column("timestamp", DateTime, server_default=utcnow()) - ) - -"GREATEST" function -------------------- - -The "GREATEST" function is given any number of arguments and returns the one -that is of the highest value - it's equivalent to Python's ``max`` -function. A SQL standard version versus a CASE based version which only -accommodates two arguments:: - - from sqlalchemy.sql import expression - from sqlalchemy.ext.compiler import compiles - from sqlalchemy.types import Numeric - - class greatest(expression.FunctionElement): - type = Numeric() - name = 'greatest' - - @compiles(greatest) - def default_greatest(element, compiler, **kw): - return compiler.visit_function(element) - - @compiles(greatest, 'sqlite') - @compiles(greatest, 'mssql') - @compiles(greatest, 'oracle') - def case_greatest(element, compiler, **kw): - arg1, arg2 = list(element.clauses) - return "CASE WHEN %s > %s THEN %s ELSE %s END" % ( - compiler.process(arg1), - compiler.process(arg2), - compiler.process(arg1), - compiler.process(arg2), - ) - -Example usage:: - - Session.query(Account).\\ - filter( - greatest( - Account.checking_balance, - Account.savings_balance) > 10000 - ) - -"false" expression ------------------- - -Render a "false" constant expression, rendering as "0" on platforms that -don't have a "false" constant:: - - from sqlalchemy.sql import expression - from sqlalchemy.ext.compiler import compiles - - class sql_false(expression.ColumnElement): - pass - - @compiles(sql_false) - def default_false(element, compiler, **kw): - return "false" - - @compiles(sql_false, 'mssql') - @compiles(sql_false, 'mysql') - @compiles(sql_false, 'oracle') - def int_false(element, compiler, **kw): - return "0" - -Example usage:: - - from sqlalchemy import select, union_all - - exp = union_all( - select([users.c.name, sql_false().label("enrolled")]), - select([customers.c.name, customers.c.enrolled]) - ) - -""" -from .. import exc -from ..sql import visitors - - -def compiles(class_, *specs): - """Register a function as a compiler for a - given :class:`.ClauseElement` type.""" - - def decorate(fn): - existing = class_.__dict__.get('_compiler_dispatcher', None) - existing_dispatch = class_.__dict__.get('_compiler_dispatch') - if not existing: - existing = _dispatcher() - - if existing_dispatch: - existing.specs['default'] = existing_dispatch - - # TODO: why is the lambda needed ? - setattr(class_, '_compiler_dispatch', - lambda *arg, **kw: existing(*arg, **kw)) - setattr(class_, '_compiler_dispatcher', existing) - - if specs: - for s in specs: - existing.specs[s] = fn - - else: - existing.specs['default'] = fn - return fn - return decorate - - -def deregister(class_): - """Remove all custom compilers associated with a given - :class:`.ClauseElement` type.""" - - if hasattr(class_, '_compiler_dispatcher'): - # regenerate default _compiler_dispatch - visitors._generate_dispatch(class_) - # remove custom directive - del class_._compiler_dispatcher - - -class _dispatcher(object): - def __init__(self): - self.specs = {} - - def __call__(self, element, compiler, **kw): - # TODO: yes, this could also switch off of DBAPI in use. - fn = self.specs.get(compiler.dialect.name, None) - if not fn: - try: - fn = self.specs['default'] - except KeyError: - raise exc.CompileError( - "%s construct has no default " - "compilation handler." % type(element)) - return fn(element, compiler, **kw) diff --git a/libs/sqlalchemy/ext/declarative/__init__.py b/libs/sqlalchemy/ext/declarative/__init__.py deleted file mode 100644 index 0ee4e33f..00000000 --- a/libs/sqlalchemy/ext/declarative/__init__.py +++ /dev/null @@ -1,1309 +0,0 @@ -# ext/declarative/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -Synopsis -======== - -SQLAlchemy object-relational configuration involves the -combination of :class:`.Table`, :func:`.mapper`, and class -objects to define a mapped class. -:mod:`~sqlalchemy.ext.declarative` allows all three to be -expressed at once within the class declaration. As much as -possible, regular SQLAlchemy schema and ORM constructs are -used directly, so that configuration between "classical" ORM -usage and declarative remain highly similar. - -As a simple example:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class SomeClass(Base): - __tablename__ = 'some_table' - id = Column(Integer, primary_key=True) - name = Column(String(50)) - -Above, the :func:`declarative_base` callable returns a new base class from -which all mapped classes should inherit. When the class definition is -completed, a new :class:`.Table` and :func:`.mapper` will have been generated. - -The resulting table and mapper are accessible via -``__table__`` and ``__mapper__`` attributes on the -``SomeClass`` class:: - - # access the mapped Table - SomeClass.__table__ - - # access the Mapper - SomeClass.__mapper__ - -Defining Attributes -=================== - -In the previous example, the :class:`.Column` objects are -automatically named with the name of the attribute to which they are -assigned. - -To name columns explicitly with a name distinct from their mapped attribute, -just give the column a name. Below, column "some_table_id" is mapped to the -"id" attribute of `SomeClass`, but in SQL will be represented as -"some_table_id":: - - class SomeClass(Base): - __tablename__ = 'some_table' - id = Column("some_table_id", Integer, primary_key=True) - -Attributes may be added to the class after its construction, and they will be -added to the underlying :class:`.Table` and -:func:`.mapper` definitions as appropriate:: - - SomeClass.data = Column('data', Unicode) - SomeClass.related = relationship(RelatedInfo) - -Classes which are constructed using declarative can interact freely -with classes that are mapped explicitly with :func:`.mapper`. - -It is recommended, though not required, that all tables -share the same underlying :class:`~sqlalchemy.schema.MetaData` object, -so that string-configured :class:`~sqlalchemy.schema.ForeignKey` -references can be resolved without issue. - -Accessing the MetaData -======================= - -The :func:`declarative_base` base class contains a -:class:`.MetaData` object where newly defined -:class:`.Table` objects are collected. This object is -intended to be accessed directly for -:class:`.MetaData`-specific operations. Such as, to issue -CREATE statements for all tables:: - - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - -:func:`declarative_base` can also receive a pre-existing -:class:`.MetaData` object, which allows a -declarative setup to be associated with an already -existing traditional collection of :class:`~sqlalchemy.schema.Table` -objects:: - - mymetadata = MetaData() - Base = declarative_base(metadata=mymetadata) - -Configuring Relationships -========================= - -Relationships to other classes are done in the usual way, with the added -feature that the class specified to :func:`~sqlalchemy.orm.relationship` -may be a string name. The "class registry" associated with ``Base`` -is used at mapper compilation time to resolve the name into the actual -class object, which is expected to have been defined once the mapper -configuration is used:: - - class User(Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - name = Column(String(50)) - addresses = relationship("Address", backref="user") - - class Address(Base): - __tablename__ = 'addresses' - - id = Column(Integer, primary_key=True) - email = Column(String(50)) - user_id = Column(Integer, ForeignKey('users.id')) - -Column constructs, since they are just that, are immediately usable, -as below where we define a primary join condition on the ``Address`` -class using them:: - - class Address(Base): - __tablename__ = 'addresses' - - id = Column(Integer, primary_key=True) - email = Column(String(50)) - user_id = Column(Integer, ForeignKey('users.id')) - user = relationship(User, primaryjoin=user_id == User.id) - -In addition to the main argument for :func:`~sqlalchemy.orm.relationship`, -other arguments which depend upon the columns present on an as-yet -undefined class may also be specified as strings. These strings are -evaluated as Python expressions. The full namespace available within -this evaluation includes all classes mapped for this declarative base, -as well as the contents of the ``sqlalchemy`` package, including -expression functions like :func:`~sqlalchemy.sql.expression.desc` and -:attr:`~sqlalchemy.sql.expression.func`:: - - class User(Base): - # .... - addresses = relationship("Address", - order_by="desc(Address.email)", - primaryjoin="Address.user_id==User.id") - -For the case where more than one module contains a class of the same name, -string class names can also be specified as module-qualified paths -within any of these string expressions:: - - class User(Base): - # .... - addresses = relationship("myapp.model.address.Address", - order_by="desc(myapp.model.address.Address.email)", - primaryjoin="myapp.model.address.Address.user_id==" - "myapp.model.user.User.id") - -The qualified path can be any partial path that removes ambiguity between -the names. For example, to disambiguate between -``myapp.model.address.Address`` and ``myapp.model.lookup.Address``, -we can specify ``address.Address`` or ``lookup.Address``:: - - class User(Base): - # .... - addresses = relationship("address.Address", - order_by="desc(address.Address.email)", - primaryjoin="address.Address.user_id==" - "User.id") - -.. versionadded:: 0.8 - module-qualified paths can be used when specifying string arguments - with Declarative, in order to specify specific modules. - -Two alternatives also exist to using string-based attributes. A lambda -can also be used, which will be evaluated after all mappers have been -configured:: - - class User(Base): - # ... - addresses = relationship(lambda: Address, - order_by=lambda: desc(Address.email), - primaryjoin=lambda: Address.user_id==User.id) - -Or, the relationship can be added to the class explicitly after the classes -are available:: - - User.addresses = relationship(Address, - primaryjoin=Address.user_id==User.id) - - - - -Configuring Many-to-Many Relationships -====================================== - -Many-to-many relationships are also declared in the same way -with declarative as with traditional mappings. The -``secondary`` argument to -:func:`.relationship` is as usual passed a -:class:`.Table` object, which is typically declared in the -traditional way. The :class:`.Table` usually shares -the :class:`.MetaData` object used by the declarative base:: - - keywords = Table( - 'keywords', Base.metadata, - Column('author_id', Integer, ForeignKey('authors.id')), - Column('keyword_id', Integer, ForeignKey('keywords.id')) - ) - - class Author(Base): - __tablename__ = 'authors' - id = Column(Integer, primary_key=True) - keywords = relationship("Keyword", secondary=keywords) - -Like other :func:`~sqlalchemy.orm.relationship` arguments, a string is accepted -as well, passing the string name of the table as defined in the -``Base.metadata.tables`` collection:: - - class Author(Base): - __tablename__ = 'authors' - id = Column(Integer, primary_key=True) - keywords = relationship("Keyword", secondary="keywords") - -As with traditional mapping, its generally not a good idea to use -a :class:`.Table` as the "secondary" argument which is also mapped to -a class, unless the :func:`.relationship` is declared with ``viewonly=True``. -Otherwise, the unit-of-work system may attempt duplicate INSERT and -DELETE statements against the underlying table. - -.. _declarative_sql_expressions: - -Defining SQL Expressions -======================== - -See :ref:`mapper_sql_expressions` for examples on declaratively -mapping attributes to SQL expressions. - -.. _declarative_table_args: - -Table Configuration -=================== - -Table arguments other than the name, metadata, and mapped Column -arguments are specified using the ``__table_args__`` class attribute. -This attribute accommodates both positional as well as keyword -arguments that are normally sent to the -:class:`~sqlalchemy.schema.Table` constructor. -The attribute can be specified in one of two forms. One is as a -dictionary:: - - class MyClass(Base): - __tablename__ = 'sometable' - __table_args__ = {'mysql_engine':'InnoDB'} - -The other, a tuple, where each argument is positional -(usually constraints):: - - class MyClass(Base): - __tablename__ = 'sometable' - __table_args__ = ( - ForeignKeyConstraint(['id'], ['remote_table.id']), - UniqueConstraint('foo'), - ) - -Keyword arguments can be specified with the above form by -specifying the last argument as a dictionary:: - - class MyClass(Base): - __tablename__ = 'sometable' - __table_args__ = ( - ForeignKeyConstraint(['id'], ['remote_table.id']), - UniqueConstraint('foo'), - {'autoload':True} - ) - -Using a Hybrid Approach with __table__ -======================================= - -As an alternative to ``__tablename__``, a direct -:class:`~sqlalchemy.schema.Table` construct may be used. The -:class:`~sqlalchemy.schema.Column` objects, which in this case require -their names, will be added to the mapping just like a regular mapping -to a table:: - - class MyClass(Base): - __table__ = Table('my_table', Base.metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50)) - ) - -``__table__`` provides a more focused point of control for establishing -table metadata, while still getting most of the benefits of using declarative. -An application that uses reflection might want to load table metadata elsewhere -and pass it to declarative classes:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - Base.metadata.reflect(some_engine) - - class User(Base): - __table__ = metadata.tables['user'] - - class Address(Base): - __table__ = metadata.tables['address'] - -Some configuration schemes may find it more appropriate to use ``__table__``, -such as those which already take advantage of the data-driven nature of -:class:`.Table` to customize and/or automate schema definition. - -Note that when the ``__table__`` approach is used, the object is immediately -usable as a plain :class:`.Table` within the class declaration body itself, -as a Python class is only another syntactical block. Below this is illustrated -by using the ``id`` column in the ``primaryjoin`` condition of a -:func:`.relationship`:: - - class MyClass(Base): - __table__ = Table('my_table', Base.metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50)) - ) - - widgets = relationship(Widget, - primaryjoin=Widget.myclass_id==__table__.c.id) - -Similarly, mapped attributes which refer to ``__table__`` can be placed inline, -as below where we assign the ``name`` column to the attribute ``_name``, -generating a synonym for ``name``:: - - from sqlalchemy.ext.declarative import synonym_for - - class MyClass(Base): - __table__ = Table('my_table', Base.metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50)) - ) - - _name = __table__.c.name - - @synonym_for("_name") - def name(self): - return "Name: %s" % _name - -Using Reflection with Declarative -================================= - -It's easy to set up a :class:`.Table` that uses ``autoload=True`` -in conjunction with a mapped class:: - - class MyClass(Base): - __table__ = Table('mytable', Base.metadata, - autoload=True, autoload_with=some_engine) - -However, one improvement that can be made here is to not -require the :class:`.Engine` to be available when classes are -being first declared. To achieve this, use the -:class:`.DeferredReflection` mixin, which sets up mappings -only after a special ``prepare(engine)`` step is called:: - - from sqlalchemy.ext.declarative import declarative_base, DeferredReflection - - Base = declarative_base(cls=DeferredReflection) - - class Foo(Base): - __tablename__ = 'foo' - bars = relationship("Bar") - - class Bar(Base): - __tablename__ = 'bar' - - # illustrate overriding of "bar.foo_id" to have - # a foreign key constraint otherwise not - # reflected, such as when using MySQL - foo_id = Column(Integer, ForeignKey('foo.id')) - - Base.prepare(e) - -.. versionadded:: 0.8 - Added :class:`.DeferredReflection`. - -Mapper Configuration -==================== - -Declarative makes use of the :func:`~.orm.mapper` function internally -when it creates the mapping to the declared table. The options -for :func:`~.orm.mapper` are passed directly through via the -``__mapper_args__`` class attribute. As always, arguments which reference -locally mapped columns can reference them directly from within the -class declaration:: - - from datetime import datetime - - class Widget(Base): - __tablename__ = 'widgets' - - id = Column(Integer, primary_key=True) - timestamp = Column(DateTime, nullable=False) - - __mapper_args__ = { - 'version_id_col': timestamp, - 'version_id_generator': lambda v:datetime.now() - } - -.. _declarative_inheritance: - -Inheritance Configuration -========================= - -Declarative supports all three forms of inheritance as intuitively -as possible. The ``inherits`` mapper keyword argument is not needed -as declarative will determine this from the class itself. The various -"polymorphic" keyword arguments are specified using ``__mapper_args__``. - -Joined Table Inheritance -~~~~~~~~~~~~~~~~~~~~~~~~ - -Joined table inheritance is defined as a subclass that defines its own -table:: - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - __tablename__ = 'engineers' - __mapper_args__ = {'polymorphic_identity': 'engineer'} - id = Column(Integer, ForeignKey('people.id'), primary_key=True) - primary_language = Column(String(50)) - -Note that above, the ``Engineer.id`` attribute, since it shares the -same attribute name as the ``Person.id`` attribute, will in fact -represent the ``people.id`` and ``engineers.id`` columns together, -with the "Engineer.id" column taking precedence if queried directly. -To provide the ``Engineer`` class with an attribute that represents -only the ``engineers.id`` column, give it a different attribute name:: - - class Engineer(Person): - __tablename__ = 'engineers' - __mapper_args__ = {'polymorphic_identity': 'engineer'} - engineer_id = Column('id', Integer, ForeignKey('people.id'), - primary_key=True) - primary_language = Column(String(50)) - - -.. versionchanged:: 0.7 joined table inheritance favors the subclass - column over that of the superclass, such as querying above - for ``Engineer.id``. Prior to 0.7 this was the reverse. - -.. _declarative_single_table: - -Single Table Inheritance -~~~~~~~~~~~~~~~~~~~~~~~~ - -Single table inheritance is defined as a subclass that does not have -its own table; you just leave out the ``__table__`` and ``__tablename__`` -attributes:: - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - __mapper_args__ = {'polymorphic_identity': 'engineer'} - primary_language = Column(String(50)) - -When the above mappers are configured, the ``Person`` class is mapped -to the ``people`` table *before* the ``primary_language`` column is -defined, and this column will not be included in its own mapping. -When ``Engineer`` then defines the ``primary_language`` column, the -column is added to the ``people`` table so that it is included in the -mapping for ``Engineer`` and is also part of the table's full set of -columns. Columns which are not mapped to ``Person`` are also excluded -from any other single or joined inheriting classes using the -``exclude_properties`` mapper argument. Below, ``Manager`` will have -all the attributes of ``Person`` and ``Manager`` but *not* the -``primary_language`` attribute of ``Engineer``:: - - class Manager(Person): - __mapper_args__ = {'polymorphic_identity': 'manager'} - golf_swing = Column(String(50)) - -The attribute exclusion logic is provided by the -``exclude_properties`` mapper argument, and declarative's default -behavior can be disabled by passing an explicit ``exclude_properties`` -collection (empty or otherwise) to the ``__mapper_args__``. - -Resolving Column Conflicts -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Note above that the ``primary_language`` and ``golf_swing`` columns -are "moved up" to be applied to ``Person.__table__``, as a result of their -declaration on a subclass that has no table of its own. A tricky case -comes up when two subclasses want to specify *the same* column, as below:: - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - __mapper_args__ = {'polymorphic_identity': 'engineer'} - start_date = Column(DateTime) - - class Manager(Person): - __mapper_args__ = {'polymorphic_identity': 'manager'} - start_date = Column(DateTime) - -Above, the ``start_date`` column declared on both ``Engineer`` and ``Manager`` -will result in an error:: - - sqlalchemy.exc.ArgumentError: Column 'start_date' on class - conflicts with existing - column 'people.start_date' - -In a situation like this, Declarative can't be sure -of the intent, especially if the ``start_date`` columns had, for example, -different types. A situation like this can be resolved by using -:class:`.declared_attr` to define the :class:`.Column` conditionally, taking -care to return the **existing column** via the parent ``__table__`` if it -already exists:: - - from sqlalchemy.ext.declarative import declared_attr - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - __mapper_args__ = {'polymorphic_identity': 'engineer'} - - @declared_attr - def start_date(cls): - "Start date column, if not present already." - return Person.__table__.c.get('start_date', Column(DateTime)) - - class Manager(Person): - __mapper_args__ = {'polymorphic_identity': 'manager'} - - @declared_attr - def start_date(cls): - "Start date column, if not present already." - return Person.__table__.c.get('start_date', Column(DateTime)) - -Above, when ``Manager`` is mapped, the ``start_date`` column is -already present on the ``Person`` class. Declarative lets us return -that :class:`.Column` as a result in this case, where it knows to skip -re-assigning the same column. If the mapping is mis-configured such -that the ``start_date`` column is accidentally re-assigned to a -different table (such as, if we changed ``Manager`` to be joined -inheritance without fixing ``start_date``), an error is raised which -indicates an existing :class:`.Column` is trying to be re-assigned to -a different owning :class:`.Table`. - -.. versionadded:: 0.8 :class:`.declared_attr` can be used on a non-mixin - class, and the returned :class:`.Column` or other mapped attribute - will be applied to the mapping as any other attribute. Previously, - the resulting attribute would be ignored, and also result in a warning - being emitted when a subclass was created. - -.. versionadded:: 0.8 :class:`.declared_attr`, when used either with a - mixin or non-mixin declarative class, can return an existing - :class:`.Column` already assigned to the parent :class:`.Table`, - to indicate that the re-assignment of the :class:`.Column` should be - skipped, however should still be mapped on the target class, - in order to resolve duplicate column conflicts. - -The same concept can be used with mixin classes (see -:ref:`declarative_mixins`):: - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class HasStartDate(object): - @declared_attr - def start_date(cls): - return cls.__table__.c.get('start_date', Column(DateTime)) - - class Engineer(HasStartDate, Person): - __mapper_args__ = {'polymorphic_identity': 'engineer'} - - class Manager(HasStartDate, Person): - __mapper_args__ = {'polymorphic_identity': 'manager'} - -The above mixin checks the local ``__table__`` attribute for the column. -Because we're using single table inheritance, we're sure that in this case, -``cls.__table__`` refers to ``People.__table__``. If we were mixing joined- -and single-table inheritance, we might want our mixin to check more carefully -if ``cls.__table__`` is really the :class:`.Table` we're looking for. - -Concrete Table Inheritance -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Concrete is defined as a subclass which has its own table and sets the -``concrete`` keyword argument to ``True``:: - - class Person(Base): - __tablename__ = 'people' - id = Column(Integer, primary_key=True) - name = Column(String(50)) - - class Engineer(Person): - __tablename__ = 'engineers' - __mapper_args__ = {'concrete':True} - id = Column(Integer, primary_key=True) - primary_language = Column(String(50)) - name = Column(String(50)) - -Usage of an abstract base class is a little less straightforward as it -requires usage of :func:`~sqlalchemy.orm.util.polymorphic_union`, -which needs to be created with the :class:`.Table` objects -before the class is built:: - - engineers = Table('engineers', Base.metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50)), - Column('primary_language', String(50)) - ) - managers = Table('managers', Base.metadata, - Column('id', Integer, primary_key=True), - Column('name', String(50)), - Column('golf_swing', String(50)) - ) - - punion = polymorphic_union({ - 'engineer':engineers, - 'manager':managers - }, 'type', 'punion') - - class Person(Base): - __table__ = punion - __mapper_args__ = {'polymorphic_on':punion.c.type} - - class Engineer(Person): - __table__ = engineers - __mapper_args__ = {'polymorphic_identity':'engineer', 'concrete':True} - - class Manager(Person): - __table__ = managers - __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True} - -.. _declarative_concrete_helpers: - -Using the Concrete Helpers -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Helper classes provides a simpler pattern for concrete inheritance. -With these objects, the ``__declare_last__`` helper is used to configure the -"polymorphic" loader for the mapper after all subclasses have been declared. - -.. versionadded:: 0.7.3 - -An abstract base can be declared using the -:class:`.AbstractConcreteBase` class:: - - from sqlalchemy.ext.declarative import AbstractConcreteBase - - class Employee(AbstractConcreteBase, Base): - pass - -To have a concrete ``employee`` table, use :class:`.ConcreteBase` instead:: - - from sqlalchemy.ext.declarative import ConcreteBase - - class Employee(ConcreteBase, Base): - __tablename__ = 'employee' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - __mapper_args__ = { - 'polymorphic_identity':'employee', - 'concrete':True} - - -Either ``Employee`` base can be used in the normal fashion:: - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - class Engineer(Employee): - __tablename__ = 'engineer' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - engineer_info = Column(String(40)) - __mapper_args__ = {'polymorphic_identity':'engineer', - 'concrete':True} - - -.. _declarative_mixins: - -Mixin and Custom Base Classes -============================== - -A common need when using :mod:`~sqlalchemy.ext.declarative` is to -share some functionality, such as a set of common columns, some common -table options, or other mapped properties, across many -classes. The standard Python idioms for this is to have the classes -inherit from a base which includes these common features. - -When using :mod:`~sqlalchemy.ext.declarative`, this idiom is allowed -via the usage of a custom declarative base class, as well as a "mixin" class -which is inherited from in addition to the primary base. Declarative -includes several helper features to make this work in terms of how -mappings are declared. An example of some commonly mixed-in -idioms is below:: - - from sqlalchemy.ext.declarative import declared_attr - - class MyMixin(object): - - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - __table_args__ = {'mysql_engine': 'InnoDB'} - __mapper_args__= {'always_refresh': True} - - id = Column(Integer, primary_key=True) - - class MyModel(MyMixin, Base): - name = Column(String(1000)) - -Where above, the class ``MyModel`` will contain an "id" column -as the primary key, a ``__tablename__`` attribute that derives -from the name of the class itself, as well as ``__table_args__`` -and ``__mapper_args__`` defined by the ``MyMixin`` mixin class. - -There's no fixed convention over whether ``MyMixin`` precedes -``Base`` or not. Normal Python method resolution rules apply, and -the above example would work just as well with:: - - class MyModel(Base, MyMixin): - name = Column(String(1000)) - -This works because ``Base`` here doesn't define any of the -variables that ``MyMixin`` defines, i.e. ``__tablename__``, -``__table_args__``, ``id``, etc. If the ``Base`` did define -an attribute of the same name, the class placed first in the -inherits list would determine which attribute is used on the -newly defined class. - -Augmenting the Base -~~~~~~~~~~~~~~~~~~~ - -In addition to using a pure mixin, most of the techniques in this -section can also be applied to the base class itself, for patterns that -should apply to all classes derived from a particular base. This is achieved -using the ``cls`` argument of the :func:`.declarative_base` function:: - - from sqlalchemy.ext.declarative import declared_attr - - class Base(object): - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - __table_args__ = {'mysql_engine': 'InnoDB'} - - id = Column(Integer, primary_key=True) - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base(cls=Base) - - class MyModel(Base): - name = Column(String(1000)) - -Where above, ``MyModel`` and all other classes that derive from ``Base`` will -have a table name derived from the class name, an ``id`` primary key column, -as well as the "InnoDB" engine for MySQL. - -Mixing in Columns -~~~~~~~~~~~~~~~~~ - -The most basic way to specify a column on a mixin is by simple -declaration:: - - class TimestampMixin(object): - created_at = Column(DateTime, default=func.now()) - - class MyModel(TimestampMixin, Base): - __tablename__ = 'test' - - id = Column(Integer, primary_key=True) - name = Column(String(1000)) - -Where above, all declarative classes that include ``TimestampMixin`` -will also have a column ``created_at`` that applies a timestamp to -all row insertions. - -Those familiar with the SQLAlchemy expression language know that -the object identity of clause elements defines their role in a schema. -Two ``Table`` objects ``a`` and ``b`` may both have a column called -``id``, but the way these are differentiated is that ``a.c.id`` -and ``b.c.id`` are two distinct Python objects, referencing their -parent tables ``a`` and ``b`` respectively. - -In the case of the mixin column, it seems that only one -:class:`.Column` object is explicitly created, yet the ultimate -``created_at`` column above must exist as a distinct Python object -for each separate destination class. To accomplish this, the declarative -extension creates a **copy** of each :class:`.Column` object encountered on -a class that is detected as a mixin. - -This copy mechanism is limited to simple columns that have no foreign -keys, as a :class:`.ForeignKey` itself contains references to columns -which can't be properly recreated at this level. For columns that -have foreign keys, as well as for the variety of mapper-level constructs -that require destination-explicit context, the -:class:`~.declared_attr` decorator is provided so that -patterns common to many classes can be defined as callables:: - - from sqlalchemy.ext.declarative import declared_attr - - class ReferenceAddressMixin(object): - @declared_attr - def address_id(cls): - return Column(Integer, ForeignKey('address.id')) - - class User(ReferenceAddressMixin, Base): - __tablename__ = 'user' - id = Column(Integer, primary_key=True) - -Where above, the ``address_id`` class-level callable is executed at the -point at which the ``User`` class is constructed, and the declarative -extension can use the resulting :class:`.Column` object as returned by -the method without the need to copy it. - -.. versionchanged:: > 0.6.5 - Rename 0.6.5 ``sqlalchemy.util.classproperty`` - into :class:`~.declared_attr`. - -Columns generated by :class:`~.declared_attr` can also be -referenced by ``__mapper_args__`` to a limited degree, currently -by ``polymorphic_on`` and ``version_id_col``, by specifying the -classdecorator itself into the dictionary - the declarative extension -will resolve them at class construction time:: - - class MyMixin: - @declared_attr - def type_(cls): - return Column(String(50)) - - __mapper_args__= {'polymorphic_on':type_} - - class MyModel(MyMixin, Base): - __tablename__='test' - id = Column(Integer, primary_key=True) - - - -Mixing in Relationships -~~~~~~~~~~~~~~~~~~~~~~~ - -Relationships created by :func:`~sqlalchemy.orm.relationship` are provided -with declarative mixin classes exclusively using the -:class:`.declared_attr` approach, eliminating any ambiguity -which could arise when copying a relationship and its possibly column-bound -contents. Below is an example which combines a foreign key column and a -relationship so that two classes ``Foo`` and ``Bar`` can both be configured to -reference a common target class via many-to-one:: - - class RefTargetMixin(object): - @declared_attr - def target_id(cls): - return Column('target_id', ForeignKey('target.id')) - - @declared_attr - def target(cls): - return relationship("Target") - - class Foo(RefTargetMixin, Base): - __tablename__ = 'foo' - id = Column(Integer, primary_key=True) - - class Bar(RefTargetMixin, Base): - __tablename__ = 'bar' - id = Column(Integer, primary_key=True) - - class Target(Base): - __tablename__ = 'target' - id = Column(Integer, primary_key=True) - -Using Advanced Relationship Arguments (e.g. ``primaryjoin``, etc.) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:func:`~sqlalchemy.orm.relationship` definitions which require explicit -primaryjoin, order_by etc. expressions should in all but the most -simplistic cases use **late bound** forms -for these arguments, meaning, using either the string form or a lambda. -The reason for this is that the related :class:`.Column` objects which are to -be configured using ``@declared_attr`` are not available to another -``@declared_attr`` attribute; while the methods will work and return new -:class:`.Column` objects, those are not the :class:`.Column` objects that -Declarative will be using as it calls the methods on its own, thus using -*different* :class:`.Column` objects. - -The canonical example is the primaryjoin condition that depends upon -another mixed-in column:: - - class RefTargetMixin(object): - @declared_attr - def target_id(cls): - return Column('target_id', ForeignKey('target.id')) - - @declared_attr - def target(cls): - return relationship(Target, - primaryjoin=Target.id==cls.target_id # this is *incorrect* - ) - -Mapping a class using the above mixin, we will get an error like:: - - sqlalchemy.exc.InvalidRequestError: this ForeignKey's parent column is not - yet associated with a Table. - -This is because the ``target_id`` :class:`.Column` we've called upon in our ``target()`` -method is not the same :class:`.Column` that declarative is actually going to map -to our table. - -The condition above is resolved using a lambda:: - - class RefTargetMixin(object): - @declared_attr - def target_id(cls): - return Column('target_id', ForeignKey('target.id')) - - @declared_attr - def target(cls): - return relationship(Target, - primaryjoin=lambda: Target.id==cls.target_id - ) - -or alternatively, the string form (which ultmately generates a lambda):: - - class RefTargetMixin(object): - @declared_attr - def target_id(cls): - return Column('target_id', ForeignKey('target.id')) - - @declared_attr - def target(cls): - return relationship("Target", - primaryjoin="Target.id==%s.target_id" % cls.__name__ - ) - -Mixing in deferred(), column_property(), and other MapperProperty classes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Like :func:`~sqlalchemy.orm.relationship`, all -:class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as -:func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`, -etc. ultimately involve references to columns, and therefore, when -used with declarative mixins, have the :class:`.declared_attr` -requirement so that no reliance on copying is needed:: - - class SomethingMixin(object): - - @declared_attr - def dprop(cls): - return deferred(Column(Integer)) - - class Something(SomethingMixin, Base): - __tablename__ = "something" - -Mixing in Association Proxy and Other Attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mixins can specify user-defined attributes as well as other extension -units such as :func:`.association_proxy`. The usage of -:class:`.declared_attr` is required in those cases where the attribute must -be tailored specifically to the target subclass. An example is when -constructing multiple :func:`.association_proxy` attributes which each -target a different type of child object. Below is an -:func:`.association_proxy` / mixin example which provides a scalar list of -string values to an implementing class:: - - from sqlalchemy import Column, Integer, ForeignKey, String - from sqlalchemy.orm import relationship - from sqlalchemy.ext.associationproxy import association_proxy - from sqlalchemy.ext.declarative import declarative_base, declared_attr - - Base = declarative_base() - - class HasStringCollection(object): - @declared_attr - def _strings(cls): - class StringAttribute(Base): - __tablename__ = cls.string_table_name - id = Column(Integer, primary_key=True) - value = Column(String(50), nullable=False) - parent_id = Column(Integer, - ForeignKey('%s.id' % cls.__tablename__), - nullable=False) - def __init__(self, value): - self.value = value - - return relationship(StringAttribute) - - @declared_attr - def strings(cls): - return association_proxy('_strings', 'value') - - class TypeA(HasStringCollection, Base): - __tablename__ = 'type_a' - string_table_name = 'type_a_strings' - id = Column(Integer(), primary_key=True) - - class TypeB(HasStringCollection, Base): - __tablename__ = 'type_b' - string_table_name = 'type_b_strings' - id = Column(Integer(), primary_key=True) - -Above, the ``HasStringCollection`` mixin produces a :func:`.relationship` -which refers to a newly generated class called ``StringAttribute``. The -``StringAttribute`` class is generated with it's own :class:`.Table` -definition which is local to the parent class making usage of the -``HasStringCollection`` mixin. It also produces an :func:`.association_proxy` -object which proxies references to the ``strings`` attribute onto the ``value`` -attribute of each ``StringAttribute`` instance. - -``TypeA`` or ``TypeB`` can be instantiated given the constructor -argument ``strings``, a list of strings:: - - ta = TypeA(strings=['foo', 'bar']) - tb = TypeA(strings=['bat', 'bar']) - -This list will generate a collection -of ``StringAttribute`` objects, which are persisted into a table that's -local to either the ``type_a_strings`` or ``type_b_strings`` table:: - - >>> print ta._strings - [<__main__.StringAttribute object at 0x10151cd90>, - <__main__.StringAttribute object at 0x10151ce10>] - -When constructing the :func:`.association_proxy`, the -:class:`.declared_attr` decorator must be used so that a distinct -:func:`.association_proxy` object is created for each of the ``TypeA`` -and ``TypeB`` classes. - -.. versionadded:: 0.8 :class:`.declared_attr` is usable with non-mapped - attributes, including user-defined attributes as well as - :func:`.association_proxy`. - - -Controlling table inheritance with mixins -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``__tablename__`` attribute in conjunction with the hierarchy of -classes involved in a declarative mixin scenario controls what type of -table inheritance, if any, -is configured by the declarative extension. - -If the ``__tablename__`` is computed by a mixin, you may need to -control which classes get the computed attribute in order to get the -type of table inheritance you require. - -For example, if you had a mixin that computes ``__tablename__`` but -where you wanted to use that mixin in a single table inheritance -hierarchy, you can explicitly specify ``__tablename__`` as ``None`` to -indicate that the class should not have a table mapped:: - - from sqlalchemy.ext.declarative import declared_attr - - class Tablename: - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - class Person(Tablename, Base): - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - __tablename__ = None - __mapper_args__ = {'polymorphic_identity': 'engineer'} - primary_language = Column(String(50)) - -Alternatively, you can make the mixin intelligent enough to only -return a ``__tablename__`` in the event that no table is already -mapped in the inheritance hierarchy. To help with this, a -:func:`~sqlalchemy.ext.declarative.has_inherited_table` helper -function is provided that returns ``True`` if a parent class already -has a mapped table. - -As an example, here's a mixin that will only allow single table -inheritance:: - - from sqlalchemy.ext.declarative import declared_attr - from sqlalchemy.ext.declarative import has_inherited_table - - class Tablename(object): - @declared_attr - def __tablename__(cls): - if has_inherited_table(cls): - return None - return cls.__name__.lower() - - class Person(Tablename, Base): - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - class Engineer(Person): - primary_language = Column(String(50)) - __mapper_args__ = {'polymorphic_identity': 'engineer'} - -If you want to use a similar pattern with a mix of single and joined -table inheritance, you would need a slightly different mixin and use -it on any joined table child classes in addition to their parent -classes:: - - from sqlalchemy.ext.declarative import declared_attr - from sqlalchemy.ext.declarative import has_inherited_table - - class Tablename(object): - @declared_attr - def __tablename__(cls): - if (has_inherited_table(cls) and - Tablename not in cls.__bases__): - return None - return cls.__name__.lower() - - class Person(Tablename, Base): - id = Column(Integer, primary_key=True) - discriminator = Column('type', String(50)) - __mapper_args__ = {'polymorphic_on': discriminator} - - # This is single table inheritance - class Engineer(Person): - primary_language = Column(String(50)) - __mapper_args__ = {'polymorphic_identity': 'engineer'} - - # This is joined table inheritance - class Manager(Tablename, Person): - id = Column(Integer, ForeignKey('person.id'), primary_key=True) - preferred_recreation = Column(String(50)) - __mapper_args__ = {'polymorphic_identity': 'engineer'} - -Combining Table/Mapper Arguments from Multiple Mixins -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the case of ``__table_args__`` or ``__mapper_args__`` -specified with declarative mixins, you may want to combine -some parameters from several mixins with those you wish to -define on the class iteself. The -:class:`.declared_attr` decorator can be used -here to create user-defined collation routines that pull -from multiple collections:: - - from sqlalchemy.ext.declarative import declared_attr - - class MySQLSettings(object): - __table_args__ = {'mysql_engine':'InnoDB'} - - class MyOtherMixin(object): - __table_args__ = {'info':'foo'} - - class MyModel(MySQLSettings, MyOtherMixin, Base): - __tablename__='my_model' - - @declared_attr - def __table_args__(cls): - args = dict() - args.update(MySQLSettings.__table_args__) - args.update(MyOtherMixin.__table_args__) - return args - - id = Column(Integer, primary_key=True) - -Creating Indexes with Mixins -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To define a named, potentially multicolumn :class:`.Index` that applies to all -tables derived from a mixin, use the "inline" form of :class:`.Index` and -establish it as part of ``__table_args__``:: - - class MyMixin(object): - a = Column(Integer) - b = Column(Integer) - - @declared_attr - def __table_args__(cls): - return (Index('test_idx_%s' % cls.__tablename__, 'a', 'b'),) - - class MyModel(MyMixin, Base): - __tablename__ = 'atable' - c = Column(Integer,primary_key=True) - -Special Directives -================== - -``__declare_last__()`` -~~~~~~~~~~~~~~~~~~~~~~ - -The ``__declare_last__()`` hook allows definition of -a class level function that is automatically called by the -:meth:`.MapperEvents.after_configured` event, which occurs after mappings are -assumed to be completed and the 'configure' step has finished:: - - class MyClass(Base): - @classmethod - def __declare_last__(cls): - "" - # do something with mappings - -.. versionadded:: 0.7.3 - -.. _declarative_abstract: - -``__abstract__`` -~~~~~~~~~~~~~~~~~~~ - -``__abstract__`` causes declarative to skip the production -of a table or mapper for the class entirely. A class can be added within a -hierarchy in the same way as mixin (see :ref:`declarative_mixins`), allowing -subclasses to extend just from the special class:: - - class SomeAbstractBase(Base): - __abstract__ = True - - def some_helpful_method(self): - "" - - @declared_attr - def __mapper_args__(cls): - return {"helpful mapper arguments":True} - - class MyMappedClass(SomeAbstractBase): - "" - -One possible use of ``__abstract__`` is to use a distinct -:class:`.MetaData` for different bases:: - - Base = declarative_base() - - class DefaultBase(Base): - __abstract__ = True - metadata = MetaData() - - class OtherBase(Base): - __abstract__ = True - metadata = MetaData() - -Above, classes which inherit from ``DefaultBase`` will use one -:class:`.MetaData` as the registry of tables, and those which inherit from -``OtherBase`` will use a different one. The tables themselves can then be -created perhaps within distinct databases:: - - DefaultBase.metadata.create_all(some_engine) - OtherBase.metadata_create_all(some_other_engine) - -.. versionadded:: 0.7.3 - -Class Constructor -================= - -As a convenience feature, the :func:`declarative_base` sets a default -constructor on classes which takes keyword arguments, and assigns them -to the named attributes:: - - e = Engineer(primary_language='python') - -Sessions -======== - -Note that ``declarative`` does nothing special with sessions, and is -only intended as an easier way to configure mappers and -:class:`~sqlalchemy.schema.Table` objects. A typical application -setup using :class:`~sqlalchemy.orm.scoping.scoped_session` might look like:: - - engine = create_engine('postgresql://scott:tiger@localhost/test') - Session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) - Base = declarative_base() - -Mapped instances then make usage of -:class:`~sqlalchemy.orm.session.Session` in the usual way. - -""" - -from .api import declarative_base, synonym_for, comparable_using, \ - instrument_declarative, ConcreteBase, AbstractConcreteBase, \ - DeclarativeMeta, DeferredReflection, has_inherited_table,\ - declared_attr, as_declarative - - -__all__ = ['declarative_base', 'synonym_for', 'has_inherited_table', - 'comparable_using', 'instrument_declarative', 'declared_attr', - 'ConcreteBase', 'AbstractConcreteBase', 'DeclarativeMeta', - 'DeferredReflection'] diff --git a/libs/sqlalchemy/ext/declarative/api.py b/libs/sqlalchemy/ext/declarative/api.py deleted file mode 100644 index 2418c6e5..00000000 --- a/libs/sqlalchemy/ext/declarative/api.py +++ /dev/null @@ -1,512 +0,0 @@ -# ext/declarative/api.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Public API functions and helpers for declarative.""" - - -from ...schema import Table, MetaData -from ...orm import synonym as _orm_synonym, mapper,\ - comparable_property,\ - interfaces, properties -from ...orm.util import polymorphic_union -from ...orm.base import _mapper_or_none -from ...util import compat -from ... import exc -import weakref - -from .base import _as_declarative, \ - _declarative_constructor,\ - _DeferredMapperConfig, _add_attribute -from .clsregistry import _class_resolver - - -def instrument_declarative(cls, registry, metadata): - """Given a class, configure the class declaratively, - using the given registry, which can be any dictionary, and - MetaData object. - - """ - if '_decl_class_registry' in cls.__dict__: - raise exc.InvalidRequestError( - "Class %r already has been " - "instrumented declaratively" % cls) - cls._decl_class_registry = registry - cls.metadata = metadata - _as_declarative(cls, cls.__name__, cls.__dict__) - - -def has_inherited_table(cls): - """Given a class, return True if any of the classes it inherits from has a - mapped table, otherwise return False. - """ - for class_ in cls.__mro__[1:]: - if getattr(class_, '__table__', None) is not None: - return True - return False - - -class DeclarativeMeta(type): - def __init__(cls, classname, bases, dict_): - if '_decl_class_registry' not in cls.__dict__: - _as_declarative(cls, classname, cls.__dict__) - type.__init__(cls, classname, bases, dict_) - - def __setattr__(cls, key, value): - _add_attribute(cls, key, value) - - -def synonym_for(name, map_column=False): - """Decorator, make a Python @property a query synonym for a column. - - A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being - decorated is the 'descriptor', otherwise passes its arguments through to - synonym():: - - @synonym_for('col') - @property - def prop(self): - return 'special sauce' - - The regular ``synonym()`` is also usable directly in a declarative setting - and may be convenient for read/write properties:: - - prop = synonym('col', descriptor=property(_read_prop, _write_prop)) - - """ - def decorate(fn): - return _orm_synonym(name, map_column=map_column, descriptor=fn) - return decorate - - -def comparable_using(comparator_factory): - """Decorator, allow a Python @property to be used in query criteria. - - This is a decorator front end to - :func:`~sqlalchemy.orm.comparable_property` that passes - through the comparator_factory and the function being decorated:: - - @comparable_using(MyComparatorType) - @property - def prop(self): - return 'special sauce' - - The regular ``comparable_property()`` is also usable directly in a - declarative setting and may be convenient for read/write properties:: - - prop = comparable_property(MyComparatorType) - - """ - def decorate(fn): - return comparable_property(comparator_factory, fn) - return decorate - - -class declared_attr(interfaces._MappedAttribute, property): - """Mark a class-level method as representing the definition of - a mapped property or special declarative member name. - - @declared_attr turns the attribute into a scalar-like - property that can be invoked from the uninstantiated class. - Declarative treats attributes specifically marked with - @declared_attr as returning a construct that is specific - to mapping or declarative table configuration. The name - of the attribute is that of what the non-dynamic version - of the attribute would be. - - @declared_attr is more often than not applicable to mixins, - to define relationships that are to be applied to different - implementors of the class:: - - class ProvidesUser(object): - "A mixin that adds a 'user' relationship to classes." - - @declared_attr - def user(self): - return relationship("User") - - It also can be applied to mapped classes, such as to provide - a "polymorphic" scheme for inheritance:: - - class Employee(Base): - id = Column(Integer, primary_key=True) - type = Column(String(50), nullable=False) - - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - @declared_attr - def __mapper_args__(cls): - if cls.__name__ == 'Employee': - return { - "polymorphic_on":cls.type, - "polymorphic_identity":"Employee" - } - else: - return {"polymorphic_identity":cls.__name__} - - .. versionchanged:: 0.8 :class:`.declared_attr` can be used with - non-ORM or extension attributes, such as user-defined attributes - or :func:`.association_proxy` objects, which will be assigned - to the class at class construction time. - - - """ - - def __init__(self, fget, *arg, **kw): - super(declared_attr, self).__init__(fget, *arg, **kw) - self.__doc__ = fget.__doc__ - - def __get__(desc, self, cls): - return desc.fget(cls) - - -def declarative_base(bind=None, metadata=None, mapper=None, cls=object, - name='Base', constructor=_declarative_constructor, - class_registry=None, - metaclass=DeclarativeMeta): - """Construct a base class for declarative class definitions. - - The new base class will be given a metaclass that produces - appropriate :class:`~sqlalchemy.schema.Table` objects and makes - the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the - information provided declaratively in the class and any subclasses - of the class. - - :param bind: An optional - :class:`~sqlalchemy.engine.Connectable`, will be assigned - the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData` - instance. - - :param metadata: - An optional :class:`~sqlalchemy.schema.MetaData` instance. All - :class:`~sqlalchemy.schema.Table` objects implicitly declared by - subclasses of the base will share this MetaData. A MetaData instance - will be created if none is provided. The - :class:`~sqlalchemy.schema.MetaData` instance will be available via the - `metadata` attribute of the generated declarative base class. - - :param mapper: - An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will - be used to map subclasses to their Tables. - - :param cls: - Defaults to :class:`object`. A type to use as the base for the generated - declarative base class. May be a class or tuple of classes. - - :param name: - Defaults to ``Base``. The display name for the generated - class. Customizing this is not required, but can improve clarity in - tracebacks and debugging. - - :param constructor: - Defaults to - :func:`~sqlalchemy.ext.declarative._declarative_constructor`, an - __init__ implementation that assigns \**kwargs for declared - fields and relationships to an instance. If ``None`` is supplied, - no __init__ will be provided and construction will fall back to - cls.__init__ by way of the normal Python semantics. - - :param class_registry: optional dictionary that will serve as the - registry of class names-> mapped classes when string names - are used to identify classes inside of :func:`.relationship` - and others. Allows two or more declarative base classes - to share the same registry of class names for simplified - inter-base relationships. - - :param metaclass: - Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ - compatible callable to use as the meta type of the generated - declarative base class. - - .. seealso:: - - :func:`.as_declarative` - - """ - lcl_metadata = metadata or MetaData() - if bind: - lcl_metadata.bind = bind - - if class_registry is None: - class_registry = weakref.WeakValueDictionary() - - bases = not isinstance(cls, tuple) and (cls,) or cls - class_dict = dict(_decl_class_registry=class_registry, - metadata=lcl_metadata) - - if constructor: - class_dict['__init__'] = constructor - if mapper: - class_dict['__mapper_cls__'] = mapper - - return metaclass(name, bases, class_dict) - -def as_declarative(**kw): - """ - Class decorator for :func:`.declarative_base`. - - Provides a syntactical shortcut to the ``cls`` argument - sent to :func:`.declarative_base`, allowing the base class - to be converted in-place to a "declarative" base:: - - from sqlalchemy.ext.declarative import as_declarative - - @as_declarative() - class Base(object): - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - id = Column(Integer, primary_key=True) - - class MyMappedClass(Base): - # ... - - All keyword arguments passed to :func:`.as_declarative` are passed - along to :func:`.declarative_base`. - - .. versionadded:: 0.8.3 - - .. seealso:: - - :func:`.declarative_base` - - """ - def decorate(cls): - kw['cls'] = cls - kw['name'] = cls.__name__ - return declarative_base(**kw) - - return decorate - -class ConcreteBase(object): - """A helper class for 'concrete' declarative mappings. - - :class:`.ConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :meth:`.after_configured` event. - - :class:`.ConcreteBase` produces a mapped - table for the class itself. Compare to :class:`.AbstractConcreteBase`, - which does not. - - Example:: - - from sqlalchemy.ext.declarative import ConcreteBase - - class Employee(ConcreteBase, Base): - __tablename__ = 'employee' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - __mapper_args__ = { - 'polymorphic_identity':'employee', - 'concrete':True} - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - """ - - @classmethod - def _create_polymorphic_union(cls, mappers): - return polymorphic_union(dict( - (mp.polymorphic_identity, mp.local_table) - for mp in mappers - ), 'type', 'pjoin') - - @classmethod - def __declare_last__(cls): - m = cls.__mapper__ - if m.with_polymorphic: - return - - mappers = list(m.self_and_descendants) - pjoin = cls._create_polymorphic_union(mappers) - m._set_with_polymorphic(("*", pjoin)) - m._set_polymorphic_on(pjoin.c.type) - - -class AbstractConcreteBase(ConcreteBase): - """A helper class for 'concrete' declarative mappings. - - :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :meth:`.after_configured` event. - - :class:`.AbstractConcreteBase` does not produce a mapped - table for the class itself. Compare to :class:`.ConcreteBase`, - which does. - - Example:: - - from sqlalchemy.ext.declarative import AbstractConcreteBase - - class Employee(AbstractConcreteBase, Base): - pass - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - """ - - __abstract__ = True - - @classmethod - def __declare_last__(cls): - if hasattr(cls, '__mapper__'): - return - - # can't rely on 'self_and_descendants' here - # since technically an immediate subclass - # might not be mapped, but a subclass - # may be. - mappers = [] - stack = list(cls.__subclasses__()) - while stack: - klass = stack.pop() - stack.extend(klass.__subclasses__()) - mn = _mapper_or_none(klass) - if mn is not None: - mappers.append(mn) - pjoin = cls._create_polymorphic_union(mappers) - cls.__mapper__ = m = mapper(cls, pjoin, polymorphic_on=pjoin.c.type) - - for scls in cls.__subclasses__(): - sm = _mapper_or_none(scls) - if sm.concrete and cls in scls.__bases__: - sm._set_concrete_base(m) - - -class DeferredReflection(object): - """A helper class for construction of mappings based on - a deferred reflection step. - - Normally, declarative can be used with reflection by - setting a :class:`.Table` object using autoload=True - as the ``__table__`` attribute on a declarative class. - The caveat is that the :class:`.Table` must be fully - reflected, or at the very least have a primary key column, - at the point at which a normal declarative mapping is - constructed, meaning the :class:`.Engine` must be available - at class declaration time. - - The :class:`.DeferredReflection` mixin moves the construction - of mappers to be at a later point, after a specific - method is called which first reflects all :class:`.Table` - objects created so far. Classes can define it as such:: - - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.ext.declarative import DeferredReflection - Base = declarative_base() - - class MyClass(DeferredReflection, Base): - __tablename__ = 'mytable' - - Above, ``MyClass`` is not yet mapped. After a series of - classes have been defined in the above fashion, all tables - can be reflected and mappings created using - :meth:`.prepare`:: - - engine = create_engine("someengine://...") - DeferredReflection.prepare(engine) - - The :class:`.DeferredReflection` mixin can be applied to individual - classes, used as the base for the declarative base itself, - or used in a custom abstract class. Using an abstract base - allows that only a subset of classes to be prepared for a - particular prepare step, which is necessary for applications - that use more than one engine. For example, if an application - has two engines, you might use two bases, and prepare each - separately, e.g.:: - - class ReflectedOne(DeferredReflection, Base): - __abstract__ = True - - class ReflectedTwo(DeferredReflection, Base): - __abstract__ = True - - class MyClass(ReflectedOne): - __tablename__ = 'mytable' - - class MyOtherClass(ReflectedOne): - __tablename__ = 'myothertable' - - class YetAnotherClass(ReflectedTwo): - __tablename__ = 'yetanothertable' - - # ... etc. - - Above, the class hierarchies for ``ReflectedOne`` and - ``ReflectedTwo`` can be configured separately:: - - ReflectedOne.prepare(engine_one) - ReflectedTwo.prepare(engine_two) - - .. versionadded:: 0.8 - - """ - @classmethod - def prepare(cls, engine): - """Reflect all :class:`.Table` objects for all current - :class:`.DeferredReflection` subclasses""" - - to_map = _DeferredMapperConfig.classes_for_base(cls) - for thingy in to_map: - cls._sa_decl_prepare(thingy.local_table, engine) - thingy.map() - mapper = thingy.cls.__mapper__ - metadata = mapper.class_.metadata - for rel in mapper._props.values(): - if isinstance(rel, properties.RelationshipProperty) and \ - rel.secondary is not None: - if isinstance(rel.secondary, Table): - cls._reflect_table(rel.secondary, engine) - elif isinstance(rel.secondary, _class_resolver): - rel.secondary._resolvers += ( - cls._sa_deferred_table_resolver(engine, metadata), - ) - - @classmethod - def _sa_deferred_table_resolver(cls, engine, metadata): - def _resolve(key): - t1 = Table(key, metadata) - cls._reflect_table(t1, engine) - return t1 - return _resolve - - @classmethod - def _sa_decl_prepare(cls, local_table, engine): - # autoload Table, which is already - # present in the metadata. This - # will fill in db-loaded columns - # into the existing Table object. - if local_table is not None: - cls._reflect_table(local_table, engine) - - @classmethod - def _reflect_table(cls, table, engine): - Table(table.name, - table.metadata, - extend_existing=True, - autoload_replace=False, - autoload=True, - autoload_with=engine, - schema=table.schema) diff --git a/libs/sqlalchemy/ext/declarative/base.py b/libs/sqlalchemy/ext/declarative/base.py deleted file mode 100644 index a764f126..00000000 --- a/libs/sqlalchemy/ext/declarative/base.py +++ /dev/null @@ -1,506 +0,0 @@ -# ext/declarative/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Internal implementation for declarative.""" - -from ...schema import Table, Column -from ...orm import mapper, class_mapper, synonym -from ...orm.interfaces import MapperProperty -from ...orm.properties import ColumnProperty, CompositeProperty -from ...orm.attributes import QueryableAttribute -from ...orm.base import _is_mapped_class -from ... import util, exc -from ...sql import expression -from ... import event -from . import clsregistry -import collections -import weakref - -def _declared_mapping_info(cls): - # deferred mapping - if _DeferredMapperConfig.has_cls(cls): - return _DeferredMapperConfig.config_for_cls(cls) - # regular mapping - elif _is_mapped_class(cls): - return class_mapper(cls, configure=False) - else: - return None - - -def _as_declarative(cls, classname, dict_): - from .api import declared_attr - - # dict_ will be a dictproxy, which we can't write to, and we need to! - dict_ = dict(dict_) - - column_copies = {} - potential_columns = {} - - mapper_args_fn = None - table_args = inherited_table_args = None - tablename = None - - declarative_props = (declared_attr, util.classproperty) - - for base in cls.__mro__: - _is_declarative_inherits = hasattr(base, '_decl_class_registry') - - if '__declare_last__' in base.__dict__: - @event.listens_for(mapper, "after_configured") - def go(): - cls.__declare_last__() - if '__abstract__' in base.__dict__: - if (base is cls or - (base in cls.__bases__ and not _is_declarative_inherits) - ): - return - - class_mapped = _declared_mapping_info(base) is not None - - for name, obj in vars(base).items(): - if name == '__mapper_args__': - if not mapper_args_fn and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - # don't even invoke __mapper_args__ until - # after we've determined everything about the - # mapped table. - mapper_args_fn = lambda: cls.__mapper_args__ - elif name == '__tablename__': - if not tablename and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - tablename = cls.__tablename__ - elif name == '__table_args__': - if not table_args and ( - not class_mapped or - isinstance(obj, declarative_props) - ): - table_args = cls.__table_args__ - if not isinstance(table_args, (tuple, dict, type(None))): - raise exc.ArgumentError( - "__table_args__ value must be a tuple, " - "dict, or None") - if base is not cls: - inherited_table_args = True - elif class_mapped: - if isinstance(obj, declarative_props): - util.warn("Regular (i.e. not __special__) " - "attribute '%s.%s' uses @declared_attr, " - "but owning class %s is mapped - " - "not applying to subclass %s." - % (base.__name__, name, base, cls)) - continue - elif base is not cls: - # we're a mixin. - if isinstance(obj, Column): - if getattr(cls, name) is not obj: - # if column has been overridden - # (like by the InstrumentedAttribute of the - # superclass), skip - continue - if obj.foreign_keys: - raise exc.InvalidRequestError( - "Columns with foreign keys to other columns " - "must be declared as @declared_attr callables " - "on declarative mixin classes. ") - if name not in dict_ and not ( - '__table__' in dict_ and - (obj.name or name) in dict_['__table__'].c - ) and name not in potential_columns: - potential_columns[name] = \ - column_copies[obj] = \ - obj.copy() - column_copies[obj]._creation_order = \ - obj._creation_order - elif isinstance(obj, MapperProperty): - raise exc.InvalidRequestError( - "Mapper properties (i.e. deferred," - "column_property(), relationship(), etc.) must " - "be declared as @declared_attr callables " - "on declarative mixin classes.") - elif isinstance(obj, declarative_props): - dict_[name] = ret = \ - column_copies[obj] = getattr(cls, name) - if isinstance(ret, (Column, MapperProperty)) and \ - ret.doc is None: - ret.doc = obj.__doc__ - - # apply inherited columns as we should - for k, v in potential_columns.items(): - dict_[k] = v - - if inherited_table_args and not tablename: - table_args = None - - clsregistry.add_class(classname, cls) - our_stuff = util.OrderedDict() - - for k in list(dict_): - - # TODO: improve this ? all dunders ? - if k in ('__table__', '__tablename__', '__mapper_args__'): - continue - - value = dict_[k] - if isinstance(value, declarative_props): - value = getattr(cls, k) - - elif isinstance(value, QueryableAttribute) and \ - value.class_ is not cls and \ - value.key != k: - # detect a QueryableAttribute that's already mapped being - # assigned elsewhere in userland, turn into a synonym() - value = synonym(value.key) - setattr(cls, k, value) - - - if (isinstance(value, tuple) and len(value) == 1 and - isinstance(value[0], (Column, MapperProperty))): - util.warn("Ignoring declarative-like tuple value of attribute " - "%s: possibly a copy-and-paste error with a comma " - "left at the end of the line?" % k) - continue - if not isinstance(value, (Column, MapperProperty)): - if not k.startswith('__'): - dict_.pop(k) - setattr(cls, k, value) - continue - if k == 'metadata': - raise exc.InvalidRequestError( - "Attribute name 'metadata' is reserved " - "for the MetaData instance when using a " - "declarative base class." - ) - prop = clsregistry._deferred_relationship(cls, value) - our_stuff[k] = prop - - # set up attributes in the order they were created - our_stuff.sort(key=lambda key: our_stuff[key]._creation_order) - - # extract columns from the class dict - declared_columns = set() - name_to_prop_key = collections.defaultdict(set) - for key, c in list(our_stuff.items()): - if isinstance(c, (ColumnProperty, CompositeProperty)): - for col in c.columns: - if isinstance(col, Column) and \ - col.table is None: - _undefer_column_name(key, col) - if not isinstance(c, CompositeProperty): - name_to_prop_key[col.name].add(key) - declared_columns.add(col) - elif isinstance(c, Column): - _undefer_column_name(key, c) - name_to_prop_key[c.name].add(key) - declared_columns.add(c) - # if the column is the same name as the key, - # remove it from the explicit properties dict. - # the normal rules for assigning column-based properties - # will take over, including precedence of columns - # in multi-column ColumnProperties. - if key == c.key: - del our_stuff[key] - - for name, keys in name_to_prop_key.items(): - if len(keys) > 1: - util.warn( - "On class %r, Column object %r named directly multiple times, " - "only one will be used: %s" % - (classname, name, (", ".join(sorted(keys)))) - ) - - declared_columns = sorted( - declared_columns, key=lambda c: c._creation_order) - table = None - - if hasattr(cls, '__table_cls__'): - table_cls = util.unbound_method_to_callable(cls.__table_cls__) - else: - table_cls = Table - - if '__table__' not in dict_: - if tablename is not None: - - args, table_kw = (), {} - if table_args: - if isinstance(table_args, dict): - table_kw = table_args - elif isinstance(table_args, tuple): - if isinstance(table_args[-1], dict): - args, table_kw = table_args[0:-1], table_args[-1] - else: - args = table_args - - autoload = dict_.get('__autoload__') - if autoload: - table_kw['autoload'] = True - - cls.__table__ = table = table_cls( - tablename, cls.metadata, - *(tuple(declared_columns) + tuple(args)), - **table_kw) - else: - table = cls.__table__ - if declared_columns: - for c in declared_columns: - if not table.c.contains_column(c): - raise exc.ArgumentError( - "Can't add additional column %r when " - "specifying __table__" % c.key - ) - - if hasattr(cls, '__mapper_cls__'): - mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__) - else: - mapper_cls = mapper - - for c in cls.__bases__: - if _declared_mapping_info(c) is not None: - inherits = c - break - else: - inherits = None - - if table is None and inherits is None: - raise exc.InvalidRequestError( - "Class %r does not have a __table__ or __tablename__ " - "specified and does not inherit from an existing " - "table-mapped class." % cls - ) - elif inherits: - inherited_mapper = _declared_mapping_info(inherits) - inherited_table = inherited_mapper.local_table - inherited_mapped_table = inherited_mapper.mapped_table - - if table is None: - # single table inheritance. - # ensure no table args - if table_args: - raise exc.ArgumentError( - "Can't place __table_args__ on an inherited class " - "with no table." - ) - # add any columns declared here to the inherited table. - for c in declared_columns: - if c.primary_key: - raise exc.ArgumentError( - "Can't place primary key columns on an inherited " - "class with no table." - ) - if c.name in inherited_table.c: - if inherited_table.c[c.name] is c: - continue - raise exc.ArgumentError( - "Column '%s' on class %s conflicts with " - "existing column '%s'" % - (c, cls, inherited_table.c[c.name]) - ) - inherited_table.append_column(c) - if inherited_mapped_table is not None and \ - inherited_mapped_table is not inherited_table: - inherited_mapped_table._refresh_for_new_column(c) - - defer_map = hasattr(cls, '_sa_decl_prepare') - if defer_map: - cfg_cls = _DeferredMapperConfig - else: - cfg_cls = _MapperConfig - mt = cfg_cls(mapper_cls, - cls, table, - inherits, - declared_columns, - column_copies, - our_stuff, - mapper_args_fn) - if not defer_map: - mt.map() - - -class _MapperConfig(object): - - mapped_table = None - - def __init__(self, mapper_cls, - cls, - table, - inherits, - declared_columns, - column_copies, - properties, mapper_args_fn): - self.mapper_cls = mapper_cls - self.cls = cls - self.local_table = table - self.inherits = inherits - self.properties = properties - self.mapper_args_fn = mapper_args_fn - self.declared_columns = declared_columns - self.column_copies = column_copies - - - def _prepare_mapper_arguments(self): - properties = self.properties - if self.mapper_args_fn: - mapper_args = self.mapper_args_fn() - else: - mapper_args = {} - - # make sure that column copies are used rather - # than the original columns from any mixins - for k in ('version_id_col', 'polymorphic_on',): - if k in mapper_args: - v = mapper_args[k] - mapper_args[k] = self.column_copies.get(v, v) - - assert 'inherits' not in mapper_args, \ - "Can't specify 'inherits' explicitly with declarative mappings" - - if self.inherits: - mapper_args['inherits'] = self.inherits - - if self.inherits and not mapper_args.get('concrete', False): - # single or joined inheritance - # exclude any cols on the inherited table which are - # not mapped on the parent class, to avoid - # mapping columns specific to sibling/nephew classes - inherited_mapper = _declared_mapping_info(self.inherits) - inherited_table = inherited_mapper.local_table - - if 'exclude_properties' not in mapper_args: - mapper_args['exclude_properties'] = exclude_properties = \ - set([c.key for c in inherited_table.c - if c not in inherited_mapper._columntoproperty]) - exclude_properties.difference_update( - [c.key for c in self.declared_columns]) - - # look through columns in the current mapper that - # are keyed to a propname different than the colname - # (if names were the same, we'd have popped it out above, - # in which case the mapper makes this combination). - # See if the superclass has a similar column property. - # If so, join them together. - for k, col in list(properties.items()): - if not isinstance(col, expression.ColumnElement): - continue - if k in inherited_mapper._props: - p = inherited_mapper._props[k] - if isinstance(p, ColumnProperty): - # note here we place the subclass column - # first. See [ticket:1892] for background. - properties[k] = [col] + p.columns - result_mapper_args = mapper_args.copy() - result_mapper_args['properties'] = properties - return result_mapper_args - - def map(self): - mapper_args = self._prepare_mapper_arguments() - self.cls.__mapper__ = self.mapper_cls( - self.cls, - self.local_table, - **mapper_args - ) - -class _DeferredMapperConfig(_MapperConfig): - _configs = util.OrderedDict() - - @property - def cls(self): - return self._cls() - - @cls.setter - def cls(self, class_): - self._cls = weakref.ref(class_, self._remove_config_cls) - self._configs[self._cls] = self - - @classmethod - def _remove_config_cls(cls, ref): - cls._configs.pop(ref, None) - - @classmethod - def has_cls(cls, class_): - # 2.6 fails on weakref if class_ is an old style class - return isinstance(class_, type) and \ - weakref.ref(class_) in cls._configs - - @classmethod - def config_for_cls(cls, class_): - return cls._configs[weakref.ref(class_)] - - - @classmethod - def classes_for_base(cls, base_cls): - return [m for m in cls._configs.values() - if issubclass(m.cls, base_cls)] - - def map(self): - self._configs.pop(self._cls, None) - super(_DeferredMapperConfig, self).map() - - -def _add_attribute(cls, key, value): - """add an attribute to an existing declarative class. - - This runs through the logic to determine MapperProperty, - adds it to the Mapper, adds a column to the mapped Table, etc. - - """ - - if '__mapper__' in cls.__dict__: - if isinstance(value, Column): - _undefer_column_name(key, value) - cls.__table__.append_column(value) - cls.__mapper__.add_property(key, value) - elif isinstance(value, ColumnProperty): - for col in value.columns: - if isinstance(col, Column) and col.table is None: - _undefer_column_name(key, col) - cls.__table__.append_column(col) - cls.__mapper__.add_property(key, value) - elif isinstance(value, MapperProperty): - cls.__mapper__.add_property( - key, - clsregistry._deferred_relationship(cls, value) - ) - elif isinstance(value, QueryableAttribute) and value.key != key: - # detect a QueryableAttribute that's already mapped being - # assigned elsewhere in userland, turn into a synonym() - value = synonym(value.key) - cls.__mapper__.add_property( - key, - clsregistry._deferred_relationship(cls, value) - ) - else: - type.__setattr__(cls, key, value) - else: - type.__setattr__(cls, key, value) - - -def _declarative_constructor(self, **kwargs): - """A simple constructor that allows initialization from kwargs. - - Sets attributes on the constructed instance using the names and - values in ``kwargs``. - - Only keys that are present as - attributes of the instance's class are allowed. These could be, - for example, any mapped columns or relationships. - """ - cls_ = type(self) - for k in kwargs: - if not hasattr(cls_, k): - raise TypeError( - "%r is an invalid keyword argument for %s" % - (k, cls_.__name__)) - setattr(self, k, kwargs[k]) -_declarative_constructor.__name__ = '__init__' - - -def _undefer_column_name(key, column): - if column.key is None: - column.key = key - if column.name is None: - column.name = key diff --git a/libs/sqlalchemy/ext/declarative/clsregistry.py b/libs/sqlalchemy/ext/declarative/clsregistry.py deleted file mode 100644 index fda1cffb..00000000 --- a/libs/sqlalchemy/ext/declarative/clsregistry.py +++ /dev/null @@ -1,305 +0,0 @@ -# ext/declarative/clsregistry.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Routines to handle the string class registry used by declarative. - -This system allows specification of classes and expressions used in -:func:`.relationship` using strings. - -""" -from ...orm.properties import ColumnProperty, RelationshipProperty, \ - SynonymProperty -from ...schema import _get_table_key -from ...orm import class_mapper, interfaces -from ... import util -from ... import exc -import weakref - -# strong references to registries which we place in -# the _decl_class_registry, which is usually weak referencing. -# the internal registries here link to classes with weakrefs and remove -# themselves when all references to contained classes are removed. -_registries = set() - - -def add_class(classname, cls): - """Add a class to the _decl_class_registry associated with the - given declarative class. - - """ - if classname in cls._decl_class_registry: - # class already exists. - existing = cls._decl_class_registry[classname] - if not isinstance(existing, _MultipleClassMarker): - existing = \ - cls._decl_class_registry[classname] = \ - _MultipleClassMarker([cls, existing]) - else: - cls._decl_class_registry[classname] = cls - - try: - root_module = cls._decl_class_registry['_sa_module_registry'] - except KeyError: - cls._decl_class_registry['_sa_module_registry'] = \ - root_module = _ModuleMarker('_sa_module_registry', None) - - tokens = cls.__module__.split(".") - - # build up a tree like this: - # modulename: myapp.snacks.nuts - # - # myapp->snack->nuts->(classes) - # snack->nuts->(classes) - # nuts->(classes) - # - # this allows partial token paths to be used. - while tokens: - token = tokens.pop(0) - module = root_module.get_module(token) - for token in tokens: - module = module.get_module(token) - module.add_class(classname, cls) - - -class _MultipleClassMarker(object): - """refers to multiple classes of the same name - within _decl_class_registry. - - """ - - def __init__(self, classes, on_remove=None): - self.on_remove = on_remove - self.contents = set([ - weakref.ref(item, self._remove_item) for item in classes]) - _registries.add(self) - - def __iter__(self): - return (ref() for ref in self.contents) - - def attempt_get(self, path, key): - if len(self.contents) > 1: - raise exc.InvalidRequestError( - "Multiple classes found for path \"%s\" " - "in the registry of this declarative " - "base. Please use a fully module-qualified path." % - (".".join(path + [key])) - ) - else: - ref = list(self.contents)[0] - cls = ref() - if cls is None: - raise NameError(key) - return cls - - def _remove_item(self, ref): - self.contents.remove(ref) - if not self.contents: - _registries.discard(self) - if self.on_remove: - self.on_remove() - - def add_item(self, item): - modules = set([cls().__module__ for cls in self.contents]) - if item.__module__ in modules: - util.warn( - "This declarative base already contains a class with the " - "same class name and module name as %s.%s, and will " - "be replaced in the string-lookup table." % ( - item.__module__, - item.__name__ - ) - ) - self.contents.add(weakref.ref(item, self._remove_item)) - - -class _ModuleMarker(object): - """"refers to a module name within - _decl_class_registry. - - """ - def __init__(self, name, parent): - self.parent = parent - self.name = name - self.contents = {} - self.mod_ns = _ModNS(self) - if self.parent: - self.path = self.parent.path + [self.name] - else: - self.path = [] - _registries.add(self) - - def __contains__(self, name): - return name in self.contents - - def __getitem__(self, name): - return self.contents[name] - - def _remove_item(self, name): - self.contents.pop(name, None) - if not self.contents and self.parent is not None: - self.parent._remove_item(self.name) - _registries.discard(self) - - def resolve_attr(self, key): - return getattr(self.mod_ns, key) - - def get_module(self, name): - if name not in self.contents: - marker = _ModuleMarker(name, self) - self.contents[name] = marker - else: - marker = self.contents[name] - return marker - - def add_class(self, name, cls): - if name in self.contents: - existing = self.contents[name] - existing.add_item(cls) - else: - existing = self.contents[name] = \ - _MultipleClassMarker([cls], - on_remove=lambda: self._remove_item(name)) - - -class _ModNS(object): - def __init__(self, parent): - self.__parent = parent - - def __getattr__(self, key): - try: - value = self.__parent.contents[key] - except KeyError: - pass - else: - if value is not None: - if isinstance(value, _ModuleMarker): - return value.mod_ns - else: - assert isinstance(value, _MultipleClassMarker) - return value.attempt_get(self.__parent.path, key) - raise AttributeError("Module %r has no mapped classes " - "registered under the name %r" % (self.__parent.name, key)) - - -class _GetColumns(object): - def __init__(self, cls): - self.cls = cls - - def __getattr__(self, key): - mp = class_mapper(self.cls, configure=False) - if mp: - if key not in mp.all_orm_descriptors: - raise exc.InvalidRequestError( - "Class %r does not have a mapped column named %r" - % (self.cls, key)) - - desc = mp.all_orm_descriptors[key] - if desc.extension_type is interfaces.NOT_EXTENSION: - prop = desc.property - if isinstance(prop, SynonymProperty): - key = prop.name - elif not isinstance(prop, ColumnProperty): - raise exc.InvalidRequestError( - "Property %r is not an instance of" - " ColumnProperty (i.e. does not correspond" - " directly to a Column)." % key) - return getattr(self.cls, key) - - -class _GetTable(object): - def __init__(self, key, metadata): - self.key = key - self.metadata = metadata - - def __getattr__(self, key): - return self.metadata.tables[ - _get_table_key(key, self.key) - ] - - -def _determine_container(key, value): - if isinstance(value, _MultipleClassMarker): - value = value.attempt_get([], key) - return _GetColumns(value) - - -class _class_resolver(object): - def __init__(self, cls, prop, fallback, arg): - self.cls = cls - self.prop = prop - self.arg = self._declarative_arg = arg - self.fallback = fallback - self._dict = util.PopulateDict(self._access_cls) - self._resolvers = () - - def _access_cls(self, key): - cls = self.cls - if key in cls._decl_class_registry: - return _determine_container(key, cls._decl_class_registry[key]) - elif key in cls.metadata.tables: - return cls.metadata.tables[key] - elif key in cls.metadata._schemas: - return _GetTable(key, cls.metadata) - elif '_sa_module_registry' in cls._decl_class_registry and \ - key in cls._decl_class_registry['_sa_module_registry']: - registry = cls._decl_class_registry['_sa_module_registry'] - return registry.resolve_attr(key) - elif self._resolvers: - for resolv in self._resolvers: - value = resolv(key) - if value is not None: - return value - - return self.fallback[key] - - def __call__(self): - try: - x = eval(self.arg, globals(), self._dict) - - if isinstance(x, _GetColumns): - return x.cls - else: - return x - except NameError as n: - raise exc.InvalidRequestError( - "When initializing mapper %s, expression %r failed to " - "locate a name (%r). If this is a class name, consider " - "adding this relationship() to the %r class after " - "both dependent classes have been defined." % - (self.prop.parent, self.arg, n.args[0], self.cls) - ) - - -def _resolver(cls, prop): - import sqlalchemy - from sqlalchemy.orm import foreign, remote - - fallback = sqlalchemy.__dict__.copy() - fallback.update({'foreign': foreign, 'remote': remote}) - - def resolve_arg(arg): - return _class_resolver(cls, prop, fallback, arg) - return resolve_arg - - -def _deferred_relationship(cls, prop): - - if isinstance(prop, RelationshipProperty): - resolve_arg = _resolver(cls, prop) - - for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', - 'secondary', '_user_defined_foreign_keys', 'remote_side'): - v = getattr(prop, attr) - if isinstance(v, util.string_types): - setattr(prop, attr, resolve_arg(v)) - - if prop.backref and isinstance(prop.backref, tuple): - key, kwargs = prop.backref - for attr in ('primaryjoin', 'secondaryjoin', 'secondary', - 'foreign_keys', 'remote_side', 'order_by'): - if attr in kwargs and isinstance(kwargs[attr], str): - kwargs[attr] = resolve_arg(kwargs[attr]) - - return prop diff --git a/libs/sqlalchemy/ext/horizontal_shard.py b/libs/sqlalchemy/ext/horizontal_shard.py deleted file mode 100644 index 8b3f968d..00000000 --- a/libs/sqlalchemy/ext/horizontal_shard.py +++ /dev/null @@ -1,128 +0,0 @@ -# ext/horizontal_shard.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Horizontal sharding support. - -Defines a rudimental 'horizontal sharding' system which allows a Session to -distribute queries and persistence operations across multiple databases. - -For a usage example, see the :ref:`examples_sharding` example included in -the source distribution. - -""" - -from .. import util -from ..orm.session import Session -from ..orm.query import Query - -__all__ = ['ShardedSession', 'ShardedQuery'] - - -class ShardedQuery(Query): - def __init__(self, *args, **kwargs): - super(ShardedQuery, self).__init__(*args, **kwargs) - self.id_chooser = self.session.id_chooser - self.query_chooser = self.session.query_chooser - self._shard_id = None - - def set_shard(self, shard_id): - """return a new query, limited to a single shard ID. - - all subsequent operations with the returned query will - be against the single shard regardless of other state. - """ - - q = self._clone() - q._shard_id = shard_id - return q - - def _execute_and_instances(self, context): - def iter_for_shard(shard_id): - context.attributes['shard_id'] = shard_id - result = self._connection_from_session( - mapper=self._mapper_zero(), - shard_id=shard_id).execute( - context.statement, - self._params) - return self.instances(result, context) - - if self._shard_id is not None: - return iter_for_shard(self._shard_id) - else: - partial = [] - for shard_id in self.query_chooser(self): - partial.extend(iter_for_shard(shard_id)) - - # if some kind of in memory 'sorting' - # were done, this is where it would happen - return iter(partial) - - def get(self, ident, **kwargs): - if self._shard_id is not None: - return super(ShardedQuery, self).get(ident) - else: - ident = util.to_list(ident) - for shard_id in self.id_chooser(self, ident): - o = self.set_shard(shard_id).get(ident, **kwargs) - if o is not None: - return o - else: - return None - - -class ShardedSession(Session): - def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None, - query_cls=ShardedQuery, **kwargs): - """Construct a ShardedSession. - - :param shard_chooser: A callable which, passed a Mapper, a mapped - instance, and possibly a SQL clause, returns a shard ID. This id - may be based off of the attributes present within the object, or on - some round-robin scheme. If the scheme is based on a selection, it - should set whatever state on the instance to mark it in the future as - participating in that shard. - - :param id_chooser: A callable, passed a query and a tuple of identity - values, which should return a list of shard ids where the ID might - reside. The databases will be queried in the order of this listing. - - :param query_chooser: For a given Query, returns the list of shard_ids - where the query should be issued. Results from all shards returned - will be combined together into a single listing. - - :param shards: A dictionary of string shard names - to :class:`~sqlalchemy.engine.Engine` objects. - - """ - super(ShardedSession, self).__init__(query_cls=query_cls, **kwargs) - self.shard_chooser = shard_chooser - self.id_chooser = id_chooser - self.query_chooser = query_chooser - self.__binds = {} - self.connection_callable = self.connection - if shards is not None: - for k in shards: - self.bind_shard(k, shards[k]) - - def connection(self, mapper=None, instance=None, shard_id=None, **kwargs): - if shard_id is None: - shard_id = self.shard_chooser(mapper, instance) - - if self.transaction is not None: - return self.transaction.connection(mapper, shard_id=shard_id) - else: - return self.get_bind(mapper, - shard_id=shard_id, - instance=instance).contextual_connect(**kwargs) - - def get_bind(self, mapper, shard_id=None, - instance=None, clause=None, **kw): - if shard_id is None: - shard_id = self.shard_chooser(mapper, instance, clause=clause) - return self.__binds[shard_id] - - def bind_shard(self, shard_id, bind): - self.__binds[shard_id] = bind diff --git a/libs/sqlalchemy/ext/hybrid.py b/libs/sqlalchemy/ext/hybrid.py deleted file mode 100644 index 576e0bd4..00000000 --- a/libs/sqlalchemy/ext/hybrid.py +++ /dev/null @@ -1,808 +0,0 @@ -# ext/hybrid.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Define attributes on ORM-mapped classes that have "hybrid" behavior. - -"hybrid" means the attribute has distinct behaviors defined at the -class level and at the instance level. - -The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of -method decorator, is around 50 lines of code and has almost no -dependencies on the rest of SQLAlchemy. It can, in theory, work with -any descriptor-based expression system. - -Consider a mapping ``Interval``, representing integer ``start`` and ``end`` -values. We can define higher level functions on mapped classes that produce -SQL expressions at the class level, and Python expression evaluation at the -instance level. Below, each function decorated with :class:`.hybrid_method` or -:class:`.hybrid_property` may receive ``self`` as an instance of the class, or -as the class itself:: - - from sqlalchemy import Column, Integer - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import Session, aliased - from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method - - Base = declarative_base() - - class Interval(Base): - __tablename__ = 'interval' - - id = Column(Integer, primary_key=True) - start = Column(Integer, nullable=False) - end = Column(Integer, nullable=False) - - def __init__(self, start, end): - self.start = start - self.end = end - - @hybrid_property - def length(self): - return self.end - self.start - - @hybrid_method - def contains(self,point): - return (self.start <= point) & (point < self.end) - - @hybrid_method - def intersects(self, other): - return self.contains(other.start) | self.contains(other.end) - -Above, the ``length`` property returns the difference between the -``end`` and ``start`` attributes. With an instance of ``Interval``, -this subtraction occurs in Python, using normal Python descriptor -mechanics:: - - >>> i1 = Interval(5, 10) - >>> i1.length - 5 - -When dealing with the ``Interval`` class itself, the :class:`.hybrid_property` -descriptor evaluates the function body given the ``Interval`` class as -the argument, which when evaluated with SQLAlchemy expression mechanics -returns a new SQL expression:: - - >>> print Interval.length - interval."end" - interval.start - - >>> print Session().query(Interval).filter(Interval.length > 10) - SELECT interval.id AS interval_id, interval.start AS interval_start, - interval."end" AS interval_end - FROM interval - WHERE interval."end" - interval.start > :param_1 - -ORM methods such as :meth:`~.Query.filter_by` generally use ``getattr()`` to -locate attributes, so can also be used with hybrid attributes:: - - >>> print Session().query(Interval).filter_by(length=5) - SELECT interval.id AS interval_id, interval.start AS interval_start, - interval."end" AS interval_end - FROM interval - WHERE interval."end" - interval.start = :param_1 - -The ``Interval`` class example also illustrates two methods, -``contains()`` and ``intersects()``, decorated with -:class:`.hybrid_method`. This decorator applies the same idea to -methods that :class:`.hybrid_property` applies to attributes. The -methods return boolean values, and take advantage of the Python ``|`` -and ``&`` bitwise operators to produce equivalent instance-level and -SQL expression-level boolean behavior:: - - >>> i1.contains(6) - True - >>> i1.contains(15) - False - >>> i1.intersects(Interval(7, 18)) - True - >>> i1.intersects(Interval(25, 29)) - False - - >>> print Session().query(Interval).filter(Interval.contains(15)) - SELECT interval.id AS interval_id, interval.start AS interval_start, - interval."end" AS interval_end - FROM interval - WHERE interval.start <= :start_1 AND interval."end" > :end_1 - - >>> ia = aliased(Interval) - >>> print Session().query(Interval, ia).filter(Interval.intersects(ia)) - SELECT interval.id AS interval_id, interval.start AS interval_start, - interval."end" AS interval_end, interval_1.id AS interval_1_id, - interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end - FROM interval, interval AS interval_1 - WHERE interval.start <= interval_1.start - AND interval."end" > interval_1.start - OR interval.start <= interval_1."end" - AND interval."end" > interval_1."end" - -Defining Expression Behavior Distinct from Attribute Behavior --------------------------------------------------------------- - -Our usage of the ``&`` and ``|`` bitwise operators above was -fortunate, considering our functions operated on two boolean values to -return a new one. In many cases, the construction of an in-Python -function and a SQLAlchemy SQL expression have enough differences that -two separate Python expressions should be defined. The -:mod:`~sqlalchemy.ext.hybrid` decorators define the -:meth:`.hybrid_property.expression` modifier for this purpose. As an -example we'll define the radius of the interval, which requires the -usage of the absolute value function:: - - from sqlalchemy import func - - class Interval(object): - # ... - - @hybrid_property - def radius(self): - return abs(self.length) / 2 - - @radius.expression - def radius(cls): - return func.abs(cls.length) / 2 - -Above the Python function ``abs()`` is used for instance-level -operations, the SQL function ``ABS()`` is used via the :attr:`.func` -object for class-level expressions:: - - >>> i1.radius - 2 - - >>> print Session().query(Interval).filter(Interval.radius > 5) - SELECT interval.id AS interval_id, interval.start AS interval_start, - interval."end" AS interval_end - FROM interval - WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1 - -Defining Setters ----------------- - -Hybrid properties can also define setter methods. If we wanted -``length`` above, when set, to modify the endpoint value:: - - class Interval(object): - # ... - - @hybrid_property - def length(self): - return self.end - self.start - - @length.setter - def length(self, value): - self.end = self.start + value - -The ``length(self, value)`` method is now called upon set:: - - >>> i1 = Interval(5, 10) - >>> i1.length - 5 - >>> i1.length = 12 - >>> i1.end - 17 - -Working with Relationships --------------------------- - -There's no essential difference when creating hybrids that work with -related objects as opposed to column-based data. The need for distinct -expressions tends to be greater. Two variants of we'll illustrate -are the "join-dependent" hybrid, and the "correlated subquery" hybrid. - -Join-Dependent Relationship Hybrid -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Consider the following declarative -mapping which relates a ``User`` to a ``SavingsAccount``:: - - from sqlalchemy import Column, Integer, ForeignKey, Numeric, String - from sqlalchemy.orm import relationship - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.ext.hybrid import hybrid_property - - Base = declarative_base() - - class SavingsAccount(Base): - __tablename__ = 'account' - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey('user.id'), nullable=False) - balance = Column(Numeric(15, 5)) - - class User(Base): - __tablename__ = 'user' - id = Column(Integer, primary_key=True) - name = Column(String(100), nullable=False) - - accounts = relationship("SavingsAccount", backref="owner") - - @hybrid_property - def balance(self): - if self.accounts: - return self.accounts[0].balance - else: - return None - - @balance.setter - def balance(self, value): - if not self.accounts: - account = Account(owner=self) - else: - account = self.accounts[0] - account.balance = value - - @balance.expression - def balance(cls): - return SavingsAccount.balance - -The above hybrid property ``balance`` works with the first -``SavingsAccount`` entry in the list of accounts for this user. The -in-Python getter/setter methods can treat ``accounts`` as a Python -list available on ``self``. - -However, at the expression level, it's expected that the ``User`` class will -be used in an appropriate context such that an appropriate join to -``SavingsAccount`` will be present:: - - >>> print Session().query(User, User.balance).\\ - ... join(User.accounts).filter(User.balance > 5000) - SELECT "user".id AS user_id, "user".name AS user_name, - account.balance AS account_balance - FROM "user" JOIN account ON "user".id = account.user_id - WHERE account.balance > :balance_1 - -Note however, that while the instance level accessors need to worry -about whether ``self.accounts`` is even present, this issue expresses -itself differently at the SQL expression level, where we basically -would use an outer join:: - - >>> from sqlalchemy import or_ - >>> print (Session().query(User, User.balance).outerjoin(User.accounts). - ... filter(or_(User.balance < 5000, User.balance == None))) - SELECT "user".id AS user_id, "user".name AS user_name, - account.balance AS account_balance - FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id - WHERE account.balance < :balance_1 OR account.balance IS NULL - -Correlated Subquery Relationship Hybrid -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We can, of course, forego being dependent on the enclosing query's usage -of joins in favor of the correlated subquery, which can portably be packed -into a single column expression. A correlated subquery is more portable, but -often performs more poorly at the SQL level. Using the same technique -illustrated at :ref:`mapper_column_property_sql_expressions`, -we can adjust our ``SavingsAccount`` example to aggregate the balances for -*all* accounts, and use a correlated subquery for the column expression:: - - from sqlalchemy import Column, Integer, ForeignKey, Numeric, String - from sqlalchemy.orm import relationship - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.ext.hybrid import hybrid_property - from sqlalchemy import select, func - - Base = declarative_base() - - class SavingsAccount(Base): - __tablename__ = 'account' - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey('user.id'), nullable=False) - balance = Column(Numeric(15, 5)) - - class User(Base): - __tablename__ = 'user' - id = Column(Integer, primary_key=True) - name = Column(String(100), nullable=False) - - accounts = relationship("SavingsAccount", backref="owner") - - @hybrid_property - def balance(self): - return sum(acc.balance for acc in self.accounts) - - @balance.expression - def balance(cls): - return select([func.sum(SavingsAccount.balance)]).\\ - where(SavingsAccount.user_id==cls.id).\\ - label('total_balance') - -The above recipe will give us the ``balance`` column which renders -a correlated SELECT:: - - >>> print s.query(User).filter(User.balance > 400) - SELECT "user".id AS user_id, "user".name AS user_name - FROM "user" - WHERE (SELECT sum(account.balance) AS sum_1 - FROM account - WHERE account.user_id = "user".id) > :param_1 - -.. _hybrid_custom_comparators: - -Building Custom Comparators ---------------------------- - -The hybrid property also includes a helper that allows construction of -custom comparators. A comparator object allows one to customize the -behavior of each SQLAlchemy expression operator individually. They -are useful when creating custom types that have some highly -idiosyncratic behavior on the SQL side. - -The example class below allows case-insensitive comparisons on the attribute -named ``word_insensitive``:: - - from sqlalchemy.ext.hybrid import Comparator, hybrid_property - from sqlalchemy import func, Column, Integer, String - from sqlalchemy.orm import Session - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class CaseInsensitiveComparator(Comparator): - def __eq__(self, other): - return func.lower(self.__clause_element__()) == func.lower(other) - - class SearchWord(Base): - __tablename__ = 'searchword' - id = Column(Integer, primary_key=True) - word = Column(String(255), nullable=False) - - @hybrid_property - def word_insensitive(self): - return self.word.lower() - - @word_insensitive.comparator - def word_insensitive(cls): - return CaseInsensitiveComparator(cls.word) - -Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()`` -SQL function to both sides:: - - >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks") - SELECT searchword.id AS searchword_id, searchword.word AS searchword_word - FROM searchword - WHERE lower(searchword.word) = lower(:lower_1) - -The ``CaseInsensitiveComparator`` above implements part of the -:class:`.ColumnOperators` interface. A "coercion" operation like -lowercasing can be applied to all comparison operations (i.e. ``eq``, -``lt``, ``gt``, etc.) using :meth:`.Operators.operate`:: - - class CaseInsensitiveComparator(Comparator): - def operate(self, op, other): - return op(func.lower(self.__clause_element__()), func.lower(other)) - -Hybrid Value Objects --------------------- - -Note in our previous example, if we were to compare the -``word_insensitive`` attribute of a ``SearchWord`` instance to a plain -Python string, the plain Python string would not be coerced to lower -case - the ``CaseInsensitiveComparator`` we built, being returned by -``@word_insensitive.comparator``, only applies to the SQL side. - -A more comprehensive form of the custom comparator is to construct a -*Hybrid Value Object*. This technique applies the target value or -expression to a value object which is then returned by the accessor in -all cases. The value object allows control of all operations upon -the value as well as how compared values are treated, both on the SQL -expression side as well as the Python value side. Replacing the -previous ``CaseInsensitiveComparator`` class with a new -``CaseInsensitiveWord`` class:: - - class CaseInsensitiveWord(Comparator): - "Hybrid value representing a lower case representation of a word." - - def __init__(self, word): - if isinstance(word, basestring): - self.word = word.lower() - elif isinstance(word, CaseInsensitiveWord): - self.word = word.word - else: - self.word = func.lower(word) - - def operate(self, op, other): - if not isinstance(other, CaseInsensitiveWord): - other = CaseInsensitiveWord(other) - return op(self.word, other.word) - - def __clause_element__(self): - return self.word - - def __str__(self): - return self.word - - key = 'word' - "Label to apply to Query tuple results" - -Above, the ``CaseInsensitiveWord`` object represents ``self.word``, -which may be a SQL function, or may be a Python native. By -overriding ``operate()`` and ``__clause_element__()`` to work in terms -of ``self.word``, all comparison operations will work against the -"converted" form of ``word``, whether it be SQL side or Python side. -Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` -object unconditionally from a single hybrid call:: - - class SearchWord(Base): - __tablename__ = 'searchword' - id = Column(Integer, primary_key=True) - word = Column(String(255), nullable=False) - - @hybrid_property - def word_insensitive(self): - return CaseInsensitiveWord(self.word) - -The ``word_insensitive`` attribute now has case-insensitive comparison -behavior universally, including SQL expression vs. Python expression -(note the Python value is converted to lower case on the Python side -here):: - - >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks") - SELECT searchword.id AS searchword_id, searchword.word AS searchword_word - FROM searchword - WHERE lower(searchword.word) = :lower_1 - -SQL expression versus SQL expression:: - - >>> sw1 = aliased(SearchWord) - >>> sw2 = aliased(SearchWord) - >>> print Session().query( - ... sw1.word_insensitive, - ... sw2.word_insensitive).\\ - ... filter( - ... sw1.word_insensitive > sw2.word_insensitive - ... ) - SELECT lower(searchword_1.word) AS lower_1, - lower(searchword_2.word) AS lower_2 - FROM searchword AS searchword_1, searchword AS searchword_2 - WHERE lower(searchword_1.word) > lower(searchword_2.word) - -Python only expression:: - - >>> ws1 = SearchWord(word="SomeWord") - >>> ws1.word_insensitive == "sOmEwOrD" - True - >>> ws1.word_insensitive == "XOmEwOrX" - False - >>> print ws1.word_insensitive - someword - -The Hybrid Value pattern is very useful for any kind of value that may -have multiple representations, such as timestamps, time deltas, units -of measurement, currencies and encrypted passwords. - -.. seealso:: - - `Hybrids and Value Agnostic Types - `_ - - on the techspot.zzzeek.org blog - - `Value Agnostic Types, Part II - `_ - - on the techspot.zzzeek.org blog - -.. _hybrid_transformers: - -Building Transformers ----------------------- - -A *transformer* is an object which can receive a :class:`.Query` -object and return a new one. The :class:`.Query` object includes a -method :meth:`.with_transformation` that returns a new :class:`.Query` -transformed by the given function. - -We can combine this with the :class:`.Comparator` class to produce one type -of recipe which can both set up the FROM clause of a query as well as assign -filtering criterion. - -Consider a mapped class ``Node``, which assembles using adjacency list -into a hierarchical tree pattern:: - - from sqlalchemy import Column, Integer, ForeignKey - from sqlalchemy.orm import relationship - from sqlalchemy.ext.declarative import declarative_base - Base = declarative_base() - - class Node(Base): - __tablename__ = 'node' - id =Column(Integer, primary_key=True) - parent_id = Column(Integer, ForeignKey('node.id')) - parent = relationship("Node", remote_side=id) - -Suppose we wanted to add an accessor ``grandparent``. This would -return the ``parent`` of ``Node.parent``. When we have an instance of -``Node``, this is simple:: - - from sqlalchemy.ext.hybrid import hybrid_property - - class Node(Base): - # ... - - @hybrid_property - def grandparent(self): - return self.parent.parent - -For the expression, things are not so clear. We'd need to construct -a :class:`.Query` where we :meth:`~.Query.join` twice along -``Node.parent`` to get to the ``grandparent``. We can instead return -a transforming callable that we'll combine with the -:class:`.Comparator` class to receive any :class:`.Query` object, and -return a new one that's joined to the ``Node.parent`` attribute and -filtered based on the given criterion:: - - from sqlalchemy.ext.hybrid import Comparator - - class GrandparentTransformer(Comparator): - def operate(self, op, other): - def transform(q): - cls = self.__clause_element__() - parent_alias = aliased(cls) - return q.join(parent_alias, cls.parent).\\ - filter(op(parent_alias.parent, other)) - return transform - - Base = declarative_base() - - class Node(Base): - __tablename__ = 'node' - id =Column(Integer, primary_key=True) - parent_id = Column(Integer, ForeignKey('node.id')) - parent = relationship("Node", remote_side=id) - - @hybrid_property - def grandparent(self): - return self.parent.parent - - @grandparent.comparator - def grandparent(cls): - return GrandparentTransformer(cls) - -The ``GrandparentTransformer`` overrides the core -:meth:`.Operators.operate` method at the base of the -:class:`.Comparator` hierarchy to return a query-transforming -callable, which then runs the given comparison operation in a -particular context. Such as, in the example above, the ``operate`` -method is called, given the :attr:`.Operators.eq` callable as well as -the right side of the comparison ``Node(id=5)``. A function -``transform`` is then returned which will transform a :class:`.Query` -first to join to ``Node.parent``, then to compare ``parent_alias`` -using :attr:`.Operators.eq` against the left and right sides, passing -into :class:`.Query.filter`: - -.. sourcecode:: pycon+sql - - >>> from sqlalchemy.orm import Session - >>> session = Session() - {sql}>>> session.query(Node).\\ - ... with_transformation(Node.grandparent==Node(id=5)).\\ - ... all() - SELECT node.id AS node_id, node.parent_id AS node_parent_id - FROM node JOIN node AS node_1 ON node_1.id = node.parent_id - WHERE :param_1 = node_1.parent_id - {stop} - -We can modify the pattern to be more verbose but flexible by separating -the "join" step from the "filter" step. The tricky part here is ensuring -that successive instances of ``GrandparentTransformer`` use the same -:class:`.AliasedClass` object against ``Node``. Below we use a simple -memoizing approach that associates a ``GrandparentTransformer`` -with each class:: - - class Node(Base): - - # ... - - @grandparent.comparator - def grandparent(cls): - # memoize a GrandparentTransformer - # per class - if '_gp' not in cls.__dict__: - cls._gp = GrandparentTransformer(cls) - return cls._gp - - class GrandparentTransformer(Comparator): - - def __init__(self, cls): - self.parent_alias = aliased(cls) - - @property - def join(self): - def go(q): - return q.join(self.parent_alias, Node.parent) - return go - - def operate(self, op, other): - return op(self.parent_alias.parent, other) - -.. sourcecode:: pycon+sql - - {sql}>>> session.query(Node).\\ - ... with_transformation(Node.grandparent.join).\\ - ... filter(Node.grandparent==Node(id=5)) - SELECT node.id AS node_id, node.parent_id AS node_parent_id - FROM node JOIN node AS node_1 ON node_1.id = node.parent_id - WHERE :param_1 = node_1.parent_id - {stop} - -The "transformer" pattern is an experimental pattern that starts -to make usage of some functional programming paradigms. -While it's only recommended for advanced and/or patient developers, -there's probably a whole lot of amazing things it can be used for. - -""" -from .. import util -from ..orm import attributes, interfaces - -HYBRID_METHOD = util.symbol('HYBRID_METHOD') -"""Symbol indicating an :class:`_InspectionAttr` that's - of type :class:`.hybrid_method`. - - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. - - .. seealso:: - - :attr:`.Mapper.all_orm_attributes` - -""" - -HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY') -"""Symbol indicating an :class:`_InspectionAttr` that's - of type :class:`.hybrid_method`. - - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. - - .. seealso:: - - :attr:`.Mapper.all_orm_attributes` - -""" - -class hybrid_method(interfaces._InspectionAttr): - """A decorator which allows definition of a Python object method with both - instance-level and class-level behavior. - - """ - - is_attribute = True - extension_type = HYBRID_METHOD - - def __init__(self, func, expr=None): - """Create a new :class:`.hybrid_method`. - - Usage is typically via decorator:: - - from sqlalchemy.ext.hybrid import hybrid_method - - class SomeClass(object): - @hybrid_method - def value(self, x, y): - return self._value + x + y - - @value.expression - def value(self, x, y): - return func.some_function(self._value, x, y) - - """ - self.func = func - self.expr = expr or func - - def __get__(self, instance, owner): - if instance is None: - return self.expr.__get__(owner, owner.__class__) - else: - return self.func.__get__(instance, owner) - - def expression(self, expr): - """Provide a modifying decorator that defines a - SQL-expression producing method.""" - - self.expr = expr - return self - - -class hybrid_property(interfaces._InspectionAttr): - """A decorator which allows definition of a Python descriptor with both - instance-level and class-level behavior. - - """ - - is_attribute = True - extension_type = HYBRID_PROPERTY - - def __init__(self, fget, fset=None, fdel=None, expr=None): - """Create a new :class:`.hybrid_property`. - - Usage is typically via decorator:: - - from sqlalchemy.ext.hybrid import hybrid_property - - class SomeClass(object): - @hybrid_property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - - """ - self.fget = fget - self.fset = fset - self.fdel = fdel - self.expr = expr or fget - util.update_wrapper(self, fget) - - def __get__(self, instance, owner): - if instance is None: - return self.expr(owner) - else: - return self.fget(instance) - - def __set__(self, instance, value): - if self.fset is None: - raise AttributeError("can't set attribute") - self.fset(instance, value) - - def __delete__(self, instance): - if self.fdel is None: - raise AttributeError("can't delete attribute") - self.fdel(instance) - - def setter(self, fset): - """Provide a modifying decorator that defines a value-setter method.""" - - self.fset = fset - return self - - def deleter(self, fdel): - """Provide a modifying decorator that defines a - value-deletion method.""" - - self.fdel = fdel - return self - - def expression(self, expr): - """Provide a modifying decorator that defines a SQL-expression - producing method.""" - - self.expr = expr - return self - - def comparator(self, comparator): - """Provide a modifying decorator that defines a custom - comparator producing method. - - The return value of the decorated method should be an instance of - :class:`~.hybrid.Comparator`. - - """ - - proxy_attr = attributes.\ - create_proxied_attribute(self) - - def expr(owner): - return proxy_attr(owner, self.__name__, self, comparator(owner)) - self.expr = expr - return self - - -class Comparator(interfaces.PropComparator): - """A helper class that allows easy construction of custom - :class:`~.orm.interfaces.PropComparator` - classes for usage with hybrids.""" - - property = None - - def __init__(self, expression): - self.expression = expression - - def __clause_element__(self): - expr = self.expression - while hasattr(expr, '__clause_element__'): - expr = expr.__clause_element__() - return expr - - def adapt_to_entity(self, adapt_to_entity): - # interesting.... - return self diff --git a/libs/sqlalchemy/ext/instrumentation.py b/libs/sqlalchemy/ext/instrumentation.py deleted file mode 100644 index 2cf36e9b..00000000 --- a/libs/sqlalchemy/ext/instrumentation.py +++ /dev/null @@ -1,407 +0,0 @@ -"""Extensible class instrumentation. - -The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate -systems of class instrumentation within the ORM. Class instrumentation -refers to how the ORM places attributes on the class which maintain -data and track changes to that data, as well as event hooks installed -on the class. - -.. note:: - The extension package is provided for the benefit of integration - with other object management packages, which already perform - their own instrumentation. It is not intended for general use. - -For examples of how the instrumentation extension is used, -see the example :ref:`examples_instrumentation`. - -.. versionchanged:: 0.8 - The :mod:`sqlalchemy.orm.instrumentation` was split out so - that all functionality having to do with non-standard - instrumentation was moved out to :mod:`sqlalchemy.ext.instrumentation`. - When imported, the module installs itself within - :mod:`sqlalchemy.orm.instrumentation` so that it - takes effect, including recognition of - ``__sa_instrumentation_manager__`` on mapped classes, as - well :data:`.instrumentation_finders` - being used to determine class instrumentation resolution. - -""" -from ..orm import instrumentation as orm_instrumentation -from ..orm.instrumentation import ( - ClassManager, InstrumentationFactory, _default_state_getter, - _default_dict_getter, _default_manager_getter -) -from ..orm import attributes, collections, base as orm_base -from .. import util -from ..orm import exc as orm_exc -import weakref - -INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__' -"""Attribute, elects custom instrumentation when present on a mapped class. - -Allows a class to specify a slightly or wildly different technique for -tracking changes made to mapped attributes and collections. - -Only one instrumentation implementation is allowed in a given object -inheritance hierarchy. - -The value of this attribute must be a callable and will be passed a class -object. The callable must return one of: - - - An instance of an InstrumentationManager or subclass - - An object implementing all or some of InstrumentationManager (TODO) - - A dictionary of callables, implementing all or some of the above (TODO) - - An instance of a ClassManager or subclass - -This attribute is consulted by SQLAlchemy instrumentation -resolution, once the :mod:`sqlalchemy.ext.instrumentation` module -has been imported. If custom finders are installed in the global -instrumentation_finders list, they may or may not choose to honor this -attribute. - -""" - - -def find_native_user_instrumentation_hook(cls): - """Find user-specified instrumentation management for a class.""" - return getattr(cls, INSTRUMENTATION_MANAGER, None) - -instrumentation_finders = [find_native_user_instrumentation_hook] -"""An extensible sequence of callables which return instrumentation -implementations - -When a class is registered, each callable will be passed a class object. -If None is returned, the -next finder in the sequence is consulted. Otherwise the return must be an -instrumentation factory that follows the same guidelines as -sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER. - -By default, the only finder is find_native_user_instrumentation_hook, which -searches for INSTRUMENTATION_MANAGER. If all finders return None, standard -ClassManager instrumentation is used. - -""" - - -class ExtendedInstrumentationRegistry(InstrumentationFactory): - """Extends :class:`.InstrumentationFactory` with additional - bookkeeping, to accommodate multiple types of - class managers. - - """ - _manager_finders = weakref.WeakKeyDictionary() - _state_finders = weakref.WeakKeyDictionary() - _dict_finders = weakref.WeakKeyDictionary() - _extended = False - - def _locate_extended_factory(self, class_): - for finder in instrumentation_finders: - factory = finder(class_) - if factory is not None: - manager = self._extended_class_manager(class_, factory) - return manager, factory - else: - return None, None - - def _check_conflicts(self, class_, factory): - existing_factories = self._collect_management_factories_for(class_).\ - difference([factory]) - if existing_factories: - raise TypeError( - "multiple instrumentation implementations specified " - "in %s inheritance hierarchy: %r" % ( - class_.__name__, list(existing_factories))) - - def _extended_class_manager(self, class_, factory): - manager = factory(class_) - if not isinstance(manager, ClassManager): - manager = _ClassInstrumentationAdapter(class_, manager) - - if factory != ClassManager and not self._extended: - # somebody invoked a custom ClassManager. - # reinstall global "getter" functions with the more - # expensive ones. - self._extended = True - _install_instrumented_lookups() - - self._manager_finders[class_] = manager.manager_getter() - self._state_finders[class_] = manager.state_getter() - self._dict_finders[class_] = manager.dict_getter() - return manager - - def _collect_management_factories_for(self, cls): - """Return a collection of factories in play or specified for a - hierarchy. - - Traverses the entire inheritance graph of a cls and returns a - collection of instrumentation factories for those classes. Factories - are extracted from active ClassManagers, if available, otherwise - instrumentation_finders is consulted. - - """ - hierarchy = util.class_hierarchy(cls) - factories = set() - for member in hierarchy: - manager = self.manager_of_class(member) - if manager is not None: - factories.add(manager.factory) - else: - for finder in instrumentation_finders: - factory = finder(member) - if factory is not None: - break - else: - factory = None - factories.add(factory) - factories.discard(None) - return factories - - def unregister(self, class_): - if class_ in self._manager_finders: - del self._manager_finders[class_] - del self._state_finders[class_] - del self._dict_finders[class_] - super(ExtendedInstrumentationRegistry, self).unregister(class_) - - def manager_of_class(self, cls): - if cls is None: - return None - return self._manager_finders.get(cls, _default_manager_getter)(cls) - - def state_of(self, instance): - if instance is None: - raise AttributeError("None has no persistent state.") - return self._state_finders.get( - instance.__class__, _default_state_getter)(instance) - - def dict_of(self, instance): - if instance is None: - raise AttributeError("None has no persistent state.") - return self._dict_finders.get( - instance.__class__, _default_dict_getter)(instance) - - -orm_instrumentation._instrumentation_factory = \ - _instrumentation_factory = ExtendedInstrumentationRegistry() -orm_instrumentation.instrumentation_finders = instrumentation_finders - - -class InstrumentationManager(object): - """User-defined class instrumentation extension. - - :class:`.InstrumentationManager` can be subclassed in order - to change - how class instrumentation proceeds. This class exists for - the purposes of integration with other object management - frameworks which would like to entirely modify the - instrumentation methodology of the ORM, and is not intended - for regular usage. For interception of class instrumentation - events, see :class:`.InstrumentationEvents`. - - The API for this class should be considered as semi-stable, - and may change slightly with new releases. - - .. versionchanged:: 0.8 - :class:`.InstrumentationManager` was moved from - :mod:`sqlalchemy.orm.instrumentation` to - :mod:`sqlalchemy.ext.instrumentation`. - - """ - - # r4361 added a mandatory (cls) constructor to this interface. - # given that, perhaps class_ should be dropped from all of these - # signatures. - - def __init__(self, class_): - pass - - def manage(self, class_, manager): - setattr(class_, '_default_class_manager', manager) - - def dispose(self, class_, manager): - delattr(class_, '_default_class_manager') - - def manager_getter(self, class_): - def get(cls): - return cls._default_class_manager - return get - - def instrument_attribute(self, class_, key, inst): - pass - - def post_configure_attribute(self, class_, key, inst): - pass - - def install_descriptor(self, class_, key, inst): - setattr(class_, key, inst) - - def uninstall_descriptor(self, class_, key): - delattr(class_, key) - - def install_member(self, class_, key, implementation): - setattr(class_, key, implementation) - - def uninstall_member(self, class_, key): - delattr(class_, key) - - def instrument_collection_class(self, class_, key, collection_class): - return collections.prepare_instrumentation(collection_class) - - def get_instance_dict(self, class_, instance): - return instance.__dict__ - - def initialize_instance_dict(self, class_, instance): - pass - - def install_state(self, class_, instance, state): - setattr(instance, '_default_state', state) - - def remove_state(self, class_, instance): - delattr(instance, '_default_state') - - def state_getter(self, class_): - return lambda instance: getattr(instance, '_default_state') - - def dict_getter(self, class_): - return lambda inst: self.get_instance_dict(class_, inst) - - -class _ClassInstrumentationAdapter(ClassManager): - """Adapts a user-defined InstrumentationManager to a ClassManager.""" - - def __init__(self, class_, override): - self._adapted = override - self._get_state = self._adapted.state_getter(class_) - self._get_dict = self._adapted.dict_getter(class_) - - ClassManager.__init__(self, class_) - - def manage(self): - self._adapted.manage(self.class_, self) - - def dispose(self): - self._adapted.dispose(self.class_) - - def manager_getter(self): - return self._adapted.manager_getter(self.class_) - - def instrument_attribute(self, key, inst, propagated=False): - ClassManager.instrument_attribute(self, key, inst, propagated) - if not propagated: - self._adapted.instrument_attribute(self.class_, key, inst) - - def post_configure_attribute(self, key): - super(_ClassInstrumentationAdapter, self).post_configure_attribute(key) - self._adapted.post_configure_attribute(self.class_, key, self[key]) - - def install_descriptor(self, key, inst): - self._adapted.install_descriptor(self.class_, key, inst) - - def uninstall_descriptor(self, key): - self._adapted.uninstall_descriptor(self.class_, key) - - def install_member(self, key, implementation): - self._adapted.install_member(self.class_, key, implementation) - - def uninstall_member(self, key): - self._adapted.uninstall_member(self.class_, key) - - def instrument_collection_class(self, key, collection_class): - return self._adapted.instrument_collection_class( - self.class_, key, collection_class) - - def initialize_collection(self, key, state, factory): - delegate = getattr(self._adapted, 'initialize_collection', None) - if delegate: - return delegate(key, state, factory) - else: - return ClassManager.initialize_collection(self, key, - state, factory) - - def new_instance(self, state=None): - instance = self.class_.__new__(self.class_) - self.setup_instance(instance, state) - return instance - - def _new_state_if_none(self, instance): - """Install a default InstanceState if none is present. - - A private convenience method used by the __init__ decorator. - """ - if self.has_state(instance): - return False - else: - return self.setup_instance(instance) - - def setup_instance(self, instance, state=None): - self._adapted.initialize_instance_dict(self.class_, instance) - - if state is None: - state = self._state_constructor(instance, self) - - # the given instance is assumed to have no state - self._adapted.install_state(self.class_, instance, state) - return state - - def teardown_instance(self, instance): - self._adapted.remove_state(self.class_, instance) - - def has_state(self, instance): - try: - self._get_state(instance) - except orm_exc.NO_STATE: - return False - else: - return True - - def state_getter(self): - return self._get_state - - def dict_getter(self): - return self._get_dict - - -def _install_instrumented_lookups(): - """Replace global class/object management functions - with ExtendedInstrumentationRegistry implementations, which - allow multiple types of class managers to be present, - at the cost of performance. - - This function is called only by ExtendedInstrumentationRegistry - and unit tests specific to this behavior. - - The _reinstall_default_lookups() function can be called - after this one to re-establish the default functions. - - """ - _install_lookups( - dict( - instance_state=_instrumentation_factory.state_of, - instance_dict=_instrumentation_factory.dict_of, - manager_of_class=_instrumentation_factory.manager_of_class - ) - ) - - -def _reinstall_default_lookups(): - """Restore simplified lookups.""" - _install_lookups( - dict( - instance_state=_default_state_getter, - instance_dict=_default_dict_getter, - manager_of_class=_default_manager_getter - ) - ) - - -def _install_lookups(lookups): - global instance_state, instance_dict, manager_of_class - instance_state = lookups['instance_state'] - instance_dict = lookups['instance_dict'] - manager_of_class = lookups['manager_of_class'] - orm_base.instance_state = attributes.instance_state = \ - orm_instrumentation.instance_state = instance_state - orm_base.instance_dict = attributes.instance_dict = \ - orm_instrumentation.instance_dict = instance_dict - orm_base.manager_of_class = attributes.manager_of_class = \ - orm_instrumentation.manager_of_class = manager_of_class diff --git a/libs/sqlalchemy/ext/mutable.py b/libs/sqlalchemy/ext/mutable.py deleted file mode 100644 index 82410031..00000000 --- a/libs/sqlalchemy/ext/mutable.py +++ /dev/null @@ -1,634 +0,0 @@ -# ext/mutable.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Provide support for tracking of in-place changes to scalar values, -which are propagated into ORM change events on owning parent objects. - -.. versionadded:: 0.7 :mod:`sqlalchemy.ext.mutable` replaces SQLAlchemy's - legacy approach to in-place mutations of scalar values; see - :ref:`07_migration_mutation_extension`. - -.. _mutable_scalars: - -Establishing Mutability on Scalar Column Values -=============================================== - -A typical example of a "mutable" structure is a Python dictionary. -Following the example introduced in :ref:`types_toplevel`, we -begin with a custom type that marshals Python dictionaries into -JSON strings before being persisted:: - - from sqlalchemy.types import TypeDecorator, VARCHAR - import json - - class JSONEncodedDict(TypeDecorator): - "Represents an immutable structure as a json-encoded string." - - impl = VARCHAR - - def process_bind_param(self, value, dialect): - if value is not None: - value = json.dumps(value) - return value - - def process_result_value(self, value, dialect): - if value is not None: - value = json.loads(value) - return value - -The usage of ``json`` is only for the purposes of example. The -:mod:`sqlalchemy.ext.mutable` extension can be used -with any type whose target Python type may be mutable, including -:class:`.PickleType`, :class:`.postgresql.ARRAY`, etc. - -When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself -tracks all parents which reference it. Below, we illustrate the a simple -version of the :class:`.MutableDict` dictionary object, which applies -the :class:`.Mutable` mixin to a plain Python dictionary:: - - import collections - from sqlalchemy.ext.mutable import Mutable - - class MutableDict(Mutable, dict): - @classmethod - def coerce(cls, key, value): - "Convert plain dictionaries to MutableDict." - - if not isinstance(value, MutableDict): - if isinstance(value, dict): - return MutableDict(value) - - # this call will raise ValueError - return Mutable.coerce(key, value) - else: - return value - - def __setitem__(self, key, value): - "Detect dictionary set events and emit change events." - - dict.__setitem__(self, key, value) - self.changed() - - def __delitem__(self, key): - "Detect dictionary del events and emit change events." - - dict.__delitem__(self, key) - self.changed() - -The above dictionary class takes the approach of subclassing the Python -built-in ``dict`` to produce a dict -subclass which routes all mutation events through ``__setitem__``. There are -variants on this approach, such as subclassing ``UserDict.UserDict`` or -``collections.MutableMapping``; the part that's important to this example is -that the :meth:`.Mutable.changed` method is called whenever an in-place -change to the datastructure takes place. - -We also redefine the :meth:`.Mutable.coerce` method which will be used to -convert any values that are not instances of ``MutableDict``, such -as the plain dictionaries returned by the ``json`` module, into the -appropriate type. Defining this method is optional; we could just as well -created our ``JSONEncodedDict`` such that it always returns an instance -of ``MutableDict``, and additionally ensured that all calling code -uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not -overridden, any values applied to a parent object which are not instances -of the mutable type will raise a ``ValueError``. - -Our new ``MutableDict`` type offers a class method -:meth:`~.Mutable.as_mutable` which we can use within column metadata -to associate with types. This method grabs the given type object or -class and associates a listener that will detect all future mappings -of this type, applying event listening instrumentation to the mapped -attribute. Such as, with classical table metadata:: - - from sqlalchemy import Table, Column, Integer - - my_data = Table('my_data', metadata, - Column('id', Integer, primary_key=True), - Column('data', MutableDict.as_mutable(JSONEncodedDict)) - ) - -Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict`` -(if the type object was not an instance already), which will intercept any -attributes which are mapped against this type. Below we establish a simple -mapping against the ``my_data`` table:: - - from sqlalchemy import mapper - - class MyDataClass(object): - pass - - # associates mutation listeners with MyDataClass.data - mapper(MyDataClass, my_data) - -The ``MyDataClass.data`` member will now be notified of in place changes -to its value. - -There's no difference in usage when using declarative:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class MyDataClass(Base): - __tablename__ = 'my_data' - id = Column(Integer, primary_key=True) - data = Column(MutableDict.as_mutable(JSONEncodedDict)) - -Any in-place changes to the ``MyDataClass.data`` member -will flag the attribute as "dirty" on the parent object:: - - >>> from sqlalchemy.orm import Session - - >>> sess = Session() - >>> m1 = MyDataClass(data={'value1':'foo'}) - >>> sess.add(m1) - >>> sess.commit() - - >>> m1.data['value1'] = 'bar' - >>> assert m1 in sess.dirty - True - -The ``MutableDict`` can be associated with all future instances -of ``JSONEncodedDict`` in one step, using -:meth:`~.Mutable.associate_with`. This is similar to -:meth:`~.Mutable.as_mutable` except it will intercept all occurrences -of ``MutableDict`` in all mappings unconditionally, without -the need to declare it individually:: - - MutableDict.associate_with(JSONEncodedDict) - - class MyDataClass(Base): - __tablename__ = 'my_data' - id = Column(Integer, primary_key=True) - data = Column(JSONEncodedDict) - - -Supporting Pickling --------------------- - -The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the -placement of a ``weakref.WeakKeyDictionary`` upon the value object, which -stores a mapping of parent mapped objects keyed to the attribute name under -which they are associated with this value. ``WeakKeyDictionary`` objects are -not picklable, due to the fact that they contain weakrefs and function -callbacks. In our case, this is a good thing, since if this dictionary were -picklable, it could lead to an excessively large pickle size for our value -objects that are pickled by themselves outside of the context of the parent. -The developer responsibility here is only to provide a ``__getstate__`` method -that excludes the :meth:`~MutableBase._parents` collection from the pickle -stream:: - - class MyMutableType(Mutable): - def __getstate__(self): - d = self.__dict__.copy() - d.pop('_parents', None) - return d - -With our dictionary example, we need to return the contents of the dict itself -(and also restore them on __setstate__):: - - class MutableDict(Mutable, dict): - # .... - - def __getstate__(self): - return dict(self) - - def __setstate__(self, state): - self.update(state) - -In the case that our mutable value object is pickled as it is attached to one -or more parent objects that are also part of the pickle, the :class:`.Mutable` -mixin will re-establish the :attr:`.Mutable._parents` collection on each value -object as the owning parents themselves are unpickled. - -.. _mutable_composites: - -Establishing Mutability on Composites -===================================== - -Composites are a special ORM feature which allow a single scalar attribute to -be assigned an object value which represents information "composed" from one -or more columns from the underlying mapped table. The usual example is that of -a geometric "point", and is introduced in :ref:`mapper_composite`. - -.. versionchanged:: 0.7 - The internals of :func:`.orm.composite` have been - greatly simplified and in-place mutation detection is no longer enabled by - default; instead, the user-defined value must detect changes on its own and - propagate them to all owning parents. The :mod:`sqlalchemy.ext.mutable` - extension provides the helper class :class:`.MutableComposite`, which is a - slight variant on the :class:`.Mutable` class. - -As is the case with :class:`.Mutable`, the user-defined composite class -subclasses :class:`.MutableComposite` as a mixin, and detects and delivers -change events to its parents via the :meth:`.MutableComposite.changed` method. -In the case of a composite class, the detection is usually via the usage of -Python descriptors (i.e. ``@property``), or alternatively via the special -Python method ``__setattr__()``. Below we expand upon the ``Point`` class -introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite` -and to also route attribute set events via ``__setattr__`` to the -:meth:`.MutableComposite.changed` method:: - - from sqlalchemy.ext.mutable import MutableComposite - - class Point(MutableComposite): - def __init__(self, x, y): - self.x = x - self.y = y - - def __setattr__(self, key, value): - "Intercept set events" - - # set the attribute - object.__setattr__(self, key, value) - - # alert all parents to the change - self.changed() - - def __composite_values__(self): - return self.x, self.y - - def __eq__(self, other): - return isinstance(other, Point) and \\ - other.x == self.x and \\ - other.y == self.y - - def __ne__(self, other): - return not self.__eq__(other) - -The :class:`.MutableComposite` class uses a Python metaclass to automatically -establish listeners for any usage of :func:`.orm.composite` that specifies our -``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class, -listeners are established which will route change events from ``Point`` -objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes:: - - from sqlalchemy.orm import composite, mapper - from sqlalchemy import Table, Column - - vertices = Table('vertices', metadata, - Column('id', Integer, primary_key=True), - Column('x1', Integer), - Column('y1', Integer), - Column('x2', Integer), - Column('y2', Integer), - ) - - class Vertex(object): - pass - - mapper(Vertex, vertices, properties={ - 'start': composite(Point, vertices.c.x1, vertices.c.y1), - 'end': composite(Point, vertices.c.x2, vertices.c.y2) - }) - -Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members -will flag the attribute as "dirty" on the parent object:: - - >>> from sqlalchemy.orm import Session - - >>> sess = Session() - >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15)) - >>> sess.add(v1) - >>> sess.commit() - - >>> v1.end.x = 8 - >>> assert v1 in sess.dirty - True - -Coercing Mutable Composites ---------------------------- - -The :meth:`.MutableBase.coerce` method is also supported on composite types. -In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce` -method is only called for attribute set operations, not load operations. -Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent -to using a :func:`.validates` validation routine for all attributes which -make use of the custom composite type:: - - class Point(MutableComposite): - # other Point methods - # ... - - def coerce(cls, key, value): - if isinstance(value, tuple): - value = Point(*value) - elif not isinstance(value, Point): - raise ValueError("tuple or Point expected") - return value - -.. versionadded:: 0.7.10,0.8.0b2 - Support for the :meth:`.MutableBase.coerce` method in conjunction with - objects of type :class:`.MutableComposite`. - -Supporting Pickling --------------------- - -As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper -class uses a ``weakref.WeakKeyDictionary`` available via the -:meth:`MutableBase._parents` attribute which isn't picklable. If we need to -pickle instances of ``Point`` or its owning class ``Vertex``, we at least need -to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary. -Below we define both a ``__getstate__`` and a ``__setstate__`` that package up -the minimal form of our ``Point`` class:: - - class Point(MutableComposite): - # ... - - def __getstate__(self): - return self.x, self.y - - def __setstate__(self, state): - self.x, self.y = state - -As with :class:`.Mutable`, the :class:`.MutableComposite` augments the -pickling process of the parent's object-relational state so that the -:meth:`MutableBase._parents` collection is restored to all ``Point`` objects. - -""" -from ..orm.attributes import flag_modified -from .. import event, types -from ..orm import mapper, object_mapper, Mapper -from ..util import memoized_property -import weakref - - -class MutableBase(object): - """Common base class to :class:`.Mutable` - and :class:`.MutableComposite`. - - """ - - @memoized_property - def _parents(self): - """Dictionary of parent object->attribute name on the parent. - - This attribute is a so-called "memoized" property. It initializes - itself with a new ``weakref.WeakKeyDictionary`` the first time - it is accessed, returning the same object upon subsequent access. - - """ - - return weakref.WeakKeyDictionary() - - @classmethod - def coerce(cls, key, value): - """Given a value, coerce it into the target type. - - Can be overridden by custom subclasses to coerce incoming - data into a particular type. - - By default, raises ``ValueError``. - - This method is called in different scenarios depending on if - the parent class is of type :class:`.Mutable` or of type - :class:`.MutableComposite`. In the case of the former, it is called - for both attribute-set operations as well as during ORM loading - operations. For the latter, it is only called during attribute-set - operations; the mechanics of the :func:`.composite` construct - handle coercion during load operations. - - - :param key: string name of the ORM-mapped attribute being set. - :param value: the incoming value. - :return: the method should return the coerced value, or raise - ``ValueError`` if the coercion cannot be completed. - - """ - if value is None: - return None - msg = "Attribute '%s' does not accept objects of type %s" - raise ValueError(msg % (key, type(value))) - - @classmethod - def _listen_on_attribute(cls, attribute, coerce, parent_cls): - """Establish this type as a mutation listener for the given - mapped descriptor. - - """ - key = attribute.key - if parent_cls is not attribute.class_: - return - - # rely on "propagate" here - parent_cls = attribute.class_ - - def load(state, *args): - """Listen for objects loaded or refreshed. - - Wrap the target data member's value with - ``Mutable``. - - """ - val = state.dict.get(key, None) - if val is not None: - if coerce: - val = cls.coerce(key, val) - state.dict[key] = val - val._parents[state.obj()] = key - - def set(target, value, oldvalue, initiator): - """Listen for set/replace events on the target - data member. - - Establish a weak reference to the parent object - on the incoming value, remove it for the one - outgoing. - - """ - if not isinstance(value, cls): - value = cls.coerce(key, value) - if value is not None: - value._parents[target.obj()] = key - if isinstance(oldvalue, cls): - oldvalue._parents.pop(target.obj(), None) - return value - - def pickle(state, state_dict): - val = state.dict.get(key, None) - if val is not None: - if 'ext.mutable.values' not in state_dict: - state_dict['ext.mutable.values'] = [] - state_dict['ext.mutable.values'].append(val) - - def unpickle(state, state_dict): - if 'ext.mutable.values' in state_dict: - for val in state_dict['ext.mutable.values']: - val._parents[state.obj()] = key - - event.listen(parent_cls, 'load', load, - raw=True, propagate=True) - event.listen(parent_cls, 'refresh', load, - raw=True, propagate=True) - event.listen(attribute, 'set', set, - raw=True, retval=True, propagate=True) - event.listen(parent_cls, 'pickle', pickle, - raw=True, propagate=True) - event.listen(parent_cls, 'unpickle', unpickle, - raw=True, propagate=True) - - -class Mutable(MutableBase): - """Mixin that defines transparent propagation of change - events to a parent object. - - See the example in :ref:`mutable_scalars` for usage information. - - """ - - def changed(self): - """Subclasses should call this method whenever change events occur.""" - - for parent, key in self._parents.items(): - flag_modified(parent, key) - - @classmethod - def associate_with_attribute(cls, attribute): - """Establish this type as a mutation listener for the given - mapped descriptor. - - """ - cls._listen_on_attribute(attribute, True, attribute.class_) - - @classmethod - def associate_with(cls, sqltype): - """Associate this wrapper with all future mapped columns - of the given type. - - This is a convenience method that calls - ``associate_with_attribute`` automatically. - - .. warning:: - - The listeners established by this method are *global* - to all mappers, and are *not* garbage collected. Only use - :meth:`.associate_with` for types that are permanent to an - application, not with ad-hoc types else this will cause unbounded - growth in memory usage. - - """ - - def listen_for_type(mapper, class_): - for prop in mapper.column_attrs: - if isinstance(prop.columns[0].type, sqltype): - cls.associate_with_attribute(getattr(class_, prop.key)) - - event.listen(mapper, 'mapper_configured', listen_for_type) - - @classmethod - def as_mutable(cls, sqltype): - """Associate a SQL type with this mutable Python type. - - This establishes listeners that will detect ORM mappings against - the given type, adding mutation event trackers to those mappings. - - The type is returned, unconditionally as an instance, so that - :meth:`.as_mutable` can be used inline:: - - Table('mytable', metadata, - Column('id', Integer, primary_key=True), - Column('data', MyMutableType.as_mutable(PickleType)) - ) - - Note that the returned type is always an instance, even if a class - is given, and that only columns which are declared specifically with - that type instance receive additional instrumentation. - - To associate a particular mutable type with all occurrences of a - particular type, use the :meth:`.Mutable.associate_with` classmethod - of the particular :class:`.Mutable` subclass to establish a global - association. - - .. warning:: - - The listeners established by this method are *global* - to all mappers, and are *not* garbage collected. Only use - :meth:`.as_mutable` for types that are permanent to an application, - not with ad-hoc types else this will cause unbounded growth - in memory usage. - - """ - sqltype = types.to_instance(sqltype) - - def listen_for_type(mapper, class_): - for prop in mapper.column_attrs: - if prop.columns[0].type is sqltype: - cls.associate_with_attribute(getattr(class_, prop.key)) - - event.listen(mapper, 'mapper_configured', listen_for_type) - - return sqltype - - - -class MutableComposite(MutableBase): - """Mixin that defines transparent propagation of change - events on a SQLAlchemy "composite" object to its - owning parent or parents. - - See the example in :ref:`mutable_composites` for usage information. - - """ - - def changed(self): - """Subclasses should call this method whenever change events occur.""" - - for parent, key in self._parents.items(): - - prop = object_mapper(parent).get_property(key) - for value, attr_name in zip( - self.__composite_values__(), - prop._attribute_keys): - setattr(parent, attr_name, value) - -def _setup_composite_listener(): - def _listen_for_type(mapper, class_): - for prop in mapper.iterate_properties: - if (hasattr(prop, 'composite_class') and - isinstance(prop.composite_class, type) and - issubclass(prop.composite_class, MutableComposite)): - prop.composite_class._listen_on_attribute( - getattr(class_, prop.key), False, class_) - if not event.contains(Mapper, "mapper_configured", _listen_for_type): - event.listen(Mapper, 'mapper_configured', _listen_for_type) -_setup_composite_listener() - - -class MutableDict(Mutable, dict): - """A dictionary type that implements :class:`.Mutable`. - - .. versionadded:: 0.8 - - """ - - def __setitem__(self, key, value): - """Detect dictionary set events and emit change events.""" - dict.__setitem__(self, key, value) - self.changed() - - def __delitem__(self, key): - """Detect dictionary del events and emit change events.""" - dict.__delitem__(self, key) - self.changed() - - def clear(self): - dict.clear(self) - self.changed() - - @classmethod - def coerce(cls, key, value): - """Convert plain dictionary to MutableDict.""" - if not isinstance(value, MutableDict): - if isinstance(value, dict): - return MutableDict(value) - return Mutable.coerce(key, value) - else: - return value - - def __getstate__(self): - return dict(self) - - def __setstate__(self, state): - self.update(state) diff --git a/libs/sqlalchemy/ext/orderinglist.py b/libs/sqlalchemy/ext/orderinglist.py deleted file mode 100644 index 9310c607..00000000 --- a/libs/sqlalchemy/ext/orderinglist.py +++ /dev/null @@ -1,365 +0,0 @@ -# ext/orderinglist.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""A custom list that manages index/position information for contained -elements. - -:author: Jason Kirtland - -``orderinglist`` is a helper for mutable ordered relationships. It will -intercept list operations performed on a :func:`.relationship`-managed -collection and -automatically synchronize changes in list position onto a target scalar -attribute. - -Example: A ``slide`` table, where each row refers to zero or more entries -in a related ``bullet`` table. The bullets within a slide are -displayed in order based on the value of the ``position`` column in the -``bullet`` table. As entries are reordered in memory, the value of the -``position`` attribute should be updated to reflect the new sort order:: - - - Base = declarative_base() - - class Slide(Base): - __tablename__ = 'slide' - - id = Column(Integer, primary_key=True) - name = Column(String) - - bullets = relationship("Bullet", order_by="Bullet.position") - - class Bullet(Base): - __tablename__ = 'bullet' - id = Column(Integer, primary_key=True) - slide_id = Column(Integer, ForeignKey('slide.id')) - position = Column(Integer) - text = Column(String) - -The standard relationship mapping will produce a list-like attribute on each -``Slide`` containing all related ``Bullet`` objects, -but coping with changes in ordering is not handled automatically. -When appending a ``Bullet`` into ``Slide.bullets``, the ``Bullet.position`` -attribute will remain unset until manually assigned. When the ``Bullet`` -is inserted into the middle of the list, the following ``Bullet`` objects -will also need to be renumbered. - -The :class:`.OrderingList` object automates this task, managing the -``position`` attribute on all ``Bullet`` objects in the collection. It is -constructed using the :func:`.ordering_list` factory:: - - from sqlalchemy.ext.orderinglist import ordering_list - - Base = declarative_base() - - class Slide(Base): - __tablename__ = 'slide' - - id = Column(Integer, primary_key=True) - name = Column(String) - - bullets = relationship("Bullet", order_by="Bullet.position", - collection_class=ordering_list('position')) - - class Bullet(Base): - __tablename__ = 'bullet' - id = Column(Integer, primary_key=True) - slide_id = Column(Integer, ForeignKey('slide.id')) - position = Column(Integer) - text = Column(String) - -With the above mapping the ``Bullet.position`` attribute is managed:: - - s = Slide() - s.bullets.append(Bullet()) - s.bullets.append(Bullet()) - s.bullets[1].position - >>> 1 - s.bullets.insert(1, Bullet()) - s.bullets[2].position - >>> 2 - -The :class:`.OrderingList` construct only works with **changes** to a collection, -and not the initial load from the database, and requires that the list be -sorted when loaded. Therefore, be sure to -specify ``order_by`` on the :func:`.relationship` against the target ordering -attribute, so that the ordering is correct when first loaded. - -.. warning:: - - :class:`.OrderingList` only provides limited functionality when a primary - key column or unique column is the target of the sort. Since changing the - order of entries often means that two rows must trade values, this is not - possible when the value is constrained by a primary key or unique - constraint, since one of the rows would temporarily have to point to a - third available value so that the other row could take its old - value. :class:`.OrderingList` doesn't do any of this for you, - nor does SQLAlchemy itself. - -:func:`.ordering_list` takes the name of the related object's ordering attribute as -an argument. By default, the zero-based integer index of the object's -position in the :func:`.ordering_list` is synchronized with the ordering attribute: -index 0 will get position 0, index 1 position 1, etc. To start numbering at 1 -or some other integer, provide ``count_from=1``. - - -""" -from ..orm.collections import collection -from .. import util - -__all__ = ['ordering_list'] - - -def ordering_list(attr, count_from=None, **kw): - """Prepares an :class:`OrderingList` factory for use in mapper definitions. - - Returns an object suitable for use as an argument to a Mapper - relationship's ``collection_class`` option. e.g.:: - - from sqlalchemy.ext.orderinglist import ordering_list - - class Slide(Base): - __tablename__ = 'slide' - - id = Column(Integer, primary_key=True) - name = Column(String) - - bullets = relationship("Bullet", order_by="Bullet.position", - collection_class=ordering_list('position')) - - :param attr: - Name of the mapped attribute to use for storage and retrieval of - ordering information - - :param count_from: - Set up an integer-based ordering, starting at ``count_from``. For - example, ``ordering_list('pos', count_from=1)`` would create a 1-based - list in SQL, storing the value in the 'pos' column. Ignored if - ``ordering_func`` is supplied. - - Additional arguments are passed to the :class:`.OrderingList` constructor. - - """ - - kw = _unsugar_count_from(count_from=count_from, **kw) - return lambda: OrderingList(attr, **kw) - - -# Ordering utility functions - - -def count_from_0(index, collection): - """Numbering function: consecutive integers starting at 0.""" - - return index - - -def count_from_1(index, collection): - """Numbering function: consecutive integers starting at 1.""" - - return index + 1 - - -def count_from_n_factory(start): - """Numbering function: consecutive integers starting at arbitrary start.""" - - def f(index, collection): - return index + start - try: - f.__name__ = 'count_from_%i' % start - except TypeError: - pass - return f - - -def _unsugar_count_from(**kw): - """Builds counting functions from keyword arguments. - - Keyword argument filter, prepares a simple ``ordering_func`` from a - ``count_from`` argument, otherwise passes ``ordering_func`` on unchanged. - """ - - count_from = kw.pop('count_from', None) - if kw.get('ordering_func', None) is None and count_from is not None: - if count_from == 0: - kw['ordering_func'] = count_from_0 - elif count_from == 1: - kw['ordering_func'] = count_from_1 - else: - kw['ordering_func'] = count_from_n_factory(count_from) - return kw - - -class OrderingList(list): - """A custom list that manages position information for its children. - - The :class:`.OrderingList` object is normally set up using the - :func:`.ordering_list` factory function, used in conjunction with - the :func:`.relationship` function. - - """ - - def __init__(self, ordering_attr=None, ordering_func=None, - reorder_on_append=False): - """A custom list that manages position information for its children. - - ``OrderingList`` is a ``collection_class`` list implementation that - syncs position in a Python list with a position attribute on the - mapped objects. - - This implementation relies on the list starting in the proper order, - so be **sure** to put an ``order_by`` on your relationship. - - :param ordering_attr: - Name of the attribute that stores the object's order in the - relationship. - - :param ordering_func: Optional. A function that maps the position in - the Python list to a value to store in the - ``ordering_attr``. Values returned are usually (but need not be!) - integers. - - An ``ordering_func`` is called with two positional parameters: the - index of the element in the list, and the list itself. - - If omitted, Python list indexes are used for the attribute values. - Two basic pre-built numbering functions are provided in this module: - ``count_from_0`` and ``count_from_1``. For more exotic examples - like stepped numbering, alphabetical and Fibonacci numbering, see - the unit tests. - - :param reorder_on_append: - Default False. When appending an object with an existing (non-None) - ordering value, that value will be left untouched unless - ``reorder_on_append`` is true. This is an optimization to avoid a - variety of dangerous unexpected database writes. - - SQLAlchemy will add instances to the list via append() when your - object loads. If for some reason the result set from the database - skips a step in the ordering (say, row '1' is missing but you get - '2', '3', and '4'), reorder_on_append=True would immediately - renumber the items to '1', '2', '3'. If you have multiple sessions - making changes, any of whom happen to load this collection even in - passing, all of the sessions would try to "clean up" the numbering - in their commits, possibly causing all but one to fail with a - concurrent modification error. - - Recommend leaving this with the default of False, and just call - ``reorder()`` if you're doing ``append()`` operations with - previously ordered instances or when doing some housekeeping after - manual sql operations. - - """ - self.ordering_attr = ordering_attr - if ordering_func is None: - ordering_func = count_from_0 - self.ordering_func = ordering_func - self.reorder_on_append = reorder_on_append - - # More complex serialization schemes (multi column, e.g.) are possible by - # subclassing and reimplementing these two methods. - def _get_order_value(self, entity): - return getattr(entity, self.ordering_attr) - - def _set_order_value(self, entity, value): - setattr(entity, self.ordering_attr, value) - - def reorder(self): - """Synchronize ordering for the entire collection. - - Sweeps through the list and ensures that each object has accurate - ordering information set. - - """ - for index, entity in enumerate(self): - self._order_entity(index, entity, True) - - # As of 0.5, _reorder is no longer semi-private - _reorder = reorder - - def _order_entity(self, index, entity, reorder=True): - have = self._get_order_value(entity) - - # Don't disturb existing ordering if reorder is False - if have is not None and not reorder: - return - - should_be = self.ordering_func(index, self) - if have != should_be: - self._set_order_value(entity, should_be) - - def append(self, entity): - super(OrderingList, self).append(entity) - self._order_entity(len(self) - 1, entity, self.reorder_on_append) - - def _raw_append(self, entity): - """Append without any ordering behavior.""" - - super(OrderingList, self).append(entity) - _raw_append = collection.adds(1)(_raw_append) - - def insert(self, index, entity): - super(OrderingList, self).insert(index, entity) - self._reorder() - - def remove(self, entity): - super(OrderingList, self).remove(entity) - self._reorder() - - def pop(self, index=-1): - entity = super(OrderingList, self).pop(index) - self._reorder() - return entity - - def __setitem__(self, index, entity): - if isinstance(index, slice): - step = index.step or 1 - start = index.start or 0 - if start < 0: - start += len(self) - stop = index.stop or len(self) - if stop < 0: - stop += len(self) - - for i in range(start, stop, step): - self.__setitem__(i, entity[i]) - else: - self._order_entity(index, entity, True) - super(OrderingList, self).__setitem__(index, entity) - - def __delitem__(self, index): - super(OrderingList, self).__delitem__(index) - self._reorder() - - def __setslice__(self, start, end, values): - super(OrderingList, self).__setslice__(start, end, values) - self._reorder() - - def __delslice__(self, start, end): - super(OrderingList, self).__delslice__(start, end) - self._reorder() - - def __reduce__(self): - return _reconstitute, (self.__class__, self.__dict__, list(self)) - - for func_name, func in list(locals().items()): - if (util.callable(func) and func.__name__ == func_name and - not func.__doc__ and hasattr(list, func_name)): - func.__doc__ = getattr(list, func_name).__doc__ - del func_name, func - - -def _reconstitute(cls, dict_, items): - """ Reconstitute an :class:`.OrderingList`. - - This is the adjoint to :meth:`.OrderingList.__reduce__`. It is used for - unpickling :class:`.OrderingList` objects. - - """ - obj = cls.__new__(cls) - obj.__dict__.update(dict_) - list.extend(obj, items) - return obj diff --git a/libs/sqlalchemy/ext/serializer.py b/libs/sqlalchemy/ext/serializer.py deleted file mode 100644 index 388cd404..00000000 --- a/libs/sqlalchemy/ext/serializer.py +++ /dev/null @@ -1,156 +0,0 @@ -# ext/serializer.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Serializer/Deserializer objects for usage with SQLAlchemy query structures, -allowing "contextual" deserialization. - -Any SQLAlchemy query structure, either based on sqlalchemy.sql.* -or sqlalchemy.orm.* can be used. The mappers, Tables, Columns, Session -etc. which are referenced by the structure are not persisted in serialized -form, but are instead re-associated with the query structure -when it is deserialized. - -Usage is nearly the same as that of the standard Python pickle module:: - - from sqlalchemy.ext.serializer import loads, dumps - metadata = MetaData(bind=some_engine) - Session = scoped_session(sessionmaker()) - - # ... define mappers - - query = Session.query(MyClass).filter(MyClass.somedata=='foo').order_by(MyClass.sortkey) - - # pickle the query - serialized = dumps(query) - - # unpickle. Pass in metadata + scoped_session - query2 = loads(serialized, metadata, Session) - - print query2.all() - -Similar restrictions as when using raw pickle apply; mapped classes must be -themselves be pickleable, meaning they are importable from a module-level -namespace. - -The serializer module is only appropriate for query structures. It is not -needed for: - -* instances of user-defined classes. These contain no references to engines, - sessions or expression constructs in the typical case and can be serialized - directly. - -* Table metadata that is to be loaded entirely from the serialized structure - (i.e. is not already declared in the application). Regular - pickle.loads()/dumps() can be used to fully dump any ``MetaData`` object, - typically one which was reflected from an existing database at some previous - point in time. The serializer module is specifically for the opposite case, - where the Table metadata is already present in memory. - -""" - -from ..orm import class_mapper -from ..orm.session import Session -from ..orm.mapper import Mapper -from ..orm.interfaces import MapperProperty -from ..orm.attributes import QueryableAttribute -from .. import Table, Column -from ..engine import Engine -from ..util import pickle, byte_buffer, b64encode, b64decode, text_type -import re - - -__all__ = ['Serializer', 'Deserializer', 'dumps', 'loads'] - - -def Serializer(*args, **kw): - pickler = pickle.Pickler(*args, **kw) - - def persistent_id(obj): - #print "serializing:", repr(obj) - if isinstance(obj, QueryableAttribute): - cls = obj.impl.class_ - key = obj.impl.key - id = "attribute:" + key + ":" + b64encode(pickle.dumps(cls)) - elif isinstance(obj, Mapper) and not obj.non_primary: - id = "mapper:" + b64encode(pickle.dumps(obj.class_)) - elif isinstance(obj, MapperProperty) and not obj.parent.non_primary: - id = "mapperprop:" + b64encode(pickle.dumps(obj.parent.class_)) + \ - ":" + obj.key - elif isinstance(obj, Table): - id = "table:" + text_type(obj.key) - elif isinstance(obj, Column) and isinstance(obj.table, Table): - id = "column:" + text_type(obj.table.key) + ":" + text_type(obj.key) - elif isinstance(obj, Session): - id = "session:" - elif isinstance(obj, Engine): - id = "engine:" - else: - return None - return id - - pickler.persistent_id = persistent_id - return pickler - -our_ids = re.compile( - r'(mapperprop|mapper|table|column|session|attribute|engine):(.*)') - - -def Deserializer(file, metadata=None, scoped_session=None, engine=None): - unpickler = pickle.Unpickler(file) - - def get_engine(): - if engine: - return engine - elif scoped_session and scoped_session().bind: - return scoped_session().bind - elif metadata and metadata.bind: - return metadata.bind - else: - return None - - def persistent_load(id): - m = our_ids.match(text_type(id)) - if not m: - return None - else: - type_, args = m.group(1, 2) - if type_ == 'attribute': - key, clsarg = args.split(":") - cls = pickle.loads(b64decode(clsarg)) - return getattr(cls, key) - elif type_ == "mapper": - cls = pickle.loads(b64decode(args)) - return class_mapper(cls) - elif type_ == "mapperprop": - mapper, keyname = args.split(':') - cls = pickle.loads(b64decode(mapper)) - return class_mapper(cls).attrs[keyname] - elif type_ == "table": - return metadata.tables[args] - elif type_ == "column": - table, colname = args.split(':') - return metadata.tables[table].c[colname] - elif type_ == "session": - return scoped_session() - elif type_ == "engine": - return get_engine() - else: - raise Exception("Unknown token: %s" % type_) - unpickler.persistent_load = persistent_load - return unpickler - - -def dumps(obj, protocol=0): - buf = byte_buffer() - pickler = Serializer(buf, protocol) - pickler.dump(obj) - return buf.getvalue() - - -def loads(data, metadata=None, scoped_session=None, engine=None): - buf = byte_buffer(data) - unpickler = Deserializer(buf, metadata, scoped_session, engine) - return unpickler.load() diff --git a/libs/sqlalchemy/inspection.py b/libs/sqlalchemy/inspection.py deleted file mode 100644 index fe9e4055..00000000 --- a/libs/sqlalchemy/inspection.py +++ /dev/null @@ -1,92 +0,0 @@ -# sqlalchemy/inspect.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The inspection module provides the :func:`.inspect` function, -which delivers runtime information about a wide variety -of SQLAlchemy objects, both within the Core as well as the -ORM. - -The :func:`.inspect` function is the entry point to SQLAlchemy's -public API for viewing the configuration and construction -of in-memory objects. Depending on the type of object -passed to :func:`.inspect`, the return value will either be -a related object which provides a known interface, or in many -cases it will return the object itself. - -The rationale for :func:`.inspect` is twofold. One is that -it replaces the need to be aware of a large variety of "information -getting" functions in SQLAlchemy, such as :meth:`.Inspector.from_engine`, -:func:`.orm.attributes.instance_state`, :func:`.orm.class_mapper`, -and others. The other is that the return value of :func:`.inspect` -is guaranteed to obey a documented API, thus allowing third party -tools which build on top of SQLAlchemy configurations to be constructed -in a forwards-compatible way. - -.. versionadded:: 0.8 The :func:`.inspect` system is introduced - as of version 0.8. - -""" - -from . import util, exc -_registrars = util.defaultdict(list) - - -def inspect(subject, raiseerr=True): - """Produce an inspection object for the given target. - - The returned value in some cases may be the - same object as the one given, such as if a - :class:`.Mapper` object is passed. In other - cases, it will be an instance of the registered - inspection type for the given object, such as - if an :class:`.engine.Engine` is passed, an - :class:`.Inspector` object is returned. - - :param subject: the subject to be inspected. - :param raiseerr: When ``True``, if the given subject - does not - correspond to a known SQLAlchemy inspected type, - :class:`sqlalchemy.exc.NoInspectionAvailable` - is raised. If ``False``, ``None`` is returned. - - """ - type_ = type(subject) - for cls in type_.__mro__: - if cls in _registrars: - reg = _registrars[cls] - if reg is True: - return subject - ret = reg(subject) - if ret is not None: - break - else: - reg = ret = None - - if raiseerr and ( - reg is None or ret is None - ): - raise exc.NoInspectionAvailable( - "No inspection system is " - "available for object of type %s" % - type_) - return ret - - -def _inspects(*types): - def decorate(fn_or_cls): - for type_ in types: - if type_ in _registrars: - raise AssertionError( - "Type %s is already " - "registered" % type_) - _registrars[type_] = fn_or_cls - return fn_or_cls - return decorate - - -def _self_inspects(cls): - _inspects(cls)(True) - return cls diff --git a/libs/sqlalchemy/interfaces.py b/libs/sqlalchemy/interfaces.py deleted file mode 100644 index ed50a645..00000000 --- a/libs/sqlalchemy/interfaces.py +++ /dev/null @@ -1,310 +0,0 @@ -# sqlalchemy/interfaces.py -# Copyright (C) 2007-2014 the SQLAlchemy authors and contributors -# Copyright (C) 2007 Jason Kirtland jek@discorporate.us -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Deprecated core event interfaces. - -This module is **deprecated** and is superseded by the -event system. - -""" - -from . import event, util - - -class PoolListener(object): - """Hooks into the lifecycle of connections in a :class:`.Pool`. - - .. note:: - - :class:`.PoolListener` is deprecated. Please - refer to :class:`.PoolEvents`. - - Usage:: - - class MyListener(PoolListener): - def connect(self, dbapi_con, con_record): - '''perform connect operations''' - # etc. - - # create a new pool with a listener - p = QueuePool(..., listeners=[MyListener()]) - - # add a listener after the fact - p.add_listener(MyListener()) - - # usage with create_engine() - e = create_engine("url://", listeners=[MyListener()]) - - All of the standard connection :class:`~sqlalchemy.pool.Pool` types can - accept event listeners for key connection lifecycle events: - creation, pool check-out and check-in. There are no events fired - when a connection closes. - - For any given DB-API connection, there will be one ``connect`` - event, `n` number of ``checkout`` events, and either `n` or `n - 1` - ``checkin`` events. (If a ``Connection`` is detached from its - pool via the ``detach()`` method, it won't be checked back in.) - - These are low-level events for low-level objects: raw Python - DB-API connections, without the conveniences of the SQLAlchemy - ``Connection`` wrapper, ``Dialect`` services or ``ClauseElement`` - execution. If you execute SQL through the connection, explicitly - closing all cursors and other resources is recommended. - - Events also receive a ``_ConnectionRecord``, a long-lived internal - ``Pool`` object that basically represents a "slot" in the - connection pool. ``_ConnectionRecord`` objects have one public - attribute of note: ``info``, a dictionary whose contents are - scoped to the lifetime of the DB-API connection managed by the - record. You can use this shared storage area however you like. - - There is no need to subclass ``PoolListener`` to handle events. - Any class that implements one or more of these methods can be used - as a pool listener. The ``Pool`` will inspect the methods - provided by a listener object and add the listener to one or more - internal event queues based on its capabilities. In terms of - efficiency and function call overhead, you're much better off only - providing implementations for the hooks you'll be using. - - """ - - @classmethod - def _adapt_listener(cls, self, listener): - """Adapt a :class:`.PoolListener` to individual - :class:`event.Dispatch` events. - - """ - - listener = util.as_interface(listener, methods=('connect', - 'first_connect', 'checkout', 'checkin')) - if hasattr(listener, 'connect'): - event.listen(self, 'connect', listener.connect) - if hasattr(listener, 'first_connect'): - event.listen(self, 'first_connect', listener.first_connect) - if hasattr(listener, 'checkout'): - event.listen(self, 'checkout', listener.checkout) - if hasattr(listener, 'checkin'): - event.listen(self, 'checkin', listener.checkin) - - def connect(self, dbapi_con, con_record): - """Called once for each new DB-API connection or Pool's ``creator()``. - - dbapi_con - A newly connected raw DB-API connection (not a SQLAlchemy - ``Connection`` wrapper). - - con_record - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - def first_connect(self, dbapi_con, con_record): - """Called exactly once for the first DB-API connection. - - dbapi_con - A newly connected raw DB-API connection (not a SQLAlchemy - ``Connection`` wrapper). - - con_record - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - def checkout(self, dbapi_con, con_record, con_proxy): - """Called when a connection is retrieved from the Pool. - - dbapi_con - A raw DB-API connection - - con_record - The ``_ConnectionRecord`` that persistently manages the connection - - con_proxy - The ``_ConnectionFairy`` which manages the connection for the span of - the current checkout. - - If you raise an ``exc.DisconnectionError``, the current - connection will be disposed and a fresh connection retrieved. - Processing of all checkout listeners will abort and restart - using the new connection. - """ - - def checkin(self, dbapi_con, con_record): - """Called when a connection returns to the pool. - - Note that the connection may be closed, and may be None if the - connection has been invalidated. ``checkin`` will not be called - for detached connections. (They do not return to the pool.) - - dbapi_con - A raw DB-API connection - - con_record - The ``_ConnectionRecord`` that persistently manages the connection - - """ - - -class ConnectionProxy(object): - """Allows interception of statement execution by Connections. - - .. note:: - - :class:`.ConnectionProxy` is deprecated. Please - refer to :class:`.ConnectionEvents`. - - Either or both of the ``execute()`` and ``cursor_execute()`` - may be implemented to intercept compiled statement and - cursor level executions, e.g.:: - - class MyProxy(ConnectionProxy): - def execute(self, conn, execute, clauseelement, - *multiparams, **params): - print "compiled statement:", clauseelement - return execute(clauseelement, *multiparams, **params) - - def cursor_execute(self, execute, cursor, statement, - parameters, context, executemany): - print "raw statement:", statement - return execute(cursor, statement, parameters, context) - - The ``execute`` argument is a function that will fulfill the default - execution behavior for the operation. The signature illustrated - in the example should be used. - - The proxy is installed into an :class:`~sqlalchemy.engine.Engine` via - the ``proxy`` argument:: - - e = create_engine('someurl://', proxy=MyProxy()) - - """ - - @classmethod - def _adapt_listener(cls, self, listener): - - def adapt_execute(conn, clauseelement, multiparams, params): - - def execute_wrapper(clauseelement, *multiparams, **params): - return clauseelement, multiparams, params - - return listener.execute(conn, execute_wrapper, - clauseelement, *multiparams, - **params) - - event.listen(self, 'before_execute', adapt_execute) - - def adapt_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - - def execute_wrapper( - cursor, - statement, - parameters, - context, - ): - return statement, parameters - - return listener.cursor_execute( - execute_wrapper, - cursor, - statement, - parameters, - context, - executemany, - ) - - event.listen(self, 'before_cursor_execute', adapt_cursor_execute) - - def do_nothing_callback(*arg, **kw): - pass - - def adapt_listener(fn): - - def go(conn, *arg, **kw): - fn(conn, do_nothing_callback, *arg, **kw) - - return util.update_wrapper(go, fn) - - event.listen(self, 'begin', adapt_listener(listener.begin)) - event.listen(self, 'rollback', - adapt_listener(listener.rollback)) - event.listen(self, 'commit', adapt_listener(listener.commit)) - event.listen(self, 'savepoint', - adapt_listener(listener.savepoint)) - event.listen(self, 'rollback_savepoint', - adapt_listener(listener.rollback_savepoint)) - event.listen(self, 'release_savepoint', - adapt_listener(listener.release_savepoint)) - event.listen(self, 'begin_twophase', - adapt_listener(listener.begin_twophase)) - event.listen(self, 'prepare_twophase', - adapt_listener(listener.prepare_twophase)) - event.listen(self, 'rollback_twophase', - adapt_listener(listener.rollback_twophase)) - event.listen(self, 'commit_twophase', - adapt_listener(listener.commit_twophase)) - - def execute(self, conn, execute, clauseelement, *multiparams, **params): - """Intercept high level execute() events.""" - - return execute(clauseelement, *multiparams, **params) - - def cursor_execute(self, execute, cursor, statement, parameters, - context, executemany): - """Intercept low-level cursor execute() events.""" - - return execute(cursor, statement, parameters, context) - - def begin(self, conn, begin): - """Intercept begin() events.""" - - return begin() - - def rollback(self, conn, rollback): - """Intercept rollback() events.""" - - return rollback() - - def commit(self, conn, commit): - """Intercept commit() events.""" - - return commit() - - def savepoint(self, conn, savepoint, name=None): - """Intercept savepoint() events.""" - - return savepoint(name=name) - - def rollback_savepoint(self, conn, rollback_savepoint, name, context): - """Intercept rollback_savepoint() events.""" - - return rollback_savepoint(name, context) - - def release_savepoint(self, conn, release_savepoint, name, context): - """Intercept release_savepoint() events.""" - - return release_savepoint(name, context) - - def begin_twophase(self, conn, begin_twophase, xid): - """Intercept begin_twophase() events.""" - - return begin_twophase(xid) - - def prepare_twophase(self, conn, prepare_twophase, xid): - """Intercept prepare_twophase() events.""" - - return prepare_twophase(xid) - - def rollback_twophase(self, conn, rollback_twophase, xid, is_prepared): - """Intercept rollback_twophase() events.""" - - return rollback_twophase(xid, is_prepared) - - def commit_twophase(self, conn, commit_twophase, xid, is_prepared): - """Intercept commit_twophase() events.""" - - return commit_twophase(xid, is_prepared) diff --git a/libs/sqlalchemy/log.py b/libs/sqlalchemy/log.py deleted file mode 100644 index 935761d5..00000000 --- a/libs/sqlalchemy/log.py +++ /dev/null @@ -1,214 +0,0 @@ -# sqlalchemy/log.py -# Copyright (C) 2006-2014 the SQLAlchemy authors and contributors -# Includes alterations by Vinay Sajip vinay_sajip@yahoo.co.uk -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Logging control and utilities. - -Control of logging for SA can be performed from the regular python logging -module. The regular dotted module namespace is used, starting at -'sqlalchemy'. For class-level logging, the class name is appended. - -The "echo" keyword parameter, available on SQLA :class:`.Engine` -and :class:`.Pool` objects, corresponds to a logger specific to that -instance only. - -""" - -import logging -import sys - -# set initial level to WARN. This so that -# log statements don't occur in the absense of explicit -# logging being enabled for 'sqlalchemy'. -rootlogger = logging.getLogger('sqlalchemy') -if rootlogger.level == logging.NOTSET: - rootlogger.setLevel(logging.WARN) - - -def _add_default_handler(logger): - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(logging.Formatter( - '%(asctime)s %(levelname)s %(name)s %(message)s')) - logger.addHandler(handler) - - -_logged_classes = set() - - -def class_logger(cls): - logger = logging.getLogger(cls.__module__ + "." + cls.__name__) - cls._should_log_debug = lambda self: logger.isEnabledFor(logging.DEBUG) - cls._should_log_info = lambda self: logger.isEnabledFor(logging.INFO) - cls.logger = logger - _logged_classes.add(cls) - return cls - -class Identified(object): - logging_name = None - - def _should_log_debug(self): - return self.logger.isEnabledFor(logging.DEBUG) - - def _should_log_info(self): - return self.logger.isEnabledFor(logging.INFO) - - -class InstanceLogger(object): - """A logger adapter (wrapper) for :class:`.Identified` subclasses. - - This allows multiple instances (e.g. Engine or Pool instances) - to share a logger, but have its verbosity controlled on a - per-instance basis. - - The basic functionality is to return a logging level - which is based on an instance's echo setting. - - Default implementation is: - - 'debug' -> logging.DEBUG - True -> logging.INFO - False -> Effective level of underlying logger - (logging.WARNING by default) - None -> same as False - """ - - # Map echo settings to logger levels - _echo_map = { - None: logging.NOTSET, - False: logging.NOTSET, - True: logging.INFO, - 'debug': logging.DEBUG, - } - - def __init__(self, echo, name): - self.echo = echo - self.logger = logging.getLogger(name) - - # if echo flag is enabled and no handlers, - # add a handler to the list - if self._echo_map[echo] <= logging.INFO \ - and not self.logger.handlers: - _add_default_handler(self.logger) - - # - # Boilerplate convenience methods - # - def debug(self, msg, *args, **kwargs): - """Delegate a debug call to the underlying logger.""" - - self.log(logging.DEBUG, msg, *args, **kwargs) - - def info(self, msg, *args, **kwargs): - """Delegate an info call to the underlying logger.""" - - self.log(logging.INFO, msg, *args, **kwargs) - - def warning(self, msg, *args, **kwargs): - """Delegate a warning call to the underlying logger.""" - - self.log(logging.WARNING, msg, *args, **kwargs) - - warn = warning - - def error(self, msg, *args, **kwargs): - """ - Delegate an error call to the underlying logger. - """ - self.log(logging.ERROR, msg, *args, **kwargs) - - def exception(self, msg, *args, **kwargs): - """Delegate an exception call to the underlying logger.""" - - kwargs["exc_info"] = 1 - self.log(logging.ERROR, msg, *args, **kwargs) - - def critical(self, msg, *args, **kwargs): - """Delegate a critical call to the underlying logger.""" - - self.log(logging.CRITICAL, msg, *args, **kwargs) - - def log(self, level, msg, *args, **kwargs): - """Delegate a log call to the underlying logger. - - The level here is determined by the echo - flag as well as that of the underlying logger, and - logger._log() is called directly. - - """ - - # inline the logic from isEnabledFor(), - # getEffectiveLevel(), to avoid overhead. - - if self.logger.manager.disable >= level: - return - - selected_level = self._echo_map[self.echo] - if selected_level == logging.NOTSET: - selected_level = self.logger.getEffectiveLevel() - - if level >= selected_level: - self.logger._log(level, msg, args, **kwargs) - - def isEnabledFor(self, level): - """Is this logger enabled for level 'level'?""" - - if self.logger.manager.disable >= level: - return False - return level >= self.getEffectiveLevel() - - def getEffectiveLevel(self): - """What's the effective level for this logger?""" - - level = self._echo_map[self.echo] - if level == logging.NOTSET: - level = self.logger.getEffectiveLevel() - return level - - -def instance_logger(instance, echoflag=None): - """create a logger for an instance that implements :class:`.Identified`.""" - - if instance.logging_name: - name = "%s.%s.%s" % (instance.__class__.__module__, - instance.__class__.__name__, instance.logging_name) - else: - name = "%s.%s" % (instance.__class__.__module__, - instance.__class__.__name__) - - instance._echo = echoflag - - if echoflag in (False, None): - # if no echo setting or False, return a Logger directly, - # avoiding overhead of filtering - logger = logging.getLogger(name) - else: - # if a specified echo flag, return an EchoLogger, - # which checks the flag, overrides normal log - # levels by calling logger._log() - logger = InstanceLogger(echoflag, name) - - instance.logger = logger - - -class echo_property(object): - __doc__ = """\ - When ``True``, enable log output for this element. - - This has the effect of setting the Python logging level for the namespace - of this element's class and object reference. A value of boolean ``True`` - indicates that the loglevel ``logging.INFO`` will be set for the logger, - whereas the string value ``debug`` will set the loglevel to - ``logging.DEBUG``. - """ - - def __get__(self, instance, owner): - if instance is None: - return self - else: - return instance._echo - - def __set__(self, instance, value): - instance_logger(instance, echoflag=value) diff --git a/libs/sqlalchemy/orm/__init__.py b/libs/sqlalchemy/orm/__init__.py deleted file mode 100644 index 7825a70a..00000000 --- a/libs/sqlalchemy/orm/__init__.py +++ /dev/null @@ -1,267 +0,0 @@ -# orm/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" -Functional constructs for ORM configuration. - -See the SQLAlchemy object relational tutorial and mapper configuration -documentation for an overview of how this module is used. - -""" - -from . import exc -from .mapper import ( - Mapper, - _mapper_registry, - class_mapper, - configure_mappers, - reconstructor, - validates - ) -from .interfaces import ( - EXT_CONTINUE, - EXT_STOP, - PropComparator, - ) -from .deprecated_interfaces import ( - MapperExtension, - SessionExtension, - AttributeExtension, -) -from .util import ( - aliased, - join, - object_mapper, - outerjoin, - polymorphic_union, - was_deleted, - with_parent, - with_polymorphic, - ) -from .properties import ColumnProperty -from .relationships import RelationshipProperty -from .descriptor_props import ( - ComparableProperty, - CompositeProperty, - SynonymProperty, - ) -from .relationships import ( - foreign, - remote, -) -from .session import ( - Session, - object_session, - sessionmaker, - make_transient -) -from .scoping import ( - scoped_session -) -from . import mapper as mapperlib -from .query import AliasOption, Query, Bundle -from ..util.langhelpers import public_factory -from .. import util as _sa_util -from . import strategies as _strategies - -def create_session(bind=None, **kwargs): - """Create a new :class:`.Session` - with no automation enabled by default. - - This function is used primarily for testing. The usual - route to :class:`.Session` creation is via its constructor - or the :func:`.sessionmaker` function. - - :param bind: optional, a single Connectable to use for all - database access in the created - :class:`~sqlalchemy.orm.session.Session`. - - :param \*\*kwargs: optional, passed through to the - :class:`.Session` constructor. - - :returns: an :class:`~sqlalchemy.orm.session.Session` instance - - The defaults of create_session() are the opposite of that of - :func:`sessionmaker`; ``autoflush`` and ``expire_on_commit`` are - False, ``autocommit`` is True. In this sense the session acts - more like the "classic" SQLAlchemy 0.3 session with these. - - Usage:: - - >>> from sqlalchemy.orm import create_session - >>> session = create_session() - - It is recommended to use :func:`sessionmaker` instead of - create_session(). - - """ - kwargs.setdefault('autoflush', False) - kwargs.setdefault('autocommit', True) - kwargs.setdefault('expire_on_commit', False) - return Session(bind=bind, **kwargs) - -relationship = public_factory(RelationshipProperty, ".orm.relationship") - -def relation(*arg, **kw): - """A synonym for :func:`relationship`.""" - - return relationship(*arg, **kw) - - -def dynamic_loader(argument, **kw): - """Construct a dynamically-loading mapper property. - - This is essentially the same as - using the ``lazy='dynamic'`` argument with :func:`relationship`:: - - dynamic_loader(SomeClass) - - # is the same as - - relationship(SomeClass, lazy="dynamic") - - See the section :ref:`dynamic_relationship` for more details - on dynamic loading. - - """ - kw['lazy'] = 'dynamic' - return relationship(argument, **kw) - - -column_property = public_factory(ColumnProperty, ".orm.column_property") -composite = public_factory(CompositeProperty, ".orm.composite") - - -def backref(name, **kwargs): - """Create a back reference with explicit keyword arguments, which are the - same arguments one can send to :func:`relationship`. - - Used with the ``backref`` keyword argument to :func:`relationship` in - place of a string argument, e.g.:: - - 'items':relationship(SomeItem, backref=backref('parent', lazy='subquery')) - - """ - return (name, kwargs) - - -def deferred(*columns, **kw): - """Indicate a column-based mapped attribute that by default will - not load unless accessed. - - :param \*columns: columns to be mapped. This is typically a single - :class:`.Column` object, however a collection is supported in order - to support multiple columns mapped under the same attribute. - - :param \**kw: additional keyword arguments passed to :class:`.ColumnProperty`. - - .. seealso:: - - :ref:`deferred` - - """ - return ColumnProperty(deferred=True, *columns, **kw) - - -mapper = public_factory(Mapper, ".orm.mapper") - -synonym = public_factory(SynonymProperty, ".orm.synonym") - -comparable_property = public_factory(ComparableProperty, - ".orm.comparable_property") - - -@_sa_util.deprecated("0.7", message=":func:`.compile_mappers` " - "is renamed to :func:`.configure_mappers`") -def compile_mappers(): - """Initialize the inter-mapper relationships of all mappers that have - been defined. - - """ - configure_mappers() - - -def clear_mappers(): - """Remove all mappers from all classes. - - This function removes all instrumentation from classes and disposes - of their associated mappers. Once called, the classes are unmapped - and can be later re-mapped with new mappers. - - :func:`.clear_mappers` is *not* for normal use, as there is literally no - valid usage for it outside of very specific testing scenarios. Normally, - mappers are permanent structural components of user-defined classes, and - are never discarded independently of their class. If a mapped class itself - is garbage collected, its mapper is automatically disposed of as well. As - such, :func:`.clear_mappers` is only for usage in test suites that re-use - the same classes with different mappings, which is itself an extremely rare - use case - the only such use case is in fact SQLAlchemy's own test suite, - and possibly the test suites of other ORM extension libraries which - intend to test various combinations of mapper construction upon a fixed - set of classes. - - """ - mapperlib._CONFIGURE_MUTEX.acquire() - try: - while _mapper_registry: - try: - # can't even reliably call list(weakdict) in jython - mapper, b = _mapper_registry.popitem() - mapper.dispose() - except KeyError: - pass - finally: - mapperlib._CONFIGURE_MUTEX.release() - -from . import strategy_options - -joinedload = strategy_options.joinedload._unbound_fn -joinedload_all = strategy_options.joinedload._unbound_all_fn -contains_eager = strategy_options.contains_eager._unbound_fn -defer = strategy_options.defer._unbound_fn -undefer = strategy_options.undefer._unbound_fn -undefer_group = strategy_options.undefer_group._unbound_fn -load_only = strategy_options.load_only._unbound_fn -lazyload = strategy_options.lazyload._unbound_fn -lazyload_all = strategy_options.lazyload_all._unbound_all_fn -subqueryload = strategy_options.subqueryload._unbound_fn -subqueryload_all = strategy_options.subqueryload_all._unbound_all_fn -immediateload = strategy_options.immediateload._unbound_fn -noload = strategy_options.noload._unbound_fn -defaultload = strategy_options.defaultload._unbound_fn - -from .strategy_options import Load - -def eagerload(*args, **kwargs): - """A synonym for :func:`joinedload()`.""" - return joinedload(*args, **kwargs) - - -def eagerload_all(*args, **kwargs): - """A synonym for :func:`joinedload_all()`""" - return joinedload_all(*args, **kwargs) - - - - -contains_alias = public_factory(AliasOption, ".orm.contains_alias") - - - -def __go(lcls): - global __all__ - from .. import util as sa_util - from . import dynamic - from . import events - import inspect as _inspect - - __all__ = sorted(name for name, obj in lcls.items() - if not (name.startswith('_') or _inspect.ismodule(obj))) - - _sa_util.dependencies.resolve_all("sqlalchemy.orm") - -__go(locals()) - diff --git a/libs/sqlalchemy/orm/attributes.py b/libs/sqlalchemy/orm/attributes.py deleted file mode 100644 index e5f8550a..00000000 --- a/libs/sqlalchemy/orm/attributes.py +++ /dev/null @@ -1,1541 +0,0 @@ -# orm/attributes.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Defines instrumentation for class attributes and their interaction -with instances. - -This module is usually not directly visible to user applications, but -defines a large part of the ORM's interactivity. - - -""" - -import operator -from .. import util, event, inspection -from . import interfaces, collections, exc as orm_exc - -from .base import instance_state, instance_dict, manager_of_class - -from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\ - NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\ - INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\ - PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\ - PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT -from .base import state_str, instance_str - -@inspection._self_inspects -class QueryableAttribute(interfaces._MappedAttribute, - interfaces._InspectionAttr, - interfaces.PropComparator): - """Base class for :term:`descriptor` objects that intercept - attribute events on behalf of a :class:`.MapperProperty` - object. The actual :class:`.MapperProperty` is accessible - via the :attr:`.QueryableAttribute.property` - attribute. - - - .. seealso:: - - :class:`.InstrumentedAttribute` - - :class:`.MapperProperty` - - :attr:`.Mapper.all_orm_descriptors` - - :attr:`.Mapper.attrs` - """ - - is_attribute = True - - def __init__(self, class_, key, impl=None, - comparator=None, parententity=None, - of_type=None): - self.class_ = class_ - self.key = key - self.impl = impl - self.comparator = comparator - self._parententity = parententity - self._of_type = of_type - - manager = manager_of_class(class_) - # manager is None in the case of AliasedClass - if manager: - # propagate existing event listeners from - # immediate superclass - for base in manager._bases: - if key in base: - self.dispatch._update(base[key].dispatch) - - @util.memoized_property - def _supports_population(self): - return self.impl.supports_population - - def get_history(self, instance, passive=PASSIVE_OFF): - return self.impl.get_history(instance_state(instance), - instance_dict(instance), passive) - - def __selectable__(self): - # TODO: conditionally attach this method based on clause_element ? - return self - - - @util.memoized_property - def info(self): - """Return the 'info' dictionary for the underlying SQL element. - - The behavior here is as follows: - - * If the attribute is a column-mapped property, i.e. - :class:`.ColumnProperty`, which is mapped directly - to a schema-level :class:`.Column` object, this attribute - will return the :attr:`.SchemaItem.info` dictionary associated - with the core-level :class:`.Column` object. - - * If the attribute is a :class:`.ColumnProperty` but is mapped to - any other kind of SQL expression other than a :class:`.Column`, - the attribute will refer to the :attr:`.MapperProperty.info` dictionary - associated directly with the :class:`.ColumnProperty`, assuming the SQL - expression itself does not have it's own ``.info`` attribute - (which should be the case, unless a user-defined SQL construct - has defined one). - - * If the attribute refers to any other kind of :class:`.MapperProperty`, - including :class:`.RelationshipProperty`, the attribute will refer - to the :attr:`.MapperProperty.info` dictionary associated with - that :class:`.MapperProperty`. - - * To access the :attr:`.MapperProperty.info` dictionary of the :class:`.MapperProperty` - unconditionally, including for a :class:`.ColumnProperty` that's - associated directly with a :class:`.schema.Column`, the attribute - can be referred to using :attr:`.QueryableAttribute.property` - attribute, as ``MyClass.someattribute.property.info``. - - .. versionadded:: 0.8.0 - - .. seealso:: - - :attr:`.SchemaItem.info` - - :attr:`.MapperProperty.info` - - """ - return self.comparator.info - - @util.memoized_property - def parent(self): - """Return an inspection instance representing the parent. - - This will be either an instance of :class:`.Mapper` - or :class:`.AliasedInsp`, depending upon the nature - of the parent entity which this attribute is associated - with. - - """ - return inspection.inspect(self._parententity) - - @property - def expression(self): - return self.comparator.__clause_element__() - - def __clause_element__(self): - return self.comparator.__clause_element__() - - def _query_clause_element(self): - """like __clause_element__(), but called specifically - by :class:`.Query` to allow special behavior.""" - - return self.comparator._query_clause_element() - - def adapt_to_entity(self, adapt_to_entity): - assert not self._of_type - return self.__class__(adapt_to_entity.entity, self.key, impl=self.impl, - comparator=self.comparator.adapt_to_entity(adapt_to_entity), - parententity=adapt_to_entity) - - def of_type(self, cls): - return QueryableAttribute( - self.class_, - self.key, - self.impl, - self.comparator.of_type(cls), - self._parententity, - of_type=cls) - - def label(self, name): - return self._query_clause_element().label(name) - - def operate(self, op, *other, **kwargs): - return op(self.comparator, *other, **kwargs) - - def reverse_operate(self, op, other, **kwargs): - return op(other, self.comparator, **kwargs) - - def hasparent(self, state, optimistic=False): - return self.impl.hasparent(state, optimistic=optimistic) is not False - - def __getattr__(self, key): - try: - return getattr(self.comparator, key) - except AttributeError: - raise AttributeError( - 'Neither %r object nor %r object associated with %s ' - 'has an attribute %r' % ( - type(self).__name__, - type(self.comparator).__name__, - self, - key) - ) - - def __str__(self): - return "%s.%s" % (self.class_.__name__, self.key) - - @util.memoized_property - def property(self): - """Return the :class:`.MapperProperty` associated with this - :class:`.QueryableAttribute`. - - - Return values here will commonly be instances of - :class:`.ColumnProperty` or :class:`.RelationshipProperty`. - - - """ - return self.comparator.property - - -class InstrumentedAttribute(QueryableAttribute): - """Class bound instrumented attribute which adds basic - :term:`descriptor` methods. - - See :class:`.QueryableAttribute` for a description of most features. - - - """ - - def __set__(self, instance, value): - self.impl.set(instance_state(instance), - instance_dict(instance), value, None) - - def __delete__(self, instance): - self.impl.delete(instance_state(instance), instance_dict(instance)) - - def __get__(self, instance, owner): - if instance is None: - return self - - dict_ = instance_dict(instance) - if self._supports_population and self.key in dict_: - return dict_[self.key] - else: - return self.impl.get(instance_state(instance), dict_) - - -def create_proxied_attribute(descriptor): - """Create an QueryableAttribute / user descriptor hybrid. - - Returns a new QueryableAttribute type that delegates descriptor - behavior and getattr() to the given descriptor. - """ - - # TODO: can move this to descriptor_props if the need for this - # function is removed from ext/hybrid.py - - class Proxy(QueryableAttribute): - """Presents the :class:`.QueryableAttribute` interface as a - proxy on top of a Python descriptor / :class:`.PropComparator` - combination. - - """ - - def __init__(self, class_, key, descriptor, - comparator, - adapt_to_entity=None, doc=None, - original_property=None): - self.class_ = class_ - self.key = key - self.descriptor = descriptor - self.original_property = original_property - self._comparator = comparator - self._adapt_to_entity = adapt_to_entity - self.__doc__ = doc - - @property - def property(self): - return self.comparator.property - - @util.memoized_property - def comparator(self): - if util.callable(self._comparator): - self._comparator = self._comparator() - if self._adapt_to_entity: - self._comparator = self._comparator.adapt_to_entity( - self._adapt_to_entity) - return self._comparator - - def adapt_to_entity(self, adapt_to_entity): - return self.__class__(adapt_to_entity.entity, self.key, self.descriptor, - self._comparator, - adapt_to_entity) - - def __get__(self, instance, owner): - if instance is None: - return self - else: - return self.descriptor.__get__(instance, owner) - - def __str__(self): - return "%s.%s" % (self.class_.__name__, self.key) - - def __getattr__(self, attribute): - """Delegate __getattr__ to the original descriptor and/or - comparator.""" - - try: - return getattr(descriptor, attribute) - except AttributeError: - try: - return getattr(self.comparator, attribute) - except AttributeError: - raise AttributeError( - 'Neither %r object nor %r object associated with %s ' - 'has an attribute %r' % ( - type(descriptor).__name__, - type(self.comparator).__name__, - self, - attribute) - ) - - Proxy.__name__ = type(descriptor).__name__ + 'Proxy' - - util.monkeypatch_proxied_specials(Proxy, type(descriptor), - name='descriptor', - from_instance=descriptor) - return Proxy - -OP_REMOVE = util.symbol("REMOVE") -OP_APPEND = util.symbol("APPEND") -OP_REPLACE = util.symbol("REPLACE") - -class Event(object): - """A token propagated throughout the course of a chain of attribute - events. - - Serves as an indicator of the source of the event and also provides - a means of controlling propagation across a chain of attribute - operations. - - The :class:`.Event` object is sent as the ``initiator`` argument - when dealing with the :meth:`.AttributeEvents.append`, - :meth:`.AttributeEvents.set`, - and :meth:`.AttributeEvents.remove` events. - - The :class:`.Event` object is currently interpreted by the backref - event handlers, and is used to control the propagation of operations - across two mutually-dependent attributes. - - .. versionadded:: 0.9.0 - - """ - - impl = None - """The :class:`.AttributeImpl` which is the current event initiator. - """ - - op = None - """The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE` or :attr:`.OP_REPLACE`, - indicating the source operation. - - """ - - def __init__(self, attribute_impl, op): - self.impl = attribute_impl - self.op = op - self.parent_token = self.impl.parent_token - - - @property - def key(self): - return self.impl.key - - def hasparent(self, state): - return self.impl.hasparent(state) - -class AttributeImpl(object): - """internal implementation for instrumented attributes.""" - - def __init__(self, class_, key, - callable_, dispatch, trackparent=False, extension=None, - compare_function=None, active_history=False, - parent_token=None, expire_missing=True, - send_modified_events=True, - **kwargs): - """Construct an AttributeImpl. - - \class_ - associated class - - key - string name of the attribute - - \callable_ - optional function which generates a callable based on a parent - instance, which produces the "default" values for a scalar or - collection attribute when it's first accessed, if not present - already. - - trackparent - if True, attempt to track if an instance has a parent attached - to it via this attribute. - - extension - a single or list of AttributeExtension object(s) which will - receive set/delete/append/remove/etc. events. Deprecated. - The event package is now used. - - compare_function - a function that compares two values which are normally - assignable to this attribute. - - active_history - indicates that get_history() should always return the "old" value, - even if it means executing a lazy callable upon attribute change. - - parent_token - Usually references the MapperProperty, used as a key for - the hasparent() function to identify an "owning" attribute. - Allows multiple AttributeImpls to all match a single - owner attribute. - - expire_missing - if False, don't add an "expiry" callable to this attribute - during state.expire_attributes(None), if no value is present - for this key. - - send_modified_events - if False, the InstanceState._modified_event method will have no effect; - this means the attribute will never show up as changed in a - history entry. - """ - self.class_ = class_ - self.key = key - self.callable_ = callable_ - self.dispatch = dispatch - self.trackparent = trackparent - self.parent_token = parent_token or self - self.send_modified_events = send_modified_events - if compare_function is None: - self.is_equal = operator.eq - else: - self.is_equal = compare_function - - # TODO: pass in the manager here - # instead of doing a lookup - attr = manager_of_class(class_)[key] - - for ext in util.to_list(extension or []): - ext._adapt_listener(attr, ext) - - if active_history: - self.dispatch._active_history = True - - self.expire_missing = expire_missing - - def __str__(self): - return "%s.%s" % (self.class_.__name__, self.key) - - def _get_active_history(self): - """Backwards compat for impl.active_history""" - - return self.dispatch._active_history - - def _set_active_history(self, value): - self.dispatch._active_history = value - - active_history = property(_get_active_history, _set_active_history) - - def hasparent(self, state, optimistic=False): - """Return the boolean value of a `hasparent` flag attached to - the given state. - - The `optimistic` flag determines what the default return value - should be if no `hasparent` flag can be located. - - As this function is used to determine if an instance is an - *orphan*, instances that were loaded from storage should be - assumed to not be orphans, until a True/False value for this - flag is set. - - An instance attribute that is loaded by a callable function - will also not have a `hasparent` flag. - - """ - msg = "This AttributeImpl is not configured to track parents." - assert self.trackparent, msg - - return state.parents.get(id(self.parent_token), optimistic) \ - is not False - - def sethasparent(self, state, parent_state, value): - """Set a boolean flag on the given item corresponding to - whether or not it is attached to a parent object via the - attribute represented by this ``InstrumentedAttribute``. - - """ - msg = "This AttributeImpl is not configured to track parents." - assert self.trackparent, msg - - id_ = id(self.parent_token) - if value: - state.parents[id_] = parent_state - else: - if id_ in state.parents: - last_parent = state.parents[id_] - - if last_parent is not False and \ - last_parent.key != parent_state.key: - - if last_parent.obj() is None: - raise orm_exc.StaleDataError( - "Removing state %s from parent " - "state %s along attribute '%s', " - "but the parent record " - "has gone stale, can't be sure this " - "is the most recent parent." % - (state_str(state), - state_str(parent_state), - self.key)) - - return - - state.parents[id_] = False - - def set_callable(self, state, callable_): - """Set a callable function for this attribute on the given object. - - This callable will be executed when the attribute is next - accessed, and is assumed to construct part of the instances - previously stored state. When its value or values are loaded, - they will be established as part of the instance's *committed - state*. While *trackparent* information will be assembled for - these instances, attribute-level event handlers will not be - fired. - - The callable overrides the class level callable set in the - ``InstrumentedAttribute`` constructor. - - """ - state.callables[self.key] = callable_ - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - raise NotImplementedError() - - def get_all_pending(self, state, dict_): - """Return a list of tuples of (state, obj) - for all objects in this attribute's current state - + history. - - Only applies to object-based attributes. - - This is an inlining of existing functionality - which roughly corresponds to: - - get_state_history( - state, - key, - passive=PASSIVE_NO_INITIALIZE).sum() - - """ - raise NotImplementedError() - - def initialize(self, state, dict_): - """Initialize the given state's attribute with an empty value.""" - - dict_[self.key] = None - return None - - def get(self, state, dict_, passive=PASSIVE_OFF): - """Retrieve a value from the given object. - If a callable is assembled on this object's attribute, and - passive is False, the callable will be executed and the - resulting value will be set as the new value for this attribute. - """ - if self.key in dict_: - return dict_[self.key] - else: - # if history present, don't load - key = self.key - if key not in state.committed_state or \ - state.committed_state[key] is NEVER_SET: - if not passive & CALLABLES_OK: - return PASSIVE_NO_RESULT - - if key in state.callables: - callable_ = state.callables[key] - value = callable_(state, passive) - elif self.callable_: - value = self.callable_(state, passive) - else: - value = ATTR_EMPTY - - if value is PASSIVE_NO_RESULT or value is NEVER_SET: - return value - elif value is ATTR_WAS_SET: - try: - return dict_[key] - except KeyError: - # TODO: no test coverage here. - raise KeyError( - "Deferred loader for attribute " - "%r failed to populate " - "correctly" % key) - elif value is not ATTR_EMPTY: - return self.set_committed_value(state, dict_, value) - - if not passive & INIT_OK: - return NEVER_SET - else: - # Return a new, empty value - return self.initialize(state, dict_) - - def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, value, initiator, passive=passive) - - def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, None, initiator, - passive=passive, check_old=value) - - def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, None, initiator, - passive=passive, check_old=value, pop=True) - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, check_old=None, pop=False): - raise NotImplementedError() - - def get_committed_value(self, state, dict_, passive=PASSIVE_OFF): - """return the unchanged value of this attribute""" - - if self.key in state.committed_state: - value = state.committed_state[self.key] - if value is NO_VALUE: - return None - else: - return value - else: - return self.get(state, dict_, passive=passive) - - def set_committed_value(self, state, dict_, value): - """set an attribute value on the given instance and 'commit' it.""" - - dict_[self.key] = value - state._commit(dict_, [self.key]) - return value - - -class ScalarAttributeImpl(AttributeImpl): - """represents a scalar value-holding InstrumentedAttribute.""" - - accepts_scalar_loader = True - uses_objects = False - supports_population = True - collection = False - - def delete(self, state, dict_): - - # TODO: catch key errors, convert to attributeerror? - if self.dispatch._active_history: - old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) - else: - old = dict_.get(self.key, NO_VALUE) - - if self.dispatch.remove: - self.fire_remove_event(state, dict_, old, self._remove_token) - state._modified_event(dict_, self, old) - del dict_[self.key] - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - if self.key in dict_: - return History.from_scalar_attribute(self, state, dict_[self.key]) - else: - if passive & INIT_OK: - passive ^= INIT_OK - current = self.get(state, dict_, passive=passive) - if current is PASSIVE_NO_RESULT: - return HISTORY_BLANK - else: - return History.from_scalar_attribute(self, state, current) - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, check_old=None, pop=False): - if self.dispatch._active_history: - old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) - else: - old = dict_.get(self.key, NO_VALUE) - - if self.dispatch.set: - value = self.fire_replace_event(state, dict_, - value, old, initiator) - state._modified_event(dict_, self, old) - dict_[self.key] = value - - @util.memoized_property - def _replace_token(self): - return Event(self, OP_REPLACE) - - @util.memoized_property - def _append_token(self): - return Event(self, OP_REPLACE) - - @util.memoized_property - def _remove_token(self): - return Event(self, OP_REMOVE) - - def fire_replace_event(self, state, dict_, value, previous, initiator): - for fn in self.dispatch.set: - value = fn(state, value, previous, initiator or self._replace_token) - return value - - def fire_remove_event(self, state, dict_, value, initiator): - for fn in self.dispatch.remove: - fn(state, value, initiator or self._remove_token) - - @property - def type(self): - self.property.columns[0].type - - -class ScalarObjectAttributeImpl(ScalarAttributeImpl): - """represents a scalar-holding InstrumentedAttribute, - where the target object is also instrumented. - - Adds events to delete/set operations. - - """ - - accepts_scalar_loader = False - uses_objects = True - supports_population = True - collection = False - - def delete(self, state, dict_): - old = self.get(state, dict_) - self.fire_remove_event(state, dict_, old, self._remove_token) - del dict_[self.key] - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - if self.key in dict_: - return History.from_object_attribute(self, state, dict_[self.key]) - else: - if passive & INIT_OK: - passive ^= INIT_OK - current = self.get(state, dict_, passive=passive) - if current is PASSIVE_NO_RESULT: - return HISTORY_BLANK - else: - return History.from_object_attribute(self, state, current) - - def get_all_pending(self, state, dict_): - if self.key in dict_: - current = dict_[self.key] - if current is not None: - ret = [(instance_state(current), current)] - else: - ret = [(None, None)] - - if self.key in state.committed_state: - original = state.committed_state[self.key] - if original not in (NEVER_SET, PASSIVE_NO_RESULT, None) and \ - original is not current: - - ret.append((instance_state(original), original)) - return ret - else: - return [] - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, check_old=None, pop=False): - """Set a value on the given InstanceState. - - """ - if self.dispatch._active_history: - old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) - else: - old = self.get(state, dict_, passive=PASSIVE_NO_FETCH) - - if check_old is not None and \ - old is not PASSIVE_NO_RESULT and \ - check_old is not old: - if pop: - return - else: - raise ValueError( - "Object %s not associated with %s on attribute '%s'" % ( - instance_str(check_old), - state_str(state), - self.key - )) - value = self.fire_replace_event(state, dict_, value, old, initiator) - dict_[self.key] = value - - - def fire_remove_event(self, state, dict_, value, initiator): - if self.trackparent and value is not None: - self.sethasparent(instance_state(value), state, False) - - for fn in self.dispatch.remove: - fn(state, value, initiator or self._remove_token) - - state._modified_event(dict_, self, value) - - def fire_replace_event(self, state, dict_, value, previous, initiator): - if self.trackparent: - if (previous is not value and - previous is not None and - previous is not PASSIVE_NO_RESULT): - self.sethasparent(instance_state(previous), state, False) - - for fn in self.dispatch.set: - value = fn(state, value, previous, initiator or self._replace_token) - - state._modified_event(dict_, self, previous) - - if self.trackparent: - if value is not None: - self.sethasparent(instance_state(value), state, True) - - return value - - -class CollectionAttributeImpl(AttributeImpl): - """A collection-holding attribute that instruments changes in membership. - - Only handles collections of instrumented objects. - - InstrumentedCollectionAttribute holds an arbitrary, user-specified - container object (defaulting to a list) and brokers access to the - CollectionAdapter, a "view" onto that object that presents consistent bag - semantics to the orm layer independent of the user data implementation. - - """ - accepts_scalar_loader = False - uses_objects = True - supports_population = True - collection = True - - def __init__(self, class_, key, callable_, dispatch, - typecallable=None, trackparent=False, extension=None, - copy_function=None, compare_function=None, **kwargs): - super(CollectionAttributeImpl, self).__init__( - class_, - key, - callable_, dispatch, - trackparent=trackparent, - extension=extension, - compare_function=compare_function, - **kwargs) - - if copy_function is None: - copy_function = self.__copy - self.copy = copy_function - self.collection_factory = typecallable - - def __copy(self, item): - return [y for y in collections.collection_adapter(item)] - - def get_history(self, state, dict_, passive=PASSIVE_OFF): - current = self.get(state, dict_, passive=passive) - if current is PASSIVE_NO_RESULT: - return HISTORY_BLANK - else: - return History.from_collection(self, state, current) - - def get_all_pending(self, state, dict_): - if self.key not in dict_: - return [] - - current = dict_[self.key] - current = getattr(current, '_sa_adapter') - - if self.key in state.committed_state: - original = state.committed_state[self.key] - if original not in (NO_VALUE, NEVER_SET): - current_states = [((c is not None) and - instance_state(c) or None, c) - for c in current] - original_states = [((c is not None) and - instance_state(c) or None, c) - for c in original] - - current_set = dict(current_states) - original_set = dict(original_states) - - return \ - [(s, o) for s, o in current_states - if s not in original_set] + \ - [(s, o) for s, o in current_states - if s in original_set] + \ - [(s, o) for s, o in original_states - if s not in current_set] - - return [(instance_state(o), o) for o in current] - - @util.memoized_property - def _append_token(self): - return Event(self, OP_APPEND) - - @util.memoized_property - def _remove_token(self): - return Event(self, OP_REMOVE) - - def fire_append_event(self, state, dict_, value, initiator): - for fn in self.dispatch.append: - value = fn(state, value, initiator or self._append_token) - - state._modified_event(dict_, self, NEVER_SET, True) - - if self.trackparent and value is not None: - self.sethasparent(instance_state(value), state, True) - - return value - - def fire_pre_remove_event(self, state, dict_, initiator): - state._modified_event(dict_, self, NEVER_SET, True) - - def fire_remove_event(self, state, dict_, value, initiator): - if self.trackparent and value is not None: - self.sethasparent(instance_state(value), state, False) - - for fn in self.dispatch.remove: - fn(state, value, initiator or self._remove_token) - - state._modified_event(dict_, self, NEVER_SET, True) - - def delete(self, state, dict_): - if self.key not in dict_: - return - - state._modified_event(dict_, self, NEVER_SET, True) - - collection = self.get_collection(state, state.dict) - collection.clear_with_event() - # TODO: catch key errors, convert to attributeerror? - del dict_[self.key] - - def initialize(self, state, dict_): - """Initialize this attribute with an empty collection.""" - - _, user_data = self._initialize_collection(state) - dict_[self.key] = user_data - return user_data - - def _initialize_collection(self, state): - return state.manager.initialize_collection( - self.key, state, self.collection_factory) - - def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - collection = self.get_collection(state, dict_, passive=passive) - if collection is PASSIVE_NO_RESULT: - value = self.fire_append_event(state, dict_, value, initiator) - assert self.key not in dict_, \ - "Collection was loaded during event handling." - state._get_pending_mutation(self.key).append(value) - else: - collection.append_with_event(value, initiator) - - def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - collection = self.get_collection(state, state.dict, passive=passive) - if collection is PASSIVE_NO_RESULT: - self.fire_remove_event(state, dict_, value, initiator) - assert self.key not in dict_, \ - "Collection was loaded during event handling." - state._get_pending_mutation(self.key).remove(value) - else: - collection.remove_with_event(value, initiator) - - def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - try: - # TODO: better solution here would be to add - # a "popper" role to collections.py to complement - # "remover". - self.remove(state, dict_, value, initiator, passive=passive) - except (ValueError, KeyError, IndexError): - pass - - def set(self, state, dict_, value, initiator, - passive=PASSIVE_OFF, pop=False): - """Set a value on the given object. - - """ - - self._set_iterable( - state, dict_, value, - lambda adapter, i: adapter.adapt_like_to_iterable(i)) - - def _set_iterable(self, state, dict_, iterable, adapter=None): - """Set a collection value from an iterable of state-bearers. - - ``adapter`` is an optional callable invoked with a CollectionAdapter - and the iterable. Should return an iterable of state-bearing - instances suitable for appending via a CollectionAdapter. Can be used - for, e.g., adapting an incoming dictionary into an iterator of values - rather than keys. - - """ - # pulling a new collection first so that an adaptation exception does - # not trigger a lazy load of the old collection. - new_collection, user_data = self._initialize_collection(state) - if adapter: - new_values = list(adapter(new_collection, iterable)) - else: - new_values = list(iterable) - - old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) - if old is PASSIVE_NO_RESULT: - old = self.initialize(state, dict_) - elif old is iterable: - # ignore re-assignment of the current collection, as happens - # implicitly with in-place operators (foo.collection |= other) - return - - # place a copy of "old" in state.committed_state - state._modified_event(dict_, self, old, True) - - old_collection = getattr(old, '_sa_adapter') - - dict_[self.key] = user_data - - collections.bulk_replace(new_values, old_collection, new_collection) - old_collection.unlink(old) - - def _invalidate_collection(self, collection): - adapter = getattr(collection, '_sa_adapter') - adapter.invalidated = True - - def set_committed_value(self, state, dict_, value): - """Set an attribute value on the given instance and 'commit' it.""" - - collection, user_data = self._initialize_collection(state) - - if value: - collection.append_multiple_without_event(value) - - state.dict[self.key] = user_data - - state._commit(dict_, [self.key]) - - if self.key in state._pending_mutations: - # pending items exist. issue a modified event, - # add/remove new items. - state._modified_event(dict_, self, user_data, True) - - pending = state._pending_mutations.pop(self.key) - added = pending.added_items - removed = pending.deleted_items - for item in added: - collection.append_without_event(item) - for item in removed: - collection.remove_without_event(item) - - return user_data - - def get_collection(self, state, dict_, - user_data=None, passive=PASSIVE_OFF): - """Retrieve the CollectionAdapter associated with the given state. - - Creates a new CollectionAdapter if one does not exist. - - """ - if user_data is None: - user_data = self.get(state, dict_, passive=passive) - if user_data is PASSIVE_NO_RESULT: - return user_data - - return getattr(user_data, '_sa_adapter') - - -def backref_listeners(attribute, key, uselist): - """Apply listeners to synchronize a two-way relationship.""" - - # use easily recognizable names for stack traces - - parent_token = attribute.impl.parent_token - parent_impl = attribute.impl - - def _acceptable_key_err(child_state, initiator, child_impl): - raise ValueError( - "Bidirectional attribute conflict detected: " - 'Passing object %s to attribute "%s" ' - 'triggers a modify event on attribute "%s" ' - 'via the backref "%s".' % ( - state_str(child_state), - initiator.parent_token, - child_impl.parent_token, - attribute.impl.parent_token - ) - ) - - def emit_backref_from_scalar_set_event(state, child, oldchild, initiator): - if oldchild is child: - return child - if oldchild is not None and oldchild is not PASSIVE_NO_RESULT: - # With lazy=None, there's no guarantee that the full collection is - # present when updating via a backref. - old_state, old_dict = instance_state(oldchild),\ - instance_dict(oldchild) - impl = old_state.manager[key].impl - - if initiator.impl is not impl or \ - initiator.op not in (OP_REPLACE, OP_REMOVE): - impl.pop(old_state, - old_dict, - state.obj(), - parent_impl._append_token, - passive=PASSIVE_NO_FETCH) - - if child is not None: - child_state, child_dict = instance_state(child),\ - instance_dict(child) - child_impl = child_state.manager[key].impl - if initiator.parent_token is not parent_token and \ - initiator.parent_token is not child_impl.parent_token: - _acceptable_key_err(state, initiator, child_impl) - elif initiator.impl is not child_impl or \ - initiator.op not in (OP_APPEND, OP_REPLACE): - child_impl.append( - child_state, - child_dict, - state.obj(), - initiator, - passive=PASSIVE_NO_FETCH) - return child - - def emit_backref_from_collection_append_event(state, child, initiator): - if child is None: - return - - child_state, child_dict = instance_state(child), \ - instance_dict(child) - child_impl = child_state.manager[key].impl - - if initiator.parent_token is not parent_token and \ - initiator.parent_token is not child_impl.parent_token: - _acceptable_key_err(state, initiator, child_impl) - elif initiator.impl is not child_impl or \ - initiator.op not in (OP_APPEND, OP_REPLACE): - child_impl.append( - child_state, - child_dict, - state.obj(), - initiator, - passive=PASSIVE_NO_FETCH) - return child - - def emit_backref_from_collection_remove_event(state, child, initiator): - if child is not None: - child_state, child_dict = instance_state(child),\ - instance_dict(child) - child_impl = child_state.manager[key].impl - if initiator.impl is not child_impl or \ - initiator.op not in (OP_REMOVE, OP_REPLACE): - child_impl.pop( - child_state, - child_dict, - state.obj(), - initiator, - passive=PASSIVE_NO_FETCH) - - if uselist: - event.listen(attribute, "append", - emit_backref_from_collection_append_event, - retval=True, raw=True) - else: - event.listen(attribute, "set", - emit_backref_from_scalar_set_event, - retval=True, raw=True) - # TODO: need coverage in test/orm/ of remove event - event.listen(attribute, "remove", - emit_backref_from_collection_remove_event, - retval=True, raw=True) - -_NO_HISTORY = util.symbol('NO_HISTORY') -_NO_STATE_SYMBOLS = frozenset([ - id(PASSIVE_NO_RESULT), - id(NO_VALUE), - id(NEVER_SET)]) - -History = util.namedtuple("History", [ - "added", "unchanged", "deleted" - ]) - - -class History(History): - """A 3-tuple of added, unchanged and deleted values, - representing the changes which have occurred on an instrumented - attribute. - - The easiest way to get a :class:`.History` object for a particular - attribute on an object is to use the :func:`.inspect` function:: - - from sqlalchemy import inspect - - hist = inspect(myobject).attrs.myattribute.history - - Each tuple member is an iterable sequence: - - * ``added`` - the collection of items added to the attribute (the first - tuple element). - - * ``unchanged`` - the collection of items that have not changed on the - attribute (the second tuple element). - - * ``deleted`` - the collection of items that have been removed from the - attribute (the third tuple element). - - """ - - def __bool__(self): - return self != HISTORY_BLANK - __nonzero__ = __bool__ - - def empty(self): - """Return True if this :class:`.History` has no changes - and no existing, unchanged state. - - """ - - return not bool( - (self.added or self.deleted) - or self.unchanged and self.unchanged != [None] - ) - - def sum(self): - """Return a collection of added + unchanged + deleted.""" - - return (self.added or []) +\ - (self.unchanged or []) +\ - (self.deleted or []) - - def non_deleted(self): - """Return a collection of added + unchanged.""" - - return (self.added or []) +\ - (self.unchanged or []) - - def non_added(self): - """Return a collection of unchanged + deleted.""" - - return (self.unchanged or []) +\ - (self.deleted or []) - - def has_changes(self): - """Return True if this :class:`.History` has changes.""" - - return bool(self.added or self.deleted) - - def as_state(self): - return History( - [(c is not None) - and instance_state(c) or None - for c in self.added], - [(c is not None) - and instance_state(c) or None - for c in self.unchanged], - [(c is not None) - and instance_state(c) or None - for c in self.deleted], - ) - - @classmethod - def from_scalar_attribute(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, _NO_HISTORY) - - if original is _NO_HISTORY: - if current is NEVER_SET: - return cls((), (), ()) - else: - return cls((), [current], ()) - # don't let ClauseElement expressions here trip things up - elif attribute.is_equal(current, original) is True: - return cls((), [current], ()) - else: - # current convention on native scalars is to not - # include information - # about missing previous value in "deleted", but - # we do include None, which helps in some primary - # key situations - if id(original) in _NO_STATE_SYMBOLS: - deleted = () - else: - deleted = [original] - if current is NEVER_SET: - return cls((), (), deleted) - else: - return cls([current], (), deleted) - - @classmethod - def from_object_attribute(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, _NO_HISTORY) - - if original is _NO_HISTORY: - if current is NO_VALUE or current is NEVER_SET: - return cls((), (), ()) - else: - return cls((), [current], ()) - elif current is original: - return cls((), [current], ()) - else: - # current convention on related objects is to not - # include information - # about missing previous value in "deleted", and - # to also not include None - the dependency.py rules - # ignore the None in any case. - if id(original) in _NO_STATE_SYMBOLS or original is None: - deleted = () - else: - deleted = [original] - if current is NO_VALUE or current is NEVER_SET: - return cls((), (), deleted) - else: - return cls([current], (), deleted) - - @classmethod - def from_collection(cls, attribute, state, current): - original = state.committed_state.get(attribute.key, _NO_HISTORY) - - if current is NO_VALUE or current is NEVER_SET: - return cls((), (), ()) - - current = getattr(current, '_sa_adapter') - if original in (NO_VALUE, NEVER_SET): - return cls(list(current), (), ()) - elif original is _NO_HISTORY: - return cls((), list(current), ()) - else: - - current_states = [((c is not None) and instance_state(c) - or None, c) - for c in current - ] - original_states = [((c is not None) and instance_state(c) - or None, c) - for c in original - ] - - current_set = dict(current_states) - original_set = dict(original_states) - - return cls( - [o for s, o in current_states if s not in original_set], - [o for s, o in current_states if s in original_set], - [o for s, o in original_states if s not in current_set] - ) - -HISTORY_BLANK = History(None, None, None) - - -def get_history(obj, key, passive=PASSIVE_OFF): - """Return a :class:`.History` record for the given object - and attribute key. - - :param obj: an object whose class is instrumented by the - attributes package. - - :param key: string attribute name. - - :param passive: indicates loading behavior for the attribute - if the value is not already present. This is a - bitflag attribute, which defaults to the symbol - :attr:`.PASSIVE_OFF` indicating all necessary SQL - should be emitted. - - """ - if passive is True: - util.warn_deprecated("Passing True for 'passive' is deprecated. " - "Use attributes.PASSIVE_NO_INITIALIZE") - passive = PASSIVE_NO_INITIALIZE - elif passive is False: - util.warn_deprecated("Passing False for 'passive' is " - "deprecated. Use attributes.PASSIVE_OFF") - passive = PASSIVE_OFF - - return get_state_history(instance_state(obj), key, passive) - - -def get_state_history(state, key, passive=PASSIVE_OFF): - return state.get_history(key, passive) - - -def has_parent(cls, obj, key, optimistic=False): - """TODO""" - manager = manager_of_class(cls) - state = instance_state(obj) - return manager.has_parent(state, key, optimistic) - - -def register_attribute(class_, key, **kw): - comparator = kw.pop('comparator', None) - parententity = kw.pop('parententity', None) - doc = kw.pop('doc', None) - desc = register_descriptor(class_, key, - comparator, parententity, doc=doc) - register_attribute_impl(class_, key, **kw) - return desc - - -def register_attribute_impl(class_, key, - uselist=False, callable_=None, - useobject=False, - impl_class=None, backref=None, **kw): - - manager = manager_of_class(class_) - if uselist: - factory = kw.pop('typecallable', None) - typecallable = manager.instrument_collection_class( - key, factory or list) - else: - typecallable = kw.pop('typecallable', None) - - dispatch = manager[key].dispatch - - if impl_class: - impl = impl_class(class_, key, typecallable, dispatch, **kw) - elif uselist: - impl = CollectionAttributeImpl(class_, key, callable_, dispatch, - typecallable=typecallable, **kw) - elif useobject: - impl = ScalarObjectAttributeImpl(class_, key, callable_, - dispatch, **kw) - else: - impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) - - manager[key].impl = impl - - if backref: - backref_listeners(manager[key], backref, uselist) - - manager.post_configure_attribute(key) - return manager[key] - - -def register_descriptor(class_, key, comparator=None, - parententity=None, doc=None): - manager = manager_of_class(class_) - - descriptor = InstrumentedAttribute(class_, key, comparator=comparator, - parententity=parententity) - - descriptor.__doc__ = doc - - manager.instrument_attribute(key, descriptor) - return descriptor - - -def unregister_attribute(class_, key): - manager_of_class(class_).uninstrument_attribute(key) - - -def init_collection(obj, key): - """Initialize a collection attribute and return the collection adapter. - - This function is used to provide direct access to collection internals - for a previously unloaded attribute. e.g.:: - - collection_adapter = init_collection(someobject, 'elements') - for elem in values: - collection_adapter.append_without_event(elem) - - For an easier way to do the above, see - :func:`~sqlalchemy.orm.attributes.set_committed_value`. - - obj is an instrumented object instance. An InstanceState - is accepted directly for backwards compatibility but - this usage is deprecated. - - """ - state = instance_state(obj) - dict_ = state.dict - return init_state_collection(state, dict_, key) - - -def init_state_collection(state, dict_, key): - """Initialize a collection attribute and return the collection adapter.""" - - attr = state.manager[key].impl - user_data = attr.initialize(state, dict_) - return attr.get_collection(state, dict_, user_data) - - -def set_committed_value(instance, key, value): - """Set the value of an attribute with no history events. - - Cancels any previous history present. The value should be - a scalar value for scalar-holding attributes, or - an iterable for any collection-holding attribute. - - This is the same underlying method used when a lazy loader - fires off and loads additional data from the database. - In particular, this method can be used by application code - which has loaded additional attributes or collections through - separate queries, which can then be attached to an instance - as though it were part of its original loaded state. - - """ - state, dict_ = instance_state(instance), instance_dict(instance) - state.manager[key].impl.set_committed_value(state, dict_, value) - - -def set_attribute(instance, key, value): - """Set the value of an attribute, firing history events. - - This function may be used regardless of instrumentation - applied directly to the class, i.e. no descriptors are required. - Custom attribute management schemes will need to make usage - of this method to establish attribute state as understood - by SQLAlchemy. - - """ - state, dict_ = instance_state(instance), instance_dict(instance) - state.manager[key].impl.set(state, dict_, value, None) - - -def get_attribute(instance, key): - """Get the value of an attribute, firing any callables required. - - This function may be used regardless of instrumentation - applied directly to the class, i.e. no descriptors are required. - Custom attribute management schemes will need to make usage - of this method to make usage of attribute state as understood - by SQLAlchemy. - - """ - state, dict_ = instance_state(instance), instance_dict(instance) - return state.manager[key].impl.get(state, dict_) - - -def del_attribute(instance, key): - """Delete the value of an attribute, firing history events. - - This function may be used regardless of instrumentation - applied directly to the class, i.e. no descriptors are required. - Custom attribute management schemes will need to make usage - of this method to establish attribute state as understood - by SQLAlchemy. - - """ - state, dict_ = instance_state(instance), instance_dict(instance) - state.manager[key].impl.delete(state, dict_) - - -def flag_modified(instance, key): - """Mark an attribute on an instance as 'modified'. - - This sets the 'modified' flag on the instance and - establishes an unconditional change event for the given attribute. - - """ - state, dict_ = instance_state(instance), instance_dict(instance) - impl = state.manager[key].impl - state._modified_event(dict_, impl, NO_VALUE) diff --git a/libs/sqlalchemy/orm/base.py b/libs/sqlalchemy/orm/base.py deleted file mode 100644 index e3b75eda..00000000 --- a/libs/sqlalchemy/orm/base.py +++ /dev/null @@ -1,453 +0,0 @@ -# orm/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Constants and rudimental functions used throughout the ORM. - -""" - -from .. import util, inspection, exc as sa_exc -from ..sql import expression -from . import exc -import operator - -PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT', -"""Symbol returned by a loader callable or other attribute/history -retrieval operation when a value could not be determined, based -on loader callable flags. -""" -) - -ATTR_WAS_SET = util.symbol('ATTR_WAS_SET', -"""Symbol returned by a loader callable to indicate the -retrieved value, or values, were assigned to their attributes -on the target object. -""") - -ATTR_EMPTY = util.symbol('ATTR_EMPTY', -"""Symbol used internally to indicate an attribute had no callable. -""") - -NO_VALUE = util.symbol('NO_VALUE', -"""Symbol which may be placed as the 'previous' value of an attribute, -indicating no value was loaded for an attribute when it was modified, -and flags indicated we were not to load it. -""" -) - -NEVER_SET = util.symbol('NEVER_SET', -"""Symbol which may be placed as the 'previous' value of an attribute -indicating that the attribute had not been assigned to previously. -""" -) - -NO_CHANGE = util.symbol("NO_CHANGE", -"""No callables or SQL should be emitted on attribute access -and no state should change""", canonical=0 -) - -CALLABLES_OK = util.symbol("CALLABLES_OK", -"""Loader callables can be fired off if a value -is not present.""", canonical=1 -) - -SQL_OK = util.symbol("SQL_OK", -"""Loader callables can emit SQL at least on scalar value -attributes.""", canonical=2) - -RELATED_OBJECT_OK = util.symbol("RELATED_OBJECT_OK", -"""callables can use SQL to load related objects as well -as scalar value attributes. -""", canonical=4 -) - -INIT_OK = util.symbol("INIT_OK", -"""Attributes should be initialized with a blank -value (None or an empty collection) upon get, if no other -value can be obtained. -""", canonical=8 -) - -NON_PERSISTENT_OK = util.symbol("NON_PERSISTENT_OK", -"""callables can be emitted if the parent is not persistent.""", -canonical=16 -) - -LOAD_AGAINST_COMMITTED = util.symbol("LOAD_AGAINST_COMMITTED", -"""callables should use committed values as primary/foreign keys during a load -""", canonical=32 -) - -# pre-packaged sets of flags used as inputs -PASSIVE_OFF = util.symbol("PASSIVE_OFF", - "Callables can be emitted in all cases.", - canonical=(RELATED_OBJECT_OK | NON_PERSISTENT_OK | - INIT_OK | CALLABLES_OK | SQL_OK) -) -PASSIVE_RETURN_NEVER_SET = util.symbol("PASSIVE_RETURN_NEVER_SET", - """PASSIVE_OFF ^ INIT_OK""", - canonical=PASSIVE_OFF ^ INIT_OK -) -PASSIVE_NO_INITIALIZE = util.symbol("PASSIVE_NO_INITIALIZE", - "PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK", - canonical=PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK -) -PASSIVE_NO_FETCH = util.symbol("PASSIVE_NO_FETCH", - "PASSIVE_OFF ^ SQL_OK", - canonical=PASSIVE_OFF ^ SQL_OK -) -PASSIVE_NO_FETCH_RELATED = util.symbol("PASSIVE_NO_FETCH_RELATED", - "PASSIVE_OFF ^ RELATED_OBJECT_OK", - canonical=PASSIVE_OFF ^ RELATED_OBJECT_OK -) -PASSIVE_ONLY_PERSISTENT = util.symbol("PASSIVE_ONLY_PERSISTENT", - "PASSIVE_OFF ^ NON_PERSISTENT_OK", - canonical=PASSIVE_OFF ^ NON_PERSISTENT_OK -) - -DEFAULT_MANAGER_ATTR = '_sa_class_manager' -DEFAULT_STATE_ATTR = '_sa_instance_state' -_INSTRUMENTOR = ('mapper', 'instrumentor') - -EXT_CONTINUE = util.symbol('EXT_CONTINUE') -EXT_STOP = util.symbol('EXT_STOP') - -ONETOMANY = util.symbol('ONETOMANY', -"""Indicates the one-to-many direction for a :func:`.relationship`. - -This symbol is typically used by the internals but may be exposed within -certain API features. - -""") - -MANYTOONE = util.symbol('MANYTOONE', -"""Indicates the many-to-one direction for a :func:`.relationship`. - -This symbol is typically used by the internals but may be exposed within -certain API features. - -""") - -MANYTOMANY = util.symbol('MANYTOMANY', -"""Indicates the many-to-many direction for a :func:`.relationship`. - -This symbol is typically used by the internals but may be exposed within -certain API features. - -""") - -NOT_EXTENSION = util.symbol('NOT_EXTENSION', -"""Symbol indicating an :class:`_InspectionAttr` that's - not part of sqlalchemy.ext. - - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. - -""") - -_none_set = frozenset([None]) - - -def _generative(*assertions): - """Mark a method as generative, e.g. method-chained.""" - - @util.decorator - def generate(fn, *args, **kw): - self = args[0]._clone() - for assertion in assertions: - assertion(self, fn.__name__) - fn(self, *args[1:], **kw) - return self - return generate - - -# these can be replaced by sqlalchemy.ext.instrumentation -# if augmented class instrumentation is enabled. -def manager_of_class(cls): - return cls.__dict__.get(DEFAULT_MANAGER_ATTR, None) - -instance_state = operator.attrgetter(DEFAULT_STATE_ATTR) - -instance_dict = operator.attrgetter('__dict__') - -def instance_str(instance): - """Return a string describing an instance.""" - - return state_str(instance_state(instance)) - -def state_str(state): - """Return a string describing an instance via its InstanceState.""" - - if state is None: - return "None" - else: - return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj())) - -def state_class_str(state): - """Return a string describing an instance's class via its InstanceState.""" - - if state is None: - return "None" - else: - return '<%s>' % (state.class_.__name__, ) - - -def attribute_str(instance, attribute): - return instance_str(instance) + "." + attribute - - -def state_attribute_str(state, attribute): - return state_str(state) + "." + attribute - -def object_mapper(instance): - """Given an object, return the primary Mapper associated with the object - instance. - - Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` - if no mapping is configured. - - This function is available via the inspection system as:: - - inspect(instance).mapper - - Using the inspection system will raise - :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is - not part of a mapping. - - """ - return object_state(instance).mapper - - -def object_state(instance): - """Given an object, return the :class:`.InstanceState` - associated with the object. - - Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` - if no mapping is configured. - - Equivalent functionality is available via the :func:`.inspect` - function as:: - - inspect(instance) - - Using the inspection system will raise - :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is - not part of a mapping. - - """ - state = _inspect_mapped_object(instance) - if state is None: - raise exc.UnmappedInstanceError(instance) - else: - return state - - -@inspection._inspects(object) -def _inspect_mapped_object(instance): - try: - return instance_state(instance) - # TODO: whats the py-2/3 syntax to catch two - # different kinds of exceptions at once ? - except exc.UnmappedClassError: - return None - except exc.NO_STATE: - return None - - - -def _class_to_mapper(class_or_mapper): - insp = inspection.inspect(class_or_mapper, False) - if insp is not None: - return insp.mapper - else: - raise exc.UnmappedClassError(class_or_mapper) - - -def _mapper_or_none(entity): - """Return the :class:`.Mapper` for the given class or None if the - class is not mapped.""" - - insp = inspection.inspect(entity, False) - if insp is not None: - return insp.mapper - else: - return None - - -def _is_mapped_class(entity): - """Return True if the given object is a mapped class, - :class:`.Mapper`, or :class:`.AliasedClass`.""" - - insp = inspection.inspect(entity, False) - return insp is not None and \ - hasattr(insp, "mapper") and \ - ( - insp.is_mapper - or insp.is_aliased_class - ) - -def _attr_as_key(attr): - if hasattr(attr, 'key'): - return attr.key - else: - return expression._column_as_key(attr) - - - -def _orm_columns(entity): - insp = inspection.inspect(entity, False) - if hasattr(insp, 'selectable'): - return [c for c in insp.selectable.c] - else: - return [entity] - - - -def _is_aliased_class(entity): - insp = inspection.inspect(entity, False) - return insp is not None and \ - getattr(insp, "is_aliased_class", False) - - -def _entity_descriptor(entity, key): - """Return a class attribute given an entity and string name. - - May return :class:`.InstrumentedAttribute` or user-defined - attribute. - - """ - insp = inspection.inspect(entity) - if insp.is_selectable: - description = entity - entity = insp.c - elif insp.is_aliased_class: - entity = insp.entity - description = entity - elif hasattr(insp, "mapper"): - description = entity = insp.mapper.class_ - else: - description = entity - - try: - return getattr(entity, key) - except AttributeError: - raise sa_exc.InvalidRequestError( - "Entity '%s' has no property '%s'" % - (description, key) - ) - -_state_mapper = util.dottedgetter('manager.mapper') - -@inspection._inspects(type) -def _inspect_mapped_class(class_, configure=False): - try: - class_manager = manager_of_class(class_) - if not class_manager.is_mapped: - return None - mapper = class_manager.mapper - if configure and mapper._new_mappers: - mapper._configure_all() - return mapper - - except exc.NO_STATE: - return None - -def class_mapper(class_, configure=True): - """Given a class, return the primary :class:`.Mapper` associated - with the key. - - Raises :exc:`.UnmappedClassError` if no mapping is configured - on the given class, or :exc:`.ArgumentError` if a non-class - object is passed. - - Equivalent functionality is available via the :func:`.inspect` - function as:: - - inspect(some_mapped_class) - - Using the inspection system will raise - :class:`sqlalchemy.exc.NoInspectionAvailable` if the class is not mapped. - - """ - mapper = _inspect_mapped_class(class_, configure=configure) - if mapper is None: - if not isinstance(class_, type): - raise sa_exc.ArgumentError( - "Class object expected, got '%r'." % class_) - raise exc.UnmappedClassError(class_) - else: - return mapper - - -class _InspectionAttr(object): - """A base class applied to all ORM objects that can be returned - by the :func:`.inspect` function. - - The attributes defined here allow the usage of simple boolean - checks to test basic facts about the object returned. - - While the boolean checks here are basically the same as using - the Python isinstance() function, the flags here can be used without - the need to import all of these classes, and also such that - the SQLAlchemy class system can change while leaving the flags - here intact for forwards-compatibility. - - """ - - is_selectable = False - """Return True if this object is an instance of :class:`.Selectable`.""" - - is_aliased_class = False - """True if this object is an instance of :class:`.AliasedClass`.""" - - is_instance = False - """True if this object is an instance of :class:`.InstanceState`.""" - - is_mapper = False - """True if this object is an instance of :class:`.Mapper`.""" - - is_property = False - """True if this object is an instance of :class:`.MapperProperty`.""" - - is_attribute = False - """True if this object is a Python :term:`descriptor`. - - This can refer to one of many types. Usually a - :class:`.QueryableAttribute` which handles attributes events on behalf - of a :class:`.MapperProperty`. But can also be an extension type - such as :class:`.AssociationProxy` or :class:`.hybrid_property`. - The :attr:`._InspectionAttr.extension_type` will refer to a constant - identifying the specific subtype. - - .. seealso:: - - :attr:`.Mapper.all_orm_descriptors` - - """ - - is_clause_element = False - """True if this object is an instance of :class:`.ClauseElement`.""" - - extension_type = NOT_EXTENSION - """The extension type, if any. - Defaults to :data:`.interfaces.NOT_EXTENSION` - - .. versionadded:: 0.8.0 - - .. seealso:: - - :data:`.HYBRID_METHOD` - - :data:`.HYBRID_PROPERTY` - - :data:`.ASSOCIATION_PROXY` - - """ - -class _MappedAttribute(object): - """Mixin for attributes which should be replaced by mapper-assigned - attributes. - - """ diff --git a/libs/sqlalchemy/orm/collections.py b/libs/sqlalchemy/orm/collections.py deleted file mode 100644 index 87e351b6..00000000 --- a/libs/sqlalchemy/orm/collections.py +++ /dev/null @@ -1,1550 +0,0 @@ -# orm/collections.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Support for collections of mapped entities. - -The collections package supplies the machinery used to inform the ORM of -collection membership changes. An instrumentation via decoration approach is -used, allowing arbitrary types (including built-ins) to be used as entity -collections without requiring inheritance from a base class. - -Instrumentation decoration relays membership change events to the -:class:`.CollectionAttributeImpl` that is currently managing the collection. -The decorators observe function call arguments and return values, tracking -entities entering or leaving the collection. Two decorator approaches are -provided. One is a bundle of generic decorators that map function arguments -and return values to events:: - - from sqlalchemy.orm.collections import collection - class MyClass(object): - # ... - - @collection.adds(1) - def store(self, item): - self.data.append(item) - - @collection.removes_return() - def pop(self): - return self.data.pop() - - -The second approach is a bundle of targeted decorators that wrap appropriate -append and remove notifiers around the mutation methods present in the -standard Python ``list``, ``set`` and ``dict`` interfaces. These could be -specified in terms of generic decorator recipes, but are instead hand-tooled -for increased efficiency. The targeted decorators occasionally implement -adapter-like behavior, such as mapping bulk-set methods (``extend``, -``update``, ``__setslice__``, etc.) into the series of atomic mutation events -that the ORM requires. - -The targeted decorators are used internally for automatic instrumentation of -entity collection classes. Every collection class goes through a -transformation process roughly like so: - -1. If the class is a built-in, substitute a trivial sub-class -2. Is this class already instrumented? -3. Add in generic decorators -4. Sniff out the collection interface through duck-typing -5. Add targeted decoration to any undecorated interface method - -This process modifies the class at runtime, decorating methods and adding some -bookkeeping properties. This isn't possible (or desirable) for built-in -classes like ``list``, so trivial sub-classes are substituted to hold -decoration:: - - class InstrumentedList(list): - pass - -Collection classes can be specified in ``relationship(collection_class=)`` as -types or a function that returns an instance. Collection classes are -inspected and instrumented during the mapper compilation phase. The -collection_class callable will be executed once to produce a specimen -instance, and the type of that specimen will be instrumented. Functions that -return built-in types like ``lists`` will be adapted to produce instrumented -instances. - -When extending a known type like ``list``, additional decorations are not -generally not needed. Odds are, the extension method will delegate to a -method that's already instrumented. For example:: - - class QueueIsh(list): - def push(self, item): - self.append(item) - def shift(self): - return self.pop(0) - -There's no need to decorate these methods. ``append`` and ``pop`` are already -instrumented as part of the ``list`` interface. Decorating them would fire -duplicate events, which should be avoided. - -The targeted decoration tries not to rely on other methods in the underlying -collection class, but some are unavoidable. Many depend on 'read' methods -being present to properly instrument a 'write', for example, ``__setitem__`` -needs ``__getitem__``. "Bulk" methods like ``update`` and ``extend`` may also -reimplemented in terms of atomic appends and removes, so the ``extend`` -decoration will actually perform many ``append`` operations and not call the -underlying method at all. - -Tight control over bulk operation and the firing of events is also possible by -implementing the instrumentation internally in your methods. The basic -instrumentation package works under the general assumption that collection -mutation will not raise unusual exceptions. If you want to closely -orchestrate append and remove events with exception management, internal -instrumentation may be the answer. Within your method, -``collection_adapter(self)`` will retrieve an object that you can use for -explicit control over triggering append and remove events. - -The owning object and :class:`.CollectionAttributeImpl` are also reachable -through the adapter, allowing for some very sophisticated behavior. - -""" - -import inspect -import operator -import weakref - -from ..sql import expression -from .. import util, exc as sa_exc -from . import base - - -__all__ = ['collection', 'collection_adapter', - 'mapped_collection', 'column_mapped_collection', - 'attribute_mapped_collection'] - -__instrumentation_mutex = util.threading.Lock() - - -class _PlainColumnGetter(object): - """Plain column getter, stores collection of Column objects - directly. - - Serializes to a :class:`._SerializableColumnGetterV2` - which has more expensive __call__() performance - and some rare caveats. - - """ - def __init__(self, cols): - self.cols = cols - self.composite = len(cols) > 1 - - def __reduce__(self): - return _SerializableColumnGetterV2._reduce_from_cols(self.cols) - - def _cols(self, mapper): - return self.cols - - def __call__(self, value): - state = base.instance_state(value) - m = base._state_mapper(state) - - key = [ - m._get_state_attr_by_column(state, state.dict, col) - for col in self._cols(m) - ] - - if self.composite: - return tuple(key) - else: - return key[0] - - -class _SerializableColumnGetter(object): - """Column-based getter used in version 0.7.6 only. - - Remains here for pickle compatibility with 0.7.6. - - """ - def __init__(self, colkeys): - self.colkeys = colkeys - self.composite = len(colkeys) > 1 - - def __reduce__(self): - return _SerializableColumnGetter, (self.colkeys,) - - def __call__(self, value): - state = base.instance_state(value) - m = base._state_mapper(state) - key = [m._get_state_attr_by_column( - state, state.dict, - m.mapped_table.columns[k]) - for k in self.colkeys] - if self.composite: - return tuple(key) - else: - return key[0] - - -class _SerializableColumnGetterV2(_PlainColumnGetter): - """Updated serializable getter which deals with - multi-table mapped classes. - - Two extremely unusual cases are not supported. - Mappings which have tables across multiple metadata - objects, or which are mapped to non-Table selectables - linked across inheriting mappers may fail to function - here. - - """ - - def __init__(self, colkeys): - self.colkeys = colkeys - self.composite = len(colkeys) > 1 - - def __reduce__(self): - return self.__class__, (self.colkeys,) - - @classmethod - def _reduce_from_cols(cls, cols): - def _table_key(c): - if not isinstance(c.table, expression.TableClause): - return None - else: - return c.table.key - colkeys = [(c.key, _table_key(c)) for c in cols] - return _SerializableColumnGetterV2, (colkeys,) - - def _cols(self, mapper): - cols = [] - metadata = getattr(mapper.local_table, 'metadata', None) - for (ckey, tkey) in self.colkeys: - if tkey is None or \ - metadata is None or \ - tkey not in metadata: - cols.append(mapper.local_table.c[ckey]) - else: - cols.append(metadata.tables[tkey].c[ckey]) - return cols - - -def column_mapped_collection(mapping_spec): - """A dictionary-based collection type with column-based keying. - - Returns a :class:`.MappedCollection` factory with a keying function - generated from mapping_spec, which may be a Column or a sequence - of Columns. - - The key value must be immutable for the lifetime of the object. You - can not, for example, map on foreign key values if those key values will - change during the session, i.e. from None to a database-assigned integer - after a session flush. - - """ - cols = [expression._only_column_elements(q, "mapping_spec") - for q in util.to_list(mapping_spec) - ] - keyfunc = _PlainColumnGetter(cols) - return lambda: MappedCollection(keyfunc) - - -class _SerializableAttrGetter(object): - def __init__(self, name): - self.name = name - self.getter = operator.attrgetter(name) - - def __call__(self, target): - return self.getter(target) - - def __reduce__(self): - return _SerializableAttrGetter, (self.name, ) - - -def attribute_mapped_collection(attr_name): - """A dictionary-based collection type with attribute-based keying. - - Returns a :class:`.MappedCollection` factory with a keying based on the - 'attr_name' attribute of entities in the collection, where ``attr_name`` - is the string name of the attribute. - - The key value must be immutable for the lifetime of the object. You - can not, for example, map on foreign key values if those key values will - change during the session, i.e. from None to a database-assigned integer - after a session flush. - - """ - getter = _SerializableAttrGetter(attr_name) - return lambda: MappedCollection(getter) - - -def mapped_collection(keyfunc): - """A dictionary-based collection type with arbitrary keying. - - Returns a :class:`.MappedCollection` factory with a keying function - generated from keyfunc, a callable that takes an entity and returns a - key value. - - The key value must be immutable for the lifetime of the object. You - can not, for example, map on foreign key values if those key values will - change during the session, i.e. from None to a database-assigned integer - after a session flush. - - """ - return lambda: MappedCollection(keyfunc) - - -class collection(object): - """Decorators for entity collection classes. - - The decorators fall into two groups: annotations and interception recipes. - - The annotating decorators (appender, remover, iterator, linker, converter, - internally_instrumented) indicate the method's purpose and take no - arguments. They are not written with parens:: - - @collection.appender - def append(self, append): ... - - The recipe decorators all require parens, even those that take no - arguments:: - - @collection.adds('entity') - def insert(self, position, entity): ... - - @collection.removes_return() - def popitem(self): ... - - """ - # Bundled as a class solely for ease of use: packaging, doc strings, - # importability. - - @staticmethod - def appender(fn): - """Tag the method as the collection appender. - - The appender method is called with one positional argument: the value - to append. The method will be automatically decorated with 'adds(1)' - if not already decorated:: - - @collection.appender - def add(self, append): ... - - # or, equivalently - @collection.appender - @collection.adds(1) - def add(self, append): ... - - # for mapping type, an 'append' may kick out a previous value - # that occupies that slot. consider d['a'] = 'foo'- any previous - # value in d['a'] is discarded. - @collection.appender - @collection.replaces(1) - def add(self, entity): - key = some_key_func(entity) - previous = None - if key in self: - previous = self[key] - self[key] = entity - return previous - - If the value to append is not allowed in the collection, you may - raise an exception. Something to remember is that the appender - will be called for each object mapped by a database query. If the - database contains rows that violate your collection semantics, you - will need to get creative to fix the problem, as access via the - collection will not work. - - If the appender method is internally instrumented, you must also - receive the keyword argument '_sa_initiator' and ensure its - promulgation to collection events. - - """ - fn._sa_instrument_role = 'appender' - return fn - - @staticmethod - def remover(fn): - """Tag the method as the collection remover. - - The remover method is called with one positional argument: the value - to remove. The method will be automatically decorated with - :meth:`removes_return` if not already decorated:: - - @collection.remover - def zap(self, entity): ... - - # or, equivalently - @collection.remover - @collection.removes_return() - def zap(self, ): ... - - If the value to remove is not present in the collection, you may - raise an exception or return None to ignore the error. - - If the remove method is internally instrumented, you must also - receive the keyword argument '_sa_initiator' and ensure its - promulgation to collection events. - - """ - fn._sa_instrument_role = 'remover' - return fn - - @staticmethod - def iterator(fn): - """Tag the method as the collection remover. - - The iterator method is called with no arguments. It is expected to - return an iterator over all collection members:: - - @collection.iterator - def __iter__(self): ... - - """ - fn._sa_instrument_role = 'iterator' - return fn - - @staticmethod - def internally_instrumented(fn): - """Tag the method as instrumented. - - This tag will prevent any decoration from being applied to the - method. Use this if you are orchestrating your own calls to - :func:`.collection_adapter` in one of the basic SQLAlchemy - interface methods, or to prevent an automatic ABC method - decoration from wrapping your implementation:: - - # normally an 'extend' method on a list-like class would be - # automatically intercepted and re-implemented in terms of - # SQLAlchemy events and append(). your implementation will - # never be called, unless: - @collection.internally_instrumented - def extend(self, items): ... - - """ - fn._sa_instrumented = True - return fn - - @staticmethod - def linker(fn): - """Tag the method as a "linked to attribute" event handler. - - This optional event handler will be called when the collection class - is linked to or unlinked from the InstrumentedAttribute. It is - invoked immediately after the '_sa_adapter' property is set on - the instance. A single argument is passed: the collection adapter - that has been linked, or None if unlinking. - - """ - fn._sa_instrument_role = 'linker' - return fn - - link = linker - """deprecated; synonym for :meth:`.collection.linker`.""" - - @staticmethod - def converter(fn): - """Tag the method as the collection converter. - - This optional method will be called when a collection is being - replaced entirely, as in:: - - myobj.acollection = [newvalue1, newvalue2] - - The converter method will receive the object being assigned and should - return an iterable of values suitable for use by the ``appender`` - method. A converter must not assign values or mutate the collection, - it's sole job is to adapt the value the user provides into an iterable - of values for the ORM's use. - - The default converter implementation will use duck-typing to do the - conversion. A dict-like collection will be convert into an iterable - of dictionary values, and other types will simply be iterated:: - - @collection.converter - def convert(self, other): ... - - If the duck-typing of the object does not match the type of this - collection, a TypeError is raised. - - Supply an implementation of this method if you want to expand the - range of possible types that can be assigned in bulk or perform - validation on the values about to be assigned. - - """ - fn._sa_instrument_role = 'converter' - return fn - - @staticmethod - def adds(arg): - """Mark the method as adding an entity to the collection. - - Adds "add to collection" handling to the method. The decorator - argument indicates which method argument holds the SQLAlchemy-relevant - value. Arguments can be specified positionally (i.e. integer) or by - name:: - - @collection.adds(1) - def push(self, item): ... - - @collection.adds('entity') - def do_stuff(self, thing, entity=None): ... - - """ - def decorator(fn): - fn._sa_instrument_before = ('fire_append_event', arg) - return fn - return decorator - - @staticmethod - def replaces(arg): - """Mark the method as replacing an entity in the collection. - - Adds "add to collection" and "remove from collection" handling to - the method. The decorator argument indicates which method argument - holds the SQLAlchemy-relevant value to be added, and return value, if - any will be considered the value to remove. - - Arguments can be specified positionally (i.e. integer) or by name:: - - @collection.replaces(2) - def __setitem__(self, index, item): ... - - """ - def decorator(fn): - fn._sa_instrument_before = ('fire_append_event', arg) - fn._sa_instrument_after = 'fire_remove_event' - return fn - return decorator - - @staticmethod - def removes(arg): - """Mark the method as removing an entity in the collection. - - Adds "remove from collection" handling to the method. The decorator - argument indicates which method argument holds the SQLAlchemy-relevant - value to be removed. Arguments can be specified positionally (i.e. - integer) or by name:: - - @collection.removes(1) - def zap(self, item): ... - - For methods where the value to remove is not known at call-time, use - collection.removes_return. - - """ - def decorator(fn): - fn._sa_instrument_before = ('fire_remove_event', arg) - return fn - return decorator - - @staticmethod - def removes_return(): - """Mark the method as removing an entity in the collection. - - Adds "remove from collection" handling to the method. The return value - of the method, if any, is considered the value to remove. The method - arguments are not inspected:: - - @collection.removes_return() - def pop(self): ... - - For methods where the value to remove is known at call-time, use - collection.remove. - - """ - def decorator(fn): - fn._sa_instrument_after = 'fire_remove_event' - return fn - return decorator - - -collection_adapter = operator.attrgetter('_sa_adapter') -"""Fetch the :class:`.CollectionAdapter` for a collection.""" - - -class CollectionAdapter(object): - """Bridges between the ORM and arbitrary Python collections. - - Proxies base-level collection operations (append, remove, iterate) - to the underlying Python collection, and emits add/remove events for - entities entering or leaving the collection. - - The ORM uses :class:`.CollectionAdapter` exclusively for interaction with - entity collections. - - - """ - invalidated = False - - def __init__(self, attr, owner_state, data): - self._key = attr.key - self._data = weakref.ref(data) - self.owner_state = owner_state - self.link_to_self(data) - - def _warn_invalidated(self): - util.warn("This collection has been invalidated.") - - @property - def data(self): - "The entity collection being adapted." - return self._data() - - @util.memoized_property - def attr(self): - return self.owner_state.manager[self._key].impl - - def link_to_self(self, data): - """Link a collection to this adapter""" - - data._sa_adapter = self - if data._sa_linker: - data._sa_linker(self) - - - def unlink(self, data): - """Unlink a collection from any adapter""" - - del data._sa_adapter - if data._sa_linker: - data._sa_linker(None) - - def adapt_like_to_iterable(self, obj): - """Converts collection-compatible objects to an iterable of values. - - Can be passed any type of object, and if the underlying collection - determines that it can be adapted into a stream of values it can - use, returns an iterable of values suitable for append()ing. - - This method may raise TypeError or any other suitable exception - if adaptation fails. - - If a converter implementation is not supplied on the collection, - a default duck-typing-based implementation is used. - - """ - converter = self._data()._sa_converter - if converter is not None: - return converter(obj) - - setting_type = util.duck_type_collection(obj) - receiving_type = util.duck_type_collection(self._data()) - - if obj is None or setting_type != receiving_type: - given = obj is None and 'None' or obj.__class__.__name__ - if receiving_type is None: - wanted = self._data().__class__.__name__ - else: - wanted = receiving_type.__name__ - - raise TypeError( - "Incompatible collection type: %s is not %s-like" % ( - given, wanted)) - - # If the object is an adapted collection, return the (iterable) - # adapter. - if getattr(obj, '_sa_adapter', None) is not None: - return obj._sa_adapter - elif setting_type == dict: - if util.py3k: - return obj.values() - else: - return getattr(obj, 'itervalues', obj.values)() - else: - return iter(obj) - - def append_with_event(self, item, initiator=None): - """Add an entity to the collection, firing mutation events.""" - - self._data()._sa_appender(item, _sa_initiator=initiator) - - def append_without_event(self, item): - """Add or restore an entity to the collection, firing no events.""" - self._data()._sa_appender(item, _sa_initiator=False) - - def append_multiple_without_event(self, items): - """Add or restore an entity to the collection, firing no events.""" - appender = self._data()._sa_appender - for item in items: - appender(item, _sa_initiator=False) - - def remove_with_event(self, item, initiator=None): - """Remove an entity from the collection, firing mutation events.""" - self._data()._sa_remover(item, _sa_initiator=initiator) - - def remove_without_event(self, item): - """Remove an entity from the collection, firing no events.""" - self._data()._sa_remover(item, _sa_initiator=False) - - def clear_with_event(self, initiator=None): - """Empty the collection, firing a mutation event for each entity.""" - - remover = self._data()._sa_remover - for item in list(self): - remover(item, _sa_initiator=initiator) - - def clear_without_event(self): - """Empty the collection, firing no events.""" - - remover = self._data()._sa_remover - for item in list(self): - remover(item, _sa_initiator=False) - - def __iter__(self): - """Iterate over entities in the collection.""" - - return iter(self._data()._sa_iterator()) - - def __len__(self): - """Count entities in the collection.""" - return len(list(self._data()._sa_iterator())) - - def __bool__(self): - return True - - __nonzero__ = __bool__ - - def fire_append_event(self, item, initiator=None): - """Notify that a entity has entered the collection. - - Initiator is a token owned by the InstrumentedAttribute that - initiated the membership mutation, and should be left as None - unless you are passing along an initiator value from a chained - operation. - - """ - if initiator is not False: - if self.invalidated: - self._warn_invalidated() - return self.attr.fire_append_event( - self.owner_state, - self.owner_state.dict, - item, initiator) - else: - return item - - def fire_remove_event(self, item, initiator=None): - """Notify that a entity has been removed from the collection. - - Initiator is the InstrumentedAttribute that initiated the membership - mutation, and should be left as None unless you are passing along - an initiator value from a chained operation. - - """ - if initiator is not False: - if self.invalidated: - self._warn_invalidated() - self.attr.fire_remove_event( - self.owner_state, - self.owner_state.dict, - item, initiator) - - def fire_pre_remove_event(self, initiator=None): - """Notify that an entity is about to be removed from the collection. - - Only called if the entity cannot be removed after calling - fire_remove_event(). - - """ - if self.invalidated: - self._warn_invalidated() - self.attr.fire_pre_remove_event( - self.owner_state, - self.owner_state.dict, - initiator=initiator) - - def __getstate__(self): - return {'key': self._key, - 'owner_state': self.owner_state, - 'data': self.data} - - def __setstate__(self, d): - self._key = d['key'] - self.owner_state = d['owner_state'] - self._data = weakref.ref(d['data']) - - -def bulk_replace(values, existing_adapter, new_adapter): - """Load a new collection, firing events based on prior like membership. - - Appends instances in ``values`` onto the ``new_adapter``. Events will be - fired for any instance not present in the ``existing_adapter``. Any - instances in ``existing_adapter`` not present in ``values`` will have - remove events fired upon them. - - :param values: An iterable of collection member instances - - :param existing_adapter: A :class:`.CollectionAdapter` of - instances to be replaced - - :param new_adapter: An empty :class:`.CollectionAdapter` - to load with ``values`` - - - """ - if not isinstance(values, list): - values = list(values) - - idset = util.IdentitySet - existing_idset = idset(existing_adapter or ()) - constants = existing_idset.intersection(values or ()) - additions = idset(values or ()).difference(constants) - removals = existing_idset.difference(constants) - - for member in values or (): - if member in additions: - new_adapter.append_with_event(member) - elif member in constants: - new_adapter.append_without_event(member) - - if existing_adapter: - for member in removals: - existing_adapter.remove_with_event(member) - - -def prepare_instrumentation(factory): - """Prepare a callable for future use as a collection class factory. - - Given a collection class factory (either a type or no-arg callable), - return another factory that will produce compatible instances when - called. - - This function is responsible for converting collection_class=list - into the run-time behavior of collection_class=InstrumentedList. - - """ - # Convert a builtin to 'Instrumented*' - if factory in __canned_instrumentation: - factory = __canned_instrumentation[factory] - - # Create a specimen - cls = type(factory()) - - # Did factory callable return a builtin? - if cls in __canned_instrumentation: - # Wrap it so that it returns our 'Instrumented*' - factory = __converting_factory(cls, factory) - cls = factory() - - # Instrument the class if needed. - if __instrumentation_mutex.acquire(): - try: - if getattr(cls, '_sa_instrumented', None) != id(cls): - _instrument_class(cls) - finally: - __instrumentation_mutex.release() - - return factory - - -def __converting_factory(specimen_cls, original_factory): - """Return a wrapper that converts a "canned" collection like - set, dict, list into the Instrumented* version. - - """ - - instrumented_cls = __canned_instrumentation[specimen_cls] - - def wrapper(): - collection = original_factory() - return instrumented_cls(collection) - - # often flawed but better than nothing - wrapper.__name__ = "%sWrapper" % original_factory.__name__ - wrapper.__doc__ = original_factory.__doc__ - - return wrapper - -def _instrument_class(cls): - """Modify methods in a class and install instrumentation.""" - - # In the normal call flow, a request for any of the 3 basic collection - # types is transformed into one of our trivial subclasses - # (e.g. InstrumentedList). Catch anything else that sneaks in here... - if cls.__module__ == '__builtin__': - raise sa_exc.ArgumentError( - "Can not instrument a built-in type. Use a " - "subclass, even a trivial one.") - - roles = {} - methods = {} - - # search for _sa_instrument_role-decorated methods in - # method resolution order, assign to roles - for supercls in cls.__mro__: - for name, method in vars(supercls).items(): - if not util.callable(method): - continue - - # note role declarations - if hasattr(method, '_sa_instrument_role'): - role = method._sa_instrument_role - assert role in ('appender', 'remover', 'iterator', - 'linker', 'converter') - roles.setdefault(role, name) - - # transfer instrumentation requests from decorated function - # to the combined queue - before, after = None, None - if hasattr(method, '_sa_instrument_before'): - op, argument = method._sa_instrument_before - assert op in ('fire_append_event', 'fire_remove_event') - before = op, argument - if hasattr(method, '_sa_instrument_after'): - op = method._sa_instrument_after - assert op in ('fire_append_event', 'fire_remove_event') - after = op - if before: - methods[name] = before[0], before[1], after - elif after: - methods[name] = None, None, after - - # see if this class has "canned" roles based on a known - # collection type (dict, set, list). Apply those roles - # as needed to the "roles" dictionary, and also - # prepare "decorator" methods - collection_type = util.duck_type_collection(cls) - if collection_type in __interfaces: - canned_roles, decorators = __interfaces[collection_type] - for role, name in canned_roles.items(): - roles.setdefault(role, name) - - # apply ABC auto-decoration to methods that need it - for method, decorator in decorators.items(): - fn = getattr(cls, method, None) - if (fn and method not in methods and - not hasattr(fn, '_sa_instrumented')): - setattr(cls, method, decorator(fn)) - - # ensure all roles are present, and apply implicit instrumentation if - # needed - if 'appender' not in roles or not hasattr(cls, roles['appender']): - raise sa_exc.ArgumentError( - "Type %s must elect an appender method to be " - "a collection class" % cls.__name__) - elif (roles['appender'] not in methods and - not hasattr(getattr(cls, roles['appender']), '_sa_instrumented')): - methods[roles['appender']] = ('fire_append_event', 1, None) - - if 'remover' not in roles or not hasattr(cls, roles['remover']): - raise sa_exc.ArgumentError( - "Type %s must elect a remover method to be " - "a collection class" % cls.__name__) - elif (roles['remover'] not in methods and - not hasattr(getattr(cls, roles['remover']), '_sa_instrumented')): - methods[roles['remover']] = ('fire_remove_event', 1, None) - - if 'iterator' not in roles or not hasattr(cls, roles['iterator']): - raise sa_exc.ArgumentError( - "Type %s must elect an iterator method to be " - "a collection class" % cls.__name__) - - # apply ad-hoc instrumentation from decorators, class-level defaults - # and implicit role declarations - for method_name, (before, argument, after) in methods.items(): - setattr(cls, method_name, - _instrument_membership_mutator(getattr(cls, method_name), - before, argument, after)) - # intern the role map - for role, method_name in roles.items(): - setattr(cls, '_sa_%s' % role, getattr(cls, method_name)) - - cls._sa_adapter = None - if not hasattr(cls, '_sa_linker'): - cls._sa_linker = None - if not hasattr(cls, '_sa_converter'): - cls._sa_converter = None - cls._sa_instrumented = id(cls) - - -def _instrument_membership_mutator(method, before, argument, after): - """Route method args and/or return value through the collection adapter.""" - # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))' - if before: - fn_args = list(util.flatten_iterator(inspect.getargspec(method)[0])) - if type(argument) is int: - pos_arg = argument - named_arg = len(fn_args) > argument and fn_args[argument] or None - else: - if argument in fn_args: - pos_arg = fn_args.index(argument) - else: - pos_arg = None - named_arg = argument - del fn_args - - def wrapper(*args, **kw): - if before: - if pos_arg is None: - if named_arg not in kw: - raise sa_exc.ArgumentError( - "Missing argument %s" % argument) - value = kw[named_arg] - else: - if len(args) > pos_arg: - value = args[pos_arg] - elif named_arg in kw: - value = kw[named_arg] - else: - raise sa_exc.ArgumentError( - "Missing argument %s" % argument) - - initiator = kw.pop('_sa_initiator', None) - if initiator is False: - executor = None - else: - executor = args[0]._sa_adapter - - if before and executor: - getattr(executor, before)(value, initiator) - - if not after or not executor: - return method(*args, **kw) - else: - res = method(*args, **kw) - if res is not None: - getattr(executor, after)(res, initiator) - return res - - wrapper._sa_instrumented = True - if hasattr(method, "_sa_instrument_role"): - wrapper._sa_instrument_role = method._sa_instrument_role - wrapper.__name__ = method.__name__ - wrapper.__doc__ = method.__doc__ - return wrapper - - -def __set(collection, item, _sa_initiator=None): - """Run set events, may eventually be inlined into decorators.""" - - if _sa_initiator is not False: - executor = collection._sa_adapter - if executor: - item = executor.fire_append_event(item, _sa_initiator) - return item - - -def __del(collection, item, _sa_initiator=None): - """Run del events, may eventually be inlined into decorators.""" - if _sa_initiator is not False: - executor = collection._sa_adapter - if executor: - executor.fire_remove_event(item, _sa_initiator) - - -def __before_delete(collection, _sa_initiator=None): - """Special method to run 'commit existing value' methods""" - executor = collection._sa_adapter - if executor: - executor.fire_pre_remove_event(_sa_initiator) - - -def _list_decorators(): - """Tailored instrumentation wrappers for any list-like class.""" - - def _tidy(fn): - fn._sa_instrumented = True - fn.__doc__ = getattr(list, fn.__name__).__doc__ - - def append(fn): - def append(self, item, _sa_initiator=None): - item = __set(self, item, _sa_initiator) - fn(self, item) - _tidy(append) - return append - - def remove(fn): - def remove(self, value, _sa_initiator=None): - __before_delete(self, _sa_initiator) - # testlib.pragma exempt:__eq__ - fn(self, value) - __del(self, value, _sa_initiator) - _tidy(remove) - return remove - - def insert(fn): - def insert(self, index, value): - value = __set(self, value) - fn(self, index, value) - _tidy(insert) - return insert - - def __setitem__(fn): - def __setitem__(self, index, value): - if not isinstance(index, slice): - existing = self[index] - if existing is not None: - __del(self, existing) - value = __set(self, value) - fn(self, index, value) - else: - # slice assignment requires __delitem__, insert, __len__ - step = index.step or 1 - start = index.start or 0 - if start < 0: - start += len(self) - if index.stop is not None: - stop = index.stop - else: - stop = len(self) - if stop < 0: - stop += len(self) - - if step == 1: - for i in range(start, stop, step): - if len(self) > start: - del self[start] - - for i, item in enumerate(value): - self.insert(i + start, item) - else: - rng = list(range(start, stop, step)) - if len(value) != len(rng): - raise ValueError( - "attempt to assign sequence of size %s to " - "extended slice of size %s" % (len(value), - len(rng))) - for i, item in zip(rng, value): - self.__setitem__(i, item) - _tidy(__setitem__) - return __setitem__ - - def __delitem__(fn): - def __delitem__(self, index): - if not isinstance(index, slice): - item = self[index] - __del(self, item) - fn(self, index) - else: - # slice deletion requires __getslice__ and a slice-groking - # __getitem__ for stepped deletion - # note: not breaking this into atomic dels - for item in self[index]: - __del(self, item) - fn(self, index) - _tidy(__delitem__) - return __delitem__ - - if util.py2k: - def __setslice__(fn): - def __setslice__(self, start, end, values): - for value in self[start:end]: - __del(self, value) - values = [__set(self, value) for value in values] - fn(self, start, end, values) - _tidy(__setslice__) - return __setslice__ - - def __delslice__(fn): - def __delslice__(self, start, end): - for value in self[start:end]: - __del(self, value) - fn(self, start, end) - _tidy(__delslice__) - return __delslice__ - - def extend(fn): - def extend(self, iterable): - for value in iterable: - self.append(value) - _tidy(extend) - return extend - - def __iadd__(fn): - def __iadd__(self, iterable): - # list.__iadd__ takes any iterable and seems to let TypeError raise - # as-is instead of returning NotImplemented - for value in iterable: - self.append(value) - return self - _tidy(__iadd__) - return __iadd__ - - def pop(fn): - def pop(self, index=-1): - __before_delete(self) - item = fn(self, index) - __del(self, item) - return item - _tidy(pop) - return pop - - if not util.py2k: - def clear(fn): - def clear(self, index=-1): - for item in self: - __del(self, item) - fn(self) - _tidy(clear) - return clear - - # __imul__ : not wrapping this. all members of the collection are already - # present, so no need to fire appends... wrapping it with an explicit - # decorator is still possible, so events on *= can be had if they're - # desired. hard to imagine a use case for __imul__, though. - - l = locals().copy() - l.pop('_tidy') - return l - - -def _dict_decorators(): - """Tailored instrumentation wrappers for any dict-like mapping class.""" - - def _tidy(fn): - fn._sa_instrumented = True - fn.__doc__ = getattr(dict, fn.__name__).__doc__ - - Unspecified = util.symbol('Unspecified') - - def __setitem__(fn): - def __setitem__(self, key, value, _sa_initiator=None): - if key in self: - __del(self, self[key], _sa_initiator) - value = __set(self, value, _sa_initiator) - fn(self, key, value) - _tidy(__setitem__) - return __setitem__ - - def __delitem__(fn): - def __delitem__(self, key, _sa_initiator=None): - if key in self: - __del(self, self[key], _sa_initiator) - fn(self, key) - _tidy(__delitem__) - return __delitem__ - - def clear(fn): - def clear(self): - for key in self: - __del(self, self[key]) - fn(self) - _tidy(clear) - return clear - - def pop(fn): - def pop(self, key, default=Unspecified): - if key in self: - __del(self, self[key]) - if default is Unspecified: - return fn(self, key) - else: - return fn(self, key, default) - _tidy(pop) - return pop - - def popitem(fn): - def popitem(self): - __before_delete(self) - item = fn(self) - __del(self, item[1]) - return item - _tidy(popitem) - return popitem - - def setdefault(fn): - def setdefault(self, key, default=None): - if key not in self: - self.__setitem__(key, default) - return default - else: - return self.__getitem__(key) - _tidy(setdefault) - return setdefault - - def update(fn): - def update(self, __other=Unspecified, **kw): - if __other is not Unspecified: - if hasattr(__other, 'keys'): - for key in list(__other): - if (key not in self or - self[key] is not __other[key]): - self[key] = __other[key] - else: - for key, value in __other: - if key not in self or self[key] is not value: - self[key] = value - for key in kw: - if key not in self or self[key] is not kw[key]: - self[key] = kw[key] - _tidy(update) - return update - - l = locals().copy() - l.pop('_tidy') - l.pop('Unspecified') - return l - -_set_binop_bases = (set, frozenset) - - -def _set_binops_check_strict(self, obj): - """Allow only set, frozenset and self.__class__-derived - objects in binops.""" - return isinstance(obj, _set_binop_bases + (self.__class__,)) - - -def _set_binops_check_loose(self, obj): - """Allow anything set-like to participate in set binops.""" - return (isinstance(obj, _set_binop_bases + (self.__class__,)) or - util.duck_type_collection(obj) == set) - - -def _set_decorators(): - """Tailored instrumentation wrappers for any set-like class.""" - - def _tidy(fn): - fn._sa_instrumented = True - fn.__doc__ = getattr(set, fn.__name__).__doc__ - - Unspecified = util.symbol('Unspecified') - - def add(fn): - def add(self, value, _sa_initiator=None): - if value not in self: - value = __set(self, value, _sa_initiator) - # testlib.pragma exempt:__hash__ - fn(self, value) - _tidy(add) - return add - - def discard(fn): - def discard(self, value, _sa_initiator=None): - # testlib.pragma exempt:__hash__ - if value in self: - __del(self, value, _sa_initiator) - # testlib.pragma exempt:__hash__ - fn(self, value) - _tidy(discard) - return discard - - def remove(fn): - def remove(self, value, _sa_initiator=None): - # testlib.pragma exempt:__hash__ - if value in self: - __del(self, value, _sa_initiator) - # testlib.pragma exempt:__hash__ - fn(self, value) - _tidy(remove) - return remove - - def pop(fn): - def pop(self): - __before_delete(self) - item = fn(self) - __del(self, item) - return item - _tidy(pop) - return pop - - def clear(fn): - def clear(self): - for item in list(self): - self.remove(item) - _tidy(clear) - return clear - - def update(fn): - def update(self, value): - for item in value: - self.add(item) - _tidy(update) - return update - - def __ior__(fn): - def __ior__(self, value): - if not _set_binops_check_strict(self, value): - return NotImplemented - for item in value: - self.add(item) - return self - _tidy(__ior__) - return __ior__ - - def difference_update(fn): - def difference_update(self, value): - for item in value: - self.discard(item) - _tidy(difference_update) - return difference_update - - def __isub__(fn): - def __isub__(self, value): - if not _set_binops_check_strict(self, value): - return NotImplemented - for item in value: - self.discard(item) - return self - _tidy(__isub__) - return __isub__ - - def intersection_update(fn): - def intersection_update(self, other): - want, have = self.intersection(other), set(self) - remove, add = have - want, want - have - - for item in remove: - self.remove(item) - for item in add: - self.add(item) - _tidy(intersection_update) - return intersection_update - - def __iand__(fn): - def __iand__(self, other): - if not _set_binops_check_strict(self, other): - return NotImplemented - want, have = self.intersection(other), set(self) - remove, add = have - want, want - have - - for item in remove: - self.remove(item) - for item in add: - self.add(item) - return self - _tidy(__iand__) - return __iand__ - - def symmetric_difference_update(fn): - def symmetric_difference_update(self, other): - want, have = self.symmetric_difference(other), set(self) - remove, add = have - want, want - have - - for item in remove: - self.remove(item) - for item in add: - self.add(item) - _tidy(symmetric_difference_update) - return symmetric_difference_update - - def __ixor__(fn): - def __ixor__(self, other): - if not _set_binops_check_strict(self, other): - return NotImplemented - want, have = self.symmetric_difference(other), set(self) - remove, add = have - want, want - have - - for item in remove: - self.remove(item) - for item in add: - self.add(item) - return self - _tidy(__ixor__) - return __ixor__ - - l = locals().copy() - l.pop('_tidy') - l.pop('Unspecified') - return l - - -class InstrumentedList(list): - """An instrumented version of the built-in list.""" - - -class InstrumentedSet(set): - """An instrumented version of the built-in set.""" - - -class InstrumentedDict(dict): - """An instrumented version of the built-in dict.""" - - -__canned_instrumentation = { - list: InstrumentedList, - set: InstrumentedSet, - dict: InstrumentedDict, - } - -__interfaces = { - list: ( - {'appender': 'append', 'remover': 'remove', - 'iterator': '__iter__'}, _list_decorators() - ), - - set: ({'appender': 'add', - 'remover': 'remove', - 'iterator': '__iter__'}, _set_decorators() - ), - - # decorators are required for dicts and object collections. - dict: ({'iterator': 'values'}, _dict_decorators()) if util.py3k - else ({'iterator': 'itervalues'}, _dict_decorators()), - } - - -class MappedCollection(dict): - """A basic dictionary-based collection class. - - Extends dict with the minimal bag semantics that collection - classes require. ``set`` and ``remove`` are implemented in terms - of a keying function: any callable that takes an object and - returns an object for use as a dictionary key. - - """ - - def __init__(self, keyfunc): - """Create a new collection with keying provided by keyfunc. - - keyfunc may be any callable any callable that takes an object and - returns an object for use as a dictionary key. - - The keyfunc will be called every time the ORM needs to add a member by - value-only (such as when loading instances from the database) or - remove a member. The usual cautions about dictionary keying apply- - ``keyfunc(object)`` should return the same output for the life of the - collection. Keying based on mutable properties can result in - unreachable instances "lost" in the collection. - - """ - self.keyfunc = keyfunc - - @collection.appender - @collection.internally_instrumented - def set(self, value, _sa_initiator=None): - """Add an item by value, consulting the keyfunc for the key.""" - - key = self.keyfunc(value) - self.__setitem__(key, value, _sa_initiator) - - @collection.remover - @collection.internally_instrumented - def remove(self, value, _sa_initiator=None): - """Remove an item by value, consulting the keyfunc for the key.""" - - key = self.keyfunc(value) - # Let self[key] raise if key is not in this collection - # testlib.pragma exempt:__ne__ - if self[key] != value: - raise sa_exc.InvalidRequestError( - "Can not remove '%s': collection holds '%s' for key '%s'. " - "Possible cause: is the MappedCollection key function " - "based on mutable properties or properties that only obtain " - "values after flush?" % - (value, self[key], key)) - self.__delitem__(key, _sa_initiator) - - @collection.converter - def _convert(self, dictlike): - """Validate and convert a dict-like object into values for set()ing. - - This is called behind the scenes when a MappedCollection is replaced - entirely by another collection, as in:: - - myobj.mappedcollection = {'a':obj1, 'b': obj2} # ... - - Raises a TypeError if the key in any (key, value) pair in the dictlike - object does not match the key that this collection's keyfunc would - have assigned for that value. - - """ - for incoming_key, value in util.dictlike_iteritems(dictlike): - new_key = self.keyfunc(value) - if incoming_key != new_key: - raise TypeError( - "Found incompatible key %r for value %r; this " - "collection's " - "keying function requires a key of %r for this value." % ( - incoming_key, value, new_key)) - yield value - -# ensure instrumentation is associated with -# these built-in classes; if a user-defined class -# subclasses these and uses @internally_instrumented, -# the superclass is otherwise not instrumented. -# see [ticket:2406]. -_instrument_class(MappedCollection) -_instrument_class(InstrumentedList) -_instrument_class(InstrumentedSet) diff --git a/libs/sqlalchemy/orm/dependency.py b/libs/sqlalchemy/orm/dependency.py deleted file mode 100644 index 34a2af39..00000000 --- a/libs/sqlalchemy/orm/dependency.py +++ /dev/null @@ -1,1165 +0,0 @@ -# orm/dependency.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Relationship dependencies. - -""" - -from .. import sql, util, exc as sa_exc -from . import attributes, exc, sync, unitofwork, \ - util as mapperutil -from .interfaces import ONETOMANY, MANYTOONE, MANYTOMANY - - -class DependencyProcessor(object): - def __init__(self, prop): - self.prop = prop - self.cascade = prop.cascade - self.mapper = prop.mapper - self.parent = prop.parent - self.secondary = prop.secondary - self.direction = prop.direction - self.post_update = prop.post_update - self.passive_deletes = prop.passive_deletes - self.passive_updates = prop.passive_updates - self.enable_typechecks = prop.enable_typechecks - if self.passive_deletes: - self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE - else: - self._passive_delete_flag = attributes.PASSIVE_OFF - if self.passive_updates: - self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE - else: - self._passive_update_flag = attributes.PASSIVE_OFF - - self.key = prop.key - if not self.prop.synchronize_pairs: - raise sa_exc.ArgumentError( - "Can't build a DependencyProcessor for relationship %s. " - "No target attributes to populate between parent and " - "child are present" % - self.prop) - - @classmethod - def from_relationship(cls, prop): - return _direction_to_processor[prop.direction](prop) - - def hasparent(self, state): - """return True if the given object instance has a parent, - according to the ``InstrumentedAttribute`` handled by this - ``DependencyProcessor``. - - """ - return self.parent.class_manager.get_impl(self.key).hasparent(state) - - def per_property_preprocessors(self, uow): - """establish actions and dependencies related to a flush. - - These actions will operate on all relevant states in - the aggregate. - - """ - uow.register_preprocessor(self, True) - - def per_property_flush_actions(self, uow): - after_save = unitofwork.ProcessAll(uow, self, False, True) - before_delete = unitofwork.ProcessAll(uow, self, True, True) - - parent_saves = unitofwork.SaveUpdateAll( - uow, - self.parent.primary_base_mapper - ) - child_saves = unitofwork.SaveUpdateAll( - uow, - self.mapper.primary_base_mapper - ) - - parent_deletes = unitofwork.DeleteAll( - uow, - self.parent.primary_base_mapper - ) - child_deletes = unitofwork.DeleteAll( - uow, - self.mapper.primary_base_mapper - ) - - self.per_property_dependencies(uow, - parent_saves, - child_saves, - parent_deletes, - child_deletes, - after_save, - before_delete - ) - - def per_state_flush_actions(self, uow, states, isdelete): - """establish actions and dependencies related to a flush. - - These actions will operate on all relevant states - individually. This occurs only if there are cycles - in the 'aggregated' version of events. - - """ - - parent_base_mapper = self.parent.primary_base_mapper - child_base_mapper = self.mapper.primary_base_mapper - child_saves = unitofwork.SaveUpdateAll(uow, child_base_mapper) - child_deletes = unitofwork.DeleteAll(uow, child_base_mapper) - - # locate and disable the aggregate processors - # for this dependency - - if isdelete: - before_delete = unitofwork.ProcessAll(uow, self, True, True) - before_delete.disabled = True - else: - after_save = unitofwork.ProcessAll(uow, self, False, True) - after_save.disabled = True - - # check if the "child" side is part of the cycle - - if child_saves not in uow.cycles: - # based on the current dependencies we use, the saves/ - # deletes should always be in the 'cycles' collection - # together. if this changes, we will have to break up - # this method a bit more. - assert child_deletes not in uow.cycles - - # child side is not part of the cycle, so we will link per-state - # actions to the aggregate "saves", "deletes" actions - child_actions = [ - (child_saves, False), (child_deletes, True) - ] - child_in_cycles = False - else: - child_in_cycles = True - - # check if the "parent" side is part of the cycle - if not isdelete: - parent_saves = unitofwork.SaveUpdateAll( - uow, - self.parent.base_mapper) - parent_deletes = before_delete = None - if parent_saves in uow.cycles: - parent_in_cycles = True - else: - parent_deletes = unitofwork.DeleteAll( - uow, - self.parent.base_mapper) - parent_saves = after_save = None - if parent_deletes in uow.cycles: - parent_in_cycles = True - - # now create actions /dependencies for each state. - for state in states: - # detect if there's anything changed or loaded - # by a preprocessor on this state/attribute. if not, - # we should be able to skip it entirely. - sum_ = state.manager[self.key].impl.get_all_pending( - state, state.dict) - - if not sum_: - continue - - if isdelete: - before_delete = unitofwork.ProcessState(uow, - self, True, state) - if parent_in_cycles: - parent_deletes = unitofwork.DeleteState( - uow, - state, - parent_base_mapper) - else: - after_save = unitofwork.ProcessState(uow, self, False, state) - if parent_in_cycles: - parent_saves = unitofwork.SaveUpdateState( - uow, - state, - parent_base_mapper) - - if child_in_cycles: - child_actions = [] - for child_state, child in sum_: - if child_state not in uow.states: - child_action = (None, None) - else: - (deleted, listonly) = uow.states[child_state] - if deleted: - child_action = ( - unitofwork.DeleteState( - uow, child_state, - child_base_mapper), - True) - else: - child_action = ( - unitofwork.SaveUpdateState( - uow, child_state, - child_base_mapper), - False) - child_actions.append(child_action) - - # establish dependencies between our possibly per-state - # parent action and our possibly per-state child action. - for child_action, childisdelete in child_actions: - self.per_state_dependencies(uow, parent_saves, - parent_deletes, - child_action, - after_save, before_delete, - isdelete, childisdelete) - - def presort_deletes(self, uowcommit, states): - return False - - def presort_saves(self, uowcommit, states): - return False - - def process_deletes(self, uowcommit, states): - pass - - def process_saves(self, uowcommit, states): - pass - - def prop_has_changes(self, uowcommit, states, isdelete): - if not isdelete or self.passive_deletes: - passive = attributes.PASSIVE_NO_INITIALIZE - elif self.direction is MANYTOONE: - passive = attributes.PASSIVE_NO_FETCH_RELATED - else: - passive = attributes.PASSIVE_OFF - - for s in states: - # TODO: add a high speed method - # to InstanceState which returns: attribute - # has a non-None value, or had one - history = uowcommit.get_attribute_history( - s, - self.key, - passive) - if history and not history.empty(): - return True - else: - return states and \ - not self.prop._is_self_referential and \ - self.mapper in uowcommit.mappers - - def _verify_canload(self, state): - if self.prop.uselist and state is None: - raise exc.FlushError( - "Can't flush None value found in " - "collection %s" % (self.prop, )) - elif state is not None and \ - not self.mapper._canload(state, - allow_subtypes=not self.enable_typechecks): - if self.mapper._canload(state, allow_subtypes=True): - raise exc.FlushError('Attempting to flush an item of type ' - '%(x)s as a member of collection ' - '"%(y)s". Expected an object of type ' - '%(z)s or a polymorphic subclass of ' - 'this type. If %(x)s is a subclass of ' - '%(z)s, configure mapper "%(zm)s" to ' - 'load this subtype polymorphically, or ' - 'set enable_typechecks=False to allow ' - 'any subtype to be accepted for flush. ' - % { - 'x': state.class_, - 'y': self.prop, - 'z': self.mapper.class_, - 'zm': self.mapper, - }) - else: - raise exc.FlushError( - 'Attempting to flush an item of type ' - '%(x)s as a member of collection ' - '"%(y)s". Expected an object of type ' - '%(z)s or a polymorphic subclass of ' - 'this type.' % { - 'x': state.class_, - 'y': self.prop, - 'z': self.mapper.class_, - }) - - def _synchronize(self, state, child, associationrow, - clearkeys, uowcommit): - raise NotImplementedError() - - def _get_reversed_processed_set(self, uow): - if not self.prop._reverse_property: - return None - - process_key = tuple(sorted( - [self.key] + - [p.key for p in self.prop._reverse_property] - )) - return uow.memo( - ('reverse_key', process_key), - set - ) - - def _post_update(self, state, uowcommit, related): - for x in related: - if x is not None: - uowcommit.issue_post_update( - state, - [r for l, r in self.prop.synchronize_pairs] - ) - break - - def _pks_changed(self, uowcommit, state): - raise NotImplementedError() - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, self.prop) - - -class OneToManyDP(DependencyProcessor): - - def per_property_dependencies(self, uow, parent_saves, - child_saves, - parent_deletes, - child_deletes, - after_save, - before_delete, - ): - if self.post_update: - child_post_updates = unitofwork.IssuePostUpdate( - uow, - self.mapper.primary_base_mapper, - False) - child_pre_updates = unitofwork.IssuePostUpdate( - uow, - self.mapper.primary_base_mapper, - True) - - uow.dependencies.update([ - (child_saves, after_save), - (parent_saves, after_save), - (after_save, child_post_updates), - - (before_delete, child_pre_updates), - (child_pre_updates, parent_deletes), - (child_pre_updates, child_deletes), - - ]) - else: - uow.dependencies.update([ - (parent_saves, after_save), - (after_save, child_saves), - (after_save, child_deletes), - - (child_saves, parent_deletes), - (child_deletes, parent_deletes), - - (before_delete, child_saves), - (before_delete, child_deletes), - ]) - - def per_state_dependencies(self, uow, - save_parent, - delete_parent, - child_action, - after_save, before_delete, - isdelete, childisdelete): - - if self.post_update: - - child_post_updates = unitofwork.IssuePostUpdate( - uow, - self.mapper.primary_base_mapper, - False) - child_pre_updates = unitofwork.IssuePostUpdate( - uow, - self.mapper.primary_base_mapper, - True) - - # TODO: this whole block is not covered - # by any tests - if not isdelete: - if childisdelete: - uow.dependencies.update([ - (child_action, after_save), - (after_save, child_post_updates), - ]) - else: - uow.dependencies.update([ - (save_parent, after_save), - (child_action, after_save), - (after_save, child_post_updates), - ]) - else: - if childisdelete: - uow.dependencies.update([ - (before_delete, child_pre_updates), - (child_pre_updates, delete_parent), - ]) - else: - uow.dependencies.update([ - (before_delete, child_pre_updates), - (child_pre_updates, delete_parent), - ]) - elif not isdelete: - uow.dependencies.update([ - (save_parent, after_save), - (after_save, child_action), - (save_parent, child_action) - ]) - else: - uow.dependencies.update([ - (before_delete, child_action), - (child_action, delete_parent) - ]) - - def presort_deletes(self, uowcommit, states): - # head object is being deleted, and we manage its list of - # child objects the child objects have to have their - # foreign key to the parent set to NULL - should_null_fks = not self.cascade.delete and \ - not self.passive_deletes == 'all' - - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - for child in history.deleted: - if child is not None and self.hasparent(child) is False: - if self.cascade.delete_orphan: - uowcommit.register_object(child, isdelete=True) - else: - uowcommit.register_object(child) - - if should_null_fks: - for child in history.unchanged: - if child is not None: - uowcommit.register_object(child, - operation="delete", prop=self.prop) - - def presort_saves(self, uowcommit, states): - children_added = uowcommit.memo(('children_added', self), set) - - for state in states: - pks_changed = self._pks_changed(uowcommit, state) - - if not pks_changed or self.passive_updates: - passive = attributes.PASSIVE_NO_INITIALIZE - else: - passive = attributes.PASSIVE_OFF - - history = uowcommit.get_attribute_history( - state, - self.key, - passive) - if history: - for child in history.added: - if child is not None: - uowcommit.register_object(child, cancel_delete=True, - operation="add", - prop=self.prop) - - children_added.update(history.added) - - for child in history.deleted: - if not self.cascade.delete_orphan: - uowcommit.register_object(child, isdelete=False, - operation='delete', - prop=self.prop) - elif self.hasparent(child) is False: - uowcommit.register_object(child, isdelete=True, - operation="delete", prop=self.prop) - for c, m, st_, dct_ in self.mapper.cascade_iterator( - 'delete', child): - uowcommit.register_object( - st_, - isdelete=True) - - if pks_changed: - if history: - for child in history.unchanged: - if child is not None: - uowcommit.register_object( - child, - False, - self.passive_updates, - operation="pk change", - prop=self.prop) - - def process_deletes(self, uowcommit, states): - # head object is being deleted, and we manage its list of - # child objects the child objects have to have their foreign - # key to the parent set to NULL this phase can be called - # safely for any cascade but is unnecessary if delete cascade - # is on. - - if self.post_update or not self.passive_deletes == 'all': - children_added = uowcommit.memo(('children_added', self), set) - - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - for child in history.deleted: - if child is not None and \ - self.hasparent(child) is False: - self._synchronize( - state, - child, - None, True, - uowcommit, False) - if self.post_update and child: - self._post_update(child, uowcommit, [state]) - - if self.post_update or not self.cascade.delete: - for child in set(history.unchanged).\ - difference(children_added): - if child is not None: - self._synchronize( - state, - child, - None, True, - uowcommit, False) - if self.post_update and child: - self._post_update(child, - uowcommit, - [state]) - - # technically, we can even remove each child from the - # collection here too. but this would be a somewhat - # inconsistent behavior since it wouldn't happen - #if the old parent wasn't deleted but child was moved. - - def process_saves(self, uowcommit, states): - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - attributes.PASSIVE_NO_INITIALIZE) - if history: - for child in history.added: - self._synchronize(state, child, None, - False, uowcommit, False) - if child is not None and self.post_update: - self._post_update(child, uowcommit, [state]) - - for child in history.deleted: - if not self.cascade.delete_orphan and \ - not self.hasparent(child): - self._synchronize(state, child, None, True, - uowcommit, False) - - if self._pks_changed(uowcommit, state): - for child in history.unchanged: - self._synchronize(state, child, None, - False, uowcommit, True) - - def _synchronize(self, state, child, - associationrow, clearkeys, uowcommit, - pks_changed): - source = state - dest = child - self._verify_canload(child) - if dest is None or \ - (not self.post_update and uowcommit.is_deleted(dest)): - return - if clearkeys: - sync.clear(dest, self.mapper, self.prop.synchronize_pairs) - else: - sync.populate(source, self.parent, dest, self.mapper, - self.prop.synchronize_pairs, uowcommit, - self.passive_updates and pks_changed) - - def _pks_changed(self, uowcommit, state): - return sync.source_modified( - uowcommit, - state, - self.parent, - self.prop.synchronize_pairs) - - -class ManyToOneDP(DependencyProcessor): - def __init__(self, prop): - DependencyProcessor.__init__(self, prop) - self.mapper._dependency_processors.append(DetectKeySwitch(prop)) - - def per_property_dependencies(self, uow, - parent_saves, - child_saves, - parent_deletes, - child_deletes, - after_save, - before_delete): - - if self.post_update: - parent_post_updates = unitofwork.IssuePostUpdate( - uow, - self.parent.primary_base_mapper, - False) - parent_pre_updates = unitofwork.IssuePostUpdate( - uow, - self.parent.primary_base_mapper, - True) - - uow.dependencies.update([ - (child_saves, after_save), - (parent_saves, after_save), - (after_save, parent_post_updates), - - (after_save, parent_pre_updates), - (before_delete, parent_pre_updates), - - (parent_pre_updates, child_deletes), - ]) - else: - uow.dependencies.update([ - (child_saves, after_save), - (after_save, parent_saves), - (parent_saves, child_deletes), - (parent_deletes, child_deletes) - ]) - - def per_state_dependencies(self, uow, - save_parent, - delete_parent, - child_action, - after_save, before_delete, - isdelete, childisdelete): - - if self.post_update: - - if not isdelete: - parent_post_updates = unitofwork.IssuePostUpdate( - uow, - self.parent.primary_base_mapper, - False) - if childisdelete: - uow.dependencies.update([ - (after_save, parent_post_updates), - (parent_post_updates, child_action) - ]) - else: - uow.dependencies.update([ - (save_parent, after_save), - (child_action, after_save), - - (after_save, parent_post_updates) - ]) - else: - parent_pre_updates = unitofwork.IssuePostUpdate( - uow, - self.parent.primary_base_mapper, - True) - - uow.dependencies.update([ - (before_delete, parent_pre_updates), - (parent_pre_updates, delete_parent), - (parent_pre_updates, child_action) - ]) - - elif not isdelete: - if not childisdelete: - uow.dependencies.update([ - (child_action, after_save), - (after_save, save_parent), - ]) - else: - uow.dependencies.update([ - (after_save, save_parent), - ]) - - else: - if childisdelete: - uow.dependencies.update([ - (delete_parent, child_action) - ]) - - def presort_deletes(self, uowcommit, states): - if self.cascade.delete or self.cascade.delete_orphan: - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - if self.cascade.delete_orphan: - todelete = history.sum() - else: - todelete = history.non_deleted() - for child in todelete: - if child is None: - continue - uowcommit.register_object(child, isdelete=True, - operation="delete", prop=self.prop) - t = self.mapper.cascade_iterator('delete', child) - for c, m, st_, dct_ in t: - uowcommit.register_object( - st_, isdelete=True) - - def presort_saves(self, uowcommit, states): - for state in states: - uowcommit.register_object(state, operation="add", prop=self.prop) - if self.cascade.delete_orphan: - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - for child in history.deleted: - if self.hasparent(child) is False: - uowcommit.register_object(child, isdelete=True, - operation="delete", prop=self.prop) - - t = self.mapper.cascade_iterator('delete', child) - for c, m, st_, dct_ in t: - uowcommit.register_object(st_, isdelete=True) - - def process_deletes(self, uowcommit, states): - if self.post_update and \ - not self.cascade.delete_orphan and \ - not self.passive_deletes == 'all': - - # post_update means we have to update our - # row to not reference the child object - # before we can DELETE the row - for state in states: - self._synchronize(state, None, None, True, uowcommit) - if state and self.post_update: - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - self._post_update(state, uowcommit, history.sum()) - - def process_saves(self, uowcommit, states): - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - attributes.PASSIVE_NO_INITIALIZE) - if history: - for child in history.added: - self._synchronize(state, child, None, False, - uowcommit, "add") - - if self.post_update: - self._post_update(state, uowcommit, history.sum()) - - def _synchronize(self, state, child, associationrow, - clearkeys, uowcommit, operation=None): - if state is None or \ - (not self.post_update and uowcommit.is_deleted(state)): - return - - if operation is not None and \ - child is not None and \ - not uowcommit.session._contains_state(child): - util.warn( - "Object of type %s not in session, %s " - "operation along '%s' won't proceed" % - (mapperutil.state_class_str(child), operation, self.prop)) - return - - if clearkeys or child is None: - sync.clear(state, self.parent, self.prop.synchronize_pairs) - else: - self._verify_canload(child) - sync.populate(child, self.mapper, state, - self.parent, - self.prop.synchronize_pairs, - uowcommit, - False) - - -class DetectKeySwitch(DependencyProcessor): - """For many-to-one relationships with no one-to-many backref, - searches for parents through the unit of work when a primary - key has changed and updates them. - - Theoretically, this approach could be expanded to support transparent - deletion of objects referenced via many-to-one as well, although - the current attribute system doesn't do enough bookkeeping for this - to be efficient. - - """ - - def per_property_preprocessors(self, uow): - if self.prop._reverse_property: - if self.passive_updates: - return - else: - if False in (prop.passive_updates for \ - prop in self.prop._reverse_property): - return - - uow.register_preprocessor(self, False) - - def per_property_flush_actions(self, uow): - parent_saves = unitofwork.SaveUpdateAll( - uow, - self.parent.base_mapper) - after_save = unitofwork.ProcessAll(uow, self, False, False) - uow.dependencies.update([ - (parent_saves, after_save) - ]) - - def per_state_flush_actions(self, uow, states, isdelete): - pass - - def presort_deletes(self, uowcommit, states): - pass - - def presort_saves(self, uow, states): - if not self.passive_updates: - # for non-passive updates, register in the preprocess stage - # so that mapper save_obj() gets a hold of changes - self._process_key_switches(states, uow) - - def prop_has_changes(self, uow, states, isdelete): - if not isdelete and self.passive_updates: - d = self._key_switchers(uow, states) - return bool(d) - - return False - - def process_deletes(self, uowcommit, states): - assert False - - def process_saves(self, uowcommit, states): - # for passive updates, register objects in the process stage - # so that we avoid ManyToOneDP's registering the object without - # the listonly flag in its own preprocess stage (results in UPDATE) - # statements being emitted - assert self.passive_updates - self._process_key_switches(states, uowcommit) - - def _key_switchers(self, uow, states): - switched, notswitched = uow.memo( - ('pk_switchers', self), - lambda: (set(), set()) - ) - - allstates = switched.union(notswitched) - for s in states: - if s not in allstates: - if self._pks_changed(uow, s): - switched.add(s) - else: - notswitched.add(s) - return switched - - def _process_key_switches(self, deplist, uowcommit): - switchers = self._key_switchers(uowcommit, deplist) - if switchers: - # if primary key values have actually changed somewhere, perform - # a linear search through the UOW in search of a parent. - for state in uowcommit.session.identity_map.all_states(): - if not issubclass(state.class_, self.parent.class_): - continue - dict_ = state.dict - related = state.get_impl(self.key).get(state, dict_, - passive=self._passive_update_flag) - if related is not attributes.PASSIVE_NO_RESULT and \ - related is not None: - related_state = attributes.instance_state(dict_[self.key]) - if related_state in switchers: - uowcommit.register_object(state, - False, - self.passive_updates) - sync.populate( - related_state, - self.mapper, state, - self.parent, self.prop.synchronize_pairs, - uowcommit, self.passive_updates) - - def _pks_changed(self, uowcommit, state): - return bool(state.key) and sync.source_modified(uowcommit, - state, - self.mapper, - self.prop.synchronize_pairs) - - -class ManyToManyDP(DependencyProcessor): - - def per_property_dependencies(self, uow, parent_saves, - child_saves, - parent_deletes, - child_deletes, - after_save, - before_delete - ): - - uow.dependencies.update([ - (parent_saves, after_save), - (child_saves, after_save), - (after_save, child_deletes), - - # a rowswitch on the parent from deleted to saved - # can make this one occur, as the "save" may remove - # an element from the - # "deleted" list before we have a chance to - # process its child rows - (before_delete, parent_saves), - - (before_delete, parent_deletes), - (before_delete, child_deletes), - (before_delete, child_saves), - ]) - - def per_state_dependencies(self, uow, - save_parent, - delete_parent, - child_action, - after_save, before_delete, - isdelete, childisdelete): - if not isdelete: - if childisdelete: - uow.dependencies.update([ - (save_parent, after_save), - (after_save, child_action), - ]) - else: - uow.dependencies.update([ - (save_parent, after_save), - (child_action, after_save), - ]) - else: - uow.dependencies.update([ - (before_delete, child_action), - (before_delete, delete_parent) - ]) - - def presort_deletes(self, uowcommit, states): - # TODO: no tests fail if this whole - # thing is removed !!!! - if not self.passive_deletes: - # if no passive deletes, load history on - # the collection, so that prop_has_changes() - # returns True - for state in states: - uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - - def presort_saves(self, uowcommit, states): - if not self.passive_updates: - # if no passive updates, load history on - # each collection where parent has changed PK, - # so that prop_has_changes() returns True - for state in states: - if self._pks_changed(uowcommit, state): - history = uowcommit.get_attribute_history( - state, - self.key, - attributes.PASSIVE_OFF) - - if not self.cascade.delete_orphan: - return - - # check for child items removed from the collection - # if delete_orphan check is turned on. - for state in states: - history = uowcommit.get_attribute_history( - state, - self.key, - attributes.PASSIVE_NO_INITIALIZE) - if history: - for child in history.deleted: - if self.hasparent(child) is False: - uowcommit.register_object(child, isdelete=True, - operation="delete", prop=self.prop) - for c, m, st_, dct_ in self.mapper.cascade_iterator( - 'delete', - child): - uowcommit.register_object( - st_, isdelete=True) - - def process_deletes(self, uowcommit, states): - secondary_delete = [] - secondary_insert = [] - secondary_update = [] - - processed = self._get_reversed_processed_set(uowcommit) - tmp = set() - for state in states: - # this history should be cached already, as - # we loaded it in preprocess_deletes - history = uowcommit.get_attribute_history( - state, - self.key, - self._passive_delete_flag) - if history: - for child in history.non_added(): - if child is None or \ - (processed is not None and - (state, child) in processed): - continue - associationrow = {} - if not self._synchronize( - state, - child, - associationrow, - False, uowcommit, "delete"): - continue - secondary_delete.append(associationrow) - - tmp.update((c, state) for c in history.non_added()) - - if processed is not None: - processed.update(tmp) - - self._run_crud(uowcommit, secondary_insert, - secondary_update, secondary_delete) - - def process_saves(self, uowcommit, states): - secondary_delete = [] - secondary_insert = [] - secondary_update = [] - - processed = self._get_reversed_processed_set(uowcommit) - tmp = set() - - for state in states: - need_cascade_pks = not self.passive_updates and \ - self._pks_changed(uowcommit, state) - if need_cascade_pks: - passive = attributes.PASSIVE_OFF - else: - passive = attributes.PASSIVE_NO_INITIALIZE - history = uowcommit.get_attribute_history(state, self.key, - passive) - if history: - for child in history.added: - if (processed is not None and - (state, child) in processed): - continue - associationrow = {} - if not self._synchronize(state, - child, - associationrow, - False, uowcommit, "add"): - continue - secondary_insert.append(associationrow) - for child in history.deleted: - if (processed is not None and - (state, child) in processed): - continue - associationrow = {} - if not self._synchronize(state, - child, - associationrow, - False, uowcommit, "delete"): - continue - secondary_delete.append(associationrow) - - tmp.update((c, state) - for c in history.added + history.deleted) - - if need_cascade_pks: - - for child in history.unchanged: - associationrow = {} - sync.update(state, - self.parent, - associationrow, - "old_", - self.prop.synchronize_pairs) - sync.update(child, - self.mapper, - associationrow, - "old_", - self.prop.secondary_synchronize_pairs) - - secondary_update.append(associationrow) - - if processed is not None: - processed.update(tmp) - - self._run_crud(uowcommit, secondary_insert, - secondary_update, secondary_delete) - - def _run_crud(self, uowcommit, secondary_insert, - secondary_update, secondary_delete): - connection = uowcommit.transaction.connection(self.mapper) - - if secondary_delete: - associationrow = secondary_delete[0] - statement = self.secondary.delete(sql.and_(*[ - c == sql.bindparam(c.key, type_=c.type) - for c in self.secondary.c - if c.key in associationrow - ])) - result = connection.execute(statement, secondary_delete) - - if result.supports_sane_multi_rowcount() and \ - result.rowcount != len(secondary_delete): - raise exc.StaleDataError( - "DELETE statement on table '%s' expected to delete " - "%d row(s); Only %d were matched." % - (self.secondary.description, len(secondary_delete), - result.rowcount) - ) - - if secondary_update: - associationrow = secondary_update[0] - statement = self.secondary.update(sql.and_(*[ - c == sql.bindparam("old_" + c.key, type_=c.type) - for c in self.secondary.c - if c.key in associationrow - ])) - result = connection.execute(statement, secondary_update) - if result.supports_sane_multi_rowcount() and \ - result.rowcount != len(secondary_update): - raise exc.StaleDataError( - "UPDATE statement on table '%s' expected to update " - "%d row(s); Only %d were matched." % - (self.secondary.description, len(secondary_update), - result.rowcount) - ) - - if secondary_insert: - statement = self.secondary.insert() - connection.execute(statement, secondary_insert) - - def _synchronize(self, state, child, associationrow, - clearkeys, uowcommit, operation): - - # this checks for None if uselist=True - self._verify_canload(child) - - # but if uselist=False we get here. If child is None, - # no association row can be generated, so return. - if child is None: - return False - - if child is not None and not uowcommit.session._contains_state(child): - if not child.deleted: - util.warn( - "Object of type %s not in session, %s " - "operation along '%s' won't proceed" % - (mapperutil.state_class_str(child), operation, self.prop)) - return False - - sync.populate_dict(state, self.parent, associationrow, - self.prop.synchronize_pairs) - sync.populate_dict(child, self.mapper, associationrow, - self.prop.secondary_synchronize_pairs) - - return True - - def _pks_changed(self, uowcommit, state): - return sync.source_modified( - uowcommit, - state, - self.parent, - self.prop.synchronize_pairs) - -_direction_to_processor = { - ONETOMANY: OneToManyDP, - MANYTOONE: ManyToOneDP, - MANYTOMANY: ManyToManyDP, -} diff --git a/libs/sqlalchemy/orm/deprecated_interfaces.py b/libs/sqlalchemy/orm/deprecated_interfaces.py deleted file mode 100644 index 020b7c71..00000000 --- a/libs/sqlalchemy/orm/deprecated_interfaces.py +++ /dev/null @@ -1,590 +0,0 @@ -# orm/deprecated_interfaces.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .. import event, util -from .interfaces import EXT_CONTINUE - -@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") -class MapperExtension(object): - """Base implementation for :class:`.Mapper` event hooks. - - .. note:: - - :class:`.MapperExtension` is deprecated. Please - refer to :func:`.event.listen` as well as - :class:`.MapperEvents`. - - New extension classes subclass :class:`.MapperExtension` and are specified - using the ``extension`` mapper() argument, which is a single - :class:`.MapperExtension` or a list of such:: - - from sqlalchemy.orm.interfaces import MapperExtension - - class MyExtension(MapperExtension): - def before_insert(self, mapper, connection, instance): - print "instance %s before insert !" % instance - - m = mapper(User, users_table, extension=MyExtension()) - - A single mapper can maintain a chain of ``MapperExtension`` - objects. When a particular mapping event occurs, the - corresponding method on each ``MapperExtension`` is invoked - serially, and each method has the ability to halt the chain - from proceeding further:: - - m = mapper(User, users_table, extension=[ext1, ext2, ext3]) - - Each ``MapperExtension`` method returns the symbol - EXT_CONTINUE by default. This symbol generally means "move - to the next ``MapperExtension`` for processing". For methods - that return objects like translated rows or new object - instances, EXT_CONTINUE means the result of the method - should be ignored. In some cases it's required for a - default mapper activity to be performed, such as adding a - new instance to a result list. - - The symbol EXT_STOP has significance within a chain - of ``MapperExtension`` objects that the chain will be stopped - when this symbol is returned. Like EXT_CONTINUE, it also - has additional significance in some cases that a default - mapper activity will not be performed. - - """ - - @classmethod - def _adapt_instrument_class(cls, self, listener): - cls._adapt_listener_methods(self, listener, ('instrument_class',)) - - @classmethod - def _adapt_listener(cls, self, listener): - cls._adapt_listener_methods( - self, listener, - ( - 'init_instance', - 'init_failed', - 'translate_row', - 'create_instance', - 'append_result', - 'populate_instance', - 'reconstruct_instance', - 'before_insert', - 'after_insert', - 'before_update', - 'after_update', - 'before_delete', - 'after_delete' - )) - - @classmethod - def _adapt_listener_methods(cls, self, listener, methods): - - for meth in methods: - me_meth = getattr(MapperExtension, meth) - ls_meth = getattr(listener, meth) - - if not util.methods_equivalent(me_meth, ls_meth): - if meth == 'reconstruct_instance': - def go(ls_meth): - def reconstruct(instance, ctx): - ls_meth(self, instance) - return reconstruct - event.listen(self.class_manager, 'load', - go(ls_meth), raw=False, propagate=True) - elif meth == 'init_instance': - def go(ls_meth): - def init_instance(instance, args, kwargs): - ls_meth(self, self.class_, - self.class_manager.original_init, - instance, args, kwargs) - return init_instance - event.listen(self.class_manager, 'init', - go(ls_meth), raw=False, propagate=True) - elif meth == 'init_failed': - def go(ls_meth): - def init_failed(instance, args, kwargs): - util.warn_exception(ls_meth, self, self.class_, - self.class_manager.original_init, - instance, args, kwargs) - - return init_failed - event.listen(self.class_manager, 'init_failure', - go(ls_meth), raw=False, propagate=True) - else: - event.listen(self, "%s" % meth, ls_meth, - raw=False, retval=True, propagate=True) - - def instrument_class(self, mapper, class_): - """Receive a class when the mapper is first constructed, and has - applied instrumentation to the mapped class. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - return EXT_CONTINUE - - def init_instance(self, mapper, class_, oldinit, instance, args, kwargs): - """Receive an instance when it's constructor is called. - - This method is only called during a userland construction of - an object. It is not called when an object is loaded from the - database. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - return EXT_CONTINUE - - def init_failed(self, mapper, class_, oldinit, instance, args, kwargs): - """Receive an instance when it's constructor has been called, - and raised an exception. - - This method is only called during a userland construction of - an object. It is not called when an object is loaded from the - database. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - return EXT_CONTINUE - - def translate_row(self, mapper, context, row): - """Perform pre-processing on the given result row and return a - new row instance. - - This is called when the mapper first receives a row, before - the object identity or the instance itself has been derived - from that row. The given row may or may not be a - ``RowProxy`` object - it will always be a dictionary-like - object which contains mapped columns as keys. The - returned object should also be a dictionary-like object - which recognizes mapped columns as keys. - - If the ultimate return value is EXT_CONTINUE, the row - is not translated. - - """ - return EXT_CONTINUE - - def create_instance(self, mapper, selectcontext, row, class_): - """Receive a row when a new object instance is about to be - created from that row. - - The method can choose to create the instance itself, or it can return - EXT_CONTINUE to indicate normal object creation should take place. - - mapper - The mapper doing the operation - - selectcontext - The QueryContext generated from the Query. - - row - The result row from the database - - class\_ - The class we are mapping. - - return value - A new object instance, or EXT_CONTINUE - - """ - return EXT_CONTINUE - - def append_result(self, mapper, selectcontext, row, instance, - result, **flags): - """Receive an object instance before that instance is appended - to a result list. - - If this method returns EXT_CONTINUE, result appending will proceed - normally. if this method returns any other value or None, - result appending will not proceed for this instance, giving - this extension an opportunity to do the appending itself, if - desired. - - mapper - The mapper doing the operation. - - selectcontext - The QueryContext generated from the Query. - - row - The result row from the database. - - instance - The object instance to be appended to the result. - - result - List to which results are being appended. - - \**flags - extra information about the row, same as criterion in - ``create_row_processor()`` method of - :class:`~sqlalchemy.orm.interfaces.MapperProperty` - """ - - return EXT_CONTINUE - - def populate_instance(self, mapper, selectcontext, row, - instance, **flags): - """Receive an instance before that instance has - its attributes populated. - - This usually corresponds to a newly loaded instance but may - also correspond to an already-loaded instance which has - unloaded attributes to be populated. The method may be called - many times for a single instance, as multiple result rows are - used to populate eagerly loaded collections. - - If this method returns EXT_CONTINUE, instance population will - proceed normally. If any other value or None is returned, - instance population will not proceed, giving this extension an - opportunity to populate the instance itself, if desired. - - .. deprecated:: 0.5 - Most usages of this hook are obsolete. For a - generic "object has been newly created from a row" hook, use - ``reconstruct_instance()``, or the ``@orm.reconstructor`` - decorator. - - """ - return EXT_CONTINUE - - def reconstruct_instance(self, mapper, instance): - """Receive an object instance after it has been created via - ``__new__``, and after initial attribute population has - occurred. - - This typically occurs when the instance is created based on - incoming result rows, and is only called once for that - instance's lifetime. - - Note that during a result-row load, this method is called upon - the first row received for this instance. Note that some - attributes and collections may or may not be loaded or even - initialized, depending on what's present in the result rows. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - return EXT_CONTINUE - - def before_insert(self, mapper, connection, instance): - """Receive an object instance before that instance is inserted - into its table. - - This is a good place to set up primary key values and such - that aren't handled otherwise. - - Column-based attributes can be modified within this method - which will result in the new value being inserted. However - *no* changes to the overall flush plan can be made, and - manipulation of the ``Session`` will not have the desired effect. - To manipulate the ``Session`` within an extension, use - ``SessionExtension``. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - def after_insert(self, mapper, connection, instance): - """Receive an object instance after that instance is inserted. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - def before_update(self, mapper, connection, instance): - """Receive an object instance before that instance is updated. - - Note that this method is called for all instances that are marked as - "dirty", even those which have no net changes to their column-based - attributes. An object is marked as dirty when any of its column-based - attributes have a "set attribute" operation called or when any of its - collections are modified. If, at update time, no column-based - attributes have any net changes, no UPDATE statement will be issued. - This means that an instance being sent to before_update is *not* a - guarantee that an UPDATE statement will be issued (although you can - affect the outcome here). - - To detect if the column-based attributes on the object have net - changes, and will therefore generate an UPDATE statement, use - ``object_session(instance).is_modified(instance, - include_collections=False)``. - - Column-based attributes can be modified within this method - which will result in the new value being updated. However - *no* changes to the overall flush plan can be made, and - manipulation of the ``Session`` will not have the desired effect. - To manipulate the ``Session`` within an extension, use - ``SessionExtension``. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - def after_update(self, mapper, connection, instance): - """Receive an object instance after that instance is updated. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - def before_delete(self, mapper, connection, instance): - """Receive an object instance before that instance is deleted. - - Note that *no* changes to the overall flush plan can be made - here; and manipulation of the ``Session`` will not have the - desired effect. To manipulate the ``Session`` within an - extension, use ``SessionExtension``. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - def after_delete(self, mapper, connection, instance): - """Receive an object instance after that instance is deleted. - - The return value is only significant within the ``MapperExtension`` - chain; the parent mapper's behavior isn't modified by this method. - - """ - - return EXT_CONTINUE - - -@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") -class SessionExtension(object): - - """Base implementation for :class:`.Session` event hooks. - - .. note:: - - :class:`.SessionExtension` is deprecated. Please - refer to :func:`.event.listen` as well as - :class:`.SessionEvents`. - - Subclasses may be installed into a :class:`.Session` (or - :class:`.sessionmaker`) using the ``extension`` keyword - argument:: - - from sqlalchemy.orm.interfaces import SessionExtension - - class MySessionExtension(SessionExtension): - def before_commit(self, session): - print "before commit!" - - Session = sessionmaker(extension=MySessionExtension()) - - The same :class:`.SessionExtension` instance can be used - with any number of sessions. - - """ - - @classmethod - def _adapt_listener(cls, self, listener): - for meth in [ - 'before_commit', - 'after_commit', - 'after_rollback', - 'before_flush', - 'after_flush', - 'after_flush_postexec', - 'after_begin', - 'after_attach', - 'after_bulk_update', - 'after_bulk_delete', - ]: - me_meth = getattr(SessionExtension, meth) - ls_meth = getattr(listener, meth) - - if not util.methods_equivalent(me_meth, ls_meth): - event.listen(self, meth, getattr(listener, meth)) - - def before_commit(self, session): - """Execute right before commit is called. - - Note that this may not be per-flush if a longer running - transaction is ongoing.""" - - def after_commit(self, session): - """Execute after a commit has occurred. - - Note that this may not be per-flush if a longer running - transaction is ongoing.""" - - def after_rollback(self, session): - """Execute after a rollback has occurred. - - Note that this may not be per-flush if a longer running - transaction is ongoing.""" - - def before_flush(self, session, flush_context, instances): - """Execute before flush process has started. - - `instances` is an optional list of objects which were passed to - the ``flush()`` method. """ - - def after_flush(self, session, flush_context): - """Execute after flush has completed, but before commit has been - called. - - Note that the session's state is still in pre-flush, i.e. 'new', - 'dirty', and 'deleted' lists still show pre-flush state as well - as the history settings on instance attributes.""" - - def after_flush_postexec(self, session, flush_context): - """Execute after flush has completed, and after the post-exec - state occurs. - - This will be when the 'new', 'dirty', and 'deleted' lists are in - their final state. An actual commit() may or may not have - occurred, depending on whether or not the flush started its own - transaction or participated in a larger transaction. """ - - def after_begin(self, session, transaction, connection): - """Execute after a transaction is begun on a connection - - `transaction` is the SessionTransaction. This method is called - after an engine level transaction is begun on a connection. """ - - def after_attach(self, session, instance): - """Execute after an instance is attached to a session. - - This is called after an add, delete or merge. """ - - def after_bulk_update(self, session, query, query_context, result): - """Execute after a bulk update operation to the session. - - This is called after a session.query(...).update() - - `query` is the query object that this update operation was - called on. `query_context` was the query context object. - `result` is the result object returned from the bulk operation. - """ - - def after_bulk_delete(self, session, query, query_context, result): - """Execute after a bulk delete operation to the session. - - This is called after a session.query(...).delete() - - `query` is the query object that this delete operation was - called on. `query_context` was the query context object. - `result` is the result object returned from the bulk operation. - """ - - -@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces") -class AttributeExtension(object): - """Base implementation for :class:`.AttributeImpl` event hooks, events - that fire upon attribute mutations in user code. - - .. note:: - - :class:`.AttributeExtension` is deprecated. Please - refer to :func:`.event.listen` as well as - :class:`.AttributeEvents`. - - :class:`.AttributeExtension` is used to listen for set, - remove, and append events on individual mapped attributes. - It is established on an individual mapped attribute using - the `extension` argument, available on - :func:`.column_property`, :func:`.relationship`, and - others:: - - from sqlalchemy.orm.interfaces import AttributeExtension - from sqlalchemy.orm import mapper, relationship, column_property - - class MyAttrExt(AttributeExtension): - def append(self, state, value, initiator): - print "append event !" - return value - - def set(self, state, value, oldvalue, initiator): - print "set event !" - return value - - mapper(SomeClass, sometable, properties={ - 'foo':column_property(sometable.c.foo, extension=MyAttrExt()), - 'bar':relationship(Bar, extension=MyAttrExt()) - }) - - Note that the :class:`.AttributeExtension` methods - :meth:`~.AttributeExtension.append` and - :meth:`~.AttributeExtension.set` need to return the - ``value`` parameter. The returned value is used as the - effective value, and allows the extension to change what is - ultimately persisted. - - AttributeExtension is assembled within the descriptors associated - with a mapped class. - - """ - - active_history = True - """indicates that the set() method would like to receive the 'old' value, - even if it means firing lazy callables. - - Note that ``active_history`` can also be set directly via - :func:`.column_property` and :func:`.relationship`. - - """ - - @classmethod - def _adapt_listener(cls, self, listener): - event.listen(self, 'append', listener.append, - active_history=listener.active_history, - raw=True, retval=True) - event.listen(self, 'remove', listener.remove, - active_history=listener.active_history, - raw=True, retval=True) - event.listen(self, 'set', listener.set, - active_history=listener.active_history, - raw=True, retval=True) - - def append(self, state, value, initiator): - """Receive a collection append event. - - The returned value will be used as the actual value to be - appended. - - """ - return value - - def remove(self, state, value, initiator): - """Receive a remove event. - - No return value is defined. - - """ - pass - - def set(self, state, value, oldvalue, initiator): - """Receive a set event. - - The returned value will be used as the actual value to be - set. - - """ - return value diff --git a/libs/sqlalchemy/orm/descriptor_props.py b/libs/sqlalchemy/orm/descriptor_props.py deleted file mode 100644 index 24b0a15e..00000000 --- a/libs/sqlalchemy/orm/descriptor_props.py +++ /dev/null @@ -1,677 +0,0 @@ -# orm/descriptor_props.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Descriptor properties are more "auxiliary" properties -that exist as configurational elements, but don't participate -as actively in the load/persist ORM loop. - -""" - -from .interfaces import MapperProperty, PropComparator -from .util import _none_set -from . import attributes -from .. import util, sql, exc as sa_exc, event, schema -from ..sql import expression -from . import properties -from . import query - - -class DescriptorProperty(MapperProperty): - """:class:`.MapperProperty` which proxies access to a - user-defined descriptor.""" - - doc = None - - def instrument_class(self, mapper): - prop = self - - class _ProxyImpl(object): - accepts_scalar_loader = False - expire_missing = True - collection = False - - def __init__(self, key): - self.key = key - - if hasattr(prop, 'get_history'): - def get_history(self, state, dict_, - passive=attributes.PASSIVE_OFF): - return prop.get_history(state, dict_, passive) - - if self.descriptor is None: - desc = getattr(mapper.class_, self.key, None) - if mapper._is_userland_descriptor(desc): - self.descriptor = desc - - if self.descriptor is None: - def fset(obj, value): - setattr(obj, self.name, value) - - def fdel(obj): - delattr(obj, self.name) - - def fget(obj): - return getattr(obj, self.name) - - self.descriptor = property( - fget=fget, - fset=fset, - fdel=fdel, - ) - - proxy_attr = attributes.\ - create_proxied_attribute(self.descriptor)\ - ( - self.parent.class_, - self.key, - self.descriptor, - lambda: self._comparator_factory(mapper), - doc=self.doc, - original_property=self - ) - proxy_attr.impl = _ProxyImpl(self.key) - mapper.class_manager.instrument_attribute(self.key, proxy_attr) - - -@util.langhelpers.dependency_for("sqlalchemy.orm.properties") -class CompositeProperty(DescriptorProperty): - """Defines a "composite" mapped attribute, representing a collection - of columns as one attribute. - - :class:`.CompositeProperty` is constructed using the :func:`.composite` - function. - - .. seealso:: - - :ref:`mapper_composite` - - """ - def __init__(self, class_, *attrs, **kwargs): - """Return a composite column-based property for use with a Mapper. - - See the mapping documentation section :ref:`mapper_composite` for a full - usage example. - - The :class:`.MapperProperty` returned by :func:`.composite` - is the :class:`.CompositeProperty`. - - :param class\_: - The "composite type" class. - - :param \*cols: - List of Column objects to be mapped. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - scalar attribute should be loaded when replaced, if not - already loaded. See the same flag on :func:`.column_property`. - - .. versionchanged:: 0.7 - This flag specifically becomes meaningful - - previously it was a placeholder. - - :param group: - A group name for this property when marked as deferred. - - :param deferred: - When True, the column property is "deferred", meaning that it does not - load immediately, and is instead loaded when the attribute is first - accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`. - - :param comparator_factory: a class which extends - :class:`.CompositeProperty.Comparator` which provides custom SQL clause - generation for comparison operations. - - :param doc: - optional string that will be applied as the doc on the - class-bound descriptor. - - :param info: Optional data dictionary which will be populated into the - :attr:`.MapperProperty.info` attribute of this object. - - .. versionadded:: 0.8 - - :param extension: - an :class:`.AttributeExtension` instance, - or list of extensions, which will be prepended to the list of - attribute listeners for the resulting descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - """ - - self.attrs = attrs - self.composite_class = class_ - self.active_history = kwargs.get('active_history', False) - self.deferred = kwargs.get('deferred', False) - self.group = kwargs.get('group', None) - self.comparator_factory = kwargs.pop('comparator_factory', - self.__class__.Comparator) - if 'info' in kwargs: - self.info = kwargs.pop('info') - - util.set_creation_order(self) - self._create_descriptor() - - - def instrument_class(self, mapper): - super(CompositeProperty, self).instrument_class(mapper) - self._setup_event_handlers() - - def do_init(self): - """Initialization which occurs after the :class:`.CompositeProperty` - has been associated with its parent mapper. - - """ - self._init_props() - self._setup_arguments_on_columns() - - def _create_descriptor(self): - """Create the Python descriptor that will serve as - the access point on instances of the mapped class. - - """ - - def fget(instance): - dict_ = attributes.instance_dict(instance) - state = attributes.instance_state(instance) - - if self.key not in dict_: - # key not present. Iterate through related - # attributes, retrieve their values. This - # ensures they all load. - values = [ - getattr(instance, key) - for key in self._attribute_keys - ] - - # current expected behavior here is that the composite is - # created on access if the object is persistent or if - # col attributes have non-None. This would be better - # if the composite were created unconditionally, - # but that would be a behavioral change. - if self.key not in dict_ and ( - state.key is not None or - not _none_set.issuperset(values) - ): - dict_[self.key] = self.composite_class(*values) - state.manager.dispatch.refresh(state, None, [self.key]) - - return dict_.get(self.key, None) - - def fset(instance, value): - dict_ = attributes.instance_dict(instance) - state = attributes.instance_state(instance) - attr = state.manager[self.key] - previous = dict_.get(self.key, attributes.NO_VALUE) - for fn in attr.dispatch.set: - value = fn(state, value, previous, attr.impl) - dict_[self.key] = value - if value is None: - for key in self._attribute_keys: - setattr(instance, key, None) - else: - for key, value in zip( - self._attribute_keys, - value.__composite_values__()): - setattr(instance, key, value) - - def fdel(instance): - state = attributes.instance_state(instance) - dict_ = attributes.instance_dict(instance) - previous = dict_.pop(self.key, attributes.NO_VALUE) - attr = state.manager[self.key] - attr.dispatch.remove(state, previous, attr.impl) - for key in self._attribute_keys: - setattr(instance, key, None) - - self.descriptor = property(fget, fset, fdel) - - @util.memoized_property - def _comparable_elements(self): - return [ - getattr(self.parent.class_, prop.key) - for prop in self.props - ] - - def _init_props(self): - self.props = props = [] - for attr in self.attrs: - if isinstance(attr, str): - prop = self.parent.get_property(attr) - elif isinstance(attr, schema.Column): - prop = self.parent._columntoproperty[attr] - elif isinstance(attr, attributes.InstrumentedAttribute): - prop = attr.property - else: - raise sa_exc.ArgumentError( - "Composite expects Column objects or mapped " - "attributes/attribute names as arguments, got: %r" - % (attr,)) - props.append(prop) - - @property - def columns(self): - return [a for a in self.attrs if isinstance(a, schema.Column)] - - def _setup_arguments_on_columns(self): - """Propagate configuration arguments made on this composite - to the target columns, for those that apply. - - """ - for prop in self.props: - prop.active_history = self.active_history - if self.deferred: - prop.deferred = self.deferred - prop.strategy_class = prop._strategy_lookup( - ("deferred", True), - ("instrument", True)) - prop.group = self.group - - def _setup_event_handlers(self): - """Establish events that populate/expire the composite attribute.""" - - def load_handler(state, *args): - dict_ = state.dict - - if self.key in dict_: - return - - # if column elements aren't loaded, skip. - # __get__() will initiate a load for those - # columns - for k in self._attribute_keys: - if k not in dict_: - return - - #assert self.key not in dict_ - dict_[self.key] = self.composite_class( - *[state.dict[key] for key in - self._attribute_keys] - ) - - def expire_handler(state, keys): - if keys is None or set(self._attribute_keys).intersection(keys): - state.dict.pop(self.key, None) - - def insert_update_handler(mapper, connection, state): - """After an insert or update, some columns may be expired due - to server side defaults, or re-populated due to client side - defaults. Pop out the composite value here so that it - recreates. - - """ - - state.dict.pop(self.key, None) - - event.listen(self.parent, 'after_insert', - insert_update_handler, raw=True) - event.listen(self.parent, 'after_update', - insert_update_handler, raw=True) - event.listen(self.parent, 'load', - load_handler, raw=True, propagate=True) - event.listen(self.parent, 'refresh', - load_handler, raw=True, propagate=True) - event.listen(self.parent, 'expire', - expire_handler, raw=True, propagate=True) - - # TODO: need a deserialize hook here - - @util.memoized_property - def _attribute_keys(self): - return [ - prop.key for prop in self.props - ] - - def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): - """Provided for userland code that uses attributes.get_history().""" - - added = [] - deleted = [] - - has_history = False - for prop in self.props: - key = prop.key - hist = state.manager[key].impl.get_history(state, dict_) - if hist.has_changes(): - has_history = True - - non_deleted = hist.non_deleted() - if non_deleted: - added.extend(non_deleted) - else: - added.append(None) - if hist.deleted: - deleted.extend(hist.deleted) - else: - deleted.append(None) - - if has_history: - return attributes.History( - [self.composite_class(*added)], - (), - [self.composite_class(*deleted)] - ) - else: - return attributes.History( - (), [self.composite_class(*added)], () - ) - - def _comparator_factory(self, mapper): - return self.comparator_factory(self, mapper) - - class CompositeBundle(query.Bundle): - def __init__(self, property, expr): - self.property = property - super(CompositeProperty.CompositeBundle, self).__init__( - property.key, *expr) - - def create_row_processor(self, query, procs, labels): - def proc(row, result): - return self.property.composite_class(*[proc(row, result) for proc in procs]) - return proc - - - class Comparator(PropComparator): - """Produce boolean, comparison, and other operators for - :class:`.CompositeProperty` attributes. - - See the example in :ref:`composite_operations` for an overview - of usage , as well as the documentation for :class:`.PropComparator`. - - See also: - - :class:`.PropComparator` - - :class:`.ColumnOperators` - - :ref:`types_operators` - - :attr:`.TypeEngine.comparator_factory` - - """ - - - __hash__ = None - - @property - def clauses(self): - return self.__clause_element__() - - def __clause_element__(self): - return expression.ClauseList(group=False, *self._comparable_elements) - - def _query_clause_element(self): - return CompositeProperty.CompositeBundle(self.prop, self.__clause_element__()) - - @util.memoized_property - def _comparable_elements(self): - if self._adapt_to_entity: - return [ - getattr( - self._adapt_to_entity.entity, - prop.key - ) for prop in self.prop._comparable_elements - ] - else: - return self.prop._comparable_elements - - def __eq__(self, other): - if other is None: - values = [None] * len(self.prop._comparable_elements) - else: - values = other.__composite_values__() - comparisons = [ - a == b - for a, b in zip(self.prop._comparable_elements, values) - ] - if self._adapt_to_entity: - comparisons = [self.adapter(x) for x in comparisons] - return sql.and_(*comparisons) - - def __ne__(self, other): - return sql.not_(self.__eq__(other)) - - def __str__(self): - return str(self.parent.class_.__name__) + "." + self.key - - -@util.langhelpers.dependency_for("sqlalchemy.orm.properties") -class ConcreteInheritedProperty(DescriptorProperty): - """A 'do nothing' :class:`.MapperProperty` that disables - an attribute on a concrete subclass that is only present - on the inherited mapper, not the concrete classes' mapper. - - Cases where this occurs include: - - * When the superclass mapper is mapped against a - "polymorphic union", which includes all attributes from - all subclasses. - * When a relationship() is configured on an inherited mapper, - but not on the subclass mapper. Concrete mappers require - that relationship() is configured explicitly on each - subclass. - - """ - - def _comparator_factory(self, mapper): - comparator_callable = None - - for m in self.parent.iterate_to_root(): - p = m._props[self.key] - if not isinstance(p, ConcreteInheritedProperty): - comparator_callable = p.comparator_factory - break - return comparator_callable - - def __init__(self): - def warn(): - raise AttributeError("Concrete %s does not implement " - "attribute %r at the instance level. Add this " - "property explicitly to %s." % - (self.parent, self.key, self.parent)) - - class NoninheritedConcreteProp(object): - def __set__(s, obj, value): - warn() - - def __delete__(s, obj): - warn() - - def __get__(s, obj, owner): - if obj is None: - return self.descriptor - warn() - self.descriptor = NoninheritedConcreteProp() - - -@util.langhelpers.dependency_for("sqlalchemy.orm.properties") -class SynonymProperty(DescriptorProperty): - - def __init__(self, name, map_column=None, - descriptor=None, comparator_factory=None, - doc=None): - """Denote an attribute name as a synonym to a mapped property, - in that the attribute will mirror the value and expression behavior - of another attribute. - - :param name: the name of the existing mapped property. This - can refer to the string name of any :class:`.MapperProperty` - configured on the class, including column-bound attributes - and relationships. - - :param descriptor: a Python :term:`descriptor` that will be used - as a getter (and potentially a setter) when this attribute is - accessed at the instance level. - - :param map_column: if ``True``, the :func:`.synonym` construct will - locate the existing named :class:`.MapperProperty` based on the - attribute name of this :func:`.synonym`, and assign it to a new - attribute linked to the name of this :func:`.synonym`. - That is, given a mapping like:: - - class MyClass(Base): - __tablename__ = 'my_table' - - id = Column(Integer, primary_key=True) - job_status = Column(String(50)) - - job_status = synonym("_job_status", map_column=True) - - The above class ``MyClass`` will now have the ``job_status`` - :class:`.Column` object mapped to the attribute named ``_job_status``, - and the attribute named ``job_status`` will refer to the synonym - itself. This feature is typically used in conjunction with the - ``descriptor`` argument in order to link a user-defined descriptor - as a "wrapper" for an existing column. - - :param comparator_factory: A subclass of :class:`.PropComparator` - that will provide custom comparison behavior at the SQL expression - level. - - .. note:: - - For the use case of providing an attribute which redefines both - Python-level and SQL-expression level behavior of an attribute, - please refer to the Hybrid attribute introduced at - :ref:`mapper_hybrids` for a more effective technique. - - .. seealso:: - - :ref:`synonyms` - examples of functionality. - - :ref:`mapper_hybrids` - Hybrids provide a better approach for - more complicated attribute-wrapping schemes than synonyms. - - """ - - self.name = name - self.map_column = map_column - self.descriptor = descriptor - self.comparator_factory = comparator_factory - self.doc = doc or (descriptor and descriptor.__doc__) or None - - util.set_creation_order(self) - - # TODO: when initialized, check _proxied_property, - # emit a warning if its not a column-based property - - @util.memoized_property - def _proxied_property(self): - return getattr(self.parent.class_, self.name).property - - def _comparator_factory(self, mapper): - prop = self._proxied_property - - if self.comparator_factory: - comp = self.comparator_factory(prop, mapper) - else: - comp = prop.comparator_factory(prop, mapper) - return comp - - def set_parent(self, parent, init): - if self.map_column: - # implement the 'map_column' option. - if self.key not in parent.mapped_table.c: - raise sa_exc.ArgumentError( - "Can't compile synonym '%s': no column on table " - "'%s' named '%s'" - % (self.name, parent.mapped_table.description, self.key)) - elif parent.mapped_table.c[self.key] in \ - parent._columntoproperty and \ - parent._columntoproperty[ - parent.mapped_table.c[self.key] - ].key == self.name: - raise sa_exc.ArgumentError( - "Can't call map_column=True for synonym %r=%r, " - "a ColumnProperty already exists keyed to the name " - "%r for column %r" % - (self.key, self.name, self.name, self.key) - ) - p = properties.ColumnProperty(parent.mapped_table.c[self.key]) - parent._configure_property( - self.name, p, - init=init, - setparent=True) - p._mapped_by_synonym = self.key - - self.parent = parent - - -@util.langhelpers.dependency_for("sqlalchemy.orm.properties") -class ComparableProperty(DescriptorProperty): - """Instruments a Python property for use in query expressions.""" - - def __init__(self, comparator_factory, descriptor=None, doc=None): - """Provides a method of applying a :class:`.PropComparator` - to any Python descriptor attribute. - - .. versionchanged:: 0.7 - :func:`.comparable_property` is superseded by - the :mod:`~sqlalchemy.ext.hybrid` extension. See the example - at :ref:`hybrid_custom_comparators`. - - Allows any Python descriptor to behave like a SQL-enabled - attribute when used at the class level in queries, allowing - redefinition of expression operator behavior. - - In the example below we redefine :meth:`.PropComparator.operate` - to wrap both sides of an expression in ``func.lower()`` to produce - case-insensitive comparison:: - - from sqlalchemy.orm import comparable_property - from sqlalchemy.orm.interfaces import PropComparator - from sqlalchemy.sql import func - from sqlalchemy import Integer, String, Column - from sqlalchemy.ext.declarative import declarative_base - - class CaseInsensitiveComparator(PropComparator): - def __clause_element__(self): - return self.prop - - def operate(self, op, other): - return op( - func.lower(self.__clause_element__()), - func.lower(other) - ) - - Base = declarative_base() - - class SearchWord(Base): - __tablename__ = 'search_word' - id = Column(Integer, primary_key=True) - word = Column(String) - word_insensitive = comparable_property(lambda prop, mapper: - CaseInsensitiveComparator(mapper.c.word, mapper) - ) - - - A mapping like the above allows the ``word_insensitive`` attribute - to render an expression like:: - - >>> print SearchWord.word_insensitive == "Trucks" - lower(search_word.word) = lower(:lower_1) - - :param comparator_factory: - A PropComparator subclass or factory that defines operator behavior - for this property. - - :param descriptor: - Optional when used in a ``properties={}`` declaration. The Python - descriptor or property to layer comparison behavior on top of. - - The like-named descriptor will be automatically retrieved from the - mapped class if left blank in a ``properties`` declaration. - - """ - self.descriptor = descriptor - self.comparator_factory = comparator_factory - self.doc = doc or (descriptor and descriptor.__doc__) or None - util.set_creation_order(self) - - def _comparator_factory(self, mapper): - return self.comparator_factory(self, mapper) - - diff --git a/libs/sqlalchemy/orm/dynamic.py b/libs/sqlalchemy/orm/dynamic.py deleted file mode 100644 index bae09d32..00000000 --- a/libs/sqlalchemy/orm/dynamic.py +++ /dev/null @@ -1,368 +0,0 @@ -# orm/dynamic.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Dynamic collection API. - -Dynamic collections act like Query() objects for read operations and support -basic add/delete mutation. - -""" - -from .. import log, util, exc -from ..sql import operators -from . import ( - attributes, object_session, util as orm_util, strategies, - object_mapper, exc as orm_exc, properties - ) -from .query import Query - -@log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="dynamic") -class DynaLoader(strategies.AbstractRelationshipLoader): - def init_class_attribute(self, mapper): - self.is_class_level = True - if not self.uselist: - raise exc.InvalidRequestError( - "On relationship %s, 'dynamic' loaders cannot be used with " - "many-to-one/one-to-one relationships and/or " - "uselist=False." % self.parent_property) - strategies._register_attribute(self, - mapper, - useobject=True, - uselist=True, - impl_class=DynamicAttributeImpl, - target_mapper=self.parent_property.mapper, - order_by=self.parent_property.order_by, - query_class=self.parent_property.query_class, - backref=self.parent_property.back_populates, - ) - -class DynamicAttributeImpl(attributes.AttributeImpl): - uses_objects = True - accepts_scalar_loader = False - supports_population = False - collection = False - - def __init__(self, class_, key, typecallable, - dispatch, - target_mapper, order_by, query_class=None, **kw): - super(DynamicAttributeImpl, self).\ - __init__(class_, key, typecallable, dispatch, **kw) - self.target_mapper = target_mapper - self.order_by = order_by - if not query_class: - self.query_class = AppenderQuery - elif AppenderMixin in query_class.mro(): - self.query_class = query_class - else: - self.query_class = mixin_user_query(query_class) - - def get(self, state, dict_, passive=attributes.PASSIVE_OFF): - if not passive & attributes.SQL_OK: - return self._get_collection_history(state, - attributes.PASSIVE_NO_INITIALIZE).added_items - else: - return self.query_class(self, state) - - def get_collection(self, state, dict_, user_data=None, - passive=attributes.PASSIVE_NO_INITIALIZE): - if not passive & attributes.SQL_OK: - return self._get_collection_history(state, - passive).added_items - else: - history = self._get_collection_history(state, passive) - return history.added_plus_unchanged - - @util.memoized_property - def _append_token(self): - return attributes.Event(self, attributes.OP_APPEND) - - @util.memoized_property - def _remove_token(self): - return attributes.Event(self, attributes.OP_REMOVE) - - def fire_append_event(self, state, dict_, value, initiator, - collection_history=None): - if collection_history is None: - collection_history = self._modified_event(state, dict_) - - collection_history.add_added(value) - - for fn in self.dispatch.append: - value = fn(state, value, initiator or self._append_token) - - if self.trackparent and value is not None: - self.sethasparent(attributes.instance_state(value), state, True) - - def fire_remove_event(self, state, dict_, value, initiator, - collection_history=None): - if collection_history is None: - collection_history = self._modified_event(state, dict_) - - collection_history.add_removed(value) - - if self.trackparent and value is not None: - self.sethasparent(attributes.instance_state(value), state, False) - - for fn in self.dispatch.remove: - fn(state, value, initiator or self._remove_token) - - def _modified_event(self, state, dict_): - - if self.key not in state.committed_state: - state.committed_state[self.key] = CollectionHistory(self, state) - - state._modified_event(dict_, - self, - attributes.NEVER_SET) - - # this is a hack to allow the fixtures.ComparableEntity fixture - # to work - dict_[self.key] = True - return state.committed_state[self.key] - - def set(self, state, dict_, value, initiator, - passive=attributes.PASSIVE_OFF, - check_old=None, pop=False): - if initiator and initiator.parent_token is self.parent_token: - return - - if pop and value is None: - return - self._set_iterable(state, dict_, value) - - def _set_iterable(self, state, dict_, iterable, adapter=None): - new_values = list(iterable) - if state.has_identity: - old_collection = util.IdentitySet(self.get(state, dict_)) - - collection_history = self._modified_event(state, dict_) - if not state.has_identity: - old_collection = collection_history.added_items - else: - old_collection = old_collection.union( - collection_history.added_items) - - idset = util.IdentitySet - constants = old_collection.intersection(new_values) - additions = idset(new_values).difference(constants) - removals = old_collection.difference(constants) - - for member in new_values: - if member in additions: - self.fire_append_event(state, dict_, member, None, - collection_history=collection_history) - - for member in removals: - self.fire_remove_event(state, dict_, member, None, - collection_history=collection_history) - - def delete(self, *args, **kwargs): - raise NotImplementedError() - - def set_committed_value(self, state, dict_, value): - raise NotImplementedError("Dynamic attributes don't support " - "collection population.") - - def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): - c = self._get_collection_history(state, passive) - return c.as_history() - - def get_all_pending(self, state, dict_): - c = self._get_collection_history( - state, attributes.PASSIVE_NO_INITIALIZE) - return [ - (attributes.instance_state(x), x) - for x in - c.all_items - ] - - def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF): - if self.key in state.committed_state: - c = state.committed_state[self.key] - else: - c = CollectionHistory(self, state) - - if state.has_identity and (passive & attributes.INIT_OK): - return CollectionHistory(self, state, apply_to=c) - else: - return c - - def append(self, state, dict_, value, initiator, - passive=attributes.PASSIVE_OFF): - if initiator is not self: - self.fire_append_event(state, dict_, value, initiator) - - def remove(self, state, dict_, value, initiator, - passive=attributes.PASSIVE_OFF): - if initiator is not self: - self.fire_remove_event(state, dict_, value, initiator) - - def pop(self, state, dict_, value, initiator, - passive=attributes.PASSIVE_OFF): - self.remove(state, dict_, value, initiator, passive=passive) - - -class AppenderMixin(object): - query_class = None - - def __init__(self, attr, state): - super(AppenderMixin, self).__init__(attr.target_mapper, None) - self.instance = instance = state.obj() - self.attr = attr - - mapper = object_mapper(instance) - prop = mapper._props[self.attr.key] - self._criterion = prop.compare( - operators.eq, - instance, - value_is_parent=True, - alias_secondary=False) - - if self.attr.order_by: - self._order_by = self.attr.order_by - - def session(self): - sess = object_session(self.instance) - if sess is not None and self.autoflush and sess.autoflush \ - and self.instance in sess: - sess.flush() - if not orm_util.has_identity(self.instance): - return None - else: - return sess - session = property(session, lambda s, x: None) - - def __iter__(self): - sess = self.session - if sess is None: - return iter(self.attr._get_collection_history( - attributes.instance_state(self.instance), - attributes.PASSIVE_NO_INITIALIZE).added_items) - else: - return iter(self._clone(sess)) - - def __getitem__(self, index): - sess = self.session - if sess is None: - return self.attr._get_collection_history( - attributes.instance_state(self.instance), - attributes.PASSIVE_NO_INITIALIZE).indexed(index) - else: - return self._clone(sess).__getitem__(index) - - def count(self): - sess = self.session - if sess is None: - return len(self.attr._get_collection_history( - attributes.instance_state(self.instance), - attributes.PASSIVE_NO_INITIALIZE).added_items) - else: - return self._clone(sess).count() - - def _clone(self, sess=None): - # note we're returning an entirely new Query class instance - # here without any assignment capabilities; the class of this - # query is determined by the session. - instance = self.instance - if sess is None: - sess = object_session(instance) - if sess is None: - raise orm_exc.DetachedInstanceError( - "Parent instance %s is not bound to a Session, and no " - "contextual session is established; lazy load operation " - "of attribute '%s' cannot proceed" % ( - orm_util.instance_str(instance), self.attr.key)) - - if self.query_class: - query = self.query_class(self.attr.target_mapper, session=sess) - else: - query = sess.query(self.attr.target_mapper) - - query._criterion = self._criterion - query._order_by = self._order_by - - return query - - def extend(self, iterator): - for item in iterator: - self.attr.append( - attributes.instance_state(self.instance), - attributes.instance_dict(self.instance), item, None) - - def append(self, item): - self.attr.append( - attributes.instance_state(self.instance), - attributes.instance_dict(self.instance), item, None) - - def remove(self, item): - self.attr.remove( - attributes.instance_state(self.instance), - attributes.instance_dict(self.instance), item, None) - - -class AppenderQuery(AppenderMixin, Query): - """A dynamic query that supports basic collection storage operations.""" - - -def mixin_user_query(cls): - """Return a new class with AppenderQuery functionality layered over.""" - name = 'Appender' + cls.__name__ - return type(name, (AppenderMixin, cls), {'query_class': cls}) - - -class CollectionHistory(object): - """Overrides AttributeHistory to receive append/remove events directly.""" - - def __init__(self, attr, state, apply_to=None): - if apply_to: - coll = AppenderQuery(attr, state).autoflush(False) - self.unchanged_items = util.OrderedIdentitySet(coll) - self.added_items = apply_to.added_items - self.deleted_items = apply_to.deleted_items - self._reconcile_collection = True - else: - self.deleted_items = util.OrderedIdentitySet() - self.added_items = util.OrderedIdentitySet() - self.unchanged_items = util.OrderedIdentitySet() - self._reconcile_collection = False - - @property - def added_plus_unchanged(self): - return list(self.added_items.union(self.unchanged_items)) - - @property - def all_items(self): - return list(self.added_items.union( - self.unchanged_items).union(self.deleted_items)) - - def as_history(self): - if self._reconcile_collection: - added = self.added_items.difference(self.unchanged_items) - deleted = self.deleted_items.intersection(self.unchanged_items) - unchanged = self.unchanged_items.difference(deleted) - else: - added, unchanged, deleted = self.added_items,\ - self.unchanged_items,\ - self.deleted_items - return attributes.History( - list(added), - list(unchanged), - list(deleted), - ) - - def indexed(self, index): - return list(self.added_items)[index] - - def add_added(self, value): - self.added_items.add(value) - - def add_removed(self, value): - if value in self.added_items: - self.added_items.remove(value) - else: - self.deleted_items.add(value) - diff --git a/libs/sqlalchemy/orm/evaluator.py b/libs/sqlalchemy/orm/evaluator.py deleted file mode 100644 index e1dd9606..00000000 --- a/libs/sqlalchemy/orm/evaluator.py +++ /dev/null @@ -1,123 +0,0 @@ -# orm/evaluator.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -import operator -from ..sql import operators - - -class UnevaluatableError(Exception): - pass - -_straight_ops = set(getattr(operators, op) - for op in ('add', 'mul', 'sub', - 'div', - 'mod', 'truediv', - 'lt', 'le', 'ne', 'gt', 'ge', 'eq')) - - -_notimplemented_ops = set(getattr(operators, op) - for op in ('like_op', 'notlike_op', 'ilike_op', - 'notilike_op', 'between_op', 'in_op', - 'notin_op', 'endswith_op', 'concat_op')) - - -class EvaluatorCompiler(object): - def process(self, clause): - meth = getattr(self, "visit_%s" % clause.__visit_name__, None) - if not meth: - raise UnevaluatableError( - "Cannot evaluate %s" % type(clause).__name__) - return meth(clause) - - def visit_grouping(self, clause): - return self.process(clause.element) - - def visit_null(self, clause): - return lambda obj: None - - def visit_false(self, clause): - return lambda obj: False - - def visit_true(self, clause): - return lambda obj: True - - def visit_column(self, clause): - if 'parentmapper' in clause._annotations: - key = clause._annotations['parentmapper'].\ - _columntoproperty[clause].key - else: - key = clause.key - get_corresponding_attr = operator.attrgetter(key) - return lambda obj: get_corresponding_attr(obj) - - def visit_clauselist(self, clause): - evaluators = list(map(self.process, clause.clauses)) - if clause.operator is operators.or_: - def evaluate(obj): - has_null = False - for sub_evaluate in evaluators: - value = sub_evaluate(obj) - if value: - return True - has_null = has_null or value is None - if has_null: - return None - return False - elif clause.operator is operators.and_: - def evaluate(obj): - for sub_evaluate in evaluators: - value = sub_evaluate(obj) - if not value: - if value is None: - return None - return False - return True - else: - raise UnevaluatableError( - "Cannot evaluate clauselist with operator %s" % - clause.operator) - - return evaluate - - def visit_binary(self, clause): - eval_left, eval_right = list(map(self.process, - [clause.left, clause.right])) - operator = clause.operator - if operator is operators.is_: - def evaluate(obj): - return eval_left(obj) == eval_right(obj) - elif operator is operators.isnot: - def evaluate(obj): - return eval_left(obj) != eval_right(obj) - elif operator in _straight_ops: - def evaluate(obj): - left_val = eval_left(obj) - right_val = eval_right(obj) - if left_val is None or right_val is None: - return None - return operator(eval_left(obj), eval_right(obj)) - else: - raise UnevaluatableError( - "Cannot evaluate %s with operator %s" % - (type(clause).__name__, clause.operator)) - return evaluate - - def visit_unary(self, clause): - eval_inner = self.process(clause.element) - if clause.operator is operators.inv: - def evaluate(obj): - value = eval_inner(obj) - if value is None: - return None - return not value - return evaluate - raise UnevaluatableError( - "Cannot evaluate %s with operator %s" % - (type(clause).__name__, clause.operator)) - - def visit_bindparam(self, clause): - val = clause.value - return lambda obj: val diff --git a/libs/sqlalchemy/orm/events.py b/libs/sqlalchemy/orm/events.py deleted file mode 100644 index a09154dd..00000000 --- a/libs/sqlalchemy/orm/events.py +++ /dev/null @@ -1,1656 +0,0 @@ -# orm/events.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""ORM event interfaces. - -""" -from .. import event, exc, util -from .base import _mapper_or_none -import inspect -import weakref -from . import interfaces -from . import mapperlib, instrumentation -from .session import Session, sessionmaker -from .scoping import scoped_session -from .attributes import QueryableAttribute - -class InstrumentationEvents(event.Events): - """Events related to class instrumentation events. - - The listeners here support being established against - any new style class, that is any object that is a subclass - of 'type'. Events will then be fired off for events - against that class. If the "propagate=True" flag is passed - to event.listen(), the event will fire off for subclasses - of that class as well. - - The Python ``type`` builtin is also accepted as a target, - which when used has the effect of events being emitted - for all classes. - - Note the "propagate" flag here is defaulted to ``True``, - unlike the other class level events where it defaults - to ``False``. This means that new subclasses will also - be the subject of these events, when a listener - is established on a superclass. - - .. versionchanged:: 0.8 - events here will emit based - on comparing the incoming class to the type of class - passed to :func:`.event.listen`. Previously, the - event would fire for any class unconditionally regardless - of what class was sent for listening, despite - documentation which stated the contrary. - - """ - - _target_class_doc = "SomeBaseClass" - _dispatch_target = instrumentation.InstrumentationFactory - - - @classmethod - def _accept_with(cls, target): - if isinstance(target, type): - return _InstrumentationEventsHold(target) - else: - return None - - @classmethod - def _listen(cls, event_key, propagate=True): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - def listen(target_cls, *arg): - listen_cls = target() - if propagate and issubclass(target_cls, listen_cls): - return fn(target_cls, *arg) - elif not propagate and target_cls is listen_cls: - return fn(target_cls, *arg) - - def remove(ref): - key = event.registry._EventKey(None, identifier, listen, - instrumentation._instrumentation_factory) - getattr(instrumentation._instrumentation_factory.dispatch, - identifier).remove(key) - - target = weakref.ref(target.class_, remove) - - event_key.\ - with_dispatch_target(instrumentation._instrumentation_factory).\ - with_wrapper(listen).base_listen() - - @classmethod - def _clear(cls): - super(InstrumentationEvents, cls)._clear() - instrumentation._instrumentation_factory.dispatch._clear() - - def class_instrument(self, cls): - """Called after the given class is instrumented. - - To get at the :class:`.ClassManager`, use - :func:`.manager_of_class`. - - """ - - def class_uninstrument(self, cls): - """Called before the given class is uninstrumented. - - To get at the :class:`.ClassManager`, use - :func:`.manager_of_class`. - - """ - - def attribute_instrument(self, cls, key, inst): - """Called when an attribute is instrumented.""" - - - -class _InstrumentationEventsHold(object): - """temporary marker object used to transfer from _accept_with() to - _listen() on the InstrumentationEvents class. - - """ - def __init__(self, class_): - self.class_ = class_ - - dispatch = event.dispatcher(InstrumentationEvents) - -class InstanceEvents(event.Events): - """Define events specific to object lifecycle. - - e.g.:: - - from sqlalchemy import event - - def my_load_listener(target, context): - print "on load!" - - event.listen(SomeClass, 'load', my_load_listener) - - Available targets include: - - * mapped classes - * unmapped superclasses of mapped or to-be-mapped classes - (using the ``propagate=True`` flag) - * :class:`.Mapper` objects - * the :class:`.Mapper` class itself and the :func:`.mapper` - function indicate listening for all mappers. - - .. versionchanged:: 0.8.0 instance events can be associated with - unmapped superclasses of mapped classes. - - Instance events are closely related to mapper events, but - are more specific to the instance and its instrumentation, - rather than its system of persistence. - - When using :class:`.InstanceEvents`, several modifiers are - available to the :func:`.event.listen` function. - - :param propagate=False: When True, the event listener should - be applied to all inheriting classes as well as the - class which is the target of this listener. - :param raw=False: When True, the "target" argument passed - to applicable event listener functions will be the - instance's :class:`.InstanceState` management - object, rather than the mapped instance itself. - - """ - - _target_class_doc = "SomeClass" - - _dispatch_target = instrumentation.ClassManager - - @classmethod - def _new_classmanager_instance(cls, class_, classmanager): - _InstanceEventsHold.populate(class_, classmanager) - - @classmethod - @util.dependencies("sqlalchemy.orm") - def _accept_with(cls, orm, target): - if isinstance(target, instrumentation.ClassManager): - return target - elif isinstance(target, mapperlib.Mapper): - return target.class_manager - elif target is orm.mapper: - return instrumentation.ClassManager - elif isinstance(target, type): - if issubclass(target, mapperlib.Mapper): - return instrumentation.ClassManager - else: - manager = instrumentation.manager_of_class(target) - if manager: - return manager - else: - return _InstanceEventsHold(target) - return None - - @classmethod - def _listen(cls, event_key, raw=False, propagate=False): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - if not raw: - orig_fn = fn - - def wrap(state, *arg, **kw): - return orig_fn(state.obj(), *arg, **kw) - fn = wrap - event_key = event_key.with_wrapper(fn) - - event_key.base_listen(propagate=propagate) - - if propagate: - for mgr in target.subclass_managers(True): - event_key.with_dispatch_target(mgr).base_listen(propagate=True) - - @classmethod - def _clear(cls): - super(InstanceEvents, cls)._clear() - _InstanceEventsHold._clear() - - def first_init(self, manager, cls): - """Called when the first instance of a particular mapping is called. - - """ - - def init(self, target, args, kwargs): - """Receive an instance when it's constructor is called. - - This method is only called during a userland construction of - an object. It is not called when an object is loaded from the - database. - - """ - - def init_failure(self, target, args, kwargs): - """Receive an instance when it's constructor has been called, - and raised an exception. - - This method is only called during a userland construction of - an object. It is not called when an object is loaded from the - database. - - """ - - def load(self, target, context): - """Receive an object instance after it has been created via - ``__new__``, and after initial attribute population has - occurred. - - This typically occurs when the instance is created based on - incoming result rows, and is only called once for that - instance's lifetime. - - Note that during a result-row load, this method is called upon - the first row received for this instance. Note that some - attributes and collections may or may not be loaded or even - initialized, depending on what's present in the result rows. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param context: the :class:`.QueryContext` corresponding to the - current :class:`.Query` in progress. This argument may be - ``None`` if the load does not correspond to a :class:`.Query`, - such as during :meth:`.Session.merge`. - - """ - - def refresh(self, target, context, attrs): - """Receive an object instance after one or more attributes have - been refreshed from a query. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param context: the :class:`.QueryContext` corresponding to the - current :class:`.Query` in progress. - :param attrs: iterable collection of attribute names which - were populated, or None if all column-mapped, non-deferred - attributes were populated. - - """ - - def expire(self, target, attrs): - """Receive an object instance after its attributes or some subset - have been expired. - - 'keys' is a list of attribute names. If None, the entire - state was expired. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param attrs: iterable collection of attribute - names which were expired, or None if all attributes were - expired. - - """ - - def resurrect(self, target): - """Receive an object instance as it is 'resurrected' from - garbage collection, which occurs when a "dirty" state falls - out of scope. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - - """ - - def pickle(self, target, state_dict): - """Receive an object instance when its associated state is - being pickled. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param state_dict: the dictionary returned by - :class:`.InstanceState.__getstate__`, containing the state - to be pickled. - - """ - - def unpickle(self, target, state_dict): - """Receive an object instance after it's associated state has - been unpickled. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param state_dict: the dictionary sent to - :class:`.InstanceState.__setstate__`, containing the state - dictionary which was pickled. - - """ - -class _EventsHold(event.RefCollection): - """Hold onto listeners against unmapped, uninstrumented classes. - - Establish _listen() for that class' mapper/instrumentation when - those objects are created for that class. - - """ - def __init__(self, class_): - self.class_ = class_ - - @classmethod - def _clear(cls): - cls.all_holds.clear() - - class HoldEvents(object): - _dispatch_target = None - - @classmethod - def _listen(cls, event_key, raw=False, propagate=False): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - if target.class_ in target.all_holds: - collection = target.all_holds[target.class_] - else: - collection = target.all_holds[target.class_] = {} - - event.registry._stored_in_collection(event_key, target) - collection[event_key._key] = (event_key, raw, propagate) - - if propagate: - stack = list(target.class_.__subclasses__()) - while stack: - subclass = stack.pop(0) - stack.extend(subclass.__subclasses__()) - subject = target.resolve(subclass) - if subject is not None: - event_key.with_dispatch_target(subject).\ - listen(raw=raw, propagate=propagate) - - def remove(self, event_key): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - collection = target.all_holds[target.class_] - del collection[event_key._key] - - @classmethod - def populate(cls, class_, subject): - for subclass in class_.__mro__: - if subclass in cls.all_holds: - collection = cls.all_holds[subclass] - for event_key, raw, propagate in collection.values(): - if propagate or subclass is class_: - # since we can't be sure in what order different classes - # in a hierarchy are triggered with populate(), - # we rely upon _EventsHold for all event - # assignment, instead of using the generic propagate - # flag. - event_key.with_dispatch_target(subject).\ - listen(raw=raw, propagate=False) - - -class _InstanceEventsHold(_EventsHold): - all_holds = weakref.WeakKeyDictionary() - - def resolve(self, class_): - return instrumentation.manager_of_class(class_) - - class HoldInstanceEvents(_EventsHold.HoldEvents, InstanceEvents): - pass - - dispatch = event.dispatcher(HoldInstanceEvents) - - -class MapperEvents(event.Events): - """Define events specific to mappings. - - e.g.:: - - from sqlalchemy import event - - def my_before_insert_listener(mapper, connection, target): - # execute a stored procedure upon INSERT, - # apply the value to the row to be inserted - target.calculated_value = connection.scalar( - "select my_special_function(%d)" - % target.special_number) - - # associate the listener function with SomeClass, - # to execute during the "before_insert" hook - event.listen( - SomeClass, 'before_insert', my_before_insert_listener) - - Available targets include: - - * mapped classes - * unmapped superclasses of mapped or to-be-mapped classes - (using the ``propagate=True`` flag) - * :class:`.Mapper` objects - * the :class:`.Mapper` class itself and the :func:`.mapper` - function indicate listening for all mappers. - - .. versionchanged:: 0.8.0 mapper events can be associated with - unmapped superclasses of mapped classes. - - Mapper events provide hooks into critical sections of the - mapper, including those related to object instrumentation, - object loading, and object persistence. In particular, the - persistence methods :meth:`~.MapperEvents.before_insert`, - and :meth:`~.MapperEvents.before_update` are popular - places to augment the state being persisted - however, these - methods operate with several significant restrictions. The - user is encouraged to evaluate the - :meth:`.SessionEvents.before_flush` and - :meth:`.SessionEvents.after_flush` methods as more - flexible and user-friendly hooks in which to apply - additional database state during a flush. - - When using :class:`.MapperEvents`, several modifiers are - available to the :func:`.event.listen` function. - - :param propagate=False: When True, the event listener should - be applied to all inheriting mappers and/or the mappers of - inheriting classes, as well as any - mapper which is the target of this listener. - :param raw=False: When True, the "target" argument passed - to applicable event listener functions will be the - instance's :class:`.InstanceState` management - object, rather than the mapped instance itself. - :param retval=False: when True, the user-defined event function - must have a return value, the purpose of which is either to - control subsequent event propagation, or to otherwise alter - the operation in progress by the mapper. Possible return - values are: - - * ``sqlalchemy.orm.interfaces.EXT_CONTINUE`` - continue event - processing normally. - * ``sqlalchemy.orm.interfaces.EXT_STOP`` - cancel all subsequent - event handlers in the chain. - * other values - the return value specified by specific listeners, - such as :meth:`~.MapperEvents.translate_row` or - :meth:`~.MapperEvents.create_instance`. - - """ - - _target_class_doc = "SomeClass" - _dispatch_target = mapperlib.Mapper - - @classmethod - def _new_mapper_instance(cls, class_, mapper): - _MapperEventsHold.populate(class_, mapper) - - @classmethod - @util.dependencies("sqlalchemy.orm") - def _accept_with(cls, orm, target): - if target is orm.mapper: - return mapperlib.Mapper - elif isinstance(target, type): - if issubclass(target, mapperlib.Mapper): - return target - else: - mapper = _mapper_or_none(target) - if mapper is not None: - return mapper - else: - return _MapperEventsHold(target) - else: - return target - - @classmethod - def _listen(cls, event_key, - raw=False, retval=False, propagate=False): - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - if not raw or not retval: - if not raw: - meth = getattr(cls, identifier) - try: - target_index = \ - inspect.getargspec(meth)[0].index('target') - 1 - except ValueError: - target_index = None - - wrapped_fn = fn - - def wrap(*arg, **kw): - if not raw and target_index is not None: - arg = list(arg) - arg[target_index] = arg[target_index].obj() - if not retval: - wrapped_fn(*arg, **kw) - return interfaces.EXT_CONTINUE - else: - return wrapped_fn(*arg, **kw) - fn = wrap - event_key = event_key.with_wrapper(wrap) - - if propagate: - for mapper in target.self_and_descendants: - event_key.with_dispatch_target(mapper).base_listen(propagate=True) - else: - event_key.base_listen() - - @classmethod - def _clear(cls): - super(MapperEvents, cls)._clear() - _MapperEventsHold._clear() - - def instrument_class(self, mapper, class_): - """Receive a class when the mapper is first constructed, - before instrumentation is applied to the mapped class. - - This event is the earliest phase of mapper construction. - Most attributes of the mapper are not yet initialized. - - This listener can either be applied to the :class:`.Mapper` - class overall, or to any un-mapped class which serves as a base - for classes that will be mapped (using the ``propagate=True`` flag):: - - Base = declarative_base() - - @event.listens_for(Base, "instrument_class", propagate=True) - def on_new_class(mapper, cls_): - " ... " - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param class\_: the mapped class. - - """ - - def mapper_configured(self, mapper, class_): - """Called when the mapper for the class is fully configured. - - This event is the latest phase of mapper construction, and - is invoked when the mapped classes are first used, so that - relationships between mappers can be resolved. When the event is - called, the mapper should be in its final state. - - While the configuration event normally occurs automatically, - it can be forced to occur ahead of time, in the case where the event - is needed before any actual mapper usage, by using the - :func:`.configure_mappers` function. - - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param class\_: the mapped class. - - """ - # TODO: need coverage for this event - - def after_configured(self): - """Called after a series of mappers have been configured. - - This corresponds to the :func:`.orm.configure_mappers` call, which - note is usually called automatically as mappings are first - used. - - Theoretically this event is called once per - application, but is actually called any time new mappers - have been affected by a :func:`.orm.configure_mappers` - call. If new mappings are constructed after existing ones have - already been used, this event can be called again. - - """ - - def translate_row(self, mapper, context, row): - """Perform pre-processing on the given result row and return a - new row instance. - - This listener is typically registered with ``retval=True``. - It is called when the mapper first receives a row, before - the object identity or the instance itself has been derived - from that row. The given row may or may not be a - :class:`.RowProxy` object - it will always be a dictionary-like - object which contains mapped columns as keys. The - returned object should also be a dictionary-like object - which recognizes mapped columns as keys. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param context: the :class:`.QueryContext`, which includes - a handle to the current :class:`.Query` in progress as well - as additional state information. - :param row: the result row being handled. This may be - an actual :class:`.RowProxy` or may be a dictionary containing - :class:`.Column` objects as keys. - :return: When configured with ``retval=True``, the function - should return a dictionary-like row object, or ``EXT_CONTINUE``, - indicating the original row should be used. - - - """ - - def create_instance(self, mapper, context, row, class_): - """Receive a row when a new object instance is about to be - created from that row. - - The method can choose to create the instance itself, or it can return - EXT_CONTINUE to indicate normal object creation should take place. - This listener is typically registered with ``retval=True``. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param context: the :class:`.QueryContext`, which includes - a handle to the current :class:`.Query` in progress as well - as additional state information. - :param row: the result row being handled. This may be - an actual :class:`.RowProxy` or may be a dictionary containing - :class:`.Column` objects as keys. - :param class\_: the mapped class. - :return: When configured with ``retval=True``, the return value - should be a newly created instance of the mapped class, - or ``EXT_CONTINUE`` indicating that default object construction - should take place. - - """ - - def append_result(self, mapper, context, row, target, - result, **flags): - """Receive an object instance before that instance is appended - to a result list. - - This is a rarely used hook which can be used to alter - the construction of a result list returned by :class:`.Query`. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param context: the :class:`.QueryContext`, which includes - a handle to the current :class:`.Query` in progress as well - as additional state information. - :param row: the result row being handled. This may be - an actual :class:`.RowProxy` or may be a dictionary containing - :class:`.Column` objects as keys. - :param target: the mapped instance being populated. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :param result: a list-like object where results are being - appended. - :param \**flags: Additional state information about the - current handling of the row. - :return: If this method is registered with ``retval=True``, - a return value of ``EXT_STOP`` will prevent the instance - from being appended to the given result list, whereas a - return value of ``EXT_CONTINUE`` will result in the default - behavior of appending the value to the result list. - - """ - - def populate_instance(self, mapper, context, row, - target, **flags): - """Receive an instance before that instance has - its attributes populated. - - This usually corresponds to a newly loaded instance but may - also correspond to an already-loaded instance which has - unloaded attributes to be populated. The method may be called - many times for a single instance, as multiple result rows are - used to populate eagerly loaded collections. - - Most usages of this hook are obsolete. For a - generic "object has been newly created from a row" hook, use - :meth:`.InstanceEvents.load`. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param context: the :class:`.QueryContext`, which includes - a handle to the current :class:`.Query` in progress as well - as additional state information. - :param row: the result row being handled. This may be - an actual :class:`.RowProxy` or may be a dictionary containing - :class:`.Column` objects as keys. - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: When configured with ``retval=True``, a return - value of ``EXT_STOP`` will bypass instance population by - the mapper. A value of ``EXT_CONTINUE`` indicates that - default instance population should take place. - - """ - - def before_insert(self, mapper, connection, target): - """Receive an object instance before an INSERT statement - is emitted corresponding to that instance. - - This event is used to modify local, non-object related - attributes on the instance before an INSERT occurs, as well - as to emit additional SQL statements on the given - connection. - - The event is often called for a batch of objects of the - same class before their INSERT statements are emitted at - once in a later step. In the extremely rare case that - this is not desirable, the :func:`.mapper` can be - configured with ``batch=False``, which will cause - batches of instances to be broken up into individual - (and more poorly performing) event->persist->event - steps. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** - :class:`.Connection` **only.** Handlers here should **not** make - alterations to the state of the :class:`.Session` overall, and - in general should not affect any :func:`.relationship` -mapped - attributes, as session cascade rules will not function properly, - nor is it always known if the related class has already been - handled. Operations that **are not supported in mapper - events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, or - another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit INSERT statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being persisted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - - """ - - def after_insert(self, mapper, connection, target): - """Receive an object instance after an INSERT statement - is emitted corresponding to that instance. - - This event is used to modify in-Python-only - state on the instance after an INSERT occurs, as well - as to emit additional SQL statements on the given - connection. - - The event is often called for a batch of objects of the - same class after their INSERT statements have been - emitted at once in a previous step. In the extremely - rare case that this is not desirable, the - :func:`.mapper` can be configured with ``batch=False``, - which will cause batches of instances to be broken up - into individual (and more poorly performing) - event->persist->event steps. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** - :class:`.Connection` **only.** Handlers here should **not** make - alterations to the state of the :class:`.Session` overall, and in - general should not affect any :func:`.relationship` -mapped - attributes, as session cascade rules will not function properly, - nor is it always known if the related class has already been - handled. Operations that **are not supported in mapper - events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, - or another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit INSERT statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being persisted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - - """ - - def before_update(self, mapper, connection, target): - """Receive an object instance before an UPDATE statement - is emitted corresponding to that instance. - - This event is used to modify local, non-object related - attributes on the instance before an UPDATE occurs, as well - as to emit additional SQL statements on the given - connection. - - This method is called for all instances that are - marked as "dirty", *even those which have no net changes - to their column-based attributes*. An object is marked - as dirty when any of its column-based attributes have a - "set attribute" operation called or when any of its - collections are modified. If, at update time, no - column-based attributes have any net changes, no UPDATE - statement will be issued. This means that an instance - being sent to :meth:`~.MapperEvents.before_update` is - *not* a guarantee that an UPDATE statement will be - issued, although you can affect the outcome here by - modifying attributes so that a net change in value does - exist. - - To detect if the column-based attributes on the object have net - changes, and will therefore generate an UPDATE statement, use - ``object_session(instance).is_modified(instance, - include_collections=False)``. - - The event is often called for a batch of objects of the - same class before their UPDATE statements are emitted at - once in a later step. In the extremely rare case that - this is not desirable, the :func:`.mapper` can be - configured with ``batch=False``, which will cause - batches of instances to be broken up into individual - (and more poorly performing) event->persist->event - steps. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` - **only.** Handlers here should **not** make alterations to the - state of the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, - or another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit UPDATE statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being persisted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - """ - - def after_update(self, mapper, connection, target): - """Receive an object instance after an UPDATE statement - is emitted corresponding to that instance. - - This event is used to modify in-Python-only - state on the instance after an UPDATE occurs, as well - as to emit additional SQL statements on the given - connection. - - This method is called for all instances that are - marked as "dirty", *even those which have no net changes - to their column-based attributes*, and for which - no UPDATE statement has proceeded. An object is marked - as dirty when any of its column-based attributes have a - "set attribute" operation called or when any of its - collections are modified. If, at update time, no - column-based attributes have any net changes, no UPDATE - statement will be issued. This means that an instance - being sent to :meth:`~.MapperEvents.after_update` is - *not* a guarantee that an UPDATE statement has been - issued. - - To detect if the column-based attributes on the object have net - changes, and therefore resulted in an UPDATE statement, use - ``object_session(instance).is_modified(instance, - include_collections=False)``. - - The event is often called for a batch of objects of the - same class after their UPDATE statements have been emitted at - once in a previous step. In the extremely rare case that - this is not desirable, the :func:`.mapper` can be - configured with ``batch=False``, which will cause - batches of instances to be broken up into individual - (and more poorly performing) event->persist->event - steps. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` - **only.** Handlers here should **not** make alterations to the - state of the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, - or another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit UPDATE statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being persisted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - - """ - - def before_delete(self, mapper, connection, target): - """Receive an object instance before a DELETE statement - is emitted corresponding to that instance. - - This event is used to emit additional SQL statements on - the given connection as well as to perform application - specific bookkeeping related to a deletion event. - - The event is often called for a batch of objects of the - same class before their DELETE statements are emitted at - once in a later step. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` - **only.** Handlers here should **not** make alterations to the - state of the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, - or another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit DELETE statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being deleted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - - """ - - def after_delete(self, mapper, connection, target): - """Receive an object instance after a DELETE statement - has been emitted corresponding to that instance. - - This event is used to emit additional SQL statements on - the given connection as well as to perform application - specific bookkeeping related to a deletion event. - - The event is often called for a batch of objects of the - same class after their DELETE statements have been emitted at - once in a previous step. - - .. warning:: - Mapper-level flush events are designed to operate **on attributes - local to the immediate object being handled - and via SQL operations with the given** :class:`.Connection` - **only.** Handlers here should **not** make alterations to the - state of the :class:`.Session` overall, and in general should not - affect any :func:`.relationship` -mapped attributes, as - session cascade rules will not function properly, nor is it - always known if the related class has already been handled. - Operations that **are not supported in mapper events** include: - - * :meth:`.Session.add` - * :meth:`.Session.delete` - * Mapped collection append, add, remove, delete, discard, etc. - * Mapped relationship attribute set/del events, - i.e. ``someobject.related = someotherobject`` - - Operations which manipulate the state of the object - relative to other objects are better handled: - - * In the ``__init__()`` method of the mapped object itself, - or another method designed to establish some particular state. - * In a ``@validates`` handler, see :ref:`simple_validators` - * Within the :meth:`.SessionEvents.before_flush` event. - - :param mapper: the :class:`.Mapper` which is the target - of this event. - :param connection: the :class:`.Connection` being used to - emit DELETE statements for this instance. This - provides a handle into the current transaction on the - target database specific to this instance. - :param target: the mapped instance being deleted. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - :return: No return value is supported by this event. - - """ - -class _MapperEventsHold(_EventsHold): - all_holds = weakref.WeakKeyDictionary() - - def resolve(self, class_): - return _mapper_or_none(class_) - - class HoldMapperEvents(_EventsHold.HoldEvents, MapperEvents): - pass - - dispatch = event.dispatcher(HoldMapperEvents) - - -class SessionEvents(event.Events): - """Define events specific to :class:`.Session` lifecycle. - - e.g.:: - - from sqlalchemy import event - from sqlalchemy.orm import sessionmaker - - def my_before_commit(session): - print "before commit!" - - Session = sessionmaker() - - event.listen(Session, "before_commit", my_before_commit) - - The :func:`~.event.listen` function will accept - :class:`.Session` objects as well as the return result - of :class:`~.sessionmaker()` and :class:`~.scoped_session()`. - - Additionally, it accepts the :class:`.Session` class which - will apply listeners to all :class:`.Session` instances - globally. - - """ - - _target_class_doc = "SomeSessionOrFactory" - - _dispatch_target = Session - - @classmethod - def _accept_with(cls, target): - if isinstance(target, scoped_session): - - target = target.session_factory - if not isinstance(target, sessionmaker) and \ - ( - not isinstance(target, type) or - not issubclass(target, Session) - ): - raise exc.ArgumentError( - "Session event listen on a scoped_session " - "requires that its creation callable " - "is associated with the Session class.") - - if isinstance(target, sessionmaker): - return target.class_ - elif isinstance(target, type): - if issubclass(target, scoped_session): - return Session - elif issubclass(target, Session): - return target - elif isinstance(target, Session): - return target - else: - return None - - def after_transaction_create(self, session, transaction): - """Execute when a new :class:`.SessionTransaction` is created. - - This event differs from :meth:`~.SessionEvents.after_begin` - in that it occurs for each :class:`.SessionTransaction` - overall, as opposed to when transactions are begun - on individual database connections. It is also invoked - for nested transactions and subtransactions, and is always - matched by a corresponding - :meth:`~.SessionEvents.after_transaction_end` event - (assuming normal operation of the :class:`.Session`). - - :param session: the target :class:`.Session`. - :param transaction: the target :class:`.SessionTransaction`. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`~.SessionEvents.after_transaction_end` - - """ - - def after_transaction_end(self, session, transaction): - """Execute when the span of a :class:`.SessionTransaction` ends. - - This event differs from :meth:`~.SessionEvents.after_commit` - in that it corresponds to all :class:`.SessionTransaction` - objects in use, including those for nested transactions - and subtransactions, and is always matched by a corresponding - :meth:`~.SessionEvents.after_transaction_create` event. - - :param session: the target :class:`.Session`. - :param transaction: the target :class:`.SessionTransaction`. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`~.SessionEvents.after_transaction_create` - - """ - - def before_commit(self, session): - """Execute before commit is called. - - .. note:: - - The :meth:`~.SessionEvents.before_commit` hook is *not* per-flush, - that is, the :class:`.Session` can emit SQL to the database - many times within the scope of a transaction. - For interception of these events, use the :meth:`~.SessionEvents.before_flush`, - :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec` - events. - - :param session: The target :class:`.Session`. - - .. seealso:: - - :meth:`~.SessionEvents.after_commit` - - :meth:`~.SessionEvents.after_begin` - - :meth:`~.SessionEvents.after_transaction_create` - - :meth:`~.SessionEvents.after_transaction_end` - - """ - - def after_commit(self, session): - """Execute after a commit has occurred. - - .. note:: - - The :meth:`~.SessionEvents.after_commit` hook is *not* per-flush, - that is, the :class:`.Session` can emit SQL to the database - many times within the scope of a transaction. - For interception of these events, use the :meth:`~.SessionEvents.before_flush`, - :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec` - events. - - .. note:: - - The :class:`.Session` is not in an active tranasction - when the :meth:`~.SessionEvents.after_commit` event is invoked, and therefore - can not emit SQL. To emit SQL corresponding to every transaction, - use the :meth:`~.SessionEvents.before_commit` event. - - :param session: The target :class:`.Session`. - - .. seealso:: - - :meth:`~.SessionEvents.before_commit` - - :meth:`~.SessionEvents.after_begin` - - :meth:`~.SessionEvents.after_transaction_create` - - :meth:`~.SessionEvents.after_transaction_end` - - """ - - def after_rollback(self, session): - """Execute after a real DBAPI rollback has occurred. - - Note that this event only fires when the *actual* rollback against - the database occurs - it does *not* fire each time the - :meth:`.Session.rollback` method is called, if the underlying - DBAPI transaction has already been rolled back. In many - cases, the :class:`.Session` will not be in - an "active" state during this event, as the current - transaction is not valid. To acquire a :class:`.Session` - which is active after the outermost rollback has proceeded, - use the :meth:`.SessionEvents.after_soft_rollback` event, checking the - :attr:`.Session.is_active` flag. - - :param session: The target :class:`.Session`. - - """ - - def after_soft_rollback(self, session, previous_transaction): - """Execute after any rollback has occurred, including "soft" - rollbacks that don't actually emit at the DBAPI level. - - This corresponds to both nested and outer rollbacks, i.e. - the innermost rollback that calls the DBAPI's - rollback() method, as well as the enclosing rollback - calls that only pop themselves from the transaction stack. - - The given :class:`.Session` can be used to invoke SQL and - :meth:`.Session.query` operations after an outermost rollback - by first checking the :attr:`.Session.is_active` flag:: - - @event.listens_for(Session, "after_soft_rollback") - def do_something(session, previous_transaction): - if session.is_active: - session.execute("select * from some_table") - - :param session: The target :class:`.Session`. - :param previous_transaction: The :class:`.SessionTransaction` - transactional marker object which was just closed. The current - :class:`.SessionTransaction` for the given :class:`.Session` is - available via the :attr:`.Session.transaction` attribute. - - .. versionadded:: 0.7.3 - - """ - - def before_flush(self, session, flush_context, instances): - """Execute before flush process has started. - - :param session: The target :class:`.Session`. - :param flush_context: Internal :class:`.UOWTransaction` object - which handles the details of the flush. - :param instances: Usually ``None``, this is the collection of - objects which can be passed to the :meth:`.Session.flush` method - (note this usage is deprecated). - - .. seealso:: - - :meth:`~.SessionEvents.after_flush` - - :meth:`~.SessionEvents.after_flush_postexec` - - """ - - def after_flush(self, session, flush_context): - """Execute after flush has completed, but before commit has been - called. - - Note that the session's state is still in pre-flush, i.e. 'new', - 'dirty', and 'deleted' lists still show pre-flush state as well - as the history settings on instance attributes. - - :param session: The target :class:`.Session`. - :param flush_context: Internal :class:`.UOWTransaction` object - which handles the details of the flush. - - .. seealso:: - - :meth:`~.SessionEvents.before_flush` - - :meth:`~.SessionEvents.after_flush_postexec` - - """ - - def after_flush_postexec(self, session, flush_context): - """Execute after flush has completed, and after the post-exec - state occurs. - - This will be when the 'new', 'dirty', and 'deleted' lists are in - their final state. An actual commit() may or may not have - occurred, depending on whether or not the flush started its own - transaction or participated in a larger transaction. - - :param session: The target :class:`.Session`. - :param flush_context: Internal :class:`.UOWTransaction` object - which handles the details of the flush. - - - .. seealso:: - - :meth:`~.SessionEvents.before_flush` - - :meth:`~.SessionEvents.after_flush` - - """ - - def after_begin(self, session, transaction, connection): - """Execute after a transaction is begun on a connection - - :param session: The target :class:`.Session`. - :param transaction: The :class:`.SessionTransaction`. - :param connection: The :class:`~.engine.Connection` object - which will be used for SQL statements. - - .. seealso:: - - :meth:`~.SessionEvents.before_commit` - - :meth:`~.SessionEvents.after_commit` - - :meth:`~.SessionEvents.after_transaction_create` - - :meth:`~.SessionEvents.after_transaction_end` - - """ - - def before_attach(self, session, instance): - """Execute before an instance is attached to a session. - - This is called before an add, delete or merge causes - the object to be part of the session. - - .. versionadded:: 0.8. Note that :meth:`~.SessionEvents.after_attach` now - fires off after the item is part of the session. - :meth:`.before_attach` is provided for those cases where - the item should not yet be part of the session state. - - .. seealso:: - - :meth:`~.SessionEvents.after_attach` - - """ - - def after_attach(self, session, instance): - """Execute after an instance is attached to a session. - - This is called after an add, delete or merge. - - .. note:: - - As of 0.8, this event fires off *after* the item - has been fully associated with the session, which is - different than previous releases. For event - handlers that require the object not yet - be part of session state (such as handlers which - may autoflush while the target object is not - yet complete) consider the - new :meth:`.before_attach` event. - - .. seealso:: - - :meth:`~.SessionEvents.before_attach` - - """ - - @event._legacy_signature("0.9", - ["session", "query", "query_context", "result"], - lambda update_context: ( - update_context.session, - update_context.query, - update_context.context, - update_context.result)) - def after_bulk_update(self, update_context): - """Execute after a bulk update operation to the session. - - This is called as a result of the :meth:`.Query.update` method. - - :param update_context: an "update context" object which contains - details about the update, including these attributes: - - * ``session`` - the :class:`.Session` involved - * ``query`` -the :class:`.Query` object that this update operation was - called upon. - * ``context`` The :class:`.QueryContext` object, corresponding - to the invocation of an ORM query. - * ``result`` the :class:`.ResultProxy` returned as a result of the - bulk UPDATE operation. - - - """ - - @event._legacy_signature("0.9", - ["session", "query", "query_context", "result"], - lambda delete_context: ( - delete_context.session, - delete_context.query, - delete_context.context, - delete_context.result)) - def after_bulk_delete(self, delete_context): - """Execute after a bulk delete operation to the session. - - This is called as a result of the :meth:`.Query.delete` method. - - :param delete_context: a "delete context" object which contains - details about the update, including these attributes: - - * ``session`` - the :class:`.Session` involved - * ``query`` -the :class:`.Query` object that this update operation was - called upon. - * ``context`` The :class:`.QueryContext` object, corresponding - to the invocation of an ORM query. - * ``result`` the :class:`.ResultProxy` returned as a result of the - bulk DELETE operation. - - - """ - - -class AttributeEvents(event.Events): - """Define events for object attributes. - - These are typically defined on the class-bound descriptor for the - target class. - - e.g.:: - - from sqlalchemy import event - - def my_append_listener(target, value, initiator): - print "received append event for target: %s" % target - - event.listen(MyClass.collection, 'append', my_append_listener) - - Listeners have the option to return a possibly modified version - of the value, when the ``retval=True`` flag is passed - to :func:`~.event.listen`:: - - def validate_phone(target, value, oldvalue, initiator): - "Strip non-numeric characters from a phone number" - - return re.sub(r'(?![0-9])', '', value) - - # setup listener on UserContact.phone attribute, instructing - # it to use the return value - listen(UserContact.phone, 'set', validate_phone, retval=True) - - A validation function like the above can also raise an exception - such as :exc:`ValueError` to halt the operation. - - Several modifiers are available to the :func:`~.event.listen` function. - - :param active_history=False: When True, indicates that the - "set" event would like to receive the "old" value being - replaced unconditionally, even if this requires firing off - database loads. Note that ``active_history`` can also be - set directly via :func:`.column_property` and - :func:`.relationship`. - - :param propagate=False: When True, the listener function will - be established not just for the class attribute given, but - for attributes of the same name on all current subclasses - of that class, as well as all future subclasses of that - class, using an additional listener that listens for - instrumentation events. - :param raw=False: When True, the "target" argument to the - event will be the :class:`.InstanceState` management - object, rather than the mapped instance itself. - :param retval=False: when True, the user-defined event - listening must return the "value" argument from the - function. This gives the listening function the opportunity - to change the value that is ultimately used for a "set" - or "append" event. - - """ - - _target_class_doc = "SomeClass.some_attribute" - _dispatch_target = QueryableAttribute - - @staticmethod - def _set_dispatch(cls, dispatch_cls): - event.Events._set_dispatch(cls, dispatch_cls) - dispatch_cls._active_history = False - - @classmethod - def _accept_with(cls, target): - # TODO: coverage - if isinstance(target, interfaces.MapperProperty): - return getattr(target.parent.class_, target.key) - else: - return target - - @classmethod - def _listen(cls, event_key, active_history=False, - raw=False, retval=False, - propagate=False): - - target, identifier, fn = \ - event_key.dispatch_target, event_key.identifier, event_key.fn - - if active_history: - target.dispatch._active_history = True - - if not raw or not retval: - orig_fn = fn - - def wrap(target, value, *arg): - if not raw: - target = target.obj() - if not retval: - orig_fn(target, value, *arg) - return value - else: - return orig_fn(target, value, *arg) - fn = wrap - event_key = event_key.with_wrapper(wrap) - - event_key.base_listen(propagate=propagate) - - if propagate: - manager = instrumentation.manager_of_class(target.class_) - - for mgr in manager.subclass_managers(True): - event_key.with_dispatch_target(mgr[target.key]).base_listen(propagate=True) - - def append(self, target, value, initiator): - """Receive a collection append event. - - :param target: the object instance receiving the event. - If the listener is registered with ``raw=True``, this will - be the :class:`.InstanceState` object. - :param value: the value being appended. If this listener - is registered with ``retval=True``, the listener - function must return this value, or a new value which - replaces it. - :param initiator: An instance of :class:`.attributes.Event` - representing the initiation of the event. May be modified - from it's original value by backref handlers in order to control - chained event propagation. - - .. versionchanged:: 0.9.0 the ``initiator`` argument is now - passed as a :class:`.attributes.Event` object, and may be modified - by backref handlers within a chain of backref-linked events. - - :return: if the event was registered with ``retval=True``, - the given value, or a new effective value, should be returned. - - """ - - def remove(self, target, value, initiator): - """Receive a collection remove event. - - :param target: the object instance receiving the event. - If the listener is registered with ``raw=True``, this will - be the :class:`.InstanceState` object. - :param value: the value being removed. - :param initiator: An instance of :class:`.attributes.Event` - representing the initiation of the event. May be modified - from it's original value by backref handlers in order to control - chained event propagation. - - .. versionchanged:: 0.9.0 the ``initiator`` argument is now - passed as a :class:`.attributes.Event` object, and may be modified - by backref handlers within a chain of backref-linked events. - - :return: No return value is defined for this event. - """ - - def set(self, target, value, oldvalue, initiator): - """Receive a scalar set event. - - :param target: the object instance receiving the event. - If the listener is registered with ``raw=True``, this will - be the :class:`.InstanceState` object. - :param value: the value being set. If this listener - is registered with ``retval=True``, the listener - function must return this value, or a new value which - replaces it. - :param oldvalue: the previous value being replaced. This - may also be the symbol ``NEVER_SET`` or ``NO_VALUE``. - If the listener is registered with ``active_history=True``, - the previous value of the attribute will be loaded from - the database if the existing value is currently unloaded - or expired. - :param initiator: An instance of :class:`.attributes.Event` - representing the initiation of the event. May be modified - from it's original value by backref handlers in order to control - chained event propagation. - - .. versionchanged:: 0.9.0 the ``initiator`` argument is now - passed as a :class:`.attributes.Event` object, and may be modified - by backref handlers within a chain of backref-linked events. - - :return: if the event was registered with ``retval=True``, - the given value, or a new effective value, should be returned. - - """ - diff --git a/libs/sqlalchemy/orm/exc.py b/libs/sqlalchemy/orm/exc.py deleted file mode 100644 index d1ef1ded..00000000 --- a/libs/sqlalchemy/orm/exc.py +++ /dev/null @@ -1,163 +0,0 @@ -# orm/exc.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""SQLAlchemy ORM exceptions.""" -from .. import exc as sa_exc, util - -NO_STATE = (AttributeError, KeyError) -"""Exception types that may be raised by instrumentation implementations.""" - - -class StaleDataError(sa_exc.SQLAlchemyError): - """An operation encountered database state that is unaccounted for. - - Conditions which cause this to happen include: - - * A flush may have attempted to update or delete rows - and an unexpected number of rows were matched during - the UPDATE or DELETE statement. Note that when - version_id_col is used, rows in UPDATE or DELETE statements - are also matched against the current known version - identifier. - - * A mapped object with version_id_col was refreshed, - and the version number coming back from the database does - not match that of the object itself. - - * A object is detached from its parent object, however - the object was previously attached to a different parent - identity which was garbage collected, and a decision - cannot be made if the new parent was really the most - recent "parent". - - .. versionadded:: 0.7.4 - - """ - -ConcurrentModificationError = StaleDataError - - -class FlushError(sa_exc.SQLAlchemyError): - """A invalid condition was detected during flush().""" - - -class UnmappedError(sa_exc.InvalidRequestError): - """Base for exceptions that involve expected mappings not present.""" - - -class ObjectDereferencedError(sa_exc.SQLAlchemyError): - """An operation cannot complete due to an object being garbage - collected. - - """ - - -class DetachedInstanceError(sa_exc.SQLAlchemyError): - """An attempt to access unloaded attributes on a - mapped instance that is detached.""" - - -class UnmappedInstanceError(UnmappedError): - """An mapping operation was requested for an unknown instance.""" - - @util.dependencies("sqlalchemy.orm.base") - def __init__(self, base, obj, msg=None): - if not msg: - try: - base.class_mapper(type(obj)) - name = _safe_cls_name(type(obj)) - msg = ("Class %r is mapped, but this instance lacks " - "instrumentation. This occurs when the instance" - "is created before sqlalchemy.orm.mapper(%s) " - "was called." % (name, name)) - except UnmappedClassError: - msg = _default_unmapped(type(obj)) - if isinstance(obj, type): - msg += ( - '; was a class (%s) supplied where an instance was ' - 'required?' % _safe_cls_name(obj)) - UnmappedError.__init__(self, msg) - - def __reduce__(self): - return self.__class__, (None, self.args[0]) - - -class UnmappedClassError(UnmappedError): - """An mapping operation was requested for an unknown class.""" - - def __init__(self, cls, msg=None): - if not msg: - msg = _default_unmapped(cls) - UnmappedError.__init__(self, msg) - - def __reduce__(self): - return self.__class__, (None, self.args[0]) - - -class ObjectDeletedError(sa_exc.InvalidRequestError): - """A refresh operation failed to retrieve the database - row corresponding to an object's known primary key identity. - - A refresh operation proceeds when an expired attribute is - accessed on an object, or when :meth:`.Query.get` is - used to retrieve an object which is, upon retrieval, detected - as expired. A SELECT is emitted for the target row - based on primary key; if no row is returned, this - exception is raised. - - The true meaning of this exception is simply that - no row exists for the primary key identifier associated - with a persistent object. The row may have been - deleted, or in some cases the primary key updated - to a new value, outside of the ORM's management of the target - object. - - """ - @util.dependencies("sqlalchemy.orm.base") - def __init__(self, base, state, msg=None): - if not msg: - msg = "Instance '%s' has been deleted, or its "\ - "row is otherwise not present." % base.state_str(state) - - sa_exc.InvalidRequestError.__init__(self, msg) - - def __reduce__(self): - return self.__class__, (None, self.args[0]) - - -class UnmappedColumnError(sa_exc.InvalidRequestError): - """Mapping operation was requested on an unknown column.""" - - -class NoResultFound(sa_exc.InvalidRequestError): - """A database result was required but none was found.""" - - -class MultipleResultsFound(sa_exc.InvalidRequestError): - """A single database result was required but more than one were found.""" - - -def _safe_cls_name(cls): - try: - cls_name = '.'.join((cls.__module__, cls.__name__)) - except AttributeError: - cls_name = getattr(cls, '__name__', None) - if cls_name is None: - cls_name = repr(cls) - return cls_name - -@util.dependencies("sqlalchemy.orm.base") -def _default_unmapped(base, cls): - try: - mappers = base.manager_of_class(cls).mappers - except NO_STATE: - mappers = {} - except TypeError: - mappers = {} - name = _safe_cls_name(cls) - - if not mappers: - return "Class '%s' is not mapped" % name diff --git a/libs/sqlalchemy/orm/identity.py b/libs/sqlalchemy/orm/identity.py deleted file mode 100644 index a91085d2..00000000 --- a/libs/sqlalchemy/orm/identity.py +++ /dev/null @@ -1,240 +0,0 @@ -# orm/identity.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -import weakref -from . import attributes -from .. import util - -class IdentityMap(dict): - def __init__(self): - self._modified = set() - self._wr = weakref.ref(self) - - def replace(self, state): - raise NotImplementedError() - - def add(self, state): - raise NotImplementedError() - - def update(self, dict): - raise NotImplementedError("IdentityMap uses add() to insert data") - - def clear(self): - raise NotImplementedError("IdentityMap uses remove() to remove data") - - def _manage_incoming_state(self, state): - state._instance_dict = self._wr - - if state.modified: - self._modified.add(state) - - def _manage_removed_state(self, state): - del state._instance_dict - self._modified.discard(state) - - def _dirty_states(self): - return self._modified - - def check_modified(self): - """return True if any InstanceStates present have been marked - as 'modified'. - - """ - return bool(self._modified) - - def has_key(self, key): - return key in self - - def popitem(self): - raise NotImplementedError("IdentityMap uses remove() to remove data") - - def pop(self, key, *args): - raise NotImplementedError("IdentityMap uses remove() to remove data") - - def setdefault(self, key, default=None): - raise NotImplementedError("IdentityMap uses add() to insert data") - - def copy(self): - raise NotImplementedError() - - def __setitem__(self, key, value): - raise NotImplementedError("IdentityMap uses add() to insert data") - - def __delitem__(self, key): - raise NotImplementedError("IdentityMap uses remove() to remove data") - - -class WeakInstanceDict(IdentityMap): - def __init__(self): - IdentityMap.__init__(self) - - def __getitem__(self, key): - state = dict.__getitem__(self, key) - o = state.obj() - if o is None: - raise KeyError(key) - return o - - def __contains__(self, key): - try: - if dict.__contains__(self, key): - state = dict.__getitem__(self, key) - o = state.obj() - else: - return False - except KeyError: - return False - else: - return o is not None - - def contains_state(self, state): - return dict.get(self, state.key) is state - - def replace(self, state): - if dict.__contains__(self, state.key): - existing = dict.__getitem__(self, state.key) - if existing is not state: - self._manage_removed_state(existing) - else: - return - - dict.__setitem__(self, state.key, state) - self._manage_incoming_state(state) - - def add(self, state): - key = state.key - # inline of self.__contains__ - if dict.__contains__(self, key): - try: - existing_state = dict.__getitem__(self, key) - if existing_state is not state: - o = existing_state.obj() - if o is not None: - raise AssertionError( - "A conflicting state is already " - "present in the identity map for key %r" - % (key, )) - else: - return - except KeyError: - pass - dict.__setitem__(self, key, state) - self._manage_incoming_state(state) - - def get(self, key, default=None): - state = dict.get(self, key, default) - if state is default: - return default - o = state.obj() - if o is None: - return default - return o - - def _items(self): - values = self.all_states() - result = [] - for state in values: - value = state.obj() - if value is not None: - result.append((state.key, value)) - return result - - def _values(self): - values = self.all_states() - result = [] - for state in values: - value = state.obj() - if value is not None: - result.append(value) - - return result - - if util.py2k: - items = _items - values = _values - - def iteritems(self): - return iter(self.items()) - - def itervalues(self): - return iter(self.values()) - else: - def items(self): - return iter(self._items()) - - def values(self): - return iter(self._values()) - - def all_states(self): - if util.py2k: - return dict.values(self) - else: - return list(dict.values(self)) - - def discard(self, state): - st = dict.get(self, state.key, None) - if st is state: - dict.pop(self, state.key, None) - self._manage_removed_state(state) - - def prune(self): - return 0 - - -class StrongInstanceDict(IdentityMap): - def all_states(self): - return [attributes.instance_state(o) for o in self.values()] - - def contains_state(self, state): - return ( - state.key in self and - attributes.instance_state(self[state.key]) is state) - - def replace(self, state): - if dict.__contains__(self, state.key): - existing = dict.__getitem__(self, state.key) - existing = attributes.instance_state(existing) - if existing is not state: - self._manage_removed_state(existing) - else: - return - - dict.__setitem__(self, state.key, state.obj()) - self._manage_incoming_state(state) - - def add(self, state): - if state.key in self: - if attributes.instance_state(dict.__getitem__(self, - state.key)) is not state: - raise AssertionError('A conflicting state is already ' - 'present in the identity map for key %r' - % (state.key, )) - else: - dict.__setitem__(self, state.key, state.obj()) - self._manage_incoming_state(state) - - def discard(self, state): - obj = dict.get(self, state.key, None) - if obj is not None: - st = attributes.instance_state(obj) - if st is state: - dict.pop(self, state.key, None) - self._manage_removed_state(state) - - def prune(self): - """prune unreferenced, non-dirty states.""" - - ref_count = len(self) - dirty = [s.obj() for s in self.all_states() if s.modified] - - # work around http://bugs.python.org/issue6149 - keepers = weakref.WeakValueDictionary() - keepers.update(self) - - dict.clear(self) - dict.update(self, keepers) - self.modified = bool(dirty) - return ref_count - len(self) diff --git a/libs/sqlalchemy/orm/instrumentation.py b/libs/sqlalchemy/orm/instrumentation.py deleted file mode 100644 index 68b4f061..00000000 --- a/libs/sqlalchemy/orm/instrumentation.py +++ /dev/null @@ -1,499 +0,0 @@ -# orm/instrumentation.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Defines SQLAlchemy's system of class instrumentation. - -This module is usually not directly visible to user applications, but -defines a large part of the ORM's interactivity. - -instrumentation.py deals with registration of end-user classes -for state tracking. It interacts closely with state.py -and attributes.py which establish per-instance and per-class-attribute -instrumentation, respectively. - -The class instrumentation system can be customized on a per-class -or global basis using the :mod:`sqlalchemy.ext.instrumentation` -module, which provides the means to build and specify -alternate instrumentation forms. - -.. versionchanged: 0.8 - The instrumentation extension system was moved out of the - ORM and into the external :mod:`sqlalchemy.ext.instrumentation` - package. When that package is imported, it installs - itself within sqlalchemy.orm so that its more comprehensive - resolution mechanics take effect. - -""" - - -from . import exc, collections, interfaces, state -from .. import util -from . import base - -class ClassManager(dict): - """tracks state information at the class level.""" - - MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR - STATE_ATTR = base.DEFAULT_STATE_ATTR - - deferred_scalar_loader = None - - original_init = object.__init__ - - factory = None - - def __init__(self, class_): - self.class_ = class_ - self.info = {} - self.new_init = None - self.local_attrs = {} - self.originals = {} - - self._bases = [mgr for mgr in [ - manager_of_class(base) - for base in self.class_.__bases__ - if isinstance(base, type) - ] if mgr is not None] - - for base in self._bases: - self.update(base) - - self.dispatch._events._new_classmanager_instance(class_, self) - #events._InstanceEventsHold.populate(class_, self) - - for basecls in class_.__mro__: - mgr = manager_of_class(basecls) - if mgr is not None: - self.dispatch._update(mgr.dispatch) - self.manage() - self._instrument_init() - - if '__del__' in class_.__dict__: - util.warn("__del__() method on class %s will " - "cause unreachable cycles and memory leaks, " - "as SQLAlchemy instrumentation often creates " - "reference cycles. Please remove this method." % - class_) - - def __hash__(self): - return id(self) - - def __eq__(self, other): - return other is self - - @property - def is_mapped(self): - return 'mapper' in self.__dict__ - - @util.memoized_property - def mapper(self): - # raises unless self.mapper has been assigned - raise exc.UnmappedClassError(self.class_) - - def _all_sqla_attributes(self, exclude=None): - """return an iterator of all classbound attributes that are - implement :class:`._InspectionAttr`. - - This includes :class:`.QueryableAttribute` as well as extension - types such as :class:`.hybrid_property` and :class:`.AssociationProxy`. - - """ - if exclude is None: - exclude = set() - for supercls in self.class_.__mro__: - for key in set(supercls.__dict__).difference(exclude): - exclude.add(key) - val = supercls.__dict__[key] - if isinstance(val, interfaces._InspectionAttr): - yield key, val - - - def _attr_has_impl(self, key): - """Return True if the given attribute is fully initialized. - - i.e. has an impl. - """ - - return key in self and self[key].impl is not None - - def _subclass_manager(self, cls): - """Create a new ClassManager for a subclass of this ClassManager's - class. - - This is called automatically when attributes are instrumented so that - the attributes can be propagated to subclasses against their own - class-local manager, without the need for mappers etc. to have already - pre-configured managers for the full class hierarchy. Mappers - can post-configure the auto-generated ClassManager when needed. - - """ - manager = manager_of_class(cls) - if manager is None: - manager = _instrumentation_factory.create_manager_for_cls(cls) - return manager - - def _instrument_init(self): - # TODO: self.class_.__init__ is often the already-instrumented - # __init__ from an instrumented superclass. We still need to make - # our own wrapper, but it would - # be nice to wrap the original __init__ and not our existing wrapper - # of such, since this adds method overhead. - self.original_init = self.class_.__init__ - self.new_init = _generate_init(self.class_, self) - self.install_member('__init__', self.new_init) - - def _uninstrument_init(self): - if self.new_init: - self.uninstall_member('__init__') - self.new_init = None - - @util.memoized_property - def _state_constructor(self): - self.dispatch.first_init(self, self.class_) - return state.InstanceState - - def manage(self): - """Mark this instance as the manager for its class.""" - - setattr(self.class_, self.MANAGER_ATTR, self) - - def dispose(self): - """Dissasociate this manager from its class.""" - - delattr(self.class_, self.MANAGER_ATTR) - - @util.hybridmethod - def manager_getter(self): - return _default_manager_getter - - @util.hybridmethod - def state_getter(self): - """Return a (instance) -> InstanceState callable. - - "state getter" callables should raise either KeyError or - AttributeError if no InstanceState could be found for the - instance. - """ - - return _default_state_getter - - @util.hybridmethod - def dict_getter(self): - return _default_dict_getter - - - def instrument_attribute(self, key, inst, propagated=False): - if propagated: - if key in self.local_attrs: - return # don't override local attr with inherited attr - else: - self.local_attrs[key] = inst - self.install_descriptor(key, inst) - self[key] = inst - - for cls in self.class_.__subclasses__(): - manager = self._subclass_manager(cls) - manager.instrument_attribute(key, inst, True) - - def subclass_managers(self, recursive): - for cls in self.class_.__subclasses__(): - mgr = manager_of_class(cls) - if mgr is not None and mgr is not self: - yield mgr - if recursive: - for m in mgr.subclass_managers(True): - yield m - - def post_configure_attribute(self, key): - _instrumentation_factory.dispatch.\ - attribute_instrument(self.class_, key, self[key]) - - def uninstrument_attribute(self, key, propagated=False): - if key not in self: - return - if propagated: - if key in self.local_attrs: - return # don't get rid of local attr - else: - del self.local_attrs[key] - self.uninstall_descriptor(key) - del self[key] - for cls in self.class_.__subclasses__(): - manager = manager_of_class(cls) - if manager: - manager.uninstrument_attribute(key, True) - - def unregister(self): - """remove all instrumentation established by this ClassManager.""" - - self._uninstrument_init() - - self.mapper = self.dispatch = None - self.info.clear() - - for key in list(self): - if key in self.local_attrs: - self.uninstrument_attribute(key) - - def install_descriptor(self, key, inst): - if key in (self.STATE_ATTR, self.MANAGER_ATTR): - raise KeyError("%r: requested attribute name conflicts with " - "instrumentation attribute of the same name." % - key) - setattr(self.class_, key, inst) - - def uninstall_descriptor(self, key): - delattr(self.class_, key) - - def install_member(self, key, implementation): - if key in (self.STATE_ATTR, self.MANAGER_ATTR): - raise KeyError("%r: requested attribute name conflicts with " - "instrumentation attribute of the same name." % - key) - self.originals.setdefault(key, getattr(self.class_, key, None)) - setattr(self.class_, key, implementation) - - def uninstall_member(self, key): - original = self.originals.pop(key, None) - if original is not None: - setattr(self.class_, key, original) - - def instrument_collection_class(self, key, collection_class): - return collections.prepare_instrumentation(collection_class) - - def initialize_collection(self, key, state, factory): - user_data = factory() - adapter = collections.CollectionAdapter( - self.get_impl(key), state, user_data) - return adapter, user_data - - def is_instrumented(self, key, search=False): - if search: - return key in self - else: - return key in self.local_attrs - - def get_impl(self, key): - return self[key].impl - - @property - def attributes(self): - return iter(self.values()) - - ## InstanceState management - - def new_instance(self, state=None): - instance = self.class_.__new__(self.class_) - setattr(instance, self.STATE_ATTR, - state or self._state_constructor(instance, self)) - return instance - - def setup_instance(self, instance, state=None): - setattr(instance, self.STATE_ATTR, - state or self._state_constructor(instance, self)) - - def teardown_instance(self, instance): - delattr(instance, self.STATE_ATTR) - - def _serialize(self, state, state_dict): - return _SerializeManager(state, state_dict) - - def _new_state_if_none(self, instance): - """Install a default InstanceState if none is present. - - A private convenience method used by the __init__ decorator. - - """ - if hasattr(instance, self.STATE_ATTR): - return False - elif self.class_ is not instance.__class__ and \ - self.is_mapped: - # this will create a new ClassManager for the - # subclass, without a mapper. This is likely a - # user error situation but allow the object - # to be constructed, so that it is usable - # in a non-ORM context at least. - return self._subclass_manager(instance.__class__).\ - _new_state_if_none(instance) - else: - state = self._state_constructor(instance, self) - setattr(instance, self.STATE_ATTR, state) - return state - - def has_state(self, instance): - return hasattr(instance, self.STATE_ATTR) - - def has_parent(self, state, key, optimistic=False): - """TODO""" - return self.get_impl(key).hasparent(state, optimistic=optimistic) - - def __bool__(self): - """All ClassManagers are non-zero regardless of attribute state.""" - return True - - __nonzero__ = __bool__ - - def __repr__(self): - return '<%s of %r at %x>' % ( - self.__class__.__name__, self.class_, id(self)) - -class _SerializeManager(object): - """Provide serialization of a :class:`.ClassManager`. - - The :class:`.InstanceState` uses ``__init__()`` on serialize - and ``__call__()`` on deserialize. - - """ - def __init__(self, state, d): - self.class_ = state.class_ - manager = state.manager - manager.dispatch.pickle(state, d) - - def __call__(self, state, inst, state_dict): - state.manager = manager = manager_of_class(self.class_) - if manager is None: - raise exc.UnmappedInstanceError( - inst, - "Cannot deserialize object of type %r - " - "no mapper() has " - "been configured for this class within the current " - "Python process!" % - self.class_) - elif manager.is_mapped and not manager.mapper.configured: - manager.mapper._configure_all() - - # setup _sa_instance_state ahead of time so that - # unpickle events can access the object normally. - # see [ticket:2362] - if inst is not None: - manager.setup_instance(inst, state) - manager.dispatch.unpickle(state, state_dict) - -class InstrumentationFactory(object): - """Factory for new ClassManager instances.""" - - def create_manager_for_cls(self, class_): - assert class_ is not None - assert manager_of_class(class_) is None - - # give a more complicated subclass - # a chance to do what it wants here - manager, factory = self._locate_extended_factory(class_) - - if factory is None: - factory = ClassManager - manager = factory(class_) - - self._check_conflicts(class_, factory) - - manager.factory = factory - - self.dispatch.class_instrument(class_) - return manager - - def _locate_extended_factory(self, class_): - """Overridden by a subclass to do an extended lookup.""" - return None, None - - def _check_conflicts(self, class_, factory): - """Overridden by a subclass to test for conflicting factories.""" - return - - def unregister(self, class_): - manager = manager_of_class(class_) - manager.unregister() - manager.dispose() - self.dispatch.class_uninstrument(class_) - if ClassManager.MANAGER_ATTR in class_.__dict__: - delattr(class_, ClassManager.MANAGER_ATTR) - -# this attribute is replaced by sqlalchemy.ext.instrumentation -# when importred. -_instrumentation_factory = InstrumentationFactory() - -# these attributes are replaced by sqlalchemy.ext.instrumentation -# when a non-standard InstrumentationManager class is first -# used to instrument a class. -instance_state = _default_state_getter = base.instance_state - -instance_dict = _default_dict_getter = base.instance_dict - -manager_of_class = _default_manager_getter = base.manager_of_class - -def register_class(class_): - """Register class instrumentation. - - Returns the existing or newly created class manager. - - """ - - manager = manager_of_class(class_) - if manager is None: - manager = _instrumentation_factory.create_manager_for_cls(class_) - return manager - - -def unregister_class(class_): - """Unregister class instrumentation.""" - - _instrumentation_factory.unregister(class_) - - -def is_instrumented(instance, key): - """Return True if the given attribute on the given instance is - instrumented by the attributes package. - - This function may be used regardless of instrumentation - applied directly to the class, i.e. no descriptors are required. - - """ - return manager_of_class(instance.__class__).\ - is_instrumented(key, search=True) - - -def _generate_init(class_, class_manager): - """Build an __init__ decorator that triggers ClassManager events.""" - - # TODO: we should use the ClassManager's notion of the - # original '__init__' method, once ClassManager is fixed - # to always reference that. - original__init__ = class_.__init__ - assert original__init__ - - # Go through some effort here and don't change the user's __init__ - # calling signature, including the unlikely case that it has - # a return value. - # FIXME: need to juggle local names to avoid constructor argument - # clashes. - func_body = """\ -def __init__(%(apply_pos)s): - new_state = class_manager._new_state_if_none(%(self_arg)s) - if new_state: - return new_state._initialize_instance(%(apply_kw)s) - else: - return original__init__(%(apply_kw)s) -""" - func_vars = util.format_argspec_init(original__init__, grouped=False) - func_text = func_body % func_vars - - if util.py2k: - func = getattr(original__init__, 'im_func', original__init__) - func_defaults = getattr(func, 'func_defaults', None) - else: - func_defaults = getattr(original__init__, '__defaults__', None) - func_kw_defaults = getattr(original__init__, '__kwdefaults__', None) - - env = locals().copy() - exec(func_text, env) - __init__ = env['__init__'] - __init__.__doc__ = original__init__.__doc__ - - if func_defaults: - __init__.__defaults__ = func_defaults - if not util.py2k and func_kw_defaults: - __init__.__kwdefaults__ = func_kw_defaults - - return __init__ diff --git a/libs/sqlalchemy/orm/interfaces.py b/libs/sqlalchemy/orm/interfaces.py deleted file mode 100644 index 3d5559be..00000000 --- a/libs/sqlalchemy/orm/interfaces.py +++ /dev/null @@ -1,577 +0,0 @@ -# orm/interfaces.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -Contains various base classes used throughout the ORM. - -Defines the now deprecated ORM extension classes as well -as ORM internals. - -Other than the deprecated extensions, this module and the -classes within should be considered mostly private. - -""" - -from __future__ import absolute_import - -from .. import exc as sa_exc, util, inspect -from ..sql import operators -from collections import deque -from .base import ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION -from .base import _InspectionAttr, _MappedAttribute -from .path_registry import PathRegistry -import collections - - -__all__ = ( - 'AttributeExtension', - 'EXT_CONTINUE', - 'EXT_STOP', - 'ExtensionOption', - 'InstrumentationManager', - 'LoaderStrategy', - 'MapperExtension', - 'MapperOption', - 'MapperProperty', - 'PropComparator', - 'PropertyOption', - 'SessionExtension', - 'StrategizedOption', - 'StrategizedProperty', - ) - - - -class MapperProperty(_MappedAttribute, _InspectionAttr): - """Manage the relationship of a ``Mapper`` to a single class - attribute, as well as that attribute as it appears on individual - instances of the class, including attribute instrumentation, - attribute access, loading behavior, and dependency calculations. - - The most common occurrences of :class:`.MapperProperty` are the - mapped :class:`.Column`, which is represented in a mapping as - an instance of :class:`.ColumnProperty`, - and a reference to another class produced by :func:`.relationship`, - represented in the mapping as an instance of - :class:`.RelationshipProperty`. - - """ - - cascade = frozenset() - """The set of 'cascade' attribute names. - - This collection is checked before the 'cascade_iterator' method is called. - - """ - - is_property = True - - def setup(self, context, entity, path, adapter, **kwargs): - """Called by Query for the purposes of constructing a SQL statement. - - Each MapperProperty associated with the target mapper processes the - statement referenced by the query context, adding columns and/or - criterion as appropriate. - """ - - pass - - def create_row_processor(self, context, path, - mapper, row, adapter): - """Return a 3-tuple consisting of three row processing functions. - - """ - return None, None, None - - def cascade_iterator(self, type_, state, visited_instances=None, - halt_on=None): - """Iterate through instances related to the given instance for - a particular 'cascade', starting with this MapperProperty. - - Return an iterator3-tuples (instance, mapper, state). - - Note that the 'cascade' collection on this MapperProperty is - checked first for the given type before cascade_iterator is called. - - See PropertyLoader for the related instance implementation. - """ - - return iter(()) - - def set_parent(self, parent, init): - self.parent = parent - - def instrument_class(self, mapper): # pragma: no-coverage - raise NotImplementedError() - - @util.memoized_property - def info(self): - """Info dictionary associated with the object, allowing user-defined - data to be associated with this :class:`.MapperProperty`. - - The dictionary is generated when first accessed. Alternatively, - it can be specified as a constructor argument to the - :func:`.column_property`, :func:`.relationship`, or :func:`.composite` - functions. - - .. versionadded:: 0.8 Added support for .info to all - :class:`.MapperProperty` subclasses. - - .. seealso:: - - :attr:`.QueryableAttribute.info` - - :attr:`.SchemaItem.info` - - """ - return {} - - _configure_started = False - _configure_finished = False - - def init(self): - """Called after all mappers are created to assemble - relationships between mappers and perform other post-mapper-creation - initialization steps. - - """ - self._configure_started = True - self.do_init() - self._configure_finished = True - - @property - def class_attribute(self): - """Return the class-bound descriptor corresponding to this - :class:`.MapperProperty`. - - This is basically a ``getattr()`` call:: - - return getattr(self.parent.class_, self.key) - - I.e. if this :class:`.MapperProperty` were named ``addresses``, - and the class to which it is mapped is ``User``, this sequence - is possible:: - - >>> from sqlalchemy import inspect - >>> mapper = inspect(User) - >>> addresses_property = mapper.attrs.addresses - >>> addresses_property.class_attribute is User.addresses - True - >>> User.addresses.property is addresses_property - True - - - """ - - return getattr(self.parent.class_, self.key) - - def do_init(self): - """Perform subclass-specific initialization post-mapper-creation - steps. - - This is a template method called by the ``MapperProperty`` - object's init() method. - - """ - - pass - - def post_instrument_class(self, mapper): - """Perform instrumentation adjustments that need to occur - after init() has completed. - - """ - pass - - def is_primary(self): - """Return True if this ``MapperProperty``'s mapper is the - primary mapper for its class. - - This flag is used to indicate that the ``MapperProperty`` can - define attribute instrumentation for the class at the class - level (as opposed to the individual instance level). - """ - - return not self.parent.non_primary - - def merge(self, session, source_state, source_dict, dest_state, - dest_dict, load, _recursive): - """Merge the attribute represented by this ``MapperProperty`` - from source to destination object""" - - pass - - def compare(self, operator, value, **kw): - """Return a compare operation for the columns represented by - this ``MapperProperty`` to the given value, which may be a - column value or an instance. 'operator' is an operator from - the operators module, or from sql.Comparator. - - By default uses the PropComparator attached to this MapperProperty - under the attribute name "comparator". - """ - - return operator(self.comparator, value) - - def __repr__(self): - return '<%s at 0x%x; %s>' % ( - self.__class__.__name__, - id(self), getattr(self, 'key', 'no key')) - -class PropComparator(operators.ColumnOperators): - """Defines boolean, comparison, and other operators for - :class:`.MapperProperty` objects. - - SQLAlchemy allows for operators to - be redefined at both the Core and ORM level. :class:`.PropComparator` - is the base class of operator redefinition for ORM-level operations, - including those of :class:`.ColumnProperty`, - :class:`.RelationshipProperty`, and :class:`.CompositeProperty`. - - .. note:: With the advent of Hybrid properties introduced in SQLAlchemy - 0.7, as well as Core-level operator redefinition in - SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator` - instances is extremely rare. See :ref:`hybrids_toplevel` as well - as :ref:`types_operators`. - - User-defined subclasses of :class:`.PropComparator` may be created. The - built-in Python comparison and math operator methods, such as - :meth:`.operators.ColumnOperators.__eq__`, - :meth:`.operators.ColumnOperators.__lt__`, and - :meth:`.operators.ColumnOperators.__add__`, can be overridden to provide - new operator behavior. The custom :class:`.PropComparator` is passed to - the :class:`.MapperProperty` instance via the ``comparator_factory`` - argument. In each case, - the appropriate subclass of :class:`.PropComparator` should be used:: - - # definition of custom PropComparator subclasses - - from sqlalchemy.orm.properties import \\ - ColumnProperty,\\ - CompositeProperty,\\ - RelationshipProperty - - class MyColumnComparator(ColumnProperty.Comparator): - def __eq__(self, other): - return self.__clause_element__() == other - - class MyRelationshipComparator(RelationshipProperty.Comparator): - def any(self, expression): - "define the 'any' operation" - # ... - - class MyCompositeComparator(CompositeProperty.Comparator): - def __gt__(self, other): - "redefine the 'greater than' operation" - - return sql.and_(*[a>b for a, b in - zip(self.__clause_element__().clauses, - other.__composite_values__())]) - - - # application of custom PropComparator subclasses - - from sqlalchemy.orm import column_property, relationship, composite - from sqlalchemy import Column, String - - class SomeMappedClass(Base): - some_column = column_property(Column("some_column", String), - comparator_factory=MyColumnComparator) - - some_relationship = relationship(SomeOtherClass, - comparator_factory=MyRelationshipComparator) - - some_composite = composite( - Column("a", String), Column("b", String), - comparator_factory=MyCompositeComparator - ) - - Note that for column-level operator redefinition, it's usually - simpler to define the operators at the Core level, using the - :attr:`.TypeEngine.comparator_factory` attribute. See - :ref:`types_operators` for more detail. - - See also: - - :class:`.ColumnProperty.Comparator` - - :class:`.RelationshipProperty.Comparator` - - :class:`.CompositeProperty.Comparator` - - :class:`.ColumnOperators` - - :ref:`types_operators` - - :attr:`.TypeEngine.comparator_factory` - - """ - - def __init__(self, prop, parentmapper, adapt_to_entity=None): - self.prop = self.property = prop - self._parentmapper = parentmapper - self._adapt_to_entity = adapt_to_entity - - def __clause_element__(self): - raise NotImplementedError("%r" % self) - - def _query_clause_element(self): - return self.__clause_element__() - - def adapt_to_entity(self, adapt_to_entity): - """Return a copy of this PropComparator which will use the given - :class:`.AliasedInsp` to produce corresponding expressions. - """ - return self.__class__(self.prop, self._parentmapper, adapt_to_entity) - - @property - def adapter(self): - """Produce a callable that adapts column expressions - to suit an aliased version of this comparator. - - """ - if self._adapt_to_entity is None: - return None - else: - return self._adapt_to_entity._adapt_element - - @util.memoized_property - def info(self): - return self.property.info - - @staticmethod - def any_op(a, b, **kwargs): - return a.any(b, **kwargs) - - @staticmethod - def has_op(a, b, **kwargs): - return a.has(b, **kwargs) - - @staticmethod - def of_type_op(a, class_): - return a.of_type(class_) - - def of_type(self, class_): - """Redefine this object in terms of a polymorphic subclass. - - Returns a new PropComparator from which further criterion can be - evaluated. - - e.g.:: - - query.join(Company.employees.of_type(Engineer)).\\ - filter(Engineer.name=='foo') - - :param \class_: a class or mapper indicating that criterion will be - against this specific subclass. - - - """ - - return self.operate(PropComparator.of_type_op, class_) - - def any(self, criterion=None, **kwargs): - """Return true if this collection contains any member that meets the - given criterion. - - The usual implementation of ``any()`` is - :meth:`.RelationshipProperty.Comparator.any`. - - :param criterion: an optional ClauseElement formulated against the - member class' table or attributes. - - :param \**kwargs: key/value pairs corresponding to member class - attribute names which will be compared via equality to the - corresponding values. - - """ - - return self.operate(PropComparator.any_op, criterion, **kwargs) - - def has(self, criterion=None, **kwargs): - """Return true if this element references a member which meets the - given criterion. - - The usual implementation of ``has()`` is - :meth:`.RelationshipProperty.Comparator.has`. - - :param criterion: an optional ClauseElement formulated against the - member class' table or attributes. - - :param \**kwargs: key/value pairs corresponding to member class - attribute names which will be compared via equality to the - corresponding values. - - """ - - return self.operate(PropComparator.has_op, criterion, **kwargs) - - -class StrategizedProperty(MapperProperty): - """A MapperProperty which uses selectable strategies to affect - loading behavior. - - There is a single strategy selected by default. Alternate - strategies can be selected at Query time through the usage of - ``StrategizedOption`` objects via the Query.options() method. - - """ - - strategy_wildcard_key = None - - def _get_context_loader(self, context, path): - load = None - - # use EntityRegistry.__getitem__()->PropRegistry here so - # that the path is stated in terms of our base - search_path = dict.__getitem__(path, self) - - # search among: exact match, "attr.*", "default" strategy - # if any. - for path_key in ( - search_path._loader_key, - search_path._wildcard_path_loader_key, - search_path._default_path_loader_key - ): - if path_key in context.attributes: - load = context.attributes[path_key] - break - - return load - - def _get_strategy(self, key): - try: - return self._strategies[key] - except KeyError: - cls = self._strategy_lookup(*key) - self._strategies[key] = self._strategies[cls] = strategy = cls(self) - return strategy - - def _get_strategy_by_cls(self, cls): - return self._get_strategy(cls._strategy_keys[0]) - - def setup(self, context, entity, path, adapter, **kwargs): - loader = self._get_context_loader(context, path) - if loader and loader.strategy: - strat = self._get_strategy(loader.strategy) - else: - strat = self.strategy - strat.setup_query(context, entity, path, loader, adapter, **kwargs) - - def create_row_processor(self, context, path, mapper, row, adapter): - loader = self._get_context_loader(context, path) - if loader and loader.strategy: - strat = self._get_strategy(loader.strategy) - else: - strat = self.strategy - return strat.create_row_processor(context, path, loader, - mapper, row, adapter) - - def do_init(self): - self._strategies = {} - self.strategy = self._get_strategy_by_cls(self.strategy_class) - - def post_instrument_class(self, mapper): - if self.is_primary() and \ - not mapper.class_manager._attr_has_impl(self.key): - self.strategy.init_class_attribute(mapper) - - - _strategies = collections.defaultdict(dict) - - @classmethod - def strategy_for(cls, **kw): - def decorate(dec_cls): - dec_cls._strategy_keys = [] - key = tuple(sorted(kw.items())) - cls._strategies[cls][key] = dec_cls - dec_cls._strategy_keys.append(key) - return dec_cls - return decorate - - @classmethod - def _strategy_lookup(cls, *key): - for prop_cls in cls.__mro__: - if prop_cls in cls._strategies: - strategies = cls._strategies[prop_cls] - try: - return strategies[key] - except KeyError: - pass - raise Exception("can't locate strategy for %s %s" % (cls, key)) - - -class MapperOption(object): - """Describe a modification to a Query.""" - - propagate_to_loaders = False - """if True, indicate this option should be carried along - Query object generated by scalar or object lazy loaders. - """ - - def process_query(self, query): - pass - - def process_query_conditionally(self, query): - """same as process_query(), except that this option may not - apply to the given query. - - Used when secondary loaders resend existing options to a new - Query.""" - - self.process_query(query) - - - - -class LoaderStrategy(object): - """Describe the loading behavior of a StrategizedProperty object. - - The ``LoaderStrategy`` interacts with the querying process in three - ways: - - * it controls the configuration of the ``InstrumentedAttribute`` - placed on a class to handle the behavior of the attribute. this - may involve setting up class-level callable functions to fire - off a select operation when the attribute is first accessed - (i.e. a lazy load) - - * it processes the ``QueryContext`` at statement construction time, - where it can modify the SQL statement that is being produced. - Simple column attributes may add their represented column to the - list of selected columns, *eager loading* properties may add - ``LEFT OUTER JOIN`` clauses to the statement. - - * It produces "row processor" functions at result fetching time. - These "row processor" functions populate a particular attribute - on a particular mapped instance. - - """ - def __init__(self, parent): - self.parent_property = parent - self.is_class_level = False - self.parent = self.parent_property.parent - self.key = self.parent_property.key - - def init_class_attribute(self, mapper): - pass - - def setup_query(self, context, entity, path, loadopt, adapter, **kwargs): - pass - - def create_row_processor(self, context, path, loadopt, mapper, - row, adapter): - """Return row processing functions which fulfill the contract - specified by MapperProperty.create_row_processor. - - StrategizedProperty delegates its create_row_processor method - directly to this method. """ - - return None, None, None - - def __str__(self): - return str(self.parent_property) diff --git a/libs/sqlalchemy/orm/loading.py b/libs/sqlalchemy/orm/loading.py deleted file mode 100644 index af77fe3e..00000000 --- a/libs/sqlalchemy/orm/loading.py +++ /dev/null @@ -1,610 +0,0 @@ -# orm/loading.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""private module containing functions used to convert database -rows into object instances and associated state. - -the functions here are called primarily by Query, Mapper, -as well as some of the attribute loading strategies. - -""" - - -from .. import util -from . import attributes, exc as orm_exc, state as statelib -from .interfaces import EXT_CONTINUE -from ..sql import util as sql_util -from .util import _none_set, state_str -from .. import exc as sa_exc - -_new_runid = util.counter() - - -def instances(query, cursor, context): - """Return an ORM result as an iterator.""" - session = query.session - - context.runid = _new_runid() - - filter_fns = [ent.filter_fn - for ent in query._entities] - filtered = id in filter_fns - - single_entity = len(query._entities) == 1 and \ - query._entities[0].supports_single_entity - - if filtered: - if single_entity: - filter_fn = id - else: - def filter_fn(row): - return tuple(fn(x) for x, fn in zip(row, filter_fns)) - - custom_rows = single_entity and \ - query._entities[0].custom_rows - - (process, labels) = \ - list(zip(*[ - query_entity.row_processor(query, - context, custom_rows) - for query_entity in query._entities - ])) - - while True: - context.progress = {} - context.partials = {} - - if query._yield_per: - fetch = cursor.fetchmany(query._yield_per) - if not fetch: - break - else: - fetch = cursor.fetchall() - - if custom_rows: - rows = [] - for row in fetch: - process[0](row, rows) - elif single_entity: - rows = [process[0](row, None) for row in fetch] - else: - rows = [util.KeyedTuple([proc(row, None) for proc in process], - labels) for row in fetch] - - if filtered: - rows = util.unique_list(rows, filter_fn) - - if context.refresh_state and query._only_load_props \ - and context.refresh_state in context.progress: - context.refresh_state._commit( - context.refresh_state.dict, query._only_load_props) - context.progress.pop(context.refresh_state) - - statelib.InstanceState._commit_all_states( - list(context.progress.items()), - session.identity_map - ) - - for state, (dict_, attrs) in context.partials.items(): - state._commit(dict_, attrs) - - for row in rows: - yield row - - if not query._yield_per: - break - - -@util.dependencies("sqlalchemy.orm.query") -def merge_result(querylib, query, iterator, load=True): - """Merge a result into this :class:`.Query` object's Session.""" - - session = query.session - if load: - # flush current contents if we expect to load data - session._autoflush() - - autoflush = session.autoflush - try: - session.autoflush = False - single_entity = len(query._entities) == 1 - if single_entity: - if isinstance(query._entities[0], querylib._MapperEntity): - result = [session._merge( - attributes.instance_state(instance), - attributes.instance_dict(instance), - load=load, _recursive={}) - for instance in iterator] - else: - result = list(iterator) - else: - mapped_entities = [i for i, e in enumerate(query._entities) - if isinstance(e, querylib._MapperEntity)] - result = [] - keys = [ent._label_name for ent in query._entities] - for row in iterator: - newrow = list(row) - for i in mapped_entities: - if newrow[i] is not None: - newrow[i] = session._merge( - attributes.instance_state(newrow[i]), - attributes.instance_dict(newrow[i]), - load=load, _recursive={}) - result.append(util.KeyedTuple(newrow, keys)) - - return iter(result) - finally: - session.autoflush = autoflush - - -def get_from_identity(session, key, passive): - """Look up the given key in the given session's identity map, - check the object for expired state if found. - - """ - instance = session.identity_map.get(key) - if instance is not None: - - state = attributes.instance_state(instance) - - # expired - ensure it still exists - if state.expired: - if not passive & attributes.SQL_OK: - # TODO: no coverage here - return attributes.PASSIVE_NO_RESULT - elif not passive & attributes.RELATED_OBJECT_OK: - # this mode is used within a flush and the instance's - # expired state will be checked soon enough, if necessary - return instance - try: - state(state, passive) - except orm_exc.ObjectDeletedError: - session._remove_newly_deleted([state]) - return None - return instance - else: - return None - - -def load_on_ident(query, key, - refresh_state=None, lockmode=None, - only_load_props=None): - """Load the given identity key from the database.""" - - if key is not None: - ident = key[1] - else: - ident = None - - if refresh_state is None: - q = query._clone() - q._get_condition() - else: - q = query._clone() - - if ident is not None: - mapper = query._mapper_zero() - - (_get_clause, _get_params) = mapper._get_clause - - # None present in ident - turn those comparisons - # into "IS NULL" - if None in ident: - nones = set([ - _get_params[col].key for col, value in - zip(mapper.primary_key, ident) if value is None - ]) - _get_clause = sql_util.adapt_criterion_to_null( - _get_clause, nones) - - _get_clause = q._adapt_clause(_get_clause, True, False) - q._criterion = _get_clause - - params = dict([ - (_get_params[primary_key].key, id_val) - for id_val, primary_key in zip(ident, mapper.primary_key) - ]) - - q._params = params - - if lockmode is not None: - version_check = True - q = q.with_lockmode(lockmode) - elif query._for_update_arg is not None: - version_check = True - q._for_update_arg = query._for_update_arg - else: - version_check = False - - q._get_options( - populate_existing=bool(refresh_state), - version_check=version_check, - only_load_props=only_load_props, - refresh_state=refresh_state) - q._order_by = None - - try: - return q.one() - except orm_exc.NoResultFound: - return None - - -def instance_processor(mapper, context, path, adapter, - polymorphic_from=None, - only_load_props=None, - refresh_state=None, - polymorphic_discriminator=None): - - """Produce a mapper level row processor callable - which processes rows into mapped instances.""" - - # note that this method, most of which exists in a closure - # called _instance(), resists being broken out, as - # attempts to do so tend to add significant function - # call overhead. _instance() is the most - # performance-critical section in the whole ORM. - - pk_cols = mapper.primary_key - - if polymorphic_from or refresh_state: - polymorphic_on = None - else: - if polymorphic_discriminator is not None: - polymorphic_on = polymorphic_discriminator - else: - polymorphic_on = mapper.polymorphic_on - polymorphic_instances = util.PopulateDict( - _configure_subclass_mapper( - mapper, - context, path, adapter) - ) - - version_id_col = mapper.version_id_col - - if adapter: - pk_cols = [adapter.columns[c] for c in pk_cols] - if polymorphic_on is not None: - polymorphic_on = adapter.columns[polymorphic_on] - if version_id_col is not None: - version_id_col = adapter.columns[version_id_col] - - identity_class = mapper._identity_class - - new_populators = [] - existing_populators = [] - eager_populators = [] - - load_path = context.query._current_path + path \ - if context.query._current_path.path \ - else path - - def populate_state(state, dict_, row, isnew, only_load_props): - if isnew: - if context.propagate_options: - state.load_options = context.propagate_options - if state.load_options: - state.load_path = load_path - - if not new_populators: - _populators(mapper, context, path, row, adapter, - new_populators, - existing_populators, - eager_populators - ) - - if isnew: - populators = new_populators - else: - populators = existing_populators - - if only_load_props is None: - for key, populator in populators: - populator(state, dict_, row) - elif only_load_props: - for key, populator in populators: - if key in only_load_props: - populator(state, dict_, row) - - session_identity_map = context.session.identity_map - - listeners = mapper.dispatch - - translate_row = listeners.translate_row or None - create_instance = listeners.create_instance or None - populate_instance = listeners.populate_instance or None - append_result = listeners.append_result or None - populate_existing = context.populate_existing or mapper.always_refresh - invoke_all_eagers = context.invoke_all_eagers - - if mapper.allow_partial_pks: - is_not_primary_key = _none_set.issuperset - else: - is_not_primary_key = _none_set.issubset - - def _instance(row, result): - if not new_populators and invoke_all_eagers: - _populators(mapper, context, path, row, adapter, - new_populators, - existing_populators, - eager_populators - ) - - if translate_row: - for fn in translate_row: - ret = fn(mapper, context, row) - if ret is not EXT_CONTINUE: - row = ret - break - - if polymorphic_on is not None: - discriminator = row[polymorphic_on] - if discriminator is not None: - _instance = polymorphic_instances[discriminator] - if _instance: - return _instance(row, result) - - # determine identity key - if refresh_state: - identitykey = refresh_state.key - if identitykey is None: - # super-rare condition; a refresh is being called - # on a non-instance-key instance; this is meant to only - # occur within a flush() - identitykey = mapper._identity_key_from_state(refresh_state) - else: - identitykey = ( - identity_class, - tuple([row[column] for column in pk_cols]) - ) - - instance = session_identity_map.get(identitykey) - if instance is not None: - state = attributes.instance_state(instance) - dict_ = attributes.instance_dict(instance) - - isnew = state.runid != context.runid - currentload = not isnew - loaded_instance = False - - if not currentload and \ - version_id_col is not None and \ - context.version_check and \ - mapper._get_state_attr_by_column( - state, - dict_, - mapper.version_id_col) != \ - row[version_id_col]: - - raise orm_exc.StaleDataError( - "Instance '%s' has version id '%s' which " - "does not match database-loaded version id '%s'." - % (state_str(state), - mapper._get_state_attr_by_column( - state, dict_, - mapper.version_id_col), - row[version_id_col])) - elif refresh_state: - # out of band refresh_state detected (i.e. its not in the - # session.identity_map) honor it anyway. this can happen - # if a _get() occurs within save_obj(), such as - # when eager_defaults is True. - state = refresh_state - instance = state.obj() - dict_ = attributes.instance_dict(instance) - isnew = state.runid != context.runid - currentload = True - loaded_instance = False - else: - # check for non-NULL values in the primary key columns, - # else no entity is returned for the row - if is_not_primary_key(identitykey[1]): - return None - - isnew = True - currentload = True - loaded_instance = True - - if create_instance: - for fn in create_instance: - instance = fn(mapper, context, - row, mapper.class_) - if instance is not EXT_CONTINUE: - manager = attributes.manager_of_class( - instance.__class__) - # TODO: if manager is None, raise a friendly error - # about returning instances of unmapped types - manager.setup_instance(instance) - break - else: - instance = mapper.class_manager.new_instance() - else: - instance = mapper.class_manager.new_instance() - - dict_ = attributes.instance_dict(instance) - state = attributes.instance_state(instance) - state.key = identitykey - - # attach instance to session. - state.session_id = context.session.hash_key - session_identity_map.add(state) - - if currentload or populate_existing: - # state is being fully loaded, so populate. - # add to the "context.progress" collection. - if isnew: - state.runid = context.runid - context.progress[state] = dict_ - - if populate_instance: - for fn in populate_instance: - ret = fn(mapper, context, row, state, - only_load_props=only_load_props, - instancekey=identitykey, isnew=isnew) - if ret is not EXT_CONTINUE: - break - else: - populate_state(state, dict_, row, isnew, only_load_props) - else: - populate_state(state, dict_, row, isnew, only_load_props) - - if loaded_instance: - state.manager.dispatch.load(state, context) - elif isnew: - state.manager.dispatch.refresh(state, context, only_load_props) - - elif state in context.partials or state.unloaded or eager_populators: - # state is having a partial set of its attributes - # refreshed. Populate those attributes, - # and add to the "context.partials" collection. - if state in context.partials: - isnew = False - (d_, attrs) = context.partials[state] - else: - isnew = True - attrs = state.unloaded - context.partials[state] = (dict_, attrs) - - if populate_instance: - for fn in populate_instance: - ret = fn(mapper, context, row, state, - only_load_props=attrs, - instancekey=identitykey, isnew=isnew) - if ret is not EXT_CONTINUE: - break - else: - populate_state(state, dict_, row, isnew, attrs) - else: - populate_state(state, dict_, row, isnew, attrs) - - for key, pop in eager_populators: - if key not in state.unloaded: - pop(state, dict_, row) - - if isnew: - state.manager.dispatch.refresh(state, context, attrs) - - if result is not None: - if append_result: - for fn in append_result: - if fn(mapper, context, row, state, - result, instancekey=identitykey, - isnew=isnew) is not EXT_CONTINUE: - break - else: - result.append(instance) - else: - result.append(instance) - - return instance - return _instance - - -def _populators(mapper, context, path, row, adapter, - new_populators, existing_populators, eager_populators): - """Produce a collection of attribute level row processor - callables.""" - - delayed_populators = [] - pops = (new_populators, existing_populators, delayed_populators, - eager_populators) - - for prop in mapper._props.values(): - - for i, pop in enumerate(prop.create_row_processor( - context, - path, - mapper, row, adapter)): - if pop is not None: - pops[i].append((prop.key, pop)) - - if delayed_populators: - new_populators.extend(delayed_populators) - - -def _configure_subclass_mapper(mapper, context, path, adapter): - """Produce a mapper level row processor callable factory for mappers - inheriting this one.""" - - def configure_subclass_mapper(discriminator): - try: - sub_mapper = mapper.polymorphic_map[discriminator] - except KeyError: - raise AssertionError( - "No such polymorphic_identity %r is defined" % - discriminator) - if sub_mapper is mapper: - return None - - return instance_processor( - sub_mapper, - context, - path, - adapter, - polymorphic_from=mapper) - return configure_subclass_mapper - - -def load_scalar_attributes(mapper, state, attribute_names): - """initiate a column-based attribute refresh operation.""" - - #assert mapper is _state_mapper(state) - session = state.session - if not session: - raise orm_exc.DetachedInstanceError( - "Instance %s is not bound to a Session; " - "attribute refresh operation cannot proceed" % - (state_str(state))) - - has_key = bool(state.key) - - result = False - - if mapper.inherits and not mapper.concrete: - statement = mapper._optimized_get_statement(state, attribute_names) - if statement is not None: - result = load_on_ident( - session.query(mapper).from_statement(statement), - None, - only_load_props=attribute_names, - refresh_state=state - ) - - if result is False: - if has_key: - identity_key = state.key - else: - # this codepath is rare - only valid when inside a flush, and the - # object is becoming persistent but hasn't yet been assigned - # an identity_key. - # check here to ensure we have the attrs we need. - pk_attrs = [mapper._columntoproperty[col].key - for col in mapper.primary_key] - if state.expired_attributes.intersection(pk_attrs): - raise sa_exc.InvalidRequestError( - "Instance %s cannot be refreshed - it's not " - " persistent and does not " - "contain a full primary key." % state_str(state)) - identity_key = mapper._identity_key_from_state(state) - - if (_none_set.issubset(identity_key) and \ - not mapper.allow_partial_pks) or \ - _none_set.issuperset(identity_key): - util.warn("Instance %s to be refreshed doesn't " - "contain a full primary key - can't be refreshed " - "(and shouldn't be expired, either)." - % state_str(state)) - return - - result = load_on_ident( - session.query(mapper), - identity_key, - refresh_state=state, - only_load_props=attribute_names) - - # if instance is pending, a refresh operation - # may not complete (even if PK attributes are assigned) - if has_key and result is None: - raise orm_exc.ObjectDeletedError(state) diff --git a/libs/sqlalchemy/orm/mapper.py b/libs/sqlalchemy/orm/mapper.py deleted file mode 100644 index a853efc3..00000000 --- a/libs/sqlalchemy/orm/mapper.py +++ /dev/null @@ -1,2690 +0,0 @@ -# orm/mapper.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Logic to map Python classes to and from selectables. - -Defines the :class:`~sqlalchemy.orm.mapper.Mapper` class, the central -configurational unit which associates a class with a database table. - -This is a semi-private module; the main configurational API of the ORM is -available in :class:`~sqlalchemy.orm.`. - -""" -from __future__ import absolute_import - -import types -import weakref -from itertools import chain -from collections import deque - -from .. import sql, util, log, exc as sa_exc, event, schema, inspection -from ..sql import expression, visitors, operators, util as sql_util -from . import instrumentation, attributes, exc as orm_exc, loading -from . import properties -from .interfaces import MapperProperty, _InspectionAttr, _MappedAttribute - -from .base import _class_to_mapper, _state_mapper, class_mapper, \ - state_str, _INSTRUMENTOR -from .path_registry import PathRegistry - -import sys - - -_mapper_registry = weakref.WeakKeyDictionary() -_already_compiling = False - -_memoized_configured_property = util.group_expirable_memoized_property() - - -# a constant returned by _get_attr_by_column to indicate -# this mapper is not handling an attribute for a particular -# column -NO_ATTRIBUTE = util.symbol('NO_ATTRIBUTE') - -# lock used to synchronize the "mapper configure" step -_CONFIGURE_MUTEX = util.threading.RLock() - - -@inspection._self_inspects -@log.class_logger -class Mapper(_InspectionAttr): - """Define the correlation of class attributes to database table - columns. - - The :class:`.Mapper` object is instantiated using the - :func:`~sqlalchemy.orm.mapper` function. For information - about instantiating new :class:`.Mapper` objects, see - that function's documentation. - - - When :func:`.mapper` is used - explicitly to link a user defined class with table - metadata, this is referred to as *classical mapping*. - Modern SQLAlchemy usage tends to favor the - :mod:`sqlalchemy.ext.declarative` extension for class - configuration, which - makes usage of :func:`.mapper` behind the scenes. - - Given a particular class known to be mapped by the ORM, - the :class:`.Mapper` which maintains it can be acquired - using the :func:`.inspect` function:: - - from sqlalchemy import inspect - - mapper = inspect(MyClass) - - A class which was mapped by the :mod:`sqlalchemy.ext.declarative` - extension will also have its mapper available via the ``__mapper__`` - attribute. - - - """ - - _new_mappers = False - - def __init__(self, - class_, - local_table=None, - properties=None, - primary_key=None, - non_primary=False, - inherits=None, - inherit_condition=None, - inherit_foreign_keys=None, - extension=None, - order_by=False, - always_refresh=False, - version_id_col=None, - version_id_generator=None, - polymorphic_on=None, - _polymorphic_map=None, - polymorphic_identity=None, - concrete=False, - with_polymorphic=None, - allow_partial_pks=True, - batch=True, - column_prefix=None, - include_properties=None, - exclude_properties=None, - passive_updates=True, - eager_defaults=False, - legacy_is_orphan=False, - _compiled_cache_size=100, - ): - """Return a new :class:`~.Mapper` object. - - This function is typically used behind the scenes - via the Declarative extension. When using Declarative, - many of the usual :func:`.mapper` arguments are handled - by the Declarative extension itself, including ``class_``, - ``local_table``, ``properties``, and ``inherits``. - Other options are passed to :func:`.mapper` using - the ``__mapper_args__`` class variable:: - - class MyClass(Base): - __tablename__ = 'my_table' - id = Column(Integer, primary_key=True) - type = Column(String(50)) - alt = Column("some_alt", Integer) - - __mapper_args__ = { - 'polymorphic_on' : type - } - - - Explicit use of :func:`.mapper` - is often referred to as *classical mapping*. The above - declarative example is equivalent in classical form to:: - - my_table = Table("my_table", metadata, - Column('id', Integer, primary_key=True), - Column('type', String(50)), - Column("some_alt", Integer) - ) - - class MyClass(object): - pass - - mapper(MyClass, my_table, - polymorphic_on=my_table.c.type, - properties={ - 'alt':my_table.c.some_alt - }) - - .. seealso:: - - :ref:`classical_mapping` - discussion of direct usage of - :func:`.mapper` - - :param class\_: The class to be mapped. When using Declarative, - this argument is automatically passed as the declared class - itself. - - :param local_table: The :class:`.Table` or other selectable - to which the class is mapped. May be ``None`` if - this mapper inherits from another mapper using single-table - inheritance. When using Declarative, this argument is - automatically passed by the extension, based on what - is configured via the ``__table__`` argument or via the - :class:`.Table` produced as a result of the ``__tablename__`` - and :class:`.Column` arguments present. - - :param always_refresh: If True, all query operations for this mapped - class will overwrite all data within object instances that already - exist within the session, erasing any in-memory changes with - whatever information was loaded from the database. Usage of this - flag is highly discouraged; as an alternative, see the method - :meth:`.Query.populate_existing`. - - :param allow_partial_pks: Defaults to True. Indicates that a - composite primary key with some NULL values should be considered as - possibly existing within the database. This affects whether a - mapper will assign an incoming row to an existing identity, as well - as if :meth:`.Session.merge` will check the database first for a - particular primary key value. A "partial primary key" can occur if - one has mapped to an OUTER JOIN, for example. - - :param batch: Defaults to ``True``, indicating that save operations - of multiple entities can be batched together for efficiency. - Setting to False indicates - that an instance will be fully saved before saving the next - instance. This is used in the extremely rare case that a - :class:`.MapperEvents` listener requires being called - in between individual row persistence operations. - - :param column_prefix: A string which will be prepended - to the mapped attribute name when :class:`.Column` - objects are automatically assigned as attributes to the - mapped class. Does not affect explicitly specified - column-based properties. - - See the section :ref:`column_prefix` for an example. - - :param concrete: If True, indicates this mapper should use concrete - table inheritance with its parent mapper. - - See the section :ref:`concrete_inheritance` for an example. - - :param eager_defaults: if True, the ORM will immediately fetch the - value of server-generated default values after an INSERT or UPDATE, - rather than leaving them as expired to be fetched on next access. - This can be used for event schemes where the server-generated values - are needed immediately before the flush completes. By default, - this scheme will emit an individual ``SELECT`` statement per row - inserted or updated, which note can add significant performance - overhead. However, if the - target database supports :term:`RETURNING`, the default values will be - returned inline with the INSERT or UPDATE statement, which can - greatly enhance performance for an application that needs frequent - access to just-generated server defaults. - - .. versionchanged:: 0.9.0 The ``eager_defaults`` option can now - make use of :term:`RETURNING` for backends which support it. - - :param exclude_properties: A list or set of string column names to - be excluded from mapping. - - See :ref:`include_exclude_cols` for an example. - - :param extension: A :class:`.MapperExtension` instance or - list of :class:`.MapperExtension` instances which will be applied - to all operations by this :class:`.Mapper`. **Deprecated.** - Please see :class:`.MapperEvents`. - - :param include_properties: An inclusive list or set of string column - names to map. - - See :ref:`include_exclude_cols` for an example. - - :param inherits: A mapped class or the corresponding :class:`.Mapper` - of one indicating a superclass to which this :class:`.Mapper` - should *inherit* from. The mapped class here must be a subclass - of the other mapper's class. When using Declarative, this argument - is passed automatically as a result of the natural class - hierarchy of the declared classes. - - .. seealso:: - - :ref:`inheritance_toplevel` - - :param inherit_condition: For joined table inheritance, a SQL - expression which will - define how the two tables are joined; defaults to a natural join - between the two tables. - - :param inherit_foreign_keys: When ``inherit_condition`` is used and the - columns present are missing a :class:`.ForeignKey` configuration, - this parameter can be used to specify which columns are "foreign". - In most cases can be left as ``None``. - - :param legacy_is_orphan: Boolean, defaults to ``False``. - When ``True``, specifies that "legacy" orphan consideration - is to be applied to objects mapped by this mapper, which means - that a pending (that is, not persistent) object is auto-expunged - from an owning :class:`.Session` only when it is de-associated - from *all* parents that specify a ``delete-orphan`` cascade towards - this mapper. The new default behavior is that the object is auto-expunged - when it is de-associated with *any* of its parents that specify - ``delete-orphan`` cascade. This behavior is more consistent with - that of a persistent object, and allows behavior to be consistent - in more scenarios independently of whether or not an orphanable - object has been flushed yet or not. - - See the change note and example at :ref:`legacy_is_orphan_addition` - for more detail on this change. - - .. versionadded:: 0.8 - the consideration of a pending object as - an "orphan" has been modified to more closely match the - behavior as that of persistent objects, which is that the object - is expunged from the :class:`.Session` as soon as it is - de-associated from any of its orphan-enabled parents. Previously, - the pending object would be expunged only if de-associated - from all of its orphan-enabled parents. The new flag ``legacy_is_orphan`` - is added to :func:`.orm.mapper` which re-establishes the - legacy behavior. - - :param non_primary: Specify that this :class:`.Mapper` is in addition - to the "primary" mapper, that is, the one used for persistence. - The :class:`.Mapper` created here may be used for ad-hoc - mapping of the class to an alternate selectable, for loading - only. - - The ``non_primary`` feature is rarely needed with modern - usage. - - :param order_by: A single :class:`.Column` or list of :class:`.Column` - objects for which selection operations should use as the default - ordering for entities. By default mappers have no pre-defined - ordering. - - :param passive_updates: Indicates UPDATE behavior of foreign key - columns when a primary key column changes on a joined-table - inheritance mapping. Defaults to ``True``. - - When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will handle - propagation of an UPDATE from a source column to dependent columns - on joined-table rows. - - When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The :class:`.Mapper` here will - emit an UPDATE statement for the dependent columns during a - primary key change. - - ..seealso:: - - :ref:`passive_updates` - description of a similar feature as - used with :func:`.relationship` - - :param polymorphic_on: Specifies the column, attribute, or - SQL expression used to determine the target class for an - incoming row, when inheriting classes are present. - - This value is commonly a :class:`.Column` object that's - present in the mapped :class:`.Table`:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - - __mapper_args__ = { - "polymorphic_on":discriminator, - "polymorphic_identity":"employee" - } - - It may also be specified - as a SQL expression, as in this example where we - use the :func:`.case` construct to provide a conditional - approach:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - - __mapper_args__ = { - "polymorphic_on":case([ - (discriminator == "EN", "engineer"), - (discriminator == "MA", "manager"), - ], else_="employee"), - "polymorphic_identity":"employee" - } - - It may also refer to any attribute - configured with :func:`.column_property`, or to the - string name of one:: - - class Employee(Base): - __tablename__ = 'employee' - - id = Column(Integer, primary_key=True) - discriminator = Column(String(50)) - employee_type = column_property( - case([ - (discriminator == "EN", "engineer"), - (discriminator == "MA", "manager"), - ], else_="employee") - ) - - __mapper_args__ = { - "polymorphic_on":employee_type, - "polymorphic_identity":"employee" - } - - .. versionchanged:: 0.7.4 - ``polymorphic_on`` may be specified as a SQL expression, - or refer to any attribute configured with - :func:`.column_property`, or to the string name of one. - - When setting ``polymorphic_on`` to reference an - attribute or expression that's not present in the - locally mapped :class:`.Table`, yet the value - of the discriminator should be persisted to the database, - the value of the - discriminator is not automatically set on new - instances; this must be handled by the user, - either through manual means or via event listeners. - A typical approach to establishing such a listener - looks like:: - - from sqlalchemy import event - from sqlalchemy.orm import object_mapper - - @event.listens_for(Employee, "init", propagate=True) - def set_identity(instance, *arg, **kw): - mapper = object_mapper(instance) - instance.discriminator = mapper.polymorphic_identity - - Where above, we assign the value of ``polymorphic_identity`` - for the mapped class to the ``discriminator`` attribute, - thus persisting the value to the ``discriminator`` column - in the database. - - .. seealso:: - - :ref:`inheritance_toplevel` - - :param polymorphic_identity: Specifies the value which - identifies this particular class as returned by the - column expression referred to by the ``polymorphic_on`` - setting. As rows are received, the value corresponding - to the ``polymorphic_on`` column expression is compared - to this value, indicating which subclass should - be used for the newly reconstructed object. - - :param properties: A dictionary mapping the string names of object - attributes to :class:`.MapperProperty` instances, which define the - persistence behavior of that attribute. Note that :class:`.Column` - objects present in - the mapped :class:`.Table` are automatically placed into - ``ColumnProperty`` instances upon mapping, unless overridden. - When using Declarative, this argument is passed automatically, - based on all those :class:`.MapperProperty` instances declared - in the declared class body. - - :param primary_key: A list of :class:`.Column` objects which define the - primary key to be used against this mapper's selectable unit. - This is normally simply the primary key of the ``local_table``, but - can be overridden here. - - :param version_id_col: A :class:`.Column` - that will be used to keep a running version id of rows - in the table. This is used to detect concurrent updates or - the presence of stale data in a flush. The methodology is to - detect if an UPDATE statement does not match the last known - version id, a - :class:`~sqlalchemy.orm.exc.StaleDataError` exception is - thrown. - By default, the column must be of :class:`.Integer` type, - unless ``version_id_generator`` specifies an alternative version - generator. - - .. seealso:: - - :ref:`mapper_version_counter` - discussion of version counting - and rationale. - - :param version_id_generator: Define how new version ids should - be generated. Defaults to ``None``, which indicates that - a simple integer counting scheme be employed. To provide a custom - versioning scheme, provide a callable function of the form:: - - def generate_version(version): - return next_version - - Alternatively, server-side versioning functions such as triggers, - or programmatic versioning schemes outside of the version id generator - may be used, by specifying the value ``False``. - Please see :ref:`server_side_version_counter` for a discussion - of important points when using this option. - - .. versionadded:: 0.9.0 ``version_id_generator`` supports server-side - version number generation. - - .. seealso:: - - :ref:`custom_version_counter` - - :ref:`server_side_version_counter` - - - :param with_polymorphic: A tuple in the form ``(, - )`` indicating the default style of "polymorphic" - loading, that is, which tables are queried at once. is - any single or list of mappers and/or classes indicating the - inherited classes that should be loaded at once. The special value - ``'*'`` may be used to indicate all descending classes should be - loaded immediately. The second tuple argument - indicates a selectable that will be used to query for multiple - classes. - - .. seealso:: - - :ref:`with_polymorphic` - discussion of polymorphic querying techniques. - - """ - - self.class_ = util.assert_arg_type(class_, type, 'class_') - - self.class_manager = None - - self._primary_key_argument = util.to_list(primary_key) - self.non_primary = non_primary - - if order_by is not False: - self.order_by = util.to_list(order_by) - else: - self.order_by = order_by - - self.always_refresh = always_refresh - - if isinstance(version_id_col, MapperProperty): - self.version_id_prop = version_id_col - self.version_id_col = None - else: - self.version_id_col = version_id_col - if version_id_generator is False: - self.version_id_generator = False - elif version_id_generator is None: - self.version_id_generator = lambda x: (x or 0) + 1 - else: - self.version_id_generator = version_id_generator - - self.concrete = concrete - self.single = False - self.inherits = inherits - self.local_table = local_table - self.inherit_condition = inherit_condition - self.inherit_foreign_keys = inherit_foreign_keys - self._init_properties = properties or {} - self._delete_orphans = [] - self.batch = batch - self.eager_defaults = eager_defaults - self.column_prefix = column_prefix - self.polymorphic_on = expression._clause_element_as_expr( - polymorphic_on) - self._dependency_processors = [] - self.validators = util.immutabledict() - self.passive_updates = passive_updates - self.legacy_is_orphan = legacy_is_orphan - self._clause_adapter = None - self._requires_row_aliasing = False - self._inherits_equated_pairs = None - self._memoized_values = {} - self._compiled_cache_size = _compiled_cache_size - self._reconstructor = None - self._deprecated_extensions = util.to_list(extension or []) - - self.allow_partial_pks = allow_partial_pks - - self._set_with_polymorphic(with_polymorphic) - - if isinstance(self.local_table, expression.SelectBase): - raise sa_exc.InvalidRequestError( - "When mapping against a select() construct, map against " - "an alias() of the construct instead." - "This because several databases don't allow a " - "SELECT from a subquery that does not have an alias." - ) - - if self.with_polymorphic and \ - isinstance(self.with_polymorphic[1], - expression.SelectBase): - self.with_polymorphic = (self.with_polymorphic[0], - self.with_polymorphic[1].alias()) - - # our 'polymorphic identity', a string name that when located in a - # result set row indicates this Mapper should be used to construct - # the object instance for that row. - self.polymorphic_identity = polymorphic_identity - - # a dictionary of 'polymorphic identity' names, associating those - # names with Mappers that will be used to construct object instances - # upon a select operation. - if _polymorphic_map is None: - self.polymorphic_map = {} - else: - self.polymorphic_map = _polymorphic_map - - if include_properties is not None: - self.include_properties = util.to_set(include_properties) - else: - self.include_properties = None - if exclude_properties: - self.exclude_properties = util.to_set(exclude_properties) - else: - self.exclude_properties = None - - self.configured = False - - # prevent this mapper from being constructed - # while a configure_mappers() is occurring (and defer a - # configure_mappers() until construction succeeds) - _CONFIGURE_MUTEX.acquire() - try: - self.dispatch._events._new_mapper_instance(class_, self) - self._configure_inheritance() - self._configure_legacy_instrument_class() - self._configure_class_instrumentation() - self._configure_listeners() - self._configure_properties() - self._configure_polymorphic_setter() - self._configure_pks() - Mapper._new_mappers = True - self._log("constructed") - self._expire_memoizations() - finally: - _CONFIGURE_MUTEX.release() - - # major attributes initialized at the classlevel so that - # they can be Sphinx-documented. - - is_mapper = True - """Part of the inspection API.""" - - @property - def mapper(self): - """Part of the inspection API. - - Returns self. - - """ - return self - - @property - def entity(self): - """Part of the inspection API. - - Returns self.class\_. - - """ - return self.class_ - - local_table = None - """The :class:`.Selectable` which this :class:`.Mapper` manages. - - Typically is an instance of :class:`.Table` or :class:`.Alias`. - May also be ``None``. - - The "local" table is the - selectable that the :class:`.Mapper` is directly responsible for - managing from an attribute access and flush perspective. For - non-inheriting mappers, the local table is the same as the - "mapped" table. For joined-table inheritance mappers, local_table - will be the particular sub-table of the overall "join" which - this :class:`.Mapper` represents. If this mapper is a - single-table inheriting mapper, local_table will be ``None``. - - .. seealso:: - - :attr:`~.Mapper.mapped_table`. - - """ - - mapped_table = None - """The :class:`.Selectable` to which this :class:`.Mapper` is mapped. - - Typically an instance of :class:`.Table`, :class:`.Join`, or - :class:`.Alias`. - - The "mapped" table is the selectable that - the mapper selects from during queries. For non-inheriting - mappers, the mapped table is the same as the "local" table. - For joined-table inheritance mappers, mapped_table references the - full :class:`.Join` representing full rows for this particular - subclass. For single-table inheritance mappers, mapped_table - references the base table. - - .. seealso:: - - :attr:`~.Mapper.local_table`. - - """ - - inherits = None - """References the :class:`.Mapper` which this :class:`.Mapper` - inherits from, if any. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - configured = None - """Represent ``True`` if this :class:`.Mapper` has been configured. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - .. seealso:: - - :func:`.configure_mappers`. - - """ - - concrete = None - """Represent ``True`` if this :class:`.Mapper` is a concrete - inheritance mapper. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - tables = None - """An iterable containing the collection of :class:`.Table` objects - which this :class:`.Mapper` is aware of. - - If the mapper is mapped to a :class:`.Join`, or an :class:`.Alias` - representing a :class:`.Select`, the individual :class:`.Table` - objects that comprise the full construct will be represented here. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - primary_key = None - """An iterable containing the collection of :class:`.Column` objects - which comprise the 'primary key' of the mapped table, from the - perspective of this :class:`.Mapper`. - - This list is against the selectable in :attr:`~.Mapper.mapped_table`. In - the case of inheriting mappers, some columns may be managed by a - superclass mapper. For example, in the case of a :class:`.Join`, the - primary key is determined by all of the primary key columns across all - tables referenced by the :class:`.Join`. - - The list is also not necessarily the same as the primary key column - collection associated with the underlying tables; the :class:`.Mapper` - features a ``primary_key`` argument that can override what the - :class:`.Mapper` considers as primary key columns. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - class_ = None - """The Python class which this :class:`.Mapper` maps. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - class_manager = None - """The :class:`.ClassManager` which maintains event listeners - and class-bound descriptors for this :class:`.Mapper`. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - single = None - """Represent ``True`` if this :class:`.Mapper` is a single table - inheritance mapper. - - :attr:`~.Mapper.local_table` will be ``None`` if this flag is set. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - non_primary = None - """Represent ``True`` if this :class:`.Mapper` is a "non-primary" - mapper, e.g. a mapper that is used only to selet rows but not for - persistence management. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - polymorphic_on = None - """The :class:`.Column` or SQL expression specified as the - ``polymorphic_on`` argument - for this :class:`.Mapper`, within an inheritance scenario. - - This attribute is normally a :class:`.Column` instance but - may also be an expression, such as one derived from - :func:`.cast`. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - polymorphic_map = None - """A mapping of "polymorphic identity" identifiers mapped to - :class:`.Mapper` instances, within an inheritance scenario. - - The identifiers can be of any type which is comparable to the - type of column represented by :attr:`~.Mapper.polymorphic_on`. - - An inheritance chain of mappers will all reference the same - polymorphic map object. The object is used to correlate incoming - result rows to target mappers. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - polymorphic_identity = None - """Represent an identifier which is matched against the - :attr:`~.Mapper.polymorphic_on` column during result row loading. - - Used only with inheritance, this object can be of any type which is - comparable to the type of column represented by - :attr:`~.Mapper.polymorphic_on`. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - base_mapper = None - """The base-most :class:`.Mapper` in an inheritance chain. - - In a non-inheriting scenario, this attribute will always be this - :class:`.Mapper`. In an inheritance scenario, it references - the :class:`.Mapper` which is parent to all other :class:`.Mapper` - objects in the inheritance chain. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - columns = None - """A collection of :class:`.Column` or other scalar expression - objects maintained by this :class:`.Mapper`. - - The collection behaves the same as that of the ``c`` attribute on - any :class:`.Table` object, except that only those columns included in - this mapping are present, and are keyed based on the attribute name - defined in the mapping, not necessarily the ``key`` attribute of the - :class:`.Column` itself. Additionally, scalar expressions mapped - by :func:`.column_property` are also present here. - - This is a *read only* attribute determined during mapper construction. - Behavior is undefined if directly modified. - - """ - - validators = None - """An immutable dictionary of attributes which have been decorated - using the :func:`~.orm.validates` decorator. - - The dictionary contains string attribute names as keys - mapped to the actual validation method. - - """ - - c = None - """A synonym for :attr:`~.Mapper.columns`.""" - - @util.memoized_property - def _path_registry(self): - return PathRegistry.per_mapper(self) - - def _configure_inheritance(self): - """Configure settings related to inherting and/or inherited mappers - being present.""" - - # a set of all mappers which inherit from this one. - self._inheriting_mappers = util.WeakSequence() - - if self.inherits: - if isinstance(self.inherits, type): - self.inherits = class_mapper(self.inherits, configure=False) - if not issubclass(self.class_, self.inherits.class_): - raise sa_exc.ArgumentError( - "Class '%s' does not inherit from '%s'" % - (self.class_.__name__, self.inherits.class_.__name__)) - if self.non_primary != self.inherits.non_primary: - np = not self.non_primary and "primary" or "non-primary" - raise sa_exc.ArgumentError( - "Inheritance of %s mapper for class '%s' is " - "only allowed from a %s mapper" % - (np, self.class_.__name__, np)) - # inherit_condition is optional. - if self.local_table is None: - self.local_table = self.inherits.local_table - self.mapped_table = self.inherits.mapped_table - self.single = True - elif not self.local_table is self.inherits.local_table: - if self.concrete: - self.mapped_table = self.local_table - for mapper in self.iterate_to_root(): - if mapper.polymorphic_on is not None: - mapper._requires_row_aliasing = True - else: - if self.inherit_condition is None: - # figure out inherit condition from our table to the - # immediate table of the inherited mapper, not its - # full table which could pull in other stuff we dont - # want (allows test/inheritance.InheritTest4 to pass) - self.inherit_condition = sql_util.join_condition( - self.inherits.local_table, - self.local_table) - self.mapped_table = sql.join( - self.inherits.mapped_table, - self.local_table, - self.inherit_condition) - - fks = util.to_set(self.inherit_foreign_keys) - self._inherits_equated_pairs = sql_util.criterion_as_pairs( - self.mapped_table.onclause, - consider_as_foreign_keys=fks) - else: - self.mapped_table = self.local_table - - if self.polymorphic_identity is not None and not self.concrete: - self._identity_class = self.inherits._identity_class - else: - self._identity_class = self.class_ - - if self.version_id_col is None: - self.version_id_col = self.inherits.version_id_col - self.version_id_generator = self.inherits.version_id_generator - elif self.inherits.version_id_col is not None and \ - self.version_id_col is not self.inherits.version_id_col: - util.warn( - "Inheriting version_id_col '%s' does not match inherited " - "version_id_col '%s' and will not automatically populate " - "the inherited versioning column. " - "version_id_col should only be specified on " - "the base-most mapper that includes versioning." % - (self.version_id_col.description, - self.inherits.version_id_col.description) - ) - - if self.order_by is False and \ - not self.concrete and \ - self.inherits.order_by is not False: - self.order_by = self.inherits.order_by - - self.polymorphic_map = self.inherits.polymorphic_map - self.batch = self.inherits.batch - self.inherits._inheriting_mappers.append(self) - self.base_mapper = self.inherits.base_mapper - self.passive_updates = self.inherits.passive_updates - self._all_tables = self.inherits._all_tables - - if self.polymorphic_identity is not None: - self.polymorphic_map[self.polymorphic_identity] = self - - else: - self._all_tables = set() - self.base_mapper = self - self.mapped_table = self.local_table - if self.polymorphic_identity is not None: - self.polymorphic_map[self.polymorphic_identity] = self - self._identity_class = self.class_ - - if self.mapped_table is None: - raise sa_exc.ArgumentError( - "Mapper '%s' does not have a mapped_table specified." - % self) - - def _set_with_polymorphic(self, with_polymorphic): - if with_polymorphic == '*': - self.with_polymorphic = ('*', None) - elif isinstance(with_polymorphic, (tuple, list)): - if isinstance(with_polymorphic[0], util.string_types + (tuple, list)): - self.with_polymorphic = with_polymorphic - else: - self.with_polymorphic = (with_polymorphic, None) - elif with_polymorphic is not None: - raise sa_exc.ArgumentError("Invalid setting for with_polymorphic") - else: - self.with_polymorphic = None - - if isinstance(self.local_table, expression.SelectBase): - raise sa_exc.InvalidRequestError( - "When mapping against a select() construct, map against " - "an alias() of the construct instead." - "This because several databases don't allow a " - "SELECT from a subquery that does not have an alias." - ) - - if self.with_polymorphic and \ - isinstance(self.with_polymorphic[1], - expression.SelectBase): - self.with_polymorphic = (self.with_polymorphic[0], - self.with_polymorphic[1].alias()) - if self.configured: - self._expire_memoizations() - - def _set_concrete_base(self, mapper): - """Set the given :class:`.Mapper` as the 'inherits' for this - :class:`.Mapper`, assuming this :class:`.Mapper` is concrete - and does not already have an inherits.""" - - assert self.concrete - assert not self.inherits - assert isinstance(mapper, Mapper) - self.inherits = mapper - self.inherits.polymorphic_map.update(self.polymorphic_map) - self.polymorphic_map = self.inherits.polymorphic_map - for mapper in self.iterate_to_root(): - if mapper.polymorphic_on is not None: - mapper._requires_row_aliasing = True - self.batch = self.inherits.batch - for mp in self.self_and_descendants: - mp.base_mapper = self.inherits.base_mapper - self.inherits._inheriting_mappers.append(self) - self.passive_updates = self.inherits.passive_updates - self._all_tables = self.inherits._all_tables - for key, prop in mapper._props.items(): - if key not in self._props and \ - not self._should_exclude(key, key, local=False, - column=None): - self._adapt_inherited_property(key, prop, False) - - def _set_polymorphic_on(self, polymorphic_on): - self.polymorphic_on = polymorphic_on - self._configure_polymorphic_setter(True) - - def _configure_legacy_instrument_class(self): - - if self.inherits: - self.dispatch._update(self.inherits.dispatch) - super_extensions = set( - chain(*[m._deprecated_extensions - for m in self.inherits.iterate_to_root()])) - else: - super_extensions = set() - - for ext in self._deprecated_extensions: - if ext not in super_extensions: - ext._adapt_instrument_class(self, ext) - - def _configure_listeners(self): - if self.inherits: - super_extensions = set( - chain(*[m._deprecated_extensions - for m in self.inherits.iterate_to_root()])) - else: - super_extensions = set() - - for ext in self._deprecated_extensions: - if ext not in super_extensions: - ext._adapt_listener(self, ext) - - def _configure_class_instrumentation(self): - """If this mapper is to be a primary mapper (i.e. the - non_primary flag is not set), associate this Mapper with the - given class_ and entity name. - - Subsequent calls to ``class_mapper()`` for the class_/entity - name combination will return this mapper. Also decorate the - `__init__` method on the mapped class to include optional - auto-session attachment logic. - - """ - manager = attributes.manager_of_class(self.class_) - - if self.non_primary: - if not manager or not manager.is_mapped: - raise sa_exc.InvalidRequestError( - "Class %s has no primary mapper configured. Configure " - "a primary mapper first before setting up a non primary " - "Mapper." % self.class_) - self.class_manager = manager - self._identity_class = manager.mapper._identity_class - _mapper_registry[self] = True - return - - if manager is not None: - assert manager.class_ is self.class_ - if manager.is_mapped: - raise sa_exc.ArgumentError( - "Class '%s' already has a primary mapper defined. " - "Use non_primary=True to " - "create a non primary Mapper. clear_mappers() will " - "remove *all* current mappers from all classes." % - self.class_) - #else: - # a ClassManager may already exist as - # ClassManager.instrument_attribute() creates - # new managers for each subclass if they don't yet exist. - - _mapper_registry[self] = True - - self.dispatch.instrument_class(self, self.class_) - - if manager is None: - manager = instrumentation.register_class(self.class_) - - self.class_manager = manager - - manager.mapper = self - manager.deferred_scalar_loader = util.partial( - loading.load_scalar_attributes, self) - - # The remaining members can be added by any mapper, - # e_name None or not. - if manager.info.get(_INSTRUMENTOR, False): - return - - event.listen(manager, 'first_init', _event_on_first_init, raw=True) - event.listen(manager, 'init', _event_on_init, raw=True) - event.listen(manager, 'resurrect', _event_on_resurrect, raw=True) - - for key, method in util.iterate_attributes(self.class_): - if isinstance(method, types.FunctionType): - if hasattr(method, '__sa_reconstructor__'): - self._reconstructor = method - event.listen(manager, 'load', _event_on_load, raw=True) - elif hasattr(method, '__sa_validators__'): - validation_opts = method.__sa_validation_opts__ - for name in method.__sa_validators__: - self.validators = self.validators.union( - {name: (method, validation_opts)} - ) - - manager.info[_INSTRUMENTOR] = self - - - @classmethod - def _configure_all(cls): - """Class-level path to the :func:`.configure_mappers` call. - """ - configure_mappers() - - def dispose(self): - # Disable any attribute-based compilation. - self.configured = True - - if hasattr(self, '_configure_failed'): - del self._configure_failed - - if not self.non_primary and \ - self.class_manager is not None and \ - self.class_manager.is_mapped and \ - self.class_manager.mapper is self: - instrumentation.unregister_class(self.class_) - - def _configure_pks(self): - - self.tables = sql_util.find_tables(self.mapped_table) - - self._pks_by_table = {} - self._cols_by_table = {} - - all_cols = util.column_set(chain(*[ - col.proxy_set for col in - self._columntoproperty])) - - pk_cols = util.column_set(c for c in all_cols if c.primary_key) - - # identify primary key columns which are also mapped by this mapper. - tables = set(self.tables + [self.mapped_table]) - self._all_tables.update(tables) - for t in tables: - if t.primary_key and pk_cols.issuperset(t.primary_key): - # ordering is important since it determines the ordering of - # mapper.primary_key (and therefore query.get()) - self._pks_by_table[t] = \ - util.ordered_column_set(t.primary_key).\ - intersection(pk_cols) - self._cols_by_table[t] = \ - util.ordered_column_set(t.c).\ - intersection(all_cols) - - # determine cols that aren't expressed within our tables; mark these - # as "read only" properties which are refreshed upon INSERT/UPDATE - self._readonly_props = set( - self._columntoproperty[col] - for col in self._columntoproperty - if not hasattr(col, 'table') or - col.table not in self._cols_by_table) - - # if explicit PK argument sent, add those columns to the - # primary key mappings - if self._primary_key_argument: - for k in self._primary_key_argument: - if k.table not in self._pks_by_table: - self._pks_by_table[k.table] = util.OrderedSet() - self._pks_by_table[k.table].add(k) - - # otherwise, see that we got a full PK for the mapped table - elif self.mapped_table not in self._pks_by_table or \ - len(self._pks_by_table[self.mapped_table]) == 0: - raise sa_exc.ArgumentError( - "Mapper %s could not assemble any primary " - "key columns for mapped table '%s'" % - (self, self.mapped_table.description)) - elif self.local_table not in self._pks_by_table and \ - isinstance(self.local_table, schema.Table): - util.warn("Could not assemble any primary " - "keys for locally mapped table '%s' - " - "no rows will be persisted in this Table." - % self.local_table.description) - - if self.inherits and \ - not self.concrete and \ - not self._primary_key_argument: - # if inheriting, the "primary key" for this mapper is - # that of the inheriting (unless concrete or explicit) - self.primary_key = self.inherits.primary_key - else: - # determine primary key from argument or mapped_table pks - - # reduce to the minimal set of columns - if self._primary_key_argument: - primary_key = sql_util.reduce_columns( - [self.mapped_table.corresponding_column(c) for c in - self._primary_key_argument], - ignore_nonexistent_tables=True) - else: - primary_key = sql_util.reduce_columns( - self._pks_by_table[self.mapped_table], - ignore_nonexistent_tables=True) - - if len(primary_key) == 0: - raise sa_exc.ArgumentError( - "Mapper %s could not assemble any primary " - "key columns for mapped table '%s'" % - (self, self.mapped_table.description)) - - self.primary_key = tuple(primary_key) - self._log("Identified primary key columns: %s", primary_key) - - def _configure_properties(self): - - # Column and other ClauseElement objects which are mapped - self.columns = self.c = util.OrderedProperties() - - # object attribute names mapped to MapperProperty objects - self._props = util.OrderedDict() - - # table columns mapped to lists of MapperProperty objects - # using a list allows a single column to be defined as - # populating multiple object attributes - self._columntoproperty = _ColumnMapping(self) - - # load custom properties - if self._init_properties: - for key, prop in self._init_properties.items(): - self._configure_property(key, prop, False) - - # pull properties from the inherited mapper if any. - if self.inherits: - for key, prop in self.inherits._props.items(): - if key not in self._props and \ - not self._should_exclude(key, key, local=False, - column=None): - self._adapt_inherited_property(key, prop, False) - - # create properties for each column in the mapped table, - # for those columns which don't already map to a property - for column in self.mapped_table.columns: - if column in self._columntoproperty: - continue - - column_key = (self.column_prefix or '') + column.key - - if self._should_exclude( - column.key, column_key, - local=self.local_table.c.contains_column(column), - column=column - ): - continue - - # adjust the "key" used for this column to that - # of the inheriting mapper - for mapper in self.iterate_to_root(): - if column in mapper._columntoproperty: - column_key = mapper._columntoproperty[column].key - - self._configure_property(column_key, - column, - init=False, - setparent=True) - - def _configure_polymorphic_setter(self, init=False): - """Configure an attribute on the mapper representing the - 'polymorphic_on' column, if applicable, and not - already generated by _configure_properties (which is typical). - - Also create a setter function which will assign this - attribute to the value of the 'polymorphic_identity' - upon instance construction, also if applicable. This - routine will run when an instance is created. - - """ - setter = False - - if self.polymorphic_on is not None: - setter = True - - if isinstance(self.polymorphic_on, util.string_types): - # polymorphic_on specified as as string - link - # it to mapped ColumnProperty - try: - self.polymorphic_on = self._props[self.polymorphic_on] - except KeyError: - raise sa_exc.ArgumentError( - "Can't determine polymorphic_on " - "value '%s' - no attribute is " - "mapped to this name." % self.polymorphic_on) - - if self.polymorphic_on in self._columntoproperty: - # polymorphic_on is a column that is already mapped - # to a ColumnProperty - prop = self._columntoproperty[self.polymorphic_on] - polymorphic_key = prop.key - self.polymorphic_on = prop.columns[0] - polymorphic_key = prop.key - elif isinstance(self.polymorphic_on, MapperProperty): - # polymorphic_on is directly a MapperProperty, - # ensure it's a ColumnProperty - if not isinstance(self.polymorphic_on, - properties.ColumnProperty): - raise sa_exc.ArgumentError( - "Only direct column-mapped " - "property or SQL expression " - "can be passed for polymorphic_on") - prop = self.polymorphic_on - self.polymorphic_on = prop.columns[0] - polymorphic_key = prop.key - elif not expression._is_column(self.polymorphic_on): - # polymorphic_on is not a Column and not a ColumnProperty; - # not supported right now. - raise sa_exc.ArgumentError( - "Only direct column-mapped " - "property or SQL expression " - "can be passed for polymorphic_on" - ) - else: - # polymorphic_on is a Column or SQL expression and - # doesn't appear to be mapped. this means it can be 1. - # only present in the with_polymorphic selectable or - # 2. a totally standalone SQL expression which we'd - # hope is compatible with this mapper's mapped_table - col = self.mapped_table.corresponding_column( - self.polymorphic_on) - if col is None: - # polymorphic_on doesn't derive from any - # column/expression isn't present in the mapped - # table. we will make a "hidden" ColumnProperty - # for it. Just check that if it's directly a - # schema.Column and we have with_polymorphic, it's - # likely a user error if the schema.Column isn't - # represented somehow in either mapped_table or - # with_polymorphic. Otherwise as of 0.7.4 we - # just go with it and assume the user wants it - # that way (i.e. a CASE statement) - setter = False - instrument = False - col = self.polymorphic_on - if isinstance(col, schema.Column) and ( - self.with_polymorphic is None or \ - self.with_polymorphic[1].\ - corresponding_column(col) is None - ): - raise sa_exc.InvalidRequestError( - "Could not map polymorphic_on column " - "'%s' to the mapped table - polymorphic " - "loads will not function properly" - % col.description) - else: - # column/expression that polymorphic_on derives from - # is present in our mapped table - # and is probably mapped, but polymorphic_on itself - # is not. This happens when - # the polymorphic_on is only directly present in the - # with_polymorphic selectable, as when use - # polymorphic_union. - # we'll make a separate ColumnProperty for it. - instrument = True - key = getattr(col, 'key', None) - if key: - if self._should_exclude(col.key, col.key, False, col): - raise sa_exc.InvalidRequestError( - "Cannot exclude or override the " - "discriminator column %r" % - col.key) - else: - self.polymorphic_on = col = \ - col.label("_sa_polymorphic_on") - key = col.key - - self._configure_property( - key, - properties.ColumnProperty(col, - _instrument=instrument), - init=init, setparent=True) - polymorphic_key = key - else: - # no polymorphic_on was set. - # check inheriting mappers for one. - for mapper in self.iterate_to_root(): - # determine if polymorphic_on of the parent - # should be propagated here. If the col - # is present in our mapped table, or if our mapped - # table is the same as the parent (i.e. single table - # inheritance), we can use it - if mapper.polymorphic_on is not None: - if self.mapped_table is mapper.mapped_table: - self.polymorphic_on = mapper.polymorphic_on - else: - self.polymorphic_on = \ - self.mapped_table.corresponding_column( - mapper.polymorphic_on) - # we can use the parent mapper's _set_polymorphic_identity - # directly; it ensures the polymorphic_identity of the - # instance's mapper is used so is portable to subclasses. - if self.polymorphic_on is not None: - self._set_polymorphic_identity = \ - mapper._set_polymorphic_identity - self._validate_polymorphic_identity = \ - mapper._validate_polymorphic_identity - else: - self._set_polymorphic_identity = None - return - - if setter: - def _set_polymorphic_identity(state): - dict_ = state.dict - state.get_impl(polymorphic_key).set(state, dict_, - state.manager.mapper.polymorphic_identity, None) - - def _validate_polymorphic_identity(mapper, state, dict_): - if polymorphic_key in dict_ and \ - dict_[polymorphic_key] not in \ - mapper._acceptable_polymorphic_identities: - util.warn( - "Flushing object %s with " - "incompatible polymorphic identity %r; the " - "object may not refresh and/or load correctly" % ( - state_str(state), - dict_[polymorphic_key] - ) - ) - - self._set_polymorphic_identity = _set_polymorphic_identity - self._validate_polymorphic_identity = _validate_polymorphic_identity - else: - self._set_polymorphic_identity = None - - - _validate_polymorphic_identity = None - - @_memoized_configured_property - def _version_id_prop(self): - if self.version_id_col is not None: - return self._columntoproperty[self.version_id_col] - else: - return None - - @_memoized_configured_property - def _acceptable_polymorphic_identities(self): - identities = set() - - stack = deque([self]) - while stack: - item = stack.popleft() - if item.mapped_table is self.mapped_table: - identities.add(item.polymorphic_identity) - stack.extend(item._inheriting_mappers) - - return identities - - def _adapt_inherited_property(self, key, prop, init): - if not self.concrete: - self._configure_property(key, prop, init=False, setparent=False) - elif key not in self._props: - self._configure_property( - key, - properties.ConcreteInheritedProperty(), - init=init, setparent=True) - - def _configure_property(self, key, prop, init=True, setparent=True): - self._log("_configure_property(%s, %s)", key, prop.__class__.__name__) - - if not isinstance(prop, MapperProperty): - prop = self._property_from_column(key, prop) - - if isinstance(prop, properties.ColumnProperty): - col = self.mapped_table.corresponding_column(prop.columns[0]) - - # if the column is not present in the mapped table, - # test if a column has been added after the fact to the - # parent table (or their parent, etc.) [ticket:1570] - if col is None and self.inherits: - path = [self] - for m in self.inherits.iterate_to_root(): - col = m.local_table.corresponding_column(prop.columns[0]) - if col is not None: - for m2 in path: - m2.mapped_table._reset_exported() - col = self.mapped_table.corresponding_column( - prop.columns[0]) - break - path.append(m) - - # subquery expression, column not present in the mapped - # selectable. - if col is None: - col = prop.columns[0] - - # column is coming in after _readonly_props was - # initialized; check for 'readonly' - if hasattr(self, '_readonly_props') and \ - (not hasattr(col, 'table') or - col.table not in self._cols_by_table): - self._readonly_props.add(prop) - - else: - # if column is coming in after _cols_by_table was - # initialized, ensure the col is in the right set - if hasattr(self, '_cols_by_table') and \ - col.table in self._cols_by_table and \ - col not in self._cols_by_table[col.table]: - self._cols_by_table[col.table].add(col) - - # if this properties.ColumnProperty represents the "polymorphic - # discriminator" column, mark it. We'll need this when rendering - # columns in SELECT statements. - if not hasattr(prop, '_is_polymorphic_discriminator'): - prop._is_polymorphic_discriminator = \ - (col is self.polymorphic_on or - prop.columns[0] is self.polymorphic_on) - - self.columns[key] = col - for col in prop.columns + prop._orig_columns: - for col in col.proxy_set: - self._columntoproperty[col] = prop - - prop.key = key - - if setparent: - prop.set_parent(self, init) - - if key in self._props and \ - getattr(self._props[key], '_mapped_by_synonym', False): - syn = self._props[key]._mapped_by_synonym - raise sa_exc.ArgumentError( - "Can't call map_column=True for synonym %r=%r, " - "a ColumnProperty already exists keyed to the name " - "%r for column %r" % (syn, key, key, syn) - ) - - if key in self._props and \ - not isinstance(prop, properties.ColumnProperty) and \ - not isinstance(self._props[key], properties.ColumnProperty): - util.warn("Property %s on %s being replaced with new " - "property %s; the old property will be discarded" % ( - self._props[key], - self, - prop, - )) - - self._props[key] = prop - - if not self.non_primary: - prop.instrument_class(self) - - for mapper in self._inheriting_mappers: - mapper._adapt_inherited_property(key, prop, init) - - if init: - prop.init() - prop.post_instrument_class(self) - - if self.configured: - self._expire_memoizations() - - def _property_from_column(self, key, prop): - """generate/update a :class:`.ColumnProprerty` given a - :class:`.Column` object. """ - - # we were passed a Column or a list of Columns; - # generate a properties.ColumnProperty - columns = util.to_list(prop) - column = columns[0] - if not expression._is_column(column): - raise sa_exc.ArgumentError( - "%s=%r is not an instance of MapperProperty or Column" - % (key, prop)) - - prop = self._props.get(key, None) - - if isinstance(prop, properties.ColumnProperty): - if prop.parent is self: - raise sa_exc.InvalidRequestError( - "Implicitly combining column %s with column " - "%s under attribute '%s'. Please configure one " - "or more attributes for these same-named columns " - "explicitly." - % (prop.columns[-1], column, key)) - - # existing properties.ColumnProperty from an inheriting - # mapper. make a copy and append our column to it - prop = prop.copy() - prop.columns.insert(0, column) - self._log("inserting column to existing list " - "in properties.ColumnProperty %s" % (key)) - return prop - elif prop is None or isinstance(prop, - properties.ConcreteInheritedProperty): - mapped_column = [] - for c in columns: - mc = self.mapped_table.corresponding_column(c) - if mc is None: - mc = self.local_table.corresponding_column(c) - if mc is not None: - # if the column is in the local table but not the - # mapped table, this corresponds to adding a - # column after the fact to the local table. - # [ticket:1523] - self.mapped_table._reset_exported() - mc = self.mapped_table.corresponding_column(c) - if mc is None: - raise sa_exc.ArgumentError( - "When configuring property '%s' on %s, " - "column '%s' is not represented in the mapper's " - "table. Use the `column_property()` function to " - "force this column to be mapped as a read-only " - "attribute." % (key, self, c)) - mapped_column.append(mc) - return properties.ColumnProperty(*mapped_column) - else: - raise sa_exc.ArgumentError( - "WARNING: when configuring property '%s' on %s, " - "column '%s' conflicts with property '%r'. " - "To resolve this, map the column to the class under a " - "different name in the 'properties' dictionary. Or, " - "to remove all awareness of the column entirely " - "(including its availability as a foreign key), " - "use the 'include_properties' or 'exclude_properties' " - "mapper arguments to control specifically which table " - "columns get mapped." % - (key, self, column.key, prop)) - - def _post_configure_properties(self): - """Call the ``init()`` method on all ``MapperProperties`` - attached to this mapper. - - This is a deferred configuration step which is intended - to execute once all mappers have been constructed. - - """ - - self._log("_post_configure_properties() started") - l = [(key, prop) for key, prop in self._props.items()] - for key, prop in l: - self._log("initialize prop %s", key) - - if prop.parent is self and not prop._configure_started: - prop.init() - - if prop._configure_finished: - prop.post_instrument_class(self) - - self._log("_post_configure_properties() complete") - self.configured = True - - def add_properties(self, dict_of_properties): - """Add the given dictionary of properties to this mapper, - using `add_property`. - - """ - for key, value in dict_of_properties.items(): - self.add_property(key, value) - - def add_property(self, key, prop): - """Add an individual MapperProperty to this mapper. - - If the mapper has not been configured yet, just adds the - property to the initial properties dictionary sent to the - constructor. If this Mapper has already been configured, then - the given MapperProperty is configured immediately. - - """ - self._init_properties[key] = prop - self._configure_property(key, prop, init=self.configured) - - def _expire_memoizations(self): - for mapper in self.iterate_to_root(): - _memoized_configured_property.expire_instance(mapper) - - @property - def _log_desc(self): - return "(" + self.class_.__name__ + \ - "|" + \ - (self.local_table is not None and - self.local_table.description or - str(self.local_table)) +\ - (self.non_primary and - "|non-primary" or "") + ")" - - def _log(self, msg, *args): - self.logger.info( - "%s " + msg, *((self._log_desc,) + args) - ) - - def _log_debug(self, msg, *args): - self.logger.debug( - "%s " + msg, *((self._log_desc,) + args) - ) - - def __repr__(self): - return '' % ( - id(self), self.class_.__name__) - - def __str__(self): - return "Mapper|%s|%s%s" % ( - self.class_.__name__, - self.local_table is not None and - self.local_table.description or None, - self.non_primary and "|non-primary" or "" - ) - - def _is_orphan(self, state): - orphan_possible = False - for mapper in self.iterate_to_root(): - for (key, cls) in mapper._delete_orphans: - orphan_possible = True - - has_parent = attributes.manager_of_class(cls).has_parent( - state, key, optimistic=state.has_identity) - - if self.legacy_is_orphan and has_parent: - return False - elif not self.legacy_is_orphan and not has_parent: - return True - - if self.legacy_is_orphan: - return orphan_possible - else: - return False - - def has_property(self, key): - return key in self._props - - def get_property(self, key, _configure_mappers=True): - """return a MapperProperty associated with the given key. - """ - - if _configure_mappers and Mapper._new_mappers: - configure_mappers() - - try: - return self._props[key] - except KeyError: - raise sa_exc.InvalidRequestError( - "Mapper '%s' has no property '%s'" % (self, key)) - - def get_property_by_column(self, column): - """Given a :class:`.Column` object, return the - :class:`.MapperProperty` which maps this column.""" - - return self._columntoproperty[column] - - @property - def iterate_properties(self): - """return an iterator of all MapperProperty objects.""" - if Mapper._new_mappers: - configure_mappers() - return iter(self._props.values()) - - def _mappers_from_spec(self, spec, selectable): - """given a with_polymorphic() argument, return the set of mappers it - represents. - - Trims the list of mappers to just those represented within the given - selectable, if present. This helps some more legacy-ish mappings. - - """ - if spec == '*': - mappers = list(self.self_and_descendants) - elif spec: - mappers = set() - for m in util.to_list(spec): - m = _class_to_mapper(m) - if not m.isa(self): - raise sa_exc.InvalidRequestError( - "%r does not inherit from %r" % - (m, self)) - - if selectable is None: - mappers.update(m.iterate_to_root()) - else: - mappers.add(m) - mappers = [m for m in self.self_and_descendants if m in mappers] - else: - mappers = [] - - if selectable is not None: - tables = set(sql_util.find_tables(selectable, - include_aliases=True)) - mappers = [m for m in mappers if m.local_table in tables] - return mappers - - def _selectable_from_mappers(self, mappers, innerjoin): - """given a list of mappers (assumed to be within this mapper's - inheritance hierarchy), construct an outerjoin amongst those mapper's - mapped tables. - - """ - from_obj = self.mapped_table - for m in mappers: - if m is self: - continue - if m.concrete: - raise sa_exc.InvalidRequestError( - "'with_polymorphic()' requires 'selectable' argument " - "when concrete-inheriting mappers are used.") - elif not m.single: - if innerjoin: - from_obj = from_obj.join(m.local_table, - m.inherit_condition) - else: - from_obj = from_obj.outerjoin(m.local_table, - m.inherit_condition) - - return from_obj - - @_memoized_configured_property - def _single_table_criterion(self): - if self.single and \ - self.inherits and \ - self.polymorphic_on is not None: - return self.polymorphic_on.in_( - m.polymorphic_identity - for m in self.self_and_descendants) - else: - return None - - @_memoized_configured_property - def _with_polymorphic_mappers(self): - if Mapper._new_mappers: - configure_mappers() - if not self.with_polymorphic: - return [] - return self._mappers_from_spec(*self.with_polymorphic) - - @_memoized_configured_property - def _with_polymorphic_selectable(self): - if not self.with_polymorphic: - return self.mapped_table - - spec, selectable = self.with_polymorphic - if selectable is not None: - return selectable - else: - return self._selectable_from_mappers( - self._mappers_from_spec(spec, selectable), - False) - - with_polymorphic_mappers = _with_polymorphic_mappers - """The list of :class:`.Mapper` objects included in the - default "polymorphic" query. - - """ - - @property - def selectable(self): - """The :func:`.select` construct this :class:`.Mapper` selects from - by default. - - Normally, this is equivalent to :attr:`.mapped_table`, unless - the ``with_polymorphic`` feature is in use, in which case the - full "polymorphic" selectable is returned. - - """ - return self._with_polymorphic_selectable - - def _with_polymorphic_args(self, spec=None, selectable=False, - innerjoin=False): - if self.with_polymorphic: - if not spec: - spec = self.with_polymorphic[0] - if selectable is False: - selectable = self.with_polymorphic[1] - elif selectable is False: - selectable = None - mappers = self._mappers_from_spec(spec, selectable) - if selectable is not None: - return mappers, selectable - else: - return mappers, self._selectable_from_mappers(mappers, - innerjoin) - - @_memoized_configured_property - def _polymorphic_properties(self): - return list(self._iterate_polymorphic_properties( - self._with_polymorphic_mappers)) - - - def _iterate_polymorphic_properties(self, mappers=None): - """Return an iterator of MapperProperty objects which will render into - a SELECT.""" - if mappers is None: - mappers = self._with_polymorphic_mappers - - if not mappers: - for c in self.iterate_properties: - yield c - else: - # in the polymorphic case, filter out discriminator columns - # from other mappers, as these are sometimes dependent on that - # mapper's polymorphic selectable (which we don't want rendered) - for c in util.unique_list( - chain(*[ - list(mapper.iterate_properties) for mapper in - [self] + mappers - ]) - ): - if getattr(c, '_is_polymorphic_discriminator', False) and \ - (self.polymorphic_on is None or - c.columns[0] is not self.polymorphic_on): - continue - yield c - - @util.memoized_property - def attrs(self): - """A namespace of all :class:`.MapperProperty` objects - associated this mapper. - - This is an object that provides each property based on - its key name. For instance, the mapper for a - ``User`` class which has ``User.name`` attribute would - provide ``mapper.attrs.name``, which would be the - :class:`.ColumnProperty` representing the ``name`` - column. The namespace object can also be iterated, - which would yield each :class:`.MapperProperty`. - - :class:`.Mapper` has several pre-filtered views - of this attribute which limit the types of properties - returned, inclding :attr:`.synonyms`, :attr:`.column_attrs`, - :attr:`.relationships`, and :attr:`.composites`. - - .. seealso:: - - :attr:`.Mapper.all_orm_descriptors` - - """ - if Mapper._new_mappers: - configure_mappers() - return util.ImmutableProperties(self._props) - - @util.memoized_property - def all_orm_descriptors(self): - """A namespace of all :class:`._InspectionAttr` attributes associated - with the mapped class. - - These attributes are in all cases Python :term:`descriptors` associated - with the mapped class or its superclasses. - - This namespace includes attributes that are mapped to the class - as well as attributes declared by extension modules. - It includes any Python descriptor type that inherits from - :class:`._InspectionAttr`. This includes :class:`.QueryableAttribute`, - as well as extension types such as :class:`.hybrid_property`, - :class:`.hybrid_method` and :class:`.AssociationProxy`. - - To distinguish between mapped attributes and extension attributes, - the attribute :attr:`._InspectionAttr.extension_type` will refer - to a constant that distinguishes between different extension types. - - When dealing with a :class:`.QueryableAttribute`, the - :attr:`.QueryableAttribute.property` attribute refers to the - :class:`.MapperProperty` property, which is what you get when referring - to the collection of mapped properties via :attr:`.Mapper.attrs`. - - .. versionadded:: 0.8.0 - - .. seealso:: - - :attr:`.Mapper.attrs` - - """ - return util.ImmutableProperties( - dict(self.class_manager._all_sqla_attributes())) - - @_memoized_configured_property - def synonyms(self): - """Return a namespace of all :class:`.SynonymProperty` - properties maintained by this :class:`.Mapper`. - - .. seealso:: - - :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` - objects. - - """ - return self._filter_properties(properties.SynonymProperty) - - @_memoized_configured_property - def column_attrs(self): - """Return a namespace of all :class:`.ColumnProperty` - properties maintained by this :class:`.Mapper`. - - .. seealso:: - - :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` - objects. - - """ - return self._filter_properties(properties.ColumnProperty) - - @_memoized_configured_property - def relationships(self): - """Return a namespace of all :class:`.RelationshipProperty` - properties maintained by this :class:`.Mapper`. - - .. seealso:: - - :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` - objects. - - """ - return self._filter_properties(properties.RelationshipProperty) - - @_memoized_configured_property - def composites(self): - """Return a namespace of all :class:`.CompositeProperty` - properties maintained by this :class:`.Mapper`. - - .. seealso:: - - :attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty` - objects. - - """ - return self._filter_properties(properties.CompositeProperty) - - def _filter_properties(self, type_): - if Mapper._new_mappers: - configure_mappers() - return util.ImmutableProperties(util.OrderedDict( - (k, v) for k, v in self._props.items() - if isinstance(v, type_) - )) - - @_memoized_configured_property - def _get_clause(self): - """create a "get clause" based on the primary key. this is used - by query.get() and many-to-one lazyloads to load this item - by primary key. - - """ - params = [(primary_key, sql.bindparam(None, type_=primary_key.type)) - for primary_key in self.primary_key] - return sql.and_(*[k == v for (k, v) in params]), \ - util.column_dict(params) - - @_memoized_configured_property - def _equivalent_columns(self): - """Create a map of all *equivalent* columns, based on - the determination of column pairs that are equated to - one another based on inherit condition. This is designed - to work with the queries that util.polymorphic_union - comes up with, which often don't include the columns from - the base table directly (including the subclass table columns - only). - - The resulting structure is a dictionary of columns mapped - to lists of equivalent columns, i.e. - - { - tablea.col1: - set([tableb.col1, tablec.col1]), - tablea.col2: - set([tabled.col2]) - } - - """ - result = util.column_dict() - - def visit_binary(binary): - if binary.operator == operators.eq: - if binary.left in result: - result[binary.left].add(binary.right) - else: - result[binary.left] = util.column_set((binary.right,)) - if binary.right in result: - result[binary.right].add(binary.left) - else: - result[binary.right] = util.column_set((binary.left,)) - for mapper in self.base_mapper.self_and_descendants: - if mapper.inherit_condition is not None: - visitors.traverse( - mapper.inherit_condition, {}, - {'binary': visit_binary}) - - return result - - def _is_userland_descriptor(self, obj): - if isinstance(obj, (_MappedAttribute, - instrumentation.ClassManager, - expression.ColumnElement)): - return False - else: - return True - - def _should_exclude(self, name, assigned_name, local, column): - """determine whether a particular property should be implicitly - present on the class. - - This occurs when properties are propagated from an inherited class, or - are applied from the columns present in the mapped table. - - """ - - # check for class-bound attributes and/or descriptors, - # either local or from an inherited class - if local: - if self.class_.__dict__.get(assigned_name, None) is not None \ - and self._is_userland_descriptor( - self.class_.__dict__[assigned_name]): - return True - else: - if getattr(self.class_, assigned_name, None) is not None \ - and self._is_userland_descriptor( - getattr(self.class_, assigned_name)): - return True - - if self.include_properties is not None and \ - name not in self.include_properties and \ - (column is None or column not in self.include_properties): - self._log("not including property %s" % (name)) - return True - - if self.exclude_properties is not None and \ - ( - name in self.exclude_properties or \ - (column is not None and column in self.exclude_properties) - ): - self._log("excluding property %s" % (name)) - return True - - return False - - def common_parent(self, other): - """Return true if the given mapper shares a - common inherited parent as this mapper.""" - - return self.base_mapper is other.base_mapper - - def _canload(self, state, allow_subtypes): - s = self.primary_mapper() - if self.polymorphic_on is not None or allow_subtypes: - return _state_mapper(state).isa(s) - else: - return _state_mapper(state) is s - - def isa(self, other): - """Return True if the this mapper inherits from the given mapper.""" - - m = self - while m and m is not other: - m = m.inherits - return bool(m) - - def iterate_to_root(self): - m = self - while m: - yield m - m = m.inherits - - @_memoized_configured_property - def self_and_descendants(self): - """The collection including this mapper and all descendant mappers. - - This includes not just the immediately inheriting mappers but - all their inheriting mappers as well. - - """ - descendants = [] - stack = deque([self]) - while stack: - item = stack.popleft() - descendants.append(item) - stack.extend(item._inheriting_mappers) - return util.WeakSequence(descendants) - - def polymorphic_iterator(self): - """Iterate through the collection including this mapper and - all descendant mappers. - - This includes not just the immediately inheriting mappers but - all their inheriting mappers as well. - - To iterate through an entire hierarchy, use - ``mapper.base_mapper.polymorphic_iterator()``. - - """ - return iter(self.self_and_descendants) - - def primary_mapper(self): - """Return the primary mapper corresponding to this mapper's class key - (class).""" - - return self.class_manager.mapper - - @property - def primary_base_mapper(self): - return self.class_manager.mapper.base_mapper - - def identity_key_from_row(self, row, adapter=None): - """Return an identity-map key for use in storing/retrieving an - item from the identity map. - - :param row: A :class:`.RowProxy` instance. The columns which are mapped - by this :class:`.Mapper` should be locatable in the row, preferably - via the :class:`.Column` object directly (as is the case when a - :func:`.select` construct is executed), or via string names of the form - ``_``. - - """ - pk_cols = self.primary_key - if adapter: - pk_cols = [adapter.columns[c] for c in pk_cols] - - return self._identity_class, \ - tuple(row[column] for column in pk_cols) - - def identity_key_from_primary_key(self, primary_key): - """Return an identity-map key for use in storing/retrieving an - item from an identity map. - - :param primary_key: A list of values indicating the identifier. - - """ - return self._identity_class, tuple(primary_key) - - def identity_key_from_instance(self, instance): - """Return the identity key for the given instance, based on - its primary key attributes. - - If the instance's state is expired, calling this method - will result in a database check to see if the object has been deleted. - If the row no longer exists, - :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. - - This value is typically also found on the instance state under the - attribute name `key`. - - """ - return self.identity_key_from_primary_key( - self.primary_key_from_instance(instance)) - - def _identity_key_from_state(self, state): - dict_ = state.dict - manager = state.manager - return self._identity_class, tuple([ - manager[self._columntoproperty[col].key].\ - impl.get(state, dict_, attributes.PASSIVE_OFF) - for col in self.primary_key - ]) - - def primary_key_from_instance(self, instance): - """Return the list of primary key values for the given - instance. - - If the instance's state is expired, calling this method - will result in a database check to see if the object has been deleted. - If the row no longer exists, - :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. - - """ - state = attributes.instance_state(instance) - return self._primary_key_from_state(state) - - def _primary_key_from_state(self, state): - dict_ = state.dict - manager = state.manager - return [ - manager[self._columntoproperty[col].key].\ - impl.get(state, dict_, attributes.PASSIVE_OFF) - for col in self.primary_key - ] - - def _get_state_attr_by_column(self, state, dict_, column, - passive=attributes.PASSIVE_OFF): - prop = self._columntoproperty[column] - return state.manager[prop.key].impl.get(state, dict_, passive=passive) - - def _set_state_attr_by_column(self, state, dict_, column, value): - prop = self._columntoproperty[column] - state.manager[prop.key].impl.set(state, dict_, value, None) - - def _get_committed_attr_by_column(self, obj, column): - state = attributes.instance_state(obj) - dict_ = attributes.instance_dict(obj) - return self._get_committed_state_attr_by_column(state, dict_, column) - - def _get_committed_state_attr_by_column(self, state, dict_, - column, passive=attributes.PASSIVE_OFF): - - prop = self._columntoproperty[column] - return state.manager[prop.key].impl.\ - get_committed_value(state, dict_, passive=passive) - - def _optimized_get_statement(self, state, attribute_names): - """assemble a WHERE clause which retrieves a given state by primary - key, using a minimized set of tables. - - Applies to a joined-table inheritance mapper where the - requested attribute names are only present on joined tables, - not the base table. The WHERE clause attempts to include - only those tables to minimize joins. - - """ - props = self._props - - tables = set(chain( - *[sql_util.find_tables(c, check_columns=True) - for key in attribute_names - for c in props[key].columns] - )) - - if self.base_mapper.local_table in tables: - return None - - class ColumnsNotAvailable(Exception): - pass - - def visit_binary(binary): - leftcol = binary.left - rightcol = binary.right - if leftcol is None or rightcol is None: - return - - if leftcol.table not in tables: - leftval = self._get_committed_state_attr_by_column( - state, state.dict, - leftcol, - passive=attributes.PASSIVE_NO_INITIALIZE) - if leftval is attributes.PASSIVE_NO_RESULT or leftval is None: - raise ColumnsNotAvailable() - binary.left = sql.bindparam(None, leftval, - type_=binary.right.type) - elif rightcol.table not in tables: - rightval = self._get_committed_state_attr_by_column( - state, state.dict, - rightcol, - passive=attributes.PASSIVE_NO_INITIALIZE) - if rightval is attributes.PASSIVE_NO_RESULT or \ - rightval is None: - raise ColumnsNotAvailable() - binary.right = sql.bindparam(None, rightval, - type_=binary.right.type) - - allconds = [] - - try: - start = False - for mapper in reversed(list(self.iterate_to_root())): - if mapper.local_table in tables: - start = True - elif not isinstance(mapper.local_table, expression.TableClause): - return None - if start and not mapper.single: - allconds.append(visitors.cloned_traverse( - mapper.inherit_condition, - {}, - {'binary': visit_binary} - ) - ) - except ColumnsNotAvailable: - return None - - cond = sql.and_(*allconds) - - cols = [] - for key in attribute_names: - cols.extend(props[key].columns) - return sql.select(cols, cond, use_labels=True) - - def cascade_iterator(self, type_, state, halt_on=None): - """Iterate each element and its mapper in an object graph, - for all relationships that meet the given cascade rule. - - :param type_: - The name of the cascade rule (i.e. save-update, delete, - etc.) - - :param state: - The lead InstanceState. child items will be processed per - the relationships defined for this object's mapper. - - the return value are object instances; this provides a strong - reference so that they don't fall out of scope immediately. - - """ - visited_states = set() - prp, mpp = object(), object() - - visitables = deque([(deque(self._props.values()), prp, - state, state.dict)]) - - while visitables: - iterator, item_type, parent_state, parent_dict = visitables[-1] - if not iterator: - visitables.pop() - continue - - if item_type is prp: - prop = iterator.popleft() - if type_ not in prop.cascade: - continue - queue = deque(prop.cascade_iterator(type_, parent_state, - parent_dict, visited_states, halt_on)) - if queue: - visitables.append((queue, mpp, None, None)) - elif item_type is mpp: - instance, instance_mapper, corresponding_state, \ - corresponding_dict = iterator.popleft() - yield instance, instance_mapper, \ - corresponding_state, corresponding_dict - visitables.append((deque(instance_mapper._props.values()), - prp, corresponding_state, - corresponding_dict)) - - @_memoized_configured_property - def _compiled_cache(self): - return util.LRUCache(self._compiled_cache_size) - - @_memoized_configured_property - def _sorted_tables(self): - table_to_mapper = {} - - for mapper in self.base_mapper.self_and_descendants: - for t in mapper.tables: - table_to_mapper.setdefault(t, mapper) - - extra_dependencies = [] - for table, mapper in table_to_mapper.items(): - super_ = mapper.inherits - if super_: - extra_dependencies.extend([ - (super_table, table) - for super_table in super_.tables - ]) - - def skip(fk): - # attempt to skip dependencies that are not - # significant to the inheritance chain - # for two tables that are related by inheritance. - # while that dependency may be important, it's techinically - # not what we mean to sort on here. - parent = table_to_mapper.get(fk.parent.table) - dep = table_to_mapper.get(fk.column.table) - if parent is not None and \ - dep is not None and \ - dep is not parent and \ - dep.inherit_condition is not None: - cols = set(sql_util._find_columns(dep.inherit_condition)) - if parent.inherit_condition is not None: - cols = cols.union(sql_util._find_columns( - parent.inherit_condition)) - return fk.parent not in cols and fk.column not in cols - else: - return fk.parent not in cols - return False - - sorted_ = sql_util.sort_tables(table_to_mapper, - skip_fn=skip, - extra_dependencies=extra_dependencies) - - ret = util.OrderedDict() - for t in sorted_: - ret[t] = table_to_mapper[t] - return ret - - def _memo(self, key, callable_): - if key in self._memoized_values: - return self._memoized_values[key] - else: - self._memoized_values[key] = value = callable_() - return value - - @util.memoized_property - def _table_to_equated(self): - """memoized map of tables to collections of columns to be - synchronized upwards to the base mapper.""" - - result = util.defaultdict(list) - - for table in self._sorted_tables: - cols = set(table.c) - for m in self.iterate_to_root(): - if m._inherits_equated_pairs and \ - cols.intersection( - util.reduce(set.union, - [l.proxy_set for l, r in m._inherits_equated_pairs]) - ): - result[table].append((m, m._inherits_equated_pairs)) - - return result - - -def configure_mappers(): - """Initialize the inter-mapper relationships of all mappers that - have been constructed thus far. - - This function can be called any number of times, but in - most cases is handled internally. - - """ - - if not Mapper._new_mappers: - return - - _call_configured = None - _CONFIGURE_MUTEX.acquire() - try: - global _already_compiling - if _already_compiling: - return - _already_compiling = True - try: - - # double-check inside mutex - if not Mapper._new_mappers: - return - - # initialize properties on all mappers - # note that _mapper_registry is unordered, which - # may randomly conceal/reveal issues related to - # the order of mapper compilation - for mapper in list(_mapper_registry): - if getattr(mapper, '_configure_failed', False): - e = sa_exc.InvalidRequestError( - "One or more mappers failed to initialize - " - "can't proceed with initialization of other " - "mappers. Original exception was: %s" - % mapper._configure_failed) - e._configure_failed = mapper._configure_failed - raise e - if not mapper.configured: - try: - mapper._post_configure_properties() - mapper._expire_memoizations() - mapper.dispatch.mapper_configured( - mapper, mapper.class_) - _call_configured = mapper - except: - exc = sys.exc_info()[1] - if not hasattr(exc, '_configure_failed'): - mapper._configure_failed = exc - raise - - Mapper._new_mappers = False - finally: - _already_compiling = False - finally: - _CONFIGURE_MUTEX.release() - if _call_configured is not None: - _call_configured.dispatch.after_configured() - - -def reconstructor(fn): - """Decorate a method as the 'reconstructor' hook. - - Designates a method as the "reconstructor", an ``__init__``-like - method that will be called by the ORM after the instance has been - loaded from the database or otherwise reconstituted. - - The reconstructor will be invoked with no arguments. Scalar - (non-collection) database-mapped attributes of the instance will - be available for use within the function. Eagerly-loaded - collections are generally not yet available and will usually only - contain the first element. ORM state changes made to objects at - this stage will not be recorded for the next flush() operation, so - the activity within a reconstructor should be conservative. - - """ - fn.__sa_reconstructor__ = True - return fn - - -def validates(*names, **kw): - """Decorate a method as a 'validator' for one or more named properties. - - Designates a method as a validator, a method which receives the - name of the attribute as well as a value to be assigned, or in the - case of a collection, the value to be added to the collection. - The function can then raise validation exceptions to halt the - process from continuing (where Python's built-in ``ValueError`` - and ``AssertionError`` exceptions are reasonable choices), or can - modify or replace the value before proceeding. The function should - otherwise return the given value. - - Note that a validator for a collection **cannot** issue a load of that - collection within the validation routine - this usage raises - an assertion to avoid recursion overflows. This is a reentrant - condition which is not supported. - - :param \*names: list of attribute names to be validated. - :param include_removes: if True, "remove" events will be - sent as well - the validation function must accept an additional - argument "is_remove" which will be a boolean. - - .. versionadded:: 0.7.7 - :param include_backrefs: defaults to ``True``; if ``False``, the - validation function will not emit if the originator is an attribute - event related via a backref. This can be used for bi-directional - :func:`.validates` usage where only one validator should emit per - attribute operation. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :ref:`simple_validators` - usage examples for :func:`.validates` - - """ - include_removes = kw.pop('include_removes', False) - include_backrefs = kw.pop('include_backrefs', True) - - def wrap(fn): - fn.__sa_validators__ = names - fn.__sa_validation_opts__ = { - "include_removes": include_removes, - "include_backrefs": include_backrefs - } - return fn - return wrap - - -def _event_on_load(state, ctx): - instrumenting_mapper = state.manager.info[_INSTRUMENTOR] - if instrumenting_mapper._reconstructor: - instrumenting_mapper._reconstructor(state.obj()) - - -def _event_on_first_init(manager, cls): - """Initial mapper compilation trigger. - - instrumentation calls this one when InstanceState - is first generated, and is needed for legacy mutable - attributes to work. - """ - - instrumenting_mapper = manager.info.get(_INSTRUMENTOR) - if instrumenting_mapper: - if Mapper._new_mappers: - configure_mappers() - - -def _event_on_init(state, args, kwargs): - """Run init_instance hooks. - - This also includes mapper compilation, normally not needed - here but helps with some piecemeal configuration - scenarios (such as in the ORM tutorial). - - """ - - instrumenting_mapper = state.manager.info.get(_INSTRUMENTOR) - if instrumenting_mapper: - if Mapper._new_mappers: - configure_mappers() - if instrumenting_mapper._set_polymorphic_identity: - instrumenting_mapper._set_polymorphic_identity(state) - - -def _event_on_resurrect(state): - # re-populate the primary key elements - # of the dict based on the mapping. - instrumenting_mapper = state.manager.info.get(_INSTRUMENTOR) - if instrumenting_mapper: - for col, val in zip(instrumenting_mapper.primary_key, state.key[1]): - instrumenting_mapper._set_state_attr_by_column( - state, state.dict, col, val) - - -class _ColumnMapping(dict): - """Error reporting helper for mapper._columntoproperty.""" - - def __init__(self, mapper): - self.mapper = mapper - - def __missing__(self, column): - prop = self.mapper._props.get(column) - if prop: - raise orm_exc.UnmappedColumnError( - "Column '%s.%s' is not available, due to " - "conflicting property '%s':%r" % ( - column.table.name, column.name, column.key, prop)) - raise orm_exc.UnmappedColumnError( - "No column %s is configured on mapper %s..." % - (column, self.mapper)) diff --git a/libs/sqlalchemy/orm/path_registry.py b/libs/sqlalchemy/orm/path_registry.py deleted file mode 100644 index 3397626b..00000000 --- a/libs/sqlalchemy/orm/path_registry.py +++ /dev/null @@ -1,261 +0,0 @@ -# orm/path_registry.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Path tracking utilities, representing mapper graph traversals. - -""" - -from .. import inspection -from .. import util -from .. import exc -from itertools import chain -from .base import class_mapper - -def _unreduce_path(path): - return PathRegistry.deserialize(path) - - -_WILDCARD_TOKEN = "*" -_DEFAULT_TOKEN = "_sa_default" - -class PathRegistry(object): - """Represent query load paths and registry functions. - - Basically represents structures like: - - (, "orders", , "items", ) - - These structures are generated by things like - query options (joinedload(), subqueryload(), etc.) and are - used to compose keys stored in the query._attributes dictionary - for various options. - - They are then re-composed at query compile/result row time as - the query is formed and as rows are fetched, where they again - serve to compose keys to look up options in the context.attributes - dictionary, which is copied from query._attributes. - - The path structure has a limited amount of caching, where each - "root" ultimately pulls from a fixed registry associated with - the first mapper, that also contains elements for each of its - property keys. However paths longer than two elements, which - are the exception rather than the rule, are generated on an - as-needed basis. - - """ - - def __eq__(self, other): - return other is not None and \ - self.path == other.path - - def set(self, attributes, key, value): - attributes[(key, self.path)] = value - - def setdefault(self, attributes, key, value): - attributes.setdefault((key, self.path), value) - - def get(self, attributes, key, value=None): - key = (key, self.path) - if key in attributes: - return attributes[key] - else: - return value - - def __len__(self): - return len(self.path) - - @property - def length(self): - return len(self.path) - - def pairs(self): - path = self.path - for i in range(0, len(path), 2): - yield path[i], path[i + 1] - - def contains_mapper(self, mapper): - for path_mapper in [ - self.path[i] for i in range(0, len(self.path), 2) - ]: - if path_mapper.is_mapper and \ - path_mapper.isa(mapper): - return True - else: - return False - - def contains(self, attributes, key): - return (key, self.path) in attributes - - def __reduce__(self): - return _unreduce_path, (self.serialize(), ) - - def serialize(self): - path = self.path - return list(zip( - [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], - [path[i].key for i in range(1, len(path), 2)] + [None] - )) - - @classmethod - def deserialize(cls, path): - if path is None: - return None - - p = tuple(chain(*[(class_mapper(mcls), - class_mapper(mcls).attrs[key] - if key is not None else None) - for mcls, key in path])) - if p and p[-1] is None: - p = p[0:-1] - return cls.coerce(p) - - @classmethod - def per_mapper(cls, mapper): - return EntityRegistry( - cls.root, mapper - ) - - @classmethod - def coerce(cls, raw): - return util.reduce(lambda prev, next: prev[next], raw, cls.root) - - def token(self, token): - if token.endswith(':' + _WILDCARD_TOKEN): - return TokenRegistry(self, token) - elif token.endswith(":" + _DEFAULT_TOKEN): - return TokenRegistry(self.root, token) - else: - raise exc.ArgumentError("invalid token: %s" % token) - - def __add__(self, other): - return util.reduce( - lambda prev, next: prev[next], - other.path, self) - - def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, self.path, ) - - -class RootRegistry(PathRegistry): - """Root registry, defers to mappers so that - paths are maintained per-root-mapper. - - """ - path = () - has_entity = False - def __getitem__(self, entity): - return entity._path_registry - -PathRegistry.root = RootRegistry() - -class TokenRegistry(PathRegistry): - def __init__(self, parent, token): - self.token = token - self.parent = parent - self.path = parent.path + (token,) - - has_entity = False - - def __getitem__(self, entity): - raise NotImplementedError() - -class PropRegistry(PathRegistry): - def __init__(self, parent, prop): - # restate this path in terms of the - # given MapperProperty's parent. - insp = inspection.inspect(parent[-1]) - if not insp.is_aliased_class or insp._use_mapper_path: - parent = parent.parent[prop.parent] - elif insp.is_aliased_class and insp.with_polymorphic_mappers: - if prop.parent is not insp.mapper and \ - prop.parent in insp.with_polymorphic_mappers: - subclass_entity = parent[-1]._entity_for_mapper(prop.parent) - parent = parent.parent[subclass_entity] - - self.prop = prop - self.parent = parent - self.path = parent.path + (prop,) - - @util.memoized_property - def has_entity(self): - return hasattr(self.prop, "mapper") - - @util.memoized_property - def entity(self): - return self.prop.mapper - - @util.memoized_property - def _wildcard_path_loader_key(self): - """Given a path (mapper A, prop X), replace the prop with the wildcard, - e.g. (mapper A, 'relationship:.*') or (mapper A, 'column:.*'), then - return within the ("loader", path) structure. - - """ - return ("loader", - self.parent.token( - "%s:%s" % (self.prop.strategy_wildcard_key, _WILDCARD_TOKEN) - ).path - ) - - @util.memoized_property - def _default_path_loader_key(self): - return ("loader", - self.parent.token( - "%s:%s" % (self.prop.strategy_wildcard_key, _DEFAULT_TOKEN) - ).path - ) - - @util.memoized_property - def _loader_key(self): - return ("loader", self.path) - - @property - def mapper(self): - return self.entity - - @property - def entity_path(self): - return self[self.entity] - - def __getitem__(self, entity): - if isinstance(entity, (int, slice)): - return self.path[entity] - else: - return EntityRegistry( - self, entity - ) - -class EntityRegistry(PathRegistry, dict): - is_aliased_class = False - has_entity = True - - def __init__(self, parent, entity): - self.key = entity - self.parent = parent - self.is_aliased_class = entity.is_aliased_class - self.entity = entity - self.path = parent.path + (entity,) - self.entity_path = self - - @property - def mapper(self): - return inspection.inspect(self.entity).mapper - - def __bool__(self): - return True - __nonzero__ = __bool__ - - def __getitem__(self, entity): - if isinstance(entity, (int, slice)): - return self.path[entity] - else: - return dict.__getitem__(self, entity) - - def __missing__(self, key): - self[key] = item = PropRegistry(self, key) - return item - - - diff --git a/libs/sqlalchemy/orm/persistence.py b/libs/sqlalchemy/orm/persistence.py deleted file mode 100644 index 35631988..00000000 --- a/libs/sqlalchemy/orm/persistence.py +++ /dev/null @@ -1,1085 +0,0 @@ -# orm/persistence.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""private module containing functions used to emit INSERT, UPDATE -and DELETE statements on behalf of a :class:`.Mapper` and its descending -mappers. - -The functions here are called only by the unit of work functions -in unitofwork.py. - -""" - -import operator -from itertools import groupby -from .. import sql, util, exc as sa_exc, schema -from . import attributes, sync, exc as orm_exc, evaluator -from .base import _state_mapper, state_str, _attr_as_key -from ..sql import expression -from . import loading - - -def save_obj(base_mapper, states, uowtransaction, single=False): - """Issue ``INSERT`` and/or ``UPDATE`` statements for a list - of objects. - - This is called within the context of a UOWTransaction during a - flush operation, given a list of states to be flushed. The - base mapper in an inheritance hierarchy handles the inserts/ - updates for all descendant mappers. - - """ - - # if batch=false, call _save_obj separately for each object - if not single and not base_mapper.batch: - for state in _sort_states(states): - save_obj(base_mapper, [state], uowtransaction, single=True) - return - - states_to_insert, states_to_update = _organize_states_for_save( - base_mapper, - states, - uowtransaction) - - cached_connections = _cached_connection_dict(base_mapper) - - for table, mapper in base_mapper._sorted_tables.items(): - insert = _collect_insert_commands(base_mapper, uowtransaction, - table, states_to_insert) - - update = _collect_update_commands(base_mapper, uowtransaction, - table, states_to_update) - - if update: - _emit_update_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, update) - - if insert: - _emit_insert_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, insert) - - _finalize_insert_update_commands(base_mapper, uowtransaction, - states_to_insert, states_to_update) - - -def post_update(base_mapper, states, uowtransaction, post_update_cols): - """Issue UPDATE statements on behalf of a relationship() which - specifies post_update. - - """ - cached_connections = _cached_connection_dict(base_mapper) - - states_to_update = _organize_states_for_post_update( - base_mapper, - states, uowtransaction) - - for table, mapper in base_mapper._sorted_tables.items(): - update = _collect_post_update_commands(base_mapper, uowtransaction, - table, states_to_update, - post_update_cols) - - if update: - _emit_post_update_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, update) - - -def delete_obj(base_mapper, states, uowtransaction): - """Issue ``DELETE`` statements for a list of objects. - - This is called within the context of a UOWTransaction during a - flush operation. - - """ - - cached_connections = _cached_connection_dict(base_mapper) - - states_to_delete = _organize_states_for_delete( - base_mapper, - states, - uowtransaction) - - table_to_mapper = base_mapper._sorted_tables - - for table in reversed(list(table_to_mapper.keys())): - delete = _collect_delete_commands(base_mapper, uowtransaction, - table, states_to_delete) - - mapper = table_to_mapper[table] - - _emit_delete_statements(base_mapper, uowtransaction, - cached_connections, mapper, table, delete) - - for state, state_dict, mapper, has_identity, connection \ - in states_to_delete: - mapper.dispatch.after_delete(mapper, connection, state) - - -def _organize_states_for_save(base_mapper, states, uowtransaction): - """Make an initial pass across a set of states for INSERT or - UPDATE. - - This includes splitting out into distinct lists for - each, calling before_insert/before_update, obtaining - key information for each state including its dictionary, - mapper, the connection to use for the execution per state, - and the identity flag. - - """ - - states_to_insert = [] - states_to_update = [] - - for state, dict_, mapper, connection in _connections_for_states( - base_mapper, uowtransaction, - states): - - has_identity = bool(state.key) - instance_key = state.key or mapper._identity_key_from_state(state) - - row_switch = None - - # call before_XXX extensions - if not has_identity: - mapper.dispatch.before_insert(mapper, connection, state) - else: - mapper.dispatch.before_update(mapper, connection, state) - - if mapper._validate_polymorphic_identity: - mapper._validate_polymorphic_identity(mapper, state, dict_) - - # detect if we have a "pending" instance (i.e. has - # no instance_key attached to it), and another instance - # with the same identity key already exists as persistent. - # convert to an UPDATE if so. - if not has_identity and \ - instance_key in uowtransaction.session.identity_map: - instance = \ - uowtransaction.session.identity_map[instance_key] - existing = attributes.instance_state(instance) - if not uowtransaction.is_deleted(existing): - raise orm_exc.FlushError( - "New instance %s with identity key %s conflicts " - "with persistent instance %s" % - (state_str(state), instance_key, - state_str(existing))) - - base_mapper._log_debug( - "detected row switch for identity %s. " - "will update %s, remove %s from " - "transaction", instance_key, - state_str(state), state_str(existing)) - - # remove the "delete" flag from the existing element - uowtransaction.remove_state_actions(existing) - row_switch = existing - - if not has_identity and not row_switch: - states_to_insert.append( - (state, dict_, mapper, connection, - has_identity, instance_key, row_switch) - ) - else: - states_to_update.append( - (state, dict_, mapper, connection, - has_identity, instance_key, row_switch) - ) - - return states_to_insert, states_to_update - - -def _organize_states_for_post_update(base_mapper, states, - uowtransaction): - """Make an initial pass across a set of states for UPDATE - corresponding to post_update. - - This includes obtaining key information for each state - including its dictionary, mapper, the connection to use for - the execution per state. - - """ - return list(_connections_for_states(base_mapper, uowtransaction, - states)) - - -def _organize_states_for_delete(base_mapper, states, uowtransaction): - """Make an initial pass across a set of states for DELETE. - - This includes calling out before_delete and obtaining - key information for each state including its dictionary, - mapper, the connection to use for the execution per state. - - """ - states_to_delete = [] - - for state, dict_, mapper, connection in _connections_for_states( - base_mapper, uowtransaction, - states): - - mapper.dispatch.before_delete(mapper, connection, state) - - states_to_delete.append((state, dict_, mapper, - bool(state.key), connection)) - return states_to_delete - - -def _collect_insert_commands(base_mapper, uowtransaction, table, - states_to_insert): - """Identify sets of values to use in INSERT statements for a - list of states. - - """ - insert = [] - for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_insert: - if table not in mapper._pks_by_table: - continue - - pks = mapper._pks_by_table[table] - - params = {} - value_params = {} - - has_all_pks = True - has_all_defaults = True - for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col and \ - mapper.version_id_generator is not False: - val = mapper.version_id_generator(None) - params[col.key] = val - else: - # pull straight from the dict for - # pending objects - prop = mapper._columntoproperty[col] - value = state_dict.get(prop.key, None) - - if value is None: - if col in pks: - has_all_pks = False - elif col.default is None and \ - col.server_default is None: - params[col.key] = value - elif col.server_default is not None and \ - mapper.base_mapper.eager_defaults: - has_all_defaults = False - - elif isinstance(value, sql.ClauseElement): - value_params[col] = value - else: - params[col.key] = value - - insert.append((state, state_dict, params, mapper, - connection, value_params, has_all_pks, - has_all_defaults)) - return insert - - -def _collect_update_commands(base_mapper, uowtransaction, - table, states_to_update): - """Identify sets of values to use in UPDATE statements for a - list of states. - - This function works intricately with the history system - to determine exactly what values should be updated - as well as how the row should be matched within an UPDATE - statement. Includes some tricky scenarios where the primary - key of an object might have been changed. - - """ - - update = [] - for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_update: - if table not in mapper._pks_by_table: - continue - - pks = mapper._pks_by_table[table] - - params = {} - value_params = {} - - hasdata = hasnull = False - for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col: - params[col._label] = \ - mapper._get_committed_state_attr_by_column( - row_switch or state, - row_switch and row_switch.dict - or state_dict, - col) - - prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE - ) - if history.added: - params[col.key] = history.added[0] - hasdata = True - else: - if mapper.version_id_generator is not False: - val = mapper.version_id_generator(params[col._label]) - params[col.key] = val - - # HACK: check for history, in case the - # history is only - # in a different table than the one - # where the version_id_col is. - for prop in mapper._columntoproperty.values(): - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE) - if history.added: - hasdata = True - else: - prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE) - if history.added: - if isinstance(history.added[0], - sql.ClauseElement): - value_params[col] = history.added[0] - else: - value = history.added[0] - params[col.key] = value - - if col in pks: - if history.deleted and \ - not row_switch: - # if passive_updates and sync detected - # this was a pk->pk sync, use the new - # value to locate the row, since the - # DB would already have set this - if ("pk_cascaded", state, col) in \ - uowtransaction.attributes: - value = history.added[0] - params[col._label] = value - else: - # use the old value to - # locate the row - value = history.deleted[0] - params[col._label] = value - hasdata = True - else: - # row switch logic can reach us here - # remove the pk from the update params - # so the update doesn't - # attempt to include the pk in the - # update statement - del params[col.key] - value = history.added[0] - params[col._label] = value - if value is None: - hasnull = True - else: - hasdata = True - elif col in pks: - value = state.manager[prop.key].impl.get( - state, state_dict) - if value is None: - hasnull = True - params[col._label] = value - if hasdata: - if hasnull: - raise orm_exc.FlushError( - "Can't update table " - "using NULL for primary " - "key value") - update.append((state, state_dict, params, mapper, - connection, value_params)) - return update - - -def _collect_post_update_commands(base_mapper, uowtransaction, table, - states_to_update, post_update_cols): - """Identify sets of values to use in UPDATE statements for a - list of states within a post_update operation. - - """ - - update = [] - for state, state_dict, mapper, connection in states_to_update: - if table not in mapper._pks_by_table: - continue - pks = mapper._pks_by_table[table] - params = {} - hasdata = False - - for col in mapper._cols_by_table[table]: - if col in pks: - params[col._label] = \ - mapper._get_state_attr_by_column( - state, - state_dict, col) - - elif col in post_update_cols: - prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE) - if history.added: - value = history.added[0] - params[col.key] = value - hasdata = True - if hasdata: - update.append((state, state_dict, params, mapper, - connection)) - return update - - -def _collect_delete_commands(base_mapper, uowtransaction, table, - states_to_delete): - """Identify values to use in DELETE statements for a list of - states to be deleted.""" - - delete = util.defaultdict(list) - - for state, state_dict, mapper, has_identity, connection \ - in states_to_delete: - if not has_identity or table not in mapper._pks_by_table: - continue - - params = {} - delete[connection].append(params) - for col in mapper._pks_by_table[table]: - params[col.key] = \ - value = \ - mapper._get_state_attr_by_column( - state, state_dict, col) - if value is None: - raise orm_exc.FlushError( - "Can't delete from table " - "using NULL for primary " - "key value") - - if mapper.version_id_col is not None and \ - table.c.contains_column(mapper.version_id_col): - params[mapper.version_id_col.key] = \ - mapper._get_committed_state_attr_by_column( - state, state_dict, - mapper.version_id_col) - return delete - - -def _emit_update_statements(base_mapper, uowtransaction, - cached_connections, mapper, table, update): - """Emit UPDATE statements corresponding to value lists collected - by _collect_update_commands().""" - - needs_version_id = mapper.version_id_col is not None and \ - table.c.contains_column(mapper.version_id_col) - - def update_stmt(): - clause = sql.and_() - - for col in mapper._pks_by_table[table]: - clause.clauses.append(col == sql.bindparam(col._label, - type_=col.type)) - - if needs_version_id: - clause.clauses.append(mapper.version_id_col ==\ - sql.bindparam(mapper.version_id_col._label, - type_=mapper.version_id_col.type)) - - stmt = table.update(clause) - if mapper.base_mapper.eager_defaults: - stmt = stmt.return_defaults() - elif mapper.version_id_col is not None: - stmt = stmt.return_defaults(mapper.version_id_col) - - return stmt - - statement = base_mapper._memo(('update', table), update_stmt) - - rows = 0 - for state, state_dict, params, mapper, \ - connection, value_params in update: - - if value_params: - c = connection.execute( - statement.values(value_params), - params) - else: - c = cached_connections[connection].\ - execute(statement, params) - - _postfetch( - mapper, - uowtransaction, - table, - state, - state_dict, - c, - c.context.compiled_parameters[0], - value_params) - rows += c.rowcount - - if connection.dialect.supports_sane_rowcount: - if rows != len(update): - raise orm_exc.StaleDataError( - "UPDATE statement on table '%s' expected to " - "update %d row(s); %d were matched." % - (table.description, len(update), rows)) - - elif needs_version_id: - util.warn("Dialect %s does not support updated rowcount " - "- versioning cannot be verified." % - c.dialect.dialect_description, - stacklevel=12) - - -def _emit_insert_statements(base_mapper, uowtransaction, - cached_connections, mapper, table, insert): - """Emit INSERT statements corresponding to value lists collected - by _collect_insert_commands().""" - - statement = base_mapper._memo(('insert', table), table.insert) - - for (connection, pkeys, hasvalue, has_all_pks, has_all_defaults), \ - records in groupby(insert, - lambda rec: (rec[4], - list(rec[2].keys()), - bool(rec[5]), - rec[6], rec[7]) - ): - if \ - ( - has_all_defaults - or not base_mapper.eager_defaults - or not connection.dialect.implicit_returning - ) and has_all_pks and not hasvalue: - - records = list(records) - multiparams = [rec[2] for rec in records] - - c = cached_connections[connection].\ - execute(statement, multiparams) - - for (state, state_dict, params, mapper_rec, - conn, value_params, has_all_pks, has_all_defaults), \ - last_inserted_params in \ - zip(records, c.context.compiled_parameters): - _postfetch( - mapper_rec, - uowtransaction, - table, - state, - state_dict, - c, - last_inserted_params, - value_params) - - else: - if not has_all_defaults and base_mapper.eager_defaults: - statement = statement.return_defaults() - elif mapper.version_id_col is not None: - statement = statement.return_defaults(mapper.version_id_col) - - for state, state_dict, params, mapper_rec, \ - connection, value_params, \ - has_all_pks, has_all_defaults in records: - - if value_params: - result = connection.execute( - statement.values(value_params), - params) - else: - result = cached_connections[connection].\ - execute(statement, params) - - primary_key = result.context.inserted_primary_key - - if primary_key is not None: - # set primary key attributes - for pk, col in zip(primary_key, - mapper._pks_by_table[table]): - prop = mapper_rec._columntoproperty[col] - if state_dict.get(prop.key) is None: - # TODO: would rather say: - #state_dict[prop.key] = pk - mapper_rec._set_state_attr_by_column( - state, - state_dict, - col, pk) - - _postfetch( - mapper_rec, - uowtransaction, - table, - state, - state_dict, - result, - result.context.compiled_parameters[0], - value_params) - - -def _emit_post_update_statements(base_mapper, uowtransaction, - cached_connections, mapper, table, update): - """Emit UPDATE statements corresponding to value lists collected - by _collect_post_update_commands().""" - - def update_stmt(): - clause = sql.and_() - - for col in mapper._pks_by_table[table]: - clause.clauses.append(col == sql.bindparam(col._label, - type_=col.type)) - - return table.update(clause) - - statement = base_mapper._memo(('post_update', table), update_stmt) - - # execute each UPDATE in the order according to the original - # list of states to guarantee row access order, but - # also group them into common (connection, cols) sets - # to support executemany(). - for key, grouper in groupby( - update, lambda rec: (rec[4], list(rec[2].keys())) - ): - connection = key[0] - multiparams = [params for state, state_dict, - params, mapper, conn in grouper] - cached_connections[connection].\ - execute(statement, multiparams) - - -def _emit_delete_statements(base_mapper, uowtransaction, cached_connections, - mapper, table, delete): - """Emit DELETE statements corresponding to value lists collected - by _collect_delete_commands().""" - - need_version_id = mapper.version_id_col is not None and \ - table.c.contains_column(mapper.version_id_col) - - def delete_stmt(): - clause = sql.and_() - for col in mapper._pks_by_table[table]: - clause.clauses.append( - col == sql.bindparam(col.key, type_=col.type)) - - if need_version_id: - clause.clauses.append( - mapper.version_id_col == - sql.bindparam( - mapper.version_id_col.key, - type_=mapper.version_id_col.type - ) - ) - - return table.delete(clause) - - for connection, del_objects in delete.items(): - statement = base_mapper._memo(('delete', table), delete_stmt) - - connection = cached_connections[connection] - - if need_version_id: - # TODO: need test coverage for this [ticket:1761] - if connection.dialect.supports_sane_rowcount: - rows = 0 - # execute deletes individually so that versioned - # rows can be verified - for params in del_objects: - c = connection.execute(statement, params) - rows += c.rowcount - if rows != len(del_objects): - raise orm_exc.StaleDataError( - "DELETE statement on table '%s' expected to " - "delete %d row(s); %d were matched." % - (table.description, len(del_objects), c.rowcount) - ) - else: - util.warn( - "Dialect %s does not support deleted rowcount " - "- versioning cannot be verified." % - connection.dialect.dialect_description, - stacklevel=12) - connection.execute(statement, del_objects) - else: - connection.execute(statement, del_objects) - - -def _finalize_insert_update_commands(base_mapper, uowtransaction, - states_to_insert, states_to_update): - """finalize state on states that have been inserted or updated, - including calling after_insert/after_update events. - - """ - for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_insert + \ - states_to_update: - - if mapper._readonly_props: - readonly = state.unmodified_intersection( - [p.key for p in mapper._readonly_props - if p.expire_on_flush or p.key not in state.dict] - ) - if readonly: - state._expire_attributes(state.dict, readonly) - - # if eager_defaults option is enabled, load - # all expired cols. Else if we have a version_id_col, make sure - # it isn't expired. - toload_now = [] - - if base_mapper.eager_defaults: - toload_now.extend(state._unloaded_non_object) - elif mapper.version_id_col is not None and \ - mapper.version_id_generator is False: - prop = mapper._columntoproperty[mapper.version_id_col] - if prop.key in state.unloaded: - toload_now.extend([prop.key]) - - if toload_now: - state.key = base_mapper._identity_key_from_state(state) - loading.load_on_ident( - uowtransaction.session.query(base_mapper), - state.key, refresh_state=state, - only_load_props=toload_now) - - # call after_XXX extensions - if not has_identity: - mapper.dispatch.after_insert(mapper, connection, state) - else: - mapper.dispatch.after_update(mapper, connection, state) - - -def _postfetch(mapper, uowtransaction, table, - state, dict_, result, params, value_params): - """Expire attributes in need of newly persisted database state, - after an INSERT or UPDATE statement has proceeded for that - state.""" - - prefetch_cols = result.context.prefetch_cols - postfetch_cols = result.context.postfetch_cols - returning_cols = result.context.returning_cols - - if mapper.version_id_col is not None: - prefetch_cols = list(prefetch_cols) + [mapper.version_id_col] - - if returning_cols: - row = result.context.returned_defaults - if row is not None: - for col in returning_cols: - if col.primary_key: - continue - mapper._set_state_attr_by_column(state, dict_, col, row[col]) - - for c in prefetch_cols: - if c.key in params and c in mapper._columntoproperty: - mapper._set_state_attr_by_column(state, dict_, c, params[c.key]) - - if postfetch_cols: - state._expire_attributes(state.dict, - [mapper._columntoproperty[c].key - for c in postfetch_cols if c in - mapper._columntoproperty] - ) - - # synchronize newly inserted ids from one table to the next - # TODO: this still goes a little too often. would be nice to - # have definitive list of "columns that changed" here - for m, equated_pairs in mapper._table_to_equated[table]: - sync.populate(state, m, state, m, - equated_pairs, - uowtransaction, - mapper.passive_updates) - - -def _connections_for_states(base_mapper, uowtransaction, states): - """Return an iterator of (state, state.dict, mapper, connection). - - The states are sorted according to _sort_states, then paired - with the connection they should be using for the given - unit of work transaction. - - """ - # if session has a connection callable, - # organize individual states with the connection - # to use for update - if uowtransaction.session.connection_callable: - connection_callable = \ - uowtransaction.session.connection_callable - else: - connection = None - connection_callable = None - - for state in _sort_states(states): - if connection_callable: - connection = connection_callable(base_mapper, state.obj()) - elif not connection: - connection = uowtransaction.transaction.connection( - base_mapper) - - mapper = _state_mapper(state) - - yield state, state.dict, mapper, connection - - -def _cached_connection_dict(base_mapper): - # dictionary of connection->connection_with_cache_options. - return util.PopulateDict( - lambda conn: conn.execution_options( - compiled_cache=base_mapper._compiled_cache - )) - - -def _sort_states(states): - pending = set(states) - persistent = set(s for s in pending if s.key is not None) - pending.difference_update(persistent) - return sorted(pending, key=operator.attrgetter("insert_order")) + \ - sorted(persistent, key=lambda q: q.key[1]) - - -class BulkUD(object): - """Handle bulk update and deletes via a :class:`.Query`.""" - - def __init__(self, query): - self.query = query.enable_eagerloads(False) - - @property - def session(self): - return self.query.session - - @classmethod - def _factory(cls, lookup, synchronize_session, *arg): - try: - klass = lookup[synchronize_session] - except KeyError: - raise sa_exc.ArgumentError( - "Valid strategies for session synchronization " - "are %s" % (", ".join(sorted(repr(x) - for x in lookup)))) - else: - return klass(*arg) - - def exec_(self): - self._do_pre() - self._do_pre_synchronize() - self._do_exec() - self._do_post_synchronize() - self._do_post() - - def _do_pre(self): - query = self.query - self.context = context = query._compile_context() - if len(context.statement.froms) != 1 or \ - not isinstance(context.statement.froms[0], schema.Table): - - self.primary_table = query._only_entity_zero( - "This operation requires only one Table or " - "entity be specified as the target." - ).mapper.local_table - else: - self.primary_table = context.statement.froms[0] - - session = query.session - - if query._autoflush: - session._autoflush() - - def _do_pre_synchronize(self): - pass - - def _do_post_synchronize(self): - pass - - -class BulkEvaluate(BulkUD): - """BulkUD which does the 'evaluate' method of session state resolution.""" - - def _additional_evaluators(self, evaluator_compiler): - pass - - def _do_pre_synchronize(self): - query = self.query - try: - evaluator_compiler = evaluator.EvaluatorCompiler() - if query.whereclause is not None: - eval_condition = evaluator_compiler.process( - query.whereclause) - else: - def eval_condition(obj): - return True - - self._additional_evaluators(evaluator_compiler) - - except evaluator.UnevaluatableError: - raise sa_exc.InvalidRequestError( - "Could not evaluate current criteria in Python. " - "Specify 'fetch' or False for the " - "synchronize_session parameter.") - target_cls = query._mapper_zero().class_ - - #TODO: detect when the where clause is a trivial primary key match - self.matched_objects = [ - obj for (cls, pk), obj in - query.session.identity_map.items() - if issubclass(cls, target_cls) and - eval_condition(obj)] - - -class BulkFetch(BulkUD): - """BulkUD which does the 'fetch' method of session state resolution.""" - - def _do_pre_synchronize(self): - query = self.query - session = query.session - select_stmt = self.context.statement.with_only_columns( - self.primary_table.primary_key) - self.matched_rows = session.execute( - select_stmt, - params=query._params).fetchall() - - -class BulkUpdate(BulkUD): - """BulkUD which handles UPDATEs.""" - - def __init__(self, query, values): - super(BulkUpdate, self).__init__(query) - self.query._no_select_modifiers("update") - self.values = values - - @classmethod - def factory(cls, query, synchronize_session, values): - return BulkUD._factory({ - "evaluate": BulkUpdateEvaluate, - "fetch": BulkUpdateFetch, - False: BulkUpdate - }, synchronize_session, query, values) - - def _do_exec(self): - update_stmt = sql.update(self.primary_table, - self.context.whereclause, self.values) - - self.result = self.query.session.execute( - update_stmt, params=self.query._params) - self.rowcount = self.result.rowcount - - def _do_post(self): - session = self.query.session - session.dispatch.after_bulk_update(self) - - -class BulkDelete(BulkUD): - """BulkUD which handles DELETEs.""" - - def __init__(self, query): - super(BulkDelete, self).__init__(query) - self.query._no_select_modifiers("delete") - - @classmethod - def factory(cls, query, synchronize_session): - return BulkUD._factory({ - "evaluate": BulkDeleteEvaluate, - "fetch": BulkDeleteFetch, - False: BulkDelete - }, synchronize_session, query) - - def _do_exec(self): - delete_stmt = sql.delete(self.primary_table, - self.context.whereclause) - - self.result = self.query.session.execute(delete_stmt, - params=self.query._params) - self.rowcount = self.result.rowcount - - def _do_post(self): - session = self.query.session - session.dispatch.after_bulk_delete(self) - - -class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate): - """BulkUD which handles UPDATEs using the "evaluate" - method of session resolution.""" - - def _additional_evaluators(self, evaluator_compiler): - self.value_evaluators = {} - for key, value in self.values.items(): - key = _attr_as_key(key) - self.value_evaluators[key] = evaluator_compiler.process( - expression._literal_as_binds(value)) - - def _do_post_synchronize(self): - session = self.query.session - states = set() - evaluated_keys = list(self.value_evaluators.keys()) - for obj in self.matched_objects: - state, dict_ = attributes.instance_state(obj),\ - attributes.instance_dict(obj) - - # only evaluate unmodified attributes - to_evaluate = state.unmodified.intersection( - evaluated_keys) - for key in to_evaluate: - dict_[key] = self.value_evaluators[key](obj) - - state._commit(dict_, list(to_evaluate)) - - # expire attributes with pending changes - # (there was no autoflush, so they are overwritten) - state._expire_attributes(dict_, - set(evaluated_keys). - difference(to_evaluate)) - states.add(state) - session._register_altered(states) - - -class BulkDeleteEvaluate(BulkEvaluate, BulkDelete): - """BulkUD which handles DELETEs using the "evaluate" - method of session resolution.""" - - def _do_post_synchronize(self): - self.query.session._remove_newly_deleted( - [attributes.instance_state(obj) - for obj in self.matched_objects]) - - -class BulkUpdateFetch(BulkFetch, BulkUpdate): - """BulkUD which handles UPDATEs using the "fetch" - method of session resolution.""" - - def _do_post_synchronize(self): - session = self.query.session - target_mapper = self.query._mapper_zero() - - states = set([ - attributes.instance_state(session.identity_map[identity_key]) - for identity_key in [ - target_mapper.identity_key_from_primary_key( - list(primary_key)) - for primary_key in self.matched_rows - ] - if identity_key in session.identity_map - ]) - attrib = [_attr_as_key(k) for k in self.values] - for state in states: - session._expire_state(state, attrib) - session._register_altered(states) - - -class BulkDeleteFetch(BulkFetch, BulkDelete): - """BulkUD which handles DELETEs using the "fetch" - method of session resolution.""" - - def _do_post_synchronize(self): - session = self.query.session - target_mapper = self.query._mapper_zero() - for primary_key in self.matched_rows: - # TODO: inline this and call remove_newly_deleted - # once - identity_key = target_mapper.identity_key_from_primary_key( - list(primary_key)) - if identity_key in session.identity_map: - session._remove_newly_deleted( - [attributes.instance_state( - session.identity_map[identity_key] - )] - ) diff --git a/libs/sqlalchemy/orm/properties.py b/libs/sqlalchemy/orm/properties.py deleted file mode 100644 index a0def7d3..00000000 --- a/libs/sqlalchemy/orm/properties.py +++ /dev/null @@ -1,259 +0,0 @@ -# orm/properties.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""MapperProperty implementations. - -This is a private module which defines the behavior of invidual ORM- -mapped attributes. - -""" -from __future__ import absolute_import - -from .. import util, log -from ..sql import expression -from . import attributes -from .util import _orm_full_deannotate - -from .interfaces import PropComparator, StrategizedProperty - -__all__ = ['ColumnProperty', 'CompositeProperty', 'SynonymProperty', - 'ComparableProperty', 'RelationshipProperty'] - - -@log.class_logger -class ColumnProperty(StrategizedProperty): - """Describes an object attribute that corresponds to a table column. - - Public constructor is the :func:`.orm.column_property` function. - - """ - - strategy_wildcard_key = 'column' - - def __init__(self, *columns, **kwargs): - """Provide a column-level property for use with a Mapper. - - Column-based properties can normally be applied to the mapper's - ``properties`` dictionary using the :class:`.Column` element directly. - Use this function when the given column is not directly present within the - mapper's selectable; examples include SQL expressions, functions, and - scalar SELECT queries. - - Columns that aren't present in the mapper's selectable won't be persisted - by the mapper and are effectively "read-only" attributes. - - :param \*cols: - list of Column objects to be mapped. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - scalar attribute should be loaded when replaced, if not - already loaded. Normally, history tracking logic for - simple non-primary-key scalar values only needs to be - aware of the "new" value in order to perform a flush. This - flag is available for applications that make use of - :func:`.attributes.get_history` or :meth:`.Session.is_modified` - which also need to know - the "previous" value of the attribute. - - .. versionadded:: 0.6.6 - - :param comparator_factory: a class which extends - :class:`.ColumnProperty.Comparator` which provides custom SQL clause - generation for comparison operations. - - :param group: - a group name for this property when marked as deferred. - - :param deferred: - when True, the column property is "deferred", meaning that - it does not load immediately, and is instead loaded when the - attribute is first accessed on an instance. See also - :func:`~sqlalchemy.orm.deferred`. - - :param doc: - optional string that will be applied as the doc on the - class-bound descriptor. - - :param expire_on_flush=True: - Disable expiry on flush. A column_property() which refers - to a SQL expression (and not a single table-bound column) - is considered to be a "read only" property; populating it - has no effect on the state of data, and it can only return - database state. For this reason a column_property()'s value - is expired whenever the parent object is involved in a - flush, that is, has any kind of "dirty" state within a flush. - Setting this parameter to ``False`` will have the effect of - leaving any existing value present after the flush proceeds. - Note however that the :class:`.Session` with default expiration - settings still expires - all attributes after a :meth:`.Session.commit` call, however. - - .. versionadded:: 0.7.3 - - :param info: Optional data dictionary which will be populated into the - :attr:`.MapperProperty.info` attribute of this object. - - .. versionadded:: 0.8 - - :param extension: - an - :class:`.AttributeExtension` - instance, or list of extensions, which will be prepended - to the list of attribute listeners for the resulting - descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - """ - self._orig_columns = [expression._labeled(c) for c in columns] - self.columns = [expression._labeled(_orm_full_deannotate(c)) - for c in columns] - self.group = kwargs.pop('group', None) - self.deferred = kwargs.pop('deferred', False) - self.instrument = kwargs.pop('_instrument', True) - self.comparator_factory = kwargs.pop('comparator_factory', - self.__class__.Comparator) - self.descriptor = kwargs.pop('descriptor', None) - self.extension = kwargs.pop('extension', None) - self.active_history = kwargs.pop('active_history', False) - self.expire_on_flush = kwargs.pop('expire_on_flush', True) - - if 'info' in kwargs: - self.info = kwargs.pop('info') - - if 'doc' in kwargs: - self.doc = kwargs.pop('doc') - else: - for col in reversed(self.columns): - doc = getattr(col, 'doc', None) - if doc is not None: - self.doc = doc - break - else: - self.doc = None - - if kwargs: - raise TypeError( - "%s received unexpected keyword argument(s): %s" % ( - self.__class__.__name__, - ', '.join(sorted(kwargs.keys())))) - - util.set_creation_order(self) - - self.strategy_class = self._strategy_lookup( - ("deferred", self.deferred), - ("instrument", self.instrument) - ) - - @property - def expression(self): - """Return the primary column or expression for this ColumnProperty. - - """ - return self.columns[0] - - def instrument_class(self, mapper): - if not self.instrument: - return - - attributes.register_descriptor( - mapper.class_, - self.key, - comparator=self.comparator_factory(self, mapper), - parententity=mapper, - doc=self.doc - ) - - def do_init(self): - super(ColumnProperty, self).do_init() - if len(self.columns) > 1 and \ - set(self.parent.primary_key).issuperset(self.columns): - util.warn( - ("On mapper %s, primary key column '%s' is being combined " - "with distinct primary key column '%s' in attribute '%s'. " - "Use explicit properties to give each column its own mapped " - "attribute name.") % (self.parent, self.columns[1], - self.columns[0], self.key)) - - def copy(self): - return ColumnProperty( - deferred=self.deferred, - group=self.group, - active_history=self.active_history, - *self.columns) - - def _getcommitted(self, state, dict_, column, - passive=attributes.PASSIVE_OFF): - return state.get_impl(self.key).\ - get_committed_value(state, dict_, passive=passive) - - def merge(self, session, source_state, source_dict, dest_state, - dest_dict, load, _recursive): - if not self.instrument: - return - elif self.key in source_dict: - value = source_dict[self.key] - - if not load: - dest_dict[self.key] = value - else: - impl = dest_state.get_impl(self.key) - impl.set(dest_state, dest_dict, value, None) - elif dest_state.has_identity and self.key not in dest_dict: - dest_state._expire_attributes(dest_dict, [self.key]) - - class Comparator(PropComparator): - """Produce boolean, comparison, and other operators for - :class:`.ColumnProperty` attributes. - - See the documentation for :class:`.PropComparator` for a brief - overview. - - See also: - - :class:`.PropComparator` - - :class:`.ColumnOperators` - - :ref:`types_operators` - - :attr:`.TypeEngine.comparator_factory` - - """ - @util.memoized_instancemethod - def __clause_element__(self): - if self.adapter: - return self.adapter(self.prop.columns[0]) - else: - return self.prop.columns[0]._annotate({ - "parententity": self._parentmapper, - "parentmapper": self._parentmapper}) - - @util.memoized_property - def info(self): - ce = self.__clause_element__() - try: - return ce.info - except AttributeError: - return self.prop.info - - def __getattr__(self, key): - """proxy attribute access down to the mapped column. - - this allows user-defined comparison methods to be accessed. - """ - return getattr(self.__clause_element__(), key) - - def operate(self, op, *other, **kwargs): - return op(self.__clause_element__(), *other, **kwargs) - - def reverse_operate(self, op, other, **kwargs): - col = self.__clause_element__() - return op(col._bind_param(op, other), col, **kwargs) - - def __str__(self): - return str(self.parent.class_.__name__) + "." + self.key - diff --git a/libs/sqlalchemy/orm/query.py b/libs/sqlalchemy/orm/query.py deleted file mode 100644 index 6bd465e9..00000000 --- a/libs/sqlalchemy/orm/query.py +++ /dev/null @@ -1,3550 +0,0 @@ -# orm/query.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The Query class and support. - -Defines the :class:`.Query` class, the central -construct used by the ORM to construct database queries. - -The :class:`.Query` class should not be confused with the -:class:`.Select` class, which defines database -SELECT operations at the SQL (non-ORM) level. ``Query`` differs from -``Select`` in that it returns ORM-mapped objects and interacts with an -ORM session, whereas the ``Select`` construct interacts directly with the -database to return iterable result sets. - -""" - -from itertools import chain - -from . import ( - attributes, interfaces, object_mapper, persistence, - exc as orm_exc, loading - ) -from .base import _entity_descriptor, _is_aliased_class, \ - _is_mapped_class, _orm_columns, _generative -from .path_registry import PathRegistry -from .util import ( - AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased - ) -from .. import sql, util, log, exc as sa_exc, inspect, inspection -from ..sql.expression import _interpret_as_from -from ..sql import ( - util as sql_util, - expression, visitors - ) -from ..sql.base import ColumnCollection -from . import properties - -__all__ = ['Query', 'QueryContext', 'aliased'] - - -_path_registry = PathRegistry.root - -@inspection._self_inspects -@log.class_logger -class Query(object): - """ORM-level SQL construction object. - - :class:`.Query` is the source of all SELECT statements generated by the - ORM, both those formulated by end-user query operations as well as by - high level internal operations such as related collection loading. It - features a generative interface whereby successive calls return a new - :class:`.Query` object, a copy of the former with additional - criteria and options associated with it. - - :class:`.Query` objects are normally initially generated using the - :meth:`~.Session.query` method of :class:`.Session`. For a full - walkthrough of :class:`.Query` usage, see the - :ref:`ormtutorial_toplevel`. - - """ - - _enable_eagerloads = True - _enable_assertions = True - _with_labels = False - _criterion = None - _yield_per = None - _order_by = False - _group_by = False - _having = None - _distinct = False - _prefixes = None - _offset = None - _limit = None - _for_update_arg = None - _statement = None - _correlate = frozenset() - _populate_existing = False - _invoke_all_eagers = True - _version_check = False - _autoflush = True - _only_load_props = None - _refresh_state = None - _from_obj = () - _join_entities = () - _select_from_entity = None - _mapper_adapter_map = {} - _filter_aliases = None - _from_obj_alias = None - _joinpath = _joinpoint = util.immutabledict() - _execution_options = util.immutabledict() - _params = util.immutabledict() - _attributes = util.immutabledict() - _with_options = () - _with_hints = () - _enable_single_crit = True - - _current_path = _path_registry - - def __init__(self, entities, session=None): - self.session = session - self._polymorphic_adapters = {} - self._set_entities(entities) - - def _set_entities(self, entities, entity_wrapper=None): - if entity_wrapper is None: - entity_wrapper = _QueryEntity - self._entities = [] - self._primary_entity = None - for ent in util.to_list(entities): - entity_wrapper(self, ent) - - self._set_entity_selectables(self._entities) - - def _set_entity_selectables(self, entities): - self._mapper_adapter_map = d = self._mapper_adapter_map.copy() - - for ent in entities: - for entity in ent.entities: - if entity not in d: - ext_info = inspect(entity) - if not ext_info.is_aliased_class and \ - ext_info.mapper.with_polymorphic: - if ext_info.mapper.mapped_table not in \ - self._polymorphic_adapters: - self._mapper_loads_polymorphically_with( - ext_info.mapper, - sql_util.ColumnAdapter( - ext_info.selectable, - ext_info.mapper._equivalent_columns - ) - ) - aliased_adapter = None - elif ext_info.is_aliased_class: - aliased_adapter = sql_util.ColumnAdapter( - ext_info.selectable, - ext_info.mapper._equivalent_columns - ) - else: - aliased_adapter = None - - d[entity] = ( - ext_info, - aliased_adapter - ) - ent.setup_entity(*d[entity]) - - def _mapper_loads_polymorphically_with(self, mapper, adapter): - for m2 in mapper._with_polymorphic_mappers or [mapper]: - self._polymorphic_adapters[m2] = adapter - for m in m2.iterate_to_root(): - self._polymorphic_adapters[m.local_table] = adapter - - def _set_select_from(self, obj, set_base_alias): - fa = [] - select_from_alias = None - - for from_obj in obj: - info = inspect(from_obj) - - if hasattr(info, 'mapper') and \ - (info.is_mapper or info.is_aliased_class): - if set_base_alias: - raise sa_exc.ArgumentError( - "A selectable (FromClause) instance is " - "expected when the base alias is being set.") - fa.append(info.selectable) - elif not info.is_selectable: - raise sa_exc.ArgumentError( - "argument is not a mapped class, mapper, " - "aliased(), or FromClause instance.") - else: - if isinstance(from_obj, expression.SelectBase): - from_obj = from_obj.alias() - if set_base_alias: - select_from_alias = from_obj - fa.append(from_obj) - - self._from_obj = tuple(fa) - - if set_base_alias and \ - len(self._from_obj) == 1 and \ - isinstance(select_from_alias, expression.Alias): - equivs = self.__all_equivs() - self._from_obj_alias = sql_util.ColumnAdapter( - self._from_obj[0], equivs) - - def _reset_polymorphic_adapter(self, mapper): - for m2 in mapper._with_polymorphic_mappers: - self._polymorphic_adapters.pop(m2, None) - for m in m2.iterate_to_root(): - self._polymorphic_adapters.pop(m.local_table, None) - - def _adapt_polymorphic_element(self, element): - if "parententity" in element._annotations: - search = element._annotations['parententity'] - alias = self._polymorphic_adapters.get(search, None) - if alias: - return alias.adapt_clause(element) - - if isinstance(element, expression.FromClause): - search = element - elif hasattr(element, 'table'): - search = element.table - else: - return None - - alias = self._polymorphic_adapters.get(search, None) - if alias: - return alias.adapt_clause(element) - - def _adapt_col_list(self, cols): - return [ - self._adapt_clause( - expression._literal_as_text(o), - True, True) - for o in cols - ] - - @_generative() - def _adapt_all_clauses(self): - self._orm_only_adapt = False - - def _adapt_clause(self, clause, as_filter, orm_only): - """Adapt incoming clauses to transformations which - have been applied within this query.""" - - adapters = [] - - # do we adapt all expression elements or only those - # tagged as 'ORM' constructs ? - orm_only = getattr(self, '_orm_only_adapt', orm_only) - - if as_filter and self._filter_aliases: - for fa in self._filter_aliases._visitor_iterator: - adapters.append( - ( - orm_only, fa.replace - ) - ) - - if self._from_obj_alias: - # for the "from obj" alias, apply extra rule to the - # 'ORM only' check, if this query were generated from a - # subquery of itself, i.e. _from_selectable(), apply adaption - # to all SQL constructs. - adapters.append( - ( - getattr(self, '_orm_only_from_obj_alias', orm_only), - self._from_obj_alias.replace - ) - ) - - if self._polymorphic_adapters: - adapters.append( - ( - orm_only, self._adapt_polymorphic_element - ) - ) - - if not adapters: - return clause - - def replace(elem): - for _orm_only, adapter in adapters: - # if 'orm only', look for ORM annotations - # in the element before adapting. - if not _orm_only or \ - '_orm_adapt' in elem._annotations or \ - "parententity" in elem._annotations: - - e = adapter(elem) - if e is not None: - return e - - return visitors.replacement_traverse( - clause, - {}, - replace - ) - - def _entity_zero(self): - return self._entities[0] - - def _mapper_zero(self): - return self._select_from_entity or \ - self._entity_zero().entity_zero - - @property - def _mapper_entities(self): - for ent in self._entities: - if isinstance(ent, _MapperEntity): - yield ent - - def _joinpoint_zero(self): - return self._joinpoint.get( - '_joinpoint_entity', - self._mapper_zero() - ) - - def _mapper_zero_or_none(self): - if self._primary_entity: - return self._primary_entity.mapper - else: - return None - - def _only_mapper_zero(self, rationale=None): - if len(self._entities) > 1: - raise sa_exc.InvalidRequestError( - rationale or - "This operation requires a Query " - "against a single mapper." - ) - return self._mapper_zero() - - def _only_full_mapper_zero(self, methname): - if self._entities != [self._primary_entity]: - raise sa_exc.InvalidRequestError( - "%s() can only be used against " - "a single mapped class." % methname) - return self._primary_entity.entity_zero - - def _only_entity_zero(self, rationale=None): - if len(self._entities) > 1: - raise sa_exc.InvalidRequestError( - rationale or - "This operation requires a Query " - "against a single mapper." - ) - return self._entity_zero() - - def __all_equivs(self): - equivs = {} - for ent in self._mapper_entities: - equivs.update(ent.mapper._equivalent_columns) - return equivs - - def _get_condition(self): - self._order_by = self._distinct = False - return self._no_criterion_condition("get") - - def _no_criterion_condition(self, meth): - if not self._enable_assertions: - return - if self._criterion is not None or \ - self._statement is not None or self._from_obj or \ - self._limit is not None or self._offset is not None or \ - self._group_by or self._order_by or self._distinct: - raise sa_exc.InvalidRequestError( - "Query.%s() being called on a " - "Query with existing criterion. " % meth) - - self._from_obj = () - self._statement = self._criterion = None - self._order_by = self._group_by = self._distinct = False - - def _no_clauseelement_condition(self, meth): - if not self._enable_assertions: - return - if self._order_by: - raise sa_exc.InvalidRequestError( - "Query.%s() being called on a " - "Query with existing criterion. " % meth) - self._no_criterion_condition(meth) - - def _no_statement_condition(self, meth): - if not self._enable_assertions: - return - if self._statement is not None: - raise sa_exc.InvalidRequestError( - ("Query.%s() being called on a Query with an existing full " - "statement - can't apply criterion.") % meth) - - def _no_limit_offset(self, meth): - if not self._enable_assertions: - return - if self._limit is not None or self._offset is not None: - raise sa_exc.InvalidRequestError( - "Query.%s() being called on a Query which already has LIMIT " - "or OFFSET applied. To modify the row-limited results of a " - " Query, call from_self() first. " - "Otherwise, call %s() before limit() or offset() " - "are applied." - % (meth, meth) - ) - - def _no_select_modifiers(self, meth): - if not self._enable_assertions: - return - for attr, methname, notset in ( - ('_limit', 'limit()', None), - ('_offset', 'offset()', None), - ('_order_by', 'order_by()', False), - ('_group_by', 'group_by()', False), - ('_distinct', 'distinct()', False), - ): - if getattr(self, attr) is not notset: - raise sa_exc.InvalidRequestError( - "Can't call Query.%s() when %s has been called" % - (meth, methname) - ) - - def _get_options(self, populate_existing=None, - version_check=None, - only_load_props=None, - refresh_state=None): - if populate_existing: - self._populate_existing = populate_existing - if version_check: - self._version_check = version_check - if refresh_state: - self._refresh_state = refresh_state - if only_load_props: - self._only_load_props = set(only_load_props) - return self - - def _clone(self): - cls = self.__class__ - q = cls.__new__(cls) - q.__dict__ = self.__dict__.copy() - return q - - @property - def statement(self): - """The full SELECT statement represented by this Query. - - The statement by default will not have disambiguating labels - applied to the construct unless with_labels(True) is called - first. - - """ - - stmt = self._compile_context(labels=self._with_labels).\ - statement - if self._params: - stmt = stmt.params(self._params) - - - # TODO: there's no tests covering effects of - # the annotation not being there - return stmt._annotate({'no_replacement_traverse': True}) - - def subquery(self, name=None, with_labels=False, reduce_columns=False): - """return the full SELECT statement represented by - this :class:`.Query`, embedded within an :class:`.Alias`. - - Eager JOIN generation within the query is disabled. - - :param name: string name to be assigned as the alias; - this is passed through to :meth:`.FromClause.alias`. - If ``None``, a name will be deterministically generated - at compile time. - - :param with_labels: if True, :meth:`.with_labels` will be called - on the :class:`.Query` first to apply table-qualified labels - to all columns. - - :param reduce_columns: if True, :meth:`.Select.reduce_columns` will - be called on the resulting :func:`.select` construct, - to remove same-named columns where one also refers to the other - via foreign key or WHERE clause equivalence. - - .. versionchanged:: 0.8 the ``with_labels`` and ``reduce_columns`` - keyword arguments were added. - - """ - q = self.enable_eagerloads(False) - if with_labels: - q = q.with_labels() - q = q.statement - if reduce_columns: - q = q.reduce_columns() - return q.alias(name=name) - - def cte(self, name=None, recursive=False): - """Return the full SELECT statement represented by this - :class:`.Query` represented as a common table expression (CTE). - - .. versionadded:: 0.7.6 - - Parameters and usage are the same as those of the - :meth:`.SelectBase.cte` method; see that method for - further details. - - Here is the `Postgresql WITH - RECURSIVE example - `_. - Note that, in this example, the ``included_parts`` cte and the - ``incl_alias`` alias of it are Core selectables, which - means the columns are accessed via the ``.c.`` attribute. The - ``parts_alias`` object is an :func:`.orm.aliased` instance of the - ``Part`` entity, so column-mapped attributes are available - directly:: - - from sqlalchemy.orm import aliased - - class Part(Base): - __tablename__ = 'part' - part = Column(String, primary_key=True) - sub_part = Column(String, primary_key=True) - quantity = Column(Integer) - - included_parts = session.query( - Part.sub_part, - Part.part, - Part.quantity).\\ - filter(Part.part=="our part").\\ - cte(name="included_parts", recursive=True) - - incl_alias = aliased(included_parts, name="pr") - parts_alias = aliased(Part, name="p") - included_parts = included_parts.union_all( - session.query( - parts_alias.part, - parts_alias.sub_part, - parts_alias.quantity).\\ - filter(parts_alias.part==incl_alias.c.sub_part) - ) - - q = session.query( - included_parts.c.sub_part, - func.sum(included_parts.c.quantity). - label('total_quantity') - ).\\ - group_by(included_parts.c.sub_part) - - See also: - - :meth:`.SelectBase.cte` - - """ - return self.enable_eagerloads(False).\ - statement.cte(name=name, recursive=recursive) - - def label(self, name): - """Return the full SELECT statement represented by this - :class:`.Query`, converted - to a scalar subquery with a label of the given name. - - Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.label`. - - .. versionadded:: 0.6.5 - - """ - - return self.enable_eagerloads(False).statement.label(name) - - def as_scalar(self): - """Return the full SELECT statement represented by this - :class:`.Query`, converted to a scalar subquery. - - Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.as_scalar`. - - .. versionadded:: 0.6.5 - - """ - - return self.enable_eagerloads(False).statement.as_scalar() - - @property - def selectable(self): - """Return the :class:`.Select` object emitted by this :class:`.Query`. - - Used for :func:`.inspect` compatibility, this is equivalent to:: - - query.enable_eagerloads(False).with_labels().statement - - """ - return self.__clause_element__() - - def __clause_element__(self): - return self.enable_eagerloads(False).with_labels().statement - - @_generative() - def enable_eagerloads(self, value): - """Control whether or not eager joins and subqueries are - rendered. - - When set to False, the returned Query will not render - eager joins regardless of :func:`~sqlalchemy.orm.joinedload`, - :func:`~sqlalchemy.orm.subqueryload` options - or mapper-level ``lazy='joined'``/``lazy='subquery'`` - configurations. - - This is used primarily when nesting the Query's - statement into a subquery or other - selectable. - - """ - self._enable_eagerloads = value - - @_generative() - def with_labels(self): - """Apply column labels to the return value of Query.statement. - - Indicates that this Query's `statement` accessor should return - a SELECT statement that applies labels to all columns in the - form _; this is commonly used to - disambiguate columns from multiple tables which have the same - name. - - When the `Query` actually issues SQL to load rows, it always - uses column labeling. - - """ - self._with_labels = True - - @_generative() - def enable_assertions(self, value): - """Control whether assertions are generated. - - When set to False, the returned Query will - not assert its state before certain operations, - including that LIMIT/OFFSET has not been applied - when filter() is called, no criterion exists - when get() is called, and no "from_statement()" - exists when filter()/order_by()/group_by() etc. - is called. This more permissive mode is used by - custom Query subclasses to specify criterion or - other modifiers outside of the usual usage patterns. - - Care should be taken to ensure that the usage - pattern is even possible. A statement applied - by from_statement() will override any criterion - set by filter() or order_by(), for example. - - """ - self._enable_assertions = value - - @property - def whereclause(self): - """A readonly attribute which returns the current WHERE criterion for - this Query. - - This returned value is a SQL expression construct, or ``None`` if no - criterion has been established. - - """ - return self._criterion - - @_generative() - def _with_current_path(self, path): - """indicate that this query applies to objects loaded - within a certain path. - - Used by deferred loaders (see strategies.py) which transfer - query options from an originating query to a newly generated - query intended for the deferred load. - - """ - self._current_path = path - - @_generative(_no_clauseelement_condition) - def with_polymorphic(self, - cls_or_mappers, - selectable=None, - polymorphic_on=None): - """Load columns for inheriting classes. - - :meth:`.Query.with_polymorphic` applies transformations - to the "main" mapped class represented by this :class:`.Query`. - The "main" mapped class here means the :class:`.Query` - object's first argument is a full class, i.e. - ``session.query(SomeClass)``. These transformations allow additional - tables to be present in the FROM clause so that columns for a - joined-inheritance subclass are available in the query, both for the - purposes of load-time efficiency as well as the ability to use - these columns at query time. - - See the documentation section :ref:`with_polymorphic` for - details on how this method is used. - - .. versionchanged:: 0.8 - A new and more flexible function - :func:`.orm.with_polymorphic` supersedes - :meth:`.Query.with_polymorphic`, as it can apply the equivalent - functionality to any set of columns or classes in the - :class:`.Query`, not just the "zero mapper". See that - function for a description of arguments. - - """ - - if not self._primary_entity: - raise sa_exc.InvalidRequestError( - "No primary mapper set up for this Query.") - entity = self._entities[0]._clone() - self._entities = [entity] + self._entities[1:] - entity.set_with_polymorphic(self, - cls_or_mappers, - selectable=selectable, - polymorphic_on=polymorphic_on) - - @_generative() - def yield_per(self, count): - """Yield only ``count`` rows at a time. - - WARNING: use this method with caution; if the same instance is present - in more than one batch of rows, end-user changes to attributes will be - overwritten. - - In particular, it's usually impossible to use this setting with - eagerly loaded collections (i.e. any lazy='joined' or 'subquery') - since those collections will be cleared for a new load when - encountered in a subsequent result batch. In the case of 'subquery' - loading, the full result for all rows is fetched which generally - defeats the purpose of :meth:`~sqlalchemy.orm.query.Query.yield_per`. - - Also note that while :meth:`~sqlalchemy.orm.query.Query.yield_per` - will set the ``stream_results`` execution option to True, currently - this is only understood by :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect - which will stream results using server side cursors instead of pre-buffer - all rows for this query. Other DBAPIs pre-buffer all rows before - making them available. - - """ - self._yield_per = count - self._execution_options = self._execution_options.union( - {"stream_results": True}) - - def get(self, ident): - """Return an instance based on the given primary key identifier, - or ``None`` if not found. - - E.g.:: - - my_user = session.query(User).get(5) - - some_object = session.query(VersionedFoo).get((5, 10)) - - :meth:`~.Query.get` is special in that it provides direct - access to the identity map of the owning :class:`.Session`. - If the given primary key identifier is present - in the local identity map, the object is returned - directly from this collection and no SQL is emitted, - unless the object has been marked fully expired. - If not present, - a SELECT is performed in order to locate the object. - - :meth:`~.Query.get` also will perform a check if - the object is present in the identity map and - marked as expired - a SELECT - is emitted to refresh the object as well as to - ensure that the row is still present. - If not, :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. - - :meth:`~.Query.get` is only used to return a single - mapped instance, not multiple instances or - individual column constructs, and strictly - on a single primary key value. The originating - :class:`.Query` must be constructed in this way, - i.e. against a single mapped entity, - with no additional filtering criterion. Loading - options via :meth:`~.Query.options` may be applied - however, and will be used if the object is not - yet locally present. - - A lazy-loading, many-to-one attribute configured - by :func:`.relationship`, using a simple - foreign-key-to-primary-key criterion, will also use an - operation equivalent to :meth:`~.Query.get` in order to retrieve - the target value from the local identity map - before querying the database. See :doc:`/orm/loading` - for further details on relationship loading. - - :param ident: A scalar or tuple value representing - the primary key. For a composite primary key, - the order of identifiers corresponds in most cases - to that of the mapped :class:`.Table` object's - primary key columns. For a :func:`.mapper` that - was given the ``primary key`` argument during - construction, the order of identifiers corresponds - to the elements present in this collection. - - :return: The object instance, or ``None``. - - """ - - # convert composite types to individual args - if hasattr(ident, '__composite_values__'): - ident = ident.__composite_values__() - - ident = util.to_list(ident) - - mapper = self._only_full_mapper_zero("get") - - if len(ident) != len(mapper.primary_key): - raise sa_exc.InvalidRequestError( - "Incorrect number of values in identifier to formulate " - "primary key for query.get(); primary key columns are %s" % - ','.join("'%s'" % c for c in mapper.primary_key)) - - key = mapper.identity_key_from_primary_key(ident) - - if not self._populate_existing and \ - not mapper.always_refresh and \ - self._for_update_arg is None: - - instance = loading.get_from_identity( - self.session, key, attributes.PASSIVE_OFF) - if instance is not None: - # reject calls for id in identity map but class - # mismatch. - if not issubclass(instance.__class__, mapper.class_): - return None - return instance - - return loading.load_on_ident(self, key) - - @_generative() - def correlate(self, *args): - """Return a :class:`.Query` construct which will correlate the given - FROM clauses to that of an enclosing :class:`.Query` or - :func:`~.expression.select`. - - The method here accepts mapped classes, :func:`.aliased` constructs, - and :func:`.mapper` constructs as arguments, which are resolved into - expression constructs, in addition to appropriate expression - constructs. - - The correlation arguments are ultimately passed to - :meth:`.Select.correlate` after coercion to expression constructs. - - The correlation arguments take effect in such cases - as when :meth:`.Query.from_self` is used, or when - a subquery as returned by :meth:`.Query.subquery` is - embedded in another :func:`~.expression.select` construct. - - """ - - self._correlate = self._correlate.union( - _interpret_as_from(s) - if s is not None else None - for s in args) - - @_generative() - def autoflush(self, setting): - """Return a Query with a specific 'autoflush' setting. - - Note that a Session with autoflush=False will - not autoflush, even if this flag is set to True at the - Query level. Therefore this flag is usually used only - to disable autoflush for a specific Query. - - """ - self._autoflush = setting - - @_generative() - def populate_existing(self): - """Return a :class:`.Query` that will expire and refresh all instances - as they are loaded, or reused from the current :class:`.Session`. - - :meth:`.populate_existing` does not improve behavior when - the ORM is used normally - the :class:`.Session` object's usual - behavior of maintaining a transaction and expiring all attributes - after rollback or commit handles object state automatically. - This method is not intended for general use. - - """ - self._populate_existing = True - - @_generative() - def _with_invoke_all_eagers(self, value): - """Set the 'invoke all eagers' flag which causes joined- and - subquery loaders to traverse into already-loaded related objects - and collections. - - Default is that of :attr:`.Query._invoke_all_eagers`. - - """ - self._invoke_all_eagers = value - - def with_parent(self, instance, property=None): - """Add filtering criterion that relates the given instance - to a child object or collection, using its attribute state - as well as an established :func:`.relationship()` - configuration. - - The method uses the :func:`.with_parent` function to generate - the clause, the result of which is passed to :meth:`.Query.filter`. - - Parameters are the same as :func:`.with_parent`, with the exception - that the given property can be None, in which case a search is - performed against this :class:`.Query` object's target mapper. - - """ - - if property is None: - mapper = object_mapper(instance) - - for prop in mapper.iterate_properties: - if isinstance(prop, properties.RelationshipProperty) and \ - prop.mapper is self._mapper_zero(): - property = prop - break - else: - raise sa_exc.InvalidRequestError( - "Could not locate a property which relates instances " - "of class '%s' to instances of class '%s'" % - ( - self._mapper_zero().class_.__name__, - instance.__class__.__name__) - ) - - return self.filter(with_parent(instance, property)) - - @_generative() - def add_entity(self, entity, alias=None): - """add a mapped entity to the list of result columns - to be returned.""" - - if alias is not None: - entity = aliased(entity, alias) - - self._entities = list(self._entities) - m = _MapperEntity(self, entity) - self._set_entity_selectables([m]) - - @_generative() - def with_session(self, session): - """Return a :class:`.Query` that will use the given :class:`.Session`. - - """ - - self.session = session - - def from_self(self, *entities): - """return a Query that selects from this Query's - SELECT statement. - - \*entities - optional list of entities which will replace - those being selected. - - """ - fromclause = self.with_labels().enable_eagerloads(False).\ - _enable_single_crit(False).\ - statement.correlate(None) - q = self._from_selectable(fromclause) - if entities: - q._set_entities(entities) - return q - - @_generative() - def _enable_single_crit(self, val): - self._enable_single_crit = val - - @_generative() - def _from_selectable(self, fromclause): - for attr in ( - '_statement', '_criterion', - '_order_by', '_group_by', - '_limit', '_offset', - '_joinpath', '_joinpoint', - '_distinct', '_having', - '_prefixes', - ): - self.__dict__.pop(attr, None) - self._set_select_from([fromclause], True) - - # this enables clause adaptation for non-ORM - # expressions. - self._orm_only_from_obj_alias = False - - old_entities = self._entities - self._entities = [] - for e in old_entities: - e.adapt_to_selectable(self, self._from_obj[0]) - - def values(self, *columns): - """Return an iterator yielding result tuples corresponding - to the given list of columns""" - - if not columns: - return iter(()) - q = self._clone() - q._set_entities(columns, entity_wrapper=_ColumnEntity) - if not q._yield_per: - q._yield_per = 10 - return iter(q) - _values = values - - def value(self, column): - """Return a scalar result corresponding to the given - column expression.""" - try: - return next(self.values(column))[0] - except StopIteration: - return None - - @_generative() - def with_entities(self, *entities): - """Return a new :class:`.Query` replacing the SELECT list with the - given entities. - - e.g.:: - - # Users, filtered on some arbitrary criterion - # and then ordered by related email address - q = session.query(User).\\ - join(User.address).\\ - filter(User.name.like('%ed%')).\\ - order_by(Address.email) - - # given *only* User.id==5, Address.email, and 'q', what - # would the *next* User in the result be ? - subq = q.with_entities(Address.email).\\ - order_by(None).\\ - filter(User.id==5).\\ - subquery() - q = q.join((subq, subq.c.email < Address.email)).\\ - limit(1) - - .. versionadded:: 0.6.5 - - """ - self._set_entities(entities) - - @_generative() - def add_columns(self, *column): - """Add one or more column expressions to the list - of result columns to be returned.""" - - self._entities = list(self._entities) - l = len(self._entities) - for c in column: - _ColumnEntity(self, c) - # _ColumnEntity may add many entities if the - # given arg is a FROM clause - self._set_entity_selectables(self._entities[l:]) - - @util.pending_deprecation("0.7", - ":meth:`.add_column` is superseded by :meth:`.add_columns`", - False) - def add_column(self, column): - """Add a column expression to the list of result columns to be - returned. - - Pending deprecation: :meth:`.add_column` will be superseded by - :meth:`.add_columns`. - - """ - return self.add_columns(column) - - def options(self, *args): - """Return a new Query object, applying the given list of - mapper options. - - Most supplied options regard changing how column- and - relationship-mapped attributes are loaded. See the sections - :ref:`deferred` and :doc:`/orm/loading` for reference - documentation. - - """ - return self._options(False, *args) - - def _conditional_options(self, *args): - return self._options(True, *args) - - @_generative() - def _options(self, conditional, *args): - # most MapperOptions write to the '_attributes' dictionary, - # so copy that as well - self._attributes = self._attributes.copy() - opts = tuple(util.flatten_iterator(args)) - self._with_options = self._with_options + opts - if conditional: - for opt in opts: - opt.process_query_conditionally(self) - else: - for opt in opts: - opt.process_query(self) - - def with_transformation(self, fn): - """Return a new :class:`.Query` object transformed by - the given function. - - E.g.:: - - def filter_something(criterion): - def transform(q): - return q.filter(criterion) - return transform - - q = q.with_transformation(filter_something(x==5)) - - This allows ad-hoc recipes to be created for :class:`.Query` - objects. See the example at :ref:`hybrid_transformers`. - - .. versionadded:: 0.7.4 - - """ - return fn(self) - - @_generative() - def with_hint(self, selectable, text, dialect_name='*'): - """Add an indexing hint for the given entity or selectable to - this :class:`.Query`. - - Functionality is passed straight through to - :meth:`~sqlalchemy.sql.expression.Select.with_hint`, - with the addition that ``selectable`` can be a - :class:`.Table`, :class:`.Alias`, or ORM entity / mapped class - /etc. - """ - selectable = inspect(selectable).selectable - - self._with_hints += ((selectable, text, dialect_name),) - - @_generative() - def execution_options(self, **kwargs): - """ Set non-SQL options which take effect during execution. - - The options are the same as those accepted by - :meth:`.Connection.execution_options`. - - Note that the ``stream_results`` execution option is enabled - automatically if the :meth:`~sqlalchemy.orm.query.Query.yield_per()` - method is used. - - """ - self._execution_options = self._execution_options.union(kwargs) - - @_generative() - def with_lockmode(self, mode): - """Return a new :class:`.Query` object with the specified "locking mode", - which essentially refers to the ``FOR UPDATE`` clause. - - .. deprecated:: 0.9.0 superseded by :meth:`.Query.with_for_update`. - - :param mode: a string representing the desired locking mode. - Valid values are: - - * ``None`` - translates to no lockmode - - * ``'update'`` - translates to ``FOR UPDATE`` - (standard SQL, supported by most dialects) - - * ``'update_nowait'`` - translates to ``FOR UPDATE NOWAIT`` - (supported by Oracle, PostgreSQL 8.1 upwards) - - * ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL), - and ``FOR SHARE`` (for PostgreSQL) - - .. seealso:: - - :meth:`.Query.with_for_update` - improved API for - specifying the ``FOR UPDATE`` clause. - - """ - self._for_update_arg = LockmodeArg.parse_legacy_query(mode) - - @_generative() - def with_for_update(self, read=False, nowait=False, of=None): - """return a new :class:`.Query` with the specified options for the - ``FOR UPDATE`` clause. - - The behavior of this method is identical to that of - :meth:`.SelectBase.with_for_update`. When called with no arguments, - the resulting ``SELECT`` statement will have a ``FOR UPDATE`` clause - appended. When additional arguments are specified, backend-specific - options such as ``FOR UPDATE NOWAIT`` or ``LOCK IN SHARE MODE`` - can take effect. - - E.g.:: - - q = sess.query(User).with_for_update(nowait=True, of=User) - - The above query on a Postgresql backend will render like:: - - SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT - - .. versionadded:: 0.9.0 :meth:`.Query.with_for_update` supersedes - the :meth:`.Query.with_lockmode` method. - - .. seealso:: - - :meth:`.GenerativeSelect.with_for_update` - Core level method with - full argument and behavioral description. - - """ - self._for_update_arg = LockmodeArg(read=read, nowait=nowait, of=of) - - @_generative() - def params(self, *args, **kwargs): - """add values for bind parameters which may have been - specified in filter(). - - parameters may be specified using \**kwargs, or optionally a single - dictionary as the first positional argument. The reason for both is - that \**kwargs is convenient, however some parameter dictionaries - contain unicode keys in which case \**kwargs cannot be used. - - """ - if len(args) == 1: - kwargs.update(args[0]) - elif len(args) > 0: - raise sa_exc.ArgumentError( - "params() takes zero or one positional argument, " - "which is a dictionary.") - self._params = self._params.copy() - self._params.update(kwargs) - - @_generative(_no_statement_condition, _no_limit_offset) - def filter(self, *criterion): - """apply the given filtering criterion to a copy - of this :class:`.Query`, using SQL expressions. - - e.g.:: - - session.query(MyClass).filter(MyClass.name == 'some name') - - Multiple criteria are joined together by AND:: - - session.query(MyClass).\\ - filter(MyClass.name == 'some name', MyClass.id > 5) - - The criterion is any SQL expression object applicable to the - WHERE clause of a select. String expressions are coerced - into SQL expression constructs via the :func:`.text` construct. - - .. versionchanged:: 0.7.5 - Multiple criteria joined by AND. - - See also: - - :meth:`.Query.filter_by` - filter on keyword expressions. - - """ - for criterion in list(criterion): - criterion = expression._literal_as_text(criterion) - - criterion = self._adapt_clause(criterion, True, True) - - if self._criterion is not None: - self._criterion = self._criterion & criterion - else: - self._criterion = criterion - - def filter_by(self, **kwargs): - """apply the given filtering criterion to a copy - of this :class:`.Query`, using keyword expressions. - - e.g.:: - - session.query(MyClass).filter_by(name = 'some name') - - Multiple criteria are joined together by AND:: - - session.query(MyClass).\\ - filter_by(name = 'some name', id = 5) - - The keyword expressions are extracted from the primary - entity of the query, or the last entity that was the - target of a call to :meth:`.Query.join`. - - See also: - - :meth:`.Query.filter` - filter on SQL expressions. - - """ - - clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value - for key, value in kwargs.items()] - return self.filter(sql.and_(*clauses)) - - @_generative(_no_statement_condition, _no_limit_offset) - def order_by(self, *criterion): - """apply one or more ORDER BY criterion to the query and return - the newly resulting ``Query`` - - All existing ORDER BY settings can be suppressed by - passing ``None`` - this will suppress any ORDER BY configured - on mappers as well. - - Alternatively, an existing ORDER BY setting on the Query - object can be entirely cancelled by passing ``False`` - as the value - use this before calling methods where - an ORDER BY is invalid. - - """ - - if len(criterion) == 1: - if criterion[0] is False: - if '_order_by' in self.__dict__: - del self._order_by - return - if criterion[0] is None: - self._order_by = None - return - - criterion = self._adapt_col_list(criterion) - - if self._order_by is False or self._order_by is None: - self._order_by = criterion - else: - self._order_by = self._order_by + criterion - - @_generative(_no_statement_condition, _no_limit_offset) - def group_by(self, *criterion): - """apply one or more GROUP BY criterion to the query and return - the newly resulting :class:`.Query`""" - - criterion = list(chain(*[_orm_columns(c) for c in criterion])) - criterion = self._adapt_col_list(criterion) - - if self._group_by is False: - self._group_by = criterion - else: - self._group_by = self._group_by + criterion - - @_generative(_no_statement_condition, _no_limit_offset) - def having(self, criterion): - """apply a HAVING criterion to the query and return the - newly resulting :class:`.Query`. - - :meth:`~.Query.having` is used in conjunction with :meth:`~.Query.group_by`. - - HAVING criterion makes it possible to use filters on aggregate - functions like COUNT, SUM, AVG, MAX, and MIN, eg.:: - - q = session.query(User.id).\\ - join(User.addresses).\\ - group_by(User.id).\\ - having(func.count(Address.id) > 2) - - """ - - if isinstance(criterion, util.string_types): - criterion = sql.text(criterion) - - if criterion is not None and \ - not isinstance(criterion, sql.ClauseElement): - raise sa_exc.ArgumentError( - "having() argument must be of type " - "sqlalchemy.sql.ClauseElement or string") - - criterion = self._adapt_clause(criterion, True, True) - - if self._having is not None: - self._having = self._having & criterion - else: - self._having = criterion - - def union(self, *q): - """Produce a UNION of this Query against one or more queries. - - e.g.:: - - q1 = sess.query(SomeClass).filter(SomeClass.foo=='bar') - q2 = sess.query(SomeClass).filter(SomeClass.bar=='foo') - - q3 = q1.union(q2) - - The method accepts multiple Query objects so as to control - the level of nesting. A series of ``union()`` calls such as:: - - x.union(y).union(z).all() - - will nest on each ``union()``, and produces:: - - SELECT * FROM (SELECT * FROM (SELECT * FROM X UNION - SELECT * FROM y) UNION SELECT * FROM Z) - - Whereas:: - - x.union(y, z).all() - - produces:: - - SELECT * FROM (SELECT * FROM X UNION SELECT * FROM y UNION - SELECT * FROM Z) - - Note that many database backends do not allow ORDER BY to - be rendered on a query called within UNION, EXCEPT, etc. - To disable all ORDER BY clauses including those configured - on mappers, issue ``query.order_by(None)`` - the resulting - :class:`.Query` object will not render ORDER BY within - its SELECT statement. - - """ - - return self._from_selectable( - expression.union(*([self] + list(q)))) - - def union_all(self, *q): - """Produce a UNION ALL of this Query against one or more queries. - - Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See - that method for usage examples. - - """ - return self._from_selectable( - expression.union_all(*([self] + list(q))) - ) - - def intersect(self, *q): - """Produce an INTERSECT of this Query against one or more queries. - - Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See - that method for usage examples. - - """ - return self._from_selectable( - expression.intersect(*([self] + list(q))) - ) - - def intersect_all(self, *q): - """Produce an INTERSECT ALL of this Query against one or more queries. - - Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See - that method for usage examples. - - """ - return self._from_selectable( - expression.intersect_all(*([self] + list(q))) - ) - - def except_(self, *q): - """Produce an EXCEPT of this Query against one or more queries. - - Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See - that method for usage examples. - - """ - return self._from_selectable( - expression.except_(*([self] + list(q))) - ) - - def except_all(self, *q): - """Produce an EXCEPT ALL of this Query against one or more queries. - - Works the same way as :meth:`~sqlalchemy.orm.query.Query.union`. See - that method for usage examples. - - """ - return self._from_selectable( - expression.except_all(*([self] + list(q))) - ) - - def join(self, *props, **kwargs): - """Create a SQL JOIN against this :class:`.Query` object's criterion - and apply generatively, returning the newly resulting :class:`.Query`. - - **Simple Relationship Joins** - - Consider a mapping between two classes ``User`` and ``Address``, - with a relationship ``User.addresses`` representing a collection - of ``Address`` objects associated with each ``User``. The most common - usage of :meth:`~.Query.join` is to create a JOIN along this - relationship, using the ``User.addresses`` attribute as an indicator - for how this should occur:: - - q = session.query(User).join(User.addresses) - - Where above, the call to :meth:`~.Query.join` along ``User.addresses`` - will result in SQL equivalent to:: - - SELECT user.* FROM user JOIN address ON user.id = address.user_id - - In the above example we refer to ``User.addresses`` as passed to - :meth:`~.Query.join` as the *on clause*, that is, it indicates - how the "ON" portion of the JOIN should be constructed. For a - single-entity query such as the one above (i.e. we start by selecting - only from ``User`` and nothing else), the relationship can also be - specified by its string name:: - - q = session.query(User).join("addresses") - - :meth:`~.Query.join` can also accommodate multiple - "on clause" arguments to produce a chain of joins, such as below - where a join across four related entities is constructed:: - - q = session.query(User).join("orders", "items", "keywords") - - The above would be shorthand for three separate calls to - :meth:`~.Query.join`, each using an explicit attribute to indicate - the source entity:: - - q = session.query(User).\\ - join(User.orders).\\ - join(Order.items).\\ - join(Item.keywords) - - **Joins to a Target Entity or Selectable** - - A second form of :meth:`~.Query.join` allows any mapped entity - or core selectable construct as a target. In this usage, - :meth:`~.Query.join` will attempt - to create a JOIN along the natural foreign key relationship between - two entities:: - - q = session.query(User).join(Address) - - The above calling form of :meth:`~.Query.join` will raise an error if - either there are no foreign keys between the two entities, or if - there are multiple foreign key linkages between them. In the - above calling form, :meth:`~.Query.join` is called upon to - create the "on clause" automatically for us. The target can - be any mapped entity or selectable, such as a :class:`.Table`:: - - q = session.query(User).join(addresses_table) - - **Joins to a Target with an ON Clause** - - The third calling form allows both the target entity as well - as the ON clause to be passed explicitly. Suppose for - example we wanted to join to ``Address`` twice, using - an alias the second time. We use :func:`~sqlalchemy.orm.aliased` - to create a distinct alias of ``Address``, and join - to it using the ``target, onclause`` form, so that the - alias can be specified explicitly as the target along with - the relationship to instruct how the ON clause should proceed:: - - a_alias = aliased(Address) - - q = session.query(User).\\ - join(User.addresses).\\ - join(a_alias, User.addresses).\\ - filter(Address.email_address=='ed@foo.com').\\ - filter(a_alias.email_address=='ed@bar.com') - - Where above, the generated SQL would be similar to:: - - SELECT user.* FROM user - JOIN address ON user.id = address.user_id - JOIN address AS address_1 ON user.id=address_1.user_id - WHERE address.email_address = :email_address_1 - AND address_1.email_address = :email_address_2 - - The two-argument calling form of :meth:`~.Query.join` - also allows us to construct arbitrary joins with SQL-oriented - "on clause" expressions, not relying upon configured relationships - at all. Any SQL expression can be passed as the ON clause - when using the two-argument form, which should refer to the target - entity in some way as well as an applicable source entity:: - - q = session.query(User).join(Address, User.id==Address.user_id) - - .. versionchanged:: 0.7 - In SQLAlchemy 0.6 and earlier, the two argument form of - :meth:`~.Query.join` requires the usage of a tuple: - ``query(User).join((Address, User.id==Address.user_id))``\ . - This calling form is accepted in 0.7 and further, though - is not necessary unless multiple join conditions are passed to - a single :meth:`~.Query.join` call, which itself is also not - generally necessary as it is now equivalent to multiple - calls (this wasn't always the case). - - **Advanced Join Targeting and Adaption** - - There is a lot of flexibility in what the "target" can be when using - :meth:`~.Query.join`. As noted previously, it also accepts - :class:`.Table` constructs and other selectables such as - :func:`.alias` and :func:`.select` constructs, with either the one - or two-argument forms:: - - addresses_q = select([Address.user_id]).\\ - where(Address.email_address.endswith("@bar.com")).\\ - alias() - - q = session.query(User).\\ - join(addresses_q, addresses_q.c.user_id==User.id) - - :meth:`~.Query.join` also features the ability to *adapt* a - :meth:`~sqlalchemy.orm.relationship` -driven ON clause to the target - selectable. Below we construct a JOIN from ``User`` to a subquery - against ``Address``, allowing the relationship denoted by - ``User.addresses`` to *adapt* itself to the altered target:: - - address_subq = session.query(Address).\\ - filter(Address.email_address == 'ed@foo.com').\\ - subquery() - - q = session.query(User).join(address_subq, User.addresses) - - Producing SQL similar to:: - - SELECT user.* FROM user - JOIN ( - SELECT address.id AS id, - address.user_id AS user_id, - address.email_address AS email_address - FROM address - WHERE address.email_address = :email_address_1 - ) AS anon_1 ON user.id = anon_1.user_id - - The above form allows one to fall back onto an explicit ON - clause at any time:: - - q = session.query(User).\\ - join(address_subq, User.id==address_subq.c.user_id) - - **Controlling what to Join From** - - While :meth:`~.Query.join` exclusively deals with the "right" - side of the JOIN, we can also control the "left" side, in those - cases where it's needed, using :meth:`~.Query.select_from`. - Below we construct a query against ``Address`` but can still - make usage of ``User.addresses`` as our ON clause by instructing - the :class:`.Query` to select first from the ``User`` - entity:: - - q = session.query(Address).select_from(User).\\ - join(User.addresses).\\ - filter(User.name == 'ed') - - Which will produce SQL similar to:: - - SELECT address.* FROM user - JOIN address ON user.id=address.user_id - WHERE user.name = :name_1 - - **Constructing Aliases Anonymously** - - :meth:`~.Query.join` can construct anonymous aliases - using the ``aliased=True`` flag. This feature is useful - when a query is being joined algorithmically, such as - when querying self-referentially to an arbitrary depth:: - - q = session.query(Node).\\ - join("children", "children", aliased=True) - - When ``aliased=True`` is used, the actual "alias" construct - is not explicitly available. To work with it, methods such as - :meth:`.Query.filter` will adapt the incoming entity to - the last join point:: - - q = session.query(Node).\\ - join("children", "children", aliased=True).\\ - filter(Node.name == 'grandchild 1') - - When using automatic aliasing, the ``from_joinpoint=True`` - argument can allow a multi-node join to be broken into - multiple calls to :meth:`~.Query.join`, so that - each path along the way can be further filtered:: - - q = session.query(Node).\\ - join("children", aliased=True).\\ - filter(Node.name='child 1').\\ - join("children", aliased=True, from_joinpoint=True).\\ - filter(Node.name == 'grandchild 1') - - The filtering aliases above can then be reset back to the - original ``Node`` entity using :meth:`~.Query.reset_joinpoint`:: - - q = session.query(Node).\\ - join("children", "children", aliased=True).\\ - filter(Node.name == 'grandchild 1').\\ - reset_joinpoint().\\ - filter(Node.name == 'parent 1) - - For an example of ``aliased=True``, see the distribution - example :ref:`examples_xmlpersistence` which illustrates - an XPath-like query system using algorithmic joins. - - :param \*props: A collection of one or more join conditions, - each consisting of a relationship-bound attribute or string - relationship name representing an "on clause", or a single - target entity, or a tuple in the form of ``(target, onclause)``. - A special two-argument calling form of the form ``target, onclause`` - is also accepted. - :param aliased=False: If True, indicate that the JOIN target should be - anonymously aliased. Subsequent calls to :meth:`~.Query.filter` - and similar will adapt the incoming criterion to the target - alias, until :meth:`~.Query.reset_joinpoint` is called. - :param from_joinpoint=False: When using ``aliased=True``, a setting - of True here will cause the join to be from the most recent - joined target, rather than starting back from the original - FROM clauses of the query. - - See also: - - :ref:`ormtutorial_joins` in the ORM tutorial. - - :ref:`inheritance_toplevel` for details on how :meth:`~.Query.join` - is used for inheritance relationships. - - :func:`.orm.join` - a standalone ORM-level join function, - used internally by :meth:`.Query.join`, which in previous - SQLAlchemy versions was the primary ORM-level joining interface. - - """ - aliased, from_joinpoint = kwargs.pop('aliased', False),\ - kwargs.pop('from_joinpoint', False) - if kwargs: - raise TypeError("unknown arguments: %s" % - ','.join(kwargs.keys)) - return self._join(props, - outerjoin=False, create_aliases=aliased, - from_joinpoint=from_joinpoint) - - def outerjoin(self, *props, **kwargs): - """Create a left outer join against this ``Query`` object's criterion - and apply generatively, returning the newly resulting ``Query``. - - Usage is the same as the ``join()`` method. - - """ - aliased, from_joinpoint = kwargs.pop('aliased', False), \ - kwargs.pop('from_joinpoint', False) - if kwargs: - raise TypeError("unknown arguments: %s" % - ','.join(kwargs)) - return self._join(props, - outerjoin=True, create_aliases=aliased, - from_joinpoint=from_joinpoint) - - def _update_joinpoint(self, jp): - self._joinpoint = jp - # copy backwards to the root of the _joinpath - # dict, so that no existing dict in the path is mutated - while 'prev' in jp: - f, prev = jp['prev'] - prev = prev.copy() - prev[f] = jp - jp['prev'] = (f, prev) - jp = prev - self._joinpath = jp - - @_generative(_no_statement_condition, _no_limit_offset) - def _join(self, keys, outerjoin, create_aliases, from_joinpoint): - """consumes arguments from join() or outerjoin(), places them into a - consistent format with which to form the actual JOIN constructs. - - """ - - if not from_joinpoint: - self._reset_joinpoint() - - if len(keys) == 2 and \ - isinstance(keys[0], (expression.FromClause, - type, AliasedClass)) and \ - isinstance(keys[1], (str, expression.ClauseElement, - interfaces.PropComparator)): - # detect 2-arg form of join and - # convert to a tuple. - keys = (keys,) - - for arg1 in util.to_list(keys): - if isinstance(arg1, tuple): - # "tuple" form of join, multiple - # tuples are accepted as well. The simpler - # "2-arg" form is preferred. May deprecate - # the "tuple" usage. - arg1, arg2 = arg1 - else: - arg2 = None - - # determine onclause/right_entity. there - # is a little bit of legacy behavior still at work here - # which means they might be in either order. may possibly - # lock this down to (right_entity, onclause) in 0.6. - if isinstance(arg1, (interfaces.PropComparator, util.string_types)): - right_entity, onclause = arg2, arg1 - else: - right_entity, onclause = arg1, arg2 - - left_entity = prop = None - - if isinstance(onclause, util.string_types): - left_entity = self._joinpoint_zero() - - descriptor = _entity_descriptor(left_entity, onclause) - onclause = descriptor - - # check for q.join(Class.propname, from_joinpoint=True) - # and Class is that of the current joinpoint - elif from_joinpoint and \ - isinstance(onclause, interfaces.PropComparator): - left_entity = onclause._parententity - - info = inspect(self._joinpoint_zero()) - left_mapper, left_selectable, left_is_aliased = \ - getattr(info, 'mapper', None), \ - info.selectable, \ - getattr(info, 'is_aliased_class', None) - - if left_mapper is left_entity: - left_entity = self._joinpoint_zero() - descriptor = _entity_descriptor(left_entity, - onclause.key) - onclause = descriptor - - if isinstance(onclause, interfaces.PropComparator): - if right_entity is None: - right_entity = onclause.property.mapper - of_type = getattr(onclause, '_of_type', None) - if of_type: - right_entity = of_type - else: - right_entity = onclause.property.mapper - - left_entity = onclause._parententity - - prop = onclause.property - if not isinstance(onclause, attributes.QueryableAttribute): - onclause = prop - - if not create_aliases: - # check for this path already present. - # don't render in that case. - edge = (left_entity, right_entity, prop.key) - if edge in self._joinpoint: - # The child's prev reference might be stale -- - # it could point to a parent older than the - # current joinpoint. If this is the case, - # then we need to update it and then fix the - # tree's spine with _update_joinpoint. Copy - # and then mutate the child, which might be - # shared by a different query object. - jp = self._joinpoint[edge].copy() - jp['prev'] = (edge, self._joinpoint) - self._update_joinpoint(jp) - continue - - elif onclause is not None and right_entity is None: - # TODO: no coverage here - raise NotImplementedError("query.join(a==b) not supported.") - - self._join_left_to_right( - left_entity, - right_entity, onclause, - outerjoin, create_aliases, prop) - - - def _join_left_to_right(self, left, right, - onclause, outerjoin, create_aliases, prop): - """append a JOIN to the query's from clause.""" - - self._polymorphic_adapters = self._polymorphic_adapters.copy() - - if left is None: - if self._from_obj: - left = self._from_obj[0] - elif self._entities: - left = self._entities[0].entity_zero_or_selectable - - if left is right and \ - not create_aliases: - raise sa_exc.InvalidRequestError( - "Can't construct a join from %s to %s, they " - "are the same entity" % - (left, right)) - - l_info = inspect(left) - r_info = inspect(right) - - - overlap = False - if not create_aliases: - right_mapper = getattr(r_info, "mapper", None) - # if the target is a joined inheritance mapping, - # be more liberal about auto-aliasing. - if right_mapper and ( - right_mapper.with_polymorphic or - isinstance(right_mapper.mapped_table, expression.Join) - ): - for from_obj in self._from_obj or [l_info.selectable]: - if sql_util.selectables_overlap(l_info.selectable, from_obj) and \ - sql_util.selectables_overlap(from_obj, r_info.selectable): - overlap = True - break - elif sql_util.selectables_overlap(l_info.selectable, r_info.selectable): - overlap = True - - - if overlap and l_info.selectable is r_info.selectable: - raise sa_exc.InvalidRequestError( - "Can't join table/selectable '%s' to itself" % - l_info.selectable) - - right, onclause = self._prepare_right_side( - r_info, right, onclause, - create_aliases, - prop, overlap) - - # if joining on a MapperProperty path, - # track the path to prevent redundant joins - if not create_aliases and prop: - self._update_joinpoint({ - '_joinpoint_entity': right, - 'prev': ((left, right, prop.key), self._joinpoint) - }) - else: - self._joinpoint = {'_joinpoint_entity': right} - - self._join_to_left(l_info, left, right, onclause, outerjoin) - - def _prepare_right_side(self, r_info, right, onclause, create_aliases, - prop, overlap): - info = r_info - - right_mapper, right_selectable, right_is_aliased = \ - getattr(info, 'mapper', None), \ - info.selectable, \ - getattr(info, 'is_aliased_class', False) - - if right_mapper: - self._join_entities += (info, ) - - if right_mapper and prop and \ - not right_mapper.common_parent(prop.mapper): - raise sa_exc.InvalidRequestError( - "Join target %s does not correspond to " - "the right side of join condition %s" % (right, onclause) - ) - - if not right_mapper and prop: - right_mapper = prop.mapper - - need_adapter = False - - if right_mapper and right is right_selectable: - if not right_selectable.is_derived_from( - right_mapper.mapped_table): - raise sa_exc.InvalidRequestError( - "Selectable '%s' is not derived from '%s'" % - (right_selectable.description, - right_mapper.mapped_table.description)) - - if isinstance(right_selectable, expression.SelectBase): - # TODO: this isn't even covered now! - right_selectable = right_selectable.alias() - need_adapter = True - - right = aliased(right_mapper, right_selectable) - - aliased_entity = right_mapper and \ - not right_is_aliased and \ - ( - right_mapper.with_polymorphic and isinstance( - right_mapper._with_polymorphic_selectable, - expression.Alias) - or - overlap # test for overlap: - # orm/inheritance/relationships.py - # SelfReferentialM2MTest - ) - - if not need_adapter and (create_aliases or aliased_entity): - right = aliased(right, flat=True) - need_adapter = True - - # if an alias() of the right side was generated here, - # apply an adapter to all subsequent filter() calls - # until reset_joinpoint() is called. - if need_adapter: - self._filter_aliases = ORMAdapter(right, - equivalents=right_mapper and - right_mapper._equivalent_columns or {}, - chain_to=self._filter_aliases) - - # if the onclause is a ClauseElement, adapt it with any - # adapters that are in place right now - if isinstance(onclause, expression.ClauseElement): - onclause = self._adapt_clause(onclause, True, True) - - # if an alias() on the right side was generated, - # which is intended to wrap a the right side in a subquery, - # ensure that columns retrieved from this target in the result - # set are also adapted. - if aliased_entity and not create_aliases: - self._mapper_loads_polymorphically_with( - right_mapper, - ORMAdapter( - right, - equivalents=right_mapper._equivalent_columns - ) - ) - - return right, onclause - - def _join_to_left(self, l_info, left, right, onclause, outerjoin): - info = l_info - left_mapper = getattr(info, 'mapper', None) - left_selectable = info.selectable - - if self._from_obj: - replace_clause_index, clause = sql_util.find_join_source( - self._from_obj, - left_selectable) - if clause is not None: - try: - clause = orm_join(clause, - right, - onclause, isouter=outerjoin) - except sa_exc.ArgumentError as ae: - raise sa_exc.InvalidRequestError( - "Could not find a FROM clause to join from. " - "Tried joining to %s, but got: %s" % (right, ae)) - - self._from_obj = \ - self._from_obj[:replace_clause_index] + \ - (clause, ) + \ - self._from_obj[replace_clause_index + 1:] - return - - if left_mapper: - for ent in self._entities: - if ent.corresponds_to(left): - clause = ent.selectable - break - else: - clause = left - else: - clause = left_selectable - - assert clause is not None - try: - clause = orm_join(clause, right, onclause, isouter=outerjoin) - except sa_exc.ArgumentError as ae: - raise sa_exc.InvalidRequestError( - "Could not find a FROM clause to join from. " - "Tried joining to %s, but got: %s" % (right, ae)) - self._from_obj = self._from_obj + (clause,) - - def _reset_joinpoint(self): - self._joinpoint = self._joinpath - self._filter_aliases = None - - @_generative(_no_statement_condition) - def reset_joinpoint(self): - """Return a new :class:`.Query`, where the "join point" has - been reset back to the base FROM entities of the query. - - This method is usually used in conjunction with the - ``aliased=True`` feature of the :meth:`~.Query.join` - method. See the example in :meth:`~.Query.join` for how - this is used. - - """ - self._reset_joinpoint() - - @_generative(_no_clauseelement_condition) - def select_from(self, *from_obj): - """Set the FROM clause of this :class:`.Query` explicitly. - - :meth:`.Query.select_from` is often used in conjunction with - :meth:`.Query.join` in order to control which entity is selected - from on the "left" side of the join. - - The entity or selectable object here effectively replaces the - "left edge" of any calls to :meth:`~.Query.join`, when no - joinpoint is otherwise established - usually, the default "join - point" is the leftmost entity in the :class:`~.Query` object's - list of entities to be selected. - - A typical example:: - - q = session.query(Address).select_from(User).\\ - join(User.addresses).\\ - filter(User.name == 'ed') - - Which produces SQL equivalent to:: - - SELECT address.* FROM user - JOIN address ON user.id=address.user_id - WHERE user.name = :name_1 - - :param \*from_obj: collection of one or more entities to apply - to the FROM clause. Entities can be mapped classes, - :class:`.AliasedClass` objects, :class:`.Mapper` objects - as well as core :class:`.FromClause` elements like subqueries. - - .. versionchanged:: 0.9 - This method no longer applies the given FROM object - to be the selectable from which matching entities - select from; the :meth:`.select_entity_from` method - now accomplishes this. See that method for a description - of this behavior. - - .. seealso:: - - :meth:`~.Query.join` - - :meth:`.Query.select_entity_from` - - """ - - self._set_select_from(from_obj, False) - - @_generative(_no_clauseelement_condition) - def select_entity_from(self, from_obj): - """Set the FROM clause of this :class:`.Query` to a - core selectable, applying it as a replacement FROM clause - for corresponding mapped entities. - - This method is similar to the :meth:`.Query.select_from` - method, in that it sets the FROM clause of the query. However, - where :meth:`.Query.select_from` only affects what is placed - in the FROM, this method also applies the given selectable - to replace the FROM which the selected entities would normally - select from. - - The given ``from_obj`` must be an instance of a :class:`.FromClause`, - e.g. a :func:`.select` or :class:`.Alias` construct. - - An example would be a :class:`.Query` that selects ``User`` entities, - but uses :meth:`.Query.select_entity_from` to have the entities - selected from a :func:`.select` construct instead of the - base ``user`` table:: - - select_stmt = select([User]).where(User.id == 7) - - q = session.query(User).\\ - select_entity_from(select_stmt).\\ - filter(User.name == 'ed') - - The query generated will select ``User`` entities directly - from the given :func:`.select` construct, and will be:: - - SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name - FROM (SELECT "user".id AS id, "user".name AS name - FROM "user" - WHERE "user".id = :id_1) AS anon_1 - WHERE anon_1.name = :name_1 - - Notice above that even the WHERE criterion was "adapted" such that - the ``anon_1`` subquery effectively replaces all references to the - ``user`` table, except for the one that it refers to internally. - - Compare this to :meth:`.Query.select_from`, which as of - version 0.9, does not affect existing entities. The - statement below:: - - q = session.query(User).\\ - select_from(select_stmt).\\ - filter(User.name == 'ed') - - Produces SQL where both the ``user`` table as well as the - ``select_stmt`` construct are present as separate elements - in the FROM clause. No "adaptation" of the ``user`` table - is applied:: - - SELECT "user".id AS user_id, "user".name AS user_name - FROM "user", (SELECT "user".id AS id, "user".name AS name - FROM "user" - WHERE "user".id = :id_1) AS anon_1 - WHERE "user".name = :name_1 - - :meth:`.Query.select_entity_from` maintains an older - behavior of :meth:`.Query.select_from`. In modern usage, - similar results can also be achieved using :func:`.aliased`:: - - select_stmt = select([User]).where(User.id == 7) - user_from_select = aliased(User, select_stmt.alias()) - - q = session.query(user_from_select) - - :param from_obj: a :class:`.FromClause` object that will replace - the FROM clause of this :class:`.Query`. - - .. seealso:: - - :meth:`.Query.select_from` - - .. versionadded:: 0.8 - :meth:`.Query.select_entity_from` was added to specify - the specific behavior of entity replacement, however - the :meth:`.Query.select_from` maintains this behavior - as well until 0.9. - - """ - - self._set_select_from([from_obj], True) - - def __getitem__(self, item): - if isinstance(item, slice): - start, stop, step = util.decode_slice(item) - - if isinstance(stop, int) and \ - isinstance(start, int) and \ - stop - start <= 0: - return [] - - # perhaps we should execute a count() here so that we - # can still use LIMIT/OFFSET ? - elif (isinstance(start, int) and start < 0) \ - or (isinstance(stop, int) and stop < 0): - return list(self)[item] - - res = self.slice(start, stop) - if step is not None: - return list(res)[None:None:item.step] - else: - return list(res) - else: - if item == -1: - return list(self)[-1] - else: - return list(self[item:item + 1])[0] - - @_generative(_no_statement_condition) - def slice(self, start, stop): - """apply LIMIT/OFFSET to the ``Query`` based on a " - "range and return the newly resulting ``Query``.""" - - if start is not None and stop is not None: - self._offset = (self._offset or 0) + start - self._limit = stop - start - elif start is None and stop is not None: - self._limit = stop - elif start is not None and stop is None: - self._offset = (self._offset or 0) + start - - if self._offset == 0: - self._offset = None - - @_generative(_no_statement_condition) - def limit(self, limit): - """Apply a ``LIMIT`` to the query and return the newly resulting - - ``Query``. - - """ - self._limit = limit - - @_generative(_no_statement_condition) - def offset(self, offset): - """Apply an ``OFFSET`` to the query and return the newly resulting - ``Query``. - - """ - self._offset = offset - - @_generative(_no_statement_condition) - def distinct(self, *criterion): - """Apply a ``DISTINCT`` to the query and return the newly resulting - ``Query``. - - :param \*expr: optional column expressions. When present, - the Postgresql dialect will render a ``DISTINCT ON (>)`` - construct. - - """ - if not criterion: - self._distinct = True - else: - criterion = self._adapt_col_list(criterion) - if isinstance(self._distinct, list): - self._distinct += criterion - else: - self._distinct = criterion - - @_generative() - def prefix_with(self, *prefixes): - """Apply the prefixes to the query and return the newly resulting - ``Query``. - - :param \*prefixes: optional prefixes, typically strings, - not using any commas. In particular is useful for MySQL keywords. - - e.g.:: - - query = sess.query(User.name).\\ - prefix_with('HIGH_PRIORITY').\\ - prefix_with('SQL_SMALL_RESULT', 'ALL') - - Would render:: - - SELECT HIGH_PRIORITY SQL_SMALL_RESULT ALL users.name AS users_name - FROM users - - .. versionadded:: 0.7.7 - - """ - if self._prefixes: - self._prefixes += prefixes - else: - self._prefixes = prefixes - - def all(self): - """Return the results represented by this ``Query`` as a list. - - This results in an execution of the underlying query. - - """ - return list(self) - - @_generative(_no_clauseelement_condition) - def from_statement(self, statement): - """Execute the given SELECT statement and return results. - - This method bypasses all internal statement compilation, and the - statement is executed without modification. - - The statement argument is either a string, a ``select()`` construct, - or a ``text()`` construct, and should return the set of columns - appropriate to the entity class represented by this ``Query``. - - """ - if isinstance(statement, util.string_types): - statement = sql.text(statement) - - if not isinstance(statement, - (expression.TextClause, - expression.SelectBase)): - raise sa_exc.ArgumentError( - "from_statement accepts text(), select(), " - "and union() objects only.") - - self._statement = statement - - def first(self): - """Return the first result of this ``Query`` or - None if the result doesn't contain any row. - - first() applies a limit of one within the generated SQL, so that - only one primary entity row is generated on the server side - (note this may consist of multiple result rows if join-loaded - collections are present). - - Calling ``first()`` results in an execution of the underlying query. - - """ - if self._statement is not None: - ret = list(self)[0:1] - else: - ret = list(self[0:1]) - if len(ret) > 0: - return ret[0] - else: - return None - - def one(self): - """Return exactly one result or raise an exception. - - Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query selects - no rows. Raises ``sqlalchemy.orm.exc.MultipleResultsFound`` - if multiple object identities are returned, or if multiple - rows are returned for a query that does not return object - identities. - - Note that an entity query, that is, one which selects one or - more mapped classes as opposed to individual column attributes, - may ultimately represent many rows but only one row of - unique entity or entities - this is a successful result for one(). - - Calling ``one()`` results in an execution of the underlying query. - - .. versionchanged:: 0.6 - ``one()`` fully fetches all results instead of applying - any kind of limit, so that the "unique"-ing of entities does not - conceal multiple object identities. - - """ - ret = list(self) - - l = len(ret) - if l == 1: - return ret[0] - elif l == 0: - raise orm_exc.NoResultFound("No row was found for one()") - else: - raise orm_exc.MultipleResultsFound( - "Multiple rows were found for one()") - - def scalar(self): - """Return the first element of the first result or None - if no rows present. If multiple rows are returned, - raises MultipleResultsFound. - - >>> session.query(Item).scalar() - - >>> session.query(Item.id).scalar() - 1 - >>> session.query(Item.id).filter(Item.id < 0).scalar() - None - >>> session.query(Item.id, Item.name).scalar() - 1 - >>> session.query(func.count(Parent.id)).scalar() - 20 - - This results in an execution of the underlying query. - - """ - try: - ret = self.one() - if not isinstance(ret, tuple): - return ret - return ret[0] - except orm_exc.NoResultFound: - return None - - def __iter__(self): - context = self._compile_context() - context.statement.use_labels = True - if self._autoflush and not self._populate_existing: - self.session._autoflush() - return self._execute_and_instances(context) - - def _connection_from_session(self, **kw): - conn = self.session.connection( - **kw) - if self._execution_options: - conn = conn.execution_options(**self._execution_options) - return conn - - def _execute_and_instances(self, querycontext): - conn = self._connection_from_session( - mapper=self._mapper_zero_or_none(), - clause=querycontext.statement, - close_with_result=True) - - result = conn.execute(querycontext.statement, self._params) - return loading.instances(self, result, querycontext) - - @property - def column_descriptions(self): - """Return metadata about the columns which would be - returned by this :class:`.Query`. - - Format is a list of dictionaries:: - - user_alias = aliased(User, name='user2') - q = sess.query(User, User.id, user_alias) - - # this expression: - q.column_descriptions - - # would return: - [ - { - 'name':'User', - 'type':User, - 'aliased':False, - 'expr':User, - }, - { - 'name':'id', - 'type':Integer(), - 'aliased':False, - 'expr':User.id, - }, - { - 'name':'user2', - 'type':User, - 'aliased':True, - 'expr':user_alias - } - ] - - """ - return [ - { - 'name': ent._label_name, - 'type': ent.type, - 'aliased': getattr(ent, 'is_aliased_class', False), - 'expr': ent.expr - } - for ent in self._entities - ] - - def instances(self, cursor, __context=None): - """Given a ResultProxy cursor as returned by connection.execute(), - return an ORM result as an iterator. - - e.g.:: - - result = engine.execute("select * from users") - for u in session.query(User).instances(result): - print u - """ - context = __context - if context is None: - context = QueryContext(self) - - return loading.instances(self, cursor, context) - - def merge_result(self, iterator, load=True): - """Merge a result into this :class:`.Query` object's Session. - - Given an iterator returned by a :class:`.Query` of the same structure - as this one, return an identical iterator of results, with all mapped - instances merged into the session using :meth:`.Session.merge`. This - is an optimized method which will merge all mapped instances, - preserving the structure of the result rows and unmapped columns with - less method overhead than that of calling :meth:`.Session.merge` - explicitly for each value. - - The structure of the results is determined based on the column list of - this :class:`.Query` - if these do not correspond, unchecked errors - will occur. - - The 'load' argument is the same as that of :meth:`.Session.merge`. - - For an example of how :meth:`~.Query.merge_result` is used, see - the source code for the example :ref:`examples_caching`, where - :meth:`~.Query.merge_result` is used to efficiently restore state - from a cache back into a target :class:`.Session`. - - """ - - return loading.merge_result(self, iterator, load) - - @property - def _select_args(self): - return { - 'limit': self._limit, - 'offset': self._offset, - 'distinct': self._distinct, - 'prefixes': self._prefixes, - 'group_by': self._group_by or None, - 'having': self._having - } - - @property - def _should_nest_selectable(self): - kwargs = self._select_args - return (kwargs.get('limit') is not None or - kwargs.get('offset') is not None or - kwargs.get('distinct', False)) - - def exists(self): - """A convenience method that turns a query into an EXISTS subquery - of the form EXISTS (SELECT 1 FROM ... WHERE ...). - - e.g.:: - - q = session.query(User).filter(User.name == 'fred') - session.query(q.exists()) - - Producing SQL similar to:: - - SELECT EXISTS ( - SELECT 1 FROM users WHERE users.name = :name_1 - ) AS anon_1 - - .. versionadded:: 0.8.1 - - """ - return sql.exists(self.with_labels().statement.with_only_columns(['1'])) - - def count(self): - """Return a count of rows this Query would return. - - This generates the SQL for this Query as follows:: - - SELECT count(1) AS count_1 FROM ( - SELECT - ) AS anon_1 - - .. versionchanged:: 0.7 - The above scheme is newly refined as of 0.7b3. - - For fine grained control over specific columns - to count, to skip the usage of a subquery or - otherwise control of the FROM clause, - or to use other aggregate functions, - use :attr:`~sqlalchemy.sql.expression.func` - expressions in conjunction - with :meth:`~.Session.query`, i.e.:: - - from sqlalchemy import func - - # count User records, without - # using a subquery. - session.query(func.count(User.id)) - - # return count of user "id" grouped - # by "name" - session.query(func.count(User.id)).\\ - group_by(User.name) - - from sqlalchemy import distinct - - # count distinct "name" values - session.query(func.count(distinct(User.name))) - - """ - col = sql.func.count(sql.literal_column('*')) - return self.from_self(col).scalar() - - def delete(self, synchronize_session='evaluate'): - """Perform a bulk delete query. - - Deletes rows matched by this query from the database. - - :param synchronize_session: chooses the strategy for the removal of - matched objects from the session. Valid values are: - - ``False`` - don't synchronize the session. This option is the most - efficient and is reliable once the session is expired, which - typically occurs after a commit(), or explicitly using - expire_all(). Before the expiration, objects may still remain in - the session which were in fact deleted which can lead to confusing - results if they are accessed via get() or already loaded - collections. - - ``'fetch'`` - performs a select query before the delete to find - objects that are matched by the delete query and need to be - removed from the session. Matched objects are removed from the - session. - - ``'evaluate'`` - Evaluate the query's criteria in Python straight - on the objects in the session. If evaluation of the criteria isn't - implemented, an error is raised. In that case you probably - want to use the 'fetch' strategy as a fallback. - - The expression evaluator currently doesn't account for differing - string collations between the database and Python. - - :return: the count of rows matched as returned by the database's - "row count" feature. - - This method has several key caveats: - - * The method does **not** offer in-Python cascading of relationships - it - is assumed that ON DELETE CASCADE/SET NULL/etc. is configured for any foreign key - references which require it, otherwise the database may emit an - integrity violation if foreign key references are being enforced. - - After the DELETE, dependent objects in the :class:`.Session` which - were impacted by an ON DELETE may not contain the current - state, or may have been deleted. This issue is resolved once the - :class:`.Session` is expired, - which normally occurs upon :meth:`.Session.commit` or can be forced - by using :meth:`.Session.expire_all`. Accessing an expired object - whose row has been deleted will invoke a SELECT to locate the - row; when the row is not found, an :class:`~sqlalchemy.orm.exc.ObjectDeletedError` - is raised. - - * The :meth:`.MapperEvents.before_delete` and - :meth:`.MapperEvents.after_delete` - events are **not** invoked from this method. Instead, the - :meth:`.SessionEvents.after_bulk_delete` method is provided to act - upon a mass DELETE of entity rows. - - .. seealso:: - - :meth:`.Query.update` - - :ref:`inserts_and_updates` - Core SQL tutorial - - """ - #TODO: cascades need handling. - - delete_op = persistence.BulkDelete.factory( - self, synchronize_session) - delete_op.exec_() - return delete_op.rowcount - - def update(self, values, synchronize_session='evaluate'): - """Perform a bulk update query. - - Updates rows matched by this query in the database. - - :param values: a dictionary with attributes names as keys and literal - values or sql expressions as values. - - :param synchronize_session: chooses the strategy to update the - attributes on objects in the session. Valid values are: - - ``False`` - don't synchronize the session. This option is the most - efficient and is reliable once the session is expired, which - typically occurs after a commit(), or explicitly using - expire_all(). Before the expiration, updated objects may still - remain in the session with stale values on their attributes, which - can lead to confusing results. - - ``'fetch'`` - performs a select query before the update to find - objects that are matched by the update query. The updated - attributes are expired on matched objects. - - ``'evaluate'`` - Evaluate the Query's criteria in Python straight - on the objects in the session. If evaluation of the criteria isn't - implemented, an exception is raised. - - The expression evaluator currently doesn't account for differing - string collations between the database and Python. - - :return: the count of rows matched as returned by the database's - "row count" feature. - - This method has several key caveats: - - * The method does **not** offer in-Python cascading of relationships - it - is assumed that ON UPDATE CASCADE is configured for any foreign key - references which require it, otherwise the database may emit an - integrity violation if foreign key references are being enforced. - - After the UPDATE, dependent objects in the :class:`.Session` which - were impacted by an ON UPDATE CASCADE may not contain the current - state; this issue is resolved once the :class:`.Session` is expired, - which normally occurs upon :meth:`.Session.commit` or can be forced - by using :meth:`.Session.expire_all`. - - * As of 0.8, this method will support multiple table updates, as detailed - in :ref:`multi_table_updates`, and this behavior does extend to support - updates of joined-inheritance and other multiple table mappings. However, - the **join condition of an inheritance mapper is currently not - automatically rendered**. - Care must be taken in any multiple-table update to explicitly include - the joining condition between those tables, even in mappings where - this is normally automatic. - E.g. if a class ``Engineer`` subclasses ``Employee``, an UPDATE of the - ``Engineer`` local table using criteria against the ``Employee`` - local table might look like:: - - session.query(Engineer).\\ - filter(Engineer.id == Employee.id).\\ - filter(Employee.name == 'dilbert').\\ - update({"engineer_type": "programmer"}) - - * The :meth:`.MapperEvents.before_update` and - :meth:`.MapperEvents.after_update` - events are **not** invoked from this method. Instead, the - :meth:`.SessionEvents.after_bulk_update` method is provided to act - upon a mass UPDATE of entity rows. - - .. seealso:: - - :meth:`.Query.delete` - - :ref:`inserts_and_updates` - Core SQL tutorial - - """ - - #TODO: value keys need to be mapped to corresponding sql cols and - # instr.attr.s to string keys - #TODO: updates of manytoone relationships need to be converted to - # fk assignments - #TODO: cascades need handling. - - update_op = persistence.BulkUpdate.factory( - self, synchronize_session, values) - update_op.exec_() - return update_op.rowcount - - - def _compile_context(self, labels=True): - context = QueryContext(self) - - if context.statement is not None: - return context - - context.labels = labels - - context._for_update_arg = self._for_update_arg - - for entity in self._entities: - entity.setup_context(self, context) - - for rec in context.create_eager_joins: - strategy = rec[0] - strategy(*rec[1:]) - - if context.from_clause: - # "load from explicit FROMs" mode, - # i.e. when select_from() or join() is used - context.froms = list(context.from_clause) - # this would fix... - #import pdb - #pdb.set_trace() - #context.froms += tuple(context.from_clause) - - else: - # "load from discrete FROMs" mode, - # i.e. when each _MappedEntity has its own FROM - context.froms = context.froms - - if self._enable_single_crit: - self._adjust_for_single_inheritance(context) - - if not context.primary_columns: - if self._only_load_props: - raise sa_exc.InvalidRequestError( - "No column-based properties specified for " - "refresh operation. Use session.expire() " - "to reload collections and related items.") - else: - raise sa_exc.InvalidRequestError( - "Query contains no columns with which to " - "SELECT from.") - - if context.multi_row_eager_loaders and self._should_nest_selectable: - context.statement = self._compound_eager_statement(context) - else: - context.statement = self._simple_statement(context) - return context - - def _compound_eager_statement(self, context): - # for eager joins present and LIMIT/OFFSET/DISTINCT, - # wrap the query inside a select, - # then append eager joins onto that - - if context.order_by: - order_by_col_expr = list( - chain(*[ - sql_util.unwrap_order_by(o) - for o in context.order_by - ]) - ) - else: - context.order_by = None - order_by_col_expr = [] - - inner = sql.select( - context.primary_columns + order_by_col_expr, - context.whereclause, - from_obj=context.froms, - use_labels=context.labels, - # TODO: this order_by is only needed if - # LIMIT/OFFSET is present in self._select_args, - # else the application on the outside is enough - order_by=context.order_by, - **self._select_args - ) - - for hint in self._with_hints: - inner = inner.with_hint(*hint) - - if self._correlate: - inner = inner.correlate(*self._correlate) - - inner = inner.alias() - - equivs = self.__all_equivs() - - context.adapter = sql_util.ColumnAdapter(inner, equivs) - - statement = sql.select( - [inner] + context.secondary_columns, - use_labels=context.labels) - - statement._for_update_arg = context._for_update_arg - - from_clause = inner - for eager_join in context.eager_joins.values(): - # EagerLoader places a 'stop_on' attribute on the join, - # giving us a marker as to where the "splice point" of - # the join should be - from_clause = sql_util.splice_joins( - from_clause, - eager_join, eager_join.stop_on) - - statement.append_from(from_clause) - - if context.order_by: - statement.append_order_by( - *context.adapter.copy_and_process( - context.order_by - ) - ) - - statement.append_order_by(*context.eager_order_by) - return statement - - def _simple_statement(self, context): - if not context.order_by: - context.order_by = None - - if self._distinct and context.order_by: - order_by_col_expr = list( - chain(*[ - sql_util.unwrap_order_by(o) - for o in context.order_by - ]) - ) - context.primary_columns += order_by_col_expr - - context.froms += tuple(context.eager_joins.values()) - - statement = sql.select( - context.primary_columns + - context.secondary_columns, - context.whereclause, - from_obj=context.froms, - use_labels=context.labels, - order_by=context.order_by, - **self._select_args - ) - - statement._for_update_arg = context._for_update_arg - - for hint in self._with_hints: - statement = statement.with_hint(*hint) - - if self._correlate: - statement = statement.correlate(*self._correlate) - - if context.eager_order_by: - statement.append_order_by(*context.eager_order_by) - return statement - - def _adjust_for_single_inheritance(self, context): - """Apply single-table-inheritance filtering. - - For all distinct single-table-inheritance mappers represented in - the columns clause of this query, add criterion to the WHERE - clause of the given QueryContext such that only the appropriate - subtypes are selected from the total results. - - """ - for (ext_info, adapter) in self._mapper_adapter_map.values(): - if ext_info in self._join_entities: - continue - single_crit = ext_info.mapper._single_table_criterion - if single_crit is not None: - if adapter: - single_crit = adapter.traverse(single_crit) - single_crit = self._adapt_clause(single_crit, False, False) - context.whereclause = sql.and_( - sql.True_._ifnone(context.whereclause), - single_crit) - - def __str__(self): - return str(self._compile_context().statement) - -from ..sql.selectable import ForUpdateArg - -class LockmodeArg(ForUpdateArg): - @classmethod - def parse_legacy_query(self, mode): - if mode in (None, False): - return None - - if mode == "read": - read = True - nowait = False - elif mode == "update": - read = nowait = False - elif mode == "update_nowait": - nowait = True - read = False - else: - raise sa_exc.ArgumentError( - "Unknown with_lockmode argument: %r" % mode) - - return LockmodeArg(read=read, nowait=nowait) - -class _QueryEntity(object): - """represent an entity column returned within a Query result.""" - - def __new__(cls, *args, **kwargs): - if cls is _QueryEntity: - entity = args[1] - if not isinstance(entity, util.string_types) and \ - _is_mapped_class(entity): - cls = _MapperEntity - elif isinstance(entity, Bundle): - cls = _BundleEntity - else: - cls = _ColumnEntity - return object.__new__(cls) - - def _clone(self): - q = self.__class__.__new__(self.__class__) - q.__dict__ = self.__dict__.copy() - return q - - -class _MapperEntity(_QueryEntity): - """mapper/class/AliasedClass entity""" - - def __init__(self, query, entity): - if not query._primary_entity: - query._primary_entity = self - query._entities.append(self) - - self.entities = [entity] - self.expr = entity - - supports_single_entity = True - - def setup_entity(self, ext_info, aliased_adapter): - self.mapper = ext_info.mapper - self.aliased_adapter = aliased_adapter - self.selectable = ext_info.selectable - self.is_aliased_class = ext_info.is_aliased_class - self._with_polymorphic = ext_info.with_polymorphic_mappers - self._polymorphic_discriminator = \ - ext_info.polymorphic_on - self.entity_zero = ext_info - if ext_info.is_aliased_class: - self._label_name = self.entity_zero.name - else: - self._label_name = self.mapper.class_.__name__ - self.path = self.entity_zero._path_registry - self.custom_rows = bool(self.mapper.dispatch.append_result) - - def set_with_polymorphic(self, query, cls_or_mappers, - selectable, polymorphic_on): - """Receive an update from a call to query.with_polymorphic(). - - Note the newer style of using a free standing with_polymporphic() - construct doesn't make use of this method. - - - """ - if self.is_aliased_class: - # TODO: invalidrequest ? - raise NotImplementedError( - "Can't use with_polymorphic() against " - "an Aliased object" - ) - - if cls_or_mappers is None: - query._reset_polymorphic_adapter(self.mapper) - return - - mappers, from_obj = self.mapper._with_polymorphic_args( - cls_or_mappers, selectable) - self._with_polymorphic = mappers - self._polymorphic_discriminator = polymorphic_on - - self.selectable = from_obj - query._mapper_loads_polymorphically_with(self.mapper, - sql_util.ColumnAdapter(from_obj, - self.mapper._equivalent_columns)) - - filter_fn = id - - @property - def type(self): - return self.mapper.class_ - - @property - def entity_zero_or_selectable(self): - return self.entity_zero - - def corresponds_to(self, entity): - if entity.is_aliased_class: - if self.is_aliased_class: - if entity._base_alias is self.entity_zero._base_alias: - return True - return False - elif self.is_aliased_class: - if self.entity_zero._use_mapper_path: - return entity in self._with_polymorphic - else: - return entity is self.entity_zero - - return entity.common_parent(self.entity_zero) - - def adapt_to_selectable(self, query, sel): - query._entities.append(self) - - def _get_entity_clauses(self, query, context): - - adapter = None - - if not self.is_aliased_class: - if query._polymorphic_adapters: - adapter = query._polymorphic_adapters.get(self.mapper, None) - else: - adapter = self.aliased_adapter - - if adapter: - if query._from_obj_alias: - ret = adapter.wrap(query._from_obj_alias) - else: - ret = adapter - else: - ret = query._from_obj_alias - - return ret - - def row_processor(self, query, context, custom_rows): - adapter = self._get_entity_clauses(query, context) - - if context.adapter and adapter: - adapter = adapter.wrap(context.adapter) - elif not adapter: - adapter = context.adapter - - # polymorphic mappers which have concrete tables in - # their hierarchy usually - # require row aliasing unconditionally. - if not adapter and self.mapper._requires_row_aliasing: - adapter = sql_util.ColumnAdapter( - self.selectable, - self.mapper._equivalent_columns) - - if query._primary_entity is self: - _instance = loading.instance_processor( - self.mapper, - context, - self.path, - adapter, - only_load_props=query._only_load_props, - refresh_state=context.refresh_state, - polymorphic_discriminator=self._polymorphic_discriminator - ) - else: - _instance = loading.instance_processor( - self.mapper, - context, - self.path, - adapter, - polymorphic_discriminator=self._polymorphic_discriminator - ) - - return _instance, self._label_name - - def setup_context(self, query, context): - adapter = self._get_entity_clauses(query, context) - - #if self._adapted_selectable is None: - context.froms += (self.selectable,) - - if context.order_by is False and self.mapper.order_by: - context.order_by = self.mapper.order_by - - # apply adaptation to the mapper's order_by if needed. - if adapter: - context.order_by = adapter.adapt_list( - util.to_list( - context.order_by - ) - ) - - if self._with_polymorphic: - poly_properties = self.mapper._iterate_polymorphic_properties( - self._with_polymorphic) - else: - poly_properties = self.mapper._polymorphic_properties - - for value in poly_properties: - if query._only_load_props and \ - value.key not in query._only_load_props: - continue - value.setup( - context, - self, - self.path, - adapter, - only_load_props=query._only_load_props, - column_collection=context.primary_columns - ) - - if self._polymorphic_discriminator is not None and \ - self._polymorphic_discriminator \ - is not self.mapper.polymorphic_on: - - if adapter: - pd = adapter.columns[self._polymorphic_discriminator] - else: - pd = self._polymorphic_discriminator - context.primary_columns.append(pd) - - def __str__(self): - return str(self.mapper) - -@inspection._self_inspects -class Bundle(object): - """A grouping of SQL expressions that are returned by a :class:`.Query` - under one namespace. - - The :class:`.Bundle` essentially allows nesting of the tuple-based - results returned by a column-oriented :class:`.Query` object. It also - is extensible via simple subclassing, where the primary capability - to override is that of how the set of expressions should be returned, - allowing post-processing as well as custom return types, without - involving ORM identity-mapped classes. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :ref:`bundles` - - """ - - single_entity = False - """If True, queries for a single Bundle will be returned as a single - entity, rather than an element within a keyed tuple.""" - - def __init__(self, name, *exprs, **kw): - """Construct a new :class:`.Bundle`. - - e.g.:: - - bn = Bundle("mybundle", MyClass.x, MyClass.y) - - for row in session.query(bn).filter(bn.c.x == 5).filter(bn.c.y == 4): - print(row.mybundle.x, row.mybundle.y) - - :param name: name of the bundle. - :param \*exprs: columns or SQL expressions comprising the bundle. - :param single_entity=False: if True, rows for this :class:`.Bundle` - can be returned as a "single entity" outside of any enclosing tuple - in the same manner as a mapped entity. - - """ - self.name = self._label = name - self.exprs = exprs - self.c = self.columns = ColumnCollection() - self.columns.update((getattr(col, "key", col._label), col) - for col in exprs) - self.single_entity = kw.pop('single_entity', self.single_entity) - - columns = None - """A namespace of SQL expressions referred to by this :class:`.Bundle`. - - e.g.:: - - bn = Bundle("mybundle", MyClass.x, MyClass.y) - - q = sess.query(bn).filter(bn.c.x == 5) - - Nesting of bundles is also supported:: - - b1 = Bundle("b1", - Bundle('b2', MyClass.a, MyClass.b), - Bundle('b3', MyClass.x, MyClass.y) - ) - - q = sess.query(b1).filter(b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9) - - .. seealso:: - - :attr:`.Bundle.c` - - """ - - c = None - """An alias for :attr:`.Bundle.columns`.""" - - def _clone(self): - cloned = self.__class__.__new__(self.__class__) - cloned.__dict__.update(self.__dict__) - return cloned - - def __clause_element__(self): - return expression.ClauseList(group=False, *self.c) - - @property - def clauses(self): - return self.__clause_element__().clauses - - def label(self, name): - """Provide a copy of this :class:`.Bundle` passing a new label.""" - - cloned = self._clone() - cloned.name = name - return cloned - - def create_row_processor(self, query, procs, labels): - """Produce the "row processing" function for this :class:`.Bundle`. - - May be overridden by subclasses. - - .. seealso:: - - :ref:`bundles` - includes an example of subclassing. - - """ - def proc(row, result): - return util.KeyedTuple([proc(row, None) for proc in procs], labels) - return proc - - -class _BundleEntity(_QueryEntity): - def __init__(self, query, bundle, setup_entities=True): - query._entities.append(self) - self.bundle = self.expr = bundle - self.type = type(bundle) - self._label_name = bundle.name - self._entities = [] - - if setup_entities: - for expr in bundle.exprs: - if isinstance(expr, Bundle): - _BundleEntity(self, expr) - else: - _ColumnEntity(self, expr, namespace=self) - - self.entities = () - - self.filter_fn = lambda item: item - - self.supports_single_entity = self.bundle.single_entity - - custom_rows = False - - @property - def entity_zero(self): - for ent in self._entities: - ezero = ent.entity_zero - if ezero is not None: - return ezero - else: - return None - - def corresponds_to(self, entity): - # TODO: this seems to have no effect for - # _ColumnEntity either - return False - - @property - def entity_zero_or_selectable(self): - for ent in self._entities: - ezero = ent.entity_zero_or_selectable - if ezero is not None: - return ezero - else: - return None - - def adapt_to_selectable(self, query, sel): - c = _BundleEntity(query, self.bundle, setup_entities=False) - #c._label_name = self._label_name - #c.entity_zero = self.entity_zero - #c.entities = self.entities - - for ent in self._entities: - ent.adapt_to_selectable(c, sel) - - def setup_entity(self, ext_info, aliased_adapter): - for ent in self._entities: - ent.setup_entity(ext_info, aliased_adapter) - - def setup_context(self, query, context): - for ent in self._entities: - ent.setup_context(query, context) - - def row_processor(self, query, context, custom_rows): - procs, labels = zip( - *[ent.row_processor(query, context, custom_rows) - for ent in self._entities] - ) - - proc = self.bundle.create_row_processor(query, procs, labels) - - return proc, self._label_name - -class _ColumnEntity(_QueryEntity): - """Column/expression based entity.""" - - def __init__(self, query, column, namespace=None): - self.expr = column - self.namespace = namespace - - if isinstance(column, util.string_types): - column = sql.literal_column(column) - self._label_name = column.name - elif isinstance(column, ( - attributes.QueryableAttribute, - interfaces.PropComparator - )): - self._label_name = column.key - column = column._query_clause_element() - else: - self._label_name = getattr(column, 'key', None) - - if not isinstance(column, expression.ColumnElement) and \ - hasattr(column, '_select_iterable'): - for c in column._select_iterable: - if c is column: - break - _ColumnEntity(query, c, namespace=column) - - if c is not column: - return - elif isinstance(column, Bundle): - _BundleEntity(query, column) - return - - if not isinstance(column, sql.ColumnElement): - raise sa_exc.InvalidRequestError( - "SQL expression, column, or mapped entity " - "expected - got '%r'" % (column, ) - ) - - self.type = type_ = column.type - if type_.hashable: - self.filter_fn = lambda item: item - else: - counter = util.counter() - self.filter_fn = lambda item: counter() - - # If the Column is unnamed, give it a - # label() so that mutable column expressions - # can be located in the result even - # if the expression's identity has been changed - # due to adaption. - - if not column._label and not getattr(column, 'is_literal', False): - column = column.label(self._label_name) - - query._entities.append(self) - - self.column = column - self.froms = set() - - # look for ORM entities represented within the - # given expression. Try to count only entities - # for columns whose FROM object is in the actual list - # of FROMs for the overall expression - this helps - # subqueries which were built from ORM constructs from - # leaking out their entities into the main select construct - self.actual_froms = actual_froms = set(column._from_objects) - - self.entities = util.OrderedSet( - elem._annotations['parententity'] - for elem in visitors.iterate(column, {}) - if 'parententity' in elem._annotations - and actual_froms.intersection(elem._from_objects) - ) - - if self.entities: - self.entity_zero = list(self.entities)[0] - elif self.namespace is not None: - self.entity_zero = self.namespace - else: - self.entity_zero = None - - supports_single_entity = False - custom_rows = False - - @property - def entity_zero_or_selectable(self): - if self.entity_zero is not None: - return self.entity_zero - elif self.actual_froms: - return list(self.actual_froms)[0] - else: - return None - - def adapt_to_selectable(self, query, sel): - c = _ColumnEntity(query, sel.corresponding_column(self.column)) - c._label_name = self._label_name - c.entity_zero = self.entity_zero - c.entities = self.entities - - def setup_entity(self, ext_info, aliased_adapter): - if 'selectable' not in self.__dict__: - self.selectable = ext_info.selectable - self.froms.add(ext_info.selectable) - - def corresponds_to(self, entity): - # TODO: just returning False here, - # no tests fail - if self.entity_zero is None: - return False - elif _is_aliased_class(entity): - # TODO: polymorphic subclasses ? - return entity is self.entity_zero - else: - return not _is_aliased_class(self.entity_zero) and \ - entity.common_parent(self.entity_zero) - - def _resolve_expr_against_query_aliases(self, query, expr, context): - return query._adapt_clause(expr, False, True) - - def row_processor(self, query, context, custom_rows): - column = self._resolve_expr_against_query_aliases( - query, self.column, context) - - if context.adapter: - column = context.adapter.columns[column] - - def proc(row, result): - return row[column] - - return proc, self._label_name - - def setup_context(self, query, context): - column = self._resolve_expr_against_query_aliases( - query, self.column, context) - context.froms += tuple(self.froms) - context.primary_columns.append(column) - - def __str__(self): - return str(self.column) - - -class QueryContext(object): - multi_row_eager_loaders = False - adapter = None - froms = () - for_update = None - - def __init__(self, query): - - if query._statement is not None: - if isinstance(query._statement, expression.SelectBase) and \ - not query._statement.use_labels: - self.statement = query._statement.apply_labels() - else: - self.statement = query._statement - else: - self.statement = None - self.from_clause = query._from_obj - self.whereclause = query._criterion - self.order_by = query._order_by - - self.query = query - self.session = query.session - self.populate_existing = query._populate_existing - self.invoke_all_eagers = query._invoke_all_eagers - self.version_check = query._version_check - self.refresh_state = query._refresh_state - self.primary_columns = [] - self.secondary_columns = [] - self.eager_order_by = [] - self.eager_joins = {} - self.create_eager_joins = [] - self.propagate_options = set(o for o in query._with_options if - o.propagate_to_loaders) - self.attributes = query._attributes.copy() - - -class AliasOption(interfaces.MapperOption): - - def __init__(self, alias): - """Return a :class:`.MapperOption` that will indicate to the :class:`.Query` - that the main table has been aliased. - - This is a seldom-used option to suit the - very rare case that :func:`.contains_eager` - is being used in conjunction with a user-defined SELECT - statement that aliases the parent table. E.g.:: - - # define an aliased UNION called 'ulist' - ulist = users.select(users.c.user_id==7).\\ - union(users.select(users.c.user_id>7)).\\ - alias('ulist') - - # add on an eager load of "addresses" - statement = ulist.outerjoin(addresses).\\ - select().apply_labels() - - # create query, indicating "ulist" will be an - # alias for the main table, "addresses" - # property should be eager loaded - query = session.query(User).options( - contains_alias(ulist), - contains_eager(User.addresses)) - - # then get results via the statement - results = query.from_statement(statement).all() - - :param alias: is the string name of an alias, or a - :class:`~.sql.expression.Alias` object representing - the alias. - - """ - self.alias = alias - - def process_query(self, query): - if isinstance(self.alias, util.string_types): - alias = query._mapper_zero().mapped_table.alias(self.alias) - else: - alias = self.alias - query._from_obj_alias = sql_util.ColumnAdapter(alias) - - diff --git a/libs/sqlalchemy/orm/relationships.py b/libs/sqlalchemy/orm/relationships.py deleted file mode 100644 index 982f10a4..00000000 --- a/libs/sqlalchemy/orm/relationships.py +++ /dev/null @@ -1,2511 +0,0 @@ -# orm/relationships.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Heuristics related to join conditions as used in -:func:`.relationship`. - -Provides the :class:`.JoinCondition` object, which encapsulates -SQL annotation and aliasing behavior focused on the `primaryjoin` -and `secondaryjoin` aspects of :func:`.relationship`. - -""" - -from .. import sql, util, exc as sa_exc, schema, log - -from .util import CascadeOptions, _orm_annotate, _orm_deannotate -from . import dependency -from . import attributes -from ..sql.util import ( - ClauseAdapter, - join_condition, _shallow_annotate, visit_binary_product, - _deep_deannotate, selectables_overlap - ) -from ..sql import operators, expression, visitors -from .interfaces import MANYTOMANY, MANYTOONE, ONETOMANY, StrategizedProperty, PropComparator -from ..inspection import inspect -from . import mapper as mapperlib - -def remote(expr): - """Annotate a portion of a primaryjoin expression - with a 'remote' annotation. - - See the section :ref:`relationship_custom_foreign` for a - description of use. - - .. versionadded:: 0.8 - - .. seealso:: - - :ref:`relationship_custom_foreign` - - :func:`.foreign` - - """ - return _annotate_columns(expression._clause_element_as_expr(expr), - {"remote": True}) - - -def foreign(expr): - """Annotate a portion of a primaryjoin expression - with a 'foreign' annotation. - - See the section :ref:`relationship_custom_foreign` for a - description of use. - - .. versionadded:: 0.8 - - .. seealso:: - - :ref:`relationship_custom_foreign` - - :func:`.remote` - - """ - - return _annotate_columns(expression._clause_element_as_expr(expr), - {"foreign": True}) - - -@log.class_logger -@util.langhelpers.dependency_for("sqlalchemy.orm.properties") -class RelationshipProperty(StrategizedProperty): - """Describes an object property that holds a single item or list - of items that correspond to a related database table. - - Public constructor is the :func:`.orm.relationship` function. - - See also: - - :ref:`relationship_config_toplevel` - - """ - - strategy_wildcard_key = 'relationship' - - _dependency_processor = None - - def __init__(self, argument, - secondary=None, primaryjoin=None, - secondaryjoin=None, - foreign_keys=None, - uselist=None, - order_by=False, - backref=None, - back_populates=None, - post_update=False, - cascade=False, extension=None, - viewonly=False, lazy=True, - collection_class=None, passive_deletes=False, - passive_updates=True, remote_side=None, - enable_typechecks=True, join_depth=None, - comparator_factory=None, - single_parent=False, innerjoin=False, - distinct_target_key=None, - doc=None, - active_history=False, - cascade_backrefs=True, - load_on_pending=False, - strategy_class=None, _local_remote_pairs=None, - query_class=None, - info=None): - """Provide a relationship of a primary Mapper to a secondary Mapper. - - This corresponds to a parent-child or associative table relationship. The - constructed class is an instance of :class:`.RelationshipProperty`. - - A typical :func:`.relationship`, used in a classical mapping:: - - mapper(Parent, properties={ - 'children': relationship(Child) - }) - - Some arguments accepted by :func:`.relationship` optionally accept a - callable function, which when called produces the desired value. - The callable is invoked by the parent :class:`.Mapper` at "mapper - initialization" time, which happens only when mappers are first used, and - is assumed to be after all mappings have been constructed. This can be - used to resolve order-of-declaration and other dependency issues, such as - if ``Child`` is declared below ``Parent`` in the same file:: - - mapper(Parent, properties={ - "children":relationship(lambda: Child, - order_by=lambda: Child.id) - }) - - When using the :ref:`declarative_toplevel` extension, the Declarative - initializer allows string arguments to be passed to :func:`.relationship`. - These string arguments are converted into callables that evaluate - the string as Python code, using the Declarative - class-registry as a namespace. This allows the lookup of related - classes to be automatic via their string name, and removes the need to - import related classes at all into the local module space:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class Parent(Base): - __tablename__ = 'parent' - id = Column(Integer, primary_key=True) - children = relationship("Child", order_by="Child.id") - - A full array of examples and reference documentation regarding - :func:`.relationship` is at :ref:`relationship_config_toplevel`. - - :param argument: - a mapped class, or actual :class:`.Mapper` instance, representing the - target of the relationship. - - ``argument`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param secondary: - for a many-to-many relationship, specifies the intermediary - table, and is an instance of :class:`.Table`. The ``secondary`` keyword - argument should generally only - be used for a table that is not otherwise expressed in any class - mapping, unless this relationship is declared as view only, otherwise - conflicting persistence operations can occur. - - ``secondary`` may - also be passed as a callable function which is evaluated at - mapper initialization time. - - :param active_history=False: - When ``True``, indicates that the "previous" value for a - many-to-one reference should be loaded when replaced, if - not already loaded. Normally, history tracking logic for - simple many-to-ones only needs to be aware of the "new" - value in order to perform a flush. This flag is available - for applications that make use of - :func:`.attributes.get_history` which also need to know - the "previous" value of the attribute. - - :param backref: - indicates the string name of a property to be placed on the related - mapper's class that will handle this relationship in the other - direction. The other property will be created automatically - when the mappers are configured. Can also be passed as a - :func:`backref` object to control the configuration of the - new relationship. - - :param back_populates: - Takes a string name and has the same meaning as ``backref``, - except the complementing property is **not** created automatically, - and instead must be configured explicitly on the other mapper. The - complementing property should also indicate ``back_populates`` - to this relationship to ensure proper functioning. - - :param cascade: - a comma-separated list of cascade rules which determines how - Session operations should be "cascaded" from parent to child. - This defaults to ``False``, which means the default cascade - should be used. The default value is ``"save-update, merge"``. - - Available cascades are: - - * ``save-update`` - cascade the :meth:`.Session.add` - operation. This cascade applies both to future and - past calls to :meth:`~sqlalchemy.orm.session.Session.add`, - meaning new items added to a collection or scalar relationship - get placed into the same session as that of the parent, and - also applies to items which have been removed from this - relationship but are still part of unflushed history. - - * ``merge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.merge` - operation - - * ``expunge`` - cascade the :meth:`.Session.expunge` - operation - - * ``delete`` - cascade the :meth:`.Session.delete` - operation - - * ``delete-orphan`` - if an item of the child's type is - detached from its parent, mark it for deletion. - - .. versionchanged:: 0.7 - This option does not prevent - a new instance of the child object from being persisted - without a parent to start with; to constrain against - that case, ensure the child's foreign key column(s) - is configured as NOT NULL - - * ``refresh-expire`` - cascade the :meth:`.Session.expire` - and :meth:`~sqlalchemy.orm.session.Session.refresh` operations - - * ``all`` - shorthand for "save-update,merge, refresh-expire, - expunge, delete" - - See the section :ref:`unitofwork_cascades` for more background - on configuring cascades. - - :param cascade_backrefs=True: - a boolean value indicating if the ``save-update`` cascade should - operate along an assignment event intercepted by a backref. - When set to ``False``, - the attribute managed by this relationship will not cascade - an incoming transient object into the session of a - persistent parent, if the event is received via backref. - - That is:: - - mapper(A, a_table, properties={ - 'bs':relationship(B, backref="a", cascade_backrefs=False) - }) - - If an ``A()`` is present in the session, assigning it to - the "a" attribute on a transient ``B()`` will not place - the ``B()`` into the session. To set the flag in the other - direction, i.e. so that ``A().bs.append(B())`` won't add - a transient ``A()`` into the session for a persistent ``B()``:: - - mapper(A, a_table, properties={ - 'bs':relationship(B, - backref=backref("a", cascade_backrefs=False) - ) - }) - - See the section :ref:`unitofwork_cascades` for more background - on configuring cascades. - - :param collection_class: - a class or callable that returns a new list-holding object. will - be used in place of a plain list for storing elements. - Behavior of this attribute is described in detail at - :ref:`custom_collections`. - - :param comparator_factory: - a class which extends :class:`.RelationshipProperty.Comparator` which - provides custom SQL clause generation for comparison operations. - - :param distinct_target_key=None: - Indicate if a "subquery" eager load should apply the DISTINCT - keyword to the innermost SELECT statement. When left as ``None``, - the DISTINCT keyword will be applied in those cases when the target - columns do not comprise the full primary key of the target table. - When set to ``True``, the DISTINCT keyword is applied to the innermost - SELECT unconditionally. - - It may be desirable to set this flag to False when the DISTINCT is - reducing performance of the innermost subquery beyond that of what - duplicate innermost rows may be causing. - - .. versionadded:: 0.8.3 - distinct_target_key allows the - subquery eager loader to apply a DISTINCT modifier to the - innermost SELECT. - - .. versionchanged:: 0.9.0 - distinct_target_key now defaults to - ``None``, so that the feature enables itself automatically for - those cases where the innermost query targets a non-unique - key. - - :param doc: - docstring which will be applied to the resulting descriptor. - - :param extension: - an :class:`.AttributeExtension` instance, or list of extensions, - which will be prepended to the list of attribute listeners for - the resulting descriptor placed on the class. - **Deprecated.** Please see :class:`.AttributeEvents`. - - :param foreign_keys: - a list of columns which are to be used as "foreign key" columns, - or columns which refer to the value in a remote column, within the - context of this :func:`.relationship` object's ``primaryjoin`` - condition. That is, if the ``primaryjoin`` condition of this - :func:`.relationship` is ``a.id == b.a_id``, and the values in ``b.a_id`` - are required to be present in ``a.id``, then the "foreign key" column - of this :func:`.relationship` is ``b.a_id``. - - In normal cases, the ``foreign_keys`` parameter is **not required.** - :func:`.relationship` will **automatically** determine which columns - in the ``primaryjoin`` conditition are to be considered "foreign key" - columns based on those :class:`.Column` objects that specify - :class:`.ForeignKey`, or are otherwise listed as referencing columns - in a :class:`.ForeignKeyConstraint` construct. ``foreign_keys`` is only - needed when: - - 1. There is more than one way to construct a join from the local - table to the remote table, as there are multiple foreign key - references present. Setting ``foreign_keys`` will limit the - :func:`.relationship` to consider just those columns specified - here as "foreign". - - .. versionchanged:: 0.8 - A multiple-foreign key join ambiguity can be resolved by - setting the ``foreign_keys`` parameter alone, without the - need to explicitly set ``primaryjoin`` as well. - - 2. The :class:`.Table` being mapped does not actually have - :class:`.ForeignKey` or :class:`.ForeignKeyConstraint` - constructs present, often because the table - was reflected from a database that does not support foreign key - reflection (MySQL MyISAM). - - 3. The ``primaryjoin`` argument is used to construct a non-standard - join condition, which makes use of columns or expressions that do - not normally refer to their "parent" column, such as a join condition - expressed by a complex comparison using a SQL function. - - The :func:`.relationship` construct will raise informative error messages - that suggest the use of the ``foreign_keys`` parameter when presented - with an ambiguous condition. In typical cases, if :func:`.relationship` - doesn't raise any exceptions, the ``foreign_keys`` parameter is usually - not needed. - - ``foreign_keys`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - .. seealso:: - - :ref:`relationship_foreign_keys` - - :ref:`relationship_custom_foreign` - - :func:`.foreign` - allows direct annotation of the "foreign" columns - within a ``primaryjoin`` condition. - - .. versionadded:: 0.8 - The :func:`.foreign` annotation can also be applied - directly to the ``primaryjoin`` expression, which is an alternate, - more specific system of describing which columns in a particular - ``primaryjoin`` should be considered "foreign". - - :param info: Optional data dictionary which will be populated into the - :attr:`.MapperProperty.info` attribute of this object. - - .. versionadded:: 0.8 - - :param innerjoin=False: - when ``True``, joined eager loads will use an inner join to join - against related tables instead of an outer join. The purpose - of this option is generally one of performance, as inner joins - generally perform better than outer joins. Another reason can be - the use of ``with_lockmode``, which does not support outer joins. - - This flag can be set to ``True`` when the relationship references an - object via many-to-one using local foreign keys that are not nullable, - or when the reference is one-to-one or a collection that is guaranteed - to have one or at least one entry. - - :param join_depth: - when non-``None``, an integer value indicating how many levels - deep "eager" loaders should join on a self-referring or cyclical - relationship. The number counts how many times the same Mapper - shall be present in the loading condition along a particular join - branch. When left at its default of ``None``, eager loaders - will stop chaining when they encounter a the same target mapper - which is already higher up in the chain. This option applies - both to joined- and subquery- eager loaders. - - :param lazy='select': specifies - how the related items should be loaded. Default value is - ``select``. Values include: - - * ``select`` - items should be loaded lazily when the property is first - accessed, using a separate SELECT statement, or identity map - fetch for simple many-to-one references. - - * ``immediate`` - items should be loaded as the parents are loaded, - using a separate SELECT statement, or identity map fetch for - simple many-to-one references. - - .. versionadded:: 0.6.5 - - * ``joined`` - items should be loaded "eagerly" in the same query as - that of the parent, using a JOIN or LEFT OUTER JOIN. Whether - the join is "outer" or not is determined by the ``innerjoin`` - parameter. - - * ``subquery`` - items should be loaded "eagerly" as the parents are - loaded, using one additional SQL statement, which issues a JOIN to a - subquery of the original statement, for each collection requested. - - * ``noload`` - no loading should occur at any time. This is to - support "write-only" attributes, or attributes which are - populated in some manner specific to the application. - - * ``dynamic`` - the attribute will return a pre-configured - :class:`~sqlalchemy.orm.query.Query` object for all read - operations, onto which further filtering operations can be - applied before iterating the results. See - the section :ref:`dynamic_relationship` for more details. - - * True - a synonym for 'select' - - * False - a synonym for 'joined' - - * None - a synonym for 'noload' - - Detailed discussion of loader strategies is at :doc:`/orm/loading`. - - :param load_on_pending=False: - Indicates loading behavior for transient or pending parent objects. - - When set to ``True``, causes the lazy-loader to - issue a query for a parent object that is not persistent, meaning it has - never been flushed. This may take effect for a pending object when - autoflush is disabled, or for a transient object that has been - "attached" to a :class:`.Session` but is not part of its pending - collection. - - The load_on_pending flag does not improve behavior - when the ORM is used normally - object references should be constructed - at the object level, not at the foreign key level, so that they - are present in an ordinary way before flush() proceeds. This flag - is not not intended for general use. - - .. versionadded:: 0.6.5 - - .. seealso:: - - :meth:`.Session.enable_relationship_loading` - this method establishes - "load on pending" behavior for the whole object, and also allows - loading on objects that remain transient or detached. - - :param order_by: - indicates the ordering that should be applied when loading these - items. ``order_by`` is expected to refer to one of the :class:`.Column` - objects to which the target class is mapped, or - the attribute itself bound to the target class which refers - to the column. - - ``order_by`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param passive_deletes=False: - Indicates loading behavior during delete operations. - - A value of True indicates that unloaded child items should not - be loaded during a delete operation on the parent. Normally, - when a parent item is deleted, all child items are loaded so - that they can either be marked as deleted, or have their - foreign key to the parent set to NULL. Marking this flag as - True usually implies an ON DELETE rule is in - place which will handle updating/deleting child rows on the - database side. - - Additionally, setting the flag to the string value 'all' will - disable the "nulling out" of the child foreign keys, when there - is no delete or delete-orphan cascade enabled. This is - typically used when a triggering or error raise scenario is in - place on the database side. Note that the foreign key - attributes on in-session child objects will not be changed - after a flush occurs so this is a very special use-case - setting. - - :param passive_updates=True: - Indicates loading and INSERT/UPDATE/DELETE behavior when the - source of a foreign key value changes (i.e. an "on update" - cascade), which are typically the primary key columns of the - source row. - - When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will - handle propagation of an UPDATE from a source column to - dependent rows. Note that with databases which enforce - referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), - ON UPDATE CASCADE is required for this operation. The - relationship() will update the value of the attribute on related - items which are locally present in the session during a flush. - - When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The relationship() will issue the - appropriate UPDATE statements to the database in response to the - change of a referenced key, and items locally present in the - session during a flush will also be refreshed. - - This flag should probably be set to False if primary key changes - are expected and the database in use doesn't support CASCADE - (i.e. SQLite, MySQL MyISAM tables). - - Also see the passive_updates flag on ``mapper()``. - - A future SQLAlchemy release will provide a "detect" feature for - this flag. - - :param post_update: - this indicates that the relationship should be handled by a - second UPDATE statement after an INSERT or before a - DELETE. Currently, it also will issue an UPDATE after the - instance was UPDATEd as well, although this technically should - be improved. This flag is used to handle saving bi-directional - dependencies between two individual rows (i.e. each row - references the other), where it would otherwise be impossible to - INSERT or DELETE both rows fully since one row exists before the - other. Use this flag when a particular mapping arrangement will - incur two rows that are dependent on each other, such as a table - that has a one-to-many relationship to a set of child rows, and - also has a column that references a single child row within that - list (i.e. both tables contain a foreign key to each other). If - a ``flush()`` operation returns an error that a "cyclical - dependency" was detected, this is a cue that you might want to - use ``post_update`` to "break" the cycle. - - :param primaryjoin: - a SQL expression that will be used as the primary - join of this child object against the parent object, or in a - many-to-many relationship the join of the primary object to the - association table. By default, this value is computed based on the - foreign key relationships of the parent and child tables (or association - table). - - ``primaryjoin`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param remote_side: - used for self-referential relationships, indicates the column or - list of columns that form the "remote side" of the relationship. - - ``remote_side`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - .. versionchanged:: 0.8 - The :func:`.remote` annotation can also be applied - directly to the ``primaryjoin`` expression, which is an alternate, - more specific system of describing which columns in a particular - ``primaryjoin`` should be considered "remote". - - :param query_class: - a :class:`.Query` subclass that will be used as the base of the - "appender query" returned by a "dynamic" relationship, that - is, a relationship that specifies ``lazy="dynamic"`` or was - otherwise constructed using the :func:`.orm.dynamic_loader` - function. - - :param secondaryjoin: - a SQL expression that will be used as the join of - an association table to the child object. By default, this value is - computed based on the foreign key relationships of the association and - child tables. - - ``secondaryjoin`` may also be passed as a callable function - which is evaluated at mapper initialization time, and may be passed as a - Python-evaluable string when using Declarative. - - :param single_parent=(True|False): - when True, installs a validator which will prevent objects - from being associated with more than one parent at a time. - This is used for many-to-one or many-to-many relationships that - should be treated either as one-to-one or one-to-many. Its - usage is optional unless delete-orphan cascade is also - set on this relationship(), in which case its required. - - :param uselist=(True|False): - a boolean that indicates if this property should be loaded as a - list or a scalar. In most cases, this value is determined - automatically by ``relationship()``, based on the type and direction - of the relationship - one to many forms a list, many to one - forms a scalar, many to many is a list. If a scalar is desired - where normally a list would be present, such as a bi-directional - one-to-one relationship, set uselist to False. - - :param viewonly=False: - when set to True, the relationship is used only for loading objects - within the relationship, and has no effect on the unit-of-work - flush process. Relationships with viewonly can specify any kind of - join conditions to provide additional views of related objects - onto a parent object. Note that the functionality of a viewonly - relationship has its limits - complicated join conditions may - not compile into eager or lazy loaders properly. If this is the - case, use an alternative method. - - .. versionchanged:: 0.6 - :func:`relationship` was renamed from its previous name - :func:`relation`. - - """ - - self.uselist = uselist - self.argument = argument - self.secondary = secondary - self.primaryjoin = primaryjoin - self.secondaryjoin = secondaryjoin - self.post_update = post_update - self.direction = None - self.viewonly = viewonly - self.lazy = lazy - self.single_parent = single_parent - self._user_defined_foreign_keys = foreign_keys - self.collection_class = collection_class - self.passive_deletes = passive_deletes - self.cascade_backrefs = cascade_backrefs - self.passive_updates = passive_updates - self.remote_side = remote_side - self.enable_typechecks = enable_typechecks - self.query_class = query_class - self.innerjoin = innerjoin - self.distinct_target_key = distinct_target_key - self.doc = doc - self.active_history = active_history - self.join_depth = join_depth - self.local_remote_pairs = _local_remote_pairs - self.extension = extension - self.load_on_pending = load_on_pending - self.comparator_factory = comparator_factory or \ - RelationshipProperty.Comparator - self.comparator = self.comparator_factory(self, None) - util.set_creation_order(self) - - if info is not None: - self.info = info - - if strategy_class: - self.strategy_class = strategy_class - else: - self.strategy_class = self._strategy_lookup(("lazy", self.lazy)) - - self._reverse_property = set() - - self.cascade = cascade if cascade is not False \ - else "save-update, merge" - - self.order_by = order_by - - self.back_populates = back_populates - - if self.back_populates: - if backref: - raise sa_exc.ArgumentError( - "backref and back_populates keyword arguments " - "are mutually exclusive") - self.backref = None - else: - self.backref = backref - - def instrument_class(self, mapper): - attributes.register_descriptor( - mapper.class_, - self.key, - comparator=self.comparator_factory(self, mapper), - parententity=mapper, - doc=self.doc, - ) - - class Comparator(PropComparator): - """Produce boolean, comparison, and other operators for - :class:`.RelationshipProperty` attributes. - - See the documentation for :class:`.PropComparator` for a brief overview - of ORM level operator definition. - - See also: - - :class:`.PropComparator` - - :class:`.ColumnProperty.Comparator` - - :class:`.ColumnOperators` - - :ref:`types_operators` - - :attr:`.TypeEngine.comparator_factory` - - """ - - _of_type = None - - def __init__(self, prop, parentmapper, adapt_to_entity=None, of_type=None): - """Construction of :class:`.RelationshipProperty.Comparator` - is internal to the ORM's attribute mechanics. - - """ - self.prop = prop - self._parentmapper = parentmapper - self._adapt_to_entity = adapt_to_entity - if of_type: - self._of_type = of_type - - def adapt_to_entity(self, adapt_to_entity): - return self.__class__(self.property, self._parentmapper, - adapt_to_entity=adapt_to_entity, - of_type=self._of_type) - - @util.memoized_property - def mapper(self): - """The target :class:`.Mapper` referred to by this - :class:`.RelationshipProperty.Comparator`. - - This is the "target" or "remote" side of the - :func:`.relationship`. - - """ - return self.property.mapper - - @util.memoized_property - def _parententity(self): - return self.property.parent - - def _source_selectable(self): - elem = self.property.parent._with_polymorphic_selectable - if self.adapter: - return self.adapter(elem) - else: - return elem - - def __clause_element__(self): - adapt_from = self._source_selectable() - if self._of_type: - of_type = inspect(self._of_type).mapper - else: - of_type = None - - pj, sj, source, dest, \ - secondary, target_adapter = self.property._create_joins( - source_selectable=adapt_from, - source_polymorphic=True, - of_type=of_type) - if sj is not None: - return pj & sj - else: - return pj - - def of_type(self, cls): - """Produce a construct that represents a particular 'subtype' of - attribute for the parent class. - - Currently this is usable in conjunction with :meth:`.Query.join` - and :meth:`.Query.outerjoin`. - - """ - return RelationshipProperty.Comparator( - self.property, - self._parentmapper, - adapt_to_entity=self._adapt_to_entity, - of_type=cls) - - def in_(self, other): - """Produce an IN clause - this is not implemented - for :func:`~.orm.relationship`-based attributes at this time. - - """ - raise NotImplementedError('in_() not yet supported for ' - 'relationships. For a simple many-to-one, use ' - 'in_() against the set of foreign key values.') - - __hash__ = None - - def __eq__(self, other): - """Implement the ``==`` operator. - - In a many-to-one context, such as:: - - MyClass.some_prop == - - this will typically produce a - clause such as:: - - mytable.related_id == - - Where ```` is the primary key of the given - object. - - The ``==`` operator provides partial functionality for non- - many-to-one comparisons: - - * Comparisons against collections are not supported. - Use :meth:`~.RelationshipProperty.Comparator.contains`. - * Compared to a scalar one-to-many, will produce a - clause that compares the target columns in the parent to - the given target. - * Compared to a scalar many-to-many, an alias - of the association table will be rendered as - well, forming a natural join that is part of the - main body of the query. This will not work for - queries that go beyond simple AND conjunctions of - comparisons, such as those which use OR. Use - explicit joins, outerjoins, or - :meth:`~.RelationshipProperty.Comparator.has` for - more comprehensive non-many-to-one scalar - membership tests. - * Comparisons against ``None`` given in a one-to-many - or many-to-many context produce a NOT EXISTS clause. - - """ - if isinstance(other, (util.NoneType, expression.Null)): - if self.property.direction in [ONETOMANY, MANYTOMANY]: - return ~self._criterion_exists() - else: - return _orm_annotate(self.property._optimized_compare( - None, adapt_source=self.adapter)) - elif self.property.uselist: - raise sa_exc.InvalidRequestError("Can't compare a colle" - "ction to an object or collection; use " - "contains() to test for membership.") - else: - return _orm_annotate(self.property._optimized_compare(other, - adapt_source=self.adapter)) - - def _criterion_exists(self, criterion=None, **kwargs): - if getattr(self, '_of_type', None): - info = inspect(self._of_type) - target_mapper, to_selectable, is_aliased_class = \ - info.mapper, info.selectable, info.is_aliased_class - if self.property._is_self_referential and not is_aliased_class: - to_selectable = to_selectable.alias() - - single_crit = target_mapper._single_table_criterion - if single_crit is not None: - if criterion is not None: - criterion = single_crit & criterion - else: - criterion = single_crit - else: - is_aliased_class = False - to_selectable = None - - if self.adapter: - source_selectable = self._source_selectable() - else: - source_selectable = None - - pj, sj, source, dest, secondary, target_adapter = \ - self.property._create_joins(dest_polymorphic=True, - dest_selectable=to_selectable, - source_selectable=source_selectable) - - for k in kwargs: - crit = getattr(self.property.mapper.class_, k) == kwargs[k] - if criterion is None: - criterion = crit - else: - criterion = criterion & crit - - # annotate the *local* side of the join condition, in the case - # of pj + sj this is the full primaryjoin, in the case of just - # pj its the local side of the primaryjoin. - if sj is not None: - j = _orm_annotate(pj) & sj - else: - j = _orm_annotate(pj, exclude=self.property.remote_side) - - if criterion is not None and target_adapter and not is_aliased_class: - # limit this adapter to annotated only? - criterion = target_adapter.traverse(criterion) - - # only have the "joined left side" of what we - # return be subject to Query adaption. The right - # side of it is used for an exists() subquery and - # should not correlate or otherwise reach out - # to anything in the enclosing query. - if criterion is not None: - criterion = criterion._annotate( - {'no_replacement_traverse': True}) - - crit = j & sql.True_._ifnone(criterion) - - ex = sql.exists([1], crit, from_obj=dest).correlate_except(dest) - if secondary is not None: - ex = ex.correlate_except(secondary) - return ex - - def any(self, criterion=None, **kwargs): - """Produce an expression that tests a collection against - particular criterion, using EXISTS. - - An expression like:: - - session.query(MyClass).filter( - MyClass.somereference.any(SomeRelated.x==2) - ) - - - Will produce a query like:: - - SELECT * FROM my_table WHERE - EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id - AND related.x=2) - - Because :meth:`~.RelationshipProperty.Comparator.any` uses - a correlated subquery, its performance is not nearly as - good when compared against large target tables as that of - using a join. - - :meth:`~.RelationshipProperty.Comparator.any` is particularly - useful for testing for empty collections:: - - session.query(MyClass).filter( - ~MyClass.somereference.any() - ) - - will produce:: - - SELECT * FROM my_table WHERE - NOT EXISTS (SELECT 1 FROM related WHERE - related.my_id=my_table.id) - - :meth:`~.RelationshipProperty.Comparator.any` is only - valid for collections, i.e. a :func:`.relationship` - that has ``uselist=True``. For scalar references, - use :meth:`~.RelationshipProperty.Comparator.has`. - - """ - if not self.property.uselist: - raise sa_exc.InvalidRequestError( - "'any()' not implemented for scalar " - "attributes. Use has()." - ) - - return self._criterion_exists(criterion, **kwargs) - - def has(self, criterion=None, **kwargs): - """Produce an expression that tests a scalar reference against - particular criterion, using EXISTS. - - An expression like:: - - session.query(MyClass).filter( - MyClass.somereference.has(SomeRelated.x==2) - ) - - - Will produce a query like:: - - SELECT * FROM my_table WHERE - EXISTS (SELECT 1 FROM related WHERE - related.id==my_table.related_id AND related.x=2) - - Because :meth:`~.RelationshipProperty.Comparator.has` uses - a correlated subquery, its performance is not nearly as - good when compared against large target tables as that of - using a join. - - :meth:`~.RelationshipProperty.Comparator.has` is only - valid for scalar references, i.e. a :func:`.relationship` - that has ``uselist=False``. For collection references, - use :meth:`~.RelationshipProperty.Comparator.any`. - - """ - if self.property.uselist: - raise sa_exc.InvalidRequestError( - "'has()' not implemented for collections. " - "Use any().") - return self._criterion_exists(criterion, **kwargs) - - def contains(self, other, **kwargs): - """Return a simple expression that tests a collection for - containment of a particular item. - - :meth:`~.RelationshipProperty.Comparator.contains` is - only valid for a collection, i.e. a - :func:`~.orm.relationship` that implements - one-to-many or many-to-many with ``uselist=True``. - - When used in a simple one-to-many context, an - expression like:: - - MyClass.contains(other) - - Produces a clause like:: - - mytable.id == - - Where ```` is the value of the foreign key - attribute on ``other`` which refers to the primary - key of its parent object. From this it follows that - :meth:`~.RelationshipProperty.Comparator.contains` is - very useful when used with simple one-to-many - operations. - - For many-to-many operations, the behavior of - :meth:`~.RelationshipProperty.Comparator.contains` - has more caveats. The association table will be - rendered in the statement, producing an "implicit" - join, that is, includes multiple tables in the FROM - clause which are equated in the WHERE clause:: - - query(MyClass).filter(MyClass.contains(other)) - - Produces a query like:: - - SELECT * FROM my_table, my_association_table AS - my_association_table_1 WHERE - my_table.id = my_association_table_1.parent_id - AND my_association_table_1.child_id = - - Where ```` would be the primary key of - ``other``. From the above, it is clear that - :meth:`~.RelationshipProperty.Comparator.contains` - will **not** work with many-to-many collections when - used in queries that move beyond simple AND - conjunctions, such as multiple - :meth:`~.RelationshipProperty.Comparator.contains` - expressions joined by OR. In such cases subqueries or - explicit "outer joins" will need to be used instead. - See :meth:`~.RelationshipProperty.Comparator.any` for - a less-performant alternative using EXISTS, or refer - to :meth:`.Query.outerjoin` as well as :ref:`ormtutorial_joins` - for more details on constructing outer joins. - - """ - if not self.property.uselist: - raise sa_exc.InvalidRequestError( - "'contains' not implemented for scalar " - "attributes. Use ==") - clause = self.property._optimized_compare(other, - adapt_source=self.adapter) - - if self.property.secondaryjoin is not None: - clause.negation_clause = \ - self.__negated_contains_or_equals(other) - - return clause - - def __negated_contains_or_equals(self, other): - if self.property.direction == MANYTOONE: - state = attributes.instance_state(other) - - def state_bindparam(x, state, col): - o = state.obj() # strong ref - return sql.bindparam(x, unique=True, callable_=lambda: \ - self.property.mapper._get_committed_attr_by_column(o, col)) - - def adapt(col): - if self.adapter: - return self.adapter(col) - else: - return col - - if self.property._use_get: - return sql.and_(*[ - sql.or_( - adapt(x) != state_bindparam(adapt(x), state, y), - adapt(x) == None) - for (x, y) in self.property.local_remote_pairs]) - - criterion = sql.and_(*[x == y for (x, y) in - zip( - self.property.mapper.primary_key, - self.property.\ - mapper.\ - primary_key_from_instance(other)) - ]) - return ~self._criterion_exists(criterion) - - def __ne__(self, other): - """Implement the ``!=`` operator. - - In a many-to-one context, such as:: - - MyClass.some_prop != - - This will typically produce a clause such as:: - - mytable.related_id != - - Where ```` is the primary key of the - given object. - - The ``!=`` operator provides partial functionality for non- - many-to-one comparisons: - - * Comparisons against collections are not supported. - Use - :meth:`~.RelationshipProperty.Comparator.contains` - in conjunction with :func:`~.expression.not_`. - * Compared to a scalar one-to-many, will produce a - clause that compares the target columns in the parent to - the given target. - * Compared to a scalar many-to-many, an alias - of the association table will be rendered as - well, forming a natural join that is part of the - main body of the query. This will not work for - queries that go beyond simple AND conjunctions of - comparisons, such as those which use OR. Use - explicit joins, outerjoins, or - :meth:`~.RelationshipProperty.Comparator.has` in - conjunction with :func:`~.expression.not_` for - more comprehensive non-many-to-one scalar - membership tests. - * Comparisons against ``None`` given in a one-to-many - or many-to-many context produce an EXISTS clause. - - """ - if isinstance(other, (util.NoneType, expression.Null)): - if self.property.direction == MANYTOONE: - return sql.or_(*[x != None for x in - self.property._calculated_foreign_keys]) - else: - return self._criterion_exists() - elif self.property.uselist: - raise sa_exc.InvalidRequestError("Can't compare a collection" - " to an object or collection; use " - "contains() to test for membership.") - else: - return self.__negated_contains_or_equals(other) - - @util.memoized_property - def property(self): - if mapperlib.Mapper._new_mappers: - mapperlib.Mapper._configure_all() - return self.prop - - def compare(self, op, value, - value_is_parent=False, - alias_secondary=True): - if op == operators.eq: - if value is None: - if self.uselist: - return ~sql.exists([1], self.primaryjoin) - else: - return self._optimized_compare(None, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) - else: - return self._optimized_compare(value, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) - else: - return op(self.comparator, value) - - def _optimized_compare(self, value, value_is_parent=False, - adapt_source=None, - alias_secondary=True): - if value is not None: - value = attributes.instance_state(value) - return self._lazy_strategy.lazy_clause(value, - reverse_direction=not value_is_parent, - alias_secondary=alias_secondary, - adapt_source=adapt_source) - - def __str__(self): - return str(self.parent.class_.__name__) + "." + self.key - - def merge(self, - session, - source_state, - source_dict, - dest_state, - dest_dict, - load, _recursive): - - if load: - for r in self._reverse_property: - if (source_state, r) in _recursive: - return - - if not "merge" in self._cascade: - return - - if self.key not in source_dict: - return - - if self.uselist: - instances = source_state.get_impl(self.key).\ - get(source_state, source_dict) - if hasattr(instances, '_sa_adapter'): - # convert collections to adapters to get a true iterator - instances = instances._sa_adapter - - if load: - # for a full merge, pre-load the destination collection, - # so that individual _merge of each item pulls from identity - # map for those already present. - # also assumes CollectionAttrbiuteImpl behavior of loading - # "old" list in any case - dest_state.get_impl(self.key).get(dest_state, dest_dict) - - dest_list = [] - for current in instances: - current_state = attributes.instance_state(current) - current_dict = attributes.instance_dict(current) - _recursive[(current_state, self)] = True - obj = session._merge(current_state, current_dict, - load=load, _recursive=_recursive) - if obj is not None: - dest_list.append(obj) - - if not load: - coll = attributes.init_state_collection(dest_state, - dest_dict, self.key) - for c in dest_list: - coll.append_without_event(c) - else: - dest_state.get_impl(self.key)._set_iterable(dest_state, - dest_dict, dest_list) - else: - current = source_dict[self.key] - if current is not None: - current_state = attributes.instance_state(current) - current_dict = attributes.instance_dict(current) - _recursive[(current_state, self)] = True - obj = session._merge(current_state, current_dict, - load=load, _recursive=_recursive) - else: - obj = None - - if not load: - dest_dict[self.key] = obj - else: - dest_state.get_impl(self.key).set(dest_state, - dest_dict, obj, None) - - def _value_as_iterable(self, state, dict_, key, - passive=attributes.PASSIVE_OFF): - """Return a list of tuples (state, obj) for the given - key. - - returns an empty list if the value is None/empty/PASSIVE_NO_RESULT - """ - - impl = state.manager[key].impl - x = impl.get(state, dict_, passive=passive) - if x is attributes.PASSIVE_NO_RESULT or x is None: - return [] - elif hasattr(impl, 'get_collection'): - return [ - (attributes.instance_state(o), o) for o in - impl.get_collection(state, dict_, x, passive=passive) - ] - else: - return [(attributes.instance_state(x), x)] - - def cascade_iterator(self, type_, state, dict_, - visited_states, halt_on=None): - #assert type_ in self._cascade - - # only actively lazy load on the 'delete' cascade - if type_ != 'delete' or self.passive_deletes: - passive = attributes.PASSIVE_NO_INITIALIZE - else: - passive = attributes.PASSIVE_OFF - - if type_ == 'save-update': - tuples = state.manager[self.key].impl.\ - get_all_pending(state, dict_) - - else: - tuples = self._value_as_iterable(state, dict_, self.key, - passive=passive) - - skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \ - not in self._cascade - - for instance_state, c in tuples: - if instance_state in visited_states: - continue - - if c is None: - # would like to emit a warning here, but - # would not be consistent with collection.append(None) - # current behavior of silently skipping. - # see [ticket:2229] - continue - - instance_dict = attributes.instance_dict(c) - - if halt_on and halt_on(instance_state): - continue - - if skip_pending and not instance_state.key: - continue - - instance_mapper = instance_state.manager.mapper - - if not instance_mapper.isa(self.mapper.class_manager.mapper): - raise AssertionError("Attribute '%s' on class '%s' " - "doesn't handle objects " - "of type '%s'" % ( - self.key, - self.parent.class_, - c.__class__ - )) - - visited_states.add(instance_state) - - yield c, instance_mapper, instance_state, instance_dict - - def _add_reverse_property(self, key): - other = self.mapper.get_property(key, _configure_mappers=False) - self._reverse_property.add(other) - other._reverse_property.add(self) - - if not other.mapper.common_parent(self.parent): - raise sa_exc.ArgumentError('reverse_property %r on ' - 'relationship %s references relationship %s, which ' - 'does not reference mapper %s' % (key, self, other, - self.parent)) - if self.direction in (ONETOMANY, MANYTOONE) and self.direction \ - == other.direction: - raise sa_exc.ArgumentError('%s and back-reference %s are ' - 'both of the same direction %r. Did you mean to ' - 'set remote_side on the many-to-one side ?' - % (other, self, self.direction)) - - @util.memoized_property - def mapper(self): - """Return the targeted :class:`.Mapper` for this - :class:`.RelationshipProperty`. - - This is a lazy-initializing static attribute. - - """ - if util.callable(self.argument) and \ - not isinstance(self.argument, (type, mapperlib.Mapper)): - argument = self.argument() - else: - argument = self.argument - - if isinstance(argument, type): - mapper_ = mapperlib.class_mapper(argument, - configure=False) - elif isinstance(self.argument, mapperlib.Mapper): - mapper_ = argument - else: - raise sa_exc.ArgumentError("relationship '%s' expects " - "a class or a mapper argument (received: %s)" - % (self.key, type(argument))) - return mapper_ - - @util.memoized_property - @util.deprecated("0.7", "Use .target") - def table(self): - """Return the selectable linked to this - :class:`.RelationshipProperty` object's target - :class:`.Mapper`. - """ - return self.target - - def do_init(self): - self._check_conflicts() - self._process_dependent_arguments() - self._setup_join_conditions() - self._check_cascade_settings(self._cascade) - self._post_init() - self._generate_backref() - super(RelationshipProperty, self).do_init() - self._lazy_strategy = self._get_strategy((("lazy", "select"),)) - - - def _process_dependent_arguments(self): - """Convert incoming configuration arguments to their - proper form. - - Callables are resolved, ORM annotations removed. - - """ - # accept callables for other attributes which may require - # deferred initialization. This technique is used - # by declarative "string configs" and some recipes. - for attr in ( - 'order_by', 'primaryjoin', 'secondaryjoin', - 'secondary', '_user_defined_foreign_keys', 'remote_side', - ): - attr_value = getattr(self, attr) - if util.callable(attr_value): - setattr(self, attr, attr_value()) - - # remove "annotations" which are present if mapped class - # descriptors are used to create the join expression. - for attr in 'primaryjoin', 'secondaryjoin': - val = getattr(self, attr) - if val is not None: - setattr(self, attr, _orm_deannotate( - expression._only_column_elements(val, attr)) - ) - - # ensure expressions in self.order_by, foreign_keys, - # remote_side are all columns, not strings. - if self.order_by is not False and self.order_by is not None: - self.order_by = [ - expression._only_column_elements(x, "order_by") - for x in - util.to_list(self.order_by)] - - self._user_defined_foreign_keys = \ - util.column_set( - expression._only_column_elements(x, "foreign_keys") - for x in util.to_column_set( - self._user_defined_foreign_keys - )) - - self.remote_side = \ - util.column_set( - expression._only_column_elements(x, "remote_side") - for x in - util.to_column_set(self.remote_side)) - - self.target = self.mapper.mapped_table - - - def _setup_join_conditions(self): - self._join_condition = jc = JoinCondition( - parent_selectable=self.parent.mapped_table, - child_selectable=self.mapper.mapped_table, - parent_local_selectable=self.parent.local_table, - child_local_selectable=self.mapper.local_table, - primaryjoin=self.primaryjoin, - secondary=self.secondary, - secondaryjoin=self.secondaryjoin, - parent_equivalents=self.parent._equivalent_columns, - child_equivalents=self.mapper._equivalent_columns, - consider_as_foreign_keys=self._user_defined_foreign_keys, - local_remote_pairs=self.local_remote_pairs, - remote_side=self.remote_side, - self_referential=self._is_self_referential, - prop=self, - support_sync=not self.viewonly, - can_be_synced_fn=self._columns_are_mapped - ) - self.primaryjoin = jc.deannotated_primaryjoin - self.secondaryjoin = jc.deannotated_secondaryjoin - self.direction = jc.direction - self.local_remote_pairs = jc.local_remote_pairs - self.remote_side = jc.remote_columns - self.local_columns = jc.local_columns - self.synchronize_pairs = jc.synchronize_pairs - self._calculated_foreign_keys = jc.foreign_key_columns - self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs - - def _check_conflicts(self): - """Test that this relationship is legal, warn about - inheritance conflicts.""" - - if not self.is_primary() \ - and not mapperlib.class_mapper( - self.parent.class_, - configure=False).has_property(self.key): - raise sa_exc.ArgumentError("Attempting to assign a new " - "relationship '%s' to a non-primary mapper on " - "class '%s'. New relationships can only be added " - "to the primary mapper, i.e. the very first mapper " - "created for class '%s' " % (self.key, - self.parent.class_.__name__, - self.parent.class_.__name__)) - - # check for conflicting relationship() on superclass - if not self.parent.concrete: - for inheriting in self.parent.iterate_to_root(): - if inheriting is not self.parent \ - and inheriting.has_property(self.key): - util.warn("Warning: relationship '%s' on mapper " - "'%s' supersedes the same relationship " - "on inherited mapper '%s'; this can " - "cause dependency issues during flush" - % (self.key, self.parent, inheriting)) - - def _get_cascade(self): - """Return the current cascade setting for this - :class:`.RelationshipProperty`. - """ - return self._cascade - - def _set_cascade(self, cascade): - cascade = CascadeOptions(cascade) - if 'mapper' in self.__dict__: - self._check_cascade_settings(cascade) - self._cascade = cascade - - if self._dependency_processor: - self._dependency_processor.cascade = cascade - - cascade = property(_get_cascade, _set_cascade) - - def _check_cascade_settings(self, cascade): - if cascade.delete_orphan and not self.single_parent \ - and (self.direction is MANYTOMANY or self.direction - is MANYTOONE): - raise sa_exc.ArgumentError( - 'On %s, delete-orphan cascade is not supported ' - 'on a many-to-many or many-to-one relationship ' - 'when single_parent is not set. Set ' - 'single_parent=True on the relationship().' - % self) - if self.direction is MANYTOONE and self.passive_deletes: - util.warn("On %s, 'passive_deletes' is normally configured " - "on one-to-many, one-to-one, many-to-many " - "relationships only." - % self) - - if self.passive_deletes == 'all' and \ - ("delete" in cascade or - "delete-orphan" in cascade): - raise sa_exc.ArgumentError( - "On %s, can't set passive_deletes='all' in conjunction " - "with 'delete' or 'delete-orphan' cascade" % self) - - if cascade.delete_orphan: - self.mapper.primary_mapper()._delete_orphans.append( - (self.key, self.parent.class_) - ) - - def _columns_are_mapped(self, *cols): - """Return True if all columns in the given collection are - mapped by the tables referenced by this :class:`.Relationship`. - - """ - for c in cols: - if self.secondary is not None \ - and self.secondary.c.contains_column(c): - continue - if not self.parent.mapped_table.c.contains_column(c) and \ - not self.target.c.contains_column(c): - return False - return True - - def _generate_backref(self): - """Interpret the 'backref' instruction to create a - :func:`.relationship` complementary to this one.""" - - if not self.is_primary(): - return - if self.backref is not None and not self.back_populates: - if isinstance(self.backref, util.string_types): - backref_key, kwargs = self.backref, {} - else: - backref_key, kwargs = self.backref - mapper = self.mapper.primary_mapper() - - check = set(mapper.iterate_to_root()).\ - union(mapper.self_and_descendants) - for m in check: - if m.has_property(backref_key): - raise sa_exc.ArgumentError("Error creating backref " - "'%s' on relationship '%s': property of that " - "name exists on mapper '%s'" % (backref_key, - self, m)) - - # determine primaryjoin/secondaryjoin for the - # backref. Use the one we had, so that - # a custom join doesn't have to be specified in - # both directions. - if self.secondary is not None: - # for many to many, just switch primaryjoin/ - # secondaryjoin. use the annotated - # pj/sj on the _join_condition. - pj = kwargs.pop('primaryjoin', - self._join_condition.secondaryjoin_minus_local) - sj = kwargs.pop('secondaryjoin', - self._join_condition.primaryjoin_minus_local) - else: - pj = kwargs.pop('primaryjoin', - self._join_condition.primaryjoin_reverse_remote) - sj = kwargs.pop('secondaryjoin', None) - if sj: - raise sa_exc.InvalidRequestError( - "Can't assign 'secondaryjoin' on a backref " - "against a non-secondary relationship." - ) - - foreign_keys = kwargs.pop('foreign_keys', - self._user_defined_foreign_keys) - parent = self.parent.primary_mapper() - kwargs.setdefault('viewonly', self.viewonly) - kwargs.setdefault('post_update', self.post_update) - kwargs.setdefault('passive_updates', self.passive_updates) - self.back_populates = backref_key - relationship = RelationshipProperty( - parent, self.secondary, - pj, sj, - foreign_keys=foreign_keys, - back_populates=self.key, - **kwargs) - mapper._configure_property(backref_key, relationship) - - if self.back_populates: - self._add_reverse_property(self.back_populates) - - def _post_init(self): - if self.uselist is None: - self.uselist = self.direction is not MANYTOONE - if not self.viewonly: - self._dependency_processor = \ - dependency.DependencyProcessor.from_relationship(self) - - @util.memoized_property - def _use_get(self): - """memoize the 'use_get' attribute of this RelationshipLoader's - lazyloader.""" - - strategy = self._lazy_strategy - return strategy.use_get - - @util.memoized_property - def _is_self_referential(self): - return self.mapper.common_parent(self.parent) - - def _create_joins(self, source_polymorphic=False, - source_selectable=None, dest_polymorphic=False, - dest_selectable=None, of_type=None): - if source_selectable is None: - if source_polymorphic and self.parent.with_polymorphic: - source_selectable = self.parent._with_polymorphic_selectable - - aliased = False - if dest_selectable is None: - if dest_polymorphic and self.mapper.with_polymorphic: - dest_selectable = self.mapper._with_polymorphic_selectable - aliased = True - else: - dest_selectable = self.mapper.mapped_table - - if self._is_self_referential and source_selectable is None: - dest_selectable = dest_selectable.alias() - aliased = True - else: - aliased = True - - dest_mapper = of_type or self.mapper - - single_crit = dest_mapper._single_table_criterion - aliased = aliased or (source_selectable is not None) - - primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable = \ - self._join_condition.join_targets( - source_selectable, dest_selectable, aliased, single_crit - ) - if source_selectable is None: - source_selectable = self.parent.local_table - if dest_selectable is None: - dest_selectable = self.mapper.local_table - return (primaryjoin, secondaryjoin, source_selectable, - dest_selectable, secondary, target_adapter) - -def _annotate_columns(element, annotations): - def clone(elem): - if isinstance(elem, expression.ColumnClause): - elem = elem._annotate(annotations.copy()) - elem._copy_internals(clone=clone) - return elem - - if element is not None: - element = clone(element) - return element - - -class JoinCondition(object): - def __init__(self, - parent_selectable, - child_selectable, - parent_local_selectable, - child_local_selectable, - primaryjoin=None, - secondary=None, - secondaryjoin=None, - parent_equivalents=None, - child_equivalents=None, - consider_as_foreign_keys=None, - local_remote_pairs=None, - remote_side=None, - self_referential=False, - prop=None, - support_sync=True, - can_be_synced_fn=lambda *c: True - ): - self.parent_selectable = parent_selectable - self.parent_local_selectable = parent_local_selectable - self.child_selectable = child_selectable - self.child_local_selectable = child_local_selectable - self.parent_equivalents = parent_equivalents - self.child_equivalents = child_equivalents - self.primaryjoin = primaryjoin - self.secondaryjoin = secondaryjoin - self.secondary = secondary - self.consider_as_foreign_keys = consider_as_foreign_keys - self._local_remote_pairs = local_remote_pairs - self._remote_side = remote_side - self.prop = prop - self.self_referential = self_referential - self.support_sync = support_sync - self.can_be_synced_fn = can_be_synced_fn - self._determine_joins() - self._annotate_fks() - self._annotate_remote() - self._annotate_local() - self._setup_pairs() - self._check_foreign_cols(self.primaryjoin, True) - if self.secondaryjoin is not None: - self._check_foreign_cols(self.secondaryjoin, False) - self._determine_direction() - self._check_remote_side() - self._log_joins() - - def _log_joins(self): - if self.prop is None: - return - log = self.prop.logger - log.info('%s setup primary join %s', self.prop, - self.primaryjoin) - log.info('%s setup secondary join %s', self.prop, - self.secondaryjoin) - log.info('%s synchronize pairs [%s]', self.prop, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.synchronize_pairs)) - log.info('%s secondary synchronize pairs [%s]', self.prop, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.secondary_synchronize_pairs or [])) - log.info('%s local/remote pairs [%s]', self.prop, - ','.join('(%s / %s)' % (l, r) for (l, r) in - self.local_remote_pairs)) - log.info('%s remote columns [%s]', self.prop, - ','.join('%s' % col for col in self.remote_columns) - ) - log.info('%s local columns [%s]', self.prop, - ','.join('%s' % col for col in self.local_columns) - ) - log.info('%s relationship direction %s', self.prop, - self.direction) - - def _determine_joins(self): - """Determine the 'primaryjoin' and 'secondaryjoin' attributes, - if not passed to the constructor already. - - This is based on analysis of the foreign key relationships - between the parent and target mapped selectables. - - """ - if self.secondaryjoin is not None and self.secondary is None: - raise sa_exc.ArgumentError( - "Property %s specified with secondary " - "join condition but " - "no secondary argument" % self.prop) - - # find a join between the given mapper's mapped table and - # the given table. will try the mapper's local table first - # for more specificity, then if not found will try the more - # general mapped table, which in the case of inheritance is - # a join. - try: - consider_as_foreign_keys = self.consider_as_foreign_keys or None - if self.secondary is not None: - if self.secondaryjoin is None: - self.secondaryjoin = \ - join_condition( - self.child_selectable, - self.secondary, - a_subset=self.child_local_selectable, - consider_as_foreign_keys=consider_as_foreign_keys - ) - if self.primaryjoin is None: - self.primaryjoin = \ - join_condition( - self.parent_selectable, - self.secondary, - a_subset=self.parent_local_selectable, - consider_as_foreign_keys=consider_as_foreign_keys - ) - else: - if self.primaryjoin is None: - self.primaryjoin = \ - join_condition( - self.parent_selectable, - self.child_selectable, - a_subset=self.parent_local_selectable, - consider_as_foreign_keys=consider_as_foreign_keys - ) - except sa_exc.NoForeignKeysError: - if self.secondary is not None: - raise sa_exc.NoForeignKeysError("Could not determine join " - "condition between parent/child tables on " - "relationship %s - there are no foreign keys " - "linking these tables via secondary table '%s'. " - "Ensure that referencing columns are associated " - "with a ForeignKey or ForeignKeyConstraint, or " - "specify 'primaryjoin' and 'secondaryjoin' " - "expressions." - % (self.prop, self.secondary)) - else: - raise sa_exc.NoForeignKeysError("Could not determine join " - "condition between parent/child tables on " - "relationship %s - there are no foreign keys " - "linking these tables. " - "Ensure that referencing columns are associated " - "with a ForeignKey or ForeignKeyConstraint, or " - "specify a 'primaryjoin' expression." - % self.prop) - except sa_exc.AmbiguousForeignKeysError: - if self.secondary is not None: - raise sa_exc.AmbiguousForeignKeysError( - "Could not determine join " - "condition between parent/child tables on " - "relationship %s - there are multiple foreign key " - "paths linking the tables via secondary table '%s'. " - "Specify the 'foreign_keys' " - "argument, providing a list of those columns which " - "should be counted as containing a foreign key " - "reference from the secondary table to each of the " - "parent and child tables." - % (self.prop, self.secondary)) - else: - raise sa_exc.AmbiguousForeignKeysError( - "Could not determine join " - "condition between parent/child tables on " - "relationship %s - there are multiple foreign key " - "paths linking the tables. Specify the " - "'foreign_keys' argument, providing a list of those " - "columns which should be counted as containing a " - "foreign key reference to the parent table." - % self.prop) - - @property - def primaryjoin_minus_local(self): - return _deep_deannotate(self.primaryjoin, values=("local", "remote")) - - @property - def secondaryjoin_minus_local(self): - return _deep_deannotate(self.secondaryjoin, values=("local", "remote")) - - @util.memoized_property - def primaryjoin_reverse_remote(self): - """Return the primaryjoin condition suitable for the - "reverse" direction. - - If the primaryjoin was delivered here with pre-existing - "remote" annotations, the local/remote annotations - are reversed. Otherwise, the local/remote annotations - are removed. - - """ - if self._has_remote_annotations: - def replace(element): - if "remote" in element._annotations: - v = element._annotations.copy() - del v['remote'] - v['local'] = True - return element._with_annotations(v) - elif "local" in element._annotations: - v = element._annotations.copy() - del v['local'] - v['remote'] = True - return element._with_annotations(v) - return visitors.replacement_traverse( - self.primaryjoin, {}, replace) - else: - if self._has_foreign_annotations: - # TODO: coverage - return _deep_deannotate(self.primaryjoin, - values=("local", "remote")) - else: - return _deep_deannotate(self.primaryjoin) - - def _has_annotation(self, clause, annotation): - for col in visitors.iterate(clause, {}): - if annotation in col._annotations: - return True - else: - return False - - @util.memoized_property - def _has_foreign_annotations(self): - return self._has_annotation(self.primaryjoin, "foreign") - - @util.memoized_property - def _has_remote_annotations(self): - return self._has_annotation(self.primaryjoin, "remote") - - def _annotate_fks(self): - """Annotate the primaryjoin and secondaryjoin - structures with 'foreign' annotations marking columns - considered as foreign. - - """ - if self._has_foreign_annotations: - return - - if self.consider_as_foreign_keys: - self._annotate_from_fk_list() - else: - self._annotate_present_fks() - - def _annotate_from_fk_list(self): - def check_fk(col): - if col in self.consider_as_foreign_keys: - return col._annotate({"foreign": True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, - {}, - check_fk - ) - if self.secondaryjoin is not None: - self.secondaryjoin = visitors.replacement_traverse( - self.secondaryjoin, - {}, - check_fk - ) - - def _annotate_present_fks(self): - if self.secondary is not None: - secondarycols = util.column_set(self.secondary.c) - else: - secondarycols = set() - - def is_foreign(a, b): - if isinstance(a, schema.Column) and \ - isinstance(b, schema.Column): - if a.references(b): - return a - elif b.references(a): - return b - - if secondarycols: - if a in secondarycols and b not in secondarycols: - return a - elif b in secondarycols and a not in secondarycols: - return b - - def visit_binary(binary): - if not isinstance(binary.left, sql.ColumnElement) or \ - not isinstance(binary.right, sql.ColumnElement): - return - - if "foreign" not in binary.left._annotations and \ - "foreign" not in binary.right._annotations: - col = is_foreign(binary.left, binary.right) - if col is not None: - if col.compare(binary.left): - binary.left = binary.left._annotate( - {"foreign": True}) - elif col.compare(binary.right): - binary.right = binary.right._annotate( - {"foreign": True}) - - self.primaryjoin = visitors.cloned_traverse( - self.primaryjoin, - {}, - {"binary": visit_binary} - ) - if self.secondaryjoin is not None: - self.secondaryjoin = visitors.cloned_traverse( - self.secondaryjoin, - {}, - {"binary": visit_binary} - ) - - def _refers_to_parent_table(self): - """Return True if the join condition contains column - comparisons where both columns are in both tables. - - """ - pt = self.parent_selectable - mt = self.child_selectable - result = [False] - - def visit_binary(binary): - c, f = binary.left, binary.right - if ( - isinstance(c, expression.ColumnClause) and \ - isinstance(f, expression.ColumnClause) and \ - pt.is_derived_from(c.table) and \ - pt.is_derived_from(f.table) and \ - mt.is_derived_from(c.table) and \ - mt.is_derived_from(f.table) - ): - result[0] = True - visitors.traverse( - self.primaryjoin, - {}, - {"binary": visit_binary} - ) - return result[0] - - def _tables_overlap(self): - """Return True if parent/child tables have some overlap.""" - - return selectables_overlap(self.parent_selectable, self.child_selectable) - - def _annotate_remote(self): - """Annotate the primaryjoin and secondaryjoin - structures with 'remote' annotations marking columns - considered as part of the 'remote' side. - - """ - if self._has_remote_annotations: - return - - if self.secondary is not None: - self._annotate_remote_secondary() - elif self._local_remote_pairs or self._remote_side: - self._annotate_remote_from_args() - elif self._refers_to_parent_table(): - self._annotate_selfref(lambda col: "foreign" in col._annotations) - elif self._tables_overlap(): - self._annotate_remote_with_overlap() - else: - self._annotate_remote_distinct_selectables() - - def _annotate_remote_secondary(self): - """annotate 'remote' in primaryjoin, secondaryjoin - when 'secondary' is present. - - """ - def repl(element): - if self.secondary.c.contains_column(element): - return element._annotate({"remote": True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, repl) - self.secondaryjoin = visitors.replacement_traverse( - self.secondaryjoin, {}, repl) - - def _annotate_selfref(self, fn): - """annotate 'remote' in primaryjoin, secondaryjoin - when the relationship is detected as self-referential. - - """ - def visit_binary(binary): - equated = binary.left.compare(binary.right) - if isinstance(binary.left, expression.ColumnClause) and \ - isinstance(binary.right, expression.ColumnClause): - # assume one to many - FKs are "remote" - if fn(binary.left): - binary.left = binary.left._annotate({"remote": True}) - if fn(binary.right) and not equated: - binary.right = binary.right._annotate( - {"remote": True}) - else: - self._warn_non_column_elements() - - self.primaryjoin = visitors.cloned_traverse( - self.primaryjoin, {}, - {"binary": visit_binary}) - - def _annotate_remote_from_args(self): - """annotate 'remote' in primaryjoin, secondaryjoin - when the 'remote_side' or '_local_remote_pairs' - arguments are used. - - """ - if self._local_remote_pairs: - if self._remote_side: - raise sa_exc.ArgumentError( - "remote_side argument is redundant " - "against more detailed _local_remote_side " - "argument.") - - remote_side = [r for (l, r) in self._local_remote_pairs] - else: - remote_side = self._remote_side - - if self._refers_to_parent_table(): - self._annotate_selfref(lambda col: col in remote_side) - else: - def repl(element): - if element in remote_side: - return element._annotate({"remote": True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, repl) - - def _annotate_remote_with_overlap(self): - """annotate 'remote' in primaryjoin, secondaryjoin - when the parent/child tables have some set of - tables in common, though is not a fully self-referential - relationship. - - """ - def visit_binary(binary): - binary.left, binary.right = proc_left_right(binary.left, - binary.right) - binary.right, binary.left = proc_left_right(binary.right, - binary.left) - - def proc_left_right(left, right): - if isinstance(left, expression.ColumnClause) and \ - isinstance(right, expression.ColumnClause): - if self.child_selectable.c.contains_column(right) and \ - self.parent_selectable.c.contains_column(left): - right = right._annotate({"remote": True}) - else: - self._warn_non_column_elements() - - return left, right - - self.primaryjoin = visitors.cloned_traverse( - self.primaryjoin, {}, - {"binary": visit_binary}) - - def _annotate_remote_distinct_selectables(self): - """annotate 'remote' in primaryjoin, secondaryjoin - when the parent/child tables are entirely - separate. - - """ - def repl(element): - if self.child_selectable.c.contains_column(element) and \ - ( - not self.parent_local_selectable.c.\ - contains_column(element) - or self.child_local_selectable.c.\ - contains_column(element)): - return element._annotate({"remote": True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, repl) - - def _warn_non_column_elements(self): - util.warn( - "Non-simple column elements in primary " - "join condition for property %s - consider using " - "remote() annotations to mark the remote side." - % self.prop - ) - - def _annotate_local(self): - """Annotate the primaryjoin and secondaryjoin - structures with 'local' annotations. - - This annotates all column elements found - simultaneously in the parent table - and the join condition that don't have a - 'remote' annotation set up from - _annotate_remote() or user-defined. - - """ - if self._has_annotation(self.primaryjoin, "local"): - return - - if self._local_remote_pairs: - local_side = util.column_set([l for (l, r) - in self._local_remote_pairs]) - else: - local_side = util.column_set(self.parent_selectable.c) - - def locals_(elem): - if "remote" not in elem._annotations and \ - elem in local_side: - return elem._annotate({"local": True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, locals_ - ) - - def _check_remote_side(self): - if not self.local_remote_pairs: - raise sa_exc.ArgumentError('Relationship %s could ' - 'not determine any unambiguous local/remote column ' - 'pairs based on join condition and remote_side ' - 'arguments. ' - 'Consider using the remote() annotation to ' - 'accurately mark those elements of the join ' - 'condition that are on the remote side of ' - 'the relationship.' - % (self.prop, )) - - def _check_foreign_cols(self, join_condition, primary): - """Check the foreign key columns collected and emit error - messages.""" - - can_sync = False - - foreign_cols = self._gather_columns_with_annotation( - join_condition, "foreign") - - has_foreign = bool(foreign_cols) - - if primary: - can_sync = bool(self.synchronize_pairs) - else: - can_sync = bool(self.secondary_synchronize_pairs) - - if self.support_sync and can_sync or \ - (not self.support_sync and has_foreign): - return - - # from here below is just determining the best error message - # to report. Check for a join condition using any operator - # (not just ==), perhaps they need to turn on "viewonly=True". - if self.support_sync and has_foreign and not can_sync: - err = "Could not locate any simple equality expressions "\ - "involving locally mapped foreign key columns for "\ - "%s join condition "\ - "'%s' on relationship %s." % ( - primary and 'primary' or 'secondary', - join_condition, - self.prop - ) - err += \ - " Ensure that referencing columns are associated "\ - "with a ForeignKey or ForeignKeyConstraint, or are "\ - "annotated in the join condition with the foreign() "\ - "annotation. To allow comparison operators other than "\ - "'==', the relationship can be marked as viewonly=True." - - raise sa_exc.ArgumentError(err) - else: - err = "Could not locate any relevant foreign key columns "\ - "for %s join condition '%s' on relationship %s." % ( - primary and 'primary' or 'secondary', - join_condition, - self.prop - ) - err += \ - ' Ensure that referencing columns are associated '\ - 'with a ForeignKey or ForeignKeyConstraint, or are '\ - 'annotated in the join condition with the foreign() '\ - 'annotation.' - raise sa_exc.ArgumentError(err) - - def _determine_direction(self): - """Determine if this relationship is one to many, many to one, - many to many. - - """ - if self.secondaryjoin is not None: - self.direction = MANYTOMANY - else: - parentcols = util.column_set(self.parent_selectable.c) - targetcols = util.column_set(self.child_selectable.c) - - # fk collection which suggests ONETOMANY. - onetomany_fk = targetcols.intersection( - self.foreign_key_columns) - - # fk collection which suggests MANYTOONE. - - manytoone_fk = parentcols.intersection( - self.foreign_key_columns) - - if onetomany_fk and manytoone_fk: - # fks on both sides. test for overlap of local/remote - # with foreign key - self_equated = self.remote_columns.intersection( - self.local_columns - ) - onetomany_local = self.remote_columns.\ - intersection(self.foreign_key_columns).\ - difference(self_equated) - manytoone_local = self.local_columns.\ - intersection(self.foreign_key_columns).\ - difference(self_equated) - if onetomany_local and not manytoone_local: - self.direction = ONETOMANY - elif manytoone_local and not onetomany_local: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError( - "Can't determine relationship" - " direction for relationship '%s' - foreign " - "key columns within the join condition are present " - "in both the parent and the child's mapped tables. " - "Ensure that only those columns referring " - "to a parent column are marked as foreign, " - "either via the foreign() annotation or " - "via the foreign_keys argument." % self.prop) - elif onetomany_fk: - self.direction = ONETOMANY - elif manytoone_fk: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError("Can't determine relationship " - "direction for relationship '%s' - foreign " - "key columns are present in neither the parent " - "nor the child's mapped tables" % self.prop) - - def _deannotate_pairs(self, collection): - """provide deannotation for the various lists of - pairs, so that using them in hashes doesn't incur - high-overhead __eq__() comparisons against - original columns mapped. - - """ - return [(x._deannotate(), y._deannotate()) - for x, y in collection] - - def _setup_pairs(self): - sync_pairs = [] - lrp = util.OrderedSet([]) - secondary_sync_pairs = [] - - def go(joincond, collection): - def visit_binary(binary, left, right): - if "remote" in right._annotations and \ - "remote" not in left._annotations and \ - self.can_be_synced_fn(left): - lrp.add((left, right)) - elif "remote" in left._annotations and \ - "remote" not in right._annotations and \ - self.can_be_synced_fn(right): - lrp.add((right, left)) - if binary.operator is operators.eq and \ - self.can_be_synced_fn(left, right): - if "foreign" in right._annotations: - collection.append((left, right)) - elif "foreign" in left._annotations: - collection.append((right, left)) - visit_binary_product(visit_binary, joincond) - - for joincond, collection in [ - (self.primaryjoin, sync_pairs), - (self.secondaryjoin, secondary_sync_pairs) - ]: - if joincond is None: - continue - go(joincond, collection) - - self.local_remote_pairs = self._deannotate_pairs(lrp) - self.synchronize_pairs = self._deannotate_pairs(sync_pairs) - self.secondary_synchronize_pairs = \ - self._deannotate_pairs(secondary_sync_pairs) - - @util.memoized_property - def remote_columns(self): - return self._gather_join_annotations("remote") - - @util.memoized_property - def local_columns(self): - return self._gather_join_annotations("local") - - @util.memoized_property - def foreign_key_columns(self): - return self._gather_join_annotations("foreign") - - @util.memoized_property - def deannotated_primaryjoin(self): - return _deep_deannotate(self.primaryjoin) - - @util.memoized_property - def deannotated_secondaryjoin(self): - if self.secondaryjoin is not None: - return _deep_deannotate(self.secondaryjoin) - else: - return None - - def _gather_join_annotations(self, annotation): - s = set( - self._gather_columns_with_annotation( - self.primaryjoin, annotation) - ) - if self.secondaryjoin is not None: - s.update( - self._gather_columns_with_annotation( - self.secondaryjoin, annotation) - ) - return set([x._deannotate() for x in s]) - - def _gather_columns_with_annotation(self, clause, *annotation): - annotation = set(annotation) - return set([ - col for col in visitors.iterate(clause, {}) - if annotation.issubset(col._annotations) - ]) - - def join_targets(self, source_selectable, - dest_selectable, - aliased, - single_crit=None): - """Given a source and destination selectable, create a - join between them. - - This takes into account aliasing the join clause - to reference the appropriate corresponding columns - in the target objects, as well as the extra child - criterion, equivalent column sets, etc. - - """ - - # place a barrier on the destination such that - # replacement traversals won't ever dig into it. - # its internal structure remains fixed - # regardless of context. - dest_selectable = _shallow_annotate( - dest_selectable, - {'no_replacement_traverse': True}) - - primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ - self.secondaryjoin, self.secondary - - # adjust the join condition for single table inheritance, - # in the case that the join is to a subclass - # this is analogous to the - # "_adjust_for_single_table_inheritance()" method in Query. - - if single_crit is not None: - if secondaryjoin is not None: - secondaryjoin = secondaryjoin & single_crit - else: - primaryjoin = primaryjoin & single_crit - - if aliased: - if secondary is not None: - secondary = secondary.alias() - primary_aliasizer = ClauseAdapter(secondary) - secondary_aliasizer = \ - ClauseAdapter(dest_selectable, - equivalents=self.child_equivalents).\ - chain(primary_aliasizer) - if source_selectable is not None: - primary_aliasizer = \ - ClauseAdapter(secondary).\ - chain(ClauseAdapter(source_selectable, - equivalents=self.parent_equivalents)) - secondaryjoin = \ - secondary_aliasizer.traverse(secondaryjoin) - else: - primary_aliasizer = ClauseAdapter(dest_selectable, - exclude_fn=_ColInAnnotations("local"), - equivalents=self.child_equivalents) - if source_selectable is not None: - primary_aliasizer.chain( - ClauseAdapter(source_selectable, - exclude_fn=_ColInAnnotations("remote"), - equivalents=self.parent_equivalents)) - secondary_aliasizer = None - - primaryjoin = primary_aliasizer.traverse(primaryjoin) - target_adapter = secondary_aliasizer or primary_aliasizer - target_adapter.exclude_fn = None - else: - target_adapter = None - return primaryjoin, secondaryjoin, secondary, \ - target_adapter, dest_selectable - - def create_lazy_clause(self, reverse_direction=False): - binds = util.column_dict() - lookup = util.column_dict() - equated_columns = util.column_dict() - - if reverse_direction and self.secondaryjoin is None: - for l, r in self.local_remote_pairs: - _list = lookup.setdefault(r, []) - _list.append((r, l)) - equated_columns[l] = r - else: - for l, r in self.local_remote_pairs: - _list = lookup.setdefault(l, []) - _list.append((l, r)) - equated_columns[r] = l - - def col_to_bind(col): - if col in lookup: - for tobind, equated in lookup[col]: - if equated in binds: - return None - if col not in binds: - binds[col] = sql.bindparam( - None, None, type_=col.type, unique=True) - return binds[col] - return None - - lazywhere = self.deannotated_primaryjoin - - if self.deannotated_secondaryjoin is None or not reverse_direction: - lazywhere = visitors.replacement_traverse( - lazywhere, {}, col_to_bind) - - if self.deannotated_secondaryjoin is not None: - secondaryjoin = self.deannotated_secondaryjoin - if reverse_direction: - secondaryjoin = visitors.replacement_traverse( - secondaryjoin, {}, col_to_bind) - lazywhere = sql.and_(lazywhere, secondaryjoin) - - bind_to_col = dict((binds[col].key, col) for col in binds) - - return lazywhere, bind_to_col, equated_columns - -class _ColInAnnotations(object): - """Seralizable equivalent to: - - lambda c: "name" in c._annotations - """ - def __init__(self, name): - self.name = name - - def __call__(self, c): - return self.name in c._annotations diff --git a/libs/sqlalchemy/orm/scoping.py b/libs/sqlalchemy/orm/scoping.py deleted file mode 100644 index c1f8f319..00000000 --- a/libs/sqlalchemy/orm/scoping.py +++ /dev/null @@ -1,176 +0,0 @@ -# orm/scoping.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .. import exc as sa_exc -from ..util import ScopedRegistry, ThreadLocalRegistry, warn -from . import class_mapper, exc as orm_exc -from .session import Session - - -__all__ = ['scoped_session'] - - -class scoped_session(object): - """Provides scoped management of :class:`.Session` objects. - - See :ref:`unitofwork_contextual` for a tutorial. - - """ - - def __init__(self, session_factory, scopefunc=None): - """Construct a new :class:`.scoped_session`. - - :param session_factory: a factory to create new :class:`.Session` - instances. This is usually, but not necessarily, an instance - of :class:`.sessionmaker`. - :param scopefunc: optional function which defines - the current scope. If not passed, the :class:`.scoped_session` - object assumes "thread-local" scope, and will use - a Python ``threading.local()`` in order to maintain the current - :class:`.Session`. If passed, the function should return - a hashable token; this token will be used as the key in a - dictionary in order to store and retrieve the current - :class:`.Session`. - - """ - self.session_factory = session_factory - if scopefunc: - self.registry = ScopedRegistry(session_factory, scopefunc) - else: - self.registry = ThreadLocalRegistry(session_factory) - - def __call__(self, **kw): - """Return the current :class:`.Session`, creating it - using the session factory if not present. - - :param \**kw: Keyword arguments will be passed to the - session factory callable, if an existing :class:`.Session` - is not present. If the :class:`.Session` is present and - keyword arguments have been passed, - :exc:`~sqlalchemy.exc.InvalidRequestError` is raised. - - """ - if kw: - scope = kw.pop('scope', False) - if scope is not None: - if self.registry.has(): - raise sa_exc.InvalidRequestError( - "Scoped session is already present; " - "no new arguments may be specified.") - else: - sess = self.session_factory(**kw) - self.registry.set(sess) - return sess - else: - return self.session_factory(**kw) - else: - return self.registry() - - def remove(self): - """Dispose of the current :class:`.Session`, if present. - - This will first call :meth:`.Session.close` method - on the current :class:`.Session`, which releases any existing - transactional/connection resources still being held; transactions - specifically are rolled back. The :class:`.Session` is then - discarded. Upon next usage within the same scope, - the :class:`.scoped_session` will produce a new - :class:`.Session` object. - - """ - - if self.registry.has(): - self.registry().close() - self.registry.clear() - - def configure(self, **kwargs): - """reconfigure the :class:`.sessionmaker` used by this - :class:`.scoped_session`. - - See :meth:`.sessionmaker.configure`. - - """ - - if self.registry.has(): - warn('At least one scoped session is already present. ' - ' configure() can not affect sessions that have ' - 'already been created.') - - self.session_factory.configure(**kwargs) - - def query_property(self, query_cls=None): - """return a class property which produces a :class:`.Query` object - against the class and the current :class:`.Session` when called. - - e.g.:: - - Session = scoped_session(sessionmaker()) - - class MyClass(object): - query = Session.query_property() - - # after mappers are defined - result = MyClass.query.filter(MyClass.name=='foo').all() - - Produces instances of the session's configured query class by - default. To override and use a custom implementation, provide - a ``query_cls`` callable. The callable will be invoked with - the class's mapper as a positional argument and a session - keyword argument. - - There is no limit to the number of query properties placed on - a class. - - """ - class query(object): - def __get__(s, instance, owner): - try: - mapper = class_mapper(owner) - if mapper: - if query_cls: - # custom query class - return query_cls(mapper, session=self.registry()) - else: - # session's configured query class - return self.registry().query(mapper) - except orm_exc.UnmappedClassError: - return None - return query() - -ScopedSession = scoped_session -"""Old name for backwards compatibility.""" - - -def instrument(name): - def do(self, *args, **kwargs): - return getattr(self.registry(), name)(*args, **kwargs) - return do - -for meth in Session.public_methods: - setattr(scoped_session, meth, instrument(meth)) - - -def makeprop(name): - def set(self, attr): - setattr(self.registry(), name, attr) - - def get(self): - return getattr(self.registry(), name) - - return property(get, set) - -for prop in ('bind', 'dirty', 'deleted', 'new', 'identity_map', - 'is_active', 'autoflush', 'no_autoflush', 'info'): - setattr(scoped_session, prop, makeprop(prop)) - - -def clslevel(name): - def do(cls, *args, **kwargs): - return getattr(Session, name)(*args, **kwargs) - return classmethod(do) - -for prop in ('close_all', 'object_session', 'identity_key'): - setattr(scoped_session, prop, clslevel(prop)) diff --git a/libs/sqlalchemy/orm/session.py b/libs/sqlalchemy/orm/session.py deleted file mode 100644 index c10a0efc..00000000 --- a/libs/sqlalchemy/orm/session.py +++ /dev/null @@ -1,2383 +0,0 @@ -# orm/session.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Provides the Session class and related utilities.""" - - - -import weakref -from .. import util, sql, engine, exc as sa_exc -from ..sql import util as sql_util, expression -from . import ( - SessionExtension, attributes, exc, query, - loading, identity - ) -from ..inspection import inspect -from .base import ( - object_mapper, class_mapper, - _class_to_mapper, _state_mapper, object_state, - _none_set, state_str, instance_str - ) -from .unitofwork import UOWTransaction -from . import state as statelib -import sys - -__all__ = ['Session', 'SessionTransaction', 'SessionExtension', 'sessionmaker'] - -_sessions = weakref.WeakValueDictionary() -"""Weak-referencing dictionary of :class:`.Session` objects. -""" - -def _state_session(state): - """Given an :class:`.InstanceState`, return the :class:`.Session` - associated, if any. - """ - if state.session_id: - try: - return _sessions[state.session_id] - except KeyError: - pass - return None - - - -class _SessionClassMethods(object): - """Class-level methods for :class:`.Session`, :class:`.sessionmaker`.""" - - @classmethod - def close_all(cls): - """Close *all* sessions in memory.""" - - for sess in _sessions.values(): - sess.close() - - @classmethod - @util.dependencies("sqlalchemy.orm.util") - def identity_key(cls, orm_util, *args, **kwargs): - """Return an identity key. - - This is an alias of :func:`.util.identity_key`. - - """ - return orm_util.identity_key(*args, **kwargs) - - @classmethod - def object_session(cls, instance): - """Return the :class:`.Session` to which an object belongs. - - This is an alias of :func:`.object_session`. - - """ - - return object_session(instance) - - -ACTIVE = util.symbol('ACTIVE') -PREPARED = util.symbol('PREPARED') -COMMITTED = util.symbol('COMMITTED') -DEACTIVE = util.symbol('DEACTIVE') -CLOSED = util.symbol('CLOSED') - -class SessionTransaction(object): - """A :class:`.Session`-level transaction. - - :class:`.SessionTransaction` is a mostly behind-the-scenes object - not normally referenced directly by application code. It coordinates - among multiple :class:`.Connection` objects, maintaining a database - transaction for each one individually, committing or rolling them - back all at once. It also provides optional two-phase commit behavior - which can augment this coordination operation. - - The :attr:`.Session.transaction` attribute of :class:`.Session` - refers to the current :class:`.SessionTransaction` object in use, if any. - - - A :class:`.SessionTransaction` is associated with a :class:`.Session` - in its default mode of ``autocommit=False`` immediately, associated - with no database connections. As the :class:`.Session` is called upon - to emit SQL on behalf of various :class:`.Engine` or :class:`.Connection` - objects, a corresponding :class:`.Connection` and associated - :class:`.Transaction` is added to a collection within the - :class:`.SessionTransaction` object, becoming one of the - connection/transaction pairs maintained by the - :class:`.SessionTransaction`. - - The lifespan of the :class:`.SessionTransaction` ends when the - :meth:`.Session.commit`, :meth:`.Session.rollback` or - :meth:`.Session.close` methods are called. At this point, the - :class:`.SessionTransaction` removes its association with its parent - :class:`.Session`. A :class:`.Session` that is in ``autocommit=False`` - mode will create a new :class:`.SessionTransaction` to replace it - immediately, whereas a :class:`.Session` that's in ``autocommit=True`` - mode will remain without a :class:`.SessionTransaction` until the - :meth:`.Session.begin` method is called. - - Another detail of :class:`.SessionTransaction` behavior is that it is - capable of "nesting". This means that the :meth:`.Session.begin` method - can be called while an existing :class:`.SessionTransaction` is already - present, producing a new :class:`.SessionTransaction` that temporarily - replaces the parent :class:`.SessionTransaction`. When a - :class:`.SessionTransaction` is produced as nested, it assigns itself to - the :attr:`.Session.transaction` attribute. When it is ended via - :meth:`.Session.commit` or :meth:`.Session.rollback`, it restores its - parent :class:`.SessionTransaction` back onto the - :attr:`.Session.transaction` attribute. The behavior is effectively a - stack, where :attr:`.Session.transaction` refers to the current head of - the stack. - - The purpose of this stack is to allow nesting of - :meth:`.Session.rollback` or :meth:`.Session.commit` calls in context - with various flavors of :meth:`.Session.begin`. This nesting behavior - applies to when :meth:`.Session.begin_nested` is used to emit a - SAVEPOINT transaction, and is also used to produce a so-called - "subtransaction" which allows a block of code to use a - begin/rollback/commit sequence regardless of whether or not its enclosing - code block has begun a transaction. The :meth:`.flush` method, whether - called explicitly or via autoflush, is the primary consumer of the - "subtransaction" feature, in that it wishes to guarantee that it works - within in a transaction block regardless of whether or not the - :class:`.Session` is in transactional mode when the method is called. - - See also: - - :meth:`.Session.rollback` - - :meth:`.Session.commit` - - :meth:`.Session.begin` - - :meth:`.Session.begin_nested` - - :attr:`.Session.is_active` - - :meth:`.SessionEvents.after_commit` - - :meth:`.SessionEvents.after_rollback` - - :meth:`.SessionEvents.after_soft_rollback` - - """ - - _rollback_exception = None - - def __init__(self, session, parent=None, nested=False): - self.session = session - self._connections = {} - self._parent = parent - self.nested = nested - self._state = ACTIVE - if not parent and nested: - raise sa_exc.InvalidRequestError( - "Can't start a SAVEPOINT transaction when no existing " - "transaction is in progress") - - if self.session._enable_transaction_accounting: - self._take_snapshot() - - if self.session.dispatch.after_transaction_create: - self.session.dispatch.after_transaction_create(self.session, self) - - @property - def is_active(self): - return self.session is not None and self._state is ACTIVE - - def _assert_active(self, prepared_ok=False, - rollback_ok=False, - deactive_ok=False, - closed_msg="This transaction is closed"): - if self._state is COMMITTED: - raise sa_exc.InvalidRequestError( - "This session is in 'committed' state; no further " - "SQL can be emitted within this transaction." - ) - elif self._state is PREPARED: - if not prepared_ok: - raise sa_exc.InvalidRequestError( - "This session is in 'prepared' state; no further " - "SQL can be emitted within this transaction." - ) - elif self._state is DEACTIVE: - if not deactive_ok and not rollback_ok: - if self._rollback_exception: - raise sa_exc.InvalidRequestError( - "This Session's transaction has been rolled back " - "due to a previous exception during flush." - " To begin a new transaction with this Session, " - "first issue Session.rollback()." - " Original exception was: %s" - % self._rollback_exception - ) - elif not deactive_ok: - raise sa_exc.InvalidRequestError( - "This Session's transaction has been rolled back " - "by a nested rollback() call. To begin a new " - "transaction, issue Session.rollback() first." - ) - elif self._state is CLOSED: - raise sa_exc.ResourceClosedError(closed_msg) - - @property - def _is_transaction_boundary(self): - return self.nested or not self._parent - - def connection(self, bindkey, **kwargs): - self._assert_active() - bind = self.session.get_bind(bindkey, **kwargs) - return self._connection_for_bind(bind) - - def _begin(self, nested=False): - self._assert_active() - return SessionTransaction( - self.session, self, nested=nested) - - def _iterate_parents(self, upto=None): - if self._parent is upto: - return (self,) - else: - if self._parent is None: - raise sa_exc.InvalidRequestError( - "Transaction %s is not on the active transaction list" % ( - upto)) - return (self,) + self._parent._iterate_parents(upto) - - def _take_snapshot(self): - if not self._is_transaction_boundary: - self._new = self._parent._new - self._deleted = self._parent._deleted - self._dirty = self._parent._dirty - self._key_switches = self._parent._key_switches - return - - if not self.session._flushing: - self.session.flush() - - self._new = weakref.WeakKeyDictionary() - self._deleted = weakref.WeakKeyDictionary() - self._dirty = weakref.WeakKeyDictionary() - self._key_switches = weakref.WeakKeyDictionary() - - def _restore_snapshot(self, dirty_only=False): - assert self._is_transaction_boundary - - for s in set(self._new).union(self.session._new): - self.session._expunge_state(s) - if s.key: - del s.key - - for s, (oldkey, newkey) in self._key_switches.items(): - self.session.identity_map.discard(s) - s.key = oldkey - self.session.identity_map.replace(s) - - for s in set(self._deleted).union(self.session._deleted): - if s.deleted: - #assert s in self._deleted - del s.deleted - self.session._update_impl(s, discard_existing=True) - - assert not self.session._deleted - - for s in self.session.identity_map.all_states(): - if not dirty_only or s.modified or s in self._dirty: - s._expire(s.dict, self.session.identity_map._modified) - - def _remove_snapshot(self): - assert self._is_transaction_boundary - - if not self.nested and self.session.expire_on_commit: - for s in self.session.identity_map.all_states(): - s._expire(s.dict, self.session.identity_map._modified) - for s in self._deleted: - s.session_id = None - self._deleted.clear() - - - def _connection_for_bind(self, bind): - self._assert_active() - - if bind in self._connections: - return self._connections[bind][0] - - if self._parent: - conn = self._parent._connection_for_bind(bind) - if not self.nested: - return conn - else: - if isinstance(bind, engine.Connection): - conn = bind - if conn.engine in self._connections: - raise sa_exc.InvalidRequestError( - "Session already has a Connection associated for the " - "given Connection's Engine") - else: - conn = bind.contextual_connect() - - if self.session.twophase and self._parent is None: - transaction = conn.begin_twophase() - elif self.nested: - transaction = conn.begin_nested() - else: - transaction = conn.begin() - - self._connections[conn] = self._connections[conn.engine] = \ - (conn, transaction, conn is not bind) - self.session.dispatch.after_begin(self.session, self, conn) - return conn - - def prepare(self): - if self._parent is not None or not self.session.twophase: - raise sa_exc.InvalidRequestError( - "'twophase' mode not enabled, or not root transaction; " - "can't prepare.") - self._prepare_impl() - - def _prepare_impl(self): - self._assert_active() - if self._parent is None or self.nested: - self.session.dispatch.before_commit(self.session) - - stx = self.session.transaction - if stx is not self: - for subtransaction in stx._iterate_parents(upto=self): - subtransaction.commit() - - if not self.session._flushing: - for _flush_guard in range(100): - if self.session._is_clean(): - break - self.session.flush() - else: - raise exc.FlushError( - "Over 100 subsequent flushes have occurred within " - "session.commit() - is an after_flush() hook " - "creating new objects?") - - if self._parent is None and self.session.twophase: - try: - for t in set(self._connections.values()): - t[1].prepare() - except: - with util.safe_reraise(): - self.rollback() - - self._state = PREPARED - - def commit(self): - self._assert_active(prepared_ok=True) - if self._state is not PREPARED: - self._prepare_impl() - - if self._parent is None or self.nested: - for t in set(self._connections.values()): - t[1].commit() - - self._state = COMMITTED - self.session.dispatch.after_commit(self.session) - - if self.session._enable_transaction_accounting: - self._remove_snapshot() - - self.close() - return self._parent - - def rollback(self, _capture_exception=False): - self._assert_active(prepared_ok=True, rollback_ok=True) - - stx = self.session.transaction - if stx is not self: - for subtransaction in stx._iterate_parents(upto=self): - subtransaction.close() - - if self._state in (ACTIVE, PREPARED): - for transaction in self._iterate_parents(): - if transaction._parent is None or transaction.nested: - transaction._rollback_impl() - transaction._state = DEACTIVE - break - else: - transaction._state = DEACTIVE - - sess = self.session - - if self.session._enable_transaction_accounting and \ - not sess._is_clean(): - # if items were added, deleted, or mutated - # here, we need to re-restore the snapshot - util.warn( - "Session's state has been changed on " - "a non-active transaction - this state " - "will be discarded.") - self._restore_snapshot(dirty_only=self.nested) - - self.close() - if self._parent and _capture_exception: - self._parent._rollback_exception = sys.exc_info()[1] - - sess.dispatch.after_soft_rollback(sess, self) - - return self._parent - - def _rollback_impl(self): - for t in set(self._connections.values()): - t[1].rollback() - - if self.session._enable_transaction_accounting: - self._restore_snapshot(dirty_only=self.nested) - - self.session.dispatch.after_rollback(self.session) - - def close(self): - self.session.transaction = self._parent - if self._parent is None: - for connection, transaction, autoclose in \ - set(self._connections.values()): - if autoclose: - connection.close() - else: - transaction.close() - - self._state = CLOSED - if self.session.dispatch.after_transaction_end: - self.session.dispatch.after_transaction_end(self.session, self) - - if self._parent is None: - if not self.session.autocommit: - self.session.begin() - self.session = None - self._connections = None - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self._assert_active(deactive_ok=True, prepared_ok=True) - if self.session.transaction is None: - return - if type is None: - try: - self.commit() - except: - with util.safe_reraise(): - self.rollback() - else: - self.rollback() - - -class Session(_SessionClassMethods): - """Manages persistence operations for ORM-mapped objects. - - The Session's usage paradigm is described at :doc:`/orm/session`. - - - """ - - public_methods = ( - '__contains__', '__iter__', 'add', 'add_all', 'begin', 'begin_nested', - 'close', 'commit', 'connection', 'delete', 'execute', 'expire', - 'expire_all', 'expunge', 'expunge_all', 'flush', 'get_bind', - 'is_modified', - 'merge', 'query', 'refresh', 'rollback', - 'scalar') - - def __init__(self, bind=None, autoflush=True, expire_on_commit=True, - _enable_transaction_accounting=True, - autocommit=False, twophase=False, - weak_identity_map=True, binds=None, extension=None, - info=None, - query_cls=query.Query): - """Construct a new Session. - - See also the :class:`.sessionmaker` function which is used to - generate a :class:`.Session`-producing callable with a given - set of arguments. - - :param autocommit: - - .. warning:: - - The autocommit flag is **not for general use**, and if it is used, - queries should only be invoked within the span of a - :meth:`.Session.begin` / :meth:`.Session.commit` pair. Executing - queries outside of a demarcated transaction is a legacy mode - of usage, and can in some cases lead to concurrent connection - checkouts. - - Defaults to ``False``. When ``True``, the - :class:`.Session` does not keep a persistent transaction running, and - will acquire connections from the engine on an as-needed basis, - returning them immediately after their use. Flushes will begin and - commit (or possibly rollback) their own transaction if no - transaction is present. When using this mode, the - :meth:`.Session.begin` method is used to explicitly start - transactions. - - .. seealso:: - - :ref:`session_autocommit` - - :param autoflush: When ``True``, all query operations will issue a - ``flush()`` call to this ``Session`` before proceeding. This is a - convenience feature so that ``flush()`` need not be called - repeatedly in order for database queries to retrieve results. It's - typical that ``autoflush`` is used in conjunction with - ``autocommit=False``. In this scenario, explicit calls to - ``flush()`` are rarely needed; you usually only need to call - ``commit()`` (which flushes) to finalize changes. - - :param bind: An optional ``Engine`` or ``Connection`` to which this - ``Session`` should be bound. When specified, all SQL operations - performed by this session will execute via this connectable. - - :param binds: An optional dictionary which contains more granular - "bind" information than the ``bind`` parameter provides. This - dictionary can map individual ``Table`` instances as well as - ``Mapper`` instances to individual ``Engine`` or ``Connection`` - objects. Operations which proceed relative to a particular - ``Mapper`` will consult this dictionary for the direct ``Mapper`` - instance as well as the mapper's ``mapped_table`` attribute in - order to locate an connectable to use. The full resolution is - described in the ``get_bind()`` method of ``Session``. - Usage looks like:: - - Session = sessionmaker(binds={ - SomeMappedClass: create_engine('postgresql://engine1'), - somemapper: create_engine('postgresql://engine2'), - some_table: create_engine('postgresql://engine3'), - }) - - Also see the :meth:`.Session.bind_mapper` - and :meth:`.Session.bind_table` methods. - - :param \class_: Specify an alternate class other than - ``sqlalchemy.orm.session.Session`` which should be used by the - returned class. This is the only argument that is local to the - ``sessionmaker()`` function, and is not sent directly to the - constructor for ``Session``. - - :param _enable_transaction_accounting: Defaults to ``True``. A - legacy-only flag which when ``False`` disables *all* 0.5-style - object accounting on transaction boundaries, including auto-expiry - of instances on rollback and commit, maintenance of the "new" and - "deleted" lists upon rollback, and autoflush of pending changes upon - begin(), all of which are interdependent. - - :param expire_on_commit: Defaults to ``True``. When ``True``, all - instances will be fully expired after each ``commit()``, so that - all attribute/object access subsequent to a completed transaction - will load from the most recent database state. - - :param extension: An optional - :class:`~.SessionExtension` instance, or a list - of such instances, which will receive pre- and post- commit and - flush events, as well as a post-rollback event. **Deprecated.** - Please see :class:`.SessionEvents`. - - :param info: optional dictionary of arbitrary data to be associated - with this :class:`.Session`. Is available via the :attr:`.Session.info` - attribute. Note the dictionary is copied at construction time so - that modifications to the per-:class:`.Session` dictionary will be local - to that :class:`.Session`. - - .. versionadded:: 0.9.0 - - :param query_cls: Class which should be used to create new Query - objects, as returned by the ``query()`` method. Defaults to - :class:`~sqlalchemy.orm.query.Query`. - - :param twophase: When ``True``, all transactions will be started as - a "two phase" transaction, i.e. using the "two phase" semantics - of the database in use along with an XID. During a ``commit()``, - after ``flush()`` has been issued for all attached databases, the - ``prepare()`` method on each database's ``TwoPhaseTransaction`` - will be called. This allows each database to roll back the entire - transaction, before each transaction is committed. - - :param weak_identity_map: Defaults to ``True`` - when set to - ``False``, objects placed in the :class:`.Session` will be - strongly referenced until explicitly removed or the - :class:`.Session` is closed. **Deprecated** - this option - is obsolete. - - """ - - if weak_identity_map: - self._identity_cls = identity.WeakInstanceDict - else: - util.warn_deprecated("weak_identity_map=False is deprecated. " - "This feature is not needed.") - self._identity_cls = identity.StrongInstanceDict - self.identity_map = self._identity_cls() - - self._new = {} # InstanceState->object, strong refs object - self._deleted = {} # same - self.bind = bind - self.__binds = {} - self._flushing = False - self._warn_on_events = False - self.transaction = None - self.hash_key = _new_sessionid() - self.autoflush = autoflush - self.autocommit = autocommit - self.expire_on_commit = expire_on_commit - self._enable_transaction_accounting = _enable_transaction_accounting - self.twophase = twophase - self._query_cls = query_cls - if info: - self.info.update(info) - - if extension: - for ext in util.to_list(extension): - SessionExtension._adapt_listener(self, ext) - - if binds is not None: - for mapperortable, bind in binds.items(): - insp = inspect(mapperortable) - if insp.is_selectable: - self.bind_table(mapperortable, bind) - elif insp.is_mapper: - self.bind_mapper(mapperortable, bind) - else: - assert False - - - if not self.autocommit: - self.begin() - _sessions[self.hash_key] = self - - connection_callable = None - - transaction = None - """The current active or inactive :class:`.SessionTransaction`.""" - - @util.memoized_property - def info(self): - """A user-modifiable dictionary. - - The initial value of this dictioanry can be populated using the - ``info`` argument to the :class:`.Session` constructor or - :class:`.sessionmaker` constructor or factory methods. The dictionary - here is always local to this :class:`.Session` and can be modified - independently of all other :class:`.Session` objects. - - .. versionadded:: 0.9.0 - - """ - return {} - - def begin(self, subtransactions=False, nested=False): - """Begin a transaction on this Session. - - If this Session is already within a transaction, either a plain - transaction or nested transaction, an error is raised, unless - ``subtransactions=True`` or ``nested=True`` is specified. - - The ``subtransactions=True`` flag indicates that this - :meth:`~.Session.begin` can create a subtransaction if a transaction - is already in progress. For documentation on subtransactions, please - see :ref:`session_subtransactions`. - - The ``nested`` flag begins a SAVEPOINT transaction and is equivalent - to calling :meth:`~.Session.begin_nested`. For documentation on - SAVEPOINT transactions, please see :ref:`session_begin_nested`. - - """ - if self.transaction is not None: - if subtransactions or nested: - self.transaction = self.transaction._begin( - nested=nested) - else: - raise sa_exc.InvalidRequestError( - "A transaction is already begun. Use " - "subtransactions=True to allow subtransactions.") - else: - self.transaction = SessionTransaction( - self, nested=nested) - return self.transaction # needed for __enter__/__exit__ hook - - def begin_nested(self): - """Begin a `nested` transaction on this Session. - - The target database(s) must support SQL SAVEPOINTs or a - SQLAlchemy-supported vendor implementation of the idea. - - For documentation on SAVEPOINT - transactions, please see :ref:`session_begin_nested`. - - """ - return self.begin(nested=True) - - def rollback(self): - """Rollback the current transaction in progress. - - If no transaction is in progress, this method is a pass-through. - - This method rolls back the current transaction or nested transaction - regardless of subtransactions being in effect. All subtransactions up - to the first real transaction are closed. Subtransactions occur when - begin() is called multiple times. - - .. seealso:: - - :ref:`session_rollback` - - """ - if self.transaction is None: - pass - else: - self.transaction.rollback() - - def commit(self): - """Flush pending changes and commit the current transaction. - - If no transaction is in progress, this method raises an - :exc:`~sqlalchemy.exc.InvalidRequestError`. - - By default, the :class:`.Session` also expires all database - loaded state on all ORM-managed attributes after transaction commit. - This so that subsequent operations load the most recent - data from the database. This behavior can be disabled using - the ``expire_on_commit=False`` option to :class:`.sessionmaker` or - the :class:`.Session` constructor. - - If a subtransaction is in effect (which occurs when begin() is called - multiple times), the subtransaction will be closed, and the next call - to ``commit()`` will operate on the enclosing transaction. - - When using the :class:`.Session` in its default mode of - ``autocommit=False``, a new transaction will - be begun immediately after the commit, but note that the newly begun - transaction does *not* use any connection resources until the first - SQL is actually emitted. - - .. seealso:: - - :ref:`session_committing` - - """ - if self.transaction is None: - if not self.autocommit: - self.begin() - else: - raise sa_exc.InvalidRequestError("No transaction is begun.") - - self.transaction.commit() - - def prepare(self): - """Prepare the current transaction in progress for two phase commit. - - If no transaction is in progress, this method raises an - :exc:`~sqlalchemy.exc.InvalidRequestError`. - - Only root transactions of two phase sessions can be prepared. If the - current transaction is not such, an - :exc:`~sqlalchemy.exc.InvalidRequestError` is raised. - - """ - if self.transaction is None: - if not self.autocommit: - self.begin() - else: - raise sa_exc.InvalidRequestError("No transaction is begun.") - - self.transaction.prepare() - - def connection(self, mapper=None, clause=None, - bind=None, - close_with_result=False, - **kw): - """Return a :class:`.Connection` object corresponding to this - :class:`.Session` object's transactional state. - - If this :class:`.Session` is configured with ``autocommit=False``, - either the :class:`.Connection` corresponding to the current - transaction is returned, or if no transaction is in progress, a new - one is begun and the :class:`.Connection` returned (note that no - transactional state is established with the DBAPI until the first - SQL statement is emitted). - - Alternatively, if this :class:`.Session` is configured with - ``autocommit=True``, an ad-hoc :class:`.Connection` is returned - using :meth:`.Engine.contextual_connect` on the underlying - :class:`.Engine`. - - Ambiguity in multi-bind or unbound :class:`.Session` objects can be - resolved through any of the optional keyword arguments. This - ultimately makes usage of the :meth:`.get_bind` method for resolution. - - :param bind: - Optional :class:`.Engine` to be used as the bind. If - this engine is already involved in an ongoing transaction, - that connection will be used. This argument takes precedence - over ``mapper``, ``clause``. - - :param mapper: - Optional :func:`.mapper` mapped class, used to identify - the appropriate bind. This argument takes precedence over - ``clause``. - - :param clause: - A :class:`.ClauseElement` (i.e. :func:`~.sql.expression.select`, - :func:`~.sql.expression.text`, - etc.) which will be used to locate a bind, if a bind - cannot otherwise be identified. - - :param close_with_result: Passed to :meth:`.Engine.connect`, indicating - the :class:`.Connection` should be considered "single use", - automatically closing when the first result set is closed. This - flag only has an effect if this :class:`.Session` is configured with - ``autocommit=True`` and does not already have a transaction - in progress. - - :param \**kw: - Additional keyword arguments are sent to :meth:`get_bind()`, - allowing additional arguments to be passed to custom - implementations of :meth:`get_bind`. - - """ - if bind is None: - bind = self.get_bind(mapper, clause=clause, **kw) - - return self._connection_for_bind(bind, - close_with_result=close_with_result) - - def _connection_for_bind(self, engine, **kwargs): - if self.transaction is not None: - return self.transaction._connection_for_bind(engine) - else: - return engine.contextual_connect(**kwargs) - - def execute(self, clause, params=None, mapper=None, bind=None, **kw): - """Execute a SQL expression construct or string statement within - the current transaction. - - Returns a :class:`.ResultProxy` representing - results of the statement execution, in the same manner as that of an - :class:`.Engine` or - :class:`.Connection`. - - E.g.:: - - result = session.execute( - user_table.select().where(user_table.c.id == 5) - ) - - :meth:`~.Session.execute` accepts any executable clause construct, such - as :func:`~.sql.expression.select`, - :func:`~.sql.expression.insert`, - :func:`~.sql.expression.update`, - :func:`~.sql.expression.delete`, and - :func:`~.sql.expression.text`. Plain SQL strings can be passed - as well, which in the case of :meth:`.Session.execute` only - will be interpreted the same as if it were passed via a - :func:`~.expression.text` construct. That is, the following usage:: - - result = session.execute( - "SELECT * FROM user WHERE id=:param", - {"param":5} - ) - - is equivalent to:: - - from sqlalchemy import text - result = session.execute( - text("SELECT * FROM user WHERE id=:param"), - {"param":5} - ) - - The second positional argument to :meth:`.Session.execute` is an - optional parameter set. Similar to that of - :meth:`.Connection.execute`, whether this is passed as a single - dictionary, or a list of dictionaries, determines whether the DBAPI - cursor's ``execute()`` or ``executemany()`` is used to execute the - statement. An INSERT construct may be invoked for a single row:: - - result = session.execute(users.insert(), {"id": 7, "name": "somename"}) - - or for multiple rows:: - - result = session.execute(users.insert(), [ - {"id": 7, "name": "somename7"}, - {"id": 8, "name": "somename8"}, - {"id": 9, "name": "somename9"} - ]) - - The statement is executed within the current transactional context of - this :class:`.Session`. The :class:`.Connection` which is used - to execute the statement can also be acquired directly by - calling the :meth:`.Session.connection` method. Both methods use - a rule-based resolution scheme in order to determine the - :class:`.Connection`, which in the average case is derived directly - from the "bind" of the :class:`.Session` itself, and in other cases - can be based on the :func:`.mapper` - and :class:`.Table` objects passed to the method; see the documentation - for :meth:`.Session.get_bind` for a full description of this scheme. - - The :meth:`.Session.execute` method does *not* invoke autoflush. - - The :class:`.ResultProxy` returned by the :meth:`.Session.execute` - method is returned with the "close_with_result" flag set to true; - the significance of this flag is that if this :class:`.Session` is - autocommitting and does not have a transaction-dedicated - :class:`.Connection` available, a temporary :class:`.Connection` is - established for the statement execution, which is closed (meaning, - returned to the connection pool) when the :class:`.ResultProxy` has - consumed all available data. This applies *only* when the - :class:`.Session` is configured with autocommit=True and no - transaction has been started. - - :param clause: - An executable statement (i.e. an :class:`.Executable` expression - such as :func:`.expression.select`) or string SQL statement - to be executed. - - :param params: - Optional dictionary, or list of dictionaries, containing - bound parameter values. If a single dictionary, single-row - execution occurs; if a list of dictionaries, an - "executemany" will be invoked. The keys in each dictionary - must correspond to parameter names present in the statement. - - :param mapper: - Optional :func:`.mapper` or mapped class, used to identify - the appropriate bind. This argument takes precedence over - ``clause`` when locating a bind. See :meth:`.Session.get_bind` - for more details. - - :param bind: - Optional :class:`.Engine` to be used as the bind. If - this engine is already involved in an ongoing transaction, - that connection will be used. This argument takes - precedence over ``mapper`` and ``clause`` when locating - a bind. - - :param \**kw: - Additional keyword arguments are sent to :meth:`.Session.get_bind()` - to allow extensibility of "bind" schemes. - - .. seealso:: - - :ref:`sqlexpression_toplevel` - Tutorial on using Core SQL - constructs. - - :ref:`connections_toplevel` - Further information on direct - statement execution. - - :meth:`.Connection.execute` - core level statement execution - method, which is :meth:`.Session.execute` ultimately uses - in order to execute the statement. - - """ - clause = expression._literal_as_text(clause) - - if bind is None: - bind = self.get_bind(mapper, clause=clause, **kw) - - return self._connection_for_bind(bind, close_with_result=True).execute( - clause, params or {}) - - def scalar(self, clause, params=None, mapper=None, bind=None, **kw): - """Like :meth:`~.Session.execute` but return a scalar result.""" - - return self.execute( - clause, params=params, mapper=mapper, bind=bind, **kw).scalar() - - def close(self): - """Close this Session. - - This clears all items and ends any transaction in progress. - - If this session were created with ``autocommit=False``, a new - transaction is immediately begun. Note that this new transaction does - not use any connection resources until they are first needed. - - """ - self.expunge_all() - if self.transaction is not None: - for transaction in self.transaction._iterate_parents(): - transaction.close() - - def expunge_all(self): - """Remove all object instances from this ``Session``. - - This is equivalent to calling ``expunge(obj)`` on all objects in this - ``Session``. - - """ - for state in self.identity_map.all_states() + list(self._new): - state._detach() - - self.identity_map = self._identity_cls() - self._new = {} - self._deleted = {} - - # TODO: need much more test coverage for bind_mapper() and similar ! - # TODO: + crystalize + document resolution order - # vis. bind_mapper/bind_table - - def bind_mapper(self, mapper, bind): - """Bind operations for a mapper to a Connectable. - - mapper - A mapper instance or mapped class - - bind - Any Connectable: a ``Engine`` or ``Connection``. - - All subsequent operations involving this mapper will use the given - `bind`. - - """ - if isinstance(mapper, type): - mapper = class_mapper(mapper) - - self.__binds[mapper.base_mapper] = bind - for t in mapper._all_tables: - self.__binds[t] = bind - - def bind_table(self, table, bind): - """Bind operations on a Table to a Connectable. - - table - A ``Table`` instance - - bind - Any Connectable: a ``Engine`` or ``Connection``. - - All subsequent operations involving this ``Table`` will use the - given `bind`. - - """ - self.__binds[table] = bind - - def get_bind(self, mapper=None, clause=None): - """Return a "bind" to which this :class:`.Session` is bound. - - The "bind" is usually an instance of :class:`.Engine`, - except in the case where the :class:`.Session` has been - explicitly bound directly to a :class:`.Connection`. - - For a multiply-bound or unbound :class:`.Session`, the - ``mapper`` or ``clause`` arguments are used to determine the - appropriate bind to return. - - Note that the "mapper" argument is usually present - when :meth:`.Session.get_bind` is called via an ORM - operation such as a :meth:`.Session.query`, each - individual INSERT/UPDATE/DELETE operation within a - :meth:`.Session.flush`, call, etc. - - The order of resolution is: - - 1. if mapper given and session.binds is present, - locate a bind based on mapper. - 2. if clause given and session.binds is present, - locate a bind based on :class:`.Table` objects - found in the given clause present in session.binds. - 3. if session.bind is present, return that. - 4. if clause given, attempt to return a bind - linked to the :class:`.MetaData` ultimately - associated with the clause. - 5. if mapper given, attempt to return a bind - linked to the :class:`.MetaData` ultimately - associated with the :class:`.Table` or other - selectable to which the mapper is mapped. - 6. No bind can be found, :exc:`~sqlalchemy.exc.UnboundExecutionError` - is raised. - - :param mapper: - Optional :func:`.mapper` mapped class or instance of - :class:`.Mapper`. The bind can be derived from a :class:`.Mapper` - first by consulting the "binds" map associated with this - :class:`.Session`, and secondly by consulting the :class:`.MetaData` - associated with the :class:`.Table` to which the :class:`.Mapper` - is mapped for a bind. - - :param clause: - A :class:`.ClauseElement` (i.e. :func:`~.sql.expression.select`, - :func:`~.sql.expression.text`, - etc.). If the ``mapper`` argument is not present or could not - produce a bind, the given expression construct will be searched - for a bound element, typically a :class:`.Table` associated with - bound :class:`.MetaData`. - - """ - if mapper is clause is None: - if self.bind: - return self.bind - else: - raise sa_exc.UnboundExecutionError( - "This session is not bound to a single Engine or " - "Connection, and no context was provided to locate " - "a binding.") - - c_mapper = mapper is not None and _class_to_mapper(mapper) or None - - # manually bound? - if self.__binds: - if c_mapper: - if c_mapper.base_mapper in self.__binds: - return self.__binds[c_mapper.base_mapper] - elif c_mapper.mapped_table in self.__binds: - return self.__binds[c_mapper.mapped_table] - if clause is not None: - for t in sql_util.find_tables(clause, include_crud=True): - if t in self.__binds: - return self.__binds[t] - - if self.bind: - return self.bind - - if isinstance(clause, sql.expression.ClauseElement) and clause.bind: - return clause.bind - - if c_mapper and c_mapper.mapped_table.bind: - return c_mapper.mapped_table.bind - - context = [] - if mapper is not None: - context.append('mapper %s' % c_mapper) - if clause is not None: - context.append('SQL expression') - - raise sa_exc.UnboundExecutionError( - "Could not locate a bind configured on %s or this Session" % ( - ', '.join(context))) - - def query(self, *entities, **kwargs): - """Return a new ``Query`` object corresponding to this ``Session``.""" - - return self._query_cls(entities, self, **kwargs) - - @property - @util.contextmanager - def no_autoflush(self): - """Return a context manager that disables autoflush. - - e.g.:: - - with session.no_autoflush: - - some_object = SomeClass() - session.add(some_object) - # won't autoflush - some_object.related_thing = session.query(SomeRelated).first() - - Operations that proceed within the ``with:`` block - will not be subject to flushes occurring upon query - access. This is useful when initializing a series - of objects which involve existing database queries, - where the uncompleted object should not yet be flushed. - - .. versionadded:: 0.7.6 - - """ - autoflush = self.autoflush - self.autoflush = False - yield self - self.autoflush = autoflush - - def _autoflush(self): - if self.autoflush and not self._flushing: - try: - self.flush() - except sa_exc.StatementError as e: - # note we are reraising StatementError as opposed to - # raising FlushError with "chaining" to remain compatible - # with code that catches StatementError, IntegrityError, - # etc. - e.add_detail( - "raised as a result of Query-invoked autoflush; " - "consider using a session.no_autoflush block if this " - "flush is occuring prematurely") - util.raise_from_cause(e) - - def refresh(self, instance, attribute_names=None, lockmode=None): - """Expire and refresh the attributes on the given instance. - - A query will be issued to the database and all attributes will be - refreshed with their current database value. - - Lazy-loaded relational attributes will remain lazily loaded, so that - the instance-wide refresh operation will be followed immediately by - the lazy load of that attribute. - - Eagerly-loaded relational attributes will eagerly load within the - single refresh operation. - - Note that a highly isolated transaction will return the same values as - were previously read in that same transaction, regardless of changes - in database state outside of that transaction - usage of - :meth:`~Session.refresh` usually only makes sense if non-ORM SQL - statement were emitted in the ongoing transaction, or if autocommit - mode is turned on. - - :param attribute_names: optional. An iterable collection of - string attribute names indicating a subset of attributes to - be refreshed. - - :param lockmode: Passed to the :class:`~sqlalchemy.orm.query.Query` - as used by :meth:`~sqlalchemy.orm.query.Query.with_lockmode`. - - """ - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - - self._expire_state(state, attribute_names) - - if loading.load_on_ident( - self.query(object_mapper(instance)), - state.key, refresh_state=state, - lockmode=lockmode, - only_load_props=attribute_names) is None: - raise sa_exc.InvalidRequestError( - "Could not refresh instance '%s'" % - instance_str(instance)) - - def expire_all(self): - """Expires all persistent instances within this Session. - - When any attributes on a persistent instance is next accessed, - a query will be issued using the - :class:`.Session` object's current transactional context in order to - load all expired attributes for the given instance. Note that - a highly isolated transaction will return the same values as were - previously read in that same transaction, regardless of changes - in database state outside of that transaction. - - To expire individual objects and individual attributes - on those objects, use :meth:`Session.expire`. - - The :class:`.Session` object's default behavior is to - expire all state whenever the :meth:`Session.rollback` - or :meth:`Session.commit` methods are called, so that new - state can be loaded for the new transaction. For this reason, - calling :meth:`Session.expire_all` should not be needed when - autocommit is ``False``, assuming the transaction is isolated. - - """ - for state in self.identity_map.all_states(): - state._expire(state.dict, self.identity_map._modified) - - def expire(self, instance, attribute_names=None): - """Expire the attributes on an instance. - - Marks the attributes of an instance as out of date. When an expired - attribute is next accessed, a query will be issued to the - :class:`.Session` object's current transactional context in order to - load all expired attributes for the given instance. Note that - a highly isolated transaction will return the same values as were - previously read in that same transaction, regardless of changes - in database state outside of that transaction. - - To expire all objects in the :class:`.Session` simultaneously, - use :meth:`Session.expire_all`. - - The :class:`.Session` object's default behavior is to - expire all state whenever the :meth:`Session.rollback` - or :meth:`Session.commit` methods are called, so that new - state can be loaded for the new transaction. For this reason, - calling :meth:`Session.expire` only makes sense for the specific - case that a non-ORM SQL statement was emitted in the current - transaction. - - :param instance: The instance to be refreshed. - :param attribute_names: optional list of string attribute names - indicating a subset of attributes to be expired. - - """ - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - self._expire_state(state, attribute_names) - - def _expire_state(self, state, attribute_names): - self._validate_persistent(state) - if attribute_names: - state._expire_attributes(state.dict, attribute_names) - else: - # pre-fetch the full cascade since the expire is going to - # remove associations - cascaded = list(state.manager.mapper.cascade_iterator( - 'refresh-expire', state)) - self._conditional_expire(state) - for o, m, st_, dct_ in cascaded: - self._conditional_expire(st_) - - def _conditional_expire(self, state): - """Expire a state if persistent, else expunge if pending""" - - if state.key: - state._expire(state.dict, self.identity_map._modified) - elif state in self._new: - self._new.pop(state) - state._detach() - - @util.deprecated("0.7", "The non-weak-referencing identity map " - "feature is no longer needed.") - def prune(self): - """Remove unreferenced instances cached in the identity map. - - Note that this method is only meaningful if "weak_identity_map" is set - to False. The default weak identity map is self-pruning. - - Removes any object in this Session's identity map that is not - referenced in user code, modified, new or scheduled for deletion. - Returns the number of objects pruned. - - """ - return self.identity_map.prune() - - def expunge(self, instance): - """Remove the `instance` from this ``Session``. - - This will free all internal references to the instance. Cascading - will be applied according to the *expunge* cascade rule. - - """ - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - if state.session_id is not self.hash_key: - raise sa_exc.InvalidRequestError( - "Instance %s is not present in this Session" % - state_str(state)) - - cascaded = list(state.manager.mapper.cascade_iterator( - 'expunge', state)) - self._expunge_state(state) - for o, m, st_, dct_ in cascaded: - self._expunge_state(st_) - - def _expunge_state(self, state): - if state in self._new: - self._new.pop(state) - state._detach() - elif self.identity_map.contains_state(state): - self.identity_map.discard(state) - self._deleted.pop(state, None) - state._detach() - elif self.transaction: - self.transaction._deleted.pop(state, None) - - def _register_newly_persistent(self, states): - for state in states: - mapper = _state_mapper(state) - - # prevent against last minute dereferences of the object - obj = state.obj() - if obj is not None: - - instance_key = mapper._identity_key_from_state(state) - - if _none_set.issubset(instance_key[1]) and \ - not mapper.allow_partial_pks or \ - _none_set.issuperset(instance_key[1]): - raise exc.FlushError( - "Instance %s has a NULL identity key. If this is an " - "auto-generated value, check that the database table " - "allows generation of new primary key values, and " - "that the mapped Column object is configured to " - "expect these generated values. Ensure also that " - "this flush() is not occurring at an inappropriate " - "time, such aswithin a load() event." - % state_str(state) - ) - - if state.key is None: - state.key = instance_key - elif state.key != instance_key: - # primary key switch. use discard() in case another - # state has already replaced this one in the identity - # map (see test/orm/test_naturalpks.py ReversePKsTest) - self.identity_map.discard(state) - if state in self.transaction._key_switches: - orig_key = self.transaction._key_switches[state][0] - else: - orig_key = state.key - self.transaction._key_switches[state] = ( - orig_key, instance_key) - state.key = instance_key - - self.identity_map.replace(state) - - statelib.InstanceState._commit_all_states( - ((state, state.dict) for state in states), - self.identity_map - ) - - self._register_altered(states) - # remove from new last, might be the last strong ref - for state in set(states).intersection(self._new): - self._new.pop(state) - - def _register_altered(self, states): - if self._enable_transaction_accounting and self.transaction: - for state in states: - if state in self._new: - self.transaction._new[state] = True - else: - self.transaction._dirty[state] = True - - def _remove_newly_deleted(self, states): - for state in states: - if self._enable_transaction_accounting and self.transaction: - self.transaction._deleted[state] = True - - self.identity_map.discard(state) - self._deleted.pop(state, None) - state.deleted = True - - def add(self, instance, _warn=True): - """Place an object in the ``Session``. - - Its state will be persisted to the database on the next flush - operation. - - Repeated calls to ``add()`` will be ignored. The opposite of ``add()`` - is ``expunge()``. - - """ - if _warn and self._warn_on_events: - self._flush_warning("Session.add()") - - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - - self._save_or_update_state(state) - - def add_all(self, instances): - """Add the given collection of instances to this ``Session``.""" - - if self._warn_on_events: - self._flush_warning("Session.add_all()") - - for instance in instances: - self.add(instance, _warn=False) - - def _save_or_update_state(self, state): - self._save_or_update_impl(state) - - mapper = _state_mapper(state) - for o, m, st_, dct_ in mapper.cascade_iterator( - 'save-update', - state, - halt_on=self._contains_state): - self._save_or_update_impl(st_) - - def delete(self, instance): - """Mark an instance as deleted. - - The database delete operation occurs upon ``flush()``. - - """ - if self._warn_on_events: - self._flush_warning("Session.delete()") - - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - - if state.key is None: - raise sa_exc.InvalidRequestError( - "Instance '%s' is not persisted" % - state_str(state)) - - if state in self._deleted: - return - - # ensure object is attached to allow the - # cascade operation to load deferred attributes - # and collections - self._attach(state, include_before=True) - - # grab the cascades before adding the item to the deleted list - # so that autoflush does not delete the item - # the strong reference to the instance itself is significant here - cascade_states = list(state.manager.mapper.cascade_iterator( - 'delete', state)) - - self._deleted[state] = state.obj() - self.identity_map.add(state) - - for o, m, st_, dct_ in cascade_states: - self._delete_impl(st_) - - def merge(self, instance, load=True): - """Copy the state of a given instance into a corresponding instance - within this :class:`.Session`. - - :meth:`.Session.merge` examines the primary key attributes of the - source instance, and attempts to reconcile it with an instance of the - same primary key in the session. If not found locally, it attempts - to load the object from the database based on primary key, and if - none can be located, creates a new instance. The state of each - attribute on the source instance is then copied to the target instance. - The resulting target instance is then returned by the method; the - original source instance is left unmodified, and un-associated with the - :class:`.Session` if not already. - - This operation cascades to associated instances if the association is - mapped with ``cascade="merge"``. - - See :ref:`unitofwork_merging` for a detailed discussion of merging. - - :param instance: Instance to be merged. - :param load: Boolean, when False, :meth:`.merge` switches into - a "high performance" mode which causes it to forego emitting history - events as well as all database access. This flag is used for - cases such as transferring graphs of objects into a :class:`.Session` - from a second level cache, or to transfer just-loaded objects - into the :class:`.Session` owned by a worker thread or process - without re-querying the database. - - The ``load=False`` use case adds the caveat that the given - object has to be in a "clean" state, that is, has no pending changes - to be flushed - even if the incoming object is detached from any - :class:`.Session`. This is so that when - the merge operation populates local attributes and - cascades to related objects and - collections, the values can be "stamped" onto the - target object as is, without generating any history or attribute - events, and without the need to reconcile the incoming data with - any existing related objects or collections that might not - be loaded. The resulting objects from ``load=False`` are always - produced as "clean", so it is only appropriate that the given objects - should be "clean" as well, else this suggests a mis-use of the method. - - """ - - if self._warn_on_events: - self._flush_warning("Session.merge()") - - _recursive = {} - - if load: - # flush current contents if we expect to load data - self._autoflush() - - object_mapper(instance) # verify mapped - autoflush = self.autoflush - try: - self.autoflush = False - return self._merge( - attributes.instance_state(instance), - attributes.instance_dict(instance), - load=load, _recursive=_recursive) - finally: - self.autoflush = autoflush - - def _merge(self, state, state_dict, load=True, _recursive=None): - mapper = _state_mapper(state) - if state in _recursive: - return _recursive[state] - - new_instance = False - key = state.key - - if key is None: - if not load: - raise sa_exc.InvalidRequestError( - "merge() with load=False option does not support " - "objects transient (i.e. unpersisted) objects. flush() " - "all changes on mapped instances before merging with " - "load=False.") - key = mapper._identity_key_from_state(state) - - if key in self.identity_map: - merged = self.identity_map[key] - - elif not load: - if state.modified: - raise sa_exc.InvalidRequestError( - "merge() with load=False option does not support " - "objects marked as 'dirty'. flush() all changes on " - "mapped instances before merging with load=False.") - merged = mapper.class_manager.new_instance() - merged_state = attributes.instance_state(merged) - merged_state.key = key - self._update_impl(merged_state) - new_instance = True - - elif not _none_set.issubset(key[1]) or \ - (mapper.allow_partial_pks and - not _none_set.issuperset(key[1])): - merged = self.query(mapper.class_).get(key[1]) - else: - merged = None - - if merged is None: - merged = mapper.class_manager.new_instance() - merged_state = attributes.instance_state(merged) - merged_dict = attributes.instance_dict(merged) - new_instance = True - self._save_or_update_state(merged_state) - else: - merged_state = attributes.instance_state(merged) - merged_dict = attributes.instance_dict(merged) - - _recursive[state] = merged - - # check that we didn't just pull the exact same - # state out. - if state is not merged_state: - # version check if applicable - if mapper.version_id_col is not None: - existing_version = mapper._get_state_attr_by_column( - state, - state_dict, - mapper.version_id_col, - passive=attributes.PASSIVE_NO_INITIALIZE) - - merged_version = mapper._get_state_attr_by_column( - merged_state, - merged_dict, - mapper.version_id_col, - passive=attributes.PASSIVE_NO_INITIALIZE) - - if existing_version is not attributes.PASSIVE_NO_RESULT and \ - merged_version is not attributes.PASSIVE_NO_RESULT and \ - existing_version != merged_version: - raise exc.StaleDataError( - "Version id '%s' on merged state %s " - "does not match existing version '%s'. " - "Leave the version attribute unset when " - "merging to update the most recent version." - % ( - existing_version, - state_str(merged_state), - merged_version - )) - - merged_state.load_path = state.load_path - merged_state.load_options = state.load_options - - for prop in mapper.iterate_properties: - prop.merge(self, state, state_dict, - merged_state, merged_dict, - load, _recursive) - - if not load: - # remove any history - merged_state._commit_all(merged_dict, self.identity_map) - - if new_instance: - merged_state.manager.dispatch.load(merged_state, None) - return merged - - def _validate_persistent(self, state): - if not self.identity_map.contains_state(state): - raise sa_exc.InvalidRequestError( - "Instance '%s' is not persistent within this Session" % - state_str(state)) - - def _save_impl(self, state): - if state.key is not None: - raise sa_exc.InvalidRequestError( - "Object '%s' already has an identity - it can't be registered " - "as pending" % state_str(state)) - - self._before_attach(state) - if state not in self._new: - self._new[state] = state.obj() - state.insert_order = len(self._new) - self._attach(state) - - def _update_impl(self, state, discard_existing=False): - if (self.identity_map.contains_state(state) and - state not in self._deleted): - return - - if state.key is None: - raise sa_exc.InvalidRequestError( - "Instance '%s' is not persisted" % - state_str(state)) - - if state.deleted: - raise sa_exc.InvalidRequestError( - "Instance '%s' has been deleted. Use the make_transient() " - "function to send this object back to the transient state." % - state_str(state) - ) - self._before_attach(state) - self._deleted.pop(state, None) - if discard_existing: - self.identity_map.replace(state) - else: - self.identity_map.add(state) - self._attach(state) - - def _save_or_update_impl(self, state): - if state.key is None: - self._save_impl(state) - else: - self._update_impl(state) - - def _delete_impl(self, state): - if state in self._deleted: - return - - if state.key is None: - return - - self._attach(state, include_before=True) - self._deleted[state] = state.obj() - self.identity_map.add(state) - - def enable_relationship_loading(self, obj): - """Associate an object with this :class:`.Session` for related - object loading. - - .. warning:: - - :meth:`.enable_relationship_loading` exists to serve special - use cases and is not recommended for general use. - - Accesses of attributes mapped with :func:`.relationship` - will attempt to load a value from the database using this - :class:`.Session` as the source of connectivity. The values - will be loaded based on foreign key values present on this - object - it follows that this functionality - generally only works for many-to-one-relationships. - - The object will be attached to this session, but will - **not** participate in any persistence operations; its state - for almost all purposes will remain either "transient" or - "detached", except for the case of relationship loading. - - Also note that backrefs will often not work as expected. - Altering a relationship-bound attribute on the target object - may not fire off a backref event, if the effective value - is what was already loaded from a foreign-key-holding value. - - The :meth:`.Session.enable_relationship_loading` method is - similar to the ``load_on_pending`` flag on :func:`.relationship`. Unlike - that flag, :meth:`.Session.enable_relationship_loading` allows - an object to remain transient while still being able to load - related items. - - To make a transient object associated with a :class:`.Session` - via :meth:`.Session.enable_relationship_loading` pending, add - it to the :class:`.Session` using :meth:`.Session.add` normally. - - :meth:`.Session.enable_relationship_loading` does not improve - behavior when the ORM is used normally - object references should be - constructed at the object level, not at the foreign key level, so - that they are present in an ordinary way before flush() - proceeds. This method is not intended for general use. - - .. versionadded:: 0.8 - - .. seealso:: - - ``load_on_pending`` at :func:`.relationship` - this flag - allows per-relationship loading of many-to-ones on items that - are pending. - - """ - state = attributes.instance_state(obj) - self._attach(state, include_before=True) - state._load_pending = True - - def _before_attach(self, state): - if state.session_id != self.hash_key and \ - self.dispatch.before_attach: - self.dispatch.before_attach(self, state.obj()) - - def _attach(self, state, include_before=False): - if state.key and \ - state.key in self.identity_map and \ - not self.identity_map.contains_state(state): - raise sa_exc.InvalidRequestError("Can't attach instance " - "%s; another instance with key %s is already " - "present in this session." - % (state_str(state), state.key)) - - if state.session_id and \ - state.session_id is not self.hash_key and \ - state.session_id in _sessions: - raise sa_exc.InvalidRequestError( - "Object '%s' is already attached to session '%s' " - "(this is '%s')" % (state_str(state), - state.session_id, self.hash_key)) - - if state.session_id != self.hash_key: - if include_before and \ - self.dispatch.before_attach: - self.dispatch.before_attach(self, state.obj()) - state.session_id = self.hash_key - if state.modified and state._strong_obj is None: - state._strong_obj = state.obj() - if self.dispatch.after_attach: - self.dispatch.after_attach(self, state.obj()) - - def __contains__(self, instance): - """Return True if the instance is associated with this session. - - The instance may be pending or persistent within the Session for a - result of True. - - """ - try: - state = attributes.instance_state(instance) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - return self._contains_state(state) - - def __iter__(self): - """Iterate over all pending or persistent instances within this - Session. - - """ - return iter(list(self._new.values()) + list(self.identity_map.values())) - - def _contains_state(self, state): - return state in self._new or self.identity_map.contains_state(state) - - def flush(self, objects=None): - """Flush all the object changes to the database. - - Writes out all pending object creations, deletions and modifications - to the database as INSERTs, DELETEs, UPDATEs, etc. Operations are - automatically ordered by the Session's unit of work dependency - solver. - - Database operations will be issued in the current transactional - context and do not affect the state of the transaction, unless an - error occurs, in which case the entire transaction is rolled back. - You may flush() as often as you like within a transaction to move - changes from Python to the database's transaction buffer. - - For ``autocommit`` Sessions with no active manual transaction, flush() - will create a transaction on the fly that surrounds the entire set of - operations int the flush. - - :param objects: Optional; restricts the flush operation to operate - only on elements that are in the given collection. - - This feature is for an extremely narrow set of use cases where - particular objects may need to be operated upon before the - full flush() occurs. It is not intended for general use. - - """ - - if self._flushing: - raise sa_exc.InvalidRequestError("Session is already flushing") - - if self._is_clean(): - return - try: - self._flushing = True - self._flush(objects) - finally: - self._flushing = False - - def _flush_warning(self, method): - util.warn( - "Usage of the '%s' operation is not currently supported " - "within the execution stage of the flush process. " - "Results may not be consistent. Consider using alternative " - "event listeners or connection-level operations instead." - % method) - - def _is_clean(self): - return not self.identity_map.check_modified() and \ - not self._deleted and \ - not self._new - - def _flush(self, objects=None): - - dirty = self._dirty_states - if not dirty and not self._deleted and not self._new: - self.identity_map._modified.clear() - return - - flush_context = UOWTransaction(self) - - if self.dispatch.before_flush: - self.dispatch.before_flush(self, flush_context, objects) - # re-establish "dirty states" in case the listeners - # added - dirty = self._dirty_states - - deleted = set(self._deleted) - new = set(self._new) - - dirty = set(dirty).difference(deleted) - - # create the set of all objects we want to operate upon - if objects: - # specific list passed in - objset = set() - for o in objects: - try: - state = attributes.instance_state(o) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(o) - objset.add(state) - else: - objset = None - - # store objects whose fate has been decided - processed = set() - - # put all saves/updates into the flush context. detect top-level - # orphans and throw them into deleted. - if objset: - proc = new.union(dirty).intersection(objset).difference(deleted) - else: - proc = new.union(dirty).difference(deleted) - - for state in proc: - is_orphan = ( - _state_mapper(state)._is_orphan(state) and state.has_identity) - flush_context.register_object(state, isdelete=is_orphan) - processed.add(state) - - # put all remaining deletes into the flush context. - if objset: - proc = deleted.intersection(objset).difference(processed) - else: - proc = deleted.difference(processed) - for state in proc: - flush_context.register_object(state, isdelete=True) - - if not flush_context.has_work: - return - - flush_context.transaction = transaction = self.begin( - subtransactions=True) - try: - self._warn_on_events = True - try: - flush_context.execute() - finally: - self._warn_on_events = False - - self.dispatch.after_flush(self, flush_context) - - flush_context.finalize_flush_changes() - - if not objects and self.identity_map._modified: - len_ = len(self.identity_map._modified) - - statelib.InstanceState._commit_all_states( - [(state, state.dict) for state in - self.identity_map._modified], - instance_dict=self.identity_map) - util.warn("Attribute history events accumulated on %d " - "previously clean instances " - "within inner-flush event handlers have been reset, " - "and will not result in database updates. " - "Consider using set_committed_value() within " - "inner-flush event handlers to avoid this warning." - % len_) - - # useful assertions: - #if not objects: - # assert not self.identity_map._modified - #else: - # assert self.identity_map._modified == \ - # self.identity_map._modified.difference(objects) - - self.dispatch.after_flush_postexec(self, flush_context) - - transaction.commit() - - except: - with util.safe_reraise(): - transaction.rollback(_capture_exception=True) - - def is_modified(self, instance, include_collections=True, - passive=True): - """Return ``True`` if the given instance has locally - modified attributes. - - This method retrieves the history for each instrumented - attribute on the instance and performs a comparison of the current - value to its previously committed value, if any. - - It is in effect a more expensive and accurate - version of checking for the given instance in the - :attr:`.Session.dirty` collection; a full test for - each attribute's net "dirty" status is performed. - - E.g.:: - - return session.is_modified(someobject) - - .. versionchanged:: 0.8 - When using SQLAlchemy 0.7 and earlier, the ``passive`` - flag should **always** be explicitly set to ``True``, - else SQL loads/autoflushes may proceed which can affect - the modified state itself: - ``session.is_modified(someobject, passive=True)``\ . - In 0.8 and above, the behavior is corrected and - this flag is ignored. - - A few caveats to this method apply: - - * Instances present in the :attr:`.Session.dirty` collection may report - ``False`` when tested with this method. This is because - the object may have received change events via attribute - mutation, thus placing it in :attr:`.Session.dirty`, - but ultimately the state is the same as that loaded from - the database, resulting in no net change here. - * Scalar attributes may not have recorded the previously set - value when a new value was applied, if the attribute was not loaded, - or was expired, at the time the new value was received - in these - cases, the attribute is assumed to have a change, even if there is - ultimately no net change against its database value. SQLAlchemy in - most cases does not need the "old" value when a set event occurs, so - it skips the expense of a SQL call if the old value isn't present, - based on the assumption that an UPDATE of the scalar value is - usually needed, and in those few cases where it isn't, is less - expensive on average than issuing a defensive SELECT. - - The "old" value is fetched unconditionally upon set only if the - attribute container has the ``active_history`` flag set to ``True``. - This flag is set typically for primary key attributes and scalar - object references that are not a simple many-to-one. To set this - flag for any arbitrary mapped column, use the ``active_history`` - argument with :func:`.column_property`. - - :param instance: mapped instance to be tested for pending changes. - :param include_collections: Indicates if multivalued collections - should be included in the operation. Setting this to ``False`` is a - way to detect only local-column based properties (i.e. scalar columns - or many-to-one foreign keys) that would result in an UPDATE for this - instance upon flush. - :param passive: - .. versionchanged:: 0.8 - Ignored for backwards compatibility. - When using SQLAlchemy 0.7 and earlier, this flag should always - be set to ``True``. - - """ - state = object_state(instance) - - if not state.modified: - return False - - dict_ = state.dict - - for attr in state.manager.attributes: - if \ - ( - not include_collections and - hasattr(attr.impl, 'get_collection') - ) or not hasattr(attr.impl, 'get_history'): - continue - - (added, unchanged, deleted) = \ - attr.impl.get_history(state, dict_, - passive=attributes.NO_CHANGE) - - if added or deleted: - return True - else: - return False - - @property - def is_active(self): - """True if this :class:`.Session` is in "transaction mode" and - is not in "partial rollback" state. - - The :class:`.Session` in its default mode of ``autocommit=False`` - is essentially always in "transaction mode", in that a - :class:`.SessionTransaction` is associated with it as soon as - it is instantiated. This :class:`.SessionTransaction` is immediately - replaced with a new one as soon as it is ended, due to a rollback, - commit, or close operation. - - "Transaction mode" does *not* indicate whether - or not actual database connection resources are in use; the - :class:`.SessionTransaction` object coordinates among zero or more - actual database transactions, and starts out with none, accumulating - individual DBAPI connections as different data sources are used - within its scope. The best way to track when a particular - :class:`.Session` has actually begun to use DBAPI resources is to - implement a listener using the :meth:`.SessionEvents.after_begin` - method, which will deliver both the :class:`.Session` as well as the - target :class:`.Connection` to a user-defined event listener. - - The "partial rollback" state refers to when an "inner" transaction, - typically used during a flush, encounters an error and emits a - rollback of the DBAPI connection. At this point, the - :class:`.Session` is in "partial rollback" and awaits for the user to - call :meth:`.Session.rollback`, in order to close out the - transaction stack. It is in this "partial rollback" period that the - :attr:`.is_active` flag returns False. After the call to - :meth:`.Session.rollback`, the :class:`.SessionTransaction` is replaced - with a new one and :attr:`.is_active` returns ``True`` again. - - When a :class:`.Session` is used in ``autocommit=True`` mode, the - :class:`.SessionTransaction` is only instantiated within the scope - of a flush call, or when :meth:`.Session.begin` is called. So - :attr:`.is_active` will always be ``False`` outside of a flush or - :meth:`.Session.begin` block in this mode, and will be ``True`` - within the :meth:`.Session.begin` block as long as it doesn't enter - "partial rollback" state. - - From all the above, it follows that the only purpose to this flag is - for application frameworks that wish to detect is a "rollback" is - necessary within a generic error handling routine, for - :class:`.Session` objects that would otherwise be in - "partial rollback" mode. In a typical integration case, this is also - not necessary as it is standard practice to emit - :meth:`.Session.rollback` unconditionally within the outermost - exception catch. - - To track the transactional state of a :class:`.Session` fully, - use event listeners, primarily the :meth:`.SessionEvents.after_begin`, - :meth:`.SessionEvents.after_commit`, - :meth:`.SessionEvents.after_rollback` and related events. - - """ - return self.transaction and self.transaction.is_active - - identity_map = None - """A mapping of object identities to objects themselves. - - Iterating through ``Session.identity_map.values()`` provides - access to the full set of persistent objects (i.e., those - that have row identity) currently in the session. - - .. seealso:: - - :func:`.identity_key` - helper function to produce the keys used - in this dictionary. - - """ - - @property - def _dirty_states(self): - """The set of all persistent states considered dirty. - - This method returns all states that were modified including - those that were possibly deleted. - - """ - return self.identity_map._dirty_states() - - @property - def dirty(self): - """The set of all persistent instances considered dirty. - - E.g.:: - - some_mapped_object in session.dirty - - Instances are considered dirty when they were modified but not - deleted. - - Note that this 'dirty' calculation is 'optimistic'; most - attribute-setting or collection modification operations will - mark an instance as 'dirty' and place it in this set, even if - there is no net change to the attribute's value. At flush - time, the value of each attribute is compared to its - previously saved value, and if there's no net change, no SQL - operation will occur (this is a more expensive operation so - it's only done at flush time). - - To check if an instance has actionable net changes to its - attributes, use the :meth:`.Session.is_modified` method. - - """ - return util.IdentitySet( - [state.obj() - for state in self._dirty_states - if state not in self._deleted]) - - @property - def deleted(self): - "The set of all instances marked as 'deleted' within this ``Session``" - - return util.IdentitySet(list(self._deleted.values())) - - @property - def new(self): - "The set of all instances marked as 'new' within this ``Session``." - - return util.IdentitySet(list(self._new.values())) - - -class sessionmaker(_SessionClassMethods): - """A configurable :class:`.Session` factory. - - The :class:`.sessionmaker` factory generates new - :class:`.Session` objects when called, creating them given - the configurational arguments established here. - - e.g.:: - - # global scope - Session = sessionmaker(autoflush=False) - - # later, in a local scope, create and use a session: - sess = Session() - - Any keyword arguments sent to the constructor itself will override the - "configured" keywords:: - - Session = sessionmaker() - - # bind an individual session to a connection - sess = Session(bind=connection) - - The class also includes a method :meth:`.configure`, which can - be used to specify additional keyword arguments to the factory, which - will take effect for subsequent :class:`.Session` objects generated. - This is usually used to associate one or more :class:`.Engine` objects - with an existing :class:`.sessionmaker` factory before it is first - used:: - - # application starts - Session = sessionmaker() - - # ... later - engine = create_engine('sqlite:///foo.db') - Session.configure(bind=engine) - - sess = Session() - - .. seealso: - - :ref:`session_getting` - introductory text on creating - sessions using :class:`.sessionmaker`. - - """ - - def __init__(self, bind=None, class_=Session, autoflush=True, - autocommit=False, - expire_on_commit=True, - info=None, **kw): - """Construct a new :class:`.sessionmaker`. - - All arguments here except for ``class_`` correspond to arguments - accepted by :class:`.Session` directly. See the - :meth:`.Session.__init__` docstring for more details on parameters. - - :param bind: a :class:`.Engine` or other :class:`.Connectable` with - which newly created :class:`.Session` objects will be associated. - :param class_: class to use in order to create new :class:`.Session` - objects. Defaults to :class:`.Session`. - :param autoflush: The autoflush setting to use with newly created - :class:`.Session` objects. - :param autocommit: The autocommit setting to use with newly created - :class:`.Session` objects. - :param expire_on_commit=True: the expire_on_commit setting to use - with newly created :class:`.Session` objects. - :param info: optional dictionary of information that will be available - via :attr:`.Session.info`. Note this dictionary is *updated*, not - replaced, when the ``info`` parameter is specified to the specific - :class:`.Session` construction operation. - - .. versionadded:: 0.9.0 - - :param \**kw: all other keyword arguments are passed to the constructor - of newly created :class:`.Session` objects. - - """ - kw['bind'] = bind - kw['autoflush'] = autoflush - kw['autocommit'] = autocommit - kw['expire_on_commit'] = expire_on_commit - if info is not None: - kw['info'] = info - self.kw = kw - # make our own subclass of the given class, so that - # events can be associated with it specifically. - self.class_ = type(class_.__name__, (class_,), {}) - - def __call__(self, **local_kw): - """Produce a new :class:`.Session` object using the configuration - established in this :class:`.sessionmaker`. - - In Python, the ``__call__`` method is invoked on an object when - it is "called" in the same way as a function:: - - Session = sessionmaker() - session = Session() # invokes sessionmaker.__call__() - - """ - for k, v in self.kw.items(): - if k == 'info' and 'info' in local_kw: - d = v.copy() - d.update(local_kw['info']) - local_kw['info'] = d - else: - local_kw.setdefault(k, v) - return self.class_(**local_kw) - - def configure(self, **new_kw): - """(Re)configure the arguments for this sessionmaker. - - e.g.:: - - Session = sessionmaker() - - Session.configure(bind=create_engine('sqlite://')) - """ - self.kw.update(new_kw) - - def __repr__(self): - return "%s(class_=%r,%s)" % ( - self.__class__.__name__, - self.class_.__name__, - ", ".join("%s=%r" % (k, v) for k, v in self.kw.items()) - ) - - - -def make_transient(instance): - """Make the given instance 'transient'. - - This will remove its association with any - session and additionally will remove its "identity key", - such that it's as though the object were newly constructed, - except retaining its values. It also resets the - "deleted" flag on the state if this object - had been explicitly deleted by its session. - - Attributes which were "expired" or deferred at the - instance level are reverted to undefined, and - will not trigger any loads. - - """ - state = attributes.instance_state(instance) - s = _state_session(state) - if s: - s._expunge_state(state) - - # remove expired state and - # deferred callables - state.callables.clear() - if state.key: - del state.key - if state.deleted: - del state.deleted - - -def object_session(instance): - """Return the ``Session`` to which instance belongs. - - If the instance is not a mapped instance, an error is raised. - - """ - - try: - return _state_session(attributes.instance_state(instance)) - except exc.NO_STATE: - raise exc.UnmappedInstanceError(instance) - - -_new_sessionid = util.counter() diff --git a/libs/sqlalchemy/orm/state.py b/libs/sqlalchemy/orm/state.py deleted file mode 100644 index 9712dd05..00000000 --- a/libs/sqlalchemy/orm/state.py +++ /dev/null @@ -1,617 +0,0 @@ -# orm/state.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Defines instrumentation of instances. - -This module is usually not directly visible to user applications, but -defines a large part of the ORM's interactivity. - -""" - -import weakref -from .. import util -from . import exc as orm_exc, interfaces -from .path_registry import PathRegistry -from .base import PASSIVE_NO_RESULT, SQL_OK, NEVER_SET, ATTR_WAS_SET, \ - NO_VALUE, PASSIVE_NO_INITIALIZE, INIT_OK, PASSIVE_OFF -from . import base - -class InstanceState(interfaces._InspectionAttr): - """tracks state information at the instance level.""" - - session_id = None - key = None - runid = None - load_options = util.EMPTY_SET - load_path = () - insert_order = None - _strong_obj = None - modified = False - expired = False - deleted = False - _load_pending = False - - is_instance = True - - def __init__(self, obj, manager): - self.class_ = obj.__class__ - self.manager = manager - self.obj = weakref.ref(obj, self._cleanup) - self.callables = {} - self.committed_state = {} - - @util.memoized_property - def attrs(self): - """Return a namespace representing each attribute on - the mapped object, including its current value - and history. - - The returned object is an instance of :class:`.AttributeState`. - - """ - return util.ImmutableProperties( - dict( - (key, AttributeState(self, key)) - for key in self.manager - ) - ) - - @property - def transient(self): - """Return true if the object is transient.""" - return self.key is None and \ - not self._attached - - @property - def pending(self): - """Return true if the object is pending.""" - return self.key is None and \ - self._attached - - @property - def persistent(self): - """Return true if the object is persistent.""" - return self.key is not None and \ - self._attached - - @property - def detached(self): - """Return true if the object is detached.""" - return self.key is not None and \ - not self._attached - - @property - @util.dependencies("sqlalchemy.orm.session") - def _attached(self, sessionlib): - return self.session_id is not None and \ - self.session_id in sessionlib._sessions - - @property - @util.dependencies("sqlalchemy.orm.session") - def session(self, sessionlib): - """Return the owning :class:`.Session` for this instance, - or ``None`` if none available.""" - return sessionlib._state_session(self) - - @property - def object(self): - """Return the mapped object represented by this - :class:`.InstanceState`.""" - return self.obj() - - @property - def identity(self): - """Return the mapped identity of the mapped object. - This is the primary key identity as persisted by the ORM - which can always be passed directly to - :meth:`.Query.get`. - - Returns ``None`` if the object has no primary key identity. - - .. note:: - An object which is transient or pending - does **not** have a mapped identity until it is flushed, - even if its attributes include primary key values. - - """ - if self.key is None: - return None - else: - return self.key[1] - - @property - def identity_key(self): - """Return the identity key for the mapped object. - - This is the key used to locate the object within - the :attr:`.Session.identity_map` mapping. It contains - the identity as returned by :attr:`.identity` within it. - - - """ - # TODO: just change .key to .identity_key across - # the board ? probably - return self.key - - @util.memoized_property - def parents(self): - return {} - - @util.memoized_property - def _pending_mutations(self): - return {} - - @util.memoized_property - def mapper(self): - """Return the :class:`.Mapper` used for this mapepd object.""" - return self.manager.mapper - - @property - def has_identity(self): - """Return ``True`` if this object has an identity key. - - This should always have the same value as the - expression ``state.persistent or state.detached``. - - """ - return bool(self.key) - - def _detach(self): - self.session_id = self._strong_obj = None - - def _dispose(self): - self._detach() - del self.obj - - def _cleanup(self, ref): - instance_dict = self._instance_dict() - if instance_dict: - instance_dict.discard(self) - - self.callables = {} - self.session_id = self._strong_obj = None - del self.obj - - def obj(self): - return None - - @property - def dict(self): - o = self.obj() - if o is not None: - return base.instance_dict(o) - else: - return {} - - def _initialize_instance(*mixed, **kwargs): - self, instance, args = mixed[0], mixed[1], mixed[2:] - manager = self.manager - - manager.dispatch.init(self, args, kwargs) - - try: - return manager.original_init(*mixed[1:], **kwargs) - except: - manager.dispatch.init_failure(self, args, kwargs) - raise - - def get_history(self, key, passive): - return self.manager[key].impl.get_history(self, self.dict, passive) - - def get_impl(self, key): - return self.manager[key].impl - - def _get_pending_mutation(self, key): - if key not in self._pending_mutations: - self._pending_mutations[key] = PendingCollection() - return self._pending_mutations[key] - - def __getstate__(self): - state_dict = {'instance': self.obj()} - state_dict.update( - (k, self.__dict__[k]) for k in ( - 'committed_state', '_pending_mutations', 'modified', 'expired', - 'callables', 'key', 'parents', 'load_options', - 'class_', - ) if k in self.__dict__ - ) - if self.load_path: - state_dict['load_path'] = self.load_path.serialize() - - state_dict['manager'] = self.manager._serialize(self, state_dict) - - return state_dict - - def __setstate__(self, state_dict): - inst = state_dict['instance'] - if inst is not None: - self.obj = weakref.ref(inst, self._cleanup) - self.class_ = inst.__class__ - else: - # None being possible here generally new as of 0.7.4 - # due to storage of state in "parents". "class_" - # also new. - self.obj = None - self.class_ = state_dict['class_'] - - self.committed_state = state_dict.get('committed_state', {}) - self._pending_mutations = state_dict.get('_pending_mutations', {}) - self.parents = state_dict.get('parents', {}) - self.modified = state_dict.get('modified', False) - self.expired = state_dict.get('expired', False) - self.callables = state_dict.get('callables', {}) - - self.__dict__.update([ - (k, state_dict[k]) for k in ( - 'key', 'load_options', - ) if k in state_dict - ]) - - if 'load_path' in state_dict: - self.load_path = PathRegistry.\ - deserialize(state_dict['load_path']) - - state_dict['manager'](self, inst, state_dict) - - def _initialize(self, key): - """Set this attribute to an empty value or collection, - based on the AttributeImpl in use.""" - - self.manager.get_impl(key).initialize(self, self.dict) - - def _reset(self, dict_, key): - """Remove the given attribute and any - callables associated with it.""" - - old = dict_.pop(key, None) - if old is not None and self.manager[key].impl.collection: - self.manager[key].impl._invalidate_collection(old) - self.callables.pop(key, None) - - def _expire_attribute_pre_commit(self, dict_, key): - """a fast expire that can be called by column loaders during a load. - - The additional bookkeeping is finished up in commit_all(). - - Should only be called for scalar attributes. - - This method is actually called a lot with joined-table - loading, when the second table isn't present in the result. - - """ - dict_.pop(key, None) - self.callables[key] = self - - @classmethod - def _row_processor(cls, manager, fn, key): - impl = manager[key].impl - if impl.collection: - def _set_callable(state, dict_, row): - old = dict_.pop(key, None) - if old is not None: - impl._invalidate_collection(old) - state.callables[key] = fn - else: - def _set_callable(state, dict_, row): - state.callables[key] = fn - return _set_callable - - def _expire(self, dict_, modified_set): - self.expired = True - if self.modified: - modified_set.discard(self) - - self.modified = False - self._strong_obj = None - - self.committed_state.clear() - - InstanceState._pending_mutations._reset(self) - - # clear out 'parents' collection. not - # entirely clear how we can best determine - # which to remove, or not. - InstanceState.parents._reset(self) - - for key in self.manager: - impl = self.manager[key].impl - if impl.accepts_scalar_loader and \ - (impl.expire_missing or key in dict_): - self.callables[key] = self - old = dict_.pop(key, None) - if impl.collection and old is not None: - impl._invalidate_collection(old) - - self.manager.dispatch.expire(self, None) - - def _expire_attributes(self, dict_, attribute_names): - pending = self.__dict__.get('_pending_mutations', None) - - for key in attribute_names: - impl = self.manager[key].impl - if impl.accepts_scalar_loader: - self.callables[key] = self - old = dict_.pop(key, None) - if impl.collection and old is not None: - impl._invalidate_collection(old) - - self.committed_state.pop(key, None) - if pending: - pending.pop(key, None) - - self.manager.dispatch.expire(self, attribute_names) - - def __call__(self, state, passive): - """__call__ allows the InstanceState to act as a deferred - callable for loading expired attributes, which is also - serializable (picklable). - - """ - - if not passive & SQL_OK: - return PASSIVE_NO_RESULT - - toload = self.expired_attributes.\ - intersection(self.unmodified) - - self.manager.deferred_scalar_loader(self, toload) - - # if the loader failed, or this - # instance state didn't have an identity, - # the attributes still might be in the callables - # dict. ensure they are removed. - for k in toload.intersection(self.callables): - del self.callables[k] - - return ATTR_WAS_SET - - @property - def unmodified(self): - """Return the set of keys which have no uncommitted changes""" - - return set(self.manager).difference(self.committed_state) - - def unmodified_intersection(self, keys): - """Return self.unmodified.intersection(keys).""" - - return set(keys).intersection(self.manager).\ - difference(self.committed_state) - - @property - def unloaded(self): - """Return the set of keys which do not have a loaded value. - - This includes expired attributes and any other attribute that - was never populated or modified. - - """ - return set(self.manager).\ - difference(self.committed_state).\ - difference(self.dict) - - @property - def _unloaded_non_object(self): - return self.unloaded.intersection( - attr for attr in self.manager - if self.manager[attr].impl.accepts_scalar_loader - ) - - @property - def expired_attributes(self): - """Return the set of keys which are 'expired' to be loaded by - the manager's deferred scalar loader, assuming no pending - changes. - - see also the ``unmodified`` collection which is intersected - against this set when a refresh operation occurs. - - """ - return set([k for k, v in self.callables.items() if v is self]) - - def _instance_dict(self): - return None - - def _modified_event(self, dict_, attr, previous, collection=False): - if not attr.send_modified_events: - return - if attr.key not in self.committed_state: - if collection: - if previous is NEVER_SET: - if attr.key in dict_: - previous = dict_[attr.key] - - if previous not in (None, NO_VALUE, NEVER_SET): - previous = attr.copy(previous) - - self.committed_state[attr.key] = previous - - # assert self._strong_obj is None or self.modified - - if (self.session_id and self._strong_obj is None) \ - or not self.modified: - instance_dict = self._instance_dict() - if instance_dict: - instance_dict._modified.add(self) - - # only create _strong_obj link if attached - # to a session - - inst = self.obj() - if self.session_id: - self._strong_obj = inst - - if inst is None: - raise orm_exc.ObjectDereferencedError( - "Can't emit change event for attribute '%s' - " - "parent object of type %s has been garbage " - "collected." - % ( - self.manager[attr.key], - base.state_class_str(self) - )) - self.modified = True - - def _commit(self, dict_, keys): - """Commit attributes. - - This is used by a partial-attribute load operation to mark committed - those attributes which were refreshed from the database. - - Attributes marked as "expired" can potentially remain "expired" after - this step if a value was not populated in state.dict. - - """ - for key in keys: - self.committed_state.pop(key, None) - - self.expired = False - - for key in set(self.callables).\ - intersection(keys).\ - intersection(dict_): - del self.callables[key] - - def _commit_all(self, dict_, instance_dict=None): - """commit all attributes unconditionally. - - This is used after a flush() or a full load/refresh - to remove all pending state from the instance. - - - all attributes are marked as "committed" - - the "strong dirty reference" is removed - - the "modified" flag is set to False - - any "expired" markers/callables for attributes loaded are removed. - - Attributes marked as "expired" can potentially remain - "expired" after this step if a value was not populated in state.dict. - - """ - self._commit_all_states([(self, dict_)], instance_dict) - - @classmethod - def _commit_all_states(self, iter, instance_dict=None): - """Mass version of commit_all().""" - - for state, dict_ in iter: - state.committed_state.clear() - InstanceState._pending_mutations._reset(state) - - callables = state.callables - for key in list(callables): - if key in dict_ and callables[key] is state: - del callables[key] - - if instance_dict and state.modified: - instance_dict._modified.discard(state) - - state.modified = state.expired = False - state._strong_obj = None - - -class AttributeState(object): - """Provide an inspection interface corresponding - to a particular attribute on a particular mapped object. - - The :class:`.AttributeState` object is accessed - via the :attr:`.InstanceState.attrs` collection - of a particular :class:`.InstanceState`:: - - from sqlalchemy import inspect - - insp = inspect(some_mapped_object) - attr_state = insp.attrs.some_attribute - - """ - - def __init__(self, state, key): - self.state = state - self.key = key - - @property - def loaded_value(self): - """The current value of this attribute as loaded from the database. - - If the value has not been loaded, or is otherwise not present - in the object's dictionary, returns NO_VALUE. - - """ - return self.state.dict.get(self.key, NO_VALUE) - - @property - def value(self): - """Return the value of this attribute. - - This operation is equivalent to accessing the object's - attribute directly or via ``getattr()``, and will fire - off any pending loader callables if needed. - - """ - return self.state.manager[self.key].__get__( - self.state.obj(), self.state.class_) - - @property - def history(self): - """Return the current pre-flush change history for - this attribute, via the :class:`.History` interface. - - This method will **not** emit loader callables if the value of the - attribute is unloaded. - - .. seealso:: - - :meth:`.AttributeState.load_history` - retrieve history - using loader callables if the value is not locally present. - - :func:`.attributes.get_history` - underlying function - - """ - return self.state.get_history(self.key, - PASSIVE_NO_INITIALIZE) - - def load_history(self): - """Return the current pre-flush change history for - this attribute, via the :class:`.History` interface. - - This method **will** emit loader callables if the value of the - attribute is unloaded. - - .. seealso:: - - :attr:`.AttributeState.history` - - :func:`.attributes.get_history` - underlying function - - .. versionadded:: 0.9.0 - - """ - return self.state.get_history(self.key, - PASSIVE_OFF ^ INIT_OK) - - - -class PendingCollection(object): - """A writable placeholder for an unloaded collection. - - Stores items appended to and removed from a collection that has not yet - been loaded. When the collection is loaded, the changes stored in - PendingCollection are applied to it to produce the final result. - - """ - def __init__(self): - self.deleted_items = util.IdentitySet() - self.added_items = util.OrderedIdentitySet() - - def append(self, value): - if value in self.deleted_items: - self.deleted_items.remove(value) - else: - self.added_items.add(value) - - def remove(self, value): - if value in self.added_items: - self.added_items.remove(value) - else: - self.deleted_items.add(value) diff --git a/libs/sqlalchemy/orm/strategies.py b/libs/sqlalchemy/orm/strategies.py deleted file mode 100644 index 033e3d06..00000000 --- a/libs/sqlalchemy/orm/strategies.py +++ /dev/null @@ -1,1439 +0,0 @@ -# orm/strategies.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""sqlalchemy.orm.interfaces.LoaderStrategy - implementations, and related MapperOptions.""" - -from .. import exc as sa_exc, inspect -from .. import util, log, event -from ..sql import util as sql_util, visitors -from . import ( - attributes, interfaces, exc as orm_exc, loading, - unitofwork, util as orm_util - ) -from .state import InstanceState -from .util import _none_set -from . import properties -from .interfaces import ( - LoaderStrategy, StrategizedProperty - ) -from .session import _state_session -import itertools - -def _register_attribute(strategy, mapper, useobject, - compare_function=None, - typecallable=None, - uselist=False, - callable_=None, - proxy_property=None, - active_history=False, - impl_class=None, - **kw -): - - prop = strategy.parent_property - - attribute_ext = list(util.to_list(prop.extension, default=[])) - - listen_hooks = [] - - if useobject and prop.single_parent: - listen_hooks.append(single_parent_validator) - - if prop.key in prop.parent.validators: - fn, opts = prop.parent.validators[prop.key] - listen_hooks.append( - lambda desc, prop: orm_util._validator_events(desc, - prop.key, fn, **opts) - ) - - if useobject: - listen_hooks.append(unitofwork.track_cascade_events) - - # need to assemble backref listeners - # after the singleparentvalidator, mapper validator - backref = kw.pop('backref', None) - if backref: - listen_hooks.append( - lambda desc, prop: attributes.backref_listeners(desc, - backref, - uselist) - ) - - for m in mapper.self_and_descendants: - if prop is m._props.get(prop.key): - - desc = attributes.register_attribute_impl( - m.class_, - prop.key, - parent_token=prop, - uselist=uselist, - compare_function=compare_function, - useobject=useobject, - extension=attribute_ext, - trackparent=useobject and (prop.single_parent - or prop.direction is interfaces.ONETOMANY), - typecallable=typecallable, - callable_=callable_, - active_history=active_history, - impl_class=impl_class, - send_modified_events=not useobject or not prop.viewonly, - doc=prop.doc, - **kw - ) - - for hook in listen_hooks: - hook(desc, prop) - -@properties.ColumnProperty.strategy_for(instrument=False, deferred=False) -class UninstrumentedColumnLoader(LoaderStrategy): - """Represent the a non-instrumented MapperProperty. - - The polymorphic_on argument of mapper() often results in this, - if the argument is against the with_polymorphic selectable. - - """ - def __init__(self, parent): - super(UninstrumentedColumnLoader, self).__init__(parent) - self.columns = self.parent_property.columns - - def setup_query(self, context, entity, path, loadopt, adapter, - column_collection=None, **kwargs): - for c in self.columns: - if adapter: - c = adapter.columns[c] - column_collection.append(c) - - def create_row_processor(self, context, path, loadopt, mapper, row, adapter): - return None, None, None - - -@log.class_logger -@properties.ColumnProperty.strategy_for(instrument=True, deferred=False) -class ColumnLoader(LoaderStrategy): - """Provide loading behavior for a :class:`.ColumnProperty`.""" - - def __init__(self, parent): - super(ColumnLoader, self).__init__(parent) - self.columns = self.parent_property.columns - self.is_composite = hasattr(self.parent_property, 'composite_class') - - def setup_query(self, context, entity, path, loadopt, - adapter, column_collection, **kwargs): - for c in self.columns: - if adapter: - c = adapter.columns[c] - column_collection.append(c) - - def init_class_attribute(self, mapper): - self.is_class_level = True - coltype = self.columns[0].type - # TODO: check all columns ? check for foreign key as well? - active_history = self.parent_property.active_history or \ - self.columns[0].primary_key or \ - mapper.version_id_col in set(self.columns) - - _register_attribute(self, mapper, useobject=False, - compare_function=coltype.compare_values, - active_history=active_history - ) - - def create_row_processor(self, context, path, - loadopt, mapper, row, adapter): - key = self.key - # look through list of columns represented here - # to see which, if any, is present in the row. - for col in self.columns: - if adapter: - col = adapter.columns[col] - if col is not None and col in row: - def fetch_col(state, dict_, row): - dict_[key] = row[col] - return fetch_col, None, None - else: - def expire_for_non_present_col(state, dict_, row): - state._expire_attribute_pre_commit(dict_, key) - return expire_for_non_present_col, None, None - - - -@log.class_logger -@properties.ColumnProperty.strategy_for(deferred=True, instrument=True) -class DeferredColumnLoader(LoaderStrategy): - """Provide loading behavior for a deferred :class:`.ColumnProperty`.""" - - def __init__(self, parent): - super(DeferredColumnLoader, self).__init__(parent) - if hasattr(self.parent_property, 'composite_class'): - raise NotImplementedError("Deferred loading for composite " - "types not implemented yet") - self.columns = self.parent_property.columns - self.group = self.parent_property.group - - def create_row_processor(self, context, path, loadopt, mapper, row, adapter): - col = self.columns[0] - if adapter: - col = adapter.columns[col] - - key = self.key - if col in row: - return self.parent_property._get_strategy_by_cls(ColumnLoader).\ - create_row_processor( - context, path, loadopt, mapper, row, adapter) - - elif not self.is_class_level: - set_deferred_for_local_state = InstanceState._row_processor( - mapper.class_manager, - LoadDeferredColumns(key), key) - return set_deferred_for_local_state, None, None - else: - def reset_col_for_deferred(state, dict_, row): - # reset state on the key so that deferred callables - # fire off on next access. - state._reset(dict_, key) - return reset_col_for_deferred, None, None - - def init_class_attribute(self, mapper): - self.is_class_level = True - - _register_attribute(self, mapper, useobject=False, - compare_function=self.columns[0].type.compare_values, - callable_=self._load_for_state, - expire_missing=False - ) - - def setup_query(self, context, entity, path, loadopt, adapter, - only_load_props=None, **kwargs): - if ( - loadopt and self.group and - loadopt.local_opts.get('undefer_group', False) == self.group - ) or (only_load_props and self.key in only_load_props): - self.parent_property._get_strategy_by_cls(ColumnLoader).\ - setup_query(context, entity, - path, loadopt, adapter, **kwargs) - - def _load_for_state(self, state, passive): - if not state.key: - return attributes.ATTR_EMPTY - - if not passive & attributes.SQL_OK: - return attributes.PASSIVE_NO_RESULT - - localparent = state.manager.mapper - - if self.group: - toload = [ - p.key for p in - localparent.iterate_properties - if isinstance(p, StrategizedProperty) and - isinstance(p.strategy, DeferredColumnLoader) and - p.group == self.group - ] - else: - toload = [self.key] - - # narrow the keys down to just those which have no history - group = [k for k in toload if k in state.unmodified] - - session = _state_session(state) - if session is None: - raise orm_exc.DetachedInstanceError( - "Parent instance %s is not bound to a Session; " - "deferred load operation of attribute '%s' cannot proceed" % - (orm_util.state_str(state), self.key) - ) - - query = session.query(localparent) - if loading.load_on_ident(query, state.key, - only_load_props=group, refresh_state=state) is None: - raise orm_exc.ObjectDeletedError(state) - - return attributes.ATTR_WAS_SET - - - -class LoadDeferredColumns(object): - """serializable loader object used by DeferredColumnLoader""" - - def __init__(self, key): - self.key = key - - def __call__(self, state, passive=attributes.PASSIVE_OFF): - key = self.key - - localparent = state.manager.mapper - prop = localparent._props[key] - strategy = prop._strategies[DeferredColumnLoader] - return strategy._load_for_state(state, passive) - - - -class AbstractRelationshipLoader(LoaderStrategy): - """LoaderStratgies which deal with related objects.""" - - def __init__(self, parent): - super(AbstractRelationshipLoader, self).__init__(parent) - self.mapper = self.parent_property.mapper - self.target = self.parent_property.target - self.uselist = self.parent_property.uselist - - - -@log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="noload") -@properties.RelationshipProperty.strategy_for(lazy=None) -class NoLoader(AbstractRelationshipLoader): - """Provide loading behavior for a :class:`.RelationshipProperty` - with "lazy=None". - - """ - - def init_class_attribute(self, mapper): - self.is_class_level = True - - _register_attribute(self, mapper, - useobject=True, - uselist=self.parent_property.uselist, - typecallable=self.parent_property.collection_class, - ) - - def create_row_processor(self, context, path, loadopt, mapper, row, adapter): - def invoke_no_load(state, dict_, row): - state._initialize(self.key) - return invoke_no_load, None, None - - - -@log.class_logger -@properties.RelationshipProperty.strategy_for(lazy=True) -@properties.RelationshipProperty.strategy_for(lazy="select") -class LazyLoader(AbstractRelationshipLoader): - """Provide loading behavior for a :class:`.RelationshipProperty` - with "lazy=True", that is loads when first accessed. - - """ - - def __init__(self, parent): - super(LazyLoader, self).__init__(parent) - join_condition = self.parent_property._join_condition - self._lazywhere, \ - self._bind_to_col, \ - self._equated_columns = join_condition.create_lazy_clause() - - self._rev_lazywhere, \ - self._rev_bind_to_col, \ - self._rev_equated_columns = join_condition.create_lazy_clause( - reverse_direction=True) - - self.logger.info("%s lazy loading clause %s", self, self._lazywhere) - - # determine if our "lazywhere" clause is the same as the mapper's - # get() clause. then we can just use mapper.get() - self.use_get = not self.uselist and \ - self.mapper._get_clause[0].compare( - self._lazywhere, - use_proxies=True, - equivalents=self.mapper._equivalent_columns - ) - - if self.use_get: - for col in list(self._equated_columns): - if col in self.mapper._equivalent_columns: - for c in self.mapper._equivalent_columns[col]: - self._equated_columns[c] = self._equated_columns[col] - - self.logger.info("%s will use query.get() to " - "optimize instance loads" % self) - - def init_class_attribute(self, mapper): - self.is_class_level = True - - active_history = ( - self.parent_property.active_history or - self.parent_property.direction is not interfaces.MANYTOONE or - not self.use_get - ) - - # MANYTOONE currently only needs the - # "old" value for delete-orphan - # cascades. the required _SingleParentValidator - # will enable active_history - # in that case. otherwise we don't need the - # "old" value during backref operations. - _register_attribute(self, - mapper, - useobject=True, - callable_=self._load_for_state, - uselist=self.parent_property.uselist, - backref=self.parent_property.back_populates, - typecallable=self.parent_property.collection_class, - active_history=active_history - ) - - def lazy_clause(self, state, reverse_direction=False, - alias_secondary=False, - adapt_source=None, - passive=None): - if state is None: - return self._lazy_none_clause( - reverse_direction, - adapt_source=adapt_source) - - if not reverse_direction: - criterion, bind_to_col, rev = \ - self._lazywhere, \ - self._bind_to_col, \ - self._equated_columns - else: - criterion, bind_to_col, rev = \ - self._rev_lazywhere, \ - self._rev_bind_to_col, \ - self._rev_equated_columns - - if reverse_direction: - mapper = self.parent_property.mapper - else: - mapper = self.parent_property.parent - - o = state.obj() # strong ref - dict_ = attributes.instance_dict(o) - - # use the "committed state" only if we're in a flush - # for this state. - - if passive and passive & attributes.LOAD_AGAINST_COMMITTED: - def visit_bindparam(bindparam): - if bindparam._identifying_key in bind_to_col: - bindparam.callable = \ - lambda: mapper._get_committed_state_attr_by_column( - state, dict_, - bind_to_col[bindparam._identifying_key]) - else: - def visit_bindparam(bindparam): - if bindparam._identifying_key in bind_to_col: - bindparam.callable = \ - lambda: mapper._get_state_attr_by_column( - state, dict_, - bind_to_col[bindparam._identifying_key]) - - if self.parent_property.secondary is not None and alias_secondary: - criterion = sql_util.ClauseAdapter( - self.parent_property.secondary.alias()).\ - traverse(criterion) - - criterion = visitors.cloned_traverse( - criterion, {}, {'bindparam': visit_bindparam}) - - if adapt_source: - criterion = adapt_source(criterion) - return criterion - - def _lazy_none_clause(self, reverse_direction=False, adapt_source=None): - if not reverse_direction: - criterion, bind_to_col, rev = \ - self._lazywhere, \ - self._bind_to_col,\ - self._equated_columns - else: - criterion, bind_to_col, rev = \ - self._rev_lazywhere, \ - self._rev_bind_to_col, \ - self._rev_equated_columns - - criterion = sql_util.adapt_criterion_to_null(criterion, bind_to_col) - - if adapt_source: - criterion = adapt_source(criterion) - return criterion - - def _load_for_state(self, state, passive): - if not state.key and \ - ( - ( - not self.parent_property.load_on_pending - and not state._load_pending - ) - or not state.session_id - ): - return attributes.ATTR_EMPTY - - pending = not state.key - ident_key = None - - if ( - (not passive & attributes.SQL_OK and not self.use_get) - or - (not passive & attributes.NON_PERSISTENT_OK and pending) - ): - return attributes.PASSIVE_NO_RESULT - - session = _state_session(state) - if not session: - raise orm_exc.DetachedInstanceError( - "Parent instance %s is not bound to a Session; " - "lazy load operation of attribute '%s' cannot proceed" % - (orm_util.state_str(state), self.key) - ) - - # if we have a simple primary key load, check the - # identity map without generating a Query at all - if self.use_get: - ident = self._get_ident_for_use_get( - session, - state, - passive - ) - if attributes.PASSIVE_NO_RESULT in ident: - return attributes.PASSIVE_NO_RESULT - elif attributes.NEVER_SET in ident: - return attributes.NEVER_SET - - if _none_set.issuperset(ident): - return None - - ident_key = self.mapper.identity_key_from_primary_key(ident) - instance = loading.get_from_identity(session, ident_key, passive) - if instance is not None: - return instance - elif not passive & attributes.SQL_OK or \ - not passive & attributes.RELATED_OBJECT_OK: - return attributes.PASSIVE_NO_RESULT - - return self._emit_lazyload(session, state, ident_key, passive) - - def _get_ident_for_use_get(self, session, state, passive): - instance_mapper = state.manager.mapper - - if passive & attributes.LOAD_AGAINST_COMMITTED: - get_attr = instance_mapper._get_committed_state_attr_by_column - else: - get_attr = instance_mapper._get_state_attr_by_column - - dict_ = state.dict - - return [ - get_attr( - state, - dict_, - self._equated_columns[pk], - passive=passive) - for pk in self.mapper.primary_key - ] - - @util.dependencies("sqlalchemy.orm.strategy_options") - def _emit_lazyload(self, strategy_options, session, state, ident_key, passive): - q = session.query(self.mapper)._adapt_all_clauses() - - q = q._with_invoke_all_eagers(False) - - pending = not state.key - - # don't autoflush on pending - if pending: - q = q.autoflush(False) - - if state.load_path: - q = q._with_current_path(state.load_path[self.parent_property]) - - if state.load_options: - q = q._conditional_options(*state.load_options) - - if self.use_get: - return loading.load_on_ident(q, ident_key) - - if self.parent_property.order_by: - q = q.order_by(*util.to_list(self.parent_property.order_by)) - - for rev in self.parent_property._reverse_property: - # reverse props that are MANYTOONE are loading *this* - # object from get(), so don't need to eager out to those. - if rev.direction is interfaces.MANYTOONE and \ - rev._use_get and \ - not isinstance(rev.strategy, LazyLoader): - q = q.options(strategy_options.Load(rev.parent).lazyload(rev.key)) - - lazy_clause = self.lazy_clause(state, passive=passive) - - if pending: - bind_values = sql_util.bind_values(lazy_clause) - if None in bind_values: - return None - - q = q.filter(lazy_clause) - - result = q.all() - if self.uselist: - return result - else: - l = len(result) - if l: - if l > 1: - util.warn( - "Multiple rows returned with " - "uselist=False for lazily-loaded attribute '%s' " - % self.parent_property) - - return result[0] - else: - return None - - def create_row_processor(self, context, path, loadopt, - mapper, row, adapter): - key = self.key - if not self.is_class_level: - # we are not the primary manager for this attribute - # on this class - set up a - # per-instance lazyloader, which will override the - # class-level behavior. - # this currently only happens when using a - # "lazyload" option on a "no load" - # attribute - "eager" attributes always have a - # class-level lazyloader installed. - set_lazy_callable = InstanceState._row_processor( - mapper.class_manager, - LoadLazyAttribute(key), key) - - return set_lazy_callable, None, None - else: - def reset_for_lazy_callable(state, dict_, row): - # we are the primary manager for this attribute on - # this class - reset its - # per-instance attribute state, so that the class-level - # lazy loader is - # executed when next referenced on this instance. - # this is needed in - # populate_existing() types of scenarios to reset - # any existing state. - state._reset(dict_, key) - - return reset_for_lazy_callable, None, None - - - -class LoadLazyAttribute(object): - """serializable loader object used by LazyLoader""" - - def __init__(self, key): - self.key = key - - def __call__(self, state, passive=attributes.PASSIVE_OFF): - key = self.key - instance_mapper = state.manager.mapper - prop = instance_mapper._props[key] - strategy = prop._strategies[LazyLoader] - - return strategy._load_for_state(state, passive) - - -@properties.RelationshipProperty.strategy_for(lazy="immediate") -class ImmediateLoader(AbstractRelationshipLoader): - def init_class_attribute(self, mapper): - self.parent_property.\ - _get_strategy_by_cls(LazyLoader).\ - init_class_attribute(mapper) - - def setup_query(self, context, entity, - path, loadopt, adapter, column_collection=None, - parentmapper=None, **kwargs): - pass - - def create_row_processor(self, context, path, loadopt, - mapper, row, adapter): - def load_immediate(state, dict_, row): - state.get_impl(self.key).get(state, dict_) - - return None, None, load_immediate - - -@log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="subquery") -class SubqueryLoader(AbstractRelationshipLoader): - def __init__(self, parent): - super(SubqueryLoader, self).__init__(parent) - self.join_depth = self.parent_property.join_depth - - def init_class_attribute(self, mapper): - self.parent_property.\ - _get_strategy_by_cls(LazyLoader).\ - init_class_attribute(mapper) - - def setup_query(self, context, entity, - path, loadopt, adapter, - column_collection=None, - parentmapper=None, **kwargs): - - if not context.query._enable_eagerloads: - return - - path = path[self.parent_property] - - # build up a path indicating the path from the leftmost - # entity to the thing we're subquery loading. - with_poly_info = path.get(context.attributes, - "path_with_polymorphic", None) - if with_poly_info is not None: - effective_entity = with_poly_info.entity - else: - effective_entity = self.mapper - - subq_path = context.attributes.get(('subquery_path', None), - orm_util.PathRegistry.root) - - subq_path = subq_path + path - - # if not via query option, check for - # a cycle - if not path.contains(context.attributes, "loader"): - if self.join_depth: - if path.length / 2 > self.join_depth: - return - elif subq_path.contains_mapper(self.mapper): - return - - subq_mapper, leftmost_mapper, leftmost_attr, leftmost_relationship = \ - self._get_leftmost(subq_path) - - orig_query = context.attributes.get( - ("orig_query", SubqueryLoader), - context.query) - - # generate a new Query from the original, then - # produce a subquery from it. - left_alias = self._generate_from_original_query( - orig_query, leftmost_mapper, - leftmost_attr, leftmost_relationship, - entity.mapper - ) - - # generate another Query that will join the - # left alias to the target relationships. - # basically doing a longhand - # "from_self()". (from_self() itself not quite industrial - # strength enough for all contingencies...but very close) - q = orig_query.session.query(effective_entity) - q._attributes = { - ("orig_query", SubqueryLoader): orig_query, - ('subquery_path', None): subq_path - } - q = q._enable_single_crit(False) - - to_join, local_attr, parent_alias = \ - self._prep_for_joins(left_alias, subq_path) - q = q.order_by(*local_attr) - q = q.add_columns(*local_attr) - - q = self._apply_joins(q, to_join, left_alias, - parent_alias, effective_entity) - - q = self._setup_options(q, subq_path, orig_query, effective_entity) - q = self._setup_outermost_orderby(q) - - # add new query to attributes to be picked up - # by create_row_processor - path.set(context.attributes, "subquery", q) - - def _get_leftmost(self, subq_path): - subq_path = subq_path.path - subq_mapper = orm_util._class_to_mapper(subq_path[0]) - - # determine attributes of the leftmost mapper - if self.parent.isa(subq_mapper) and self.parent_property is subq_path[1]: - leftmost_mapper, leftmost_prop = \ - self.parent, self.parent_property - else: - leftmost_mapper, leftmost_prop = \ - subq_mapper, \ - subq_path[1] - - leftmost_cols = leftmost_prop.local_columns - - leftmost_attr = [ - leftmost_mapper._columntoproperty[c].class_attribute - for c in leftmost_cols - ] - return subq_mapper, leftmost_mapper, leftmost_attr, leftmost_prop - - def _generate_from_original_query(self, - orig_query, leftmost_mapper, - leftmost_attr, leftmost_relationship, - entity_mapper - ): - # reformat the original query - # to look only for significant columns - q = orig_query._clone().correlate(None) - - # set a real "from" if not present, as this is more - # accurate than just going off of the column expression - if not q._from_obj and entity_mapper.isa(leftmost_mapper): - q._set_select_from([entity_mapper], False) - - target_cols = q._adapt_col_list(leftmost_attr) - - # select from the identity columns of the outer - q._set_entities(target_cols) - - distinct_target_key = leftmost_relationship.distinct_target_key - - if distinct_target_key is True: - q._distinct = True - elif distinct_target_key is None: - # if target_cols refer to a non-primary key or only - # part of a composite primary key, set the q as distinct - for t in set(c.table for c in target_cols): - if not set(target_cols).issuperset(t.primary_key): - q._distinct = True - break - - if q._order_by is False: - q._order_by = leftmost_mapper.order_by - - # don't need ORDER BY if no limit/offset - if q._limit is None and q._offset is None: - q._order_by = None - - # the original query now becomes a subquery - # which we'll join onto. - - embed_q = q.with_labels().subquery() - left_alias = orm_util.AliasedClass(leftmost_mapper, embed_q, - use_mapper_path=True) - return left_alias - - def _prep_for_joins(self, left_alias, subq_path): - # figure out what's being joined. a.k.a. the fun part - to_join = [] - pairs = list(subq_path.pairs()) - - for i, (mapper, prop) in enumerate(pairs): - if i > 0: - # look at the previous mapper in the chain - - # if it is as or more specific than this prop's - # mapper, use that instead. - # note we have an assumption here that - # the non-first element is always going to be a mapper, - # not an AliasedClass - - prev_mapper = pairs[i - 1][1].mapper - to_append = prev_mapper if prev_mapper.isa(mapper) else mapper - else: - to_append = mapper - - to_join.append((to_append, prop.key)) - - # determine the immediate parent class we are joining from, - # which needs to be aliased. - if len(to_join) > 1: - info = inspect(to_join[-1][0]) - - if len(to_join) < 2: - # in the case of a one level eager load, this is the - # leftmost "left_alias". - parent_alias = left_alias - elif info.mapper.isa(self.parent): - # In the case of multiple levels, retrieve - # it from subq_path[-2]. This is the same as self.parent - # in the vast majority of cases, and [ticket:2014] - # illustrates a case where sub_path[-2] is a subclass - # of self.parent - parent_alias = orm_util.AliasedClass(to_join[-1][0], - use_mapper_path=True) - else: - # if of_type() were used leading to this relationship, - # self.parent is more specific than subq_path[-2] - parent_alias = orm_util.AliasedClass(self.parent, - use_mapper_path=True) - - local_cols = self.parent_property.local_columns - - local_attr = [ - getattr(parent_alias, self.parent._columntoproperty[c].key) - for c in local_cols - ] - return to_join, local_attr, parent_alias - - def _apply_joins(self, q, to_join, left_alias, parent_alias, - effective_entity): - for i, (mapper, key) in enumerate(to_join): - - # we need to use query.join() as opposed to - # orm.join() here because of the - # rich behavior it brings when dealing with - # "with_polymorphic" mappers. "aliased" - # and "from_joinpoint" take care of most of - # the chaining and aliasing for us. - - first = i == 0 - middle = i < len(to_join) - 1 - second_to_last = i == len(to_join) - 2 - last = i == len(to_join) - 1 - - if first: - attr = getattr(left_alias, key) - if last and effective_entity is not self.mapper: - attr = attr.of_type(effective_entity) - else: - if last and effective_entity is not self.mapper: - attr = getattr(parent_alias, key).\ - of_type(effective_entity) - else: - attr = key - - if second_to_last: - q = q.join(parent_alias, attr, from_joinpoint=True) - else: - q = q.join(attr, aliased=middle, from_joinpoint=True) - return q - - def _setup_options(self, q, subq_path, orig_query, effective_entity): - # propagate loader options etc. to the new query. - # these will fire relative to subq_path. - q = q._with_current_path(subq_path) - q = q._conditional_options(*orig_query._with_options) - if orig_query._populate_existing: - q._populate_existing = orig_query._populate_existing - - return q - - def _setup_outermost_orderby(self, q): - if self.parent_property.order_by: - # if there's an ORDER BY, alias it the same - # way joinedloader does, but we have to pull out - # the "eagerjoin" from the query. - # this really only picks up the "secondary" table - # right now. - eagerjoin = q._from_obj[0] - eager_order_by = \ - eagerjoin._target_adapter.\ - copy_and_process( - util.to_list( - self.parent_property.order_by - ) - ) - q = q.order_by(*eager_order_by) - return q - - class _SubqCollections(object): - """Given a :class:`.Query` used to emit the "subquery load", - provide a load interface that executes the query at the - first moment a value is needed. - - """ - _data = None - - def __init__(self, subq): - self.subq = subq - - def get(self, key, default): - if self._data is None: - self._load() - return self._data.get(key, default) - - def _load(self): - self._data = dict( - (k, [vv[0] for vv in v]) - for k, v in itertools.groupby( - self.subq, - lambda x: x[1:] - ) - ) - - def loader(self, state, dict_, row): - if self._data is None: - self._load() - - def create_row_processor(self, context, path, loadopt, - mapper, row, adapter): - if not self.parent.class_manager[self.key].impl.supports_population: - raise sa_exc.InvalidRequestError( - "'%s' does not support object " - "population - eager loading cannot be applied." % - self) - - path = path[self.parent_property] - - subq = path.get(context.attributes, 'subquery') - - if subq is None: - return None, None, None - - local_cols = self.parent_property.local_columns - - # cache the loaded collections in the context - # so that inheriting mappers don't re-load when they - # call upon create_row_processor again - collections = path.get(context.attributes, "collections") - if collections is None: - collections = self._SubqCollections(subq) - path.set(context.attributes, 'collections', collections) - - if adapter: - local_cols = [adapter.columns[c] for c in local_cols] - - if self.uselist: - return self._create_collection_loader(collections, local_cols) - else: - return self._create_scalar_loader(collections, local_cols) - - def _create_collection_loader(self, collections, local_cols): - def load_collection_from_subq(state, dict_, row): - collection = collections.get( - tuple([row[col] for col in local_cols]), - () - ) - state.get_impl(self.key).\ - set_committed_value(state, dict_, collection) - - return load_collection_from_subq, None, None, collections.loader - - def _create_scalar_loader(self, collections, local_cols): - def load_scalar_from_subq(state, dict_, row): - collection = collections.get( - tuple([row[col] for col in local_cols]), - (None,) - ) - if len(collection) > 1: - util.warn( - "Multiple rows returned with " - "uselist=False for eagerly-loaded attribute '%s' " - % self) - - scalar = collection[0] - state.get_impl(self.key).\ - set_committed_value(state, dict_, scalar) - - return load_scalar_from_subq, None, None, collections.loader - - - -@log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="joined") -@properties.RelationshipProperty.strategy_for(lazy=False) -class JoinedLoader(AbstractRelationshipLoader): - """Provide loading behavior for a :class:`.RelationshipProperty` - using joined eager loading. - - """ - def __init__(self, parent): - super(JoinedLoader, self).__init__(parent) - self.join_depth = self.parent_property.join_depth - - def init_class_attribute(self, mapper): - self.parent_property.\ - _get_strategy_by_cls(LazyLoader).init_class_attribute(mapper) - - def setup_query(self, context, entity, path, loadopt, adapter, \ - column_collection=None, parentmapper=None, - allow_innerjoin=True, - **kwargs): - """Add a left outer join to the statement thats being constructed.""" - - if not context.query._enable_eagerloads: - return - - path = path[self.parent_property] - - with_polymorphic = None - - user_defined_adapter = self._init_user_defined_eager_proc( - loadopt, context) if loadopt else False - - if user_defined_adapter is not False: - clauses, adapter, add_to_collection = \ - self._setup_query_on_user_defined_adapter( - context, entity, path, adapter, - user_defined_adapter - ) - else: - # if not via query option, check for - # a cycle - if not path.contains(context.attributes, "loader"): - if self.join_depth: - if path.length / 2 > self.join_depth: - return - elif path.contains_mapper(self.mapper): - return - - clauses, adapter, add_to_collection, \ - allow_innerjoin = self._generate_row_adapter( - context, entity, path, loadopt, adapter, - column_collection, parentmapper, allow_innerjoin - ) - - with_poly_info = path.get( - context.attributes, - "path_with_polymorphic", - None - ) - if with_poly_info is not None: - with_polymorphic = with_poly_info.with_polymorphic_mappers - else: - with_polymorphic = None - - path = path[self.mapper] - - for value in self.mapper._iterate_polymorphic_properties( - mappers=with_polymorphic): - value.setup( - context, - entity, - path, - clauses, - parentmapper=self.mapper, - column_collection=add_to_collection, - allow_innerjoin=allow_innerjoin) - - if with_poly_info is not None and \ - None in set(context.secondary_columns): - raise sa_exc.InvalidRequestError( - "Detected unaliased columns when generating joined " - "load. Make sure to use aliased=True or flat=True " - "when using joined loading with with_polymorphic()." - ) - - def _init_user_defined_eager_proc(self, loadopt, context): - - # check if the opt applies at all - if "eager_from_alias" not in loadopt.local_opts: - # nope - return False - - path = loadopt.path.parent - - # the option applies. check if the "user_defined_eager_row_processor" - # has been built up. - adapter = path.get(context.attributes, - "user_defined_eager_row_processor", False) - if adapter is not False: - # just return it - return adapter - - # otherwise figure it out. - alias = loadopt.local_opts["eager_from_alias"] - - root_mapper, prop = path[-2:] - - #from .mapper import Mapper - #from .interfaces import MapperProperty - #assert isinstance(root_mapper, Mapper) - #assert isinstance(prop, MapperProperty) - - if alias is not None: - if isinstance(alias, str): - alias = prop.target.alias(alias) - adapter = sql_util.ColumnAdapter(alias, - equivalents=prop.mapper._equivalent_columns) - else: - if path.contains(context.attributes, "path_with_polymorphic"): - with_poly_info = path.get(context.attributes, - "path_with_polymorphic") - adapter = orm_util.ORMAdapter( - with_poly_info.entity, - equivalents=prop.mapper._equivalent_columns) - else: - adapter = context.query._polymorphic_adapters.get(prop.mapper, None) - path.set(context.attributes, - "user_defined_eager_row_processor", - adapter) - - return adapter - - def _setup_query_on_user_defined_adapter(self, context, entity, - path, adapter, user_defined_adapter): - - # apply some more wrapping to the "user defined adapter" - # if we are setting up the query for SQL render. - adapter = entity._get_entity_clauses(context.query, context) - - if adapter and user_defined_adapter: - user_defined_adapter = user_defined_adapter.wrap(adapter) - path.set(context.attributes, "user_defined_eager_row_processor", - user_defined_adapter) - elif adapter: - user_defined_adapter = adapter - path.set(context.attributes, "user_defined_eager_row_processor", - user_defined_adapter) - - add_to_collection = context.primary_columns - return user_defined_adapter, adapter, add_to_collection - - def _generate_row_adapter(self, - context, entity, path, loadopt, adapter, - column_collection, parentmapper, allow_innerjoin - ): - with_poly_info = path.get( - context.attributes, - "path_with_polymorphic", - None - ) - if with_poly_info: - to_adapt = with_poly_info.entity - else: - to_adapt = orm_util.AliasedClass(self.mapper, - flat=True, - use_mapper_path=True) - clauses = orm_util.ORMAdapter( - to_adapt, - equivalents=self.mapper._equivalent_columns, - adapt_required=True) - assert clauses.aliased_class is not None - - if self.parent_property.direction != interfaces.MANYTOONE: - context.multi_row_eager_loaders = True - - innerjoin = allow_innerjoin and ( - loadopt.local_opts.get( - 'innerjoin', self.parent_property.innerjoin) - if loadopt is not None - else self.parent_property.innerjoin - ) - if not innerjoin: - # if this is an outer join, all eager joins from - # here must also be outer joins - allow_innerjoin = False - - context.create_eager_joins.append( - (self._create_eager_join, context, - entity, path, adapter, - parentmapper, clauses, innerjoin) - ) - - add_to_collection = context.secondary_columns - path.set(context.attributes, "eager_row_processor", clauses) - - return clauses, adapter, add_to_collection, allow_innerjoin - - def _create_eager_join(self, context, entity, - path, adapter, parentmapper, - clauses, innerjoin): - - if parentmapper is None: - localparent = entity.mapper - else: - localparent = parentmapper - - # whether or not the Query will wrap the selectable in a subquery, - # and then attach eager load joins to that (i.e., in the case of - # LIMIT/OFFSET etc.) - should_nest_selectable = context.multi_row_eager_loaders and \ - context.query._should_nest_selectable - - entity_key = None - - if entity not in context.eager_joins and \ - not should_nest_selectable and \ - context.from_clause: - index, clause = \ - sql_util.find_join_source( - context.from_clause, entity.selectable) - if clause is not None: - # join to an existing FROM clause on the query. - # key it to its list index in the eager_joins dict. - # Query._compile_context will adapt as needed and - # append to the FROM clause of the select(). - entity_key, default_towrap = index, clause - - if entity_key is None: - entity_key, default_towrap = entity, entity.selectable - - towrap = context.eager_joins.setdefault(entity_key, default_towrap) - - if adapter: - if getattr(adapter, 'aliased_class', None): - onclause = getattr( - adapter.aliased_class, self.key, - self.parent_property) - else: - onclause = getattr( - orm_util.AliasedClass( - self.parent, - adapter.selectable, - use_mapper_path=True - ), - self.key, self.parent_property - ) - - else: - onclause = self.parent_property - - assert clauses.aliased_class is not None - context.eager_joins[entity_key] = eagerjoin = \ - orm_util.join( - towrap, - clauses.aliased_class, - onclause, - isouter=not innerjoin - ) - - # send a hint to the Query as to where it may "splice" this join - eagerjoin.stop_on = entity.selectable - - if self.parent_property.secondary is None and \ - not parentmapper: - # for parentclause that is the non-eager end of the join, - # ensure all the parent cols in the primaryjoin are actually - # in the - # columns clause (i.e. are not deferred), so that aliasing applied - # by the Query propagates those columns outward. - # This has the effect - # of "undefering" those columns. - for col in sql_util._find_columns( - self.parent_property.primaryjoin): - if localparent.mapped_table.c.contains_column(col): - if adapter: - col = adapter.columns[col] - context.primary_columns.append(col) - - if self.parent_property.order_by: - context.eager_order_by += \ - eagerjoin._target_adapter.\ - copy_and_process( - util.to_list( - self.parent_property.order_by - ) - ) - - def _create_eager_adapter(self, context, row, adapter, path, loadopt): - user_defined_adapter = self._init_user_defined_eager_proc( - loadopt, context) if loadopt else False - - if user_defined_adapter is not False: - decorator = user_defined_adapter - # user defined eagerloads are part of the "primary" - # portion of the load. - # the adapters applied to the Query should be honored. - if context.adapter and decorator: - decorator = decorator.wrap(context.adapter) - elif context.adapter: - decorator = context.adapter - else: - decorator = path.get(context.attributes, "eager_row_processor") - if decorator is None: - return False - - try: - self.mapper.identity_key_from_row(row, decorator) - return decorator - except KeyError: - # no identity key - dont return a row - # processor, will cause a degrade to lazy - return False - - def create_row_processor(self, context, path, loadopt, mapper, row, adapter): - if not self.parent.class_manager[self.key].impl.supports_population: - raise sa_exc.InvalidRequestError( - "'%s' does not support object " - "population - eager loading cannot be applied." % - self) - - our_path = path[self.parent_property] - - eager_adapter = self._create_eager_adapter( - context, - row, - adapter, our_path, loadopt) - - if eager_adapter is not False: - key = self.key - - _instance = loading.instance_processor( - self.mapper, - context, - our_path[self.mapper], - eager_adapter) - - if not self.uselist: - return self._create_scalar_loader(context, key, _instance) - else: - return self._create_collection_loader(context, key, _instance) - else: - return self.parent_property.\ - _get_strategy_by_cls(LazyLoader).\ - create_row_processor( - context, path, loadopt, - mapper, row, adapter) - - def _create_collection_loader(self, context, key, _instance): - def load_collection_from_joined_new_row(state, dict_, row): - collection = attributes.init_state_collection( - state, dict_, key) - result_list = util.UniqueAppender(collection, - 'append_without_event') - context.attributes[(state, key)] = result_list - _instance(row, result_list) - - def load_collection_from_joined_existing_row(state, dict_, row): - if (state, key) in context.attributes: - result_list = context.attributes[(state, key)] - else: - # appender_key can be absent from context.attributes - # with isnew=False when self-referential eager loading - # is used; the same instance may be present in two - # distinct sets of result columns - collection = attributes.init_state_collection(state, - dict_, key) - result_list = util.UniqueAppender( - collection, - 'append_without_event') - context.attributes[(state, key)] = result_list - _instance(row, result_list) - - def load_collection_from_joined_exec(state, dict_, row): - _instance(row, None) - - return load_collection_from_joined_new_row, \ - load_collection_from_joined_existing_row, \ - None, load_collection_from_joined_exec - - def _create_scalar_loader(self, context, key, _instance): - def load_scalar_from_joined_new_row(state, dict_, row): - # set a scalar object instance directly on the parent - # object, bypassing InstrumentedAttribute event handlers. - dict_[key] = _instance(row, None) - - def load_scalar_from_joined_existing_row(state, dict_, row): - # call _instance on the row, even though the object has - # been created, so that we further descend into properties - existing = _instance(row, None) - if existing is not None \ - and key in dict_ \ - and existing is not dict_[key]: - util.warn( - "Multiple rows returned with " - "uselist=False for eagerly-loaded attribute '%s' " - % self) - - def load_scalar_from_joined_exec(state, dict_, row): - _instance(row, None) - - return load_scalar_from_joined_new_row, \ - load_scalar_from_joined_existing_row, \ - None, load_scalar_from_joined_exec - - - -def single_parent_validator(desc, prop): - def _do_check(state, value, oldvalue, initiator): - if value is not None and initiator.key == prop.key: - hasparent = initiator.hasparent(attributes.instance_state(value)) - if hasparent and oldvalue is not value: - raise sa_exc.InvalidRequestError( - "Instance %s is already associated with an instance " - "of %s via its %s attribute, and is only allowed a " - "single parent." % - (orm_util.instance_str(value), state.class_, prop) - ) - return value - - def append(state, value, initiator): - return _do_check(state, value, None, initiator) - - def set_(state, value, oldvalue, initiator): - return _do_check(state, value, oldvalue, initiator) - - event.listen(desc, 'append', append, raw=True, retval=True, - active_history=True) - event.listen(desc, 'set', set_, raw=True, retval=True, - active_history=True) diff --git a/libs/sqlalchemy/orm/strategy_options.py b/libs/sqlalchemy/orm/strategy_options.py deleted file mode 100644 index 6e838ccb..00000000 --- a/libs/sqlalchemy/orm/strategy_options.py +++ /dev/null @@ -1,924 +0,0 @@ -# orm/strategy_options.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -""" - -""" - -from .interfaces import MapperOption, PropComparator -from .. import util -from ..sql.base import _generative, Generative -from .. import exc as sa_exc, inspect -from .base import _is_aliased_class, _class_to_mapper -from . import util as orm_util -from .path_registry import PathRegistry, TokenRegistry, \ - _WILDCARD_TOKEN, _DEFAULT_TOKEN - -class Load(Generative, MapperOption): - """Represents loader options which modify the state of a - :class:`.Query` in order to affect how various mapped attributes are loaded. - - .. versionadded:: 0.9.0 The :meth:`.Load` system is a new foundation for - the existing system of loader options, including options such as - :func:`.orm.joinedload`, :func:`.orm.defer`, and others. In particular, - it introduces a new method-chained system that replaces the need for - dot-separated paths as well as "_all()" options such as :func:`.orm.joinedload_all`. - - A :class:`.Load` object can be used directly or indirectly. To use one - directly, instantiate given the parent class. This style of usage is - useful when dealing with a :class:`.Query` that has multiple entities, - or when producing a loader option that can be applied generically to - any style of query:: - - myopt = Load(MyClass).joinedload("widgets") - - The above ``myopt`` can now be used with :meth:`.Query.options`:: - - session.query(MyClass).options(myopt) - - The :class:`.Load` construct is invoked indirectly whenever one makes use - of the various loader options that are present in ``sqlalchemy.orm``, including - options such as :func:`.orm.joinedload`, :func:`.orm.defer`, :func:`.orm.subqueryload`, - and all the rest. These constructs produce an "anonymous" form of the - :class:`.Load` object which tracks attributes and options, but is not linked - to a parent class until it is associated with a parent :class:`.Query`:: - - # produce "unbound" Load object - myopt = joinedload("widgets") - - # when applied using options(), the option is "bound" to the - # class observed in the given query, e.g. MyClass - session.query(MyClass).options(myopt) - - Whether the direct or indirect style is used, the :class:`.Load` object - returned now represents a specific "path" along the entities of a :class:`.Query`. - This path can be traversed using a standard method-chaining approach. - Supposing a class hierarchy such as ``User``, ``User.addresses -> Address``, - ``User.orders -> Order`` and ``Order.items -> Item``, we can specify a variety - of loader options along each element in the "path":: - - session.query(User).options( - joinedload("addresses"), - subqueryload("orders").joinedload("items") - ) - - Where above, the ``addresses`` collection will be joined-loaded, the - ``orders`` collection will be subquery-loaded, and within that subquery load - the ``items`` collection will be joined-loaded. - - - """ - def __init__(self, entity): - insp = inspect(entity) - self.path = insp._path_registry - self.context = {} - self.local_opts = {} - - def _generate(self): - cloned = super(Load, self)._generate() - cloned.local_opts = {} - return cloned - - strategy = None - propagate_to_loaders = False - - def process_query(self, query): - self._process(query, True) - - def process_query_conditionally(self, query): - self._process(query, False) - - def _process(self, query, raiseerr): - current_path = query._current_path - if current_path: - for (token, start_path), loader in self.context.items(): - chopped_start_path = self._chop_path(start_path, current_path) - if chopped_start_path is not None: - query._attributes[(token, chopped_start_path)] = loader - else: - query._attributes.update(self.context) - - def _generate_path(self, path, attr, wildcard_key, raiseerr=True): - if raiseerr and not path.has_entity: - if isinstance(path, TokenRegistry): - raise sa_exc.ArgumentError( - "Wildcard token cannot be followed by another entity") - else: - raise sa_exc.ArgumentError( - "Attribute '%s' of entity '%s' does not " - "refer to a mapped entity" % - (path.prop.key, path.parent.entity) - ) - - if isinstance(attr, util.string_types): - default_token = attr.endswith(_DEFAULT_TOKEN) - if attr.endswith(_WILDCARD_TOKEN) or default_token: - if default_token: - self.propagate_to_loaders = False - if wildcard_key: - attr = "%s:%s" % (wildcard_key, attr) - return path.token(attr) - - try: - # use getattr on the class to work around - # synonyms, hybrids, etc. - attr = getattr(path.entity.class_, attr) - except AttributeError: - if raiseerr: - raise sa_exc.ArgumentError( - "Can't find property named '%s' on the " - "mapped entity %s in this Query. " % ( - attr, path.entity) - ) - else: - return None - else: - attr = attr.property - - path = path[attr] - else: - prop = attr.property - - if not prop.parent.common_parent(path.mapper): - if raiseerr: - raise sa_exc.ArgumentError("Attribute '%s' does not " - "link from element '%s'" % (attr, path.entity)) - else: - return None - - if getattr(attr, '_of_type', None): - ac = attr._of_type - ext_info = inspect(ac) - - path_element = ext_info.mapper - if not ext_info.is_aliased_class: - ac = orm_util.with_polymorphic( - ext_info.mapper.base_mapper, - ext_info.mapper, aliased=True, - _use_mapper_path=True) - path.entity_path[prop].set(self.context, - "path_with_polymorphic", inspect(ac)) - path = path[prop][path_element] - else: - path = path[prop] - - if path.has_entity: - path = path.entity_path - return path - - def _coerce_strat(self, strategy): - if strategy is not None: - strategy = tuple(sorted(strategy.items())) - return strategy - - @_generative - def set_relationship_strategy(self, attr, strategy, propagate_to_loaders=True): - strategy = self._coerce_strat(strategy) - - self.propagate_to_loaders = propagate_to_loaders - # if the path is a wildcard, this will set propagate_to_loaders=False - self.path = self._generate_path(self.path, attr, "relationship") - self.strategy = strategy - if strategy is not None: - self._set_path_strategy() - - @_generative - def set_column_strategy(self, attrs, strategy, opts=None): - strategy = self._coerce_strat(strategy) - - for attr in attrs: - path = self._generate_path(self.path, attr, "column") - cloned = self._generate() - cloned.strategy = strategy - cloned.path = path - cloned.propagate_to_loaders = True - if opts: - cloned.local_opts.update(opts) - cloned._set_path_strategy() - - def _set_path_strategy(self): - if self.path.has_entity: - self.path.parent.set(self.context, "loader", self) - else: - self.path.set(self.context, "loader", self) - - def __getstate__(self): - d = self.__dict__.copy() - d["path"] = self.path.serialize() - return d - - def __setstate__(self, state): - self.__dict__.update(state) - self.path = PathRegistry.deserialize(self.path) - - def _chop_path(self, to_chop, path): - i = -1 - - for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)): - if isinstance(c_token, util.string_types): - # TODO: this is approximated from the _UnboundLoad - # version and probably has issues, not fully covered. - - if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): - return to_chop - elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_token.key: - return None - - if c_token is p_token: - continue - else: - return None - return to_chop[i+1:] - - -class _UnboundLoad(Load): - """Represent a loader option that isn't tied to a root entity. - - The loader option will produce an entity-linked :class:`.Load` - object when it is passed :meth:`.Query.options`. - - This provides compatibility with the traditional system - of freestanding options, e.g. ``joinedload('x.y.z')``. - - """ - def __init__(self): - self.path = () - self._to_bind = set() - self.local_opts = {} - - _is_chain_link = False - - def _set_path_strategy(self): - self._to_bind.add(self) - - def _generate_path(self, path, attr, wildcard_key): - if wildcard_key and isinstance(attr, util.string_types) and \ - attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN): - if attr == _DEFAULT_TOKEN: - self.propagate_to_loaders = False - attr = "%s:%s" % (wildcard_key, attr) - - return path + (attr, ) - - def __getstate__(self): - d = self.__dict__.copy() - d['path'] = ret = [] - for token in util.to_list(self.path): - if isinstance(token, PropComparator): - ret.append((token._parentmapper.class_, token.key)) - else: - ret.append(token) - return d - - def __setstate__(self, state): - ret = [] - for key in state['path']: - if isinstance(key, tuple): - cls, propkey = key - ret.append(getattr(cls, propkey)) - else: - ret.append(key) - state['path'] = tuple(ret) - self.__dict__ = state - - def _process(self, query, raiseerr): - for val in self._to_bind: - val._bind_loader(query, query._attributes, raiseerr) - - @classmethod - def _from_keys(self, meth, keys, chained, kw): - opt = _UnboundLoad() - - def _split_key(key): - if isinstance(key, util.string_types): - # coerce fooload('*') into "default loader strategy" - if key == _WILDCARD_TOKEN: - return (_DEFAULT_TOKEN, ) - # coerce fooload(".*") into "wildcard on default entity" - elif key.startswith("." + _WILDCARD_TOKEN): - key = key[1:] - return key.split(".") - else: - return (key,) - all_tokens = [token for key in keys for token in _split_key(key)] - - for token in all_tokens[0:-1]: - if chained: - opt = meth(opt, token, **kw) - else: - opt = opt.defaultload(token) - opt._is_chain_link = True - - opt = meth(opt, all_tokens[-1], **kw) - opt._is_chain_link = False - - return opt - - - def _chop_path(self, to_chop, path): - i = -1 - for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())): - if isinstance(c_token, util.string_types): - if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN): - return to_chop - elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key: - return None - elif isinstance(c_token, PropComparator): - if c_token.property is not p_prop: - return None - else: - i += 1 - - return to_chop[i:] - - - def _bind_loader(self, query, context, raiseerr): - start_path = self.path - # _current_path implies we're in a - # secondary load with an existing path - - current_path = query._current_path - if current_path: - start_path = self._chop_path(start_path, current_path) - - if not start_path: - return None - - token = start_path[0] - if isinstance(token, util.string_types): - entity = self._find_entity_basestring(query, token, raiseerr) - elif isinstance(token, PropComparator): - prop = token.property - entity = self._find_entity_prop_comparator( - query, - prop.key, - token._parententity, - raiseerr) - - else: - raise sa_exc.ArgumentError( - "mapper option expects " - "string key or list of attributes") - - if not entity: - return - - path_element = entity.entity_zero - - # transfer our entity-less state into a Load() object - # with a real entity path. - loader = Load(path_element) - loader.context = context - loader.strategy = self.strategy - - path = loader.path - for token in start_path: - loader.path = path = loader._generate_path( - loader.path, token, None, raiseerr) - if path is None: - return - - loader.local_opts.update(self.local_opts) - - if loader.path.has_entity: - effective_path = loader.path.parent - else: - effective_path = loader.path - - # prioritize "first class" options over those - # that were "links in the chain", e.g. "x" and "y" in someload("x.y.z") - # versus someload("x") / someload("x.y") - if self._is_chain_link: - effective_path.setdefault(context, "loader", loader) - else: - effective_path.set(context, "loader", loader) - - def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): - if _is_aliased_class(mapper): - searchfor = mapper - else: - searchfor = _class_to_mapper(mapper) - for ent in query._mapper_entities: - if ent.corresponds_to(searchfor): - return ent - else: - if raiseerr: - if not list(query._mapper_entities): - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - raise sa_exc.ArgumentError( - "Can't find property '%s' on any entity " - "specified in this Query. Note the full path " - "from root (%s) to target entity must be specified." - % (token, ",".join(str(x) for - x in query._mapper_entities)) - ) - else: - return None - - def _find_entity_basestring(self, query, token, raiseerr): - if token.endswith(':' + _WILDCARD_TOKEN): - if len(list(query._mapper_entities)) != 1: - if raiseerr: - raise sa_exc.ArgumentError( - "Wildcard loader can only be used with exactly " - "one entity. Use Load(ent) to specify " - "specific entities.") - - for ent in query._mapper_entities: - # return only the first _MapperEntity when searching - # based on string prop name. Ideally object - # attributes are used to specify more exactly. - return ent - else: - if raiseerr: - raise sa_exc.ArgumentError( - "Query has only expression-based entities - " - "can't find property named '%s'." - % (token, ) - ) - else: - return None - - - -class loader_option(object): - def __init__(self): - pass - - def __call__(self, fn): - self.name = name = fn.__name__ - self.fn = fn - if hasattr(Load, name): - raise TypeError("Load class already has a %s method." % (name)) - setattr(Load, name, fn) - - return self - - def _add_unbound_fn(self, fn): - self._unbound_fn = fn - fn_doc = self.fn.__doc__ - self.fn.__doc__ = """Produce a new :class:`.Load` object with the -:func:`.orm.%(name)s` option applied. - -See :func:`.orm.%(name)s` for usage examples. - -""" % {"name": self.name} - - fn.__doc__ = fn_doc - return self - - def _add_unbound_all_fn(self, fn): - self._unbound_all_fn = fn - fn.__doc__ = """Produce a standalone "all" option for :func:`.orm.%(name)s`. - -.. deprecated:: 0.9.0 - - The "_all()" style is replaced by method chaining, e.g.:: - - session.query(MyClass).options( - %(name)s("someattribute").%(name)s("anotherattribute") - ) - -""" % {"name": self.name} - return self - -@loader_option() -def contains_eager(loadopt, attr, alias=None): - """Indicate that the given attribute should be eagerly loaded from - columns stated manually in the query. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - The option is used in conjunction with an explicit join that loads - the desired rows, i.e.:: - - sess.query(Order).\\ - join(Order.user).\\ - options(contains_eager(Order.user)) - - The above query would join from the ``Order`` entity to its related - ``User`` entity, and the returned ``Order`` objects would have the - ``Order.user`` attribute pre-populated. - - :func:`contains_eager` also accepts an `alias` argument, which is the - string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` - construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when - the eagerly-loaded rows are to come from an aliased table:: - - user_alias = aliased(User) - sess.query(Order).\\ - join((user_alias, Order.user)).\\ - options(contains_eager(Order.user, alias=user_alias)) - - .. seealso:: - - :ref:`contains_eager` - - """ - if alias is not None: - if not isinstance(alias, str): - info = inspect(alias) - alias = info.selectable - - cloned = loadopt.set_relationship_strategy( - attr, - {"lazy": "joined"}, - propagate_to_loaders=False - ) - cloned.local_opts['eager_from_alias'] = alias - return cloned - -@contains_eager._add_unbound_fn -def contains_eager(*keys, **kw): - return _UnboundLoad()._from_keys(_UnboundLoad.contains_eager, keys, True, kw) - -@loader_option() -def load_only(loadopt, *attrs): - """Indicate that for a particular entity, only the given list - of column-based attribute names should be loaded; all others will be - deferred. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - Example - given a class ``User``, load only the ``name`` and ``fullname`` - attributes:: - - session.query(User).options(load_only("name", "fullname")) - - Example - given a relationship ``User.addresses -> Address``, specify - subquery loading for the ``User.addresses`` collection, but on each ``Address`` - object load only the ``email_address`` attribute:: - - session.query(User).options( - subqueryload("addreses").load_only("email_address") - ) - - For a :class:`.Query` that has multiple entities, the lead entity can be - specifically referred to using the :class:`.Load` constructor:: - - session.query(User, Address).join(User.addresses).options( - Load(User).load_only("name", "fullname"), - Load(Address).load_only("email_addres") - ) - - - .. versionadded:: 0.9.0 - - """ - cloned = loadopt.set_column_strategy( - attrs, - {"deferred": False, "instrument": True} - ) - cloned.set_column_strategy("*", - {"deferred": True, "instrument": True}) - return cloned - -@load_only._add_unbound_fn -def load_only(*attrs): - return _UnboundLoad().load_only(*attrs) - -@loader_option() -def joinedload(loadopt, attr, innerjoin=None): - """Indicate that the given attribute should be loaded using joined - eager loading. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - examples:: - - # joined-load the "orders" collection on "User" - query(User).options(joinedload(User.orders)) - - # joined-load Order.items and then Item.keywords - query(Order).options(joinedload(Order.items).joinedload(Item.keywords)) - - # lazily load Order.items, but when Items are loaded, - # joined-load the keywords collection - query(Order).options(lazyload(Order.items).joinedload(Item.keywords)) - - :func:`.orm.joinedload` also accepts a keyword argument `innerjoin=True` which - indicates using an inner join instead of an outer:: - - query(Order).options(joinedload(Order.user, innerjoin=True)) - - .. note:: - - The joins produced by :func:`.orm.joinedload` are **anonymously aliased**. - The criteria by which the join proceeds cannot be modified, nor can the - :class:`.Query` refer to these joins in any way, including ordering. - - To produce a specific SQL JOIN which is explicitly available, use - :meth:`.Query.join`. To combine explicit JOINs with eager loading - of collections, use :func:`.orm.contains_eager`; see :ref:`contains_eager`. - - .. seealso:: - - :ref:`loading_toplevel` - - :ref:`contains_eager` - - :func:`.orm.subqueryload` - - :func:`.orm.lazyload` - - """ - loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"}) - if innerjoin is not None: - loader.local_opts['innerjoin'] = innerjoin - return loader - -@joinedload._add_unbound_fn -def joinedload(*keys, **kw): - return _UnboundLoad._from_keys( - _UnboundLoad.joinedload, keys, False, kw) - -@joinedload._add_unbound_all_fn -def joinedload_all(*keys, **kw): - return _UnboundLoad._from_keys( - _UnboundLoad.joinedload, keys, True, kw) - - -@loader_option() -def subqueryload(loadopt, attr): - """Indicate that the given attribute should be loaded using - subquery eager loading. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - examples:: - - # subquery-load the "orders" collection on "User" - query(User).options(subqueryload(User.orders)) - - # subquery-load Order.items and then Item.keywords - query(Order).options(subqueryload(Order.items).subqueryload(Item.keywords)) - - # lazily load Order.items, but when Items are loaded, - # subquery-load the keywords collection - query(Order).options(lazyload(Order.items).subqueryload(Item.keywords)) - - - .. seealso:: - - :ref:`loading_toplevel` - - :func:`.orm.joinedload` - - :func:`.orm.lazyload` - - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"}) - -@subqueryload._add_unbound_fn -def subqueryload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {}) - -@subqueryload._add_unbound_all_fn -def subqueryload_all(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, True, {}) - -@loader_option() -def lazyload(loadopt, attr): - """Indicate that the given attribute should be loaded using "lazy" - loading. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - """ - return loadopt.set_relationship_strategy(attr, {"lazy": "select"}) - -@lazyload._add_unbound_fn -def lazyload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {}) - -@lazyload._add_unbound_all_fn -def lazyload_all(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, True, {}) - -@loader_option() -def immediateload(loadopt, attr): - """Indicate that the given attribute should be loaded using - an immediate load with a per-attribute SELECT statement. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - .. seealso:: - - :ref:`loading_toplevel` - - :func:`.orm.joinedload` - - :func:`.orm.lazyload` - - """ - loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"}) - return loader - -@immediateload._add_unbound_fn -def immediateload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {}) - - -@loader_option() -def noload(loadopt, attr): - """Indicate that the given relationship attribute should remain unloaded. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - :func:`.orm.noload` applies to :func:`.relationship` attributes; for - column-based attributes, see :func:`.orm.defer`. - - """ - - return loadopt.set_relationship_strategy(attr, {"lazy": "noload"}) - -@noload._add_unbound_fn -def noload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {}) - -@loader_option() -def defaultload(loadopt, attr): - """Indicate an attribute should load using its default loader style. - - This method is used to link to other loader options, such as - to set the :func:`.orm.defer` option on a class that is linked to - a relationship of the parent class being loaded, :func:`.orm.defaultload` - can be used to navigate this path without changing the loading style - of the relationship:: - - session.query(MyClass).options(defaultload("someattr").defer("some_column")) - - .. seealso:: - - :func:`.orm.defer` - - :func:`.orm.undefer` - - """ - return loadopt.set_relationship_strategy( - attr, - None - ) - -@defaultload._add_unbound_fn -def defaultload(*keys): - return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {}) - -@loader_option() -def defer(loadopt, key): - """Indicate that the given column-oriented attribute should be deferred, e.g. - not loaded until accessed. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - e.g.:: - - from sqlalchemy.orm import defer - - session.query(MyClass).options( - defer("attribute_one"), - defer("attribute_two")) - - session.query(MyClass).options( - defer(MyClass.attribute_one), - defer(MyClass.attribute_two)) - - To specify a deferred load of an attribute on a related class, - the path can be specified one token at a time, specifying the loading - style for each link along the chain. To leave the loading style - for a link unchanged, use :func:`.orm.defaultload`:: - - session.query(MyClass).options(defaultload("someattr").defer("some_column")) - - A :class:`.Load` object that is present on a certain path can have - :meth:`.Load.defer` called multiple times, each will operate on the same - parent entity:: - - - session.query(MyClass).options( - defaultload("someattr"). - defer("some_column"). - defer("some_other_column"). - defer("another_column") - ) - - :param key: Attribute to be deferred. - - :param \*addl_attrs: Deprecated; this option supports the old 0.8 style - of specifying a path as a series of attributes, which is now superseded - by the method-chained style. - - .. seealso:: - - :ref:`deferred` - - :func:`.orm.undefer` - - """ - return loadopt.set_column_strategy( - (key, ), - {"deferred": True, "instrument": True} - ) - - -@defer._add_unbound_fn -def defer(key, *addl_attrs): - return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {}) - -@loader_option() -def undefer(loadopt, key): - """Indicate that the given column-oriented attribute should be undeferred, e.g. - specified within the SELECT statement of the entity as a whole. - - The column being undeferred is typically set up on the mapping as a - :func:`.deferred` attribute. - - This function is part of the :class:`.Load` interface and supports - both method-chained and standalone operation. - - Examples:: - - # undefer two columns - session.query(MyClass).options(undefer("col1"), undefer("col2")) - - # undefer all columns specific to a single class using Load + * - session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*")) - - :param key: Attribute to be undeferred. - - :param \*addl_attrs: Deprecated; this option supports the old 0.8 style - of specifying a path as a series of attributes, which is now superseded - by the method-chained style. - - .. seealso:: - - :ref:`deferred` - - :func:`.orm.defer` - - :func:`.orm.undefer_group` - - """ - return loadopt.set_column_strategy( - (key, ), - {"deferred": False, "instrument": True} - ) - -@undefer._add_unbound_fn -def undefer(key, *addl_attrs): - return _UnboundLoad._from_keys(_UnboundLoad.undefer, (key, ) + addl_attrs, False, {}) - -@loader_option() -def undefer_group(loadopt, name): - """Indicate that columns within the given deferred group name should be undeferred. - - The columns being undeferred are set up on the mapping as - :func:`.deferred` attributes and include a "group" name. - - E.g:: - - session.query(MyClass).options(undefer_group("large_attrs")) - - To undefer a group of attributes on a related entity, the path can be - spelled out using relationship loader options, such as :func:`.orm.defaultload`:: - - session.query(MyClass).options(defaultload("someattr").undefer_group("large_attrs")) - - .. versionchanged:: 0.9.0 :func:`.orm.undefer_group` is now specific to a - particiular entity load path. - - .. seealso:: - - :ref:`deferred` - - :func:`.orm.defer` - - :func:`.orm.undefer` - - """ - return loadopt.set_column_strategy( - "*", - None, - {"undefer_group": name} - ) - -@undefer_group._add_unbound_fn -def undefer_group(name): - return _UnboundLoad().undefer_group(name) - diff --git a/libs/sqlalchemy/orm/sync.py b/libs/sqlalchemy/orm/sync.py deleted file mode 100644 index cf735fc5..00000000 --- a/libs/sqlalchemy/orm/sync.py +++ /dev/null @@ -1,118 +0,0 @@ -# orm/sync.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""private module containing functions used for copying data -between instances based on join conditions. - -""" - -from . import exc, util as orm_util, attributes - - -def populate(source, source_mapper, dest, dest_mapper, - synchronize_pairs, uowcommit, flag_cascaded_pks): - source_dict = source.dict - dest_dict = dest.dict - - for l, r in synchronize_pairs: - try: - # inline of source_mapper._get_state_attr_by_column - prop = source_mapper._columntoproperty[l] - value = source.manager[prop.key].impl.get(source, source_dict, - attributes.PASSIVE_OFF) - except exc.UnmappedColumnError: - _raise_col_to_prop(False, source_mapper, l, dest_mapper, r) - - try: - # inline of dest_mapper._set_state_attr_by_column - prop = dest_mapper._columntoproperty[r] - dest.manager[prop.key].impl.set(dest, dest_dict, value, None) - except exc.UnmappedColumnError: - _raise_col_to_prop(True, source_mapper, l, dest_mapper, r) - - # technically the "r.primary_key" check isn't - # needed here, but we check for this condition to limit - # how often this logic is invoked for memory/performance - # reasons, since we only need this info for a primary key - # destination. - if flag_cascaded_pks and l.primary_key and \ - r.primary_key and \ - r.references(l): - uowcommit.attributes[("pk_cascaded", dest, r)] = True - - -def clear(dest, dest_mapper, synchronize_pairs): - for l, r in synchronize_pairs: - if r.primary_key: - raise AssertionError( - "Dependency rule tried to blank-out primary key " - "column '%s' on instance '%s'" % - (r, orm_util.state_str(dest)) - ) - try: - dest_mapper._set_state_attr_by_column(dest, dest.dict, r, None) - except exc.UnmappedColumnError: - _raise_col_to_prop(True, None, l, dest_mapper, r) - - -def update(source, source_mapper, dest, old_prefix, synchronize_pairs): - for l, r in synchronize_pairs: - try: - oldvalue = source_mapper._get_committed_attr_by_column( - source.obj(), l) - value = source_mapper._get_state_attr_by_column( - source, source.dict, l) - except exc.UnmappedColumnError: - _raise_col_to_prop(False, source_mapper, l, None, r) - dest[r.key] = value - dest[old_prefix + r.key] = oldvalue - - -def populate_dict(source, source_mapper, dict_, synchronize_pairs): - for l, r in synchronize_pairs: - try: - value = source_mapper._get_state_attr_by_column( - source, source.dict, l) - except exc.UnmappedColumnError: - _raise_col_to_prop(False, source_mapper, l, None, r) - - dict_[r.key] = value - - -def source_modified(uowcommit, source, source_mapper, synchronize_pairs): - """return true if the source object has changes from an old to a - new value on the given synchronize pairs - - """ - for l, r in synchronize_pairs: - try: - prop = source_mapper._columntoproperty[l] - except exc.UnmappedColumnError: - _raise_col_to_prop(False, source_mapper, l, None, r) - history = uowcommit.get_attribute_history(source, prop.key, - attributes.PASSIVE_NO_INITIALIZE) - if bool(history.deleted): - return True - else: - return False - - -def _raise_col_to_prop(isdest, source_mapper, source_column, - dest_mapper, dest_column): - if isdest: - raise exc.UnmappedColumnError("Can't execute sync rule for " - "destination column '%s'; mapper '%s' does not map " - "this column. Try using an explicit `foreign_keys` " - "collection which does not include this column (or use " - "a viewonly=True relation)." % (dest_column, - dest_mapper)) - else: - raise exc.UnmappedColumnError("Can't execute sync rule for " - "source column '%s'; mapper '%s' does not map this " - "column. Try using an explicit `foreign_keys` " - "collection which does not include destination column " - "'%s' (or use a viewonly=True relation)." - % (source_column, source_mapper, dest_column)) diff --git a/libs/sqlalchemy/orm/unitofwork.py b/libs/sqlalchemy/orm/unitofwork.py deleted file mode 100644 index 2964705a..00000000 --- a/libs/sqlalchemy/orm/unitofwork.py +++ /dev/null @@ -1,646 +0,0 @@ -# orm/unitofwork.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The internals for the unit of work system. - -The session's flush() process passes objects to a contextual object -here, which assembles flush tasks based on mappers and their properties, -organizes them in order of dependency, and executes. - -""" - -from .. import util, event -from ..util import topological -from . import attributes, persistence, util as orm_util - - -def track_cascade_events(descriptor, prop): - """Establish event listeners on object attributes which handle - cascade-on-set/append. - - """ - key = prop.key - - def append(state, item, initiator): - # process "save_update" cascade rules for when - # an instance is appended to the list of another instance - - if item is None: - return - - sess = state.session - if sess: - if sess._warn_on_events: - sess._flush_warning("collection append") - - prop = state.manager.mapper._props[key] - item_state = attributes.instance_state(item) - if prop._cascade.save_update and \ - (prop.cascade_backrefs or key == initiator.key) and \ - not sess._contains_state(item_state): - sess._save_or_update_state(item_state) - return item - - def remove(state, item, initiator): - if item is None: - return - - sess = state.session - if sess: - - prop = state.manager.mapper._props[key] - - if sess._warn_on_events: - sess._flush_warning( - "collection remove" - if prop.uselist - else "related attribute delete") - - # expunge pending orphans - item_state = attributes.instance_state(item) - if prop._cascade.delete_orphan and \ - item_state in sess._new and \ - prop.mapper._is_orphan(item_state): - sess.expunge(item) - - def set_(state, newvalue, oldvalue, initiator): - # process "save_update" cascade rules for when an instance - # is attached to another instance - if oldvalue is newvalue: - return newvalue - - sess = state.session - if sess: - - if sess._warn_on_events: - sess._flush_warning("related attribute set") - - prop = state.manager.mapper._props[key] - if newvalue is not None: - newvalue_state = attributes.instance_state(newvalue) - if prop._cascade.save_update and \ - (prop.cascade_backrefs or key == initiator.key) and \ - not sess._contains_state(newvalue_state): - sess._save_or_update_state(newvalue_state) - - if oldvalue is not None and \ - oldvalue is not attributes.PASSIVE_NO_RESULT and \ - prop._cascade.delete_orphan: - # possible to reach here with attributes.NEVER_SET ? - oldvalue_state = attributes.instance_state(oldvalue) - - if oldvalue_state in sess._new and \ - prop.mapper._is_orphan(oldvalue_state): - sess.expunge(oldvalue) - return newvalue - - event.listen(descriptor, 'append', append, raw=True, retval=True) - event.listen(descriptor, 'remove', remove, raw=True, retval=True) - event.listen(descriptor, 'set', set_, raw=True, retval=True) - - -class UOWTransaction(object): - def __init__(self, session): - self.session = session - - # dictionary used by external actors to - # store arbitrary state information. - self.attributes = {} - - # dictionary of mappers to sets of - # DependencyProcessors, which are also - # set to be part of the sorted flush actions, - # which have that mapper as a parent. - self.deps = util.defaultdict(set) - - # dictionary of mappers to sets of InstanceState - # items pending for flush which have that mapper - # as a parent. - self.mappers = util.defaultdict(set) - - # a dictionary of Preprocess objects, which gather - # additional states impacted by the flush - # and determine if a flush action is needed - self.presort_actions = {} - - # dictionary of PostSortRec objects, each - # one issues work during the flush within - # a certain ordering. - self.postsort_actions = {} - - # a set of 2-tuples, each containing two - # PostSortRec objects where the second - # is dependent on the first being executed - # first - self.dependencies = set() - - # dictionary of InstanceState-> (isdelete, listonly) - # tuples, indicating if this state is to be deleted - # or insert/updated, or just refreshed - self.states = {} - - # tracks InstanceStates which will be receiving - # a "post update" call. Keys are mappers, - # values are a set of states and a set of the - # columns which should be included in the update. - self.post_update_states = util.defaultdict(lambda: (set(), set())) - - @property - def has_work(self): - return bool(self.states) - - def is_deleted(self, state): - """return true if the given state is marked as deleted - within this uowtransaction.""" - - return state in self.states and self.states[state][0] - - def memo(self, key, callable_): - if key in self.attributes: - return self.attributes[key] - else: - self.attributes[key] = ret = callable_() - return ret - - def remove_state_actions(self, state): - """remove pending actions for a state from the uowtransaction.""" - - isdelete = self.states[state][0] - - self.states[state] = (isdelete, True) - - def get_attribute_history(self, state, key, - passive=attributes.PASSIVE_NO_INITIALIZE): - """facade to attributes.get_state_history(), including - caching of results.""" - - hashkey = ("history", state, key) - - # cache the objects, not the states; the strong reference here - # prevents newly loaded objects from being dereferenced during the - # flush process - - if hashkey in self.attributes: - history, state_history, cached_passive = self.attributes[hashkey] - # if the cached lookup was "passive" and now - # we want non-passive, do a non-passive lookup and re-cache - - if not cached_passive & attributes.SQL_OK \ - and passive & attributes.SQL_OK: - impl = state.manager[key].impl - history = impl.get_history(state, state.dict, - attributes.PASSIVE_OFF | - attributes.LOAD_AGAINST_COMMITTED) - if history and impl.uses_objects: - state_history = history.as_state() - else: - state_history = history - self.attributes[hashkey] = (history, state_history, passive) - else: - impl = state.manager[key].impl - # TODO: store the history as (state, object) tuples - # so we don't have to keep converting here - history = impl.get_history(state, state.dict, passive | - attributes.LOAD_AGAINST_COMMITTED) - if history and impl.uses_objects: - state_history = history.as_state() - else: - state_history = history - self.attributes[hashkey] = (history, state_history, - passive) - - return state_history - - def has_dep(self, processor): - return (processor, True) in self.presort_actions - - def register_preprocessor(self, processor, fromparent): - key = (processor, fromparent) - if key not in self.presort_actions: - self.presort_actions[key] = Preprocess(processor, fromparent) - - def register_object(self, state, isdelete=False, - listonly=False, cancel_delete=False, - operation=None, prop=None): - if not self.session._contains_state(state): - if not state.deleted and operation is not None: - util.warn("Object of type %s not in session, %s operation " - "along '%s' will not proceed" % - (orm_util.state_class_str(state), operation, prop)) - return False - - if state not in self.states: - mapper = state.manager.mapper - - if mapper not in self.mappers: - self._per_mapper_flush_actions(mapper) - - self.mappers[mapper].add(state) - self.states[state] = (isdelete, listonly) - else: - if not listonly and (isdelete or cancel_delete): - self.states[state] = (isdelete, False) - return True - - def issue_post_update(self, state, post_update_cols): - mapper = state.manager.mapper.base_mapper - states, cols = self.post_update_states[mapper] - states.add(state) - cols.update(post_update_cols) - - def _per_mapper_flush_actions(self, mapper): - saves = SaveUpdateAll(self, mapper.base_mapper) - deletes = DeleteAll(self, mapper.base_mapper) - self.dependencies.add((saves, deletes)) - - for dep in mapper._dependency_processors: - dep.per_property_preprocessors(self) - - for prop in mapper.relationships: - if prop.viewonly: - continue - dep = prop._dependency_processor - dep.per_property_preprocessors(self) - - @util.memoized_property - def _mapper_for_dep(self): - """return a dynamic mapping of (Mapper, DependencyProcessor) to - True or False, indicating if the DependencyProcessor operates - on objects of that Mapper. - - The result is stored in the dictionary persistently once - calculated. - - """ - return util.PopulateDict( - lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop - ) - - def filter_states_for_dep(self, dep, states): - """Filter the given list of InstanceStates to those relevant to the - given DependencyProcessor. - - """ - mapper_for_dep = self._mapper_for_dep - return [s for s in states if mapper_for_dep[(s.manager.mapper, dep)]] - - def states_for_mapper_hierarchy(self, mapper, isdelete, listonly): - checktup = (isdelete, listonly) - for mapper in mapper.base_mapper.self_and_descendants: - for state in self.mappers[mapper]: - if self.states[state] == checktup: - yield state - - def _generate_actions(self): - """Generate the full, unsorted collection of PostSortRecs as - well as dependency pairs for this UOWTransaction. - - """ - # execute presort_actions, until all states - # have been processed. a presort_action might - # add new states to the uow. - while True: - ret = False - for action in list(self.presort_actions.values()): - if action.execute(self): - ret = True - if not ret: - break - - # see if the graph of mapper dependencies has cycles. - self.cycles = cycles = topological.find_cycles( - self.dependencies, - list(self.postsort_actions.values())) - - if cycles: - # if yes, break the per-mapper actions into - # per-state actions - convert = dict( - (rec, set(rec.per_state_flush_actions(self))) - for rec in cycles - ) - - # rewrite the existing dependencies to point to - # the per-state actions for those per-mapper actions - # that were broken up. - for edge in list(self.dependencies): - if None in edge or \ - edge[0].disabled or edge[1].disabled or \ - cycles.issuperset(edge): - self.dependencies.remove(edge) - elif edge[0] in cycles: - self.dependencies.remove(edge) - for dep in convert[edge[0]]: - self.dependencies.add((dep, edge[1])) - elif edge[1] in cycles: - self.dependencies.remove(edge) - for dep in convert[edge[1]]: - self.dependencies.add((edge[0], dep)) - - return set([a for a in self.postsort_actions.values() - if not a.disabled - ] - ).difference(cycles) - - def execute(self): - postsort_actions = self._generate_actions() - - #sort = topological.sort(self.dependencies, postsort_actions) - #print "--------------" - #print "\ndependencies:", self.dependencies - #print "\ncycles:", self.cycles - #print "\nsort:", list(sort) - #print "\nCOUNT OF POSTSORT ACTIONS", len(postsort_actions) - - # execute - if self.cycles: - for set_ in topological.sort_as_subsets( - self.dependencies, - postsort_actions): - while set_: - n = set_.pop() - n.execute_aggregate(self, set_) - else: - for rec in topological.sort( - self.dependencies, - postsort_actions): - rec.execute(self) - - def finalize_flush_changes(self): - """mark processed objects as clean / deleted after a successful - flush(). - - this method is called within the flush() method after the - execute() method has succeeded and the transaction has been committed. - - """ - states = set(self.states) - isdel = set( - s for (s, (isdelete, listonly)) in self.states.items() - if isdelete - ) - other = states.difference(isdel) - self.session._remove_newly_deleted(isdel) - self.session._register_newly_persistent(other) - - -class IterateMappersMixin(object): - def _mappers(self, uow): - if self.fromparent: - return iter( - m for m in - self.dependency_processor.parent.self_and_descendants - if uow._mapper_for_dep[(m, self.dependency_processor)] - ) - else: - return self.dependency_processor.mapper.self_and_descendants - - -class Preprocess(IterateMappersMixin): - def __init__(self, dependency_processor, fromparent): - self.dependency_processor = dependency_processor - self.fromparent = fromparent - self.processed = set() - self.setup_flush_actions = False - - def execute(self, uow): - delete_states = set() - save_states = set() - - for mapper in self._mappers(uow): - for state in uow.mappers[mapper].difference(self.processed): - (isdelete, listonly) = uow.states[state] - if not listonly: - if isdelete: - delete_states.add(state) - else: - save_states.add(state) - - if delete_states: - self.dependency_processor.presort_deletes(uow, delete_states) - self.processed.update(delete_states) - if save_states: - self.dependency_processor.presort_saves(uow, save_states) - self.processed.update(save_states) - - if (delete_states or save_states): - if not self.setup_flush_actions and ( - self.dependency_processor.\ - prop_has_changes(uow, delete_states, True) or - self.dependency_processor.\ - prop_has_changes(uow, save_states, False) - ): - self.dependency_processor.per_property_flush_actions(uow) - self.setup_flush_actions = True - return True - else: - return False - - -class PostSortRec(object): - disabled = False - - def __new__(cls, uow, *args): - key = (cls, ) + args - if key in uow.postsort_actions: - return uow.postsort_actions[key] - else: - uow.postsort_actions[key] = \ - ret = \ - object.__new__(cls) - return ret - - def execute_aggregate(self, uow, recs): - self.execute(uow) - - def __repr__(self): - return "%s(%s)" % ( - self.__class__.__name__, - ",".join(str(x) for x in self.__dict__.values()) - ) - - -class ProcessAll(IterateMappersMixin, PostSortRec): - def __init__(self, uow, dependency_processor, delete, fromparent): - self.dependency_processor = dependency_processor - self.delete = delete - self.fromparent = fromparent - uow.deps[dependency_processor.parent.base_mapper].\ - add(dependency_processor) - - def execute(self, uow): - states = self._elements(uow) - if self.delete: - self.dependency_processor.process_deletes(uow, states) - else: - self.dependency_processor.process_saves(uow, states) - - def per_state_flush_actions(self, uow): - # this is handled by SaveUpdateAll and DeleteAll, - # since a ProcessAll should unconditionally be pulled - # into per-state if either the parent/child mappers - # are part of a cycle - return iter([]) - - def __repr__(self): - return "%s(%s, delete=%s)" % ( - self.__class__.__name__, - self.dependency_processor, - self.delete - ) - - def _elements(self, uow): - for mapper in self._mappers(uow): - for state in uow.mappers[mapper]: - (isdelete, listonly) = uow.states[state] - if isdelete == self.delete and not listonly: - yield state - - -class IssuePostUpdate(PostSortRec): - def __init__(self, uow, mapper, isdelete): - self.mapper = mapper - self.isdelete = isdelete - - def execute(self, uow): - states, cols = uow.post_update_states[self.mapper] - states = [s for s in states if uow.states[s][0] == self.isdelete] - - persistence.post_update(self.mapper, states, uow, cols) - - -class SaveUpdateAll(PostSortRec): - def __init__(self, uow, mapper): - self.mapper = mapper - assert mapper is mapper.base_mapper - - def execute(self, uow): - persistence.save_obj(self.mapper, - uow.states_for_mapper_hierarchy(self.mapper, False, False), - uow - ) - - def per_state_flush_actions(self, uow): - states = list(uow.states_for_mapper_hierarchy( - self.mapper, False, False)) - base_mapper = self.mapper.base_mapper - delete_all = DeleteAll(uow, base_mapper) - for state in states: - # keep saves before deletes - - # this ensures 'row switch' operations work - action = SaveUpdateState(uow, state, base_mapper) - uow.dependencies.add((action, delete_all)) - yield action - - for dep in uow.deps[self.mapper]: - states_for_prop = uow.filter_states_for_dep(dep, states) - dep.per_state_flush_actions(uow, states_for_prop, False) - - -class DeleteAll(PostSortRec): - def __init__(self, uow, mapper): - self.mapper = mapper - assert mapper is mapper.base_mapper - - def execute(self, uow): - persistence.delete_obj(self.mapper, - uow.states_for_mapper_hierarchy(self.mapper, True, False), - uow - ) - - def per_state_flush_actions(self, uow): - states = list(uow.states_for_mapper_hierarchy( - self.mapper, True, False)) - base_mapper = self.mapper.base_mapper - save_all = SaveUpdateAll(uow, base_mapper) - for state in states: - # keep saves before deletes - - # this ensures 'row switch' operations work - action = DeleteState(uow, state, base_mapper) - uow.dependencies.add((save_all, action)) - yield action - - for dep in uow.deps[self.mapper]: - states_for_prop = uow.filter_states_for_dep(dep, states) - dep.per_state_flush_actions(uow, states_for_prop, True) - - -class ProcessState(PostSortRec): - def __init__(self, uow, dependency_processor, delete, state): - self.dependency_processor = dependency_processor - self.delete = delete - self.state = state - - def execute_aggregate(self, uow, recs): - cls_ = self.__class__ - dependency_processor = self.dependency_processor - delete = self.delete - our_recs = [r for r in recs - if r.__class__ is cls_ and - r.dependency_processor is dependency_processor and - r.delete is delete] - recs.difference_update(our_recs) - states = [self.state] + [r.state for r in our_recs] - if delete: - dependency_processor.process_deletes(uow, states) - else: - dependency_processor.process_saves(uow, states) - - def __repr__(self): - return "%s(%s, %s, delete=%s)" % ( - self.__class__.__name__, - self.dependency_processor, - orm_util.state_str(self.state), - self.delete - ) - - -class SaveUpdateState(PostSortRec): - def __init__(self, uow, state, mapper): - self.state = state - self.mapper = mapper - - def execute_aggregate(self, uow, recs): - cls_ = self.__class__ - mapper = self.mapper - our_recs = [r for r in recs - if r.__class__ is cls_ and - r.mapper is mapper] - recs.difference_update(our_recs) - persistence.save_obj(mapper, - [self.state] + - [r.state for r in our_recs], - uow) - - def __repr__(self): - return "%s(%s)" % ( - self.__class__.__name__, - orm_util.state_str(self.state) - ) - - -class DeleteState(PostSortRec): - def __init__(self, uow, state, mapper): - self.state = state - self.mapper = mapper - - def execute_aggregate(self, uow, recs): - cls_ = self.__class__ - mapper = self.mapper - our_recs = [r for r in recs - if r.__class__ is cls_ and - r.mapper is mapper] - recs.difference_update(our_recs) - states = [self.state] + [r.state for r in our_recs] - persistence.delete_obj(mapper, - [s for s in states if uow.states[s][0]], - uow) - - def __repr__(self): - return "%s(%s)" % ( - self.__class__.__name__, - orm_util.state_str(self.state) - ) diff --git a/libs/sqlalchemy/orm/util.py b/libs/sqlalchemy/orm/util.py deleted file mode 100644 index dd85f2ef..00000000 --- a/libs/sqlalchemy/orm/util.py +++ /dev/null @@ -1,953 +0,0 @@ -# orm/util.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -from .. import sql, util, event, exc as sa_exc, inspection -from ..sql import expression, util as sql_util, operators -from .interfaces import PropComparator, MapperProperty -from . import attributes -import re - -from .base import instance_str, state_str, state_class_str, attribute_str, \ - state_attribute_str, object_mapper, object_state, _none_set -from .base import class_mapper, _class_to_mapper -from .base import _InspectionAttr -from .path_registry import PathRegistry - -all_cascades = frozenset(("delete", "delete-orphan", "all", "merge", - "expunge", "save-update", "refresh-expire", - "none")) - - -class CascadeOptions(frozenset): - """Keeps track of the options sent to relationship().cascade""" - - _add_w_all_cascades = all_cascades.difference([ - 'all', 'none', 'delete-orphan']) - _allowed_cascades = all_cascades - - def __new__(cls, arg): - values = set([ - c for c - in re.split('\s*,\s*', arg or "") - if c - ]) - - if values.difference(cls._allowed_cascades): - raise sa_exc.ArgumentError( - "Invalid cascade option(s): %s" % - ", ".join([repr(x) for x in - sorted( - values.difference(cls._allowed_cascades) - )]) - ) - - if "all" in values: - values.update(cls._add_w_all_cascades) - if "none" in values: - values.clear() - values.discard('all') - - self = frozenset.__new__(CascadeOptions, values) - self.save_update = 'save-update' in values - self.delete = 'delete' in values - self.refresh_expire = 'refresh-expire' in values - self.merge = 'merge' in values - self.expunge = 'expunge' in values - self.delete_orphan = "delete-orphan" in values - - if self.delete_orphan and not self.delete: - util.warn("The 'delete-orphan' cascade " - "option requires 'delete'.") - return self - - def __repr__(self): - return "CascadeOptions(%r)" % ( - ",".join([x for x in sorted(self)]) - ) - - -def _validator_events(desc, key, validator, include_removes, include_backrefs): - """Runs a validation method on an attribute value to be set or appended.""" - - if not include_backrefs: - def detect_is_backref(state, initiator): - impl = state.manager[key].impl - return initiator.impl is not impl - - if include_removes: - def append(state, value, initiator): - if include_backrefs or not detect_is_backref(state, initiator): - return validator(state.obj(), key, value, False) - else: - return value - - def set_(state, value, oldvalue, initiator): - if include_backrefs or not detect_is_backref(state, initiator): - return validator(state.obj(), key, value, False) - else: - return value - - def remove(state, value, initiator): - if include_backrefs or not detect_is_backref(state, initiator): - validator(state.obj(), key, value, True) - - else: - def append(state, value, initiator): - if include_backrefs or not detect_is_backref(state, initiator): - return validator(state.obj(), key, value) - else: - return value - - def set_(state, value, oldvalue, initiator): - if include_backrefs or not detect_is_backref(state, initiator): - return validator(state.obj(), key, value) - else: - return value - - event.listen(desc, 'append', append, raw=True, retval=True) - event.listen(desc, 'set', set_, raw=True, retval=True) - if include_removes: - event.listen(desc, "remove", remove, raw=True, retval=True) - - -def polymorphic_union(table_map, typecolname, - aliasname='p_union', cast_nulls=True): - """Create a ``UNION`` statement used by a polymorphic mapper. - - See :ref:`concrete_inheritance` for an example of how - this is used. - - :param table_map: mapping of polymorphic identities to - :class:`.Table` objects. - :param typecolname: string name of a "discriminator" column, which will be - derived from the query, producing the polymorphic identity for - each row. If ``None``, no polymorphic discriminator is generated. - :param aliasname: name of the :func:`~sqlalchemy.sql.expression.alias()` - construct generated. - :param cast_nulls: if True, non-existent columns, which are represented - as labeled NULLs, will be passed into CAST. This is a legacy behavior - that is problematic on some backends such as Oracle - in which case it - can be set to False. - - """ - - colnames = util.OrderedSet() - colnamemaps = {} - types = {} - for key in table_map: - table = table_map[key] - - # mysql doesnt like selecting from a select; - # make it an alias of the select - if isinstance(table, sql.Select): - table = table.alias() - table_map[key] = table - - m = {} - for c in table.c: - colnames.add(c.key) - m[c.key] = c - types[c.key] = c.type - colnamemaps[table] = m - - def col(name, table): - try: - return colnamemaps[table][name] - except KeyError: - if cast_nulls: - return sql.cast(sql.null(), types[name]).label(name) - else: - return sql.type_coerce(sql.null(), types[name]).label(name) - - result = [] - for type, table in table_map.items(): - if typecolname is not None: - result.append( - sql.select([col(name, table) for name in colnames] + - [sql.literal_column(sql_util._quote_ddl_expr(type)). - label(typecolname)], - from_obj=[table])) - else: - result.append(sql.select([col(name, table) for name in colnames], - from_obj=[table])) - return sql.union_all(*result).alias(aliasname) - - -def identity_key(*args, **kwargs): - """Generate "identity key" tuples, as are used as keys in the - :attr:`.Session.identity_map` dictionary. - - This function has several call styles: - - * ``identity_key(class, ident)`` - - This form receives a mapped class and a primary key scalar or - tuple as an argument. - - E.g.:: - - >>> identity_key(MyClass, (1, 2)) - (, (1, 2)) - - :param class: mapped class (must be a positional argument) - :param ident: primary key, may be a scalar or tuple argument. - - - * ``identity_key(instance=instance)`` - - This form will produce the identity key for a given instance. The - instance need not be persistent, only that its primary key attributes - are populated (else the key will contain ``None`` for those missing - values). - - E.g.:: - - >>> instance = MyClass(1, 2) - >>> identity_key(instance=instance) - (, (1, 2)) - - In this form, the given instance is ultimately run though - :meth:`.Mapper.identity_key_from_instance`, which will have the - effect of performing a database check for the corresponding row - if the object is expired. - - :param instance: object instance (must be given as a keyword arg) - - * ``identity_key(class, row=row)`` - - This form is similar to the class/tuple form, except is passed a - database result row as a :class:`.RowProxy` object. - - E.g.:: - - >>> row = engine.execute("select * from table where a=1 and b=2").first() - >>> identity_key(MyClass, row=row) - (, (1, 2)) - - :param class: mapped class (must be a positional argument) - :param row: :class:`.RowProxy` row returned by a :class:`.ResultProxy` - (must be given as a keyword arg) - - """ - if args: - if len(args) == 1: - class_ = args[0] - try: - row = kwargs.pop("row") - except KeyError: - ident = kwargs.pop("ident") - elif len(args) == 2: - class_, ident = args - elif len(args) == 3: - class_, ident = args - else: - raise sa_exc.ArgumentError("expected up to three " - "positional arguments, got %s" % len(args)) - if kwargs: - raise sa_exc.ArgumentError("unknown keyword arguments: %s" - % ", ".join(kwargs)) - mapper = class_mapper(class_) - if "ident" in locals(): - return mapper.identity_key_from_primary_key(util.to_list(ident)) - return mapper.identity_key_from_row(row) - instance = kwargs.pop("instance") - if kwargs: - raise sa_exc.ArgumentError("unknown keyword arguments: %s" - % ", ".join(kwargs.keys)) - mapper = object_mapper(instance) - return mapper.identity_key_from_instance(instance) - - -class ORMAdapter(sql_util.ColumnAdapter): - """Extends ColumnAdapter to accept ORM entities. - - The selectable is extracted from the given entity, - and the AliasedClass if any is referenced. - - """ - def __init__(self, entity, equivalents=None, adapt_required=False, - chain_to=None): - info = inspection.inspect(entity) - - self.mapper = info.mapper - selectable = info.selectable - is_aliased_class = info.is_aliased_class - if is_aliased_class: - self.aliased_class = entity - else: - self.aliased_class = None - sql_util.ColumnAdapter.__init__(self, selectable, - equivalents, chain_to, - adapt_required=adapt_required) - - def replace(self, elem): - entity = elem._annotations.get('parentmapper', None) - if not entity or entity.isa(self.mapper): - return sql_util.ColumnAdapter.replace(self, elem) - else: - return None - -class AliasedClass(object): - """Represents an "aliased" form of a mapped class for usage with Query. - - The ORM equivalent of a :func:`sqlalchemy.sql.expression.alias` - construct, this object mimics the mapped class using a - __getattr__ scheme and maintains a reference to a - real :class:`~sqlalchemy.sql.expression.Alias` object. - - Usage is via the :func:`.orm.aliased` function, or alternatively - via the :func:`.orm.with_polymorphic` function. - - Usage example:: - - # find all pairs of users with the same name - user_alias = aliased(User) - session.query(User, user_alias).\\ - join((user_alias, User.id > user_alias.id)).\\ - filter(User.name==user_alias.name) - - The resulting object is an instance of :class:`.AliasedClass`. - This object implements an attribute scheme which produces the - same attribute and method interface as the original mapped - class, allowing :class:`.AliasedClass` to be compatible - with any attribute technique which works on the original class, - including hybrid attributes (see :ref:`hybrids_toplevel`). - - The :class:`.AliasedClass` can be inspected for its underlying - :class:`.Mapper`, aliased selectable, and other information - using :func:`.inspect`:: - - from sqlalchemy import inspect - my_alias = aliased(MyClass) - insp = inspect(my_alias) - - The resulting inspection object is an instance of :class:`.AliasedInsp`. - - See :func:`.aliased` and :func:`.with_polymorphic` for construction - argument descriptions. - - """ - def __init__(self, cls, alias=None, - name=None, - flat=False, - adapt_on_names=False, - # TODO: None for default here? - with_polymorphic_mappers=(), - with_polymorphic_discriminator=None, - base_alias=None, - use_mapper_path=False): - mapper = _class_to_mapper(cls) - if alias is None: - alias = mapper._with_polymorphic_selectable.alias( - name=name, flat=flat) - self._aliased_insp = AliasedInsp( - self, - mapper, - alias, - name, - with_polymorphic_mappers - if with_polymorphic_mappers - else mapper.with_polymorphic_mappers, - with_polymorphic_discriminator - if with_polymorphic_discriminator is not None - else mapper.polymorphic_on, - base_alias, - use_mapper_path, - adapt_on_names - ) - - self.__name__ = 'AliasedClass_%s' % mapper.class_.__name__ - - def __getattr__(self, key): - try: - _aliased_insp = self.__dict__['_aliased_insp'] - except KeyError: - raise AttributeError() - else: - for base in _aliased_insp._target.__mro__: - try: - attr = object.__getattribute__(base, key) - except AttributeError: - continue - else: - break - else: - raise AttributeError(key) - - if isinstance(attr, PropComparator): - ret = attr.adapt_to_entity(_aliased_insp) - setattr(self, key, ret) - return ret - elif hasattr(attr, 'func_code'): - is_method = getattr(_aliased_insp._target, key, None) - if is_method and is_method.__self__ is not None: - return util.types.MethodType(attr.__func__, self, self) - else: - return None - elif hasattr(attr, '__get__'): - ret = attr.__get__(None, self) - if isinstance(ret, PropComparator): - return ret.adapt_to_entity(_aliased_insp) - else: - return ret - else: - return attr - - def __repr__(self): - return '' % ( - id(self), self._aliased_insp._target.__name__) - - -class AliasedInsp(_InspectionAttr): - """Provide an inspection interface for an - :class:`.AliasedClass` object. - - The :class:`.AliasedInsp` object is returned - given an :class:`.AliasedClass` using the - :func:`.inspect` function:: - - from sqlalchemy import inspect - from sqlalchemy.orm import aliased - - my_alias = aliased(MyMappedClass) - insp = inspect(my_alias) - - Attributes on :class:`.AliasedInsp` - include: - - * ``entity`` - the :class:`.AliasedClass` represented. - * ``mapper`` - the :class:`.Mapper` mapping the underlying class. - * ``selectable`` - the :class:`.Alias` construct which ultimately - represents an aliased :class:`.Table` or :class:`.Select` - construct. - * ``name`` - the name of the alias. Also is used as the attribute - name when returned in a result tuple from :class:`.Query`. - * ``with_polymorphic_mappers`` - collection of :class:`.Mapper` objects - indicating all those mappers expressed in the select construct - for the :class:`.AliasedClass`. - * ``polymorphic_on`` - an alternate column or SQL expression which - will be used as the "discriminator" for a polymorphic load. - - .. seealso:: - - :ref:`inspection_toplevel` - - """ - - def __init__(self, entity, mapper, selectable, name, - with_polymorphic_mappers, polymorphic_on, - _base_alias, _use_mapper_path, adapt_on_names): - self.entity = entity - self.mapper = mapper - self.selectable = selectable - self.name = name - self.with_polymorphic_mappers = with_polymorphic_mappers - self.polymorphic_on = polymorphic_on - self._base_alias = _base_alias or self - self._use_mapper_path = _use_mapper_path - - self._adapter = sql_util.ClauseAdapter(selectable, - equivalents=mapper._equivalent_columns, - adapt_on_names=adapt_on_names) - - self._adapt_on_names = adapt_on_names - self._target = mapper.class_ - - for poly in self.with_polymorphic_mappers: - if poly is not mapper: - setattr(self.entity, poly.class_.__name__, - AliasedClass(poly.class_, selectable, base_alias=self, - adapt_on_names=adapt_on_names, - use_mapper_path=_use_mapper_path)) - - is_aliased_class = True - "always returns True" - - @property - def class_(self): - """Return the mapped class ultimately represented by this - :class:`.AliasedInsp`.""" - return self.mapper.class_ - - @util.memoized_property - def _path_registry(self): - if self._use_mapper_path: - return self.mapper._path_registry - else: - return PathRegistry.per_mapper(self) - - def __getstate__(self): - return { - 'entity': self.entity, - 'mapper': self.mapper, - 'alias': self.selectable, - 'name': self.name, - 'adapt_on_names': self._adapt_on_names, - 'with_polymorphic_mappers': - self.with_polymorphic_mappers, - 'with_polymorphic_discriminator': - self.polymorphic_on, - 'base_alias': self._base_alias, - 'use_mapper_path': self._use_mapper_path - } - - def __setstate__(self, state): - self.__init__( - state['entity'], - state['mapper'], - state['alias'], - state['name'], - state['with_polymorphic_mappers'], - state['with_polymorphic_discriminator'], - state['base_alias'], - state['use_mapper_path'], - state['adapt_on_names'] - ) - - def _adapt_element(self, elem): - return self._adapter.traverse(elem).\ - _annotate({ - 'parententity': self.entity, - 'parentmapper': self.mapper} - ) - - def _entity_for_mapper(self, mapper): - self_poly = self.with_polymorphic_mappers - if mapper in self_poly: - return getattr(self.entity, mapper.class_.__name__)._aliased_insp - elif mapper.isa(self.mapper): - return self - else: - assert False, "mapper %s doesn't correspond to %s" % (mapper, self) - - def __repr__(self): - return '' % ( - id(self), self.class_.__name__) - - -inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) -inspection._inspects(AliasedInsp)(lambda target: target) - - -def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False): - """Produce an alias of the given element, usually an :class:`.AliasedClass` - instance. - - E.g.:: - - my_alias = aliased(MyClass) - - session.query(MyClass, my_alias).filter(MyClass.id > my_alias.id) - - The :func:`.aliased` function is used to create an ad-hoc mapping - of a mapped class to a new selectable. By default, a selectable - is generated from the normally mapped selectable (typically a - :class:`.Table`) using the :meth:`.FromClause.alias` method. - However, :func:`.aliased` can also be used to link the class to - a new :func:`.select` statement. Also, the :func:`.with_polymorphic` - function is a variant of :func:`.aliased` that is intended to specify - a so-called "polymorphic selectable", that corresponds to the union - of several joined-inheritance subclasses at once. - - For convenience, the :func:`.aliased` function also accepts plain - :class:`.FromClause` constructs, such as a :class:`.Table` or - :func:`.select` construct. In those cases, the :meth:`.FromClause.alias` - method is called on the object and the new :class:`.Alias` object - returned. The returned :class:`.Alias` is not ORM-mapped in this case. - - :param element: element to be aliased. Is normally a mapped class, - but for convenience can also be a :class:`.FromClause` element. - - :param alias: Optional selectable unit to map the element to. This should - normally be a :class:`.Alias` object corresponding to the :class:`.Table` - to which the class is mapped, or to a :func:`.select` construct that - is compatible with the mapping. By default, a simple anonymous - alias of the mapped table is generated. - - :param name: optional string name to use for the alias, if not specified - by the ``alias`` parameter. The name, among other things, forms the - attribute name that will be accessible via tuples returned by a - :class:`.Query` object. - - :param flat: Boolean, will be passed through to the :meth:`.FromClause.alias` - call so that aliases of :class:`.Join` objects don't include an enclosing - SELECT. This can lead to more efficient queries in many circumstances. - A JOIN against a nested JOIN will be rewritten as a JOIN against an aliased - SELECT subquery on backends that don't support this syntax. - - .. versionadded:: 0.9.0 - - .. seealso:: :meth:`.Join.alias` - - :param adapt_on_names: if True, more liberal "matching" will be used when - mapping the mapped columns of the ORM entity to those of the - given selectable - a name-based match will be performed if the - given selectable doesn't otherwise have a column that corresponds - to one on the entity. The use case for this is when associating - an entity with some derived selectable such as one that uses - aggregate functions:: - - class UnitPrice(Base): - __tablename__ = 'unit_price' - ... - unit_id = Column(Integer) - price = Column(Numeric) - - aggregated_unit_price = Session.query( - func.sum(UnitPrice.price).label('price') - ).group_by(UnitPrice.unit_id).subquery() - - aggregated_unit_price = aliased(UnitPrice, - alias=aggregated_unit_price, adapt_on_names=True) - - Above, functions on ``aggregated_unit_price`` which refer to - ``.price`` will return the - ``fund.sum(UnitPrice.price).label('price')`` column, as it is - matched on the name "price". Ordinarily, the "price" function - wouldn't have any "column correspondence" to the actual - ``UnitPrice.price`` column as it is not a proxy of the original. - - .. versionadded:: 0.7.3 - - - """ - if isinstance(element, expression.FromClause): - if adapt_on_names: - raise sa_exc.ArgumentError( - "adapt_on_names only applies to ORM elements" - ) - return element.alias(name, flat=flat) - else: - return AliasedClass(element, alias=alias, flat=flat, - name=name, adapt_on_names=adapt_on_names) - - -def with_polymorphic(base, classes, selectable=False, - flat=False, - polymorphic_on=None, aliased=False, - innerjoin=False, _use_mapper_path=False): - """Produce an :class:`.AliasedClass` construct which specifies - columns for descendant mappers of the given base. - - .. versionadded:: 0.8 - :func:`.orm.with_polymorphic` is in addition to the existing - :class:`.Query` method :meth:`.Query.with_polymorphic`, - which has the same purpose but is not as flexible in its usage. - - Using this method will ensure that each descendant mapper's - tables are included in the FROM clause, and will allow filter() - criterion to be used against those tables. The resulting - instances will also have those columns already loaded so that - no "post fetch" of those columns will be required. - - See the examples at :ref:`with_polymorphic`. - - :param base: Base class to be aliased. - - :param classes: a single class or mapper, or list of - class/mappers, which inherit from the base class. - Alternatively, it may also be the string ``'*'``, in which case - all descending mapped classes will be added to the FROM clause. - - :param aliased: when True, the selectable will be wrapped in an - alias, that is ``(SELECT * FROM ) AS anon_1``. - This can be important when using the with_polymorphic() - to create the target of a JOIN on a backend that does not - support parenthesized joins, such as SQLite and older - versions of MySQL. - - :param flat: Boolean, will be passed through to the :meth:`.FromClause.alias` - call so that aliases of :class:`.Join` objects don't include an enclosing - SELECT. This can lead to more efficient queries in many circumstances. - A JOIN against a nested JOIN will be rewritten as a JOIN against an aliased - SELECT subquery on backends that don't support this syntax. - - Setting ``flat`` to ``True`` implies the ``aliased`` flag is - also ``True``. - - .. versionadded:: 0.9.0 - - .. seealso:: :meth:`.Join.alias` - - :param selectable: a table or select() statement that will - be used in place of the generated FROM clause. This argument is - required if any of the desired classes use concrete table - inheritance, since SQLAlchemy currently cannot generate UNIONs - among tables automatically. If used, the ``selectable`` argument - must represent the full set of tables and columns mapped by every - mapped class. Otherwise, the unaccounted mapped columns will - result in their table being appended directly to the FROM clause - which will usually lead to incorrect results. - - :param polymorphic_on: a column to be used as the "discriminator" - column for the given selectable. If not given, the polymorphic_on - attribute of the base classes' mapper will be used, if any. This - is useful for mappings that don't have polymorphic loading - behavior by default. - - :param innerjoin: if True, an INNER JOIN will be used. This should - only be specified if querying for one specific subtype only - """ - primary_mapper = _class_to_mapper(base) - mappers, selectable = primary_mapper.\ - _with_polymorphic_args(classes, selectable, - innerjoin=innerjoin) - if aliased or flat: - selectable = selectable.alias(flat=flat) - return AliasedClass(base, - selectable, - with_polymorphic_mappers=mappers, - with_polymorphic_discriminator=polymorphic_on, - use_mapper_path=_use_mapper_path) - - -def _orm_annotate(element, exclude=None): - """Deep copy the given ClauseElement, annotating each element with the - "_orm_adapt" flag. - - Elements within the exclude collection will be cloned but not annotated. - - """ - return sql_util._deep_annotate(element, {'_orm_adapt': True}, exclude) - - -def _orm_deannotate(element): - """Remove annotations that link a column to a particular mapping. - - Note this doesn't affect "remote" and "foreign" annotations - passed by the :func:`.orm.foreign` and :func:`.orm.remote` - annotators. - - """ - - return sql_util._deep_deannotate(element, - values=("_orm_adapt", "parententity") - ) - - -def _orm_full_deannotate(element): - return sql_util._deep_deannotate(element) - - -class _ORMJoin(expression.Join): - """Extend Join to support ORM constructs as input.""" - - __visit_name__ = expression.Join.__visit_name__ - - def __init__(self, left, right, onclause=None, isouter=False): - - left_info = inspection.inspect(left) - left_orm_info = getattr(left, '_joined_from_info', left_info) - - right_info = inspection.inspect(right) - adapt_to = right_info.selectable - - self._joined_from_info = right_info - - if isinstance(onclause, util.string_types): - onclause = getattr(left_orm_info.entity, onclause) - - if isinstance(onclause, attributes.QueryableAttribute): - on_selectable = onclause.comparator._source_selectable() - prop = onclause.property - elif isinstance(onclause, MapperProperty): - prop = onclause - on_selectable = prop.parent.selectable - else: - prop = None - - if prop: - if sql_util.clause_is_present(on_selectable, left_info.selectable): - adapt_from = on_selectable - else: - adapt_from = left_info.selectable - - pj, sj, source, dest, \ - secondary, target_adapter = prop._create_joins( - source_selectable=adapt_from, - dest_selectable=adapt_to, - source_polymorphic=True, - dest_polymorphic=True, - of_type=right_info.mapper) - - if sj is not None: - if isouter: - # note this is an inner join from secondary->right - right = sql.join(secondary, right, sj) - onclause = pj - else: - left = sql.join(left, secondary, pj, isouter) - onclause = sj - else: - onclause = pj - self._target_adapter = target_adapter - - expression.Join.__init__(self, left, right, onclause, isouter) - - def join(self, right, onclause=None, isouter=False, join_to_left=None): - return _ORMJoin(self, right, onclause, isouter) - - def outerjoin(self, right, onclause=None, join_to_left=None): - return _ORMJoin(self, right, onclause, True) - - -def join(left, right, onclause=None, isouter=False, join_to_left=None): - """Produce an inner join between left and right clauses. - - :func:`.orm.join` is an extension to the core join interface - provided by :func:`.sql.expression.join()`, where the - left and right selectables may be not only core selectable - objects such as :class:`.Table`, but also mapped classes or - :class:`.AliasedClass` instances. The "on" clause can - be a SQL expression, or an attribute or string name - referencing a configured :func:`.relationship`. - - :func:`.orm.join` is not commonly needed in modern usage, - as its functionality is encapsulated within that of the - :meth:`.Query.join` method, which features a - significant amount of automation beyond :func:`.orm.join` - by itself. Explicit usage of :func:`.orm.join` - with :class:`.Query` involves usage of the - :meth:`.Query.select_from` method, as in:: - - from sqlalchemy.orm import join - session.query(User).\\ - select_from(join(User, Address, User.addresses)).\\ - filter(Address.email_address=='foo@bar.com') - - In modern SQLAlchemy the above join can be written more - succinctly as:: - - session.query(User).\\ - join(User.addresses).\\ - filter(Address.email_address=='foo@bar.com') - - See :meth:`.Query.join` for information on modern usage - of ORM level joins. - - .. versionchanged:: 0.8.1 - the ``join_to_left`` parameter - is no longer used, and is deprecated. - - """ - return _ORMJoin(left, right, onclause, isouter) - - -def outerjoin(left, right, onclause=None, join_to_left=None): - """Produce a left outer join between left and right clauses. - - This is the "outer join" version of the :func:`.orm.join` function, - featuring the same behavior except that an OUTER JOIN is generated. - See that function's documentation for other usage details. - - """ - return _ORMJoin(left, right, onclause, True) - - -def with_parent(instance, prop): - """Create filtering criterion that relates this query's primary entity - to the given related instance, using established :func:`.relationship()` - configuration. - - The SQL rendered is the same as that rendered when a lazy loader - would fire off from the given parent on that attribute, meaning - that the appropriate state is taken from the parent object in - Python without the need to render joins to the parent table - in the rendered statement. - - .. versionchanged:: 0.6.4 - This method accepts parent instances in all - persistence states, including transient, persistent, and detached. - Only the requisite primary key/foreign key attributes need to - be populated. Previous versions didn't work with transient - instances. - - :param instance: - An instance which has some :func:`.relationship`. - - :param property: - String property name, or class-bound attribute, which indicates - what relationship from the instance should be used to reconcile the - parent/child relationship. - - """ - if isinstance(prop, util.string_types): - mapper = object_mapper(instance) - prop = getattr(mapper.class_, prop).property - elif isinstance(prop, attributes.QueryableAttribute): - prop = prop.property - - return prop.compare(operators.eq, - instance, - value_is_parent=True) - - - -def has_identity(object): - """Return True if the given object has a database - identity. - - This typically corresponds to the object being - in either the persistent or detached state. - - .. seealso:: - - :func:`.was_deleted` - - """ - state = attributes.instance_state(object) - return state.has_identity - -def was_deleted(object): - """Return True if the given object was deleted - within a session flush. - - .. versionadded:: 0.8.0 - - """ - - state = attributes.instance_state(object) - return state.deleted - - - - -def randomize_unitofwork(): - """Use random-ordering sets within the unit of work in order - to detect unit of work sorting issues. - - This is a utility function that can be used to help reproduce - inconsistent unit of work sorting issues. For example, - if two kinds of objects A and B are being inserted, and - B has a foreign key reference to A - the A must be inserted first. - However, if there is no relationship between A and B, the unit of work - won't know to perform this sorting, and an operation may or may not - fail, depending on how the ordering works out. Since Python sets - and dictionaries have non-deterministic ordering, such an issue may - occur on some runs and not on others, and in practice it tends to - have a great dependence on the state of the interpreter. This leads - to so-called "heisenbugs" where changing entirely irrelevant aspects - of the test program still cause the failure behavior to change. - - By calling ``randomize_unitofwork()`` when a script first runs, the - ordering of a key series of sets within the unit of work implementation - are randomized, so that the script can be minimized down to the fundamental - mapping and operation that's failing, while still reproducing the issue - on at least some runs. - - This utility is also available when running the test suite via the - ``--reversetop`` flag. - - .. versionadded:: 0.8.1 created a standalone version of the - ``--reversetop`` feature. - - """ - from sqlalchemy.orm import unitofwork, session, mapper, dependency - from sqlalchemy.util import topological - from sqlalchemy.testing.util import RandomSet - topological.set = unitofwork.set = session.set = mapper.set = \ - dependency.set = RandomSet - diff --git a/libs/sqlalchemy/pool.py b/libs/sqlalchemy/pool.py deleted file mode 100644 index 3adfb320..00000000 --- a/libs/sqlalchemy/pool.py +++ /dev/null @@ -1,1110 +0,0 @@ -# sqlalchemy/pool.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -"""Connection pooling for DB-API connections. - -Provides a number of connection pool implementations for a variety of -usage scenarios and thread behavior requirements imposed by the -application, DB-API or database itself. - -Also provides a DB-API 2.0 connection proxying mechanism allowing -regular DB-API connect() methods to be transparently managed by a -SQLAlchemy connection pool. -""" - -import time -import traceback -import weakref - -from . import exc, log, event, interfaces, util -from .util import queue as sqla_queue -from .util import threading, memoized_property, \ - chop_traceback - -from collections import deque -proxies = {} - - -def manage(module, **params): - """Return a proxy for a DB-API module that automatically - pools connections. - - Given a DB-API 2.0 module and pool management parameters, returns - a proxy for the module that will automatically pool connections, - creating new connection pools for each distinct set of connection - arguments sent to the decorated module's connect() function. - - :param module: a DB-API 2.0 database module - - :param poolclass: the class used by the pool module to provide - pooling. Defaults to :class:`.QueuePool`. - - :param \*\*params: will be passed through to *poolclass* - - """ - try: - return proxies[module] - except KeyError: - return proxies.setdefault(module, _DBProxy(module, **params)) - - -def clear_managers(): - """Remove all current DB-API 2.0 managers. - - All pools and connections are disposed. - """ - - for manager in proxies.values(): - manager.close() - proxies.clear() - -reset_rollback = util.symbol('reset_rollback') -reset_commit = util.symbol('reset_commit') -reset_none = util.symbol('reset_none') - -class _ConnDialect(object): - """partial implementation of :class:`.Dialect` - which provides DBAPI connection methods. - - When a :class:`.Pool` is combined with an :class:`.Engine`, - the :class:`.Engine` replaces this with its own - :class:`.Dialect`. - - """ - def do_rollback(self, dbapi_connection): - dbapi_connection.rollback() - - def do_commit(self, dbapi_connection): - dbapi_connection.commit() - - def do_close(self, dbapi_connection): - dbapi_connection.close() - -class Pool(log.Identified): - """Abstract base class for connection pools.""" - - _dialect = _ConnDialect() - - def __init__(self, - creator, recycle=-1, echo=None, - use_threadlocal=False, - logging_name=None, - reset_on_return=True, - listeners=None, - events=None, - _dispatch=None, - _dialect=None): - """ - Construct a Pool. - - :param creator: a callable function that returns a DB-API - connection object. The function will be called with - parameters. - - :param recycle: If set to non -1, number of seconds between - connection recycling, which means upon checkout, if this - timeout is surpassed the connection will be closed and - replaced with a newly opened connection. Defaults to -1. - - :param logging_name: String identifier which will be used within - the "name" field of logging records generated within the - "sqlalchemy.pool" logger. Defaults to a hexstring of the object's - id. - - :param echo: If True, connections being pulled and retrieved - from the pool will be logged to the standard output, as well - as pool sizing information. Echoing can also be achieved by - enabling logging for the "sqlalchemy.pool" - namespace. Defaults to False. - - :param use_threadlocal: If set to True, repeated calls to - :meth:`connect` within the same application thread will be - guaranteed to return the same connection object, if one has - already been retrieved from the pool and has not been - returned yet. Offers a slight performance advantage at the - cost of individual transactions by default. The - :meth:`unique_connection` method is provided to bypass the - threadlocal behavior installed into :meth:`connect`. - - :param reset_on_return: Configures the action to take - on connections as they are returned to the pool. - See the argument description in :class:`.QueuePool` for - more detail. - - :param events: a list of 2-tuples, each of the form - ``(callable, target)`` which will be passed to event.listen() - upon construction. Provided here so that event listeners - can be assigned via ``create_engine`` before dialect-level - listeners are applied. - - :param listeners: Deprecated. A list of - :class:`~sqlalchemy.interfaces.PoolListener`-like objects or - dictionaries of callables that receive events when DB-API - connections are created, checked out and checked in to the - pool. This has been superseded by - :func:`~sqlalchemy.event.listen`. - - """ - if logging_name: - self.logging_name = self._orig_logging_name = logging_name - else: - self._orig_logging_name = None - - log.instance_logger(self, echoflag=echo) - self._threadconns = threading.local() - self._creator = creator - self._recycle = recycle - self._use_threadlocal = use_threadlocal - if reset_on_return in ('rollback', True, reset_rollback): - self._reset_on_return = reset_rollback - elif reset_on_return in (None, False, reset_none): - self._reset_on_return = reset_none - elif reset_on_return in ('commit', reset_commit): - self._reset_on_return = reset_commit - else: - raise exc.ArgumentError( - "Invalid value for 'reset_on_return': %r" - % reset_on_return) - - self.echo = echo - if _dispatch: - self.dispatch._update(_dispatch, only_propagate=False) - if _dialect: - self._dialect = _dialect - if events: - for fn, target in events: - event.listen(self, target, fn) - if listeners: - util.warn_deprecated( - "The 'listeners' argument to Pool (and " - "create_engine()) is deprecated. Use event.listen().") - for l in listeners: - self.add_listener(l) - - def _close_connection(self, connection): - self.logger.debug("Closing connection %r", connection) - try: - self._dialect.do_close(connection) - except (SystemExit, KeyboardInterrupt): - raise - except: - self.logger.error("Exception closing connection %r", - connection, exc_info=True) - - @util.deprecated( - 2.7, "Pool.add_listener is deprecated. Use event.listen()") - def add_listener(self, listener): - """Add a :class:`.PoolListener`-like object to this pool. - - ``listener`` may be an object that implements some or all of - PoolListener, or a dictionary of callables containing implementations - of some or all of the named methods in PoolListener. - - """ - interfaces.PoolListener._adapt_listener(self, listener) - - def unique_connection(self): - """Produce a DBAPI connection that is not referenced by any - thread-local context. - - This method is different from :meth:`.Pool.connect` only if the - ``use_threadlocal`` flag has been set to ``True``. - - """ - - return _ConnectionFairy.checkout(self) - - def _create_connection(self): - """Called by subclasses to create a new ConnectionRecord.""" - - return _ConnectionRecord(self) - - def recreate(self): - """Return a new :class:`.Pool`, of the same class as this one - and configured with identical creation arguments. - - This method is used in conjunection with :meth:`dispose` - to close out an entire :class:`.Pool` and create a new one in - its place. - - """ - - raise NotImplementedError() - - def dispose(self): - """Dispose of this pool. - - This method leaves the possibility of checked-out connections - remaining open, as it only affects connections that are - idle in the pool. - - See also the :meth:`Pool.recreate` method. - - """ - - raise NotImplementedError() - - def _replace(self): - """Dispose + recreate this pool. - - Subclasses may employ special logic to - move threads waiting on this pool to the - new one. - - """ - self.dispose() - return self.recreate() - - def connect(self): - """Return a DBAPI connection from the pool. - - The connection is instrumented such that when its - ``close()`` method is called, the connection will be returned to - the pool. - - """ - if not self._use_threadlocal: - return _ConnectionFairy.checkout(self) - - try: - rec = self._threadconns.current() - except AttributeError: - pass - else: - if rec is not None: - return rec.checkout_existing() - - return _ConnectionFairy.checkout(self, self._threadconns) - - def _return_conn(self, record): - """Given a _ConnectionRecord, return it to the :class:`.Pool`. - - This method is called when an instrumented DBAPI connection - has its ``close()`` method called. - - """ - if self._use_threadlocal: - try: - del self._threadconns.current - except AttributeError: - pass - self._do_return_conn(record) - - def _do_get(self): - """Implementation for :meth:`get`, supplied by subclasses.""" - - raise NotImplementedError() - - def _do_return_conn(self, conn): - """Implementation for :meth:`return_conn`, supplied by subclasses.""" - - raise NotImplementedError() - - def status(self): - raise NotImplementedError() - - -class _ConnectionRecord(object): - - def __init__(self, pool): - self.__pool = pool - self.connection = self.__connect() - self.finalize_callback = deque() - - pool.dispatch.first_connect.\ - for_modify(pool.dispatch).\ - exec_once(self.connection, self) - pool.dispatch.connect(self.connection, self) - - @util.memoized_property - def info(self): - return {} - - @classmethod - def checkout(cls, pool): - rec = pool._do_get() - try: - dbapi_connection = rec.get_connection() - except: - rec.checkin() - raise - fairy = _ConnectionFairy(dbapi_connection, rec) - rec.fairy_ref = weakref.ref( - fairy, - lambda ref: _finalize_fairy and \ - _finalize_fairy( - dbapi_connection, - rec, pool, ref, pool._echo) - ) - _refs.add(rec) - if pool._echo: - pool.logger.debug("Connection %r checked out from pool", - dbapi_connection) - return fairy - - def checkin(self): - self.fairy_ref = None - connection = self.connection - pool = self.__pool - while self.finalize_callback: - finalizer = self.finalize_callback.pop() - finalizer(connection) - if pool.dispatch.checkin: - pool.dispatch.checkin(connection, self) - pool._return_conn(self) - - - def close(self): - if self.connection is not None: - self.__pool._close_connection(self.connection) - - def invalidate(self, e=None): - if e is not None: - self.__pool.logger.info( - "Invalidate connection %r (reason: %s:%s)", - self.connection, e.__class__.__name__, e) - else: - self.__pool.logger.info( - "Invalidate connection %r", self.connection) - self.__close() - self.connection = None - - def get_connection(self): - if self.connection is None: - self.connection = self.__connect() - self.info.clear() - if self.__pool.dispatch.connect: - self.__pool.dispatch.connect(self.connection, self) - elif self.__pool._recycle > -1 and \ - time.time() - self.starttime > self.__pool._recycle: - self.__pool.logger.info( - "Connection %r exceeded timeout; recycling", - self.connection) - self.__close() - self.connection = self.__connect() - self.info.clear() - if self.__pool.dispatch.connect: - self.__pool.dispatch.connect(self.connection, self) - return self.connection - - def __close(self): - self.__pool._close_connection(self.connection) - - def __connect(self): - try: - self.starttime = time.time() - connection = self.__pool._creator() - self.__pool.logger.debug("Created new connection %r", connection) - return connection - except Exception as e: - self.__pool.logger.debug("Error on connect(): %s", e) - raise - - -def _finalize_fairy(connection, connection_record, pool, ref, echo, fairy=None): - """Cleanup for a :class:`._ConnectionFairy` whether or not it's already - been garbage collected. - - """ - _refs.discard(connection_record) - - if ref is not None and \ - connection_record.fairy_ref is not ref: - return - - if connection is not None: - if connection_record and echo: - pool.logger.debug("Connection %r being returned to pool", - connection) - - try: - fairy = fairy or _ConnectionFairy(connection, connection_record) - if pool.dispatch.reset: - pool.dispatch.reset(fairy, connection_record) - if pool._reset_on_return is reset_rollback: - if echo: - pool.logger.debug("Connection %s rollback-on-return", - connection) - pool._dialect.do_rollback(fairy) - elif pool._reset_on_return is reset_commit: - if echo: - pool.logger.debug("Connection %s commit-on-return", - connection) - pool._dialect.do_commit(fairy) - - # Immediately close detached instances - if not connection_record: - pool._close_connection(connection) - except Exception as e: - if connection_record: - connection_record.invalidate(e=e) - if isinstance(e, (SystemExit, KeyboardInterrupt)): - raise - - if connection_record: - connection_record.checkin() - - -_refs = set() - - -class _ConnectionFairy(object): - """Proxies a DB-API connection and provides return-on-dereference - support.""" - - def __init__(self, dbapi_connection, connection_record): - self.connection = dbapi_connection - self._connection_record = connection_record - - @classmethod - def checkout(cls, pool, threadconns=None, fairy=None): - if not fairy: - fairy = _ConnectionRecord.checkout(pool) - - fairy._pool = pool - fairy._counter = 0 - fairy._echo = pool._should_log_debug() - - if threadconns is not None: - threadconns.current = weakref.ref(fairy) - - if fairy.connection is None: - raise exc.InvalidRequestError("This connection is closed") - fairy._counter += 1 - - if not pool.dispatch.checkout or fairy._counter != 1: - return fairy - - # Pool listeners can trigger a reconnection on checkout - attempts = 2 - while attempts > 0: - try: - pool.dispatch.checkout(fairy.connection, - fairy._connection_record, - fairy) - return fairy - except exc.DisconnectionError as e: - pool.logger.info( - "Disconnection detected on checkout: %s", e) - fairy._connection_record.invalidate(e) - fairy.connection = fairy._connection_record.get_connection() - attempts -= 1 - - pool.logger.info("Reconnection attempts exhausted on checkout") - fairy.invalidate() - raise exc.InvalidRequestError("This connection is closed") - - def checkout_existing(self): - return _ConnectionFairy.checkout(self._pool, fairy=self) - - def checkin(self): - _finalize_fairy(self.connection, self._connection_record, - self._pool, None, self._echo, fairy=self) - self.connection = None - self._connection_record = None - - _close = checkin - - @property - def _logger(self): - return self._pool.logger - - @property - def is_valid(self): - return self.connection is not None - - @util.memoized_property - def info(self): - """Info dictionary associated with the underlying DBAPI connection - referred to by this :class:`.ConnectionFairy`, allowing user-defined - data to be associated with the connection. - - The data here will follow along with the DBAPI connection including - after it is returned to the connection pool and used again - in subsequent instances of :class:`.ConnectionFairy`. - - """ - return self._connection_record.info - - def invalidate(self, e=None): - """Mark this connection as invalidated. - - The connection will be immediately closed. The containing - ConnectionRecord will create a new connection when next used. - """ - - if self.connection is None: - raise exc.InvalidRequestError("This connection is closed") - if self._connection_record: - self._connection_record.invalidate(e=e) - self.connection = None - self.checkin() - - def cursor(self, *args, **kwargs): - return self.connection.cursor(*args, **kwargs) - - def __getattr__(self, key): - return getattr(self.connection, key) - - - def detach(self): - """Separate this connection from its Pool. - - This means that the connection will no longer be returned to the - pool when closed, and will instead be literally closed. The - containing ConnectionRecord is separated from the DB-API connection, - and will create a new connection when next used. - - Note that any overall connection limiting constraints imposed by a - Pool implementation may be violated after a detach, as the detached - connection is removed from the pool's knowledge and control. - """ - - if self._connection_record is not None: - _refs.remove(self._connection_record) - self._connection_record.fairy_ref = None - self._connection_record.connection = None - # TODO: should this be _return_conn? - self._pool._do_return_conn(self._connection_record) - self.info = self.info.copy() - self._connection_record = None - - def close(self): - self._counter -= 1 - if self._counter == 0: - self.checkin() - - - -class SingletonThreadPool(Pool): - """A Pool that maintains one connection per thread. - - Maintains one connection per each thread, never moving a connection to a - thread other than the one which it was created in. - - Options are the same as those of :class:`.Pool`, as well as: - - :param pool_size: The number of threads in which to maintain connections - at once. Defaults to five. - - :class:`.SingletonThreadPool` is used by the SQLite dialect - automatically when a memory-based database is used. - See :ref:`sqlite_toplevel`. - - """ - - def __init__(self, creator, pool_size=5, **kw): - kw['use_threadlocal'] = True - Pool.__init__(self, creator, **kw) - self._conn = threading.local() - self._all_conns = set() - self.size = pool_size - - def recreate(self): - self.logger.info("Pool recreating") - return self.__class__(self._creator, - pool_size=self.size, - recycle=self._recycle, - echo=self.echo, - logging_name=self._orig_logging_name, - use_threadlocal=self._use_threadlocal, - reset_on_return=self._reset_on_return, - _dispatch=self.dispatch, - _dialect=self._dialect) - - def dispose(self): - """Dispose of this pool.""" - - for conn in self._all_conns: - try: - conn.close() - except (SystemExit, KeyboardInterrupt): - raise - except: - # pysqlite won't even let you close a conn from a thread - # that didn't create it - pass - - self._all_conns.clear() - - def _cleanup(self): - while len(self._all_conns) > self.size: - c = self._all_conns.pop() - c.close() - - def status(self): - return "SingletonThreadPool id:%d size: %d" % \ - (id(self), len(self._all_conns)) - - def _do_return_conn(self, conn): - pass - - def _do_get(self): - try: - c = self._conn.current() - if c: - return c - except AttributeError: - pass - c = self._create_connection() - self._conn.current = weakref.ref(c) - self._all_conns.add(c) - if len(self._all_conns) > self.size: - self._cleanup() - return c - - -class QueuePool(Pool): - """A :class:`.Pool` that imposes a limit on the number of open connections. - - :class:`.QueuePool` is the default pooling implementation used for - all :class:`.Engine` objects, unless the SQLite dialect is in use. - - """ - - def __init__(self, creator, pool_size=5, max_overflow=10, timeout=30, - **kw): - """ - Construct a QueuePool. - - :param creator: a callable function that returns a DB-API - connection object. The function will be called with - parameters. - - :param pool_size: The size of the pool to be maintained, - defaults to 5. This is the largest number of connections that - will be kept persistently in the pool. Note that the pool - begins with no connections; once this number of connections - is requested, that number of connections will remain. - ``pool_size`` can be set to 0 to indicate no size limit; to - disable pooling, use a :class:`~sqlalchemy.pool.NullPool` - instead. - - :param max_overflow: The maximum overflow size of the - pool. When the number of checked-out connections reaches the - size set in pool_size, additional connections will be - returned up to this limit. When those additional connections - are returned to the pool, they are disconnected and - discarded. It follows then that the total number of - simultaneous connections the pool will allow is pool_size + - `max_overflow`, and the total number of "sleeping" - connections the pool will allow is pool_size. `max_overflow` - can be set to -1 to indicate no overflow limit; no limit - will be placed on the total number of concurrent - connections. Defaults to 10. - - :param timeout: The number of seconds to wait before giving up - on returning a connection. Defaults to 30. - - :param recycle: If set to non -1, number of seconds between - connection recycling, which means upon checkout, if this - timeout is surpassed the connection will be closed and - replaced with a newly opened connection. Defaults to -1. - - :param echo: If True, connections being pulled and retrieved - from the pool will be logged to the standard output, as well - as pool sizing information. Echoing can also be achieved by - enabling logging for the "sqlalchemy.pool" - namespace. Defaults to False. - - :param use_threadlocal: If set to True, repeated calls to - :meth:`connect` within the same application thread will be - guaranteed to return the same connection object, if one has - already been retrieved from the pool and has not been - returned yet. Offers a slight performance advantage at the - cost of individual transactions by default. The - :meth:`unique_connection` method is provided to bypass the - threadlocal behavior installed into :meth:`connect`. - - :param reset_on_return: Determine steps to take on - connections as they are returned to the pool. - reset_on_return can have any of these values: - - * 'rollback' - call rollback() on the connection, - to release locks and transaction resources. - This is the default value. The vast majority - of use cases should leave this value set. - * True - same as 'rollback', this is here for - backwards compatibility. - * 'commit' - call commit() on the connection, - to release locks and transaction resources. - A commit here may be desirable for databases that - cache query plans if a commit is emitted, - such as Microsoft SQL Server. However, this - value is more dangerous than 'rollback' because - any data changes present on the transaction - are committed unconditionally. - * None - don't do anything on the connection. - This setting should only be made on a database - that has no transaction support at all, - namely MySQL MyISAM. By not doing anything, - performance can be improved. This - setting should **never be selected** for a - database that supports transactions, - as it will lead to deadlocks and stale - state. - * False - same as None, this is here for - backwards compatibility. - - .. versionchanged:: 0.7.6 - ``reset_on_return`` accepts values. - - :param listeners: A list of - :class:`~sqlalchemy.interfaces.PoolListener`-like objects or - dictionaries of callables that receive events when DB-API - connections are created, checked out and checked in to the - pool. - - """ - Pool.__init__(self, creator, **kw) - self._pool = sqla_queue.Queue(pool_size) - self._overflow = 0 - pool_size - self._max_overflow = max_overflow - self._timeout = timeout - self._overflow_lock = threading.Lock() - - def _do_return_conn(self, conn): - try: - self._pool.put(conn, False) - except sqla_queue.Full: - try: - conn.close() - finally: - self._dec_overflow() - - def _do_get(self): - use_overflow = self._max_overflow > -1 - - try: - wait = use_overflow and self._overflow >= self._max_overflow - return self._pool.get(wait, self._timeout) - except sqla_queue.SAAbort as aborted: - return aborted.context._do_get() - except sqla_queue.Empty: - if use_overflow and self._overflow >= self._max_overflow: - if not wait: - return self._do_get() - else: - raise exc.TimeoutError( - "QueuePool limit of size %d overflow %d reached, " - "connection timed out, timeout %d" % - (self.size(), self.overflow(), self._timeout)) - - if self._inc_overflow(): - try: - return self._create_connection() - except: - self._dec_overflow() - raise - else: - return self._do_get() - - def _inc_overflow(self): - if self._max_overflow == -1: - self._overflow += 1 - return True - with self._overflow_lock: - if self._overflow < self._max_overflow: - self._overflow += 1 - return True - else: - return False - - def _dec_overflow(self): - if self._max_overflow == -1: - self._overflow -= 1 - return True - with self._overflow_lock: - self._overflow -= 1 - return True - - def recreate(self): - self.logger.info("Pool recreating") - return self.__class__(self._creator, pool_size=self._pool.maxsize, - max_overflow=self._max_overflow, - timeout=self._timeout, - recycle=self._recycle, echo=self.echo, - logging_name=self._orig_logging_name, - use_threadlocal=self._use_threadlocal, - reset_on_return=self._reset_on_return, - _dispatch=self.dispatch, - _dialect=self._dialect) - - def dispose(self): - while True: - try: - conn = self._pool.get(False) - conn.close() - except sqla_queue.Empty: - break - - self._overflow = 0 - self.size() - self.logger.info("Pool disposed. %s", self.status()) - - def _replace(self): - self.dispose() - np = self.recreate() - self._pool.abort(np) - return np - - def status(self): - return "Pool size: %d Connections in pool: %d "\ - "Current Overflow: %d Current Checked out "\ - "connections: %d" % (self.size(), - self.checkedin(), - self.overflow(), - self.checkedout()) - - def size(self): - return self._pool.maxsize - - def checkedin(self): - return self._pool.qsize() - - def overflow(self): - return self._overflow - - def checkedout(self): - return self._pool.maxsize - self._pool.qsize() + self._overflow - - -class NullPool(Pool): - """A Pool which does not pool connections. - - Instead it literally opens and closes the underlying DB-API connection - per each connection open/close. - - Reconnect-related functions such as ``recycle`` and connection - invalidation are not supported by this Pool implementation, since - no connections are held persistently. - - .. versionchanged:: 0.7 - :class:`.NullPool` is used by the SQlite dialect automatically - when a file-based database is used. See :ref:`sqlite_toplevel`. - - """ - - def status(self): - return "NullPool" - - def _do_return_conn(self, conn): - conn.close() - - def _do_get(self): - return self._create_connection() - - def recreate(self): - self.logger.info("Pool recreating") - - return self.__class__(self._creator, - recycle=self._recycle, - echo=self.echo, - logging_name=self._orig_logging_name, - use_threadlocal=self._use_threadlocal, - reset_on_return=self._reset_on_return, - _dispatch=self.dispatch, - _dialect=self._dialect) - - def dispose(self): - pass - - -class StaticPool(Pool): - """A Pool of exactly one connection, used for all requests. - - Reconnect-related functions such as ``recycle`` and connection - invalidation (which is also used to support auto-reconnect) are not - currently supported by this Pool implementation but may be implemented - in a future release. - - """ - - @memoized_property - def _conn(self): - return self._creator() - - @memoized_property - def connection(self): - return _ConnectionRecord(self) - - def status(self): - return "StaticPool" - - def dispose(self): - if '_conn' in self.__dict__: - self._conn.close() - self._conn = None - - def recreate(self): - self.logger.info("Pool recreating") - return self.__class__(creator=self._creator, - recycle=self._recycle, - use_threadlocal=self._use_threadlocal, - reset_on_return=self._reset_on_return, - echo=self.echo, - logging_name=self._orig_logging_name, - _dispatch=self.dispatch, - _dialect=self._dialect) - - def _create_connection(self): - return self._conn - - def _do_return_conn(self, conn): - pass - - def _do_get(self): - return self.connection - - -class AssertionPool(Pool): - """A :class:`.Pool` that allows at most one checked out connection at - any given time. - - This will raise an exception if more than one connection is checked out - at a time. Useful for debugging code that is using more connections - than desired. - - .. versionchanged:: 0.7 - :class:`.AssertionPool` also logs a traceback of where - the original connection was checked out, and reports - this in the assertion error raised. - - """ - def __init__(self, *args, **kw): - self._conn = None - self._checked_out = False - self._store_traceback = kw.pop('store_traceback', True) - self._checkout_traceback = None - Pool.__init__(self, *args, **kw) - - def status(self): - return "AssertionPool" - - def _do_return_conn(self, conn): - if not self._checked_out: - raise AssertionError("connection is not checked out") - self._checked_out = False - assert conn is self._conn - - def dispose(self): - self._checked_out = False - if self._conn: - self._conn.close() - - def recreate(self): - self.logger.info("Pool recreating") - return self.__class__(self._creator, echo=self.echo, - logging_name=self._orig_logging_name, - _dispatch=self.dispatch, - _dialect=self._dialect) - - def _do_get(self): - if self._checked_out: - if self._checkout_traceback: - suffix = ' at:\n%s' % ''.join( - chop_traceback(self._checkout_traceback)) - else: - suffix = '' - raise AssertionError("connection is already checked out" + suffix) - - if not self._conn: - self._conn = self._create_connection() - - self._checked_out = True - if self._store_traceback: - self._checkout_traceback = traceback.format_stack() - return self._conn - - -class _DBProxy(object): - """Layers connection pooling behavior on top of a standard DB-API module. - - Proxies a DB-API 2.0 connect() call to a connection pool keyed to the - specific connect parameters. Other functions and attributes are delegated - to the underlying DB-API module. - """ - - def __init__(self, module, poolclass=QueuePool, **kw): - """Initializes a new proxy. - - module - a DB-API 2.0 module - - poolclass - a Pool class, defaulting to QueuePool - - Other parameters are sent to the Pool object's constructor. - - """ - - self.module = module - self.kw = kw - self.poolclass = poolclass - self.pools = {} - self._create_pool_mutex = threading.Lock() - - def close(self): - for key in list(self.pools): - del self.pools[key] - - def __del__(self): - self.close() - - def __getattr__(self, key): - return getattr(self.module, key) - - def get_pool(self, *args, **kw): - key = self._serialize(*args, **kw) - try: - return self.pools[key] - except KeyError: - self._create_pool_mutex.acquire() - try: - if key not in self.pools: - kw.pop('sa_pool_key', None) - pool = self.poolclass(lambda: - self.module.connect(*args, **kw), **self.kw) - self.pools[key] = pool - return pool - else: - return self.pools[key] - finally: - self._create_pool_mutex.release() - - def connect(self, *args, **kw): - """Activate a connection to the database. - - Connect to the database using this DBProxy's module and the given - connect arguments. If the arguments match an existing pool, the - connection will be returned from the pool's current thread-local - connection instance, or if there is no thread-local connection - instance it will be checked out from the set of pooled connections. - - If the pool has no available connections and allows new connections - to be created, a new database connection will be made. - - """ - - return self.get_pool(*args, **kw).connect() - - def dispose(self, *args, **kw): - """Dispose the pool referenced by the given connect arguments.""" - - key = self._serialize(*args, **kw) - try: - del self.pools[key] - except KeyError: - pass - - def _serialize(self, *args, **kw): - if "sa_pool_key" in kw: - return kw['sa_pool_key'] - - return tuple( - list(args) + - [(k, kw[k]) for k in sorted(kw)] - ) diff --git a/libs/sqlalchemy/processors.py b/libs/sqlalchemy/processors.py deleted file mode 100644 index 0abf063b..00000000 --- a/libs/sqlalchemy/processors.py +++ /dev/null @@ -1,131 +0,0 @@ -# sqlalchemy/processors.py -# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors -# Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""defines generic type conversion functions, as used in bind and result -processors. - -They all share one common characteristic: None is passed through unchanged. - -""" - -import codecs -import re -import datetime - - -def str_to_datetime_processor_factory(regexp, type_): - rmatch = regexp.match - # Even on python2.6 datetime.strptime is both slower than this code - # and it does not support microseconds. - has_named_groups = bool(regexp.groupindex) - - def process(value): - if value is None: - return None - else: - try: - m = rmatch(value) - except TypeError: - raise ValueError("Couldn't parse %s string '%r' " - "- value is not a string." % - (type_.__name__, value)) - if m is None: - raise ValueError("Couldn't parse %s string: " - "'%s'" % (type_.__name__, value)) - if has_named_groups: - groups = m.groupdict(0) - return type_(**dict(list(zip(iter(groups.keys()), - list(map(int, iter(groups.values()))))))) - else: - return type_(*list(map(int, m.groups(0)))) - return process - - -def boolean_to_int(value): - if value is None: - return None - else: - return int(value) - - -def py_fallback(): - def to_unicode_processor_factory(encoding, errors=None): - decoder = codecs.getdecoder(encoding) - - def process(value): - if value is None: - return None - else: - # decoder returns a tuple: (value, len). Simply dropping the - # len part is safe: it is done that way in the normal - # 'xx'.decode(encoding) code path. - return decoder(value, errors)[0] - return process - - def to_decimal_processor_factory(target_class, scale): - fstring = "%%.%df" % scale - - def process(value): - if value is None: - return None - else: - return target_class(fstring % value) - return process - - def to_float(value): - if value is None: - return None - else: - return float(value) - - def to_str(value): - if value is None: - return None - else: - return str(value) - - def int_to_boolean(value): - if value is None: - return None - else: - return value and True or False - - DATETIME_RE = re.compile( - "(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)(?:\.(\d+))?") - TIME_RE = re.compile("(\d+):(\d+):(\d+)(?:\.(\d+))?") - DATE_RE = re.compile("(\d+)-(\d+)-(\d+)") - - str_to_datetime = str_to_datetime_processor_factory(DATETIME_RE, - datetime.datetime) - str_to_time = str_to_datetime_processor_factory(TIME_RE, datetime.time) - str_to_date = str_to_datetime_processor_factory(DATE_RE, datetime.date) - return locals() - -try: - from sqlalchemy.cprocessors import UnicodeResultProcessor, \ - DecimalResultProcessor, \ - to_float, to_str, int_to_boolean, \ - str_to_datetime, str_to_time, \ - str_to_date - - def to_unicode_processor_factory(encoding, errors=None): - # this is cumbersome but it would be even more so on the C side - if errors is not None: - return UnicodeResultProcessor(encoding, errors).process - else: - return UnicodeResultProcessor(encoding).process - - def to_decimal_processor_factory(target_class, scale): - # Note that the scale argument is not taken into account for integer - # values in the C implementation while it is in the Python one. - # For example, the Python implementation might return - # Decimal('5.00000') whereas the C implementation will - # return Decimal('5'). These are equivalent of course. - return DecimalResultProcessor(target_class, "%%.%df" % scale).process - -except ImportError: - globals().update(py_fallback()) diff --git a/libs/sqlalchemy/schema.py b/libs/sqlalchemy/schema.py deleted file mode 100644 index 17214153..00000000 --- a/libs/sqlalchemy/schema.py +++ /dev/null @@ -1,54 +0,0 @@ -# schema.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Compatiblity namespace for sqlalchemy.sql.schema and related. - -""" - - -from .sql.schema import ( - CheckConstraint, - Column, - ColumnDefault, - Constraint, - DefaultClause, - DefaultGenerator, - FetchedValue, - ForeignKey, - ForeignKeyConstraint, - Index, - MetaData, - PassiveDefault, - PrimaryKeyConstraint, - SchemaItem, - Sequence, - Table, - ThreadLocalMetaData, - UniqueConstraint, - _get_table_key, - ColumnCollectionConstraint, - ) - - -from .sql.ddl import ( - DDL, - CreateTable, - DropTable, - CreateSequence, - DropSequence, - CreateIndex, - DropIndex, - CreateSchema, - DropSchema, - _DropView, - CreateColumn, - AddConstraint, - DropConstraint, - DDLBase, - DDLElement, - _CreateDropBase, - _DDLCompiles -) diff --git a/libs/sqlalchemy/sql/__init__.py b/libs/sqlalchemy/sql/__init__.py deleted file mode 100644 index 9ed6049a..00000000 --- a/libs/sqlalchemy/sql/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -# sql/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .expression import ( - Alias, - ClauseElement, - ColumnCollection, - ColumnElement, - CompoundSelect, - Delete, - FromClause, - Insert, - Join, - Select, - Selectable, - TableClause, - Update, - alias, - and_, - asc, - between, - bindparam, - case, - cast, - collate, - column, - delete, - desc, - distinct, - except_, - except_all, - exists, - extract, - false, - False_, - func, - insert, - intersect, - intersect_all, - join, - label, - literal, - literal_column, - modifier, - not_, - null, - or_, - outerjoin, - outparam, - over, - select, - subquery, - table, - text, - true, - True_, - tuple_, - type_coerce, - union, - union_all, - update, - ) - -from .visitors import ClauseVisitor - - -def __go(lcls): - global __all__ - from .. import util as _sa_util - - import inspect as _inspect - - __all__ = sorted(name for name, obj in lcls.items() - if not (name.startswith('_') or _inspect.ismodule(obj))) - - from .annotation import _prepare_annotations, Annotated - from .elements import AnnotatedColumnElement, ClauseList - from .selectable import AnnotatedFromClause - _prepare_annotations(ColumnElement, AnnotatedColumnElement) - _prepare_annotations(FromClause, AnnotatedFromClause) - _prepare_annotations(ClauseList, Annotated) - - _sa_util.dependencies.resolve_all("sqlalchemy.sql") - -__go(locals()) - diff --git a/libs/sqlalchemy/sql/annotation.py b/libs/sqlalchemy/sql/annotation.py deleted file mode 100644 index b5b7849d..00000000 --- a/libs/sqlalchemy/sql/annotation.py +++ /dev/null @@ -1,182 +0,0 @@ -# sql/annotation.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The :class:`.Annotated` class and related routines; creates hash-equivalent -copies of SQL constructs which contain context-specific markers and associations. - -""" - -from .. import util -from . import operators - -class Annotated(object): - """clones a ClauseElement and applies an 'annotations' dictionary. - - Unlike regular clones, this clone also mimics __hash__() and - __cmp__() of the original element so that it takes its place - in hashed collections. - - A reference to the original element is maintained, for the important - reason of keeping its hash value current. When GC'ed, the - hash value may be reused, causing conflicts. - - """ - - def __new__(cls, *args): - if not args: - # clone constructor - return object.__new__(cls) - else: - element, values = args - # pull appropriate subclass from registry of annotated - # classes - try: - cls = annotated_classes[element.__class__] - except KeyError: - cls = _new_annotation_type(element.__class__, cls) - return object.__new__(cls) - - def __init__(self, element, values): - self.__dict__ = element.__dict__.copy() - self.__element = element - self._annotations = values - - def _annotate(self, values): - _values = self._annotations.copy() - _values.update(values) - return self._with_annotations(_values) - - def _with_annotations(self, values): - clone = self.__class__.__new__(self.__class__) - clone.__dict__ = self.__dict__.copy() - clone._annotations = values - return clone - - def _deannotate(self, values=None, clone=True): - if values is None: - return self.__element - else: - _values = self._annotations.copy() - for v in values: - _values.pop(v, None) - return self._with_annotations(_values) - - def _compiler_dispatch(self, visitor, **kw): - return self.__element.__class__._compiler_dispatch(self, visitor, **kw) - - @property - def _constructor(self): - return self.__element._constructor - - def _clone(self): - clone = self.__element._clone() - if clone is self.__element: - # detect immutable, don't change anything - return self - else: - # update the clone with any changes that have occurred - # to this object's __dict__. - clone.__dict__.update(self.__dict__) - return self.__class__(clone, self._annotations) - - def __hash__(self): - return hash(self.__element) - - def __eq__(self, other): - if isinstance(self.__element, operators.ColumnOperators): - return self.__element.__class__.__eq__(self, other) - else: - return hash(other) == hash(self) - - - -# hard-generate Annotated subclasses. this technique -# is used instead of on-the-fly types (i.e. type.__new__()) -# so that the resulting objects are pickleable. -annotated_classes = {} - - - -def _deep_annotate(element, annotations, exclude=None): - """Deep copy the given ClauseElement, annotating each element - with the given annotations dictionary. - - Elements within the exclude collection will be cloned but not annotated. - - """ - def clone(elem): - if exclude and \ - hasattr(elem, 'proxy_set') and \ - elem.proxy_set.intersection(exclude): - newelem = elem._clone() - elif annotations != elem._annotations: - newelem = elem._annotate(annotations) - else: - newelem = elem - newelem._copy_internals(clone=clone) - return newelem - - if element is not None: - element = clone(element) - return element - - -def _deep_deannotate(element, values=None): - """Deep copy the given element, removing annotations.""" - - cloned = util.column_dict() - - def clone(elem): - # if a values dict is given, - # the elem must be cloned each time it appears, - # as there may be different annotations in source - # elements that are remaining. if totally - # removing all annotations, can assume the same - # slate... - if values or elem not in cloned: - newelem = elem._deannotate(values=values, clone=True) - newelem._copy_internals(clone=clone) - if not values: - cloned[elem] = newelem - return newelem - else: - return cloned[elem] - - if element is not None: - element = clone(element) - return element - - -def _shallow_annotate(element, annotations): - """Annotate the given ClauseElement and copy its internals so that - internal objects refer to the new annotated object. - - Basically used to apply a "dont traverse" annotation to a - selectable, without digging throughout the whole - structure wasting time. - """ - element = element._annotate(annotations) - element._copy_internals() - return element - -def _new_annotation_type(cls, base_cls): - if issubclass(cls, Annotated): - return cls - elif cls in annotated_classes: - return annotated_classes[cls] - annotated_classes[cls] = anno_cls = type( - "Annotated%s" % cls.__name__, - (base_cls, cls), {}) - globals()["Annotated%s" % cls.__name__] = anno_cls - return anno_cls - -def _prepare_annotations(target_hierarchy, base_cls): - stack = [target_hierarchy] - while stack: - cls = stack.pop() - stack.extend(cls.__subclasses__()) - - _new_annotation_type(cls, base_cls) diff --git a/libs/sqlalchemy/sql/base.py b/libs/sqlalchemy/sql/base.py deleted file mode 100644 index e6c5d6ed..00000000 --- a/libs/sqlalchemy/sql/base.py +++ /dev/null @@ -1,348 +0,0 @@ -# sql/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Foundational utilities common to many sql modules. - -""" - - -from .. import util, exc -import itertools -from .visitors import ClauseVisitor - - -PARSE_AUTOCOMMIT = util.symbol('PARSE_AUTOCOMMIT') -NO_ARG = util.symbol('NO_ARG') - -class Immutable(object): - """mark a ClauseElement as 'immutable' when expressions are cloned.""" - - def unique_params(self, *optionaldict, **kwargs): - raise NotImplementedError("Immutable objects do not support copying") - - def params(self, *optionaldict, **kwargs): - raise NotImplementedError("Immutable objects do not support copying") - - def _clone(self): - return self - - - -def _from_objects(*elements): - return itertools.chain(*[element._from_objects for element in elements]) - -@util.decorator -def _generative(fn, *args, **kw): - """Mark a method as generative.""" - - self = args[0]._generate() - fn(self, *args[1:], **kw) - return self - - -class Generative(object): - """Allow a ClauseElement to generate itself via the - @_generative decorator. - - """ - - def _generate(self): - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - return s - - -class Executable(Generative): - """Mark a ClauseElement as supporting execution. - - :class:`.Executable` is a superclass for all "statement" types - of objects, including :func:`select`, :func:`delete`, :func:`update`, - :func:`insert`, :func:`text`. - - """ - - supports_execution = True - _execution_options = util.immutabledict() - _bind = None - - @_generative - def execution_options(self, **kw): - """ Set non-SQL options for the statement which take effect during - execution. - - Execution options can be set on a per-statement or - per :class:`.Connection` basis. Additionally, the - :class:`.Engine` and ORM :class:`~.orm.query.Query` objects provide - access to execution options which they in turn configure upon - connections. - - The :meth:`execution_options` method is generative. A new - instance of this statement is returned that contains the options:: - - statement = select([table.c.x, table.c.y]) - statement = statement.execution_options(autocommit=True) - - Note that only a subset of possible execution options can be applied - to a statement - these include "autocommit" and "stream_results", - but not "isolation_level" or "compiled_cache". - See :meth:`.Connection.execution_options` for a full list of - possible options. - - .. seealso:: - - :meth:`.Connection.execution_options()` - - :meth:`.Query.execution_options()` - - """ - if 'isolation_level' in kw: - raise exc.ArgumentError( - "'isolation_level' execution option may only be specified " - "on Connection.execution_options(), or " - "per-engine using the isolation_level " - "argument to create_engine()." - ) - if 'compiled_cache' in kw: - raise exc.ArgumentError( - "'compiled_cache' execution option may only be specified " - "on Connection.execution_options(), not per statement." - ) - self._execution_options = self._execution_options.union(kw) - - def execute(self, *multiparams, **params): - """Compile and execute this :class:`.Executable`.""" - e = self.bind - if e is None: - label = getattr(self, 'description', self.__class__.__name__) - msg = ('This %s is not directly bound to a Connection or Engine.' - 'Use the .execute() method of a Connection or Engine ' - 'to execute this construct.' % label) - raise exc.UnboundExecutionError(msg) - return e._execute_clauseelement(self, multiparams, params) - - def scalar(self, *multiparams, **params): - """Compile and execute this :class:`.Executable`, returning the - result's scalar representation. - - """ - return self.execute(*multiparams, **params).scalar() - - @property - def bind(self): - """Returns the :class:`.Engine` or :class:`.Connection` to - which this :class:`.Executable` is bound, or None if none found. - - This is a traversal which checks locally, then - checks among the "from" clauses of associated objects - until a bound engine or connection is found. - - """ - if self._bind is not None: - return self._bind - - for f in _from_objects(self): - if f is self: - continue - engine = f.bind - if engine is not None: - return engine - else: - return None - - -class SchemaEventTarget(object): - """Base class for elements that are the targets of :class:`.DDLEvents` - events. - - This includes :class:`.SchemaItem` as well as :class:`.SchemaType`. - - """ - - def _set_parent(self, parent): - """Associate with this SchemaEvent's parent object.""" - - raise NotImplementedError() - - def _set_parent_with_dispatch(self, parent): - self.dispatch.before_parent_attach(self, parent) - self._set_parent(parent) - self.dispatch.after_parent_attach(self, parent) - -class SchemaVisitor(ClauseVisitor): - """Define the visiting for ``SchemaItem`` objects.""" - - __traverse_options__ = {'schema_visitor': True} - -class ColumnCollection(util.OrderedProperties): - """An ordered dictionary that stores a list of ColumnElement - instances. - - Overrides the ``__eq__()`` method to produce SQL clauses between - sets of correlated columns. - - """ - - def __init__(self, *cols): - super(ColumnCollection, self).__init__() - self._data.update((c.key, c) for c in cols) - self.__dict__['_all_cols'] = util.column_set(self) - - def __str__(self): - return repr([str(c) for c in self]) - - def replace(self, column): - """add the given column to this collection, removing unaliased - versions of this column as well as existing columns with the - same key. - - e.g.:: - - t = Table('sometable', metadata, Column('col1', Integer)) - t.columns.replace(Column('col1', Integer, key='columnone')) - - will remove the original 'col1' from the collection, and add - the new column under the name 'columnname'. - - Used by schema.Column to override columns during table reflection. - - """ - if column.name in self and column.key != column.name: - other = self[column.name] - if other.name == other.key: - del self._data[other.name] - self._all_cols.remove(other) - if column.key in self._data: - self._all_cols.remove(self._data[column.key]) - self._all_cols.add(column) - self._data[column.key] = column - - def add(self, column): - """Add a column to this collection. - - The key attribute of the column will be used as the hash key - for this dictionary. - - """ - self[column.key] = column - - def __delitem__(self, key): - raise NotImplementedError() - - def __setattr__(self, key, object): - raise NotImplementedError() - - def __setitem__(self, key, value): - if key in self: - - # this warning is primarily to catch select() statements - # which have conflicting column names in their exported - # columns collection - - existing = self[key] - if not existing.shares_lineage(value): - util.warn('Column %r on table %r being replaced by ' - '%r, which has the same key. Consider ' - 'use_labels for select() statements.' % (key, - getattr(existing, 'table', None), value)) - self._all_cols.remove(existing) - # pop out memoized proxy_set as this - # operation may very well be occurring - # in a _make_proxy operation - util.memoized_property.reset(value, "proxy_set") - self._all_cols.add(value) - self._data[key] = value - - def clear(self): - self._data.clear() - self._all_cols.clear() - - def remove(self, column): - del self._data[column.key] - self._all_cols.remove(column) - - def update(self, value): - self._data.update(value) - self._all_cols.clear() - self._all_cols.update(self._data.values()) - - def extend(self, iter): - self.update((c.key, c) for c in iter) - - __hash__ = None - - @util.dependencies("sqlalchemy.sql.elements") - def __eq__(self, elements, other): - l = [] - for c in other: - for local in self: - if c.shares_lineage(local): - l.append(c == local) - return elements.and_(*l) - - def __contains__(self, other): - if not isinstance(other, util.string_types): - raise exc.ArgumentError("__contains__ requires a string argument") - return util.OrderedProperties.__contains__(self, other) - - def __setstate__(self, state): - self.__dict__['_data'] = state['_data'] - self.__dict__['_all_cols'] = util.column_set(self._data.values()) - - def contains_column(self, col): - # this has to be done via set() membership - return col in self._all_cols - - def as_immutable(self): - return ImmutableColumnCollection(self._data, self._all_cols) - - -class ImmutableColumnCollection(util.ImmutableProperties, ColumnCollection): - def __init__(self, data, colset): - util.ImmutableProperties.__init__(self, data) - self.__dict__['_all_cols'] = colset - - extend = remove = util.ImmutableProperties._immutable - - -class ColumnSet(util.ordered_column_set): - def contains_column(self, col): - return col in self - - def extend(self, cols): - for col in cols: - self.add(col) - - def __add__(self, other): - return list(self) + list(other) - - @util.dependencies("sqlalchemy.sql.elements") - def __eq__(self, elements, other): - l = [] - for c in other: - for local in self: - if c.shares_lineage(local): - l.append(c == local) - return elements.and_(*l) - - def __hash__(self): - return hash(tuple(x for x in self)) - -def _bind_or_error(schemaitem, msg=None): - bind = schemaitem.bind - if not bind: - name = schemaitem.__class__.__name__ - label = getattr(schemaitem, 'fullname', - getattr(schemaitem, 'name', None)) - if label: - item = '%s object %r' % (name, label) - else: - item = '%s object' % name - if msg is None: - msg = "%s is not bound to an Engine or Connection. "\ - "Execution can not proceed without a database to execute "\ - "against." % item - raise exc.UnboundExecutionError(msg) - return bind diff --git a/libs/sqlalchemy/sql/compiler.py b/libs/sqlalchemy/sql/compiler.py deleted file mode 100644 index ade3c623..00000000 --- a/libs/sqlalchemy/sql/compiler.py +++ /dev/null @@ -1,2897 +0,0 @@ -# sql/compiler.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Base SQL and DDL compiler implementations. - -Classes provided include: - -:class:`.compiler.SQLCompiler` - renders SQL -strings - -:class:`.compiler.DDLCompiler` - renders DDL -(data definition language) strings - -:class:`.compiler.GenericTypeCompiler` - renders -type specification strings. - -To generate user-defined SQL strings, see -:doc:`/ext/compiler`. - -""" - -import re -from . import schema, sqltypes, operators, functions, \ - util as sql_util, visitors, elements, selectable, base -from .. import util, exc -import decimal -import itertools - -RESERVED_WORDS = set([ - 'all', 'analyse', 'analyze', 'and', 'any', 'array', - 'as', 'asc', 'asymmetric', 'authorization', 'between', - 'binary', 'both', 'case', 'cast', 'check', 'collate', - 'column', 'constraint', 'create', 'cross', 'current_date', - 'current_role', 'current_time', 'current_timestamp', - 'current_user', 'default', 'deferrable', 'desc', - 'distinct', 'do', 'else', 'end', 'except', 'false', - 'for', 'foreign', 'freeze', 'from', 'full', 'grant', - 'group', 'having', 'ilike', 'in', 'initially', 'inner', - 'intersect', 'into', 'is', 'isnull', 'join', 'leading', - 'left', 'like', 'limit', 'localtime', 'localtimestamp', - 'natural', 'new', 'not', 'notnull', 'null', 'off', 'offset', - 'old', 'on', 'only', 'or', 'order', 'outer', 'overlaps', - 'placing', 'primary', 'references', 'right', 'select', - 'session_user', 'set', 'similar', 'some', 'symmetric', 'table', - 'then', 'to', 'trailing', 'true', 'union', 'unique', 'user', - 'using', 'verbose', 'when', 'where']) - -LEGAL_CHARACTERS = re.compile(r'^[A-Z0-9_$]+$', re.I) -ILLEGAL_INITIAL_CHARACTERS = set([str(x) for x in range(0, 10)]).union(['$']) - -BIND_PARAMS = re.compile(r'(? ', - operators.ge: ' >= ', - operators.eq: ' = ', - operators.concat_op: ' || ', - operators.between_op: ' BETWEEN ', - operators.match_op: ' MATCH ', - operators.in_op: ' IN ', - operators.notin_op: ' NOT IN ', - operators.comma_op: ', ', - operators.from_: ' FROM ', - operators.as_: ' AS ', - operators.is_: ' IS ', - operators.isnot: ' IS NOT ', - operators.collate: ' COLLATE ', - - # unary - operators.exists: 'EXISTS ', - operators.distinct_op: 'DISTINCT ', - operators.inv: 'NOT ', - - # modifiers - operators.desc_op: ' DESC', - operators.asc_op: ' ASC', - operators.nullsfirst_op: ' NULLS FIRST', - operators.nullslast_op: ' NULLS LAST', - -} - -FUNCTIONS = { - functions.coalesce: 'coalesce%(expr)s', - functions.current_date: 'CURRENT_DATE', - functions.current_time: 'CURRENT_TIME', - functions.current_timestamp: 'CURRENT_TIMESTAMP', - functions.current_user: 'CURRENT_USER', - functions.localtime: 'LOCALTIME', - functions.localtimestamp: 'LOCALTIMESTAMP', - functions.random: 'random%(expr)s', - functions.sysdate: 'sysdate', - functions.session_user: 'SESSION_USER', - functions.user: 'USER' -} - -EXTRACT_MAP = { - 'month': 'month', - 'day': 'day', - 'year': 'year', - 'second': 'second', - 'hour': 'hour', - 'doy': 'doy', - 'minute': 'minute', - 'quarter': 'quarter', - 'dow': 'dow', - 'week': 'week', - 'epoch': 'epoch', - 'milliseconds': 'milliseconds', - 'microseconds': 'microseconds', - 'timezone_hour': 'timezone_hour', - 'timezone_minute': 'timezone_minute' -} - -COMPOUND_KEYWORDS = { - selectable.CompoundSelect.UNION: 'UNION', - selectable.CompoundSelect.UNION_ALL: 'UNION ALL', - selectable.CompoundSelect.EXCEPT: 'EXCEPT', - selectable.CompoundSelect.EXCEPT_ALL: 'EXCEPT ALL', - selectable.CompoundSelect.INTERSECT: 'INTERSECT', - selectable.CompoundSelect.INTERSECT_ALL: 'INTERSECT ALL' -} - -class Compiled(object): - """Represent a compiled SQL or DDL expression. - - The ``__str__`` method of the ``Compiled`` object should produce - the actual text of the statement. ``Compiled`` objects are - specific to their underlying database dialect, and also may - or may not be specific to the columns referenced within a - particular set of bind parameters. In no case should the - ``Compiled`` object be dependent on the actual values of those - bind parameters, even though it may reference those values as - defaults. - """ - - def __init__(self, dialect, statement, bind=None, - compile_kwargs=util.immutabledict()): - """Construct a new ``Compiled`` object. - - :param dialect: ``Dialect`` to compile against. - - :param statement: ``ClauseElement`` to be compiled. - - :param bind: Optional Engine or Connection to compile this - statement against. - - :param compile_kwargs: additional kwargs that will be - passed to the initial call to :meth:`.Compiled.process`. - - .. versionadded:: 0.8 - - """ - - self.dialect = dialect - self.bind = bind - if statement is not None: - self.statement = statement - self.can_execute = statement.supports_execution - self.string = self.process(self.statement, **compile_kwargs) - - @util.deprecated("0.7", ":class:`.Compiled` objects now compile " - "within the constructor.") - def compile(self): - """Produce the internal string representation of this element. - """ - pass - - def _execute_on_connection(self, connection, multiparams, params): - return connection._execute_compiled(self, multiparams, params) - - @property - def sql_compiler(self): - """Return a Compiled that is capable of processing SQL expressions. - - If this compiler is one, it would likely just return 'self'. - - """ - - raise NotImplementedError() - - def process(self, obj, **kwargs): - return obj._compiler_dispatch(self, **kwargs) - - def __str__(self): - """Return the string text of the generated SQL or DDL.""" - - return self.string or '' - - def construct_params(self, params=None): - """Return the bind params for this compiled object. - - :param params: a dict of string/object pairs whose values will - override bind values compiled in to the - statement. - """ - - raise NotImplementedError() - - @property - def params(self): - """Return the bind params for this compiled object.""" - return self.construct_params() - - def execute(self, *multiparams, **params): - """Execute this compiled object.""" - - e = self.bind - if e is None: - raise exc.UnboundExecutionError( - "This Compiled object is not bound to any Engine " - "or Connection.") - return e._execute_compiled(self, multiparams, params) - - def scalar(self, *multiparams, **params): - """Execute this compiled object and return the result's - scalar value.""" - - return self.execute(*multiparams, **params).scalar() - - -class TypeCompiler(object): - """Produces DDL specification for TypeEngine objects.""" - - def __init__(self, dialect): - self.dialect = dialect - - def process(self, type_): - return type_._compiler_dispatch(self) - - - -class _CompileLabel(visitors.Visitable): - """lightweight label object which acts as an expression.Label.""" - - __visit_name__ = 'label' - __slots__ = 'element', 'name' - - def __init__(self, col, name, alt_names=()): - self.element = col - self.name = name - self._alt_names = (col,) + alt_names - - @property - def proxy_set(self): - return self.element.proxy_set - - @property - def type(self): - return self.element.type - - -class SQLCompiler(Compiled): - """Default implementation of Compiled. - - Compiles ClauseElements into SQL strings. Uses a similar visit - paradigm as visitors.ClauseVisitor but implements its own traversal. - - """ - - extract_map = EXTRACT_MAP - - compound_keywords = COMPOUND_KEYWORDS - - isdelete = isinsert = isupdate = False - """class-level defaults which can be set at the instance - level to define if this Compiled instance represents - INSERT/UPDATE/DELETE - """ - - returning = None - """holds the "returning" collection of columns if - the statement is CRUD and defines returning columns - either implicitly or explicitly - """ - - returning_precedes_values = False - """set to True classwide to generate RETURNING - clauses before the VALUES or WHERE clause (i.e. MSSQL) - """ - - render_table_with_column_in_update_from = False - """set to True classwide to indicate the SET clause - in a multi-table UPDATE statement should qualify - columns with the table name (i.e. MySQL only) - """ - - ansi_bind_rules = False - """SQL 92 doesn't allow bind parameters to be used - in the columns clause of a SELECT, nor does it allow - ambiguous expressions like "? = ?". A compiler - subclass can set this flag to False if the target - driver/DB enforces this - """ - - def __init__(self, dialect, statement, column_keys=None, - inline=False, **kwargs): - """Construct a new ``DefaultCompiler`` object. - - dialect - Dialect to be used - - statement - ClauseElement to be compiled - - column_keys - a list of column names to be compiled into an INSERT or UPDATE - statement. - - """ - self.column_keys = column_keys - - # compile INSERT/UPDATE defaults/sequences inlined (no pre- - # execute) - self.inline = inline or getattr(statement, 'inline', False) - - # a dictionary of bind parameter keys to BindParameter - # instances. - self.binds = {} - - # a dictionary of BindParameter instances to "compiled" names - # that are actually present in the generated SQL - self.bind_names = util.column_dict() - - # stack which keeps track of nested SELECT statements - self.stack = [] - - # relates label names in the final SQL to a tuple of local - # column/label name, ColumnElement object (if any) and - # TypeEngine. ResultProxy uses this for type processing and - # column targeting - self.result_map = {} - - # true if the paramstyle is positional - self.positional = dialect.positional - if self.positional: - self.positiontup = [] - self.bindtemplate = BIND_TEMPLATES[dialect.paramstyle] - - self.ctes = None - - # an IdentifierPreparer that formats the quoting of identifiers - self.preparer = dialect.identifier_preparer - self.label_length = dialect.label_length \ - or dialect.max_identifier_length - - # a map which tracks "anonymous" identifiers that are created on - # the fly here - self.anon_map = util.PopulateDict(self._process_anon) - - # a map which tracks "truncated" names based on - # dialect.label_length or dialect.max_identifier_length - self.truncated_names = {} - Compiled.__init__(self, dialect, statement, **kwargs) - - if self.positional and dialect.paramstyle == 'numeric': - self._apply_numbered_params() - - @util.memoized_instancemethod - def _init_cte_state(self): - """Initialize collections related to CTEs only if - a CTE is located, to save on the overhead of - these collections otherwise. - - """ - # collect CTEs to tack on top of a SELECT - self.ctes = util.OrderedDict() - self.ctes_by_name = {} - self.ctes_recursive = False - if self.positional: - self.cte_positional = [] - - def _apply_numbered_params(self): - poscount = itertools.count(1) - self.string = re.sub( - r'\[_POSITION\]', - lambda m: str(util.next(poscount)), - self.string) - - @util.memoized_property - def _bind_processors(self): - return dict( - (key, value) for key, value in - ((self.bind_names[bindparam], - bindparam.type._cached_bind_processor(self.dialect)) - for bindparam in self.bind_names) - if value is not None - ) - - def is_subquery(self): - return len(self.stack) > 1 - - @property - def sql_compiler(self): - return self - - def construct_params(self, params=None, _group_number=None, _check=True): - """return a dictionary of bind parameter keys and values""" - - if params: - pd = {} - for bindparam, name in self.bind_names.items(): - if bindparam.key in params: - pd[name] = params[bindparam.key] - elif name in params: - pd[name] = params[name] - elif _check and bindparam.required: - if _group_number: - raise exc.InvalidRequestError( - "A value is required for bind parameter %r, " - "in parameter group %d" % - (bindparam.key, _group_number)) - else: - raise exc.InvalidRequestError( - "A value is required for bind parameter %r" - % bindparam.key) - else: - pd[name] = bindparam.effective_value - return pd - else: - pd = {} - for bindparam in self.bind_names: - if _check and bindparam.required: - if _group_number: - raise exc.InvalidRequestError( - "A value is required for bind parameter %r, " - "in parameter group %d" % - (bindparam.key, _group_number)) - else: - raise exc.InvalidRequestError( - "A value is required for bind parameter %r" - % bindparam.key) - pd[self.bind_names[bindparam]] = bindparam.effective_value - return pd - - @property - def params(self): - """Return the bind param dictionary embedded into this - compiled object, for those values that are present.""" - return self.construct_params(_check=False) - - def default_from(self): - """Called when a SELECT statement has no froms, and no FROM clause is - to be appended. - - Gives Oracle a chance to tack on a ``FROM DUAL`` to the string output. - - """ - return "" - - def visit_grouping(self, grouping, asfrom=False, **kwargs): - return "(" + grouping.element._compiler_dispatch(self, **kwargs) + ")" - - def visit_label(self, label, - add_to_result_map=None, - within_label_clause=False, - within_columns_clause=False, - render_label_as_label=None, - **kw): - # only render labels within the columns clause - # or ORDER BY clause of a select. dialect-specific compilers - # can modify this behavior. - render_label_with_as = within_columns_clause and not within_label_clause - render_label_only = render_label_as_label is label - - if render_label_only or render_label_with_as: - if isinstance(label.name, elements._truncated_label): - labelname = self._truncated_identifier("colident", label.name) - else: - labelname = label.name - - if render_label_with_as: - if add_to_result_map is not None: - add_to_result_map( - labelname, - label.name, - (label, labelname, ) + label._alt_names, - label.type - ) - - return label.element._compiler_dispatch(self, - within_columns_clause=True, - within_label_clause=True, - **kw) + \ - OPERATORS[operators.as_] + \ - self.preparer.format_label(label, labelname) - elif render_label_only: - return labelname - else: - return label.element._compiler_dispatch(self, - within_columns_clause=False, - **kw) - - def visit_column(self, column, add_to_result_map=None, - include_table=True, **kwargs): - name = orig_name = column.name - if name is None: - raise exc.CompileError("Cannot compile Column object until " - "its 'name' is assigned.") - - is_literal = column.is_literal - if not is_literal and isinstance(name, elements._truncated_label): - name = self._truncated_identifier("colident", name) - - if add_to_result_map is not None: - add_to_result_map( - name, - orig_name, - (column, name, column.key), - column.type - ) - - if is_literal: - name = self.escape_literal_column(name) - else: - name = self.preparer.quote(name) - - table = column.table - if table is None or not include_table or not table.named_with_column: - return name - else: - if table.schema: - schema_prefix = self.preparer.quote_schema(table.schema) + '.' - else: - schema_prefix = '' - tablename = table.name - if isinstance(tablename, elements._truncated_label): - tablename = self._truncated_identifier("alias", tablename) - - return schema_prefix + \ - self.preparer.quote(tablename) + \ - "." + name - - def escape_literal_column(self, text): - """provide escaping for the literal_column() construct.""" - - # TODO: some dialects might need different behavior here - return text.replace('%', '%%') - - def visit_fromclause(self, fromclause, **kwargs): - return fromclause.name - - def visit_index(self, index, **kwargs): - return index.name - - def visit_typeclause(self, typeclause, **kwargs): - return self.dialect.type_compiler.process(typeclause.type) - - def post_process_text(self, text): - return text - - def visit_textclause(self, textclause, **kw): - def do_bindparam(m): - name = m.group(1) - if name in textclause._bindparams: - return self.process(textclause._bindparams[name], **kw) - else: - return self.bindparam_string(name, **kw) - - # un-escape any \:params - return BIND_PARAMS_ESC.sub(lambda m: m.group(1), - BIND_PARAMS.sub(do_bindparam, - self.post_process_text(textclause.text)) - ) - - def visit_text_as_from(self, taf, iswrapper=False, - compound_index=0, force_result_map=False, - asfrom=False, - parens=True, **kw): - - toplevel = not self.stack - entry = self._default_stack_entry if toplevel else self.stack[-1] - - populate_result_map = force_result_map or ( - compound_index == 0 and ( - toplevel or \ - entry['iswrapper'] - ) - ) - - if populate_result_map: - for c in taf.c: - self._add_to_result_map( - c.key, c.key, (c,), c.type - ) - - text = self.process(taf.element, **kw) - if asfrom and parens: - text = "(%s)" % text - return text - - - def visit_null(self, expr, **kw): - return 'NULL' - - def visit_true(self, expr, **kw): - if self.dialect.supports_native_boolean: - return 'true' - else: - return "1" - - def visit_false(self, expr, **kw): - if self.dialect.supports_native_boolean: - return 'false' - else: - return "0" - - def visit_clauselist(self, clauselist, order_by_select=None, **kw): - if order_by_select is not None: - return self._order_by_clauselist( - clauselist, order_by_select, **kw) - - sep = clauselist.operator - if sep is None: - sep = " " - else: - sep = OPERATORS[clauselist.operator] - return sep.join( - s for s in - ( - c._compiler_dispatch(self, **kw) - for c in clauselist.clauses) - if s) - - def _order_by_clauselist(self, clauselist, order_by_select, **kw): - # look through raw columns collection for labels. - # note that its OK we aren't expanding tables and other selectables - # here; we can only add a label in the ORDER BY for an individual - # label expression in the columns clause. - - raw_col = set(l._order_by_label_element.name - for l in order_by_select._raw_columns - if l._order_by_label_element is not None) - - return ", ".join( - s for s in - ( - c._compiler_dispatch(self, - render_label_as_label= - c._order_by_label_element if - c._order_by_label_element is not None and - c._order_by_label_element.name in raw_col - else None, - **kw) - for c in clauselist.clauses) - if s) - - def visit_case(self, clause, **kwargs): - x = "CASE " - if clause.value is not None: - x += clause.value._compiler_dispatch(self, **kwargs) + " " - for cond, result in clause.whens: - x += "WHEN " + cond._compiler_dispatch( - self, **kwargs - ) + " THEN " + result._compiler_dispatch( - self, **kwargs) + " " - if clause.else_ is not None: - x += "ELSE " + clause.else_._compiler_dispatch( - self, **kwargs - ) + " " - x += "END" - return x - - def visit_cast(self, cast, **kwargs): - return "CAST(%s AS %s)" % \ - (cast.clause._compiler_dispatch(self, **kwargs), - cast.typeclause._compiler_dispatch(self, **kwargs)) - - def visit_over(self, over, **kwargs): - return "%s OVER (%s)" % ( - over.func._compiler_dispatch(self, **kwargs), - ' '.join( - '%s BY %s' % (word, clause._compiler_dispatch(self, **kwargs)) - for word, clause in ( - ('PARTITION', over.partition_by), - ('ORDER', over.order_by) - ) - if clause is not None and len(clause) - ) - ) - - def visit_extract(self, extract, **kwargs): - field = self.extract_map.get(extract.field, extract.field) - return "EXTRACT(%s FROM %s)" % (field, - extract.expr._compiler_dispatch(self, **kwargs)) - - def visit_function(self, func, add_to_result_map=None, **kwargs): - if add_to_result_map is not None: - add_to_result_map( - func.name, func.name, (), func.type - ) - - disp = getattr(self, "visit_%s_func" % func.name.lower(), None) - if disp: - return disp(func, **kwargs) - else: - name = FUNCTIONS.get(func.__class__, func.name + "%(expr)s") - return ".".join(list(func.packagenames) + [name]) % \ - {'expr': self.function_argspec(func, **kwargs)} - - def visit_next_value_func(self, next_value, **kw): - return self.visit_sequence(next_value.sequence) - - def visit_sequence(self, sequence): - raise NotImplementedError( - "Dialect '%s' does not support sequence increments." % - self.dialect.name - ) - - def function_argspec(self, func, **kwargs): - return func.clause_expr._compiler_dispatch(self, **kwargs) - - - def visit_compound_select(self, cs, asfrom=False, - parens=True, compound_index=0, **kwargs): - toplevel = not self.stack - entry = self._default_stack_entry if toplevel else self.stack[-1] - - self.stack.append( - { - 'correlate_froms': entry['correlate_froms'], - 'iswrapper': toplevel, - 'asfrom_froms': entry['asfrom_froms'] - }) - - keyword = self.compound_keywords.get(cs.keyword) - - text = (" " + keyword + " ").join( - (c._compiler_dispatch(self, - asfrom=asfrom, parens=False, - compound_index=i, **kwargs) - for i, c in enumerate(cs.selects)) - ) - - group_by = cs._group_by_clause._compiler_dispatch( - self, asfrom=asfrom, **kwargs) - if group_by: - text += " GROUP BY " + group_by - - text += self.order_by_clause(cs, **kwargs) - text += (cs._limit is not None or cs._offset is not None) and \ - self.limit_clause(cs) or "" - - if self.ctes and \ - compound_index == 0 and toplevel: - text = self._render_cte_clause() + text - - self.stack.pop(-1) - if asfrom and parens: - return "(" + text + ")" - else: - return text - - def visit_unary(self, unary, **kw): - if unary.operator: - if unary.modifier: - raise exc.CompileError( - "Unary expression does not support operator " - "and modifier simultaneously") - disp = getattr(self, "visit_%s_unary_operator" % - unary.operator.__name__, None) - if disp: - return disp(unary, unary.operator, **kw) - else: - return self._generate_generic_unary_operator(unary, - OPERATORS[unary.operator], **kw) - elif unary.modifier: - disp = getattr(self, "visit_%s_unary_modifier" % - unary.modifier.__name__, None) - if disp: - return disp(unary, unary.modifier, **kw) - else: - return self._generate_generic_unary_modifier(unary, - OPERATORS[unary.modifier], **kw) - else: - raise exc.CompileError( - "Unary expression has no operator or modifier") - - def visit_istrue_unary_operator(self, element, operator, **kw): - if self.dialect.supports_native_boolean: - return self.process(element.element, **kw) - else: - return "%s = 1" % self.process(element.element, **kw) - - def visit_isfalse_unary_operator(self, element, operator, **kw): - if self.dialect.supports_native_boolean: - return "NOT %s" % self.process(element.element, **kw) - else: - return "%s = 0" % self.process(element.element, **kw) - - def visit_binary(self, binary, **kw): - # don't allow "? = ?" to render - if self.ansi_bind_rules and \ - isinstance(binary.left, elements.BindParameter) and \ - isinstance(binary.right, elements.BindParameter): - kw['literal_binds'] = True - - operator = binary.operator - disp = getattr(self, "visit_%s_binary" % operator.__name__, None) - if disp: - return disp(binary, operator, **kw) - else: - try: - opstring = OPERATORS[operator] - except KeyError: - raise exc.UnsupportedCompilationError(self, operator) - else: - return self._generate_generic_binary(binary, opstring, **kw) - - def visit_custom_op_binary(self, element, operator, **kw): - return self._generate_generic_binary(element, - " " + operator.opstring + " ", **kw) - - def visit_custom_op_unary_operator(self, element, operator, **kw): - return self._generate_generic_unary_operator(element, - operator.opstring + " ", **kw) - - def visit_custom_op_unary_modifier(self, element, operator, **kw): - return self._generate_generic_unary_modifier(element, - " " + operator.opstring, **kw) - - def _generate_generic_binary(self, binary, opstring, **kw): - return binary.left._compiler_dispatch(self, **kw) + \ - opstring + \ - binary.right._compiler_dispatch(self, **kw) - - def _generate_generic_unary_operator(self, unary, opstring, **kw): - return opstring + unary.element._compiler_dispatch(self, **kw) - - def _generate_generic_unary_modifier(self, unary, opstring, **kw): - return unary.element._compiler_dispatch(self, **kw) + opstring - - @util.memoized_property - def _like_percent_literal(self): - return elements.literal_column("'%'", type_=sqltypes.STRINGTYPE) - - def visit_contains_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__add__(binary.right).__add__(percent) - return self.visit_like_op_binary(binary, operator, **kw) - - def visit_notcontains_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__add__(binary.right).__add__(percent) - return self.visit_notlike_op_binary(binary, operator, **kw) - - def visit_startswith_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__radd__( - binary.right - ) - return self.visit_like_op_binary(binary, operator, **kw) - - def visit_notstartswith_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__radd__( - binary.right - ) - return self.visit_notlike_op_binary(binary, operator, **kw) - - def visit_endswith_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__add__(binary.right) - return self.visit_like_op_binary(binary, operator, **kw) - - def visit_notendswith_op_binary(self, binary, operator, **kw): - binary = binary._clone() - percent = self._like_percent_literal - binary.right = percent.__add__(binary.right) - return self.visit_notlike_op_binary(binary, operator, **kw) - - def visit_like_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - - # TODO: use ternary here, not "and"/ "or" - return '%s LIKE %s' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def visit_notlike_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - return '%s NOT LIKE %s' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def visit_ilike_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - return 'lower(%s) LIKE lower(%s)' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def visit_notilike_op_binary(self, binary, operator, **kw): - escape = binary.modifiers.get("escape", None) - return 'lower(%s) NOT LIKE lower(%s)' % ( - binary.left._compiler_dispatch(self, **kw), - binary.right._compiler_dispatch(self, **kw)) \ - + ( - ' ESCAPE ' + - self.render_literal_value(escape, sqltypes.STRINGTYPE) - if escape else '' - ) - - def visit_bindparam(self, bindparam, within_columns_clause=False, - literal_binds=False, - skip_bind_expression=False, - **kwargs): - - if not skip_bind_expression and bindparam.type._has_bind_expression: - bind_expression = bindparam.type.bind_expression(bindparam) - return self.process(bind_expression, - skip_bind_expression=True) - - if literal_binds or \ - (within_columns_clause and \ - self.ansi_bind_rules): - if bindparam.value is None: - raise exc.CompileError("Bind parameter '%s' without a " - "renderable value not allowed here." - % bindparam.key) - return self.render_literal_bindparam(bindparam, - within_columns_clause=True, **kwargs) - - name = self._truncate_bindparam(bindparam) - - if name in self.binds: - existing = self.binds[name] - if existing is not bindparam: - if (existing.unique or bindparam.unique) and \ - not existing.proxy_set.intersection( - bindparam.proxy_set): - raise exc.CompileError( - "Bind parameter '%s' conflicts with " - "unique bind parameter of the same name" % - bindparam.key - ) - elif existing._is_crud or bindparam._is_crud: - raise exc.CompileError( - "bindparam() name '%s' is reserved " - "for automatic usage in the VALUES or SET " - "clause of this " - "insert/update statement. Please use a " - "name other than column name when using bindparam() " - "with insert() or update() (for example, 'b_%s')." - % (bindparam.key, bindparam.key) - ) - - self.binds[bindparam.key] = self.binds[name] = bindparam - - return self.bindparam_string(name, **kwargs) - - def render_literal_bindparam(self, bindparam, **kw): - value = bindparam.value - return self.render_literal_value(value, bindparam.type) - - def render_literal_value(self, value, type_): - """Render the value of a bind parameter as a quoted literal. - - This is used for statement sections that do not accept bind parameters - on the target driver/database. - - This should be implemented by subclasses using the quoting services - of the DBAPI. - - """ - - processor = type_._cached_literal_processor(self.dialect) - if processor: - return processor(value) - else: - raise NotImplementedError( - "Don't know how to literal-quote value %r" % value) - - def _truncate_bindparam(self, bindparam): - if bindparam in self.bind_names: - return self.bind_names[bindparam] - - bind_name = bindparam.key - if isinstance(bind_name, elements._truncated_label): - bind_name = self._truncated_identifier("bindparam", bind_name) - - # add to bind_names for translation - self.bind_names[bindparam] = bind_name - - return bind_name - - def _truncated_identifier(self, ident_class, name): - if (ident_class, name) in self.truncated_names: - return self.truncated_names[(ident_class, name)] - - anonname = name.apply_map(self.anon_map) - - if len(anonname) > self.label_length: - counter = self.truncated_names.get(ident_class, 1) - truncname = anonname[0:max(self.label_length - 6, 0)] + \ - "_" + hex(counter)[2:] - self.truncated_names[ident_class] = counter + 1 - else: - truncname = anonname - self.truncated_names[(ident_class, name)] = truncname - return truncname - - def _anonymize(self, name): - return name % self.anon_map - - def _process_anon(self, key): - (ident, derived) = key.split(' ', 1) - anonymous_counter = self.anon_map.get(derived, 1) - self.anon_map[derived] = anonymous_counter + 1 - return derived + "_" + str(anonymous_counter) - - def bindparam_string(self, name, positional_names=None, **kw): - if self.positional: - if positional_names is not None: - positional_names.append(name) - else: - self.positiontup.append(name) - return self.bindtemplate % {'name': name} - - def visit_cte(self, cte, asfrom=False, ashint=False, - fromhints=None, - **kwargs): - self._init_cte_state() - if self.positional: - kwargs['positional_names'] = self.cte_positional - - if isinstance(cte.name, elements._truncated_label): - cte_name = self._truncated_identifier("alias", cte.name) - else: - cte_name = cte.name - - if cte_name in self.ctes_by_name: - existing_cte = self.ctes_by_name[cte_name] - # we've generated a same-named CTE that we are enclosed in, - # or this is the same CTE. just return the name. - if cte in existing_cte._restates or cte is existing_cte: - return self.preparer.format_alias(cte, cte_name) - elif existing_cte in cte._restates: - # we've generated a same-named CTE that is - # enclosed in us - we take precedence, so - # discard the text for the "inner". - del self.ctes[existing_cte] - else: - raise exc.CompileError( - "Multiple, unrelated CTEs found with " - "the same name: %r" % - cte_name) - - self.ctes_by_name[cte_name] = cte - - if cte._cte_alias is not None: - orig_cte = cte._cte_alias - if orig_cte not in self.ctes: - self.visit_cte(orig_cte) - cte_alias_name = cte._cte_alias.name - if isinstance(cte_alias_name, elements._truncated_label): - cte_alias_name = self._truncated_identifier("alias", cte_alias_name) - else: - orig_cte = cte - cte_alias_name = None - if not cte_alias_name and cte not in self.ctes: - if cte.recursive: - self.ctes_recursive = True - text = self.preparer.format_alias(cte, cte_name) - if cte.recursive: - if isinstance(cte.original, selectable.Select): - col_source = cte.original - elif isinstance(cte.original, selectable.CompoundSelect): - col_source = cte.original.selects[0] - else: - assert False - recur_cols = [c for c in - util.unique_list(col_source.inner_columns) - if c is not None] - - text += "(%s)" % (", ".join( - self.preparer.format_column(ident) - for ident in recur_cols)) - text += " AS \n" + \ - cte.original._compiler_dispatch( - self, asfrom=True, **kwargs - ) - self.ctes[cte] = text - - if asfrom: - if cte_alias_name: - text = self.preparer.format_alias(cte, cte_alias_name) - text += " AS " + cte_name - else: - return self.preparer.format_alias(cte, cte_name) - return text - - def visit_alias(self, alias, asfrom=False, ashint=False, - iscrud=False, - fromhints=None, **kwargs): - if asfrom or ashint: - if isinstance(alias.name, elements._truncated_label): - alias_name = self._truncated_identifier("alias", alias.name) - else: - alias_name = alias.name - - if ashint: - return self.preparer.format_alias(alias, alias_name) - elif asfrom: - ret = alias.original._compiler_dispatch(self, - asfrom=True, **kwargs) + \ - " AS " + \ - self.preparer.format_alias(alias, alias_name) - - if fromhints and alias in fromhints: - ret = self.format_from_hint_text(ret, alias, - fromhints[alias], iscrud) - - return ret - else: - return alias.original._compiler_dispatch(self, **kwargs) - - def _add_to_result_map(self, keyname, name, objects, type_): - if not self.dialect.case_sensitive: - keyname = keyname.lower() - - if keyname in self.result_map: - # conflicting keyname, just double up the list - # of objects. this will cause an "ambiguous name" - # error if an attempt is made by the result set to - # access. - e_name, e_obj, e_type = self.result_map[keyname] - self.result_map[keyname] = e_name, e_obj + objects, e_type - else: - self.result_map[keyname] = name, objects, type_ - - def _label_select_column(self, select, column, - populate_result_map, - asfrom, column_clause_args, - name=None, - within_columns_clause=True): - """produce labeled columns present in a select().""" - - if column.type._has_column_expression and \ - populate_result_map: - col_expr = column.type.column_expression(column) - add_to_result_map = lambda keyname, name, objects, type_: \ - self._add_to_result_map( - keyname, name, - objects + (column,), type_) - else: - col_expr = column - if populate_result_map: - add_to_result_map = self._add_to_result_map - else: - add_to_result_map = None - - if not within_columns_clause: - result_expr = col_expr - elif isinstance(column, elements.Label): - if col_expr is not column: - result_expr = _CompileLabel( - col_expr, - column.name, - alt_names=(column.element,) - ) - else: - result_expr = col_expr - - elif select is not None and name: - result_expr = _CompileLabel( - col_expr, - name, - alt_names=(column._key_label,) - ) - - elif \ - asfrom and \ - isinstance(column, elements.ColumnClause) and \ - not column.is_literal and \ - column.table is not None and \ - not isinstance(column.table, selectable.Select): - result_expr = _CompileLabel(col_expr, - elements._as_truncated(column.name), - alt_names=(column.key,)) - elif not isinstance(column, - (elements.UnaryExpression, elements.TextClause)) \ - and (not hasattr(column, 'name') or \ - isinstance(column, functions.Function)): - result_expr = _CompileLabel(col_expr, column.anon_label) - elif col_expr is not column: - # TODO: are we sure "column" has a .name and .key here ? - # assert isinstance(column, elements.ColumnClause) - result_expr = _CompileLabel(col_expr, - elements._as_truncated(column.name), - alt_names=(column.key,)) - else: - result_expr = col_expr - - column_clause_args.update( - within_columns_clause=within_columns_clause, - add_to_result_map=add_to_result_map - ) - return result_expr._compiler_dispatch( - self, - **column_clause_args - ) - - def format_from_hint_text(self, sqltext, table, hint, iscrud): - hinttext = self.get_from_hint_text(table, hint) - if hinttext: - sqltext += " " + hinttext - return sqltext - - def get_select_hint_text(self, byfroms): - return None - - def get_from_hint_text(self, table, text): - return None - - def get_crud_hint_text(self, table, text): - return None - - def _transform_select_for_nested_joins(self, select): - """Rewrite any "a JOIN (b JOIN c)" expression as - "a JOIN (select * from b JOIN c) AS anon", to support - databases that can't parse a parenthesized join correctly - (i.e. sqlite the main one). - - """ - cloned = {} - column_translate = [{}] - - # TODO: should we be using isinstance() for this, - # as this whole system won't work for custom Join/Select - # subclasses where compilation routines - # call down to compiler.visit_join(), compiler.visit_select() - join_name = selectable.Join.__visit_name__ - select_name = selectable.Select.__visit_name__ - - def visit(element, **kw): - if element in column_translate[-1]: - return column_translate[-1][element] - - elif element in cloned: - return cloned[element] - - newelem = cloned[element] = element._clone() - - if newelem.__visit_name__ is join_name and \ - isinstance(newelem.right, selectable.FromGrouping): - - newelem._reset_exported() - newelem.left = visit(newelem.left, **kw) - - right = visit(newelem.right, **kw) - - selectable_ = selectable.Select( - [right.element], - use_labels=True).alias() - - for c in selectable_.c: - c._key_label = c.key - c._label = c.name - - translate_dict = dict( - zip(newelem.right.element.c, selectable_.c) - ) - - translate_dict[right.element.left] = selectable_ - translate_dict[right.element.right] = selectable_ - - # propagate translations that we've gained - # from nested visit(newelem.right) outwards - # to the enclosing select here. this happens - # only when we have more than one level of right - # join nesting, i.e. "a JOIN (b JOIN (c JOIN d))" - for k, v in list(column_translate[-1].items()): - if v in translate_dict: - # remarkably, no current ORM tests (May 2013) - # hit this condition, only test_join_rewriting - # does. - column_translate[-1][k] = translate_dict[v] - - column_translate[-1].update(translate_dict) - - newelem.right = selectable_ - - newelem.onclause = visit(newelem.onclause, **kw) - elif newelem.__visit_name__ is select_name: - column_translate.append({}) - newelem._copy_internals(clone=visit, **kw) - del column_translate[-1] - else: - newelem._copy_internals(clone=visit, **kw) - - return newelem - - return visit(select) - - def _transform_result_map_for_nested_joins(self, select, transformed_select): - inner_col = dict((c._key_label, c) for - c in transformed_select.inner_columns) - - d = dict( - (inner_col[c._key_label], c) - for c in select.inner_columns - ) - for key, (name, objs, typ) in list(self.result_map.items()): - objs = tuple([d.get(col, col) for col in objs]) - self.result_map[key] = (name, objs, typ) - - - _default_stack_entry = util.immutabledict([ - ('iswrapper', False), - ('correlate_froms', frozenset()), - ('asfrom_froms', frozenset()) - ]) - - def _display_froms_for_select(self, select, asfrom): - # utility method to help external dialects - # get the correct from list for a select. - # specifically the oracle dialect needs this feature - # right now. - toplevel = not self.stack - entry = self._default_stack_entry if toplevel else self.stack[-1] - - correlate_froms = entry['correlate_froms'] - asfrom_froms = entry['asfrom_froms'] - - if asfrom: - froms = select._get_display_froms( - explicit_correlate_froms=\ - correlate_froms.difference(asfrom_froms), - implicit_correlate_froms=()) - else: - froms = select._get_display_froms( - explicit_correlate_froms=correlate_froms, - implicit_correlate_froms=asfrom_froms) - return froms - - def visit_select(self, select, asfrom=False, parens=True, - iswrapper=False, fromhints=None, - compound_index=0, - force_result_map=False, - positional_names=None, - nested_join_translation=False, - **kwargs): - - needs_nested_translation = \ - select.use_labels and \ - not nested_join_translation and \ - not self.stack and \ - not self.dialect.supports_right_nested_joins - - if needs_nested_translation: - transformed_select = self._transform_select_for_nested_joins(select) - text = self.visit_select( - transformed_select, asfrom=asfrom, parens=parens, - iswrapper=iswrapper, fromhints=fromhints, - compound_index=compound_index, - force_result_map=force_result_map, - positional_names=positional_names, - nested_join_translation=True, **kwargs - ) - - toplevel = not self.stack - entry = self._default_stack_entry if toplevel else self.stack[-1] - - - populate_result_map = force_result_map or ( - compound_index == 0 and ( - toplevel or \ - entry['iswrapper'] - ) - ) - - if needs_nested_translation: - if populate_result_map: - self._transform_result_map_for_nested_joins( - select, transformed_select) - return text - - correlate_froms = entry['correlate_froms'] - asfrom_froms = entry['asfrom_froms'] - - if asfrom: - froms = select._get_display_froms( - explicit_correlate_froms= - correlate_froms.difference(asfrom_froms), - implicit_correlate_froms=()) - else: - froms = select._get_display_froms( - explicit_correlate_froms=correlate_froms, - implicit_correlate_froms=asfrom_froms) - - new_correlate_froms = set(selectable._from_objects(*froms)) - all_correlate_froms = new_correlate_froms.union(correlate_froms) - - new_entry = { - 'asfrom_froms': new_correlate_froms, - 'iswrapper': iswrapper, - 'correlate_froms': all_correlate_froms - } - self.stack.append(new_entry) - - column_clause_args = kwargs.copy() - column_clause_args.update({ - 'positional_names': positional_names, - 'within_label_clause': False, - 'within_columns_clause': False - }) - - # the actual list of columns to print in the SELECT column list. - inner_columns = [ - c for c in [ - self._label_select_column(select, - column, - populate_result_map, asfrom, - column_clause_args, - name=name) - for name, column in select._columns_plus_names - ] - if c is not None - ] - - text = "SELECT " # we're off to a good start ! - - if select._hints: - byfrom = dict([ - (from_, hinttext % { - 'name':from_._compiler_dispatch( - self, ashint=True) - }) - for (from_, dialect), hinttext in - select._hints.items() - if dialect in ('*', self.dialect.name) - ]) - hint_text = self.get_select_hint_text(byfrom) - if hint_text: - text += hint_text + " " - - if select._prefixes: - text += self._generate_prefixes(select, select._prefixes, **kwargs) - - text += self.get_select_precolumns(select) - text += ', '.join(inner_columns) - - if froms: - text += " \nFROM " - - if select._hints: - text += ', '.join([f._compiler_dispatch(self, - asfrom=True, fromhints=byfrom, - **kwargs) - for f in froms]) - else: - text += ', '.join([f._compiler_dispatch(self, - asfrom=True, **kwargs) - for f in froms]) - else: - text += self.default_from() - - if select._whereclause is not None: - t = select._whereclause._compiler_dispatch(self, **kwargs) - if t: - text += " \nWHERE " + t - - if select._group_by_clause.clauses: - group_by = select._group_by_clause._compiler_dispatch( - self, **kwargs) - if group_by: - text += " GROUP BY " + group_by - - if select._having is not None: - t = select._having._compiler_dispatch(self, **kwargs) - if t: - text += " \nHAVING " + t - - if select._order_by_clause.clauses: - if self.dialect.supports_simple_order_by_label: - order_by_select = select - else: - order_by_select = None - - text += self.order_by_clause(select, - order_by_select=order_by_select, **kwargs) - - if select._limit is not None or select._offset is not None: - text += self.limit_clause(select) - - if select._for_update_arg is not None: - text += self.for_update_clause(select) - - if self.ctes and \ - compound_index == 0 and toplevel: - text = self._render_cte_clause() + text - - self.stack.pop(-1) - - if asfrom and parens: - return "(" + text + ")" - else: - return text - - def _generate_prefixes(self, stmt, prefixes, **kw): - clause = " ".join( - prefix._compiler_dispatch(self, **kw) - for prefix, dialect_name in prefixes - if dialect_name is None or - dialect_name == self.dialect.name - ) - if clause: - clause += " " - return clause - - def _render_cte_clause(self): - if self.positional: - self.positiontup = self.cte_positional + self.positiontup - cte_text = self.get_cte_preamble(self.ctes_recursive) + " " - cte_text += ", \n".join( - [txt for txt in self.ctes.values()] - ) - cte_text += "\n " - return cte_text - - def get_cte_preamble(self, recursive): - if recursive: - return "WITH RECURSIVE" - else: - return "WITH" - - def get_select_precolumns(self, select): - """Called when building a ``SELECT`` statement, position is just - before column list. - - """ - return select._distinct and "DISTINCT " or "" - - def order_by_clause(self, select, **kw): - order_by = select._order_by_clause._compiler_dispatch(self, **kw) - if order_by: - return " ORDER BY " + order_by - else: - return "" - - def for_update_clause(self, select): - return " FOR UPDATE" - - def returning_clause(self, stmt, returning_cols): - raise exc.CompileError( - "RETURNING is not supported by this " - "dialect's statement compiler.") - - def limit_clause(self, select): - text = "" - if select._limit is not None: - text += "\n LIMIT " + self.process(elements.literal(select._limit)) - if select._offset is not None: - if select._limit is None: - text += "\n LIMIT -1" - text += " OFFSET " + self.process(elements.literal(select._offset)) - return text - - def visit_table(self, table, asfrom=False, iscrud=False, ashint=False, - fromhints=None, **kwargs): - if asfrom or ashint: - if getattr(table, "schema", None): - ret = self.preparer.quote_schema(table.schema) + \ - "." + self.preparer.quote(table.name) - else: - ret = self.preparer.quote(table.name) - if fromhints and table in fromhints: - ret = self.format_from_hint_text(ret, table, - fromhints[table], iscrud) - return ret - else: - return "" - - def visit_join(self, join, asfrom=False, **kwargs): - return ( - join.left._compiler_dispatch(self, asfrom=True, **kwargs) + - (join.isouter and " LEFT OUTER JOIN " or " JOIN ") + - join.right._compiler_dispatch(self, asfrom=True, **kwargs) + - " ON " + - join.onclause._compiler_dispatch(self, **kwargs) - ) - - def visit_insert(self, insert_stmt, **kw): - self.isinsert = True - colparams = self._get_colparams(insert_stmt, **kw) - - if not colparams and \ - not self.dialect.supports_default_values and \ - not self.dialect.supports_empty_insert: - raise exc.CompileError("The '%s' dialect with current database " - "version settings does not support empty " - "inserts." % - self.dialect.name) - - if insert_stmt._has_multi_parameters: - if not self.dialect.supports_multivalues_insert: - raise exc.CompileError("The '%s' dialect with current database " - "version settings does not support " - "in-place multirow inserts." % - self.dialect.name) - colparams_single = colparams[0] - else: - colparams_single = colparams - - - preparer = self.preparer - supports_default_values = self.dialect.supports_default_values - - text = "INSERT " - - if insert_stmt._prefixes: - text += self._generate_prefixes(insert_stmt, - insert_stmt._prefixes, **kw) - - text += "INTO " - table_text = preparer.format_table(insert_stmt.table) - - if insert_stmt._hints: - dialect_hints = dict([ - (table, hint_text) - for (table, dialect), hint_text in - insert_stmt._hints.items() - if dialect in ('*', self.dialect.name) - ]) - if insert_stmt.table in dialect_hints: - table_text = self.format_from_hint_text( - table_text, - insert_stmt.table, - dialect_hints[insert_stmt.table], - True - ) - - text += table_text - - if colparams_single or not supports_default_values: - text += " (%s)" % ', '.join([preparer.format_column(c[0]) - for c in colparams_single]) - - if self.returning or insert_stmt._returning: - self.returning = self.returning or insert_stmt._returning - returning_clause = self.returning_clause( - insert_stmt, self.returning) - - if self.returning_precedes_values: - text += " " + returning_clause - - if insert_stmt.select is not None: - text += " %s" % self.process(insert_stmt.select, **kw) - elif not colparams and supports_default_values: - text += " DEFAULT VALUES" - elif insert_stmt._has_multi_parameters: - text += " VALUES %s" % ( - ", ".join( - "(%s)" % ( - ', '.join(c[1] for c in colparam_set) - ) - for colparam_set in colparams - ) - ) - else: - text += " VALUES (%s)" % \ - ', '.join([c[1] for c in colparams]) - - if self.returning and not self.returning_precedes_values: - text += " " + returning_clause - - return text - - def update_limit_clause(self, update_stmt): - """Provide a hook for MySQL to add LIMIT to the UPDATE""" - return None - - def update_tables_clause(self, update_stmt, from_table, - extra_froms, **kw): - """Provide a hook to override the initial table clause - in an UPDATE statement. - - MySQL overrides this. - - """ - return from_table._compiler_dispatch(self, asfrom=True, - iscrud=True, **kw) - - def update_from_clause(self, update_stmt, - from_table, extra_froms, - from_hints, - **kw): - """Provide a hook to override the generation of an - UPDATE..FROM clause. - - MySQL and MSSQL override this. - - """ - return "FROM " + ', '.join( - t._compiler_dispatch(self, asfrom=True, - fromhints=from_hints, **kw) - for t in extra_froms) - - def visit_update(self, update_stmt, **kw): - self.stack.append( - {'correlate_froms': set([update_stmt.table]), - "iswrapper": False, - "asfrom_froms": set([update_stmt.table])}) - - self.isupdate = True - - extra_froms = update_stmt._extra_froms - - text = "UPDATE " - - if update_stmt._prefixes: - text += self._generate_prefixes(update_stmt, - update_stmt._prefixes, **kw) - - table_text = self.update_tables_clause(update_stmt, update_stmt.table, - extra_froms, **kw) - - colparams = self._get_colparams(update_stmt, extra_froms, **kw) - - if update_stmt._hints: - dialect_hints = dict([ - (table, hint_text) - for (table, dialect), hint_text in - update_stmt._hints.items() - if dialect in ('*', self.dialect.name) - ]) - if update_stmt.table in dialect_hints: - table_text = self.format_from_hint_text( - table_text, - update_stmt.table, - dialect_hints[update_stmt.table], - True - ) - else: - dialect_hints = None - - text += table_text - - text += ' SET ' - include_table = extra_froms and \ - self.render_table_with_column_in_update_from - text += ', '.join( - c[0]._compiler_dispatch(self, - include_table=include_table) + - '=' + c[1] for c in colparams - ) - - if self.returning or update_stmt._returning: - if not self.returning: - self.returning = update_stmt._returning - if self.returning_precedes_values: - text += " " + self.returning_clause( - update_stmt, self.returning) - - if extra_froms: - extra_from_text = self.update_from_clause( - update_stmt, - update_stmt.table, - extra_froms, - dialect_hints, **kw) - if extra_from_text: - text += " " + extra_from_text - - if update_stmt._whereclause is not None: - text += " WHERE " + self.process(update_stmt._whereclause) - - limit_clause = self.update_limit_clause(update_stmt) - if limit_clause: - text += " " + limit_clause - - if self.returning and not self.returning_precedes_values: - text += " " + self.returning_clause( - update_stmt, self.returning) - - self.stack.pop(-1) - - return text - - def _create_crud_bind_param(self, col, value, required=False, name=None): - if name is None: - name = col.key - bindparam = elements.BindParameter(name, value, - type_=col.type, required=required) - bindparam._is_crud = True - return bindparam._compiler_dispatch(self) - - def _get_colparams(self, stmt, extra_tables=None, **kw): - """create a set of tuples representing column/string pairs for use - in an INSERT or UPDATE statement. - - Also generates the Compiled object's postfetch, prefetch, and - returning column collections, used for default handling and ultimately - populating the ResultProxy's prefetch_cols() and postfetch_cols() - collections. - - """ - - self.postfetch = [] - self.prefetch = [] - self.returning = [] - - # no parameters in the statement, no parameters in the - # compiled params - return binds for all columns - if self.column_keys is None and stmt.parameters is None: - return [ - (c, self._create_crud_bind_param(c, - None, required=True)) - for c in stmt.table.columns - ] - - if stmt._has_multi_parameters: - stmt_parameters = stmt.parameters[0] - else: - stmt_parameters = stmt.parameters - - # if we have statement parameters - set defaults in the - # compiled params - if self.column_keys is None: - parameters = {} - else: - parameters = dict((elements._column_as_key(key), REQUIRED) - for key in self.column_keys - if not stmt_parameters or - key not in stmt_parameters) - - # create a list of column assignment clauses as tuples - values = [] - - if stmt_parameters is not None: - for k, v in stmt_parameters.items(): - colkey = elements._column_as_key(k) - if colkey is not None: - parameters.setdefault(colkey, v) - else: - # a non-Column expression on the left side; - # add it to values() in an "as-is" state, - # coercing right side to bound param - if elements._is_literal(v): - v = self.process(elements.BindParameter(None, v, type_=k.type), **kw) - else: - v = self.process(v.self_group(), **kw) - - values.append((k, v)) - - need_pks = self.isinsert and \ - not self.inline and \ - not stmt._returning - - implicit_returning = need_pks and \ - self.dialect.implicit_returning and \ - stmt.table.implicit_returning - - if self.isinsert: - implicit_return_defaults = implicit_returning and stmt._return_defaults - elif self.isupdate: - implicit_return_defaults = self.dialect.implicit_returning and \ - stmt.table.implicit_returning and \ - stmt._return_defaults - - if implicit_return_defaults: - if stmt._return_defaults is True: - implicit_return_defaults = set(stmt.table.c) - else: - implicit_return_defaults = set(stmt._return_defaults) - - postfetch_lastrowid = need_pks and self.dialect.postfetch_lastrowid - - check_columns = {} - # special logic that only occurs for multi-table UPDATE - # statements - if extra_tables and stmt_parameters: - normalized_params = dict( - (elements._clause_element_as_expr(c), param) - for c, param in stmt_parameters.items() - ) - assert self.isupdate - affected_tables = set() - for t in extra_tables: - for c in t.c: - if c in normalized_params: - affected_tables.add(t) - check_columns[c.key] = c - value = normalized_params[c] - if elements._is_literal(value): - value = self._create_crud_bind_param( - c, value, required=value is REQUIRED) - else: - self.postfetch.append(c) - value = self.process(value.self_group(), **kw) - values.append((c, value)) - # determine tables which are actually - # to be updated - process onupdate and - # server_onupdate for these - for t in affected_tables: - for c in t.c: - if c in normalized_params: - continue - elif c.onupdate is not None and not c.onupdate.is_sequence: - if c.onupdate.is_clause_element: - values.append( - (c, self.process(c.onupdate.arg.self_group(), **kw)) - ) - self.postfetch.append(c) - else: - values.append( - (c, self._create_crud_bind_param(c, None)) - ) - self.prefetch.append(c) - elif c.server_onupdate is not None: - self.postfetch.append(c) - - if self.isinsert and stmt.select_names: - # for an insert from select, we can only use names that - # are given, so only select for those names. - cols = (stmt.table.c[elements._column_as_key(name)] - for name in stmt.select_names) - else: - # iterate through all table columns to maintain - # ordering, even for those cols that aren't included - cols = stmt.table.columns - - for c in cols: - if c.key in parameters and c.key not in check_columns: - value = parameters.pop(c.key) - if elements._is_literal(value): - value = self._create_crud_bind_param( - c, value, required=value is REQUIRED, - name=c.key - if not stmt._has_multi_parameters - else "%s_0" % c.key - ) - else: - if isinstance(value, elements.BindParameter) and \ - value.type._isnull: - value = value._clone() - value.type = c.type - - if c.primary_key and implicit_returning: - self.returning.append(c) - value = self.process(value.self_group(), **kw) - elif implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - value = self.process(value.self_group(), **kw) - else: - self.postfetch.append(c) - value = self.process(value.self_group(), **kw) - values.append((c, value)) - - elif self.isinsert: - if c.primary_key and \ - need_pks and \ - ( - implicit_returning or - not postfetch_lastrowid or - c is not stmt.table._autoincrement_column - ): - - if implicit_returning: - if c.default is not None: - if c.default.is_sequence: - if self.dialect.supports_sequences and \ - (not c.default.optional or \ - not self.dialect.sequences_optional): - proc = self.process(c.default, **kw) - values.append((c, proc)) - self.returning.append(c) - elif c.default.is_clause_element: - values.append( - (c, - self.process(c.default.arg.self_group(), **kw)) - ) - self.returning.append(c) - else: - values.append( - (c, self._create_crud_bind_param(c, None)) - ) - self.prefetch.append(c) - else: - self.returning.append(c) - else: - if ( - c.default is not None and - ( - not c.default.is_sequence or - self.dialect.supports_sequences - ) - ) or \ - c is stmt.table._autoincrement_column and ( - self.dialect.supports_sequences or - self.dialect.preexecute_autoincrement_sequences - ): - - values.append( - (c, self._create_crud_bind_param(c, None)) - ) - - self.prefetch.append(c) - - elif c.default is not None: - if c.default.is_sequence: - if self.dialect.supports_sequences and \ - (not c.default.optional or \ - not self.dialect.sequences_optional): - proc = self.process(c.default, **kw) - values.append((c, proc)) - if implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - elif not c.primary_key: - self.postfetch.append(c) - elif c.default.is_clause_element: - values.append( - (c, self.process(c.default.arg.self_group(), **kw)) - ) - - if implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - elif not c.primary_key: - # dont add primary key column to postfetch - self.postfetch.append(c) - else: - values.append( - (c, self._create_crud_bind_param(c, None)) - ) - self.prefetch.append(c) - elif c.server_default is not None: - if implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - elif not c.primary_key: - self.postfetch.append(c) - elif implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - - elif self.isupdate: - if c.onupdate is not None and not c.onupdate.is_sequence: - if c.onupdate.is_clause_element: - values.append( - (c, self.process(c.onupdate.arg.self_group(), **kw)) - ) - if implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - else: - self.postfetch.append(c) - else: - values.append( - (c, self._create_crud_bind_param(c, None)) - ) - self.prefetch.append(c) - elif c.server_onupdate is not None: - if implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - else: - self.postfetch.append(c) - elif implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - - if parameters and stmt_parameters: - check = set(parameters).intersection( - elements._column_as_key(k) for k in stmt.parameters - ).difference(check_columns) - if check: - raise exc.CompileError( - "Unconsumed column names: %s" % - (", ".join(check)) - ) - - if stmt._has_multi_parameters: - values_0 = values - values = [values] - - values.extend( - [ - ( - c, - self._create_crud_bind_param( - c, row[c.key], - name="%s_%d" % (c.key, i + 1) - ) - if c.key in row else param - ) - for (c, param) in values_0 - ] - for i, row in enumerate(stmt.parameters[1:]) - ) - - return values - - def visit_delete(self, delete_stmt, **kw): - self.stack.append({'correlate_froms': set([delete_stmt.table]), - "iswrapper": False, - "asfrom_froms": set([delete_stmt.table])}) - self.isdelete = True - - text = "DELETE " - - if delete_stmt._prefixes: - text += self._generate_prefixes(delete_stmt, - delete_stmt._prefixes, **kw) - - text += "FROM " - table_text = delete_stmt.table._compiler_dispatch(self, - asfrom=True, iscrud=True) - - if delete_stmt._hints: - dialect_hints = dict([ - (table, hint_text) - for (table, dialect), hint_text in - delete_stmt._hints.items() - if dialect in ('*', self.dialect.name) - ]) - if delete_stmt.table in dialect_hints: - table_text = self.format_from_hint_text( - table_text, - delete_stmt.table, - dialect_hints[delete_stmt.table], - True - ) - - else: - dialect_hints = None - - text += table_text - - if delete_stmt._returning: - self.returning = delete_stmt._returning - if self.returning_precedes_values: - text += " " + self.returning_clause( - delete_stmt, delete_stmt._returning) - - if delete_stmt._whereclause is not None: - text += " WHERE " - text += delete_stmt._whereclause._compiler_dispatch(self) - - if self.returning and not self.returning_precedes_values: - text += " " + self.returning_clause( - delete_stmt, delete_stmt._returning) - - self.stack.pop(-1) - - return text - - def visit_savepoint(self, savepoint_stmt): - return "SAVEPOINT %s" % self.preparer.format_savepoint(savepoint_stmt) - - def visit_rollback_to_savepoint(self, savepoint_stmt): - return "ROLLBACK TO SAVEPOINT %s" % \ - self.preparer.format_savepoint(savepoint_stmt) - - def visit_release_savepoint(self, savepoint_stmt): - return "RELEASE SAVEPOINT %s" % \ - self.preparer.format_savepoint(savepoint_stmt) - - -class DDLCompiler(Compiled): - - @util.memoized_property - def sql_compiler(self): - return self.dialect.statement_compiler(self.dialect, None) - - @util.memoized_property - def type_compiler(self): - return self.dialect.type_compiler - - @property - def preparer(self): - return self.dialect.identifier_preparer - - def construct_params(self, params=None): - return None - - def visit_ddl(self, ddl, **kwargs): - # table events can substitute table and schema name - context = ddl.context - if isinstance(ddl.target, schema.Table): - context = context.copy() - - preparer = self.dialect.identifier_preparer - path = preparer.format_table_seq(ddl.target) - if len(path) == 1: - table, sch = path[0], '' - else: - table, sch = path[-1], path[0] - - context.setdefault('table', table) - context.setdefault('schema', sch) - context.setdefault('fullname', preparer.format_table(ddl.target)) - - return self.sql_compiler.post_process_text(ddl.statement % context) - - def visit_create_schema(self, create): - schema = self.preparer.format_schema(create.element) - return "CREATE SCHEMA " + schema - - def visit_drop_schema(self, drop): - schema = self.preparer.format_schema(drop.element) - text = "DROP SCHEMA " + schema - if drop.cascade: - text += " CASCADE" - return text - - def visit_create_table(self, create): - table = create.element - preparer = self.dialect.identifier_preparer - - text = "\n" + " ".join(['CREATE'] + \ - table._prefixes + \ - ['TABLE', - preparer.format_table(table), - "("]) - separator = "\n" - - # if only one primary key, specify it along with the column - first_pk = False - for create_column in create.columns: - column = create_column.element - try: - processed = self.process(create_column, - first_pk=column.primary_key - and not first_pk) - if processed is not None: - text += separator - separator = ", \n" - text += "\t" + processed - if column.primary_key: - first_pk = True - except exc.CompileError as ce: - util.raise_from_cause( - exc.CompileError(util.u("(in table '%s', column '%s'): %s") % ( - table.description, - column.name, - ce.args[0] - ))) - - const = self.create_table_constraints(table) - if const: - text += ", \n\t" + const - - text += "\n)%s\n\n" % self.post_create_table(table) - return text - - def visit_create_column(self, create, first_pk=False): - column = create.element - - if column.system: - return None - - text = self.get_column_specification( - column, - first_pk=first_pk - ) - const = " ".join(self.process(constraint) \ - for constraint in column.constraints) - if const: - text += " " + const - - return text - - def create_table_constraints(self, table): - - # On some DB order is significant: visit PK first, then the - # other constraints (engine.ReflectionTest.testbasic failed on FB2) - constraints = [] - if table.primary_key: - constraints.append(table.primary_key) - - constraints.extend([c for c in table._sorted_constraints - if c is not table.primary_key]) - - return ", \n\t".join(p for p in - (self.process(constraint) - for constraint in constraints - if ( - constraint._create_rule is None or - constraint._create_rule(self)) - and ( - not self.dialect.supports_alter or - not getattr(constraint, 'use_alter', False) - )) if p is not None - ) - - def visit_drop_table(self, drop): - return "\nDROP TABLE " + self.preparer.format_table(drop.element) - - def visit_drop_view(self, drop): - return "\nDROP VIEW " + self.preparer.format_table(drop.element) - - - def _verify_index_table(self, index): - if index.table is None: - raise exc.CompileError("Index '%s' is not associated " - "with any table." % index.name) - - - def visit_create_index(self, create, include_schema=False, - include_table_schema=True): - index = create.element - self._verify_index_table(index) - preparer = self.preparer - text = "CREATE " - if index.unique: - text += "UNIQUE " - text += "INDEX %s ON %s (%s)" \ - % ( - self._prepared_index_name(index, - include_schema=include_schema), - preparer.format_table(index.table, - use_schema=include_table_schema), - ', '.join( - self.sql_compiler.process(expr, - include_table=False, literal_binds=True) for - expr in index.expressions) - ) - return text - - def visit_drop_index(self, drop): - index = drop.element - return "\nDROP INDEX " + self._prepared_index_name(index, - include_schema=True) - - def _prepared_index_name(self, index, include_schema=False): - if include_schema and index.table is not None and index.table.schema: - schema = index.table.schema - schema_name = self.preparer.quote_schema(schema) - else: - schema_name = None - - ident = index.name - if isinstance(ident, elements._truncated_label): - max_ = self.dialect.max_index_name_length or \ - self.dialect.max_identifier_length - if len(ident) > max_: - ident = ident[0:max_ - 8] + \ - "_" + util.md5_hex(ident)[-4:] - else: - self.dialect.validate_identifier(ident) - - index_name = self.preparer.quote(ident) - - if schema_name: - index_name = schema_name + "." + index_name - return index_name - - def visit_add_constraint(self, create): - return "ALTER TABLE %s ADD %s" % ( - self.preparer.format_table(create.element.table), - self.process(create.element) - ) - - def visit_create_sequence(self, create): - text = "CREATE SEQUENCE %s" % \ - self.preparer.format_sequence(create.element) - if create.element.increment is not None: - text += " INCREMENT BY %d" % create.element.increment - if create.element.start is not None: - text += " START WITH %d" % create.element.start - return text - - def visit_drop_sequence(self, drop): - return "DROP SEQUENCE %s" % \ - self.preparer.format_sequence(drop.element) - - def visit_drop_constraint(self, drop): - return "ALTER TABLE %s DROP CONSTRAINT %s%s" % ( - self.preparer.format_table(drop.element.table), - self.preparer.format_constraint(drop.element), - drop.cascade and " CASCADE" or "" - ) - - def get_column_specification(self, column, **kwargs): - colspec = self.preparer.format_column(column) + " " + \ - self.dialect.type_compiler.process(column.type) - default = self.get_column_default_string(column) - if default is not None: - colspec += " DEFAULT " + default - - if not column.nullable: - colspec += " NOT NULL" - return colspec - - def post_create_table(self, table): - return '' - - def get_column_default_string(self, column): - if isinstance(column.server_default, schema.DefaultClause): - if isinstance(column.server_default.arg, util.string_types): - return "'%s'" % column.server_default.arg - else: - return self.sql_compiler.process(column.server_default.arg) - else: - return None - - def visit_check_constraint(self, constraint): - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - self.preparer.format_constraint(constraint) - text += "CHECK (%s)" % self.sql_compiler.process(constraint.sqltext, - include_table=False, - literal_binds=True) - text += self.define_constraint_deferrability(constraint) - return text - - def visit_column_check_constraint(self, constraint): - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - self.preparer.format_constraint(constraint) - text += "CHECK (%s)" % constraint.sqltext - text += self.define_constraint_deferrability(constraint) - return text - - def visit_primary_key_constraint(self, constraint): - if len(constraint) == 0: - return '' - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - self.preparer.format_constraint(constraint) - text += "PRIMARY KEY " - text += "(%s)" % ', '.join(self.preparer.quote(c.name) - for c in constraint) - text += self.define_constraint_deferrability(constraint) - return text - - def visit_foreign_key_constraint(self, constraint): - preparer = self.dialect.identifier_preparer - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - preparer.format_constraint(constraint) - remote_table = list(constraint._elements.values())[0].column.table - text += "FOREIGN KEY(%s) REFERENCES %s (%s)" % ( - ', '.join(preparer.quote(f.parent.name) - for f in constraint._elements.values()), - self.define_constraint_remote_table( - constraint, remote_table, preparer), - ', '.join(preparer.quote(f.column.name) - for f in constraint._elements.values()) - ) - text += self.define_constraint_match(constraint) - text += self.define_constraint_cascades(constraint) - text += self.define_constraint_deferrability(constraint) - return text - - def define_constraint_remote_table(self, constraint, table, preparer): - """Format the remote table clause of a CREATE CONSTRAINT clause.""" - - return preparer.format_table(table) - - def visit_unique_constraint(self, constraint): - text = "" - if constraint.name is not None: - text += "CONSTRAINT %s " % \ - self.preparer.format_constraint(constraint) - text += "UNIQUE (%s)" % ( - ', '.join(self.preparer.quote(c.name) - for c in constraint)) - text += self.define_constraint_deferrability(constraint) - return text - - def define_constraint_cascades(self, constraint): - text = "" - if constraint.ondelete is not None: - text += " ON DELETE %s" % constraint.ondelete - if constraint.onupdate is not None: - text += " ON UPDATE %s" % constraint.onupdate - return text - - def define_constraint_deferrability(self, constraint): - text = "" - if constraint.deferrable is not None: - if constraint.deferrable: - text += " DEFERRABLE" - else: - text += " NOT DEFERRABLE" - if constraint.initially is not None: - text += " INITIALLY %s" % constraint.initially - return text - - def define_constraint_match(self, constraint): - text = "" - if constraint.match is not None: - text += " MATCH %s" % constraint.match - return text - - -class GenericTypeCompiler(TypeCompiler): - - def visit_FLOAT(self, type_): - return "FLOAT" - - def visit_REAL(self, type_): - return "REAL" - - def visit_NUMERIC(self, type_): - if type_.precision is None: - return "NUMERIC" - elif type_.scale is None: - return "NUMERIC(%(precision)s)" % \ - {'precision': type_.precision} - else: - return "NUMERIC(%(precision)s, %(scale)s)" % \ - {'precision': type_.precision, - 'scale': type_.scale} - - def visit_DECIMAL(self, type_): - if type_.precision is None: - return "DECIMAL" - elif type_.scale is None: - return "DECIMAL(%(precision)s)" % \ - {'precision': type_.precision} - else: - return "DECIMAL(%(precision)s, %(scale)s)" % \ - {'precision': type_.precision, - 'scale': type_.scale} - - def visit_INTEGER(self, type_): - return "INTEGER" - - def visit_SMALLINT(self, type_): - return "SMALLINT" - - def visit_BIGINT(self, type_): - return "BIGINT" - - def visit_TIMESTAMP(self, type_): - return 'TIMESTAMP' - - def visit_DATETIME(self, type_): - return "DATETIME" - - def visit_DATE(self, type_): - return "DATE" - - def visit_TIME(self, type_): - return "TIME" - - def visit_CLOB(self, type_): - return "CLOB" - - def visit_NCLOB(self, type_): - return "NCLOB" - - def _render_string_type(self, type_, name): - - text = name - if type_.length: - text += "(%d)" % type_.length - if type_.collation: - text += ' COLLATE "%s"' % type_.collation - return text - - def visit_CHAR(self, type_): - return self._render_string_type(type_, "CHAR") - - def visit_NCHAR(self, type_): - return self._render_string_type(type_, "NCHAR") - - def visit_VARCHAR(self, type_): - return self._render_string_type(type_, "VARCHAR") - - def visit_NVARCHAR(self, type_): - return self._render_string_type(type_, "NVARCHAR") - - def visit_TEXT(self, type_): - return self._render_string_type(type_, "TEXT") - - def visit_BLOB(self, type_): - return "BLOB" - - def visit_BINARY(self, type_): - return "BINARY" + (type_.length and "(%d)" % type_.length or "") - - def visit_VARBINARY(self, type_): - return "VARBINARY" + (type_.length and "(%d)" % type_.length or "") - - def visit_BOOLEAN(self, type_): - return "BOOLEAN" - - def visit_large_binary(self, type_): - return self.visit_BLOB(type_) - - def visit_boolean(self, type_): - return self.visit_BOOLEAN(type_) - - def visit_time(self, type_): - return self.visit_TIME(type_) - - def visit_datetime(self, type_): - return self.visit_DATETIME(type_) - - def visit_date(self, type_): - return self.visit_DATE(type_) - - def visit_big_integer(self, type_): - return self.visit_BIGINT(type_) - - def visit_small_integer(self, type_): - return self.visit_SMALLINT(type_) - - def visit_integer(self, type_): - return self.visit_INTEGER(type_) - - def visit_real(self, type_): - return self.visit_REAL(type_) - - def visit_float(self, type_): - return self.visit_FLOAT(type_) - - def visit_numeric(self, type_): - return self.visit_NUMERIC(type_) - - def visit_string(self, type_): - return self.visit_VARCHAR(type_) - - def visit_unicode(self, type_): - return self.visit_VARCHAR(type_) - - def visit_text(self, type_): - return self.visit_TEXT(type_) - - def visit_unicode_text(self, type_): - return self.visit_TEXT(type_) - - def visit_enum(self, type_): - return self.visit_VARCHAR(type_) - - def visit_null(self, type_): - raise exc.CompileError("Can't generate DDL for %r; " - "did you forget to specify a " - "type on this Column?" % type_) - - def visit_type_decorator(self, type_): - return self.process(type_.type_engine(self.dialect)) - - def visit_user_defined(self, type_): - return type_.get_col_spec() - - -class IdentifierPreparer(object): - """Handle quoting and case-folding of identifiers based on options.""" - - reserved_words = RESERVED_WORDS - - legal_characters = LEGAL_CHARACTERS - - illegal_initial_characters = ILLEGAL_INITIAL_CHARACTERS - - def __init__(self, dialect, initial_quote='"', - final_quote=None, escape_quote='"', omit_schema=False): - """Construct a new ``IdentifierPreparer`` object. - - initial_quote - Character that begins a delimited identifier. - - final_quote - Character that ends a delimited identifier. Defaults to - `initial_quote`. - - omit_schema - Prevent prepending schema name. Useful for databases that do - not support schemae. - """ - - self.dialect = dialect - self.initial_quote = initial_quote - self.final_quote = final_quote or self.initial_quote - self.escape_quote = escape_quote - self.escape_to_quote = self.escape_quote * 2 - self.omit_schema = omit_schema - self._strings = {} - - def _escape_identifier(self, value): - """Escape an identifier. - - Subclasses should override this to provide database-dependent - escaping behavior. - """ - - return value.replace(self.escape_quote, self.escape_to_quote) - - def _unescape_identifier(self, value): - """Canonicalize an escaped identifier. - - Subclasses should override this to provide database-dependent - unescaping behavior that reverses _escape_identifier. - """ - - return value.replace(self.escape_to_quote, self.escape_quote) - - def quote_identifier(self, value): - """Quote an identifier. - - Subclasses should override this to provide database-dependent - quoting behavior. - """ - - return self.initial_quote + \ - self._escape_identifier(value) + \ - self.final_quote - - def _requires_quotes(self, value): - """Return True if the given identifier requires quoting.""" - lc_value = value.lower() - return (lc_value in self.reserved_words - or value[0] in self.illegal_initial_characters - or not self.legal_characters.match(util.text_type(value)) - or (lc_value != value)) - - def quote_schema(self, schema, force=None): - """Conditionally quote a schema. - - Subclasses can override this to provide database-dependent - quoting behavior for schema names. - - the 'force' flag should be considered deprecated. - - """ - return self.quote(schema, force) - - def quote(self, ident, force=None): - """Conditionally quote an identifier. - - the 'force' flag should be considered deprecated. - """ - - force = getattr(ident, "quote", None) - - if force is None: - if ident in self._strings: - return self._strings[ident] - else: - if self._requires_quotes(ident): - self._strings[ident] = self.quote_identifier(ident) - else: - self._strings[ident] = ident - return self._strings[ident] - elif force: - return self.quote_identifier(ident) - else: - return ident - - def format_sequence(self, sequence, use_schema=True): - name = self.quote(sequence.name) - if not self.omit_schema and use_schema and sequence.schema is not None: - name = self.quote_schema(sequence.schema) + "." + name - return name - - def format_label(self, label, name=None): - return self.quote(name or label.name) - - def format_alias(self, alias, name=None): - return self.quote(name or alias.name) - - def format_savepoint(self, savepoint, name=None): - return self.quote(name or savepoint.ident) - - def format_constraint(self, constraint): - return self.quote(constraint.name) - - def format_table(self, table, use_schema=True, name=None): - """Prepare a quoted table and schema name.""" - - if name is None: - name = table.name - result = self.quote(name) - if not self.omit_schema and use_schema \ - and getattr(table, "schema", None): - result = self.quote_schema(table.schema) + "." + result - return result - - def format_schema(self, name, quote=None): - """Prepare a quoted schema name.""" - - return self.quote(name, quote) - - def format_column(self, column, use_table=False, - name=None, table_name=None): - """Prepare a quoted column name.""" - - if name is None: - name = column.name - if not getattr(column, 'is_literal', False): - if use_table: - return self.format_table( - column.table, use_schema=False, - name=table_name) + "." + self.quote(name) - else: - return self.quote(name) - else: - # literal textual elements get stuck into ColumnClause a lot, - # which shouldn't get quoted - - if use_table: - return self.format_table(column.table, - use_schema=False, name=table_name) + '.' + name - else: - return name - - def format_table_seq(self, table, use_schema=True): - """Format table name and schema as a tuple.""" - - # Dialects with more levels in their fully qualified references - # ('database', 'owner', etc.) could override this and return - # a longer sequence. - - if not self.omit_schema and use_schema and \ - getattr(table, 'schema', None): - return (self.quote_schema(table.schema), - self.format_table(table, use_schema=False)) - else: - return (self.format_table(table, use_schema=False), ) - - @util.memoized_property - def _r_identifiers(self): - initial, final, escaped_final = \ - [re.escape(s) for s in - (self.initial_quote, self.final_quote, - self._escape_identifier(self.final_quote))] - r = re.compile( - r'(?:' - r'(?:%(initial)s((?:%(escaped)s|[^%(final)s])+)%(final)s' - r'|([^\.]+))(?=\.|$))+' % - {'initial': initial, - 'final': final, - 'escaped': escaped_final}) - return r - - def unformat_identifiers(self, identifiers): - """Unpack 'schema.table.column'-like strings into components.""" - - r = self._r_identifiers - return [self._unescape_identifier(i) - for i in [a or b for a, b in r.findall(identifiers)]] diff --git a/libs/sqlalchemy/sql/ddl.py b/libs/sqlalchemy/sql/ddl.py deleted file mode 100644 index bda87650..00000000 --- a/libs/sqlalchemy/sql/ddl.py +++ /dev/null @@ -1,864 +0,0 @@ -# sql/ddl.py -# Copyright (C) 2009-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -""" -Provides the hierarchy of DDL-defining schema items as well as routines -to invoke them for a create/drop call. - -""" - -from .. import util -from .elements import ClauseElement -from .visitors import traverse -from .base import Executable, _generative, SchemaVisitor, _bind_or_error -from ..util import topological -from .. import event -from .. import exc - -class _DDLCompiles(ClauseElement): - def _compiler(self, dialect, **kw): - """Return a compiler appropriate for this ClauseElement, given a - Dialect.""" - - return dialect.ddl_compiler(dialect, self, **kw) - - -class DDLElement(Executable, _DDLCompiles): - """Base class for DDL expression constructs. - - This class is the base for the general purpose :class:`.DDL` class, - as well as the various create/drop clause constructs such as - :class:`.CreateTable`, :class:`.DropTable`, :class:`.AddConstraint`, - etc. - - :class:`.DDLElement` integrates closely with SQLAlchemy events, - introduced in :ref:`event_toplevel`. An instance of one is - itself an event receiving callable:: - - event.listen( - users, - 'after_create', - AddConstraint(constraint).execute_if(dialect='postgresql') - ) - - .. seealso:: - - :class:`.DDL` - - :class:`.DDLEvents` - - :ref:`event_toplevel` - - :ref:`schema_ddl_sequences` - - """ - - _execution_options = Executable.\ - _execution_options.union({'autocommit': True}) - - target = None - on = None - dialect = None - callable_ = None - - def _execute_on_connection(self, connection, multiparams, params): - return connection._execute_ddl(self, multiparams, params) - - def execute(self, bind=None, target=None): - """Execute this DDL immediately. - - Executes the DDL statement in isolation using the supplied - :class:`.Connectable` or - :class:`.Connectable` assigned to the ``.bind`` - property, if not supplied. If the DDL has a conditional ``on`` - criteria, it will be invoked with None as the event. - - :param bind: - Optional, an ``Engine`` or ``Connection``. If not supplied, a valid - :class:`.Connectable` must be present in the - ``.bind`` property. - - :param target: - Optional, defaults to None. The target SchemaItem for the - execute call. Will be passed to the ``on`` callable if any, - and may also provide string expansion data for the - statement. See ``execute_at`` for more information. - - """ - - if bind is None: - bind = _bind_or_error(self) - - if self._should_execute(target, bind): - return bind.execute(self.against(target)) - else: - bind.engine.logger.info( - "DDL execution skipped, criteria not met.") - - @util.deprecated("0.7", "See :class:`.DDLEvents`, as well as " - ":meth:`.DDLElement.execute_if`.") - def execute_at(self, event_name, target): - """Link execution of this DDL to the DDL lifecycle of a SchemaItem. - - Links this ``DDLElement`` to a ``Table`` or ``MetaData`` instance, - executing it when that schema item is created or dropped. The DDL - statement will be executed using the same Connection and transactional - context as the Table create/drop itself. The ``.bind`` property of - this statement is ignored. - - :param event: - One of the events defined in the schema item's ``.ddl_events``; - e.g. 'before-create', 'after-create', 'before-drop' or 'after-drop' - - :param target: - The Table or MetaData instance for which this DDLElement will - be associated with. - - A DDLElement instance can be linked to any number of schema items. - - ``execute_at`` builds on the ``append_ddl_listener`` interface of - :class:`.MetaData` and :class:`.Table` objects. - - Caveat: Creating or dropping a Table in isolation will also trigger - any DDL set to ``execute_at`` that Table's MetaData. This may change - in a future release. - - """ - - def call_event(target, connection, **kw): - if self._should_execute_deprecated(event_name, - target, connection, **kw): - return connection.execute(self.against(target)) - - event.listen(target, "" + event_name.replace('-', '_'), call_event) - - @_generative - def against(self, target): - """Return a copy of this DDL against a specific schema item.""" - - self.target = target - - @_generative - def execute_if(self, dialect=None, callable_=None, state=None): - """Return a callable that will execute this - DDLElement conditionally. - - Used to provide a wrapper for event listening:: - - event.listen( - metadata, - 'before_create', - DDL("my_ddl").execute_if(dialect='postgresql') - ) - - :param dialect: May be a string, tuple or a callable - predicate. If a string, it will be compared to the name of the - executing database dialect:: - - DDL('something').execute_if(dialect='postgresql') - - If a tuple, specifies multiple dialect names:: - - DDL('something').execute_if(dialect=('postgresql', 'mysql')) - - :param callable_: A callable, which will be invoked with - four positional arguments as well as optional keyword - arguments: - - :ddl: - This DDL element. - - :target: - The :class:`.Table` or :class:`.MetaData` object which is the - target of this event. May be None if the DDL is executed - explicitly. - - :bind: - The :class:`.Connection` being used for DDL execution - - :tables: - Optional keyword argument - a list of Table objects which are to - be created/ dropped within a MetaData.create_all() or drop_all() - method call. - - :state: - Optional keyword argument - will be the ``state`` argument - passed to this function. - - :checkfirst: - Keyword argument, will be True if the 'checkfirst' flag was - set during the call to ``create()``, ``create_all()``, - ``drop()``, ``drop_all()``. - - If the callable returns a true value, the DDL statement will be - executed. - - :param state: any value which will be passed to the callable\_ - as the ``state`` keyword argument. - - .. seealso:: - - :class:`.DDLEvents` - - :ref:`event_toplevel` - - """ - self.dialect = dialect - self.callable_ = callable_ - self.state = state - - def _should_execute(self, target, bind, **kw): - if self.on is not None and \ - not self._should_execute_deprecated(None, target, bind, **kw): - return False - - if isinstance(self.dialect, util.string_types): - if self.dialect != bind.engine.name: - return False - elif isinstance(self.dialect, (tuple, list, set)): - if bind.engine.name not in self.dialect: - return False - if self.callable_ is not None and \ - not self.callable_(self, target, bind, state=self.state, **kw): - return False - - return True - - def _should_execute_deprecated(self, event, target, bind, **kw): - if self.on is None: - return True - elif isinstance(self.on, util.string_types): - return self.on == bind.engine.name - elif isinstance(self.on, (tuple, list, set)): - return bind.engine.name in self.on - else: - return self.on(self, event, target, bind, **kw) - - def __call__(self, target, bind, **kw): - """Execute the DDL as a ddl_listener.""" - - if self._should_execute(target, bind, **kw): - return bind.execute(self.against(target)) - - def _check_ddl_on(self, on): - if (on is not None and - (not isinstance(on, util.string_types + (tuple, list, set)) and - not util.callable(on))): - raise exc.ArgumentError( - "Expected the name of a database dialect, a tuple " - "of names, or a callable for " - "'on' criteria, got type '%s'." % type(on).__name__) - - def bind(self): - if self._bind: - return self._bind - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - def _generate(self): - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - return s - - -class DDL(DDLElement): - """A literal DDL statement. - - Specifies literal SQL DDL to be executed by the database. DDL objects - function as DDL event listeners, and can be subscribed to those events - listed in :class:`.DDLEvents`, using either :class:`.Table` or - :class:`.MetaData` objects as targets. Basic templating support allows - a single DDL instance to handle repetitive tasks for multiple tables. - - Examples:: - - from sqlalchemy import event, DDL - - tbl = Table('users', metadata, Column('uid', Integer)) - event.listen(tbl, 'before_create', DDL('DROP TRIGGER users_trigger')) - - spow = DDL('ALTER TABLE %(table)s SET secretpowers TRUE') - event.listen(tbl, 'after_create', spow.execute_if(dialect='somedb')) - - drop_spow = DDL('ALTER TABLE users SET secretpowers FALSE') - connection.execute(drop_spow) - - When operating on Table events, the following ``statement`` - string substitions are available:: - - %(table)s - the Table name, with any required quoting applied - %(schema)s - the schema name, with any required quoting applied - %(fullname)s - the Table name including schema, quoted if needed - - The DDL's "context", if any, will be combined with the standard - substutions noted above. Keys present in the context will override - the standard substitutions. - - """ - - __visit_name__ = "ddl" - - def __init__(self, statement, on=None, context=None, bind=None): - """Create a DDL statement. - - :param statement: - A string or unicode string to be executed. Statements will be - processed with Python's string formatting operator. See the - ``context`` argument and the ``execute_at`` method. - - A literal '%' in a statement must be escaped as '%%'. - - SQL bind parameters are not available in DDL statements. - - :param on: - .. deprecated:: 0.7 - See :meth:`.DDLElement.execute_if`. - - Optional filtering criteria. May be a string, tuple or a callable - predicate. If a string, it will be compared to the name of the - executing database dialect:: - - DDL('something', on='postgresql') - - If a tuple, specifies multiple dialect names:: - - DDL('something', on=('postgresql', 'mysql')) - - If a callable, it will be invoked with four positional arguments - as well as optional keyword arguments: - - :ddl: - This DDL element. - - :event: - The name of the event that has triggered this DDL, such as - 'after-create' Will be None if the DDL is executed explicitly. - - :target: - The ``Table`` or ``MetaData`` object which is the target of - this event. May be None if the DDL is executed explicitly. - - :connection: - The ``Connection`` being used for DDL execution - - :tables: - Optional keyword argument - a list of Table objects which are to - be created/ dropped within a MetaData.create_all() or drop_all() - method call. - - - If the callable returns a true value, the DDL statement will be - executed. - - :param context: - Optional dictionary, defaults to None. These values will be - available for use in string substitutions on the DDL statement. - - :param bind: - Optional. A :class:`.Connectable`, used by - default when ``execute()`` is invoked without a bind argument. - - - .. seealso:: - - :class:`.DDLEvents` - - :mod:`sqlalchemy.event` - - """ - - if not isinstance(statement, util.string_types): - raise exc.ArgumentError( - "Expected a string or unicode SQL statement, got '%r'" % - statement) - - self.statement = statement - self.context = context or {} - - self._check_ddl_on(on) - self.on = on - self._bind = bind - - def __repr__(self): - return '<%s@%s; %s>' % ( - type(self).__name__, id(self), - ', '.join([repr(self.statement)] + - ['%s=%r' % (key, getattr(self, key)) - for key in ('on', 'context') - if getattr(self, key)])) - - - -class _CreateDropBase(DDLElement): - """Base class for DDL constucts that represent CREATE and DROP or - equivalents. - - The common theme of _CreateDropBase is a single - ``element`` attribute which refers to the element - to be created or dropped. - - """ - - def __init__(self, element, on=None, bind=None): - self.element = element - self._check_ddl_on(on) - self.on = on - self.bind = bind - - def _create_rule_disable(self, compiler): - """Allow disable of _create_rule using a callable. - - Pass to _create_rule using - util.portable_instancemethod(self._create_rule_disable) - to retain serializability. - - """ - return False - - -class CreateSchema(_CreateDropBase): - """Represent a CREATE SCHEMA statement. - - .. versionadded:: 0.7.4 - - The argument here is the string name of the schema. - - """ - - __visit_name__ = "create_schema" - - def __init__(self, name, quote=None, **kw): - """Create a new :class:`.CreateSchema` construct.""" - - self.quote = quote - super(CreateSchema, self).__init__(name, **kw) - - -class DropSchema(_CreateDropBase): - """Represent a DROP SCHEMA statement. - - The argument here is the string name of the schema. - - .. versionadded:: 0.7.4 - - """ - - __visit_name__ = "drop_schema" - - def __init__(self, name, quote=None, cascade=False, **kw): - """Create a new :class:`.DropSchema` construct.""" - - self.quote = quote - self.cascade = cascade - super(DropSchema, self).__init__(name, **kw) - - -class CreateTable(_CreateDropBase): - """Represent a CREATE TABLE statement.""" - - __visit_name__ = "create_table" - - def __init__(self, element, on=None, bind=None): - """Create a :class:`.CreateTable` construct. - - :param element: a :class:`.Table` that's the subject - of the CREATE - :param on: See the description for 'on' in :class:`.DDL`. - :param bind: See the description for 'bind' in :class:`.DDL`. - - """ - super(CreateTable, self).__init__(element, on=on, bind=bind) - self.columns = [CreateColumn(column) - for column in element.columns - ] - - -class _DropView(_CreateDropBase): - """Semi-public 'DROP VIEW' construct. - - Used by the test suite for dialect-agnostic drops of views. - This object will eventually be part of a public "view" API. - - """ - __visit_name__ = "drop_view" - - -class CreateColumn(_DDLCompiles): - """Represent a :class:`.Column` as rendered in a CREATE TABLE statement, - via the :class:`.CreateTable` construct. - - This is provided to support custom column DDL within the generation - of CREATE TABLE statements, by using the - compiler extension documented in :ref:`sqlalchemy.ext.compiler_toplevel` - to extend :class:`.CreateColumn`. - - Typical integration is to examine the incoming :class:`.Column` - object, and to redirect compilation if a particular flag or condition - is found:: - - from sqlalchemy import schema - from sqlalchemy.ext.compiler import compiles - - @compiles(schema.CreateColumn) - def compile(element, compiler, **kw): - column = element.element - - if "special" not in column.info: - return compiler.visit_create_column(element, **kw) - - text = "%s SPECIAL DIRECTIVE %s" % ( - column.name, - compiler.type_compiler.process(column.type) - ) - default = compiler.get_column_default_string(column) - if default is not None: - text += " DEFAULT " + default - - if not column.nullable: - text += " NOT NULL" - - if column.constraints: - text += " ".join( - compiler.process(const) - for const in column.constraints) - return text - - The above construct can be applied to a :class:`.Table` as follows:: - - from sqlalchemy import Table, Metadata, Column, Integer, String - from sqlalchemy import schema - - metadata = MetaData() - - table = Table('mytable', MetaData(), - Column('x', Integer, info={"special":True}, primary_key=True), - Column('y', String(50)), - Column('z', String(20), info={"special":True}) - ) - - metadata.create_all(conn) - - Above, the directives we've added to the :attr:`.Column.info` collection - will be detected by our custom compilation scheme:: - - CREATE TABLE mytable ( - x SPECIAL DIRECTIVE INTEGER NOT NULL, - y VARCHAR(50), - z SPECIAL DIRECTIVE VARCHAR(20), - PRIMARY KEY (x) - ) - - The :class:`.CreateColumn` construct can also be used to skip certain - columns when producing a ``CREATE TABLE``. This is accomplished by - creating a compilation rule that conditionally returns ``None``. - This is essentially how to produce the same effect as using the - ``system=True`` argument on :class:`.Column`, which marks a column - as an implicitly-present "system" column. - - For example, suppose we wish to produce a :class:`.Table` which skips - rendering of the Postgresql ``xmin`` column against the Postgresql backend, - but on other backends does render it, in anticipation of a triggered rule. - A conditional compilation rule could skip this name only on Postgresql:: - - from sqlalchemy.schema import CreateColumn - - @compiles(CreateColumn, "postgresql") - def skip_xmin(element, compiler, **kw): - if element.element.name == 'xmin': - return None - else: - return compiler.visit_create_column(element, **kw) - - - my_table = Table('mytable', metadata, - Column('id', Integer, primary_key=True), - Column('xmin', Integer) - ) - - Above, a :class:`.CreateTable` construct will generate a ``CREATE TABLE`` - which only includes the ``id`` column in the string; the ``xmin`` column - will be omitted, but only against the Postgresql backend. - - .. versionadded:: 0.8.3 The :class:`.CreateColumn` construct supports - skipping of columns by returning ``None`` from a custom compilation rule. - - .. versionadded:: 0.8 The :class:`.CreateColumn` construct was added - to support custom column creation styles. - - """ - __visit_name__ = 'create_column' - - def __init__(self, element): - self.element = element - - -class DropTable(_CreateDropBase): - """Represent a DROP TABLE statement.""" - - __visit_name__ = "drop_table" - - -class CreateSequence(_CreateDropBase): - """Represent a CREATE SEQUENCE statement.""" - - __visit_name__ = "create_sequence" - - -class DropSequence(_CreateDropBase): - """Represent a DROP SEQUENCE statement.""" - - __visit_name__ = "drop_sequence" - - -class CreateIndex(_CreateDropBase): - """Represent a CREATE INDEX statement.""" - - __visit_name__ = "create_index" - - -class DropIndex(_CreateDropBase): - """Represent a DROP INDEX statement.""" - - __visit_name__ = "drop_index" - - -class AddConstraint(_CreateDropBase): - """Represent an ALTER TABLE ADD CONSTRAINT statement.""" - - __visit_name__ = "add_constraint" - - def __init__(self, element, *args, **kw): - super(AddConstraint, self).__init__(element, *args, **kw) - element._create_rule = util.portable_instancemethod( - self._create_rule_disable) - - -class DropConstraint(_CreateDropBase): - """Represent an ALTER TABLE DROP CONSTRAINT statement.""" - - __visit_name__ = "drop_constraint" - - def __init__(self, element, cascade=False, **kw): - self.cascade = cascade - super(DropConstraint, self).__init__(element, **kw) - element._create_rule = util.portable_instancemethod( - self._create_rule_disable) - - -class DDLBase(SchemaVisitor): - def __init__(self, connection): - self.connection = connection - - -class SchemaGenerator(DDLBase): - - def __init__(self, dialect, connection, checkfirst=False, - tables=None, **kwargs): - super(SchemaGenerator, self).__init__(connection, **kwargs) - self.checkfirst = checkfirst - self.tables = tables - self.preparer = dialect.identifier_preparer - self.dialect = dialect - self.memo = {} - - def _can_create_table(self, table): - self.dialect.validate_identifier(table.name) - if table.schema: - self.dialect.validate_identifier(table.schema) - return not self.checkfirst or \ - not self.dialect.has_table(self.connection, - table.name, schema=table.schema) - - def _can_create_sequence(self, sequence): - return self.dialect.supports_sequences and \ - ( - (not self.dialect.sequences_optional or - not sequence.optional) and - ( - not self.checkfirst or - not self.dialect.has_sequence( - self.connection, - sequence.name, - schema=sequence.schema) - ) - ) - - def visit_metadata(self, metadata): - if self.tables is not None: - tables = self.tables - else: - tables = list(metadata.tables.values()) - collection = [t for t in sort_tables(tables) - if self._can_create_table(t)] - seq_coll = [s for s in metadata._sequences.values() - if s.column is None and self._can_create_sequence(s)] - - metadata.dispatch.before_create(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for seq in seq_coll: - self.traverse_single(seq, create_ok=True) - - for table in collection: - self.traverse_single(table, create_ok=True) - - metadata.dispatch.after_create(metadata, self.connection, - tables=collection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_table(self, table, create_ok=False): - if not create_ok and not self._can_create_table(table): - return - - table.dispatch.before_create(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for column in table.columns: - if column.default is not None: - self.traverse_single(column.default) - - self.connection.execute(CreateTable(table)) - - if hasattr(table, 'indexes'): - for index in table.indexes: - self.traverse_single(index) - - table.dispatch.after_create(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_sequence(self, sequence, create_ok=False): - if not create_ok and not self._can_create_sequence(sequence): - return - self.connection.execute(CreateSequence(sequence)) - - def visit_index(self, index): - self.connection.execute(CreateIndex(index)) - - -class SchemaDropper(DDLBase): - - def __init__(self, dialect, connection, checkfirst=False, - tables=None, **kwargs): - super(SchemaDropper, self).__init__(connection, **kwargs) - self.checkfirst = checkfirst - self.tables = tables - self.preparer = dialect.identifier_preparer - self.dialect = dialect - self.memo = {} - - def visit_metadata(self, metadata): - if self.tables is not None: - tables = self.tables - else: - tables = list(metadata.tables.values()) - - collection = [ - t - for t in reversed(sort_tables(tables)) - if self._can_drop_table(t) - ] - - seq_coll = [ - s - for s in metadata._sequences.values() - if s.column is None and self._can_drop_sequence(s) - ] - - metadata.dispatch.before_drop( - metadata, self.connection, tables=collection, - checkfirst=self.checkfirst, _ddl_runner=self) - - for table in collection: - self.traverse_single(table, drop_ok=True) - - for seq in seq_coll: - self.traverse_single(seq, drop_ok=True) - - metadata.dispatch.after_drop( - metadata, self.connection, tables=collection, - checkfirst=self.checkfirst, _ddl_runner=self) - - def _can_drop_table(self, table): - self.dialect.validate_identifier(table.name) - if table.schema: - self.dialect.validate_identifier(table.schema) - return not self.checkfirst or self.dialect.has_table(self.connection, - table.name, schema=table.schema) - - def _can_drop_sequence(self, sequence): - return self.dialect.supports_sequences and \ - ((not self.dialect.sequences_optional or - not sequence.optional) and - (not self.checkfirst or - self.dialect.has_sequence( - self.connection, - sequence.name, - schema=sequence.schema)) - ) - - def visit_index(self, index): - self.connection.execute(DropIndex(index)) - - def visit_table(self, table, drop_ok=False): - if not drop_ok and not self._can_drop_table(table): - return - - table.dispatch.before_drop(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - for column in table.columns: - if column.default is not None: - self.traverse_single(column.default) - - self.connection.execute(DropTable(table)) - - table.dispatch.after_drop(table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self) - - def visit_sequence(self, sequence, drop_ok=False): - if not drop_ok and not self._can_drop_sequence(sequence): - return - self.connection.execute(DropSequence(sequence)) - -def sort_tables(tables, skip_fn=None, extra_dependencies=None): - """sort a collection of Table objects in order of - their foreign-key dependency.""" - - tables = list(tables) - tuples = [] - if extra_dependencies is not None: - tuples.extend(extra_dependencies) - - def visit_foreign_key(fkey): - if fkey.use_alter: - return - elif skip_fn and skip_fn(fkey): - return - parent_table = fkey.column.table - if parent_table in tables: - child_table = fkey.parent.table - if parent_table is not child_table: - tuples.append((parent_table, child_table)) - - for table in tables: - traverse(table, - {'schema_visitor': True}, - {'foreign_key': visit_foreign_key}) - - tuples.extend( - [parent, table] for parent in table._extra_dependencies - ) - - return list(topological.sort(tuples, tables)) - diff --git a/libs/sqlalchemy/sql/default_comparator.py b/libs/sqlalchemy/sql/default_comparator.py deleted file mode 100644 index c39dce9c..00000000 --- a/libs/sqlalchemy/sql/default_comparator.py +++ /dev/null @@ -1,278 +0,0 @@ -# sql/default_comparator.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Default implementation of SQL comparison operations. -""" - -from .. import exc, util -from . import operators -from . import type_api -from .elements import BindParameter, True_, False_, BinaryExpression, \ - Null, _const_expr, _clause_element_as_expr, \ - ClauseList, ColumnElement, TextClause, UnaryExpression, \ - collate, _is_literal, _literal_as_text -from .selectable import SelectBase, Alias, Selectable, ScalarSelect - -class _DefaultColumnComparator(operators.ColumnOperators): - """Defines comparison and math operations. - - See :class:`.ColumnOperators` and :class:`.Operators` for descriptions - of all operations. - - """ - - @util.memoized_property - def type(self): - return self.expr.type - - def operate(self, op, *other, **kwargs): - o = self.operators[op.__name__] - return o[0](self, self.expr, op, *(other + o[1:]), **kwargs) - - def reverse_operate(self, op, other, **kwargs): - o = self.operators[op.__name__] - return o[0](self, self.expr, op, other, reverse=True, *o[1:], **kwargs) - - def _adapt_expression(self, op, other_comparator): - """evaluate the return type of , - and apply any adaptations to the given operator. - - This method determines the type of a resulting binary expression - given two source types and an operator. For example, two - :class:`.Column` objects, both of the type :class:`.Integer`, will - produce a :class:`.BinaryExpression` that also has the type - :class:`.Integer` when compared via the addition (``+``) operator. - However, using the addition operator with an :class:`.Integer` - and a :class:`.Date` object will produce a :class:`.Date`, assuming - "days delta" behavior by the database (in reality, most databases - other than Postgresql don't accept this particular operation). - - The method returns a tuple of the form , . - The resulting operator and type will be those applied to the - resulting :class:`.BinaryExpression` as the final operator and the - right-hand side of the expression. - - Note that only a subset of operators make usage of - :meth:`._adapt_expression`, - including math operators and user-defined operators, but not - boolean comparison or special SQL keywords like MATCH or BETWEEN. - - """ - return op, other_comparator.type - - def _boolean_compare(self, expr, op, obj, negate=None, reverse=False, - _python_is_types=(util.NoneType, bool), - **kwargs): - - if isinstance(obj, _python_is_types + (Null, True_, False_)): - - # allow x ==/!= True/False to be treated as a literal. - # this comes out to "== / != true/false" or "1/0" if those - # constants aren't supported and works on all platforms - if op in (operators.eq, operators.ne) and \ - isinstance(obj, (bool, True_, False_)): - return BinaryExpression(expr, - _literal_as_text(obj), - op, - type_=type_api.BOOLEANTYPE, - negate=negate, modifiers=kwargs) - else: - # all other None/True/False uses IS, IS NOT - if op in (operators.eq, operators.is_): - return BinaryExpression(expr, _const_expr(obj), - operators.is_, - negate=operators.isnot) - elif op in (operators.ne, operators.isnot): - return BinaryExpression(expr, _const_expr(obj), - operators.isnot, - negate=operators.is_) - else: - raise exc.ArgumentError( - "Only '=', '!=', 'is_()', 'isnot()' operators can " - "be used with None/True/False") - else: - obj = self._check_literal(expr, op, obj) - - if reverse: - return BinaryExpression(obj, - expr, - op, - type_=type_api.BOOLEANTYPE, - negate=negate, modifiers=kwargs) - else: - return BinaryExpression(expr, - obj, - op, - type_=type_api.BOOLEANTYPE, - negate=negate, modifiers=kwargs) - - def _binary_operate(self, expr, op, obj, reverse=False, result_type=None, - **kw): - obj = self._check_literal(expr, op, obj) - - if reverse: - left, right = obj, expr - else: - left, right = expr, obj - - if result_type is None: - op, result_type = left.comparator._adapt_expression( - op, right.comparator) - - return BinaryExpression(left, right, op, type_=result_type) - - def _scalar(self, expr, op, fn, **kw): - return fn(expr) - - def _in_impl(self, expr, op, seq_or_selectable, negate_op, **kw): - seq_or_selectable = _clause_element_as_expr(seq_or_selectable) - - if isinstance(seq_or_selectable, ScalarSelect): - return self._boolean_compare(expr, op, seq_or_selectable, - negate=negate_op) - elif isinstance(seq_or_selectable, SelectBase): - - # TODO: if we ever want to support (x, y, z) IN (select x, - # y, z from table), we would need a multi-column version of - # as_scalar() to produce a multi- column selectable that - # does not export itself as a FROM clause - - return self._boolean_compare( - expr, op, seq_or_selectable.as_scalar(), - negate=negate_op, **kw) - elif isinstance(seq_or_selectable, (Selectable, TextClause)): - return self._boolean_compare(expr, op, seq_or_selectable, - negate=negate_op, **kw) - - # Handle non selectable arguments as sequences - args = [] - for o in seq_or_selectable: - if not _is_literal(o): - if not isinstance(o, operators.ColumnOperators): - raise exc.InvalidRequestError('in() function accept' - 's either a list of non-selectable values, ' - 'or a selectable: %r' % o) - elif o is None: - o = Null() - else: - o = expr._bind_param(op, o) - args.append(o) - if len(args) == 0: - - # Special case handling for empty IN's, behave like - # comparison against zero row selectable. We use != to - # build the contradiction as it handles NULL values - # appropriately, i.e. "not (x IN ())" should not return NULL - # values for x. - - util.warn('The IN-predicate on "%s" was invoked with an ' - 'empty sequence. This results in a ' - 'contradiction, which nonetheless can be ' - 'expensive to evaluate. Consider alternative ' - 'strategies for improved performance.' % expr) - if op is operators.in_op: - return expr != expr - else: - return expr == expr - - return self._boolean_compare(expr, op, - ClauseList(*args).self_group(against=op), - negate=negate_op) - - def _unsupported_impl(self, expr, op, *arg, **kw): - raise NotImplementedError("Operator '%s' is not supported on " - "this expression" % op.__name__) - - def _neg_impl(self, expr, op, **kw): - """See :meth:`.ColumnOperators.__neg__`.""" - return UnaryExpression(expr, operator=operators.neg) - - def _match_impl(self, expr, op, other, **kw): - """See :meth:`.ColumnOperators.match`.""" - return self._boolean_compare(expr, operators.match_op, - self._check_literal(expr, operators.match_op, - other)) - - def _distinct_impl(self, expr, op, **kw): - """See :meth:`.ColumnOperators.distinct`.""" - return UnaryExpression(expr, operator=operators.distinct_op, - type_=expr.type) - - def _between_impl(self, expr, op, cleft, cright, **kw): - """See :meth:`.ColumnOperators.between`.""" - return BinaryExpression( - expr, - ClauseList( - self._check_literal(expr, operators.and_, cleft), - self._check_literal(expr, operators.and_, cright), - operator=operators.and_, - group=False, group_contents=False), - operators.between_op) - - def _collate_impl(self, expr, op, other, **kw): - return collate(expr, other) - - # a mapping of operators with the method they use, along with - # their negated operator for comparison operators - operators = { - "add": (_binary_operate,), - "mul": (_binary_operate,), - "sub": (_binary_operate,), - "div": (_binary_operate,), - "mod": (_binary_operate,), - "truediv": (_binary_operate,), - "custom_op": (_binary_operate,), - "concat_op": (_binary_operate,), - "lt": (_boolean_compare, operators.ge), - "le": (_boolean_compare, operators.gt), - "ne": (_boolean_compare, operators.eq), - "gt": (_boolean_compare, operators.le), - "ge": (_boolean_compare, operators.lt), - "eq": (_boolean_compare, operators.ne), - "like_op": (_boolean_compare, operators.notlike_op), - "ilike_op": (_boolean_compare, operators.notilike_op), - "notlike_op": (_boolean_compare, operators.like_op), - "notilike_op": (_boolean_compare, operators.ilike_op), - "contains_op": (_boolean_compare, operators.notcontains_op), - "startswith_op": (_boolean_compare, operators.notstartswith_op), - "endswith_op": (_boolean_compare, operators.notendswith_op), - "desc_op": (_scalar, UnaryExpression._create_desc), - "asc_op": (_scalar, UnaryExpression._create_asc), - "nullsfirst_op": (_scalar, UnaryExpression._create_nullsfirst), - "nullslast_op": (_scalar, UnaryExpression._create_nullslast), - "in_op": (_in_impl, operators.notin_op), - "notin_op": (_in_impl, operators.in_op), - "is_": (_boolean_compare, operators.is_), - "isnot": (_boolean_compare, operators.isnot), - "collate": (_collate_impl,), - "match_op": (_match_impl,), - "distinct_op": (_distinct_impl,), - "between_op": (_between_impl, ), - "neg": (_neg_impl,), - "getitem": (_unsupported_impl,), - "lshift": (_unsupported_impl,), - "rshift": (_unsupported_impl,), - } - - def _check_literal(self, expr, operator, other): - if isinstance(other, (ColumnElement, TextClause)): - if isinstance(other, BindParameter) and \ - other.type._isnull: - other = other._clone() - other.type = expr.type - return other - elif hasattr(other, '__clause_element__'): - other = other.__clause_element__() - elif isinstance(other, type_api.TypeEngine.Comparator): - other = other.expr - - if isinstance(other, (SelectBase, Alias)): - return other.as_scalar() - elif not isinstance(other, (ColumnElement, TextClause)): - return expr._bind_param(operator, other) - else: - return other - diff --git a/libs/sqlalchemy/sql/dml.py b/libs/sqlalchemy/sql/dml.py deleted file mode 100644 index 22694348..00000000 --- a/libs/sqlalchemy/sql/dml.py +++ /dev/null @@ -1,770 +0,0 @@ -# sql/dml.py -# Copyright (C) 2009-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -""" -Provide :class:`.Insert`, :class:`.Update` and :class:`.Delete`. - -""" - -from .base import Executable, _generative, _from_objects -from .elements import ClauseElement, _literal_as_text, Null, and_, _clone -from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes -from .. import util -from .. import exc - -class UpdateBase(HasPrefixes, Executable, ClauseElement): - """Form the base for ``INSERT``, ``UPDATE``, and ``DELETE`` statements. - - """ - - __visit_name__ = 'update_base' - - _execution_options = \ - Executable._execution_options.union({'autocommit': True}) - kwargs = util.immutabledict() - _hints = util.immutabledict() - _prefixes = () - - def _process_colparams(self, parameters): - def process_single(p): - if isinstance(p, (list, tuple)): - return dict( - (c.key, pval) - for c, pval in zip(self.table.c, p) - ) - else: - return p - - if isinstance(parameters, (list, tuple)) and \ - isinstance(parameters[0], (list, tuple, dict)): - - if not self._supports_multi_parameters: - raise exc.InvalidRequestError( - "This construct does not support " - "multiple parameter sets.") - - return [process_single(p) for p in parameters], True - else: - return process_single(parameters), False - - def params(self, *arg, **kw): - """Set the parameters for the statement. - - This method raises ``NotImplementedError`` on the base class, - and is overridden by :class:`.ValuesBase` to provide the - SET/VALUES clause of UPDATE and INSERT. - - """ - raise NotImplementedError( - "params() is not supported for INSERT/UPDATE/DELETE statements." - " To set the values for an INSERT or UPDATE statement, use" - " stmt.values(**parameters).") - - def bind(self): - """Return a 'bind' linked to this :class:`.UpdateBase` - or a :class:`.Table` associated with it. - - """ - return self._bind or self.table.bind - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - @_generative - def returning(self, *cols): - """Add a :term:`RETURNING` or equivalent clause to this statement. - - e.g.:: - - stmt = table.update().\\ - where(table.c.data == 'value').\\ - values(status='X').\\ - returning(table.c.server_flag, table.c.updated_timestamp) - - for server_flag, updated_timestamp in connection.execute(stmt): - print(server_flag, updated_timestamp) - - The given collection of column expressions should be derived from - the table that is - the target of the INSERT, UPDATE, or DELETE. While :class:`.Column` - objects are typical, the elements can also be expressions:: - - stmt = table.insert().returning( - (table.c.first_name + " " + table.c.last_name).label('fullname') - ) - - Upon compilation, a RETURNING clause, or database equivalent, - will be rendered within the statement. For INSERT and UPDATE, - the values are the newly inserted/updated values. For DELETE, - the values are those of the rows which were deleted. - - Upon execution, the values of the columns to be returned - are made available via the result set and can be iterated - using :meth:`.ResultProxy.fetchone` and similar. For DBAPIs which do not - natively support returning values (i.e. cx_oracle), - SQLAlchemy will approximate this behavior at the result level - so that a reasonable amount of behavioral neutrality is - provided. - - Note that not all databases/DBAPIs - support RETURNING. For those backends with no support, - an exception is raised upon compilation and/or execution. - For those who do support it, the functionality across backends - varies greatly, including restrictions on executemany() - and other statements which return multiple rows. Please - read the documentation notes for the database in use in - order to determine the availability of RETURNING. - - .. seealso:: - - :meth:`.ValuesBase.return_defaults` - an alternative method tailored - towards efficient fetching of server-side defaults and triggers - for single-row INSERTs or UPDATEs. - - - """ - self._returning = cols - - - @_generative - def with_hint(self, text, selectable=None, dialect_name="*"): - """Add a table hint for a single table to this - INSERT/UPDATE/DELETE statement. - - .. note:: - - :meth:`.UpdateBase.with_hint` currently applies only to - Microsoft SQL Server. For MySQL INSERT/UPDATE/DELETE hints, use - :meth:`.UpdateBase.prefix_with`. - - The text of the hint is rendered in the appropriate - location for the database backend in use, relative - to the :class:`.Table` that is the subject of this - statement, or optionally to that of the given - :class:`.Table` passed as the ``selectable`` argument. - - The ``dialect_name`` option will limit the rendering of a particular - hint to a particular backend. Such as, to add a hint - that only takes effect for SQL Server:: - - mytable.insert().with_hint("WITH (PAGLOCK)", dialect_name="mssql") - - .. versionadded:: 0.7.6 - - :param text: Text of the hint. - :param selectable: optional :class:`.Table` that specifies - an element of the FROM clause within an UPDATE or DELETE - to be the subject of the hint - applies only to certain backends. - :param dialect_name: defaults to ``*``, if specified as the name - of a particular dialect, will apply these hints only when - that dialect is in use. - """ - if selectable is None: - selectable = self.table - - self._hints = self._hints.union( - {(selectable, dialect_name): text}) - - -class ValuesBase(UpdateBase): - """Supplies support for :meth:`.ValuesBase.values` to - INSERT and UPDATE constructs.""" - - __visit_name__ = 'values_base' - - _supports_multi_parameters = False - _has_multi_parameters = False - select = None - - def __init__(self, table, values, prefixes): - self.table = _interpret_as_from(table) - self.parameters, self._has_multi_parameters = \ - self._process_colparams(values) - if prefixes: - self._setup_prefixes(prefixes) - - @_generative - def values(self, *args, **kwargs): - """specify a fixed VALUES clause for an INSERT statement, or the SET - clause for an UPDATE. - - Note that the :class:`.Insert` and :class:`.Update` constructs support - per-execution time formatting of the VALUES and/or SET clauses, - based on the arguments passed to :meth:`.Connection.execute`. However, - the :meth:`.ValuesBase.values` method can be used to "fix" a particular - set of parameters into the statement. - - Multiple calls to :meth:`.ValuesBase.values` will produce a new - construct, each one with the parameter list modified to include - the new parameters sent. In the typical case of a single - dictionary of parameters, the newly passed keys will replace - the same keys in the previous construct. In the case of a list-based - "multiple values" construct, each new list of values is extended - onto the existing list of values. - - :param \**kwargs: key value pairs representing the string key - of a :class:`.Column` mapped to the value to be rendered into the - VALUES or SET clause:: - - users.insert().values(name="some name") - - users.update().where(users.c.id==5).values(name="some name") - - :param \*args: Alternatively, a dictionary, tuple or list - of dictionaries or tuples can be passed as a single positional - argument in order to form the VALUES or - SET clause of the statement. The single dictionary form - works the same as the kwargs form:: - - users.insert().values({"name": "some name"}) - - If a tuple is passed, the tuple should contain the same number - of columns as the target :class:`.Table`:: - - users.insert().values((5, "some name")) - - The :class:`.Insert` construct also supports multiply-rendered VALUES - construct, for those backends which support this SQL syntax - (SQLite, Postgresql, MySQL). This mode is indicated by passing a list - of one or more dictionaries/tuples:: - - users.insert().values([ - {"name": "some name"}, - {"name": "some other name"}, - {"name": "yet another name"}, - ]) - - In the case of an :class:`.Update` - construct, only the single dictionary/tuple form is accepted, - else an exception is raised. It is also an exception case to - attempt to mix the single-/multiple- value styles together, - either through multiple :meth:`.ValuesBase.values` calls - or by sending a list + kwargs at the same time. - - .. note:: - - Passing a multiple values list is *not* the same - as passing a multiple values list to the :meth:`.Connection.execute` - method. Passing a list of parameter sets to :meth:`.ValuesBase.values` - produces a construct of this form:: - - INSERT INTO table (col1, col2, col3) VALUES - (col1_0, col2_0, col3_0), - (col1_1, col2_1, col3_1), - ... - - whereas a multiple list passed to :meth:`.Connection.execute` - has the effect of using the DBAPI - `executemany() `_ - method, which provides a high-performance system of invoking - a single-row INSERT statement many times against a series - of parameter sets. The "executemany" style is supported by - all database backends, as it does not depend on a special SQL - syntax. - - .. versionadded:: 0.8 - Support for multiple-VALUES INSERT statements. - - - .. seealso:: - - :ref:`inserts_and_updates` - SQL Expression - Language Tutorial - - :func:`~.expression.insert` - produce an ``INSERT`` statement - - :func:`~.expression.update` - produce an ``UPDATE`` statement - - """ - if self.select is not None: - raise exc.InvalidRequestError( - "This construct already inserts from a SELECT") - if self._has_multi_parameters and kwargs: - raise exc.InvalidRequestError( - "This construct already has multiple parameter sets.") - - if args: - if len(args) > 1: - raise exc.ArgumentError( - "Only a single dictionary/tuple or list of " - "dictionaries/tuples is accepted positionally.") - v = args[0] - else: - v = {} - - if self.parameters is None: - self.parameters, self._has_multi_parameters = \ - self._process_colparams(v) - else: - if self._has_multi_parameters: - self.parameters = list(self.parameters) - p, self._has_multi_parameters = self._process_colparams(v) - if not self._has_multi_parameters: - raise exc.ArgumentError( - "Can't mix single-values and multiple values " - "formats in one statement") - - self.parameters.extend(p) - else: - self.parameters = self.parameters.copy() - p, self._has_multi_parameters = self._process_colparams(v) - if self._has_multi_parameters: - raise exc.ArgumentError( - "Can't mix single-values and multiple values " - "formats in one statement") - self.parameters.update(p) - - if kwargs: - if self._has_multi_parameters: - raise exc.ArgumentError( - "Can't pass kwargs and multiple parameter sets " - "simultaenously") - else: - self.parameters.update(kwargs) - - @_generative - def return_defaults(self, *cols): - """Make use of a :term:`RETURNING` clause for the purpose - of fetching server-side expressions and defaults. - - E.g.:: - - stmt = table.insert().values(data='newdata').return_defaults() - - result = connection.execute(stmt) - - server_created_at = result.returned_defaults['created_at'] - - When used against a backend that supports RETURNING, all column - values generated by SQL expression or server-side-default will be added - to any existing RETURNING clause, provided that - :meth:`.UpdateBase.returning` is not used simultaneously. The column values - will then be available on the result using the - :attr:`.ResultProxy.returned_defaults` accessor as a - dictionary, referring to values keyed to the :class:`.Column` object - as well as its ``.key``. - - This method differs from :meth:`.UpdateBase.returning` in these ways: - - 1. :meth:`.ValuesBase.return_defaults` is only intended for use with - an INSERT or an UPDATE statement that matches exactly one row. - While the RETURNING construct in the general sense supports multiple - rows for a multi-row UPDATE or DELETE statement, or for special - cases of INSERT that return multiple rows (e.g. INSERT from SELECT, - multi-valued VALUES clause), :meth:`.ValuesBase.return_defaults` - is intended only - for an "ORM-style" single-row INSERT/UPDATE statement. The row - returned by the statement is also consumed implcitly when - :meth:`.ValuesBase.return_defaults` is used. By contrast, - :meth:`.UpdateBase.returning` leaves the RETURNING result-set intact - with a collection of any number of rows. - - 2. It is compatible with the existing logic to fetch auto-generated - primary key values, also known as "implicit returning". Backends that - support RETURNING will automatically make use of RETURNING in order - to fetch the value of newly generated primary keys; while the - :meth:`.UpdateBase.returning` method circumvents this behavior, - :meth:`.ValuesBase.return_defaults` leaves it intact. - - 3. It can be called against any backend. Backends that don't support - RETURNING will skip the usage of the feature, rather than raising - an exception. The return value of :attr:`.ResultProxy.returned_defaults` - will be ``None`` - - :meth:`.ValuesBase.return_defaults` is used by the ORM to provide - an efficient implementation for the ``eager_defaults`` feature of - :func:`.mapper`. - - :param cols: optional list of column key names or :class:`.Column` - objects. If omitted, all column expressions evaulated on the server - are added to the returning list. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.UpdateBase.returning` - - :attr:`.ResultProxy.returned_defaults` - - """ - self._return_defaults = cols or True - - -class Insert(ValuesBase): - """Represent an INSERT construct. - - The :class:`.Insert` object is created using the - :func:`~.expression.insert()` function. - - .. seealso:: - - :ref:`coretutorial_insert_expressions` - - """ - __visit_name__ = 'insert' - - _supports_multi_parameters = True - - def __init__(self, - table, - values=None, - inline=False, - bind=None, - prefixes=None, - returning=None, - return_defaults=False, - **kwargs): - """Construct an :class:`.Insert` object. - - Similar functionality is available via the - :meth:`~.TableClause.insert` method on - :class:`~.schema.Table`. - - :param table: :class:`.TableClause` which is the subject of the insert. - - :param values: collection of values to be inserted; see - :meth:`.Insert.values` for a description of allowed formats here. - Can be omitted entirely; a :class:`.Insert` construct will also - dynamically render the VALUES clause at execution time based on - the parameters passed to :meth:`.Connection.execute`. - - :param inline: if True, SQL defaults will be compiled 'inline' into the - statement and not pre-executed. - - If both `values` and compile-time bind parameters are present, the - compile-time bind parameters override the information specified - within `values` on a per-key basis. - - The keys within `values` can be either :class:`~sqlalchemy.schema.Column` - objects or their string identifiers. Each key may reference one of: - - * a literal data value (i.e. string, number, etc.); - * a Column object; - * a SELECT statement. - - If a ``SELECT`` statement is specified which references this - ``INSERT`` statement's table, the statement will be correlated - against the ``INSERT`` statement. - - .. seealso:: - - :ref:`coretutorial_insert_expressions` - SQL Expression Tutorial - - :ref:`inserts_and_updates` - SQL Expression Tutorial - - """ - ValuesBase.__init__(self, table, values, prefixes) - self._bind = bind - self.select = self.select_names = None - self.inline = inline - self._returning = returning - self.kwargs = kwargs - self._return_defaults = return_defaults - - def get_children(self, **kwargs): - if self.select is not None: - return self.select, - else: - return () - - @_generative - def from_select(self, names, select): - """Return a new :class:`.Insert` construct which represents - an ``INSERT...FROM SELECT`` statement. - - e.g.:: - - sel = select([table1.c.a, table1.c.b]).where(table1.c.c > 5) - ins = table2.insert().from_select(['a', 'b'], sel) - - :param names: a sequence of string column names or :class:`.Column` - objects representing the target columns. - :param select: a :func:`.select` construct, :class:`.FromClause` - or other construct which resolves into a :class:`.FromClause`, - such as an ORM :class:`.Query` object, etc. The order of - columns returned from this FROM clause should correspond to the - order of columns sent as the ``names`` parameter; while this - is not checked before passing along to the database, the database - would normally raise an exception if these column lists don't - correspond. - - .. note:: - - Depending on backend, it may be necessary for the :class:`.Insert` - statement to be constructed using the ``inline=True`` flag; this - flag will prevent the implicit usage of ``RETURNING`` when the - ``INSERT`` statement is rendered, which isn't supported on a backend - such as Oracle in conjunction with an ``INSERT..SELECT`` combination:: - - sel = select([table1.c.a, table1.c.b]).where(table1.c.c > 5) - ins = table2.insert(inline=True).from_select(['a', 'b'], sel) - - .. note:: - - A SELECT..INSERT construct in SQL has no VALUES clause. Therefore - :class:`.Column` objects which utilize Python-side defaults - (e.g. as described at :ref:`metadata_defaults_toplevel`) - will **not** take effect when using :meth:`.Insert.from_select`. - - .. versionadded:: 0.8.3 - - """ - if self.parameters: - raise exc.InvalidRequestError( - "This construct already inserts value expressions") - - self.parameters, self._has_multi_parameters = \ - self._process_colparams(dict((n, Null()) for n in names)) - - self.select_names = names - self.select = _interpret_as_select(select) - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self.parameters = self.parameters.copy() - if self.select is not None: - self.select = _clone(self.select) - - -class Update(ValuesBase): - """Represent an Update construct. - - The :class:`.Update` object is created using the :func:`update()` function. - - """ - __visit_name__ = 'update' - - def __init__(self, - table, - whereclause=None, - values=None, - inline=False, - bind=None, - prefixes=None, - returning=None, - return_defaults=False, - **kwargs): - """Construct an :class:`.Update` object. - - E.g.:: - - from sqlalchemy import update - - stmt = update(users).where(users.c.id==5).\\ - values(name='user #5') - - Similar functionality is available via the - :meth:`~.TableClause.update` method on - :class:`.Table`:: - - stmt = users.update().\\ - where(users.c.id==5).\\ - values(name='user #5') - - :param table: A :class:`.Table` object representing the database - table to be updated. - - :param whereclause: Optional SQL expression describing the ``WHERE`` - condition of the ``UPDATE`` statement. Modern applications - may prefer to use the generative :meth:`~Update.where()` - method to specify the ``WHERE`` clause. - - The WHERE clause can refer to multiple tables. - For databases which support this, an ``UPDATE FROM`` clause will - be generated, or on MySQL, a multi-table update. The statement - will fail on databases that don't have support for multi-table - update statements. A SQL-standard method of referring to - additional tables in the WHERE clause is to use a correlated - subquery:: - - users.update().values(name='ed').where( - users.c.name==select([addresses.c.email_address]).\\ - where(addresses.c.user_id==users.c.id).\\ - as_scalar() - ) - - .. versionchanged:: 0.7.4 - The WHERE clause can refer to multiple tables. - - :param values: - Optional dictionary which specifies the ``SET`` conditions of the - ``UPDATE``. If left as ``None``, the ``SET`` - conditions are determined from those parameters passed to the - statement during the execution and/or compilation of the - statement. When compiled standalone without any parameters, - the ``SET`` clause generates for all columns. - - Modern applications may prefer to use the generative - :meth:`.Update.values` method to set the values of the - UPDATE statement. - - :param inline: - if True, SQL defaults present on :class:`.Column` objects via - the ``default`` keyword will be compiled 'inline' into the statement - and not pre-executed. This means that their values will not - be available in the dictionary returned from - :meth:`.ResultProxy.last_updated_params`. - - If both ``values`` and compile-time bind parameters are present, the - compile-time bind parameters override the information specified - within ``values`` on a per-key basis. - - The keys within ``values`` can be either :class:`.Column` - objects or their string identifiers (specifically the "key" of the - :class:`.Column`, normally but not necessarily equivalent to - its "name"). Normally, the - :class:`.Column` objects used here are expected to be - part of the target :class:`.Table` that is the table - to be updated. However when using MySQL, a multiple-table - UPDATE statement can refer to columns from any of - the tables referred to in the WHERE clause. - - The values referred to in ``values`` are typically: - - * a literal data value (i.e. string, number, etc.) - * a SQL expression, such as a related :class:`.Column`, - a scalar-returning :func:`.select` construct, - etc. - - When combining :func:`.select` constructs within the values - clause of an :func:`.update` construct, - the subquery represented by the :func:`.select` should be - *correlated* to the parent table, that is, providing criterion - which links the table inside the subquery to the outer table - being updated:: - - users.update().values( - name=select([addresses.c.email_address]).\\ - where(addresses.c.user_id==users.c.id).\\ - as_scalar() - ) - - .. seealso:: - - :ref:`inserts_and_updates` - SQL Expression - Language Tutorial - - - """ - ValuesBase.__init__(self, table, values, prefixes) - self._bind = bind - self._returning = returning - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - self.inline = inline - self.kwargs = kwargs - self._return_defaults = return_defaults - - - def get_children(self, **kwargs): - if self._whereclause is not None: - return self._whereclause, - else: - return () - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self._whereclause = clone(self._whereclause, **kw) - self.parameters = self.parameters.copy() - - @_generative - def where(self, whereclause): - """return a new update() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - if self._whereclause is not None: - self._whereclause = and_(self._whereclause, - _literal_as_text(whereclause)) - else: - self._whereclause = _literal_as_text(whereclause) - - @property - def _extra_froms(self): - # TODO: this could be made memoized - # if the memoization is reset on each generative call. - froms = [] - seen = set([self.table]) - - if self._whereclause is not None: - for item in _from_objects(self._whereclause): - if not seen.intersection(item._cloned_set): - froms.append(item) - seen.update(item._cloned_set) - - return froms - - -class Delete(UpdateBase): - """Represent a DELETE construct. - - The :class:`.Delete` object is created using the :func:`delete()` function. - - """ - - __visit_name__ = 'delete' - - def __init__(self, - table, - whereclause=None, - bind=None, - returning=None, - prefixes=None, - **kwargs): - """Construct :class:`.Delete` object. - - Similar functionality is available via the - :meth:`~.TableClause.delete` method on - :class:`~.schema.Table`. - - :param table: The table to be updated. - - :param whereclause: A :class:`.ClauseElement` describing the ``WHERE`` - condition of the ``UPDATE`` statement. Note that the - :meth:`~Delete.where()` generative method may be used instead. - - .. seealso:: - - :ref:`deletes` - SQL Expression Tutorial - - """ - self._bind = bind - self.table = _interpret_as_from(table) - self._returning = returning - - if prefixes: - self._setup_prefixes(prefixes) - - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - - self.kwargs = kwargs - - def get_children(self, **kwargs): - if self._whereclause is not None: - return self._whereclause, - else: - return () - - @_generative - def where(self, whereclause): - """Add the given WHERE clause to a newly returned delete construct.""" - - if self._whereclause is not None: - self._whereclause = and_(self._whereclause, - _literal_as_text(whereclause)) - else: - self._whereclause = _literal_as_text(whereclause) - - def _copy_internals(self, clone=_clone, **kw): - # TODO: coverage - self._whereclause = clone(self._whereclause, **kw) - diff --git a/libs/sqlalchemy/sql/elements.py b/libs/sqlalchemy/sql/elements.py deleted file mode 100644 index 0e888fcf..00000000 --- a/libs/sqlalchemy/sql/elements.py +++ /dev/null @@ -1,2880 +0,0 @@ -# sql/elements.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Core SQL expression elements, including :class:`.ClauseElement`, -:class:`.ColumnElement`, and derived classes. - -""" - -from __future__ import unicode_literals - -from .. import util, exc, inspection -from . import type_api -from . import operators -from .visitors import Visitable, cloned_traverse, traverse -from .annotation import Annotated -import itertools -from .base import Executable, PARSE_AUTOCOMMIT, Immutable, NO_ARG -from .base import _generative, Generative - -import re -import operator - -def _clone(element, **kw): - return element._clone() - -def collate(expression, collation): - """Return the clause ``expression COLLATE collation``. - - e.g.:: - - collate(mycolumn, 'utf8_bin') - - produces:: - - mycolumn COLLATE utf8_bin - - """ - - expr = _literal_as_binds(expression) - return BinaryExpression( - expr, - _literal_as_text(collation), - operators.collate, type_=expr.type) - -def between(ctest, cleft, cright): - """Return a ``BETWEEN`` predicate clause. - - Equivalent of SQL ``clausetest BETWEEN clauseleft AND clauseright``. - - The :func:`between()` method on all - :class:`.ColumnElement` subclasses provides - similar functionality. - - """ - ctest = _literal_as_binds(ctest) - return ctest.between(cleft, cright) - -def literal(value, type_=None): - """Return a literal clause, bound to a bind parameter. - - Literal clauses are created automatically when non- :class:`.ClauseElement` - objects (such as strings, ints, dates, etc.) are used in a comparison - operation with a :class:`.ColumnElement` - subclass, such as a :class:`~sqlalchemy.schema.Column` object. - Use this function to force the - generation of a literal clause, which will be created as a - :class:`BindParameter` with a bound value. - - :param value: the value to be bound. Can be any Python object supported by - the underlying DB-API, or is translatable via the given type argument. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` which - will provide bind-parameter translation for this literal. - - """ - return BindParameter(None, value, type_=type_, unique=True) - - - -def type_coerce(expression, type_): - """Coerce the given expression into the given type, - on the Python side only. - - :func:`.type_coerce` is roughly similar to :func:`.cast`, except no - "CAST" expression is rendered - the given type is only applied towards - expression typing and against received result values. - - e.g.:: - - from sqlalchemy.types import TypeDecorator - import uuid - - class AsGuid(TypeDecorator): - impl = String - - def process_bind_param(self, value, dialect): - if value is not None: - return str(value) - else: - return None - - def process_result_value(self, value, dialect): - if value is not None: - return uuid.UUID(value) - else: - return None - - conn.execute( - select([type_coerce(mytable.c.ident, AsGuid)]).\\ - where( - type_coerce(mytable.c.ident, AsGuid) == - uuid.uuid3(uuid.NAMESPACE_URL, 'bar') - ) - ) - - :param expression: Column-oriented expression. - :param type_: A :class:`.TypeEngine` class or instance indicating - the type to which the CAST should apply. - - .. seealso:: - - :func:`.cast` - - """ - type_ = type_api.to_instance(type_) - - if hasattr(expression, '__clause_element__'): - return type_coerce(expression.__clause_element__(), type_) - elif isinstance(expression, BindParameter): - bp = expression._clone() - bp.type = type_ - return bp - elif not isinstance(expression, Visitable): - if expression is None: - return Null() - else: - return literal(expression, type_=type_) - else: - return Label(None, expression, type_=type_) - - - - - -def outparam(key, type_=None): - """Create an 'OUT' parameter for usage in functions (stored procedures), - for databases which support them. - - The ``outparam`` can be used like a regular function parameter. - The "output" value will be available from the - :class:`~sqlalchemy.engine.ResultProxy` object via its ``out_parameters`` - attribute, which returns a dictionary containing the values. - - """ - return BindParameter( - key, None, type_=type_, unique=False, isoutparam=True) - - - - -def not_(clause): - """Return a negation of the given clause, i.e. ``NOT(clause)``. - - The ``~`` operator is also overloaded on all - :class:`.ColumnElement` subclasses to produce the - same result. - - """ - return operators.inv(_literal_as_binds(clause)) - - - -@inspection._self_inspects -class ClauseElement(Visitable): - """Base class for elements of a programmatically constructed SQL - expression. - - """ - __visit_name__ = 'clause' - - _annotations = {} - supports_execution = False - _from_objects = [] - bind = None - _is_clone_of = None - is_selectable = False - is_clause_element = True - - _order_by_label_element = None - - def _clone(self): - """Create a shallow copy of this ClauseElement. - - This method may be used by a generative API. Its also used as - part of the "deep" copy afforded by a traversal that combines - the _copy_internals() method. - - """ - c = self.__class__.__new__(self.__class__) - c.__dict__ = self.__dict__.copy() - ClauseElement._cloned_set._reset(c) - ColumnElement.comparator._reset(c) - - # this is a marker that helps to "equate" clauses to each other - # when a Select returns its list of FROM clauses. the cloning - # process leaves around a lot of remnants of the previous clause - # typically in the form of column expressions still attached to the - # old table. - c._is_clone_of = self - - return c - - @property - def _constructor(self): - """return the 'constructor' for this ClauseElement. - - This is for the purposes for creating a new object of - this type. Usually, its just the element's __class__. - However, the "Annotated" version of the object overrides - to return the class of its proxied element. - - """ - return self.__class__ - - @util.memoized_property - def _cloned_set(self): - """Return the set consisting all cloned ancestors of this - ClauseElement. - - Includes this ClauseElement. This accessor tends to be used for - FromClause objects to identify 'equivalent' FROM clauses, regardless - of transformative operations. - - """ - s = util.column_set() - f = self - while f is not None: - s.add(f) - f = f._is_clone_of - return s - - def __getstate__(self): - d = self.__dict__.copy() - d.pop('_is_clone_of', None) - return d - - def _annotate(self, values): - """return a copy of this ClauseElement with annotations - updated by the given dictionary. - - """ - return Annotated(self, values) - - def _with_annotations(self, values): - """return a copy of this ClauseElement with annotations - replaced by the given dictionary. - - """ - return Annotated(self, values) - - def _deannotate(self, values=None, clone=False): - """return a copy of this :class:`.ClauseElement` with annotations - removed. - - :param values: optional tuple of individual values - to remove. - - """ - if clone: - # clone is used when we are also copying - # the expression for a deep deannotation - return self._clone() - else: - # if no clone, since we have no annotations we return - # self - return self - - def _execute_on_connection(self, connection, multiparams, params): - return connection._execute_clauseelement(self, multiparams, params) - - def unique_params(self, *optionaldict, **kwargs): - """Return a copy with :func:`bindparam()` elements replaced. - - Same functionality as ``params()``, except adds `unique=True` - to affected bind parameters so that multiple statements can be - used. - - """ - return self._params(True, optionaldict, kwargs) - - def params(self, *optionaldict, **kwargs): - """Return a copy with :func:`bindparam()` elements replaced. - - Returns a copy of this ClauseElement with :func:`bindparam()` - elements replaced with values taken from the given dictionary:: - - >>> clause = column('x') + bindparam('foo') - >>> print clause.compile().params - {'foo':None} - >>> print clause.params({'foo':7}).compile().params - {'foo':7} - - """ - return self._params(False, optionaldict, kwargs) - - def _params(self, unique, optionaldict, kwargs): - if len(optionaldict) == 1: - kwargs.update(optionaldict[0]) - elif len(optionaldict) > 1: - raise exc.ArgumentError( - "params() takes zero or one positional dictionary argument") - - def visit_bindparam(bind): - if bind.key in kwargs: - bind.value = kwargs[bind.key] - bind.required = False - if unique: - bind._convert_to_unique() - return cloned_traverse(self, {}, {'bindparam': visit_bindparam}) - - def compare(self, other, **kw): - """Compare this ClauseElement to the given ClauseElement. - - Subclasses should override the default behavior, which is a - straight identity comparison. - - \**kw are arguments consumed by subclass compare() methods and - may be used to modify the criteria for comparison. - (see :class:`.ColumnElement`) - - """ - return self is other - - def _copy_internals(self, clone=_clone, **kw): - """Reassign internal elements to be clones of themselves. - - Called during a copy-and-traverse operation on newly - shallow-copied elements to create a deep copy. - - The given clone function should be used, which may be applying - additional transformations to the element (i.e. replacement - traversal, cloned traversal, annotations). - - """ - pass - - def get_children(self, **kwargs): - """Return immediate child elements of this :class:`.ClauseElement`. - - This is used for visit traversal. - - \**kwargs may contain flags that change the collection that is - returned, for example to return a subset of items in order to - cut down on larger traversals, or to return child items from a - different context (such as schema-level collections instead of - clause-level). - - """ - return [] - - def self_group(self, against=None): - """Apply a 'grouping' to this :class:`.ClauseElement`. - - This method is overridden by subclasses to return a - "grouping" construct, i.e. parenthesis. In particular - it's used by "binary" expressions to provide a grouping - around themselves when placed into a larger expression, - as well as by :func:`.select` constructs when placed into - the FROM clause of another :func:`.select`. (Note that - subqueries should be normally created using the - :meth:`.Select.alias` method, as many platforms require - nested SELECT statements to be named). - - As expressions are composed together, the application of - :meth:`self_group` is automatic - end-user code should never - need to use this method directly. Note that SQLAlchemy's - clause constructs take operator precedence into account - - so parenthesis might not be needed, for example, in - an expression like ``x OR (y AND z)`` - AND takes precedence - over OR. - - The base :meth:`self_group` method of :class:`.ClauseElement` - just returns self. - """ - return self - - @util.dependencies("sqlalchemy.engine.default") - def compile(self, default, bind=None, dialect=None, **kw): - """Compile this SQL expression. - - The return value is a :class:`~.Compiled` object. - Calling ``str()`` or ``unicode()`` on the returned value will yield a - string representation of the result. The - :class:`~.Compiled` object also can return a - dictionary of bind parameter names and values - using the ``params`` accessor. - - :param bind: An ``Engine`` or ``Connection`` from which a - ``Compiled`` will be acquired. This argument takes precedence over - this :class:`.ClauseElement`'s bound engine, if any. - - :param column_keys: Used for INSERT and UPDATE statements, a list of - column names which should be present in the VALUES clause of the - compiled statement. If ``None``, all columns from the target table - object are rendered. - - :param dialect: A ``Dialect`` instance from which a ``Compiled`` - will be acquired. This argument takes precedence over the `bind` - argument as well as this :class:`.ClauseElement`'s bound engine, if - any. - - :param inline: Used for INSERT statements, for a dialect which does - not support inline retrieval of newly generated primary key - columns, will force the expression used to create the new primary - key value to be rendered inline within the INSERT statement's - VALUES clause. This typically refers to Sequence execution but may - also refer to any server-side default generation function - associated with a primary key `Column`. - - """ - - if not dialect: - if bind: - dialect = bind.dialect - elif self.bind: - dialect = self.bind.dialect - bind = self.bind - else: - dialect = default.DefaultDialect() - return self._compiler(dialect, bind=bind, **kw) - - def _compiler(self, dialect, **kw): - """Return a compiler appropriate for this ClauseElement, given a - Dialect.""" - - return dialect.statement_compiler(dialect, self, **kw) - - def __str__(self): - if util.py3k: - return str(self.compile()) - else: - return unicode(self.compile()).encode('ascii', 'backslashreplace') - - def __and__(self, other): - return and_(self, other) - - def __or__(self, other): - return or_(self, other) - - def __invert__(self): - if hasattr(self, 'negation_clause'): - return self.negation_clause - else: - return self._negate() - - def __bool__(self): - raise TypeError("Boolean value of this clause is not defined") - - __nonzero__ = __bool__ - - def _negate(self): - return UnaryExpression( - self.self_group(against=operators.inv), - operator=operators.inv, - negate=None) - - def __repr__(self): - friendly = getattr(self, 'description', None) - if friendly is None: - return object.__repr__(self) - else: - return '<%s.%s at 0x%x; %s>' % ( - self.__module__, self.__class__.__name__, id(self), friendly) - - - -class ColumnElement(ClauseElement, operators.ColumnOperators): - """Represent a column-oriented SQL expression suitable for usage in the - "columns" clause, WHERE clause etc. of a statement. - - While the most familiar kind of :class:`.ColumnElement` is the - :class:`.Column` object, :class:`.ColumnElement` serves as the basis - for any unit that may be present in a SQL expression, including - the expressions themselves, SQL functions, bound parameters, - literal expressions, keywords such as ``NULL``, etc. - :class:`.ColumnElement` is the ultimate base class for all such elements. - - A :class:`.ColumnElement` provides the ability to generate new - :class:`.ColumnElement` - objects using Python expressions. This means that Python operators - such as ``==``, ``!=`` and ``<`` are overloaded to mimic SQL operations, - and allow the instantiation of further :class:`.ColumnElement` instances - which are composed from other, more fundamental :class:`.ColumnElement` - objects. For example, two :class:`.ColumnClause` objects can be added - together with the addition operator ``+`` to produce - a :class:`.BinaryExpression`. - Both :class:`.ColumnClause` and :class:`.BinaryExpression` are subclasses - of :class:`.ColumnElement`:: - - >>> from sqlalchemy.sql import column - >>> column('a') + column('b') - - >>> print column('a') + column('b') - a + b - - :class:`.ColumnElement` supports the ability to be a *proxy* element, - which indicates that the :class:`.ColumnElement` may be associated with - a :class:`.Selectable` which was derived from another :class:`.Selectable`. - An example of a "derived" :class:`.Selectable` is an :class:`.Alias` of a - :class:`~sqlalchemy.schema.Table`. For the ambitious, an in-depth - discussion of this concept can be found at - `Expression Transformations `_. - - """ - - __visit_name__ = 'column' - primary_key = False - foreign_keys = [] - _label = None - _key_label = None - _alt_names = () - - def self_group(self, against=None): - if against in (operators.and_, operators.or_, operators._asbool) and \ - self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: - return AsBoolean(self, operators.istrue, operators.isfalse) - else: - return self - - def _negate(self): - if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: - return AsBoolean(self, operators.isfalse, operators.istrue) - else: - return super(ColumnElement, self)._negate() - - @util.memoized_property - def type(self): - return type_api.NULLTYPE - - @util.memoized_property - def comparator(self): - return self.type.comparator_factory(self) - - def __getattr__(self, key): - try: - return getattr(self.comparator, key) - except AttributeError: - raise AttributeError( - 'Neither %r object nor %r object has an attribute %r' % ( - type(self).__name__, - type(self.comparator).__name__, - key) - ) - - def operate(self, op, *other, **kwargs): - return op(self.comparator, *other, **kwargs) - - def reverse_operate(self, op, other, **kwargs): - return op(other, self.comparator, **kwargs) - - def _bind_param(self, operator, obj): - return BindParameter(None, obj, - _compared_to_operator=operator, - _compared_to_type=self.type, unique=True) - - @property - def expression(self): - """Return a column expression. - - Part of the inspection interface; returns self. - - """ - return self - - @property - def _select_iterable(self): - return (self, ) - - @util.memoized_property - def base_columns(self): - return util.column_set(c for c in self.proxy_set - if not hasattr(c, '_proxies')) - - @util.memoized_property - def proxy_set(self): - s = util.column_set([self]) - if hasattr(self, '_proxies'): - for c in self._proxies: - s.update(c.proxy_set) - return s - - def shares_lineage(self, othercolumn): - """Return True if the given :class:`.ColumnElement` - has a common ancestor to this :class:`.ColumnElement`.""" - - return bool(self.proxy_set.intersection(othercolumn.proxy_set)) - - def _compare_name_for_result(self, other): - """Return True if the given column element compares to this one - when targeting within a result row.""" - - return hasattr(other, 'name') and hasattr(self, 'name') and \ - other.name == self.name - - def _make_proxy(self, selectable, name=None, name_is_truncatable=False, **kw): - """Create a new :class:`.ColumnElement` representing this - :class:`.ColumnElement` as it appears in the select list of a - descending selectable. - - """ - if name is None: - name = self.anon_label - try: - key = str(self) - except exc.UnsupportedCompilationError: - key = self.anon_label - else: - key = name - co = ColumnClause( - _as_truncated(name) if name_is_truncatable else name, - type_=getattr(self, 'type', None), - _selectable=selectable - ) - co._proxies = [self] - if selectable._is_clone_of is not None: - co._is_clone_of = \ - selectable._is_clone_of.columns.get(key) - selectable._columns[key] = co - return co - - def compare(self, other, use_proxies=False, equivalents=None, **kw): - """Compare this ColumnElement to another. - - Special arguments understood: - - :param use_proxies: when True, consider two columns that - share a common base column as equivalent (i.e. shares_lineage()) - - :param equivalents: a dictionary of columns as keys mapped to sets - of columns. If the given "other" column is present in this - dictionary, if any of the columns in the corresponding set() pass the - comparison test, the result is True. This is used to expand the - comparison to other columns that may be known to be equivalent to - this one via foreign key or other criterion. - - """ - to_compare = (other, ) - if equivalents and other in equivalents: - to_compare = equivalents[other].union(to_compare) - - for oth in to_compare: - if use_proxies and self.shares_lineage(oth): - return True - elif hash(oth) == hash(self): - return True - else: - return False - - def label(self, name): - """Produce a column label, i.e. `` AS ``. - - This is a shortcut to the :func:`~.expression.label` function. - - if 'name' is None, an anonymous label name will be generated. - - """ - return Label(name, self, self.type) - - @util.memoized_property - def anon_label(self): - """provides a constant 'anonymous label' for this ColumnElement. - - This is a label() expression which will be named at compile time. - The same label() is returned each time anon_label is called so - that expressions can reference anon_label multiple times, producing - the same label name at compile time. - - the compiler uses this function automatically at compile time - for expressions that are known to be 'unnamed' like binary - expressions and function calls. - - """ - return _anonymous_label('%%(%d %s)s' % (id(self), getattr(self, - 'name', 'anon'))) - - - -class BindParameter(ColumnElement): - """Represent a bound parameter value. - - """ - - __visit_name__ = 'bindparam' - - _is_crud = False - - def __init__(self, key, value=NO_ARG, type_=None, - unique=False, required=NO_ARG, - quote=None, callable_=None, - isoutparam=False, - _compared_to_operator=None, - _compared_to_type=None): - """Construct a new :class:`.BindParameter`. - - :param key: - the key for this bind param. Will be used in the generated - SQL statement for dialects that use named parameters. This - value may be modified when part of a compilation operation, - if other :class:`BindParameter` objects exist with the same - key, or if its length is too long and truncation is - required. - - :param value: - Initial value for this bind param. This value may be - overridden by the dictionary of parameters sent to statement - compilation/execution. - - Defaults to ``None``, however if neither ``value`` nor - ``callable`` are passed explicitly, the ``required`` flag will be - set to ``True`` which has the effect of requiring a value be present - when the statement is actually executed. - - .. versionchanged:: 0.8 The ``required`` flag is set to ``True`` - automatically if ``value`` or ``callable`` is not passed. - - :param callable\_: - A callable function that takes the place of "value". The function - will be called at statement execution time to determine the - ultimate value. Used for scenarios where the actual bind - value cannot be determined at the point at which the clause - construct is created, but embedded bind values are still desirable. - - :param type\_: - A ``TypeEngine`` object that will be used to pre-process the - value corresponding to this :class:`BindParameter` at - execution time. - - :param unique: - if True, the key name of this BindParamClause will be - modified if another :class:`BindParameter` of the same name - already has been located within the containing - :class:`.ClauseElement`. - - :param required: - If ``True``, a value is required at execution time. If not passed, - is set to ``True`` or ``False`` based on whether or not - one of ``value`` or ``callable`` were passed.. - - .. versionchanged:: 0.8 If the ``required`` flag is not specified, - it will be set automatically to ``True`` or ``False`` depending - on whether or not the ``value`` or ``callable`` parameters - were specified. - - :param quote: - True if this parameter name requires quoting and is not - currently known as a SQLAlchemy reserved word; this currently - only applies to the Oracle backend. - - :param isoutparam: - if True, the parameter should be treated like a stored procedure - "OUT" parameter. - - .. seealso:: - - :func:`.outparam` - - - - """ - if isinstance(key, ColumnClause): - type_ = key.type - key = key.name - if required is NO_ARG: - required = (value is NO_ARG and callable_ is None) - if value is NO_ARG: - value = None - - if quote is not None: - key = quoted_name(key, quote) - - if unique: - self.key = _anonymous_label('%%(%d %s)s' % (id(self), key - or 'param')) - else: - self.key = key or _anonymous_label('%%(%d param)s' - % id(self)) - - # identifying key that won't change across - # clones, used to identify the bind's logical - # identity - self._identifying_key = self.key - - # key that was passed in the first place, used to - # generate new keys - self._orig_key = key or 'param' - - self.unique = unique - self.value = value - self.callable = callable_ - self.isoutparam = isoutparam - self.required = required - if type_ is None: - if _compared_to_type is not None: - self.type = \ - _compared_to_type.coerce_compared_value( - _compared_to_operator, value) - else: - self.type = type_api._type_map.get(type(value), - type_api.NULLTYPE) - elif isinstance(type_, type): - self.type = type_() - else: - self.type = type_ - - def _with_value(self, value): - """Return a copy of this :class:`.BindParameter` with the given value set.""" - cloned = self._clone() - cloned.value = value - cloned.callable = None - cloned.required = False - if cloned.type is type_api.NULLTYPE: - cloned.type = type_api._type_map.get(type(value), - type_api.NULLTYPE) - return cloned - - @property - def effective_value(self): - """Return the value of this bound parameter, - taking into account if the ``callable`` parameter - was set. - - The ``callable`` value will be evaluated - and returned if present, else ``value``. - - """ - if self.callable: - return self.callable() - else: - return self.value - - def _clone(self): - c = ClauseElement._clone(self) - if self.unique: - c.key = _anonymous_label('%%(%d %s)s' % (id(c), c._orig_key - or 'param')) - return c - - def _convert_to_unique(self): - if not self.unique: - self.unique = True - self.key = _anonymous_label('%%(%d %s)s' % (id(self), - self._orig_key or 'param')) - - def compare(self, other, **kw): - """Compare this :class:`BindParameter` to the given - clause.""" - - return isinstance(other, BindParameter) \ - and self.type._compare_type_affinity(other.type) \ - and self.value == other.value - - def __getstate__(self): - """execute a deferred value for serialization purposes.""" - - d = self.__dict__.copy() - v = self.value - if self.callable: - v = self.callable() - d['callable'] = None - d['value'] = v - return d - - def __repr__(self): - return 'BindParameter(%r, %r, type_=%r)' % (self.key, - self.value, self.type) - - -class TypeClause(ClauseElement): - """Handle a type keyword in a SQL statement. - - Used by the ``Case`` statement. - - """ - - __visit_name__ = 'typeclause' - - def __init__(self, type): - self.type = type - - -class TextClause(Executable, ClauseElement): - """Represent a literal SQL text fragment. - - Public constructor is the :func:`text()` function. - - """ - - __visit_name__ = 'textclause' - - _bind_params_regex = re.compile(r'(?`` - to specify bind parameters; they will be compiled to their - engine-specific format. - - :param autocommit: - Deprecated. Use .execution_options(autocommit=) - to set the autocommit option. - - :param bind: - an optional connection or engine to be used for this text query. - - :param bindparams: - Deprecated. A list of :func:`.bindparam` instances used to - provide information about parameters embedded in the statement. - This argument now invokes the :meth:`.TextClause.bindparams` - method on the construct before returning it. E.g.:: - - stmt = text("SELECT * FROM table WHERE id=:id", - bindparams=[bindparam('id', value=5, type_=Integer)]) - - Is equivalent to:: - - stmt = text("SELECT * FROM table WHERE id=:id").\\ - bindparams(bindparam('id', value=5, type_=Integer)) - - .. deprecated:: 0.9.0 the :meth:`.TextClause.bindparams` method - supersedes the ``bindparams`` argument to :func:`.text`. - - :param typemap: - Deprecated. A dictionary mapping the names of columns - represented in the columns clause of a ``SELECT`` statement - to type objects, - which will be used to perform post-processing on columns within - the result set. This parameter now invokes the :meth:`.TextClause.columns` - method, which returns a :class:`.TextAsFrom` construct that gains - a ``.c`` collection and can be embedded in other expressions. E.g.:: - - stmt = text("SELECT * FROM table", - typemap={'id': Integer, 'name': String}, - ) - - Is equivalent to:: - - stmt = text("SELECT * FROM table").columns(id=Integer, name=String) - - Or alternatively:: - - from sqlalchemy.sql import column - stmt = text("SELECT * FROM table").columns( - column('id', Integer), - column('name', String) - ) - - .. deprecated:: 0.9.0 the :meth:`.TextClause.columns` method - supersedes the ``typemap`` argument to :func:`.text`. - - """ - stmt = TextClause(text, bind=bind) - if bindparams: - stmt = stmt.bindparams(*bindparams) - if typemap: - stmt = stmt.columns(**typemap) - if autocommit is not None: - util.warn_deprecated('autocommit on text() is deprecated. ' - 'Use .execution_options(autocommit=True)') - stmt = stmt.execution_options(autocommit=autocommit) - - return stmt - - @_generative - def bindparams(self, *binds, **names_to_values): - """Establish the values and/or types of bound parameters within - this :class:`.TextClause` construct. - - Given a text construct such as:: - - from sqlalchemy import text - stmt = text("SELECT id, name FROM user WHERE name=:name " - "AND timestamp=:timestamp") - - the :meth:`.TextClause.bindparams` method can be used to establish - the initial value of ``:name`` and ``:timestamp``, - using simple keyword arguments:: - - stmt = stmt.bindparams(name='jack', - timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5)) - - Where above, new :class:`.BindParameter` objects - will be generated with the names ``name`` and ``timestamp``, and - values of ``jack`` and ``datetime.datetime(2012, 10, 8, 15, 12, 5)``, - respectively. The types will be - inferred from the values given, in this case :class:`.String` and - :class:`.DateTime`. - - When specific typing behavior is needed, the positional ``*binds`` - argument can be used in which to specify :func:`.bindparam` constructs - directly. These constructs must include at least the ``key`` argument, - then an optional value and type:: - - from sqlalchemy import bindparam - stmt = stmt.bindparams( - bindparam('name', value='jack', type_=String), - bindparam('timestamp', type_=DateTime) - ) - - Above, we specified the type of :class:`.DateTime` for the ``timestamp`` - bind, and the type of :class:`.String` for the ``name`` bind. In - the case of ``name`` we also set the default value of ``"jack"``. - - Additional bound parameters can be supplied at statement execution - time, e.g.:: - - result = connection.execute(stmt, - timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5)) - - The :meth:`.TextClause.bindparams` method can be called repeatedly, where - it will re-use existing :class:`.BindParameter` objects to add new information. - For example, we can call :meth:`.TextClause.bindparams` first with - typing information, and a second time with value information, and it - will be combined:: - - stmt = text("SELECT id, name FROM user WHERE name=:name " - "AND timestamp=:timestamp") - stmt = stmt.bindparams( - bindparam('name', type_=String), - bindparam('timestamp', type_=DateTime) - ) - stmt = stmt.bindparams( - name='jack', - timestamp=datetime.datetime(2012, 10, 8, 15, 12, 5) - ) - - - .. versionadded:: 0.9.0 The :meth:`.TextClause.bindparams` method supersedes - the argument ``bindparams`` passed to :func:`~.expression.text`. - - - """ - self._bindparams = new_params = self._bindparams.copy() - - for bind in binds: - try: - existing = new_params[bind.key] - except KeyError: - raise exc.ArgumentError( - "This text() construct doesn't define a " - "bound parameter named %r" % bind.key) - else: - new_params[existing.key] = bind - - for key, value in names_to_values.items(): - try: - existing = new_params[key] - except KeyError: - raise exc.ArgumentError( - "This text() construct doesn't define a " - "bound parameter named %r" % key) - else: - new_params[key] = existing._with_value(value) - - - - @util.dependencies('sqlalchemy.sql.selectable') - def columns(self, selectable, *cols, **types): - """Turn this :class:`.TextClause` object into a :class:`.TextAsFrom` - object that can be embedded into another statement. - - This function essentially bridges the gap between an entirely - textual SELECT statement and the SQL expression language concept - of a "selectable":: - - from sqlalchemy.sql import column, text - - stmt = text("SELECT id, name FROM some_table") - stmt = stmt.columns(column('id'), column('name')).alias('st') - - stmt = select([mytable]).\\ - select_from( - mytable.join(stmt, mytable.c.name == stmt.c.name) - ).where(stmt.c.id > 5) - - Above, we used untyped :func:`.column` elements. These can also have - types specified, which will impact how the column behaves in expressions - as well as determining result set behavior:: - - stmt = text("SELECT id, name, timestamp FROM some_table") - stmt = stmt.columns( - column('id', Integer), - column('name', Unicode), - column('timestamp', DateTime) - ) - - for id, name, timestamp in connection.execute(stmt): - print(id, name, timestamp) - - Keyword arguments allow just the names and types of columns to be specified, - where the :func:`.column` elements will be generated automatically:: - - stmt = text("SELECT id, name, timestamp FROM some_table") - stmt = stmt.columns( - id=Integer, - name=Unicode, - timestamp=DateTime - ) - - for id, name, timestamp in connection.execute(stmt): - print(id, name, timestamp) - - The :meth:`.TextClause.columns` method provides a direct - route to calling :meth:`.FromClause.alias` as well as :meth:`.SelectBase.cte` - against a textual SELECT statement:: - - stmt = stmt.columns(id=Integer, name=String).cte('st') - - stmt = select([sometable]).where(sometable.c.id == stmt.c.id) - - .. versionadded:: 0.9.0 :func:`.text` can now be converted into a fully - featured "selectable" construct using the :meth:`.TextClause.columns` - method. This method supersedes the ``typemap`` argument to - :func:`.text`. - - """ - - col_by_name = dict( - (col.key, col) for col in cols - ) - for key, type_ in types.items(): - col_by_name[key] = ColumnClause(key, type_) - - return selectable.TextAsFrom(self, list(col_by_name.values())) - - @property - def type(self): - return type_api.NULLTYPE - - @property - def comparator(self): - return self.type.comparator_factory(self) - - def self_group(self, against=None): - if against is operators.in_op: - return Grouping(self) - else: - return self - - def _copy_internals(self, clone=_clone, **kw): - self._bindparams = dict((b.key, clone(b, **kw)) - for b in self._bindparams.values()) - - def get_children(self, **kwargs): - return list(self._bindparams.values()) - - -class Null(ColumnElement): - """Represent the NULL keyword in a SQL statement. - - :class:`.Null` is accessed as a constant via the - :func:`.null` function. - - """ - - __visit_name__ = 'null' - - @util.memoized_property - def type(self): - return type_api.NULLTYPE - - @classmethod - def _singleton(cls): - """Return a constant :class:`.Null` construct.""" - - return NULL - - def compare(self, other): - return isinstance(other, Null) - - -class False_(ColumnElement): - """Represent the ``false`` keyword, or equivalent, in a SQL statement. - - :class:`.False_` is accessed as a constant via the - :func:`.false` function. - - """ - - __visit_name__ = 'false' - - @util.memoized_property - def type(self): - return type_api.BOOLEANTYPE - - def _negate(self): - return TRUE - - @classmethod - def _singleton(cls): - """Return a constant :class:`.False_` construct. - - E.g.:: - - >>> from sqlalchemy import false - >>> print select([t.c.x]).where(false()) - SELECT x FROM t WHERE false - - A backend which does not support true/false constants will render as - an expression against 1 or 0:: - - >>> print select([t.c.x]).where(false()) - SELECT x FROM t WHERE 0 = 1 - - The :func:`.true` and :func:`.false` constants also feature - "short circuit" operation within an :func:`.and_` or :func:`.or_` - conjunction:: - - >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) - SELECT x FROM t WHERE true - - >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) - SELECT x FROM t WHERE false - - .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature - better integrated behavior within conjunctions and on dialects - that don't support true/false constants. - - .. seealso:: - - :func:`.true` - - """ - - return FALSE - - def compare(self, other): - return isinstance(other, False_) - -class True_(ColumnElement): - """Represent the ``true`` keyword, or equivalent, in a SQL statement. - - :class:`.True_` is accessed as a constant via the - :func:`.true` function. - - """ - - __visit_name__ = 'true' - - @util.memoized_property - def type(self): - return type_api.BOOLEANTYPE - - def _negate(self): - return FALSE - - @classmethod - def _ifnone(cls, other): - if other is None: - return cls._singleton() - else: - return other - - @classmethod - def _singleton(cls): - """Return a constant :class:`.True_` construct. - - E.g.:: - - >>> from sqlalchemy import true - >>> print select([t.c.x]).where(true()) - SELECT x FROM t WHERE true - - A backend which does not support true/false constants will render as - an expression against 1 or 0:: - - >>> print select([t.c.x]).where(true()) - SELECT x FROM t WHERE 1 = 1 - - The :func:`.true` and :func:`.false` constants also feature - "short circuit" operation within an :func:`.and_` or :func:`.or_` - conjunction:: - - >>> print select([t.c.x]).where(or_(t.c.x > 5, true())) - SELECT x FROM t WHERE true - - >>> print select([t.c.x]).where(and_(t.c.x > 5, false())) - SELECT x FROM t WHERE false - - .. versionchanged:: 0.9 :func:`.true` and :func:`.false` feature - better integrated behavior within conjunctions and on dialects - that don't support true/false constants. - - .. seealso:: - - :func:`.false` - - """ - - return TRUE - - def compare(self, other): - return isinstance(other, True_) - -NULL = Null() -FALSE = False_() -TRUE = True_() - -class ClauseList(ClauseElement): - """Describe a list of clauses, separated by an operator. - - By default, is comma-separated, such as a column listing. - - """ - __visit_name__ = 'clauselist' - - def __init__(self, *clauses, **kwargs): - self.operator = kwargs.pop('operator', operators.comma_op) - self.group = kwargs.pop('group', True) - self.group_contents = kwargs.pop('group_contents', True) - if self.group_contents: - self.clauses = [ - _literal_as_text(clause).self_group(against=self.operator) - for clause in clauses] - else: - self.clauses = [ - _literal_as_text(clause) - for clause in clauses] - - def __iter__(self): - return iter(self.clauses) - - def __len__(self): - return len(self.clauses) - - @property - def _select_iterable(self): - return iter(self) - - def append(self, clause): - if self.group_contents: - self.clauses.append(_literal_as_text(clause).\ - self_group(against=self.operator)) - else: - self.clauses.append(_literal_as_text(clause)) - - def _copy_internals(self, clone=_clone, **kw): - self.clauses = [clone(clause, **kw) for clause in self.clauses] - - def get_children(self, **kwargs): - return self.clauses - - @property - def _from_objects(self): - return list(itertools.chain(*[c._from_objects for c in self.clauses])) - - def self_group(self, against=None): - if self.group and operators.is_precedent(self.operator, against): - return Grouping(self) - else: - return self - - def compare(self, other, **kw): - """Compare this :class:`.ClauseList` to the given :class:`.ClauseList`, - including a comparison of all the clause items. - - """ - if not isinstance(other, ClauseList) and len(self.clauses) == 1: - return self.clauses[0].compare(other, **kw) - elif isinstance(other, ClauseList) and \ - len(self.clauses) == len(other.clauses): - for i in range(0, len(self.clauses)): - if not self.clauses[i].compare(other.clauses[i], **kw): - return False - else: - return self.operator == other.operator - else: - return False - - - -class BooleanClauseList(ClauseList, ColumnElement): - __visit_name__ = 'clauselist' - - def __init__(self, *arg, **kw): - raise NotImplementedError( - "BooleanClauseList has a private constructor") - - @classmethod - def _construct(cls, operator, continue_on, skip_on, *clauses, **kw): - convert_clauses = [] - - clauses = util.coerce_generator_arg(clauses) - for clause in clauses: - clause = _literal_as_text(clause) - - if isinstance(clause, continue_on): - continue - elif isinstance(clause, skip_on): - return clause.self_group(against=operators._asbool) - - convert_clauses.append(clause) - - if len(convert_clauses) == 1: - return convert_clauses[0].self_group(against=operators._asbool) - elif not convert_clauses and clauses: - return clauses[0].self_group(against=operators._asbool) - - convert_clauses = [c.self_group(against=operator) - for c in convert_clauses] - - self = cls.__new__(cls) - self.clauses = convert_clauses - self.group = True - self.operator = operator - self.group_contents = True - self.type = type_api.BOOLEANTYPE - return self - - @classmethod - def and_(cls, *clauses): - """Join a list of clauses together using the ``AND`` operator. - - The ``&`` operator is also overloaded on all :class:`.ColumnElement` - subclasses to produce the - same result. - - """ - return cls._construct(operators.and_, True_, False_, *clauses) - - @classmethod - def or_(cls, *clauses): - """Join a list of clauses together using the ``OR`` operator. - - The ``|`` operator is also overloaded on all - :class:`.ColumnElement` subclasses to produce the - same result. - - """ - return cls._construct(operators.or_, False_, True_, *clauses) - - @property - def _select_iterable(self): - return (self, ) - - def self_group(self, against=None): - if not self.clauses: - return self - else: - return super(BooleanClauseList, self).self_group(against=against) - - def _negate(self): - return ClauseList._negate(self) - - -and_ = BooleanClauseList.and_ -or_ = BooleanClauseList.or_ - -class Tuple(ClauseList, ColumnElement): - """Represent a SQL tuple.""" - - def __init__(self, *clauses, **kw): - """Return a :class:`.Tuple`. - - Main usage is to produce a composite IN construct:: - - from sqlalchemy import tuple_ - - tuple_(table.c.col1, table.c.col2).in_( - [(1, 2), (5, 12), (10, 19)] - ) - - .. warning:: - - The composite IN construct is not supported by all backends, - and is currently known to work on Postgresql and MySQL, - but not SQLite. Unsupported backends will raise - a subclass of :class:`~sqlalchemy.exc.DBAPIError` when such - an expression is invoked. - - """ - - clauses = [_literal_as_binds(c) for c in clauses] - self.type = kw.pop('type_', None) - if self.type is None: - self.type = _type_from_args(clauses) - super(Tuple, self).__init__(*clauses, **kw) - - @property - def _select_iterable(self): - return (self, ) - - def _bind_param(self, operator, obj): - return Tuple(*[ - BindParameter(None, o, _compared_to_operator=operator, - _compared_to_type=self.type, unique=True) - for o in obj - ]).self_group() - - -class Case(ColumnElement): - """Represent a SQL ``CASE`` construct. - - - """ - __visit_name__ = 'case' - - def __init__(self, whens, value=None, else_=None): - """Produce a :class:`.Case` object. - - :param whens: A sequence of pairs, or alternatively a dict, - to be translated into "WHEN / THEN" clauses. - - :param value: Optional for simple case statements, produces - a column expression as in "CASE WHEN ..." - - :param else\_: Optional as well, for case defaults produces - the "ELSE" portion of the "CASE" statement. - - The expressions used for THEN and ELSE, - when specified as strings, will be interpreted - as bound values. To specify textual SQL expressions - for these, use the :func:`literal_column` - construct. - - The expressions used for the WHEN criterion - may only be literal strings when "value" is - present, i.e. CASE table.somecol WHEN "x" THEN "y". - Otherwise, literal strings are not accepted - in this position, and either the text() - or literal() constructs must be used to - interpret raw string values. - - Usage examples:: - - case([(orderline.c.qty > 100, item.c.specialprice), - (orderline.c.qty > 10, item.c.bulkprice) - ], else_=item.c.regularprice) - - case(value=emp.c.type, whens={ - 'engineer': emp.c.salary * 1.1, - 'manager': emp.c.salary * 3, - }) - - Using :func:`.literal_column()`, to allow for databases that - do not support bind parameters in the ``then`` clause. The type - can be specified which determines the type of the :func:`case()` construct - overall:: - - case([(orderline.c.qty > 100, - literal_column("'greaterthan100'", String)), - (orderline.c.qty > 10, literal_column("'greaterthan10'", - String)) - ], else_=literal_column("'lethan10'", String)) - - """ - - try: - whens = util.dictlike_iteritems(whens) - except TypeError: - pass - - if value is not None: - whenlist = [ - (_literal_as_binds(c).self_group(), - _literal_as_binds(r)) for (c, r) in whens - ] - else: - whenlist = [ - (_no_literals(c).self_group(), - _literal_as_binds(r)) for (c, r) in whens - ] - - if whenlist: - type_ = list(whenlist[-1])[-1].type - else: - type_ = None - - if value is None: - self.value = None - else: - self.value = _literal_as_binds(value) - - self.type = type_ - self.whens = whenlist - if else_ is not None: - self.else_ = _literal_as_binds(else_) - else: - self.else_ = None - - def _copy_internals(self, clone=_clone, **kw): - if self.value is not None: - self.value = clone(self.value, **kw) - self.whens = [(clone(x, **kw), clone(y, **kw)) - for x, y in self.whens] - if self.else_ is not None: - self.else_ = clone(self.else_, **kw) - - def get_children(self, **kwargs): - if self.value is not None: - yield self.value - for x, y in self.whens: - yield x - yield y - if self.else_ is not None: - yield self.else_ - - @property - def _from_objects(self): - return list(itertools.chain(*[x._from_objects for x in - self.get_children()])) - - -def literal_column(text, type_=None): - """Return a textual column expression, as would be in the columns - clause of a ``SELECT`` statement. - - The object returned supports further expressions in the same way as any - other column object, including comparison, math and string operations. - The type\_ parameter is important to determine proper expression behavior - (such as, '+' means string concatenation or numerical addition based on - the type). - - :param text: the text of the expression; can be any SQL expression. - Quoting rules will not be applied. To specify a column-name expression - which should be subject to quoting rules, use the :func:`column` - function. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` - object which will - provide result-set translation and additional expression semantics for - this column. If left as None the type will be NullType. - - """ - return ColumnClause(text, type_=type_, is_literal=True) - - - -class Cast(ColumnElement): - """Represent the SQL ``CAST`` construct.""" - - __visit_name__ = 'cast' - - def __init__(self, expression, type_): - """Return a :class:`.Cast` object. - - Equivalent of SQL ``CAST(clause AS totype)``. - - E.g.:: - - cast(table.c.unit_price * table.c.qty, Numeric(10,4)) - - or:: - - cast(table.c.timestamp, DATE) - - :param expression: Column-oriented expression. - :param type_: A :class:`.TypeEngine` class or instance indicating - the type to which the CAST should apply. - - .. seealso:: - - :func:`.type_coerce` - Python-side type coercion without emitting - CAST. - - """ - self.type = type_api.to_instance(type_) - self.clause = _literal_as_binds(expression, type_=self.type) - self.typeclause = TypeClause(self.type) - - def _copy_internals(self, clone=_clone, **kw): - self.clause = clone(self.clause, **kw) - self.typeclause = clone(self.typeclause, **kw) - - def get_children(self, **kwargs): - return self.clause, self.typeclause - - @property - def _from_objects(self): - return self.clause._from_objects - - -class Extract(ColumnElement): - """Represent a SQL EXTRACT clause, ``extract(field FROM expr)``.""" - - __visit_name__ = 'extract' - - def __init__(self, field, expr, **kwargs): - """Return a :class:`.Extract` construct. - - This is typically available as :func:`.extract` - as well as ``func.extract`` from the - :data:`.func` namespace. - - """ - self.type = type_api.INTEGERTYPE - self.field = field - self.expr = _literal_as_binds(expr, None) - - def _copy_internals(self, clone=_clone, **kw): - self.expr = clone(self.expr, **kw) - - def get_children(self, **kwargs): - return self.expr, - - @property - def _from_objects(self): - return self.expr._from_objects - - -class UnaryExpression(ColumnElement): - """Define a 'unary' expression. - - A unary expression has a single column expression - and an operator. The operator can be placed on the left - (where it is called the 'operator') or right (where it is called the - 'modifier') of the column expression. - - """ - __visit_name__ = 'unary' - - def __init__(self, element, operator=None, modifier=None, - type_=None, negate=None): - self.operator = operator - self.modifier = modifier - self.element = element.self_group(against=self.operator or self.modifier) - self.type = type_api.to_instance(type_) - self.negate = negate - - @classmethod - def _create_nullsfirst(cls, column): - """Return a NULLS FIRST ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol).nullsfirst()) - - produces:: - - ORDER BY mycol DESC NULLS FIRST - - """ - return UnaryExpression( - _literal_as_text(column), modifier=operators.nullsfirst_op) - - - @classmethod - def _create_nullslast(cls, column): - """Return a NULLS LAST ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol).nullslast()) - - produces:: - - ORDER BY mycol DESC NULLS LAST - - """ - return UnaryExpression( - _literal_as_text(column), modifier=operators.nullslast_op) - - - @classmethod - def _create_desc(cls, column): - """Return a descending ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(desc(table1.mycol)) - - produces:: - - ORDER BY mycol DESC - - """ - return UnaryExpression( - _literal_as_text(column), modifier=operators.desc_op) - - @classmethod - def _create_asc(cls, column): - """Return an ascending ``ORDER BY`` clause element. - - e.g.:: - - someselect.order_by(asc(table1.mycol)) - - produces:: - - ORDER BY mycol ASC - - """ - return UnaryExpression( - _literal_as_text(column), modifier=operators.asc_op) - - @classmethod - def _create_distinct(cls, expr): - """Return a ``DISTINCT`` clause. - - e.g.:: - - distinct(a) - - renders:: - - DISTINCT a - - """ - expr = _literal_as_binds(expr) - return UnaryExpression(expr, - operator=operators.distinct_op, type_=expr.type) - - @util.memoized_property - def _order_by_label_element(self): - if self.modifier in (operators.desc_op, operators.asc_op): - return self.element._order_by_label_element - else: - return None - - @property - def _from_objects(self): - return self.element._from_objects - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - def get_children(self, **kwargs): - return self.element, - - def compare(self, other, **kw): - """Compare this :class:`UnaryExpression` against the given - :class:`.ClauseElement`.""" - - return ( - isinstance(other, UnaryExpression) and - self.operator == other.operator and - self.modifier == other.modifier and - self.element.compare(other.element, **kw) - ) - - def _negate(self): - if self.negate is not None: - return UnaryExpression( - self.element, - operator=self.negate, - negate=self.operator, - modifier=self.modifier, - type_=self.type) - else: - return ClauseElement._negate(self) - - def self_group(self, against=None): - if self.operator and operators.is_precedent(self.operator, against): - return Grouping(self) - else: - return self - - -class AsBoolean(UnaryExpression): - - def __init__(self, element, operator, negate): - self.element = element - self.type = type_api.BOOLEANTYPE - self.operator = operator - self.negate = negate - self.modifier = None - - def self_group(self, against=None): - return self - - def _negate(self): - return self.element._negate() - - -class BinaryExpression(ColumnElement): - """Represent an expression that is ``LEFT RIGHT``. - - A :class:`.BinaryExpression` is generated automatically - whenever two column expressions are used in a Python binary expresion:: - - >>> from sqlalchemy.sql import column - >>> column('a') + column('b') - - >>> print column('a') + column('b') - a + b - - """ - - __visit_name__ = 'binary' - - def __init__(self, left, right, operator, type_=None, - negate=None, modifiers=None): - # allow compatibility with libraries that - # refer to BinaryExpression directly and pass strings - if isinstance(operator, util.string_types): - operator = operators.custom_op(operator) - self._orig = (left, right) - self.left = left.self_group(against=operator) - self.right = right.self_group(against=operator) - self.operator = operator - self.type = type_api.to_instance(type_) - self.negate = negate - - if modifiers is None: - self.modifiers = {} - else: - self.modifiers = modifiers - - def __bool__(self): - if self.operator in (operator.eq, operator.ne): - return self.operator(hash(self._orig[0]), hash(self._orig[1])) - else: - raise TypeError("Boolean value of this clause is not defined") - - __nonzero__ = __bool__ - - @property - def is_comparison(self): - return operators.is_comparison(self.operator) - - @property - def _from_objects(self): - return self.left._from_objects + self.right._from_objects - - def _copy_internals(self, clone=_clone, **kw): - self.left = clone(self.left, **kw) - self.right = clone(self.right, **kw) - - def get_children(self, **kwargs): - return self.left, self.right - - def compare(self, other, **kw): - """Compare this :class:`BinaryExpression` against the - given :class:`BinaryExpression`.""" - - return ( - isinstance(other, BinaryExpression) and - self.operator == other.operator and - ( - self.left.compare(other.left, **kw) and - self.right.compare(other.right, **kw) or - ( - operators.is_commutative(self.operator) and - self.left.compare(other.right, **kw) and - self.right.compare(other.left, **kw) - ) - ) - ) - - def self_group(self, against=None): - if operators.is_precedent(self.operator, against): - return Grouping(self) - else: - return self - - def _negate(self): - if self.negate is not None: - return BinaryExpression( - self.left, - self.right, - self.negate, - negate=self.operator, - type_=type_api.BOOLEANTYPE, - modifiers=self.modifiers) - else: - return super(BinaryExpression, self)._negate() - - - - -class Grouping(ColumnElement): - """Represent a grouping within a column expression""" - - __visit_name__ = 'grouping' - - def __init__(self, element): - self.element = element - self.type = getattr(element, 'type', type_api.NULLTYPE) - - def self_group(self, against=None): - return self - - @property - def _label(self): - return getattr(self.element, '_label', None) or self.anon_label - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - def get_children(self, **kwargs): - return self.element, - - @property - def _from_objects(self): - return self.element._from_objects - - def __getattr__(self, attr): - return getattr(self.element, attr) - - def __getstate__(self): - return {'element': self.element, 'type': self.type} - - def __setstate__(self, state): - self.element = state['element'] - self.type = state['type'] - - def compare(self, other, **kw): - return isinstance(other, Grouping) and \ - self.element.compare(other.element) - - -class Over(ColumnElement): - """Represent an OVER clause. - - This is a special operator against a so-called - "window" function, as well as any aggregate function, - which produces results relative to the result set - itself. It's supported only by certain database - backends. - - """ - __visit_name__ = 'over' - - order_by = None - partition_by = None - - def __init__(self, func, partition_by=None, order_by=None): - """Produce an :class:`.Over` object against a function. - - Used against aggregate or so-called "window" functions, - for database backends that support window functions. - - E.g.:: - - from sqlalchemy import over - over(func.row_number(), order_by='x') - - Would produce "ROW_NUMBER() OVER(ORDER BY x)". - - :param func: a :class:`.FunctionElement` construct, typically - generated by :data:`~.expression.func`. - :param partition_by: a column element or string, or a list - of such, that will be used as the PARTITION BY clause - of the OVER construct. - :param order_by: a column element or string, or a list - of such, that will be used as the ORDER BY clause - of the OVER construct. - - This function is also available from the :data:`~.expression.func` - construct itself via the :meth:`.FunctionElement.over` method. - - .. versionadded:: 0.7 - - """ - self.func = func - if order_by is not None: - self.order_by = ClauseList(*util.to_list(order_by)) - if partition_by is not None: - self.partition_by = ClauseList(*util.to_list(partition_by)) - - @util.memoized_property - def type(self): - return self.func.type - - def get_children(self, **kwargs): - return [c for c in - (self.func, self.partition_by, self.order_by) - if c is not None] - - def _copy_internals(self, clone=_clone, **kw): - self.func = clone(self.func, **kw) - if self.partition_by is not None: - self.partition_by = clone(self.partition_by, **kw) - if self.order_by is not None: - self.order_by = clone(self.order_by, **kw) - - @property - def _from_objects(self): - return list(itertools.chain( - *[c._from_objects for c in - (self.func, self.partition_by, self.order_by) - if c is not None] - )) - - -class Label(ColumnElement): - """Represents a column label (AS). - - Represent a label, as typically applied to any column-level - element using the ``AS`` sql keyword. - - """ - - __visit_name__ = 'label' - - def __init__(self, name, element, type_=None): - """Return a :class:`Label` object for the - given :class:`.ColumnElement`. - - A label changes the name of an element in the columns clause of a - ``SELECT`` statement, typically via the ``AS`` SQL keyword. - - This functionality is more conveniently available via the - :meth:`.ColumnElement.label` method on :class:`.ColumnElement`. - - :param name: label name - - :param obj: a :class:`.ColumnElement`. - - """ - while isinstance(element, Label): - element = element.element - if name: - self.name = name - else: - self.name = _anonymous_label('%%(%d %s)s' % (id(self), - getattr(element, 'name', 'anon'))) - self.key = self._label = self._key_label = self.name - self._element = element - self._type = type_ - self._proxies = [element] - - def __reduce__(self): - return self.__class__, (self.name, self._element, self._type) - - @util.memoized_property - def _order_by_label_element(self): - return self - - @util.memoized_property - def type(self): - return type_api.to_instance( - self._type or getattr(self._element, 'type', None) - ) - - @util.memoized_property - def element(self): - return self._element.self_group(against=operators.as_) - - def self_group(self, against=None): - sub_element = self._element.self_group(against=against) - if sub_element is not self._element: - return Label(self.name, - sub_element, - type_=self._type) - else: - return self - - @property - def primary_key(self): - return self.element.primary_key - - @property - def foreign_keys(self): - return self.element.foreign_keys - - def get_children(self, **kwargs): - return self.element, - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - @property - def _from_objects(self): - return self.element._from_objects - - def _make_proxy(self, selectable, name=None, **kw): - e = self.element._make_proxy(selectable, - name=name if name else self.name) - e._proxies.append(self) - if self._type is not None: - e.type = self._type - return e - - -class ColumnClause(Immutable, ColumnElement): - """Represents a generic column expression from any textual string. - - This includes columns associated with tables, aliases and select - statements, but also any arbitrary text. May or may not be bound - to an underlying :class:`.Selectable`. - - :class:`.ColumnClause` is constructed by itself typically via - the :func:`~.expression.column` function. It may be placed directly - into constructs such as :func:`.select` constructs:: - - from sqlalchemy.sql import column, select - - c1, c2 = column("c1"), column("c2") - s = select([c1, c2]).where(c1==5) - - There is also a variant on :func:`~.expression.column` known - as :func:`~.expression.literal_column` - the difference is that - in the latter case, the string value is assumed to be an exact - expression, rather than a column name, so that no quoting rules - or similar are applied:: - - from sqlalchemy.sql import literal_column, select - - s = select([literal_column("5 + 7")]) - - :class:`.ColumnClause` can also be used in a table-like - fashion by combining the :func:`~.expression.column` function - with the :func:`~.expression.table` function, to produce - a "lightweight" form of table metadata:: - - from sqlalchemy.sql import table, column - - user = table("user", - column("id"), - column("name"), - column("description"), - ) - - The above construct can be created in an ad-hoc fashion and is - not associated with any :class:`.schema.MetaData`, unlike it's - more full fledged :class:`.schema.Table` counterpart. - - """ - __visit_name__ = 'column' - - onupdate = default = server_default = server_onupdate = None - - _memoized_property = util.group_expirable_memoized_property() - - def __init__(self, text, type_=None, is_literal=False, _selectable=None): - """Construct a :class:`.ColumnClause` object. - - :param text: the text of the element. - - :param type: :class:`.types.TypeEngine` object which can associate - this :class:`.ColumnClause` with a type. - - :param is_literal: if True, the :class:`.ColumnClause` is assumed to - be an exact expression that will be delivered to the output with no - quoting rules applied regardless of case sensitive settings. the - :func:`literal_column()` function is usually used to create such a - :class:`.ColumnClause`. - - :param text: the name of the column. Quoting rules will be applied - to the clause like any other column name. For textual column constructs - that are not to be quoted, use the :func:`literal_column` function. - - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` object - which will provide result-set translation for this column. - - - """ - - self.key = self.name = text - self.table = _selectable - self.type = type_api.to_instance(type_) - self.is_literal = is_literal - - def _compare_name_for_result(self, other): - if self.is_literal or \ - self.table is None or \ - not hasattr(other, 'proxy_set') or ( - isinstance(other, ColumnClause) and other.is_literal - ): - return super(ColumnClause, self).\ - _compare_name_for_result(other) - else: - return other.proxy_set.intersection(self.proxy_set) - - def _get_table(self): - return self.__dict__['table'] - - def _set_table(self, table): - self._memoized_property.expire_instance(self) - self.__dict__['table'] = table - table = property(_get_table, _set_table) - - @_memoized_property - def _from_objects(self): - t = self.table - if t is not None: - return [t] - else: - return [] - - @util.memoized_property - def description(self): - if util.py3k: - return self.name - else: - return self.name.encode('ascii', 'backslashreplace') - - @_memoized_property - def _key_label(self): - if self.key != self.name: - return self._gen_label(self.key) - else: - return self._label - - @_memoized_property - def _label(self): - return self._gen_label(self.name) - - def _gen_label(self, name): - t = self.table - - if self.is_literal: - return None - - elif t is not None and t.named_with_column: - if getattr(t, 'schema', None): - label = t.schema.replace('.', '_') + "_" + \ - t.name + "_" + name - else: - label = t.name + "_" + name - - # propagate name quoting rules for labels. - if getattr(name, "quote", None) is not None: - if isinstance(label, quoted_name): - label.quote = name.quote - else: - label = quoted_name(label, name.quote) - elif getattr(t.name, "quote", None) is not None: - # can't get this situation to occur, so let's - # assert false on it for now - assert not isinstance(label, quoted_name) - label = quoted_name(label, t.name.quote) - - # ensure the label name doesn't conflict with that - # of an existing column - if label in t.c: - _label = label - counter = 1 - while _label in t.c: - _label = label + "_" + str(counter) - counter += 1 - label = _label - - return _as_truncated(label) - - else: - return name - - def _bind_param(self, operator, obj): - return BindParameter(self.name, obj, - _compared_to_operator=operator, - _compared_to_type=self.type, - unique=True) - - def _make_proxy(self, selectable, name=None, attach=True, - name_is_truncatable=False, **kw): - # propagate the "is_literal" flag only if we are keeping our name, - # otherwise its considered to be a label - is_literal = self.is_literal and (name is None or name == self.name) - c = self._constructor( - _as_truncated(name or self.name) if \ - name_is_truncatable else \ - (name or self.name), - type_=self.type, - _selectable=selectable, - is_literal=is_literal - ) - if name is None: - c.key = self.key - c._proxies = [self] - if selectable._is_clone_of is not None: - c._is_clone_of = \ - selectable._is_clone_of.columns.get(c.key) - - if attach: - selectable._columns[c.key] = c - return c - - -class _IdentifiedClause(Executable, ClauseElement): - - __visit_name__ = 'identified' - _execution_options = \ - Executable._execution_options.union({'autocommit': False}) - - def __init__(self, ident): - self.ident = ident - - -class SavepointClause(_IdentifiedClause): - __visit_name__ = 'savepoint' - - -class RollbackToSavepointClause(_IdentifiedClause): - __visit_name__ = 'rollback_to_savepoint' - - -class ReleaseSavepointClause(_IdentifiedClause): - __visit_name__ = 'release_savepoint' - - -class quoted_name(util.text_type): - """Represent a SQL identifier combined with quoting preferences. - - :class:`.quoted_name` is a Python unicode/str subclass which - represents a particular identifier name along with a - ``quote`` flag. This ``quote`` flag, when set to - ``True`` or ``False``, overrides automatic quoting behavior - for this identifier in order to either unconditionally quote - or to not quote the name. If left at its default of ``None``, - quoting behavior is applied to the identifier on a per-backend basis - based on an examination of the token itself. - - A :class:`.quoted_name` object with ``quote=True`` is also - prevented from being modified in the case of a so-called - "name normalize" option. Certain database backends, such as - Oracle, Firebird, and DB2 "normalize" case-insensitive names - as uppercase. The SQLAlchemy dialects for these backends - convert from SQLAlchemy's lower-case-means-insensitive convention - to the upper-case-means-insensitive conventions of those backends. - The ``quote=True`` flag here will prevent this conversion from occurring - to support an identifier that's quoted as all lower case against - such a backend. - - The :class:`.quoted_name` object is normally created automatically - when specifying the name for key schema constructs such as :class:`.Table`, - :class:`.Column`, and others. The class can also be passed explicitly - as the name to any function that receives a name which can be quoted. - Such as to use the :meth:`.Engine.has_table` method with an unconditionally - quoted name:: - - from sqlaclchemy import create_engine - from sqlalchemy.sql.elements import quoted_name - - engine = create_engine("oracle+cx_oracle://some_dsn") - engine.has_table(quoted_name("some_table", True)) - - The above logic will run the "has table" logic against the Oracle backend, - passing the name exactly as ``"some_table"`` without converting to - upper case. - - .. versionadded:: 0.9.0 - - """ - - def __new__(cls, value, quote): - if value is None: - return None - # experimental - don't bother with quoted_name - # if quote flag is None. doesn't seem to make any dent - # in performance however - # elif not sprcls and quote is None: - # return value - elif isinstance(value, cls) and ( - quote is None or value.quote == quote - ): - return value - self = super(quoted_name, cls).__new__(cls, value) - self.quote = quote - return self - - def __reduce__(self): - return quoted_name, (util.text_type(self), self.quote) - - @util.memoized_instancemethod - def lower(self): - if self.quote: - return self - else: - return util.text_type(self).lower() - - @util.memoized_instancemethod - def upper(self): - if self.quote: - return self - else: - return util.text_type(self).upper() - - def __repr__(self): - backslashed = self.encode('ascii', 'backslashreplace') - if not util.py2k: - backslashed = backslashed.decode('ascii') - return "'%s'" % backslashed - -class _truncated_label(quoted_name): - """A unicode subclass used to identify symbolic " - "names that may require truncation.""" - - def __new__(cls, value, quote=None): - quote = getattr(value, "quote", quote) - #return super(_truncated_label, cls).__new__(cls, value, quote, True) - return super(_truncated_label, cls).__new__(cls, value, quote) - - def __reduce__(self): - return self.__class__, (util.text_type(self), self.quote) - - def apply_map(self, map_): - return self - -# for backwards compatibility in case -# someone is re-implementing the -# _truncated_identifier() sequence in a custom -# compiler -_generated_label = _truncated_label - - -class _anonymous_label(_truncated_label): - """A unicode subclass used to identify anonymously - generated names.""" - - def __add__(self, other): - return _anonymous_label( - quoted_name( - util.text_type.__add__(self, util.text_type(other)), - self.quote) - ) - - def __radd__(self, other): - return _anonymous_label( - quoted_name( - util.text_type.__add__(util.text_type(other), self), - self.quote) - ) - - def apply_map(self, map_): - if self.quote is not None: - # preserve quoting only if necessary - return quoted_name(self % map_, self.quote) - else: - # else skip the constructor call - return self % map_ - - -def _as_truncated(value): - """coerce the given value to :class:`._truncated_label`. - - Existing :class:`._truncated_label` and - :class:`._anonymous_label` objects are passed - unchanged. - """ - - if isinstance(value, _truncated_label): - return value - else: - return _truncated_label(value) - - -def _string_or_unprintable(element): - if isinstance(element, util.string_types): - return element - else: - try: - return str(element) - except: - return "unprintable element %r" % element - - -def _expand_cloned(elements): - """expand the given set of ClauseElements to be the set of all 'cloned' - predecessors. - - """ - return itertools.chain(*[x._cloned_set for x in elements]) - - -def _select_iterables(elements): - """expand tables into individual columns in the - given list of column expressions. - - """ - return itertools.chain(*[c._select_iterable for c in elements]) - - -def _cloned_intersection(a, b): - """return the intersection of sets a and b, counting - any overlap between 'cloned' predecessors. - - The returned set is in terms of the entities present within 'a'. - - """ - all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) - return set(elem for elem in a - if all_overlap.intersection(elem._cloned_set)) - -def _cloned_difference(a, b): - all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) - return set(elem for elem in a - if not all_overlap.intersection(elem._cloned_set)) - - -def _labeled(element): - if not hasattr(element, 'name'): - return element.label(None) - else: - return element - - -def _is_column(col): - """True if ``col`` is an instance of :class:`.ColumnElement`.""" - - return isinstance(col, ColumnElement) - - -def _find_columns(clause): - """locate Column objects within the given expression.""" - - cols = util.column_set() - traverse(clause, {}, {'column': cols.add}) - return cols - - -# there is some inconsistency here between the usage of -# inspect() vs. checking for Visitable and __clause_element__. -# Ideally all functions here would derive from inspect(), -# however the inspect() versions add significant callcount -# overhead for critical functions like _interpret_as_column_or_from(). -# Generally, the column-based functions are more performance critical -# and are fine just checking for __clause_element__(). it's only -# _interpret_as_from() where we'd like to be able to receive ORM entities -# that have no defined namespace, hence inspect() is needed there. - - -def _column_as_key(element): - if isinstance(element, util.string_types): - return element - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - try: - return element.key - except AttributeError: - return None - - -def _clause_element_as_expr(element): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - else: - return element - - -def _literal_as_text(element): - if isinstance(element, Visitable): - return element - elif hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif isinstance(element, util.string_types): - return TextClause(util.text_type(element)) - elif isinstance(element, (util.NoneType, bool)): - return _const_expr(element) - else: - raise exc.ArgumentError( - "SQL expression object or string expected." - ) - - -def _no_literals(element): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif not isinstance(element, Visitable): - raise exc.ArgumentError("Ambiguous literal: %r. Use the 'text()' " - "function to indicate a SQL expression " - "literal, or 'literal()' to indicate a " - "bound value." % element) - else: - return element - - -def _is_literal(element): - return not isinstance(element, Visitable) and \ - not hasattr(element, '__clause_element__') - - -def _only_column_elements_or_none(element, name): - if element is None: - return None - else: - return _only_column_elements(element, name) - - -def _only_column_elements(element, name): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, ColumnElement): - raise exc.ArgumentError( - "Column-based expression object expected for argument " - "'%s'; got: '%s', type %s" % (name, element, type(element))) - return element - -def _literal_as_binds(element, name=None, type_=None): - if hasattr(element, '__clause_element__'): - return element.__clause_element__() - elif not isinstance(element, Visitable): - if element is None: - return Null() - else: - return BindParameter(name, element, type_=type_, unique=True) - else: - return element - - -def _interpret_as_column_or_from(element): - if isinstance(element, Visitable): - return element - elif hasattr(element, '__clause_element__'): - return element.__clause_element__() - - insp = inspection.inspect(element, raiseerr=False) - if insp is None: - if isinstance(element, (util.NoneType, bool)): - return _const_expr(element) - elif hasattr(insp, "selectable"): - return insp.selectable - - return ColumnClause(str(element), is_literal=True) - - -def _const_expr(element): - if isinstance(element, (Null, False_, True_)): - return element - elif element is None: - return Null() - elif element is False: - return False_() - elif element is True: - return True_() - else: - raise exc.ArgumentError( - "Expected None, False, or True" - ) - - -def _type_from_args(args): - for a in args: - if not a.type._isnull: - return a.type - else: - return type_api.NULLTYPE - - -def _corresponding_column_or_error(fromclause, column, - require_embedded=False): - c = fromclause.corresponding_column(column, - require_embedded=require_embedded) - if c is None: - raise exc.InvalidRequestError( - "Given column '%s', attached to table '%s', " - "failed to locate a corresponding column from table '%s'" - % - (column, - getattr(column, 'table', None), - fromclause.description) - ) - return c - - -class AnnotatedColumnElement(Annotated): - def __init__(self, element, values): - Annotated.__init__(self, element, values) - ColumnElement.comparator._reset(self) - for attr in ('name', 'key', 'table'): - if self.__dict__.get(attr, False) is None: - self.__dict__.pop(attr) - - def _with_annotations(self, values): - clone = super(AnnotatedColumnElement, self)._with_annotations(values) - ColumnElement.comparator._reset(clone) - return clone - - @util.memoized_property - def name(self): - """pull 'name' from parent, if not present""" - return self._Annotated__element.name - - @util.memoized_property - def table(self): - """pull 'table' from parent, if not present""" - return self._Annotated__element.table - - @util.memoized_property - def key(self): - """pull 'key' from parent, if not present""" - return self._Annotated__element.key - - @util.memoized_property - def info(self): - return self._Annotated__element.info - diff --git a/libs/sqlalchemy/sql/expression.py b/libs/sqlalchemy/sql/expression.py deleted file mode 100644 index c99665b4..00000000 --- a/libs/sqlalchemy/sql/expression.py +++ /dev/null @@ -1,127 +0,0 @@ -# sql/expression.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Defines the public namespace for SQL expression constructs. - -Prior to version 0.9, this module contained all of "elements", "dml", -"default_comparator" and "selectable". The module was broken up -and most "factory" functions were moved to be grouped with their associated -class. - -""" - -__all__ = [ - 'Alias', 'ClauseElement', 'ColumnCollection', 'ColumnElement', - 'CompoundSelect', 'Delete', 'FromClause', 'Insert', 'Join', 'Select', - 'Selectable', 'TableClause', 'Update', 'alias', 'and_', 'asc', 'between', - 'bindparam', 'case', 'cast', 'column', 'delete', 'desc', 'distinct', - 'except_', 'except_all', 'exists', 'extract', 'func', 'modifier', - 'collate', 'insert', 'intersect', 'intersect_all', 'join', 'label', - 'literal', 'literal_column', 'not_', 'null', 'nullsfirst', 'nullslast', - 'or_', 'outparam', 'outerjoin', 'over', 'select', 'subquery', - 'table', 'text', - 'tuple_', 'type_coerce', 'union', 'union_all', 'update'] - - -from .visitors import Visitable -from .functions import func, modifier, FunctionElement -from ..util.langhelpers import public_factory -from .elements import ClauseElement, ColumnElement,\ - BindParameter, UnaryExpression, BooleanClauseList, \ - Label, Cast, Case, ColumnClause, TextClause, Over, Null, \ - True_, False_, BinaryExpression, Tuple, TypeClause, Extract, \ - Grouping, not_, \ - collate, literal_column, between,\ - literal, outparam, type_coerce, ClauseList - -from .elements import SavepointClause, RollbackToSavepointClause, \ - ReleaseSavepointClause - -from .base import ColumnCollection, Generative, Executable, \ - PARSE_AUTOCOMMIT - -from .selectable import Alias, Join, Select, Selectable, TableClause, \ - CompoundSelect, CTE, FromClause, FromGrouping, SelectBase, \ - alias, GenerativeSelect, \ - subquery, HasPrefixes, Exists, ScalarSelect, TextAsFrom - - -from .dml import Insert, Update, Delete, UpdateBase, ValuesBase - -# factory functions - these pull class-bound constructors and classmethods -# from SQL elements and selectables into public functions. This allows -# the functions to be available in the sqlalchemy.sql.* namespace and -# to be auto-cross-documenting from the function to the class itself. - -and_ = public_factory(BooleanClauseList.and_, ".expression.and_") -or_ = public_factory(BooleanClauseList.or_, ".expression.or_") -bindparam = public_factory(BindParameter, ".expression.bindparam") -select = public_factory(Select, ".expression.select") -text = public_factory(TextClause._create_text, ".expression.text") -table = public_factory(TableClause, ".expression.table") -column = public_factory(ColumnClause, ".expression.column") -over = public_factory(Over, ".expression.over") -label = public_factory(Label, ".expression.label") -case = public_factory(Case, ".expression.case") -cast = public_factory(Cast, ".expression.cast") -extract = public_factory(Extract, ".expression.extract") -tuple_ = public_factory(Tuple, ".expression.tuple_") -except_ = public_factory(CompoundSelect._create_except, ".expression.except_") -except_all = public_factory(CompoundSelect._create_except_all, ".expression.except_all") -intersect = public_factory(CompoundSelect._create_intersect, ".expression.intersect") -intersect_all = public_factory(CompoundSelect._create_intersect_all, ".expression.intersect_all") -union = public_factory(CompoundSelect._create_union, ".expression.union") -union_all = public_factory(CompoundSelect._create_union_all, ".expression.union_all") -exists = public_factory(Exists, ".expression.exists") -nullsfirst = public_factory(UnaryExpression._create_nullsfirst, ".expression.nullsfirst") -nullslast = public_factory(UnaryExpression._create_nullslast, ".expression.nullslast") -asc = public_factory(UnaryExpression._create_asc, ".expression.asc") -desc = public_factory(UnaryExpression._create_desc, ".expression.desc") -distinct = public_factory(UnaryExpression._create_distinct, ".expression.distinct") -true = public_factory(True_._singleton, ".expression.true") -false = public_factory(False_._singleton, ".expression.false") -null = public_factory(Null._singleton, ".expression.null") -join = public_factory(Join._create_join, ".expression.join") -outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin") -insert = public_factory(Insert, ".expression.insert") -update = public_factory(Update, ".expression.update") -delete = public_factory(Delete, ".expression.delete") - - -# internal functions still being called from tests and the ORM, -# these might be better off in some other namespace -from .base import _from_objects -from .elements import _literal_as_text, _clause_element_as_expr,\ - _is_column, _labeled, _only_column_elements, _string_or_unprintable, \ - _truncated_label, _clone, _cloned_difference, _cloned_intersection,\ - _column_as_key, _literal_as_binds, _select_iterables, \ - _corresponding_column_or_error -from .selectable import _interpret_as_from - - - -# old names for compatibility -_Executable = Executable -_BindParamClause = BindParameter -_Label = Label -_SelectBase = SelectBase -_BinaryExpression = BinaryExpression -_Cast = Cast -_Null = Null -_False = False_ -_True = True_ -_TextClause = TextClause -_UnaryExpression = UnaryExpression -_Case = Case -_Tuple = Tuple -_Over = Over -_Generative = Generative -_TypeClause = TypeClause -_Extract = Extract -_Exists = Exists -_Grouping = Grouping -_FromGrouping = FromGrouping -_ScalarSelect = ScalarSelect diff --git a/libs/sqlalchemy/sql/functions.py b/libs/sqlalchemy/sql/functions.py deleted file mode 100644 index f300e241..00000000 --- a/libs/sqlalchemy/sql/functions.py +++ /dev/null @@ -1,534 +0,0 @@ -# sql/functions.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""SQL function API, factories, and built-in functions. - -""" -from . import sqltypes, schema -from .base import Executable -from .elements import ClauseList, Cast, Extract, _literal_as_binds, \ - literal_column, _type_from_args, ColumnElement, _clone,\ - Over, BindParameter -from .selectable import FromClause, Select - -from . import operators -from .visitors import VisitableType -from .. import util - -_registry = util.defaultdict(dict) - - -def register_function(identifier, fn, package="_default"): - """Associate a callable with a particular func. name. - - This is normally called by _GenericMeta, but is also - available by itself so that a non-Function construct - can be associated with the :data:`.func` accessor (i.e. - CAST, EXTRACT). - - """ - reg = _registry[package] - reg[identifier] = fn - - -class FunctionElement(Executable, ColumnElement, FromClause): - """Base for SQL function-oriented constructs. - - .. seealso:: - - :class:`.Function` - named SQL function. - - :data:`.func` - namespace which produces registered or ad-hoc - :class:`.Function` instances. - - :class:`.GenericFunction` - allows creation of registered function - types. - - """ - - packagenames = () - - def __init__(self, *clauses, **kwargs): - """Construct a :class:`.FunctionElement`. - """ - args = [_literal_as_binds(c, self.name) for c in clauses] - self.clause_expr = ClauseList( - operator=operators.comma_op, - group_contents=True, *args).\ - self_group() - - def _execute_on_connection(self, connection, multiparams, params): - return connection._execute_function(self, multiparams, params) - - @property - def columns(self): - """Fulfill the 'columns' contract of :class:`.ColumnElement`. - - Returns a single-element list consisting of this object. - - """ - return [self] - - @util.memoized_property - def clauses(self): - """Return the underlying :class:`.ClauseList` which contains - the arguments for this :class:`.FunctionElement`. - - """ - return self.clause_expr.element - - def over(self, partition_by=None, order_by=None): - """Produce an OVER clause against this function. - - Used against aggregate or so-called "window" functions, - for database backends that support window functions. - - The expression:: - - func.row_number().over(order_by='x') - - is shorthand for:: - - from sqlalchemy import over - over(func.row_number(), order_by='x') - - See :func:`~.expression.over` for a full description. - - .. versionadded:: 0.7 - - """ - return Over(self, partition_by=partition_by, order_by=order_by) - - @property - def _from_objects(self): - return self.clauses._from_objects - - def get_children(self, **kwargs): - return self.clause_expr, - - def _copy_internals(self, clone=_clone, **kw): - self.clause_expr = clone(self.clause_expr, **kw) - self._reset_exported() - FunctionElement.clauses._reset(self) - - def select(self): - """Produce a :func:`~.expression.select` construct - against this :class:`.FunctionElement`. - - This is shorthand for:: - - s = select([function_element]) - - """ - s = Select([self]) - if self._execution_options: - s = s.execution_options(**self._execution_options) - return s - - def scalar(self): - """Execute this :class:`.FunctionElement` against an embedded - 'bind' and return a scalar value. - - This first calls :meth:`~.FunctionElement.select` to - produce a SELECT construct. - - Note that :class:`.FunctionElement` can be passed to - the :meth:`.Connectable.scalar` method of :class:`.Connection` - or :class:`.Engine`. - - """ - return self.select().execute().scalar() - - def execute(self): - """Execute this :class:`.FunctionElement` against an embedded - 'bind'. - - This first calls :meth:`~.FunctionElement.select` to - produce a SELECT construct. - - Note that :class:`.FunctionElement` can be passed to - the :meth:`.Connectable.execute` method of :class:`.Connection` - or :class:`.Engine`. - - """ - return self.select().execute() - - def _bind_param(self, operator, obj): - return BindParameter(None, obj, _compared_to_operator=operator, - _compared_to_type=self.type, unique=True) - - -class _FunctionGenerator(object): - """Generate :class:`.Function` objects based on getattr calls.""" - - def __init__(self, **opts): - self.__names = [] - self.opts = opts - - def __getattr__(self, name): - # passthru __ attributes; fixes pydoc - if name.startswith('__'): - try: - return self.__dict__[name] - except KeyError: - raise AttributeError(name) - - elif name.endswith('_'): - name = name[0:-1] - f = _FunctionGenerator(**self.opts) - f.__names = list(self.__names) + [name] - return f - - def __call__(self, *c, **kwargs): - o = self.opts.copy() - o.update(kwargs) - - tokens = len(self.__names) - - if tokens == 2: - package, fname = self.__names - elif tokens == 1: - package, fname = "_default", self.__names[0] - else: - package = None - - if package is not None: - func = _registry[package].get(fname) - if func is not None: - return func(*c, **o) - - return Function(self.__names[-1], - packagenames=self.__names[0:-1], *c, **o) - - -func = _FunctionGenerator() -"""Generate SQL function expressions. - - :data:`.func` is a special object instance which generates SQL - functions based on name-based attributes, e.g.:: - - >>> print func.count(1) - count(:param_1) - - The element is a column-oriented SQL element like any other, and is - used in that way:: - - >>> print select([func.count(table.c.id)]) - SELECT count(sometable.id) FROM sometable - - Any name can be given to :data:`.func`. If the function name is unknown to - SQLAlchemy, it will be rendered exactly as is. For common SQL functions - which SQLAlchemy is aware of, the name may be interpreted as a *generic - function* which will be compiled appropriately to the target database:: - - >>> print func.current_timestamp() - CURRENT_TIMESTAMP - - To call functions which are present in dot-separated packages, - specify them in the same manner:: - - >>> print func.stats.yield_curve(5, 10) - stats.yield_curve(:yield_curve_1, :yield_curve_2) - - SQLAlchemy can be made aware of the return type of functions to enable - type-specific lexical and result-based behavior. For example, to ensure - that a string-based function returns a Unicode value and is similarly - treated as a string in expressions, specify - :class:`~sqlalchemy.types.Unicode` as the type: - - >>> print func.my_string(u'hi', type_=Unicode) + ' ' + \ - ... func.my_string(u'there', type_=Unicode) - my_string(:my_string_1) || :my_string_2 || my_string(:my_string_3) - - The object returned by a :data:`.func` call is usually an instance of - :class:`.Function`. - This object meets the "column" interface, including comparison and labeling - functions. The object can also be passed the :meth:`~.Connectable.execute` - method of a :class:`.Connection` or :class:`.Engine`, where it will be - wrapped inside of a SELECT statement first:: - - print connection.execute(func.current_timestamp()).scalar() - - In a few exception cases, the :data:`.func` accessor - will redirect a name to a built-in expression such as :func:`.cast` - or :func:`.extract`, as these names have well-known meaning - but are not exactly the same as "functions" from a SQLAlchemy - perspective. - - .. versionadded:: 0.8 :data:`.func` can return non-function expression - constructs for common quasi-functional names like :func:`.cast` - and :func:`.extract`. - - Functions which are interpreted as "generic" functions know how to - calculate their return type automatically. For a listing of known generic - functions, see :ref:`generic_functions`. - -""" - -modifier = _FunctionGenerator(group=False) - -class Function(FunctionElement): - """Describe a named SQL function. - - See the superclass :class:`.FunctionElement` for a description - of public methods. - - .. seealso:: - - :data:`.func` - namespace which produces registered or ad-hoc - :class:`.Function` instances. - - :class:`.GenericFunction` - allows creation of registered function - types. - - """ - - __visit_name__ = 'function' - - def __init__(self, name, *clauses, **kw): - """Construct a :class:`.Function`. - - The :data:`.func` construct is normally used to construct - new :class:`.Function` instances. - - """ - self.packagenames = kw.pop('packagenames', None) or [] - self.name = name - self._bind = kw.get('bind', None) - self.type = sqltypes.to_instance(kw.get('type_', None)) - - FunctionElement.__init__(self, *clauses, **kw) - - def _bind_param(self, operator, obj): - return BindParameter(self.name, obj, - _compared_to_operator=operator, - _compared_to_type=self.type, - unique=True) - - -class _GenericMeta(VisitableType): - def __init__(cls, clsname, bases, clsdict): - cls.name = name = clsdict.get('name', clsname) - cls.identifier = identifier = clsdict.get('identifier', name) - package = clsdict.pop('package', '_default') - # legacy - if '__return_type__' in clsdict: - cls.type = clsdict['__return_type__'] - register_function(identifier, cls, package) - super(_GenericMeta, cls).__init__(clsname, bases, clsdict) - - -class GenericFunction(util.with_metaclass(_GenericMeta, Function)): - """Define a 'generic' function. - - A generic function is a pre-established :class:`.Function` - class that is instantiated automatically when called - by name from the :data:`.func` attribute. Note that - calling any name from :data:`.func` has the effect that - a new :class:`.Function` instance is created automatically, - given that name. The primary use case for defining - a :class:`.GenericFunction` class is so that a function - of a particular name may be given a fixed return type. - It can also include custom argument parsing schemes as well - as additional methods. - - Subclasses of :class:`.GenericFunction` are automatically - registered under the name of the class. For - example, a user-defined function ``as_utc()`` would - be available immediately:: - - from sqlalchemy.sql.functions import GenericFunction - from sqlalchemy.types import DateTime - - class as_utc(GenericFunction): - type = DateTime - - print select([func.as_utc()]) - - User-defined generic functions can be organized into - packages by specifying the "package" attribute when defining - :class:`.GenericFunction`. Third party libraries - containing many functions may want to use this in order - to avoid name conflicts with other systems. For example, - if our ``as_utc()`` function were part of a package - "time":: - - class as_utc(GenericFunction): - type = DateTime - package = "time" - - The above function would be available from :data:`.func` - using the package name ``time``:: - - print select([func.time.as_utc()]) - - A final option is to allow the function to be accessed - from one name in :data:`.func` but to render as a different name. - The ``identifier`` attribute will override the name used to - access the function as loaded from :data:`.func`, but will retain - the usage of ``name`` as the rendered name:: - - class GeoBuffer(GenericFunction): - type = Geometry - package = "geo" - name = "ST_Buffer" - identifier = "buffer" - - The above function will render as follows:: - - >>> print func.geo.buffer() - ST_Buffer() - - .. versionadded:: 0.8 :class:`.GenericFunction` now supports - automatic registration of new functions as well as package - and custom naming support. - - .. versionchanged:: 0.8 The attribute name ``type`` is used - to specify the function's return type at the class level. - Previously, the name ``__return_type__`` was used. This - name is still recognized for backwards-compatibility. - - """ - - coerce_arguments = True - - def __init__(self, *args, **kwargs): - parsed_args = kwargs.pop('_parsed_args', None) - if parsed_args is None: - parsed_args = [_literal_as_binds(c) for c in args] - self.packagenames = [] - self._bind = kwargs.get('bind', None) - self.clause_expr = ClauseList( - operator=operators.comma_op, - group_contents=True, *parsed_args).self_group() - self.type = sqltypes.to_instance( - kwargs.pop("type_", None) or getattr(self, 'type', None)) - - -register_function("cast", Cast) -register_function("extract", Extract) - - -class next_value(GenericFunction): - """Represent the 'next value', given a :class:`.Sequence` - as it's single argument. - - Compiles into the appropriate function on each backend, - or will raise NotImplementedError if used on a backend - that does not provide support for sequences. - - """ - type = sqltypes.Integer() - name = "next_value" - - def __init__(self, seq, **kw): - assert isinstance(seq, schema.Sequence), \ - "next_value() accepts a Sequence object as input." - self._bind = kw.get('bind', None) - self.sequence = seq - - @property - def _from_objects(self): - return [] - - -class AnsiFunction(GenericFunction): - def __init__(self, **kwargs): - GenericFunction.__init__(self, **kwargs) - - -class ReturnTypeFromArgs(GenericFunction): - """Define a function whose return type is the same as its arguments.""" - - def __init__(self, *args, **kwargs): - args = [_literal_as_binds(c) for c in args] - kwargs.setdefault('type_', _type_from_args(args)) - kwargs['_parsed_args'] = args - GenericFunction.__init__(self, *args, **kwargs) - - -class coalesce(ReturnTypeFromArgs): - pass - - -class max(ReturnTypeFromArgs): - pass - - -class min(ReturnTypeFromArgs): - pass - - -class sum(ReturnTypeFromArgs): - pass - - -class now(GenericFunction): - type = sqltypes.DateTime - - -class concat(GenericFunction): - type = sqltypes.String - - -class char_length(GenericFunction): - type = sqltypes.Integer - - def __init__(self, arg, **kwargs): - GenericFunction.__init__(self, arg, **kwargs) - - -class random(GenericFunction): - pass - - -class count(GenericFunction): - """The ANSI COUNT aggregate function. With no arguments, - emits COUNT \*. - - """ - type = sqltypes.Integer - - def __init__(self, expression=None, **kwargs): - if expression is None: - expression = literal_column('*') - GenericFunction.__init__(self, expression, **kwargs) - - -class current_date(AnsiFunction): - type = sqltypes.Date - - -class current_time(AnsiFunction): - type = sqltypes.Time - - -class current_timestamp(AnsiFunction): - type = sqltypes.DateTime - - -class current_user(AnsiFunction): - type = sqltypes.String - - -class localtime(AnsiFunction): - type = sqltypes.DateTime - - -class localtimestamp(AnsiFunction): - type = sqltypes.DateTime - - -class session_user(AnsiFunction): - type = sqltypes.String - - -class sysdate(AnsiFunction): - type = sqltypes.DateTime - - -class user(AnsiFunction): - type = sqltypes.String diff --git a/libs/sqlalchemy/sql/operators.py b/libs/sqlalchemy/sql/operators.py deleted file mode 100644 index d7ec977a..00000000 --- a/libs/sqlalchemy/sql/operators.py +++ /dev/null @@ -1,854 +0,0 @@ -# sql/operators.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Defines operators used in SQL expressions.""" - -from .. import util - - -from operator import ( - and_, or_, inv, add, mul, sub, mod, truediv, lt, le, ne, gt, ge, eq, neg, - getitem, lshift, rshift - ) - -if util.py2k: - from operator import div -else: - div = truediv - - - -class Operators(object): - """Base of comparison and logical operators. - - Implements base methods :meth:`~sqlalchemy.sql.operators.Operators.operate` and - :meth:`~sqlalchemy.sql.operators.Operators.reverse_operate`, as well as - :meth:`~sqlalchemy.sql.operators.Operators.__and__`, - :meth:`~sqlalchemy.sql.operators.Operators.__or__`, - :meth:`~sqlalchemy.sql.operators.Operators.__invert__`. - - Usually is used via its most common subclass - :class:`.ColumnOperators`. - - """ - def __and__(self, other): - """Implement the ``&`` operator. - - When used with SQL expressions, results in an - AND operation, equivalent to - :func:`~.expression.and_`, that is:: - - a & b - - is equivalent to:: - - from sqlalchemy import and_ - and_(a, b) - - Care should be taken when using ``&`` regarding - operator precedence; the ``&`` operator has the highest precedence. - The operands should be enclosed in parenthesis if they contain - further sub expressions:: - - (a == 2) & (b == 4) - - """ - return self.operate(and_, other) - - def __or__(self, other): - """Implement the ``|`` operator. - - When used with SQL expressions, results in an - OR operation, equivalent to - :func:`~.expression.or_`, that is:: - - a | b - - is equivalent to:: - - from sqlalchemy import or_ - or_(a, b) - - Care should be taken when using ``|`` regarding - operator precedence; the ``|`` operator has the highest precedence. - The operands should be enclosed in parenthesis if they contain - further sub expressions:: - - (a == 2) | (b == 4) - - """ - return self.operate(or_, other) - - def __invert__(self): - """Implement the ``~`` operator. - - When used with SQL expressions, results in a - NOT operation, equivalent to - :func:`~.expression.not_`, that is:: - - ~a - - is equivalent to:: - - from sqlalchemy import not_ - not_(a) - - """ - return self.operate(inv) - - def op(self, opstring, precedence=0): - """produce a generic operator function. - - e.g.:: - - somecolumn.op("*")(5) - - produces:: - - somecolumn * 5 - - This function can also be used to make bitwise operators explicit. For - example:: - - somecolumn.op('&')(0xff) - - is a bitwise AND of the value in ``somecolumn``. - - :param operator: a string which will be output as the infix operator - between this element and the expression passed to the - generated function. - - :param precedence: precedence to apply to the operator, when - parenthesizing expressions. A lower number will cause the expression - to be parenthesized when applied against another operator with - higher precedence. The default value of ``0`` is lower than all - operators except for the comma (``,``) and ``AS`` operators. - A value of 100 will be higher or equal to all operators, and -100 - will be lower than or equal to all operators. - - .. versionadded:: 0.8 - added the 'precedence' argument. - - .. seealso:: - - :ref:`types_operators` - - """ - operator = custom_op(opstring, precedence) - - def against(other): - return operator(self, other) - return against - - def operate(self, op, *other, **kwargs): - """Operate on an argument. - - This is the lowest level of operation, raises - :class:`NotImplementedError` by default. - - Overriding this on a subclass can allow common - behavior to be applied to all operations. - For example, overriding :class:`.ColumnOperators` - to apply ``func.lower()`` to the left and right - side:: - - class MyComparator(ColumnOperators): - def operate(self, op, other): - return op(func.lower(self), func.lower(other)) - - :param op: Operator callable. - :param \*other: the 'other' side of the operation. Will - be a single scalar for most operations. - :param \**kwargs: modifiers. These may be passed by special - operators such as :meth:`ColumnOperators.contains`. - - - """ - raise NotImplementedError(str(op)) - - def reverse_operate(self, op, other, **kwargs): - """Reverse operate on an argument. - - Usage is the same as :meth:`operate`. - - """ - raise NotImplementedError(str(op)) - - -class custom_op(object): - """Represent a 'custom' operator. - - :class:`.custom_op` is normally instantitated when the - :meth:`.ColumnOperators.op` method is used to create a - custom operator callable. The class can also be used directly - when programmatically constructing expressions. E.g. - to represent the "factorial" operation:: - - from sqlalchemy.sql import UnaryExpression - from sqlalchemy.sql import operators - from sqlalchemy import Numeric - - unary = UnaryExpression(table.c.somecolumn, - modifier=operators.custom_op("!"), - type_=Numeric) - - """ - __name__ = 'custom_op' - - def __init__(self, opstring, precedence=0): - self.opstring = opstring - self.precedence = precedence - - def __eq__(self, other): - return isinstance(other, custom_op) and \ - other.opstring == self.opstring - - def __hash__(self): - return id(self) - - def __call__(self, left, right, **kw): - return left.operate(self, right, **kw) - - -class ColumnOperators(Operators): - """Defines boolean, comparison, and other operators for - :class:`.ColumnElement` expressions. - - By default, all methods call down to - :meth:`.operate` or :meth:`.reverse_operate`, - passing in the appropriate operator function from the - Python builtin ``operator`` module or - a SQLAlchemy-specific operator function from - :mod:`sqlalchemy.expression.operators`. For example - the ``__eq__`` function:: - - def __eq__(self, other): - return self.operate(operators.eq, other) - - Where ``operators.eq`` is essentially:: - - def eq(a, b): - return a == b - - The core column expression unit :class:`.ColumnElement` - overrides :meth:`.Operators.operate` and others - to return further :class:`.ColumnElement` constructs, - so that the ``==`` operation above is replaced by a clause - construct. - - See also: - - :ref:`types_operators` - - :attr:`.TypeEngine.comparator_factory` - - :class:`.ColumnOperators` - - :class:`.PropComparator` - - """ - - timetuple = None - """Hack, allows datetime objects to be compared on the LHS.""" - - def __lt__(self, other): - """Implement the ``<`` operator. - - In a column context, produces the clause ``a < b``. - - """ - return self.operate(lt, other) - - def __le__(self, other): - """Implement the ``<=`` operator. - - In a column context, produces the clause ``a <= b``. - - """ - return self.operate(le, other) - - __hash__ = Operators.__hash__ - - def __eq__(self, other): - """Implement the ``==`` operator. - - In a column context, produces the clause ``a = b``. - If the target is ``None``, produces ``a IS NULL``. - - """ - return self.operate(eq, other) - - def __ne__(self, other): - """Implement the ``!=`` operator. - - In a column context, produces the clause ``a != b``. - If the target is ``None``, produces ``a IS NOT NULL``. - - """ - return self.operate(ne, other) - - def __gt__(self, other): - """Implement the ``>`` operator. - - In a column context, produces the clause ``a > b``. - - """ - return self.operate(gt, other) - - def __ge__(self, other): - """Implement the ``>=`` operator. - - In a column context, produces the clause ``a >= b``. - - """ - return self.operate(ge, other) - - def __neg__(self): - """Implement the ``-`` operator. - - In a column context, produces the clause ``-a``. - - """ - return self.operate(neg) - - def __getitem__(self, index): - """Implement the [] operator. - - This can be used by some database-specific types - such as Postgresql ARRAY and HSTORE. - - """ - return self.operate(getitem, index) - - def __lshift__(self, other): - """implement the << operator. - - Not used by SQLAlchemy core, this is provided - for custom operator systems which want to use - << as an extension point. - """ - return self.operate(lshift, other) - - def __rshift__(self, other): - """implement the >> operator. - - Not used by SQLAlchemy core, this is provided - for custom operator systems which want to use - >> as an extension point. - """ - return self.operate(rshift, other) - - def concat(self, other): - """Implement the 'concat' operator. - - In a column context, produces the clause ``a || b``, - or uses the ``concat()`` operator on MySQL. - - """ - return self.operate(concat_op, other) - - def like(self, other, escape=None): - """Implement the ``like`` operator. - - In a column context, produces the clause ``a LIKE other``. - - E.g.:: - - select([sometable]).where(sometable.c.column.like("%foobar%")) - - :param other: expression to be compared - :param escape: optional escape character, renders the ``ESCAPE`` - keyword, e.g.:: - - somecolumn.like("foo/%bar", escape="/") - - .. seealso:: - - :meth:`.ColumnOperators.ilike` - - """ - return self.operate(like_op, other, escape=escape) - - def ilike(self, other, escape=None): - """Implement the ``ilike`` operator. - - In a column context, produces the clause ``a ILIKE other``. - - E.g.:: - - select([sometable]).where(sometable.c.column.ilike("%foobar%")) - - :param other: expression to be compared - :param escape: optional escape character, renders the ``ESCAPE`` - keyword, e.g.:: - - somecolumn.ilike("foo/%bar", escape="/") - - .. seealso:: - - :meth:`.ColumnOperators.like` - - """ - return self.operate(ilike_op, other, escape=escape) - - def in_(self, other): - """Implement the ``in`` operator. - - In a column context, produces the clause ``a IN other``. - "other" may be a tuple/list of column expressions, - or a :func:`~.expression.select` construct. - - """ - return self.operate(in_op, other) - - def notin_(self, other): - """implement the ``NOT IN`` operator. - - This is equivalent to using negation with :meth:`.ColumnOperators.in_`, - i.e. ``~x.in_(y)``. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.ColumnOperators.in_` - - """ - return self.operate(notin_op, other) - - def notlike(self, other, escape=None): - """implement the ``NOT LIKE`` operator. - - This is equivalent to using negation with - :meth:`.ColumnOperators.like`, i.e. ``~x.like(y)``. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.ColumnOperators.like` - - """ - return self.operate(notlike_op, other, escape=escape) - - def notilike(self, other, escape=None): - """implement the ``NOT ILIKE`` operator. - - This is equivalent to using negation with - :meth:`.ColumnOperators.ilike`, i.e. ``~x.ilike(y)``. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.ColumnOperators.ilike` - - """ - return self.operate(notilike_op, other, escape=escape) - - def is_(self, other): - """Implement the ``IS`` operator. - - Normally, ``IS`` is generated automatically when comparing to a - value of ``None``, which resolves to ``NULL``. However, explicit - usage of ``IS`` may be desirable if comparing to boolean values - on certain platforms. - - .. versionadded:: 0.7.9 - - .. seealso:: :meth:`.ColumnOperators.isnot` - - """ - return self.operate(is_, other) - - def isnot(self, other): - """Implement the ``IS NOT`` operator. - - Normally, ``IS NOT`` is generated automatically when comparing to a - value of ``None``, which resolves to ``NULL``. However, explicit - usage of ``IS NOT`` may be desirable if comparing to boolean values - on certain platforms. - - .. versionadded:: 0.7.9 - - .. seealso:: :meth:`.ColumnOperators.is_` - - """ - return self.operate(isnot, other) - - def startswith(self, other, **kwargs): - """Implement the ``startwith`` operator. - - In a column context, produces the clause ``LIKE '%'`` - - """ - return self.operate(startswith_op, other, **kwargs) - - def endswith(self, other, **kwargs): - """Implement the 'endswith' operator. - - In a column context, produces the clause ``LIKE '%'`` - - """ - return self.operate(endswith_op, other, **kwargs) - - def contains(self, other, **kwargs): - """Implement the 'contains' operator. - - In a column context, produces the clause ``LIKE '%%'`` - - """ - return self.operate(contains_op, other, **kwargs) - - def match(self, other, **kwargs): - """Implements the 'match' operator. - - In a column context, this produces a MATCH clause, i.e. - ``MATCH ''``. The allowed contents of ``other`` - are database backend specific. - - """ - return self.operate(match_op, other, **kwargs) - - def desc(self): - """Produce a :func:`~.expression.desc` clause against the - parent object.""" - return self.operate(desc_op) - - def asc(self): - """Produce a :func:`~.expression.asc` clause against the - parent object.""" - return self.operate(asc_op) - - def nullsfirst(self): - """Produce a :func:`~.expression.nullsfirst` clause against the - parent object.""" - return self.operate(nullsfirst_op) - - def nullslast(self): - """Produce a :func:`~.expression.nullslast` clause against the - parent object.""" - return self.operate(nullslast_op) - - def collate(self, collation): - """Produce a :func:`~.expression.collate` clause against - the parent object, given the collation string.""" - return self.operate(collate, collation) - - def __radd__(self, other): - """Implement the ``+`` operator in reverse. - - See :meth:`.ColumnOperators.__add__`. - - """ - return self.reverse_operate(add, other) - - def __rsub__(self, other): - """Implement the ``-`` operator in reverse. - - See :meth:`.ColumnOperators.__sub__`. - - """ - return self.reverse_operate(sub, other) - - def __rmul__(self, other): - """Implement the ``*`` operator in reverse. - - See :meth:`.ColumnOperators.__mul__`. - - """ - return self.reverse_operate(mul, other) - - def __rdiv__(self, other): - """Implement the ``/`` operator in reverse. - - See :meth:`.ColumnOperators.__div__`. - - """ - return self.reverse_operate(div, other) - - def between(self, cleft, cright): - """Produce a :func:`~.expression.between` clause against - the parent object, given the lower and upper range.""" - return self.operate(between_op, cleft, cright) - - def distinct(self): - """Produce a :func:`~.expression.distinct` clause against the - parent object. - - """ - return self.operate(distinct_op) - - def __add__(self, other): - """Implement the ``+`` operator. - - In a column context, produces the clause ``a + b`` - if the parent object has non-string affinity. - If the parent object has a string affinity, - produces the concatenation operator, ``a || b`` - - see :meth:`.ColumnOperators.concat`. - - """ - return self.operate(add, other) - - def __sub__(self, other): - """Implement the ``-`` operator. - - In a column context, produces the clause ``a - b``. - - """ - return self.operate(sub, other) - - def __mul__(self, other): - """Implement the ``*`` operator. - - In a column context, produces the clause ``a * b``. - - """ - return self.operate(mul, other) - - def __div__(self, other): - """Implement the ``/`` operator. - - In a column context, produces the clause ``a / b``. - - """ - return self.operate(div, other) - - def __mod__(self, other): - """Implement the ``%`` operator. - - In a column context, produces the clause ``a % b``. - - """ - return self.operate(mod, other) - - def __truediv__(self, other): - """Implement the ``//`` operator. - - In a column context, produces the clause ``a / b``. - - """ - return self.operate(truediv, other) - - def __rtruediv__(self, other): - """Implement the ``//`` operator in reverse. - - See :meth:`.ColumnOperators.__truediv__`. - - """ - return self.reverse_operate(truediv, other) - - -def from_(): - raise NotImplementedError() - - -def as_(): - raise NotImplementedError() - - -def exists(): - raise NotImplementedError() - - -def istrue(a): - raise NotImplementedError() - -def isfalse(a): - raise NotImplementedError() - -def is_(a, b): - return a.is_(b) - - -def isnot(a, b): - return a.isnot(b) - - -def collate(a, b): - return a.collate(b) - - -def op(a, opstring, b): - return a.op(opstring)(b) - - -def like_op(a, b, escape=None): - return a.like(b, escape=escape) - - -def notlike_op(a, b, escape=None): - return a.notlike(b, escape=escape) - - -def ilike_op(a, b, escape=None): - return a.ilike(b, escape=escape) - - -def notilike_op(a, b, escape=None): - return a.notilike(b, escape=escape) - - -def between_op(a, b, c): - return a.between(b, c) - - -def in_op(a, b): - return a.in_(b) - - -def notin_op(a, b): - return a.notin_(b) - - -def distinct_op(a): - return a.distinct() - - -def startswith_op(a, b, escape=None): - return a.startswith(b, escape=escape) - - -def notstartswith_op(a, b, escape=None): - return ~a.startswith(b, escape=escape) - - -def endswith_op(a, b, escape=None): - return a.endswith(b, escape=escape) - - -def notendswith_op(a, b, escape=None): - return ~a.endswith(b, escape=escape) - - -def contains_op(a, b, escape=None): - return a.contains(b, escape=escape) - - -def notcontains_op(a, b, escape=None): - return ~a.contains(b, escape=escape) - - -def match_op(a, b): - return a.match(b) - - -def comma_op(a, b): - raise NotImplementedError() - - -def concat_op(a, b): - return a.concat(b) - - -def desc_op(a): - return a.desc() - - -def asc_op(a): - return a.asc() - - -def nullsfirst_op(a): - return a.nullsfirst() - - -def nullslast_op(a): - return a.nullslast() - - -_commutative = set([eq, ne, add, mul]) - -_comparison = set([eq, ne, lt, gt, ge, le, between_op]) - - -def is_comparison(op): - return op in _comparison - - -def is_commutative(op): - return op in _commutative - - -def is_ordering_modifier(op): - return op in (asc_op, desc_op, - nullsfirst_op, nullslast_op) - -_associative = _commutative.union([concat_op, and_, or_]) - -_natural_self_precedent = _associative.union([getitem]) -"""Operators where if we have (a op b) op c, we don't want to -parenthesize (a op b). - -""" - -_asbool = util.symbol('_asbool', canonical=-10) -_smallest = util.symbol('_smallest', canonical=-100) -_largest = util.symbol('_largest', canonical=100) - -_PRECEDENCE = { - from_: 15, - getitem: 15, - mul: 8, - truediv: 8, - div: 8, - mod: 8, - neg: 8, - add: 7, - sub: 7, - - concat_op: 6, - match_op: 6, - - ilike_op: 6, - notilike_op: 6, - like_op: 6, - notlike_op: 6, - in_op: 6, - notin_op: 6, - - is_: 6, - isnot: 6, - - eq: 5, - ne: 5, - gt: 5, - lt: 5, - ge: 5, - le: 5, - - between_op: 5, - distinct_op: 5, - inv: 5, - istrue: 5, - isfalse: 5, - and_: 3, - or_: 2, - comma_op: -1, - - desc_op: 3, - asc_op: 3, - collate: 4, - - as_: -1, - exists: 0, - _asbool: -10, - _smallest: _smallest, - _largest: _largest -} - - -def is_precedent(operator, against): - if operator is against and operator in _natural_self_precedent: - return False - else: - return (_PRECEDENCE.get(operator, - getattr(operator, 'precedence', _smallest)) <= - _PRECEDENCE.get(against, - getattr(against, 'precedence', _largest))) diff --git a/libs/sqlalchemy/sql/schema.py b/libs/sqlalchemy/sql/schema.py deleted file mode 100644 index 6ee92871..00000000 --- a/libs/sqlalchemy/sql/schema.py +++ /dev/null @@ -1,3150 +0,0 @@ -# sql/schema.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The schema module provides the building blocks for database metadata. - -Each element within this module describes a database entity which can be -created and dropped, or is otherwise part of such an entity. Examples include -tables, columns, sequences, and indexes. - -All entities are subclasses of :class:`~sqlalchemy.schema.SchemaItem`, and as -defined in this module they are intended to be agnostic of any vendor-specific -constructs. - -A collection of entities are grouped into a unit called -:class:`~sqlalchemy.schema.MetaData`. MetaData serves as a logical grouping of -schema elements, and can also be associated with an actual database connection -such that operations involving the contained elements can contact the database -as needed. - -Two of the elements here also build upon their "syntactic" counterparts, which -are defined in :class:`~sqlalchemy.sql.expression.`, specifically -:class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.schema.Column`. -Since these objects are part of the SQL expression language, they are usable -as components in SQL expressions. - -""" - -import re -import inspect -from .. import exc, util, event, inspection -from .base import SchemaEventTarget -from . import visitors -from . import type_api -from .base import _bind_or_error, ColumnCollection -from .elements import ClauseElement, ColumnClause, _truncated_label, \ - _as_truncated, TextClause, _literal_as_text,\ - ColumnElement, _find_columns, quoted_name -from .selectable import TableClause -import collections -import sqlalchemy -from . import ddl - -RETAIN_SCHEMA = util.symbol('retain_schema') - - -def _get_table_key(name, schema): - if schema is None: - return name - else: - return schema + "." + name - - -def _validate_dialect_kwargs(kwargs, name): - # validate remaining kwargs that they all specify DB prefixes - - for k in kwargs: - m = re.match('^(.+?)_.*', k) - if m is None: - raise TypeError("Additional arguments should be " - "named _, got '%s'" % k) - -@inspection._self_inspects -class SchemaItem(SchemaEventTarget, visitors.Visitable): - """Base class for items that define a database schema.""" - - __visit_name__ = 'schema_item' - - def _execute_on_connection(self, connection, multiparams, params): - return connection._execute_default(self, multiparams, params) - - def _init_items(self, *args): - """Initialize the list of child items for this SchemaItem.""" - - for item in args: - if item is not None: - item._set_parent_with_dispatch(self) - - def get_children(self, **kwargs): - """used to allow SchemaVisitor access""" - return [] - - def __repr__(self): - return util.generic_repr(self) - - @property - @util.deprecated('0.9', 'Use ``.name.quote``') - def quote(self): - """Return the value of the ``quote`` flag passed - to this schema object, for those schema items which - have a ``name`` field. - - """ - - return self.name.quote - - @util.memoized_property - def info(self): - """Info dictionary associated with the object, allowing user-defined - data to be associated with this :class:`.SchemaItem`. - - The dictionary is automatically generated when first accessed. - It can also be specified in the constructor of some objects, - such as :class:`.Table` and :class:`.Column`. - - """ - return {} - - def _schema_item_copy(self, schema_item): - if 'info' in self.__dict__: - schema_item.info = self.info.copy() - schema_item.dispatch._update(self.dispatch) - return schema_item - - -class Table(SchemaItem, TableClause): - """Represent a table in a database. - - e.g.:: - - mytable = Table("mytable", metadata, - Column('mytable_id', Integer, primary_key=True), - Column('value', String(50)) - ) - - The :class:`.Table` object constructs a unique instance of itself based - on its name and optional schema name within the given - :class:`.MetaData` object. Calling the :class:`.Table` - constructor with the same name and same :class:`.MetaData` argument - a second time will return the *same* :class:`.Table` object - in this way - the :class:`.Table` constructor acts as a registry function. - - .. seealso:: - - :ref:`metadata_describing` - Introduction to database metadata - - Constructor arguments are as follows: - - :param name: The name of this table as represented in the database. - - The table name, along with the value of the ``schema`` parameter, - forms a key which uniquely identifies this :class:`.Table` within - the owning :class:`.MetaData` collection. - Additional calls to :class:`.Table` with the same name, metadata, - and schema name will return the same :class:`.Table` object. - - Names which contain no upper case characters - will be treated as case insensitive names, and will not be quoted - unless they are a reserved word or contain special characters. - A name with any number of upper case characters is considered - to be case sensitive, and will be sent as quoted. - - To enable unconditional quoting for the table name, specify the flag - ``quote=True`` to the constructor, or use the :class:`.quoted_name` - construct to specify the name. - - :param metadata: a :class:`.MetaData` object which will contain this - table. The metadata is used as a point of association of this table - with other tables which are referenced via foreign key. It also - may be used to associate this table with a particular - :class:`.Connectable`. - - :param \*args: Additional positional arguments are used primarily - to add the list of :class:`.Column` objects contained within this - table. Similar to the style of a CREATE TABLE statement, other - :class:`.SchemaItem` constructs may be added here, including - :class:`.PrimaryKeyConstraint`, and :class:`.ForeignKeyConstraint`. - - :param autoload: Defaults to False: the Columns for this table should - be reflected from the database. Usually there will be no Column - objects in the constructor if this property is set. - - :param autoload_replace: If ``True``, when using ``autoload=True`` - and ``extend_existing=True``, - replace ``Column`` objects already present in the ``Table`` that's - in the ``MetaData`` registry with - what's reflected. Otherwise, all existing columns will be - excluded from the reflection process. Note that this does - not impact ``Column`` objects specified in the same call to ``Table`` - which includes ``autoload``, those always take precedence. - Defaults to ``True``. - - .. versionadded:: 0.7.5 - - :param autoload_with: If autoload==True, this is an optional Engine - or Connection instance to be used for the table reflection. If - ``None``, the underlying MetaData's bound connectable will be used. - - :param extend_existing: When ``True``, indicates that if this - :class:`.Table` is already present in the given :class:`.MetaData`, - apply further arguments within the constructor to the existing - :class:`.Table`. - - If ``extend_existing`` or ``keep_existing`` are not set, an error is - raised if additional table modifiers are specified when - the given :class:`.Table` is already present in the :class:`.MetaData`. - - .. versionchanged:: 0.7.4 - ``extend_existing`` will work in conjunction - with ``autoload=True`` to run a new reflection operation against - the database; new :class:`.Column` objects will be produced - from database metadata to replace those existing with the same - name, and additional :class:`.Column` objects not present - in the :class:`.Table` will be added. - - As is always the case with ``autoload=True``, :class:`.Column` - objects can be specified in the same :class:`.Table` constructor, - which will take precedence. I.e.:: - - Table("mytable", metadata, - Column('y', Integer), - extend_existing=True, - autoload=True, - autoload_with=engine - ) - - The above will overwrite all columns within ``mytable`` which - are present in the database, except for ``y`` which will be used as is - from the above definition. If the ``autoload_replace`` flag - is set to False, no existing columns will be replaced. - - :param implicit_returning: True by default - indicates that - RETURNING can be used by default to fetch newly inserted primary key - values, for backends which support this. Note that - create_engine() also provides an implicit_returning flag. - - :param include_columns: A list of strings indicating a subset of - columns to be loaded via the ``autoload`` operation; table columns who - aren't present in this list will not be represented on the resulting - ``Table`` object. Defaults to ``None`` which indicates all columns - should be reflected. - - :param info: Optional data dictionary which will be populated into the - :attr:`.SchemaItem.info` attribute of this object. - - :param keep_existing: When ``True``, indicates that if this Table - is already present in the given :class:`.MetaData`, ignore - further arguments within the constructor to the existing - :class:`.Table`, and return the :class:`.Table` object as - originally created. This is to allow a function that wishes - to define a new :class:`.Table` on first call, but on - subsequent calls will return the same :class:`.Table`, - without any of the declarations (particularly constraints) - being applied a second time. Also see extend_existing. - - If extend_existing or keep_existing are not set, an error is - raised if additional table modifiers are specified when - the given :class:`.Table` is already present in the :class:`.MetaData`. - - :param listeners: A list of tuples of the form ``(, )`` - which will be passed to :func:`.event.listen` upon construction. - This alternate hook to :func:`.event.listen` allows the establishment - of a listener function specific to this :class:`.Table` before - the "autoload" process begins. Particularly useful for - the :meth:`.DDLEvents.column_reflect` event:: - - def listen_for_reflect(table, column_info): - "handle the column reflection event" - # ... - - t = Table( - 'sometable', - autoload=True, - listeners=[ - ('column_reflect', listen_for_reflect) - ]) - - :param mustexist: When ``True``, indicates that this Table must already - be present in the given :class:`.MetaData` collection, else - an exception is raised. - - :param prefixes: - A list of strings to insert after CREATE in the CREATE TABLE - statement. They will be separated by spaces. - - :param quote: Force quoting of this table's name on or off, corresponding - to ``True`` or ``False``. When left at its default of ``None``, - the column identifier will be quoted according to whether the name is - case sensitive (identifiers with at least one upper case character are - treated as case sensitive), or if it's a reserved word. This flag - is only needed to force quoting of a reserved word which is not known - by the SQLAlchemy dialect. - - :param quote_schema: same as 'quote' but applies to the schema identifier. - - :param schema: The schema name for this table, which is required if - the table resides in a schema other than the default selected schema - for the engine's database connection. Defaults to ``None``. - - The quoting rules for the schema name are the same as those for the - ``name`` parameter, in that quoting is applied for reserved words or - case-sensitive names; to enable unconditional quoting for the - schema name, specify the flag - ``quote_schema=True`` to the constructor, or use the :class:`.quoted_name` - construct to specify the name. - - - :param useexisting: Deprecated. Use extend_existing. - - """ - - __visit_name__ = 'table' - - def __new__(cls, *args, **kw): - if not args: - # python3k pickle seems to call this - return object.__new__(cls) - - try: - name, metadata, args = args[0], args[1], args[2:] - except IndexError: - raise TypeError("Table() takes at least two arguments") - - schema = kw.get('schema', None) - if schema is None: - schema = metadata.schema - keep_existing = kw.pop('keep_existing', False) - extend_existing = kw.pop('extend_existing', False) - if 'useexisting' in kw: - msg = "useexisting is deprecated. Use extend_existing." - util.warn_deprecated(msg) - if extend_existing: - msg = "useexisting is synonymous with extend_existing." - raise exc.ArgumentError(msg) - extend_existing = kw.pop('useexisting', False) - - if keep_existing and extend_existing: - msg = "keep_existing and extend_existing are mutually exclusive." - raise exc.ArgumentError(msg) - - mustexist = kw.pop('mustexist', False) - key = _get_table_key(name, schema) - if key in metadata.tables: - if not keep_existing and not extend_existing and bool(args): - raise exc.InvalidRequestError( - "Table '%s' is already defined for this MetaData " - "instance. Specify 'extend_existing=True' " - "to redefine " - "options and columns on an " - "existing Table object." % key) - table = metadata.tables[key] - if extend_existing: - table._init_existing(*args, **kw) - return table - else: - if mustexist: - raise exc.InvalidRequestError( - "Table '%s' not defined" % (key)) - table = object.__new__(cls) - table.dispatch.before_parent_attach(table, metadata) - metadata._add_table(name, schema, table) - try: - table._init(name, metadata, *args, **kw) - table.dispatch.after_parent_attach(table, metadata) - return table - except: - #metadata._remove_table(name, schema) - raise - - - @property - @util.deprecated('0.9', 'Use ``table.schema.quote``') - def quote_schema(self): - """Return the value of the ``quote_schema`` flag passed - to this :class:`.Table`. - """ - - return self.schema.quote - - def __init__(self, *args, **kw): - """Constructor for :class:`~.schema.Table`. - - This method is a no-op. See the top-level - documentation for :class:`~.schema.Table` - for constructor arguments. - - """ - # __init__ is overridden to prevent __new__ from - # calling the superclass constructor. - - def _init(self, name, metadata, *args, **kwargs): - super(Table, self).__init__(quoted_name(name, kwargs.pop('quote', None))) - self.metadata = metadata - - self.schema = kwargs.pop('schema', None) - if self.schema is None: - self.schema = metadata.schema - else: - quote_schema = kwargs.pop('quote_schema', None) - self.schema = quoted_name(self.schema, quote_schema) - - self.indexes = set() - self.constraints = set() - self._columns = ColumnCollection() - PrimaryKeyConstraint()._set_parent_with_dispatch(self) - self.foreign_keys = set() - self._extra_dependencies = set() - self.kwargs = {} - if self.schema is not None: - self.fullname = "%s.%s" % (self.schema, self.name) - else: - self.fullname = self.name - - autoload = kwargs.pop('autoload', False) - autoload_with = kwargs.pop('autoload_with', None) - # this argument is only used with _init_existing() - kwargs.pop('autoload_replace', True) - include_columns = kwargs.pop('include_columns', None) - - self.implicit_returning = kwargs.pop('implicit_returning', True) - - if 'info' in kwargs: - self.info = kwargs.pop('info') - if 'listeners' in kwargs: - listeners = kwargs.pop('listeners') - for evt, fn in listeners: - event.listen(self, evt, fn) - - self._prefixes = kwargs.pop('prefixes', []) - - self._extra_kwargs(**kwargs) - - # load column definitions from the database if 'autoload' is defined - # we do it after the table is in the singleton dictionary to support - # circular foreign keys - if autoload: - self._autoload(metadata, autoload_with, include_columns) - - # initialize all the column, etc. objects. done after reflection to - # allow user-overrides - self._init_items(*args) - - def _autoload(self, metadata, autoload_with, include_columns, - exclude_columns=()): - if self.primary_key.columns: - PrimaryKeyConstraint(*[ - c for c in self.primary_key.columns - if c.key in exclude_columns - ])._set_parent_with_dispatch(self) - - if autoload_with: - autoload_with.run_callable( - autoload_with.dialect.reflecttable, - self, include_columns, exclude_columns - ) - else: - bind = _bind_or_error(metadata, - msg="No engine is bound to this Table's MetaData. " - "Pass an engine to the Table via " - "autoload_with=, " - "or associate the MetaData with an engine via " - "metadata.bind=") - bind.run_callable( - bind.dialect.reflecttable, - self, include_columns, exclude_columns - ) - - @property - def _sorted_constraints(self): - """Return the set of constraints as a list, sorted by creation - order. - - """ - return sorted(self.constraints, key=lambda c: c._creation_order) - - def _init_existing(self, *args, **kwargs): - autoload = kwargs.pop('autoload', False) - autoload_with = kwargs.pop('autoload_with', None) - autoload_replace = kwargs.pop('autoload_replace', True) - schema = kwargs.pop('schema', None) - if schema and schema != self.schema: - raise exc.ArgumentError( - "Can't change schema of existing table from '%s' to '%s'", - (self.schema, schema)) - - include_columns = kwargs.pop('include_columns', None) - - if include_columns is not None: - for c in self.c: - if c.name not in include_columns: - self._columns.remove(c) - - for key in ('quote', 'quote_schema'): - if key in kwargs: - raise exc.ArgumentError( - "Can't redefine 'quote' or 'quote_schema' arguments") - - if 'info' in kwargs: - self.info = kwargs.pop('info') - - if autoload: - if not autoload_replace: - exclude_columns = [c.name for c in self.c] - else: - exclude_columns = () - self._autoload( - self.metadata, autoload_with, include_columns, exclude_columns) - - self._extra_kwargs(**kwargs) - self._init_items(*args) - - def _extra_kwargs(self, **kwargs): - # validate remaining kwargs that they all specify DB prefixes - _validate_dialect_kwargs(kwargs, "Table") - self.kwargs.update(kwargs) - - def _init_collections(self): - pass - - @util.memoized_property - def _autoincrement_column(self): - for col in self.primary_key: - if col.autoincrement and \ - col.type._type_affinity is not None and \ - issubclass(col.type._type_affinity, type_api.INTEGERTYPE._type_affinity) and \ - (not col.foreign_keys or col.autoincrement == 'ignore_fk') and \ - isinstance(col.default, (type(None), Sequence)) and \ - (col.server_default is None or col.server_default.reflected): - return col - - @property - def key(self): - """Return the 'key' for this :class:`.Table`. - - This value is used as the dictionary key within the - :attr:`.MetaData.tables` collection. It is typically the same - as that of :attr:`.Table.name` for a table with no :attr:`.Table.schema` - set; otherwise it is typically of the form ``schemaname.tablename``. - - """ - return _get_table_key(self.name, self.schema) - - def __repr__(self): - return "Table(%s)" % ', '.join( - [repr(self.name)] + [repr(self.metadata)] + - [repr(x) for x in self.columns] + - ["%s=%s" % (k, repr(getattr(self, k))) for k in ['schema']]) - - def __str__(self): - return _get_table_key(self.description, self.schema) - - @property - def bind(self): - """Return the connectable associated with this Table.""" - - return self.metadata and self.metadata.bind or None - - def add_is_dependent_on(self, table): - """Add a 'dependency' for this Table. - - This is another Table object which must be created - first before this one can, or dropped after this one. - - Usually, dependencies between tables are determined via - ForeignKey objects. However, for other situations that - create dependencies outside of foreign keys (rules, inheriting), - this method can manually establish such a link. - - """ - self._extra_dependencies.add(table) - - def append_column(self, column): - """Append a :class:`~.schema.Column` to this :class:`~.schema.Table`. - - The "key" of the newly added :class:`~.schema.Column`, i.e. the - value of its ``.key`` attribute, will then be available - in the ``.c`` collection of this :class:`~.schema.Table`, and the - column definition will be included in any CREATE TABLE, SELECT, - UPDATE, etc. statements generated from this :class:`~.schema.Table` - construct. - - Note that this does **not** change the definition of the table - as it exists within any underlying database, assuming that - table has already been created in the database. Relational - databases support the addition of columns to existing tables - using the SQL ALTER command, which would need to be - emitted for an already-existing table that doesn't contain - the newly added column. - - """ - - column._set_parent_with_dispatch(self) - - def append_constraint(self, constraint): - """Append a :class:`~.schema.Constraint` to this - :class:`~.schema.Table`. - - This has the effect of the constraint being included in any - future CREATE TABLE statement, assuming specific DDL creation - events have not been associated with the given - :class:`~.schema.Constraint` object. - - Note that this does **not** produce the constraint within the - relational database automatically, for a table that already exists - in the database. To add a constraint to an - existing relational database table, the SQL ALTER command must - be used. SQLAlchemy also provides the - :class:`.AddConstraint` construct which can produce this SQL when - invoked as an executable clause. - - """ - - constraint._set_parent_with_dispatch(self) - - def append_ddl_listener(self, event_name, listener): - """Append a DDL event listener to this ``Table``. - - .. deprecated:: 0.7 - See :class:`.DDLEvents`. - - """ - - def adapt_listener(target, connection, **kw): - listener(event_name, target, connection) - - event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) - - def _set_parent(self, metadata): - metadata._add_table(self.name, self.schema, self) - self.metadata = metadata - - def get_children(self, column_collections=True, - schema_visitor=False, **kw): - if not schema_visitor: - return TableClause.get_children( - self, column_collections=column_collections, **kw) - else: - if column_collections: - return list(self.columns) - else: - return [] - - def exists(self, bind=None): - """Return True if this table exists.""" - - if bind is None: - bind = _bind_or_error(self) - - return bind.run_callable(bind.dialect.has_table, - self.name, schema=self.schema) - - def create(self, bind=None, checkfirst=False): - """Issue a ``CREATE`` statement for this - :class:`.Table`, using the given :class:`.Connectable` - for connectivity. - - .. seealso:: - - :meth:`.MetaData.create_all`. - - """ - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst) - - def drop(self, bind=None, checkfirst=False): - """Issue a ``DROP`` statement for this - :class:`.Table`, using the given :class:`.Connectable` - for connectivity. - - .. seealso:: - - :meth:`.MetaData.drop_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst) - - def tometadata(self, metadata, schema=RETAIN_SCHEMA): - """Return a copy of this :class:`.Table` associated with a different - :class:`.MetaData`. - - E.g.:: - - some_engine = create_engine("sqlite:///some.db") - - # create two metadata - meta1 = MetaData() - meta2 = MetaData() - - # load 'users' from the sqlite engine - users_table = Table('users', meta1, autoload=True, - autoload_with=some_engine) - - # create the same Table object for the plain metadata - users_table_2 = users_table.tometadata(meta2) - - :param metadata: Target :class:`.MetaData` object. - :param schema: Optional string name of a target schema, or - ``None`` for no schema. The :class:`.Table` object will be - given this schema name upon copy. Defaults to the special - symbol :attr:`.RETAIN_SCHEMA` which indicates no change should be - made to the schema name of the resulting :class:`.Table`. - - """ - - if schema is RETAIN_SCHEMA: - schema = self.schema - elif schema is None: - schema = metadata.schema - key = _get_table_key(self.name, schema) - if key in metadata.tables: - util.warn("Table '%s' already exists within the given " - "MetaData - not copying." % self.description) - return metadata.tables[key] - - args = [] - for c in self.columns: - args.append(c.copy(schema=schema)) - table = Table( - self.name, metadata, schema=schema, - *args, **self.kwargs - ) - for c in self.constraints: - table.append_constraint(c.copy(schema=schema, target_table=table)) - - for index in self.indexes: - # skip indexes that would be generated - # by the 'index' flag on Column - if len(index.columns) == 1 and \ - list(index.columns)[0].index: - continue - Index(index.name, - unique=index.unique, - *[table.c[col] for col in index.columns.keys()], - **index.kwargs) - return self._schema_item_copy(table) - - -class Column(SchemaItem, ColumnClause): - """Represents a column in a database table.""" - - __visit_name__ = 'column' - - def __init__(self, *args, **kwargs): - """ - Construct a new ``Column`` object. - - :param name: The name of this column as represented in the database. - This argument may be the first positional argument, or specified - via keyword. - - Names which contain no upper case characters - will be treated as case insensitive names, and will not be quoted - unless they are a reserved word. Names with any number of upper - case characters will be quoted and sent exactly. Note that this - behavior applies even for databases which standardize upper - case names as case insensitive such as Oracle. - - The name field may be omitted at construction time and applied - later, at any time before the Column is associated with a - :class:`.Table`. This is to support convenient - usage within the :mod:`~sqlalchemy.ext.declarative` extension. - - :param type\_: The column's type, indicated using an instance which - subclasses :class:`~sqlalchemy.types.TypeEngine`. If no arguments - are required for the type, the class of the type can be sent - as well, e.g.:: - - # use a type with arguments - Column('data', String(50)) - - # use no arguments - Column('level', Integer) - - The ``type`` argument may be the second positional argument - or specified by keyword. - - If the ``type`` is ``None`` or is omitted, it will first default to the special - type :class:`.NullType`. If and when this :class:`.Column` is - made to refer to another column using :class:`.ForeignKey` - and/or :class:`.ForeignKeyConstraint`, the type of the remote-referenced - column will be copied to this column as well, at the moment that - the foreign key is resolved against that remote :class:`.Column` - object. - - .. versionchanged:: 0.9.0 - Support for propagation of type to a :class:`.Column` from its - :class:`.ForeignKey` object has been improved and should be - more reliable and timely. - - :param \*args: Additional positional arguments include various - :class:`.SchemaItem` derived constructs which will be applied - as options to the column. These include instances of - :class:`.Constraint`, :class:`.ForeignKey`, :class:`.ColumnDefault`, - and :class:`.Sequence`. In some cases an equivalent keyword - argument is available such as ``server_default``, ``default`` - and ``unique``. - - :param autoincrement: This flag may be set to ``False`` to - indicate an integer primary key column that should not be - considered to be the "autoincrement" column, that is - the integer primary key column which generates values - implicitly upon INSERT and whose value is usually returned - via the DBAPI cursor.lastrowid attribute. It defaults - to ``True`` to satisfy the common use case of a table - with a single integer primary key column. If the table - has a composite primary key consisting of more than one - integer column, set this flag to True only on the - column that should be considered "autoincrement". - - The setting *only* has an effect for columns which are: - - * Integer derived (i.e. INT, SMALLINT, BIGINT). - - * Part of the primary key - - * Are not referenced by any foreign keys, unless - the value is specified as ``'ignore_fk'`` - - .. versionadded:: 0.7.4 - - * have no server side or client side defaults (with the exception - of Postgresql SERIAL). - - The setting has these two effects on columns that meet the - above criteria: - - * DDL issued for the column will include database-specific - keywords intended to signify this column as an - "autoincrement" column, such as AUTO INCREMENT on MySQL, - SERIAL on Postgresql, and IDENTITY on MS-SQL. It does - *not* issue AUTOINCREMENT for SQLite since this is a - special SQLite flag that is not required for autoincrementing - behavior. See the SQLite dialect documentation for - information on SQLite's AUTOINCREMENT. - - * The column will be considered to be available as - cursor.lastrowid or equivalent, for those dialects which - "post fetch" newly inserted identifiers after a row has - been inserted (SQLite, MySQL, MS-SQL). It does not have - any effect in this regard for databases that use sequences - to generate primary key identifiers (i.e. Firebird, Postgresql, - Oracle). - - .. versionchanged:: 0.7.4 - ``autoincrement`` accepts a special value ``'ignore_fk'`` - to indicate that autoincrementing status regardless of foreign - key references. This applies to certain composite foreign key - setups, such as the one demonstrated in the ORM documentation - at :ref:`post_update`. - - :param default: A scalar, Python callable, or - :class:`.ColumnElement` expression representing the - *default value* for this column, which will be invoked upon insert - if this column is otherwise not specified in the VALUES clause of - the insert. This is a shortcut to using :class:`.ColumnDefault` as - a positional argument; see that class for full detail on the - structure of the argument. - - Contrast this argument to ``server_default`` which creates a - default generator on the database side. - - :param doc: optional String that can be used by the ORM or similar - to document attributes. This attribute does not render SQL - comments (a future attribute 'comment' will achieve that). - - :param key: An optional string identifier which will identify this - ``Column`` object on the :class:`.Table`. When a key is provided, - this is the only identifier referencing the ``Column`` within the - application, including ORM attribute mapping; the ``name`` field - is used only when rendering SQL. - - :param index: When ``True``, indicates that the column is indexed. - This is a shortcut for using a :class:`.Index` construct on the - table. To specify indexes with explicit names or indexes that - contain multiple columns, use the :class:`.Index` construct - instead. - - :param info: Optional data dictionary which will be populated into the - :attr:`.SchemaItem.info` attribute of this object. - - :param nullable: If set to the default of ``True``, indicates the - column will be rendered as allowing NULL, else it's rendered as - NOT NULL. This parameter is only used when issuing CREATE TABLE - statements. - - :param onupdate: A scalar, Python callable, or - :class:`~sqlalchemy.sql.expression.ClauseElement` representing a - default value to be applied to the column within UPDATE - statements, which wil be invoked upon update if this column is not - present in the SET clause of the update. This is a shortcut to - using :class:`.ColumnDefault` as a positional argument with - ``for_update=True``. - - :param primary_key: If ``True``, marks this column as a primary key - column. Multiple columns can have this flag set to specify - composite primary keys. As an alternative, the primary key of a - :class:`.Table` can be specified via an explicit - :class:`.PrimaryKeyConstraint` object. - - :param server_default: A :class:`.FetchedValue` instance, str, Unicode - or :func:`~sqlalchemy.sql.expression.text` construct representing - the DDL DEFAULT value for the column. - - String types will be emitted as-is, surrounded by single quotes:: - - Column('x', Text, server_default="val") - - x TEXT DEFAULT 'val' - - A :func:`~sqlalchemy.sql.expression.text` expression will be - rendered as-is, without quotes:: - - Column('y', DateTime, server_default=text('NOW()')) - - y DATETIME DEFAULT NOW() - - Strings and text() will be converted into a :class:`.DefaultClause` - object upon initialization. - - Use :class:`.FetchedValue` to indicate that an already-existing - column will generate a default value on the database side which - will be available to SQLAlchemy for post-fetch after inserts. This - construct does not specify any DDL and the implementation is left - to the database, such as via a trigger. - - :param server_onupdate: A :class:`.FetchedValue` instance - representing a database-side default generation function. This - indicates to SQLAlchemy that a newly generated value will be - available after updates. This construct does not specify any DDL - and the implementation is left to the database, such as via a - trigger. - - :param quote: Force quoting of this column's name on or off, - corresponding to ``True`` or ``False``. When left at its default - of ``None``, the column identifier will be quoted according to - whether the name is case sensitive (identifiers with at least one - upper case character are treated as case sensitive), or if it's a - reserved word. This flag is only needed to force quoting of a - reserved word which is not known by the SQLAlchemy dialect. - - :param unique: When ``True``, indicates that this column contains a - unique constraint, or if ``index`` is ``True`` as well, indicates - that the :class:`.Index` should be created with the unique flag. - To specify multiple columns in the constraint/index or to specify - an explicit name, use the :class:`.UniqueConstraint` or - :class:`.Index` constructs explicitly. - - :param system: When ``True``, indicates this is a "system" column, - that is a column which is automatically made available by the - database, and should not be included in the columns list for a - ``CREATE TABLE`` statement. - - For more elaborate scenarios where columns should be conditionally - rendered differently on different backends, consider custom - compilation rules for :class:`.CreateColumn`. - - ..versionadded:: 0.8.3 Added the ``system=True`` parameter to - :class:`.Column`. - - """ - - name = kwargs.pop('name', None) - type_ = kwargs.pop('type_', None) - args = list(args) - if args: - if isinstance(args[0], util.string_types): - if name is not None: - raise exc.ArgumentError( - "May not pass name positionally and as a keyword.") - name = args.pop(0) - if args: - coltype = args[0] - - if hasattr(coltype, "_sqla_type"): - if type_ is not None: - raise exc.ArgumentError( - "May not pass type_ positionally and as a keyword.") - type_ = args.pop(0) - - if name is not None: - name = quoted_name(name, kwargs.pop('quote', None)) - elif "quote" in kwargs: - raise exc.ArgumentError("Explicit 'name' is required when " - "sending 'quote' argument") - - super(Column, self).__init__(name, type_) - self.key = kwargs.pop('key', name) - self.primary_key = kwargs.pop('primary_key', False) - self.nullable = kwargs.pop('nullable', not self.primary_key) - self.default = kwargs.pop('default', None) - self.server_default = kwargs.pop('server_default', None) - self.server_onupdate = kwargs.pop('server_onupdate', None) - - # these default to None because .index and .unique is *not* - # an informational flag about Column - there can still be an - # Index or UniqueConstraint referring to this Column. - self.index = kwargs.pop('index', None) - self.unique = kwargs.pop('unique', None) - - self.system = kwargs.pop('system', False) - self.doc = kwargs.pop('doc', None) - self.onupdate = kwargs.pop('onupdate', None) - self.autoincrement = kwargs.pop('autoincrement', True) - self.constraints = set() - self.foreign_keys = set() - - # check if this Column is proxying another column - if '_proxies' in kwargs: - self._proxies = kwargs.pop('_proxies') - # otherwise, add DDL-related events - elif isinstance(self.type, SchemaEventTarget): - self.type._set_parent_with_dispatch(self) - - if self.default is not None: - if isinstance(self.default, (ColumnDefault, Sequence)): - args.append(self.default) - else: - if getattr(self.type, '_warn_on_bytestring', False): - if isinstance(self.default, util.binary_type): - util.warn("Unicode column received non-unicode " - "default value.") - args.append(ColumnDefault(self.default)) - - if self.server_default is not None: - if isinstance(self.server_default, FetchedValue): - args.append(self.server_default._as_for_update(False)) - else: - args.append(DefaultClause(self.server_default)) - - if self.onupdate is not None: - if isinstance(self.onupdate, (ColumnDefault, Sequence)): - args.append(self.onupdate) - else: - args.append(ColumnDefault(self.onupdate, for_update=True)) - - if self.server_onupdate is not None: - if isinstance(self.server_onupdate, FetchedValue): - args.append(self.server_onupdate._as_for_update(True)) - else: - args.append(DefaultClause(self.server_onupdate, - for_update=True)) - self._init_items(*args) - - util.set_creation_order(self) - - if 'info' in kwargs: - self.info = kwargs.pop('info') - - if kwargs: - raise exc.ArgumentError( - "Unknown arguments passed to Column: " + repr(list(kwargs))) - -# @property -# def quote(self): -# return getattr(self.name, "quote", None) - - def __str__(self): - if self.name is None: - return "(no name)" - elif self.table is not None: - if self.table.named_with_column: - return (self.table.description + "." + self.description) - else: - return self.description - else: - return self.description - - def references(self, column): - """Return True if this Column references the given column via foreign - key.""" - - for fk in self.foreign_keys: - if fk.column.proxy_set.intersection(column.proxy_set): - return True - else: - return False - - def append_foreign_key(self, fk): - fk._set_parent_with_dispatch(self) - - def __repr__(self): - kwarg = [] - if self.key != self.name: - kwarg.append('key') - if self.primary_key: - kwarg.append('primary_key') - if not self.nullable: - kwarg.append('nullable') - if self.onupdate: - kwarg.append('onupdate') - if self.default: - kwarg.append('default') - if self.server_default: - kwarg.append('server_default') - return "Column(%s)" % ', '.join( - [repr(self.name)] + [repr(self.type)] + - [repr(x) for x in self.foreign_keys if x is not None] + - [repr(x) for x in self.constraints] + - [(self.table is not None and "table=<%s>" % - self.table.description or "table=None")] + - ["%s=%s" % (k, repr(getattr(self, k))) for k in kwarg]) - - def _set_parent(self, table): - if not self.name: - raise exc.ArgumentError( - "Column must be constructed with a non-blank name or " - "assign a non-blank .name before adding to a Table.") - if self.key is None: - self.key = self.name - - existing = getattr(self, 'table', None) - if existing is not None and existing is not table: - raise exc.ArgumentError( - "Column object already assigned to Table '%s'" % - existing.description) - - if self.key in table._columns: - col = table._columns.get(self.key) - if col is not self: - for fk in col.foreign_keys: - table.foreign_keys.remove(fk) - if fk.constraint in table.constraints: - # this might have been removed - # already, if it's a composite constraint - # and more than one col being replaced - table.constraints.remove(fk.constraint) - - table._columns.replace(self) - - if self.primary_key: - table.primary_key._replace(self) - Table._autoincrement_column._reset(table) - elif self.key in table.primary_key: - raise exc.ArgumentError( - "Trying to redefine primary-key column '%s' as a " - "non-primary-key column on table '%s'" % ( - self.key, table.fullname)) - self.table = table - - if self.index: - if isinstance(self.index, util.string_types): - raise exc.ArgumentError( - "The 'index' keyword argument on Column is boolean only. " - "To create indexes with a specific name, create an " - "explicit Index object external to the Table.") - Index(_truncated_label('ix_%s' % self._label), - self, unique=bool(self.unique)) - elif self.unique: - if isinstance(self.unique, util.string_types): - raise exc.ArgumentError( - "The 'unique' keyword argument on Column is boolean " - "only. To create unique constraints or indexes with a " - "specific name, append an explicit UniqueConstraint to " - "the Table's list of elements, or create an explicit " - "Index object external to the Table.") - table.append_constraint(UniqueConstraint(self.key)) - - fk_key = (table.key, self.key) - if fk_key in self.table.metadata._fk_memos: - for fk in self.table.metadata._fk_memos[fk_key]: - fk._set_remote_table(table) - - def _on_table_attach(self, fn): - if self.table is not None: - fn(self, self.table) - event.listen(self, 'after_parent_attach', fn) - - def copy(self, **kw): - """Create a copy of this ``Column``, unitialized. - - This is used in ``Table.tometadata``. - - """ - - # Constraint objects plus non-constraint-bound ForeignKey objects - args = \ - [c.copy(**kw) for c in self.constraints] + \ - [c.copy(**kw) for c in self.foreign_keys if not c.constraint] - - type_ = self.type - if isinstance(type_, SchemaEventTarget): - type_ = type_.copy(**kw) - - c = self._constructor( - name=self.name, - type_=type_, - key=self.key, - primary_key=self.primary_key, - nullable=self.nullable, - unique=self.unique, - system=self.system, - #quote=self.quote, - index=self.index, - autoincrement=self.autoincrement, - default=self.default, - server_default=self.server_default, - onupdate=self.onupdate, - server_onupdate=self.server_onupdate, - doc=self.doc, - *args - ) - return self._schema_item_copy(c) - - def _make_proxy(self, selectable, name=None, key=None, - name_is_truncatable=False, **kw): - """Create a *proxy* for this column. - - This is a copy of this ``Column`` referenced by a different parent - (such as an alias or select statement). The column should - be used only in select scenarios, as its full DDL/default - information is not transferred. - - """ - fk = [ForeignKey(f.column, _constraint=f.constraint) - for f in self.foreign_keys] - if name is None and self.name is None: - raise exc.InvalidRequestError("Cannot initialize a sub-selectable" - " with this Column object until it's 'name' has " - "been assigned.") - try: - c = self._constructor( - _as_truncated(name or self.name) if \ - name_is_truncatable else (name or self.name), - self.type, - key=key if key else name if name else self.key, - primary_key=self.primary_key, - nullable=self.nullable, - _proxies=[self], *fk) - except TypeError: - util.raise_from_cause( - TypeError( - "Could not create a copy of this %r object. " - "Ensure the class includes a _constructor() " - "attribute or method which accepts the " - "standard Column constructor arguments, or " - "references the Column class itself." % self.__class__) - ) - - c.table = selectable - selectable._columns.add(c) - if selectable._is_clone_of is not None: - c._is_clone_of = selectable._is_clone_of.columns[c.key] - if self.primary_key: - selectable.primary_key.add(c) - c.dispatch.after_parent_attach(c, selectable) - return c - - def get_children(self, schema_visitor=False, **kwargs): - if schema_visitor: - return [x for x in (self.default, self.onupdate) - if x is not None] + \ - list(self.foreign_keys) + list(self.constraints) - else: - return ColumnClause.get_children(self, **kwargs) - - -class ForeignKey(SchemaItem): - """Defines a dependency between two columns. - - ``ForeignKey`` is specified as an argument to a :class:`.Column` object, - e.g.:: - - t = Table("remote_table", metadata, - Column("remote_id", ForeignKey("main_table.id")) - ) - - Note that ``ForeignKey`` is only a marker object that defines - a dependency between two columns. The actual constraint - is in all cases represented by the :class:`.ForeignKeyConstraint` - object. This object will be generated automatically when - a ``ForeignKey`` is associated with a :class:`.Column` which - in turn is associated with a :class:`.Table`. Conversely, - when :class:`.ForeignKeyConstraint` is applied to a :class:`.Table`, - ``ForeignKey`` markers are automatically generated to be - present on each associated :class:`.Column`, which are also - associated with the constraint object. - - Note that you cannot define a "composite" foreign key constraint, - that is a constraint between a grouping of multiple parent/child - columns, using ``ForeignKey`` objects. To define this grouping, - the :class:`.ForeignKeyConstraint` object must be used, and applied - to the :class:`.Table`. The associated ``ForeignKey`` objects - are created automatically. - - The ``ForeignKey`` objects associated with an individual - :class:`.Column` object are available in the `foreign_keys` collection - of that column. - - Further examples of foreign key configuration are in - :ref:`metadata_foreignkeys`. - - """ - - __visit_name__ = 'foreign_key' - - def __init__(self, column, _constraint=None, use_alter=False, name=None, - onupdate=None, ondelete=None, deferrable=None, - initially=None, link_to_name=False, match=None): - """ - Construct a column-level FOREIGN KEY. - - The :class:`.ForeignKey` object when constructed generates a - :class:`.ForeignKeyConstraint` which is associated with the parent - :class:`.Table` object's collection of constraints. - - :param column: A single target column for the key relationship. A - :class:`.Column` object or a column name as a string: - ``tablename.columnkey`` or ``schema.tablename.columnkey``. - ``columnkey`` is the ``key`` which has been assigned to the column - (defaults to the column name itself), unless ``link_to_name`` is - ``True`` in which case the rendered name of the column is used. - - .. versionadded:: 0.7.4 - Note that if the schema name is not included, and the - underlying :class:`.MetaData` has a "schema", that value will - be used. - - :param name: Optional string. An in-database name for the key if - `constraint` is not provided. - - :param onupdate: Optional string. If set, emit ON UPDATE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param ondelete: Optional string. If set, emit ON DELETE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT - DEFERRABLE when issuing DDL for this constraint. - - :param initially: Optional string. If set, emit INITIALLY when - issuing DDL for this constraint. - - :param link_to_name: if True, the string name given in ``column`` is - the rendered name of the referenced column, not its locally - assigned ``key``. - - :param use_alter: passed to the underlying - :class:`.ForeignKeyConstraint` to indicate the constraint should be - generated/dropped externally from the CREATE TABLE/ DROP TABLE - statement. See that classes' constructor for details. - - :param match: Optional string. If set, emit MATCH when issuing - DDL for this constraint. Typical values include SIMPLE, PARTIAL - and FULL. - - """ - - self._colspec = column - if isinstance(self._colspec, util.string_types): - self._table_column = None - else: - if hasattr(self._colspec, '__clause_element__'): - self._table_column = self._colspec.__clause_element__() - else: - self._table_column = self._colspec - - if not isinstance(self._table_column, ColumnClause): - raise exc.ArgumentError( - "String, Column, or Column-bound argument " - "expected, got %r" % self._table_column) - elif not isinstance(self._table_column.table, (util.NoneType, TableClause)): - raise exc.ArgumentError( - "ForeignKey received Column not bound " - "to a Table, got: %r" % self._table_column.table - ) - - # the linked ForeignKeyConstraint. - # ForeignKey will create this when parent Column - # is attached to a Table, *or* ForeignKeyConstraint - # object passes itself in when creating ForeignKey - # markers. - self.constraint = _constraint - self.parent = None - self.use_alter = use_alter - self.name = name - self.onupdate = onupdate - self.ondelete = ondelete - self.deferrable = deferrable - self.initially = initially - self.link_to_name = link_to_name - self.match = match - - def __repr__(self): - return "ForeignKey(%r)" % self._get_colspec() - - def copy(self, schema=None): - """Produce a copy of this :class:`.ForeignKey` object. - - The new :class:`.ForeignKey` will not be bound - to any :class:`.Column`. - - This method is usually used by the internal - copy procedures of :class:`.Column`, :class:`.Table`, - and :class:`.MetaData`. - - :param schema: The returned :class:`.ForeignKey` will - reference the original table and column name, qualified - by the given string schema name. - - """ - - fk = ForeignKey( - self._get_colspec(schema=schema), - use_alter=self.use_alter, - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - deferrable=self.deferrable, - initially=self.initially, - link_to_name=self.link_to_name, - match=self.match - ) - return self._schema_item_copy(fk) - - - def _get_colspec(self, schema=None): - """Return a string based 'column specification' for this - :class:`.ForeignKey`. - - This is usually the equivalent of the string-based "tablename.colname" - argument first passed to the object's constructor. - - """ - if schema: - _schema, tname, colname = self._column_tokens - return "%s.%s.%s" % (schema, tname, colname) - elif self._table_column is not None: - return "%s.%s" % ( - self._table_column.table.fullname, self._table_column.key) - else: - return self._colspec - - - def _table_key(self): - if self._table_column is not None: - if self._table_column.table is None: - return None - else: - return self._table_column.table.key - else: - schema, tname, colname = self._column_tokens - return _get_table_key(tname, schema) - - - - target_fullname = property(_get_colspec) - - def references(self, table): - """Return True if the given :class:`.Table` is referenced by this - :class:`.ForeignKey`.""" - - return table.corresponding_column(self.column) is not None - - def get_referent(self, table): - """Return the :class:`.Column` in the given :class:`.Table` - referenced by this :class:`.ForeignKey`. - - Returns None if this :class:`.ForeignKey` does not reference the given - :class:`.Table`. - - """ - - return table.corresponding_column(self.column) - - @util.memoized_property - def _column_tokens(self): - """parse a string-based _colspec into its component parts.""" - - m = self._colspec.split('.') - if m is None: - raise exc.ArgumentError( - "Invalid foreign key column specification: %s" % - self._colspec) - if (len(m) == 1): - tname = m.pop() - colname = None - else: - colname = m.pop() - tname = m.pop() - - # A FK between column 'bar' and table 'foo' can be - # specified as 'foo', 'foo.bar', 'dbo.foo.bar', - # 'otherdb.dbo.foo.bar'. Once we have the column name and - # the table name, treat everything else as the schema - # name. Some databases (e.g. Sybase) support - # inter-database foreign keys. See tickets#1341 and -- - # indirectly related -- Ticket #594. This assumes that '.' - # will never appear *within* any component of the FK. - - if (len(m) > 0): - schema = '.'.join(m) - else: - schema = None - return schema, tname, colname - - def _resolve_col_tokens(self): - if self.parent is None: - raise exc.InvalidRequestError( - "this ForeignKey object does not yet have a " - "parent Column associated with it.") - - elif self.parent.table is None: - raise exc.InvalidRequestError( - "this ForeignKey's parent column is not yet associated " - "with a Table.") - - parenttable = self.parent.table - - # assertion, can be commented out. - # basically Column._make_proxy() sends the actual - # target Column to the ForeignKey object, so the - # string resolution here is never called. - for c in self.parent.base_columns: - if isinstance(c, Column): - assert c.table is parenttable - break - else: - assert False - ###################### - - schema, tname, colname = self._column_tokens - - if schema is None and parenttable.metadata.schema is not None: - schema = parenttable.metadata.schema - - tablekey = _get_table_key(tname, schema) - return parenttable, tablekey, colname - - - def _link_to_col_by_colstring(self, parenttable, table, colname): - if not hasattr(self.constraint, '_referred_table'): - self.constraint._referred_table = table - else: - assert self.constraint._referred_table is table - - _column = None - if colname is None: - # colname is None in the case that ForeignKey argument - # was specified as table name only, in which case we - # match the column name to the same column on the - # parent. - key = self.parent - _column = table.c.get(self.parent.key, None) - elif self.link_to_name: - key = colname - for c in table.c: - if c.name == colname: - _column = c - else: - key = colname - _column = table.c.get(colname, None) - - if _column is None: - raise exc.NoReferencedColumnError( - "Could not initialize target column for ForeignKey '%s' on table '%s': " - "table '%s' has no column named '%s'" % ( - self._colspec, parenttable.name, table.name, key), - table.name, key) - - self._set_target_column(_column) - - def _set_target_column(self, column): - # propagate TypeEngine to parent if it didn't have one - if self.parent.type._isnull: - self.parent.type = column.type - - # super-edgy case, if other FKs point to our column, - # they'd get the type propagated out also. - if isinstance(self.parent.table, Table): - fk_key = (self.parent.table.key, self.parent.key) - if fk_key in self.parent.table.metadata._fk_memos: - for fk in self.parent.table.metadata._fk_memos[fk_key]: - if fk.parent.type._isnull: - fk.parent.type = column.type - - self.column = column - - @util.memoized_property - def column(self): - """Return the target :class:`.Column` referenced by this - :class:`.ForeignKey`. - - If no target column has been established, an exception - is raised. - - .. versionchanged:: 0.9.0 - Foreign key target column resolution now occurs as soon as both - the ForeignKey object and the remote Column to which it refers - are both associated with the same MetaData object. - - """ - - if isinstance(self._colspec, util.string_types): - - parenttable, tablekey, colname = self._resolve_col_tokens() - - if tablekey not in parenttable.metadata: - raise exc.NoReferencedTableError( - "Foreign key associated with column '%s' could not find " - "table '%s' with which to generate a " - "foreign key to target column '%s'" % - (self.parent, tablekey, colname), - tablekey) - elif parenttable.key not in parenttable.metadata: - raise exc.InvalidRequestError( - "Table %s is no longer associated with its " - "parent MetaData" % parenttable) - else: - raise exc.NoReferencedColumnError( - "Could not initialize target column for " - "ForeignKey '%s' on table '%s': " - "table '%s' has no column named '%s'" % ( - self._colspec, parenttable.name, tablekey, colname), - tablekey, colname) - elif hasattr(self._colspec, '__clause_element__'): - _column = self._colspec.__clause_element__() - return _column - else: - _column = self._colspec - return _column - - def _set_parent(self, column): - if self.parent is not None and self.parent is not column: - raise exc.InvalidRequestError( - "This ForeignKey already has a parent !") - self.parent = column - self.parent.foreign_keys.add(self) - self.parent._on_table_attach(self._set_table) - - def _set_remote_table(self, table): - parenttable, tablekey, colname = self._resolve_col_tokens() - self._link_to_col_by_colstring(parenttable, table, colname) - self.constraint._validate_dest_table(table) - - def _remove_from_metadata(self, metadata): - parenttable, table_key, colname = self._resolve_col_tokens() - fk_key = (table_key, colname) - - if self in metadata._fk_memos[fk_key]: - # TODO: no test coverage for self not in memos - metadata._fk_memos[fk_key].remove(self) - - def _set_table(self, column, table): - # standalone ForeignKey - create ForeignKeyConstraint - # on the hosting Table when attached to the Table. - if self.constraint is None and isinstance(table, Table): - self.constraint = ForeignKeyConstraint( - [], [], use_alter=self.use_alter, name=self.name, - onupdate=self.onupdate, ondelete=self.ondelete, - deferrable=self.deferrable, initially=self.initially, - match=self.match, - ) - self.constraint._elements[self.parent] = self - self.constraint._set_parent_with_dispatch(table) - table.foreign_keys.add(self) - - # set up remote ".column" attribute, or a note to pick it - # up when the other Table/Column shows up - if isinstance(self._colspec, util.string_types): - parenttable, table_key, colname = self._resolve_col_tokens() - fk_key = (table_key, colname) - if table_key in parenttable.metadata.tables: - table = parenttable.metadata.tables[table_key] - try: - self._link_to_col_by_colstring(parenttable, table, colname) - except exc.NoReferencedColumnError: - # this is OK, we'll try later - pass - parenttable.metadata._fk_memos[fk_key].append(self) - elif hasattr(self._colspec, '__clause_element__'): - _column = self._colspec.__clause_element__() - self._set_target_column(_column) - else: - _column = self._colspec - self._set_target_column(_column) - - - -class _NotAColumnExpr(object): - def _not_a_column_expr(self): - raise exc.InvalidRequestError( - "This %s cannot be used directly " - "as a column expression." % self.__class__.__name__) - - __clause_element__ = self_group = lambda self: self._not_a_column_expr() - _from_objects = property(lambda self: self._not_a_column_expr()) - - -class DefaultGenerator(_NotAColumnExpr, SchemaItem): - """Base class for column *default* values.""" - - __visit_name__ = 'default_generator' - - is_sequence = False - is_server_default = False - column = None - - def __init__(self, for_update=False): - self.for_update = for_update - - def _set_parent(self, column): - self.column = column - if self.for_update: - self.column.onupdate = self - else: - self.column.default = self - - def execute(self, bind=None, **kwargs): - if bind is None: - bind = _bind_or_error(self) - return bind._execute_default(self, **kwargs) - - @property - def bind(self): - """Return the connectable associated with this default.""" - if getattr(self, 'column', None) is not None: - return self.column.table.bind - else: - return None - - -class ColumnDefault(DefaultGenerator): - """A plain default value on a column. - - This could correspond to a constant, a callable function, - or a SQL clause. - - :class:`.ColumnDefault` is generated automatically - whenever the ``default``, ``onupdate`` arguments of - :class:`.Column` are used. A :class:`.ColumnDefault` - can be passed positionally as well. - - For example, the following:: - - Column('foo', Integer, default=50) - - Is equivalent to:: - - Column('foo', Integer, ColumnDefault(50)) - - - """ - - def __init__(self, arg, **kwargs): - """"Construct a new :class:`.ColumnDefault`. - - - :param arg: argument representing the default value. - May be one of the following: - - * a plain non-callable Python value, such as a - string, integer, boolean, or other simple type. - The default value will be used as is each time. - * a SQL expression, that is one which derives from - :class:`.ColumnElement`. The SQL expression will - be rendered into the INSERT or UPDATE statement, - or in the case of a primary key column when - RETURNING is not used may be - pre-executed before an INSERT within a SELECT. - * A Python callable. The function will be invoked for each - new row subject to an INSERT or UPDATE. - The callable must accept exactly - zero or one positional arguments. The one-argument form - will receive an instance of the :class:`.ExecutionContext`, - which provides contextual information as to the current - :class:`.Connection` in use as well as the current - statement and parameters. - - """ - super(ColumnDefault, self).__init__(**kwargs) - if isinstance(arg, FetchedValue): - raise exc.ArgumentError( - "ColumnDefault may not be a server-side default type.") - if util.callable(arg): - arg = self._maybe_wrap_callable(arg) - self.arg = arg - - @util.memoized_property - def is_callable(self): - return util.callable(self.arg) - - @util.memoized_property - def is_clause_element(self): - return isinstance(self.arg, ClauseElement) - - @util.memoized_property - def is_scalar(self): - return not self.is_callable and \ - not self.is_clause_element and \ - not self.is_sequence - - def _maybe_wrap_callable(self, fn): - """Wrap callables that don't accept a context. - - The alternative here is to require that - a simple callable passed to "default" would need - to be of the form "default=lambda ctx: datetime.now". - That is the more "correct" way to go, but the case - of using a zero-arg callable for "default" is so - much more prominent than the context-specific one - I'm having trouble justifying putting that inconvenience - on everyone. - - """ - if inspect.isfunction(fn) or inspect.ismethod(fn): - inspectable = fn - elif inspect.isclass(fn): - inspectable = fn.__init__ - elif hasattr(fn, '__call__'): - inspectable = fn.__call__ - else: - # probably not inspectable, try anyways. - inspectable = fn - try: - argspec = inspect.getargspec(inspectable) - except TypeError: - return lambda ctx: fn() - - defaulted = argspec[3] is not None and len(argspec[3]) or 0 - positionals = len(argspec[0]) - defaulted - - # Py3K compat - no unbound methods - if inspect.ismethod(inspectable) or inspect.isclass(fn): - positionals -= 1 - - if positionals == 0: - return lambda ctx: fn() - elif positionals == 1: - return fn - else: - raise exc.ArgumentError( - "ColumnDefault Python function takes zero or one " - "positional arguments") - - def _visit_name(self): - if self.for_update: - return "column_onupdate" - else: - return "column_default" - __visit_name__ = property(_visit_name) - - def __repr__(self): - return "ColumnDefault(%r)" % self.arg - - -class Sequence(DefaultGenerator): - """Represents a named database sequence. - - The :class:`.Sequence` object represents the name and configurational - parameters of a database sequence. It also represents - a construct that can be "executed" by a SQLAlchemy :class:`.Engine` - or :class:`.Connection`, rendering the appropriate "next value" function - for the target database and returning a result. - - The :class:`.Sequence` is typically associated with a primary key column:: - - some_table = Table('some_table', metadata, - Column('id', Integer, Sequence('some_table_seq'), primary_key=True) - ) - - When CREATE TABLE is emitted for the above :class:`.Table`, if the - target platform supports sequences, a CREATE SEQUENCE statement will - be emitted as well. For platforms that don't support sequences, - the :class:`.Sequence` construct is ignored. - - .. seealso:: - - :class:`.CreateSequence` - - :class:`.DropSequence` - - """ - - __visit_name__ = 'sequence' - - is_sequence = True - - def __init__(self, name, start=None, increment=None, schema=None, - optional=False, quote=None, metadata=None, - quote_schema=None, - for_update=False): - """Construct a :class:`.Sequence` object. - - :param name: The name of the sequence. - :param start: the starting index of the sequence. This value is - used when the CREATE SEQUENCE command is emitted to the database - as the value of the "START WITH" clause. If ``None``, the - clause is omitted, which on most platforms indicates a starting - value of 1. - :param increment: the increment value of the sequence. This - value is used when the CREATE SEQUENCE command is emitted to - the database as the value of the "INCREMENT BY" clause. If ``None``, - the clause is omitted, which on most platforms indicates an - increment of 1. - :param schema: Optional schema name for the sequence, if located - in a schema other than the default. - :param optional: boolean value, when ``True``, indicates that this - :class:`.Sequence` object only needs to be explicitly generated - on backends that don't provide another way to generate primary - key identifiers. Currently, it essentially means, "don't create - this sequence on the Postgresql backend, where the SERIAL keyword - creates a sequence for us automatically". - :param quote: boolean value, when ``True`` or ``False``, explicitly - forces quoting of the schema name on or off. When left at its - default of ``None``, normal quoting rules based on casing and reserved - words take place. - :param quote_schema: set the quoting preferences for the ``schema`` - name. - :param metadata: optional :class:`.MetaData` object which will be - associated with this :class:`.Sequence`. A :class:`.Sequence` - that is associated with a :class:`.MetaData` gains access to the - ``bind`` of that :class:`.MetaData`, meaning the - :meth:`.Sequence.create` and :meth:`.Sequence.drop` methods will - make usage of that engine automatically. - - .. versionchanged:: 0.7 - Additionally, the appropriate CREATE SEQUENCE/ - DROP SEQUENCE DDL commands will be emitted corresponding to this - :class:`.Sequence` when :meth:`.MetaData.create_all` and - :meth:`.MetaData.drop_all` are invoked. - - Note that when a :class:`.Sequence` is applied to a :class:`.Column`, - the :class:`.Sequence` is automatically associated with the - :class:`.MetaData` object of that column's parent :class:`.Table`, - when that association is made. The :class:`.Sequence` will then - be subject to automatic CREATE SEQUENCE/DROP SEQUENCE corresponding - to when the :class:`.Table` object itself is created or dropped, - rather than that of the :class:`.MetaData` object overall. - :param for_update: Indicates this :class:`.Sequence`, when associated - with a :class:`.Column`, should be invoked for UPDATE statements - on that column's table, rather than for INSERT statements, when - no value is otherwise present for that column in the statement. - - """ - super(Sequence, self).__init__(for_update=for_update) - self.name = quoted_name(name, quote) - self.start = start - self.increment = increment - self.optional = optional - if metadata is not None and schema is None and metadata.schema: - self.schema = schema = metadata.schema - else: - self.schema = quoted_name(schema, quote_schema) - self.metadata = metadata - self._key = _get_table_key(name, schema) - if metadata: - self._set_metadata(metadata) - - @util.memoized_property - def is_callable(self): - return False - - @util.memoized_property - def is_clause_element(self): - return False - - @util.dependencies("sqlalchemy.sql.functions.func") - def next_value(self, func): - """Return a :class:`.next_value` function element - which will render the appropriate increment function - for this :class:`.Sequence` within any SQL expression. - - """ - return func.next_value(self, bind=self.bind) - - def _set_parent(self, column): - super(Sequence, self)._set_parent(column) - column._on_table_attach(self._set_table) - - def _set_table(self, column, table): - self._set_metadata(table.metadata) - - def _set_metadata(self, metadata): - self.metadata = metadata - self.metadata._sequences[self._key] = self - - @property - def bind(self): - if self.metadata: - return self.metadata.bind - else: - return None - - def create(self, bind=None, checkfirst=True): - """Creates this sequence in the database.""" - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst) - - def drop(self, bind=None, checkfirst=True): - """Drops this sequence from the database.""" - - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst) - - def _not_a_column_expr(self): - raise exc.InvalidRequestError( - "This %s cannot be used directly " - "as a column expression. Use func.next_value(sequence) " - "to produce a 'next value' function that's usable " - "as a column element." - % self.__class__.__name__) - - -@inspection._self_inspects -class FetchedValue(_NotAColumnExpr, SchemaEventTarget): - """A marker for a transparent database-side default. - - Use :class:`.FetchedValue` when the database is configured - to provide some automatic default for a column. - - E.g.:: - - Column('foo', Integer, FetchedValue()) - - Would indicate that some trigger or default generator - will create a new value for the ``foo`` column during an - INSERT. - - .. seealso:: - - :ref:`triggered_columns` - - """ - is_server_default = True - reflected = False - has_argument = False - - def __init__(self, for_update=False): - self.for_update = for_update - - def _as_for_update(self, for_update): - if for_update == self.for_update: - return self - else: - return self._clone(for_update) - - def _clone(self, for_update): - n = self.__class__.__new__(self.__class__) - n.__dict__.update(self.__dict__) - n.__dict__.pop('column', None) - n.for_update = for_update - return n - - def _set_parent(self, column): - self.column = column - if self.for_update: - self.column.server_onupdate = self - else: - self.column.server_default = self - - def __repr__(self): - return util.generic_repr(self) - - -class DefaultClause(FetchedValue): - """A DDL-specified DEFAULT column value. - - :class:`.DefaultClause` is a :class:`.FetchedValue` - that also generates a "DEFAULT" clause when - "CREATE TABLE" is emitted. - - :class:`.DefaultClause` is generated automatically - whenever the ``server_default``, ``server_onupdate`` arguments of - :class:`.Column` are used. A :class:`.DefaultClause` - can be passed positionally as well. - - For example, the following:: - - Column('foo', Integer, server_default="50") - - Is equivalent to:: - - Column('foo', Integer, DefaultClause("50")) - - """ - - has_argument = True - - def __init__(self, arg, for_update=False, _reflected=False): - util.assert_arg_type(arg, (util.string_types[0], - ClauseElement, - TextClause), 'arg') - super(DefaultClause, self).__init__(for_update) - self.arg = arg - self.reflected = _reflected - - def __repr__(self): - return "DefaultClause(%r, for_update=%r)" % \ - (self.arg, self.for_update) - - -class PassiveDefault(DefaultClause): - """A DDL-specified DEFAULT column value. - - .. deprecated:: 0.6 - :class:`.PassiveDefault` is deprecated. - Use :class:`.DefaultClause`. - """ - @util.deprecated("0.6", - ":class:`.PassiveDefault` is deprecated. " - "Use :class:`.DefaultClause`.", - False) - def __init__(self, *arg, **kw): - DefaultClause.__init__(self, *arg, **kw) - - -class Constraint(SchemaItem): - """A table-level SQL constraint.""" - - __visit_name__ = 'constraint' - - def __init__(self, name=None, deferrable=None, initially=None, - _create_rule=None, - **kw): - """Create a SQL constraint. - - :param name: - Optional, the in-database name of this ``Constraint``. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - :param _create_rule: - a callable which is passed the DDLCompiler object during - compilation. Returns True or False to signal inline generation of - this Constraint. - - The AddConstraint and DropConstraint DDL constructs provide - DDLElement's more comprehensive "conditional DDL" approach that is - passed a database connection when DDL is being issued. _create_rule - is instead called during any CREATE TABLE compilation, where there - may not be any transaction/connection in progress. However, it - allows conditional compilation of the constraint even for backends - which do not support addition of constraints through ALTER TABLE, - which currently includes SQLite. - - _create_rule is used by some types to create constraints. - Currently, its call signature is subject to change at any time. - - :param \**kwargs: - Dialect-specific keyword parameters, see the documentation - for various dialects and constraints regarding options here. - - """ - - self.name = name - self.deferrable = deferrable - self.initially = initially - self._create_rule = _create_rule - util.set_creation_order(self) - _validate_dialect_kwargs(kw, self.__class__.__name__) - self.kwargs = kw - - @property - def table(self): - try: - if isinstance(self.parent, Table): - return self.parent - except AttributeError: - pass - raise exc.InvalidRequestError( - "This constraint is not bound to a table. Did you " - "mean to call table.append_constraint(constraint) ?") - - def _set_parent(self, parent): - self.parent = parent - parent.constraints.add(self) - - def copy(self, **kw): - raise NotImplementedError() - - -def _to_schema_column(element): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, Column): - raise exc.ArgumentError("schema.Column object expected") - return element - - -def _to_schema_column_or_string(element): - if hasattr(element, '__clause_element__'): - element = element.__clause_element__() - if not isinstance(element, util.string_types + (ColumnElement, )): - msg = "Element %r is not a string name or column element" - raise exc.ArgumentError(msg % element) - return element - - -class ColumnCollectionMixin(object): - def __init__(self, *columns): - self.columns = ColumnCollection() - self._pending_colargs = [_to_schema_column_or_string(c) - for c in columns] - if self._pending_colargs and \ - isinstance(self._pending_colargs[0], Column) and \ - isinstance(self._pending_colargs[0].table, Table): - self._set_parent_with_dispatch(self._pending_colargs[0].table) - - def _set_parent(self, table): - for col in self._pending_colargs: - if isinstance(col, util.string_types): - col = table.c[col] - self.columns.add(col) - - -class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): - """A constraint that proxies a ColumnCollection.""" - - def __init__(self, *columns, **kw): - """ - :param \*columns: - A sequence of column names or Column objects. - - :param name: - Optional, the in-database name of this constraint. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - """ - ColumnCollectionMixin.__init__(self, *columns) - Constraint.__init__(self, **kw) - - def _set_parent(self, table): - ColumnCollectionMixin._set_parent(self, table) - Constraint._set_parent(self, table) - - def __contains__(self, x): - return x in self.columns - - def copy(self, **kw): - c = self.__class__(name=self.name, deferrable=self.deferrable, - initially=self.initially, *self.columns.keys()) - return self._schema_item_copy(c) - - def contains_column(self, col): - return self.columns.contains_column(col) - - def __iter__(self): - # inlining of - # return iter(self.columns) - # ColumnCollection->OrderedProperties->OrderedDict - ordered_dict = self.columns._data - return (ordered_dict[key] for key in ordered_dict._list) - - def __len__(self): - return len(self.columns._data) - - -class CheckConstraint(Constraint): - """A table- or column-level CHECK constraint. - - Can be included in the definition of a Table or Column. - """ - - def __init__(self, sqltext, name=None, deferrable=None, - initially=None, table=None, _create_rule=None, - _autoattach=True): - """Construct a CHECK constraint. - - :param sqltext: - A string containing the constraint definition, which will be used - verbatim, or a SQL expression construct. If given as a string, - the object is converted to a :class:`.Text` object. If the textual - string includes a colon character, escape this using a backslash:: - - CheckConstraint(r"foo ~ E'a(?\:b|c)d") - - :param name: - Optional, the in-database name of the constraint. - - :param deferrable: - Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when - issuing DDL for this constraint. - - :param initially: - Optional string. If set, emit INITIALLY when issuing DDL - for this constraint. - - """ - - super(CheckConstraint, self).\ - __init__(name, deferrable, initially, _create_rule) - self.sqltext = _literal_as_text(sqltext) - if table is not None: - self._set_parent_with_dispatch(table) - elif _autoattach: - cols = _find_columns(self.sqltext) - tables = set([c.table for c in cols - if isinstance(c.table, Table)]) - if len(tables) == 1: - self._set_parent_with_dispatch( - tables.pop()) - - def __visit_name__(self): - if isinstance(self.parent, Table): - return "check_constraint" - else: - return "column_check_constraint" - __visit_name__ = property(__visit_name__) - - def copy(self, target_table=None, **kw): - if target_table is not None: - def replace(col): - if self.table.c.contains_column(col): - return target_table.c[col.key] - else: - return None - sqltext = visitors.replacement_traverse(self.sqltext, {}, replace) - else: - sqltext = self.sqltext - c = CheckConstraint(sqltext, - name=self.name, - initially=self.initially, - deferrable=self.deferrable, - _create_rule=self._create_rule, - table=target_table, - _autoattach=False) - return self._schema_item_copy(c) - - -class ForeignKeyConstraint(Constraint): - """A table-level FOREIGN KEY constraint. - - Defines a single column or composite FOREIGN KEY ... REFERENCES - constraint. For a no-frills, single column foreign key, adding a - :class:`.ForeignKey` to the definition of a :class:`.Column` is a shorthand - equivalent for an unnamed, single column :class:`.ForeignKeyConstraint`. - - Examples of foreign key configuration are in :ref:`metadata_foreignkeys`. - - """ - __visit_name__ = 'foreign_key_constraint' - - def __init__(self, columns, refcolumns, name=None, onupdate=None, - ondelete=None, deferrable=None, initially=None, use_alter=False, - link_to_name=False, match=None, table=None): - """Construct a composite-capable FOREIGN KEY. - - :param columns: A sequence of local column names. The named columns - must be defined and present in the parent Table. The names should - match the ``key`` given to each column (defaults to the name) unless - ``link_to_name`` is True. - - :param refcolumns: A sequence of foreign column names or Column - objects. The columns must all be located within the same Table. - - :param name: Optional, the in-database name of the key. - - :param onupdate: Optional string. If set, emit ON UPDATE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param ondelete: Optional string. If set, emit ON DELETE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - - :param deferrable: Optional bool. If set, emit DEFERRABLE or NOT - DEFERRABLE when issuing DDL for this constraint. - - :param initially: Optional string. If set, emit INITIALLY when - issuing DDL for this constraint. - - :param link_to_name: if True, the string name given in ``column`` is - the rendered name of the referenced column, not its locally assigned - ``key``. - - :param use_alter: If True, do not emit the DDL for this constraint as - part of the CREATE TABLE definition. Instead, generate it via an - ALTER TABLE statement issued after the full collection of tables - have been created, and drop it via an ALTER TABLE statement before - the full collection of tables are dropped. This is shorthand for the - usage of :class:`.AddConstraint` and :class:`.DropConstraint` applied - as "after-create" and "before-drop" events on the MetaData object. - This is normally used to generate/drop constraints on objects that - are mutually dependent on each other. - - :param match: Optional string. If set, emit MATCH when issuing - DDL for this constraint. Typical values include SIMPLE, PARTIAL - and FULL. - - """ - super(ForeignKeyConstraint, self).\ - __init__(name, deferrable, initially) - - self.onupdate = onupdate - self.ondelete = ondelete - self.link_to_name = link_to_name - if self.name is None and use_alter: - raise exc.ArgumentError("Alterable Constraint requires a name") - self.use_alter = use_alter - self.match = match - - self._elements = util.OrderedDict() - - # standalone ForeignKeyConstraint - create - # associated ForeignKey objects which will be applied to hosted - # Column objects (in col.foreign_keys), either now or when attached - # to the Table for string-specified names - for col, refcol in zip(columns, refcolumns): - self._elements[col] = ForeignKey( - refcol, - _constraint=self, - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - use_alter=self.use_alter, - link_to_name=self.link_to_name, - match=self.match, - deferrable=self.deferrable, - initially=self.initially - ) - - if table is not None: - self._set_parent_with_dispatch(table) - elif columns and \ - isinstance(columns[0], Column) and \ - columns[0].table is not None: - self._set_parent_with_dispatch(columns[0].table) - - def _validate_dest_table(self, table): - table_keys = set([elem._table_key() for elem in self._elements.values()]) - if None not in table_keys and len(table_keys) > 1: - elem0, elem1 = sorted(table_keys)[0:2] - raise exc.ArgumentError( - 'ForeignKeyConstraint on %s(%s) refers to ' - 'multiple remote tables: %s and %s' % ( - table.fullname, - self._col_description, - elem0, - elem1 - )) - - @property - def _col_description(self): - return ", ".join(self._elements) - - @property - def columns(self): - return list(self._elements) - - @property - def elements(self): - return list(self._elements.values()) - - def _set_parent(self, table): - super(ForeignKeyConstraint, self)._set_parent(table) - - self._validate_dest_table(table) - - for col, fk in self._elements.items(): - # string-specified column names now get - # resolved to Column objects - if isinstance(col, util.string_types): - try: - col = table.c[col] - except KeyError: - raise exc.ArgumentError( - "Can't create ForeignKeyConstraint " - "on table '%s': no column " - "named '%s' is present." % (table.description, col)) - - if not hasattr(fk, 'parent') or \ - fk.parent is not col: - fk._set_parent_with_dispatch(col) - - if self.use_alter: - def supports_alter(ddl, event, schema_item, bind, **kw): - return table in set(kw['tables']) and \ - bind.dialect.supports_alter - - event.listen(table.metadata, "after_create", - ddl.AddConstraint(self, on=supports_alter)) - event.listen(table.metadata, "before_drop", - ddl.DropConstraint(self, on=supports_alter)) - - def copy(self, schema=None, **kw): - fkc = ForeignKeyConstraint( - [x.parent.key for x in self._elements.values()], - [x._get_colspec(schema=schema) for x in self._elements.values()], - name=self.name, - onupdate=self.onupdate, - ondelete=self.ondelete, - use_alter=self.use_alter, - deferrable=self.deferrable, - initially=self.initially, - link_to_name=self.link_to_name, - match=self.match - ) - for self_fk, other_fk in zip( - self._elements.values(), - fkc._elements.values()): - self_fk._schema_item_copy(other_fk) - return self._schema_item_copy(fkc) - - -class PrimaryKeyConstraint(ColumnCollectionConstraint): - """A table-level PRIMARY KEY constraint. - - Defines a single column or composite PRIMARY KEY constraint. For a - no-frills primary key, adding ``primary_key=True`` to one or more - ``Column`` definitions is a shorthand equivalent for an unnamed single- or - multiple-column PrimaryKeyConstraint. - """ - - __visit_name__ = 'primary_key_constraint' - - def _set_parent(self, table): - super(PrimaryKeyConstraint, self)._set_parent(table) - - if table.primary_key in table.constraints: - table.constraints.remove(table.primary_key) - table.primary_key = self - table.constraints.add(self) - - for c in self.columns: - c.primary_key = True - - def _replace(self, col): - self.columns.replace(col) - - -class UniqueConstraint(ColumnCollectionConstraint): - """A table-level UNIQUE constraint. - - Defines a single column or composite UNIQUE constraint. For a no-frills, - single column constraint, adding ``unique=True`` to the ``Column`` - definition is a shorthand equivalent for an unnamed, single column - UniqueConstraint. - """ - - __visit_name__ = 'unique_constraint' - - -class Index(ColumnCollectionMixin, SchemaItem): - """A table-level INDEX. - - Defines a composite (one or more column) INDEX. - - E.g.:: - - sometable = Table("sometable", metadata, - Column("name", String(50)), - Column("address", String(100)) - ) - - Index("some_index", sometable.c.name) - - For a no-frills, single column index, adding - :class:`.Column` also supports ``index=True``:: - - sometable = Table("sometable", metadata, - Column("name", String(50), index=True) - ) - - For a composite index, multiple columns can be specified:: - - Index("some_index", sometable.c.name, sometable.c.address) - - Functional indexes are supported as well, keeping in mind that at least - one :class:`.Column` must be present:: - - Index("some_index", func.lower(sometable.c.name)) - - .. versionadded:: 0.8 support for functional and expression-based indexes. - - .. seealso:: - - :ref:`schema_indexes` - General information on :class:`.Index`. - - :ref:`postgresql_indexes` - PostgreSQL-specific options available for the - :class:`.Index` construct. - - :ref:`mysql_indexes` - MySQL-specific options available for the - :class:`.Index` construct. - - :ref:`mssql_indexes` - MSSQL-specific options available for the - :class:`.Index` construct. - - """ - - __visit_name__ = 'index' - - def __init__(self, name, *expressions, **kw): - """Construct an index object. - - :param name: - The name of the index - - :param \*expressions: - Column expressions to include in the index. The expressions - are normally instances of :class:`.Column`, but may also - be arbitrary SQL expressions which ultmately refer to a - :class:`.Column`. - - :param unique: - Defaults to False: create a unique index. - - :param \**kw: - Other keyword arguments may be interpreted by specific dialects. - - """ - self.table = None - - columns = [] - for expr in expressions: - if not isinstance(expr, ClauseElement): - columns.append(expr) - else: - cols = [] - visitors.traverse(expr, {}, {'column': cols.append}) - if cols: - columns.append(cols[0]) - else: - columns.append(expr) - - self.expressions = expressions - self.name = quoted_name(name, kw.pop("quote", None)) - self.unique = kw.pop('unique', False) - self.kwargs = kw - - # will call _set_parent() if table-bound column - # objects are present - ColumnCollectionMixin.__init__(self, *columns) - - - - def _set_parent(self, table): - ColumnCollectionMixin._set_parent(self, table) - - if self.table is not None and table is not self.table: - raise exc.ArgumentError( - "Index '%s' is against table '%s', and " - "cannot be associated with table '%s'." % ( - self.name, - self.table.description, - table.description - ) - ) - self.table = table - for c in self.columns: - if c.table != self.table: - raise exc.ArgumentError( - "Column '%s' is not part of table '%s'." % - (c, self.table.description) - ) - table.indexes.add(self) - - self.expressions = [ - expr if isinstance(expr, ClauseElement) - else colexpr - for expr, colexpr in zip(self.expressions, self.columns) - ] - - @property - def bind(self): - """Return the connectable associated with this Index.""" - - return self.table.bind - - def create(self, bind=None): - """Issue a ``CREATE`` statement for this - :class:`.Index`, using the given :class:`.Connectable` - for connectivity. - - .. seealso:: - - :meth:`.MetaData.create_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, self) - return self - - def drop(self, bind=None): - """Issue a ``DROP`` statement for this - :class:`.Index`, using the given :class:`.Connectable` - for connectivity. - - .. seealso:: - - :meth:`.MetaData.drop_all`. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, self) - - def __repr__(self): - return 'Index(%s)' % ( - ", ".join( - [repr(self.name)] + - [repr(c) for c in self.columns] + - (self.unique and ["unique=True"] or []) - )) - - -class MetaData(SchemaItem): - """A collection of :class:`.Table` objects and their associated schema - constructs. - - Holds a collection of :class:`.Table` objects as well as - an optional binding to an :class:`.Engine` or - :class:`.Connection`. If bound, the :class:`.Table` objects - in the collection and their columns may participate in implicit SQL - execution. - - The :class:`.Table` objects themselves are stored in the - :attr:`.MetaData.tables` dictionary. - - :class:`.MetaData` is a thread-safe object for read operations. Construction - of new tables within a single :class:`.MetaData` object, either explicitly - or via reflection, may not be completely thread-safe. - - .. seealso:: - - :ref:`metadata_describing` - Introduction to database metadata - - """ - - __visit_name__ = 'metadata' - - def __init__(self, bind=None, reflect=False, schema=None, - quote_schema=None): - """Create a new MetaData object. - - :param bind: - An Engine or Connection to bind to. May also be a string or URL - instance, these are passed to create_engine() and this MetaData will - be bound to the resulting engine. - - :param reflect: - Optional, automatically load all tables from the bound database. - Defaults to False. ``bind`` is required when this option is set. - - .. deprecated:: 0.8 - Please use the :meth:`.MetaData.reflect` method. - - :param schema: - The default schema to use for the :class:`.Table`, - :class:`.Sequence`, and other objects associated with this - :class:`.MetaData`. Defaults to ``None``. - - :param quote_schema: - Sets the ``quote_schema`` flag for those :class:`.Table`, - :class:`.Sequence`, and other objects which make usage of the - local ``schema`` name. - - .. versionadded:: 0.7.4 - ``schema`` and ``quote_schema`` parameters. - - """ - self.tables = util.immutabledict() - self.schema = quoted_name(schema, quote_schema) - self._schemas = set() - self._sequences = {} - self._fk_memos = collections.defaultdict(list) - - self.bind = bind - if reflect: - util.warn("reflect=True is deprecate; please " - "use the reflect() method.") - if not bind: - raise exc.ArgumentError( - "A bind must be supplied in conjunction " - "with reflect=True") - self.reflect() - - tables = None - """A dictionary of :class:`.Table` objects keyed to their name or "table key". - - The exact key is that determined by the :attr:`.Table.key` attribute; - for a table with no :attr:`.Table.schema` attribute, this is the same - as :attr:`.Table.name`. For a table with a schema, it is typically of the - form ``schemaname.tablename``. - - .. seealso:: - - :attr:`.MetaData.sorted_tables` - - """ - - def __repr__(self): - return 'MetaData(bind=%r)' % self.bind - - def __contains__(self, table_or_key): - if not isinstance(table_or_key, util.string_types): - table_or_key = table_or_key.key - return table_or_key in self.tables - - def _add_table(self, name, schema, table): - key = _get_table_key(name, schema) - dict.__setitem__(self.tables, key, table) - if schema: - self._schemas.add(schema) - - - - def _remove_table(self, name, schema): - key = _get_table_key(name, schema) - removed = dict.pop(self.tables, key, None) - if removed is not None: - for fk in removed.foreign_keys: - fk._remove_from_metadata(self) - if self._schemas: - self._schemas = set([t.schema - for t in self.tables.values() - if t.schema is not None]) - - - def __getstate__(self): - return {'tables': self.tables, - 'schema': self.schema, - 'schemas': self._schemas, - 'sequences': self._sequences, - 'fk_memos': self._fk_memos} - - def __setstate__(self, state): - self.tables = state['tables'] - self.schema = state['schema'] - self._bind = None - self._sequences = state['sequences'] - self._schemas = state['schemas'] - self._fk_memos = state['fk_memos'] - - def is_bound(self): - """True if this MetaData is bound to an Engine or Connection.""" - - return self._bind is not None - - def bind(self): - """An :class:`.Engine` or :class:`.Connection` to which this - :class:`.MetaData` is bound. - - Typically, a :class:`.Engine` is assigned to this attribute - so that "implicit execution" may be used, or alternatively - as a means of providing engine binding information to an - ORM :class:`.Session` object:: - - engine = create_engine("someurl://") - metadata.bind = engine - - .. seealso:: - - :ref:`dbengine_implicit` - background on "bound metadata" - - """ - return self._bind - - @util.dependencies("sqlalchemy.engine.url") - def _bind_to(self, url, bind): - """Bind this MetaData to an Engine, Connection, string or URL.""" - - if isinstance(bind, util.string_types + (url.URL, )): - self._bind = sqlalchemy.create_engine(bind) - else: - self._bind = bind - bind = property(bind, _bind_to) - - def clear(self): - """Clear all Table objects from this MetaData.""" - - dict.clear(self.tables) - self._schemas.clear() - self._fk_memos.clear() - - def remove(self, table): - """Remove the given Table object from this MetaData.""" - - self._remove_table(table.name, table.schema) - - @property - def sorted_tables(self): - """Returns a list of :class:`.Table` objects sorted in order of - foreign key dependency. - - The sorting will place :class:`.Table` objects that have dependencies - first, before the dependencies themselves, representing the - order in which they can be created. To get the order in which - the tables would be dropped, use the ``reversed()`` Python built-in. - - .. seealso:: - - :attr:`.MetaData.tables` - - :meth:`.Inspector.get_table_names` - - """ - return ddl.sort_tables(self.tables.values()) - - def reflect(self, bind=None, schema=None, views=False, only=None, - extend_existing=False, - autoload_replace=True): - """Load all available table definitions from the database. - - Automatically creates ``Table`` entries in this ``MetaData`` for any - table available in the database but not yet present in the - ``MetaData``. May be called multiple times to pick up tables recently - added to the database, however no special action is taken if a table - in this ``MetaData`` no longer exists in the database. - - :param bind: - A :class:`.Connectable` used to access the database; if None, uses - the existing bind on this ``MetaData``, if any. - - :param schema: - Optional, query and reflect tables from an alterate schema. - If None, the schema associated with this :class:`.MetaData` - is used, if any. - - :param views: - If True, also reflect views. - - :param only: - Optional. Load only a sub-set of available named tables. May be - specified as a sequence of names or a callable. - - If a sequence of names is provided, only those tables will be - reflected. An error is raised if a table is requested but not - available. Named tables already present in this ``MetaData`` are - ignored. - - If a callable is provided, it will be used as a boolean predicate to - filter the list of potential table names. The callable is called - with a table name and this ``MetaData`` instance as positional - arguments and should return a true value for any table to reflect. - - :param extend_existing: Passed along to each :class:`.Table` as - :paramref:`.Table.extend_existing`. - - .. versionadded:: 0.9.1 - - :param autoload_replace: Passed along to each :class:`.Table` as - :paramref:`.Table.autoload_replace`. - - .. versionadded:: 0.9.1 - - - """ - if bind is None: - bind = _bind_or_error(self) - - with bind.connect() as conn: - - reflect_opts = { - 'autoload': True, - 'autoload_with': conn, - 'extend_existing': extend_existing, - 'autoload_replace': autoload_replace - } - - if schema is None: - schema = self.schema - - if schema is not None: - reflect_opts['schema'] = schema - - available = util.OrderedSet(bind.engine.table_names(schema, - connection=conn)) - if views: - available.update( - bind.dialect.get_view_names(conn, schema) - ) - - if schema is not None: - available_w_schema = util.OrderedSet(["%s.%s" % (schema, name) - for name in available]) - else: - available_w_schema = available - - current = set(self.tables) - - if only is None: - load = [name for name, schname in - zip(available, available_w_schema) - if extend_existing or schname not in current] - elif util.callable(only): - load = [name for name, schname in - zip(available, available_w_schema) - if (extend_existing or schname not in current) - and only(name, self)] - else: - missing = [name for name in only if name not in available] - if missing: - s = schema and (" schema '%s'" % schema) or '' - raise exc.InvalidRequestError( - 'Could not reflect: requested table(s) not available ' - 'in %s%s: (%s)' % - (bind.engine.url, s, ', '.join(missing))) - load = [name for name in only if extend_existing or - name not in current] - - for name in load: - Table(name, self, **reflect_opts) - - def append_ddl_listener(self, event_name, listener): - """Append a DDL event listener to this ``MetaData``. - - .. deprecated:: 0.7 - See :class:`.DDLEvents`. - - """ - def adapt_listener(target, connection, **kw): - tables = kw['tables'] - listener(event, target, connection, tables=tables) - - event.listen(self, "" + event_name.replace('-', '_'), adapt_listener) - - def create_all(self, bind=None, tables=None, checkfirst=True): - """Create all tables stored in this metadata. - - Conditional by default, will not attempt to recreate tables already - present in the target database. - - :param bind: - A :class:`.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. - - :param tables: - Optional list of ``Table`` objects, which is a subset of the total - tables in the ``MetaData`` (others are ignored). - - :param checkfirst: - Defaults to True, don't issue CREATEs for tables already present - in the target database. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaGenerator, - self, - checkfirst=checkfirst, - tables=tables) - - def drop_all(self, bind=None, tables=None, checkfirst=True): - """Drop all tables stored in this metadata. - - Conditional by default, will not attempt to drop tables not present in - the target database. - - :param bind: - A :class:`.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. - - :param tables: - Optional list of ``Table`` objects, which is a subset of the - total tables in the ``MetaData`` (others are ignored). - - :param checkfirst: - Defaults to True, only issue DROPs for tables confirmed to be - present in the target database. - - """ - if bind is None: - bind = _bind_or_error(self) - bind._run_visitor(ddl.SchemaDropper, - self, - checkfirst=checkfirst, - tables=tables) - - -class ThreadLocalMetaData(MetaData): - """A MetaData variant that presents a different ``bind`` in every thread. - - Makes the ``bind`` property of the MetaData a thread-local value, allowing - this collection of tables to be bound to different ``Engine`` - implementations or connections in each thread. - - The ThreadLocalMetaData starts off bound to None in each thread. Binds - must be made explicitly by assigning to the ``bind`` property or using - ``connect()``. You can also re-bind dynamically multiple times per - thread, just like a regular ``MetaData``. - - """ - - __visit_name__ = 'metadata' - - def __init__(self): - """Construct a ThreadLocalMetaData.""" - - self.context = util.threading.local() - self.__engines = {} - super(ThreadLocalMetaData, self).__init__() - - def bind(self): - """The bound Engine or Connection for this thread. - - This property may be assigned an Engine or Connection, or assigned a - string or URL to automatically create a basic Engine for this bind - with ``create_engine()``.""" - - return getattr(self.context, '_engine', None) - - @util.dependencies("sqlalchemy.engine.url") - def _bind_to(self, url, bind): - """Bind to a Connectable in the caller's thread.""" - - if isinstance(bind, util.string_types + (url.URL, )): - try: - self.context._engine = self.__engines[bind] - except KeyError: - e = sqlalchemy.create_engine(bind) - self.__engines[bind] = e - self.context._engine = e - else: - # TODO: this is squirrely. we shouldnt have to hold onto engines - # in a case like this - if bind not in self.__engines: - self.__engines[bind] = bind - self.context._engine = bind - - bind = property(bind, _bind_to) - - def is_bound(self): - """True if there is a bind for this thread.""" - return (hasattr(self.context, '_engine') and - self.context._engine is not None) - - def dispose(self): - """Dispose all bound engines, in all thread contexts.""" - - for e in self.__engines.values(): - if hasattr(e, 'dispose'): - e.dispose() - - - diff --git a/libs/sqlalchemy/sql/selectable.py b/libs/sqlalchemy/sql/selectable.py deleted file mode 100644 index 951268b2..00000000 --- a/libs/sqlalchemy/sql/selectable.py +++ /dev/null @@ -1,3001 +0,0 @@ -# sql/selectable.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""The :class:`.FromClause` class of SQL expression elements, representing -SQL tables and derived rowsets. - -""" - -from .elements import ClauseElement, TextClause, ClauseList, \ - and_, Grouping, UnaryExpression, literal_column -from .elements import _clone, \ - _literal_as_text, _interpret_as_column_or_from, _expand_cloned,\ - _select_iterables, _anonymous_label, _clause_element_as_expr,\ - _cloned_intersection, _cloned_difference, True_, _only_column_elements -from .base import Immutable, Executable, _generative, \ - ColumnCollection, ColumnSet, _from_objects, Generative -from . import type_api -from .. import inspection -from .. import util -from .. import exc -from operator import attrgetter -from . import operators -import operator -from .annotation import Annotated -import itertools - -def _interpret_as_from(element): - insp = inspection.inspect(element, raiseerr=False) - if insp is None: - if isinstance(element, util.string_types): - return TextClause(util.text_type(element)) - elif hasattr(insp, "selectable"): - return insp.selectable - raise exc.ArgumentError("FROM expression expected") - -def _interpret_as_select(element): - element = _interpret_as_from(element) - if isinstance(element, Alias): - element = element.original - if not isinstance(element, Select): - element = element.select() - return element - -def subquery(alias, *args, **kwargs): - """Return an :class:`.Alias` object derived - from a :class:`.Select`. - - name - alias name - - \*args, \**kwargs - - all other arguments are delivered to the - :func:`select` function. - - """ - return Select(*args, **kwargs).alias(alias) - - - -def alias(selectable, name=None, flat=False): - """Return an :class:`.Alias` object. - - An :class:`.Alias` represents any :class:`.FromClause` - with an alternate name assigned within SQL, typically using the ``AS`` - clause when generated, e.g. ``SELECT * FROM table AS aliasname``. - - Similar functionality is available via the - :meth:`~.FromClause.alias` method - available on all :class:`.FromClause` subclasses. - - When an :class:`.Alias` is created from a :class:`.Table` object, - this has the effect of the table being rendered - as ``tablename AS aliasname`` in a SELECT statement. - - For :func:`.select` objects, the effect is that of creating a named - subquery, i.e. ``(select ...) AS aliasname``. - - The ``name`` parameter is optional, and provides the name - to use in the rendered SQL. If blank, an "anonymous" name - will be deterministically generated at compile time. - Deterministic means the name is guaranteed to be unique against - other constructs used in the same statement, and will also be the - same name for each successive compilation of the same statement - object. - - :param selectable: any :class:`.FromClause` subclass, - such as a table, select statement, etc. - - :param name: string name to be assigned as the alias. - If ``None``, a name will be deterministically generated - at compile time. - - :param flat: Will be passed through to if the given selectable - is an instance of :class:`.Join` - see :meth:`.Join.alias` - for details. - - .. versionadded:: 0.9.0 - - """ - return selectable.alias(name=name, flat=flat) - - -class Selectable(ClauseElement): - """mark a class as being selectable""" - __visit_name__ = 'selectable' - - is_selectable = True - - @property - def selectable(self): - return self - - -class FromClause(Selectable): - """Represent an element that can be used within the ``FROM`` - clause of a ``SELECT`` statement. - - The most common forms of :class:`.FromClause` are the - :class:`.Table` and the :func:`.select` constructs. Key - features common to all :class:`.FromClause` objects include: - - * a :attr:`.c` collection, which provides per-name access to a collection - of :class:`.ColumnElement` objects. - * a :attr:`.primary_key` attribute, which is a collection of all those - :class:`.ColumnElement` objects that indicate the ``primary_key`` flag. - * Methods to generate various derivations of a "from" clause, including - :meth:`.FromClause.alias`, :meth:`.FromClause.join`, - :meth:`.FromClause.select`. - - - """ - __visit_name__ = 'fromclause' - named_with_column = False - _hide_froms = [] - - schema = None - """Define the 'schema' attribute for this :class:`.FromClause`. - - This is typically ``None`` for most objects except that of :class:`.Table`, - where it is taken as the value of the :paramref:`.Table.schema` argument. - - """ - - _memoized_property = util.group_expirable_memoized_property(["_columns"]) - - @util.dependencies("sqlalchemy.sql.functions") - def count(self, functions, whereclause=None, **params): - """return a SELECT COUNT generated against this - :class:`.FromClause`.""" - - if self.primary_key: - col = list(self.primary_key)[0] - else: - col = list(self.columns)[0] - return Select( - [functions.func.count(col).label('tbl_row_count')], - whereclause, - from_obj=[self], - **params) - - def select(self, whereclause=None, **params): - """return a SELECT of this :class:`.FromClause`. - - .. seealso:: - - :func:`~.sql.expression.select` - general purpose - method which allows for arbitrary column lists. - - """ - - return Select([self], whereclause, **params) - - def join(self, right, onclause=None, isouter=False): - """return a join of this :class:`.FromClause` against another - :class:`.FromClause`.""" - - return Join(self, right, onclause, isouter) - - def outerjoin(self, right, onclause=None): - """return an outer join of this :class:`.FromClause` against another - :class:`.FromClause`.""" - - return Join(self, right, onclause, True) - - def alias(self, name=None, flat=False): - """return an alias of this :class:`.FromClause`. - - This is shorthand for calling:: - - from sqlalchemy import alias - a = alias(self, name=name) - - See :func:`~.expression.alias` for details. - - """ - - return Alias(self, name) - - def is_derived_from(self, fromclause): - """Return True if this FromClause is 'derived' from the given - FromClause. - - An example would be an Alias of a Table is derived from that Table. - - """ - # this is essentially an "identity" check in the base class. - # Other constructs override this to traverse through - # contained elements. - return fromclause in self._cloned_set - - def _is_lexical_equivalent(self, other): - """Return True if this FromClause and the other represent - the same lexical identity. - - This tests if either one is a copy of the other, or - if they are the same via annotation identity. - - """ - return self._cloned_set.intersection(other._cloned_set) - - @util.dependencies("sqlalchemy.sql.util") - def replace_selectable(self, sqlutil, old, alias): - """replace all occurrences of FromClause 'old' with the given Alias - object, returning a copy of this :class:`.FromClause`. - - """ - - return sqlutil.ClauseAdapter(alias).traverse(self) - - def correspond_on_equivalents(self, column, equivalents): - """Return corresponding_column for the given column, or if None - search for a match in the given dictionary. - - """ - col = self.corresponding_column(column, require_embedded=True) - if col is None and col in equivalents: - for equiv in equivalents[col]: - nc = self.corresponding_column(equiv, require_embedded=True) - if nc: - return nc - return col - - def corresponding_column(self, column, require_embedded=False): - """Given a :class:`.ColumnElement`, return the exported - :class:`.ColumnElement` object from this :class:`.Selectable` - which corresponds to that original - :class:`~sqlalchemy.schema.Column` via a common ancestor - column. - - :param column: the target :class:`.ColumnElement` to be matched - - :param require_embedded: only return corresponding columns for - the given :class:`.ColumnElement`, if the given :class:`.ColumnElement` - is actually present within a sub-element - of this :class:`.FromClause`. Normally the column will match if - it merely shares a common ancestor with one of the exported - columns of this :class:`.FromClause`. - - """ - - def embedded(expanded_proxy_set, target_set): - for t in target_set.difference(expanded_proxy_set): - if not set(_expand_cloned([t]) - ).intersection(expanded_proxy_set): - return False - return True - - # don't dig around if the column is locally present - if self.c.contains_column(column): - return column - col, intersect = None, None - target_set = column.proxy_set - cols = self.c - for c in cols: - expanded_proxy_set = set(_expand_cloned(c.proxy_set)) - i = target_set.intersection(expanded_proxy_set) - if i and (not require_embedded - or embedded(expanded_proxy_set, target_set)): - if col is None: - - # no corresponding column yet, pick this one. - - col, intersect = c, i - elif len(i) > len(intersect): - - # 'c' has a larger field of correspondence than - # 'col'. i.e. selectable.c.a1_x->a1.c.x->table.c.x - # matches a1.c.x->table.c.x better than - # selectable.c.x->table.c.x does. - - col, intersect = c, i - elif i == intersect: - - # they have the same field of correspondence. see - # which proxy_set has fewer columns in it, which - # indicates a closer relationship with the root - # column. Also take into account the "weight" - # attribute which CompoundSelect() uses to give - # higher precedence to columns based on vertical - # position in the compound statement, and discard - # columns that have no reference to the target - # column (also occurs with CompoundSelect) - - col_distance = util.reduce(operator.add, - [sc._annotations.get('weight', 1) for sc in - col.proxy_set if sc.shares_lineage(column)]) - c_distance = util.reduce(operator.add, - [sc._annotations.get('weight', 1) for sc in - c.proxy_set if sc.shares_lineage(column)]) - if c_distance < col_distance: - col, intersect = c, i - return col - - @property - def description(self): - """a brief description of this FromClause. - - Used primarily for error message formatting. - - """ - return getattr(self, 'name', self.__class__.__name__ + " object") - - def _reset_exported(self): - """delete memoized collections when a FromClause is cloned.""" - - self._memoized_property.expire_instance(self) - - @_memoized_property - def columns(self): - """A named-based collection of :class:`.ColumnElement` objects - maintained by this :class:`.FromClause`. - - The :attr:`.columns`, or :attr:`.c` collection, is the gateway - to the construction of SQL expressions using table-bound or - other selectable-bound columns:: - - select([mytable]).where(mytable.c.somecolumn == 5) - - """ - - if '_columns' not in self.__dict__: - self._init_collections() - self._populate_column_collection() - return self._columns.as_immutable() - - @_memoized_property - def primary_key(self): - """Return the collection of Column objects which comprise the - primary key of this FromClause.""" - - self._init_collections() - self._populate_column_collection() - return self.primary_key - - @_memoized_property - def foreign_keys(self): - """Return the collection of ForeignKey objects which this - FromClause references.""" - - self._init_collections() - self._populate_column_collection() - return self.foreign_keys - - c = property(attrgetter('columns'), - doc="An alias for the :attr:`.columns` attribute.") - _select_iterable = property(attrgetter('columns')) - - def _init_collections(self): - assert '_columns' not in self.__dict__ - assert 'primary_key' not in self.__dict__ - assert 'foreign_keys' not in self.__dict__ - - self._columns = ColumnCollection() - self.primary_key = ColumnSet() - self.foreign_keys = set() - - @property - def _cols_populated(self): - return '_columns' in self.__dict__ - - def _populate_column_collection(self): - """Called on subclasses to establish the .c collection. - - Each implementation has a different way of establishing - this collection. - - """ - - def _refresh_for_new_column(self, column): - """Given a column added to the .c collection of an underlying - selectable, produce the local version of that column, assuming this - selectable ultimately should proxy this column. - - this is used to "ping" a derived selectable to add a new column - to its .c. collection when a Column has been added to one of the - Table objects it ultimtely derives from. - - If the given selectable hasn't populated it's .c. collection yet, - it should at least pass on the message to the contained selectables, - but it will return None. - - This method is currently used by Declarative to allow Table - columns to be added to a partially constructed inheritance - mapping that may have already produced joins. The method - isn't public right now, as the full span of implications - and/or caveats aren't yet clear. - - It's also possible that this functionality could be invoked by - default via an event, which would require that - selectables maintain a weak referencing collection of all - derivations. - - """ - if not self._cols_populated: - return None - elif column.key in self.columns and self.columns[column.key] is column: - return column - else: - return None - - -class Join(FromClause): - """represent a ``JOIN`` construct between two :class:`.FromClause` - elements. - - The public constructor function for :class:`.Join` is the module-level - :func:`join()` function, as well as the :func:`join()` method available - off all :class:`.FromClause` subclasses. - - """ - __visit_name__ = 'join' - - def __init__(self, left, right, onclause=None, isouter=False): - """Construct a new :class:`.Join`. - - The usual entrypoint here is the :func:`~.expression.join` - function or the :meth:`.FromClause.join` method of any - :class:`.FromClause` object. - - """ - self.left = _interpret_as_from(left) - self.right = _interpret_as_from(right).self_group() - - if onclause is None: - self.onclause = self._match_primaries(self.left, self.right) - else: - self.onclause = onclause - - self.isouter = isouter - - @classmethod - def _create_outerjoin(cls, left, right, onclause=None): - """Return an ``OUTER JOIN`` clause element. - - The returned object is an instance of :class:`.Join`. - - Similar functionality is also available via the - :meth:`~.FromClause.outerjoin()` method on any - :class:`.FromClause`. - - :param left: The left side of the join. - - :param right: The right side of the join. - - :param onclause: Optional criterion for the ``ON`` clause, is - derived from foreign key relationships established between - left and right otherwise. - - To chain joins together, use the :meth:`.FromClause.join` or - :meth:`.FromClause.outerjoin` methods on the resulting - :class:`.Join` object. - - """ - return cls(left, right, onclause, isouter=True) - - - @classmethod - def _create_join(cls, left, right, onclause=None, isouter=False): - """Return a ``JOIN`` clause element (regular inner join). - - The returned object is an instance of :class:`.Join`. - - Similar functionality is also available via the - :meth:`~.FromClause.join()` method on any - :class:`.FromClause`. - - :param left: The left side of the join. - - :param right: The right side of the join. - - :param onclause: Optional criterion for the ``ON`` clause, is - derived from foreign key relationships established between - left and right otherwise. - - :param isouter: if True, produce an outer join; synonymous - with :func:`.outerjoin`. - - To chain joins together, use the :meth:`.FromClause.join` or - :meth:`.FromClause.outerjoin` methods on the resulting - :class:`.Join` object. - - - """ - return cls(left, right, onclause, isouter) - - - @property - def description(self): - return "Join object on %s(%d) and %s(%d)" % ( - self.left.description, - id(self.left), - self.right.description, - id(self.right)) - - def is_derived_from(self, fromclause): - return fromclause is self or \ - self.left.is_derived_from(fromclause) or \ - self.right.is_derived_from(fromclause) - - def self_group(self, against=None): - return FromGrouping(self) - - @util.dependencies("sqlalchemy.sql.util") - def _populate_column_collection(self, sqlutil): - columns = [c for c in self.left.columns] + \ - [c for c in self.right.columns] - - self.primary_key.extend(sqlutil.reduce_columns( - (c for c in columns if c.primary_key), self.onclause)) - self._columns.update((col._label, col) for col in columns) - self.foreign_keys.update(itertools.chain( - *[col.foreign_keys for col in columns])) - - def _refresh_for_new_column(self, column): - col = self.left._refresh_for_new_column(column) - if col is None: - col = self.right._refresh_for_new_column(column) - if col is not None: - if self._cols_populated: - self._columns[col._label] = col - self.foreign_keys.add(col) - if col.primary_key: - self.primary_key.add(col) - return col - return None - - def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() - self.left = clone(self.left, **kw) - self.right = clone(self.right, **kw) - self.onclause = clone(self.onclause, **kw) - - def get_children(self, **kwargs): - return self.left, self.right, self.onclause - - def _match_primaries(self, left, right): - if isinstance(left, Join): - left_right = left.right - else: - left_right = None - return self._join_condition(left, right, a_subset=left_right) - - @classmethod - def _join_condition(cls, a, b, ignore_nonexistent_tables=False, - a_subset=None, - consider_as_foreign_keys=None): - """create a join condition between two tables or selectables. - - e.g.:: - - join_condition(tablea, tableb) - - would produce an expression along the lines of:: - - tablea.c.id==tableb.c.tablea_id - - The join is determined based on the foreign key relationships - between the two selectables. If there are multiple ways - to join, or no way to join, an error is raised. - - :param ignore_nonexistent_tables: Deprecated - this - flag is no longer used. Only resolution errors regarding - the two given tables are propagated. - - :param a_subset: An optional expression that is a sub-component - of ``a``. An attempt will be made to join to just this sub-component - first before looking at the full ``a`` construct, and if found - will be successful even if there are other ways to join to ``a``. - This allows the "right side" of a join to be passed thereby - providing a "natural join". - - """ - crit = [] - constraints = set() - - for left in (a_subset, a): - if left is None: - continue - for fk in sorted( - b.foreign_keys, - key=lambda fk: fk.parent._creation_order): - if consider_as_foreign_keys is not None and \ - fk.parent not in consider_as_foreign_keys: - continue - try: - col = fk.get_referent(left) - except exc.NoReferenceError as nrte: - if nrte.table_name == left.name: - raise - else: - continue - - if col is not None: - crit.append(col == fk.parent) - constraints.add(fk.constraint) - if left is not b: - for fk in sorted( - left.foreign_keys, - key=lambda fk: fk.parent._creation_order): - if consider_as_foreign_keys is not None and \ - fk.parent not in consider_as_foreign_keys: - continue - try: - col = fk.get_referent(b) - except exc.NoReferenceError as nrte: - if nrte.table_name == b.name: - raise - else: - # this is totally covered. can't get - # coverage to mark it. - continue - - if col is not None: - crit.append(col == fk.parent) - constraints.add(fk.constraint) - if crit: - break - - if len(crit) == 0: - if isinstance(b, FromGrouping): - hint = " Perhaps you meant to convert the right side to a "\ - "subquery using alias()?" - else: - hint = "" - raise exc.NoForeignKeysError( - "Can't find any foreign key relationships " - "between '%s' and '%s'.%s" % (a.description, b.description, hint)) - elif len(constraints) > 1: - raise exc.AmbiguousForeignKeysError( - "Can't determine join between '%s' and '%s'; " - "tables have more than one foreign key " - "constraint relationship between them. " - "Please specify the 'onclause' of this " - "join explicitly." % (a.description, b.description)) - elif len(crit) == 1: - return (crit[0]) - else: - return and_(*crit) - - - def select(self, whereclause=None, **kwargs): - """Create a :class:`.Select` from this :class:`.Join`. - - The equivalent long-hand form, given a :class:`.Join` object - ``j``, is:: - - from sqlalchemy import select - j = select([j.left, j.right], **kw).\\ - where(whereclause).\\ - select_from(j) - - :param whereclause: the WHERE criterion that will be sent to - the :func:`select()` function - - :param \**kwargs: all other kwargs are sent to the - underlying :func:`select()` function. - - """ - collist = [self.left, self.right] - - return Select(collist, whereclause, from_obj=[self], **kwargs) - - @property - def bind(self): - return self.left.bind or self.right.bind - - @util.dependencies("sqlalchemy.sql.util") - def alias(self, sqlutil, name=None, flat=False): - """return an alias of this :class:`.Join`. - - The default behavior here is to first produce a SELECT - construct from this :class:`.Join`, then to produce a - :class:`.Alias` from that. So given a join of the form:: - - j = table_a.join(table_b, table_a.c.id == table_b.c.a_id) - - The JOIN by itself would look like:: - - table_a JOIN table_b ON table_a.id = table_b.a_id - - Whereas the alias of the above, ``j.alias()``, would in a - SELECT context look like:: - - (SELECT table_a.id AS table_a_id, table_b.id AS table_b_id, - table_b.a_id AS table_b_a_id - FROM table_a - JOIN table_b ON table_a.id = table_b.a_id) AS anon_1 - - The equivalent long-hand form, given a :class:`.Join` object - ``j``, is:: - - from sqlalchemy import select, alias - j = alias( - select([j.left, j.right]).\\ - select_from(j).\\ - with_labels(True).\\ - correlate(False), - name=name - ) - - The selectable produced by :meth:`.Join.alias` features the same - columns as that of the two individual selectables presented under - a single name - the individual columns are "auto-labeled", meaning - the ``.c.`` collection of the resulting :class:`.Alias` represents - the names of the individual columns using a ``_`` - scheme:: - - j.c.table_a_id - j.c.table_b_a_id - - :meth:`.Join.alias` also features an alternate - option for aliasing joins which produces no enclosing SELECT and - does not normally apply labels to the column names. The - ``flat=True`` option will call :meth:`.FromClause.alias` - against the left and right sides individually. - Using this option, no new ``SELECT`` is produced; - we instead, from a construct as below:: - - j = table_a.join(table_b, table_a.c.id == table_b.c.a_id) - j = j.alias(flat=True) - - we get a result like this:: - - table_a AS table_a_1 JOIN table_b AS table_b_1 ON - table_a_1.id = table_b_1.a_id - - The ``flat=True`` argument is also propagated to the contained - selectables, so that a composite join such as:: - - j = table_a.join( - table_b.join(table_c, - table_b.c.id == table_c.c.b_id), - table_b.c.a_id == table_a.c.id - ).alias(flat=True) - - Will produce an expression like:: - - table_a AS table_a_1 JOIN ( - table_b AS table_b_1 JOIN table_c AS table_c_1 - ON table_b_1.id = table_c_1.b_id - ) ON table_a_1.id = table_b_1.a_id - - The standalone :func:`~.expression.alias` function as well as the - base :meth:`.FromClause.alias` method also support the ``flat=True`` - argument as a no-op, so that the argument can be passed to the - ``alias()`` method of any selectable. - - .. versionadded:: 0.9.0 Added the ``flat=True`` option to create - "aliases" of joins without enclosing inside of a SELECT - subquery. - - :param name: name given to the alias. - - :param flat: if True, produce an alias of the left and right - sides of this :class:`.Join` and return the join of those - two selectables. This produces join expression that does not - include an enclosing SELECT. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :func:`~.expression.alias` - - """ - if flat: - assert name is None, "Can't send name argument with flat" - left_a, right_a = self.left.alias(flat=True), \ - self.right.alias(flat=True) - adapter = sqlutil.ClauseAdapter(left_a).\ - chain(sqlutil.ClauseAdapter(right_a)) - - return left_a.join(right_a, - adapter.traverse(self.onclause), isouter=self.isouter) - else: - return self.select(use_labels=True, correlate=False).alias(name) - - @property - def _hide_froms(self): - return itertools.chain(*[_from_objects(x.left, x.right) - for x in self._cloned_set]) - - @property - def _from_objects(self): - return [self] + \ - self.onclause._from_objects + \ - self.left._from_objects + \ - self.right._from_objects - - -class Alias(FromClause): - """Represents an table or selectable alias (AS). - - Represents an alias, as typically applied to any table or - sub-select within a SQL statement using the ``AS`` keyword (or - without the keyword on certain databases such as Oracle). - - This object is constructed from the :func:`~.expression.alias` module level - function as well as the :meth:`.FromClause.alias` method available on all - :class:`.FromClause` subclasses. - - """ - - __visit_name__ = 'alias' - named_with_column = True - - def __init__(self, selectable, name=None): - baseselectable = selectable - while isinstance(baseselectable, Alias): - baseselectable = baseselectable.element - self.original = baseselectable - self.supports_execution = baseselectable.supports_execution - if self.supports_execution: - self._execution_options = baseselectable._execution_options - self.element = selectable - if name is None: - if self.original.named_with_column: - name = getattr(self.original, 'name', None) - name = _anonymous_label('%%(%d %s)s' % (id(self), name - or 'anon')) - self.name = name - - @property - def description(self): - if util.py3k: - return self.name - else: - return self.name.encode('ascii', 'backslashreplace') - - def as_scalar(self): - try: - return self.element.as_scalar() - except AttributeError: - raise AttributeError("Element %s does not support " - "'as_scalar()'" % self.element) - - def is_derived_from(self, fromclause): - if fromclause in self._cloned_set: - return True - return self.element.is_derived_from(fromclause) - - def _populate_column_collection(self): - for col in self.element.columns: - col._make_proxy(self) - - def _refresh_for_new_column(self, column): - col = self.element._refresh_for_new_column(column) - if col is not None: - if not self._cols_populated: - return None - else: - return col._make_proxy(self) - else: - return None - - def _copy_internals(self, clone=_clone, **kw): - # don't apply anything to an aliased Table - # for now. May want to drive this from - # the given **kw. - if isinstance(self.element, TableClause): - return - self._reset_exported() - self.element = clone(self.element, **kw) - baseselectable = self.element - while isinstance(baseselectable, Alias): - baseselectable = baseselectable.element - self.original = baseselectable - - def get_children(self, column_collections=True, **kw): - if column_collections: - for c in self.c: - yield c - yield self.element - - @property - def _from_objects(self): - return [self] - - @property - def bind(self): - return self.element.bind - - -class CTE(Alias): - """Represent a Common Table Expression. - - The :class:`.CTE` object is obtained using the - :meth:`.SelectBase.cte` method from any selectable. - See that method for complete examples. - - .. versionadded:: 0.7.6 - - """ - __visit_name__ = 'cte' - - def __init__(self, selectable, - name=None, - recursive=False, - _cte_alias=None, - _restates=frozenset()): - self.recursive = recursive - self._cte_alias = _cte_alias - self._restates = _restates - super(CTE, self).__init__(selectable, name=name) - - def alias(self, name=None, flat=False): - return CTE( - self.original, - name=name, - recursive=self.recursive, - _cte_alias=self, - ) - - def union(self, other): - return CTE( - self.original.union(other), - name=self.name, - recursive=self.recursive, - _restates=self._restates.union([self]) - ) - - def union_all(self, other): - return CTE( - self.original.union_all(other), - name=self.name, - recursive=self.recursive, - _restates=self._restates.union([self]) - ) - - - - -class FromGrouping(FromClause): - """Represent a grouping of a FROM clause""" - __visit_name__ = 'grouping' - - def __init__(self, element): - self.element = element - - def _init_collections(self): - pass - - @property - def columns(self): - return self.element.columns - - @property - def primary_key(self): - return self.element.primary_key - - @property - def foreign_keys(self): - return self.element.foreign_keys - - def is_derived_from(self, element): - return self.element.is_derived_from(element) - - def alias(self, **kw): - return FromGrouping(self.element.alias(**kw)) - - @property - def _hide_froms(self): - return self.element._hide_froms - - def get_children(self, **kwargs): - return self.element, - - def _copy_internals(self, clone=_clone, **kw): - self.element = clone(self.element, **kw) - - @property - def _from_objects(self): - return self.element._from_objects - - def __getattr__(self, attr): - return getattr(self.element, attr) - - def __getstate__(self): - return {'element': self.element} - - def __setstate__(self, state): - self.element = state['element'] - -class TableClause(Immutable, FromClause): - """Represents a minimal "table" construct. - - This is a lightweight table object that has only a name and a - collection of columns, which are typically produced - by the :func:`.expression.column` function:: - - from sqlalchemy.sql import table, column - - user = table("user", - column("id"), - column("name"), - column("description"), - ) - - The :class:`.TableClause` construct serves as the base for - the more commonly used :class:`~.schema.Table` object, providing - the usual set of :class:`~.expression.FromClause` services including - the ``.c.`` collection and statement generation methods. - - It does **not** provide all the additional schema-level services - of :class:`~.schema.Table`, including constraints, references to other - tables, or support for :class:`.MetaData`-level services. It's useful - on its own as an ad-hoc construct used to generate quick SQL - statements when a more fully fledged :class:`~.schema.Table` - is not on hand. - - """ - - __visit_name__ = 'table' - - named_with_column = True - - implicit_returning = False - """:class:`.TableClause` doesn't support having a primary key or column - -level defaults, so implicit returning doesn't apply.""" - - _autoincrement_column = None - """No PK or default support so no autoincrement column.""" - - def __init__(self, name, *columns): - """Produce a new :class:`.TableClause`. - - The object returned is an instance of :class:`.TableClause`, which - represents the "syntactical" portion of the schema-level - :class:`~.schema.Table` object. - It may be used to construct lightweight table constructs. - - Note that the :func:`.expression.table` function is not part of - the ``sqlalchemy`` namespace. It must be imported from the - ``sql`` package:: - - from sqlalchemy.sql import table, column - - :param name: Name of the table. - - :param columns: A collection of :func:`.expression.column` constructs. - - """ - - super(TableClause, self).__init__() - self.name = self.fullname = name - self._columns = ColumnCollection() - self.primary_key = ColumnSet() - self.foreign_keys = set() - for c in columns: - self.append_column(c) - - def _init_collections(self): - pass - - @util.memoized_property - def description(self): - if util.py3k: - return self.name - else: - return self.name.encode('ascii', 'backslashreplace') - - def append_column(self, c): - self._columns[c.key] = c - c.table = self - - def get_children(self, column_collections=True, **kwargs): - if column_collections: - return [c for c in self.c] - else: - return [] - - @util.dependencies("sqlalchemy.sql.functions") - def count(self, functions, whereclause=None, **params): - """return a SELECT COUNT generated against this - :class:`.TableClause`.""" - - if self.primary_key: - col = list(self.primary_key)[0] - else: - col = list(self.columns)[0] - return Select( - [functions.func.count(col).label('tbl_row_count')], - whereclause, - from_obj=[self], - **params) - - @util.dependencies("sqlalchemy.sql.dml") - def insert(self, dml, values=None, inline=False, **kwargs): - """Generate an :func:`.insert` construct against this - :class:`.TableClause`. - - E.g.:: - - table.insert().values(name='foo') - - See :func:`.insert` for argument and usage information. - - """ - - return dml.Insert(self, values=values, inline=inline, **kwargs) - - @util.dependencies("sqlalchemy.sql.dml") - def update(self, dml, whereclause=None, values=None, inline=False, **kwargs): - """Generate an :func:`.update` construct against this - :class:`.TableClause`. - - E.g.:: - - table.update().where(table.c.id==7).values(name='foo') - - See :func:`.update` for argument and usage information. - - """ - - return dml.Update(self, whereclause=whereclause, - values=values, inline=inline, **kwargs) - - @util.dependencies("sqlalchemy.sql.dml") - def delete(self, dml, whereclause=None, **kwargs): - """Generate a :func:`.delete` construct against this - :class:`.TableClause`. - - E.g.:: - - table.delete().where(table.c.id==7) - - See :func:`.delete` for argument and usage information. - - """ - - return dml.Delete(self, whereclause, **kwargs) - - @property - def _from_objects(self): - return [self] - - -class ForUpdateArg(ClauseElement): - - @classmethod - def parse_legacy_select(self, arg): - """Parse the for_update arugment of :func:`.select`. - - :param mode: Defines the lockmode to use. - - ``None`` - translates to no lockmode - - ``'update'`` - translates to ``FOR UPDATE`` - (standard SQL, supported by most dialects) - - ``'nowait'`` - translates to ``FOR UPDATE NOWAIT`` - (supported by Oracle, PostgreSQL 8.1 upwards) - - ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL), - and ``FOR SHARE`` (for PostgreSQL) - - ``'read_nowait'`` - translates to ``FOR SHARE NOWAIT`` - (supported by PostgreSQL). ``FOR SHARE`` and - ``FOR SHARE NOWAIT`` (PostgreSQL). - - """ - if arg in (None, False): - return None - - nowait = read = False - if arg == 'nowait': - nowait = True - elif arg == 'read': - read = True - elif arg == 'read_nowait': - read = nowait = True - elif arg is not True: - raise exc.ArgumentError("Unknown for_update argument: %r" % arg) - - return ForUpdateArg(read=read, nowait=nowait) - - @property - def legacy_for_update_value(self): - if self.read and not self.nowait: - return "read" - elif self.read and self.nowait: - return "read_nowait" - elif self.nowait: - return "nowait" - else: - return True - - def _copy_internals(self, clone=_clone, **kw): - if self.of is not None: - self.of = [clone(col, **kw) for col in self.of] - - def __init__(self, nowait=False, read=False, of=None): - """Represents arguments specified to :meth:`.Select.for_update`. - - .. versionadded:: 0.9.0 - """ - - self.nowait = nowait - self.read = read - if of is not None: - self.of = [_interpret_as_column_or_from(elem) - for elem in util.to_list(of)] - else: - self.of = None - - -class SelectBase(Executable, FromClause): - """Base class for SELECT statements. - - - This includes :class:`.Select`, :class:`.CompoundSelect` and - :class:`.TextAsFrom`. - - - """ - - def as_scalar(self): - """return a 'scalar' representation of this selectable, which can be - used as a column expression. - - Typically, a select statement which has only one column in its columns - clause is eligible to be used as a scalar expression. - - The returned object is an instance of - :class:`ScalarSelect`. - - """ - return ScalarSelect(self) - - - def label(self, name): - """return a 'scalar' representation of this selectable, embedded as a - subquery with a label. - - .. seealso:: - - :meth:`~.SelectBase.as_scalar`. - - """ - return self.as_scalar().label(name) - - def cte(self, name=None, recursive=False): - """Return a new :class:`.CTE`, or Common Table Expression instance. - - Common table expressions are a SQL standard whereby SELECT - statements can draw upon secondary statements specified along - with the primary statement, using a clause called "WITH". - Special semantics regarding UNION can also be employed to - allow "recursive" queries, where a SELECT statement can draw - upon the set of rows that have previously been selected. - - SQLAlchemy detects :class:`.CTE` objects, which are treated - similarly to :class:`.Alias` objects, as special elements - to be delivered to the FROM clause of the statement as well - as to a WITH clause at the top of the statement. - - .. versionadded:: 0.7.6 - - :param name: name given to the common table expression. Like - :meth:`._FromClause.alias`, the name can be left as ``None`` - in which case an anonymous symbol will be used at query - compile time. - :param recursive: if ``True``, will render ``WITH RECURSIVE``. - A recursive common table expression is intended to be used in - conjunction with UNION ALL in order to derive rows - from those already selected. - - The following examples illustrate two examples from - Postgresql's documentation at - http://www.postgresql.org/docs/8.4/static/queries-with.html. - - Example 1, non recursive:: - - from sqlalchemy import Table, Column, String, Integer, MetaData, \\ - select, func - - metadata = MetaData() - - orders = Table('orders', metadata, - Column('region', String), - Column('amount', Integer), - Column('product', String), - Column('quantity', Integer) - ) - - regional_sales = select([ - orders.c.region, - func.sum(orders.c.amount).label('total_sales') - ]).group_by(orders.c.region).cte("regional_sales") - - - top_regions = select([regional_sales.c.region]).\\ - where( - regional_sales.c.total_sales > - select([ - func.sum(regional_sales.c.total_sales)/10 - ]) - ).cte("top_regions") - - statement = select([ - orders.c.region, - orders.c.product, - func.sum(orders.c.quantity).label("product_units"), - func.sum(orders.c.amount).label("product_sales") - ]).where(orders.c.region.in_( - select([top_regions.c.region]) - )).group_by(orders.c.region, orders.c.product) - - result = conn.execute(statement).fetchall() - - Example 2, WITH RECURSIVE:: - - from sqlalchemy import Table, Column, String, Integer, MetaData, \\ - select, func - - metadata = MetaData() - - parts = Table('parts', metadata, - Column('part', String), - Column('sub_part', String), - Column('quantity', Integer), - ) - - included_parts = select([ - parts.c.sub_part, - parts.c.part, - parts.c.quantity]).\\ - where(parts.c.part=='our part').\\ - cte(recursive=True) - - - incl_alias = included_parts.alias() - parts_alias = parts.alias() - included_parts = included_parts.union_all( - select([ - parts_alias.c.part, - parts_alias.c.sub_part, - parts_alias.c.quantity - ]). - where(parts_alias.c.part==incl_alias.c.sub_part) - ) - - statement = select([ - included_parts.c.sub_part, - func.sum(included_parts.c.quantity). - label('total_quantity') - ]).\ - select_from(included_parts.join(parts, - included_parts.c.part==parts.c.part)).\\ - group_by(included_parts.c.sub_part) - - result = conn.execute(statement).fetchall() - - - .. seealso:: - - :meth:`.orm.query.Query.cte` - ORM version of :meth:`.SelectBase.cte`. - - """ - return CTE(self, name=name, recursive=recursive) - - @_generative - @util.deprecated('0.6', - message="``autocommit()`` is deprecated. Use " - ":meth:`.Executable.execution_options` with the " - "'autocommit' flag.") - def autocommit(self): - """return a new selectable with the 'autocommit' flag set to - True. - """ - - self._execution_options = \ - self._execution_options.union({'autocommit': True}) - - def _generate(self): - """Override the default _generate() method to also clear out - exported collections.""" - - s = self.__class__.__new__(self.__class__) - s.__dict__ = self.__dict__.copy() - s._reset_exported() - return s - - @property - def _from_objects(self): - return [self] - -class GenerativeSelect(SelectBase): - """Base class for SELECT statements where additional elements can be - added. - - This serves as the base for :class:`.Select` and :class:`.CompoundSelect` - where elements such as ORDER BY, GROUP BY can be added and column rendering - can be controlled. Compare to :class:`.TextAsFrom`, which, while it - subclasses :class:`.SelectBase` and is also a SELECT construct, represents - a fixed textual string which cannot be altered at this level, only - wrapped as a subquery. - - .. versionadded:: 0.9.0 :class:`.GenerativeSelect` was added to - provide functionality specific to :class:`.Select` and :class:`.CompoundSelect` - while allowing :class:`.SelectBase` to be used for other SELECT-like - objects, e.g. :class:`.TextAsFrom`. - - """ - _order_by_clause = ClauseList() - _group_by_clause = ClauseList() - _limit = None - _offset = None - _for_update_arg = None - - def __init__(self, - use_labels=False, - for_update=False, - limit=None, - offset=None, - order_by=None, - group_by=None, - bind=None, - autocommit=None): - self.use_labels = use_labels - - if for_update is not False: - self._for_update_arg = ForUpdateArg.parse_legacy_select(for_update) - - if autocommit is not None: - util.warn_deprecated('autocommit on select() is ' - 'deprecated. Use .execution_options(a' - 'utocommit=True)') - self._execution_options = \ - self._execution_options.union( - {'autocommit': autocommit}) - if limit is not None: - self._limit = util.asint(limit) - if offset is not None: - self._offset = util.asint(offset) - self._bind = bind - - if order_by is not None: - self._order_by_clause = ClauseList(*util.to_list(order_by)) - if group_by is not None: - self._group_by_clause = ClauseList(*util.to_list(group_by)) - - @property - def for_update(self): - """Provide legacy dialect support for the ``for_update`` attribute. - """ - if self._for_update_arg is not None: - return self._for_update_arg.legacy_for_update_value - else: - return None - - @for_update.setter - def for_update(self, value): - self._for_update_arg = ForUpdateArg.parse_legacy_select(value) - - @_generative - def with_for_update(self, nowait=False, read=False, of=None): - """Specify a ``FOR UPDATE`` clause for this :class:`.GenerativeSelect`. - - E.g.:: - - stmt = select([table]).with_for_update(nowait=True) - - On a database like Postgresql or Oracle, the above would render a - statement like:: - - SELECT table.a, table.b FROM table FOR UPDATE NOWAIT - - on other backends, the ``nowait`` option is ignored and instead - would produce:: - - SELECT table.a, table.b FROM table FOR UPDATE - - When called with no arguments, the statement will render with - the suffix ``FOR UPDATE``. Additional arguments can then be - provided which allow for common database-specific - variants. - - :param nowait: boolean; will render ``FOR UPDATE NOWAIT`` on Oracle and - Postgresql dialects. - - :param read: boolean; will render ``LOCK IN SHARE MODE`` on MySQL, - ``FOR SHARE`` on Postgresql. On Postgresql, when combined with - ``nowait``, will render ``FOR SHARE NOWAIT``. - - :param of: SQL expression or list of SQL expression elements - (typically :class:`.Column` objects or a compatible expression) which - will render into a ``FOR UPDATE OF`` clause; supported by PostgreSQL - and Oracle. May render as a table or as a column depending on - backend. - - .. versionadded:: 0.9.0 - - """ - self._for_update_arg = ForUpdateArg(nowait=nowait, read=read, of=of) - - @_generative - def apply_labels(self): - """return a new selectable with the 'use_labels' flag set to True. - - This will result in column expressions being generated using labels - against their table name, such as "SELECT somecolumn AS - tablename_somecolumn". This allows selectables which contain multiple - FROM clauses to produce a unique set of column names regardless of - name conflicts among the individual FROM clauses. - - """ - self.use_labels = True - - @_generative - def limit(self, limit): - """return a new selectable with the given LIMIT criterion - applied.""" - - self._limit = util.asint(limit) - - @_generative - def offset(self, offset): - """return a new selectable with the given OFFSET criterion - applied.""" - - self._offset = util.asint(offset) - - @_generative - def order_by(self, *clauses): - """return a new selectable with the given list of ORDER BY - criterion applied. - - The criterion will be appended to any pre-existing ORDER BY - criterion. - - """ - - self.append_order_by(*clauses) - - @_generative - def group_by(self, *clauses): - """return a new selectable with the given list of GROUP BY - criterion applied. - - The criterion will be appended to any pre-existing GROUP BY - criterion. - - """ - - self.append_group_by(*clauses) - - def append_order_by(self, *clauses): - """Append the given ORDER BY criterion applied to this selectable. - - The criterion will be appended to any pre-existing ORDER BY criterion. - - This is an **in-place** mutation method; the - :meth:`~.GenerativeSelect.order_by` method is preferred, as it provides standard - :term:`method chaining`. - - """ - if len(clauses) == 1 and clauses[0] is None: - self._order_by_clause = ClauseList() - else: - if getattr(self, '_order_by_clause', None) is not None: - clauses = list(self._order_by_clause) + list(clauses) - self._order_by_clause = ClauseList(*clauses) - - def append_group_by(self, *clauses): - """Append the given GROUP BY criterion applied to this selectable. - - The criterion will be appended to any pre-existing GROUP BY criterion. - - This is an **in-place** mutation method; the - :meth:`~.GenerativeSelect.group_by` method is preferred, as it provides standard - :term:`method chaining`. - - """ - if len(clauses) == 1 and clauses[0] is None: - self._group_by_clause = ClauseList() - else: - if getattr(self, '_group_by_clause', None) is not None: - clauses = list(self._group_by_clause) + list(clauses) - self._group_by_clause = ClauseList(*clauses) - - -class CompoundSelect(GenerativeSelect): - """Forms the basis of ``UNION``, ``UNION ALL``, and other - SELECT-based set operations. - - - .. seealso:: - - :func:`.union` - - :func:`.union_all` - - :func:`.intersect` - - :func:`.intersect_all` - - :func:`.except` - - :func:`.except_all` - - """ - - __visit_name__ = 'compound_select' - - UNION = util.symbol('UNION') - UNION_ALL = util.symbol('UNION ALL') - EXCEPT = util.symbol('EXCEPT') - EXCEPT_ALL = util.symbol('EXCEPT ALL') - INTERSECT = util.symbol('INTERSECT') - INTERSECT_ALL = util.symbol('INTERSECT ALL') - - def __init__(self, keyword, *selects, **kwargs): - self._auto_correlate = kwargs.pop('correlate', False) - self.keyword = keyword - self.selects = [] - - numcols = None - - # some DBs do not like ORDER BY in the inner queries of a UNION, etc. - for n, s in enumerate(selects): - s = _clause_element_as_expr(s) - - if not numcols: - numcols = len(s.c) - elif len(s.c) != numcols: - raise exc.ArgumentError('All selectables passed to ' - 'CompoundSelect must have identical numbers of ' - 'columns; select #%d has %d columns, select ' - '#%d has %d' % (1, len(self.selects[0].c), n - + 1, len(s.c))) - - self.selects.append(s.self_group(self)) - - GenerativeSelect.__init__(self, **kwargs) - - @classmethod - def _create_union(cls, *selects, **kwargs): - """Return a ``UNION`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - A similar :func:`union()` method is available on all - :class:`.FromClause` subclasses. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.UNION, *selects, **kwargs) - - @classmethod - def _create_union_all(cls, *selects, **kwargs): - """Return a ``UNION ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - A similar :func:`union_all()` method is available on all - :class:`.FromClause` subclasses. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.UNION_ALL, *selects, **kwargs) - - - @classmethod - def _create_except(cls, *selects, **kwargs): - """Return an ``EXCEPT`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.EXCEPT, *selects, **kwargs) - - - @classmethod - def _create_except_all(cls, *selects, **kwargs): - """Return an ``EXCEPT ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.EXCEPT_ALL, *selects, **kwargs) - - - @classmethod - def _create_intersect(cls, *selects, **kwargs): - """Return an ``INTERSECT`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.INTERSECT, *selects, **kwargs) - - - @classmethod - def _create_intersect_all(cls, *selects, **kwargs): - """Return an ``INTERSECT ALL`` of multiple selectables. - - The returned object is an instance of - :class:`.CompoundSelect`. - - \*selects - a list of :class:`.Select` instances. - - \**kwargs - available keyword arguments are the same as those of - :func:`select`. - - """ - return CompoundSelect(CompoundSelect.INTERSECT_ALL, *selects, **kwargs) - - - def _scalar_type(self): - return self.selects[0]._scalar_type() - - def self_group(self, against=None): - return FromGrouping(self) - - def is_derived_from(self, fromclause): - for s in self.selects: - if s.is_derived_from(fromclause): - return True - return False - - def _populate_column_collection(self): - for cols in zip(*[s.c for s in self.selects]): - - # this is a slightly hacky thing - the union exports a - # column that resembles just that of the *first* selectable. - # to get at a "composite" column, particularly foreign keys, - # you have to dig through the proxies collection which we - # generate below. We may want to improve upon this, such as - # perhaps _make_proxy can accept a list of other columns - # that are "shared" - schema.column can then copy all the - # ForeignKeys in. this would allow the union() to have all - # those fks too. - - proxy = cols[0]._make_proxy(self, - name=cols[0]._label if self.use_labels else None, - key=cols[0]._key_label if self.use_labels else None) - - # hand-construct the "_proxies" collection to include all - # derived columns place a 'weight' annotation corresponding - # to how low in the list of select()s the column occurs, so - # that the corresponding_column() operation can resolve - # conflicts - - proxy._proxies = [c._annotate({'weight': i + 1}) for (i, - c) in enumerate(cols)] - - def _refresh_for_new_column(self, column): - for s in self.selects: - s._refresh_for_new_column(column) - - if not self._cols_populated: - return None - - raise NotImplementedError("CompoundSelect constructs don't support " - "addition of columns to underlying selectables") - - def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() - self.selects = [clone(s, **kw) for s in self.selects] - if hasattr(self, '_col_map'): - del self._col_map - for attr in ('_order_by_clause', '_group_by_clause', '_for_update_arg'): - if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr), **kw)) - - def get_children(self, column_collections=True, **kwargs): - return (column_collections and list(self.c) or []) \ - + [self._order_by_clause, self._group_by_clause] \ - + list(self.selects) - - def bind(self): - if self._bind: - return self._bind - for s in self.selects: - e = s.bind - if e: - return e - else: - return None - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - -class HasPrefixes(object): - _prefixes = () - - @_generative - def prefix_with(self, *expr, **kw): - """Add one or more expressions following the statement keyword, i.e. - SELECT, INSERT, UPDATE, or DELETE. Generative. - - This is used to support backend-specific prefix keywords such as those - provided by MySQL. - - E.g.:: - - stmt = table.insert().prefix_with("LOW_PRIORITY", dialect="mysql") - - Multiple prefixes can be specified by multiple calls - to :meth:`.prefix_with`. - - :param \*expr: textual or :class:`.ClauseElement` construct which - will be rendered following the INSERT, UPDATE, or DELETE - keyword. - :param \**kw: A single keyword 'dialect' is accepted. This is an - optional string dialect name which will - limit rendering of this prefix to only that dialect. - - """ - dialect = kw.pop('dialect', None) - if kw: - raise exc.ArgumentError("Unsupported argument(s): %s" % - ",".join(kw)) - self._setup_prefixes(expr, dialect) - - def _setup_prefixes(self, prefixes, dialect=None): - self._prefixes = self._prefixes + tuple( - [(_literal_as_text(p), dialect) for p in prefixes]) - - - -class Select(HasPrefixes, GenerativeSelect): - """Represents a ``SELECT`` statement. - - """ - - __visit_name__ = 'select' - - _prefixes = () - _hints = util.immutabledict() - _distinct = False - _from_cloned = None - _correlate = () - _correlate_except = None - _memoized_property = SelectBase._memoized_property - - def __init__(self, - columns=None, - whereclause=None, - from_obj=None, - distinct=False, - having=None, - correlate=True, - prefixes=None, - **kwargs): - """Construct a new :class:`.Select`. - - Similar functionality is also available via the :meth:`.FromClause.select` - method on any :class:`.FromClause`. - - All arguments which accept :class:`.ClauseElement` arguments also accept - string arguments, which will be converted as appropriate into - either :func:`text()` or :func:`literal_column()` constructs. - - .. seealso:: - - :ref:`coretutorial_selecting` - Core Tutorial description of - :func:`.select`. - - :param columns: - A list of :class:`.ClauseElement` objects, typically - :class:`.ColumnElement` objects or subclasses, which will form the - columns clause of the resulting statement. For all members which are - instances of :class:`.Selectable`, the individual :class:`.ColumnElement` - members of the :class:`.Selectable` will be added individually to the - columns clause. For example, specifying a - :class:`~sqlalchemy.schema.Table` instance will result in all the - contained :class:`~sqlalchemy.schema.Column` objects within to be added - to the columns clause. - - This argument is not present on the form of :func:`select()` - available on :class:`~sqlalchemy.schema.Table`. - - :param whereclause: - A :class:`.ClauseElement` expression which will be used to form the - ``WHERE`` clause. - - :param from_obj: - A list of :class:`.ClauseElement` objects which will be added to the - ``FROM`` clause of the resulting statement. Note that "from" objects are - automatically located within the columns and whereclause ClauseElements. - Use this parameter to explicitly specify "from" objects which are not - automatically locatable. This could include - :class:`~sqlalchemy.schema.Table` objects that aren't otherwise present, - or :class:`.Join` objects whose presence will supercede that of the - :class:`~sqlalchemy.schema.Table` objects already located in the other - clauses. - - :param autocommit: - Deprecated. Use .execution_options(autocommit=) - to set the autocommit option. - - :param bind=None: - an :class:`~.Engine` or :class:`~.Connection` instance - to which the - resulting :class:`.Select` object will be bound. The :class:`.Select` - object will otherwise automatically bind to whatever - :class:`~.base.Connectable` instances can be located within its contained - :class:`.ClauseElement` members. - - :param correlate=True: - indicates that this :class:`.Select` object should have its - contained :class:`.FromClause` elements "correlated" to an enclosing - :class:`.Select` object. This means that any :class:`.ClauseElement` - instance within the "froms" collection of this :class:`.Select` - which is also present in the "froms" collection of an - enclosing select will not be rendered in the ``FROM`` clause - of this select statement. - - :param distinct=False: - when ``True``, applies a ``DISTINCT`` qualifier to the columns - clause of the resulting statement. - - The boolean argument may also be a column expression or list - of column expressions - this is a special calling form which - is understood by the Postgresql dialect to render the - ``DISTINCT ON ()`` syntax. - - ``distinct`` is also available via the :meth:`~.Select.distinct` - generative method. - - :param for_update=False: - when ``True``, applies ``FOR UPDATE`` to the end of the - resulting statement. - - .. deprecated:: 0.9.0 - use :meth:`.GenerativeSelect.with_for_update` - to specify the structure of the ``FOR UPDATE`` clause. - - ``for_update`` accepts various string values interpreted by - specific backends, including: - - * ``"read"`` - on MySQL, translates to ``LOCK IN SHARE MODE``; - on Postgresql, translates to ``FOR SHARE``. - * ``"nowait"`` - on Postgresql and Oracle, translates to - ``FOR UPDATE NOWAIT``. - * ``"read_nowait"`` - on Postgresql, translates to - ``FOR SHARE NOWAIT``. - - .. seealso:: - - :meth:`.GenerativeSelect.with_for_update` - improved API for - specifying the ``FOR UPDATE`` clause. - - :param group_by: - a list of :class:`.ClauseElement` objects which will comprise the - ``GROUP BY`` clause of the resulting select. - - :param having: - a :class:`.ClauseElement` that will comprise the ``HAVING`` clause - of the resulting select when ``GROUP BY`` is used. - - :param limit=None: - a numerical value which usually compiles to a ``LIMIT`` - expression in the resulting select. Databases that don't - support ``LIMIT`` will attempt to provide similar - functionality. - - :param offset=None: - a numeric value which usually compiles to an ``OFFSET`` - expression in the resulting select. Databases that don't - support ``OFFSET`` will attempt to provide similar - functionality. - - :param order_by: - a scalar or list of :class:`.ClauseElement` objects which will - comprise the ``ORDER BY`` clause of the resulting select. - - :param use_labels=False: - when ``True``, the statement will be generated using labels - for each column in the columns clause, which qualify each - column with its parent table's (or aliases) name so that name - conflicts between columns in different tables don't occur. - The format of the label is _. The "c" - collection of the resulting :class:`.Select` object will use these - names as well for targeting column members. - - use_labels is also available via the :meth:`~.GenerativeSelect.apply_labels` - generative method. - - """ - self._auto_correlate = correlate - if distinct is not False: - if distinct is True: - self._distinct = True - else: - self._distinct = [ - _literal_as_text(e) - for e in util.to_list(distinct) - ] - - if from_obj is not None: - self._from_obj = util.OrderedSet( - _interpret_as_from(f) - for f in util.to_list(from_obj)) - else: - self._from_obj = util.OrderedSet() - - try: - cols_present = bool(columns) - except TypeError: - raise exc.ArgumentError("columns argument to select() must " - "be a Python list or other iterable") - - if cols_present: - self._raw_columns = [] - for c in columns: - c = _interpret_as_column_or_from(c) - if isinstance(c, ScalarSelect): - c = c.self_group(against=operators.comma_op) - self._raw_columns.append(c) - else: - self._raw_columns = [] - - if whereclause is not None: - self._whereclause = _literal_as_text(whereclause) - else: - self._whereclause = None - - if having is not None: - self._having = _literal_as_text(having) - else: - self._having = None - - if prefixes: - self._setup_prefixes(prefixes) - - GenerativeSelect.__init__(self, **kwargs) - - @property - def _froms(self): - # would love to cache this, - # but there's just enough edge cases, particularly now that - # declarative encourages construction of SQL expressions - # without tables present, to just regen this each time. - froms = [] - seen = set() - translate = self._from_cloned - - def add(items): - for item in items: - if item is self: - raise exc.InvalidRequestError( - "select() construct refers to itself as a FROM") - if translate and item in translate: - item = translate[item] - if not seen.intersection(item._cloned_set): - froms.append(item) - seen.update(item._cloned_set) - - add(_from_objects(*self._raw_columns)) - if self._whereclause is not None: - add(_from_objects(self._whereclause)) - add(self._from_obj) - - return froms - - def _get_display_froms(self, explicit_correlate_froms=None, - implicit_correlate_froms=None): - """Return the full list of 'from' clauses to be displayed. - - Takes into account a set of existing froms which may be - rendered in the FROM clause of enclosing selects; this Select - may want to leave those absent if it is automatically - correlating. - - """ - froms = self._froms - - toremove = set(itertools.chain(*[ - _expand_cloned(f._hide_froms) - for f in froms])) - if toremove: - # if we're maintaining clones of froms, - # add the copies out to the toremove list. only include - # clones that are lexical equivalents. - if self._from_cloned: - toremove.update( - self._from_cloned[f] for f in - toremove.intersection(self._from_cloned) - if self._from_cloned[f]._is_lexical_equivalent(f) - ) - # filter out to FROM clauses not in the list, - # using a list to maintain ordering - froms = [f for f in froms if f not in toremove] - - if self._correlate: - to_correlate = self._correlate - if to_correlate: - froms = [ - f for f in froms if f not in - _cloned_intersection( - _cloned_intersection(froms, explicit_correlate_froms or ()), - to_correlate - ) - ] - - if self._correlate_except is not None: - - froms = [ - f for f in froms if f not in - _cloned_difference( - _cloned_intersection(froms, explicit_correlate_froms or ()), - self._correlate_except - ) - ] - - if self._auto_correlate and \ - implicit_correlate_froms and \ - len(froms) > 1: - - froms = [ - f for f in froms if f not in - _cloned_intersection(froms, implicit_correlate_froms) - ] - - if not len(froms): - raise exc.InvalidRequestError("Select statement '%s" - "' returned no FROM clauses due to " - "auto-correlation; specify " - "correlate() to control " - "correlation manually." % self) - - return froms - - def _scalar_type(self): - elem = self._raw_columns[0] - cols = list(elem._select_iterable) - return cols[0].type - - @property - def froms(self): - """Return the displayed list of FromClause elements.""" - - return self._get_display_froms() - - @_generative - def with_hint(self, selectable, text, dialect_name='*'): - """Add an indexing hint for the given selectable to this - :class:`.Select`. - - The text of the hint is rendered in the appropriate - location for the database backend in use, relative - to the given :class:`.Table` or :class:`.Alias` passed as the - ``selectable`` argument. The dialect implementation - typically uses Python string substitution syntax - with the token ``%(name)s`` to render the name of - the table or alias. E.g. when using Oracle, the - following:: - - select([mytable]).\\ - with_hint(mytable, "+ index(%(name)s ix_mytable)") - - Would render SQL as:: - - select /*+ index(mytable ix_mytable) */ ... from mytable - - The ``dialect_name`` option will limit the rendering of a particular - hint to a particular backend. Such as, to add hints for both Oracle - and Sybase simultaneously:: - - select([mytable]).\\ - with_hint(mytable, "+ index(%(name)s ix_mytable)", 'oracle').\\ - with_hint(mytable, "WITH INDEX ix_mytable", 'sybase') - - """ - self._hints = self._hints.union( - {(selectable, dialect_name): text}) - - @property - def type(self): - raise exc.InvalidRequestError("Select objects don't have a type. " - "Call as_scalar() on this Select object " - "to return a 'scalar' version of this Select.") - - @_memoized_property.method - def locate_all_froms(self): - """return a Set of all FromClause elements referenced by this Select. - - This set is a superset of that returned by the ``froms`` property, - which is specifically for those FromClause elements that would - actually be rendered. - - """ - froms = self._froms - return froms + list(_from_objects(*froms)) - - @property - def inner_columns(self): - """an iterator of all ColumnElement expressions which would - be rendered into the columns clause of the resulting SELECT statement. - - """ - return _select_iterables(self._raw_columns) - - def is_derived_from(self, fromclause): - if self in fromclause._cloned_set: - return True - - for f in self.locate_all_froms(): - if f.is_derived_from(fromclause): - return True - return False - - def _copy_internals(self, clone=_clone, **kw): - - # Select() object has been cloned and probably adapted by the - # given clone function. Apply the cloning function to internal - # objects - - # 1. keep a dictionary of the froms we've cloned, and what - # they've become. This is consulted later when we derive - # additional froms from "whereclause" and the columns clause, - # which may still reference the uncloned parent table. - # as of 0.7.4 we also put the current version of _froms, which - # gets cleared on each generation. previously we were "baking" - # _froms into self._from_obj. - self._from_cloned = from_cloned = dict((f, clone(f, **kw)) - for f in self._from_obj.union(self._froms)) - - # 3. update persistent _from_obj with the cloned versions. - self._from_obj = util.OrderedSet(from_cloned[f] for f in - self._from_obj) - - # the _correlate collection is done separately, what can happen - # here is the same item is _correlate as in _from_obj but the - # _correlate version has an annotation on it - (specifically - # RelationshipProperty.Comparator._criterion_exists() does - # this). Also keep _correlate liberally open with it's previous - # contents, as this set is used for matching, not rendering. - self._correlate = set(clone(f) for f in - self._correlate).union(self._correlate) - - # 4. clone other things. The difficulty here is that Column - # objects are not actually cloned, and refer to their original - # .table, resulting in the wrong "from" parent after a clone - # operation. Hence _from_cloned and _from_obj supercede what is - # present here. - self._raw_columns = [clone(c, **kw) for c in self._raw_columns] - for attr in '_whereclause', '_having', '_order_by_clause', \ - '_group_by_clause', '_for_update_arg': - if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr), **kw)) - - # erase exported column list, _froms collection, - # etc. - self._reset_exported() - - def get_children(self, column_collections=True, **kwargs): - """return child elements as per the ClauseElement specification.""" - - return (column_collections and list(self.columns) or []) + \ - self._raw_columns + list(self._froms) + \ - [x for x in - (self._whereclause, self._having, - self._order_by_clause, self._group_by_clause) - if x is not None] - - @_generative - def column(self, column): - """return a new select() construct with the given column expression - added to its columns clause. - - """ - self.append_column(column) - - @util.dependencies("sqlalchemy.sql.util") - def reduce_columns(self, sqlutil, only_synonyms=True): - """Return a new :func`.select` construct with redundantly - named, equivalently-valued columns removed from the columns clause. - - "Redundant" here means two columns where one refers to the - other either based on foreign key, or via a simple equality - comparison in the WHERE clause of the statement. The primary purpose - of this method is to automatically construct a select statement - with all uniquely-named columns, without the need to use - table-qualified labels as :meth:`.apply_labels` does. - - When columns are omitted based on foreign key, the referred-to - column is the one that's kept. When columns are omitted based on - WHERE eqivalence, the first column in the columns clause is the - one that's kept. - - :param only_synonyms: when True, limit the removal of columns - to those which have the same name as the equivalent. Otherwise, - all columns that are equivalent to another are removed. - - .. versionadded:: 0.8 - - """ - return self.with_only_columns( - sqlutil.reduce_columns( - self.inner_columns, - only_synonyms=only_synonyms, - *(self._whereclause, ) + tuple(self._from_obj) - ) - ) - - @_generative - def with_only_columns(self, columns): - """Return a new :func:`.select` construct with its columns - clause replaced with the given columns. - - .. versionchanged:: 0.7.3 - Due to a bug fix, this method has a slight - behavioral change as of version 0.7.3. - Prior to version 0.7.3, the FROM clause of - a :func:`.select` was calculated upfront and as new columns - were added; in 0.7.3 and later it's calculated - at compile time, fixing an issue regarding late binding - of columns to parent tables. This changes the behavior of - :meth:`.Select.with_only_columns` in that FROM clauses no - longer represented in the new list are dropped, - but this behavior is more consistent in - that the FROM clauses are consistently derived from the - current columns clause. The original intent of this method - is to allow trimming of the existing columns list to be fewer - columns than originally present; the use case of replacing - the columns list with an entirely different one hadn't - been anticipated until 0.7.3 was released; the usage - guidelines below illustrate how this should be done. - - This method is exactly equivalent to as if the original - :func:`.select` had been called with the given columns - clause. I.e. a statement:: - - s = select([table1.c.a, table1.c.b]) - s = s.with_only_columns([table1.c.b]) - - should be exactly equivalent to:: - - s = select([table1.c.b]) - - This means that FROM clauses which are only derived - from the column list will be discarded if the new column - list no longer contains that FROM:: - - >>> table1 = table('t1', column('a'), column('b')) - >>> table2 = table('t2', column('a'), column('b')) - >>> s1 = select([table1.c.a, table2.c.b]) - >>> print s1 - SELECT t1.a, t2.b FROM t1, t2 - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print s2 - SELECT t2.b FROM t1 - - The preferred way to maintain a specific FROM clause - in the construct, assuming it won't be represented anywhere - else (i.e. not in the WHERE clause, etc.) is to set it using - :meth:`.Select.select_from`:: - - >>> s1 = select([table1.c.a, table2.c.b]).\\ - ... select_from(table1.join(table2, - ... table1.c.a==table2.c.a)) - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print s2 - SELECT t2.b FROM t1 JOIN t2 ON t1.a=t2.a - - Care should also be taken to use the correct - set of column objects passed to :meth:`.Select.with_only_columns`. - Since the method is essentially equivalent to calling the - :func:`.select` construct in the first place with the given - columns, the columns passed to :meth:`.Select.with_only_columns` - should usually be a subset of those which were passed - to the :func:`.select` construct, not those which are available - from the ``.c`` collection of that :func:`.select`. That - is:: - - s = select([table1.c.a, table1.c.b]).select_from(table1) - s = s.with_only_columns([table1.c.b]) - - and **not**:: - - # usually incorrect - s = s.with_only_columns([s.c.b]) - - The latter would produce the SQL:: - - SELECT b - FROM (SELECT t1.a AS a, t1.b AS b - FROM t1), t1 - - Since the :func:`.select` construct is essentially being - asked to select both from ``table1`` as well as itself. - - """ - self._reset_exported() - rc = [] - for c in columns: - c = _interpret_as_column_or_from(c) - if isinstance(c, ScalarSelect): - c = c.self_group(against=operators.comma_op) - rc.append(c) - self._raw_columns = rc - - @_generative - def where(self, whereclause): - """return a new select() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - - self.append_whereclause(whereclause) - - @_generative - def having(self, having): - """return a new select() construct with the given expression added to - its HAVING clause, joined to the existing clause via AND, if any. - - """ - self.append_having(having) - - @_generative - def distinct(self, *expr): - """Return a new select() construct which will apply DISTINCT to its - columns clause. - - :param \*expr: optional column expressions. When present, - the Postgresql dialect will render a ``DISTINCT ON (>)`` - construct. - - """ - if expr: - expr = [_literal_as_text(e) for e in expr] - if isinstance(self._distinct, list): - self._distinct = self._distinct + expr - else: - self._distinct = expr - else: - self._distinct = True - - @_generative - def select_from(self, fromclause): - """return a new :func:`.select` construct with the - given FROM expression - merged into its list of FROM objects. - - E.g.:: - - table1 = table('t1', column('a')) - table2 = table('t2', column('b')) - s = select([table1.c.a]).\\ - select_from( - table1.join(table2, table1.c.a==table2.c.b) - ) - - The "from" list is a unique set on the identity of each element, - so adding an already present :class:`.Table` or other selectable - will have no effect. Passing a :class:`.Join` that refers - to an already present :class:`.Table` or other selectable will have - the effect of concealing the presence of that selectable as - an individual element in the rendered FROM list, instead - rendering it into a JOIN clause. - - While the typical purpose of :meth:`.Select.select_from` is to - replace the default, derived FROM clause with a join, it can - also be called with individual table elements, multiple times - if desired, in the case that the FROM clause cannot be fully - derived from the columns clause:: - - select([func.count('*')]).select_from(table1) - - """ - self.append_from(fromclause) - - @_generative - def correlate(self, *fromclauses): - """return a new :class:`.Select` which will correlate the given FROM - clauses to that of an enclosing :class:`.Select`. - - Calling this method turns off the :class:`.Select` object's - default behavior of "auto-correlation". Normally, FROM elements - which appear in a :class:`.Select` that encloses this one via - its :term:`WHERE clause`, ORDER BY, HAVING or - :term:`columns clause` will be omitted from this :class:`.Select` - object's :term:`FROM clause`. - Setting an explicit correlation collection using the - :meth:`.Select.correlate` method provides a fixed list of FROM objects - that can potentially take place in this process. - - When :meth:`.Select.correlate` is used to apply specific FROM clauses - for correlation, the FROM elements become candidates for - correlation regardless of how deeply nested this :class:`.Select` - object is, relative to an enclosing :class:`.Select` which refers to - the same FROM object. This is in contrast to the behavior of - "auto-correlation" which only correlates to an immediate enclosing - :class:`.Select`. Multi-level correlation ensures that the link - between enclosed and enclosing :class:`.Select` is always via - at least one WHERE/ORDER BY/HAVING/columns clause in order for - correlation to take place. - - If ``None`` is passed, the :class:`.Select` object will correlate - none of its FROM entries, and all will render unconditionally - in the local FROM clause. - - :param \*fromclauses: a list of one or more :class:`.FromClause` - constructs, or other compatible constructs (i.e. ORM-mapped - classes) to become part of the correlate collection. - - .. versionchanged:: 0.8.0 ORM-mapped classes are accepted by - :meth:`.Select.correlate`. - - .. versionchanged:: 0.8.0 The :meth:`.Select.correlate` method no - longer unconditionally removes entries from the FROM clause; instead, - the candidate FROM entries must also be matched by a FROM entry - located in an enclosing :class:`.Select`, which ultimately encloses - this one as present in the WHERE clause, ORDER BY clause, HAVING - clause, or columns clause of an enclosing :meth:`.Select`. - - .. versionchanged:: 0.8.2 explicit correlation takes place - via any level of nesting of :class:`.Select` objects; in previous - 0.8 versions, correlation would only occur relative to the immediate - enclosing :class:`.Select` construct. - - .. seealso:: - - :meth:`.Select.correlate_except` - - :ref:`correlated_subqueries` - - """ - self._auto_correlate = False - if fromclauses and fromclauses[0] is None: - self._correlate = () - else: - self._correlate = set(self._correlate).union( - _interpret_as_from(f) for f in fromclauses) - - @_generative - def correlate_except(self, *fromclauses): - """return a new :class:`.Select` which will omit the given FROM - clauses from the auto-correlation process. - - Calling :meth:`.Select.correlate_except` turns off the - :class:`.Select` object's default behavior of - "auto-correlation" for the given FROM elements. An element - specified here will unconditionally appear in the FROM list, while - all other FROM elements remain subject to normal auto-correlation - behaviors. - - .. versionchanged:: 0.8.2 The :meth:`.Select.correlate_except` - method was improved to fully prevent FROM clauses specified here - from being omitted from the immediate FROM clause of this - :class:`.Select`. - - If ``None`` is passed, the :class:`.Select` object will correlate - all of its FROM entries. - - .. versionchanged:: 0.8.2 calling ``correlate_except(None)`` will - correctly auto-correlate all FROM clauses. - - :param \*fromclauses: a list of one or more :class:`.FromClause` - constructs, or other compatible constructs (i.e. ORM-mapped - classes) to become part of the correlate-exception collection. - - .. seealso:: - - :meth:`.Select.correlate` - - :ref:`correlated_subqueries` - - """ - - self._auto_correlate = False - if fromclauses and fromclauses[0] is None: - self._correlate_except = () - else: - self._correlate_except = set(self._correlate_except or ()).union( - _interpret_as_from(f) for f in fromclauses) - - def append_correlation(self, fromclause): - """append the given correlation expression to this select() - construct. - - This is an **in-place** mutation method; the - :meth:`~.Select.correlate` method is preferred, as it provides standard - :term:`method chaining`. - - """ - - self._auto_correlate = False - self._correlate = set(self._correlate).union( - _interpret_as_from(f) for f in fromclause) - - def append_column(self, column): - """append the given column expression to the columns clause of this - select() construct. - - This is an **in-place** mutation method; the - :meth:`~.Select.column` method is preferred, as it provides standard - :term:`method chaining`. - - """ - self._reset_exported() - column = _interpret_as_column_or_from(column) - - if isinstance(column, ScalarSelect): - column = column.self_group(against=operators.comma_op) - - self._raw_columns = self._raw_columns + [column] - - def append_prefix(self, clause): - """append the given columns clause prefix expression to this select() - construct. - - This is an **in-place** mutation method; the - :meth:`~.Select.prefix_with` method is preferred, as it provides standard - :term:`method chaining`. - - """ - clause = _literal_as_text(clause) - self._prefixes = self._prefixes + (clause,) - - def append_whereclause(self, whereclause): - """append the given expression to this select() construct's WHERE - criterion. - - The expression will be joined to existing WHERE criterion via AND. - - This is an **in-place** mutation method; the - :meth:`~.Select.where` method is preferred, as it provides standard - :term:`method chaining`. - - """ - - self._reset_exported() - self._whereclause = and_(True_._ifnone(self._whereclause), whereclause) - - def append_having(self, having): - """append the given expression to this select() construct's HAVING - criterion. - - The expression will be joined to existing HAVING criterion via AND. - - This is an **in-place** mutation method; the - :meth:`~.Select.having` method is preferred, as it provides standard - :term:`method chaining`. - - """ - self._reset_exported() - self._having = and_(True_._ifnone(self._having), having) - - def append_from(self, fromclause): - """append the given FromClause expression to this select() construct's - FROM clause. - - This is an **in-place** mutation method; the - :meth:`~.Select.select_from` method is preferred, as it provides standard - :term:`method chaining`. - - """ - self._reset_exported() - fromclause = _interpret_as_from(fromclause) - self._from_obj = self._from_obj.union([fromclause]) - - - @_memoized_property - def _columns_plus_names(self): - if self.use_labels: - names = set() - def name_for_col(c): - if c._label is None: - return (None, c) - name = c._label - if name in names: - name = c.anon_label - else: - names.add(name) - return name, c - - return [ - name_for_col(c) - for c in util.unique_list(_select_iterables(self._raw_columns)) - ] - else: - return [ - (None, c) - for c in util.unique_list(_select_iterables(self._raw_columns)) - ] - - def _populate_column_collection(self): - for name, c in self._columns_plus_names: - if not hasattr(c, '_make_proxy'): - continue - if name is None: - key = None - elif self.use_labels: - key = c._key_label - if key is not None and key in self.c: - key = c.anon_label - else: - key = None - - c._make_proxy(self, key=key, - name=name, - name_is_truncatable=True) - - def _refresh_for_new_column(self, column): - for fromclause in self._froms: - col = fromclause._refresh_for_new_column(column) - if col is not None: - if col in self.inner_columns and self._cols_populated: - our_label = col._key_label if self.use_labels else col.key - if our_label not in self.c: - return col._make_proxy(self, - name=col._label if self.use_labels else None, - key=col._key_label if self.use_labels else None, - name_is_truncatable=True) - return None - return None - - def self_group(self, against=None): - """return a 'grouping' construct as per the ClauseElement - specification. - - This produces an element that can be embedded in an expression. Note - that this method is called automatically as needed when constructing - expressions and should not require explicit use. - - """ - if isinstance(against, CompoundSelect): - return self - return FromGrouping(self) - - def union(self, other, **kwargs): - """return a SQL UNION of this select() construct against the given - selectable.""" - - return CompoundSelect._create_union(self, other, **kwargs) - - def union_all(self, other, **kwargs): - """return a SQL UNION ALL of this select() construct against the given - selectable. - - """ - return CompoundSelect._create_union_all(self, other, **kwargs) - - def except_(self, other, **kwargs): - """return a SQL EXCEPT of this select() construct against the given - selectable.""" - - return CompoundSelect._create_except(self, other, **kwargs) - - def except_all(self, other, **kwargs): - """return a SQL EXCEPT ALL of this select() construct against the - given selectable. - - """ - return CompoundSelect._create_except_all(self, other, **kwargs) - - def intersect(self, other, **kwargs): - """return a SQL INTERSECT of this select() construct against the given - selectable. - - """ - return CompoundSelect._create_intersect(self, other, **kwargs) - - def intersect_all(self, other, **kwargs): - """return a SQL INTERSECT ALL of this select() construct against the - given selectable. - - """ - return CompoundSelect._create_intersect_all(self, other, **kwargs) - - def bind(self): - if self._bind: - return self._bind - froms = self._froms - if not froms: - for c in self._raw_columns: - e = c.bind - if e: - self._bind = e - return e - else: - e = list(froms)[0].bind - if e: - self._bind = e - return e - - return None - - def _set_bind(self, bind): - self._bind = bind - bind = property(bind, _set_bind) - - -class ScalarSelect(Generative, Grouping): - _from_objects = [] - - def __init__(self, element): - self.element = element - self.type = element._scalar_type() - - @property - def columns(self): - raise exc.InvalidRequestError('Scalar Select expression has no ' - 'columns; use this object directly within a ' - 'column-level expression.') - c = columns - - @_generative - def where(self, crit): - """Apply a WHERE clause to the SELECT statement referred to - by this :class:`.ScalarSelect`. - - """ - self.element = self.element.where(crit) - - def self_group(self, **kwargs): - return self - - -class Exists(UnaryExpression): - """Represent an ``EXISTS`` clause. - - """ - __visit_name__ = UnaryExpression.__visit_name__ - _from_objects = [] - - - def __init__(self, *args, **kwargs): - """Construct a new :class:`.Exists` against an existing - :class:`.Select` object. - - Calling styles are of the following forms:: - - # use on an existing select() - s = select([table.c.col1]).where(table.c.col2==5) - s = exists(s) - - # construct a select() at once - exists(['*'], **select_arguments).where(criterion) - - # columns argument is optional, generates "EXISTS (SELECT *)" - # by default. - exists().where(table.c.col2==5) - - """ - if args and isinstance(args[0], (SelectBase, ScalarSelect)): - s = args[0] - else: - if not args: - args = ([literal_column('*')],) - s = Select(*args, **kwargs).as_scalar().self_group() - - UnaryExpression.__init__(self, s, operator=operators.exists, - type_=type_api.BOOLEANTYPE) - - def select(self, whereclause=None, **params): - return Select([self], whereclause, **params) - - def correlate(self, *fromclause): - e = self._clone() - e.element = self.element.correlate(*fromclause).self_group() - return e - - def correlate_except(self, *fromclause): - e = self._clone() - e.element = self.element.correlate_except(*fromclause).self_group() - return e - - def select_from(self, clause): - """return a new :class:`.Exists` construct, applying the given - expression to the :meth:`.Select.select_from` method of the select - statement contained. - - """ - e = self._clone() - e.element = self.element.select_from(clause).self_group() - return e - - def where(self, clause): - """return a new exists() construct with the given expression added to - its WHERE clause, joined to the existing clause via AND, if any. - - """ - e = self._clone() - e.element = self.element.where(clause).self_group() - return e - - -class TextAsFrom(SelectBase): - """Wrap a :class:`.TextClause` construct within a :class:`.SelectBase` - interface. - - This allows the :class:`.TextClause` object to gain a ``.c`` collection and - other FROM-like capabilities such as :meth:`.FromClause.alias`, - :meth:`.SelectBase.cte`, etc. - - The :class:`.TextAsFrom` construct is produced via the - :meth:`.TextClause.columns` method - see that method for details. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :func:`.text` - - :meth:`.TextClause.columns` - - """ - __visit_name__ = "text_as_from" - - def __init__(self, text, columns): - self.element = text - self.column_args = columns - - @property - def _bind(self): - return self.element._bind - - @_generative - def bindparams(self, *binds, **bind_as_values): - self.element = self.element.bindparams(*binds, **bind_as_values) - - def _populate_column_collection(self): - for c in self.column_args: - c._make_proxy(self) - - def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() - self.element = clone(self.element, **kw) - - def _scalar_type(self): - return self.column_args[0].type - -class AnnotatedFromClause(Annotated): - def __init__(self, element, values): - # force FromClause to generate their internal - # collections into __dict__ - element.c - Annotated.__init__(self, element, values) - - diff --git a/libs/sqlalchemy/sql/sqltypes.py b/libs/sqlalchemy/sql/sqltypes.py deleted file mode 100644 index 702e7736..00000000 --- a/libs/sqlalchemy/sql/sqltypes.py +++ /dev/null @@ -1,1628 +0,0 @@ -# sql/sqltypes.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""SQL specific types. - -""" - -import datetime as dt -import codecs - -from .type_api import TypeEngine, TypeDecorator, to_instance -from .elements import quoted_name, type_coerce -from .default_comparator import _DefaultColumnComparator -from .. import exc, util, processors -from .base import _bind_or_error, SchemaEventTarget -from . import operators -from .. import event -from ..util import pickle -import decimal - -if util.jython: - import array - -class _DateAffinity(object): - """Mixin date/time specific expression adaptations. - - Rules are implemented within Date,Time,Interval,DateTime, Numeric, - Integer. Based on http://www.postgresql.org/docs/current/static - /functions-datetime.html. - - """ - - @property - def _expression_adaptations(self): - raise NotImplementedError() - - class Comparator(TypeEngine.Comparator): - _blank_dict = util.immutabledict() - - def _adapt_expression(self, op, other_comparator): - othertype = other_comparator.type._type_affinity - return op, \ - to_instance(self.type._expression_adaptations.get(op, self._blank_dict).\ - get(othertype, NULLTYPE)) - comparator_factory = Comparator - -class Concatenable(object): - """A mixin that marks a type as supporting 'concatenation', - typically strings.""" - - class Comparator(TypeEngine.Comparator): - def _adapt_expression(self, op, other_comparator): - if op is operators.add and isinstance(other_comparator, - (Concatenable.Comparator, NullType.Comparator)): - return operators.concat_op, self.expr.type - else: - return op, self.expr.type - - comparator_factory = Comparator - - -class String(Concatenable, TypeEngine): - """The base for all string and character types. - - In SQL, corresponds to VARCHAR. Can also take Python unicode objects - and encode to the database's encoding in bind params (and the reverse for - result sets.) - - The `length` field is usually required when the `String` type is - used within a CREATE TABLE statement, as VARCHAR requires a length - on most databases. - - """ - - __visit_name__ = 'string' - - def __init__(self, length=None, collation=None, - convert_unicode=False, - unicode_error=None, - _warn_on_bytestring=False - ): - """ - Create a string-holding type. - - :param length: optional, a length for the column for use in - DDL and CAST expressions. May be safely omitted if no ``CREATE - TABLE`` will be issued. Certain databases may require a - ``length`` for use in DDL, and will raise an exception when - the ``CREATE TABLE`` DDL is issued if a ``VARCHAR`` - with no length is included. Whether the value is - interpreted as bytes or characters is database specific. - - :param collation: Optional, a column-level collation for - use in DDL and CAST expressions. Renders using the - COLLATE keyword supported by SQLite, MySQL, and Postgresql. - E.g.:: - - >>> from sqlalchemy import cast, select, String - >>> print select([cast('some string', String(collation='utf8'))]) - SELECT CAST(:param_1 AS VARCHAR COLLATE utf8) AS anon_1 - - .. versionadded:: 0.8 Added support for COLLATE to all - string types. - - :param convert_unicode: When set to ``True``, the - :class:`.String` type will assume that - input is to be passed as Python ``unicode`` objects, - and results returned as Python ``unicode`` objects. - If the DBAPI in use does not support Python unicode - (which is fewer and fewer these days), SQLAlchemy - will encode/decode the value, using the - value of the ``encoding`` parameter passed to - :func:`.create_engine` as the encoding. - - When using a DBAPI that natively supports Python - unicode objects, this flag generally does not - need to be set. For columns that are explicitly - intended to store non-ASCII data, the :class:`.Unicode` - or :class:`.UnicodeText` - types should be used regardless, which feature - the same behavior of ``convert_unicode`` but - also indicate an underlying column type that - directly supports unicode, such as ``NVARCHAR``. - - For the extremely rare case that Python ``unicode`` - is to be encoded/decoded by SQLAlchemy on a backend - that does natively support Python ``unicode``, - the value ``force`` can be passed here which will - cause SQLAlchemy's encode/decode services to be - used unconditionally. - - :param unicode_error: Optional, a method to use to handle Unicode - conversion errors. Behaves like the ``errors`` keyword argument to - the standard library's ``string.decode()`` functions. This flag - requires that ``convert_unicode`` is set to ``force`` - otherwise, - SQLAlchemy is not guaranteed to handle the task of unicode - conversion. Note that this flag adds significant performance - overhead to row-fetching operations for backends that already - return unicode objects natively (which most DBAPIs do). This - flag should only be used as a last resort for reading - strings from a column with varied or corrupted encodings. - - """ - if unicode_error is not None and convert_unicode != 'force': - raise exc.ArgumentError("convert_unicode must be 'force' " - "when unicode_error is set.") - - self.length = length - self.collation = collation - self.convert_unicode = convert_unicode - self.unicode_error = unicode_error - self._warn_on_bytestring = _warn_on_bytestring - - def literal_processor(self, dialect): - def process(value): - value = value.replace("'", "''") - return "'%s'" % value - return process - - def bind_processor(self, dialect): - if self.convert_unicode or dialect.convert_unicode: - if dialect.supports_unicode_binds and \ - self.convert_unicode != 'force': - if self._warn_on_bytestring: - def process(value): - if isinstance(value, util.binary_type): - util.warn("Unicode type received non-unicode bind " - "param value.") - return value - return process - else: - return None - else: - encoder = codecs.getencoder(dialect.encoding) - warn_on_bytestring = self._warn_on_bytestring - - def process(value): - if isinstance(value, util.text_type): - return encoder(value, self.unicode_error)[0] - elif warn_on_bytestring and value is not None: - util.warn("Unicode type received non-unicode bind " - "param value") - return value - return process - else: - return None - - def result_processor(self, dialect, coltype): - wants_unicode = self.convert_unicode or dialect.convert_unicode - needs_convert = wants_unicode and \ - (dialect.returns_unicode_strings is not True or - self.convert_unicode in ('force', 'force_nocheck')) - needs_isinstance = ( - needs_convert and - dialect.returns_unicode_strings and - self.convert_unicode != 'force_nocheck' - ) - - if needs_convert: - to_unicode = processors.to_unicode_processor_factory( - dialect.encoding, self.unicode_error) - - if needs_isinstance: - # we wouldn't be here unless convert_unicode='force' - # was specified, or the driver has erratic unicode-returning - # habits. since we will be getting back unicode - # in most cases, we check for it (decode will fail). - def process(value): - if isinstance(value, util.text_type): - return value - else: - return to_unicode(value) - return process - else: - # here, we assume that the object is not unicode, - # avoiding expensive isinstance() check. - return to_unicode - else: - return None - - @property - def python_type(self): - if self.convert_unicode: - return util.text_type - else: - return str - - def get_dbapi_type(self, dbapi): - return dbapi.STRING - - -class Text(String): - """A variably sized string type. - - In SQL, usually corresponds to CLOB or TEXT. Can also take Python - unicode objects and encode to the database's encoding in bind - params (and the reverse for result sets.) In general, TEXT objects - do not have a length; while some databases will accept a length - argument here, it will be rejected by others. - - """ - __visit_name__ = 'text' - - -class Unicode(String): - """A variable length Unicode string type. - - The :class:`.Unicode` type is a :class:`.String` subclass - that assumes input and output as Python ``unicode`` data, - and in that regard is equivalent to the usage of the - ``convert_unicode`` flag with the :class:`.String` type. - However, unlike plain :class:`.String`, it also implies an - underlying column type that is explicitly supporting of non-ASCII - data, such as ``NVARCHAR`` on Oracle and SQL Server. - This can impact the output of ``CREATE TABLE`` statements - and ``CAST`` functions at the dialect level, and can - also affect the handling of bound parameters in some - specific DBAPI scenarios. - - The encoding used by the :class:`.Unicode` type is usually - determined by the DBAPI itself; most modern DBAPIs - feature support for Python ``unicode`` objects as bound - values and result set values, and the encoding should - be configured as detailed in the notes for the target - DBAPI in the :ref:`dialect_toplevel` section. - - For those DBAPIs which do not support, or are not configured - to accommodate Python ``unicode`` objects - directly, SQLAlchemy does the encoding and decoding - outside of the DBAPI. The encoding in this scenario - is determined by the ``encoding`` flag passed to - :func:`.create_engine`. - - When using the :class:`.Unicode` type, it is only appropriate - to pass Python ``unicode`` objects, and not plain ``str``. - If a plain ``str`` is passed under Python 2, a warning - is emitted. If you notice your application emitting these warnings but - you're not sure of the source of them, the Python - ``warnings`` filter, documented at - http://docs.python.org/library/warnings.html, - can be used to turn these warnings into exceptions - which will illustrate a stack trace:: - - import warnings - warnings.simplefilter('error') - - For an application that wishes to pass plain bytestrings - and Python ``unicode`` objects to the ``Unicode`` type - equally, the bytestrings must first be decoded into - unicode. The recipe at :ref:`coerce_to_unicode` illustrates - how this is done. - - See also: - - :class:`.UnicodeText` - unlengthed textual counterpart - to :class:`.Unicode`. - - """ - - __visit_name__ = 'unicode' - - def __init__(self, length=None, **kwargs): - """ - Create a :class:`.Unicode` object. - - Parameters are the same as that of :class:`.String`, - with the exception that ``convert_unicode`` - defaults to ``True``. - - """ - kwargs.setdefault('convert_unicode', True) - kwargs.setdefault('_warn_on_bytestring', True) - super(Unicode, self).__init__(length=length, **kwargs) - - -class UnicodeText(Text): - """An unbounded-length Unicode string type. - - See :class:`.Unicode` for details on the unicode - behavior of this object. - - Like :class:`.Unicode`, usage the :class:`.UnicodeText` type implies a - unicode-capable type being used on the backend, such as - ``NCLOB``, ``NTEXT``. - - """ - - __visit_name__ = 'unicode_text' - - def __init__(self, length=None, **kwargs): - """ - Create a Unicode-converting Text type. - - Parameters are the same as that of :class:`.Text`, - with the exception that ``convert_unicode`` - defaults to ``True``. - - """ - kwargs.setdefault('convert_unicode', True) - kwargs.setdefault('_warn_on_bytestring', True) - super(UnicodeText, self).__init__(length=length, **kwargs) - - -class Integer(_DateAffinity, TypeEngine): - """A type for ``int`` integers.""" - - __visit_name__ = 'integer' - - def get_dbapi_type(self, dbapi): - return dbapi.NUMBER - - @property - def python_type(self): - return int - - def literal_processor(self, dialect): - def process(value): - return str(value) - return process - - @util.memoized_property - def _expression_adaptations(self): - # TODO: need a dictionary object that will - # handle operators generically here, this is incomplete - return { - operators.add: { - Date: Date, - Integer: self.__class__, - Numeric: Numeric, - }, - operators.mul: { - Interval: Interval, - Integer: self.__class__, - Numeric: Numeric, - }, - operators.div: { - Integer: self.__class__, - Numeric: Numeric, - }, - operators.truediv: { - Integer: self.__class__, - Numeric: Numeric, - }, - operators.sub: { - Integer: self.__class__, - Numeric: Numeric, - }, - } - - - -class SmallInteger(Integer): - """A type for smaller ``int`` integers. - - Typically generates a ``SMALLINT`` in DDL, and otherwise acts like - a normal :class:`.Integer` on the Python side. - - """ - - __visit_name__ = 'small_integer' - - -class BigInteger(Integer): - """A type for bigger ``int`` integers. - - Typically generates a ``BIGINT`` in DDL, and otherwise acts like - a normal :class:`.Integer` on the Python side. - - """ - - __visit_name__ = 'big_integer' - - - -class Numeric(_DateAffinity, TypeEngine): - """A type for fixed precision numbers. - - Typically generates DECIMAL or NUMERIC. Returns - ``decimal.Decimal`` objects by default, applying - conversion as needed. - - .. note:: - - The `cdecimal `_ library - is a high performing alternative to Python's built-in - ``decimal.Decimal`` type, which performs very poorly in high volume - situations. SQLAlchemy 0.7 is tested against ``cdecimal`` and supports - it fully. The type is not necessarily supported by DBAPI - implementations however, most of which contain an import for plain - ``decimal`` in their source code, even though some such as psycopg2 - provide hooks for alternate adapters. SQLAlchemy imports ``decimal`` - globally as well. The most straightforward and - foolproof way to use "cdecimal" given current DBAPI and Python support - is to patch it directly into sys.modules before anything else is - imported:: - - import sys - import cdecimal - sys.modules["decimal"] = cdecimal - - While the global patch is a little ugly, it's particularly - important to use just one decimal library at a time since - Python Decimal and cdecimal Decimal objects - are not currently compatible *with each other*:: - - >>> import cdecimal - >>> import decimal - >>> decimal.Decimal("10") == cdecimal.Decimal("10") - False - - SQLAlchemy will provide more natural support of - cdecimal if and when it becomes a standard part of Python - installations and is supported by all DBAPIs. - - """ - - __visit_name__ = 'numeric' - - _default_decimal_return_scale = 10 - - def __init__(self, precision=None, scale=None, - decimal_return_scale=None, asdecimal=True): - """ - Construct a Numeric. - - :param precision: the numeric precision for use in DDL ``CREATE - TABLE``. - - :param scale: the numeric scale for use in DDL ``CREATE TABLE``. - - :param asdecimal: default True. Return whether or not - values should be sent as Python Decimal objects, or - as floats. Different DBAPIs send one or the other based on - datatypes - the Numeric type will ensure that return values - are one or the other across DBAPIs consistently. - - :param decimal_return_scale: Default scale to use when converting - from floats to Python decimals. Floating point values will typically - be much longer due to decimal inaccuracy, and most floating point - database types don't have a notion of "scale", so by default the - float type looks for the first ten decimal places when converting. - Specfiying this value will override that length. Types which - do include an explicit ".scale" value, such as the base :class:`.Numeric` - as well as the MySQL float types, will use the value of ".scale" - as the default for decimal_return_scale, if not otherwise specified. - - .. versionadded:: 0.9.0 - - When using the ``Numeric`` type, care should be taken to ensure - that the asdecimal setting is apppropriate for the DBAPI in use - - when Numeric applies a conversion from Decimal->float or float-> - Decimal, this conversion incurs an additional performance overhead - for all result columns received. - - DBAPIs that return Decimal natively (e.g. psycopg2) will have - better accuracy and higher performance with a setting of ``True``, - as the native translation to Decimal reduces the amount of floating- - point issues at play, and the Numeric type itself doesn't need - to apply any further conversions. However, another DBAPI which - returns floats natively *will* incur an additional conversion - overhead, and is still subject to floating point data loss - in - which case ``asdecimal=False`` will at least remove the extra - conversion overhead. - - """ - self.precision = precision - self.scale = scale - self.decimal_return_scale = decimal_return_scale - self.asdecimal = asdecimal - - @property - def _effective_decimal_return_scale(self): - if self.decimal_return_scale is not None: - return self.decimal_return_scale - elif getattr(self, "scale", None) is not None: - return self.scale - else: - return self._default_decimal_return_scale - - def get_dbapi_type(self, dbapi): - return dbapi.NUMBER - - def literal_processor(self, dialect): - def process(value): - return str(value) - return process - - @property - def python_type(self): - if self.asdecimal: - return decimal.Decimal - else: - return float - - def bind_processor(self, dialect): - if dialect.supports_native_decimal: - return None - else: - return processors.to_float - - def result_processor(self, dialect, coltype): - if self.asdecimal: - if dialect.supports_native_decimal: - # we're a "numeric", DBAPI will give us Decimal directly - return None - else: - util.warn('Dialect %s+%s does *not* support Decimal ' - 'objects natively, and SQLAlchemy must ' - 'convert from floating point - rounding ' - 'errors and other issues may occur. Please ' - 'consider storing Decimal numbers as strings ' - 'or integers on this platform for lossless ' - 'storage.' % (dialect.name, dialect.driver)) - - # we're a "numeric", DBAPI returns floats, convert. - return processors.to_decimal_processor_factory( - decimal.Decimal, - self.scale if self.scale is not None - else self._default_decimal_return_scale) - else: - if dialect.supports_native_decimal: - return processors.to_float - else: - return None - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.mul: { - Interval: Interval, - Numeric: self.__class__, - Integer: self.__class__, - }, - operators.div: { - Numeric: self.__class__, - Integer: self.__class__, - }, - operators.truediv: { - Numeric: self.__class__, - Integer: self.__class__, - }, - operators.add: { - Numeric: self.__class__, - Integer: self.__class__, - }, - operators.sub: { - Numeric: self.__class__, - Integer: self.__class__, - } - } - - -class Float(Numeric): - """A type for ``float`` numbers. - - Returns Python ``float`` objects by default, applying - conversion as needed. - - """ - - __visit_name__ = 'float' - - scale = None - - def __init__(self, precision=None, asdecimal=False, - decimal_return_scale=None, **kwargs): - """ - Construct a Float. - - :param precision: the numeric precision for use in DDL ``CREATE - TABLE``. - - :param asdecimal: the same flag as that of :class:`.Numeric`, but - defaults to ``False``. Note that setting this flag to ``True`` - results in floating point conversion. - - :param decimal_return_scale: Default scale to use when converting - from floats to Python decimals. Floating point values will typically - be much longer due to decimal inaccuracy, and most floating point - database types don't have a notion of "scale", so by default the - float type looks for the first ten decimal places when converting. - Specfiying this value will override that length. Note that the - MySQL float types, which do include "scale", will use "scale" - as the default for decimal_return_scale, if not otherwise specified. - - .. versionadded:: 0.9.0 - - :param \**kwargs: deprecated. Additional arguments here are ignored - by the default :class:`.Float` type. For database specific - floats that support additional arguments, see that dialect's - documentation for details, such as - :class:`sqlalchemy.dialects.mysql.FLOAT`. - - """ - self.precision = precision - self.asdecimal = asdecimal - self.decimal_return_scale = decimal_return_scale - if kwargs: - util.warn_deprecated("Additional keyword arguments " - "passed to Float ignored.") - - def result_processor(self, dialect, coltype): - if self.asdecimal: - return processors.to_decimal_processor_factory( - decimal.Decimal, - self._effective_decimal_return_scale) - else: - return None - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.mul: { - Interval: Interval, - Numeric: self.__class__, - }, - operators.div: { - Numeric: self.__class__, - }, - operators.truediv: { - Numeric: self.__class__, - }, - operators.add: { - Numeric: self.__class__, - }, - operators.sub: { - Numeric: self.__class__, - } - } - - -class DateTime(_DateAffinity, TypeEngine): - """A type for ``datetime.datetime()`` objects. - - Date and time types return objects from the Python ``datetime`` - module. Most DBAPIs have built in support for the datetime - module, with the noted exception of SQLite. In the case of - SQLite, date and time types are stored as strings which are then - converted back to datetime objects when rows are returned. - - """ - - __visit_name__ = 'datetime' - - def __init__(self, timezone=False): - """Construct a new :class:`.DateTime`. - - :param timezone: boolean. If True, and supported by the - backend, will produce 'TIMESTAMP WITH TIMEZONE'. For backends - that don't support timezone aware timestamps, has no - effect. - - """ - self.timezone = timezone - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.datetime - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add: { - Interval: self.__class__, - }, - operators.sub: { - Interval: self.__class__, - DateTime: Interval, - }, - } - - -class Date(_DateAffinity, TypeEngine): - """A type for ``datetime.date()`` objects.""" - - __visit_name__ = 'date' - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.date - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add: { - Integer: self.__class__, - Interval: DateTime, - Time: DateTime, - }, - operators.sub: { - # date - integer = date - Integer: self.__class__, - - # date - date = integer. - Date: Integer, - - Interval: DateTime, - - # date - datetime = interval, - # this one is not in the PG docs - # but works - DateTime: Interval, - }, - } - - -class Time(_DateAffinity, TypeEngine): - """A type for ``datetime.time()`` objects.""" - - __visit_name__ = 'time' - - def __init__(self, timezone=False): - self.timezone = timezone - - def get_dbapi_type(self, dbapi): - return dbapi.DATETIME - - @property - def python_type(self): - return dt.time - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add: { - Date: DateTime, - Interval: self.__class__ - }, - operators.sub: { - Time: Interval, - Interval: self.__class__, - }, - } - - -class _Binary(TypeEngine): - """Define base behavior for binary types.""" - - def __init__(self, length=None): - self.length = length - - def literal_processor(self, dialect): - def process(value): - value = value.decode(self.dialect.encoding).replace("'", "''") - return "'%s'" % value - return process - - @property - def python_type(self): - return util.binary_type - - # Python 3 - sqlite3 doesn't need the `Binary` conversion - # here, though pg8000 does to indicate "bytea" - def bind_processor(self, dialect): - DBAPIBinary = dialect.dbapi.Binary - - def process(value): - if value is not None: - return DBAPIBinary(value) - else: - return None - return process - - # Python 3 has native bytes() type - # both sqlite3 and pg8000 seem to return it, - # psycopg2 as of 2.5 returns 'memoryview' - if util.py2k: - def result_processor(self, dialect, coltype): - if util.jython: - def process(value): - if value is not None: - if isinstance(value, array.array): - return value.tostring() - return str(value) - else: - return None - else: - process = processors.to_str - return process - else: - def result_processor(self, dialect, coltype): - def process(value): - if value is not None: - value = bytes(value) - return value - return process - - def coerce_compared_value(self, op, value): - """See :meth:`.TypeEngine.coerce_compared_value` for a description.""" - - if isinstance(value, util.string_types): - return self - else: - return super(_Binary, self).coerce_compared_value(op, value) - - def get_dbapi_type(self, dbapi): - return dbapi.BINARY - - -class LargeBinary(_Binary): - """A type for large binary byte data. - - The Binary type generates BLOB or BYTEA when tables are created, - and also converts incoming values using the ``Binary`` callable - provided by each DB-API. - - """ - - __visit_name__ = 'large_binary' - - def __init__(self, length=None): - """ - Construct a LargeBinary type. - - :param length: optional, a length for the column for use in - DDL statements, for those BLOB types that accept a length - (i.e. MySQL). It does *not* produce a small BINARY/VARBINARY - type - use the BINARY/VARBINARY types specifically for those. - May be safely omitted if no ``CREATE - TABLE`` will be issued. Certain databases may require a - *length* for use in DDL, and will raise an exception when - the ``CREATE TABLE`` DDL is issued. - - """ - _Binary.__init__(self, length=length) - - -class Binary(LargeBinary): - """Deprecated. Renamed to LargeBinary.""" - - def __init__(self, *arg, **kw): - util.warn_deprecated('The Binary type has been renamed to ' - 'LargeBinary.') - LargeBinary.__init__(self, *arg, **kw) - - - -class SchemaType(SchemaEventTarget): - """Mark a type as possibly requiring schema-level DDL for usage. - - Supports types that must be explicitly created/dropped (i.e. PG ENUM type) - as well as types that are complimented by table or schema level - constraints, triggers, and other rules. - - :class:`.SchemaType` classes can also be targets for the - :meth:`.DDLEvents.before_parent_attach` and - :meth:`.DDLEvents.after_parent_attach` events, where the events fire off - surrounding the association of the type object with a parent - :class:`.Column`. - - .. seealso:: - - :class:`.Enum` - - :class:`.Boolean` - - - """ - - def __init__(self, name=None, schema=None, metadata=None, - inherit_schema=False, quote=None): - if name is not None: - self.name = quoted_name(name, quote) - else: - self.name = None - self.schema = schema - self.metadata = metadata - self.inherit_schema = inherit_schema - if self.metadata: - event.listen( - self.metadata, - "before_create", - util.portable_instancemethod(self._on_metadata_create) - ) - event.listen( - self.metadata, - "after_drop", - util.portable_instancemethod(self._on_metadata_drop) - ) - - def _set_parent(self, column): - column._on_table_attach(util.portable_instancemethod(self._set_table)) - - def _set_table(self, column, table): - if self.inherit_schema: - self.schema = table.schema - - event.listen( - table, - "before_create", - util.portable_instancemethod( - self._on_table_create) - ) - event.listen( - table, - "after_drop", - util.portable_instancemethod(self._on_table_drop) - ) - if self.metadata is None: - # TODO: what's the difference between self.metadata - # and table.metadata here ? - event.listen( - table.metadata, - "before_create", - util.portable_instancemethod(self._on_metadata_create) - ) - event.listen( - table.metadata, - "after_drop", - util.portable_instancemethod(self._on_metadata_drop) - ) - - def copy(self, **kw): - return self.adapt(self.__class__) - - def adapt(self, impltype, **kw): - schema = kw.pop('schema', self.schema) - metadata = kw.pop('metadata', self.metadata) - return impltype(name=self.name, - schema=schema, - metadata=metadata, - inherit_schema=self.inherit_schema, - **kw - ) - - @property - def bind(self): - return self.metadata and self.metadata.bind or None - - def create(self, bind=None, checkfirst=False): - """Issue CREATE ddl for this type, if applicable.""" - - if bind is None: - bind = _bind_or_error(self) - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t.create(bind=bind, checkfirst=checkfirst) - - def drop(self, bind=None, checkfirst=False): - """Issue DROP ddl for this type, if applicable.""" - - if bind is None: - bind = _bind_or_error(self) - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t.drop(bind=bind, checkfirst=checkfirst) - - def _on_table_create(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_table_create(target, bind, **kw) - - def _on_table_drop(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_table_drop(target, bind, **kw) - - def _on_metadata_create(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_metadata_create(target, bind, **kw) - - def _on_metadata_drop(self, target, bind, **kw): - t = self.dialect_impl(bind.dialect) - if t.__class__ is not self.__class__ and isinstance(t, SchemaType): - t._on_metadata_drop(target, bind, **kw) - -class Enum(String, SchemaType): - """Generic Enum Type. - - The Enum type provides a set of possible string values which the - column is constrained towards. - - By default, uses the backend's native ENUM type if available, - else uses VARCHAR + a CHECK constraint. - - .. seealso:: - - :class:`~.postgresql.ENUM` - PostgreSQL-specific type, - which has additional functionality. - - """ - - __visit_name__ = 'enum' - - def __init__(self, *enums, **kw): - """Construct an enum. - - Keyword arguments which don't apply to a specific backend are ignored - by that backend. - - :param \*enums: string or unicode enumeration labels. If unicode - labels are present, the `convert_unicode` flag is auto-enabled. - - :param convert_unicode: Enable unicode-aware bind parameter and - result-set processing for this Enum's data. This is set - automatically based on the presence of unicode label strings. - - :param metadata: Associate this type directly with a ``MetaData`` - object. For types that exist on the target database as an - independent schema construct (Postgresql), this type will be - created and dropped within ``create_all()`` and ``drop_all()`` - operations. If the type is not associated with any ``MetaData`` - object, it will associate itself with each ``Table`` in which it is - used, and will be created when any of those individual tables are - created, after a check is performed for it's existence. The type is - only dropped when ``drop_all()`` is called for that ``Table`` - object's metadata, however. - - :param name: The name of this type. This is required for Postgresql - and any future supported database which requires an explicitly - named type, or an explicitly named constraint in order to generate - the type and/or a table that uses it. - - :param native_enum: Use the database's native ENUM type when - available. Defaults to True. When False, uses VARCHAR + check - constraint for all backends. - - :param schema: Schema name of this type. For types that exist on the - target database as an independent schema construct (Postgresql), - this parameter specifies the named schema in which the type is - present. - - .. note:: - - The ``schema`` of the :class:`.Enum` type does not - by default make use of the ``schema`` established on the - owning :class:`.Table`. If this behavior is desired, - set the ``inherit_schema`` flag to ``True``. - - :param quote: Set explicit quoting preferences for the type's name. - - :param inherit_schema: When ``True``, the "schema" from the owning - :class:`.Table` will be copied to the "schema" attribute of this - :class:`.Enum`, replacing whatever value was passed for the - ``schema`` attribute. This also takes effect when using the - :meth:`.Table.tometadata` operation. - - .. versionadded:: 0.8 - - """ - self.enums = enums - self.native_enum = kw.pop('native_enum', True) - convert_unicode = kw.pop('convert_unicode', None) - if convert_unicode is None: - for e in enums: - if isinstance(e, util.text_type): - convert_unicode = True - break - else: - convert_unicode = False - - if self.enums: - length = max(len(x) for x in self.enums) - else: - length = 0 - String.__init__(self, - length=length, - convert_unicode=convert_unicode, - ) - SchemaType.__init__(self, **kw) - - def __repr__(self): - return util.generic_repr(self, - to_inspect=[Enum, SchemaType], - ) - - def _should_create_constraint(self, compiler): - return not self.native_enum or \ - not compiler.dialect.supports_native_enum - - @util.dependencies("sqlalchemy.sql.schema") - def _set_table(self, schema, column, table): - if self.native_enum: - SchemaType._set_table(self, column, table) - - e = schema.CheckConstraint( - type_coerce(column, self).in_(self.enums), - name=self.name, - _create_rule=util.portable_instancemethod( - self._should_create_constraint) - ) - table.append_constraint(e) - - def adapt(self, impltype, **kw): - schema = kw.pop('schema', self.schema) - metadata = kw.pop('metadata', self.metadata) - if issubclass(impltype, Enum): - return impltype(name=self.name, - schema=schema, - metadata=metadata, - convert_unicode=self.convert_unicode, - native_enum=self.native_enum, - inherit_schema=self.inherit_schema, - *self.enums, - **kw - ) - else: - return super(Enum, self).adapt(impltype, **kw) - - -class PickleType(TypeDecorator): - """Holds Python objects, which are serialized using pickle. - - PickleType builds upon the Binary type to apply Python's - ``pickle.dumps()`` to incoming objects, and ``pickle.loads()`` on - the way out, allowing any pickleable Python object to be stored as - a serialized binary field. - - To allow ORM change events to propagate for elements associated - with :class:`.PickleType`, see :ref:`mutable_toplevel`. - - """ - - impl = LargeBinary - - def __init__(self, protocol=pickle.HIGHEST_PROTOCOL, - pickler=None, comparator=None): - """ - Construct a PickleType. - - :param protocol: defaults to ``pickle.HIGHEST_PROTOCOL``. - - :param pickler: defaults to cPickle.pickle or pickle.pickle if - cPickle is not available. May be any object with - pickle-compatible ``dumps` and ``loads`` methods. - - :param comparator: a 2-arg callable predicate used - to compare values of this type. If left as ``None``, - the Python "equals" operator is used to compare values. - - """ - self.protocol = protocol - self.pickler = pickler or pickle - self.comparator = comparator - super(PickleType, self).__init__() - - def __reduce__(self): - return PickleType, (self.protocol, - None, - self.comparator) - - def bind_processor(self, dialect): - impl_processor = self.impl.bind_processor(dialect) - dumps = self.pickler.dumps - protocol = self.protocol - if impl_processor: - def process(value): - if value is not None: - value = dumps(value, protocol) - return impl_processor(value) - else: - def process(value): - if value is not None: - value = dumps(value, protocol) - return value - return process - - def result_processor(self, dialect, coltype): - impl_processor = self.impl.result_processor(dialect, coltype) - loads = self.pickler.loads - if impl_processor: - def process(value): - value = impl_processor(value) - if value is None: - return None - return loads(value) - else: - def process(value): - if value is None: - return None - return loads(value) - return process - - def compare_values(self, x, y): - if self.comparator: - return self.comparator(x, y) - else: - return x == y - - -class Boolean(TypeEngine, SchemaType): - """A bool datatype. - - Boolean typically uses BOOLEAN or SMALLINT on the DDL side, and on - the Python side deals in ``True`` or ``False``. - - """ - - __visit_name__ = 'boolean' - - def __init__(self, create_constraint=True, name=None): - """Construct a Boolean. - - :param create_constraint: defaults to True. If the boolean - is generated as an int/smallint, also create a CHECK constraint - on the table that ensures 1 or 0 as a value. - - :param name: if a CHECK constraint is generated, specify - the name of the constraint. - - """ - self.create_constraint = create_constraint - self.name = name - - def _should_create_constraint(self, compiler): - return not compiler.dialect.supports_native_boolean - - @util.dependencies("sqlalchemy.sql.schema") - def _set_table(self, schema, column, table): - if not self.create_constraint: - return - - e = schema.CheckConstraint( - type_coerce(column, self).in_([0, 1]), - name=self.name, - _create_rule=util.portable_instancemethod( - self._should_create_constraint) - ) - table.append_constraint(e) - - @property - def python_type(self): - return bool - - def bind_processor(self, dialect): - if dialect.supports_native_boolean: - return None - else: - return processors.boolean_to_int - - def result_processor(self, dialect, coltype): - if dialect.supports_native_boolean: - return None - else: - return processors.int_to_boolean - - -class Interval(_DateAffinity, TypeDecorator): - """A type for ``datetime.timedelta()`` objects. - - The Interval type deals with ``datetime.timedelta`` objects. In - PostgreSQL, the native ``INTERVAL`` type is used; for others, the - value is stored as a date which is relative to the "epoch" - (Jan. 1, 1970). - - Note that the ``Interval`` type does not currently provide date arithmetic - operations on platforms which do not support interval types natively. Such - operations usually require transformation of both sides of the expression - (such as, conversion of both sides into integer epoch values first) which - currently is a manual procedure (such as via - :attr:`~sqlalchemy.sql.expression.func`). - - """ - - impl = DateTime - epoch = dt.datetime.utcfromtimestamp(0) - - def __init__(self, native=True, - second_precision=None, - day_precision=None): - """Construct an Interval object. - - :param native: when True, use the actual - INTERVAL type provided by the database, if - supported (currently Postgresql, Oracle). - Otherwise, represent the interval data as - an epoch value regardless. - - :param second_precision: For native interval types - which support a "fractional seconds precision" parameter, - i.e. Oracle and Postgresql - - :param day_precision: for native interval types which - support a "day precision" parameter, i.e. Oracle. - - """ - super(Interval, self).__init__() - self.native = native - self.second_precision = second_precision - self.day_precision = day_precision - - def adapt(self, cls, **kw): - if self.native and hasattr(cls, '_adapt_from_generic_interval'): - return cls._adapt_from_generic_interval(self, **kw) - else: - return self.__class__( - native=self.native, - second_precision=self.second_precision, - day_precision=self.day_precision, - **kw) - - @property - def python_type(self): - return dt.timedelta - - def bind_processor(self, dialect): - impl_processor = self.impl.bind_processor(dialect) - epoch = self.epoch - if impl_processor: - def process(value): - if value is not None: - value = epoch + value - return impl_processor(value) - else: - def process(value): - if value is not None: - value = epoch + value - return value - return process - - def result_processor(self, dialect, coltype): - impl_processor = self.impl.result_processor(dialect, coltype) - epoch = self.epoch - if impl_processor: - def process(value): - value = impl_processor(value) - if value is None: - return None - return value - epoch - else: - def process(value): - if value is None: - return None - return value - epoch - return process - - @util.memoized_property - def _expression_adaptations(self): - return { - operators.add: { - Date: DateTime, - Interval: self.__class__, - DateTime: DateTime, - Time: Time, - }, - operators.sub: { - Interval: self.__class__ - }, - operators.mul: { - Numeric: self.__class__ - }, - operators.truediv: { - Numeric: self.__class__ - }, - operators.div: { - Numeric: self.__class__ - } - } - - @property - def _type_affinity(self): - return Interval - - def coerce_compared_value(self, op, value): - """See :meth:`.TypeEngine.coerce_compared_value` for a description.""" - - return self.impl.coerce_compared_value(op, value) - - -class REAL(Float): - """The SQL REAL type.""" - - __visit_name__ = 'REAL' - - -class FLOAT(Float): - """The SQL FLOAT type.""" - - __visit_name__ = 'FLOAT' - - -class NUMERIC(Numeric): - """The SQL NUMERIC type.""" - - __visit_name__ = 'NUMERIC' - - -class DECIMAL(Numeric): - """The SQL DECIMAL type.""" - - __visit_name__ = 'DECIMAL' - - -class INTEGER(Integer): - """The SQL INT or INTEGER type.""" - - __visit_name__ = 'INTEGER' -INT = INTEGER - - -class SMALLINT(SmallInteger): - """The SQL SMALLINT type.""" - - __visit_name__ = 'SMALLINT' - - -class BIGINT(BigInteger): - """The SQL BIGINT type.""" - - __visit_name__ = 'BIGINT' - - -class TIMESTAMP(DateTime): - """The SQL TIMESTAMP type.""" - - __visit_name__ = 'TIMESTAMP' - - def get_dbapi_type(self, dbapi): - return dbapi.TIMESTAMP - - -class DATETIME(DateTime): - """The SQL DATETIME type.""" - - __visit_name__ = 'DATETIME' - - -class DATE(Date): - """The SQL DATE type.""" - - __visit_name__ = 'DATE' - - -class TIME(Time): - """The SQL TIME type.""" - - __visit_name__ = 'TIME' - - -class TEXT(Text): - """The SQL TEXT type.""" - - __visit_name__ = 'TEXT' - - -class CLOB(Text): - """The CLOB type. - - This type is found in Oracle and Informix. - """ - - __visit_name__ = 'CLOB' - - -class VARCHAR(String): - """The SQL VARCHAR type.""" - - __visit_name__ = 'VARCHAR' - - -class NVARCHAR(Unicode): - """The SQL NVARCHAR type.""" - - __visit_name__ = 'NVARCHAR' - - -class CHAR(String): - """The SQL CHAR type.""" - - __visit_name__ = 'CHAR' - - -class NCHAR(Unicode): - """The SQL NCHAR type.""" - - __visit_name__ = 'NCHAR' - - -class BLOB(LargeBinary): - """The SQL BLOB type.""" - - __visit_name__ = 'BLOB' - - -class BINARY(_Binary): - """The SQL BINARY type.""" - - __visit_name__ = 'BINARY' - - -class VARBINARY(_Binary): - """The SQL VARBINARY type.""" - - __visit_name__ = 'VARBINARY' - - -class BOOLEAN(Boolean): - """The SQL BOOLEAN type.""" - - __visit_name__ = 'BOOLEAN' - -class NullType(TypeEngine): - """An unknown type. - - :class:`.NullType` is used as a default type for those cases where - a type cannot be determined, including: - - * During table reflection, when the type of a column is not recognized - by the :class:`.Dialect` - * When constructing SQL expressions using plain Python objects of - unknown types (e.g. ``somecolumn == my_special_object``) - * When a new :class:`.Column` is created, and the given type is passed - as ``None`` or is not passed at all. - - The :class:`.NullType` can be used within SQL expression invocation - without issue, it just has no behavior either at the expression construction - level or at the bind-parameter/result processing level. :class:`.NullType` - will result in a :exc:`.CompileError` if the compiler is asked to render - the type itself, such as if it is used in a :func:`.cast` operation - or within a schema creation operation such as that invoked by - :meth:`.MetaData.create_all` or the :class:`.CreateTable` construct. - - """ - __visit_name__ = 'null' - - _isnull = True - - def literal_processor(self, dialect): - def process(value): - return "NULL" - return process - - class Comparator(TypeEngine.Comparator): - def _adapt_expression(self, op, other_comparator): - if isinstance(other_comparator, NullType.Comparator) or \ - not operators.is_commutative(op): - return op, self.expr.type - else: - return other_comparator._adapt_expression(op, self) - comparator_factory = Comparator - - -NULLTYPE = NullType() -BOOLEANTYPE = Boolean() -STRINGTYPE = String() -INTEGERTYPE = Integer() - -_type_map = { - int: Integer(), - float: Numeric(), - bool: BOOLEANTYPE, - decimal.Decimal: Numeric(), - dt.date: Date(), - dt.datetime: DateTime(), - dt.time: Time(), - dt.timedelta: Interval(), - util.NoneType: NULLTYPE -} - -if util.py3k: - _type_map[bytes] = LargeBinary() - _type_map[str] = Unicode() -else: - _type_map[unicode] = Unicode() - _type_map[str] = String() - - -# back-assign to type_api -from . import type_api -type_api.BOOLEANTYPE = BOOLEANTYPE -type_api.STRINGTYPE = STRINGTYPE -type_api.INTEGERTYPE = INTEGERTYPE -type_api.NULLTYPE = NULLTYPE -type_api._type_map = _type_map - -# this one, there's all kinds of ways to play it, but at the EOD -# there's just a giant dependency cycle between the typing system and -# the expression element system, as you might expect. We can use -# importlaters or whatnot, but the typing system just necessarily has -# to have some kind of connection like this. right now we're injecting the -# _DefaultColumnComparator implementation into the TypeEngine.Comparator interface. -# Alternatively TypeEngine.Comparator could have an "impl" injected, though -# just injecting the base is simpler, error free, and more performant. -class Comparator(_DefaultColumnComparator): - BOOLEANTYPE = BOOLEANTYPE - -TypeEngine.Comparator.__bases__ = (Comparator, ) + TypeEngine.Comparator.__bases__ - diff --git a/libs/sqlalchemy/sql/type_api.py b/libs/sqlalchemy/sql/type_api.py deleted file mode 100644 index c6aad92b..00000000 --- a/libs/sqlalchemy/sql/type_api.py +++ /dev/null @@ -1,1053 +0,0 @@ -# sql/types_api.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Base types API. - -""" - - -from .. import exc, util -from . import operators -from .visitors import Visitable - -# these are back-assigned by sqltypes. -BOOLEANTYPE = None -INTEGERTYPE = None -NULLTYPE = None -STRINGTYPE = None - -class TypeEngine(Visitable): - """Base for built-in types.""" - - _sqla_type = True - _isnull = False - - class Comparator(operators.ColumnOperators): - """Base class for custom comparison operations defined at the - type level. See :attr:`.TypeEngine.comparator_factory`. - - - """ - - def __init__(self, expr): - self.expr = expr - - def __reduce__(self): - return _reconstitute_comparator, (self.expr, ) - - - hashable = True - """Flag, if False, means values from this type aren't hashable. - - Used by the ORM when uniquing result lists. - - """ - - comparator_factory = Comparator - """A :class:`.TypeEngine.Comparator` class which will apply - to operations performed by owning :class:`.ColumnElement` objects. - - The :attr:`.comparator_factory` attribute is a hook consulted by - the core expression system when column and SQL expression operations - are performed. When a :class:`.TypeEngine.Comparator` class is - associated with this attribute, it allows custom re-definition of - all existing operators, as well as definition of new operators. - Existing operators include those provided by Python operator overloading - such as :meth:`.operators.ColumnOperators.__add__` and - :meth:`.operators.ColumnOperators.__eq__`, - those provided as standard - attributes of :class:`.operators.ColumnOperators` such as - :meth:`.operators.ColumnOperators.like` - and :meth:`.operators.ColumnOperators.in_`. - - Rudimentary usage of this hook is allowed through simple subclassing - of existing types, or alternatively by using :class:`.TypeDecorator`. - See the documentation section :ref:`types_operators` for examples. - - .. versionadded:: 0.8 The expression system was enhanced to support - customization of operators on a per-type level. - - """ - - def copy_value(self, value): - return value - - def literal_processor(self, dialect): - """Return a conversion function for processing literal values that are - to be rendered directly without using binds. - - This function is used when the compiler makes use of the - "literal_binds" flag, typically used in DDL generation as well - as in certain scenarios where backends don't accept bound parameters. - - .. versionadded:: 0.9.0 - - """ - return None - - def bind_processor(self, dialect): - """Return a conversion function for processing bind values. - - Returns a callable which will receive a bind parameter value - as the sole positional argument and will return a value to - send to the DB-API. - - If processing is not necessary, the method should return ``None``. - - :param dialect: Dialect instance in use. - - """ - return None - - def result_processor(self, dialect, coltype): - """Return a conversion function for processing result row values. - - Returns a callable which will receive a result row column - value as the sole positional argument and will return a value - to return to the user. - - If processing is not necessary, the method should return ``None``. - - :param dialect: Dialect instance in use. - - :param coltype: DBAPI coltype argument received in cursor.description. - - """ - return None - - def column_expression(self, colexpr): - """Given a SELECT column expression, return a wrapping SQL expression. - - This is typically a SQL function that wraps a column expression - as rendered in the columns clause of a SELECT statement. - It is used for special data types that require - columns to be wrapped in some special database function in order - to coerce the value before being sent back to the application. - It is the SQL analogue of the :meth:`.TypeEngine.result_processor` - method. - - The method is evaluated at statement compile time, as opposed - to statement construction time. - - See also: - - :ref:`types_sql_value_processing` - - """ - - return None - - @util.memoized_property - def _has_column_expression(self): - """memoized boolean, check if column_expression is implemented. - - Allows the method to be skipped for the vast majority of expression - types that don't use this feature. - - """ - - return self.__class__.column_expression.__code__ \ - is not TypeEngine.column_expression.__code__ - - def bind_expression(self, bindvalue): - """"Given a bind value (i.e. a :class:`.BindParameter` instance), - return a SQL expression in its place. - - This is typically a SQL function that wraps the existing bound - parameter within the statement. It is used for special data types - that require literals being wrapped in some special database function - in order to coerce an application-level value into a database-specific - format. It is the SQL analogue of the - :meth:`.TypeEngine.bind_processor` method. - - The method is evaluated at statement compile time, as opposed - to statement construction time. - - Note that this method, when implemented, should always return - the exact same structure, without any conditional logic, as it - may be used in an executemany() call against an arbitrary number - of bound parameter sets. - - See also: - - :ref:`types_sql_value_processing` - - """ - return None - - @util.memoized_property - def _has_bind_expression(self): - """memoized boolean, check if bind_expression is implemented. - - Allows the method to be skipped for the vast majority of expression - types that don't use this feature. - - """ - - return self.__class__.bind_expression.__code__ \ - is not TypeEngine.bind_expression.__code__ - - def compare_values(self, x, y): - """Compare two values for equality.""" - - return x == y - - def get_dbapi_type(self, dbapi): - """Return the corresponding type object from the underlying DB-API, if - any. - - This can be useful for calling ``setinputsizes()``, for example. - - """ - return None - - @property - def python_type(self): - """Return the Python type object expected to be returned - by instances of this type, if known. - - Basically, for those types which enforce a return type, - or are known across the board to do such for all common - DBAPIs (like ``int`` for example), will return that type. - - If a return type is not defined, raises - ``NotImplementedError``. - - Note that any type also accommodates NULL in SQL which - means you can also get back ``None`` from any type - in practice. - - """ - raise NotImplementedError() - - def with_variant(self, type_, dialect_name): - """Produce a new type object that will utilize the given - type when applied to the dialect of the given name. - - e.g.:: - - from sqlalchemy.types import String - from sqlalchemy.dialects import mysql - - s = String() - - s = s.with_variant(mysql.VARCHAR(collation='foo'), 'mysql') - - The construction of :meth:`.TypeEngine.with_variant` is always - from the "fallback" type to that which is dialect specific. - The returned type is an instance of :class:`.Variant`, which - itself provides a :meth:`~sqlalchemy.types.Variant.with_variant` - that can be called repeatedly. - - :param type_: a :class:`.TypeEngine` that will be selected - as a variant from the originating type, when a dialect - of the given name is in use. - :param dialect_name: base name of the dialect which uses - this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) - - .. versionadded:: 0.7.2 - - """ - return Variant(self, {dialect_name: type_}) - - - @util.memoized_property - def _type_affinity(self): - """Return a rudimental 'affinity' value expressing the general class - of type.""" - - typ = None - for t in self.__class__.__mro__: - if t in (TypeEngine, UserDefinedType): - return typ - elif issubclass(t, (TypeEngine, UserDefinedType)): - typ = t - else: - return self.__class__ - - def dialect_impl(self, dialect): - """Return a dialect-specific implementation for this - :class:`.TypeEngine`. - - """ - try: - return dialect._type_memos[self]['impl'] - except KeyError: - return self._dialect_info(dialect)['impl'] - - - def _cached_literal_processor(self, dialect): - """Return a dialect-specific literal processor for this type.""" - try: - return dialect._type_memos[self]['literal'] - except KeyError: - d = self._dialect_info(dialect) - d['literal'] = lp = d['impl'].literal_processor(dialect) - return lp - - def _cached_bind_processor(self, dialect): - """Return a dialect-specific bind processor for this type.""" - - try: - return dialect._type_memos[self]['bind'] - except KeyError: - d = self._dialect_info(dialect) - d['bind'] = bp = d['impl'].bind_processor(dialect) - return bp - - def _cached_result_processor(self, dialect, coltype): - """Return a dialect-specific result processor for this type.""" - - try: - return dialect._type_memos[self][coltype] - except KeyError: - d = self._dialect_info(dialect) - # key assumption: DBAPI type codes are - # constants. Else this dictionary would - # grow unbounded. - d[coltype] = rp = d['impl'].result_processor(dialect, coltype) - return rp - - def _dialect_info(self, dialect): - """Return a dialect-specific registry which - caches a dialect-specific implementation, bind processing - function, and one or more result processing functions.""" - - if self in dialect._type_memos: - return dialect._type_memos[self] - else: - impl = self._gen_dialect_impl(dialect) - if impl is self: - impl = self.adapt(type(self)) - # this can't be self, else we create a cycle - assert impl is not self - dialect._type_memos[self] = d = {'impl': impl} - return d - - def _gen_dialect_impl(self, dialect): - return dialect.type_descriptor(self) - - def adapt(self, cls, **kw): - """Produce an "adapted" form of this type, given an "impl" class - to work with. - - This method is used internally to associate generic - types with "implementation" types that are specific to a particular - dialect. - """ - return util.constructor_copy(self, cls, **kw) - - - def coerce_compared_value(self, op, value): - """Suggest a type for a 'coerced' Python value in an expression. - - Given an operator and value, gives the type a chance - to return a type which the value should be coerced into. - - The default behavior here is conservative; if the right-hand - side is already coerced into a SQL type based on its - Python type, it is usually left alone. - - End-user functionality extension here should generally be via - :class:`.TypeDecorator`, which provides more liberal behavior in that - it defaults to coercing the other side of the expression into this - type, thus applying special Python conversions above and beyond those - needed by the DBAPI to both ides. It also provides the public method - :meth:`.TypeDecorator.coerce_compared_value` which is intended for - end-user customization of this behavior. - - """ - _coerced_type = _type_map.get(type(value), NULLTYPE) - if _coerced_type is NULLTYPE or _coerced_type._type_affinity \ - is self._type_affinity: - return self - else: - return _coerced_type - - def _compare_type_affinity(self, other): - return self._type_affinity is other._type_affinity - - def compile(self, dialect=None): - """Produce a string-compiled form of this :class:`.TypeEngine`. - - When called with no arguments, uses a "default" dialect - to produce a string result. - - :param dialect: a :class:`.Dialect` instance. - - """ - # arg, return value is inconsistent with - # ClauseElement.compile()....this is a mistake. - - if not dialect: - dialect = self._default_dialect() - - return dialect.type_compiler.process(self) - - @util.dependencies("sqlalchemy.engine.default") - def _default_dialect(self, default): - if self.__class__.__module__.startswith("sqlalchemy.dialects"): - tokens = self.__class__.__module__.split(".")[0:3] - mod = ".".join(tokens) - return getattr(__import__(mod).dialects, tokens[-1]).dialect() - else: - return default.DefaultDialect() - - def __str__(self): - if util.py2k: - return unicode(self.compile()).\ - encode('ascii', 'backslashreplace') - else: - return str(self.compile()) - - def __repr__(self): - return util.generic_repr(self) - -class UserDefinedType(TypeEngine): - """Base for user defined types. - - This should be the base of new types. Note that - for most cases, :class:`.TypeDecorator` is probably - more appropriate:: - - import sqlalchemy.types as types - - class MyType(types.UserDefinedType): - def __init__(self, precision = 8): - self.precision = precision - - def get_col_spec(self): - return "MYTYPE(%s)" % self.precision - - def bind_processor(self, dialect): - def process(value): - return value - return process - - def result_processor(self, dialect, coltype): - def process(value): - return value - return process - - Once the type is made, it's immediately usable:: - - table = Table('foo', meta, - Column('id', Integer, primary_key=True), - Column('data', MyType(16)) - ) - - """ - __visit_name__ = "user_defined" - - - class Comparator(TypeEngine.Comparator): - def _adapt_expression(self, op, other_comparator): - if hasattr(self.type, 'adapt_operator'): - util.warn_deprecated( - "UserDefinedType.adapt_operator is deprecated. Create " - "a UserDefinedType.Comparator subclass instead which " - "generates the desired expression constructs, given a " - "particular operator." - ) - return self.type.adapt_operator(op), self.type - else: - return op, self.type - - comparator_factory = Comparator - - def coerce_compared_value(self, op, value): - """Suggest a type for a 'coerced' Python value in an expression. - - Default behavior for :class:`.UserDefinedType` is the - same as that of :class:`.TypeDecorator`; by default it returns - ``self``, assuming the compared value should be coerced into - the same type as this one. See - :meth:`.TypeDecorator.coerce_compared_value` for more detail. - - .. versionchanged:: 0.8 :meth:`.UserDefinedType.coerce_compared_value` - now returns ``self`` by default, rather than falling onto the - more fundamental behavior of - :meth:`.TypeEngine.coerce_compared_value`. - - """ - - return self - - -class TypeDecorator(TypeEngine): - """Allows the creation of types which add additional functionality - to an existing type. - - This method is preferred to direct subclassing of SQLAlchemy's - built-in types as it ensures that all required functionality of - the underlying type is kept in place. - - Typical usage:: - - import sqlalchemy.types as types - - class MyType(types.TypeDecorator): - '''Prefixes Unicode values with "PREFIX:" on the way in and - strips it off on the way out. - ''' - - impl = types.Unicode - - def process_bind_param(self, value, dialect): - return "PREFIX:" + value - - def process_result_value(self, value, dialect): - return value[7:] - - def copy(self): - return MyType(self.impl.length) - - The class-level "impl" attribute is required, and can reference any - TypeEngine class. Alternatively, the load_dialect_impl() method - can be used to provide different type classes based on the dialect - given; in this case, the "impl" variable can reference - ``TypeEngine`` as a placeholder. - - Types that receive a Python type that isn't similar to the ultimate type - used may want to define the :meth:`TypeDecorator.coerce_compared_value` - method. This is used to give the expression system a hint when coercing - Python objects into bind parameters within expressions. Consider this - expression:: - - mytable.c.somecol + datetime.date(2009, 5, 15) - - Above, if "somecol" is an ``Integer`` variant, it makes sense that - we're doing date arithmetic, where above is usually interpreted - by databases as adding a number of days to the given date. - The expression system does the right thing by not attempting to - coerce the "date()" value into an integer-oriented bind parameter. - - However, in the case of ``TypeDecorator``, we are usually changing an - incoming Python type to something new - ``TypeDecorator`` by default will - "coerce" the non-typed side to be the same type as itself. Such as below, - we define an "epoch" type that stores a date value as an integer:: - - class MyEpochType(types.TypeDecorator): - impl = types.Integer - - epoch = datetime.date(1970, 1, 1) - - def process_bind_param(self, value, dialect): - return (value - self.epoch).days - - def process_result_value(self, value, dialect): - return self.epoch + timedelta(days=value) - - Our expression of ``somecol + date`` with the above type will coerce the - "date" on the right side to also be treated as ``MyEpochType``. - - This behavior can be overridden via the - :meth:`~TypeDecorator.coerce_compared_value` method, which returns a type - that should be used for the value of the expression. Below we set it such - that an integer value will be treated as an ``Integer``, and any other - value is assumed to be a date and will be treated as a ``MyEpochType``:: - - def coerce_compared_value(self, op, value): - if isinstance(value, int): - return Integer() - else: - return self - - """ - - __visit_name__ = "type_decorator" - - def __init__(self, *args, **kwargs): - """Construct a :class:`.TypeDecorator`. - - Arguments sent here are passed to the constructor - of the class assigned to the ``impl`` class level attribute, - assuming the ``impl`` is a callable, and the resulting - object is assigned to the ``self.impl`` instance attribute - (thus overriding the class attribute of the same name). - - If the class level ``impl`` is not a callable (the unusual case), - it will be assigned to the same instance attribute 'as-is', - ignoring those arguments passed to the constructor. - - Subclasses can override this to customize the generation - of ``self.impl`` entirely. - - """ - - if not hasattr(self.__class__, 'impl'): - raise AssertionError("TypeDecorator implementations " - "require a class-level variable " - "'impl' which refers to the class of " - "type being decorated") - self.impl = to_instance(self.__class__.impl, *args, **kwargs) - - coerce_to_is_types = (util.NoneType, ) - """Specify those Python types which should be coerced at the expression - level to "IS " when compared using ``==`` (and same for - ``IS NOT`` in conjunction with ``!=``. - - For most SQLAlchemy types, this includes ``NoneType``, as well as ``bool``. - - :class:`.TypeDecorator` modifies this list to only include ``NoneType``, - as typedecorator implementations that deal with boolean types are common. - - Custom :class:`.TypeDecorator` classes can override this attribute to - return an empty tuple, in which case no values will be coerced to - constants. - - ..versionadded:: 0.8.2 - Added :attr:`.TypeDecorator.coerce_to_is_types` to allow for easier - control of ``__eq__()`` ``__ne__()`` operations. - - """ - - class Comparator(TypeEngine.Comparator): - - def operate(self, op, *other, **kwargs): - kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types - return super(TypeDecorator.Comparator, self).operate( - op, *other, **kwargs) - - def reverse_operate(self, op, other, **kwargs): - kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types - return super(TypeDecorator.Comparator, self).reverse_operate( - op, other, **kwargs) - - @property - def comparator_factory(self): - return type("TDComparator", - (TypeDecorator.Comparator, self.impl.comparator_factory), - {}) - - def _gen_dialect_impl(self, dialect): - """ - #todo - """ - adapted = dialect.type_descriptor(self) - if adapted is not self: - return adapted - - # otherwise adapt the impl type, link - # to a copy of this TypeDecorator and return - # that. - typedesc = self.load_dialect_impl(dialect).dialect_impl(dialect) - tt = self.copy() - if not isinstance(tt, self.__class__): - raise AssertionError('Type object %s does not properly ' - 'implement the copy() method, it must ' - 'return an object of type %s' % (self, - self.__class__)) - tt.impl = typedesc - return tt - - @property - def _type_affinity(self): - """ - #todo - """ - return self.impl._type_affinity - - def type_engine(self, dialect): - """Return a dialect-specific :class:`.TypeEngine` instance - for this :class:`.TypeDecorator`. - - In most cases this returns a dialect-adapted form of - the :class:`.TypeEngine` type represented by ``self.impl``. - Makes usage of :meth:`dialect_impl` but also traverses - into wrapped :class:`.TypeDecorator` instances. - Behavior can be customized here by overriding - :meth:`load_dialect_impl`. - - """ - adapted = dialect.type_descriptor(self) - if type(adapted) is not type(self): - return adapted - elif isinstance(self.impl, TypeDecorator): - return self.impl.type_engine(dialect) - else: - return self.load_dialect_impl(dialect) - - def load_dialect_impl(self, dialect): - """Return a :class:`.TypeEngine` object corresponding to a dialect. - - This is an end-user override hook that can be used to provide - differing types depending on the given dialect. It is used - by the :class:`.TypeDecorator` implementation of :meth:`type_engine` - to help determine what type should ultimately be returned - for a given :class:`.TypeDecorator`. - - By default returns ``self.impl``. - - """ - return self.impl - - def __getattr__(self, key): - """Proxy all other undefined accessors to the underlying - implementation.""" - return getattr(self.impl, key) - - def process_literal_param(self, value, dialect): - """Receive a literal parameter value to be rendered inline within - a statement. - - This method is used when the compiler renders a - literal value without using binds, typically within DDL - such as in the "server default" of a column or an expression - within a CHECK constraint. - - The returned string will be rendered into the output string. - - .. versionadded:: 0.9.0 - - """ - raise NotImplementedError() - - def process_bind_param(self, value, dialect): - """Receive a bound parameter value to be converted. - - Subclasses override this method to return the - value that should be passed along to the underlying - :class:`.TypeEngine` object, and from there to the - DBAPI ``execute()`` method. - - The operation could be anything desired to perform custom - behavior, such as transforming or serializing data. - This could also be used as a hook for validating logic. - - This operation should be designed with the reverse operation - in mind, which would be the process_result_value method of - this class. - - :param value: Data to operate upon, of any type expected by - this method in the subclass. Can be ``None``. - :param dialect: the :class:`.Dialect` in use. - - """ - - raise NotImplementedError() - - def process_result_value(self, value, dialect): - """Receive a result-row column value to be converted. - - Subclasses should implement this method to operate on data - fetched from the database. - - Subclasses override this method to return the - value that should be passed back to the application, - given a value that is already processed by - the underlying :class:`.TypeEngine` object, originally - from the DBAPI cursor method ``fetchone()`` or similar. - - The operation could be anything desired to perform custom - behavior, such as transforming or serializing data. - This could also be used as a hook for validating logic. - - :param value: Data to operate upon, of any type expected by - this method in the subclass. Can be ``None``. - :param dialect: the :class:`.Dialect` in use. - - This operation should be designed to be reversible by - the "process_bind_param" method of this class. - - """ - - raise NotImplementedError() - - @util.memoized_property - def _has_bind_processor(self): - """memoized boolean, check if process_bind_param is implemented. - - Allows the base process_bind_param to raise - NotImplementedError without needing to test an expensive - exception throw. - - """ - - return self.__class__.process_bind_param.__code__ \ - is not TypeDecorator.process_bind_param.__code__ - - @util.memoized_property - def _has_literal_processor(self): - """memoized boolean, check if process_literal_param is implemented. - - - """ - - return self.__class__.process_literal_param.__code__ \ - is not TypeDecorator.process_literal_param.__code__ - - def literal_processor(self, dialect): - """Provide a literal processing function for the given - :class:`.Dialect`. - - Subclasses here will typically override :meth:`.TypeDecorator.process_literal_param` - instead of this method directly. - - By default, this method makes use of :meth:`.TypeDecorator.process_bind_param` - if that method is implemented, where :meth:`.TypeDecorator.process_literal_param` - is not. The rationale here is that :class:`.TypeDecorator` typically deals - with Python conversions of data that are above the layer of database - presentation. With the value converted by :meth:`.TypeDecorator.process_bind_param`, - the underlying type will then handle whether it needs to be presented to the - DBAPI as a bound parameter or to the database as an inline SQL value. - - .. versionadded:: 0.9.0 - - """ - if self._has_literal_processor: - process_param = self.process_literal_param - elif self._has_bind_processor: - # the bind processor should normally be OK - # for TypeDecorator since it isn't doing DB-level - # handling, the handling here won't be different for bound vs. - # literals. - process_param = self.process_bind_param - else: - process_param = None - - if process_param: - impl_processor = self.impl.literal_processor(dialect) - if impl_processor: - def process(value): - return impl_processor(process_param(value, dialect)) - else: - def process(value): - return process_param(value, dialect) - - return process - else: - return self.impl.literal_processor(dialect) - - def bind_processor(self, dialect): - """Provide a bound value processing function for the - given :class:`.Dialect`. - - This is the method that fulfills the :class:`.TypeEngine` - contract for bound value conversion. :class:`.TypeDecorator` - will wrap a user-defined implementation of - :meth:`process_bind_param` here. - - User-defined code can override this method directly, - though its likely best to use :meth:`process_bind_param` so that - the processing provided by ``self.impl`` is maintained. - - :param dialect: Dialect instance in use. - - This method is the reverse counterpart to the - :meth:`result_processor` method of this class. - - """ - if self._has_bind_processor: - process_param = self.process_bind_param - impl_processor = self.impl.bind_processor(dialect) - if impl_processor: - def process(value): - return impl_processor(process_param(value, dialect)) - - else: - def process(value): - return process_param(value, dialect) - - return process - else: - return self.impl.bind_processor(dialect) - - @util.memoized_property - def _has_result_processor(self): - """memoized boolean, check if process_result_value is implemented. - - Allows the base process_result_value to raise - NotImplementedError without needing to test an expensive - exception throw. - - """ - return self.__class__.process_result_value.__code__ \ - is not TypeDecorator.process_result_value.__code__ - - def result_processor(self, dialect, coltype): - """Provide a result value processing function for the given - :class:`.Dialect`. - - This is the method that fulfills the :class:`.TypeEngine` - contract for result value conversion. :class:`.TypeDecorator` - will wrap a user-defined implementation of - :meth:`process_result_value` here. - - User-defined code can override this method directly, - though its likely best to use :meth:`process_result_value` so that - the processing provided by ``self.impl`` is maintained. - - :param dialect: Dialect instance in use. - :param coltype: An SQLAlchemy data type - - This method is the reverse counterpart to the - :meth:`bind_processor` method of this class. - - """ - if self._has_result_processor: - process_value = self.process_result_value - impl_processor = self.impl.result_processor(dialect, - coltype) - if impl_processor: - def process(value): - return process_value(impl_processor(value), dialect) - - else: - def process(value): - return process_value(value, dialect) - - return process - else: - return self.impl.result_processor(dialect, coltype) - - def coerce_compared_value(self, op, value): - """Suggest a type for a 'coerced' Python value in an expression. - - By default, returns self. This method is called by - the expression system when an object using this type is - on the left or right side of an expression against a plain Python - object which does not yet have a SQLAlchemy type assigned:: - - expr = table.c.somecolumn + 35 - - Where above, if ``somecolumn`` uses this type, this method will - be called with the value ``operator.add`` - and ``35``. The return value is whatever SQLAlchemy type should - be used for ``35`` for this particular operation. - - """ - return self - - def copy(self): - """Produce a copy of this :class:`.TypeDecorator` instance. - - This is a shallow copy and is provided to fulfill part of - the :class:`.TypeEngine` contract. It usually does not - need to be overridden unless the user-defined :class:`.TypeDecorator` - has local state that should be deep-copied. - - """ - - instance = self.__class__.__new__(self.__class__) - instance.__dict__.update(self.__dict__) - return instance - - def get_dbapi_type(self, dbapi): - """Return the DBAPI type object represented by this - :class:`.TypeDecorator`. - - By default this calls upon :meth:`.TypeEngine.get_dbapi_type` of the - underlying "impl". - """ - return self.impl.get_dbapi_type(dbapi) - - def compare_values(self, x, y): - """Given two values, compare them for equality. - - By default this calls upon :meth:`.TypeEngine.compare_values` - of the underlying "impl", which in turn usually - uses the Python equals operator ``==``. - - This function is used by the ORM to compare - an original-loaded value with an intercepted - "changed" value, to determine if a net change - has occurred. - - """ - return self.impl.compare_values(x, y) - - def __repr__(self): - return util.generic_repr(self, to_inspect=self.impl) - - -class Variant(TypeDecorator): - """A wrapping type that selects among a variety of - implementations based on dialect in use. - - The :class:`.Variant` type is typically constructed - using the :meth:`.TypeEngine.with_variant` method. - - .. versionadded:: 0.7.2 - - .. seealso:: :meth:`.TypeEngine.with_variant` for an example of use. - - """ - - def __init__(self, base, mapping): - """Construct a new :class:`.Variant`. - - :param base: the base 'fallback' type - :param mapping: dictionary of string dialect names to - :class:`.TypeEngine` instances. - - """ - self.impl = base - self.mapping = mapping - - def load_dialect_impl(self, dialect): - if dialect.name in self.mapping: - return self.mapping[dialect.name] - else: - return self.impl - - def with_variant(self, type_, dialect_name): - """Return a new :class:`.Variant` which adds the given - type + dialect name to the mapping, in addition to the - mapping present in this :class:`.Variant`. - - :param type_: a :class:`.TypeEngine` that will be selected - as a variant from the originating type, when a dialect - of the given name is in use. - :param dialect_name: base name of the dialect which uses - this type. (i.e. ``'postgresql'``, ``'mysql'``, etc.) - - """ - - if dialect_name in self.mapping: - raise exc.ArgumentError( - "Dialect '%s' is already present in " - "the mapping for this Variant" % dialect_name) - mapping = self.mapping.copy() - mapping[dialect_name] = type_ - return Variant(self.impl, mapping) - -def _reconstitute_comparator(expression): - return expression.comparator - - -def to_instance(typeobj, *arg, **kw): - if typeobj is None: - return NULLTYPE - - if util.callable(typeobj): - return typeobj(*arg, **kw) - else: - return typeobj - - -def adapt_type(typeobj, colspecs): - if isinstance(typeobj, type): - typeobj = typeobj() - for t in typeobj.__class__.__mro__[0:-1]: - try: - impltype = colspecs[t] - break - except KeyError: - pass - else: - # couldnt adapt - so just return the type itself - # (it may be a user-defined type) - return typeobj - # if we adapted the given generic type to a database-specific type, - # but it turns out the originally given "generic" type - # is actually a subclass of our resulting type, then we were already - # given a more specific type than that required; so use that. - if (issubclass(typeobj.__class__, impltype)): - return typeobj - return typeobj.adapt(impltype) - - diff --git a/libs/sqlalchemy/sql/util.py b/libs/sqlalchemy/sql/util.py deleted file mode 100644 index 50ce30aa..00000000 --- a/libs/sqlalchemy/sql/util.py +++ /dev/null @@ -1,601 +0,0 @@ -# sql/util.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""High level utilities which build upon other modules here. - -""" - -from .. import exc, util -from .base import _from_objects, ColumnSet -from . import operators, visitors -from itertools import chain -from collections import deque - -from .elements import BindParameter, ColumnClause, ColumnElement, \ - Null, UnaryExpression, literal_column, Label -from .selectable import ScalarSelect, Join, FromClause, FromGrouping -from .schema import Column - -join_condition = util.langhelpers.public_factory( - Join._join_condition, - ".sql.util.join_condition") - -# names that are still being imported from the outside -from .annotation import _shallow_annotate, _deep_annotate, _deep_deannotate -from .elements import _find_columns -from .ddl import sort_tables - - -def find_join_source(clauses, join_to): - """Given a list of FROM clauses and a selectable, - return the first index and element from the list of - clauses which can be joined against the selectable. returns - None, None if no match is found. - - e.g.:: - - clause1 = table1.join(table2) - clause2 = table4.join(table5) - - join_to = table2.join(table3) - - find_join_source([clause1, clause2], join_to) == clause1 - - """ - - selectables = list(_from_objects(join_to)) - for i, f in enumerate(clauses): - for s in selectables: - if f.is_derived_from(s): - return i, f - else: - return None, None - - -def visit_binary_product(fn, expr): - """Produce a traversal of the given expression, delivering - column comparisons to the given function. - - The function is of the form:: - - def my_fn(binary, left, right) - - For each binary expression located which has a - comparison operator, the product of "left" and - "right" will be delivered to that function, - in terms of that binary. - - Hence an expression like:: - - and_( - (a + b) == q + func.sum(e + f), - j == r - ) - - would have the traversal:: - - a q - a e - a f - b q - b e - b f - j r - - That is, every combination of "left" and - "right" that doesn't further contain - a binary comparison is passed as pairs. - - """ - stack = [] - - def visit(element): - if isinstance(element, ScalarSelect): - # we dont want to dig into correlated subqueries, - # those are just column elements by themselves - yield element - elif element.__visit_name__ == 'binary' and \ - operators.is_comparison(element.operator): - stack.insert(0, element) - for l in visit(element.left): - for r in visit(element.right): - fn(stack[0], l, r) - stack.pop(0) - for elem in element.get_children(): - visit(elem) - else: - if isinstance(element, ColumnClause): - yield element - for elem in element.get_children(): - for e in visit(elem): - yield e - list(visit(expr)) - - -def find_tables(clause, check_columns=False, - include_aliases=False, include_joins=False, - include_selects=False, include_crud=False): - """locate Table objects within the given expression.""" - - tables = [] - _visitors = {} - - if include_selects: - _visitors['select'] = _visitors['compound_select'] = tables.append - - if include_joins: - _visitors['join'] = tables.append - - if include_aliases: - _visitors['alias'] = tables.append - - if include_crud: - _visitors['insert'] = _visitors['update'] = \ - _visitors['delete'] = lambda ent: tables.append(ent.table) - - if check_columns: - def visit_column(column): - tables.append(column.table) - _visitors['column'] = visit_column - - _visitors['table'] = tables.append - - visitors.traverse(clause, {'column_collections': False}, _visitors) - return tables - - - -def unwrap_order_by(clause): - """Break up an 'order by' expression into individual column-expressions, - without DESC/ASC/NULLS FIRST/NULLS LAST""" - - cols = util.column_set() - stack = deque([clause]) - while stack: - t = stack.popleft() - if isinstance(t, ColumnElement) and \ - ( - not isinstance(t, UnaryExpression) or \ - not operators.is_ordering_modifier(t.modifier) - ): - cols.add(t) - else: - for c in t.get_children(): - stack.append(c) - return cols - - -def clause_is_present(clause, search): - """Given a target clause and a second to search within, return True - if the target is plainly present in the search without any - subqueries or aliases involved. - - Basically descends through Joins. - - """ - - for elem in surface_selectables(search): - if clause == elem: # use == here so that Annotated's compare - return True - else: - return False - -def surface_selectables(clause): - stack = [clause] - while stack: - elem = stack.pop() - yield elem - if isinstance(elem, Join): - stack.extend((elem.left, elem.right)) - elif isinstance(elem, FromGrouping): - stack.append(elem.element) - -def selectables_overlap(left, right): - """Return True if left/right have some overlapping selectable""" - - return bool( - set(surface_selectables(left)).intersection( - surface_selectables(right) - ) - ) - -def bind_values(clause): - """Return an ordered list of "bound" values in the given clause. - - E.g.:: - - >>> expr = and_( - ... table.c.foo==5, table.c.foo==7 - ... ) - >>> bind_values(expr) - [5, 7] - """ - - v = [] - - def visit_bindparam(bind): - v.append(bind.effective_value) - - visitors.traverse(clause, {}, {'bindparam': visit_bindparam}) - return v - - -def _quote_ddl_expr(element): - if isinstance(element, util.string_types): - element = element.replace("'", "''") - return "'%s'" % element - else: - return repr(element) - - -class _repr_params(object): - """A string view of bound parameters, truncating - display to the given number of 'multi' parameter sets. - - """ - def __init__(self, params, batches): - self.params = params - self.batches = batches - - def __repr__(self): - if isinstance(self.params, (list, tuple)) and \ - len(self.params) > self.batches and \ - isinstance(self.params[0], (list, dict, tuple)): - msg = " ... displaying %i of %i total bound parameter sets ... " - return ' '.join(( - repr(self.params[:self.batches - 2])[0:-1], - msg % (self.batches, len(self.params)), - repr(self.params[-2:])[1:] - )) - else: - return repr(self.params) - - - - -def adapt_criterion_to_null(crit, nulls): - """given criterion containing bind params, convert selected elements - to IS NULL. - - """ - - def visit_binary(binary): - if isinstance(binary.left, BindParameter) \ - and binary.left._identifying_key in nulls: - # reverse order if the NULL is on the left side - binary.left = binary.right - binary.right = Null() - binary.operator = operators.is_ - binary.negate = operators.isnot - elif isinstance(binary.right, BindParameter) \ - and binary.right._identifying_key in nulls: - binary.right = Null() - binary.operator = operators.is_ - binary.negate = operators.isnot - - return visitors.cloned_traverse(crit, {}, {'binary': visit_binary}) - - -def splice_joins(left, right, stop_on=None): - if left is None: - return right - - stack = [(right, None)] - - adapter = ClauseAdapter(left) - ret = None - while stack: - (right, prevright) = stack.pop() - if isinstance(right, Join) and right is not stop_on: - right = right._clone() - right._reset_exported() - right.onclause = adapter.traverse(right.onclause) - stack.append((right.left, right)) - else: - right = adapter.traverse(right) - if prevright is not None: - prevright.left = right - if ret is None: - ret = right - - return ret - - -def reduce_columns(columns, *clauses, **kw): - """given a list of columns, return a 'reduced' set based on natural - equivalents. - - the set is reduced to the smallest list of columns which have no natural - equivalent present in the list. A "natural equivalent" means that two - columns will ultimately represent the same value because they are related - by a foreign key. - - \*clauses is an optional list of join clauses which will be traversed - to further identify columns that are "equivalent". - - \**kw may specify 'ignore_nonexistent_tables' to ignore foreign keys - whose tables are not yet configured, or columns that aren't yet present. - - This function is primarily used to determine the most minimal "primary key" - from a selectable, by reducing the set of primary key columns present - in the the selectable to just those that are not repeated. - - """ - ignore_nonexistent_tables = kw.pop('ignore_nonexistent_tables', False) - only_synonyms = kw.pop('only_synonyms', False) - - columns = util.ordered_column_set(columns) - - omit = util.column_set() - for col in columns: - for fk in chain(*[c.foreign_keys for c in col.proxy_set]): - for c in columns: - if c is col: - continue - try: - fk_col = fk.column - except exc.NoReferencedColumnError: - # TODO: add specific coverage here - # to test/sql/test_selectable ReduceTest - if ignore_nonexistent_tables: - continue - else: - raise - except exc.NoReferencedTableError: - # TODO: add specific coverage here - # to test/sql/test_selectable ReduceTest - if ignore_nonexistent_tables: - continue - else: - raise - if fk_col.shares_lineage(c) and \ - (not only_synonyms or \ - c.name == col.name): - omit.add(col) - break - - if clauses: - def visit_binary(binary): - if binary.operator == operators.eq: - cols = util.column_set(chain(*[c.proxy_set - for c in columns.difference(omit)])) - if binary.left in cols and binary.right in cols: - for c in reversed(columns): - if c.shares_lineage(binary.right) and \ - (not only_synonyms or \ - c.name == binary.left.name): - omit.add(c) - break - for clause in clauses: - if clause is not None: - visitors.traverse(clause, {}, {'binary': visit_binary}) - - return ColumnSet(columns.difference(omit)) - - -def criterion_as_pairs(expression, consider_as_foreign_keys=None, - consider_as_referenced_keys=None, any_operator=False): - """traverse an expression and locate binary criterion pairs.""" - - if consider_as_foreign_keys and consider_as_referenced_keys: - raise exc.ArgumentError("Can only specify one of " - "'consider_as_foreign_keys' or " - "'consider_as_referenced_keys'") - - def col_is(a, b): - #return a is b - return a.compare(b) - - def visit_binary(binary): - if not any_operator and binary.operator is not operators.eq: - return - if not isinstance(binary.left, ColumnElement) or \ - not isinstance(binary.right, ColumnElement): - return - - if consider_as_foreign_keys: - if binary.left in consider_as_foreign_keys and \ - (col_is(binary.right, binary.left) or - binary.right not in consider_as_foreign_keys): - pairs.append((binary.right, binary.left)) - elif binary.right in consider_as_foreign_keys and \ - (col_is(binary.left, binary.right) or - binary.left not in consider_as_foreign_keys): - pairs.append((binary.left, binary.right)) - elif consider_as_referenced_keys: - if binary.left in consider_as_referenced_keys and \ - (col_is(binary.right, binary.left) or - binary.right not in consider_as_referenced_keys): - pairs.append((binary.left, binary.right)) - elif binary.right in consider_as_referenced_keys and \ - (col_is(binary.left, binary.right) or - binary.left not in consider_as_referenced_keys): - pairs.append((binary.right, binary.left)) - else: - if isinstance(binary.left, Column) and \ - isinstance(binary.right, Column): - if binary.left.references(binary.right): - pairs.append((binary.right, binary.left)) - elif binary.right.references(binary.left): - pairs.append((binary.left, binary.right)) - pairs = [] - visitors.traverse(expression, {}, {'binary': visit_binary}) - return pairs - - - -class AliasedRow(object): - """Wrap a RowProxy with a translation map. - - This object allows a set of keys to be translated - to those present in a RowProxy. - - """ - def __init__(self, row, map): - # AliasedRow objects don't nest, so un-nest - # if another AliasedRow was passed - if isinstance(row, AliasedRow): - self.row = row.row - else: - self.row = row - self.map = map - - def __contains__(self, key): - return self.map[key] in self.row - - def has_key(self, key): - return key in self - - def __getitem__(self, key): - return self.row[self.map[key]] - - def keys(self): - return self.row.keys() - - -class ClauseAdapter(visitors.ReplacingCloningVisitor): - """Clones and modifies clauses based on column correspondence. - - E.g.:: - - table1 = Table('sometable', metadata, - Column('col1', Integer), - Column('col2', Integer) - ) - table2 = Table('someothertable', metadata, - Column('col1', Integer), - Column('col2', Integer) - ) - - condition = table1.c.col1 == table2.c.col1 - - make an alias of table1:: - - s = table1.alias('foo') - - calling ``ClauseAdapter(s).traverse(condition)`` converts - condition to read:: - - s.c.col1 == table2.c.col1 - - """ - def __init__(self, selectable, equivalents=None, - include=None, exclude=None, - include_fn=None, exclude_fn=None, - adapt_on_names=False): - self.__traverse_options__ = {'stop_on': [selectable]} - self.selectable = selectable - if include: - assert not include_fn - self.include_fn = lambda e: e in include - else: - self.include_fn = include_fn - if exclude: - assert not exclude_fn - self.exclude_fn = lambda e: e in exclude - else: - self.exclude_fn = exclude_fn - self.equivalents = util.column_dict(equivalents or {}) - self.adapt_on_names = adapt_on_names - - def _corresponding_column(self, col, require_embedded, - _seen=util.EMPTY_SET): - newcol = self.selectable.corresponding_column( - col, - require_embedded=require_embedded) - if newcol is None and col in self.equivalents and col not in _seen: - for equiv in self.equivalents[col]: - newcol = self._corresponding_column(equiv, - require_embedded=require_embedded, - _seen=_seen.union([col])) - if newcol is not None: - return newcol - if self.adapt_on_names and newcol is None: - newcol = self.selectable.c.get(col.name) - return newcol - - magic_flag = False - def replace(self, col): - if not self.magic_flag and isinstance(col, FromClause) and \ - self.selectable.is_derived_from(col): - return self.selectable - elif not isinstance(col, ColumnElement): - return None - elif self.include_fn and not self.include_fn(col): - return None - elif self.exclude_fn and self.exclude_fn(col): - return None - else: - return self._corresponding_column(col, True) - - -class ColumnAdapter(ClauseAdapter): - """Extends ClauseAdapter with extra utility functions. - - Provides the ability to "wrap" this ClauseAdapter - around another, a columns dictionary which returns - adapted elements given an original, and an - adapted_row() factory. - - """ - def __init__(self, selectable, equivalents=None, - chain_to=None, include=None, - exclude=None, adapt_required=False): - ClauseAdapter.__init__(self, selectable, equivalents, include, exclude) - if chain_to: - self.chain(chain_to) - self.columns = util.populate_column_dict(self._locate_col) - self.adapt_required = adapt_required - - def wrap(self, adapter): - ac = self.__class__.__new__(self.__class__) - ac.__dict__ = self.__dict__.copy() - ac._locate_col = ac._wrap(ac._locate_col, adapter._locate_col) - ac.adapt_clause = ac._wrap(ac.adapt_clause, adapter.adapt_clause) - ac.adapt_list = ac._wrap(ac.adapt_list, adapter.adapt_list) - ac.columns = util.populate_column_dict(ac._locate_col) - return ac - - adapt_clause = ClauseAdapter.traverse - adapt_list = ClauseAdapter.copy_and_process - - def _wrap(self, local, wrapped): - def locate(col): - col = local(col) - return wrapped(col) - return locate - - def _locate_col(self, col): - c = self._corresponding_column(col, True) - if c is None: - c = self.adapt_clause(col) - - # anonymize labels in case they have a hardcoded name - if isinstance(c, Label): - c = c.label(None) - - # adapt_required used by eager loading to indicate that - # we don't trust a result row column that is not translated. - # this is to prevent a column from being interpreted as that - # of the child row in a self-referential scenario, see - # inheritance/test_basic.py->EagerTargetingTest.test_adapt_stringency - if self.adapt_required and c is col: - return None - - return c - - def adapted_row(self, row): - return AliasedRow(row, self.columns) - - def __getstate__(self): - d = self.__dict__.copy() - del d['columns'] - return d - - def __setstate__(self, state): - self.__dict__.update(state) - self.columns = util.PopulateDict(self._locate_col) - diff --git a/libs/sqlalchemy/sql/visitors.py b/libs/sqlalchemy/sql/visitors.py deleted file mode 100644 index d9ad04fc..00000000 --- a/libs/sqlalchemy/sql/visitors.py +++ /dev/null @@ -1,314 +0,0 @@ -# sql/visitors.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Visitor/traversal interface and library functions. - -SQLAlchemy schema and expression constructs rely on a Python-centric -version of the classic "visitor" pattern as the primary way in which -they apply functionality. The most common use of this pattern -is statement compilation, where individual expression classes match -up to rendering methods that produce a string result. Beyond this, -the visitor system is also used to inspect expressions for various -information and patterns, as well as for usage in -some kinds of expression transformation. Other kinds of transformation -use a non-visitor traversal system. - -For many examples of how the visit system is used, see the -sqlalchemy.sql.util and the sqlalchemy.sql.compiler modules. -For an introduction to clause adaption, see -http://techspot.zzzeek.org/2008/01/23/expression-transformations/ - -""" - -from collections import deque -from .. import util -import operator -from .. import exc - -__all__ = ['VisitableType', 'Visitable', 'ClauseVisitor', - 'CloningVisitor', 'ReplacingCloningVisitor', 'iterate', - 'iterate_depthfirst', 'traverse_using', 'traverse', - 'traverse_depthfirst', - 'cloned_traverse', 'replacement_traverse'] - - -class VisitableType(type): - """Metaclass which assigns a `_compiler_dispatch` method to classes - having a `__visit_name__` attribute. - - The _compiler_dispatch attribute becomes an instance method which - looks approximately like the following:: - - def _compiler_dispatch (self, visitor, **kw): - '''Look for an attribute named "visit_" + self.__visit_name__ - on the visitor, and call it with the same kw params.''' - visit_attr = 'visit_%s' % self.__visit_name__ - return getattr(visitor, visit_attr)(self, **kw) - - Classes having no __visit_name__ attribute will remain unaffected. - """ - def __init__(cls, clsname, bases, clsdict): - if clsname != 'Visitable' and \ - hasattr(cls, '__visit_name__'): - _generate_dispatch(cls) - - super(VisitableType, cls).__init__(clsname, bases, clsdict) - - -def _generate_dispatch(cls): - """Return an optimized visit dispatch function for the cls - for use by the compiler. - """ - if '__visit_name__' in cls.__dict__: - visit_name = cls.__visit_name__ - if isinstance(visit_name, str): - # There is an optimization opportunity here because the - # the string name of the class's __visit_name__ is known at - # this early stage (import time) so it can be pre-constructed. - getter = operator.attrgetter("visit_%s" % visit_name) - - def _compiler_dispatch(self, visitor, **kw): - try: - meth = getter(visitor) - except AttributeError: - raise exc.UnsupportedCompilationError(visitor, cls) - else: - return meth(self, **kw) - else: - # The optimization opportunity is lost for this case because the - # __visit_name__ is not yet a string. As a result, the visit - # string has to be recalculated with each compilation. - def _compiler_dispatch(self, visitor, **kw): - visit_attr = 'visit_%s' % self.__visit_name__ - try: - meth = getattr(visitor, visit_attr) - except AttributeError: - raise exc.UnsupportedCompilationError(visitor, cls) - else: - return meth(self, **kw) - - _compiler_dispatch.__doc__ = \ - """Look for an attribute named "visit_" + self.__visit_name__ - on the visitor, and call it with the same kw params. - """ - cls._compiler_dispatch = _compiler_dispatch - - -class Visitable(util.with_metaclass(VisitableType, object)): - """Base class for visitable objects, applies the - ``VisitableType`` metaclass. - - """ - - -class ClauseVisitor(object): - """Base class for visitor objects which can traverse using - the traverse() function. - - """ - - __traverse_options__ = {} - - def traverse_single(self, obj, **kw): - for v in self._visitor_iterator: - meth = getattr(v, "visit_%s" % obj.__visit_name__, None) - if meth: - return meth(obj, **kw) - - def iterate(self, obj): - """traverse the given expression structure, returning an iterator - of all elements. - - """ - return iterate(obj, self.__traverse_options__) - - def traverse(self, obj): - """traverse and visit the given expression structure.""" - - return traverse(obj, self.__traverse_options__, self._visitor_dict) - - @util.memoized_property - def _visitor_dict(self): - visitors = {} - - for name in dir(self): - if name.startswith('visit_'): - visitors[name[6:]] = getattr(self, name) - return visitors - - @property - def _visitor_iterator(self): - """iterate through this visitor and each 'chained' visitor.""" - - v = self - while v: - yield v - v = getattr(v, '_next', None) - - def chain(self, visitor): - """'chain' an additional ClauseVisitor onto this ClauseVisitor. - - the chained visitor will receive all visit events after this one. - - """ - tail = list(self._visitor_iterator)[-1] - tail._next = visitor - return self - - -class CloningVisitor(ClauseVisitor): - """Base class for visitor objects which can traverse using - the cloned_traverse() function. - - """ - - def copy_and_process(self, list_): - """Apply cloned traversal to the given list of elements, and return - the new list. - - """ - return [self.traverse(x) for x in list_] - - def traverse(self, obj): - """traverse and visit the given expression structure.""" - - return cloned_traverse( - obj, self.__traverse_options__, self._visitor_dict) - - -class ReplacingCloningVisitor(CloningVisitor): - """Base class for visitor objects which can traverse using - the replacement_traverse() function. - - """ - - def replace(self, elem): - """receive pre-copied elements during a cloning traversal. - - If the method returns a new element, the element is used - instead of creating a simple copy of the element. Traversal - will halt on the newly returned element if it is re-encountered. - """ - return None - - def traverse(self, obj): - """traverse and visit the given expression structure.""" - - def replace(elem): - for v in self._visitor_iterator: - e = v.replace(elem) - if e is not None: - return e - return replacement_traverse(obj, self.__traverse_options__, replace) - - -def iterate(obj, opts): - """traverse the given expression structure, returning an iterator. - - traversal is configured to be breadth-first. - - """ - stack = deque([obj]) - while stack: - t = stack.popleft() - yield t - for c in t.get_children(**opts): - stack.append(c) - - -def iterate_depthfirst(obj, opts): - """traverse the given expression structure, returning an iterator. - - traversal is configured to be depth-first. - - """ - stack = deque([obj]) - traversal = deque() - while stack: - t = stack.pop() - traversal.appendleft(t) - for c in t.get_children(**opts): - stack.append(c) - return iter(traversal) - - -def traverse_using(iterator, obj, visitors): - """visit the given expression structure using the given iterator of - objects. - - """ - for target in iterator: - meth = visitors.get(target.__visit_name__, None) - if meth: - meth(target) - return obj - - -def traverse(obj, opts, visitors): - """traverse and visit the given expression structure using the default - iterator. - - """ - return traverse_using(iterate(obj, opts), obj, visitors) - - -def traverse_depthfirst(obj, opts, visitors): - """traverse and visit the given expression structure using the - depth-first iterator. - - """ - return traverse_using(iterate_depthfirst(obj, opts), obj, visitors) - - -def cloned_traverse(obj, opts, visitors): - """clone the given expression structure, allowing - modifications by visitors.""" - - cloned = {} - stop_on = set(opts.get('stop_on', [])) - - def clone(elem): - if elem in stop_on: - return elem - else: - if id(elem) not in cloned: - cloned[id(elem)] = newelem = elem._clone() - newelem._copy_internals(clone=clone) - meth = visitors.get(newelem.__visit_name__, None) - if meth: - meth(newelem) - return cloned[id(elem)] - - if obj is not None: - obj = clone(obj) - return obj - - -def replacement_traverse(obj, opts, replace): - """clone the given expression structure, allowing element - replacement by a given replacement function.""" - - cloned = {} - stop_on = set([id(x) for x in opts.get('stop_on', [])]) - - def clone(elem, **kw): - if id(elem) in stop_on or \ - 'no_replacement_traverse' in elem._annotations: - return elem - else: - newelem = replace(elem) - if newelem is not None: - stop_on.add(id(newelem)) - return newelem - else: - if elem not in cloned: - cloned[elem] = newelem = elem._clone() - newelem._copy_internals(clone=clone, **kw) - return cloned[elem] - - if obj is not None: - obj = clone(obj, **opts) - return obj diff --git a/libs/sqlalchemy/types.py b/libs/sqlalchemy/types.py deleted file mode 100644 index 3994bd4a..00000000 --- a/libs/sqlalchemy/types.py +++ /dev/null @@ -1,77 +0,0 @@ -# types.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Compatiblity namespace for sqlalchemy.sql.types. - -""" - -__all__ = ['TypeEngine', 'TypeDecorator', 'UserDefinedType', - 'INT', 'CHAR', 'VARCHAR', 'NCHAR', 'NVARCHAR', 'TEXT', 'Text', - 'FLOAT', 'NUMERIC', 'REAL', 'DECIMAL', 'TIMESTAMP', 'DATETIME', - 'CLOB', 'BLOB', 'BINARY', 'VARBINARY', 'BOOLEAN', 'BIGINT', - 'SMALLINT', 'INTEGER', 'DATE', 'TIME', 'String', 'Integer', - 'SmallInteger', 'BigInteger', 'Numeric', 'Float', 'DateTime', - 'Date', 'Time', 'LargeBinary', 'Binary', 'Boolean', 'Unicode', - 'Concatenable', 'UnicodeText', 'PickleType', 'Interval', 'Enum'] - -from .sql.type_api import ( - adapt_type, - TypeEngine, - TypeDecorator, - Variant, - to_instance, - UserDefinedType -) -from .sql.sqltypes import ( - BIGINT, - BINARY, - BLOB, - BOOLEAN, - BigInteger, - Binary, - _Binary, - Boolean, - CHAR, - CLOB, - Concatenable, - DATE, - DATETIME, - DECIMAL, - Date, - DateTime, - Enum, - FLOAT, - Float, - INT, - INTEGER, - Integer, - Interval, - LargeBinary, - NCHAR, - NVARCHAR, - NullType, - NULLTYPE, - NUMERIC, - Numeric, - PickleType, - REAL, - SchemaType, - SMALLINT, - SmallInteger, - String, - STRINGTYPE, - TEXT, - TIME, - TIMESTAMP, - Text, - Time, - Unicode, - UnicodeText, - VARBINARY, - VARCHAR, - _type_map - ) - diff --git a/libs/sqlalchemy/util/__init__.py b/libs/sqlalchemy/util/__init__.py deleted file mode 100644 index eba64ed1..00000000 --- a/libs/sqlalchemy/util/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -# util/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -from .compat import callable, cmp, reduce, \ - threading, py3k, py33, py2k, jython, pypy, cpython, win32, \ - pickle, dottedgetter, parse_qsl, namedtuple, next, reraise, \ - raise_from_cause, text_type, string_types, int_types, binary_type, \ - quote_plus, with_metaclass, print_, itertools_filterfalse, u, ue, b,\ - unquote_plus, unquote, b64decode, b64encode, byte_buffer, itertools_filter,\ - iterbytes, StringIO, inspect_getargspec - -from ._collections import KeyedTuple, ImmutableContainer, immutabledict, \ - Properties, OrderedProperties, ImmutableProperties, OrderedDict, \ - OrderedSet, IdentitySet, OrderedIdentitySet, column_set, \ - column_dict, ordered_column_set, populate_column_dict, unique_list, \ - UniqueAppender, PopulateDict, EMPTY_SET, to_list, to_set, \ - to_column_set, update_copy, flatten_iterator, \ - LRUCache, ScopedRegistry, ThreadLocalRegistry, WeakSequence, \ - coerce_generator_arg - -from .langhelpers import iterate_attributes, class_hierarchy, \ - portable_instancemethod, unbound_method_to_callable, \ - getargspec_init, format_argspec_init, format_argspec_plus, \ - get_func_kwargs, get_cls_kwargs, decorator, as_interface, \ - memoized_property, memoized_instancemethod, md5_hex, \ - group_expirable_memoized_property, dependencies, decode_slice, \ - monkeypatch_proxied_specials, asbool, bool_or_str, coerce_kw_type,\ - duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\ - classproperty, set_creation_order, warn_exception, warn, NoneType,\ - constructor_copy, methods_equivalent, chop_traceback, asint,\ - generic_repr, counter, PluginLoader, hybridmethod, safe_reraise,\ - get_callable_argspec, only_once - -from .deprecations import warn_deprecated, warn_pending_deprecation, \ - deprecated, pending_deprecation, inject_docstring_text - -# things that used to be not always available, -# but are now as of current support Python versions -from collections import defaultdict -from functools import partial -from functools import update_wrapper -from contextlib import contextmanager diff --git a/libs/sqlalchemy/util/_collections.py b/libs/sqlalchemy/util/_collections.py deleted file mode 100644 index c0a24ba4..00000000 --- a/libs/sqlalchemy/util/_collections.py +++ /dev/null @@ -1,957 +0,0 @@ -# util/_collections.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Collection classes and helpers.""" - -from __future__ import absolute_import -import weakref -import operator -from .compat import threading, itertools_filterfalse -from . import py2k -import types - -EMPTY_SET = frozenset() - - -class KeyedTuple(tuple): - """``tuple`` subclass that adds labeled names. - - E.g.:: - - >>> k = KeyedTuple([1, 2, 3], labels=["one", "two", "three"]) - >>> k.one - 1 - >>> k.two - 2 - - Result rows returned by :class:`.Query` that contain multiple - ORM entities and/or column expressions make use of this - class to return rows. - - The :class:`.KeyedTuple` exhibits similar behavior to the - ``collections.namedtuple()`` construct provided in the Python - standard library, however is architected very differently. - Unlike ``collections.namedtuple()``, :class:`.KeyedTuple` is - does not rely on creation of custom subtypes in order to represent - a new series of keys, instead each :class:`.KeyedTuple` instance - receives its list of keys in place. The subtype approach - of ``collections.namedtuple()`` introduces significant complexity - and performance overhead, which is not necessary for the - :class:`.Query` object's use case. - - .. versionchanged:: 0.8 - Compatibility methods with ``collections.namedtuple()`` have been - added including :attr:`.KeyedTuple._fields` and - :meth:`.KeyedTuple._asdict`. - - .. seealso:: - - :ref:`ormtutorial_querying` - - """ - - def __new__(cls, vals, labels=None): - t = tuple.__new__(cls, vals) - t._labels = [] - if labels: - t.__dict__.update(zip(labels, vals)) - t._labels = labels - return t - - def keys(self): - """Return a list of string key names for this :class:`.KeyedTuple`. - - .. seealso:: - - :attr:`.KeyedTuple._fields` - - """ - - return [l for l in self._labels if l is not None] - - @property - def _fields(self): - """Return a tuple of string key names for this :class:`.KeyedTuple`. - - This method provides compatibility with ``collections.namedtuple()``. - - .. versionadded:: 0.8 - - .. seealso:: - - :meth:`.KeyedTuple.keys` - - """ - return tuple(self.keys()) - - def _asdict(self): - """Return the contents of this :class:`.KeyedTuple` as a dictionary. - - This method provides compatibility with ``collections.namedtuple()``, - with the exception that the dictionary returned is **not** ordered. - - .. versionadded:: 0.8 - - """ - return dict((key, self.__dict__[key]) for key in self.keys()) - - -class ImmutableContainer(object): - def _immutable(self, *arg, **kw): - raise TypeError("%s object is immutable" % self.__class__.__name__) - - __delitem__ = __setitem__ = __setattr__ = _immutable - - -class immutabledict(ImmutableContainer, dict): - - clear = pop = popitem = setdefault = \ - update = ImmutableContainer._immutable - - def __new__(cls, *args): - new = dict.__new__(cls) - dict.__init__(new, *args) - return new - - def __init__(self, *args): - pass - - def __reduce__(self): - return immutabledict, (dict(self), ) - - def union(self, d): - if not self: - return immutabledict(d) - else: - d2 = immutabledict(self) - dict.update(d2, d) - return d2 - - def __repr__(self): - return "immutabledict(%s)" % dict.__repr__(self) - - -class Properties(object): - """Provide a __getattr__/__setattr__ interface over a dict.""" - - def __init__(self, data): - self.__dict__['_data'] = data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(list(self._data.values())) - - def __add__(self, other): - return list(self) + list(other) - - def __setitem__(self, key, object): - self._data[key] = object - - def __getitem__(self, key): - return self._data[key] - - def __delitem__(self, key): - del self._data[key] - - def __setattr__(self, key, object): - self._data[key] = object - - def __getstate__(self): - return {'_data': self.__dict__['_data']} - - def __setstate__(self, state): - self.__dict__['_data'] = state['_data'] - - def __getattr__(self, key): - try: - return self._data[key] - except KeyError: - raise AttributeError(key) - - def __contains__(self, key): - return key in self._data - - def as_immutable(self): - """Return an immutable proxy for this :class:`.Properties`.""" - - return ImmutableProperties(self._data) - - def update(self, value): - self._data.update(value) - - def get(self, key, default=None): - if key in self: - return self[key] - else: - return default - - def keys(self): - return list(self._data) - - def values(self): - return list(self._data.values()) - - def items(self): - return list(self._data.items()) - - def has_key(self, key): - return key in self._data - - def clear(self): - self._data.clear() - - -class OrderedProperties(Properties): - """Provide a __getattr__/__setattr__ interface with an OrderedDict - as backing store.""" - def __init__(self): - Properties.__init__(self, OrderedDict()) - - -class ImmutableProperties(ImmutableContainer, Properties): - """Provide immutable dict/object attribute to an underlying dictionary.""" - - -class OrderedDict(dict): - """A dict that returns keys/values/items in the order they were added.""" - - def __init__(self, ____sequence=None, **kwargs): - self._list = [] - if ____sequence is None: - if kwargs: - self.update(**kwargs) - else: - self.update(____sequence, **kwargs) - - def clear(self): - self._list = [] - dict.clear(self) - - def copy(self): - return self.__copy__() - - def __copy__(self): - return OrderedDict(self) - - def sort(self, *arg, **kw): - self._list.sort(*arg, **kw) - - def update(self, ____sequence=None, **kwargs): - if ____sequence is not None: - if hasattr(____sequence, 'keys'): - for key in ____sequence.keys(): - self.__setitem__(key, ____sequence[key]) - else: - for key, value in ____sequence: - self[key] = value - if kwargs: - self.update(kwargs) - - def setdefault(self, key, value): - if key not in self: - self.__setitem__(key, value) - return value - else: - return self.__getitem__(key) - - def __iter__(self): - return iter(self._list) - - - if py2k: - def values(self): - return [self[key] for key in self._list] - - def keys(self): - return self._list - - def itervalues(self): - return iter([self[key] for key in self._list]) - - def iterkeys(self): - return iter(self) - - def iteritems(self): - return iter(self.items()) - - def items(self): - return [(key, self[key]) for key in self._list] - else: - def values(self): - #return (self[key] for key in self) - return (self[key] for key in self._list) - - def keys(self): - #return iter(self) - return iter(self._list) - - def items(self): - #return ((key, self[key]) for key in self) - return ((key, self[key]) for key in self._list) - - _debug_iter = False - if _debug_iter: - # normally disabled to reduce function call - # overhead - def __iter__(self): - len_ = len(self._list) - for item in self._list: - yield item - assert len_ == len(self._list), \ - "Dictionary changed size during iteration" - def values(self): - return (self[key] for key in self) - def keys(self): - return iter(self) - def items(self): - return ((key, self[key]) for key in self) - - - def __setitem__(self, key, object): - if key not in self: - try: - self._list.append(key) - except AttributeError: - # work around Python pickle loads() with - # dict subclass (seems to ignore __setstate__?) - self._list = [key] - dict.__setitem__(self, key, object) - - def __delitem__(self, key): - dict.__delitem__(self, key) - self._list.remove(key) - - def pop(self, key, *default): - present = key in self - value = dict.pop(self, key, *default) - if present: - self._list.remove(key) - return value - - def popitem(self): - item = dict.popitem(self) - self._list.remove(item[0]) - return item - - -class OrderedSet(set): - def __init__(self, d=None): - set.__init__(self) - self._list = [] - if d is not None: - self.update(d) - - def add(self, element): - if element not in self: - self._list.append(element) - set.add(self, element) - - def remove(self, element): - set.remove(self, element) - self._list.remove(element) - - def insert(self, pos, element): - if element not in self: - self._list.insert(pos, element) - set.add(self, element) - - def discard(self, element): - if element in self: - self._list.remove(element) - set.remove(self, element) - - def clear(self): - set.clear(self) - self._list = [] - - def __getitem__(self, key): - return self._list[key] - - def __iter__(self): - return iter(self._list) - - def __add__(self, other): - return self.union(other) - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self._list) - - __str__ = __repr__ - - def update(self, iterable): - for e in iterable: - if e not in self: - self._list.append(e) - set.add(self, e) - return self - - __ior__ = update - - def union(self, other): - result = self.__class__(self) - result.update(other) - return result - - __or__ = union - - def intersection(self, other): - other = set(other) - return self.__class__(a for a in self if a in other) - - __and__ = intersection - - def symmetric_difference(self, other): - other = set(other) - result = self.__class__(a for a in self if a not in other) - result.update(a for a in other if a not in self) - return result - - __xor__ = symmetric_difference - - def difference(self, other): - other = set(other) - return self.__class__(a for a in self if a not in other) - - __sub__ = difference - - def intersection_update(self, other): - other = set(other) - set.intersection_update(self, other) - self._list = [a for a in self._list if a in other] - return self - - __iand__ = intersection_update - - def symmetric_difference_update(self, other): - set.symmetric_difference_update(self, other) - self._list = [a for a in self._list if a in self] - self._list += [a for a in other._list if a in self] - return self - - __ixor__ = symmetric_difference_update - - def difference_update(self, other): - set.difference_update(self, other) - self._list = [a for a in self._list if a in self] - return self - - __isub__ = difference_update - - -class IdentitySet(object): - """A set that considers only object id() for uniqueness. - - This strategy has edge cases for builtin types- it's possible to have - two 'foo' strings in one of these sets, for example. Use sparingly. - - """ - - _working_set = set - - def __init__(self, iterable=None): - self._members = dict() - if iterable: - for o in iterable: - self.add(o) - - def add(self, value): - self._members[id(value)] = value - - def __contains__(self, value): - return id(value) in self._members - - def remove(self, value): - del self._members[id(value)] - - def discard(self, value): - try: - self.remove(value) - except KeyError: - pass - - def pop(self): - try: - pair = self._members.popitem() - return pair[1] - except KeyError: - raise KeyError('pop from an empty set') - - def clear(self): - self._members.clear() - - def __cmp__(self, other): - raise TypeError('cannot compare sets using cmp()') - - def __eq__(self, other): - if isinstance(other, IdentitySet): - return self._members == other._members - else: - return False - - def __ne__(self, other): - if isinstance(other, IdentitySet): - return self._members != other._members - else: - return True - - def issubset(self, iterable): - other = type(self)(iterable) - - if len(self) > len(other): - return False - for m in itertools_filterfalse(other._members.__contains__, - iter(self._members.keys())): - return False - return True - - def __le__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.issubset(other) - - def __lt__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return len(self) < len(other) and self.issubset(other) - - def issuperset(self, iterable): - other = type(self)(iterable) - - if len(self) < len(other): - return False - - for m in itertools_filterfalse(self._members.__contains__, - iter(other._members.keys())): - return False - return True - - def __ge__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.issuperset(other) - - def __gt__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return len(self) > len(other) and self.issuperset(other) - - def union(self, iterable): - result = type(self)() - # testlib.pragma exempt:__hash__ - members = self._member_id_tuples() - other = _iter_id(iterable) - result._members.update(self._working_set(members).union(other)) - return result - - def __or__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.union(other) - - def update(self, iterable): - self._members = self.union(iterable)._members - - def __ior__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - self.update(other) - return self - - def difference(self, iterable): - result = type(self)() - # testlib.pragma exempt:__hash__ - members = self._member_id_tuples() - other = _iter_id(iterable) - result._members.update(self._working_set(members).difference(other)) - return result - - def __sub__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.difference(other) - - def difference_update(self, iterable): - self._members = self.difference(iterable)._members - - def __isub__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - self.difference_update(other) - return self - - def intersection(self, iterable): - result = type(self)() - # testlib.pragma exempt:__hash__ - members = self._member_id_tuples() - other = _iter_id(iterable) - result._members.update(self._working_set(members).intersection(other)) - return result - - def __and__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.intersection(other) - - def intersection_update(self, iterable): - self._members = self.intersection(iterable)._members - - def __iand__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - self.intersection_update(other) - return self - - def symmetric_difference(self, iterable): - result = type(self)() - # testlib.pragma exempt:__hash__ - members = self._member_id_tuples() - other = _iter_id(iterable) - result._members.update( - self._working_set(members).symmetric_difference(other)) - return result - - def _member_id_tuples(self): - return ((id(v), v) for v in self._members.values()) - - def __xor__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - return self.symmetric_difference(other) - - def symmetric_difference_update(self, iterable): - self._members = self.symmetric_difference(iterable)._members - - def __ixor__(self, other): - if not isinstance(other, IdentitySet): - return NotImplemented - self.symmetric_difference(other) - return self - - def copy(self): - return type(self)(iter(self._members.values())) - - __copy__ = copy - - def __len__(self): - return len(self._members) - - def __iter__(self): - return iter(self._members.values()) - - def __hash__(self): - raise TypeError('set objects are unhashable') - - def __repr__(self): - return '%s(%r)' % (type(self).__name__, list(self._members.values())) - - -class WeakSequence(object): - def __init__(self, __elements=()): - self._storage = [ - weakref.ref(element, self._remove) for element in __elements - ] - - def append(self, item): - self._storage.append(weakref.ref(item, self._remove)) - - def _remove(self, ref): - self._storage.remove(ref) - - def __len__(self): - return len(self._storage) - - def __iter__(self): - return (obj for obj in - (ref() for ref in self._storage) if obj is not None) - - def __getitem__(self, index): - try: - obj = self._storage[index] - except KeyError: - raise IndexError("Index %s out of range" % index) - else: - return obj() - - -class OrderedIdentitySet(IdentitySet): - class _working_set(OrderedSet): - # a testing pragma: exempt the OIDS working set from the test suite's - # "never call the user's __hash__" assertions. this is a big hammer, - # but it's safe here: IDS operates on (id, instance) tuples in the - # working set. - __sa_hash_exempt__ = True - - def __init__(self, iterable=None): - IdentitySet.__init__(self) - self._members = OrderedDict() - if iterable: - for o in iterable: - self.add(o) - - -class PopulateDict(dict): - """A dict which populates missing values via a creation function. - - Note the creation function takes a key, unlike - collections.defaultdict. - - """ - - def __init__(self, creator): - self.creator = creator - - def __missing__(self, key): - self[key] = val = self.creator(key) - return val - -# Define collections that are capable of storing -# ColumnElement objects as hashable keys/elements. -# At this point, these are mostly historical, things -# used to be more complicated. -column_set = set -column_dict = dict -ordered_column_set = OrderedSet -populate_column_dict = PopulateDict - -def unique_list(seq, hashfunc=None): - seen = {} - if not hashfunc: - return [x for x in seq - if x not in seen - and not seen.__setitem__(x, True)] - else: - return [x for x in seq - if hashfunc(x) not in seen - and not seen.__setitem__(hashfunc(x), True)] - - -class UniqueAppender(object): - """Appends items to a collection ensuring uniqueness. - - Additional appends() of the same object are ignored. Membership is - determined by identity (``is a``) not equality (``==``). - """ - - def __init__(self, data, via=None): - self.data = data - self._unique = {} - if via: - self._data_appender = getattr(data, via) - elif hasattr(data, 'append'): - self._data_appender = data.append - elif hasattr(data, 'add'): - self._data_appender = data.add - - def append(self, item): - id_ = id(item) - if id_ not in self._unique: - self._data_appender(item) - self._unique[id_] = True - - def __iter__(self): - return iter(self.data) - -def coerce_generator_arg(arg): - if len(arg) == 1 and isinstance(arg[0], types.GeneratorType): - return list(arg[0]) - else: - return arg - -def to_list(x, default=None): - if x is None: - return default - if not isinstance(x, (list, tuple)): - return [x] - else: - return x - - -def to_set(x): - if x is None: - return set() - if not isinstance(x, set): - return set(to_list(x)) - else: - return x - - -def to_column_set(x): - if x is None: - return column_set() - if not isinstance(x, column_set): - return column_set(to_list(x)) - else: - return x - - -def update_copy(d, _new=None, **kw): - """Copy the given dict and update with the given values.""" - - d = d.copy() - if _new: - d.update(_new) - d.update(**kw) - return d - - -def flatten_iterator(x): - """Given an iterator of which further sub-elements may also be - iterators, flatten the sub-elements into a single iterator. - - """ - for elem in x: - if not isinstance(elem, str) and hasattr(elem, '__iter__'): - for y in flatten_iterator(elem): - yield y - else: - yield elem - - -class LRUCache(dict): - """Dictionary with 'squishy' removal of least - recently used items. - - """ - def __init__(self, capacity=100, threshold=.5): - self.capacity = capacity - self.threshold = threshold - self._counter = 0 - - def _inc_counter(self): - self._counter += 1 - return self._counter - - def __getitem__(self, key): - item = dict.__getitem__(self, key) - item[2] = self._inc_counter() - return item[1] - - def values(self): - return [i[1] for i in dict.values(self)] - - def setdefault(self, key, value): - if key in self: - return self[key] - else: - self[key] = value - return value - - def __setitem__(self, key, value): - item = dict.get(self, key) - if item is None: - item = [key, value, self._inc_counter()] - dict.__setitem__(self, key, item) - else: - item[1] = value - self._manage_size() - - def _manage_size(self): - while len(self) > self.capacity + self.capacity * self.threshold: - by_counter = sorted(dict.values(self), - key=operator.itemgetter(2), - reverse=True) - for item in by_counter[self.capacity:]: - try: - del self[item[0]] - except KeyError: - # if we couldnt find a key, most - # likely some other thread broke in - # on us. loop around and try again - break - - -class ScopedRegistry(object): - """A Registry that can store one or multiple instances of a single - class on the basis of a "scope" function. - - The object implements ``__call__`` as the "getter", so by - calling ``myregistry()`` the contained object is returned - for the current scope. - - :param createfunc: - a callable that returns a new object to be placed in the registry - - :param scopefunc: - a callable that will return a key to store/retrieve an object. - """ - - def __init__(self, createfunc, scopefunc): - """Construct a new :class:`.ScopedRegistry`. - - :param createfunc: A creation function that will generate - a new value for the current scope, if none is present. - - :param scopefunc: A function that returns a hashable - token representing the current scope (such as, current - thread identifier). - - """ - self.createfunc = createfunc - self.scopefunc = scopefunc - self.registry = {} - - def __call__(self): - key = self.scopefunc() - try: - return self.registry[key] - except KeyError: - return self.registry.setdefault(key, self.createfunc()) - - def has(self): - """Return True if an object is present in the current scope.""" - - return self.scopefunc() in self.registry - - def set(self, obj): - """Set the value forthe current scope.""" - - self.registry[self.scopefunc()] = obj - - def clear(self): - """Clear the current scope, if any.""" - - try: - del self.registry[self.scopefunc()] - except KeyError: - pass - - -class ThreadLocalRegistry(ScopedRegistry): - """A :class:`.ScopedRegistry` that uses a ``threading.local()`` - variable for storage. - - """ - def __init__(self, createfunc): - self.createfunc = createfunc - self.registry = threading.local() - - def __call__(self): - try: - return self.registry.value - except AttributeError: - val = self.registry.value = self.createfunc() - return val - - def has(self): - return hasattr(self.registry, "value") - - def set(self, obj): - self.registry.value = obj - - def clear(self): - try: - del self.registry.value - except AttributeError: - pass - - -def _iter_id(iterable): - """Generator: ((id(o), o) for o in iterable).""" - - for item in iterable: - yield id(item), item diff --git a/libs/sqlalchemy/util/compat.py b/libs/sqlalchemy/util/compat.py deleted file mode 100644 index f1346406..00000000 --- a/libs/sqlalchemy/util/compat.py +++ /dev/null @@ -1,215 +0,0 @@ -# util/compat.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Handle Python version/platform incompatibilities.""" - -import sys - -try: - import threading -except ImportError: - import dummy_threading as threading - -py33 = sys.version_info >= (3, 3) -py32 = sys.version_info >= (3, 2) -py3k = sys.version_info >= (3, 0) -py2k = sys.version_info < (3, 0) -jython = sys.platform.startswith('java') -pypy = hasattr(sys, 'pypy_version_info') -win32 = sys.platform.startswith('win') -cpython = not pypy and not jython # TODO: something better for this ? - -import collections -next = next - -if py3k: - import pickle -else: - try: - import cPickle as pickle - except ImportError: - import pickle - -ArgSpec = collections.namedtuple("ArgSpec", - ["args", "varargs", "keywords", "defaults"]) - -if py3k: - import builtins - - from inspect import getfullargspec as inspect_getfullargspec - from urllib.parse import quote_plus, unquote_plus, parse_qsl, quote, unquote - import configparser - from io import StringIO - - from io import BytesIO as byte_buffer - - def inspect_getargspec(func): - return ArgSpec( - *inspect_getfullargspec(func)[0:4] - ) - - string_types = str, - binary_type = bytes - text_type = str - int_types = int, - iterbytes = iter - - def u(s): - return s - - def ue(s): - return s - - def b(s): - return s.encode("latin-1") - - if py32: - callable = callable - else: - def callable(fn): - return hasattr(fn, '__call__') - - def cmp(a, b): - return (a > b) - (a < b) - - from functools import reduce - - print_ = getattr(builtins, "print") - - import_ = getattr(builtins, '__import__') - - import itertools - itertools_filterfalse = itertools.filterfalse - itertools_filter = filter - itertools_imap = map - - import base64 - def b64encode(x): - return base64.b64encode(x).decode('ascii') - def b64decode(x): - return base64.b64decode(x.encode('ascii')) - -else: - from inspect import getargspec as inspect_getfullargspec - inspect_getargspec = inspect_getfullargspec - from urllib import quote_plus, unquote_plus, quote, unquote - from urlparse import parse_qsl - import ConfigParser as configparser - from StringIO import StringIO - from cStringIO import StringIO as byte_buffer - - string_types = basestring, - binary_type = str - text_type = unicode - int_types = int, long - def iterbytes(buf): - return (ord(byte) for byte in buf) - - def u(s): - # this differs from what six does, which doesn't support non-ASCII - # strings - we only use u() with - # literal source strings, and all our source files with non-ascii - # in them (all are tests) are utf-8 encoded. - return unicode(s, "utf-8") - - def ue(s): - return unicode(s, "unicode_escape") - - def b(s): - return s - - def import_(*args): - if len(args) == 4: - args = args[0:3] + ([str(arg) for arg in args[3]],) - return __import__(*args) - - callable = callable - cmp = cmp - reduce = reduce - - import base64 - b64encode = base64.b64encode - b64decode = base64.b64decode - - def print_(*args, **kwargs): - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - for arg in enumerate(args): - if not isinstance(arg, basestring): - arg = str(arg) - fp.write(arg) - - import itertools - itertools_filterfalse = itertools.ifilterfalse - itertools_filter = itertools.ifilter - itertools_imap = itertools.imap - - -import time -if win32 or jython: - time_func = time.clock -else: - time_func = time.time - -from collections import namedtuple -from operator import attrgetter as dottedgetter - - -if py3k: - def reraise(tp, value, tb=None, cause=None): - if cause is not None: - value.__cause__ = cause - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - def raise_from_cause(exception, exc_info=None): - if exc_info is None: - exc_info = sys.exc_info() - exc_type, exc_value, exc_tb = exc_info - reraise(type(exception), exception, tb=exc_tb, cause=exc_value) -else: - exec("def reraise(tp, value, tb=None, cause=None):\n" - " raise tp, value, tb\n") - - def raise_from_cause(exception, exc_info=None): - # not as nice as that of Py3K, but at least preserves - # the code line where the issue occurred - if exc_info is None: - exc_info = sys.exc_info() - exc_type, exc_value, exc_tb = exc_info - reraise(type(exception), exception, tb=exc_tb) - -if py3k: - exec_ = getattr(builtins, 'exec') -else: - def exec_(func_text, globals_, lcl=None): - if lcl is None: - exec('exec func_text in globals_') - else: - exec('exec func_text in globals_, lcl') - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass. - - Drops the middle class upon creation. - - Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ - - """ - - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - diff --git a/libs/sqlalchemy/util/deprecations.py b/libs/sqlalchemy/util/deprecations.py deleted file mode 100644 index c8854dc3..00000000 --- a/libs/sqlalchemy/util/deprecations.py +++ /dev/null @@ -1,143 +0,0 @@ -# util/deprecations.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Helpers related to deprecation of functions, methods, classes, other -functionality.""" - -from .. import exc -import warnings -import re -from .langhelpers import decorator - - -def warn_deprecated(msg, stacklevel=3): - warnings.warn(msg, exc.SADeprecationWarning, stacklevel=stacklevel) - - -def warn_pending_deprecation(msg, stacklevel=3): - warnings.warn(msg, exc.SAPendingDeprecationWarning, stacklevel=stacklevel) - - -def deprecated(version, message=None, add_deprecation_to_docstring=True): - """Decorates a function and issues a deprecation warning on use. - - :param message: - If provided, issue message in the warning. A sensible default - is used if not provided. - - :param add_deprecation_to_docstring: - Default True. If False, the wrapped function's __doc__ is left - as-is. If True, the 'message' is prepended to the docs if - provided, or sensible default if message is omitted. - - """ - - if add_deprecation_to_docstring: - header = ".. deprecated:: %s %s" % \ - (version, (message or '')) - else: - header = None - - if message is None: - message = "Call to deprecated function %(func)s" - - def decorate(fn): - return _decorate_with_warning( - fn, exc.SADeprecationWarning, - message % dict(func=fn.__name__), header) - return decorate - - -def pending_deprecation(version, message=None, - add_deprecation_to_docstring=True): - """Decorates a function and issues a pending deprecation warning on use. - - :param version: - An approximate future version at which point the pending deprecation - will become deprecated. Not used in messaging. - - :param message: - If provided, issue message in the warning. A sensible default - is used if not provided. - - :param add_deprecation_to_docstring: - Default True. If False, the wrapped function's __doc__ is left - as-is. If True, the 'message' is prepended to the docs if - provided, or sensible default if message is omitted. - """ - - if add_deprecation_to_docstring: - header = ".. deprecated:: %s (pending) %s" % \ - (version, (message or '')) - else: - header = None - - if message is None: - message = "Call to deprecated function %(func)s" - - def decorate(fn): - return _decorate_with_warning( - fn, exc.SAPendingDeprecationWarning, - message % dict(func=fn.__name__), header) - return decorate - - -def _sanitize_restructured_text(text): - def repl(m): - type_, name = m.group(1, 2) - if type_ in ("func", "meth"): - name += "()" - return name - return re.sub(r'\:(\w+)\:`~?\.?(.+?)`', repl, text) - - -def _decorate_with_warning(func, wtype, message, docstring_header=None): - """Wrap a function with a warnings.warn and augmented docstring.""" - - message = _sanitize_restructured_text(message) - - @decorator - def warned(fn, *args, **kwargs): - warnings.warn(wtype(message), stacklevel=3) - return fn(*args, **kwargs) - - doc = func.__doc__ is not None and func.__doc__ or '' - if docstring_header is not None: - docstring_header %= dict(func=func.__name__) - - doc = inject_docstring_text(doc, docstring_header, 1) - - decorated = warned(func) - decorated.__doc__ = doc - return decorated - -import textwrap - -def _dedent_docstring(text): - split_text = text.split("\n", 1) - if len(split_text) == 1: - return text - else: - firstline, remaining = split_text - if not firstline.startswith(" "): - return firstline + "\n" + textwrap.dedent(remaining) - else: - return textwrap.dedent(text) - -def inject_docstring_text(doctext, injecttext, pos): - doctext = _dedent_docstring(doctext or "") - lines = doctext.split('\n') - injectlines = textwrap.dedent(injecttext).split("\n") - if injectlines[0]: - injectlines.insert(0, "") - - blanks = [num for num, line in enumerate(lines) if not line.strip()] - blanks.insert(0, 0) - - inject_pos = blanks[min(pos, len(blanks) - 1)] - - lines = lines[0:inject_pos] + injectlines + lines[inject_pos:] - return "\n".join(lines) diff --git a/libs/sqlalchemy/util/langhelpers.py b/libs/sqlalchemy/util/langhelpers.py deleted file mode 100644 index b6ca7eb2..00000000 --- a/libs/sqlalchemy/util/langhelpers.py +++ /dev/null @@ -1,1208 +0,0 @@ -# util/langhelpers.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Routines to help with the creation, loading and introspection of -modules, classes, hierarchies, attributes, functions, and methods. - -""" -import itertools -import inspect -import operator -import re -import sys -import types -import warnings -from functools import update_wrapper -from .. import exc -import hashlib -from . import compat -from . import _collections - -def md5_hex(x): - if compat.py3k: - x = x.encode('utf-8') - m = hashlib.md5() - m.update(x) - return m.hexdigest() - -class safe_reraise(object): - """Reraise an exception after invoking some - handler code. - - Stores the existing exception info before - invoking so that it is maintained across a potential - coroutine context switch. - - e.g.:: - - try: - sess.commit() - except: - with safe_reraise(): - sess.rollback() - - """ - - def __enter__(self): - self._exc_info = sys.exc_info() - - def __exit__(self, type_, value, traceback): - # see #2703 for notes - if type_ is None: - exc_type, exc_value, exc_tb = self._exc_info - self._exc_info = None # remove potential circular references - compat.reraise(exc_type, exc_value, exc_tb) - else: - self._exc_info = None # remove potential circular references - compat.reraise(type_, value, traceback) - -def decode_slice(slc): - """decode a slice object as sent to __getitem__. - - takes into account the 2.5 __index__() method, basically. - - """ - ret = [] - for x in slc.start, slc.stop, slc.step: - if hasattr(x, '__index__'): - x = x.__index__() - ret.append(x) - return tuple(ret) - -def _unique_symbols(used, *bases): - used = set(used) - for base in bases: - pool = itertools.chain((base,), - compat.itertools_imap(lambda i: base + str(i), - range(1000))) - for sym in pool: - if sym not in used: - used.add(sym) - yield sym - break - else: - raise NameError("exhausted namespace for symbol base %s" % base) - - -def decorator(target): - """A signature-matching decorator factory.""" - - def decorate(fn): - if not inspect.isfunction(fn): - raise Exception("not a decoratable function") - spec = compat.inspect_getfullargspec(fn) - names = tuple(spec[0]) + spec[1:3] + (fn.__name__,) - targ_name, fn_name = _unique_symbols(names, 'target', 'fn') - - metadata = dict(target=targ_name, fn=fn_name) - metadata.update(format_argspec_plus(spec, grouped=False)) - metadata['name'] = fn.__name__ - code = """\ -def %(name)s(%(args)s): - return %(target)s(%(fn)s, %(apply_kw)s) -""" % metadata - decorated = _exec_code_in_env(code, - {targ_name: target, fn_name: fn}, - fn.__name__) - decorated.__defaults__ = getattr(fn, 'im_func', fn).__defaults__ - return update_wrapper(decorated, fn) - return update_wrapper(decorate, target) - -def _exec_code_in_env(code, env, fn_name): - exec(code, env) - return env[fn_name] - -def public_factory(target, location): - """Produce a wrapping function for the given cls or classmethod. - - Rationale here is so that the __init__ method of the - class can serve as documentation for the function. - - """ - if isinstance(target, type): - fn = target.__init__ - callable_ = target - doc = "Construct a new :class:`.%s` object. \n\n"\ - "This constructor is mirrored as a public API function; see :func:`~%s` "\ - "for a full usage and argument description." % ( - target.__name__, location, ) - else: - fn = callable_ = target - doc = "This function is mirrored; see :func:`~%s` "\ - "for a description of arguments." % location - - location_name = location.split(".")[-1] - spec = compat.inspect_getfullargspec(fn) - del spec[0][0] - metadata = format_argspec_plus(spec, grouped=False) - metadata['name'] = location_name - code = """\ -def %(name)s(%(args)s): - return cls(%(apply_kw)s) -""" % metadata - env = {'cls': callable_, 'symbol': symbol} - exec(code, env) - decorated = env[location_name] - decorated.__doc__ = fn.__doc__ - if compat.py2k or hasattr(fn, '__func__'): - fn.__func__.__doc__ = doc - else: - fn.__doc__ = doc - return decorated - - -class PluginLoader(object): - - def __init__(self, group, auto_fn=None): - self.group = group - self.impls = {} - self.auto_fn = auto_fn - - def load(self, name): - if name in self.impls: - return self.impls[name]() - - if self.auto_fn: - loader = self.auto_fn(name) - if loader: - self.impls[name] = loader - return loader() - - try: - import pkg_resources - except ImportError: - pass - else: - for impl in pkg_resources.iter_entry_points( - self.group, name): - self.impls[name] = impl.load - return impl.load() - - raise exc.ArgumentError( - "Can't load plugin: %s:%s" % - (self.group, name)) - - def register(self, name, modulepath, objname): - def load(): - mod = compat.import_(modulepath) - for token in modulepath.split(".")[1:]: - mod = getattr(mod, token) - return getattr(mod, objname) - self.impls[name] = load - - -def get_cls_kwargs(cls, _set=None): - """Return the full set of inherited kwargs for the given `cls`. - - Probes a class's __init__ method, collecting all named arguments. If the - __init__ defines a \**kwargs catch-all, then the constructor is presumed to - pass along unrecognized keywords to it's base classes, and the collection - process is repeated recursively on each of the bases. - - Uses a subset of inspect.getargspec() to cut down on method overhead. - No anonymous tuple arguments please ! - - """ - toplevel = _set == None - if toplevel: - _set = set() - - ctr = cls.__dict__.get('__init__', False) - - has_init = ctr and isinstance(ctr, types.FunctionType) and \ - isinstance(ctr.__code__, types.CodeType) - - if has_init: - names, has_kw = inspect_func_args(ctr) - _set.update(names) - - if not has_kw and not toplevel: - return None - - if not has_init or has_kw: - for c in cls.__bases__: - if get_cls_kwargs(c, _set) is None: - break - - _set.discard('self') - return _set - - - -try: - # TODO: who doesn't have this constant? - from inspect import CO_VARKEYWORDS - - def inspect_func_args(fn): - co = fn.__code__ - nargs = co.co_argcount - names = co.co_varnames - args = list(names[:nargs]) - has_kw = bool(co.co_flags & CO_VARKEYWORDS) - return args, has_kw - -except ImportError: - def inspect_func_args(fn): - names, _, has_kw, _ = inspect.getargspec(fn) - return names, bool(has_kw) - - -def get_func_kwargs(func): - """Return the set of legal kwargs for the given `func`. - - Uses getargspec so is safe to call for methods, functions, - etc. - - """ - - return compat.inspect_getargspec(func)[0] - -def get_callable_argspec(fn, no_self=False): - if isinstance(fn, types.FunctionType): - return compat.inspect_getargspec(fn) - elif isinstance(fn, types.MethodType) and no_self: - spec = compat.inspect_getargspec(fn.__func__) - return compat.ArgSpec(spec.args[1:], spec.varargs, spec.keywords, spec.defaults) - elif hasattr(fn, '__func__'): - return compat.inspect_getargspec(fn.__func__) - elif hasattr(fn, '__call__') and \ - not hasattr(fn.__call__, '__call__'): # functools.partial does this; - # not much we can do - return get_callable_argspec(fn.__call__) - else: - raise ValueError("Can't inspect function: %s" % fn) - -def format_argspec_plus(fn, grouped=True): - """Returns a dictionary of formatted, introspected function arguments. - - A enhanced variant of inspect.formatargspec to support code generation. - - fn - An inspectable callable or tuple of inspect getargspec() results. - grouped - Defaults to True; include (parens, around, argument) lists - - Returns: - - args - Full inspect.formatargspec for fn - self_arg - The name of the first positional argument, varargs[0], or None - if the function defines no positional arguments. - apply_pos - args, re-written in calling rather than receiving syntax. Arguments are - passed positionally. - apply_kw - Like apply_pos, except keyword-ish args are passed as keywords. - - Example:: - - >>> format_argspec_plus(lambda self, a, b, c=3, **d: 123) - {'args': '(self, a, b, c=3, **d)', - 'self_arg': 'self', - 'apply_kw': '(self, a, b, c=c, **d)', - 'apply_pos': '(self, a, b, c, **d)'} - - """ - if compat.callable(fn): - spec = compat.inspect_getfullargspec(fn) - else: - # we accept an existing argspec... - spec = fn - args = inspect.formatargspec(*spec) - if spec[0]: - self_arg = spec[0][0] - elif spec[1]: - self_arg = '%s[0]' % spec[1] - else: - self_arg = None - - if compat.py3k: - apply_pos = inspect.formatargspec(spec[0], spec[1], - spec[2], None, spec[4]) - num_defaults = 0 - if spec[3]: - num_defaults += len(spec[3]) - if spec[4]: - num_defaults += len(spec[4]) - name_args = spec[0] + spec[4] - else: - apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2]) - num_defaults = 0 - if spec[3]: - num_defaults += len(spec[3]) - name_args = spec[0] - - if num_defaults: - defaulted_vals = name_args[0 - num_defaults:] - else: - defaulted_vals = () - - apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], - defaulted_vals, - formatvalue=lambda x: '=' + x) - if grouped: - return dict(args=args, self_arg=self_arg, - apply_pos=apply_pos, apply_kw=apply_kw) - else: - return dict(args=args[1:-1], self_arg=self_arg, - apply_pos=apply_pos[1:-1], apply_kw=apply_kw[1:-1]) - - -def format_argspec_init(method, grouped=True): - """format_argspec_plus with considerations for typical __init__ methods - - Wraps format_argspec_plus with error handling strategies for typical - __init__ cases:: - - object.__init__ -> (self) - other unreflectable (usually C) -> (self, *args, **kwargs) - - """ - try: - return format_argspec_plus(method, grouped=grouped) - except TypeError: - if method is object.__init__: - args = grouped and '(self)' or 'self' - else: - args = (grouped and '(self, *args, **kwargs)' - or 'self, *args, **kwargs') - return dict(self_arg='self', args=args, apply_pos=args, apply_kw=args) - - -def getargspec_init(method): - """inspect.getargspec with considerations for typical __init__ methods - - Wraps inspect.getargspec with error handling for typical __init__ cases:: - - object.__init__ -> (self) - other unreflectable (usually C) -> (self, *args, **kwargs) - - """ - try: - return inspect.getargspec(method) - except TypeError: - if method is object.__init__: - return (['self'], None, None, None) - else: - return (['self'], 'args', 'kwargs', None) - - -def unbound_method_to_callable(func_or_cls): - """Adjust the incoming callable such that a 'self' argument is not - required. - - """ - - if isinstance(func_or_cls, types.MethodType) and not func_or_cls.__self__: - return func_or_cls.__func__ - else: - return func_or_cls - - -def generic_repr(obj, additional_kw=(), to_inspect=None): - """Produce a __repr__() based on direct association of the __init__() - specification vs. same-named attributes present. - - """ - if to_inspect is None: - to_inspect = [obj] - else: - to_inspect = _collections.to_list(to_inspect) - - missing = object() - - pos_args = [] - kw_args = _collections.OrderedDict() - vargs = None - for i, insp in enumerate(to_inspect): - try: - (_args, _vargs, vkw, defaults) = \ - inspect.getargspec(insp.__init__) - except TypeError: - continue - else: - default_len = defaults and len(defaults) or 0 - if i == 0: - if _vargs: - vargs = _vargs - if default_len: - pos_args.extend(_args[1:-default_len]) - else: - pos_args.extend(_args[1:]) - else: - kw_args.update([ - (arg, missing) for arg in _args[1:-default_len] - ]) - - if default_len: - kw_args.update([ - (arg, default) - for arg, default - in zip(_args[-default_len:], defaults) - ]) - output = [] - - output.extend(repr(getattr(obj, arg, None)) for arg in pos_args) - - if vargs is not None and hasattr(obj, vargs): - output.extend([repr(val) for val in getattr(obj, vargs)]) - - for arg, defval in kw_args.items(): - try: - val = getattr(obj, arg, missing) - if val is not missing and val != defval: - output.append('%s=%r' % (arg, val)) - except: - pass - - if additional_kw: - for arg, defval in additional_kw: - try: - val = getattr(obj, arg, missing) - if val is not missing and val != defval: - output.append('%s=%r' % (arg, val)) - except: - pass - - return "%s(%s)" % (obj.__class__.__name__, ", ".join(output)) - - -class portable_instancemethod(object): - """Turn an instancemethod into a (parent, name) pair - to produce a serializable callable. - - """ - def __init__(self, meth): - self.target = meth.__self__ - self.name = meth.__name__ - - def __call__(self, *arg, **kw): - return getattr(self.target, self.name)(*arg, **kw) - - -def class_hierarchy(cls): - """Return an unordered sequence of all classes related to cls. - - Traverses diamond hierarchies. - - Fibs slightly: subclasses of builtin types are not returned. Thus - class_hierarchy(class A(object)) returns (A, object), not A plus every - class systemwide that derives from object. - - Old-style classes are discarded and hierarchies rooted on them - will not be descended. - - """ - if compat.py2k: - if isinstance(cls, types.ClassType): - return list() - - hier = set([cls]) - process = list(cls.__mro__) - while process: - c = process.pop() - if compat.py2k: - if isinstance(c, types.ClassType): - continue - bases = (_ for _ in c.__bases__ - if _ not in hier and not isinstance(_, types.ClassType)) - else: - bases = (_ for _ in c.__bases__ if _ not in hier) - - for b in bases: - process.append(b) - hier.add(b) - - if compat.py3k: - if c.__module__ == 'builtins' or not hasattr(c, '__subclasses__'): - continue - else: - if c.__module__ == '__builtin__' or not hasattr(c, '__subclasses__'): - continue - - for s in [_ for _ in c.__subclasses__() if _ not in hier]: - process.append(s) - hier.add(s) - return list(hier) - - -def iterate_attributes(cls): - """iterate all the keys and attributes associated - with a class, without using getattr(). - - Does not use getattr() so that class-sensitive - descriptors (i.e. property.__get__()) are not called. - - """ - keys = dir(cls) - for key in keys: - for c in cls.__mro__: - if key in c.__dict__: - yield (key, c.__dict__[key]) - break - - -def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, - name='self.proxy', from_instance=None): - """Automates delegation of __specials__ for a proxying type.""" - - if only: - dunders = only - else: - if skip is None: - skip = ('__slots__', '__del__', '__getattribute__', - '__metaclass__', '__getstate__', '__setstate__') - dunders = [m for m in dir(from_cls) - if (m.startswith('__') and m.endswith('__') and - not hasattr(into_cls, m) and m not in skip)] - - for method in dunders: - try: - fn = getattr(from_cls, method) - if not hasattr(fn, '__call__'): - continue - fn = getattr(fn, 'im_func', fn) - except AttributeError: - continue - try: - spec = inspect.getargspec(fn) - fn_args = inspect.formatargspec(spec[0]) - d_args = inspect.formatargspec(spec[0][1:]) - except TypeError: - fn_args = '(self, *args, **kw)' - d_args = '(*args, **kw)' - - py = ("def %(method)s%(fn_args)s: " - "return %(name)s.%(method)s%(d_args)s" % locals()) - - env = from_instance is not None and {name: from_instance} or {} - compat.exec_(py, env) - try: - env[method].__defaults__ = fn.__defaults__ - except AttributeError: - pass - setattr(into_cls, method, env[method]) - - -def methods_equivalent(meth1, meth2): - """Return True if the two methods are the same implementation.""" - - return getattr(meth1, '__func__', meth1) is getattr(meth2, '__func__', meth2) - - -def as_interface(obj, cls=None, methods=None, required=None): - """Ensure basic interface compliance for an instance or dict of callables. - - Checks that ``obj`` implements public methods of ``cls`` or has members - listed in ``methods``. If ``required`` is not supplied, implementing at - least one interface method is sufficient. Methods present on ``obj`` that - are not in the interface are ignored. - - If ``obj`` is a dict and ``dict`` does not meet the interface - requirements, the keys of the dictionary are inspected. Keys present in - ``obj`` that are not in the interface will raise TypeErrors. - - Raises TypeError if ``obj`` does not meet the interface criteria. - - In all passing cases, an object with callable members is returned. In the - simple case, ``obj`` is returned as-is; if dict processing kicks in then - an anonymous class is returned. - - obj - A type, instance, or dictionary of callables. - cls - Optional, a type. All public methods of cls are considered the - interface. An ``obj`` instance of cls will always pass, ignoring - ``required``.. - methods - Optional, a sequence of method names to consider as the interface. - required - Optional, a sequence of mandatory implementations. If omitted, an - ``obj`` that provides at least one interface method is considered - sufficient. As a convenience, required may be a type, in which case - all public methods of the type are required. - - """ - if not cls and not methods: - raise TypeError('a class or collection of method names are required') - - if isinstance(cls, type) and isinstance(obj, cls): - return obj - - interface = set(methods or [m for m in dir(cls) if not m.startswith('_')]) - implemented = set(dir(obj)) - - complies = operator.ge - if isinstance(required, type): - required = interface - elif not required: - required = set() - complies = operator.gt - else: - required = set(required) - - if complies(implemented.intersection(interface), required): - return obj - - # No dict duck typing here. - if not type(obj) is dict: - qualifier = complies is operator.gt and 'any of' or 'all of' - raise TypeError("%r does not implement %s: %s" % ( - obj, qualifier, ', '.join(interface))) - - class AnonymousInterface(object): - """A callable-holding shell.""" - - if cls: - AnonymousInterface.__name__ = 'Anonymous' + cls.__name__ - found = set() - - for method, impl in dictlike_iteritems(obj): - if method not in interface: - raise TypeError("%r: unknown in this interface" % method) - if not compat.callable(impl): - raise TypeError("%r=%r is not callable" % (method, impl)) - setattr(AnonymousInterface, method, staticmethod(impl)) - found.add(method) - - if complies(found, required): - return AnonymousInterface - - raise TypeError("dictionary does not contain required keys %s" % - ', '.join(required - found)) - - -class memoized_property(object): - """A read-only @property that is only evaluated once.""" - def __init__(self, fget, doc=None): - self.fget = fget - self.__doc__ = doc or fget.__doc__ - self.__name__ = fget.__name__ - - def __get__(self, obj, cls): - if obj is None: - return self - obj.__dict__[self.__name__] = result = self.fget(obj) - return result - - def _reset(self, obj): - memoized_property.reset(obj, self.__name__) - - @classmethod - def reset(cls, obj, name): - obj.__dict__.pop(name, None) - - -class memoized_instancemethod(object): - """Decorate a method memoize its return value. - - Best applied to no-arg methods: memoization is not sensitive to - argument values, and will always return the same value even when - called with different arguments. - - """ - def __init__(self, fget, doc=None): - self.fget = fget - self.__doc__ = doc or fget.__doc__ - self.__name__ = fget.__name__ - - def __get__(self, obj, cls): - if obj is None: - return self - - def oneshot(*args, **kw): - result = self.fget(obj, *args, **kw) - memo = lambda *a, **kw: result - memo.__name__ = self.__name__ - memo.__doc__ = self.__doc__ - obj.__dict__[self.__name__] = memo - return result - - oneshot.__name__ = self.__name__ - oneshot.__doc__ = self.__doc__ - return oneshot - - -class group_expirable_memoized_property(object): - """A family of @memoized_properties that can be expired in tandem.""" - - def __init__(self, attributes=()): - self.attributes = [] - if attributes: - self.attributes.extend(attributes) - - def expire_instance(self, instance): - """Expire all memoized properties for *instance*.""" - stash = instance.__dict__ - for attribute in self.attributes: - stash.pop(attribute, None) - - def __call__(self, fn): - self.attributes.append(fn.__name__) - return memoized_property(fn) - - def method(self, fn): - self.attributes.append(fn.__name__) - return memoized_instancemethod(fn) - - - -def dependency_for(modulename): - def decorate(obj): - # TODO: would be nice to improve on this import silliness, - # unfortunately importlib doesn't work that great either - tokens = modulename.split(".") - mod = compat.import_(".".join(tokens[0:-1]), globals(), locals(), tokens[-1]) - mod = getattr(mod, tokens[-1]) - setattr(mod, obj.__name__, obj) - return obj - return decorate - -class dependencies(object): - """Apply imported dependencies as arguments to a function. - - E.g.:: - - @util.dependencies( - "sqlalchemy.sql.widget", - "sqlalchemy.engine.default" - ); - def some_func(self, widget, default, arg1, arg2, **kw): - # ... - - Rationale is so that the impact of a dependency cycle can be - associated directly with the few functions that cause the cycle, - and not pollute the module-level namespace. - - """ - - def __init__(self, *deps): - self.import_deps = [] - for dep in deps: - tokens = dep.split(".") - self.import_deps.append( - dependencies._importlater( - ".".join(tokens[0:-1]), - tokens[-1] - ) - ) - - def __call__(self, fn): - import_deps = self.import_deps - spec = compat.inspect_getfullargspec(fn) - - spec_zero = list(spec[0]) - hasself = spec_zero[0] in ('self', 'cls') - - for i in range(len(import_deps)): - spec[0][i + (1 if hasself else 0)] = "import_deps[%r]" % i - - inner_spec = format_argspec_plus(spec, grouped=False) - - for impname in import_deps: - del spec_zero[1 if hasself else 0] - spec[0][:] = spec_zero - - outer_spec = format_argspec_plus(spec, grouped=False) - - code = 'lambda %(args)s: fn(%(apply_kw)s)' % { - "args": outer_spec['args'], - "apply_kw": inner_spec['apply_kw'] - } - - decorated = eval(code, locals()) - decorated.__defaults__ = getattr(fn, 'im_func', fn).__defaults__ - return update_wrapper(decorated, fn) - - @classmethod - def resolve_all(cls, path): - for m in list(dependencies._unresolved): - if m._full_path.startswith(path): - m._resolve() - - _unresolved = set() - _by_key = {} - - class _importlater(object): - _unresolved = set() - - _by_key = {} - - def __new__(cls, path, addtl): - key = path + "." + addtl - if key in dependencies._by_key: - return dependencies._by_key[key] - else: - dependencies._by_key[key] = imp = object.__new__(cls) - return imp - - def __init__(self, path, addtl): - self._il_path = path - self._il_addtl = addtl - dependencies._unresolved.add(self) - - - @property - def _full_path(self): - return self._il_path + "." + self._il_addtl - - @memoized_property - def module(self): - if self in dependencies._unresolved: - raise ImportError( - "importlater.resolve_all() hasn't " - "been called (this is %s %s)" - % (self._il_path, self._il_addtl)) - - return getattr(self._initial_import, self._il_addtl) - - def _resolve(self): - dependencies._unresolved.discard(self) - self._initial_import = compat.import_( - self._il_path, globals(), locals(), - [self._il_addtl]) - - def __getattr__(self, key): - if key == 'module': - raise ImportError("Could not resolve module %s" - % self._full_path) - try: - attr = getattr(self.module, key) - except AttributeError: - raise AttributeError( - "Module %s has no attribute '%s'" % - (self._full_path, key) - ) - self.__dict__[key] = attr - return attr - - -# from paste.deploy.converters -def asbool(obj): - if isinstance(obj, compat.string_types): - obj = obj.strip().lower() - if obj in ['true', 'yes', 'on', 'y', 't', '1']: - return True - elif obj in ['false', 'no', 'off', 'n', 'f', '0']: - return False - else: - raise ValueError("String is not true/false: %r" % obj) - return bool(obj) - - -def bool_or_str(*text): - """Return a callable that will evaulate a string as - boolean, or one of a set of "alternate" string values. - - """ - def bool_or_value(obj): - if obj in text: - return obj - else: - return asbool(obj) - return bool_or_value - - -def asint(value): - """Coerce to integer.""" - - if value is None: - return value - return int(value) - - -def coerce_kw_type(kw, key, type_, flexi_bool=True): - """If 'key' is present in dict 'kw', coerce its value to type 'type\_' if - necessary. If 'flexi_bool' is True, the string '0' is considered false - when coercing to boolean. - """ - - if key in kw and type(kw[key]) is not type_ and kw[key] is not None: - if type_ is bool and flexi_bool: - kw[key] = asbool(kw[key]) - else: - kw[key] = type_(kw[key]) - - -def constructor_copy(obj, cls, **kw): - """Instantiate cls using the __dict__ of obj as constructor arguments. - - Uses inspect to match the named arguments of ``cls``. - - """ - - names = get_cls_kwargs(cls) - kw.update((k, obj.__dict__[k]) for k in names if k in obj.__dict__) - return cls(**kw) - - -def counter(): - """Return a threadsafe counter function.""" - - lock = compat.threading.Lock() - counter = itertools.count(1) - - # avoid the 2to3 "next" transformation... - def _next(): - lock.acquire() - try: - return next(counter) - finally: - lock.release() - - return _next - - -def duck_type_collection(specimen, default=None): - """Given an instance or class, guess if it is or is acting as one of - the basic collection types: list, set and dict. If the __emulates__ - property is present, return that preferentially. - """ - - if hasattr(specimen, '__emulates__'): - # canonicalize set vs sets.Set to a standard: the builtin set - if (specimen.__emulates__ is not None and - issubclass(specimen.__emulates__, set)): - return set - else: - return specimen.__emulates__ - - isa = isinstance(specimen, type) and issubclass or isinstance - if isa(specimen, list): - return list - elif isa(specimen, set): - return set - elif isa(specimen, dict): - return dict - - if hasattr(specimen, 'append'): - return list - elif hasattr(specimen, 'add'): - return set - elif hasattr(specimen, 'set'): - return dict - else: - return default - - -def assert_arg_type(arg, argtype, name): - if isinstance(arg, argtype): - return arg - else: - if isinstance(argtype, tuple): - raise exc.ArgumentError( - "Argument '%s' is expected to be one of type %s, got '%s'" % - (name, ' or '.join("'%s'" % a for a in argtype), type(arg))) - else: - raise exc.ArgumentError( - "Argument '%s' is expected to be of type '%s', got '%s'" % - (name, argtype, type(arg))) - - -def dictlike_iteritems(dictlike): - """Return a (key, value) iterator for almost any dict-like object.""" - - if compat.py3k: - if hasattr(dictlike, 'items'): - return list(dictlike.items()) - else: - if hasattr(dictlike, 'iteritems'): - return dictlike.iteritems() - elif hasattr(dictlike, 'items'): - return iter(dictlike.items()) - - getter = getattr(dictlike, '__getitem__', getattr(dictlike, 'get', None)) - if getter is None: - raise TypeError( - "Object '%r' is not dict-like" % dictlike) - - if hasattr(dictlike, 'iterkeys'): - def iterator(): - for key in dictlike.iterkeys(): - yield key, getter(key) - return iterator() - elif hasattr(dictlike, 'keys'): - return iter((key, getter(key)) for key in dictlike.keys()) - else: - raise TypeError( - "Object '%r' is not dict-like" % dictlike) - - -class classproperty(property): - """A decorator that behaves like @property except that operates - on classes rather than instances. - - The decorator is currently special when using the declarative - module, but note that the - :class:`~.sqlalchemy.ext.declarative.declared_attr` - decorator should be used for this purpose with declarative. - - """ - - def __init__(self, fget, *arg, **kw): - super(classproperty, self).__init__(fget, *arg, **kw) - self.__doc__ = fget.__doc__ - - def __get__(desc, self, cls): - return desc.fget(cls) - - -class hybridmethod(object): - """Decorate a function as cls- or instance- level.""" - def __init__(self, func, expr=None): - self.func = func - - def __get__(self, instance, owner): - if instance is None: - return self.func.__get__(owner, owner.__class__) - else: - return self.func.__get__(instance, owner) - - -class _symbol(int): - def __new__(self, name, doc=None, canonical=None): - """Construct a new named symbol.""" - assert isinstance(name, compat.string_types) - if canonical is None: - canonical = hash(name) - v = int.__new__(_symbol, canonical) - v.name = name - if doc: - v.__doc__ = doc - return v - - def __reduce__(self): - return symbol, (self.name, "x", int(self)) - - def __str__(self): - return repr(self) - - def __repr__(self): - return "symbol(%r)" % self.name - -_symbol.__name__ = 'symbol' - - -class symbol(object): - """A constant symbol. - - >>> symbol('foo') is symbol('foo') - True - >>> symbol('foo') - - - A slight refinement of the MAGICCOOKIE=object() pattern. The primary - advantage of symbol() is its repr(). They are also singletons. - - Repeated calls of symbol('name') will all return the same instance. - - The optional ``doc`` argument assigns to ``__doc__``. This - is strictly so that Sphinx autoattr picks up the docstring we want - (it doesn't appear to pick up the in-module docstring if the datamember - is in a different module - autoattribute also blows up completely). - If Sphinx fixes/improves this then we would no longer need - ``doc`` here. - - """ - symbols = {} - _lock = compat.threading.Lock() - - def __new__(cls, name, doc=None, canonical=None): - cls._lock.acquire() - try: - sym = cls.symbols.get(name) - if sym is None: - cls.symbols[name] = sym = _symbol(name, doc, canonical) - return sym - finally: - symbol._lock.release() - - -_creation_order = 1 - - -def set_creation_order(instance): - """Assign a '_creation_order' sequence to the given instance. - - This allows multiple instances to be sorted in order of creation - (typically within a single thread; the counter is not particularly - threadsafe). - - """ - global _creation_order - instance._creation_order = _creation_order - _creation_order += 1 - - -def warn_exception(func, *args, **kwargs): - """executes the given function, catches all exceptions and converts to - a warning. - - """ - try: - return func(*args, **kwargs) - except: - warn("%s('%s') ignored" % sys.exc_info()[0:2]) - - -def warn(msg, stacklevel=3): - """Issue a warning. - - If msg is a string, :class:`.exc.SAWarning` is used as - the category. - - .. note:: - - This function is swapped out when the test suite - runs, with a compatible version that uses - warnings.warn_explicit, so that the warnings registry can - be controlled. - - """ - if isinstance(msg, compat.string_types): - warnings.warn(msg, exc.SAWarning, stacklevel=stacklevel) - else: - warnings.warn(msg, stacklevel=stacklevel) - - -def only_once(fn): - """Decorate the given function to be a no-op after it is called exactly - once.""" - - once = [fn] - def go(*arg, **kw): - if once: - once_fn = once.pop() - return once_fn(*arg, **kw) - - return update_wrapper(go, fn) - - -_SQLA_RE = re.compile(r'sqlalchemy/([a-z_]+/){0,2}[a-z_]+\.py') -_UNITTEST_RE = re.compile(r'unit(?:2|test2?/)') - -def chop_traceback(tb, exclude_prefix=_UNITTEST_RE, exclude_suffix=_SQLA_RE): - """Chop extraneous lines off beginning and end of a traceback. - - :param tb: - a list of traceback lines as returned by ``traceback.format_stack()`` - - :param exclude_prefix: - a regular expression object matching lines to skip at beginning of ``tb`` - - :param exclude_suffix: - a regular expression object matching lines to skip at end of ``tb`` - """ - start = 0 - end = len(tb) - 1 - while start <= end and exclude_prefix.search(tb[start]): - start += 1 - while start <= end and exclude_suffix.search(tb[end]): - end -= 1 - return tb[start:end + 1] - -NoneType = type(None) diff --git a/libs/sqlalchemy/util/queue.py b/libs/sqlalchemy/util/queue.py deleted file mode 100644 index 82ff55a5..00000000 --- a/libs/sqlalchemy/util/queue.py +++ /dev/null @@ -1,236 +0,0 @@ -# util/queue.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""An adaptation of Py2.3/2.4's Queue module which supports reentrant -behavior, using RLock instead of Lock for its mutex object. The -Queue object is used exclusively by the sqlalchemy.pool.QueuePool -class. - -This is to support the connection pool's usage of weakref callbacks to return -connections to the underlying Queue, which can in extremely -rare cases be invoked within the ``get()`` method of the Queue itself, -producing a ``put()`` inside the ``get()`` and therefore a reentrant -condition. - -An additional change includes a special "abort" method which can be used -to immediately raise a special exception for threads that are blocking -on get(). This is to accommodate a rare race condition that can occur -within QueuePool. - -""" - -from collections import deque -from time import time as _time -from .compat import threading - - -__all__ = ['Empty', 'Full', 'Queue', 'SAAbort'] - - -class Empty(Exception): - "Exception raised by Queue.get(block=0)/get_nowait()." - - pass - - -class Full(Exception): - "Exception raised by Queue.put(block=0)/put_nowait()." - - pass - - -class SAAbort(Exception): - "Special SQLA exception to abort waiting" - def __init__(self, context): - self.context = context - - -class Queue: - def __init__(self, maxsize=0): - """Initialize a queue object with a given maximum size. - - If `maxsize` is <= 0, the queue size is infinite. - """ - - self._init(maxsize) - # mutex must be held whenever the queue is mutating. All methods - # that acquire mutex must release it before returning. mutex - # is shared between the two conditions, so acquiring and - # releasing the conditions also acquires and releases mutex. - self.mutex = threading.RLock() - # Notify not_empty whenever an item is added to the queue; a - # thread waiting to get is notified then. - self.not_empty = threading.Condition(self.mutex) - # Notify not_full whenever an item is removed from the queue; - # a thread waiting to put is notified then. - self.not_full = threading.Condition(self.mutex) - - # when this is set, SAAbort is raised within get(). - self._sqla_abort_context = False - - def qsize(self): - """Return the approximate size of the queue (not reliable!).""" - - self.mutex.acquire() - n = self._qsize() - self.mutex.release() - return n - - def empty(self): - """Return True if the queue is empty, False otherwise (not - reliable!).""" - - self.mutex.acquire() - n = self._empty() - self.mutex.release() - return n - - def full(self): - """Return True if the queue is full, False otherwise (not - reliable!).""" - - self.mutex.acquire() - n = self._full() - self.mutex.release() - return n - - def put(self, item, block=True, timeout=None): - """Put an item into the queue. - - If optional args `block` is True and `timeout` is None (the - default), block if necessary until a free slot is - available. If `timeout` is a positive number, it blocks at - most `timeout` seconds and raises the ``Full`` exception if no - free slot was available within that time. Otherwise (`block` - is false), put an item on the queue if a free slot is - immediately available, else raise the ``Full`` exception - (`timeout` is ignored in that case). - """ - - self.not_full.acquire() - try: - if not block: - if self._full(): - raise Full - elif timeout is None: - while self._full(): - self.not_full.wait() - else: - if timeout < 0: - raise ValueError("'timeout' must be a positive number") - endtime = _time() + timeout - while self._full(): - remaining = endtime - _time() - if remaining <= 0.0: - raise Full - self.not_full.wait(remaining) - self._put(item) - self.not_empty.notify() - finally: - self.not_full.release() - - def put_nowait(self, item): - """Put an item into the queue without blocking. - - Only enqueue the item if a free slot is immediately available. - Otherwise raise the ``Full`` exception. - """ - return self.put(item, False) - - def get(self, block=True, timeout=None): - """Remove and return an item from the queue. - - If optional args `block` is True and `timeout` is None (the - default), block if necessary until an item is available. If - `timeout` is a positive number, it blocks at most `timeout` - seconds and raises the ``Empty`` exception if no item was - available within that time. Otherwise (`block` is false), - return an item if one is immediately available, else raise the - ``Empty`` exception (`timeout` is ignored in that case). - """ - self.not_empty.acquire() - try: - if not block: - if self._empty(): - raise Empty - elif timeout is None: - while self._empty(): - # wait for only half a second, then - # loop around, so that we can see a change in - # _sqla_abort_context in case we missed the notify_all() - # called by abort() - self.not_empty.wait(.5) - if self._sqla_abort_context: - raise SAAbort(self._sqla_abort_context) - else: - if timeout < 0: - raise ValueError("'timeout' must be a positive number") - endtime = _time() + timeout - while self._empty(): - remaining = endtime - _time() - if remaining <= 0.0: - raise Empty - self.not_empty.wait(remaining) - if self._sqla_abort_context: - raise SAAbort(self._sqla_abort_context) - item = self._get() - self.not_full.notify() - return item - finally: - self.not_empty.release() - - def abort(self, context): - """Issue an 'abort', will force any thread waiting on get() - to stop waiting and raise SAAbort. - - """ - self._sqla_abort_context = context - if not self.not_full.acquire(False): - return - try: - # note that this is now optional - # as the waiters in get() both loop around - # to check the _sqla_abort_context flag periodically - self.not_empty.notify_all() - finally: - self.not_full.release() - - def get_nowait(self): - """Remove and return an item from the queue without blocking. - - Only get an item if one is immediately available. Otherwise - raise the ``Empty`` exception. - """ - - return self.get(False) - - # Override these methods to implement other queue organizations - # (e.g. stack or priority queue). - # These will only be called with appropriate locks held - - # Initialize the queue representation - def _init(self, maxsize): - self.maxsize = maxsize - self.queue = deque() - - def _qsize(self): - return len(self.queue) - - # Check whether the queue is empty - def _empty(self): - return not self.queue - - # Check whether the queue is full - def _full(self): - return self.maxsize > 0 and len(self.queue) == self.maxsize - - # Put a new item in the queue - def _put(self, item): - self.queue.append(item) - - # Get an item from the queue - def _get(self): - return self.queue.popleft() diff --git a/libs/sqlalchemy/util/topological.py b/libs/sqlalchemy/util/topological.py deleted file mode 100644 index fe7e7689..00000000 --- a/libs/sqlalchemy/util/topological.py +++ /dev/null @@ -1,96 +0,0 @@ -# util/topological.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Topological sorting algorithms.""" - -from ..exc import CircularDependencyError -from .. import util - -__all__ = ['sort', 'sort_as_subsets', 'find_cycles'] - - -def sort_as_subsets(tuples, allitems): - - edges = util.defaultdict(set) - for parent, child in tuples: - edges[child].add(parent) - - todo = set(allitems) - - while todo: - output = set() - for node in list(todo): - if not todo.intersection(edges[node]): - output.add(node) - - if not output: - raise CircularDependencyError( - "Circular dependency detected.", - find_cycles(tuples, allitems), - _gen_edges(edges) - ) - - todo.difference_update(output) - yield output - - -def sort(tuples, allitems): - """sort the given list of items by dependency. - - 'tuples' is a list of tuples representing a partial ordering. - """ - - for set_ in sort_as_subsets(tuples, allitems): - for s in set_: - yield s - - -def find_cycles(tuples, allitems): - # adapted from: - # http://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html - - edges = util.defaultdict(set) - for parent, child in tuples: - edges[parent].add(child) - nodes_to_test = set(edges) - - output = set() - - # we'd like to find all nodes that are - # involved in cycles, so we do the full - # pass through the whole thing for each - # node in the original list. - - # we can go just through parent edge nodes. - # if a node is only a child and never a parent, - # by definition it can't be part of a cycle. same - # if it's not in the edges at all. - for node in nodes_to_test: - stack = [node] - todo = nodes_to_test.difference(stack) - while stack: - top = stack[-1] - for node in edges[top]: - if node in stack: - cyc = stack[stack.index(node):] - todo.difference_update(cyc) - output.update(cyc) - - if node in todo: - stack.append(node) - todo.remove(node) - break - else: - node = stack.pop() - return output - - -def _gen_edges(edges): - return set([ - (right, left) - for left in edges - for right in edges[left] - ]) diff --git a/libs/tempita/__init__.py b/libs/tempita/__init__.py deleted file mode 100644 index 2af21476..00000000 --- a/libs/tempita/__init__.py +++ /dev/null @@ -1,1160 +0,0 @@ -""" -A small templating language - -This implements a small templating language. This language implements -if/elif/else, for/continue/break, expressions, and blocks of Python -code. The syntax is:: - - {{any expression (function calls etc)}} - {{any expression | filter}} - {{for x in y}}...{{endfor}} - {{if x}}x{{elif y}}y{{else}}z{{endif}} - {{py:x=1}} - {{py: - def foo(bar): - return 'baz' - }} - {{default var = default_value}} - {{# comment}} - -You use this with the ``Template`` class or the ``sub`` shortcut. -The ``Template`` class takes the template string and the name of -the template (for errors) and a default namespace. Then (like -``string.Template``) you can call the ``tmpl.substitute(**kw)`` -method to make a substitution (or ``tmpl.substitute(a_dict)``). - -``sub(content, **kw)`` substitutes the template immediately. You -can use ``__name='tmpl.html'`` to set the name of the template. - -If there are syntax errors ``TemplateError`` will be raised. -""" - -import re -import sys -import cgi -from urllib import quote as url_quote -import os -import tokenize -from cStringIO import StringIO -from tempita._looper import looper -from tempita.compat3 import bytes, basestring_, next, is_unicode, coerce_text - -__all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', - 'sub_html', 'html', 'bunch'] - -token_re = re.compile(r'\{\{|\}\}') -in_re = re.compile(r'\s+in\s+') -var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) - - -class TemplateError(Exception): - """Exception raised while parsing a template - """ - - def __init__(self, message, position, name=None): - Exception.__init__(self, message) - self.position = position - self.name = name - - def __str__(self): - msg = ' '.join(self.args) - if self.position: - msg = '%s at line %s column %s' % ( - msg, self.position[0], self.position[1]) - if self.name: - msg += ' in %s' % self.name - return msg - - -class _TemplateContinue(Exception): - pass - - -class _TemplateBreak(Exception): - pass - - -def get_file_template(name, from_template): - path = os.path.join(os.path.dirname(from_template.name), name) - return from_template.__class__.from_filename( - path, namespace=from_template.namespace, - get_template=from_template.get_template) - - -class Template(object): - - default_namespace = { - 'start_braces': '{{', - 'end_braces': '}}', - 'looper': looper, - } - - default_encoding = 'utf8' - default_inherit = None - - def __init__(self, content, name=None, namespace=None, stacklevel=None, - get_template=None, default_inherit=None, line_offset=0): - self.content = content - self._unicode = is_unicode(content) - if name is None and stacklevel is not None: - try: - caller = sys._getframe(stacklevel) - except ValueError: - pass - else: - globals = caller.f_globals - lineno = caller.f_lineno - if '__file__' in globals: - name = globals['__file__'] - if name.endswith('.pyc') or name.endswith('.pyo'): - name = name[:-1] - elif '__name__' in globals: - name = globals['__name__'] - else: - name = '' - if lineno: - name += ':%s' % lineno - self.name = name - self._parsed = parse(content, name=name, line_offset=line_offset) - if namespace is None: - namespace = {} - self.namespace = namespace - self.get_template = get_template - if default_inherit is not None: - self.default_inherit = default_inherit - - def from_filename(cls, filename, namespace=None, encoding=None, - default_inherit=None, get_template=get_file_template): - f = open(filename, 'rb') - c = f.read() - f.close() - if encoding: - c = c.decode(encoding) - return cls(content=c, name=filename, namespace=namespace, - default_inherit=default_inherit, get_template=get_template) - - from_filename = classmethod(from_filename) - - def __repr__(self): - return '<%s %s name=%r>' % ( - self.__class__.__name__, - hex(id(self))[2:], self.name) - - def substitute(self, *args, **kw): - if args: - if kw: - raise TypeError( - "You can only give positional *or* keyword arguments") - if len(args) > 1: - raise TypeError( - "You can only give one positional argument") - if not hasattr(args[0], 'items'): - raise TypeError( - "If you pass in a single argument, you must pass in a dictionary-like object (with a .items() method); you gave %r" - % (args[0],)) - kw = args[0] - ns = kw - ns['__template_name__'] = self.name - if self.namespace: - ns.update(self.namespace) - result, defs, inherit = self._interpret(ns) - if not inherit: - inherit = self.default_inherit - if inherit: - result = self._interpret_inherit(result, defs, inherit, ns) - return result - - def _interpret(self, ns): - __traceback_hide__ = True - parts = [] - defs = {} - self._interpret_codes(self._parsed, ns, out=parts, defs=defs) - if '__inherit__' in defs: - inherit = defs.pop('__inherit__') - else: - inherit = None - return ''.join(parts), defs, inherit - - def _interpret_inherit(self, body, defs, inherit_template, ns): - __traceback_hide__ = True - if not self.get_template: - raise TemplateError( - 'You cannot use inheritance without passing in get_template', - position=None, name=self.name) - templ = self.get_template(inherit_template, self) - self_ = TemplateObject(self.name) - for name, value in defs.iteritems(): - setattr(self_, name, value) - self_.body = body - ns = ns.copy() - ns['self'] = self_ - return templ.substitute(ns) - - def _interpret_codes(self, codes, ns, out, defs): - __traceback_hide__ = True - for item in codes: - if isinstance(item, basestring_): - out.append(item) - else: - self._interpret_code(item, ns, out, defs) - - def _interpret_code(self, code, ns, out, defs): - __traceback_hide__ = True - name, pos = code[0], code[1] - if name == 'py': - self._exec(code[2], ns, pos) - elif name == 'continue': - raise _TemplateContinue() - elif name == 'break': - raise _TemplateBreak() - elif name == 'for': - vars, expr, content = code[2], code[3], code[4] - expr = self._eval(expr, ns, pos) - self._interpret_for(vars, expr, content, ns, out, defs) - elif name == 'cond': - parts = code[2:] - self._interpret_if(parts, ns, out, defs) - elif name == 'expr': - parts = code[2].split('|') - base = self._eval(parts[0], ns, pos) - for part in parts[1:]: - func = self._eval(part, ns, pos) - base = func(base) - out.append(self._repr(base, pos)) - elif name == 'default': - var, expr = code[2], code[3] - if var not in ns: - result = self._eval(expr, ns, pos) - ns[var] = result - elif name == 'inherit': - expr = code[2] - value = self._eval(expr, ns, pos) - defs['__inherit__'] = value - elif name == 'def': - name = code[2] - signature = code[3] - parts = code[4] - ns[name] = defs[name] = TemplateDef(self, name, signature, body=parts, ns=ns, - pos=pos) - elif name == 'comment': - return - else: - assert 0, "Unknown code: %r" % name - - def _interpret_for(self, vars, expr, content, ns, out, defs): - __traceback_hide__ = True - for item in expr: - if len(vars) == 1: - ns[vars[0]] = item - else: - if len(vars) != len(item): - raise ValueError( - 'Need %i items to unpack (got %i items)' - % (len(vars), len(item))) - for name, value in zip(vars, item): - ns[name] = value - try: - self._interpret_codes(content, ns, out, defs) - except _TemplateContinue: - continue - except _TemplateBreak: - break - - def _interpret_if(self, parts, ns, out, defs): - __traceback_hide__ = True - # @@: if/else/else gets through - for part in parts: - assert not isinstance(part, basestring_) - name, pos = part[0], part[1] - if name == 'else': - result = True - else: - result = self._eval(part[2], ns, pos) - if result: - self._interpret_codes(part[3], ns, out, defs) - break - - def _eval(self, code, ns, pos): - __traceback_hide__ = True - try: - try: - value = eval(code, self.default_namespace, ns) - except SyntaxError, e: - raise SyntaxError( - 'invalid syntax in expression: %s' % code) - return value - except: - exc_info = sys.exc_info() - e = exc_info[1] - if getattr(e, 'args', None): - arg0 = e.args[0] - else: - arg0 = coerce_text(e) - e.args = (self._add_line_info(arg0, pos),) - raise exc_info[0], e, exc_info[2] - - def _exec(self, code, ns, pos): - __traceback_hide__ = True - try: - exec code in self.default_namespace, ns - except: - exc_info = sys.exc_info() - e = exc_info[1] - if e.args: - e.args = (self._add_line_info(e.args[0], pos),) - else: - e.args = (self._add_line_info(None, pos),) - raise exc_info[0], e, exc_info[2] - - def _repr(self, value, pos): - __traceback_hide__ = True - try: - if value is None: - return '' - if self._unicode: - try: - value = unicode(value) - except UnicodeDecodeError: - value = bytes(value) - else: - if not isinstance(value, basestring_): - value = coerce_text(value) - if (is_unicode(value) - and self.default_encoding): - value = value.encode(self.default_encoding) - except: - exc_info = sys.exc_info() - e = exc_info[1] - e.args = (self._add_line_info(e.args[0], pos),) - raise exc_info[0], e, exc_info[2] - else: - if self._unicode and isinstance(value, bytes): - if not self.default_encoding: - raise UnicodeDecodeError( - 'Cannot decode bytes value %r into unicode ' - '(no default_encoding provided)' % value) - try: - value = value.decode(self.default_encoding) - except UnicodeDecodeError, e: - raise UnicodeDecodeError( - e.encoding, - e.object, - e.start, - e.end, - e.reason + ' in string %r' % value) - elif not self._unicode and is_unicode(value): - if not self.default_encoding: - raise UnicodeEncodeError( - 'Cannot encode unicode value %r into bytes ' - '(no default_encoding provided)' % value) - value = value.encode(self.default_encoding) - return value - - def _add_line_info(self, msg, pos): - msg = "%s at line %s column %s" % ( - msg, pos[0], pos[1]) - if self.name: - msg += " in file %s" % self.name - return msg - - -def sub(content, **kw): - name = kw.get('__name') - tmpl = Template(content, name=name) - return tmpl.substitute(kw) - - -def paste_script_template_renderer(content, vars, filename=None): - tmpl = Template(content, name=filename) - return tmpl.substitute(vars) - - -class bunch(dict): - - def __init__(self, **kw): - for name, value in kw.iteritems(): - setattr(self, name, value) - - def __setattr__(self, name, value): - self[name] = value - - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError(name) - - def __getitem__(self, key): - if 'default' in self: - try: - return dict.__getitem__(self, key) - except KeyError: - return dict.__getitem__(self, 'default') - else: - return dict.__getitem__(self, key) - - def __repr__(self): - items = [ - (k, v) for k, v in self.iteritems()] - items.sort() - return '<%s %s>' % ( - self.__class__.__name__, - ' '.join(['%s=%r' % (k, v) for k, v in items])) - -############################################################ -## HTML Templating -############################################################ - - -class html(object): - - def __init__(self, value): - self.value = value - - def __str__(self): - return self.value - - def __html__(self): - return self.value - - def __repr__(self): - return '<%s %r>' % ( - self.__class__.__name__, self.value) - - -def html_quote(value, force=True): - if not force and hasattr(value, '__html__'): - return value.__html__() - if value is None: - return '' - if not isinstance(value, basestring_): - value = coerce_text(value) - if sys.version >= "3" and isinstance(value, bytes): - value = cgi.escape(value.decode('latin1'), 1) - value = value.encode('latin1') - else: - value = cgi.escape(value, 1) - if sys.version < "3": - if is_unicode(value): - value = value.encode('ascii', 'xmlcharrefreplace') - return value - - -def url(v): - v = coerce_text(v) - if is_unicode(v): - v = v.encode('utf8') - return url_quote(v) - - -def attr(**kw): - kw = list(kw.iteritems()) - kw.sort() - parts = [] - for name, value in kw: - if value is None: - continue - if name.endswith('_'): - name = name[:-1] - parts.append('%s="%s"' % (html_quote(name), html_quote(value))) - return html(' '.join(parts)) - - -class HTMLTemplate(Template): - - default_namespace = Template.default_namespace.copy() - default_namespace.update(dict( - html=html, - attr=attr, - url=url, - html_quote=html_quote, - )) - - def _repr(self, value, pos): - if hasattr(value, '__html__'): - value = value.__html__() - quote = False - else: - quote = True - plain = Template._repr(self, value, pos) - if quote: - return html_quote(plain) - else: - return plain - - -def sub_html(content, **kw): - name = kw.get('__name') - tmpl = HTMLTemplate(content, name=name) - return tmpl.substitute(kw) - - -class TemplateDef(object): - def __init__(self, template, func_name, func_signature, - body, ns, pos, bound_self=None): - self._template = template - self._func_name = func_name - self._func_signature = func_signature - self._body = body - self._ns = ns - self._pos = pos - self._bound_self = bound_self - - def __repr__(self): - return '' % ( - self._func_name, self._func_signature, - self._template.name, self._pos) - - def __str__(self): - return self() - - def __call__(self, *args, **kw): - values = self._parse_signature(args, kw) - ns = self._ns.copy() - ns.update(values) - if self._bound_self is not None: - ns['self'] = self._bound_self - out = [] - subdefs = {} - self._template._interpret_codes(self._body, ns, out, subdefs) - return ''.join(out) - - def __get__(self, obj, type=None): - if obj is None: - return self - return self.__class__( - self._template, self._func_name, self._func_signature, - self._body, self._ns, self._pos, bound_self=obj) - - def _parse_signature(self, args, kw): - values = {} - sig_args, var_args, var_kw, defaults = self._func_signature - extra_kw = {} - for name, value in kw.iteritems(): - if not var_kw and name not in sig_args: - raise TypeError( - 'Unexpected argument %s' % name) - if name in sig_args: - values[sig_args] = value - else: - extra_kw[name] = value - args = list(args) - sig_args = list(sig_args) - while args: - while sig_args and sig_args[0] in values: - sig_args.pop(0) - if sig_args: - name = sig_args.pop(0) - values[name] = args.pop(0) - elif var_args: - values[var_args] = tuple(args) - break - else: - raise TypeError( - 'Extra position arguments: %s' - % ', '.join(repr(v) for v in args)) - for name, value_expr in defaults.iteritems(): - if name not in values: - values[name] = self._template._eval( - value_expr, self._ns, self._pos) - for name in sig_args: - if name not in values: - raise TypeError( - 'Missing argument: %s' % name) - if var_kw: - values[var_kw] = extra_kw - return values - - -class TemplateObject(object): - - def __init__(self, name): - self.__name = name - self.get = TemplateObjectGetter(self) - - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, self.__name) - - -class TemplateObjectGetter(object): - - def __init__(self, template_obj): - self.__template_obj = template_obj - - def __getattr__(self, attr): - return getattr(self.__template_obj, attr, Empty) - - def __repr__(self): - return '<%s around %r>' % (self.__class__.__name__, self.__template_obj) - - -class _Empty(object): - def __call__(self, *args, **kw): - return self - - def __str__(self): - return '' - - def __repr__(self): - return 'Empty' - - def __unicode__(self): - return u'' - - def __iter__(self): - return iter(()) - - def __bool__(self): - return False - - if sys.version < "3": - __nonzero__ = __bool__ - -Empty = _Empty() -del _Empty - -############################################################ -## Lexing and Parsing -############################################################ - - -def lex(s, name=None, trim_whitespace=True, line_offset=0): - """ - Lex a string into chunks: - - >>> lex('hey') - ['hey'] - >>> lex('hey {{you}}') - ['hey ', ('you', (1, 7))] - >>> lex('hey {{') - Traceback (most recent call last): - ... - TemplateError: No }} to finish last expression at line 1 column 7 - >>> lex('hey }}') - Traceback (most recent call last): - ... - TemplateError: }} outside expression at line 1 column 7 - >>> lex('hey {{ {{') - Traceback (most recent call last): - ... - TemplateError: {{ inside expression at line 1 column 10 - - """ - in_expr = False - chunks = [] - last = 0 - last_pos = (1, 1) - for match in token_re.finditer(s): - expr = match.group(0) - pos = find_position(s, match.end(), line_offset) - if expr == '{{' and in_expr: - raise TemplateError('{{ inside expression', position=pos, - name=name) - elif expr == '}}' and not in_expr: - raise TemplateError('}} outside expression', position=pos, - name=name) - if expr == '{{': - part = s[last:match.start()] - if part: - chunks.append(part) - in_expr = True - else: - chunks.append((s[last:match.start()], last_pos)) - in_expr = False - last = match.end() - last_pos = pos - if in_expr: - raise TemplateError('No }} to finish last expression', - name=name, position=last_pos) - part = s[last:] - if part: - chunks.append(part) - if trim_whitespace: - chunks = trim_lex(chunks) - return chunks - -statement_re = re.compile(r'^(?:if |elif |for |def |inherit |default |py:)') -single_statements = ['else', 'endif', 'endfor', 'enddef', 'continue', 'break'] -trail_whitespace_re = re.compile(r'\n\r?[\t ]*$') -lead_whitespace_re = re.compile(r'^[\t ]*\n') - - -def trim_lex(tokens): - r""" - Takes a lexed set of tokens, and removes whitespace when there is - a directive on a line by itself: - - >>> tokens = lex('{{if x}}\nx\n{{endif}}\ny', trim_whitespace=False) - >>> tokens - [('if x', (1, 3)), '\nx\n', ('endif', (3, 3)), '\ny'] - >>> trim_lex(tokens) - [('if x', (1, 3)), 'x\n', ('endif', (3, 3)), 'y'] - """ - last_trim = None - for i in range(len(tokens)): - current = tokens[i] - if isinstance(tokens[i], basestring_): - # we don't trim this - continue - item = current[0] - if not statement_re.search(item) and item not in single_statements: - continue - if not i: - prev = '' - else: - prev = tokens[i - 1] - if i + 1 >= len(tokens): - next_chunk = '' - else: - next_chunk = tokens[i + 1] - if (not isinstance(next_chunk, basestring_) - or not isinstance(prev, basestring_)): - continue - prev_ok = not prev or trail_whitespace_re.search(prev) - if i == 1 and not prev.strip(): - prev_ok = True - if last_trim is not None and last_trim + 2 == i and not prev.strip(): - prev_ok = 'last' - if (prev_ok - and (not next_chunk or lead_whitespace_re.search(next_chunk) - or (i == len(tokens) - 2 and not next_chunk.strip()))): - if prev: - if ((i == 1 and not prev.strip()) - or prev_ok == 'last'): - tokens[i - 1] = '' - else: - m = trail_whitespace_re.search(prev) - # +1 to leave the leading \n on: - prev = prev[:m.start() + 1] - tokens[i - 1] = prev - if next_chunk: - last_trim = i - if i == len(tokens) - 2 and not next_chunk.strip(): - tokens[i + 1] = '' - else: - m = lead_whitespace_re.search(next_chunk) - next_chunk = next_chunk[m.end():] - tokens[i + 1] = next_chunk - return tokens - - -def find_position(string, index, line_offset): - """Given a string and index, return (line, column)""" - leading = string[:index].splitlines() - return (len(leading) + line_offset, len(leading[-1]) + 1) - - -def parse(s, name=None, line_offset=0): - r""" - Parses a string into a kind of AST - - >>> parse('{{x}}') - [('expr', (1, 3), 'x')] - >>> parse('foo') - ['foo'] - >>> parse('{{if x}}test{{endif}}') - [('cond', (1, 3), ('if', (1, 3), 'x', ['test']))] - >>> parse('series->{{for x in y}}x={{x}}{{endfor}}') - ['series->', ('for', (1, 11), ('x',), 'y', ['x=', ('expr', (1, 27), 'x')])] - >>> parse('{{for x, y in z:}}{{continue}}{{endfor}}') - [('for', (1, 3), ('x', 'y'), 'z', [('continue', (1, 21))])] - >>> parse('{{py:x=1}}') - [('py', (1, 3), 'x=1')] - >>> parse('{{if x}}a{{elif y}}b{{else}}c{{endif}}') - [('cond', (1, 3), ('if', (1, 3), 'x', ['a']), ('elif', (1, 12), 'y', ['b']), ('else', (1, 23), None, ['c']))] - - Some exceptions:: - - >>> parse('{{continue}}') - Traceback (most recent call last): - ... - TemplateError: continue outside of for loop at line 1 column 3 - >>> parse('{{if x}}foo') - Traceback (most recent call last): - ... - TemplateError: No {{endif}} at line 1 column 3 - >>> parse('{{else}}') - Traceback (most recent call last): - ... - TemplateError: else outside of an if block at line 1 column 3 - >>> parse('{{if x}}{{for x in y}}{{endif}}{{endfor}}') - Traceback (most recent call last): - ... - TemplateError: Unexpected endif at line 1 column 25 - >>> parse('{{if}}{{endif}}') - Traceback (most recent call last): - ... - TemplateError: if with no expression at line 1 column 3 - >>> parse('{{for x y}}{{endfor}}') - Traceback (most recent call last): - ... - TemplateError: Bad for (no "in") in 'x y' at line 1 column 3 - >>> parse('{{py:x=1\ny=2}}') - Traceback (most recent call last): - ... - TemplateError: Multi-line py blocks must start with a newline at line 1 column 3 - """ - tokens = lex(s, name=name, line_offset=line_offset) - result = [] - while tokens: - next_chunk, tokens = parse_expr(tokens, name) - result.append(next_chunk) - return result - - -def parse_expr(tokens, name, context=()): - if isinstance(tokens[0], basestring_): - return tokens[0], tokens[1:] - expr, pos = tokens[0] - expr = expr.strip() - if expr.startswith('py:'): - expr = expr[3:].lstrip(' \t') - if expr.startswith('\n') or expr.startswith('\r'): - expr = expr.lstrip('\r\n') - if '\r' in expr: - expr = expr.replace('\r\n', '\n') - expr = expr.replace('\r', '') - expr += '\n' - else: - if '\n' in expr: - raise TemplateError( - 'Multi-line py blocks must start with a newline', - position=pos, name=name) - return ('py', pos, expr), tokens[1:] - elif expr in ('continue', 'break'): - if 'for' not in context: - raise TemplateError( - 'continue outside of for loop', - position=pos, name=name) - return (expr, pos), tokens[1:] - elif expr.startswith('if '): - return parse_cond(tokens, name, context) - elif (expr.startswith('elif ') - or expr == 'else'): - raise TemplateError( - '%s outside of an if block' % expr.split()[0], - position=pos, name=name) - elif expr in ('if', 'elif', 'for'): - raise TemplateError( - '%s with no expression' % expr, - position=pos, name=name) - elif expr in ('endif', 'endfor', 'enddef'): - raise TemplateError( - 'Unexpected %s' % expr, - position=pos, name=name) - elif expr.startswith('for '): - return parse_for(tokens, name, context) - elif expr.startswith('default '): - return parse_default(tokens, name, context) - elif expr.startswith('inherit '): - return parse_inherit(tokens, name, context) - elif expr.startswith('def '): - return parse_def(tokens, name, context) - elif expr.startswith('#'): - return ('comment', pos, tokens[0][0]), tokens[1:] - return ('expr', pos, tokens[0][0]), tokens[1:] - - -def parse_cond(tokens, name, context): - start = tokens[0][1] - pieces = [] - context = context + ('if',) - while 1: - if not tokens: - raise TemplateError( - 'Missing {{endif}}', - position=start, name=name) - if (isinstance(tokens[0], tuple) - and tokens[0][0] == 'endif'): - return ('cond', start) + tuple(pieces), tokens[1:] - next_chunk, tokens = parse_one_cond(tokens, name, context) - pieces.append(next_chunk) - - -def parse_one_cond(tokens, name, context): - (first, pos), tokens = tokens[0], tokens[1:] - content = [] - if first.endswith(':'): - first = first[:-1] - if first.startswith('if '): - part = ('if', pos, first[3:].lstrip(), content) - elif first.startswith('elif '): - part = ('elif', pos, first[5:].lstrip(), content) - elif first == 'else': - part = ('else', pos, None, content) - else: - assert 0, "Unexpected token %r at %s" % (first, pos) - while 1: - if not tokens: - raise TemplateError( - 'No {{endif}}', - position=pos, name=name) - if (isinstance(tokens[0], tuple) - and (tokens[0][0] == 'endif' - or tokens[0][0].startswith('elif ') - or tokens[0][0] == 'else')): - return part, tokens - next_chunk, tokens = parse_expr(tokens, name, context) - content.append(next_chunk) - - -def parse_for(tokens, name, context): - first, pos = tokens[0] - tokens = tokens[1:] - context = ('for',) + context - content = [] - assert first.startswith('for ') - if first.endswith(':'): - first = first[:-1] - first = first[3:].strip() - match = in_re.search(first) - if not match: - raise TemplateError( - 'Bad for (no "in") in %r' % first, - position=pos, name=name) - vars = first[:match.start()] - if '(' in vars: - raise TemplateError( - 'You cannot have () in the variable section of a for loop (%r)' - % vars, position=pos, name=name) - vars = tuple([ - v.strip() for v in first[:match.start()].split(',') - if v.strip()]) - expr = first[match.end():] - while 1: - if not tokens: - raise TemplateError( - 'No {{endfor}}', - position=pos, name=name) - if (isinstance(tokens[0], tuple) - and tokens[0][0] == 'endfor'): - return ('for', pos, vars, expr, content), tokens[1:] - next_chunk, tokens = parse_expr(tokens, name, context) - content.append(next_chunk) - - -def parse_default(tokens, name, context): - first, pos = tokens[0] - assert first.startswith('default ') - first = first.split(None, 1)[1] - parts = first.split('=', 1) - if len(parts) == 1: - raise TemplateError( - "Expression must be {{default var=value}}; no = found in %r" % first, - position=pos, name=name) - var = parts[0].strip() - if ',' in var: - raise TemplateError( - "{{default x, y = ...}} is not supported", - position=pos, name=name) - if not var_re.search(var): - raise TemplateError( - "Not a valid variable name for {{default}}: %r" - % var, position=pos, name=name) - expr = parts[1].strip() - return ('default', pos, var, expr), tokens[1:] - - -def parse_inherit(tokens, name, context): - first, pos = tokens[0] - assert first.startswith('inherit ') - expr = first.split(None, 1)[1] - return ('inherit', pos, expr), tokens[1:] - - -def parse_def(tokens, name, context): - first, start = tokens[0] - tokens = tokens[1:] - assert first.startswith('def ') - first = first.split(None, 1)[1] - if first.endswith(':'): - first = first[:-1] - if '(' not in first: - func_name = first - sig = ((), None, None, {}) - elif not first.endswith(')'): - raise TemplateError("Function definition doesn't end with ): %s" % first, - position=start, name=name) - else: - first = first[:-1] - func_name, sig_text = first.split('(', 1) - sig = parse_signature(sig_text, name, start) - context = context + ('def',) - content = [] - while 1: - if not tokens: - raise TemplateError( - 'Missing {{enddef}}', - position=start, name=name) - if (isinstance(tokens[0], tuple) - and tokens[0][0] == 'enddef'): - return ('def', start, func_name, sig, content), tokens[1:] - next_chunk, tokens = parse_expr(tokens, name, context) - content.append(next_chunk) - - -def parse_signature(sig_text, name, pos): - tokens = tokenize.generate_tokens(StringIO(sig_text).readline) - sig_args = [] - var_arg = None - var_kw = None - defaults = {} - - def get_token(pos=False): - try: - tok_type, tok_string, (srow, scol), (erow, ecol), line = next(tokens) - except StopIteration: - return tokenize.ENDMARKER, '' - if pos: - return tok_type, tok_string, (srow, scol), (erow, ecol) - else: - return tok_type, tok_string - while 1: - var_arg_type = None - tok_type, tok_string = get_token() - if tok_type == tokenize.ENDMARKER: - break - if tok_type == tokenize.OP and (tok_string == '*' or tok_string == '**'): - var_arg_type = tok_string - tok_type, tok_string = get_token() - if tok_type != tokenize.NAME: - raise TemplateError('Invalid signature: (%s)' % sig_text, - position=pos, name=name) - var_name = tok_string - tok_type, tok_string = get_token() - if tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','): - if var_arg_type == '*': - var_arg = var_name - elif var_arg_type == '**': - var_kw = var_name - else: - sig_args.append(var_name) - if tok_type == tokenize.ENDMARKER: - break - continue - if var_arg_type is not None: - raise TemplateError('Invalid signature: (%s)' % sig_text, - position=pos, name=name) - if tok_type == tokenize.OP and tok_string == '=': - nest_type = None - unnest_type = None - nest_count = 0 - start_pos = end_pos = None - parts = [] - while 1: - tok_type, tok_string, s, e = get_token(True) - if start_pos is None: - start_pos = s - end_pos = e - if tok_type == tokenize.ENDMARKER and nest_count: - raise TemplateError('Invalid signature: (%s)' % sig_text, - position=pos, name=name) - if (not nest_count and - (tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','))): - default_expr = isolate_expression(sig_text, start_pos, end_pos) - defaults[var_name] = default_expr - sig_args.append(var_name) - break - parts.append((tok_type, tok_string)) - if nest_count and tok_type == tokenize.OP and tok_string == nest_type: - nest_count += 1 - elif nest_count and tok_type == tokenize.OP and tok_string == unnest_type: - nest_count -= 1 - if not nest_count: - nest_type = unnest_type = None - elif not nest_count and tok_type == tokenize.OP and tok_string in ('(', '[', '{'): - nest_type = tok_string - nest_count = 1 - unnest_type = {'(': ')', '[': ']', '{': '}'}[nest_type] - return sig_args, var_arg, var_kw, defaults - - -def isolate_expression(string, start_pos, end_pos): - srow, scol = start_pos - srow -= 1 - erow, ecol = end_pos - erow -= 1 - lines = string.splitlines(True) - if srow == erow: - return lines[srow][scol:ecol] - parts = [lines[srow][scol:]] - parts.extend(lines[srow+1:erow]) - if erow < len(lines): - # It'll sometimes give (end_row_past_finish, 0) - parts.append(lines[erow][:ecol]) - return ''.join(parts) - -_fill_command_usage = """\ -%prog [OPTIONS] TEMPLATE arg=value - -Use py:arg=value to set a Python value; otherwise all values are -strings. -""" - - -def fill_command(args=None): - import sys - import optparse - import pkg_resources - import os - if args is None: - args = sys.argv[1:] - dist = pkg_resources.get_distribution('Paste') - parser = optparse.OptionParser( - version=coerce_text(dist), - usage=_fill_command_usage) - parser.add_option( - '-o', '--output', - dest='output', - metavar="FILENAME", - help="File to write output to (default stdout)") - parser.add_option( - '--html', - dest='use_html', - action='store_true', - help="Use HTML style filling (including automatic HTML quoting)") - parser.add_option( - '--env', - dest='use_env', - action='store_true', - help="Put the environment in as top-level variables") - options, args = parser.parse_args(args) - if len(args) < 1: - print('You must give a template filename') - sys.exit(2) - template_name = args[0] - args = args[1:] - vars = {} - if options.use_env: - vars.update(os.environ) - for value in args: - if '=' not in value: - print('Bad argument: %r' % value) - sys.exit(2) - name, value = value.split('=', 1) - if name.startswith('py:'): - name = name[:3] - value = eval(value) - vars[name] = value - if template_name == '-': - template_content = sys.stdin.read() - template_name = '' - else: - f = open(template_name, 'rb') - template_content = f.read() - f.close() - if options.use_html: - TemplateClass = HTMLTemplate - else: - TemplateClass = Template - template = TemplateClass(template_content, name=template_name) - result = template.substitute(vars) - if options.output: - f = open(options.output, 'wb') - f.write(result) - f.close() - else: - sys.stdout.write(result) - -if __name__ == '__main__': - fill_command() diff --git a/libs/tempita/_looper.py b/libs/tempita/_looper.py deleted file mode 100644 index 6784c7cd..00000000 --- a/libs/tempita/_looper.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Helper for looping over sequences, particular in templates. - -Often in a loop in a template it's handy to know what's next up, -previously up, if this is the first or last item in the sequence, etc. -These can be awkward to manage in a normal Python loop, but using the -looper you can get a better sense of the context. Use like:: - - >>> for loop, item in looper(['a', 'b', 'c']): - ... print loop.number, item - ... if not loop.last: - ... print '---' - 1 a - --- - 2 b - --- - 3 c - -""" - -import sys -from tempita.compat3 import basestring_ - -__all__ = ['looper'] - - -class looper(object): - """ - Helper for looping (particularly in templates) - - Use this like:: - - for loop, item in looper(seq): - if loop.first: - ... - """ - - def __init__(self, seq): - self.seq = seq - - def __iter__(self): - return looper_iter(self.seq) - - def __repr__(self): - return '<%s for %r>' % ( - self.__class__.__name__, self.seq) - - -class looper_iter(object): - - def __init__(self, seq): - self.seq = list(seq) - self.pos = 0 - - def __iter__(self): - return self - - def __next__(self): - if self.pos >= len(self.seq): - raise StopIteration - result = loop_pos(self.seq, self.pos), self.seq[self.pos] - self.pos += 1 - return result - - if sys.version < "3": - next = __next__ - - -class loop_pos(object): - - def __init__(self, seq, pos): - self.seq = seq - self.pos = pos - - def __repr__(self): - return '' % ( - self.seq[self.pos], self.pos) - - def index(self): - return self.pos - index = property(index) - - def number(self): - return self.pos + 1 - number = property(number) - - def item(self): - return self.seq[self.pos] - item = property(item) - - def __next__(self): - try: - return self.seq[self.pos + 1] - except IndexError: - return None - __next__ = property(__next__) - - if sys.version < "3": - next = __next__ - - def previous(self): - if self.pos == 0: - return None - return self.seq[self.pos - 1] - previous = property(previous) - - def odd(self): - return not self.pos % 2 - odd = property(odd) - - def even(self): - return self.pos % 2 - even = property(even) - - def first(self): - return self.pos == 0 - first = property(first) - - def last(self): - return self.pos == len(self.seq) - 1 - last = property(last) - - def length(self): - return len(self.seq) - length = property(length) - - def first_group(self, getter=None): - """ - Returns true if this item is the start of a new group, - where groups mean that some attribute has changed. The getter - can be None (the item itself changes), an attribute name like - ``'.attr'``, a function, or a dict key or list index. - """ - if self.first: - return True - return self._compare_group(self.item, self.previous, getter) - - def last_group(self, getter=None): - """ - Returns true if this item is the end of a new group, - where groups mean that some attribute has changed. The getter - can be None (the item itself changes), an attribute name like - ``'.attr'``, a function, or a dict key or list index. - """ - if self.last: - return True - return self._compare_group(self.item, self.__next__, getter) - - def _compare_group(self, item, other, getter): - if getter is None: - return item != other - elif (isinstance(getter, basestring_) - and getter.startswith('.')): - getter = getter[1:] - if getter.endswith('()'): - getter = getter[:-2] - return getattr(item, getter)() != getattr(other, getter)() - else: - return getattr(item, getter) != getattr(other, getter) - elif hasattr(getter, '__call__'): - return getter(item) != getter(other) - else: - return item[getter] != other[getter] diff --git a/libs/tempita/compat3.py b/libs/tempita/compat3.py deleted file mode 100644 index 5e18fa01..00000000 --- a/libs/tempita/compat3.py +++ /dev/null @@ -1,45 +0,0 @@ -import sys - -__all__ = ['b', 'basestring_', 'bytes', 'next', 'is_unicode'] - -if sys.version < "3": - b = bytes = str - basestring_ = basestring -else: - - def b(s): - if isinstance(s, str): - return s.encode('latin1') - return bytes(s) - basestring_ = (bytes, str) - bytes = bytes -text = str - -if sys.version < "3": - - def next(obj): - return obj.next() -else: - next = next - -if sys.version < "3": - - def is_unicode(obj): - return isinstance(obj, unicode) -else: - - def is_unicode(obj): - return isinstance(obj, str) - - -def coerce_text(v): - if not isinstance(v, basestring_): - if sys.version < "3": - attr = '__unicode__' - else: - attr = '__str__' - if hasattr(v, attr): - return unicode(v) - else: - return bytes(v) - return v From 96e8a909d81d4d3a9942c2a127436320eec1b998 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Feb 2014 22:19:55 +0100 Subject: [PATCH 017/468] database manage init --- couchpotato/__init__.py | 8 ++ couchpotato/api.py | 3 + couchpotato/core/database.py | 109 +++++++++++++++++ couchpotato/core/media/__init__.py | 9 -- couchpotato/core/media/_base/media/index.py | 10 +- couchpotato/core/media/_base/media/main.py | 56 ++------- couchpotato/core/notifications/core/index.py | 2 + couchpotato/core/notifications/core/main.py | 23 +--- couchpotato/core/plugins/base.py | 18 +++ couchpotato/core/plugins/category/index.py | 2 + couchpotato/core/plugins/category/main.py | 24 +--- couchpotato/core/plugins/file/main.py | 6 +- couchpotato/core/plugins/profile/index.py | 1 + couchpotato/core/plugins/profile/main.py | 16 +-- couchpotato/core/plugins/quality/index.py | 1 + couchpotato/core/plugins/quality/main.py | 16 +-- couchpotato/core/plugins/release/index.py | 4 + couchpotato/core/plugins/release/main.py | 41 ++----- .../core/plugins/userscript/template.js | 2 +- .../providers/torrent/torrentpotato/main.py | 6 +- .../{settings/__init__.py => settings.py} | 19 ++- couchpotato/core/settings/index.py | 16 --- couchpotato/environment.py | 2 + couchpotato/static/style/api.css | 50 +++++++- couchpotato/templates/api.html | 6 +- couchpotato/templates/database.html | 115 ++++++++++++++++++ 26 files changed, 382 insertions(+), 183 deletions(-) create mode 100644 couchpotato/core/database.py rename couchpotato/core/{settings/__init__.py => settings.py} (93%) delete mode 100644 couchpotato/core/settings/index.py create mode 100644 couchpotato/templates/database.html diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index dba45861..ce034db2 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -75,8 +75,16 @@ def apiDocs(): addView('docs', apiDocs) +# Database debug manager +def databaseManage(): + return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env) + +addView('database', databaseManage) + + # Make non basic auth option to get api key class KeyHandler(RequestHandler): + def get(self, *args, **kwargs): api_key = None diff --git a/couchpotato/api.py b/couchpotato/api.py index ba7f7b69..ffa78cea 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -93,6 +93,7 @@ class ApiHandler(RequestHandler): # Split array arguments kwargs = getParams(kwargs) + kwargs['_request'] = self # Remove t random string try: del kwargs['t'] @@ -127,6 +128,8 @@ class ApiHandler(RequestHandler): api_locks[route].release() + post = get + def addApiView(route, func, static = False, docs = None, **kwargs): diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py new file mode 100644 index 00000000..0f3ece73 --- /dev/null +++ b/couchpotato/core/database.py @@ -0,0 +1,109 @@ +import json +import time +import traceback +from couchpotato import CPLog +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent + +log = CPLog(__name__) + + +class Database(object): + + indexes = [] + db = None + + def __init__(self): + + addApiView('database.list_documents', self.listDocuments) + addApiView('database.document.update', self.updateDocument) + addApiView('database.document.delete', self.deleteDocument) + + addEvent('database.setup_index', self.setupIndex) + + def test(): + time.sleep(1) + self.listDocuments() + + addEvent('app.load', test) + + + def getDB(self): + + if not self.db: + from couchpotato import get_db + self.db = get_db() + + return self.db + + def setupIndex(self, index_name, klass): + + self.indexes.append(index_name) + + db = self.getDB() + + # Category index + try: + db.add_index(klass(db.path, index_name)) + db.reindex_index(index_name) + except: + previous_version = db.indexes_names[index_name]._version + current_version = klass._version + + # Only edit index if versions are different + if previous_version < current_version: + log.debug('Index "%s" already exists, updating and reindexing', index_name) + db.edit_index(klass(db.path, index_name), reindex = True) + + def deleteDocument(self, **kwargs): + + db = self.getDB() + + try: + document = db.get(id) + db.delete(document) + + return { + 'success': True + } + except: + return { + 'success': False, + 'error': traceback.format_exc() + } + + def updateDocument(self, **kwargs): + + db = self.getDB() + + try: + + document = json.loads(kwargs.get('_request').get_argument('document')) + d = db.update(document) + document.update(d) + + return { + 'success': True, + 'document': document + } + except: + return { + 'success': False, + 'error': traceback.format_exc() + } + + def listDocuments(self, **kwargs): + db = self.getDB() + + results = { + 'unknown': [] + } + + for document in db.all('id'): + key = document.get('_t', 'unknown') + if not results.get(key): + results[key] = [] + results[key].append(document) + + + return results diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index c7ae732a..aee1a59b 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -10,15 +10,6 @@ class MediaBase(Plugin): _type = None - default_dict = { - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}}, - 'library': {'titles': {}, 'files': {}}, - 'files': {}, - 'status': {}, - 'category': {}, - } - def initType(self): addEvent('media.types', self.getType) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 68e6610c..7cfe8079 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -7,6 +7,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString class MediaIMDBIndex(HashIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = 'I' @@ -19,8 +20,8 @@ class MediaIMDBIndex(HashIndex): if data.get('_t') == 'media' and data.get('identifier'): return int(data['identifier'].strip('t')), None - def run_to_dict(self, db, media_id, dict = None): - if not dict: dict = {} + def run_to_dict(self, db, media_id, dict_dept = None): + if not dict_dept: dict_dept = {} return db.get('id', media_id) @@ -34,6 +35,7 @@ class MediaIMDBIndex(HashIndex): class MediaStatusIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' @@ -48,6 +50,7 @@ class MediaStatusIndex(TreeBasedIndex): class MediaTypeIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' @@ -62,6 +65,7 @@ class MediaTypeIndex(TreeBasedIndex): class TitleSearchIndex(MultiTreeBasedIndex): + _version = 1 custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex from itertools import izip @@ -93,6 +97,7 @@ from couchpotato.core.helpers.encoding import simplifyString""" class TitleIndex(TreeBasedIndex): + _version = 1 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -125,6 +130,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" class StartsWithIndex(TreeBasedIndex): + _version = 1 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 721c1d00..472e4d29 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -14,6 +14,15 @@ log = CPLog(__name__) class MediaPlugin(MediaBase): + _database = { + 'media': MediaIMDBIndex, + 'media_search_title': MediaStatusIndex, + 'media_status': MediaTypeIndex, + 'media_by_type': TitleSearchIndex, + 'media_title': TitleIndex, + 'media_startswith': StartsWithIndex, + } + def __init__(self): addApiView('media.refresh', self.refresh, docs = { @@ -57,8 +66,6 @@ class MediaPlugin(MediaBase): addApiView('media.available_chars', self.charView) - addEvent('database.setup', self.databaseSetup) - addEvent('app.load', self.addSingleRefreshView, priority = 100) addEvent('app.load', self.addSingleListView, priority = 100) addEvent('app.load', self.addSingleCharView, priority = 100) @@ -69,51 +76,6 @@ class MediaPlugin(MediaBase): addEvent('media.delete', self.delete) addEvent('media.restatus', self.restatus) - def databaseSetup(self): - - db = get_db() - - # IMDB index - try: - db.add_index(MediaIMDBIndex(db.path, 'media')) - except: - log.debug('Index already exists') - db.edit_index(MediaIMDBIndex(db.path, 'media')) - - # Title index - try: - db.add_index(TitleSearchIndex(db.path, 'media_search_title')) - except: - log.debug('Index already exists') - db.edit_index(TitleSearchIndex(db.path, 'media_search_title')) - - # Status index - try: - db.add_index(MediaStatusIndex(db.path, 'media_status')) - except: - log.debug('Index already exists') - db.edit_index(MediaStatusIndex(db.path, 'media_status')) - - # Type index - try: - db.add_index(MediaTypeIndex(db.path, 'media_by_type')) - except: - log.debug('Index already exists') - db.edit_index(MediaTypeIndex(db.path, 'media_by_type')) - - # Title index - try: db.add_index(TitleIndex(db.path, 'media_title')) - except: - log.debug('Index already exists') - db.edit_index(TitleIndex(db.path, 'media_title')) - - # Startswith index - try: db.add_index(StartsWithIndex(db.path, 'media_startswith')) - except: - log.debug('Index already exists') - db.edit_index(StartsWithIndex(db.path, 'media_startswith')) - - def refresh(self, id = '', **kwargs): handlers = [] ids = splitString(id) diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py index a6fb13d4..3e92a463 100644 --- a/couchpotato/core/notifications/core/index.py +++ b/couchpotato/core/notifications/core/index.py @@ -3,6 +3,7 @@ from CodernityDB.tree_index import TreeBasedIndex class NotificationIndex(TreeBasedIndex): + _version = 1 custom_header = """from CodernityDB.tree_index import TreeBasedIndex import time""" @@ -23,6 +24,7 @@ import time""" class NotificationUnreadIndex(TreeBasedIndex): + _version = 1 custom_header = """from CodernityDB.tree_index import TreeBasedIndex import time""" diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 5610ef28..eefa0dc3 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -18,6 +18,11 @@ log = CPLog(__name__) class CoreNotifier(Notification): + _database = { + 'notification': NotificationIndex, + 'notification_unread': NotificationUnreadIndex + } + m_lock = None listen_to = [ @@ -60,28 +65,10 @@ class CoreNotifier(Notification): addEvent('app.load', self.clean) addEvent('app.load', self.checkMessages) - addEvent('database.setup', self.databaseSetup) - self.messages = [] self.listeners = [] self.m_lock = threading.Lock() - def databaseSetup(self): - - db = get_db() - - try: - db.add_index(NotificationIndex(db.path, 'notification')) - except: - log.debug('Index already exists') - db.edit_index(NotificationIndex(db.path, 'notification')) - - try: - db.add_index(NotificationUnreadIndex(db.path, 'notification_unread')) - except: - log.debug('Index already exists') - db.edit_index(NotificationUnreadIndex(db.path, 'notification_unread')) - def clean(self): try: db = get_db() diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index a315cf81..7ce5db41 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -1,3 +1,4 @@ +from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toSafeString, \ toUnicode, sp @@ -24,6 +25,7 @@ log = CPLog(__name__) class Plugin(object): _class_name = None + _database = None plugin_path = None enabled_option = 'enabled' @@ -53,6 +55,22 @@ class Plugin(object): if self.auto_register_static: self.registerStatic(inspect.getfile(self.__class__)) + # Setup database + if self._database: + addEvent('database.setup', self.databaseSetup) + + def databaseSetup(self): + + db = get_db() + + for index_name in self._database: + klass = self._database[index_name] + + fireEvent('database.setup_index', index_name, klass) + + def afterDatabaseSetup(self): + print self._database_indexes + def conf(self, attr, value = None, default = None, section = None): class_name = self.getName().lower().split(':')[0].lower() return Env.setting(attr, section = section if section else class_name, value = value, default = default) diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py index 107f927d..6445de3c 100644 --- a/couchpotato/core/plugins/category/index.py +++ b/couchpotato/core/plugins/category/index.py @@ -2,6 +2,7 @@ from CodernityDB.tree_index import TreeBasedIndex class CategoryIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = 'i' @@ -16,6 +17,7 @@ class CategoryIndex(TreeBasedIndex): class CategoryMediaIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index 4761c11c..e68b62c1 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -12,6 +12,11 @@ log = CPLog(__name__) class CategoryPlugin(Plugin): + _database = { + 'category': CategoryIndex, + 'category_media': CategoryMediaIndex, + } + def __init__(self): addApiView('category.save', self.save) addApiView('category.save_order', self.saveOrder) @@ -25,25 +30,6 @@ class CategoryPlugin(Plugin): }) addEvent('category.all', self.all) - addEvent('database.setup', self.databaseSetup) - - def databaseSetup(self): - - db = get_db() - - # Category index - try: - db.add_index(CategoryIndex(db.path, 'category')) - except: - log.debug('Index already exists') - db.edit_index(CategoryIndex(db.path, 'category')) - - # Category media_id index - try: - db.add_index(CategoryMediaIndex(db.path, 'category_media')) - except: - log.debug('Index already exists') - db.edit_index(CategoryMediaIndex(db.path, 'category_media')) def allView(self, **kwargs): diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 9c862fcf..1d4313be 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -47,9 +47,9 @@ class FileManager(Plugin): for x in file_dict.keys(): files.extend(file_dict[x]) - for file in scandir.scandir(cache_dir): - if os.path.splitext(file.name)[1] in ['.png', '.jpg', '.jpeg']: - file_path = os.path.join(cache_dir, file.name) + for f in scandir.scandir(cache_dir): + if os.path.splitext(f.name)[1] in ['.png', '.jpg', '.jpeg']: + file_path = os.path.join(cache_dir, f.name) if toUnicode(file_path) not in files: os.remove(file_path) except: diff --git a/couchpotato/core/plugins/profile/index.py b/couchpotato/core/plugins/profile/index.py index dbba1632..c2bf9445 100644 --- a/couchpotato/core/plugins/profile/index.py +++ b/couchpotato/core/plugins/profile/index.py @@ -2,6 +2,7 @@ from CodernityDB.tree_index import TreeBasedIndex class ProfileIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = 'i' diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 53a20d58..401fe48a 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -12,7 +12,9 @@ log = CPLog(__name__) class ProfilePlugin(Plugin): - to_dict = {'types': {}} + _database = { + 'profile': ProfileIndex + } def __init__(self): addEvent('profile.all', self.all) @@ -29,21 +31,9 @@ class ProfilePlugin(Plugin): }"""} }) - addEvent('database.setup', self.databaseSetup) - addEvent('app.initialize', self.fill, priority = 90) addEvent('app.load', self.forceDefaults) - def databaseSetup(self): - - db = get_db() - - try: - db.add_index(ProfileIndex(db.path, 'profile')) - except: - log.debug('Index already exists') - db.edit_index(ProfileIndex(db.path, 'profile')) - def forceDefaults(self): # Get all active movies without profile diff --git a/couchpotato/core/plugins/quality/index.py b/couchpotato/core/plugins/quality/index.py index cf2d5c77..6d160a7b 100644 --- a/couchpotato/core/plugins/quality/index.py +++ b/couchpotato/core/plugins/quality/index.py @@ -3,6 +3,7 @@ from hashlib import md5 class QualityIndex(HashIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 4e23ea31..345da394 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -14,6 +14,10 @@ log = CPLog(__name__) class QualityPlugin(Plugin): + _database = { + 'quality': QualityIndex + } + qualities = [ {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, @@ -49,7 +53,6 @@ class QualityPlugin(Plugin): }) addEvent('app.initialize', self.fill, priority = 10) - addEvent('database.setup', self.databaseSetup) addEvent('app.test', self.doTest) @@ -63,17 +66,6 @@ class QualityPlugin(Plugin): def getOrder(self): return self.order - def databaseSetup(self): - - db = get_db() - - # Quality index - try: - db.add_index(QualityIndex(db.path, 'quality')) - except: - log.debug('Index already exists') - db.edit_index(QualityIndex(db.path, 'quality')) - def preReleases(self): return self.pre_releases diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 2c343716..d6978bbf 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -4,6 +4,7 @@ from CodernityDB.tree_index import TreeBasedIndex class ReleaseIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' @@ -30,6 +31,7 @@ class ReleaseIndex(TreeBasedIndex): class ReleaseStatusIndex(TreeBasedIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' @@ -44,6 +46,7 @@ class ReleaseStatusIndex(TreeBasedIndex): class ReleaseIDIndex(HashIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' @@ -58,6 +61,7 @@ class ReleaseIDIndex(HashIndex): class ReleaseDownloadIndex(HashIndex): + _version = 1 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 20d67bb9..b18e4336 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -17,6 +17,13 @@ log = CPLog(__name__) class Release(Plugin): + _database = { + 'release': ReleaseIndex, + 'release_status': ReleaseStatusIndex, + 'release_identifier': ReleaseIDIndex, + 'release_download': ReleaseDownloadIndex + } + def __init__(self): addApiView('release.manual_download', self.manualDownload, docs = { 'desc': 'Send a release manually to the downloaders', @@ -45,44 +52,10 @@ class Release(Plugin): addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) - addEvent('database.setup', self.databaseSetup) - # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanDone) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4) - def databaseSetup(self): - - db = get_db() - - # Release media_id index - try: - db.add_index(ReleaseIndex(db.path, 'release')) - except: - log.debug('Index already exists') - db.edit_index(ReleaseIndex(db.path, 'release')) - - # Release status index - try: - db.add_index(ReleaseStatusIndex(db.path, 'release_status')) - except: - log.debug('Index already exists') - db.edit_index(ReleaseStatusIndex(db.path, 'release_status')) - - # Release identifier index - try: - db.add_index(ReleaseIDIndex(db.path, 'release_identifier')) - except: - log.debug('Index already exists') - db.edit_index(ReleaseIDIndex(db.path, 'release_identifier')) - - # Release identifier index - try: - db.add_index(ReleaseDownloadIndex(db.path, 'release_download')) - except: - log.debug('Index already exists') - db.edit_index(ReleaseDownloadIndex(db.path, 'release_download')) - def cleanDone(self): log.debug('Removing releases from dashboard') diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index f5928b85..202f5dc6 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -141,4 +141,4 @@ if(document.location.href.indexOf(host) == -1) else setVersion(); -} \ No newline at end of file +} diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/providers/torrent/torrentpotato/main.py index a58fd61c..cfd0339c 100644 --- a/couchpotato/core/providers/torrent/torrentpotato/main.py +++ b/couchpotato/core/providers/torrent/torrentpotato/main.py @@ -75,7 +75,7 @@ class TorrentPotato(TorrentProvider): pass_keys = splitString(self.conf('pass_key'), clean = False) extra_score = splitString(self.conf('extra_score'), clean = False) - list = [] + host_list = [] for nr in range(len(hosts)): try: key = pass_keys[nr] @@ -93,7 +93,7 @@ class TorrentPotato(TorrentProvider): try: seed_time = seed_times[nr] except: seed_time = '' - list.append({ + host_list.append({ 'use': uses[nr], 'host': host, 'name': name, @@ -103,7 +103,7 @@ class TorrentPotato(TorrentProvider): 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 }) - return list + return host_list def belongsTo(self, url, provider = None, host = None): diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings.py similarity index 93% rename from couchpotato/core/settings/__init__.py rename to couchpotato/core/settings.py index 23f48b13..9c1fbc88 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings.py @@ -1,11 +1,12 @@ from __future__ import with_statement import traceback +from CodernityDB.hash_index import HashIndex from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat -from couchpotato.core.settings.index import PropertyIndex import ConfigParser +from hashlib import md5 class Settings(object): @@ -73,7 +74,7 @@ class Settings(object): try: db.add_index(PropertyIndex(db.path, 'property')) except: - self.log.debug('Index already exists') + self.log.debug('Index for properties already exists') db.edit_index(PropertyIndex(db.path, 'property')) def parser(self): @@ -250,3 +251,17 @@ class Settings(object): 'identifier': identifier, 'value': toUnicode(value), }) + +class PropertyIndex(HashIndex): + _version = 1 + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(PropertyIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return md5(key).hexdigest() + + def make_key_value(self, data): + if data.get('_t') == 'property': + return md5(data['identifier']).hexdigest(), None diff --git a/couchpotato/core/settings/index.py b/couchpotato/core/settings/index.py deleted file mode 100644 index 68ec52d8..00000000 --- a/couchpotato/core/settings/index.py +++ /dev/null @@ -1,16 +0,0 @@ -from CodernityDB.hash_index import UniqueHashIndex, HashIndex -from hashlib import md5 - - -class PropertyIndex(HashIndex): - - def __init__(self, *args, **kwargs): - kwargs['key_format'] = '32s' - super(PropertyIndex, self).__init__(*args, **kwargs) - - def make_key(self, key): - return md5(key).hexdigest() - - def make_key_value(self, data): - if data.get('_t') == 'property': - return md5(data['identifier']).hexdigest(), None diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 40d1d7af..f7cfd0af 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -1,3 +1,4 @@ +from couchpotato.core.database import Database from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings @@ -14,6 +15,7 @@ class Env(object): _debug = False _dev = False _settings = Settings() + _database = Database() _loader = Loader() _cache = None _options = None diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css index 0c9f0f08..f8d49afa 100644 --- a/couchpotato/static/style/api.css +++ b/couchpotato/static/style/api.css @@ -92,4 +92,52 @@ pre { .api .return { float: left; width: 700px; - } \ No newline at end of file + } + +.database table { + font-size: 11px; +} + + .database table th { + text-align: left; + } + + .database table tr:hover { + position: relative; + z-index: 20; + } + + .database table td { + vertical-align: top; + position: relative; + } + + .database table .id { + width: 100px; + } + + .database table ._rev { + width: 60px; + } + + .database table ._t { + width: 60px; + } + + .database table .form { + width: 600px; + } + + .database table form { + width: 600px; + } + + .database textarea { + font-size: 12px; + width: 100%; + height: 200px; + } + + .database input[type=submit] { + display: block; + } diff --git a/couchpotato/templates/api.html b/couchpotato/templates/api.html index 1a5c4ce3..11d7ea9d 100644 --- a/couchpotato/templates/api.html +++ b/couchpotato/templates/api.html @@ -10,7 +10,7 @@

CouchPotato API Documentation

You can access the API via
{{ Env.get('api_base') }}
- To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome. + To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome. All the data that you see there are from the API.

@@ -26,7 +26,7 @@ Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
- + {% for route in routes %} {% if api_docs.get(route) %}
@@ -68,4 +68,4 @@
- \ No newline at end of file + diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html new file mode 100644 index 00000000..6dcb35e6 --- /dev/null +++ b/couchpotato/templates/database.html @@ -0,0 +1,115 @@ +{% autoescape None %} + + + + + + + + + + CouchPotato Database Management + + + +

CouchPotato Database Management

+
+ + + From a6be59bbea62b09fb4e5aec0d59ec4eb4c0724c2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Feb 2014 08:16:10 +0100 Subject: [PATCH 018/468] Delete documents --- couchpotato/core/database.py | 4 +++- couchpotato/templates/database.html | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 0f3ece73..39c7ba15 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -60,7 +60,9 @@ class Database(object): db = self.getDB() try: - document = db.get(id) + + document_id = kwargs.get('_request').get_argument('id') + document = db.get('id', document_id) db.delete(document) return { diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html index 6dcb35e6..00ac7937 100644 --- a/couchpotato/templates/database.html +++ b/couchpotato/templates/database.html @@ -55,12 +55,15 @@ (e).stop(); if(confirm('Are you sure?')){ + var a = this; new Request.JSON({ 'url': api_base + 'database.document.delete', - 'data': this.get('data-id'), + 'data': { + 'id': a.get('data-id') + }, 'onSuccess': function(){ - console.log(arguments); + a.getParent('tr').destroy(); } }).send(); } From 3bbeec513a7d6b65e2105e9992181544d691bb35 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Feb 2014 08:24:15 +0100 Subject: [PATCH 019/468] Events --- couchpotato/templates/database.html | 58 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html index 00ac7937..7688bc6f 100644 --- a/couchpotato/templates/database.html +++ b/couchpotato/templates/database.html @@ -38,7 +38,7 @@ ) ), new Element('td.actions').grab( - new Element('a.delete', {'text': 'delete', 'data-id': doc['_id']}) + new Element('a.delete[href=#]', {'text': 'delete', 'data-id': doc['_id']}) ) ).inject(list) }); @@ -51,42 +51,46 @@ var c = $('container'); // Delete - c.addEvent('click:relay(.delete)', function(e){ - (e).stop(); + c.addEvents({ - if(confirm('Are you sure?')){ - var a = this; + // Delete delegate + 'click:relay(.delete)': function(e){ + (e).stop(); + + if(!document.location.href.contains('no_confirm') && confirm('Are you sure?')){ + var a = this; + + new Request.JSON({ + 'url': api_base + 'database.document.delete', + 'data': { + 'id': a.get('data-id') + }, + 'onSuccess': function(){ + a.getParent('tr').destroy(); + } + }).send(); + } + }, + + // Form submit + 'submit:relay(form)': function(e){ + (e).stop(); + + var form = this; new Request.JSON({ - 'url': api_base + 'database.document.delete', + 'url': api_base + 'database.document.update', 'data': { - 'id': a.get('data-id') + 'document': form.getElement('textarea').get('value') }, - 'onSuccess': function(){ - a.getParent('tr').destroy(); + 'onSuccess': function(response){ + form.getElement('textarea').set('value', JSON.stringify(response.document, null, 4)); + form.getParent('tr').getElement('._rev').set('text', response.document['_rev']); } }).send(); } }); - // Form submit - c.addEvent('submit:relay(form)', function(e){ - (e).stop(); - - var form = this; - - new Request.JSON({ - 'url': api_base + 'database.document.update', - 'data': { - 'document': form.getElement('textarea').get('value') - }, - 'onSuccess': function(response){ - form.getElement('textarea').set('value', JSON.stringify(response.document, null, 4)); - form.getParent('tr').getElement('._rev').set('text', response.document['_rev']); - } - }).send(); - }); - new Request.JSON({ 'url': api_base + 'database.list_documents', 'method': 'get', From 061b79eac0fa83a2f53ff47579752e6889b719a2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Feb 2014 08:25:17 +0100 Subject: [PATCH 020/468] Title --- couchpotato/templates/database.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html index 7688bc6f..02a6d561 100644 --- a/couchpotato/templates/database.html +++ b/couchpotato/templates/database.html @@ -111,11 +111,11 @@ - CouchPotato Database Management + Document Manager -

CouchPotato Database Management

+

Documents

From eb1556f3e8879a27dac2a5ebcc0e8c56e1020149 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Feb 2014 21:14:06 +0100 Subject: [PATCH 021/468] Add filter on database manage page --- couchpotato/core/database.py | 11 ++-- couchpotato/static/style/api.css | 89 +++++++++++++++++------------ couchpotato/templates/database.html | 26 ++++++++- 3 files changed, 82 insertions(+), 44 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 39c7ba15..ee27e37e 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -21,13 +21,6 @@ class Database(object): addEvent('database.setup_index', self.setupIndex) - def test(): - time.sleep(1) - self.listDocuments() - - addEvent('app.load', test) - - def getDB(self): if not self.db: @@ -103,6 +96,10 @@ class Database(object): for document in db.all('id'): key = document.get('_t', 'unknown') + + if kwargs.get('show') and key != kwargs.get('show'): + continue + if not results.get(key): results[key] = [] results[key].append(document) diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css index f8d49afa..3d6952d6 100644 --- a/couchpotato/static/style/api.css +++ b/couchpotato/static/style/api.css @@ -94,50 +94,69 @@ pre { width: 700px; } -.database table { - font-size: 11px; +.database { + padding: 20px; + margin: 0; } - .database table th { - text-align: left; + .database * { + margin: 0; + padding: 0; } - .database table tr:hover { - position: relative; - z-index: 20; + .database .nav { + } + .database .nav li { + display: inline-block; + } + .database .nav li a { + padding: 5px; + } + + .database table { + font-size: 11px; } - .database table td { - vertical-align: top; + .database table th { + text-align: left; + } + + .database table tr:hover { position: relative; + z-index: 20; } - .database table .id { - width: 100px; - } - - .database table ._rev { - width: 60px; - } - - .database table ._t { - width: 60px; - } - - .database table .form { - width: 600px; - } - - .database table form { - width: 600px; - } - - .database textarea { - font-size: 12px; - width: 100%; - height: 200px; + .database table td { + vertical-align: top; + position: relative; } - .database input[type=submit] { - display: block; + .database table .id { + width: 100px; } + + .database table ._rev { + width: 60px; + } + + .database table ._t { + width: 60px; + } + + .database table .form { + width: 600px; + } + + .database table form { + width: 600px; + } + + .database textarea { + font-size: 12px; + width: 100%; + height: 200px; + } + + .database input[type=submit] { + display: block; + } diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html index 02a6d561..b12ad23e 100644 --- a/couchpotato/templates/database.html +++ b/couchpotato/templates/database.html @@ -91,12 +91,34 @@ } }); + var show_split = document.location.search.split('show='), + show = show_split.length > 1 ? show_split[1] : null; + new Request.JSON({ 'url': api_base + 'database.list_documents', + 'data': show ? 'show='+show : null, 'method': 'get', 'onSuccess': function(data){ + var nav = new Element('ul.nav').grab( + new Element('li').grab( + new Element('a', { + 'href': './', + 'text': 'All' + }) + ) + ).inject(c, 'before'); + Object.each(data, function(documents, name){ + + if(documents.length > 0) + new Element('li').grab( + new Element('a', { + 'href': './?show=' + name, + 'text': name + }) + ).inject(nav) + var list = createList(name, documents); if(list) list.inject(c); @@ -113,10 +135,10 @@ Document Manager - +

Documents

-
+
From 1f982b79997c6f39070b66770f452b2039c58fbf Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 14 Feb 2014 19:36:23 +0100 Subject: [PATCH 022/468] Migration start --- couchpotato/core/database.py | 111 +++++++++++++++++++++++++++++++++++ couchpotato/runner.py | 2 + 2 files changed, 113 insertions(+) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index ee27e37e..4376cf00 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -1,9 +1,11 @@ import json +import os import time import traceback from couchpotato import CPLog from couchpotato.api import addApiView from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import toUnicode log = CPLog(__name__) @@ -20,6 +22,7 @@ class Database(object): addApiView('database.document.delete', self.deleteDocument) addEvent('database.setup_index', self.setupIndex) + addEvent('app.migrate', self.migrate) def getDB(self): @@ -106,3 +109,111 @@ class Database(object): return results + + def migrate(self): + time.sleep(1) + + from couchpotato import Env + old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db') + + if os.path.isfile(old_db): + + import sqlite3 + conn = sqlite3.connect(old_db) + + c = conn.cursor() + + migrate_list = { + 'category': ['id', 'label', 'order', 'required', 'preferred', 'ignored', 'destination'], + 'profile': ['id', 'label', 'order', 'core', 'hide'], + 'profiletype': ['id', 'order', 'finish', 'wait_for', 'quality_id', 'profile_id'], + 'quality': ['id', 'identifier', 'order', 'size_min', 'size_max'], + 'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'], + 'library': ['id', 'identifier'], + 'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'], + 'status': ['id', 'identifier'], + 'properties': ['id', 'identifier', 'value'], + } + + migrate_data = {} + + for ml in migrate_list: + migrate_data[ml] = {} + rows = migrate_list[ml] + c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml)) + for p in c.fetchall(): + columns = {} + for row in migrate_list[ml]: + columns[row] = p[rows.index(row)] + migrate_data[ml][p[0]] = columns + + db = self.getDB() + + # Categories + categories = migrate_data['category'] + category_link = {} + for x in categories: + continue + + c = categories[x] + new_c = db.insert({ + '_t': 'category', + 'order': c.get('order', 999), + 'label': toUnicode(c.get('label', '')), + 'ignored': toUnicode(c.get('ignored', '')), + 'preferred': toUnicode(c.get('preferred', '')), + 'required': toUnicode(c.get('required', '')), + 'destination': toUnicode(c.get('destination', '')), + }) + + category_link[x] = new_c.get('_id') + + # Profiles + new_profiles = db.all('profile', with_doc = True) + new_profiles_by_label = {} + for x in new_profiles: + + # Remove default non core profiles + if not x['doc'].get('core'): + db.delete(x['doc']) + else: + new_profiles_by_label[x['doc']['label']] = x['_id'] + + profiles = migrate_data['profile'] + profile_link = {} + for x in profiles: + p = profiles[x] + + exists = new_profiles_by_label.get(p.get('label')) + + # Update existing with order only + if exists and p.get('core'): + profile = db.get('id', exists) + profile['order'] = p.get('order') + db.update(profile) + + profile_link[x] = profile.get('_id') + else: + + new_profile = { + '_t': 'profile', + 'label': p.get('label'), + 'order': int(p.get('order', 999)), + 'core': p.get('core', False), + 'qualities': [], + 'wait_for': [], + 'finish': [] + } + + types = migrate_data['profiletype'] + for profile_type in types: + p_type = types[profile_type] + if types[profile_type]['profile_id'] == p['id']: + new_profile['finish'].append(p_type['finish']) + new_profile['wait_for'].append(p_type['wait_for']) + new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier']) + print new_profile + + new_profile.update(db.insert(new_profile)) + + profile_link[x] = new_profile.get('_id') diff --git a/couchpotato/runner.py b/couchpotato/runner.py index c79317ae..86ec2b26 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -89,6 +89,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db_exists = db.exists() if db_exists: db.open() + db.compact() else: db.create() @@ -223,6 +224,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En fireEvent('database.setup') if not db_exists: fireEvent('app.initialize', in_order = True) + fireEvent('app.migrate') # Go go go! from tornado.ioloop import IOLoop From 04fb81e071c8e97396acfe6b21801f50d431eed1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 4 Mar 2014 22:42:03 +0100 Subject: [PATCH 023/468] More import --- couchpotato/core/database.py | 90 +++++++++++++++++++++- couchpotato/core/media/movie/_base/main.py | 6 +- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 4376cf00..80ff9c38 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -4,7 +4,7 @@ import time import traceback from couchpotato import CPLog from couchpotato.api import addApiView -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode log = CPLog(__name__) @@ -129,8 +129,10 @@ class Database(object): 'profiletype': ['id', 'order', 'finish', 'wait_for', 'quality_id', 'profile_id'], 'quality': ['id', 'identifier', 'order', 'size_min', 'size_max'], 'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'], - 'library': ['id', 'identifier'], + 'library': ['id', 'identifier', 'info'], + 'librarytitle': ['id', 'title', 'default', 'libraries_id'], 'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'], + 'releaseinfo': ['id', 'identifier', 'value', 'release_id'], 'status': ['id', 'identifier'], 'properties': ['id', 'identifier', 'value'], } @@ -150,7 +152,7 @@ class Database(object): db = self.getDB() # Categories - categories = migrate_data['category'] + categories = migrate_data.get('category', []) category_link = {} for x in categories: continue @@ -212,8 +214,88 @@ class Database(object): new_profile['finish'].append(p_type['finish']) new_profile['wait_for'].append(p_type['wait_for']) new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier']) - print new_profile new_profile.update(db.insert(new_profile)) profile_link[x] = new_profile.get('_id') + + # Qualities + new_qualities = db.all('quality', with_doc = True) + new_qualities_by_identifier = {} + for x in new_qualities: + new_qualities_by_identifier[x['doc']['identifier']] = x['_id'] + + qualities = migrate_data['quality'] + quality_link = {} + for x in qualities: + q = qualities[x] + q_id = new_qualities_by_identifier[q.get('identifier')] + + quality = db.get('id', q_id) + quality['order'] = q.get('order') + quality['size_min'] = q.get('size_min') + quality['size_max'] = q.get('size_max') + db.update(quality) + + quality_link[x] = quality + + # Titles + titles = migrate_data['librarytitle'] + titles_by_library = {} + for x in titles: + title = titles[x] + if title.get('default'): + titles_by_library[title.get('libraries_id')] = title.get('title') + + # Releases + releaseinfos = migrate_data['releaseinfo'] + for x in releaseinfos: + info = releaseinfos[x] + if not migrate_data['release'][info.get('release_id')].get('info'): + migrate_data['release'][info.get('release_id')]['info'] = {} + + migrate_data['release'][info.get('release_id')]['info'][info.get('identifier')] = info.get('value') + + releases = migrate_data['release'] + releases_by_media = {} + for x in releases: + release = releases[x] + if not releases_by_media.get(release.get('movie_id')): + releases_by_media[release.get('movie_id')] = [] + + releases_by_media[release.get('movie_id')].append(release) + + # Media + statuses = migrate_data['status'] + libraries = migrate_data['library'] + medias = migrate_data['movie'] + media_link = {} + for x in medias: + m = medias[x] + + status = statuses.get(m['status_id']).get('identifier') + if status != 'active': continue + + l = libraries[m['library_id']] + profile_id = profile_link.get(m['profile_id']) + category_id = category_link.get(m['category_id']) + title = titles_by_library.get(m['library_id']) + releases = releases_by_media.get(x, []) + + added_media = fireEvent('movie.add', { + 'info': json.loads(l.get('info', '')), + 'identifier': l.get('identifier'), + 'profile_id': profile_id, + 'category_id': category_id, + 'title': title + }, force_readd = False, search_after = False, status = status, single = True) + + for rel in releases: + if not rel.get('info'): continue + + quality = quality_link[rel.get('quality_id')] + added_rel = fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) + + # Update status + added_id = db.get('release_identifier', added_rel[0]) + fireEvent('release.update_status', added_id.get('_id'), statuses[rel.get('status_id')].get('identifier')) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 54efd677..1beb463a 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -53,7 +53,7 @@ class MovieBase(MovieTypeBase): log.error(msg) fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) return False - else: + elif not params.get('info'): try: is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True) if not is_movie: @@ -64,7 +64,9 @@ class MovieBase(MovieTypeBase): except: pass - info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) + info = params.get('info') + if not info: + info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) # Set default title default_title = toUnicode(info.get('title')) From f0044a93427ca6fc3418f35b87a76040dd966272 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 11:43:50 +0100 Subject: [PATCH 024/468] Use correct index for movie_type --- couchpotato/core/media/_base/media/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 472e4d29..97d874c2 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -18,7 +18,7 @@ class MediaPlugin(MediaBase): 'media': MediaIMDBIndex, 'media_search_title': MediaStatusIndex, 'media_status': MediaTypeIndex, - 'media_by_type': TitleSearchIndex, + 'media_by_type': MediaTypeIndex, 'media_title': TitleIndex, 'media_startswith': StartsWithIndex, } From a0bcb03dde3848365b44236b876194983e0fd714 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 15:12:00 +0100 Subject: [PATCH 025/468] Index fixes --- couchpotato/core/media/_base/media/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 97d874c2..5066cdc6 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -16,8 +16,8 @@ class MediaPlugin(MediaBase): _database = { 'media': MediaIMDBIndex, - 'media_search_title': MediaStatusIndex, - 'media_status': MediaTypeIndex, + 'media_search_title': TitleSearchIndex, + 'media_status': MediaStatusIndex, 'media_by_type': MediaTypeIndex, 'media_title': TitleIndex, 'media_startswith': StartsWithIndex, @@ -163,7 +163,7 @@ class MediaPlugin(MediaBase): else: all_media_ids = set([x['_id'] for x in db.all('media')]) - media_ids = all_media_ids + media_ids = list(all_media_ids) filter_by = {} # Filter on movie status From ff600133356018340bdaa63b9102135b2a2edc5a Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 15:24:08 +0100 Subject: [PATCH 026/468] Only use filename for cache api call --- couchpotato/core/media/movie/_base/static/movie.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 835847e3..fd359f95 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -158,7 +158,7 @@ var Movie = new Class({ }), self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', { 'class': 'type_image poster', - 'src': Api.createUrl('file.cache') + (self.data.files || {})['image_poster'][0] + 'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop() }): null, self.data_container = new Element('div.data.inlay.light').adopt( self.info_container = new Element('div.info').adopt( From 2632b344387e4b425a5c50ce903932ed6cf307ed Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 15:24:19 +0100 Subject: [PATCH 027/468] Use proper default id --- couchpotato/core/plugins/profile/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 401fe48a..c978e6ae 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -42,10 +42,11 @@ class ProfilePlugin(Plugin): medias = db.run('media', 'with_status', ['active']) profile_ids = [x.get('_id') for x in self.all()] + default_id = profile_ids[0] for media in medias: if media.get('profile_id') not in profile_ids: - media['profile_id'] = profile_ids[0].get('_id') + media['profile_id'] = default_id db.update(media) except: log.error('Failed: %s', traceback.format_exc()) From b25e0ea3934ce7011f2bff1bfe916beb3f2cffbd Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 15:24:33 +0100 Subject: [PATCH 028/468] Remove file_type references --- couchpotato/core/providers/metadata/base.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 0ae0c3e2..39ee4945 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -89,19 +89,12 @@ class MetaDataBase(Plugin): def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original'): if not data: data = {} if not movie_info: movie_info = {} - file_types = fireEvent('file.types', single = True) - file_type = {} - - for ft in file_types: - # TODO: change type to "image_"+wanted_file_type - if ft.get('identifier') == wanted_file_type: - file_type = ft - break # See if it is in current files - for cur_file in data.get('files', []): - if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')): - return cur_file.get('path') + files = data.get('files', {}) + if files.get('image_'+wanted_file_type): + if os.path.isfile(files['image_'+wanted_file_type][0]): + return files['image_'+wanted_file_type][0] # Download using existing info try: From 81ba95f5400a887d69280991d39cadcaeb09e23c Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 15:41:16 +0100 Subject: [PATCH 029/468] Migrate library file info --- couchpotato/core/database.py | 48 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 80ff9c38..e7e3af7f 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -111,6 +111,7 @@ class Database(object): return results def migrate(self): + time.sleep(1) from couchpotato import Env @@ -131,10 +132,13 @@ class Database(object): 'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'], 'library': ['id', 'identifier', 'info'], 'librarytitle': ['id', 'title', 'default', 'libraries_id'], + 'library_files__file_library': ['library_id', 'file_id'], 'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'], 'releaseinfo': ['id', 'identifier', 'value', 'release_id'], 'status': ['id', 'identifier'], 'properties': ['id', 'identifier', 'value'], + 'file': ['id', 'path', 'type_id'], + 'filetype': ['identifier', 'id'] } migrate_data = {} @@ -147,7 +151,13 @@ class Database(object): columns = {} for row in migrate_list[ml]: columns[row] = p[rows.index(row)] - migrate_data[ml][p[0]] = columns + + if not migrate_data[ml].get(p[0]): + migrate_data[ml][p[0]] = columns + else: + if not isinstance(migrate_data[ml][p[0]], list): + migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]] + migrate_data[ml][p[0]].append(columns) db = self.getDB() @@ -268,12 +278,17 @@ class Database(object): # Media statuses = migrate_data['status'] libraries = migrate_data['library'] + library_files = migrate_data['library_files__file_library'] + all_files = migrate_data['file'] + poster_type = migrate_data['filetype']['poster'] medias = migrate_data['movie'] media_link = {} for x in medias: m = medias[x] status = statuses.get(m['status_id']).get('identifier') + + # Only migrate wanted movies if status != 'active': continue l = libraries[m['library_id']] @@ -281,6 +296,9 @@ class Database(object): category_id = category_link.get(m['category_id']) title = titles_by_library.get(m['library_id']) releases = releases_by_media.get(x, []) + files = library_files.get(m['library_id'], []) + if not isinstance(files, list): + files = [files] added_media = fireEvent('movie.add', { 'info': json.loads(l.get('info', '')), @@ -290,6 +308,17 @@ class Database(object): 'title': title }, force_readd = False, search_after = False, status = status, single = True) + for f in files: + file = all_files[f.get('file_id')] + + # Only migrate posters + if file.get('type_id') == poster_type.get('id'): + if not added_media['files'].get('image_poster'): + added_media['files']['image_poster'] = [] + if file.get('path') not in added_media['files']['image_poster']: + added_media['files']['image_poster'].append(file.get('path')) + db.update(added_media) + for rel in releases: if not rel.get('info'): continue @@ -297,5 +326,18 @@ class Database(object): added_rel = fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) # Update status - added_id = db.get('release_identifier', added_rel[0]) - fireEvent('release.update_status', added_id.get('_id'), statuses[rel.get('status_id')].get('identifier')) + added_rel['status'] = status + added_rel['last_edit'] = int(time.time()) + db.update(added_rel) + + break + + return + + # rename old database + os.rename(old_db, old_db + '.old') + + if os.path.isfile(old_db + '-wal'): + os.rename(old_db + '-wal', old_db + '-wal.old') + if os.path.isfile(old_db + '-shm'): + os.rename(old_db + '-shm', old_db + '-shm.old') \ No newline at end of file From 2ce0f7beb4ef81addedd662b42753e6a7bdc5384 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 16:27:08 +0100 Subject: [PATCH 030/468] Migrate update --- couchpotato/core/database.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index e7e3af7f..f8bfcc57 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -6,6 +6,7 @@ from couchpotato import CPLog from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import getImdb log = CPLog(__name__) @@ -112,10 +113,11 @@ class Database(object): def migrate(self): - time.sleep(1) - from couchpotato import Env old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db') + if not os.path.isfile(old_db): return + + time.sleep(1) if os.path.isfile(old_db): @@ -282,31 +284,33 @@ class Database(object): all_files = migrate_data['file'] poster_type = migrate_data['filetype']['poster'] medias = migrate_data['movie'] - media_link = {} for x in medias: m = medias[x] status = statuses.get(m['status_id']).get('identifier') - - # Only migrate wanted movies - if status != 'active': continue - l = libraries[m['library_id']] + + # Only migrate wanted movies, Skip if no identifier present + if status != 'active' or not getImdb(l.get('identifier')): continue + profile_id = profile_link.get(m['profile_id']) category_id = category_link.get(m['category_id']) title = titles_by_library.get(m['library_id']) releases = releases_by_media.get(x, []) + info = json.loads(l.get('info', '')) + files = library_files.get(m['library_id'], []) if not isinstance(files, list): files = [files] added_media = fireEvent('movie.add', { - 'info': json.loads(l.get('info', '')), + 'info': info, 'identifier': l.get('identifier'), 'profile_id': profile_id, 'category_id': category_id, 'title': title - }, force_readd = False, search_after = False, status = status, single = True) + }, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True) + added_media['files'] = added_media.get('files', {}) for f in files: file = all_files[f.get('file_id')] @@ -323,16 +327,10 @@ class Database(object): if not rel.get('info'): continue quality = quality_link[rel.get('quality_id')] - added_rel = fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) - # Update status - added_rel['status'] = status - added_rel['last_edit'] = int(time.time()) - db.update(added_rel) - - break - - return + # Add status to keys + rel['info']['status'] = status + added_rel_identifiers = fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) # rename old database os.rename(old_db, old_db + '.old') From 4625c920c3d9946eac7a6cadeced2578e4926763 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 16:27:27 +0100 Subject: [PATCH 031/468] Stop notify frontend triggering for media add --- couchpotato/core/media/movie/_base/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 1beb463a..8c329a28 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -3,7 +3,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import splitString, tryInt, getTitle +from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase import time @@ -45,7 +45,7 @@ class MovieBase(MovieTypeBase): addEvent('movie.update_info', self.updateInfo) addEvent('movie.update_release_dates', self.updateReleaseDate) - def add(self, params = None, force_readd = True, search_after = True, status = None): + def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): if not params: params = {} if not params.get('identifier'): @@ -150,7 +150,8 @@ class MovieBase(MovieTypeBase): db.update(m) # Do full update to get images etc - fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) + if update_after: + fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) # Remove releases for rel in db.run('release', 'for_media', m['_id']): @@ -163,7 +164,8 @@ class MovieBase(MovieTypeBase): onComplete = self.createOnComplete(m['_id']) onComplete() - if added: + if added and notify_after: + if params.get('title'): message = 'Successfully added "%s" to your wanted list.' % params.get('title', '') else: From 3eaf5c9bc09e46a1725f87e0fba323f30038199e Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 16:27:55 +0100 Subject: [PATCH 032/468] Use status from release when available --- couchpotato/core/plugins/release/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index b18e4336..5cc3c01f 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -349,7 +349,7 @@ class Release(Plugin): 'identifier': rel_identifier, 'media_id': media.get('_id'), 'quality': quality.get('identifier'), - 'status': 'available', + 'status': rel.get('status', 'available'), 'last_edit': int(time.time()), 'info': {} } From e51e6b2171f2aea8e4b4932dd5bd329a4c0b7c00 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 5 Mar 2014 16:43:35 +0100 Subject: [PATCH 033/468] Use correct release status Remove continue from category migrate --- couchpotato/core/database.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index f8bfcc57..3f4cf376 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -167,9 +167,8 @@ class Database(object): categories = migrate_data.get('category', []) category_link = {} for x in categories: - continue - c = categories[x] + new_c = db.insert({ '_t': 'category', 'order': c.get('order', 999), @@ -329,8 +328,8 @@ class Database(object): quality = quality_link[rel.get('quality_id')] # Add status to keys - rel['info']['status'] = status - added_rel_identifiers = fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) + rel['info']['status'] = statuses.get(rel.get('status_id')).get('identifier') + fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) # rename old database os.rename(old_db, old_db + '.old') From d16dd7c75d6ee805ac4a7d5a716948aa2cf1841d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 6 Mar 2014 21:30:59 +0100 Subject: [PATCH 034/468] Use proper sorting --- couchpotato/core/event.py | 9 ++------- couchpotato/core/helpers/variable.py | 13 +++---------- libs/axl/axel.py | 7 +++++-- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index a36c430d..8af2967e 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -1,5 +1,5 @@ from axl.axel import Event -from couchpotato.core.helpers.variable import mergeDicts, natcmp +from couchpotato.core.helpers.variable import mergeDicts, natsortKey from couchpotato.core.logger import CPLog import threading import traceback @@ -51,11 +51,6 @@ def addEvent(name, handler, priority = 100): }) -def removeEvent(name, handler): - e = events[name] - e -= handler - - def fireEvent(name, *args, **kwargs): if name not in events: return @@ -106,7 +101,7 @@ def fireEvent(name, *args, **kwargs): result = e(*args, **kwargs) result_keys = result.keys() - result_keys.sort(natcmp) + result_keys.sort(key = natsortKey) if options['single'] and not options['merge']: results = None diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 7977688c..775765e9 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -214,16 +214,9 @@ def tryFloat(s): return float(s) except: return 0 - -def natsortKey(s): - return map(tryInt, re.findall(r'(\d+|\D+)', s)) - - -def natcmp(a, b): - a2 = natsortKey(a) - b2 = natsortKey(b) - - return (a2 > b2) - (a2 < b2) +def natsortKey(string_): + """See http://www.codinghorror.com/blog/archives/001018.html""" + return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)] def toIterable(value): diff --git a/libs/axl/axel.py b/libs/axl/axel.py index 46940da7..d0f069ab 100644 --- a/libs/axl/axel.py +++ b/libs/axl/axel.py @@ -11,7 +11,7 @@ # Source: http://pypi.python.org/pypi/axel # Docs: http://packages.python.org/axel -from couchpotato.core.helpers.variable import natcmp +from couchpotato.core.helpers.variable import natsortKey import Queue import hashlib import sys @@ -158,7 +158,10 @@ class Event(object): t.daemon = True t.start() - for handler in sorted(self.handlers.iterkeys(), cmp = natcmp): + handler_keys = self.handlers.keys() + handler_keys.sort(key = natsortKey) + + for handler in handler_keys: self.queue.put(handler) if self.asynchronous: From 89e561c99134261fd58f49b2983b4f14ed1e1a39 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 6 Mar 2014 22:39:11 +0100 Subject: [PATCH 035/468] Use time to index notifications --- couchpotato/core/notifications/core/index.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py index 3e92a463..6d65b905 100644 --- a/couchpotato/core/notifications/core/index.py +++ b/couchpotato/core/notifications/core/index.py @@ -17,10 +17,7 @@ import time""" def make_key_value(self, data): if data.get('_t') == 'notification': - added = data.get('added', time.time()) - data['added'] = added - - return added, None + return data.get('time'), None class NotificationUnreadIndex(TreeBasedIndex): @@ -38,7 +35,4 @@ import time""" def make_key_value(self, data): if data.get('_t') == 'notification' and not data.get('read'): - added = data.get('added', time.time()) - data['added'] = added - - return added, None + return data.get('time'), None From 2d28cb68975dd9505ae078d136439f3ede4636db Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 11:18:14 +0100 Subject: [PATCH 036/468] Backup databases --- couchpotato/runner.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 86ec2b26..ba0d97bb 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -7,6 +7,7 @@ from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers +from scandir import scandir from tornado.httpserver import HTTPServer from tornado.web import Application, StaticFileHandler, RedirectHandler from uuid import uuid4 @@ -17,6 +18,8 @@ import sys import time import traceback import warnings +import re +import tarfile def getOptions(base_path, args): @@ -88,13 +91,41 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db = ThreadSafeDatabase(db_path) db_exists = db.exists() if db_exists: + + # Backup before start and cleanup old backups + backup_path = toUnicode(os.path.join(data_dir, 'db_backup')) + backup_count = 5 + existing_backups = [] + if not os.path.isdir(backup_path): os.makedirs(backup_path) + + for root, dirs, files in scandir.walk(backup_path): + for file in files: + ints = re.findall('\d+', file) + + # Delete non zip files + if len(ints) != 1: + os.remove(os.path.join(backup_path, file)) + else: + existing_backups.append((int(ints[0]), file)) + + # Remove all but the last 5 + for eb in existing_backups[:-backup_count]: + os.remove(os.path.join(backup_path, eb[1])) + + # Create new backup + new_backup = toUnicode(os.path.join(backup_path, '%s.tar.gz' % int(time.time()))) + zipf = tarfile.open(new_backup, 'w:gz') + for root, dirs, files in scandir.walk(db_path): + for file in files: + zipf.add(os.path.join(root, file), arcname = 'database/%s' % os.path.join(root[len(db_path)+1:], file)) + zipf.close() + + # Open last db.open() - db.compact() + else: db.create() - # TODO:Backup before start and cleanup old databases - # Register environment settings Env.set('app_dir', toUnicode(base_path)) Env.set('data_dir', toUnicode(data_dir)) From 1d4a7894e8fc203b5949e9e3363bbffbd5df1bdb Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 12:39:58 +0100 Subject: [PATCH 037/468] Fix broken files on refresh --- couchpotato/core/media/movie/_base/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 8c329a28..9878abb0 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -1,3 +1,4 @@ +import os import traceback from couchpotato import get_db from couchpotato.api import addApiView @@ -298,11 +299,17 @@ class MovieBase(MovieTypeBase): continue file_type = 'image_%s' % image_type - if file_type not in media['files']: - file_path = fireEvent('file.download', url = image, single = True) - media['files']['image_%s' % image_type] = [file_path]; + existing_files = list(set(media['files'].get(file_type, []))) + for ef in media['files'].get(file_type, []): + if not os.path.isfile(ef): + existing_files.remove(ef) + media['files'][file_type] = existing_files - break + if file_type not in media['files'] or len(media['files'].get(file_type, [])) == 0: + file_path = fireEvent('file.download', url = image, single = True) + media['files']['image_%s' % image_type] = [file_path] + + break db.update(media) From aa80ed3d4be5eb2785b61c8bd3d3c9f8a5c2ba4a Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 15:33:12 +0100 Subject: [PATCH 038/468] Don't log big object --- couchpotato/core/media/movie/_base/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 9878abb0..45d243d0 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -140,6 +140,8 @@ class MovieBase(MovieTypeBase): m['profile_id'] = params.get('profile_id', default_profile.get('id')) m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) else: + try: del params['info'] + except: pass log.debug('Movie already exists, not updating: %s', params) added = False From ec4e680d62770b282bcaabdf754eb63757e1b7a1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 15:41:08 +0100 Subject: [PATCH 039/468] Always check and remove files on movie info --- couchpotato/core/media/movie/_base/main.py | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 45d243d0..bc16388b 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -296,22 +296,31 @@ class MovieBase(MovieTypeBase): images = info.get('images', []) media['files'] = media.get('files', {}) for image_type in ['poster']: + + # Remove non-existing files + file_type = 'image_%s' % image_type + existing_files = list(set(media['files'].get(file_type, []))) + for ef in media['files'].get(file_type, []): + if not os.path.isfile(ef): + existing_files.remove(ef) + + # Replace new files list + media['files'][file_type] = existing_files + if len(existing_files) == 0: + del media['files'][file_type] + + # Loop over type for image in images.get(image_type, []): if not isinstance(image, (str, unicode)): continue - file_type = 'image_%s' % image_type - existing_files = list(set(media['files'].get(file_type, []))) - for ef in media['files'].get(file_type, []): - if not os.path.isfile(ef): - existing_files.remove(ef) - media['files'][file_type] = existing_files - if file_type not in media['files'] or len(media['files'].get(file_type, [])) == 0: file_path = fireEvent('file.download', url = image, single = True) - media['files']['image_%s' % image_type] = [file_path] - - break + if file_path: + media['files'][file_type] = [file_path] + break + else: + break db.update(media) From d2b82a37b26b28ca77446c8c024cd03ba95b4d85 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 15:42:06 +0100 Subject: [PATCH 040/468] Only migrate file if it is found --- couchpotato/core/database.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 3f4cf376..d9968134 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -168,7 +168,7 @@ class Database(object): category_link = {} for x in categories: c = categories[x] - + new_c = db.insert({ '_t': 'category', 'order': c.get('order', 999), @@ -318,8 +318,9 @@ class Database(object): if file.get('type_id') == poster_type.get('id'): if not added_media['files'].get('image_poster'): added_media['files']['image_poster'] = [] - if file.get('path') not in added_media['files']['image_poster']: + if file.get('path') not in added_media['files']['image_poster'] and os.path.isfile(file.get('path')): added_media['files']['image_poster'].append(file.get('path')) + db.update(added_media) for rel in releases: @@ -337,4 +338,4 @@ class Database(object): if os.path.isfile(old_db + '-wal'): os.rename(old_db + '-wal', old_db + '-wal.old') if os.path.isfile(old_db + '-shm'): - os.rename(old_db + '-shm', old_db + '-shm.old') \ No newline at end of file + os.rename(old_db + '-shm', old_db + '-shm.old') From 8a295c72baef022c575f2723035e9fe830855300 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 15:43:05 +0100 Subject: [PATCH 041/468] Only add single image file --- couchpotato/core/database.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index d9968134..abf32d12 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -312,14 +312,13 @@ class Database(object): added_media['files'] = added_media.get('files', {}) for f in files: - file = all_files[f.get('file_id')] + ffile = all_files[f.get('file_id')] # Only migrate posters - if file.get('type_id') == poster_type.get('id'): - if not added_media['files'].get('image_poster'): - added_media['files']['image_poster'] = [] - if file.get('path') not in added_media['files']['image_poster'] and os.path.isfile(file.get('path')): - added_media['files']['image_poster'].append(file.get('path')) + if ffile.get('type_id') == poster_type.get('id'): + if ffile.get('path') not in added_media['files']['image_poster'] and os.path.isfile(ffile.get('path')): + added_media['files']['image_poster'] = [ffile.get('path')] + break db.update(added_media) From 527d6ab7ff2de479e0f6b60d651b8bbb5851d452 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 15:44:09 +0100 Subject: [PATCH 042/468] Properly offset media listing --- couchpotato/core/media/_base/media/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5066cdc6..5d0784e4 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -197,19 +197,21 @@ class MediaPlugin(MediaBase): if total_count == 0: return 0, [] + offset = 0 + limit = -1 if limit_offset: splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset limit = tryInt(splt[0]) offset = tryInt(0 if len(splt) is 1 else splt[1]) - start = offset * limit - end = start + limit - media_ids = media_ids[start:end] # List movies based on title order medias = [] for m in db.all('media_title'): media_id = m['_id'] if media_id not in media_ids: continue + if offset > 0: + offset -= 1 + continue media = db.run('media', 'to_dict', media_id) @@ -220,7 +222,7 @@ class MediaPlugin(MediaBase): # remove from media ids media_ids.remove(media_id) - if len(media_ids) == 0: break + if len(media_ids) == 0 or len(medias) == limit: break return total_count, medias From 3d6e84e11c3548988768feabb276916027afb54c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 16:05:52 +0100 Subject: [PATCH 043/468] Migrate logging --- couchpotato/core/database.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index abf32d12..e4d58138 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -117,6 +117,7 @@ class Database(object): old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db') if not os.path.isfile(old_db): return + log.info('Migrating database, hold on..') time.sleep(1) if os.path.isfile(old_db): @@ -164,6 +165,7 @@ class Database(object): db = self.getDB() # Categories + log.debug('Importing categories') categories = migrate_data.get('category', []) category_link = {} for x in categories: @@ -182,6 +184,7 @@ class Database(object): category_link[x] = new_c.get('_id') # Profiles + log.debug('Importing profiles') new_profiles = db.all('profile', with_doc = True) new_profiles_by_label = {} for x in new_profiles: @@ -231,6 +234,7 @@ class Database(object): profile_link[x] = new_profile.get('_id') # Qualities + log.debug('Importing quality sizes') new_qualities = db.all('quality', with_doc = True) new_qualities_by_identifier = {} for x in new_qualities: @@ -277,6 +281,7 @@ class Database(object): releases_by_media[release.get('movie_id')].append(release) # Media + log.debug('Importing media: processing %s', len(migrate_data['movie'])) statuses = migrate_data['status'] libraries = migrate_data['library'] library_files = migrate_data['library_files__file_library'] @@ -316,7 +321,7 @@ class Database(object): # Only migrate posters if ffile.get('type_id') == poster_type.get('id'): - if ffile.get('path') not in added_media['files']['image_poster'] and os.path.isfile(ffile.get('path')): + if ffile.get('path') not in added_media['files'].get('image_poster', []) and os.path.isfile(ffile.get('path')): added_media['files']['image_poster'] = [ffile.get('path')] break @@ -332,6 +337,7 @@ class Database(object): fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) # rename old database + log.info('Renaming old database to %s ', old_db + '.old') os.rename(old_db, old_db + '.old') if os.path.isfile(old_db + '-wal'): From dd9c65db4ce628da70545d7b1987744bbe7f1170 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 20:19:52 +0100 Subject: [PATCH 044/468] Migrate properties --- couchpotato/core/database.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index e4d58138..ab82ec18 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -164,6 +164,13 @@ class Database(object): db = self.getDB() + # Use properties + log.debug('Importing properties') + properties = migrate_data['properties'] + for x in properties: + property = properties[x] + Env.prop(property.get('identifier'), property.get('value')) + # Categories log.debug('Importing categories') categories = migrate_data.get('category', []) From cfa87026546c822c0d42a088709be44d19e804ea Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 7 Mar 2014 20:42:08 +0100 Subject: [PATCH 045/468] Close sqlite connection --- couchpotato/core/database.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index ab82ec18..31534286 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -125,8 +125,6 @@ class Database(object): import sqlite3 conn = sqlite3.connect(old_db) - c = conn.cursor() - migrate_list = { 'category': ['id', 'label', 'order', 'required', 'preferred', 'ignored', 'destination'], 'profile': ['id', 'label', 'order', 'core', 'hide'], @@ -146,10 +144,13 @@ class Database(object): migrate_data = {} + c = conn.cursor() + for ml in migrate_list: migrate_data[ml] = {} rows = migrate_list[ml] c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml)) + for p in c.fetchall(): columns = {} for row in migrate_list[ml]: @@ -162,18 +163,20 @@ class Database(object): migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]] migrate_data[ml][p[0]].append(columns) + c.close() + db = self.getDB() # Use properties - log.debug('Importing properties') properties = migrate_data['properties'] + log.info('Importing %s properties', len(properties)) for x in properties: property = properties[x] Env.prop(property.get('identifier'), property.get('value')) # Categories - log.debug('Importing categories') categories = migrate_data.get('category', []) + log.info('Importing %s categories', len(categories)) category_link = {} for x in categories: c = categories[x] @@ -288,7 +291,7 @@ class Database(object): releases_by_media[release.get('movie_id')].append(release) # Media - log.debug('Importing media: processing %s', len(migrate_data['movie'])) + log.debug('Importing %s media items', len(migrate_data['movie'])) statuses = migrate_data['status'] libraries = migrate_data['library'] library_files = migrate_data['library_files__file_library'] @@ -343,11 +346,11 @@ class Database(object): rel['info']['status'] = statuses.get(rel.get('status_id')).get('identifier') fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) - # rename old database - log.info('Renaming old database to %s ', old_db + '.old') - os.rename(old_db, old_db + '.old') + # rename old database + log.info('Renaming old database to %s ', old_db + '.old') + os.rename(old_db, old_db + '.old') - if os.path.isfile(old_db + '-wal'): - os.rename(old_db + '-wal', old_db + '-wal.old') - if os.path.isfile(old_db + '-shm'): - os.rename(old_db + '-shm', old_db + '-shm.old') + if os.path.isfile(old_db + '-wal'): + os.rename(old_db + '-wal', old_db + '-wal.old') + if os.path.isfile(old_db + '-shm'): + os.rename(old_db + '-shm', old_db + '-shm.old') From a7a4499dd48d41930908ec5bfed657358210cdee Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 10:31:59 +0100 Subject: [PATCH 046/468] Thumbnail check --- couchpotato/core/media/movie/_base/static/movie.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index fd359f95..2b0c7b73 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -187,7 +187,7 @@ var Movie = new Class({ ) ); - if(!self.thumbnail || self.thumbnail.empty) + if(!self.thumbnail) self.el.addClass('no_thumbnail'); //self.changeView(self.view); From 9f4ea662da1feb80637145e9654beb2d9255a757 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 11:59:12 +0100 Subject: [PATCH 047/468] Use natural sorting Conflicts: couchpotato/core/helpers/request.py --- couchpotato/core/helpers/request.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index b59673f9..a0baa85f 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -1,4 +1,5 @@ from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import natsortKey from urllib import unquote import re @@ -7,9 +8,13 @@ def getParams(params): reg = re.compile('^[a-z0-9_\.]+$') - temp = {} + # Sort keys + param_keys = params.keys() + param_keys.sort(key = natsortKey) - for param, value in sorted(params.items()): + temp = {} + for param in param_keys: + value = params[param] nest = re.split("([\[\]]+)", param) if len(nest) > 1: From ca34cbd1804c963f97e274aedf6c3062c94bd1b8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 12:34:42 +0100 Subject: [PATCH 048/468] Check for year in coming soon --- couchpotato/core/plugins/dashboard/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 7f7c4fe9..d0d3bfef 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -1,3 +1,4 @@ +from datetime import date from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent @@ -43,6 +44,8 @@ class Dashboard(Plugin): active_ids = [x['_id'] for x in db.run('media', 'with_status', 'active', with_doc = False)] medias = [] + now_year = date.today().year + if len(active_ids) > 0: # Order by title or randomize @@ -70,8 +73,8 @@ class Dashboard(Plugin): if coming_soon: # Don't list older movies - if ((not late and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or - (late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))): + if ((not late and (media['info']['year'] >= now_year-1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or + (late and ((media['info']['year'] < now_year-1) or ((eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))))): medias.append(media) if len(medias) >= limit: From f8bfd6fd3f0823bae90542d2b81302a865a2f0bd Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 16:50:53 +0100 Subject: [PATCH 049/468] Coming soon thumbnail height fix --- couchpotato/core/media/movie/_base/static/movie.css | 1 + couchpotato/static/scripts/page/home.js | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index a88a2077..b15b7ccf 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -127,6 +127,7 @@ margin: 0; padding: 0; vertical-align: top; + line-height: 0; } @media all and (max-width: 800px) { diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index dbf8d7b8..84033f15 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -106,10 +106,9 @@ Page.Home = new Class({ timer, highest = 100; - images.each(function(img_container){ - img_container.getElements('img').addEvent('load', function(){ - var img = this, - height = img.getSize().y; + images.each(function(img){ + img.addEvent('load', function(){ + var height = img.getSize().y; if(!highest || highest < height){ highest = height; if(timer) clearTimeout(timer); @@ -124,10 +123,8 @@ Page.Home = new Class({ if(timer) clearTimeout(timer); timer = (function(){ var highest = 100; - images.each(function(img_container){ - var img = img_container.getElement('img'); - if(!img) return - + images.each(function(img){ + img.setStyle('height', null); var height = img.getSize().y; if(!highest || highest < height) highest = height; From 274e2c1cc2de5e8030fc5109fb983ee5839d814f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 20:22:35 +0100 Subject: [PATCH 050/468] Use proper download_info dict --- couchpotato/core/plugins/renamer/main.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index f2feefd0..00cdbc09 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -862,12 +862,14 @@ Remove it if you want it to be renamed (again, or at least let it try again) no_status_support = [] try: for rel in rels: - if rel['info'].get('download_id') and rel['info'].get('download_downloader'): - download_ids.append({'id': rel['info']['download_id'], 'downloader': rel['info']['download_downloader']}) + if not rel.get('download_info'): continue - ds = rel['info'].get('download_status_support') + if rel['download_info'].get('id') and rel['download_info'].get('downloader'): + download_ids.append(rel['download_info']) + + ds = rel['download_info'].get('status_support') if ds is False or ds == 'False': - no_status_support.append(ss(rel['info'].get('download_downloader'))) + no_status_support.append(ss(rel['download_info'].get('downloader'))) except: log.error('Error getting download IDs from database') self.checking_snatched = False @@ -893,15 +895,16 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: for rel in rels: movie_dict = db.get('id', rel.get('media_id')) + download_info = rel.get('download_info') - if not isinstance(rel['info'], dict): + if not isinstance(download_info, dict): log.error('Faulty release found without any info, ignoring.') fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True) continue # Check if download ID is available - if not rel['info'].get('download_id') or not rel['info'].get('download_downloader'): - log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel['info'].get('download_downloader', 'unknown'), rel['info']['name'])) + if not download_info.get('id') or not download_info.get('downloader'): + log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (download_info.get('downloader', 'unknown'), rel['info']['name'])) scan_required = True # Continue with next release @@ -913,8 +916,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) found_release = False for release_download in release_downloads: found_release = False - if rel['info'].get('download_id'): - if release_download['id'] == rel['info']['download_id'] and release_download['downloader'] == rel['info']['download_downloader']: + if download_info.get('id'): + if release_download['id'] == download_info['id'] and release_download['downloader'] == download_info['downloader']: log.debug('Found release by id: %s', release_download['id']) found_release = True break From f5d29eafe049f694e52921a34e9956980eea2401 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 22:14:54 +0100 Subject: [PATCH 051/468] Extend title helper --- couchpotato/core/helpers/variable.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 4eb004d4..ede89ccc 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -236,8 +236,11 @@ def getTitle(media_dict): try: return media_dict['info']['titles'][0] except: - log.error('Could not get title for %s', media_dict.get('identifier')) - return None + try: + return media_dict['media']['info']['titles'][0] + except: + log.error('Could not get title for %s', media_dict.get('identifier')) + return None log.error('Could not get title for %s', media_dict['identifier']) return None From c9be74ce8019575505ddf423ad54f29c6a8d6908 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 22:15:07 +0100 Subject: [PATCH 052/468] Use correct keys when renaming media --- couchpotato/core/plugins/renamer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 00cdbc09..8d145500 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -227,7 +227,7 @@ class Renamer(Plugin): media_title = getTitle(group) # Add _UNKNOWN_ if no library item is connected - if not group.get('info') or not media_title: + if not group.get('media') or not media_title: self.tagRelease(group = group, tag = 'unknown') continue # Rename the files using the library data @@ -238,7 +238,7 @@ class Renamer(Plugin): group['media'] = fireEvent('movie.add', params = { 'identifier': group['identifier'], 'profile_id': None - }, search_after = False, status = 'done') + }, search_after = False, status = 'done', single = True) else: group['media'] = fireEvent('movie.update_info', identifier = group['media']['identifier'], single = True) From 7f33a3847c654df1bb47b6939be0dc3c8332e923 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 22:16:19 +0100 Subject: [PATCH 053/468] Try existing images first --- couchpotato/core/providers/metadata/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 39ee4945..31e610cb 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -26,7 +26,7 @@ class MetaDataBase(Plugin): # Update library to get latest info try: - group['media'] = fireEvent('movie.update_info', group['media']['identifier'], extended = True, single = True) + group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = group['media']['identifier'], extended = True, single = True) except: log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) @@ -91,14 +91,14 @@ class MetaDataBase(Plugin): if not movie_info: movie_info = {} # See if it is in current files - files = data.get('files', {}) - if files.get('image_'+wanted_file_type): - if os.path.isfile(files['image_'+wanted_file_type][0]): - return files['image_'+wanted_file_type][0] + files = data['media'].get('files') + if files.get('image_' + wanted_file_type): + if os.path.isfile(files['image_' + wanted_file_type][0]): + return files['image_' + wanted_file_type][0] # Download using existing info try: - images = data['info']['images'][wanted_file_type] + images = movie_info['images'][wanted_file_type] file_path = fireEvent('file.download', url = images[0], single = True) return file_path except: From 5cc4260d8e4265e4e2c2ac2fd733eac0cc283bdb Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 22:30:17 +0100 Subject: [PATCH 054/468] Remove unused method --- couchpotato/core/media/movie/_base/main.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index bc16388b..8fc6c37b 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -359,22 +359,3 @@ class MovieBase(MovieTypeBase): log.error('Failed updating release dates: %s', traceback.format_exc()) return {} - - def simplifyTitle(self, title): - """ - Removes all special chars from a title so it's easier to make sortable - @param title: media title - @return: string, simplified title - """ - - title = toUnicode(title) - - nr_prefix = '' if title[0] in ascii_letters else '#' - title = simplifyString(title) - - for prefix in ['the ']: - if prefix == title[:len(prefix)]: - title = title[len(prefix):] - break - - return nr_prefix + title From 3f64173905a9c4f91a071caf358910a5423a13b6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 22:44:28 +0100 Subject: [PATCH 055/468] Don't trigger toasts on init --- .../core/notifications/core/static/notification.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 717fbf11..2cd4fff1 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -103,7 +103,9 @@ var NotificationBase = new Class({ self.request = Api.request('notification.listener', { 'data': {'init':true}, - 'onSuccess': self.processData.bind(self) + 'onSuccess': function(json){ + self.processData(json, true) + } }).send() setInterval(function(){ @@ -124,7 +126,9 @@ var NotificationBase = new Class({ return; self.request = Api.request('nonblock/notification.listener', { - 'onSuccess': self.processData.bind(self), + 'onSuccess': function(json){ + self.processData(json, false) + }, 'data': { 'last_id': self.last_id }, @@ -141,14 +145,14 @@ var NotificationBase = new Class({ this.stopped = true; }, - processData: function(json){ + processData: function(json, init){ var self = this; // Process data if(json){ Array.each(json.result, function(result){ App.trigger(result._t || result.type, [result]); - if(result.message && result.read === undefined) + if(result.message && result.read === undefined && !init) self.showMessage(result.message); }) From 73dfa232f9a313128ebb522b9dfe961e14f34826 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 8 Mar 2014 23:14:16 +0100 Subject: [PATCH 056/468] Allow "or" for media and release status in movie.list --- couchpotato/core/media/_base/media/main.py | 29 +++++++++---------- .../media/movie/_base/static/movie.actions.js | 13 +++++---- couchpotato/static/scripts/page/manage.js | 4 ++- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5d0784e4..76172677 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -111,7 +111,6 @@ class MediaPlugin(MediaBase): except: log.error('Refresh handler for non existing media: %s', traceback.format_exc()) - def addSingleRefreshView(self): for media_type in fireEvent('media.types', merge = True): @@ -143,7 +142,7 @@ class MediaPlugin(MediaBase): 'media': media, } - def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None): + def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): db = get_db() @@ -189,6 +188,12 @@ class MediaPlugin(MediaBase): if search: filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)] + if status_or and 'media_status' in filter_by and 'release_status' in filter_by: + filter_by['status'] = list(filter_by['media_status']) + list(filter_by['release_status']) + del filter_by['media_status'] + del filter_by['release_status'] + + # Filter by combining ids for x in filter_by: media_ids = [n for n in media_ids if n in filter_by[x]] @@ -228,20 +233,14 @@ class MediaPlugin(MediaBase): def listView(self, **kwargs): - types = splitString(kwargs.get('type')) - status = splitString(kwargs.get('status')) - release_status = splitString(kwargs.get('release_status')) - limit_offset = kwargs.get('limit_offset') - starts_with = kwargs.get('starts_with') - search = kwargs.get('search') - total_movies, movies = self.list( - types = types, - status = status, - release_status = release_status, - limit_offset = limit_offset, - starts_with = starts_with, - search = search + types = splitString(kwargs.get('type')), + status = splitString(kwargs.get('status')), + release_status = splitString(kwargs.get('release_status')), + status_or = kwargs.get('status_or') is not None, + limit_offset = kwargs.get('limit_offset'), + starts_with = kwargs.get('starts_with'), + search = kwargs.get('search') ) return { diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 4152d824..fd0b8837 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -825,12 +825,13 @@ MA.Files = new Class({ create: function(){ var self = this; - self.el = new Element('a.directory', { - 'title': 'Available files', - 'events': { - 'click': self.show.bind(self) - } - }); + if(self.movie.data.releases && self.movie.data.releases.length > 0) + self.el = new Element('a.directory', { + 'title': 'Available files', + 'events': { + 'click': self.show.bind(self) + } + }); }, diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index ed9120fa..50e6dfa8 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -28,7 +28,9 @@ Page.Manage = new Class({ self.list = new MovieList({ 'identifier': 'manage', 'filter': { - 'release_status': 'done' + 'status': 'done', + 'release_status': 'done', + 'status_or': 1 }, 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Readd, MA.Edit, MA.Delete], 'menu': [self.refresh_button, self.refresh_quick], From 131326675e45500532948825ea39d9664e66d86d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 00:24:54 +0100 Subject: [PATCH 057/468] Update release on rename Prevent RevConflict --- couchpotato/core/plugins/manage/main.py | 7 +++---- couchpotato/core/plugins/release/main.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index d29321ce..e047d88f 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -106,7 +106,6 @@ class Manage(Plugin): log.info('Updating manage library: %s', folder) fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder) - onFound = self.createAddToLibrary(folder, added_identifiers) fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True) @@ -122,7 +121,7 @@ class Manage(Plugin): for done_movie in done_movies: if done_movie['identifier'] not in added_identifiers: - fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') + fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all') else: db = get_db() @@ -191,7 +190,7 @@ class Manage(Plugin): added_identifiers.append(group['identifier']) # Add it to release and update the info - fireEvent('release.add', group = group) + fireEvent('release.add', group = group, update_info = False) fireEvent('movie.update_info', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier'])) else: self.updateProgress(folder) @@ -239,7 +238,7 @@ class Manage(Plugin): if groups: for group in groups.values(): - if group.get('info'): + if group.get('media'): fireEvent('release.add', group = group) def getDiskSpace(self): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 5cc3c01f..fb8e7945 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -81,7 +81,7 @@ class Release(Plugin): elif rel['status'] in ['snatched', 'downloaded']: self.updateStatus(rel['_id'], status = 'ignore') - def add(self, group): + def add(self, group, update_info = True): try: db = get_db() @@ -95,7 +95,7 @@ class Release(Plugin): media = fireEvent('movie.add', params = { 'identifier': group['identifier'], 'profile_id': None, - }, search_after = False, status = 'done', single = True) + }, search_after = False, update_after = update_info, notify_after = False, status = 'done', single = True) # Add Release release = { From 519b832d8c0b7ca0b01f9aa66726687c509b0b6a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 11:52:15 +0100 Subject: [PATCH 058/468] Time migration --- couchpotato/core/database.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 31534286..5760b26e 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -117,11 +117,14 @@ class Database(object): old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db') if not os.path.isfile(old_db): return + log.info('=' * 30) log.info('Migrating database, hold on..') time.sleep(1) if os.path.isfile(old_db): + migrate_start = time.time() + import sqlite3 conn = sqlite3.connect(old_db) @@ -165,6 +168,8 @@ class Database(object): c.close() + log.info('Getting data took %s', time.time() - migrate_start) + db = self.getDB() # Use properties @@ -194,7 +199,7 @@ class Database(object): category_link[x] = new_c.get('_id') # Profiles - log.debug('Importing profiles') + log.info('Importing profiles') new_profiles = db.all('profile', with_doc = True) new_profiles_by_label = {} for x in new_profiles: @@ -244,7 +249,7 @@ class Database(object): profile_link[x] = new_profile.get('_id') # Qualities - log.debug('Importing quality sizes') + log.info('Importing quality sizes') new_qualities = db.all('quality', with_doc = True) new_qualities_by_identifier = {} for x in new_qualities: @@ -291,7 +296,7 @@ class Database(object): releases_by_media[release.get('movie_id')].append(release) # Media - log.debug('Importing %s media items', len(migrate_data['movie'])) + log.info('Importing %s media items', len(migrate_data['movie'])) statuses = migrate_data['status'] libraries = migrate_data['library'] library_files = migrate_data['library_files__file_library'] @@ -305,7 +310,7 @@ class Database(object): l = libraries[m['library_id']] # Only migrate wanted movies, Skip if no identifier present - if status != 'active' or not getImdb(l.get('identifier')): continue + if not getImdb(l.get('identifier')): continue profile_id = profile_link.get(m['profile_id']) category_id = category_link.get(m['category_id']) @@ -354,3 +359,6 @@ class Database(object): os.rename(old_db + '-wal', old_db + '-wal.old') if os.path.isfile(old_db + '-shm'): os.rename(old_db + '-shm', old_db + '-shm.old') + + log.info('Total migration took %s', time.time() - migrate_start) + log.info('=' * 30) From dd6baa72fae71c8d4609b29328d961545a2a4a46 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 14:29:54 +0100 Subject: [PATCH 059/468] Faster search title index --- couchpotato/core/media/_base/media/index.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 7cfe8079..956cac56 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -83,12 +83,17 @@ from couchpotato.core.helpers.encoding import simplifyString""" out = set() title = str(simplifyString(data.get('title').lower())) l = self.__l - max_l = len(title) - for x in xrange(l - 1, max_l): - m = (title, ) - for y in xrange(0, x): - m += (title[y + 1:],) - out.update(set(''.join(x).rjust(32, '_').lower() for x in izip(*m))) + title_split = title.split() + + for x in range(len(title_split)): + combo = ' '.join(title_split[x:])[:32].strip() + out.add(combo.rjust(32, '_')) + combo_range = max(l, min(len(combo), 32)) + + for cx in range(1, combo_range): + ccombo = combo[:-cx].strip() + if len(ccombo) > l: + out.add(ccombo.rjust(32, '_')) return out, None From bef76f0118b47c98546296ff57bac7e87af313ae Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 14:30:23 +0100 Subject: [PATCH 060/468] Add download info to release when available --- couchpotato/core/plugins/release/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index fb8e7945..2fcd6d62 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -354,6 +354,13 @@ class Release(Plugin): 'info': {} } + # Add downloader info if provided + try: + release['download_info'] = rel['download_info'] + del rel['download_info'] + except: + pass + try: rls = db.get('release_identifier', rel_identifier, with_doc = True)['doc'] except: From da1b4302008fef89079e3a96c7374f5669b3d656 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 14:30:43 +0100 Subject: [PATCH 061/468] Only update media when needed --- couchpotato/core/media/movie/_base/main.py | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 8fc6c37b..85d08973 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -84,7 +84,9 @@ class MovieBase(MovieTypeBase): def_title = toUnicode(titles[0]) # Default profile and category - default_profile = fireEvent('profile.default', single = True) + default_profile = {} + if not params.get('profile_id'): + default_profile = fireEvent('profile.default', single = True) cat_id = params.get('category_id') try: @@ -100,6 +102,13 @@ class MovieBase(MovieTypeBase): 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 else None, } + # Update movie info + try: del info['in_wanted'] + except: pass + try: del info['in_library'] + except: pass + media['info'] = info + new = False try: m = db.get('media', params.get('identifier'), with_doc = True)['doc'] @@ -110,13 +119,6 @@ class MovieBase(MovieTypeBase): # Update dict to be usable m.update(media) - # Update movie info - try: del info['in_wanted'] - except: pass - try: del info['in_library'] - except: pass - m['info'] = info - added = True do_search = False search_after = search_after and self.conf('search_on_add', section = 'moviesearcher') @@ -139,22 +141,20 @@ class MovieBase(MovieTypeBase): m['profile_id'] = params.get('profile_id', default_profile.get('id')) m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) + m['last_edit'] = int(time.time()) + + do_search = True + db.update(m) else: try: del params['info'] except: pass log.debug('Movie already exists, not updating: %s', params) added = False - if force_readd: - m['last_edit'] = int(time.time()) - do_search = True - - if added: - db.update(m) - + # Trigger update info + if added and update_after: # Do full update to get images etc - if update_after: - fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) + fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) # Remove releases for rel in db.run('release', 'for_media', m['_id']): From 488b631c38695ec4277e919216f50d3e5055c07f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 14:32:41 +0100 Subject: [PATCH 062/468] Migrate release files --- couchpotato/core/database.py | 80 ++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 5760b26e..3317c5a6 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -139,6 +139,7 @@ class Database(object): 'library_files__file_library': ['library_id', 'file_id'], 'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'], 'releaseinfo': ['id', 'identifier', 'value', 'release_id'], + 'release_files__file_release': ['release_id', 'file_id'], 'status': ['id', 'identifier'], 'properties': ['id', 'identifier', 'value'], 'file': ['id', 'path', 'type_id'], @@ -295,11 +296,19 @@ class Database(object): releases_by_media[release.get('movie_id')].append(release) + # Type ids + types = migrate_data['filetype'] + type_by_id = {} + for t in types: + type = types[t] + type_by_id[type.get('id')] = type + # Media log.info('Importing %s media items', len(migrate_data['movie'])) statuses = migrate_data['status'] libraries = migrate_data['library'] library_files = migrate_data['library_files__file_library'] + releases_files = migrate_data['release_files__file_release'] all_files = migrate_data['file'] poster_type = migrate_data['filetype']['poster'] medias = migrate_data['movie'] @@ -340,16 +349,76 @@ class Database(object): added_media['files']['image_poster'] = [ffile.get('path')] break - db.update(added_media) + if 'image_poster' in added_media['files']: + db.update(added_media) for rel in releases: - if not rel.get('info'): continue + + empty_info = False + if not rel.get('info'): + empty_info = True + rel['info'] = {} quality = quality_link[rel.get('quality_id')] + release_status = statuses.get(rel.get('status_id')).get('identifier') + + if rel['info'].get('download_id'): + status_support = rel['info'].get('download_status_support', False) in [True, 'true', 'True'] + rel['info']['download_info'] = { + 'id': rel['info'].get('download_id'), + 'downloader': rel['info'].get('download_downloader'), + 'status_support': status_support, + } # Add status to keys - rel['info']['status'] = statuses.get(rel.get('status_id')).get('identifier') - fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) + rel['info']['status'] = release_status + if not empty_info: + fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True) + else: + release = { + '_t': 'release', + 'identifier': rel.get('identifier'), + 'media_id': added_media.get('_id'), + 'quality': quality.get('identifier'), + 'status': release_status, + 'last_edit': int(time.time()), + 'files': {} + } + + # Add downloader info if provided + try: + release['download_info'] = rel['info']['download_info'] + del rel['download_info'] + except: + pass + + # Add files + release_files = releases_files.get(rel.get('id'), []) + if not isinstance(release_files, list): + release_files = [release_files] + + if len(release_files) == 0: + continue + + for f in release_files: + rfile = all_files[f.get('file_id')] + file_type = type_by_id.get(rfile.get('type_id')).get('identifier') + + if not release['files'].get(file_type): + release['files'][file_type] = [] + + release['files'][file_type].append(rfile.get('path')) + + try: + rls = db.get('release_identifier', rel.get('identifier'), with_doc = True)['doc'] + rls.update(release) + db.update(rls) + except: + db.insert(release) + + log.info('Total migration took %s', time.time() - migrate_start) + log.info('=' * 30) + # rename old database log.info('Renaming old database to %s ', old_db + '.old') @@ -359,6 +428,3 @@ class Database(object): os.rename(old_db + '-wal', old_db + '-wal.old') if os.path.isfile(old_db + '-shm'): os.rename(old_db + '-shm', old_db + '-shm.old') - - log.info('Total migration took %s', time.time() - migrate_start) - log.info('=' * 30) From 85bc3ddde69d9a9df0442128ac476ed1b3319869 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 15:29:53 +0100 Subject: [PATCH 063/468] Don't try to reuse generator --- couchpotato/core/media/_base/media/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 76172677..76622230 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -396,6 +396,7 @@ class MediaPlugin(MediaBase): db = get_db() m = db.get('id', media_id) + previous_status = m['status'] log.debug('Changing status for %s', getTitle(m)) if not m['profile_id']: @@ -404,18 +405,20 @@ class MediaPlugin(MediaBase): move_to_wanted = True profile = db.get('id', m['profile_id']) - media_releases = db.run('release', 'for_media', m['_id']) + media_releases = list(db.run('release', 'for_media', m['_id'])) for q_identifier in profile['qualities']: index = profile['qualities'].index(q_identifier) for release in media_releases: - if q_identifier is release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): + if q_identifier == release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): move_to_wanted = False m['status'] = 'active' if move_to_wanted else 'done' - db.update(m) + # Only update when status has changed + if previous_status != m['status']: + db.update(m) return True except: From 61edcfe4f339e5c043fddbfd5ca4ae3db40ab688 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 15:30:21 +0100 Subject: [PATCH 064/468] Remove debug comment --- couchpotato/core/plugins/manage/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index e047d88f..965bd9f3 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -68,7 +68,7 @@ class Manage(Plugin): return self.updateLibrary(full = False) def updateLibrary(self, full = True): - last_update = 0 #float(Env.prop('manage.last_update', default = 0)) + last_update = float(Env.prop('manage.last_update', default = 0)) if self.in_progress: log.info('Already updating library: %s', self.in_progress) From 58f198ddad6087d5095b9f431fbb5d0ec9874fe0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 23:39:46 +0100 Subject: [PATCH 065/468] Zero fill identifier when adding movie --- couchpotato/core/database.py | 8 ++++++-- couchpotato/core/media/movie/_base/main.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 3317c5a6..59221813 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -337,9 +337,13 @@ class Database(object): 'profile_id': profile_id, 'category_id': category_id, 'title': title - }, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True) - added_media['files'] = added_media.get('files', {}) + }, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True) + if not added_media: + log.error('Failed adding media %s: %s', (l.get('identifier'), info)) + continue + + added_media['files'] = added_media.get('files', {}) for f in files: ffile = all_files[f.get('file_id')] diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 85d08973..a59337ab 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -4,7 +4,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import splitString, getTitle +from couchpotato.core.helpers.variable import splitString, getTitle, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase import time @@ -49,6 +49,9 @@ class MovieBase(MovieTypeBase): def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): if not params: params = {} + # Make sure it's a correct zero filled imdb id + params['identifier'] = getImdb(params.get('identifier', '')) + if not params.get('identifier'): msg = 'Can\'t add movie without imdb identifier.' log.error(msg) From e3f6df712077d471e83c55df2e7b5c6d1a0a3b3d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 9 Mar 2014 23:47:33 +0100 Subject: [PATCH 066/468] Convert sp to unicode --- couchpotato/core/helpers/encoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py index 0b85f64b..f896a62e 100644 --- a/couchpotato/core/helpers/encoding.py +++ b/couchpotato/core/helpers/encoding.py @@ -76,7 +76,7 @@ def sp(path, *args): # Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit) path = re.sub('^//', '/', path) - return path + return toUnicode(path) def ek(original, *args): From 33e2f63ed53ad0ea72ca76accb6af1631ce77562 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 00:00:33 +0100 Subject: [PATCH 067/468] Don't try to use release info if it doesn't exist --- .../core/media/movie/_base/static/movie.actions.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index fd0b8837..cb4be00c 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -170,8 +170,8 @@ MA.Release = new Class({ self.movie.data.releases.each(function(release){ var quality = Quality.getQuality(release.quality) || {}, - info = release.info, - provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : ''); + info = release.info || {}, + provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : ''); var release_name = self.get(release, 'name'); if(release.files && release.files.length > 0){ @@ -193,12 +193,12 @@ MA.Release = new Class({ new Element('span.name', {'text': release_name, 'title': release_name}), new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), new Element('span.quality', {'text': quality.label || 'n/a'}), - new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), + new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.provider', { 'text': provider, 'title': provider }), - release.info['detail_url'] ? new Element('a.info.icon2', { - 'href': release.info['detail_url'], + info['detail_url'] ? new Element('a.info.icon2', { + 'href': info['detail_url'], 'target': '_blank' }) : new Element('a'), new Element('a.download.icon2', { @@ -358,7 +358,7 @@ MA.Release = new Class({ }, get: function(release, type){ - return release.info[type] !== undefined ? release.info[type] : 'n/a' + return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a' }, download: function(release){ From bca597c4e273570425af4cdcfc451a03fcb6b0dd Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 00:23:41 +0100 Subject: [PATCH 068/468] Speed up log view in Chrome --- couchpotato/core/plugins/log/static/log.css | 22 ++++++++++----------- couchpotato/core/plugins/log/static/log.js | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 524b588d..df9ab1c3 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -9,6 +9,7 @@ bottom: 0; left: 0; background: #4E5969; + z-index: 200; } .page.log .nav li { @@ -27,12 +28,12 @@ cursor: default; background: rgba(255,255,255,.1); } - + @media all and (max-width: 480px) { .page.log .nav { font-size: 14px; } - + .page.log .nav li { padding: 5px; } @@ -47,14 +48,7 @@ .page.log .container { padding: 30px 0 60px; overflow: hidden; -} - -.page.log .container span { - float: left; - width: 86%; line-height: 150%; - padding: 3px 0; - border-top: 1px solid rgba(255, 255, 255, 0.2); font-size: 11px; font-family: Lucida Console, Monaco, Nimbus Mono L; } @@ -67,11 +61,17 @@ .page.log .container .time { clear: both; - width: 14%; color: lightgrey; padding: 3px 0; font-size: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.2); + position: relative; } - .page.log .container .time:last-child { display: none; } + .page.log .container .time:last-child { display: none; } + + .page.log .container .time span { + float: right; + width: 86%; + } diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 159bfeaa..e450b078 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -73,10 +73,10 @@ Page.Log = new Class({ .replace(/\u001b\[31m/gi, '') .replace(/\u001b\[36m/gi, '') .replace(/\u001b\[33m/gi, '') - .replace(/\u001b\[0m\n/gi, '
') + .replace(/\u001b\[0m\n/gi, '
') .replace(/\u001b\[0m/gi, '') - return '
' + text + '
'; + return '
' + text + '
'; } }) From bf9a43b3d14be89366a414c2299956ed6c67e683 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 14:10:29 +0100 Subject: [PATCH 069/468] Make sure name is string type, not bs4 class --- couchpotato/core/providers/torrent/iptorrents/main.py | 2 +- couchpotato/core/providers/torrent/publichd/main.py | 2 +- couchpotato/core/providers/torrent/thepiratebay/main.py | 2 +- couchpotato/core/providers/torrent/torrentleech/main.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/providers/torrent/iptorrents/main.py b/couchpotato/core/providers/torrent/iptorrents/main.py index 5c22ae04..7ff3cb91 100644 --- a/couchpotato/core/providers/torrent/iptorrents/main.py +++ b/couchpotato/core/providers/torrent/iptorrents/main.py @@ -72,7 +72,7 @@ class IPTorrents(TorrentProvider): torrent = cells[1].find('a') torrent_id = torrent['href'].replace('/details.php?id=', '') - torrent_name = torrent.string + torrent_name = unicode(torrent.string) torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.') torrent_details_url = self.urls['base_url'] + torrent['href'] torrent_size = self.parseSize(result.find_all('td')[5].string) diff --git a/couchpotato/core/providers/torrent/publichd/main.py b/couchpotato/core/providers/torrent/publichd/main.py index b200b1fd..8749b582 100644 --- a/couchpotato/core/providers/torrent/publichd/main.py +++ b/couchpotato/core/providers/torrent/publichd/main.py @@ -54,7 +54,7 @@ class PublicHD(TorrentMagnetProvider): results.append({ 'id': url['id'][0], - 'name': info_url.string, + 'name': unicode(info_url.string), 'url': download['href'], 'detail_url': self.urls['detail'] % url['id'][0], 'size': self.parseSize(result.find_all('td')[7].string), diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py index 61f12ad1..3bf53f75 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py @@ -88,7 +88,7 @@ class ThePirateBay(TorrentMagnetProvider): results.append({ 'id': re.search('/(?P\d+)/', link['href']).group('id'), - 'name': link.string, + 'name': unicode(link.string), 'url': download['href'], 'detail_url': self.getDomain(link['href']), 'size': self.parseSize(size), diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py index 80c426f8..cb7cc043 100644 --- a/couchpotato/core/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/providers/torrent/torrentleech/main.py @@ -56,7 +56,7 @@ class TorrentLeech(TorrentProvider): results.append({ 'id': link['href'].replace('/torrent/', ''), - 'name': link.string, + 'name': unicode(link.string), 'url': self.urls['download'] % url['href'], 'detail_url': self.urls['download'] % details['href'], 'size': self.parseSize(result.find_all('td')[4].string), From 8916ea52994899cd21803c750ae6cd468bbc634e Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 14:12:19 +0100 Subject: [PATCH 070/468] Don't unicode ints and floats --- couchpotato/core/plugins/release/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 2fcd6d62..d8e99a73 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -373,7 +373,7 @@ class Release(Plugin): if not isinstance(rel[info], (str, unicode, int, long, float)): continue - rls['info'][info] = toUnicode(rel[info]) + rls['info'][info] = toUnicode(rel[info]) if isinstance(rel[info], (str, unicode)) else rel[info] except: log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) From 720af9085afeb559dbebda699f735e4d68d2d4d8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 14:30:16 +0100 Subject: [PATCH 071/468] Get new info when titles are missing --- couchpotato/core/media/movie/_base/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index a59337ab..62f2b2ad 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -69,7 +69,7 @@ class MovieBase(MovieTypeBase): pass info = params.get('info') - if not info: + if not info or (info and len(info.get('titles', [])) == 0): info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) # Set default title From 28661ab11af5186e0adc11443f1ecb31b25eaba0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 10 Mar 2014 20:24:04 +0100 Subject: [PATCH 072/468] Move providers under media --- .../{ => media/_base}/providers/__init__.py | 0 .../_base}/providers/automation/__init__.py | 0 .../_base}/providers/automation/base.py | 0 .../providers/automation/bluray/__init__.py | 0 .../providers/automation/bluray/main.py | 0 .../providers/automation/flixster/__init__.py | 0 .../providers/automation/flixster/main.py | 0 .../automation/goodfilms/__init__.py | 0 .../providers/automation/goodfilms/main.py | 0 .../providers/automation/imdb/__init__.py | 0 .../_base}/providers/automation/imdb/main.py | 0 .../providers/automation/itunes/__init__.py | 0 .../providers/automation/itunes/main.py | 0 .../automation/kinepolis/__init__.py | 0 .../providers/automation/kinepolis/main.py | 0 .../automation/letterboxd/__init__.py | 0 .../providers/automation/letterboxd/main.py | 0 .../automation/moviemeter/__init__.py | 0 .../providers/automation/moviemeter/main.py | 0 .../automation/movies_io/__init__.py | 0 .../providers/automation/movies_io/main.py | 0 .../automation/rottentomatoes/__init__.py | 0 .../automation/rottentomatoes/main.py | 0 .../providers/automation/trakt/__init__.py | 0 .../_base}/providers/automation/trakt/main.py | 0 .../core/{ => media/_base}/providers/base.py | 0 .../_base}/providers/info/__init__.py | 0 .../providers/info/_modifier/__init__.py | 0 .../_base}/providers/info/_modifier/main.py | 0 .../{ => media/_base}/providers/info/base.py | 0 .../providers/info/couchpotatoapi/__init__.py | 0 .../providers/info/couchpotatoapi/main.py | 0 .../_base}/providers/info/omdbapi/__init__.py | 0 .../_base}/providers/info/omdbapi/main.py | 0 .../providers/info/themoviedb/__init__.py | 0 .../_base}/providers/info/themoviedb/main.py | 0 .../_base}/providers/metadata/__init__.py | 0 .../_base}/providers/metadata/base.py | 0 .../_base}/providers/metadata/wmc/__init__.py | 0 .../_base}/providers/metadata/wmc/main.py | 0 .../providers/metadata/xbmc/__init__.py | 0 .../_base}/providers/metadata/xbmc/main.py | 0 .../_base}/providers/nzb/__init__.py | 0 .../{ => media/_base}/providers/nzb/base.py | 0 .../providers/nzb/binsearch/__init__.py | 0 .../_base}/providers/nzb/binsearch/main.py | 0 .../_base}/providers/nzb/newznab/__init__.py | 0 .../_base}/providers/nzb/newznab/main.py | 0 .../_base}/providers/nzb/nzbclub/__init__.py | 0 .../_base}/providers/nzb/nzbclub/main.py | 0 .../_base}/providers/nzb/nzbindex/__init__.py | 0 .../_base}/providers/nzb/nzbindex/main.py | 0 .../providers/nzb/omgwtfnzbs/__init__.py | 0 .../_base}/providers/nzb/omgwtfnzbs/main.py | 0 .../_base}/providers/torrent/__init__.py | 0 .../providers/torrent/awesomehd/__init__.py | 0 .../providers/torrent/awesomehd/main.py | 0 .../_base}/providers/torrent/base.py | 0 .../providers/torrent/bithdtv/__init__.py | 0 .../_base}/providers/torrent/bithdtv/main.py | 0 .../providers/torrent/bitsoup/__init__.py | 0 .../_base}/providers/torrent/bitsoup/main.py | 0 .../providers/torrent/hdbits/__init__.py | 108 +++++++++--------- .../_base}/providers/torrent/hdbits/main.py | 0 .../torrent/ilovetorrents/__init__.py | 0 .../providers/torrent/ilovetorrents/main.py | 0 .../providers/torrent/iptorrents/__init__.py | 0 .../providers/torrent/iptorrents/main.py | 0 .../torrent/kickasstorrents/__init__.py | 0 .../providers/torrent/kickasstorrents/main.py | 0 .../torrent/passthepopcorn/__init__.py | 0 .../providers/torrent/passthepopcorn/main.py | 0 .../providers/torrent/publichd/__init__.py | 0 .../_base}/providers/torrent/publichd/main.py | 0 .../providers/torrent/sceneaccess/__init__.py | 0 .../providers/torrent/sceneaccess/main.py | 0 .../torrent/thepiratebay/__init__.py | 0 .../providers/torrent/thepiratebay/main.py | 0 .../torrent/torrentbytes/__init__.py | 0 .../providers/torrent/torrentbytes/main.py | 0 .../providers/torrent/torrentday/__init__.py | 0 .../providers/torrent/torrentday/main.py | 0 .../torrent/torrentleech/__init__.py | 0 .../providers/torrent/torrentleech/main.py | 0 .../torrent/torrentpotato/__init__.py | 0 .../providers/torrent/torrentpotato/main.py | 0 .../torrent/torrentshack/__init__.py | 0 .../providers/torrent/torrentshack/main.py | 0 .../_base}/providers/torrent/yify/__init__.py | 0 .../_base}/providers/torrent/yify/main.py | 0 .../_base}/providers/trailer/__init__.py | 0 .../_base}/providers/trailer/base.py | 0 .../providers/trailer/hdtrailers/__init__.py | 0 .../providers/trailer/hdtrailers/main.py | 0 .../_base}/providers/userscript/__init__.py | 0 .../providers/userscript/allocine/__init__.py | 0 .../providers/userscript/allocine/main.py | 0 .../userscript/appletrailers/__init__.py | 0 .../userscript/appletrailers/main.py | 0 .../_base}/providers/userscript/base.py | 0 .../userscript/criticker/__init__.py | 0 .../providers/userscript/criticker/main.py | 0 .../providers/userscript/filmweb/__init__.py | 0 .../providers/userscript/filmweb/main.py | 0 .../userscript/flickchart/__init__.py | 0 .../providers/userscript/flickchart/main.py | 0 .../providers/userscript/imdb/__init__.py | 0 .../_base}/providers/userscript/imdb/main.py | 0 .../userscript/letterboxd/__init__.py | 0 .../providers/userscript/letterboxd/main.py | 0 .../userscript/moviemeter/__init__.py | 0 .../providers/userscript/moviemeter/main.py | 0 .../providers/userscript/moviesio/__init__.py | 0 .../providers/userscript/moviesio/main.py | 0 .../providers/userscript/reddit/__init__.py | 0 .../providers/userscript/reddit/main.py | 0 .../userscript/rottentomatoes/__init__.py | 0 .../userscript/rottentomatoes/main.py | 0 .../providers/userscript/sharethe/__init__.py | 0 .../providers/userscript/sharethe/main.py | 0 .../providers/userscript/tmdb/__init__.py | 0 .../_base}/providers/userscript/tmdb/main.py | 0 .../providers/userscript/trakt/__init__.py | 0 .../_base}/providers/userscript/trakt/main.py | 0 .../providers/userscript/whiwa/__init__.py | 0 .../_base}/providers/userscript/whiwa/main.py | 0 .../userscript/youteather/__init__.py | 0 .../providers/userscript/youteather/main.py | 0 128 files changed, 54 insertions(+), 54 deletions(-) rename couchpotato/core/{ => media/_base}/providers/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/bluray/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/bluray/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/flixster/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/flixster/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/goodfilms/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/goodfilms/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/imdb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/imdb/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/itunes/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/itunes/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/kinepolis/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/kinepolis/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/letterboxd/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/letterboxd/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/moviemeter/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/moviemeter/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/movies_io/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/movies_io/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/rottentomatoes/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/rottentomatoes/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/trakt/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/automation/trakt/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/_modifier/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/_modifier/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/couchpotatoapi/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/couchpotatoapi/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/omdbapi/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/omdbapi/main.py (100%) mode change 100755 => 100644 rename couchpotato/core/{ => media/_base}/providers/info/themoviedb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/info/themoviedb/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/wmc/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/wmc/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/xbmc/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/metadata/xbmc/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/binsearch/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/binsearch/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/newznab/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/newznab/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/nzbclub/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/nzbclub/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/nzbindex/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/nzbindex/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/omgwtfnzbs/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/nzb/omgwtfnzbs/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/awesomehd/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/awesomehd/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/bithdtv/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/bithdtv/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/bitsoup/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/bitsoup/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/hdbits/__init__.py (96%) rename couchpotato/core/{ => media/_base}/providers/torrent/hdbits/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/ilovetorrents/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/ilovetorrents/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/iptorrents/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/iptorrents/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/kickasstorrents/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/kickasstorrents/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/passthepopcorn/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/passthepopcorn/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/publichd/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/publichd/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/sceneaccess/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/sceneaccess/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/thepiratebay/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/thepiratebay/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentbytes/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentbytes/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentday/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentday/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentleech/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentleech/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentpotato/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentpotato/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentshack/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/torrentshack/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/yify/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/torrent/yify/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/trailer/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/trailer/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/trailer/hdtrailers/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/trailer/hdtrailers/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/allocine/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/allocine/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/appletrailers/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/appletrailers/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/base.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/criticker/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/criticker/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/filmweb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/filmweb/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/flickchart/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/flickchart/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/imdb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/imdb/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/letterboxd/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/letterboxd/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/moviemeter/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/moviemeter/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/moviesio/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/moviesio/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/reddit/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/reddit/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/rottentomatoes/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/rottentomatoes/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/sharethe/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/sharethe/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/tmdb/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/tmdb/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/trakt/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/trakt/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/whiwa/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/whiwa/main.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/youteather/__init__.py (100%) rename couchpotato/core/{ => media/_base}/providers/userscript/youteather/main.py (100%) diff --git a/couchpotato/core/providers/__init__.py b/couchpotato/core/media/_base/providers/__init__.py similarity index 100% rename from couchpotato/core/providers/__init__.py rename to couchpotato/core/media/_base/providers/__init__.py diff --git a/couchpotato/core/providers/automation/__init__.py b/couchpotato/core/media/_base/providers/automation/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/__init__.py rename to couchpotato/core/media/_base/providers/automation/__init__.py diff --git a/couchpotato/core/providers/automation/base.py b/couchpotato/core/media/_base/providers/automation/base.py similarity index 100% rename from couchpotato/core/providers/automation/base.py rename to couchpotato/core/media/_base/providers/automation/base.py diff --git a/couchpotato/core/providers/automation/bluray/__init__.py b/couchpotato/core/media/_base/providers/automation/bluray/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/bluray/__init__.py rename to couchpotato/core/media/_base/providers/automation/bluray/__init__.py diff --git a/couchpotato/core/providers/automation/bluray/main.py b/couchpotato/core/media/_base/providers/automation/bluray/main.py similarity index 100% rename from couchpotato/core/providers/automation/bluray/main.py rename to couchpotato/core/media/_base/providers/automation/bluray/main.py diff --git a/couchpotato/core/providers/automation/flixster/__init__.py b/couchpotato/core/media/_base/providers/automation/flixster/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/flixster/__init__.py rename to couchpotato/core/media/_base/providers/automation/flixster/__init__.py diff --git a/couchpotato/core/providers/automation/flixster/main.py b/couchpotato/core/media/_base/providers/automation/flixster/main.py similarity index 100% rename from couchpotato/core/providers/automation/flixster/main.py rename to couchpotato/core/media/_base/providers/automation/flixster/main.py diff --git a/couchpotato/core/providers/automation/goodfilms/__init__.py b/couchpotato/core/media/_base/providers/automation/goodfilms/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/goodfilms/__init__.py rename to couchpotato/core/media/_base/providers/automation/goodfilms/__init__.py diff --git a/couchpotato/core/providers/automation/goodfilms/main.py b/couchpotato/core/media/_base/providers/automation/goodfilms/main.py similarity index 100% rename from couchpotato/core/providers/automation/goodfilms/main.py rename to couchpotato/core/media/_base/providers/automation/goodfilms/main.py diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/media/_base/providers/automation/imdb/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/imdb/__init__.py rename to couchpotato/core/media/_base/providers/automation/imdb/__init__.py diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/media/_base/providers/automation/imdb/main.py similarity index 100% rename from couchpotato/core/providers/automation/imdb/main.py rename to couchpotato/core/media/_base/providers/automation/imdb/main.py diff --git a/couchpotato/core/providers/automation/itunes/__init__.py b/couchpotato/core/media/_base/providers/automation/itunes/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/itunes/__init__.py rename to couchpotato/core/media/_base/providers/automation/itunes/__init__.py diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/media/_base/providers/automation/itunes/main.py similarity index 100% rename from couchpotato/core/providers/automation/itunes/main.py rename to couchpotato/core/media/_base/providers/automation/itunes/main.py diff --git a/couchpotato/core/providers/automation/kinepolis/__init__.py b/couchpotato/core/media/_base/providers/automation/kinepolis/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/kinepolis/__init__.py rename to couchpotato/core/media/_base/providers/automation/kinepolis/__init__.py diff --git a/couchpotato/core/providers/automation/kinepolis/main.py b/couchpotato/core/media/_base/providers/automation/kinepolis/main.py similarity index 100% rename from couchpotato/core/providers/automation/kinepolis/main.py rename to couchpotato/core/media/_base/providers/automation/kinepolis/main.py diff --git a/couchpotato/core/providers/automation/letterboxd/__init__.py b/couchpotato/core/media/_base/providers/automation/letterboxd/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/letterboxd/__init__.py rename to couchpotato/core/media/_base/providers/automation/letterboxd/__init__.py diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/media/_base/providers/automation/letterboxd/main.py similarity index 100% rename from couchpotato/core/providers/automation/letterboxd/main.py rename to couchpotato/core/media/_base/providers/automation/letterboxd/main.py diff --git a/couchpotato/core/providers/automation/moviemeter/__init__.py b/couchpotato/core/media/_base/providers/automation/moviemeter/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/moviemeter/__init__.py rename to couchpotato/core/media/_base/providers/automation/moviemeter/__init__.py diff --git a/couchpotato/core/providers/automation/moviemeter/main.py b/couchpotato/core/media/_base/providers/automation/moviemeter/main.py similarity index 100% rename from couchpotato/core/providers/automation/moviemeter/main.py rename to couchpotato/core/media/_base/providers/automation/moviemeter/main.py diff --git a/couchpotato/core/providers/automation/movies_io/__init__.py b/couchpotato/core/media/_base/providers/automation/movies_io/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/movies_io/__init__.py rename to couchpotato/core/media/_base/providers/automation/movies_io/__init__.py diff --git a/couchpotato/core/providers/automation/movies_io/main.py b/couchpotato/core/media/_base/providers/automation/movies_io/main.py similarity index 100% rename from couchpotato/core/providers/automation/movies_io/main.py rename to couchpotato/core/media/_base/providers/automation/movies_io/main.py diff --git a/couchpotato/core/providers/automation/rottentomatoes/__init__.py b/couchpotato/core/media/_base/providers/automation/rottentomatoes/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/rottentomatoes/__init__.py rename to couchpotato/core/media/_base/providers/automation/rottentomatoes/__init__.py diff --git a/couchpotato/core/providers/automation/rottentomatoes/main.py b/couchpotato/core/media/_base/providers/automation/rottentomatoes/main.py similarity index 100% rename from couchpotato/core/providers/automation/rottentomatoes/main.py rename to couchpotato/core/media/_base/providers/automation/rottentomatoes/main.py diff --git a/couchpotato/core/providers/automation/trakt/__init__.py b/couchpotato/core/media/_base/providers/automation/trakt/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/trakt/__init__.py rename to couchpotato/core/media/_base/providers/automation/trakt/__init__.py diff --git a/couchpotato/core/providers/automation/trakt/main.py b/couchpotato/core/media/_base/providers/automation/trakt/main.py similarity index 100% rename from couchpotato/core/providers/automation/trakt/main.py rename to couchpotato/core/media/_base/providers/automation/trakt/main.py diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/media/_base/providers/base.py similarity index 100% rename from couchpotato/core/providers/base.py rename to couchpotato/core/media/_base/providers/base.py diff --git a/couchpotato/core/providers/info/__init__.py b/couchpotato/core/media/_base/providers/info/__init__.py similarity index 100% rename from couchpotato/core/providers/info/__init__.py rename to couchpotato/core/media/_base/providers/info/__init__.py diff --git a/couchpotato/core/providers/info/_modifier/__init__.py b/couchpotato/core/media/_base/providers/info/_modifier/__init__.py similarity index 100% rename from couchpotato/core/providers/info/_modifier/__init__.py rename to couchpotato/core/media/_base/providers/info/_modifier/__init__.py diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/media/_base/providers/info/_modifier/main.py similarity index 100% rename from couchpotato/core/providers/info/_modifier/main.py rename to couchpotato/core/media/_base/providers/info/_modifier/main.py diff --git a/couchpotato/core/providers/info/base.py b/couchpotato/core/media/_base/providers/info/base.py similarity index 100% rename from couchpotato/core/providers/info/base.py rename to couchpotato/core/media/_base/providers/info/base.py diff --git a/couchpotato/core/providers/info/couchpotatoapi/__init__.py b/couchpotato/core/media/_base/providers/info/couchpotatoapi/__init__.py similarity index 100% rename from couchpotato/core/providers/info/couchpotatoapi/__init__.py rename to couchpotato/core/media/_base/providers/info/couchpotatoapi/__init__.py diff --git a/couchpotato/core/providers/info/couchpotatoapi/main.py b/couchpotato/core/media/_base/providers/info/couchpotatoapi/main.py similarity index 100% rename from couchpotato/core/providers/info/couchpotatoapi/main.py rename to couchpotato/core/media/_base/providers/info/couchpotatoapi/main.py diff --git a/couchpotato/core/providers/info/omdbapi/__init__.py b/couchpotato/core/media/_base/providers/info/omdbapi/__init__.py similarity index 100% rename from couchpotato/core/providers/info/omdbapi/__init__.py rename to couchpotato/core/media/_base/providers/info/omdbapi/__init__.py diff --git a/couchpotato/core/providers/info/omdbapi/main.py b/couchpotato/core/media/_base/providers/info/omdbapi/main.py old mode 100755 new mode 100644 similarity index 100% rename from couchpotato/core/providers/info/omdbapi/main.py rename to couchpotato/core/media/_base/providers/info/omdbapi/main.py diff --git a/couchpotato/core/providers/info/themoviedb/__init__.py b/couchpotato/core/media/_base/providers/info/themoviedb/__init__.py similarity index 100% rename from couchpotato/core/providers/info/themoviedb/__init__.py rename to couchpotato/core/media/_base/providers/info/themoviedb/__init__.py diff --git a/couchpotato/core/providers/info/themoviedb/main.py b/couchpotato/core/media/_base/providers/info/themoviedb/main.py similarity index 100% rename from couchpotato/core/providers/info/themoviedb/main.py rename to couchpotato/core/media/_base/providers/info/themoviedb/main.py diff --git a/couchpotato/core/providers/metadata/__init__.py b/couchpotato/core/media/_base/providers/metadata/__init__.py similarity index 100% rename from couchpotato/core/providers/metadata/__init__.py rename to couchpotato/core/media/_base/providers/metadata/__init__.py diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/media/_base/providers/metadata/base.py similarity index 100% rename from couchpotato/core/providers/metadata/base.py rename to couchpotato/core/media/_base/providers/metadata/base.py diff --git a/couchpotato/core/providers/metadata/wmc/__init__.py b/couchpotato/core/media/_base/providers/metadata/wmc/__init__.py similarity index 100% rename from couchpotato/core/providers/metadata/wmc/__init__.py rename to couchpotato/core/media/_base/providers/metadata/wmc/__init__.py diff --git a/couchpotato/core/providers/metadata/wmc/main.py b/couchpotato/core/media/_base/providers/metadata/wmc/main.py similarity index 100% rename from couchpotato/core/providers/metadata/wmc/main.py rename to couchpotato/core/media/_base/providers/metadata/wmc/main.py diff --git a/couchpotato/core/providers/metadata/xbmc/__init__.py b/couchpotato/core/media/_base/providers/metadata/xbmc/__init__.py similarity index 100% rename from couchpotato/core/providers/metadata/xbmc/__init__.py rename to couchpotato/core/media/_base/providers/metadata/xbmc/__init__.py diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/media/_base/providers/metadata/xbmc/main.py similarity index 100% rename from couchpotato/core/providers/metadata/xbmc/main.py rename to couchpotato/core/media/_base/providers/metadata/xbmc/main.py diff --git a/couchpotato/core/providers/nzb/__init__.py b/couchpotato/core/media/_base/providers/nzb/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/__init__.py rename to couchpotato/core/media/_base/providers/nzb/__init__.py diff --git a/couchpotato/core/providers/nzb/base.py b/couchpotato/core/media/_base/providers/nzb/base.py similarity index 100% rename from couchpotato/core/providers/nzb/base.py rename to couchpotato/core/media/_base/providers/nzb/base.py diff --git a/couchpotato/core/providers/nzb/binsearch/__init__.py b/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/binsearch/__init__.py rename to couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py diff --git a/couchpotato/core/providers/nzb/binsearch/main.py b/couchpotato/core/media/_base/providers/nzb/binsearch/main.py similarity index 100% rename from couchpotato/core/providers/nzb/binsearch/main.py rename to couchpotato/core/media/_base/providers/nzb/binsearch/main.py diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/newznab/__init__.py rename to couchpotato/core/media/_base/providers/nzb/newznab/__init__.py diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/media/_base/providers/nzb/newznab/main.py similarity index 100% rename from couchpotato/core/providers/nzb/newznab/main.py rename to couchpotato/core/media/_base/providers/nzb/newznab/main.py diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/nzbclub/__init__.py rename to couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py similarity index 100% rename from couchpotato/core/providers/nzb/nzbclub/main.py rename to couchpotato/core/media/_base/providers/nzb/nzbclub/main.py diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/nzbindex/__init__.py rename to couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py similarity index 100% rename from couchpotato/core/providers/nzb/nzbindex/main.py rename to couchpotato/core/media/_base/providers/nzb/nzbindex/main.py diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py similarity index 100% rename from couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py rename to couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py similarity index 100% rename from couchpotato/core/providers/nzb/omgwtfnzbs/main.py rename to couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py diff --git a/couchpotato/core/providers/torrent/__init__.py b/couchpotato/core/media/_base/providers/torrent/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/__init__.py rename to couchpotato/core/media/_base/providers/torrent/__init__.py diff --git a/couchpotato/core/providers/torrent/awesomehd/__init__.py b/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/awesomehd/__init__.py rename to couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py diff --git a/couchpotato/core/providers/torrent/awesomehd/main.py b/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py similarity index 100% rename from couchpotato/core/providers/torrent/awesomehd/main.py rename to couchpotato/core/media/_base/providers/torrent/awesomehd/main.py diff --git a/couchpotato/core/providers/torrent/base.py b/couchpotato/core/media/_base/providers/torrent/base.py similarity index 100% rename from couchpotato/core/providers/torrent/base.py rename to couchpotato/core/media/_base/providers/torrent/base.py diff --git a/couchpotato/core/providers/torrent/bithdtv/__init__.py b/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/bithdtv/__init__.py rename to couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py diff --git a/couchpotato/core/providers/torrent/bithdtv/main.py b/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py similarity index 100% rename from couchpotato/core/providers/torrent/bithdtv/main.py rename to couchpotato/core/media/_base/providers/torrent/bithdtv/main.py diff --git a/couchpotato/core/providers/torrent/bitsoup/__init__.py b/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/bitsoup/__init__.py rename to couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py diff --git a/couchpotato/core/providers/torrent/bitsoup/main.py b/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py similarity index 100% rename from couchpotato/core/providers/torrent/bitsoup/main.py rename to couchpotato/core/media/_base/providers/torrent/bitsoup/main.py diff --git a/couchpotato/core/providers/torrent/hdbits/__init__.py b/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py similarity index 96% rename from couchpotato/core/providers/torrent/hdbits/__init__.py rename to couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py index 1e9aa3ce..b54adb0b 100644 --- a/couchpotato/core/providers/torrent/hdbits/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py @@ -1,54 +1,54 @@ -from .main import HDBits - - -def start(): - return HDBits() - -config = [{ - 'name': 'hdbits', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'HDBits', - 'description': 'See HDBits', - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'passkey', - 'default': '', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - }, - ], - }, - ], -}] +from .main import HDBits + + +def start(): + return HDBits() + +config = [{ + 'name': 'hdbits', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'HDBits', + 'description': 'See HDBits', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'passkey', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/torrent/hdbits/main.py b/couchpotato/core/media/_base/providers/torrent/hdbits/main.py similarity index 100% rename from couchpotato/core/providers/torrent/hdbits/main.py rename to couchpotato/core/media/_base/providers/torrent/hdbits/main.py diff --git a/couchpotato/core/providers/torrent/ilovetorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/ilovetorrents/__init__.py rename to couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py diff --git a/couchpotato/core/providers/torrent/ilovetorrents/main.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py similarity index 100% rename from couchpotato/core/providers/torrent/ilovetorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py diff --git a/couchpotato/core/providers/torrent/iptorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/iptorrents/__init__.py rename to couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py diff --git a/couchpotato/core/providers/torrent/iptorrents/main.py b/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py similarity index 100% rename from couchpotato/core/providers/torrent/iptorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/iptorrents/main.py diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/kickasstorrents/__init__.py rename to couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py similarity index 100% rename from couchpotato/core/providers/torrent/kickasstorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py diff --git a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/passthepopcorn/__init__.py rename to couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py diff --git a/couchpotato/core/providers/torrent/passthepopcorn/main.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py similarity index 100% rename from couchpotato/core/providers/torrent/passthepopcorn/main.py rename to couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/publichd/__init__.py rename to couchpotato/core/media/_base/providers/torrent/publichd/__init__.py diff --git a/couchpotato/core/providers/torrent/publichd/main.py b/couchpotato/core/media/_base/providers/torrent/publichd/main.py similarity index 100% rename from couchpotato/core/providers/torrent/publichd/main.py rename to couchpotato/core/media/_base/providers/torrent/publichd/main.py diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/sceneaccess/__init__.py rename to couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py similarity index 100% rename from couchpotato/core/providers/torrent/sceneaccess/main.py rename to couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/thepiratebay/__init__.py rename to couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py similarity index 100% rename from couchpotato/core/providers/torrent/thepiratebay/main.py rename to couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py diff --git a/couchpotato/core/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentbytes/__init__.py rename to couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py diff --git a/couchpotato/core/providers/torrent/torrentbytes/main.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentbytes/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py diff --git a/couchpotato/core/providers/torrent/torrentday/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentday/__init__.py rename to couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py diff --git a/couchpotato/core/providers/torrent/torrentday/main.py b/couchpotato/core/media/_base/providers/torrent/torrentday/main.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentday/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentday/main.py diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentleech/__init__.py rename to couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentleech/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentleech/main.py diff --git a/couchpotato/core/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentpotato/__init__.py rename to couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentpotato/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py diff --git a/couchpotato/core/providers/torrent/torrentshack/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentshack/__init__.py rename to couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py diff --git a/couchpotato/core/providers/torrent/torrentshack/main.py b/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py similarity index 100% rename from couchpotato/core/providers/torrent/torrentshack/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentshack/main.py diff --git a/couchpotato/core/providers/torrent/yify/__init__.py b/couchpotato/core/media/_base/providers/torrent/yify/__init__.py similarity index 100% rename from couchpotato/core/providers/torrent/yify/__init__.py rename to couchpotato/core/media/_base/providers/torrent/yify/__init__.py diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/media/_base/providers/torrent/yify/main.py similarity index 100% rename from couchpotato/core/providers/torrent/yify/main.py rename to couchpotato/core/media/_base/providers/torrent/yify/main.py diff --git a/couchpotato/core/providers/trailer/__init__.py b/couchpotato/core/media/_base/providers/trailer/__init__.py similarity index 100% rename from couchpotato/core/providers/trailer/__init__.py rename to couchpotato/core/media/_base/providers/trailer/__init__.py diff --git a/couchpotato/core/providers/trailer/base.py b/couchpotato/core/media/_base/providers/trailer/base.py similarity index 100% rename from couchpotato/core/providers/trailer/base.py rename to couchpotato/core/media/_base/providers/trailer/base.py diff --git a/couchpotato/core/providers/trailer/hdtrailers/__init__.py b/couchpotato/core/media/_base/providers/trailer/hdtrailers/__init__.py similarity index 100% rename from couchpotato/core/providers/trailer/hdtrailers/__init__.py rename to couchpotato/core/media/_base/providers/trailer/hdtrailers/__init__.py diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/media/_base/providers/trailer/hdtrailers/main.py similarity index 100% rename from couchpotato/core/providers/trailer/hdtrailers/main.py rename to couchpotato/core/media/_base/providers/trailer/hdtrailers/main.py diff --git a/couchpotato/core/providers/userscript/__init__.py b/couchpotato/core/media/_base/providers/userscript/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/__init__.py rename to couchpotato/core/media/_base/providers/userscript/__init__.py diff --git a/couchpotato/core/providers/userscript/allocine/__init__.py b/couchpotato/core/media/_base/providers/userscript/allocine/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/allocine/__init__.py rename to couchpotato/core/media/_base/providers/userscript/allocine/__init__.py diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/media/_base/providers/userscript/allocine/main.py similarity index 100% rename from couchpotato/core/providers/userscript/allocine/main.py rename to couchpotato/core/media/_base/providers/userscript/allocine/main.py diff --git a/couchpotato/core/providers/userscript/appletrailers/__init__.py b/couchpotato/core/media/_base/providers/userscript/appletrailers/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/appletrailers/__init__.py rename to couchpotato/core/media/_base/providers/userscript/appletrailers/__init__.py diff --git a/couchpotato/core/providers/userscript/appletrailers/main.py b/couchpotato/core/media/_base/providers/userscript/appletrailers/main.py similarity index 100% rename from couchpotato/core/providers/userscript/appletrailers/main.py rename to couchpotato/core/media/_base/providers/userscript/appletrailers/main.py diff --git a/couchpotato/core/providers/userscript/base.py b/couchpotato/core/media/_base/providers/userscript/base.py similarity index 100% rename from couchpotato/core/providers/userscript/base.py rename to couchpotato/core/media/_base/providers/userscript/base.py diff --git a/couchpotato/core/providers/userscript/criticker/__init__.py b/couchpotato/core/media/_base/providers/userscript/criticker/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/criticker/__init__.py rename to couchpotato/core/media/_base/providers/userscript/criticker/__init__.py diff --git a/couchpotato/core/providers/userscript/criticker/main.py b/couchpotato/core/media/_base/providers/userscript/criticker/main.py similarity index 100% rename from couchpotato/core/providers/userscript/criticker/main.py rename to couchpotato/core/media/_base/providers/userscript/criticker/main.py diff --git a/couchpotato/core/providers/userscript/filmweb/__init__.py b/couchpotato/core/media/_base/providers/userscript/filmweb/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/filmweb/__init__.py rename to couchpotato/core/media/_base/providers/userscript/filmweb/__init__.py diff --git a/couchpotato/core/providers/userscript/filmweb/main.py b/couchpotato/core/media/_base/providers/userscript/filmweb/main.py similarity index 100% rename from couchpotato/core/providers/userscript/filmweb/main.py rename to couchpotato/core/media/_base/providers/userscript/filmweb/main.py diff --git a/couchpotato/core/providers/userscript/flickchart/__init__.py b/couchpotato/core/media/_base/providers/userscript/flickchart/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/flickchart/__init__.py rename to couchpotato/core/media/_base/providers/userscript/flickchart/__init__.py diff --git a/couchpotato/core/providers/userscript/flickchart/main.py b/couchpotato/core/media/_base/providers/userscript/flickchart/main.py similarity index 100% rename from couchpotato/core/providers/userscript/flickchart/main.py rename to couchpotato/core/media/_base/providers/userscript/flickchart/main.py diff --git a/couchpotato/core/providers/userscript/imdb/__init__.py b/couchpotato/core/media/_base/providers/userscript/imdb/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/imdb/__init__.py rename to couchpotato/core/media/_base/providers/userscript/imdb/__init__.py diff --git a/couchpotato/core/providers/userscript/imdb/main.py b/couchpotato/core/media/_base/providers/userscript/imdb/main.py similarity index 100% rename from couchpotato/core/providers/userscript/imdb/main.py rename to couchpotato/core/media/_base/providers/userscript/imdb/main.py diff --git a/couchpotato/core/providers/userscript/letterboxd/__init__.py b/couchpotato/core/media/_base/providers/userscript/letterboxd/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/letterboxd/__init__.py rename to couchpotato/core/media/_base/providers/userscript/letterboxd/__init__.py diff --git a/couchpotato/core/providers/userscript/letterboxd/main.py b/couchpotato/core/media/_base/providers/userscript/letterboxd/main.py similarity index 100% rename from couchpotato/core/providers/userscript/letterboxd/main.py rename to couchpotato/core/media/_base/providers/userscript/letterboxd/main.py diff --git a/couchpotato/core/providers/userscript/moviemeter/__init__.py b/couchpotato/core/media/_base/providers/userscript/moviemeter/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/moviemeter/__init__.py rename to couchpotato/core/media/_base/providers/userscript/moviemeter/__init__.py diff --git a/couchpotato/core/providers/userscript/moviemeter/main.py b/couchpotato/core/media/_base/providers/userscript/moviemeter/main.py similarity index 100% rename from couchpotato/core/providers/userscript/moviemeter/main.py rename to couchpotato/core/media/_base/providers/userscript/moviemeter/main.py diff --git a/couchpotato/core/providers/userscript/moviesio/__init__.py b/couchpotato/core/media/_base/providers/userscript/moviesio/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/moviesio/__init__.py rename to couchpotato/core/media/_base/providers/userscript/moviesio/__init__.py diff --git a/couchpotato/core/providers/userscript/moviesio/main.py b/couchpotato/core/media/_base/providers/userscript/moviesio/main.py similarity index 100% rename from couchpotato/core/providers/userscript/moviesio/main.py rename to couchpotato/core/media/_base/providers/userscript/moviesio/main.py diff --git a/couchpotato/core/providers/userscript/reddit/__init__.py b/couchpotato/core/media/_base/providers/userscript/reddit/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/reddit/__init__.py rename to couchpotato/core/media/_base/providers/userscript/reddit/__init__.py diff --git a/couchpotato/core/providers/userscript/reddit/main.py b/couchpotato/core/media/_base/providers/userscript/reddit/main.py similarity index 100% rename from couchpotato/core/providers/userscript/reddit/main.py rename to couchpotato/core/media/_base/providers/userscript/reddit/main.py diff --git a/couchpotato/core/providers/userscript/rottentomatoes/__init__.py b/couchpotato/core/media/_base/providers/userscript/rottentomatoes/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/rottentomatoes/__init__.py rename to couchpotato/core/media/_base/providers/userscript/rottentomatoes/__init__.py diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/media/_base/providers/userscript/rottentomatoes/main.py similarity index 100% rename from couchpotato/core/providers/userscript/rottentomatoes/main.py rename to couchpotato/core/media/_base/providers/userscript/rottentomatoes/main.py diff --git a/couchpotato/core/providers/userscript/sharethe/__init__.py b/couchpotato/core/media/_base/providers/userscript/sharethe/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/sharethe/__init__.py rename to couchpotato/core/media/_base/providers/userscript/sharethe/__init__.py diff --git a/couchpotato/core/providers/userscript/sharethe/main.py b/couchpotato/core/media/_base/providers/userscript/sharethe/main.py similarity index 100% rename from couchpotato/core/providers/userscript/sharethe/main.py rename to couchpotato/core/media/_base/providers/userscript/sharethe/main.py diff --git a/couchpotato/core/providers/userscript/tmdb/__init__.py b/couchpotato/core/media/_base/providers/userscript/tmdb/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/tmdb/__init__.py rename to couchpotato/core/media/_base/providers/userscript/tmdb/__init__.py diff --git a/couchpotato/core/providers/userscript/tmdb/main.py b/couchpotato/core/media/_base/providers/userscript/tmdb/main.py similarity index 100% rename from couchpotato/core/providers/userscript/tmdb/main.py rename to couchpotato/core/media/_base/providers/userscript/tmdb/main.py diff --git a/couchpotato/core/providers/userscript/trakt/__init__.py b/couchpotato/core/media/_base/providers/userscript/trakt/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/trakt/__init__.py rename to couchpotato/core/media/_base/providers/userscript/trakt/__init__.py diff --git a/couchpotato/core/providers/userscript/trakt/main.py b/couchpotato/core/media/_base/providers/userscript/trakt/main.py similarity index 100% rename from couchpotato/core/providers/userscript/trakt/main.py rename to couchpotato/core/media/_base/providers/userscript/trakt/main.py diff --git a/couchpotato/core/providers/userscript/whiwa/__init__.py b/couchpotato/core/media/_base/providers/userscript/whiwa/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/whiwa/__init__.py rename to couchpotato/core/media/_base/providers/userscript/whiwa/__init__.py diff --git a/couchpotato/core/providers/userscript/whiwa/main.py b/couchpotato/core/media/_base/providers/userscript/whiwa/main.py similarity index 100% rename from couchpotato/core/providers/userscript/whiwa/main.py rename to couchpotato/core/media/_base/providers/userscript/whiwa/main.py diff --git a/couchpotato/core/providers/userscript/youteather/__init__.py b/couchpotato/core/media/_base/providers/userscript/youteather/__init__.py similarity index 100% rename from couchpotato/core/providers/userscript/youteather/__init__.py rename to couchpotato/core/media/_base/providers/userscript/youteather/__init__.py diff --git a/couchpotato/core/providers/userscript/youteather/main.py b/couchpotato/core/media/_base/providers/userscript/youteather/main.py similarity index 100% rename from couchpotato/core/providers/userscript/youteather/main.py rename to couchpotato/core/media/_base/providers/userscript/youteather/main.py From 471229216a4f5eec4d9553251b78388339506f28 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 18:45:50 +0100 Subject: [PATCH 073/468] Provider restructure --- couchpotato/core/loader.py | 5 +- .../_base/providers/automation/__init__.py | 21 ---- .../media/_base/providers/automation/base.py | 94 +-------------- .../core/media/_base/providers/base.py | 17 ++- .../core/media/_base/providers/info/base.py | 6 +- .../media/_base/providers/metadata/base.py | 104 +---------------- .../_base/providers/metadata/wmc/main.py | 8 -- .../core/media/_base/providers/nzb/base.py | 2 +- .../_base/providers/nzb/binsearch/__init__.py | 5 - .../_base/providers/nzb/binsearch/main.py | 25 +--- .../_base/providers/nzb/newznab/__init__.py | 5 - .../media/_base/providers/nzb/newznab/main.py | 31 +++-- .../_base/providers/nzb/nzbclub/__init__.py | 5 - .../media/_base/providers/nzb/nzbclub/main.py | 26 +---- .../_base/providers/nzb/nzbindex/__init__.py | 5 - .../_base/providers/nzb/nzbindex/main.py | 57 +++++---- .../providers/nzb/omgwtfnzbs/__init__.py | 5 - .../_base/providers/nzb/omgwtfnzbs/main.py | 4 +- .../providers/torrent/awesomehd/__init__.py | 5 - .../_base/providers/torrent/awesomehd/main.py | 4 +- .../media/_base/providers/torrent/base.py | 19 ++- .../providers/torrent/bithdtv/__init__.py | 5 - .../_base/providers/torrent/bithdtv/main.py | 17 +-- .../providers/torrent/bitsoup/__init__.py | 5 - .../_base/providers/torrent/bitsoup/main.py | 6 +- .../providers/torrent/hdbits/__init__.py | 5 - .../_base/providers/torrent/hdbits/main.py | 4 +- .../torrent/ilovetorrents/__init__.py | 5 - .../providers/torrent/ilovetorrents/main.py | 4 +- .../providers/torrent/iptorrents/__init__.py | 5 - .../providers/torrent/iptorrents/main.py | 68 +++++------ .../torrent/kickasstorrents/__init__.py | 5 - .../providers/torrent/kickasstorrents/main.py | 8 +- .../torrent/passthepopcorn/__init__.py | 5 - .../providers/torrent/passthepopcorn/main.py | 50 ++------ .../providers/torrent/publichd/__init__.py | 5 - .../_base/providers/torrent/publichd/main.py | 15 ++- .../providers/torrent/sceneaccess/__init__.py | 5 - .../providers/torrent/sceneaccess/main.py | 49 +++----- .../torrent/thepiratebay/__init__.py | 5 - .../providers/torrent/thepiratebay/main.py | 32 +++-- .../torrent/torrentbytes/__init__.py | 5 - .../providers/torrent/torrentbytes/main.py | 4 +- .../providers/torrent/torrentday/__init__.py | 5 - .../providers/torrent/torrentday/main.py | 19 +-- .../torrent/torrentleech/__init__.py | 5 - .../providers/torrent/torrentleech/main.py | 24 ++-- .../torrent/torrentpotato/__init__.py | 5 - .../providers/torrent/torrentpotato/main.py | 26 ++--- .../torrent/torrentshack/__init__.py | 5 - .../providers/torrent/torrentshack/main.py | 22 ++-- .../_base/providers/torrent/yify/__init__.py | 5 - .../_base/providers/torrent/yify/main.py | 4 +- .../providers/userscript/moviesio/main.py | 6 - .../trailer => movie/providers}/__init__.py | 0 .../movie/providers/automation/__init__.py | 21 ++++ .../media/movie/providers/automation/base.py | 96 +++++++++++++++ .../providers/automation/bluray/__init__.py | 0 .../providers/automation/bluray/main.py | 2 +- .../providers/automation/flixster/__init__.py | 0 .../providers/automation/flixster/main.py | 2 +- .../automation/goodfilms/__init__.py | 0 .../providers/automation/goodfilms/main.py | 2 +- .../providers/automation/imdb/__init__.py | 0 .../providers/automation/imdb/main.py | 5 +- .../providers/automation/itunes/__init__.py | 0 .../providers/automation/itunes/main.py | 2 +- .../automation/kinepolis/__init__.py | 0 .../providers/automation/kinepolis/main.py | 2 +- .../automation/letterboxd/__init__.py | 0 .../providers/automation/letterboxd/main.py | 2 +- .../automation/moviemeter/__init__.py | 0 .../providers/automation/moviemeter/main.py | 2 +- .../automation/movies_io/__init__.py | 0 .../providers/automation/movies_io/main.py | 2 +- .../automation/rottentomatoes/__init__.py | 0 .../automation/rottentomatoes/main.py | 2 +- .../providers/automation/trakt/__init__.py | 0 .../providers/automation/trakt/main.py | 2 +- .../core/media/movie/providers/base.py | 5 + .../media/movie/providers/info/__init__.py | 0 .../providers/info/_modifier/__init__.py | 2 +- .../providers/info/_modifier/main.py | 0 .../providers/info/couchpotatoapi/__init__.py | 0 .../providers/info/couchpotatoapi/main.py | 8 +- .../providers/info/omdbapi/__init__.py | 0 .../providers/info/omdbapi/main.py | 10 +- .../providers/info/themoviedb/__init__.py | 0 .../providers/info/themoviedb/main.py | 6 +- .../movie/providers/metadata/__init__.py | 0 .../media/movie/providers/metadata/base.py | 109 ++++++++++++++++++ .../providers/metadata/wmc/__init__.py | 0 .../movie/providers/metadata/wmc/main.py | 8 ++ .../providers/metadata/xbmc/__init__.py | 0 .../providers/metadata/xbmc/main.py | 4 +- .../media/movie/providers/nzb/__init__.py | 14 +++ .../core/media/movie/providers/nzb/base.py | 0 .../movie/providers/nzb/binsearch/__init__.py | 7 ++ .../movie/providers/nzb/binsearch/main.py | 24 ++++ .../movie/providers/nzb/newznab/__init__.py | 7 ++ .../media/movie/providers/nzb/newznab/main.py | 18 +++ .../movie/providers/nzb/nzbclub/__init__.py | 7 ++ .../media/movie/providers/nzb/nzbclub/main.py | 25 ++++ .../movie/providers/nzb/nzbindex/__init__.py | 7 ++ .../movie/providers/nzb/nzbindex/main.py | 28 +++++ .../providers/nzb/omgwtfnzbs/__init__.py | 7 ++ .../movie/providers/nzb/omgwtfnzbs/main.py | 9 ++ .../media/movie/providers/torrent/__init__.py | 14 +++ .../providers/torrent/awesomehd/__init__.py | 8 ++ .../movie/providers/torrent/awesomehd/main.py | 9 ++ .../providers/torrent/bithdtv/__init__.py | 7 ++ .../movie/providers/torrent/bithdtv/main.py | 17 +++ .../providers/torrent/bitsoup/__init__.py | 7 ++ .../movie/providers/torrent/bitsoup/main.py | 26 +++++ .../providers/torrent/hdbits/__init__.py | 7 ++ .../movie/providers/torrent/hdbits/main.py | 9 ++ .../torrent/ilovetorrents/__init__.py | 7 ++ .../providers/torrent/ilovetorrents/main.py | 9 ++ .../providers/torrent/iptorrents/__init__.py | 7 ++ .../providers/torrent/iptorrents/main.py | 20 ++++ .../torrent/kickasstorrents/__init__.py | 7 ++ .../providers/torrent/kickasstorrents/main.py | 9 ++ .../torrent/passthepopcorn/__init__.py | 7 ++ .../providers/torrent/passthepopcorn/main.py | 36 ++++++ .../providers/torrent/publichd/__init__.py | 7 ++ .../movie/providers/torrent/publichd/main.py | 12 ++ .../providers/torrent/sceneaccess/__init__.py | 7 ++ .../providers/torrent/sceneaccess/main.py | 30 +++++ .../torrent/thepiratebay/__init__.py | 7 ++ .../providers/torrent/thepiratebay/main.py | 24 ++++ .../torrent/torrentbytes/__init__.py | 7 ++ .../providers/torrent/torrentbytes/main.py | 9 ++ .../providers/torrent/torrentday/__init__.py | 7 ++ .../providers/torrent/torrentday/main.py | 18 +++ .../torrent/torrentleech/__init__.py | 7 ++ .../providers/torrent/torrentleech/main.py | 25 ++++ .../torrent/torrentpotato/__init__.py | 7 ++ .../providers/torrent/torrentpotato/main.py | 17 +++ .../torrent/torrentshack/__init__.py | 7 ++ .../providers/torrent/torrentshack/main.py | 34 ++++++ .../movie/providers/torrent/yify/__init__.py | 7 ++ .../movie/providers/torrent/yify/main.py | 9 ++ .../media/movie/providers/trailer/__init__.py | 0 .../providers/trailer/base.py | 2 +- .../providers/trailer/hdtrailers/__init__.py | 0 .../providers/trailer/hdtrailers/main.py | 2 +- .../movie/providers/userscript/__init__.py | 0 .../providers/userscript/allocine/__init__.py | 2 - .../providers/userscript/allocine/main.py | 2 +- .../userscript/appletrailers/__init__.py | 2 - .../userscript/appletrailers/main.py | 2 +- .../userscript/criticker/__init__.py | 2 - .../providers/userscript/criticker/main.py | 2 +- .../providers/userscript/filmweb/__init__.py | 2 - .../providers/userscript/filmweb/main.py | 2 +- .../userscript/flickchart/__init__.py | 2 - .../providers/userscript/flickchart/main.py | 2 +- .../providers/userscript/imdb/__init__.py | 2 - .../providers/userscript/imdb/main.py | 2 +- .../userscript/letterboxd/__init__.py | 2 - .../providers/userscript/letterboxd/main.py | 2 +- .../userscript/moviemeter/__init__.py | 2 - .../providers/userscript/moviemeter/main.py | 2 +- .../providers/userscript/moviesio/__init__.py | 2 - .../providers/userscript/moviesio/main.py | 6 + .../providers/userscript/reddit/__init__.py | 2 - .../providers/userscript/reddit/main.py | 2 +- .../userscript/rottentomatoes/__init__.py | 2 - .../userscript/rottentomatoes/main.py | 2 +- .../providers/userscript/sharethe/__init__.py | 2 - .../providers/userscript/sharethe/main.py | 2 +- .../providers/userscript/tmdb/__init__.py | 2 - .../providers/userscript/tmdb/main.py | 2 +- .../providers/userscript/trakt/__init__.py | 2 - .../providers/userscript/trakt/main.py | 2 +- .../providers/userscript/whiwa/__init__.py | 2 - .../providers/userscript/whiwa/main.py | 2 +- .../userscript/youteather/__init__.py | 2 - .../providers/userscript/youteather/main.py | 2 +- couchpotato/core/notifications/base.py | 2 +- libs/importlib/__init__.py | 38 ------ 181 files changed, 1118 insertions(+), 779 deletions(-) delete mode 100644 couchpotato/core/media/_base/providers/metadata/wmc/main.py delete mode 100644 couchpotato/core/media/_base/providers/userscript/moviesio/main.py rename couchpotato/core/media/{_base/providers/trailer => movie/providers}/__init__.py (100%) create mode 100644 couchpotato/core/media/movie/providers/automation/__init__.py create mode 100644 couchpotato/core/media/movie/providers/automation/base.py rename couchpotato/core/media/{_base => movie}/providers/automation/bluray/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/bluray/main.py (97%) rename couchpotato/core/media/{_base => movie}/providers/automation/flixster/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/flixster/main.py (94%) rename couchpotato/core/media/{_base => movie}/providers/automation/goodfilms/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/goodfilms/main.py (94%) rename couchpotato/core/media/{_base => movie}/providers/automation/imdb/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/imdb/main.py (96%) rename couchpotato/core/media/{_base => movie}/providers/automation/itunes/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/itunes/main.py (96%) rename couchpotato/core/media/{_base => movie}/providers/automation/kinepolis/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/kinepolis/main.py (89%) rename couchpotato/core/media/{_base => movie}/providers/automation/letterboxd/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/letterboxd/main.py (94%) rename couchpotato/core/media/{_base => movie}/providers/automation/moviemeter/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/moviemeter/main.py (90%) rename couchpotato/core/media/{_base => movie}/providers/automation/movies_io/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/movies_io/main.py (92%) rename couchpotato/core/media/{_base => movie}/providers/automation/rottentomatoes/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/rottentomatoes/main.py (95%) rename couchpotato/core/media/{_base => movie}/providers/automation/trakt/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/automation/trakt/main.py (94%) create mode 100644 couchpotato/core/media/movie/providers/base.py create mode 100644 couchpotato/core/media/movie/providers/info/__init__.py rename couchpotato/core/media/{_base => movie}/providers/info/_modifier/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/info/_modifier/main.py (100%) rename couchpotato/core/media/{_base => movie}/providers/info/couchpotatoapi/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/info/couchpotatoapi/main.py (98%) rename couchpotato/core/media/{_base => movie}/providers/info/omdbapi/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/info/omdbapi/main.py (98%) rename couchpotato/core/media/{_base => movie}/providers/info/themoviedb/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/info/themoviedb/main.py (98%) create mode 100644 couchpotato/core/media/movie/providers/metadata/__init__.py create mode 100644 couchpotato/core/media/movie/providers/metadata/base.py rename couchpotato/core/media/{_base => movie}/providers/metadata/wmc/__init__.py (100%) create mode 100644 couchpotato/core/media/movie/providers/metadata/wmc/main.py rename couchpotato/core/media/{_base => movie}/providers/metadata/xbmc/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/metadata/xbmc/main.py (98%) create mode 100644 couchpotato/core/media/movie/providers/nzb/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/base.py create mode 100644 couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/binsearch/main.py create mode 100644 couchpotato/core/media/movie/providers/nzb/newznab/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/newznab/main.py create mode 100644 couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/nzbclub/main.py create mode 100644 couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/nzbindex/main.py create mode 100644 couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py create mode 100644 couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/awesomehd/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/bithdtv/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/bitsoup/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/hdbits/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/iptorrents/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/publichd/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/publichd/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentday/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentleech/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentshack/main.py create mode 100644 couchpotato/core/media/movie/providers/torrent/yify/__init__.py create mode 100644 couchpotato/core/media/movie/providers/torrent/yify/main.py create mode 100644 couchpotato/core/media/movie/providers/trailer/__init__.py rename couchpotato/core/media/{_base => movie}/providers/trailer/base.py (78%) rename couchpotato/core/media/{_base => movie}/providers/trailer/hdtrailers/__init__.py (100%) rename couchpotato/core/media/{_base => movie}/providers/trailer/hdtrailers/main.py (98%) create mode 100644 couchpotato/core/media/movie/providers/userscript/__init__.py rename couchpotato/core/media/{_base => movie}/providers/userscript/allocine/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/allocine/main.py (90%) rename couchpotato/core/media/{_base => movie}/providers/userscript/appletrailers/__init__.py (85%) rename couchpotato/core/media/{_base => movie}/providers/userscript/appletrailers/main.py (85%) rename couchpotato/core/media/{_base => movie}/providers/userscript/criticker/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/criticker/main.py (51%) rename couchpotato/core/media/{_base => movie}/providers/userscript/filmweb/__init__.py (82%) rename couchpotato/core/media/{_base => movie}/providers/userscript/filmweb/main.py (89%) rename couchpotato/core/media/{_base => movie}/providers/userscript/flickchart/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/flickchart/main.py (90%) rename couchpotato/core/media/{_base => movie}/providers/userscript/imdb/__init__.py (81%) rename couchpotato/core/media/{_base => movie}/providers/userscript/imdb/main.py (76%) rename couchpotato/core/media/{_base => movie}/providers/userscript/letterboxd/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/letterboxd/main.py (50%) rename couchpotato/core/media/{_base => movie}/providers/userscript/moviemeter/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/moviemeter/main.py (58%) rename couchpotato/core/media/{_base => movie}/providers/userscript/moviesio/__init__.py (83%) create mode 100644 couchpotato/core/media/movie/providers/userscript/moviesio/main.py rename couchpotato/core/media/{_base => movie}/providers/userscript/reddit/__init__.py (82%) rename couchpotato/core/media/{_base => movie}/providers/userscript/reddit/main.py (85%) rename couchpotato/core/media/{_base => movie}/providers/userscript/rottentomatoes/__init__.py (85%) rename couchpotato/core/media/{_base => movie}/providers/userscript/rottentomatoes/main.py (92%) rename couchpotato/core/media/{_base => movie}/providers/userscript/sharethe/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/sharethe/main.py (58%) rename couchpotato/core/media/{_base => movie}/providers/userscript/tmdb/__init__.py (81%) rename couchpotato/core/media/{_base => movie}/providers/userscript/tmdb/main.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/trakt/__init__.py (81%) rename couchpotato/core/media/{_base => movie}/providers/userscript/trakt/main.py (68%) rename couchpotato/core/media/{_base => movie}/providers/userscript/whiwa/__init__.py (81%) rename couchpotato/core/media/{_base => movie}/providers/userscript/whiwa/main.py (50%) rename couchpotato/core/media/{_base => movie}/providers/userscript/youteather/__init__.py (83%) rename couchpotato/core/media/{_base => movie}/providers/userscript/youteather/main.py (85%) delete mode 100644 libs/importlib/__init__.py diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 6c3a719d..5a528b13 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -24,9 +24,6 @@ class Loader(object): 'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')), } - # Add providers to loader - self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False) - # Add media to loader self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True) @@ -103,7 +100,7 @@ class Loader(object): def loadSettings(self, module, name, save = True): if not hasattr(module, 'config'): - log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__) + #log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__) return False try: diff --git a/couchpotato/core/media/_base/providers/automation/__init__.py b/couchpotato/core/media/_base/providers/automation/__init__.py index 93f6c10a..e69de29b 100644 --- a/couchpotato/core/media/_base/providers/automation/__init__.py +++ b/couchpotato/core/media/_base/providers/automation/__init__.py @@ -1,21 +0,0 @@ -config = [{ - 'name': 'automation_providers', - 'groups': [ - { - 'label': 'Watchlists', - 'description': 'Check watchlists for new movies', - 'type': 'list', - 'name': 'watchlist_providers', - 'tab': 'automation', - 'options': [], - }, - { - 'label': 'Automated', - 'description': 'Uses minimal requirements', - 'type': 'list', - 'name': 'automation_providers', - 'tab': 'automation', - 'options': [], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/automation/base.py b/couchpotato/core/media/_base/providers/automation/base.py index e57f5c63..20ce2e16 100644 --- a/couchpotato/core/media/_base/providers/automation/base.py +++ b/couchpotato/core/media/_base/providers/automation/base.py @@ -1,96 +1,8 @@ -from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import Provider -from couchpotato.environment import Env -from couchpotato.core.helpers.variable import splitString -import time +from couchpotato.core.media._base.providers.base import Provider log = CPLog(__name__) -class Automation(Provider): - - enabled_option = 'automation_enabled' - http_time_between_calls = 2 - - interval = 1800 - last_checked = 0 - - def __init__(self): - addEvent('automation.get_movies', self._getMovies) - - def _getMovies(self): - - if self.isDisabled(): - return - - if not self.canCheck(): - log.debug('Just checked, skipping %s', self.getName()) - return [] - - self.last_checked = time.time() - - return self.getIMDBids() - - def search(self, name, year = None, imdb_only = False): - - prop_name = 'automation.cached.%s.%s' % (name, year) - cached_imdb = Env.prop(prop_name, default = False) - if cached_imdb and imdb_only: - return cached_imdb - - result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True) - - if len(result) > 0: - if imdb_only and result[0].get('imdb'): - Env.prop(prop_name, result[0].get('imdb')) - - return result[0].get('imdb') if imdb_only else result[0] - else: - return None - - def isMinimalMovie(self, movie): - if not movie.get('rating'): - log.info('ignoring %s as no rating is available for.', (movie['original_title'])) - return False - - if movie['rating'] and movie['rating'].get('imdb'): - movie['votes'] = movie['rating']['imdb'][1] - movie['rating'] = movie['rating']['imdb'][0] - - for minimal_type in ['year', 'rating', 'votes']: - type_value = movie.get(minimal_type, 0) - type_min = self.getMinimal(minimal_type) - if type_value < type_min: - log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value)) - return False - - movie_genres = [genre.lower() for genre in movie['genres']] - required_genres = splitString(self.getMinimal('required_genres').lower()) - ignored_genres = splitString(self.getMinimal('ignored_genres').lower()) - - req_match = 0 - for req_set in required_genres: - req = splitString(req_set, '&') - req_match += len(list(set(movie_genres) & set(req))) == len(req) - - if self.getMinimal('required_genres') and req_match == 0: - log.info2('Required genre(s) missing for %s', movie['original_title']) - return False - - for ign_set in ignored_genres: - ign = splitString(ign_set, '&') - if len(list(set(movie_genres) & set(ign))) == len(ign): - log.info2('%s has blacklisted genre(s): %s', (movie['original_title'], ign)) - return False - - return True - - def getMinimal(self, min_type): - return Env.setting(min_type, 'automation') - - def getIMDBids(self): - return [] - - def canCheck(self): - return time.time() > self.last_checked + self.interval +class BaseAutomation(Provider): + pass diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index c772be47..761aedca 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -14,7 +14,6 @@ import xml.etree.ElementTree as XMLTree log = CPLog(__name__) - class MultiProvider(Plugin): def __init__(self): @@ -102,7 +101,6 @@ class Provider(Plugin): class YarrProvider(Provider): protocol = None # nzb, torrent, torrent_magnet - type = 'movie' cat_ids = {} cat_backup_id = None @@ -180,7 +178,7 @@ class YarrProvider(Provider): return 'try_next' - def search(self, movie, quality): + def search(self, media, quality): if self.isDisabled(): return [] @@ -192,15 +190,17 @@ class YarrProvider(Provider): # Create result container imdb_results = hasattr(self, '_search') - results = ResultList(self, movie, quality, imdb_results = imdb_results) + results = ResultList(self, media, quality, imdb_results = imdb_results) # Do search based on imdb id if imdb_results: - self._search(movie, quality, results) + self._search(media, quality, results) # Search possible titles else: - for title in possibleTitles(getTitle(movie)): - self._searchOnTitle(title, movie, quality, results) + media_title = fireEvent('library.query', media['library'], single = True) + + for title in possibleTitles(media_title): + self._searchOnTitle(title, media, quality, results) return results @@ -243,8 +243,7 @@ class YarrProvider(Provider): def getCatId(self, identifier): - for cats in self.cat_ids: - ids, qualities = cats + for ids, qualities in self.cat_ids: if identifier in qualities: return ids diff --git a/couchpotato/core/media/_base/providers/info/base.py b/couchpotato/core/media/_base/providers/info/base.py index 1b43ab8a..90a9153c 100644 --- a/couchpotato/core/media/_base/providers/info/base.py +++ b/couchpotato/core/media/_base/providers/info/base.py @@ -1,5 +1,5 @@ -from couchpotato.core.providers.base import Provider +from couchpotato.core.media._base.providers.base import Provider -class MovieProvider(Provider): - type = 'movie' +class BaseInfoProvider(Provider): + type = 'unknown' diff --git a/couchpotato/core/media/_base/providers/metadata/base.py b/couchpotato/core/media/_base/providers/metadata/base.py index 31e610cb..2a8c5cfe 100644 --- a/couchpotato/core/media/_base/providers/metadata/base.py +++ b/couchpotato/core/media/_base/providers/metadata/base.py @@ -1,110 +1,8 @@ -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import sp -from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.environment import Env -import os -import shutil -import traceback log = CPLog(__name__) class MetaDataBase(Plugin): - - enabled_option = 'meta_enabled' - - def __init__(self): - addEvent('renamer.after', self.create) - - def create(self, message = None, group = None): - if self.isDisabled(): return - if not group: group = {} - - log.info('Creating %s metadata.', self.getName()) - - # Update library to get latest info - try: - group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = group['media']['identifier'], extended = True, single = True) - except: - log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) - - root_name = self.getRootName(group) - meta_name = os.path.basename(root_name) - root = os.path.dirname(root_name) - - movie_info = group['media'].get('info') - - for file_type in ['nfo', 'thumbnail', 'fanart']: - try: - # Get file path - name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root) - - if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): - - # Get file content - content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group) - if content: - log.debug('Creating %s file: %s', (file_type, name)) - if os.path.isfile(content): - content = sp(content) - name = sp(name) - - shutil.copy2(content, name) - shutil.copyfile(content, name) - - # Try and copy stats seperately - try: shutil.copystat(content, name) - except: pass - else: - self.createFile(name, content) - group['renamed_files'].append(name) - - try: - os.chmod(sp(name), Env.getPermission('file')) - except: - log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) - - except: - log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) - - def getRootName(self, data = None): - if not data: data = {} - return os.path.join(data['destination_dir'], data['filename']) - - def getFanartName(self, name, root): - return - - def getThumbnailName(self, name, root): - return - - def getNfoName(self, name, root): - return - - def getNfo(self, movie_info = None, data = None): - if not data: data = {} - if not movie_info: movie_info = {} - - def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original'): - if not data: data = {} - if not movie_info: movie_info = {} - - # See if it is in current files - files = data['media'].get('files') - if files.get('image_' + wanted_file_type): - if os.path.isfile(files['image_' + wanted_file_type][0]): - return files['image_' + wanted_file_type][0] - - # Download using existing info - try: - images = movie_info['images'][wanted_file_type] - file_path = fireEvent('file.download', url = images[0], single = True) - return file_path - except: - pass - - def getFanart(self, movie_info = None, data = None): - if not data: data = {} - if not movie_info: movie_info = {} - return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original') + pass diff --git a/couchpotato/core/media/_base/providers/metadata/wmc/main.py b/couchpotato/core/media/_base/providers/metadata/wmc/main.py deleted file mode 100644 index f84897b4..00000000 --- a/couchpotato/core/media/_base/providers/metadata/wmc/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from couchpotato.core.providers.metadata.base import MetaDataBase -import os - - -class WindowsMediaCenter(MetaDataBase): - - def getThumbnailName(self, name, root): - return os.path.join(root, 'folder.jpg') diff --git a/couchpotato/core/media/_base/providers/nzb/base.py b/couchpotato/core/media/_base/providers/nzb/base.py index 53c73af0..31cbb477 100644 --- a/couchpotato/core/media/_base/providers/nzb/base.py +++ b/couchpotato/core/media/_base/providers/nzb/base.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.base import YarrProvider +from couchpotato.core.media._base.providers.base import YarrProvider import time diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py b/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py index c80ee6d9..48e71cdb 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py @@ -1,8 +1,3 @@ -from .main import BinSearch - - -def start(): - return BinSearch() config = [{ 'name': 'binsearch', diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch/main.py b/couchpotato/core/media/_base/providers/nzb/binsearch/main.py index 60007d5a..fb72a848 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch/main.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch/main.py @@ -1,16 +1,14 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.nzb.base import NZBProvider -from couchpotato.environment import Env import re import traceback +from couchpotato.core.media._base.providers.nzb.base import NZBProvider log = CPLog(__name__) -class BinSearch(NZBProvider): +class Base(NZBProvider): urls = { 'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s', @@ -18,23 +16,11 @@ class BinSearch(NZBProvider): 'search': 'https://www.binsearch.info/index.php?%s', } - http_time_between_calls = 4 # Seconds + http_time_between_calls = 4 # Seconds - def _search(self, movie, quality, results): + def _search(self, media, quality, results): - arguments = tryUrlencode({ - 'q': movie['identifier'], - 'm': 'n', - 'max': 400, - 'adv_age': Env.setting('retention', 'nzb'), - 'adv_sort': 'date', - 'adv_col': 'on', - 'adv_nfo': 'on', - 'minsize': quality.get('size_min'), - 'maxsize': quality.get('size_max'), - }) - - data = self.getHTMLData(self.urls['search'] % arguments) + data = self.getHTMLData(self.urls['search'] % self.buildUrl(media, quality)) if data: try: @@ -101,4 +87,3 @@ class BinSearch(NZBProvider): log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc())) return 'try_next' - diff --git a/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py b/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py index 86a25a4f..8426e3ed 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py @@ -1,8 +1,3 @@ -from .main import Newznab - - -def start(): - return Newznab() config = [{ 'name': 'newznab', diff --git a/couchpotato/core/media/_base/providers/nzb/newznab/main.py b/couchpotato/core/media/_base/providers/nzb/newznab/main.py index c6830b79..acc4a7d3 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab/main.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab/main.py @@ -2,8 +2,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import ResultList -from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.core.media._base.providers.nzb.base import NZBProvider from couchpotato.environment import Env from dateutil.parser import parse from urllib2 import HTTPError @@ -15,39 +14,35 @@ import urllib2 log = CPLog(__name__) -class Newznab(NZBProvider, RSS): +class Base(NZBProvider, RSS): urls = { - 'download': 'get&id=%s', 'detail': 'details&id=%s', - 'search': 'movie', + 'download': 't=get&id=%s' } limits_reached = {} http_time_between_calls = 1 # Seconds - def search(self, movie, quality): + def search(self, media, quality): hosts = self.getHosts() - results = ResultList(self, movie, quality, imdb_results = True) + results = ResultList(self, media, quality, imdb_results = True) for host in hosts: if self.isDisabled(host): continue - self._searchOnHost(host, movie, quality, results) + self._searchOnHost(host, media, quality, results) return results - def _searchOnHost(self, host, movie, quality, results): + def _searchOnHost(self, host, media, quality, results): - arguments = tryUrlencode({ - 'imdbid': movie['identifier'].replace('tt', ''), - 'apikey': host['api_key'], - 'extended': 1 - }) + ('&%s' % host['custom_tag'] if host.get('custom_tag') else '') - url = '%s&%s' % (self.getUrl(host['host'], self.urls['search']), arguments) + query = self.buildUrl(media, host['api_key']) + + url = '%s&%s' % (self.getUrl(host['host']), query) nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) @@ -136,11 +131,11 @@ class Newznab(NZBProvider, RSS): if result: return result - def getUrl(self, host, type): + def getUrl(self, host): if '?page=newznabapi' in host: - return cleanHost(host)[:-1] + '&t=' + type + return cleanHost(host)[:-1] + '&' - return cleanHost(host) + 'api?t=' + type + return cleanHost(host) + 'api?' def isDisabled(self, host = None): return not self.isEnabled(host) diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py index 02a69404..60c9bc4e 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py @@ -1,8 +1,3 @@ -from .main import NZBClub - - -def start(): - return NZBClub() config = [{ 'name': 'nzbclub', diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py b/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py index 44121a60..322f8198 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py @@ -1,40 +1,26 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse import time log = CPLog(__name__) -class NZBClub(NZBProvider, RSS): +class Base(NZBProvider, RSS): urls = { 'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s', } - http_time_between_calls = 4 #seconds + http_time_between_calls = 4 #seconds - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): - q = '"%s %s"' % (title, movie['info']['year']) - - q_param = tryUrlencode({ - 'q': q, - }) - - params = tryUrlencode({ - 'ig': 1, - 'rpp': 200, - 'st': 5, - 'sp': 1, - 'ns': 1, - }) - - nzbs = self.getRSSData(self.urls['search'] % ('%s&%s' % (q_param, params))) + nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media)) for nzb in nzbs: diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py index acb53e19..4ce51050 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py @@ -1,8 +1,3 @@ -from .main import NzbIndex - - -def start(): - return NzbIndex() config = [{ 'name': 'nzbindex', diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py b/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py index d29d94ff..d21dc0b2 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py @@ -1,10 +1,10 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.nzb.base import NZBProvider -from couchpotato.environment import Env +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse import re import time @@ -12,7 +12,7 @@ import time log = CPLog(__name__) -class NzbIndex(NZBProvider, RSS): +class Base(NZBProvider, RSS): urls = { 'download': 'https://www.nzbindex.com/download/', @@ -21,28 +21,44 @@ class NzbIndex(NZBProvider, RSS): http_time_between_calls = 1 # Seconds - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): - q = '"%s %s" | "%s (%s)"' % (title, movie['info']['year'], title, movie['info']['year']) - arguments = tryUrlencode({ - 'q': q, - 'age': Env.setting('retention', 'nzb'), - 'sort': 'agedesc', - 'minsize': quality.get('size_min'), - 'maxsize': quality.get('size_max'), - 'rating': 1, - 'max': 250, - 'more': 1, - 'complete': 1, - }) - - nzbs = self.getRSSData(self.urls['search'] % arguments) + nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media, quality)) for nzb in nzbs: enclosure = self.getElement(nzb, 'enclosure').attrib nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4]) + title = self.getTextElement(nzb, "title") + + match = fireEvent('matcher.parse', title, parser='usenet', single = True) + if not match.chains: + log.info('Unable to parse release with title "%s"', title) + continue + + # TODO should we consider other lower-weight chains here? + info = fireEvent('matcher.flatten_info', match.chains[0].info, single = True) + + release_name = fireEvent('matcher.construct_from_raw', info.get('release_name'), single = True) + + file_name = info.get('detail', {}).get('file_name') + file_name = file_name[0] if file_name else None + + title = release_name or file_name + + # Strip extension from parsed title (if one exists) + ext_pos = title.rfind('.') + + # Assume extension if smaller than 4 characters + # TODO this should probably be done a better way + if len(title[ext_pos + 1:]) <= 4: + title = title[:ext_pos] + + if not title: + log.info('Unable to find release name from match') + continue + try: description = self.getTextElement(nzb, "description") except: @@ -57,7 +73,7 @@ class NzbIndex(NZBProvider, RSS): results.append({ 'id': nzbindex_id, - 'name': self.getTextElement(nzb, "title"), + 'name': title, 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'size': tryInt(enclosure['length']) / 1024 / 1024, 'url': enclosure['url'], @@ -76,4 +92,3 @@ class NzbIndex(NZBProvider, RSS): item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text) except: pass - diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py index 2f3990de..841c04e7 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py @@ -1,8 +1,3 @@ -from .main import OMGWTFNZBs - - -def start(): - return OMGWTFNZBs() config = [{ 'name': 'omgwtfnzbs', diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py index 73254632..6ecefe37 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse from urlparse import urlparse, parse_qs import time @@ -11,7 +11,7 @@ import time log = CPLog(__name__) -class OMGWTFNZBs(NZBProvider, RSS): +class Base(NZBProvider, RSS): urls = { 'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s', diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py b/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py index 6f076703..1ab625d8 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py @@ -1,8 +1,3 @@ -from .main import AwesomeHD - - -def start(): - return AwesomeHD() config = [{ 'name': 'awesomehd', diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py b/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py index cc26030c..3a370435 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py @@ -1,14 +1,14 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class AwesomeHD(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://awesome-hd.net/', diff --git a/couchpotato/core/media/_base/providers/torrent/base.py b/couchpotato/core/media/_base/providers/torrent/base.py index e134c8f3..ea582e49 100644 --- a/couchpotato/core/media/_base/providers/torrent/base.py +++ b/couchpotato/core/media/_base/providers/torrent/base.py @@ -1,7 +1,6 @@ -import traceback from couchpotato.core.helpers.variable import getImdb, md5, cleanHost from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import YarrProvider +from couchpotato.core.media._base.providers.base import YarrProvider from couchpotato.environment import Env import time @@ -15,6 +14,22 @@ class TorrentProvider(YarrProvider): proxy_domain = None proxy_list = [] + def imdbMatch(self, url, imdbId): + if getImdb(url) == imdbId: + return True + + if url[:4] == 'http': + try: + cache_key = md5(url) + data = self.getCache(cache_key, url) + except IOError: + log.error('Failed to open %s.', url) + return False + + return getImdb(data) == imdbId + + return False + def getDomain(self, url = ''): forced_domain = self.conf('domain') diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py b/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py index ffc5363f..d2b73913 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py @@ -1,8 +1,3 @@ -from .main import BiTHDTV - - -def start(): - return BiTHDTV() config = [{ 'name': 'bithdtv', diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py b/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py index 259de6bc..ce2ea71a 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py @@ -1,14 +1,14 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class BiTHDTV(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'http://www.bit-hdtv.com/', @@ -19,18 +19,13 @@ class BiTHDTV(TorrentProvider): } # Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken - cat_id_movies = 7 - http_time_between_calls = 1 #seconds - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): - arguments = tryUrlencode({ - 'search': '%s %s' % (title.replace(':', ''), movie['info']['year']), - 'cat': self.cat_id_movies - }) + query = self.buildUrl(media) - url = "%s&%s" % (self.urls['search'], arguments) + url = "%s&%s" % (self.urls['search'], query) data = self.getHTMLData(url) diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py b/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py index da07cc3b..efa34a77 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py @@ -1,8 +1,3 @@ -from .main import Bitsoup - - -def start(): - return Bitsoup() config = [{ 'name': 'bitsoup', diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py b/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py index c4035635..9a8c143f 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py @@ -2,13 +2,13 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class Bitsoup(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://www.bitsoup.me/', @@ -28,6 +28,7 @@ class Bitsoup(TorrentProvider): }) url = "%s&%s" % (self.urls['search'], arguments) + url = self.urls['search'] % self.buildUrl(media, quality) data = self.getHTMLData(url) if data: @@ -84,4 +85,3 @@ class Bitsoup(TorrentProvider): return 'logout.php' in output.lower() loginCheckSuccess = loginSuccess - diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py b/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py index b54adb0b..c9e75fe5 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py @@ -1,8 +1,3 @@ -from .main import HDBits - - -def start(): - return HDBits() config = [{ 'name': 'hdbits', diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits/main.py b/couchpotato/core/media/_base/providers/torrent/hdbits/main.py index 461745b9..a1ceb573 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits/main.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits/main.py @@ -1,15 +1,15 @@ from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import re import json import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class HDBits(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://hdbits.org/', diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py index f3f7b479..2a9f403d 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py @@ -1,8 +1,3 @@ -from .main import ILoveTorrents - - -def start(): - return ILoveTorrents() config = [{ 'name': 'ilovetorrents', diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py index d15be35b..516cf07e 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py @@ -2,14 +2,14 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class ILoveTorrents(TorrentProvider): +class Base(TorrentProvider): urls = { 'download': 'https://www.ilovetorrents.me/%s', diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py index 579d7974..48f8219b 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py @@ -1,8 +1,3 @@ -from .main import IPTorrents - - -def start(): - return IPTorrents() config = [{ 'name': 'iptorrents', diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py b/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py index 7ff3cb91..cfd568e2 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py @@ -1,14 +1,15 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode, toSafeString +from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider +import six log = CPLog(__name__) -class IPTorrents(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://www.iptorrents.com/', @@ -18,26 +19,33 @@ class IPTorrents(TorrentProvider): 'search': 'https://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d', } - cat_ids = [ - ([48], ['720p', '1080p', 'bd50']), - ([72], ['cam', 'ts', 'tc', 'r5', 'scr']), - ([7], ['dvdrip', 'brrip']), - ([6], ['dvdr']), - ] - http_time_between_calls = 1 #seconds cat_backup_id = None - def _searchOnTitle(self, title, movie, quality, results): + def buildUrl(self, title, media, quality): + return self._buildUrl(title.replace(':', ''), quality['identifier']) + + def _buildUrl(self, query, quality_identifier): + + cat_ids = self.getCatId(quality_identifier) + + if not cat_ids: + log.warning('Unable to find category ids for identifier "%s"', quality_identifier) + return None + + return self.urls['search'] % ("&".join(("l%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%')) + + def _searchOnTitle(self, title, media, quality, results): freeleech = '' if not self.conf('freeleech') else '&free=on' + base_url = self.buildUrl(title, media, quality) + if not base_url: return + pages = 1 current_page = 1 while current_page <= pages and not self.shuttingDown(): - - url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), current_page) - data = self.getHTMLData(url) + data = self.getHTMLData(base_url % (freeleech, current_page)) if data: html = BeautifulSoup(data) @@ -57,27 +65,21 @@ class IPTorrents(TorrentProvider): entries = result_table.find_all('tr') - columns = self.getColumns(entries) - - if 'seeders' not in columns or 'leechers' not in columns: - log.warning('Unrecognized table format returned') - return - for result in entries[1:]: - cells = result.find_all('td') - if len(cells) <= 1: + torrent = result.find_all('td') + if len(torrent) <= 1: break - torrent = cells[1].find('a') + torrent = torrent[1].find('a') torrent_id = torrent['href'].replace('/details.php?id=', '') - torrent_name = unicode(torrent.string) + torrent_name = six.text_type(torrent.string) torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.') torrent_details_url = self.urls['base_url'] + torrent['href'] torrent_size = self.parseSize(result.find_all('td')[5].string) - torrent_seeders = tryInt(cells[columns['seeders']].string) - torrent_leechers = tryInt(cells[columns['leechers']].string) + torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string) + torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string) results.append({ 'id': torrent_id, @@ -95,20 +97,6 @@ class IPTorrents(TorrentProvider): current_page += 1 - def getColumns(self, entries): - result = {} - - for x, col in enumerate(entries[0].find_all('th')): - name = col.text or col.find('img')['title'] - key = toSafeString(name).strip().lower() - - if not key: - continue - - result[key] = x - - return result - def getLoginParams(self): return { 'username': self.conf('username'), diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py index ffe1040b..92dcc0fc 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py @@ -1,8 +1,3 @@ -from .main import KickAssTorrents - - -def start(): - return KickAssTorrents() config = [{ 'name': 'kickasstorrents', diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py index 8752348e..8b624652 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py @@ -1,14 +1,14 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentMagnetProvider import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider log = CPLog(__name__) -class KickAssTorrents(TorrentMagnetProvider): +class Base(TorrentMagnetProvider): urls = { 'detail': '%s/%s', @@ -34,9 +34,9 @@ class KickAssTorrents(TorrentMagnetProvider): 'http://www.kickassproxy.info', ] - def _search(self, movie, quality, results): + def _search(self, media, quality, results): - data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', movie['identifier'].replace('tt', ''))) + data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', media['identifier'].replace('tt', ''))) if data: diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py index a3e57c79..119c4255 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py @@ -1,8 +1,3 @@ -from .main import PassThePopcorn - - -def start(): - return PassThePopcorn() config = [{ 'name': 'passthepopcorn', diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py index bc6136fc..a7975e29 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from dateutil.parser import parse import htmlentitydefs import json @@ -13,56 +13,28 @@ import six log = CPLog(__name__) -class PassThePopcorn(TorrentProvider): +class Base(TorrentProvider): urls = { - 'domain': 'https://tls.passthepopcorn.me', - 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', - 'torrent': 'https://tls.passthepopcorn.me/torrents.php', - 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', - 'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login', - 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' + 'domain': 'https://tls.passthepopcorn.me', + 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', + 'torrent': 'https://tls.passthepopcorn.me/torrents.php', + 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', + 'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login', + 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' } http_time_between_calls = 2 - quality_search_params = { - 'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, - '1080p': {'resolution': '1080p'}, - '720p': {'resolution': '720p'}, - 'brrip': {'media': 'Blu-ray'}, - 'dvdr': {'resolution': 'anysd'}, - 'dvdrip': {'media': 'DVD'}, - 'scr': {'media': 'DVD-Screener'}, - 'r5': {'media': 'R5'}, - 'tc': {'media': 'TC'}, - 'ts': {'media': 'TS'}, - 'cam': {'media': 'CAM'} - } + def _search(self, media, quality, results): - post_search_filters = { - 'bd50': {'Codec': ['BD50']}, - '1080p': {'Resolution': ['1080p']}, - '720p': {'Resolution': ['720p']}, - 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, - 'dvdr': {'Codec': ['DVD5', 'DVD9']}, - 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, - 'scr': {'Source': ['DVD-Screener']}, - 'r5': {'Source': ['R5']}, - 'tc': {'Source': ['TC']}, - 'ts': {'Source': ['TS']}, - 'cam': {'Source': ['CAM']} - } - - def _search(self, movie, quality, results): - - movie_title = getTitle(movie) + movie_title = getTitle(media) quality_id = quality['identifier'] params = mergeDicts(self.quality_search_params[quality_id].copy(), { 'order_by': 'relevance', 'order_way': 'descending', - 'searchstr': movie['identifier'] + 'searchstr': media['identifier'] }) url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) diff --git a/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py b/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py index 3c20c51f..d24cc68e 100644 --- a/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py @@ -1,8 +1,3 @@ -from .main import PublicHD - - -def start(): - return PublicHD() config = [{ 'name': 'publichd', diff --git a/couchpotato/core/media/_base/providers/torrent/publichd/main.py b/couchpotato/core/media/_base/providers/torrent/publichd/main.py index 8749b582..ad2035ed 100644 --- a/couchpotato/core/media/_base/providers/torrent/publichd/main.py +++ b/couchpotato/core/media/_base/providers/torrent/publichd/main.py @@ -2,15 +2,16 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentMagnetProvider from urlparse import parse_qs import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider +import six log = CPLog(__name__) -class PublicHD(TorrentMagnetProvider): +class Base(TorrentMagnetProvider): urls = { 'test': 'https://publichd.se', @@ -24,13 +25,15 @@ class PublicHD(TorrentMagnetProvider): if not quality.get('hd', False): return [] - return super(PublicHD, self).search(movie, quality) + return super(Base, self).search(movie, quality) - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): + + query = self.buildUrl(media) params = tryUrlencode({ 'page':'torrents', - 'search': '%s %s' % (title, movie['info']['year']), + 'search': query, 'active': 1, }) @@ -54,7 +57,7 @@ class PublicHD(TorrentMagnetProvider): results.append({ 'id': url['id'][0], - 'name': unicode(info_url.string), + 'name': six.text_type(info_url.string), 'url': download['href'], 'detail_url': self.urls['detail'] % url['id'][0], 'size': self.parseSize(result.find_all('td')[7].string), diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py index 3fa5d97f..68592a37 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py @@ -1,8 +1,3 @@ -from .main import SceneAccess - - -def start(): - return SceneAccess() config = [{ 'name': 'sceneaccess', diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py index fd8a2bed..29abbb6d 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py @@ -1,14 +1,14 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class SceneAccess(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://www.sceneaccess.eu/', @@ -16,35 +16,15 @@ class SceneAccess(TorrentProvider): 'login_check': 'https://www.sceneaccess.eu/inbox', 'detail': 'https://www.sceneaccess.eu/details?id=%s', 'search': 'https://www.sceneaccess.eu/browse?c%d=%d', + 'archive': 'https://www.sceneaccess.eu/archive?&c%d=%d', 'download': 'https://www.sceneaccess.eu/%s', } - cat_ids = [ - ([22], ['720p', '1080p']), - ([7], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), - ([8], ['dvdr']), - ] - - http_time_between_calls = 1 #seconds - - def _search(self, movie, quality, results): - - cat = self.getCatId(quality['identifier']) - if not cat: - return - - url = self.urls['search'] % ( - cat[0], - cat[0] - ) - - arguments = tryUrlencode({ - 'search': movie['identifier'], - 'method': 3, - }) - url = "%s&%s" % (url, arguments) + http_time_between_calls = 1 #seconds + def _search(self, media, quality, results): + url = self.buildUrl(media, quality) data = self.getHTMLData(url) if data: @@ -77,13 +57,6 @@ class SceneAccess(TorrentProvider): except: log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) - def getLoginParams(self): - return { - 'username': self.conf('username'), - 'password': self.conf('password'), - 'submit': 'come on in', - } - def getMoreInfo(self, item): full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) @@ -93,6 +66,14 @@ class SceneAccess(TorrentProvider): item['description'] = description return item + # Login + def getLoginParams(self): + return { + 'username': self.conf('username'), + 'password': self.conf('password'), + 'submit': 'come on in', + } + def loginSuccess(self, output): return '/inbox' in output.lower() diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py index 8b3921cd..047562a5 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py @@ -1,8 +1,3 @@ -from .main import ThePirateBay - - -def start(): - return ThePirateBay() config = [{ 'name': 'thepiratebay', diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py index 3bf53f75..64995628 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py @@ -1,29 +1,22 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentMagnetProvider import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider import six log = CPLog(__name__) -class ThePirateBay(TorrentMagnetProvider): +class Base(TorrentMagnetProvider): urls = { - 'detail': '%s/torrent/%s', - 'search': '%s/search/%s/%s/7/%s' + 'detail': '%s/torrent/%s', + 'search': '%s/search/%%s/%%s/7/%%s' } - cat_ids = [ - ([207], ['720p', '1080p']), - ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), - ([201, 207], ['brrip']), - ([202], ['dvdr']) - ] - cat_backup_id = 200 disable_provider = False http_time_between_calls = 0 @@ -37,18 +30,21 @@ class ThePirateBay(TorrentMagnetProvider): 'http://nl.tpb.li', 'http://proxybay.eu', 'https://www.getpirate.com', - 'http://piratebay.io', + 'http://piratebay.io', ] - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): page = 0 total_pages = 1 cats = self.getCatId(quality['identifier']) + search_url = self.urls['search'] % self.getDomain() + while page < total_pages: - search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['info']['year'])), page, ','.join(str(x) for x in cats)) + search_url = search_url % self.buildUrl(media, page, cats) + page += 1 data = self.getHTMLData(search_url) @@ -88,7 +84,7 @@ class ThePirateBay(TorrentMagnetProvider): results.append({ 'id': re.search('/(?P\d+)/', link['href']).group('id'), - 'name': unicode(link.string), + 'name': six.text_type(link.string), 'url': download['href'], 'detail_url': self.getDomain(link['href']), 'size': self.parseSize(size), @@ -102,7 +98,7 @@ class ThePirateBay(TorrentMagnetProvider): log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) def isEnabled(self): - return super(ThePirateBay, self).isEnabled() and self.getDomain() + return super(Base, self).isEnabled() and self.getDomain() def correctProxy(self, data): return 'title="Pirate Search"' in data @@ -110,7 +106,7 @@ class ThePirateBay(TorrentMagnetProvider): def getMoreInfo(self, item): full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) - nfo_pre = html.find('div', attrs = {'class': 'nfo'}) + nfo_pre = html.find('div', attrs = {'class':'nfo'}) description = toUnicode(nfo_pre.text) if nfo_pre else '' item['description'] = description diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py index 79dec932..46eb52eb 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py @@ -1,8 +1,3 @@ -from .main import TorrentBytes - - -def start(): - return TorrentBytes() config = [{ 'name': 'torrentbytes', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py index b12b1e06..4b89db1e 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py @@ -2,13 +2,13 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class TorrentBytes(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://www.torrentbytes.net/', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py index 133ec914..5067590d 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py @@ -1,8 +1,3 @@ -from .main import TorrentDay - - -def start(): - return TorrentDay() config = [{ 'name': 'torrentday', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday/main.py b/couchpotato/core/media/_base/providers/torrent/torrentday/main.py index 83177a22..eebfaed2 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday/main.py @@ -1,11 +1,11 @@ from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class TorrentDay(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'http://www.td.af/', @@ -16,25 +16,18 @@ class TorrentDay(TorrentProvider): 'download': 'http://www.td.af/download.php/%s/%s', } - cat_ids = [ - ([11], ['720p', '1080p']), - ([1, 21, 25], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), - ([3], ['dvdr']), - ([5], ['bd50']), - ] + http_time_between_calls = 1 #seconds - http_time_between_calls = 1 #seconds + def _search(self, media, quality, results): - def _searchOnTitle(self, title, movie, quality, results): - - q = '"%s %s"' % (title, movie['info']['year']) + query = self.buildUrl(media) data = { '/browse.php?': None, 'cata': 'yes', 'jxt': 8, 'jxw': 'b', - 'search': q, + 'search': query, } data = self.getJsonData(self.urls['search'], data = data) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py index e64d4baa..2cdbd57b 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py @@ -1,8 +1,3 @@ -from .main import TorrentLeech - - -def start(): - return TorrentLeech() config = [{ 'name': 'torrentleech', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py b/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py index cb7cc043..1c907f4e 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py @@ -1,15 +1,14 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback - +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider +import six log = CPLog(__name__) -class TorrentLeech(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'http://www.torrentleech.org/', @@ -20,22 +19,13 @@ class TorrentLeech(TorrentProvider): 'download': 'http://www.torrentleech.org%s', } - cat_ids = [ - ([13], ['720p', '1080p']), - ([8], ['cam']), - ([9], ['ts', 'tc']), - ([10], ['r5', 'scr']), - ([11], ['dvdrip']), - ([14], ['brrip']), - ([12], ['dvdr']), - ] - http_time_between_calls = 1 #seconds cat_backup_id = None - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): + + url = self.urls['search'] % self.buildUrl(media, quality) - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality['identifier'])[0]) data = self.getHTMLData(url) if data: @@ -56,7 +46,7 @@ class TorrentLeech(TorrentProvider): results.append({ 'id': link['href'].replace('/torrent/', ''), - 'name': unicode(link.string), + 'name': six.text_type(link.string), 'url': self.urls['download'] % url['href'], 'detail_url': self.urls['download'] % details['href'], 'size': self.parseSize(result.find_all('td')[4].string), diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py index 03795ffb..50e76029 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py @@ -1,8 +1,3 @@ -from .main import TorrentPotato - - -def start(): - return TorrentPotato() config = [{ 'name': 'torrentpotato', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py index cfd0339c..b86ca6be 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py @@ -1,45 +1,37 @@ -from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import ResultList -from couchpotato.core.providers.torrent.base import TorrentProvider from urlparse import urlparse import re import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class TorrentPotato(TorrentProvider): +class Base(TorrentProvider): urls = {} limits_reached = {} http_time_between_calls = 1 # Seconds - def search(self, movie, quality): + def search(self, media, quality): hosts = self.getHosts() - results = ResultList(self, movie, quality, imdb_results = True) + results = ResultList(self, media, quality, imdb_results = True) for host in hosts: if self.isDisabled(host): continue - self._searchOnHost(host, movie, quality, results) + self._searchOnHost(host, media, quality, results) return results - def _searchOnHost(self, host, movie, quality, results): + def _searchOnHost(self, host, media, quality, results): - arguments = tryUrlencode({ - 'user': host['name'], - 'passkey': host['pass_key'], - 'imdbid': movie['identifier'] - }) - url = '%s?%s' % (host['host'], arguments) - - torrents = self.getJsonData(url, cache_timeout = 1800) + torrents = self.getJsonData(self.buildUrl(media, host), cache_timeout = 1800) if torrents: try: @@ -110,7 +102,7 @@ class TorrentPotato(TorrentProvider): hosts = self.getHosts() for host in hosts: - result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider) + result = super(Base, self).belongsTo(url, host = host['host'], provider = provider) if result: return result diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py index 058236e4..50edbf55 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py @@ -1,8 +1,3 @@ -from .main import TorrentShack - - -def start(): - return TorrentShack() config = [{ 'name': 'torrentshack', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py b/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py index 6c6dc3b8..21776a27 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py @@ -1,15 +1,14 @@ from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider import six log = CPLog(__name__) -class TorrentShack(TorrentProvider): +class Base(TorrentProvider): urls = { 'test': 'https://torrentshack.net/', @@ -20,21 +19,11 @@ class TorrentShack(TorrentProvider): 'download': 'https://torrentshack.net/%s', } - cat_ids = [ - ([970], ['bd50']), - ([300], ['720p', '1080p']), - ([350], ['dvdr']), - ([400], ['brrip', 'dvdrip']), - ] - http_time_between_calls = 1 #seconds - cat_backup_id = 400 - def _searchOnTitle(self, title, movie, quality, results): + def _search(self, media, quality, results): - scene_only = '1' if self.conf('scene_only') else '' - - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), scene_only, self.getCatId(quality['identifier'])[0]) + url = self.urls['search'] % self.buildUrl(media, quality) data = self.getHTMLData(url) if data: @@ -77,3 +66,6 @@ class TorrentShack(TorrentProvider): return 'logout.php' in output.lower() loginCheckSuccess = loginSuccess + + def getSceneOnly(self): + return '1' if self.conf('scene_only') else '' diff --git a/couchpotato/core/media/_base/providers/torrent/yify/__init__.py b/couchpotato/core/media/_base/providers/torrent/yify/__init__.py index 3a359608..9623eb5d 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify/__init__.py +++ b/couchpotato/core/media/_base/providers/torrent/yify/__init__.py @@ -1,8 +1,3 @@ -from .main import Yify - - -def start(): - return Yify() config = [{ 'name': 'yify', diff --git a/couchpotato/core/media/_base/providers/torrent/yify/main.py b/couchpotato/core/media/_base/providers/torrent/yify/main.py index 080e6e36..028554d0 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify/main.py +++ b/couchpotato/core/media/_base/providers/torrent/yify/main.py @@ -1,12 +1,12 @@ from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.torrent.base import TorrentMagnetProvider import traceback +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider log = CPLog(__name__) -class Yify(TorrentMagnetProvider): +class Base(TorrentProvider): urls = { 'test': '%s/api', diff --git a/couchpotato/core/media/_base/providers/userscript/moviesio/main.py b/couchpotato/core/media/_base/providers/userscript/moviesio/main.py deleted file mode 100644 index 5dab6183..00000000 --- a/couchpotato/core/media/_base/providers/userscript/moviesio/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase - - -class MoviesIO(UserscriptBase): - - includes = ['*://movies.io/m/*'] diff --git a/couchpotato/core/media/_base/providers/trailer/__init__.py b/couchpotato/core/media/movie/providers/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/trailer/__init__.py rename to couchpotato/core/media/movie/providers/__init__.py diff --git a/couchpotato/core/media/movie/providers/automation/__init__.py b/couchpotato/core/media/movie/providers/automation/__init__.py new file mode 100644 index 00000000..93f6c10a --- /dev/null +++ b/couchpotato/core/media/movie/providers/automation/__init__.py @@ -0,0 +1,21 @@ +config = [{ + 'name': 'automation_providers', + 'groups': [ + { + 'label': 'Watchlists', + 'description': 'Check watchlists for new movies', + 'type': 'list', + 'name': 'watchlist_providers', + 'tab': 'automation', + 'options': [], + }, + { + 'label': 'Automated', + 'description': 'Uses minimal requirements', + 'type': 'list', + 'name': 'automation_providers', + 'tab': 'automation', + 'options': [], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py new file mode 100644 index 00000000..8c98c10c --- /dev/null +++ b/couchpotato/core/media/movie/providers/automation/base.py @@ -0,0 +1,96 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.automation.base import BaseAutomation +from couchpotato.environment import Env +from couchpotato.core.helpers.variable import splitString +import time + +log = CPLog(__name__) + + +class Automation(BaseAutomation): + + enabled_option = 'automation_enabled' + http_time_between_calls = 2 + + interval = 1800 + last_checked = 0 + + def __init__(self): + addEvent('automation.get_movies', self._getMovies) + + def _getMovies(self): + + if self.isDisabled(): + return + + if not self.canCheck(): + log.debug('Just checked, skipping %s', self.getName()) + return [] + + self.last_checked = time.time() + + return self.getIMDBids() + + def search(self, name, year = None, imdb_only = False): + + prop_name = 'automation.cached.%s.%s' % (name, year) + cached_imdb = Env.prop(prop_name, default = False) + if cached_imdb and imdb_only: + return cached_imdb + + result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True) + + if len(result) > 0: + if imdb_only and result[0].get('imdb'): + Env.prop(prop_name, result[0].get('imdb')) + + return result[0].get('imdb') if imdb_only else result[0] + else: + return None + + def isMinimalMovie(self, movie): + if not movie.get('rating'): + log.info('ignoring %s as no rating is available for.', (movie['original_title'])) + return False + + if movie['rating'] and movie['rating'].get('imdb'): + movie['votes'] = movie['rating']['imdb'][1] + movie['rating'] = movie['rating']['imdb'][0] + + for minimal_type in ['year', 'rating', 'votes']: + type_value = movie.get(minimal_type, 0) + type_min = self.getMinimal(minimal_type) + if type_value < type_min: + log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value)) + return False + + movie_genres = [genre.lower() for genre in movie['genres']] + required_genres = splitString(self.getMinimal('required_genres').lower()) + ignored_genres = splitString(self.getMinimal('ignored_genres').lower()) + + req_match = 0 + for req_set in required_genres: + req = splitString(req_set, '&') + req_match += len(list(set(movie_genres) & set(req))) == len(req) + + if self.getMinimal('required_genres') and req_match == 0: + log.info2('Required genre(s) missing for %s', movie['original_title']) + return False + + for ign_set in ignored_genres: + ign = splitString(ign_set, '&') + if len(list(set(movie_genres) & set(ign))) == len(ign): + log.info2('%s has blacklisted genre(s): %s', (movie['original_title'], ign)) + return False + + return True + + def getMinimal(self, min_type): + return Env.setting(min_type, 'automation') + + def getIMDBids(self): + return [] + + def canCheck(self): + return time.time() > self.last_checked + self.interval diff --git a/couchpotato/core/media/_base/providers/automation/bluray/__init__.py b/couchpotato/core/media/movie/providers/automation/bluray/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/bluray/__init__.py rename to couchpotato/core/media/movie/providers/automation/bluray/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/bluray/main.py b/couchpotato/core/media/movie/providers/automation/bluray/main.py similarity index 97% rename from couchpotato/core/media/_base/providers/automation/bluray/main.py rename to couchpotato/core/media/movie/providers/automation/bluray/main.py index ddd7b8ab..2e8ebff7 100644 --- a/couchpotato/core/media/_base/providers/automation/bluray/main.py +++ b/couchpotato/core/media/movie/providers/automation/bluray/main.py @@ -2,7 +2,7 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/flixster/__init__.py b/couchpotato/core/media/movie/providers/automation/flixster/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/flixster/__init__.py rename to couchpotato/core/media/movie/providers/automation/flixster/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/flixster/main.py b/couchpotato/core/media/movie/providers/automation/flixster/main.py similarity index 94% rename from couchpotato/core/media/_base/providers/automation/flixster/main.py rename to couchpotato/core/media/movie/providers/automation/flixster/main.py index f07ecd6b..cebe89dc 100644 --- a/couchpotato/core/media/_base/providers/automation/flixster/main.py +++ b/couchpotato/core/media/movie/providers/automation/flixster/main.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/goodfilms/__init__.py b/couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/goodfilms/__init__.py rename to couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/goodfilms/main.py b/couchpotato/core/media/movie/providers/automation/goodfilms/main.py similarity index 94% rename from couchpotato/core/media/_base/providers/automation/goodfilms/main.py rename to couchpotato/core/media/movie/providers/automation/goodfilms/main.py index e668a4fb..3ab51e04 100644 --- a/couchpotato/core/media/_base/providers/automation/goodfilms/main.py +++ b/couchpotato/core/media/movie/providers/automation/goodfilms/main.py @@ -1,6 +1,6 @@ from bs4 import BeautifulSoup from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/imdb/__init__.py b/couchpotato/core/media/movie/providers/automation/imdb/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/imdb/__init__.py rename to couchpotato/core/media/movie/providers/automation/imdb/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/imdb/main.py b/couchpotato/core/media/movie/providers/automation/imdb/main.py similarity index 96% rename from couchpotato/core/media/_base/providers/automation/imdb/main.py rename to couchpotato/core/media/movie/providers/automation/imdb/main.py index 6ca81b70..9b25e180 100644 --- a/couchpotato/core/media/_base/providers/automation/imdb/main.py +++ b/couchpotato/core/media/movie/providers/automation/imdb/main.py @@ -7,9 +7,8 @@ from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import getImdb, splitString, tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation - -from couchpotato.core.providers.base import MultiProvider +from couchpotato.core.media._base.providers.base import MultiProvider +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/itunes/__init__.py b/couchpotato/core/media/movie/providers/automation/itunes/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/itunes/__init__.py rename to couchpotato/core/media/movie/providers/automation/itunes/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/itunes/main.py b/couchpotato/core/media/movie/providers/automation/itunes/main.py similarity index 96% rename from couchpotato/core/media/_base/providers/automation/itunes/main.py rename to couchpotato/core/media/movie/providers/automation/itunes/main.py index 086c981d..3257a668 100644 --- a/couchpotato/core/media/_base/providers/automation/itunes/main.py +++ b/couchpotato/core/media/movie/providers/automation/itunes/main.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import md5, splitString, tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation from xml.etree.ElementTree import QName import datetime import traceback diff --git a/couchpotato/core/media/_base/providers/automation/kinepolis/__init__.py b/couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/kinepolis/__init__.py rename to couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/kinepolis/main.py b/couchpotato/core/media/movie/providers/automation/kinepolis/main.py similarity index 89% rename from couchpotato/core/media/_base/providers/automation/kinepolis/main.py rename to couchpotato/core/media/movie/providers/automation/kinepolis/main.py index 4158d488..a1e099e2 100644 --- a/couchpotato/core/media/_base/providers/automation/kinepolis/main.py +++ b/couchpotato/core/media/movie/providers/automation/kinepolis/main.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.rss import RSS from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation import datetime log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/letterboxd/__init__.py b/couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/letterboxd/__init__.py rename to couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/letterboxd/main.py b/couchpotato/core/media/movie/providers/automation/letterboxd/main.py similarity index 94% rename from couchpotato/core/media/_base/providers/automation/letterboxd/main.py rename to couchpotato/core/media/movie/providers/automation/letterboxd/main.py index dbbf53b1..70e574f1 100644 --- a/couchpotato/core/media/_base/providers/automation/letterboxd/main.py +++ b/couchpotato/core/media/movie/providers/automation/letterboxd/main.py @@ -1,7 +1,7 @@ from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt, splitString, removeEmpty from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation import re log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/moviemeter/__init__.py b/couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/moviemeter/__init__.py rename to couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/moviemeter/main.py b/couchpotato/core/media/movie/providers/automation/moviemeter/main.py similarity index 90% rename from couchpotato/core/media/_base/providers/automation/moviemeter/main.py rename to couchpotato/core/media/movie/providers/automation/moviemeter/main.py index dae764bf..890c8bc0 100644 --- a/couchpotato/core/media/_base/providers/automation/moviemeter/main.py +++ b/couchpotato/core/media/movie/providers/automation/moviemeter/main.py @@ -1,7 +1,7 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.rss import RSS from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/movies_io/__init__.py b/couchpotato/core/media/movie/providers/automation/movies_io/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/movies_io/__init__.py rename to couchpotato/core/media/movie/providers/automation/movies_io/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/movies_io/main.py b/couchpotato/core/media/movie/providers/automation/movies_io/main.py similarity index 92% rename from couchpotato/core/media/_base/providers/automation/movies_io/main.py rename to couchpotato/core/media/movie/providers/automation/movies_io/main.py index 0737e2e6..890cbde0 100644 --- a/couchpotato/core/media/_base/providers/automation/movies_io/main.py +++ b/couchpotato/core/media/movie/providers/automation/movies_io/main.py @@ -2,7 +2,7 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/automation/rottentomatoes/__init__.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/rottentomatoes/__init__.py rename to couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py similarity index 95% rename from couchpotato/core/media/_base/providers/automation/rottentomatoes/main.py rename to couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py index c873a8e1..56e0e7c5 100644 --- a/couchpotato/core/media/_base/providers/automation/rottentomatoes/main.py +++ b/couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation from xml.etree.ElementTree import QName import datetime import re diff --git a/couchpotato/core/media/_base/providers/automation/trakt/__init__.py b/couchpotato/core/media/movie/providers/automation/trakt/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/automation/trakt/__init__.py rename to couchpotato/core/media/movie/providers/automation/trakt/__init__.py diff --git a/couchpotato/core/media/_base/providers/automation/trakt/main.py b/couchpotato/core/media/movie/providers/automation/trakt/main.py similarity index 94% rename from couchpotato/core/media/_base/providers/automation/trakt/main.py rename to couchpotato/core/media/movie/providers/automation/trakt/main.py index 0109daf3..881b14a1 100644 --- a/couchpotato/core/media/_base/providers/automation/trakt/main.py +++ b/couchpotato/core/media/movie/providers/automation/trakt/main.py @@ -1,7 +1,7 @@ from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import sha1 from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation +from couchpotato.core.media.movie.providers.automation.base import Automation import base64 log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/base.py b/couchpotato/core/media/movie/providers/base.py new file mode 100644 index 00000000..4e80d5d3 --- /dev/null +++ b/couchpotato/core/media/movie/providers/base.py @@ -0,0 +1,5 @@ +from couchpotato.core.media._base.providers.info.base import BaseInfoProvider + + +class MovieProvider(BaseInfoProvider): + type = 'movie' diff --git a/couchpotato/core/media/movie/providers/info/__init__.py b/couchpotato/core/media/movie/providers/info/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/couchpotato/core/media/_base/providers/info/_modifier/__init__.py b/couchpotato/core/media/movie/providers/info/_modifier/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/info/_modifier/__init__.py rename to couchpotato/core/media/movie/providers/info/_modifier/__init__.py index 9dfab703..3bdf5e0d 100644 --- a/couchpotato/core/media/_base/providers/info/_modifier/__init__.py +++ b/couchpotato/core/media/movie/providers/info/_modifier/__init__.py @@ -1,7 +1,7 @@ from .main import MovieResultModifier - def start(): + return MovieResultModifier() config = [] diff --git a/couchpotato/core/media/_base/providers/info/_modifier/main.py b/couchpotato/core/media/movie/providers/info/_modifier/main.py similarity index 100% rename from couchpotato/core/media/_base/providers/info/_modifier/main.py rename to couchpotato/core/media/movie/providers/info/_modifier/main.py diff --git a/couchpotato/core/media/_base/providers/info/couchpotatoapi/__init__.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/info/couchpotatoapi/__init__.py rename to couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py diff --git a/couchpotato/core/media/_base/providers/info/couchpotatoapi/main.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py similarity index 98% rename from couchpotato/core/media/_base/providers/info/couchpotatoapi/main.py rename to couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py index a07eb135..469866cd 100644 --- a/couchpotato/core/media/_base/providers/info/couchpotatoapi/main.py +++ b/couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py @@ -1,10 +1,12 @@ +import base64 +import time + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.providers.info.base import MovieProvider +from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.environment import Env -import base64 -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/info/omdbapi/__init__.py b/couchpotato/core/media/movie/providers/info/omdbapi/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/info/omdbapi/__init__.py rename to couchpotato/core/media/movie/providers/info/omdbapi/__init__.py diff --git a/couchpotato/core/media/_base/providers/info/omdbapi/main.py b/couchpotato/core/media/movie/providers/info/omdbapi/main.py similarity index 98% rename from couchpotato/core/media/_base/providers/info/omdbapi/main.py rename to couchpotato/core/media/movie/providers/info/omdbapi/main.py index 8f04d3b6..34e7d569 100644 --- a/couchpotato/core/media/_base/providers/info/omdbapi/main.py +++ b/couchpotato/core/media/movie/providers/info/omdbapi/main.py @@ -1,11 +1,13 @@ +import json +import re +import traceback + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString from couchpotato.core.logger import CPLog -from couchpotato.core.providers.info.base import MovieProvider -import json -import re -import traceback +from couchpotato.core.media.movie.providers.base import MovieProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/info/themoviedb/__init__.py b/couchpotato/core/media/movie/providers/info/themoviedb/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/info/themoviedb/__init__.py rename to couchpotato/core/media/movie/providers/info/themoviedb/__init__.py diff --git a/couchpotato/core/media/_base/providers/info/themoviedb/main.py b/couchpotato/core/media/movie/providers/info/themoviedb/main.py similarity index 98% rename from couchpotato/core/media/_base/providers/info/themoviedb/main.py rename to couchpotato/core/media/movie/providers/info/themoviedb/main.py index 1b706b52..36f3a489 100644 --- a/couchpotato/core/media/_base/providers/info/themoviedb/main.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb/main.py @@ -1,10 +1,12 @@ +import traceback + from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.providers.info.base import MovieProvider +from couchpotato.core.media.movie.providers.base import MovieProvider import tmdb3 -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/metadata/__init__.py b/couchpotato/core/media/movie/providers/metadata/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py new file mode 100644 index 00000000..7dacf554 --- /dev/null +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -0,0 +1,109 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import sp +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.metadata.base import MetaDataBase +from couchpotato.environment import Env +import os +import shutil +import traceback + +log = CPLog(__name__) + + +class MovieMetaData(MetaDataBase): + + enabled_option = 'meta_enabled' + + def __init__(self): + addEvent('renamer.after', self.create) + + def create(self, message = None, group = None): + if self.isDisabled(): return + if not group: group = {} + + log.info('Creating %s metadata.', self.getName()) + + # Update library to get latest info + try: + group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = group['media']['identifier'], extended = True, single = True) + except: + log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) + + root_name = self.getRootName(group) + meta_name = os.path.basename(root_name) + root = os.path.dirname(root_name) + + movie_info = group['media'].get('info') + + for file_type in ['nfo', 'thumbnail', 'fanart']: + try: + # Get file path + name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root) + + if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): + + # Get file content + content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group) + if content: + log.debug('Creating %s file: %s', (file_type, name)) + if os.path.isfile(content): + content = sp(content) + name = sp(name) + + shutil.copy2(content, name) + shutil.copyfile(content, name) + + # Try and copy stats seperately + try: shutil.copystat(content, name) + except: pass + else: + self.createFile(name, content) + group['renamed_files'].append(name) + + try: + os.chmod(sp(name), Env.getPermission('file')) + except: + log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) + + except: + log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) + + def getRootName(self, data = None): + if not data: data = {} + return os.path.join(data['destination_dir'], data['filename']) + + def getFanartName(self, name, root): + return + + def getThumbnailName(self, name, root): + return + + def getNfoName(self, name, root): + return + + def getNfo(self, movie_info = None, data = None): + if not data: data = {} + if not movie_info: movie_info = {} + + def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original'): + if not data: data = {} + if not movie_info: movie_info = {} + + # See if it is in current files + files = data['media'].get('files') + if files.get('image_' + wanted_file_type): + if os.path.isfile(files['image_' + wanted_file_type][0]): + return files['image_' + wanted_file_type][0] + + # Download using existing info + try: + images = movie_info['images'][wanted_file_type] + file_path = fireEvent('file.download', url = images[0], single = True) + return file_path + except: + pass + + def getFanart(self, movie_info = None, data = None): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original') diff --git a/couchpotato/core/media/_base/providers/metadata/wmc/__init__.py b/couchpotato/core/media/movie/providers/metadata/wmc/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/metadata/wmc/__init__.py rename to couchpotato/core/media/movie/providers/metadata/wmc/__init__.py diff --git a/couchpotato/core/media/movie/providers/metadata/wmc/main.py b/couchpotato/core/media/movie/providers/metadata/wmc/main.py new file mode 100644 index 00000000..bdb96a44 --- /dev/null +++ b/couchpotato/core/media/movie/providers/metadata/wmc/main.py @@ -0,0 +1,8 @@ +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData +import os + + +class WindowsMediaCenter(MovieMetaData): + + def getThumbnailName(self, name, root): + return os.path.join(root, 'folder.jpg') diff --git a/couchpotato/core/media/_base/providers/metadata/xbmc/__init__.py b/couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/metadata/xbmc/__init__.py rename to couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py diff --git a/couchpotato/core/media/_base/providers/metadata/xbmc/main.py b/couchpotato/core/media/movie/providers/metadata/xbmc/main.py similarity index 98% rename from couchpotato/core/media/_base/providers/metadata/xbmc/main.py rename to couchpotato/core/media/movie/providers/metadata/xbmc/main.py index 90cab8f3..61f36473 100644 --- a/couchpotato/core/media/_base/providers/metadata/xbmc/main.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc/main.py @@ -1,7 +1,7 @@ +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog -from couchpotato.core.providers.metadata.base import MetaDataBase from xml.etree.ElementTree import Element, SubElement, tostring import os import re @@ -10,7 +10,7 @@ import xml.dom.minidom log = CPLog(__name__) -class XBMC(MetaDataBase): +class XBMC(MovieMetaData): def getFanartName(self, name, root): return self.createMetaName(self.conf('meta_fanart_name'), name, root) diff --git a/couchpotato/core/media/movie/providers/nzb/__init__.py b/couchpotato/core/media/movie/providers/nzb/__init__.py new file mode 100644 index 00000000..88d9865d --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/__init__.py @@ -0,0 +1,14 @@ +config = [{ + 'name': 'nzb_providers', + 'groups': [ + { + 'label': 'Usenet Providers', + 'description': 'Providers searching usenet for new releases', + 'wizard': True, + 'type': 'list', + 'name': 'nzb_providers', + 'tab': 'searcher', + 'options': [], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/nzb/base.py b/couchpotato/core/media/movie/providers/nzb/base.py new file mode 100644 index 00000000..e69de29b diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py b/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py new file mode 100644 index 00000000..13a4bfb9 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py @@ -0,0 +1,7 @@ +from .main import BinSearch + + +def start(): + return BinSearch() + +config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch/main.py b/couchpotato/core/media/movie/providers/nzb/binsearch/main.py new file mode 100644 index 00000000..9b4cf042 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/binsearch/main.py @@ -0,0 +1,24 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.nzb.binsearch.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider +from couchpotato.environment import Env + +log = CPLog(__name__) + + +class BinSearch(MovieProvider, Base): + + def buildUrl(self, media, quality): + query = tryUrlencode({ + 'q': media['identifier'], + 'm': 'n', + 'max': 400, + 'adv_age': Env.setting('retention', 'nzb'), + 'adv_sort': 'date', + 'adv_col': 'on', + 'adv_nfo': 'on', + 'minsize': quality.get('size_min'), + 'maxsize': quality.get('size_max'), + }) + return query diff --git a/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py b/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py new file mode 100644 index 00000000..2b25ce28 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py @@ -0,0 +1,7 @@ +from .main import Newznab + + +def start(): + return Newznab() + +config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/newznab/main.py b/couchpotato/core/media/movie/providers/nzb/newznab/main.py new file mode 100644 index 00000000..b67e4a66 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/newznab/main.py @@ -0,0 +1,18 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.nzb.newznab.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class Newznab(MovieProvider, Base): + + def buildUrl(self, media, api_key): + query = tryUrlencode({ + 't': 'movie', + 'imdbid': media['identifier'].replace('tt', ''), + 'apikey': api_key, + 'extended': 1 + }) + return query diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py b/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py new file mode 100644 index 00000000..31df9653 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py @@ -0,0 +1,7 @@ +from .main import NZBClub + + +def start(): + return NZBClub() + +config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub/main.py b/couchpotato/core/media/movie/providers/nzb/nzbclub/main.py new file mode 100644 index 00000000..d90f7457 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/nzbclub/main.py @@ -0,0 +1,25 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.nzb.nzbclub.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class NZBClub(MovieProvider, Base): + + def buildUrl(self, media): + + q = tryUrlencode({ + 'q': '"%s"' % fireEvent('library.query', media, single = True), + }) + + query = tryUrlencode({ + 'ig': 1, + 'rpp': 200, + 'st': 5, + 'sp': 1, + 'ns': 1, + }) + return '%s&%s' % (q, query) diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py b/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py new file mode 100644 index 00000000..acf9e915 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py @@ -0,0 +1,7 @@ +from .main import NzbIndex + + +def start(): + return NzbIndex() + +config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex/main.py b/couchpotato/core/media/movie/providers/nzb/nzbindex/main.py new file mode 100644 index 00000000..5f98d58d --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/nzbindex/main.py @@ -0,0 +1,28 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.nzb.nzbindex.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider +from couchpotato.environment import Env + +log = CPLog(__name__) + + +class NzbIndex(MovieProvider, Base): + + def buildUrl(self, media, quality): + title = fireEvent('library.query', media, include_year = False, single = True) + year = media['year'] + + query = tryUrlencode({ + 'q': '"%s %s" | "%s (%s)"' % (title, year, title, year), + 'age': Env.setting('retention', 'nzb'), + 'sort': 'agedesc', + 'minsize': quality.get('size_min'), + 'maxsize': quality.get('size_max'), + 'rating': 1, + 'max': 250, + 'more': 1, + 'complete': 1, + }) + return query diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py new file mode 100644 index 00000000..086db2c7 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py @@ -0,0 +1,7 @@ +from .main import OMGWTFNZBs + + +def start(): + return OMGWTFNZBs() + +config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py new file mode 100644 index 00000000..f0f96907 --- /dev/null +++ b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.nzb.omgwtfnzbs.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class OMGWTFNZBs(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/__init__.py b/couchpotato/core/media/movie/providers/torrent/__init__.py new file mode 100644 index 00000000..12dda708 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/__init__.py @@ -0,0 +1,14 @@ +config = [{ + 'name': 'torrent_providers', + 'groups': [ + { + 'label': 'Torrent Providers', + 'description': 'Providers searching torrent sites for new releases', + 'wizard': True, + 'type': 'list', + 'name': 'torrent_providers', + 'tab': 'searcher', + 'options': [], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py b/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py new file mode 100644 index 00000000..7cc37a20 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py @@ -0,0 +1,8 @@ +from .main import AwesomeHD + + +def start(): + return AwesomeHD() + +config = [] + diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd/main.py b/couchpotato/core/media/movie/providers/torrent/awesomehd/main.py new file mode 100644 index 00000000..55f23d94 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/awesomehd/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.awesomehd.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class AwesomeHD(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py b/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py new file mode 100644 index 00000000..0145dcde --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py @@ -0,0 +1,7 @@ +from .main import BiTHDTV + + +def start(): + return BiTHDTV() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv/main.py b/couchpotato/core/media/movie/providers/torrent/bithdtv/main.py new file mode 100644 index 00000000..03be850b --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv/main.py @@ -0,0 +1,17 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.bithdtv.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class BiTHDTV(MovieProvider, Base): + + def buildUrl(self, media): + query = tryUrlencode({ + 'search': fireEvent('library.query', media, single = True), + 'cat': 7 # Movie cat + }) + return query diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py b/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py new file mode 100644 index 00000000..8be80c8f --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py @@ -0,0 +1,7 @@ +from .main import Bitsoup + + +def start(): + return Bitsoup() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup/main.py b/couchpotato/core/media/movie/providers/torrent/bitsoup/main.py new file mode 100644 index 00000000..b67ae229 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup/main.py @@ -0,0 +1,26 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.bitsoup.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class Bitsoup(MovieProvider, Base): + cat_ids = [ + ([41], ['720p', '1080p']), + ([20], ['dvdr']), + ([19], ['brrip', 'dvdrip']), + ] + cat_backup_id = 0 + + def buildUrl(self, media, quality): + query = tryUrlencode({ + 'search': '"%s" %s' % ( + fireEvent('library.query', media, include_year = False, single = True), + media['year'] + ), + 'cat': self.getCatId(quality['identifier'])[0], + }) + return query diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py b/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py new file mode 100644 index 00000000..1c7b4357 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py @@ -0,0 +1,7 @@ +from .main import HDBits + + +def start(): + return HDBits() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits/main.py b/couchpotato/core/media/movie/providers/torrent/hdbits/main.py new file mode 100644 index 00000000..2a1e23f6 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/hdbits/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.hdbits.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class HDBits(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py new file mode 100644 index 00000000..f20367de --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py @@ -0,0 +1,7 @@ +from .main import ILoveTorrents + + +def start(): + return ILoveTorrents() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py new file mode 100644 index 00000000..5e90e73d --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.ilovetorrents.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class ILoveTorrents(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py new file mode 100644 index 00000000..8741a132 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py @@ -0,0 +1,7 @@ +from .main import IPTorrents + + +def start(): + return IPTorrents() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents/main.py b/couchpotato/core/media/movie/providers/torrent/iptorrents/main.py new file mode 100644 index 00000000..21a66422 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents/main.py @@ -0,0 +1,20 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.iptorrents.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class IPTorrents(MovieProvider, Base): + + cat_ids = [ + ([48], ['720p', '1080p', 'bd50']), + ([72], ['cam', 'ts', 'tc', 'r5', 'scr']), + ([7], ['dvdrip', 'brrip']), + ([6], ['dvdr']), + ] + + def buildUrl(self, title, media, quality): + query = '%s %s' % (title.replace(':', ''), media['year']) + + return self._buildUrl(query, quality['identifier']) diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py new file mode 100644 index 00000000..cb293876 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py @@ -0,0 +1,7 @@ +from .main import KickAssTorrents + + +def start(): + return KickAssTorrents() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py new file mode 100644 index 00000000..d9adeccd --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.kickasstorrents.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class KickAssTorrents(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py new file mode 100644 index 00000000..c437737c --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py @@ -0,0 +1,7 @@ +from .main import PassThePopcorn + + +def start(): + return PassThePopcorn() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py new file mode 100644 index 00000000..6c90c14d --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py @@ -0,0 +1,36 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.passthepopcorn.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class PassThePopcorn(MovieProvider, Base): + + quality_search_params = { + 'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, + '1080p': {'resolution': '1080p'}, + '720p': {'resolution': '720p'}, + 'brrip': {'media': 'Blu-ray'}, + 'dvdr': {'resolution': 'anysd'}, + 'dvdrip': {'media': 'DVD'}, + 'scr': {'media': 'DVD-Screener'}, + 'r5': {'media': 'R5'}, + 'tc': {'media': 'TC'}, + 'ts': {'media': 'TS'}, + 'cam': {'media': 'CAM'} + } + + post_search_filters = { + 'bd50': {'Codec': ['BD50']}, + '1080p': {'Resolution': ['1080p']}, + '720p': {'Resolution': ['720p']}, + 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, + 'dvdr': {'Codec': ['DVD5', 'DVD9']}, + 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, + 'scr': {'Source': ['DVD-Screener']}, + 'r5': {'Source': ['R5']}, + 'tc': {'Source': ['TC']}, + 'ts': {'Source': ['TS']}, + 'cam': {'Source': ['CAM']} + } diff --git a/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py b/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py new file mode 100644 index 00000000..c58d2b53 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py @@ -0,0 +1,7 @@ +from .main import PublicHD + + +def start(): + return PublicHD() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/publichd/main.py b/couchpotato/core/media/movie/providers/torrent/publichd/main.py new file mode 100644 index 00000000..5c3cd946 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/publichd/main.py @@ -0,0 +1,12 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.publichd.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class PublicHD(MovieProvider, Base): + + def buildUrl(self, media): + return fireEvent('library.query', media, single = True) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py new file mode 100644 index 00000000..14932221 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py @@ -0,0 +1,7 @@ +from .main import SceneAccess + + +def start(): + return SceneAccess() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py new file mode 100644 index 00000000..7ed756e2 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py @@ -0,0 +1,30 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.event import fireEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.sceneaccess.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class SceneAccess(MovieProvider, Base): + + cat_ids = [ + ([22], ['720p', '1080p']), + ([7], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), + ([8], ['dvdr']), + ] + + def buildUrl(self, media, quality): + url = self.urls['search'] % ( + self.getCatId(quality['identifier'])[0], + self.getCatId(quality['identifier'])[0] + ) + + arguments = tryUrlencode({ + 'search': fireEvent('library.query', media, single = True), + 'method': 3, + }) + query = "%s&%s" % (url, arguments) + + return query diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py new file mode 100644 index 00000000..3bd44a4e --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py @@ -0,0 +1,7 @@ +from .main import ThePirateBay + + +def start(): + return ThePirateBay() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py new file mode 100644 index 00000000..138f311d --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py @@ -0,0 +1,24 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.thepiratebay.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class ThePirateBay(MovieProvider, Base): + + cat_ids = [ + ([207], ['720p', '1080p']), + ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + ([201, 207], ['brrip']), + ([202], ['dvdr']) + ] + + def buildUrl(self, media, page, cats): + return ( + tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)), + page, + ','.join(str(x) for x in cats) + ) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py new file mode 100644 index 00000000..39bef365 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py @@ -0,0 +1,7 @@ +from .main import TorrentBytes + + +def start(): + return TorrentBytes() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py new file mode 100644 index 00000000..d0131875 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.torrentbytes.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class TorrentBytes(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py new file mode 100644 index 00000000..0ebf09e0 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py @@ -0,0 +1,7 @@ +from .main import TorrentDay + + +def start(): + return TorrentDay() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday/main.py b/couchpotato/core/media/movie/providers/torrent/torrentday/main.py new file mode 100644 index 00000000..566b0380 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentday/main.py @@ -0,0 +1,18 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.torrentday.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class TorrentDay(MovieProvider, Base): + + cat_ids = [ + ([11], ['720p', '1080p']), + ([1, 21, 25], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), + ([3], ['dvdr']), + ([5], ['bd50']), + ] + def buildUrl(self, media): + return fireEvent('library.query', media, single = True) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py new file mode 100644 index 00000000..afb8f1e6 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py @@ -0,0 +1,7 @@ +from .main import TorrentLeech + + +def start(): + return TorrentLeech() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech/main.py b/couchpotato/core/media/movie/providers/torrent/torrentleech/main.py new file mode 100644 index 00000000..3b6b858d --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech/main.py @@ -0,0 +1,25 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.torrentleech.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class TorrentLeech(MovieProvider, Base): + + cat_ids = [ + ([13], ['720p', '1080p']), + ([8], ['cam']), + ([9], ['ts', 'tc']), + ([10], ['r5', 'scr']), + ([11], ['dvdrip']), + ([14], ['brrip']), + ([12], ['dvdr']), + ] + + def buildUrl(self, media, quality): + return ( + tryUrlencode(fireEvent('library.query', media, single = True)), + self.getCatId(quality['identifier'])[0] + ) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py new file mode 100644 index 00000000..58b9d7b2 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py @@ -0,0 +1,7 @@ +from .main import TorrentPotato + + +def start(): + return TorrentPotato() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py new file mode 100644 index 00000000..a2ddae88 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py @@ -0,0 +1,17 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.torrentpotato.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class TorrentPotato(MovieProvider, Base): + + def buildUrl(self, media, host): + arguments = tryUrlencode({ + 'user': host['name'], + 'passkey': host['pass_key'], + 'imdbid': media['identifier'] + }) + return '%s?%s' % (host['host'], arguments) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py new file mode 100644 index 00000000..7448a47c --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py @@ -0,0 +1,7 @@ +from .main import TorrentShack + + +def start(): + return TorrentShack() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack/main.py b/couchpotato/core/media/movie/providers/torrent/torrentshack/main.py new file mode 100644 index 00000000..9e01475b --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack/main.py @@ -0,0 +1,34 @@ +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.torrentshack.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class TorrentShack(MovieProvider, Base): + + # TorrentShack movie search categories + # Movies/x264 - 300 + # Movies/DVD-R - 350 + # Movies/XviD - 400 + # Full Blu-ray - 970 + # + # REMUX - 320 (not included) + # Movies-HD Pack - 982 (not included) + # Movies-SD Pack - 983 (not included) + + cat_ids = [ + ([970], ['bd50']), + ([300], ['720p', '1080p']), + ([350], ['dvdr']), + ([400], ['brrip', 'dvdrip']), + ] + cat_backup_id = 400 + + def buildUrl(self, media, quality): + query = (tryUrlencode(fireEvent('library.query', media, single = True)), + self.getCatId(quality['identifier'])[0], + self.getSceneOnly()) + return query diff --git a/couchpotato/core/media/movie/providers/torrent/yify/__init__.py b/couchpotato/core/media/movie/providers/torrent/yify/__init__.py new file mode 100644 index 00000000..ac9baa2d --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/yify/__init__.py @@ -0,0 +1,7 @@ +from .main import Yify + + +def start(): + return Yify() + +config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/yify/main.py b/couchpotato/core/media/movie/providers/torrent/yify/main.py new file mode 100644 index 00000000..24ee8ea7 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/yify/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.yify.main import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + + +class Yify(MovieProvider, Base): + pass diff --git a/couchpotato/core/media/movie/providers/trailer/__init__.py b/couchpotato/core/media/movie/providers/trailer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/couchpotato/core/media/_base/providers/trailer/base.py b/couchpotato/core/media/movie/providers/trailer/base.py similarity index 78% rename from couchpotato/core/media/_base/providers/trailer/base.py rename to couchpotato/core/media/movie/providers/trailer/base.py index 338ca9b3..c1a6f71d 100644 --- a/couchpotato/core/media/_base/providers/trailer/base.py +++ b/couchpotato/core/media/movie/providers/trailer/base.py @@ -1,6 +1,6 @@ from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import Provider +from couchpotato.core.media._base.providers.base import Provider log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/trailer/hdtrailers/__init__.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py similarity index 100% rename from couchpotato/core/media/_base/providers/trailer/hdtrailers/__init__.py rename to couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py diff --git a/couchpotato/core/media/_base/providers/trailer/hdtrailers/main.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py similarity index 98% rename from couchpotato/core/media/_base/providers/trailer/hdtrailers/main.py rename to couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py index 6af83ac7..ea2fe95d 100644 --- a/couchpotato/core/media/_base/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py @@ -2,10 +2,10 @@ from bs4 import SoupStrainer, BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import mergeDicts, getTitle from couchpotato.core.logger import CPLog -from couchpotato.core.providers.trailer.base import TrailerProvider from string import digits, ascii_letters from urllib2 import HTTPError import re +from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/userscript/__init__.py b/couchpotato/core/media/movie/providers/userscript/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/couchpotato/core/media/_base/providers/userscript/allocine/__init__.py b/couchpotato/core/media/movie/providers/userscript/allocine/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/allocine/__init__.py rename to couchpotato/core/media/movie/providers/userscript/allocine/__init__.py index cb2ba992..d272a7f9 100644 --- a/couchpotato/core/media/_base/providers/userscript/allocine/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/allocine/__init__.py @@ -3,5 +3,3 @@ from .main import AlloCine def start(): return AlloCine() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/allocine/main.py b/couchpotato/core/media/movie/providers/userscript/allocine/main.py similarity index 90% rename from couchpotato/core/media/_base/providers/userscript/allocine/main.py rename to couchpotato/core/media/movie/providers/userscript/allocine/main.py index f8ca630d..c3400ee4 100644 --- a/couchpotato/core/media/_base/providers/userscript/allocine/main.py +++ b/couchpotato/core/media/movie/providers/userscript/allocine/main.py @@ -1,6 +1,6 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.providers.userscript.base import UserscriptBase import traceback +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/userscript/appletrailers/__init__.py b/couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py similarity index 85% rename from couchpotato/core/media/_base/providers/userscript/appletrailers/__init__.py rename to couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py index 075217a8..28eedbbd 100644 --- a/couchpotato/core/media/_base/providers/userscript/appletrailers/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py @@ -3,5 +3,3 @@ from .main import AppleTrailers def start(): return AppleTrailers() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/appletrailers/main.py b/couchpotato/core/media/movie/providers/userscript/appletrailers/main.py similarity index 85% rename from couchpotato/core/media/_base/providers/userscript/appletrailers/main.py rename to couchpotato/core/media/movie/providers/userscript/appletrailers/main.py index 693065d1..41b72c4d 100644 --- a/couchpotato/core/media/_base/providers/userscript/appletrailers/main.py +++ b/couchpotato/core/media/movie/providers/userscript/appletrailers/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re diff --git a/couchpotato/core/media/_base/providers/userscript/criticker/__init__.py b/couchpotato/core/media/movie/providers/userscript/criticker/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/criticker/__init__.py rename to couchpotato/core/media/movie/providers/userscript/criticker/__init__.py index ae24aa1e..550855ea 100644 --- a/couchpotato/core/media/_base/providers/userscript/criticker/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/criticker/__init__.py @@ -3,5 +3,3 @@ from .main import Criticker def start(): return Criticker() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/criticker/main.py b/couchpotato/core/media/movie/providers/userscript/criticker/main.py similarity index 51% rename from couchpotato/core/media/_base/providers/userscript/criticker/main.py rename to couchpotato/core/media/movie/providers/userscript/criticker/main.py index 680d9a36..7ab63c98 100644 --- a/couchpotato/core/media/_base/providers/userscript/criticker/main.py +++ b/couchpotato/core/media/movie/providers/userscript/criticker/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class Criticker(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/filmweb/__init__.py b/couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py similarity index 82% rename from couchpotato/core/media/_base/providers/userscript/filmweb/__init__.py rename to couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py index 3098610c..38354b45 100644 --- a/couchpotato/core/media/_base/providers/userscript/filmweb/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py @@ -3,5 +3,3 @@ from .main import Filmweb def start(): return Filmweb() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/filmweb/main.py b/couchpotato/core/media/movie/providers/userscript/filmweb/main.py similarity index 89% rename from couchpotato/core/media/_base/providers/userscript/filmweb/main.py rename to couchpotato/core/media/movie/providers/userscript/filmweb/main.py index 66c5fcdb..18e5d303 100644 --- a/couchpotato/core/media/_base/providers/userscript/filmweb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/filmweb/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re diff --git a/couchpotato/core/media/_base/providers/userscript/flickchart/__init__.py b/couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/flickchart/__init__.py rename to couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py index 18a88ffe..5ff48e54 100644 --- a/couchpotato/core/media/_base/providers/userscript/flickchart/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py @@ -3,5 +3,3 @@ from .main import Flickchart def start(): return Flickchart() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/flickchart/main.py b/couchpotato/core/media/movie/providers/userscript/flickchart/main.py similarity index 90% rename from couchpotato/core/media/_base/providers/userscript/flickchart/main.py rename to couchpotato/core/media/movie/providers/userscript/flickchart/main.py index a66bd38f..6c2837bf 100644 --- a/couchpotato/core/media/_base/providers/userscript/flickchart/main.py +++ b/couchpotato/core/media/movie/providers/userscript/flickchart/main.py @@ -1,6 +1,6 @@ from couchpotato.core.event import fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import traceback log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/userscript/imdb/__init__.py b/couchpotato/core/media/movie/providers/userscript/imdb/__init__.py similarity index 81% rename from couchpotato/core/media/_base/providers/userscript/imdb/__init__.py rename to couchpotato/core/media/movie/providers/userscript/imdb/__init__.py index c25319b7..e310cc31 100644 --- a/couchpotato/core/media/_base/providers/userscript/imdb/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/imdb/__init__.py @@ -3,5 +3,3 @@ from .main import IMDB def start(): return IMDB() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/imdb/main.py b/couchpotato/core/media/movie/providers/userscript/imdb/main.py similarity index 76% rename from couchpotato/core/media/_base/providers/userscript/imdb/main.py rename to couchpotato/core/media/movie/providers/userscript/imdb/main.py index 2a6efd6b..14363933 100644 --- a/couchpotato/core/media/_base/providers/userscript/imdb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/imdb/main.py @@ -1,6 +1,6 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import getImdb -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class IMDB(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/letterboxd/__init__.py b/couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/letterboxd/__init__.py rename to couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py index 2fd89000..78ef77b4 100644 --- a/couchpotato/core/media/_base/providers/userscript/letterboxd/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py @@ -3,5 +3,3 @@ from .main import Letterboxd def start(): return Letterboxd() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/letterboxd/main.py b/couchpotato/core/media/movie/providers/userscript/letterboxd/main.py similarity index 50% rename from couchpotato/core/media/_base/providers/userscript/letterboxd/main.py rename to couchpotato/core/media/movie/providers/userscript/letterboxd/main.py index c0d91d79..852c1b79 100644 --- a/couchpotato/core/media/_base/providers/userscript/letterboxd/main.py +++ b/couchpotato/core/media/movie/providers/userscript/letterboxd/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class Letterboxd(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/moviemeter/__init__.py b/couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/moviemeter/__init__.py rename to couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py index 7a05a75a..aae8ca71 100644 --- a/couchpotato/core/media/_base/providers/userscript/moviemeter/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py @@ -3,5 +3,3 @@ from .main import MovieMeter def start(): return MovieMeter() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/moviemeter/main.py b/couchpotato/core/media/movie/providers/userscript/moviemeter/main.py similarity index 58% rename from couchpotato/core/media/_base/providers/userscript/moviemeter/main.py rename to couchpotato/core/media/movie/providers/userscript/moviemeter/main.py index 3593d432..f1b254fc 100644 --- a/couchpotato/core/media/_base/providers/userscript/moviemeter/main.py +++ b/couchpotato/core/media/movie/providers/userscript/moviemeter/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class MovieMeter(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/moviesio/__init__.py b/couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/moviesio/__init__.py rename to couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py index e29e8d08..3f0497fc 100644 --- a/couchpotato/core/media/_base/providers/userscript/moviesio/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py @@ -3,5 +3,3 @@ from .main import MoviesIO def start(): return MoviesIO() - -config = [] diff --git a/couchpotato/core/media/movie/providers/userscript/moviesio/main.py b/couchpotato/core/media/movie/providers/userscript/moviesio/main.py new file mode 100644 index 00000000..a466baf9 --- /dev/null +++ b/couchpotato/core/media/movie/providers/userscript/moviesio/main.py @@ -0,0 +1,6 @@ +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + + +class MoviesIO(UserscriptBase): + + includes = ['*://movies.io/m/*'] diff --git a/couchpotato/core/media/_base/providers/userscript/reddit/__init__.py b/couchpotato/core/media/movie/providers/userscript/reddit/__init__.py similarity index 82% rename from couchpotato/core/media/_base/providers/userscript/reddit/__init__.py rename to couchpotato/core/media/movie/providers/userscript/reddit/__init__.py index a74bbf0d..d803bb54 100644 --- a/couchpotato/core/media/_base/providers/userscript/reddit/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/reddit/__init__.py @@ -3,5 +3,3 @@ from .main import Reddit def start(): return Reddit() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/reddit/main.py b/couchpotato/core/media/movie/providers/userscript/reddit/main.py similarity index 85% rename from couchpotato/core/media/_base/providers/userscript/reddit/main.py rename to couchpotato/core/media/movie/providers/userscript/reddit/main.py index 9790f6e2..ec59e69a 100644 --- a/couchpotato/core/media/_base/providers/userscript/reddit/main.py +++ b/couchpotato/core/media/movie/providers/userscript/reddit/main.py @@ -1,6 +1,6 @@ from couchpotato import fireEvent from couchpotato.core.helpers.variable import splitString -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class Reddit(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/rottentomatoes/__init__.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py similarity index 85% rename from couchpotato/core/media/_base/providers/userscript/rottentomatoes/__init__.py rename to couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py index 363f103e..b041d5a1 100644 --- a/couchpotato/core/media/_base/providers/userscript/rottentomatoes/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py @@ -3,5 +3,3 @@ from .main import RottenTomatoes def start(): return RottenTomatoes() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py similarity index 92% rename from couchpotato/core/media/_base/providers/userscript/rottentomatoes/main.py rename to couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py index 0b16a441..0b3145e0 100644 --- a/couchpotato/core/media/_base/providers/userscript/rottentomatoes/main.py +++ b/couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re import traceback diff --git a/couchpotato/core/media/_base/providers/userscript/sharethe/__init__.py b/couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/sharethe/__init__.py rename to couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py index 3cf393af..cf553da7 100644 --- a/couchpotato/core/media/_base/providers/userscript/sharethe/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py @@ -3,5 +3,3 @@ from .main import ShareThe def start(): return ShareThe() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/sharethe/main.py b/couchpotato/core/media/movie/providers/userscript/sharethe/main.py similarity index 58% rename from couchpotato/core/media/_base/providers/userscript/sharethe/main.py rename to couchpotato/core/media/movie/providers/userscript/sharethe/main.py index d22b67eb..ce11229a 100644 --- a/couchpotato/core/media/_base/providers/userscript/sharethe/main.py +++ b/couchpotato/core/media/movie/providers/userscript/sharethe/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class ShareThe(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/tmdb/__init__.py b/couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py similarity index 81% rename from couchpotato/core/media/_base/providers/userscript/tmdb/__init__.py rename to couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py index c77330c3..609f364f 100644 --- a/couchpotato/core/media/_base/providers/userscript/tmdb/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py @@ -3,5 +3,3 @@ from .main import TMDB def start(): return TMDB() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/tmdb/main.py b/couchpotato/core/media/movie/providers/userscript/tmdb/main.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/tmdb/main.py rename to couchpotato/core/media/movie/providers/userscript/tmdb/main.py index b718fc3b..9638bea4 100644 --- a/couchpotato/core/media/_base/providers/userscript/tmdb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/tmdb/main.py @@ -1,5 +1,5 @@ from couchpotato.core.event import fireEvent -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re diff --git a/couchpotato/core/media/_base/providers/userscript/trakt/__init__.py b/couchpotato/core/media/movie/providers/userscript/trakt/__init__.py similarity index 81% rename from couchpotato/core/media/_base/providers/userscript/trakt/__init__.py rename to couchpotato/core/media/movie/providers/userscript/trakt/__init__.py index 39c17c32..ddba4408 100644 --- a/couchpotato/core/media/_base/providers/userscript/trakt/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/trakt/__init__.py @@ -3,5 +3,3 @@ from .main import Trakt def start(): return Trakt() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/trakt/main.py b/couchpotato/core/media/movie/providers/userscript/trakt/main.py similarity index 68% rename from couchpotato/core/media/_base/providers/userscript/trakt/main.py rename to couchpotato/core/media/movie/providers/userscript/trakt/main.py index 43e06dee..17cfdddc 100644 --- a/couchpotato/core/media/_base/providers/userscript/trakt/main.py +++ b/couchpotato/core/media/movie/providers/userscript/trakt/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class Trakt(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/whiwa/__init__.py b/couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py similarity index 81% rename from couchpotato/core/media/_base/providers/userscript/whiwa/__init__.py rename to couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py index c8fd3c9d..3b14f477 100644 --- a/couchpotato/core/media/_base/providers/userscript/whiwa/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py @@ -3,5 +3,3 @@ from .main import WHiWA def start(): return WHiWA() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/whiwa/main.py b/couchpotato/core/media/movie/providers/userscript/whiwa/main.py similarity index 50% rename from couchpotato/core/media/_base/providers/userscript/whiwa/main.py rename to couchpotato/core/media/movie/providers/userscript/whiwa/main.py index 40ffa2a9..e0a01200 100644 --- a/couchpotato/core/media/_base/providers/userscript/whiwa/main.py +++ b/couchpotato/core/media/movie/providers/userscript/whiwa/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase class WHiWA(UserscriptBase): diff --git a/couchpotato/core/media/_base/providers/userscript/youteather/__init__.py b/couchpotato/core/media/movie/providers/userscript/youteather/__init__.py similarity index 83% rename from couchpotato/core/media/_base/providers/userscript/youteather/__init__.py rename to couchpotato/core/media/movie/providers/userscript/youteather/__init__.py index f31e911e..8eb44c79 100644 --- a/couchpotato/core/media/_base/providers/userscript/youteather/__init__.py +++ b/couchpotato/core/media/movie/providers/userscript/youteather/__init__.py @@ -3,5 +3,3 @@ from .main import YouTheater def start(): return YouTheater() - -config = [] diff --git a/couchpotato/core/media/_base/providers/userscript/youteather/main.py b/couchpotato/core/media/movie/providers/userscript/youteather/main.py similarity index 85% rename from couchpotato/core/media/_base/providers/userscript/youteather/main.py rename to couchpotato/core/media/movie/providers/userscript/youteather/main.py index 3efd3686..34c97688 100644 --- a/couchpotato/core/media/_base/providers/userscript/youteather/main.py +++ b/couchpotato/core/media/movie/providers/userscript/youteather/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.providers.userscript.base import UserscriptBase +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re class YouTheater(UserscriptBase): diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 63d2075e..0bfb337c 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -1,7 +1,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import Provider +from couchpotato.core.media._base.providers.base import Provider from couchpotato.environment import Env log = CPLog(__name__) diff --git a/libs/importlib/__init__.py b/libs/importlib/__init__.py deleted file mode 100644 index ad31a1ac..00000000 --- a/libs/importlib/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Backport of importlib.import_module from 3.x.""" -# While not critical (and in no way guaranteed!), it would be nice to keep this -# code compatible with Python 2.3. -import sys - -def _resolve_name(name, package, level): - """Return the absolute name of the module to be imported.""" - if not hasattr(package, 'rindex'): - raise ValueError("'package' not set to a string") - dot = len(package) - for x in xrange(level, 1, -1): - try: - dot = package.rindex('.', 0, dot) - except ValueError: - raise ValueError("attempted relative import beyond top-level " - "package") - return "%s.%s" % (package[:dot], name) - - -def import_module(name, package=None): - """Import a module. - - The 'package' argument is required when performing a relative import. It - specifies the package to use as the anchor point from which to resolve the - relative import to an absolute import. - - """ - if name.startswith('.'): - if not package: - raise TypeError("relative imports require the 'package' argument") - level = 0 - for character in name: - if character != '.': - break - level += 1 - name = _resolve_name(name[level:], package, level) - __import__(name) - return sys.modules[name] From c20f64685f84abaecb06794e681dc5706b65d066 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 22:15:27 +0100 Subject: [PATCH 074/468] Autoload from single file --- couchpotato/core/loader.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 5a528b13..1b4fc5fa 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -93,7 +93,12 @@ class Loader(object): self.addModule(priority, plugin_type, module, os.path.basename(dir_name)) for name in os.listdir(dir_name): - if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')): + path = os.path.join(dir_name, name) + ext = os.path.splitext(path)[1] + ext_length = len(ext) + if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py'))) + or (os.path.isfile(path) and ext == '.py')): + name = name[:-ext_length] if ext_length > 0 else name module_name = '%s.%s' % (module, name) self.addModule(priority, plugin_type, module_name, name) @@ -118,11 +123,16 @@ class Loader(object): def loadPlugins(self, module, name): - if not hasattr(module, 'start'): + if not hasattr(module, 'autoload'): log.debug('Skip startup for plugin %s as it has no start section' % module.__file__) return False try: - module.start() + # Load single file plugin + if isinstance(module.autoload, (str, unicode)): + getattr(module, module.autoload)() + # Load folder plugin + else: + module.autoload() return True except: log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc())) From 917e8136078ab2738379e35439d34454c0c7f9d8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 22:15:58 +0100 Subject: [PATCH 075/468] Move _base to single file --- .../core/_base/{_core/main.py => _core.py} | 98 +++++++++++++++++ couchpotato/core/_base/_core/__init__.py | 101 ------------------ .../{clientscript/main.py => clientscript.py} | 2 + .../core/_base/clientscript/__init__.py | 7 -- .../_base/{desktop/main.py => desktop.py} | 3 + couchpotato/core/_base/desktop/__init__.py | 7 -- .../_base/{scheduler/main.py => scheduler.py} | 2 + couchpotato/core/_base/scheduler/__init__.py | 7 -- couchpotato/core/_base/updater/__init__.py | 2 +- 9 files changed, 106 insertions(+), 123 deletions(-) rename couchpotato/core/_base/{_core/main.py => _core.py} (63%) delete mode 100644 couchpotato/core/_base/_core/__init__.py rename couchpotato/core/_base/{clientscript/main.py => clientscript.py} (99%) delete mode 100644 couchpotato/core/_base/clientscript/__init__.py rename couchpotato/core/_base/{desktop/main.py => desktop.py} (97%) delete mode 100644 couchpotato/core/_base/desktop/__init__.py rename couchpotato/core/_base/{scheduler/main.py => scheduler.py} (99%) delete mode 100644 couchpotato/core/_base/scheduler/__init__.py diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core.py similarity index 63% rename from couchpotato/core/_base/_core/main.py rename to couchpotato/core/_base/_core.py index 02e21f2d..ee266512 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core.py @@ -15,6 +15,8 @@ import webbrowser log = CPLog(__name__) +autoload = 'Core' + class Core(Plugin): @@ -186,3 +188,99 @@ class Core(Plugin): signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) + + +config = [{ + 'name': 'core', + 'order': 1, + 'groups': [ + { + 'tab': 'general', + 'name': 'basics', + 'description': 'Needs restart before changes take effect.', + 'wizard': True, + 'options': [ + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'port', + 'default': 5050, + 'type': 'int', + 'description': 'The port I should listen to.', + }, + { + 'name': 'ssl_cert', + 'description': 'Path to SSL server.crt', + 'advanced': True, + }, + { + 'name': 'ssl_key', + 'description': 'Path to SSL server.key', + 'advanced': True, + }, + { + 'name': 'launch_browser', + 'default': True, + 'type': 'bool', + 'description': 'Launch the browser when I start.', + 'wizard': True, + }, + ], + }, + { + 'tab': 'general', + 'name': 'advanced', + 'description': "For those who know what they're doing", + 'advanced': True, + 'options': [ + { + 'name': 'api_key', + 'default': uuid4().hex, + 'readonly': 1, + 'description': 'Let 3rd party app do stuff. Docs', + }, + { + 'name': 'debug', + 'default': 0, + 'type': 'bool', + 'description': 'Enable debugging.', + }, + { + 'name': 'development', + 'default': 0, + 'type': 'bool', + 'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.', + }, + { + 'name': 'data_dir', + 'type': 'directory', + 'description': 'Where cache/logs/etc are stored. Keep empty for defaults.', + }, + { + 'name': 'url_base', + 'default': '', + 'description': 'When using mod_proxy use this to append the url with this.', + }, + { + 'name': 'permission_folder', + 'default': '0755', + 'label': 'Folder CHMOD', + 'description': 'Can be either decimal (493) or octal (leading zero: 0755)', + }, + { + 'name': 'permission_file', + 'default': '0755', + 'label': 'File CHMOD', + 'description': 'Same as Folder CHMOD but for files', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py deleted file mode 100644 index 58965bbb..00000000 --- a/couchpotato/core/_base/_core/__init__.py +++ /dev/null @@ -1,101 +0,0 @@ -from .main import Core -from uuid import uuid4 - - -def start(): - return Core() - -config = [{ - 'name': 'core', - 'order': 1, - 'groups': [ - { - 'tab': 'general', - 'name': 'basics', - 'description': 'Needs restart before changes take effect.', - 'wizard': True, - 'options': [ - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'port', - 'default': 5050, - 'type': 'int', - 'description': 'The port I should listen to.', - }, - { - 'name': 'ssl_cert', - 'description': 'Path to SSL server.crt', - 'advanced': True, - }, - { - 'name': 'ssl_key', - 'description': 'Path to SSL server.key', - 'advanced': True, - }, - { - 'name': 'launch_browser', - 'default': True, - 'type': 'bool', - 'description': 'Launch the browser when I start.', - 'wizard': True, - }, - ], - }, - { - 'tab': 'general', - 'name': 'advanced', - 'description': "For those who know what they're doing", - 'advanced': True, - 'options': [ - { - 'name': 'api_key', - 'default': uuid4().hex, - 'readonly': 1, - 'description': 'Let 3rd party app do stuff. Docs', - }, - { - 'name': 'debug', - 'default': 0, - 'type': 'bool', - 'description': 'Enable debugging.', - }, - { - 'name': 'development', - 'default': 0, - 'type': 'bool', - 'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.', - }, - { - 'name': 'data_dir', - 'type': 'directory', - 'description': 'Where cache/logs/etc are stored. Keep empty for defaults.', - }, - { - 'name': 'url_base', - 'default': '', - 'description': 'When using mod_proxy use this to append the url with this.', - }, - { - 'name': 'permission_folder', - 'default': '0755', - 'label': 'Folder CHMOD', - 'description': 'Can be either decimal (493) or octal (leading zero: 0755)', - }, - { - 'name': 'permission_file', - 'default': '0755', - 'label': 'File CHMOD', - 'description': 'Same as Folder CHMOD but for files', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript.py similarity index 99% rename from couchpotato/core/_base/clientscript/main.py rename to couchpotato/core/_base/clientscript.py index c1be7e73..42ad28af 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript.py @@ -13,6 +13,8 @@ import traceback log = CPLog(__name__) +autoload = 'ClientScript' + class ClientScript(Plugin): diff --git a/couchpotato/core/_base/clientscript/__init__.py b/couchpotato/core/_base/clientscript/__init__.py deleted file mode 100644 index 8070044e..00000000 --- a/couchpotato/core/_base/clientscript/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import ClientScript - - -def start(): - return ClientScript() - -config = [] diff --git a/couchpotato/core/_base/desktop/main.py b/couchpotato/core/_base/desktop.py similarity index 97% rename from couchpotato/core/_base/desktop/main.py rename to couchpotato/core/_base/desktop.py index c3beff17..9a365636 100644 --- a/couchpotato/core/_base/desktop/main.py +++ b/couchpotato/core/_base/desktop.py @@ -5,6 +5,9 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'Desktop' + + if Env.get('desktop'): class Desktop(Plugin): diff --git a/couchpotato/core/_base/desktop/__init__.py b/couchpotato/core/_base/desktop/__init__.py deleted file mode 100644 index e59ca523..00000000 --- a/couchpotato/core/_base/desktop/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Desktop - - -def start(): - return Desktop() - -config = [] diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler.py similarity index 99% rename from couchpotato/core/_base/scheduler/main.py rename to couchpotato/core/_base/scheduler.py index 3d835f94..ae5bfd3b 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler.py @@ -5,6 +5,8 @@ from couchpotato.core.plugins.base import Plugin log = CPLog(__name__) +autoload = 'Scheduler' + class Scheduler(Plugin): diff --git a/couchpotato/core/_base/scheduler/__init__.py b/couchpotato/core/_base/scheduler/__init__.py deleted file mode 100644 index abfc2305..00000000 --- a/couchpotato/core/_base/scheduler/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Scheduler - - -def start(): - return Scheduler() - -config = [] diff --git a/couchpotato/core/_base/updater/__init__.py b/couchpotato/core/_base/updater/__init__.py index 7ad30d27..a62eafb5 100644 --- a/couchpotato/core/_base/updater/__init__.py +++ b/couchpotato/core/_base/updater/__init__.py @@ -3,7 +3,7 @@ from couchpotato.environment import Env import os -def start(): +def autoload(): return Updater() config = [{ From 2a60c5248321c7b6f5d7fb47371ca67ffcc1679e Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 22:28:56 +0100 Subject: [PATCH 076/468] Move downloaders to single file --- .../{blackhole/main.py => blackhole.py} | 53 ++++++++++ .../core/downloaders/blackhole/__init__.py | 56 ---------- .../downloaders/{deluge/main.py => deluge.py} | 89 ++++++++++++++++ .../core/downloaders/deluge/__init__.py | 91 ---------------- .../downloaders/{nzbget/main.py => nzbget.py} | 75 +++++++++++++ .../core/downloaders/nzbget/__init__.py | 77 -------------- .../{nzbvortex/main.py => nzbvortex.py} | 55 ++++++++++ .../core/downloaders/nzbvortex/__init__.py | 57 ---------- .../{pneumatic/main.py => pneumatic.py} | 36 +++++++ .../core/downloaders/pneumatic/__init__.py | 38 ------- .../{rtorrent/main.py => rtorrent.py} | 91 ++++++++++++++++ .../core/downloaders/rtorrent/__init__.py | 100 ------------------ .../{sabnzbd/main.py => sabnzbd.py} | 76 +++++++++++++ .../core/downloaders/sabnzbd/__init__.py | 79 -------------- .../{synology/main.py => synology.py} | 51 +++++++++ .../core/downloaders/synology/__init__.py | 53 ---------- .../{transmission/main.py => transmission.py} | 92 ++++++++++++++++ .../core/downloaders/transmission/__init__.py | 95 ----------------- .../{utorrent/main.py => utorrent.py} | 78 ++++++++++++++ .../core/downloaders/utorrent/__init__.py | 80 -------------- 20 files changed, 696 insertions(+), 726 deletions(-) rename couchpotato/core/downloaders/{blackhole/main.py => blackhole.py} (67%) delete mode 100644 couchpotato/core/downloaders/blackhole/__init__.py rename couchpotato/core/downloaders/{deluge/main.py => deluge.py} (77%) delete mode 100644 couchpotato/core/downloaders/deluge/__init__.py rename couchpotato/core/downloaders/{nzbget/main.py => nzbget.py} (75%) delete mode 100644 couchpotato/core/downloaders/nzbget/__init__.py rename couchpotato/core/downloaders/{nzbvortex/main.py => nzbvortex.py} (76%) delete mode 100644 couchpotato/core/downloaders/nzbvortex/__init__.py rename couchpotato/core/downloaders/{pneumatic/main.py => pneumatic.py} (69%) delete mode 100644 couchpotato/core/downloaders/pneumatic/__init__.py rename couchpotato/core/downloaders/{rtorrent/main.py => rtorrent.py} (74%) mode change 100755 => 100644 delete mode 100755 couchpotato/core/downloaders/rtorrent/__init__.py rename couchpotato/core/downloaders/{sabnzbd/main.py => sabnzbd.py} (71%) delete mode 100644 couchpotato/core/downloaders/sabnzbd/__init__.py rename couchpotato/core/downloaders/{synology/main.py => synology.py} (78%) delete mode 100644 couchpotato/core/downloaders/synology/__init__.py rename couchpotato/core/downloaders/{transmission/main.py => transmission.py} (76%) delete mode 100644 couchpotato/core/downloaders/transmission/__init__.py rename couchpotato/core/downloaders/{utorrent/main.py => utorrent.py} (83%) delete mode 100644 couchpotato/core/downloaders/utorrent/__init__.py diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole.py similarity index 67% rename from couchpotato/core/downloaders/blackhole/main.py rename to couchpotato/core/downloaders/blackhole.py index 9a018354..e58db5af 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole.py @@ -8,6 +8,8 @@ import traceback log = CPLog(__name__) +autoload = 'Blackhole' + class Blackhole(Downloader): @@ -100,3 +102,54 @@ class Blackhole(Downloader): return super(Blackhole, self).isEnabled(manual, data) and \ ((self.conf('use_for') in for_protocol)) + + +config = [{ + 'name': 'blackhole', + 'order': 30, + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'blackhole', + 'label': 'Black hole', + 'description': 'Download the NZB/Torrent to a specific folder. Note: Seeding and copying/linking features do not work with Black hole.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': True, + 'type': 'enabler', + 'radio_group': 'nzb,torrent', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Directory where the .nzb (or .torrent) file is saved to.', + 'default': getDownloadDir() + }, + { + 'name': 'use_for', + 'label': 'Use for', + 'default': 'both', + 'type': 'dropdown', + 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')], + }, + { + 'name': 'create_subdir', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Create a sub directory when saving the .nzb (or .torrent).', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py deleted file mode 100644 index 92d18e7f..00000000 --- a/couchpotato/core/downloaders/blackhole/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -from .main import Blackhole -from couchpotato.core.helpers.variable import getDownloadDir - - -def start(): - return Blackhole() - -config = [{ - 'name': 'blackhole', - 'order': 30, - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'blackhole', - 'label': 'Black hole', - 'description': 'Download the NZB/Torrent to a specific folder. Note: Seeding and copying/linking features do not work with Black hole.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': True, - 'type': 'enabler', - 'radio_group': 'nzb,torrent', - }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Directory where the .nzb (or .torrent) file is saved to.', - 'default': getDownloadDir() - }, - { - 'name': 'use_for', - 'label': 'Use for', - 'default': 'both', - 'type': 'dropdown', - 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')], - }, - { - 'name': 'create_subdir', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Create a sub directory when saving the .nzb (or .torrent).', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge.py similarity index 77% rename from couchpotato/core/downloaders/deluge/main.py rename to couchpotato/core/downloaders/deluge.py index 59300958..65e4aa92 100644 --- a/couchpotato/core/downloaders/deluge/main.py +++ b/couchpotato/core/downloaders/deluge.py @@ -13,6 +13,8 @@ import traceback log = CPLog(__name__) +autoload = 'Deluge' + class Deluge(Downloader): @@ -291,3 +293,90 @@ class DelugeRPC(object): return torrent_hash return False + + +config = [{ + 'name': 'deluge', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'deluge', + 'label': 'Deluge', + 'description': 'Use Deluge to download torrents.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'localhost:58846', + 'description': 'Hostname with port. Usually localhost:58846', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Download to this directory. Keep empty for default Deluge download directory.', + }, + { + 'name': 'completed_directory', + 'type': 'directory', + 'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.', + 'advanced': True, + }, + { + 'name': 'label', + 'description': 'Label to add to torrents in the Deluge UI.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'type': 'bool', + 'default': True, + 'advanced': True, + 'description': 'Remove the torrent from Deluge after it has finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/deluge/__init__.py b/couchpotato/core/downloaders/deluge/__init__.py deleted file mode 100644 index 09fae751..00000000 --- a/couchpotato/core/downloaders/deluge/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -from .main import Deluge - - -def start(): - return Deluge() - -config = [{ - 'name': 'deluge', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'deluge', - 'label': 'Deluge', - 'description': 'Use Deluge to download torrents.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'torrent', - }, - { - 'name': 'host', - 'default': 'localhost:58846', - 'description': 'Hostname with port. Usually localhost:58846', - }, - { - 'name': 'username', - }, - { - 'name': 'password', - 'type': 'password', - }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Download to this directory. Keep empty for default Deluge download directory.', - }, - { - 'name': 'completed_directory', - 'type': 'directory', - 'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.', - 'advanced': True, - }, - { - 'name': 'label', - 'description': 'Label to add to torrents in the Deluge UI.', - }, - { - 'name': 'remove_complete', - 'label': 'Remove torrent', - 'type': 'bool', - 'default': True, - 'advanced': True, - 'description': 'Remove the torrent from Deluge after it has finished seeding.', - }, - { - 'name': 'delete_files', - 'label': 'Remove files', - 'default': True, - 'type': 'bool', - 'advanced': True, - 'description': 'Also remove the leftover files.', - }, - { - 'name': 'paused', - 'type': 'bool', - 'advanced': True, - 'default': False, - 'description': 'Add the torrent paused.', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget.py similarity index 75% rename from couchpotato/core/downloaders/nzbget/main.py rename to couchpotato/core/downloaders/nzbget.py index 3dad8670..743e2768 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget.py @@ -12,6 +12,8 @@ import xmlrpclib log = CPLog(__name__) +autoload = 'NZBGet' + class NZBGet(Downloader): @@ -214,3 +216,76 @@ class NZBGet(Downloader): def getRPC(self): url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc return xmlrpclib.ServerProxy(url) + + +config = [{ + 'name': 'nzbget', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'nzbget', + 'label': 'NZBGet', + 'description': 'Use NZBGet to download NZBs.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'nzb', + }, + { + 'name': 'host', + 'default': 'localhost:6789', + 'description': 'Hostname with port. Usually localhost:6789', + }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, + { + 'name': 'username', + 'default': 'nzbget', + 'advanced': True, + 'description': 'Set a different username to connect. Default: nzbget', + }, + { + 'name': 'password', + 'type': 'password', + 'description': 'Default NZBGet password is tegbzn6789', + }, + { + 'name': 'category', + 'default': 'Movies', + 'description': 'The category CP places the nzb in. Like movies or couchpotato', + }, + { + 'name': 'priority', + 'advanced': True, + 'default': '0', + 'type': 'dropdown', + 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], + 'description': 'Only change this if you are using NZBget 9.0 or higher', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py deleted file mode 100644 index 551eb42c..00000000 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -from .main import NZBGet - - -def start(): - return NZBGet() - -config = [{ - 'name': 'nzbget', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'nzbget', - 'label': 'NZBGet', - 'description': 'Use NZBGet to download NZBs.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'nzb', - }, - { - 'name': 'host', - 'default': 'localhost:6789', - 'description': 'Hostname with port. Usually localhost:6789', - }, - { - 'name': 'ssl', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Use HyperText Transfer Protocol Secure, or https', - }, - { - 'name': 'username', - 'default': 'nzbget', - 'advanced': True, - 'description': 'Set a different username to connect. Default: nzbget', - }, - { - 'name': 'password', - 'type': 'password', - 'description': 'Default NZBGet password is tegbzn6789', - }, - { - 'name': 'category', - 'default': 'Movies', - 'description': 'The category CP places the nzb in. Like movies or couchpotato', - }, - { - 'name': 'priority', - 'advanced': True, - 'default': '0', - 'type': 'dropdown', - 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], - 'description': 'Only change this if you are using NZBget 9.0 or higher', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex.py similarity index 76% rename from couchpotato/core/downloaders/nzbvortex/main.py rename to couchpotato/core/downloaders/nzbvortex.py index d1525c89..1084e668 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex.py @@ -18,6 +18,8 @@ import urllib2 log = CPLog(__name__) +autoload = 'NZBVortex' + class NZBVortex(Downloader): @@ -186,3 +188,56 @@ class HTTPSConnection(httplib.HTTPSConnection): class HTTPSHandler(urllib2.HTTPSHandler): def https_open(self, req): return self.do_open(HTTPSConnection, req) + + +config = [{ + 'name': 'nzbvortex', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'nzbvortex', + 'label': 'NZBVortex', + 'description': 'Use NZBVortex to download NZBs.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'nzb', + }, + { + 'name': 'host', + 'default': 'localhost:4321', + 'description': 'Hostname with port. Usually localhost:4321', + }, + { + 'name': 'ssl', + 'default': 1, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, + { + 'name': 'api_key', + 'label': 'Api Key', + }, + { + 'name': 'manual', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py deleted file mode 100644 index 1c2d699e..00000000 --- a/couchpotato/core/downloaders/nzbvortex/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from .main import NZBVortex - - -def start(): - return NZBVortex() - -config = [{ - 'name': 'nzbvortex', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'nzbvortex', - 'label': 'NZBVortex', - 'description': 'Use NZBVortex to download NZBs.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'nzb', - }, - { - 'name': 'host', - 'default': 'localhost:4321', - 'description': 'Hostname with port. Usually localhost:4321', - }, - { - 'name': 'ssl', - 'default': 1, - 'type': 'bool', - 'advanced': True, - 'description': 'Use HyperText Transfer Protocol Secure, or https', - }, - { - 'name': 'api_key', - 'label': 'Api Key', - }, - { - 'name': 'manual', - 'default': False, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic.py similarity index 69% rename from couchpotato/core/downloaders/pneumatic/main.py rename to couchpotato/core/downloaders/pneumatic.py index bc1f6d04..823cd388 100644 --- a/couchpotato/core/downloaders/pneumatic/main.py +++ b/couchpotato/core/downloaders/pneumatic.py @@ -7,6 +7,8 @@ import traceback log = CPLog(__name__) +autoload = 'Pneumatic' + class Pneumatic(Downloader): @@ -71,3 +73,37 @@ class Pneumatic(Downloader): return True return False + + +config = [{ + 'name': 'pneumatic', + 'order': 30, + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'pneumatic', + 'label': 'Pneumatic', + 'description': 'Use Pneumatic to download .strm files.', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Directory where the .strm file is saved to.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/pneumatic/__init__.py b/couchpotato/core/downloaders/pneumatic/__init__.py deleted file mode 100644 index 698643fb..00000000 --- a/couchpotato/core/downloaders/pneumatic/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from .main import Pneumatic - - -def start(): - return Pneumatic() - -config = [{ - 'name': 'pneumatic', - 'order': 30, - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'pneumatic', - 'label': 'Pneumatic', - 'description': 'Use Pneumatic to download .strm files.', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Directory where the .strm file is saved to.', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent.py old mode 100755 new mode 100644 similarity index 74% rename from couchpotato/core/downloaders/rtorrent/main.py rename to couchpotato/core/downloaders/rtorrent.py index 79ab9482..e2936ca5 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent.py @@ -15,6 +15,8 @@ from scandir import scandir log = CPLog(__name__) +autoload = 'rTorrent' + class rTorrent(Downloader): @@ -291,3 +293,92 @@ class rTorrent(Downloader): torrent.erase() # just removes the torrent, doesn't delete data return True + + +config = [{ + 'name': 'rtorrent', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'rtorrent', + 'label': 'rTorrent', + 'description': '', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'localhost:80', + 'description': 'RPC Communication URI. Usually scgi://localhost:5000, ' + 'httprpc://localhost/rutorrent or localhost:80' + }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, + { + 'name': 'rpc_url', + 'type': 'string', + 'default': 'RPC2', + 'advanced': True, + 'description': 'Change if your RPC mount is at a different path.', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'label', + 'description': 'Label to apply on added torrents.', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Download to this directory. Keep empty for default rTorrent download directory.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': False, + 'advanced': True, + 'type': 'bool', + 'description': 'Remove the torrent after it finishes seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py deleted file mode 100755 index f793cad1..00000000 --- a/couchpotato/core/downloaders/rtorrent/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -from .main import rTorrent - - -def start(): - return rTorrent() - -config = [{ - 'name': 'rtorrent', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'rtorrent', - 'label': 'rTorrent', - 'description': '', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'torrent', - }, -# @RuudBurger: How do I migrate this? -# { -# 'name': 'url', -# 'default': 'http://localhost:80/RPC2', -# 'description': 'XML-RPC Endpoint URI. Usually scgi://localhost:5000 ' -# 'or http://localhost:80/RPC2' -# }, - { - 'name': 'host', - 'default': 'localhost:80', - 'description': 'RPC Communication URI. Usually scgi://localhost:5000, ' - 'httprpc://localhost/rutorrent or localhost:80' - }, - { - 'name': 'ssl', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Use HyperText Transfer Protocol Secure, or https', - }, - { - 'name': 'rpc_url', - 'type': 'string', - 'default': 'RPC2', - 'advanced': True, - 'description': 'Change if your RPC mount is at a different path.', - }, - { - 'name': 'username', - }, - { - 'name': 'password', - 'type': 'password', - }, - { - 'name': 'label', - 'description': 'Label to apply on added torrents.', - }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Download to this directory. Keep empty for default rTorrent download directory.', - }, - { - 'name': 'remove_complete', - 'label': 'Remove torrent', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'description': 'Remove the torrent after it finishes seeding.', - }, - { - 'name': 'delete_files', - 'label': 'Remove files', - 'default': True, - 'type': 'bool', - 'advanced': True, - 'description': 'Also remove the leftover files.', - }, - { - 'name': 'paused', - 'type': 'bool', - 'advanced': True, - 'default': False, - 'description': 'Add the torrent paused.', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd.py similarity index 71% rename from couchpotato/core/downloaders/sabnzbd/main.py rename to couchpotato/core/downloaders/sabnzbd.py index ba58c090..33e51fa7 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -11,6 +11,8 @@ import traceback log = CPLog(__name__) +autoload = 'Sabnzbd' + class Sabnzbd(Downloader): @@ -201,3 +203,77 @@ class Sabnzbd(Downloader): else: return data + +config = [{ + 'name': 'sabnzbd', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'sabnzbd', + 'label': 'Sabnzbd', + 'description': 'Use SABnzbd (0.7+) to download NZBs.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'nzb', + }, + { + 'name': 'host', + 'default': 'localhost:8080', + }, + { + 'name': 'ssl', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use HyperText Transfer Protocol Secure, or https', + }, + { + 'name': 'api_key', + 'label': 'Api Key', + 'description': 'Used for all calls to Sabnzbd.', + }, + { + 'name': 'category', + 'label': 'Category', + 'description': 'The category CP places the nzb in. Like movies or couchpotato', + }, + { + 'name': 'priority', + 'label': 'Priority', + 'type': 'dropdown', + 'default': '0', + 'advanced': True, + 'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)], + 'description': 'Add to the queue with this priority.', + }, + { + 'name': 'manual', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'remove_complete', + 'advanced': True, + 'label': 'Remove NZB', + 'default': False, + 'type': 'bool', + 'description': 'Remove the NZB from history after it completed.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py deleted file mode 100644 index 2990078a..00000000 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -from .main import Sabnzbd - - -def start(): - return Sabnzbd() - -config = [{ - 'name': 'sabnzbd', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'sabnzbd', - 'label': 'Sabnzbd', - 'description': 'Use SABnzbd (0.7+) to download NZBs.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'nzb', - }, - { - 'name': 'host', - 'default': 'localhost:8080', - }, - { - 'name': 'ssl', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Use HyperText Transfer Protocol Secure, or https', - }, - { - 'name': 'api_key', - 'label': 'Api Key', - 'description': 'Used for all calls to Sabnzbd.', - }, - { - 'name': 'category', - 'label': 'Category', - 'description': 'The category CP places the nzb in. Like movies or couchpotato', - }, - { - 'name': 'priority', - 'label': 'Priority', - 'type': 'dropdown', - 'default': '0', - 'advanced': True, - 'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)], - 'description': 'Add to the queue with this priority.', - }, - { - 'name': 'manual', - 'default': False, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'remove_complete', - 'advanced': True, - 'label': 'Remove NZB', - 'default': False, - 'type': 'bool', - 'description': 'Remove the NZB from history after it completed.', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology.py similarity index 78% rename from couchpotato/core/downloaders/synology/main.py rename to couchpotato/core/downloaders/synology.py index 7e5b6098..edc732a2 100644 --- a/couchpotato/core/downloaders/synology/main.py +++ b/couchpotato/core/downloaders/synology.py @@ -8,6 +8,8 @@ import traceback log = CPLog(__name__) +autoload = 'Synology' + class Synology(Downloader): @@ -160,3 +162,52 @@ class SynologyRPC(object): def test(self): return bool(self._login()) + + +config = [{ + 'name': 'synology', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'synology', + 'label': 'Synology', + 'description': 'Use Synology Download Station to download.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'nzb,torrent', + }, + { + 'name': 'host', + 'default': 'localhost:5000', + 'description': 'Hostname with port. Usually localhost:5000', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'use_for', + 'label': 'Use for', + 'default': 'both', + 'type': 'dropdown', + 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')], + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/synology/__init__.py b/couchpotato/core/downloaders/synology/__init__.py deleted file mode 100644 index d0c57c2f..00000000 --- a/couchpotato/core/downloaders/synology/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from .main import Synology - - -def start(): - return Synology() - -config = [{ - 'name': 'synology', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'synology', - 'label': 'Synology', - 'description': 'Use Synology Download Station to download.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'nzb,torrent', - }, - { - 'name': 'host', - 'default': 'localhost:5000', - 'description': 'Hostname with port. Usually localhost:5000', - }, - { - 'name': 'username', - }, - { - 'name': 'password', - 'type': 'password', - }, - { - 'name': 'use_for', - 'label': 'Use for', - 'default': 'both', - 'type': 'dropdown', - 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')], - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission.py similarity index 76% rename from couchpotato/core/downloaders/transmission/main.py rename to couchpotato/core/downloaders/transmission.py index 4c42bf0f..a9567805 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission.py @@ -12,6 +12,8 @@ import urllib2 log = CPLog(__name__) +autoload = 'Transmission' + class Transmission(Downloader): @@ -251,3 +253,93 @@ class TransmissionRPC(object): post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag} return self._request(post_data) + +config = [{ + 'name': 'transmission', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'transmission', + 'label': 'Transmission', + 'description': 'Use Transmission to download torrents.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'localhost:9091', + 'description': 'Hostname with port. Usually localhost:9091', + }, + { + 'name': 'rpc_url', + 'type': 'string', + 'default': 'transmission', + 'advanced': True, + 'description': 'Change if you don\'t run Transmission RPC at the default url.', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Download to this directory. Keep empty for default Transmission download directory.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Remove the torrent from Transmission after it finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'stalled_as_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Consider a stalled torrent as failed', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py deleted file mode 100644 index 4c9b4aad..00000000 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -from .main import Transmission - - -def start(): - return Transmission() - -config = [{ - 'name': 'transmission', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'transmission', - 'label': 'Transmission', - 'description': 'Use Transmission to download torrents.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'torrent', - }, - { - 'name': 'host', - 'default': 'localhost:9091', - 'description': 'Hostname with port. Usually localhost:9091', - }, - { - 'name': 'rpc_url', - 'type': 'string', - 'default': 'transmission', - 'advanced': True, - 'description': 'Change if you don\'t run Transmission RPC at the default url.', - }, - { - 'name': 'username', - }, - { - 'name': 'password', - 'type': 'password', - }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Download to this directory. Keep empty for default Transmission download directory.', - }, - { - 'name': 'remove_complete', - 'label': 'Remove torrent', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Remove the torrent from Transmission after it finished seeding.', - }, - { - 'name': 'delete_files', - 'label': 'Remove files', - 'default': True, - 'type': 'bool', - 'advanced': True, - 'description': 'Also remove the leftover files.', - }, - { - 'name': 'paused', - 'type': 'bool', - 'advanced': True, - 'default': False, - 'description': 'Add the torrent paused.', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'stalled_as_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Consider a stalled torrent as failed', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent.py similarity index 83% rename from couchpotato/core/downloaders/utorrent/main.py rename to couchpotato/core/downloaders/utorrent.py index 6a5e4257..345b17e5 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -19,6 +19,8 @@ import urllib2 log = CPLog(__name__) +autoload = 'uTorrent' + class uTorrent(Downloader): @@ -340,3 +342,79 @@ class uTorrentAPI(object): return False response = json.loads(data) return int(response.get('build')) + + +config = [{ + 'name': 'utorrent', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'utorrent', + 'label': 'uTorrent', + 'description': 'Use uTorrent (3.0+) to download torrents.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'localhost:8000', + 'description': 'Port can be found in settings when enabling WebUI.', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'label', + 'description': 'Label to add torrent as.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Remove the torrent from uTorrent after it finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py deleted file mode 100644 index da160956..00000000 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -from .main import uTorrent - - -def start(): - return uTorrent() - -config = [{ - 'name': 'utorrent', - 'groups': [ - { - 'tab': 'downloaders', - 'list': 'download_providers', - 'name': 'utorrent', - 'label': 'uTorrent', - 'description': 'Use uTorrent (3.0+) to download torrents.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - 'radio_group': 'torrent', - }, - { - 'name': 'host', - 'default': 'localhost:8000', - 'description': 'Port can be found in settings when enabling WebUI.', - }, - { - 'name': 'username', - }, - { - 'name': 'password', - 'type': 'password', - }, - { - 'name': 'label', - 'description': 'Label to add torrent as.', - }, - { - 'name': 'remove_complete', - 'label': 'Remove torrent', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Remove the torrent from uTorrent after it finished seeding.', - }, - { - 'name': 'delete_files', - 'label': 'Remove files', - 'default': True, - 'type': 'bool', - 'advanced': True, - 'description': 'Also remove the leftover files.', - }, - { - 'name': 'paused', - 'type': 'bool', - 'advanced': True, - 'default': False, - 'description': 'Add the torrent paused.', - }, - { - 'name': 'manual', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', - }, - { - 'name': 'delete_failed', - 'default': True, - 'advanced': True, - 'type': 'bool', - 'description': 'Delete a release after the download has failed.', - }, - ], - } - ], -}] From 8492c9b2141d243ab1d02129fb9823325f7d9078 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 22:31:47 +0100 Subject: [PATCH 077/468] Move notifications to single file --- .../{boxcar/main.py => boxcar.py} | 30 ++++++++ .../core/notifications/boxcar/__init__.py | 34 ---------- .../{boxcar2/main.py => boxcar2.py} | 32 +++++++++ .../core/notifications/boxcar2/__init__.py | 34 ---------- .../core/notifications/core/__init__.py | 4 +- .../notifications/{email/main.py => email.py} | 64 +++++++++++++++++ .../core/notifications/email/__init__.py | 68 ------------------- .../notifications/{growl/main.py => growl.py} | 41 +++++++++++ .../core/notifications/growl/__init__.py | 46 ------------- .../notifications/{nmj/main.py => nmj.py} | 28 ++++++++ .../core/notifications/nmj/__init__.py | 34 ---------- .../__init__.py => notifymyandroid.py} | 37 +++++++++- .../notifications/notifymyandroid/main.py | 35 ---------- .../{notifymywp/__init__.py => notifymywp.py} | 27 +++++++- .../core/notifications/notifymywp/main.py | 25 ------- .../core/notifications/plex/__init__.py | 2 +- .../{prowl/__init__.py => prowl.py} | 37 +++++++++- couchpotato/core/notifications/prowl/main.py | 35 ---------- .../{pushalot/__init__.py => pushalot.py} | 40 ++++++++++- .../core/notifications/pushalot/main.py | 38 ----------- .../{pushbullet/main.py => pushbullet.py} | 36 ++++++++++ .../core/notifications/pushbullet/__init__.py | 40 ----------- .../{pushover/main.py => pushover.py} | 38 +++++++++++ .../core/notifications/pushover/__init__.py | 40 ----------- .../{synoindex/main.py => synoindex.py} | 22 ++++++ .../core/notifications/synoindex/__init__.py | 24 ------- .../{toasty/main.py => toasty.py} | 32 +++++++++ .../core/notifications/toasty/__init__.py | 34 ---------- .../notifications/{trakt/main.py => trakt.py} | 29 ++++++++ .../core/notifications/trakt/__init__.py | 31 --------- .../core/notifications/twitter/__init__.py | 2 +- .../notifications/{xbmc/main.py => xbmc.py} | 65 ++++++++++++++++++ .../core/notifications/xbmc/__init__.py | 68 ------------------- .../{xmpp/__init__.py => xmpp.py} | 47 ++++++++++++- couchpotato/core/notifications/xmpp/main.py | 43 ------------ 35 files changed, 593 insertions(+), 649 deletions(-) rename couchpotato/core/notifications/{boxcar/main.py => boxcar.py} (57%) delete mode 100644 couchpotato/core/notifications/boxcar/__init__.py rename couchpotato/core/notifications/{boxcar2/main.py => boxcar2.py} (59%) delete mode 100644 couchpotato/core/notifications/boxcar2/__init__.py rename couchpotato/core/notifications/{email/main.py => email.py} (57%) delete mode 100644 couchpotato/core/notifications/email/__init__.py rename couchpotato/core/notifications/{growl/main.py => growl.py} (62%) delete mode 100644 couchpotato/core/notifications/growl/__init__.py rename couchpotato/core/notifications/{nmj/main.py => nmj.py} (84%) delete mode 100644 couchpotato/core/notifications/nmj/__init__.py rename couchpotato/core/notifications/{notifymyandroid/__init__.py => notifymyandroid.py} (54%) delete mode 100644 couchpotato/core/notifications/notifymyandroid/main.py rename couchpotato/core/notifications/{notifymywp/__init__.py => notifymywp.py} (58%) delete mode 100644 couchpotato/core/notifications/notifymywp/main.py rename couchpotato/core/notifications/{prowl/__init__.py => prowl.py} (50%) delete mode 100644 couchpotato/core/notifications/prowl/main.py rename couchpotato/core/notifications/{pushalot/__init__.py => pushalot.py} (55%) delete mode 100644 couchpotato/core/notifications/pushalot/main.py rename couchpotato/core/notifications/{pushbullet/main.py => pushbullet.py} (66%) delete mode 100644 couchpotato/core/notifications/pushbullet/__init__.py rename couchpotato/core/notifications/{pushover/main.py => pushover.py} (60%) delete mode 100644 couchpotato/core/notifications/pushover/__init__.py rename couchpotato/core/notifications/{synoindex/main.py => synoindex.py} (69%) delete mode 100644 couchpotato/core/notifications/synoindex/__init__.py rename couchpotato/core/notifications/{toasty/main.py => toasty.py} (54%) delete mode 100644 couchpotato/core/notifications/toasty/__init__.py rename couchpotato/core/notifications/{trakt/main.py => trakt.py} (70%) delete mode 100644 couchpotato/core/notifications/trakt/__init__.py rename couchpotato/core/notifications/{xbmc/main.py => xbmc.py} (79%) mode change 100755 => 100644 delete mode 100644 couchpotato/core/notifications/xbmc/__init__.py rename couchpotato/core/notifications/{xmpp/__init__.py => xmpp.py} (52%) delete mode 100644 couchpotato/core/notifications/xmpp/main.py diff --git a/couchpotato/core/notifications/boxcar/main.py b/couchpotato/core/notifications/boxcar.py similarity index 57% rename from couchpotato/core/notifications/boxcar/main.py rename to couchpotato/core/notifications/boxcar.py index 49aab316..f5006ef9 100644 --- a/couchpotato/core/notifications/boxcar/main.py +++ b/couchpotato/core/notifications/boxcar.py @@ -33,3 +33,33 @@ class Boxcar(Notification): def isEnabled(self): return super(Boxcar, self).isEnabled() and self.conf('email') + + +config = [{ + 'name': 'boxcar', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'boxcar', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'email', + 'description': 'Your Boxcar registration emailaddress.' + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/boxcar/__init__.py b/couchpotato/core/notifications/boxcar/__init__.py deleted file mode 100644 index faab7a5c..00000000 --- a/couchpotato/core/notifications/boxcar/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from .main import Boxcar - - -def start(): - return Boxcar() - -config = [{ - 'name': 'boxcar', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'boxcar', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'email', - 'description': 'Your Boxcar registration emailaddress.' - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/boxcar2/main.py b/couchpotato/core/notifications/boxcar2.py similarity index 59% rename from couchpotato/core/notifications/boxcar2/main.py rename to couchpotato/core/notifications/boxcar2.py index 6633ca70..3e19c19b 100644 --- a/couchpotato/core/notifications/boxcar2/main.py +++ b/couchpotato/core/notifications/boxcar2.py @@ -4,6 +4,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Boxcar2' + class Boxcar2(Notification): @@ -37,3 +39,33 @@ class Boxcar2(Notification): def isEnabled(self): return super(Boxcar2, self).isEnabled() and self.conf('token') + + +config = [{ + 'name': 'boxcar2', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'boxcar2', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'token', + 'description': ('Your Boxcar access token.', 'Can be found in the app under settings') + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/boxcar2/__init__.py b/couchpotato/core/notifications/boxcar2/__init__.py deleted file mode 100644 index da7f99c0..00000000 --- a/couchpotato/core/notifications/boxcar2/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from .main import Boxcar2 - - -def start(): - return Boxcar2() - -config = [{ - 'name': 'boxcar2', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'boxcar2', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'token', - 'description': ('Your Boxcar access token.', 'Can be found in the app under settings') - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/core/__init__.py b/couchpotato/core/notifications/core/__init__.py index b68a915a..9c4fb373 100644 --- a/couchpotato/core/notifications/core/__init__.py +++ b/couchpotato/core/notifications/core/__init__.py @@ -1,7 +1,5 @@ from .main import CoreNotifier -def start(): +def autoload(): return CoreNotifier() - -config = [] diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email.py similarity index 57% rename from couchpotato/core/notifications/email/main.py rename to couchpotato/core/notifications/email.py index b8544016..0a9ff069 100644 --- a/couchpotato/core/notifications/email/main.py +++ b/couchpotato/core/notifications/email.py @@ -66,3 +66,67 @@ class Email(Notification): log.error('E-mail failed: %s', traceback.format_exc()) return False + + +config = [{ + 'name': 'email', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'email', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'from', + 'label': 'Send e-mail from', + }, + { + 'name': 'to', + 'label': 'Send e-mail to', + }, + { + 'name': 'smtp_server', + 'label': 'SMTP server', + }, + { 'name': 'smtp_port', + 'label': 'SMTP server port', + 'default': '25', + 'type': 'int', + }, + { + 'name': 'ssl', + 'label': 'Enable SSL', + 'default': 0, + 'type': 'bool', + }, + { + 'name': 'starttls', + 'label': 'Enable StartTLS', + 'default': 0, + 'type': 'bool', + }, + { + 'name': 'smtp_user', + 'label': 'SMTP user', + }, + { + 'name': 'smtp_pass', + 'label': 'SMTP password', + 'type': 'password', + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/email/__init__.py b/couchpotato/core/notifications/email/__init__.py deleted file mode 100644 index aaf087b9..00000000 --- a/couchpotato/core/notifications/email/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -from .main import Email - - -def start(): - return Email() - -config = [{ - 'name': 'email', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'email', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'from', - 'label': 'Send e-mail from', - }, - { - 'name': 'to', - 'label': 'Send e-mail to', - }, - { - 'name': 'smtp_server', - 'label': 'SMTP server', - }, - { 'name': 'smtp_port', - 'label': 'SMTP server port', - 'default': '25', - 'type': 'int', - }, - { - 'name': 'ssl', - 'label': 'Enable SSL', - 'default': 0, - 'type': 'bool', - }, - { - 'name': 'starttls', - 'label': 'Enable StartTLS', - 'default': 0, - 'type': 'bool', - }, - { - 'name': 'smtp_user', - 'label': 'SMTP user', - }, - { - 'name': 'smtp_pass', - 'label': 'SMTP password', - 'type': 'password', - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl.py similarity index 62% rename from couchpotato/core/notifications/growl/main.py rename to couchpotato/core/notifications/growl.py index a3927ed2..8d67efcc 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl.py @@ -64,3 +64,44 @@ class Growl(Notification): return False + +config = [{ + 'name': 'growl', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'growl', + 'description': 'Version 1.4+', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'on_snatch', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + { + 'name': 'hostname', + 'description': 'Notify growl over network. Needs restart.', + 'advanced': True, + }, + { + 'name': 'port', + 'type': 'int', + 'advanced': True, + }, + { + 'name': 'password', + 'type': 'password', + 'advanced': True, + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/growl/__init__.py b/couchpotato/core/notifications/growl/__init__.py deleted file mode 100644 index dd01cb91..00000000 --- a/couchpotato/core/notifications/growl/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -from .main import Growl - - -def start(): - return Growl() - -config = [{ - 'name': 'growl', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'growl', - 'description': 'Version 1.4+', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'on_snatch', - 'default': False, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - { - 'name': 'hostname', - 'description': 'Notify growl over network. Needs restart.', - 'advanced': True, - }, - { - 'name': 'port', - 'type': 'int', - 'advanced': True, - }, - { - 'name': 'password', - 'type': 'password', - 'advanced': True, - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/nmj/main.py b/couchpotato/core/notifications/nmj.py similarity index 84% rename from couchpotato/core/notifications/nmj/main.py rename to couchpotato/core/notifications/nmj.py index 967b70e7..12bb2ad7 100644 --- a/couchpotato/core/notifications/nmj/main.py +++ b/couchpotato/core/notifications/nmj.py @@ -118,3 +118,31 @@ class NMJ(Notification): } +config = [{ + 'name': 'nmj', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'nmj', + 'label': 'NMJ', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'host', + 'default': 'localhost', + }, + { + 'name': 'database', + }, + { + 'name': 'mount', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/nmj/__init__.py b/couchpotato/core/notifications/nmj/__init__.py deleted file mode 100644 index 461a450e..00000000 --- a/couchpotato/core/notifications/nmj/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from .main import NMJ - - -def start(): - return NMJ() - -config = [{ - 'name': 'nmj', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'nmj', - 'label': 'NMJ', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'host', - 'default': 'localhost', - }, - { - 'name': 'database', - }, - { - 'name': 'mount', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/notifymyandroid/__init__.py b/couchpotato/core/notifications/notifymyandroid.py similarity index 54% rename from couchpotato/core/notifications/notifymyandroid/__init__.py rename to couchpotato/core/notifications/notifymyandroid.py index 7d4f4aeb..8b083601 100644 --- a/couchpotato/core/notifications/notifymyandroid/__init__.py +++ b/couchpotato/core/notifications/notifymyandroid.py @@ -1,8 +1,39 @@ -from .main import NotifyMyAndroid +from couchpotato.core.helpers.variable import splitString +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +import pynma +import six + +log = CPLog(__name__) -def start(): - return NotifyMyAndroid() +class NotifyMyAndroid(Notification): + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + nma = pynma.PyNMA() + keys = splitString(self.conf('api_key')) + nma.addkey(keys) + nma.developerkey(self.conf('dev_key')) + + response = nma.push( + application = self.default_title, + event = message.split(' ')[0], + description = message, + priority = self.conf('priority'), + batch_mode = len(keys) > 1 + ) + + successful = 0 + for key in keys: + if not response[str(key)]['code'] == six.u('200'): + log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) + else: + successful += 1 + + return successful == len(keys) + config = [{ 'name': 'notifymyandroid', diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py deleted file mode 100644 index 16465101..00000000 --- a/couchpotato/core/notifications/notifymyandroid/main.py +++ /dev/null @@ -1,35 +0,0 @@ -from couchpotato.core.helpers.variable import splitString -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification -import pynma -import six - -log = CPLog(__name__) - - -class NotifyMyAndroid(Notification): - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - nma = pynma.PyNMA() - keys = splitString(self.conf('api_key')) - nma.addkey(keys) - nma.developerkey(self.conf('dev_key')) - - response = nma.push( - application = self.default_title, - event = message.split(' ')[0], - description = message, - priority = self.conf('priority'), - batch_mode = len(keys) > 1 - ) - - successful = 0 - for key in keys: - if not response[str(key)]['code'] == six.u('200'): - log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) - else: - successful += 1 - - return successful == len(keys) diff --git a/couchpotato/core/notifications/notifymywp/__init__.py b/couchpotato/core/notifications/notifymywp.py similarity index 58% rename from couchpotato/core/notifications/notifymywp/__init__.py rename to couchpotato/core/notifications/notifymywp.py index 4fcf1a9a..914897f7 100644 --- a/couchpotato/core/notifications/notifymywp/__init__.py +++ b/couchpotato/core/notifications/notifymywp.py @@ -1,8 +1,29 @@ -from .main import NotifyMyWP +from couchpotato.core.helpers.variable import splitString +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +from pynmwp import PyNMWP +import six + +log = CPLog(__name__) -def start(): - return NotifyMyWP() +class NotifyMyWP(Notification): + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + keys = splitString(self.conf('api_key')) + p = PyNMWP(keys, self.conf('dev_key')) + + response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) + + for key in keys: + if not response[key]['Code'] == six.u('200'): + log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message'])) + return False + + return response + config = [{ 'name': 'notifymywp', diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py deleted file mode 100644 index 74010441..00000000 --- a/couchpotato/core/notifications/notifymywp/main.py +++ /dev/null @@ -1,25 +0,0 @@ -from couchpotato.core.helpers.variable import splitString -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification -from pynmwp import PyNMWP -import six - -log = CPLog(__name__) - - -class NotifyMyWP(Notification): - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - keys = splitString(self.conf('api_key')) - p = PyNMWP(keys, self.conf('dev_key')) - - response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) - - for key in keys: - if not response[key]['Code'] == six.u('200'): - log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message'])) - return False - - return response diff --git a/couchpotato/core/notifications/plex/__init__.py b/couchpotato/core/notifications/plex/__init__.py index 0de92ca3..4a64ec5b 100755 --- a/couchpotato/core/notifications/plex/__init__.py +++ b/couchpotato/core/notifications/plex/__init__.py @@ -1,7 +1,7 @@ from .main import Plex -def start(): +def autoload(): return Plex() config = [{ diff --git a/couchpotato/core/notifications/prowl/__init__.py b/couchpotato/core/notifications/prowl.py similarity index 50% rename from couchpotato/core/notifications/prowl/__init__.py rename to couchpotato/core/notifications/prowl.py index 3721a0ad..ecadb7f5 100644 --- a/couchpotato/core/notifications/prowl/__init__.py +++ b/couchpotato/core/notifications/prowl.py @@ -1,8 +1,39 @@ -from .main import Prowl +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +import traceback + +log = CPLog(__name__) -def start(): - return Prowl() +class Prowl(Notification): + + urls = { + 'api': 'https://api.prowlapp.com/publicapi/add' + } + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + data = { + 'apikey': self.conf('api_key'), + 'application': self.default_title, + 'description': toUnicode(message), + 'priority': self.conf('priority'), + } + headers = { + 'Content-type': 'application/x-www-form-urlencoded' + } + + try: + self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) + log.info('Prowl notifications sent.') + return True + except: + log.error('Prowl failed: %s', traceback.format_exc()) + + return False + config = [{ 'name': 'prowl', diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py deleted file mode 100644 index b3385863..00000000 --- a/couchpotato/core/notifications/prowl/main.py +++ /dev/null @@ -1,35 +0,0 @@ -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification -import traceback - -log = CPLog(__name__) - - -class Prowl(Notification): - - urls = { - 'api': 'https://api.prowlapp.com/publicapi/add' - } - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - data = { - 'apikey': self.conf('api_key'), - 'application': self.default_title, - 'description': toUnicode(message), - 'priority': self.conf('priority'), - } - headers = { - 'Content-type': 'application/x-www-form-urlencoded' - } - - try: - self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) - log.info('Prowl notifications sent.') - return True - except: - log.error('Prowl failed: %s', traceback.format_exc()) - - return False diff --git a/couchpotato/core/notifications/pushalot/__init__.py b/couchpotato/core/notifications/pushalot.py similarity index 55% rename from couchpotato/core/notifications/pushalot/__init__.py rename to couchpotato/core/notifications/pushalot.py index ad0c853f..9f717e42 100644 --- a/couchpotato/core/notifications/pushalot/__init__.py +++ b/couchpotato/core/notifications/pushalot.py @@ -1,8 +1,42 @@ -from .main import Pushalot +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +import traceback + +log = CPLog(__name__) -def start(): - return Pushalot() +class Pushalot(Notification): + + urls = { + 'api': 'https://pushalot.com/api/sendmessage' + } + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + data = { + 'AuthorizationToken': self.conf('auth_token'), + 'Title': self.default_title, + 'Body': toUnicode(message), + 'IsImportant': self.conf('important'), + 'IsSilent': self.conf('silent'), + 'Image': toUnicode(self.getNotificationImage('medium') + '?1'), + 'Source': toUnicode(self.default_title) + } + + headers = { + 'Content-type': 'application/x-www-form-urlencoded' + } + + try: + self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) + return True + except: + log.error('PushAlot failed: %s', traceback.format_exc()) + + return False + config = [{ 'name': 'pushalot', diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py deleted file mode 100644 index 306ee1d1..00000000 --- a/couchpotato/core/notifications/pushalot/main.py +++ /dev/null @@ -1,38 +0,0 @@ -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification -import traceback - -log = CPLog(__name__) - - -class Pushalot(Notification): - - urls = { - 'api': 'https://pushalot.com/api/sendmessage' - } - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - data = { - 'AuthorizationToken': self.conf('auth_token'), - 'Title': self.default_title, - 'Body': toUnicode(message), - 'IsImportant': self.conf('important'), - 'IsSilent': self.conf('silent'), - 'Image': toUnicode(self.getNotificationImage('medium') + '?1'), - 'Source': toUnicode(self.default_title) - } - - headers = { - 'Content-type': 'application/x-www-form-urlencoded' - } - - try: - self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) - return True - except: - log.error('PushAlot failed: %s', traceback.format_exc()) - - return False diff --git a/couchpotato/core/notifications/pushbullet/main.py b/couchpotato/core/notifications/pushbullet.py similarity index 66% rename from couchpotato/core/notifications/pushbullet/main.py rename to couchpotato/core/notifications/pushbullet.py index 487fb3aa..beda0b3a 100644 --- a/couchpotato/core/notifications/pushbullet/main.py +++ b/couchpotato/core/notifications/pushbullet.py @@ -67,3 +67,39 @@ class Pushbullet(Notification): log.debug(ex) return None + + +config = [{ + 'name': 'pushbullet', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'pushbullet', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'label': 'User API Key' + }, + { + 'name': 'devices', + 'default': '', + 'advanced': True, + 'description': 'IDs of devices to send notifications to, empty = all devices' + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/pushbullet/__init__.py b/couchpotato/core/notifications/pushbullet/__init__.py deleted file mode 100644 index c52e7781..00000000 --- a/couchpotato/core/notifications/pushbullet/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -from .main import Pushbullet - - -def start(): - return Pushbullet() - -config = [{ - 'name': 'pushbullet', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'pushbullet', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'api_key', - 'label': 'User API Key' - }, - { - 'name': 'devices', - 'default': '', - 'advanced': True, - 'description': 'IDs of devices to send notifications to, empty = all devices' - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover.py similarity index 60% rename from couchpotato/core/notifications/pushover/main.py rename to couchpotato/core/notifications/pushover.py index b90c2118..c5e5257b 100644 --- a/couchpotato/core/notifications/pushover/main.py +++ b/couchpotato/core/notifications/pushover.py @@ -6,6 +6,8 @@ from httplib import HTTPSConnection log = CPLog(__name__) +autoload = 'Pushover' + class Pushover(Notification): @@ -47,3 +49,39 @@ class Pushover(Notification): else: log.error('Pushover notification failed.') return False + + +config = [{ + 'name': 'pushover', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'pushover', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'user_key', + 'description': 'Register on pushover.net to get one.' + }, + { + 'name': 'priority', + 'default': 0, + 'type': 'dropdown', + 'values': [('Normal', 0), ('High', 1)], + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/pushover/__init__.py b/couchpotato/core/notifications/pushover/__init__.py deleted file mode 100644 index da764860..00000000 --- a/couchpotato/core/notifications/pushover/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -from .main import Pushover - - -def start(): - return Pushover() - -config = [{ - 'name': 'pushover', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'pushover', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'user_key', - 'description': 'Register on pushover.net to get one.' - }, - { - 'name': 'priority', - 'default': 0, - 'type': 'dropdown', - 'values': [('Normal', 0), ('High', 1)], - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/synoindex/main.py b/couchpotato/core/notifications/synoindex.py similarity index 69% rename from couchpotato/core/notifications/synoindex/main.py rename to couchpotato/core/notifications/synoindex.py index ec7a64ef..7fd20841 100644 --- a/couchpotato/core/notifications/synoindex/main.py +++ b/couchpotato/core/notifications/synoindex.py @@ -6,6 +6,8 @@ import subprocess log = CPLog(__name__) +autoload = 'Synoindex' + class Synoindex(Notification): @@ -35,3 +37,23 @@ class Synoindex(Notification): return { 'success': os.path.isfile(self.index_path) } + + +config = [{ + 'name': 'synoindex', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'synoindex', + 'description': 'Automaticly adds index to Synology Media Server.', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + } + ], + } + ], +}] diff --git a/couchpotato/core/notifications/synoindex/__init__.py b/couchpotato/core/notifications/synoindex/__init__.py deleted file mode 100644 index 89d07b06..00000000 --- a/couchpotato/core/notifications/synoindex/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from .main import Synoindex - - -def start(): - return Synoindex() - -config = [{ - 'name': 'synoindex', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'synoindex', - 'description': 'Automaticly adds index to Synology Media Server.', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - } - ], - } - ], -}] diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty.py similarity index 54% rename from couchpotato/core/notifications/toasty/main.py rename to couchpotato/core/notifications/toasty.py index ea1f2192..d0bbe8a5 100644 --- a/couchpotato/core/notifications/toasty/main.py +++ b/couchpotato/core/notifications/toasty.py @@ -5,6 +5,8 @@ import traceback log = CPLog(__name__) +autoload = 'Toasty' + class Toasty(Notification): @@ -29,3 +31,33 @@ class Toasty(Notification): log.error('Toasty failed: %s', traceback.format_exc()) return False + + +config = [{ + 'name': 'toasty', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'toasty', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'label': 'Device ID', + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/toasty/__init__.py b/couchpotato/core/notifications/toasty/__init__.py deleted file mode 100644 index 31e055a0..00000000 --- a/couchpotato/core/notifications/toasty/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from .main import Toasty - - -def start(): - return Toasty() - -config = [{ - 'name': 'toasty', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'toasty', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'api_key', - 'label': 'Device ID', - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt.py similarity index 70% rename from couchpotato/core/notifications/trakt/main.py rename to couchpotato/core/notifications/trakt.py index 760f9b64..8f35deab 100644 --- a/couchpotato/core/notifications/trakt/main.py +++ b/couchpotato/core/notifications/trakt.py @@ -4,6 +4,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Trakt' + class Trakt(Notification): @@ -62,3 +64,30 @@ class Trakt(Notification): log.error('Failed to call trakt, check your login.') return False + + +config = [{ + 'name': 'trakt', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'trakt', + 'label': 'Trakt', + 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings', + 'options': [ + { + 'name': 'notification_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'remove_watchlist_enabled', + 'label': 'Remove from watchlist', + 'default': False, + 'type': 'bool', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py deleted file mode 100644 index 20e2e3f9..00000000 --- a/couchpotato/core/notifications/trakt/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -from .main import Trakt - - -def start(): - return Trakt() - -config = [{ - 'name': 'trakt', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'trakt', - 'label': 'Trakt', - 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings', - 'options': [ - { - 'name': 'notification_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'remove_watchlist_enabled', - 'label': 'Remove from watchlist', - 'default': False, - 'type': 'bool', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/twitter/__init__.py b/couchpotato/core/notifications/twitter/__init__.py index 1b9c7699..b6b42bb5 100644 --- a/couchpotato/core/notifications/twitter/__init__.py +++ b/couchpotato/core/notifications/twitter/__init__.py @@ -1,7 +1,7 @@ from .main import Twitter -def start(): +def autoload(): return Twitter() config = [{ diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc.py old mode 100755 new mode 100644 similarity index 79% rename from couchpotato/core/notifications/xbmc/main.py rename to couchpotato/core/notifications/xbmc.py index 44e3573c..48c5c9bb --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc.py @@ -11,6 +11,8 @@ from requests.packages.urllib3.exceptions import MaxRetryError log = CPLog(__name__) +autoload = 'XBMC' + class XBMC(Notification): @@ -210,3 +212,66 @@ class XBMC(Notification): log.error('Failed sending request to XBMC: %s', traceback.format_exc()) return [] + +config = [{ + 'name': 'xbmc', + 'groups': [ + { + 'tab': 'notifications', + 'list': 'notification_providers', + 'name': 'xbmc', + 'label': 'XBMC', + 'description': 'v11 (Eden) and v12 (Frodo)', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'host', + 'default': 'localhost:8080', + }, + { + 'name': 'username', + 'default': 'xbmc', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'only_first', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Only update the first host when movie snatched, useful for synced XBMC', + }, + { + 'name': 'remote_dir_scan', + 'label': 'Remote Folder Scan', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', + }, + { + 'name': 'force_full_scan', + 'label': 'Always do a full scan', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.', + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py deleted file mode 100644 index 34fed632..00000000 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -from .main import XBMC - - -def start(): - return XBMC() - -config = [{ - 'name': 'xbmc', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'xbmc', - 'label': 'XBMC', - 'description': 'v11 (Eden) and v12 (Frodo)', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'host', - 'default': 'localhost:8080', - }, - { - 'name': 'username', - 'default': 'xbmc', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'only_first', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Only update the first host when movie snatched, useful for synced XBMC', - }, - { - 'name': 'remote_dir_scan', - 'label': 'Remote Folder Scan', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', - }, - { - 'name': 'force_full_scan', - 'label': 'Always do a full scan', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.', - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] diff --git a/couchpotato/core/notifications/xmpp/__init__.py b/couchpotato/core/notifications/xmpp.py similarity index 52% rename from couchpotato/core/notifications/xmpp/__init__.py rename to couchpotato/core/notifications/xmpp.py index 0e3e14d9..6a978e5b 100644 --- a/couchpotato/core/notifications/xmpp/__init__.py +++ b/couchpotato/core/notifications/xmpp.py @@ -1,8 +1,49 @@ -from .main import Xmpp +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +from time import sleep +import traceback +import xmpp + +log = CPLog(__name__) + +autoload = 'Xmpp' -def start(): - return Xmpp() +class Xmpp(Notification): + + def notify(self, message = '', data = None, listener = None): + if not data: data = {} + + try: + jid = xmpp.protocol.JID(self.conf('username')) + client = xmpp.Client(jid.getDomain(), debug = []) + + # Connect + if not client.connect(server = (self.conf('hostname'), self.conf('port'))): + log.error('XMPP failed: Connection to server failed.') + return False + + # Authenticate + if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()): + log.error('XMPP failed: Failed to authenticate.') + return False + + # Send message + client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat')) + + # Disconnect + # some older servers will not send the message if you disconnect immediately after sending + sleep(1) + client.disconnect() + + log.info('XMPP notifications sent.') + return True + + except: + log.error('XMPP failed: %s', traceback.format_exc()) + + return False + config = [{ 'name': 'xmpp', diff --git a/couchpotato/core/notifications/xmpp/main.py b/couchpotato/core/notifications/xmpp/main.py deleted file mode 100644 index 0011e41c..00000000 --- a/couchpotato/core/notifications/xmpp/main.py +++ /dev/null @@ -1,43 +0,0 @@ -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification -from time import sleep -import traceback -import xmpp - -log = CPLog(__name__) - - -class Xmpp(Notification): - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - try: - jid = xmpp.protocol.JID(self.conf('username')) - client = xmpp.Client(jid.getDomain(), debug = []) - - # Connect - if not client.connect(server = (self.conf('hostname'), self.conf('port'))): - log.error('XMPP failed: Connection to server failed.') - return False - - # Authenticate - if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()): - log.error('XMPP failed: Failed to authenticate.') - return False - - # Send message - client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat')) - - # Disconnect - # some older servers will not send the message if you disconnect immediately after sending - sleep(1) - client.disconnect() - - log.info('XMPP notifications sent.') - return True - - except: - log.error('XMPP failed: %s', traceback.format_exc()) - - return False From 0b61ec1e13c163978df54dbafee78a63380ec9c7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 22:47:42 +0100 Subject: [PATCH 078/468] Move plugins to single file --- .../{automation/__init__.py => automation.py} | 53 +++++- couchpotato/core/plugins/automation/main.py | 49 ----- .../plugins/{browser/main.py => browser.py} | 3 + couchpotato/core/plugins/browser/__init__.py | 7 - couchpotato/core/plugins/category/__init__.py | 4 +- .../plugins/{custom/main.py => custom.py} | 2 + couchpotato/core/plugins/custom/__init__.py | 7 - .../{dashboard/main.py => dashboard.py} | 2 + .../core/plugins/dashboard/__init__.py | 7 - .../core/plugins/{file/main.py => file.py} | 2 + couchpotato/core/plugins/file/__init__.py | 7 - couchpotato/core/plugins/log/__init__.py | 4 +- .../plugins/{manage/main.py => manage.py} | 41 +++- couchpotato/core/plugins/manage/__init__.py | 43 ----- couchpotato/core/plugins/profile/__init__.py | 4 +- couchpotato/core/plugins/quality/__init__.py | 2 +- couchpotato/core/plugins/release/__init__.py | 4 +- .../plugins/{renamer/main.py => renamer.py} | 176 ++++++++++++++++- couchpotato/core/plugins/renamer/__init__.py | 178 ------------------ .../plugins/{scanner/main.py => scanner.py} | 0 couchpotato/core/plugins/scanner/__init__.py | 7 - couchpotato/core/plugins/score/__init__.py | 4 +- .../plugins/{subtitle/main.py => subtitle.py} | 27 +++ couchpotato/core/plugins/subtitle/__init__.py | 35 ---- .../plugins/{trailer/main.py => trailer.py} | 36 ++++ couchpotato/core/plugins/trailer/__init__.py | 38 ---- .../core/plugins/userscript/__init__.py | 4 +- couchpotato/core/plugins/wizard/__init__.py | 2 +- 28 files changed, 345 insertions(+), 403 deletions(-) rename couchpotato/core/plugins/{automation/__init__.py => automation.py} (54%) delete mode 100644 couchpotato/core/plugins/automation/main.py rename couchpotato/core/plugins/{browser/main.py => browser.py} (99%) delete mode 100644 couchpotato/core/plugins/browser/__init__.py rename couchpotato/core/plugins/{custom/main.py => custom.py} (96%) delete mode 100644 couchpotato/core/plugins/custom/__init__.py rename couchpotato/core/plugins/{dashboard/main.py => dashboard.py} (99%) delete mode 100644 couchpotato/core/plugins/dashboard/__init__.py rename couchpotato/core/plugins/{file/main.py => file.py} (99%) delete mode 100644 couchpotato/core/plugins/file/__init__.py rename couchpotato/core/plugins/{manage/main.py => manage.py} (89%) delete mode 100644 couchpotato/core/plugins/manage/__init__.py rename couchpotato/core/plugins/{renamer/main.py => renamer.py} (89%) mode change 100755 => 100644 delete mode 100755 couchpotato/core/plugins/renamer/__init__.py rename couchpotato/core/plugins/{scanner/main.py => scanner.py} (100%) delete mode 100644 couchpotato/core/plugins/scanner/__init__.py rename couchpotato/core/plugins/{subtitle/main.py => subtitle.py} (69%) delete mode 100644 couchpotato/core/plugins/subtitle/__init__.py rename couchpotato/core/plugins/{trailer/main.py => trailer.py} (61%) delete mode 100644 couchpotato/core/plugins/trailer/__init__.py diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation.py similarity index 54% rename from couchpotato/core/plugins/automation/__init__.py rename to couchpotato/core/plugins/automation.py index 482a0090..e17dc36a 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation.py @@ -1,8 +1,55 @@ -from .main import Automation +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.environment import Env + +log = CPLog(__name__) + +autoload = 'Automation' -def start(): - return Automation() +class Automation(Plugin): + + def __init__(self): + + addEvent('app.load', self.setCrons) + + if not Env.get('dev'): + addEvent('app.load', self.addMovies) + + addEvent('setting.save.automation.hour.after', self.setCrons) + + def setCrons(self): + fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) + + def addMovies(self): + + movies = fireEvent('automation.get_movies', merge = True) + movie_ids = [] + + for imdb_id in movies: + + if self.shuttingDown(): + break + + prop_name = 'automation.added.%s' % imdb_id + added = Env.prop(prop_name, default = False) + if not added: + added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True) + if added_movie: + movie_ids.append(added_movie['id']) + Env.prop(prop_name, True) + + for movie_id in movie_ids: + + if self.shuttingDown(): + break + + movie_dict = fireEvent('media.get', movie_id, single = True) + fireEvent('movie.searcher.single', movie_dict) + + return True + config = [{ 'name': 'automation', diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py deleted file mode 100644 index 2edcd3be..00000000 --- a/couchpotato/core/plugins/automation/main.py +++ /dev/null @@ -1,49 +0,0 @@ -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.environment import Env - -log = CPLog(__name__) - - -class Automation(Plugin): - - def __init__(self): - - addEvent('app.load', self.setCrons) - - if not Env.get('dev'): - addEvent('app.load', self.addMovies) - - addEvent('setting.save.automation.hour.after', self.setCrons) - - def setCrons(self): - fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) - - def addMovies(self): - - movies = fireEvent('automation.get_movies', merge = True) - movie_ids = [] - - for imdb_id in movies: - - if self.shuttingDown(): - break - - prop_name = 'automation.added.%s' % imdb_id - added = Env.prop(prop_name, default = False) - if not added: - added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True) - if added_movie: - movie_ids.append(added_movie['id']) - Env.prop(prop_name, True) - - for movie_id in movie_ids: - - if self.shuttingDown(): - break - - movie_dict = fireEvent('media.get', movie_id, single = True) - fireEvent('movie.searcher.single', movie_dict) - - return True diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser.py similarity index 99% rename from couchpotato/core/plugins/browser/main.py rename to couchpotato/core/plugins/browser.py index 956a7680..8bfe53cc 100644 --- a/couchpotato/core/plugins/browser/main.py +++ b/couchpotato/core/plugins/browser.py @@ -17,6 +17,9 @@ if os.name == 'nt': else: import win32file #@UnresolvedImport +autoload = 'FileBrowser' + + class FileBrowser(Plugin): def __init__(self): diff --git a/couchpotato/core/plugins/browser/__init__.py b/couchpotato/core/plugins/browser/__init__.py deleted file mode 100644 index fae50657..00000000 --- a/couchpotato/core/plugins/browser/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import FileBrowser - - -def start(): - return FileBrowser() - -config = [] diff --git a/couchpotato/core/plugins/category/__init__.py b/couchpotato/core/plugins/category/__init__.py index dcdae90b..d147092f 100644 --- a/couchpotato/core/plugins/category/__init__.py +++ b/couchpotato/core/plugins/category/__init__.py @@ -1,7 +1,5 @@ from .main import CategoryPlugin -def start(): +def autoload(): return CategoryPlugin() - -config = [] diff --git a/couchpotato/core/plugins/custom/main.py b/couchpotato/core/plugins/custom.py similarity index 96% rename from couchpotato/core/plugins/custom/main.py rename to couchpotato/core/plugins/custom.py index a15c915c..c9cff3ea 100644 --- a/couchpotato/core/plugins/custom/main.py +++ b/couchpotato/core/plugins/custom.py @@ -6,6 +6,8 @@ import os log = CPLog(__name__) +autoload = 'Custom' + class Custom(Plugin): diff --git a/couchpotato/core/plugins/custom/__init__.py b/couchpotato/core/plugins/custom/__init__.py deleted file mode 100644 index 20a39351..00000000 --- a/couchpotato/core/plugins/custom/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Custom - - -def start(): - return Custom() - -config = [] diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard.py similarity index 99% rename from couchpotato/core/plugins/dashboard/main.py rename to couchpotato/core/plugins/dashboard.py index d0d3bfef..4a9aaf95 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard.py @@ -10,6 +10,8 @@ import time log = CPLog(__name__) +autoload = 'Dashboard' + class Dashboard(Plugin): diff --git a/couchpotato/core/plugins/dashboard/__init__.py b/couchpotato/core/plugins/dashboard/__init__.py deleted file mode 100644 index c43a44eb..00000000 --- a/couchpotato/core/plugins/dashboard/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Dashboard - - -def start(): - return Dashboard() - -config = [] diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file.py similarity index 99% rename from couchpotato/core/plugins/file/main.py rename to couchpotato/core/plugins/file.py index 1d4313be..41104378 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file.py @@ -14,6 +14,8 @@ import traceback log = CPLog(__name__) +autoload = 'FileManager' + class FileManager(Plugin): diff --git a/couchpotato/core/plugins/file/__init__.py b/couchpotato/core/plugins/file/__init__.py deleted file mode 100644 index 3dced3d0..00000000 --- a/couchpotato/core/plugins/file/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import FileManager - - -def start(): - return FileManager() - -config = [] diff --git a/couchpotato/core/plugins/log/__init__.py b/couchpotato/core/plugins/log/__init__.py index f5d9d105..3760b567 100644 --- a/couchpotato/core/plugins/log/__init__.py +++ b/couchpotato/core/plugins/log/__init__.py @@ -1,7 +1,5 @@ from .main import Logging -def start(): +def autoload(): return Logging() - -config = [] diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage.py similarity index 89% rename from couchpotato/core/plugins/manage/main.py rename to couchpotato/core/plugins/manage.py index 965bd9f3..205de136 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage.py @@ -12,9 +12,10 @@ import sys import time import traceback - log = CPLog(__name__) +autoload = 'Manage' + class Manage(Plugin): @@ -268,3 +269,41 @@ class Manage(Plugin): return free_space + +config = [{ + 'name': 'manage', + 'groups': [ + { + 'tab': 'manage', + 'label': 'Movie Library Manager', + 'description': 'Add your existing movie folders.', + 'options': [ + { + 'name': 'enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'library', + 'type': 'directories', + 'description': 'Folder where the movies should be moved to.', + }, + { + 'label': 'Cleanup After', + 'name': 'cleanup', + 'type': 'bool', + 'description': 'Remove movie from db if it can\'t be found after re-scan.', + 'default': True, + }, + { + 'label': 'Scan at startup', + 'name': 'startup_scan', + 'type': 'bool', + 'default': True, + 'advanced': True, + 'description': 'Do a quick scan on startup. On slow systems better disable this.', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py deleted file mode 100644 index c992dee6..00000000 --- a/couchpotato/core/plugins/manage/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from .main import Manage - - -def start(): - return Manage() - -config = [{ - 'name': 'manage', - 'groups': [ - { - 'tab': 'manage', - 'label': 'Movie Library Manager', - 'description': 'Add your existing movie folders.', - 'options': [ - { - 'name': 'enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'library', - 'type': 'directories', - 'description': 'Folder where the movies should be moved to.', - }, - { - 'label': 'Cleanup After', - 'name': 'cleanup', - 'type': 'bool', - 'description': 'Remove movie from db if it can\'t be found after re-scan.', - 'default': True, - }, - { - 'label': 'Scan at startup', - 'name': 'startup_scan', - 'type': 'bool', - 'default': True, - 'advanced': True, - 'description': 'Do a quick scan on startup. On slow systems better disable this.', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/profile/__init__.py b/couchpotato/core/plugins/profile/__init__.py index c07bc7c5..15a74eee 100644 --- a/couchpotato/core/plugins/profile/__init__.py +++ b/couchpotato/core/plugins/profile/__init__.py @@ -1,7 +1,5 @@ from .main import ProfilePlugin -def start(): +def autoload(): return ProfilePlugin() - -config = [] diff --git a/couchpotato/core/plugins/quality/__init__.py b/couchpotato/core/plugins/quality/__init__.py index 2630f1a3..fc9cda64 100644 --- a/couchpotato/core/plugins/quality/__init__.py +++ b/couchpotato/core/plugins/quality/__init__.py @@ -1,7 +1,7 @@ from .main import QualityPlugin -def start(): +def autoload(): return QualityPlugin() config = [] diff --git a/couchpotato/core/plugins/release/__init__.py b/couchpotato/core/plugins/release/__init__.py index 08c6a57c..e6e60c4b 100644 --- a/couchpotato/core/plugins/release/__init__.py +++ b/couchpotato/core/plugins/release/__init__.py @@ -1,7 +1,5 @@ from .main import Release -def start(): +def autoload(): return Release() - -config = [] diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer.py old mode 100755 new mode 100644 similarity index 89% rename from couchpotato/core/plugins/renamer/main.py rename to couchpotato/core/plugins/renamer.py index 8d145500..7e4fc99c --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer.py @@ -9,7 +9,6 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from scandir import scandir from unrar2 import RarFile -import errno import fnmatch import os import re @@ -21,6 +20,8 @@ from six.moves import filter log = CPLog(__name__) +autoload = 'Renamer' + class Renamer(Plugin): @@ -1187,3 +1188,176 @@ Remove it if you want it to be renamed (again, or at least let it try again) folder = None return folder, media_folder, files, extr_files + + +rename_options = { + 'pre': '<', + 'post': '>', + 'choices': { + 'ext': 'Extention (mkv)', + 'namethe': 'Moviename, The', + 'thename': 'The Moviename', + 'year': 'Year (2011)', + 'first': 'First letter (M)', + 'quality': 'Quality (720p)', + 'quality_type': '(HD) or (SD)', + 'video': 'Video (x264)', + 'audio': 'Audio (DTS)', + 'group': 'Releasegroup name', + 'source': 'Source media (Bluray)', + 'resolution_width': 'resolution width (1280)', + 'resolution_height': 'resolution height (720)', + 'audio_channels': 'audio channels (7.1)', + 'original': 'Original filename', + 'original_folder': 'Original foldername', + 'imdb_id': 'IMDB id (tt0123456)', + 'cd': 'CD number (cd1)', + 'cd_nr': 'Just the cd nr. (1)', + 'mpaa': 'MPAA Rating', + 'category': 'Category label', + }, +} + +config = [{ + 'name': 'renamer', + 'order': 40, + 'description': 'Move and rename your downloaded movies to your movie directory.', + 'groups': [ + { + 'tab': 'renamer', + 'name': 'renamer', + 'label': 'Rename downloaded movies', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'from', + 'type': 'directory', + 'description': 'Folder where CP searches for movies.', + }, + { + 'name': 'to', + 'type': 'directory', + 'description': 'Default folder where the movies are moved to.', + }, + { + 'name': 'folder_name', + 'label': 'Folder naming', + 'description': 'Name of the folder. Keep empty for no folder.', + 'default': ' ()', + 'type': 'choice', + 'options': rename_options + }, + { + 'name': 'file_name', + 'label': 'File naming', + 'description': 'Name of the file', + 'default': '.', + 'type': 'choice', + 'options': rename_options + }, + { + 'name': 'unrar', + 'type': 'bool', + 'description': 'Extract rar files if found.', + 'default': False, + }, + { + 'name': 'cleanup', + 'type': 'bool', + 'description': 'Cleanup leftover files after successful rename.', + 'default': False, + }, + { + 'advanced': True, + 'name': 'run_every', + 'label': 'Run every', + 'default': 1, + 'type': 'int', + 'unit': 'min(s)', + 'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is completed or handle failed download if these options are enabled'), + }, + { + 'advanced': True, + 'name': 'force_every', + 'label': 'Force every', + 'default': 2, + 'type': 'int', + 'unit': 'hour(s)', + 'description': 'Forces the renamer to scan every X hours', + }, + { + 'advanced': True, + 'name': 'next_on_failed', + 'default': True, + 'type': 'bool', + 'description': 'Try the next best release for a movie after a download failed.', + }, + { + 'name': 'move_leftover', + 'type': 'bool', + 'description': 'Move all leftover file after renaming, to the movie folder.', + 'default': False, + 'advanced': True, + }, + { + 'advanced': True, + 'name': 'separator', + 'label': 'File-Separator', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), + }, + { + 'advanced': True, + 'name': 'foldersep', + 'label': 'Folder-Separator', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), + }, + { + 'name': 'file_action', + 'label': 'Torrent File Action', + 'default': 'link', + 'type': 'dropdown', + 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], + 'description': ('Link, Copy or Move after download completed.', + 'Link first tries hard link, then sym link and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'), + 'advanced': True, + }, + { + 'advanced': True, + 'name': 'ntfs_permission', + 'label': 'NTFS Permission', + 'type': 'bool', + 'hidden': os.name != 'nt', + 'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).', + 'default': False, + }, + ], + }, { + 'tab': 'renamer', + 'name': 'meta_renamer', + 'label': 'Advanced renaming', + 'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.', + 'advanced': True, + 'options': [ + { + 'name': 'rename_nfo', + 'label': 'Rename .NFO', + 'description': 'Rename original .nfo file', + 'type': 'bool', + 'default': True, + }, + { + 'name': 'nfo_name', + 'label': 'NFO naming', + 'default': '.orig.', + 'type': 'choice', + 'options': rename_options + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py deleted file mode 100755 index e238f5eb..00000000 --- a/couchpotato/core/plugins/renamer/__init__.py +++ /dev/null @@ -1,178 +0,0 @@ -from couchpotato.core.plugins.renamer.main import Renamer -import os - - -def start(): - return Renamer() - -rename_options = { - 'pre': '<', - 'post': '>', - 'choices': { - 'ext': 'Extention (mkv)', - 'namethe': 'Moviename, The', - 'thename': 'The Moviename', - 'year': 'Year (2011)', - 'first': 'First letter (M)', - 'quality': 'Quality (720p)', - 'quality_type': '(HD) or (SD)', - 'video': 'Video (x264)', - 'audio': 'Audio (DTS)', - 'group': 'Releasegroup name', - 'source': 'Source media (Bluray)', - 'resolution_width': 'resolution width (1280)', - 'resolution_height': 'resolution height (720)', - 'audio_channels': 'audio channels (7.1)', - 'original': 'Original filename', - 'original_folder': 'Original foldername', - 'imdb_id': 'IMDB id (tt0123456)', - 'cd': 'CD number (cd1)', - 'cd_nr': 'Just the cd nr. (1)', - 'mpaa': 'MPAA Rating', - 'category': 'Category label', - }, -} - -config = [{ - 'name': 'renamer', - 'order': 40, - 'description': 'Move and rename your downloaded movies to your movie directory.', - 'groups': [ - { - 'tab': 'renamer', - 'name': 'renamer', - 'label': 'Rename downloaded movies', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'from', - 'type': 'directory', - 'description': 'Folder where CP searches for movies.', - }, - { - 'name': 'to', - 'type': 'directory', - 'description': 'Default folder where the movies are moved to.', - }, - { - 'name': 'folder_name', - 'label': 'Folder naming', - 'description': 'Name of the folder. Keep empty for no folder.', - 'default': ' ()', - 'type': 'choice', - 'options': rename_options - }, - { - 'name': 'file_name', - 'label': 'File naming', - 'description': 'Name of the file', - 'default': '.', - 'type': 'choice', - 'options': rename_options - }, - { - 'name': 'unrar', - 'type': 'bool', - 'description': 'Extract rar files if found.', - 'default': False, - }, - { - 'name': 'cleanup', - 'type': 'bool', - 'description': 'Cleanup leftover files after successful rename.', - 'default': False, - }, - { - 'advanced': True, - 'name': 'run_every', - 'label': 'Run every', - 'default': 1, - 'type': 'int', - 'unit': 'min(s)', - 'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is completed or handle failed download if these options are enabled'), - }, - { - 'advanced': True, - 'name': 'force_every', - 'label': 'Force every', - 'default': 2, - 'type': 'int', - 'unit': 'hour(s)', - 'description': 'Forces the renamer to scan every X hours', - }, - { - 'advanced': True, - 'name': 'next_on_failed', - 'default': True, - 'type': 'bool', - 'description': 'Try the next best release for a movie after a download failed.', - }, - { - 'name': 'move_leftover', - 'type': 'bool', - 'description': 'Move all leftover file after renaming, to the movie folder.', - 'default': False, - 'advanced': True, - }, - { - 'advanced': True, - 'name': 'separator', - 'label': 'File-Separator', - 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), - }, - { - 'advanced': True, - 'name': 'foldersep', - 'label': 'Folder-Separator', - 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), - }, - { - 'name': 'file_action', - 'label': 'Torrent File Action', - 'default': 'link', - 'type': 'dropdown', - 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], - 'description': ('Link, Copy or Move after download completed.', - 'Link first tries hard link, then sym link and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'), - 'advanced': True, - }, - { - 'advanced': True, - 'name': 'ntfs_permission', - 'label': 'NTFS Permission', - 'type': 'bool', - 'hidden': os.name != 'nt', - 'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).', - 'default': False, - }, - ], - }, { - 'tab': 'renamer', - 'name': 'meta_renamer', - 'label': 'Advanced renaming', - 'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.', - 'advanced': True, - 'options': [ - { - 'name': 'rename_nfo', - 'label': 'Rename .NFO', - 'description': 'Rename original .nfo file', - 'type': 'bool', - 'default': True, - }, - { - 'name': 'nfo_name', - 'label': 'NFO naming', - 'default': '.orig.', - 'type': 'choice', - 'options': rename_options - }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner.py similarity index 100% rename from couchpotato/core/plugins/scanner/main.py rename to couchpotato/core/plugins/scanner.py diff --git a/couchpotato/core/plugins/scanner/__init__.py b/couchpotato/core/plugins/scanner/__init__.py deleted file mode 100644 index 66c6b39c..00000000 --- a/couchpotato/core/plugins/scanner/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Scanner - - -def start(): - return Scanner() - -config = [] diff --git a/couchpotato/core/plugins/score/__init__.py b/couchpotato/core/plugins/score/__init__.py index a960081c..65cadd99 100644 --- a/couchpotato/core/plugins/score/__init__.py +++ b/couchpotato/core/plugins/score/__init__.py @@ -1,7 +1,5 @@ from .main import Score -def start(): +def autoload(): return Score() - -config = [] diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle.py similarity index 69% rename from couchpotato/core/plugins/subtitle/main.py rename to couchpotato/core/plugins/subtitle.py index c00c757e..06a77f9a 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle.py @@ -9,6 +9,8 @@ import traceback log = CPLog(__name__) +autoload = 'Subtitle' + class Subtitle(Plugin): @@ -47,3 +49,28 @@ class Subtitle(Plugin): def getLanguages(self): return splitString(self.conf('languages')) + + +config = [{ + 'name': 'subtitle', + 'groups': [ + { + 'tab': 'renamer', + 'name': 'subtitle', + 'label': 'Download subtitles', + 'description': 'after rename', + 'options': [ + { + 'name': 'enabled', + 'label': 'Search and download subtitles', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'languages', + 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'), + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py deleted file mode 100644 index 59847aee..00000000 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from .main import Subtitle - - -def start(): - return Subtitle() - -config = [{ - 'name': 'subtitle', - 'groups': [ - { - 'tab': 'renamer', - 'name': 'subtitle', - 'label': 'Download subtitles', - 'description': 'after rename', - 'options': [ - { - 'name': 'enabled', - 'label': 'Search and download subtitles', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'languages', - 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'), - }, -# { -# 'name': 'automatic', -# 'default': True, -# 'type': 'bool', -# 'description': 'Automaticly search & download for movies in library', -# }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer.py similarity index 61% rename from couchpotato/core/plugins/trailer/main.py rename to couchpotato/core/plugins/trailer.py index 36084066..b202e939 100644 --- a/couchpotato/core/plugins/trailer/main.py +++ b/couchpotato/core/plugins/trailer.py @@ -6,6 +6,8 @@ import os log = CPLog(__name__) +autoload = 'Trailer' + class Trailer(Plugin): @@ -40,3 +42,37 @@ class Trailer(Plugin): break return True + + +config = [{ + 'name': 'trailer', + 'groups': [ + { + 'tab': 'renamer', + 'name': 'trailer', + 'label': 'Download trailer', + 'description': 'after rename', + 'options': [ + { + 'name': 'enabled', + 'label': 'Search and download trailers', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'quality', + 'default': '720p', + 'type': 'dropdown', + 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')], + }, + { + 'name': 'name', + 'label': 'Naming', + 'default': '-trailer', + 'advanced': True, + 'description': 'Use <filename> to use above settings.' + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py deleted file mode 100644 index e7a6d26e..00000000 --- a/couchpotato/core/plugins/trailer/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from .main import Trailer - - -def start(): - return Trailer() - -config = [{ - 'name': 'trailer', - 'groups': [ - { - 'tab': 'renamer', - 'name': 'trailer', - 'label': 'Download trailer', - 'description': 'after rename', - 'options': [ - { - 'name': 'enabled', - 'label': 'Search and download trailers', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'quality', - 'default': '720p', - 'type': 'dropdown', - 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')], - }, - { - 'name': 'name', - 'label': 'Naming', - 'default': '-trailer', - 'advanced': True, - 'description': 'Use <filename> to use above settings.' - }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/userscript/__init__.py b/couchpotato/core/plugins/userscript/__init__.py index 184f5d79..9d708593 100644 --- a/couchpotato/core/plugins/userscript/__init__.py +++ b/couchpotato/core/plugins/userscript/__init__.py @@ -1,7 +1,5 @@ from .main import Userscript -def start(): +def autoload(): return Userscript() - -config = [] diff --git a/couchpotato/core/plugins/wizard/__init__.py b/couchpotato/core/plugins/wizard/__init__.py index eda6f25a..7a272b44 100644 --- a/couchpotato/core/plugins/wizard/__init__.py +++ b/couchpotato/core/plugins/wizard/__init__.py @@ -1,7 +1,7 @@ from .main import Wizard -def start(): +def autoload(): return Wizard() config = [{ From f3adfca9c5cdba3fcfe0536ed1a578c4851ac35e Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 23:01:42 +0100 Subject: [PATCH 079/468] Move media to single file --- .../core/media/_base/media/__init__.py | 4 +- couchpotato/core/media/_base/media/main.py | 2 + .../media/_base/providers/automation/base.py | 2 +- .../nzb/{binsearch/main.py => binsearch.py} | 28 ++++++ .../_base/providers/nzb/binsearch/__init__.py | 27 ------ .../nzb/{newznab/main.py => newznab.py} | 56 ++++++++++++ .../_base/providers/nzb/newznab/__init__.py | 55 ------------ .../nzb/{nzbclub/main.py => nzbclub.py} | 28 ++++++ .../_base/providers/nzb/nzbclub/__init__.py | 27 ------ .../nzb/{nzbindex/main.py => nzbindex.py} | 29 +++++++ .../_base/providers/nzb/nzbindex/__init__.py | 28 ------ .../nzb/{omgwtfnzbs/main.py => omgwtfnzbs.py} | 37 ++++++++ .../providers/nzb/omgwtfnzbs/__init__.py | 36 -------- .../{awesomehd/main.py => awesomehd.py} | 69 +++++++++++++++ .../providers/torrent/awesomehd/__init__.py | 68 --------------- .../torrent/{bithdtv/main.py => bithdtv.py} | 52 +++++++++++ .../providers/torrent/bithdtv/__init__.py | 51 ----------- .../torrent/{bitsoup/main.py => bitsoup.py} | 52 +++++++++++ .../providers/torrent/bitsoup/__init__.py | 51 ----------- .../torrent/{hdbits/main.py => hdbits.py} | 50 +++++++++++ .../providers/torrent/hdbits/__init__.py | 49 ----------- .../main.py => ilovetorrents.py} | 57 ++++++++++++ .../torrent/ilovetorrents/__init__.py | 56 ------------ .../{iptorrents/main.py => iptorrents.py} | 58 +++++++++++++ .../providers/torrent/iptorrents/__init__.py | 57 ------------ .../main.py => kickasstorrents.py} | 49 +++++++++++ .../torrent/kickasstorrents/__init__.py | 48 ----------- .../main.py => passthepopcorn.py} | 86 +++++++++++++++++++ .../torrent/passthepopcorn/__init__.py | 85 ------------------ .../torrent/{publichd/main.py => publichd.py} | 43 ++++++++++ .../providers/torrent/publichd/__init__.py | 42 --------- .../{sceneaccess/main.py => sceneaccess.py} | 52 +++++++++++ .../providers/torrent/sceneaccess/__init__.py | 51 ----------- .../{thepiratebay/main.py => thepiratebay.py} | 49 +++++++++++ .../torrent/thepiratebay/__init__.py | 48 ----------- .../{torrentbytes/main.py => torrentbytes.py} | 51 +++++++++++ .../torrent/torrentbytes/__init__.py | 51 ----------- .../{torrentday/main.py => torrentday.py} | 52 +++++++++++ .../providers/torrent/torrentday/__init__.py | 51 ----------- .../{torrentleech/main.py => torrentleech.py} | 52 +++++++++++ .../torrent/torrentleech/__init__.py | 51 ----------- .../main.py => torrentpotato.py} | 63 ++++++++++++++ .../torrent/torrentpotato/__init__.py | 62 ------------- .../{torrentshack/main.py => torrentshack.py} | 57 ++++++++++++ .../torrent/torrentshack/__init__.py | 56 ------------ .../torrent/{yify/main.py => yify.py} | 51 ++++++++++- .../_base/providers/torrent/yify/__init__.py | 48 ----------- .../core/media/_base/search/__init__.py | 4 +- .../core/media/_base/searcher/__init__.py | 2 +- .../media/movie/providers/automation/base.py | 4 +- 50 files changed, 1128 insertions(+), 1109 deletions(-) rename couchpotato/core/media/_base/providers/nzb/{binsearch/main.py => binsearch.py} (79%) delete mode 100644 couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py rename couchpotato/core/media/_base/providers/nzb/{newznab/main.py => newznab.py} (72%) delete mode 100644 couchpotato/core/media/_base/providers/nzb/newznab/__init__.py rename couchpotato/core/media/_base/providers/nzb/{nzbclub/main.py => nzbclub.py} (76%) delete mode 100644 couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py rename couchpotato/core/media/_base/providers/nzb/{nzbindex/main.py => nzbindex.py} (80%) delete mode 100644 couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py rename couchpotato/core/media/_base/providers/nzb/{omgwtfnzbs/main.py => omgwtfnzbs.py} (67%) delete mode 100644 couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py rename couchpotato/core/media/_base/providers/torrent/{awesomehd/main.py => awesomehd.py} (55%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py rename couchpotato/core/media/_base/providers/torrent/{bithdtv/main.py => bithdtv.py} (64%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py rename couchpotato/core/media/_base/providers/torrent/{bitsoup/main.py => bitsoup.py} (64%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py rename couchpotato/core/media/_base/providers/torrent/{hdbits/main.py => hdbits.py} (55%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py rename couchpotato/core/media/_base/providers/torrent/{ilovetorrents/main.py => ilovetorrents.py} (72%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py rename couchpotato/core/media/_base/providers/torrent/{iptorrents/main.py => iptorrents.py} (69%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py rename couchpotato/core/media/_base/providers/torrent/{kickasstorrents/main.py => kickasstorrents.py} (71%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py rename couchpotato/core/media/_base/providers/torrent/{passthepopcorn/main.py => passthepopcorn.py} (69%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py rename couchpotato/core/media/_base/providers/torrent/{publichd/main.py => publichd.py} (68%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/publichd/__init__.py rename couchpotato/core/media/_base/providers/torrent/{sceneaccess/main.py => sceneaccess.py} (65%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py rename couchpotato/core/media/_base/providers/torrent/{thepiratebay/main.py => thepiratebay.py} (71%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py rename couchpotato/core/media/_base/providers/torrent/{torrentbytes/main.py => torrentbytes.py} (63%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py rename couchpotato/core/media/_base/providers/torrent/{torrentday/main.py => torrentday.py} (53%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py rename couchpotato/core/media/_base/providers/torrent/{torrentleech/main.py => torrentleech.py} (61%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py rename couchpotato/core/media/_base/providers/torrent/{torrentpotato/main.py => torrentpotato.py} (65%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py rename couchpotato/core/media/_base/providers/torrent/{torrentshack/main.py => torrentshack.py} (58%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py rename couchpotato/core/media/_base/providers/torrent/{yify/main.py => yify.py} (54%) delete mode 100644 couchpotato/core/media/_base/providers/torrent/yify/__init__.py diff --git a/couchpotato/core/media/_base/media/__init__.py b/couchpotato/core/media/_base/media/__init__.py index e5f5a0ec..b1cde097 100644 --- a/couchpotato/core/media/_base/media/__init__.py +++ b/couchpotato/core/media/_base/media/__init__.py @@ -1,7 +1,5 @@ from .main import MediaPlugin -def start(): +def autoload(): return MediaPlugin() - -config = [] diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 76622230..08082338 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -11,6 +11,8 @@ from string import ascii_lowercase log = CPLog(__name__) +autoload = 'MediaPlugin' + class MediaPlugin(MediaBase): diff --git a/couchpotato/core/media/_base/providers/automation/base.py b/couchpotato/core/media/_base/providers/automation/base.py index 20ce2e16..21d205ae 100644 --- a/couchpotato/core/media/_base/providers/automation/base.py +++ b/couchpotato/core/media/_base/providers/automation/base.py @@ -4,5 +4,5 @@ from couchpotato.core.media._base.providers.base import Provider log = CPLog(__name__) -class BaseAutomation(Provider): +class AutomationBase(Provider): pass diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch/main.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py similarity index 79% rename from couchpotato/core/media/_base/providers/nzb/binsearch/main.py rename to couchpotato/core/media/_base/providers/nzb/binsearch.py index fb72a848..0334bb49 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch/main.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -87,3 +87,31 @@ class Base(NZBProvider): log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc())) return 'try_next' + + +config = [{ + 'name': 'binsearch', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'nzb_providers', + 'name': 'binsearch', + 'description': 'Free provider, less accurate. See BinSearch', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py b/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py deleted file mode 100644 index 48e71cdb..00000000 --- a/couchpotato/core/media/_base/providers/nzb/binsearch/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ - -config = [{ - 'name': 'binsearch', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'nzb_providers', - 'name': 'binsearch', - 'description': 'Free provider, less accurate. See BinSearch', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/nzb/newznab/main.py b/couchpotato/core/media/_base/providers/nzb/newznab.py similarity index 72% rename from couchpotato/core/media/_base/providers/nzb/newznab/main.py rename to couchpotato/core/media/_base/providers/nzb/newznab.py index acc4a7d3..bbc77a3b 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab/main.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -187,3 +187,59 @@ class Base(NZBProvider, RSS): log.error('Failed download from %s: %s', (host, traceback.format_exc())) return 'try_next' + + +config = [{ + 'name': 'newznab', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'nzb_providers', + 'name': 'newznab', + 'order': 10, + 'description': 'Enable NewzNab such as NZB.su, \ + NZBs.org, DOGnzb.cr, \ + Spotweb, NZBGeek, \ + SmackDown, NZBFinder', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': True, + }, + { + 'name': 'use', + 'default': '0,0,0,0,0,0' + }, + { + 'name': 'host', + 'default': 'api.nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', + 'description': 'The hostname of your newznab provider', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': '0,0,0,0,0,0', + 'description': 'Starting score for each release found via this provider.', + }, + { + 'name': 'custom_tag', + 'advanced': True, + 'label': 'Custom tag', + 'default': ',,,,,', + 'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org', + }, + { + 'name': 'api_key', + 'default': ',,,,,', + 'label': 'Api Key', + 'description': 'Can be found on your profile page', + 'type': 'combined', + 'combine': ['use', 'host', 'api_key', 'extra_score', 'custom_tag'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py b/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py deleted file mode 100644 index 8426e3ed..00000000 --- a/couchpotato/core/media/_base/providers/nzb/newznab/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ - -config = [{ - 'name': 'newznab', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'nzb_providers', - 'name': 'newznab', - 'order': 10, - 'description': 'Enable NewzNab such as NZB.su, \ - NZBs.org, DOGnzb.cr, \ - Spotweb, NZBGeek, \ - SmackDown, NZBFinder', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': True, - }, - { - 'name': 'use', - 'default': '0,0,0,0,0,0' - }, - { - 'name': 'host', - 'default': 'api.nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', - 'description': 'The hostname of your newznab provider', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'default': '0,0,0,0,0,0', - 'description': 'Starting score for each release found via this provider.', - }, - { - 'name': 'custom_tag', - 'advanced': True, - 'label': 'Custom tag', - 'default': ',,,,,', - 'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org', - }, - { - 'name': 'api_key', - 'default': ',,,,,', - 'label': 'Api Key', - 'description': 'Can be found on your profile page', - 'type': 'combined', - 'combine': ['use', 'host', 'api_key', 'extra_score', 'custom_tag'], - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py b/couchpotato/core/media/_base/providers/nzb/nzbclub.py similarity index 76% rename from couchpotato/core/media/_base/providers/nzb/nzbclub/main.py rename to couchpotato/core/media/_base/providers/nzb/nzbclub.py index 322f8198..562de5ca 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub.py @@ -67,3 +67,31 @@ class Base(NZBProvider, RSS): return False return True + + +config = [{ + 'name': 'nzbclub', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'nzb_providers', + 'name': 'NZBClub', + 'description': 'Free provider, less accurate. See NZBClub', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py deleted file mode 100644 index 60c9bc4e..00000000 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ - -config = [{ - 'name': 'nzbclub', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'nzb_providers', - 'name': 'NZBClub', - 'description': 'Free provider, less accurate. See NZBClub', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py b/couchpotato/core/media/_base/providers/nzb/nzbindex.py similarity index 80% rename from couchpotato/core/media/_base/providers/nzb/nzbindex/main.py rename to couchpotato/core/media/_base/providers/nzb/nzbindex.py index d21dc0b2..c81165e1 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex.py @@ -92,3 +92,32 @@ class Base(NZBProvider, RSS): item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text) except: pass + + +config = [{ + 'name': 'nzbindex', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'nzb_providers', + 'name': 'nzbindex', + 'description': 'Free provider, less accurate. See NZBIndex', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': True, + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py b/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py deleted file mode 100644 index 4ce51050..00000000 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ - -config = [{ - 'name': 'nzbindex', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'nzb_providers', - 'name': 'nzbindex', - 'description': 'Free provider, less accurate. See NZBIndex', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': True, - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py similarity index 67% rename from couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py rename to couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index 6ecefe37..29434262 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/main.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -61,3 +61,40 @@ class Base(NZBProvider, RSS): 'detail_url': self.urls['detail_url'] % nzb_id, 'description': self.getTextElement(nzb, 'description') }) + + +config = [{ + 'name': 'omgwtfnzbs', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'nzb_providers', + 'name': 'OMGWTFNZBs', + 'description': 'See OMGWTFNZBs', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'api_key', + 'label': 'Api Key', + 'default': '', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': 20, + 'type': 'int', + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py deleted file mode 100644 index 841c04e7..00000000 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ - -config = [{ - 'name': 'omgwtfnzbs', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'nzb_providers', - 'name': 'OMGWTFNZBs', - 'description': 'See OMGWTFNZBs', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'api_key', - 'label': 'Api Key', - 'default': '', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'default': 20, - 'type': 'int', - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py b/couchpotato/core/media/_base/providers/torrent/awesomehd.py similarity index 55% rename from couchpotato/core/media/_base/providers/torrent/awesomehd/main.py rename to couchpotato/core/media/_base/providers/torrent/awesomehd.py index 3a370435..f8098626 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd/main.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd.py @@ -67,3 +67,72 @@ class Base(TorrentProvider): except: log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) + + +config = [{ + 'name': 'awesomehd', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'Awesome-HD', + 'description': 'See AHD', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'passkey', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'only_internal', + 'advanced': True, + 'type': 'bool', + 'default': 1, + 'description': 'Only search for internal releases.' + }, + { + 'name': 'prefer_internal', + 'advanced': True, + 'type': 'bool', + 'default': 1, + 'description': 'Favors internal releases over non-internal releases.' + }, + { + 'name': 'favor', + 'advanced': True, + 'default': 'both', + 'type': 'dropdown', + 'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')], + 'description': 'Give extra scoring to encodes or remuxes.' + }, + { + 'name': 'extra_score', + 'advanced': True, + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + }, + ], + }, + ], +}] + diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py b/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py deleted file mode 100644 index 1ab625d8..00000000 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ - -config = [{ - 'name': 'awesomehd', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'Awesome-HD', - 'description': 'See AHD', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'passkey', - 'default': '', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'only_internal', - 'advanced': True, - 'type': 'bool', - 'default': 1, - 'description': 'Only search for internal releases.' - }, - { - 'name': 'prefer_internal', - 'advanced': True, - 'type': 'bool', - 'default': 1, - 'description': 'Favors internal releases over non-internal releases.' - }, - { - 'name': 'favor', - 'advanced': True, - 'default': 'both', - 'type': 'dropdown', - 'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')], - 'description': 'Give extra scoring to encodes or remuxes.' - }, - { - 'name': 'extra_score', - 'advanced': True, - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - }, - ], - }, - ], -}] - diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py similarity index 64% rename from couchpotato/core/media/_base/providers/torrent/bithdtv/main.py rename to couchpotato/core/media/_base/providers/torrent/bithdtv.py index ce2ea71a..619ae91a 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv/main.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -82,3 +82,55 @@ class Base(TorrentProvider): return 'logout.php' in output.lower() loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'bithdtv', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'BiT-HDTV', + 'description': 'See BiT-HDTV', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py b/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py deleted file mode 100644 index d2b73913..00000000 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'bithdtv', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'BiT-HDTV', - 'description': 'See BiT-HDTV', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py similarity index 64% rename from couchpotato/core/media/_base/providers/torrent/bitsoup/main.py rename to couchpotato/core/media/_base/providers/torrent/bitsoup.py index 9a8c143f..fd2f4dbe 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup/main.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -85,3 +85,55 @@ class Base(TorrentProvider): return 'logout.php' in output.lower() loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'bitsoup', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'Bitsoup', + 'description': 'See Bitsoup', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py b/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py deleted file mode 100644 index efa34a77..00000000 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'bitsoup', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'Bitsoup', - 'description': 'See Bitsoup', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits/main.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py similarity index 55% rename from couchpotato/core/media/_base/providers/torrent/hdbits/main.py rename to couchpotato/core/media/_base/providers/torrent/hdbits.py index a1ceb573..4d31bb3e 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits/main.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -61,3 +61,53 @@ class Base(TorrentProvider): }) except: log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) + + +config = [{ + 'name': 'hdbits', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'HDBits', + 'description': 'See HDBits', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'passkey', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py b/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py deleted file mode 100644 index c9e75fe5..00000000 --- a/couchpotato/core/media/_base/providers/torrent/hdbits/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ - -config = [{ - 'name': 'hdbits', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'HDBits', - 'description': 'See HDBits', - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'passkey', - 'default': '', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py similarity index 72% rename from couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index 516cf07e..1f74db48 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -126,3 +126,60 @@ class Base(TorrentProvider): return 'logout.php' in output.lower() loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'ilovetorrents', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'ILoveTorrents', + 'description': 'Where the Love of Torrents is Born', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False + }, + { + 'name': 'username', + 'label': 'Username', + 'type': 'string', + 'default': '', + 'description': 'The user name for your ILT account', + }, + { + 'name': 'password', + 'label': 'Password', + 'type': 'password', + 'default': '', + 'description': 'The password for your ILT account.', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + } + ] +}] diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py deleted file mode 100644 index 2a9f403d..00000000 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ - -config = [{ - 'name': 'ilovetorrents', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'ILoveTorrents', - 'description': 'Where the Love of Torrents is Born', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False - }, - { - 'name': 'username', - 'label': 'Username', - 'type': 'string', - 'default': '', - 'description': 'The user name for your ILT account', - }, - { - 'name': 'password', - 'label': 'Password', - 'type': 'password', - 'default': '', - 'description': 'The password for your ILT account.', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - } - ] -}] diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py similarity index 69% rename from couchpotato/core/media/_base/providers/torrent/iptorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/iptorrents.py index cfd568e2..71302f9e 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -109,3 +109,61 @@ class Base(TorrentProvider): def loginCheckSuccess(self, output): return '/logout.php' in output.lower() + + +config = [{ + 'name': 'iptorrents', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'IPTorrents', + 'description': 'See IPTorrents', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'freeleech', + 'default': 0, + 'type': 'bool', + 'description': 'Only search for [FreeLeech] torrents.', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py deleted file mode 100644 index 48f8219b..00000000 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ - -config = [{ - 'name': 'iptorrents', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'IPTorrents', - 'description': 'See IPTorrents', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'freeleech', - 'default': 0, - 'type': 'bool', - 'description': 'Only search for [FreeLeech] torrents.', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py similarity index 71% rename from couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py rename to couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 8b624652..f99ead0d 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -112,3 +112,52 @@ class Base(TorrentMagnetProvider): def correctProxy(self, data): return 'search query' in data.lower() + + +config = [{ + 'name': 'kickasstorrents', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'KickAssTorrents', + 'description': 'See KickAssTorrents', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': True, + }, + { + 'name': 'domain', + 'advanced': True, + 'label': 'Proxy server', + 'description': 'Domain for requests, keep empty to let CouchPotato pick.', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py deleted file mode 100644 index 92dcc0fc..00000000 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ - -config = [{ - 'name': 'kickasstorrents', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'KickAssTorrents', - 'description': 'See KickAssTorrents', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': True, - }, - { - 'name': 'domain', - 'advanced': True, - 'label': 'Proxy server', - 'description': 'Domain for requests, keep empty to let CouchPotato pick.', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py similarity index 69% rename from couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py rename to couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index a7975e29..cb845bfb 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/main.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -176,3 +176,89 @@ class Base(TorrentProvider): return False loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'passthepopcorn', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'PassThePopcorn', + 'description': 'See PassThePopcorn.me', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False + }, + { + 'name': 'domain', + 'advanced': True, + 'label': 'Proxy server', + 'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).', + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'passkey', + 'default': '', + }, + { + 'name': 'prefer_golden', + 'advanced': True, + 'type': 'bool', + 'label': 'Prefer golden', + 'default': 1, + 'description': 'Favors Golden Popcorn-releases over all other releases.' + }, + { + 'name': 'prefer_scene', + 'advanced': True, + 'type': 'bool', + 'label': 'Prefer scene', + 'default': 0, + 'description': 'Favors scene-releases over non-scene releases.' + }, + { + 'name': 'require_approval', + 'advanced': True, + 'type': 'bool', + 'label': 'Require approval', + 'default': 0, + 'description': 'Require staff-approval for releases to be accepted.' + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + } + ] +}] diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py deleted file mode 100644 index 119c4255..00000000 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ - -config = [{ - 'name': 'passthepopcorn', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'PassThePopcorn', - 'description': 'See PassThePopcorn.me', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False - }, - { - 'name': 'domain', - 'advanced': True, - 'label': 'Proxy server', - 'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).', - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'passkey', - 'default': '', - }, - { - 'name': 'prefer_golden', - 'advanced': True, - 'type': 'bool', - 'label': 'Prefer golden', - 'default': 1, - 'description': 'Favors Golden Popcorn-releases over all other releases.' - }, - { - 'name': 'prefer_scene', - 'advanced': True, - 'type': 'bool', - 'label': 'Prefer scene', - 'default': 0, - 'description': 'Favors scene-releases over non-scene releases.' - }, - { - 'name': 'require_approval', - 'advanced': True, - 'type': 'bool', - 'label': 'Require approval', - 'default': 0, - 'description': 'Require staff-approval for releases to be accepted.' - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - } - ] -}] diff --git a/couchpotato/core/media/_base/providers/torrent/publichd/main.py b/couchpotato/core/media/_base/providers/torrent/publichd.py similarity index 68% rename from couchpotato/core/media/_base/providers/torrent/publichd/main.py rename to couchpotato/core/media/_base/providers/torrent/publichd.py index ad2035ed..39e7f60a 100644 --- a/couchpotato/core/media/_base/providers/torrent/publichd/main.py +++ b/couchpotato/core/media/_base/providers/torrent/publichd.py @@ -89,3 +89,46 @@ class Base(TorrentMagnetProvider): item['description'] = description return item + + +config = [{ + 'name': 'publichd', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'PublicHD', + 'description': 'Public Torrent site with only HD content. See PublicHD', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': True, + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py b/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py deleted file mode 100644 index d24cc68e..00000000 --- a/couchpotato/core/media/_base/providers/torrent/publichd/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ - -config = [{ - 'name': 'publichd', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'PublicHD', - 'description': 'Public Torrent site with only HD content. See PublicHD', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': True, - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py similarity index 65% rename from couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py rename to couchpotato/core/media/_base/providers/torrent/sceneaccess.py index 29abbb6d..a8ee615e 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -78,3 +78,55 @@ class Base(TorrentProvider): return '/inbox' in output.lower() loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'sceneaccess', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'SceneAccess', + 'description': 'See SceneAccess', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py deleted file mode 100644 index 68592a37..00000000 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'sceneaccess', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'SceneAccess', - 'description': 'See SceneAccess', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py similarity index 71% rename from couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py rename to couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 64995628..f8e631d8 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -111,3 +111,52 @@ class Base(TorrentMagnetProvider): item['description'] = description return item + + +config = [{ + 'name': 'thepiratebay', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'ThePirateBay', + 'description': 'The world\'s largest bittorrent tracker. See ThePirateBay', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False + }, + { + 'name': 'domain', + 'advanced': True, + 'label': 'Proxy server', + 'description': 'Domain for requests, keep empty to let CouchPotato pick.', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + } + ] +}] diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py deleted file mode 100644 index 047562a5..00000000 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ - -config = [{ - 'name': 'thepiratebay', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'ThePirateBay', - 'description': 'The world\'s largest bittorrent tracker. See ThePirateBay', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False - }, - { - 'name': 'domain', - 'advanced': True, - 'label': 'Proxy server', - 'description': 'Domain for requests, keep empty to let CouchPotato pick.', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - } - ] -}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py similarity index 63% rename from couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentbytes.py index 4b89db1e..603cd6ba 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -80,3 +80,54 @@ class Base(TorrentProvider): loginCheckSuccess = loginSuccess + +config = [{ + 'name': 'torrentbytes', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentBytes', + 'description': 'See TorrentBytes', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py deleted file mode 100644 index 46eb52eb..00000000 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'torrentbytes', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'TorrentBytes', - 'description': 'See TorrentBytes', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday/main.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py similarity index 53% rename from couchpotato/core/media/_base/providers/torrent/torrentday/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentday.py index eebfaed2..dfb605bd 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -59,3 +59,55 @@ class Base(TorrentProvider): def loginCheckSuccess(self, output): return 'logout.php' in output.lower() + + +config = [{ + 'name': 'torrentday', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentDay', + 'description': 'See TorrentDay', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py deleted file mode 100644 index 5067590d..00000000 --- a/couchpotato/core/media/_base/providers/torrent/torrentday/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'torrentday', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'TorrentDay', - 'description': 'See TorrentDay', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py similarity index 61% rename from couchpotato/core/media/_base/providers/torrent/torrentleech/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentleech.py index 1c907f4e..36bd0f70 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -69,3 +69,55 @@ class Base(TorrentProvider): return '/user/account/logout' in output.lower() or 'welcome back' in output.lower() loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'torrentleech', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentLeech', + 'description': 'See TorrentLeech', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py deleted file mode 100644 index 2cdbd57b..00000000 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ - -config = [{ - 'name': 'torrentleech', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'TorrentLeech', - 'description': 'See TorrentLeech', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 20, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py similarity index 65% rename from couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentpotato.py index b86ca6be..00ed433f 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py @@ -119,3 +119,66 @@ class Base(TorrentProvider): return False return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use']) + + +config = [{ + 'name': 'torrentpotato', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentPotato', + 'order': 10, + 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'use', + 'default': '' + }, + { + 'name': 'host', + 'default': '', + 'description': 'The url path of your TorrentPotato provider.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': '0', + 'description': 'Starting score for each release found via this provider.', + }, + { + 'name': 'name', + 'label': 'Username', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'default': '1', + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'default': '40', + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'pass_key', + 'default': ',', + 'label': 'Pass Key', + 'description': 'Can be found on your profile page', + 'type': 'combined', + 'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py deleted file mode 100644 index 50e76029..00000000 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ - -config = [{ - 'name': 'torrentpotato', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'TorrentPotato', - 'order': 10, - 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'use', - 'default': '' - }, - { - 'name': 'host', - 'default': '', - 'description': 'The url path of your TorrentPotato provider.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'default': '0', - 'description': 'Starting score for each release found via this provider.', - }, - { - 'name': 'name', - 'label': 'Username', - 'default': '', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'default': '1', - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'default': '40', - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'pass_key', - 'default': ',', - 'label': 'Pass Key', - 'description': 'Can be found on your profile page', - 'type': 'combined', - 'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'], - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py similarity index 58% rename from couchpotato/core/media/_base/providers/torrent/torrentshack/main.py rename to couchpotato/core/media/_base/providers/torrent/torrentshack.py index 21776a27..cc2a599d 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack/main.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -69,3 +69,60 @@ class Base(TorrentProvider): def getSceneOnly(self): return '1' if self.conf('scene_only') else '' + + +config = [{ + 'name': 'torrentshack', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentShack', + 'description': 'See TorrentShack', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'scene_only', + 'type': 'bool', + 'default': False, + 'description': 'Only allow scene releases.' + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py b/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py deleted file mode 100644 index 50edbf55..00000000 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ - -config = [{ - 'name': 'torrentshack', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'TorrentShack', - 'description': 'See TorrentShack', - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': False, - }, - { - 'name': 'username', - 'default': '', - }, - { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'scene_only', - 'type': 'bool', - 'default': False, - 'description': 'Only allow scene releases.' - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/_base/providers/torrent/yify/main.py b/couchpotato/core/media/_base/providers/torrent/yify.py similarity index 54% rename from couchpotato/core/media/_base/providers/torrent/yify/main.py rename to couchpotato/core/media/_base/providers/torrent/yify.py index 028554d0..3ba287ac 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify/main.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -29,7 +29,7 @@ class Base(TorrentProvider): if not quality.get('hd', False): return [] - return super(Yify, self).search(movie, quality) + return super(Base, self).search(movie, quality) def _search(self, movie, quality, results): @@ -64,3 +64,52 @@ class Base(TorrentProvider): def correctProxy(self, data): data = data.lower() return 'yify' in data and 'yts' in data + + +config = [{ + 'name': 'yify', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'Yify', + 'description': 'Free provider, less accurate. Small HD movies, encoded by Yify.', + 'wizard': False, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': 0 + }, + { + 'name': 'domain', + 'advanced': True, + 'label': 'Proxy server', + 'description': 'Domain for requests, keep empty to let CouchPotato pick.', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + } + ] +}] diff --git a/couchpotato/core/media/_base/providers/torrent/yify/__init__.py b/couchpotato/core/media/_base/providers/torrent/yify/__init__.py deleted file mode 100644 index 9623eb5d..00000000 --- a/couchpotato/core/media/_base/providers/torrent/yify/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ - -config = [{ - 'name': 'yify', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'Yify', - 'description': 'Free provider, less accurate. Small HD movies, encoded by Yify.', - 'wizard': False, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': 0 - }, - { - 'name': 'domain', - 'advanced': True, - 'label': 'Proxy server', - 'description': 'Domain for requests, keep empty to let CouchPotato pick.', - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - } - ] -}] diff --git a/couchpotato/core/media/_base/search/__init__.py b/couchpotato/core/media/_base/search/__init__.py index 09bc84ef..c23fdb72 100644 --- a/couchpotato/core/media/_base/search/__init__.py +++ b/couchpotato/core/media/_base/search/__init__.py @@ -1,7 +1,5 @@ from .main import Search -def start(): +def autoload(): return Search() - -config = [] diff --git a/couchpotato/core/media/_base/searcher/__init__.py b/couchpotato/core/media/_base/searcher/__init__.py index 72c7d6ef..bf69b950 100644 --- a/couchpotato/core/media/_base/searcher/__init__.py +++ b/couchpotato/core/media/_base/searcher/__init__.py @@ -1,7 +1,7 @@ from .main import Searcher -def start(): +def autoload(): return Searcher() config = [{ diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py index 8c98c10c..68c259ac 100644 --- a/couchpotato/core/media/movie/providers/automation/base.py +++ b/couchpotato/core/media/movie/providers/automation/base.py @@ -1,6 +1,6 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.automation.base import BaseAutomation +from couchpotato.core.media._base.providers.automation.base import AutomationBase from couchpotato.environment import Env from couchpotato.core.helpers.variable import splitString import time @@ -8,7 +8,7 @@ import time log = CPLog(__name__) -class Automation(BaseAutomation): +class Automation(AutomationBase): enabled_option = 'automation_enabled' http_time_between_calls = 2 From 4513f03e8f2b8f2bd92c306fff70f7899a153bda Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 11 Mar 2014 23:31:37 +0100 Subject: [PATCH 080/468] Move movie to single file --- .../core/media/_base/providers/base.py | 1 + .../core/media/movie/_base/__init__.py | 2 +- .../automation/{bluray/main.py => bluray.py} | 30 ++++++++ .../providers/automation/bluray/__init__.py | 32 -------- .../{flixster/main.py => flixster.py} | 33 +++++++++ .../providers/automation/flixster/__init__.py | 35 --------- .../{goodfilms/main.py => goodfilms.py} | 27 +++++++ .../automation/goodfilms/__init__.py | 29 -------- .../automation/{imdb/main.py => imdb.py} | 68 +++++++++++++++++ .../providers/automation/imdb/__init__.py | 70 ------------------ .../automation/{itunes/main.py => itunes.py} | 35 +++++++++ .../providers/automation/itunes/__init__.py | 37 ---------- .../{kinepolis/main.py => kinepolis.py} | 23 ++++++ .../automation/kinepolis/__init__.py | 25 ------- .../{letterboxd/main.py => letterboxd.py} | 33 +++++++++ .../automation/letterboxd/__init__.py | 35 --------- .../{moviemeter/main.py => moviemeter.py} | 23 ++++++ .../automation/moviemeter/__init__.py | 25 ------- .../{movies_io/main.py => movies_io.py} | 33 +++++++++ .../automation/movies_io/__init__.py | 35 --------- .../main.py => rottentomatoes.py} | 41 ++++++++++ .../automation/rottentomatoes/__init__.py | 43 ----------- .../automation/{trakt/main.py => trakt.py} | 37 ++++++++++ .../providers/automation/trakt/__init__.py | 39 ---------- .../info/{_modifier/main.py => _modifier.py} | 2 + .../providers/info/_modifier/__init__.py | 7 -- .../main.py => couchpotatoapi.py} | 2 + .../providers/info/couchpotatoapi/__init__.py | 7 -- .../info/{omdbapi/main.py => omdbapi.py} | 2 + .../movie/providers/info/omdbapi/__init__.py | 7 -- .../{themoviedb/main.py => themoviedb.py} | 24 +++++- .../providers/info/themoviedb/__init__.py | 25 ------- .../metadata/{wmc/__init__.py => wmc.py} | 12 ++- .../movie/providers/metadata/wmc/main.py | 8 -- .../metadata/{xbmc/main.py => xbmc.py} | 69 +++++++++++++++++ .../movie/providers/metadata/xbmc/__init__.py | 70 ------------------ .../media/movie/providers/nzb/__init__.py | 14 ---- .../nzb/{binsearch/main.py => binsearch.py} | 0 .../movie/providers/nzb/binsearch/__init__.py | 7 -- .../nzb/{newznab/main.py => newznab.py} | 0 .../movie/providers/nzb/newznab/__init__.py | 7 -- .../nzb/{nzbclub/main.py => nzbclub.py} | 0 .../movie/providers/nzb/nzbclub/__init__.py | 7 -- .../nzb/{nzbindex/main.py => nzbindex.py} | 0 .../movie/providers/nzb/nzbindex/__init__.py | 7 -- .../nzb/{omgwtfnzbs/main.py => omgwtfnzbs.py} | 0 .../providers/nzb/omgwtfnzbs/__init__.py | 7 -- .../media/movie/providers/torrent/__init__.py | 14 ---- .../{awesomehd/main.py => awesomehd.py} | 2 + .../providers/torrent/awesomehd/__init__.py | 8 -- .../torrent/{bithdtv/main.py => bithdtv.py} | 2 + .../providers/torrent/bithdtv/__init__.py | 7 -- .../torrent/{bitsoup/main.py => bitsoup.py} | 2 + .../providers/torrent/bitsoup/__init__.py | 7 -- .../torrent/{hdbits/main.py => hdbits.py} | 2 + .../providers/torrent/hdbits/__init__.py | 7 -- .../main.py => ilovetorrents.py} | 2 + .../torrent/ilovetorrents/__init__.py | 7 -- .../{iptorrents/main.py => iptorrents.py} | 0 .../providers/torrent/iptorrents/__init__.py | 7 -- .../main.py => kickasstorrents.py} | 0 .../torrent/kickasstorrents/__init__.py | 7 -- .../main.py => passthepopcorn.py} | 0 .../torrent/passthepopcorn/__init__.py | 7 -- .../torrent/{publichd/main.py => publichd.py} | 2 + .../providers/torrent/publichd/__init__.py | 7 -- .../{sceneaccess/main.py => sceneaccess.py} | 2 + .../providers/torrent/sceneaccess/__init__.py | 7 -- .../{thepiratebay/main.py => thepiratebay.py} | 2 + .../torrent/thepiratebay/__init__.py | 7 -- .../{torrentbytes/main.py => torrentbytes.py} | 2 + .../torrent/torrentbytes/__init__.py | 7 -- .../{torrentday/main.py => torrentday.py} | 2 + .../providers/torrent/torrentday/__init__.py | 7 -- .../{torrentleech/main.py => torrentleech.py} | 2 + .../torrent/torrentleech/__init__.py | 7 -- .../main.py => torrentpotato.py} | 2 + .../torrent/torrentpotato/__init__.py | 7 -- .../{torrentshack/main.py => torrentshack.py} | 2 + .../torrent/torrentshack/__init__.py | 7 -- .../torrent/{yify/main.py => yify.py} | 2 + .../movie/providers/torrent/yify/__init__.py | 7 -- .../{hdtrailers/main.py => hdtrailers.py} | 2 + .../providers/trailer/hdtrailers/__init__.py | 7 -- .../{allocine/main.py => allocine.py} | 2 + .../providers/userscript/allocine/__init__.py | 5 -- .../main.py => appletrailers.py} | 2 + .../userscript/appletrailers/__init__.py | 5 -- .../{criticker/main.py => criticker.py} | 2 + .../userscript/criticker/__init__.py | 5 -- .../{filmweb/main.py => filmweb.py} | 2 + .../providers/userscript/filmweb/__init__.py | 5 -- .../{flickchart/main.py => flickchart.py} | 2 + .../userscript/flickchart/__init__.py | 5 -- .../userscript/{imdb/main.py => imdb.py} | 3 +- .../providers/userscript/imdb/__init__.py | 5 -- .../{letterboxd/main.py => letterboxd.py} | 2 + .../userscript/letterboxd/__init__.py | 5 -- .../{moviemeter/main.py => moviemeter.py} | 2 + .../userscript/moviemeter/__init__.py | 5 -- .../{moviesio/main.py => moviesio.py} | 2 + .../providers/userscript/moviesio/__init__.py | 5 -- .../userscript/{reddit/main.py => reddit.py} | 2 + .../providers/userscript/reddit/__init__.py | 5 -- .../main.py => rottentomatoes.py} | 2 + .../userscript/rottentomatoes/__init__.py | 5 -- .../{sharethe/main.py => sharethe.py} | 2 + .../providers/userscript/sharethe/__init__.py | 5 -- .../userscript/{tmdb/main.py => tmdb.py} | 2 + .../providers/userscript/tmdb/__init__.py | 5 -- .../userscript/{trakt/main.py => trakt.py} | 2 + .../providers/userscript/trakt/__init__.py | 5 -- .../userscript/{whiwa/main.py => whiwa.py} | 2 + .../providers/userscript/whiwa/__init__.py | 5 -- .../{youteather/main.py => youteather.py} | 3 + .../userscript/youteather/__init__.py | 5 -- .../movie/{searcher/main.py => searcher.py} | 69 +++++++++++++++++ .../core/media/movie/searcher/__init__.py | 74 ------------------- 118 files changed, 624 insertions(+), 879 deletions(-) rename couchpotato/core/media/movie/providers/automation/{bluray/main.py => bluray.py} (75%) delete mode 100644 couchpotato/core/media/movie/providers/automation/bluray/__init__.py rename couchpotato/core/media/movie/providers/automation/{flixster/main.py => flixster.py} (58%) delete mode 100644 couchpotato/core/media/movie/providers/automation/flixster/__init__.py rename couchpotato/core/media/movie/providers/automation/{goodfilms/main.py => goodfilms.py} (66%) delete mode 100644 couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py rename couchpotato/core/media/movie/providers/automation/{imdb/main.py => imdb.py} (66%) delete mode 100644 couchpotato/core/media/movie/providers/automation/imdb/__init__.py rename couchpotato/core/media/movie/providers/automation/{itunes/main.py => itunes.py} (65%) delete mode 100644 couchpotato/core/media/movie/providers/automation/itunes/__init__.py rename couchpotato/core/media/movie/providers/automation/{kinepolis/main.py => kinepolis.py} (57%) delete mode 100644 couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py rename couchpotato/core/media/movie/providers/automation/{letterboxd/main.py => letterboxd.py} (59%) delete mode 100644 couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py rename couchpotato/core/media/movie/providers/automation/{moviemeter/main.py => moviemeter.py} (58%) delete mode 100644 couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py rename couchpotato/core/media/movie/providers/automation/{movies_io/main.py => movies_io.py} (54%) delete mode 100644 couchpotato/core/media/movie/providers/automation/movies_io/__init__.py rename couchpotato/core/media/movie/providers/automation/{rottentomatoes/main.py => rottentomatoes.py} (57%) delete mode 100644 couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py rename couchpotato/core/media/movie/providers/automation/{trakt/main.py => trakt.py} (55%) delete mode 100644 couchpotato/core/media/movie/providers/automation/trakt/__init__.py rename couchpotato/core/media/movie/providers/info/{_modifier/main.py => _modifier.py} (98%) delete mode 100644 couchpotato/core/media/movie/providers/info/_modifier/__init__.py rename couchpotato/core/media/movie/providers/info/{couchpotatoapi/main.py => couchpotatoapi.py} (99%) delete mode 100644 couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py rename couchpotato/core/media/movie/providers/info/{omdbapi/main.py => omdbapi.py} (99%) delete mode 100644 couchpotato/core/media/movie/providers/info/omdbapi/__init__.py rename couchpotato/core/media/movie/providers/info/{themoviedb/main.py => themoviedb.py} (92%) delete mode 100644 couchpotato/core/media/movie/providers/info/themoviedb/__init__.py rename couchpotato/core/media/movie/providers/metadata/{wmc/__init__.py => wmc.py} (66%) delete mode 100644 couchpotato/core/media/movie/providers/metadata/wmc/main.py rename couchpotato/core/media/movie/providers/metadata/{xbmc/main.py => xbmc.py} (74%) delete mode 100644 couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py rename couchpotato/core/media/movie/providers/nzb/{binsearch/main.py => binsearch.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py rename couchpotato/core/media/movie/providers/nzb/{newznab/main.py => newznab.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/nzb/newznab/__init__.py rename couchpotato/core/media/movie/providers/nzb/{nzbclub/main.py => nzbclub.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py rename couchpotato/core/media/movie/providers/nzb/{nzbindex/main.py => nzbindex.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py rename couchpotato/core/media/movie/providers/nzb/{omgwtfnzbs/main.py => omgwtfnzbs.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py rename couchpotato/core/media/movie/providers/torrent/{awesomehd/main.py => awesomehd.py} (91%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py rename couchpotato/core/media/movie/providers/torrent/{bithdtv/main.py => bithdtv.py} (96%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py rename couchpotato/core/media/movie/providers/torrent/{bitsoup/main.py => bitsoup.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py rename couchpotato/core/media/movie/providers/torrent/{hdbits/main.py => hdbits.py} (92%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py rename couchpotato/core/media/movie/providers/torrent/{ilovetorrents/main.py => ilovetorrents.py} (90%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py rename couchpotato/core/media/movie/providers/torrent/{iptorrents/main.py => iptorrents.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py rename couchpotato/core/media/movie/providers/torrent/{kickasstorrents/main.py => kickasstorrents.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py rename couchpotato/core/media/movie/providers/torrent/{passthepopcorn/main.py => passthepopcorn.py} (100%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py rename couchpotato/core/media/movie/providers/torrent/{publichd/main.py => publichd.py} (94%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/publichd/__init__.py rename couchpotato/core/media/movie/providers/torrent/{sceneaccess/main.py => sceneaccess.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py rename couchpotato/core/media/movie/providers/torrent/{thepiratebay/main.py => thepiratebay.py} (96%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py rename couchpotato/core/media/movie/providers/torrent/{torrentbytes/main.py => torrentbytes.py} (90%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py rename couchpotato/core/media/movie/providers/torrent/{torrentday/main.py => torrentday.py} (95%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py rename couchpotato/core/media/movie/providers/torrent/{torrentleech/main.py => torrentleech.py} (96%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py rename couchpotato/core/media/movie/providers/torrent/{torrentpotato/main.py => torrentpotato.py} (95%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py rename couchpotato/core/media/movie/providers/torrent/{torrentshack/main.py => torrentshack.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py rename couchpotato/core/media/movie/providers/torrent/{yify/main.py => yify.py} (93%) delete mode 100644 couchpotato/core/media/movie/providers/torrent/yify/__init__.py rename couchpotato/core/media/movie/providers/trailer/{hdtrailers/main.py => hdtrailers.py} (99%) delete mode 100644 couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py rename couchpotato/core/media/movie/providers/userscript/{allocine/main.py => allocine.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/allocine/__init__.py rename couchpotato/core/media/movie/providers/userscript/{appletrailers/main.py => appletrailers.py} (95%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py rename couchpotato/core/media/movie/providers/userscript/{criticker/main.py => criticker.py} (87%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/criticker/__init__.py rename couchpotato/core/media/movie/providers/userscript/{filmweb/main.py => filmweb.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py rename couchpotato/core/media/movie/providers/userscript/{flickchart/main.py => flickchart.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py rename couchpotato/core/media/movie/providers/userscript/{imdb/main.py => imdb.py} (87%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/imdb/__init__.py rename couchpotato/core/media/movie/providers/userscript/{letterboxd/main.py => letterboxd.py} (86%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py rename couchpotato/core/media/movie/providers/userscript/{moviemeter/main.py => moviemeter.py} (88%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py rename couchpotato/core/media/movie/providers/userscript/{moviesio/main.py => moviesio.py} (87%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py rename couchpotato/core/media/movie/providers/userscript/{reddit/main.py => reddit.py} (96%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/reddit/__init__.py rename couchpotato/core/media/movie/providers/userscript/{rottentomatoes/main.py => rottentomatoes.py} (97%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py rename couchpotato/core/media/movie/providers/userscript/{sharethe/main.py => sharethe.py} (89%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py rename couchpotato/core/media/movie/providers/userscript/{tmdb/main.py => tmdb.py} (96%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py rename couchpotato/core/media/movie/providers/userscript/{trakt/main.py => trakt.py} (92%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/trakt/__init__.py rename couchpotato/core/media/movie/providers/userscript/{whiwa/main.py => whiwa.py} (89%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py rename couchpotato/core/media/movie/providers/userscript/{youteather/main.py => youteather.py} (95%) delete mode 100644 couchpotato/core/media/movie/providers/userscript/youteather/__init__.py rename couchpotato/core/media/movie/{searcher/main.py => searcher.py} (82%) delete mode 100644 couchpotato/core/media/movie/searcher/__init__.py diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 761aedca..95ab1284 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -14,6 +14,7 @@ import xml.etree.ElementTree as XMLTree log = CPLog(__name__) + class MultiProvider(Plugin): def __init__(self): diff --git a/couchpotato/core/media/movie/_base/__init__.py b/couchpotato/core/media/movie/_base/__init__.py index 22211332..acec594f 100644 --- a/couchpotato/core/media/movie/_base/__init__.py +++ b/couchpotato/core/media/movie/_base/__init__.py @@ -1,7 +1,7 @@ from .main import MovieBase -def start(): +def autoload(): return MovieBase() config = [] diff --git a/couchpotato/core/media/movie/providers/automation/bluray/main.py b/couchpotato/core/media/movie/providers/automation/bluray.py similarity index 75% rename from couchpotato/core/media/movie/providers/automation/bluray/main.py rename to couchpotato/core/media/movie/providers/automation/bluray.py index 2e8ebff7..b411900d 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray/main.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'Bluray' + class Bluray(Automation, RSS): @@ -73,3 +75,31 @@ class Bluray(Automation, RSS): movies.append(imdb['imdb']) return movies + + +config = [{ + 'name': 'bluray', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'bluray_automation', + 'label': 'Blu-ray.com', + 'description': 'Imports movies from blu-ray.com.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'backlog', + 'advanced': True, + 'description': 'Parses the history until the minimum movie year is reached. (Will be disabled once it has completed)', + 'default': False, + 'type': 'bool', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/bluray/__init__.py b/couchpotato/core/media/movie/providers/automation/bluray/__init__.py deleted file mode 100644 index 519a7119..00000000 --- a/couchpotato/core/media/movie/providers/automation/bluray/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from .main import Bluray - - -def start(): - return Bluray() - -config = [{ - 'name': 'bluray', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'bluray_automation', - 'label': 'Blu-ray.com', - 'description': 'Imports movies from blu-ray.com.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'backlog', - 'advanced': True, - 'description': 'Parses the history until the minimum movie year is reached. (Will be disabled once it has completed)', - 'default': False, - 'type': 'bool', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/flixster/main.py b/couchpotato/core/media/movie/providers/automation/flixster.py similarity index 58% rename from couchpotato/core/media/movie/providers/automation/flixster/main.py rename to couchpotato/core/media/movie/providers/automation/flixster.py index cebe89dc..53a06d76 100644 --- a/couchpotato/core/media/movie/providers/automation/flixster/main.py +++ b/couchpotato/core/media/movie/providers/automation/flixster.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'Flixster' + class Flixster(Automation): @@ -48,3 +50,34 @@ class Flixster(Automation): }) return movies + + +config = [{ + 'name': 'flixster', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'flixster_automation', + 'label': 'Flixster', + 'description': 'Import movies from any public Flixster watchlist', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_ids_use', + 'label': 'Use', + }, + { + 'name': 'automation_ids', + 'label': 'User ID', + 'type': 'combined', + 'combine': ['automation_ids_use', 'automation_ids'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/flixster/__init__.py b/couchpotato/core/media/movie/providers/automation/flixster/__init__.py deleted file mode 100644 index 71bd83c0..00000000 --- a/couchpotato/core/media/movie/providers/automation/flixster/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from .main import Flixster - - -def start(): - return Flixster() - -config = [{ - 'name': 'flixster', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'flixster_automation', - 'label': 'Flixster', - 'description': 'Import movies from any public Flixster watchlist', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_ids_use', - 'label': 'Use', - }, - { - 'name': 'automation_ids', - 'label': 'User ID', - 'type': 'combined', - 'combine': ['automation_ids_use', 'automation_ids'], - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/goodfilms/main.py b/couchpotato/core/media/movie/providers/automation/goodfilms.py similarity index 66% rename from couchpotato/core/media/movie/providers/automation/goodfilms/main.py rename to couchpotato/core/media/movie/providers/automation/goodfilms.py index 3ab51e04..2524f661 100644 --- a/couchpotato/core/media/movie/providers/automation/goodfilms/main.py +++ b/couchpotato/core/media/movie/providers/automation/goodfilms.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'Goodfilms' + class Goodfilms(Automation): @@ -52,3 +54,28 @@ class Goodfilms(Automation): page += 1 return movies + + +config = [{ + 'name': 'goodfilms', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'goodfilms_automation', + 'label': 'Goodfilms', + 'description': 'import movies from your Goodfilms queue', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_username', + 'label': 'Username', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py b/couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py deleted file mode 100644 index e04ccd0d..00000000 --- a/couchpotato/core/media/movie/providers/automation/goodfilms/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from .main import Goodfilms - - -def start(): - return Goodfilms() - -config = [{ - 'name': 'goodfilms', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'goodfilms_automation', - 'label': 'Goodfilms', - 'description': 'import movies from your Goodfilms queue', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_username', - 'label': 'Username', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/imdb/main.py b/couchpotato/core/media/movie/providers/automation/imdb.py similarity index 66% rename from couchpotato/core/media/movie/providers/automation/imdb/main.py rename to couchpotato/core/media/movie/providers/automation/imdb.py index 9b25e180..d5b92259 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb/main.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -13,6 +13,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'IMDB' + class IMDB(MultiProvider): @@ -141,3 +143,69 @@ class IMDBAutomation(IMDBBase): log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) return movies + + +config = [{ + 'name': 'imdb', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'imdb_automation_watchlist', + 'label': 'IMDB', + 'description': 'From any public IMDB watchlists.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + }, + ], + }, + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'imdb_automation_charts', + 'label': 'IMDB', + 'description': 'Import movies from IMDB Charts', + 'options': [ + { + 'name': 'automation_providers_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_charts_theater', + 'type': 'bool', + 'label': 'In Theaters', + 'description': 'New Movies In-Theaters chart', + 'default': True, + }, + { + 'name': 'automation_charts_top250', + 'type': 'bool', + 'label': 'TOP 250', + 'description': 'IMDB TOP 250 chart', + 'default': True, + }, + { + 'name': 'automation_charts_boxoffice', + 'type': 'bool', + 'label': 'Box office TOP 10', + 'description': 'IMDB Box office TOP 10 chart', + 'default': True, + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/imdb/__init__.py b/couchpotato/core/media/movie/providers/automation/imdb/__init__.py deleted file mode 100644 index f9baabf2..00000000 --- a/couchpotato/core/media/movie/providers/automation/imdb/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from .main import IMDB - - -def start(): - return IMDB() - -config = [{ - 'name': 'imdb', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'imdb_automation_watchlist', - 'label': 'IMDB', - 'description': 'From any public IMDB watchlists.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_urls_use', - 'label': 'Use', - }, - { - 'name': 'automation_urls', - 'label': 'url', - 'type': 'combined', - 'combine': ['automation_urls_use', 'automation_urls'], - }, - ], - }, - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'imdb_automation_charts', - 'label': 'IMDB', - 'description': 'Import movies from IMDB Charts', - 'options': [ - { - 'name': 'automation_providers_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_charts_theater', - 'type': 'bool', - 'label': 'In Theaters', - 'description': 'New Movies In-Theaters chart', - 'default': True, - }, - { - 'name': 'automation_charts_top250', - 'type': 'bool', - 'label': 'TOP 250', - 'description': 'IMDB TOP 250 chart', - 'default': True, - }, - { - 'name': 'automation_charts_boxoffice', - 'type': 'bool', - 'label': 'Box office TOP 10', - 'description': 'IMDB Box office TOP 10 chart', - 'default': True, - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/itunes/main.py b/couchpotato/core/media/movie/providers/automation/itunes.py similarity index 65% rename from couchpotato/core/media/movie/providers/automation/itunes/main.py rename to couchpotato/core/media/movie/providers/automation/itunes.py index 3257a668..1feb96fa 100644 --- a/couchpotato/core/media/movie/providers/automation/itunes/main.py +++ b/couchpotato/core/media/movie/providers/automation/itunes.py @@ -9,6 +9,8 @@ import xml.etree.ElementTree as XMLTree log = CPLog(__name__) +autoload = 'ITunes' + class ITunes(Automation, RSS): @@ -58,3 +60,36 @@ class ITunes(Automation, RSS): log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc())) return movies + + +config = [{ + 'name': 'itunes', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'itunes_automation', + 'label': 'iTunes', + 'description': 'From any iTunes Store feed. Url should be the RSS link.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + 'default': ',', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + 'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/itunes/__init__.py b/couchpotato/core/media/movie/providers/automation/itunes/__init__.py deleted file mode 100644 index 13526f43..00000000 --- a/couchpotato/core/media/movie/providers/automation/itunes/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from .main import ITunes - - -def start(): - return ITunes() - -config = [{ - 'name': 'itunes', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'itunes_automation', - 'label': 'iTunes', - 'description': 'From any iTunes Store feed. Url should be the RSS link.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_urls_use', - 'label': 'Use', - 'default': ',', - }, - { - 'name': 'automation_urls', - 'label': 'url', - 'type': 'combined', - 'combine': ['automation_urls_use', 'automation_urls'], - 'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/kinepolis/main.py b/couchpotato/core/media/movie/providers/automation/kinepolis.py similarity index 57% rename from couchpotato/core/media/movie/providers/automation/kinepolis/main.py rename to couchpotato/core/media/movie/providers/automation/kinepolis.py index a1e099e2..93505723 100644 --- a/couchpotato/core/media/movie/providers/automation/kinepolis/main.py +++ b/couchpotato/core/media/movie/providers/automation/kinepolis.py @@ -5,6 +5,8 @@ import datetime log = CPLog(__name__) +autoload = 'Kinepolis' + class Kinepolis(Automation, RSS): @@ -27,3 +29,24 @@ class Kinepolis(Automation, RSS): movies.append(imdb['imdb']) return movies + + +config = [{ + 'name': 'kinepolis', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'kinepolis_automation', + 'label': 'Kinepolis', + 'description': 'Imports movies from the current top 10 of kinepolis.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py b/couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py deleted file mode 100644 index cc4c5706..00000000 --- a/couchpotato/core/media/movie/providers/automation/kinepolis/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .main import Kinepolis - - -def start(): - return Kinepolis() - -config = [{ - 'name': 'kinepolis', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'kinepolis_automation', - 'label': 'Kinepolis', - 'description': 'Imports movies from the current top 10 of kinepolis.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/letterboxd/main.py b/couchpotato/core/media/movie/providers/automation/letterboxd.py similarity index 59% rename from couchpotato/core/media/movie/providers/automation/letterboxd/main.py rename to couchpotato/core/media/movie/providers/automation/letterboxd.py index 70e574f1..55a95498 100644 --- a/couchpotato/core/media/movie/providers/automation/letterboxd/main.py +++ b/couchpotato/core/media/movie/providers/automation/letterboxd.py @@ -6,6 +6,8 @@ import re log = CPLog(__name__) +autoload = 'Letterboxd' + class Letterboxd(Automation): @@ -49,3 +51,34 @@ class Letterboxd(Automation): movies.append({'title': match[0], 'year': match[1] }) return movies + + +config = [{ + 'name': 'letterboxd', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'letterboxd_automation', + 'label': 'Letterboxd', + 'description': 'Import movies from any public Letterboxd watchlist', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + }, + { + 'name': 'automation_urls', + 'label': 'Username', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py b/couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py deleted file mode 100644 index 88bfe6a1..00000000 --- a/couchpotato/core/media/movie/providers/automation/letterboxd/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from .main import Letterboxd - - -def start(): - return Letterboxd() - -config = [{ - 'name': 'letterboxd', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'letterboxd_automation', - 'label': 'Letterboxd', - 'description': 'Import movies from any public Letterboxd watchlist', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_urls_use', - 'label': 'Use', - }, - { - 'name': 'automation_urls', - 'label': 'Username', - 'type': 'combined', - 'combine': ['automation_urls_use', 'automation_urls'], - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/moviemeter/main.py b/couchpotato/core/media/movie/providers/automation/moviemeter.py similarity index 58% rename from couchpotato/core/media/movie/providers/automation/moviemeter/main.py rename to couchpotato/core/media/movie/providers/automation/moviemeter.py index 890c8bc0..883fcfa1 100644 --- a/couchpotato/core/media/movie/providers/automation/moviemeter/main.py +++ b/couchpotato/core/media/movie/providers/automation/moviemeter.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'Moviemeter' + class Moviemeter(Automation, RSS): @@ -26,3 +28,24 @@ class Moviemeter(Automation, RSS): movies.append(imdb['imdb']) return movies + + +config = [{ + 'name': 'moviemeter', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'moviemeter_automation', + 'label': 'Moviemeter', + 'description': 'Imports movies from the current top 10 of moviemeter.nl.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py b/couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py deleted file mode 100644 index 0e9a4edc..00000000 --- a/couchpotato/core/media/movie/providers/automation/moviemeter/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .main import Moviemeter - - -def start(): - return Moviemeter() - -config = [{ - 'name': 'moviemeter', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'moviemeter_automation', - 'label': 'Moviemeter', - 'description': 'Imports movies from the current top 10 of moviemeter.nl.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/movies_io/main.py b/couchpotato/core/media/movie/providers/automation/movies_io.py similarity index 54% rename from couchpotato/core/media/movie/providers/automation/movies_io/main.py rename to couchpotato/core/media/movie/providers/automation/movies_io.py index 890cbde0..3b0b54f2 100644 --- a/couchpotato/core/media/movie/providers/automation/movies_io/main.py +++ b/couchpotato/core/media/movie/providers/automation/movies_io.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.automation.base import Automation log = CPLog(__name__) +autoload = 'MoviesIO' + class MoviesIO(Automation, RSS): @@ -37,3 +39,34 @@ class MoviesIO(Automation, RSS): movies.append(imdb) return movies + + +config = [{ + 'name': 'moviesio', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'moviesio', + 'label': 'Movies.IO', + 'description': 'Imports movies from Movies.io RSS watchlists', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/movies_io/__init__.py b/couchpotato/core/media/movie/providers/automation/movies_io/__init__.py deleted file mode 100644 index 0361223b..00000000 --- a/couchpotato/core/media/movie/providers/automation/movies_io/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from .main import MoviesIO - - -def start(): - return MoviesIO() - -config = [{ - 'name': 'moviesio', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'moviesio', - 'label': 'Movies.IO', - 'description': 'Imports movies from Movies.io RSS watchlists', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_urls_use', - 'label': 'Use', - }, - { - 'name': 'automation_urls', - 'label': 'url', - 'type': 'combined', - 'combine': ['automation_urls_use', 'automation_urls'], - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py similarity index 57% rename from couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py rename to couchpotato/core/media/movie/providers/automation/rottentomatoes.py index 56e0e7c5..279b4f8a 100644 --- a/couchpotato/core/media/movie/providers/automation/rottentomatoes/main.py +++ b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py @@ -8,6 +8,8 @@ import re log = CPLog(__name__) +autoload = 'Rottentomatoes' + class Rottentomatoes(Automation, RSS): @@ -51,3 +53,42 @@ class Rottentomatoes(Automation, RSS): movies.append(imdb['imdb']) return movies + + +config = [{ + 'name': 'rottentomatoes', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'rottentomatoes_automation', + 'label': 'Rottentomatoes', + 'description': 'Imports movies from rottentomatoes rss feeds specified below.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + 'default': '1', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + 'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml', + }, + { + 'name': 'tomatometer_percent', + 'default': '80', + 'label': 'Tomatometer', + 'description': 'Use as extra scoring requirement', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py deleted file mode 100644 index 1d3026d3..00000000 --- a/couchpotato/core/media/movie/providers/automation/rottentomatoes/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from .main import Rottentomatoes - - -def start(): - return Rottentomatoes() - -config = [{ - 'name': 'rottentomatoes', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'rottentomatoes_automation', - 'label': 'Rottentomatoes', - 'description': 'Imports movies from rottentomatoes rss feeds specified below.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_urls_use', - 'label': 'Use', - 'default': '1', - }, - { - 'name': 'automation_urls', - 'label': 'url', - 'type': 'combined', - 'combine': ['automation_urls_use', 'automation_urls'], - 'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml', - }, - { - 'name': 'tomatometer_percent', - 'default': '80', - 'label': 'Tomatometer', - 'description': 'Use as extra scoring requirement', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/trakt/main.py b/couchpotato/core/media/movie/providers/automation/trakt.py similarity index 55% rename from couchpotato/core/media/movie/providers/automation/trakt/main.py rename to couchpotato/core/media/movie/providers/automation/trakt.py index 881b14a1..0c945520 100644 --- a/couchpotato/core/media/movie/providers/automation/trakt/main.py +++ b/couchpotato/core/media/movie/providers/automation/trakt.py @@ -6,6 +6,8 @@ import base64 log = CPLog(__name__) +autoload = 'Trakt' + class Trakt(Automation): @@ -42,3 +44,38 @@ class Trakt(Automation): data = self.getJsonData(self.urls['base'] + method_url, headers = headers) return data if data else [] + + +config = [{ + 'name': 'trakt', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'trakt_automation', + 'label': 'Trakt', + 'description': 'import movies from your own watchlist', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_api_key', + 'label': 'Apikey', + }, + { + 'name': 'automation_username', + 'label': 'Username', + }, + { + 'name': 'automation_password', + 'label': 'Password', + 'type': 'password', + 'description': 'When you have "Protect my data" checked on trakt.', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/automation/trakt/__init__.py b/couchpotato/core/media/movie/providers/automation/trakt/__init__.py deleted file mode 100644 index 6ae2806b..00000000 --- a/couchpotato/core/media/movie/providers/automation/trakt/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from .main import Trakt - - -def start(): - return Trakt() - -config = [{ - 'name': 'trakt', - 'groups': [ - { - 'tab': 'automation', - 'list': 'watchlist_providers', - 'name': 'trakt_automation', - 'label': 'Trakt', - 'description': 'import movies from your own watchlist', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'automation_api_key', - 'label': 'Apikey', - }, - { - 'name': 'automation_username', - 'label': 'Username', - }, - { - 'name': 'automation_password', - 'label': 'Password', - 'type': 'password', - 'description': 'When you have "Protect my data" checked on trakt.', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/info/_modifier/main.py b/couchpotato/core/media/movie/providers/info/_modifier.py similarity index 98% rename from couchpotato/core/media/movie/providers/info/_modifier/main.py rename to couchpotato/core/media/movie/providers/info/_modifier.py index 010f1f5b..9e822234 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier/main.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -9,6 +9,8 @@ import traceback log = CPLog(__name__) +autoload = 'MovieResultModifier' + class MovieResultModifier(Plugin): diff --git a/couchpotato/core/media/movie/providers/info/_modifier/__init__.py b/couchpotato/core/media/movie/providers/info/_modifier/__init__.py deleted file mode 100644 index 3bdf5e0d..00000000 --- a/couchpotato/core/media/movie/providers/info/_modifier/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import MovieResultModifier - -def start(): - - return MovieResultModifier() - -config = [] diff --git a/couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py similarity index 99% rename from couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py rename to couchpotato/core/media/movie/providers/info/couchpotatoapi.py index 469866cd..272fab9b 100644 --- a/couchpotato/core/media/movie/providers/info/couchpotatoapi/main.py +++ b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py @@ -10,6 +10,8 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'CouchPotatoApi' + class CouchPotatoApi(MovieProvider): diff --git a/couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py deleted file mode 100644 index 196dde6a..00000000 --- a/couchpotato/core/media/movie/providers/info/couchpotatoapi/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import CouchPotatoApi - - -def start(): - return CouchPotatoApi() - -config = [] diff --git a/couchpotato/core/media/movie/providers/info/omdbapi/main.py b/couchpotato/core/media/movie/providers/info/omdbapi.py similarity index 99% rename from couchpotato/core/media/movie/providers/info/omdbapi/main.py rename to couchpotato/core/media/movie/providers/info/omdbapi.py index 34e7d569..342df3ca 100644 --- a/couchpotato/core/media/movie/providers/info/omdbapi/main.py +++ b/couchpotato/core/media/movie/providers/info/omdbapi.py @@ -11,6 +11,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'OMDBAPI' + class OMDBAPI(MovieProvider): diff --git a/couchpotato/core/media/movie/providers/info/omdbapi/__init__.py b/couchpotato/core/media/movie/providers/info/omdbapi/__init__.py deleted file mode 100644 index b7ea3932..00000000 --- a/couchpotato/core/media/movie/providers/info/omdbapi/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import OMDBAPI - - -def start(): - return OMDBAPI() - -config = [] diff --git a/couchpotato/core/media/movie/providers/info/themoviedb/main.py b/couchpotato/core/media/movie/providers/info/themoviedb.py similarity index 92% rename from couchpotato/core/media/movie/providers/info/themoviedb/main.py rename to couchpotato/core/media/movie/providers/info/themoviedb.py index 36f3a489..9f5c5e88 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb/main.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -7,9 +7,10 @@ from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.base import MovieProvider import tmdb3 - log = CPLog(__name__) +autoload = 'TheMovieDb' + class TheMovieDb(MovieProvider): @@ -175,3 +176,24 @@ class TheMovieDb(MovieProvider): log.error('No API key provided.') return True return False + + +config = [{ + 'name': 'themoviedb', + 'groups': [ + { + 'tab': 'providers', + 'name': 'tmdb', + 'label': 'TheMovieDB', + 'hidden': True, + 'description': 'Used for all calls to TheMovieDB.', + 'options': [ + { + 'name': 'api_key', + 'default': '9b939aee0aaafc12a65bf448e4af9543', + 'label': 'Api Key', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/info/themoviedb/__init__.py b/couchpotato/core/media/movie/providers/info/themoviedb/__init__.py deleted file mode 100644 index b981950e..00000000 --- a/couchpotato/core/media/movie/providers/info/themoviedb/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .main import TheMovieDb - - -def start(): - return TheMovieDb() - -config = [{ - 'name': 'themoviedb', - 'groups': [ - { - 'tab': 'providers', - 'name': 'tmdb', - 'label': 'TheMovieDB', - 'hidden': True, - 'description': 'Used for all calls to TheMovieDB.', - 'options': [ - { - 'name': 'api_key', - 'default': '9b939aee0aaafc12a65bf448e4af9543', - 'label': 'Api Key', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/metadata/wmc/__init__.py b/couchpotato/core/media/movie/providers/metadata/wmc.py similarity index 66% rename from couchpotato/core/media/movie/providers/metadata/wmc/__init__.py rename to couchpotato/core/media/movie/providers/metadata/wmc.py index 167a24d7..50db02db 100644 --- a/couchpotato/core/media/movie/providers/metadata/wmc/__init__.py +++ b/couchpotato/core/media/movie/providers/metadata/wmc.py @@ -1,8 +1,14 @@ -from .main import WindowsMediaCenter +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData +import os + +autoload = 'WindowsMediaCenter' -def start(): - return WindowsMediaCenter() +class WindowsMediaCenter(MovieMetaData): + + def getThumbnailName(self, name, root): + return os.path.join(root, 'folder.jpg') + config = [{ 'name': 'windowsmediacenter', diff --git a/couchpotato/core/media/movie/providers/metadata/wmc/main.py b/couchpotato/core/media/movie/providers/metadata/wmc/main.py deleted file mode 100644 index bdb96a44..00000000 --- a/couchpotato/core/media/movie/providers/metadata/wmc/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData -import os - - -class WindowsMediaCenter(MovieMetaData): - - def getThumbnailName(self, name, root): - return os.path.join(root, 'folder.jpg') diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc/main.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py similarity index 74% rename from couchpotato/core/media/movie/providers/metadata/xbmc/main.py rename to couchpotato/core/media/movie/providers/metadata/xbmc.py index 61f36473..e9904cf7 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc/main.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -10,6 +10,9 @@ import xml.dom.minidom log = CPLog(__name__) +autoload = 'XBMC' + + class XBMC(MovieMetaData): def getFanartName(self, name, root): @@ -172,3 +175,69 @@ class XBMC(MovieMetaData): xml_string = text_re.sub('>\g<1>%s is the rootname of the movie. For example "/path/to/movie cd1.mkv" will be "/path/to/movie"' + }, + { + 'name': 'meta_url_only', + 'label': 'Only IMDB URL', + 'default': False, + 'advanced': True, + 'description': 'Create a nfo with only the IMDB url inside', + 'type': 'bool', + }, + { + 'name': 'meta_fanart', + 'label': 'Fanart', + 'default': True, + 'type': 'bool', + }, + { + 'name': 'meta_fanart_name', + 'label': 'Fanart filename', + 'default': '%s-fanart.jpg', + 'advanced': True, + }, + { + 'name': 'meta_thumbnail', + 'label': 'Thumbnail', + 'default': True, + 'type': 'bool', + }, + { + 'name': 'meta_thumbnail_name', + 'label': 'Thumbnail filename', + 'default': '%s.tbn', + 'advanced': True, + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py b/couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py deleted file mode 100644 index deb5c908..00000000 --- a/couchpotato/core/media/movie/providers/metadata/xbmc/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from .main import XBMC - - -def start(): - return XBMC() - -config = [{ - 'name': 'xbmc', - 'groups': [ - { - 'tab': 'renamer', - 'subtab': 'metadata', - 'name': 'xbmc_metadata', - 'label': 'XBMC', - 'description': 'Enable metadata XBMC can understand', - 'options': [ - { - 'name': 'meta_enabled', - 'default': False, - 'type': 'enabler', - }, - { - 'name': 'meta_nfo', - 'label': 'NFO', - 'default': True, - 'type': 'bool', - }, - { - 'name': 'meta_nfo_name', - 'label': 'NFO filename', - 'default': '%s.nfo', - 'advanced': True, - 'description': '%s is the rootname of the movie. For example "/path/to/movie cd1.mkv" will be "/path/to/movie"' - }, - { - 'name': 'meta_url_only', - 'label': 'Only IMDB URL', - 'default': False, - 'advanced': True, - 'description': 'Create a nfo with only the IMDB url inside', - 'type': 'bool', - }, - { - 'name': 'meta_fanart', - 'label': 'Fanart', - 'default': True, - 'type': 'bool', - }, - { - 'name': 'meta_fanart_name', - 'label': 'Fanart filename', - 'default': '%s-fanart.jpg', - 'advanced': True, - }, - { - 'name': 'meta_thumbnail', - 'label': 'Thumbnail', - 'default': True, - 'type': 'bool', - }, - { - 'name': 'meta_thumbnail_name', - 'label': 'Thumbnail filename', - 'default': '%s.tbn', - 'advanced': True, - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/nzb/__init__.py b/couchpotato/core/media/movie/providers/nzb/__init__.py index 88d9865d..e69de29b 100644 --- a/couchpotato/core/media/movie/providers/nzb/__init__.py +++ b/couchpotato/core/media/movie/providers/nzb/__init__.py @@ -1,14 +0,0 @@ -config = [{ - 'name': 'nzb_providers', - 'groups': [ - { - 'label': 'Usenet Providers', - 'description': 'Providers searching usenet for new releases', - 'wizard': True, - 'type': 'list', - 'name': 'nzb_providers', - 'tab': 'searcher', - 'options': [], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch/main.py b/couchpotato/core/media/movie/providers/nzb/binsearch.py similarity index 100% rename from couchpotato/core/media/movie/providers/nzb/binsearch/main.py rename to couchpotato/core/media/movie/providers/nzb/binsearch.py diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py b/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py deleted file mode 100644 index 13a4bfb9..00000000 --- a/couchpotato/core/media/movie/providers/nzb/binsearch/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import BinSearch - - -def start(): - return BinSearch() - -config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/newznab/main.py b/couchpotato/core/media/movie/providers/nzb/newznab.py similarity index 100% rename from couchpotato/core/media/movie/providers/nzb/newznab/main.py rename to couchpotato/core/media/movie/providers/nzb/newznab.py diff --git a/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py b/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py deleted file mode 100644 index 2b25ce28..00000000 --- a/couchpotato/core/media/movie/providers/nzb/newznab/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Newznab - - -def start(): - return Newznab() - -config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub/main.py b/couchpotato/core/media/movie/providers/nzb/nzbclub.py similarity index 100% rename from couchpotato/core/media/movie/providers/nzb/nzbclub/main.py rename to couchpotato/core/media/movie/providers/nzb/nzbclub.py diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py b/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py deleted file mode 100644 index 31df9653..00000000 --- a/couchpotato/core/media/movie/providers/nzb/nzbclub/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import NZBClub - - -def start(): - return NZBClub() - -config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex/main.py b/couchpotato/core/media/movie/providers/nzb/nzbindex.py similarity index 100% rename from couchpotato/core/media/movie/providers/nzb/nzbindex/main.py rename to couchpotato/core/media/movie/providers/nzb/nzbindex.py diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py b/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py deleted file mode 100644 index acf9e915..00000000 --- a/couchpotato/core/media/movie/providers/nzb/nzbindex/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import NzbIndex - - -def start(): - return NzbIndex() - -config = [] diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py similarity index 100% rename from couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/main.py rename to couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py deleted file mode 100644 index 086db2c7..00000000 --- a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import OMGWTFNZBs - - -def start(): - return OMGWTFNZBs() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/__init__.py b/couchpotato/core/media/movie/providers/torrent/__init__.py index 12dda708..e69de29b 100644 --- a/couchpotato/core/media/movie/providers/torrent/__init__.py +++ b/couchpotato/core/media/movie/providers/torrent/__init__.py @@ -1,14 +0,0 @@ -config = [{ - 'name': 'torrent_providers', - 'groups': [ - { - 'label': 'Torrent Providers', - 'description': 'Providers searching torrent sites for new releases', - 'wizard': True, - 'type': 'list', - 'name': 'torrent_providers', - 'tab': 'searcher', - 'options': [], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd/main.py b/couchpotato/core/media/movie/providers/torrent/awesomehd.py similarity index 91% rename from couchpotato/core/media/movie/providers/torrent/awesomehd/main.py rename to couchpotato/core/media/movie/providers/torrent/awesomehd.py index 55f23d94..d96b59ab 100644 --- a/couchpotato/core/media/movie/providers/torrent/awesomehd/main.py +++ b/couchpotato/core/media/movie/providers/torrent/awesomehd.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'AwesomeHD' + class AwesomeHD(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py b/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py deleted file mode 100644 index 7cc37a20..00000000 --- a/couchpotato/core/media/movie/providers/torrent/awesomehd/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .main import AwesomeHD - - -def start(): - return AwesomeHD() - -config = [] - diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv/main.py b/couchpotato/core/media/movie/providers/torrent/bithdtv.py similarity index 96% rename from couchpotato/core/media/movie/providers/torrent/bithdtv/main.py rename to couchpotato/core/media/movie/providers/torrent/bithdtv.py index 03be850b..f1f9cff4 100644 --- a/couchpotato/core/media/movie/providers/torrent/bithdtv/main.py +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'BiTHDTV' + class BiTHDTV(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py b/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py deleted file mode 100644 index 0145dcde..00000000 --- a/couchpotato/core/media/movie/providers/torrent/bithdtv/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import BiTHDTV - - -def start(): - return BiTHDTV() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup/main.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py similarity index 97% rename from couchpotato/core/media/movie/providers/torrent/bitsoup/main.py rename to couchpotato/core/media/movie/providers/torrent/bitsoup.py index b67ae229..7170ebd0 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup/main.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'Bitsoup' + class Bitsoup(MovieProvider, Base): cat_ids = [ diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py b/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py deleted file mode 100644 index 8be80c8f..00000000 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Bitsoup - - -def start(): - return Bitsoup() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits/main.py b/couchpotato/core/media/movie/providers/torrent/hdbits.py similarity index 92% rename from couchpotato/core/media/movie/providers/torrent/hdbits/main.py rename to couchpotato/core/media/movie/providers/torrent/hdbits.py index 2a1e23f6..e44e3b3f 100644 --- a/couchpotato/core/media/movie/providers/torrent/hdbits/main.py +++ b/couchpotato/core/media/movie/providers/torrent/hdbits.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'HDBits' + class HDBits(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py b/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py deleted file mode 100644 index 1c7b4357..00000000 --- a/couchpotato/core/media/movie/providers/torrent/hdbits/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import HDBits - - -def start(): - return HDBits() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py similarity index 90% rename from couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py rename to couchpotato/core/media/movie/providers/torrent/ilovetorrents.py index 5e90e73d..ad0bac32 100644 --- a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/main.py +++ b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'ILoveTorrents' + class ILoveTorrents(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py deleted file mode 100644 index f20367de..00000000 --- a/couchpotato/core/media/movie/providers/torrent/ilovetorrents/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import ILoveTorrents - - -def start(): - return ILoveTorrents() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents/main.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py similarity index 100% rename from couchpotato/core/media/movie/providers/torrent/iptorrents/main.py rename to couchpotato/core/media/movie/providers/torrent/iptorrents.py diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py deleted file mode 100644 index 8741a132..00000000 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import IPTorrents - - -def start(): - return IPTorrents() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py similarity index 100% rename from couchpotato/core/media/movie/providers/torrent/kickasstorrents/main.py rename to couchpotato/core/media/movie/providers/torrent/kickasstorrents.py diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py deleted file mode 100644 index cb293876..00000000 --- a/couchpotato/core/media/movie/providers/torrent/kickasstorrents/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import KickAssTorrents - - -def start(): - return KickAssTorrents() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py similarity index 100% rename from couchpotato/core/media/movie/providers/torrent/passthepopcorn/main.py rename to couchpotato/core/media/movie/providers/torrent/passthepopcorn.py diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py deleted file mode 100644 index c437737c..00000000 --- a/couchpotato/core/media/movie/providers/torrent/passthepopcorn/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import PassThePopcorn - - -def start(): - return PassThePopcorn() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/publichd/main.py b/couchpotato/core/media/movie/providers/torrent/publichd.py similarity index 94% rename from couchpotato/core/media/movie/providers/torrent/publichd/main.py rename to couchpotato/core/media/movie/providers/torrent/publichd.py index 5c3cd946..4dd7efb6 100644 --- a/couchpotato/core/media/movie/providers/torrent/publichd/main.py +++ b/couchpotato/core/media/movie/providers/torrent/publichd.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'PublicHD' + class PublicHD(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py b/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py deleted file mode 100644 index c58d2b53..00000000 --- a/couchpotato/core/media/movie/providers/torrent/publichd/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import PublicHD - - -def start(): - return PublicHD() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py similarity index 97% rename from couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py rename to couchpotato/core/media/movie/providers/torrent/sceneaccess.py index 7ed756e2..ead32e23 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'SceneAccess' + class SceneAccess(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py deleted file mode 100644 index 14932221..00000000 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import SceneAccess - - -def start(): - return SceneAccess() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py similarity index 96% rename from couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py rename to couchpotato/core/media/movie/providers/torrent/thepiratebay.py index 138f311d..4f6f3bb5 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'ThePirateBay' + class ThePirateBay(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py deleted file mode 100644 index 3bd44a4e..00000000 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import ThePirateBay - - -def start(): - return ThePirateBay() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py similarity index 90% rename from couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py rename to couchpotato/core/media/movie/providers/torrent/torrentbytes.py index d0131875..cef52552 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'TorrentBytes' + class TorrentBytes(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py deleted file mode 100644 index 39bef365..00000000 --- a/couchpotato/core/media/movie/providers/torrent/torrentbytes/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import TorrentBytes - - -def start(): - return TorrentBytes() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday/main.py b/couchpotato/core/media/movie/providers/torrent/torrentday.py similarity index 95% rename from couchpotato/core/media/movie/providers/torrent/torrentday/main.py rename to couchpotato/core/media/movie/providers/torrent/torrentday.py index 566b0380..fa90306e 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentday/main.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentday.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'TorrentDay' + class TorrentDay(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py deleted file mode 100644 index 0ebf09e0..00000000 --- a/couchpotato/core/media/movie/providers/torrent/torrentday/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import TorrentDay - - -def start(): - return TorrentDay() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech/main.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py similarity index 96% rename from couchpotato/core/media/movie/providers/torrent/torrentleech/main.py rename to couchpotato/core/media/movie/providers/torrent/torrentleech.py index 3b6b858d..5483adc7 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'TorrentLeech' + class TorrentLeech(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py deleted file mode 100644 index afb8f1e6..00000000 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import TorrentLeech - - -def start(): - return TorrentLeech() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py similarity index 95% rename from couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py rename to couchpotato/core/media/movie/providers/torrent/torrentpotato.py index a2ddae88..1e5903a5 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentpotato/main.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'TorrentPotato' + class TorrentPotato(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py deleted file mode 100644 index 58b9d7b2..00000000 --- a/couchpotato/core/media/movie/providers/torrent/torrentpotato/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import TorrentPotato - - -def start(): - return TorrentPotato() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack/main.py b/couchpotato/core/media/movie/providers/torrent/torrentshack.py similarity index 97% rename from couchpotato/core/media/movie/providers/torrent/torrentshack/main.py rename to couchpotato/core/media/movie/providers/torrent/torrentshack.py index 9e01475b..8bf7fb94 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentshack/main.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'TorrentShack' + class TorrentShack(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py b/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py deleted file mode 100644 index 7448a47c..00000000 --- a/couchpotato/core/media/movie/providers/torrent/torrentshack/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import TorrentShack - - -def start(): - return TorrentShack() - -config = [] diff --git a/couchpotato/core/media/movie/providers/torrent/yify/main.py b/couchpotato/core/media/movie/providers/torrent/yify.py similarity index 93% rename from couchpotato/core/media/movie/providers/torrent/yify/main.py rename to couchpotato/core/media/movie/providers/torrent/yify.py index 24ee8ea7..16c76949 100644 --- a/couchpotato/core/media/movie/providers/torrent/yify/main.py +++ b/couchpotato/core/media/movie/providers/torrent/yify.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'Yify' + class Yify(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/yify/__init__.py b/couchpotato/core/media/movie/providers/torrent/yify/__init__.py deleted file mode 100644 index ac9baa2d..00000000 --- a/couchpotato/core/media/movie/providers/torrent/yify/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import Yify - - -def start(): - return Yify() - -config = [] diff --git a/couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py similarity index 99% rename from couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py rename to couchpotato/core/media/movie/providers/trailer/hdtrailers.py index ea2fe95d..1f5b4f63 100644 --- a/couchpotato/core/media/movie/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py @@ -9,6 +9,8 @@ from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider log = CPLog(__name__) +autoload = 'HDTrailers' + class HDTrailers(TrailerProvider): diff --git a/couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py deleted file mode 100644 index 83b93004..00000000 --- a/couchpotato/core/media/movie/providers/trailer/hdtrailers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import HDTrailers - - -def start(): - return HDTrailers() - -config = [] diff --git a/couchpotato/core/media/movie/providers/userscript/allocine/main.py b/couchpotato/core/media/movie/providers/userscript/allocine.py similarity index 97% rename from couchpotato/core/media/movie/providers/userscript/allocine/main.py rename to couchpotato/core/media/movie/providers/userscript/allocine.py index c3400ee4..2eaef42e 100644 --- a/couchpotato/core/media/movie/providers/userscript/allocine/main.py +++ b/couchpotato/core/media/movie/providers/userscript/allocine.py @@ -4,6 +4,8 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBas log = CPLog(__name__) +autoload = 'AlloCine' + class AlloCine(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/allocine/__init__.py b/couchpotato/core/media/movie/providers/userscript/allocine/__init__.py deleted file mode 100644 index d272a7f9..00000000 --- a/couchpotato/core/media/movie/providers/userscript/allocine/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import AlloCine - - -def start(): - return AlloCine() diff --git a/couchpotato/core/media/movie/providers/userscript/appletrailers/main.py b/couchpotato/core/media/movie/providers/userscript/appletrailers.py similarity index 95% rename from couchpotato/core/media/movie/providers/userscript/appletrailers/main.py rename to couchpotato/core/media/movie/providers/userscript/appletrailers.py index 41b72c4d..af201ed5 100644 --- a/couchpotato/core/media/movie/providers/userscript/appletrailers/main.py +++ b/couchpotato/core/media/movie/providers/userscript/appletrailers.py @@ -1,6 +1,8 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +autoload = 'AppleTrailers' + class AppleTrailers(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py b/couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py deleted file mode 100644 index 28eedbbd..00000000 --- a/couchpotato/core/media/movie/providers/userscript/appletrailers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import AppleTrailers - - -def start(): - return AppleTrailers() diff --git a/couchpotato/core/media/movie/providers/userscript/criticker/main.py b/couchpotato/core/media/movie/providers/userscript/criticker.py similarity index 87% rename from couchpotato/core/media/movie/providers/userscript/criticker/main.py rename to couchpotato/core/media/movie/providers/userscript/criticker.py index 7ab63c98..cc0bee84 100644 --- a/couchpotato/core/media/movie/providers/userscript/criticker/main.py +++ b/couchpotato/core/media/movie/providers/userscript/criticker.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'Criticker' + class Criticker(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/criticker/__init__.py b/couchpotato/core/media/movie/providers/userscript/criticker/__init__.py deleted file mode 100644 index 550855ea..00000000 --- a/couchpotato/core/media/movie/providers/userscript/criticker/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Criticker - - -def start(): - return Criticker() diff --git a/couchpotato/core/media/movie/providers/userscript/filmweb/main.py b/couchpotato/core/media/movie/providers/userscript/filmweb.py similarity index 97% rename from couchpotato/core/media/movie/providers/userscript/filmweb/main.py rename to couchpotato/core/media/movie/providers/userscript/filmweb.py index 18e5d303..f4e36529 100644 --- a/couchpotato/core/media/movie/providers/userscript/filmweb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/filmweb.py @@ -1,6 +1,8 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +autoload = 'Filmweb' + class Filmweb(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py b/couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py deleted file mode 100644 index 38354b45..00000000 --- a/couchpotato/core/media/movie/providers/userscript/filmweb/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Filmweb - - -def start(): - return Filmweb() diff --git a/couchpotato/core/media/movie/providers/userscript/flickchart/main.py b/couchpotato/core/media/movie/providers/userscript/flickchart.py similarity index 97% rename from couchpotato/core/media/movie/providers/userscript/flickchart/main.py rename to couchpotato/core/media/movie/providers/userscript/flickchart.py index 6c2837bf..2df0acf8 100644 --- a/couchpotato/core/media/movie/providers/userscript/flickchart/main.py +++ b/couchpotato/core/media/movie/providers/userscript/flickchart.py @@ -5,6 +5,8 @@ import traceback log = CPLog(__name__) +autoload = 'Flickchart' + class Flickchart(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py b/couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py deleted file mode 100644 index 5ff48e54..00000000 --- a/couchpotato/core/media/movie/providers/userscript/flickchart/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Flickchart - - -def start(): - return Flickchart() diff --git a/couchpotato/core/media/movie/providers/userscript/imdb/main.py b/couchpotato/core/media/movie/providers/userscript/imdb.py similarity index 87% rename from couchpotato/core/media/movie/providers/userscript/imdb/main.py rename to couchpotato/core/media/movie/providers/userscript/imdb.py index 14363933..dccc4832 100644 --- a/couchpotato/core/media/movie/providers/userscript/imdb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/imdb.py @@ -1,7 +1,8 @@ -from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import getImdb from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'IMDB' + class IMDB(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/imdb/__init__.py b/couchpotato/core/media/movie/providers/userscript/imdb/__init__.py deleted file mode 100644 index e310cc31..00000000 --- a/couchpotato/core/media/movie/providers/userscript/imdb/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import IMDB - - -def start(): - return IMDB() diff --git a/couchpotato/core/media/movie/providers/userscript/letterboxd/main.py b/couchpotato/core/media/movie/providers/userscript/letterboxd.py similarity index 86% rename from couchpotato/core/media/movie/providers/userscript/letterboxd/main.py rename to couchpotato/core/media/movie/providers/userscript/letterboxd.py index 852c1b79..43b5d309 100644 --- a/couchpotato/core/media/movie/providers/userscript/letterboxd/main.py +++ b/couchpotato/core/media/movie/providers/userscript/letterboxd.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'Letterboxd' + class Letterboxd(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py b/couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py deleted file mode 100644 index 78ef77b4..00000000 --- a/couchpotato/core/media/movie/providers/userscript/letterboxd/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Letterboxd - - -def start(): - return Letterboxd() diff --git a/couchpotato/core/media/movie/providers/userscript/moviemeter/main.py b/couchpotato/core/media/movie/providers/userscript/moviemeter.py similarity index 88% rename from couchpotato/core/media/movie/providers/userscript/moviemeter/main.py rename to couchpotato/core/media/movie/providers/userscript/moviemeter.py index f1b254fc..4c9bb221 100644 --- a/couchpotato/core/media/movie/providers/userscript/moviemeter/main.py +++ b/couchpotato/core/media/movie/providers/userscript/moviemeter.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'MovieMeter' + class MovieMeter(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py b/couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py deleted file mode 100644 index aae8ca71..00000000 --- a/couchpotato/core/media/movie/providers/userscript/moviemeter/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import MovieMeter - - -def start(): - return MovieMeter() diff --git a/couchpotato/core/media/movie/providers/userscript/moviesio/main.py b/couchpotato/core/media/movie/providers/userscript/moviesio.py similarity index 87% rename from couchpotato/core/media/movie/providers/userscript/moviesio/main.py rename to couchpotato/core/media/movie/providers/userscript/moviesio.py index a466baf9..0381d64a 100644 --- a/couchpotato/core/media/movie/providers/userscript/moviesio/main.py +++ b/couchpotato/core/media/movie/providers/userscript/moviesio.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'MoviesIO' + class MoviesIO(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py b/couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py deleted file mode 100644 index 3f0497fc..00000000 --- a/couchpotato/core/media/movie/providers/userscript/moviesio/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import MoviesIO - - -def start(): - return MoviesIO() diff --git a/couchpotato/core/media/movie/providers/userscript/reddit/main.py b/couchpotato/core/media/movie/providers/userscript/reddit.py similarity index 96% rename from couchpotato/core/media/movie/providers/userscript/reddit/main.py rename to couchpotato/core/media/movie/providers/userscript/reddit.py index ec59e69a..3ab1d08d 100644 --- a/couchpotato/core/media/movie/providers/userscript/reddit/main.py +++ b/couchpotato/core/media/movie/providers/userscript/reddit.py @@ -2,6 +2,8 @@ from couchpotato import fireEvent from couchpotato.core.helpers.variable import splitString from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'Reddit' + class Reddit(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/reddit/__init__.py b/couchpotato/core/media/movie/providers/userscript/reddit/__init__.py deleted file mode 100644 index d803bb54..00000000 --- a/couchpotato/core/media/movie/providers/userscript/reddit/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Reddit - - -def start(): - return Reddit() diff --git a/couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py similarity index 97% rename from couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py rename to couchpotato/core/media/movie/providers/userscript/rottentomatoes.py index 0b3145e0..cb661467 100644 --- a/couchpotato/core/media/movie/providers/userscript/rottentomatoes/main.py +++ b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py @@ -5,6 +5,8 @@ import traceback log = CPLog(__name__) +autoload = 'RottenTomatoes' + class RottenTomatoes(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py deleted file mode 100644 index b041d5a1..00000000 --- a/couchpotato/core/media/movie/providers/userscript/rottentomatoes/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import RottenTomatoes - - -def start(): - return RottenTomatoes() diff --git a/couchpotato/core/media/movie/providers/userscript/sharethe/main.py b/couchpotato/core/media/movie/providers/userscript/sharethe.py similarity index 89% rename from couchpotato/core/media/movie/providers/userscript/sharethe/main.py rename to couchpotato/core/media/movie/providers/userscript/sharethe.py index ce11229a..ef2f537e 100644 --- a/couchpotato/core/media/movie/providers/userscript/sharethe/main.py +++ b/couchpotato/core/media/movie/providers/userscript/sharethe.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'ShareThe' + class ShareThe(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py b/couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py deleted file mode 100644 index cf553da7..00000000 --- a/couchpotato/core/media/movie/providers/userscript/sharethe/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import ShareThe - - -def start(): - return ShareThe() diff --git a/couchpotato/core/media/movie/providers/userscript/tmdb/main.py b/couchpotato/core/media/movie/providers/userscript/tmdb.py similarity index 96% rename from couchpotato/core/media/movie/providers/userscript/tmdb/main.py rename to couchpotato/core/media/movie/providers/userscript/tmdb.py index 9638bea4..71ab0d7b 100644 --- a/couchpotato/core/media/movie/providers/userscript/tmdb/main.py +++ b/couchpotato/core/media/movie/providers/userscript/tmdb.py @@ -2,6 +2,8 @@ from couchpotato.core.event import fireEvent from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +autoload = 'TMDB' + class TMDB(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py b/couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py deleted file mode 100644 index 609f364f..00000000 --- a/couchpotato/core/media/movie/providers/userscript/tmdb/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import TMDB - - -def start(): - return TMDB() diff --git a/couchpotato/core/media/movie/providers/userscript/trakt/main.py b/couchpotato/core/media/movie/providers/userscript/trakt.py similarity index 92% rename from couchpotato/core/media/movie/providers/userscript/trakt/main.py rename to couchpotato/core/media/movie/providers/userscript/trakt.py index 17cfdddc..cd40c69c 100644 --- a/couchpotato/core/media/movie/providers/userscript/trakt/main.py +++ b/couchpotato/core/media/movie/providers/userscript/trakt.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'Trakt' + class Trakt(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/trakt/__init__.py b/couchpotato/core/media/movie/providers/userscript/trakt/__init__.py deleted file mode 100644 index ddba4408..00000000 --- a/couchpotato/core/media/movie/providers/userscript/trakt/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import Trakt - - -def start(): - return Trakt() diff --git a/couchpotato/core/media/movie/providers/userscript/whiwa/main.py b/couchpotato/core/media/movie/providers/userscript/whiwa.py similarity index 89% rename from couchpotato/core/media/movie/providers/userscript/whiwa/main.py rename to couchpotato/core/media/movie/providers/userscript/whiwa.py index e0a01200..bd602d20 100644 --- a/couchpotato/core/media/movie/providers/userscript/whiwa/main.py +++ b/couchpotato/core/media/movie/providers/userscript/whiwa.py @@ -1,5 +1,7 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase +autoload = 'WHiWA' + class WHiWA(UserscriptBase): diff --git a/couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py b/couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py deleted file mode 100644 index 3b14f477..00000000 --- a/couchpotato/core/media/movie/providers/userscript/whiwa/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import WHiWA - - -def start(): - return WHiWA() diff --git a/couchpotato/core/media/movie/providers/userscript/youteather/main.py b/couchpotato/core/media/movie/providers/userscript/youteather.py similarity index 95% rename from couchpotato/core/media/movie/providers/userscript/youteather/main.py rename to couchpotato/core/media/movie/providers/userscript/youteather.py index 34c97688..3a35df37 100644 --- a/couchpotato/core/media/movie/providers/userscript/youteather/main.py +++ b/couchpotato/core/media/movie/providers/userscript/youteather.py @@ -1,6 +1,9 @@ from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +autoload = 'YouTheater' + + class YouTheater(UserscriptBase): id_re = re.compile("view\.php\?id=(\d+)") includes = ['http://www.youtheater.com/view.php?id=*', 'http://youtheater.com/view.php?id=*', diff --git a/couchpotato/core/media/movie/providers/userscript/youteather/__init__.py b/couchpotato/core/media/movie/providers/userscript/youteather/__init__.py deleted file mode 100644 index 8eb44c79..00000000 --- a/couchpotato/core/media/movie/providers/userscript/youteather/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import YouTheater - - -def start(): - return YouTheater() diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher.py similarity index 82% rename from couchpotato/core/media/movie/searcher/main.py rename to couchpotato/core/media/movie/searcher.py index dfc2ee20..192040fc 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher.py @@ -347,3 +347,72 @@ class MovieSearcher(SearcherBase, MovieTypeBase): class SearchSetupError(Exception): pass + + +config = [{ + 'name': 'moviesearcher', + 'order': 20, + 'groups': [ + { + 'tab': 'searcher', + 'name': 'movie_searcher', + 'label': 'Movie search', + 'description': 'Search options for movies', + 'advanced': True, + 'options': [ + { + 'name': 'always_search', + 'default': False, + 'migrate_from': 'searcher', + 'type': 'bool', + 'label': 'Always search', + 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', + }, + { + 'name': 'run_on_launch', + 'migrate_from': 'searcher', + 'label': 'Run on launch', + 'advanced': True, + 'default': 0, + 'type': 'bool', + 'description': 'Force run the searcher after (re)start.', + }, + { + 'name': 'search_on_add', + 'label': 'Search after add', + 'advanced': True, + 'default': 1, + 'type': 'bool', + 'description': 'Disable this to only search for movies on cron.', + }, + { + 'name': 'cron_day', + 'migrate_from': 'searcher', + 'label': 'Day', + 'advanced': True, + 'default': '*', + 'type': 'string', + 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.', + }, + { + 'name': 'cron_hour', + 'migrate_from': 'searcher', + 'label': 'Hour', + 'advanced': True, + 'default': random.randint(0, 23), + 'type': 'string', + 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', + }, + { + 'name': 'cron_minute', + 'migrate_from': 'searcher', + 'label': 'Minute', + 'advanced': True, + 'default': random.randint(0, 59), + 'type': 'string', + 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/searcher/__init__.py b/couchpotato/core/media/movie/searcher/__init__.py deleted file mode 100644 index 4ae1ed32..00000000 --- a/couchpotato/core/media/movie/searcher/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -from .main import MovieSearcher -import random - - -def start(): - return MovieSearcher() - -config = [{ - 'name': 'moviesearcher', - 'order': 20, - 'groups': [ - { - 'tab': 'searcher', - 'name': 'movie_searcher', - 'label': 'Movie search', - 'description': 'Search options for movies', - 'advanced': True, - 'options': [ - { - 'name': 'always_search', - 'default': False, - 'migrate_from': 'searcher', - 'type': 'bool', - 'label': 'Always search', - 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', - }, - { - 'name': 'run_on_launch', - 'migrate_from': 'searcher', - 'label': 'Run on launch', - 'advanced': True, - 'default': 0, - 'type': 'bool', - 'description': 'Force run the searcher after (re)start.', - }, - { - 'name': 'search_on_add', - 'label': 'Search after add', - 'advanced': True, - 'default': 1, - 'type': 'bool', - 'description': 'Disable this to only search for movies on cron.', - }, - { - 'name': 'cron_day', - 'migrate_from': 'searcher', - 'label': 'Day', - 'advanced': True, - 'default': '*', - 'type': 'string', - 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.', - }, - { - 'name': 'cron_hour', - 'migrate_from': 'searcher', - 'label': 'Hour', - 'advanced': True, - 'default': random.randint(0, 23), - 'type': 'string', - 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', - }, - { - 'name': 'cron_minute', - 'migrate_from': 'searcher', - 'label': 'Minute', - 'advanced': True, - 'default': random.randint(0, 59), - 'type': 'string', - 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." - }, - ], - }, - ], -}] From 79d6a6f85fd21ac50dfb32abc32f81269f18d081 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 09:40:20 +0100 Subject: [PATCH 081/468] Import fixes --- couchpotato/core/downloaders/base.py | 2 +- couchpotato/core/downloaders/blackhole.py | 1 + couchpotato/core/downloaders/rtorrent.py | 2 +- couchpotato/core/media/movie/providers/nzb/binsearch.py | 2 +- couchpotato/core/media/movie/providers/nzb/newznab.py | 2 +- couchpotato/core/media/movie/providers/nzb/nzbclub.py | 2 +- couchpotato/core/media/movie/providers/nzb/nzbindex.py | 2 +- couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py | 2 +- couchpotato/core/media/movie/providers/torrent/awesomehd.py | 2 +- couchpotato/core/media/movie/providers/torrent/bithdtv.py | 2 +- couchpotato/core/media/movie/providers/torrent/bitsoup.py | 2 +- couchpotato/core/media/movie/providers/torrent/hdbits.py | 2 +- couchpotato/core/media/movie/providers/torrent/ilovetorrents.py | 2 +- couchpotato/core/media/movie/providers/torrent/iptorrents.py | 2 +- .../core/media/movie/providers/torrent/kickasstorrents.py | 2 +- .../core/media/movie/providers/torrent/passthepopcorn.py | 2 +- couchpotato/core/media/movie/providers/torrent/publichd.py | 2 +- couchpotato/core/media/movie/providers/torrent/sceneaccess.py | 2 +- couchpotato/core/media/movie/providers/torrent/thepiratebay.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentbytes.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentday.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentleech.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentpotato.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentshack.py | 2 +- couchpotato/core/media/movie/providers/torrent/yify.py | 2 +- 25 files changed, 25 insertions(+), 24 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 3bcf1f31..f2a0c1dc 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -3,9 +3,9 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog -from couchpotato.core.providers.base import Provider import random import re +from couchpotato.core.media._base.providers.base import Provider log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/blackhole.py b/couchpotato/core/downloaders/blackhole.py index e58db5af..dbacde0a 100644 --- a/couchpotato/core/downloaders/blackhole.py +++ b/couchpotato/core/downloaders/blackhole.py @@ -1,6 +1,7 @@ from __future__ import with_statement from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import sp +from couchpotato.core.helpers.variable import getDownloadDir from couchpotato.core.logger import CPLog from couchpotato.environment import Env import os diff --git a/couchpotato/core/downloaders/rtorrent.py b/couchpotato/core/downloaders/rtorrent.py index e2936ca5..5ca623ad 100644 --- a/couchpotato/core/downloaders/rtorrent.py +++ b/couchpotato/core/downloaders/rtorrent.py @@ -1,5 +1,5 @@ from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.logger import CPLog diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch.py b/couchpotato/core/media/movie/providers/nzb/binsearch.py index 9b4cf042..9bc2b8e8 100644 --- a/couchpotato/core/media/movie/providers/nzb/binsearch.py +++ b/couchpotato/core/media/movie/providers/nzb/binsearch.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.nzb.binsearch.main import Base +from couchpotato.core.media._base.providers.nzb.binsearch import Base from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.environment import Env diff --git a/couchpotato/core/media/movie/providers/nzb/newznab.py b/couchpotato/core/media/movie/providers/nzb/newznab.py index b67e4a66..9682e74e 100644 --- a/couchpotato/core/media/movie/providers/nzb/newznab.py +++ b/couchpotato/core/media/movie/providers/nzb/newznab.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.nzb.newznab.main import Base +from couchpotato.core.media._base.providers.nzb.newznab import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub.py b/couchpotato/core/media/movie/providers/nzb/nzbclub.py index d90f7457..f1b0d6bd 100644 --- a/couchpotato/core/media/movie/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/movie/providers/nzb/nzbclub.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.nzb.nzbclub.main import Base +from couchpotato.core.media._base.providers.nzb.nzbclub import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex.py b/couchpotato/core/media/movie/providers/nzb/nzbindex.py index 5f98d58d..411f924a 100644 --- a/couchpotato/core/media/movie/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/movie/providers/nzb/nzbindex.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.nzb.nzbindex.main import Base +from couchpotato.core.media._base.providers.nzb.nzbindex import Base from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.environment import Env diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py index f0f96907..653adb6b 100644 --- a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.nzb.omgwtfnzbs.main import Base +from couchpotato.core.media._base.providers.nzb.omgwtfnzbs import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd.py b/couchpotato/core/media/movie/providers/torrent/awesomehd.py index d96b59ab..b1c81f18 100644 --- a/couchpotato/core/media/movie/providers/torrent/awesomehd.py +++ b/couchpotato/core/media/movie/providers/torrent/awesomehd.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.awesomehd.main import Base +from couchpotato.core.media._base.providers.torrent.awesomehd import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv.py b/couchpotato/core/media/movie/providers/torrent/bithdtv.py index f1f9cff4..8b77199b 100644 --- a/couchpotato/core/media/movie/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.bithdtv.main import Base +from couchpotato.core.media._base.providers.torrent.bithdtv import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py index 7170ebd0..578de69d 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.bitsoup.main import Base +from couchpotato.core.media._base.providers.torrent.bitsoup import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits.py b/couchpotato/core/media/movie/providers/torrent/hdbits.py index e44e3b3f..016f1a12 100644 --- a/couchpotato/core/media/movie/providers/torrent/hdbits.py +++ b/couchpotato/core/media/movie/providers/torrent/hdbits.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.hdbits.main import Base +from couchpotato.core.media._base.providers.torrent.hdbits import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py index ad0bac32..cfd773ad 100644 --- a/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.ilovetorrents.main import Base +from couchpotato.core.media._base.providers.torrent.ilovetorrents import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index 21a66422..ba2b738c 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.iptorrents.main import Base +from couchpotato.core.media._base.providers.torrent.iptorrents import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py index d9adeccd..b7bb0253 100644 --- a/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.kickasstorrents.main import Base +from couchpotato.core.media._base.providers.torrent.kickasstorrents import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py index 6c90c14d..1e994135 100644 --- a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.passthepopcorn.main import Base +from couchpotato.core.media._base.providers.torrent.passthepopcorn import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/publichd.py b/couchpotato/core/media/movie/providers/torrent/publichd.py index 4dd7efb6..ce54c468 100644 --- a/couchpotato/core/media/movie/providers/torrent/publichd.py +++ b/couchpotato/core/media/movie/providers/torrent/publichd.py @@ -1,6 +1,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.publichd.main import Base +from couchpotato.core.media._base.providers.torrent.publichd import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py index ead32e23..2a778391 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.event import fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.sceneaccess.main import Base +from couchpotato.core.media._base.providers.torrent.sceneaccess import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py index 4f6f3bb5..457e53d5 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -1,7 +1,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.thepiratebay.main import Base +from couchpotato.core.media._base.providers.torrent.thepiratebay import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py index cef52552..48fc68a4 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.torrentbytes.main import Base +from couchpotato.core.media._base.providers.torrent.torrentbytes import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday.py b/couchpotato/core/media/movie/providers/torrent/torrentday.py index fa90306e..03fe3fe0 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentday.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentday.py @@ -1,6 +1,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.torrentday.main import Base +from couchpotato.core.media._base.providers.torrent.torrentday import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py index 5483adc7..9de6451c 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.torrentleech.main import Base +from couchpotato.core.media._base.providers.torrent.torrentleech import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py index 1e5903a5..9c992071 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.torrentpotato.main import Base +from couchpotato.core.media._base.providers.torrent.torrentpotato import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack.py b/couchpotato/core/media/movie/providers/torrent/torrentshack.py index 8bf7fb94..24ac863a 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack.py @@ -1,7 +1,7 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.torrentshack.main import Base +from couchpotato.core.media._base.providers.torrent.torrentshack import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/torrent/yify.py b/couchpotato/core/media/movie/providers/torrent/yify.py index 16c76949..d30132d6 100644 --- a/couchpotato/core/media/movie/providers/torrent/yify.py +++ b/couchpotato/core/media/movie/providers/torrent/yify.py @@ -1,5 +1,5 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.yify.main import Base +from couchpotato.core.media._base.providers.torrent.yify import Base from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) From 12c08154c582d92d535ae3cc8a2b345233ba446b Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 21:41:29 +0100 Subject: [PATCH 082/468] Optimize imports --- couchpotato/api.py | 12 +++-- couchpotato/core/_base/_core.py | 16 ++++--- couchpotato/core/_base/clientscript.py | 8 ++-- couchpotato/core/_base/scheduler.py | 2 + couchpotato/core/_base/updater/__init__.py | 3 +- couchpotato/core/_base/updater/main.py | 46 +++++++++++++------ couchpotato/core/database.py | 2 + couchpotato/core/downloaders/base.py | 6 ++- couchpotato/core/downloaders/blackhole.py | 6 ++- couchpotato/core/downloaders/deluge.py | 12 +++-- couchpotato/core/downloaders/nzbget.py | 10 ++-- couchpotato/core/downloaders/nzbvortex.py | 10 ++-- couchpotato/core/downloaders/pneumatic.py | 6 ++- couchpotato/core/downloaders/rtorrent.py | 12 +++-- couchpotato/core/downloaders/sabnzbd.py | 12 +++-- couchpotato/core/downloaders/synology.py | 6 ++- couchpotato/core/downloaders/transmission.py | 10 ++-- couchpotato/core/downloaders/utorrent.py | 14 +++--- couchpotato/core/event.py | 6 ++- couchpotato/core/helpers/encoding.py | 4 +- couchpotato/core/helpers/request.py | 5 +- couchpotato/core/helpers/rss.py | 4 +- couchpotato/core/helpers/variable.py | 6 ++- couchpotato/core/loader.py | 10 ++-- couchpotato/core/logger.py | 17 +++++-- couchpotato/core/media/__init__.py | 2 + couchpotato/core/media/_base/media/index.py | 4 +- couchpotato/core/media/_base/media/main.py | 4 +- .../core/media/_base/providers/base.py | 16 ++++--- .../core/media/_base/providers/nzb/base.py | 3 +- .../media/_base/providers/nzb/binsearch.py | 6 ++- .../core/media/_base/providers/nzb/newznab.py | 12 +++-- .../core/media/_base/providers/nzb/nzbclub.py | 4 +- .../media/_base/providers/nzb/nzbindex.py | 6 ++- .../media/_base/providers/nzb/omgwtfnzbs.py | 6 ++- .../_base/providers/torrent/awesomehd.py | 6 ++- .../media/_base/providers/torrent/base.py | 4 +- .../media/_base/providers/torrent/bithdtv.py | 4 +- .../media/_base/providers/torrent/bitsoup.py | 4 +- .../media/_base/providers/torrent/hdbits.py | 7 +-- .../_base/providers/torrent/ilovetorrents.py | 6 ++- .../_base/providers/torrent/iptorrents.py | 4 +- .../providers/torrent/kickasstorrents.py | 6 ++- .../_base/providers/torrent/passthepopcorn.py | 12 +++-- .../media/_base/providers/torrent/publichd.py | 8 ++-- .../_base/providers/torrent/sceneaccess.py | 4 +- .../_base/providers/torrent/thepiratebay.py | 6 ++- .../_base/providers/torrent/torrentbytes.py | 4 +- .../_base/providers/torrent/torrentleech.py | 4 +- .../_base/providers/torrent/torrentpotato.py | 8 ++-- .../_base/providers/torrent/torrentshack.py | 4 +- .../media/_base/providers/torrent/yify.py | 4 +- .../media/_base/providers/userscript/base.py | 4 +- couchpotato/core/media/_base/searcher/main.py | 6 ++- .../core/media/movie/_base/__init__.py | 2 - couchpotato/core/media/movie/_base/main.py | 4 +- .../media/movie/providers/automation/base.py | 4 +- .../media/movie/providers/automation/imdb.py | 1 - .../movie/providers/automation/itunes.py | 10 ++-- .../movie/providers/automation/kinepolis.py | 4 +- .../movie/providers/automation/letterboxd.py | 4 +- .../providers/automation/rottentomatoes.py | 8 ++-- .../media/movie/providers/automation/trakt.py | 4 +- .../media/movie/providers/info/_modifier.py | 6 ++- .../media/movie/providers/metadata/base.py | 8 ++-- .../media/movie/providers/metadata/wmc.py | 4 +- .../media/movie/providers/metadata/xbmc.py | 10 ++-- .../movie/providers/trailer/hdtrailers.py | 8 ++-- .../movie/providers/userscript/allocine.py | 4 +- .../providers/userscript/appletrailers.py | 4 +- .../movie/providers/userscript/filmweb.py | 4 +- .../movie/providers/userscript/flickchart.py | 4 +- .../providers/userscript/rottentomatoes.py | 6 ++- .../media/movie/providers/userscript/tmdb.py | 4 +- .../movie/providers/userscript/youteather.py | 4 +- couchpotato/core/media/movie/searcher.py | 12 +++-- .../core/media/movie/suggestion/__init__.py | 2 - couchpotato/core/notifications/boxcar.py | 4 +- couchpotato/core/notifications/core/index.py | 1 - couchpotato/core/notifications/core/main.py | 12 +++-- couchpotato/core/notifications/email.py | 10 ++-- couchpotato/core/notifications/growl.py | 4 +- couchpotato/core/notifications/nmj.py | 6 ++- couchpotato/core/notifications/plex/client.py | 2 + couchpotato/core/notifications/plex/server.py | 5 +- couchpotato/core/notifications/prowl.py | 4 +- couchpotato/core/notifications/pushalot.py | 4 +- couchpotato/core/notifications/pushbullet.py | 6 ++- couchpotato/core/notifications/pushover.py | 4 +- couchpotato/core/notifications/synoindex.py | 6 ++- couchpotato/core/notifications/toasty.py | 4 +- .../core/notifications/twitter/main.py | 4 +- couchpotato/core/notifications/xbmc.py | 8 ++-- couchpotato/core/notifications/xmpp.py | 6 ++- couchpotato/core/plugins/base.py | 18 ++++---- couchpotato/core/plugins/browser.py | 8 ++-- couchpotato/core/plugins/category/main.py | 2 + couchpotato/core/plugins/custom.py | 4 +- couchpotato/core/plugins/dashboard.py | 6 ++- couchpotato/core/plugins/file.py | 8 ++-- couchpotato/core/plugins/log/main.py | 6 ++- couchpotato/core/plugins/manage.py | 12 +++-- couchpotato/core/plugins/profile/main.py | 2 + couchpotato/core/plugins/quality/__init__.py | 2 - couchpotato/core/plugins/quality/index.py | 3 +- couchpotato/core/plugins/quality/main.py | 4 +- couchpotato/core/plugins/release/index.py | 1 + couchpotato/core/plugins/release/main.py | 10 ++-- couchpotato/core/plugins/renamer.py | 14 +++--- couchpotato/core/plugins/scanner.py | 12 +++-- couchpotato/core/plugins/score/scores.py | 6 ++- couchpotato/core/plugins/subtitle.py | 4 +- couchpotato/core/plugins/trailer.py | 4 +- couchpotato/core/plugins/userscript/main.py | 4 +- couchpotato/core/settings.py | 5 +- couchpotato/environment.py | 3 +- couchpotato/runner.py | 24 +++++----- 117 files changed, 507 insertions(+), 279 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index ffa78cea..5fdee986 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,15 +1,17 @@ -from couchpotato.core.helpers.request import getParams -from couchpotato.core.logger import CPLog from functools import wraps from threading import Thread -from tornado.gen import coroutine -from tornado.web import RequestHandler, asynchronous import json import threading -import tornado import traceback import urllib +from couchpotato.core.helpers.request import getParams +from couchpotato.core.logger import CPLog +from tornado.gen import coroutine +from tornado.web import RequestHandler, asynchronous +import tornado + + log = CPLog(__name__) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index ee266512..730bc884 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -1,10 +1,3 @@ -from couchpotato.api import addApiView -from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.variable import cleanHost, md5 -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.environment import Env -from tornado.ioloop import IOLoop from uuid import uuid4 import os import platform @@ -13,6 +6,15 @@ import time import traceback import webbrowser +from couchpotato.api import addApiView +from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.variable import cleanHost, md5 +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.environment import Env +from tornado.ioloop import IOLoop + + log = CPLog(__name__) autoload = 'Core' diff --git a/couchpotato/core/_base/clientscript.py b/couchpotato/core/_base/clientscript.py index 42ad28af..593593af 100644 --- a/couchpotato/core/_base/clientscript.py +++ b/couchpotato/core/_base/clientscript.py @@ -1,3 +1,7 @@ +import os +import re +import traceback + from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.variable import tryInt @@ -7,9 +11,7 @@ from couchpotato.environment import Env from minify.cssmin import cssmin from minify.jsmin import jsmin from tornado.web import StaticFileHandler -import os -import re -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/_base/scheduler.py b/couchpotato/core/_base/scheduler.py index ae5bfd3b..16a3a4f2 100644 --- a/couchpotato/core/_base/scheduler.py +++ b/couchpotato/core/_base/scheduler.py @@ -68,6 +68,8 @@ class Scheduler(Plugin): 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) } + return True + def queue(self, handlers = None): if not handlers: handlers = [] diff --git a/couchpotato/core/_base/updater/__init__.py b/couchpotato/core/_base/updater/__init__.py index a62eafb5..16e08b81 100644 --- a/couchpotato/core/_base/updater/__init__.py +++ b/couchpotato/core/_base/updater/__init__.py @@ -1,6 +1,7 @@ +import os + from .main import Updater from couchpotato.environment import Env -import os def autoload(): diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 4f96e929..17ed1bdd 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -1,29 +1,33 @@ -from couchpotato.api import addApiView -from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.helpers.encoding import ss -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.environment import Env -from datetime import datetime -from dateutil.parser import parse -from git.repository import LocalRepository import json import os import shutil import tarfile import time import traceback +import zipfile +from datetime import datetime +from threading import RLock + +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.helpers.encoding import ss +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.environment import Env +from dateutil.parser import parse +from git.repository import LocalRepository from scandir import scandir import version -import zipfile from six.moves import filter + log = CPLog(__name__) class Updater(Plugin): available_notified = False + _lock = RLock() def __init__(self): @@ -101,7 +105,17 @@ class Updater(Plugin): return False def info(self, **kwargs): - return self.updater.info() + self._lock.acquire() + + info = {} + try: + info = self.updater.info() + except: + log.error('Failed getting updater info: %s', traceback.format_exc()) + + self._lock.release() + + return info def checkView(self, **kwargs): return { @@ -145,12 +159,15 @@ class BaseUpdater(Plugin): pass def info(self): + + version = self.getVersion() + return { 'last_check': self.last_check, 'update_version': self.update_version, - 'version': self.getVersion(), + 'version': version, 'repo_name': '%s/%s' % (self.repo_user, self.repo_name), - 'branch': self.branch, + 'branch': version.get('branch', self.branch), } def getVersion(self): @@ -213,10 +230,11 @@ class GitUpdater(BaseUpdater): output = self.repo.getHead() # Yes, please log.debug('Git version output: %s', output.hash) self.version = { - 'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())), + 'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())), 'hash': output.hash[:8], 'date': output.getDate(), 'type': 'git', + 'branch': self.repo.getCurrentBranch().name } except Exception as e: log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 59221813..33ae0d68 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -2,12 +2,14 @@ import json import os import time import traceback + from couchpotato import CPLog from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getImdb + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index f2a0c1dc..bb7db606 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,12 +1,14 @@ from base64 import b32decode, b16encode +import random +import re + from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog -import random -import re from couchpotato.core.media._base.providers.base import Provider + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/blackhole.py b/couchpotato/core/downloaders/blackhole.py index dbacde0a..d77e7464 100644 --- a/couchpotato/core/downloaders/blackhole.py +++ b/couchpotato/core/downloaders/blackhole.py @@ -1,11 +1,13 @@ from __future__ import with_statement +import os +import traceback + from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getDownloadDir from couchpotato.core.logger import CPLog from couchpotato.environment import Env -import os -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/deluge.py b/couchpotato/core/downloaders/deluge.py index 65e4aa92..2a386ad3 100644 --- a/couchpotato/core/downloaders/deluge.py +++ b/couchpotato/core/downloaders/deluge.py @@ -1,15 +1,17 @@ from base64 import b64encode, b16encode, b32decode +from datetime import timedelta +from hashlib import sha1 +import os.path +import re +import traceback + from bencode import bencode as benc, bdecode from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.variable import tryFloat, cleanHost from couchpotato.core.logger import CPLog -from datetime import timedelta -from hashlib import sha1 from synchronousdeluge import DelugeClient -import os.path -import re -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/nzbget.py b/couchpotato/core/downloaders/nzbget.py index 743e2768..115e0d24 100644 --- a/couchpotato/core/downloaders/nzbget.py +++ b/couchpotato/core/downloaders/nzbget.py @@ -1,8 +1,4 @@ from base64 import standard_b64encode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import ss, sp -from couchpotato.core.helpers.variable import tryInt, md5, cleanHost -from couchpotato.core.logger import CPLog from datetime import timedelta import re import shutil @@ -10,6 +6,12 @@ import socket import traceback import xmlrpclib +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import ss, sp +from couchpotato.core.helpers.variable import tryInt, md5, cleanHost +from couchpotato.core.logger import CPLog + + log = CPLog(__name__) autoload = 'NZBGet' diff --git a/couchpotato/core/downloaders/nzbvortex.py b/couchpotato/core/downloaders/nzbvortex.py index 1084e668..573e2592 100644 --- a/couchpotato/core/downloaders/nzbvortex.py +++ b/couchpotato/core/downloaders/nzbvortex.py @@ -1,8 +1,4 @@ from base64 import b64encode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import tryUrlencode, sp -from couchpotato.core.helpers.variable import cleanHost -from couchpotato.core.logger import CPLog from urllib2 import URLError from uuid import uuid4 import hashlib @@ -16,6 +12,12 @@ import time import traceback import urllib2 +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import tryUrlencode, sp +from couchpotato.core.helpers.variable import cleanHost +from couchpotato.core.logger import CPLog + + log = CPLog(__name__) autoload = 'NZBVortex' diff --git a/couchpotato/core/downloaders/pneumatic.py b/couchpotato/core/downloaders/pneumatic.py index 823cd388..7500b18f 100644 --- a/couchpotato/core/downloaders/pneumatic.py +++ b/couchpotato/core/downloaders/pneumatic.py @@ -1,9 +1,11 @@ from __future__ import with_statement +import os +import traceback + from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog -import os -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/rtorrent.py b/couchpotato/core/downloaders/rtorrent.py index 5ca623ad..fb8948c6 100644 --- a/couchpotato/core/downloaders/rtorrent.py +++ b/couchpotato/core/downloaders/rtorrent.py @@ -1,18 +1,20 @@ +from base64 import b16encode, b32decode +from datetime import timedelta +from hashlib import sha1 +from urlparse import urlparse +import os + from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.logger import CPLog -from base64 import b16encode, b32decode from bencode import bencode, bdecode -from datetime import timedelta -from hashlib import sha1 from rtorrent import RTorrent from rtorrent.err import MethodError -from urlparse import urlparse -import os from scandir import scandir + log = CPLog(__name__) autoload = 'rTorrent' diff --git a/couchpotato/core/downloaders/sabnzbd.py b/couchpotato/core/downloaders/sabnzbd.py index 33e51fa7..548889fa 100644 --- a/couchpotato/core/downloaders/sabnzbd.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -1,14 +1,16 @@ -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp -from couchpotato.core.helpers.variable import cleanHost, mergeDicts -from couchpotato.core.logger import CPLog -from couchpotato.environment import Env from datetime import timedelta from urllib2 import URLError import json import os import traceback +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp +from couchpotato.core.helpers.variable import cleanHost, mergeDicts +from couchpotato.core.logger import CPLog +from couchpotato.environment import Env + + log = CPLog(__name__) autoload = 'Sabnzbd' diff --git a/couchpotato/core/downloaders/synology.py b/couchpotato/core/downloaders/synology.py index edc732a2..46d889fd 100644 --- a/couchpotato/core/downloaders/synology.py +++ b/couchpotato/core/downloaders/synology.py @@ -1,10 +1,12 @@ +import json +import traceback + from couchpotato.core.downloaders.base import Downloader from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog -import json import requests -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index a9567805..f49cb9df 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -1,8 +1,4 @@ from base64 import b64encode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import isInt, sp -from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost -from couchpotato.core.logger import CPLog from datetime import timedelta import httplib import json @@ -10,6 +6,12 @@ import os.path import re import urllib2 +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import isInt, sp +from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost +from couchpotato.core.logger import CPLog + + log = CPLog(__name__) autoload = 'Transmission' diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index 345b17e5..dae89505 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -1,12 +1,6 @@ from base64 import b16encode, b32decode -from bencode import bencode as benc, bdecode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList -from couchpotato.core.helpers.encoding import isInt, ss, sp -from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost -from couchpotato.core.logger import CPLog from datetime import timedelta from hashlib import sha1 -from multipartpost import MultipartPostHandler import cookielib import httplib import json @@ -17,6 +11,14 @@ import time import urllib import urllib2 +from bencode import bencode as benc, bdecode +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import isInt, ss, sp +from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost +from couchpotato.core.logger import CPLog +from multipartpost import MultipartPostHandler + + log = CPLog(__name__) autoload = 'uTorrent' diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 8af2967e..7246cde1 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -1,8 +1,10 @@ +import threading +import traceback + from axl.axel import Event from couchpotato.core.helpers.variable import mergeDicts, natsortKey from couchpotato.core.logger import CPLog -import threading -import traceback + log = CPLog(__name__) events = {} diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py index f896a62e..ff6a3e59 100644 --- a/couchpotato/core/helpers/encoding.py +++ b/couchpotato/core/helpers/encoding.py @@ -1,12 +1,14 @@ -from couchpotato.core.logger import CPLog from string import ascii_letters, digits from urllib import quote_plus import os import re import traceback import unicodedata + +from couchpotato.core.logger import CPLog import six + log = CPLog(__name__) diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index a0baa85f..88bd7a0c 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -1,8 +1,9 @@ -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import natsortKey from urllib import unquote import re +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import natsortKey + def getParams(params): diff --git a/couchpotato/core/helpers/rss.py b/couchpotato/core/helpers/rss.py index 1a4d37c2..f455007e 100644 --- a/couchpotato/core/helpers/rss.py +++ b/couchpotato/core/helpers/rss.py @@ -1,6 +1,8 @@ -from couchpotato.core.logger import CPLog import xml.etree.ElementTree as XMLTree +from couchpotato.core.logger import CPLog + + log = CPLog(__name__) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index ede89ccc..5eab960c 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -1,5 +1,3 @@ -from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss -from couchpotato.core.logger import CPLog import collections import hashlib import os @@ -8,9 +6,13 @@ import random import re import string import sys + +from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss +from couchpotato.core.logger import CPLog import six from six.moves import map, zip, filter + log = CPLog(__name__) diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 1b4fc5fa..6d84c45e 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -1,11 +1,13 @@ -from couchpotato.core.event import fireEvent -from couchpotato.core.logger import CPLog -from importhelper import import_module import os import sys import traceback + +from couchpotato.core.event import fireEvent +from couchpotato.core.logger import CPLog +from importhelper import import_module import six + log = CPLog(__name__) @@ -124,7 +126,7 @@ class Loader(object): def loadPlugins(self, module, name): if not hasattr(module, 'autoload'): - log.debug('Skip startup for plugin %s as it has no start section' % module.__file__) + #log.debug('Skip startup for plugin %s as it has no start section' % module.__file__) return False try: # Load single file plugin diff --git a/couchpotato/core/logger.py b/couchpotato/core/logger.py index 8223f146..6bbe1953 100644 --- a/couchpotato/core/logger.py +++ b/couchpotato/core/logger.py @@ -7,6 +7,9 @@ class CPLog(object): context = '' replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey'] + Env = None + is_develop = False + def __init__(self, context = ''): if context.endswith('.main'): context = context[:-5] @@ -14,6 +17,14 @@ class CPLog(object): self.context = context self.logger = logging.getLogger() + def setup(self): + + if not self.Env: + from couchpotato.environment import Env + + self.Env = Env + self.is_develop = Env.get('dev') + def info(self, msg, replace_tuple = ()): self.logger.info(self.addContext(msg, replace_tuple)) @@ -37,7 +48,6 @@ class CPLog(object): def safeMessage(self, msg, replace_tuple = ()): - from couchpotato.environment import Env from couchpotato.core.helpers.encoding import ss, toUnicode msg = ss(msg) @@ -53,7 +63,8 @@ class CPLog(object): except Exception as e: self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e)) - if not Env.get('dev'): + self.setup() + if not self.is_develop: for replace in self.replace_private: msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg) @@ -61,7 +72,7 @@ class CPLog(object): # Replace api key try: - api_key = Env.setting('api_key') + api_key = self.Env.setting('api_key') if api_key: msg = msg.replace(api_key, 'API_KEY') except: diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index aee1a59b..97c98393 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,8 +1,10 @@ import traceback + from couchpotato import get_db, CPLog from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.plugins.base import Plugin + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 956cac56..dc5bab6f 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -1,8 +1,8 @@ -from itertools import izip from string import ascii_letters +from hashlib import md5 + from CodernityDB.hash_index import HashIndex from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex -from hashlib import md5 from couchpotato.core.helpers.encoding import toUnicode, simplifyString diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 08082338..a3d99170 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,4 +1,6 @@ import traceback +from string import ascii_lowercase + from couchpotato import tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent @@ -7,7 +9,7 @@ from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase from .index import MediaIMDBIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex -from string import ascii_lowercase + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 95ab1284..94c70a01 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -1,10 +1,3 @@ -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import ss -from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ - possibleTitles, getTitle -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.environment import Env from urlparse import urlparse import json import re @@ -12,6 +5,15 @@ import time import traceback import xml.etree.ElementTree as XMLTree +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import ss +from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ + possibleTitles +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.environment import Env + + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/nzb/base.py b/couchpotato/core/media/_base/providers/nzb/base.py index 31cbb477..5e19e524 100644 --- a/couchpotato/core/media/_base/providers/nzb/base.py +++ b/couchpotato/core/media/_base/providers/nzb/base.py @@ -1,6 +1,7 @@ -from couchpotato.core.media._base.providers.base import YarrProvider import time +from couchpotato.core.media._base.providers.base import YarrProvider + class NZBProvider(YarrProvider): diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index 0334bb49..270b2a11 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -1,10 +1,12 @@ +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import re -import traceback from couchpotato.core.media._base.providers.nzb.base import NZBProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index bbc77a3b..c6430363 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -1,3 +1,9 @@ +from urllib2 import HTTPError +from urlparse import urlparse +import time +import traceback +import urllib2 + from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt @@ -5,11 +11,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.base import NZBProvider from couchpotato.environment import Env from dateutil.parser import parse -from urllib2 import HTTPError -from urlparse import urlparse -import time -import traceback -import urllib2 + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub.py b/couchpotato/core/media/_base/providers/nzb/nzbclub.py index 562de5ca..6ca66718 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub.py @@ -1,3 +1,5 @@ +import time + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.rss import RSS @@ -5,7 +7,7 @@ from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex.py b/couchpotato/core/media/_base/providers/nzb/nzbindex.py index c81165e1..8baa59a4 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex.py @@ -1,3 +1,6 @@ +import re +import time + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.rss import RSS @@ -6,8 +9,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.event import fireEvent from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse -import re -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index 29434262..1db101a2 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -1,3 +1,6 @@ +from urlparse import urlparse, parse_qs +import time + from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.rss import RSS @@ -5,8 +8,7 @@ from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.base import NZBProvider from dateutil.parser import parse -from urlparse import urlparse, parse_qs -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd.py b/couchpotato/core/media/_base/providers/torrent/awesomehd.py index f8098626..bf104561 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd.py @@ -1,10 +1,12 @@ +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import re -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/base.py b/couchpotato/core/media/_base/providers/torrent/base.py index ea582e49..585768b4 100644 --- a/couchpotato/core/media/_base/providers/torrent/base.py +++ b/couchpotato/core/media/_base/providers/torrent/base.py @@ -1,8 +1,10 @@ +import time + from couchpotato.core.helpers.variable import getImdb, md5, cleanHost from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.base import YarrProvider from couchpotato.environment import Env -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py index 619ae91a..b55bfff0 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index fd2f4dbe..d841c0ff 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index 4d31bb3e..73bfc169 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -1,11 +1,12 @@ -from couchpotato.core.helpers.variable import tryInt -from couchpotato.core.logger import CPLog - import re import json import traceback + +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index 1f74db48..d9db53b5 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -1,11 +1,13 @@ +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import re -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index 71302f9e..4b9bd79c 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -1,11 +1,13 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index f99ead0d..8f82e263 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -1,10 +1,12 @@ +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import re -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index cb845bfb..56550d2b 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -1,15 +1,17 @@ -from couchpotato.core.helpers.encoding import tryUrlencode -from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts -from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.base import TorrentProvider -from dateutil.parser import parse import htmlentitydefs import json import re import time import traceback + +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider +from dateutil.parser import parse import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/publichd.py b/couchpotato/core/media/_base/providers/torrent/publichd.py index 39e7f60a..c17b655a 100644 --- a/couchpotato/core/media/_base/providers/torrent/publichd.py +++ b/couchpotato/core/media/_base/providers/torrent/publichd.py @@ -1,13 +1,15 @@ +from urlparse import parse_qs +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from urlparse import parse_qs -import re -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py index a8ee615e..9a27b051 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index f8e631d8..b0a778ce 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -1,12 +1,14 @@ +import re +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import re -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py index 603cd6ba..afb66eb3 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py index 36bd0f70..208520df 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py index 00ed433f..2a17b2a4 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py @@ -1,11 +1,13 @@ -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat -from couchpotato.core.logger import CPLog from urlparse import urlparse import re import traceback + +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat +from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index cc2a599d..6c9d4cc5 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -1,10 +1,12 @@ +import traceback + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 3ba287ac..84ce80b7 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -1,8 +1,10 @@ +import traceback + from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -import traceback from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/providers/userscript/base.py b/couchpotato/core/media/_base/providers/userscript/base.py index 531510b0..6491ac34 100644 --- a/couchpotato/core/media/_base/providers/userscript/base.py +++ b/couchpotato/core/media/_base/providers/userscript/base.py @@ -1,9 +1,11 @@ +from urlparse import urlparse + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import getImdb, md5 from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from urlparse import urlparse + log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 8c0eaa0f..5a280438 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -1,11 +1,13 @@ +import datetime +import re + from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase -import datetime -import re + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/_base/__init__.py b/couchpotato/core/media/movie/_base/__init__.py index acec594f..14720463 100644 --- a/couchpotato/core/media/movie/_base/__init__.py +++ b/couchpotato/core/media/movie/_base/__init__.py @@ -3,5 +3,3 @@ from .main import MovieBase def autoload(): return MovieBase() - -config = [] diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 62f2b2ad..8112eda3 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -1,5 +1,7 @@ import os import traceback +import time + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent @@ -7,9 +9,9 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, getTitle, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase -import time import six + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py index 68c259ac..5b228653 100644 --- a/couchpotato/core/media/movie/providers/automation/base.py +++ b/couchpotato/core/media/movie/providers/automation/base.py @@ -1,9 +1,11 @@ +import time + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.automation.base import AutomationBase from couchpotato.environment import Env from couchpotato.core.helpers.variable import splitString -import time + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index d5b92259..10150f2d 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -5,7 +5,6 @@ from bs4 import BeautifulSoup from couchpotato import fireEvent from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import getImdb, splitString, tryInt - from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.base import MultiProvider from couchpotato.core.media.movie.providers.automation.base import Automation diff --git a/couchpotato/core/media/movie/providers/automation/itunes.py b/couchpotato/core/media/movie/providers/automation/itunes.py index 1feb96fa..bb6eef29 100644 --- a/couchpotato/core/media/movie/providers/automation/itunes.py +++ b/couchpotato/core/media/movie/providers/automation/itunes.py @@ -1,12 +1,14 @@ -from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5, splitString, tryInt -from couchpotato.core.logger import CPLog -from couchpotato.core.media.movie.providers.automation.base import Automation from xml.etree.ElementTree import QName import datetime import traceback import xml.etree.ElementTree as XMLTree +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.helpers.variable import md5, splitString, tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media.movie.providers.automation.base import Automation + + log = CPLog(__name__) autoload = 'ITunes' diff --git a/couchpotato/core/media/movie/providers/automation/kinepolis.py b/couchpotato/core/media/movie/providers/automation/kinepolis.py index 93505723..a0f25965 100644 --- a/couchpotato/core/media/movie/providers/automation/kinepolis.py +++ b/couchpotato/core/media/movie/providers/automation/kinepolis.py @@ -1,7 +1,9 @@ +import datetime + from couchpotato.core.helpers.rss import RSS from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.automation.base import Automation -import datetime + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/automation/letterboxd.py b/couchpotato/core/media/movie/providers/automation/letterboxd.py index 55a95498..fa914d8a 100644 --- a/couchpotato/core/media/movie/providers/automation/letterboxd.py +++ b/couchpotato/core/media/movie/providers/automation/letterboxd.py @@ -1,8 +1,10 @@ +import re + from bs4 import BeautifulSoup from couchpotato.core.helpers.variable import tryInt, splitString, removeEmpty from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.automation.base import Automation -import re + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/automation/rottentomatoes.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py index 279b4f8a..a01f76d2 100644 --- a/couchpotato/core/media/movie/providers/automation/rottentomatoes.py +++ b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py @@ -1,10 +1,12 @@ +from xml.etree.ElementTree import QName +import datetime +import re + from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.automation.base import Automation -from xml.etree.ElementTree import QName -import datetime -import re + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/automation/trakt.py b/couchpotato/core/media/movie/providers/automation/trakt.py index 0c945520..440a59d8 100644 --- a/couchpotato/core/media/movie/providers/automation/trakt.py +++ b/couchpotato/core/media/movie/providers/automation/trakt.py @@ -1,8 +1,10 @@ +import base64 + from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import sha1 from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.automation.base import Automation -import base64 + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index 9e822234..20697b0b 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -1,11 +1,13 @@ +import copy +import traceback + from CodernityDB.database import RecordNotFound from couchpotato import get_db from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -import copy -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index 7dacf554..97d5206e 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -1,11 +1,13 @@ +import os +import shutil +import traceback + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.metadata.base import MetaDataBase from couchpotato.environment import Env -import os -import shutil -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/metadata/wmc.py b/couchpotato/core/media/movie/providers/metadata/wmc.py index 50db02db..8faa6d2a 100644 --- a/couchpotato/core/media/movie/providers/metadata/wmc.py +++ b/couchpotato/core/media/movie/providers/metadata/wmc.py @@ -1,6 +1,8 @@ -from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData import os +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData + + autoload = 'WindowsMediaCenter' diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index e9904cf7..9f6bb7dc 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -1,13 +1,15 @@ -from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import getTitle -from couchpotato.core.logger import CPLog from xml.etree.ElementTree import Element, SubElement, tostring import os import re import traceback import xml.dom.minidom +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import getTitle +from couchpotato.core.logger import CPLog + + log = CPLog(__name__) autoload = 'XBMC' diff --git a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py index 1f5b4f63..084c8ea2 100644 --- a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py +++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py @@ -1,12 +1,14 @@ +from string import digits, ascii_letters +from urllib2 import HTTPError +import re + from bs4 import SoupStrainer, BeautifulSoup from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import mergeDicts, getTitle from couchpotato.core.logger import CPLog -from string import digits, ascii_letters -from urllib2 import HTTPError -import re from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider + log = CPLog(__name__) autoload = 'HDTrailers' diff --git a/couchpotato/core/media/movie/providers/userscript/allocine.py b/couchpotato/core/media/movie/providers/userscript/allocine.py index 2eaef42e..238a6b54 100644 --- a/couchpotato/core/media/movie/providers/userscript/allocine.py +++ b/couchpotato/core/media/movie/providers/userscript/allocine.py @@ -1,7 +1,9 @@ -from couchpotato.core.logger import CPLog import traceback + +from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + log = CPLog(__name__) autoload = 'AlloCine' diff --git a/couchpotato/core/media/movie/providers/userscript/appletrailers.py b/couchpotato/core/media/movie/providers/userscript/appletrailers.py index af201ed5..c59722c7 100644 --- a/couchpotato/core/media/movie/providers/userscript/appletrailers.py +++ b/couchpotato/core/media/movie/providers/userscript/appletrailers.py @@ -1,6 +1,8 @@ -from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + + autoload = 'AppleTrailers' diff --git a/couchpotato/core/media/movie/providers/userscript/filmweb.py b/couchpotato/core/media/movie/providers/userscript/filmweb.py index f4e36529..a4a51a08 100644 --- a/couchpotato/core/media/movie/providers/userscript/filmweb.py +++ b/couchpotato/core/media/movie/providers/userscript/filmweb.py @@ -1,6 +1,8 @@ -from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + + autoload = 'Filmweb' diff --git a/couchpotato/core/media/movie/providers/userscript/flickchart.py b/couchpotato/core/media/movie/providers/userscript/flickchart.py index 2df0acf8..c54a8cab 100644 --- a/couchpotato/core/media/movie/providers/userscript/flickchart.py +++ b/couchpotato/core/media/movie/providers/userscript/flickchart.py @@ -1,7 +1,9 @@ +import traceback + from couchpotato.core.event import fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.userscript.base import UserscriptBase -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py index cb661467..902192e2 100644 --- a/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py +++ b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py @@ -1,8 +1,10 @@ -from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re import traceback +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + + log = CPLog(__name__) autoload = 'RottenTomatoes' diff --git a/couchpotato/core/media/movie/providers/userscript/tmdb.py b/couchpotato/core/media/movie/providers/userscript/tmdb.py index 71ab0d7b..f11dba27 100644 --- a/couchpotato/core/media/movie/providers/userscript/tmdb.py +++ b/couchpotato/core/media/movie/providers/userscript/tmdb.py @@ -1,6 +1,8 @@ +import re + from couchpotato.core.event import fireEvent from couchpotato.core.media._base.providers.userscript.base import UserscriptBase -import re + autoload = 'TMDB' diff --git a/couchpotato/core/media/movie/providers/userscript/youteather.py b/couchpotato/core/media/movie/providers/userscript/youteather.py index 3a35df37..e7e63b46 100644 --- a/couchpotato/core/media/movie/providers/userscript/youteather.py +++ b/couchpotato/core/media/movie/providers/userscript/youteather.py @@ -1,6 +1,8 @@ -from couchpotato.core.media._base.providers.userscript.base import UserscriptBase import re +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + + autoload = 'YouTheater' diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 192040fc..2043d01c 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -1,3 +1,9 @@ +from datetime import date +import random +import re +import time +import traceback + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync @@ -7,11 +13,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media.movie import MovieTypeBase from couchpotato.environment import Env -from datetime import date -import random -import re -import time -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/media/movie/suggestion/__init__.py b/couchpotato/core/media/movie/suggestion/__init__.py index 50083fe7..81e42f03 100644 --- a/couchpotato/core/media/movie/suggestion/__init__.py +++ b/couchpotato/core/media/movie/suggestion/__init__.py @@ -3,5 +3,3 @@ from .main import Suggestion def start(): return Suggestion() - -config = [] diff --git a/couchpotato/core/notifications/boxcar.py b/couchpotato/core/notifications/boxcar.py index f5006ef9..3777a42d 100644 --- a/couchpotato/core/notifications/boxcar.py +++ b/couchpotato/core/notifications/boxcar.py @@ -1,7 +1,9 @@ +import time + from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import time + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py index 6d65b905..c7985b5b 100644 --- a/couchpotato/core/notifications/core/index.py +++ b/couchpotato/core/notifications/core/index.py @@ -1,4 +1,3 @@ -import time from CodernityDB.tree_index import TreeBasedIndex diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index eefa0dc3..1a506c1e 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -1,3 +1,9 @@ +from operator import itemgetter +import threading +import time +import traceback +import uuid + from couchpotato import get_db from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.core.event import addEvent, fireEvent @@ -7,11 +13,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from .index import NotificationIndex, NotificationUnreadIndex from couchpotato.environment import Env -from operator import itemgetter -import threading -import time -import traceback -import uuid + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/email.py b/couchpotato/core/notifications/email.py index 0a9ff069..b42650b9 100644 --- a/couchpotato/core/notifications/email.py +++ b/couchpotato/core/notifications/email.py @@ -1,12 +1,14 @@ +from email.mime.text import MIMEText +from email.utils import formatdate, make_msgid +import smtplib +import traceback + from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from couchpotato.environment import Env -from email.mime.text import MIMEText -from email.utils import formatdate, make_msgid -import smtplib -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/growl.py b/couchpotato/core/notifications/growl.py index 8d67efcc..c7847da2 100644 --- a/couchpotato/core/notifications/growl.py +++ b/couchpotato/core/notifications/growl.py @@ -1,9 +1,11 @@ +import traceback + from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from couchpotato.environment import Env from gntp import notifier -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/nmj.py b/couchpotato/core/notifications/nmj.py index 12bb2ad7..6ef70d43 100644 --- a/couchpotato/core/notifications/nmj.py +++ b/couchpotato/core/notifications/nmj.py @@ -1,10 +1,12 @@ +import re +import telnetlib + from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import re -import telnetlib + try: import xml.etree.cElementTree as etree diff --git a/couchpotato/core/notifications/plex/client.py b/couchpotato/core/notifications/plex/client.py index 8864230d..84cf7af6 100644 --- a/couchpotato/core/notifications/plex/client.py +++ b/couchpotato/core/notifications/plex/client.py @@ -1,9 +1,11 @@ import json + from couchpotato import CPLog from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import tryUrlencode import requests + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py index b66db8fe..cd11f49b 100644 --- a/couchpotato/core/notifications/plex/server.py +++ b/couchpotato/core/notifications/plex/server.py @@ -1,9 +1,10 @@ from datetime import timedelta, datetime -from couchpotato.core.helpers.variable import cleanHost -from couchpotato import CPLog from urlparse import urlparse import traceback +from couchpotato.core.helpers.variable import cleanHost +from couchpotato import CPLog + try: import xml.etree.cElementTree as etree diff --git a/couchpotato/core/notifications/prowl.py b/couchpotato/core/notifications/prowl.py index ecadb7f5..b276d7f4 100644 --- a/couchpotato/core/notifications/prowl.py +++ b/couchpotato/core/notifications/prowl.py @@ -1,7 +1,9 @@ +import traceback + from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/pushalot.py b/couchpotato/core/notifications/pushalot.py index 9f717e42..0ce9027f 100644 --- a/couchpotato/core/notifications/pushalot.py +++ b/couchpotato/core/notifications/pushalot.py @@ -1,7 +1,9 @@ +import traceback + from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/pushbullet.py b/couchpotato/core/notifications/pushbullet.py index beda0b3a..fd1a376b 100644 --- a/couchpotato/core/notifications/pushbullet.py +++ b/couchpotato/core/notifications/pushbullet.py @@ -1,9 +1,11 @@ +import base64 +import json + from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import base64 -import json + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index c5e5257b..95593747 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -1,8 +1,10 @@ +from httplib import HTTPSConnection + from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -from httplib import HTTPSConnection + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/synoindex.py b/couchpotato/core/notifications/synoindex.py index 7fd20841..cd4ff8f9 100644 --- a/couchpotato/core/notifications/synoindex.py +++ b/couchpotato/core/notifications/synoindex.py @@ -1,8 +1,10 @@ +import os +import subprocess + from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import os -import subprocess + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/toasty.py b/couchpotato/core/notifications/toasty.py index d0bbe8a5..919a9d1d 100644 --- a/couchpotato/core/notifications/toasty.py +++ b/couchpotato/core/notifications/toasty.py @@ -1,7 +1,9 @@ +import traceback + from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py index 559c830f..0d02191e 100644 --- a/couchpotato/core/notifications/twitter/main.py +++ b/couchpotato/core/notifications/twitter/main.py @@ -1,3 +1,5 @@ +from urlparse import parse_qsl + from couchpotato.api import addApiView from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import cleanHost @@ -5,9 +7,9 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from couchpotato.environment import Env from pytwitter import Api -from urlparse import parse_qsl import oauth2 + log = CPLog(__name__) diff --git a/couchpotato/core/notifications/xbmc.py b/couchpotato/core/notifications/xbmc.py index 48c5c9bb..682801cf 100644 --- a/couchpotato/core/notifications/xbmc.py +++ b/couchpotato/core/notifications/xbmc.py @@ -1,14 +1,16 @@ -from couchpotato.core.helpers.variable import splitString, getTitle -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification import base64 import json import socket import traceback import urllib + +from couchpotato.core.helpers.variable import splitString, getTitle +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification import requests from requests.packages.urllib3.exceptions import MaxRetryError + log = CPLog(__name__) autoload = 'XBMC' diff --git a/couchpotato/core/notifications/xmpp.py b/couchpotato/core/notifications/xmpp.py index 6a978e5b..f9916cd0 100644 --- a/couchpotato/core/notifications/xmpp.py +++ b/couchpotato/core/notifications/xmpp.py @@ -1,9 +1,11 @@ -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification from time import sleep import traceback + +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification import xmpp + log = CPLog(__name__) autoload = 'Xmpp' diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 77440239..2bd15e55 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -1,3 +1,12 @@ +from urlparse import urlparse +import glob +import inspect +import os.path +import re +import time +import traceback +import urllib2 + from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toSafeString, \ @@ -10,14 +19,7 @@ from requests.packages.urllib3 import Timeout from requests.packages.urllib3.exceptions import MaxRetryError from tornado import template from tornado.web import StaticFileHandler -from urlparse import urlparse -import glob -import inspect -import os.path -import re -import time -import traceback -import urllib2 + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/browser.py b/couchpotato/core/plugins/browser.py index 8bfe53cc..9492a2d9 100644 --- a/couchpotato/core/plugins/browser.py +++ b/couchpotato/core/plugins/browser.py @@ -1,11 +1,13 @@ -from couchpotato.api import addApiView -from couchpotato.core.helpers.variable import getUserDir -from couchpotato.core.plugins.base import Plugin import ctypes import os import string + +from couchpotato.api import addApiView +from couchpotato.core.helpers.variable import getUserDir +from couchpotato.core.plugins.base import Plugin import six + if os.name == 'nt': import imp try: diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py index e68b62c1..a0852cc1 100644 --- a/couchpotato/core/plugins/category/main.py +++ b/couchpotato/core/plugins/category/main.py @@ -1,4 +1,5 @@ import traceback + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent @@ -7,6 +8,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import CategoryIndex, CategoryMediaIndex + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/custom.py b/couchpotato/core/plugins/custom.py index c9cff3ea..20b4c3f7 100644 --- a/couchpotato/core/plugins/custom.py +++ b/couchpotato/core/plugins/custom.py @@ -1,8 +1,10 @@ +import os + from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -import os + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index 4a9aaf95..57cd7afa 100644 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -1,12 +1,14 @@ from datetime import date +import random as rndm +import time + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -import random as rndm -import time + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py index 41104378..77b3ba81 100644 --- a/couchpotato/core/plugins/file.py +++ b/couchpotato/core/plugins/file.py @@ -1,3 +1,7 @@ +import os.path +import time +import traceback + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent @@ -8,9 +12,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from scandir import scandir from tornado.web import StaticFileHandler -import os.path -import time -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index 2f471586..ced069ea 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -1,11 +1,13 @@ +import os +import traceback + from couchpotato.api import addApiView from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -import os -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 205de136..e1e084f1 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -1,3 +1,9 @@ +import ctypes +import os +import sys +import time +import traceback + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync @@ -6,11 +12,7 @@ from couchpotato.core.helpers.variable import splitString, getTitle, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -import ctypes -import os -import sys -import time -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index c978e6ae..8f838f45 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -1,4 +1,5 @@ import traceback + from couchpotato import get_db, tryInt from couchpotato.api import addApiView from couchpotato.core.event import addEvent @@ -7,6 +8,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ProfileIndex + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/quality/__init__.py b/couchpotato/core/plugins/quality/__init__.py index fc9cda64..7710251c 100644 --- a/couchpotato/core/plugins/quality/__init__.py +++ b/couchpotato/core/plugins/quality/__init__.py @@ -3,5 +3,3 @@ from .main import QualityPlugin def autoload(): return QualityPlugin() - -config = [] diff --git a/couchpotato/core/plugins/quality/index.py b/couchpotato/core/plugins/quality/index.py index 6d160a7b..78043972 100644 --- a/couchpotato/core/plugins/quality/index.py +++ b/couchpotato/core/plugins/quality/index.py @@ -1,6 +1,7 @@ -from CodernityDB.hash_index import HashIndex from hashlib import md5 +from CodernityDB.hash_index import HashIndex + class QualityIndex(HashIndex): _version = 1 diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 96a22f40..ee06d4c5 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -1,4 +1,6 @@ import traceback +import re + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent @@ -7,7 +9,7 @@ from couchpotato.core.helpers.variable import mergeDicts, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.quality.index import QualityIndex -import re + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index d6978bbf..ee762539 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -1,4 +1,5 @@ from hashlib import md5 + from CodernityDB.hash_index import HashIndex from CodernityDB.tree_index import TreeBasedIndex diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index d8e99a73..66ca7a3b 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -1,3 +1,8 @@ +from inspect import ismethod, isfunction +import os +import time +import traceback + from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent @@ -7,10 +12,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex from couchpotato.environment import Env -from inspect import ismethod, isfunction -import os -import time -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 7e4fc99c..585f6894 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -1,3 +1,10 @@ +import fnmatch +import os +import re +import shutil +import time +import traceback + from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync @@ -9,15 +16,10 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from scandir import scandir from unrar2 import RarFile -import fnmatch -import os -import re -import shutil -import time -import traceback import six from six.moves import filter + log = CPLog(__name__) autoload = 'Renamer' diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 3ed50798..e444489f 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -1,3 +1,9 @@ +import os +import re +import threading +import time +import traceback + from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp @@ -10,13 +16,9 @@ from guessit import guess_movie_info from scandir import scandir from subliminal.videos import Video import enzyme -import os -import re -import threading -import time -import traceback from six.moves import filter, map, zip + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index c1f5123a..a53608c0 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -1,10 +1,12 @@ +import re +import traceback + from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.environment import Env -import re -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/subtitle.py b/couchpotato/core/plugins/subtitle.py index 06a77f9a..9fd7ef16 100644 --- a/couchpotato/core/plugins/subtitle.py +++ b/couchpotato/core/plugins/subtitle.py @@ -1,3 +1,5 @@ +import traceback + from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, sp from couchpotato.core.helpers.variable import splitString @@ -5,7 +7,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env import subliminal -import traceback + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/trailer.py b/couchpotato/core/plugins/trailer.py index b202e939..ae525862 100644 --- a/couchpotato/core/plugins/trailer.py +++ b/couchpotato/core/plugins/trailer.py @@ -1,8 +1,10 @@ +import os + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import getExt, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -import os + log = CPLog(__name__) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 113c0351..f857fb96 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -1,3 +1,5 @@ +import os + from couchpotato import index from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent @@ -6,7 +8,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from tornado.web import RequestHandler -import os + log = CPLog(__name__) diff --git a/couchpotato/core/settings.py b/couchpotato/core/settings.py index da909c7e..be2a0d01 100644 --- a/couchpotato/core/settings.py +++ b/couchpotato/core/settings.py @@ -1,12 +1,13 @@ from __future__ import with_statement import traceback +import ConfigParser +from hashlib import md5 + from CodernityDB.hash_index import HashIndex from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat -import ConfigParser -from hashlib import md5 class Settings(object): diff --git a/couchpotato/environment.py b/couchpotato/environment.py index f7cfd0af..8c1ee17c 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -1,8 +1,9 @@ +import os + from couchpotato.core.database import Database from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings -import os class Env(object): diff --git a/couchpotato/runner.py b/couchpotato/runner.py index ba0d97bb..d38a786e 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -1,15 +1,4 @@ -from CodernityDB.database_thread_safe import ThreadSafeDatabase -from argparse import ArgumentParser -from cache import FileSystemCache -from couchpotato import KeyHandler, LoginHandler, LogoutHandler -from couchpotato.api import NonBlockHandler, ApiHandler -from couchpotato.core.event import fireEventAsync, fireEvent -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers -from scandir import scandir -from tornado.httpserver import HTTPServer -from tornado.web import Application, StaticFileHandler, RedirectHandler from uuid import uuid4 import locale import logging @@ -21,6 +10,18 @@ import warnings import re import tarfile +from CodernityDB.database_thread_safe import ThreadSafeDatabase +from argparse import ArgumentParser +from cache import FileSystemCache +from couchpotato import KeyHandler, LoginHandler, LogoutHandler +from couchpotato.api import NonBlockHandler, ApiHandler +from couchpotato.core.event import fireEventAsync, fireEvent +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import getDataDir, tryInt +from scandir import scandir +from tornado.httpserver import HTTPServer +from tornado.web import Application, StaticFileHandler, RedirectHandler + def getOptions(base_path, args): @@ -177,7 +178,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En logger.addHandler(hdlr2) # Start logging & enable colors - import color_logs from couchpotato.core.logger import CPLog log = CPLog(__name__) log.debug('Started with options %s', options) From 11aaaecb7b46d1e7fb6422b518111804183284af Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 21:47:07 +0100 Subject: [PATCH 083/468] Get colors back, remove logged string --- couchpotato/core/loader.py | 3 +++ couchpotato/runner.py | 1 + 2 files changed, 4 insertions(+) diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 6d84c45e..17cb7682 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -146,6 +146,9 @@ class Loader(object): self.modules[priority] = {} module = module.lstrip('.') + if plugin_type.startswith('couchpotato_core'): + plugin_type = plugin_type[17:] + self.modules[priority][module] = { 'priority': priority, 'module': module, diff --git a/couchpotato/runner.py b/couchpotato/runner.py index d38a786e..8b7fda3b 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -178,6 +178,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En logger.addHandler(hdlr2) # Start logging & enable colors + import color_logs from couchpotato.core.logger import CPLog log = CPLog(__name__) log.debug('Started with options %s', options) From 6a9c3dac77cdd8162347cf2a60fb542e0664b051 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 22:30:10 +0100 Subject: [PATCH 084/468] Rename so it doesn't try to import itself --- couchpotato/core/downloaders/{rtorrent.py => rtorrent_.py} | 0 couchpotato/core/notifications/{email.py => email_.py} | 0 couchpotato/runner.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename couchpotato/core/downloaders/{rtorrent.py => rtorrent_.py} (100%) rename couchpotato/core/notifications/{email.py => email_.py} (100%) diff --git a/couchpotato/core/downloaders/rtorrent.py b/couchpotato/core/downloaders/rtorrent_.py similarity index 100% rename from couchpotato/core/downloaders/rtorrent.py rename to couchpotato/core/downloaders/rtorrent_.py diff --git a/couchpotato/core/notifications/email.py b/couchpotato/core/notifications/email_.py similarity index 100% rename from couchpotato/core/notifications/email.py rename to couchpotato/core/notifications/email_.py diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 8b7fda3b..f55d11e0 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -153,7 +153,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler', 'tornado', 'requests']: logging.getLogger(logger_name).setLevel(logging.ERROR) - for logger_name in ['gntp', 'migrate']: + for logger_name in ['gntp']: logging.getLogger(logger_name).setLevel(logging.WARNING) # Use reloader From f775c9da0b98d3fb85254cc1cc3d93007c6abafb Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 22:34:32 +0100 Subject: [PATCH 085/468] Only show log after successful load --- couchpotato/core/loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 17cb7682..6d8d14ac 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -56,12 +56,10 @@ class Loader(object): if m is None: continue - log.info('Loading %s: %s', (plugin['type'], plugin['name'])) - # Save default settings for plugin/provider did_save += self.loadSettings(m, module_name, save = False) - self.loadPlugins(m, plugin.get('name')) + self.loadPlugins(m, plugin.get('type'), plugin.get('name')) except ImportError as e: # todo:: subclass ImportError for missing requirements. if e.message.lower().startswith("missing"): @@ -123,7 +121,7 @@ class Loader(object): log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc())) return False - def loadPlugins(self, module, name): + def loadPlugins(self, module, type, name): if not hasattr(module, 'autoload'): #log.debug('Skip startup for plugin %s as it has no start section' % module.__file__) @@ -135,6 +133,8 @@ class Loader(object): # Load folder plugin else: module.autoload() + + log.info('Loaded %s: %s', (type, name)) return True except: log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc())) From e5e768c56fcdf66209a50aee177bc0ae1c5d6f49 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 22:46:12 +0100 Subject: [PATCH 086/468] Migrate hide profile --- couchpotato/core/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 33ae0d68..6bbdb959 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -7,7 +7,7 @@ from couchpotato import CPLog from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import getImdb +from couchpotato.core.helpers.variable import getImdb, tryInt log = CPLog(__name__) @@ -223,7 +223,8 @@ class Database(object): # Update existing with order only if exists and p.get('core'): profile = db.get('id', exists) - profile['order'] = p.get('order') + profile['order'] = tryInt(p.get('order')) + profile['hide'] = p.get('hide') in [1, True, 'true', 'True'] db.update(profile) profile_link[x] = profile.get('_id') From 83843ae210435a5c1160bae22fcce1dfc5ad4796 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 23:15:14 +0100 Subject: [PATCH 087/468] Make sure in_library only contains relevant releases --- couchpotato/core/media/movie/_base/static/search.js | 4 ++-- couchpotato/core/media/movie/providers/info/_modifier.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index edd1d23e..cc837394 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -134,8 +134,8 @@ Block.Search.MovieItem = new Class({ if(self.info.in_library){ var in_library = []; - self.info.in_library.releases.each(function(release){ - in_library.include(release.quality.label) + (self.info.in_library.releases || []).each(function(release){ + in_library.include(release.quality) }); } diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index 20697b0b..d9a15e8c 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -106,7 +106,11 @@ class MovieResultModifier(Plugin): for release in db.run('release', 'for_media', media.get('_id')): if release.get('status') == 'done': - temp['in_library'] = media + if not temp['in_library']: + temp['in_library'] = media + temp['in_library']['releases'] = [] + + temp['in_library']['releases'].append(release) except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) From 0921c5e160e4ee0562e9512c9034f373c36816c1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 23:44:24 +0100 Subject: [PATCH 088/468] Autoload suggestions --- couchpotato/core/media/movie/suggestion/__init__.py | 5 ----- couchpotato/core/media/movie/suggestion/main.py | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/movie/suggestion/__init__.py b/couchpotato/core/media/movie/suggestion/__init__.py index 81e42f03..e69de29b 100644 --- a/couchpotato/core/media/movie/suggestion/__init__.py +++ b/couchpotato/core/media/movie/suggestion/__init__.py @@ -1,5 +0,0 @@ -from .main import Suggestion - - -def start(): - return Suggestion() diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index bc42e456..62b9f57d 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -6,6 +6,9 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env +autoload = 'Suggestion' + + class Suggestion(Plugin): def __init__(self): From 48ec6fc757b74b9ff5f2870a056573e45331940a Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 12 Mar 2014 23:55:56 +0100 Subject: [PATCH 089/468] Missing autoloads --- couchpotato/core/media/movie/_base/main.py | 2 ++ couchpotato/core/media/movie/providers/nzb/binsearch.py | 2 ++ couchpotato/core/media/movie/providers/nzb/newznab.py | 2 ++ couchpotato/core/media/movie/providers/nzb/nzbclub.py | 2 ++ couchpotato/core/media/movie/providers/nzb/nzbindex.py | 2 ++ couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py | 2 ++ couchpotato/core/media/movie/searcher.py | 2 ++ couchpotato/core/plugins/scanner.py | 2 ++ 8 files changed, 16 insertions(+) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 8112eda3..36e0a479 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -14,6 +14,8 @@ import six log = CPLog(__name__) +autoload = 'MovieBase' + class MovieBase(MovieTypeBase): diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch.py b/couchpotato/core/media/movie/providers/nzb/binsearch.py index 9bc2b8e8..f4c16c4c 100644 --- a/couchpotato/core/media/movie/providers/nzb/binsearch.py +++ b/couchpotato/core/media/movie/providers/nzb/binsearch.py @@ -6,6 +6,8 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'BinSearch' + class BinSearch(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/nzb/newznab.py b/couchpotato/core/media/movie/providers/nzb/newznab.py index 9682e74e..dd4d94c0 100644 --- a/couchpotato/core/media/movie/providers/nzb/newznab.py +++ b/couchpotato/core/media/movie/providers/nzb/newznab.py @@ -5,6 +5,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'Newznab' + class Newznab(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub.py b/couchpotato/core/media/movie/providers/nzb/nzbclub.py index f1b0d6bd..2a43ba2f 100644 --- a/couchpotato/core/media/movie/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/movie/providers/nzb/nzbclub.py @@ -6,6 +6,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'NZBClub' + class NZBClub(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex.py b/couchpotato/core/media/movie/providers/nzb/nzbindex.py index 411f924a..0b5b2b43 100644 --- a/couchpotato/core/media/movie/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/movie/providers/nzb/nzbindex.py @@ -7,6 +7,8 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'NzbIndex' + class NzbIndex(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py index 653adb6b..f4527f6d 100644 --- a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'OMGWTFNZBs' + class OMGWTFNZBs(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 2043d01c..a1e915b1 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -17,6 +17,8 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'MovieSearcher' + class MovieSearcher(SearcherBase, MovieTypeBase): diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index e444489f..0066c368 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -21,6 +21,8 @@ from six.moves import filter, map, zip log = CPLog(__name__) +autoload = 'Scanner' + class Scanner(Plugin): From 8b2eb50f29310cdbcefb9f531f07251b3689c111 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 11:51:45 +0100 Subject: [PATCH 090/468] Base classes for matcher and library --- .../core/media/_base/library/__init__.py | 6 + couchpotato/core/media/_base/library/base.py | 13 + couchpotato/core/media/_base/library/main.py | 18 ++ .../core/media/_base/matcher/__init__.py | 6 + couchpotato/core/media/_base/matcher/base.py | 84 ++++++ couchpotato/core/media/_base/matcher/main.py | 89 ++++++ libs/caper/__init__.py | 195 ++++++++++++ libs/caper/constraint.py | 134 +++++++++ libs/caper/group.py | 284 ++++++++++++++++++ libs/caper/helpers.py | 80 +++++ libs/caper/matcher.py | 144 +++++++++ libs/caper/objects.py | 124 ++++++++ libs/caper/parsers/__init__.py | 0 libs/caper/parsers/anime.py | 88 ++++++ libs/caper/parsers/base.py | 84 ++++++ libs/caper/parsers/scene.py | 230 ++++++++++++++ libs/caper/parsers/usenet.py | 115 +++++++ libs/caper/result.py | 213 +++++++++++++ libs/caper/step.py | 96 ++++++ libs/logr/__init__.py | 225 ++++++++++++++ 20 files changed, 2228 insertions(+) create mode 100644 couchpotato/core/media/_base/library/__init__.py create mode 100644 couchpotato/core/media/_base/library/base.py create mode 100644 couchpotato/core/media/_base/library/main.py create mode 100644 couchpotato/core/media/_base/matcher/__init__.py create mode 100644 couchpotato/core/media/_base/matcher/base.py create mode 100644 couchpotato/core/media/_base/matcher/main.py create mode 100644 libs/caper/__init__.py create mode 100644 libs/caper/constraint.py create mode 100644 libs/caper/group.py create mode 100644 libs/caper/helpers.py create mode 100644 libs/caper/matcher.py create mode 100644 libs/caper/objects.py create mode 100644 libs/caper/parsers/__init__.py create mode 100644 libs/caper/parsers/anime.py create mode 100644 libs/caper/parsers/base.py create mode 100644 libs/caper/parsers/scene.py create mode 100644 libs/caper/parsers/usenet.py create mode 100644 libs/caper/result.py create mode 100644 libs/caper/step.py create mode 100644 libs/logr/__init__.py diff --git a/couchpotato/core/media/_base/library/__init__.py b/couchpotato/core/media/_base/library/__init__.py new file mode 100644 index 00000000..3e1babe4 --- /dev/null +++ b/couchpotato/core/media/_base/library/__init__.py @@ -0,0 +1,6 @@ +from .main import Library + +def autoload(): + return Library() + +config = [] diff --git a/couchpotato/core/media/_base/library/base.py b/couchpotato/core/media/_base/library/base.py new file mode 100644 index 00000000..553eff5a --- /dev/null +++ b/couchpotato/core/media/_base/library/base.py @@ -0,0 +1,13 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.plugins.base import Plugin + + +class LibraryBase(Plugin): + + _type = None + + def initType(self): + addEvent('library.types', self.getType) + + def getType(self): + return self._type diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py new file mode 100644 index 00000000..a723de50 --- /dev/null +++ b/couchpotato/core/media/_base/library/main.py @@ -0,0 +1,18 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.media._base.library.base import LibraryBase + + +class Library(LibraryBase): + def __init__(self): + addEvent('library.title', self.title) + + def title(self, library): + return fireEvent( + 'library.query', + library, + + condense = False, + include_year = False, + include_identifier = False, + single = True + ) diff --git a/couchpotato/core/media/_base/matcher/__init__.py b/couchpotato/core/media/_base/matcher/__init__.py new file mode 100644 index 00000000..1e4cda37 --- /dev/null +++ b/couchpotato/core/media/_base/matcher/__init__.py @@ -0,0 +1,6 @@ +from .main import Matcher + +def autoload(): + return Matcher() + +config = [] diff --git a/couchpotato/core/media/_base/matcher/base.py b/couchpotato/core/media/_base/matcher/base.py new file mode 100644 index 00000000..86511263 --- /dev/null +++ b/couchpotato/core/media/_base/matcher/base.py @@ -0,0 +1,84 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class MatcherBase(Plugin): + type = None + + def __init__(self): + if self.type: + addEvent('%s.matcher.correct' % self.type, self.correct) + + def correct(self, chain, release, media, quality): + raise NotImplementedError() + + def flattenInfo(self, info): + # Flatten dictionary of matches (chain info) + if isinstance(info, dict): + return dict([(key, self.flattenInfo(value)) for key, value in info.items()]) + + # Flatten matches + result = None + + for match in info: + if isinstance(match, dict): + if result is None: + result = {} + + for key, value in match.items(): + if key not in result: + result[key] = [] + + result[key].append(value) + else: + if result is None: + result = [] + + result.append(match) + + return result + + def constructFromRaw(self, match): + if not match: + return None + + parts = [ + ''.join([ + y for y in x[1:] if y + ]) for x in match + ] + + return ''.join(parts)[:-1].strip() + + def simplifyValue(self, value): + if not value: + return value + + if isinstance(value, basestring): + return simplifyString(value) + + if isinstance(value, list): + return [self.simplifyValue(x) for x in value] + + raise ValueError("Unsupported value type") + + def chainMatch(self, chain, group, tags): + info = self.flattenInfo(chain.info[group]) + + found_tags = [] + for tag, accepted in tags.items(): + values = [self.simplifyValue(x) for x in info.get(tag, [None])] + + if any([val in accepted for val in values]): + found_tags.append(tag) + + log.debug('tags found: %s, required: %s' % (found_tags, tags.keys())) + + if set(tags.keys()) == set(found_tags): + return True + + return all([key in found_tags for key, value in tags.items()]) diff --git a/couchpotato/core/media/_base/matcher/main.py b/couchpotato/core/media/_base/matcher/main.py new file mode 100644 index 00000000..2034249b --- /dev/null +++ b/couchpotato/core/media/_base/matcher/main.py @@ -0,0 +1,89 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import possibleTitles +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.matcher.base import MatcherBase +from caper import Caper + +log = CPLog(__name__) + + +class Matcher(MatcherBase): + + def __init__(self): + super(Matcher, self).__init__() + + self.caper = Caper() + + addEvent('matcher.parse', self.parse) + addEvent('matcher.match', self.match) + + addEvent('matcher.flatten_info', self.flattenInfo) + addEvent('matcher.construct_from_raw', self.constructFromRaw) + + addEvent('matcher.correct_title', self.correctTitle) + addEvent('matcher.correct_quality', self.correctQuality) + + def parse(self, name, parser='scene'): + return self.caper.parse(name, parser) + + def match(self, release, media, quality): + match = fireEvent('matcher.parse', release['name'], single = True) + + if len(match.chains) < 1: + log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) + return False + + for chain in match.chains: + if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True): + return chain + + return False + + def correctTitle(self, chain, media): + root_library = media['library']['root_library'] + + if 'show_name' not in chain.info or not len(chain.info['show_name']): + log.info('Wrong: missing show name in parsed result') + return False + + # Get the lower-case parsed show name from the chain + chain_words = [x.lower() for x in chain.info['show_name']] + + # Build a list of possible titles of the media we are searching for + titles = root_library['info']['titles'] + + # Add year suffix titles (will result in ['', ' ', '', ...]) + suffixes = [None, root_library['info']['year']] + + titles = [ + title + ((' %s' % suffix) if suffix else '') + for title in titles + for suffix in suffixes + ] + + # Check show titles match + # TODO check xem names + for title in titles: + for valid_words in [x.split(' ') for x in possibleTitles(title)]: + + if valid_words == chain_words: + return True + + return False + + def correctQuality(self, chain, quality, quality_map): + if quality['identifier'] not in quality_map: + log.info2('Wrong: unknown preferred quality %s', quality['identifier']) + return False + + if 'video' not in chain.info: + log.info2('Wrong: no video tags found') + return False + + video_tags = quality_map[quality['identifier']] + + if not self.chainMatch(chain, 'video', video_tags): + log.info2('Wrong: %s tags not in chain', video_tags) + return False + + return True diff --git a/libs/caper/__init__.py b/libs/caper/__init__.py new file mode 100644 index 00000000..95fb6d73 --- /dev/null +++ b/libs/caper/__init__.py @@ -0,0 +1,195 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logr import Logr +from caper.matcher import FragmentMatcher +from caper.objects import CaperFragment, CaperClosure +from caper.parsers.anime import AnimeParser +from caper.parsers.scene import SceneParser +from caper.parsers.usenet import UsenetParser + + +__version_info__ = ('0', '3', '1') +__version_branch__ = 'master' + +__version__ = "%s%s" % ( + '.'.join(__version_info__), + '-' + __version_branch__ if __version_branch__ else '' +) + + +CL_START_CHARS = ['(', '[', '<', '>'] +CL_END_CHARS = [')', ']', '<', '>'] +CL_END_STRINGS = [' - '] + +STRIP_START_CHARS = ''.join(CL_START_CHARS) +STRIP_END_CHARS = ''.join(CL_END_CHARS) +STRIP_CHARS = ''.join(['_', ' ', '.']) + +FRAGMENT_SEPARATORS = ['.', '-', '_', ' '] + + +CL_START = 0 +CL_END = 1 + + +class Caper(object): + def __init__(self, debug=False): + self.debug = debug + + self.parsers = { + 'anime': AnimeParser, + 'scene': SceneParser, + 'usenet': UsenetParser + } + + def _closure_split(self, name): + """ + :type name: str + + :rtype: list of CaperClosure + """ + + closures = [] + + def end_closure(closures, buf): + buf = buf.strip(STRIP_CHARS) + if len(buf) < 2: + return + + cur = CaperClosure(len(closures), buf) + cur.left = closures[len(closures) - 1] if len(closures) > 0 else None + + if cur.left: + cur.left.right = cur + + closures.append(cur) + + state = CL_START + buf = "" + for x, ch in enumerate(name): + # Check for start characters + if state == CL_START and ch in CL_START_CHARS: + end_closure(closures, buf) + + state = CL_END + buf = "" + + buf += ch + + if state == CL_END and ch in CL_END_CHARS: + # End character found, create the closure + end_closure(closures, buf) + + state = CL_START + buf = "" + elif state == CL_START and buf[-3:] in CL_END_STRINGS: + # End string found, create the closure + end_closure(closures, buf[:-3]) + + state = CL_START + buf = "" + + end_closure(closures, buf) + + return closures + + def _clean_closure(self, closure): + """ + :type closure: str + + :rtype: str + """ + + return closure.lstrip(STRIP_START_CHARS).rstrip(STRIP_END_CHARS) + + def _fragment_split(self, closures): + """ + :type closures: list of CaperClosure + + :rtype: list of CaperClosure + """ + + cur_position = 0 + cur = None + + def end_fragment(fragments, cur, cur_position): + cur.position = cur_position + + cur.left = fragments[len(fragments) - 1] if len(fragments) > 0 else None + if cur.left: + cur.left_sep = cur.left.right_sep + cur.left.right = cur + + cur.right_sep = ch + + fragments.append(cur) + + for closure in closures: + closure.fragments = [] + + separator_buffer = "" + + for x, ch in enumerate(self._clean_closure(closure.value)): + if not cur: + cur = CaperFragment(closure) + + if ch in FRAGMENT_SEPARATORS: + if cur.value: + separator_buffer = "" + + separator_buffer += ch + + if cur.value or not closure.fragments: + end_fragment(closure.fragments, cur, cur_position) + elif len(separator_buffer) > 1: + cur.value = separator_buffer.strip() + + if cur.value: + end_fragment(closure.fragments, cur, cur_position) + + separator_buffer = "" + + # Reset + cur = None + cur_position += 1 + else: + cur.value += ch + + # Finish parsing the last fragment + if cur and cur.value: + end_fragment(closure.fragments, cur, cur_position) + + # Reset + cur_position = 0 + cur = None + + return closures + + def parse(self, name, parser='scene'): + closures = self._closure_split(name) + closures = self._fragment_split(closures) + + # Print closures + for closure in closures: + Logr.debug("closure [%s]", closure.value) + + for fragment in closure.fragments: + Logr.debug("\tfragment [%s]", fragment.value) + + if parser not in self.parsers: + raise ValueError("Unknown parser") + + # TODO autodetect the parser type + return self.parsers[parser](self.debug).run(closures) diff --git a/libs/caper/constraint.py b/libs/caper/constraint.py new file mode 100644 index 00000000..e092d33d --- /dev/null +++ b/libs/caper/constraint.py @@ -0,0 +1,134 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class CaptureConstraint(object): + def __init__(self, capture_group, constraint_type, comparisons=None, target=None, **kwargs): + """Capture constraint object + + :type capture_group: CaptureGroup + """ + + self.capture_group = capture_group + + self.constraint_type = constraint_type + self.target = target + + self.comparisons = comparisons if comparisons else [] + self.kwargs = {} + + for orig_key, value in kwargs.items(): + key = orig_key.split('__') + if len(key) != 2: + self.kwargs[orig_key] = value + continue + name, method = key + + method = 'constraint_match_' + method + if not hasattr(self, method): + self.kwargs[orig_key] = value + continue + + self.comparisons.append((name, getattr(self, method), value)) + + def execute(self, parent_node, node, **kwargs): + func_name = 'constraint_%s' % self.constraint_type + + if hasattr(self, func_name): + return getattr(self, func_name)(parent_node, node, **kwargs) + + raise ValueError('Unknown constraint type "%s"' % self.constraint_type) + + # + # Node Matching + # + + def constraint_match(self, parent_node, node): + results = [] + total_weight = 0 + + for name, method, argument in self.comparisons: + weight, success = method(node, name, argument) + total_weight += weight + results.append(success) + + return total_weight / (float(len(results)) or 1), all(results) if len(results) > 0 else False + + def constraint_match_eq(self, node, name, expected): + if not hasattr(node, name): + return 1.0, False + + return 1.0, getattr(node, name) == expected + + def constraint_match_re(self, node, name, arg): + # Node match + if name == 'node': + group, minimum_weight = arg if type(arg) is tuple and len(arg) > 1 else (arg, 0) + + weight, match, num_fragments = self.capture_group.parser.matcher.fragment_match(node, group) + return weight, weight > minimum_weight + + # Regex match + if type(arg).__name__ == 'SRE_Pattern': + return 1.0, arg.match(getattr(node, name)) is not None + + # Value match + if hasattr(node, name): + match = self.capture_group.parser.matcher.value_match(getattr(node, name), arg, single=True) + return 1.0, match is not None + + raise ValueError("Unknown constraint match type '%s'" % name) + + # + # Result + # + + def constraint_result(self, parent_node, fragment): + ctag = self.kwargs.get('tag') + if not ctag: + return 0, False + + ckey = self.kwargs.get('key') + + for tag, result in parent_node.captured(): + if tag != ctag: + continue + + if not ckey or ckey in result.keys(): + return 1.0, True + + return 0.0, False + + # + # Failure + # + + def constraint_failure(self, parent_node, fragment, match): + if not match or not match.success: + return 1.0, True + + return 0, False + + # + # Success + # + + def constraint_success(self, parent_node, fragment, match): + if match and match.success: + return 1.0, True + + return 0, False + + def __repr__(self): + return "CaptureConstraint(comparisons=%s)" % repr(self.comparisons) diff --git a/libs/caper/group.py b/libs/caper/group.py new file mode 100644 index 00000000..8f0399ef --- /dev/null +++ b/libs/caper/group.py @@ -0,0 +1,284 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from logr import Logr +from caper import CaperClosure, CaperFragment +from caper.helpers import clean_dict +from caper.result import CaperFragmentNode, CaperClosureNode +from caper.step import CaptureStep +from caper.constraint import CaptureConstraint + + +class CaptureGroup(object): + def __init__(self, parser, result): + """Capture group object + + :type parser: caper.parsers.base.Parser + :type result: caper.result.CaperResult + """ + + self.parser = parser + self.result = result + + #: @type: list of CaptureStep + self.steps = [] + + #: type: str + self.step_source = None + + #: @type: list of CaptureConstraint + self.pre_constraints = [] + + #: :type: list of CaptureConstraint + self.post_constraints = [] + + def capture_fragment(self, tag, regex=None, func=None, single=True, **kwargs): + Logr.debug('capture_fragment("%s", "%s", %s, %s)', tag, regex, func, single) + + if self.step_source != 'fragment': + if self.step_source is None: + self.step_source = 'fragment' + else: + raise ValueError("Unable to mix fragment and closure capturing in a group") + + self.steps.append(CaptureStep( + self, tag, + 'fragment', + regex=regex, + func=func, + single=single, + **kwargs + )) + + return self + + def capture_closure(self, tag, regex=None, func=None, single=True, **kwargs): + Logr.debug('capture_closure("%s", "%s", %s, %s)', tag, regex, func, single) + + if self.step_source != 'closure': + if self.step_source is None: + self.step_source = 'closure' + else: + raise ValueError("Unable to mix fragment and closure capturing in a group") + + self.steps.append(CaptureStep( + self, tag, + 'closure', + regex=regex, + func=func, + single=single, + **kwargs + )) + + return self + + def until_closure(self, **kwargs): + self.pre_constraints.append(CaptureConstraint(self, 'match', target='closure', **kwargs)) + + return self + + def until_fragment(self, **kwargs): + self.pre_constraints.append(CaptureConstraint(self, 'match', target='fragment', **kwargs)) + + return self + + def until_result(self, **kwargs): + self.pre_constraints.append(CaptureConstraint(self, 'result', **kwargs)) + + return self + + def until_failure(self, **kwargs): + self.post_constraints.append(CaptureConstraint(self, 'failure', **kwargs)) + + return self + + def until_success(self, **kwargs): + self.post_constraints.append(CaptureConstraint(self, 'success', **kwargs)) + + return self + + def parse_subject(self, parent_head, subject): + Logr.debug("parse_subject (%s) subject: %s", self.step_source, repr(subject)) + + if type(subject) is CaperClosure: + return self.parse_closure(parent_head, subject) + + if type(subject) is CaperFragment: + return self.parse_fragment(parent_head, subject) + + raise ValueError('Unknown subject (%s)', subject) + + def parse_fragment(self, parent_head, subject): + parent_node = parent_head[0] if type(parent_head) is list else parent_head + + nodes, match = self.match(parent_head, parent_node, subject) + + # Capturing broke on constraint, return now + if not match: + return nodes + + Logr.debug('created fragment node with subject.value: "%s"' % subject.value) + + result = [CaperFragmentNode( + parent_node.closure, + subject.take_right(match.num_fragments), + parent_head, + match + )] + + # Branch if the match was indefinite (weight below 1.0) + if match.result and match.weight < 1.0: + if match.num_fragments == 1: + result.append(CaperFragmentNode(parent_node.closure, [subject], parent_head)) + else: + nodes.append(CaperFragmentNode(parent_node.closure, [subject], parent_head)) + + nodes.append(result[0] if len(result) == 1 else result) + + return nodes + + def parse_closure(self, parent_head, subject): + parent_node = parent_head[0] if type(parent_head) is list else parent_head + + nodes, match = self.match(parent_head, parent_node, subject) + + # Capturing broke on constraint, return now + if not match: + return nodes + + Logr.debug('created closure node with subject.value: "%s"' % subject.value) + + result = [CaperClosureNode( + subject, + parent_head, + match + )] + + # Branch if the match was indefinite (weight below 1.0) + if match.result and match.weight < 1.0: + if match.num_fragments == 1: + result.append(CaperClosureNode(subject, parent_head)) + else: + nodes.append(CaperClosureNode(subject, parent_head)) + + nodes.append(result[0] if len(result) == 1 else result) + + return nodes + + def match(self, parent_head, parent_node, subject): + nodes = [] + + # Check pre constaints + broke, definite = self.check_constraints(self.pre_constraints, parent_head, subject) + + if broke: + nodes.append(parent_head) + + if definite: + return nodes, None + + # Try match subject against the steps available + match = None + + for step in self.steps: + if step.source == 'closure' and type(subject) is not CaperClosure: + pass + elif step.source == 'fragment' and type(subject) is CaperClosure: + Logr.debug('Closure encountered on fragment step, jumping into fragments') + return [CaperClosureNode(subject, parent_head, None)], None + + match = step.execute(subject) + + if match.success: + if type(match.result) is dict: + match.result = clean_dict(match.result) + + Logr.debug('Found match with weight %s, match: %s, num_fragments: %s' % ( + match.weight, match.result, match.num_fragments + )) + + step.matched = True + + break + + if all([step.single and step.matched for step in self.steps]): + Logr.debug('All steps completed, group finished') + parent_node.finished_groups.append(self) + return nodes, match + + # Check post constraints + broke, definite = self.check_constraints(self.post_constraints, parent_head, subject, match=match) + if broke: + return nodes, None + + return nodes, match + + def check_constraints(self, constraints, parent_head, subject, **kwargs): + parent_node = parent_head[0] if type(parent_head) is list else parent_head + + # Check constraints + for constraint in [c for c in constraints if c.target == subject.__key__ or not c.target]: + Logr.debug("Testing constraint %s against subject %s", repr(constraint), repr(subject)) + + weight, success = constraint.execute(parent_node, subject, **kwargs) + + if success: + Logr.debug('capturing broke on "%s" at %s', subject.value, constraint) + parent_node.finished_groups.append(self) + + return True, weight == 1.0 + + return False, None + + def execute(self): + heads_finished = None + + while heads_finished is None or not (len(heads_finished) == len(self.result.heads) and all(heads_finished)): + heads_finished = [] + + heads = self.result.heads + self.result.heads = [] + + for head in heads: + node = head[0] if type(head) is list else head + + if self in node.finished_groups: + Logr.debug("head finished for group") + self.result.heads.append(head) + heads_finished.append(True) + continue + + Logr.debug('') + + Logr.debug(node) + + next_subject = node.next() + + Logr.debug('----------[%s] (%s)----------' % (next_subject, repr(next_subject.value) if next_subject else None)) + + if next_subject: + for node_result in self.parse_subject(head, next_subject): + self.result.heads.append(node_result) + + Logr.debug('Heads: %s', self.result.heads) + + heads_finished.append(self in node.finished_groups or next_subject is None) + + if len(self.result.heads) == 0: + self.result.heads = heads + + Logr.debug("heads_finished: %s, self.result.heads: %s", heads_finished, self.result.heads) + + Logr.debug("group finished") diff --git a/libs/caper/helpers.py b/libs/caper/helpers.py new file mode 100644 index 00000000..ded5d482 --- /dev/null +++ b/libs/caper/helpers.py @@ -0,0 +1,80 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + + +def is_list_type(obj, element_type): + if not type(obj) is list: + return False + + if len(obj) < 1: + raise ValueError("Unable to determine list element type from empty list") + + return type(obj[0]) is element_type + + +def clean_dict(target, remove=None): + """Recursively remove items matching a value 'remove' from the dictionary + + :type target: dict + """ + if type(target) is not dict: + raise ValueError("Target is required to be a dict") + + remove_keys = [] + for key in target.keys(): + if type(target[key]) is not dict: + if target[key] == remove: + remove_keys.append(key) + else: + clean_dict(target[key], remove) + + for key in remove_keys: + target.pop(key) + + return target + + +def update_dict(a, b): + for key, value in b.items(): + if key not in a: + a[key] = value + elif isinstance(a[key], dict) and isinstance(value, dict): + update_dict(a[key], value) + elif isinstance(a[key], list): + a[key].append(value) + else: + a[key] = [a[key], value] + + +def xrange_six(start, stop=None, step=None): + if stop is not None and step is not None: + if PY3: + return range(start, stop, step) + else: + return xrange(start, stop, step) + else: + if PY3: + return range(start) + else: + return xrange(start) + + +def delta_seconds(td): + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 diff --git a/libs/caper/matcher.py b/libs/caper/matcher.py new file mode 100644 index 00000000..3acf2e68 --- /dev/null +++ b/libs/caper/matcher.py @@ -0,0 +1,144 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from caper.helpers import is_list_type, update_dict, delta_seconds +from datetime import datetime +from logr import Logr +import re + + +class FragmentMatcher(object): + def __init__(self, pattern_groups): + self.regex = {} + + self.construct_patterns(pattern_groups) + + def construct_patterns(self, pattern_groups): + compile_start = datetime.now() + compile_count = 0 + + for group_name, patterns in pattern_groups: + if group_name not in self.regex: + self.regex[group_name] = [] + + # Transform into weight groups + if type(patterns[0]) is str or type(patterns[0][0]) not in [int, float]: + patterns = [(1.0, patterns)] + + for weight, patterns in patterns: + weight_patterns = [] + + for pattern in patterns: + # Transform into multi-fragment patterns + if type(pattern) is str: + pattern = (pattern,) + + if type(pattern) is tuple and len(pattern) == 2: + if type(pattern[0]) is str and is_list_type(pattern[1], str): + pattern = (pattern,) + + result = [] + for value in pattern: + if type(value) is tuple: + if len(value) == 2: + # Construct OR-list pattern + value = value[0] % '|'.join(value[1]) + elif len(value) == 1: + value = value[0] + + result.append(re.compile(value, re.IGNORECASE)) + compile_count += 1 + + weight_patterns.append(tuple(result)) + + self.regex[group_name].append((weight, weight_patterns)) + + Logr.info("Compiled %s patterns in %ss", compile_count, delta_seconds(datetime.now() - compile_start)) + + def find_group(self, name): + for group_name, weight_groups in self.regex.items(): + if group_name and group_name == name: + return group_name, weight_groups + + return None, None + + def value_match(self, value, group_name=None, single=True): + result = None + + for group, weight_groups in self.regex.items(): + if group_name and group != group_name: + continue + + # TODO handle multiple weights + weight, patterns = weight_groups[0] + + for pattern in patterns: + match = pattern[0].match(value) + if not match: + continue + + if result is None: + result = {} + if group not in result: + result[group] = {} + + result[group].update(match.groupdict()) + + if single: + return result + + return result + + def fragment_match(self, fragment, group_name=None): + """Follow a fragment chain to try find a match + + :type fragment: caper.objects.CaperFragment + :type group_name: str or None + + :return: The weight of the match found between 0.0 and 1.0, + where 1.0 means perfect match and 0.0 means no match + :rtype: (float, dict, int) + """ + + group_name, weight_groups = self.find_group(group_name) + + for weight, patterns in weight_groups: + for pattern in patterns: + cur_fragment = fragment + success = True + result = {} + + # Ignore empty patterns + if len(pattern) < 1: + break + + for fragment_pattern in pattern: + if not cur_fragment: + success = False + break + + match = fragment_pattern.match(cur_fragment.value) + if match: + update_dict(result, match.groupdict()) + else: + success = False + break + + cur_fragment = cur_fragment.right if cur_fragment else None + + if success: + Logr.debug("Found match with weight %s" % weight) + return float(weight), result, len(pattern) + + return 0.0, None, 1 diff --git a/libs/caper/objects.py b/libs/caper/objects.py new file mode 100644 index 00000000..b7d9084d --- /dev/null +++ b/libs/caper/objects.py @@ -0,0 +1,124 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from caper.helpers import xrange_six + + +class CaperClosure(object): + __key__ = 'closure' + + def __init__(self, index, value): + #: :type: int + self.index = index + + #: :type: str + self.value = value + + #: :type: CaperClosure + self.left = None + #: :type: CaperClosure + self.right = None + + #: :type: list of CaperFragment + self.fragments = [] + + def __str__(self): + return "" % repr(self.result) + + def __repr__(self): + return self.__str__() diff --git a/libs/caper/parsers/__init__.py b/libs/caper/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/caper/parsers/anime.py b/libs/caper/parsers/anime.py new file mode 100644 index 00000000..86c70917 --- /dev/null +++ b/libs/caper/parsers/anime.py @@ -0,0 +1,88 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from caper.parsers.base import Parser + + +REGEX_GROUP = re.compile(r'(\(|\[)(?P.*?)(\)|\])', re.IGNORECASE) + + +PATTERN_GROUPS = [ + ('identifier', [ + r'S(?P\d+)E(?P\d+)', + r'(S(?P\d+))|(E(?P\d+))', + + r'Ep(?P\d+)', + r'$(?P\d+)^', + + (r'Episode', r'(?P\d+)'), + ]), + ('video', [ + (r'(?P%s)', [ + 'Hi10P' + ]), + (r'.(?P%s)', [ + '720p', + '1080p', + + '960x720', + '1920x1080' + ]), + (r'(?P%s)', [ + 'BD' + ]), + ]), + ('audio', [ + (r'(?P%s)', [ + 'FLAC' + ]), + ]) +] + + +class AnimeParser(Parser): + def __init__(self, debug=False): + super(AnimeParser, self).__init__(PATTERN_GROUPS, debug) + + def capture_group(self, fragment): + match = REGEX_GROUP.match(fragment.value) + + if not match: + return None + + return match.group('group') + + def run(self, closures): + """ + :type closures: list of CaperClosure + """ + + self.setup(closures) + + self.capture_closure('group', func=self.capture_group)\ + .execute(once=True) + + self.capture_fragment('show_name', single=False)\ + .until_fragment(value__re='identifier')\ + .until_fragment(value__re='video')\ + .execute() + + self.capture_fragment('identifier', regex='identifier') \ + .capture_fragment('video', regex='video', single=False) \ + .capture_fragment('audio', regex='audio', single=False) \ + .execute() + + self.result.build() + return self.result diff --git a/libs/caper/parsers/base.py b/libs/caper/parsers/base.py new file mode 100644 index 00000000..16bbc19f --- /dev/null +++ b/libs/caper/parsers/base.py @@ -0,0 +1,84 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from caper import FragmentMatcher +from caper.group import CaptureGroup +from caper.result import CaperResult, CaperClosureNode, CaperRootNode +from logr import Logr + + +class Parser(object): + def __init__(self, matcher, debug=False): + self.debug = debug + + self.matcher = matcher + + self.closures = None + #: :type: caper.result.CaperResult + self.result = None + + self._match_cache = None + self._fragment_pos = None + self._closure_pos = None + self._history = None + + self.reset() + + def reset(self): + self.closures = None + self.result = CaperResult() + + self._match_cache = {} + self._fragment_pos = -1 + self._closure_pos = -1 + self._history = [] + + def setup(self, closures): + """ + :type closures: list of CaperClosure + """ + + self.reset() + self.closures = closures + + self.result.heads = [CaperRootNode(closures[0])] + + def run(self, closures): + """ + :type closures: list of CaperClosure + """ + + raise NotImplementedError() + + # + # Capture Methods + # + + def capture_fragment(self, tag, regex=None, func=None, single=True, **kwargs): + return CaptureGroup(self, self.result).capture_fragment( + tag, + regex=regex, + func=func, + single=single, + **kwargs + ) + + def capture_closure(self, tag, regex=None, func=None, single=True, **kwargs): + return CaptureGroup(self, self.result).capture_closure( + tag, + regex=regex, + func=func, + single=single, + **kwargs + ) diff --git a/libs/caper/parsers/scene.py b/libs/caper/parsers/scene.py new file mode 100644 index 00000000..cd0a8fdf --- /dev/null +++ b/libs/caper/parsers/scene.py @@ -0,0 +1,230 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logr import Logr +from caper import FragmentMatcher +from caper.parsers.base import Parser +from caper.result import CaperFragmentNode + + +PATTERN_GROUPS = [ + ('identifier', [ + (1.0, [ + # S01E01-E02 + ('^S(?P\d+)E(?P\d+)$', '^E(?P\d+)$'), + # 'S03 E01 to E08' or 'S03 E01 - E09' + ('^S(?P\d+)$', '^E(?P\d+)$', '^(to|-)$', '^E(?P\d+)$'), + # 'E01 to E08' or 'E01 - E09' + ('^E(?P\d+)$', '^(to|-)$', '^E(?P\d+)$'), + + # S01-S03 + ('^S(?P\d+)$', '^S(?P\d+)$'), + + # S02E13 + r'^S(?P\d+)E(?P\d+)$', + # S01 E13 + (r'^(S(?P\d+))$', r'^(E(?P\d+))$'), + # S02 + # E13 + r'^((S(?P\d+))|(E(?P\d+)))$', + # 3x19 + r'^(?P\d+)x(?P\d+)$', + + # 2013.09.15 + (r'^(?P\d{4})$', r'^(?P\d{2})$', r'^(?P\d{2})$'), + # 09.15.2013 + (r'^(?P\d{2})$', r'^(?P\d{2})$', r'^(?P\d{4})$'), + # TODO - US/UK Date Format Conflict? will only support US format for now.. + # 15.09.2013 + #(r'^(?P\d{2})$', r'^(?P\d{2})$', r'^(?P\d{4})$'), + # 130915 + r'^(?P\d{2})(?P\d{2})(?P\d{2})$', + + # Season 3 Episode 14 + (r'^Se(ason)?$', r'^(?P\d+)$', r'^Ep(isode)?$', r'^(?P\d+)$'), + # Season 3 + (r'^Se(ason)?$', r'^(?P\d+)$'), + # Episode 14 + (r'^Ep(isode)?$', r'^(?P\d+)$'), + + # Part.3 + # Part.1.and.Part.3 + ('^Part$', '(?P\d+)'), + + r'(?PSpecial)', + r'(?PNZ|AU|US|UK)' + ]), + (0.8, [ + # 100 - 1899, 2100 - 9999 (skips 1900 to 2099 - so we don't get years my mistake) + # TODO - Update this pattern on 31 Dec 2099 + r'^(?P([1-9])|(1[0-8])|(2[1-9])|([3-9][0-9]))(?P\d{2})$' + ]), + (0.5, [ + # 100 - 9999 + r'^(?P([1-9])|([1-9][0-9]))(?P\d{2})$' + ]) + ]), + + ('video', [ + r'(?PFS|WS)', + + (r'(?P%s)', [ + '480p', + '720p', + '1080p' + ]), + + # + # Source + # + + (r'(?P%s)', [ + 'DVDRiP', + # HDTV + 'HDTV', + 'PDTV', + 'DSR', + # WEB + 'WEBRip', + 'WEBDL', + # BluRay + 'BluRay', + 'B(D|R)Rip', + # DVD + 'DVDR', + 'DVD9', + 'DVD5' + ]), + + # For multi-fragment 'WEB-DL', 'WEB-Rip', etc... matches + ('(?PWEB)', '(?PDL|Rip)'), + + # + # Codec + # + + (r'(?P%s)', [ + 'x264', + 'XViD', + 'H264', + 'AVC' + ]), + + # For multi-fragment 'H 264' tags + ('(?PH)', '(?P264)'), + ]), + + ('dvd', [ + r'D(ISC)?(?P\d+)', + + r'R(?P[0-8])', + + (r'(?P%s)', [ + 'PAL', + 'NTSC' + ]), + ]), + + ('audio', [ + (r'(?P%s)', [ + 'AC3', + 'TrueHD' + ]), + + (r'(?P%s)', [ + 'GERMAN', + 'DUTCH', + 'FRENCH', + 'SWEDiSH', + 'DANiSH', + 'iTALiAN' + ]), + ]), + + ('scene', [ + r'(?PPROPER|REAL)', + ]) +] + + +class SceneParser(Parser): + matcher = None + + def __init__(self, debug=False): + if not SceneParser.matcher: + SceneParser.matcher = FragmentMatcher(PATTERN_GROUPS) + Logr.info("Fragment matcher for %s created", self.__class__.__name__) + + super(SceneParser, self).__init__(SceneParser.matcher, debug) + + def capture_group(self, fragment): + if fragment.closure.index + 1 != len(self.closures): + return None + + if fragment.left_sep != '-' or fragment.right: + return None + + return fragment.value + + def run(self, closures): + """ + :type closures: list of CaperClosure + """ + + self.setup(closures) + + self.capture_fragment('show_name', single=False)\ + .until_fragment(node__re='identifier')\ + .until_fragment(node__re='video')\ + .until_fragment(node__re='dvd')\ + .until_fragment(node__re='audio')\ + .until_fragment(node__re='scene')\ + .execute() + + self.capture_fragment('identifier', regex='identifier', single=False)\ + .capture_fragment('video', regex='video', single=False)\ + .capture_fragment('dvd', regex='dvd', single=False)\ + .capture_fragment('audio', regex='audio', single=False)\ + .capture_fragment('scene', regex='scene', single=False)\ + .until_fragment(left_sep__eq='-', right__eq=None)\ + .execute() + + self.capture_fragment('group', func=self.capture_group)\ + .execute() + + self.print_tree(self.result.heads) + + self.result.build() + return self.result + + def print_tree(self, heads): + if not self.debug: + return + + for head in heads: + head = head if type(head) is list else [head] + + if type(head[0]) is CaperFragmentNode: + for fragment in head[0].fragments: + Logr.debug(fragment.value) + else: + Logr.debug(head[0].closure.value) + + for node in head: + Logr.debug('\t' + str(node).ljust(55) + '\t' + ( + str(node.match.weight) + '\t' + str(node.match.result) + ) if node.match else '') + + if len(head) > 0 and head[0].parent: + self.print_tree([head[0].parent]) diff --git a/libs/caper/parsers/usenet.py b/libs/caper/parsers/usenet.py new file mode 100644 index 00000000..f622d43b --- /dev/null +++ b/libs/caper/parsers/usenet.py @@ -0,0 +1,115 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logr import Logr +from caper import FragmentMatcher +from caper.parsers.base import Parser + + +PATTERN_GROUPS = [ + ('usenet', [ + r'\[(?P#[\w\.@]+)\]', + r'^\[(?P\w+)\]$', + r'\[(?PFULL)\]', + r'\[\s?(?PTOWN)\s?\]', + r'(.*?\s)?[_\W]*(?Pwww\..*?\.[a-z0-9]+)[_\W]*(.*?\s)?', + r'(.*?\s)?[_\W]*(?P(www\.)?[-\w]+\.(com|org|info))[_\W]*(.*?\s)?' + ]), + + ('part', [ + r'.?(?P\d+)/(?P\d+).?' + ]), + + ('detail', [ + r'[\s-]*\w*?[\s-]*\"(?P.*?)\"[\s-]*\w*?[\s-]*(?P[\d,\.]*\s?MB)?[\s-]*(?PyEnc)?', + r'(?P[\d,\.]*\s?MB)[\s-]*(?PyEnc)', + r'(?P[\d,\.]*\s?MB)|(?PyEnc)' + ]) +] + + +class UsenetParser(Parser): + matcher = None + + def __init__(self, debug=False): + if not UsenetParser.matcher: + UsenetParser.matcher = FragmentMatcher(PATTERN_GROUPS) + Logr.info("Fragment matcher for %s created", self.__class__.__name__) + + super(UsenetParser, self).__init__(UsenetParser.matcher, debug) + + def run(self, closures): + """ + :type closures: list of CaperClosure + """ + + self.setup(closures) + + # Capture usenet or part info until we get a part or matching fails + self.capture_closure('usenet', regex='usenet', single=False)\ + .capture_closure('part', regex='part', single=True) \ + .until_result(tag='part') \ + .until_failure()\ + .execute() + + is_town_release, has_part = self.get_state() + + if not is_town_release: + self.capture_release_name() + + # If we already have the part (TOWN releases), ignore matching part again + if not is_town_release and not has_part: + self.capture_fragment('part', regex='part', single=True)\ + .until_closure(node__re='usenet')\ + .until_success()\ + .execute() + + # Capture any leftover details + self.capture_closure('usenet', regex='usenet', single=False)\ + .capture_closure('detail', regex='detail', single=False)\ + .execute() + + self.result.build() + return self.result + + def capture_release_name(self): + self.capture_closure('detail', regex='detail', single=False)\ + .until_failure()\ + .execute() + + self.capture_fragment('release_name', single=False, include_separators=True) \ + .until_closure(node__re='usenet') \ + .until_closure(node__re='detail') \ + .until_closure(node__re='part') \ + .until_fragment(value__eq='-')\ + .execute() + + # Capture any detail after the release name + self.capture_closure('detail', regex='detail', single=False)\ + .until_failure()\ + .execute() + + def get_state(self): + # TODO multiple-chains? + is_town_release = False + has_part = False + + for tag, result in self.result.heads[0].captured(): + if tag == 'usenet' and result.get('group') == 'TOWN': + is_town_release = True + + if tag == 'part': + has_part = True + + return is_town_release, has_part diff --git a/libs/caper/result.py b/libs/caper/result.py new file mode 100644 index 00000000..c9e34237 --- /dev/null +++ b/libs/caper/result.py @@ -0,0 +1,213 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +from logr import Logr + + +GROUP_MATCHES = ['identifier'] + + +class CaperNode(object): + def __init__(self, closure, parent=None, match=None): + """ + :type parent: CaperNode + :type weight: float + """ + + #: :type: caper.objects.CaperClosure + self.closure = closure + + #: :type: CaperNode + self.parent = parent + + #: :type: CaptureMatch + self.match = match + + #: :type: list of CaptureGroup + self.finished_groups = [] + + def next(self): + raise NotImplementedError() + + def captured(self): + cur = self + + if cur.match: + yield cur.match.tag, cur.match.result + + while cur.parent: + cur = cur.parent + + if cur.match: + yield cur.match.tag, cur.match.result + + +class CaperRootNode(CaperNode): + def __init__(self, closure): + """ + :type closure: caper.objects.CaperClosure or list of caper.objects.CaperClosure + """ + super(CaperRootNode, self).__init__(closure) + + def next(self): + return self.closure + + +class CaperClosureNode(CaperNode): + def __init__(self, closure, parent=None, match=None): + """ + :type closure: caper.objects.CaperClosure or list of caper.objects.CaperClosure + """ + super(CaperClosureNode, self).__init__(closure, parent, match) + + def next(self): + if not self.closure: + return None + + if self.match: + # Jump to next closure if we have a match + return self.closure.right + elif len(self.closure.fragments) > 0: + # Otherwise parse the fragments + return self.closure.fragments[0] + + return None + + def __str__(self): + return "" % repr(self.match) + + def __repr__(self): + return self.__str__() + + +class CaperFragmentNode(CaperNode): + def __init__(self, closure, fragments, parent=None, match=None): + """ + :type closure: caper.objects.CaperClosure + :type fragments: list of caper.objects.CaperFragment + """ + super(CaperFragmentNode, self).__init__(closure, parent, match) + + #: :type: caper.objects.CaperFragment or list of caper.objects.CaperFragment + self.fragments = fragments + + def next(self): + if len(self.fragments) > 0 and self.fragments[-1] and self.fragments[-1].right: + return self.fragments[-1].right + + if self.closure.right: + return self.closure.right + + return None + + def __str__(self): + return "" % repr(self.match) + + def __repr__(self): + return self.__str__() + + +class CaperResult(object): + def __init__(self): + #: :type: list of CaperNode + self.heads = [] + + self.chains = [] + + def build(self): + max_matched = 0 + + for head in self.heads: + for chain in self.combine_chain(head): + if chain.num_matched > max_matched: + max_matched = chain.num_matched + + self.chains.append(chain) + + for chain in self.chains: + chain.weights.append(chain.num_matched / float(max_matched or chain.num_matched or 1)) + chain.finish() + + self.chains.sort(key=lambda chain: chain.weight, reverse=True) + + for chain in self.chains: + Logr.debug("chain weight: %.02f", chain.weight) + Logr.debug("\tInfo: %s", chain.info) + + Logr.debug("\tWeights: %s", chain.weights) + Logr.debug("\tNumber of Fragments Matched: %s", chain.num_matched) + + def combine_chain(self, subject, chain=None): + nodes = subject if type(subject) is list else [subject] + + if chain is None: + chain = CaperResultChain() + + result = [] + + for x, node in enumerate(nodes): + node_chain = chain if x == len(nodes) - 1 else chain.copy() + + if not node.parent: + result.append(node_chain) + continue + + node_chain.update(node) + result.extend(self.combine_chain(node.parent, node_chain)) + + return result + + +class CaperResultChain(object): + def __init__(self): + #: :type: float + self.weight = None + self.info = {} + self.num_matched = 0 + + self.weights = [] + + def update(self, subject): + """ + :type subject: CaperFragmentNode + """ + if not subject.match or not subject.match.success: + return + + # TODO this should support closure nodes + if type(subject) is CaperFragmentNode: + self.num_matched += len(subject.fragments) if subject.fragments is not None else 0 + + self.weights.append(subject.match.weight) + + if subject.match: + if subject.match.tag not in self.info: + self.info[subject.match.tag] = [] + + self.info[subject.match.tag].insert(0, subject.match.result) + + def finish(self): + self.weight = sum(self.weights) / len(self.weights) + + def copy(self): + chain = CaperResultChain() + + chain.weight = self.weight + chain.info = copy.deepcopy(self.info) + + chain.num_matched = self.num_matched + chain.weights = copy.copy(self.weights) + + return chain \ No newline at end of file diff --git a/libs/caper/step.py b/libs/caper/step.py new file mode 100644 index 00000000..817514b6 --- /dev/null +++ b/libs/caper/step.py @@ -0,0 +1,96 @@ +# Copyright 2013 Dean Gardiner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from caper.objects import CaptureMatch +from logr import Logr + + +class CaptureStep(object): + REPR_KEYS = ['regex', 'func', 'single'] + + def __init__(self, capture_group, tag, source, regex=None, func=None, single=None, **kwargs): + #: @type: CaptureGroup + self.capture_group = capture_group + + #: @type: str + self.tag = tag + #: @type: str + self.source = source + #: @type: str + self.regex = regex + #: @type: function + self.func = func + #: @type: bool + self.single = single + + self.kwargs = kwargs + + self.matched = False + + def execute(self, fragment): + """Execute step on fragment + + :type fragment: CaperFragment + :rtype : CaptureMatch + """ + + match = CaptureMatch(self.tag, self) + + if self.regex: + weight, result, num_fragments = self.capture_group.parser.matcher.fragment_match(fragment, self.regex) + Logr.debug('(execute) [regex] tag: "%s"', self.tag) + + if not result: + return match + + # Populate CaptureMatch + match.success = True + match.weight = weight + match.result = result + match.num_fragments = num_fragments + elif self.func: + result = self.func(fragment) + Logr.debug('(execute) [func] %s += "%s"', self.tag, match) + + if not result: + return match + + # Populate CaptureMatch + match.success = True + match.weight = 1.0 + match.result = result + else: + Logr.debug('(execute) [raw] %s += "%s"', self.tag, fragment.value) + + include_separators = self.kwargs.get('include_separators', False) + + # Populate CaptureMatch + match.success = True + match.weight = 1.0 + + if include_separators: + match.result = (fragment.left_sep, fragment.value, fragment.right_sep) + else: + match.result = fragment.value + + return match + + def __repr__(self): + attribute_values = [key + '=' + repr(getattr(self, key)) + for key in self.REPR_KEYS + if hasattr(self, key) and getattr(self, key)] + + attribute_string = ', ' + ', '.join(attribute_values) if len(attribute_values) > 0 else '' + + return "CaptureStep('%s'%s)" % (self.tag, attribute_string) diff --git a/libs/logr/__init__.py b/libs/logr/__init__.py new file mode 100644 index 00000000..7a2d7b2e --- /dev/null +++ b/libs/logr/__init__.py @@ -0,0 +1,225 @@ +# logr - Simple python logging wrapper +# Packed by Dean Gardiner +# +# File part of: +# rdio-sock - Rdio WebSocket Library +# Copyright (C) 2013 fzza- + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import inspect +import logging +import os +import sys + +IGNORE = () +PY3 = sys.version_info[0] == 3 + + +class Logr(object): + loggers = {} + handler = None + + trace_origin = False + name = "Logr" + + @staticmethod + def configure(level=logging.WARNING, handler=None, formatter=None, trace_origin=False, name="Logr"): + """Configure Logr + + @param handler: Logger message handler + @type handler: logging.Handler or None + + @param formatter: Logger message Formatter + @type formatter: logging.Formatter or None + """ + if formatter is None: + formatter = LogrFormatter() + + if handler is None: + handler = logging.StreamHandler() + + handler.setFormatter(formatter) + handler.setLevel(level) + Logr.handler = handler + + Logr.trace_origin = trace_origin + Logr.name = name + + @staticmethod + def configure_check(): + if Logr.handler is None: + Logr.configure() + + @staticmethod + def _get_name_from_path(filename): + try: + return os.path.splitext(os.path.basename(filename))[0] + except TypeError: + return "" + + @staticmethod + def get_frame_class(frame): + if len(frame.f_code.co_varnames) <= 0: + return None + + farg = frame.f_code.co_varnames[0] + + if farg not in frame.f_locals: + return None + + if farg == 'self': + return frame.f_locals[farg].__class__ + + if farg == 'cls': + return frame.f_locals[farg] + + return None + + + @staticmethod + def get_logger_name(): + if not Logr.trace_origin: + return Logr.name + + stack = inspect.stack() + + for x in xrange_six(len(stack)): + frame = stack[x][0] + name = None + + # Try find name of function defined inside a class + frame_class = Logr.get_frame_class(frame) + + if frame_class: + class_name = frame_class.__name__ + module_name = frame_class.__module__ + + if module_name != '__main__': + name = module_name + '.' + class_name + else: + name = class_name + + # Try find name of function defined outside of a class + if name is None: + if frame.f_code.co_name in frame.f_globals: + name = frame.f_globals.get('__name__') + if name == '__main__': + name = Logr._get_name_from_path(frame.f_globals.get('__file__')) + name = name + elif frame.f_code.co_name == '': + name = Logr._get_name_from_path(frame.f_globals.get('__file__')) + + if name is not None and name not in IGNORE: + return name + + return "" + + @staticmethod + def get_logger(): + """Get or create logger (if it does not exist) + + @rtype: RootLogger + """ + name = Logr.get_logger_name() + if name not in Logr.loggers: + Logr.configure_check() + Logr.loggers[name] = logging.Logger(name) + Logr.loggers[name].addHandler(Logr.handler) + return Logr.loggers[name] + + @staticmethod + def debug(msg, *args, **kwargs): + Logr.get_logger().debug(msg, *args, **kwargs) + + @staticmethod + def info(msg, *args, **kwargs): + Logr.get_logger().info(msg, *args, **kwargs) + + @staticmethod + def warning(msg, *args, **kwargs): + Logr.get_logger().warning(msg, *args, **kwargs) + + warn = warning + + @staticmethod + def error(msg, *args, **kwargs): + Logr.get_logger().error(msg, *args, **kwargs) + + @staticmethod + def exception(msg, *args, **kwargs): + Logr.get_logger().exception(msg, *args, **kwargs) + + @staticmethod + def critical(msg, *args, **kwargs): + Logr.get_logger().critical(msg, *args, **kwargs) + + fatal = critical + + @staticmethod + def log(level, msg, *args, **kwargs): + Logr.get_logger().log(level, msg, *args, **kwargs) + + +class LogrFormatter(logging.Formatter): + LENGTH_NAME = 32 + LENGTH_LEVEL_NAME = 5 + + def __init__(self, fmt=None, datefmt=None): + if sys.version_info[:2] > (2,6): + super(LogrFormatter, self).__init__(fmt, datefmt) + else: + logging.Formatter.__init__(self, fmt, datefmt) + + def usesTime(self): + return True + + def format(self, record): + record.message = record.getMessage() + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + s = "%(asctime)s %(name)s %(levelname)s %(message)s" % { + 'asctime': record.asctime, + 'name': record.name[-self.LENGTH_NAME:].rjust(self.LENGTH_NAME, ' '), + 'levelname': record.levelname[:self.LENGTH_LEVEL_NAME].ljust(self.LENGTH_LEVEL_NAME, ' '), + 'message': record.message + } + + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + if s[-1:] != "\n": + s += "\n" + try: + s += record.exc_text + except UnicodeError: + s = s + record.exc_text.decode(sys.getfilesystemencoding(), + 'replace') + return s + + +def xrange_six(start, stop=None, step=None): + if stop is not None and step is not None: + if PY3: + return range(start, stop, step) + else: + return xrange(start, stop, step) + else: + if PY3: + return range(start) + else: + return xrange(start) From b611a98bae120cc4d463609017cd38d8e30f17ef Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 11:55:05 +0100 Subject: [PATCH 091/468] Missing fireEvent import in torrentleech --- couchpotato/core/media/movie/providers/torrent/torrentleech.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py index 9de6451c..07ace2b9 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py @@ -1,3 +1,4 @@ +from couchpotato import fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.torrentleech import Base From 72d318323e5799eb572c05c2434577ab09a5f925 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 12:17:53 +0100 Subject: [PATCH 092/468] Release ignore didn't get correct parameter --- couchpotato/core/plugins/release/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 66ca7a3b..b749ee61 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -174,13 +174,13 @@ class Release(Plugin): return False - def ignore(self, release_id = None, **kwargs): + def ignore(self, id = None, **kwargs): db = get_db() try: - rel = db.get('id', release_id, with_doc = True) - self.updateStatus(release_id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored') + rel = db.get('id', id, with_doc = True) + self.updateStatus(id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored') return { 'success': True From 7e08454edd6c330ce95bad64edacf31db4cb8bec Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 12:23:11 +0100 Subject: [PATCH 093/468] Import issues --- couchpotato/core/media/_base/providers/nzb/newznab.py | 1 + couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py | 2 +- couchpotato/core/media/_base/providers/torrent/base.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentpotato.py | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index c6430363..9f30f384 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -8,6 +8,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.base import ResultList from couchpotato.core.media._base.providers.nzb.base import NZBProvider from couchpotato.environment import Env from dateutil.parser import parse diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index 1db101a2..ab9efbda 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -35,7 +35,7 @@ class Base(NZBProvider, RSS): if quality['identifier'] in fireEvent('quality.pre_releases', single = True): return [] - return super(OMGWTFNZBs, self).search(movie, quality) + return super(Base, self).search(movie, quality) def _searchOnTitle(self, title, movie, quality, results): diff --git a/couchpotato/core/media/_base/providers/torrent/base.py b/couchpotato/core/media/_base/providers/torrent/base.py index 585768b4..2ed496c5 100644 --- a/couchpotato/core/media/_base/providers/torrent/base.py +++ b/couchpotato/core/media/_base/providers/torrent/base.py @@ -1,4 +1,5 @@ import time +import traceback from couchpotato.core.helpers.variable import getImdb, md5, cleanHost from couchpotato.core.logger import CPLog diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py index 2a17b2a4..84718f04 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py @@ -5,6 +5,7 @@ import traceback from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.base import ResultList from couchpotato.core.media._base.providers.torrent.base import TorrentProvider From 1b2d72531f6fe024c6eed120eb5e0a03c8c8b93a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 12:31:55 +0100 Subject: [PATCH 094/468] Newznab url creation failed --- couchpotato/core/media/_base/providers/nzb/newznab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 9f30f384..41147d01 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -86,7 +86,7 @@ class Base(NZBProvider, RSS): 'name_extra': name_extra, 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, - 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), + 'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), 'content': self.getTextElement(nzb, 'description'), 'score': host['extra_score'], From 178f770b16012d780ae8e0894c976bf170e65f84 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 12:33:01 +0100 Subject: [PATCH 095/468] Use key to get release last_edit --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 585f6894..be9170c0 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -935,7 +935,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) #Check status if already missing and for how long, if > 1 week, set to ignored else to missing if rel.get('status') == 'missing': - if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60: + if rel.get('last_edit') < int(time.time()) - 7 * 24 * 60 * 60: fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True) else: # Set the release to missing From f5aae23111c9b70d913bdcb7b0281f18fe16d725 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 12:49:50 +0100 Subject: [PATCH 096/468] Make sure log navigation doesn't overlay mask --- couchpotato/core/plugins/log/static/log.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index df9ab1c3..bcab6e23 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -9,7 +9,7 @@ bottom: 0; left: 0; background: #4E5969; - z-index: 200; + z-index: 100; } .page.log .nav li { From ee702d92e6bf3ca9ceebdad34ec0e21259fe561d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 15:34:43 +0100 Subject: [PATCH 097/468] Delete empty folders and leftover .pyc files on restart --- couchpotato/core/_base/_core.py | 4 ++++ couchpotato/core/_base/updater/main.py | 11 ++++++----- couchpotato/core/plugins/base.py | 21 +++++++++++++++++++++ couchpotato/core/plugins/renamer.py | 19 ------------------- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 730bc884..0a98103b 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -51,6 +51,7 @@ class Core(Plugin): addEvent('app.api_url', self.createApiUrl) addEvent('app.version', self.version) addEvent('app.load', self.checkDataDir) + addEvent('app.load', self.cleanUpFolders) addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.api_key', self.checkApikey) @@ -75,6 +76,9 @@ class Core(Plugin): return True + def cleanUpFolders(self): + self.deleteEmptyFolder(Env.get('app_dir'), show_error = False) + def available(self, **kwargs): return { 'success': True diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 17ed1bdd..cba91b7e 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -142,6 +142,10 @@ class Updater(Plugin): 'success': success } + def doShutdown(self): + self.updater.deletePyc(show_logs = False) + return super(Updater, self).doShutdown() + class BaseUpdater(Plugin): @@ -176,7 +180,7 @@ class BaseUpdater(Plugin): def check(self): pass - def deletePyc(self, only_excess = True): + def deletePyc(self, only_excess = True, show_logs = True): for root, dirs, files in scandir.walk(ss(Env.get('app_dir'))): @@ -186,7 +190,7 @@ class BaseUpdater(Plugin): for excess_pyc_file in excess_pyc_files: full_path = os.path.join(root, excess_pyc_file) - log.debug('Removing old PYC file: %s', full_path) + if show_logs: log.debug('Removing old PYC file: %s', full_path) try: os.remove(full_path) except: @@ -212,9 +216,6 @@ class GitUpdater(BaseUpdater): log.info('Updating to latest version') self.repo.pull() - # Delete leftover .pyc files - self.deletePyc() - return True except: log.error('Failed updating via GIT: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 2bd15e55..ace9de37 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -17,6 +17,7 @@ from couchpotato.environment import Env import requests from requests.packages.urllib3 import Timeout from requests.packages.urllib3.exceptions import MaxRetryError +from scandir import scandir from tornado import template from tornado.web import StaticFileHandler @@ -146,6 +147,26 @@ class Plugin(object): return False + def deleteEmptyFolder(self, folder, show_error = True): + folder = sp(folder) + + for root, dirs, files in scandir.walk(folder): + + for dir_name in dirs: + full_path = os.path.join(root, dir_name) + if len(os.listdir(full_path)) == 0: + try: + os.rmdir(full_path) + except: + if show_error: + log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) + + try: + os.rmdir(folder) + except: + if show_error: + log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) + # http request def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True): url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]") diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index be9170c0..1701c034 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -823,25 +823,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) return string - def deleteEmptyFolder(self, folder, show_error = True): - folder = sp(folder) - - loge = log.error if show_error else log.debug - for root, dirs, files in scandir.walk(folder): - - for dir_name in dirs: - full_path = os.path.join(root, dir_name) - if len(os.listdir(full_path)) == 0: - try: - os.rmdir(full_path) - except: - loge('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) - - try: - os.rmdir(folder) - except: - loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) - def checkSnatched(self, fire_scan = True): if self.checking_snatched: From 42e3c95f87e4fcbcf0235d30f5fc919bb8aa0d14 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sat, 15 Mar 2014 17:05:26 +0100 Subject: [PATCH 098/468] added qbittorrent-python --- libs/qbittorrent/__init__.py | 1 + libs/qbittorrent/base.py | 62 ++++++++++++++++++++++++++++++++++ libs/qbittorrent/client.py | 33 +++++++++++++++++++ libs/qbittorrent/file.py | 13 ++++++++ libs/qbittorrent/helpers.py | 7 ++++ libs/qbittorrent/torrent.py | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 libs/qbittorrent/__init__.py create mode 100644 libs/qbittorrent/base.py create mode 100644 libs/qbittorrent/client.py create mode 100644 libs/qbittorrent/file.py create mode 100644 libs/qbittorrent/helpers.py create mode 100644 libs/qbittorrent/torrent.py diff --git a/libs/qbittorrent/__init__.py b/libs/qbittorrent/__init__.py new file mode 100644 index 00000000..5e3048b2 --- /dev/null +++ b/libs/qbittorrent/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1' \ No newline at end of file diff --git a/libs/qbittorrent/base.py b/libs/qbittorrent/base.py new file mode 100644 index 00000000..328e008a --- /dev/null +++ b/libs/qbittorrent/base.py @@ -0,0 +1,62 @@ +from urlparse import urljoin +import logging + +log = logging.getLogger(__name__) + + +class Base(object): + properties = {} + + def __init__(self, url, session, client=None): + self._client = client + self._url = url + self._session = session + + @staticmethod + def _convert(response, response_type): + if response_type == 'json': + try: + return response.json() + except ValueError: + pass + + return response + + def _get(self, path='', response_type='json', **kwargs): + r = self._session.get(urljoin(self._url, path), **kwargs) + return self._convert(r, response_type) + + def _post(self, path='', response_type='json', data=None, **kwargs): + r = self._session.post(urljoin(self._url, path), data, **kwargs) + return self._convert(r, response_type) + + def _fill(self, data): + for key, value in data.items(): + if self.set_property(self, key, value): + continue + + log.debug('%s is missing item with key "%s" and value %s', self.__class__, key, repr(value)) + + @classmethod + def parse(cls, client, data): + obj = cls(client._url, client._session, client) + obj._fill(data) + + return obj + + @classmethod + def set_property(cls, obj, key, value): + prop = cls.properties.get(key, {}) + + if prop.get('key'): + key = prop['key'] + + if not hasattr(obj, key): + return False + + + if prop.get('parse'): + value = prop['parse'](value) + + setattr(obj, key, value) + return True diff --git a/libs/qbittorrent/client.py b/libs/qbittorrent/client.py new file mode 100644 index 00000000..0365a1a8 --- /dev/null +++ b/libs/qbittorrent/client.py @@ -0,0 +1,33 @@ +from qbittorrent.base import Base +from qbittorrent.torrent import Torrent +from requests import Session +from requests.auth import HTTPDigestAuth + + +class QBittorrentClient(Base): + def __init__(self, url, username=None, password=None): + super(QBittorrentClient, self).__init__(url, Session()) + + if username and password: + self._session.auth = HTTPDigestAuth(username, password) + + def test_connection(self): + r = self._get(response_type='response') + + return r.status_code == 200 + + def add_file(self, file): + self._post('command/upload', files={'torrent': file}) + + def add_url(self, urls): + if type(urls) is not list: + urls = [urls] + + urls = '%0A'.join(urls) + + self._post('command/download', data={'urls': urls}) + + def get_torrents(self): + r = self._get('json/torrents') + + return [Torrent.parse(self, x) for x in r] diff --git a/libs/qbittorrent/file.py b/libs/qbittorrent/file.py new file mode 100644 index 00000000..0c020572 --- /dev/null +++ b/libs/qbittorrent/file.py @@ -0,0 +1,13 @@ +from qbittorrent.base import Base + + +class File(Base): + def __init__(self, url, session, client=None): + super(File, self).__init__(url, session, client) + + self.name = None + + self.progress = None + self.priority = None + + self.is_seed = None diff --git a/libs/qbittorrent/helpers.py b/libs/qbittorrent/helpers.py new file mode 100644 index 00000000..253f03e8 --- /dev/null +++ b/libs/qbittorrent/helpers.py @@ -0,0 +1,7 @@ +def try_convert(value, to_type, default=None): + try: + return to_type(value) + except ValueError: + return default + except TypeError: + return default diff --git a/libs/qbittorrent/torrent.py b/libs/qbittorrent/torrent.py new file mode 100644 index 00000000..f7efdb5c --- /dev/null +++ b/libs/qbittorrent/torrent.py @@ -0,0 +1,64 @@ +from qbittorrent.base import Base +from qbittorrent.file import File +from qbittorrent.helpers import try_convert + + +class Torrent(Base): + properties = { + 'num_seeds': { + 'key': 'seeds', + 'parse': lambda value: try_convert(value, int) + }, + 'num_leechs': { + 'key': 'leechs', + 'parse': lambda value: try_convert(value, int) + }, + 'ratio': { + 'parse': lambda value: try_convert(value, float) + } + } + + def __init__(self, url, session, client=None): + super(Torrent, self).__init__(url, session, client) + + self.hash = None + self.name = None + + self.state = None + self.ratio = None + self.progress = None + self.priority = None + + self.seeds = None + self.leechs = None + + # + # Commands + # + + def pause(self): + self._post('command/pause', data={'hash': self.hash}) + + def resume(self): + self._post('command/resume', data={'hash': self.hash}) + + def remove(self): + self._post('command/delete', data={'hashes': self.hash}) + + def delete(self): + self._post('command/deletePerm', data={'hashes': self.hash}) + + def recheck(self): + self._post('command/recheck', data={'hash': self.hash}) + + # + # Fetch details + # + + def get_files(self): + r = self._get('json/propertiesFiles/%s' % self.hash) + + return [File.parse(self._client, x) for x in r] + + def get_trackers(self): + pass From bf1d93f2567df37236fee0be017e134273da3098 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 23:42:58 +0100 Subject: [PATCH 099/468] Test download connection failed --- couchpotato/core/downloaders/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index bb7db606..269bdc62 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -162,7 +162,7 @@ class Downloader(Provider): (d_manual and manual or d_manual is False) and \ (not data or self.isCorrectProtocol(data.get('protocol'))) - def _test(self): + def _test(self, **kwargs): t = self.test() if isinstance(t, tuple): return {'success': t[0], 'msg': t[1]} From eee16c7a3d3e8d0b69df6c0ca9a98d630b72f226 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 23:43:22 +0100 Subject: [PATCH 100/468] Newznab failed when doing manual download --- couchpotato/core/media/_base/providers/nzb/newznab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 41147d01..4430ac26 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -130,7 +130,7 @@ class Base(NZBProvider, RSS): hosts = self.getHosts() for host in hosts: - result = super(Newznab, self).belongsTo(url, host = host['host'], provider = provider) + result = super(Base, self).belongsTo(url, host = host['host'], provider = provider) if result: return result From 36125f10670468669e5402a869076b5fb9487505 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 23:43:46 +0100 Subject: [PATCH 101/468] Make sure to use proper category id --- couchpotato/core/media/movie/_base/main.py | 2 +- couchpotato/core/plugins/renamer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 36e0a479..edefa611 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -106,7 +106,7 @@ class MovieBase(MovieTypeBase): 'identifier': params.get('identifier'), 'status': status if status else 'active', 'profile_id': params.get('profile_id', default_profile.get('_id')), - 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 else None, + 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None, } # Update movie info diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 1701c034..244899fa 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -256,7 +256,7 @@ class Renamer(Plugin): destination = to_folder category_label = '' - if media.get('category_id'): + if media.get('category_id') and media.get('category_id') != '-1': try: category = db.get('id', media['category_id']) category_label = category['label'] From b68cea39211d4b61fce44a26f998d6a9e7a5508d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Mar 2014 23:44:07 +0100 Subject: [PATCH 102/468] Getting release download didn't use correct key --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 244899fa..0f531bc6 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -1036,7 +1036,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) if release_download and release_download.get('id'): try: - rls = db.get('release_download', '%s_%s' % (release_download.get('downloader'), release_download.get('id')), with_doc = True)['doc'] + rls = db.get('release_download', '%s-%s' % (release_download.get('downloader'), release_download.get('id')), with_doc = True)['doc'] except: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) From c6d20eb91f8b119ae485470415da4a1a5ce7ae7b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 00:06:39 +0100 Subject: [PATCH 103/468] Code cleanup --- couchpotato/core/plugins/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index ace9de37..7d52e1eb 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -64,16 +64,11 @@ class Plugin(object): def databaseSetup(self): - db = get_db() - for index_name in self._database: klass = self._database[index_name] fireEvent('database.setup_index', index_name, klass) - def afterDatabaseSetup(self): - print self._database_indexes - def conf(self, attr, value = None, default = None, section = None): class_name = self.getName().lower().split(':')[0].lower() return Env.setting(attr, section = section if section else class_name, value = value, default = default) From 951fbdccbdc193f1aaab88673d82c03666d24dd0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 00:46:56 +0100 Subject: [PATCH 104/468] Don't load MovieBase twice --- couchpotato/core/media/movie/_base/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index edefa611..b2234cfc 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -14,8 +14,6 @@ import six log = CPLog(__name__) -autoload = 'MovieBase' - class MovieBase(MovieTypeBase): From 5853a373f3f5053f5cf6101291614e0ee900c588 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 09:20:53 +0100 Subject: [PATCH 105/468] Add library query --- .../core/media/_base/providers/base.py | 2 +- couchpotato/core/media/movie/library.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 couchpotato/core/media/movie/library.py diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 94c70a01..13f0f6e0 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -200,7 +200,7 @@ class YarrProvider(Provider): self._search(media, quality, results) # Search possible titles else: - media_title = fireEvent('library.query', media['library'], single = True) + media_title = fireEvent('library.query', media, single = True) for title in possibleTitles(media_title): self._searchOnTitle(title, media, quality, results) diff --git a/couchpotato/core/media/movie/library.py b/couchpotato/core/media/movie/library.py new file mode 100644 index 00000000..a6e29f32 --- /dev/null +++ b/couchpotato/core/media/movie/library.py @@ -0,0 +1,29 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.library.base import LibraryBase + + +log = CPLog(__name__) + +autoload = 'MovieLibraryPlugin' + + +class MovieLibraryPlugin(LibraryBase): + + def __init__(self): + addEvent('library.query', self.query) + + def query(self, media, first = True, include_year = True, **kwargs): + if media.get('type') != 'movie': + return + + titles = media['info'].get('titles', []) + + # Add year identifier to titles + if include_year: + titles = [title + (' %s' % str(media['info']['year'])) for title in titles] + + if first: + return titles[0] if titles else None + + return titles From f42fb2fdd2a41493cb10c8ef73196e63771613a6 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 11:35:28 +0100 Subject: [PATCH 106/468] updated qbittorrent-python --- libs/qbittorrent/client.py | 39 +++++++++++++++++++++++++++++++++++++ libs/qbittorrent/torrent.py | 17 ++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/libs/qbittorrent/client.py b/libs/qbittorrent/client.py index 0365a1a8..bc59cd0e 100644 --- a/libs/qbittorrent/client.py +++ b/libs/qbittorrent/client.py @@ -2,6 +2,7 @@ from qbittorrent.base import Base from qbittorrent.torrent import Torrent from requests import Session from requests.auth import HTTPDigestAuth +import time class QBittorrentClient(Base): @@ -28,6 +29,44 @@ class QBittorrentClient(Base): self._post('command/download', data={'urls': urls}) def get_torrents(self): + """Fetch all torrents + + :return: list of Torrent + """ r = self._get('json/torrents') return [Torrent.parse(self, x) for x in r] + + def get_torrent(self, hash, include_general=True, max_retries=5): + """Fetch details for torrent by info_hash. + + :param info_hash: Torrent info hash + :param include_general: Include general torrent properties + :param max_retries: Maximum number of retries to wait for torrent to appear in client + + :rtype: Torrent or None + """ + + torrent = None + retries = 0 + + # Try find torrent in client + while retries < max_retries: + # TODO this wouldn't be very efficient with large numbers of torrents on the client + torrents = dict([(t.hash, t) for t in self.get_torrents()]) + + if hash in torrents: + torrent = torrents[hash] + break + + retries += 1 + time.sleep(1) + + if torrent is None: + return None + + # Fetch general properties for torrent + if include_general: + torrent.update_general() + + return torrent diff --git a/libs/qbittorrent/torrent.py b/libs/qbittorrent/torrent.py index f7efdb5c..a9ff51d2 100644 --- a/libs/qbittorrent/torrent.py +++ b/libs/qbittorrent/torrent.py @@ -32,6 +32,10 @@ class Torrent(Base): self.seeds = None self.leechs = None + # General properties + self.comment = None + self.save_path = None + # # Commands # @@ -62,3 +66,16 @@ class Torrent(Base): def get_trackers(self): pass + + # + # Update torrent details + # + + def update_general(self): + r = self._get('json/propertiesGeneral/%s' % self.hash) + + if r: + self._fill(r) + return True + + return False From fe16115b2063e3ddb28a3cc4efa5f3d5cf7bd06d Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 14:00:01 +0100 Subject: [PATCH 107/468] add qbittorrent client --- couchpotato/core/downloaders/qbittorrent_.py | 244 +++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 couchpotato/core/downloaders/qbittorrent_.py diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py new file mode 100644 index 00000000..c1fdc2a7 --- /dev/null +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -0,0 +1,244 @@ +from base64 import b16encode, b32decode +from hashlib import sha1 +import os + +from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core.helpers.encoding import sp +from couchpotato.core.helpers.variable import cleanHost +from couchpotato.core.logger import CPLog + +from qbittorrent.client import QBittorrentClient + +log = CPLog(__name__) + +autoload = 'qBittorrent' + + +class qBittorrent(Downloader): + + protocol = ['torrent', 'torrent_magnet'] + qb = None + + def __init__(self): + super(qBittorrent, self).__init__() + + def connect(self): + if self.qb is not None: + return self.qb + + url = cleanHost(self.conf('host'), protocol = True, ssl = False) + + if self.conf('username') and self.conf('password'): + self.qb = QBittorrentClient( + url, + username = self.conf('username'), + password = self.conf('password') + ) + else: + self.qb = QBittorrentClient(url) + + return self.qb + + def test(self): + if self.connect(): + return True + + return False + + + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + + log.debug('Sending "%s" to qBittorrent.', (data.get('name'))) + + if not self.connect(): + return False + + if not filedata and data.get('protocol') == 'torrent': + log.error('Failed sending torrent, no data') + return False + + info = bdecode(filedata)["info"] + torrent_hash = sha1(bencode(info)).hexdigest().upper() + + # Convert base 32 to hex + if len(torrent_hash) == 32: + torrent_hash = b16encode(b32decode(torrent_hash)) + + # Send request to qBittorrent + try: + if data.get('protocol') == 'torrent_magnet': + torrent = self.qb.add_url(filedata) + else: + torrent = self.qb.add_file(filedata) + + if not torrent: + log.error('Unable to find the torrent, did it fail to load?') + return False + + return self.downloadReturnId(torrent_hash) + except Exception as e: + log.error('Failed to send torrent to qBittorrent: %s', e) + return False + + def getTorrentStatus(self, torrent): + + if torrent.state in ('uploading', 'queuedUP', 'stalledUP'): + return 'seeding' + + if torrent.progress == 1: + return 'completed' + + return 'busy' + + def getAllDownloadStatus(self, ids): + log.debug('Checking qBittorrent download status.') + + if not self.connect(): + return [] + + try: + torrents = self.qb.get_torrents() + self.qb.update_general() # get extra info + + release_downloads = ReleaseDownloadList(self) + + for torrent in torrents: + if torrent.hash in ids: + torrent_files = [] + t_files = torrent.get_files() + + check_dir = os.path.join(torrent.save_path, torrent.name) + if os.path.isdir(check_dir): + torrent.save_path = os.path.isdir(check_dir) + + if len(t_files) > 1 and os.path.isdir(torrent.save_path): # multi file torrent + for root, _, files in os.walk(torrent.save_path): + for f in files: + p = os.path.join(root, f) + if os.path.isfile(p): + torrent_files.append(sp(p)) + + else: # multi or single file placed directly in torrent.save_path + for f in t_files: + p = os.path.join(torrent.save_path, f.name) + if os.path.isfile(p): + torrent_files.append(sp(p)) + + release_downloads.append({ + 'id': torrent.hash, + 'name': torrent.name, + 'status': self.getTorrentStatus(torrent), + 'seed_ratio': torrent.ratio, + 'original_status': torrent.state, + 'timeleft': torrent.progress * 100 if torrent.progress else -1, # percentage + 'folder': sp(torrent.save_path), + 'files': '|'.join(torrent_files) + }) + + return release_downloads + + except Exception as e: + log.error('Failed to get status from qBittorrent: %s', e) + return [] + + def pause(self, release_download, pause = True): + if not self.connect(): + return False + + torrent = self.qb.get_torrent(release_download['id']) + if torrent is None: + return False + + if pause: + return torrent.pause() + return torrent.resume() + + def removeFailed(self, release_download): + log.info('%s failed downloading, deleting...', release_download['name']) + return self.processComplete(release_download, delete_files = True) + + def processComplete(self, release_download, delete_files): + log.debug('Requesting qBittorrent to remove the torrent %s%s.', + (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) + + if not self.connect(): + return False + + torrent = self.qb.find_torrent(release_download['id']) + + if torrent is None: + return False + + if delete_files: + torrent.delete() # deletes torrent with data + else: + torrent.remove() # just removes the torrent, doesn't delete data + + return True + + +config = [{ + 'name': 'qbittorrent', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'qbittorrent', + 'label': 'qbittorrent', + 'description': '', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'http://localhost:8080/', + 'description': 'RPC Communication URI. Usually http://localhost:8080/' + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': False, + 'advanced': True, + 'type': 'bool', + 'description': 'Remove the torrent after it finishes seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] From 89d7a924fb316d327dc6c182e6b5b684655d022b Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Mar 2014 15:49:16 +0000 Subject: [PATCH 108/468] Proxy; Fixed a link with SSL --- couchpotato/core/providers/torrent/yify/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py index fe1b8204..75fc8475 100644 --- a/couchpotato/core/providers/torrent/yify/main.py +++ b/couchpotato/core/providers/torrent/yify/main.py @@ -21,7 +21,7 @@ class Yify(TorrentMagnetProvider): 'http://yify-torrents.com.come.in', 'http://yts.re', 'http://yts.im' - 'https://yify-torrents.im', + 'http://yify-torrents.im', ] def search(self, movie, quality): From db11c1b7a8da1b112de1bcb6ba75b590da0ecaad Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 16:53:15 +0100 Subject: [PATCH 109/468] BinSearch: only check if there aren't enough parts --- couchpotato/core/media/_base/providers/nzb/binsearch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index 270b2a11..90e403d2 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -53,7 +53,7 @@ class Base(NZBProvider): total = tryInt(parts.group('total')) parts = tryInt(parts.group('parts')) - if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower())): + if (total / parts) < 1 and ((total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower()))): log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total)) return False From e063028c7df6b14d33f4e28d0653f7b44e06d271 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 16:54:21 +0100 Subject: [PATCH 110/468] Remove Yify ssl url --- couchpotato/core/media/_base/providers/torrent/yify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 84ce80b7..9380e651 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -23,7 +23,7 @@ class Base(TorrentProvider): 'http://yify-torrents.com.come.in', 'http://yts.re', 'http://yts.im' - 'https://yify-torrents.im', + 'http://yify-torrents.im', ] def search(self, movie, quality): From d74578ec662a5912965ad381a69b42d18292168d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 17:54:57 +0100 Subject: [PATCH 111/468] FreeBSD guide update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19616889..10d3b773 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ FreeBSD : * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time * To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato` -* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato` +* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato` * Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato` * Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf` * Open your browser and go to: `http://server:5050/` From ec3a6e65ae457683d09a1517f190ed1cb9cf32a0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 18:27:58 +0100 Subject: [PATCH 112/468] Safer way to get reddit url --- couchpotato/core/media/movie/providers/userscript/reddit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/userscript/reddit.py b/couchpotato/core/media/movie/providers/userscript/reddit.py index 3ab1d08d..8cb81079 100644 --- a/couchpotato/core/media/movie/providers/userscript/reddit.py +++ b/couchpotato/core/media/movie/providers/userscript/reddit.py @@ -10,7 +10,8 @@ class Reddit(UserscriptBase): includes = ['*://www.reddit.com/r/Ijustwatched/comments/*'] def getMovie(self, url): - name = splitString(url, '/')[-1] + name = splitString(splitString(url, '/ijw_')[-1], '/')[0] + if name.startswith('ijw_'): name = name[4:] From 9618a8d5432eca80d1ae43f09dddf4a07f583267 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 20:55:13 +0100 Subject: [PATCH 113/468] Position yeah in thumblist --- couchpotato/core/media/movie/_base/static/movie.css | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index b15b7ccf..d579d542 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -344,6 +344,7 @@ top: auto; right: auto; color: #FFF; + line-height: 18px; } .touch_enabled .movies.list_list .movie .info .year { From 6aa0b7c748ddc082156b0b77aafe8cc24034c886 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 21:04:23 +0100 Subject: [PATCH 114/468] Missing autoload --- couchpotato/core/media/movie/providers/torrent/iptorrents.py | 2 ++ .../core/media/movie/providers/torrent/kickasstorrents.py | 2 ++ .../core/media/movie/providers/torrent/passthepopcorn.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index ba2b738c..a1ad4aab 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'IPTorrents' + class IPTorrents(MovieProvider, Base): diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py index b7bb0253..2b9b1969 100644 --- a/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'KickAssTorrents' + class KickAssTorrents(MovieProvider, Base): pass diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py index 1e994135..d1f378b6 100644 --- a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py @@ -4,6 +4,8 @@ from couchpotato.core.media.movie.providers.base import MovieProvider log = CPLog(__name__) +autoload = 'PassThePopcorn' + class PassThePopcorn(MovieProvider, Base): From d6675f3311e9a58f95ee05516356ca15778df6b0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 21:18:31 +0100 Subject: [PATCH 115/468] Use correct super class --- .../core/media/_base/providers/torrent/kickasstorrents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 8f82e263..03f0a99c 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -110,7 +110,7 @@ class Base(TorrentMagnetProvider): return tryInt(age) def isEnabled(self): - return super(KickAssTorrents, self).isEnabled() and self.getDomain() + return super(Base, self).isEnabled() and self.getDomain() def correctProxy(self, data): return 'search query' in data.lower() From e3fa695ad4a5092e177551f09d60efda9cf69fad Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 21:31:42 +0100 Subject: [PATCH 116/468] ThePirateBay don't overwrite search_url --- .../core/media/_base/providers/torrent/thepiratebay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index b0a778ce..42006d12 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -41,11 +41,11 @@ class Base(TorrentMagnetProvider): total_pages = 1 cats = self.getCatId(quality['identifier']) - search_url = self.urls['search'] % self.getDomain() + base_search_url = self.urls['search'] % self.getDomain() while page < total_pages: - search_url = search_url % self.buildUrl(media, page, cats) + search_url = base_search_url % self.buildUrl(media, page, cats) page += 1 From 742d5cbfb3ad805fd427ab0925ffb62a61949dcc Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 21:33:45 +0100 Subject: [PATCH 117/468] qbittorrent downloader working --- couchpotato/core/downloaders/qbittorrent_.py | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index c1fdc2a7..bd4154c3 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -1,4 +1,5 @@ from base64 import b16encode, b32decode +from bencode import bencode, bdecode from hashlib import sha1 import os @@ -13,7 +14,6 @@ log = CPLog(__name__) autoload = 'qBittorrent' - class qBittorrent(Downloader): protocol = ['torrent', 'torrent_magnet'] @@ -45,7 +45,6 @@ class qBittorrent(Downloader): return False - def download(self, data = None, media = None, filedata = None): if not media: media = {} if not data: data = {} @@ -59,6 +58,15 @@ class qBittorrent(Downloader): log.error('Failed sending torrent, no data') return False + + if data.get('protocol') == 'torrent_magnet': + filedata = self.magnetToTorrent(data.get('url')) + + if filedata is False: + return False + + data['protocol'] = 'torrent' + info = bdecode(filedata)["info"] torrent_hash = sha1(bencode(info)).hexdigest().upper() @@ -68,14 +76,7 @@ class qBittorrent(Downloader): # Send request to qBittorrent try: - if data.get('protocol') == 'torrent_magnet': - torrent = self.qb.add_url(filedata) - else: - torrent = self.qb.add_file(filedata) - - if not torrent: - log.error('Unable to find the torrent, did it fail to load?') - return False + self.qb.add_file(filedata) return self.downloadReturnId(torrent_hash) except Exception as e: @@ -100,18 +101,18 @@ class qBittorrent(Downloader): try: torrents = self.qb.get_torrents() - self.qb.update_general() # get extra info release_downloads = ReleaseDownloadList(self) for torrent in torrents: if torrent.hash in ids: + self.qb.update_general() # get extra info torrent_files = [] t_files = torrent.get_files() check_dir = os.path.join(torrent.save_path, torrent.name) if os.path.isdir(check_dir): - torrent.save_path = os.path.isdir(check_dir) + torrent.save_path = check_dir if len(t_files) > 1 and os.path.isdir(torrent.save_path): # multi file torrent for root, _, files in os.walk(torrent.save_path): From 0e5f89d7d68a37b2a49098332e90da4e7b469415 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Mar 2014 21:33:49 +0100 Subject: [PATCH 118/468] Use identifier to log already snatch quality --- couchpotato/core/media/movie/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index a1e915b1..515f5f91 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -192,7 +192,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('release.delete', release.get('_id'), single = True) else: - log.info('Better quality (%s) already available or snatched for %s', (quality['label'], default_title)) + log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title)) fireEvent('media.restatus', movie['_id']) break From 95295e47ab0b020a50102b611d723fdf5af69f33 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 21:50:11 +0100 Subject: [PATCH 119/468] catch vars so we dont spam log --- libs/qbittorrent/torrent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/qbittorrent/torrent.py b/libs/qbittorrent/torrent.py index a9ff51d2..3fcc4bff 100644 --- a/libs/qbittorrent/torrent.py +++ b/libs/qbittorrent/torrent.py @@ -36,6 +36,11 @@ class Torrent(Base): self.comment = None self.save_path = None + self.eta = None + self.size = None + self.dlspeed = None + self.upspeed = None + # # Commands # From a95671491dc54cb3d51f782e035291f26cfe2d27 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 22:13:59 +0100 Subject: [PATCH 120/468] added missing vars to Torrent and File class --- libs/qbittorrent/file.py | 2 ++ libs/qbittorrent/torrent.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/libs/qbittorrent/file.py b/libs/qbittorrent/file.py index 0c020572..29ba04e5 100644 --- a/libs/qbittorrent/file.py +++ b/libs/qbittorrent/file.py @@ -11,3 +11,5 @@ class File(Base): self.priority = None self.is_seed = None + + self.size = None diff --git a/libs/qbittorrent/torrent.py b/libs/qbittorrent/torrent.py index 3fcc4bff..68ec2ce0 100644 --- a/libs/qbittorrent/torrent.py +++ b/libs/qbittorrent/torrent.py @@ -40,6 +40,16 @@ class Torrent(Base): self.size = None self.dlspeed = None self.upspeed = None + self.nb_connections = None + self.share_ratio = None + self.piece_size = None + self.total_wasted = None + self.total_downloaded = None + self.total_uploaded = None + self.creation_date = None + self.time_elapsed = None + self.up_limit = None + self.dl_limit = None # # Commands From f3cd569e77de92d4f567eedc4b16c5197fbe0f62 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 22:15:37 +0100 Subject: [PATCH 121/468] fixed release_downloads, now properly returning data --- couchpotato/core/downloaders/qbittorrent_.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index bd4154c3..61f8f8b2 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -68,12 +68,13 @@ class qBittorrent(Downloader): data['protocol'] = 'torrent' info = bdecode(filedata)["info"] - torrent_hash = sha1(bencode(info)).hexdigest().upper() + torrent_hash = sha1(bencode(info)).hexdigest() # Convert base 32 to hex if len(torrent_hash) == 32: torrent_hash = b16encode(b32decode(torrent_hash)) + print torrent_hash # Send request to qBittorrent try: self.qb.add_file(filedata) @@ -106,7 +107,7 @@ class qBittorrent(Downloader): for torrent in torrents: if torrent.hash in ids: - self.qb.update_general() # get extra info + torrent.update_general() # get extra info torrent_files = [] t_files = torrent.get_files() From db163e7bd13c8b48cd375b914509d3ff89c23b50 Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sun, 16 Mar 2014 22:16:42 +0100 Subject: [PATCH 122/468] remove debug stuff --- couchpotato/core/downloaders/qbittorrent_.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index 61f8f8b2..349af176 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -74,7 +74,6 @@ class qBittorrent(Downloader): if len(torrent_hash) == 32: torrent_hash = b16encode(b32decode(torrent_hash)) - print torrent_hash # Send request to qBittorrent try: self.qb.add_file(filedata) From 63fd35a95cc4956a62e287ff3897934feb438a52 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 18 Mar 2014 22:59:01 +0100 Subject: [PATCH 123/468] Media helpers --- couchpotato/core/media/__init__.py | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 97c98393..f9b3b46c 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,7 +1,9 @@ +import os import traceback from couchpotato import get_db, CPLog from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.plugins.base import Plugin @@ -45,3 +47,53 @@ class MediaBase(Plugin): log.error('Failed creating onComplete: %s', traceback.format_exc()) return notifyFront + + def getDefaultTitle(self, info, ): + + # Set default title + default_title = toUnicode(info.get('title')) + titles = info.get('titles', []) + counter = 0 + def_title = None + for title in titles: + if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): + def_title = toUnicode(title) + break + counter += 1 + + if not def_title: + def_title = toUnicode(titles[0]) + + return def_title or 'UNKNOWN' + + def getPoster(self, image_urls, existing_files): + image_type = 'poster' + + # Remove non-existing files + file_type = 'image_%s' % image_type + + # Make existing unique + unique_files = list(set(existing_files.get(file_type, []))) + + # Remove files that can't be found + for ef in unique_files: + if not os.path.isfile(ef): + unique_files.remove(ef) + + # Replace new files list + existing_files[file_type] = unique_files + if len(existing_files) == 0: + del existing_files[file_type] + + # Loop over type + for image in image_urls.get(image_type, []): + if not isinstance(image, (str, unicode)): + continue + + if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0: + file_path = fireEvent('file.download', url = image, single = True) + if file_path: + existing_files[file_type] = [file_path] + break + else: + break From 3f0a0f552b3b32371e1151ac39ad3ed2b8780a05 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 18 Mar 2014 22:59:39 +0100 Subject: [PATCH 124/468] Keep dict keys and only make array if all are ints in request params --- couchpotato/core/helpers/request.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 88bd7a0c..4cad8848 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -2,7 +2,7 @@ from urllib import unquote import re from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import natsortKey +from couchpotato.core.helpers.variable import natsortKey, tryInt def getParams(params): @@ -43,7 +43,7 @@ def getParams(params): return dictToList(temp) - +non_decimal = re.compile(r'[^\d.]+') def dictToList(params): if type(params) is dict: @@ -53,7 +53,15 @@ def dictToList(params): convert = lambda text: int(text) if text.isdigit() else text.lower() alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] sorted_keys = sorted(value.keys(), key = alphanum_key) - new_value = [dictToList(value[k]) for k in sorted_keys] + + all_ints = 0 + for pnr in sorted_keys: + all_ints += 1 if non_decimal.sub('', pnr) == pnr else 0 + + if all_ints == len(sorted_keys): + new_value = [dictToList(value[k]) for k in sorted_keys] + else: + new_value = value except: new_value = value From 5a0a5ad83b6016068eab4122144a7bf78103e86e Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 19 Mar 2014 15:45:20 +1030 Subject: [PATCH 125/468] only compare filesize as int. --- couchpotato/core/media/movie/searcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 515f5f91..58874443 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -233,12 +233,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # File to small - if nzb['size'] and preferred_quality['size_min'] > nzb['size']: + if nzb['size'] and int(preferred_quality['size_min']) > int(nzb['size']): log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) return False # File to large - if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: + if nzb['size'] and int(preferred_quality['size_max']) < int(nzb['size']): log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) return False From c6016a25df42db1e1f3d4983ff77bbee7d3f8911 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 09:22:51 +0100 Subject: [PATCH 126/468] Destroy index and re-add on updated version --- couchpotato/core/database.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 6bbdb959..76c4a417 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -42,17 +42,21 @@ class Database(object): db = self.getDB() # Category index + index_instance = klass(db.path, index_name) try: - db.add_index(klass(db.path, index_name)) + db.add_index(index_instance) db.reindex_index(index_name) except: - previous_version = db.indexes_names[index_name]._version + previous = db.indexes_names[index_name] + previous_version = previous._version current_version = klass._version # Only edit index if versions are different if previous_version < current_version: log.debug('Index "%s" already exists, updating and reindexing', index_name) - db.edit_index(klass(db.path, index_name), reindex = True) + db.destroy_index(previous) + db.add_index(index_instance) + db.reindex_index(index_name) def deleteDocument(self, **kwargs): From 8c5e0cf0a7473c0c1f3307b3565b7f3f3d0d9f67 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 17:55:21 +0100 Subject: [PATCH 127/468] Make media index multi identifier based --- couchpotato/core/media/_base/media/index.py | 34 ++++++++++++++++----- couchpotato/core/media/_base/media/main.py | 6 ++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index dc5bab6f..5065871f 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -1,30 +1,48 @@ from string import ascii_letters from hashlib import md5 -from CodernityDB.hash_index import HashIndex from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex from couchpotato.core.helpers.encoding import toUnicode, simplifyString -class MediaIMDBIndex(HashIndex): - _version = 1 +class MediaIndex(MultiTreeBasedIndex): + _version = 2 + + custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" def __init__(self, *args, **kwargs): - kwargs['key_format'] = 'I' - super(MediaIMDBIndex, self).__init__(*args, **kwargs) + kwargs['key_format'] = '32s' + super(MediaIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return int(key.strip('t')) + return md5(key).hexdigest() def make_key_value(self, data): - if data.get('_t') == 'media' and data.get('identifier'): - return int(data['identifier'].strip('t')), None + if data.get('_t') == 'media' and (data.get('identifier') or data.get('identifiers')): + + identifiers = data.get('identifiers', {}) + if data.get('identifier') and 'imdb' not in identifiers: + identifiers['imdb'] = data.get('identifier') + + ids = [] + for x in identifiers: + ids.append(md5('%s-%s' % (x, data['identifiers'][x])).hexdigest()) + + return ids, None def run_to_dict(self, db, media_id, dict_dept = None): if not dict_dept: dict_dept = {} return db.get('id', media_id) + def run_identifiers(self, db, identifiers, with_doc = False): + for x in identifiers: + try: + media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) + return media + except: + pass + def run_with_status(self, db, status = [], with_doc = True): status = list(status if isinstance(status, (list, tuple)) else [status]) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index a3d99170..5413c0fc 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -8,18 +8,16 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIMDBIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex +from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex log = CPLog(__name__) -autoload = 'MediaPlugin' - class MediaPlugin(MediaBase): _database = { - 'media': MediaIMDBIndex, + 'media': MediaIndex, 'media_search_title': TitleSearchIndex, 'media_status': MediaStatusIndex, 'media_by_type': MediaTypeIndex, From 445724573dd206a24e999a51e50712f53f816649 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 18:02:45 +0100 Subject: [PATCH 128/468] Make sure movie is added with multi identifier --- couchpotato/core/media/_base/media/main.py | 2 +- couchpotato/core/media/movie/_base/main.py | 8 +++++--- couchpotato/core/media/movie/providers/info/_modifier.py | 2 +- couchpotato/core/plugins/release/main.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5413c0fc..85047e9a 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -125,7 +125,7 @@ class MediaPlugin(MediaBase): imdb_id = getImdb(str(media_id)) if imdb_id: - m = db.get('media', imdb_id, with_doc = True)['doc'] + m = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] else: m = db.get('id', media_id) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index b2234cfc..d8060d71 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -101,7 +101,9 @@ class MovieBase(MovieTypeBase): '_t': 'media', 'type': 'movie', 'title': def_title, - 'identifier': params.get('identifier'), + 'identifiers': { + 'imdb': params.get('identifier') + }, 'status': status if status else 'active', 'profile_id': params.get('profile_id', default_profile.get('_id')), 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None, @@ -116,7 +118,7 @@ class MovieBase(MovieTypeBase): new = False try: - m = db.get('media', params.get('identifier'), with_doc = True)['doc'] + m = db.get('media', 'imdb-%s' % params.get('identifier'), with_doc = True)['doc'] except: new = True m = db.insert(media) @@ -262,7 +264,7 @@ class MovieBase(MovieTypeBase): if media_id: media = db.get('id', media_id) else: - media = db.get('media', identifier, with_doc = True)['doc'] + media = db.get('media', 'imdb-%s' % identifier, with_doc = True)['doc'] info = fireEvent('movie.info', merge = True, extended = extended, identifier = media.get('identifier')) diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index d9a15e8c..131cc058 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -95,7 +95,7 @@ class MovieResultModifier(Plugin): media = None try: - media = db.get('media', imdb, with_doc = True)['doc'] + media = db.get('media', 'imdb-%s' % imdb, with_doc = True)['doc'] except RecordNotFound: pass diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index b749ee61..40475a4d 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -92,7 +92,7 @@ class Release(Plugin): # Add movie if it doesn't exist try: - media = db.get('media', group['identifier'], with_doc = True)['doc'] + media = db.get('media', 'imdb-%s' % group['identifier'], with_doc = True)['doc'] except: media = fireEvent('movie.add', params = { 'identifier': group['identifier'], From b347f761a71221aff8280e13cfe8d50102cdb2f6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 18:13:55 +0100 Subject: [PATCH 129/468] Ignore faulty category tables They don't have any categories anyway, so might aswell ignore the error. --- couchpotato/core/database.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 76c4a417..3aa1bb7c 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -159,7 +159,15 @@ class Database(object): for ml in migrate_list: migrate_data[ml] = {} rows = migrate_list[ml] - c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml)) + + try: + c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml)) + except: + # ignore faulty destination_id database + if ml == 'category': + migrate_data[ml] = {} + else: + raise for p in c.fetchall(): columns = {} From 5b2dfffe0f858fb2eac4299842968732821b102c Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 22:09:27 +0100 Subject: [PATCH 130/468] Use correct year key --- couchpotato/core/media/movie/providers/nzb/nzbindex.py | 2 +- couchpotato/core/media/movie/providers/torrent/bitsoup.py | 2 +- couchpotato/core/media/movie/providers/torrent/iptorrents.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex.py b/couchpotato/core/media/movie/providers/nzb/nzbindex.py index 0b5b2b43..70e939dc 100644 --- a/couchpotato/core/media/movie/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/movie/providers/nzb/nzbindex.py @@ -14,7 +14,7 @@ class NzbIndex(MovieProvider, Base): def buildUrl(self, media, quality): title = fireEvent('library.query', media, include_year = False, single = True) - year = media['year'] + year = media['info']['year'] query = tryUrlencode({ 'q': '"%s %s" | "%s (%s)"' % (title, year, title, year), diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py index 578de69d..b3ecf2e7 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -21,7 +21,7 @@ class Bitsoup(MovieProvider, Base): query = tryUrlencode({ 'search': '"%s" %s' % ( fireEvent('library.query', media, include_year = False, single = True), - media['year'] + media['info']['year'] ), 'cat': self.getCatId(quality['identifier'])[0], }) diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index a1ad4aab..4f9d6517 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -17,6 +17,6 @@ class IPTorrents(MovieProvider, Base): ] def buildUrl(self, title, media, quality): - query = '%s %s' % (title.replace(':', ''), media['year']) + query = '%s %s' % (title.replace(':', ''), media['info']['year']) return self._buildUrl(query, quality['identifier']) From f4e373447eb07f167ae72c47085216a1ad8c6896 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 22:37:44 +0100 Subject: [PATCH 131/468] File not properly send to Sabnzbd --- couchpotato/core/helpers/variable.py | 2 +- couchpotato/core/plugins/base.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 5eab960c..96faefcf 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -293,7 +293,7 @@ def isSubFolder(sub_folder, base_folder): return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep) # From SABNZBD -re_password = [re.compile(r'([^/\\]+)[/\\](.+)'), re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)] +re_password = [re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)] def scanForPassword(name): m = None for reg in re_password: diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 7d52e1eb..08f8efd4 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -7,7 +7,6 @@ import time import traceback import urllib2 -from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toSafeString, \ toUnicode, sp @@ -332,7 +331,7 @@ class Plugin(object): return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag) def createFileName(self, data, filedata, media): - name = sp(os.path.join(self.createNzbName(data, media))) + name = self.createNzbName(data, media) if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata: return '%s.%s' % (name, 'rar') return '%s.%s' % (name, data.get('protocol')) From a2d22b6feb87fa42937631d2ea1c4eda0874dbb0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 22:46:14 +0100 Subject: [PATCH 132/468] Set cleanup interval --- couchpotato/core/plugins/file.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py index 77b3ba81..b2533c07 100644 --- a/couchpotato/core/plugins/file.py +++ b/couchpotato/core/plugins/file.py @@ -4,7 +4,7 @@ import traceback from couchpotato import get_db from couchpotato.api import addApiView -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.logger import CPLog @@ -32,12 +32,11 @@ class FileManager(Plugin): 'return': {'type': 'file'} }) - addEvent('app.load', self.cleanup) + fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24) def cleanup(self): # Wait a bit after starting before cleanup - time.sleep(2) log.debug('Cleaning up unused files') try: From be46ed12ac8ccedc885ff111fe30480e77f870e2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 23:06:27 +0100 Subject: [PATCH 133/468] get identifier helper --- couchpotato/core/helpers/variable.py | 8 +++-- couchpotato/core/media/_base/media/main.py | 5 +--- .../providers/torrent/kickasstorrents.py | 4 +-- .../_base/providers/torrent/passthepopcorn.py | 4 +-- couchpotato/core/media/movie/_base/main.py | 29 ++++++++++--------- .../media/movie/providers/nzb/binsearch.py | 3 +- .../core/media/movie/providers/nzb/newznab.py | 3 +- .../movie/providers/torrent/torrentpotato.py | 3 +- couchpotato/core/media/movie/searcher.py | 8 ++--- couchpotato/core/plugins/base.py | 5 ++-- couchpotato/core/plugins/renamer.py | 4 +-- couchpotato/core/plugins/scanner.py | 4 +-- 12 files changed, 43 insertions(+), 37 deletions(-) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 96faefcf..34e66e94 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -227,6 +227,10 @@ def toIterable(value): return [value] +def getIdentifier(media): + return media.get('identifier') or media.get('identifiers', {}).get('imdb') + + def getTitle(media_dict): try: try: @@ -241,10 +245,10 @@ def getTitle(media_dict): try: return media_dict['media']['info']['titles'][0] except: - log.error('Could not get title for %s', media_dict.get('identifier')) + log.error('Could not get title for %s', getIdentifier(media_dict)) return None - log.error('Could not get title for %s', media_dict['identifier']) + log.error('Could not get title for %s', getIdentifier(media_dict)) return None except: log.error('Could not get title for library item: %s', media_dict) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 85047e9a..c09d4168 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -99,13 +99,10 @@ class MediaPlugin(MediaBase): try: media = get_db().get('id', media_id) - - - default_title = getTitle(media) event = '%s.update_info' % media.get('type') def handler(): - fireEvent(event, identifier = media.get('identifier'), default_title = default_title, on_complete = self.createOnComplete(media_id)) + fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id)) if handler: return handler diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 03f0a99c..b347ccbe 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -2,7 +2,7 @@ import re import traceback from bs4 import BeautifulSoup -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider @@ -38,7 +38,7 @@ class Base(TorrentMagnetProvider): def _search(self, media, quality, results): - data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', media['identifier'].replace('tt', ''))) + data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', getIdentifier(media).replace('tt', ''))) if data: diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index 56550d2b..5368ad35 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -5,7 +5,7 @@ import time import traceback from couchpotato.core.helpers.encoding import tryUrlencode -from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts +from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from dateutil.parser import parse @@ -36,7 +36,7 @@ class Base(TorrentProvider): params = mergeDicts(self.quality_search_params[quality_id].copy(), { 'order_by': 'relevance', 'order_way': 'descending', - 'searchstr': media['identifier'] + 'searchstr': getIdentifier(media) }) url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index d8060d71..932c8b29 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -6,7 +6,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import splitString, getTitle, getImdb +from couchpotato.core.helpers.variable import splitString, getTitle, getImdb, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase import six @@ -266,7 +266,7 @@ class MovieBase(MovieTypeBase): else: media = db.get('media', 'imdb-%s' % identifier, with_doc = True)['doc'] - info = fireEvent('movie.info', merge = True, extended = extended, identifier = media.get('identifier')) + info = fireEvent('movie.info', merge = True, extended = extended, identifier = getIdentifier(media)) # Don't need those here try: del info['in_wanted'] @@ -275,7 +275,7 @@ class MovieBase(MovieTypeBase): except: pass if not info or len(info) == 0: - log.error('Could not update, no movie info to work with: %s', media.get('identifier')) + log.error('Could not update, no movie info to work with: %s', identifier) return False # Update basic info @@ -285,19 +285,20 @@ class MovieBase(MovieTypeBase): log.debug('Adding titles: %s', titles) # Define default title - def_title = None if default_title: - counter = 0 - for title in titles: - if title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): - def_title = toUnicode(title) - break - counter += 1 + def_title = None + if default_title: + counter = 0 + for title in titles: + if title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title): + def_title = toUnicode(title) + break + counter += 1 - if not def_title: - def_title = toUnicode(titles[0]) + if not def_title: + def_title = toUnicode(titles[0]) - media['title'] = def_title + media['title'] = def_title # Files images = info.get('images', []) @@ -357,7 +358,7 @@ class MovieBase(MovieTypeBase): dates = media.get('info').get('release_date') if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: - dates = fireEvent('movie.info.release_date', identifier = media['identifier'], merge = True) + dates = fireEvent('movie.info.release_date', identifier = getIdentifier(media), merge = True) media['info'].update({'release_date': dates}) db.update(media) diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch.py b/couchpotato/core/media/movie/providers/nzb/binsearch.py index f4c16c4c..d6f48522 100644 --- a/couchpotato/core/media/movie/providers/nzb/binsearch.py +++ b/couchpotato/core/media/movie/providers/nzb/binsearch.py @@ -1,4 +1,5 @@ from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.binsearch import Base from couchpotato.core.media.movie.providers.base import MovieProvider @@ -13,7 +14,7 @@ class BinSearch(MovieProvider, Base): def buildUrl(self, media, quality): query = tryUrlencode({ - 'q': media['identifier'], + 'q': getIdentifier(media), 'm': 'n', 'max': 400, 'adv_age': Env.setting('retention', 'nzb'), diff --git a/couchpotato/core/media/movie/providers/nzb/newznab.py b/couchpotato/core/media/movie/providers/nzb/newznab.py index dd4d94c0..9783f8d5 100644 --- a/couchpotato/core/media/movie/providers/nzb/newznab.py +++ b/couchpotato/core/media/movie/providers/nzb/newznab.py @@ -1,4 +1,5 @@ from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.newznab import Base from couchpotato.core.media.movie.providers.base import MovieProvider @@ -13,7 +14,7 @@ class Newznab(MovieProvider, Base): def buildUrl(self, media, api_key): query = tryUrlencode({ 't': 'movie', - 'imdbid': media['identifier'].replace('tt', ''), + 'imdbid': getIdentifier(media).replace('tt', ''), 'apikey': api_key, 'extended': 1 }) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py index 9c992071..67573537 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py @@ -1,4 +1,5 @@ from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.torrentpotato import Base from couchpotato.core.media.movie.providers.base import MovieProvider @@ -14,6 +15,6 @@ class TorrentPotato(MovieProvider, Base): arguments = tryUrlencode({ 'user': host['name'], 'passkey': host['pass_key'], - 'imdbid': media['identifier'] + 'imdbid': getIdentifier(media), }) return '%s?%s' % (host['host'], arguments) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 515f5f91..d217c656 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -8,7 +8,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString -from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb +from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media.movie import MovieTypeBase @@ -95,10 +95,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): try: self.single(media, search_protocols) except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (media['identifier'], traceback.format_exc())) + log.error('Forcing library update for %s, if you see this often, please report: %s', (getIdentifier(media), traceback.format_exc())) fireEvent('movie.update_info', media_id) except: - log.error('Search failed for %s: %s', (media['identifier'], traceback.format_exc())) + log.error('Search failed for %s: %s', (getIdentifier(media), traceback.format_exc())) self.in_progress['to_go'] -= 1 @@ -257,7 +257,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return True # Check if nzb contains imdb link - if getImdb(nzb.get('description', '')) == media['identifier']: + if getImdb(nzb.get('description', '')) == getIdentifier(media): return True for raw_title in media['info']['titles']: diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 08f8efd4..1e4bfeba 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -10,7 +10,7 @@ import urllib2 from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toSafeString, \ toUnicode, sp -from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt +from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.environment import Env import requests @@ -338,7 +338,8 @@ class Plugin(object): def cpTag(self, media): if Env.setting('enabled', 'renamer'): - return '.cp(' + media.get('identifier') + ')' if media.get('identifier') else '' + identifier = getIdentifier(media) + return '.cp(' + identifier + ')' if identifier else '' return '' diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 0f531bc6..4cfc9875 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -10,7 +10,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss, sp from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ - getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder + getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -1043,7 +1043,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) if rls: media = db.get('id', rls['media_id']) release_download.update({ - 'imdb_id': media['identifier'], + 'imdb_id': getIdentifier(media), 'quality': rls['quality'], 'protocol': rls.get('info', {}).get('protocol') or rls.get('info', {}).get('type'), 'release_id': rls['_id'], diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 0066c368..65ae257a 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -8,7 +8,7 @@ from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \ - splitString + splitString, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from enzyme.exceptions import NoParserError, ParseError @@ -403,7 +403,7 @@ class Scanner(Plugin): if not group['media']: log.error('Unable to determine media: %s', group['identifiers']) else: - group['identifier'] = group['media'].get('identifier') or group['media']['info'].get('imdb') + group['identifier'] = getIdentifier(group['media']) or group['media']['info'].get('imdb') processed_movies[identifier] = group From 8316b5cb29109c9d3c2ad892e743bc64cffe092d Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 23:24:16 +0100 Subject: [PATCH 134/468] Key identifiers missing --- couchpotato/core/media/_base/media/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 5065871f..069cb639 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -6,7 +6,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString class MediaIndex(MultiTreeBasedIndex): - _version = 2 + _version = 3 custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" @@ -26,7 +26,7 @@ class MediaIndex(MultiTreeBasedIndex): ids = [] for x in identifiers: - ids.append(md5('%s-%s' % (x, data['identifiers'][x])).hexdigest()) + ids.append(md5('%s-%s' % (x, identifiers[x])).hexdigest()) return ids, None From e314c605f117eb2c5eda0b8c8c8fe502221a67e6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 19 Mar 2014 23:28:12 +0100 Subject: [PATCH 135/468] Missing identifier key --- couchpotato/core/plugins/manage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index e1e084f1..990bc900 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -8,7 +8,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.helpers.encoding import sp -from couchpotato.core.helpers.variable import splitString, getTitle, tryInt +from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -123,7 +123,7 @@ class Manage(Plugin): total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) for done_movie in done_movies: - if done_movie['identifier'] not in added_identifiers: + if getIdentifier(done_movie) not in added_identifiers: fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all') else: From 21c9d7fcc39deee77ab62053287a9d67448f73d1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 20 Mar 2014 16:53:27 +0100 Subject: [PATCH 136/468] Use identifier helper --- couchpotato/core/media/_base/providers/torrent/hdbits.py | 4 ++-- couchpotato/core/media/_base/providers/torrent/yify.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index 73bfc169..fa45b83f 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -2,7 +2,7 @@ import re import json import traceback -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -44,7 +44,7 @@ class Base(TorrentProvider): def _search(self, movie, quality, results): - match = re.match(r'tt(\d{7})', movie['identifier']) + match = re.match(r'tt(\d{7})', getIdentifier(movie)) data = self._post_query(imdb = {'id': match.group(1)}) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 9380e651..fe7a9b40 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -1,6 +1,6 @@ import traceback -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -35,7 +35,7 @@ class Base(TorrentProvider): def _search(self, movie, quality, results): - search_url = self.urls['search'] % (self.getDomain(), movie['identifier'], quality['identifier']) + search_url = self.urls['search'] % (self.getDomain(), getIdentifier(movie), quality['identifier']) data = self.getJsonData(search_url) From 00419910b49056d5f2a0e8a21fcf034574f8f300 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 20 Mar 2014 16:54:54 +0100 Subject: [PATCH 137/468] Use getIdentifier in suggestions --- couchpotato/core/media/movie/suggestion/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index 62b9f57d..e9b071d3 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -1,7 +1,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent -from couchpotato.core.helpers.variable import splitString, removeDuplicate +from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -87,7 +87,7 @@ class Suggestion(Plugin): if len(new_suggestions) - 1 < limit: db = get_db() active_movies = db.run('media', 'with_status', ['active', 'done']) - movies = [x['identifier'] for x in active_movies] + movies = [getIdentifier(x) for x in active_movies] movies.extend(seen) ignored.extend([x.get('imdb') for x in cached_suggestion]) From ff94bd6a9017dde07c043cc1498a68333612c3bc Mon Sep 17 00:00:00 2001 From: Friso Abcouwer Date: Thu, 20 Mar 2014 20:17:13 +0100 Subject: [PATCH 138/468] Rewrite contributing.md --- contributing.md | 51 ++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/contributing.md b/contributing.md index d5db0b42..0d82d71c 100644 --- a/contributing.md +++ b/contributing.md @@ -1,25 +1,36 @@ -## Got a issue/feature request or submitting a pull request? +# Contributing to CouchPotatoServer -Make sure you think of the following things: +1. [Contributing](#contributing) +2. [Submitting an Issue](#issues) +3. [Submitting a Pull Request](#pull-requests) -## Issue - * Search through the existing (and closed) issues first, see if you can get your answer there. - * Double check the result manually, because it could be an external issue. - * Post logs! Without seeing what is going on, I can't reproduce the error. - * Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP. - * What is the movie + quality you are searching for? - * What are you're settings for the specific problem? - * What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby) - * Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs! +## Contributing +Thank you for your interest in contributing to CouchPotato. There are several ways to help out, even if you've never worked on an open source project before. +If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/RuudBurger/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first! +If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request. +Lastly, for anything related to CouchPotato, feel free to stop by the [forum](http://couchpota.to/forum/) or the [#couchpotato](http://webchat.freenode.net/?channels=couchpotato) IRC channel at irc.freenode.net. + +## Issues +Issues are intended for reporting bugs and weird behaviour or suggesting improvements to CouchPotatoServer. +Before you submit an issue, please go through the following checklist: + * Search through existing issues (*including closed issues!*) first: you might be able to get your answer there. + * Double check your issue manually, because it could be an external issue. + * Post logs with your issue: Without seeing what is going on, the developers can't reproduce the error. + * Check the logs yourself before submitting them. Obvious errors like permission or HTTP errors are often not related to CouchPotato. + * What movie and quality are you searching for? + * What are your settings for the specific problem? + * What providers are you using? (While your logs include these, scanning through hundreds of lines of logs isn't our hobby) + * Post the logs from the *config* directory, please do not copy paste the UI. Use pastebin to store these logs! * Give a short step by step of how to reproduce the error. - * What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example. - * I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;) - * If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!! + * What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed then when you use CP on OSX or Windows. + * Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag. + * If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!! + +The more relevant information you can provide, the more likely it is the issue will be resolved rather than closed. -## Pull Request - * Make sure you're pull request is made for develop branch (or relevant feature branch) +## Pull Requests +Pull requests are intended for contributing code or documentation to the project. Before you submit a pull request, consider the following: + * Make sure your pull request is made for the *develop* branch (or relevant feature branch). * Have you tested your PR? If not, why? - * Are there any limitations of your PR we should know of? - * Make sure to keep you're PR up-to-date with the branch you're trying to push into. - -**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)** + * Does your PR have any limitations we should know of? + * Is your PR up-to-date with the branch you're trying to push into? From 384a2e0e152dd06990e69909fed13a43db933b20 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 11:06:25 +0100 Subject: [PATCH 139/468] Suggestions not showing --- couchpotato/core/media/movie/suggestion/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index e9b071d3..1c6e95f4 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -30,7 +30,7 @@ class Suggestion(Plugin): if not movies or len(movies) == 0: db = get_db() active_movies = db.run('media', 'with_status', ['active', 'done']) - movies = [x['identifier'] for x in active_movies] + movies = [getIdentifier(x) for x in active_movies] if not ignored or len(ignored) == 0: ignored = splitString(Env.prop('suggest_ignore', default = '')) From 41e94e1e22236a1a161c754b27f1ff087f5c846e Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 12:15:46 +0100 Subject: [PATCH 140/468] Make sure media dict has category key --- couchpotato/core/media/__init__.py | 2 +- couchpotato/core/media/_base/media/index.py | 5 ----- couchpotato/core/media/_base/media/main.py | 17 ++++++++++------- couchpotato/core/media/movie/_base/main.py | 4 ++-- couchpotato/core/media/movie/searcher.py | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index f9b3b46c..47997662 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -25,7 +25,7 @@ class MediaBase(Plugin): def onComplete(): try: db = get_db() - media = db.get('id', media_id) + media = fireEvent('media.get', media_id, single = True) event_name = '%s.searcher.single' % media.get('type') fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id)) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 069cb639..808eb124 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -30,11 +30,6 @@ class MediaIndex(MultiTreeBasedIndex): return ids, None - def run_to_dict(self, db, media_id, dict_dept = None): - if not dict_dept: dict_dept = {} - - return db.get('id', media_id) - def run_identifiers(self, db, identifiers, with_doc = False): for x in identifiers: try: diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index c09d4168..de8a703e 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -121,16 +121,19 @@ class MediaPlugin(MediaBase): imdb_id = getImdb(str(media_id)) + media = None if imdb_id: - m = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] + media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] else: - m = db.get('id', media_id) + media = db.get('id', media_id) - results = None - if m: - results = db.run('media', 'to_dict', m['_id']) + if media: - return results + # Attach category + if media.get('category_id'): + media['category'] = db.get('id', media.get('category_id')) + + return media def getView(self, id = None, **kwargs): @@ -217,7 +220,7 @@ class MediaPlugin(MediaBase): offset -= 1 continue - media = db.run('media', 'to_dict', media_id) + media = fireEvent('media.get', media_id, single = True) media['releases'] = list(db.run('release', 'for_media', media_id)) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 932c8b29..499dba9c 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -168,7 +168,7 @@ class MovieBase(MovieTypeBase): if rel['status'] is 'available': db.delete(rel) - movie_dict = db.run('media', 'to_dict', m['_id']) + movie_dict = fireEvent('media.get', m['_id'], single = True) if do_search and search_after: onComplete = self.createOnComplete(m['_id']) @@ -229,7 +229,7 @@ class MovieBase(MovieTypeBase): m = db.get('id', media_id) - movie_dict = db.run('media', 'to_dict', m['_id']) + movie_dict = fireEvent('media.get', m['_id'], single = True) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) except: diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index d217c656..6a426315 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -90,7 +90,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): for media_id in medias: - media = db.run('media', 'to_dict', media_id) + media = fireEvent('media.get', media_id, single = True) try: self.single(media, search_protocols) @@ -335,7 +335,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): rel['status'] = 'ignored' db.update(rel) - movie_dict = db.run('media', 'to_dict', media_id) + movie_dict = fireEvent('media.get', media_id, single = True) log.info('Trying next release for: %s', getTitle(movie_dict)) self.single(movie_dict, manual = manual) From 6243ed3bd5746e3c8fa70cadeb5eb8994347cc4d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 12:30:44 +0100 Subject: [PATCH 141/468] Add destination support to Synology downloader --- couchpotato/core/downloaders/synology.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/synology.py b/couchpotato/core/downloaders/synology.py index 46d889fd..f7145dea 100644 --- a/couchpotato/core/downloaders/synology.py +++ b/couchpotato/core/downloaders/synology.py @@ -33,7 +33,7 @@ class Synology(Downloader): try: # Send request to Synology - srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) + srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'), self.conf('destination')) if data['protocol'] == 'torrent_magnet': log.info('Adding torrent URL %s', data['url']) response = srpc.create_task(url = data['url']) @@ -84,7 +84,7 @@ class SynologyRPC(object): """SynologyRPC lite library""" - def __init__(self, host = 'localhost', port = 5000, username = None, password = None): + def __init__(self, host = 'localhost', port = 5000, username = None, password = None, destination = None): super(SynologyRPC, self).__init__() @@ -92,6 +92,7 @@ class SynologyRPC(object): self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port) self.username = username self.password = password + self.destination = destination self.session_name = 'DownloadStation' def _login(self): @@ -144,6 +145,10 @@ class SynologyRPC(object): 'version': '1', 'method': 'create', '_sid': self.sid} + + if self.destination and len(self.destination) > 0: + args['destination'] = self.destination + if url: log.info('Login success, adding torrent URI') args['uri'] = url @@ -195,6 +200,11 @@ config = [{ 'name': 'password', 'type': 'password', }, + { + 'name': 'destination', + 'description': 'Specify existing destination share to where your files will be downloaded, usually Downloads', + 'advanced': True, + }, { 'name': 'use_for', 'label': 'Use for', From 1224b98745711a0f95e2e88fe175f05291e52128 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 13:15:35 +0100 Subject: [PATCH 142/468] Add releases to media.get. re-use where possible --- couchpotato/core/media/_base/media/main.py | 4 ++-- couchpotato/core/media/movie/searcher.py | 5 ++--- couchpotato/core/plugins/manage.py | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index de8a703e..06bbf671 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -133,6 +133,8 @@ class MediaPlugin(MediaBase): if media.get('category_id'): media['category'] = db.get('id', media.get('category_id')) + media['releases'] = list(db.run('release', 'for_media', media['_id'])) + return media def getView(self, id = None, **kwargs): @@ -222,8 +224,6 @@ class MediaPlugin(MediaBase): media = fireEvent('media.get', media_id, single = True) - media['releases'] = list(db.run('release', 'for_media', media_id)) - # Merge releases with movie dict medias.append(media) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 6a426315..84ef50ee 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -142,7 +142,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): profile = db.get('id', movie['profile_id']) quality_order = fireEvent('quality.order', single = True) - media_releases = db.run('release', 'for_media', movie['_id']) ret = False @@ -161,7 +160,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): has_better_quality = 0 # See if better quality is available - for release in media_releases: + for release in movie.get('releases', []): if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: has_better_quality += 1 @@ -187,7 +186,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): ret = True # Remove releases that aren't found anymore - for release in db.run('release', 'for_media', movie['_id']): + for release in movie.get('releases', []): if release.get('status') == 'available' and release.get('identifier') not in found_releases: fireEvent('release.delete', release.get('_id'), single = True) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 990bc900..623ce6f4 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -127,8 +127,7 @@ class Manage(Plugin): fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all') else: - db = get_db() - releases = list(db.run('release', 'for_media', done_movie.get('_id'))) + releases = done_movie.get('releases', []) for release in releases: if release.get('files'): From f734e27d238feab7da218189a094caf9565508c4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 13:22:16 +0100 Subject: [PATCH 143/468] Make sure to safe size as int --- couchpotato/core/media/movie/searcher.py | 6 +++--- couchpotato/core/plugins/quality/main.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 4205054a..e479d2af 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -8,7 +8,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString -from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier +from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media.movie import MovieTypeBase @@ -232,12 +232,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # File to small - if nzb['size'] and int(preferred_quality['size_min']) > int(nzb['size']): + if nzb['size'] and tryInt(preferred_quality['size_min']) > tryInt(nzb['size']): log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) return False # File to large - if nzb['size'] and int(preferred_quality['size_max']) < int(nzb['size']): + if nzb['size'] and tryInt(preferred_quality['size_max']) < tryInt(nzb['size']): log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) return False diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index ee06d4c5..e64b5035 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -5,7 +5,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.variable import mergeDicts, getExt +from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.quality.index import QualityIndex @@ -121,7 +121,7 @@ class QualityPlugin(Plugin): quality = db.get('quality', kwargs.get('identifier'), with_doc = True) if quality: - quality['doc'][kwargs.get('value_type')] = kwargs.get('value') + quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value')) db.update(quality['doc']) self.cached_qualities = None @@ -148,8 +148,8 @@ class QualityPlugin(Plugin): '_t': 'quality', 'order': order, 'identifier': q.get('identifier'), - 'size_min': q.get('size')[0], - 'size_max': q.get('size')[1] + 'size_min': tryInt(q.get('size')[0]), + 'size_max': tryInt(q.get('size')[1]), }) log.info('Creating profile: %s', q.get('label')) From a8159c9e55588c67c498a2d6edfd15852bd05830 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 13:23:52 +0100 Subject: [PATCH 144/468] Make sure quality sizes are int on migrate --- couchpotato/core/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 3aa1bb7c..19fde904 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -279,8 +279,8 @@ class Database(object): quality = db.get('id', q_id) quality['order'] = q.get('order') - quality['size_min'] = q.get('size_min') - quality['size_max'] = q.get('size_max') + quality['size_min'] = tryInt(q.get('size_min')) + quality['size_max'] = tryInt(q.get('size_max')) db.update(quality) quality_link[x] = quality From 6115f83a0982263b760c1c74211c1b5e44285a94 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 13:29:34 +0100 Subject: [PATCH 145/468] Update TPB proxies --- .../core/media/_base/providers/torrent/thepiratebay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 42006d12..4d819619 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -29,9 +29,9 @@ class Base(TorrentMagnetProvider): 'http://pirateproxy.ca', 'http://tpb.al', 'http://www.tpb.gr', - 'http://nl.tpb.li', + 'http://bayproxy.me', 'http://proxybay.eu', - 'https://www.getpirate.com', + 'http://www.getpirate.com', 'http://piratebay.io', ] From 37bc54e01e91b4f6e7721fa9e1f9127f14e84792 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 13:32:36 +0100 Subject: [PATCH 146/468] Add title to boxcar2 message closes #2977 --- couchpotato/core/notifications/boxcar2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/boxcar2.py b/couchpotato/core/notifications/boxcar2.py index 3e19c19b..04ce4f38 100644 --- a/couchpotato/core/notifications/boxcar2.py +++ b/couchpotato/core/notifications/boxcar2.py @@ -25,7 +25,7 @@ class Boxcar2(Notification): data = { 'user_credentials': self.conf('token'), - 'notification[title]': toUnicode(message), + 'notification[title]': toUnicode('%s - %s' % (self.default_title, message)), 'notification[long_message]': toUnicode(long_message), } From 27e4800ed222039e7f7c21bb2b8b298499af6384 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 14:31:49 +0100 Subject: [PATCH 147/468] try next release, use media_id --- couchpotato/core/media/movie/_base/static/movie.actions.js | 2 +- couchpotato/core/media/movie/searcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index cb4be00c..5dcaed78 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -428,7 +428,7 @@ MA.Release = new Class({ Api.request('movie.searcher.try_next', { 'data': { - 'id': self.movie.get('_id') + 'media_id': self.movie.get('_id') } }); diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index e479d2af..5a95629d 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -38,7 +38,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = { 'desc': 'Marks the snatched results as ignored and try the next best release', 'params': { - 'id': {'desc': 'The id of the movie'}, + 'media_id': {'desc': 'The id of the media'}, }, }) From 8bdbf8df2e60faf1bdbe398b2aa1d45772fe4115 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Tue, 11 Mar 2014 00:29:56 +1300 Subject: [PATCH 148/468] Updated rtorrent-python library - Fixed bug with basic auth on secure connections - Added 'test_connection' method to RTorrent class - Minor adjustment to authorization encoding --- libs/rtorrent/__init__.py | 7 +++-- libs/rtorrent/lib/xmlrpc/basic_auth.py | 38 ++++++++++++++++++++------ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py index d70e6e10..a3f16078 100755 --- a/libs/rtorrent/__init__.py +++ b/libs/rtorrent/__init__.py @@ -89,13 +89,16 @@ class RTorrent: def _get_conn(self): """Get ServerProxy instance""" - if self.username is not None and self.password is not None: + + if self.username and self.password: if self.scheme == 'scgi': raise NotImplementedError() + secure = self.scheme == 'https' + return self.sp( self.uri, - transport=BasicAuthTransport(self.username, self.password), + transport=BasicAuthTransport(secure, self.username, self.password), **self.sp_kwargs ) diff --git a/libs/rtorrent/lib/xmlrpc/basic_auth.py b/libs/rtorrent/lib/xmlrpc/basic_auth.py index 20c02d9a..c5654a21 100644 --- a/libs/rtorrent/lib/xmlrpc/basic_auth.py +++ b/libs/rtorrent/lib/xmlrpc/basic_auth.py @@ -20,24 +20,46 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from base64 import encodestring -import string +from base64 import b64encode +import httplib import xmlrpclib class BasicAuthTransport(xmlrpclib.Transport): - def __init__(self, username=None, password=None): + def __init__(self, secure=False, username=None, password=None): xmlrpclib.Transport.__init__(self) + self.secure = secure + self.username = username self.password = password def send_auth(self, h): - if self.username is not None and self.password is not None: - h.putheader('AUTHORIZATION', "Basic %s" % string.replace( - encodestring("%s:%s" % (self.username, self.password)), - "\012", "" - )) + if not self.username or not self.password: + return + + auth = b64encode("%s:%s" % (self.username, self.password)) + + h.putheader('Authorization', "Basic %s" % auth) + + def make_connection(self, host): + if self._connection and host == self._connection[0]: + return self._connection[1] + + chost, self._extra_headers, x509 = self.get_host_info(host) + + if self.secure: + try: + self._connection = host, httplib.HTTPSConnection(chost, None, **(x509 or {})) + except AttributeError: + raise NotImplementedError( + "your version of httplib doesn't support HTTPS" + ) + else: + self._connection = host, httplib.HTTPConnection(chost) + + return self._connection[1] + def single_request(self, host, handler, request_body, verbose=0): # issue XML-RPC request From 28062eacb66eed52c2697f685170f4e963f0bdf4 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Tue, 11 Mar 2014 15:13:49 +1300 Subject: [PATCH 149/468] [rtorrent] cleaned up connection, '+https' is now added to 'httprpc' protocol if SSL option is enabled --- couchpotato/core/downloaders/rtorrent_.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index fb8948c6..d5dbf7d8 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -59,20 +59,24 @@ class rTorrent(Downloader): return self.rt url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + + # Automatically add '+https' to 'httprpc' protocol if SSL is enabled + if self.conf('ssl') and url.startswith('httprpc://'): + url = url.replace('httprpc://', 'httprpc+https://') + parsed = urlparse(url) # rpc_url is only used on http/https scgi pass-through if parsed.scheme in ['http', 'https']: url += self.conf('rpc_url') - if self.conf('username') and self.conf('password'): - self.rt = RTorrent( - url, - self.conf('username'), - self.conf('password') - ) - else: - self.rt = RTorrent(url) + print url + + self.rt = RTorrent( + url, + self.conf('username'), + self.conf('password') + ) self.error_msg = '' try: From 61f1fdabd131a9c89bae2aec6cfa3c312ed16f63 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 22 Mar 2014 02:46:05 +1300 Subject: [PATCH 150/468] Removed print statement from rtorrent downloader --- couchpotato/core/downloaders/rtorrent_.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index d5dbf7d8..6e5c4b62 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -70,8 +70,6 @@ class rTorrent(Downloader): if parsed.scheme in ['http', 'https']: url += self.conf('rpc_url') - print url - self.rt = RTorrent( url, self.conf('username'), From 89c83001ca992565e68e844c37cf9d8bbb345ead Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 14:49:44 +0100 Subject: [PATCH 151/468] Move status_get outside index --- couchpotato/core/media/_base/media/index.py | 8 -------- couchpotato/core/media/_base/media/main.py | 19 +++++++++++++++---- couchpotato/core/media/movie/searcher.py | 10 ++++------ .../core/media/movie/suggestion/main.py | 5 ++--- couchpotato/core/plugins/dashboard.py | 2 +- couchpotato/core/plugins/profile/main.py | 4 ++-- couchpotato/core/plugins/release/main.py | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 808eb124..4733ffaf 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -38,14 +38,6 @@ class MediaIndex(MultiTreeBasedIndex): except: pass - def run_with_status(self, db, status = [], with_doc = True): - - status = list(status if isinstance(status, (list, tuple)) else [status]) - - for s in status: - for ms in db.get_many('media_status', s, with_doc = with_doc): - yield ms['doc'] if with_doc else ms - class MediaStatusIndex(TreeBasedIndex): _version = 1 diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 06bbf671..d1f29148 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -74,6 +74,7 @@ class MediaPlugin(MediaBase): addEvent('app.load', self.addSingleDeleteView, priority = 100) addEvent('media.get', self.get) + addEvent('media.with_status', self.withStatus) addEvent('media.list', self.list) addEvent('media.delete', self.delete) addEvent('media.restatus', self.restatus) @@ -146,6 +147,16 @@ class MediaPlugin(MediaBase): 'media': media, } + def withStatus(self, status, with_doc = True): + + db = get_db() + + status = list(status if isinstance(status, (list, tuple)) else [status]) + + for s in status: + for ms in db.get_many('media_status', s, with_doc = with_doc): + yield ms['doc'] if with_doc else ms + def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): db = get_db() @@ -172,13 +183,13 @@ class MediaPlugin(MediaBase): # Filter on movie status if status and len(status) > 0: filter_by['media_status'] = set() - for media_status in db.run('media', 'with_status', status, with_doc = False): + for media_status in fireEvent('media.with_status', status, with_doc = False, single = True): filter_by['media_status'].add(media_status.get('_id')) # Filter on release status if release_status and len(release_status) > 0: filter_by['release_status'] = set() - for release_status in db.run('release', 'with_status', release_status, with_doc = False): + for release_status in fireEvent('media.with_status', release_status, with_doc = False, single = True): filter_by['release_status'].add(release_status.get('media_id')) # Add search filters @@ -285,13 +296,13 @@ class MediaPlugin(MediaBase): # Filter on movie status if status and len(status) > 0: filter_by['media_status'] = set() - for media_status in db.run('media', 'with_status', status, with_doc = False): + for media_status in fireEvent('media.with_status', status, with_doc = False, single = True): filter_by['media_status'].add(media_status.get('_id')) # Filter on release status if release_status and len(release_status) > 0: filter_by['release_status'] = set() - for release_status in db.run('release', 'with_status', release_status, with_doc = False): + for release_status in fireEvent('media.with_status', release_status, with_doc = False, single = True): filter_by['release_status'].add(release_status.get('media_id')) # Filter by combining ids diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 5a95629d..682fa69f 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -74,9 +74,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.in_progress = True fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started') - db = get_db() - - medias = [x['_id'] for x in db.run('media', 'with_status', 'active', with_doc = False)] + medias = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)] random.shuffle(medias) total = len(medias) @@ -316,9 +314,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return False - def tryNextReleaseView(self, id = None, **kwargs): + def tryNextReleaseView(self, media_id = None, **kwargs): - trynext = self.tryNextRelease(id, manual = True) + trynext = self.tryNextRelease(media_id, manual = True) return { 'success': trynext @@ -328,7 +326,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): try: db = get_db() - rels = db.run('media', 'with_status', media_id, status = ['snatched', 'done']) + rels = fireEvent('media.with_status', ['snatched', 'done'], single = True) for rel in rels: rel['status'] = 'ignored' diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index 1c6e95f4..5db403f6 100644 --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -28,8 +28,7 @@ class Suggestion(Plugin): else: if not movies or len(movies) == 0: - db = get_db() - active_movies = db.run('media', 'with_status', ['active', 'done']) + active_movies = fireEvent('media.with_status', ['active', 'done'], single = True) movies = [getIdentifier(x) for x in active_movies] if not ignored or len(ignored) == 0: @@ -86,7 +85,7 @@ class Suggestion(Plugin): # Get new results and add them if len(new_suggestions) - 1 < limit: db = get_db() - active_movies = db.run('media', 'with_status', ['active', 'done']) + active_movies = fireEvent('media.with_status', ['active', 'done'], single = True) movies = [getIdentifier(x) for x in active_movies] movies.extend(seen) diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index 57cd7afa..fad68b06 100644 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -45,7 +45,7 @@ class Dashboard(Plugin): limit = tryInt(splt[0]) # Get all active medias - active_ids = [x['_id'] for x in db.run('media', 'with_status', 'active', with_doc = False)] + active_ids = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)] medias = [] now_year = date.today().year diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 8f838f45..5d15c111 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -2,7 +2,7 @@ import traceback from couchpotato import get_db, tryInt from couchpotato.api import addApiView -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -41,7 +41,7 @@ class ProfilePlugin(Plugin): # Get all active movies without profile try: db = get_db() - medias = db.run('media', 'with_status', ['active']) + medias = fireEvent('media.with_status', 'active', single = True) profile_ids = [x.get('_id') for x in self.all()] default_id = profile_ids[0] diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 40475a4d..53209080 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -67,7 +67,7 @@ class Release(Plugin): db = get_db() # get movies last_edit more than a week ago - medias = db.run('media', 'with_status', ['done']) + medias = fireEvent('media.with_status', 'done', single = True) for media in medias: if media.get('last_edit', 0) > (now - week): From b4a3ac8081b97177a7d72e7514f9f260dc7daf9b Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 16:20:25 +0100 Subject: [PATCH 152/468] Remove get_session --- couchpotato/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index ce034db2..1946963b 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -48,10 +48,6 @@ def addView(route, func, static = False): views[route] = func -def get_session(): - return None - - def get_db(): return Env.get('db') From b63f7b7e5d7a2cd501eaf870f5b4075d2909076e Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 16:21:49 +0100 Subject: [PATCH 153/468] Move with_status out of releases --- couchpotato/core/media/_base/media/main.py | 4 ++-- couchpotato/core/plugins/release/index.py | 8 -------- couchpotato/core/plugins/release/main.py | 11 +++++++++++ couchpotato/core/plugins/renamer.py | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index d1f29148..5a6fc79d 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -189,7 +189,7 @@ class MediaPlugin(MediaBase): # Filter on release status if release_status and len(release_status) > 0: filter_by['release_status'] = set() - for release_status in fireEvent('media.with_status', release_status, with_doc = False, single = True): + for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True): filter_by['release_status'].add(release_status.get('media_id')) # Add search filters @@ -302,7 +302,7 @@ class MediaPlugin(MediaBase): # Filter on release status if release_status and len(release_status) > 0: filter_by['release_status'] = set() - for release_status in fireEvent('media.with_status', release_status, with_doc = False, single = True): + for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True): filter_by['release_status'].add(release_status.get('media_id')) # Filter by combining ids diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index ee762539..23c1576d 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -22,14 +22,6 @@ class ReleaseIndex(TreeBasedIndex): for release in db.get_many('release', media_id, with_doc = True): yield release['doc'] - def run_with_status(self, db, status = [], with_doc = True): - - status = list(status if isinstance(status, (list, tuple)) else [status]) - - for s in status: - for ms in db.get_many('release_status', s, with_doc = with_doc): - yield ms['doc'] if with_doc else ms - class ReleaseStatusIndex(TreeBasedIndex): _version = 1 diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 53209080..3ebb93b3 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -53,6 +53,7 @@ class Release(Plugin): addEvent('release.delete', self.delete) addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) + addEvent('release.with_status', self.withStatus) # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanDone) @@ -422,3 +423,13 @@ class Release(Plugin): log.error('Failed: %s', traceback.format_exc()) return False + + def withStatus(self, status, with_doc = True): + + db = get_db() + + status = list(status if isinstance(status, (list, tuple)) else [status]) + + for s in status: + for ms in db.get_many('release_status', s, with_doc = with_doc): + yield ms['doc'] if with_doc else ms diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 4cfc9875..b452648c 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -834,7 +834,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: db = get_db() - rels = list(db.run('release', 'with_status', ['snatched', 'seeding', 'missing'])) + rels = list(fireEvent('release.with_status', ['snatched', 'seeding', 'missing'], single = True)) if not rels: #No releases found that need status checking From 269d779df703772737691688268a680ae3b7bb46 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 16:28:40 +0100 Subject: [PATCH 154/468] Move for_media out of release index --- couchpotato/core/media/_base/media/main.py | 6 +++--- couchpotato/core/media/movie/_base/main.py | 6 +++--- .../core/media/movie/providers/info/_modifier.py | 4 ++-- couchpotato/core/plugins/release/index.py | 4 ---- couchpotato/core/plugins/release/main.py | 10 +++++++++- couchpotato/core/plugins/renamer.py | 2 +- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5a6fc79d..be3faf4f 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -134,7 +134,7 @@ class MediaPlugin(MediaBase): if media.get('category_id'): media['category'] = db.get('id', media.get('category_id')) - media['releases'] = list(db.run('release', 'for_media', media['_id'])) + media['releases'] = list(fireEvent('release.for_media', media['_id'], single = True)) return media @@ -352,7 +352,7 @@ class MediaPlugin(MediaBase): deleted = True else: - media_releases = list(db.run('release', 'for_media', media['_id'])) + media_releases = list(fireEvent('release.for_media', media['_id'], single = True)) total_releases = len(media_releases) total_deleted = 0 @@ -418,7 +418,7 @@ class MediaPlugin(MediaBase): move_to_wanted = True profile = db.get('id', m['profile_id']) - media_releases = list(db.run('release', 'for_media', m['_id'])) + media_releases = list(fireEvent('release.for_media', m['_id'], single = True)) for q_identifier in profile['qualities']: index = profile['qualities'].index(q_identifier) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 499dba9c..a783cd1e 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -138,7 +138,7 @@ class MovieBase(MovieTypeBase): elif force_readd: # Clean snatched history - for release in db.run('release', 'for_media', m['_id']): + for release in fireEvent('release.for_media', m['_id'], single = True): if release.get('status') in ['downloaded', 'snatched', 'done']: if params.get('ignore_previous', False): release['status'] = 'ignored' @@ -164,7 +164,7 @@ class MovieBase(MovieTypeBase): fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) # Remove releases - for rel in db.run('release', 'for_media', m['_id']): + for rel in fireEvent('release.for_media', m['_id'], single = True): if rel['status'] is 'available': db.delete(rel) @@ -215,7 +215,7 @@ class MovieBase(MovieTypeBase): m['category_id'] = cat_id if len(cat_id) > 0 else None # Remove releases - for rel in db.run('release', 'for_media', m['_id']): + for rel in fireEvent('release.for_media', m['_id'], single = True): if rel['status'] is 'available': db.delete(rel) diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index 131cc058..cea0f49f 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -3,7 +3,7 @@ import traceback from CodernityDB.database import RecordNotFound from couchpotato import get_db -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -104,7 +104,7 @@ class MovieResultModifier(Plugin): if media.get('status') == 'active': temp['in_wanted'] = media - for release in db.run('release', 'for_media', media.get('_id')): + for release in fireEvent('release.for_media', media['_id'], single = True): if release.get('status') == 'done': if not temp['in_library']: temp['in_library'] = media diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index 23c1576d..f509f486 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -18,10 +18,6 @@ class ReleaseIndex(TreeBasedIndex): if data.get('_t') == 'release' and data.get('media_id'): return data['media_id'], None - def run_for_media(self, db, media_id): - for release in db.get_many('release', media_id, with_doc = True): - yield release['doc'] - class ReleaseStatusIndex(TreeBasedIndex): _version = 1 diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 3ebb93b3..a80dfc09 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -54,6 +54,7 @@ class Release(Plugin): addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) addEvent('release.with_status', self.withStatus) + addEvent('release.for_media', self.forMedia) # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanDone) @@ -74,7 +75,7 @@ class Release(Plugin): if media.get('last_edit', 0) > (now - week): continue - for rel in db.run('release', 'for_media', media['_id']): + for rel in fireEvent('release.for_media', media['_id'], single = True): # Remove all available releases if rel['status'] in ['available']: @@ -433,3 +434,10 @@ class Release(Plugin): for s in status: for ms in db.get_many('release_status', s, with_doc = with_doc): yield ms['doc'] if with_doc else ms + + def forMedia(self, media_id): + + db = get_db() + + for release in db.get_many('release', media_id, with_doc = True): + yield release['doc'] diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index b452648c..a259802e 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -446,7 +446,7 @@ class Renamer(Plugin): log.error('Failed marking movie finished: %s', (traceback.format_exc())) # Go over current movie releases - for release in db.run('release', 'for_media', media['_id']): + for release in fireEvent('release.for_media', media['_id'], single = True): # When a release already exists if release.get('status') == 'done': From a818276b6d507395a7af0e1c253bbb2f04f91860 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 16:32:31 +0100 Subject: [PATCH 155/468] Move multi-identifier search out of index --- couchpotato/core/media/_base/media/index.py | 8 -------- couchpotato/core/media/_base/media/main.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 4733ffaf..42e69905 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -30,14 +30,6 @@ class MediaIndex(MultiTreeBasedIndex): return ids, None - def run_identifiers(self, db, identifiers, with_doc = False): - for x in identifiers: - try: - media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) - return media - except: - pass - class MediaStatusIndex(TreeBasedIndex): _version = 1 diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index be3faf4f..f6bcd718 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -75,6 +75,7 @@ class MediaPlugin(MediaBase): addEvent('media.get', self.get) addEvent('media.with_status', self.withStatus) + addEvent('media.with_identifiers', self.withIdentifiers) addEvent('media.list', self.list) addEvent('media.delete', self.delete) addEvent('media.restatus', self.restatus) @@ -157,6 +158,19 @@ class MediaPlugin(MediaBase): for ms in db.get_many('media_status', s, with_doc = with_doc): yield ms['doc'] if with_doc else ms + def withIdentifiers(self, identifiers, with_doc = False): + + db = get_db() + + for x in identifiers: + try: + media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) + return media + except: + pass + + log.error('No media found with identifiers: %s', identifiers) + def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): db = get_db() From e12dcc2fb8116c5afe5d603afd46e790a2b2ca36 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 16:37:35 +0100 Subject: [PATCH 156/468] Also return releases on notify frontend --- couchpotato/core/media/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 47997662..8e704dec 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -38,8 +38,7 @@ class MediaBase(Plugin): def notifyFront(): try: - db = get_db() - media = db.get('id', media_id) + media = fireEvent('media.get', media_id, single = True) event_name = '%s.update' % media.get('type') fireEvent('notify.frontend', type = event_name, data = media) From b4610e5c23bfde12add9f9947fac900aa9501ef5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 17:34:24 +0100 Subject: [PATCH 157/468] Make sure to make release download id lowercase --- couchpotato/core/plugins/release/index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py index f509f486..8265fe33 100644 --- a/couchpotato/core/plugins/release/index.py +++ b/couchpotato/core/plugins/release/index.py @@ -50,15 +50,15 @@ class ReleaseIDIndex(HashIndex): class ReleaseDownloadIndex(HashIndex): - _version = 1 + _version = 2 def __init__(self, *args, **kwargs): kwargs['key_format'] = '32s' super(ReleaseDownloadIndex, self).__init__(*args, **kwargs) def make_key(self, key): - return md5(key).hexdigest() + return md5(key.lower()).hexdigest() def make_key_value(self, data): if data.get('_t') == 'release' and data.get('download_info') and data['download_info']['id'] and data['download_info']['downloader']: - return md5('%s-%s' % (data['download_info']['downloader'], data['download_info']['id'])).hexdigest(), None + return md5(('%s-%s' % (data['download_info']['downloader'], data['download_info']['id'])).lower()).hexdigest(), None From ad94cce283c43b20a16c3b88cdb20132406d15d7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 17:35:26 +0100 Subject: [PATCH 158/468] Make release files normal list --- couchpotato/core/downloaders/deluge.py | 2 +- couchpotato/core/downloaders/qbittorrent_.py | 2 +- couchpotato/core/downloaders/rtorrent_.py | 2 +- couchpotato/core/downloaders/transmission.py | 2 +- couchpotato/core/downloaders/utorrent.py | 2 +- couchpotato/core/plugins/renamer.py | 39 ++++++++++++-------- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/couchpotato/core/downloaders/deluge.py b/couchpotato/core/downloaders/deluge.py index 2a386ad3..26592ca7 100644 --- a/couchpotato/core/downloaders/deluge.py +++ b/couchpotato/core/downloaders/deluge.py @@ -147,7 +147,7 @@ class Deluge(Downloader): 'seed_ratio': torrent['ratio'], 'timeleft': str(timedelta(seconds = torrent['eta'])), 'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])), - 'files': '|'.join(torrent_files), + 'files': torrent_files, }) return release_downloads diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index 349af176..558222d3 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -135,7 +135,7 @@ class qBittorrent(Downloader): 'original_status': torrent.state, 'timeleft': torrent.progress * 100 if torrent.progress else -1, # percentage 'folder': sp(torrent.save_path), - 'files': '|'.join(torrent_files) + 'files': torrent_files }) return release_downloads diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index 6e5c4b62..98946b29 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -245,7 +245,7 @@ class rTorrent(Downloader): 'original_status': torrent.state, 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, 'folder': sp(torrent.directory), - 'files': '|'.join(torrent_files) + 'files': torrent_files }) return release_downloads diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index f49cb9df..dd11166d 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -141,7 +141,7 @@ class Transmission(Downloader): 'seed_ratio': torrent['uploadRatio'], 'timeleft': str(timedelta(seconds = torrent['eta'])), 'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])), - 'files': '|'.join(torrent_files) + 'files': torrent_files }) return release_downloads diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index dae89505..671dd9ec 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -184,7 +184,7 @@ class uTorrent(Downloader): 'original_status': torrent[1], 'timeleft': str(timedelta(seconds = torrent[10])), 'folder': sp(torrent[26]), - 'files': '|'.join(torrent_files) + 'files': torrent_files }) return release_downloads diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index a259802e..47267345 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -79,16 +79,22 @@ class Renamer(Plugin): downloader = kwargs.get('downloader') download_id = kwargs.get('download_id') - files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')]) + files = [sp(filename) for filename in splitString(kwargs.get('files'), '|')] status = kwargs.get('status', 'completed') release_download = None if not base_folder and media_folder: release_download = {'folder': media_folder} - release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {}) + + if download_id: + release_download.update({ + 'id': download_id, + 'downloader': downloader, + 'status': status, + 'files': files + }) fire_handle = fireEvent if not async else fireEventAsync - fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download) return { @@ -142,7 +148,7 @@ class Renamer(Plugin): log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder) # Update to the from folder - if len(splitString(release_download.get('files'), '|')) == 1: + if len(release_download.get('files'), []) == 1: new_media_folder = from_folder else: new_media_folder = os.path.join(from_folder, os.path.basename(media_folder)) @@ -152,7 +158,7 @@ class Renamer(Plugin): return # Update the files - new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in splitString(release_download.get('files'), '|')] + new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in release_download.get('files', [])] if new_files and not os.path.isfile(new_files[0]): log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder) return @@ -160,7 +166,7 @@ class Renamer(Plugin): # Update release_download info to the from folder log.debug('Release %s found in the \'from\' folder.', media_folder) release_download['folder'] = new_media_folder - release_download['files'] = '|'.join(new_files) + release_download['files'] = new_files media_folder = new_media_folder if media_folder: @@ -182,11 +188,10 @@ class Renamer(Plugin): log.info('Scanning media folder %s...', media_folder) folder = os.path.dirname(media_folder) - if release_download.get('files', ''): - files = splitString(release_download['files'], '|') - + release_files = release_download.get('files', []) + if release_files: # If there is only one file in the torrent, the downloader did not create a subfolder - if len(files) == 1: + if len(release_files) == 1: folder = media_folder else: # Get all files from the specified folder @@ -643,8 +648,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif isinstance(release_download, dict): # Tag download_files if they are known - if release_download['files']: - tag_files = splitString(release_download['files'], '|') + if release_download.get('files', []): + tag_files = release_download.get('files', []) # Tag all files in release folder else: @@ -678,8 +683,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif isinstance(release_download, dict): # Untag download_files if they are known - if release_download['files']: - tag_files = splitString(release_download['files'], '|') + if release_download.get('files'): + tag_files = release_download.get('files', []) # Untag all files in release folder else: @@ -719,8 +724,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) ignore_files = [] # Find tag on download_files if they are known - if release_download['files']: - tag_files = splitString(release_download['files'], '|') + if release_download.get('files'): + tag_files = release_download.get('files', []) # Find tag on all files in release folder else: @@ -1040,6 +1045,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) except: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) + print rls + if rls: media = db.get('id', rls['media_id']) release_download.update({ From 78e2ff4870b0a270b1488e51d5159c0277203b6c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:01:52 +0100 Subject: [PATCH 159/468] Cleanup --- couchpotato/core/downloaders/base.py | 2 +- couchpotato/core/plugins/renamer.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 269bdc62..8042c4a2 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -210,7 +210,7 @@ class ReleaseDownloadList(list): 'status': 'busy', 'downloader': self.provider.getName(), 'folder': '', - 'files': '', + 'files': [], } return mergeDicts(defaults, result) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 47267345..f3b22c98 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -911,7 +911,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) found_release = True break else: - if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['identifier']: + if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == getIdentifier(movie_dict): log.debug('Found release by release name or imdb ID: %s', release_download['name']) found_release = True break @@ -1045,8 +1045,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) except: log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader'))) - print rls - if rls: media = db.get('id', rls['media_id']) release_download.update({ From 25b1d86c5059cb82479b7452413a404e1d30beb8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:10:32 +0100 Subject: [PATCH 160/468] get identifier in awesomehd --- couchpotato/core/media/_base/providers/torrent/awesomehd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd.py b/couchpotato/core/media/_base/providers/torrent/awesomehd.py index bf104561..1372f6cf 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd.py @@ -2,7 +2,7 @@ import re import traceback from bs4 import BeautifulSoup -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -22,7 +22,7 @@ class Base(TorrentProvider): def _search(self, movie, quality, results): - data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['identifier'], self.conf('only_internal'))) + data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), getIdentifier(movie), self.conf('only_internal'))) if data: try: From 334078fc34cda83168a5ea6457b6c91f49873b11 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:25:36 +0100 Subject: [PATCH 161/468] Allow password tag in returned release dict --- couchpotato/core/plugins/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 1e4bfeba..c5892f51 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -326,6 +326,8 @@ class Plugin(object): if name_password: release_name, password = name_password tag += '{{%s}}' % password + elif data.get('password'): + tag += '{{%s}}' % data.get('password') max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag) From caf4eab104685baee155c8a62b32a4309ecda76e Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:34:58 +0100 Subject: [PATCH 162/468] Get correct size from HDBits api. fix #2997 --- couchpotato/core/media/_base/providers/torrent/hdbits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index fa45b83f..c2e85ac5 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -56,7 +56,7 @@ class Base(TorrentProvider): 'name': result['name'], 'url': self.urls['download'] % (result['id'], self.conf('passkey')), 'detail_url': self.urls['detail'] % result['id'], - 'size': self.parseSize(result['size']), + 'size': tryInt(result['size'])/1024/1024, 'seeders': tryInt(result['seeders']), 'leechers': tryInt(result['leechers']) }) From 1f647b3cc79143898e23a3b83db22eb7a42d3711 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:46:01 +0100 Subject: [PATCH 163/468] Autoload missing for notifications. fix #2980 --- couchpotato/core/notifications/boxcar.py | 2 ++ couchpotato/core/notifications/email_.py | 2 ++ couchpotato/core/notifications/growl.py | 2 ++ couchpotato/core/notifications/nmj.py | 2 ++ couchpotato/core/notifications/notifymyandroid.py | 2 ++ couchpotato/core/notifications/notifymywp.py | 2 ++ couchpotato/core/notifications/prowl.py | 2 ++ couchpotato/core/notifications/pushalot.py | 2 ++ couchpotato/core/notifications/pushbullet.py | 2 ++ 9 files changed, 18 insertions(+) diff --git a/couchpotato/core/notifications/boxcar.py b/couchpotato/core/notifications/boxcar.py index 3777a42d..a7acc882 100644 --- a/couchpotato/core/notifications/boxcar.py +++ b/couchpotato/core/notifications/boxcar.py @@ -7,6 +7,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Boxcar' + class Boxcar(Notification): diff --git a/couchpotato/core/notifications/email_.py b/couchpotato/core/notifications/email_.py index b42650b9..938a3614 100644 --- a/couchpotato/core/notifications/email_.py +++ b/couchpotato/core/notifications/email_.py @@ -12,6 +12,8 @@ from couchpotato.environment import Env log = CPLog(__name__) +autoload = 'Email' + class Email(Notification): diff --git a/couchpotato/core/notifications/growl.py b/couchpotato/core/notifications/growl.py index c7847da2..160e383e 100644 --- a/couchpotato/core/notifications/growl.py +++ b/couchpotato/core/notifications/growl.py @@ -9,6 +9,8 @@ from gntp import notifier log = CPLog(__name__) +autoload = 'Growl' + class Growl(Notification): diff --git a/couchpotato/core/notifications/nmj.py b/couchpotato/core/notifications/nmj.py index 6ef70d43..4b9b92bd 100644 --- a/couchpotato/core/notifications/nmj.py +++ b/couchpotato/core/notifications/nmj.py @@ -15,6 +15,8 @@ except ImportError: log = CPLog(__name__) +autoload = 'NMJ' + class NMJ(Notification): diff --git a/couchpotato/core/notifications/notifymyandroid.py b/couchpotato/core/notifications/notifymyandroid.py index 8b083601..ed7a24c8 100644 --- a/couchpotato/core/notifications/notifymyandroid.py +++ b/couchpotato/core/notifications/notifymyandroid.py @@ -6,6 +6,8 @@ import six log = CPLog(__name__) +autoload = 'NotifyMyAndroid' + class NotifyMyAndroid(Notification): diff --git a/couchpotato/core/notifications/notifymywp.py b/couchpotato/core/notifications/notifymywp.py index 914897f7..262fd8d1 100644 --- a/couchpotato/core/notifications/notifymywp.py +++ b/couchpotato/core/notifications/notifymywp.py @@ -6,6 +6,8 @@ import six log = CPLog(__name__) +autoload = 'NotifyMyWP' + class NotifyMyWP(Notification): diff --git a/couchpotato/core/notifications/prowl.py b/couchpotato/core/notifications/prowl.py index b276d7f4..fdece326 100644 --- a/couchpotato/core/notifications/prowl.py +++ b/couchpotato/core/notifications/prowl.py @@ -7,6 +7,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Prowl' + class Prowl(Notification): diff --git a/couchpotato/core/notifications/pushalot.py b/couchpotato/core/notifications/pushalot.py index 0ce9027f..f8fa187e 100644 --- a/couchpotato/core/notifications/pushalot.py +++ b/couchpotato/core/notifications/pushalot.py @@ -7,6 +7,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Pushalot' + class Pushalot(Notification): diff --git a/couchpotato/core/notifications/pushbullet.py b/couchpotato/core/notifications/pushbullet.py index fd1a376b..56cf2288 100644 --- a/couchpotato/core/notifications/pushbullet.py +++ b/couchpotato/core/notifications/pushbullet.py @@ -9,6 +9,8 @@ from couchpotato.core.notifications.base import Notification log = CPLog(__name__) +autoload = 'Pushbullet' + class Pushbullet(Notification): From f50852fee03f65c1895115c3db60ffe495d86202 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 18:52:11 +0100 Subject: [PATCH 164/468] Get proper iTunes namespace. fix #2978 --- couchpotato/core/media/movie/providers/automation/itunes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/automation/itunes.py b/couchpotato/core/media/movie/providers/automation/itunes.py index bb6eef29..fcb20b12 100644 --- a/couchpotato/core/media/movie/providers/automation/itunes.py +++ b/couchpotato/core/media/movie/providers/automation/itunes.py @@ -26,7 +26,7 @@ class ITunes(Automation, RSS): urls = splitString(self.conf('automation_urls')) namespace = 'http://www.w3.org/2005/Atom' - namespace_im = 'https://rss.itunes.apple.com' + namespace_im = 'http://itunes.apple.com/rss' index = -1 for url in urls: From 5a4467adb9520769c1923514de4e5e3c16b8d986 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 21 Mar 2014 21:40:38 +0100 Subject: [PATCH 165/468] Only return movies from omdb --- couchpotato/core/media/movie/providers/info/omdbapi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/media/movie/providers/info/omdbapi.py b/couchpotato/core/media/movie/providers/info/omdbapi.py index 342df3ca..d3a83b63 100644 --- a/couchpotato/core/media/movie/providers/info/omdbapi.py +++ b/couchpotato/core/media/movie/providers/info/omdbapi.py @@ -81,6 +81,9 @@ class OMDBAPI(MovieProvider): if movie.get('Response') == 'Parse Error' or movie.get('Response') == 'False': return movie_data + if movie.get('Type').lower() != 'movie': + return movie_data + tmp_movie = movie.copy() for key in tmp_movie: if tmp_movie.get(key).lower() == 'n/a': From d4eca60b1d18953a8258e4f0461d1c2856891cef Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 09:52:21 +0100 Subject: [PATCH 166/468] Identifiers fix --- couchpotato/core/media/_base/media/main.py | 2 +- couchpotato/core/media/movie/providers/metadata/base.py | 3 ++- couchpotato/core/plugins/renamer.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index f6bcd718..43a6d406 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -169,7 +169,7 @@ class MediaPlugin(MediaBase): except: pass - log.error('No media found with identifiers: %s', identifiers) + log.debug('No media found with identifiers: %s', identifiers) def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index 97d5206e..b9fc5d71 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -4,6 +4,7 @@ import traceback from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import sp +from couchpotato.core.helpers.variable import getIdentifier from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.metadata.base import MetaDataBase from couchpotato.environment import Env @@ -27,7 +28,7 @@ class MovieMetaData(MetaDataBase): # Update library to get latest info try: - group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = group['media']['identifier'], extended = True, single = True) + group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True) except: log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index f3b22c98..4eca7836 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -248,7 +248,7 @@ class Renamer(Plugin): 'profile_id': None }, search_after = False, status = 'done', single = True) else: - group['media'] = fireEvent('movie.update_info', identifier = group['media']['identifier'], single = True) + group['media'] = fireEvent('movie.update_info', identifier = getIdentifier(group['media']), single = True) if not group['media'] or not group['media'].get('_id'): log.error('Could not rename, no library item to work with: %s', group_identifier) From 1602fe88e63bc67a19854fde116cc1679241eccb Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 09:59:22 +0100 Subject: [PATCH 167/468] Close connection not cursor --- couchpotato/core/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 19fde904..ff3ce34c 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -181,7 +181,7 @@ class Database(object): migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]] migrate_data[ml][p[0]].append(columns) - c.close() + conn.close() log.info('Getting data took %s', time.time() - migrate_start) From f976e04597660609835e46b8fac901e23252c7da Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 12:19:42 +0100 Subject: [PATCH 168/468] Save 3d in quality profile --- couchpotato/core/plugins/profile/main.py | 17 +++++- .../core/plugins/profile/static/profile.css | 52 +++++++++++++--- .../core/plugins/profile/static/profile.js | 59 +++++++++++++------ 3 files changed, 101 insertions(+), 27 deletions(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 5d15c111..196e2029 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -79,7 +79,8 @@ class ProfilePlugin(Plugin): 'core': kwargs.get('core', False), 'qualities': [], 'wait_for': [], - 'finish': [] + 'finish': [], + '3d': [] } # Update types @@ -88,6 +89,7 @@ class ProfilePlugin(Plugin): profile['qualities'].append(type.get('quality')) profile['wait_for'].append(tryInt(type.get('wait_for'))) profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True) + profile['3d'].append((tryInt(type.get('3d')) == 1) if order > 0 else True) order += 1 id = kwargs.get('id') @@ -184,6 +186,14 @@ class ProfilePlugin(Plugin): }, { 'label': 'SD', 'qualities': ['dvdrip', 'dvdr'] + }, { + 'label': 'Prefer 3D HD', + 'qualities': ['720p', '1080p', '720p', '1080p'], + '3d': [True, True] + }, { + 'label': '3D HD', + 'qualities': ['720p', '1080p'], + '3d': [True, True] }] # Create default quality profile @@ -197,12 +207,15 @@ class ProfilePlugin(Plugin): 'order': order, 'qualities': profile.get('qualities'), 'finish': [], - 'wait_for': [] + 'wait_for': [], + '3d': [] } + threed = profile.get('3d', []) for q in profile.get('qualities'): pro['finish'].append(True) pro['wait_for'].append(0) + pro['3d'].append(threed.pop() if threed else False) db.insert(pro) order += 1 diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index d94576b9..e8d6f555 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -29,11 +29,10 @@ .profile .qualities { min-height: 80px; - padding-top: 0; } .profile .formHint { - width: 250px !important; + width: 210px !important; vertical-align: top !important; margin: 0 !important; padding-left: 3px !important; @@ -78,21 +77,55 @@ } .profile .quality_type select { - width: 186px; + width: 120px; margin-left: -1px; } - .profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle { + .profile .types li.is_empty .check, + .profile .types li.is_empty .delete, + .profile .types li.is_empty .handle, + .profile .types li.is_empty .check_label { visibility: hidden; } + .profile .types .type label { + display: inline-block; + width: auto; + float: none; + text-transform: uppercase; + font-size: 11px; + font-weight: normal; + margin-right: 20px; + text-shadow: none; + vertical-align: bottom; + padding: 0; + height: 17px; + } + .profile .types .type label .check { + margin-right: 5px; + } + .profile .types .type label .check_label { + display: inline-block; + vertical-align: top; + height: 16px; + line-height: 13px; + } + + .profile .types .type .threed { + display: none; + } + + .profile .types .type.allow_3d .threed { + display: inline-block; + } + .profile .types .type .handle { - background: url('../../static/profile_plugin/handle.png') center; + background: url('../../plugin/profile_plugin/handle.png') center; display: inline-block; height: 20px; width: 20px; - cursor: -webkit-grab; cursor: -moz-grab; + cursor: -webkit-grab; cursor: grab; margin: 0; } @@ -106,6 +139,9 @@ font-size: 13px; color: #fd5353; } + .profile .types .type:not(.allow_3d) .delete { + margin-left: 55px; + } .profile .types .type:hover:not(.is_empty) .delete { visibility: visible; @@ -144,7 +180,7 @@ } #profile_ordering li .handle { - background: url('../../static/profile_plugin/handle.png') center; + background: url('../../plugin/profile_plugin/handle.png') center; width: 20px; float: right; } @@ -154,4 +190,4 @@ float: right; width: 250px; margin: 0; - } \ No newline at end of file + } diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 1bd2edce..2c3d05e7 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -63,6 +63,7 @@ var Profile = new Class({ data.types.include({ 'quality': quality, 'finish': data.finish[nr] || false, + '3d': data['3d'] ? data['3d'][nr] || false : false, 'wait_for': data.wait_for[nr] || 0 }) }); @@ -124,7 +125,8 @@ var Profile = new Class({ if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1) data.types.include({ 'quality': type.getElement('select').get('value'), - 'finish': +type.getElement('input[type=checkbox]').checked, + 'finish': +type.getElement('input.finish[type=checkbox]').checked, + '3d': +type.getElement('input.3d[type=checkbox]').checked, 'wait_for': 0 }); }) @@ -240,6 +242,7 @@ Profile.Type = new Class({ self.addEvent('change', function(){ self.el[self.qualities.get('value') == '-1' ? 'addClass' : 'removeClass']('is_empty'); + self.el[Quality.getQuality(self.qualities.get('value')).allow_3d ? 'addClass': 'removeClass']('allow_3d'); self.deleted = self.qualities.get('value') == '-1'; }); @@ -250,24 +253,40 @@ Profile.Type = new Class({ var data = self.data; self.el = new Element('li.type').adopt( - new Element('span.quality_type').adopt( + new Element('span.quality_type').grab( self.fillQualities() ), - new Element('span.finish').adopt( - self.finish = new Element('input.inlay.finish[type=checkbox]', { - 'checked': data.finish !== undefined ? data.finish : 1, - 'events': { - 'change': function(e){ - if(self.el == self.el.getParent().getElement(':first-child')){ - self.finish_class.check(); - alert('Top quality always finishes the search') - return; - } + self.finish_container = new Element('label.finish').adopt( + new Element('span.finish').grab( + self.finish = new Element('input.inlay.finish[type=checkbox]', { + 'checked': data.finish !== undefined ? data.finish : 1, + 'events': { + 'change': function(e){ + if(self.el == self.el.getParent().getElement(':first-child')){ + self.finish_class.check(); + alert('Top quality always finishes the search') + return; + } - self.fireEvent('change'); + self.fireEvent('change'); + } } - } - }) + }) + ), + new Element('span.check_label[text=finish]') + ), + self['3d_container'] = new Element('label.threed').adopt( + new Element('span.3d').grab( + self['3d'] = new Element('input.inlay.3d[type=checkbox]', { + 'checked': data['3d'] !== undefined ? data['3d'] : 1, + 'events': { + 'change': function(e){ + self.fireEvent('change'); + } + } + }) + ), + new Element('span.check_label[text=3D]') ), new Element('span.delete.icon2', { 'events': { @@ -279,7 +298,11 @@ Profile.Type = new Class({ self.el[self.data.quality ? 'removeClass' : 'addClass']('is_empty'); + if(self.data.quality && Quality.getQuality(self.data.quality).allow_3d) + self.el.addClass('allow_3d'); + self.finish_class = new Form.Check(self.finish); + self['3d_class'] = new Form.Check(self['3d']); }, @@ -290,7 +313,7 @@ Profile.Type = new Class({ 'events': { 'change': self.fireEvent.bind(self, 'change') } - }).adopt( + }).grab( new Element('option', { 'text': '+ Add another quality', 'value': -1 @@ -300,7 +323,8 @@ Profile.Type = new Class({ Object.each(Quality.qualities, function(q){ new Element('option', { 'text': q.label, - 'value': q.identifier + 'value': q.identifier, + 'data-allow_3d': q.allow_3d }).inject(self.qualities) }); @@ -316,6 +340,7 @@ Profile.Type = new Class({ return { 'quality': self.qualities.get('value'), 'finish': +self.finish.checked, + '3d': +self['3d'].checked, 'wait_for': 0 } }, From 59347400c30f9d8a2ff26a1fef70703d826e4f1f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 12:20:16 +0100 Subject: [PATCH 169/468] Add 3D tags --- couchpotato/core/plugins/quality/main.py | 11 ++++++++--- couchpotato/core/plugins/scanner.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index e64b5035..750d4009 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -21,9 +21,9 @@ class QualityPlugin(Plugin): } qualities = [ - {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, - {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, - {'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, + {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, + {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, + {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, @@ -34,6 +34,11 @@ class QualityPlugin(Plugin): {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]} ] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] + threed_tags = { + 'hsbs': [('half', 'sbs')], + 'fsbs': [('full', 'sbs')], + '3d': [], + } cached_qualities = None cached_order = None diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 65ae257a..4d1cc7d9 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -85,7 +85,7 @@ class Scanner(Plugin): 'hdtv': ['hdtv'] } - clean = '[ _\,\.\(\)\[\]\-]?(extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ + clean = '[ _\,\.\(\)\[\]\-]?(3d|hsbs|sbs|extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ '|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 From 61c95240c2ce26798d3236c92e07231e142233c8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 12:35:56 +0100 Subject: [PATCH 170/468] Failed editing movie --- couchpotato/core/media/_base/media/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 43a6d406..f9d29a8d 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -132,8 +132,8 @@ class MediaPlugin(MediaBase): if media: # Attach category - if media.get('category_id'): - media['category'] = db.get('id', media.get('category_id')) + try: media['category'] = db.get('id', media.get('category_id')) + except: pass media['releases'] = list(fireEvent('release.for_media', media['_id'], single = True)) From d38d581d1d65d24660916fab9eb075c22a21705c Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 12:36:11 +0100 Subject: [PATCH 171/468] Add 3d to quality tags on movie item --- couchpotato/core/media/movie/_base/static/movie.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 2b0c7b73..38012e97 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -197,7 +197,7 @@ var Movie = new Class({ if(self.profile.data) self.profile.getTypes().each(function(type){ - var q = self.addQuality(type.get('quality')); + var q = self.addQuality(type.get('quality'), type.get('3d')); if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ q.addClass('finish'); q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') @@ -222,11 +222,11 @@ var Movie = new Class({ self.data.releases.each(function(release){ - var q = self.quality.getElement('.q_'+ release.quality), + var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')), status = release.status; if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) - var q = self.addQuality(release.quality) + var q = self.addQuality(release.quality, release.is_3d || false) if (q && !q.hasClass(status)){ q.addClass(status); @@ -236,13 +236,13 @@ var Movie = new Class({ }); }, - addQuality: function(quality){ + addQuality: function(quality, is_3d){ var self = this; var q = Quality.getQuality(quality); return new Element('span', { - 'text': q.label, - 'class': 'q_'+q.identifier, + 'text': q.label + (is_3d ? ' 3D' : ''), + 'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''), 'title': '' }).inject(self.quality); From 4e70c1882b90aaec37e82a2f9c3f928f2238b5b4 Mon Sep 17 00:00:00 2001 From: Ramon van Dam Date: Sat, 22 Mar 2014 14:31:29 +0100 Subject: [PATCH 172/468] Added FilmCentrum.nl as a provider for the Chrome extension --- .../core/media/movie/providers/userscript/filmcentrum.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 couchpotato/core/media/movie/providers/userscript/filmcentrum.py diff --git a/couchpotato/core/media/movie/providers/userscript/filmcentrum.py b/couchpotato/core/media/movie/providers/userscript/filmcentrum.py new file mode 100644 index 00000000..b2b15a9e --- /dev/null +++ b/couchpotato/core/media/movie/providers/userscript/filmcentrum.py @@ -0,0 +1,8 @@ +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + +autoload = 'FilmCentrum' + + +class FilmCentrum(UserscriptBase): + + includes = ['*://filmcentrum.nl/films/*'] From 98183ccc1e8d6c4c67c0591bd687d9c1f82cd7c0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 14:38:58 +0100 Subject: [PATCH 173/468] Sorted releases --- couchpotato/core/media/_base/media/main.py | 6 +++--- couchpotato/core/plugins/release/main.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index f9d29a8d..5b690cfa 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -135,7 +135,7 @@ class MediaPlugin(MediaBase): try: media['category'] = db.get('id', media.get('category_id')) except: pass - media['releases'] = list(fireEvent('release.for_media', media['_id'], single = True)) + media['releases'] = fireEvent('release.for_media', media['_id'], single = True) return media @@ -366,7 +366,7 @@ class MediaPlugin(MediaBase): deleted = True else: - media_releases = list(fireEvent('release.for_media', media['_id'], single = True)) + media_releases = fireEvent('release.for_media', media['_id'], single = True) total_releases = len(media_releases) total_deleted = 0 @@ -432,7 +432,7 @@ class MediaPlugin(MediaBase): move_to_wanted = True profile = db.get('id', m['profile_id']) - media_releases = list(fireEvent('release.for_media', m['_id'], single = True)) + media_releases = fireEvent('release.for_media', m['_id'], single = True) for q_identifier in profile['qualities']: index = profile['qualities'].index(q_identifier) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index a80dfc09..b390c284 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -438,6 +438,10 @@ class Release(Plugin): def forMedia(self, media_id): db = get_db() + raw_releases = list(db.get_many('release', media_id, with_doc = True)) - for release in db.get_many('release', media_id, with_doc = True): - yield release['doc'] + releases = [] + for r in sorted(raw_releases, key = lambda k: k['doc']['info']['score'], reverse = True): + releases.append(r['doc']) + + return releases From 81efd4bce782fbe39ef7e2899c70464622c6ffab Mon Sep 17 00:00:00 2001 From: jkaberg Date: Sat, 22 Mar 2014 18:35:11 +0100 Subject: [PATCH 174/468] [qbittorrent] minor cleanup --- couchpotato/core/downloaders/qbittorrent_.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index 558222d3..ac48d327 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -107,25 +107,24 @@ class qBittorrent(Downloader): for torrent in torrents: if torrent.hash in ids: torrent.update_general() # get extra info + torrent_filelist = torrent.get_files() + torrent_files = [] - t_files = torrent.get_files() + torrent_dir = os.path.join(torrent.save_path, torrent.name) - check_dir = os.path.join(torrent.save_path, torrent.name) - if os.path.isdir(check_dir): - torrent.save_path = check_dir + if os.path.isdir(torrent_dir): + torrent.save_path = torrent_dir - if len(t_files) > 1 and os.path.isdir(torrent.save_path): # multi file torrent + if len(torrent_filelist) > 1 and os.path.isdir(torrent_dir): # multi file torrent, path.isdir check makes sure we're not in the root download folder for root, _, files in os.walk(torrent.save_path): for f in files: - p = os.path.join(root, f) - if os.path.isfile(p): - torrent_files.append(sp(p)) + torrent_files.append(sp(os.path.join(root, f))) else: # multi or single file placed directly in torrent.save_path - for f in t_files: - p = os.path.join(torrent.save_path, f.name) - if os.path.isfile(p): - torrent_files.append(sp(p)) + for f in torrent_filelist: + file_path = os.path.join(torrent.save_path, f.name) + if os.path.isfile(file_path): + torrent_files.append(sp(file_path)) release_downloads.append({ 'id': torrent.hash, From 77a355279776a3e271d0094e37fb6984ed37dd49 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 21:03:19 +0100 Subject: [PATCH 175/468] Add 3d support for searching --- couchpotato/core/media/_base/searcher/main.py | 12 +++++ .../media/movie/_base/static/movie.actions.js | 2 +- couchpotato/core/media/movie/searcher.py | 16 +++++-- couchpotato/core/plugins/profile/main.py | 4 +- couchpotato/core/plugins/quality/main.py | 44 ++++++++++++++++--- couchpotato/core/plugins/release/main.py | 5 +++ 6 files changed, 71 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 5a280438..6ed43364 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -17,6 +17,7 @@ class Searcher(SearcherBase): def __init__(self): addEvent('searcher.protocols', self.getSearchProtocols) addEvent('searcher.contains_other_quality', self.containsOtherQuality) + addEvent('searcher.correct_3d', self.correct3D) addEvent('searcher.correct_year', self.correctYear) addEvent('searcher.correct_name', self.correctName) addEvent('searcher.correct_words', self.correctWords) @@ -123,6 +124,17 @@ class Searcher(SearcherBase): return not (found.get(preferred_quality['identifier']) and len(found) == 1) + def correct3D(self, nzb, preferred_quality = None): + if not preferred_quality: preferred_quality = {} + if not preferred_quality.get('custom'): return + + threed = preferred_quality['custom'].get('3d') + + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + + return threed == guess.get('is_3d') + def correctYear(self, haystack, year, year_range): if not isinstance(haystack, (list, tuple, set)): diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 5dcaed78..cfcd5fe8 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -192,7 +192,7 @@ MA.Release = new Class({ }).adopt( new Element('span.name', {'text': release_name, 'title': release_name}), new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), - new Element('span.quality', {'text': quality.label || 'n/a'}), + new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}), new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.score', {'text': self.get(release, 'score')}), diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 682fa69f..f894a374 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -143,14 +143,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase): ret = False + index = 0 for q_identifier in profile.get('qualities'): - index = profile['qualities'].index(q_identifier) quality_custom = { 'quality': q_identifier, 'finish': profile['finish'][index], - 'wait_for': profile['wait_for'][index] + 'wait_for': profile['wait_for'][index], + '3d': profile['3d'][index] if profile['3d'] else False } + index += 1 + if not self.conf('always_search') and not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']): too_early_to_search.append(q_identifier) continue @@ -168,6 +171,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): quality = fireEvent('quality.single', identifier = q_identifier, single = True) log.info('Search for %s in %s', (default_title, quality['label'])) + # Extend quality with profile customs + quality['custom'] = quality_custom + results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] if len(results) == 0: log.debug('Nothing found for %s in %s', (default_title, quality['label'])) @@ -221,13 +227,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if not fireEvent('searcher.correct_words', nzb['name'], media, single = True): return False - preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) + preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) # Contains lower quality string if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True): log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) return False + # Contains lower quality string + if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, single = True): + log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label'])) + return False # File to small if nzb['size'] and tryInt(preferred_quality['size_min']) > tryInt(nzb['size']): diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 196e2029..8acf93e4 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -188,11 +188,11 @@ class ProfilePlugin(Plugin): 'qualities': ['dvdrip', 'dvdr'] }, { 'label': 'Prefer 3D HD', - 'qualities': ['720p', '1080p', '720p', '1080p'], + 'qualities': ['1080p', '720p', '720p', '1080p'], '3d': [True, True] }, { 'label': '3D HD', - 'qualities': ['720p', '1080p'], + 'qualities': ['1080p', '720p'], '3d': [True, True] }] diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 750d4009..c52a0572 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -190,14 +190,19 @@ class QualityPlugin(Plugin): # Start with 0 score = {} for quality in qualities: - score[quality.get('identifier')] = 0 + score[quality.get('identifier')] = { + 'score': 0, + '3d': {} + } for cur_file in files: words = re.split('\W+', cur_file.lower()) for quality in qualities: contains_score = self.containsTagScore(quality, words, cur_file) - self.calcScore(score, quality, contains_score) + threedscore = self.contains3D(quality, words, cur_file) if quality.get('allow_3d') else (0, None) + + self.calcScore(score, quality, contains_score, threedscore) # Try again with loose testing for quality in qualities: @@ -213,10 +218,13 @@ class QualityPlugin(Plugin): if not has_non_zero: return None - heighest_quality = max(score, key = score.get) + heighest_quality = max(score, key = lambda p: score[p]['score']) if heighest_quality: for quality in qualities: if quality.get('identifier') == heighest_quality: + quality['is_3d'] = False + if score[heighest_quality].get('3d'): + quality['is_3d'] = True return self.setCache(cache_key, quality) return None @@ -260,6 +268,23 @@ class QualityPlugin(Plugin): return score + def contains3D(self, quality, words, cur_file = ''): + cur_file = ss(cur_file) + + for key in self.threed_tags: + tags = self.threed_tags.get(key, []) + + for tag in tags: + if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in cur_file.lower()): + log.debug('Found %s in %s', (tag, cur_file)) + return 1, key + + if list(set([key]) & set(words)): + log.debug('Found %s in %s', (tag, cur_file)) + return 1, key + + return 0, None + def guessLooseScore(self, quality, extra = None): score = 0 @@ -282,9 +307,16 @@ class QualityPlugin(Plugin): return score - def calcScore(self, score, quality, add_score): + def calcScore(self, score, quality, add_score, threedscore = (0, None)): - score[quality['identifier']] += add_score + score[quality['identifier']]['score'] += add_score + + threedscore, threedtag = threedscore + if threedscore and threedtag: + if threedscore not in score[quality['identifier']]['3d']: + score[quality['identifier']]['3d'][threedtag] = 0 + + score[quality['identifier']]['3d'][threedtag] += threedscore # Set order for allow calculation (and cache) if not self.cached_order: @@ -294,7 +326,7 @@ class QualityPlugin(Plugin): if add_score != 0: for allow in quality.get('allow', []): - score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 + score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 def doTest(self): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index b390c284..867655dc 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -343,6 +343,10 @@ class Release(Plugin): found_releases = [] + is_3d = False + try: is_3d = quality['custom']['3d'] + except: pass + for rel in search_results: rel_identifier = md5(rel['url']) @@ -353,6 +357,7 @@ class Release(Plugin): 'identifier': rel_identifier, 'media_id': media.get('_id'), 'quality': quality.get('identifier'), + 'is_3d': is_3d, 'status': rel.get('status', 'available'), 'last_edit': int(time.time()), 'info': {} From 1cabf64993658de9edcf0a1977de1631d69f75b0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 21:32:27 +0100 Subject: [PATCH 176/468] Add 3d categories --- couchpotato/core/media/movie/providers/torrent/bitsoup.py | 1 + couchpotato/core/media/movie/providers/torrent/iptorrents.py | 1 + couchpotato/core/media/movie/providers/torrent/thepiratebay.py | 1 + 3 files changed, 3 insertions(+) diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py index b3ecf2e7..bdc6cd4e 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -12,6 +12,7 @@ autoload = 'Bitsoup' class Bitsoup(MovieProvider, Base): cat_ids = [ ([41], ['720p', '1080p']), + ([17], ['3d']), ([20], ['dvdr']), ([19], ['brrip', 'dvdrip']), ] diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index 4f9d6517..34105133 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -11,6 +11,7 @@ class IPTorrents(MovieProvider, Base): cat_ids = [ ([48], ['720p', '1080p', 'bd50']), + ([87], ['3d']), ([72], ['cam', 'ts', 'tc', 'r5', 'scr']), ([7], ['dvdrip', 'brrip']), ([6], ['dvdr']), diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py index 457e53d5..9233fe73 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -13,6 +13,7 @@ class ThePirateBay(MovieProvider, Base): cat_ids = [ ([207], ['720p', '1080p']), + ([209], ['3d']), ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([201, 207], ['brrip']), ([202], ['dvdr']) From 2ccdc8ffdc402060a8987dc3dea3d0ba83138ed7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 22:00:26 +0100 Subject: [PATCH 177/468] Allow 3d categories --- couchpotato/core/media/_base/providers/base.py | 10 ++++++++-- .../core/media/_base/providers/nzb/omgwtfnzbs.py | 2 +- .../media/_base/providers/torrent/ilovetorrents.py | 2 +- .../core/media/_base/providers/torrent/iptorrents.py | 8 ++++---- .../media/_base/providers/torrent/kickasstorrents.py | 2 +- .../core/media/_base/providers/torrent/thepiratebay.py | 2 +- .../core/media/_base/providers/torrent/torrentbytes.py | 2 +- .../core/media/movie/providers/torrent/bitsoup.py | 4 ++-- .../core/media/movie/providers/torrent/iptorrents.py | 4 ++-- .../core/media/movie/providers/torrent/sceneaccess.py | 6 ++---- .../core/media/movie/providers/torrent/thepiratebay.py | 2 +- .../core/media/movie/providers/torrent/torrentleech.py | 2 +- .../core/media/movie/providers/torrent/torrentshack.py | 2 +- 13 files changed, 26 insertions(+), 22 deletions(-) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 13f0f6e0..4476bc49 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -244,10 +244,16 @@ class YarrProvider(Provider): return 0 - def getCatId(self, identifier): + def getCatId(self, quality = None): + if not quality: quality = {} + identifier = quality.get('identifier') + + want_3d = False + if quality.get('custom'): + want_3d = quality['custom'].get('3d') for ids, qualities in self.cat_ids: - if identifier in qualities: + if identifier in qualities or (want_3d and '3d' in qualities): return ids if self.cat_backup_id: diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index ab9efbda..fe0338a5 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -42,7 +42,7 @@ class Base(NZBProvider, RSS): q = '%s %s' % (title, movie['info']['year']) params = tryUrlencode({ 'search': q, - 'catid': ','.join([str(x) for x in self.getCatId(quality['identifier'])]), + 'catid': ','.join([str(x) for x in self.getCatId(quality)]), 'user': self.conf('username', default = ''), 'api': self.conf('api_key', default = ''), }) diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index d9db53b5..9a9cd63a 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -36,7 +36,7 @@ class Base(TorrentProvider): page = 0 total_pages = 1 - cats = self.getCatId(quality['identifier']) + cats = self.getCatId(quality) while page < total_pages: diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index 4b9bd79c..d36ba799 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -25,14 +25,14 @@ class Base(TorrentProvider): cat_backup_id = None def buildUrl(self, title, media, quality): - return self._buildUrl(title.replace(':', ''), quality['identifier']) + return self._buildUrl(title.replace(':', ''), quality) - def _buildUrl(self, query, quality_identifier): + def _buildUrl(self, query, quality): - cat_ids = self.getCatId(quality_identifier) + cat_ids = self.getCatId(quality) if not cat_ids: - log.warning('Unable to find category ids for identifier "%s"', quality_identifier) + log.warning('Unable to find category ids for identifier "%s"', quality.get('identifier')) return None return self.urls['search'] % ("&".join(("l%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%')) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index b347ccbe..1cfb5700 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -42,7 +42,7 @@ class Base(TorrentMagnetProvider): if data: - cat_ids = self.getCatId(quality['identifier']) + cat_ids = self.getCatId(quality) table_order = ['name', 'size', None, 'age', 'seeds', 'leechers'] try: diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 4d819619..26eeb6e1 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -39,7 +39,7 @@ class Base(TorrentMagnetProvider): page = 0 total_pages = 1 - cats = self.getCatId(quality['identifier']) + cats = self.getCatId(quality) base_search_url = self.urls['search'] % self.getDomain() diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py index afb66eb3..c01a6d7f 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -36,7 +36,7 @@ class Base(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality['identifier'])[0]) + url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality)[0]) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py index bdc6cd4e..c351ab9f 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -11,8 +11,8 @@ autoload = 'Bitsoup' class Bitsoup(MovieProvider, Base): cat_ids = [ - ([41], ['720p', '1080p']), ([17], ['3d']), + ([41], ['720p', '1080p']), ([20], ['dvdr']), ([19], ['brrip', 'dvdrip']), ] @@ -24,6 +24,6 @@ class Bitsoup(MovieProvider, Base): fireEvent('library.query', media, include_year = False, single = True), media['info']['year'] ), - 'cat': self.getCatId(quality['identifier'])[0], + 'cat': self.getCatId(quality)[0], }) return query diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index 34105133..f06aadfc 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -10,8 +10,8 @@ autoload = 'IPTorrents' class IPTorrents(MovieProvider, Base): cat_ids = [ - ([48], ['720p', '1080p', 'bd50']), ([87], ['3d']), + ([48], ['720p', '1080p', 'bd50']), ([72], ['cam', 'ts', 'tc', 'r5', 'scr']), ([7], ['dvdrip', 'brrip']), ([6], ['dvdr']), @@ -20,4 +20,4 @@ class IPTorrents(MovieProvider, Base): def buildUrl(self, title, media, quality): query = '%s %s' % (title.replace(':', ''), media['info']['year']) - return self._buildUrl(query, quality['identifier']) + return self._buildUrl(query, quality) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py index 2a778391..dee4b32c 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -18,10 +18,8 @@ class SceneAccess(MovieProvider, Base): ] def buildUrl(self, media, quality): - url = self.urls['search'] % ( - self.getCatId(quality['identifier'])[0], - self.getCatId(quality['identifier'])[0] - ) + cat_id = self.getCatId(quality)[0] + url = self.urls['search'] % (cat_id, cat_id) arguments = tryUrlencode({ 'search': fireEvent('library.query', media, single = True), diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py index 9233fe73..4990a579 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -12,8 +12,8 @@ autoload = 'ThePirateBay' class ThePirateBay(MovieProvider, Base): cat_ids = [ - ([207], ['720p', '1080p']), ([209], ['3d']), + ([207], ['720p', '1080p']), ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([201, 207], ['brrip']), ([202], ['dvdr']) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py index 07ace2b9..9ab1580e 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py @@ -24,5 +24,5 @@ class TorrentLeech(MovieProvider, Base): def buildUrl(self, media, quality): return ( tryUrlencode(fireEvent('library.query', media, single = True)), - self.getCatId(quality['identifier'])[0] + self.getCatId(quality)[0] ) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack.py b/couchpotato/core/media/movie/providers/torrent/torrentshack.py index 24ac863a..85e420dd 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack.py @@ -31,6 +31,6 @@ class TorrentShack(MovieProvider, Base): def buildUrl(self, media, quality): query = (tryUrlencode(fireEvent('library.query', media, single = True)), - self.getCatId(quality['identifier'])[0], + self.getCatId(quality)[0], self.getSceneOnly()) return query From 7bb723d6b3772b29a4836d3f0f964c395c058fa2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 22:39:26 +0100 Subject: [PATCH 178/468] Key errors --- couchpotato/core/media/movie/searcher.py | 2 +- couchpotato/core/plugins/release/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index f894a374..d6d4821c 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -149,7 +149,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): 'quality': q_identifier, 'finish': profile['finish'][index], 'wait_for': profile['wait_for'][index], - '3d': profile['3d'][index] if profile['3d'] else False + '3d': profile['3d'][index] if profile.get('3d') else False } index += 1 diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 867655dc..437e736c 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -446,7 +446,7 @@ class Release(Plugin): raw_releases = list(db.get_many('release', media_id, with_doc = True)) releases = [] - for r in sorted(raw_releases, key = lambda k: k['doc']['info']['score'], reverse = True): + for r in sorted(raw_releases, key = lambda k: k['doc'].get('info', {}).get('score', 0), reverse = True): releases.append(r['doc']) return releases From 72f6516a1c3d9112f5cc55912a6a0c78db203b34 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 23:01:03 +0100 Subject: [PATCH 179/468] Fix handle image url --- .../core/plugins/category/static/category.css | 6 +++--- couchpotato/core/plugins/profile/static/profile.css | 6 +++--- .../profile/static => static/images}/handle.png | Bin 3 files changed, 6 insertions(+), 6 deletions(-) rename couchpotato/{core/plugins/profile/static => static/images}/handle.png (100%) diff --git a/couchpotato/core/plugins/category/static/category.css b/couchpotato/core/plugins/category/static/category.css index 0987c197..3218a79c 100644 --- a/couchpotato/core/plugins/category/static/category.css +++ b/couchpotato/core/plugins/category/static/category.css @@ -22,7 +22,7 @@ .category > .delete:hover { opacity: 1; } - + .category .ctrlHolder:hover { background: none; } @@ -69,7 +69,7 @@ } #category_ordering li .handle { - background: url('../../static/profile_plugin/handle.png') center; + background: url('../../images/handle.png') center; width: 20px; float: right; } @@ -79,4 +79,4 @@ float: right; width: 250px; margin: 0; - } \ No newline at end of file + } diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index e8d6f555..073fa573 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -22,7 +22,7 @@ .profile > .delete:hover { opacity: 1; } - + .profile .ctrlHolder:hover { background: none; } @@ -120,7 +120,7 @@ } .profile .types .type .handle { - background: url('../../plugin/profile_plugin/handle.png') center; + background: url('../../images/handle.png') center; display: inline-block; height: 20px; width: 20px; @@ -180,7 +180,7 @@ } #profile_ordering li .handle { - background: url('../../plugin/profile_plugin/handle.png') center; + background: url('../../images/handle.png') center; width: 20px; float: right; } diff --git a/couchpotato/core/plugins/profile/static/handle.png b/couchpotato/static/images/handle.png similarity index 100% rename from couchpotato/core/plugins/profile/static/handle.png rename to couchpotato/static/images/handle.png From 745b262800afd8b89ed2ec566d8042c6314c1977 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 22 Mar 2014 23:10:31 +0100 Subject: [PATCH 180/468] Don't check 3d checkbox on add --- couchpotato/core/plugins/profile/static/profile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 2c3d05e7..47b92a56 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -100,7 +100,7 @@ var Profile = new Class({ 'onComplete': function(json){ if(json.success){ self.data = json.profile; - self.type_container.getElement('li:first-child input[type=checkbox]') + self.type_container.getElement('li:first-child input.finish[type=checkbox]') .set('checked', true) .getParent().addClass('checked'); } @@ -278,7 +278,7 @@ Profile.Type = new Class({ self['3d_container'] = new Element('label.threed').adopt( new Element('span.3d').grab( self['3d'] = new Element('input.inlay.3d[type=checkbox]', { - 'checked': data['3d'] !== undefined ? data['3d'] : 1, + 'checked': data['3d'] !== undefined ? data['3d'] : 0, 'events': { 'change': function(e){ self.fireEvent('change'); From 74fd7c684ed003cdf239fe4541f97242b5a97bd5 Mon Sep 17 00:00:00 2001 From: Wouter van Os Date: Sun, 23 Mar 2014 15:18:18 +0100 Subject: [PATCH 181/468] Fixed first item in quality group First item within a quality group had always 3d on "true". --- couchpotato/core/plugins/profile/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 8acf93e4..a7fdcd41 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -89,7 +89,7 @@ class ProfilePlugin(Plugin): profile['qualities'].append(type.get('quality')) profile['wait_for'].append(tryInt(type.get('wait_for'))) profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True) - profile['3d'].append((tryInt(type.get('3d')) == 1) if order > 0 else True) + profile['3d'].append(tryInt(type.get('3d'))) order += 1 id = kwargs.get('id') From f3ae63c7a9f921344cc48f39b9da4e26b2f5b129 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 17:50:49 +0100 Subject: [PATCH 182/468] Don't extend release_download when none is set --- couchpotato/core/plugins/renamer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 4eca7836..9099c44b 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -204,7 +204,8 @@ class Renamer(Plugin): db = get_db() # Extend the download info with info stored in the downloaded release - release_download = self.extendReleaseDownload(release_download) + if release_download: + release_download = self.extendReleaseDownload(release_download) # Unpack any archives extr_files = None From e870fab277ee708159a24eaeefa28a815956ada4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 17:55:12 +0100 Subject: [PATCH 183/468] Scanner didn't use correct get key to determine movie --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 4d1cc7d9..133ec452 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -597,7 +597,7 @@ class Scanner(Plugin): if imdb_id: try: db = get_db() - return db.get('media', imdb_id, with_doc = True)['doc'] + return db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] except: log.debug('Movie "%s" not in library, just getting info', imdb_id) return { From d5c6942266d756e82524ed528b672ee7885468a1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 17:55:33 +0100 Subject: [PATCH 184/468] Use media_id to get movie in renamer --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 9099c44b..8c5a182d 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -249,7 +249,7 @@ class Renamer(Plugin): 'profile_id': None }, search_after = False, status = 'done', single = True) else: - group['media'] = fireEvent('movie.update_info', identifier = getIdentifier(group['media']), single = True) + group['media'] = fireEvent('movie.update_info', media_id = group['media'].get('_id'), single = True) if not group['media'] or not group['media'].get('_id'): log.error('Could not rename, no library item to work with: %s', group_identifier) From d7bf9dba014f10225e26cc9a97b50fb1fe175b08 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 20:14:10 +0100 Subject: [PATCH 185/468] Skip leftover release info --- couchpotato/core/database.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index ff3ce34c..e65e0dec 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -297,6 +297,11 @@ class Database(object): releaseinfos = migrate_data['releaseinfo'] for x in releaseinfos: info = releaseinfos[x] + + # Skip if release doesn't exist for this info + if not migrate_data['release'].get(info.get('release_id')): + continue + if not migrate_data['release'][info.get('release_id')].get('info'): migrate_data['release'][info.get('release_id')]['info'] = {} From 93eb33811a2778d00b25cbf3f1e8c6c27374e878 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 21:50:24 +0100 Subject: [PATCH 186/468] Use correct default type --- couchpotato/core/media/_base/providers/torrent/yify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index fe7a9b40..620f9d1a 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -81,7 +81,7 @@ config = [{ { 'name': 'enabled', 'type': 'enabler', - 'default': 0 + 'default': False }, { 'name': 'domain', From 1998c779c7deb641f6d10437ac516c0cd2d4e099 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 21:50:37 +0100 Subject: [PATCH 187/468] Add more trackers --- couchpotato/core/downloaders/base.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 8042c4a2..69711991 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -24,17 +24,21 @@ class Downloader(Provider): ] torrent_trackers = [ - 'http://tracker.publicbt.com/announce', 'udp://tracker.istole.it:80/announce', - 'udp://fr33domtracker.h33t.com:3310/announce', 'http://tracker.istole.it/announce', - 'http://tracker.ccc.de/announce', + 'udp://fr33domtracker.h33t.com:3310/announce', + 'http://tracker.publicbt.com/announce', 'udp://tracker.publicbt.com:80/announce', + 'http://tracker.ccc.de/announce', 'udp://tracker.ccc.de:80/announce', 'http://exodus.desync.com/announce', 'http://exodus.desync.com:6969/announce', 'http://tracker.publichd.eu/announce', + 'udp://tracker.publichd.eu:80/announce', 'http://tracker.openbittorrent.com/announce', + 'udp://tracker.openbittorrent.com/announce', + 'udp://tracker.openbittorrent.com:80/announce', + 'udp://open.demonii.com:1337/announce', ] def __init__(self): From e7420367f15e921a01bb9366d74690e538b01026 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 23 Mar 2014 21:58:08 +0100 Subject: [PATCH 188/468] Add Torrentz provider --- .../media/_base/providers/torrent/torrentz.py | 128 ++++++++++++++++++ .../media/movie/providers/torrent/torrentz.py | 15 ++ 2 files changed, 143 insertions(+) create mode 100644 couchpotato/core/media/_base/providers/torrent/torrentz.py create mode 100644 couchpotato/core/media/movie/providers/torrent/torrentz.py diff --git a/couchpotato/core/media/_base/providers/torrent/torrentz.py b/couchpotato/core/media/_base/providers/torrent/torrentz.py new file mode 100644 index 00000000..3937f64e --- /dev/null +++ b/couchpotato/core/media/_base/providers/torrent/torrentz.py @@ -0,0 +1,128 @@ +import re +import traceback + +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.helpers.variable import tryInt, splitString +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider +import six + + +log = CPLog(__name__) + + +class Base(TorrentMagnetProvider, RSS): + + urls = { + 'detail': 'https://torrentz.eu/%s', + 'search': 'https://torrentz.eu/feed?q=%s', + 'verified_search': 'https://torrentz.eu/feed_verified?q=%s' + } + + http_time_between_calls = 0 + + def _search(self, media, quality, results): + + search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search'] + + # Create search parameters + search_params = self.buildUrl(media) + + smin = quality.get('size_min') + smax = quality.get('size_max') + if smin and smax: + search_params += ' size %sm - %sm' % (smin, smax) + + min_seeds = tryInt(self.conf('minimal_seeds')) + if min_seeds: + search_params += ' seed > %s' % (min_seeds - 1) + + rss_data = self.getRSSData(search_url % search_params) + + if rss_data: + try: + + for result in rss_data: + + name = self.getTextElement(result, 'title') + detail_url = self.getTextElement(result, 'link') + description = self.getTextElement(result, 'description') + + magnet = splitString(detail_url, '/')[-1] + magnet_url = 'magnet:?xt=urn:btih:%s&dn=%s&tr=%s' % (magnet.upper(), tryUrlencode(name), tryUrlencode('udp://tracker.openbittorrent.com/announce')) + + reg = re.search('Size: (?P\d+) MB Seeds: (?P[\d,]+) Peers: (?P[\d,]+)', six.text_type(description)) + size = reg.group('size') + seeds = reg.group('seeds').replace(',', '') + peers = reg.group('peers').replace(',', '') + + results.append({ + 'id': magnet, + 'name': six.text_type(name), + 'url': magnet_url, + 'detail_url': detail_url, + 'size': tryInt(size), + 'seeders': tryInt(seeds), + 'leechers': tryInt(peers), + }) + + except: + log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) + + +config = [{ + 'name': 'torrentz', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'Torrentz', + 'description': 'Torrentz is a free, fast and powerful meta-search engine. Torrentz', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False + }, + { + 'name': 'verified_only', + 'type': 'bool', + 'default': True, + 'advanced': True, + 'description': 'Only search verified releases', + }, + { + 'name': 'minimal_seeds', + 'type': 'int', + 'default': 1, + 'advanced': True, + 'description': 'Only return releases with minimal X seeds', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + } + ] +}] diff --git a/couchpotato/core/media/movie/providers/torrent/torrentz.py b/couchpotato/core/media/movie/providers/torrent/torrentz.py new file mode 100644 index 00000000..742554c4 --- /dev/null +++ b/couchpotato/core/media/movie/providers/torrent/torrentz.py @@ -0,0 +1,15 @@ +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.logger import CPLog +from couchpotato.core.event import fireEvent +from couchpotato.core.media._base.providers.torrent.torrentz import Base +from couchpotato.core.media.movie.providers.base import MovieProvider + +log = CPLog(__name__) + +autoload = 'Torrentz' + + +class Torrentz(MovieProvider, Base): + + def buildUrl(self, media): + return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)) From 0861b21532e7ab69447ffa34354e3dda2cdf875c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 24 Mar 2014 21:40:05 +0100 Subject: [PATCH 189/468] Make sure title is valid when adding it to index --- couchpotato/core/media/_base/media/index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 42e69905..b3cd82fc 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -99,7 +99,7 @@ from couchpotato.core.helpers.encoding import simplifyString""" class TitleIndex(TreeBasedIndex): - _version = 1 + _version = 2 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -113,14 +113,14 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" return self.simplify(key) def make_key_value(self, data): - if data.get('_t') == 'media' and data.get('title') is not None: + if data.get('_t') == 'media' and data.get('title') is not None and len(data.get('title')) > 0: return self.simplify(data['title']), None def simplify(self, title): title = toUnicode(title) - nr_prefix = '' if title[0] in ascii_letters else '#' + nr_prefix = '' if title and len(title) > 0 and title[0] in ascii_letters else '#' title = simplifyString(title) for prefix in ['the ']: @@ -132,7 +132,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" class StartsWithIndex(TreeBasedIndex): - _version = 1 + _version = 2 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -158,4 +158,4 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" title = title[len(prefix):] break - return str(title[0] if title[0] in ascii_letters else '#').lower() + return str(title[0] if title and len(title) > 0 and title[0] in ascii_letters else '#').lower() From ffb3359e66ec0130a287170a4e26caed845ef2a9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 24 Mar 2014 21:41:48 +0100 Subject: [PATCH 190/468] Compact and reindex database api calls --- couchpotato/core/database.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index e65e0dec..18488677 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -21,6 +21,8 @@ class Database(object): def __init__(self): addApiView('database.list_documents', self.listDocuments) + addApiView('database.reindex', self.reindex) + addApiView('database.compact', self.compact) addApiView('database.document.update', self.updateDocument) addApiView('database.document.delete', self.deleteDocument) @@ -117,6 +119,34 @@ class Database(object): return results + def reindex(self, **kwargs): + + success = True + try: + db = self.getDB() + db.reindex() + except: + log.error('Failed index: %s', traceback.format_exc()) + success = False + + return { + 'success': success + } + + def compact(self, **kwargs): + + success = True + try: + db = self.getDB() + db.compact() + except: + log.error('Failed compact: %s', traceback.format_exc()) + success = False + + return { + 'success': success + } + def migrate(self): from couchpotato import Env From 83c5d701b36484bfca6f7ba08d61d9b42dbd1c9c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 24 Mar 2014 22:14:47 +0100 Subject: [PATCH 191/468] Use release as dict not class --- couchpotato/core/plugins/renamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 8c5a182d..59e3b7d7 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -578,11 +578,11 @@ class Renamer(Plugin): # Remove matching releases for release in remove_releases: - log.debug('Removing release %s', release.identifier) + log.debug('Removing release %s', release.get('identifier')) try: db.delete(release) except: - log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) + log.error('Failed removing %s: %s', (release, traceback.format_exc())) if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download): if media_folder: From f2250661308b61945424623db4b28b1d4746dafb Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 29 Mar 2014 01:21:05 +1300 Subject: [PATCH 192/468] [rtorrent] Removed broken ratio group usage --- couchpotato/core/downloaders/rtorrent_.py | 45 ----------------------- 1 file changed, 45 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index 98946b29..a37999ad 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -94,44 +94,6 @@ class rTorrent(Downloader): return False - def updateProviderGroup(self, name, data): - if data.get('seed_time'): - log.info('seeding time ignored, not supported') - - if not name: - return False - - if not self.connect(): - return False - - views = self.rt.get_views() - - if name not in views: - self.rt.create_group(name) - - group = self.rt.get_group(name) - - try: - if data.get('seed_ratio'): - ratio = int(float(data.get('seed_ratio')) * 100) - log.debug('Updating provider ratio to %s, group name: %s', (ratio, name)) - - # Explicitly set all group options to ensure it is setup correctly - group.set_upload('1M') - group.set_min(ratio) - group.set_max(ratio) - group.set_command('d.stop') - group.enable() - else: - # Reset group action and disable it - group.set_command() - group.disable() - except MethodError as err: - log.error('Unable to set group options: %s', err.msg) - return False - - return True - def download(self, data = None, media = None, filedata = None): if not media: media = {} @@ -142,10 +104,6 @@ class rTorrent(Downloader): if not self.connect(): return False - group_name = 'cp_' + data.get('provider').lower() - if not self.updateProviderGroup(group_name, data): - return False - torrent_params = {} if self.conf('label'): torrent_params['label'] = self.conf('label') @@ -186,9 +144,6 @@ class rTorrent(Downloader): if self.conf('directory'): torrent.set_directory(self.conf('directory')) - # Set Ratio Group - torrent.set_visible(group_name) - # Start torrent if not self.conf('paused', default = 0): torrent.start() From f99a94d685b6b59446b049f62fddf2dc4005c2ba Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 29 Mar 2014 21:25:26 +0100 Subject: [PATCH 193/468] Update unrar2 lib to 0.99.3 Fixes #2930 --- libs/unrar2/__init__.py | 2 +- libs/unrar2/unix.py | 151 ++++++++++++++++++++++++++-------------- 2 files changed, 98 insertions(+), 55 deletions(-) diff --git a/libs/unrar2/__init__.py b/libs/unrar2/__init__.py index b5c9a4d3..fe27cfe1 100644 --- a/libs/unrar2/__init__.py +++ b/libs/unrar2/__init__.py @@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a higher level interface which makes some common operations easier. """ -__version__ = '0.99.2' +__version__ = '0.99.3' try: WindowsError diff --git a/libs/unrar2/unix.py b/libs/unrar2/unix.py index 21f384cf..bd9ee859 100644 --- a/libs/unrar2/unix.py +++ b/libs/unrar2/unix.py @@ -33,115 +33,158 @@ from rar_exceptions import * class UnpackerNotInstalled(Exception): pass rar_executable_cached = None +rar_executable_version = None def call_unrar(params): "Calls rar/unrar command line executable, returns stdout pipe" global rar_executable_cached if rar_executable_cached is None: - for command in ('unrar', 'rar', os.path.join(os.path.dirname(__file__), 'unrar')): + for command in ('unrar', 'rar'): try: - subprocess.Popen([command], stdout = subprocess.PIPE) + subprocess.Popen([command], stdout=subprocess.PIPE) rar_executable_cached = command break except OSError: pass if rar_executable_cached is None: raise UnpackerNotInstalled("No suitable RAR unpacker installed") - + assert type(params) == list, "params must be list" args = [rar_executable_cached] + params try: gc.disable() # See http://bugs.python.org/issue1336 - return subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) finally: gc.enable() class RarFileImplementation(object): - def init(self, password = None): + def init(self, password=None): + global rar_executable_version self.password = password - - - + + stdoutdata, stderrdata = self.call('v', []).communicate() - + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - if line.find("CRC failed") >= 0: - raise IncorrectRARPassword + if line.find("CRC failed")>=0: + raise IncorrectRARPassword accum = [] source = iter(stdoutdata.splitlines()) line = '' - while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): - if line.strip().endswith('is not RAR archive'): - raise InvalidRARArchive + while not (line.startswith('UNRAR')): line = source.next() - while not line.startswith('Pathname/Comment'): - accum.append(line.rstrip('\n')) + signature = line + # The code below is mighty flaky + # and will probably crash on localized versions of RAR + # but I see no safe way to rewrite it using a CLI tool + if signature.startswith("UNRAR 4"): + rar_executable_version = 4 + while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): + if line.strip().endswith('is not RAR archive'): + raise InvalidRARArchive + line = source.next() + while not line.startswith('Pathname/Comment'): + accum.append(line.rstrip('\n')) + line = source.next() + if len(accum): + accum[0] = accum[0][9:] # strip out "Comment:" part + self.comment = '\n'.join(accum[:-1]) + else: + self.comment = None + elif signature.startswith("UNRAR 5"): + rar_executable_version = 5 line = source.next() - if len(accum): - accum[0] = accum[0][9:] - self.comment = '\n'.join(accum[:-1]) + while not line.startswith('Archive:'): + if line.strip().endswith('is not RAR archive'): + raise InvalidRARArchive + accum.append(line.rstrip('\n')) + line = source.next() + if len(accum): + self.comment = '\n'.join(accum[:-1]).strip() + else: + self.comment = None else: - self.comment = None - + raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: " + + signature.split(" ")[1]) + + def escaped_password(self): return '-' if self.password == None else self.password - - - def call(self, cmd, options = [], files = []): - options2 = options + ['p' + self.escaped_password()] - soptions = ['-' + x for x in options2] - return call_unrar([cmd] + soptions + ['--', self.archiveName] + files) + + + def call(self, cmd, options=[], files=[]): + options2 = options + ['p'+self.escaped_password()] + soptions = ['-'+x for x in options2] + return call_unrar([cmd]+soptions+['--',self.archiveName]+files) def infoiter(self): - - stdoutdata, stderrdata = self.call('v', ['c-']).communicate() - + + command = "v" if rar_executable_version == 4 else "l" + stdoutdata, stderrdata = self.call(command, ['c-']).communicate() + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - + accum = [] source = iter(stdoutdata.splitlines()) line = '' - while not line.startswith('--------------'): + while not line.startswith('-----------'): if line.strip().endswith('is not RAR archive'): raise InvalidRARArchive - if line.find("CRC failed") >= 0: - raise IncorrectRARPassword + if line.startswith("CRC failed") or line.startswith("Checksum error"): + raise IncorrectRARPassword line = source.next() line = source.next() i = 0 re_spaces = re.compile(r"\s+") - while not line.startswith('--------------'): - accum.append(line) - if len(accum) == 2: + if rar_executable_version == 4: + while not line.startswith('-----------'): + accum.append(line) + if len(accum)==2: + data = {} + data['index'] = i + # asterisks mark password-encrypted files + data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files + fields = re_spaces.split(accum[1].strip()) + data['size'] = int(fields[0]) + attr = fields[5] + data['isdir'] = 'd' in attr.lower() + data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M') + data['comment'] = None + yield data + accum = [] + i += 1 + line = source.next() + elif rar_executable_version == 5: + while not line.startswith('-----------'): + fields = line.strip().lstrip("*").split() data = {} data['index'] = i - data['filename'] = accum[0].strip() - info = re_spaces.split(accum[1].strip()) - data['size'] = int(info[0]) - attr = info[5] + data['filename'] = " ".join(fields[4:]) + data['size'] = int(fields[1]) + attr = fields[0] data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(info[3] + " " + info[4], '%d-%m-%y %H:%M') + data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M') data['comment'] = None yield data - accum = [] i += 1 - line = source.next() + line = source.next() + def read_files(self, checker): res = [] for info in self.infoiter(): checkres = checker(info) - if checkres == True and not info.isdir: + if checkres==True and not info.isdir: pipe = self.call('p', ['inul'], [info.filename]).stdout res.append((info, pipe.read())) - return res - + return res + def extract(self, checker, path, withSubpath, overwrite): res = [] command = 'x' @@ -151,7 +194,7 @@ class RarFileImplementation(object): if overwrite: options.append('o+') else: - options.append('o-') + options.append('o-') if not path.endswith(os.sep): path += os.sep names = [] @@ -159,17 +202,17 @@ class RarFileImplementation(object): checkres = checker(info) if type(checkres) in [str, unicode]: raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") - if checkres == True and not info.isdir: + if checkres==True and not info.isdir: names.append(info.filename) res.append(info) names.append(path) proc = self.call(command, options, names) stdoutdata, stderrdata = proc.communicate() - if stderrdata.find("CRC failed") >= 0: - raise IncorrectRARPassword - return res - + if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0: + raise IncorrectRARPassword + return res + def destruct(self): pass - + From 6152ddbd5f196da848578fbc3bfb0ab91e9f12cd Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 29 Mar 2014 21:31:59 +0100 Subject: [PATCH 194/468] Unrar cleanup --- libs/unrar2/unix.py | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/libs/unrar2/unix.py b/libs/unrar2/unix.py index bd9ee859..47129f84 100644 --- a/libs/unrar2/unix.py +++ b/libs/unrar2/unix.py @@ -41,36 +41,36 @@ def call_unrar(params): if rar_executable_cached is None: for command in ('unrar', 'rar'): try: - subprocess.Popen([command], stdout=subprocess.PIPE) + subprocess.Popen([command], stdout = subprocess.PIPE) rar_executable_cached = command break except OSError: pass if rar_executable_cached is None: raise UnpackerNotInstalled("No suitable RAR unpacker installed") - + assert type(params) == list, "params must be list" args = [rar_executable_cached] + params try: gc.disable() # See http://bugs.python.org/issue1336 - return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE) finally: gc.enable() class RarFileImplementation(object): - def init(self, password=None): + def init(self, password = None): global rar_executable_version self.password = password - - + + stdoutdata, stderrdata = self.call('v', []).communicate() - + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - if line.find("CRC failed")>=0: - raise IncorrectRARPassword + if line.find("CRC failed") >= 0: + raise IncorrectRARPassword accum = [] source = iter(stdoutdata.splitlines()) line = '' @@ -107,28 +107,28 @@ class RarFileImplementation(object): else: self.comment = None else: - raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: " + raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: " + signature.split(" ")[1]) - - + + def escaped_password(self): return '-' if self.password == None else self.password - - - def call(self, cmd, options=[], files=[]): - options2 = options + ['p'+self.escaped_password()] - soptions = ['-'+x for x in options2] - return call_unrar([cmd]+soptions+['--',self.archiveName]+files) + + + def call(self, cmd, options = [], files = []): + options2 = options + ['p' + self.escaped_password()] + soptions = ['-' + x for x in options2] + return call_unrar([cmd] + soptions + ['--', self.archiveName] + files) def infoiter(self): - + command = "v" if rar_executable_version == 4 else "l" stdoutdata, stderrdata = self.call(command, ['c-']).communicate() - + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - + accum = [] source = iter(stdoutdata.splitlines()) line = '' @@ -136,7 +136,7 @@ class RarFileImplementation(object): if line.strip().endswith('is not RAR archive'): raise InvalidRARArchive if line.startswith("CRC failed") or line.startswith("Checksum error"): - raise IncorrectRARPassword + raise IncorrectRARPassword line = source.next() line = source.next() i = 0 @@ -144,7 +144,7 @@ class RarFileImplementation(object): if rar_executable_version == 4: while not line.startswith('-----------'): accum.append(line) - if len(accum)==2: + if len(accum) == 2: data = {} data['index'] = i # asterisks mark password-encrypted files @@ -153,7 +153,7 @@ class RarFileImplementation(object): data['size'] = int(fields[0]) attr = fields[5] data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M') + data['datetime'] = time.strptime(fields[3] + " " + fields[4], '%d-%m-%y %H:%M') data['comment'] = None yield data accum = [] @@ -168,23 +168,23 @@ class RarFileImplementation(object): data['size'] = int(fields[1]) attr = fields[0] data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M') + data['datetime'] = time.strptime(fields[2] + " " + fields[3], '%d-%m-%y %H:%M') data['comment'] = None yield data i += 1 line = source.next() - + def read_files(self, checker): res = [] for info in self.infoiter(): checkres = checker(info) - if checkres==True and not info.isdir: + if checkres == True and not info.isdir: pipe = self.call('p', ['inul'], [info.filename]).stdout res.append((info, pipe.read())) - return res + return res + - def extract(self, checker, path, withSubpath, overwrite): res = [] command = 'x' @@ -202,17 +202,17 @@ class RarFileImplementation(object): checkres = checker(info) if type(checkres) in [str, unicode]: raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") - if checkres==True and not info.isdir: + if checkres == True and not info.isdir: names.append(info.filename) res.append(info) names.append(path) proc = self.call(command, options, names) stdoutdata, stderrdata = proc.communicate() - if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0: - raise IncorrectRARPassword - return res - + if stderrdata.find("CRC failed") >= 0 or stderrdata.find("Checksum error") >= 0: + raise IncorrectRARPassword + return res + def destruct(self): pass - + From 99e77e409aadce424cf90d09ddc79060eb788f71 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 Mar 2014 23:44:14 +0100 Subject: [PATCH 195/468] Spring cleanup --- couchpotato/__init__.py | 9 +- couchpotato/core/_base/_core.py | 2 +- couchpotato/core/_base/clientscript.py | 9 +- couchpotato/core/_base/updater/main.py | 6 +- .../core/_base/updater/static/updater.js | 4 +- couchpotato/core/database.py | 2 - couchpotato/core/downloaders/base.py | 1 + couchpotato/core/downloaders/nzbget.py | 2 +- couchpotato/core/downloaders/qbittorrent_.py | 1 + couchpotato/core/downloaders/rtorrent_.py | 6 +- couchpotato/core/downloaders/transmission.py | 1 + couchpotato/core/downloaders/utorrent.py | 16 +-- couchpotato/core/helpers/request.py | 3 +- couchpotato/core/helpers/variable.py | 7 +- couchpotato/core/loader.py | 13 +- couchpotato/core/media/__init__.py | 1 + .../core/media/_base/library/__init__.py | 1 + .../core/media/_base/matcher/__init__.py | 1 + couchpotato/core/media/_base/media/main.py | 8 +- .../media/_base/providers/nzb/binsearch.py | 8 +- .../core/media/_base/providers/nzb/nzbclub.py | 4 +- .../media/_base/providers/nzb/nzbindex.py | 2 +- .../media/_base/providers/nzb/omgwtfnzbs.py | 2 +- .../media/_base/providers/torrent/bithdtv.py | 6 +- .../media/_base/providers/torrent/bitsoup.py | 6 +- .../media/_base/providers/torrent/hdbits.py | 4 +- .../_base/providers/torrent/ilovetorrents.py | 10 +- .../_base/providers/torrent/iptorrents.py | 10 +- .../providers/torrent/kickasstorrents.py | 2 +- .../_base/providers/torrent/passthepopcorn.py | 36 +++--- .../media/_base/providers/torrent/publichd.py | 2 +- .../_base/providers/torrent/sceneaccess.py | 18 +-- .../_base/providers/torrent/thepiratebay.py | 2 +- .../_base/providers/torrent/torrentbytes.py | 6 +- .../_base/providers/torrent/torrentday.py | 2 +- .../_base/providers/torrent/torrentleech.py | 14 +-- .../_base/providers/torrent/torrentshack.py | 10 +- .../media/_base/providers/torrent/yify.py | 2 +- .../core/media/_base/search/static/search.css | 46 ++++--- .../core/media/_base/search/static/search.js | 46 +++---- couchpotato/core/media/_base/searcher/main.py | 3 +- .../core/media/movie/_base/static/list.js | 60 ++++----- .../media/movie/_base/static/movie.actions.js | 40 ++---- .../core/media/movie/_base/static/movie.css | 5 - .../core/media/movie/_base/static/movie.js | 42 +++---- .../core/media/movie/_base/static/search.js | 18 +-- .../movie/providers/automation/bluray.py | 2 +- .../movie/providers/automation/goodfilms.py | 5 +- .../movie/providers/automation/letterboxd.py | 5 +- .../media/movie/providers/info/themoviedb.py | 1 + .../media/movie/providers/torrent/bithdtv.py | 2 +- .../movie/providers/torrent/passthepopcorn.py | 44 +++---- .../movie/providers/torrent/thepiratebay.py | 10 +- .../movie/providers/torrent/torrentday.py | 1 + .../movie/providers/trailer/hdtrailers.py | 2 +- couchpotato/core/media/movie/searcher.py | 1 - .../media/movie/suggestion/static/suggest.css | 9 +- .../media/movie/suggestion/static/suggest.js | 4 +- couchpotato/core/notifications/core/main.py | 2 +- .../notifications/core/static/notification.js | 26 ++-- couchpotato/core/notifications/email_.py | 3 +- couchpotato/core/notifications/growl.py | 2 + couchpotato/core/notifications/nmj.py | 1 + couchpotato/core/notifications/pushalot.py | 2 +- couchpotato/core/notifications/pushover.py | 7 +- .../notifications/twitter/static/twitter.js | 2 +- couchpotato/core/notifications/xbmc.py | 2 +- couchpotato/core/plugins/base.py | 7 +- couchpotato/core/plugins/browser.py | 3 +- .../core/plugins/category/static/category.js | 26 ++-- couchpotato/core/plugins/dashboard.py | 4 +- couchpotato/core/plugins/file.py | 3 +- couchpotato/core/plugins/log/main.py | 6 +- couchpotato/core/plugins/log/static/log.css | 2 +- couchpotato/core/plugins/log/static/log.js | 7 +- couchpotato/core/plugins/manage.py | 8 +- .../core/plugins/profile/static/profile.js | 12 +- couchpotato/core/plugins/quality/main.py | 5 +- .../core/plugins/quality/static/quality.js | 18 +-- couchpotato/core/plugins/renamer.py | 12 +- couchpotato/core/plugins/scanner.py | 7 +- .../{bookmark.js => bookmark.js_tmpl} | 2 +- couchpotato/core/plugins/userscript/main.py | 4 +- .../plugins/userscript/static/userscript.js | 8 +- .../{template.js => template.js_tmpl} | 0 .../core/plugins/wizard/static/wizard.css | 5 +- .../core/plugins/wizard/static/wizard.js | 16 +-- couchpotato/core/settings.py | 6 +- couchpotato/runner.py | 16 +-- couchpotato/static/scripts/couchpotato.js | 2 +- .../static/scripts/misc/downloaders.js | 2 +- couchpotato/static/scripts/page/home.js | 2 +- couchpotato/static/scripts/page/manage.js | 8 +- couchpotato/static/scripts/page/settings.js | 7 +- couchpotato/static/scripts/page/wanted.js | 10 +- couchpotato/static/style/main.css | 114 +++++++++--------- couchpotato/static/style/settings.css | 2 +- couchpotato/templates/database.html | 2 +- 98 files changed, 469 insertions(+), 489 deletions(-) rename couchpotato/core/plugins/userscript/{bookmark.js => bookmark.js_tmpl} (95%) rename couchpotato/core/plugins/userscript/{template.js => template.js_tmpl} (100%) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 1946963b..a16a1f5f 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -15,6 +15,7 @@ log = CPLog(__name__) views = {} template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) + class BaseHandler(RequestHandler): def get_current_user(self): @@ -54,7 +55,7 @@ def get_db(): # Web view def index(): - return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) + return template_loader.load('index.thtml').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) addView('', index) @@ -66,14 +67,14 @@ def apiDocs(): del api_docs[''] del api_docs_missing[''] - return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) + return template_loader.load('api.thtml').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) addView('docs', apiDocs) # Database debug manager def databaseManage(): - return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env) + return template_loader.load('database.thtml').generate(fireEvent = fireEvent, Env = Env) addView('database', databaseManage) @@ -107,7 +108,7 @@ class LoginHandler(BaseHandler): if self.get_current_user(): self.redirect(Env.get('web_base')) else: - self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)) + self.write(template_loader.load('login.thtml').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)) def post(self, *args, **kwargs): diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 0a98103b..1ca7d878 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -189,7 +189,7 @@ class Core(Plugin): def signalHandler(self): if Env.get('daemonized'): return - def signal_handler(signal, frame): + def signal_handler(*args, **kwargs): fireEvent('app.shutdown', single = True) signal.signal(signal.SIGINT, signal_handler) diff --git a/couchpotato/core/_base/clientscript.py b/couchpotato/core/_base/clientscript.py index 593593af..603c57dc 100644 --- a/couchpotato/core/_base/clientscript.py +++ b/couchpotato/core/_base/clientscript.py @@ -57,13 +57,12 @@ class ClientScript(Plugin): ], } - urls = {'style': {}, 'script': {}} minified = {'style': {}, 'script': {}} paths = {'style': {}, 'script': {}} comment = { - 'style': '/*** %s:%d ***/\n', - 'script': '// %s:%d\n' + 'style': '/*** %s:%d ***/\n', + 'script': '// %s:%d\n' } html = { @@ -95,7 +94,6 @@ class ClientScript(Plugin): else: self.registerStyle(core_url, file_path, position = 'front') - def minify(self): # Create cache dir @@ -129,7 +127,7 @@ class ClientScript(Plugin): data = cssmin(data) data = data.replace('../images/', '../static/images/') data = data.replace('../fonts/', '../static/fonts/') - data = data.replace('../../static/', '../static/') # Replace inside plugins + data = data.replace('../../static/', '../static/') # Replace inside plugins raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) @@ -192,6 +190,7 @@ class ClientScript(Plugin): prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow'] prefix_tags = ['ms', 'moz', 'webkit'] + def prefix(self, data): trimmed_data = re.sub('(\t|\n|\r)+', '', data) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index cba91b7e..cde17eb2 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -164,14 +164,14 @@ class BaseUpdater(Plugin): def info(self): - version = self.getVersion() + current_version = self.getVersion() return { 'last_check': self.last_check, 'update_version': self.update_version, - 'version': version, + 'version': current_version, 'repo_name': '%s/%s' % (self.repo_user, self.repo_name), - 'branch': version.get('branch', self.branch), + 'branch': current_version.get('branch', self.branch), } def getVersion(self): diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js index be436ed2..331e0707 100644 --- a/couchpotato/core/_base/updater/static/updater.js +++ b/couchpotato/core/_base/updater/static/updater.js @@ -5,7 +5,7 @@ var UpdaterBase = new Class({ initialize: function(){ var self = this; - App.addEvent('load', self.info.bind(self, 2000)) + App.addEvent('load', self.info.bind(self, 2000)); App.addEvent('unload', function(){ if(self.timer) clearTimeout(self.timer); @@ -66,7 +66,7 @@ var UpdaterBase = new Class({ var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch; if(data.update_version.changelog) - changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash + changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash; self.message = new Element('div.message.update').adopt( new Element('span', { diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 18488677..532befae 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -116,7 +116,6 @@ class Database(object): results[key] = [] results[key].append(document) - return results def reindex(self, **kwargs): @@ -473,7 +472,6 @@ class Database(object): log.info('Total migration took %s', time.time() - migrate_start) log.info('=' * 30) - # rename old database log.info('Renaming old database to %s ', old_db + '.old') os.rename(old_db, old_db + '.old') diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 69711991..dfbff9ff 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -188,6 +188,7 @@ class Downloader(Provider): def pause(self, release_download, pause): return + class ReleaseDownloadList(list): provider = None diff --git a/couchpotato/core/downloaders/nzbget.py b/couchpotato/core/downloaders/nzbget.py index 115e0d24..cfa75985 100644 --- a/couchpotato/core/downloaders/nzbget.py +++ b/couchpotato/core/downloaders/nzbget.py @@ -146,7 +146,7 @@ class NZBGet(Downloader): 'timeleft': timeleft, }) - for nzb in queue: # 'Parameters' is not passed in rpc.postqueue + for nzb in queue: # 'Parameters' is not passed in rpc.postqueue if nzb['NZBID'] in ids: log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) release_downloads.append({ diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index ac48d327..0f88beba 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -14,6 +14,7 @@ log = CPLog(__name__) autoload = 'qBittorrent' + class qBittorrent(Downloader): protocol = ['torrent', 'torrent_magnet'] diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index 98946b29..04b2a620 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -78,10 +78,10 @@ class rTorrent(Downloader): self.error_msg = '' try: - self.rt._verify_conn() + self.rt._verify_conn() except AssertionError as e: - self.error_msg = e.message - self.rt = None + self.error_msg = e.message + self.rt = None return self.rt diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index dd11166d..5c25dcf4 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -160,6 +160,7 @@ class Transmission(Downloader): log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) return self.trpc.remove_torrent(release_download['id'], delete_files) + class TransmissionRPC(object): """TransmissionRPC lite library""" diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index 671dd9ec..83df48ea 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -29,14 +29,14 @@ class uTorrent(Downloader): protocol = ['torrent', 'torrent_magnet'] utorrent_api = None status_flags = { - 'STARTED' : 1, - 'CHECKING' : 2, - 'CHECK-START' : 4, - 'CHECKED' : 8, - 'ERROR' : 16, - 'PAUSED' : 32, - 'QUEUED' : 64, - 'LOADED' : 128 + 'STARTED': 1, + 'CHECKING': 2, + 'CHECK-START': 4, + 'CHECKED': 8, + 'ERROR': 16, + 'PAUSED': 32, + 'QUEUED': 64, + 'LOADED': 128 } def connect(self): diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 4cad8848..4c0add18 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -2,7 +2,7 @@ from urllib import unquote import re from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import natsortKey, tryInt +from couchpotato.core.helpers.variable import natsortKey def getParams(params): @@ -44,6 +44,7 @@ def getParams(params): return dictToList(temp) non_decimal = re.compile(r'[^\d.]+') + def dictToList(params): if type(params) is dict: diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 34e66e94..459d0e9b 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -216,6 +216,7 @@ def tryFloat(s): return float(s) except: return 0 + def natsortKey(string_): """See http://www.codinghorror.com/blog/archives/001018.html""" return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)] @@ -247,9 +248,6 @@ def getTitle(media_dict): except: log.error('Could not get title for %s', getIdentifier(media_dict)) return None - - log.error('Could not get title for %s', getIdentifier(media_dict)) - return None except: log.error('Could not get title for library item: %s', media_dict) return None @@ -296,8 +294,11 @@ def isSubFolder(sub_folder, base_folder): # Returns True if sub_folder is the same as or inside base_folder return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep) + # From SABNZBD re_password = [re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)] + + def scanForPassword(name): m = None for reg in re_password: diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 6d8d14ac..11df37b4 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -12,19 +12,22 @@ log = CPLog(__name__) class Loader(object): - plugins = {} - providers = {} - modules = {} + + def __init__(self): + self.plugins = {} + self.providers = {} + self.modules = {} + self.paths = {} def preload(self, root = ''): core = os.path.join(root, 'couchpotato', 'core') - self.paths = { + self.paths.update({ 'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')), 'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')), 'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')), 'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')), - } + }) # Add media to loader self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 8e704dec..c491c016 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -5,6 +5,7 @@ from couchpotato import get_db, CPLog from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.plugins.base import Plugin +import six log = CPLog(__name__) diff --git a/couchpotato/core/media/_base/library/__init__.py b/couchpotato/core/media/_base/library/__init__.py index 3e1babe4..a404f81c 100644 --- a/couchpotato/core/media/_base/library/__init__.py +++ b/couchpotato/core/media/_base/library/__init__.py @@ -1,5 +1,6 @@ from .main import Library + def autoload(): return Library() diff --git a/couchpotato/core/media/_base/matcher/__init__.py b/couchpotato/core/media/_base/matcher/__init__.py index 1e4cda37..c8b1e821 100644 --- a/couchpotato/core/media/_base/matcher/__init__.py +++ b/couchpotato/core/media/_base/matcher/__init__.py @@ -1,5 +1,6 @@ from .main import Matcher + def autoload(): return Matcher() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 5b690cfa..d452839b 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -222,7 +222,6 @@ class MediaPlugin(MediaBase): del filter_by['media_status'] del filter_by['release_status'] - # Filter by combining ids for x in filter_by: media_ids = [n for n in media_ids if n in filter_by[x]] @@ -281,7 +280,7 @@ class MediaPlugin(MediaBase): for media_type in fireEvent('media.types', merge = True): def tempList(*args, **kwargs): - return self.listView(types = media_type, *args, **kwargs) + return self.listView(types = media_type, **kwargs) addApiView('%s.list' % media_type, tempList) def availableChars(self, types = None, status = None, release_status = None): @@ -350,7 +349,7 @@ class MediaPlugin(MediaBase): for media_type in fireEvent('media.types', merge = True): def tempChar(*args, **kwargs): - return self.charView(types = media_type, *args, **kwargs) + return self.charView(types = media_type, **kwargs) addApiView('%s.available_chars' % media_type, tempChar) def delete(self, media_id, delete_from = None): @@ -382,9 +381,8 @@ class MediaPlugin(MediaBase): if release.get('status') == 'done': db.delete(release) total_deleted += 1 - new_media_status = 'active' - if total_releases == total_deleted: + if total_releases == total_deleted and media['status'] != 'active': db.delete(media) deleted = True elif new_media_status: diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index 90e403d2..90778af8 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -18,7 +18,7 @@ class Base(NZBProvider): 'search': 'https://www.binsearch.info/index.php?%s', } - http_time_between_calls = 4 # Seconds + http_time_between_calls = 4 # Seconds def _search(self, media, quality, results): @@ -28,7 +28,7 @@ class Base(NZBProvider): try: html = BeautifulSoup(data) - main_table = html.find('table', attrs = {'id':'r2'}) + main_table = html.find('table', attrs = {'id': 'r2'}) if not main_table: return @@ -36,11 +36,11 @@ class Base(NZBProvider): items = main_table.find_all('tr') for row in items: - title = row.find('span', attrs = {'class':'s'}) + title = row.find('span', attrs = {'class': 's'}) if not title: continue - nzb_id = row.find('input', attrs = {'type':'checkbox'})['name'] + nzb_id = row.find('input', attrs = {'type': 'checkbox'})['name'] info = row.find('span', attrs = {'class':'d'}) size_match = re.search('size:.(?P[0-9\.]+.[GMB]+)', info.text) diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub.py b/couchpotato/core/media/_base/providers/nzb/nzbclub.py index 6ca66718..06ae7fec 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub.py @@ -18,7 +18,7 @@ class Base(NZBProvider, RSS): 'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s', } - http_time_between_calls = 4 #seconds + http_time_between_calls = 4 # seconds def _search(self, media, quality, results): @@ -55,7 +55,7 @@ class Base(NZBProvider, RSS): def getMoreInfo(self, item): full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) - nfo_pre = html.find('pre', attrs = {'class':'nfo'}) + nfo_pre = html.find('pre', attrs = {'class': 'nfo'}) description = toUnicode(nfo_pre.text) if nfo_pre else '' item['description'] = description diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex.py b/couchpotato/core/media/_base/providers/nzb/nzbindex.py index 8baa59a4..ddcce8ad 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex.py @@ -91,7 +91,7 @@ class Base(NZBProvider, RSS): nfo_url = re.search('href=\"(?P.+)\" ', item['description']).group('nfo') full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000) html = BeautifulSoup(full_description) - item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text) + item['description'] = toUnicode(html.find('pre', attrs = {'id': 'nfo0'}).text) except: pass diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index fe0338a5..acd12cf9 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -20,7 +20,7 @@ class Base(NZBProvider, RSS): 'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds cat_ids = [ ([15], ['dvdrip']), diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py index b55bfff0..94aafcd1 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -21,7 +21,7 @@ class Base(TorrentProvider): } # Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _search(self, media, quality, results): @@ -40,7 +40,7 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - result_table = html.find('table', attrs = {'width' : '750', 'class' : ''}) + result_table = html.find('table', attrs = {'width': '750', 'class': ''}) if result_table is None: return @@ -74,7 +74,7 @@ class Base(TorrentProvider): def getMoreInfo(self, item): full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) - nfo_pre = html.find('table', attrs = {'class':'detail'}) + nfo_pre = html.find('table', attrs = {'class': 'detail'}) description = toUnicode(nfo_pre.text) if nfo_pre else '' item['description'] = description diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index d841c0ff..9aeb6243 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -20,7 +20,7 @@ class Base(TorrentProvider): 'baseurl': 'https://www.bitsoup.me/%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _searchOnTitle(self, title, movie, quality, results): @@ -30,7 +30,7 @@ class Base(TorrentProvider): }) url = "%s&%s" % (self.urls['search'], arguments) - url = self.urls['search'] % self.buildUrl(media, quality) + url = self.urls['search'] % self.buildUrl(movie, quality) data = self.getHTMLData(url) if data: @@ -74,7 +74,6 @@ class Base(TorrentProvider): except: log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) - def getLoginParams(self): return { 'username': self.conf('username'), @@ -82,7 +81,6 @@ class Base(TorrentProvider): 'ssl': 'yes', } - def loginSuccess(self, output): return 'logout.php' in output.lower() diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index c2e85ac5..6cf8d57d 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -19,7 +19,7 @@ class Base(TorrentProvider): 'api': 'https://hdbits.org/api/torrents' } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _post_query(self, **params): @@ -56,7 +56,7 @@ class Base(TorrentProvider): 'name': result['name'], 'url': self.urls['download'] % (result['id'], self.conf('passkey')), 'detail_url': self.urls['detail'] % result['id'], - 'size': tryInt(result['size'])/1024/1024, + 'size': tryInt(result['size']) / 1024 / 1024, 'seeders': tryInt(result['seeders']), 'leechers': tryInt(result['leechers']) }) diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index 9a9cd63a..56b7dac5 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -23,9 +23,9 @@ class Base(TorrentProvider): } cat_ids = [ - (['41'], ['720p', '1080p', 'brrip']), - (['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), - (['20'], ['dvdr']) + (['41'], ['720p', '1080p', 'brrip']), + (['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + (['20'], ['dvdr']) ] cat_backup_id = 200 @@ -79,7 +79,7 @@ class Base(TorrentProvider): return confirmed + trusted + vip + moderated id = re.search('id=(?P\d+)&', link).group('id') - url = self.urls['download'] % (download) + url = self.urls['download'] % download fileSize = self.parseSize(result.select('td.rowhead')[5].text) results.append({ @@ -113,7 +113,7 @@ class Base(TorrentProvider): try: full_description = self.getHTMLData(item['detail_url']) html = BeautifulSoup(full_description) - nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1] + nfo_pre = html.find('td', attrs = {'class': 'main'}).findAll('table')[1] description = toUnicode(nfo_pre.text) if nfo_pre else '' except: log.error('Failed getting more info for %s', item['name']) diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index d36ba799..29580d49 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -21,7 +21,7 @@ class Base(TorrentProvider): 'search': 'https://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds cat_backup_id = None def buildUrl(self, title, media, quality): @@ -53,14 +53,14 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - page_nav = html.find('span', attrs = {'class' : 'page_nav'}) + page_nav = html.find('span', attrs = {'class': 'page_nav'}) if page_nav: next_link = page_nav.find("a", text = "Next") if next_link: final_page_link = next_link.previous_sibling.previous_sibling pages = int(final_page_link.string) - result_table = html.find('table', attrs = {'class' : 'torrents'}) + result_table = html.find('table', attrs = {'class': 'torrents'}) if not result_table or 'nothing found!' in data.lower(): return @@ -80,8 +80,8 @@ class Base(TorrentProvider): torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.') torrent_details_url = self.urls['base_url'] + torrent['href'] torrent_size = self.parseSize(result.find_all('td')[5].string) - torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string) - torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string) + torrent_seeders = tryInt(result.find('td', attrs = {'class': 'ac t_seeders'}).string) + torrent_leechers = tryInt(result.find('td', attrs = {'class': 'ac t_leechers'}).string) results.append({ 'id': torrent_id, diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 1cfb5700..534dc926 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -26,7 +26,7 @@ class Base(TorrentMagnetProvider): (['dvd'], ['dvdr']), ] - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds cat_backup_id = None proxy_list = [ diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index 5368ad35..80e23488 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -18,12 +18,12 @@ log = CPLog(__name__) class Base(TorrentProvider): urls = { - 'domain': 'https://tls.passthepopcorn.me', - 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', - 'torrent': 'https://tls.passthepopcorn.me/torrents.php', - 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', - 'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login', - 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' + 'domain': 'https://tls.passthepopcorn.me', + 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', + 'torrent': 'https://tls.passthepopcorn.me/torrents.php', + 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', + 'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login', + 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' } http_time_between_calls = 2 @@ -136,23 +136,23 @@ class Base(TorrentProvider): def htmlToUnicode(self, text): def fixup(m): - text = m.group(0) - if text[:2] == "&#": + txt = m.group(0) + if txt[:2] == "&#": # character reference try: - if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) + if txt[:3] == "&#x": + return unichr(int(txt[3:-1], 16)) else: - return unichr(int(text[2:-1])) + return unichr(int(txt[2:-1])) except ValueError: pass else: # named entity try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]]) except KeyError: pass - return text # leave as is + return txt # leave as is return re.sub("&#?\w+;", fixup, six.u('%s') % text) def unicodeToASCII(self, text): @@ -164,11 +164,11 @@ class Base(TorrentProvider): def getLoginParams(self): return { - 'username': self.conf('username'), - 'password': self.conf('password'), - 'passkey': self.conf('passkey'), - 'keeplogged': '1', - 'login': 'Login' + 'username': self.conf('username'), + 'password': self.conf('password'), + 'passkey': self.conf('passkey'), + 'keeplogged': '1', + 'login': 'Login' } def loginSuccess(self, output): diff --git a/couchpotato/core/media/_base/providers/torrent/publichd.py b/couchpotato/core/media/_base/providers/torrent/publichd.py index c17b655a..1090be2b 100644 --- a/couchpotato/core/media/_base/providers/torrent/publichd.py +++ b/couchpotato/core/media/_base/providers/torrent/publichd.py @@ -34,7 +34,7 @@ class Base(TorrentMagnetProvider): query = self.buildUrl(media) params = tryUrlencode({ - 'page':'torrents', + 'page': 'torrents', 'search': query, 'active': 1, }) diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py index 9a27b051..2622e318 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -22,7 +22,7 @@ class Base(TorrentProvider): 'download': 'https://www.sceneaccess.eu/%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _search(self, media, quality, results): @@ -33,16 +33,16 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - resultsTable = html.find('table', attrs = {'id' : 'torrents-table'}) + resultsTable = html.find('table', attrs = {'id': 'torrents-table'}) if resultsTable is None: return - entries = resultsTable.find_all('tr', attrs = {'class' : 'tt_row'}) + entries = resultsTable.find_all('tr', attrs = {'class': 'tt_row'}) for result in entries: - link = result.find('td', attrs = {'class' : 'ttr_name'}).find('a') - url = result.find('td', attrs = {'class' : 'td_dl'}).find('a') - leechers = result.find('td', attrs = {'class' : 'ttr_leechers'}).find('a') + link = result.find('td', attrs = {'class': 'ttr_name'}).find('a') + url = result.find('td', attrs = {'class': 'td_dl'}).find('a') + leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a') torrent_id = link['href'].replace('details?id=', '') results.append({ @@ -50,8 +50,8 @@ class Base(TorrentProvider): 'name': link['title'], 'url': self.urls['download'] % url['href'], 'detail_url': self.urls['detail'] % torrent_id, - 'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]), - 'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string), + 'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]), + 'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string), 'leechers': tryInt(leechers.string) if leechers else 0, 'get_more_info': self.getMoreInfo, }) @@ -62,7 +62,7 @@ class Base(TorrentProvider): def getMoreInfo(self, item): full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) - nfo_pre = html.find('div', attrs = {'id':'details_table'}) + nfo_pre = html.find('div', attrs = {'id': 'details_table'}) description = toUnicode(nfo_pre.text) if nfo_pre else '' item['description'] = description diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 26eeb6e1..d401e6b3 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -108,7 +108,7 @@ class Base(TorrentMagnetProvider): def getMoreInfo(self, item): full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) - nfo_pre = html.find('div', attrs = {'class':'nfo'}) + nfo_pre = html.find('div', attrs = {'class': 'nfo'}) description = toUnicode(nfo_pre.text) if nfo_pre else '' item['description'] = description diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py index c01a6d7f..85a4fda1 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -31,7 +31,7 @@ class Base(TorrentProvider): ([20], ['dvdr']), ] - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds cat_backup_id = None def _searchOnTitle(self, title, movie, quality, results): @@ -43,7 +43,7 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - result_table = html.find('table', attrs = {'border' : '1'}) + result_table = html.find('table', attrs = {'border': '1'}) if not result_table: return @@ -52,7 +52,7 @@ class Base(TorrentProvider): for result in entries[1:]: cells = result.find_all('td') - link = cells[1].find('a', attrs = {'class' : 'index'}) + link = cells[1].find('a', attrs = {'class': 'index'}) full_id = link['href'].replace('details.php?id=', '') torrent_id = full_id[:6] diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index dfb605bd..bcc0593b 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -16,7 +16,7 @@ class Base(TorrentProvider): 'download': 'http://www.td.af/download.php/%s/%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _search(self, media, quality, results): diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py index 208520df..fe427985 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -21,7 +21,7 @@ class Base(TorrentProvider): 'download': 'http://www.torrentleech.org%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds cat_backup_id = None def _search(self, media, quality, results): @@ -34,7 +34,7 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - result_table = html.find('table', attrs = {'id' : 'torrenttable'}) + result_table = html.find('table', attrs = {'id': 'torrenttable'}) if not result_table: return @@ -42,9 +42,9 @@ class Base(TorrentProvider): for result in entries[1:]: - link = result.find('td', attrs = {'class' : 'name'}).find('a') - url = result.find('td', attrs = {'class' : 'quickdownload'}).find('a') - details = result.find('td', attrs = {'class' : 'name'}).find('a') + link = result.find('td', attrs = {'class': 'name'}).find('a') + url = result.find('td', attrs = {'class': 'quickdownload'}).find('a') + details = result.find('td', attrs = {'class': 'name'}).find('a') results.append({ 'id': link['href'].replace('/torrent/', ''), @@ -52,8 +52,8 @@ class Base(TorrentProvider): 'url': self.urls['download'] % url['href'], 'detail_url': self.urls['download'] % details['href'], 'size': self.parseSize(result.find_all('td')[4].string), - 'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string), - 'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string), + 'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string), + 'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string), }) except: diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index 6c9d4cc5..2a285b2a 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -21,7 +21,7 @@ class Base(TorrentProvider): 'download': 'https://torrentshack.net/%s', } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # Seconds def _search(self, media, quality, results): @@ -32,16 +32,16 @@ class Base(TorrentProvider): html = BeautifulSoup(data) try: - result_table = html.find('table', attrs = {'id' : 'torrent_table'}) + result_table = html.find('table', attrs = {'id': 'torrent_table'}) if not result_table: return - entries = result_table.find_all('tr', attrs = {'class' : 'torrent'}) + entries = result_table.find_all('tr', attrs = {'class': 'torrent'}) for result in entries: - link = result.find('span', attrs = {'class' : 'torrent_name_link'}).parent - url = result.find('td', attrs = {'class' : 'torrent_td'}).find('a') + link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent + url = result.find('td', attrs = {'class': 'torrent_td'}).find('a') results.append({ 'id': link['href'].replace('torrents.php?torrentid=', ''), diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 620f9d1a..a6941004 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -16,7 +16,7 @@ class Base(TorrentProvider): 'detail': '%s/api/movie.json?id=%s' } - http_time_between_calls = 1 #seconds + http_time_between_calls = 1 # seconds proxy_list = [ 'http://yify.unlocktorrent.com', diff --git a/couchpotato/core/media/_base/search/static/search.css b/couchpotato/core/media/_base/search/static/search.css index 57210d68..2defe24d 100644 --- a/couchpotato/core/media/_base/search/static/search.css +++ b/couchpotato/core/media/_base/search/static/search.css @@ -6,12 +6,11 @@ top: 0; text-align: right; height: 100%; - border-bottom: 4px solid transparent; transition: all .4s cubic-bezier(0.9,0,0.1,1); position: absolute; z-index: 20; - border: 1px solid transparent; - border-width: 0 0 4px; + border: 0 solid transparent; + border-bottom-width: 4px; } .search_form:hover { border-color: #047792; @@ -22,19 +21,19 @@ right: 44px; } } - + .search_form.focused, .search_form.shown { border-color: #04bce6; } - + .search_form .input { height: 100%; overflow: hidden; width: 45px; transition: all .4s cubic-bezier(0.9,0,0.1,1); } - + .search_form.focused .input, .search_form.shown .input { width: 380px; @@ -49,7 +48,6 @@ color: #FFF; font-size: 25px; height: 100%; - padding: 10px; width: 100%; opacity: 0; padding: 0 40px 0 10px; @@ -59,23 +57,23 @@ .search_form.shown .input input { opacity: 1; } - + .search_form input::-ms-clear { width : 0; height: 0; } - + @media all and (max-width: 480px) { .search_form .input input { font-size: 15px; } - + .search_form.focused .input, .search_form.shown .input { width: 277px; } } - + .search_form .input a { position: absolute; top: 0; @@ -89,7 +87,7 @@ font-size: 15px; color: #FFF; } - + .search_form .input a:after { content: "\e03e"; } @@ -97,7 +95,7 @@ .search_form.shown.filled .input a:after { content: "\e04e"; } - + @media all and (max-width: 480px) { .search_form .input a { line-height: 44px; @@ -167,13 +165,13 @@ .media_result .options select[name=title] { width: 170px; } .media_result .options select[name=profile] { width: 90px; } .media_result .options select[name=category] { width: 80px; } - + @media all and (max-width: 480px) { - + .media_result .options select[name=title] { width: 90px; } .media_result .options select[name=profile] { width: 50px; } .media_result .options select[name=category] { width: 50px; } - + } .media_result .options .button { @@ -227,14 +225,14 @@ right: 7px; vertical-align: middle; } - + .media_result .info h2 { margin: 0; font-weight: normal; font-size: 20px; padding: 0; } - + .search_form .info h2 { position: absolute; width: 100%; @@ -247,12 +245,12 @@ overflow: hidden; white-space: nowrap; } - + .search_form .info h2 .title { position: absolute; width: 88%; } - + .media_result .info h2 .year { padding: 0 5px; text-align: center; @@ -260,14 +258,14 @@ width: 12%; right: 0; } - + @media all and (max-width: 480px) { - + .search_form .info h2 .year { font-size: 12px; margin-top: 7px; } - + } .search_form .mask, @@ -277,4 +275,4 @@ width: 100%; left: 0; top: 0; -} \ No newline at end of file +} diff --git a/couchpotato/core/media/_base/search/static/search.js b/couchpotato/core/media/_base/search/static/search.js index 470dcf0b..1892ad1e 100644 --- a/couchpotato/core/media/_base/search/static/search.js +++ b/couchpotato/core/media/_base/search/static/search.js @@ -16,7 +16,7 @@ Block.Search = new Class({ 'keyup': self.keyup.bind(self), 'focus': function(){ if(focus_timer) clearTimeout(focus_timer); - self.el.addClass('focused') + self.el.addClass('focused'); if(this.get('value')) self.hideResults(false) }, @@ -57,17 +57,17 @@ Block.Search = new Class({ (e).preventDefault(); if(self.last_q === ''){ - self.input.blur() + self.input.blur(); self.last_q = null; } else { self.last_q = ''; self.input.set('value', ''); - self.input.focus() + self.input.focus(); - self.media = {} - self.results.empty() + self.media = {}; + self.results.empty(); self.el.removeClass('filled') } @@ -92,16 +92,16 @@ Block.Search = new Class({ self.hidden = bool; }, - keyup: function(e){ + keyup: function(){ var self = this; - self.el[self.q() ? 'addClass' : 'removeClass']('filled') + self.el[self.q() ? 'addClass' : 'removeClass']('filled'); if(self.q() != self.last_q){ if(self.api_request && self.api_request.isRunning()) self.api_request.cancel(); - if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) + if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer); self.autocomplete_timer = self.autocomplete.delay(300, self) } @@ -111,7 +111,7 @@ Block.Search = new Class({ var self = this; if(!self.q()){ - self.hideResults(true) + self.hideResults(true); return } @@ -139,7 +139,7 @@ Block.Search = new Class({ }) } else - self.fill(q, cache) + self.fill(q, cache); self.last_q = q; @@ -148,31 +148,31 @@ Block.Search = new Class({ fill: function(q, json){ var self = this; - self.cache[q] = json + self.cache[q] = json; - self.media = {} - self.results.empty() - - Object.each(json, function(media, type){ + self.media = {}; + self.results.empty(); + + Object.each(json, function(media){ if(typeOf(media) == 'array'){ Object.each(media, function(m){ - + var m = new Block.Search[m.type.capitalize() + 'Item'](m); - $(m).inject(self.results) - self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m - + $(m).inject(self.results); + self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m; + if(q == m.imdb) m.showOptions() - + }); } - }) + }); // Calculate result heights var w = window.getSize(), rc = self.result_container.getCoordinates(); - self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') + self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px'); self.mask.fade('out') }, @@ -185,4 +185,4 @@ Block.Search = new Class({ return this.input.get('value').trim(); } -}); \ No newline at end of file +}); diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 6ed43364..fc07ded2 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -14,6 +14,7 @@ log = CPLog(__name__) class Searcher(SearcherBase): + # noinspection PyMissingConstructor def __init__(self): addEvent('searcher.protocols', self.getSearchProtocols) addEvent('searcher.contains_other_quality', self.containsOtherQuality) @@ -195,7 +196,7 @@ class Searcher(SearcherBase): req = splitString(req_set, '&') req_match += len(list(set(rel_words) & set(req))) == len(req) - if len(required_words) > 0 and req_match == 0: + if len(required_words) > 0 and req_match == 0: log.info2('Wrong: Required word missing: %s', rel_name) return False diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index ce405957..1e8a38f4 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -26,7 +26,7 @@ var MovieList = new Class({ self.filter = self.options.filter || { 'starts_with': null, 'search': null - } + }; self.el = new Element('div.movies').adopt( self.title = self.options.title ? new Element('h2', { @@ -52,7 +52,7 @@ var MovieList = new Class({ self.getMovies(); - App.on('movie.added', self.movieAdded.bind(self)) + App.on('movie.added', self.movieAdded.bind(self)); App.on('movie.deleted', self.movieDeleted.bind(self)) }, @@ -96,7 +96,7 @@ var MovieList = new Class({ if(self.options.load_more) self.scrollspy = new ScrollSpy({ min: function(){ - var c = self.load_more.getCoordinates() + var c = self.load_more.getCoordinates(); return c.top - window.document.getSize().y - 300 }, onEnter: self.loadMore.bind(self) @@ -179,7 +179,7 @@ var MovieList = new Class({ m.fireEvent('injected'); - self.movies.include(m) + self.movies.include(m); self.movies_added[movie._id] = true; }, @@ -187,7 +187,7 @@ var MovieList = new Class({ var self = this; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - self.el.addClass('with_navigation') + self.el.addClass('with_navigation'); self.navigation = new Element('div.alph_nav').adopt( self.mass_edit_form = new Element('div.mass_edit_form').adopt( @@ -242,7 +242,7 @@ var MovieList = new Class({ this.addClass(a); el.inject(el.getParent(), 'top'); - el.getSiblings().hide() + el.getSiblings().hide(); setTimeout(function(){ el.getSiblings().setStyle('display', null); }, 100) @@ -286,7 +286,7 @@ var MovieList = new Class({ 'status': self.options.status }, self.filter), 'onSuccess': function(json){ - available_chars = json.chars + available_chars = json.chars; available_chars.each(function(c){ self.letters[c.capitalize()].addClass('available') @@ -300,7 +300,7 @@ var MovieList = new Class({ self.navigation_alpha = new Element('ul.numbers', { 'events': { 'click:relay(li.available)': function(e, el){ - self.activateLetter(el.get('data-letter')) + self.activateLetter(el.get('data-letter')); self.getMovies(true) } } @@ -318,7 +318,7 @@ var MovieList = new Class({ // All self.letters['all'] = new Element('li.letter_all.available.active', { - 'text': 'ALL', + 'text': 'ALL' }).inject(self.navigation_alpha); // Chars @@ -334,7 +334,7 @@ var MovieList = new Class({ if (self.options.menu.length > 0) self.options.menu.each(function(menu_item){ self.navigation_menu.addLink(menu_item); - }) + }); else self.navigation_menu.hide(); @@ -347,15 +347,15 @@ var MovieList = new Class({ movies = self.movies.length; self.movies.each(function(movie){ selected += movie.isSelected() ? 1 : 0 - }) + }); var indeterminate = selected > 0 && selected < movies, checked = selected == movies && selected > 0; - self.mass_edit_select.set('indeterminate', indeterminate) + self.mass_edit_select.set('indeterminate', indeterminate); - self.mass_edit_select_class[checked ? 'check' : 'uncheck']() - self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate') + self.mass_edit_select_class[checked ? 'check' : 'uncheck'](); + self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate'); self.mass_edit_selected.set('text', selected); }, @@ -371,7 +371,7 @@ var MovieList = new Class({ 'events': { 'click': function(e){ (e).preventDefault(); - this.set('text', 'Deleting..') + this.set('text', 'Deleting..'); Api.request('media.delete', { 'data': { 'id': ids.join(','), @@ -383,7 +383,7 @@ var MovieList = new Class({ var erase_movies = []; self.movies.each(function(movie){ if (movie.isSelected()){ - $(movie).destroy() + $(movie).destroy(); erase_movies.include(movie); } }); @@ -410,7 +410,7 @@ var MovieList = new Class({ changeQualitySelected: function(){ var self = this; - var ids = self.getSelectedMovies() + var ids = self.getSelectedMovies(); Api.request('movie.edit', { 'data': { @@ -423,11 +423,11 @@ var MovieList = new Class({ refreshSelected: function(){ var self = this; - var ids = self.getSelectedMovies() + var ids = self.getSelectedMovies(); Api.request('media.refresh', { 'data': { - 'id': ids.join(','), + 'id': ids.join(',') } }); }, @@ -435,7 +435,7 @@ var MovieList = new Class({ getSelectedMovies: function(){ var self = this; - var ids = [] + var ids = []; self.movies.each(function(movie){ if (movie.isSelected()) ids.include(movie.get('_id')) @@ -459,11 +459,11 @@ var MovieList = new Class({ reset: function(){ var self = this; - self.movies = [] + self.movies = []; if(self.mass_edit_select) - self.calculateSelected() + self.calculateSelected(); if(self.navigation_alpha) - self.navigation_alpha.getElements('.active').removeClass('active') + self.navigation_alpha.getElements('.active').removeClass('active'); self.offset = 0; if(self.scrollspy){ @@ -475,7 +475,7 @@ var MovieList = new Class({ activateLetter: function(letter){ var self = this; - self.reset() + self.reset(); self.letters[letter || 'all'].addClass('active'); self.filter.starts_with = letter; @@ -487,7 +487,7 @@ var MovieList = new Class({ self.el .removeClass(self.current_view+'_list') - .addClass(new_view+'_list') + .addClass(new_view+'_list'); self.current_view = new_view; Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000}); @@ -504,9 +504,9 @@ var MovieList = new Class({ if(self.search_timer) clearTimeout(self.search_timer); self.search_timer = (function(){ var search_value = self.navigation_search_input.get('value'); - if (search_value == self.last_search_value) return + if (search_value == self.last_search_value) return; - self.reset() + self.reset(); self.activateLetter(); self.filter.search = search_value; @@ -563,7 +563,7 @@ var MovieList = new Class({ if(self.loader_first){ var lf = self.loader_first; - self.loader_first.addClass('hide') + self.loader_first.addClass('hide'); self.loader_first = null; setTimeout(function(){ lf.destroy(); @@ -603,10 +603,10 @@ var MovieList = new Class({ var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined); if(self.title) - self.title[is_empty ? 'hide' : 'show']() + self.title[is_empty ? 'hide' : 'show'](); if(self.description) - self.description.setStyle('display', [is_empty ? 'none' : '']) + self.description.setStyle('display', [is_empty ? 'none' : '']); if(is_empty && self.options.on_empty_element){ self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index cfcd5fe8..4d9dcccb 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -60,22 +60,6 @@ var MovieAction = new Class({ 'z-index': '1' } }).inject(self.movie, 'top').fade('hide'); - //self.positionMask(); - }, - - positionMask: function(){ - var self = this, - movie = $(self.movie), - s = movie.getSize() - - return; - - return self.mask.setStyles({ - 'width': s.x, - 'height': s.y - }).position({ - 'relativeTo': movie - }) }, toElement: function(){ @@ -122,7 +106,7 @@ MA.Release = new Class({ }); if(!self.movie.data.releases || self.movie.data.releases.length == 0) - self.el.hide() + self.el.hide(); else self.showHelper(); @@ -164,7 +148,7 @@ MA.Release = new Class({ new Element('span.age', {'text': 'Age'}), new Element('span.score', {'text': 'Score'}), new Element('span.provider', {'text': 'Provider'}) - ).inject(self.release_container) + ).inject(self.release_container); if(self.movie.data.releases) self.movie.data.releases.each(function(release){ @@ -186,7 +170,7 @@ MA.Release = new Class({ } // Create release - var item = new Element('div', { + release['el'] = new Element('div', { 'class': 'item '+release.status, 'id': 'release_'+release._id }).adopt( @@ -219,7 +203,6 @@ MA.Release = new Class({ } }) ).inject(self.release_container); - release['el'] = item; if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) @@ -242,13 +225,13 @@ MA.Release = new Class({ status_el.set('text', new_status); if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) - var q = self.addQuality(release.quality_id); + q = self.addQuality(release.quality_id); if(q && !q.hasClass(new_status)) { q.removeClass(release.status).addClass(new_status); q.set('title', q.get('title').replace(release.status, new_status)); } - } + }; App.on('release.update_status', update_handle); @@ -391,12 +374,11 @@ MA.Release = new Class({ }, ignore: function(release){ - var self = this; Api.request('release.ignore', { 'data': { 'id': release._id - }, + } }) }, @@ -456,7 +438,7 @@ MA.Trailer = new Class({ watch: function(offset){ var self = this; - var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18' + var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'; var url = data_url.substitute({ 'title': encodeURI(self.getTitle()), 'year': self.get('year'), @@ -515,7 +497,7 @@ MA.Trailer = new Class({ } } - } + }; self.player.addEventListener('onStateChange', change_quality); } @@ -532,7 +514,7 @@ MA.Trailer = new Class({ $(self.movie).setStyle('height', null); setTimeout(function(){ - self.container.destroy() + self.container.destroy(); self.close_button.destroy(); }, 1800) } @@ -661,7 +643,7 @@ MA.Edit = new Class({ self.movie.slide('out'); } -}) +}); MA.Refresh = new Class({ @@ -847,7 +829,7 @@ MA.Files = new Class({ new Element('div.item.head').adopt( new Element('span.name', {'text': 'File'}), new Element('span.type', {'text': 'Type'}) - ).inject(self.files_container) + ).inject(self.files_container); if(self.movie.data.releases) Array.each(self.movie.data.releases, function(release){ diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index d579d542..6718dc3a 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -398,7 +398,6 @@ .movies .data .quality span { padding: 2px 3px; - font-weight: bold; opacity: 0.5; font-size: 10px; height: 16px; @@ -451,7 +450,6 @@ right: 20px; line-height: 0; top: 0; - display: block; width: auto; opacity: 0; display: none; @@ -833,7 +831,6 @@ } .movies .alph_nav .search input { - padding: 6px 5px; width: 100%; height: 44px; display: inline-block; @@ -841,7 +838,6 @@ background: none; color: #444; font-size: 14px; - padding: 10px; padding: 0 10px 0 30px; border-bottom: 1px solid rgba(0,0,0,.08); } @@ -1043,7 +1039,6 @@ } .movies .progress > div .percentage { - font-weight: bold; display: inline-block; text-transform: uppercase; font-weight: normal; diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 38012e97..25d5e08f 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -23,7 +23,7 @@ var Movie = new Class({ addEvents: function(){ var self = this; - self.global_events = {} + self.global_events = {}; // Do refresh with new data self.global_events['movie.update'] = function(notification){ @@ -32,7 +32,7 @@ var Movie = new Class({ self.busy(false); self.removeView(); self.update.delay(2000, self, notification); - } + }; App.on('movie.update', self.global_events['movie.update']); // Add spinner on load / search @@ -40,20 +40,20 @@ var Movie = new Class({ self.global_events[listener] = function(notification){ if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1))) self.busy(true); - } + }; App.on(listener, self.global_events[listener]); - }) + }); // Remove spinner self.global_events['movie.searcher.ended'] = function(notification){ if(notification.data && self.data._id == notification.data._id) self.busy(false) - } + }; App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); // Reload when releases have updated self.global_events['release.update_status'] = function(notification){ - var data = notification.data + var data = notification.data; if(data && self.data._id == data.movie_id){ if(!self.data.releases) @@ -62,7 +62,7 @@ var Movie = new Class({ self.data.releases.push({'quality': data.quality, 'status': data.status}); self.updateReleases(); } - } + }; App.on('release.update_status', self.global_events['release.update_status']); @@ -73,7 +73,7 @@ var Movie = new Class({ self.el.destroy(); delete self.list.movies_added[self.get('id')]; - self.list.movies.erase(self) + self.list.movies.erase(self); self.list.checkIfEmpty(); @@ -117,18 +117,6 @@ var Movie = new Class({ }).inject(self.el, 'top').fade('hide'); }, - positionMask: function(){ - var self = this, - s = self.el.getSize() - - return self.mask.setStyles({ - 'width': s.x, - 'height': s.y - }).position({ - 'relativeTo': self.el - }) - }, - update: function(notification){ var self = this; @@ -209,7 +197,7 @@ var Movie = new Class({ self.updateReleases(); Object.each(self.options.actions, function(action, key){ - self.action[key.toLowerCase()] = action = new self.options.actions[key](self) + self.action[key.toLowerCase()] = action = new self.options.actions[key](self); if(action.el) self.actions.adopt(action) }); @@ -226,7 +214,7 @@ var Movie = new Class({ status = release.status; if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) - var q = self.addQuality(release.quality, release.is_3d || false) + q = self.addQuality(release.quality, release.is_3d || false); if (q && !q.hasClass(status)){ q.addClass(status); @@ -252,9 +240,9 @@ var Movie = new Class({ var self = this; if(self.data.title) - return self.getUnprefixedTitle(self.data.title) + return self.getUnprefixedTitle(self.data.title); else if(self.data.info.titles.length > 0) - return self.getUnprefixedTitle(self.data.info.titles[0]) + return self.getUnprefixedTitle(self.data.info.titles[0]); return 'Unknown movie' }, @@ -275,12 +263,12 @@ var Movie = new Class({ self.el.addEvent('outerClick', function(){ self.removeView(); self.slide('out') - }) + }); el.show(); self.data_container.addClass('hide_right'); } else { - self.el.removeEvents('outerClick') + self.el.removeEvents('outerClick'); setTimeout(function(){ if(self.el) @@ -297,7 +285,7 @@ var Movie = new Class({ if(self.el) self.el .removeClass(self.view+'_view') - .addClass(new_view+'_view') + .addClass(new_view+'_view'); self.view = new_view; }, diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index cc837394..3b426762 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -41,7 +41,7 @@ Block.Search.MovieItem = new Class({ ) ) ) - ) + ); if(info.titles) info.titles.each(function(title){ @@ -132,19 +132,19 @@ Block.Search.MovieItem = new Class({ if(!self.options_el.hasClass('set')){ - if(self.info.in_library){ + if(info.in_library){ var in_library = []; - (self.info.in_library.releases || []).each(function(release){ + (info.in_library.releases || []).each(function(release){ in_library.include(release.quality) }); } self.options_el.grab( new Element('div', { - 'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : '' + 'class': info.in_wanted && info.in_wanted.profile_id || in_library ? 'in_library_wanted' : '' }).adopt( - self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', { - 'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label') + info.in_wanted && info.in_wanted.profile_id ? new Element('span.in_wanted', { + 'text': 'Already in wanted list: ' + Quality.getProfile(info.in_wanted.profile_id).get('label') }) : (in_library ? new Element('span.in_library', { 'text': 'Already in library: ' + in_library.join(', ') }) : null), @@ -172,7 +172,7 @@ Block.Search.MovieItem = new Class({ new Element('option', { 'text': alt.title }).inject(self.title_select) - }) + }); // Fill categories @@ -215,9 +215,9 @@ Block.Search.MovieItem = new Class({ loadingMask: function(){ var self = this; - self.mask = new Element('div.mask').inject(self.el).fade('hide') + self.mask = new Element('div.mask').inject(self.el).fade('hide'); - createSpinner(self.mask) + createSpinner(self.mask); self.mask.fade('in') }, diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index b411900d..b0f99f6c 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -62,7 +62,7 @@ class Bluray(Automation, RSS): name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip() year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip() - if not name.find('/') == -1: # make sure it is not a double movie release + if not name.find('/') == -1: # make sure it is not a double movie release continue if tryInt(year) < self.getMinimal('year'): diff --git a/couchpotato/core/media/movie/providers/automation/goodfilms.py b/couchpotato/core/media/movie/providers/automation/goodfilms.py index 2524f661..483cf029 100644 --- a/couchpotato/core/media/movie/providers/automation/goodfilms.py +++ b/couchpotato/core/media/movie/providers/automation/goodfilms.py @@ -46,7 +46,10 @@ class Goodfilms(Automation): break for movie in this_watch_list: - movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] }) + movies.append({ + 'title': movie['data-film-title'], + 'year': movie['data-film-year'] + }) if not 'next page' in data.lower(): break diff --git a/couchpotato/core/media/movie/providers/automation/letterboxd.py b/couchpotato/core/media/movie/providers/automation/letterboxd.py index fa914d8a..e9fc8741 100644 --- a/couchpotato/core/media/movie/providers/automation/letterboxd.py +++ b/couchpotato/core/media/movie/providers/automation/letterboxd.py @@ -50,7 +50,10 @@ class Letterboxd(Automation): for movie in soup.find_all('a', attrs = {'class': 'frame'}): match = removeEmpty(self.pattern.split(movie['title'])) - movies.append({'title': match[0], 'year': match[1] }) + movies.append({ + 'title': match[0], + 'year': match[1] + }) return movies diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index 9f5c5e88..b48822cc 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -74,6 +74,7 @@ class TheMovieDb(MovieProvider): if not result: try: log.debug('Getting info: %s', cache_key) + # noinspection PyArgumentList movie = tmdb3.Movie(identifier) try: exists = movie.title is not None except: exists = False diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv.py b/couchpotato/core/media/movie/providers/torrent/bithdtv.py index 8b77199b..e7b16af9 100644 --- a/couchpotato/core/media/movie/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv.py @@ -14,6 +14,6 @@ class BiTHDTV(MovieProvider, Base): def buildUrl(self, media): query = tryUrlencode({ 'search': fireEvent('library.query', media, single = True), - 'cat': 7 # Movie cat + 'cat': 7 # Movie cat }) return query diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py index d1f378b6..bbaea265 100644 --- a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py @@ -10,29 +10,29 @@ autoload = 'PassThePopcorn' class PassThePopcorn(MovieProvider, Base): quality_search_params = { - 'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, - '1080p': {'resolution': '1080p'}, - '720p': {'resolution': '720p'}, - 'brrip': {'media': 'Blu-ray'}, - 'dvdr': {'resolution': 'anysd'}, - 'dvdrip': {'media': 'DVD'}, - 'scr': {'media': 'DVD-Screener'}, - 'r5': {'media': 'R5'}, - 'tc': {'media': 'TC'}, - 'ts': {'media': 'TS'}, - 'cam': {'media': 'CAM'} + 'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, + '1080p': {'resolution': '1080p'}, + '720p': {'resolution': '720p'}, + 'brrip': {'media': 'Blu-ray'}, + 'dvdr': {'resolution': 'anysd'}, + 'dvdrip': {'media': 'DVD'}, + 'scr': {'media': 'DVD-Screener'}, + 'r5': {'media': 'R5'}, + 'tc': {'media': 'TC'}, + 'ts': {'media': 'TS'}, + 'cam': {'media': 'CAM'} } post_search_filters = { - 'bd50': {'Codec': ['BD50']}, - '1080p': {'Resolution': ['1080p']}, - '720p': {'Resolution': ['720p']}, - 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, - 'dvdr': {'Codec': ['DVD5', 'DVD9']}, - 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, - 'scr': {'Source': ['DVD-Screener']}, - 'r5': {'Source': ['R5']}, - 'tc': {'Source': ['TC']}, - 'ts': {'Source': ['TS']}, - 'cam': {'Source': ['CAM']} + 'bd50': {'Codec': ['BD50']}, + '1080p': {'Resolution': ['1080p']}, + '720p': {'Resolution': ['720p']}, + 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, + 'dvdr': {'Codec': ['DVD5', 'DVD9']}, + 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, + 'scr': {'Source': ['DVD-Screener']}, + 'r5': {'Source': ['R5']}, + 'tc': {'Source': ['TC']}, + 'ts': {'Source': ['TS']}, + 'cam': {'Source': ['CAM']} } diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py index 4990a579..f804d709 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -12,11 +12,11 @@ autoload = 'ThePirateBay' class ThePirateBay(MovieProvider, Base): cat_ids = [ - ([209], ['3d']), - ([207], ['720p', '1080p']), - ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), - ([201, 207], ['brrip']), - ([202], ['dvdr']) + ([209], ['3d']), + ([207], ['720p', '1080p']), + ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + ([201, 207], ['brrip']), + ([202], ['dvdr']) ] def buildUrl(self, media, page, cats): diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday.py b/couchpotato/core/media/movie/providers/torrent/torrentday.py index 03fe3fe0..872a7d17 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentday.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentday.py @@ -16,5 +16,6 @@ class TorrentDay(MovieProvider, Base): ([3], ['dvdr']), ([5], ['bd50']), ] + def buildUrl(self, media): return fireEvent('library.query', media, single = True) diff --git a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py index 084c8ea2..ad040a3b 100644 --- a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py +++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py @@ -52,7 +52,7 @@ class HDTrailers(TrailerProvider): return result_data def findViaAlternative(self, group): - results = {'480p':[], '720p':[], '1080p':[]} + results = {'480p': [], '720p': [], '1080p': []} movie_name = getTitle(group) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index d6d4821c..68a76ade 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -249,7 +249,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) return False - # Provider specific functions get_more = nzb.get('get_more_info') if get_more: diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css index d4ba734b..82f0c21c 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.css +++ b/couchpotato/core/media/movie/suggestion/static/suggest.css @@ -91,7 +91,6 @@ padding: 0 3px 10px 0; } .suggestions .media_result .data:before { - bottom: 0; content: ''; display: block; height: 10px; @@ -107,7 +106,7 @@ z-index: 3; pointer-events: none; } - + .suggestions .media_result .data .info .plot.full { top: 0; overflow: auto; @@ -123,14 +122,14 @@ .suggestions .media_result .options select[name=title] { width: 100%; } .suggestions .media_result .options select[name=profile] { width: 100%; } .suggestions .media_result .options select[name=category] { width: 100%; } - - .suggestions .media_result .button { + + .suggestions .media_result .button { position: absolute; margin: 2px 0 0 0; right: 15px; bottom: 15px; } - + .suggestions .media_result .thumbnail { width: 100px; diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js index c4e5630a..a0547e4a 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.js +++ b/couchpotato/core/media/movie/suggestion/static/suggest.js @@ -116,7 +116,7 @@ var SuggestList = new Class({ } } }) : null - ) + ); $(m).inject(self.el); @@ -150,4 +150,4 @@ var SuggestList = new Class({ return this.el; } -}) +}); diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 1a506c1e..0cd8e579 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -131,7 +131,7 @@ class CoreNotifier(Notification): for message in messages: if message.get('time') > last_check: - message['sticky'] = True # Always sticky core messages + message['sticky'] = True # Always sticky core messages message_type = 'core.message.important' if message.get('important') else 'core.message' fireEvent(message_type, message = message.get('message'), data = message) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 2cd4fff1..c999fc77 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -17,14 +17,14 @@ var NotificationBase = new Class({ App.addEvent('load', self.addTestButtons.bind(self)); // Notification bar - self.notifications = [] + self.notifications = []; App.addEvent('load', function(){ App.block.notification = new Block.Menu(self, { 'button_class': 'icon2.eye-open', 'class': 'notification_menu', 'onOpen': self.markAsRead.bind(self) - }) + }); $(App.block.notification).inject(App.getBlock('search'), 'after'); self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide(); @@ -40,7 +40,7 @@ var NotificationBase = new Class({ var self = this; var added = new Date(); - added.setTime(result.added*1000) + added.setTime(result.added*1000); result.el = App.getBlock('notification').addLink( new Element('span.'+(result.read ? 'read' : '' )).adopt( @@ -51,7 +51,7 @@ var NotificationBase = new Class({ self.notifications.include(result); if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){ - var sticky = true + var sticky = true; App.trigger('message', [result.message, sticky, result]) } else if(!result.read){ @@ -62,7 +62,7 @@ var NotificationBase = new Class({ setBadge: function(value){ var self = this; - self.badge.set('text', value) + self.badge.set('text', value); self.badge[value ? 'show' : 'hide']() }, @@ -73,9 +73,9 @@ var NotificationBase = new Class({ if(!force_ids) { var rn = self.notifications.filter(function(n){ return !n.read && n.data.important === undefined - }) + }); - var ids = [] + var ids = []; rn.each(function(n){ ids.include(n._id) }) @@ -106,7 +106,7 @@ var NotificationBase = new Class({ 'onSuccess': function(json){ self.processData(json, true) } - }).send() + }).send(); setInterval(function(){ @@ -141,7 +141,7 @@ var NotificationBase = new Class({ stopPoll: function(){ if(this.request) - this.request.cancel() + this.request.cancel(); this.stopped = true; }, @@ -154,7 +154,7 @@ var NotificationBase = new Class({ App.trigger(result._t || result.type, [result]); if(result.message && result.read === undefined && !init) self.showMessage(result.message); - }) + }); if(json.result.length > 0) self.last_id = json.result.getLast().message_id @@ -180,11 +180,11 @@ var NotificationBase = new Class({ }, 10); var hide_message = function(){ - new_message.addClass('hide') + new_message.addClass('hide'); setTimeout(function(){ new_message.destroy(); }, 1000); - } + }; if(sticky) new_message.grab( @@ -206,7 +206,7 @@ var NotificationBase = new Class({ addTestButtons: function(){ var self = this; - var setting_page = App.getPage('Settings') + var setting_page = App.getPage('Settings'); setting_page.addEvent('create', function(){ Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self)) }) diff --git a/couchpotato/core/notifications/email_.py b/couchpotato/core/notifications/email_.py index 938a3614..a63eb3de 100644 --- a/couchpotato/core/notifications/email_.py +++ b/couchpotato/core/notifications/email_.py @@ -97,7 +97,8 @@ config = [{ 'name': 'smtp_server', 'label': 'SMTP server', }, - { 'name': 'smtp_port', + { + 'name': 'smtp_port', 'label': 'SMTP server port', 'default': '25', 'type': 'int', diff --git a/couchpotato/core/notifications/growl.py b/couchpotato/core/notifications/growl.py index 160e383e..e60e7ef7 100644 --- a/couchpotato/core/notifications/growl.py +++ b/couchpotato/core/notifications/growl.py @@ -19,6 +19,8 @@ class Growl(Notification): def __init__(self): super(Growl, self).__init__() + self.growl = None + if self.isEnabled(): addEvent('app.load', self.register) diff --git a/couchpotato/core/notifications/nmj.py b/couchpotato/core/notifications/nmj.py index 4b9b92bd..7579cafa 100644 --- a/couchpotato/core/notifications/nmj.py +++ b/couchpotato/core/notifications/nmj.py @@ -20,6 +20,7 @@ autoload = 'NMJ' class NMJ(Notification): + # noinspection PyMissingConstructor def __init__(self): addEvent('renamer.after', self.addToLibrary) addApiView(self.testNotifyName(), self.test) diff --git a/couchpotato/core/notifications/pushalot.py b/couchpotato/core/notifications/pushalot.py index f8fa187e..fa781bc5 100644 --- a/couchpotato/core/notifications/pushalot.py +++ b/couchpotato/core/notifications/pushalot.py @@ -30,7 +30,7 @@ class Pushalot(Notification): } headers = { - 'Content-type': 'application/x-www-form-urlencoded' + 'Content-type': 'application/x-www-form-urlencoded' } try: diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index 95593747..1925f729 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -33,10 +33,9 @@ class Pushover(Notification): 'url_title': toUnicode('%s on IMDb' % getTitle(data)), }) - http_handler.request('POST', - "/1/messages.json", - headers = {'Content-type': 'application/x-www-form-urlencoded'}, - body = tryUrlencode(api_data) + http_handler.request('POST', '/1/messages.json', + headers = {'Content-type': 'application/x-www-form-urlencoded'}, + body = tryUrlencode(api_data) ) response = http_handler.getresponse() diff --git a/couchpotato/core/notifications/twitter/static/twitter.js b/couchpotato/core/notifications/twitter/static/twitter.js index 2c4e6e31..c817fe03 100644 --- a/couchpotato/core/notifications/twitter/static/twitter.js +++ b/couchpotato/core/notifications/twitter/static/twitter.js @@ -59,7 +59,7 @@ var TwitterNotification = new Class({ ).inject(fieldset.getElement('.test_button'), 'before'); }) - }, + } }); diff --git a/couchpotato/core/notifications/xbmc.py b/couchpotato/core/notifications/xbmc.py index 682801cf..28439a75 100644 --- a/couchpotato/core/notifications/xbmc.py +++ b/couchpotato/core/notifications/xbmc.py @@ -89,7 +89,7 @@ class XBMC(Notification): self.use_json_notifications[host] = False # send the text message - resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) + resp = self.notifyXBMCnoJSON(host, {'title': self.default_title, 'message': message}) for r in resp: if r.get('result') and r['result'] == 'OK': log.debug('Message delivered successfully!') diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index c5892f51..13eb7caf 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -43,8 +43,8 @@ class Plugin(object): http_failed_disabled = {} http_opener = requests.Session() - def __new__(typ, *args, **kwargs): - new_plugin = super(Plugin, typ).__new__(typ) + def __new__(cls, *args, **kwargs): + new_plugin = super(Plugin, cls).__new__(cls) new_plugin.registerPlugin() return new_plugin @@ -329,7 +329,7 @@ class Plugin(object): elif data.get('password'): tag += '{{%s}}' % data.get('password') - max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames + max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag) def createFileName(self, data, filedata, media): @@ -349,6 +349,7 @@ class Plugin(object): now = time.time() file_too_new = False + file_time = [] for cur_file in files: # File got removed while checking diff --git a/couchpotato/core/plugins/browser.py b/couchpotato/core/plugins/browser.py index 9492a2d9..6880f3b4 100644 --- a/couchpotato/core/plugins/browser.py +++ b/couchpotato/core/plugins/browser.py @@ -17,7 +17,8 @@ if os.name == 'nt': raise ImportError("Missing the win32file module, which is a part of the prerequisite \ pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/") else: - import win32file #@UnresolvedImport + # noinspection PyUnresolvedReferences + import win32file autoload = 'FileBrowser' diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js index fedd2849..b3234912 100644 --- a/couchpotato/core/plugins/category/static/category.js +++ b/couchpotato/core/plugins/category/static/category.js @@ -9,7 +9,7 @@ var CategoryListBase = new Class({ setup: function(categories){ var self = this; - self.categories = [] + self.categories = []; Array.each(categories, self.createCategory.bind(self)); }, @@ -17,7 +17,7 @@ var CategoryListBase = new Class({ addSettings: function(){ var self = this; - self.settings = App.getPage('Settings') + self.settings = App.getPage('Settings'); self.settings.addEvent('create', function(){ var tab = self.settings.createSubTab('category', { 'label': 'Categories', @@ -31,7 +31,7 @@ var CategoryListBase = new Class({ self.createList(); self.createOrdering(); - }) + }); // Add categories in renamer self.settings.addEvent('create', function(){ @@ -97,9 +97,9 @@ var CategoryListBase = new Class({ createCategory: function(data){ var self = this; - var data = data || {'id': randomString()} - var category = new Category(data) - self.categories.include(category) + var data = data || {'id': randomString()}; + var category = new Category(data); + self.categories.include(category); return category; }, @@ -108,7 +108,7 @@ var CategoryListBase = new Class({ var self = this; var category_list; - var group = self.settings.createGroup({ + self.settings.createGroup({ 'label': 'Category ordering' }).adopt( new Element('.ctrlHolder#category_ordering').adopt( @@ -118,7 +118,7 @@ var CategoryListBase = new Class({ 'html': 'Change the order the categories are in the dropdown list.
First one will be default.' }) ) - ).inject(self.content) + ).inject(self.content); Array.each(self.categories, function(category){ new Element('li', {'data-id': category.data._id}).adopt( @@ -145,7 +145,7 @@ var CategoryListBase = new Class({ var ids = []; - self.category_sortable.list.getElements('li').each(function(el, nr){ + self.category_sortable.list.getElements('li').each(function(el){ ids.include(el.get('data-id')); }); @@ -157,7 +157,7 @@ var CategoryListBase = new Class({ } -}) +}); window.CategoryList = new CategoryListBase(); @@ -235,8 +235,6 @@ var Category = new Class({ if(self.save_timer) clearTimeout(self.save_timer); self.save_timer = (function(){ - var data = self.getData(); - Api.request('category.save', { 'data': self.getData(), 'useSpinner': true, @@ -257,7 +255,7 @@ var Category = new Class({ getData: function(){ var self = this; - var data = { + return { 'id' : self.data._id, 'label' : self.el.getElement('.category_label input').get('value'), 'required' : self.el.getElement('.category_required input').get('value'), @@ -265,8 +263,6 @@ var Category = new Class({ 'ignored' : self.el.getElement('.category_ignored input').get('value'), 'destination': self.data.destination } - - return data }, del: function(){ diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index fad68b06..7a26b2a0 100644 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -77,8 +77,8 @@ class Dashboard(Plugin): if coming_soon: # Don't list older movies - if ((not late and (media['info']['year'] >= now_year-1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or - (late and ((media['info']['year'] < now_year-1) or ((eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))))): + if ((not late and (media['info']['year'] >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or + (late and (media['info']['year'] < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))): medias.append(media) if len(medias) >= limit: diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py index b2533c07..92d85c50 100644 --- a/couchpotato/core/plugins/file.py +++ b/couchpotato/core/plugins/file.py @@ -1,5 +1,4 @@ import os.path -import time import traceback from couchpotato import get_db @@ -64,7 +63,7 @@ class FileManager(Plugin): def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None): if not urlopen_kwargs: urlopen_kwargs = {} - if not dest: # to Cache + if not dest: # to Cache dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url))) if not overwrite and os.path.isfile(dest): diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index ced069ea..2565398e 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -67,14 +67,14 @@ class Logging(Plugin): if x is nr: current_path = path - log = '' + log_content = '' if current_path: f = open(current_path, 'r') - log = f.read() + log_content = f.read() return { 'success': True, - 'log': toUnicode(log), + 'log': toUnicode(log_content), 'total': total, } diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index bcab6e23..b706835a 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -50,7 +50,7 @@ overflow: hidden; line-height: 150%; font-size: 11px; - font-family: Lucida Console, Monaco, Nimbus Mono L; + font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; } .page.log .container .error { diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index e450b078..c5effeba 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -42,7 +42,7 @@ Page.Log = new Class({ } } }).inject(nav); - }; + } new Element('li', { 'text': 'clear', @@ -63,7 +63,6 @@ Page.Log = new Class({ }, addColors: function(text){ - var self = this; text = text .replace(/&/g, '&') @@ -74,9 +73,9 @@ Page.Log = new Class({ .replace(/\u001b\[36m/gi, '
') .replace(/\u001b\[33m/gi, '') .replace(/\u001b\[0m\n/gi, '
') - .replace(/\u001b\[0m/gi, '') + .replace(/\u001b\[0m/gi, ''); return '
' + text + '
'; } -}) +}); diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 623ce6f4..f6b0ddd7 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -160,7 +160,7 @@ class Manage(Plugin): except: log.error('Failed updating library: %s', (traceback.format_exc())) - while True and not self.shuttingDown(): + while self.in_progress and len(self.in_progress) > 0 and not self.shuttingDown(): delete_me = {} @@ -171,14 +171,12 @@ class Manage(Plugin): for delete in delete_me: del self.in_progress[delete] - if len(self.in_progress) == 0: - break - time.sleep(1) fireEvent('notify.frontend', type = 'manage.updating', data = False) self.in_progress = False + # noinspection PyDefaultArgument def createAddToLibrary(self, folder, added_identifiers = []): def addToLibrary(group, total_found, to_go): @@ -219,7 +217,7 @@ class Manage(Plugin): pr = self.in_progress[folder] pr['to_go'] -= 1 - avg = (time.time() - pr['started'])/(pr['total'] - pr['to_go']) + avg = (time.time() - pr['started']) / (pr['total'] - pr['to_go']) pr['eta'] = tryInt(avg * pr['to_go']) diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 47b92a56..8441b6a8 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -119,7 +119,7 @@ var Profile = new Class({ 'label' : self.el.getElement('.quality_label input').get('value'), 'wait_for' : self.el.getElement('.wait_for input').get('value'), 'types': [] - } + }; Array.each(self.type_container.getElements('.type'), function(type){ if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1) @@ -129,7 +129,7 @@ var Profile = new Class({ '3d': +type.getElement('input.3d[type=checkbox]').checked, 'wait_for': 0 }); - }) + }); return data }, @@ -261,10 +261,10 @@ Profile.Type = new Class({ self.finish = new Element('input.inlay.finish[type=checkbox]', { 'checked': data.finish !== undefined ? data.finish : 1, 'events': { - 'change': function(e){ + 'change': function(){ if(self.el == self.el.getParent().getElement(':first-child')){ self.finish_class.check(); - alert('Top quality always finishes the search') + alert('Top quality always finishes the search'); return; } @@ -280,7 +280,7 @@ Profile.Type = new Class({ self['3d'] = new Element('input.inlay.3d[type=checkbox]', { 'checked': data['3d'] !== undefined ? data['3d'] : 0, 'events': { - 'change': function(e){ + 'change': function(){ self.fireEvent('change'); } } @@ -363,4 +363,4 @@ Profile.Type = new Class({ return this.el; } -}) +}); diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index c52a0572..c27815ed 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -63,6 +63,7 @@ class QualityPlugin(Plugin): addEvent('app.test', self.doTest) + self.order = [] self.addOrder() def addOrder(self): @@ -247,12 +248,12 @@ class QualityPlugin(Plugin): qualities = [qualities] if isinstance(qualities, (str, unicode)) else qualities for alt in qualities: - if (isinstance(alt, tuple)): + if isinstance(alt, tuple): if len(set(words) & set(alt)) == len(alt): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) score += points.get(tag_type) - if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()): + if isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower(): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) score += points.get(tag_type) / 2 diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 613096fc..d103908b 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -8,7 +8,7 @@ var QualityBase = new Class({ self.qualities = data.qualities; - self.profiles = [] + self.profiles = []; Array.each(data.profiles, self.createProfilesClass.bind(self)); App.addEvent('load', self.addSettings.bind(self)) @@ -37,7 +37,7 @@ var QualityBase = new Class({ addSettings: function(){ var self = this; - self.settings = App.getPage('Settings') + self.settings = App.getPage('Settings'); self.settings.addEvent('create', function(){ var tab = self.settings.createSubTab('profile', { 'label': 'Quality', @@ -91,9 +91,9 @@ var QualityBase = new Class({ createProfilesClass: function(data){ var self = this; - var data = data || {'id': randomString()} - var profile = new Profile(data) - self.profiles.include(profile) + var data = data || {'id': randomString()}; + var profile = new Profile(data); + self.profiles.include(profile); return profile; }, @@ -102,7 +102,7 @@ var QualityBase = new Class({ var self = this; var profile_list; - var group = self.settings.createGroup({ + self.settings.createGroup({ 'label': 'Profile Defaults', 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' }).adopt( @@ -113,7 +113,7 @@ var QualityBase = new Class({ 'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.
First one will be default.' }) ) - ).inject(self.content) + ).inject(self.content); Array.each(self.profiles, function(profile){ var check; @@ -175,14 +175,14 @@ var QualityBase = new Class({ 'description': 'Edit the minimal and maximum sizes (in MB) for each quality.', 'advanced': true, 'name': 'sizes' - }).inject(self.content) + }).inject(self.content); new Element('div.item.head.ctrlHolder').adopt( new Element('span.label', {'text': 'Quality'}), new Element('span.min', {'text': 'Min'}), new Element('span.max', {'text': 'Max'}) - ).inject(group) + ).inject(group); Array.each(self.qualities, function(quality){ new Element('div.ctrlHolder.item').adopt( diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 59e3b7d7..2d2e94a2 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -148,7 +148,7 @@ class Renamer(Plugin): log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder) # Update to the from folder - if len(release_download.get('files'), []) == 1: + if len(release_download.get('files', [])) == 1: new_media_folder = from_folder else: new_media_folder = os.path.join(from_folder, os.path.basename(media_folder)) @@ -504,7 +504,7 @@ class Renamer(Plugin): fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) # Remove leftover files - if not remove_leftovers: # Don't remove anything + if not remove_leftovers: # Don't remove anything break log.debug('Removing leftover files') @@ -529,8 +529,8 @@ class Renamer(Plugin): parent_dir = os.path.dirname(src) if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and \ - not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \ - not isSubFolder(parent_dir, base_folder): + not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \ + not isSubFolder(parent_dir, base_folder): delete_folders.append(parent_dir) @@ -659,7 +659,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) for filename in tag_files: - # Dont tag .ignore files + # Don't tag .ignore files if os.path.splitext(filename)[1] == '.ignore': continue @@ -1017,7 +1017,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) if release_download['pause'] and self.conf('file_action') == 'link': fireEvent('download.pause', release_download = release_download, pause = False, single = True) if release_download['process_complete']: - #First make sure the files were succesfully processed + # First make sure the files were successfully processed if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'): # Remove the seeding tag if it exists self.untagRelease(release_download = release_download, tag = 'renamed_already') diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 133ec452..9818ff06 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -28,7 +28,7 @@ class Scanner(Plugin): ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo', - 'thumbs.db', 'ehthumbs.db', 'desktop.ini'] #unpacking, smb-crap, hidden files + 'thumbs.db', 'ehthumbs.db', 'desktop.ini'] # unpacking, smb-crap, hidden files ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] extensions = { 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'], @@ -589,7 +589,7 @@ class Scanner(Plugin): if len(movie) > 0: imdb_id = movie[0].get('imdb') - log.debug('Found movie via search: %s', cur_file) + log.debug('Found movie via search: %s', identifier) if imdb_id: break else: log.debug('Identifier to short to use for search: %s', identifier) @@ -835,6 +835,7 @@ class Scanner(Plugin): cleaned = ' '.join(re.split('\W+', simplifyString(release_name))) cleaned = re.sub(self.clean, ' ', cleaned) + year = None for year_str in [file_name, release_name, cleaned]: if not year_str: continue year = self.findYear(year_str) @@ -843,7 +844,7 @@ class Scanner(Plugin): cp_guess = {} - if year: # Split name on year + if year: # Split name on year try: movie_name = cleaned.rsplit(year, 1).pop(0).strip() if movie_name: diff --git a/couchpotato/core/plugins/userscript/bookmark.js b/couchpotato/core/plugins/userscript/bookmark.js_tmpl similarity index 95% rename from couchpotato/core/plugins/userscript/bookmark.js rename to couchpotato/core/plugins/userscript/bookmark.js_tmpl index cdba3b40..cc04baf7 100644 --- a/couchpotato/core/plugins/userscript/bookmark.js +++ b/couchpotato/core/plugins/userscript/bookmark.js_tmpl @@ -32,7 +32,7 @@ var isCorrectUrl = function() { } var addUserscript = function() { // Add window param - document.body.setAttribute('cp_auto_open', true) + document.body.setAttribute('cp_auto_open', 'true') // Load userscript var e = document.createElement('script'); diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index f857fb96..bddd56a5 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -35,7 +35,7 @@ class Userscript(Plugin): 'host': host, } - return self.renderTemplate(__file__, 'bookmark.js', **params) + return self.renderTemplate(__file__, 'bookmark.js_tmpl', **params) def getIncludes(self, **kwargs): @@ -60,7 +60,7 @@ class Userscript(Plugin): 'host': '%s://%s' % (self.request.protocol, self.request.headers.get('X-Forwarded-Host') or self.request.headers.get('host')), } - script = klass.renderTemplate(__file__, 'template.js', **params) + script = klass.renderTemplate(__file__, 'template.js_tmpl', **params) klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script) self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js') diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 11daa068..56cd0d4f 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -12,7 +12,7 @@ Page.Userscript = new Class({ } }, - indexAction: function(param){ + indexAction: function(){ var self = this; self.el.adopt( @@ -60,7 +60,7 @@ var UserscriptSettingTab = new Class({ addSettings: function(){ var self = this; - self.settings = App.getPage('Settings') + self.settings = App.getPage('Settings'); self.settings.addEvent('create', function(){ var host_url = window.location.protocol + '//' + window.location.host; @@ -74,7 +74,7 @@ var UserscriptSettingTab = new Class({ 'text': 'Install userscript', 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', 'target': '_blank' - }), + }), new Element('span.or[text=or]'), new Element('span.bookmarklet').adopt( new Element('a.button.green', { @@ -86,7 +86,7 @@ var UserscriptSettingTab = new Class({ 'target': '', 'events': { 'click': function(e){ - (e).stop() + (e).stop(); alert('Drag it to your bookmark ;)') } } diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js_tmpl similarity index 100% rename from couchpotato/core/plugins/userscript/template.js rename to couchpotato/core/plugins/userscript/template.js_tmpl diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css index c27c1d83..9af32ed0 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.css +++ b/couchpotato/core/plugins/wizard/static/wizard.css @@ -5,10 +5,9 @@ .page.wizard h1 { padding: 10px 0; - margin: 0 5px; display: block; font-size: 30px; - margin-top: 80px; + margin: 80px 5px 0; } .page.wizard .description { @@ -52,7 +51,7 @@ font-weight: normal; border-bottom: 4px solid transparent; } - + .page.wizard .tabs li:hover a { border-color: #047792; } .page.wizard .tabs li.done a { border-color: #04bce6; } diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index b4857abb..eab27c03 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -111,10 +111,10 @@ Page.Wizard = new Class({ var form = self.el.getElement('.uniForm'); var tabs = self.el.getElement('.tabs'); - self.groups.each(function(group, nr){ + self.groups.each(function(group){ if(self.headers[group]){ - group_container = new Element('.wgroup_'+group, { + var group_container = new Element('.wgroup_'+group, { 'styles': { 'opacity': 0.2 }, @@ -129,7 +129,7 @@ Page.Wizard = new Class({ }) } - var content = self.headers[group].content + var content = self.headers[group].content; group_container.adopt( new Element('h1', { 'text': self.headers[group].title @@ -144,7 +144,7 @@ Page.Wizard = new Class({ var tab_navigation = tabs.getElement('.t_'+group); if(!tab_navigation && self.headers[group] && self.headers[group].include){ - tab_navigation = [] + tab_navigation = []; self.headers[group].include.each(function(inc){ tab_navigation.include(tabs.getElement('.t_'+inc)); }) @@ -157,7 +157,7 @@ Page.Wizard = new Class({ self.headers[group].include.each(function(inc){ self.el.getElement('.tab_'+inc).inject(group_container); - }) + }); new Element('li.t_'+group).adopt( new Element('a', { @@ -215,9 +215,9 @@ Page.Wizard = new Class({ self.groups.each(function(groups2, nr2){ var t2 = self.el.getElement('.t_'+groups2); t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done'); - }) + }); g.tween('opacity', 1); - } + }; if(nr == 0) func(); @@ -241,4 +241,4 @@ Page.Wizard = new Class({ } -}); \ No newline at end of file +}); diff --git a/couchpotato/core/settings.py b/couchpotato/core/settings.py index be2a0d01..16c827c8 100644 --- a/couchpotato/core/settings.py +++ b/couchpotato/core/settings.py @@ -56,6 +56,10 @@ class Settings(object): addEvent('database.setup', self.databaseSetup) + self.file = None + self.p = None + self.log = None + def setFile(self, config_file): self.file = config_file @@ -195,7 +199,6 @@ class Settings(object): def getOptions(self): return self.options - def view(self, **kwargs): return { 'options': self.getOptions(), @@ -254,6 +257,7 @@ class Settings(object): 'value': toUnicode(value), }) + class PropertyIndex(HashIndex): _version = 1 diff --git a/couchpotato/runner.py b/couchpotato/runner.py index f55d11e0..dcdac5c8 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -100,14 +100,14 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En if not os.path.isdir(backup_path): os.makedirs(backup_path) for root, dirs, files in scandir.walk(backup_path): - for file in files: - ints = re.findall('\d+', file) + for backup_file in files: + ints = re.findall('\d+', backup_file) # Delete non zip files if len(ints) != 1: - os.remove(os.path.join(backup_path, file)) + os.remove(os.path.join(backup_path, backup_file)) else: - existing_backups.append((int(ints[0]), file)) + existing_backups.append((int(ints[0]), backup_file)) # Remove all but the last 5 for eb in existing_backups[:-backup_count]: @@ -117,8 +117,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En new_backup = toUnicode(os.path.join(backup_path, '%s.tar.gz' % int(time.time()))) zipf = tarfile.open(new_backup, 'w:gz') for root, dirs, files in scandir.walk(db_path): - for file in files: - zipf.add(os.path.join(root, file), arcname = 'database/%s' % os.path.join(root[len(db_path)+1:], file)) + for zfilename in files: + zipf.add(os.path.join(root, zfilename), arcname = 'database/%s' % os.path.join(root[len(db_path) + 1:], zfilename)) zipf.close() # Open last @@ -178,6 +178,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En logger.addHandler(hdlr2) # Start logging & enable colors + # noinspection PyUnresolvedReferences import color_logs from couchpotato.core.logger import CPLog log = CPLog(__name__) @@ -212,7 +213,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En } # Load the app - application = Application([], + application = Application( + [], log_function = lambda x: None, debug = config['use_reloader'], gzip = True, diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 03332281..bbaf0525 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -369,7 +369,7 @@ // Create parallel callback var callbacks = []; - self.global_events[name].each(function(handle, nr){ + self.global_events[name].each(function(handle){ callbacks.push(function(callback){ var results = handle.apply(handle, args || []); diff --git a/couchpotato/static/scripts/misc/downloaders.js b/couchpotato/static/scripts/misc/downloaders.js index 5127275c..c3611a2f 100644 --- a/couchpotato/static/scripts/misc/downloaders.js +++ b/couchpotato/static/scripts/misc/downloaders.js @@ -68,7 +68,7 @@ var DownloadersBase = new Class({ testButtonName: function(fieldset){ var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf(" 0 ? ', ' + new Date ().increment('second', folder_progress.eta).timeDiffInWords().replace('from now', 'to go') : '') }), - new Element('span.percentage', {'text': folder_progress.total ? (((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100).round() + '%' : '0%'}) + new Element('span.percentage', {'text': folder_progress.total ? Math.round(((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100) + '%' : '0%'}) ).inject(self.progress_container) }); diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 74f3c7f0..09155486 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -370,7 +370,7 @@ var OptionBase = new Class({ createTooltip(self.options.description[1]).inject(hint, 'top'); } else { - var hint = new Element('p.formHint', { + new Element('p.formHint', { 'html': self.options.description || '' }).inject(self.el) } @@ -1312,8 +1312,9 @@ Option.Combined = new Class({ var head = new Element('div.head').inject(self.combined_list); Object.each(self.inputs, function(input, name){ + var _in = input.getNext(); self.labels[name] = input.getPrevious().get('text'); - self.descriptions[name] = (_in = input.getNext()) ? _in.get('text') : ''; + self.descriptions[name] = _in ? _in.get('text') : ''; new Element('abbr', { 'class': name, @@ -1465,4 +1466,4 @@ var createTooltip = function(description){ ); return tip; -} +}; diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index 6a3d4cbc..ca57f957 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -73,7 +73,7 @@ Page.Wanted = new Class({ } else { var progress = json.movie; - self.manual_search.set('text', 'Searching.. (' + (((progress.total-progress.to_go)/progress.total)*100).round() + '%)'); + self.manual_search.set('text', 'Searching.. (' + Math.round(((progress.total-progress.to_go)/progress.total)*100) + '%)'); } } }); @@ -87,7 +87,7 @@ Page.Wanted = new Class({ var self = this; var options = { 'name': 'Scan_folder' - } + }; if(!self.folder_browser){ self.folder_browser = new Option['Directory']("Scan", "folder", "", options); @@ -96,9 +96,9 @@ Page.Wanted = new Class({ var folder = self.folder_browser.getValue(); Api.request('renamer.scan', { 'data': { - 'base_folder': folder, - }, - }); + 'base_folder': folder + } + }); }; self.folder_browser.inject(self.el, 'top'); diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 0ade5182..5d2ed6b0 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -111,7 +111,7 @@ body > .spinner, .mask{ width: 100%; padding: 200px; } - + @media all and (max-width: 480px) { body > .mask { padding: 20px; @@ -185,9 +185,9 @@ body > .spinner, .mask{ .icon2.completed:before { content: "\e070"; } .icon2.info:before { content: "\e089"; } .icon2.attention:before { content: "\e009"; } -.icon2.readd:before { - display: inline-block; - content: "\e04b"; +.icon2.readd:before { + display: inline-block; + content: "\e04b"; transform: scale(-1, 1); } .icon2.imdb:before { @@ -200,8 +200,8 @@ body > .spinner, .mask{ position: relative; top: -3px; } -.icon2.menu:before { - content: "\e076\00a0 \e076\00a0 \e076\00a0"; +.icon2.menu:before { + content: "\e076\00a0 \e076\00a0 \e076\00a0"; line-height: 6px; transform: scaleX(2); width: 20px; @@ -213,7 +213,7 @@ body > .spinner, .mask{ margin-left: 5px; } @media screen and (-webkit-min-device-pixel-ratio:0) { - .icon2.menu:before { + .icon2.menu:before { margin-top: -7px; } } @@ -229,7 +229,7 @@ body > .spinner, .mask{ box-shadow: 0 0 10px rgba(0,0,0,.1); transition: all .4s ease-in-out; } - + @media all and (max-width: 480px) { .header { height: 44px; @@ -253,7 +253,7 @@ body > .spinner, .mask{ left: 0; bottom: 0; } - + .header .foldout { width: 44px; height: 100%; @@ -264,7 +264,7 @@ body > .spinner, .mask{ line-height: 42px; color: #FFF; } - + .header .logo { display: inline-block; font-size: 3em; @@ -274,36 +274,36 @@ body > .spinner, .mask{ color: #FFF; font-weight: normal; vertical-align: top; - font-family: Lobster; + font-family: Lobster, sans-serif; } - + @media all and (max-width: 480px) { .header .foldout { display: inline-block; } - + .header .logo { padding-top: 7px; border: 0; font-size: 1.7em; } } - + @media all and (min-width: 481px) and (max-width: 640px) { - + .header .logo { display: none; } - + } - + .header .navigation ul { display: inline-block; margin: 0; padding: 0; height: 100%; } - + .header .navigation li { color: #fff; display: inline-block; @@ -317,7 +317,7 @@ body > .spinner, .mask{ .header .navigation li:first-child { border: none; } - + .header .navigation li a { display: block; padding: 15px; @@ -327,13 +327,13 @@ body > .spinner, .mask{ border-width: 0 0 4px 0; font-weight: normal; } - + .header .navigation li:hover a { border-color: #047792; } .header .navigation li.active a { border-color: #04bce6; } - + .header .navigation li.disabled { color: #e5e5e5; } .header .navigation li a { color: #fff; } - + .header .navigation .backtotop { opacity: 0; display: block; @@ -349,24 +349,24 @@ body > .spinner, .mask{ font-weight: normal; } .header:hover .navigation .backtotop { color: #fff; } - + @media all and (max-width: 480px) { - + body { position: absolute; width: 100%; transition: all .5s cubic-bezier(0.9,0,0.1,1); left: 0; } - + .menu_shown body { left: 240px; } - + .header .navigation { height: 100%; } - + .menu_shown .header .navigation .overlay { position: fixed; right: 0; @@ -374,7 +374,7 @@ body > .spinner, .mask{ bottom: 0; left: 240px; } - + .header .navigation ul { width: 240px; position: fixed; @@ -382,11 +382,11 @@ body > .spinner, .mask{ background: rgba(0,0,0,.5); transition: all .5s cubic-bezier(0.9,0,0.1,1); } - + .menu_shown .header .navigation ul { left: 0; } - + .header .navigation ul li { display: block; text-align: left; @@ -397,7 +397,7 @@ body > .spinner, .mask{ border-width: 0 4px 0 0; padding: 5px 20px; } - + .header .navigation ul li.separator { background-color: rgba(255,255,255, .07); height: 5px; @@ -410,31 +410,31 @@ body > .spinner, .mask{ height: 100%; border-left: 1px solid rgba(255,255,255,.07); } - + @media all and (max-width: 480px) { .header .more_menu { display: none; } } - + .header .more_menu .button { height: 100%; line-height: 66px; text-align: center; padding: 0; } - + .header .more_menu .wrapper { width: 200px; margin-left: -106px; margin-top: 22px; } - + @media all and (max-width: 480px) { .header .more_menu .button { line-height: 44px; } - + .header .more_menu .wrapper { margin-top: 0; } @@ -454,12 +454,12 @@ body > .spinner, .mask{ top: 0; right: 0; } - + .header .notification_menu { right: 60px; display: block; } - + @media all and (max-width: 480px) { .header .notification_menu { right: 0; @@ -685,15 +685,15 @@ body > .spinner, .mask{ border-bottom: 4px solid transparent; border-radius: 0; } - + .more_menu .button:hover { border-color: #047792; } - + .more_menu.show .button { border-color: #04bce6; } - + .more_menu .wrapper { display: none; top: 0; @@ -706,23 +706,23 @@ body > .spinner, .mask{ color: #444; background: #fff; } - + .more_menu.show .wrapper { display: block; top: 44px; } - + .more_menu ul { padding: 0; margin: 0; list-style: none; } - + .more_menu .wrapper li { width: 100%; height: auto; } - + .more_menu .wrapper li a { display: block; border-bottom: 1px solid rgba(255,255,255,0.2); @@ -735,21 +735,21 @@ body > .spinner, .mask{ } .more_menu .wrapper li:first-child a { padding-top: 5px; } .more_menu .wrapper li:last-child a { padding-bottom: 5px; } - + .more_menu .wrapper li .separator { border-bottom: 1px solid rgba(0,0,0,.1); display: block; height: 1px; margin: 5px 0; } - + .more_menu .wrapper li:last-child a { border: none; } .more_menu .wrapper li a:hover { background: rgba(0,0,0,0.05); } - + .messages { position: fixed; right: 0; @@ -823,7 +823,7 @@ body > .spinner, .mask{ margin: -200px 0 0 -200px; } @media all and (max-width: 480px) { - + .login form { padding: 0; height: 300px; @@ -833,9 +833,9 @@ body > .spinner, .mask{ top: 10px; margin: 0; } - + } - + .page.login .ctrlHolder { padding: 0; margin: 0 0 20px; @@ -843,31 +843,31 @@ body > .spinner, .mask{ .page.login .ctrlHolder:hover { background: none; } - + .page.login input[type=text], .page.login input[type=password] { width: 100% !important; font-size: 25px; padding: 14px !important; } - + .page.login .remember_me { font-size: 15px; float: left; width: 150px; padding: 20px 0; } - + .page.login .remember_me .check { margin: 5px 5px 0 0; } - + .page.login .button { font-size: 25px; padding: 20px; float: right; } - + /* Fonts */ @font-face { font-family: 'Elusive-Icons'; @@ -936,4 +936,4 @@ body > .spinner, .mask{ url('../fonts/Lobster-webfont.svg#lobster_1.4regular') format('svg'); font-weight: normal; font-style: normal; -} \ No newline at end of file +} diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 5bb226f3..bb39de9c 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -741,7 +741,7 @@ .tooltip { position: absolute; - right: 0px; + right: 0; width: 30px; height: 30px; } diff --git a/couchpotato/templates/database.html b/couchpotato/templates/database.html index b12ad23e..e5125387 100644 --- a/couchpotato/templates/database.html +++ b/couchpotato/templates/database.html @@ -26,7 +26,7 @@ ) ); - documents.each(function(doc, nr){ + documents.each(function(doc){ new Element('tr.document').adopt( new Element('td.id', {'text': doc['_id']}), new Element('td._rev', {'text': doc['_rev']}), From 304de5adb615448a144222eaafb53dce8f96719a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 Mar 2014 23:47:35 +0100 Subject: [PATCH 196/468] Load correct html files --- couchpotato/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index a16a1f5f..092bd406 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -55,7 +55,7 @@ def get_db(): # Web view def index(): - return template_loader.load('index.thtml').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) + return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) addView('', index) @@ -67,14 +67,14 @@ def apiDocs(): del api_docs[''] del api_docs_missing[''] - return template_loader.load('api.thtml').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) + return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) addView('docs', apiDocs) # Database debug manager def databaseManage(): - return template_loader.load('database.thtml').generate(fireEvent = fireEvent, Env = Env) + return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env) addView('database', databaseManage) @@ -108,7 +108,7 @@ class LoginHandler(BaseHandler): if self.get_current_user(): self.redirect(Env.get('web_base')) else: - self.write(template_loader.load('login.thtml').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)) + self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)) def post(self, *args, **kwargs): From 17787c5a4fb1cac480377a8926e0261c18f16442 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 29 Mar 2014 12:45:12 +1300 Subject: [PATCH 197/468] Fixed searching bug in IPT provider --- couchpotato/core/media/_base/providers/torrent/iptorrents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index 29580d49..0433d3ef 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -18,7 +18,7 @@ class Base(TorrentProvider): 'base_url': 'https://www.iptorrents.com', 'login': 'https://www.iptorrents.com/torrents/', 'login_check': 'https://www.iptorrents.com/inbox.php', - 'search': 'https://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d', + 'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d', } http_time_between_calls = 1 # Seconds From f8cc8acfec6bcaa54d23df444768da759e25dfc5 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 30 Mar 2014 21:54:44 +1300 Subject: [PATCH 198/468] Fixed release files bug in renamer --- couchpotato/core/plugins/renamer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 2d2e94a2..1e83140d 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -190,6 +190,8 @@ class Renamer(Plugin): release_files = release_download.get('files', []) if release_files: + files = release_files + # If there is only one file in the torrent, the downloader did not create a subfolder if len(release_files) == 1: folder = media_folder From f8b2547a45877fdec1dde00854a6cb3735998739 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Mar 2014 17:33:44 +0200 Subject: [PATCH 199/468] Movie add faulty parameter. fix #3020 --- couchpotato/core/plugins/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/automation.py b/couchpotato/core/plugins/automation.py index e17dc36a..b0566249 100644 --- a/couchpotato/core/plugins/automation.py +++ b/couchpotato/core/plugins/automation.py @@ -35,7 +35,7 @@ class Automation(Plugin): prop_name = 'automation.added.%s' % imdb_id added = Env.prop(prop_name, default = False) if not added: - added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True) + added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_after = True, single = True) if added_movie: movie_ids.append(added_movie['id']) Env.prop(prop_name, True) From fb7dbd57162e1be5ac632860915c13c87533c909 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Mar 2014 23:26:14 +0200 Subject: [PATCH 200/468] Update Userscript --- couchpotato/core/plugins/userscript/main.py | 2 +- couchpotato/core/plugins/userscript/template.js_tmpl | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index bddd56a5..4ca8ed37 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -15,7 +15,7 @@ log = CPLog(__name__) class Userscript(Plugin): - version = 4 + version = 5 def __init__(self): addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True) diff --git a/couchpotato/core/plugins/userscript/template.js_tmpl b/couchpotato/core/plugins/userscript/template.js_tmpl index 202f5dc6..5a32da39 100644 --- a/couchpotato/core/plugins/userscript/template.js_tmpl +++ b/couchpotato/core/plugins/userscript/template.js_tmpl @@ -25,13 +25,16 @@ var version = {{version}}, host = '{{host}}', api = '{{api}}'; -function create() { +var create = function() { + var A, B; switch (arguments.length) { case 1: - var A = document.createTextNode(arguments[0]); + A = document.createTextNode(arguments[0]); break; default: - var A = document.createElement(arguments[0]), B = arguments[1]; + A = document.createElement(arguments[0]); + B = arguments[1]; + for ( var b in B) { if (b.indexOf("on") == 0){ A.addEventListener(b.substring(2), B[b], false); From 4f059c2549efcf0863ff6325e7b75dec3424fda0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Mar 2014 23:32:56 +0200 Subject: [PATCH 201/468] Point to browser extension --- couchpotato/core/plugins/userscript/static/userscript.js | 6 +++--- couchpotato/static/scripts/couchpotato.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 56cd0d4f..e270dc50 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -67,12 +67,12 @@ var UserscriptSettingTab = new Class({ self.settings.createGroup({ 'name': 'userscript', - 'label': 'Install the bookmarklet or userscript', + 'label': 'Install the browser extension or bookmarklet', 'description': 'Easily add movies via imdb.com, appletrailers and more' }).inject(self.settings.tabs.automation.content, 'top').adopt( new Element('a.userscript.button', { - 'text': 'Install userscript', - 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', + 'text': 'Install extension', + 'href': 'https://couchpota.to/extension/', 'target': '_blank' }), new Element('span.or[text=or]'), diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index bbaf0525..27ae9c09 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -317,8 +317,8 @@ return new Element('div.group_userscript').adopt( new Element('a.userscript.button', { - 'text': 'Install userscript', - 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', + 'text': 'Install extension', + 'href': 'https://couchpota.to/extension/', 'target': '_blank' }), new Element('span.or[text=or]'), From eb0a8454bc7368d5bd81246645c101d9541e58e3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Mar 2014 23:35:18 +0200 Subject: [PATCH 202/468] Update description text --- couchpotato/core/plugins/wizard/static/wizard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index eab27c03..d5302e73 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -35,7 +35,7 @@ Page.Wizard = new Class({ }, 'automation': { 'title': 'Easily add movies to your wanted list!', - 'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' + + 'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the extension or drag the bookmarklet to your bookmarks.' + '
Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)', 'content': function(){ return App.createUserscriptButtons().setStyles({ From 95369e79a565c84958b179e6194effff40ee3d2a Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 31 Mar 2014 00:31:19 +0200 Subject: [PATCH 203/468] Add profile to returned in_wanted values --- couchpotato/core/media/movie/providers/info/_modifier.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index cea0f49f..bf257272 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -104,6 +104,9 @@ class MovieResultModifier(Plugin): if media.get('status') == 'active': temp['in_wanted'] = media + try: temp['in_wanted']['profile'] = db.get('id', media['profile_id']) + except: temp['in_wanted']['profile'] = {'label': ''} + for release in fireEvent('release.for_media', media['_id'], single = True): if release.get('status') == 'done': if not temp['in_library']: From e8dcf5ee0208d2f086d2b73ec37a095c9a23c030 Mon Sep 17 00:00:00 2001 From: bazbjzy Date: Mon, 31 Mar 2014 17:59:14 -0700 Subject: [PATCH 204/468] Added ability to configure Pushover custom sounds. --- couchpotato/core/notifications/pushover.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index 1925f729..34c0cb83 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -25,6 +25,7 @@ class Pushover(Notification): 'token': self.app_token, 'message': toUnicode(message), 'priority': self.conf('priority'), + 'sound': self.conf('sound'), } if data and data.get('identifier'): @@ -82,6 +83,12 @@ config = [{ 'advanced': True, 'description': 'Also send message when movie is snatched.', }, + { + 'name': 'sound', + 'advanced': True, + 'description': 'Define custom sound for Pushover alert.' + }, + ], } ], From 686e0a9441c058015a6ef4e13096a4674c3c5c03 Mon Sep 17 00:00:00 2001 From: bazbjzy Date: Mon, 31 Mar 2014 18:04:02 -0700 Subject: [PATCH 205/468] Removed empty line --- couchpotato/core/notifications/pushover.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index 34c0cb83..ea4b00a3 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -88,7 +88,6 @@ config = [{ 'advanced': True, 'description': 'Define custom sound for Pushover alert.' }, - ], } ], From 5c89a52f23d80bb546fa3b43140c47d44ac05c1e Mon Sep 17 00:00:00 2001 From: jeremiahelroy Date: Tue, 1 Apr 2014 00:48:08 -0400 Subject: [PATCH 206/468] making the scanner follow symlinks --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 9818ff06..6b7c7b0f 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -129,7 +129,7 @@ class Scanner(Plugin): check_file_date = True try: files = [] - for root, dirs, walk_files in scandir.walk(folder): + for root, dirs, walk_files in scandir.walk(folder, followlinks=True): files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) # Break if CP wants to shut down From e609931d2ca82018688982c5cee3ff68ecd0e6b3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 1 Apr 2014 20:33:47 +0200 Subject: [PATCH 207/468] Show off browser extension --- .../plugins/userscript/static/userscript.gif | Bin 0 -> 2409884 bytes .../plugins/userscript/static/userscript.js | 2 +- .../plugins/userscript/static/userscript.png | Bin 101726 -> 0 bytes .../core/plugins/wizard/static/wizard.js | 2 +- couchpotato/static/style/settings.css | 10 ++++++++-- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 couchpotato/core/plugins/userscript/static/userscript.gif delete mode 100644 couchpotato/core/plugins/userscript/static/userscript.png diff --git a/couchpotato/core/plugins/userscript/static/userscript.gif b/couchpotato/core/plugins/userscript/static/userscript.gif new file mode 100644 index 0000000000000000000000000000000000000000..d4b00bc366368d341c91ba97334456a08a657b98 GIT binary patch literal 2409884 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

Mywlg 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

npis=D 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

W?()v0ycHt{!l1uzDpikaD(rGtnwK(8P1K-+R!c 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Ð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 z≪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

GFc$4o!OjR_r?!>I1tkx|;A=&Y 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

l77Rgc6OSPuPQH;a~w|zpT^TCU=NxNlwX^Vh> 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%0Df0@FYx5 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

WMvHna;Jh$4b@}{p{|~sIfK+ z>^hBjWIqIN%~+mjHBs-QSsn@r3Zx?rOIAmf#Z>ZR$41M`xqDM;`!M>4Qkr$Tl?=UsErE$dXS^~YR~YYdshwe_lvoTUOcLLISYsB?Vg3c)a4{5#3V!n z60>?sF4goFjaFO^D)sr5Gy1C=&z;}I>3q28zt$SW0xyFc-u?RH={5RRr%)(X8SiG5 z#os$&Kl9AQB|SH;?lm;U=6o0Y{WSQ*F|Zq2dC)#?e%KRFnPI6(%Cw)5r_Z?U(!FyB z$B^LPu2zH|%aU-p6A+kACs6h-^Y+(%f|=2vD{8Dm{Du|Jo6CI(Yn0jCse$bOyZcUs**mp>-j_c38k&357^%(eCXoYrbGe{9+> zu3+O{CoDJV{lv>l?K6G-9q(fMq|}!{8bevoqVR0QqyGFwY$u=8&5*z3x&3Lual*xT zi>J{D;ed>9ryVrn3s*<$G<#EfIfKbc@lLIfkV=TQ3}Iw7SqhV@U|t|O(e~QS|GCfD zchl$9yO8jr>0stQK5bJ<+_bAn++u)&ONm#HSKR?n|KruIr(F{(DLbF~-!tAj zI%_y`E@3|CoEvzb^=>u2_Wqk6H`(7O_qFA?Exu1QOzTEYHT=oW@z1`yV6i=b@X44_ zxrKdBNV4PJ_)8m|4WBpRtGqmxUSUkQ^%ETOuL&KuiNMZrEIwc}nvVPDg6dqY-UYiC zK)q!?x}%|33`O$%2y&tUUKG6nj!i*_#45Srw>0iuwU~=`Px)Mxp<&T--8t6o5Tf!c zn9HS!^U8CL*5>ZB>2OUl-(>ntRAv7A?x&fUf0+pMuiO}(SXsW0O@{}qw7Zq1kV?Cq z%9C|b)#WbbcxT*78Ep4)`HCeDGHNz_Gij5pVmE<_GSImi*GXCKpkVna{#HjV`(Vyk zr?|YLBFEUp^r9T+RYkYW>fHpT#Ds0vj~{bB!xu9M9lMM!Z{NIIZKH*r3{-qiT~X^n|eJ`WAT9F>oc)@t2k(d0b8Z}-3XA099B zL|on-Iz8~8cRpdhnep~9#|@rODu}E}QP614Y_*IB;<*^MbYp`lVlk7*y zLZRn}MWLIlM5fE0)7>ndeo|Lzdk-sS)+_MwaQu_N<0jqWOP7cEisZb{KP~lo4E@OF z>hUjs78pI9tJ-E#E*BWsnE7j`fY1EqH0Q5|oq~g_7EDjV3PY~ds4e7)USIng`+aZz zp5KQxwRr)ZI3B6ELd6#;7DsCL?>220^V|1H+&gDV9m>2yrVPtw6BsH4y?0B=3X{lKjZ0ns3F7Gt{XoO6ckvFX-~UdH0a z)|Oj&@>l8+mGa~IWR~|5FPUN+OKf<=%*DylhO2F&^x8RBviX4tZ&itr6Pfcu^{*bL z+d55tudk16LbXy=&2T}1oXsOz*!NZlkayn9g^dS=J_v;yr# zQ7F;gt_U}U{RgZJkq8~hdspjlo6{fo!v&p0+xhollAKe57|9&iX$D;A(oI|U_}}Gk zB;$Zma#@0pV!G3oRfO)R5z$3;S2WzM7YK%mLm3$khkIKYrcqq$MFgwG?c#W<&^T4% z&f!e8U^7azC9b>enRl_G_|j!Pz+f@<`yv^>Pb5v#?zelL)1h3R&&LIJ_W$UcyrRUp zV6M{gbsC#^GXFX=YCr|+UU@h__gE*JZ-`DJj{6OUA9xf$mhrGPW5=~eW-ue&v@CL` zN^F%Y2mklZVb8R4QawZ{)ZppPB{{>VE$rgKuHPmN&3Q7)xu)lRg`6qcW0Tqv-5;xC zisC}+u-$DGH3Gu(6(b+Kg^gaV>o@FfSmTd(`y`fcfs}^wq`3W;aTlHy<-vx)Tzx

vgQ}}S0Hq2E@OB=h|Q`nPhzw3C4<*0r735vVP~C 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

xhZ!U9^kg2(25iIx=}g>JdI_;7vOA9@9{H!D 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⪼|7f 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%0A`Y zYO0!sntG;M`nI|T*82J;d-s{_-EU=JYHw)fE~sMLFTm&mhHhzXYUyig?e?F_H9PkM z_MYZePUZ&#EDn$b9^KN$%hKNczZl)gn(TehnPTG#<9gWkfU%v6pS``qe+t$d z|4Y&ZrD_LN9S(XrTAMh!9&vQ_b~+U5d^ps_@~DfIr;DwHi~Ug-w@?@N09Uu;ZeIRJ zyiOcF5#s4X^*Vmi>v*X5QDyIAM&91e-V_&azi4m&@Z;X@Cjvu!LZT@re0<3YzFx|H zCr|kYh6Mx!1coLBhQ*!?4i1XU42n()3k&>@O^-Ys9-c|ksTl?7*%!}e=V#=VXXh5>7F^29&CAQboOiJzzl2p-R3T95 z#buX^FV_}VHJ4Pima!_!tDCM=HeRW&6TtN9n&xYjSFY9Fs%>hkYiz1(;WXUnzJ8;v zskNi|R!>X&AiL@Mt=qk~+Xvcu?zRhHdf!CH?T+rA;oko7-oeS

cstALt(*7`i(+ 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

{Y)6g@cTM(~ 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 literal 0 HcmV?d00001 diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index e270dc50..7e819dbb 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -96,7 +96,7 @@ var UserscriptSettingTab = new Class({ }) ) ).setStyles({ - 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.png')+"')" + 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.gif')+"')" }); }); diff --git a/couchpotato/core/plugins/userscript/static/userscript.png b/couchpotato/core/plugins/userscript/static/userscript.png deleted file mode 100644 index c8e7657783a20c6e920521f23e94fcde73964a25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101726 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 EKNb^AV*mgE diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index d5302e73..733ef1ee 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -39,7 +39,7 @@ Page.Wizard = new Class({ '
Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)', 'content': function(){ return App.createUserscriptButtons().setStyles({ - 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.png')+"')" + 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.gif')+"')" }) } }, diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index bb39de9c..1f77b8e1 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -698,12 +698,18 @@ } .group_userscript { - background: center bottom no-repeat; - min-height: 360px; + background: 5px 75px no-repeat; + min-height: 460px; font-size: 20px; font-weight: normal; } + .settings .group_userscript { + background-position: center 120px; + background-size: auto 70%; + min-height: 360px; + } + .group_userscript h2 .hint { display: block; margin: 0 !important; From 0f7c3f5d0f0c990f7cdebd924ef0901009f2c1af Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 1 Apr 2014 20:38:27 +0200 Subject: [PATCH 208/468] Use correct id returned from automation add. fix #3050 --- couchpotato/core/plugins/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/automation.py b/couchpotato/core/plugins/automation.py index b0566249..39d7c9e7 100644 --- a/couchpotato/core/plugins/automation.py +++ b/couchpotato/core/plugins/automation.py @@ -37,7 +37,7 @@ class Automation(Plugin): if not added: added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_after = True, single = True) if added_movie: - movie_ids.append(added_movie['id']) + movie_ids.append(added_movie['_id']) Env.prop(prop_name, True) for movie_id in movie_ids: From 9540ae5a198bcb4ec6c5ed18db0d8b4a11f7be3a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 1 Apr 2014 20:48:58 +0200 Subject: [PATCH 209/468] Hotlink userscript gif --- .../plugins/userscript/static/userscript.gif | Bin 2409884 -> 0 bytes .../plugins/userscript/static/userscript.js | 2 +- .../core/plugins/wizard/static/wizard.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 couchpotato/core/plugins/userscript/static/userscript.gif diff --git a/couchpotato/core/plugins/userscript/static/userscript.gif b/couchpotato/core/plugins/userscript/static/userscript.gif deleted file mode 100644 index d4b00bc366368d341c91ba97334456a08a657b98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2409884 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

Mywlg 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

npis=D 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

VJ0RN+~5u zJ0|;)0<{6f`Bo>kf+k+g$p2pT!(-+&w-9re5bF&8 z9Q$Z494#NwaiUZkWe(XhWVexb!^r-wj?}ZvbIZ>Jp{W|>8EM2BP30h_vn+XgELcvC zl$snq2nVaKJT=UDKLlby02jkpA1ubA4k7Uu(Hx6E^3X?|{Nf?nzaQ-dDWtI`tFF*s zCODCKa1}3(QZn4|4nqwkOA3+gntZjKEd=OUFfV9)`wDXmcTh|zu^Mf&R5xVjv8^7~ZjR-J@w8TBbpEap^ zb9t%~_=W4~ND0Q=1Z-zT{sHP%HX&6ofVYl%&Aknclq z$!2=KUJa4Q!mj4QuX<%0!J>Vlql3`p(gm)3yHRI96xCHy9x6}=LBmi2@+_03oCq)f z4ff@+STqptrOHYSwX$SMTQH>he(~GGK_yaZ(9Z0!zk#ECkbE95j_~A+#la>%$QyM6 z#jlK&oUD|(#c|l?1$X>%V%&1-=@Pz^S8`9hfHObtg;-U9qx{%L!i9IW_q(XDxk5F# zA59x96-$#rhL=*H*?t^Y0KAw2O(onqZH6WRYSarvy)s0#z;Nxv+DDtpr^GnN%2XI8 zjOvThpM0_XkR12joC=jnPw_Ylx!m!w_%_({I3I3fKce?!z212or%$4PV&o$z2agAb z^Czd}QHV}U!y4SwaR_2TtUsTt>J140Bv~lITv|ESXrCu$=*Q7-dn0sEJfti#7YMhU zK)@{lAWP2kOOD&Jmg6FzoR^VCUkPAMfeQTqW+JJ$hyWu$n22KYt}^Vbt=4-U_WQi3 z&bauYHFdCi+JDOa?70;tAdGH$YimvMO}N8zCWa+*XF`*vBWxo{+xGX%Pf46(HT~-2iU-_cM)^&%-L>CG;e`Zfp&vQo5UAir9nlQE z&iF#$K0^W{?5P{*fg4Tob=m|J(2Xd1h7hWA6eF1{9>-;vVij zPn{o1jVXxx_GKV6L)iwaTpm1h>NYGoS>X+9L;951OqaNIaYW!N4C;ozhMIfYr~MG; zxMfnkBQfZdM>CHFJGhZV|kaVBM%NOXG_;qFD$0>z*i z6c85$lt6@A9s;R}&G#4;YyGI1W`Qz(a8{G?IzNa2VVvb}3c-8ab8F$lRgS#3&WR}Gz7>E(BuO1;4~ zaZPrQt`!ye3Em=+`-`GReVzRD!RD~WJz8~FWWSR`nRDV&9c zIn^57>9%N#?C@wx;z}c%CazwWRf9=H5qc!X^B;=0LB+GOS~>z1E8igd zC-o86h+xD#39x zB6h{8V81p_+_$HUN1n=iDc!Y#e^5)uoc1Jox)oFquUMjQOngKmL^Magk_&Qh}yz_(B^ zvI|Eg5p1Zl#h9x7goHDh==!~oX%o7bV7?dDlD%z=0{k8!o_oS-w>!snk0>`{iDH;a<07Tv_idO==`rZ3;e9 zchj+2FL{1$WdAE-|L$6n_xuP+uiy7lO-)!TM*|-qL9Daj4C341D3}@n%xbxp=x=N? z-e2Hfd%V-B0^QQc{HE0ZMkP6pttN@TI5W0;A93+z_0H2uLzJu*rB06&_dVf@r7kfw zfxw=EZlLBq68zenx*ZCFgm-i8a1tl3IP}TnxJ}q`e)lN&?>Qos5F%(v-GrjjC4wI! z;U1RXZ!)|k&AmTGzQuU$UGE=A8T@|YNPL==lUua9ClP1MxF7kh)NjL(Cd{j2?!!P7 zcZy)|ym>*Z=|cC5oYksji_`Cqx+Y`zIppc8X#REFtJ|4W!;sQf2%vX|UtXMIX;dxV zXh@DOii1#=w9f#jidK$F3Pf*~+T;~P@S>Tffk-V!1V>?EH`0E#$+~QRrFmJ#am$Y@ zb|UYcVOnz8&YOQzJ?F=}5~cP%pY+Yo(rW6RSQ_fvV{yi)KXOMCN4;z(#73d6~&q3uZc*K z${^Lck}d}if}oxItA%>mqAwG-cQ5pbDhKAQ#*j}-FV!BL>X#{hn|*O}<2W0};%8Z^ zYT_xZpd@z_h27uS7&3s1&lIa_Q}dn+J{OahJR=H!nHvaSc_(m%Ij8c{!PgObJfJ86@(>zbwHbjF_6 z(Tarw9JU8lluSn2W>8>}ENg2f8DUeM0lh41XCslUBxhhOvz=D{r2)>^K|agzhC`(u z|1*dJMnF?=hGgT39!%hy1_)kgCBz{FwO{dN9o8m6bZ46ZLNJ6~)w^?|YbWy# zALA36^N_P*&BWfc1d*cYiN^B8!3h(QMu3U5cK443v4mGu2}`trpg^f25lgfl2P3LuCTFqdPSC{P6uAmHgtPG!$jCfnU&9IMERym zFfT02>OggeRp|YpKs;hQPaI#eSs>B#qvSQrCJA>gp$_~GIh3m|dbiX=tY{-#{h0$u zYG^$@)^YaYppw%1Rhwek3IJ+uY6F3`7O($&v&mY`zcOu8|KA6l+K+qGkmI>-$+mmx z^p(4_igVV$Y&}=8+p!nfw&uXQ%r~w|-pz4S{W`oPC~Kg4H#_+5-3+5>%}TtK@G3Am zt^@~CVYc-bHp$q>pH!SvDr!Mmg9O zoRs;Q<(0NyPcI|uIDua>rhGXYd^{-7(yO>IrSM}>_r_Ir?`#`$M^5-P_KmYli?iom z?0gsBJaWu$IJ_On7OUzz{=l9-n4Y5vi-uc`Hs_a#=Z?3oewzNGN3%O!Js#-$0C(XPL^U>#*>cW4ke1nNgGw;HxUM|c0;u$Q{q;$b%M=XSwhd-+t_735Hdfu(hdK#qCS-Wwp zPW7^4)Ld0{Y_$Ip09%Z3lRh#ZBO!_rwE)(TE2FvRwM2N_& zMAxiz?ipVD5N~OV%=*cmDgIcY){=cA^)!cOp-_WHJm7t{kivF+yns?kHcO1=IT>W7 zqFkE>1i^2pzOnI*ri+>di3-n=GQC<a@A!I!nSPB=_2J!aZCVqs$r&5h(+7^>!0DLt+B&{tAUyPs z@6L={n%OXkrN|%R1t8_$2%BbZWK5s9@w4kc&YA2UeZOcE=6G?c2GP6&s3}Bh`Qmm? zywLR;Im}LuY5H`P?=#`=*TxFj0aWwDD@POk@9y4IY}|XEokj7;^e)jD+-~W*y(0M6 zQp?hD=0+uyNu1G;`C1`<*eja}z_H?I{fqmS6B656kN{eXQNSuDY|m*D4+Gd&>}gy4V# zKiAFn8?IFY>+9Tlx8xZ#$Z2~-g>bvgTz{s^+)sqxum!>GyMkK771z~GE@of9i6bnp zn7YhmRW=2`X0A-EbDA+7Wcp&yJWq#;Z6H=$63yXlg6Dm?^IQqsLr- z7b1l{t4j~Q^XreBeGdJKe|_#VwQ^_aXeVS^!ce>52cE5pa7*%`NI;3rEQQU7fUlf- zlpdcHt&Cb74VMc(=_OuSUQMOF5_@&eJU^-57B z>EFq9nHAp~^ZRtn9s=JN#CrZ!%aQ~BtNr-Tat7tPR3u=2+M)d{ZNDT(q)jh#$KsiP zZb&ry06P;{s|6!p+SS6mQB4z?`7@^X)TWDX>lIWfA(Kdk+lt>>)rR#c{kN;71{c1! zrS)sP+yqp5ue+l3ycqI>ZoEA2tWJ|2hYnYPZ8qbi=lRDIdJwjq7iBQv=HKLWU3AtR z)CY21@)rdvCvItXXx6~{Ox%A_uYLvSIMJdENcBp0+Q$7D`p?x&+*!{t6(yF6 zZI3&|!^pVvQprW~coa{(mja&y+qHdFv=8IeX`v1;q=~=Ovl`Zib^R#rc7bx;bJ|$; z7B<@HQbq~A)ZQyBLpcn>T_$R%4KJxmDc%^Vy1#okGrTh2#4F`21xr;PhEVOeFDbu- zIBoZ#I+l?5kdZVb&Or~Xj{@JUA5rqt*KO8!>btDpj5-M89EEX8T_ZPnMsN4&yZqJH zpB+g$)7Pm6>(AoU&hXmRbaZnD6nltf3k>Qy8VBZ|f7tR0L$K6`9Hz z8Ji#+n>b9BP&95;jd9Jxq@7@ri!jcInDFZH#6?VkX-rlhG>L~QH3DaKhX1byXHktS ztcOs@Mn&xqlRRAE{zOrQY|;L(*$OUc7h>XvFI<%05ZBHwL*7w9Jy?L>1m%bJH#!O zndJFH`ln5DRb!3tCgv+Rw~bh(L$E7m`XUKGg~`KVs;APLvrG1I#w!rgNlNN(oEd;> zn(lRJJ+6NZmn%OxT!brc#}@=bXaB}59MhXEiW*EXuh_^91SI77B^1rI&8-=yhxAys zD_K`4zv*gf+fK1-fiy=?Wi*?Z+e3ylru$ED6Uip!+YpNt+(G-LLT{=k5b`qFeE0-1 zsxN8)(<;zW!ORIz3p_4OZjLntVofx) zZZ?@zvf{|LGRNb%g66nltuA=h&COS=tSMAFvvWz!a|oBYw3X?__c%K@vyaeeBe|kK z5uAa6I3s%;xyanG8fO=hpX6l{eg=7+2;LAMf+}0mDJ}>^h|jao=3*0y+(ANJc< z*g77bt!p2{wmEJ?wV)1OD6}M~QM$AoXkkBD6WCtQ35ZZ}iNvaTSy>=s4gD4mhUeH) zrjoWH>?t@?7h7vgj2?bE$c&rI9r)bianckgxbyZ-YpLW&7V;5+c6tX|hR z-W0@0p|Y_AXf@B_hhvpyW4Lz4<90AQA-HER@mBU$iFa3$0NLi_vIztbwO4}@0H_6B zOxZP00{{R}KzJ9pyw7NO|E+#rtez!6^9&5e;2f&)-<)4BFV^4M!OJtIO@+aPkTsB6 z=t<9Tokm-?V-2JO57q%8#b4cLA$|LC<#`CG@+XweN9Y3@#2;e(s`g5*Ty*POiScu^ zQV{g|Pmz(8*LSbNDg_o4)h@AV7P?+1MyI6Pb~T%dNff^UJFJGbCdM&I+y3{BmkxvJ zE;AR88??@bR(y8Uu8fo+g@%)xdvL8HMtSJEKX`oh1pe+^8f6?W4492H;{VTz(f;yV zi&X{yZ75VRcUPWuM6tQeB}u!anu6zwmlLb!LQ-xfX5;bxP^0FFoB+I{I97iJC)^y* z7lKZ-w@ZcEu$_UfH$%$XCl-Ah%qUc@hcRYnF;r(@mjgSC_x7*%E7B~%5e}f2hrmSR zsxEu_@bI?pUAHEX9EJ8x3j3{{g1H?CMr~tSk?O>Q)wOQTB-0@%Y|HVKK%`M;kWYN^ zjJD$tndCM?+Z&m4M|Vlw9YhaG_#9Oqx$l+`wxGn-NI!$O4S%la&+zsS1ECsmA$HYl zx^C$uSc-3zpIZ->)^dYy$#S<4`dj_xn{mPYt@rPknQn1naDu5RQQDh?qD(())DFL0 z7S0G@dxMxQBD~4K)G9%z;80#_LjmI>ai)G-rU)t}d!S+LvK4kKcQ}^KJ_#Ej+sa$Kss3aOg8^GymW}0#@Dpmz2j=FK|FLz}e^EVe8$VlYP)jc* zh#;x1gfuMOT}y~`2}%eeik@A%mu^@}8UaB;K^6ogM7lvq1r!Anj1Ldr-=Fyl&b;Q# zyk_p}y5E6+shfau{i zBu*6pC`6r@u1!km!}#!ko<1~-AIx5bi^fOkOn`+ZaBMRY9v@|Pn!ZXQvjmb%1D*M& z700AUbPnKb3J}7C@hugElo(kamLwVuJ#X&XDa0&zKvIN2r8~%?l%?E>cOQ59QgYA< z(uLmy$wy-uIzqWfMc*SB&?7uyd>;z)+v&9yw>`~$KikgO|^vz zuKBdz&-swY{5%`qa!OmvBd3&*jiJlMEh&fb+67$!z#xSusGo zCQ8X1lmOsj9Rr4gQS5@CKE>UVvZ+@?=Agj@j<(nW!OJw5^2KcnbNJeg({&C|cr#>t zw6CNOa}L~pbC74g1KV9OwX7kdj>sJPQ4pUfHVWuw8m%K1g$N{>HACoVFayGu-a8PL zAt*MFtd0fv#JqAz7EmHgZw+Begur(oVn;Cbz(X7saMp*@#lue2n?dco53&nzNdllp zv4>B<{scR#6Qdn=v!vm~Tt zD0qq`sj4KiQ>ZwT>6_x&!k_Vbol4ig66(cz6FNuoCC!tS9yxx$KAlJ1vq&c5#(ueXzPj@RRNHx`5QE%UN-g7Qg^erg!^YcXt?1#+uRz}IY} zIea{zlq1F1`9JsJ8&^JaP&PS+;Ff~$_}uXLtC09@2sC~sJ~CY4{*e-8)7a;jLkv`m z-8_i(H1Rn~){c^o-QLUzM+5@$H;#;JV1?sfuAK|dom?P=!A zya9Cz8SsJf;7)0v@GBGG;ePbDQ22>jJPgPzH_3~!-;P$$Co|yz2xW z4KeNU6Qd;VzZ=_scRwEwweluQPVCGxpN3tYs9z?8YKFFvpQgNL1BvC@Xx8(_)FT%+ zIGSC|ii#tVw;4F3Pji-rkG9a6B5GLa!GNv-#|^LCt-=pH_YLl7%P_ znFOF|sKy#L@f5N}gI<|^nNq~VmC%}RwN^SLn9dbjY+|KD=6TJ0YN3FfVJJT@rsLbB z$K87mg-h`6OxraX6*?sfB5oj)tTj0nDyhAJW){S@4x>el-4HyPdFw!QD#cTar|5Ms znf)F(^h&rys+@Mhb6={@0O<=%B?!QY=|cjLzb(U<2w1!ND#O&)p&Qa~MfYPOuVIv>4!6zqDHeEVVsx$Z-vrhQ)N!*&ouRqkN36E&cdy4p z3Tc2(*UTrYUjJ8wta#N}XD2Aua|^9M?H+ z96Ps&7!M~O4-X$NpD?cw8}Iome1hlsq%HZ+i1Q0c2%VBY&tE=|<2lbOd|pE4yrhz- zh^Uw_pP00Qgp{1*>A0vQDQ_SpEp|a#<$|2a1#O#)vYMCWG^MfZ(z2>DQp~cfB64El za!NXKs)q8sBJz@Q@^ZQgGLj1BzKZgvs*A6h zJy~7fM%}4-w;QXYA|a z?Hd;7Cv?$2IMP2P$^TZEe<<0XloH@`IUvA4;6`vju-^?P-kYABfj7;9c<@09&A~T9 zLay6~`1^!}2Hf&fxb?pg5qj=QXhhs?F45bezISfjx)Xl;Zp7WYFduO zon6m+2cP$ibq!3t9GUGO92t1MG&nN>{_tyc`|G>)t*x!y?Y$rS zhrhlqzWa4_^ylZ^fB*iSK3V?XfjR*W(R}*N)ZQo*n}pL)bJ@!{>^Z|C{g(29B%F-j z%212DUf>z6L_UMo%Hd2gt2(FQ)~eB5y!%Lz!Q+Qxg$f~iE5nbg$4d!O7=FXHn#l_N zOo^)_ZMD-K1#%=NdgJ0E`-r)njg zF?rUuJ6&T{?>zRb{o{PI`)G;D^C$bu9T&!vyf2+mz5Dzw{V&x^PY*Z4E5xt8>AEAk zHBr&i?v+v`yEijz|GeaC!S^p~FOyCL%wBXIf7_X^&%OVs`_Hd~FO8*UJ>GvJz8t>F zz1Gw7py1cSW(jz-^6Tvb)cKC7kgtq5M(NKSHEYrAsn!f?hr}o*wVw-7qY_6Judc-i z=lHOw9M!-^V)*+`W#!&2%d`a9xg&FMV8}urKeuo$MfNBAH1bTnwfkneK94N3jJLKX zz2c7}X(LK_^X0_b2424Icc*|1qqv_(WTQBN604#dpPcw>iX}&Vt`zsBOe4u}$$9UK zD>yJG2|8e!C*;UKt%sOg*y+NCo-OeU{%$M$69}#Pbh6 zKIj*`NmqLR7r&IxAid{`>yVW9lXs*07T<$LWv|nH8VE8`$iMU+?xN*pA+XT#7u} zzJq8weh=n8{-qUh*YxSmwtgy;qMCb?i%5>P_3+1LyI_M)t=Fs*#2Or-Bm0l)nKH$S z?|ns|E48-#E0W_BZ1{E1@f7eeq5f|8`=p-wzaLXpY2DuzydOo1(xTp}{(5)&MD=)F znfc%E$Bvq)@9Ea8=s({aS=GM}ISQ$N?A&Bl6&wE({iCD02f{jW7r9dMbEx{oVg8r9 z*&T01o2u3RSv0e;L|^+)C@8@w@PPSiQoq35(-+7vGIr@)zaXwRUZbJ;0nWJepGw|y z^}vJsXPmAHYg|K|v0qCgq&y)! ziH3etYs|H&6$5ay&mWOjc{p{MtE9UHz7u676$lAY%5e zZ92uqzfNctWpP(6DEY~Xua*6=E~My38o4cs{|$9slIV&=UVg7-x@64C$A%cwmcm$% zWYJZom$CSWo&7@`!0%brYLAJ_1=S6yU-_1!!m8N+lWtwvUZeIZ|&%sJPp3iS zLHXwk`u8_NDjc@(8h>bZTh0*FYn$`Z1{BMC4YXzv{HI&lUqL7#2tWeTP!tH{-ry>M zf^HG(!yezSC=Qu92WO@K^{^qQmzzg#K$`D)3Nhaqx+5j65~t!NRCqk>@HbfgF@p}v zwL2%H$aoq%hyQ4z1hGH)-Z#mo$)1jb`RSoycpVMyw}_dH@%a1Sax`kU869s^c-!l* z!iwIS8fKKo-|*HsUip9!03Zk;6vFp!$0}_4+0g1o4qL{BB=)(^qyfED-v)O($(*^C zw}%F{SP+3bo{VMqAz19>DK5XbVDs~0c4o1&xVPfCxd$PGjRuG@Exx{{CUfuUKTm1o z(e#4aw=(ALZ9lf(Bb%n6EO(Tce9wfvUL#Z92@GYiLkd0FMNLt|7OR zSZ~;%lLrfz{d44j9L4>NhrHwQG?J)@W5JRj?2GdUPK43kg>S`C7DIYpZe4i#aohZc zUBPl_#OvWt*B=Tfo)Q9E%X*9b1L?2s?!CYDLfmgh?T^E*CsG+QRq|6UgeS`vxvASp z@8>cf`Fxo_d`3B7Qti*e7i8%l+K1HJA2;}C?yiO#Ts0wMCkh7nAE-2H{FlKDTD(7} z-nt$a@IC+K^tb){Ti4IWTC{}|L`h&fBycLc~(*IHVhA_>IHiok(6^I6yjp9R!3Y{@KX2? zawE@F=aq-oZJ+CU9?E)m!^QN>bK_QXymYy47<9y*l9WE#-doCzPwa>%&IJ^n+zx&w zA4v1EBUJkY#)g#Ag=y;thU$fj=IZi>BE6e~w(VHX9zfU$&@%^+t2^uh;BC1NAub(P zTNOfaxRA$i4Y^^Zz9ns2&&2z8|Mwg%>%DGBE;-G0y{!2;zAgkgKM5X zsPYc^u9s9J>amPS7c@u~438D)xP7ID+|HF^=1t~RAkq7f0aPY8E>rF#A@g!)rgSG+ zbusfIDvS9bQ@xfPJD8+qkY%7qVq449&(E6v^&sMQ_=>2PjeICEA=_a09_w)G9xC>( zTq4XO5tbgqbQ$TRh)ks9gwQw|qnN39jGG0zf`TchD(A2~7}8H}9ZDr2(>3*yTev)4 z=G}1<3xN{vy84h+^)iQdNJ6?9&-Bw(a+AM_W`2#&(oM)$xsz#loN-D4I9HQZWS_b7 zBwGRp(aJ9<9F8&@PSOz1Y!nAoI}42o4-`8=x#Ns~dWBV7kxU7| za6-{ZoKtyCkcT2m2n9_h6qC)1aT@5Q+F~-bScL`UB#3=}a*orEhRUJQ1qUK>NKj!e z6MDhem+`lmc0;&3gQfFBxb2g{PGEATbfAMIyjub$l&Xj(E4~W(QkxDPDLoNS9vUh= zGANj2FHl!3z#C+7I+PU;=Zg>vJ8Gj^eX}|gqa+ow+R#}~xy#v($^;||W%G-UQAH!- zr7*4vc}4OC!vg92BKqc{)6$;Iv#WGvC7fj_-H6IW3fiC!g-l1A{;teAsI;uB)C@wz z1tPpBkamISav~-<5K)f8l-E?16VXvrctj}r!2wFdwaU&MUC)W)y^Qe}#D-BZBpO|I zAl+@6W`rj)mxj6PhrCWhh2vwaj*!9JxQY`hMw9|D;*HV^Le!8{;rXOv?%KcNnH>1C z&%>om3l-`Pr?V~9S4_@RN`@$9APdqP4TFc#>2!f*Nc~LUsJ0wUyY%hH>8>Mj(NE^h-ee&iLN ze5RUOwgrHWQu=|Xz^t?MFS>ClG2)Fv##%6vk5p+|P-&WowUVeR!&X^Ju-j5Fw`p{w zKtwh0u!@2K{g8GeD7ykDWIg&i0pWy2`zx{DjG(ilR^6*(@dqCrO&}X+C~p_^?LbuB z0a`l|SsRG{7mkd`u0C^;gd|U*>zbj(1?k`Kl&kudHtp38;Y@1mAQ^sb?H?YiI^@+j z)b`^Z>DZPYI+XrSC~X~StSM-Exs(NKE_y7{Mn6$Ypl>|PPwz9#C>)`7?Uc0owEbHQ z3FM7EHfa23P@kIr1X-8&7*+Jquk6w>*q7IM#2qsDERSuc;;B-GI(@@_e)3Jn23DSA zdBu)HbQ!}EAiQ3=Y=B0wV5q#9d}?u-K2MhX$}uW3kRLnj_7O!A8yyO)Ewl1hREA4X!{XN3AH3F5Lpbip1L&SK;~os^;8QH zHCcn5xaKbF6{cnDp%5r8{@)cRz->{wX~y==`o&mSW%b0aNy?uy!x8Y_}l& zqf!rJbKzfcn%eaS#J#646YI1J(*x*x>_({x_H|o+9Xxvt0}23C<%vdp6VooG<9BK9 zvvQ{R(t*>*>PS+RQauU}8BuKb&7Hivm!bKg_uyIiZUJfSdWVbfBNj~h8I5AYg38&1 zV&rAy>~JSkx#}%9d+-Fh3Xi#4r{YaR*a4_hKGtnKsyq;N8;_|UMED0*nM@$l!NL>VLaK`9?1XpAG#pq9r9inICEYQCoZZYWj}u$@PC=E1B#jMpG@M`HZ$leXRdy z(2i=TJFIJHOn&mXt1;1d7|RV&T?W{&wC7#JG^f&Tr?!rJz;o``G{5$hr9S4okvGXS z6UV!c>w1kBvkHn{KW2D+$O94T)q#MJEIiWYmKejSaMnU+cT49~LE}{Rv$57;2B1ku z_8LJ9G#yh5V5-6KT0EvY5Q9m_v`{cLAnTD7wvK|~nM5ZgVyF}2p^BJtm+`;%#xbf` zIOAl}Z>~`82R{;$J(L^%@xHEm)>J5!%;Yj9+&v}LJ>2tpip~XKqBixN7*A~|O`|4i zm5S3ZtxlJhiNEm+WeibxOXx-&43U+H|iW&}j5_eW}A;@B%}H zaQ@W!#-f$FsWU~L>Wj4>>e_4lXP&CC0w6Pk3o>M4j_&fQA~+v9_U1a?&xABy4qzfL zVE#)U=e>%pF<*$f!20tZnrgnl*2wZts^q>Z=DzAA7?7KFA(skZ64Nn?_$R7&^YWj! zNvcl&S2)E;SduMXQtw_8UY+I^pRF4k>Q9R-Hb4Hr>Sdy7R)aWO2`mM7zz$DI5Cb zW|OJQjivm2uO%8w*(OGNi>dv@7mNPWKBF_|@{16JQZvlX0WIhigLBjZf&{bsE+S~8N?{ruuSt2pH_{rLX{;2)=AIX@Cz_rrA z$+NxV|L#rjrcY2VOae_4b(D#6!G#=tG~yyAcC4nx1$w9iuu=;jjg9tp5BHAE{8ztG zm$D?u-?K5cG`QNt6hc2TwVUL__$*g zz^+X(0$ZIWn?H8mpXdk>rZs?* zuw2?S23h$?OI}jF`)N#VnhAtdcmc%Ev(hOLW^l{ec^O98ukIeTEcv+hW^?S~-fO1U z*oiUPcOV*L3A2EPQQjkhP`~)!RhvVJy%>%+<0(t)bq83E`-faTSX|E`PJjh_ZM|V_ z4Q6qOPRDRPTEkA(h?Y(qIjvWjV-Sp-RVN_5G+|4*cz-|@a=H1N%DZpMW-!_2ZyW$r z+YH8vg`6|}E{KI_kAF8D|E3`Ts~9ZBn(t%r9d-|Xum=86YWkur0Fyn9jwZk!!=Vhu z(}L7*>W{wkH*GF96|w+7FN19;^B=Zp5XAt?<$_zudJQfZ&y2dvGVa6FU0l1an7aVx=TOv z9vo-%e0$clF&4Q5J_4SXeCcW0D=*nQ2QmH_fPi1Zvbi9-HL(!d`%jAgoE$c$tfHN}I&uB!UGMAs`y|vYdI@ccqp=a=M z3cUOgK(j{V@Quup>@p+xY5nZ4vZNdzZjI=`Z-4dB_7NKdf>@$RXf}pGMqlXz-7F%D z7Hxj%$MuC4&mKAexOPHPyAEw@VqEJ@MIS-G(@}j*AoIA3ji)_($00kDRc6)Bs}Ubh z{}liEH~)Tr++TkwN~C})$6w!7<#Gu)+#{n{SZk`Uz5DMI0x2x}4?W`-g*JDflCfJE zIytYHjInH1!Xf4thd0VUwGQ*NnM>3E^do3xnDZFmlTqgkrLe)}WoLONYF@U|ZPuI< z&FJ_lE>{OlauMYF7+)cMhNVm_Ixn^lQ&}ea`w}gI$9i|){9aT;LOJYXfKqZ zpcPcIbNBo)yX!STh(Z$)Wi|{B0T>&XsDWy-lVk^VjQiR^BBd7z@4c2$&LC8&tCldN6yGUfaOm6{YNdN4RS;}|BC5%+$ovSU?OyrONFXk= zQctv>&Yf>&2cUkEt{qEfLIcP~hS;C}Z90)0HBOf>Z(d9k`27i3xZlVtkhDctr_>#i zGJCUgnzKj^nHWQ@{l|oqc!#SSdc0W_a^s3O-8T>XW1hgFuGsj%327q^#>p#WNRf&Z zzEsV6&1uH&M&59r@=st;Osvwl?`Ej5D>m7+26mKF17Gw_+f_d%r9J_=`VbJ zhA|gen6#21!nfjS*HAL38BWJ1*fUk7;D*9f<>*1tB29s%cMM7i3QtX7f_Xsn+Hzfz zOqk+ygGOgV%!SVw#{9Q4%`s@-YwGE&jo@qza%=F)ONm~1%wuvRM>R9054lK?J5j)D zdIxw1alG^JlwUBuASE_D8~LUcWP4T@&}z@wvUEj`xIHG-xwO8*htm{P(-VtNvU$*~ z8KrO_9{exKiD6+tM$LdOxF~^S{g$J1v20yvB+7p8^}sXQt*yZBsH>ptOHq}72@z7) zN7h$-IH=}n5lna*#QJ-lN~Z`NwpP3eki}ein2q<$I>K(75m5M{@yr5B0yUeMbI=na>T`JE^snIzB^AXtl?= zmH(Qw%A^=Z6`(S8p%Y9Y;0)Z42EJzpP>&|H1t^*ARxX0^({N_B-(NE!r}bZ@fKgt| zH8#GK=<`mEilg3AttJ4Y&W&49rat~VS8hI$xw(?OY~ZDDa_>Hy7bnoW{`2K{Wx6o* zfX52P4zMboaY;3}PS0bak-ljmXfVi|aQ;p(Hp9M>Vpg~Kg%;if-ft6UE5q^_-Xc}X ziNFVC-Xe&<^>U?$-B}{&dGHfvwVflSzYX7tE!L=gFH61C1M6iN{Kd*MlokQQgK+$y zbhHu({qHMZ*Tvz-5^I8~C=~+()9U6%Sp)rR3=Bx(dr+!XN2uWM>X)3Y2Bpp_=l_=K z^k6TiH&ypVjolfWf#yO~lPj5Rrx#Wr1Ci`HFVEdi6My})$m)RZ1Yj7du8Ek4!ov08 zsz|Q{093 zTC%F8L=xca6RfvMThyLSF-?()2{FUU+aA%2om=u)b>Ftz0il~;Y`Ju+BH5u_fxa9) z+T58>eAOjGYY`U#d`arOje#GZ$zsOh1KkTjDveE_&+`lsEgq$qOco`rAu?Gt*3ON_ z_+8vR8o{Ab(j}$2(fYfSP7Vg{xm;zif>;ODscY(rp=#_iy0qMAjoA1ha5imSEBPXjG4i%)ra z@*~+@{62rK4!>RH%!^BQBq`7oWNf1F;x%z4R|5rZ>J8`59G_0K)l zFqo5|f(+kD92brt=equaQXcCV<&IC@UKJ2Yr*+Kjk~eU6Dj;MG72wGu@fl(flEOLL z$~~#FH?<^_imu3(L2Rwe_spEqatXO~yS6E*c02DAE(WZ&-TUIF!gowx_u@~Dy;;g( zJ~gX+^Pe1?-F#rA%M<*^cj-0dqSKtcOV#X?OFV}7wc?TVPVZT++4Ss=Cw(ju1rRF+ zEE0m@EAEnlT?rq)PAaa9)KsfORf{Ys=bkqx=Lx)oq%sEN-*cX3J>YYkiNIF3I(}wd zKzD5m-IN6*UOZU&0v7MR&87>1M&`Yg2|Cd_(A{^js6JZrn8zj>%PtNx?-*UPRp2dm zoZiOJw8Y=g;t4h6kWym5z)xDdq3kL0T2V`)X#!cI_X>c~U}`(}0OTjZ*4^rpTL-jw zIl$hWUy3@h|EZ`9hE1p_EB@V{J5rSvERnn@Zr2IyL@>xwAudTz4mU2E^s!voxR|BH zWwHS>94R64XmnO`jDbMg=oP(i7AzI0<88Ol;zJW_EAhS3;n*k5c%~$Ax1=XNA$6GO zE1&%mSp`XWi*zNIT30qXMNF^$vIgcTHzA&a)PTp~fg6&-|M_K9`#!2p;<)IK85?3~ zuY;O=)fmty#uC&0jg%MZD^9YW)jt>!`TA9*da7kYfw#3Wp5 z(@J`~FbX%sq}`i_@JWH^si~mI1RUbRPBb$BH=~`BH{lns2yRsL;)H_W8u@n!69Na^ zQ;1ev#+oR^NCE?v$pZ^i^4wAM(^^oP?J<-9z-yQQ7Fu9I3auk);G2q@V=bFIp(X0d zFR~E4H@bGS_Ym4wJtYjSC72p@ha5g*!M8WVRDU@T`juHmu?6tL!B@&fdCw&ISC|>& zo9Tx$SkbSx$_9PhH6}waXa1^mp3^L2;gJnjs#8{y`O8n{tDyPt(OF>WYay)o(+}2Q zB&bAG0+9@QC%Gd-J}-w3%Tb->DxNa>1CzqulcjRnaFb`i6H9_mCNSQi>>;euow?MwFls|4nqQo0we4DFq(t| z2%l&^9Gnmzvp7T+qs8eGqmV=r2SsNd4=4vp(V2q+N2e}&2*KK@T^kfMN2uhs}@{*lYlhe_U3(!2Y zjxSJ6L>IS9VLaIvt6dXS=~v%>x9d4x-|g0=UmGg2Vz5}*S2n$`l7{u8gogtEYMk5> zeoi7L&9WCr6JR4V`Y!bZ)IpbGR?T>nq#og%>M#R1Y3!0I@oS_l$5=HP0`cR zEUJ|Bhd)FEtQ|P^3gDbm)yP`Hy&2;J_U`dQ1L#f*`ZN^!CGtC4_Bt(2hZ?TP29nFK)7W1WJ%j7(I0xL8EyecJ9njAlL8Tqsqg92){fNOtPQ>j>7ph8&LZ8P4k}-# z0G~~jqg!xi0@lhUDY6Tk_wT~u9%U8Le5LC14`>WS01xUpPTTS%cLrd})3>YVvkHV~ zIcfN>5X4L@bI$6r)WhH!*s`-Eo1nypJC@?xmS6WQ|8Di-j9+qCRLxeBFveZm{d!hO zmP|**TJ1wo;6)$tyvl-?3SZl~UvIxK7j6%hB-!@=e20)mn0xlrS)vTsPMvZX9wtK5+)CeM;8IGm@ zo5y^8gbBF=$#wxS0F?2_K&3f~1C=1AKiXUgh3H-cW(qDlh1qGMy6FbisR zCpFWXBr8ud|25A1r?RsJidE)t(UWjPGdjhymG)gEN{!6#=~;GE(k;v_ZSnMfD(O`- z+6a^Dnf7)A3H0Gsg+2z9lqGtUMj>wLL)VzM2gU!81)cl`ojhe7^X6QPX*L7tYyXs# z$#^9QGBu--Yd}>Xh-aZnorSpP6SHNPGdzct;GFku6i*Suij*k1<|s83Lb(GbsSy3A z8D_Eu70{P5q#-M;)gG_R?bCFS*(v)zjF+{*)5?}t!l_RbM2G+kD8pmQ~M2o}Ik zz(<~qp2|jWeNaLfL=#t?Wfx8?t!*bF0YqpTngBF%XMY`94OFodo;B8q9IhVIr&qRS zCBUpYreMPvZoY85~b0Oc9V6?zBRqe2obMJ zusWM|fqcwf&1r?AW31z|o!I0`sb zjA8LtZ&u07AGw{-2^PnwJ_h1J%fI!oyiGb3Ap6+8i96@CkMsy!m{YilB#_>$U07q} zRR1jK;%gp{u%e%{YRz7&=6}>Y4HoNIGU+?2uFHLmSVn*LX!)UX5Vp~g)uA58sAn^H8g(5SszA@L*X#rQoXf%k zPy93tSo3siF1#0C^)S7^*o`A`J%FRBJV(PWvrx>3SP4oJ06b zCG;ztFUF(m$%UFTHF7hx9LxP$*~VD7M!egDpWZ@eHiS5%M;NnX3pe1|X{d$NBZaLU z3Pipm>V!idyxO)?`W-6o62+xYQV#4vDS&fp17BUhcQKn=IGwuhK{OB!F8y=A%mna7Je6v5@6nQZX}WxX zpcsPKAEyj}D8d)rJ1H`OjPy{yqd_M1w@lDLi2~fVbxFeWaL1h)KVcEwwotfZJcGSh zl*F0`>qS@C{b_j-d-+hSzZa(&a3oHG)!(^~#>EM-%{ipuGae=A%31%;D(j3C*QySa z{p=WWr)xiFchv;o5f+HM;UUimVfCSi1=|?MPYGOcxchcStUKvkH$A-=iT|>hSomew z(+_nqK__z`eZv;D`Elc>z0autLeRcIp`AM{T?u~>JJH`~yM~6ZOaq(lJN)_Mkz?eQR7mYGuwBrED4XCgZkK`5wooOLZq$M-l>@3=tyj^gOIH^oes;-igD z%bEb>8y;j7iwQ+QJ-@;Y!=VP@jOsoqMnm8n<@Y?EY($B&{0mcAfaJf5<7g*oijgF- zh%2->8Q}C@1Vo22njOH|u_Uc#n21l0EpqRB^ZDfWdw&LhIQ#+wPwwI#{U`trjOcz2 zxg@v=9LI?+-ZT3vZ}W*cZc+Llz_b8zh5xk53}g7&g}bO)c9^b2Bc1I}Ct*=jqW|K| zXIaI@Z`lS}VNn+z95GSz@4NY{3cKujY27C_NW#ops|Ggk-`#O36w?u)J#?Sa7Swcp%_r8CV&^hW&$$tB zf}?!?_2A^^&6D4GCrE(fPwr=^3!p|94`*RT8G~O0`=T+2>t(^aQ`M|64l(N5!|^IL z4OxEwEDBUp3i(pl8f4BYj*+ly_1_IH6TX*wa2W@V#~Me+VT8$YG3J_Pu>jwvv*5^R zrD3jgz~1*=BQlChP*AWJ7RSt|6u86FcYVHze=w21UIT_?6qb#y*%;!yl?;?@47o@r`YVx${=Sgj*fWwpz&NSAVfGB`=5FSdk6PYZVz~57x^QlYP5H(sp3t z3J&>qT@}96<7?zYuzJNAY*lz=Ac3tD7Q`tq(Jm3d2Wvkfc^JAnbcbwB*iSewM}&I|zpG zho3HTcZ$s_)u~a$^@i}wdi?c)Z3`rl)!}v_GHPS6H=x58??ZI(q-JBPyaYAr_@y4^ zy1RRVT8ufv4tm@+Tj_DAz@4Q()i_cV-U1Q%F@Ppd#SKRKc35R3BBpy*L7{f(3aTdnnVr&(E z`Jp~Ia*^zR0$3IJK;u}baOvJb-J)^>Tqc~9Wf;gO8=rO5kU^o>H4bhq3I9aW6Sf0f ztT<8j;42oG_WCeGkWiMMlyg_$porI}UfCtz`fkfO(TCwA5`X3Gl9k!=c|T|BjnKDt zd>tHI;n4rGckLNp{&KQo?umP3YcEcCiQ;Xe_R-e_r#Lg~}T|fTP zi!ldKi@Wa}S2+KU2v48O56tfaBF0kw1@(+zs{!rnMvjDSl&85fXd~6t^{}ntkJH1b z4C+a3e@f%NW#WILGzcq293dtMrSAaP*EWqY7Dg})8sTAphz0f!jjCp@_EYq^LS6dLo4Q=!dG4F<|5FfC^plL#@4v=bXN{wXOn4Ziu_v=Hb3h4Ch8baqgsq3cg{$0vN&w>N-ty#PLFx`8z$Io=^}%1S<9tUsUvQR$c7Aa3iFCIeW@l zq)OYdN_xorQeV!aR;QZPd%FYDGa1yUQ|~kr_PP%CGsDgexVlN>$(#%tsF5sNXB&g( zmveeU((2AL>4T?HNiu~3|H`>$-Rj+kw;M5ruK%?vO6)U*RBG##;kY^z?$FgU;by!7 znUe?hq^)hD5~NKf%U++YB41PEgpc*`BFR*v z-Px()8y>Zc7)|e$c{6h(q+yf)8_}%W#Kl&2XMOJC!b=sK>@(GpHCT*wIKPR%YyLN# z^z&g)W`7(YpD0K3yp`+NrvJ+DiWv83u$M*7ydEWn$N7{U^u%sg^JyI=24x7fyRL|~ zNjuEGzR3QF-iG1Yrv3X=TRMUQ{2R&j<9cL)Ld%<^i_bHrB^|RS3N^KjZOWdl%~=xQ zMB8l!6OB@qQ{1OlpCH=22<43Wa+M(UA*8(UUDy4Vnzv4E$cToISHC@*M;-^9uhw#B zcEZi~-#?J_{5N1cnL&L>t3sMnG+E-Q)h96_Den1p`>8a9*+Q__;YqCtgY##Y+z_OR zK_lNz-J!eSs!zMo?~&sdZ(hWVz4Lq3{ULC4VJQAA>C0epvkNzrWwXQ;xsYW3bB~1< zs?%E=Z?dRAKh4rt&(L_e`)d80XJbi&(r9gdE=x+sMQ3eD;=2Jq!<%jxg}^GZdr1#{ zJ$FUoAoWp5)3@Spe@%Jf6?43a9tatQ9`0tF%u;@&-?!j0aoH#Go)>OO1ur8kaZs=M z1iRMzV7GgvaZ@mdEwhK z$+m<1t3jp2)OsC`Yk#@|nHMl&z0cgMYe7tTZlLk>%3Y=8DEo2S2-hKI!?lfAgOI0$1T6a_}!%mEZenXMM$F${%E6 zqs!VC=g)*kb@Y88V&r=G9#ReePSqQg`Xb`9Z_j@WtE-`{^R6rnspXCql=rT8l^kql zNwG24e!jMIO#N7($!|#H*6R`+zcY9KZtbqi!pSb&Kv0`-~r2O@BT8jhQw~18rJ@< z*LS)%aHiQG+JIf8^E$iUJ>kwc^Bfz$vFm7S&5`Si*s7$VD_2`RW8Y=lpQ)Vdy|92b zUp`X;3jDWzX0R9?PY`Ui4zKn^AjSj3744xU)7 zPPQiz?D@oJ5-avgn4i^UNda%a2F~+xO zQ5e(sj&$q-_2RTnp|Km1=!x05RNi+1rS;rU(rXHHDlcc))%jx=x&~Tr{1TzlhB4Xj z#jc}Q0N0QGe1;;z#93>vaE>xX=(&yT-^XR>qZ3!ABAV(_g<1iKZl6Np(2+Y+lhc=3wyqK(Mm~GLh=X6AhVP)gO^9E10d-`S?%EKL7Lg3qi2xUz)_Cu6Y zF@nF((hmXjueY5>Xi8I6YVSey0veMs>6rzcM<_d@*~MsSmOk3knAPRV#s-UPd5s0# z8H|W~39Z6I9`$zBvzlC3K|TU?FcjOmwSoZe`U~+uVxK zCib;jtYC{`8GdfdJe5X&K(II?($jJ-XwZw3M!z*{uUP^zKPkQb*)_+a3f1tIKcRrV zEZ^uWFK?`;O*hDpe0mrCKZ?%8AF8(v_}^`v;uuIrry0=W}1z{k@noUjpS4ygEMs6Iv9h zpKASDRM8u=CYy>D?~dSodb-$(QTrLlCIQ7m`pWBqEF`bDBj^=tR-y6GK&-7>xZ+WB zR9vL|ZRZ|RQ>o}P|C!vN_`!LNDwXV{rLcc&9$6@VGlX*en)9=Q-0LAu)2OMjV7VLs z`g*X_GqvkYY@ zQDNSGqC&|DU+;Q0s-5n0myO%m<@oOneLuqOmwbt5I_vk#uN^^^0S<$dQm1P#v|K}$ zFi5TC^eD^b9Ja+;J<`h?rWB05&klzPS;5utc|iQWM#Y${vv=i(Z4TiG=wJ2qhZg8S zxlcVuKe6)J*=ZUL4+6snKb%-&QNly>gT;Yqg60Cy)Z1Yfk2jRkdc^3baNk;hEfwnk zlqus7*4>+f560SlK+Y(|wMeO}jW_y!3^%%Q&Hw$Uwx#3-^&>3@nuF;m#HV?;cj(kG z`RY*l>;GAUf_pv;t5`E|L-TvrluuUC_YQ}21*lQW1*DnnM#&=X6S1r2* za!uFFzkFU{m7%`KPk-#S)pv{y)fGgQ9KyRs2v)(KZ z`s3=9_eXPIUk}T@8X~Xvxp9-#>he*xG0a>q`1oW20rTnIKQ_J&(5$BoteKfQQROMc z#mOvNEF4MwNA>Rv^Z%(lqr%NQs;pDZW(8k*x2AnOivF;3<#A2qi!}P)ck?9+91NcV z#V91yEcZ;Zn`iR2uO1a2idZ2AZY-=Ubk?XAWpl zKHjX5*$Do;()~$?QlmxvDLenu@iOx3a6nXJI~$GQpxvIK2|aiLKxQpwMmGvO6m*je z0z3lBlnYP-0D4{X4QRgNCLO=TwrF8uXTsbHElKmh@57%E8}F5W-9W9f*V43OKgCo` zMvkJCd@Wa+oq_XOx<7-zBFkcZEpKL+d|S3%94-Bj(-MQd&eyp@(}~wX9bZ9$^_Otp zbtLZS-iT2>%hgcp@tckrEmf15ZfTy@yIrJ)T9^IdaV+nx_v_1acvGW%usDUd0-vcp zozF%!0F{;*M0((;NBrp8$Bqlzi{cf{8T$`Yt{?mUk9zflZrj^B^*_3y1)-FGVfUic ziGtWh0YHSr!TZGe4IWL|xSjX=78V@j@8ROVwrHUchXTw~9M#+;xfyfK7Hc1`1g@3h z7&0LO)9u;K-t`l~u_^C{2lmDvd>7l;QX8zoRkw?lT)(;7LVx+^Y;hia*K?un_c#x$ z0gr36sss?Gmah@R@PBX(mmO9yt#Rd3?S&I;WC0yj$B?R|owiUJ_Hfqrmd9oevp5TaXNqaHrN5t* zHF*}%A4^dTV*j^!0#IaVTVTGT_$5w@`Yb+W@RqB@U8H~=-p9rg*3c967#`cgF?9c2 z1a8Cqa6sJQO~v3HuQyHcL;umT$;jqs8e`+@v$iWyyY7;+lf3qET(=EE1qny`48CNA zd1|)Nq~nuzL62X-Xe9I^`(iY-82Mt3-Z)mvzhUKm@`V4>H0u&}G6FH9kgs zRPbQ9{>q;Y^Q^GF3v|>_Gj;20m*4UY&7YLD8`8(r#bV;nGjzi4Rtr#2MenRTzCGia z$?GD^`Je2NKaC-ZEf|^T*YBz~_Z#{D)O`3)@w}@Gv7v{s`U5dzR8eb8eiMTW;bm$n1!b1{9Qu9NE!uLIweES)3+FcU zfyA?qy8KJ`?hIRXZvu4YLE1}PBjG(A8u+_@y4gg!A%;s_vQ)3@d1jG-Q|qCWaFu=9 zFjIWomN|Nfp}wVY?dy)Uw?~e|iLOY+KB4c(TB!0^aV5~wB160%7$RVu1w+u{>K>Jp zGR15R*>jB?SEgD>^~hClxx^jq&lOeiLm+E%(c149ZVDbLS1~fWTpm(XFVf)DHwAlwr0c#ZK(00RaqS+2g1O`9HZxDg zByK4c5$mW>6MHqNun-)U$AO@G>k1Vcrg|95!qo)ax)O&ELe>LaUC*7NLxZ*)Em%GsZ5 z*!@}*w%b3`C|;;^;J^!|LRAkV-Hhh{N>0r7*GGS+ZeJ&Ow#>rB z=pKCtF{5M-U8WGE^FTdeD7|Ov`sml5GjU51_eed&&k$MLoWAO8g-zF5Z7_Pd)!?OH z@l&HWBxT}LI{4{RlP|6D&&(DLnv0)mHD-v|YN=b^Bt77MhLZF1GzU1gUe~iTJPKyp zzYM+R?g?JVUF=Ans@9PpPUQr9y6maHR>?@qx$5cifin;PAuOmt7@}eo>PyDeTrXMp zkQrtXHIg5oBknq@aS!lRgosZcy1rBG=R_9wo{HAbl>&RhM@Qtd(+pU$Vd>e{D`8gieV_?#i)42V?hT&cXYP#%@#{CqO{ z-KCJnasMQjcs3vBMQt@CkCrIk9MFZ!G@t5xBl!8W1ulQW4YVhv^oYyFeT(joq@u4D zUn5H<%#y0&hTlY21twj%CcEz}t%UPxHB#Or zA*}Yr1&yicYq$r^ji;^|B_EP38MmjLJlOs!Tb@f%Kya$U8QVEI-n3b$Y>5}KsiV_G zm-;nU(LVXq*Gy{Dt$)GDEob1;Jr>PAf zK77NFLmk|!6zyy7wl}oL9NV!{0*M)`+ywt2Op|C4Du@ozu|5xNiGO?nQX`#d_SDx1 zAm_Ornl88Gp8R|6sWWO&T5>}Gw+`hJQwfhG8t91NZTp=*ryJ$V(XW~hawi{48A@>9 z+QM^4A5NDq=!!8i_JlHWo}}p$E)EG#-!3_-m1*R3=&Cw9LXVV>W^@+=D0wq7tBZcwx5aoG|A^ny0wH zAmwqFLN9j|V*GQ}y1?urTIwh1%R#M#_)F+*kGScgH$fls%@Or@ONY{kkAND0<{1~a zOQo9=Pbh+e0fgv-`|mtZzZl~EIfy-Z)txj*EZv4FGjRUe`ai&*E)yRDaP=3ZR zxMprh#Yp6;KRQt*)9pu|SoeFcQ`-elL4bR_cqU_QxVO?eh>CyJ^XbA4sIujzSfAw2 zgT96{tN{q2pMLV3+FlWa6XhJzRlg+S`9&tfS2NGu%N~Dy60Dum%sS|;6>o8Bt6flK4GPW1dDW8c;_k^FfQ~aak zbwUbKdn*H>dEkH&`W?T}VjoQOZ{0%=gK>H5+vERmTw)15Lo4+Z$M!=+<;9C!@fcqx zqQ9I1yd|>h#R}UB>E4I_;5wjwhqdoF- zxAQ$J&Cz`AOyUxA50{CR{<`TZ<#z1z0jnsE2Jv#9x#Vwi;yz}T?jO+1m?hGBXR`3 zb&IJthrVGje2vw?GK_RPd%2w_ZsH_u!a)C{tS|IPfmjEekD6+n$E5-)(ingKokPxr z4QJ37iU4>e7P6{3FN+~r&Jz0vke%+4XadTGRx5Ma(k%=PDI|W7B{hHJrq1x@)DUw; zIbY?dM(N<$cVFRv=!LE24}Q$r6gv*My&`K%pBRL2cYyyFH*jgQNf`Ygy=pD>2Y-f^bI#MknfY@s3 znsmZV06q^W-QGi9qM(-q5}gd0DTWN6Dzz?zln5o%^2JNps+R=F5qj&6;E@49&Is{8 z0hqHu>DKCd?No9kMS@=~bDc$I=OYS$lF8QRXXsK{9CYhW^!&2=6ax5IH$S?MvjVl} zT5k>6ZwZ-ry}IE#A$DCrlJrn$cJ1CR0(uJt9TSReN73PtUp}`8ey8z}@v;BBmgD`< z2W=|s0#(fvF~jS#0|g}0p*UEx7}=|)FA z!@BdWs;=s?wAQ%vwFl2Y>1^ei8l=6bVjB}Z;%ab@j<)thx3IBV3<(4S?ZLjTw*YzY z5mvxNmjTc}c_LS}kgyU&DHEL|fILvQFc-jTLLf`RD(gmLe)jp{;yR;q8a~C62Sg6H z#GgpTCBeM5&NA^o=_o%QB7=?EcKn7NXCg_f0i?iPR6YXpUFZh zg59g}&Xy0HE&a?aC$vO0CUc-h=hTyQOK^=TML2$qg28<@cBc*YjBf54b-0d@V9P(= zKJ-j)tnm6BJu=S-T?;rHMcP;-TF=7pwFCvKc&fgxNi``+2vp%g%m6?L1symmD#rs5 z0WjtFkbgp4I2~IB*eAMXlE_3xGkfmdIj-g*p&cV_rEk#Y<q~#pKgC8(#6Q^Wrr{;jx~+=3H1~0B zXz^+^y_jwNz6G&uVu@O!W4GA2GT~oZoBH%Eq{tmaDM0)JkLG0uiu#gLb90%O{l@1L zvnm9c2e2+;qKg2?Xv$7;VoOR4F9kMrw~nh7Ei1%$KEf&5+}WA};9b0tCZaEUH3Pq? znO3wNkED30*Y10>4kQZpSsWoPcKz{P%K7WjOk}43DPZE` zzWOyqytS=Hk4N`xhB6t}OWbQ?kpTR2NL_0~L-s)mCGB21%%5*2QGeFAg_ zKpqiO#GUszck@7$>Jn|2t}-8Vi*9g>?bN|!-Dirh7Qf-{1>T8r{;qaDZoeLa4HOrw z7`)$CXd9%Mjvp5U{h%CuMF%ReL+MPAH9#VGKQT)HO#p^)?tN_)w3P~yTCN?h;lHWr z6%5}5oxf}TobFVS#JV(zbxdTvl`wx;G#RvPYyyIV8;!2- z&HvZBray27)yXUDkJE@=)H(&X`Z}OQS^bj3q+h0^gnCwHTomXYsf&GR`!t<~54J`3 z?HVvdCliIGJadV#!V@T@_{SIR0)Oc*VGq0uD}_oq08B4HKB3{TCM0RI0cxp^h18`&{R~Zki_LvQ)daUbyb+4cVu}tCOq$4zEyg@dM(in2nJEW z>r4^3ovpV+ul*Micd6!LGdhj!*Z3*qw0L4gHfYy)=)ghN6W27u-?(-kd|7GQM}gOC z%7r(g;SY5NVWJmqvvGGzg#nVtpJxgG05=@=8j&X!XNw;9SxO$SiRg9az0S{%Kor~OE$@B#X=O;B4K5W{K+WM^Moc8O;LNKAY% z#}&;MNvMVs6u~3Heft!#;i349H+|h~RVxSDzzyi+^OQ=vHG?9wI8yr60hY~$Gam&y zUnva|cS)})*m2RuF1ER%8|~wBSk)#LKvJ*0;P+F=maZhMQ?7~~=Rf}Om-$F#)HF)| zW0)5p(OaNi`K<4x80|p*-(h9scGixJ$g@7jhG+ag;}qL^0mtW+_qB{s3RKZ0Xu_fA zxc4m=0rH)K%N4xKA6{=w@hT%0!xALAwc60DEM(knn{1)rk2eB$+~koM=#+q7!9Xt`OD6)$w05Qds1O)_`b1EO9)`Da4f2ye$=L~yS4ouIt$ zwO!FI=5j(|3?D^IWIGI(;*!*mnu+06w|MXz+b`hyvCO()@9Ix1jm7+Z$l;Pq@% zupYl}J+5fvtaNXG-dd_`W0=n@nVE$?6tg)d%*~vKRRS{pl%6dYAeMMK{{b*}na|yh zpOyUuPms-g@`BdRvq+!WQOhpBk;H7WAHHCZrhbY{xvjq2@7O6KvPwoigMty#@mV~m z%=ATcU-&@g-*;c{4YZ%T3Ha}J!lYpe0rcy=bQ0lgIsc&^U$a~6yvw}`F5<3Wk^YyK z#Df<|%g?hHzT(6JIbvQc{SzNucJfK+RI#`_G&jyT)=JC+Ncyy4;4w>D$1S`Zw$b!E zer36v^(g-F%64dTsB(c_!>CwunUj+{?_N8Jx_;OQt6g;~x$| zMQPdrv?y+Dmt+%=hFuj-{owJy2V&lKZ#h;iZ*DH(20#5O>OHLcX?q409e~R#kT*_; zf7~#1S^i^Fy`&A)RRGGDr5Plr6NAz1{f{ zi__bn`j15_=lXS=N)tZT)aQ&!8%2_5eAY9uH4H8R8E9ogAUk-I+PFV^#nScrM?7Lof{Rqvzq$RRj)aqu^z=2Kl`#H zA^&K}Kd$GG86}V&-VO?Lmp@%$dKzcmx&CqD#-WQ3`i_xO-R~aW#_jD+aIf*GRyd8C zQ2YMLrrN~9k+kkL_xa7k^A->P{T<(#!yt~^m1LGo+Z-fRTN5Duv6)0)HC+OOXatqq z7D4`|yD@PcJz3HLgdi{`{^Kgyj*9$H>~NA)Lv(ULjDch`_{zm{#@xek(c#uFb%Tw-7Yk1IN<%-ZJWvl<}M zk=8q|%Dn+gR(6t!B~tl*39}#Ne%|aaBA5&{WLul+-nJHb_?FjCyo5UY#qNu^vRYrA zZhbG-P4aQf0TFntRlk+9x(!(~iapb-BB<3_GcP zL1QaNicT4*hxeR<3T?Hl(7)2&hAl?S2sA2CDGf53sN2^n_43|w@h-dh0dF6+L7G%EKMNDdK~Vg3_`%^5t|?ndT3$IZnOc$5 zN;wiQ+hP7b&=13Au9#R}l@H<=zUc$WZUCgT8lE8? z+8N}n)$IP{YBaMw_x#l#T}^xpy;QVdu0&Bc?_5~24&QNGWWx^9##h3eaSV7gz6!Hk zibVjYLB!GI`)P@@y6Z#?IgN`qFzhjWe*|-xI04y)vXI6^(Xdu7L@A+XmkX2oXoUSR zB4t3`*wYX2b*=b{Ru4J>P~acUH-rvn;4Qtl?rEct$@a5cVDE4)jO+9~cJ<)?_Fma0 zItU|72X6QPQF6lGP)B{U2A=C|-iNA(19K!f(tv9Fo_(Jt22TGwS#}_!2&yCuMH+r~ z5Z?;q?~DWH;D;xa+608tAFI)lg;R$~M$LfR!2v$WVA4}crn<>2+<7WpzT66GWk_}O zC*%>)nULzOOr-V3e|z*bXy-wEsH-T~VR8F_+W9Y3CsvOo`Q56SC6k4CrC@1upPRA3 z0U{G;EhXYXS=!FGik+7%$?N|G7*YcuQ*FF2$0dP4;_>@c<zXW^-fe3I zNEb*e-CWW^(8;c_7*WEMmdgpY)JRn3P0hS6aj`r?hJtemg45O~n*`tM961Dgx` zI=C`9d{Vs|R3nX&iz^Kz-5wn%tN>^6abGnRQkwm{gjc<|2QNDzzE7^TW`6l#tr{t5 z&3>c7=+1~jCn9yq6CC`?h?dw!P0MQWPcTb^YiJihpWKYV<{3c(b7&q^L&U;V+3PS@ z;*qg3+Q*gh?W>xf>RZn&T=UT4&v!D}>dq4cA~p1b&Gu(CufE{D+qwpr03n(+K6>%t zyvgqU`U2(Zj{-nwA31Ub;etMg2kjL-rui>&=H%NXv;4I8S!V zDny0Zb28OzthQVTRh=h4vgUzW-D0v$o0uSI8KBTXFs1qVP)5}dkYY{lF$K(lotA{M zCXDRUU!^g?oxJ+I(MVi{55=$l@jihdOFM{jIc)GT6Qh<5v_ksPI1r)S!a9J}Ip2tsSWdMk0( z>5jM?2)zs-gwPrB#ICOsH~#In5^_XG_&_-X4!uF{CG0=OOblM{J}jv^uGt2SXR)3} z3Zfsi@dph5XsT{jMp-dc_2snDFvY@Rc#c_(syZ+?yg$9Q=r9e}K_!Hikzz&=P!_IBDS zY8rGQY2v$>@v4N_pu~??x-mSs2ODqU7CjaWzNxam;fJa-^i*08^isbf_P9!Lyh3)RsJgJ0d59g$=?T6iYNYsT*N7_l5Gi=*(O&9p?%w*ujYoEA zFj$$2Xyl?q+n~h4tIYlZ8=uJtV_1@cZlKoC(PfF)Br0igkZ8q&{VX!6VjK?Oo;t=0 zCmaYt964^yhQx6rvdf^py^<$~*zH-#>{$C^+kg}9UI9V{iM#M{O86LTJJ!+r_|vhY zHg3^JGb1u!w7K{w5XbfPg~LEEgT_pSv4!xq{St*Gl99ZW5h6g84q8n@?xz8txdZH$ z&i%I}lSTv8x}h!`f?_DUmxjO|_Oe<8WIYr7f)rrIIPYcS8(srt-j$>*?)vMaaaBQN zNkIf~Ni{4~CzWivY-L-TF5yFnANrwSQ>Mgm(@b750gC$ zDQ_3c4N}cEW(^vhx!hB9FC^h0!}K~Vumm@{U5!5)h{RbEi4Sgbf$p*~4wCA^T%c8a%%wFmgixZc%mlZ3udy z6d*dp%JqRb0z#q{vJMrf`UuYdZMal^P;o?6B;r*B3v7DJJJ6R8{j&(iiNWk1p9yp~ zxTY6+s6TY>UU0h#v~4i{7(GG`M!a#At9q%(ZvnK!+Zq@wDs1=ADlGi-tKjAy;%<9( zZDzEYBn7(&X)lfTdo0;N_FkWpNDPrk>w$=TfOybtT>0LuiyTK%Sl6SIiN_rkZad_L z90+T(*HzRWvo(BZ#0*x5NSuWkd(wS-pmH7(+#MDSVcRnQC^U`z_u|CIwi^EQQ6Ez8p-2;sCwcd4n4%VuY^oj`CM{!@trI=+FU(1B3D3f#m zo{v)!E2&W0teMZ~{(tG-u8i^>5nb$}O46*p3!vnTh-syJ#0RV6u2fx0^v9oEt+tU`4+g%8He z`Me#|lnmFbryx^AcjA?UuA+P!d8q63e2(XK*`~;mMVrc&q|76&Dmul%3%!>_7*78{ zSy-S^SUXj^MLJ+U7tWQ)zZx=>X(8tmYs?Nz=Gz4BuXuAJZryTM>jKoCrkz-%KHFx0 zMrrS5pAfS|?}O8kHZeA7{PF`5u*zB3=~c5o83$5MXe;Sz+6z^X>V&Lp+a^uqNk+LNUY z!d!9Pz4qQA+#^7x80h&nSc*XNW~clW3aYdK)jy8B&AXE?hMWhYHdf{Cj-Qm-pSd0q zXy%b3LL$4g?ule*{EdSzD3BM3B6`)^sRo~5Cak@6o`#CwL{HrH@Gvlsi_>PtXatIr zZ{OJg!NU@ZwZ}kmNOWfW>YO(A`a@z#Al7u+u!N8}hsY)V?g#oBr-uB~;<6#K#_a zFNgET#khVdMCoh37Zz0pBvP+^B!C>61ioz{h`UdQrK)N*s7rdTt1#W;`x_^(CC)*s z74$c9MW5dbzSB{M?1J1+hcjv0F4#Y$d9lN?9Z8O{5}Sr8`#uKX#8k9-pj}L{SC2zo+(HQA z9#3yWp8?xm);j{fAIB&+0=@j7mmTrvCw-a}dm$|sM}bFB;9imPg{z3OY`h~A*Y1=yD~ z%xO2(5H9>71({0N#?%wqZ3@sg=uihhPt}s{TDU>ri+K^pkU^ebu)RGVH&27 zjTxe09*jbdbwhoHss}l`$5zq9ZYbqhSlj5%Ns#?+sQFe>8Ud5WR`92zi$$NX->q`5^1Ut5SKXWCql*kFom2;(ZM6>lUu>Thr@3hpr|=?phwvH-q+ z_{fn++5M3NWLddF0@AS*%5Xz*?_NAQGA17u_I|>lEW}%ZqjKX%1G5x1MZ>@1!9{NG zC3-zIOi#@KJgs>R{TG*Xs2?^&!91=8*B9VAnPBHtNNWtXlYW@CCUyn_TNLiB0wjRF z4LvA;WC*dHG|cBw$K6!zAP49fFSQdMkRlpxnvd>{!MzZ|t$RSJLTt|}{OKr4O8``Ee!qvT&M@dF8 zDn{Drjx139DlT&rMCL%J_-OfAg)7ytU?Jvl47PY5 z=3NXftw2K7=8YCX*I3x&6zl!H!{PpG^<%teU>B$NeI{U{3no6$u}}O^%|gsQ z8s?c0(?x+K&cU9J?kpMVfe`b8KKG1*pQ2$u^DxidFn_!-&v=-&0@$D#Mm+$M?1r6Q zMI0_fe-)xLX_%)%%%Tu|numGF9MM^UOfWGg`RMT&%zXlkHVg0NqnV?q$8LyB3TBWt zwL-vr5x`z?C5>a4!xUWPEao#E^L!N@P&7N}hI&;4PNQSLv$5xekPabkjlWZdnDc@U zPYF*ugxD8?k-)#WcXUi!Ov(LGbP*HAjY_N4=)KpB`O*Pr$s6!42`T&sH&? zY1sEMIJrC6#{`>QbLg+5X8l6kf1{YM6imk`?j>#Ka7@u(+(V&7VF@9cf?XhBUKU{M zd7@}Yu<{@z=y9J1KND&X&FJ;qlgLaK=C*fSe5-5x9~EBoLFNn-{$2-uCI)_e5#%w7 z7-NCbVxis>GCEAPT@#&GX8fQtH$&V^#- ztaG+wO4PEjvU{D$?llZ8kg{K?R#Z^+FGdhQBj(s(mYnA0JRu%Sm6_Vl}K6A^UzgtrJxXxR>qrJV!)(Rir z-t_M0?mH9GXyuQd*5H01Lx%vDw|CuMw}YsAE1s_ zlE{$tUr;hGe0S?XA!mh zsoQ(|pS7JR*(Yrr6|m}a=#{ffwRXl2(wV`s!>N+NRJ9fZkez$$T=}LhWjt;0Uw2?n; zeft76V@i=K3d;+`8=|DDMN}C;n=@L7F&pAuHy>ppUe4izh&I9!TBOBRx-*h4l{Zh# z87Wz+Hh3A7^E5}mXCU(P2nxApqWL+lh0A7-m2^s0Yf6(RCE-8$i}DkfF+OgB6?wi;S5Xiv7Fxv7nu#eZ7VzqmX^+MCxyON zu861#TfF4)ZcLdR3#+!0?&z-$)`=*&q=Gdc9|iG4Qb%VqEzZt8%dqSIZ9@#dI;L__ zBF;gzh{7$X5f2}Hd{xYRi`I8Os$@_0Nh@QD|xKpbvnUNBf+xl#}+O4 zvia8NU}a?09tM&%pJFRXPHU&4&C5Gr6~P6pkn->m^~^Im-K(IpI@TJ2NTqV0xp194 zL3yO=8it#LdJA~kFZvB;KuSB@Ci$#x_=2gQj%4%gj4XbnLP<;L=!ZuDgO z$7Gqd+&&ZES}WGKdeJoQC*qN?5A7s?OJho5mb<~y3FJ(p>)YFl%g6~kTPx!Y``@i(bJq|L_kx1j~75}SkA7s`$7 z1;FlpWFU+iM!$INDOB6RSGzMs-Yo-ix_)TK-0L1w*#EoJ6k|!O&lys)<(xNO;)pa% z^`V7;Tu+~8#H4kwTt;rTzK}{RRI{e6v*EUtbHuZweLDUJOn1M{ue@6!OHR9&ah8s7 zP2D6otojgZ|n@+l8wLNoPob`v@HN!Q@)h;`{nlEwZ)K%^S<1M0$e`piCOEy5 zmpL=^p0&1WBa!qC7nNEo_h6?*8T#Q2WxZFn-sgbkyF#qd@Du#0*8^rnJ=w9XefS&h zS!U%_d=2IZh2C_|sIwp|q(l*We(1cFXfoE4`58Oq#YYlXR5%8BM0tf;M(*~?(DvH4$EPCt=q)NPy&N9ok@AdeaH}GH^_<=Dh?RFa ze@r-pAS$M#lIAXgcF^Duik7Ss-8p$&tvXRCa3JA8%Cp^H)+%o_A#C+}Au1Vlu8JIj zh+`44#ODh>b$7bNlpx20@?P@1z{cNp7M(xF73WYgj9$~T?Fd{#`|5zk3LRoZ>c#s} zKw?`o#$J*Fv1sFiPLex9Rj7!tS>nq7c0<)Ua>bea7^+-hma$J6u6}A@mzHymOC1&K z^P-oiwuL6d^}xIUaq6hqOqK2)Omd7>^f!7Y1;Ih>=tPMYs@E7*0lS5Arpr;SYY#Vml%h4sK`?WKqecaVgaCDbUVSE3`?TE}q~ zdgC@{q_sZEey@@=O*o16uV^OOKZfrOvbPFzJ2sPl2-N%W+_94tAGD4qWZl0^oxZ1k zOU>mQ(ko}K8{BBEns#u7Qz9*Pjbfh&2PvzaIvKMZb?d~KlIbf>wc_zlIrfnN*YhOm zW2Y&QL>VRG0uvkI$LourNZ}9j8F*qHSbkgpYrYM%irm5B?PL}vUTCdr`5h$MP++*X zY!;imwB3shzz{Vk9F*)FLLzN6L%S{reZSsHLS+P|H9s*FAQ(-Lb|Y%f2;kqkm97Ng z5}gzn2*!q#SnInCy+1(teQK7XW%S{;6e`C4bU!Ek=6@MYGFHBVg0y`KeyMdp45sCz z1fPP8jyWQ=914?8B|zjc8VDzZrFuhQzIpQOIYHZK);kIm`(^fF_X6nFDT3PhoVy6@ z#Wv|Fz;SK0S*!l89zJ?SM1koavHwnxJ%9g*F=Ez=iMVT!L_)ec9>Htkm;Ranp>CIhF%Igl@(9WS8&dKt( zPw$Z~VE{#Rk~3IpP>p1+9{8pz4}=DSRn@o`5a$dE+NKa7?cG(%?6#tUwhK4)D#G55b>u=aLAx%fb06oyLQ5~4fJ$&n;ARrd0~}XvIKIiA?$T4qLtB$ zyZ1D`@3A24@l~{}0^e@sSnU{s4=Pp_AV6=aITK*yBpSrsP5^1JEtDuZH0Iq+$NL4T z0|zoh4umU{b3p_jU>}f!q;Tr{hnu?ZO{ElPl@0iGU!Hk=`Ov&n-8b13P|e)$fs)YO z!1w~zdV3qH&_KYQsv%&;%HxCX-zq#O;lzE|FQwja@lq_eK^o!w38X|f(b@p2kgM=Y zcXW2=c==j$DZ_mvqrwe>bOT)}2BccXpwj6|8%`)7)&c>h5TVEb{?Ox_N8gX4P*4pNOnC}aQi8ii&KOpO_T>yka>wa*kOS6EF5>o?7Q$S`h)Pg5w zrd{PGn%Ws}xW%KlibqG1x#MRx2*0+wf&ZiET>M&Y|3AL-)(*AKYn`^W&Zl)wg&ivE zXptnLb(~ICk`Q)i>!8vJA*@u2qzFmap)#qggt#5n0deEb;dYO2_uX&5zhIxowd?wP zuIqih-p^+8HSfVHK+lJ#C zhA$8KZ3+i3lhM@#bY!@I_rEhFrGQL0J0GTVL+c>#4YT#1{2qAE8Yg(QMK&@7a`9hl zE`jc9gstfTPHy0bHi|bX>*FNixEUd(NI>qF9;LQ%f#8wpGuBcN*BgPB30`)M@zn$T zMMQ(44w)m0a~QK@3d}5gClgoQ|1R`HfL9aI8@>s*v6^F(Pg@3q_K%->i55~E6mo9` zrKlvG0z-99i2DLvia_4Wi02xnHe@Q^S8^>+5Z5mbrGV9^sE+DZLVq-Ah3D^*B240l zX}88VKfdzMX?y#kwRVUH_LcYk4zhg{c88$e$jX2IS3yvC-3yX(^Oif78x8_s{!e0! zw{FUL0x1i=vgr?KNd2D|H*La;x(if)@SklRLI`-+qkr|iYxwnu;Z9If|pRS8~y z1lc*n*v)`i+pgHp1e+rS><;nTB}3z~irT4xL~dyu3+%|+Ob@U%2Y~ZBM6c@gOz2Dq+>>Yvex^(T`F=&tiq%{d^PL_Uk&r1P;s#aJweb z%Pj}w;!x(FABy7U&mZwSu<-4<|#zc;LQmpr?E z=1h$>JC-;{B~Dd=cSvC0Jz!b!)oZ!pT{w7h&GDW7 zrEfV_R4lJtN-D75hoj;&fs5I8i1Op0@<`qr!wQpd@u*g8I$rlC^($-&fXy zW`2*|+w^E{a6lHi+)h&Cp0@gk#1nhS5{vi)QN+{n=K+{cU<82f&!hu(i=31ap)Mqp zxhM)<6sDK_5yKIsFH$9(it0tgF-4VX(~+_ZYd&luDpf~+2$E|`IS5Iv^lXTuO&%7W zMu6;7!M3r)dm3TcH9O^}!~-+Xtd$P&;TKTz8R#yoc$ajJ+#$+YOs>f+N#Mf62%t2L znC%ZwbASW@z^OIxZA#Hzgt%u^%x>(;B4`H)evJvwV8V7GVA>x$oiDb#3){gJ=go*x z`^8%vH#>5iMn8)7X+&|Hx4Q{2{Ktv{Z06}-+B>+eX)bK@Em#`2^tN5wT8_8?3sbg> zH<`fLoYI~B^E(<{l6zVS9n?3DB6guTiC1ZK3bv!?Y@!O5Nx|;WXde~4V@4bnC*I10 zYngkYOmRU?iLDbPlWCU1fpd$*ZW_@Y6_6bg(S9tlD{SBqWc-PuwWtO3ZTSzgVtNg8 zy{Ge{%<&0yrHBVvdLMg+XGjIYxmp`9pqy9h(TB zYOt(|IejHU$0GSbVmy2Y3!XJ2SX(46N~%t*f$w68Z%I)3okw=Y=Tca((1)kJn2hpxY(N=%>S`ILy9~OoyO<;oHlR~zVyALPcQB*1#Hudp$&6okLErPXp zimhuPIapYj`shc3F1b zl6SKIpSzQG#Qjg0M-!}GI(ARM>NNw?8~< zr7+WE-HC3e64H#Q0GFQ0gL!plXHvy+5&`)vEL9_-Qcs4}>ST0mt>TP{E9pS!0LB7vr-8&zN8*X3q|ek*f-v3V`pRI2_pmf8KsHqfxu! za(1YIZvC&~mc$8w%qW{oN9NZYrRdJ%z8y?Bz40slX0warp=bE#kUTS&B5?Z$&{`Zk zg9>WeR~mE(NawuGz-ed4*Bv#VO*o=mN@yw*cE?RXUizG^PTRYP+@%uxth_^xJOtrx zK1wPo%~FbCy(h!cuY3Os4!#ErqMW-w0$-!akFpWa`(d&O=eo*Y= zF4-wfp!(@4rXmOX1Fo zwVPi6GgAx`Dt=l6`IbDv?O*L)J%US3KVLpO$a%Qc*0q0n>*gm}t8ZAq_F-XeJ+Pv9 zQ6>kT7%$q}Bc@YIcjL5g)>2M9G_MCn?|>Fk#2HGc*`i>1{#A zBTi(U>LFYnc}of%U$0MITX^exT*ZsQ&<7J?na3u!^z33+OfN7(;sY|@A3FB?U6h&6 z>f+bDuU_L@FKzp)JL;itXIwzebZbC}$q35WF=?r2i?K&o_g=*h?El_%ZR=SZbNJ5x z&Uc+@P5jbv>yJ$i=GQZyO{>D!b-nO*N)ws=`EK2d+nI+?5s0Ld%rJ6vQ?_<1@i5(e zo132_>6B+JA-JhOZ)!*<&3^k*`Q1E5L9JWx((J_L1Ac$KzIzzF0>69yezm0!Ye6}i z5>jC`dPTEHD~5`pKoC!ApP~OHB)_FW$Ia@TWnlh7<@;@<@CKiQYb*!yFRc6h`{!Tt zkFFeEyYkw`vAB#bMifFJ3#ANM^v@LRFXnBOgOZLss3lKP3j=@NcSzz1bL#_CCr=%}eI zNmCv4fnbBt-|qb#Z5q)qH4*TcW1~ZSp|nY9h`MWo@fk6i4EL-5L^|qqXqi<)eoj8W z&U3b7a&*;);`xcd(Nsg2{vDQLoq@b*`fFjP+ch+lYp$Aq-@0;< z-C&+w^!2{eQ3p0&f61VC$ly@WH<&?K2_Is-y=3;j)yn1NuiCgSK|hSVz2seR>PO$% z5pq<|*L9}bI39cPk*%{Ir?xG7j8USx@~horgTLK3JFKQb4YW0YLj8)zVr_)=WzCQw z9U)uie1Pd$ZnnJ=1GD~KI4e$XQ+*TTi#4<2Kex4dRC=yw=2v?%)iUhVF3n^(v8bfi z*m&#izhTz2-6lG&hq-S<7T4wlEhRsMY6;E2gF8O?OyrduTHT}B%-!1((8z_w7j~g?72N);-~er z2#LT$-F(fhu>$ix1*}Reg+IIHhGCuu=@(0cQ_D#@4G-j_>56^}TRt)gVPI8Bxd9-` z4qv}@d_WuTBg{y>o#*MMjp{XoQ=_N^6~kj;vg%?b36Uy{wzS?1e0t{#-_(InAvg4yc4CL0p%RKx$LWm=guBK0 z-B7oot(9(g@jCuSNIPQHH^Em;kC{vMRRoay%3-^4Y9Hin0eQI5kt*O(uu+vum~ zAVatGAPf$XuyIsAG#0@tCV4j_$L&XdY*s!G`kb~M4O z;S_N!{rlCXUyoW=PLyvM4BkC%?J~V4O{u_7?d!#~e-A3`R}{J+%KlogLt6YODKz=b zGyCxvc@8ja@014|RE=^6>Mypb+JAaKak|)x% zAI{Bo#$qy)UDQcBoBnc19DbLkcj_dCEaO2bt^C@j8`~0ZaxSX#?8@4|gQohgZgMIx z^8Rn*34=Z{#LptlOH(B9|2ippyXK#Z6@RopCfzLI(vF{UJN+(9Q6r`t59Za?R$*P7 z%}kB_{85Q3LW{1Xv(8QT&ny*uhJK|93>J{Gbu;r`(PkZ5ZU5CB8YOllr*R z`WSCVLIUbkD#PqHd;Sf>;2NS}iSAua7_mRH*Bf!sYnMsC%mr+5c5lCM??Me!^wn#J zhWtHfMgKPd+Wy#3;?QiF&*DQMC*I)S_|s3Y#+R%(-b+XDw_Fk?3!3{>C*pjeM38wB9TsoTNv>d872rSU_!CxryUqv_Z>a~`Zq~g7em(vS#l1Xuv7V;L2EDF zRxp0p*~c%=oHZzxmSv%ZCU+(Oj8?D?;zR3;WVg@Wd}4Rfu7LQ*aPeOURvm$?1t33k zL*|y^6yi990<-FjW7_d0@P$XQC&Hx7ST{p)s9F@LDG%f@0yKQXf=!#-fR@Z~(?OsK ztHN}V?^z_YDiWFGh@JL@yx50&RxJ83ZE6K!xGl;A%@}=^+`r)wL8NcU6}NK4h7@2f zLbmX-G4&JBQ_cUy8DENrP5V4KGZ|r6RzJFI`K%X~QLbxV4E6j? z!)poS4h&)!O0y@{t&8C}EjG!Ki9F6*bLVZwXKoar)Q~VvZ zV!rBO9dPoTM=J5{YyVkcbTct`Mi%&rzb}z(G!4l8A%%=RUqSzf*trwrY#}G2Es&9^ z;TKzrC^AfIC4UTi%X`baVYXx^4couogDdna16xN5gN7JhOEeFEp>3rws8YC!Af)s# z0{es>L2|D?aF9}{bu9S~Dgsm>Vvxc=D1?ZRz2wm<`xyZ}<$ma}K$U!A7U)xk4ARit zQW*h5-~bk>u}|S+B=Cq8lDS|lG|aO=PT>dx`xM?oWkD2nt6FKE4nogX zMURH$^CQfLW$6snGyWv^Wzy|eyY%Jv(S_t-^B6)rI?XU1N5|E38UPX++ z;yQAom|Rxjk5>4URd`EMI2!$*$AW1$-gmj4>+5J^9%V}P~W#z?J#Fx^Bf0ASX=AfzuEn9%fnn=v}dIsF7q>Ql-X!)KcA#YuCMGQ4q$ z0Dob?g514};e`{HE(=!S=6r&LJ`kb5ztAm6=o=}F>z4a{5?pyAUxj|+=dEz96nZoW zeEq>zjR}-Wuxl4R00*u|E6AgCDu|2|ZPE?|ueZLk}b< zoDvnji3*Baxa+du^K#f)Um+)(MrmSDBEh{b5#Ih!r~~i2Fwaw8v36#UM`Uitd^WpP ziw2OX7$s0%Gj=ZrJJKYuh~&$Q&B8~Y=g@!$EaZ+9dB|0o9)v2SbMCa0ao)}K<0C${ zWfr5&=koXz4&#qBMo=tcW3qxG1?RN$%wvUK1hA)4YNNH@1%W-$V9%cUfF$9Hf1kpO zr3gr5P&#A=+5EOopuIPQUL76QGdvFtV~$KCLxkR`!Yz@(*9(Nc@gj^je~Nk0w~OJ^ zDD;mP9>LOx(`D{Vu%-0W2AtwbB*QZm?Aic2byw*1L+;x^|FW~nP`wT350sb)%pr_` zMWAt{!fz>i140o%R`_+uUIsUM&O`@PGCcc6Z^d^~2r~16XlHM*Pb2w zKLqjJlZ1!ywZ$6}9uk=K!;xdhA^}`yo60|xO>zCBGJ61M<-;r(X(6!mu598{?>f{B zL8rCUXq};xcanE;ssy$cAj_FjQwY)A1Thx;ispHIx)Hy-P@&huU!`jIh}H2N68d@z z(aSWy#7|xt0I^FrT(_u=Gy1a_6g1eU0YqE|`6$62{j!B+`s`E4!>M5mLE+y@yGT|9 z5*QSzqTw$1MXb<|8iMuz;{Jo>TVwA@{p1x1)?cOtAQWz?LhC`EwQHs?6P!X8R=F@J zLm%8Z;0%L!r<`*}l3*eQxIQQpu~7(}2U7wRK4Vd)Nis+}n5+?a^(*`u1SV1PRXDBM zh)%&We2axliinI>toId?gBV^K;EIE<6M321jRko$39TjSz(u*QmOeVDa9@`Ds)bm| z$7}*^?G2D`qyobNLI-Gr)oGOD_P=9{Qr&rN!DRkQzI8OS|c86icK8sw*70I54 zum1u}3E)5rZqn0s&oZGq;evMkdoN0DD#55g?G#{CIVlJlfiG7W_nYQZIdpr_mHgu) z;5LIe!}FXeF(vT5TfycC#W{!~kVi9>I9X+No+iBaDie~K^p$9Zw-MhDC3G)VI7P0< zI(+tv6nKm=R$M>P1Gjg3AwF4Y3B6>nM+fj(3E$`?-Cv7I{UHd{w)1pd_UIQ7Gl_bJ@FLfn)P&%TlUR$lqFs9>Kb+0;$my)q?6=`)Mn ze$;&@FJJhRN&t90XsmZ~MuQ`z($&%?59vuCi`*ue@ee7qf$g646S)QCP$@XrG3yQW zJzE=U$Q-9_4{J6{-dQB89?>pj0VN1bp>@$(;Ar#&V7|w+4qe3B-8{*Y4y5UMT5u)%SL&QNsGC}AO@9nc7 zAQyvug5+j*Xr4@=^)$$*sN4(v(5I=_tAnQB!S^DAt$Q{IR<+Me3w(`023RQ0UlE|r zbVV~f3BWb2Kobq08pI8pX7-8rDc{N|UG&hU2dl8cw05DNijJ3xygtd&XPALYt>l-y zs#b;XG|1$o+y}wHvlO1GG`B%vj{Ji)aW5aF?_E^dei7S8S=>%7g zxa*WStXg5M2BTg0dR)F|kivi}575w&F9ETyco<~>{y6~6l3DTG^=GJlYpe{o$H+kv zYYOclfuHTo@8rI+(nyaStXE;`tUmEE{?7=a@xzzL&3;KSjUxwanWH#lE}8Za!Oz9^ z8WH%YejZkufH^*77z89&wx=fndxocXh=A4$uxCvt$Qu4rwaiM*H&UgTF42hD(2EB* z{ar#M8UfK|Vx-IRLJ<^_EW?h?j1ESujNBgM&1h+&3R4YVcNtI>voW$TZsf`sc9}Q& z-pV*lhW+$Pj{?2po9hm!-tkqAv?t};Yd-^X}>->lVk4Ag3=V8!O0}jpgq{}O_O|R|{ zi}nCryTB}~7F{OB{;2pN!K~v>t_6v(hMHuWREtI)zd18wLCyt}%gc%=6CVT!{2m)rrY7AM+B|7T`J8N?R<{1tTUW9BV=crAh zaJLu5`$aJ;t7L}RF}MP~Dp}Bqp?a=lzsd7K6zA>he>(>`^Uw7Lm%pVff77FVH$439 z{n@mfeYM|DtSklH|18BXFKh3Ebq2Q^70Mlo3R`yEY1#PilO1;+M+7{*{p)<%E9s>eW0NhOc_c%($Wz?emXe72O3iU>vqrJ*#D=z| zR|LUd>lkJFHsJ+ZN^}g!kxd1)pm&%?^4c76>Kydk2{+%vec3JASb<$gzspX+m9RRy zRq%ju%@0TY(^02Q^piK1VT;$+H(3_>me<>Fkax@yp_l8)k%tBj3ZRt@e!Jv6oyU(K zMmxrqbwjV=@^DU?*R0uF{*^1IDaHF>u=(Mzj8oA|ME(3{53nv=1+#X_%zZmCa%jM4 zit?epHS$pE$&)oVYFfh%!r}iZ{PO14im{Fn!K)Mm-`ax2z%%>z{Q0ampI%Nn{yQ(g^g%*aAO9OjQHI~d`11mB z@a2&!GNln3?JKRRbfBy(J+B-YFWRrTGJIC$Lh0{_zwC4MOMa*8&7O{!s)xB20v)^U z&PU`VvBq`sc|v}9nR$s}a+yo?FBR<;k(-`u+VWYq#G$Ye zXuS&Nd6XBI_vDMwe?JfC(GY)|t5(NGKQIpbjcs@|llEjQm#|6N_<;T{;WwXH?vT)8 zQ+2Ap<-A=3VeWxAAysFZw5F}F?Rg<1x%_m}Zf*Z)#av$RL_oyctCkm#^L8glQ3&h2 zlMC|l&UQry@u(4L?pCR>V8Z3b)6s{nPcn-^V}t0ZwDZQ*nd>*!pWqfpU>wi( z|Jgl>-E`bmwX)vj3T;{s)ZZb7lJwsYUk=p{*nwyHU^HkH^b7a%m&$tDo5`d7YXd2s2x!Z;dn`p1?Gb^a+L z??J-Ky-wCt`UVT|z(PpvhH6m)sl;WWmp&MVY_Aboh$tl|H#2dTz9n%-kI*vqYq?9}>{`J7N7RNrH?7e0wG!sAp+o5Nme(~5GPxPFdt+LVl}oGq2V%!HNbzxq%6|(N@{`~HR{sUMn`pA_w1x=mf*FdW>hgJ*zs&$)sR|& zdo2-J*=;WKTXfK=Q;I!HYLEEwVAhA20;8X-vem3z0b1~x&D_<}Q0)d!>;JlBRYBc+ zJ1$*sjVr`zvF2&tH@;5n62l90>-g#=HbunxXcInJRugXG?f5sNN<)1{Pil_d^9pQG zM}gRv%wBHT1Kwud3=7H0Jd{jJVl?)Y+>T831EXg$cg~M(`_Il1iKgp^4_h46HRYm` z06OVfi)egv>Ck6g>tdBou~KbtVv#{W$jU60JZ#V>p}p>8qV?5TR^ie8wZMOQbRNQZ7fHXu<{jJP-RY6ie($NwB*?& za!u2``CR{qbzGkA-Y$mKgp{s7$U3sO?>v5-SsL`Bwu;_x5j)*D;yqPc9R`u%9&0l^ zQ(vnJ7UT$D8QABAM|DA8i|H1V{J^`!qdSf0=6~Vt2OjdMv^_j;*;X^OY9cQsjUdx0 z;*Qk4@u-T61e+ytM1Ii^=6Cg{Og)bmc}_KB_G(&;pChgX?w|aS-IQWpB|TzD_*#|I zF<^Ale<`rZqX%kW?@N!Tdu|oh3iqUM{}Ryfcf$`7&&P{+t;r}j-B4c>aN_x`WB{1T z5-udjbjROS>m_?2KF$H5o*KSIK3AYUf9S-;E0Befl_+{*Gxj`lnAnPme#whMQW7Sk zZfISvMxTw(MoRhfh5jqgc99Yg0!wy&hxnBHTq^L&ZWNoJ*7?t99j?0Afwve4P_ zOtH?i($njt>jRqrPey9tZO)`kvHC#=@y$0&l5X{Snh+^XC zRT=D%+O#6uqKSv>lW7^8W*a1^ZW+!Wu6LG=7fAJnWM-#;W))O}5%yOB5s^=Wpn#aY zvkfoV_~Kb}I~uHH)|}*Fd6hUcAaTXh%#P3EU-EHh`yL?Q!L$%p-Q$y`1wh-GqgxOe^>)GQvPt&_M*i!krY?0GNeOResOHmb z(=wW6$qLZylnmZaSm4Z#3M57^h^BImfk94@Uu0Xv(ST8vUUh1{yF}Ae z?zJ+RWs}VO22ZD3W{%K?C!6tovXw3u^NX|Y-Lv?mS=Jx{H$*hO3pA@q(i@ZEZoJc9 z^QE?sXnuo@Z%s0l)65dI!#}Iv!g^OSYdXRIJTY4?8&G5zQ=f7T?-Dp1?ct~N{*VP83tLYf$2&50GjR>H8Op->klfdU{<#<33EIGldW#r&(n92!Fg)rQ>nQR z$4Hlm1k9rQrK=Yhcb3=~F`*=!ubYqru6&D1Omb?0+Ko7n=6ZkjYvHX#E9_C67-)J? zW)UvKtG}DI$Y7^X8f0AQP^xU z0bJM+gS!f}KQwDu$6uuPSr+n5CfH7IiDqSNE6!EBJ5tjm%tJBl9mUY?Iv*)y>vgcL z3TcSJQ8T;Q`7!B#${yp&S<8zQ3p*{e_$Wp|vl0-$saLr8l35EA8%7q};%u|o0vngj zL&&9O7x@UQX7_TUc``P{J>3BH%90;t%n; zC2Tt(5i^iv+etGkWCv)ZI5FQuOf+vNn!Y3;v^Zk{dy#U@xI~7okV5f9v%7q}9T8m# zK;!lBXKM_qhz3hMT(L|qUxu4fo1Wt1%Yau)Xz9^6uor1&O+@`X6{=8XdYr0fcLm$f z{6NrbV)qw82-F)>o96S`7i73rcD4;4B5KwznVqjBqCb&z5@crGL^G51TRUZDhh#dL zt|q344P<&uM=Gg?=agRitXmLPBX!;Y-xFO*SM9cq-IL!5hES=xYk*kXES3d`jUk>% zrj@dY#?dY3!dGa4uC&ifWqjA4oPhIu#R*y;)|;vG>&`ajJDNfYlk|gVA^;moeX==#X7OrXA4$)(YBrn$8~y%J z_zS#dIm}2M&s_rA`Ywv6GPaO`r2`V~2qN4>TvYDolv;{f&LMe|N{;cef`U;E)Zid8 z`L5jg?A$uH4#xz4?|ZhxabjfQJ3A`McTS|LZmnNpvqRrHC6+jaUql686P;w#-Wai4 z0z$U|0;nkOTB+bG;dmV(inVmJkw8z@ITvGdgMg8QTMV(a5*^eHE!clGVv-A@yOpGYTy_gjzaaT!M2`trUfngED-eW^F zUWm{AT`rD$n^R|NY%&lifo9FL(Q~W(9p+f1Jv!gUwa;h61qC-a+-M~c(d}7IxBdxA zJt@59NPTR>(x$gzoXi%-@RJ##j1$-7`-grd)$xug!^6&|WHj+qtqGkM8q?}Z8}wi+ zf|IEcX)ZSsy3XwP7$n&ANl?)v`Gnb1*Q6w_USgkm>+xG*xsV+}0;itVhTHcexT%N% zLdqzq?~v3tH`q5>cr~TvobW-u_X0ejHDqqMvNhSYVzI1lB`QBpq(v`V<7N%l0Ie@x zZrl2H(`Tw-{Qq<*)EtN38`rV1AezxTyLcD27zVnt3^tNap3~^GZ3;1->NH3OuK5kP zacf?u{qF>>qCG2tfgNpsKTNt2vNhS+sb%ip;{ux-@~uudoo@Ai{YfWJ@4xhY2vq^& zG{CzI_hxkvo;OMAtTs8n#_vG5N==`g_(RZT_^k7d6k~*hUeK%x|KCj~%__n3&P|<& z7PAbyxs8=J2=BQo#usf^suQ+}UFBrwF*8>x%5_PAlnx@PJ$K-In!jMIt6BfDH#M3a1G8OPMxrTGjq!6=@8OT-Ci{Da1#p%Qp-mZ_cqFnT@3 z5_;;IEeZAGEvv`ahC;IG6KyTPZ#9K)Jx$7AMYbWG%$i7&3vb=|OYd$c0F~S8b(YX& zn|Sd}?W)(3W40FV8wTwVu9u&YcATOk5{i0&7WM-?%VcCG0971jJ zKG~+7-Io78U1N*tNxHJ2}y`hAi*i` zx!{UKCPWxIB`SNG6{Q&2vID&qF>D)gl`QLTdthQ`b+Rl~%Vn*1S(~fTOo|>FkVqR} z^youIj!-5l9d+bh2x6gv_rqeeo>X@}q^>Az1G%imJVeggYCIHHf1WADMfi<3-rObH zKUFXX3B#HOcdQVI^vB5tB+*^7+p|9}BP*f?c+K>ZobkKsP8;Pt{`BJJKCRO5KxwIc z-6dLu#$gVEYIIFF-n#EbZp3jDhnU|r&A}0m2?v2F*kh57J{fv0$dQ!F3@1h>aQEAH zBI@nK>iU@*{uuLN((8hrhkUN}rAO5o4csWbfIL;)9&=#7&9@`w?g?Xq!1K`0*7Op} zudl+e{S`~|TV0ZS-4AN8-<<~qM`LWMaV3&BJctQL!jGBBQ#xL-n1bsdv{&l@cpQgF zPvEhK9X%l)OuZN-U(dNkolT~{F{*B8Gb zTyI0ZBXBZ^SB1N5fqvimIn%J+I=8DX!tJ`oY>XVX{59fC@u$gfH@1mEW#Hq;Z#aX@ zw{y{yU98Dbzj4=r2aT#CSdzWn~|{?Ef1ULmaU z%`qX7qz1s@;}p_;c_mbL12`zY!zS6=e=4OtRIV=kM{a&rB7GS z^>V*(f54Iax0%Ec!sNxE>FoL_XL+&LxZjZG>Wu&GpxsrT^APYzN-#A_KextZEre+L z7q8TNq4&t9;(7BYGcy$LQcOTL5T(SHy50DS&Mr7_bT1z6+0Vwz-m$S9DI@rf(5kj2 z3h@)1`|k3k5^K(w-n}FOd60G|hZ~K%JTv0mT3fw6QDOPI18Uf$qU22H8Z!w)ey`b8 zzibtIE>TCkG#C(1QsA{VL!Nn z#p_?=G>(k(FCl#9iaMdv%tMhc?7OkhcorX;F(bF!%!LOz0IjolD?n|JHq?y6*Wc13 zH-6evW>S=n+`Gtkh|Mdril+se{w^``m4Jd;=P-86RyRwMDESrIrq-+6uwMnOFKG7J z_gqeS%15n6&sz*AlkkfK-7Kt)xd=DBs=c|gu=2e5U23Q+DX(g$nz&^|BKG(%sWPwN zlX>TEF=ZmJdf!h{>ht)JfJ0x!QZgSoEeWMH5k%{vz?SthjDU_j%ts>uZj^b3(v;_T zL_?Gu>VRJGx2?Sk!>%s&%K4pyijcU5ziA_54sl_<(eO*wR4BlMc` z&=#MPvDy6+&qaRKUJBUkTT!T6OYM=JT{gH!%J)ty7iZCHrspi4&AfDPm_+)iD8+3o zabSbT`n2p6Q*F`XU@HqgwOC$W1Vz+e??_*uoBd4={oYO-*qu6W z`lx1IOwLPre3XX~Sb$GO-3w>Wk+WTq|M?~*kj2w-iC$T4axgs-=l)1@}zNvlp@ z4|yRoDC(CR&+y=Mr&+y=IEZOCw={YvPNy_pY34Es59EQ2TCgJ35*r#?1~Nkb8ZztV zhLr@93`*u*v;Mc(dPWAfk-n5ugX&D}vzr^=TZS$kCzS8`B*48;fh>FYnB>TL*kR3x zSCh8d7;9ts442_s!WKrEoj1RQpi?^dE2T_n{l@1?alm8!aspOiUZn|jt)ZSE6Noy~ zV6c827Q**AZ{BcGTvEjbk%LI4gV%QY?x&qz%M#)!oM7KUVs&1u!u&Zk)K^;yOiKD2 z@8kyAHs#kvNPMCf(x26HN`E%{2b4dq5to&Rm8NB;SWZjaH!2dV^AYoAL-BH(ZW`LM zfo^eE0`;AUsfyFKjI)=(o6J96+&R5RFPJ*wS4gbN#Oj%yk6-63Ce+Ozo3p4Zi!_Ym zSJ!Qtp9(sY6xhlyS=-7meCCT7CrO+{XvOLBj&=?gK;uIW{9V2N8 z?oEQO={|rIOOl_rF_ku?+LuWgQKX1N21V94hf*@j8~<@?H>}XAj+S|E`ijK9KUfue zA}jIHJte+Pw6{s#D$1A#Oh17y&xF_@vp46W;m^4DOae+ySqi1zxR-b_P85c?v z^D$w}7Hl!`DTdQqy%sAo{YwSb|2?QnFXEStcEF7LL?Rl&#%ji))Ga)xDnFiK@KN-U zGC--wM5nCiw5ed8%*!b2|H*7Z2qS@aCy!)OdQ84Jgkb5>RRvvwRwZYb>s6|m&Epo! zz9OLh05vaxE5z4Q#H$4S%1io zMDTKB{2RjL7hi{&sDpOp{iL7Ge#c-N{&MkXZgj0Rm8o>+R8bRD_+6~tG(q>%MjSUL z@UcpF&BAln z^j03L<6Z#rKO{kNC75Sg%m9}4dC4AZgL$Juy0IV!So{#ldKs5J%1f0=Fnw4^ksAF1 zi%vGuc`8BYVyOpXP~Wj8c1m~%3-gABJm)J^*3hGRTw4@o13kKrYG`9T%p8U0y@*yw zASDvas1%ZzA@;MQ7f3hIRLE~Nm^v1Q%|v}8?0ZDfqSo?WQuIEukYCl9HiEeVtJg-* zd&WWrOQDx)%Kl8!{Z7_f#r7ZyxiDt}QN#5ajwd`V%zZeL`~7Cl%J=&@GwZTDR60K{ zV0$Gw$9uvrN|3g7$X*uqkDrH7wTE7pW-E>(pY5qKo!aIL-cFc=#Id)pJj36Qwe9ZL zy)HrbQ4wovU^*XA{VZf{4>F47Rwr?*twD|aKh!M$C>2tqLe}>nMFj0;1WGK2pTMH~ zzrzo!kX2t1mw9@BP9d)^A=2_z7gJFqHCpP~&RQM{um!0F=;0mFT~z3Ht;U6-llgW1 zXMuL~-S;n{9uW2%?m-sxWcs(FZ_=SC`{XPd| z(BTCvWan{|7T?#_18rkz{|VGR391cXxt-Ox5Peym;5c`eHgbzBXdHA2_&pe0Kv zMGtB}0n)VB%Nc5I*n8lv=E#rPBftBbV4lFZ9$28H(u)htSLy1LO$4&_T|K zlRkf+{pb5LQ29~TocRym$ooGICxB|U61G1q*uJIaaFTY){KUCjH>t0KY|b;H@r?N% zj_I?whAH?afiW@Dghy-nAcU;2)zD}xBuovtJImH2pO39E`e%-9U1&tdw#HITj32xdhW@LNXF5(uzF|75RgA{Mfi0@=c1d};=-t%2TXHtQN}`UlKt zhH%5Jag*&b9otW{+e~V`29*c?=1b0g#2WJSNcmaBDfrnWXz}hdMoB(THJ#{LUo;)Q zpW1j)ihMDWqv^=`Q;Af*J*VRVF~`EHC@pR(5WE-YroM#CL$=lA!e}|2J=HUNCEYK` z&S}=U$nBe_^j9KP`md1&5h`dO7JV^P-!uX8BEk0KZ`{X$7T53=;zg58TFW{b)U~ZU z71&A*zp`$YE$h|);IY{{&Lo<9p7#4nb4zz30LDz#Qf~EYfxRIrXo_ds6@JqP8(cWc zHjRjj<=SpjLw8DHv}5}2{0nV-XhMxy#tTT%er>4)>c;QR9q=-o_1Ds3n02<**C1}a z(06<6#1&R(yzLeOBy_93Hnq>b2yuvjM4xUis_0%1T8HCrk%#GD%D=oj_VV^DtRC0f zxd;5f0e(&efA+vHg9m*4&vjHDqKmg~rxbpy2CDUf@ujeHJqYs*cqM>9OM+eC!qK(B zm19_h8Rh>dx)*<@zdsJ(+w97)x!(=L$YtbyYn$6#5)u+Mm*kd9sobjFU83faB;+1K zNJXe^?j^ZZl*&Do%B|Av>$l%Ou*V*I?0i1wywCgnd?A<|p^q7`N&;M^yb)0WG2jZp zXb=t_eqv7`3J+QaaiDPux3p95?KEh*Q^hMbgEyn@ z5Cb$H?72hcLMkS@uAn)NQ7DW;<&I~I3)N8dem7|yJe&*mj<4c+$?$@~yw6~<+dbB) zntp0EvLf02*A+n93SR=inzTDubXy{R|4|OQS>PFFdQCeMKTak9b<&GJa}96ZzdO!M6ThD8JUq zC(+yk0NV*SnJ}X}`zb92d9tpDtCg(vsK7`2;;U{CT+(m_`_4Ci{saNJyEmntPzB!F z^AxfvQh&mg=e7<$OhwaM+xsJ>Z2w$3RH5k~Z%Cm+qID(Oh$F!qXn&xhtS)ags#P#Q zdVXV|0Mcc8u&o-szZE<%trsiZ9qC00OkS&pk-Gg)E0nk1;9nd%kFX@h7{;K zxUf1~I6Yp7Lmibqb@83!;mf_1>1n7vSqgZ(d>y)V>4u#;4g51N-#o>zVI2-`@nw+y1VV;B+qJ{p7D}dSyQV*hdH5}8=y6w3LIJI zP!3UuLI-%F+_)_V@D*~cXCqr6kqwaJ!U~5yN8G6pFJlS&IdI{YliO#BIcYGRreKkx z=~#aE=sNT$3YxeMjVy;I0-zMCc*=L7|58;C6Y8u`udG@mB#ks3QLpw86;;owI$ToW zXx?)4EZOCmt2*z|+{b->K5SHy1W1XmM?s?#gzDc)9c|eoRtmQHqd>|6e;R*9#=KFaG_o1M+zn`kCfIk{l8?29Oez~Ll43g@p?WoWl6UWo9Cc4z4SmP zl@@npvg{#o>riO6IEDrZ;wU{zP_?}T>Ao&a9vCuyyq(&NOV~?&<`i#2@QymX z%Q>XD8vOcjK41|E0CE9H%;Nbap+Le+0|#owy+CaNDsqJs@rc|pgdi?hbq>_tchjm& z)urK*Q|qN9;=rXeDGbj$Ene&*3T(Izf|V+$ofb8x2({-_*^JA$1BL~MM_qh1c@7kW z5#>>2ZiiGk6Tz?kNfA9!1?u2un@~eaRX^y1#uG^cLZeyyTN|qnX2o{+4w%3xrFiI6 zyp=rhRR|9dT6hBqWfJl@b^AQ+&GE0SbI@j#u_|`sH8FAvb3~T9rd{5JEcZLiqCffX|ALEYN$(o^7-`r7JJHNCG4#rX>NOoibB_ zE>{qQ0^LDPp8?0?q0XLgmv@L7Jluu?jR!36s-Sx#(Y_w{u67Uk8EKv;LSAw0+Sf&% z;YDXAWFzsSVh0%);)P=I$XOp@7qk*9-cg|8p4Nnj=8FxZkbKH!-#X0M8ChJ3loVo^ z2l%PmBjW_`X@EViT&IVEeEwFXaa!ab6C6HP5Qjl_5g_>}Pe*gnP6C8QLC#L?`DQBm z43i~L<@+(>3v)gKruGQ|Zp9IKYL5&Yt92X`?kz{|p7uu~{e+c{I<_4Ye(ovygnDg( zApRVK2y=!1`HuX6M;-wn{=7iuwcL2(`SZWpo#zx}F94cX?%2mMI5{Dpa5`vk0&$td_^%En)(Mn9{l0j9c+H%__L_G_J~ z_m7+atI&zIU*1moa;7I9H8+X)HWintx}D1DWjB`IKlY|zS>o23?2M*EAvoXf%Zzmb z|7Gyjz01XIXP&nt^sz-H?w{-byDGOGqihtl#dLYi{vvz7<<))B$(2Um#o&dPzlvXu zT)Q9qXrSkGQ&WKFoo|0BGplP#P0{cE{_U9mP;ytj^X0s^uiuTf53f(@G%q)9SRKEl ziO}KOy5xSH>z8%0zVKIEjWo8)ef92N>dwaU^ION>|9AfHuVonAlX?-1B83G3Gg1n=Yk4JI~!PB8tVE9?@Zm-+oGcc7Inj?GA-4fRbpiE64_Yed# z%imEYprI3*Eg+z@k?M7Iu2)Rhom$u89vQI}&ZQwf<^(alp4=d3!9n6t8THGG!6OmN zrM^teKtp1!*QXNm*CJY_&UHmvH*VJ^tSHA_E?yEh>n+bK#xpzaUQO;svlXIdkyX+s znxW8SH0-JlfjESETEjLnPl6ceIshmJb-nt}nG53}fsbw;>=v*C7OVmnu zQ_CnMf=9o83Nx%O;?gG{E<#Qekac0jx3!8&@9#L4M0SrKM+CPdL+gzgk=n9`#mVX7 zx^yoSWzxg)0gdDWwG~;dj?*N$li1w^cCkqr`h)z*Iz_E}r|7LtFM?afG)sMBhe9&9 zpM1HDSii7(*?J55-+cP1nLq2n85#rH0Dv52j)rhWd4^oJ{xNOurH<$%ditXsr9AaA z`|^+oaLpqa@z+vlCJz_lAlZMUGE?2(VWC#D2!~d489iK69O)22DmuLb2O%Tt5Om>V zBM2wO#OjZ1g<~az5b^xtai<$(-!z6ouv36$hReI_%#5&Un-dCz`sC35pxOB1T*7$R}9?H5~6H^?1@-o(m#U5PpC)hwmVA)YOb zZoE5?WAKpkL)_{mG5k`vJU-+&kF+cAh(m84b;7IVx-jJ2P@eoDY-dL;0o!lb^WS!^ErdR_{d?9Jt}YR zQsTNAVrJ)!`L0$g%nYHSNQB$JS8s*go~?#c`<@uC80EMKMu}~v2237}XS;2OC`rt) zM8mc+SZI=8hrhE<8wUM!i7x(P%t@gezUb*uWrw9$kSjXT!GjKzqvdMHF9(+} zP$lC|dy6aB$GSCA96Nw=4I46~>I%!RBzH>=dVg%o%F*5-KG`)vs20m{a3>t-)q<%kr7@H+JJ)W@!2TzuH#FbqY5GAa!JpGeo4r_cmS>{6f*DVI6aU_F!sv2jFjI8AZqRzf%h7PDQhBPy z6??yEC^5`lWosQGQCpaOu3AI622NLP*tndZR)zK(W?P?~2Ww-A0T;?i?}fdpqpL$@ zhl`vfN-&7l3yVrk@uBE-yr{v85S7Q=Dr36mrL${%*|>tAi*0a2=T( z5vo!X0qlkobIv4HWd#Emik{L4o4#JTM7aU0h=j|h>sDm@IL?+(dsx7bkYd>^$>3e< z<#Q2EvZ3Px)=OxmQ@<9JYsP%7UMGmTmPkrm4g*!Kcly*CTNu*QYQm-z`~B9z2Sx3- z5$Y2&NuYqX}VvdTkr{jb|(@Uq_R$c*tJ*6T!7ol5hj?~ zQcm0hxLlZ_`(J7IxX(IWvD*o36Z2dAsDN}m>y_oos6b`? z5K2n&Wu{E3=|@J~Q}NiJ;mv7~%EMVG7mh*pCRT89r0M-)JB!=ZgHI7Me9=>X6LtdK z34-}5gfHM!!Q=;MY&=l>-Cj+LI)boP2LlJ+X5rhpNO=lV*=!wRwoPDJc>IE#iBc)M zy83<&f%A;hs=@3K9DG*k85!OE9M9mP{ks&BL^gp{uf~feM$r9ADPXLUC4xw(+n+H9 zHf$W?nW0mwdIG@ORPAp`KcX19gmaHH=D&o*;W>+gOlxbJh)+gaMn+5j4PYX|)t@h# z*urd&p#>iqBFRp0AU4N-jrsuWWw`{HVS61SFo`c-#s%xiDG5LjI_Yi;OFu$Q#DC02 zlKB!s|9xS|BZ7od$rb%6>L70al`oS?1?$0h6&_r&WC>N{y_?d`Trf$pkSflq)zo(w z<1bFOf%X3q4B?ODS58t?=pIUlpkuiMifNKWyCQ2xXG=KQQwT5%{7;elg8)*YGJbLy zE;yzvoFU6)T$vER#@J=b0EDC|Ec#vP|X4?oKrZYkWz=;{DeY=cKugpCCZ7(;`QB{^M2@HQ~U z5^F{`^CZ*{(%bJoOtX-4aHdGB0>sN6CM@!C4a+C5z#Zah(0l5chE z`i7(LSn|)sfF3XOt7h-%v>!6OJ*b*yH*mzgGZo+33wwOm?s>Meq2P1`ObRC;<(^1;31^d+bUl9O9Ej+m7G2nfrK-FCUv zHL4hoQ|zEQ;}y#4??kRQ+}dFI4mrbm2OWDdwdQs&`;$(dx91=6^xT+cfe80!kP!4*c91>Ifx}27Ky5Cww1f=K>xllnD zsj>0zv(>i9Pk!1ZzvPae?o$qcB;X*=Q|SS?w?~Znv~Q`ye8GAb}r!Y;_DBe z#qhYS!9?C&E0|RW86C+RRD8EE>D+yW*YJj6KYU4bsNe*bu7|lYYK8tv76;waXh3bapmG0d5K)lS|Oq17O=Q@cAy0;c?Vj^xpi&`+L*xu*mhhQQW4}Nl>eDk#il;0L(oDlyvec^o3_m5QgqeE{MZH>bHu62n~sxbmw?b zh+O)aF}CxLS`Hm$h&cd4l075}>u;e*~D7kbl4zg+i zJ4OYcw1C0FZ^T%n$H}oW?%uLFb`<4=M3m&twnlpxt6Ylg5?jE=66vO zVW=)KJy8z2PY&v>&Q>dQ1n@bOUNd5G*}rFc}scpAktYID~@UCbGZhK~15% zpM!WdE@RLbOit7gQ+Rou*H+w$i8!+&GMi@#WXxrc%J1Il`i|Hc4*MjU@UaK($v z3UFctc&zoQuz2jcn)451vuAut$54<0^(4@xpi5q3BIXg$D8SxQU*Uh>h4nXe_YJWd z3$vozO1yD&-;85%bu9gkCX4?V>P3xbj-bPa8p6tD`alJZq-)w$RZ~O|DIOY)VV){V zZ@(|SedALfOZ1F#dh`zTbUE9gkg3=Lj2!L~UxD$GR?@?-@&og}1P z#C}xA7LEGHY`nq%zGCP()-!}%091^xcv7_Q)C*q4? zf0`dhN@DVuveWa%FJlAbU`xG1>UFGhIHtxCbQv3Vp+Yw@ej_{{f#(W2Jcpjx5o})A z(L?n8FWHaQ8E>txMtaT$7X^NhhqaX7@Lt!A)B(%Ll?$uvre_MsQ24E$V5LY7EO?zB zEeDg8m9(XN#u~8fUqNongIcE{r{$n>J3@aa1UxOxJARL1lKOFSdUV}(WD?jVK66u; zu^^IVj;gwrl>XpJS3m?M&Oae+ogT!EJ(&@5oJza*0T!9gaGZN_W>C2r=v$)IT~4x^G%)A9XM;fnlT$)hz?ttnX$7??7#m7^GbGnK4KZ`KkvQsn=5D z9dBz}Uj2rXGWd%fTu#4j02ch~7Nem1irC3R&FA77KyON$Gkv(>PGULa#18vV#~$xf zYy%wf7%n?79`yV*1g52V{1xKF4&=;9*a;kSZ*p2_ImoX0t8>xT<+DI_ic;`2Bd)SC zh@*y0O4Hk-$F=1cc!mc@Kraj<2)XRh0f|Tq`{d)2{WsEsF?6#U#yO}1%I6={k*J6Myn^)9(ptR^sTO9 z*F;aMw%+tTl4C&4jp~u0uCpJ1V2LEwN@wDf@ULO5Z*Ly>_O);D^7kv0o!NXh+-nBE z-kr3`whf-p6xC1u{906`Eh?}+P=6V*MF%5tgAcwPzESWw(7NGEL9)h@=5)utw28>x z(=Bfwcy|Rfr`Fz@vP$SGtln7OpNCEO`c8jqdE}M2TGvv|X=mg}Nb=@|tCc@S-*$R? z6n7sgY))*keSg-7xOt+ah2s(NF!o$*fP(qVXwP2f{jZ%059D;B+OitDlY6sF)F%2~ zSw}w`XvG$;1f#Va^Qs3ws2t0~e!31%-t~W+!TI_7=hnCP`?z2*m_q>k2k|Vwh|ul8 zFhs4UF#x!og&IpbV2FaD;oHY19p{+Y6?^<-3vo7Bcj0{-F7n8aqV z7D_^AbXrk@D3{Ab`*_mQu}Li-n35#yJXo3xAIB0?Oe$fL9!ag$SIHJ$WE~3J#Zl75 zbKXHCw|kx?73|(7EPb@Y#Zfkvs@5;VA?HD4jtKf44N}5#S!I;!RtMQwG?6J8B-q&L zBw=_E*|%rr2X!e_ppI%@hfbzq?7}=$SwMS26)AJ$PjmzE0y#4m1sd>69OLy%?4&J& zCF2S<;WBhPnxo%^f?fV#ip$z~mIBElm#NC4svSeQbWp)6`OJARnjEuHSr}xa0G9P> z`JJxn6R(!NH>M-uvXP6_CR{q)WiI*&^1e)cR(jLl3=l{OOTa zCAxI@-^~1Xm)}q(X8Rnw{EyhSw)^o7t?~jJ+wW7iD%+mv zO|DjcaMM#8`)N8lo%{jRX&#y)vNs`_|L=RZ-g3pIuiHft59Pm4{?f2qDGHKmYAW+t zHf=h8GP*(H#zNG!E|tzEII$&2vIxv`ps68~y2{_rQoC->SH(rH@>As=bQolRM8rpx zj4w#qjDK-wUClqdVxrCY>c~knw)d7hH7~MH4tUl-Yi`9vcJJu8mecFISI&Q$wENkD zLy$TSoR503+X_KZ0bpZKK_hjK{;AeWNB@S%y7>0w<$#di+pa(3psLEQi#qoRN@Xgf zPPOSoF{%v5RnW2w(EYE0aM@&sd*nzRWa-1xopX4Bk=! zY1D8rCMetZ)1;08hX?R?`#tFVBkz|Ap!k_*wvqV21(}}~yU5`4E_KVli#(rVZHO)& ziFvCgetl(n#Iwe?{YC)3OamLDuphrlI90+b@|4HfYLU{X@8}MH^>Tx44_`@HUu*?O zco6}0>T;9yqi5Rgmi|RoZ4X%Vd>+qmib~7)F$LF{*w8B}YeY-7t#iI3c!iu$(8ZSz z8sWPtjpnZ2c^DR!q)uW5UY* z2^72MxRgr+y@2fa$&^rk7C~8jEmM2eyU>bbtrrvtW-YO1MzkWOx65I>S^@Z?{k!|D z|IMXRi!o2WBa1iX`3CYbz14j3&6k?W+W9)<+m>f5FR@C@<((dk4`*jCr9M3$67IJB zvd*qg^2FBb*DnEZYV(gC!wXwos>z$z%4Regk49>6N?B*l2gwbXL$3}rZ@b$;mXGIU zU=UHb(8I5*mC*{hIXNA@%99aH$@C{+9V}6F6xVO~t`$~q`Sncp(_&el+YqB@6dP}UipvF;9Uk^hZ1FjuQ|%VG;(hIK65rtCmh&^w zzEn5wDC68IcO5<7R?fi*zMPE1%!Y49L1-|8k&KtEKY{2wt#V{J74UDf^ZA8ck6So} zrDI>)GsYgKdMUC-+OX{*aXAsX3UDcy-< zN$J)>N9};fIJmvq{f$dUt9uUzWA-j_`n=Kzt|CVSKq4^(N`Cb6o~7MK*Y|B^9ge7i ztka6mg^%lCMSc!loSe%mMb+t#34Bw4J$%sVaR0AC#CVgYNoxI5DC26{-#qc)lX4EP zjWVwv>d`fa8dEwP^NV=jdKp`bn0mceHQL4U^qCRjttSGPo~9$MSr8kROM(qgUG6A zM#agP9Df%htlIVV%T><{;)jmtpVR417v_ottS@LYo0as6j6{OsLi&8v*od?7#gnlc zO)tidi@9xGa1*|!^m6$7+E-rn^YwCE8*A|a*mRmFR*CtovT80-S;y2=M`!xDi%VDG zu3ucx$@0hsN!=>qYDut+bl#_Smwl@N>6U|V{v@s4TF?`ljL)U~weMU+AN^tmXmh?7 z7skV^rsIl^{`j7b^L&4S3es&M4cF1P-G}rhj%trAY*`Lz{60ENQDqFNe5ZnNb@Z(o zlC}*=2IX< zF%T^+sD_*1}t>LaDgBVEL?+1!ma}~5LO$CE?a?qrNV=*xMMcJ>N0kf+>{Zeo?88Kz~&@xoG80<%aM_=;X^hE@bUl>&|M zfdA-5bD1%ux2j{{U(ky>|D7{*Qoq z_6qXe#Ft8pI*1>3!90U@A%QY#Y0?Y$4Md|z|2IfGdR3%=fZ9xsk|m71B(v*XTWcmq?WBe%*9W?NMZf=chbb zyWG<{$4x9VMXpvD2G2-0sEK>h&?fol*&vf=^Y$}p{MiZxgu`hwIg;%yKlK->LDTki z%L8dgSHBQM1wB7=elp!5*fjvcohytd=?2oE4g_(j52^kH@d9@-A1-(nC|g3ra=6SP zqMjh{4p)mTpkIvVDUXn3;Cuy~;K)6wTLggnP%k!eby@%r2d;7&UD_QWNS4v{r$eZK z(p|d@Fq1323c_#%2WK90h%4OTtVyOb$_a>as@&oCV(tLM98uE;04oP*%o5>DfD~NY zZkpQQO;*66{l&x|Po@8^zV5#u^q)?zxdlx=t=GhpW<1nu>IqcMX6)VJnN;`UO&AuQ z)j|>it+iM6FDWLl+{}|Fp zRuiN+G>5z(zC-b5Ro1wAtjddm*{?kg&R4t7=ahIBO2xm39S>^wj+AkilJ*@d!Afx8 z$J1`*sDoLh@5c&va5Jr@0)VC|(i*^jcm1q|d>6$xH7nAtv-2iMk31q@UtdjJy z0oZJiMg*wd4sK8aZ27^J9wDt*4GC`p_fV*+-8^CN;!p-(BZByPvo;k;muMu219;jI ze4PlISjXVkU+D&GvAPTPa#B(%1F#Fz^5)-jHy?@Ht?`Zj()S`KOyEKiHYC#}5-N#? zkDoJ*=*8QBOf&4kU-{x>kXa+ce1|TpWH30aJ`f9Z-ATHVXQ6lO{PTmQHCIeM?&|o=L^j5223P2bAdchg zs*BS+Ws2vXVSra;+G5t%yydl)y;TnVJ5$lA@2l{&&NHtl`=#E&C&4R?>7@vVl%?Re zVPdDuke8$iN@;yhZB7m(y%Q?{6iNVuu-q3H=(>S?>28=zH4w2)mDmQT6@X@r(muWB z>Gbi1=4fz1aF|_&(gl}-Z5b_2dazg<|RN*7hie9pF5NH;uCm!gF9U+De4{#3kL936HCr6e3^LQ|>c ziLT{{#?Vx$0#A`EK(J;10_CV+x9>xuV466Xh-z`|M9~y2ctS=l#$5pIR%cYlmXa22 zmLMV1@^Z?kG1^k^<-P2by4;_A?x^kfE8mMClcy!LD%@Fx_lA97ducb4XuGpHyVw!w z0M`bumk0ilO)RhB%|Dqtq>2Hq%>L%p`^{YA=e&LB*~D z;Hzz`DRl860CtrihI*eGRx4dX5<${ZyEjDYr0v2s)Zz4tef)(BJPebd+Q`KW@idOm zq$5b$``{XD1Q7yVdYh_+++=J6H0tQ!Robi9Jf;arfJVwQKL&nwMg`J??ohSJL}+=L z(T{Z1RjvZqP);uL-3!qx@RM`rFDEaj%Xvs@6J7S6zVtH;$W3}~pENjzMJF^hO$29RX1lzdThry;legS zz+hm=bd^S+o;z9aB!|V*3@AM5_14rmf=m)guZ{@qNLTIy$w)O^XygjwPSkFnfjXSU z1`^@!z~vM^c!YqQP7*@`u}b_S>{vz#QScrWLy~j?i3pz`$B)s~N4e^&u$F!TviiG9 z7#9)9$0Z8h0t8__zTPTq4t3X}?QBK#Rif?Nwiw!^-m{{T>S)8!efB|*;}2ErJ{?VE zdYo+?94lz&2srkBWXbg2=#$S9&0Dgm;%2Ype|MAbqiFFz@lU66X4ice{c~zO)$$H= zWaBU27#n?SQnY{v$^=&I{b$sR)U1Swdw0fTdN9v7C79Qb>^<}Hki2N7j&+yqvJy!D z7Hlh}PP}{n!9|3AxAN`M3R&{sZRW8duoSo7=OoaiWa}!@;8Ep8&nn$=I*z>fLkF(A zs;(PyOs|&yDXSH`@@IQB-9So1i$0?44-|I?eXbzsncOs?gMRcJ{YDi3@jZ3hQk9im zbwMv~NiUMYeV*6B`I9CccU2=F^)5LQfi}HrF0EITjm4gsk}0|J6IFuReWtomZT&ZL zlF9Esc)0Ehc{@paTW9|ft9=fFtxa=_qWZnyg*nFc%$bMh+eI8l^_TNH{}rUR&tsZ` zuQXR&q#RQJxAx?@*J*=yAL_=lCF=EoQ|Es=DbK?uerB8x9@z-AV5`DEe7`=>HMk|O zx*lm`^r(zcko~m>W(qajA%?GgPuJ94)SFNr9HA<^lQhbYnPlDkw$b(3^yVky=bwYZ zHK$TD^;{V9t^s!N-R_>Z_tNejt}B}UtuC6=EuXKfF8XWiZUZjnx&6fNm}eu~a{RZ> zD&^E`4o(_GZnuBCK&fqfoKVS#tLIe>+#6Y)QP_8a#KhBCn%PfA`s%i@BH` zl7=``^ZI9cdsWQ4!~gz$F8!8va03L+$GYWViFpnf2`w^QOQ+3&j8-FOc@|`dhdSjb zTAtnBL5Dd@VM&E1^=OEQ5D+9~k-He?etoZfrG%!g#Hf#oBkkeL!Ku1D+wj>pK+JlJ}vkId5#xF7TNZ}QVT&5CO# ztzArYM>W(na$-=mdhXMDmVh_Qy_B!jRqC0r(4Q$~=-*8>ik%fL+GV+PlPzaiS#eC4 zGJNu8pT-&5x73%@By_x!A`&;5507muJg^V5z|jJbaWvj_a)$;=0sseyo2k!oc%^t(P3?w>-!Ap8t`DE=@hTwo zYk64_mkT_6yq2ZJEuVbUQtWBdxq2t;VWy6c4;s>N*$3QTk4gTNX@7sg`)NaqxNl?JQ(L*4Ej%_m8`8pa(tw*zQzRGC)75qPy;C@_nW~qW>+I3V z%ylIoz#50VgPFNa1G-Ph_Z^5e`B&6Cv7FlrR$$c{y;ff>@7^wtJDl~09$D`;L-Y*) zTM^2BJgm2O74w|1s5h!vB&W}vk;;lt7kr^1gBjhdp=R*ozNeQeuLx=6n9Rm&Ak9l~ zm6uimlTB-BC(o*kruJ$Mr2no2XuLYj5}VLNx-O7>cn8|p;O;K8r{z?$f59E;M`KZO|0qd5s0hfa1g2BoLYBf25 z%aUiydzGE$n-dO$P*h5YPQdX5SF} zI<->rmW87xC^(atSr zqUVA0V&_&J^e8kireFbW?n7y=6H>k0{lV=(JSc(W`=o-X<$pr>YS8qQmU)7jNqpY* zi0~h`pRS%Xt=FOd3)jG@@93R?B_EJ|8QN}Q^32Okgdtn($$U>%OYs=Wxf6RxMXUOm zb#?I-Y5?nYH?l%RLFZs-Qhd$jM$uj7Nqh1eI~M8+o_QQh`w;0rplr$;2w$06vYVlU zoH$I;w#5_qR^#}A@8vMe@UZZyIiMM!68juP6eq{Cyk}|PJ#{!-qbEaQ2#AuOqdI${ zGF(0PXk1w*h>#8Pw!XGG% zr@#bpsTzoWXZPc$KvG3q4cZ(Cx{e0va~Gsy@I-8u$=H{I{RJhbo?p99Wqm@Qjtcnw zII4&>i=6c@ z#GEg7(D%-LA`S4Q!NF93G(L6?`g%4;574PVY=#IEr~n&5cwx^xvI+n{kVb+W80*)V zBEe;bf@NMGiW526!tzY;WI-*j*4J@z2bPXUi1d++kxSx$eGbA)p@yl z;1rM~oUjW?;RuQ7QF=y2m4GHBD#)D*5b*~9+Z0zG_k+Z(>uAz;;~+I~_N9PTp*^#+ zAEIU`)@c)8LQ~d?euY#Uz0MZ5o3PrGov}IoDtBA5`C4x0JDIB|em`%rVz8j84|$An zI`f1t|7Bpfb;ZPux}JaU*m#QD6xpbg-QPRMpemtpy_t;o})JZC{0aU zb{(#Y*L zZC7jw;5;^8fL;l+f)??(9}$Pi@pUdI4;$bs5MDCR2v~Yddbg6_ZfDJJ`1krN%GyZn z`)A}z@VRpssD<`a(JL-BbW~!6S33N>ZtD6yKjnes(aSCuFwxaK(?i-F+4L~KET`=A`KEgS9=tx?Gwp$cra}2(>;g_A-qn|nX$Ik`Ed}aoiR9~N= zygCb3_e}`i zM}BguL1l+9yi_ynck1-C?!w2c1=rBA5B)!wNU~2jEq4wO@LO?bD`sbdwd-b1*_@{<(CcBzghv-Ch9^5!jX^3-}b-O zT?%K5=tUQLdU$)szV?hTU3I|hVB=y|df~kjKDVu!dnZp_ZWCKIU@k_R?Kk>Or^yYK z+K8G&REA1<79jCjW#y$7LxbI+=1{u7?kF~`Nln0$>(T+~q>knVq&fvFfdLZA!DImV zzqsTHWap>muYK_bSfaiU&9PlQ$e0u|h7z4Dw*z2L;_M%-MGwWh?=p-tK)WR0zVHcy zIL7^9A8z#>I}MTh1V8eAle1NYp3NRmNZHrWHxHd3pxl5$44YGq?OcG!Uu`5_{iWDq zar5vim56EK&;l8wl=}eq<146*mPfVCrG;V-C~lv_LM);5&uK9Te(c)R0+I} zEN9_e=m2!>6&5~tbYm|$VK6MSuxM_{rp3hDBfM@j);AP?!3iQM#+}WiHQKkgR23DK zTeeUiQ(}H&$nc}_uUY`AgesAO%lZ?oVFQ`_!WIsyWbFQK)Y|uB(V#>RQ81ZTLTLG)?qrg#s+M;pA>&Kq~sLwuq1*RBl`G*A;Rcd>y$(X6nWq5xR%<&#``}Et* z@V|vk8$`^?XCH$rdp>GRHjE-FBSn4-tDb9|!p0Q3+IuWi`&-p{ggd?Y9cq@=@5jcZ zsIObOOSmK9s(h+fh`yTT5Uo4(RrAOF{LL9X@%>NG*x2m$4u%g!f9L+?#WaX)S5x3u z0HjhtKvBU3&<4geH7A8i}(RN*L06x@*nDx?LUB*v;R!@|1w)o1`o^udNIhtT7Vz>_lk3 z#7*6=HMG3AbVOIJJcR-xV}xVUMKFDxBq<;J0!@P@QsB_TCoKR8%vbh7gupWHTNYkD zE+scFoWd0|nHLJ6ierJQGDM)0z>@@Oq)>%DD^6$mbWKwi?>p1Ags=(>%LBEC9hEQi86ZsmX*o`QNEGbVhUDy=o}(mAu{FpnAqKm_J$^PKR~ z9P((vO$TWJf8=5L#x8Iwd&Gnvy1WiNUS+516Bo)S3OlAu;furvxq12*U$vWHcTH88F79gL8>%x|8?gtkx#2SHlSzHJb~y zCs8a{@YE3C!5h3bs+j^xnG-96qskjP)!Ot|o$qXhj%6;e<&5kRnv**(F5MM^6YWlWJ0?Rd`!LD~8IglSNh&kp%GuZE9BBh8qP55HN9L&DVxSWK-`u%B6jAnr&ZTn$@S@`8Z|)Ssz&9KR|!84pg6O~Wb{Sm1ALkCc$2yCV2GbWT6Bm$+87HyFKoMvh zPC&>WRj$}AuV@54g9;S9m(KGDuDRyO!~ROz%|~NsNA$5bhyRPlJk{~2a{cyzM{@?< z@AS`HV&>h0GvZX|omTItp8vzeos4G&zPVj&U;0jm^y0qi)3*-rxR&LE>G~wq`p(&Cd1dEOF{XdIt*HR=x$>D14DX0@ba zKKVJV*mfx5dSv?8LLW=A>#fhk)@|kbfDz>UrJeiUD<);%$Ua{|8!*hqyAjV`9@}eS zV6$)+$g_$|L+Z&YE&Ht8a|10~hw&S~h_Ml@^^%|$V61YKs&=kV!DQ7O3*lRygH^H) zggJNnM+?-inMsLd?5&Y9xE!p@WD#HULD13ZgZ1+T$*AKbJq%10$QH2n^xJBbO2AXS zwJ(e-AWZ-yGxEfw4I4GV{Pg~f-t_-)DymH1^jmonbz;GxKjoRPC9)dt<8AM&>_i6B z-nRmygLUum9L4kw@YwvI>$m)9yZ;O;ae!+%)4`4l$NgypoqDQMwS<1B zj(!)Bwd{<3T}poiHneq-RWzeNRW6|MA#3Wun>r@evP9j~kdBcuCw|-8Ps&D`{?ML! zO>X?rDJna0;;Idpwd)PV#@<@T%p@pV5KE4qBPxlQXB*;_ZEG5SWZKuXNP9R0DW+iN7WN020RBl1>GzK3$Md0uzzo{~2(~V@4&E&0k?g5Cg>#U1Y*8&D{hbuz-c~rFp|8PW19e!=+u3l?{=VuS?56 zZY~`yt^77*F!e11PZ8+<0PI)*w>Kc77(^zNzy_|^NM(xEKY8`R^JopD_8TtVQ0nfN-AXRF;D)%db`MN0(8)aJVJX$>-y8V0X_V4K`p{}#XRcC+q zt_aUm{+ST}_u}^P>yJF05F0&(Oa})@PoS-bVBHphrx7+Ri3ncu5QC{ z(ny}Tu)($U>1n7%j+kM|;(IlCo}^`wZd$#1+IX?L!h$VX<-uh6nH$G{;F|SQRi<&( zI8-8#2mqAm&!*MMB=VmHu0M+ZB}0azcqQH1se|dFj;&?D!C?IKnqcPuNmVT!T^r8MdQ$u@APz+&={%H(2fGIW)rVz(x0wA>F?TJi9j>=P zip&I-p3YpeXPBA!!ua!w^kx|Ssj`J$T()Xm*+F(X45G7u?tp;aVz`xZw`ABNy4zjP zs`{m5e!G>GV4jEm(R{XjRB^{Zet}Rd+gWpxmxDP&3+j-iQJ$2t~~{;f`7)@z(eKV+m^)>m>2nSnlymTgSV;m3tWf zG8n5FZv8PsCAQwlRe8nBD+U0HF?GsCTzj12Jzuuy?7#Kr=%{fn1*N+WH?%j zq&gsqsQuUmH7dypZx6%+T+ObqxyaPVVAAKxGrtc@P?ILEF7%PBLhSfH3*^`-KYqJr zn#6{-)0k<##HKKGDr-`9UyQWSTG}$Ei%iFS%-~6JTWs%UFLk!xU^DQ-<8s6T@SjXX zU67v`!A8gZjC_Cgjm_%BxNQ^OfTQiU$}jDE+v#88yPMzKir?&hXs^Hh?$OI}8 zA$^nEh*`Tu6M#^~ziT<87G?mNOC%66eEnT?RqKA{kW?~jhLFf7b|?0gszfwoWOZkF zc3`^duGhxe;ulSeR!Q>Yox2TQ`(B@lWLxj1JW~+aFOeOT-cikHiY`L6==MmOtECi2 zud%a!uhAZAvP)}l6Jp9vR>lTMm+YWXv8P?CG#r9q!#x@L&cZ~GCHt}}-Q0!TukUg1 zE?+_^#3cDq&z{Qd^QSd>>vN5kJ2kG3{tFUMH?>@-#@s#zj5)#BbceyUyyi^Sj)eov zXJG{i-i9=@saHv8eze*hn+uF;$8N8Bx^VYNDpVQ3dCyN6BiJ=;c4mhErJA|g)w|-B z+-)HfmqMdU7vK8nyns)JcsvyTxE91d0=K`K{jKC9Mn=D_dor5-L(L?=T5otdOV)dh zJ$B1PhpI!A3M0Ail#bZw&fL+4j0Ta)-2d|ns>eOXIdQHM>h8{JCq5a*wB*~I05l8! z6_3hu#Cp6fq03jEP8IoCc`Rp!GcD$UX=Gx>;ZIBk*Q4su85h3P z_OafpV9}j+*pv5F>|zZP{1dh&hUvacmp4G7t1;tUeIE90QO;g|QcYd-o!O|0g62Kj zChh6bcd{kF)B*!Pc|2`!eKhk+?P5XGiF<#~`F7BOT1Ey&Sr!0r{gbE9X$D z>&twGI8c4aKU}K!^ImG_JT)e$vp4m?=y7u@WR)+!A>{jcOJAZrBFU*nfZ4S?MlLi? zzS?Q)fb;%aB}wb)G`D;ZSJPli(3Zq6D==z&K)h$0T*;LR&e3*OHElS zq;N1;Iu>U}1}(R4=sJEtVdA1-@R4$4PT)B7xGX`34<35jfjW1))gfIEn4kU`!i9W8A z%G&X=i0?!%FbFx6C)aU!kBeKPlj!|nHtC_vkt8Q9+>ph8PjvncWtQVqzZgin-=*@qZ& zmm&4D9a!6u`yXMe4@P-DFNV#md8+%u?c4h$q!aJP;E&bvykE6^6z8}$eE;oJ*y9JD zEteb;_1`G1Ty2qAVz+EdoN5VAsdc`vYdKf8arUd$xbw}Zl0=WU+>NcsITjBkaH*F(1a zuN+0CXCwR9oAB$-#j?NZuCM-DLIMBP0HAK$Cw@HNI2wRlOX7nZ2l(_9jy0$dKbie4 zGH#KJ^oaL`w~`vajx_TLi4}*$DRipciyJmM=xDq7dDr*G;@E)bldrvBZ@>P%!g~8~ zP^ItNJ&r%iOe+Utl@>qBU;kM+d-m}4i@#^afgGzg{yegq#`7t*78%crn3O#8so~@B zU@Vw_Is7fZ7ck5JtqA`&nd_&5n+5YP$xC%?y(cL%SMD+ep2Fy)uprqf@QDsKn+ns} zhbiM(60*T8{EDfAihQQfcw>ku0VIG0iZPSL`N@J*AjT3XLLiI12MSLCmA1$V?}6fg zL?wQZFbyb1NYu6jNe-V*Ajm53$pU~Rf&D~uDp@=>Ni>*j;YPOMPZHe+icSGVsX)p0 zMAy{h^F_%XC&9_*f{EOOWI}54xuRrIEGQ`Wj_VeXFnk9_0PsctFYwo`23VT_J{4l( z_A^xXGkkd=s%Y>aGkA;{(l?x;J_UYW4>p?0=-UFTGr(h8VEt^UJ|khZkh~jMbylgfG_yXgm-n1Bx3+e*a-LKsOF$905nz{`@P|R+x?z61|*q&l< zPqnjs2e6f?YUB@t?f28Q;0g-Acpf_`K&H5X!Fm1Pp;>t5+oJfzvkBB@ODUT5vw3z==`dS`0g%1`WwRZMTF4$x(u=nB0ia7%C+w zgc8P}gcehL^(fZ_U<3y23Zw7}66P-l^<}`41z-G)lh-zviaNj7 zK9GcGo%xP<4KNe~8_s}As^m)UTa}bH6qgqB#~91h$SK8rWW9NU1-{2|CW-ThCt7?i zyk0iN{*l1;Edmh{!Me&||FF;Yr5!OM_eNU$!7xSXq_pM*^U#;sjrX z3yy*1Zh+Y%x%)&8V>x)PDoZthg>fYQ_z_pMvyBUY;s`GwV4>%`3vX=0{6-4A+Y5a| zD*YQOqvWXI9ZDjg5L`luN~5@L!$Ptv3CJQ}EED{gdMl0TPH|;8w&-SY(G@Dw-4Iw1 zqbi=pM1;dGVEIX_;CNL8&wVMknNw%A>SJe>u8(F+KO~#Jg|Xt;W^x27Kia(9XF4Bo z%`TgCBlWU5L;YY5MmvS+5)sPVEWJqfS5wUCscd5>XpYe-=8>tgGR9>FXY7u9-ru^s zIXExt6i=1~AV42ePvmHnbcRD7gbQK>ETN{zN>>5c;Z&t}ToFmW{s3O}SD-#Ypeh6j z%V>aI-mZ^PWxjGmiNQ7m9Z^CVlo%?EORzru80NO$P|{OCYGk@CS5Fjxg;G!N3Kma= z-I{7_9Bq`|QM8A0vCBETX>~5wXwu5}rHS&XgUCNcib~r~|p?QKvZM z#7YF9)WvzJZC>JSXwUQQ|NwxL+Qp;!3*)|X9jXFe z(27er*{Dd7=?E4Ep5=N3jEYwa=598Nk{tGsT0~3zKBs}~_KPM<7%`q`g|A+_T zM&zXe*5?P_XQk|$qmI)o5Xpcko#9EBiv3U~wyQbr1s1u3K>YREo}5>LIX z*WiY1xa&c=+|ZHNNDVv*Y2fsr1cnscZm8nefrdvsyMiz3v3b_B3$@4Bd!<$9Z9{-a zM46n(bUVmljdkn>tczwVh)3+SOMi1>UyA7clB@FghvZxn8{Zo?gP!|GF^^1~5aIwp z7m@AVK4O9ZxZtU%k;5s7bElYHzZ`)$7wTR{%%uTxzcAR9N8oQ&Q&&b)H+5j#LtQL~ zXQ{yf|Ly=-scpPi(5m}`vwGU;O^HqBY@?xzrp(^)jAEy z1-!umvM=sT-wGPJ9>pY00G(&}ikSeh1RaJB%9tT@N#{w}94w2W?oYLdm|`xY!Lyj* zpZnoY0r29z%m1A>5>*_H%wgGz%l{;$%)+&c~s1r8NN3Tf6gR;~Cbtg7>X2U!glm%NRcWe)n zJ~c?7sMCN_RZ2~hudi4bJpE(U?G$z%!}QeX>4Gqv8B~P}KxbTJs3IT{P(v)p`h>=I z$qP9&1>OnJGpf-uk~DpB+%yo~Ymm=&@uzS>bg#P~+oUQRR~(doDBNnwpLt_gH%OLM zYlanSw4h;RaXLT3YW5)BnBt%m2G%l-!{1+^uI(wE|Wn9ng|5|yET%Hfn+<+U=^-A zA9MA*uh#hlDcAW8{&G9}zdQ(DB%o!AZC8PfnEB~?)R4VHovRUmDGSqcD>L&GcE!A6t3 z0>0f6m%0a@7uw<>1*MgC&GFD(h%(!AmB`H}#|?|P1|!9Wu%ii~-^)B8f!g@m?hq`f!+s zu^kXeF&p)4ZJX2%(!=sfQ6XVO&xm$lxGLxt8m%2eD!AT7P$3?yi?*Ev!Fos4AUw{!`ev z)0i;T$n~{QP@n!Teu108TD;2d^B}A{C%?^JOR^7>woiK<;co$ZCohNCrZ@Q= z+a!hUohm~5lqWtlt#qv=)Z$kEt&eI0@W)EB5d>JlsY5T5iL#G6kFGc*f)t3L^HWS| zc`Vo3LDCVRt__g#KFn;2xfbx{8sJOHJ^+n|+(1GzvVUIVeExH>Fe7~>L$Js~xF_BI zA=UF)%suGd(x>_lpYl8!vW}ON*c%U9U|A)wqWHng7eyr{MStyHr_DFkHZpO3t*TFl zT4j^Wt)Vyu^oB&(H%7D_W1Y0Z`d_H2~ zo2ShtnCmLv&jE*@PbV}ktF((vfhn|~MmgX_G?NwqT8VuWn+?2fJOQ?S_m64+YCd9?hC2f zC*iHgrZQae-2WgZe2;|mqs=lxB|??!onk(|Z4mktBW!T>)64#753NT1YkPH_D2v%p z=a}tDD~;ocMV+f#oo2s3QmsxI{0yF{wY$w;73fkkp@mQAA%lj}(YH+(9UlkXf%87N zS-RZr>yYDEX`ZFOk!D=wa7i?nA2{NiwKCh{^XgVHyboh}i<#4@#`5r+abGFl3&<%q z;N#GB731|+_b|2|s`KiStSIr{$*Lo3On@U+B<+OdPyaJ9(4X)=#c_?V{YQsjm9!sQ ziTD>~2i9b`Oc4PiBOb$&!y;cogTs}>sHfe-FtMaHu>@hKW4dw;6$~7=Md-2S1(h;QpGz>G$v-G z+o?n;a4ZU-_Xz^f00k^&PJ#}SoXz#X{A_qF0~=`mN4y@C8}H<3mal0cXU1hIDYud1 zsGSJOHFp(|N-=PI*qrZ(2X1hl^TXBVTS|2!xDu3yno)Z7Zxp%CVFb)_9r;Bb&FwDI z5QE+wv7oW%WsLFn{v8SQ0gkLCdYbai0_ZF zx1(lG2C5-~Z!Dw|&6}azLtR@=c-EfjGe+4Wv&t}%uRpxHl$FaO197D&KhJMrwULii z{Q=jQ$lp8h%hGx2=AW)0TBtrD8t&(Lsy?cM=q9h{O-g-p@9qTgN5olymIH`Dkf5q^JJcy^G{tf9OqE3^{Jn6!cnGH^SrINTp*WzPihj+xF`@>;3$fN7T8Ax&URV} zjx^_uR=-!|vB9LR=UAAMt_DA|_%J!gp2hamJyQH-)D{0?<~ftzqTuQb=_gc$(Th*( z6~8|FrgqJP2LJC8GmYaIY0L&2&gT}*b0h1*@ii}An zW=rA;0LFrzxKZ`l+>EE=9%^XHWgj;b;%NFU{%wy<#_j1=1Du(ndDW9KO(oZYG-y@L zQ`%I_B|PIhK9yOfH3&BqF`CX85S3QqICsgHJpjFt`55<{yD7;E?=_hDIBzP8U54dH zS=ycU$U z8dK(LvR)$A#@ymC(^NGa!LjF^Ot}`_Z?@Bf?+mYZESHJoda{gFYSjhix#ycCSbVKO zQ5vzqWW2fkx}9vfcL`MgtEw+tyz~97pWnp=y?0Ga4uNXL=;RwWXUYm1Dl)tWHFRzU zT|{BUf?|04K&MH`FczPq&?gRt)~_%5J{*ipxXPlCKt#S07((0KuXo6n=K?|qid?D1 z?P+V=6}Tbs0M&GZJqFSRIcxe_PjAE!Us88wu}k~G*aT;!?*p4MTD)0XRMeQsf5+Ld zvk>J-&Q`aW=`@u%E*D>R#bT8h(UqUt^yj(`UMU}>6?779-b_R#p)r()B`3A#6ykl| zOTkUs=IM0>7uo=*(`9!stWV7wb`w%WZ@8%imMs!}_zRO(8Vjrz=*&-sn0yI!DNa^DrI@I?no z(o|Ta_lYn?^j*~OIK+|&;&K6Kpk#qiO(f`#b{7PN-33V#NhgL%v=&xclOYP}@GKGw zJWojCh$oUQ-B$b0W1ZRDBJc3Hz;2fbm?;{qf_RE(&_X#VS16vWeAv%qwgr^WC6LFv z&|t0X0kQb|0z8ZX2re~Q#2b)!w9IxDZHt0BuvGUgadqR?mA}Fs=$?1Yi0n%zxNQV> zcc008AbXQ+^EnsAeU-hNL&H(!MW}vUoOz1dfh%7P7P?9XdQtpcuRW-BR=XnIU~rXJ z;iiYR@?0)DP5?B(_}wf9YaM;>=piNVIPc&B;=Ui0RId;TPe`a%w|TfH#ZS6d>ux^# zG|05o`9hl0ZF*;Q-?&ulO}GueQ;x|Gup~bxGgrb13x^b8PzF#YqENPm#MNg?=|!`c z7hzfQ(4Bz?0I>NW226xY$peC*1uBZnn32pcME)g<*dv_`n(scWihYa(e*E_4CdbObELK4 z?@BVJ*^^)VE5|rm*bGTHO}}zxj=^UIn@aNgLT&2FS1H}bY#t1d)Yd8sbt^#TC!yS> z#*f38*+i}nozyw@9_mvxm{p8`@!{>*PlWhEoaBf!|H?XO%gYpY^K7V90)40Au=~n; zY~gU$ADueIE{e_Enf!ydSK14L?2B2JIqViS_b*dB9{I=rv8~g1@O*s0*m>-y2P`SH zRrq1i#*OT!Z{2jogLtGSox{SY zU0tYaFhuVt`Ll8JXYxg}smym=*L-d1z#>0~D zr;Ogz!$;;@-o$FKhVq_M7d~Oum#U2Kn|@Z-m}{u@t#E08YVZ*`P~2GFgHW6<+x_vJ z@XgQx)m=yCPjT$B&(L%^-ELsvR2h_hq1xMm>8hDAd@ZZ&oZde(ENPi0gg-Ru$q{Og z#AoT1_&@G zX#@+RwvwRD>^R}%vj7m+7Rh5b$$mfMne>g9mv@?P*Yp&U)L?n)I0`-f>A*v6hY@Js@kI zgw*^)E7^pur}o^*7VSKa_~af=G)^&jLYMu*HssutegazGR58mcwjRHI|ISPeVG*;P zho1V7;lVN>v?u4{RKE5DZ3NZ~=s~Ce5;5zUe%m0E0qcYa>%cTh$hP&^X=%Y|GbHr~8%7*ssAhAGUJuuku>+~w!7LPNCn6YXW80ORCe(KXZA zYFz@0QA=!9*c$l8jVv?VyW?J#H;CBP!FXzN>fm7vJ zi39b;Wnal(CN+HthQy}3LF=V&F1@6asy)+{oSmv%SumG7*m1vk$F?&c4l=cc+Qdqh zC9<|6eR=(=3Kfrg#m5n!;DwXz!keZ879TWb=SwScsapn83+kAe57bLlhEKU2IxyBa z1bSDuz;PJFKSfeRLj*KTU6H0Q^TFChkPB0%j9dLCR#RZ7EV(&JY>LGDo~R@Xbxei+ z7pHcdR$1kO!Ws+ml?vmlD*rT`?bt>v2t%jT1;C9aA2iMPWXt^KjU)7GNlm(&52sJs zszFWG4;v)eLaZhk->3g$u`s&f5S_#K>1VddZm)C=eO$FwdKMAv*SPCnd(o>Q_D}!E zQMngthW^C}p)5zmX}#CXIhA7thOuCpFw}UMtcgzI3kF&qf(+Y}jPYQJ_leWNN$B?g ziNhqED*NPmqQw@(N|&snDu|bbN?8J}`pJr^(8!dvJy`tsqeTc8zU!u@q#ru*{q_6V`q%q3{Gfg-YW7Z z+0#HG02!sJ#8bv64}&xv3zmo`*$oKe8|2E(oK!ld&f#AeY97!Gm?`U6R@TTk!N!H?PT9lMTg40LuW%i)3b37 z<5q@>+tQ+Ely`*0b1qPw4ikqS7P|2#DQqQ)Ws_~Y5f_F*E$2Qwo_JoiRetF7FrO}c z;NmVH+L-@+qR&xxF*ko~h zqH6@t*c4PMVrd1s4CTUnB9CE?OMf&{1$l%UE>lijrEl_#+U5Bc>9h61!aO}Qv~Nf` zqsFoBlPBfwA|R9Js~KSm!Xtzxl?LFDN7IScd>kb zHd>FNjXey#azO{0T-#4YUAGqT)ePnF4)JXXlNE$f9ma%>oz0R4O1}iL+6N8pf-J9+ zUVU8CboE~$s;GI6sR%g~Kzo0L8eUWqf3Sqp+Vg&S*^ncBWjE^)A&YK!z}Lst)bbOg z$ge!mJ=1)KpmEcN;;&cUIWzFholC!*-r%R!t0xxTSgIf{|IfVuB<#smsPzTWSXB}x zl&nomDuh7K;dxx@HawOQVPlzMGYw*+{anlPL1_!mu2zTUC@U&U|Jd+vP_%!sK`7KW zF}kH0Y2`HrsAjC0t)waoeMj8>_$n2JS?(`Ci3nf81ub51-IS6h+z&JQ>C*RJegN~` z*NIZ-YAyBY@y2*Px0P&Z`$)bKH`FRN=?3qX2SwyHkJrz5Ka~QTz#2zc{iS)9fy9p= z2uIm@H+X}>eP>B4jro+Y97ZCa0{tBf)r*`C7)!>z?#Q&s(O|HfR5+vCksrNp+!!gR-ZR zRYI7$^yvAXe1E%5y8oGKFne`qcW3mGe^+m?G%u{{Y6S9 zf|D(8X>t6r^l?@<;i~xrPR_`4W?4A&#Im$XO3OhpceZHOKB9TYKsV7uH#xhevSXen zX>F1};Ioxnmd^;H-{9vHvQbI7V+Oc(t^kC?re;88{RLEHz!%?p3pw~3Usj2eU3qq-(nW`wLb6EZ75)&Cw%o;M%iEoB-9`kgvFobN&brFb3emKi`jdb*%z!_ zW>YDz?8qLe@YbKOG!}}B$`^UPceSN;kn5BEW?(;)^6kk2Sfr=mZrqWwf|JUpO-*)~ z#=T2LY%}kMU`@HnntkG%_D`y;fAq&v{7ys`*F3*z{=VWtc`F_Pl*9w0Ysq}fAR$_! zW2BaNVa&O6y2XDH+k&CBF`uN&+TJX!Xsq(GbM5y9_saW-XrB$3Fa7i@nX>sP9I@uA z?(!|4mrb+nlFvk-i;tf`-`BQ5pAgR1ZvrpR8*i-_be*g88YO*uL5d6neiqFJKQG0! zvw_aIB?~b?>xX+*pAN>G;3s}J1D0Js^Z%k(Z1gJs^8B*rV>xiI#(HHBV1!;$88wbZNq>QTQMb^+GR7?ZQl{=992%3~-EqMIfpx5tj-TWd>j9 z0`t+Ze8UGLI|uieyaQGy9C$<8OB#4Iq z&Hx>?UG&U%?T$*(*pdi!|1=rhvG9fztCbditN406pAl=N(UxEq=6UNbmiHF$aPKZU z;}*F+(=WDf!Sany?_$GAy-wp3mxpWM&nC1=XDZk{8*+}2q=6@!$mahB|DOChrgy#K z&wl@q_O*5GrQ|lp$m5N^c6Q{kdA_s8`A?Vg{QpQ`ehW*o;XeFV(sQ?~4N=>?FStOi zx^kRib-eX`wJEEW+cjYf*XLw^jkG*9&YNssZ|kPU{d(@->)WvfHh+Bm<`YwtKNTFw zLJi-J9KoVyCd9*Y3VXM6FaFd&?rxz5JBHjIQLZ@2Vsz>e3Exc_G#Q67bWCS5oL3qy zCLO7sgr+Cjvm05d?_-QfATjB^+^K5IY-yVk)6{#@;t8ZL@AV`?H*FKNudIzXM9aLc zFuDz5laG;|t~S4Sf>G)bx0Mhl;{V3x#mLV#y0-nZEmo~|&E^%mrI^}L{i@AgSY9(@ z2F{ax$#E^18{@Th-?jRnF8AMe%rJWxW0~A?L+5LS^b45wF6Fyf z7MFs)n$bEYPuZ5a`tqNnXs=<9+JGk^MH=&2-nLvye+F%Q1vY4fDW_XQ_Gy6{&Of#{a&sJL|Zphb%p;@U+xH{mDexmGT~!~(V^QUu~lLwMG2 z3WUF}h*3^^XCYQsi=oQq;N5Bq&eNYT3D?H>z2vl@k!p+lCZWp#3KfCVZoWCmt}&H2 zlSLe2muh@(U~bC1w<8pN5{-tX6^S@H#j1H%-H_s5w6eC>;Vl`ceMHIOJ8}z(f6>65 z8eD^utCjU93pUmp*)>?pH|AH5%HHp^Ce+;O6uV1qP*ai~6tom5-dt*2)hpNqi{TPWWDssYno>9>Xs1 zrTq8khG5XlW|~qg>CxL)BG=0l0()id@0Aemr$$+is)!{wT*Zy*xW8cXC4Fv&$Mw&) z=ErBEs*KH^2~f?ffH1#g@ttoi+4RP5)uK%WVO-bLFbg*2EX)t*e1xP%pEa~+vd9ze zfBw+rF4g$Jo*x*`*&_Z~c0K0o4X>Gs*P!GjPg8*lqUFw4q?P&w3}2QH96h-soFQ^O zkaCZydocC(&G(MqAB03NcYmt<^cH&F>9k6mPSmJCQ6o+=W|Dcwq{<>?&y}vBfGe6m z6C`_D1W&x%ohD>?624wj>U^g^{m>!j2+?Wd;LTY(LUs1({k3hKGdS5TD3u%3q%l|j zreXu`Q!yKL&mqfzC#d-46mQsn7Y`4VAAgkYynOjKru5yfUngKyBG>V3ymg`29Klo) z;_h{m%Gg8`ji~9QJw{HsnVOd*F>=)v_qFaAs*xI90YoaP6+1d+(x|u7Jo4G^!@X%#; zQDd;AW%If;N!Y*fP~c0MVoHf%R(DMbUAkH=OTm`Vvq=#?)RmUHT3TJ4dM8)8`HB`< z+-=D8gxq&YWXwKSPvFAwkKFON(CAPv$t~`<{9@BInIBGB64ybe^=W#bo>gKrmqvTG z>i5e`tQ&JLQ<=7kBe6AJQt$C<>6Vy{q=JBAqOv=W=N&s4Un;ZjewWA$r%HZYz{am& zJ(P{AU;I0pK+L%j!JxVz9Wp)%4XMftnXD?gi#OH&G-Y?i)fpL$Hw zQ?p5rshZh14=a$g5gwIF*)w%6@q?TdATdoZ1|SpLtNxZCuF6a;DQch@+8uUmjOISl)_&j(a8kF9lj%lM< zDj3N}I(Fs8$P4s{Jhavs9ptqvAQ6K!&wjEk>TEoov#aVcY|~uRWjh$p}*f{u)LEgBn+A{?Rqij1yXNXOudJW;pVi0mzqXj?eRbs z0fYaZzi%;>S-7!MEvsM>Q?*!Zjmag57GtT{h6nYgh9$#XGKx!KN^&k94m1!Qsr;EV z61J&v$@oFgmj-Eqc&f&AH<|LKk<_>-Z-@^ab^YBB8J7%i{oSK3X4nyX*dS#J7pmc>VR;O~d=RS&BNT33I7!^$`D&8xt6v>8ybVwcL| z#}@ETZbVBw^*mxc!dNdNF5(Zj53m5^A1*^;FJ>1ptUl z9QT4=6H}pzEQAysf`IH`z-J=4q$vCIk9&uh0P0gI&NU=fA@P%$)3=8-!L;lRC7w$qSwm)bIa(H(!JfxkHU=r*wq18r;|dHCPGtotimKGNZRqX|>dY z{56TWK|$+wX17=`lvrgyyJdJnHYZ7U3lxo@P7&%seaWT>sshne4ccF2ESz5eA9k zi9BT>5k;V%@SV1zrDQyi3&F+M7<_@Wa)@>E0*s1mpQOnYlpMPa&JuHvEzw4ghCWP` zSDtb0T(n;{%P84OinC;0wt6|U2U)t1^vEkqh-dj3P?THg9$0B~^D~2WG|NnJ9QD)6 z;t;a-cko3AJ<{&H*qPM~JKw(Y3fXq!XGP0AGmveo5vc%idqkN-z9=P7e8Kz%8| z!mQ9PE_x|Cl3o{-lC8!}_9O71 z*4lZ@!dnCuCpHd*#dwKv(>KmhhtGKf3D@0BA65Wa>Pry&y7gz?4_-$l7)tK~re zML$1a5e;=@Ikc1gvP=YZpq8zw| z^WLV30F0Rx=`xBSX2t+`ktF*b$L)nkctsH8c^6}XohKWWCayatZdt-73*?FD zpD$mUMFaWJKrRTW&5JZ2a?fhr)CUW&sPE|oG*f8 zlFhsjIF9$CM;MCNvGH-U&uzz5McY})7fp#(!}zU7xF;7;on-z-al6>M5Q;SKnrCP_ zJ>(jNW=0mQI2Y<&8l>+_>lbe>2$Em)z4UTbur5e)T(tSZ2ZeGvrfg9{*-v4T*uwTP z%89Np-Y@M3KoV%e!59hs5f*OZNWX+sGY&XT)IG807+sJ0Ar9i z+t*kK03HUeb-UUw&{@?;2E*>Js{Bcq`@DKkZm`{ambja25K)Lg6T}0Ae*&0^#$pU! z5$p+#rOf!hTA+9{4Xg-4pTg_LaR@IUnwiFNxFEhhe_f5pqiS3GpVIDjW>Y=x)VhhvTM7cJgKBFhYirUsgv4u|F}OMp`R$~dwuC^J zAcuxCB0UoM|f5oKoT zWqx2f*tU5A9<>~_eu3|h<2juQP-QnZ* z;0wH_5NOX=CnfRJ{(OMw`R=>By)gIgYK5+MP2Kj=eRmWTGIt|py`t3plz6f9R+?}= zC5lDj>SRCtBDZVu3&Ih6q z#&A>8)PXyX*Jl|9ZT5J+s-K=wVLbJcC`_2c>;agOOEQ2H5HqCV!vYZe7Qp=$aM~s& zLL>?c3!)!Mpnu{NZRH-u8B4{2(5EbhGgx35F1Z5t(FymlWZOa;XX}I$gOEhZmIPvJ ziYrLP)|b}FUy8}fo8ihO)!P=K%6$29e7dBqTQz(UKtJ;@n^Cw=Q6#=HlJ~Om-m&$L z@{Yy3FCUfVzf6Yjm@Drsw-)~k|3dyUU*2F5)VfKo`FYT~ZPd2ycjcE;|Gx6hLHw2f zqOMdZH*T_j{leAui(KPecbo1QvFbQ!fX zd#ZMsw?Bmy3z2=p4oaVx8~01EVGe*uJQk^&)-n{)>Ps8+Uc1`q2W+oKhxy5MuEiKV zcvazb!`p9I*-yp~ASDapE?bOtGS>RG#_jw~SQP*mrwRV~G(naL{uLIAD-DN-hf9$c zDWqR@mi1+<6Ke245Snn_;=CIFb8i?BsS2;^uc9naiMieH#LoFauh z2v_!()5o_B`U?2^^`B8|Tjtk;o$tR|<5Yga9v&NA+4=rm*;$3JousZJFGLIf@~-nZ zUF8S)tJ2p_&4F<38($9Czlso4gvvy;#U6UcNB*@M5GVlf z6QBq)&uPf*oBhzIpQ!p^g;iSO&elLCYQ zA@mj?p?B#ZH3_}dkG2f2nh-ap(KRPSfVgeC@D>0F*z|w1#w{^34T5al(+=FWHcn@ z)MOZ8GU6y%8Ch91Em?INIRyy5WTWX;swKUAN^qjTqE}yl!prdD^qi?BWWUi~K zpnFzJ7iX(ys-@?dtZ(dOVCrFD;bLIni8r#rn>yo7J@A%ZMmics`esHJF2?k-@i#dO zH__ELaSSwb^e{Klv(UG-G&i-h4zjfMvJ#NAGI6mo@wBpbwnp$;)64OkwR51g`z7nB z*Va)@Hjb8dvMToO5%%Y=*+*78TDdy9208|%J2`thd4)LJDml~3 zX_3TNYw5Lb-|bGRy`5*ZpC6&4*8NnfT{Ty%(5%=M&e{4&?B z#l=R2T&FL^-H0RQ-iV8iPe@CQi6kZ8NlLqy!YiG|B9L|?IW4y+{a$%a)o3o4Wd6gl zdj*9L`NRrKs~$aj{q#~`c~Ne8xo3IR+h=rz^6QqGH(jp>58gI+H#Idk_q}fE7-;Dl z>ul@j?&<9rm>3wF8T|Zp#K34`bYSB1TOcnaBVw&=lz50tRUOyWuWHMQv z?R)*)E@=bVR%Y%j-u=WpPcHi8z}xt_jC^ML9ZtEfa@`lXf8O#)etzlKc}LjlZS_?0 zH%_0Z&7cOh>a9|=x8b!)Ufb2Wn+{nECDF!ztmy0OkbIgwpHxrzs6+$*n@n>hIH zOjm0fTg=_GoUNI@V&TMr7Il@4PZiw{GcBj0HopjcxCzl}e!D&M&TRRa;*G7)=?340 zwdCf;?a>5`Q}P_w*~4!mcWoKXT7R#uPx6V)%!fAr+?{`yMKfmnr+fWiv9R?@Pg~2M zgQ*pkjPIUpHaG#(hc1<>)dIAMnh$x1}F^rODE@u>q+shhj_hFH?8nmQHzAu9x;F#YH5WqKJcwX!BmrAJm&8cX?aQDd)hUwpp0brNmWKV5Y_>Hqu7-{`<)C%)R{B=T8ol z-mn>cHw{(M=Q)$V-)%VR^-^tFTO7x51W>TBUT&Oh-fNWquz;>T8mqqD_;miR)9*f| z-T~Dzwot=EZ`Tnv4#ft$VuBd*%`(}j1oIXal*y9+Ufh;pDHHNhH`Tg5Q~TrJ7l)(z z0*^*|(n@UJI#z`oW@v_g*om19e|m37Yel(pP&@vRrZ_Tm;Wac%@OlVb=D(3WYl|I`_!4Pb(Aiy zs|Id*{M*#<`wXjiy8FtYzZThPkYiarGBK`!G{UCJiSCq6QMWy#voZNyN$2)erk$9S z$TNHE|GiXDdE+O z`UiK7QJ>3_zbJdLRXKELpeo$;Ux=y4V-VNOAuuf}Uh6Xln{V0EL}7f+@if)q*J^f@ z=IN^RLczoLOY$ivuT0TfvG;^_IWoD714L*A6a&Fglg$&0&S@3)Foz2KR_w?fZ|d36 zsXD=52Z7SBoiaiI4qvMEg|^ZMfu7#$baEcD2eam^uvOOiFI z8#TgT$D}vx&5G>D%fepBJl=-oyw_}^X@=vSvIf#Ws_%4~;cX9#hpeSSD5=($>+&Rl zyUd0WuatPYVL6~yJI!8!(e$`+4C|u{YFWQV2FdCr!spE`>A~kWYS+s{P-|vi6UTCD zqn>Fil8x;89a4BVo}~~kS^2;JSfmltniVk@Midi%lGScb*fDIvzRQe`_qr!Uo{hm)=%~z8c%d|9ae6_ zx2?YYei&j5DH_#Q@Q>VdNyaBumw99-$f@T-jxFj!&tJaFoA!{wg^VwHw2>&qQZj0~ zu72jt<^`Kv!HM_Ib(?U-OP^(CnrMu!!m2f4DX&(`qJ!Y3iXqbVSMsaUmM%3Gjny=a z+;QbDaY47cTpR&zeb-XUV}fHST3NH~d*&`d8?|U2T+n)yJDhoN z^}DP|YX8`SYt9?my~ZPPeSd!c`~V;Z5(&)gG7JSqV9UA$F1=g^U3V-~rgF$dZYAc0 zZ|&??l#}^Fmk>fdrt8$_DX%;I;zG5th_m$xW`hBZT}P+|z$fjgQvhpWtca{Z{cU@_ z6_Kp-Cd-y78bO0DVOWUvB7srWIAKL7h1#QzQz05iqG=~5{Q({ho_#7orUZ=?DPD7p z;vBY&Mg9(Szp{;(Ao}`XBFztZMfKjdqWq}YKH+nW@}exx85%C)ijup?L_0ms;1+u! z!IF_Pv6$>IZ=>O;Om#a~= zy~0<1uc7*4si{(+&L`{gWD!1S=7kW5fT2-jN--{3La+VO4Q?YY2@L0TI#=z`k4gRC zBJ=r*@2*4IU6@#6hR2OUDa-etYmQqk_lZy++A@wpV(XYuvv5KAV7!7kB~c?x1G9r= z#AueVbYVLA4(gJ{5cR3}TYnZMJW_^krCh4ZjcnMlN9t#%F1yc2Gtc_o@=Ew=Es>!gsL{kTqT;zYzzzLdu)4XD33i`tttDzgX`~+1O%4A7#y&m4BzyN z(jo?yD#PapQSSSihuMfWEMxO$mg>bsDw;_E#w(y1@>~Vchh%LJCpX_GlP8I9X%M@v zFT5{6h+HhAHvzer?JKb#`S30;Bgp^2{-%Wt0-Vh#&!jmf&3+pI=ECtu_KzK<48B%MAhHzF3(4B=5w)MTZT>vp%$My}G#y4iQkj*xSOxmJV#0mWnT=rw8yX#j* z)U#q02a&L$@GM;r7bi|;ViDf9mv8?o|A`Emy8}_gnDcJ^0$=HE?H+{c^$VXX{5>8> z(^N25Vge)X-BXUjCm|7*4iTV)JWCvgx#zB0Q!Mg9-hWMbFEEIzVt9Tw-PjH9tApnQ ztgq{C;LgTh!^o^=>(AI*K}+s>KDgImioBG1FTx|j&_Bwr&!6UhU;aV>cPZj(6W7(4 z2k!n4JY_?6`R>J?6|1v|i%ew8t3UHcOz4}kih)1hDZfNfhpdz8UBufc7XJqi1%fp| zmw4UogTd%hO?2^#A_M>H>e_{xxZsHE59==}80Gl>zU#Y0bClc#0@E+?sUZ1s zE^Jf%Uur$ndlnG#;~u;DlgX(^lQ~bAR5&=7xLq6*IC2oS#>lNHl>>Y{~m-xXE#W1))RL_&qvk!TvOTnXv&1{LS zI>cJGt17=;3H>8Do%o^eu}uHtoOVCBgKBQ{AJ!yM~LsIQfO+31no@f)LYdoxqG zjRv>alybS#oip>s<>BL&cE8I!LX6-%{$s{wW#ex87@MuAav{*&4&?KQO_tV0mX1Z1 zCNyh_a{P^)r@8y$Qz|(gj9xu%7m3H7Bsup}lZxN8BgesV#O_{&i&(`2+zT3yopI8O zImZg~5)WL&e>M~D7GGUH7H6K2Q7bN7FW^EM?Skn7JpK_JH6THb5){g^sunKx>Y-@A zim?7e?hl*(uQ#je?^H?kBbSc(mt2HvSGXRJCTf;&^ga~)G%7eCSoYkr=F>y&Qk?M3 z_NwJ_)ObYI^!u8Feg3?NswGO*WWTAdXX)o0(W0{IFM=<>mLnTWq$$9;q2vX}r!zFu6nN&y4cq{+iyWWzOe;dSA!OOY&EEDf6S4ThIr zzf^{s3#FJhH&_UzX!yU@v1%}2X?O~}F}Oj84X|M48(!GIG1z*I=1h5M&+N1Hrj+nT z>kpT)5XAuscdaeEEt#9Ew&9t`$Ud-_~Uf(KwcenW6Mft|tmtWrv zZ=hSqt%Oo6|G+c7-sDy_Sn4*}6gCtHHRXG~y*JQQXw{Un)s(CIUU#Oc>~d4)pQig> zO;0bszsvGIqv~BjVN=FHLyge;8msqr_h3obx8_#tCC#skts4EBInZ_B-Wy&~ciggC3`XA2N`&AEDAmK@H{=f!_~; zrIjJ2*6bubq_lER-f4G2^`KLFpZ)d#Dgd0P2X@p0e@q`7_4abD2BOm2c|r%)SbOV2 zhd1NsdMNa(j?ixyL?(Nv-Fxtab#R1rs6%h$qe73-spO+{YLRu{ z_$=3POSf4|SK{&@SRZLV3ouans?&$19(7H>VaL(jhpU#G*O)J*6*_k`^Ygj!cfxXTs?^H=qs%5!^ZcyZF9M{d^XjSu{-^Y zRe;(l41=;qrT_Np&3<{6J_bTg%Il9M&^fHL{mN{gz4&rK$` zev)}KrMJ^7K0Ap!H(Bs^?7rfp8f)*@^mfDjaVtgo2!6U!7&2ElZaFvYbQ5f;IPt-n zy=rd!$ZxQ9Zn|m*F> z+TY2484D?M3qNdnQ(OD>)2Gr}rwnSQ%37y1|1Lgi?O@Ja;PeJ$tpVlJPlrQ`26Lav z^``{mr-fTM+-q8vS!TUz#zzK+UMW(g{3hOIEWd7Tp=5v>^|`$MPJFNd2W8K_0_N}r ztHy{q&&#Xz0LvriRcpi?Zy-xUI2bv+C@Q@4;M`Z$zl-j>OQ+{1p?*_<&sRZ_HL}ej zYhhIEx8X&>`31?0B|v#Cy6uZv=r@s@-%56-#9P-IXV-FDKgHY{yR^F) zOGAtaihSckeS0|6uRt94XdTE#%}9+*-+w%wpa*`fKSTMu@x*pR3pWnNP|;Y%CgiG3 z+h$$(YF!*J`K6gz3!#PcMgKBxT#ss1CoBjF7s8Vshl4!^Z2wj z=v?n}GhTaBtpA>tD45|v+PBU;{9UE<(+!stJY~U9ZDq(1FJf!1NMW}RpBZ9_a z0b(^1<<8C>p3Bp3T$?JRb$Jv(Z@eZ-B?&zL5nd5LF7x8)fd{sTEo7+boxI3_|)%@)`dVgG+dYrc&`BwefHx zayWhZ-S8?mO&+u?SgcULnag6r?X)fbvdXhXPO-8gF0Hb~5_(y_&Qjf~=JjwK+shi6 znF#e(3GrNN4XbWgUu!^f+UpjC@}T)F)e-2Md5j7(n<`k%?N0 zZtYQBG;gge3mcHD+1{~kVGO=Q=DgClB!-1+CI5c2D3>|JFNeFP{^s9sX2iUD+e>r9!VLQI1&j z&aY7m>Az*%rD>YKgUhTQoac=r$Mk>N&)01yF;CXfOve8mxd)XQpf?g(>qK+2(o+)gUIQyIbt^*{45!?(^*ZG|Oc(K*wtSN>a5b_&MD;ymC=%%2&DRTCDX> z4eJBnJ6w_1S46F19xonV4-$xx-`Frh;M>*`E~Ki0LC``s^b(`JH=Y2ISOg%ksgMLb zktJvmyaBqCqHa%MGG3Gvam2FNu0qix3viKFG()62L0m=!I{E%{$}gN4_j^yMfO{#U z!=e~(RjH`ZZZX-AQhKE!8>)}SFg1)Oh9R#*1y8VKXH=De>MVoAxH8;x63iKzO~N4d zbG)$$p*`>CDQ;v6=QIDXv}9(j0+gq>)Vo z+1h*4_7ugEPmCnx_L&B7B*S-&CNdI4CUxy)_D9+#Jes3S8Wb_^t1{UNwk0rI*94a5 z6p9e+tHuP6(k+^7p3;86<9CbF&6Vj2XPT?{w7+!gOy4!vTsyk( zSnPuJ&0`p7FXZB3PpVSVO@^XBtQYsG9xXZ;pfHN))*QpFbaB`5!Lf14smN8s3Vq|V z5>zbXk1s?aKX4+{8x0nsQtTA=xG*mCFybE$j2HG2Alz`6Y8@C6E+PGQ!&6B_g2@P3 z%Jt5^Q{4RS6TRhSo{T7eQ3Qrrm6Cu=t7DkPW4cmE386CTBoUqV6y<^?{Ht~L87WMq zrTuYQWS%IGl>l7VfJf|RcGu}TCdDA%k}br~NOJpiURLp*gw*J)kzg02hW!LGmuiCk ziao1(gkw)7k!22yOz{Oc8haM7tXLU-$3P~4@0@iK>p2c)=i~%rYGR4QMtxt;*A5;+ z*!r3jw@&_uD({8Y>|y_<4*0CA1{?-F+C|8{LjV1Ae`IEk+LdY-BDY<<^^mWC+tm6$ zxY~p6xks8i*I!=Gcpd2HL1?LSo12pieSgre)cdM%ux9+VW%q2q9`ld)w+=LYcDOGZ zysYAdvid!4iLCW3?b9XRVgarE{nqxls?`D72q?W9dr6)*Le}*6a;{Idh(c@X5LOv<+@NB3Q^q*Kww)1A zE#a!OH=&dmbjIh_)6<0g@} zRHI8)qTbYmqWJdZSB;lp;JbXWX-yJO|HDO_)pEMH#s0*vz3Q9VzAZ9S*rs=;<#AY8 z6)Q}UyL+I}>5hxuL6vJ*=Q12apFfzl7NAiO<`$y)KuSy|j% z@ImKPty5-1o_1OJy0Q#ebIdUo#f2;>2L4m^=Ol;g|B@x0;~Qtj7lFq^^RrM##jDP< z%iQs|x@9z()0`w6Ip2kwtX#n~>22cr=~6=DAWY>sy;AmmK6kWHX%iZ!JwE4stiCYum*k%26W$l8O1Rh3#|Yk*%WvMAl!&`EhaG+ zQE@g~478s(#2$%P#rH|7bWhbrf_*evsX(lu4%}$hxyTIcn;?#?I!{a^FIIqoUT*SkUDv z0~pnC-bm`$No0Ha5v;E3K>WEynbJ0w0defZuA25As}ILd+ctl9?M>^u>NOs+v>hL| zG8A|3;@{bM6Wpy0T~W`zTLGYvZa-_2s{-9~MBr<)v2T&VL36i5kam(#onN}?67Jo) z*N4l{Hm7*4?@k6c@}8hC7yc<>N=t)rqOs{!73r2(b7mZQF%eeAMAzS@G*aLNW&jKa z_%SfY!{W+%Q?P)6pCF+u1r8vQ>i{R2fVy1i6)vts((#~Foi;wy1gFwoLb}{7Qd7Df z21PuT9%nA#r!(H}Vz=?71-zc`JHS);+=eV%XVn+ZN6lwDP8*Tr*qU4lDwV?EKFRD+ zoFv_1G3=1-{-E<2yz3sU>#jt{Q~Rz5;l`*aV}*yHL?-~|PViZ6bxk+CHe(zU3A~i( zS@t5>a2qZsc5hCbZhbO2+%j

TN2=Dv?sxQ8WO6bURr&pK$r#&hRri=tESDakJkR@+H0i#)h`Vv98S^J{b-{Nxbye$$<72c3%X!W3PmzRaF!^=b*{J>L>UwOJEdM(~XaJeH3+;7MHd z;2l1+x0_W%q3(FvKjwx$Jh)jaLZ}fC4I3fN2$24*a5)DxC&1}fG`T{!#CW~y z0Mtu}+rwzuK1+xfpe08@8V(dhi3VmvMXg5*h33g4AnP{-St~w6X*^0s@>=oH4BLWP z!l)W~G8^v&M7`q>xfGmIX`DX-f2T98RT|||41SW$_-ca=9fm+b#8b>-IR?Q<(jx)( zppOaF*4%3r0uY)y_P|8~Q;E(%Z}aW}nz}L!QUz#7GA@6?^t%q@Pr)e3hUIG5sn-S? zPNeY&%yDIeq(=mW&wA{e#qYn9M3_KeUSysscQL^wxQ?Fl&!fI9_g=3#QyNE!wsVba z^E$M2s6>0t*6z7w*E7GoO=sOw;x(QT-V_0pz5g8fE=Q)MW}a#@hPT?WMHn_UGh>c3 zHgR~XoQ>D#Z07ncAK7hPWLHXk`{ptZ%8_?zUQvrK13I_or%{hU=oyyLvloUtq&+*123z0hpHFDK0EEdlA0or|2;;jbL^&P1hr(JfpsojC zOa6iQQ{eP}K!6e0kk>l*2cU2Y@f{iO#KCVLK?RW^#1U*UhtLJY=aYd1C2IRD)C-_@ zk&VBriK9BBLJEw=0iFev@|BoE0T`(^NG*VW0>qckf}=->{$y;SFnmC;+ldZqx(A}C zq4s2>g5IGp*|xg{@M20>uiDiE@cFq8QmZq=w8H4WxtXj`%$rh(_XLoqI#kdeC<(gZ z+aP6_hg_crKdgQ1juW|xq|EywIP+;VRQ%l3CY0&H@wb_L^CYM(HNPaa@?7F&t|TZd zrFO~XuF|}P@Td&#=mZsu}MLC)7<9{3F_1k4nG zC4|jkIkjR4B{R}YL(FW@W|=fk-bC7L~SSRt4^_=LaY^pFGxWjetXPpW0C^rm-iZ$%nqAGeke z%PGH93(aDIkeP16O#z$>G!0d@4uu~78?rYdo4}Wt;WOOJLR-QIA`c32J0Tt0ZCfuz zol2W+SR9MG{f(uhOD{32uZmDV**%@dtS)jrx|FsoZqjW*&y@@>PSbgF?uPb2&&~|r zoQny=f%ye5y2nI413J{_6=v_NW4*5e5o1)_uE<^-^V~<;dIQk^b6m^`rrIpB?rPoLCGbEAE+vz8B_bOVizqDZ_ zE;P@D1YiEP?o>9TWQ1v&u_u8M_vuo!JT#a#+bL}|`b5T6PBC`_P2q2%t7nOdk&t_# zEuUroD9^BP5=2_0B?UImNuRn;5$g6 z9J_~OdM(!EkClan@@)LYA7huEgOGPGq~JyP za(mc7!(y+lz)VYK#}fEiSD7e&X7xXIHxCSx z<40X2&s~rwXD$AiU3F~`w9%slaN{4UY;nd*h5wD=$FDY@9pBO49E=WO z@>a5OitIn%HY*br%yu%?6$*FEf%ossp4vG(IlcwNsZ&q#LF8|$s0`)4+ z2tbwrOmw*i8}-*CfB*X&__<+M$Q(FPh!R!A+r1GVxb^VGRxW?;&mal&MZ+NDg4c2WXNTbw%3_hYHHw(}iM zgX1PK-eb!qxAAdAak{zTd;d2ezxtxHDaX)?bQ~9sw2TX6fTpDIpbnkgFx7j__ zDXFJZ_`lBuT^+Z_3vFT^B$nH92k05z`zEO|uJIH7mQvYtD*D%~RL$vx)mK--H@M3F zm1Pd~#@RhjOl}>u`|a+-Vh4|*MQ+6&7}Aj=GQWoU!?d6hs3bj{KA;zknt(g5|Mwq? zc5ubC>-@EU16srO0?bRp)E{4@N7>f+jA|^j_N$ZJxVM|sKxFarqENJtf(~9~L(8c~ zkfZcRBRXvawz^OygjuODqhX$Pnq3Cw62Jh=b%6$6gOR9#C~wELVwhh%=5Gker&$YG z{f}A%%6Qf_0$If&gWyzsP#5)18zihSPS)kh5y>jsu$>jR)j&gkHT@4nSLF2na0o5f zt>U8y*MHYBTk6^F$ndtiG0b2BdG$;PmY@`c8yIo2Z{bj!l_i;-?QH=Nd$|NOhQ9nyXG@)5U_^mC)bz>1S34b${EopO#ujzDMDMywkf39yr<7wL3*)doq`u z%#J%sQ=ZF1*9G*k4mwLo-FliTwXZ@fY3ojG7*FfMD)LhZ!(}=;4}Ym!psbo4q5#mJ8%b?C=P5?bc)z5%fOQe{*x zxzkbdBo63eZjl3gwUVASi{C2asUgUkm?!1&#b1}Cd(sc1o+rE>?HLPMW)(F-&r3juk-~hY096}HX3a)pBh7lU zs}kAw8_u{cIfgPOxnG%&^6?X^dlK%Xe`!0K?_=X|hlo}l#2)Lp`0y;h;kHjLwqkhW z8E5?=dB<65YR2$s=hRDODcA65c`9)>flw9?UwE4^9~nBAS}Q++wq# z8N_x&3X4fpkzoX%R`_~yXki)1<=AZ0qoeNCbR>f*3?$> zAxkhWZUq2#S*D9uhqdFw$VvK*6tv?KaLtG^9Hk)-=P%Df7=lq;d;;pr8-Z!*;Nfq zJ^Je6pO3pzJVJzzw)3)^phkI(-mh<_Aoik%-IshDhf`drh8Z`HL8$_-fN4`{6x|e{kZcXEE87u>HfQWSRB7mCO zCqEO}ZmMVI+nBe?SJ54eTrhdApI9HEU|hX&uCyCZdq zkshwZT@y5QXYJSJVGpJkZ(c%fT{60waZ^=`kDUK^YIgmu87ub(s_Z2%EwQBXQ~jBr z(sh-g)Xy{n$qtdrfL1zqnOVt2za&OIf#szkCjvgShaL z!<(>Ab$XZaVLbPi<0IM*${n8UquFwK_-ZcnXnonpjB&XTpL+271fu?@JwG%=^+0bn}KT2caVq^_ZMslTC*`kG*M&ek( zF@^!@GnpT>O13jkVGyI5&AjpoXsn`FLiHHsaSjB}ng&_;&lwhz#@wq5^afUmSAX+D zTz@PB6;k@&d$YD3)auGtJOiZvpwZrpvqdYmX=D#r@^_}-8J6swy7S+oNiMGh#&F^1 z1jKO+0$mGd*oR)9wL7jxrBe2m??jWE(1p8+WjlBE78Q?=uvd?;w|k;X8F+C~!P$?y z3S=pl`Kg^Mf{)OCgqAmeF^t_~*3K|zwxT0SkXJqPlZ%Q;Iiwq`l&sd&k`(qmrNwWv z_`z8r4#Loj6G>@$PA6)u0@ki{oaT@;9P}i{{5}i4w+&suMrk;xAqARw1NA!#EmcE` zG^msbbRR~d^+a>Y;7J{-M+>T^p;#Q~l-g|alUdHAygRJJO}@nYwx}V1*^~l3N=65+ zMsC%h(-Md;`-vB4%~$%Bj=RISHoW{v4(fscsnMcKJq<4v<;NUGwhNFvGlrH*=g!&A(#Sq6B&JvrtxF`;c)b{&SHnBqK{P`heuyl zAuIY&%{24GN2c;T_8BGoBnCA~FTe*ledZxtOHB2x@Di-yZ+uCY3en6#ZK4`7c%;qw z<}6cGGX^eHAtyv;|EA(L-f+52-YcEW<32RsfFvyQjI#pM|OQQH#L>H&g^JuE69VT)b!V3lCZKPom!g55L_WFL}p0n|B(1%Gs~2#@?vB=9iz~i$)HCTx zrHYvLk#N{kL4h080<6j>Q0sNcSCpXf#JrHA3Qi|JRSDXlfZDLr&VMq~Iw4Y*YBttN z;;|_jj6Sa)eTNL&q(rGiW;ZaBPENjb+I52x0F*vvkkm_>o`l3e)n#PEK>siz4$*K^nLiSN{by z@+6OU=Ezr;AmnNDtkNb(d2|PEV2{)zjngu~GP;271B` z7Qz=`Fp5FtvkaO!@LV;bPled7gEgrUVN2lc0C=-fug@cPm%z8{(=aNKj}qR#L}*uU z%q63J1#r0z*1|cUm(x3F$klzwJTjt@589wYHIWf369Pmoxl1B~GcXXj0x>}~ytIUx zph9wKC_fe`kcB>}K;00)g)D=1TAgpe;T;KRy#w2?LY1nZcQA0K4%V)cJY&Mk1JQcv zKbH>&o7I!x^;M_M;v!80_7&);7P8$Iy&v<7_TrX&-84H*yR%KZ$VB6Tymt=8u0*`V z6QBKp4K=fr4O$9;4g1a9KD%Exn43EPs!pK90GlCibAPAem(osEl%-h?ow}b` z`O%DPYj1^jIQ{HnQVU6dp6~xR0djI5%_rZ=clXZR@(Ted+TvnJH5ne0*Cc7RXB;Mq z0R~reXGb~ue={&%ZJ@_d`f-3OVVqz*2mGcUGKkpjrUID(;9oa`73PNRN)Vm}sYnHx zDdI2F4c2jv-^QZaZlVkLphthhPHLfpPH}f1fEa2-vBKa!8Iit(G)MuvXbloKfcgOj zI}~sql^(4D*MsVP_`v-F^o|{r3l;b|t=th)Sk|Tv(u5C1kJ}a(Jo! z&^}Akax>{^Giey3B*A>2RBZg%V*A&8W`Qa74ZpPSYZTHbqT*3cq~O~9G_NSXSCKR;a$tw<~T)Wi^` zNa`$@3?AsMo$oAKj79D6&ScY_eW!;Jdv>F7Er`2H z^tcL^K|`HaKxh0=xhl}HFzAJ4NV*b?*THJnpoUbaP-XCYJi1>CDpjEj;#}j%;ATFk zh%Jyied5M@C;zz(Q%L!&-!3OD&Uj)i2H7p(jzpsF1 zD&qfp0PT~nYn(4{dZbEz~V7JdDnjDBV#P&2)({vjcOJkryZ|5rCN>u zk2zQQd@8Q`#k-K7bse(TTjWJ|l=S~+KXR1dFcxI-A+*gIRiT>vWeJ?AMljHDGabxN z4b4?T+jS7QA$;dBDvkr@2oRwd)O`hHB~*=g6n9Rrggc{xUM@His@Pwn0_9>Lc7lye z3}jX2+9EnQ`4)7Y5*(;P?qh+NYIHi?;Ltl5;=}#)VNlDG=!OFRA`}(~7z|YWv7Lih z$Lc+;0%ehBi&W4-z#mz({Entuu+<2E0EnmnpA^7iX=m8GD_9-74%V2a_i3_5Q1uV- zyIKFA^4zU}w3D57?2!yA4c2qe5zEOV9Mo1VOry~M=o&C7X&OF$eT_N)q?swwF)+Rs|A9mi~KJ+VK_h^O{bc5bxP!9^;ZGtLR9+3B!6okXp zalmpF%48!bjfT9!hxxETc^6UpHh>yg=s-I7BpFdIKm@8kAdVpJ=@8u5{4v&&9ZPQ# zpW(V9)+^FFb+C1Gy-$MJtweXS^rwbb$=HUOIg8^0aQG68RbjA}2F_6+s2O&dZhtG7pnwdjpiPO0{g_`4 zC_zztGGg)0#EEyetN(j#9Fl)5nI2QgoXu_oNKY|bZ>v!K?87TVG{aGv2+-<6x{!NA zL8@y&r+aPClsvgNaf|3&HdKhMco^GsPBJF5N(zrT>bU`T_}|)z%EumAckJJk+r>6{ z=8V=>9m~6w>Y0BbKLPKb)ZCvwi4Kghzu(j^oH*t*Kk=68*&J&cxcw!{0C zH=pI+keUAYa;DKedGr4|>|i(H)jo~34I{#K7{xU@U!@#yl3`7E>>pnehMFO$mt_y5 zQTx)&sFSDef^h9+m9|&jXgFi1*JIt*^xqChJ=5ss$I!r?`Z{MCR{3SPM)&8I8Ct|8 z4}W^l2FGP&9-H|z0&S?&`N?AwZ0E~}4h~KAUZ<#NoW)8z`S@<)t@Xz|{`cfTtqxJ<$H}MzTnbwu`OcNCEawy zs_U3qE1M&sUQu>ObNizle$_J`ojOnTN4uOK_lj}rJbS7>T8|H{3DuUob=w#eSk@R< zQf5awQeI;hpIj`AB&OfH_rffEG$NwHRsyVl;A%LZCE>&7}vQep1Bf5V6G{T?AgSdvWq{F+An3vivxU; z(-q&637fg!P7}F*C%gTw`7fy0#3`92gfps%qYCy@dPo`ZkC}0}%^$oFh@Do&G+Vo< zt4qmJ)a0QYBz?v8ff=-y7}6e!Se_vH)&{+4>y+0N{4I+jH*x2U*AuS!+m?~HcAX+w zs5vlHlw%b0a40u>ea*M;WRW4&pA-fSOJ;`=g5&8giRcqfr=c4T22A#4mwXJZ3T~kt zG0c0a%5Jp1M|+87k z+oh^%4RnlLJY!ddQ+F|&!w#xf77n|@`%V!vAlDqt9Ty${GVnTBCNWu8x-9Z`yhaSs zSP9Yn5#(Pib7JnSel@)7_jLwqKi|3XxxY3Xvadb1u_Pe>^rpMdA*6_qe=m@@hbZB? z-o(=4lq3HUmK!p@|AhCaZwhew$NNYtv^l?NT}N|;Y_mOSpf=$pvDb?UBQ^38+|IM(u-O>p5Xzh@0s^o?Ss;@7@ zsRFWbqHe_56)SbLrNB0+Bv@j0Wms}6(o-|yOsGMmkAvJw%EPQPmd@noiZDFY2K}lj zjQC*=y-pKq-LI9v9x_*m`)0?Z$k$*g-tCliR*}M(BHelD7Abvi`GTBQDWs)h&3>+ry%UeW7dmH-WGs1{iwvG za|c~skjyaHt}ny2P@q;b0PFfr&*Tidby=)n7*J63$bam$cj`yVAzV-Ob6aW9r2=3zwbY>3RNU`VV-`a!xV__;p)(y~ zc2-cCz?yBSas*>pt(6(JU}Wp_FdUImWimrQ5>p)Nc90|AoXaq&T^^+tstwW?MnD3c z7@AsyL@p}f;%$r!uU637rZ%H3u286dLb)?WWTu%JC(NlUQWiPTP0OMcFD(b0nmOxt zri5wLlC4i$%o!vvfeAy)ktA0T)rAeTr=?#Yz1wo3g+6Zdl?S)waVLhpx3CQbuWDe12`bF z8MVQX$!lr?GGejSFev3jPHZ;vMu`vATH2l-^!>6og5~zxfr`R=3xtS~kFm8)BPJV# z2o%6;o15x5dPpTTx8&CDh?IF(*Ho_kOh&o!$5`xFcxh0$+uH*h*FF5=!2N>wb#hll z|IOTmbxr|clV`$!e}3w$v$i4ZqKggeM$plEC!&D^F&|%9f-$voh7H7UyFWP6)jmwK ztvtz}9j_KL*i=LcJfz@@5?$HG#Y!S4JqWD^(VP)vRU|AIZH0L6D>AvQ8aEp!%fjBx zn(8$Mr&@+QEfQq7#Zin{T_a;FMTXev5ECVBb}ib2u%kq5=h|nmaad$Ds(`@QUX_^` zDpjKg7ug6aBgs!q^0Xm(Gzu~w&csftB*Z2RBH5N}I<@HP5sn?)*%4-(@@VYO&kCq# zlL+G_Z*gLsF-Xq^A*IVAr=Fp*wToP1Ha&z|-6zEr=Mqa}CY?WV5QjV{h^5ZP7~A(c$Q<@^R|$H(5{$dw1`J&^x@Zmy0g&Y13qwdZh_^Z#Q&imil?);~#AW6(S zvhODCk;P%4(?Do$*tut~JVKomk!u!Q?=&?RvJx;y{Qt@~dTsq-dt>}t-}!xSKi}*P zO|NKW%T64^$^R+gHf|mlnWWFuMVMTz&1KvBuKCZ@JEL)N(~CC)#pj@2#a}8Cw8G)S z?1mRxrYj?y#=Rs>%(w-|eZtt4al&xPq(?M$UoI_#^qV}yvI&cNS@j}qDsmvYq!pb^ z6i410wmjnH5LfrlN+j9g(pGb@X4c<*8MG3jc9^yI*bFh4zcidwcWg>krzrVonhSdHk%dll?GtdL2105oOCf`TFAn)0p1!Qw?e~0o$TpneJr0ifU-HDK4`9t*53%})-S(V zq(|Z%aBCMx*0WRn$4Rii(oIc5$Ue=uM^~D2;IM;F>r$dRUpYBNAjr81hTWReZ2z*^ zR&^b@qQdzkfVv{fSu<2e#~xY!)pwvfWEMFH?+j_~zCo*aeBWA~P&L~vjpSThf4k1R zxgmR*snJSPW+3i=TbcY_$K_Q7J^3|TsoMS8u_)t8A_Gdjw2m@cQ~q}i&!iR{tFHY! zOmXF6UhX8I!jf9nQV=+~Io4!*xrCN2c`(HcN~kkTT9b5Ez0R{@=T0j>3I5@0$GIto zLxHe#M{qQ)(TWa1IRZl!LOtb3u92+n5(m=71?s9SCD_*y>;Mpxb)d~l620%0zAW@q zK#wRS=2{Vs4LOm6n9p`_z3)^s+&qkGNC6ISTeuYDwCc^YXD>N)?Z@(<0KFsFZ3F(* zTez~ur@l+7B_LA+-K+(A z@nO-9q7aUhOawQb60cGU-Dy(Ly_hBebLe)YMiG7DO0yH*DX(aXe{)T$aAiD0Aqmm~ zLut`V0nk6oq;X4d!yobo%b{VJA<8;gNVhc25r(LhcJW|by5u*CxN4z`%c%I$8uFh+ z?5~Cl@ZqsKP?%2iG8TIzvWpc^pQP)mc9x~VW%2m&ueP!;-D>9rBugjGnu*F?hA&ZK zj_@ma970K(dVl{^KoKe_7Ihcjbj1JWg{5+QQ4|AhiVLhD#^JhuO0Pg#jNjwbKC-oZ z(Wd&2A2#9n(YH09{qse!TP|PsxhV3{B)Yu64m~Rg`8Q*s;{>l0aWLfAg^!q;RtHUA28&2M!awyTddz znfj-QTxQ8>vE;p}skI8AWyR)L>J6F|%cF7hEtdCp++^m?*2VEcn%kR=N>RAFej`Z6 zB92T*xfoO;WzUGY)6gWIWD^_8P>cQeknhhV`pG?p3LhhvtnQJ9%|LcPm8`~qW0oC- z7FP%qZ@!=oAp&9Hl#aWlXo;dx54Y?%7-_SWk?7N#doRAmbWPi%)>Q!cuv&><6 z19eg4rq}6sLZ)NQ=PLj!e@W3=u{&F+NdcD6h~1Sy=OxL+8Ke4S7{gA+Z|8!_xI?=t0Esn0t?M?BEyv&0AdN&no~@g5n%z2= zSp8PFFMqPmS6%bt-d=aw@i<8+?Y_ZT+DX$pN4BO+|BUtck2@XI3JT{n`zE~JB8W}Rz?*i|WMLLSIO4`#jqQEApt61bom!&N0 zamB#ECXv6Rm>}1G#AgdzAQ%nEkoRb7Z@*jV-J`|m-?lB@8bTXg206oNjVJ8Uk6vjB z+kQED+tUhHn~6xm@Wj{Nx!Ur6D_L~C%Ow66z4t2f>Gt=JPrffY79dEqlIMvX9H#?k zM6o)^nJplT4N#cVW~#i5K$K>wMQJoJ6%Mgl1_rBF)+YM{9}jl3yCBgQ<+FVdF0a*X z8R)MB#x6nDv-RUWk`yg;;AB&RN|?%n8Iz#Y>{zq@r8=T}PK_%m!D!9+65*=zmYu3+bctrZPhhj*WP zka}OgiAg@Js=a;1d)s@rSD!3iyifV~#QpIzTEq?a4M*MR18XUw$POW$Es85?Cjh)c z>7tj-z*x@fG(H%){D&zWVn>w4s#XAx^BTgsASes*UsnYtOOU8#;o%~&XB*I5DNZ2! zA>p8Gf!I{P*vto~`wPea2Tq2|HY@`d?g7zG!1ijfi5AFuCB;D`19I3p-skXtByKEO zq(3lTon&q`bJ&W68BOvEKY?&)&Utf7d3;CC3|f60JKQHg0n(UOebE)n>PA=cm0M z)=!7W-XB`^NpSdD;}2+zV#n@&$a>jdVauVbbs#!G61pM{pMhrB%hDq;l*MF!Z=Y(u z6%9e&h=Ii{$63K8PuHwSQiCDuOht5m=ufV&wrD?AMRKMG1fXFBDrg)q+{FrfrLM|^Z3tyKKkQ8?d=1Sh-s(!_@Y&J98j0f@6B^8d)FEwX2WHghUl{TuC8?_Y)};$*bD*g zuP3+@C3iO=9{&Yi`8C)Hu^c1&8RT^K_WHw-S6UL~16yVjw1{PwF#h4PU9OEe3A)L# zyOu|`O8yN0aRj;RV9S3!{sR8aJn|JG?7S+bi%%Jo8LeVgIu=p&Tk{^=+g(LAH>wFP z4L!DA32m#d$A2>4v-5>V?seWaZg;KSox02UkIr^iyrgXm*fw#c&2%sD;&`W956#1p*b*jp33s%qa-o&?2VL4|aPliA0y_~T7 zRJzBTq5Ipf1>E|xZ7j;}e$>qeT(nj0Mo(CiHemJt7+Y&M^j><^b_N?sNvVa~6gx{7 z_x)w)Y^&T)P4O6;KA zyZ`yUIW(BiLJ*|QX4tfOyV<|X$h&0MbFORiX=)zmt^I2;?dj8}XSm(?TkN;5&n#Yx z9=uWOHazo5x--kq-~(|k{OC&WgQTRa!Dezu1$gd5!Y$|m5KrI^wGen}apH3oTc>#e{qbk5m!X1vgj z-ODjIK6#~&`nLA}Ie&uEa^VzUY}>iS3HeZtxZ@WIH|6}Z|zoP2iHa<+yGYmbH3_bJ!LxWDx-Q5DBgrp*(4lr~h3JTJtgrt-p zogyLKDWJrli1>)|^8F9q>*ur9S?8>M_TFdT_w~7cY;xRvj30NbE+1lVerNPa(Q+-k z|IGUN{)d0nPy2&DH#zS-Sm=|{fAuLO;JfN=12Hc-+%>jARUtJN(xV*2^PP7<&D>L+ zLu17s=RA_P>;JH^(*xxr52PY!9b8&#trB#S%Dv;h&KbnrlGBv`83+Gk1*R zSnm&E$zSdK!_yXxhl&fj64~Hl^WOtuBCY!VcmhYhgRqSi1rcP&pwmnJT|_cxe=~1q&7Y8Euz^bwtCG+CU}XMz zeTfZLG12iv6bE1U85O?#G*MnfdG*V>+|%jSa`i|H3uCYx8?EGi{8*%!;H{w#FE$`D zS+16~db(NN$YKlK?bRjvHms5?4@<0tdYa-Q>{X9eAH+aOwK{Th> zN)TR1ol@4MJJEOeh;CeTJ=lFw(t8tA{+WfQU1v&uZ<60fgnPsI)9u2TYS<0(6(#wF z9qQuzB=WtoR<}lQopY;ZX1yuYA#b~HmHtiu#%;!pIw;$D)-!!V*3dK4hKn8=R`Al9 z+45R5!;$9STpX|4h{`>%w{Q|+(+l5Zc(q50k^kswc!BH9Uq{DEpHMN!o-=Wu)e5Ub zxUt`|>F1lR6~-QHGT-kRkSse%4x*1|$yXW@?bL`-4X3%}r}{mlC;Xi!oUf3zDXt~4 zKvUEyqRBAMojK>qZ!h6WBl%>3$z@4H*ueCQAg8yhfQ(!t(T;KSK4$H? zEfW#1f@?X@xUQ}x1Btl(&#{T~ff$oE{kcdbeJR9zO1isK#3`^n?LvHRdwvHkLd zlVV$)@1g#B0t1d9=176(fd7qDi zQKDtl&N^bvMtE-mB4<)aXI&7<(dH5Sm(zq#$NH1z%i>(Ql*Ab`V`Td(Dz0WYto;KD zay`0nC@95WTXb8<=u;xgOWkARE=)>n8V$2I7(&kk@31A2GzpVkl^bDz77&_u4lZ-iCUp^{idaKCQSz`Iy!XdGVz%9I~Tqlw{JGplPoR zkz@%fsynCO4*Z>JMZD3Uv?MB7rqnMkc6kndpyj@4Oc&|aNrU(K#R@XkclX*jUw(NZ zR96ti_HhDI7{s|p$d{NZEeNae06jR19#{@hJQv2UR4w4Ok1_g zkRxWfMpqfq?=grc7o3xs=4RPE5rqutwWM*KkAslslH3IT3jA94x+KDBQxmlBy82b< znmcM$(!xo68OR{K${=-Z>nG1PBj47u$itn;O-2G*qVQ016*}Uw_OplQ3Q+NBbu%FE zitBSEzwn^d*v~R@SUE5DeV;fsX7rMR_dGs-NHl-611K|}Yt{ErtS{lL-}MwS6XnnQ zBZOC`%HbrQ*WDs-C>tng6DcT|`5^A&2t|=~wnO(Yo^#oGf(PvJ;^kw@!Am~J&rlbD z)l+6v?4{89(Wy~8b&rB&qW%@Cvdh>QGwWQ<{%x7cLNj}UIwuTkYLuCQF^EK+_t5pM zqnU25(cF|aDU?x?x8(3Q(XcHU%3`royaKHN>sgN^mLP-g<*DdVxFW~Tm)~9#ak3IlFrBsp8+ty+&QG(3>xyoE5*?wL9iGT9fzLfiy9_mrUSI6U zP7Pz6x;$kJC>+-ojs6o(D-;LBTrZYJZZSmS>G`*8%(hm4M#`cE6z>VhrrcD<-@JNq z15flVvLIovq&Lhr9_-*y)zuY0U+ z)mh@@TD}Nl^ENLxM&_=9B~9@{dy(TS`4c=O11Gm~A<}0Y52q>kO^6$XN(3q}9tGSv ztV+yF2Xj(SrgTvyX5+)UVAPd0 zA@FNVbWX=lyn+`V zsDxLTsU{FNj2iMnFFT_h0n%%v?EG@q#8CgjGC7rx)&m6E48t((0Ae>4jODpyUmBq z^SI2o7MdA}!4kmeND(AJNQYo|fnpLa)S3dKlp>wCd%oc)WtMw*=>Y&R5kkbHw*hQJ zK%N$Nj_G_{E=9<}+Elz9M)PTqyPE;0Mp;6_6By_H^g@M&cFkTkC+S+FDn1<$;7;05BJN1n>s`>s(rzZNM5hYJMUUu358|eM-@D>*YPuQ{ zGFFNk^n>Ef)w_qeM~DvL;m@6<8hZq( zfq)?_&jE!PCaWLHIp*3aa7_`#-VjUEjqPHMI?S(x=W2|QZH$(M#|VdH_DM3PpmpOXfR>H$pAX(zhl?lC z53!ggum`Z%7q%&)d7QY9(p=o*w4P0G6K1k^oPh%ZM^?qml^lxaYIPT*ec&}hd zPt2-)$KP(KT92$BN@99RcA3ad?+YYW2nb@q4yY7*9U*&oNrA}1$DC7bA0r4mso%PhM@tgqelC0O^}oI+tZ!(tprp+k(!36BsE>=wP+fV{U%a`+}}lbN_@S)Eoug3q%2 zO!zWHYuzOsFQNGe<=ls$W9+}`Mdm*LPHCB@C*dF;fE#(;b&MJV*Thv@S_qEB``Vwv zrTOVM{7a;ey>*X(Advu(W#TmuNE9w3(bZGWcqkCu#S`q$Mcs8ZYWTS#0C7D+h%PP- zf5>tde`7bVK@XiYkXaDRxP}$?!qcYpK$nU9{uB&@SgP=Deuo1_ZH{((cld}&| z7A;yJguaMSE!%dxr1rllydhZ;$!SC6U+&_u0q`Sm)a$E)r#<{ESbA;(%QAqk08eXy z7ur3XR5$=dj|Z=J zix=QP*?94*Fz=5M$oDQ5qI!Mbtc z$%)^HP(czi`5aMjo|4}(MWj&m=fn_B5yXb7ylzEvJ9*~c?ARa*NvNE1>}UVQzm7Qg z{CAbhn|9Z|DKNb?$DN_yi=>`@Xp|`r##f%=t?f#27?09BZ>@sg){6l(KPzs1Wi9De zY(9N0HdfKm$XtSpyXjU{PvAh_3YIVrrv6V>eT)?yyOXD&qjOgz>z7I%FuIYxvSJN} z%kmK5)smB-wm*xJQL?Av&gSUJNHPN*mP!pMN|qPp zJ`o7^r+V1U8`=$>UJ_cyQc+`palrJ9NHGKsvR&%*qz77p2NTGk-blyZNJhaf|I5@r z&~tHgR}bS>PfK}Ziz=Er@iwglnUOubLlfsF#+#U(iwW&fmleF)tAV$>;W|Cgc?xE9 z_r~oWC<*{Tc3+w29P{zC61Y4bCl*q~Vi^`eQbZY4NT>Z0)wxYg-y_BnH4BTNtdLkn?j$Vx@YSKf62BNYbt%U^2DG|_p!lB&uh;=LlOBXjw6sJqKSeKtt z4h0uUE|Z{K*5A!Rq{(>F{dXZKBoLRDuaqhf^kz77X8^~o78FlM!rgc**IYB%FIHOG zttIkXN?T6xQdZi&<{7)mt}_K!U&Lk#1y`nAHgcDv1cw$kjEg$oz7!kWt7(}~Z(t_2 zo~Yn7pR}?qcLtiwPGP0Es2lX2o~k-il;WPSV=qq$PfG1uVt#xr-H}UF{83t@tWRhy zyPx=4o$5N*f3k#Dj%*w^%a8wZB9c_);u^l@aQ*ju4b!e8{qtq+&o&lp0p2nZ*=_zj zfswmR|0Tw=E@8juXXUJ@V}HW%RFh1xlxKa0{8qG&O_eiThV9Q!{kf56y$%E6w%%^l z&dk<+)0$*<*(6EteHBac%&;?Kh?RvEqljbC%US$CKc6dTsb2mg%UC=a0YHlfh7lwRThtVXAIX%LA94RcvLQEa{+b;%L(XCxN5emqHD6tO# zynmyA?pn>bG^SC}JsvFM)z zugqHANXk1EEY2P&OPN<7cnGI?e;`K}%!Ez3AX8_)@GVF?8%@l>iP>(m=-B{v*i6v5 zm;#>JQLe(jMT`7}P-+0*Z!iP^H~~liu2cq0qKE|o zfs~Xqv|tb&4TSDW^vqBuS|}4Uj7^FOO3wsiVrGOgGc&VMg4v;L>}(teX#);cYA#l0 zE)G_%D{%{P@x!@<&;mT10z4uDdOm_&Y=Q!ULIT_(iY_P+7$t&2Nhpb8lth)S#l*G5 zl(fVJc*KSH#l^)WHLa!O)TJ;uG?ZFKT1G}5FDuR@E2kzWCL|{zhQY{TjC|#lH5KH< z73B34)bte9%#|cWm264Mx*jU(`YHr%RapsD3_(>%QB_q#6=$S|S5ecpQP(iXX<6dc z-SOJCcwH+3MuVUrLBOjM^zAiNFdAwG8X5!*Lsv~zMJ-(~Z6$hbEgc=(P+chnT?H*& zbyHms0tzI{LLc0}48Pun~9 zLcQHl-XZBehL%3w?mkxv3ik;n`T7R?26_9rv-<@H-?jD$2s8}z^$zq43Ji$~3X2Z* z77O;a2@duR{-45v9|Q-7Cx&<{g?M{}1lfgzh2Qhhx))b`-zW6`y<4G=GD4%X9|ZYc zT@@A_78Vv078&y}EaYL>!-o+m;gJu*qcS35G9wdSM1|guihmXza5p9@G&UhS?zVOO zgV^}QwDhc!oSfWTMz*rhq&IOds^3)9Slp<2Ue?&y*wWnG-rnBX)qACZp`nqHk?~Ix zGv%*mW@hH+=a&|jwl-IGc6Rpm_6`pZ$lp)S&VK&=`}coi`~NNKOTZQlw@z(AcN8_V zn8Q$QVQ(z;T1Y<8%$p^f4F3LL zbkJ8H+lDV=iR=QN+;I)Arcpfi#T-W(MSC(#gjOZ?8^7X$1qzFz^k&}7y>;nL;L$%5 z8ZJ4RcDn)-V0ViB0-EAtC$omU)EGqrjd;zV2I>yyIp$P2PKx`NrdU zYWTDL;%?*P&VRdYy-^HQDQk~s{@yzjLE@)H1)X%3ewQN&m)i_!HsC9 zp3mzstoQcPAFu3`^FF3gVR0Y=VS9x}qGc6p_g8p^HWM%dpFbrleTfrLRlGM3OTF>E z0;a0Lrt6-p6DG9m*8?3CysO)`T|zX*4?8AFKSS(b?fz@N>f?X6^(p(H#k?%_up+4V zrAp1hr{~$W4?Gg}-g$}SiZ49awQ=0s&r8An)A4+9Q1E)o;5PqaalR2>7I$FEzcc&1 zN3wVJt)Dr)%u!Va>Xy8?nX)gCbElrhtMFw_zk`~c3aw~hd+DW^cbQ?jfcQP7z_#KS zr0-r8ul?%xzf~&>%}oT1DG%}lf@Ji4GI!!Mzcr-nw4|th%j6Jyd)F%a=4;=7^O8+2 z3tt@?fkNd{C0Es;qvv~_`ZYC*WjDuEHbNT=mE7O#c(lYHDb>6;s{RkIOf8Z`E~b76 zb|gTVxihMnpJ>+gr9K)>r2JOf1q4DunO!l(WgHSI+>!}0I*ii6b-k(er{s|!(~H1j zjP=7})h`-pzRH~MA_i0yFB1i7dtffd?(2fxm54f)P3zh76bwQU`?ABTPInEMd+4O8>s;?MWy)j$6MG;f?nZz_S~wi@>c6f@6WOH=wky+ZB|Hn$ zO>rZ7le>)cg9be}|K_OZd4~bae2uwJG|oo2=Si@J)H9wCKJy4iQ}`&J0HGK$b$q zL*GfUk(9}pECHgKDq%9=1J6;0(ZytDc}vq5bKQ+Qj~#_C7TI=zU(OXlkymjleEnDJ@#UrYs|mN^=J%KnuH@YxaV%6s06=XA)KR`8wBOg|=#fY5E;Z35TW^PBe@VcO6p# z2&urC|2G-2I88uFiW1oWgWe@u6mqS+)(+8|E~1pLz^~jkOHZ(Pb?s-x+FqKuql$U# zb4-az^qvL#e-@SZLW=bkz8a*`G8Q;lzIN$&ZD&m~Eh=h!edEy;zL$2UIYqwQCghvR z&P(euAZrrypUzI=&qGg|AH(9@euHem#2oxYyhH=Mnpd?Ptf)%Y`gwZPs0|NFCybhlK|! zN-Es(SAn($8?^RHJzraDV-!Dbz=BE$*I;#V(jQak#5Mcitz-lKszxdT*rly#uFD08 zCU&$8D6QHiHwkYcc83OArvlQJ`b5JxYz=gb0y7e;+=Ufw2eD4|*pHn6Ox zsY%KFb2~=G9Y{dSn<@OhW%k^Jv zZ%d8>rA*f@;a=@^%H)~iJl-m((zoCJ$05xF@_)9oylp@KH(Qd@RJ$_g{f{wcZfI4$ z*8A@rGpaukg(>eE%ME?0ymbhat!GU+MTdUgT>PuBU+>1RPGxfbEbkdLx6O?m-E04| zLQYY9UwO!TPrQGXfl;yJSI5(kA4}?7dd=pi(2V;-s>>(D*jd;oa*XQ520XQ;uU#@N z;PRmwg?6(wz28Zu|NABGOQoLXYI3RJ#Ww2FxOK$(=x$`!O`eI)(BK+h@=In15!zpd zB#cE+x_IBR!ml%|!n6x1CwE83xNR!y`dy!P0{0y9#mCZOGG%=37oJ#}h#=FrVDiP6 z1-AP`Z()VLp#mExEhd_}*A&Xy{~r3^Z^tffB;T=!8aE%Le^>o4r0|&373$KXJmeSh zpD=iP@7{!RW*w9(y%+}tP2#!Sp|^AMJaZU2$+6CUjSX9zGjUmaL4>utN>QE1 zX~r1LG|Wp$Rs@`)!Ch4s#pH1Fm`P9PlKJHXb%+g$b9;{xeld{}0^gupQw)9iu8=;o z$KKl}(hCK{=GKWVGR(k9x)XmWW18|UD(DFjz&R8# z)&<*j0m6yQOl~lVE}kPe4B$^qHbLd%J*W1zAapA$H{<@I5T(gbe&lhAFJaOCV|CwMHs9 zm*_Nz?j$Wc0K-%dg~A`wvO`b+hOcmn>9l*F)&YkAhHZa9_#Se}g@!v8mNyZq=mwox zM^3@%kH`;oQSs9@$tPOyeG}*$`sRysCUw?W6(9qfP_m_~_^r9niaj`ZmO8*+@&pcL z1tgvdCA$6ySSM4PHbFxtln>ERpNP9KT=Kd?07x{TB?3x445e;OqkEmUoCdu+^>_mg z`-bLF*pPN@xR*C2u!UlK(Hr{(&agiZC&ogK5Y((h*mLbvx>(;GEY02?jcEz2o}4Pl zo|bWGllCma*26ZbVV2q{`ivP!4u)G) zjNkz}=md?g5@28+=0T)qtdFtV;*z$8TAARF*Xi@tL+0RA3a|ZChDjm6{qNJ>Cos#>dJLz~%ZQS%}TmX)e9inw4)MbcOir8qPVrmJ$w# z(IPT`LjrfnRUB?|cG_x+U;6&E+>6!%LZTlNbKG{=ycb(gE4rJ87_q%U$Hx z35C6a9Jhd+ukjD8WAoAqVOd1zS0b!Cp0)cnY`K1+Y;dqLyQ-}D(@vT| zKVqRX{sq%mIy2jl@!O#^v#+SSgSZug*_a;d|3H=x-hXDx23UXtho4gF7KQ0v`{GZN zb>(?(rJnbvd5X-8dH{>&U;w}wIMA@8TCP1bYuQuSE#{KV0@&IY*6Lq{Z?!`|=@xFg z`LmYaHMqlSY8S9h$!VTYs2osgAy&kfz;WQByak7CrE#D3DvX*IE7Agx0N7zIN3E$! z8}L$r7#($f;)^@nf$Lv*wIA3f*2gO4n2hDy)ji$6fMmD$n5F%RR96YxF} zH3idpJG3SN!8O3dnv&_aAk|ugdu^G;+xFm^7Seqx8H4B3Z-3sYWumL245+&ZtL1)J zYfxA#;YvSZ0v#}^7kXGQ;zB=JTmK#jo2jMSUx(u0vOBfV^-FT-b3OsPC^silxAgr6 zQ7c!C%}mBJc`dxF@dH8c?gp5vYx;t#@u;h*svGYa+~S)n?qL(&zEO3ff$2G$IG-My zrRi{~!BQ=BhYZ`qLifs;M@TqOcE<#ntwX6(*|RhhVEt!`Gomk9mhsti{i zNIl!D?&e384K#e5DStStPg)XkTHeZV#+{VGa4lJX*wel>(-*du6tdQs^4zOr*;J`P zGV=dPhy4zK&FjJD#h^>@GE)GcK(XxkBu^Qk?Yz7F;X_#F29I!Lb4gA^&nP_Xq3A2q z)`3wrk&I@(srF&j_Y^(tllnXl5<5hSI!d_st15Y`E|>T}FTvlpcOZK@zC7e%Fzw_o zYLfvXC%O0yaBULZTtXD@yAt__O-)(0q2JKa1tPHXZ>_H8oyy+tlPX!xAGX0a_;+%; zj@@{QT$}B?+2$U0EYdMf<#Zm~cW}M$nE2K`#r0vGtMgb~WLv)n71_bF+_O~KEq2kt zbMd~*w3pwlEzz4Jy}M0Kt-D($pJt0|iwymi78gMG9uW~wCqr8@(Px-MYqCsR4(P8a zpsiT%GpVArTBfb`?>D;WcRuJZK@OCl`^(9F&rAlalW1>+(*gr%s|#rTt7w6f{XX;q zH!u3D$$cjD13p!>#<_#WAN#BgXezKYCBz}a%cP;Cq`{Q;L%!U@hRgj%2E#V=1JA1l z-6Y^|;DZ4^u_xrA2k!@s!f9)+7*16K5lOVgwF3?p{nR)*Lj$^e13GF`+FK684|7Ky z3M%1?pS|S ze@OxDGWXz6c>m_b@G$-0lkk2A`cEY;bTwF-O#hLJq>)pfkzYyUrw6n(>vTWEM>#>UI(Cu)!rXW_K}!Y9hYY2TzxfL6wzt<(Jrr)90404I-7tW0vhnq+!44saar z1WZ8wPMkVS01dB-ViV$y6Xgz*e|$zbFUO{+I;I#7Cq=6#=#xLwCr_!bOt3%tEaXT- z_jgimWkTxjBs6&v_Gr@J)r7VoO=TC=<8|s3@@EFa&n%8p4r5c?$s^DFsjlEj?v9kE zXb|fJg|{OGiwOnW#4HPXjvGG5N~G`|n|)wN!F>f`?3xXO&igr1u%=Ov9?i!+n&+OF zN<_aFo zA;^melf_EM#b-Q=5!G|)udeO_ZyTGfmz?J&FR~CRdL@_hd@1r*W=XG>(pF|4JzB^e zo6jDbBPP#f^UP0IFJG-icMs?4FAXWEaPx@u1s>vZ_}|&W!`ZM$3#pG*?;g(Pbj+`e z%_XVNEq1K>B+s2cn)@NSR@t#qbU25c0P;-$(f;dbG>A5Do&GwArgNRrZ=LGe1{%2` ziQHgLS*N}RgdMH3!a#iFD6!wzd4W#GE$NQnqCCw$LeC-2b-h zjdqa!6e#o-IB%Q%`WD)6gT-jp;gJHW0 z6TAFGVARnr9}y^dxxSl_vKN=P=kK%^{%<$&Xg4JV7;%LPt=WB%vKw5p|M2ndqrBa4 zr~Ri+U&|hUeQNa8e--%RXfMnUl>To&4E7a8gr|SnZ}MY(2m^8xcX^24avy(d|F_>K zbx?Hup!e}X#{(1cI9u>u5P~O?((a(m* z2Op32yI}`oPQOR;_5(ZjotzIQ^VTJ?M_BZKl)T3~oxn9I;Bn0nqx9+ewWAu|KXaeR z6}%_N;}dQMGS%H<{rn$1yg&`-(|<=m+7Ex=soxpy9xFSa>gHdFV=p-Kk0~w>k(Xrn zlM99?e|Ile$xCm4@V-476hhdLCAQ%uVCySOySTBJVkvk8cr3J zCLYYotm-QMgHhAjiD5$>FX+06tO%t$PowoVzK9q+K`f|}aeRH_f2$ze}W!;KT z61IzxxFhRnkSp~eMacfl+q8zW>HBha21vWbKbdl7;m2*0T!BaLSXHdO;^0`$9{)nO ze%Ia@YZjJn#K29)RE8e3lCtd1@C#mclEgwi?h`~aHN^ql=fzqskgwp2?(2qEYQASP zZM%1QYq44L;}A6JUBXQ2Lr|>YlH!+vWanK8{~o8(vUIhd^;O`$)5^8)J8dnkAWKBfzCW8P>X-Hg) zJ7`*F@Jy#Ag2V~{Ni(lgTK z@kim&>BDz2A*%JCb_%?wOhkY992qk5f5#*XOY~HBKlc&bub}&-(*5RA3eY`6=W{VW z0Mp6%`QfSG*q&sLp209Bi~D?lsIxf}TQrBe7jD5RwmQDQB1P!=YsOMveLF^9F}g(# z4a_%t|1p)5KDZetqfJl)E2E7^*4Vox5BohfN9O#WO-|uA?oilpeU)$Sd;A%4)<s9b@mZK?KGso9Zw7O)WU`pj5(~k5p_7cR8 zLA~rA;PhTj_?Fmq#yMx!8~AhP5`Ga@3B!8*vAP`i^7b1lt?p0u%}nJ1Hhc2w4x;_C z^C7!?%EFGnhff_BM^C)z^79YR__#UTP$Ff4AAJMD*@td#aC40Dec%or#3^4dVj)6bn8?bYXY8Xp|* zcRu^w|2*rTFgJNzIX^sPg2wu4=W!_YD&UITxOQ~HC!HVI(;@AAaV}zfNp>v4Qczwc zM^eZnz6R*f1|Zzxo@c-0LEmH-*KwuZgvSE>iD1TpUiv@;bq_Iu_P}-!Gjxf27M&JF z!_7D8a1WdEYQj?plSFIn)l(iVd2h z|B#;p?#BGLx6dj>FpkG`X|co$a(X+3tsxsC8G}odjHyd>5xmJUrZXTl3rxP{E`YFi z)v}%dK5P@e$-PvOpxBwtcF$*v<2SXI{d8SY1m-4^Mpvs7TAv!IR&uz#r(>saobDkQ z!#xu>q*JiF}QmQNacbJq%uv*5+ zTG%*h)ES1h-Xy!11qr+{)YY%yBg3n&;}I!Z&%tE=QZUz3zWTw~?aRlyIeOxSw8sKh zy+$Rzz64yd+e{N(i=K~sBtKO_Hbq?oF`kSOVfJ+&uN@A`I}9#|&$Svoi8;)@`GXTO zm8N9RKgD+Qz>P5IrDf?Jn95YThrUxdp!e=1BUxgbLwRvP+AK3ULvTw}RBs9wW09Wf za(62yiFUB>|^&>Eger1y&6qXPlQ=I=Z%s&iD_t|nB-xSgn+rG?Npl|Ps_%aZ8kLT6c#G7ToV+%P2 zC&=O;)K90E-E)GXw)=hok4gF5#7nEnW%DZY2ye=fgox&MKl8sQRjPt6j4x@G-a-sP zvUrZ655gIV3GxU0gB1xef%nprryajxUfU%{2G>U&`0xhN=xP+nK23+nAnaAm`xa$V z1P1mb;!muXG>t=?=pDsFE{3lHlm}vbR?^IWSzBz@XB1rKdZZE9J4AjQQnT83UkX3} z=*Con<`wtDg%>POvM4qys4e&fcoTZ5n@5<<<@(v`vtxX0Tde-A`G^X9k^14&e9ggg zF#Mq>Yo?1we^d1fp?kY>bc>_e`wd0?NnKc~jgPz}To%;VbNxT%)|)krlk4=HM$Byy z7_J{9G5b-uJMSvc{=tL%kxjBBu{heZuMja43VUlLh<&~ZE)54d$<+bhZvIrh|LT+D zR@fXzS4h?Kj&F81xE8u+Ln!fG6z744OJ7O=oS8EED;?=9%v$|y#*><66Ok;jKK&Bs z*?#tYGOG^_6p=DC1GG^mELZw)O>k!u8afL&f4v51XCIS7{zZ*M=7B8)*sQlJpz|{q$@LyFMhbB_H zdfZlk&i38Q)u@X-zSmhgmmp=7zF@Y!5-O<93MhdGR@u!%EYXp0>>Fr(0q9Qq>#%IG zKfF+tB(bfo)$iCI6#d3xSN!W2rA)~(Tn#t8`6@vv(%%s4X1E<-4KoH~wUtycib;*f zAsR6+CkvQ}6c%(1szHSwcJw?Kl9`3LXSQ#czt)?H*A(?gA~bNHS{US7KypLvoUZ{s zqYB+y0QxQ>yGw++5&>h-E0+nzG)7&jyAYI?L8@tB-P$PSB$D#SUY&wodG}t$OpSXvt-Mah)jJ zT8ITAT8|uU?jOBktEg?#{G_49+@@ddO<$-E=z0P8h7O2;iZREkYx6^%6hrh{Ksc}H zupf$MamAZ-0-oV5I{X6e$Y=~TNv}~$PcRzW5_P*o<3^WOr!vG+M^TSVQl$o?!$?*L zZM}6xy`+i+0f?o4^b>+sjCZtlZKP#zu1{{XX#v=Z)Gvdt3Ji+23N60DRNc=XqwjXD zs8`uxsz~Su|I3C~Jfn4&ho6;|M_%_<7Lr%`V2NZ@utLn-K zTm=)+7pMp)yr!rI>s_5kn$k7&4fWieDy-fB(3xmR2UjxB6WRR!a;SB%Q9&=I~J+Y7Jd@AA20JdlW#6RI7VH zr2J7C#NBM`AEf~x$s)#r>A*bokx}1@RO2Go=rqmZqGZva>n)(`=h3FMk;}!=7*GIsc$wy%*dd#BGQh8`sVvoS!(c&33Wv{*v@6l)F6t8DKSTb z73XtrFB{5{ugevXR<`@^)<^+h+Y@tK!G3@Y}1 z^S_9yx8|ILfw~+BapvM>EAT; zQsV{HR9Pe25SGLG1P-WMBipGgXH$=uL{sr8GM*2^m{7PfWj4JxsV)G0b%DUyQ{b5~ zmm_egJ6|yXeVA({BKV{b@{|!`!st)N7Hi~c#nbRl9uJf$Z30h1* zCe(nk#0lGp9=8ILtP;rz3dZVy#Vd^L)CG28pGV(BM4Qe7Wm{%Vv5-rQ30M-NiK&G+ zV`Cg#NGeE5y(>U#0ayr5Qbm&xJ&NUTH)WjXe2`tzf#f}Ce8yLh zXHbJRAE{y1jXlX%Nkmr5oADbiDsnA}Skki>;21GUNa`Ma=EhQqh)&ib_`V;tr8k#g^C(K=y&u7C3?)5B0h1qfpky7 zDit7Oi{hW{xi=A?%tIj|?WreQ#@Q~$flZM0KU(eaG@lKkbd#vg>@=<8sO((URb5;< zamJwMeU2abXpI)`gpzc*Ke(M6V5x2FO(2nfBR^M0nGUJT!NC?-h$|VacD}0U1y=O3 z;YHZos2x+8Z*@JlxiKH@WiVp7AXAcZ#Q=msf0DgZpX_>@7h6T!+QJF24>Xq?T*RPt@@P;g-^*{I9jMN*_>nbG0Q9v`5 z-K#Ad_(Ld!qUCnmFbo=I^qfe? z1E7op-YVd}3h=n$5AKX#t(uH>j*HcHnF&pajQRs{tc^?*i_s=WI4gmL3P|36A=o&e zVr?`*C(7GWQM1BE&&Ak!9i*+4+bILSRBLfwdO)HKfyDn&w6l@EEfnn+eJk`aDcfd2 zFBf9wGJdS~DHLJsebI4l(X35vat56=wQj1X0 zr-iz4U1tbRryN7tlnZ=a>|M|Er?R+MfZ+C;*)a06$)f3JQ>3FEXOqKPoBhxul^7yI zC1@DhSWs-1Bx2}MVnP4O1Bqf8Hj$!81?!@X%Ene4o>euB*~pDQT3iT2h|9mcZY71S zao4!Y+9l8%OX0@;VV=?RU2NL5GRgxbH=~g+`NoZRC(3pTE&oWDxEg zVr*jj{q$<$1TL3EYa>B^nx)#*q@V3K%!s>#q911ZW_Khs4oW1=)_A?8idrg2o3 z>!7nY0oJvVO|Vy56UJv9GngT~9-8DhREU9tcbGflUTPe~QyC*ZoydVL$Ryb=(AB4O zV=q#w0{m%%dlpA_% z8p)0ExWlU!-~8kiSTRjIkW8~!8j?y<(oCjz*sh1VcU5mANk3x{Td5LTrqXAQygy}( z&gf5hM^F*clii%u_z#X|BhVeYxkUs^gXdbvMooz8t%lUx5pm`-xO)S=1BT_4H^4Z_ zC)LiK>epo*L**o=`cKb4WyDbO7tT7O(?5^HvlWY45SK}NsQqF=eZD4X!C&x^7Vfk; z_6t@3phSU2H;QH%W2l#S<{4vdo=3a7aK~71Pb@}L3;Z|pI%p4P`soG4t$=bkBG>*W z$TO~3D&zVq%{ob8LGo2WT$NI~h;oT0-)>0z^dxLnw75TB$i67|`@W2zx8PWPh2f3e zr3cZYzoTtUR6g7dyUZ)SNnN$F@qY}RX*^W@8^+Jf2&iVa5_qp%u`d$^i z=h{;8Rs4a6!d5kd`5#R3|0Ej`rBzf5ytE3dqKywRrM7(*q?~K-x8VXxM)6y6>{G5+ zvaa3>^X{E($o9$b|6!2YZJ)_~rg#cEduo^SKiuQ%Ct1Kxvz<=#=Q+e8_av$Y3!udi z+S6VJ;;v3BvB}px^9-6&py1}bHd;weLpfClSF3GG8=N#gBV2C$!M(|=C3r+RBvWd< z>V6TfJ@A9ZSZ!~l$$QUC&D*|16_(#?Sksd=)|umtfgiNpYwlGv`o8(5`SO%?+DyE+ zNj=`KZ?-Y8yW``VN0HY}tYal2yxZCnc5k`Xx|QhHbcYC6|9)Q7=*ip6|DSPI$;;<4 z&$O*)Uuih^g}aIhKL5{nbGp^{(;sj7^%I;c;bX2xQLB%fT<(SRhu zv699vv^$A^y1qybGw#G$_E4feaeUDlEzCcW(i>N=y?mSypS`ZnEM|-R`!MtChJSRi z<0CluY7zwl|IA0u3#%5e=OW+UswYSN)T;N3Up0ukks{@`#=HCqvFb0Z#v!sXfZ=}? zk^Q&bVGP~3#V6utpQ5X?wB6j9pmOwUg^Q}1(l=9d;u^@<$?(n-8JlA}{Hzc2QMULC zoeUd^e|!p@Q4%hIIO4MlV^H-PD7%Xzh-$%EifB?mlHLIq2_hh=h)aV299a%A7D@;8 zz#dr;(F{WEv`Y(I#+8sY_Zo`4P7)z7H~jHyL8qtdx1X3M6@X2Hk&;X`ucj z3u(nnes^=~9X9lE5E#RWhQ)lJa`#RBY@F%e)c9S>@YeeYX@lV2aIvey)1zW$V+yNEgJv6TGGnyL{0(&@p8tj#mJ{)tLCroLE3 zcXwFQ#_d%Ilk~kpyEHdx=jK(61kTsw$WQ+PQhIi`g+&t6%RdeiwUgzhUf(VzH zTC!w~#4$TbdyphqnrxP@%1(fcL6d7PjtQ1yU7W083giJG5*i6gBB>(aq5$^{L+@^! z<65Q4M+UAsGf3c8O||FKSGs25&nG;n>6vc~CTGtL8}ufGZqoaItavnd7}w_R?^S4* z?Ph-b;vQ!E5Zcq0c)L8px8BrvB4k+nRg?GRxS_<4q^RH0+jX^d1v*tN#z%EJ{jD># zKQ}4c|Du=H{_S#g_1Z1^xpexDoO3Jc4rD0Dqohpfr1o)6bwQJqEj2I0K34JDvaNr> z*XdDQ?Dbkp$N@&?<1;Wf`=(Yf*8I}dZH#*1YVUReFRQe3=k94tcGR50K5se z6R;;rr?$fgC-c%R6fy+F@V>=s9h#-IC;FJE{vTzCyp#H&UIR3iVeZ|`;Jkd#rhrOal4tfodRGRIo}P@5 zdF8dDSK&M~yZ>wKGn&o*Qpus<+O`Tz`qU4$Yjfb@_}(P4PXBuCbDyWU;?aO2!wksC zY31`JjV`IU?IeTMn*G)ucrFga^3+UGXdz*!5QueRNgYAp)Q(|Q8)@IeTq;qn4Wk3s zR0Z;#m>TCRt^He*S&jD)qBusRgd8DX31zFH%YE(^2?Hi=k6`9Cu?`-FGA2hFNZH3O ztCo%G2Kb9OKIJL5wkF?FpEylE4iWrd)uu)oNi$NB%~745Frzb?Jxo84#u}&E9sLE) z>=tT9Qx2(Ko3%e8qf=Q>WmNmHP>-N>kpp`hteKxXc=fYBNI^pL1QHk!X|95;1Wxmy zfe7|SoH(G%fy0F8fLIAdn|vW37K}R^R}CPQMxp;2DPtMiu&`4fP_M_QL{l3+hJ6CO zXWvfU@2I^UdREYT{_mau^}Q7yWAJ9?v58%)aWWw+lx2lJ*fk|gIa_|%=gr2Cu9>HL zu@7|KuDtyrG8P^Y$CdY4Lnlr|L3kTs`omK8r1LF&cMbiaK&Th#UF(QPtE(D%&`Amrz*&|Qbg z_GB|!VWVf{XP3JGoPhQhuxwypnfPQ`jp%104#A>c=1bKhrw`fuPb^Kws9#L$`?I5+ zT;at%bI&{akfHan%aj7E?_s6YE+S6C3BU+J0Sd`yls3e`xh2X#gtqjd)t>-ivpzWRAr$U;;sB2sz6}EE?l6 zpX)u{Yan@4gxV3qL#%)=_auNYhy*?yp$`HWDiPW+0*34(LQs40(^9+J;ZgfQ$lJHE z_ho4jA;rG|IPqZ5ec?}WXMevz=>uJ&+aJRZ0IlEO{?{=x@-ae(1H82a$wQBTsNTbn zU#SlsfAZaqXzG3&G_LRS{8o0m{JV)C9i1bJ(V53JnF`Cnp3?hcENG85Ty9z& z{|-!AQwldSTf3AxEhnY(>uZdkizKPnOs4-W7|Tp0OVtSlmJ2MT`8S`Fx|f?`pkH_^ zu;okXrG}t#r-3x}Kl}p8uH?rDAX|=#A3WTLchm;Lf{lL6RRq8R z4&~+|&^RD%`unyp|HB>@RZ3;70R zv%mA?V4Hqsv4iL;AK>r1Kaxv-F5`Ed=)0A>6w{n{0xLJohAM77g=5BVLi+dZdaBgq zvdx`O8K<&uG4TK5*tZlpn4uHr*jb615(Yl4Nf-6pS_1;UP zaPyDv92LkLrsd^D&)G&3pSk!dt~(S8nxf!iuA7s<@f2T|yh1Dg*=Y2UW9pvp${B%4 z`@%v08-Ws|#Am2mwh6$=%%}c0d?$u;on~w2`RjWesbR<4MENpF*E}%GTB6oM^7?%l zmPg=7^o+w@Y~(zoaJr0)<+mRJ1YJnxePY|SRkr-RGIIch4KjxV7$QY^lyTCLNO0Px z^5+QZ4^vn-$lPe};b#1o{i6Nj?t4EA5Vm|1W{$Uz;zLaAerBuJuzesu_Mw^Vf0XcD zP9VIeI#e%%6t9Ar^$E?u*U(j9(L}>T<-MQ%ehRC=*g?N;2kkKM7|d$uFBOxmn}{rS ztXDO>rn=p=iiA^%Ec5%Ts9IApdstgFrf%u4!;)Xhs==>!R$o>wR^jc;Ilav+u|V0h zVHt&!B%APB@>mI(Fk8K!l*1)rb=QOdF0-xB`{%r53<}|ID|OQTN&_updO0J?_xcEg zR+uM{J+LT7SWn4K*QFNMUUwY|oDnNrh$;~L24d#`R|rX1)5XRi&EB(Auz=|1IJvwq z{XT_yF;{G}RLHkzH<#HAvg{|T7D{c**5uu^sBZo&w8MyzuT#`p*@uj|tl1Ol%k36` zY0x4Soce^gsd$FB(Dwx12Uz3#AaDpJj@NQ)@j&azK(%CG`Uw2q5$ip?K_6KgzCxWQ znG03NDe+<3CrhDIG0IDDO*`|>a=7NI!PF8=Hw4<31YZs^JEsC0n!Eo#C`PH;IOKF# z1kN~r17Rrt^KQsaVEs#3;Eb6zeP#r0xBO%zq$Ve%UOV~aNVx&;%tgy`XqFF9S8e14 z)H(ut;U;x2sz>})1R}iu7=)t8L*3ju7EYGr{++m51mrlY(H4Mc{6x)>8}A63If<;Jm@?d~ zkYf}T_4g=6|Q=!yjCjNJUkl67MsOvx?gb}ta7Wj@>+zmwT{3=%Hxxp z)%sv?(hpDgDN>($fxwbI=K#&k+T~rj2gj)MNClc$iGx<1WLC9kx5jRqIk(iYpZ>y$ z!*u3A?hz&E0Fw6MV(iA>N}^m?=pl7}Yfc689I>8B#Fo(ip3!(GSKa8iYC5FSbZ&Qv zgsJWd)PRAYXbnkrnz#gY!+JpUf87?<1&!*KdL&QvRuUzIND^?8eQqPh@l>aCM2(2b zkdwB6&C*+?)2OP7-)enAAW)C2zU!qDhSY0+xVixiJ=>uX550rYJ0*}`w6#$G5*T!f zD9zp~5tq#t4U^5m+DBb!5KbH++_1VkD!$O{tnSYy%q$cFPf}G`NXbk&B@i!0 z-QZV@4Bs%IiafOyHK?^#e0wZ!Q@E#uY_R{e?wDW~nDxKutN!x}StPP=9$fI*M007~ z_1sbcD*BQC+Ws3}EkxI$tVK`VsX$Wp*nuo+WWHpW#}qIJhsv=@vaH=(eu$-uCsh>u zp=#E+8RcY1=lY365}b3yJQBRF-M2(pIiUHjwDY7jHvfyk3u!)6RY*$07PxgzrSWee zCRH0o1)z9PW15WS-!+^j!c9S#47e|F6;eYqH-)!Mc|$>Yx*1@a%F&r1YmGv+WSBY? zT)>4WJ-|aohW~{uH5Ad~9GAX?&=CWS{i?hJYfJ$O@w5|&&*j15rXW5X%{dZ`0(_JM z_EH9W38&68xf%_;0{|3>1$*~{5VRs+59m+>gc8UIio*Roze}krXJl*4t0-rSZt4%6 zdRUu`mw<^=0W}%!zV0`0O}I+a8^hn)2t0sJ*d;6c;Yl}~Dvc;dt%2w_7VCb(Vj93;3k9ttm=q3x<)N)<_-rWP$O;9bjR(+o+m^{^$8W4%~qTt zx%O)o7b?cU8E%k3sbt0LS`u|5HSwDhC6&_*whY*cO55wej5k=HzPI^w`05Kq0Sb#e z(;)O3?|DltT31%{DIod81GcV7}*N>+(aMcNR-Pxg) zViQ%~W``P$otq|eDB2)TLi`aV8^?#~!NRpG-~&%$#URVxV^Al}CnH2+VU#gWg2 z?%hOo(-W`CljTFu`XHJ@l6KMC-Oq=+fcO#!s)vIdCSvIT3=YBEBg%Gig(9OQ3P|Eq za9;vX(PazEjtKc*n^91xk)2BX#@oi!52jmRVJ<{PeJhm;P+^$}>mw$jAYyPXd>Fv! z6D3%Ce<38fZ(EQZj-Cgop}@1&%%J4Ae2L}1kJoGZah_x=D_+FOUKnuy?T~#6gs@) zkXOS*9u4Q8oADSLrXH8fo+K7LeJ1$i;&7`i>qQBc>N{cfy)b(q{Wf}dV2!I>DE$-k z)|$;Aacc&H^8`uWW@U^UlgRUEk%5Ocr=xA$yWp#ma%!JVlvbfi9Lux~@vD2kA1^d{ zWed?1^UeEZ(_mG+!S(3m0I81ISpmYsk3VV+2;Xk3Q8-5DRsO~_cC?*Re6zJzE5a3+ zhJ8;|608N;pq@TdRR~Jaw36c~0p~q(Ijiclfn4)Z`oXaKEHRfJn zu#XLDgz>~+TN}#Yo(%~%%@t&GHCP-(0#6;r#0&AHCYZ|KAVy&vo-|adhD6?G2(cRH z(?+sifW&DERiTfJ3+PMrBT`sa2s-n-+=(AFD;ouuc=DAFbWuCM?S*Y&^ z%m>gwL$wpQvbx~zErsU@x2*H{fY&>w0R@$IUdE6YFYC#Pi^{tC}JxE&j#(u6ky^htf6|N^Xchr0mzcdNZb6_)zY>v)E=2f)~M-ixQgf z5VD!QPMF9SOvD{Bs(AA#e+iiDagdwwGEDsd+4={8{^RrC4d2)4WntlDfF~GiASUEU zHoZC5Z)HsZf9k*qxluuPnFOxDekN13gaW4q7;ODZhx9Q~f`?q_k&z5IQcwm@#1A}E z@eW7`+*Z*cryToi+JAgW<*lY~Y)`aZO&05_I7Y-nf16KC5xoC6Ari}!Bq$|WCYZ+Q zLv4|Y>6k$M1#Z&KBeJRvUJ@a;&gn>sX(Z=bi(#5&ij#H7u9TehbrXX_*kvcIP1}%B zC7lk^;34oz!Z$>p%07Si;Fx6na}CQva`M1;6-2$T@bGlo_LA;MjM`s zI0N=(Y^W@N5w-}@jZvH(FS_qkj$#}U3S!skGy8GEOt|9_7q$SI#%2cR#o?K_T~-}L2| z=8!vreZrQXsMT4r>L1YRg7ja_8AtRzC9c^!6Z-a0W2`g?ag(J10J_tCsYjNymNFrL z%%nI`dBnvz#|FV}?mSe+0FJ=W_`3ob6G!fnkNHYv(3>9*493ETb|S{eMl95f9}Vm=T;FGwDi8x0rq2&k-vq-u8` zRC>N;vC)8`l^W6dhTKo{A-RJn}pvW&EvM!dZ z2E*pUGMxl8i5a#mPhOtiCi<0-vjP*D8RZ#Fw^K1qS79bzo+>tE@87w=IH@DEKiM^x zR4H_Fv9%H{wpgit!*%hq6+I^bYY#MuFe56H+ERyBuau8V56Ha`{M5a*Ar$%VW^&}4 z=ik41^gU6$Tq1h-@sj1Ivzt>9=X%u7xY8>Z}>PQzq@%w>2NKlB6R(r}*vRU4zkCqSP_?zUa!mpgbOa5ew5&B1`Z4jSv&U z8yZ)TC4zQ?p-{b1OaO?7wE=Lktx?2j0$t9&6VQ<1CLi+P3ixf7%3bLnGIk>hTWb;( z8^z#4w3Eye4WMSjU5Kd+4EdS!(6W=(}AOl3o*DOX8TGdYJYjV2K^ zd2*~Z=3&B@+OgZHv$68~u)MyT^37nhPRVG|Ks)pjPUNt73PUOa|JW>e!NxB>$*5+* z{JB#wS=~oqICp!Fkfvbv6|4t-WA~DBy zfAH=dGPRJj^k^W(2>sXbx%q42->*02N3jiwiW|V$QlezU{WR-9s6=cl0726v$v)bU z&n%8Q`ZI_W0$apnh$^BG7Mha+M9LDO%G*$fz9cNHQwKPKO@abM09OLna~xyE)z!UVA-X1SVOjRtd-lCSmnx(9>&p zNYuz{Tr?k)bmL(Dr4k_-JWs~=4}KBRDhk!|OwDK@;T3GvYx*m&e>3QPiIxR^~84F=W#d0wX>s9Dm^s3(mxz_KO4kxFg1qfK4 zn5GlvpJV{(vbU}WvEp~`b&C7_8)3Yw&fE(lyZ=Lwn?RgP?`8~$$hzN_Jp{+2^N>b; zjG51xa3!=8(jlx$Bx|k;8k6{f?MtF>BwP z!Ecd=&MFsLRvsti?e+%@-Z>wBFcN;`JgwR&Fhaj@`>D3fvY=mQ6)ra}Nit^?rqe^h zjvP)YNx`F?OBQ}44p}+7v*%B2LuELe2MVVCf@k?w`y?1>TZ*69$Z;l^lY7Ze zvr6Pt$rJ5{yO=L$2!e&%Pq6T)3`Elf{rYL<=dx%ewF)SX9QWk!JEHJn=1H*zf_xWO z){P{(!I#*6Ih+GVqEWr9Z^6qX3lSBfN|P7$PxNT`ckR1`xxnuaPQp|3=uPC+Ch za?!>M$cy@tI8EgF6hvf1iW{1ql>{>d-%Bww-SmMUcK3E#fFQ)$i2(=>bjJW@Xg)B} z3K02lIkqN}xNr!XD#d|_TvbuP)7j+B=^+4n){KABh58~D*OEthx{3x5v#nGFFHoS=tJB0|^jVZl(= zBA?!zm!U3g#ky^thm9IDn+o=Y+kU|xd!4WeTix@9uavWzEnV=>H=Z~~4_EFs=T-bk z5~X4=JxP`wY^jpk;_tHl7^_4u?{xO-kTeT&Xg7m!@X@+bwSFX!nQ5qsQ5hcb4FkTCL z2tw3~lNbdMlFF}XsgxMt+~`RXZGzrNUd7|R=Ofm;6pyjm?R!dLw#>U6>kgd7Gfp#Mn3c7PWmLLNbB3J zqp2<1@AzBV87TxL5#Ghe(%9P~;3|b9y2BR^su7dGL!9$eFhnD9d={Oh>G)j8rA0-8 zXuL#N365HDqiTW%O80ayOuS6`$vZ+El!sui2)=cZ_T(`eQN_V1+)XuQt|$@VWa0u2 zz@2JY5#k%oZ)@*0kcJ%TI65S(;qdNL;qn&AB?x>7k0AnE6@%h;$VU6{d;B`=Fdinw zUKhb@vY3Z1e%=Q2jAZHCrd7xe4$pbil&>kw>u01DTJYB=(*1fymb4iREi%5_2HR*0|K{3U1k;w@a61aR+d$P9Bt z?fjKo{Z+Fo1-pn!*<5U#fK4Sqv63t1#+At;$+l@kx56ZA7_u&VG+RbZ1_{fvmwS6x zqNY+#69TzUl4tu%>W>4J9NEn%)!PhQGa%K-kodzF?v%jYu%+_Fj*bwqOw!&bw)0C& z(O6h6?udLAM6wGI%je2{5v>NbDFy`=x>)np>VdiqWaBxZe!O6qDmf-=5MA@PwR-NC zhVWw|*zb&xCI2hTN7O`OmIxSWHOz5;+_^&cONH*I2$Dsk?#H=KEMxb2edK>^$lv#v zyI2C6o~m$n9r|2&SCJ>ah@aEMcV>t{ZTuzHG?j)ym2Vl#G;07({{VouS&M)(UvR?J zmmvaB9qEydX7B>dX-n+0RN{Bj+E2|g~T|mNGE{G{rVlx(fU;xBJ5Rk(| z@JE3?M1%~-=)tv5t*@rr>JDc=;l9mP+pL9-%y? z4rJ+CkiUJM>i^dnj_Pod%i-8|+M}h~aE5)D28hD~xZ}nU-k?h}E2TP!N{9uYVY=)t zFPhp7>}w)my#PT9SE0>crJW=ighyKvWa^^7hH%I(6Eb^>)IVU&3wwA8LspZE>dV7X z*odH^IztEf29hYWQZAp1YR2#T1lntHMZG5E8X!W;R%i-c{#vErAC6ptG?I+RyAxiS z2*;f9zi^aqbB!dRA94dfD&uY!`sPUk`Tn8KCCjG6vqv?SLPxwVm&_pm-tos%J*XA`N_n~u8Fem5lB8}avS4hKw9D& zr$r$0V9+{2ZTlNnI6}G8~|trSrkx0lL(Jp z5VNZl3u5>F;fTfI5kdYETL6s47r|8unQ%Hp76jm1(pQ&OaiRx*o;RGM5&c`%L-L@9 zIALAau3Afd?E0GH`va=W5ca@B>)41A5GzDL(>N#$0b9lo+1MK~5(V}$^H`iWLm&`D z6yRwGCb2~@3nB?@z?qI!s+1Jb6{gVvQ;u-zsW5H+{v1=KY3;Jc&lSH45}v5o&BD&c zQZA1kxsp*@{4kkf>w+GZ3a=kRo(2)8WAHcX&SaOIGY zec6PbgI4Y$s=JI;ejv!iU;1GqSwm8FWeeWm%B14abh>miLo^Gl*>jhivy*VV zuqS+yqc9}ykz^@EfgeSLuL7E?khbnJwqis;&A*o6J7wPOO)J84bOy?!3lOcl0<F8-=5 zmr4+eZqp>98UPMaR;dfSYr+oBK}f+DL}Uh0btLfuaPNZ=VlxD=3*tC`2|Qk+fe(!! ziID}M4SZ>+zv={DbPE(!_7@JSM!78<6<9#s;Ao#4EBY;|tpq?$KqM!K{yw7w7f`|! z5Dr_w1`#k6e@PRz*e^Uvmn0TI+G~{}a1iWX2CE1}vS1GiH!L8w?-JVFm7X*_D$Qx|&E>NtFiydG~=;1{IxB!iV z)5eP=SHg85kPfzl97K-A1fT@5#ZJGc4abhbwYpWyRqw=qRTnfVw9@!wY52$T*SC_p zR@B{JT5)qn1QRI#4udtrc(e=g>1aXm)MrUk2RW%a-X;2Ibb+ibZ8|J3`H za#9R<&ILtDX+b6(31`@j0Tu(pa+X|FGFaQ>e>Mb=0_4i>aM7v61UHC7_G^WE{*udM zSRx0NMRL@Lm!?+YaFNnN9}mY9(L$B7MU{d=Tsc!NsY^@WibDF|T*L6J`Qc z#s}!qrAq(@B@7ggv+I|7r2Prn0LELW8$f zp_wbD9$2{*>^Ux6ibv4nh_bs2lB(Oh>5`O6h>ez#7XvfP!7Y-+I_YwlRHUggPMMAV z%idd%QjK&hmLZxz!YYxZ4P}k^dY^qRnc+J+FB4@&7?OWQ(n8qcG>GJ$pDx}&DFP4N z<4Xqkiva*Ky7G7zXYmL_w1fynX716KTU$;}J}o!3jwRS!>$Vk_oZpsETaxHLZ{~C3 z;Mt4BB`(gKBy|E0HLZl>*xri^;vyg-00Nibi&OZ$!XV5Z^4@#_;>DNL1A9LM$%m^1 zXapr6>xGBypEaW=9z3wGa1w21hrXBXb03PDReJhHKxX$Ghy3fYObr7U&qlC8nTfIC zGtmOM#EN!^K=3)qHaxuHp}ZGCIM+u4Q#mldgu|7|8#%~R2w zP-CU+cr|>(1=~!Qy+M+6!P`Z*Ss-)emhU1d#MmY-*6pU;A24PIl+7RskO6Hgdd(jn zkuu2hM?w+pBnhLDIO7q?X7;#UWv!h#_3^$Z@9U6ddE+t5l-H-vw)G%$wFQYW0&^PX zkCTs96_?zt)|m%(VZpwcJYfhK0)ZmXn)>=2ke7l)t7f$l`5Yt+i8G1>K^{U@+DjoA z5Ac!&t<~*0#3cZMP{p~h@eD2uw~Oe{JWW%9l%k*#PCPHXU33{n;b9)SS-<|7;J;P(Wey5}U|!_Foj}Bpz{1friWFwQ)GOWaPuJ z>A@s?(g6$-nLhoGFfo&co{KXCdG0Ku4vaB$I{|x}qLA|YwRbMYVuJPrJP45JObpJH z&orIjTQNO{?^1VD;&ty*l8yPC1*TF&=YlXIgUU~`;E!5mSZourjS739OF9RBi$69@2O|x_wBYSa&?zWSFK*77mtC$;Ka*IPPFj z;Nm=N{8ZsCl%dnwX)m4V@io1~*z}dKah1gSvi7WpmAzWss7Jb>sHF#=DO|L}$CCN_ zC!vqX{8{6bgmXsxm*~7xUX^#(x)f5vE5(Q!;Y0c^^xsd66 z^hLz>_H*?U(Ti5nJPJrWaA=L6rBZZnfusGI!cQ|09p-0gejcSwIhg-h0Hj{E@}FoV znvS#fW#3yof{K@=yt!ie^*xB~Mpvcchm0Tq6h++Qh>Kz^JoWbcp|iI~{X?qJlQqd}v??CBs+cTRoVcswFb^M2s~$ zZGUho?rgSl&A^drF>Vv;^(52e>1gAjErBzGKLg1L>P``0YCC@CJKsrd5{3fH?15XhU>FAo2DFTv_ zA0r%khbCLI)ZhxZfeWufU6Zn=pAzQ?*oaP;T!i!fOrzx!|6z!`bBk`d##NuQL!+q= zr=Iq|?)$7FAWMDl1$Pw!1mjx-jzB0lkfXH>$UK}U>e9?GAcz(X9)J@dz=DTh#MwX8 zLt{JA<}G(~gaDH8_^1PBhb@GUBbo+kJ%3h`4?*o{Ahd}9b{IfNvOxq;204DH4(7z> zK&prs23+WBNLanKh%D2S(Qu)8Blo+>ZgP_HkCsfKvLq>~%CwRoe*r}vDY@WjWyLv` zyVbxM0aHqqa5e{Jtxp!o;luTk8A6V9?c)#FaBHDd(Z~i@MYn~_y#QjNq|Zr`!yJ^I zIa4&ql-W8!PcdRoVw|w#778Kb3^7Tr$-`RFqfbZ&8;rKnB#RZWlf@+&Lbk!9QXTYU z=^q@S!d8lJliYEO36g}B`4pDNPgX$_tj{)3)OOg(sJwv-K76Q14lP+lW&wF{6zb!5 zsmXe{8J7Vh$vVTE!H@t~B=O8r>!HLa=w@`AN7x+8%Badjs;CSFYYJRU zVxFP*AM7Mx{@I~&EbXwnS2Hg3oE37P8~%WeWC#*A9i*u^fp@ud3Ss;99eDu&NCEH$ z5aQWm6lL(jRu%+;<-oQC-|e;i0Jz7t1HiX|03ZN~BRb*D!M~eXJqgXefCPv7(}b84 zKeloOo?Zd5H-*anTyorUt>~?}Z6Y=1VEbl0|MSWP{BL0}0+~CItBT~M*XZXm6u>_I z!8~opbNrIcet-r_M3gMaSO3XV?*0cLXjXld>+>3mH>(Y>(MswnNh#K2lD$B3%+c02 zC;@-CF`#!RcDAzoT<{Ci0ftyX?zD2eCq*6c3d_#j>xudcoo|>FzpDwCVR0V5xDF8l zBhQ@j-JM1B54omL3WbA*Ap(P~{W`JFy(MNOP%j@rgro~#`-A<*;!CUoR<}b8Vl=Jw zKxRge>nnCX8m-*Ll~JDvws2_}EC0x7jtB7)QBG(p_SOwjU*r5QeBFcbC-%0pFv4Dq z%PF&J?Z?A4C7wSN@|ry6im4LggIy}$88VeIX+}F)PZW&R5|{m<|v-jtvgm*-Fya`%@GP}wH4$)SX4EH>^ObO8@ojPSL)#aajW&p zy^DUf&N941KdDy9uWtX@ctH5y{~ zUu=(B&BMk>mX<xp{dG-kPb=qm=W}2X02KkCb}cDR}vn!Q1X3wU*BQlpMWUzl1+dsR#CT-#?c| zi~FA>w@u#r@3R}#`+UEpndVF9?55p$_j}dh4d*;G(&D^0`NF}=-2G7O6-?|EK6E#b zS_IOj{4B3g4~oU?rinw{!7Kj2ObwRU!C(}e&~uAc_;E#L=YUr#eW}=lK#Gnj2>gBU zy#Csif6y`R($38qUO{dYb;Dq@?}tbKL555PbD@OihG(%jUo(XI7Za$(dssx8^Mzu# z88O9W8fL~;)>c+0kM2#Xlh5-*)e4gplw%utUR|$(sF6CPapl@k#Iv@L2-_56I{a3> zvyYL5)?(;G9_7B9t0~7_zRmmblbFdE<$!_sN7r=IWRgQuQK20{=dlspuz2mNkf8^s zL)>8w9?9|g2&p!-TQ1^-afH@M1dX4-TMX5;4(@^a^tmUWbvw-3Gp`&b8!2Qx$u=+rFNtAHE z&0Y@z5lgj?jl}f(4FvoBCmmyJ!uTB&Eiomnt{(Xr?T;h|EXSS0tYrV*5rgj~IW@>= zKS&%JIHxvnMTQy~7I4BtJ;}X3F{<50tKq=4p!CPrln=$Qnja*^p)D-<&iazapVbj2 zr3l@VvC2;gR<+5K-!IAM!@YCiHcts2C9n~7Ut@g)nG<|MGn5tyi%eDS3Cth)s!bq= z9c2eeDr3R`lo!Xnz5ap~-9_shTt~9tbdmB%2mF11aF*Ng6KT6i0o2sT&tf#3@s~~D zTxh=QeRNy@#nDNaP44k$i!hMt8$1+9kAiSAgcS*Ruk*alnT1hUo1$Ii%ajMm*d>Zu`H0Co>(Rj@b5LoMv23= zM9kj&`G5CMEj*|YJR_#`AnG?O;P*4SU`dwDPU2IU7}%MzO^wLDGk)u?H>6J|zPoru z`&rKSTjlF&iM)Z_&nW*P|Mc===k(6Jw)&8dKeX=!5x9oC5qI6sW?a-gUsuv|KSd>X z&-8~dDupHd7g$7%;GB!F|E8lElc)JJ**2i4SuHd-PRA>kU~=oY8OQx{Y+l-$b#H%) zQrETnJVIP)a++qaMM%-fbQl-s*)bmM)#^NPo~89L|41hxeI0gL;CSXXrM-=9!A69{ z>EIh9qCJ%pii%G)Xbs+vojsk|Swh!2SRU$?xKdoVCsge6yzx$e@qH%iM`zUc|4Pr6 z<+PtE@0h6BRG@xY^Hx~n$uw1H zbA6t)SrF-$IixHNhD#oj%J&3A)Xs$W*2l%H$7*;gyLePO(p}s-E5~5!ie5Ul7?h=K zrCPf4^zni!1pIs}Jjh)ql9OVY3q3t^+GW7q!?aL6zj>@Wt-&Mq1j{?7G^Loz)^=u@ zO&)Kvt*J|cJ3NGA+q`)jr{ksT4q2Z*E3M9pF|WFJeEK*{*GtD+PS@89_O&cIelVYA zdiJ1oX8uFpf7oRPI_>`)Z8r{WKbYBW*3@q1ec0kd`@e$sNILhdnxqeh-m`t)PHDP# z^o=wjC54y4GTph^nCk8N2IlY(-SH*C6S#Y)WH;oLX5Cn!%WT=jgSnN6ay${tNM3c; z?b4mB3!jV)`dD}PAXY7VD%=epadI(i3{m~U?Y_$!?eZAd(a1O@xt8<##g>PvA@S+W zws!+qErORWWp0NnPhJ2UE?}Wy=Lf457kx5g&5x;GK;%dKuCDcz*X_fpPp;J53_Wh4 z#_CFeos2#kyaz1IMRq^$?hZvh3PncbB7<|0+;U{ZcK5)W?g5>i?hnYm-#t%tdbpX$ zmrXrAp*_>`kAi5AW`1`+`_S|FLpLvT?}F~1ZavR0J$m%~@#gPGFPeIO*7;5!?cMZ7 za>|hxXg1H9d{+e@j~(uP_50Cm3i1LSi45CC_J{UN3O??BBYoP1Gv3|3R^GFu^SI|| z&&Nx>AAdhm-0b-z$opUUq(^bz(?m{_xhVV^5GGwEz$KM z>8@%2XjSh%y!aT2&@)ax6&W}7#612?z7?%FPq#a+v{m3zoZ!*8B|F`(Ch%mbVyj%3 zbm(!vrtqQ^pQLBaFQ0W`+b&vw1r5zzPoTSD6OFO)MIlke#iy2$C!;ed+vr}QVUDgF zPht3}?r_rQ=K~7EPr`-==Y~%{9UieCexWdOl{V7fJUre!@=SMRv}fchAI;oGrvt;w z!zfO|i*!D^cNmoly!bGT`q2Dh`Ok}NJUUx{@Az`L`Ne|Ja1LKOC-MdV&&Z4B5oRuW zx#vZ11sdi%D*ACm)OU2$UZ93PGNmvwoG>ysH}b4vq}LoHyWg!wUq&K(OzpO(bCa?| zg>u+`vAmEX3*dPNPInlV7KQ9k%x|5%|0=fF#fJuuOz8Iv?nbfN4c4 z8#IQ{Pj_*uP|u3*hxf-W1RXCuxO>-~de^kB$)$+J*azZLoZ2V!A+n~bu z@yH_OEGbV{izUf`qr#NbJ!@f-g`*AU06KF(D8j!9a~*4>`1_nn=pm{rT3t^D6iP4;Z7 z?;EN8v+a*&Yp%a}@MyNwG@#Q(O%i|5#-;9jRJ({0;p*a7_VP0}x zx&|v+O`N~=X#Sq>xJLM_n&SB6{QRiH+xHgp^^fMBewSTXrqXxzsqc98qqqCA zhuOk!c*1XjT0M=mFL}nXEFOhI^mbo)ZL?AiBB^W1f{n*x1ge{+V#A7Ieuw*OjZ@}A z@>(Bw-U!m6g0UXd)ELwxJt5D1Mw@$5N9QrR*D&I=z>orTAjb4=Y_W%i<9&C{(8F(o zStjXfvZuRZW{T8U32JAfysc|bzi#U&RBTdCc=}pv^mG&Qb%F5fuKS)1StwTv-R9WZ z)Jua&|4nCXUOVcwNEfUFr`tM8kR-a`Jz(`duzGKMrGqH+gti*%xO(aD%Khz?=m^0l z#MQ)>)js-an#gKe#9D@2UELX2tuBM+@jgDCFP}(R5(czbrpo zxBT#MkAS)@@L=-zZQbHy*~;Z_US-P{WXC0Nam((HtF_W!6$-N~!r|WFg4@nL`NSeJ z7#lX)A6}k!Ia9fCxKnZQFeiUd|GUnYjHQ3pPus*ST@HfV0z7O%+tjq)Kc}TB{Wwy8o&RNr;{@KV*i?yWfBGdP%SP zEAJDNFWskuZ^s?}-_+sRSJTG3HWl-0Q(J45dh2m7*7N_aG$*bPN36EIU5!7neji*d zi&(GG``~wFz4^t5)1s@XdaI>+>&b7wN4)$Ht+#eAaqH67>b1llXZL-Gzp-)h!1{#a z)&r%VZybLX-r20!+K5X0uo$s*C-Fm~-bXNTbK>uh2U|Y}Blgg#>&%E9=8MgXXMd)N zZUkLfixK_R@%H56w3}B>lSyzFYM@_NSXB7zDN)D_1`Drq2rh{>xTh+XHw& z?jVmBtM_jN{6)3n^hD%o&*L{>F55^S>@}l@e*y*6Tfz`kt&8RtSz>P|3Vm7JcJJQ& z!FVA=bAqzOhtt5AN}8C8MHU}#x?4jRRj`m*i7c_ol+yI#`%D-j?L-M`3H$~Fc$(-{ z;gnnjXXrD#eJTk;ve3Zcip+R^aI$hhwG{)7k}}ALrO_0~HqFfoxU_-7L(j6tP8}C5 zP*XT6B*ts6#p)b8a6%P^u#;lwl>VQhdyi*&|Kk9@yKQE+nVI|CGWTH$$=PP^m*y5p znrlcxDoUMghPmceNve@M6(u3nHkTxoTuM@nBuS!E9jBDve*1U-?eY13_W6F^@Ava% z6&knobBG3a2G&zn@1+!Sq_g(*GKXibJU5Z!#X;>A&??9qv=?q3ayzndIx^F zI{TbZxZ0v#y(*T2Ipd7UdPQ*LFc)qmc|b_FkEk)-^3(WA#c@1hi-unsVg%1V{?XvN z@x(hLa*-Rx$FPtLYQ)ntj7I9FjN=C_SA!v^O&fpv%>tmF4Q|nv1S5_jA1Uw%*oX{RJLTQvUBP75XH^0p& z&1ywO1hk&MrWWC(os!fB(-gO+)!stQG0+yZLpnL?UNOZD>inc;7u$ulMj>C3*Oa5F z559l=K2ShHkLM3l|3<4$s4WCORRv$s2{QijalVXRKC-EI`d2}-@N(R<3`P| zCByby3DHc`O!H%{O&?rHC}Z6^9pvEoZhr z@Ol4x59oH=OQ8wYgJLEpJBH8l9|(Wx7It9#A`4%{Ber1r`n7Yg6%6fxZB^_NT4=cy z!feUF@C}zue{5rlS$=VW2L+A$im)r~!9hJEp4D zGi0`ojKNIoD!8bRG%1oj&iSFucz{ijjl7>t)KolnOL9aiwZi^WKD> zhyEQbgS(dezt4o^)z09$yluE)&)k)}2>+@_rsovQ+dogZA*XsT3SDqLcW+wWIhC&% z`Ow5~7x%_k%7-kspUy}im<6nv(w}D)&-o6lfST+VlJf0deY|vYc}R0Ipv87D?_&By zoxAl9IhzrCpA!oLvk}ex$qiPahkr&~^f+F+_h$U#mW*w}ZR~;rM(^AOOO)Vx@Xe$e}nqpzU z?tr_)s!P%7!Ti$6R@a3+?sZj?MoWhg*LzWqilS~b+DYko5x)C}89fahNzcqubR?hp z%j_KNJ?m|fktZ%-n z_jI-Vu<^F+8)Z*_l{btI`3J-o*-lgzy^A=kvgwsmR_i{acP`Rl4vMpzAG@>+I;wf| zC)$E-%VVH#FmdO<=p9EQ+&y?(%tq%;j*NnsXsABv%;UV0rCj!Qf9rnS(p{@7T{t=eiJyn3 zWRCj{mlL45^Jd&tC$(0L|DC0hBU#BU2e5dteL;?yV)})Bxgn1+5F?10iq572BKNV^fsg{yNBxC&3CxQC+A*za z_zqBrtvskfop6qq=p!vd;ig0qeKL|;b|EeTdmh!Fb=F6WuG?Lwy|6WM{|TaMz}~Rw zv!@Mm4hn)7&tkXTIp5`Y;aKDIw@5(G8O$1DSU2F^6rNIO}>rsf$lt7Hn!RO z^H z=GOU~_ZW6SJ{oKY2s%<}vpx0dt-g9C!ojMa7asKLv<1dDP8u83SDFpL+k#!Jtb7!T zj#a5$XTvCL;Jp$|n~p=zKQE=b2>i5aMcu=jJh?B360D;sqHqkP)l-WVXe`r`d(JC9 zNo~{#reAO#lcoA@tNZ+Q^|ps?>gneiv`Ya@c{dIihatv^r!9~!9MHwa?5E?y^XwhG ziA+J~Yx{6iq(bP5;=s3hmy-i`+465Q4*b~Kjz@gJKV#TBGj9IF`)I~>a>6fy<}7dr z!uUpOX;UZjgku4()&z2*q;can8OcXpOq-nOwAm3SHnx4l7~JQ#|6vS^1))i>u{*6Vx=;{jLjBaMS*n}A0*)We!!t9Yd|Nx7e@@nZRy9}+@Uk^$uL z%c`^rLBeqTMGV|F&K#@(YHfGSfs0%$Nv^&P=Y*R$VF39B@q-Y`)QLe(N31iqi_Lg=_e! zwS`x|AI`gX!OU#EQvc5R-?E5ij7AqNvpcOYQc2NI$aj@8%=;b8=fSRNGW>zhluFE& zC9bspL#A6D`nD}Iwti!bpU-sYciN8cb6w>-YV`fp+UJzPx2N{G zVw~*pea_$b&Ix>`kV)UHWm?J}@_De6l<7ova>YQbMwqUPOxH9feVu8(!3@yo3yA7x zv@+?j&Mv)uff)l>)>f~?A48=Qn_4m6j_64>wS-pKBUXb12n1No- zPOO12)LlWc+qUw-%;7CEwG}N>lV_FJqW_t4-FHz$PNMn44r*k`6YX64cJ- zY6R1N*csvR@`{pMnxWTtMBMp`TL`g0bfMf>6?iP9MDr`i5PnO)u}pz~$NiTm$^KeM zW=Gzps6nR+t}zBezO3AqPE^z2ec)tr9RBkx$FqR*If zno30!VNE7m2>WTCcav%`>czzHqlK_IA#7i8mPZob13tv3W;$XZqT?jHmmtSYinacQ zExcHByG1ad+0A&Bx9NLLJ%XwyXoW2!dv}XJP-z#|sCj$xL?`mVd&}*I)Gu^*)R94k zoE}{`*qErU75juAY<+39q>f_#j@OZq(mFm|U*IOxZBuRFBkU|ihV32gCBp)VHtcgr z+>M?KLbwS=zI+fzkyV`K$cp_8jBbi!8vk#JrbiDNS`p0>yl ztyBJV5mIv%BR`&bHq|tdbWY@U_M_ctJztjk2>9{t2C+*;{-~^ z^h=0mFK;}v90Hm&k~1;9%Kv)wNSXLTL;GZ2?3j*RPh}yUdcFdz6D290K@j^r^aLP$ z7LOc^zxfSENxxH87Nl1p*{IB)g%mmL9XE>x;5Pxli_mG|Krc|1?=?9F6ghww{mj3Q znGf`WNu2Ei4xnH_W8rj*fxpp3uBsaVAoRB@lmla?$xz_T#@4r=&@6=Jp^tareY;=2 zyhs^t8Nbs$eL_p^!mFV@drn=cJt{u?YEQt-c7+<`nY6x(hu)z*gb+Vc*2e>nu%$ts zk)EMvLbWMiSroMF@iuO}6<7?5fe&%Yvt!sWXAEx}<@xHPnP<`2+uT5r(7kJ)GW7vL zcG7EBWjw7}kbf7{KMicsrSs+_)GqD17K`UW&RWKS49T*k4x%Ei;cuM>+?{Gkm%Vx! z8oGflXq^9jrBN1#Ye14fxWf0x?}#=t8~MW0qqnrrmz6dG8$NUdV*YlLM6k@U&@k1rSpqn>m}24+UbW=C_Lzr35Z-whVehB;z%=KZXM3 zJbti8;f{J`kMdac=ekDyRghb!d#b`)eU{D%zkS4BR!vjs(Z{9XM|avj+4<*g@X2Uj z5$9~qg_FF;2&22c)G!9hc0AvG@s~;Q`07l;7P!d4lxhx3OA1KJFsS8&n za?k=f^{HvJ`KuWEw$y)SBhPQUDZ7KhmU;X2ZiBCzL+xs125FJos2=yGlAFh>pb<2fIVbnHDO*q4s(LlB0`pc^`e4tf zPdv$D2kdoJBVP*u4u;>C392n;VeGWb_><5B#o2onGea=p;YNFTY%?&9x)73z3 z!P$O9-ltxub8)s`7m%I}wIl9jT*EQjvPZJAwz`zzZOa0WdemwJw=o5;Q$TsmWfb89 z)_Aq5U_1&O~+T_jITY01R-U&+u z`L%u6!Ykdm6lOoU1qkz=3!&te`P7aPeJ)G*gfEx{bhhI-Bj!Q8HW1g@>3k1Kr@5%66-*^AG zZsy!DD^qIAN3>;@ZcA74@{b|Nlsj(u-!*sGY93osqYu`Xg~@Al2lu`u)xH!t02-xf ziL=IcKZN`>erzk?OXoP0%|RZW1`0!9n4{sYvvmsoBNsS`MSv2KqgufutpX8JX#rvF z&!6z4J`wfC?PiH%N8DfCIPPdy>iz257uA27J08o%uO1r~C7pEFc(0mt$;U@{P5;>m zg`z?EAX?-|3v?kg`LGtKKKr1tsPXZihqK6bmK9i@p~tV|smj52mMi3>@*}steLP~QXWL720}q_ob~t&z zY)%og_`T_IBAO6u0sR4N`);Pd!E5~(y1f7U;5b5h1TT<733$}5**KNX>5YP2`@4>4 z9|PsK>uSyPd_HRY2X&94IB#0Fm3QUIL&}}XiRVPCPcOIa`nU=@Ga_ zd*;orPnlPqE?d3Yw=m#i$V#^weeX@s`R{&KItXc)2IF+qMG3Haj&OoTP1^%(_70-j zL*F*GVy<(ozPq2kE6y-XvP#yyA;3|>^1K3U^#?H8F06a0yDk=A7JH*aF4OSVseJvNV^x8su5$Cnu7)5~rLe`?@Dd1eRdK8M`75NQ~-kLGc?7j&%Iy-M!Rl0IuM!r0yAu?4}HU?}j+r1z%FxAOj-J9pUd-IPua zG~qIg#V$m5^6}yv_`@h=pY7Ok<(=C#pT*@yl-Mxw#^M1C{@AtV=3wSCn-_cIynxl| zqq+gva8|(>UCnKi<^Vo^-FfzeKYcNQF*wQjaS*L znuMWm|^ZTjf^c?VOg18`Z+ulz6(%v!P$?#Xp-? zqulWK{susPTv~M5RQlN={PuL1Zs#l5VpblqG^@Gdt zp66pHD_trQ?koHdL6qDmD`0ukB*5?C9+Ql}L{4feH1kz#4xvt(pIpVwf$P(NI8zP~ zBn1bL(17qZzFB#=d;t8N%B#2vNr83#W+Xh4GKs*L#gw$EbdIQWR=ns7s79`$^m1-V z-mv*%So|Y-8CLM_oDQzU!n`&MoBX_A{|7Xe#~5XNojasVFMXwGxCq}T%<9$>}dVSgWzWqm=sQN3;7zUegr@2 z#wevP4zqYRuNN2oXSSnk8v;0pTk|u&?I;lcrLe&FuvV0O?g= zKO5#Raw6H%SZMr6_K9Q{O`kR(iX{Z8cF~|c!UAo505Ff3MRKu}|H}S=J)HzGV72EH z&@pzkw$PzD%Zqg@5rnHkzD1~Q{_bL3mCi05v(VW*ZI1`a%q}g{9|L*J)LEq_ z!LIv6-@M?0PkSyBHBmcw_!B^XH$HnAoVvQsQ=6yK?KmyFQ5pbD0-@O zZUUT_Z(hP664kc;c{%`8w%x|(%+B3gtlev{M!($;o-75aNI0H;by4Rj^kMG;UPGjo z^)4EsL8|5)7U^>h&YBLeOW*yboHJD5`cg98L-Fy|Qi^rr%2!b)n?H8*&5x{Qs@IJ2 zzXG!Vnt%DkQIS%i3YkS%#&TQ{tO`KyX`vdLeFHGc0^4>_u*D+JeR+Q&n4|&u*w~Gc z_p|T>l2$a?N$(Pkh&GY0!}x!sWXz)Wx6MHb%X}-@oVq3x0CchC05MYNK9-Y8a_lA` zUJH<&7HXrlg(T6WVNUPut!epI13?XQ3Ie6F2u`V!Q3_Uoo98IDt!J6e&E}jflqe+x zXIZB{!}u*Tq7v`Una-eu_~qo0lH#6HGDg#vSfn(a*j{mM+!`OCb$LzG_eEuYc%pGi=osM6cak1lzZ z-FxtjQRDCd|JyhU<4#=2D^hrG=atsB$D4yoZCA4+RXRuGr<%qqzxddCZU&!kr1jr4 zk8K0(*ttMifx2COUnyeqR+COCX!FbqxeApH;d%I#BW#iwjPY0%RS-MvZGc z`~z8UHgUw1g;fL#e*-nrvWzyYQ6?iIVsr2S?hl8LA@i{IT_D8(4tysS2$AI@7~$6V zs5Y_cXd@p!BE1BfoMe<2_2N|b_8T=buAY67F>iN>)Vp*Bf8iP;c)EM$qAfChmTzNw zk-hKGy=#*`q=}cIkF!cq$ou~}!<);BL(Db4>iN~YzO?1W=A#DUlXJqpVbX0(_LSX| zWIg*o0w3mf1C-0z@!l$YYfJYIaeuGN?qfYi{tk;VKm6u($L-5F)ic#1$OXsZGv1>A zl&89nvwpmfZeA*1we^OCuhU7j;zWlbd81%70{}!J58=Q8lNmgKHJyL_cJ~kM zzYKVLB>bFbzfZ346{+O%e@Dirr9qY+&hD{$?kc=ep1@grk60uF)oNea9=!1B<6B|* z3riQM#g=}0YZ)Ex%|HzT-pIbC5B%r+pnn8}20;5oDqS+okOP6!K)o(e6xzdRVK@XI zmkN8N0N7-Tn7#n0MmD-iwCQ?}_BdA`Swj^3-kGi7k`P)8aS-oa7b^*)iqdoT!80i+ zC;eU#Fj%ao`Vt!^GGz9t1kiva$*2uYyIf*1D&F74Fp=(r6P*&=H7!TO^+Y1`Wrh(P zto9s;sRAh!(9~)mvXBcEeY)i+!<+_IOcR@wOQ=HMc}?f?nlf-(HkMq|?=sTwJe^}u%^v#-A|@Hx+TVqUqJriahO^WRfeX=qZPWfI+iW3I7va7{@ppsvO%Khz3P79QqvKGJvDC_0H|I|oPh2#B_+pPsBakmM z4`~TX7F!M1))q{fcip`!cI5*#K0iG1dn`z*Vr&d-t=4Vb{Vt_*`iNCf^TMUA9{Y(T z+=)PKcLDlJ-uTJ8K1SKE)XO6xRd==)M_%=T^dpFBsKJM|a_OORX0cT_1cNA{SW`d0 zi!#w!q0vdDfv6&RH5a)pv_DgWuEQcl8BPG9%QDjyDPqM94jLcJtrQ`qG>|^DydC6d z>V#~%Mqw*Mts#fer<)%{oAqQ^g91J~9Nzo&Zs$Ku9QE?W+h*$}}keC~JT%rDN<3*$p9Go0Vxs z%QTN_x{_-IHk!k&MW=dQ!ZuA}fD6?IB?trG8pK>yl%PI&%K^2^7j*Xq%MNvTSVr;71YhcJtx zy9=KX{eb(GG-M)8j?Ydg$QBuw=gjU@#Y;d}Xv0RHAk{h3Aszz4!4x!>JOltq09|sX z!eW)4trLb#gA|&8hUmo}qfim(Xcy}++)1||Y)l3lhl-))q7Drn1`agnl8_T53iC9B ziB>X%tGg_xmu(dKjM1xKsT^(g{M6NShsrQqNo~58QNlCRM#=dt=G3)I-sGjA*qTU* zmg>|C!v$^sp{vouX3;m6PF&U7yc&pB;Q&7iAnE}{D!p?WCho4ax@$g2FKMn%QorZ? zp5}Y~=zB0IvK6&-Z*6FxECH_~^>c1!$t~tN3yjE|Q zJ$_6xavN9oVvpJvH51}`;^j*Rmo<>hl@}Hmp$?zlB`y#6p3D)K=fI2sa#V4_NsUcY zPF&jVkwDL`+Lj(i(Wy6z?)Lw5@3QDLU4&H7P^dvADji=%SE>Tyr@r(q0q!k)vVS!L z+c4>zskk9{q&UGU2pwA7F&&d?CD zG#(n5u+A}thv>RonmlEsW3D^lp@q9Xxo3xKm`zSpvWmg3eOt7$RC7yJ3)Ys=IJG@7 zrnfDPcr`0$4z`L)O!da}uY2PliPBv@QkRC(B;ay!StakQIq+$MeS`l_kLKh1vHf6` zDg|9TCF6K)^EI1fwFZOzH=2GJt@KpJ+-TnN>TB~9|JHx)^7hT^&UZt> zcv!rY7+EFTdl}y%Vs9nUo})9Z(=a=Kfvjx7Hp>jN5uVjX@M&zOHOa}O3uH;owCWl? zt&Sb(eT^mWEjwXLmw2vIb*wSW zeDlcMt>&V{%WsxiW3QVeak`12XUS&&x@`(va05a{E#_KSROtTH1xsahm7Mo;@ z_JvL2elrxkAME_R-LlKn*bAs51l@A+RsNOrBKzY6?g-8`EdNDd_M);GDlzRha?@HF zI?))m?cq7qk&mi-#NM?Z1C@rqbXwY76y);#UoRQ!Ueq;S0Mwpd(mE_ANJ|fRFDzf& zTK11x)3@YN6NC@ENVab0J#w!h-XQG}`%clXl!Yex6a zc?Q42rt-h|o=Xj*n>PE@3W6?#=q**Jl^*UbjN_jRdS}hM`AiYBFvx zziL}J%HF7y|47|D+>uv4$JcW=2Wt}o;`r~edd^`zUVf0_LA`Bz2}Y$?s_6!~>8$wIlyc*lsB|J}gmx4ve2;o}T-LTiVm}LS_KJZP{%Q3goYR}tl zsvqAMs$8T^f-X+`zwgUIokp!y-^lm3V^{I8#4QE=yESDZ@$sxQr>oF{=Xx& zGhtHKCBMULtl>((|J}+X4j}ySdTh%6K?n*E@Mw`jryY2H_nn=fP0WQL&>9IMj<`Q} z!7yU6SnDe+HT$|BX0}nI>cr z!ZTE3i2G!^pJ-RWf2J-7gOOqvHO1a`k?N6U-v`9K8_yo-$ag$@kmjaiAj6f?uUP3# zlp;O&Lo_$CRXGXlX*JUL6>fsOA!*hb^W&Ke;+4n-Et~bt@j6?(jh7Yddf|~ql(|_q zw0W;oveUe3b@*zbvB`u$2c0!0&?s`_HJnR}pvqOMd4DrBvGHTrT&WVaJgKWH2I+5g zsLXwQ8MkC&V11jC-(uU&IMc#~P1lSL*Ay&WHOnsbMB3L8zikP)T)VWZr6B8u(7YFL z4ad3gxAzssC-f_R7;XPoxenqE^q$@We3IU^`qp|L0qF*$cUY^AaO%|>de1Z)C ztUlhh54e6%WcyC=ipEyjnzRC0Of9D8ETSE4VT%c)K6gli2O{$FEeT(8N&0_uc6_Wq zVcSFWDkkw@3;qWIQaD_6dpmM>5DjraHHN9MxziIX4pEyQrH;FNOCkg^_Y^ZhdwmB8 zfid016(L8{dn90s4g;s4l!^0at9Rb%cGcT?rHu*|*S9B(r7p;l2IwznF zs!lvmf2Z?U*-s10$Jr5j;^60=OPYlGHv%3O47V{`NY({`ah3!1-UnU;`JOK(U?-UZ5@}A-vWwj1e zN+q?cvS;N88)DSft(S~~mri^q^=P(Cs`Rv{Mb$Ru4vz zr9H6PZMSn{ol=}wMe$qn;V&bo0rG`rG3`|OhYERzS}>Mp>zRs^x>}cQp_WI*x)DtB zp$6Y)`$_#&WVhr%Tp4S6ddqE%s|ty!t?EyvVg?+tKf4?~@T?e*chKkj$hV&GZe|iAnxaUtA!j;uGrRRT!^aL``Ww#rm7kuFWxKuO1-Xq8ngTwhOdZc9 z#7j!)-OfQhW6zr~5AIBoT~2Izh8HZ(W3rulzIg4WC|#{+u-oI#JUrGnY;ETH=r*wq zp0Wno%d;`PO!P4Rt2$3fKf;8PDn`NSZYn)&Y%HRGmw(>SyUy;j!>wP8k5M39E5$IA zG#5d*wE|&$;C`By5CK(Ss`{{jKmZ$NMFRrADL<3dV*y|{&iy}73WEFrLbRi)}Da+9Mp9 zMf%!Kt0hI1!rgNl7-#V1ZexM%#{)<4g@mDsca%J_%QMmy;t-}Vr*=z0)eJt4c zLYGZQBQI|6l}+dD)_M}>NsaPNV)nj^zLgPGL)U{dvniPE1;32et~FaK?ENcJKH!@v zwJx?~pLZ>@-`FybRWNE{N)3W*4pEB~uEzC;$nUewBwm-@R0TkE{7?s7t|etrvbnEN zmYvF~x=^#K+R{m*yo#7K!PPOGok1$FyYh~I<(p!h2^6+xp%oSE_%+)kZT5KjG=s7k zU>}lLs{cfDn3dP2w)NvYX) zwigbtX(u5pVf^Ix4d$ciXJcaPS&O|>d-nZMlUJ}vM?`C6%tYE`nSW3>Jn^xm+kBCK zZ@Ex2db2X;%!!CCn5x-^B1X#uS|<5M!I&SRe@TiAyr3cHfgZ~cor#>)S-*9tN?1I_+}f1n(|dP=rDRc#n!?!+r$y-ElIO z>Xk5X=S!vD!XYan`HxFXtV#%kEw}q6Vx)!u&!5??C6?I_e;P;#t;=^XVd@H;t}Hz^9m-< z+nAT`g=v%fjji8mK8|~1HnI*>0Q9PBdzy?YLBe5)@P3p%1_+qqtQ$%8G_dypSfuadG%5=!(vmO-kgr=W2$ zSh)R&l$y9MK@|ZNc2hebj+p*`?D(h%e5A?%8D?1}7MN!+R38vcErY2qj+`EHX^8!# zZcfVCNq)ZZfCxn=0-1RtTwXPyoJ$ajRl}-@j$A^9*x5m>v^add^g6_YrhHUPmnm7b zY~aZ+#I27v1VGhBAc0-o!Uupqd0dU*whn!n$P*LU^jsNY4ywk-^y3mm;50TIHcNPN z+NmmBg*~e>M@M&Z)HI4)i^a6O*_Rap0t)fAfAhmG7UE0~?>xrG(i~*a!`XGpHDrS! zotWQMBt_nQq^njy7V^ZptslsVV&kGz5nJC;R|~ygGM0agAF+i$cLc>-UV8Mtlon9T zyTgDkNB~9@i5KPrwp%WPY-k!FDL=J>E38TNXjnw^B&~9HQSG~Va{=}v@DMM|-xA}3 zN0yM4t~`==YT`gama;f^e&`7)AX7d=x4cS*@99{t2(9Nm80gxA6V5TV!fF|Om%stR zXdoU3?nV0RkIz2icmQSCR*MbZ!Uk>i098Z|`$)e#Hlj4Dp~0EBF_~%h4!a@ET$Ey0 zg{WaRlAImUk@+Pp{yS^u_oz)N#EPUP`!W9{))W}$1YE(g*r0MGq+;n>^VMdV(1@mE zhB@vdB#hQ)-FEW$m`EX3j3LjWr;}gDRHI`=u!VTSqG&__RCCaQfw)Sx;VNov(;Gtc zk_fXRL~Dr&4%NEb`j8NyYC-L_srrK+)iHcAG+qdC1gg4mQKdqx!z`$ggH4%5GPt;M zAqo;sRGU>8qG3=`FJ-nMh_38NC$RXsUc2Oo0&AD8kVm6I|%c>l_qHtgInc z6xAxTfUs=#ZyenQ$Hfc8nBmX(i4t<0n3>JRuX7T!!<9Qocm{V%HwP8ar@SGm2mmA9 zHbn|PvoeMisRne~pvf~!6+`G~v`t>gH8l9Jq8!$DN`$UQ; zUy4w82Q(g(q!J1&g@?sy&;Wb1{?A&mdGs;nHL_3iy`dF?HA8PDU$>W|kO5s-n6FG& zNzL=wg_Ind==a!asdVCrLhci?k9%d&wFMFA1mT(a^ZfbeEwuZtQjDo?!N>Fk#B$@@ z$2Q5Qg-3>~l=9dW<68D0B~teahnB{?K3DX&EgKX=!p_iO1U6tH5xIr5Z+-tm3UXLq zxEf!s-kr>;bdt;PfVE1o&0erzJ$NM>zp^4X%(mPa`#7yNG8G^{9QoaLGZtpGC?gV{ z2)%%(1U5fRX$fo~ktWnAbxGjjdW!V-t-rlSDj5=N*l zA#_tZAn)j`3Njp4eh|;1!K;MiEFcpkhP1K~F+zj~8_pN=hefb^H0X#l&XgZ3^C7E{ zS2b1*f@Wc+K+so_N08OwlBsSz4Nil+hE7F1Q znpF;!!jB8_E<$*$2<#=>Am}z5w7AX~W%ee=lL=HS5473Eg46Ru~7te zq2ZTE&{&S5Pc?+sL`Z&MDAga7N{(fGI3811ntijhvnZvwDg7EFP(LD#maCAXi`HH) zk06|}9oQMH=ba}ZrMUak1_r_!?hPM4md3ung{NRHGoo62=B11}BjrQyO?VCI`abBt zi~cghvi=lHXq}ZytCf<(lGeI=&E{m#15r;+;92N0B=J(`!;iaARa2gUTN71|J=mGy zYcm=^jEiE{upZdcY=(tkI2-6g@;mc?H5rH#)yT5U$bdDWm!0l58cT?BnElQYZR2rp z_B&yww^4IkchBotAtHzRO<|pFy+a4sGspM`(yAu-3hEvkFqg?Z6s1ziCrneZxLLHD zSaFPli{UDQxLcKGu>!He3QbusiQp4enBn5&&dWt*VF(Hx8dz@K7$|qWJ)1(dohrpqdaUPvR($IIvxGxvwO= zsg5F*q$r>((`aZn0Fe<6dBcG?D1{m^%?`+5y?$&e4SbXh`6EIO2~}b_Fiy4X{shP8 zD5P-Yra6!)DQsP+qwFmZ%XeZc`K7@ahy$n z7gMZK%t6ip<;J$*rvdOeE{rNvvgd~1zlD~vA-y7OUbt>BR%a2BarP0a4PzimDl*(} z`&7=Gm{66pVp`Rd8gXs6W5Zl$*&-nN9l=y7=gB#Uu72K}4lIj&^n>wCL^tQT_YA`b z*?xF={{2R+xpBn8lYISz4=HGr0VP_=(>;dHP}*j`sNd>~T6TPjw!9XnH{#!g>R>$V zG%kKtlY)qZawbSOwXvwt{t)AqCB9 z|2ijIcz}A7!CL<&8@$*Gi?u_;po>D~KiJw;X;@Z=n*vbl#lMIrQhD3Wg-m@HkAX;J z8z-w3FVEu~QMs-5Ujt%Q3h12J26%ixx0JxywqEfsx`u-*=IZ}Lb&nS#H%M^_;q~!! z>{(cQ5nZQAth~}76JUvF2MINFWfq|DZymK2kzAEnF+-|E)*IvB=<6ibl{w-gzG0xLemc9xi;1DR(-c(ckkMDlatD&%m)AJKIOK8|#d z;8UpJCNtmJkXQ~%#8zD)A;NT2204&g5zdVRnxkjlnlZJ`4f_MOli2y8S zk8Ua{ym=3oJ5N$zpT|7Xl_!Nj=JID6$cKu{ zWkL45V#^{^>XZ22`f2O=mMLfFtK_4uG?X4pKQ{WvUm|)>=$1N29CV&3KGJ9PL>LWu zE>f?Z$tk+O-Xb3KtfY0KFV>xXclPa6lojV}p%Egz(!{i0-NeOmNqV%KdX&5zMW*6+ zNi|OEhvm2?`hEZc8)ANfy zU!>T%S#)C|kt|jaac~>L$N$lkM>=BqzslK?knv(=;;bA^ggP3IUsaWh7pVxizI#F% z!o}EnX~Rj8Tq{SlTqrlUK|Lild-^m6AI07z;lBbv>p~cD2RxQ5YlO$JxRCxyu#_a< zKdA!Sg(dN+6CpMfro6p1KFJ0VDnv19@?m1tF#zzVlu!%^QL$}5#)S__l?gjgw$*aS zg>VJ{>ji|YQ{|c~V5an&`)Bbe9qOrY0-g@ds3v9r&0mtBrblnxPE(#>Lu#b5t`YFP zNt8&G?+m~;ZgNpWwXBXNU&8*98))+(#s_kPys~cy8=F>or__gMI%>FUB}(^NT-h$8JkB3Ca95%)yf>xF#3MTD&&V({Je0C40bsT@cjq-L*GYP`=@x?`&l6+vqP zY?>9jMauYgcNhA#(T&jn`}l0L^>vqZpR3qQJBdOa>>>%3N6RGw6_HLi=A@`{=`Rl; zlQOQli>r9n1rt+^T_>%l{e1lKP<|et`jf5PSuZP+;4riNM@KMobc`1r?KP{|c1yvS zj@JLAV5;LJ$bNi#P{pTO(Kvj}BwQJzgSuoh?y5$br=^_zgj7~bhv-ZX=aYD)g)4BlM2soOKUu*qHeGzJ;Km<}g8Hd|Se62Ublbalbu~FVy^d zepRTm=x=G}R89_DVm`z+cwVtj9*fQ|GutByg(_`95wC^6N&Eiuj7d=Yq2bM=2CvQ> zP%-eHl^l`OwNz2o76vcapUGG0^=pGINSu+gX8b;V^t&jtH}&qN_>v39mZBbcpw zrN@^xh0PC@%rW!AjSjrsI8w^Yjp{1Zd$bETWxTKcvf^Rv$c5FFr#JMTntkT$5+D1m zXa!ky!-LE(Y_s0i+?}q{6R^Iey}4s)Vnk|l?6mz$>dakdHS1mRLo6Zf*i3nDP2O3P zvh}#h8_pwyt7K(p@H0qs2hRhqnMC5Ee9JBA4j=$&mXC)Oi$G|7DGef@z!B#Vqu3IJ zVLzKE8-QV9k(O0#Q2y4TRj@N1vevJ-Gk`jvy1(CYK#88Q)?d7Hvw-fa`WGlYvS_V? zOb3Hmi7QptjfO1378%zlF*J zU;jtZc|Rnz|9|{21wjQTjuiJU965>%5EVy?Bh%EvtxZ>@Wd#U|iitC4p{ZFm+-aF1 z?$pGxw6d<6m6f-Pt!v|R{qp?_&JX9D*Zci?y`Im$-1` zJ$3H@(GT64ILe33ZoT+2VJWJiqRq4a)O%TXss`rMW>r%stlLkDJ9sE3wAoHTxSZCb#?tp-KQITkT50w%Uz`SnHq-n-Xe1-I_-G?bMn zt^KIkos|KkRZURwFlNu@u$o$iSA~sE_?$mXJtUF!G&oH6yif3GD64Ha+@})c9+AT7 zp34}n-8c7;_q4D2c;3$ut_nl&?CC^RsQS9(!wH+pnJ3Qv9Mxh^f2#7i@wSe{i0$u& zpYHW|R&%(v_oSM}!3fj62Lu1&<0AKEs4rxkv5BipWrSP1jbQivuUy%k=BTN)ta;x~ zGbZ6G=jeXV$#84}UE@{2{sEurmnE>}*JlzMW}p=tKi^+F9|3nF$sOZArm3ta5kPXx z1~SPlAb>^!w+`yEpt@|{K(1-P$}&=Qc=5NA!zu$J-Qlp#T@R$&a|;9Qce|OF|fP;NIV@ZEv8HH_)S|G72vPg70u zvbkydzw3UM*~#)@lTkQr8>9ul%gR&XjZ0J&tWt4R-WC}hWYRW#_JSMg%!l~hkyXQI zeIDlpR}86s$9l>gwn1aRoa;6l&aH2o+L?=~492L0`t54yAxB&Ui*jT(^w#Wxya zfeCbv+)kOYoNZX7>170oEUi)(33mhW_dD$f8a+z4dIWQW7-*mfukKKVaYDIkaBI(&d`7FB94rKP!#~&Oj@UM zRODtN-SUrFJ!H!Q?aXcj#jQUn`^{N>#dx7>;VMb)x0Q8cn)x$E2Z}3?Q0a&9j6JSN{IK`m-uW4qASE;!;9P z@<9xhS9~-i+vX--Wd$s5^BL<`e@MvcPN+LLI4i!J7o(o@Kd!Zd!GOiPZz@sB+1s+) zg6|%ixkatv;`YwuL4J&$^|bdSmecanRo^Ih5Nedc@wDDCm}THV-gvi!>+-{2aOkJ} z*!-8#lJOshPZmK=H&L+fZW$LB`}m}NqG(+M$Q*?;h)Bs2t91t~u+ zIu_paEJeWS>l$%5uyi3{M@NS^1a|bE`5voyS8V5=w@z-Wl!*%^clu-*fhTPCGg|^T zYZEP4$DR>2b^luC^!evF!=L#qmycY$ST}<5s`fd9UXt70Fp%=m{;BWg6JHQ?KqWVU z{lN&&QnZhRqeB4h4ml=VmJ!wIEn0J+??jiH!FQ0_6}^IrKJAKxztC286JDIr^;g2^ zZAsMW)+w|ejl_l>d#!Sg!(AH(#O5nyzg)82FQ9{GV49CYC7K)u%z4^NU!HZ0urUQ3Y>^(>_eX4%LR9-3UL zuaSv?25wnGm^bMoC`m(BN7fPcRx!9xGkL|sA^H1PRC5gN>nwRi54<;#kW_KAFecZ@ zWFP)8zh8=P&paD{#Q4nZM*48;*%`eky!3h1E1v&urzY!1T51iux{(>UeYjgTSNT7- zH|<}_i#B|tpz`~|k&9vt`LDruc!Xg*M_YZ#<-KJJ0cJEGQ2v1LhDjM~AgwUVtffop z_4*2SOUiKCLevuWwl~h90O8GxE$+8LA4mz-F&$&6O!RQg`y^V8+yUj3bXnSJa?cbV zjP*@@acax8s>aHTs4Rk;2n#|s!K{~Vw+`L(iA2)Y(CEGX4+C#$XM0r`toYJ7qPKLU z%aEIG3c)$BhJYXw?RL5KROvwd^R891RRjcB=yE%w=TygLQsR{fNx!lUEIzKvL#6QL z{#%zE(*jcdK|;06ikBZ0CtrpjDEI{i1nI*7VP*8yBbY+7J?13Ps;MB+XN6tdT-SPh zmA?$~U$f5joPIOo?wlt_Oe+2N{wTySQ%~;g|I9oc9}iU8w6gMAJ1NS=B#r|rj{%sf|Fa08TEH(dT+VNXy@UAxC#WE6%HC;A zO%%d<5pr3IDwII<8SpU-se>d>5x{!!(BP!tV7hF+10yrx-VE3kA=--$Vo4Hzd&oN( zQ}|7Rlh1+|nnD_vUGzET6>pBf(}K?LWpBRBac&gqxu4Mc{!DqVk#aRx?<#3qoh>K# z>9Mtv!0!X0Ry@7gxPAYU(Z>b4-NULXcA6pj8o#nV#u*CZ5*xn1dchU7<4LGSLikaC z4+ca2vP9JbkC+xB+$DCrBoq%Xm*fw17imPcqEGLG=CRaL0JNtDdS{^4PRJ<+M(M+h z&rUlh54w@y($;6?L*&jU4 zK-K{0NvV7jfWCvV2~CE&x8}2=p~nIAW8tZAiJVA+Zp6#ok;tWw9dH-PO^|Y{uy8gm z>d_MP0LHO~oEQ88Ei|o>LRdt4!2(~nGb|#41yIPsfa`>CJDJoH z1NMIpGh{$Vm?W86&lf=56q%+FWD;z!8f!s6AWY?9^ESd8`7cYMWMz*xH){^ zW;EN)YDM00BWvx)KH?B(EtQkzhH6|)_#9_pUCQz4aanOdxl*aOFl53IJ99hPT1Fe^ z>3xPAuYerCAE%rmRsG`-wqR`2yVr(g%9u_u(I@;n$>vjS_vpxFrI^(f@oB-9imf0k)^Tm&l@ zT4Z71Jo+hAPJ~Fxmob{BT9cYEwVNMcHPY0KeX^h_*d0cS=fBCxhFW{y!vxIK#CZ9Z zgt{=i=G3}|NLjuAg9ZMS<*1qV*!-nT0aSpO^;N02C2}c?7JK87n<7$v69Zd_KtbL^ z1q6h($|=V=^mYGB=f5D2m7Z#CP2v#{JG~-rwAtO>u;LNa&vH(no~1s*GF}+LmY^Dn za#8CzC4mOQBpCi7t>QNsEejXKw`QO`u}9rOWF{Dm(N|`>>@tbzw2PVaa8Y<_yjuiS z9e_qRogsdKsrkdxBxjfyxxsx1uH+1tS)49v7CT-WGA&*g?Ao0a_x_D9R1`36s&6VX z27Q5S@ACUtLhk6MT*X5PLXf!-F`PtdT7;O3AjWhkMgo^e5T_)NU;u7SLS;(8jgrI; zKw(w{GsegUrqE%byd~lCd1lT?YiNehrv|UQNq-A-OG0-D93Ld1=_>N(7_f&3etQv} zE`nsx!C3?|^F`RL*26b!_f0nGU6t;=R2leVrtlB#SW#sqgYEWXW?$pu1V#$yFx9>6 zEh^(p63_V zJ+TNojzQN*Z0DM<3mPzur>BwLPtiuacE`Y=lS_I!wUI6@&L_X~U zdPrn9%s?I?!Q2+bNd$-s0J0a#-xI-)0IfNdWc*6uFYNu9e7z{P>wj&B|7+9xZF3A? z8WQ_wM3d@Xn>)gY8a-SVRNG6@gjRlbpjf46{lh*UZO1)sZ=V996*ePYRMy_Xzy&@z z3l-?bB-D%XcAk`1m~P3I!aI4#$%kuu>TAat=o!orby(b@%gT2P>+QOXJ+lRL24Nzf1IFV6F(J!&;7B`>6JevkX5PH=@aQ* z;i8WE0pyfKjx9lEA4ZQ3b=L^c)nlD^z9j3$cZwu(78mVye$~9OuDJ`M!5xFO1w~x? z9l?o2ZI7sk*}hzX?Qvw5<9EkzT zCPB=lh#|V%3HrWhew$UnD0_|UWPdEu_UNG+=iIWWv1L8?#)YGQ_KjN4`F|h%^kih_ z)$Qmb%8!1?w;FHpWWZn8+i3j_eLbadT!@&GqMMJ%j{~TJw$~rG)Mg@3*@xx5@Q4vy zQdLcE6J4IS#`g}Bf0>Bpd3TI35f_(uURtT@hvd&n&{22L;e>1($EEHsR%UbKO$_KK z9bVf`gp;6_Xfg315n8<3NLZ`(vec?;tF@Uc*t}yqtuCK8{1Ru=e@TRaX9DQ%o;P}P z2cl^w?$FU9!T8Si)gw~43lp(kgFdu4InB#uw4xCTsQ-u3;d4(#);Y)bjEj(->qr|E zNJ0>fCqamTJI8o%;<7M1bI3D z#xvmm*`OYY;LgmwDR^*o9wfvPI_L^Mg7>J1ledr{W(AP5B>BtSSJ5{Xglc1lbneUC z6UJkY***)8&c=U=v8d{x#K&xNqAoC&OV0Kko~zXxZATp^!TpP%td8S{9i2`<`>L$u zG9}{^l7hU}X~$o1)Kl6v?eUR+2amXuQtP&uowvB6{^D&40Wl>)o_fQ}TdYnI zA}S{2CS}*nmug)BmOK*wj?lun1LpGJVm7(6D+;-@?(SH6j~hPMKLN_a+^t2UB9k7E-Er&98#J27fO=x!v3|V+^W%2dl?vXs0pQ); zi4NQ?s6c?e&qUNE7tHBP4&Wsz4D`6<{M~B_@@%?g9#g9yq|<+VPZ0-q zC(YsTjF$0P#Kb4~>g+n^FTE<2Cm15IM}i1sd@#d+at~l_lK=-800w3|T(NJ%{I(<+ zIEeb^4e=9C{Og4ueP5m73mz~rh+p2DLAykda74Y}!EzYCc4I_lwa0~faHHB|?nnSz}UT@=3C zjj39@6@b^T?L{ivL^WX)QmM;+ULvzl@X4~`3hVt{7{lPodG+WhGwfV3+SxdEWblwe zbWMnzb<`j|53bXj%$Ux1+x?oa^)~X!Ey!HT-ota+#=WUKmuh$B9$WM+Md*EwnXn6S zD;={jo(gJ@oi4WVR$qH&n|Oy|{p2%srXy_b_^zxK=wsW!<9un617flz_GOD{%hu)z ztm&b0bK9rr`S!SSG8C7{jC@z%$n0%Y+AI$pQT9Es6h$#zE*d03s?Gh1fVRyn>V=JC zZ4JDyD};K4;zeYWCRL2c!h5%W1(V*fZHC>|FS3l#<~4vJKaBaDo&QNJ$f^wM4}*I; zr_h!QVwfKWRS&xu`C+z9w>-l6R$&_uf#pSSb_B+SkK_~j=ueR5c%=Ij;jJLc#p>d< z&n54(SL=BOcX@LBw>o{sxJ0YSXv)?GnmE@@JEGL_wb=^zv72d{4d1A^f`D-auYXfy z^XjZx2T2{=3NtT*M5gJV4SkTRPlYfG6x6kMF91!zv!D(F287|bD&pYMoX9z*6|_~M zIlBH$C{aGr=mn+OdgaAoO(ZqU{no2)ED_L0C>^?ItWjuJu^G$GjXFUX03ap;GbY?b z!~}}Gel6!K`1CwAdahi;FAs(vMsM5C!H&$P$6-w_nOWGxsvk@UJ&K!AH%Z6k+!xQ6 zhX#rBqw^0wH$Vgku_G6!&JD~jN7}ngT{IPM7%@uaEvL%;15ZzO4b!h1ceAkhH#)|B zz4w23q}VTWpu^Os?1zL7FXG2f`;If}5}je?j+HkTj)t-)7W8u|F@mci`Xi+1trU!<{Svrh4d0e# z7 z>YQcXc;a=jpyUe@i+)qEsJ3)(Mn59c72_Q5s9Y3=`LL`4vOYz5l)mDds_83|QUu$l zFWMcW_NBWj{D%I7DsLYE2EXq*!9sPvVdL>2XYX$;50!d$z3p)GGQk4rS~c6Ci`aGN z=#pc#7e^nREa%uzhjg)2W2koI2R(lX?4f0$4WtlFOblqVzV12)RvdW(2*L4K*U^t} zI1xYXmxv))lq`lPs0G6ks2kSstc~4U@TWx(a#K=v?5-ub9XvAnjSWcNiUB~=MMzsb zGyLr#z=SubVk7~v`76PuAhh?US(aw0m2siIN|l&b*O6V&sCqj+bc`}$;~I6uJo{Uo z?$=c{+5Atrl+F=1d_l3v?a>`{t_9^?UU=qmT&Ed&4jkJk~K0kiUeK>vFDzllA#?2>B3kU(Tos`$mw2iWoJC4OqlY~{Q~PLhL#ZehLHemjla zqo0RzeY|+3I0+ix{pixaMxM|&b=5D))3jq*s>l17(@4*OlhLQMDAoUxSO#8!d|TfJ z40n3dGZ2}{bhCBBZt{JZ19EBsY>!c+CWD4?RDeL-5MHW{LW0p}4w+v5HLLx$?~%um z9kunRJ9m2J?m;RYnf;I+NJsCd2Zl=-SHY}od#XNRcOWw6hV-^c&k_{+;s33?0wjh5 zY!82m_O@+N**FA&I)mW8B4`$LJ(O%T$M}$x?Nllgwk8esF3@>9p<<<%Um&^_t8fm6 z3P;b7i4K@7J)?a6wdG~734;wn3qx!~fYx>}6E?=g`mHk2_68ndEMU81s#{Fy<1Qn$M427f0OaGx03k&p_#Z7-6GHxG zXfkH4;~`04I)9y4VC~xSgr05X!5kln4C1@(YK;ncDxJZQdCBPIh zqAu_%u*H2Hs+q|GuaAxCq}Cn_gI1p1h&b%!tB8nM^^+AxeBA8iy;)bNKJGUNTd zB%HON`cPxPyY~*x60&V`mMTS6BNai9ipvLv4Aa}(J4;*l+1$2&kx}F@;jb`vZ>TTY zTHLK256`K@65rcti2I}g!odtG*Q~&F{+JT9cGk%HVQq14HGOgD)#Mh)Anogw%cYfl|DN#A zirBak*X!in7#joW8WCNI_|o_Y5Jm3q8R8Wv=T*&!&<&vlGiGI%K$|^7i(0ial@qN! zhEfINkfy~kr%wF~)=ZNnvl?St~C){oFR?z_IR^GdN^UYoP zyQ*DwTW+QnDSom&qujb^1|ILd+J(II*OCI^p7X~d!sdsP9rmp*&K(V@Z63X*bCuWs z>cKl*!5A%E|1e{*rwFX{%dS;R|LZb;$&2r5nX7p}&TC2>&2r^k4TA2eLhp69nbun^LL^I}^sQlEvA0fJqx_1jjk4>agYe_sSu0E&RDW&^NhnF0ww11z)vq&dcvapN@8*?1m% zxE@l>0B1Y7sPqgY1c>dY5dfVDVK9*lrej`?RRSC5)Xy+ie`m-Gtp<<@OuPu9;sm&Y6AfXBv+VuiquMjs}9=Xj4eu4DTSp_zkM znw+)r$)f*U3y^D-Pw}+;rOKum{&^MBO;*)Bes|4Jj^n)OnoQUa9Y9<+yQr;5!nSQ@ zjxEx6P$f--R!?Imurm`zmTqFHt59l^t;NgfzYtlI{yPy-ZI|;^4LRBup*ym^+;dB; zwPxzj^bsu$SCguA^9=6(W5ewS4cZUf6Ubk?F>%Sfjl2gywO90(#U zQu&y;rE;fhtB=a2IV?m_qqoj(_)?-~jJ%Xm#CyC&J#6At1!f1TugAF-mBM!Us=7O2 zXav7IObdS3h=)UfHOIXDHo(1AYsH{+=Gluu=hPpxxj6JZ0ccq-_t` z?oHBFy~?H~0@$>f6C=pFHK|}&p+c2j{wE}4ylp(A>N){~si!tO-%~Rt`aeu8RWS#f z5x|1Rdvfhiy)v6hl?#`aw`}#kE}x9Ov(dJ@SM}XGMdiue#2vNPi&;`!v(fK1;}V?` zPQY6dhp|z7CV<0`UP>~srP*BH$~3hK%LY=~Zv|a=_5s^4c-1@6uJ9$8P;DpgOxaT> z?7z3UW9WAWCJ+2|hm(BGlin&;XtcqLDv+ZLC5r+4JInGIK>sd)j{*2Vu)ILF;&`ee zV9{S1oLpB!Vy?(SKA|B11aoEXu62=w=e3h1sdw9g=rW(bXu4X5#6?8TvM2%&i1rFD z1fHm6+a+~V>1xhe$YVDOrtov^wmrj1e$q+OaI;exTD9wOkFf!huSRQ)(YJR3IG zVBW(&(lLqrrCYO6Qiy+$y_rN~)!U7Qx8EWPeZ4=&a!YENxUgCTrDd}ax7<7RnofDQ0gPmIn6F}75m45fa zj6wxA{+NCiXR`-JYvsfzWW!FfFK*%N+tBf4LXrL2QlzrnNenVhab2W?>xT(z1cY8i zZ=hJMT!2c(#7SowJ6d~@N>`b%Xad~Xj7=6QIhcfew1MM&_j@xh*j_7NjD&JY%{=C2|Ns$@?kH#;?FrrrJ z3~~Y-^x7nZDIw%Wg^V#^uay*ivjZ>~h&G22l1O&vl$1kEdy2H%IQ>gtZ;Sxc1`Q-B zH@{O1Oy@EXQ4Apxo2ZC`!>I%qBWWpCu!&rvX5xDJM>GW9%0Oc%$%>us6Z}CA9-;7El^hDsH2=y$UoVT@a@hTHX z;~Xkt&KmO&1eU3E6B@70_27X+y48-|z>StX`(Mg4^hzU>^=u^ZZ z-SSSc;^l>-Y|fg1arDbtJnu-LR1fAW3FujjNx$Ib)Zry5`gaEVC8OEtxl(fpBzGyw zo008;XP9R|2H$}ldbnU4nE4`vL4dinLN`cjH%_u$Bv@bE(t=Z>T(=@JTL;;Fq3PHb z$H*;?$%4h1PqFkY$JJ}lUf9<3Ea`me9!=G6G`0&ax)P#y^t58&VwUH>Z(>$Cu^YASv zYcQ}^eARN1tt>k`u-+;-AORMr3n3;zJ(|nYo6Dze?3zbS;ZB3y1zb~7oc#+xI~{!g z2+U8KWsC5U-!%4BTxZr53`vr{eU+Yk>RW31^Pjxe-?a-eRc^C1ckAdqi>fVvpq>6L zHtt<%{I#&{7Pc&X5yClSQf?<{)4Xs=_c}Qw8`96!%TQFDx$s`Fgkf>to52i$tTA^^ z6myeffcW|z2VKHJqvx^YkPm+8D#ksE7=P$PWR}A|h75n)8oU$NeOYd%wE6RI6jJHi zMxhr?*${{M&rD^gXwmQeg|CZ>72TXT#MP^(s;&EBy3U18MeNq+-{_lBhO!N#8B9*C z2Y+=ECCC*Ee$R}oa&GP)~(v>&g)K-33 z@yNlM_>3i~BtV$yU^rr4#F6=0uPoSuY}R9mid_l4*JE3r5yCnXJ}i9rYHGcgl%sgU zE$yl#l*H9-e45uh(-d(2(GBPCAA_1g?xb+#gFg7(q_G|~xUDH@8vE856|GWpUl<+! zOMJ5>_@)YN0?%c2`ABZY^5E84#%9kuym$?-0y@{qb*K|gui6`o_}9XU_e36uJG8B$ zukdX}u>&HYb!-B`vw<82m=SCwaAN+Zkk-9WlzHbRWJgd4g?^!s71txktRc5p@y*%b8M|PMk|M! zOZBH==qaQLpV$Gf$vq^ur)7RoOLqfV)rPqJM9b~2 zs1s|q1A3MBT3=oJlCA71Xe+0B0?2(QPF8*zQg0+0*4qo1EQHlK(PT_}P~n-6d8C>0 zrmZIiIsVuZok-M&5`+;ux!4FKk83IX7Hm$TC$U z#Y%tB!GLD-HBOwpqA3LN@z!l$jEQ%R>U8>ga->``Zb#CPw??mi&**W>5oBM7ml zEdwFei%6Stnxi`I0R0tiI+Cz)CVTq(DeSV=rY_BZ1x?A(jy2MJM&IFvHGc1V;bRO$ z`-sR`eTPq)KiYN@?}{e>!3=4}mae;A$$iKW!&Phy@~!w1n_<;vaRWqMWl2Ain64^D zP`RoG=G62M=pPlEgcw)Mu|8E2Sb_F!ShZlXn5MF)bbwSA43=8l=tM^c``v?oSLy0B zi!Hrv|F_n8KfB=vW&Ve-mZ1u6RzIure3x@n{ulZ7{6D|PH`5+l)nZziVU^r%x~qA9 zHH~4f525&47d!L@-ddln_KsV@^9X$~^Z_PR59q9S90QShir6|SPGWKITLs{uCjWp+ zq0T!2*B9Zcm=AX{rVXHEu@V^+$R^zQ2q+>P0ns~TCg)bcMx7O88}|rYZSr#5R{~HP ze2kJwm4p*XqAD&WQ2!OZ-({%`HK^IBQsdSk!dr)|hcMDO+z8VMIpMqkNMJNB#>KzWe9s*7Rg{{zALczIW(? zI`bOjQ*cAg_%NowR7dMEhFe>+Y}PIDd|&Kb1RBZ$z)Vkw4q?X@%s+Cw)5R(kj6Gzo zIU;b3qFGPMc|ZIH_3R}H{YWSXU!|yy2F4Q!ZaEoIsq#Leceo~kAJMEKV?qV?RVv+? zPcwo$Y)~|Jhq>_W$a_c)W?T`E2m0hpL;cgm`}7? zm<6;kP-dGYe{H(FP_0ZcZt$oa-}*;#^Z0Put-t9AbPs} z5#oAJ8XH@R_jbvSEZAzH&%d>*>vDG#bukomNZ^3ja^GG4`2DDZ!-He#;p=&f)}ml+ z?wA4HAwHwP=`Kk3!2EyM*&4acVr#rf_$&RfNM78jL&tXlUQ z1>o7vlf|~@p^5Uf9HP_M<~+F#!DbNz8M4@0BZ<^2^yZr07Ou(rhp_=aoU_%VFljB0L@eDO<3WLlb~!z<(- z>q(W(zgH?+ACI@&^PMjzU`7_$`FF&P6WHJ!>-X1}wKo_KyN=YM?#Ut`itcjxxlfax zBJ0A_pO17Qrgop*TZ3$Q;uSv9T}+F(e*Qp_Zcn}C#3`7DRSwbHe65J9e9hLX?BTwh z4trZ0H)(~z`=EN83)1#IY1o*?#aJ_x!c1M_%;KMoOOCv@;?&-^A?x7mu~y0Wjnav~ zNVQ@6E*y5>b-FNYWX;fedJK~z7+Vnp-MC_ZICbo;PVH*Lf~7H|Due-6Ut{EK>%4RS z-CI+`&&(W>y;wgjLF}GJfDvg8r_y*?J7!v5f6X({edBY0Y9uE5(<}n`!ftBkwJf~~ zJi?|z*rlj9 zrgk6Zk~Hc-S0Obay*baL+~#rPwW(DO3AmWD)F*Fy{FSo>j0E5s18@5gnoSu4_qk5G zyGPB<uC^ zZj-RmcG=|n6Adr~+UHvb)kInR zdQogIVm>1@@Y)vCE*TE1s;-U3*fW$rB=j2>YS5Km{tQYmBWvg!@Q+|1xBIR%s&B2- zdV_l&YrlVKf!p&m^-#;!=C*D{=Zm z<3PuAkvp-pXSHYKU!N9%{VA*SH)qLC7Lr6xrKh^MmM7pS#0L&g(1=-1Ybs z*IhE;@qE238nhH$7g5(X_B=Ou?gs6}`qQK4FY+4@$`5Hz7NVDeD*aA?EX(LqwPVK$ z=#{-O5i`X!+h+)I@V@`?^bzb8C|euxgv&a3(+vqQNGkI{>81|=w_-A#$KRU+aGq}K z+qy44v0Lnw{45H|r32KYhni4ugFX8jp7Y5+%D_l$h&cFpYHrQO@3SUUH-_a95vWdN zp=Ir%TyhGTJh7_?ZP$jHa^(06>PB*a%*MUs*^%^ zeb74SovEne_x(bDoOeev+ZU7nze(}7g@fjQ#M?5#raDjd5V@AExIMFAo6f(6ljGWm zZnWq1xib`Nc{iejlJv{O9BsIVFx-||YOQ4&fP)=>_b_!wzVCcg+OsG?dGL2gQ{gyF ziGVL$a61I5bS{4r5yL;44jNlrC_WpIiU%L8p?hwEeQV8ElrthwYOl9y4(zvjuAK?C zLMYa4*9>QZ^mK4`Ek%cjaoXh?f#(Nbg zkMl^6QTh}HvOv$7ud`}O(VbB;pIPi%oQvS;Z1`7y*O@5DFB)F4iqK!FeH_-~7S6l6 zypHI=j&IQ!e~T;eOY#L6qQ9x*&@pY$T2)#0qrgidPoDuzzRFh;^qW0f+X(DtKcHzZ zR;2;(Ke;C}WQk~5!OH^{JqL)ipav&#rdytC!$oS%CDQ*cIJ})xy77oAtDk75n*tL` zVd|r2KAX(-YhPHq3Y^3u9KH)(Pm* zP&Sib+rH`FJd-`>nxw~Z#E_=M|HRnYfo@s`DYZLIliRDQV?ZgK3>_jFN@Q?o_P$3H{2xG<&wg7` zZ@LH~wHX`YqKG8U&h-PDHPCONC1@capIJ}!F|V}>Ler=^M6#jIV1t)0ikQpEt+2Xu zuzj(`76aB#5Sz4$O?ei932t`(fz2C0H7*nw0%UWSvNcF-K6|0cRzriMf5n-XiV3QwKbMBOHpJewU~^$hXfefR4;4?OSbw2f zyRh}sEsqydh%~7ooed5oz%5&QirILafq5s}de*?$i353AZy6#rOlQMO!2JPWV;lv( zTiWZw#!Ff7omXLBD*~V`njtc^VNG~u+4b5hUWa@bxIu0FNJ0>sJ%$Ri@ksK4Pu*~5 zg@7MdtXdNx59rE@|64mGL*zvlHqn?z4Yj$94FTG1+(=PY6_lMwa0JL5^&>q13W)I zeE^Riv1^bs#iI#-^6l**Gw4SwypLVX(|9AaX=aLi#=vs`@|gbkVfqM%4=-G?F2Jo7 z?yqcqN_*AMSfcKo{La(-a`SDt8=aC1HWESqAWLZ?ivy~hh>lg>>$_(MH0Ce0bLY1B zly0vj$^HLUh@{$=NF;jU7hl)<3~APo4LaF+sua9c;n&%Vl$yEAuQgQYPwozrxcUA$g)*uu8R<;R7tbauO&BU9I;NBp(S@N2! z>1+!)8G&P4>#)&DfUNznT$!^<1IY==Dh;{VOg3Rt2u4F$3e*Agd>V08tic2j6@Vd$ zsguq|KtYIblE#Wyb^aJ4guFG4W9dULD54nPD51Yukl*hNr69eH$A&c&(V+4z`|%w{=* zMOb7hcnV>gG&jl!Wi!&+@zoU5L2(W)z_x;7ZklD&V`@iFHvZ#)*9Tvag67l5`)gJ!sHu#Sdq>A)6yF_KPFkzCW+f2}Mkx5_0J9b@2hDp6CyT2o~cGQEf~ zACN<$EwW7@z+NG6s7@43B6+QcY$=V=PZxhBu=NP+f4FlWl0UosVSXPIDfMuuX}W+9 z36lnZFeC_-Np!{7JZ?P?>P z#IJoy3clRFucgPpK|v$F!yBr%`n}I9$p8WF`Q3!qVc&mo$h(B?^Sz!h&-MjrLjEhY ztc_L+m|?BCA4e0@TMMw90qZ_+su^pXcB!I&gAmWejVWLva+v2C`sS^y8qrJMJ$HE?7s{?9qoXqLeJq_BXoDS7P1 z20dFuvn>bM56Y6|vNi1YA0duJSAbb_i;*EC=5FvavmSB8Y-9T+YpViDQr|T>*ZVYx z?5MZha2rB-#99r8LE#sOqqasl#w#S{KCyKlD9lOm(6whyuVV;>f?C)XtZUhY|Hi`T z-P^ve6rWp2?j0~gWND9yG$gDm{X=f9o)CMz^TWWm+M%wVh>J z)D8!>@L39cN^|Nlg-nVm05wgxu{`{HyT$8Vbu-D3ioa`xDK|k=GH}g4(A4R6D4rN#k^B7gP|jOPu({sf zKKAyy*467Le|5TjnlZdiNCYX&7OS_F-qhNDHfEyCa(^ietKxk)I6U%rshN2(HLt|T zvu4#g>dx(wtzI>=*5N&~Hw_r&uWVy2o=3+Om2o`bEv4qhDdm-S-_ENxjFelwqNq-* zUX8mW{>D{ZTVIV$DNX+oFyz@hl2ujN-ihXyPT%2MD_ZY(esgoORd?`34O+QA?D&5> zG&^ssThBL@8~;|=f;(ZlE>2$(8|zlgrS$O1-^7Fx7!%^YrAJ%%CDtpu3@r+_V8s|D zT-g+pYmM_g=Gmk~?)kJP3hP5WDwYX|!9~xXSuXGR(0b3fKY$9!YKsLvwYC{Hq{Au* zKOVQ-ez1`Ob`<|=pSfj}lxA={UEXr}*1eA}yhpW??N$V&(!MKvv!2uK4Xp{e^+JoA zo$c6zDdr7mzbcun8>x;1n6|fxc9)>46+o6s%*IlVY12kjp6#+zmZD$E&m}0*D4{`+ zUgh+TZ>I%HDk`5+fwxk)%N}M+7Ddeg z@4PGnbm|()52Fm;s@_ak+E8_!7gv-A-W6{YU0wY3GwRZ9u~S26b|qK6Yei&4nc$=+FO0(Rqg@x&LqcVaie; zhKeHv_ZHDyS%G_?;wa6`z?GUAniZX_FCY?bG&3tT9GRIZ+Omy%WTvfSo$8=jSy^dy z&arWhKYsu52NxF?ydORf_xrwIH^a!U{C>YO>=M)O#%p)aV!@fA+YFrVr76*Df6@I7 zTaZ=E7H7%P)WqNbaFTW4^|80~t(MAur`D_o!t$2krsrpf-iUedYlBSrsb&Gp^-@a0?Dk3hFc(Sc^4 zxK6BODRts!N5CY-`OJwB@W3$X*2)j8yTM;Q_)%qXX=LD+xpSez2FWoZr+@bJhgXwO zu4?xvZ+i1XBg>9j`8yWD(BCXTctk6R!a`dU0Sf=j7A^>LRzT_+lyFa-c61&XKn<4( zHm)L)ARFZzeavi8per^gDSq==;WD>3wv$SkPd{o>8`O+&?%Eh$1v>gEtk z71ktuLKoJpzTIHMQ?r^lR0@PTClfK%fWXZ^3LQ^^1^fv)_Ovt*WHF>gM*YY{Hj>1V zmpaLnNOM}hb&^~h%NXO{kd7(hsdSD0K^?6Qvhz1-X~U4YrIFb%ttm+-e&xZA$Xx$p zLCzg*Gd}h@U(xmvOhT4_s`cf#_>kv5rS2fwDT;5hs+GL9V;6Omw!;ob<_OnBFEMwi zW5)LOjrva3R;~}a#p#DN&`yk`4R!Lc?JSL^)NdEut}>qEdts#82)SwaUZrn|Bnu_XcVih7B_66M9}i_THPDE7zBg+1O(;!FM7Fii#II#r~h6xcxx1)%D*qoKNh;h6r&8e0Y-HgHIlE##(3g?cYQBPxkGL{UO$jV&@l?rZQIv^IMn)C!16YD-6<{`_$T z3`n?eth8jy>kv-yGFf?jdi9b}b5Mnd0N^&>c$9m(UKnM;(I9#Ft{t~lw`a-E_SyNw z8ln&JNZT3XP27^$eD{#s97;mmukF34SN$DCb}~0m*$v-P0@U%cX4IV z5jx>@_17GlhJxy0>`P;7eYWQ0kYQyVBWLVA9nx33LuNXQqj-oUzu2)&`ygT6ZK*b$ zjG}nmT0+)G{Mex#s>9r-xa*^vGW|sO7Fc?g{W>9T-Sv}P`ZpZs=G=OjBkP!Hee7g~ z#y9EFcP|*b7d%&wJw5ViPvuYvT2L*xcDM>TS(5d-7h_uB zmVSd01aorK3ZUZ01t?D84QYpYCi886qfQ>TDo?006-tZ?zh%z6^(0g`j;Wi+B^{=| zA3ndD9$rx!P*Gz!B1+XYwoDt&9+Ogy6B?}tr)IFGuHR@DbzN^i)xi0JOEwd}N_$;4ad=xaCB+^cV zXUa>LzYUD{s{E%iXjeG2E3HnKBrOaKX}$Ag(!Ht4McPXLkF@c<3+*A$GQTHT_q6U! zwQE1v7y0#*{RLO;2gPCk{8mQJXu-B8>)!y*CEVLvWxEly^7sOEPY8UN67eu8?0y5@ zV7i&o!uWRb<5+(%rfS?f@$L8GFeeO@_RIDM)c3zUzrZ}_zYhETC20ATSMlq;Ee%S= zYt25JlB4+(cdmrlb~Y7^n*(V|ZEG&c8={>ik3a$tEEMWo1^MISMK{_d)&kn-lluEW z9agPI&QVncePwgZSu(XZWCr<;}Xe`H?CA&I)r2{ME`-cXU^UtQ*B^$Qys2t zM4#SZ6JaQc{K&_CE-CJ?@ZIeX4yfI@#&flcj!r{9nq%PmPM^0i5PfOmo7%LSPlC4Q zWosYDS$@(2k1yW%W{r)WBPOwI^PyW>#P5EEyGBsWG50khh?akUSj?mvzZ7JSS5A+Y z{-?Cj?`D0G!0V)9+Hd%8?f0jKE8K55|ICm4@ohXXYuS04b~@&}a2xo$uX zuvJ!RhtBfdahK$ORRl#Unth} zRxM43-WNJ{>2>F~TMUSvDMU}5OGsSA{zFUK%!=a|b(dz1)djCQIhDT4Z=@JITa;5r z2}gCm$WFyH>139ZUskz%{Sq$;);uka^nBy1y{L1s| z61`gY6`J#eWi)2b+_URz?H=#v-R>FUiJM>bgyzO00~?;=9|*?1UgxZs-%B|A`LD#Y z@dL}=4a=HOzJ4Em`1sNEk96~?;(HrX+kby^Z2eQbQ_C(rblJ|Pi;VrB$<5Iw=Onat zOSe~N@o-2mPEC?y{Lvyhp1A0c)o1?SXBLzKkN~7HmlV%j4Lq%c>oPXQBf@l-SoAL; zNvg1$sPH7!>>L-iYqLfxptYnVrZ)zZ1!>d#1J5$9Hc%`hWtIi5F+Qn5-{CO(as-V6 zH3CJtoJ;nfkVGoRT5i+jH!o%Brp$(zaYN0h_7=j|Q4k^>V*gZ^MCTG7)$BS3lCH!M z&AFi$FG==wvKxcPTA)e)h4!;dH#H9KUZduQY$C(=k}p}9FVNJhKi7BQ!eQCRexI9B zqIVLmp5L)MnBCEr5vY;7dd0$p_&&XS<}f&MoS~!LJb2tTGA|6uKJtd(blh&`%ccs| z+URR!?t8aUQfk1=1llyWaxITtX~Rz<@@yt{}Dk zfkq@#cj5u-X$@;jwsxYth^1OZEOJ;DTy5n%0>!jsm(UT;!H3h2{+b7|9&RIS-5^G~BYQkh94A)&+E)Togloh2IK(u_I?AzmY^@;=ZWjU*_5#Y1BnBH6Ky5tA%7j4p~O^LJEns;W|OH zw;v_h?w`bL;<~(As*%s{+bwd`b<^;OJlsCG|4hV26fCP{R{y%0en=ncO$54}uCs6ahp+AZ7oPS6eYjM0=$I1g#nu09`rNNKPQAwk z8wU=)x2+ql_&FS@&i@lvuDJZ$bKepE&FC6XKjmxiLDFH$|C(@?@@?XAT36>Ujhte6 zU15uG=<8m-dY}5J?qJT-iHNUnivQgw{@2Un-)#6H%}Y#l(5fTuQz9#Q7fuA_Y)Yem zMvWlQh-S-Sqb+_JaR5dDWb+I1$RbHDG_0p^G9Eb0f1ywVE*tSieZ;K1OZi`pxC2zbi-ELvgvwlvTONf7YSZ>Ax2Z{=mO#&WaR|2vwU&Bo!qxs3iS)NRx2*)m z{VJlF1OoMNw&qZ2m zV^>*)K$VS?s!94@46*r9CoL8;SY+p`Zw zlSeD=pV3VLpR?50`G#KB=$5j_Q_I`#CLemuFbcc(D1TS$55aI{$ldz7-&-aa|6Y7r z?ecWNR*_ZqUmMar|Eh_}6Oe+?w0)A)oYMVkH`PXt{v_AYn#C@_5e5QC#HEnKWGPo+ zb)*OJ-LJrnUV7dyx|Mlm=ny}t@ zfIxxd(UIBf@lygQ{RYK@u>{f`4+}|8s_kFs+OLJ^%`pIb0XTC7W5B@}G^%M1*+T?% zUtF@|T8p+RJV&Xg#zV3p*6>C`(5wbWPI%Ja<1NI)8&^rt2|Ut(kk>~vYQ)EI^}>aO zpr$tgRLsqwO6xuyxe(7_AVhN5e{GQg8;oxYu-Qy9nfXVp`9S)t9(j@215Hcw!DljG zk2h61+v4^&V$&f6AAvT_8>^=Eu8is;g##gK#xvD2N>1EBJ=RTut1;~TB_Cs3xF;pH zM>{F;RO3znzkzvZkOPJYN%oD}`Hh4h3|tUbj~*%f;8L*hzWdJjA1tB1^@|5e2B>Iu zUQy}P2n|Gi7ixt1htIkoZH?&|HKYgJJYN6)P2Rx_TYdDKA2@V-^FceD1qBqzHiy9#z7WVVsvXD|oTeIM|RI!o;6 zBj=mob4MDV1)^Uv)^11vjS8^z`HhNKKQ_Yfrf=nlS>3}{WP2vwVBnvvW@tckm%_O# zaxipFf9g<}SZAFBUFPDYB$Ev-QfIT;B610DabXHQQsi%L`7Yf$Qmv0Z?=4CyshK!I zPkvIR?{s#u9_}$=?K1wb!X)po`Am`Pxu|-)Y1RzK6nXSqbcB7F!48Zr4hqb(l;X;` z?wkc_KT$*bnN5zuoBPEKi6N3{tRN7uDTG7a>K0B47zLV6iN98$)}Zueh-X{VU%hW8 zwp>a>A3bBU$u{_e*Z$S=mUGOTPGVZ>Gc!tfGYVKtpIh7#Wgc-c3x6Zn?sxkQ%r?)H z!uOSBpwH4Ms!?2h%xQbpFOZ5dsOK|%29FNG1HcV~iA_zZp^u%nPS1%)neC)YLDZKs zKKRd@I$bV@lofvsI81dJvfgseZ^F6gCRm^oG6N2-5&ohLXy)hR2XIW5wa91?Y!0!z zA!`cIJf@5a1cMfrW*pl_BcbN4gRgL=MuTR7-ZxOMLbW`fClxzgn3oUPbxqJqoYGpj zcMaKju|DW+)&m+ii+SY{g4y#WiLO~GWz=f7d8L>XlV>v&4C*uz{Wu=o4|RjHxFIe| zuo`FDYHqeFwRupR8|qRs%0<(ppt*)=A0^2Tzx#O-+Vm==xY?-H`V!83H;#P+sL$Qe z$S4!(lgwg^O3ZI1dKDXrrAEW{Ul%AM`(ze+T(`E$<@Ml9 z=TFjuI+ko*-=DgNd$7YKir43FQEB@#q~VYnd}@?*IIr@&zC+z~AW29aI)N9?`w=>A zbE5i9*FQpq+Fw&UZs~ZJXa{FaVl*puC>emQ@tZBO%J$9~CI((8y$HbOACY3BZVjyt z5}j3bkDxHAuD0%SsRh<7XTG{7K8GeO+?>Ouz_uNneZ46hH@N338V)EJFsPq=KsyKM zH^OKD3`UgEl%Ko9nW?C|Hs-qt!L|*DHv7Zu^J*}bZ6EO(Smld4p=YJV9%kT9G4IjY0 z8pDFTs8EL~Rt~e&OUemZBboBSx$?7^rH-ZIoV(U}ZC}P{{VL}|m+ipvc=%>Gb8yXM zU7s2VyZB|S@Xp}!0O>#*Q)CfDX`FM-QF02Scu<4obb=NS)Q)=8zpCRK)aT&gjN5Kb zHC9+7QbV{BCy#-k+qEL(7dF~|UZ8P+EDXG)Iz^dMX&;*zGE5jcLsm0MQ=1yY9}Oa+ zz0D$vXGo^d8eCefDJP02GQ~*DEJho*5sOABoNn1xhN*smwND4?i^R;_FQ&KZF})wK z%XD~ESYnITBV8_awQVWoDxE|>1XU5{uwD!0T`EQ}}iy>zHj1si$J#+T6b%umiD)}L%5l|%(~ z{7`GHn*F4Mzhdxb zdI3N{m9_N~VLFxPwl42u;!+ez#FLQ1Kk3vWl&exp>~YUHktQ(l^HH;C#O9da^%+@B zXR2)yQ;iy7ZqHR(&FLb|Djv*TM%5gx9nwE3K>GL%pq7_l9`#hQJEH&}Dr4&L0!y|{ z`H-)W1HzMd>KKYhyGIIJh2NBwG#-LtI(ylUOY9>_9mlXw?O>m>sJCo>iL}%<9kaY7 ziA@Xh(2gY-y+eWMeKw~z#gl0L$0+!h3Q@PPz%%MAgE+G>c5uF`gPj<;sXjp8qe8`e6bTg;8Q5j{Cj}=NsiKT@+)(IYb(% z&nfjwBz_Ma(U7Zxd+UEF&*dXcDWXp1b7*a4u1|-qsRr#A20wo<`NoWehGVrUxhB1a z@P#Cy4epGrS=mkbQ-R~K&C4s}B^EdYF51I9Hp?-Jn#1Q(R1teP&4rKgPtKTjaI{ zS`kG<6dX$}w)=3rh}1pfY|}sCY(cy?>KAvyD!0;HdBW6YOK|8Eh&b`QrBg-K|Hf;o z20#vw0voqpnnJ131@$Y>gQ3|^)TIsX1B*HomQ_ z5f9H|Jw5x7<_e(}hXe5pJ_q%nKp^@4J1Vl3Sj@CE&rJx$%9S(o$P*(=j;H8b_AU2c zED94q&z|E8fkZ&6PJKhgGypIL1X?R$1liX?YvY1bm1UdtB(*{p18%`8-Ic1D-^N_? zoC5dng<`_2VU(uX0{;9QO@M-I_h%OA8_g0W*1mRPq5C@l2idL|vNRPGgw!!Kw#bA! z*3RPCsWyC`bw4F*w#dd6ic0|^>~g<|x989HU8@!8tf&fJA?L{3X|T1E0*&x|k#5V9 zXqvj@7kaZF7ovijr^_XF42Diu8_a~n6xpd|yJ9}VBtQjO`)m1wFCrc* zf5x?6*OofCK7?v~EJ`G2O*>>pRt^-zY<3|mR_?4{wbj2G$CDnX7SgPn^uv#<7umn8 zg6gV>yV}kDs~?AKbZj09NeF70%fV*ge9(tb-AzV|@X*@vyLUOq_1q4WIBGPL9=|bt&aEr7fPdjW=075P>8u{%Wm}i)rb^s5i!f~2szA?3Bj>}WLV}pDx~uY+mox2?5tZT(>W(W zL@&cEr`Uug;My=i@{iO|Lh>PQw^pwJ;mhzI6}n7@+Q>-PmIGyx?k`-4`-!pF73l6| zs3w3am7!}nkWCEK!6iT)CVEs|(u!lK_no8mnsNu7OYNug)mJnP3Te<`fr6D^x zh+H;S`z?|WAh}D}D{NG%40%?8Rd`^-zhVVT$W}nJK>=sVjD$Q?KY+?-l0+7p9FEGL~ z+hhQCnByP?(BV^v+x<3tpM!;jz+yRknP*$xV|^Bi7Q8!}BmQ3;8e7akb2+)EO;f3s zEFBlrVwJr-uqIs3Qu4b%Z%>+594ot3sLOTUV~J1Syu;)u@<5u%b(Jf+0uoi#BBBb} zEW`Gz(AP7dKl}Y74#|O9fv}P9h#XL(kfNLE zj9~GQFFZ+i^&#XX5>XCpKS~7H09Fa)PXjO-#B&6?3r`cqMR zFJ>JRx?f(x_As*RspxJQ`Y6N09f6T6<%L9aB?ANZ!kUrIFEjK9XxKqE?%Dy|C>VW% z5_^S>8)D#Y2ynw7aa4|bs6Y#4NDCTj3e+5B*^XRyOc-*7Hww~{pAticv2|cc0 zIIm6U(YLU|Tl(kGbh?S46rYd;58yZp0)<=IiA$F&onzp+f7_L~Kmg+#`Af1af_p7siwn$Uwd@8#?(4_;4LzaKpiadIiq@g20vc z_+SdF`*#g+wGUy8NB;XGfLQ4~@<`w|h@Un;Ia)2u-R!K*O(Rzl%6O0}v+2%cW&eA-Y6FXM!-*uZxCMy8YHR|JMb52M-@ z*gGn8#VfhiNp#R(M=+Th^+I$lyRq6=zh*kd$V+7Hi|eQ0)LcJvv-oyFE13{`gN;3{ zF!liv5#O*TA-J8HbcPRZnSvVuwXaZccb2f%*qE*<%<*XbYv37oroIa#dT8}1@d{ur z->Tom#ty2mmn%z~*V1J|ci$b@rGj-sUwLlJ=vFn1SVpY&zzs!Xjwu*Z(U`2!D8XQq z%Pi)MniUFQJ3;NIch_I-Zhd@%afOFj+iijQW1G(TG~*)tybdxI$=Bkxw;EK<4>=`p z+J)%O6$3nIE$B4BOGgkK+J0R~qBdT8I_6|&KkI`*?c8N&K`a5^=zOpGxx705&$y6|M{es!|_Rr^O$ceOsC}*RbPz^i&%ggN5{- zYE4NA_?>aVF(7MHbua~Bsmk0&h7cK-BJnUo5SHBs-$FCE@Fo+(Xdo;#T(YbquB|hg zf~)}{dw-=H(qK6)WTwdsObVa{Z%}@sl}MoASq7 z7CnwCFjEZdO(L=N#`S0OxZ7;(ZABBm!2K#d_BlMdn%X-mz?~LgY|MxUcU~uthrzvK zFDzkGe#1>pNk?h%uNtoiG5W7^sQMxIE-qnzXJO@r#M|<)yML1*A=o29L|FTdy?Z7X z65QY0%O@r#F4_~zR*)%_9iBHU*XZxi@3ypB&9U6dV1+R5jJ8-(wj{d#ssu{;>vH(; z#rEBPiRe($Y%3=CsMjYLRc`9W2B2$35_6}XTq!(X1RDLB=@SK#xYFM)t5cLg{WW%8U z<-N--HSEKYh2o(@U; zb^}2uIKKW0VjHL3j5X!rH=v`X(73=_{bS14;n9(|;OLKsq*BWX_@Msmz*sf>`QAG# zc7m6PD(689TL(O8CYkCzl_0+#D2J!1k$&eNzV^R^D`%a0JL~Jf-@MIPWQBNXz$L)= z$marM2=ab)$C}mk&bX~7qqgCB$S^$*XLZ=MXi2!n?#X4vO=k!qmI`R7kS#K7r~0E& zK%j!i-SrK(^)ocp9dnF2f|^1AEF8QI5P?E9lc3A3!HZUJsF>N?{g$lC+-?L}X1n+; zEEv+>EmI(t?7}`zz=#9Wehg{qgJu$4Zal@L0@TVB8z0;3k*W?^WUM0P9CpFZ)Yz?#{+N8L?i@ z9}iz?qNQTD9cxrNK#2r+I|cJb3z8`(5>t@N3|K#?EdsR}04x*4+=ReG<;Z#!ws&>? zr(X_FKL0!3RG$s4SVC_k&g1?ZUGnLyI_H|u2ic`cno~KgpI!3~?)Bn+Qj?`+eImG0 z`PAz%cWQQB8EO7H&HQHHJ$N7RJT3k8|thehTEDs9;HAT)Xj zUaCT0`;JXlkUPqwj!!@?3$mZ)QB{3|)sb=6*#_MgKI{+u)%8MtJDg~YF~7eadkBQi z_#yRZ@JDitP)2M3^vXEcB0*dk5BF>rvVw=J;Ax~zp)N7dzPXM0p!VLWeNhNO!9@=v+q)kKhqvnIE z%yu2~^NiXQQbXA}-Tr#~si8$kU3#UdMbXjv@VY_R`-?lO9D+wz;L&?ZqE3fTcywRe z^Y+w+L%pal)7NEnts8~IAKvR^J?!G#;?G94oQt=Ss+01AkBe60Pv`a7>fd;>gX}4N z^B?X*H0JpEr~_U$C&gG(|6xVkm@Fy_L+yReOQ`GSZ6wBepLO42yT0Gs-kAg<{zmweo2BREo$}()WB7$fEL@- zDrU>K!M}@&EPj!bq-HDR)*{QfMqgcr-)c=u_*qh`vdD&&rXp&hbF;d)$=(8Pkj|dh zk>v|TN5bVrJH%|Mrv<{eJOkmB0&VOk(0kcPO)D0D>{z@i`kckiuGFIV;|a50EHaKT z-)J`Sr)+M1yloreX2U*zZriEnyT(xsrK4P|W1_P~_tJbXqUy;)d+^I^%5;aeiI!7# zrCWdXhZoW8GbBf&L|X#1%!+$^SI?T34q&ZcEzm{VqqBa*q0O=~%V>KZendNavaTU& z(*!nYCpcB~q>GLVBHPk4nZLc#DXOEGc{i81{oZQSbJ6D92W78q!%2Dn`O6zb%Xa6l z_2g#j`#J7Qk`*4urzBg!-_2`Y&b0L@PpRo6XPNP8?TpRnH7Boh7Qf2=cx~m)yXzH? zeLml0eBu?{+KE>eWKfK$sWkzu9$b;WHH8s0?7lkggVAFN`tLcg7$R+9NR$9+5X31! zqU1CI(pb%3GIWyxe$`Sn*+s|g8~347+04%Il^rvRt>*~JiW!_f;)pcR=+Y7*{-%3T z8o2IFQ%O#IEBvdG>H2iGQE)i4>S)#{AeB61A5e+(QdfXG&oH2_Bc3OR3jq6*Q*RZ4 z@*aS)2a%K@vu|nAtU?WGO2fV~sckl%;SS_L(>%tLj`Jj5tL z&qIt*PTEE&$`3H8+A^We!^K70T0&3(-gEfVE8>u~_@ZEzPuMxipk)%F%BuK2#iwm( zjoO50$!bQf=fO=Zk4Vx{{Th{0v3mC{U8j_5rNlvPt083`^Gd>_S}0|XS-P#7iOlXD zSo7QkyB#&4Hwa2-rQKN3uOt!(7SK1~D|D1edILO!&Wr#Tm0)QgoMITybt6)%sfaqc zza1ND5}UrCby2mM6dJX0{kx1pJ0tXOp-DrQ^kvPUfgu;`Vw+s$~hCKUs(6KxRw|U7_cTZ0NMQ;gj|=i%RHEcc>gA0*f9HT@ zn3t*#cZugq6g%URzyV^IA_(KNjD0Lg(rZV-*hx`Hxp%*AqBWBwVid4%QjJqlB0Y~; z4EK^%ghj@AyJuciT}Q;^2TgyjjBh0@m+O__G=h7pl?CQg+pL}n$773Gu7j=@NMloP ztZx3jpC+d2uU-|LF#54ny7i{0U#S?Z{l2%vofUN-y7wjpnX{ttc8~ti(sb)=sx&pm z$Nf^y(v7)DH8saaOS}Apo^99L)NkGTXV_TVbwyGQ4UduZaGpnB7Merz)heDX3NMzU zUaf;n6#%}Xve~tsq-OnA*|14u@?0vZK)>?puw8Bfngf`f{`bdOr-K<6x8k6>1^j`X zB;jS^x|0uN9AA;et|B&%5B8DIZalxf>l%&*dW%HZXrKpmw3{YZpnmPJwX7`Lk+h) zvYKdD{{jM$%OGhVWNM0~O)b=(L$K#VEv?BsQC6XSU{UyR9j(^{4@jS z$0Z{I1V@8sU}0>;r2*4TwTO_Vf^9R}k$aHwK?Pnz5D}|_*-4>+K=BF>88|9(4Jz1zg1fIED%duz=^`fzB1t*qy8_>;HY>0TqtXk# z6i_>qIGI=ICDm|O+HG18@q1w*>86(+b~@T1F5M+AnHJHR)YYE{(A(vwZGT*k4hpQb zM%1E;9M2*Ck=E2Q?7lP>os>b%WnCZSU8cTeowUM~HZ+ru^s_G9$`-kMc-IDt7@6Jy z$=?2PijW}aOBXdIP;5roaJxxZes^Bx3HgXVVBd3uEgr-Z- zKsA{0{Q9;fXq-xHsve$ZvIb1D<4Z}@5>4X+?Do#$l8##QHuy1?NO%(=g2RIN$n|&C z{8>>f2l*L$v5_^p!&ahR#e@Rp%XcB3qvpZM-jQ?A+iaJa8;DRo(uor6r-C!%k^3`| zcW7Gf2iM<$7ceP?yG7lpEAYh%DzcY84@U_U0S2TmuW1>h_ylrO>V^(xk#b- zA|YAb3dB{9Pk~3RD5yw?k{+>z7sI7Ka-Iw=15TV2s{87pctAcCX3r53r9w+-!Pn>F z0F;=nfVkyd-5+|M(BZ!KS7E}a@1cUK`d9LcO(Xm55nKUs+YJ1l>8pRo4d%oY3Q8)D zdPuhPUfni>_`?9%P%_1xDUA0(#`6o~Ro5JRC1`|R5}Uns;hHwWWNSLx`8#5h0>PCc zQxt{U6@|@TFOX1@;}qoTj+w%wCF7J8X{_>mgrMjs+iCZSkspya_T$hMlpBnc!CmLn zgQ}e&4E(LM$SS@luk^@|<)(P4#>s{9-04d_46|*RpwqMvKk0aZQP6OTH~e4_hmReN z7Be1PW9Jq;#zPmfuwb$Aja}*YOSL?l!g=s0Nm;c_nqO8aiRj*VX z6^N$4-Fx_}(1Rm#T7eqM@68z1_mJIRRstF_VCO0P8YL9Zg_=KtSjsEsy(hnB$*t1+ z&~bt@rh+cHtUU?9@dP-g5aaQ*`0om2-vngr zQp1&gllajKTu?%O(eU|uAy?W^f8!XJjfkN{9x*$LyI?8aklZIjA#gVY9xwkgqDZV||>~q$v+7?VBkQz+r-TWV0)R z)&?I=-gqQ`{hEW9&KkKeLt&=jP);NtB1?K>poPucX2MZB53TdFWnHpHgk+j69VWOM5 zgtyk5hZox4RCoT3{OnoqSNxI8D1)1a^Q`-CI(+vPRgtgyR`)q7cF_SZJX z=#Z?b=`*@0j0yhvOGqC@1gLkee36Gj_(&asT{c><$F6zTr^y$bPZDY}+W!z%&KnJs zhK0XLsV4STzM8Jv7pmTkZBb)h5xfCDS%8s2)&Lm88G(xw7GX_YzYj)|GKbOemCDGA zKT+Rn5xpOeZs8ZUh7FtDi~0u~Two%$@SiW*cIEj=SeDc1^^!4%XRK@|`ghbO<<=#A zQLEl=G0_-lpcuh?Tgaqbl7e zUJ@=qRR1WXt`uH=Sd%0{clwTe$!r?mrF|v0=wO~{!^7#-U5V<_?9+UpKyw0PqY{1u zOI}zP&%|No7dHD#T{{Ka)L|)qP~09Qa+e2fptVIQE&FNKR-A$ja!lrX>=AvqldQm~ zq-1jsc>`IT;?O3F!oE_8n9}ZEHq?_Wo}03?;+15H%J%43s}!1nxn&vcV~j)Q{_<9{ zrMFw#;7;t?t>hO+{NRqNcP$L@l>cbZs}i)>K($xw&?dgBDBfU=jOH{CqqZflfj9gO zWiFTnaW>yPSgM$QKaMSs>5F+iaDVcGZ*gqlmy-Q-k>dhXM1c2@;2jM+{}*!h@gZ!T zv2}iVY!!D0rp`dX-Er5xf^E3gB(3RkQ{n0l2588DXr+t%ti@!$@T-_h+xkD=Pj>FlYO={(mrWh?@DM$g9=Ttr z?Q~hKcq0C@Kx*|MG*Z)&kf$p=klW+Z;swYpwQAc0u%^e}*c!E^7pfg3^q%q5-&OdR zWF6O|>yO`_`!+lelm~vvL2lrQ<0(l0AQ36OK=*>!Y)Y_ks`JYDg@suuH@z@QE;jQh z3=qU`@DOc_iyXVx#FdM)joz$o@jz^l=|=Nm+F-#pYlJ~6oOebXAxD2vX*$TcYX02? z^r+{9i}zGy<(1hp^F@sy#?1G&`TY_lu(;1N*n)y{oXNe@x9-l^L~ESZ&nb(6xN!%* z*`CHP%H>=a53TkL%>2~2_nln2{OghjrU58%AvimBosze)_GXm5Xe%joEH|Aw@p(aX z^juN>)7r8F$Fbgf4z8*2*jPHKEG}-xt_iRB3zbkaV+6`TefBQP!$Q4Uq1MXe#O(?M z1Y=kZ|$ZaZ!feL~WxbW5p@hb@K5@0J3ka$SA)BI+3 zA;%ag`V%49BT0(;Ot?J1CB2Zr7r(cZB=9L2V?wM6BtlVW>>(nNAyFJVE?a`MkR%I` z$qcv)TTJ00b2o{idfii`yCr)NfwfDHClFCnP^uiB!huQ1kn?vP(^o`%P#i&%{39yP z&Co0%V4I|6zwL$kx53qn^L3}2&$2}uKNkxgow@QEVR(taqV=^++~gXDRsI6OemUy?9!$e=g3FQ>7=x|D*`ivfi2Z05VRHwm(9? zvKJA|TTd3pw?Co~E*KY$Hlq->o64n;*BMzg7q(P9pHMezKDMD7r!SgXyhc{A^WU?J zVy8FT`L>Xr7`PRRNkO6n$}>cB#Tl?z+Q|N-K!< zs4%^mcCjLVeaj{+3*}HP*9nF$zH!`iBYW6oeuSlM5ng3pDf-n*vOO=KRzT3uY(vn{TKFQ>9f1Lqw$@iroTSl*Esz@ zr6)u}8i_peQuooe-;ED^r0OLee(I@bT(DKpO_6YvJ0yEYwrFq<;@YPe~bmD+i|_0wD0J6-rc&$(EW|NZBA*7!HD(<*7t-N$jw)FN{=&@{URKNKu@ zIu}X$!nFa^(v5H=Y*atPN z1qZOK1v!l9mNg}Wz08-UwS?a*)&Y#G*X@a{oVa)7z9HEVG4 zG;gE4m@__`w4xvQwX^3joX*lbLML(Bbb{8C@eLgqy+J(gB!SPCgq;Syz)alNEWaiN z8Zk%6`2Ze4x<-S*Y}1CLM-8PO1Eb&>TqxFAu~-FN)gDAanN~O8&c(Yg-K0Vt27b}+ zQkm9AM>V@j@qUI$Q@Md!hn8eMYiRuC5&JvfLyXD)#@=AoWGiOSe{d@Y2t! z9{7e%$4Tf^0c%Eye4NjcM#d;?lRD1nC{`!p35) zDuYm=wcxgTc%flNAWGK}SYQW7tT9MN0kq5?%sqXE50^Z+QHEh^XrL3o2On;II_wxu=i zG~0hmIuql|hQ{9Lg~4TN{}16C7fb=5+7!9OEk#Hgt@W|s_?CLfxO%PZL9T5ic0+;+ zLlq$GUxH9u+3f#Obl*`)w|^hN*|JnXRNRQDprSZ4vjW7K8qQ43aF5h*=3Q2Rdz3g+ zGjpe=g{Ea?_l-C*TUxfv%$1p0*`B|Ac>duJ&fy&39KK(!>-xN3Z%lzWT`Ir+gq_%S%4*YQ6-5Go#sz6z{6%<&z#@$)9!t@=gAyn*)S zdh|}$FRfNnFwIz_OFRC(9(VeDc*fno{!jsr##) z59>_1tLZYlHMeXzTQ6HKwN#_*@Bn>)JY<>i`J+}Da_#cbYcw>jvR!Fbr6fG$5?q}o zLqvb>TKbV!XAt+|C!oX;~uZ=drPbKJ&vY|f$HqUZE zSJjeE^jZ>}gNY0H(A2YD9y}0!1OPtb#)pJqz)ItiPIvJz9@L(tddCdJ_LHB{)EX#Ict9Was%U_>jF0MZu*zYWEcJi}CZ%Bk zF+b9fzn8!^%uVZ~R}j^s2XoBuOt3rw1lNN@WABJy0huJDz8FV6ikE(by-+@Z!HQxn z6v^6@ME}5n`gP`D=Rv9JJz^FnBncI`vB`w1W7)S@+~R#3x)5(7*jki?EYj@8mbm0w z`VYaja)!X%jZsV;N8SP*s(PK`XeyMzWfKu+ZY(iV2KN#x+P>_dAi<>I<-6pJ&s#$n z5b}hju9DorL*9gQNitoAxh$6)I8$)6<3som_{SttbnZ)6Plw-qUKwPhw!Ua6Dk|f3(rjBk^G9_2uQ&VhZX;TB z&0c4W|8rkOIuF&=NUce|(55s9C~yq~;b>KLTW)X*m|e7lufW*KqBr-`17{F-50vRV z;J_l(-tE6EhF)EM2>1z+$ZejWIB0HQ^33c73b;G_z4lpS<_8gAL%QH>8wO%~i-`4b zk$@sFC02jt(c&m6ei2ZoD(QIqNln6>j}~Oy0kk(0N3}BS3fYiFgjO{$WNLZ`rVf)$ z{bDTs6^AQnm{iqCIz>OG3&%bs^-4Z6<=;0HTOoAe^;`&Iv9yRl7h-qkOQF}ej>-(7 zikRu7<+4#2=1bPzI#6Jy4g|tu7~m_M9JAb{LUarOM216^ljj9k5ecJ57z#>ut|`1D z#Agz6;#Yql<0MQ})c{mA*b}@5(=2D&@Q;4_l)7I#cg2^$#^i<;{_!Vi>vM*cEyPIN zG7)M;*hEF5b>TtntUa+GvQ#t|ink0%z}g}Qr(9;C%1EfXZNA(gg=}wy*b0YhZ^iul zbktU!)yGtYVOmuw=XH!POBp%rr2hfx)0fd)x_Rd8kv!6C*+_E5cj+wY;MKn_%)0uE zmvHE1C>_mIxJ`#TPx)u>Jk$qwumvp_N-|WoI4EdYCnpYuYg)J{1ZnjLzkSKGF$MH( z3+9Z_=D1o`M~2|Z1Ea=se9eQ}q0K=ds~Ec(?i&0*rqldUTrxMC>>_{W#Wbbx>yk&( zV{P1(7<m7pdG#jWV4@2&6YRI zGI=urv36`r4b2?%|~1=0k{O!VzQAI5?CDrJ;x@A3n3hdhx-tuZirGh1cMCW zGnoMF6k@z2;LHp%z2s~)-)D4Kb$%#e)Zxl43HlBIdWVlT!PH8Sj_uF6IU;Tj$9eEM zqUUrd!ok#6NQ9%RegN+?K_nS=$zkdDVRSYGCd)$72-vDiXlk;H2$veJ3Qgdk>n^j# zT|%>Isqr5aZ<$GBNEMcu8tV*diXN(0GLR~g-t~jX29c@N zBgQSK%+UMv2Z8d-L$YN4_+%I@Q}&sqhD51C-;hp#bbsb1h`b@7jyUN2VQO=vrhQDE z_8}tSu8FNwFH>szR7x3YGLz)$e*wz>8S-5%H0zSRCon0d8u;slQyNh5hg{xP7pu`> z8f2J8Wa(E+$^Qrr-&wL)*CBI42xu+CFTjP!XS4ti>5H>{fYjnneN$)%m5e0?uB8O? zDi35xX|bqb(SL+n&Vp8SwMiLdf+jPKBBKn$c1Wd<(uRn52VYq=yo6;@0U~VxEnOVU z$C(*}0^>OlJ%ELaP@`9asFm zJdxbv6anL#04rM2Yw_j_rR4dGG^?p&j?J_(>SU!9-lx7W?JzO8#&YhdUc4FY$} zG3paKx&!4x5zGJv2Esu@L%{`E)_;+>$L1yWi0AM@-v;i=X#+Otw6Fr!F z%!gEf&N8YKxw7JsDgos`LOca1oZPMEE>HkW$aM{=SI@|aCF){8M#u=E_xa(fN%27R zbqa}QH4*`LsKkJ)Y)6#dNgXCjjbz@}EX16vw^wu6+&7!PF3emjbR3-i4?TSFi{@h@ z*n$r6fELkPHlN6Ku$+9eoqSoWiqYdyOOzi#eRj4`A&d42=@gS~`k3YPr_i|qqV;IX zVI<3GkX1O5>kJ>JYz*-|o(#UDsFA9i@36W@z%I#q=b`1mWF?S_ljZXA`d#w7cjZ(o zupuvEf348J7DOkqKE%JIp`|o(ma3+K&W|N`T%?0h1l44iXurJvN{-z1#=^d~V&+XF zqSa%rgPNVU{#D;Aq{ZRh+|JrJ?qwYgYdViyPS&aDdl?CLlB5nc4piTKeY%7Bpw!X? zTsJ2qO@~wMhyT*RBr@2fy4Y%*q&CQ;(1$1YI_Ss0bb0@hNOrLJGqm%Up7Xsqi$S4& z`(`VN?ofq1(Ee@BVm)B#ZbYl8Lx9SV#`=D&?^@$sZTg_JaqaB(74oHIsYS-wWzB{t zTz&MfJg_=T&x*C{R`m622OKFQaVxHX`2z5*n+Uq>P!*swD5-<u4gVf-dDzXGHa$hp$Fl@k(s|Fy8AA48{DIAt5odwARWYk96 ziy8J^z<=ruRe0ImKa{!sr=3NdyDA;dXt4PJ=)$znm@!OuQ9bsUwP)j8=C9Ysz1cSZ z;t^0ZO&;uVVn)2`_5%9GFjHvu6l8?VB3Y1nR!Qu^)!4vnBlNC&_oUR8&C~bOi=Enq zP63OWwaB%HNv#58`9)A$ z1&TCvu;3p6xDC@P*8CHh`T(#F#K?L)QfK~+Mukv6_59xiv-TN10)u5S3-bC`xMtHr z;td(o!5Tb4+r5^e@x+fa5fwMa-4k`_JhM%jLy#SLL#1`YbkjkH(2)NsFYo7r59r+~ zWGPr%`nh`O-1d=n>yhq1ub-?j!7b}@KaBgW`KU`0{ekt{QZaxnP{#AeIu#ElOY|U8 zX_zd^vhHz*p(k4jhWVjQD`n^|fQ|H$ha`@H6HO(MQ~+GE+x~~ZlDw(};G^=Q|LI+q zKmA(2%Mj6FIC!%0ZY?LYl!nyGvea~ddbZOb?Si>;Ap|Boc11#opSk*Rh;bT3fJAnx3)p5^ zq=;ARw}3r|FX2)n^&@fvKMnnL`ryv0y}08%mI0!VU!bmEY|9m30per^$a0XiVLnF- z5bvBA@|KoWIEnEUuiT>!xH|J}6@^&ePGa`WCDz<0QQl&Geo~h&=?C)*AlLcJYbpb5 zH7`J&GvUSDLY)Sns?%=IvC-3;Lgk1?#j^zfGy{Oq0J{TeBY-hezC2lqMgx#B9`s{RbvDj<=V?E}+6o|oZ)&U8gHeSau?09#}VUs4*D6YVTm>^LFJM zbLkc{#pS%dWb+N%227fNzn<{^oAt2c)eF5wz+K0U$?l(N#Y)+?>kGc;X+nlAD?nry z>hIbCO>)*+{lUtxOVRemg|`HIXF7;@L6c1ui5_AxFdnwUn4QHuXE1Cz4qUPZn`mmU z7)dQtt#o*2Roy$c+^awS;Z2^QgJl3K_w~hnVdN=&0o4WUK4;}T!L+w+p-P0W6EEbA z4qy3uMJY3OJF2znE*TmG>JPCktjv$L;J*4%u4Lj`ZYA}5$#&NtQA&ea{Scn7GelJb z(5KVD07k@95c(E1o86Tr%A6=$MfvFmDHfVi<;fQo3!H zXt#JO)TOWx6mL_A)csLX{yk|n2|VzUZABc;F>=s%&a&W8 zRHZ?*&2tByeY)1lxi{Cv9yxS9?$iF7PpT7cri%X3+lF>;TuKox#{u=BAQyWV!TO_S z2OI6W$h=xbea1{IT`bdgfxpua(yhP@74<5+h-!;^2bYn-+m|9;JoWD!?Mc+9f627Ow0az~Euv*v!CN)m4|c7zq(T}V zf*q*igN=b8uTNjjd=*#$9A5ohbBAT!{{E+8rX^aESn2mn?yT>Yy@4|0wOmse5g4^d zpm?TXog%Lbz~JXEPKeNIhvK&*g;Heew%920wel=TzSY&mQIstI?W7)fDMt76Lza`1 z6(yeY34h6zUzZH-H(D{THNEOo_VRNp!F}C} zG8*4|Ip&enZdb=-iOcaZaPx>OR@-x;i58TdfbEaH6;=G2>`tJEVssBLkqeZlp>toD zWW(+{X1=ujM0AE&)68vvf>;XhtR}e)E>|jjFeAAn4>Zs$_7+B%rGCzR_UY6`rLBed zq-SLdL-%N~o*FJq}|K6|b#-;I0e`;C-iy0E2?*i*3Q$*xZ$$qd8Yp8~OG zY4N|{hJ&Nm%4ysXMWj+~33qGOjXP&dZccN3Zl`|#8Gjm;V7M7f^kSc+4P<~znR6RW z@Er=LZzFo^Hl#hI_fpg1wT(FUlUs0TaPN&W-doxfk=)aTb7mWtvK3-SM2rV6VB@Y{ z^)?kzd{1Qc-c`|UXB{9MAPf4!8vqWOx?@;5;h(8nhK-rtE+7b(&j%1(7_uFw0RC;L zL4&N;dYoL&*9DK~^C64R7#EZlrvwNSi!kEEwzCihU-u_kHtJ~6;7V<8Xo{?EI`*ls z-BB&UYHIJXeyEd1|=5W;pBHptVtoL5Slry;Ft22Q(yx4q7#*H(F{V-uzg51c!3 z6Jt5+EtFe%j5{HfbHvMcXH;;6H2oIdWWm*^fmiY;EqdeUdL@{5R{AT+r(Rrf%HqtW z-AXPB=^Mzuxl`|l8%YjS@|0FU`!lhyb0%t+bc<=ROVfWOqRnD&){CRYeKXUGpyG_T zfhGU_C8<0c+!F5-hSEPtbU;{0TuAkm<+dcfoT*QAZFUtRswOfKk+oFxGjMCfzfq@6 zEqZ2qyV$)%y!k7+%JeI9azWd?XkuqJmIhbKrI$J)}z#=Fv6>u3f<@E91DxA@;{#GA-QbkP3b``ZKD}`irwK8K;^36+Y_(mi z$&4z8SgaGlXjy20QfKu*KxW>yQF!<#qGSGUeA=tF=*Wxvza_leq|W^Ldh)gWr6OWZ zYIo~?FgxhF?PnG7RsG;^tN`p&D4l=zfJv3>oqW#ai|H@kCM*{|f3EOY^>eXH_Ft^p z&C3j8eojdk06lj#9yJ_Ygwf`ewKJr_R81c)+Jk)po>2$J9TERH~l=+ArE)?!C zJ0u@Tbn=f$a;{yEA>`FM;t4m;W!Ura1@jVs=P4I^dB(0{GUJW=YrahlQ7`$ykm4~B zV4J=LfK_%|)HGs&w3ln=7} zHJRntCq%bi6Usura0)z3wUWqmf(VNDw$8&YG&t$UHNy9ELLnoJT(fbJ&=b7~61EF0 zwHE}O!7#)G5~=;#dFS7~=hHvmc}abHm9p9Dj*NC?NgsHdV%u_x<9fIS1L=KP)O9X3 zd3ADxH4L%a^vwqQN=HSjXR)~}G#icfgs7)1{>d*1TK#Hw|9(DIe5ZKl(T@vb<8(Dr znJEJ?=jSm1Hp6ReR;H&KQv71XIj+wWlT*9mQ4#$_cUVd!G9C zH7QS-AG97qe*a~Q?gD)BJc@jv^ami^K`ravIa6NpAi{1-B*S!Pdkb1!R61aB5)coS zP-3mnd-#6h>15v>=OQZ;5RoeFt68W*ufo84eD!1s)1EU4-O3{(3&)wTKtOgzFc73l zWa%Vtzyt3X>1nP?yZ(y6`0)n0egH_rBq{6JN!enZ&xezJv%)!Hkj4NgMo6mo-=oF7 zo1f~cyaT8eJ~L+N`LtK-*S_ZJHK00x*2L#4`lr(5@9q;Ni3V+vFz{OH4Y=`bi3QHA ztI8@DY<*ujx?FnpvMAL1bL@eqG5fJh_sEsTXZwgty>u4=!|#ux^52I#y4TjDJ@p+j zewH!Mw2c@3dPjZyH|s*@gmENgV*EMmsp@5k>IsTl_0i4W`>$*bP0SncBTjN)wf4K5 z`!QoDFK%Ny$4R8{bXXbTZ9f zKzi4)sOc9*sb9J(eC)%fm_taTPcDVt_O19WA4gAq4A}YB?Y!56{tk2oG+)dbKjS<146>x*3roWtQGqSva$Qip3WQ6i4LDrK|M;p+u!9? zZuYdEy}noP-x{qsrHdX4R|3X}fAzN0#TZCA(F@tk?7^q(NOqWW{q=WT8DQI#CKOK|6E^yLnSPPR+#j=?5E zw%V1+$Bzz3UOq=$49L~Dx62P=NP{>2&dk1%ze1YK`w;=m;#Px0`k_H1XZJrZTw9Ub z!d@`A(2ThJ@BM-QmltGR6T?7;fxbiC|IVd69NTuxb?SeB<&POHpkMe+J}}k$)&I5} zpg2>t2VR{Nx2g=>Ok2%6CMmh85P9sv```J}RrS%U=%NZ-tHH0B65{0fPRB?0*56in z9*aqY+&l?+_eTDh3%?Lh(6&}I8^xVz-B%66R%f9a+V-!4aQVACBr9iUZa2+Bv9$u& zb11H!sqi!xwU!g&re{$xS!6nFagvC3Ur=$&+PdNyVX~lt(N!ZdAN!k_YG&K~;;S*1 z@(ba!MBvUk#*L~~+VpK&3jkXmYTJ?<`-{6}$DNUhqL2y+e4I5mffCF};cH=PZo2yh z=2hyP5a*37r21^_2;rhl#J(TJ3k6sY!H(Pyu~kDwCmZ0bk`0W5+!IXPrzY)F3pZ)WzEMp$^!>}n_YwxCY$0}VyyG#3 zz-Oxe;-X}FRyB|yT~b-Uhp*MOY#2ijlGJPksLfwowYUekA4Hv;2M9cnP`#ksi6JBo z;q0?0xbm8~xA`X_mU zIl)~Qy~Bcda6zqVLCp=AV4_P90##;uW5e_!B{7|~-8XPd%%TfvR)P$`t{fxD;}8J4 zvxl0DyHT$4_vX1-Psxly!>3m}S_}zg z3p-47&7}>LCm0-eDAt`RZ)ah?S9(aHt_afiuk^WaoIVJnbE;;7ifA3^PFwvz!1jbX|KZ31br#-aK z#l`n^%IsT(6+SQ9$yoIK%y~ZIZ0nhtJZ1d~f>M_NEr?(@g|ubX7Og}; zVv5z?lTL#q+kV!a&1;I=`ac@Bm8zF~UaRm@E*MhlXBka)9fO|)@9OvZHfB(D@+vDK zus8*hm15fs4fuIeF^q$p>#;WNY%xI!SA>+QCfY}HZu^D`2@*Gnb1-HqthK%;3Y6%5 zX78}riU!5RMl((r0)#JXE() zZ(o-2sRiXaF6_;s{0~c-D{e>ShJ|Drv5_Rdr6iuuq`$lHe)V&(p0`D!E|CTd)=RfI zCfk&n+P66<`SMt#0}a(lk$*qCSUBNxULkb%6*KWbsY<3K6(#}5x(WjNqTb~F}>T<%9DM}{{Hue{SoTPY7X-bMK4 zQ$FPeMo%vECv0Iy?&I`RD?8R6Sd?*5w5-^Ae=E#_j%9MBMg>YQ*($%x^;x-+J2UqC zcX^CC%{IyDLn1s%Y++oOAU4{^UiF6zIj*|gO!V7Y5QZE%2vp(!Atid;yPRa@xnzVu zi6sd^K60L1umu;a*#N-Lt9~|%&uLL<9^rTy@}MG0St-F3`e($=xKHTR>9zXrLFMR{ zq9KBv^#A5xs>wymdYPuScaq!;Qn!=+p411 z)H+q?b_Hz!puz_p0ZzxxTXTJKW8dG?87`v^ht$AuP^76tb+VGN2tDNOPGO6;El9XZcQSXRO)}fll(hu*yPZUv}G{c>e)g%BN zkD>3FMx46RELck+L_L2Wx#04=f2t9(Svbvx{E`4*_Q%9`-x7b4 z33*s(ux{aAn!o%L9_>k6_}7Z@9{*jK*Z0um)b~_gV)E1-FHJK zT3MBv2l6Uwe}T4&UtP#ioE;6&Fx$sYN{~cVAU@GIvP^0pr&*^uJ@HW|?43KwuTJre ztH~{WvcboysMLmDJO#|H0O({fb+q(!8eyLj8yWe%iuFLs*--k_r}|4LSnsBAW0k0& zUbU9SpAC0$)eCC%Lyi}d)%ayDcQK3!->mY`3arx3$CI%kx^x?VEIZe+T#r^0`olf+ z_!S2HbZ(_;#zkGhMhjE?KcUI2AKls)J08~U7$E>F#VKHG{`WCf80w-6F4Iae*Nu=r z*vR~xM1PqO&sn}6gFYDuY3$|#a~_^=w{?Tf|B+V3pUHOL+{i94v(pA~cMY&XWMZD* z!!q@LuW&~G?rRc729Pihyf@N~kG{BQM7V7P7&0q#UBG${-J{zNTW1b$9UR_ne}BFB z-k121ujrAjb@$vgM|Tb?+Xvs*7T~mX$My%0h1QKEXpT{7V@bg`V}oxVupcwdycs!o z^U&<*i9a{@2aiY3j;3hd%*q_g)126^3Dt|TKb^pSx|*;E+Ggv52pu=`U+0@-H@l5E ze33VMoIJ#c`DwH?QysqD1VXbI{$@XBb{K5&ixDK4puXfrujK4pQQ)%3mZlFvgPRo( zH$P`zR6N$qSPU;RV$=UFSzrWHj#1lN7&h5R+EWH&le{#uqL0o6Z^bNy3@}*1ZP(|* zX}3wg)FqxHg;}s!o;Mwbni)Cp|AM)=aI0LsT!4|II)(NC`ulJ9$$#B6 z&fin1SUf2tx={eo<=?L2V#06suD%=3Z|nnHZUK@TfFxp`g92)`B<~ymmHPvT3dci@)*oT=9*#LE}&y73Juf`YjbOB+}z~rYMJ4_`tTx2ydpE}>- z5eWQNKZj}nt-dHYPA16tZFogjd?{$7)q{IJt_A-vj9)kzAF=gn^|6t7wd+&I&>Q|+ zHVhb_+q4`Xk&o3o?eB;+J$HOje&nuM*W;(>55DX=HrRP=S zjj-QK+Or9{j2Oxz)%c=ymdQ6qo4?`K`NwT+-6HCNA9TCJl>ZL~0#1B>o%X_VbH^u{?~zy9-g_J#}> zc2A^r-RX55@_x0{l7OSS){S^HwRFF@?zsN=#_cPKb2*t{)X6P`R=m{H+Wh9H#E)2Vrewsdv*GcOh=YfQ5-(YPKTRps{d{b) zqDR(E?$O=%KK0}whU8BU7=A}57RLt&Ky|4vS^rapGXOwY&>n;$69o_`ZJJ}RU=H*` z6Czp|PEcFEBp0qJkpRg?5&#(TWeXP|8v^j&)i)BZ6u4D6vf1NZY??A{!~BsiXHU-) zA5p(aPGvw$>{nm+enwjO9-mTWr4w5nV;@%rM8!VTh*-8h9X#G&`6MKwF!of+d}Hd9 z5dH~+;M1AZ^ygv4d(Yv{l>0g5DwXy})&!p}ovKmt=BpaiwpYSdB2M3h!Bn{!%zcD@ zW%c`U-r~!Fk->NRHO>@XRaK)Gvl7xIrhHnt4^zN>C)KMuBgYaw345HeZ~G~ zaBjf~zfIW@b>du$xrA?Ag9iYA>F(l!!Ej;%A82>JtdESHWPs(HA|_#mBjQQLosgap zyhX$^BkyfuD&snoF7krP*-thJ{`c`*n49@nc6p%n|5773!|NFpK61u>_|gF7TX-)! ztRCJMSK=7y3k%nr4%i;oQ86q{jQ8EGXb9b3s(YZWa?E~hnoc`d?L`SrInmkHejszr?Kj}N}(>NR9m^ed-hG8PB@%Ocy!CZ4_9USO(QOt)AKs2B4Oq0 zz9Vg9Xl?6UYi(2JyD|0FP2by2)Cb)L-_oc5YyI}-^r6DqCmqKx{;cZ`zqh6uI(-yI zO>0!|gv`XecAL%M8YR{LlM7jNQZuLd?V9Lond<6Cy}54k#>DkGgAZ!*#0in~0X{?_ zo-SbGOtVLo0^E~WxfDZjq|F-{Sem%Fh3lw;MZXsWKrH#s1(Fp{^Bbl%M!JBS&&KO+0DL9f(I973388k&_X3RT^win$dVDhDaTNB*yJ zo@6hm~8$Gt(Vct9R{(;dL)*!ZC&8IW<(dcsOIo_C{P6@J55YqA5 z#i`d7!*OmnYVEWiTKl-~&#+zQjrRBsn~inCqh67R-QU%T{Ob?{d1wu$=hM&EAd(JV7H9=aU~T%n(0T5_!m zYJ%eY)%Dw{rkinAN+hB#)iai+9n$WJFKL6AW-}FT(Q{%Jnf-L#Fts*DHVZqHV`}w| zem1sIvkHjtfjB5wZd0%S!G=wYyxJXJwCk)eLu-;CvTX|0*gr1nxH7d;z}q&H2LGk* z{wnG~upYiWaZGhHm3Ei?FAX^f6xngRuv1*_PDC@ zANggOX|T%d;4?(UFM~fZTqasL30V{fWLbV)_Y5g3^L6Qo_P3^-Za+Ls z<PfkMxM7q#0qVxeDV5W<9nQP2PW*^ZxM#VDDNm#&#lx*gR#^80*$v9|B}3= zmd*e*-VSi0S0!n5~l#Na;$EretOmpgXt0S2_v*7 zjX5s4`QK^%aQjpT=yb%`0WRt^w=*la{-Zlxr?Tkaj-_+$9b?u#0*!?6AE#?ObFDr4 z?z(MQ7@4PMoQN!}JBIyUgYh5p2#t^HzL#1zNY*`RyL9^%ZYuvvSM*`e8NEH-uSf^5 zj6lS%1IHoH_!%ZKz)MFaA$EKbVRc@vqE4!mDh3L8%cKC)$PmY?ScopX3+eVEbF(2& zuyIwYJWR|rc3CL&sbeX3W?b^Q(2H77k5pN&8TIaZR#-ife*EN_(c0Z!MaSPfQ|)iR zWIP{_vcJ!waj2=Z7z_Oivk!V~)yA6hT9J*jCCC1)AA-$C*ObiG)+!DSh|EeMKJko0PRaJ4Q7wBJ1J54s)@&bc^#UV$IQKwQW#~iWACo z3b4T5)}z{p;A{Sk>YM&`0!KhGm<|$cJMDCTlZPoeS!>jzV7ZQDcNhD55WK`6o`PTM+$C( zg(J6fWj7HR9Z)vDZ@)1RSRyfeN3rLFDH#$p1q<21zh~Gd`Ml@LU|i1Ey~dqSOGiYy zNI9NFafELYn*{*HVVae949L#ZnRx!k7KM|1w~hr9r3ibd(bf7e6k7nmc_RgSPdg}dF+sR2lR%+$KDu)6|h^*GcjZ^+X5v$uX8!7!0zR@O8r4ZWPg^ z=#R{6K({auSK5%m22`^IriMW_@-h!Pp?48MqjdRR4#b{=IM)y_L+#8PQN28%?*{Th zgG$RhyyzQ(=cLr~6@GaGQ9&>4Za~)0BhGS=eH;HzEzbWyWQkB$H&97@fNV<`a}nwT=vMmve2(VJ{*Ztr z$YvBW@M_|1qL$Cr-ImLcDh@!wXB~2f5kd8j2%86+($C7OmlK^@clVrwbRxR z~2*L@gvB=e3Ltt#J@@TVUapFAE+TPia3norAuiEU-OowkewKC zj6NpnX1K#uu}M&E46H?Frt(20bTE?( z{tp2DwkX%mfGE*{M1Tw|144N@=O(u(R$U0E=$d#KF9 zLT_9p8428`d+iWByN_I*Gd+NyEB*%_0W~Ip3%Jd6Ln5#czYW!xZt*-{Gt}+p1DzMK zfT=Rvi-H3D2H@3JfRC!8Cg<3Z%lJ+e2#c;{!;`x?2|U;Ux9>!vC*>{z&S}w6;|$=r z4VVoNeGLN?%GyWx04g0$;y?`PsA4g2|F0M>A0(NCMb4ws0N`mJSeJvI+(1z|kVkZw zdjram2b~5$l87ksBy5!E(Vd1I#2_q*a5zzJl8BNc$~~08^)blHT!0f1bPpg;#vn%; z;HPDS`$Rca0J?$?{Eq`81JK)JQQa5_n3OvnXa+t@^5iE?mmzf4EHC`KxXAvW`f<_%P~KT?)3y*F7V^Xm%c!8_a_ zOS=Y48S)p2N1Ey2Qj9!P_Q;Z_W8_+X!$vodQDUQj@``J{73m)tTsr&~03l{j-m1XV z;RvOOgJ1j*8juhX5tF(aF2pvqKicEDqV^ zc&o#{2puAG_EhH-H2VZ}gChqyeodfgR+7j;?_VH<05Sa1T1_y3Ae&Kr=+BNeobSv!>2NV~@aha6uj$ zkR(y(_y$T!K*lrVrZ!-o&qF8R^4BEbWQKeMSMEO%_$UXx9V2(1-#9Lc8Q~#`8z6TE zW+5f>=t#+3 zFFJCHD@PQ;Mko;VU-%0QSvM*4s1uSskC@;9k8&XU#W&10V9wl4L=_ixb@B$DfwsW_ zZTQG;Kjd~U{H7SyDT3h{kR3AYcM_<=fli2_mx2(zhIi^Q^3im#Cm(KN*GdRP_j6$; zewsB1gTf}GjcmC^9FTm7;%zTH`%q=V;ktVH8Es=1q5suWIdzhSyx$8VXb%jJ+9d(6IVpaLD7QXIAFiXN5J_W{naH{hcZpjQznZv$SzFN4LtERg^!C32K`L@oz-abBjn z$nWO@&Q3x}4CHCzyp|m*NCcFLgNAe@YaS#KLud^7sRoq8JosNRWViUP3l~9_z+28k zvmcXV=}^;7kWd7!+CZJ=fvW)Wrx~E*^KeZboH7s35Z~WF2^qx5S@Urx#K8YBa8)j1 z`%hFK2FjdA*zmxOlMpr$B{MELBG7RRNWBn0KG}ATi^_IJj`M*hIS2{>UM~h_0w7fz z$i7MB#d)|g2XR`miKOu4>LiFpC2+m?>JAJvivw}yAx}RC<}sjT2J);Z<}4B7!AI`g zfKo-^N(LyMacwI`aYPKY&{M6UraEmQq|>w#-X&iteUkBxIpw7qz1{Y#7bF|YglVc@ z8QOh%$f!ohcAvJ!9a3|FTnM~txN9U-=vsa5ma1awh@EAPdojV&!SK_B8Wc*kduqAA zEO_5wjbHp4_`q67rmD5vOy`Samu#DT|ErCB5Vk9OYzw6)m($?SGk@h{GJorNOSlVy zg!yM2ix$->_5zfMK+5yH(rn=HUVuE0h-ivz@pZE~@cBtr{%yqF zlgrrnxe0hpC1>=Zpl%bLeAqcn$VrC$Ax1leTXuN^e%S|DK$oy5fp`YuB2g}#E?++h z6mKA`89)^YLmupbv9k3C){57sjp9E$!peBhhx;iKuBOlBMusHGk zJha6I$OKTj{s&br0oMWKCq>}aHFzfnrm+ET-G)?=fXwFQ3dNXuF{Xk6(q$k|q@yp< z!CC;23jm!b0h&l4(;PWC52z-AiN3}Ri(qLIy%sL4d;Z%|02+S{SlQYX z8S`+J4Ui23)hIz+rX!h?vX{e!vKmMpkSqJaV!&)3I*SjG57|UfsBkkP(1VEH!vHB` zK(Z&}`2eqZWc*NyHP|+iLU8Z$Y7gaJ2vb$>H+ayr2fI4cpyMAX;SA51p*Wny^<6P<*-DtWEp=WpF8OOEB zxd5$oZK(}S>x=bkJmYwviWZm%g0u!bdB1N^hPq$3icuWS z0)7Nx42WAL%d&M3#g;(?$WF$<*>k&Q7$KX}YTuixt?wkv^o@w#3C~Zc%!VIWkao9r zKGq|sMfV>zMyr|cxtvdR&h@`+8Nth$Vm;1$Dh|Jt=Xm@&>i30x{c+Fi+T5SfL$Cbv zgmn49H`C|0JFd6&zxC+}y!1Zfmv@s-_gcDc8!aes>sVzVw>h%f3-oNcfMnYBFh#vu-B|flnQFDM!pqtP%#eDLI6&Iz*w0A13&M-79gHu{!>S25d*rOw(vpF{gqy ztab3nFuIO6^QNh&kbrP)q!;G380rdb)1sP4B?CyXu5xQvvuM?UGDpged`s?j|KJVFH`ENZ#J3-iC_ zCoGidp1qkK_JN)s^<4a-dJbLb3s#GI6dEWvzrXL>(G!07pl6Xx6+guNIX*Dg!~SEY zPvyVaqX!&wRl`=7|NA(r9z}aHqM^|3FWNeit*{7hQwq&ydR^Un>}PA_6@UGeiqn4m zTefROCqcW<26bE=EUKP*8B*Etu})-{-;vWf^Tn-mur^hRUH46$0NnT_@!GDM9$5J^SLClx&Co=);*kzn__$>@kv` zlFHD@XTn?{`AA0?ZS(Thr+@3RNV)RhkZqtze(xoe@1(4srvaK5rX+s$&RuOv3^ii4 zBWr3VSr3^-84=YDnqS@hdce3zPo`3`kUCv4Js^u{tE#ijPDcZ4*u#5KZll-SN z%6Z@WPq1B!BF;DY*NIyN1V{oL>r&m$5D9p1+ylzp-h4_guk?*{^)L25`}fQ4aW}ON-LuA8?S(x3ynIumYO6DQB;K9u zf%hiAQg)r3#X*0x;=p}k7Bqx~Ji%{J$LBkPC|Fr91Q%A4@2?j4v5Yz&3Pgyd6pk2x z`LTe#o3fd$Ya|gMF%6Wlc6b6VGu>=!603-$wylPbWkq!W_Y(XjXFGk-(gdKeB zcs-5Uwd1w=Qxib-MFsf{wsx8PRTp2WTvLoaA})vS`Nd*JJg%bDwI`QoF#Lk(9JA9$ zAyDOUV;0oh=AMfuELs}T~opF99!%B=)YFuokx$R zHm>!01&ulMs6SX#>J%x`elS{cFlBoC?wl?k``O(V_lCI5{FWNFH^-{8o4CK_xvw&= zy}h%;*y{~kxuXJW${#uy=XbtNnk%C77Cdvd9oScfEIqS2qFb`Wq_Mr%u()r$mDyX9NMU1|Tv(0Rutwf|xK95#rG2ngl?N8-knWM+Wd zv~c9EaAay~+O*|83=v1=%9Rz4%GAo#%+d^8nOa%5VP#!3E3z^)I<16Qw0_+p1D>)j-K~G@+LhEIIv2@#zm?Bhg;6#?# zg7g-flg+vMOO^MiMu|i6kHYYvC>z00>(nw*Py#SB=}X6F0RTjE?8^IdNgtyNC+Oig zk75%Y#*{!&{-vYndHgAym(~r)?^3kE*KPQ5W(>csmeR~4G!@GcCw6aq**kKxi+X(R zGf$QL#xS{&g~4{j+^@n0tIrx{lOB3tT>}ujRQF89&x%l(jWxfr~9DGx0SW^S24o za|t{((pbxp+QvRJ+my6Ie@qf_vAd+b?zx$lLUF>5-e{8kZH0;>D)AVCLTY<(!4=&K6EL!>GFyOndzq!9>*Ss0RLK{h>mq55o~x3F$Ni1S1e!jYP>H zaF`&ug?Q@-r@Pp0vc4B)rvQL9;jQpZCh_G;ot~^@+kM;u^g>)Ey2aZe1KvOnM*EDF z1oZ?Z7rrj%o3@7h$V!Vm6}Mc@NcqOg-F32|g4i`Ob@zV{(x0!Li630yeY>R2Sj0aX z*wB&G#q>ln9f~-aa(tIeL~aVkry`WN2cUL#@8(_m2F|V>X;N5D`4B1iO!hu{T!ZEWFA!@EOStchmvK9&PhF`S7af5=}2FUgagzk$i*-YZ2OW3rUJJwAVL;${}l_}GNs`x zgd^*g)-T%-J~AX5v2NtJ^8#Fpof{#m-i(2{E#*d!KoQ;jC$A`$OpyTu$(lA1Iuo&J zc}}zvqX?~%rZ8bTENKY4GMrUu+K7x2NJuInCmX>$XmC5rCh&SWQ+I7oef=ewySJ(C zz?wC!KkKbM*NCa*7iv!4ZZFt)0mp>H*Ue%q+XioahOKJpk~_9TK7cS^feII z?Jyel%&a*KmF`fM%Ji}YDzQWq`6J|BZ&3Q{CmpURp-f$g99CpXJQ*j18TH;86 zQo+F+7(mrcesT~SGj|cF8ZP31u7WPhDo|Yq^-V^EJ;b~!G(VaNcWkSeer8UXs7Oxf z%G3u^)*LvbUVC*N?yCT(fRG+3!dHQ3;HyYWwzy=F&?bskTrFb4UjX5r$U_@EOg|fst+hN zSTHw%dRqkBo(;2J5EC#$e3Ljq0VgGcxNdQ38|>8;SOC>Shn}mZaS)n-t=auilSmXb zck2R@AP~D^#G(A$lW^f0Hi94%(IFw401N4c&w@fXHLq+Aro%^WTcUl>lrZHIW)&DC zfUWTZb!2%INM!!ndZ?!N+JMRSWrLwR^`(|6D^5>Z3HdUXe}m1xR+bNjHpg0-)%D5% z@$nHaS2(r|YA7HKx%um!#|+9w3_?4HHht{56<@yYi?zcQgIWTKccondIXZaiI%-&N zyk6kH{_0UU9Z^>Iq$-z5g|Lx8@@fi7PT77KF?~ftOOAZfp#caWdrlW}4)RUTozKss zRkST)r5sxU!Xr@SuuleiABM8XyN! z+Ypp0lz3SU{b_<+mxRgLaKb|F3O{kO3N|))(PttzmJV9UQ7>e9(U~Aeh9(N&-V?Bm zkTiQq9LfR1mvXb)5UC>omycS)7E+7=~ApR0eSAdA3HWTCFi=o0~CISJVwk|O_ zb3!)`USrP$=X;sKnzef=xCA|OE zZ?1x95a@pqxsHWULo(K=fcj=}b|k8<5FLYo6V&+DWQqL(B2I>m?>@G1L72TH(gGw* zmYB2vONE3hsP`tx`4yhHYr6GWChd_bBi(mvvfwv1(n0nO=1&<1;o~T|vrE zw>mP^Xfq>PUA@M=UDqNL+i?MRw94AfZOzsH5$(E9N);UgHwIgEy*i$+xCC|He&PMl z^4@p%4MA40joFgNs}UNHY$X1Kh&t^f>nWi8>{QQP?W3@yRtuA@>tfjEYvvFHh`Cy1CbAWAM&Z{_S+;-Ozi*EztvO|(*_ z9Ro=;=0Xe!xkf6ufR9*A2weq$-jK*M6Xm$v4Q^M7Eo4G_PVNd7n6S{5-Gp$Qlw@Os z*?a`O7)gFF4v^&q^HIb$$;?`boeHp)!HneyrW_dnO>JEgQL4ZIzGXyYE}nz1V52w~ zAwN=o8&j+=mzZFX3;>oa5aH#bb#uAqeq!E|$bC*iQHi}cLQ^%%oh6BXo@>g4?+FN7 zUrg&XWNe-TQsvUbC5as%w4;kw5+sgnsuKXN(L^}&)lEZT{A8{dQ|zVEslBe_0Yf-v zU-V^)xJ)S}6CBXrm@jX?-4c{z*gI-D>AGvb$`Kt$l0w2g&Z%tYq*L3XY{McLDNR7X{NN>TEmvM zGUIze=M^meqBy`)&tUXS6cc`+7Q)OyMbvXI%m7jopn3BYvI?j#KXWhJ7*MPwWUtg> z0uK}biL(|*0ML_(X;_Hw0iddizN~(rP_4X?{sKz}Hibi@eithRfPhhP>xddeYJ~s^ z)MU_;pG%{|uFb;NcUNB>hkO=t5d>iXHJ3IgvQvl?CXj0IAuSvJge0LaAw!xVyCXsi zt0@KqdM1k!n&1RE(kWZWW(o~h$W;QwN(c-a0f{Pcz=%!=LF6UNd-XY&k_|hu+=2)o zfc=4C$rE6>Onl%(U;qL39J@vL7=l7TY{*8&H^I?rz;`B!qk?cmlv^g4sv4yyKQ`_L zZPfuwEYNKNxpG8k%@Xa2{;hC-RSq1ljF29IKjY29Kv z9VTDN@n`~mo$N^9zzJ=+nJj^Xq2o zB<(pj?^#@MyXAm0&1rSsz0oo2q%%bwR^~MjZrWQjW0ld|su)^_U(t|#&z*-UA}8D| zbE+^evmI;q8*Dra8Jq?ZT zC-{-G#Lu~hOi)=G)1FmA!4Wl58o8COo-f6*nNmx+B=II(haj|AKqhCR!rDL`rqGj$ zaOLENw25>I#9Nmn42+P$;`uBgJ^aM+>|FaM7?CB4V2iA}C2Jt$YNn9cBtg67`cjcW zg?aIS*my!{(1!44BglWk)?z_=CjO4MJ)hkDu|bfGQngnlwAp7NgcaSn=g%RZ97ky z6h$+U#_F^u0c;B&scr`Er64@{7q+$s2KcROe^p|?1a605q>=TZvpP#bZ~NNd1n2o{K+usL5}lkpYONp_Z70s zeCgyTmW9FfRVPy`d8cQ!WDu$tO2`U68*~;iSCQcW#m(H0soo!R9lyQ0=+GH_bl>fp z6E7;eKWzzqvmD#^Xjcq+S#}z^^eKA|(3*o*(sMt^40p!F9jMXV1?t4`QE_Z!DxvsI z6+z44?0m81B?RF!2L-=#>>2-fCqZH_6Em{K@!h%e3&^KzsrC*Ht6#T|6k^mdKocS@X#A^yGx>bpbO z{39$N7!G8pjLx0&J~~#M5gX^xeY0rJ1FD=Lzt#8y#s~hsCqoxQ68nm5mhh1{8&-cU zw||x5_9d;7gO$Sb(cPDYCGvk^vgk{xCw_W=pz|ah$_=+3T?4h&ceL=WSbaU#3dh<-GZ3LUE<=!-EdM}Ed^{~dJu*&*a zSM-A$ri19S(W~+_;VEMke#bgH-%dRr(%B%i$vf)BW}SX?k*>WV;nCSpOTU5GyRpe6 z@{?$vTWN_3@R>Yl0tkuTqVkI5l@H!pta+6%v5%GN`&zYSN`^zx5O1+xEEv za9xGTwuK~%k_&&ms(snD`|dQ@^28?RXVQx*(8Asz=SUnkf7Svwd#>p2uTA9e^u6cd zxRo|^uKUVNg%WntvYRf}bej{vH2fwZ1a?>#0BEV*(^4>djsbv74j`rJ695oS;6T1K zq5{ab|6T#XKAXu;B5;1{Qlv!{0K;vY{QoS@Bw;7q?Hw=y=OEVIm*VB3AtF+_Rmocx$N?oQK3xYc^U$%J% zJ=`_QwLh9)ERTJ*YusTpbGE>KM+wQoGr}C9;h^!D__hptlT;eSEXgmjZK7_oY&zX{ z=eEw@SI*V!F7gjXP^%x@FuWx7?wX7{7VKJ`B|Pf;A-y);L0{1H-`<)X?VBxD2u>?k zojlk-^Q2{4Y$WCWo?h>_4-%9{u11I6pjPR3oZfq~D(*@3?N3?H8F`c0|0q`&W#?_X z-E#fX77gRReD_j}a({OTs$_?MbBXQ9_{=btxre|-JJ!WvVa#sp*q+BHEdLG(wH}l`VwK#I`;Xc6#O`%*>@oP$!Y}o?_bj(( z{6u1oy`g{8>y!s!p+WZf&x3g-yY60Xyej`%D*xQel@bHSf7Tor)9JBG_&4=Djrzj> z`z68ju9#L`$+n1(f(sWks5IGG>3?anSHC~|_Soo&!OqX+UAA9)SF4?7G0TGuUKgJZ zA2LXIvp4B^%8NgDObU)`?sHFcZ0P-;!^)g5Eq8m8!5$K8~3)anaxd=CJ(U~}CQi#wf*O&EcWA#~;yjgZV)UHG#R%caHD$62#s z9V4=kByL$n6OU$9q9532R$Tw}Ju~D%0VjG~KEgA+z9{id&RZBwZonF!VY|$CC}Kq&OT9fAW4`4I zcjC$=|Frdf5eTwxZ1YTLk^{~T>rFiO2!?+K*Nhu2^HAV3g+q?2o^#j67cH6CXBvSw zTCt~|jQdn~3d0XP@$9=h*r2h2wO#MvwLqlK?L!mmSCR?38$V_V@iA4KF5ciHj7V|e zziaV{<*vP-RKwY`R+TUvx1fQ+`Bi_UY&7xVvOFiz>-^jGD~K~^=yNMEPIMS9wW5wx z+S%x4%7MjGL2R0Opw0(rd4`z~?u6x0`)PS71*B^z0Il61U;aGrI9~ua0*Sars$`|S zj=bZ6xw%^FmYU{eHaa=vcTDGKfx!T6s0rg(%U!k#F*G$FVGf7y)6v|G2`tRKDXT!gxS!hN_rb1!L1glYw6vQAzMZN(=A?;!=G6d) z0%6fs=QO<&&JseC&Z;(_1}zSK*rGqWa5Dx(?x!OishdtX-dwc0%|T%dvI?WAue)qN z+r)G8#-@qam_xR9XHw$}U6(T0KfWYgF1|g!H)!{PJH1+*r+4HN$vdNFl|~CAW%?hY z{;W~_Yt%dQrsQr_heLB5Kjo22&4<-*-6q9a9gVE}OP@P#0{@+fZ#=N!Te1BSY~tyg z<3sLq+oZyyb*jTKq14e2w`WD?dTUrP%4FG3fe2m7q`9@xDJMEzEynnO`#FZ;sI!;J zW`DWk*jkwwtFyJMn!y9B@;w^^=3&(T=Bjj(1VM!eniKl zFufg!I>;3U@p^~Cgp7TkWlV&SbTwN{mMqBBembqlbT7i`iGt`zCE9~N%Zr^xQImz# z0zXeuJtseYE(%3jiE&^COX+Fxx(vF|RHmaDq$YzG6?@}C05NAtq#vBi4g1Bx{_CaG z8(8voCn!(>OG}PvBxa(*$66r>dGS%C#EZjO(9li+;%(Qdcn7uhL` zdYznZvA09=)PEBuIvMIH!}7FTUb%9+o{0m0{W$%k2Xn~MIZBrLvyKeo0M-pxGTKkq zfq2z9BjJOBoM8az4#7@vxuxgZX4gzH>Pu_#D+Kac-31|~ErW+Q(9t$V)M@n|L*oUC zG{*)x)@xbLS-G8%YcT@g862?=U!8W$7Qy)#80nWn7>fpJ8C6`}q+%3LcL3)&BGT;R zz=#WVr1Z}tXBow!m9YYH9|1=4xQ`}K>){@<{`EPZkqEkyK+W_*h8T;Yy&ivdQ;6qZ zW){%GR9e(i-ulskTnhiY2CEH*ub2=Kpzljg$zB?=35l+|h~mEij<$)*Kg+8_PHc75 zmqSDYXyqe;`ivH!GtJrN|agYS^aeTp5gwp z!&{k!smv*5)cUOQjf)txUZyfLt@d_xtm2to{J7qp5`(_#SR!BfU*mA%#OQLX&G6n7 z>DMs96lGDQl||l6@>BeKolAG-ky`mX z$BQ0)lLF3w{wj=qAGIJ0BklBq4|>86bKtGxPalK@Jq}!P@MXaPuhC-J-K9vF*XEsk zNY_8u-G*kg#HCY33VJ}0-;gB&q=pUP9|=w69Fm-i20+rFCv%cVa~4{$2T*NF&B%W1 zG{EvO4;98U+yku`6e5?ns3pj_eAIqKM1BOa7P#t9-v5t_3F{~R;xLLi+KCmW9OX(c zPr@&bJ6{;j72Bv0r|bdeB91ZN69vE+v4Hj?E{e@tM#*>>b>>>^kaIQ<1q@h)L3Fvo z{uc};;gRG3gNA8}74lgYf``U)sX*DTsKKoNmD)2@y-}J}n>@@pZD(xc zUQPZO7@OdX`D$aZ5L=cqIGoHW{Eaa0WL6!Z7xrZdGbP-^*xP#*biB{~WcR`&$Zcz0 zO10he_MIvWeG<_VcKfb3qle8;FuSdbyFYGA;_7S@1oD-@=G$~V(NDt`gWLm^tmTp#m4-`PQ!|)(5zOi{(UvJ9^I;YlpmBFNJ&k8LC^kokt>j$y zUodAc!1$}eS^`6l_ghF5mc>G>8H}FIGiOzHKH#yw160<)+C?sn?rE5PUIW23VT(*; z61ol7#chi_o$F8zbDrh7j|iP+d0KSX>QtUVIi$xInTGLDOG?t0Y#xs|u>I~TPe z)HV=X&>?eYu=413yKFTskV2DiT+E6rK5|z8a9X0!j&G*^ebZkYLuZjDpKGi}+%74W zO_`qP1yIk6YfbWWXR1?;0Pl2>2?pHyi^pJd)tpvqmLU4$ESPb@?9{VeUT?-;N=fv% zcuvGVAgs#j?4i9Iw--HKbtD3DqwIjgo2?{uA#nJ=*o{t`ZfgzGqjKKde0e|QcS_3a zM(vC#O6RtoHtF6bpLPDKbhdAwS=>*2p?KQh9InWM5a_MrAKf~{%d80I1iA;R&m%lt z_R9RB0eElSqbmyi!Fl~Qj-j1QKU}JQ#ai0|jS&Xm48g~neGfyI`kULf`0Z>}n#cC* z&o#H!M7on8lP&_Xn*ir4iihAh#VPXK%AGzWbFXspH-s%A=ApW}-~}$@gSbPiPK~L>sh}^3@bOS_B_8ZDLrAXep zNcAH}Wp0l1ZP1YPRWpgZN z69_gSjl?UhLm0E6+Mp1-S`ao1paO+XHpy57mp%f}Cj0fu6`JgH5=P|62dK5Y!9h5Z z*>A0Y28m#8waAj%9}*0h6U2Bn?6JX99&@mFC>>6OQzv6O1*v;VF7*Acpr`S^*bJqW zQ<;6L^1npwRI~XXdqNLjj;9jP`_q*g1+mMHn$kBp+xn+3=+ZU%PML2nNA;cEru~zi z+G{yIsv1_tp2Zc$bv_;W7H~6=v31@y{-eSDXXyT7tq4`=itf$sc*Va#3cqi|%>gg< z=Z4X33N-tixRu|w1*XwGOSfnfCTu zKjKy)M8O6;-Ez>pLYSfDY3Fy$dWxIe1T$yTS~LR;_k1wjqh&tG)phQ-Y314I40u@e z>p1r_=`b@j;`O(PPU2qs$y1Mi5q)xy^NVNK#&uVxde!!u{<=f)8Z}fiPM4HcHc?go z>!!Yq!wCT@;GsxqlQ%&kAXRlXSUKubXSl7CrM2 z5#_=a1hM@i(29|tX%z7x^FpaR{cp!mp84YN+SK{ew?F34ceD*Bt17hv#&(oPXg?3# zwC_HtbogxrI;-=|WtHW=q2ax;+M%P`cd)~eyYy(P7^|I^aL+5h0;DTxdEMhvCNbM~ zlQyPz7wj*-uw^BR^4mPe>uAL}iPaQWT|CqW^nKs^z`{BR$db>)^Z8k_j&>mMYFukO z<9p6O+e|LE&(|$yI73%5UUqcJ^2$3hma2R^H>3A(H2l(Nh<>x*Z`J?v9`Y@$6vvw? zDOKd3q2sbFA8v9FXdb!vr+al{r=*6TTS7-{BIVaih#WJ+1rKe{Vk!goc&*quSmr|4 z)6%(d@!swqQe8AyTGYaL_cffp=xiAFVLsIDh|l-fKV7Q9WTtYa zKjj}Yt~dqdMizRQqxsgp|1^d68H7$Zho(-Ry!rW9%B0VCCO7i64Tz8+AOoV>Nv16=cv4Ob&hch9ECZmCMn{>@2~Y9yXF_6 zJNZ-h(wmp*d#tiboOx6jO)HR;9O%9<0mE=y&?O6}Cx<`I7o>Nn!K`c00NZ%yF(l-6 z+v@whNTcCq>|u`E<=y!sRvNWkE%T_N>~tg_C>}93oZ`}#-kKKm+Z2n+%=dV%C%%Wi zlBbDJ&9KevqkFXr_QChqWbBgp8qZE$FZneS2vXj#DDBf zH^^>5d=b7F7H;o$3o0D^&o#d?TYP3Icz?IBl3n<2tib-NK(Q2Zt$R@EgIjq&zUt5F z-&)H8*W<@^hyU_tIw+CJ^d zM)tv;m#(|~f4&=!3Hk`zZF_Zl_9w?@K8MIc?MINtlpD5Haq*uU8VX3G6=+-ji7SRQ zW)EV^fi~kc?Z!RYO`IF~5XRt;rVPS`{oC21Xn)GlY=SP8DKzo_YEA)He%!!K-B`YS za?R!Q|6ZzuG?+c@mpRzTL%ll>^;B{+ZT`Jze5n2Pnx2j|T_1j4Ha*lc@b8sthi=BK z>Ak$BSJz#s{p+Ir;hP<6l!ZOlp7s!^U=sCu+>fvOzHV@{^>wq%`!#a#;w(FCr@Q94 zO9~|FewNk56<5b@o{3+*@wj5evCDt}xVO|?{UXfum^MfqQPP>P*RDCQBSg!u zU4N@P60c4BXFOphk$onKzu#($=-D1-c$gKv|6N4?>`9Ne`}<66e$RV~UPr)YefvII z!_3Eje|(Yo_7B**jeV~}`~Dv5`{$&`>yF?5MD)GaUH4hh z$29%w(Af63w@*8U0tGj)}sWrQww|kF~H_AN^JRu$=r9741 zjnaS};TPrZ*R1XyI8o7jX0>?V8!S0B^0M3ASAm6PlPk>qYahMjsonTDyuZHvyZ;)7 z>UXWe0iHyij7uFOIwgcxMLFFrIlFobnTEj(O-z9dPQS%ReR&TrC>A!v0J=S?)>Q3bYyN|VB|9N}ztnG`NJqH|b z-(hoM36jfY*?UiXCI91vJ8m+6pmXc}0W5jx?-!=i4CCehzJ@fCK5z9ZxB>IhUfZjN zz+Ltw1)-yKw9E?>5jQfDcVoh^YiNzFZHtN7f_(waf)di*8%9npX zPusiciCbh3dEbkS%P&vF#$61hyhtDWFFa~n!d}=fD4c zY|XhT5dUS3`aXkOh1xKw^0#xO^cA+O`m1)d;^sxUE) zK`0=Kw~fOPDT35Aq-R*btSgWdy=yr6G+96g^_Nsc-Svsey3?DCWMH0FmHduneP%<6 zuTOP;xv$@6BGYwe>YH<}``bILa63wxE3QbJw^g}bTa30`b<|`x6_*RXxOe?ZK-oP` zz9=BQbvD+Dc`o+KpP6x=+*>Z(s7xJdzCgU%9O!8LF8bnG)bYZi8w>M=5B*Nf@$yYl zSx8F1Rj@>R^^$6!G{Oq>(f_#88oz(|w0Y2``}N-_PXzU+C~I~Jm!m#rzZ`QpM;*Aa z?cDzc3hJaa9}fQW^2c9gSm~44i@HDD`kTJxM5Nxjb?I>bgoSa>KOML39sc@q|J6;K z-(XCirZO(nVa!LJj@$o=8z5sQ(GIdY&D2RDl`_IX*|lj#Oe(dWPe`%hZJO~cxiXnW zw=@Ge@PbLS_d5j2mPyE%t9tuukM5aC6HR*D$Q?{EP&l6KvtC01dUyeP)l6^p^NE7u zaHkrw7of8`_?VAZvG^liZP|Q=PVKDAwK)p-)dT*NHUKM!%u^OTP2(L4Yt)ql;SSvx zi3$1kn?b#T(&DH0!cSlrR+eQwqpT|4s`xZyS|m2HR!&^L-uZHNBReJ6>TM%axt7Sx zYv50hNUt|aEQ^mxMs4J~$Ppv9X$i#8s>$+-XlVM6(=TAu0IKD~6OitOo?OdRA;yCV zBVK0Znq)AJ#+$9A^`!=Ow4Fb>^T5jRE6eh=}W`=`@SiUpL)}O@9HuCzU^k7p>tN=nqB}lA=4z( zJX5s%wcq$FKM)sD3UJmSzXds%+Z&i zrklf?jfdMctO*DJL9Y&kLyiU)@!qgVwOUy2eP-D<%ab`q@Z4wSRXpffuyTgxm(=A< zA*FkH9x6G;eyHBmv@x_w-m9g>AU;tAYE&t(B0kEMD$T@9exUM37dEtahL(?j9{!@+BA8bkI*9p<5Q+g zYLBnRcg~}&ljfUmF3yr&il*HKHZ9B4Z@F7q8n+h>J8LT@#**AGdRoIRbo-E$)9z2P zEt}POZnFiK#X3{3Fol;7X&D`J5Cq@awnvG!Vu~C%NDXqiFZMO4I@qiB&wG)E9V#5i z&lOVKjjDR|M>sj#z`eQlNYWFLj$L+z4P0w}T@$a%N-}+hCVM+gZG=6qv7OBwo-uJFs zGh~ayxsA3~FL3849+R&xAFZ{P@4}6^c?YHG4jcdLtP!2Jtt^>WR>8JTQ_?DS$yQvf zsfsVW@$LO_u~Uq`>)6NW?yg69rwEshq_fXq8ZK@Bp!;ejUfOG>430^Dvqj`?NmkSk zLMNOPoK5za*g(}esq6H5WoFYr2|+{b&9DDOX->RssZ_HgWQ=}61N zu@y&Q|LMS(@)l|_2WCVE$I2dS=E;7VG8ykFecvv2aeQoxxkb?!PwI_dcg|4X7q(Bn zzc?sZd^Wu3``m~5qWfKoRZg%*{H%#l4}{FfJvCX|eb9PM1hUf*8@8=KULW7)<*Jr_R)k@9dMx(U}pvqtR-J{LTIA-|=*Uls1R6~9%MOX*1- zatHtmUuf7(-jja+ey;r^B8|C&l6c>eX>GeC-(9P+E86jLvHy=797MhK+qZv{KNaq+ zs3Q{MdaIThs>Ji!TAq%0Pq&)7{mE|*MNU-RIIB4AzotrALotp1y5nTp`2&bRg$S8* z7+oEIR^M&_nSYzwEIeG)7M^kTP%(DR%!k7pHsE@4k*BczdtTGZEhCzz)y_tZTyy!j z?TMN~*eM{+alS6cZkX9D(#L3;;@9i(z#D*-r5<%yD?!>fS!f{7cc;J%gjP$r)AEYD_)PY%X!)!;wJ{LGr>z1$@K ztb$AtD$~Z%Pj2ym7NGNbIx+c*Cq@ObINjdr{EtFO{B13`S$0(5o8U39^Q5@|&99Jy;TXRW4%K-^l7qyZAeByEH z^O8%<#Iip7w0XJCr_0#H7M&>$p@?G@rfD5HuZK{gw{We7|LK~NwAXVn_4KZ$yOy(% zeB$rhr8{8D^$68b*1Zp}`ZcKZI%0*8NU9^g;^=P?YNV=>dUdpe+#1|#R#T}{4cxi8 z4tu=qNGXV2*Px$q+o|@p%h$p%qXfr&#pvn*r)CDV`+Ru!t{l#?=bCssXDQz~b|IWT zuwNv!F3fao$7)87{AxMVa@>?b4f_Dc$4G~7_Vr}ZB8Jf=&!>m-H zzU(&ht^93UgNZlyJK>H|yk?kU*bktf33WI!Ua)b)>_`b$M}mYPR&kJ4$qu zWq&`W3m{JJ)T_EpE8pht`GzdrNhuI&S15=i5b;w%Y?l*$($PEyDP#KiKA~>l^vZ%b zy+?H9uZ>IllvfKt`G!YdbCkM}Kx4xm3cv2_VI{5L!|j~FmtJUmZHs-m%3|*jtAMeu zX?tbhG?_F_ex*VklM@4#B$S#Qun6uVZP^9rBT~AX&ld_P0*3$!oeheWRc+I6VtlG8TA-u!&jI3NzU-fxgl5= zrU~NA^@|a+f-fTbfCBAjBibp`cTk*jl*=Ee@jmv!C6t_Ly*>bm;1ZGmq<$T?UqCn~z=;&vzXT|?c;64C z;Tc9x{!HO63sA{Qe3AfFFVHgrQR8$RtPblXK$WXV6J0wpc{~16VUofK`}{F>3UnBV zPn9FWI66t7+FFW$lwLwofB-R5hmoXHwty&AIpQZoY*k@06l4Jh zaTp>IxtI(AvR^>;5+Zm4%swSqLPz+hG(D!T^DDKB8OOTfDFUT-xt#P0K-mHKA_Nj8 ze0*4`7p>5HMMsn>NQnyaPdQ>208=RRlIrkwY8h%Ac@sne4x~D_hr=J$Zs;eNu#mdxC_AR0?F0uv8GfarIfJa?zUtM#e`qg5r z{XkSogS5IXO8G7K1Uvd0QI-Xl_H44x+KV|w&CmDLuOe5Uj-;s`yA>!&5)Ss`S;<3q zoQ=~khAD)P~ zs{JB^lb_gt`sxMc`lwb`MM*e=1i-CSh(w_1+_LKI3e1G2ZxG7{8W)3xyTQ~(K+}qA<>VciHf6#O>Yd0 zL#EMDehQ)i12HStj^*k+7a(fr1Rs@luK*#L*845j+y$ZM=^8fNwHaKEgDUM11wN8! zQ>@gU6p$fEd(K-UL$0|<4-0Z@evn9&O`}NDs96BU29e2JEuLKDr6SMD5j-y54>+`5 zh+3?~hX@F*%N)$4D(HX!qZUtSv2o=>;wV5U2QiZng)c{s14F;rAwHwm3sm+-yItMS zhVqM5RnhT3At-~qK>fbZdHZnO>_pGj7cGPHhk+SjEQwhh{=1=A%4-d_N+8-GWV=f z|JaLFGo{rO8ycbxQ^Yw6&dG`Mnl2b2b+p%IH#8{_+J!p3u3G2k7q2xawvwKph7(|Q z^QG}N@*;rQB_#^Lf(jjVNL7&p&Igohpu|kXiPis;dR(VqdF9@#7tR%xX=|59tuC-z z?llcbWUQ=RAeneo?Kau78r*lF()gdx`=SQ-t^}<(sA!<*w2hG(hRPS}H_*sUQ94E7 z$-8n?D~FIHz-_40+5;d3bc9@i+o00rq9TvcpYewelHTZWhC zN}bPm?EDS;6@Q&nzS_vw4@u~V$-Z$Y=Gvhc{n{SK>=U8!|3>`5?n?Rh>b65&SubXG zp1kIi(|sf|I_S_U(ZIF+JyGipT?7BTzW3zp$#1jkUR_UFV|P*b`qpIL*=c=`7gT~0 z{c!S+u-2EK+m64;)?B8O4uTgqf5aX9qzy5&9Y-M5p7-`&a7}dVBRKc_q?mVponJt4 zkky^3EQ?rh)T6QR2F9GKQ~GR!t|oxs07z7Qidz45&RqgP`%mN)JI2sl5423*sy7IIY8#C zG-@9rsvzR50{>8<#wL*`6oeWHWmJy*q|%uSCvX%xhe5Sg5zpZumY!%M7M>?DvW^18 z00>WK7$pN6mLW92r2+_Ikw7;suH_Bj<2kLpD8FCF(8~mfX>~h}U!m|gernPLp$?rq zO)7vxtYnC#HCTH8qO^w=I?Rxzj zDAEy&n9d}Z&)h{{d9rI$(_$O~UuCl4hTF4fMA6eLiMOpR;~Ty5SFoSVG}TpHe6F9l zbBZZ^Z9CPKKHdKo7!&~l;zSrT7O*B z?mu9gcPWncO+_}B;O|ZCb51g--cdviqJAPM+MT`(J2d)azPgP3aZY0?UHiuATqwN0 zaedUK%@;!JOjh<(S-%*P%s;lClv^Je%)TCfL9cCH>LvL<ED-;4~_mTg+7g(Jij-tuZ^{iyvdNeP^ls*roRn-u}z9>_o}CKb=B?%113Ks=4W$pkHo zIDDC@YgM}jPShm&V9k`ZHS6#Cs(7@5Q49!mcHtl7Gn|U!hnoo#nK(Q{wp{GR46mvi z*YgJ8-UMw%0t%})0v1wiV+9iGAde+8AQWW^^}Etm7nw8KKI2yCl`j@BxKknw=IXC3 zPj{nV2p)zI_T1N<0Wk7N0XCr$ZNh_Hfdmhxc$`M`gE+yoz3zBa9;QTlp#T1&6F|)x)LCtI>dW0A4a41kve24_6W(D* zuip3J=yi^-);O{!0?N%))+L?U{QX&S%gt5K(r#(GzR-Kb_tG<8fBTHC^#@y|-jTKv zLsk5*2mVe<#Fsq1)LESXmI{PFoK`5FqV~TCX5NiMO#OA0p1MU)0A(!}ZkE=CPwyxQa~N)9dzSc<1MH+i|+ zILOw8)srx3VU|`7gD8`Qe2&P;FH*eebXe}NB^}PHA9Y6^xL`56>uZ_|F|4EWXx8lrQ=Ps z&$4pOZA!din&PRqnuaVlOczEh)d&A^c*y$G4__uPUXR&ZFI-$eug{Dx7|a~P?_lCL zDinl!1e7@w90`3iqZ#L*=J19){;pcIwt){&`T=R*DIz|$X`AhU3K<>1z{h6tsEwTU zb`oJ_KVpD&lQo`brFNyOc!u3=$cvA_ic61hdY$os7x_*nCk@Uy#H}88-Sg+kXXECM zL;f4Cq=i~<;EQy-SQ1x!KdG&0``Xo@#^L8qXWCfEwLw`LM`}d6u$%8ZG~M_IXLi^9 zdl+d`kTnrg1;cw}^7P~#*DslY%WIxl-rx6|aC!qmth!i>BZq>Cwf%%-8BdrjyrHcxJJ?$I5+<+!HNs>td?bM6tBrjK;a=-#;x|mD z>BlH-0KtC+?{iy==HBtTr)zY4+_fIX@w=$CxTTE9g0e~Z;8?;$#0g1+1>-)dgDMsN zw7M0wCx!a&$y{3*cXFfODCvyqA!&zFfp3nd1YOA$SZXLY^24e9O+!P;i~wyTS^HI{ z)PG-aL3Xc5e$=YCA@z!el9woPHDhw~)ODy?3a$9%kOkhK@4s(?6WWO|+!|a!Pnw_< zjd@xa2w|9H^jS0M z=^64lYP@ZjP&X&5pTv$UNVg3xn7C9)IqWVaONtBQ`1^{Qj)-4=KNoN>-P>k!FwCxm zCXM)1s-gI5?)(RBEas=AQ5b9$c7HyN9J+gziH1ipH_UsbRAX$israx&}g*njj zJ=nc5uMf?a!;np{KGPfPK^Fbe|0y~Tza-cHjXz8gPys=4gA+HJnVK2k#)W&9xV5y< ztgNh1bKU#`*f=_aE?jxbNq=@6UBz?@Q%n zYx@CVxcY~^S7iD?+@n*#SKlI#L9G6_x?IGv0pN<9W)X)H1w`MHF1|JOzD) zf2fg7{A-`5+TqzR36p&M0oL60;FVHp?ZbBXwI4SQ+ig9(Azv7K>vQiJ{tMMxCp5MW z|J{Fk5?>wUIL$X%`$K!NBW>TVNhm7KZ#^;sq`h&)+?E;5B^1drHP ze7dyI_x{er!T1sT2CGGjPluNe#KWsj2cuJ|^Mv4jm=!6Qe>Ho!aB4v4XfN~6zuIdiww}uqk}ULK7o1$_w+&jO0~Rvi zcC<_QsCcCo2MCf%0l+M7>*J3k-GFdFtw;t@<$@FuLnyUHAM%9Mpe1qn1znOtI3APm z=#6b~&dFjhr@>5_Hg#R~E%@E9aOA>Ku=;XDU(!R)y6W>oq)7^mz}X68T$JOtuF`Lt zDXw&32!`j;?~zA5(Jqmm*z)YAaNI@@9-Ohe8^b@caqDfaCG9$@W^#9(C;Q7^NK&M` z?sNCLVg4YMrMC%9{5Mk1hh2rop4poPpF^6Ka}Y|qj*2)J<~f6$sNV%SR(Yg#1!YSk zCzin(VxtJ|!r=N0%-%2(s%_r}IDAeXR1jS6~E2+{?>pDRobKa1?{*wy=;K14bRH#}MJ zU0H7d)lP_?HI4D>IduG_@E>?s4PnnS$6XtIqtTkXpn0UHUhJMllp7vf^N!!QDO#ag(LJSA`$OJ`(}`? zOHI^zvb(6X-KHAiYNn}xdB%OO@jgH4YQb}dy)RaC(};c@-F}C=a*aoFJ0~|?#|J#@ zhS_KYd{R7BkHAf)$z#I6k+7nL)ug3`f@ICKZS~+xe##gd=_W~T=0fhzKy#Lri&&67 znt8&D$@ghzy2thl($EtCx+D_0kpwR4Nd7I-+Glhug$#S2;G8Ijza3DHB&8pBXU%~0 zb+b`-qzCTEmA_`gYUa}&JWH)Zp(o)kycu{3mwS2yf5vOeZo+0I(q=pdAie-Q%6E<% zxlTMacFu7{%aqos7$AeAq(G7qFn}5t%pe)w_l9nh0y=XfT?~xG06lCpehA6`=Ttq# z5J0^!epwMIs|LEx6A1gRAAm7pTxnT9?^eL_`5XAWrFhvHw=<{=S$qH?;N_pHkcgbeU}A2&R;( zUYkNso1P8M0gUJ$y?L+$5_lE~zM1Wu!-4T#TArqdUJN?~;7ISaE+TSv zz4i&i<^api4RdQ@y7HaS8M188QLJIaCNNlw3qK$`w;~6Jgu_+&;0y^6w@3;lsA&)G zoE$XNVn%$^j4aE*o7|)2 z@E3PoP*=O#!PxbYQ_;MeF-AJldfZ$(Z}xdvVxO3u6i z{(Aw!7NeOgWO#(K8@J@hP4q;^{#(&!9$nu5OunCg^GxwQ^nGrszay*s;wCX%BZO_R zzglk^8G2ti^wq5X-*#8ObV3q2dT&o}s@oH~o_#EJCiHV{2INpUHW64?O;o&}BF2@< zv8UU&$U2dLBp8E$%qTukKsvkKfe+rlOj?QrMMwcdDnxGPDt(?oII~4v=WvhF7E3Vs z#nX)CG@vgP^~*8KXaSus2k)Yxk4QmBBxt4r7rl(UJ%c_6fa5uYZNP5Mi^}IB4vfKo zr{^5Rt2L1n*>jIzWLOGKsoE2LQA+CIY(vcL+UkV%1K_=jN_0805d+Q_k7%-y3YM?4 z7S`SebxlgTO%=N5@#7S`j2jg_p1ol_2X^h=HbXSpKI`wQt+dN*6dR$xT4gee7-WEL z#8BomvVWtXOEFYtp_{o)N1(98L|Bt#8+`$}Zuvga;RW&-dy~keZL!th`k9(|5d7Tj zmJNGRgTU!{vU^>w^}ddb7$J!jI5(vP*S!MzB-$hcs~#puGrz?T2O4N);DFeuU z1VN@EFk&Ue5*!_lUp9IcKtr2T(4*f`y{u@5WI?;U`nni(cxd~^Fyv=Kb>%Q=%`oaV zt=jHAdOsK3M2-8f4|#l9DGLJ*1LEJq82`gpZl{68^X{>U>S70Yz8Hc^xY-Wi?q(qm z?^HI%^oc$?&-iMDT`ahMrRG&-ZqeBNQ=5~d$p(C<#1bvPb36THLp7OBHKG@4Dx1^5!60SSGlalCaY_)~P0BKi%^m2f|pKr;21}`|xKwEa2jsr2>`4O~ zlfdhI@$;2sgb^3;roqxlh+$0Mxjv+i9LHX^AT+@!kCiYOWU_=B#zE4kVDfTTHw}rN zQEsLw6FWfU4pgs{q{lfF+6)tO!73PV+l;au4cH(?P;j9&*Xn3NSQ@^k>21fEM55zf`Xg8m3cmhd)=bp9{T&kMqh)m1 zdGv19oCQmHn5}Z%{Lb^8XK<=lMo4$0ANLcE{2+eW|A>8NvZHT;q{8MvU+AX(`rNa< z9y&IrH+LuvbTz$t!~D^9z0wiLGV0vrRpUo zQteS0RFV&p0RQGm)H(n?E?5^6xqQltvwPCn1378ho!-hEkL~`TN{7hz&-?n6KiOfv-|eurV`;TTOn0TE1NL`S zgQIp1D>}Qhl4Og?tDN#I)3Do0ib>91Eh#KWtjggiH`A4mQ*Y*sb}l3oshjyy;`fC# zjV?YoHQ)pa9r}qgGF6iD zDDdpGFVQ9;^YZb(U4O1lvils)e=)hfZ+iVw=bqyI&f9CW#k)+BZe&LPN*SB%ioE+d z`dZrG^X)D}p{aF6yasU^O)ccG`uhY=>XBv&>(aNEOBwrIl4m!OAd z#Dk75pF32vJ;DS9xc#j85=+*?GSarr>1xqauf|R zj0#x{b6-tb$P%>H%dyBzL~ms;)FiQr8&Q$PPF+Is5h@vhl^4yarW|K1mgbe5ZV*RE z4C`UUIW1$jms9#sRl*a>f@MM~xwM+*!Sy62Opyj4D&3N~W05iS!=)&%wMLD2=1b2i z=0`3arJqMht->*jG|?t&94oZnE@C3etZ(G=$ONUyzhr{a__%xVFUPChf3cy0!W*TK z#?rr*9M=yyPqXG5{(i>%b_iutUBOs>p0{*t6Sa1a%Jnzgu7_E#w_moo`MTwD{tDGg zke#V@EW7=5>N%|~#Ej~4FIq;e?|-M+=Pr3kf7K+sJN+^x{(|<;7)?&@i}+L9r0n!l z1*hjS+HFr$9Ygu{NOLQ`60w;TJd7EZ@Z2>v;@yv_?(g_tiKFy=hnjYtq?%!sfqtc| zzdv(G>@eQZ>=P5G^+Y4GA?o}Hpq&*4AlQI0mIlgw@}#oc0|1=N0T3$`gutkQs5DUK zoHQNI0T6CtFEEiCio-Mmx?{dk2T#^wt}`}*b^s;zlPD002J)AiD>Tq%eas~oTCxta zGxD-tjw}sExNG~L=mGw5xgmw5wV#yEnL@sL zRgt$%4xMkA4h3&tmKuDO7dS4kHSX4ktdDdS=S|OPUX#E~n34jgrX6_DC!x&|3@q)+ zqH;E|z><_L+(evLil@P_YiuOGhpeff8sYJZH6%$S0Go%YMURD>Yrxn-iRb|w3*NX; zkG~)e*53!T>DJ8QOC$q`C(*@`Xd}(-L=VdUsKTDb8A9bcaEhc9ETqvI5t$U&^V+o2 z9U@c4qrr0ZSPw#f$xv=|itga8eHuy`XlG7wl zWp8z;(X7z>aDtSl>klgXJvn({)13_J+_&@WKS~#W&Z=$9M}~!t)^u)NU|`9dA>&0F z*po)WyK>s#|D*Eb%Nnfa@6Zh9rCg*?O1zaf0Ciy%x#R2ARmW##l5DQ3-R1nvqdMM4uboks_s{$8h<)~B)Ajlh<$Zf%&+l>XxiRq5 zN1%QpqeME7yTXMUv*=BpB))=D4n3*CYc>hs`}w_ma`vIag>Y&HA>RdR*|OT~&ia56 zv!9V!1DE_*P!G#z6P3Pvd<&*u_0p_62`4DBU$}$ccHQHyhCs7v|4#GnstppYPsefD z2+}jPsO}a6yUc-CgU8|gQvS}$D;_-6TnYHyJDL)OD${rC2+sTOQ_tCuDfL8Up}-z! z09Z!u%197_A4NgC&; zhHo4l$XdUsn?xMtcv;JCmqzOfG{_f4w&E3JJR;b(pX4=4_=()nXNaG(@HEHwmc~?q zHRye(LbrpURH1A6-I}@kJ3yt6@r_FzYePLw*pgFkPiWjF7T7LfAPMMsLbJjVngdb^ z*L5eftU+&z%1aw9b8MSQw{8^97MrsF?!QZH6KB$qNLQK0PzM;JkaUn?4WkzAB6H(M z%H!{6)yLPMd8KVR4R`rVZ8d!+t05(`ec>r` zPbqz6zs-3g$`5#FLTTf_2MZo#-Yb0kx9HpH{{Q~>WB9;>e_m-gUc7Q)&%J*PLL1-X zhsf)JNLSXrfA1_GwxzssbXaqnOP6H+HJZhFz3Y5;^RWW^rDmTOZhy9~Jb0Ya4P^zi#E| zsBHXcK%d2p9;{T)!2vW-WXt)yp7SSx=JG-3Sehv7x3Du^WZpnOxd$1EXqXLwW&IYB zl?@}8VaH#g;(d(5)i8(M{0Cux4pK9 z$Iz)m0$~nvf_i3=+`nzX^GAk(ZDarU;{Jio+U0b|juJQdjP4^$S8N$(!M%Tb?g(|K zfmOZN#tmma>)dTmynE62?mxdrE`I30n6T9X*qZ&hu07{Qhn6T9Bh<5lZjzjLxMuJ> z$uh%g07L;MLQX2NHs#ztY39p?jaK^eaM&0CIRk7uT~GLYS0jh4s0})N84IwTY{u|l zCnu{;pAuJ`yEC1BNnui{KfDL8IdtP-m}naX-gnzOoFmIpZ@5;pu0+Z$`4udRk@_D0 zCJLQ$ti|K5wZK@{PDij#cM#z_Lq*rRw9a5P$W{d&R<}DwO+9mZgFuAL--90Ds^}(| zgjbNjg*_={p1KTkLmS?97oGIE?9q4r$JC5OgwxFS z&-de)Lnkp4?wO)fo#zenAurUs`pWsSq2BpT6MKiQkN0>rUmF>ZGf4I5f8^w~=HCA) zwrir>tC>G?t#`|~SpOxf+hz(qWOw{-aloERovDekXJ{S$6`_ZZFs~cDv+Mk?PT1K6 ze{UXMPn)Jn1F7WW`MUM6ZnAjPnfk1v7SE+?e*K*?T zBzutY47*BetYqlPO>Yo@kvjevIG_2nht3j)kWjHTFtQb@F?#G!6E?;PmYL-IaBip_ zUm8JzC9*_=>g{qQD`ZsUGwpu0?sgod;TjR1fY7*|DZ(sXIZ1U+?tsMugk;iqJPY2j zhjFvdAb|@XwJf-{2j=Oc9l{Z@LPV^@%iW3h*L6<@QDB|aTJo$F+t)G@p>c4bLx!)8 zcG6C8s%`feik=F&l?yvKaRHMhGSNF3lP6#gd9KV%Y#hef%kS9SpV;wvOMLQEhdce5 z-FF6@-7Xy5Kz%y#rqOFOb7Zi@|7nrGmifBMKcy4T`t^zXOIZD%0SCIh zuUnMmzED>M&q116D+eFs>2y?=xx>%+Mzoh|9e;|_>J|l&3WC6Lko7h9 zTmf>*=`8*v=0A}W0_0QEa2_2TFXm%vKy<48p95MufBSkeCk}P{#c&EvogR*+iSnqu zhxaNJ$kHsGf_RKj%c)>97sNyeA~^~g>hs#c-rw21bND_o?@7o^RDu>uJHUvNBBO*q zA%C6fEYkWD40~dEi%c%93||j19P{0frQeZ_+)(O2!G3z~dinj1GHX(cQ&rf*daQly z(;vJMx$$JKtJ~J*k!SeHg|0h0{ag7KXMX%F+rBDf-kF?%J{50?f((0O7H}8Ec#!ck zYv*T2pFS(AQ>+~TA-k;iBrr&2;7gegXfsfe{7Uy0iQlJgbHHa~#${PJ*%QR8O=jHu zBT53GrfZO@JSek99u`s+a9DVa1Y>eUO&?<1bYV;>rTw1T-A<2deQ%`^BcPw{^zR zWUZ%l=qJ}&BI+~!&mAiynyWlpeF52JU_*>)cs&6*H*!TU;L_Q14|`)`Enj}#6Voc% zdcB^VI#dRZuxJl9*#5a32M?TGeR~&w(ji>}6O;Ppa4>EHF#^znhr`7HvWBa~ip4Ae z5a5bcJ%ESai9qaw5n`6Pkov1I%Bt0yMWOH!u4?RPNFw^ktvj0!8Jphdm!Jp_+ict-$2K9 zaznQ6dm>q#ci7=q6At^65pvBby5@TH3nM|SYgeD0gHqemw_Rn%QS5tV80RfG5km#~ zrEGHO>8=erTOPzT+?jcpJ9+G#=ckHt&no>7Je?tWmt0)FU3cBmZwvH5?%TU{B5x&L;TBFD5(Z&cd3G z7e25KC7wy>)DJ7>v-J;@JQM?F>5XIqR5$kX!jZi$p$1jOOqxkq4;TGd97sWg+ z8gKKI)00=IySZ<7q%iW|U6JbGWLGi_55(uU-MfXInV5Q^ld!b?kG9*L^ot}4>_$w< zxswB+6X#TPPW%4tt@1OEa$i2^=>KO!Sj`b(+kX#spkDpn3>&mm zPOka;@$!|VnT|e0@8ICp%us*9VhGtJFuubjt5M{zdPQ-hxg&ukQ=K!_O=4+ZlQ=7# zPJL~;@S1rig|jYcP1NH$6iF#y;ZV1&Znx)tx`QdTS9@}*-bt*2VqJ4A>G~Q&+4H-& zFtPpud7N`$6P z@DPBfEi?lpJ_n6`YpUGBMU@_P0;!T6aIZj{RjoHWQ8V`5!gW0^Ir z3-#|Xbx+s#1Ls74J&65;J{9qDvuI|1o{tIgE;_n3!}!5Nt>t3|>?c>k@+!S&o4m8` z-;KzFvO1r*>cYc;zCh*m;GH}D?xg$O!UrZq`Oe|tm*;zajiEBnJbr|HY5}X23cvme zmPO;o4ZB37*N(RwZbwS|Y*aS4>N-0$g{XGjx7RHF^>g57pvRo-hV|c9pe6;0B%bxm zqSq+Qv_oKn0p+;}JrI1xQHYKcG$O-n!f_lEZIGjr0i=WNHf_hXO=G4~zT zx$Eq=u?Fn27;cdnM?y0X=^+}oaL9BsRXzTJCF2GUGq|-&-x{mL-LM8>NQsnkDQ5kAi6rve!tXkvnScAe!yEPseTVVY*OO=NFzm4LrI6Jyq;le50Y%R)$E5 zwzm0Efb-2amV0=nuRiup$n7>FdeZO<9z>O&A+CX$0}C3y#+{xO1}T#vo;IFCB6p@N zto)SMIebV>VEQE!@6=sGChinb(}gp;`xhEL8(r<0hK{d;M}jt2rOX9a=5F*eXQ{Za zqaFNy&b^NBw+csU*vY|(`fVC`Bj%Jj{LWX zx7+2bA+a9U{qZsA250zbOc(3Nj*!A%VU_WLuy;GddR~4lwm9*l`=WJNu;(~BWO@!A zDpw?s%|`x&p~G7~C*HTc(Zlz*>%gnG1&v?^m9Y0H0)O^s&sVb5^`1me)bP@^VniW+ z-UC1E`l-aW=SxFq*kIq+63by9roY|!zPaZ%?b9a>>|WuL{i-$F8e0_u&^$VF!9~L|xv1=uG^<1AG4pDIH7o&%PPG&o#Xv`R1EL zNkXx@!V-DBp{3x~ebgn>|w9T}ZG?qOv)Nr^acJsF>!9!de z5{pxQ*>+Ec{4_t@{dmN@y?ahGx~m8FiQ2|;Cv-{lRLswh%Y&h2o6l+=3EKtxp&3I% z9WCKBPcg>ze3X-|U5k}IB7@Jx12Ux%Cr=X}I%=%!AKP6o)T47|wbULgC2d)MHDl!y z*|%j&0deOC{}`8NNH^tqVXISpV!sl;W%?ga56H_$`H7dcRUdd*H3fIcQ{Z5l_z$<>W6{EcHb5;O*hW_$e6s}V8!6*-+JUzN(bmxHa zG1jV~PJD=05MYBXqzTT&8DBnMmq>BThC3*NG-~1*cnJpXAg-@SHgB}uMG;z$*rU?? zPiQ1)6>J#6oDW(qA-efM2N@h{Y_6@vsME-W%vlr#YOjbg>)NODkSWq9whz;X@W95> z(238s_c(Ba6$~PrmV}ON@l25X>l&Xwq5)R0oLpykE_nB?J-?Xi*7;cC#}5k@_b>&? zB_R&yW-P4@m?OVi%(k|;a;&dYN-cEv5N=7;=?)d>H8!Q~EM}fq#UHUPnYB>=Zy~(D zhZ#MD)AsG{^2q2dq0`-6c7HWD_sot6@lhfqd@r**4PySvcN{#oj48B4Uh>{==&EB^ zYy@_G^~^QLvqYkjg*h2Icdj@g-FVcn z{MVI`$w4!id3wkrn$z9xJb`zh{#Fu0p%l|h-mv%^l{CQaR2Z3@4MHqSakQv!FgP7Wtt2T~ zvMI!q6o zCewr(-7_G>G>>w1$`gTKLHYrMn#mkfWvlXzn7#&$W(Bx;d7_YXrB_km$vi(IGyEsV zGnn{hoZ9yUIk$++bEqZt&XsGnpqu&%@$fe0%IV8Xk7Vsk>|-Xr+bw)p(1SyKfHypB z(zUj3m5+HpR&rDDKh)zNk)u|en9iA2C)&NbXTYP0^KM>pL{!q`ea z^f;ven=dWKczl6}alBH-BrqF9y{aW*(7ta*kgTw*8IOWCS|kXW9XzBQ+<+Tl=6gMN@HXBB{+=dLrC-V^6D&kPc@TENTbyS$zzWGgoMmGb37S z`}fRx@Xx>=z~S5pZ+q22x0 z&0$sNnak^3*rS+%c0|LeveN-s3@uT~tl+*L2?>sNOrgrE73 z@F?)JEUwSPC?^p7XgOTvffFl4u`+@55v4tN&p5e0lLW=U#&=L76+*0fppea=|+kk zw`r-ykid{QU_Gt41JTUTGK?6&0qzDE3?c80MH9#x&C|?fnDv1d7wL91Iup&XU!|)~ zb8P{L4kb1ujE~F(t#|VcC4dzNOqix2O_;(mfEuNMSL*2>=i-R z7Or3u3HZHJTnWh&wXK5klW(BlrisDk#xe*G-qy`G>rwhsKTOq~5A-*RqoM37hg*E$ z(TJfmU6{{zSV-Kktt&WDt{mO;_Q)dBHBWZ#4{YEp*j%^3LV*s&gg|sfe+@0kr_=`01&?@KvZb z<)H6jLH~tSij6f};&eoyT10f{CA^P~aJp{+8K|r^U`)>gpOprars>*IHG1V>iBF~3 zs#K+qrzxi)o1}z1fRIktTX=?}fDO_?DnhU_i>{x?)o!9C`tnTh1Bg7j{xZ`peIO); zpm-V)T4wXsPhe#6WFu#G_PD|dpazr`=rB9 z*l%;0N*~{Ll!xi|k1S(wk#pJqDdttOxyRhJBSEeUKrK>clukp+X=cj&0>&Bgh+zz(9I3O+TmEn^ zDwwa*$0bY-;G+i65L&2;h14U*E zg|mebsOoOmq-r2%7~*Am-3-eK50bxG!wOwR0W8krTd?3OnIZ!k&DB_DRVLHx8L-{L zM^17{2pLvPw>~Gfgfg`Az!tg;QV)+jA@c`(T*ds*9tJ85X(#1wa{z1O8I}tSqcxt6 z0+Bd6fW#|aI$(1LhNXi{Zw<7B5Ex8?ZovcMhaif!AB$DM01q)3&C<*rz||9n>uai! z9%JRc72@PdA9{s@dGPQQ6YyPVE%s-a`(Gnzr+B=^m_OVCwo^59dcpovWn|YUTbo8( zwd^u)vHASi-}*Cnqh#E!sdQ_7=>tXRX{oJfod~gFRSf0%IeZt{=ijq#u`x{um60L7 zec*{{#-NiYB^R9L|JK^rJ<|W53q#MpX%Cv=-5tn-6Mvvf`Rywo+w8=x=ERT~mS{*Q z`*w>N!cq`6im1j6bRR%uflbhKbPb@~Q>s!#Q_|&=`nW1wn)WE5h@7MAzz8Ye_~wLl zA6RQrYR!RJ2|*G!%ndm15G*swuDrl?odg z9l?@WG|{jQ0~Fx^<})9O@IW}pR8R_5Dt|A7t||nuu6$h~O&vquH*!4a3PTU=i_GO~ zNdasTR};-p?%^7&d!(yyJCk~6(FmUQ8W%f5%TcgHqxdG1F$pycS2<0GMm9r((H=ZR z6i-{o#ZF4`2!U=TjZFXf7YA zQ)5H#e6LX8T~ji17NZ^$#IE6+Q%cAB_`Y<%Xmm-?6fEvD!@TC)wS0bWwtDwxnMFb) z74gUt%}jG;U1{;?P#zj~^3+cz|31wy7YAxQd>a_o^#K-J*2=%;KW;TNXc_k{LcGCw z{gG#IrN4C)-`r`CW-=T;Q&LK;q*B4^LK*@95vRB+%XIUh0Zj}DDdCYSbG0dSB`96X zfo@&HGpLDuGzE^)8gRS}wo)9IiGw5sAYVvV5%0iO(y&sl5yFbwCqruTHv9ych`H)z zc4~DN_W56}uh5Kowqv1+O|;TdrPw}DRU!C;?~3`PqFIn@pIL*XC@NL=MoNmTuYfiB z=&$H9MP-tG4}ISpK*S5)^z-bX^z`q*eJP-}CM5xbXawI{NR#M;?EV9rA%qd`fLWfx z8Uiur0)M^cn^5UCKH1o1sbU=|_kpk^f$osI{(=-Y<-Kve_hb)Z5S!c-a*$`u+4-BL zf^9Ch*d1Vfn`CWtX^shEIjiWDKVG;t^kU}g%gi?KwXYr=58_0!j%aHCUEjByF98od z9uBjEZXEyFi;!d8|D~bAdieW)ifEK!@HJ10!tk2!bAD=nVowQeNHe6#KP2jYJbuuQ z(h~)336${N%+>GkQtFUR4sX(3o;-Mkf#{RXz<1fx)B<(Z;q?mCsV29 zY9oXO;(_-5vziMu%kOlAhroK2r$%u%%-UYwS-))HEw(DfF1>+&B<2d@a)25 zm}QW?#{eOYhovwap!AUc(G|6H22%e2(_heh^FIvBIzEE++EPfPEOQCEj>u6u5^y(P zf*Ztf+Y4j_G0h-~XHWV1adJQrByT$6nIHyf=JVC!q=Yb;ikMEO&}THpa3-5Hw7%@w z^zW%KtWLJEx2f<2Z76R(%6bzP>suEb9p|6^cWPlL%-{Kp&g6&x%i!$)Z z1K`ao27koVM^{4g zaD7tEH9!d^Qyv;H%QwSL0oid}jMKy>H@W$w6f;BDrhyFBxYh_TbOCg61QJ!d&4@-< z2?NUj#}Oh(A0dcnj59#f4gbgxtDud^AOk0crY_?>R-hF}(~lc~Q52?GKm|{CCCO9( zHAA6PPYfomNo_l1X@?l<2yh&W@%}LX@^PAGCC}aiq^UW8M1W1AKt!wEShV;rOB+&m z60_+7c(ao-#0yEJ2C5=+!KVHUn>fQ%-5POYMKM;#@ye)4nOBABW|!H#ugQ!m+vbi+ z=Zcv|@8VtXTY?b{>zJW~FIyKYD3`pq9KL0E3vYh)LU-6>g$(@RN=tVt_Uy(|qIL3B zw8HAO15<1>K3UmP@6Rf=i`lhm2OBBrOutd`^K`kVP&4Jmac83ff9mA=wpPHP!o|w+ zam@9(l)d4fT9aG+F8+PM@KDeEx3Q|o-`QA9?AL-E{bbfbN+>+4S3Fw>BktNJ1* z?1FDC?Z%U}?CTpfmm;DJuo+($Rhu`D`@9Y|84czUzJbYX z@bJw|tL)f(Laa4@{ZL6&=%7B&dG9#->b7n9MSL9dkMtvc5{?#D$9*J?lq*$!9qtXy z?DQ=EjH8>DZz?%pjNIUpEyN}MDN@p4UP(8^ghW_fKrx+uHVNqmwY|(Y<<~AApZ{26 zhzwan(2I>Ior~oT{`Ct8x9zS%qNfltUq(SXd!caor%Tkli)Q~>FOT_C)POcAS&|hS znlce(hH(drOlw?od;^`{`}+pgE>DCu7E)_J)VsXJ7n3%UpFEXU)XgwVHqTnE%Dv}( zlPiOFxhx!aaB5_RhLlXMTC7;$kzMlq*2PPCVPmZqM})J?u9%6IU1F36s{ju5Sn?2) zwg(#*;+)JGMVqIIq8gWL5)%<#AKQcp+R>Y&?9DVW!G`JAr_?-ftk}^~-_)?!bJ@%K z*`+-Xj>+uw7OtFXvg+sH!aO-ei~@v(xLJfixm-&wv-vE(0pV)ERmhvj8E*ljHwPf;?h=~ zf7Iea_>TOQB;`;M7JmAEkvW{wzI`QyDk=NlrOJ?9ZX?w}7yh^KX4@Om0IumEkg2P|tt(dMZ6JJ%EqfANFm+;FRmbrz+P!T<}0= zL!RzBdT{(w1SR3Q-RoC-7k9?aJuaz^?|51ADj~*X1nHh1+$6#`5$O=DTm1q&ss!7j zX2GJki)^+ZXjBwdGwX#`+Mq_LE(g<8oe*OuDAS!nsoIhY?@sH2z*Z&UdyKBGM zSm00wnux%xq1tj&cHcWKqRtf3!v64&I@b=Tj#O2}t2MO_kwu*hYsEgbXfQ?G)Ga!Te&yJP=*$+TSaF1ec9Oc=7$ z1P_OIgkO?5Hn)(%%e+x?i~x7Mr@kkuhQO<2W`zV3tOM!D=GA1%`8?-LL$F0N4x7ME7we;HR4QiZwtzR4t&@4e2!Ho@YiF(WTtg~w78 zI?nP`kkg`i4pzkO`$P_|JxK`v(Vo|}4Ac6v&Mga-N^zPJp0)>o@}8EuRI&J)eJpqq zLZ;M?aK|i(!M1pw(z^kkZgsi90n-?s&jk^Dq=f~+i-%A3o~J~=sA$K|m&R{c47p+A znoOA|mIX_wM?jS=9KTW*=&?{1mYOHJV0VL+BA-zrg#Vc%-eVPj(`E--(B%TFT-aBe z^w#)3YFIE9t^_kI)7f`Ftc3_?{qaRdc9hpeVKX9@lvxX&7t9=V-dOo`-kg@+^~h8R7h6`Z*oSE9*0Wm$U3T3v2lgH||VJxGdPB+iF6Gq29q&i4sb zj^OGI&^;zv5%e;q6wrvJc^G6a7rQBr%R|cyfr?Re!T+xe35j#8rG^~dW`#T}qu$8mebGJ}I(QQM}#gy
AWU{$Kz=FVDY^FW)qbY7nWiq@63`epi`h`cv<$`P zfX#;lRS9_SSh~BXTgE>3km+3Fqt_+2N9t*@`N!l%w@56-)X~t zO5Gll)8bs5TG{Jj?xxk(D*rN3s=s7Ij`oX3v>V5G8$8!s8OQ?L^GOk)T zcZ9OQDt3%w5Y8hy0Y8{cxNae(>2#Tzvmis7Dk9$NV7N{^MyDA9MIez)|TsHb&2(6)qGd#LQ{tSbN5!B!k8y3=0A za%?6nk~ZiSU@zH&9|`wi(Cb0ce?Y^~UC`K4}($np*Co=s1(INf5K zgn99vmvZkti_V;P`q#wI>W6YHS3=$Wy<1M*_XxC^)iqJ+{U-1kH$Br=dBgRR`E+sg zY}kwL67Q>5hu4I-YoUz>`AbOVA206=iF;maB%GEDBHWe;Nk<-6xVtvotWez)wN;}JsEc5L?}4!V;vq4k>pX1| z78@H?-`C$Vw6I66RAbVNJ;ps{v7=rQMVj{*eoRB=Ei3zVBtRoaogB35GdIT3HSgs? zOi00C1&k1L$)&)gMZ(iV8>whZ{*SKnY--~1+Hev=NJ1}$D!KTsNu~8Qe95Ru+ybiB}@eC?p}zQ?{M9iN(#~8mSjY*ncp-OM}<)3T96E4qHhb!hSsBvd@u2K zujTGUd4&uQB^h^6qg#IU95^67-V2HIgk>Y?0*FvkN+}UTzX|}MQ7jMv!5|=mj2ZnO z40sg-4@EJgrm(RArII!ob=RTi|07f~+G;b#6QS>sbS)_G9EOYNc?n6O`GcgrFGS^! zvL$7mBIYiIVi>!#5TFEz29e<{QfB7apB4(++kMUe44jzCY>hc{Uq|q4&;_jj(5>C?Y9tgSbtrP1FqoCwxrn=uZl>H-k1O>Q-ia+3|8UOb2 zDBkqKE~si^#wVLCwPd9hYTaV#coeSIwi_gC#W5N&Mc4)S(mY+yu?|BPNJqpn0Ln;B5?8Lx-zehq~U*_GI)@ zx)fayh29WDH;7?>jRAkpl!Io_k#2#v0d%X0;D0EF1Ef~^I>OwWVUWnaIDY}{2XJ%f zJf|Yode@-`7`P*bF@?g4n?!(s;A$d>zsk+MefdU&$ZhtvO&z9u!1T)`63>!Aa>q1R z;76)^;ve{9QcKkRboyM1P&X1*OQktPs`yv8w9l~|Y17$E%SkUFj7OXmY9!p}@`E~r zUxY)$BQ>q$HFcJhn!QHmDDXe)mren2mMZon3PTE#pNbBY!LU<7d`asZp`Nt58;C>{ zN4GZgH3qV&3#Af6b=L#4jkx%$SWl-1GN~L`4EUgp(a}?(iOBd`o7El26E8sfx*g0V zz!R&@WLgDoM1lEPdDgX&`8wR^+E8#cSl)^u6v;occ_Ulwf^(C~tn@r%8G+_+u!gQP zuM?TltJqI&acz3S29ey+2^{$ty=WNhA5rTN!1_3lp21pj1_k#cGRQM2XP9F2E#{1c z_*qgJOjAh*dt`El)h~T`CK-IwE9c3~4BO76h9Io;8jM2(#`VI=3MA$}$=rQyKhuxj zs>(P(Y&a@4dG+LrdxWLlmc|R46=Pfb`D?wdP%ZTrRFFKKN^N}-<$sL3*A%+NBTiFo z@QOC$eQl&Qg-ut7&QXUEsZFCmaa?0$lOeL*(0P5j&U!ELx`sB*<_L{r0&~Rr*8n1O zs5XrTkzQpabhVwUQ#)2hoAE{hcn||VT4&F1zK9RdX0k_7`zEeiIv!gijx-RSV*qG# z!}*Lzr;r5JO<>feAnb_jo5xH61qQQY+|~&U7=X)#Hlr?vIf&XJL~`)YTmmI%??y?I zH|<974jHB~n;X>p0A9HatIiAm&M|}R@}o->(9+#Qfc$yARM#ZcDz5omDzLis6M1mJ zs-Yu>uC^})3DpVC>5JHvRx3@DCAEG?2IbjD=85$E^m((R`B*$e6`OXRNhQg$MWg6m zqyCCcAyzPumxZn&BP=%rI2=bfLqu*}P}umtvY)PBy5SiRu))fdAkJZiTz|;L0sUhJV`GDaFFff@@-WR%|BPKnhwxez9R(E%Z0VThQgzVh|l}6fkUVIyI;& zgjT@KdY?U7hx@xoLw>fe(ypZRqn_8IH>Gxa_-I-ARU1E*6X0r9`hkcN={%)u`-24UHniI!AB2XO>cuQnpIsyMn~ zl8qC2ntds(p^4DVV1}#w$gRsIBm@IXg7kdqlteO%OcPEJJ(`xW;mc({a!X?gXok%v z7iI$KW9=MP9J`epPR$cf2&s`dXdZe%M z6T`7$-7QHUamkv8u``Xj25A$<1>n(R+s2-;?2IZ9{sygFt!`ihSb^aWp%9d~O^X6I zb7XEkK+B34;i2?a4^MvAn?;nAl~@>CmY+pscRKGOnpAGesMReuUfuK^M$^Z;B4wwk z;MbPuM7j^D8PQ^>u@24P{)(?_cAIO$21~_#uGw^G{^`B$c+>QC-{gaoS6bY4*2eiV zID7Lt^3Kwa8cjtmGE>aPg!_0QZKP{WTxygEPVt`Z6o=ay%E(A5c>v~ z$mep8Z!<=a)dzl7jw?MFacyQ^Y#w@9pL6%&cz^G+z}C0o6Qsn4!@lA&mihd#fMMU{ zpsD)=x>~z&NwwT#N?-xcQ^IbJLl~iK&n32wGeK#qs?+#dbtXAOJ^NN3CZyEE@g+m` zfv-+kfSEdlRzr|&QZ*G}Q?cCTUuGMzxwjrO(rr*qIT5!GJl1N@T03jT$GM)wRKANW zPFs7>6>SMrsVJ?MA`W;f(I}X|cA&ulY_2ZL(+1h@4A8DgJP7MuctH5YVm-}oVH@KvuY~=39Z4h10 za$nUdLcU-`Kb;YHdv0Oc&IPtC`}To)Xy0te!h^G3BZK?T)%b@$0Y`S^D!s(thBrAH zPTYCh2u|aFS5hXb*Ah9q-;#KYgpY@&HH5t`-fxmKpNq~tD<1fn`7F?mt_ygo%ojSJA#b=2$kyw zhoEf0Q&&6FXuQ{%d}p{eqIA zb@IT~2CGz7R`OCrk;P}S24*OfHfFCcQc$eEoH6v|nx?OR|NOH^2ZL`KA3UJHRVCDZ zU#{{CbN`*D(U*E-WbKRWr5R%M$$0J7US zd~|%e5DjIQbf0RgdYa0@tHrakZ&R%uv$Rs(^&Byo)tR!UW)R5rC|k^qWM9`_H=Eyb zqr3ctyr_AJgv;h&Nk`p6g;F5xyS2Q%#eCk>7wrzsP0I}yF|1|%j~yj4E$}ZcVt;Tn zzsz&Rd)Dpb6iT}JyBsY|e_xg%TdK;xQ&E&|@9)BW4kB>KJnqvCoPT|4L!KU%#N+w= za(AZ(9V+h2^PVHxaj-4ChWXDQ~#LT=CvxP#y4nH>59 zKdU7h^Mm8(zfj2>M?isOQ0(b{m}qmmw5zu=HNG&AS!k?5 z%Jp;Aq-!kfK}GZsO+vV%}6 z&y(2=%3+4AUyTKlbq6JCU0F4vY?3%oQ6o=F(L5hNKEyyI_lQZGN?YQH%MnK0W^y8q zR~G74W}-G62zId>sww-HTaWZt>I)4U-dnnqI?P_h)XL8*^d2YDQ=k8qB$?cOKq8aB zji!-1THuGxeJ^!i*rf z$CN(v^wHeXjQH1jGdam)zqbUT%;Nl`;g7kqUq%|Ip7RD9kYN+0j~HvMb>NI+?d-wt zo8-%qcgZIU8ovgwE9nk?~#dK*rCj zn4WKGve9pbdfs9Vy^Mr{^5$n=$INOHc5@G_m!uUG4e(=6pj)PQ|L*FBjuCHKEKAof zFA*$T=Dc#RHe4kWBpWdcGW~2u@%>}lq_i3FzVDXwV~?=^%q|WSb2i~3x1lfLBCOZ1 zK9=o+{y66K{25(e5s1i%C}C_8ZD2=#L*1W=DSd{0_MNW(^^DKU){B2k7D>W}zG&E; zS|P+|XVJi`UwN#EKiSG;t!1m&p50|J?wW5)<+{J9zqZn?+BJVGwBTCVu1`*y&6HPU za^5)1p;5`cjV02D#uMgnI?_I55vsC6E2XigqmCZM^3GMsoYvEGw2v}Xb|B4aYg*m& z#XQ>WoCeF$8knw8{C>OB@Zye)gx04t$gqdcQA_1XEj6$ju2*FEETGg>!i^F^d1Lc zsRQRCGIwpvZjduBRicvR?lz@aG=47Z^BEUavN6*Y7eaKc2*}-8HS;)*;K^N0YKd*q z_4>(H+P*k;#Vo2Y?DIlULoxK*cAcRLDbtIi%vmPaNnc@M({l=&aIl{HTnQ zEE*lE|3IX2PK@2^JY?#H#(#0NH)5a<-{Z%zZiPjzMIh& zm$T&i`V}9W1Zp^6X8jaYgk-9^P~;l37u9&2l8vohBRetX2qt-2Naa zN-PJ(XM_YUs|hrvM3e6cT`Pi=-9uS}^vFXj#b5LNnq69vA=|0u^4!wp=6ErIDcNQl z!{+L0q|N48DEZ)wqMXwyRfWhWf>mT&(s}Z9j2L{NSk@fvucD%J@INexu27f59S92@ zj2T0Fpn$pT9FAk3HDzaXKE@Yyd0DZXIE1fKNcDYAAuT^>ucGfh_AB_$NiK1-)VVF$ z&B0R_vx!^Qo_2ZSP-$e&;1Oz!5sOMlhMb}}9dH;P2&?P1>K2`7fR{{f5q-cO3SKKd z4*%6eZzH?O1h5|Ay`WGzF#WSVe!}A31DI z%%rz^B%OaX|74JlWJ-W2XnbPN`wh0!!WM2-?YNx|wHmD^A_|hsL^`DC{oh~k#l5MP zUitcf4#5Kw=c3(%`%|4{0T?3C0USVoR>Tf~UN(EhcA=~Y!*MhU!g>L4MFc&vm(GFW z^cVntOoSmX0=ylpX=k*-5LrN37!;rlEw!eN@&jVpe5pU55CHTm3J5^~6sAB*RBr^4 z;lFNJv^6c?DToTPk0Zjgxd4JFFLpyOVwjgJ6y7B3Df(0LfP?-f1^(!Sp5IfwLzXr0 z8a(|NWN-{qk_B)s!gREOjbYHs04lo$#PJx&$N~_cCivL^01Fkt26jLKzh(moDBWOt z{XSVh6iT<90**q0>B8V<7#3zE-5CcsWf2OMV@#)jzGeebQ1rumwA(eAciL^G$7$}2YTeNli*3)` z@fQ0Ay?&G@dZ#_>M`LDyAnj3P))%bk4P^YzwM+@D*edjTsBP@>)V(FH>t2GQtG%>5 z`(VFy-YP_feL9Fk+5JxWw0~PYt722w&nN0&Aawas2?e%+^vX>R!xSiy z2+R19(0lCpGRlcv$y3RXj!_$m(1z;bAT&tNxG-oUiU!aI%gF`=Tu_|8TGIw$AP&}q zt4OFU3gG4kEJe{>)rLA8gCbZUHaHMskv5Sf3W@gOkMPn-rOIKGSqlqwQuW*PlwCmT zok*)4ds@z82m(dNNrX_Ldokkhb{5DbBuxheEQ+Kn#DG|l@C+n4J`5&}qRq4hXd~$} zFxHhAn@nZ6_X!9f3--{4-A4%tQCXyVz=UaP^q4!;mv7Ex+?Z<`$?PMa=& zEz?tw*3;S!-o)ccslJsfuh148^)2&9qu3o?nr~cLN_JwOrfhGl(|()EwC^|mimVF; z5rf(4%uHEg1?x&a(xTRBPkU=ni*>tt+0sJse*M9nML~3|cC_Jl-BHyT{UCS1_1axt z6y`2uga~LmpBSp{ejSsmYbZEJUgdO0g^?IXL>(x%#@Rl3Ga& zgQg?`JfQ>PdlnASH25+KJyb6p#l zWew%EhUN=ZHeG@)tTV99`irbUZ=ygnvVhP<_|Xemzp(Uw+|1?5#1Qp#gRpeEep)}- zo_}i(qs)3rithSdgc0fbd!9`7_#u1ZX1gPs!G5S-X^|f3OTAIey^&>Ne)N6Oadc^M zJ#N`|6V-bOak+7a57EKhX!V|Z>b=#oJ@s+@jk7)Th5b!&bW&UWuBLq+DC)RWpBSv~ zKBcFRzRyoyt6r)v`;eGp)^kU;hY1e3)7G09Eb2q)1;6h7*Oc?zt>;eJVA`99R`Nor zu~02-(g5*(W{~1%F=)1{@|P%bcPwdC@(IzKe&DrwzcuMlz4=djGdY$t+?HEo+WfO@ z_{#w4Sxuv)%W!p2^UlC9C6?56;Nx#Iod1Uc2q9I+Qd)z^0dgY)-t_%pP|s<&&ZJY> zIt{}5KIS3}x}p$H%!bWax88<5LbTDv`vPRiFo?A?0trh((J9V34Q97~Gi~t;EL0t& z10NP|2YDCox4PAPm;WX+A_1%{Fl0cYsI#}oXj{}t+pw%kbz4_8fU$ZJAV&fBQNWDZ zo$a!;ycoJ#Sz5Vlda-6TE?L@m2%~uzuqL+5W!t{*_=z{+m9jt=SPiV&!3f>*2%u>wV+Y%*kxd!?}%i zqWmsuoo?QIs6=d_d)Br#r(e{B=Wg!6a4>Q0d;sXWuxma)@3+w9wvYlK-c?_?%lqt* z55mJuyhEYg;r-w7LB3PK2+38{I?Cc137#FW7Hzy}AwS?ID_(k}{6!|2iFC#@S z3rJj+v;9vyMDRZmK|x_bVQC==8DTyyVNp2|$!lUlyy6nd;_}K8taK96iV||BlETuG zl2Vc~s#2oDQgUKaO2${@HDnZY<)sv-%#KhRy#M0eVjL}qI-PFR% z)aHhnq>LE~Z|0Y8Mzz=pE127PnL7uX`=nX$Ua^ofuyORUH3_nH@v{wxursr@H|Mpt zkg<0Qaxgb^urqTA%5-%0cjCY1ByZ?Mts4=}Vk*v8^qqZ`oPDERZiKja1-g2My4#z( z`$W3?*?0v;`?xv!y7~F~`uGN-eM92>r1bopB>XQc_>0Q=i>mlLgZ-T?{av{HeO&_* zvIAud0|Wen{1tG{T?jup`SR=6kAMIE z{_j=){|kBn_zdGSZX=Fh!LNqg8jdUUQt1DCqG$ZLdOVHmTF~36$2F5#NIe3dNqg;d zp12*!eVUr-6$x=l`WT;h43-|E4&F|8G(3B#jfGz}?QDEr0}7P%AaPJLJ?k3N3ezsq zRJmh^qfc>YV1J8Q7vZv5H+ikY`*HPrf6L*kp0JIU3bUS;cLV5?+&gD7M@~@@Kj8f4 zy=@y$ai6R7Vp<-5=*0_}RhpNyy`E98sUrya&ez}m2$(cT?d#lMZnQI-80X;+HW-|# zWB=>eMUC_#Q_UOHMY^LYpZmP;k>9C)qu`EcCxA&D zf>N|KHgzq4B~!LE9}kiu3cS}~n%9Gfp3K!Ouxp5HWNI21=ILh%yg*y3I=Y&O7`sH9 zY#H8&HCZY8Sj;Y(?KmUYGuIkVaSece;FG{_P~j`z(C~dOO{C);>w0 z?OT*rHwIX{nS!NGthYhZVY3`~r+4_(DfWi)?-X@0O|j>Cq-MGoTTvQ6ksi#?Xn&|# z&N)w-|Il_?e*5E<+|~0|ip$N4E*mE7(8)^(@Lsj(D#MkAx5h=n9~-Uzi|Wygxppzs zwdP9`s`Tb3l&SX>HrNOK6nB$KS;u1e*zPg7YZZFBoB8WZ`D58_aO|F5M(F8w zW09}gdUD5*+96?Brq8>P44tKq~kNUH=EWSMH`PcV_a`W%Nmyt;(jW1KztX962 zpJbkWw;yU5IJQ?F?%x{_diMKdf_@=>bEW9ZirikqgE`sfjag4LC)+;%J3qRufARM# zbMp1SyT2Z+d;w55jfOm#L7}%WQ1H4TTQTLMTo~qG+Pd{Fu(f)m*^0xQZAs=;d$q4M zmcMhoQ^9M5&CwdmBWwzj2+(D9v`m!U##8ZY82`bqtu?P*&<6Q_7LcbAse{p1} zUkwh&)v#?gdi5e4$6;8jkQdNvn`7dRU-9=|J0qHl^+aR8rp4Yxf)xcPw*DURt{qP( zbyrP%sC&+Pdk(_&(Rgy8xW=(_REXcLGe^DfsqF&>;WohUN%f@}+Zb|?Qol_;% z`%HZh*Ggv@-mjFpbcWkwi`vNbYOnT5*L-w%C-QxsOWvKSe)N=m$+dU*6rwH3S8`tYi{NUA$E?2JDdoXozr2C$oA&{Mpb~@3uI>TC>FMSqXCIq z1L*(&?k(Y6Q6*a}*t`m$gdCwmF9O1+zF#KeK4@fyLrf^ueBY5NlDl6sI)S)k3l*%K%`znx}UAc63VG~5U z^CbPwjEh0PB&)^L8&TxCGyQ!mfOnruGsqKzUfJS<>?NBIhvQu@>YuVADw=#h=FmHf zcXm)1TeP_rpFds6<=+B;fi;gI&*rflql!?O3eiF@4(#PmZ7tD{(%yL~2=Y94zyk@v zTn=6{tiPov1vP##Stb=Bmql*dn^$>!dj0Cj%UfXe_lN)ez|A0R_c%5ESYh*qLX78^ zjN`{QGe4`X_W$fX%41`A=r>lymWjgp(B?ATkoIB^X$wf?Bo{z9u}CpPISo-=&tKve znjg`0OB!6v6ODRLDn!B^--hG7I-g$Fj-JeV^OY&uUVukB^lOax>S98t^|V?yK^Fdt!ZZxiAXuxpR$|82@%-O>8xvY5=cd{*SC{RQ79+DLnL!%q5d%KF3F?Es5RC_X5b8z?YQLoB{RFdUd9Ir5yuMMVLc{H_osWl+^`YoML zUOl6TuEu1cZ|-BegMZePYmYk`hLj(|45YXRz*y!f%4}lWL}zo;O$aEq{27RfzUb+amQF1l#wQzdWELy@y(zsOCCUDlaBec%6^m|&wMV-#C-bH?)`D$)VY|nC$n{w)>q>3nb?>- z%vzuSYs0TI(eoz+3&7}?%)K!&{oDAdUz#WPioi!P56Ia5eJ=IBm>Mf#$Lu;s)gD;9@jt@n+y9*r_q3?X)(LUnf^V|DXMS+n!A$t< zmU#Z}R_2lmlt9xH!Zk~UB6~CSO;DYY{;j8;}jSq;J`#(P3d);ua za528Z8pggFUm}*kii=0!;^uo`?0fN;W4h3Fy3k{~I@NRY2xN z05b~6hypSru`1431vD@*9LR|R@+<<`07(cG@NyW?L>x$sqIH2u)YW3004!T}5*34K zSS@z^Gs`d#MS%fcxUW1QjeB3a6lgOTB>N;#X5MjH$OsW!A; z0CF7zYH-OaI3TArwh4zda>inCSe;V>8->ucM`-QEMp99zT!e&P?33QK;8Oz08Cw&O zrqPRS=_M$x5+37{r=f(IVghcJfJdk0Ib-Po_*Xb=G!#eQ28ij!mlp%*s7WR?(-)e# zR-AS%0Go(TL7f7m0O_LEHr<;R&4C8AiSM z_s+O9=iJQR66`9jG7uNsgimnBCv)Wr2j&*8rl~dIKLq5dmf*^Ju{Zis?l~v11JXuT zaq77LVRZ>^vac|sx4`UfVUS6N<6S(qv7j4WKCOz4Q7sBpEqbkr55-rk zawi{LG?s3{GA}n}%CF^as$w@wDx+1gC+Mv2##zGJ4^K=$+$f$OsvN8)CHb%%P#}n* zi4*wyAqYrJExD9$@-SH<2Z{%ZhJl#Y0T^_iR8!HuOD=y&CEr0R4KkN$J@r5(Z|!%E zyegmohvV(9W<0J!OH}l!rm3j{i&jgyFa(2uJRKE64Z3W~xUAQNpnE{D#uttR0NvJb zToeKcUDhRD@z4dU&|ij~!kSA|bnX?@6{inD3p$)j^8?CXt>M)6%c}8}I}+GciK1wU zBBj;(4b^J;Hntxnz;7-&KTIAX@IXLd4y#EvhY808H<9rm8%!Y5#byzx->-|`*|amo zzr?G#WPt;BbJJE!)cSDEB^fk}slI7$JW?6PS$@aYN%=`V_NjAP02IGmYUor-fU z9$L`*y2e>0<3?bO3BImWwJ7RtX{&fkUN0a>66ed4DgPUAdAh2a(DGrmKKe8xsILNT z+z>-}n8BURjmiO(I50L93x|O&R5%_qWfNSp8A{q*Y}#ncih(9=XRE+`bd_K%-sTh+ zT2lRBkK_Z-%$-V25y#7^wkI{_RJf$%qH%{!xUALGE{XhF9CqL$FmJY(toNs;dzD~J z=%|~*5A8Kmptw48-jhFR69JvGQyqHr=~JtCQ#Z10X-VreUUomDw=r*1rCK+wOTDSY z)2&RUF*7<4D_Bzb_;-#}0#K-};m2BWK6j1~k+TT^Z_4gL5A>kV9^5U1>xK6uruAUT zxE`4Gg1+`NwZRF^mk4LQS@du+iUzjc(u!{)&fwDa9yhq+_ZxFcXK|GSj|E-v>0G6v zyt%}~!osOk#nLh+0O7GD(CmQF)!f|f($V#%JlG|DSiC!Ux^`4`kSsoEA3Q*&LPX%T zQ+u7$&7GERc{XqGU;0X}lwkP-yOMET%DuVr)A*o^L%a$svrDx)dI~S)2@;Y8aUORc z9CqJ6>sB`9$dO2~MbSfhdmkRd(1!+Rv%}|Um_sLwmjDG4PpPAT_WSxugYYR(5{9c< zf`G4m(~gY8^)>Y&+(+1@OWSyBBzUSx{RPdEwOVPwnY51av(k|_UD}eF-0;#m?lSK; zc>Um~*I#$(&XT<8Yt<#nDN4TO)U>9JdOE%Q+0VIWtc$6F=eXH}!3ncE z`N602WqHQD-KD*CPtaXEgwZFGU3RV=RfA7=T%Yg+t^W>Yfm4ksQ^pciFXGzm^cfg|g44%oOOd0>&M19Hku;bR1)$zErl~vLc=fRr; z)A+xgs_{!N{w$%)$|MAUtQa6~SWUG!OP(a5auC3RY_t}qB_Sy>>adQUc>%eJ11Fe` z=EP;2*mQN4%8z_$>I>WP6iZEn?JbJU{6cK`3tT`|QR!Pb_>95p$#3SP6CUkB9(6ZJ zZ(Xju<92^pS^Bh%r@8Fn^yQ1T$qFLFtJ%2sleZqp zxR6|$`+r+Bef|5s`_Fp-KTd%(_+@CYRpQ-J1#W$4@ZEfAN!bTyMqJ5d;OxNK`CkCX z!UyHR*CD6N-g9)eESu$z*@ytBKk+qhID~T-UdBq-_n8kp_Xd=HuZab|&+x)f^F@>P zTkm&`mW%#YGCby7ytSVne||g=MT?cmIODG$s2Y@pWodhf4cHvGPdTU z$7chXXQ4-jOILP2U1{E`BzQ12i-j_Urp3OppJZS}vApYx+)*Ht^tzppC zOKpsFY|F>z2d}DO$Qds=q=Z|p53R11ak%iW~cG64351)oY z-(6UIpjkLH%6jo5y{p@N$Jy$rGX67&>D6xN_Zy*KDq}zX%RJKOKl<`y=i`;1Cqt8Z zS|1YM{xFZ*2xk6e{NUH}<)4QhpB$J9IRE{+SvdkcY6_c&gaBw$uhKfAXrSTwrmH|H zA`B<{JOBD`XcesZ;qT&M)>X`J9P&5F=669Bi?=vSQ*nyrS2}B1&cdT_wy6E4HYjH_ zBm}dM{K64Vxf`5t*t;>9^j{|KZ4%X{Lnm#W&ZTj z>eIk}fzIAhATtX)zd2IHa|BEex}=r8Xzi&7W4;U@J4S9$64;TU>45MABOH%VXp~BX z_eg@ke?Ixywx}5u|AjVBq462IIMq7``HMQbScsIofS_ys-a-XV;fgN_wr^K!QM9WN z#6i-8aYiZaEozTCw0VwazTg&Pa_J0wQLQO7PIl4g28=gx&}B87_`f{b;cei!NlBPY z6m%~RleEUs5A+Lcphgn2I8kWxuy5Om2tnPK#W|7N*n9)0?riHV{}nEAcK4xgz@6!4 zIp;?AHs6c!<(8XQ8B{kwN4dflrhC1%>$_VOa@Ia2&wa-VIQiYJ@#P)cq4kcZ7nex7`m6@Uv66Y_hB~r& zM)Eenl5vPReRc!z;DOkqh=XO=xd<>^+d154n8iCk+_0UY z<7?aa4Kz^+>zv2X{yZQqU0|hMN<8tI%+p)#r=7DQK=CT~mQDc)(5){1p2#VYDGh9< z@gH*nYVL$sMyxK}j0_bJ?28K&-fzrnsY=iOw1j*%=G`Hfvb3F9r3GVqCiqHAy>$qx zC3#^t^kc6)%X07E+jbG-&v!^*qQUGElZzx2Ffk!dF~E|h0=rQD3W-FHoBQ$;Gp-!0QK5qSWe{-q{ZtdeM7 z+32$gG7MCG%W|W5i?HA&H2gQT*Bbi}KyDLP;4q;{YKB^C@2hF(U;$kRxFinU2*C>s z<`Ta^S0nwsex#oNQ$!rup5;vVvBONX?=7bpOW%;*EyDvt-Yem>bX$^@n0L|7I5HFo zbR-o)Jbfb3H<9TfmvIsNV=t27JJX}>^f09<4~o>$*;gflpl_(M;G?6V=WZ$AE%)2% zS_f7*b&J%#!XFlY$c*$3?A67pU#nW^n5l0_T*t?VlCvC(0XoGSEQvs>Xv1uf1r4qz=cuVsJR&rBeC52yrIkvZETq z`?5H*If0XiVwO4QF|?4tGKWRId~w&Jnp8QiNVuLm_ujbeHBYQ&SVTdKF(xjckw99~ ze$q_s#r#xB5Obt(TJ>6qF(GLa#eJk2!HnXk!4x2)s!%|k*QI0^AMv-2-Fk-d`YAbM zg6wEzO$i?wMC0nf`NY$qyX|-_Fc+)YUC|+(dM};rD(0STlNbv~T*%AX(-K3wP{1uDUBVHI|& z0$#)t=xmiY#W#T!B?g)c_M}rAHUF+p0lJLrPA_%vWdZ{4igSp~|HMCICtMRNN=($O z4byA{jDxiIDex!CNL{|ql0c}orDK@Z6MaE8bsLNrqLV|v)*wAsxCzDPUDIJPYAE8{ zWWuiUh?m@)XA=L)++QtwQ8udt(XsrV-@oCh4$31=59()lTo%$|t9UILGmsyo7w|ee ziL-V?>n2uIOf4trhmNJ=Uscn#Xz6kleGxJ2X;)C@e(CI+=T8Lv<=9hxC|``ZI2y62 z2L3xTpG*2Ut(I&-9AhtY3G}=Ue7Q^rOvA6d-SKkB%RAqryQs2~!#E)*7)YCqeTgv^ zG!X$*LOWiGT%q~zlblhU3NkCOQSY+2{dL7|`))Z?+BfbagsyqV9xqRW+hz%Gx;7BK zWbtH4S0sPT?<2+3Lmx(IYpl!)3bIsi#6WHa9=f6+qACYA!xJ71bL$|X#UrR6(c`fG zRsV9yu9`ZxnQOOMlecceou@1*Jlj1G(#AFfNnMZLb{g0;XQ~*DeE+dhnKa972c9dt z$tTZ>yYF_xF`;*LC+DQ4=6pUXIP8OR(kWxII9T{S0DtD%Da+=6XDUMkO=+ewl4;|S0~vEsHeDC6MDtL`7l`%IrW^FG>RUsXM(;^_u`|#2lU`gs4z=DWE5~@VVRDbA;JrmxaRENWSx9gmSMcxT$>qJ=3ce zRt23!EFy|CU@=MBXJe62PgMpV@3CZKr~=9an7ei$kI%-E3@)@`KpVsWC{~;M(P391 zIhMK7L$BU40tuo3sZ34?%Q1jaBl|;*;b~Ibz#%linuhZ<7A$ZKpyU6Xz{WKI84kU} zB2jJj0nj-L2o}tpDjj?A_60QuQfL@B{;V7|GM20oF7V_Xm?sQH!yO>2WZ~>-yAW1! zV@Fu*Uc(h=fbv*2CP`pd`-tV4=a{gJ+(jRqTFg*EN736$}8J zeNyo(rLiM~Gw+)l)Gm9KS%*$<89?EZP3R~x%`OEMFL?xv3cL@4j+ODg z>;9?Dto&AQQXc)7({bJ-SUGXxqw`6$=JkXAPptN zFO7|q6@!r0qSFTQg<*2AYy2VWG$o%|G4vekodBdZBIw`0AqKX8!4o0Aaz&lA*v=FICzt_}MBr%Eogng*8+jeh9e9?O0F7>$z1P0?amJz6M%0b0%RX=KW?E{2<(*ZwLC{SZ6792*w=A+le2 z3N*T`09Y<$-4jN}H(wx2m0vx@RD{d#nI%{xu$?!9bj9IWytG&Hw7`wxKlHAAb;HW( zU3no7tbk$^imw|n(4W%~@}xS9q?^F2!^U&N=QMB{XOfhL;_EWTuxpwYkqL*kSI^*B zl2DIqBZzYsJmEOg>E;kCM$ghtJ*&Lwd&`&@R3M;i^C5*6bt7L<4q8%Xbja{`@hW z{V2C{0xk+Dq%rFHU2fSe>5eB1>t7`-J0o5F+$Yaea`sv;&t4yuN#CLWYm z(c{b##FkaL;n*N^$1Sov9h%HZ?R?W`$T^rK-*wUwlrA| z9(5)Wlq%>2j z@BU3^8`Hk3DAa1=E&!0D^9JesuEIbNV7QS6fdji~fVo0V1@1f>8o0Ip>P)dVCku~^ zbDB0eZQM5*WHJHA&LMkFHh6T#X^oZVRorR8i!9QElby`P00e`<_2)Svcc>3!|6sLS z3!;(k|2pB~>w6PMb6a4jigCI$D7|*!U~%I7{(!QI$9bKpfYsi#m{!MAV^a{j2oaE? zEP<_KL}h2P{j~j$+=?JWTbS-NF1D0=)%?||J~h|-qzH&&=Nf z3N(_evjH3_yQbbzU^BNM$q-Y;@ni$n6G=GGvHE#K`-H8|OmVp4xnCZ^I#YCAb#UDM zJL@FPWgsr;tp+W%&iG{iJG;2!j2P*@+rpFx^L9Tu_NTg&J>2f{K*b1|Bt#=iXs0R7 zpS<5U>G=KK&y(-seox9Zp7NNWh}=CtrQ$K3?$Psiic`kO_yI_ySrLBd!5?5P5IYp` zz`}T=8)L1*o%beU{oO&@1@i#GVcVVob_?E%#&M}uLMo87qzjOEp9g=JUl-BAPaq@bNPTtn@nYFp{Jt8c8IR~U6 z&hvjW%^(GaT<4iJNUbfyP% z))lgGYLv|aIMeC(zU{o@7_bwkgu>ZedF%#r)-O-D{;s-qS$WFmV9E2M-MGh#p_q^D zd?5ERe5a1?uBqL>IM6vWGvme%y8|CY$~z>k7gKUFO&Wy32cLcBX}UCl$y@jQs_y*- ztVvx49QEl-q!1&e1`dLExM9CyqErCJmwQdx1dh9g`*1p!39 z;JvzCDz_mYfyRK7^iv?=#)Zf(pC8w}3&rP#LIVJgF17hUo>g;t%L4rw{;b}nPdI({ zk2*K?NoU5%>zr)c!rrr@HZDV6Z?N7d&n=gyCynQaj99}5&JjCx?kN0FM(*s!uL#8T z(p|BrdrPF~u1qkCEyYyz3>`eGrz*@0tKLxAg*?GsZUSkLFjRKw`lUy2({!&eR?;uk9YR4E>I92g^sTW{idF zun&Bp^iFKIseQ3-W#UY}U5wX7(EO2}TSL6waW%Rd(@#>gzqzEYQw6@5xO{`NdK+&% zX^;cahuqFWLvOXd6+HT~JJ@S^W0vRLN4bQ~)qlZpPlJ8UDcA-mfi?QF%_W>Pg_DA5$ z_c?yB0ssa;uS|ghN0PY(8N!zXD+P*cThDfBSkCPr`_V=dC=(o9rUSosUYvp$2Z04y zB=(D20^PwD|NF)%>S>+N{G-F3#tEtj-;G-(sZi%G4WAOxiCy(d^80?gv=w`?0IXn< zsvFeO)tIWAm1eHNSvlli0g>4%6vl>*%8T~!2Cgc4*FcB#FdIB0r0&9?F zFO=WvI|1GKF{)nMV+mw^x?!ihEqxGBQ2FDiFe8R|bEr&U(;{x$7=ET~?w~Ng;GPxx6 zA{SYbFS2+fPQ)(S8P|>oi!V)TItNjkO;o5|Hq505&j!M4 z#g*h*g1#xvHaavoS6)D1$kq`*nu;fRupuGHGiV&Zh7=Z+KeX;{pM}s)-%<-- zXlK7EU7)p2-H(ITNaRP40JB9+#_9gkvF~*T92TUF(E6X5FNQE8Rtady>a?8I=~9PJ ztmPLlLE9z1Xx9xYr=VH(CAfV|@_!5L+C3YTJIk+k=JFzXHl|L#q6k{L%QW4Ca>Z=? zaF9G(py2^;x7B~2{2e4nfapyKFeAajTEoU40yKHeg;Y(^=b&%Q>`Wi6XF8e=_eH@Y zdUW?P;JT3-A}ON9@{WQ=agt`d;v*vGgvJ)rKTv#XGsvwLvW zGRXtR=v*&~eS9zrx|%n4BzxO=IVuB^CN??GVYlNm2y?Ko$Cn|qO7I|LGQOV(K33J= zwtKHwUQ*4Ts8Agn61i_Y+`J6r*h})%8fH-WxnRvut^hM zLL33s^4ocMA$O4DwnAZ$VqvY^amzrXFxw?tKp2rGB&_3H_kn&K%Bp5&^Z5vC zkO0G4in&q*uy@2)35AmY5C}=n;!AXkcN@_VS(r(#92j%^nTjjW1HP8Snt5?%;A+uSL4SNV;3zz#vcmgSOb95c!4 zcW|7UQ*j)S<*KMx%RPl_PEU)Zuoz7U^6EoXkGLk(5Sb8DYY$KLeDlz#ciAXW zeVC>z071EGRqtA%A98`t-blD#0vTpv70XT%G zxg0(4VQ2bexs<2_wWJke0Au%kK!cUFw{VNXt}K8Mbqee>cW`6eo96nS`)&m%kl-7t z6{%b{BvIFsjRx1g%(1aw;04}Kqan2}?S+aao1POcEnpnx4(Y`B1fg_95v0v2U{pI^ z$R-=P809cHAutH381zAD7x?v@m?S&6(Lgz2-AbQj8tt`f?Hw?6qyi7Y8j^jAD>p&XhiDG4&R#)X*?>$UI@3Uuh-yzjAR#P> zO=L9>&_k9ELj!U$pNpy$Nw(LNT<#;kUhn!np&nQI^a945lzBw51hrE^Lv;#yoxNW! zT3!+58Ni1PXQMVPhHc(G&VjmR73*Byp@`5~?3O)bR0S>dJo`ESlbO&-@91JKO4sbvw z9UWU0{Su~KuWA1&YAc~T4hS_wzlIK=i+~Ka0j{Iwk2}^gEBpgjCbHm$Bnp;63fq%; zpPQe0mwW4Yx5S`Xl5hHTP<>hERIQWgCIEZ&1U{JfppMP*si@#hmq@i9CN6qR%1ToM zqEy!6b;o!mRcqtqsmcOQD+X^5s+wfG6J92bm=qFo%8(HMUgqbS^{UGP8Ph|cAcg01 zc9i3JPrMFG4xL=^Z6M?SNJL?Dz4oH@dvi{Ze64a61{?KzV-erVg&oh@7_M{gg{NnH zqQJCS1b!EKru1EE4nqgvw#PCYU5M#=#}jSl7B5tKC&3C%{m6I=Cs)Uou(ZRLw-_z# zl<@nW^mx6}TK+|X+xUvV6&k9UMaaE%BovrcGDRfP3>-l)-M|9S42fKL?QKu8S& zdCUnAwlQT}9(;VB`Q^Q?>}Mz}2+v&`35NL-n=7CFdUKpMR&gzyZ10T<-%J)Wxbd!A zLV*dm6^-%jh`gc*T%oXfcd!2tI5{EY;cbuC`v~aK1EF>Th8Fhjo!bU2j^~KnRwoTJ zamRitBkLUduf~f&25@iX-Ou|Mj#4YdXiY1EVhe_uX0>He?S?_0R$C99iZ51jtp9-> znlie}ebM~?3uX5coo9Y78@S|{>=0xJUXa%343p2_vEE4#>n--yrxPwexNXN>OZ(oG z88igS-~Z}3_%!yuQ01bCf}OSLpT)CB!C+EZ7aRwCJpbf3XQa^1D~VqBdyKM&F*_Nh z_QTdZTQ3V7@4vk&CT0nemFtnvs&KJbi(LNarj?=(*Z66kl5Cc2{5Am2=_v^ z=##9lPXX%J-Ftth?j80?+@3{VZIKi^mkm$_{YUO4F{}Q{WQcgZ4}cuI#PDo#EWX6b zl{dr{F`qgEH$i_K;|&Xpn3Hnwjpe(51PI=`=0sU$pnH z+*+P)7G0v<6^k6eeVAgsy(_j|c99Fegkz9V4x^ioGdiw`A~Cn(4&TSrB9!Hrk~-kc z_adT;OYG-r5Khw2&&6!zx7qYo`Qzq9WeD&|#^w|wKa#h35+C#yUvpPn?z2y6 za5wzzF`mQyw!TAudG*wYl%C2Me_xax3EoIzD_`5L!z-Y0rW0fo2}iq~H;M zIE@J=>sLuTnlhVZYwxS@2Vdt^!A0<7hQFARX#r3# zj0-^6xUUM-spp=TfSmImA3u2DG-%}w4-f!hMAPpc*h?Mo%Nizn9dIoJD-L$eek(r1 zXsaWiPh7IHE<_YSKn>tyfjOAfy6Vspm#CZRCO&FcW;3ChtsWm#Pl};KjMvT9i(P}o zoe*WUy5e<+`L%8GN7yV0H3^Ds^LLzs1j=O@Y|mRxSMOE%cfH}-EU=blFh8PsFO>M} z&H=GDl0l#4vSn}apD>aBgQXReS+$v5;-Y-jB?Q%xPGV$jSJqcHKYnH}@d+std9{2d z!-gWDAQrt4Fl#f}GVRkN=}Qvy1sL8&_0fSMQm9K@Co9e3y`HZ@BPN-5EbAgtHZIK( zKfj?x-Dd=Y$yZ(cB8FI8X1tWd=lUxQXSflwH9$`F67O~r(0{B@t-)~?0{6HkaAT$2TUjGZB@ijJ?d@4b(b3(l0IMIb&H zEkh+~vb6Vs9C-PN4x!^>3LUp@-Hp7(z}76klBPAT{IxB^N)%QaOi1ST#8cD=ncLH5TqBvvZjs9c*r)`8Rp z>eg?T*83}H$%;+N6(T;lgF_=S@VU!P%I6@+`wE*KcuvDi1RlR)+hl2U9Ah6>J5lF7 z@a|ghs_aN(d=nxexTplrIVt1OPv)nNVaglxO`UXe@a?E7Qej9_%F%sW1Uys>d~fdJ z<2e$XbxB%fSu%@&Nt??LmXSp$kT4rHtqw%PtrvRD1Lst3$A+xh1~$2P2bP4;%9WZN zWO8l?HA+>lUiGS)+lYu?ng^JIhRmjcGRroAGgh%&kIbtPaHmhkstwrO**Xs`dHo|y z@np?Nr`!4_330fUk%H$;%hS}|v`+z|^8<9JNmv>C=go^;L80_lg{u==-Cg*olj7}0 z<~?l;n1;Y@0d$Is)4}Pg3;hhvfiK;2;qU7&eiynIkwWBj{c5eU+&4kShN)iW@U~al zY8nk~Y~>l$Yh4{(6`Fq`Q~?sPV6M5h@;RyTjbvBMv;A#6!ti;v^5H=u73B58ZJ{(Z zz7729L}=r}*Z=izH&Rpvvz1DrAwyje!-$~C`}p=>48d6?s}y2v!pi^ltP9?HaUCjO z*=zoO@>|>JmU{|L{mF`7C+St8Qt8P`crQNBO*&6lKDI(k@FZxhs*V(<+Lr<>w0#@j zV~qd6N44Rn^}=V${W(c37$zd+B$EpuY8BTm=_+4&BKK11!&}1GncdCC6db!95ZEbX z({Vf7#rsQju+fH~N}2sVmzqXy22a@v3*eaMG=O-HKFGC<<}*l^dG8||NQf3m->WMNi216YAAw6LhbYCm8>nD(|rAGjcU3oYC*s- z&|I~^!f5kvIn@Ps=|5TzBt_cB9sHB~K`Hf*W)U0g{Q%ObW~)B}?j(7m8T6dAaR7E^>w>v8T)8b?^!toKl zX$gPU0Fa8_L!kKe_n zKH4t~{g6Y(W@&*=fVj#&eHm@mt!AJGju?wBHK>6Hy7}wwMlKk%N?kLk^x%=^^{?Y{ z;oNk9yBGNT3^48njL+%>GC`tLuO~+GTsT$P%+UGdcQLu)vX|%vE1?SQF)z!t3D&XN z;{Anx-*O%9n7{j~JltXPzt{4~;NuidW`5uH)4qJ|g6*n~-D=0VqgVwCGU`e&fm`RV zgVx_)n)9P73q$q%2GWC_vF`=Gcl*X!K37KSM73Zv^fHwlX7HHBF#T2e@RF8&qXWTq z-N$<>Tpgs*>}XU(3)b(zVNAOxo3zB-&6Sh){DQB_tF)45HZH8amFmzO+al=CNoqeB zUm@|@={nCaMS?=jPE~47VcD~f4y-RonBMPrhUnNg3-0{ExoKK+@3y+rb-k@uI$^_l z)fWBd|ECxJKRt!oe%IxArP_Ym-|-Qv9m;r+1Lt@ADP5224=!=Hk1})A`#~=scetF@ zb-1n^yTgwjVG6D>jw7@WDdE${iEn!JyZWf?b@jikvYbWAN$t8nB52q@;StW_iPD`o z?FZdcu6dgznHt95>Bhu%yqn{iFjf5Cf`4ecE~k#3;W4!ER0q~3-Yw8A?M}?h86Cvh zl}p2|8uwHQ?dm=cbVRB9iN@xLwaW*O02I6xlXp0WYInKS68-y8*#CO+?hIZ2U%XN? zB|p{Z+PXmm?C1GX3SaJeV#5)G^Y9U3p8*?DCpv~_zE+vf2}kUIKcidMrPj47+ft%` za82Up-_H!p2!U^nF+cyUEpanZ|EEWu&a`-2;+02954$ZZyRd1MC4D{f84onnV>uak z^gh4$)psb3rYMRgiO(IROW`#jK*v z?uxkKOchMZx27m#$%Qdj6@P1%=5RIh#v(MXhCc92-X`+yKQTZ5UkSHP&iShZUPq(~ zS`ez>qvMhrFX27geZC&?jIFe*&1W?ozEO3H^sRL+J8-)7GY!VfvBW zr^F}}Wj?ZmvQDG&f7uNHw?SObsQTZOKF8ltn90Ntp z6&+wSIr+_~{YX@H@c?h(=rOuWr<`iK2ieZFN3dT1?@5C`sD3P2Jh0#;2LIYrjsRnV(t;9u92bjHr?C*3CFIUwzs^{q%SJv(AgLukqJ{(~OjCn2X|C z%vJRyk?2k1_`)y0IR}1Yx>~SyKzI&bq~WOGIq938>aEcI-)EC`t4WKzg!Qq6qBatW z7W0{H0A~`(iF)sO#!jrM45W0S=kbk+2xNU|NKn(NY#2}n9q^w^`CQS3D$2`?-0TUJPi&TycX zQEqJcV)2v9(CTuGl_KAnLy`=^UwV48qlZ3~GX=~~&pX}_cTT<=t%qORd zHmaCuVCQ8vvB<^{bAv`o$W&dVp9RF=(u<~anWm7m9zw-r6{G`G-U{POL2J|%6S4hS z;&UGPW(!KyAUb&?C=@5}g5k;hMy%U^+E6hWi|!r>%BdB|4T-2eLO>{tRs8f<4{CHT zLE7i`BDbWJ;6PJm&aZN5>EF%KM(pBBzgkD6q^&~7Q~v(_|Hgy(e7m-@_23cfjYAa} z_SWQ@>+Dkp1@P@)m8BuEkj1z2cY~_Q^}_zG@cqWGO6>WCN%CvU7e>wucuVF zP+Vs}O{MTv>WXTH1tfXxTD=gRE0!2nLYTNZeO?*Bi5d7rVhz`CeH3_Fm&BXsY%nqG3yWOq4(jEfApWuJ)7aY&vK68deXI%0V)B9=`k z;`p6NjhPlEt^vP-9uTLI1fazOrxMT*ObRaZizk^Q5Kd_=sVU0Xq$P4pSa^4J4+|PX zZfElDjex6Yhs@+uXg^)x=1E}m1A%SO5UirH$q6F zeuoG8?gkT;NKeghd!W1}(l6*>WTF@RJ>+Lo&XLiDPNmHLCOI7@Ibl@STt2NiI%~uP_(M;W=DB_GoHyQ^IS@^;5V}WBg zTc%1pP@uR-5m{#4)h!~!`1uHeYx5aug;QKnTaxfV4Qbw42BegJPKBDtyiavHeU+oF zN^YXY1tpx%*%P=Vr-3)(riR^F86dB)Xov40Nx`lg8SHWRt*=kNkM zOvAnU%I@>#Tgrb=Kcq!L(IsU@m)nnCUSy9RY+MYtQZUZWeP^1A?B7&A;cuQFp<^Fk zO#}&~_$drNe-=LIU6`Se|SjYvF+EUYG(C6A2betm0#bBKB);BWpyZWk$DhqV*`=`MWdS!%*3eI68Tl}| zC|PW0S2$4T(QIEZ6S&au^Ll^Z?AM=Y=})^Gs2$u^PvJspZny2dG_x2=z1n}vEn!}G zBq(F6pWtcejkQAPd_RFAaOqjeR-Ey#+*gF4nvNj^N z-<+@L7{{yXw0WcNap)L+%nWw7eMhk) zNn`8anQHE&?1;moYVLDuLt3Z=ju?Z25st0L_J^XWb^$ToAA+a2b7+O&|5%*fI7(k| zAVj9Zws6wp;RW%NJEa8ulX!BPkDa%ei{KCS&XYGl7R&(d>$GbH{VusTMPfF zMkcQ~D&l&5OnurP>0mzi5&{<6#fayw*u{K0EAgx9zx_bG{e;$)x)w;_&+XC=?=q?; z-c7*G^`1@XX>FlIorVdbxmB*dt`uxeaXOE`bXH{U5vD+PSf~7JF^=A|<$9U>2ZK|1 zNU#mW=}v?-`v}EKiY(IDD{vy^KFC~2tV%sRk_NsoflUm33bb_l;)%Kh&fU5egAq zZ8m1x%8=8C{mV=sUi!lzRC4&lI!K# zM=fKFK;UYuMnn90RxkJdwA_9ULuiz|@e1!Ky!<9w73BqR<}QDu5HMNfD`6l9D;&C+ zFb^giMG%gSP)j4Pwi1aPkYipz9r73BojXW0h>2}umdDeC>X+emGX-3Fu|kpAjF? zasrnZ4wVlQ9U)_lK=cuUph5)9Wcgn`0keNzpoc13fv@xpq){2jUwFZ!FYwqZkz5dp zMBwLsg}q+|cW1!M2*Nom_INydBOo|4@?%A_0l85{%26ybD(=#NgZ(|8F_3aMuchFj zk4P)<=CLKs{p3-Oon2hgBuAUFTLJ=>@{rT^yf9DWco$6K$l1~*g9e||!fU9(5hLm~ zJE&_Yg_Ir5r&Cm~m$F?uKU2Q_>Ztdr%r#g7*Zdo1#UnTb3w5k#o+F2}Wypx*@83>W zdq7t%xheH?(x{dNt7X6|$o(atW6Kia6@GjA;k!lJUxp7haJv6KfiH)JN?!g~!o)mj zo$HzGqR?`G-~-YA9N;P>k#T$!ZeMZV@ zl%+=2XXuSCI=p|{H5%ylSI1LGa5@8DMMLyyxj1cpSE*GyM!j6Yt0~^%yn5;|F1)Se^i*X(BS{gh9r*;*we7sJ( z5MXaYY^dD2?xYU0ov{#+=F6YGZFlQeJrTa%{7PU33X4z=pOy9EfYBI2ftQ3_Vi0sd z?zm~1!{8cF7o{+{R0#C@gm>HLMg5l%vCM4$Dwr=|l!@d1g$vAL6;pBCd$`K3uNX)) zY`&E*hlFVJ5fb-B)G~#%CXI%O!c-qzoF?J}&&=nY2AnJzKiiehf=2`JaXeRiC_IaX z9Ae;OcFs1h%O+4^+?EngHH|*C5o~=qFF8qcTmPmNA*iIK6ym}062dhGvV^#2@KE!M zCa0T@@TFV33HRa9J-wrbPO}Ex@XrY4!`jex4eb8K*NctJTbmbZhd z1hbYgqxfupNomt%nG8U<$R{w-nu{hWR8NBi*&TQ>T%59v+4OGsuNOzN;2YuiYL@F= zD8~>H*-N_jtCLE#hjF8f#A3rbIZtczUGtO@fhkGN)UOB(Qu75ksI;!$et7Qjs+}^T z5o+OsJ9AW`$LV|%W_FD6sXo=aXWH}StWgs?x-nW->-m1%j0z1T zzyM1zKr|X(5DtXHp+!MxCD12NN&fnmJX^=$+n@h6;)s{ly?tuEoh`$|JK)%xyFFAy za+OdyQ?QYU$N@NInD*Cvxa%2Qu}t5{T$@lIA(cSkO3+p;3m&m7R7u8c*s>SkQ9tmg zXMk%q2{B9LZX^m8RVCeJ3VAKdmima)FAIS6;fX$i6anZn95#O0EERV~X?q!^u;TRX zY-DLIAOkm0f>DVaYk1UtF1`TB8zn+->xE+sM8uqo9@$@*;@wZ|7QXs9Lb6Ef$nrC6 z&6ZgAG2%FPf(CFPX3W}kr;HQ}sny*lTM;7d z-ToJ541CzZcm$S=3G@;01RzixbUY5I##x?biXZp+z(CI3o8Em=Ft5(@UTYawM`fGi z-0$L|ZsPMp`8aZaCWy^l&HGqF$VVVv`GBCY5k z;}cRsLMQwN?!xtlTDB|2h?&wEJ*itiWF`w;xtqE+CJNR7H=hBQqvkP`w{P*!z2bhk zu;j-YsaQ@L>&_z_s$`1u`AxV;f49|&!el)a8sYgm;W@^K9f3zc$e08lfi~c{8~~}~ zVPPU5S|xyGgV+!utlJ++K?jqW-@Tl#aC3xvSt1U!8*xtHNeTP$6n%`95?vD$9GEil<9EsYeiiik0Pbrt;jWoCP^tc0h7NVC?*nU~I ze(FpNf_$Qwd{k5L$pt!2+iDGq7{fWMUd_H-rlojmsRBrjaWZPrjZ*v`@tfBuT_~8> zb!EHj{D}~_iEj4*Cep3H;dz@#OI+Et$6d8VM9Mt;=oO=86^B{`IJN9KCD%2dW5yl0 z5np)7X!qw*KJ3S{1sViCdJhy!1ZIi6p2v6{8n+5r(}~t5=JBzNc(CxxJN&R}>ZAMB zZupNf%@RAI6YdoRVNRzv`1B_=kBEp@jQodh8V>i^-4i1oGEk7Ji(+Q#U+}tS42{&`9+?&z%|QA-qkzUxsb-*cIEwV_toF& zF@t8jrDfNy7B_ehGf5FI7vQ}BB7^AkePXw5(!~KQpFFf$FB*zH~J-*#pO3e~^;lsuEveTt;7*D^GTB&faNlPt=M_d?NJXAgoL@&I%G>N|c|GjRx2cML3Fx9K_oNN(Rosw-H%H zc)iEd`#eVMTJZWe`Cos-fBZ_13TXcHwE0oe!M1=acQyx|f$1^-v;{soq@LY>P>T|b;7cQ1PPU?FeZ!z`# z?Nh5tE*4CUj5){r-_ccWSMof8A@0Hi6}=b7fVS?PY0l_5dvof8M}k=@9-*}~0=iv$ z);%;eW$`_3gOIpAb-MrPxe$kF*V&g-R4NNmL_`wZQC*ATHYJb0vLy7oO>^{>LWK%5)#G;#3{{u%6kxH^v&YJDJr(kY^t_%8Gm%I@#4 zHvoMXpufja1%JBR&%a^n6+!f&AIrAx1oNE411Wg48nca!2&|F#6YwB7?r*@W!<%jz zpcPoIxCUi^L({~HN7^CR#y(+SgeS?op7?m9KKG8M_fT$B%+xAYOT5pn%C~T|KriqBhLO2&^lW5PdU(u6hCm#79Aj=eGrkZw$Fr4>vh?Zl6D50xKkCD}8(H$-$Jv{F=J zFxJabrnNF$VBY<;hXbid@~+>(;KiS`$ecOWpTRa6P?f!UNKEZ z!y_N*r@s!i-aWm}?fpb>>D=RUe+!JmrX|e;_Ua!xUq1Ec=l^`8X5(3AQmP5;{1$vX z5bLu8z}%6CQ*Kp;SdvQBfQ`ujZAd(%G0I%RB#)5J^ZUTL^rFVw;Wwyr1Hdv$(?6~z z&z&G^m*TUI7RFqv3m6u(Kx;Y*MHQ0=eNHdK98tGT6HH7}|FX~Xxf?AH`eo;lO^J5! zxqBB1`V^dbP{NwL4#C$z5Ck7Z&)xd0NrG@>W8NEM^nLPtS(2vmhxyQ_=K z*dqbE@qv#viRXT>A^evc{qj#S0O8&fG@MY#e}7497%!8mIgAOKnReQbC0SYk>4UqZ zCGj&?yf3Ld*${FaVAdK(DI}KVFb*t5Z-T$F9t7BG5e6`U3=V%J?e7nddmJ!LS0313 zI+_M~Ao;(r&X>*(sY+tUnM#nEt}`L=$9~APm3rS&!-M3N&4#NE?S{L+!B;J1Px0>$Fz1_A}II0%cSv*`U;WWu(Wk@a&idR`Seip^8gx2EBSh(y9Y|NG>nzf?h!ZR zq1fPM@jIg*iVC~FGzG`+D^hrgU={36AVNHwt!%ps)8gNH?rPK>!5?_lyEqOG&9aHC zS(2p234eJDI*dO9pC-98Z0eU(nCEIFtz&yL`szypcXCjP{MJ~ksFIK;@*rI>&=fZ~ z#z{UbytDSU)wGu>ww;UX{w&62Wm<#WQI^uiRAcAmyHdo=S`YJMjvWyfBVmTQ z@Z?UP@pqnkm^8}_<>v$|@#|;?9^r8M*PRc44}WI=_x3V1H_Sg7+9?*>MY(E2$v+A0@ywgHNbUMa5II z(#3sR{}Mm?njOZyD6Jz+w$>EoeJIFVLXtFGWO9Bro)^03TEI~GtnTQVor7wl-tf%j zaGZ3!*ZiqS7`iMouri6+LbFRG%V}3FOYYb?Ldpq2eoZH`c<3bUlTQfwQDyzyze>)R zm}W<-zr8s{S{=!<+E=aYU-;+ZW8vt-$LDFCek6x*INaJc-y9!PyLB{8wU(2`L;t(U z7m+YIiVc<3ueSd{S~a)4su7iD+|Lt;VM8`CqXc&;f{&y%io+yE{`pIhFH@#TYWFJzOEe(8 z7WJ^WDo#w%%iFS989ITrOTtVH&k>DRy_0F|_l?Sb6L7vPvq5a5iOG4-*mp78=21bJ{-~e z5^=dPv4(E=MKuVttbBA(f=lo*AtD_8Vyhoz2%irsyNlk*)s{XIVQ1#5fnEi9#j1N` z%fc*#@7k7aI@gpF6s~A&1ZCM>D6<18xYDM0qF39OR0a~$$>4=%d2KAN%KFxgLZt${ zU}4{PPOHQ51aoOczrD!|@{+s)qtx@azq0W8{0jWO{q(w#&zYzD$5Nb}O7ERp?tCaq zIt@mXx*-6d)5<4xesF5dqQCP8>t2$fEaKPQm0+61pwhWr0qL{D{FUX9#^%%o6W zJk0`i!JzOl6)!cmlW*%v0S+y6XJS)_SbnL^k0CgKT^H znY;E5V(Ueq1FowmVI$Lj+PI$DNJ}#<#XCA?Xv>0+=lol((-Nlrs9FHX;Mr~-Cv`RU z`Ebnx*E9*&r=rb$FuqIhLAgNkRKs20m!?Y#F*_ufa?;BPu8);>_ z9~JhtKfQ^Y!tDK#`gHWi$(Hr`V{yc%f`w#qj}~Q;`<#Qc<1gDGEA&3Ah^i|2H?|}o z$o`^$-+NgD1%2*cE2i#&;}pQd{fYs-PbuykrIL<42i-}9ul`qvg?q4z)KLJ}Pk=wfj8Q-F~C#G>VD>`aSPsf7uh z+H?FP!fmeE=Yqg$IQAw>6^^u><|EC2*rOoeedee9x1+tNj5&uT>)p}XWm;0 zU^y%2&hz8de63W70&cs)&eaGawZW$BLcY8?`HZ(I(&#v!Ym&|E=dV*39K8PhecDT^ z&w9093Gta=R#hVMjPZZITe;QziHnVnw2p?-Jx5ucz)7#e(##C15x2UlB?^S(1-JA1 zf6DhnTvNI;`uzN@HmC1Jd=JuWX0@OeE9^gZjv#tIMwa$xoU8R$*Hxa|3kBH4&-^_9 zq#fH88=9>dYs0`A+DmA(8`;nxpzk*Fjy|5BuBesz*c}L-(oA@%#ruNA>M9Us=#~oM$9S+Y_CPgaV*x_jX1n=90J&4qF zB*AD#{^LA|2psrWLvkNLL=nZ(r#SA|fgRAWLICa)$kDU|yB-;Zz2b29O_T~4D>#K4 zaORnXV!a~SKa@tdGq1@{$zG&jQel$Xl$4Pfl9RcCIm!9#cb_gD-qpfWHk9kA=1ejnV8%vsh_^+u<7_y7kStNdA&V$9!ty&uD2_dh$mT)?+u%3~?auz16OTI-P}qTJ*@4J- zHoP53Y7+YL5rV|Zc=_n~WHuu-TNxU5jwp8g)PxW~nrQa$NN_$2VYmaU!-2J-XG10> z=ve3HyAmh@mc-+Eqfx{itm#C*ArYk#r-P9N#qjTXJ5FA5c97MTSH1sHU315=H6!SO zh03g4wu(u3PSvS`#v@+;A8Zfn@ssj5YBb3KWYub8NOzg4745js66b|4z9ztSjw<92 zl|)Smn>(M2yC5W31(}Nr#Y)HeRBLjZ29JDarv-AqjMe%Fj{6-ef6ce#?4&KTR!)&@ zD1JrgIlJVo3uI1$>8SH<&L2U;#@ZqlH*x7V^Y~zdk4lv|c0(My3j<*@$yUfX+jrHU zAP24{aqw$ErAQoQIB*~pQAicNK@&}3fwgdKd;rt|$3Dk|^OGPWZ=u7%Y{#A&eKfq3 zcI>`68{*d&CN=0UuCz^I_2aL!6heNLvIo}RRt5J3M#C~en zpv33;n#Sxsk_Hr1?%5VXO8wtv^AmdEY--zkfwU*n?k-y9?7i( z{U~;yi_tQO)EvR6>p9F^TT0jn^lG4+nu289&0sBWmbzmb2BDjVs0nstY2SoJs|*PL zh`b#fe|ofz@1lc?g(iFZ|50@A@l3Y=1K-(f!!~1c-WUywIZvo<&WCCwA%{7iHB#xk zlNlx>L`ev#Bs!{8J0K)c>QSjLQjbbSN$Tl&{QO?8`;YsN`=9&sy6*eBuFv)Pyx-4H zN=1M(EoB|w)IvJpX`It3Ye=~y^|R|veJ)3wn*#1*0BeYdEnKK68+o(=d;*YsW(Iz! z#&FG<{=^@+l}&gaj0j`EPM|@&wPZLORKcs`^VCgf>Wc}2Q$Gp~TvEn;BzUjNyB7319R%To>b~?Tlg75g@)CvNtb(hoOt;^wZcJ*NCKVD`TE!Z=^woVp{o(Kbvzs$qO;m4dHvub`j5S@YAYR9Im zcvCH59RH98#I^=7pOdpJulu5aodE!|h@g!OSbPK2l!iP?1n~jMf8N5wYYey0pfz&n zRyHt%3w}P0aG^n4M#1z2XiWnc90Jbes*}-OtDe%p>w$S{mFuRDmtg4syNU6MsJu3P z-gNr>-AITl+dK(aW%fHjCrmfa6-sfmzjk01vyGc!UhcNJqBt&O!8x4WNoa*PkA75O)-)Q1$J0?>=~P(T#2HDm%+ER zulD5SU<-sn!+Fq`pgoQ`&<_#(k(qc;08|>EdtEFXjMMM;YDYZVQ=6>sgQf^`tfdeD zRMw*206=u!j@qVP4S-j()vHEt50>|O2VIH!Zuw95rZd<+1>VH)9sCF<>Doo(CYDtb zSABV^bp+W}iDsm~Z~j<|)5?*AH&OU+G0XngfDG!fZp>2cnPv6stCkzK@<=ZGZajFK z{^?q=ZkI%CXtR{zEy}>=>q z-I9+Ix^!iu^E{1oV^FZ5Y zG$XcC!n!4S*5@J2=q@4^9h*Oz2~uo2*yzQEOnkFly#g7pFg*4a7RZH$m7MXF-B)0) z*ZhMY8Z)6LD1?Fc&$Zd@i(iL}Jp3WWKAW+bjR!kU!*Ea%J{#KL_2Z%^t)l#QD3VOP!fsxcG*k$-EG3aUHc}5cYp%bAB@z2Vgt;^~= zlC>hf>u%NF#5`S(3_jA`oIJVY?Y;9N|15Dc(!yd)BAvMQ`r6I6IvdWu#f19CnV1f| zTiR-#;uiM}?^D1prsh!;(qrzM5xXzx{w{nDd-COP*!Ya%&LL=k?Amq3*k^6<27s-S zdVH7NQ)F5Hkp0UDMVlc(+(S4qg>2$I@bSO4E=TP)5xMf>u0b%h z1r6`j0p~U#4^C~{!CfQPsOK&~Ox?#FNXWraM1o9>eD1GTjDwGa+l7i=G* zAYTjK-|0l&9!1_^Acgf_XG#KZ^_n1o&=O^Rb)%b}nP~IRWA|H3y*rN()&;ZN-vk{; z&}qQKoK@}^6P&UDx~N3_H3*{cb1j*8tRW8cf^NGimhw zQ-J0@25O`er9I6WzqV1n2z|`cyva@#G@#~)NP!F`0DvhAAd{o&uQ_TI87^G5ifW=k zJP*4EaBl7epzbuFUQf+EXK3W|wwT1~Y#Ie^CmrhrL2dVBle&rr7w_l1EBppe=uy4U zf2+J`voaRkwRg>Tdj}ClGrCHI8+Te!Xt2vXbv}*jN(8x%!a{|hWUf)vAKhygN2-*G zDTvPmC?kgJd?#=CylpNfN5a!p|LE1v$(?HF3Z6vypNR>i+-K zn%IACbJUw;8qc}NfA^>l%TWooaLfM?x6vrY0>a%665EMcs-`J{L);M1DT1UVapLhl||LOC4N59po4SNX4(LovsP~y<)MNUS=E7l=$Q2o)40) z=z}ppaf&4vyMsCpJHG%DKD@(H>9v%QpIQBGVHVJlc`*^hi-i1vu=YM{aeS-r16q22S#J zD6%!Mw0y^X-gW)mAGU8dV~TN>_DKO}cJ@n(LcV)HI^7%IO3<6x*M=d7{?@mQ?}h6= zI{iRuxvjt6gY4&X#^-e8kM!r4qR(h9L@sB(yq@*H{?8j=XdOSwwTO4V%k7cNp|d{O zT7Cw&y~&Thb&-F4*nV-xVb)!%XG>2fw(T&vdqI2Ow{v$s%wi|_PSa@XYt;Cbjg8j*OWLoX}z7%Pr z-MS3i+14h?B%P%m-1bXZTG`H=(+%zjhBd|<`e1QthpCxTebus99$%Z5xc|MLf6s!t zBULs!e0SK{%~>)Mush6wd=GF5mDnY3a?iTiw^_@13E(p6v=p8&nWOFBJehmRPl?!j z+)pW{X8H+s9{k$3V<&t0-Obpe%t4o_qiV1kXaA7et9NV0J->RG&4B0a9!$2i>IYv@ zl+`AGktOOUfQq=a>4@FpZ7w$DJ|{C!uHVTd3Gr z7k5z4Y8>vqu(6GBp&@qf{&!W;VP||~W*sY3&4)4o%6_P4L9p;d%8M^1%2t8u@tby* z{loj~{=P}>|Fm2%+-KM0HTF&KIT3A;-^o9`x~evn3cq3d6(ZXSIo^9O(@b62_<-;E zfe!{=y(nieH-@7JoLd#8npK&IeVt>%>Nsf@@lnD(37>o=2g!6aGkDhKZG31NX*JrO zP(xuAP@%}k*;zeG@su(431ZzZfx$g?oMrZ^U8yaHr1#hl=2#8v5fZ`3q`5bv2lyfr zIYVOvjWv(q6sE}IwH`KvT5`Bu9`q7K(}21|Ke=$@06+bCzUlLB0XmwSfS^wRb=v?Y zRQFUApC(>-{vMT_cvf!}(}&{#SkaR#{lRdbhGJds)TwE33%h?ic?=xQc#D0U3e|3C zbVxcx(uxfiU@J$@Y-}sm&m9m-I+q2cQB#AhFBO`C1)&?;D(wD3_!gEO^iwGxA*3qN zVjk5j2)I+EO-_<^*5iT$?G7udfqfAMKksTq&puJGM!URUE7v6`BP#Hik$Zw!jTdIMPZ z@J^qXf`#JjB@)cD+pA2w!pcR;#7)EdjoNcc-RGJN6p}8d07dD}ULcZJ1hzl7up>>{ zsI~fP!J9&J)<{_*)nwQBIQ-@$2ZuKqT#-SNm^E!FEqlZ*7Q92Ery&A z1=cnDFC}aCzN{xnvcuo%O#&+H*gWGzkL0Ys0BF}lriaTm=ArVU_XjE-w$H()p9jfx z6Gpy+IS*X!6ag;(k?$GVUA6DU2mQbP9nZ*2S3@^EtKM}#Bq!^I2o$G`B5+V2<@bAh zY0q#d9Eg9DQ^>;iqrEIIjG+SQB%`Izn0c2elvDi8~4 zL@?+1>V6A@M)zn?+qP+UZ$>vIU`&mQ`-Ft=0PFtt5Q~2{UeBHb(-ZB)4 z8BU4|M}fW6?PXXzB{C&4|CfY@ydjqt+T(8ETR1G6?yfI(ghtdp9>^HPuikFMv?~e* z=`0f^HW#O{-SaBr#QzY%lZ~i!wr8SP0HNgb5wV@{2rmr$0b9|1I4wN6`W-d%hyMPX z*R$_!-4&f#aHa(D?lBSE>__6xh9^ zKj-#QwB3K+Qlvy#)iGtE1fi%}fyx+HYUIpac>{4hvNNFsajcfZSsgk;e>x#VEU%f` zaJ!9le*zq;Ul?QS7zG5Jg8TQTO;ipjeoWBIfEU;b?Sl1Xwg$PewwIOq>zn1r0xoTrYkSFYKHIspU z0q%-&tF0!bPdI`^I}m4^^t3@S*L34*<6VbQ@WKZ0CW(+<ev9{_ZWR1l2fa`lN+7FOutryrH|Nz&78Wn3%=A8{wCm|>Q@O&9 zQypJ&t~wCFpLRXE!7^x>E%gnvgeagsmf;#4YXaOsmq2Atq&j37~ci-BHln zkiiu2@Ekv>>ppQp7$sZl?Q1!E@cwiYh)kf}UA$$vQm=7MTF@v3_q2lNk#hOvnBmL9 zlTtg1M!KLQK@aRM7r=i5ofEwdMF?MQJK-P`IKc;xiSYW}0H!+-5BFk@1E8Zk9WGG2 z=3Hs>VCFw3d_}#bp}lp@8dM2CdG$}pNH`(-q4XEy<^zVyC)GVW!2OQhLrtHqL?Yz? zuOZd`vXvT(tPS@g{(vkvputc4Xdr86;C(q@AY}ssGHd*4ramRaM*U+6X#uz-VI<}F z%rmR69Nct}Bd`xQv_%g={-C}r3h3RS`v(-W7f?*o2QzD%8TSo2l|h5UD9d@^KPu@6 z2ZRv$kr&)HyKOiaF0iJQ8qz_wobI#s{gPSfpk&~?8-v$w@$W&cR{?G2K-5q9tw&w~ zJl48l4hCCSn)4dH?a==jALGBn&or3(R$_g80oS7&-t z1Z84`6zB$e+P*fP&ChtQ4ST71d8@>0?b8?lzkYDa!6p;q6X#wn$?M~0XbFn>|CIo( z2sBg1D9~Et{?l8wbU~J>Al|jad_r>eW4+xJDl2lB+G7e_BdLFL{Q`96n8~kGJ=Lw; z%;fi<3`mKcYGy%FRS$^dCLq!_J5i2*0xJx0XHtQF=`XKOK|GfzPaEYM-wi#y)B~or zF(8uzpL#})M{W65bArnD*FbN5H_y{#1Lh$>7#ENecsfv<??q1Zm?QYMT=@(iTZdvTSin4Jxl$;bq+tqEDg4T=dIAFEW~LrJ>vIlhiro4lw1 zm<-^Z#e-1FblSF{XEgb%$U2 zA7R#TF3JBc+4?JwLKyebO&s2h3|k4vuuZ z#~jmY^8)-y-z_Ovu#*2g-&I;=187#Q6qJ6gyC0`iR*U!gw-=oUCho02c)Mi9O_~ z&S-HU2W7Cn%CtzG$Q7h520F-ex8ERSG1)Tg|v2uo1lt38PS#HYf)T z)iXHlF#!!qCvypnff9Zo`n`xi3+hC_Xo8w|i)jAcZaP@d1cwA#9x}jia&HF$(;5+JUd zgC5ek z82beP4mg4HI*^99Zbh!`{gso{w2G}hIQnNcf$WVCKg9^R>ln! zWDFDlO<-k5ppnGF=mu%pkkkYXF$sh{9fT*rVI1aK;HnY(p)WpS)499eH^O55WDG8H zJX&m3D`w0UWacBOZD^(}{KtUt^PYRBN(K`DUn}$AoCPQvGCauLOgS9my=Ij**Mh-A znZ~6(8~NH?9xNOHZs2cd25Jw0^!)@MGDrOX%lJNz5Afc45*6v~_0DyW@n~-Yf%jOc z*ZH6U--`w_Cq^zr(5OA~b+9;zetT!PIF2jsoeOZ{R2j=b=N0-mHu&}h zSoi?YZD1EL5yT|oiW~OWwxLttur#fY*Zv1CBxYr|CBF`Xgw)r$QPPhc`P=o+JQ_9+5{AyGUf7xuz{vFcp;a!bFfpNIlb2tZKXyuGzyVu!f-Jj% z*ziw>;)HZRkR=D~91h`yC+;FD1TZbrpf)Z^CDt@yNhvv|eZ{)A_J-Sti@dS3NvVhYL5(mDe!f>v8!8;U9NSbk z_A+5ue|^TK8e(JO<`?x*uPN8pctB-N$$PKdWwBa@JJbDgw?EvIF@dAruZ|1ecIl-# z^I60C-C>C5*ApMdytdm*kLWONYAE_xHIKwtc!Wj^_o~-)2i|QTwA&C=ZEx$tv9uPp ze5B=GnrB>XwSG}!8{0X>OuiIqouR0RSiKW^;nir0z-!N^f+W}Kmoa1y?RVZ)^KSR+ zNf%qKw|1u7ZhxJ2P(92!Y254c{{GCn`m=ZINZ$qSJ1(c>OLGpwQj;*2OuBv?1e+e* z)tej2RzSkQx^^Lce);?Yo`yajiUSs3!-xqvlyap1L?=kyM#fcX*oe474Hv&0&qBC4 zsQ{NT*Ci>novAr{%y9)VvVOtVZqvUUrP^lC zo4XREcOvmBQr9>O=fwkRw&`kz=VKOmK#O@IHi2Q+piJK!*XxJW`t|mh^Mni{v6z?= zAlH$nl^S6K6K8e8+7&*gMRTMpBW~X(uRh**C%DJOHVO_#;&hiV_Q3qaA!Y&oVAPXB{SM#x zy-Ts=i!XEbv-uF+{X|=`js(_(f1Fq_qwVv zfXr11KZ(d#DmT$@HG=SDFRzivAu+cRCv$rM(42Q5{m1!Y2Z)^)z%I@&ByUufOJkVI zr{IuAK~~f`B839a))CatWNw2!%V!?EJO!&2yoG+uvW_pVV~J{H#P`&{ouh-oR4XZ> zW9!?^e2GfJII+-tEs>W{qr_BmMWjCsWDh{BaUMo99@i-Tw{}Lk^7kgffgrLE%76EN zx(!kiuqx1=RW43t6_H8E2wLsnlhI+vW~SDGaN%$>$05x^sr6ve!sX6~M}?5p^#v@e z5StyT*j7bnS~k4bhoE-jT`w?Vp!l4-*2E@Lo%1y$(tDYN*wFpx5(@M5thu@%@EA=! z&F~hxg^DYLuRn)6I12r3Z%K9%c#${G@o~=xA~PZ%4lZJ6b;^7#LSgFrLsgAEympB> zGXBwvKI`QLrx)Io0~VjFgYLlXPM@q~_^q5WhvJ^hmohce>8~*-UygG$$$1C|mSLOs z^{>HH&lL#U$6-T-P6s;P)7;|O@GbK0BgaXpek}d=(dVcUsn3%{&$3Scv!~J=E**Gn z9edOnCOX{P9$?yvjPn~>-#U9?_!cXui(6m>8s&QvSsR%wk`pTqzPkTfssB?7IWXjN zlYV3i%W2%shLyWhx-Bf&c0;eB2tvNnN5M#FqOsB?b}Zded!g;gcy*(5M_~25g{!#E z6kZVlxsZD}pICJ-i{Yh#W=a;IEe90F#64Di=er-1qQ1e64AT4J|9Kpwo8||<+vI1O ze8Uta;p>^U`iV#X5(ae&9nVIny}M9IU-IiXLg+NX=KK$AMpxkVgfGVDx^&)S{p|zR zq5jo=7+6ob^-xcxSM%Nb!VCK|8m9UcaswjSY7Ut+kia`68!Xakyw>X#iEi$sL71Vp z9x8_RN4xb-9MC!IIlwnWR^Vrze9g+2HV2knoSd&+Iwx~nWmjr{*!F&wPysPD6fAYq zOexdx%DYOQvwuX1Gq;4zDGC^R=(upfrSs$I3l(A+6*%^U$?>QVeC8`|ZgJ3XI-X&e z+i$k%(5P13T0JLV@^4?y4NsO=KpWLhH3B!*DF5}J#>C_gE@bB*j7H*jSAS0n{BOnY zxBFRrv#JO7?6M=aVw<>fA3IbnozeV!u@Ly+eurkG+QyYG8IvVyeYuawo|-?__z+@3 z8E8>>SCdK0v@nV+}$KsZ!J?!L2Nqt=IgJpc}i@< z1pUE|uACBK6iNS=q`vGL+0wDfU$lA9T*s=wZca&9Kwb}Mr9lX@K)q|-A}Zzc`@@>S zExE5wW7jPcKi~S?vn{$ML%11YyVT?uBsajt4{d@+&*-GfE_tx=LFE(6O$`RJ@_!nn z=@QS-o%{Ro4Vyi}?=N+~5W8l5Z1T!_wNT<}gs$B8^Ql`83%8HP60P7xA>j#H=9p1T zN5efk`hSRkZczD|-}gB0;vz;&XOPD=`zW8vE({PFoGNi8SlYPVIOfIq#Q)tcsQf2+Woc-8BH4Yk<8YXCn03XKB0GE~;Q&AomCCy6R}lDTV5urt6ifvDPuLdqXO{U4Tb z=)n3>n^DV$2S;6qqF3m$5&{<`@Ov5eOrR1h#hZu&3l%^AMD2IXJ2|Okl zq!`}W=%i*n#afH!U~LtyL6jdCqHzmrUn#K7RQ*a6+Wt{GqCM}9D;-xrCn^QNEeZCO z#Xv!c9Yc7gPRsF{AdSYiVt}a&Gc-BKaY?Y&mpx73+br~%bA>i?R_gCQ+N{8-O+bzk zCWV3R8J??Ip4+0p)E>~gd8H!zOF;WfHYu{CMs8JKutTQGzS7iT4utIH+o3%<_vFSa zKD0l|^-D_YAmJT%FlV{Rz6bO%P1m?bNn@zeV=PvUA_v zQE;+kXtT~IlK&H@raN0OO3NBhDi7Ap^9VvmDTehkSMeTXx#)rt6QMY zkn4vl)bl~wQ*AI(yP#A7sGVG8Ux+-8Q!JT(z(ayC9H!G~B1c~s=qR+AmmATA)xVXR zvI5&BrOhPBF7fZ7-vaxk0_wGXTMjT^!Ey<=NPd}c4Wx2tQ>YI4(3ZrhVL~2R=s;1? zRsgsjmi5Y62Y2vV_-T&bYcsM5L&Zk~pn3tAg%W#$r(jt?rI=C4p0v>_tVy0Rhk$YESSrnG@J2 zs%+uP))6AMTip#9w=PnsKhLAEh5j=`DeS)WY!#jYbf9?p6Zzysm3j9i%^={3N@jV3 zsOR~KRZf?z{k*BO%h-bvp5mkfj3+oK}*48usni8qSmd2c$)imUAXs6Zc* z>yI-b;XsrZwg1RhzWVtGxv3S+UQsbv-AR_iyc|~-!X9$f@{H~CSj2S*PPh~p*a!d@ikIGuB zPa-oC2*3+ODtT=%({PXfdRc~sF;PLsoYHjJ&J1tdJ7 zK@Y{ZAMghNJSbph(71*vi1FnAx8+wGu;zkd6|!Z@`A)GF#nZUPX46llV`iWE zBxnI%iFf>BpLovx4QL`W?^d$vuOsOj`o%VF@h)`mH*x@t^VscYlT(n;2(1982%Wmi z{J#hc075$%pF&jW7y@Z4pmi4&kic}8L{EoVCaOIKLl~3nX&=-G(`y)EJAVx~;Fkda z00=-m^_?jlw8MVwJe2`j>xuI7T1w~X*8^-1Zd+rztjhyE*yFdBld*1QEnsdiX*bAT zUI?nflGw!A>GkL2r1*>W@pj1F#j=3WQy99!Xi}kxQ~dWEWXA?>k$^Pm%*d<8D7M1q zuN`{%!#uz?6GObOjR}9U--stlBlxd&O!M`_?ZrfXTFJ#1^zzKU=MO}pEGS=(sL*2j zpvS}fURVG2l4kY&m38{jTyG+=TQR$zkF~0JYguMU-ir6HAm$O_9cQ#A72heE#;D3k zROsY@0wV{^NgwYwRcN~_2<|}WhV2);_XWR~8+9{Lhbnyyf#&I^xc`07xFA)_^SrAz_m_9tb4=QEbln-ucvx22&xXSBy=9Nz z901ljq5QT#;pLUS&AHaBitD3%b%`bWYt70P1l9 z$mw20#*)~+i1o|vSKK<0@`j0PVYBk9({jJgubVr)tX5W%Vh?)t?z;OVWLxx;Uh?^Y zm=KLikf<*_oLGsQ=i!!k2v=E)2jTR6!ZGiGOM|z*L7uSB<9pWDVePd6D<@U-pRy?2 z0K4b>-v?G3U+Fe!^S{l1{^0)m-u5R1%I&6$Wz8vD1@1vXmKYoZ=cOKc&2nt`d-ch) zzb$k`AE*jEn}`)fWRcdypxq)9VEB&i|MG5puipP*^RElPZrlEV1JDe3FW+qZhyJ8o zox%Xp|M7wVKx6>?&z{?-*wJbNVx;oM!Gl)Kg0GiAuXY~{4-t_MfnFWIdt1Bif}IKB z!e-yZUp@E#VQ=Q&1!7RsfN+)d>1~I47N5>(lm`bZbOtKTb&5XzJ6_=KS1vleI$m*N z@iAWANCht{QLo+mzv}bWOpxa9y{9w>tree+Z&^EEp3C3Yn^$V;{a@2xM|$bR$6qyD zA0N9O@oH5yu*EnntoG3n^JoQ@xI*YTP<@p9yIJ zK?A0hY$(acFrT}9T@l_cdAz$k!M7}?7^03{1BuGKr36cRXi>SPj|?udFK8JuwQia0 zaebGJCT|J-kD`}2@QCCIF$k=0IluxRn)-Shd%svR$C1Og zwTkQ^>c`LeRcflAdYh_g>NWpn`+=LEE9|m6+=FwgX&AECF|wp}_AQ&+oRzi7$Q-y4#)oI76QX!EezlsX3hhe%ZA^mfi#iX08#@qW3RPPre%jq>h>@>^^H zQNorj7;pPva;YQWmsb-zB*(n6z^Z~$wRNMbU*lGCxrp==@WBkXqj>#alO;Xyrt#Sg zRbwqDWDPUz8eOB%y38BXt@;t!8=87egQ$J@!0Hbz5|e5{Hu==O4cUHXHeEiISEB;@ zPwt?TE3E$xGD>T2y%PCej~Ws`gXIi`=feN?diho_wQ_%fgt)*8y6n07_$2mrHtpbo?5l`bRnD#Gx{jb(N+;V~6FKG5c8UFv8OCJqd|ku4>Og9_7I{hp34LA$mMvZ}WRV8>r4E{&5rN-53&V5TGi<1sNc4bjNfD4QtF^S1SfJ?gO| zckPh3$857~dv)a&eJLEV0~?=OMJLBY8KTY~&*g>g%7!(6;mxS!&F))1%hPjUb>`sp%wq_LyDU%0;cfymFt)q$9TywD3tw@mlEf4J-|(tY2gJ(0e(yF5aQZ#qskvuy;OtRyLua7}d=;ekaBiq97pM`iwxgc@ zF3^f>mBz?fI_cpoO~zRfWuTECSTl|tN5j7#1|B*n69V3G3hjuE{KGO8c9N*(3Q=li zOB#v4i4r$MzWRQ`8Wycf&Ha!9mA}B!D^7(m;anceJswj^7rU!>+U@HU8V#duudJWO zW^t$Ex1_2$HMeGQ1O`~_+q3n}Ei8na<`?X<&T03QBuh(O09h`i2MM&o{5-KyvhKdp za{x6s&v(NVh)-6)itC~eqj>=B_NnsE%rK)RfFz*z3+^1+Z{3Jo5D*GNe{n5X_h&(E zSA-JNS1iOqz7_fZ$=Q2#E(%S+ApG;1)K`rMfEF~x!{1TyV(=9aneoK%E>q27qd-0A zwhPpL&eY%pZQ7`nTa@W{R(}L8vb|zg%3Z5Rd^ja`YR5=D;C;wzHT@3lq>?cAgf$#3 zm_BWJEHIxLGq|=*KLUK5p6981lX1?8+$a?W%xFGq6aPOL)IS`o|9H-arWuY%m3ivj zC(JojVkE{W1!|DdPwQZ6xKSJNL!Dr1`=T^Mu4H?$h4vkBr5RBI-SKW8=MKeK-btq3 z!?2V<9?GxXv6D@XdOE-i=DDJzRg&NVp>Odv}C2Ewvuc~MvoJ%O(p8P+-9v+x3N0fv^3@SJU2D}GXAs_bmZs33o$ovUu z-lc0TA{b?k6vsPw-4!^cxL+n2H6% zYxxECvba)4tS6euaiks?(@2_a3P`ynvPpK250*RVb+VPQZ$m_etEu{Spfn4`Tb(LP zy)^Zp0ZjZRU8^v)!Xzf;IS}#$>>DKD9_VDIsJYfpB{YMr2Uyto@{C!RgBuWQbwus* zKR+36FPDj)sHm0*vz{qb#=-z~X$qpxXxv#rsWxkuuMev`sCo4jWF5plNw>FAw~@j^ znJyKEOd7y(Q!cwJi)!mX)J7_)GX;nNd4r8&OJ1*Dd!z2Rhj=Ma?HEJ1ZW=3@#>?cI z%|SLBh6!FIy*j3@vkg2^p-+8}l}_o%r?H}JEt~%MZvdoU(8n2Xy;dNyZAvFEPOrE_ z2mMvAT7euV>PZ`|IjjfLFL(h!i!4z;(2!Qi)QenYKDgb8zsJCl%P^5cnv)=%3#e8B z>HIPe-oOM`4PRQAT4Qhy)$>#8fL~1x@8g+1OYI_^?DNI~w9HDnwXKDt{BN?PJb=#PDn`xbs?d>nyvo!mhHN7{Z zFV?ay9juY--1*y*^1f5%xyvxa)dC5?G7!$w&oe*&bWh`-ci_GF zYjw%EWpCYd?7a$pXAMC225`h`9=|4$IKWe3gLS_#^m-e0mPa(9iVw3>)YR#CS%}_V zz9D29iTjG=@YmX%^%AE)Sx`gj`Fd?A0fJ*>tY6xcPkcj5o zygq3CNy=(}Y}h5rF|*KATlyC`z>ue6u zWCEbM4bJ@I#Nl94lr`i(fcB(312ThcZUFI!EC5O8KgN}LpcbX^e-hJB-6Wl-)(PPt zBt(Iq{65p#{yr}bzn8h@8nXb!t&uK?LZVhS3C$tG$L04b6nGg^H?eUM#MJF4qLYUS z0)^(wPrj*4tgJx?F^7i*>9+CKYkvJOE{EqTnj`sEsr-QhjXEh%2f~KwjJ|ZUJLmPFx)=2M-sAZmj)A&`)#BqS{bU$xCZG-RO z&}0B@DN_%JBG450>c;Qm??Y1Gvz6k_$3Ro)K)WOFee#;t5^-Irm*F)c*Dn;{3;^vM zht-|N>GH!gi`+$+iFpMylZdRjx{7E4#MOV+PR)o6Ux!PM)80_H+5&~Z&lkB zuO{HA(R4TiJj7lOqN5=+W=3s6Mn1yv^AVg8G+5K2Fz%`p6UPb0{HAc8@9RMXYTj8U!rDib4hcF z5t_S@MnX}|*xWUjLQx}0Qq83hYLjazDuh%+Nu@q?@zvFDzyJ5g9((M)%lmcC^L*8- z!NJaH!NS22Pw>zdqT~~>Wow@2FBb{am3?fSfx`;2gm(Nbo|wf%rROM$U_rix%QsaXNKd+C*@`PoQb9FPnBvd0p z&*wTDgY5sc>ny)x@JKicGd8>_a>Z2(WvzXJ7+T+0%O(2e@{L`X@EW1pYo@X2Pun@Z zu2K|Ydz!a^_o^B+2DC(Hx`n^Rn}rWb_(1Ekog~ME(|W7f5?5edt2{qooZld9anOU; zytjl8@UIYmk|&STJZF^!xa#+k;LnYb6@?JB{I}X~JMg~JxvelZFLY5OBj6Fm@ zP!nWHQ^Iu1%jbZxxReCyFNOc*=ilzHkhA^w5GIVw(A_>h@WSKIQyz2&ZQKktK}7+c ze*zrzg}rkPUu-NsaOc!K(7u2;vltokvAcLuJ}yuvJA$M$(ameD@oXlYTC&v{T>Zxs zCKfCkJ&~u>yw|@;bUy#yz*~<4xhWLB%RaE2eL$}4)gZh|&i&(<@knOLBq{33kVQYA z;1{*m+*Nm~+pR`lE8In!-)OR%`hB-;Quc~TnzKGW)0Ylorn;NtDQFTIVs%pB6RwV@ zY!z?dFsYf+%Usb8C^flv{*HF-g`HXH+6p?TPqjaxItxSQ7e8bli8jt$DerAAM($jD)rJxk!7ReKjMVNq}$MDga*02YbiA&3=Ke`usnma}04yQ0bpxnn>F-vi8FHpLKQ_=__s}LKC0=BK%0x9d_E`_VyL>?H{cM*v{q1;zU35xL-i($*y$>y=F7C#LfgZMXDL zN{82%#JlFKi}(nmmlm*GWjka)p z*}&ku|A@SHV6(2?*?14kEh(v_Z!-r7kpQ;aU-RzzLR^0&YI-UH&e>&&Xra zEm;yKG%66+SAHO!B!(u(^_?*eyI6b9#@6^waN~df^EGub9y*%{i4fC|744#7e+h57 z{w_cAHsj8mtNCihHyTTFxK&Vv{Z5h1i)LO|sMriHci-0Q$L;(0zt%UAj>;fos+smQ z;8wG@j7VhFYDiH3Ei1or<}=tQHeTHFEduD%a4gTFsL8Aq%9~g8UOyg#N;J_Ilj7kH zA#z1nrTZ_2b<$_c-We79SS> zv+8iZ9>33C5+{bmRd9ahvy6Yqek~HB10Jy%|6BD0R`s1e4qkmd{w3`M>8oM-R zy}S^AS_lgVK>PXf@e$*>&MWabTPGew6mESl7<9G&5+N3-sHFjWvLMKJ+B_kq#C1Yn zoG*A6see-Q*$gXtldE(>BdQ%b)8Oo$77b4q#+~Ns&;}*a1)^_ql^TIUTaL;-S`*O6 zHZKTq6EJ*+hRUl)r4wO)-Vx+pTKhQq#$qZ?;x83immGGNQ7}Dpvfl<9g$P~6-dlyc zh{e^4z4L37s)~?GR)wDHKt(4Vdb~3Ow>Kfa>sa}!VC1(+xz|+UV>?kP#~g70;6*vk zcQ0hRkKCY1kd8?Ma-{6}fU@2v|7EXSI59f2)ogXHECMKgM^XHj$g%L2*#IEG4Km`_ zW!Cy+!bQJrgvewkxhMJBqe97|R?%Jw>Yfnmw_E0*nD(TuSEb4=TFz>O(^n(Qw+C`m zXab8K1H~=CyMsB{&@>K<~0BP0kLeCCE0Zk2^S5o=kLBp z5+xUjGOeOyuRw_=aJ%k;UJjS35M|nE;@B{yF|J&KlguwZqM!IIp+V}VaNxiaw)MMg ze8WBM972o?PuuC!W3KGR4g;a3AwL!&50{p@$$fCBL3W*Hk+X!QEbcp!qm&*Y#TH5u z*5!P{F9OAo;$v?m7U9iFpZ8;>!8g*$AvS)sLIJbiaX7U}2>r7PKnnq)G4W^pY64a}SnDt_pQ-M>Gd9VRcGZdzV8^8Qb@PuHFcK&CpWv^FUAM96<-P#OTZF9FW8 z#Vr?bKytZs)?^+!@$D34Mk%E zH;#f?ZD}j4qf4zq+e<^yqNm%|o}FjV(ruIBiu=IA@s& z#`EUMIaK77^|(%(Er&^CZi)q-R2PHdD$g>zrVe@=sO>U*@vXTCbE zUpqK?Z{YX;HfDaeWAEQP`9lndBl3w4{{03(^>Ep$(Ge`!Vy%{Z8EZ5e06TExaC!ub1;z#x#DJxH=|{&EI_MTN7L?2sj_ITIr}~dF!OTrqyYhMau3DOHD@8q01~PIUFx;cH@uVUX%OON(H$X z#!89Njl)$Fh5}keZssg&`&IwiPqf;J`LjW<*k(^3$qncV9!yF-gm<$vjHM02Qn(oK_U%ga>nV2h8 z-`or$9qA-Ce31MHH)effZ5bsUqjjg)%xOJ^lTtnLE}^`G&zLcNSOCm5deq{pW$922OkeeRB?pEol);nGH}^mgLaytUX(+${0C|9vXWa0>Us+r#+}jcwlh2mRxX=c~W( zwGR}3ny`3I(0+{}%j6)eQe`wet+&7t6DyP8hF+_CRA;udr?(l~w82wa!eV0E4{0F^ zIG&c~>w4ye?dxt*VglC+~P; zZsV%7b0@k-lUC$pGSmDeNKg9lp5~>#ey@+O9;cqp+&y(>Pye4I+0H7Wd)s)Q2r0uk z9Kza`II`Z(KWB$_4&0s_`h$ZMJGJCUP0*nG+QkE_l47>VtT8zakQ)^*tK{8~$D^5k ze(SX>N&|!C0_583;I6M8K`ga4bLD(V9+{jWD|U{Rrme98wUVW0(q2-n=Pd8klV^46 z>29rlHwk9uhxjlxwC$EfEWPjR`R6M}s|yD~f`_LHR1uHB)l5LdNAh z1Fl$cxqg(0=LK`PPmxYlvqsH!%!j6y%}TmpaF`9r!BDl9Z7Lxkd6ral$-Bml{o)N%~-SZPol zRJuP>{JQ}YK~1~NndZBom~=>pi?rV!I8d%huPDahvQ+OJ+bFF8wLeKyKC5NK{1;a| zZk;LDO4qbp=iZmQyK!-2Fevi`XHV_VNk)CKet*q1yc4Q zGLRjqw@8Z6|prmu+l11WoF9N z`h*!fFs(^R6*DfgC6tS%M=3s<8@4>Pk!h>`d-6DE4*0?U5M5guLd{iZ>l>B}eu0Tv!$WtOkH}1Y z?cSqo7fvWfWrI7URFmzJU`Y$7=vr<{q=1LJEU~ZBQV%?W(%<>-c*Y3lEIUPzBz= zTf?Zg#m(@9uXyv2lk9yCL>url|5eEnmWj^O+TxuJd@Dd!k3Eq23RuA^Hw*{#JkY*3 z1q``~ zktKm0gyK>>{#UhWT#=dOJIQX_ukt*;%G@du;ll&z+@yo8LYd+&JwwS_Dahu#8t4kW zL5!aVn^?s}HBPZifKF_mwPm>)zKiiMbb)@T20F)fP*txz-*Mx%Yz*B0c(@V5@P(38 zBjv-n&$yy6TH^~-ITkMdX#V`ppY!^zbFum+?Xp2Fjk2*kmkw9VMB~$CN$`Q?C=F)NOMRZD^R|91S8ml7Pt&-T=I)xrxFdrH@=#;lJ*C|C6-M zwiXoXzQw@~*MRkQZz3qRbxXjY79}<<{3|NLb)mda!r$_Ne)e{p<=|juXH=e)dV;ut zJ!aHP>s)blypjH;L5$?F2hPKk#h0}&yv@F-2Jh=_{(2W7L+WZYuOEZ&=6-ne)IYDd z@*2EvEtkrqjDPYSNY$2qbi(0FghA#>6t#yr;rJ!RLtTI{&mdt+C}4e=2DpFExn~DF zO+On6kTwmZ8jc9*|e4Bmf{`yr*`3BgL`_+GUPj7$iA}qc? zo4wm6T@0uq1gB7Q1*o|k*+njBtIYNhJ{ID*kt5+p9B|#g@3;R%BXA#=DCuX02&}_5 zaj7grd#B&1Q%*;O4e~ye?|w2juEHzUE;~L`vAFOu`#oXLQ?}Hx&D)(Ss}$nT!e&lm zW4DU#f?|15)v2QjclOS{w>eEEyZGaYPR*`omInIO&nf2@ZUh8@r^`V}8G4`0PU)qB z1LzPc0}@4qxZ{ov~%b`;l!-t&T|_G>@?fV3N6DW{~QiPUdSl zwJ5RU1o-i9_~ScoFgkxK_xz3y|24fw#Tj5jP*0d(o~| zUTZgkE7D5QaA}I@oD{LehiwVrQ512?oH>OU9$;;{el=BX#3G*rvH1r2^FDRf<`e-Y zr!$&Djl)Oeq*}_$DW|7dKy*6X@FrJPEuAeo()E-woNYiy9gd!kbnARWE0bFo+KDa9d?+(bc=w@ogl{7v`KQo%~TCnfev!k*)YW6+3zwpN$d1hiXDFh#s zNGI$ZnmXY@2VLib+!ff$jkrT-O3IYj_n0!V_i!#+ya5f<=V$52!fa?##as|(wM;2j z_35^%LuJZ+TW3=nIaki!~!wmfSl#|*SDF(o!Zu}I7m55?nj#;~+ z+4r0%v(R3$g{7>tbI@svJZXpOiVB7D2+VOp0Qjsl_>~3tMke^Y0)yY8rtO~Iv~~)di8`kF&Xs{4dhn7ei%@@>YSnQ^Phz}oCf>F%%`!&Kju7+B9B+3uMQhB$D5fGw zR!#4!qP}0bdv#6BHT;SgXeUTb$u=FcRuq*33IAVuvO9$44v{A3w2^c6&KJ1~Bh;TXw~NkLT$X@+%B|Q!EwFcTF2u(&N)*G|3lQ+ zQO{?fmnmlVyR&xa~=-;(GR;Q=o90D{pm=MiHP7=j%vp zNb-vdegMrYv_?x^y!Bd|Q-~dKp7c7lfsN>HA?U)x_yryJXKYjHT57vC5(v}DE zUPym=;-(K8?EN!6At*IG1H6j?aukAV_^8u>j0!oBy%73Hi1gn8`%Ho66V&!>fbO*!Hcs|9fJ0_%fD*VOix7E|kjc+P@X@F~I%38NlIdjM%|r5W;vG2H zbyuW217TZ%JY4{uut1GZg?aN$cBps2H#o>X4)WlJ`27te^@3_94_QIDbAt$}Mx!Eg zQO-xX^nvRP8d-72V&~4vU=bg5CFnj++2(ip ze;$|0M^%3!7@S`vzYld`9RxBZzUk+#IMaqUxj+* zVw-{;zYUb=<2#(smdT642nIdQNUJiZToTY~2MH1a2KWk1qQ%@MQ^w>LO_Vw`ctn>t z`4v?med#RxTER#De@xZ?Qt6+A1tZxLV~d39h03}DZe1oAz94`f$|dvnjtfz*e(LT7 ze}dc-q7n$ufE-zmLDVZ8Qu`xx0E%jlfSpZ)O%Rdx-pFX2YiB!hf}%f2L(KprWJVmA ze6Yu+^!+KM494gXN#-CInyDq7$ryM{(k_b-sf3ZG7~Ok-J)s&%f1+NT=AEQPYW^1T z5z%vm8~B=5=g+yDvjGzcg~?o`kP#3igx*6-PS6alk|2%y6LtaM&$&o{G<=?pTNH+iy79ml=0G~`%97p@~1xQpeAOm!X-e-z;8lZ0U$I=ahw}@8*0J$_-*feD^&fj7J(>RZT27+1EW{p%@9K8xkyt9(aV9V*{?0)NT1PPuc_EAVc^T( zfvJ4dg;>-y!`Zv@;xgUQdHE*!>2 z%YdgU^(Q+}&xKG51M)TuYR7^QZmi|dVb6J@Ss_#o*T0d5oW#Ks;@&(GLaPCice#$Y zaiZNxha?9uxzV7T2OASg(>EdZRmeMk9hPy3a0-Sxld!yeJA7)WL&zj2qeTt>`@Dcj zp=Njk^fc3?s}rfcTSC{Ta0hPBn9fnPM?B6wt~!&Vs$stJ_8tYH<9Cy5jtlr=q{&rS z@9Ju(w0oIm5zMfzg)MRyk78ama(FQ;$)_hJ|Hn2ICM(lC4bnP`r zswOU~>$`tR4MDjSgbhc`4*;hSf2kb3wiK~4x!C2C-yDHICwU?&fg*lv6%z0Pl8O_t zrLcH5_#{p|RR|p{0H^X?Qqk|aQXt8kKdB7nv-?R|0OJIDQqeK#a0X2Ne~=6UIAJ3x zd;{VQXplb-yTTQ(i*_axp3|yf$ppx|y>%Jf;A+mtZ~!`Bn({NpbuUH}G1?NmcBSCL zF7je1RyMvw-ZbjVy1+bkQnfUrc@eY!W0|tKjM?85(v``JX6{9_dCiZ0>g8J6pL?~& zcS_`SqBbZ>Zil>YB`4HS5^lpD)|h%T!HUivGy8OohkOobZLZ>)B1*xE=I@G~wZauz zH^06sYx2qh|M=OsT2{>jU6s@y>?ULI&Shu~U?x0x&>sZTaL{tcWR+|0YYv_N8Z%LA zb!2sM{#)EsgX+P~fiqwyQj`6EpUlsM>JH|Oqp$hb>P<$7I(W2x&N+VEb}x9 z)*4_s&jy)&3Z>{x*zYonWmr3BI#v89{7;T2aCG_8h~~CI3GZJ2iZM?huI$azimN>8 zWB;k^+Y|e&jfT zes@=+|EI3smVK|QTIddXcF;Trm(q;R3YNLW87y26SiiN$w8z_ei9#^Wo zHh)NiKj@ul?6S>6FB|rf_wR-K-&T%((z7%+v>pGz{zuuSpu_oS%tGQ$_uq8YFPt1E z{6x>nOs$ctlDbzcQY^ZJS5cEFZEDUF7WfjLF4QvS9b2mS(&J|?#Vz- zJ>H#hXL;^@0yW<6w`a{cGt^XaO8mJw%c@H|B^r`J+sLAGGwS*|hLtJNGrfdiL0-V# zJlgPc#^?(z5-0Ixkt z&cF5|g5^kR7pJ)RV;cahn80@LBp-IB(^zRnMO=S@nKwJ(Qv_eh5*qDrS=MQTX_8jX z!!Jh2cWn%|6tGoaQ#48!FF%nrDKFVQ^&UIPcg}6q8T&zAWqNb8U(D$jhiw?y{#sIY z6@`g>y>s9`l)i2f=n`U6Bd)aNp!v*$-jLm|q??Od$UV=gf#C;$PD} z=0V-Zimu~A2l#%#uK{|cgj?+kbS9t_Xs&1t^UBNVfYWlp3>#G{;LX4gh6 zOfY$-f0)XXV{WEfykc7aveFCkNIBx$wBuaE!yxXkAv;MTkp`6JlSeF0iAgx|Ns2SL z4n@K+*lA--sh2ou(zc1BWjo1DW{y}^Y_iN0G?i&SY-M(xL>w*BAvx34deBTFF&gwt zL^IY^8^PbB8)PSYEbO-U;4t*h9mOoluxW)@X&7Bo3iJvhdn!dD@efI&nlWN=P83#U ztURrOAF;lhg9=dqO?34PnN&nbL|Wu3O`=`VqEPd${{+Zh-iQ_3m#T*qlbhwbnG+gN zDbXvk(K2~fX&S-!P~gIBCInT5E=hFIRJ=ubaP5*a%&Y*c)T?$Q2p3fb(I7NK`h~}^&YjWWxIZaR`ZI4$heE8h#lDsuu53|K zX~iA?L)GCgEA_^+thnbTg3(7%w`N&JZDKKr-p=wMh?{pLy)Zd+8M{2kx?3wi#qddU z>37^rqs2-Ord^i5wS!AlBX=L!^}X?K7muwMol+XUwybm)?P`_wv^Z&TS#F8ohU-`= z_8~k_$lxDV|J7C;LvWTEx#wzrYI8ECMFVce&9ll=q3voJRO*QDH=#W(9sk`gGfQME zrKU&(Z}-cO(?`@w8vLCe;nAYIdFCLJ82xn`t8%iRo23AF& z?a6!{) zhd$+6R5cXG*@EQ8dq&j8@CsJ4>_~QwZS(BV5D*S1)i(od*jDZ0KgtwY-p#QjeyN7q z@Wb}M9y<-gO0DfQU<`$AAJrYl0fT)(2gy*ajlP3(tV5a5gaZ5DE67~{cGw(i0mMc% zL7m7pi3F&?0_e$r##`iS#6w;@9H*V*rxtZ-saI0F*T0c*vx64#*dxC_yG=5Y)w z+8X=N21NAnr-5Xd^#BbrA+arx#2%Ik2ecT)4zp!?>Gk0^n2y{1aa5c5>+G+f1{I3M zE;!J)2de)7td&`72v1mf4kZs2^H9P&dIgsc$ z&e{up{KsH;2t)qx++8V?P z&7zI5V|wx)8$!&f=K^b>!NP~C@dFCJ`?@_o6Y|NhFQi1wiqn0N$EMA7izC@M9w_}eCZ>n=i`)^qVdu%n zNqx}8-E59oyfX8&J^!d^5)lBVm?btzCr!A~UHI`K5Rc4uA5QxWxXP5#I22jd+G~ca zFwmTOoQK}Ee-4yLVNYZ8RBH+kQlJK7AdBZBo==2~iSO59Zh78Fh>dvCpApYSDOPp7*P z(Ir@+!w=3eTzmNPxxVk3brf0z$K@(7f;x6}hVhw)zjZtEL0N6>IF54w0hHMXPC&Ch z+cIf2Y;7(u1l&pE0=>%_8%Bg`(VkpQgRW`X2KO+FuG=b3yIYpzxM|vkpdm5veV#o~r@06E zpV*F5t@8EG@nfK@%pDQQ2(5nxiKT;VdM0<%EfcxB%r_wPF?Q10Eyq?!GNbFj8p#Rn zimzme)dF>Jec@MT0vT+5X_!Ugexr?EhcQrM3)F!MP2f8?aahS4w+A}=4iTTFDc&a5 zK$%(N(G1o;Mf23T-C3Lmi8vT_`>I`+pVpYz;2I>k=0OlAH!c(Mn(G^aMrrcF2?Vyj z*SL0pD_+^Bso;f-3e?l9JNPq{N|pK&4@m*sOVFDO(*upr-Qv8ZkJwAw6iREfx`}~h zda*=s0SZExK64%HPQ+h~>bN(VYg6YV~J8@j&S;LhZ z;~Wj@4s$mp_Y1)IR=cFNFUVUV_o&&~4@86B0@Mc*QyWlLy}>;57r3hj+);~0Y@X3n z$R*B!0QyWrdM=R;vdsjNzDuFS#Q$>6s`r4LaB8PLuofJ!LxH#!0VFCo@c6lRdkZ@C za+BIUD^m*{$2?D_i?yHc(jrmBC%mUU{}upJeA3(fSkq-G;5p% z_F=|}qNfYe-T+?c7q!uMnbID$@p82}CW8tfCr+**y!Au5xD^MCt1qITe_mW}+f^~r z_w4y|%l7A??T)weMRjT*QRJNhpxhEkdQQt@o$_yP14=aeYwa2wX3$f!%dI;~M|(w6 z@D1;%Mym~%Z(y({?YiM12c+D9PtfI$0ha_4=mZZOK&))wn%u6&R&q*Jpc3|b4NXI= zC=+3U@;IR%-j33uFu!~#!(S{rT39b7qT|m?-U`-K|0SXOMe=thBq2U1 zhKn@Fe61aR;Q9ycTY_@QC}~^8X7i_JuUmSO25o=dKt1nA{odbx={5|k5Mtkq61yV? z4%A7O>2*9OVYHF+Te?+ati4_k@-w0L+Zb)~XWi3=%TgDYO`s9;T<*WItAg>fAIVDz zhtxxW^>t$Pe?3$%p336_I1tP2BwYKPqioc30kjbL2p9REFK|~WiYC5O_M%-O)#Ift zMAm9S>EOq*H@tjp1(0bJQo8_^HP~P+2r)}vK02QND!<|b8HV}mH^!vla^x9434 z%Q{O+AG~q=yV`ggC1O_`|3xKzjLImJz=ib=`x=SMXi0`lOI4VrlFEM5mF2&lA09_} zoGba#@J?tP8M1p#BYABTULCV}Mn4mnd@bHb?fi#pnuskGjU4%JEx79uz&JsK4)I+v zuIa!btwX)Qj)<1vMw|f-c)Z*8n5Hgo?B`s8)Irs zc;P2v*`Q{d!#(;Zo?CfD{k^Vm{oZx^=8*)W_bKU7H}B*psf6shw^L$N@LuENioC_A zXB1ksra@}mSgVAFI9t}xuYs&s-xj(0WgF$G6Zu6WlApZBj$O;$`s8)@ZBg@@&lA}m z%eSi9nLq<7S3@+UFMMg=8!wWM08ByKI{_Txj^yMegO z=Odht*8Gaqi7;?f0e9 zq6=wdrxYc@kGd__IM7%!yZB{YPC_}w=#_&}P_T-oZN>1lI5UkVyr4~;h@R-UoROof zy$y^^h|yeAJ7@C2Sm(l*a@D9OYj?M* zb!zKK3$^|u=>FbjIL~J8YZI+npTnt7OD~%(|EW|+Ojn(K^1<#bN?R@R zWC)P|FHzSg;lh4H)#6nt>2|Gq4Zpa{F#%CYo{t@3qTXLTc(knKQhjn3g^_j4X|I;@6?XS69c=3mUbU*KQ zP%Jv%E@?RA-O=Z2AQ~SW!i9d=`%y4kwF7@GIKBbT+3>NhRw&m#)^E^neP&x+`#a$b zvM|?_5@xgwBziRkGOL4pn)-XCZk)fO;d5B_$nrmn^M8)+e7;(CIJ;Gn4_`SCEgPG< zdO3aVa=SudZERxM^Jw{%^SwVJ%(w3Myo(Wn^N`vv;aQw_ z8f3CJ<*riOg?`6XLn=^HmUXl<7r5uMMr=HKN$#X)lAL5+CG&jAi12HDU5c1?`AETj zwPQwlxh0&lZRO|6#?b8%Ky5UJWU4iM{P2O@O0NLGy7YvC&edjLo?9I^p!#4Xr{~C+=lUULx9&+UMn?%_A_su(~geufOR{%r=giirPSbi2< zI7SAh?EBvsx~*zy#B9fHsUb_lAopzIv=K%r!{)#J<1DV$U?O=Wu7`zN%gvC0q(ed-Px>j+egiG2VBI~<66oA!cV?8j_ zt@|$pw*HYxh-_gYg7%Bgc<4N-+X|EeU+Ls(d0Zp)F7!NYzE1Hxz13y8DeYZ!PK))c zt4M#g?I07aNCJ;z@GeDppFhfuztD&;hO0EXRNoGJ6^>w98`XB0_Hq#)ue9B|9s2O! z9W3nn7YomUy9jY?^qG#t$=1h*AriCNiGt{3{>`2;R$+HrepW;o*JAPR9G)BL-B{He zFKS=Tz=@*r8Mkz0r;eB17(JH~RK(zgar7-`xl{E_mqK}+@D}3E@^JBw*#OR5-DQbM37_1lMzf4Tv~C2l;v z8X|e{hjl1s>*VcoC!O zx}-Ee@-?!657#1afH)f?(lZ0BEW*#l_GVSle!cpNBExIZw;xJqOp`AAT;e^$%lIaD zJ@oR>KNcNmYn%FvBJGh{aw>DpC9>H&8{UvLWkR%0w&B- z+83PyrqZt;zBPX5K;FPlHduzNwWGU^-n}uaA)|+mntvyyC1L(Tbq9J6uk8;S`n;tx47^6A0E|prB8T^Aprf3C@TtvxSOVR-!j{SO6}PmF zEYx%y^7YX6W-h~V=h~hX`kfUTd3ahHr;6(t?v*XlG&8d*mbNb-nUAAsk+L<4?-s|O z)EoIB0&Ry1NdR5HOku6L!?&pXZZ=NV`$@7-yZx;BZzd*B%CSH7F@ZPd`p7Eb4+D0+BU*D*W`ufl z82cnOZr|Y`2eaYzjv467cExnPcFf43u?te;W(}OK8~fZ}WsD!ZI+Xr;p|f7zQ^p07 zGkbD#xW^Mv9@4x1@M>~aq+nMBBmSBdF9CRst%mh(3ld_*&Pm-n@$4vKWW~?af83%yoGQ^#TVrV)5wwfoV>#yj#JEB!7Y>{RvPq{q|(KEz}>(h<0+AFyOv^em#0NRa}n}y(O~U1 zvyxD%vxMDPuGI-&8#@Ovg$bUsdHANf*3bx|`vmiF6FpCJW5i}W2Y7`2yx{=QJ@P?_ zHlY`y`d=mt`Ji(&Ncdg#duYBm75xHRgMR#rb>@`t`I)SRtqZ?ehH>k~mvTmi!T$>1 zWknvMWX;*~I-(88{q92Nx#hMk@7ia)A0AfTp0oa?)gy`|dz7(Rd zJDk0HZ4F$pbsL4edYt67HHPvpI*=H^6wBa)k*YlLAO|5f+E!rX5FzD|O+k4IG0sYF2xPIGTooT-p}!tq0?7gC_-rC-9}Y>p%cf-I zSxusR$yOPDBty=oI884Hai7b=Y!Ohte~6|fN&zM~Q0YAnAM|AWqT!vd3UHUwK2&fJPMxZ%(uL(|2RvrAnZ%K@#%-=ySu>gqiC$4MXI8r~l6S~kDdv{Tv z7o4C+YMb9{_cEOj(XJEm&@3?ano=X+yR@s~BZU$@y{X&<&4z$?CIEAq%-Q{oGJb1Z zLlq5(JFgKGO@Dw8HT?BOCEJ^TJeeXFrjMAW{?) z6V-(&g~3f?hJjQUeFA^Z-aArG66cID$tm2QG$=p>AIK7CpF<*&NUn6HVFKl$4+FPPO#f_xp7rD9C z)6>{xYjWL|WrN0=)P*GFGQO6J>fbKb#gH(`L?(w9TRY5)G-E2CwIE7)^x6&_I-0o9 zww7d9Y<;usu}pOE1=weEySwY_#+6ZR)xSS>2lV{h`Nlu0S`YeVztB+D+0Q|L{EVDl z=$riW(BqKT%BZ8a)8wCZmOA6?^SZmyk4xpN=-)nkFvJ+x4%t6)3;BCMr+e%D4fm!` zB|q&C!`6N{K_~!_7O~H!hd-|L5mS`+0TtI0to;N~VDzs+E`{I(A`=cb9b$+{ZRhHl zMC3}w3#F>G&cd_$=METqh-0ata{oP39tq9B^Bm0DFxqP$y_W9{$!q`%}zj zt&d3A)L@L(E?-F)3-`$-KROU#yV||=Vs{s{|F9Fc^5m)Ox)It)y8%TxnTtmn{ME&t zc7h*uf>ZN!>*_i>`@_CiKMIaK4(W_?e?;opd0=l7nuuC%91P0at#K&f6Ygg1=k6xx z^zG7_dAXBt+E0~NpY%dHRfc}Q^-8FkY|?mY)L9wOxbOK z4w_{cJ?xcz(UV7d*bnwzd%e&GUF>3~iunh69Ncc7JiUQ9HT0-t1~%X_X1jkZc*1}1 zCv5vRG`LRvr(h(pYd%TFJ6Q-#rUzCXgQOSM*L|*-H5Pua8$E_PPwWvD+aI9`k2+=4 z&Huu3x}=(}!LlgzR<6RPn8p)-VCbEaM;-n9deH?$-WmCXNes^4;f7nVo*3+oZ=Uyy zf8YrR);@>c^KE~*VprfM_GmE~0HB9ho=tN12G=yXcm#kw`Lqjj4=7ZUN|nogna3`l zUzvZw4^M$7y{GYML>Mo}fWkB@D)(s{uW566+V;YCExgW_T)$qMjH{Eo`E2a(=-A1F ziQYkkzjC{=-gdqU3|O{gGGFB72`9re_kxu1;i zyEpDxQs~YbH0l8c#i9fCN@W7`M4mv2N65vPnbY6dx7KSv@$$XI$pd_f#~>zD6EKtQ`ffMMQ>KZ^j*N ztCi+sPf%wc%8EVIf13=Jx_Eo|mtc{HR#8kCJx<86be+s75oE+`{iqo|+Vf=3sgSH8 zsSwdvF?D&`L#gD>-Yp#Yh#352p1OH`uz{cI%)r{N$FRCE?hE}Fe7)*tw=V}zC>)P; z1u+JT9S#+!Fo1*@&mmel-^HB>2}oml_2+5zIB9bZmFo?AB^}!D9ZEH)kCWE zCgED9bc6z}CZ;A0GFi2fb=N77>ET{g@7g049Dg=&^Xxd<@M9-S?>l za+|Wx3HROspVP;S_fW#!%^U7r)5}_O@11*8Y=ApWPe>%=%4ZSVEZ5|j$PkCI(s3_p z=q1@Q*3rL`$BbWG_%|f6J=GIAET#eg%n={AxVY=Nx{!0!8Q?0EtM1}7XvnG8q``5T z>DNQj7sAtvhI-9i4U!IJ-^_u42MvY<5VDg2T-Lw9L-crq7ABNR(vM!b_)poeiK$F? zH8Ov})>@SN`rg2`73=b`SRAh0Jes3G{!Wm(pBYUUrYe1oYC6>_ZQ1f!COUblf&|Qu z-8n`M0(+qY6o*K{A&#FE;Kpz@)PmdfVKs-pw9vc+~VGu%k8q z4~6!#rSbBYGqTo($-mgj!HLF=`^tO`1s>-i6=4FMJejoV*Z=a)Vu^4f%M<+Apo)oY z=g$WKmD|K{O-%7%CS=h`dq|*@IfQ30E$&==oDEbK(ZTSQH>=<^=w)|a4&W`rP%8&u zBxYy?3EJbF81{B{1Wh=uUkGcN|$Q?9r*ox-Qd zkSFf2z0(r&nQzYL7$mi|9c@!lt1uw>#j|4N=*x;1bK*I!T8ZHLOP}ke!}n$G7!?J0uk8y+{*7uOdx~h=!)1pn!;ozVh+S?62KFJ3G72>~qe! zuA8Pq;pSRS?6YC}H9yL-9mh$H01m>xl=4tV4l5!D2yi?^Yls1x??*6~Gmb`Gc3<=z z24K!udbf1A1K=_pl5RN}27%GI0VY{rOBSKvO5FR8n{}`R*5*OUT_P(Ab!B&e{!-eb zCw&8R8C7T`@*Ybyi>2}?H!<~IE!KZ{1M{?KE()|TnX#EPGe7y1D$Kq%O7A=K?(q7% z49$0OEcL6u1)krHeAF~ssor9BQ<(N=jehI4!@buY=gbStgz7)HY5r-B1tzmE!IHpB zV@nwV$$}iJyiFLL0_KaZDetFgHSUss4rxlUkN?B!KT}y=cf>bG)ZwzEFSA+uhF2`l z<#ZremPc|rrrEksBe1kyLiJamiVpRPE`#)-444d5A?pCLz&rp{Wu3IPqEgH_t%_l! zb7^4g#*%>o=3BKbQ@RRcY7lZUPmT1C+*FTpeHj{r;4i^ZHw(XWO3oZ&qS$ z1FtAOB1FIcEo_=RK|jzQ&pkJ#q5lJ|gyqKp_A;=j_b8qf-E|Qh#2E#JV1a)J=vDN1 zilTti#iop8;Ds;>$V0Y}0vL;RSPMbmC)A9)_KvghQ?VP6Cjn>TcmvODqej|juQLCU zR!tAy`1fxf5B&IDd<(5+ShzaF-7hfcrtk3nbJ6}A-zzM{pG6!dCmN1MRqMi(503lchWS%1>zZe6H*w zZYcBOMa3`9gUaWgr^R83W<9Dl{SXUNDQ%(+sm?&}Nry|t18 zY8@g{-CaH*jE3ktWIC=&>4EnO5-5<69j^EOxjNIumiz%$3@;LN1As{*wk+|9H-9eq zv_`u9YO`Kg^5lw0p6W-8OBQdRpiiax6$b029TlHH7*^zM47j0!!O?yCrGM&!H4&hG z*Y$V)c8os60;7X_?8~b6W3#^bynJ!ujdPC29ggWWJ;|6-Rf*uQlc}UIaX)TSKuV}x zU5Px%w^f~{qC2?Bv9?@_X zrGGG;O89)ktU%c4_VxSMjz?-N*kzzUI)%iOHp2CCsWxdmE*8g zaJwcG><#3#mh|;673x4!VA@fBX1$~VGNmDn6{uka`Z_yuot)rslO(_EbUWan2hqFrbj5)edTSY4-O_fUQ&>gBF zBPaf%!6@nP8o5pC&(?Q=BFof1wD#>txtHkX@ztZ>!7xBDFKa^cx)m1=2(Y!{XZ5+$ z|0RvMKwH^v9Q3rE=54&=f`7+>)M=%6(0zkzkDmHZKK1dPzT&tX*Vw+w!U~j@I04=p z`MyWPdHs>bQhfW_`$)P^?3W3z6r%zcbWB)MK!{@^t>21g=3J*0&1g{j^|FUi9yC$DBoaXWARb!H2w4l}F z6G>)ClM_d_M7snI5ljk#Q(+jIDP)p9=6^|;@m{p8j{VZwCrYNoPX&GV#Ty_8|R$;Ob`<)kW+>~|4E!y*Vl%Ap2! zkuU%egC*+|xYe0|{o{=YIocWql)%O`Ru*EqD%JbSF5@|ahcZiXqE3he{)+cy1lk~vZg~M4aqQ>S)?eV zwuMuK3P>|>RRciJe^i@nx2j_S1T`-*j-|{w^Yyj$2*nE7ut8dtg0SNh&C-*IHxkH< zS_0-dUigbR$J^dXK1rU|*^-VPW`Z6w6Hihi28@#ufkpCMNH>c>Yy@@S94`}W>UJ0R z+66x<96`mpKy`JEB?w=;N||0eNo`G=$4WJE&9CvicTNa+-Xsl$hS!eH$!*;{^=A5_ z_ou9C-Au}1<|<5&$Bj1;#`tak+)oCu-!n4qoWv@J>`qgYyHxSC7UzBlJGcrHb^5xv zOruHvU+(i>&*;S2(?gasEj?MDx3v;0VVhSac$^;0oAAb{$Ydt!eQiw54H=}hXd)1{ z|HIft!v=LS=R^f#xfmP78vO*Vo0J+-R-Ze@G_+TJ5bCTg_t22^a=Oo*;#K*)*krjV znBBhGEvdeC_g0E@4nEvPa=9cxKyZ9ACTpz~34yCzR0AN4NPN$iMNJftpdvUx-Eh#) z{at*gf<;0!&iZl9GxddR=%3`AG#HNN^@K$QvgeC<(t~dgh6a=URzAHbLCnbX&g&#C zW;P*01MyiQ;{&u!#`%L&!v)F%_GW=Ac6cXn+D8K*)MOeoxMS+?E;a@#PA((x$qmE_KQAr) zcW#z=$NXg9wgCTBReI4fs@KA{;&Jt;M2eMQl!^UtE27YTMl>~H(a*S2LOA(KV6G~! zfoMBhc=cVKP_%0%U{SIWw*#Z=OQ@HlzM6kwiV=rOAwE_dGm5NN2jQ8BX|CgCtKue- z0(~iVe0V8Awt=Uj`9NqhO@<^V4S>S!lmo?Lg2rP6ruNda506(-3Y1OvKRt|fXgQzf2B z66YI8uWOu;xQL)x#|7!Vzj^;dbdI5_WDK43oRd97)v80!$W$RGQF6H6n`%FsZ9YNd zC#FK6SzZ`PFvk$j2%-;DRH49IY_&xFa9?>kq&vS3lmbZ1l7H$wyrWc;-w?6Kpi=XN z7wCT~h>BD!3_n@hWUOwggC1W<;&4;tC2pz|-)h`Lhh(IQo<)|*jCNWdNbPq92jYW| zUD)>8ZEB_b7E6P4DCo19Y22G^>E}>%KxNE6UJjW*8vTma)~CR>nqtdR!64ze%p=%H zC}FJ>1?m@yOALMTAt=Y5u zsbo_os$9*RA$gmw+@_MtPE0q*$0oFPq0uL(HjV_h5As>8__8fmhrVy?eQwS&#{x4& zfJ9`M=Rf~;Grv}8`C!&sW&LHGOfC%WbUP=;Jcn5`aBlr}*2!E}?d3CR=3C=)TFr_E zU1kVp-trr{ey8D3q5dLNn8msiGGD?Dt-1?hweEelRCc4oRPMk#mlCtmf336K?!kkA zHK+F$Txfn_Yt1k{z~L3dmf5v-Z~2YXZnCSC*1j~XE4nE zP-jvtkVX6~)nBK@`$lefcE9T-o>)Q4sB%N)JX$6$szJEnb{?OvL+4Y+>5OH7vzxLp zs14>SQTR~Vn5iH`PFrq}hFuOwYe*hsA^XM37P-?|xB;2)fOtjc+bo4DAV!sz1Uu(V zC_ifVNv`k#`w& z-VA_-Aff1S#tV1$M2I*Yy79X&XPcu4QH@H8Skpbn%2>#LzR7qsr8hYWEbXw(m^TyJ z`7wGGr#m_WO&zJrWU{4D8R0>5m%xh%bFr6QHHZZtr&K0%)!RVZaW0uSTrSj*XlC)&6TQlRx_6n%T2dYkndk92LvTDsMT_GimWfY0^Rxc&FUshDchoz?)d}7|o<6 zk^9HDG}j(H*a-X8_v5)kym;4}u0i*y-Ql@GNEAb}3bj(_K*|FZFxPx_++gLLU#vyU z##@5Q5cfsDUy=!!YqqOvSj;sAr>?Qd@+@xfddXqJ_4nMOpF@TeKO7Ol|85J@gih%; z9w(*sZP7mZJ!*6%phP`_i?8WyP$<(+qR=D& zXT%;t8lV05)dRD^B8j4Ii{U*`0Q7cKiLAUiQvT{t{%f6ev#9&yx@!h@T=I-Zgy%!8 zC2lX+-I|zqhpwDc^O=Ei(cD?Ax-J(XIAp6*{_(ht>v3*c#Fpxt-}l_Kj?*pvep0^` z_F8@GM+(2@uEWQ(H}S4NNMf3MHXlRh*Z!S!8~(SWD*NZd#fQ_5F^%mYdAenbJDlSv zErmFN#0w5n;L8_7Q_N+DkgMYY@RjAKto=l)7Q=x*o@s+;9diG~p#y&(pV?2^E_dIf zm-PvG#oe7M^z6^QIu??>e%9H`+E*{uY^CF&VMtJzJ0h8Z^U`s6N@U;}V1G%O)+rsw42x6mnB5>A)Hck3fR1G;wlSaCiV2g#|7+ z0teWI-FZAL8SfH~sOK0Nn3!0sm?R!DanCXFTXA)wIlftL;037oh-_sth-e@vny7ae zo%L<78%w0mYo@Q5h1XcJl{eFO0_Z%*G|_{pn^M=3t97OsIX;WADkO2Bqv!*U1a7!9 z-x&CS_jesOv!fpQ==Mc4+6|Ptfe@eLEEr&Zh~_Flvlap1?*Yd;BuFy_QGsTwAk(}6 zZX>U3N}6*$kK%PA?AC5@zw_sP+p@9g&&T_XH{lBJccxt>!tNIbd@tvjbM znuBYG-JZ-Yjb`&e!#xI=4*{G2AUt(|=Qoo8dWMhwNWiICn$t>p>X`deI`7yquf#X$ zH3fm8jLq5&frHkK=V+Rt<4y60SD9J`=Bz()nKSDXek(i{QdNK+-j_dz%k-GjzPluW z3BqQ~Y&P=><#KFB3UKo6^(A_ut1cd4EM~G`rbA+t++c~DQxX$kLkX~%4~T6H)6WcD zm~MUXf+-Q0kCx`RtF1T!{UnP?tU746O)SsDjVpK(s~(!_mBNKANs%|E@iQWw_s{(1 zw*04h8@}Zu8G%%R?f`+e{#^Y6{N0MOa~ZrfnS2}%)i}RN#mw-D%y6riZx)$vk~0xV z3z}cZD?~C~4(T$J452okfqJ~&Wv(ERsbIqlzpkiQGYjuP(LScY&JxR%@DlSNLyd|! zQx?7Q?LOBZEc<3++LtI5ko7Nx?u&@t=s~dg-OS4|;8feT)SjR}ciuyz!p6OVETf;tTKSD!UjmG(qKrzjj0@ZM>Q9VbDVgLdnY6SU zzp^!M$})OE(kjj}p2{+L2{a)s8oe6W>smC+H<#TCyf7JQH_M+f8EW6#ZQna(+3G{H z2@-*#XjBS`Q`)U0ZtSk>(7B+ENTDUIu#%2l%iqTo&;7x8*ImtP;(CTbX09k^84{~8 zn%SD9^v4Z+N?|TSUioyFE}0UqkyY-S!1<_*Mij|O7~m9r$f<+oxH7|TJYZ=;;;g2C zi%@J81MH4Ko~{APnXSoOmc@t?cWbs&?^~x=cBV~i&adoD+T5MS*j$E`UAj-4+w7bR z19?srcdQ#}LfjCY?wk--@MAEpYgVu8AxtcQ7Lwye)roGfgw{60&07Bw1)Kj)zRsaB zxUcgK^Y(Va))gHRgC7zc4PYgr#A;BGBH{(B7g_IPg|r)P+GnpY3klV7Lu3HC`rKu? zukf@2kGlj;3eX(Mq`f63Zil$~DKc`qpYydZg}vAfh| z`{bWG<}P`6pE|vK!cBJ99uwFhZXm+YTNP29fh34l#VZ9UtOP}!Ll9nbLp%8^&_8jj zBA|;2w9RrfyHZN|kV5R%ZHDU_o@Mk{veF$gy&jRCM~B`RVD3tJ3>v{ZQu~44WIjbP zV>`H7N!+bbjxT<4J$H8;5b&)-^K_v%dw@L8*?eeI9Zi0D+T=Pu>U?tlI`{2m=SR!I zM!~`1*MkFc1A}fpsR{}`sy-N&s}?Y>Jl(D*9`W zCa^mEfeNumwlx$rUhwN?>Ye#$BSEOY)QZUE(2NHbWNHJELql@N0>=PqN(IgQgl!qk zni3u!hlJP;aE=S`ObhUg5AX~R@XV6pCZgg-qT;3mcxI#GgAhD}ByP_Yyt@idhJ8Y; zeL~?%LMb9~$Q@tal`sM1iO5SVUrEdjNvJwYXi`adv62{{m)sDNG$rsPJ0vl-D|vv# zy^@!3=Vq!62j?g{{=5A}{A(bm3<)w*Ca)G}-eazi>*p?yMqJ5vqhV)jM-Q9LjR|3&I*eE8x_9&?(zQ0libvwFa zF_9TVS+W$+6+regqc?;h-?(#ucRNDN6yL!Z7IPwv&uWgpzZf3(@kB&)QCfnDG|1lOU+)_E!&~{eTa(C*Np9t|y-k|x?d~WlK_rk4(81#7 zV!Z}HAc5w8+Ub#p~fm zu5&fu|9;I90d@>CTYVH4j>MfuqJOeMsO(LDfpmX?q>UsC9G2mCw{=lCje>VVGdSQ( zmHLS^K#jAuw8y2Hrj`X&lQ1MIg8tt6j_!_Nwk7!Gk4}^Kg7FjXvNRA4845CDKh2$2g1R5&_hJfx|1`4+&9-GUrKuxydy2WE~Gz!{5m& z4T+8-L^}^xdEZo!z|_RwL%e{*vrAVt7Cb{MhzCh<#eXeXgRh}3pa_B9_$VG6iC!*> zt9pQ|F^UH}&|3rK3JgJ%T#kasu`9z02b;}S#xkE>o+W~LHv2zbUC5ru;*SQBh1j%! zWUBXcB&?z7-JDsWG{Rp;+hf zs}XcOgF`MwE6>(f(q0OiJNNl&TrhtUW(AX_yMu}@(ij^ezOsbA)kSfY8DH80o(knjk%)RU^aSQ@*P?FeDb z7yO&1(wdOnl*k#we9ZbMo9Oacr_U|k_-aA9Cp^1NNDs)?aPWJZEV7NCX5TRFvcJoS zQN&PW@$jWmLqTIzJ_7Fge)-qLat(GU$#sPEa(9ex_rc%Mn3rR*8f!H!2X85(>*UdU z8f!6;?lL&ID22_>Z@pMRsBnXODUy37^5m(_A??RQhFgb!Tn+;Sz&}gArwMRa6Tc9N zSjb(`YPXpV0G)Zun^-i@Kj$7R65GQbdg}tt6i*uEm=^B;qE-vBFt#s_aJ@osf zfdBSlTt=caS1#Qm;MoRJkX)=Ob#yWsv+)DQtLmdiDA}gz`1+y=jjL zPYK}3 z`6`~hjgsM_4-vUnox9r8(H;{PLV7Fzc#M@~oCQz6ynC5+y~(u?aLCuSM*K#_5>Csl zm8{ZoyAJ|g32?rSasg4nrTgnSY^yEv)Wa|S(W$lX&Q&n|5f16OI{Q}7;?>Vq516m4 z?f%=-+i#K{zIV;q&q?Y1#z6^s=}c0HgXio_RXKM@-<`~ppR2i*0A8nMSFo=(d{xGq z^kjW{BuPv=a+>4kmysrmSO5Myk=^J`n=@yGu?f?<@#@AUD~|P%Gp=4&0_#f+W0(i8 z&R1kv8Hng(He%)jh23L0(c%mGOdoWjfsF(uI(e3p*##%`%bzTah8^jw4uf7`y5ur1 z8#cLj`ztAOc~ISyYnzmDRtGmdG>*Z)CGVXRc)(82J6;5f1%;-gqO%-xZa4r^*#Mj# zX;wM8cPkq}iJqj2*1Lrd`(gKr;^_r07@h07JA|2a$+1;Tw&Mn|4~0%{>%~s{S`a!!DaQ*R3)8*p8H^p`m>a(0l#`eNsInWI=W{gAPW`rBnzGS`*9nkj$05zz3N81V1+3sRD+*_RKv z5A{mhC5yhw0uqHb5b<**+i!YEA6bNvyy;TEF|6g0!}%8}J%`~(>3`G1TSLSwO`C$m zW>i>^RNT*a{lEbFqV%1q2c4$miBC!VpQRrr`^EkI_UijTi7K}N9f^$k;|WQU{X7`L zh;dJts;E{Dn&h?G-!2eprn=DRzqC_=?iM!KnO$dJ$o3ahu6Zv_x^`+A&0G>AVVbf5 zxAAfceD|cNC&eA+7jFJ;{}VT5-0u?l*QGF4@hL~3yDhYxhxLX1|zTj|2dZ3K{@Fa$XOuiI7(3 zytOF|ldyNA$L&{$t+OWDMF6SFx*+1#0@{K^BE1!1kY6T7a8cS@ypd@f<*bPNG23wM z++px-79%$rXmhRq;)FtTHz1bUSTav$IE%J4=6cVAFR^orW^y$*%+?uGHREm48lH?? z=f7fp6-&onwm)3zjMU}i6riRh&gnsTW1OvYM51CTQBSV-Hjr;IWax zSw$fn3!pAS$_2G}n1LB5t`+;<{Zv=uGWkpNGNm$I=4!2#x($ds5x{~WM@gBtzy!Ck zOnKrXQXC85to{-2c8}L5?FPzj^P>2SRx@IlZXt1cG{z(Z1df!@^)|m}BgEsbwD3dH z`@_LQ^1#LgqGWdH*I|lQ^E(m`gu}eQ255!<1EP5rT}`z+=>HaFz(8kACxMAcmOdl5 z119yO&lQPOKjYA5PP16hNjtS!gSV7aihoE9_ZC)%2_r}kDE5LnvrQ%v4o6eC(n19e zy!>@*W#c7aVg3#h_a%K=%5w~W94hFXeZl-m01`+wzLzse^P!|*+9Q%FGw@B)heESx z1}jD&i)C}ZUA^EH&dZI)@hr?<%Sag#mEmj)e|i6MB?!B}ALiDE``K*S64S8}d7e1! zJ(I283(H}Ag`yse7Hf>$x&N{Awqh)JRuR0xlR}!``llIVwaQp6 z%))6$$_iaoNDPtIwcEcfmpBySLtpzYSr>Q!qU;`iVl5>=#Cd=;?ClJ7zWGUsf0NAv zujpAf1K*T#b#ZsK_{H8q4lzGxDZJO}*Th!x>3PqcB95Mvf~-_gg_>(apzj763j!^- zgSvEizc5}OshwiJ^e+{%W^unqj;V~I?>m1E0VN9e01=R|FeqF zKS+F=KmC;$Q82@cp<>HT&}Cz}77UyAlnz(V^Cbnwi)O zYG>keDofN8S^#zupI^1nQDgRz{tz2X&59)esp$CdmJc^+!%_AOL@X!&nW~I=?~VoF zcg5n0?!ugg=lYW$xU()&L$a&;xN7~fcZNGoRyteB0rA3U?7{Wh4cziKy(nqgj@vai zV&1&KekC%6%i-QE0`{NESW4Y}bE+*UP~&$?%{fMa3<+(w%5fE?gr3mWzUA zSSlqWVY*OU`5f%J7egHZPPBx(%E@cXQ%&n@`k}?D27>1}G;O=MIxngq=oTfz*;qGd zoMk7r_f9xQj>-lbaUY><*%fJG?eEW|%WcbHa{F565lix8D{nnKtv70;rOp38~h6_EXB6<7H=gMByLEI8^4*j5Fh&@7!~Om>C%~`?a$-T78!amCqB|D%%>;cx*Tg7hxPnL z&}51QvnG0BPzG{T2ycirA)Uz^qI*c7L8LFlr+bH(yr@Y1p~BGtO{&{XlsV35I+Huu zjK2zl1Rup&k4NP(MU(AqHrYJy8Kj7HacZtAl4di

W?()v0ycHt{!l1uzDpikaD(rGtnwK(8P1K-+R!c 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Ð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 z≪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

GFc$4o!OjR_r?!>I1tkx|;A=&Y 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

l77Rgc6OSPuPQH;a~w|zpT^TCU=NxNlwX^Vh> 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%0Df0@FYx5 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

WMvHna;Jh$4b@}{p{|~sIfK+ z>^hBjWIqIN%~+mjHBs-QSsn@r3Zx?rOIAmf#Z>ZR$41M`xqDM;`!M>4Qkr$Tl?=UsErE$dXS^~YR~YYdshwe_lvoTUOcLLISYsB?Vg3c)a4{5#3V!n z60>?sF4goFjaFO^D)sr5Gy1C=&z;}I>3q28zt$SW0xyFc-u?RH={5RRr%)(X8SiG5 z#os$&Kl9AQB|SH;?lm;U=6o0Y{WSQ*F|Zq2dC)#?e%KRFnPI6(%Cw)5r_Z?U(!FyB z$B^LPu2zH|%aU-p6A+kACs6h-^Y+(%f|=2vD{8Dm{Du|Jo6CI(Yn0jCse$bOyZcUs**mp>-j_c38k&357^%(eCXoYrbGe{9+> zu3+O{CoDJV{lv>l?K6G-9q(fMq|}!{8bevoqVR0QqyGFwY$u=8&5*z3x&3Lual*xT zi>J{D;ed>9ryVrn3s*<$G<#EfIfKbc@lLIfkV=TQ3}Iw7SqhV@U|t|O(e~QS|GCfD zchl$9yO8jr>0stQK5bJ<+_bAn++u)&ONm#HSKR?n|KruIr(F{(DLbF~-!tAj zI%_y`E@3|CoEvzb^=>u2_Wqk6H`(7O_qFA?Exu1QOzTEYHT=oW@z1`yV6i=b@X44_ zxrKdBNV4PJ_)8m|4WBpRtGqmxUSUkQ^%ETOuL&KuiNMZrEIwc}nvVPDg6dqY-UYiC zK)q!?x}%|33`O$%2y&tUUKG6nj!i*_#45Srw>0iuwU~=`Px)Mxp<&T--8t6o5Tf!c zn9HS!^U8CL*5>ZB>2OUl-(>ntRAv7A?x&fUf0+pMuiO}(SXsW0O@{}qw7Zq1kV?Cq z%9C|b)#WbbcxT*78Ep4)`HCeDGHNz_Gij5pVmE<_GSImi*GXCKpkVna{#HjV`(Vyk zr?|YLBFEUp^r9T+RYkYW>fHpT#Ds0vj~{bB!xu9M9lMM!Z{NIIZKH*r3{-qiT~X^n|eJ`WAT9F>oc)@t2k(d0b8Z}-3XA099B zL|on-Iz8~8cRpdhnep~9#|@rODu}E}QP614Y_*IB;<*^MbYp`lVlk7*y zLZRn}MWLIlM5fE0)7>ndeo|Lzdk-sS)+_MwaQu_N<0jqWOP7cEisZb{KP~lo4E@OF z>hUjs78pI9tJ-E#E*BWsnE7j`fY1EqH0Q5|oq~g_7EDjV3PY~ds4e7)USIng`+aZz zp5KQxwRr)ZI3B6ELd6#;7DsCL?>220^V|1H+&gDV9m>2yrVPtw6BsH4y?0B=3X{lKjZ0ns3F7Gt{XoO6ckvFX-~UdH0a z)|Oj&@>l8+mGa~IWR~|5FPUN+OKf<=%*DylhO2F&^x8RBviX4tZ&itr6Pfcu^{*bL z+d55tudk16LbXy=&2T}1oXsOz*!NZlkayn9g^dS=J_v;yr# zQ7F;gt_U}U{RgZJkq8~hdspjlo6{fo!v&p0+xhollAKe57|9&iX$D;A(oI|U_}}Gk zB;$Zma#@0pV!G3oRfO)R5z$3;S2WzM7YK%mLm3$khkIKYrcqq$MFgwG?c#W<&^T4% z&f!e8U^7azC9b>enRl_G_|j!Pz+f@<`yv^>Pb5v#?zelL)1h3R&&LIJ_W$UcyrRUp zV6M{gbsC#^GXFX=YCr|+UU@h__gE*JZ-`DJj{6OUA9xf$mhrGPW5=~eW-ue&v@CL` zN^F%Y2mklZVb8R4QawZ{)ZppPB{{>VE$rgKuHPmN&3Q7)xu)lRg`6qcW0Tqv-5;xC zisC}+u-$DGH3Gu(6(b+Kg^gaV>o@FfSmTd(`y`fcfs}^wq`3W;aTlHy<-vx)Tzx

vgQ}}S0Hq2E@OB=h|Q`nPhzw3C4<*0r735vVP~C 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

xhZ!U9^kg2(25iIx=}g>JdI_;7vOA9@9{H!D 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⪼|7f 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%0A`Y zYO0!sntG;M`nI|T*82J;d-s{_-EU=JYHw)fE~sMLFTm&mhHhzXYUyig?e?F_H9PkM z_MYZePUZ&#EDn$b9^KN$%hKNczZl)gn(TehnPTG#<9gWkfU%v6pS``qe+t$d z|4Y&ZrD_LN9S(XrTAMh!9&vQ_b~+U5d^ps_@~DfIr;DwHi~Ug-w@?@N09Uu;ZeIRJ zyiOcF5#s4X^*Vmi>v*X5QDyIAM&91e-V_&azi4m&@Z;X@Cjvu!LZT@re0<3YzFx|H zCr|kYh6Mx!1coLBhQ*!?4i1XU42n()3k&>@O^-Ys9-c|ksTl?7*%!}e=V#=VXXh5>7F^29&CAQboOiJzzl2p-R3T95 z#buX^FV_}VHJ4Pima!_!tDCM=HeRW&6TtN9n&xYjSFY9Fs%>hkYiz1(;WXUnzJ8;v zskNi|R!>X&AiL@Mt=qk~+Xvcu?zRhHdf!CH?T+rA;oko7-oeS

cstALt(*7`i(+ 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

{Y)6g@cTM(~ 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 diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 7e819dbb..3a7fd9dd 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -96,7 +96,7 @@ var UserscriptSettingTab = new Class({ }) ) ).setStyles({ - 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.gif')+"')" + 'background-image': "url('https://couchpota.to/media/images/userscript.gif')" }); }); diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index 733ef1ee..b43e3985 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -39,7 +39,7 @@ Page.Wizard = new Class({ '
Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)', 'content': function(){ return App.createUserscriptButtons().setStyles({ - 'background-image': "url('"+App.createUrl('static/plugin/userscript/userscript.gif')+"')" + 'background-image': "url('https://couchpota.to/media/images/userscript.gif')" }) } }, From 8f4219a93cd6105d9e7b60ffb8faa628fb2055ee Mon Sep 17 00:00:00 2001 From: mano3m Date: Thu, 3 Apr 2014 22:12:45 +0200 Subject: [PATCH 210/468] Check if folder exists in tagging Fixes #3069 --- couchpotato/core/plugins/renamer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 1e83140d..b04c96c6 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -655,7 +655,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) tag_files = release_download.get('files', []) # Tag all files in release folder - else: + elif release_download['folder']: for root, folders, names in scandir.walk(release_download['folder']): tag_files.extend([os.path.join(root, name) for name in names]) @@ -685,19 +685,20 @@ Remove it if you want it to be renamed (again, or at least let it try again) return False elif isinstance(release_download, dict): + + folder = release_download['folder'] + if not os.path.isdir(folder): + return False + # Untag download_files if they are known if release_download.get('files'): tag_files = release_download.get('files', []) # Untag all files in release folder else: - for root, folders, names in scandir.walk(release_download['folder']): + for root, folders, names in scandir.walk(folder): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) - folder = release_download['folder'] - if not os.path.isdir(folder): - return False - if not folder: return False @@ -732,7 +733,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find tag on all files in release folder else: - for root, folders, names in scandir.walk(release_download['folder']): + for root, folders, names in scandir.walk(folder): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) # Find all .ignore files in folder From 610edea20e1c574c39b5c154a1eb8d130f5d77a8 Mon Sep 17 00:00:00 2001 From: mano3m Date: Thu, 3 Apr 2014 22:46:22 +0200 Subject: [PATCH 211/468] Re-add path --- libs/unrar2/unix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/unrar2/unix.py b/libs/unrar2/unix.py index 47129f84..9ebab40d 100644 --- a/libs/unrar2/unix.py +++ b/libs/unrar2/unix.py @@ -39,7 +39,7 @@ def call_unrar(params): "Calls rar/unrar command line executable, returns stdout pipe" global rar_executable_cached if rar_executable_cached is None: - for command in ('unrar', 'rar'): + for command in ('unrar', 'rar', os.path.join(os.path.dirname(__file__), 'unrar')): try: subprocess.Popen([command], stdout = subprocess.PIPE) rar_executable_cached = command From 5d5cf5cf290ce426aa2b72676e737f65e1102b75 Mon Sep 17 00:00:00 2001 From: mikke89 Date: Fri, 4 Apr 2014 06:20:17 +0200 Subject: [PATCH 212/468] Display charts (such as from imdb, blu-ray.com) on home page. --- .../core/media/movie/charts/__init__.py | 1 + couchpotato/core/media/movie/charts/main.py | 89 +++++++ .../core/media/movie/charts/static/charts.css | 244 ++++++++++++++++++ .../core/media/movie/charts/static/charts.js | 148 +++++++++++ .../media/movie/providers/automation/base.py | 13 + .../movie/providers/automation/bluray.py | 42 +++ .../media/movie/providers/automation/imdb.py | 85 ++++++ .../media/movie/suggestion/static/suggest.css | 2 + .../media/movie/suggestion/static/suggest.js | 9 +- couchpotato/static/scripts/page/home.js | 73 ++++++ 10 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 couchpotato/core/media/movie/charts/__init__.py create mode 100644 couchpotato/core/media/movie/charts/main.py create mode 100644 couchpotato/core/media/movie/charts/static/charts.css create mode 100644 couchpotato/core/media/movie/charts/static/charts.js diff --git a/couchpotato/core/media/movie/charts/__init__.py b/couchpotato/core/media/movie/charts/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/couchpotato/core/media/movie/charts/__init__.py @@ -0,0 +1 @@ + diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py new file mode 100644 index 00000000..467eecc3 --- /dev/null +++ b/couchpotato/core/media/movie/charts/main.py @@ -0,0 +1,89 @@ +import time + +from couchpotato.core.logger import CPLog +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent,fireEvent +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + +autoload = 'Charts' + + +class Charts(Plugin): + + update_in_progress = False + + def __init__(self): + addApiView('charts.view', self.automationView) + addEvent('app.load', self.setCrons) + + def setCrons(self): + fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.conf('update_interval', default = 12)) + self.updateViewCache() + + + def automationView(self, force_update = False, **kwargs): + + if force_update: + charts = self.updateViewCache() + else: + charts = self.getCache('charts_cached') + if not charts: + charts = self.updateViewCache() + + return { + 'success': True, + 'count': len(charts), + 'charts': charts + } + + + def updateViewCache(self): + + if self.update_in_progress: + while self.update_in_progress: + time.sleep(1) + catched_charts = self.getCache('charts_cached') + if catched_charts: + return catched_charts + + try: + self.update_in_progress = True + charts = fireEvent('automation.get_chart_list', merge = True) + self.setCache('charts_cached', charts, timeout = 2*3600*self.conf('update_interval', default = 12)) + except: + log.error('Failed refreshing charts') + + self.update_in_progress = False + + return charts + + +config = [{ + 'name': 'charts', + 'groups': [ + { + 'label': 'Charts', + 'description': 'Displays selected charts on the home page', + 'type': 'list', + 'name': 'charts_providers', + 'tab': 'display', + 'options': [ + { + 'name': 'max_items', + 'default': 10, + 'type': 'int', + 'description': 'Maximum number of items displayed from each chart.', + }, + { + 'name': 'update_interval', + 'default': 12, + 'type': 'int', + 'advanced': True, + 'description': '(hours)', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css new file mode 100644 index 00000000..0527bda3 --- /dev/null +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -0,0 +1,244 @@ +.charts { + clear: both; +} + +.charts > h2 { + height: 40px; +} + +.charts .chart { + display: inline-block; + width: 50%; + vertical-align: top; +} + +.charts div.refresh { + margin-top: 10px; + clear:both; + background-color: rgba(255,255,255,0.1); + text-align:center; +} + +.charts p.no_charts_enabled { + padding: 0.7em 1em; +} + +.charts div.refresh a { + display:block; +} + +.charts .chart h3 a { + color: #fff; +} + + +.charts .chart .media_result { + display: inline-block; + width: 100%; + height: 150px; +} + +@media all and (max-width: 960px) { + .charts .chart { + width: 50%; + } +} + +@media all and (max-width: 600px) { + .charts .chart { + width: 100%; + } +} + +.charts .chart .media_result .data { + left: 150px; + background: #4e5969; + border: none; +} + + .charts .chart .media_result .data .info { + top: 10px; + left: 15px; + right: 15px; + bottom: 10px; + overflow: hidden; + } + + .charts .chart .media_result .data .info h2 { + white-space: normal; + max-height: 120px; + font-size: 18px; + line-height: 18px; + } + + .charts .chart .media_result .data .info .rating, + .charts .chart .media_result .data .info .genres, + .charts .chart .media_result .data .info .year { + position: static; + display: block; + padding: 0; + opacity: .6; + } + + .charts .chart .media_result .data .info .year { + margin: 10px 0 0; + } + + .charts .chart .media_result .data .info .rating { + font-size: 20px; + float: right; + margin-top: -20px; + } + .charts .chart .media_result .data .info .rating:before { + content: "\e031"; + font-family: 'Elusive-Icons'; + font-size: 14px; + margin: 0 5px 0 0; + vertical-align: bottom; + } + + .charts .chart .media_result .data .info .genres { + font-size: 11px; + font-style: italic; + text-align: right; + } + + .charts .chart .media_result .data .info .plot { + display: block; + font-size: 11px; + overflow: hidden; + text-align: justify; + height: 100%; + z-index: 2; + top: 64px; + position: absolute; + background: #4e5969; + cursor: pointer; + transition: all .4s ease-in-out; + padding: 0 3px 10px 0; + } + .charts .chart .media_result .data:before { + bottom: 0; + content: ''; + display: block; + height: 10px; + right: 0; + left: 0; + bottom: 10px; + position: absolute; + background: linear-gradient( + 0deg, + rgba(78, 89, 105, 1) 0%, + rgba(78, 89, 105, 0) 100% + ); + z-index: 3; + pointer-events: none; + } + + .charts .chart .media_result .data .info .plot.full { + top: 0; + overflow: auto; + } + +.charts .chart .media_result .data { + cursor: default; +} + +.charts .chart .media_result .options { + left: 150px; +} + .charts .chart .media_result .options select[name=title] { width: 100%; } + .charts .chart .media_result .options select[name=profile] { width: 100%; } + .charts .chart .media_result .options select[name=category] { width: 100%; } + + .charts .chart .media_result .button { + position: absolute; + margin: 2px 0 0 0; + right: 15px; + bottom: 15px; + } + + +.charts .chart .media_result .thumbnail { + width: 100px; + position: absolute; + left: 50px; +} + +.charts .chart .media_result div.chart_number { + color: white; + position: absolute; + top: 0; + background: rgb(0, 0, 0); /* fallback color */ + background: rgba(0, 0, 0, 0.3); + padding: 10px; + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; + font: bold 2em/1em Helvetica, Sans-Serif; + width: 50px; + height: 60px; +} + +.charts .chart .media_result div.chart_number.chart_in_wanted { + background: rgb(0, 255, 40); /* fallback color */ + background: rgba(0, 255, 40, 0.3); +} +.charts .chart .media_result div.chart_number.chart_in_library { + background: rgb(0, 202, 32); /* fallback color */ + background: rgba(0, 202, 32, 0.3); +} + + +.charts .chart .media_result .actions { + position: absolute; + top: 10px; + right: 10px; + display: none; + width: 90px; +} + .charts .chart .media_result:hover .actions { + display: block; + } + .charts .chart .media_result:hover h2 .title { + opacity: 0; + } + .charts .chart .media_result .data.open .actions { + display: none; + } + + .charts .chart .media_result .actions a { + margin-left: 10px; + vertical-align: middle; + } + + +.toggle_menu { + height: 50px; +} + +.toggle_menu a { + display: block; + width: 50%; + float: left; + color: #fff; + border-bottom: 1px solid rgba(255, 255, 255, 0.0666667); + text-align: center; +} + +.toggle_menu a:hover { + border-color: #047792; + border-width: 4px; +} + +.toggle_menu a.active { + border-bottom: 4px solid #04bce6; +} + +.toggle_menu a:last-child { + float: right; +} + +.toggle_menu h2 { + height: 40px; +} + diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js new file mode 100644 index 00000000..06cf9c88 --- /dev/null +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -0,0 +1,148 @@ +var Charts = new Class({ + + Implements: [Options, Events], + + initialize: function(options){ + var self = this; + self.setOptions(options); + + self.create(); + }, + + create: function(){ + var self = this; + + self.el_refreshing_text = new Element('span.refreshing', {'text': 'Refreshing charts...'}); + self.el_refresh_link = new Element('a.refresh', { 'href': '#', 'text': 'Refresh charts', 'events': { 'click': function(e) { + e.preventDefault(); + self.el.getChildren('div.chart').destroy(); + self.el_refreshing_text.show(); + self.el_refresh_link.hide(); + self.api_request = Api.request('charts.view', { + 'data': { 'force_update': 1 }, + 'onComplete': self.fill.bind(self) + }); + }}}).hide(); + self.el_refresh_container = new Element('div.refresh').grab( self.el_refreshing_text ).grab( self.el_refresh_link ); + self.el_no_charts_enabled = new Element('p.no_charts_enabled', { + 'html':'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to
settings and turn on some charts of your choice.' + }).hide(); + + self.el = new Element('div.charts').grab(self.el_no_charts_enabled).grab( self.el_refresh_container ); + + if( Cookie.read('suggestions_charts_menu_selected') === 'charts') self.el.show(); else self.el.hide(); + + self.api_request = Api.request('charts.view', { + 'onComplete': self.fill.bind(self) + }); + + }, + + fill: function(json){ + + var self = this; + + self.el_refreshing_text.hide(); + self.el_refresh_link.show(); + + if(!json || json.count == 0){ + self.el_no_charts_enabled.show(); + self.el_refresh_link.show(); + self.el_refreshing_text.hide(); + } + else + { + self.el_no_charts_enabled.hide(); + + json.charts.sort(function(a, b) { + return a.order - b.order; + }); + + Object.each(json.charts, function(chart){ + + var c = new Element('div.chart').grab( + new Element('h3').grab( new Element('a', { + 'text': chart.name, + 'href': chart.url + })) + ); + + var it = 1; + + Object.each(chart.list, function(movie){ + + var m = new Block.Search.MovieItem(movie, { + 'onAdded': function(){ + self.afterAdded(m, movie) + } + }); + var in_database_class = movie.in_wanted ? '.chart_in_wanted' : (movie.in_library ? '.chart_in_library' : ''); + var in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); + m.el.grab( new Element('div.chart_number' + in_database_class, { 'text': it++, 'title': in_database_title })); + m.data_container.grab( + new Element('div.actions').adopt( + new Element('a.add.icon2', { + 'title': 'Add movie with your default quality', + 'data-add': movie.imdb, + 'events': { + 'click': m.showOptions.bind(m) + } + }), + $(new MA.IMDB(m)), + $(new MA.Trailer(m, { + 'height': 150 + })) + ) + ); + m.data_container.removeEvents('click'); + + var plot = false; + if(m.info.plot && m.info.plot.length > 0) + plot = m.info.plot; + + // Add rating + m.info_container.adopt( + m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', { + 'text': parseFloat(m.info.rating.imdb[0]), + 'title': parseInt(m.info.rating.imdb[1]) + ' votes' + }) : null, + m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', { + 'text': m.info.genres.slice(0, 3).join(', ') + }) : null, + m.plot = plot ? new Element('span.plot', { + 'text': plot, + 'events': { + 'click': function(){ + this.toggleClass('full') + } + } + }) : null + ) + + $(m).inject(c); + + }); + + $(c).inject(self.el_refresh_container, 'before'); + + }); + + } + + self.fireEvent('loaded'); + + }, + + afterAdded: function(m, movie){ + var self = this; + + $(m).getElement('div.chart_number').addClass('chart_in_wanted').setProperty('title','Movie in wanted list'); + + + }, + + toElement: function(){ + return this.el; + } + +}) diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py index 5b228653..1a8d981f 100644 --- a/couchpotato/core/media/movie/providers/automation/base.py +++ b/couchpotato/core/media/movie/providers/automation/base.py @@ -13,6 +13,7 @@ log = CPLog(__name__) class Automation(AutomationBase): enabled_option = 'automation_enabled' + chart_enabled_option = 'chart_display_enabled' http_time_between_calls = 2 interval = 1800 @@ -20,6 +21,7 @@ class Automation(AutomationBase): def __init__(self): addEvent('automation.get_movies', self._getMovies) + addEvent('automation.get_chart_list', self._getChartList) def _getMovies(self): @@ -34,6 +36,13 @@ class Automation(AutomationBase): return self.getIMDBids() + def _getChartList(self): + + if not (self.conf(self.chart_enabled_option) or self.conf(self.chart_enabled_option) is None): + return + + return self.getChartList() + def search(self, name, year = None, imdb_only = False): prop_name = 'automation.cached.%s.%s' % (name, year) @@ -94,5 +103,9 @@ class Automation(AutomationBase): def getIMDBids(self): return [] + def getChartList(self): + # Example return: [ {'name': 'Display name of list', 'url': 'http://example.com/', 'order': 1, 'list': []} ] + return + def canCheck(self): return time.time() > self.last_checked + self.interval diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index b0f99f6c..15702b90 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -14,6 +14,8 @@ class Bluray(Automation, RSS): interval = 1800 rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml' backlog_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases&page=%s' + display_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases' + chart_order = 1 def getIMDBids(self): @@ -77,6 +79,32 @@ class Bluray(Automation, RSS): return movies + def getChartList(self): + # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) + movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []} + max_items = int(self.conf('max_items', section='charts', default=5)) + rss_movies = self.getRSSData(self.rss_url) + + for movie in rss_movies: + name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip() + year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip() + + if not name.find('/') == -1: # make sure it is not a double movie release + continue + + movie = self.search(name, year) + + if movie: + movie_list['list'].append( movie ) + if len(movie_list['list']) >= max_items: + break + + if not movie_list['list']: + return + + return [ movie_list ] + + config = [{ 'name': 'bluray', 'groups': [ @@ -101,5 +129,19 @@ config = [{ }, ], }, + { + 'tab': 'display', + 'list': 'charts_providers', + 'name': 'bluray_charts_display', + 'label': 'Blu-ray.com', + 'description': 'Display new releases from Blu-ray.com', + 'options': [ + { + 'name': 'chart_display_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, ], }] diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 10150f2d..2f126bab 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -105,6 +105,16 @@ class IMDBAutomation(IMDBBase): 'top250': 'http://www.imdb.com/chart/top', 'boxoffice': 'http://www.imdb.com/chart/', } + chart_names = { + 'theater': 'IMDB - Movies in Theaters', + 'top250': 'IMDB - Top 250 Movies', + 'boxoffice': 'IMDB - Box Office', + } + chart_order = { + 'theater': 2, + 'top250': 4, + 'boxoffice': 3, + } first_table = ['boxoffice'] @@ -144,6 +154,46 @@ class IMDBAutomation(IMDBBase): return movies + def getChartList(self): + # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) + movie_lists = [] + max_items = int(self.conf('max_items', section='charts', default=5)) + + for url in self.chart_urls: + if self.conf('chart_display_%s' % url): + movie_list = {'name': self.chart_names[url], 'url': self.chart_urls[url], 'order': self.chart_order[url], 'list': []} + data = self.getHTMLData(self.chart_urls[url]) + if data: + html = BeautifulSoup(data) + + try: + result_div = html.find('div', attrs = {'id': 'main'}) + + try: + if url in self.first_table: + table = result_div.find('table') + result_div = table if table else result_div + except: + pass + + imdb_ids = getImdb(str(result_div), multiple = True) + + for imdb_id in imdb_ids[0:max_items]: + info = self.getInfo(imdb_id) + movie_list['list'].append(info) + + if self.shuttingDown(): + break + except: + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) + + if movie_list['list']: + movie_lists.append(movie_list) + + + return movie_lists + + config = [{ 'name': 'imdb', 'groups': [ @@ -206,5 +256,40 @@ config = [{ }, ], }, + { + 'tab': 'display', + 'list': 'charts_providers', + 'name': 'imdb_charts_display', + 'label': 'IMDB', + 'description': 'Display movies from IMDB Charts', + 'options': [ + { + 'name': 'chart_display_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'chart_display_theater', + 'type': 'bool', + 'label': 'In Theaters', + 'description': 'New Movies In-Theaters chart', + 'default': False, + }, + { + 'name': 'chart_display_top250', + 'type': 'bool', + 'label': 'TOP 250', + 'description': 'IMDB TOP 250 chart', + 'default': False, + }, + { + 'name': 'chart_display_boxoffice', + 'type': 'bool', + 'label': 'Box office TOP 10', + 'description': 'IMDB Box office TOP 10 chart', + 'default': True, + }, + ], + }, ], }] diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css index 82f0c21c..a719eb2c 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.css +++ b/couchpotato/core/media/movie/suggestion/static/suggest.css @@ -1,4 +1,6 @@ .suggestions { + clear: both; + padding-top: 10px; } .suggestions > h2 { diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js index a0547e4a..494f0459 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.js +++ b/couchpotato/core/media/movie/suggestion/static/suggest.js @@ -42,11 +42,10 @@ var SuggestList = new Class({ } } - }).grab( - new Element('h2', { - 'text': 'You might like these' - }) - ); + }); + + var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected'); + if( cookie_menu_select === 'suggestions' || cookie_menu_select === null ) self.el.show(); else self.el.hide(); self.api_request = Api.request('suggestion.view', { 'onComplete': self.fill.bind(self) diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 41ad541d..40758261 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -21,7 +21,9 @@ Page.Home = new Class({ self.chain.chain( self.createAvailable.bind(self), self.createSoon.bind(self), + self.createSuggestionsChartsMenu.bind(self), self.createSuggestions.bind(self), + self.createCharts.bind(self), self.createLate.bind(self) ); @@ -151,7 +153,78 @@ Page.Home = new Class({ $(self.suggestion_list).inject(self.el); + }, + createCharts: function(){ + var self = this; + + // Charts + self.charts = new Charts({ + 'onLoaded': function(){ + self.chain.callChain(); + } + }); + + $(self.charts).inject(self.el); + + }, + + createSuggestionsChartsMenu: function(){ + var self = this; + + self.el_toggle_menu_suggestions = new Element('a.toggle_suggestions.active', { + 'href': '#', + 'events': { 'click': function(e) { + e.preventDefault(); + self.toggleSuggestionsCharts('suggestions'); + } + } + }).grab( new Element('h2', {'text': 'Suggestions'})); + + self.el_toggle_menu_charts = new Element('a.toggle_charts', { + 'href': '#', + 'events': { 'click': function(e) { + e.preventDefault(); + self.toggleSuggestionsCharts('charts'); + } + } + }).grab( new Element('h2', {'text': 'Charts'})); + + self.el_toggle_menu = new Element('div.toggle_menu').grab( + self.el_toggle_menu_suggestions + ).grab( + self.el_toggle_menu_charts + ); + + var menu_selected = Cookie.read('suggestions_charts_menu_selected'); + if( menu_selected === null ) menu_selected = 'suggestions'; + self.toggleSuggestionsCharts( menu_selected ); + + self.el_toggle_menu.inject(self.el); + + self.chain.callChain(); + + }, + + toggleSuggestionsCharts: function(menu_id){ + var self = this; + + switch(menu_id) { + case 'suggestions': + if($(self.suggestion_list)) $(self.suggestion_list).show(); + self.el_toggle_menu_suggestions.addClass('active'); + if($(self.charts)) $(self.charts).hide(); + self.el_toggle_menu_charts.removeClass('active'); + break; + case 'charts': + if($(self.charts)) $(self.charts).show(); + self.el_toggle_menu_charts.addClass('active'); + if($(self.suggestion_list)) $(self.suggestion_list).hide(); + self.el_toggle_menu_suggestions.removeClass('active'); + break; + } + + Cookie.write('suggestions_charts_menu_selected', menu_id, {'duration': 365}); }, createLate: function(){ From ff0de896c498a427100bbebdb353f98f5adefa00 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Apr 2014 15:50:20 +0200 Subject: [PATCH 213/468] Cleanup and default charts --- .../core/media/movie/charts/__init__.py | 33 +++++++++++ couchpotato/core/media/movie/charts/main.py | 35 +---------- .../core/media/movie/charts/static/charts.css | 12 ++-- .../core/media/movie/charts/static/charts.js | 58 ++++++++++++------- .../movie/providers/automation/bluray.py | 2 +- .../media/movie/providers/automation/imdb.py | 2 +- 6 files changed, 80 insertions(+), 62 deletions(-) diff --git a/couchpotato/core/media/movie/charts/__init__.py b/couchpotato/core/media/movie/charts/__init__.py index 8b137891..9c673751 100644 --- a/couchpotato/core/media/movie/charts/__init__.py +++ b/couchpotato/core/media/movie/charts/__init__.py @@ -1 +1,34 @@ +from .main import Charts + +def autoload(): + return Charts() + + +config = [{ + 'name': 'charts', + 'groups': [ + { + 'label': 'Charts', + 'description': 'Displays selected charts on the home page', + 'type': 'list', + 'name': 'charts_providers', + 'tab': 'display', + 'options': [ + { + 'name': 'max_items', + 'default': 5, + 'type': 'int', + 'description': 'Maximum number of items displayed from each chart.', + }, + { + 'name': 'update_interval', + 'default': 12, + 'type': 'int', + 'advanced': True, + 'description': '(hours)', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py index 467eecc3..e6c4b043 100644 --- a/couchpotato/core/media/movie/charts/main.py +++ b/couchpotato/core/media/movie/charts/main.py @@ -1,13 +1,13 @@ import time +from couchpotato import tryInt from couchpotato.core.logger import CPLog from couchpotato.api import addApiView from couchpotato.core.event import addEvent,fireEvent from couchpotato.core.plugins.base import Plugin -log = CPLog(__name__) -autoload = 'Charts' +log = CPLog(__name__) class Charts(Plugin): @@ -51,39 +51,10 @@ class Charts(Plugin): try: self.update_in_progress = True charts = fireEvent('automation.get_chart_list', merge = True) - self.setCache('charts_cached', charts, timeout = 2*3600*self.conf('update_interval', default = 12)) + self.setCache('charts_cached', charts, timeout = 7200 * tryInt(self.conf('update_interval', default = 12))) except: log.error('Failed refreshing charts') self.update_in_progress = False return charts - - -config = [{ - 'name': 'charts', - 'groups': [ - { - 'label': 'Charts', - 'description': 'Displays selected charts on the home page', - 'type': 'list', - 'name': 'charts_providers', - 'tab': 'display', - 'options': [ - { - 'name': 'max_items', - 'default': 10, - 'type': 'int', - 'description': 'Maximum number of items displayed from each chart.', - }, - { - 'name': 'update_interval', - 'default': 12, - 'type': 'int', - 'advanced': True, - 'description': '(hours)', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index 0527bda3..da962a45 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -15,7 +15,6 @@ .charts div.refresh { margin-top: 10px; clear:both; - background-color: rgba(255,255,255,0.1); text-align:center; } @@ -169,14 +168,10 @@ color: white; position: absolute; top: 0; - background: rgb(0, 0, 0); /* fallback color */ - background: rgba(0, 0, 0, 0.3); padding: 10px; - border-bottom-left-radius: 6px; - border-top-left-radius: 6px; font: bold 2em/1em Helvetica, Sans-Serif; width: 50px; - height: 60px; + height: 100%; } .charts .chart .media_result div.chart_number.chart_in_wanted { @@ -220,18 +215,19 @@ display: block; width: 50%; float: left; - color: #fff; + color: rgba(255,255,255,.6); border-bottom: 1px solid rgba(255, 255, 255, 0.0666667); - text-align: center; } .toggle_menu a:hover { border-color: #047792; border-width: 4px; + color: #fff; } .toggle_menu a.active { border-bottom: 4px solid #04bce6; + color: #fff; } .toggle_menu a:last-child { diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js index 06cf9c88..5b453691 100644 --- a/couchpotato/core/media/movie/charts/static/charts.js +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -12,25 +12,43 @@ var Charts = new Class({ create: function(){ var self = this; - self.el_refreshing_text = new Element('span.refreshing', {'text': 'Refreshing charts...'}); - self.el_refresh_link = new Element('a.refresh', { 'href': '#', 'text': 'Refresh charts', 'events': { 'click': function(e) { - e.preventDefault(); - self.el.getChildren('div.chart').destroy(); - self.el_refreshing_text.show(); - self.el_refresh_link.hide(); - self.api_request = Api.request('charts.view', { - 'data': { 'force_update': 1 }, - 'onComplete': self.fill.bind(self) - }); - }}}).hide(); - self.el_refresh_container = new Element('div.refresh').grab( self.el_refreshing_text ).grab( self.el_refresh_link ); - self.el_no_charts_enabled = new Element('p.no_charts_enabled', { - 'html':'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to settings and turn on some charts of your choice.' + self.el_refreshing_text = new Element('span.refreshing', { + 'text': 'Refreshing charts...' + }); + + self.el_refresh_link = new Element('a.refresh', { + 'href': '#', + 'text': 'Refresh charts', + 'events': { + 'click': function(e) { + e.preventDefault(); + self.el.getChildren('div.chart').destroy(); + self.el_refreshing_text.show(); + self.el_refresh_link.hide(); + self.api_request = Api.request('charts.view', { + 'data': { 'force_update': 1 }, + 'onComplete': self.fill.bind(self) + }); + } + } }).hide(); - self.el = new Element('div.charts').grab(self.el_no_charts_enabled).grab( self.el_refresh_container ); + self.el_refresh_container = new Element('div.refresh').grab( + self.el_refreshing_text + ).grab(self.el_refresh_link); - if( Cookie.read('suggestions_charts_menu_selected') === 'charts') self.el.show(); else self.el.hide(); + self.el_no_charts_enabled = new Element('p.no_charts_enabled', { + 'html': 'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to settings and turn on some charts of your choice.' + }).hide(); + + self.el = new Element('div.charts').grab( + self.el_no_charts_enabled + ).grab(self.el_refresh_container); + + if( Cookie.read('suggestions_charts_menu_selected') === 'charts') + self.el.show(); + else + self.el.hide(); self.api_request = Api.request('charts.view', { 'onComplete': self.fill.bind(self) @@ -50,8 +68,7 @@ var Charts = new Class({ self.el_refresh_link.show(); self.el_refreshing_text.hide(); } - else - { + else { self.el_no_charts_enabled.hide(); json.charts.sort(function(a, b) { @@ -136,8 +153,9 @@ var Charts = new Class({ afterAdded: function(m, movie){ var self = this; - $(m).getElement('div.chart_number').addClass('chart_in_wanted').setProperty('title','Movie in wanted list'); - + $(m).getElement('div.chart_number') + .addClass('chart_in_wanted') + .set('title', 'Movie in wanted list'); }, diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index 15702b90..55114fd3 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -138,7 +138,7 @@ config = [{ 'options': [ { 'name': 'chart_display_enabled', - 'default': False, + 'default': True, 'type': 'enabler', }, ], diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 2f126bab..0f2e2940 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -265,7 +265,7 @@ config = [{ 'options': [ { 'name': 'chart_display_enabled', - 'default': False, + 'default': True, 'type': 'enabler', }, { From cbb0462948f0994eeb30475926447b21414dbfd3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Apr 2014 17:10:57 +0200 Subject: [PATCH 214/468] Only list inactive downloadstatus support once --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index b04c96c6..3830b97d 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -871,7 +871,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else [] if len(no_status_support) > 0: - log.debug('Download status functionality is not implemented for one of the active downloaders: %s', no_status_support) + log.debug('Download status functionality is not implemented for one of the active downloaders: %s', list(set(no_status_support))) if not release_downloads: if fire_scan: From e547851905a109699e3b27b194eb940ab8831bff Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Apr 2014 17:17:26 +0200 Subject: [PATCH 215/468] Failed deleting from wanted --- couchpotato/core/media/_base/media/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index d452839b..62e8a167 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -382,7 +382,7 @@ class MediaPlugin(MediaBase): db.delete(release) total_deleted += 1 - if total_releases == total_deleted and media['status'] != 'active': + if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active'): db.delete(media) deleted = True elif new_media_status: From 3f16dbd09c58570b4ee89f00ddcdc1c91e76f607 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Apr 2014 17:52:33 +0200 Subject: [PATCH 216/468] Sort releases based on preferred method in api return --- couchpotato/core/plugins/release/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 437e736c..72a0a502 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -446,7 +446,14 @@ class Release(Plugin): raw_releases = list(db.get_many('release', media_id, with_doc = True)) releases = [] - for r in sorted(raw_releases, key = lambda k: k['doc'].get('info', {}).get('score', 0), reverse = True): + for r in raw_releases: releases.append(r['doc']) + releases = sorted(releases, key = lambda k: k.get('info', {}).get('score', 0), reverse = True) + + # Sort based on preferred search method + download_preference = self.conf('preferred_method', section = 'searcher') + if download_preference != 'both': + releases = sorted(releases, key = lambda k: k.get('info', {}).get('protocol', '')[:3], reverse = (download_preference == 'torrent')) + return releases From c89c99b2724d524f8911f21fd2c8e8e36c4e1e90 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Apr 2014 19:06:20 +0200 Subject: [PATCH 217/468] Don't refresh charts at startup --- couchpotato/core/media/movie/charts/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py index e6c4b043..71002752 100644 --- a/couchpotato/core/media/movie/charts/main.py +++ b/couchpotato/core/media/movie/charts/main.py @@ -20,8 +20,6 @@ class Charts(Plugin): def setCrons(self): fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.conf('update_interval', default = 12)) - self.updateViewCache() - def automationView(self, force_update = False, **kwargs): From 98a85f6950a5911be8594963d4acccae16767f2a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 09:54:24 +0200 Subject: [PATCH 218/468] Charts cleanup --- .../core/media/movie/charts/static/charts.css | 53 ++++++++---- .../core/media/movie/charts/static/charts.js | 84 ++++++++++--------- .../media/movie/suggestion/static/suggest.css | 1 + couchpotato/static/scripts/page/home.js | 2 +- 4 files changed, 83 insertions(+), 57 deletions(-) diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index da962a45..610ac153 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -1,5 +1,6 @@ .charts { clear: both; + margin-bottom: 30px; } .charts > h2 { @@ -12,18 +13,37 @@ vertical-align: top; } -.charts div.refresh { - margin-top: 10px; +.charts .refresh { clear:both; - text-align:center; + position: relative; } + .charts .refresh .refreshing { + display: block; + padding: 20px; + font-size: 20px; + text-align:center; + } + + .charts .refresh a { + text-align: center; + padding: 0; + display: none; + width: 30px; + height: 30px; + position: absolute; + right: 10px; + top: -40px; + opacity: .7; + } + + .charts .refresh a:hover { + opacity: 1; + } + .charts p.no_charts_enabled { padding: 0.7em 1em; -} - -.charts div.refresh a { - display:block; + display: none; } .charts .chart h3 a { @@ -164,7 +184,7 @@ left: 50px; } -.charts .chart .media_result div.chart_number { +.charts .chart .media_result .chart_number { color: white; position: absolute; top: 0; @@ -172,16 +192,17 @@ font: bold 2em/1em Helvetica, Sans-Serif; width: 50px; height: 100%; + text-align: center; + border-left: 8px solid transparent; } -.charts .chart .media_result div.chart_number.chart_in_wanted { - background: rgb(0, 255, 40); /* fallback color */ - background: rgba(0, 255, 40, 0.3); -} -.charts .chart .media_result div.chart_number.chart_in_library { - background: rgb(0, 202, 32); /* fallback color */ - background: rgba(0, 202, 32, 0.3); -} + .charts .chart .media_result.chart_in_wanted .chart_number { + border-color: rgba(0, 255, 40, 0.3); + } + + .charts .chart .media_result.chart_in_library .chart_number { + border-color: rgba(0, 202, 32, 0.3); + } .charts .chart .media_result .actions { diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js index 5b453691..00033f4a 100644 --- a/couchpotato/core/media/movie/charts/static/charts.js +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -12,38 +12,31 @@ var Charts = new Class({ create: function(){ var self = this; - self.el_refreshing_text = new Element('span.refreshing', { - 'text': 'Refreshing charts...' - }); - - self.el_refresh_link = new Element('a.refresh', { - 'href': '#', - 'text': 'Refresh charts', - 'events': { - 'click': function(e) { - e.preventDefault(); - self.el.getChildren('div.chart').destroy(); - self.el_refreshing_text.show(); - self.el_refresh_link.hide(); - self.api_request = Api.request('charts.view', { - 'data': { 'force_update': 1 }, - 'onComplete': self.fill.bind(self) - }); - } - } - }).hide(); - - self.el_refresh_container = new Element('div.refresh').grab( - self.el_refreshing_text - ).grab(self.el_refresh_link); - - self.el_no_charts_enabled = new Element('p.no_charts_enabled', { - 'html': 'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to settings and turn on some charts of your choice.' - }).hide(); - - self.el = new Element('div.charts').grab( - self.el_no_charts_enabled - ).grab(self.el_refresh_container); + self.el = new Element('div.charts').adopt( + self.el_no_charts_enabled = new Element('p.no_charts_enabled', { + 'html': 'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to settings and turn on some charts of your choice.' + }), + self.el_refresh_container = new Element('div.refresh').adopt( + self.el_refresh_link = new Element('a.refresh.icon2', { + 'href': '#', + 'events': { + 'click': function(e) { + e.preventDefault(); + self.el.getChildren('div.chart').destroy(); + self.el_refreshing_text.show(); + self.el_refresh_link.hide(); + self.api_request = Api.request('charts.view', { + 'data': { 'force_update': 1 }, + 'onComplete': self.fill.bind(self) + }); + } + } + }), + self.el_refreshing_text = new Element('span.refreshing', { + 'text': 'Refreshing charts...' + }) + ) + ); if( Cookie.read('suggestions_charts_menu_selected') === 'charts') self.el.show(); @@ -54,6 +47,8 @@ var Charts = new Class({ 'onComplete': self.fill.bind(self) }); + self.fireEvent.delay(0, self, 'created'); + }, fill: function(json){ @@ -93,9 +88,19 @@ var Charts = new Class({ self.afterAdded(m, movie) } }); - var in_database_class = movie.in_wanted ? '.chart_in_wanted' : (movie.in_library ? '.chart_in_library' : ''); - var in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); - m.el.grab( new Element('div.chart_number' + in_database_class, { 'text': it++, 'title': in_database_title })); + + var in_database_class = movie.in_wanted ? 'chart_in_wanted' : (movie.in_library ? 'chart_in_library' : ''), + in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); + + m.el + .addClass(in_database_class) + .grab( + new Element('div.chart_number', { + 'text': it++, + 'title': in_database_title + }) + ); + m.data_container.grab( new Element('div.actions').adopt( new Element('a.add.icon2', { @@ -134,13 +139,13 @@ var Charts = new Class({ } } }) : null - ) + ); $(m).inject(c); }); - $(c).inject(self.el_refresh_container, 'before'); + c.inject(self.el); }); @@ -150,8 +155,7 @@ var Charts = new Class({ }, - afterAdded: function(m, movie){ - var self = this; + afterAdded: function(m){ $(m).getElement('div.chart_number') .addClass('chart_in_wanted') @@ -163,4 +167,4 @@ var Charts = new Class({ return this.el; } -}) +}); diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css index a719eb2c..8e747849 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.css +++ b/couchpotato/core/media/movie/suggestion/static/suggest.css @@ -1,6 +1,7 @@ .suggestions { clear: both; padding-top: 10px; + margin-bottom: 30px; } .suggestions > h2 { diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 40758261..bf435c14 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -160,7 +160,7 @@ Page.Home = new Class({ // Charts self.charts = new Charts({ - 'onLoaded': function(){ + 'onCreated': function(){ self.chain.callChain(); } }); From 84a2afe08f467125542e455b562f38237e29bfd9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 11:30:23 +0200 Subject: [PATCH 219/468] Refactor downloaders and pages --- couchpotato/core/_base/clientscript.py | 2 -- couchpotato/core/_base/downloader/__init__.py | 20 +++++++++++++++++++ .../base.py => _base/downloader/main.py} | 8 +++++++- .../_base/downloader/static}/downloaders.js | 4 ++-- couchpotato/core/downloaders/__init__.py | 13 ------------ couchpotato/core/downloaders/blackhole.py | 4 ++-- couchpotato/core/downloaders/deluge.py | 4 ++-- couchpotato/core/downloaders/nzbget.py | 4 ++-- couchpotato/core/downloaders/nzbvortex.py | 4 ++-- couchpotato/core/downloaders/pneumatic.py | 4 ++-- couchpotato/core/downloaders/qbittorrent_.py | 8 ++++---- couchpotato/core/downloaders/rtorrent_.py | 6 +++--- couchpotato/core/downloaders/sabnzbd.py | 4 ++-- couchpotato/core/downloaders/synology.py | 4 ++-- couchpotato/core/downloaders/transmission.py | 4 ++-- couchpotato/core/downloaders/utorrent.py | 4 ++-- .../media/movie/_base/static/1_wanted.js} | 0 .../media/movie/_base/static/2_manage.js} | 0 .../notifications/core/static/notification.js | 2 +- .../notifications/twitter/static/twitter.js | 2 +- .../core/plugins/category/static/category.js | 2 +- .../core/plugins/quality/static/quality.js | 2 +- .../plugins/userscript/static/userscript.js | 2 +- couchpotato/static/scripts/couchpotato.js | 2 ++ couchpotato/static/scripts/page/about.js | 4 ++-- 25 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 couchpotato/core/_base/downloader/__init__.py rename couchpotato/core/{downloaders/base.py => _base/downloader/main.py} (97%) rename couchpotato/{static/scripts/misc => core/_base/downloader/static}/downloaders.js (94%) rename couchpotato/{static/scripts/page/wanted.js => core/media/movie/_base/static/1_wanted.js} (100%) rename couchpotato/{static/scripts/page/manage.js => core/media/movie/_base/static/2_manage.js} (100%) diff --git a/couchpotato/core/_base/clientscript.py b/couchpotato/core/_base/clientscript.py index 603c57dc..ee29578c 100644 --- a/couchpotato/core/_base/clientscript.py +++ b/couchpotato/core/_base/clientscript.py @@ -49,10 +49,8 @@ class ClientScript(Plugin): 'scripts/block/footer.js', 'scripts/block/menu.js', 'scripts/page/home.js', - 'scripts/page/wanted.js', 'scripts/page/settings.js', 'scripts/page/about.js', - 'scripts/page/manage.js', 'scripts/misc/downloaders.js', ], } diff --git a/couchpotato/core/_base/downloader/__init__.py b/couchpotato/core/_base/downloader/__init__.py new file mode 100644 index 00000000..0b9201bd --- /dev/null +++ b/couchpotato/core/_base/downloader/__init__.py @@ -0,0 +1,20 @@ +from .main import Downloader + + +def autoload(): + return Downloader() + + +config = [{ + 'name': 'download_providers', + 'groups': [ + { + 'label': 'Downloaders', + 'description': 'You can select different downloaders for each type (usenet / torrent)', + 'type': 'list', + 'name': 'download_providers', + 'tab': 'downloaders', + 'options': [], + }, + ], +}] diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/_base/downloader/main.py similarity index 97% rename from couchpotato/core/downloaders/base.py rename to couchpotato/core/_base/downloader/main.py index dfbff9ff..7db788a2 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/_base/downloader/main.py @@ -7,12 +7,18 @@ from couchpotato.core.event import addEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.base import Provider +from couchpotato.core.plugins.base import Plugin log = CPLog(__name__) -class Downloader(Provider): +## This is here to load the static files +class Downloader(Plugin): + pass + + +class DownloaderBase(Provider): protocol = [] http_time_between_calls = 0 diff --git a/couchpotato/static/scripts/misc/downloaders.js b/couchpotato/core/_base/downloader/static/downloaders.js similarity index 94% rename from couchpotato/static/scripts/misc/downloaders.js rename to couchpotato/core/_base/downloader/static/downloaders.js index c3611a2f..ec85ce3d 100644 --- a/couchpotato/static/scripts/misc/downloaders.js +++ b/couchpotato/core/_base/downloader/static/downloaders.js @@ -6,7 +6,7 @@ var DownloadersBase = new Class({ var self = this; // Add test buttons to settings page - App.addEvent('load', self.addTestButtons.bind(self)); + App.addEvent('loadSettings', self.addTestButtons.bind(self)); }, @@ -72,4 +72,4 @@ var DownloadersBase = new Class({ }); -window.Downloaders = new DownloadersBase(); +var Downloaders = new DownloadersBase(); diff --git a/couchpotato/core/downloaders/__init__.py b/couchpotato/core/downloaders/__init__.py index a81ce881..e69de29b 100644 --- a/couchpotato/core/downloaders/__init__.py +++ b/couchpotato/core/downloaders/__init__.py @@ -1,13 +0,0 @@ -config = [{ - 'name': 'download_providers', - 'groups': [ - { - 'label': 'Downloaders', - 'description': 'You can select different downloaders for each type (usenet / torrent)', - 'type': 'list', - 'name': 'download_providers', - 'tab': 'downloaders', - 'options': [], - }, - ], -}] diff --git a/couchpotato/core/downloaders/blackhole.py b/couchpotato/core/downloaders/blackhole.py index d77e7464..262776a8 100644 --- a/couchpotato/core/downloaders/blackhole.py +++ b/couchpotato/core/downloaders/blackhole.py @@ -2,7 +2,7 @@ from __future__ import with_statement import os import traceback -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core._base.downloader.main import DownloaderBase from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getDownloadDir from couchpotato.core.logger import CPLog @@ -14,7 +14,7 @@ log = CPLog(__name__) autoload = 'Blackhole' -class Blackhole(Downloader): +class Blackhole(DownloaderBase): protocol = ['nzb', 'torrent', 'torrent_magnet'] status_support = False diff --git a/couchpotato/core/downloaders/deluge.py b/couchpotato/core/downloaders/deluge.py index 26592ca7..0c49233d 100644 --- a/couchpotato/core/downloaders/deluge.py +++ b/couchpotato/core/downloaders/deluge.py @@ -6,7 +6,7 @@ import re import traceback from bencode import bencode as benc, bdecode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.variable import tryFloat, cleanHost from couchpotato.core.logger import CPLog @@ -18,7 +18,7 @@ log = CPLog(__name__) autoload = 'Deluge' -class Deluge(Downloader): +class Deluge(DownloaderBase): protocol = ['torrent', 'torrent_magnet'] log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/nzbget.py b/couchpotato/core/downloaders/nzbget.py index cfa75985..b46de778 100644 --- a/couchpotato/core/downloaders/nzbget.py +++ b/couchpotato/core/downloaders/nzbget.py @@ -6,7 +6,7 @@ import socket import traceback import xmlrpclib -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import ss, sp from couchpotato.core.helpers.variable import tryInt, md5, cleanHost from couchpotato.core.logger import CPLog @@ -17,7 +17,7 @@ log = CPLog(__name__) autoload = 'NZBGet' -class NZBGet(Downloader): +class NZBGet(DownloaderBase): protocol = ['nzb'] rpc = 'xmlrpc' diff --git a/couchpotato/core/downloaders/nzbvortex.py b/couchpotato/core/downloaders/nzbvortex.py index 573e2592..9094055f 100644 --- a/couchpotato/core/downloaders/nzbvortex.py +++ b/couchpotato/core/downloaders/nzbvortex.py @@ -12,7 +12,7 @@ import time import traceback import urllib2 -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import tryUrlencode, sp from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog @@ -23,7 +23,7 @@ log = CPLog(__name__) autoload = 'NZBVortex' -class NZBVortex(Downloader): +class NZBVortex(DownloaderBase): protocol = ['nzb'] api_level = None diff --git a/couchpotato/core/downloaders/pneumatic.py b/couchpotato/core/downloaders/pneumatic.py index 7500b18f..8cf1aebb 100644 --- a/couchpotato/core/downloaders/pneumatic.py +++ b/couchpotato/core/downloaders/pneumatic.py @@ -2,7 +2,7 @@ from __future__ import with_statement import os import traceback -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core._base.downloader.main import DownloaderBase from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog @@ -12,7 +12,7 @@ log = CPLog(__name__) autoload = 'Pneumatic' -class Pneumatic(Downloader): +class Pneumatic(DownloaderBase): protocol = ['nzb'] strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' diff --git a/couchpotato/core/downloaders/qbittorrent_.py b/couchpotato/core/downloaders/qbittorrent_.py index 0f88beba..d4bfced1 100644 --- a/couchpotato/core/downloaders/qbittorrent_.py +++ b/couchpotato/core/downloaders/qbittorrent_.py @@ -1,21 +1,21 @@ from base64 import b16encode, b32decode -from bencode import bencode, bdecode from hashlib import sha1 import os -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from bencode import bencode, bdecode +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog - from qbittorrent.client import QBittorrentClient + log = CPLog(__name__) autoload = 'qBittorrent' -class qBittorrent(Downloader): +class qBittorrent(DownloaderBase): protocol = ['torrent', 'torrent_magnet'] qb = None diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index ab3ee9a6..cccecb90 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -4,14 +4,14 @@ from hashlib import sha1 from urlparse import urlparse import os -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList + from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.logger import CPLog from bencode import bencode, bdecode from rtorrent import RTorrent -from rtorrent.err import MethodError from scandir import scandir @@ -20,7 +20,7 @@ log = CPLog(__name__) autoload = 'rTorrent' -class rTorrent(Downloader): +class rTorrent(DownloaderBase): protocol = ['torrent', 'torrent_magnet'] rt = None diff --git a/couchpotato/core/downloaders/sabnzbd.py b/couchpotato/core/downloaders/sabnzbd.py index 548889fa..cd51cb87 100644 --- a/couchpotato/core/downloaders/sabnzbd.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -4,7 +4,7 @@ import json import os import traceback -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.logger import CPLog @@ -16,7 +16,7 @@ log = CPLog(__name__) autoload = 'Sabnzbd' -class Sabnzbd(Downloader): +class Sabnzbd(DownloaderBase): protocol = ['nzb'] diff --git a/couchpotato/core/downloaders/synology.py b/couchpotato/core/downloaders/synology.py index f7145dea..125d750a 100644 --- a/couchpotato/core/downloaders/synology.py +++ b/couchpotato/core/downloaders/synology.py @@ -1,7 +1,7 @@ import json import traceback -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core._base.downloader.main import DownloaderBase from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog @@ -13,7 +13,7 @@ log = CPLog(__name__) autoload = 'Synology' -class Synology(Downloader): +class Synology(DownloaderBase): protocol = ['nzb', 'torrent', 'torrent_magnet'] status_support = False diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index 5c25dcf4..49c8bc58 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -6,7 +6,7 @@ import os.path import re import urllib2 -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost from couchpotato.core.logger import CPLog @@ -17,7 +17,7 @@ log = CPLog(__name__) autoload = 'Transmission' -class Transmission(Downloader): +class Transmission(DownloaderBase): protocol = ['torrent', 'torrent_magnet'] log = CPLog(__name__) diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index 83df48ea..08b8d5a5 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -12,7 +12,7 @@ import urllib import urllib2 from bencode import bencode as benc, bdecode -from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList +from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, ss, sp from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost from couchpotato.core.logger import CPLog @@ -24,7 +24,7 @@ log = CPLog(__name__) autoload = 'uTorrent' -class uTorrent(Downloader): +class uTorrent(DownloaderBase): protocol = ['torrent', 'torrent_magnet'] utorrent_api = None diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/core/media/movie/_base/static/1_wanted.js similarity index 100% rename from couchpotato/static/scripts/page/wanted.js rename to couchpotato/core/media/movie/_base/static/1_wanted.js diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/core/media/movie/_base/static/2_manage.js similarity index 100% rename from couchpotato/static/scripts/page/manage.js rename to couchpotato/core/media/movie/_base/static/2_manage.js diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index c999fc77..81bf9500 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -14,7 +14,7 @@ var NotificationBase = new Class({ App.on('message', self.showMessage.bind(self)); // Add test buttons to settings page - App.addEvent('load', self.addTestButtons.bind(self)); + App.addEvent('loadSettings', self.addTestButtons.bind(self)); // Notification bar self.notifications = []; diff --git a/couchpotato/core/notifications/twitter/static/twitter.js b/couchpotato/core/notifications/twitter/static/twitter.js index c817fe03..75c96a8e 100644 --- a/couchpotato/core/notifications/twitter/static/twitter.js +++ b/couchpotato/core/notifications/twitter/static/twitter.js @@ -2,7 +2,7 @@ var TwitterNotification = new Class({ initialize: function(){ var self = this; - App.addEvent('load', self.addRegisterButton.bind(self)); + App.addEvent('loadSettings', self.addRegisterButton.bind(self)); }, addRegisterButton: function(){ diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js index b3234912..6d160be1 100644 --- a/couchpotato/core/plugins/category/static/category.js +++ b/couchpotato/core/plugins/category/static/category.js @@ -3,7 +3,7 @@ var CategoryListBase = new Class({ initialize: function(){ var self = this; - App.addEvent('load', self.addSettings.bind(self)); + App.addEvent('loadSettings', self.addSettings.bind(self)); }, setup: function(categories){ diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index d103908b..285ed1d9 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -11,7 +11,7 @@ var QualityBase = new Class({ self.profiles = []; Array.each(data.profiles, self.createProfilesClass.bind(self)); - App.addEvent('load', self.addSettings.bind(self)) + App.addEvent('loadSettings', self.addSettings.bind(self)) }, diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 3a7fd9dd..2ef1ea6a 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -53,7 +53,7 @@ var UserscriptSettingTab = new Class({ initialize: function(){ var self = this; - App.addEvent('load', self.addSettings.bind(self)) + App.addEvent('loadSettings', self.addSettings.bind(self)) }, diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 27ae9c09..ffc0e9aa 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -141,6 +141,8 @@ var pg = new Page[class_name](self, {}); self.pages[class_name] = pg; + self.fireEvent('load'+class_name); + $(pg).inject(self.content); }); diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index b4326ae5..a2482f8b 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -6,7 +6,7 @@ var AboutSettingTab = new Class({ initialize: function(){ var self = this; - App.addEvent('load', self.addSettings.bind(self)) + App.addEvent('loadSettings', self.addSettings.bind(self)) }, @@ -124,4 +124,4 @@ var AboutSettingTab = new Class({ window.addEvent('domready', function(){ new AboutSettingTab(); -}); \ No newline at end of file +}); From eba36b6d577fb9fc010c8dee9be1f9802d946160 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 11:50:12 +0200 Subject: [PATCH 220/468] Allow type option in listing --- couchpotato/core/media/movie/_base/static/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index 1e8a38f4..91207505 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -552,7 +552,7 @@ var MovieList = new Class({ Api.request(self.options.api_call || 'media.list', { 'data': Object.merge({ - 'type': 'movie', + 'type': self.options.type || 'movie', 'status': self.options.status, 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null }, self.filter), From 7faa7c3dba1adb3bf4a312130e8c8142dc699328 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 12:48:10 +0200 Subject: [PATCH 221/468] Use correct super class --- couchpotato/core/_base/downloader/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/_base/downloader/main.py b/couchpotato/core/_base/downloader/main.py index 7db788a2..80890e4a 100644 --- a/couchpotato/core/_base/downloader/main.py +++ b/couchpotato/core/_base/downloader/main.py @@ -168,7 +168,7 @@ class DownloaderBase(Provider): if not data: data = {} d_manual = self.conf('manual', default = False) - return super(Downloader, self).isEnabled() and \ + return super(DownloaderBase, self).isEnabled() and \ (d_manual and manual or d_manual is False) and \ (not data or self.isCorrectProtocol(data.get('protocol'))) From ae42b62b3c755943e49b831653acd42ac7663f09 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 16:39:13 +0200 Subject: [PATCH 222/468] Remove downloaders.js from clientscript --- couchpotato/core/_base/clientscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/_base/clientscript.py b/couchpotato/core/_base/clientscript.py index ee29578c..e5dbd8f7 100644 --- a/couchpotato/core/_base/clientscript.py +++ b/couchpotato/core/_base/clientscript.py @@ -51,7 +51,6 @@ class ClientScript(Plugin): 'scripts/page/home.js', 'scripts/page/settings.js', 'scripts/page/about.js', - 'scripts/misc/downloaders.js', ], } From 685210aee3c6188dced1fb091433bcb3779124f6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 5 Apr 2014 20:37:24 +0200 Subject: [PATCH 223/468] Nested media index --- couchpotato/core/media/_base/media/index.py | 17 +++++++++++++++++ couchpotato/core/media/_base/media/main.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index b3cd82fc..d0b52c38 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -159,3 +159,20 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" break return str(title[0] if title and len(title) > 0 and title[0] in ascii_letters else '#').lower() + + + +class MediaChildrenIndex(TreeBasedIndex): + _version = 1 + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(MediaChildrenIndex, self).__init__(*args, **kwargs) + + def make_key(self, key): + return key + + def make_key_value(self, data): + if data.get('_t') == 'media' and data.get('parent_id'): + return data.get('parent_id'), None + diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 62e8a167..29a98fd1 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -8,7 +8,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex +from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex log = CPLog(__name__) @@ -23,6 +23,7 @@ class MediaPlugin(MediaBase): 'media_by_type': MediaTypeIndex, 'media_title': TitleIndex, 'media_startswith': StartsWithIndex, + 'media_children': MediaChildrenIndex, } def __init__(self): From f1ea8fa693eaebd0383b70bbee5f98128184e900 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 5 Apr 2014 21:13:28 +1300 Subject: [PATCH 224/468] [rtorrent] fixed how torrent status is determined --- couchpotato/core/downloaders/rtorrent_.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index cccecb90..86c09a40 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -154,19 +154,13 @@ class rTorrent(DownloaderBase): return False def getTorrentStatus(self, torrent): - if torrent.hashing or torrent.hash_checking or torrent.message: - return 'busy' - if not torrent.complete: return 'busy' - if not torrent.open: - return 'completed' - - if torrent.state and torrent.active: + if torrent.open: return 'seeding' - return 'busy' + return 'completed' def getAllDownloadStatus(self, ids): log.debug('Checking rTorrent download status.') From 349d7d48661028bff4ab05d0ddb817aab9b7e369 Mon Sep 17 00:00:00 2001 From: softcat Date: Tue, 8 Apr 2014 13:44:03 +0200 Subject: [PATCH 225/468] Added filmstarts.de userscript --- .../movie/providers/userscript/filmstarts.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 couchpotato/core/media/movie/providers/userscript/filmstarts.py diff --git a/couchpotato/core/media/movie/providers/userscript/filmstarts.py b/couchpotato/core/media/movie/providers/userscript/filmstarts.py new file mode 100644 index 00000000..1e35be27 --- /dev/null +++ b/couchpotato/core/media/movie/providers/userscript/filmstarts.py @@ -0,0 +1,29 @@ +from BeautifulSoup import BeautifulSoup +from couchpotato.core.media._base.providers.userscript.base import UserscriptBase + +autoload = 'Filmstarts' + +class Filmstarts(UserscriptBase): + + includes = ['*://www.filmstarts.de/kritiken/*'] + + def getMovie(self, url): + try: + data = self.getUrl(url) + except: + return + + html = BeautifulSoup(data) + table = html.find("table", attrs={"class": "table table-standard thead-standard table-striped_2 fs11"}) + + if table.find(text='Originaltitel'): + # Get original film title from the table specified above + name = table.find("div", text="Originaltitel").parent.parent.parent.td.text + else: + # If none is available get the title from the meta data + name = html.find("meta", {"property":"og:title"})['content'] + + # Year of production is not available in the meta data, so get it from the table + year = table.find("tr", text="Produktionsjahr").parent.parent.parent.td.text + + return self.search(name, year) \ No newline at end of file From 95ce26d261fba970b1f7cb51a859729ea5c53000 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 5 Apr 2014 00:52:16 +0200 Subject: [PATCH 226/468] Use more standardized codec/source names Fixes #999 --- couchpotato/core/plugins/scanner.py | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 6b7c7b0f..1f636adf 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -60,29 +60,29 @@ class Scanner(Plugin): } codecs = { - 'audio': ['dts', 'ac3', 'ac3d', 'mp3'], - 'video': ['x264', 'h264', 'divx', 'xvid'] + 'audio': ['DTS', 'AC3', 'AC3D', 'MP3'], + 'video': ['x264', 'H264', 'DivX', 'Xvid'] } audio_codec_map = { - 0x2000: 'ac3', - 0x2001: 'dts', - 0x0055: 'mp3', - 0x0050: 'mp2', - 0x0001: 'pcm', - 0x003: 'pcm', - 0x77a1: 'tta1', - 0x5756: 'wav', - 0x6750: 'vorbis', - 0xF1AC: 'flac', - 0x00ff: 'aac', + 0x2000: 'AC3', + 0x2001: 'DTS', + 0x0055: 'MP3', + 0x0050: 'MP2', + 0x0001: 'PCM', + 0x003: 'WAV', + 0x77a1: 'TTA1', + 0x5756: 'WAV', + 0x6750: 'Vorbis', + 0xF1AC: 'FLAC', + 0x00ff: 'AAC', } source_media = { - 'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'], - 'hddvd': ['hddvd', 'hd-dvd'], - 'dvd': ['dvd'], - 'hdtv': ['hdtv'] + 'Blu-ray': ['bluray', 'blu-ray', 'brrip', 'br-rip'], + 'HD DVD': ['hddvd', 'hd-dvd'], + 'DVD': ['dvd'], + 'HDTV': ['hdtv'] } clean = '[ _\,\.\(\)\[\]\-]?(3d|hsbs|sbs|extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ @@ -471,7 +471,7 @@ class Scanner(Plugin): p = enzyme.parse(filename) # Video codec - vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower() + vc = ('H264' if p.video[0].codec == 'AVC1' else p.video[0].codec) # Audio codec ac = p.audio[0].codec From a4779738622898e0078e22e113be68f76d755d55 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 13 Apr 2014 15:47:26 +0200 Subject: [PATCH 227/468] Provider fixes Fixes #3097 #3086 #3106 --- couchpotato/core/media/_base/providers/base.py | 2 +- .../core/media/_base/providers/torrent/kickasstorrents.py | 4 ++-- .../core/media/_base/providers/torrent/thepiratebay.py | 8 ++++++-- .../core/media/movie/providers/torrent/publichd.py | 2 +- .../core/media/movie/providers/torrent/thepiratebay.py | 2 +- .../core/media/movie/providers/torrent/torrentshack.py | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 4476bc49..2b668931 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -200,7 +200,7 @@ class YarrProvider(Provider): self._search(media, quality, results) # Search possible titles else: - media_title = fireEvent('library.query', media, single = True) + media_title = fireEvent('library.query', media, include_year = False, single = True) for title in possibleTitles(media_title): self._searchOnTitle(title, media, quality, results) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 534dc926..f040b9ae 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -65,8 +65,8 @@ class Base(TorrentMagnetProvider): if column_name: if column_name == 'name': - link = td.find('div', {'class': 'torrentname'}).find_all('a')[1] - new['id'] = temp.get('id')[-8:] + link = td.find('div', {'class': 'torrentname'}).find_all('a')[2] + new['id'] = temp.get('id')[-7:] new['name'] = link.text new['url'] = td.find('a', 'imagnet')['href'] new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:]) diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index d401e6b3..b840fc71 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -65,7 +65,7 @@ class Base(TorrentMagnetProvider): pass entries = results_table.find_all('tr') - for result in entries[2:]: + for result in entries[1:]: link = result.find(href = re.compile('torrent\/\d+\/')) download = result.find(href = re.compile('magnet:')) @@ -109,7 +109,11 @@ class Base(TorrentMagnetProvider): full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) html = BeautifulSoup(full_description) nfo_pre = html.find('div', attrs = {'class': 'nfo'}) - description = toUnicode(nfo_pre.text) if nfo_pre else '' + description = '' + try: + description = toUnicode(nfo_pre.text) + except: + pass item['description'] = description return item diff --git a/couchpotato/core/media/movie/providers/torrent/publichd.py b/couchpotato/core/media/movie/providers/torrent/publichd.py index ce54c468..9fed4619 100644 --- a/couchpotato/core/media/movie/providers/torrent/publichd.py +++ b/couchpotato/core/media/movie/providers/torrent/publichd.py @@ -11,4 +11,4 @@ autoload = 'PublicHD' class PublicHD(MovieProvider, Base): def buildUrl(self, media): - return fireEvent('library.query', media, single = True) + return fireEvent('library.query', media, single = True).replace(':', '') diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py index f804d709..0dc8313d 100644 --- a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py @@ -13,7 +13,7 @@ class ThePirateBay(MovieProvider, Base): cat_ids = [ ([209], ['3d']), - ([207], ['720p', '1080p']), + ([207], ['720p', '1080p', 'bd50']), ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([201, 207], ['brrip']), ([202], ['dvdr']) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack.py b/couchpotato/core/media/movie/providers/torrent/torrentshack.py index 85e420dd..01eb6d6a 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentshack.py @@ -31,6 +31,6 @@ class TorrentShack(MovieProvider, Base): def buildUrl(self, media, quality): query = (tryUrlencode(fireEvent('library.query', media, single = True)), - self.getCatId(quality)[0], - self.getSceneOnly()) + self.getSceneOnly(), + self.getCatId(quality)[0]) return query From 8739c1197f6973db8ebbed3d99a544c86cda0d6c Mon Sep 17 00:00:00 2001 From: Jonnathan Date: Sun, 13 Apr 2014 19:49:06 -0300 Subject: [PATCH 228/468] Don't show duplicated results in the blu-ray releases chart. --- couchpotato/core/media/movie/providers/automation/bluray.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index 55114fd3..630d0cbb 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -82,6 +82,7 @@ class Bluray(Automation, RSS): def getChartList(self): # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []} + movie_ids = [] max_items = int(self.conf('max_items', section='charts', default=5)) rss_movies = self.getRSSData(self.rss_url) @@ -95,6 +96,9 @@ class Bluray(Automation, RSS): movie = self.search(name, year) if movie: + if movie.get('imdb') in movie_ids: + continue + movie_ids.append(movie.get('imdb')) movie_list['list'].append( movie ) if len(movie_list['list']) >= max_items: break From d22237a5ccda1c75220d7b563b9eddc073c655cd Mon Sep 17 00:00:00 2001 From: Steven Lu Date: Sun, 8 Dec 2013 01:04:04 -0500 Subject: [PATCH 229/468] Adding in a new source for automation. --- .../automation/popular_movies/__init__.py | 24 +++++++++++++++++++ .../automation/popular_movies/main.py | 22 +++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 couchpotato/core/providers/automation/popular_movies/__init__.py create mode 100644 couchpotato/core/providers/automation/popular_movies/main.py diff --git a/couchpotato/core/providers/automation/popular_movies/__init__.py b/couchpotato/core/providers/automation/popular_movies/__init__.py new file mode 100644 index 00000000..e2eace30 --- /dev/null +++ b/couchpotato/core/providers/automation/popular_movies/__init__.py @@ -0,0 +1,24 @@ +from .main import PopularMovies + +def start(): + return PopularMovies() + +config = [{ + 'name': 'popular_movies', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'popular_movies_automation', + 'label': 'Popular Movies', + 'description': 'Imports the top titles of movies that have been in theaters.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/automation/popular_movies/main.py b/couchpotato/core/providers/automation/popular_movies/main.py new file mode 100644 index 00000000..6c56cf5a --- /dev/null +++ b/couchpotato/core/providers/automation/popular_movies/main.py @@ -0,0 +1,22 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.automation.base import Automation +import datetime + +log = CPLog(__name__) + + +class PopularMovies(Automation): + + interval = 1800 + url = 'https://s3.amazonaws.com/popular-movies/movies.json' + + def getIMDBids(self): + + movies = [] + retrieved_movies = self.getJsonData(self.url) + + for movie in retrieved_movies.get('movies'): + imdb_id = movie.get('imdb_id') + movies.append(imdb_id) + + return movies From aa37f2b0ef169fdb44d2fc0d0bbce2331ad8c69e Mon Sep 17 00:00:00 2001 From: mano3m Date: Tue, 8 Apr 2014 22:23:16 +0200 Subject: [PATCH 230/468] Add password searching in spots from spotweb --- .../core/media/_base/providers/nzb/newznab.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 4430ac26..31223103 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -2,6 +2,7 @@ from urllib2 import HTTPError from urlparse import urlparse import time import traceback +import re import urllib2 from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode @@ -20,10 +21,11 @@ log = CPLog(__name__) class Base(NZBProvider, RSS): urls = { - 'detail': 'details&id=%s', + 'detail': 'details/%s', 'download': 't=get&id=%s' } + passwords_regex = 'password|wachtwoord' limits_reached = {} http_time_between_calls = 1 # Seconds @@ -44,9 +46,7 @@ class Base(NZBProvider, RSS): def _searchOnHost(self, host, media, quality, results): query = self.buildUrl(media, host['api_key']) - url = '%s&%s' % (self.getUrl(host['host']), query) - nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) for nzb in nzbs: @@ -79,6 +79,23 @@ class Base(NZBProvider, RSS): if spotter: name_extra = spotter + description = '' + if "@spot.net" in nzb_id: + try: + # Get details for extended description to retrieve passwords + query = self.buildDetailsUrl(nzb_id, host['api_key']) + url = '%s&%s' % (self.getUrl(host['host']), query) + nzb_details = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})[0] + + description = self.getTextElement(nzb_details, 'description') + + # Extract a password from the description + password = re.search('(?:' + self.passwords_regex + ')(?: *)(?:\:|\=)(?: *)(.*?)\|\n|$', description, flags = re.I).group(1) + if password: + name = name + ' {{%s}}' % password.strip() + except: + log.debug('Error getting details of "%s": %s', (name, traceback.format_exc())) + results.append({ 'id': nzb_id, 'provider_extra': urlparse(host['host']).hostname or host['host'], @@ -87,8 +104,9 @@ class Base(NZBProvider, RSS): 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, 'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), - 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), + 'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id), 'content': self.getTextElement(nzb, 'description'), + 'description': description, 'score': host['extra_score'], }) @@ -191,6 +209,15 @@ class Base(NZBProvider, RSS): return 'try_next' + def buildDetailsUrl(self, nzb_id, api_key): + query = tryUrlencode({ + 't': 'details', + 'id': nzb_id, + 'apikey': api_key, + }) + return query + + config = [{ 'name': 'newznab', From 9b75e6af5c201ce6538aa486726c296710af36d6 Mon Sep 17 00:00:00 2001 From: mano3m Date: Tue, 15 Apr 2014 21:34:31 +0200 Subject: [PATCH 231/468] Only tag existing files Fixes #3088 --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 3830b97d..25057c6a 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -652,7 +652,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif isinstance(release_download, dict): # Tag download_files if they are known if release_download.get('files', []): - tag_files = release_download.get('files', []) + tag_files = [filename for filename in release_download.get('files', []) if os.path.exists(filename)] # Tag all files in release folder elif release_download['folder']: From b59a0f82ab631159919668568a05f3908ca3c288 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 19 Apr 2014 21:12:38 +0200 Subject: [PATCH 232/468] Add IMDB rentals list to charts This should add the IMDB rentals list to the charts and imdb automation. This is actually a nice list as you can download the movies right away instead of waiting until they release like with the rest of the imdb charts. The problem is that this does not work. And frankly I gave up. When I type this in my python command window it works: ''' from bs4 import BeautifulSoup import urllib2 data = urllib2.urlopen('http://www.imdb.com/boxoffice/rentals') html = BeautifulSoup(data) result_div = html.find('div', attrs = {'id': 'main'}) ''' Then result_div contains the list of movies. In the code from this PR result_div becomes None....?!?!?! @Ruudburger please help before I jump off my building ;) --- .../media/movie/providers/automation/imdb.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 0f2e2940..c0c2de88 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -104,16 +104,19 @@ class IMDBAutomation(IMDBBase): 'theater': 'http://www.imdb.com/movies-in-theaters/', 'top250': 'http://www.imdb.com/chart/top', 'boxoffice': 'http://www.imdb.com/chart/', + 'rentals': 'http://www.imdb.com/boxoffice/rentals', } chart_names = { 'theater': 'IMDB - Movies in Theaters', 'top250': 'IMDB - Top 250 Movies', 'boxoffice': 'IMDB - Box Office', + 'rentals': 'IMDB - Top DVD rentals', } chart_order = { 'theater': 2, - 'top250': 4, + 'top250': 5, 'boxoffice': 3, + 'rentals': 4, } first_table = ['boxoffice'] @@ -240,6 +243,13 @@ config = [{ 'description': 'New Movies In-Theaters chart', 'default': True, }, + { + 'name': 'automation_charts_rentals', + 'type': 'bool', + 'label': 'DVD Rentals', + 'description': 'Top DVD rentals chart', + 'default': True, + }, { 'name': 'automation_charts_top250', 'type': 'bool', @@ -282,6 +292,13 @@ config = [{ 'description': 'IMDB TOP 250 chart', 'default': False, }, + { + 'name': 'chart_display_rentals', + 'type': 'bool', + 'label': 'DVD Rentals', + 'description': 'Top DVD rentals chart', + 'default': True, + }, { 'name': 'chart_display_boxoffice', 'type': 'bool', From ab0f5daaf3878c88e87b088bed0f564ee0363ccf Mon Sep 17 00:00:00 2001 From: mano3m Date: Fri, 18 Apr 2014 21:24:40 +0200 Subject: [PATCH 233/468] Simplify binsearch result string fixes #3099 --- couchpotato/core/media/_base/providers/nzb/binsearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index 90778af8..fbb67bfc 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -2,7 +2,7 @@ import re import traceback from bs4 import BeautifulSoup -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, simplifyString from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.nzb.base import NZBProvider @@ -65,7 +65,7 @@ class Base(NZBProvider): results.append({ 'id': nzb_id, - 'name': title.text, + 'name': simplifyString(title.text), 'age': tryInt(age), 'size': self.parseSize(size_match.group('size')), 'url': self.urls['download'] % nzb_id, From 0eac041a26eed14f59215e09b8f563ab819a4ad5 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 19 Apr 2014 22:11:28 +0200 Subject: [PATCH 234/468] Add 'A' and 'An' to 'The' prefix This was bothering me for a long time now ;) We do put The at the end but not A nor An. Fixed now :) --- couchpotato/core/media/_base/media/index.py | 8 ++++---- couchpotato/core/media/movie/_base/static/movie.js | 4 ++++ couchpotato/core/plugins/renamer.py | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index d0b52c38..fd1affca 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -99,7 +99,7 @@ from couchpotato.core.helpers.encoding import simplifyString""" class TitleIndex(TreeBasedIndex): - _version = 2 + _version = 3 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -123,7 +123,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" nr_prefix = '' if title and len(title) > 0 and title[0] in ascii_letters else '#' title = simplifyString(title) - for prefix in ['the ']: + for prefix in ['the ', 'an ', 'a ']: if prefix == title[:len(prefix)]: title = title[len(prefix):] break @@ -132,7 +132,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" class StartsWithIndex(TreeBasedIndex): - _version = 2 + _version = 3 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -153,7 +153,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" title = toUnicode(title) title = simplifyString(title) - for prefix in ['the ']: + for prefix in ['the ', 'an ', 'a ']: if prefix == title[:len(prefix)]: title = title[len(prefix):] break diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 25d5e08f..a251e79d 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -250,6 +250,10 @@ var Movie = new Class({ getUnprefixedTitle: function(t){ if(t.substr(0, 4).toLowerCase() == 'the ') t = t.substr(4) + ', The'; + if(t.substr(0, 3).toLowerCase() == 'an ') + t = t.substr(3) + ', An'; + if(t.substr(0, 2).toLowerCase() == 'a ') + t = t.substr(2) + ', A'; return t; }, diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 3830b97d..1fd4fed6 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -290,8 +290,9 @@ class Renamer(Plugin): # Put 'The' at the end name_the = movie_name - if movie_name[:4].lower() == 'the ': - name_the = movie_name[4:] + ', The' + for prefix in ['the ', 'an ', 'a ']: + if prefix == movie_name[:len(prefix)].lower(): + name_the = movie_name[len(prefix):] + ', ' + prefix.strip().capitalize() replacements = { 'ext': 'mkv', From 72383592ba98dc77ff07d5436762137443cd1d5f Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 20 Apr 2014 10:18:44 +0200 Subject: [PATCH 235/468] Clean-up --- couchpotato/core/media/movie/_base/static/movie.js | 4 ++-- couchpotato/core/plugins/renamer.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index a251e79d..86205efc 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -250,9 +250,9 @@ var Movie = new Class({ getUnprefixedTitle: function(t){ if(t.substr(0, 4).toLowerCase() == 'the ') t = t.substr(4) + ', The'; - if(t.substr(0, 3).toLowerCase() == 'an ') + else if(t.substr(0, 3).toLowerCase() == 'an ') t = t.substr(3) + ', An'; - if(t.substr(0, 2).toLowerCase() == 'a ') + else if(t.substr(0, 2).toLowerCase() == 'a ') t = t.substr(2) + ', A'; return t; }, diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 1fd4fed6..5f2d4024 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -292,7 +292,8 @@ class Renamer(Plugin): name_the = movie_name for prefix in ['the ', 'an ', 'a ']: if prefix == movie_name[:len(prefix)].lower(): - name_the = movie_name[len(prefix):] + ', ' + prefix.strip().capitalize() + name_the = movie_name[len(prefix):] + ', ' + prefix.strip().capitalize() + break replacements = { 'ext': 'mkv', From b01aa2b38540534002253fcd859f192e145ea6de Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 20 Apr 2014 22:53:26 +0200 Subject: [PATCH 236/468] Add verified only option for kat Fixes ##3137 --- .../_base/providers/torrent/kickasstorrents.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index f040b9ae..352871a4 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -70,7 +70,8 @@ class Base(TorrentMagnetProvider): new['name'] = link.text new['url'] = td.find('a', 'imagnet')['href'] new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:]) - new['score'] = 20 if td.find('a', 'iverif') else 0 + new['verified'] = 1 if td.find('a', 'iverify') else 0 + new['score'] = 100 if td.find('a', 'iverify') else 0 elif column_name is 'size': new['size'] = self.parseSize(td.text) elif column_name is 'age': @@ -82,6 +83,10 @@ class Base(TorrentMagnetProvider): nr += 1 + # Only store verified torrents + if self.conf('only_verified') and not new['verified']: + continue + results.append(new) except: log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc()) @@ -151,6 +156,13 @@ config = [{ 'default': 40, 'description': 'Will not be (re)moved until this seed time (in hours) is met.', }, + { + 'name': 'only_verified', + 'advanced': True, + 'type': 'bool', + 'default': 1, + 'description': 'Only search for verified releases.' + }, { 'name': 'extra_score', 'advanced': True, From 0c2e65c92b2f03cc313dc38ed1e1038a2309bc1b Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 20 Apr 2014 09:16:59 +0200 Subject: [PATCH 237/468] Check for better quality Actually check the quality profile order and determine: - if the searcher needs to search for a certain quality - if the renamer needs to rename a certain qualoty release Fixes #3122 --- couchpotato/core/media/movie/searcher.py | 67 ++++++++++++------------ couchpotato/core/plugins/quality/main.py | 40 ++++++++++++++ couchpotato/core/plugins/renamer.py | 26 +++++---- 3 files changed, 86 insertions(+), 47 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 68a76ade..c41307fa 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -139,8 +139,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): db = get_db() profile = db.get('id', movie['profile_id']) - quality_order = fireEvent('quality.order', single = True) - ret = False index = 0 @@ -162,43 +160,46 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # See if better quality is available for release in movie.get('releases', []): - if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: - has_better_quality += 1 + if release['status'] not in ['available', 'ignored', 'failed']: + is_higher = fireEvent('quality.ishigher', \ + {'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \ + {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \ + profile, single = True) + if is_higher != 'higher': + has_better_quality += 1 # Don't search for quality lower then already available. - if has_better_quality is 0: - - quality = fireEvent('quality.single', identifier = q_identifier, single = True) - log.info('Search for %s in %s', (default_title, quality['label'])) - - # Extend quality with profile customs - quality['custom'] = quality_custom - - results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] - if len(results) == 0: - log.debug('Nothing found for %s in %s', (default_title, quality['label'])) - - # Check if movie isn't deleted while searching - if not fireEvent('media.get', movie.get('_id'), single = True): - break - - # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True) - - # Try find a valid result and download it - if fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): - ret = True - - # Remove releases that aren't found anymore - for release in movie.get('releases', []): - if release.get('status') == 'available' and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('_id'), single = True) - - else: + if has_better_quality > 0: log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title)) fireEvent('media.restatus', movie['_id']) break + quality = fireEvent('quality.single', identifier = q_identifier, single = True) + log.info('Search for %s in %s', (default_title, quality['label'])) + + # Extend quality with profile customs + quality['custom'] = quality_custom + + results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] + if len(results) == 0: + log.debug('Nothing found for %s in %s', (default_title, quality['label'])) + + # Check if movie isn't deleted while searching + if not fireEvent('media.get', movie.get('_id'), single = True): + break + + # Add them to this movie releases list + found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True) + + # Try find a valid result and download it + if fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): + ret = True + + # Remove releases that aren't found anymore + for release in movie.get('releases', []): + if release.get('status') == 'available' and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('_id'), single = True) + # Break if CP wants to shut down if self.shuttingDown() or ret: break diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index c27815ed..63ed2de1 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -49,6 +49,8 @@ class QualityPlugin(Plugin): addEvent('quality.guess', self.guess) addEvent('quality.pre_releases', self.preReleases) addEvent('quality.order', self.getOrder) + addEvent('quality.ishigher', self.isHigher) + addEvent('quality.isfinish', self.isFinish) addApiView('quality.size.save', self.saveSize) addApiView('quality.list', self.allView, docs = { @@ -329,6 +331,44 @@ class QualityPlugin(Plugin): for allow in quality.get('allow', []): score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 + def isFinish(self, quality, profile): + if not isinstance(profile, dict) or not profile.get('qualities'): + return False + + try: + quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] + return profile['finish'][quality_order] + except: + return False + + def isHigher(self, quality, compare_with, profile = None): + if not isinstance(profile, dict) or not profile.get('qualities'): + profile = {'qualities': self.order} + + # Try to find quality in profile, if not found: a quality we do not want is lower than anything else + try: + quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] + except: + log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \ + [identifier + ('3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) + return 'lower' + + # Try to find compare quality in profile, if not found: anything is higher than a not wanted quality + try: + compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0] + except: + log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \ + [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) + return 'higher' + + # Note to self: a lower number means higher quality + if quality_order > compare_order: + return 'lower' + elif quality_order == compare_order: + return 'equal' + else: + return 'higher' + def doTest(self): tests = { diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 5424874b..7825ff0a 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -443,14 +443,11 @@ class Renamer(Plugin): try: if media.get('status') == 'active' and media.get('profile_id'): profile = db.get('id', media['profile_id']) - if group['meta_data']['quality']['identifier'] in profile.get('qualities', []): - nr = profile['qualities'].index(group['meta_data']['quality']['identifier']) - finish = profile['finish'][nr] - if finish: - mdia = db.get('id', media['_id']) - mdia['status'] = 'done' - mdia['last_edit'] = int(time.time()) - db.update(mdia) + if fireEvent('release.isfinish', group['meta_data']['quality'], profile): + mdia = db.get('id', media['_id']) + mdia['status'] = 'done' + mdia['last_edit'] = int(time.time()) + db.update(mdia) except Exception as e: log.error('Failed marking movie finished: %s', (traceback.format_exc())) @@ -461,18 +458,19 @@ class Renamer(Plugin): # When a release already exists if release.get('status') == 'done': - release_order = quality_order.index(release['quality']) - group_quality_order = quality_order.index(group['meta_data']['quality']['identifier']) + # This is where CP removes older, lesser quality releases or releases that are not wanted anymore + is_higher = fireEvent('quality.ishigher', \ + group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, profile, single = True) - # This is where CP removes older, lesser quality releases - if release_order > group_quality_order: - log.info('Removing lesser quality %s for %s.', (media_title, release.get('quality'))) + if is_higher == 'higher': + log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality'))) for file_type in release.get('files', {}): for release_file in release['files'][file_type]: remove_files.append(release_file) remove_releases.append(release) + # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc - elif release_order == group_quality_order: + elif is_higher == 'equal': log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (media_title, release.get('quality'))) for file_type in release.get('files', {}): for release_file in release['files'][file_type]: From ce4806df64c13f1f8bf0bade131a368e6c698180 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 29 Mar 2014 21:57:17 +0100 Subject: [PATCH 238/468] Add 3D renamer option --- couchpotato/core/plugins/release/main.py | 3 ++- couchpotato/core/plugins/renamer.py | 7 +++++-- couchpotato/core/plugins/scanner.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 72a0a502..518cbce2 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -107,6 +107,7 @@ class Release(Plugin): 'media_id': media['_id'], 'identifier': release_identifier, 'quality': group['meta_data']['quality'].get('identifier'), + 'is_3d': group['meta_data']['quality'].get('is_3d', 0), 'last_edit': int(time.time()), 'status': 'done' } @@ -406,7 +407,7 @@ class Release(Plugin): rel = db.get('id', release_id) if rel and rel.get('status') != status: - release_name = rel.get('name') + release_name = rel['info'].get('name') if rel.get('files'): for file_type in rel.get('files', {}): if file_type == 'movie': diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 5424874b..d9bb4ab2 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -112,7 +112,7 @@ class Renamer(Plugin): return if not base_folder: - base_folder = self.conf('from') + base_folder = sp(self.conf('from')) from_folder = sp(self.conf('from')) to_folder = sp(self.conf('to')) @@ -315,6 +315,7 @@ class Renamer(Plugin): 'cd_nr': '', 'mpaa': media['info'].get('mpaa', ''), 'category': category_label, + '3d': '3D' if group['meta_data']['quality'].get('is_3d', 0) else '', } for file_type in group['files']: @@ -824,7 +825,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) def replaceDoubles(self, string): replaces = [ - ('\.+', '.'), ('_+', '_'), ('-+', '-'), ('\s+', ' '), + ('\.+', '.'), ('_+', '_'), ('-+', '-'), ('\s+', ' '), (' \\\\', '\\\\'), (' /', '/'), ('(\s\.)+', '.'), ('(-\.)+', '.'), ('(\s-)+', '-'), ] @@ -1056,6 +1057,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) release_download.update({ 'imdb_id': getIdentifier(media), 'quality': rls['quality'], + 'is_3d': rls['is_3d'], 'protocol': rls.get('info', {}).get('protocol') or rls.get('info', {}).get('type'), 'release_id': rls['_id'], }) @@ -1195,6 +1197,7 @@ rename_options = { 'first': 'First letter (M)', 'quality': 'Quality (720p)', 'quality_type': '(HD) or (SD)', + '3d': '3D', 'video': 'Video (x264)', 'audio': 'Audio (DTS)', 'group': 'Releasegroup name', diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 1f636adf..64d61072 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -450,6 +450,7 @@ class Scanner(Plugin): data['quality'] = None if release_download and release_download.get('quality'): data['quality'] = fireEvent('quality.single', release_download.get('quality'), single = True) + data['quality']['is_3d'] = release_download.get('is_3d', False) if not data['quality']: data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) From 442552c024d01ec31330c98bb4133c9f07d862f7 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 29 Mar 2014 22:06:15 +0100 Subject: [PATCH 239/468] fix debug msg --- couchpotato/core/plugins/quality/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index c27815ed..1ae39704 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -281,7 +281,7 @@ class QualityPlugin(Plugin): return 1, key if list(set([key]) & set(words)): - log.debug('Found %s in %s', (tag, cur_file)) + log.debug('Found %s in %s', (key, cur_file)) return 1, key return 0, None From e51ddd7a50b95d23fe5b4c3d9975c1355b5edc26 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 29 Mar 2014 23:52:16 +0100 Subject: [PATCH 240/468] BR-Disk detection fixes --- couchpotato/core/media/_base/searcher/main.py | 5 ++++- couchpotato/core/plugins/quality/main.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index fc07ded2..9a01e356 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -111,7 +111,10 @@ class Searcher(SearcherBase): # Hack for older movies that don't contain quality tag year_name = fireEvent('scanner.name_year', name, single = True) if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): - if size > 3000: # Assume dvdr + if size > 20000: # Assume bd50 + log.info('Quality was missing in name, assuming it\'s a BR-Disk based on the size: %s', size) + found['bd50'] = True + elif size > 3000: # Assume dvdr log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size) found['dvdr'] = True else: # Assume dvdrip diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 1ae39704..1970aa36 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -21,7 +21,7 @@ class QualityPlugin(Plugin): } qualities = [ - {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, + {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, @@ -213,7 +213,7 @@ class QualityPlugin(Plugin): # Return nothing if all scores are 0 has_non_zero = 0 for s in score: - if score[s] > 0: + if score[s]['score'] > 0: has_non_zero += 1 if not has_non_zero: @@ -344,6 +344,7 @@ class QualityPlugin(Plugin): 'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': '720p', 'Movie.Name.2008.German.DL.AC3.1080p.BluRay.x264-Group': '1080p', 'Movie.Name.2004.GERMAN.AC3D.DL.1080p.BluRay.x264-Group': '1080p', + 'Movie.Name.2013.BR-Disk-Group.iso': 'bd50', } correct = 0 From bfe8aa5f5f93ccc7711e05b2ba7033021b51bd4e Mon Sep 17 00:00:00 2001 From: mano3m Date: Mon, 31 Mar 2014 21:36:39 +0200 Subject: [PATCH 241/468] Add size to quality guessing And cleanup searcher --- couchpotato/core/media/_base/searcher/main.py | 30 +++++-------------- couchpotato/core/plugins/quality/main.py | 20 ++++++++++++- couchpotato/core/plugins/scanner.py | 12 ++++++-- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 9a01e356..a9a368a0 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -87,28 +87,17 @@ class Searcher(SearcherBase): def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None): if not preferred_quality: preferred_quality = {} - name = nzb['name'] - size = nzb.get('size', 0) - nzb_words = re.split('\W+', simplifyString(name)) - - qualities = fireEvent('quality.all', single = True) - - found = {} - for quality in qualities: - # Main in words - if quality['identifier'] in nzb_words: - found[quality['identifier']] = True - - # Alt in words - if list(set(nzb_words) & set(quality['alternative'])): - found[quality['identifier']] = True - # Try guessing via quality tags - guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + guess = fireEvent('quality.guess', [nzb.get('name')], nzb.get('size', None), single = True) if guess: - found[guess['identifier']] = True + return not guess['identifier'] == preferred_quality.get('identifier') + # Hack for older movies that don't contain quality tag + name = nzb['name'] + size = nzb.get('size', 0) + + found = {} year_name = fireEvent('scanner.name_year', name, single = True) if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): if size > 20000: # Assume bd50 @@ -121,11 +110,6 @@ class Searcher(SearcherBase): log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size) found['dvdrip'] = True - # Allow other qualities - for allowed in preferred_quality.get('allow'): - if found.get(allowed): - del found[allowed] - return not (found.get(preferred_quality['identifier']) and len(found) == 1) def correct3D(self, nzb, preferred_quality = None): diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 1970aa36..9c56d0ce 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -177,7 +177,7 @@ class QualityPlugin(Plugin): return False - def guess(self, files, extra = None): + def guess(self, files, extra = None, size = None): if not extra: extra = {} # Create hash for cache @@ -205,6 +205,11 @@ class QualityPlugin(Plugin): self.calcScore(score, quality, contains_score, threedscore) + # Evaluate score based on size + for quality in qualities: + size_score = self.guessSizeScore(quality, size = size) + self.calcScore(score, quality, size_score) + # Try again with loose testing for quality in qualities: loose_score = self.guessLooseScore(quality, extra = extra) @@ -308,6 +313,19 @@ class QualityPlugin(Plugin): return score + + def guessSizeScore(self, quality, size = None): + + score = 0 + + if size: + + if tryInt(size) >= tryInt(quality['size_min']) and tryInt(size) < tryInt(quality['size_max']): + log.info2('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) + score += 5 + + return score + def calcScore(self, score, quality, add_score, threedscore = (0, None)): score[quality['identifier']]['score'] += add_score diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 64d61072..86ab1b42 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -164,7 +164,7 @@ class Scanner(Plugin): identifiers = [identifier] # Identifier with quality - quality = fireEvent('quality.guess', [file_path], single = True) if not is_dvd_file else {'identifier':'dvdr'} + quality = fireEvent('quality.guess', files = [file_path], size = self.getFileSize(file_path), single = True) if not is_dvd_file else {'identifier':'dvdr'} if quality: identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', '')) identifiers = [identifier_with_quality, identifier] @@ -450,7 +450,7 @@ class Scanner(Plugin): data['quality'] = None if release_download and release_download.get('quality'): data['quality'] = fireEvent('quality.single', release_download.get('quality'), single = True) - data['quality']['is_3d'] = release_download.get('is_3d', False) + data['quality']['is_3d'] = release_download.get('is_3d', 0) if not data['quality']: data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) @@ -709,12 +709,18 @@ class Scanner(Plugin): if not file_size: file_size = [] try: - return (file_size.get('min', 0) * 1048576) < os.path.getsize(file) < (file_size.get('max', 100000) * 1048576) + return file_size.get('min', 0) < self.getFileSize(file) < file_size.get('max', 100000) except: log.error('Couldn\'t get filesize of %s.', file) return False + def getFileSize(self, file): + try: + return os.path.getsize(file) / 1024 / 1024 + except: + return None + def createStringIdentifier(self, file_path, folder = '', exclude_filename = False): year = self.findYear(file_path) From 06e9afbe69d430f849f269006461e25a1b9cebe9 Mon Sep 17 00:00:00 2001 From: mano3m Date: Thu, 3 Apr 2014 03:20:04 +0200 Subject: [PATCH 242/468] Improve quality self test --- couchpotato/core/media/_base/searcher/main.py | 2 +- couchpotato/core/plugins/quality/main.py | 52 +++++++++++-------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index a9a368a0..8f99ceae 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -88,7 +88,7 @@ class Searcher(SearcherBase): if not preferred_quality: preferred_quality = {} # Try guessing via quality tags - guess = fireEvent('quality.guess', [nzb.get('name')], nzb.get('size', None), single = True) + guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), single = True) if guess: return not guess['identifier'] == preferred_quality.get('identifier') diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 9c56d0ce..cd59c6c9 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -24,8 +24,8 @@ class QualityPlugin(Plugin): {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, - {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, - {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]}, + {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, + {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, @@ -61,7 +61,7 @@ class QualityPlugin(Plugin): addEvent('app.initialize', self.fill, priority = 10) - addEvent('app.test', self.doTest) + addEvent('app.load', self.doTest) self.order = [] self.addOrder() @@ -208,14 +208,14 @@ class QualityPlugin(Plugin): # Evaluate score based on size for quality in qualities: size_score = self.guessSizeScore(quality, size = size) - self.calcScore(score, quality, size_score) + self.calcScore(score, quality, size_score, penalty = False) # Try again with loose testing for quality in qualities: loose_score = self.guessLooseScore(quality, extra = extra) - self.calcScore(score, quality, loose_score) + self.calcScore(score, quality, loose_score, penalty = False) - # Return nothing if all scores are 0 + # Return nothing if all scores are <= 0 has_non_zero = 0 for s in score: if score[s]['score'] > 0: @@ -320,13 +320,15 @@ class QualityPlugin(Plugin): if size: - if tryInt(size) >= tryInt(quality['size_min']) and tryInt(size) < tryInt(quality['size_max']): + if tryInt(quality['size_min']) <= tryInt(size) <= tryInt(quality['size_max']): log.info2('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) score += 5 + else: + score -= 40 return score - def calcScore(self, score, quality, add_score, threedscore = (0, None)): + def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = True): score[quality['identifier']]['score'] += add_score @@ -343,31 +345,35 @@ class QualityPlugin(Plugin): for q in self.qualities: self.cached_order[q.get('identifier')] = self.qualities.index(q) - if add_score != 0: + if penalty and add_score != 0: for allow in quality.get('allow', []): score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 def doTest(self): tests = { - 'Movie Name (1999)-DVD-Rip.avi': 'dvdrip', - 'Movie Name 1999 720p Bluray.mkv': '720p', - 'Movie Name 1999 BR-Rip 720p.avi': 'brrip', - 'Movie Name 1999 720p Web Rip.avi': 'scr', - 'Movie Name 1999 Web DL.avi': 'brrip', - 'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr', - 'Movie.Name.1999.DVDRip-Group': 'dvdrip', - 'Movie.Name.1999.DVD-Rip-Group': 'dvdrip', - 'Movie.Name.1999.DVD-R-Group': 'dvdr', - 'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': '720p', - 'Movie.Name.2008.German.DL.AC3.1080p.BluRay.x264-Group': '1080p', - 'Movie.Name.2004.GERMAN.AC3D.DL.1080p.BluRay.x264-Group': '1080p', - 'Movie.Name.2013.BR-Disk-Group.iso': 'bd50', + 'Movie Name (1999)-DVD-Rip.avi': {'size': 700, 'quality': 'dvdrip'}, + 'Movie Name 1999 720p Bluray.mkv': {'size': 4200, 'quality': '720p'}, + 'Movie Name 1999 BR-Rip 720p.avi': {'size': 1000, 'quality': 'brrip'}, + 'Movie Name 1999 720p Web Rip.avi': {'size': 1200, 'quality': 'scr'}, + 'Movie Name 1999 Web DL.avi': {'size': 800, 'quality': 'brrip'}, + 'Movie.Name.1999.1080p.WEBRip.H264-Group': {'size': 1500, 'quality': 'scr'}, + 'Movie.Name.1999.DVDRip-Group': {'size': 750, 'quality': 'dvdrip'}, + 'Movie.Name.1999.DVD-Rip-Group': {'size': 700, 'quality': 'dvdrip'}, + 'Movie.Name.1999.DVD-R-Group': {'size': 4500, 'quality': 'dvdr'}, + 'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': {'size': 5500, 'quality': '720p'}, + 'Movie.Name.2008.German.DL.AC3.1080p.BluRay.x264-Group': {'size': 8500, 'extra': {'resolution_width': 1920, 'resolution_height': 1080} , 'quality': '1080p'}, + 'Movie.Name.2004.GERMAN.AC3D.DL.1080p.BluRay.x264-Group': {'size': 8000, 'quality': '1080p'}, + 'Movie.Name.2013.BR-Disk-Group.iso': {'size': 48000, 'quality': 'bd50'}, + 'Movie.Name.2013.2D+3D.BR-Disk-Group.iso': {'size': 52000, 'quality': 'bd50', 'is_3d': True}, + 'Girl.Rising.2013.NTSC.DVD9-0MNiDVD': {'size': 7200, 'quality': 'dvdr'}, + 'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True} } correct = 0 for name in tests: - success = self.guess([name]).get('identifier') == tests[name] + test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {} + success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False) if not success: log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier'))) From 47e187449d02f0bee46ef33e232f3f5641356e82 Mon Sep 17 00:00:00 2001 From: mano3m Date: Tue, 15 Apr 2014 20:47:35 +0200 Subject: [PATCH 243/468] Add use of size to scanner And check if snatched quality is the same as what we detected --- couchpotato/core/plugins/scanner.py | 35 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 86ab1b42..903b72f8 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -431,29 +431,36 @@ class Scanner(Plugin): for cur_file in files: if not self.filesizeBetween(cur_file, self.file_sizes['movie']): continue # Ignore smaller files - meta = self.getMeta(cur_file) + if not data.get('audio'): # Only get metadata from first media file + meta = self.getMeta(cur_file) - try: - data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video'])) - data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio'])) - data['resolution_width'] = meta.get('resolution_width', 720) - data['resolution_height'] = meta.get('resolution_height', 480) - data['audio_channels'] = meta.get('audio_channels', 2.0) - data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2) - except: - log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) - pass + try: + data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video'])) + data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio'])) + data['resolution_width'] = meta.get('resolution_width', 720) + data['resolution_height'] = meta.get('resolution_height', 480) + data['audio_channels'] = meta.get('audio_channels', 2.0) + data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2) + except: + log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) + pass - if data.get('audio'): break + data['size'] = data.get('size', 0) + self.getFileSize(cur_file) - # Use the quality guess first, if that failes use the quality we wanted to download data['quality'] = None + quality = fireEvent('quality.guess', size = data['size'], files = files, extra = data, single = True) + + # Use the quality that we snatched but check if it matches our guess if release_download and release_download.get('quality'): data['quality'] = fireEvent('quality.single', release_download.get('quality'), single = True) data['quality']['is_3d'] = release_download.get('is_3d', 0) + if data['quality']['identifier'] != quality['identifier']: + log.info('Different quality snatched than detected for %s: %s vs. %s. Assuming snatched quality is correct.', (files[0], data['quality']['identifier'], quality['identifier'])) + if data['quality']['is_3d'] != quality['is_3d']: + log.info('Different 3d snatched than detected for %s: %s vs. %s. Assuming snatched 3d is correct.', (files[0], data['quality']['is_3d'], quality['is_3d'])) if not data['quality']: - data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) + data['quality'] = quality if not data['quality']: data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) From 037e77860b4524e704f7fc737262483b45a1322d Mon Sep 17 00:00:00 2001 From: mano3m Date: Tue, 15 Apr 2014 21:07:37 +0200 Subject: [PATCH 244/468] Add 3D type to renamer (e.g. SBS, Half OU, etc) --- couchpotato/core/plugins/renamer.py | 2 ++ couchpotato/core/plugins/scanner.py | 30 +++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index d9bb4ab2..cc6f5789 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -316,6 +316,7 @@ class Renamer(Plugin): 'mpaa': media['info'].get('mpaa', ''), 'category': category_label, '3d': '3D' if group['meta_data']['quality'].get('is_3d', 0) else '', + '3d_type': group['meta_data'].get('3d_type'), } for file_type in group['files']: @@ -1198,6 +1199,7 @@ rename_options = { 'quality': 'Quality (720p)', 'quality_type': '(HD) or (SD)', '3d': '3D', + '3d_type': '3D Type (Full SBS)', 'video': 'Video (x264)', 'audio': 'Audio (DTS)', 'group': 'Releasegroup name', diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 903b72f8..5f69b0ed 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -6,7 +6,7 @@ import traceback from couchpotato import get_db from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp +from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp, ss from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \ splitString, getIdentifier from couchpotato.core.logger import CPLog @@ -40,6 +40,17 @@ class Scanner(Plugin): 'trailer': ['mov', 'mp4', 'flv'] } + threed_types = { + 'Half SBS': [('half', 'sbs'), ('h', 'sbs'), 'hsbs'], + 'Full SBS': [('full', 'sbs'), ('f', 'sbs'), 'fsbs'], + 'SBS': 'sbs', + 'Half OU': [('half', 'ou'), ('h', 'ou'), 'hou'], + 'Full OU': [('full', 'ou'), ('h', 'ou'), 'fou'], + 'OU': 'ou', + 'Frame Packed': ['mvc', ('complete', 'bluray')], + '3D': '3d' + } + file_types = { 'subtitle': ('subtitle', 'subtitle'), 'subtitle_extra': ('subtitle', 'subtitle_extra'), @@ -470,9 +481,24 @@ class Scanner(Plugin): filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) data['group'] = self.getGroup(filename[len(folder):]) data['source'] = self.getSourceMedia(filename) - + data['3d_type'] = self.get3dType(filename) return data + def get3dType(self, filename): + filename = ss(filename) + + words = re.split('\W+', filename.lower()) + + for key in self.threed_types: + tags = self.threed_types.get(key, []) + + for tag in tags: + if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in filename.lower()): + log.debug('Found %s in %s', (tag, filename)) + return key + + return '' + def getMeta(self, filename): try: From 00954d98f73fd030d9d2ea2cd9549be2e226f4ec Mon Sep 17 00:00:00 2001 From: mano3m Date: Fri, 18 Apr 2014 15:46:35 +0200 Subject: [PATCH 245/468] Improve scanner - Fix the disc and tag removal: they received the filename with capse - add default metadata when resolution is known --- couchpotato/core/plugins/scanner.py | 50 ++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 5f69b0ed..4f52bec7 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -43,12 +43,12 @@ class Scanner(Plugin): threed_types = { 'Half SBS': [('half', 'sbs'), ('h', 'sbs'), 'hsbs'], 'Full SBS': [('full', 'sbs'), ('f', 'sbs'), 'fsbs'], - 'SBS': 'sbs', + 'SBS': ['sbs'], 'Half OU': [('half', 'ou'), ('h', 'ou'), 'hou'], 'Full OU': [('full', 'ou'), ('h', 'ou'), 'fou'], - 'OU': 'ou', + 'OU': ['ou'], 'Frame Packed': ['mvc', ('complete', 'bluray')], - '3D': '3d' + '3D': ['3d'] } file_types = { @@ -75,6 +75,16 @@ class Scanner(Plugin): 'video': ['x264', 'H264', 'DivX', 'Xvid'] } + resolutions = { + '1080p': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78}, + '1080i': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78}, + '720p': {'resolution_width': 1280, 'resolution_height': 720, 'aspect': 1.78}, + '720i': {'resolution_width': 1280, 'resolution_height': 720, 'aspect': 1.78}, + '480p': {'resolution_width': 640, 'resolution_height': 480, 'aspect': 1.33}, + '480i': {'resolution_width': 640, 'resolution_height': 480, 'aspect': 1.33}, + 'default': {'resolution_width': 0, 'resolution_height': 0, 'aspect': 1}, + } + audio_codec_map = { 0x2000: 'AC3', 0x2001: 'DTS', @@ -96,8 +106,8 @@ class Scanner(Plugin): 'HDTV': ['hdtv'] } - clean = '[ _\,\.\(\)\[\]\-]?(3d|hsbs|sbs|extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ - '|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)' + clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ + '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1 @@ -448,10 +458,13 @@ class Scanner(Plugin): try: data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video'])) data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio'])) - data['resolution_width'] = meta.get('resolution_width', 720) - data['resolution_height'] = meta.get('resolution_height', 480) data['audio_channels'] = meta.get('audio_channels', 2.0) - data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2) + if meta.get('resolution_width'): + data['resolution_width'] = meta.get('resolution_width') + data['resolution_height'] = meta.get('resolution_height') + data['aspect'] = round(float(meta.get('resolution_width')) / meta.get('resolution_height', 1), 2) + else: + data.update(self.getResolution(cur_file)) except: log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) pass @@ -481,7 +494,8 @@ class Scanner(Plugin): filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) data['group'] = self.getGroup(filename[len(folder):]) data['source'] = self.getSourceMedia(filename) - data['3d_type'] = self.get3dType(filename) + if data['quality'].get('is_3d', 0): + data['3d_type'] = self.get3dType(filename) return data def get3dType(self, filename): @@ -761,6 +775,9 @@ class Scanner(Plugin): identifier = file_path.replace(folder, '').lstrip(os.path.sep) # root folder identifier = os.path.splitext(identifier)[0] # ext + # Make sure the identifier is lower case as all regex is with lower case tags + identifier = identifier.lower() + try: path_split = splitString(identifier, os.path.sep) identifier = path_split[-2] if len(path_split) > 1 and len(path_split[-2]) > len(path_split[-1]) else path_split[-1] # Only get filename @@ -775,8 +792,11 @@ class Scanner(Plugin): # remove cptag identifier = self.removeCPTag(identifier) - # groups, release tags, scenename cleaner, regex isn't correct - identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':') + # simplify the string + identifier = simplifyString(identifier) + + # groups, release tags, scenename cleaner + identifier = re.sub(self.clean, '::', identifier).strip(':') # Year if year and identifier[:4] != year: @@ -825,6 +845,14 @@ class Scanner(Plugin): except: return '' + def getResolution(self, filename): + try: + for key in self.resolutions: + if key in filename and key != 'default': + return self.resolutions[key] + except: + return self.resolutions['default'] + def getGroup(self, file): try: match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I) From 4179ba642bfb36b50247bf0cd5ffbb4ec97f3d2c Mon Sep 17 00:00:00 2001 From: mano3m Date: Fri, 18 Apr 2014 17:21:51 +0200 Subject: [PATCH 246/468] Various Fixes --- couchpotato/core/media/movie/_base/main.py | 2 +- .../core/media/movie/_base/static/movie.actions.js | 6 +++--- couchpotato/core/plugins/quality/main.py | 6 +++--- couchpotato/core/plugins/scanner.py | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index a783cd1e..860ec965 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -139,7 +139,7 @@ class MovieBase(MovieTypeBase): # Clean snatched history for release in fireEvent('release.for_media', m['_id'], single = True): - if release.get('status') in ['downloaded', 'snatched', 'done']: + if release.get('status') in ['downloaded', 'snatched', 'seeding', 'done']: if params.get('ignore_previous', False): release['status'] = 'ignored' db.update(release) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 4d9dcccb..722855cc 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -78,7 +78,7 @@ MA.IMDB = new Class({ create: function(){ var self = this; - self.id = self.movie.get('imdb') || self.movie.get('identifier'); + self.id = self.movie.get('imdb'); self.el = new Element('a.imdb', { 'title': 'Go to the IMDB page of ' + self.getTitle(), @@ -684,7 +684,7 @@ MA.Readd = new Class({ var movie_done = self.movie.data.status == 'done'; if(self.movie.data.releases && !movie_done) var snatched = self.movie.data.releases.filter(function(release){ - return release.status && (release.status == 'snatched' || release.status == 'downloaded' || release.status == 'done'); + return release.status && (release.status == 'snatched' || release.status == 'seeding' || release.status == 'downloaded' || release.status == 'done'); }).length; if(movie_done || snatched && snatched > 0) @@ -703,7 +703,7 @@ MA.Readd = new Class({ Api.request('movie.add', { 'data': { - 'identifier': self.movie.get('identifier'), + 'identifier': self.movie.get('imdb'), 'ignore_previous': 1 } }); diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index cd59c6c9..394ee2df 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -35,9 +35,9 @@ class QualityPlugin(Plugin): ] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] threed_tags = { - 'hsbs': [('half', 'sbs')], - 'fsbs': [('full', 'sbs')], - '3d': [], + 'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'], + 'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'], + '3d': ['2d3d', '3d2d'], } cached_qualities = None diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 4f52bec7..167e76cd 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -106,7 +106,7 @@ class Scanner(Plugin): 'HDTV': ['hdtv'] } - clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ + clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 @@ -507,7 +507,7 @@ class Scanner(Plugin): tags = self.threed_types.get(key, []) for tag in tags: - if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in filename.lower()): + if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in words): log.debug('Found %s in %s', (tag, filename)) return key @@ -770,8 +770,6 @@ class Scanner(Plugin): def createStringIdentifier(self, file_path, folder = '', exclude_filename = False): - year = self.findYear(file_path) - identifier = file_path.replace(folder, '').lstrip(os.path.sep) # root folder identifier = os.path.splitext(identifier)[0] # ext @@ -795,6 +793,8 @@ class Scanner(Plugin): # simplify the string identifier = simplifyString(identifier) + year = self.findYear(file_path) + # groups, release tags, scenename cleaner identifier = re.sub(self.clean, '::', identifier).strip(':') @@ -848,7 +848,7 @@ class Scanner(Plugin): def getResolution(self, filename): try: for key in self.resolutions: - if key in filename and key != 'default': + if key in filename.lower() and key != 'default': return self.resolutions[key] except: return self.resolutions['default'] From 669e331f6c94581bbeda8f925c8b8c9f5eb33493 Mon Sep 17 00:00:00 2001 From: mano3m Date: Mon, 21 Apr 2014 12:15:05 +0200 Subject: [PATCH 247/468] Ruud's comments --- .../core/media/_base/providers/torrent/kickasstorrents.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 352871a4..a711502a 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -70,8 +70,8 @@ class Base(TorrentMagnetProvider): new['name'] = link.text new['url'] = td.find('a', 'imagnet')['href'] new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:]) - new['verified'] = 1 if td.find('a', 'iverify') else 0 - new['score'] = 100 if td.find('a', 'iverify') else 0 + new['verified'] = True if td.find('a', 'iverify') else False + new['score'] = 100 if new['verified'] else 0 elif column_name is 'size': new['size'] = self.parseSize(td.text) elif column_name is 'age': @@ -160,7 +160,7 @@ config = [{ 'name': 'only_verified', 'advanced': True, 'type': 'bool', - 'default': 1, + 'default': False, 'description': 'Only search for verified releases.' }, { From 7cdf124f9d8d65797833278a00fc2cb1fd2cc762 Mon Sep 17 00:00:00 2001 From: mano3m Date: Mon, 21 Apr 2014 16:18:21 +0200 Subject: [PATCH 248/468] Improve charts - Add a max height to each chart with a scrollbar - Add advanced options to hide chart items already in wanted or library (note that this can be done more efficiently...) --- couchpotato/core/media/movie/charts/__init__.py | 14 ++++++++++++++ couchpotato/core/media/movie/charts/main.py | 3 +++ .../core/media/movie/charts/static/charts.css | 7 +++++++ .../core/media/movie/charts/static/charts.js | 2 +- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/charts/__init__.py b/couchpotato/core/media/movie/charts/__init__.py index 9c673751..361da51a 100644 --- a/couchpotato/core/media/movie/charts/__init__.py +++ b/couchpotato/core/media/movie/charts/__init__.py @@ -28,6 +28,20 @@ config = [{ 'advanced': True, 'description': '(hours)', }, + { + 'name': 'hide_wanted', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Hide the chart movies that are already in your wanted list.', + }, + { + 'name': 'hide_library', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Hide the chart movies that are already in your library.', + }, ], }, ], diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py index 71002752..e3c46029 100644 --- a/couchpotato/core/media/movie/charts/main.py +++ b/couchpotato/core/media/movie/charts/main.py @@ -49,6 +49,9 @@ class Charts(Plugin): try: self.update_in_progress = True charts = fireEvent('automation.get_chart_list', merge = True) + for chart in charts: + chart['hide_wanted'] = self.conf('hide_wanted') + chart['hide_library'] = self.conf('hide_library') self.setCache('charts_cached', charts, timeout = 7200 * tryInt(self.conf('update_interval', default = 12))) except: log.error('Failed refreshing charts') diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index 610ac153..4eb3f8c6 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -11,6 +11,13 @@ display: inline-block; width: 50%; vertical-align: top; + max-height: 510px; + overflow-y: auto; + scrollbar-base-color: #4e5969; +} + +.charts .chart .media_result.hidden { + display: none; } .charts .refresh { diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js index 00033f4a..d04f1c2c 100644 --- a/couchpotato/core/media/movie/charts/static/charts.js +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -89,7 +89,7 @@ var Charts = new Class({ } }); - var in_database_class = movie.in_wanted ? 'chart_in_wanted' : (movie.in_library ? 'chart_in_library' : ''), + var in_database_class = (chart.hide_wanted && movie.in_wanted) ? 'hidden' : (movie.in_wanted ? 'chart_in_wanted' : ((chart.hide_library && movie.in_library) ? 'hidden': (movie.in_library ? 'chart_in_library' : ''))), in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); m.el From 5ce817cee644c43b9b5bfd6b6296f8bfc115e0fc Mon Sep 17 00:00:00 2001 From: Dan Boehm Date: Thu, 24 Apr 2014 15:00:04 -0500 Subject: [PATCH 249/468] Support for downloading extra artwork from Fanart.tv (resolves #1023). New image types include: * clearart * discart * extrathumbs * extrafanart * logo * banner * landscape (16:9 Thumb) There are a couple things that should be noted: 1. Only English images will be downloaded. 2. The fanart image is now downloaded from Fanart.tv if it can find one, otherwise it uses TMDB like it used to. This is because the images on Fanart.tv tend to be higher resolutions & quality. 3. Since multiple extrathumbs and extrafanarts are downloaded into a subdirectory, subdirectories are now supported for metadata file names. The subdirectories will be automatically created if they don't exist. 4. Bluray discart will always be preferred over DVD. Ideally, it would prefer DVD versions for SD quality movies, but I couldn't find an easy way to determine the quality from within the plugin. I suspect major changes would be needed to the plugin system in general in order to get this to work. If a user cares about the distinction, the best work-around is to not download these in Couchpotato and run the Artwork Downloader addon from within XBMC. 5. A maximum of 4 extrathumbs and 20 extrafanarts will be downloaded. Squashed commit of the following: commit b113a4def197a9ca8545bde9f5081c0591b93b36 Author: Dan Boehm Date: Thu Apr 24 14:24:12 2014 -0500 Bug-fix and code cleanup. Fixed a bug where the movie.info event would crash if there aren't any pictures to scrape in fanart.tv. commit adf7a4675d472e9e95a316c6cccc681a52804f13 Author: Dan Boehm Date: Wed Apr 23 16:15:03 2014 -0500 Added support for extrafanart. Also, the main fanart will be taken from fanart.tv unless one does not exist. commit 1791e46c8602f40bb56fe0cf7ecb0607f35b4b12 Author: Dan Boehm Date: Wed Apr 23 15:13:14 2014 -0500 Couchpotato now downloads extrathumbs from the extra tmdb backdrops if they exist. This commit made some major changes to the core image creation functionality that makes writing multiple images to folders possible. commit c0858807873749dbc928c0260037138f51f894ca Author: Dan Boehm Date: Wed Apr 23 12:18:53 2014 -0500 Bug Fix & Implemented functionality to select bluray or dvd disc images. Currently, only blurays will be selected, unless there are no blurays. However, if a mechanism for determining the quality of the release is implemented, it would be simple to make this selection based on the quality. commit 786751371d243f53d0f5c6f2c38d92288d8608ba Author: Dan Boehm Date: Wed Apr 23 10:59:25 2014 -0500 Fixed a bug where non-HD clearart and logos couldn't be downloaded. commit feda8df483d13b5a5df3a869f25de8f2c7e6ffe3 Author: Dan Boehm Date: Wed Apr 23 10:12:31 2014 -0500 Fixed some problems that were missed with the previous merge. commit 5ddab6c40e69a5accc6c0336cd7485920ff82d8f Merge: 7273abf ff46aa0 Author: Dan Boehm Date: Wed Apr 23 10:02:11 2014 -0500 Merge branch 'develop' into fanarttv Conflicts: couchpotato/core/media/movie/providers/info/themoviedb.py couchpotato/core/providers/metadata/xbmc/__init__.py commit 7273abf827735cf245711c3d3199a6a173a964aa Author: dan Date: Thu Feb 27 13:29:57 2014 -0600 Downloads extra artwork from fanart.tv Downloads occur with correct filenaming when XBMC metadata is generated, but the image URLs are selected when the movie.info event is called. commit 9080d9d749c7e1ddbdc78f7b37a3c5f83195d580 Author: dan Date: Wed Feb 26 16:31:37 2014 -0600 Added basic functionality for fanarttv provider. This should be mostly done and is based on the tvdb provider. commit 1b39b246c2a9d65f9ef86c4e150a12d893e362c0 Author: dan Date: Wed Feb 26 14:50:17 2014 -0600 Updated fanarttv library with the correct package hierarchy (libs.fanarttv). commit 8abb7c8f8ad3347900debb9f6a6d5a7acb7df396 Author: dan Date: Wed Feb 26 13:12:48 2014 -0600 Added fanart.tv API python library (lib.fanarttv). The upstream for this library is at https://github.com/z4r/python-fanart. --- .../media/movie/providers/info/_modifier.py | 9 +- .../media/movie/providers/info/fanarttv.py | 168 ++++++++++++++++++ .../media/movie/providers/info/themoviedb.py | 36 +++- .../media/movie/providers/metadata/base.py | 150 ++++++++++++---- .../media/movie/providers/metadata/xbmc.py | 145 ++++++++++++++- libs/fanarttv/__init__.py | 110 ++++++++++++ libs/fanarttv/core.py | 44 +++++ libs/fanarttv/errors.py | 15 ++ libs/fanarttv/immutable.py | 46 +++++ libs/fanarttv/items.py | 68 +++++++ libs/fanarttv/movie.py | 103 +++++++++++ libs/fanarttv/music.py | 80 +++++++++ libs/fanarttv/tv.py | 108 +++++++++++ 13 files changed, 1037 insertions(+), 45 deletions(-) create mode 100644 couchpotato/core/media/movie/providers/info/fanarttv.py create mode 100644 libs/fanarttv/__init__.py create mode 100644 libs/fanarttv/core.py create mode 100644 libs/fanarttv/errors.py create mode 100644 libs/fanarttv/immutable.py create mode 100644 libs/fanarttv/items.py create mode 100644 libs/fanarttv/movie.py create mode 100644 libs/fanarttv/music.py create mode 100644 libs/fanarttv/tv.py diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index bf257272..beb29e74 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -26,7 +26,14 @@ class MovieResultModifier(Plugin): 'backdrop': [], 'poster_original': [], 'backdrop_original': [], - 'actors': {} + 'actors': {}, + 'landscape': [], + 'logo': [], + 'clearart': [], + 'discart': [], + 'banner': [], + 'extrathumbs': [], + 'extrafanart': [] }, 'runtime': 0, 'plot': '', diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py new file mode 100644 index 00000000..07d1a009 --- /dev/null +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -0,0 +1,168 @@ +import os +import traceback + +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media.movie.providers.base import MovieProvider +from couchpotato.core.plugins.quality import QualityPlugin + +from libs.fanarttv.movie import Movie +import libs.fanarttv.errors as fanarttv_errors + + +log = CPLog(__name__) + +autoload = 'FanartTV' + + +class FanartTV(MovieProvider): + MAX_EXTRAFANART = 20 + + def __init__(self): + addEvent('movie.extraart', self.getArt, priority=2) + + # Configure fanarttv API settings + os.environ.setdefault('FANART_APIKEY', self.conf('api_key')) + + def getArt(self, identifier): + # FIXME: I believe I should be registering a cache here... I need to look into that. + log.debug("Getting Extra Artwork from Fanart.tv...") + if not identifier: + return {} + + images = {} + + try: + try: + exists = True + movie = Movie.get(id=identifier) + except (fanarttv_errors.FanartError, IOError): + exists = False + + if exists: + images = self._parseMovie(movie, True) + + except: + log.error('Failed getting extra art for %s: %s', + (identifier, traceback.format_exc())) + return {} + + return images + + def _parseMovie(self, movie, isHD): + images = { + 'landscape': [], + 'logo': [], + 'discart': [], + 'clearart': [], + 'banner': [], + 'extrafanart': [] + } + + images['landscape'] = self._getMultImages(movie.thumbs, 1) + images['banner'] = self._getMultImages(movie.banners, 1) + images['discart'] = self._getMultImages(self._trimDiscs(movie.discs, isHD), 1) + + images['clearart'] = self._getMultImages(movie.hdarts, 1) + if len(images['clearart']) is 0: + images['clearart'] = self._getMultImages(movie.arts, 1) + + images['logo'] = self._getMultImages(movie.hdlogos, 1) + if len(images['logo']) is 0: + images['logo'] = self._getMultImages(movie.logos, 1) + + fanarts = self._getMultImages(movie.backgrounds, self.MAX_EXTRAFANART + 1) + + if fanarts: + images['backdrop_original'] = fanarts[0] + images['extrafanart'] = fanarts[1:] + + # TODO: Add support for extra backgrounds + #extraFanart = self._getMultImages(movie.backgrounds, -1) + + return images + + def _trimDiscs(self, discImages, isHD): + ''' + Return a subset of discImages based on isHD. If isHD is true, only + bluray disc images will be returned. If isHD is false, only dvd disc + images will be returned. If the resulting list would be an empty list, + then the original list is returned instead. + ''' + trimmed = [] + for disc in discImages: + if isHD and disc.disc_type == u'bluray': + trimmed.append(disc) + elif not isHD and disc.disc_type == u'dvd': + trimmed.append(disc) + + if len(trimmed) is 0: + return discImages + else: + return trimmed + + def _getImage(self, images): + image_url = None + highscore = -1 + for image in images: + if image.likes > highscore: + highscore = image.likes + image_url = image.url + + return image_url + + def _getMultImages(self, images, n): + ''' + Chooses the best n images and returns them as a list. + If n<0, all images will be returned. + ''' + image_urls = [] + pool = [] + for image in images: + if image.lang == u'en': + pool.append(image) + origPoolSize = len(pool) + + while len(pool) > 0 and (n < 0 or origPoolSize - len(pool) < n): + best = None + highscore = -1 + for image in pool: + if image.likes > highscore: + highscore = image.likes + best = image + image_urls.append(best.url) + pool.remove(best) + + return image_urls + + def isDisabled(self): + if self.conf('api_key') == '': + log.error('No API key provided.') + return True + return False + + def _determineHD(self, quality): + for qualityDef in QualityPlugin.qualities: + if quality == qualityDef.get('identifier'): + return bool(qualityDef.get('hd')) + return False + +config = [{ + 'name': 'fanarttv', + 'groups': [ + { + 'tab': 'providers', + 'name': 'fanarttv', + 'label': 'fanart.tv', + 'hidden': True, + 'description': 'Used for all calls to fanart.tv.', + 'options': [ + { + 'name': 'api_key', + 'default': 'd788b4822b9e1f44068026e05557e5d9', + 'label': 'API Key', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index b48822cc..7ffba38b 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -1,6 +1,6 @@ import traceback -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog @@ -13,6 +13,7 @@ autoload = 'TheMovieDb' class TheMovieDb(MovieProvider): + MAX_EXTRATHUMBS = 4 def __init__(self): addEvent('movie.info', self.getInfo, priority = 2) @@ -87,6 +88,13 @@ class TheMovieDb(MovieProvider): except: log.error('Failed getting info for %s: %s', (identifier, traceback.format_exc())) + # Get extra artwork via Fanart.TV and merge into images dict + try: + extraArt = fireEvent('movie.extraart', identifier)[0] + result['images'] = dict(result['images'].items() + extraArt.items()) + except IndexError: + pass + return result def parseMovie(self, movie, extended = True): @@ -100,13 +108,15 @@ class TheMovieDb(MovieProvider): poster = self.getImage(movie, type = 'poster', size = 'poster') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') + extrathumbs = self.getMultImages(movie, type='backdrops', size='original', n=self.MAX_EXTRATHUMBS, skipfirst=True) images = { 'poster': [poster] if poster else [], #'backdrop': [backdrop] if backdrop else [], 'poster_original': [poster_original] if poster_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [], - 'actors': {} + 'actors': {}, + 'extrathumbs': extrathumbs } # Genres @@ -171,6 +181,28 @@ class TheMovieDb(MovieProvider): log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) return image_url + + def getMultImages(self, movie, type='backdrops', size='original', n=-1, skipfirst=False): + ''' + If n < 0, return all images. Otherwise return n images. + If n > len(getattr(movie, type)), then return all images. + If skipfirst is True, then it will skip getattr(movie, type)[0]. This + is because backdrops[0] is typically backdrop. + ''' + image_urls = [] + try: + images = getattr(movie, type) + if n < 0 or n > len(images): + numImages = len(images) + else: + numImages = n + for i in range(int(skipfirst), numImages + int(skipfirst)): + image_urls.append(images[i].geturl(size=size)) + + except: + log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie)))) + + return image_urls def isDisabled(self): if self.conf('api_key') == '': diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index b9fc5d71..ec91b062 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -37,76 +37,150 @@ class MovieMetaData(MetaDataBase): root = os.path.dirname(root_name) movie_info = group['media'].get('info') - - for file_type in ['nfo', 'thumbnail', 'fanart']: + + for file_type in ['nfo']: try: - # Get file path - name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root) + self._createType(meta_name, root, movie_info, group, file_type, 0) + except: + log.error('Unable to create %s file: %s', ('nfo', traceback.format_exc())) - if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): - - # Get file content - content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group) - if content: - log.debug('Creating %s file: %s', (file_type, name)) - if os.path.isfile(content): - content = sp(content) - name = sp(name) - - shutil.copy2(content, name) - shutil.copyfile(content, name) - - # Try and copy stats seperately - try: shutil.copystat(content, name) - except: pass - else: - self.createFile(name, content) - group['renamed_files'].append(name) - - try: - os.chmod(sp(name), Env.getPermission('file')) - except: - log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) + for file_type in ['thumbnail', 'fanart', 'banner', 'discart', 'logo', 'clearart', 'landscape', 'extrathumbs', 'extrafanart']: + try: + if file_type == 'thumbnail': + numImages = len(movie_info['images']['poster_original']) + elif file_type == 'fanart': + numImages = len(movie_info['images']['backdrop_original']) + else: + numImages = len(movie_info['images'][file_type]) + for i in range(numImages): + self._createType(meta_name, root, movie_info, group, file_type, i) except: log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) + def _createType(self, meta_name, root, movie_info, group, file_type, i):# Get file path + name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root, i) + + if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): + + # Get file content + content = getattr(self, 'get' + file_type.capitalize())(movie_info=movie_info, data=group, i=i) + if content: + log.debug('Creating %s file: %s', (file_type, name)) + if os.path.isfile(content): + content = sp(content) + name = sp(name) + + if not os.path.exists(os.path.dirname(name)): + os.makedirs(os.path.dirname(name)) + + shutil.copy2(content, name) + shutil.copyfile(content, name) + + # Try and copy stats seperately + try: shutil.copystat(content, name) + except: pass + else: + self.createFile(name, content) + group['renamed_files'].append(name) + + try: + os.chmod(sp(name), Env.getPermission('file')) + except: + log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) + def getRootName(self, data = None): if not data: data = {} return os.path.join(data['destination_dir'], data['filename']) - def getFanartName(self, name, root): + def getFanartName(self, name, root, i): return - def getThumbnailName(self, name, root): + def getThumbnailName(self, name, root, i): return - def getNfoName(self, name, root): + def getBannerName(self, name, root, i): return - def getNfo(self, movie_info = None, data = None): + def getClearartName(self, name, root, i): + return + + def getLogoName(self, name, root, i): + return + + def getDiscartName(self, name, root, i): + return + + def getLandscapeName(self, name, root, i): + return + + def getExtrathumbsName(self, name, root, i): + return + + def getExtrafanartName(self, name, root, i): + return + + def getNfoName(self, name, root, i): + return + + def getNfo(self, movie_info=None, data=None, i=0): if not data: data = {} if not movie_info: movie_info = {} - def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original'): + def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original', i = 0): if not data: data = {} if not movie_info: movie_info = {} # See if it is in current files files = data['media'].get('files') if files.get('image_' + wanted_file_type): - if os.path.isfile(files['image_' + wanted_file_type][0]): - return files['image_' + wanted_file_type][0] + if os.path.isfile(files['image_' + wanted_file_type][i]): + return files['image_' + wanted_file_type][i] # Download using existing info try: images = movie_info['images'][wanted_file_type] - file_path = fireEvent('file.download', url = images[0], single = True) + file_path = fireEvent('file.download', url = images[i], single = True) return file_path except: pass - def getFanart(self, movie_info = None, data = None): + def getFanart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original') + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='backdrop_original', i=i) + + def getBanner(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='banner', i=i) + + def getClearart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='clearart', i=i) + + def getLogo(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='logo', i=i) + + def getDiscart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='discart', i=i) + + def getLandscape(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='landscape', i=i) + + def getExtrathumbs(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrathumbs', i=i) + + def getExtrafanart(self, movie_info=None, data=None, i=0): + if not data: data = {} + if not movie_info: movie_info = {} + return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrafanart', i=i) diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index 9f6bb7dc..318f79a7 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -17,19 +17,43 @@ autoload = 'XBMC' class XBMC(MovieMetaData): - def getFanartName(self, name, root): + def getFanartName(self, name, root, i): return self.createMetaName(self.conf('meta_fanart_name'), name, root) - def getThumbnailName(self, name, root): + def getThumbnailName(self, name, root, i): return self.createMetaName(self.conf('meta_thumbnail_name'), name, root) - def getNfoName(self, name, root): + def getNfoName(self, name, root, i): return self.createMetaName(self.conf('meta_nfo_name'), name, root) + def getBannerName(self, name, root, i): + return self.createMetaName(self.conf('meta_banner_name'), name, root) + + def getClearartName(self, name, root, i): + return self.createMetaName(self.conf('meta_clearart_name'), name, root) + + def getLogoName(self, name, root, i): + return self.createMetaName(self.conf('meta_logo_name'), name, root) + + def getDiscartName(self, name, root, i): + return self.createMetaName(self.conf('meta_discart_name'), name, root) + + def getLandscapeName(self, name, root, i): + return self.createMetaName(self.conf('meta_landscape_name'), name, root) + + def getExtrathumbsName(self, name, root, i): + return self.createMetaNameMult(self.conf('meta_extrathumbs_name'), name, root, i) + + def getExtrafanartName(self, name, root, i): + return self.createMetaNameMult(self.conf('meta_extrafanart_name'), name, root, i) + def createMetaName(self, basename, name, root): return os.path.join(root, basename.replace('%s', name)) - def getNfo(self, movie_info = None, data = None): + def createMetaNameMult(self, basename, name, root, i): + return os.path.join(root, basename.replace('%s', name).replace('%i', str(i + 1))) + + def getNfo(self, movie_info=None, data=None, i=0): if not data: data = {} if not movie_info: movie_info = {} @@ -133,6 +157,35 @@ class XBMC(MovieMetaData): for image_url in movie_info['images']['backdrop_original']: image = SubElement(fanart, 'thumb') image.text = toUnicode(image_url) + banner = SubElement(nfoxml, 'banner') + for image_url in movie_info['images']['banner']: + image = SubElement(banner, 'thumb') + image.text = toUnicode(image_url) + discart = SubElement(nfoxml, 'discart') + for image_url in movie_info['images']['discart']: + image = SubElement(discart, 'thumb') + image.text = toUnicode(image_url) + logo = SubElement(nfoxml, 'logo') + for image_url in movie_info['images']['logo']: + image = SubElement(logo, 'thumb') + image.text = toUnicode(image_url) + clearart = SubElement(nfoxml, 'clearart') + for image_url in movie_info['images']['clearart']: + image = SubElement(clearart, 'thumb') + image.text = toUnicode(image_url) + landscape = SubElement(nfoxml, 'landscape') + for image_url in movie_info['images']['landscape']: + image = SubElement(landscape, 'thumb') + image.text = toUnicode(image_url) + extrathumb = SubElement(nfoxml, 'extrathumb') + for image_url in movie_info['images']['extrathumbs']: + image = SubElement(extrathumb, 'thumb') + image.text = toUnicode(image_url) + extrafanart = SubElement(nfoxml, 'extrafanart') + for image_url in movie_info['images']['extrafanart']: + image = SubElement(extrafanart, 'thumb') + image.text = toUnicode(image_url) + # Add trailer if found trailer_found = False @@ -239,6 +292,90 @@ config = [{ 'default': '%s.tbn', 'advanced': True, }, + { + 'name': 'meta_banner', + 'label': 'Banner', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_banner_name', + 'label': 'Banner filename', + 'default': 'banner.jpg', + 'advanced': True, + }, + { + 'name': 'meta_clearart', + 'label': 'ClearArt', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_clearart_name', + 'label': 'ClearArt filename', + 'default': 'clearart.png', + 'advanced': True, + }, + { + 'name': 'meta_disc', + 'label': 'DiscArt', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_discart_name', + 'label': 'DiscArt filename', + 'default': 'disc.png', + 'advanced': True, + }, + { + 'name': 'meta_landscape', + 'label': 'Landscape', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_landscape_name', + 'label': 'Landscape filename', + 'default': 'landscape.jpg', + 'advanced': True, + }, + { + 'name': 'meta_logo', + 'label': 'ClearLogo', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_logo_name', + 'label': 'ClearLogo filename', + 'default': 'logo.png', + 'advanced': True, + }, + { + 'name': 'meta_extrathumbs', + 'label': 'Extrathumbs', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_extrathumbs_name', + 'label': 'Extrathumbs filename (%i is the image number, and must be included to have multiple images).', + 'default': 'extrathumbs/thumb%i.jpg', + 'advanced': True + }, + { + 'name': 'meta_extrafanart', + 'lavel': 'Extrafanart', + 'default': True, + 'type': 'bool' + }, + { + 'name': 'meta_extrafanart_name', + 'label': 'Extrafanart filename (%i is the image number, and must be included to have multiple images).', + 'default': 'extrafanart/extrafanart%i.jpg', + 'advanced': True + } ], }, ], diff --git a/libs/fanarttv/__init__.py b/libs/fanarttv/__init__.py new file mode 100644 index 00000000..773703ca --- /dev/null +++ b/libs/fanarttv/__init__.py @@ -0,0 +1,110 @@ +__author__ = 'Andrea De Marco <24erre@gmail.com>' +__version__ = '1.4.0' +__classifiers__ = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Software Development :: Libraries', +] +__copyright__ = "2012, %s " % __author__ +__license__ = """ + Copyright %s. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" % __copyright__ + +__docformat__ = 'restructuredtext en' + +__doc__ = """ +:abstract: Python interface to fanart.tv API +:version: %s +:author: %s +:contact: http://z4r.github.com/ +:date: 2012-04-04 +:copyright: %s +""" % (__version__, __author__, __license__) + + +def values(obj): + return [v for k, v in obj.__dict__.iteritems() if not k.startswith('_')] + +BASEURL = 'http://api.fanart.tv/webservice' + + +class FORMAT(object): + JSON = 'JSON' + XML = 'XML' + PHP = 'PHP' + + +class WS(object): + MUSIC = 'artist' + MOVIE = 'movie' + TV = 'series' + + +class TYPE(object): + ALL = 'all' + + class TV(object): + ART = 'clearart' + LOGO = 'clearlogo' + CHARACTER = 'characterart' + THUMB = 'tvthumb' + SEASONTHUMB = 'seasonthumb' + BACKGROUND = 'showbackground' + HDLOGO = 'hdtvlogo' + HDART = 'hdclearart' + POSTER = 'tvposter' + BANNER = 'tvbanner' + + class MUSIC(object): + DISC = 'cdart' + LOGO = 'musiclogo' + BACKGROUND = 'artistbackground' + COVER = 'albumcover' + THUMB = 'artistthumb' + + class MOVIE(object): + ART = 'movieart' + LOGO = 'movielogo' + DISC = 'moviedisc' + POSTER = 'movieposter' + BACKGROUND = 'moviebackground' + HDLOGO = 'hdmovielogo' + HDART = 'hdmovieclearart' + BANNER = 'moviebanner' + THUMB = 'moviethumb' + + +class SORT(object): + POPULAR = 1 + NEWEST = 2 + OLDEST = 3 + + +class LIMIT(object): + ONE = 1 + ALL = 2 + +FORMAT_LIST = values(FORMAT) +WS_LIST = values(WS) +TYPE_LIST = values(TYPE.MUSIC) + values(TYPE.TV) + values(TYPE.MOVIE) + [TYPE.ALL] +MUSIC_TYPE_LIST = values(TYPE.MUSIC) + [TYPE.ALL] +TV_TYPE_LIST = values(TYPE.TV) + [TYPE.ALL] +MOVIE_TYPE_LIST = values(TYPE.MOVIE) + [TYPE.ALL] +SORT_LIST = values(SORT) +LIMIT_LIST = values(LIMIT) diff --git a/libs/fanarttv/core.py b/libs/fanarttv/core.py new file mode 100644 index 00000000..9cd1cad7 --- /dev/null +++ b/libs/fanarttv/core.py @@ -0,0 +1,44 @@ +import libs.requests as requests +import libs.fanarttv as fanart +from libs.fanarttv.errors import RequestFanartError, ResponseFanartError + + +class Request(object): + def __init__(self, apikey, id, ws, type=None, sort=None, limit=None): + self._apikey = apikey + self._id = id + self._ws = ws + self._type = type or fanart.TYPE.ALL + self._sort = sort or fanart.SORT.POPULAR + self._limit = limit or fanart.LIMIT.ALL + self.validate() + self._response = None + + def validate(self): + for attribute_name in ('ws', 'type', 'sort', 'limit'): + attribute = getattr(self, '_' + attribute_name) + choices = getattr(fanart, attribute_name.upper() + '_LIST') + if attribute not in choices: + raise RequestFanartError('Not allowed {0}: {1} [{2}]'.format(attribute_name, attribute, ', '.join(choices))) + + def __str__(self): + return '/'.join(map(str, [ + fanart.BASEURL, + self._ws, + self._apikey, + self._id, + fanart.FORMAT.JSON, + self._type, + self._sort, + self._limit, + ])) + + def response(self): + try: + response = requests.get(str(self)) + rjson = response.json() + if not isinstance(rjson, dict): + raise Exception(response.text) + return rjson + except Exception as e: + raise ResponseFanartError(str(e)) diff --git a/libs/fanarttv/errors.py b/libs/fanarttv/errors.py new file mode 100644 index 00000000..95a71e35 --- /dev/null +++ b/libs/fanarttv/errors.py @@ -0,0 +1,15 @@ +class FanartError(Exception): + def __str__(self): + return ', '.join(map(str, self.args)) + + def __repr__(self): + name = self.__class__.__name__ + return '%s%r' % (name, self.args) + + +class ResponseFanartError(FanartError): + pass + + +class RequestFanartError(FanartError): + pass diff --git a/libs/fanarttv/immutable.py b/libs/fanarttv/immutable.py new file mode 100644 index 00000000..170de370 --- /dev/null +++ b/libs/fanarttv/immutable.py @@ -0,0 +1,46 @@ +class Immutable(object): + _mutable = False + + def __setattr__(self, name, value): + if self._mutable or name == '_mutable': + super(Immutable, self).__setattr__(name, value) + else: + raise TypeError("Can't modify immutable instance") + + def __delattr__(self, name): + if self._mutable: + super(Immutable, self).__delattr__(name) + else: + raise TypeError("Can't modify immutable instance") + + def __eq__(self, other): + return hash(self) == hash(other) + + def __hash__(self): + return hash(repr(self)) + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + ', '.join(['{0}={1}'.format(k, repr(v)) for k, v in self]) + ) + + def __iter__(self): + l = self.__dict__.keys() + l.sort() + for k in l: + if not k.startswith('_'): + yield k, getattr(self, k) + + @staticmethod + def mutablemethod(f): + def func(self, *args, **kwargs): + if isinstance(self, Immutable): + old_mutable = self._mutable + self._mutable = True + res = f(self, *args, **kwargs) + self._mutable = old_mutable + else: + res = f(self, *args, **kwargs) + return res + return func diff --git a/libs/fanarttv/items.py b/libs/fanarttv/items.py new file mode 100644 index 00000000..64848076 --- /dev/null +++ b/libs/fanarttv/items.py @@ -0,0 +1,68 @@ +import json +import os +import libs.requests as requests +from libs.fanarttv.core import Request +from libs.fanarttv.immutable import Immutable + + +class LeafItem(Immutable): + KEY = NotImplemented + + @Immutable.mutablemethod + def __init__(self, id, url, likes): + self.id = int(id) + self.url = url + self.likes = int(likes) + self._content = None + + @classmethod + def from_dict(cls, resource): + return cls(**dict([(str(k), v) for k, v in resource.iteritems()])) + + @classmethod + def extract(cls, resource): + return [cls.from_dict(i) for i in resource.get(cls.KEY, {})] + + @Immutable.mutablemethod + def content(self): + if not self._content: + self._content = requests.get(self.url).content + return self._content + + def __str__(self): + return self.url + + +class ResourceItem(Immutable): + WS = NotImplemented + request_cls = Request + + @classmethod + def from_dict(cls, map): + raise NotImplementedError + + @classmethod + def get(cls, id): + map = cls.request_cls( + apikey=os.environ.get('FANART_APIKEY'), + id=id, + ws=cls.WS + ).response() + return cls.from_dict(map) + + def json(self, **kw): + return json.dumps( + self, + default=lambda o: dict([(k, v) for k, v in o.__dict__.items() if not k.startswith('_')]), + **kw + ) + + +class CollectableItem(Immutable): + @classmethod + def from_dict(cls, key, map): + raise NotImplementedError + + @classmethod + def collection_from_dict(cls, map): + return [cls.from_dict(k, v) for k, v in map.iteritems()] diff --git a/libs/fanarttv/movie.py b/libs/fanarttv/movie.py new file mode 100644 index 00000000..fe473d4d --- /dev/null +++ b/libs/fanarttv/movie.py @@ -0,0 +1,103 @@ +import libs.fanarttv as fanart +from libs.fanarttv.items import LeafItem, Immutable, ResourceItem +__all__ = ( + 'ArtItem', + 'DiscItem', + 'LogoItem', + 'PosterItem', + 'BackgroundItem', + 'HdLogoItem', + 'HdArtItem', + 'BannerItem', + 'ThumbItem', + 'Movie', +) + + +class MovieItem(LeafItem): + + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang): + super(MovieItem, self).__init__(id, url, likes) + self.lang = lang + + +class DiscItem(MovieItem): + KEY = fanart.TYPE.MOVIE.DISC + + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang, disc, disc_type): + super(DiscItem, self).__init__(id, url, likes, lang) + self.disc = int(disc) + self.disc_type = disc_type + + +class ArtItem(MovieItem): + KEY = fanart.TYPE.MOVIE.ART + + +class LogoItem(MovieItem): + KEY = fanart.TYPE.MOVIE.LOGO + + +class PosterItem(MovieItem): + KEY = fanart.TYPE.MOVIE.POSTER + + +class BackgroundItem(MovieItem): + KEY = fanart.TYPE.MOVIE.BACKGROUND + + +class HdLogoItem(MovieItem): + KEY = fanart.TYPE.MOVIE.HDLOGO + + +class HdArtItem(MovieItem): + KEY = fanart.TYPE.MOVIE.HDART + + +class BannerItem(MovieItem): + KEY = fanart.TYPE.MOVIE.BANNER + + +class ThumbItem(MovieItem): + KEY = fanart.TYPE.MOVIE.THUMB + + +class Movie(ResourceItem): + WS = fanart.WS.MOVIE + + @Immutable.mutablemethod + def __init__(self, name, imdbid, tmdbid, arts, logos, discs, posters, backgrounds, hdlogos, hdarts, + banners, thumbs): + self.name = name + self.imdbid = imdbid + self.tmdbid = tmdbid + self.arts = arts + self.posters = posters + self.logos = logos + self.discs = discs + self.backgrounds = backgrounds + self.hdlogos = hdlogos + self.hdarts = hdarts + self.banners = banners + self.thumbs = thumbs + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + imdbid=resource['imdb_id'], + tmdbid=resource['tmdb_id'], + arts=ArtItem.extract(resource), + logos=LogoItem.extract(resource), + discs=DiscItem.extract(resource), + posters=PosterItem.extract(resource), + backgrounds=BackgroundItem.extract(resource), + hdlogos=HdLogoItem.extract(resource), + hdarts=HdArtItem.extract(resource), + banners=BannerItem.extract(resource), + thumbs=ThumbItem.extract(resource), + ) diff --git a/libs/fanarttv/music.py b/libs/fanarttv/music.py new file mode 100644 index 00000000..df634c67 --- /dev/null +++ b/libs/fanarttv/music.py @@ -0,0 +1,80 @@ +from libs.fanarttv.items import Immutable, LeafItem, ResourceItem, CollectableItem +import libs.fanarttv as fanart +__all__ = ( + 'BackgroundItem', + 'CoverItem', + 'LogoItem', + 'ThumbItem', + 'DiscItem', + 'Artist', + 'Album', +) + + +class BackgroundItem(LeafItem): + KEY = fanart.TYPE.MUSIC.BACKGROUND + + +class CoverItem(LeafItem): + KEY = fanart.TYPE.MUSIC.COVER + + +class LogoItem(LeafItem): + KEY = fanart.TYPE.MUSIC.LOGO + + +class ThumbItem(LeafItem): + KEY = fanart.TYPE.MUSIC.THUMB + + +class DiscItem(LeafItem): + KEY = fanart.TYPE.MUSIC.DISC + + @Immutable.mutablemethod + def __init__(self, id, url, likes, disc, size): + super(DiscItem, self).__init__(id, url, likes) + self.disc = int(disc) + self.size = int(size) + + +class Artist(ResourceItem): + WS = fanart.WS.MUSIC + + @Immutable.mutablemethod + def __init__(self, name, mbid, albums, backgrounds, logos, thumbs): + self.name = name + self.mbid = mbid + self.albums = albums + self.backgrounds = backgrounds + self.logos = logos + self.thumbs = thumbs + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + mbid=resource['mbid_id'], + albums=Album.collection_from_dict(resource.get('albums', {})), + backgrounds=BackgroundItem.extract(resource), + thumbs=ThumbItem.extract(resource), + logos=LogoItem.extract(resource), + ) + + +class Album(CollectableItem): + + @Immutable.mutablemethod + def __init__(self, mbid, covers, arts): + self.mbid = mbid + self.covers = covers + self.arts = arts + + @classmethod + def from_dict(cls, key, resource): + return cls( + mbid=key, + covers=CoverItem.extract(resource), + arts=DiscItem.extract(resource), + ) diff --git a/libs/fanarttv/tv.py b/libs/fanarttv/tv.py new file mode 100644 index 00000000..9b1b08a7 --- /dev/null +++ b/libs/fanarttv/tv.py @@ -0,0 +1,108 @@ +import libs.fanarttv as fanart +from libs.fanarttv.items import LeafItem, Immutable, ResourceItem +__all__ = ( + 'CharacterItem', + 'ArtItem', + 'LogoItem', + 'BackgroundItem', + 'SeasonItem', + 'ThumbItem', + 'HdLogoItem', + 'HdArtItem', + 'PosterItem', + 'BannerItem', + 'TvShow', +) + + +class TvItem(LeafItem): + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang): + super(TvItem, self).__init__(id, url, likes) + self.lang = lang + + +class SeasonedTvItem(TvItem): + @Immutable.mutablemethod + def __init__(self, id, url, likes, lang, season): + super(SeasonedTvItem, self).__init__(id, url, likes, lang) + self.season = 0 if season == 'all' else int(season or 0) + + +class CharacterItem(TvItem): + KEY = fanart.TYPE.TV.CHARACTER + + +class ArtItem(TvItem): + KEY = fanart.TYPE.TV.ART + + +class LogoItem(TvItem): + KEY = fanart.TYPE.TV.LOGO + + +class BackgroundItem(SeasonedTvItem): + KEY = fanart.TYPE.TV.BACKGROUND + + +class SeasonItem(SeasonedTvItem): + KEY = fanart.TYPE.TV.SEASONTHUMB + + +class ThumbItem(TvItem): + KEY = fanart.TYPE.TV.THUMB + + +class HdLogoItem(TvItem): + KEY = fanart.TYPE.TV.HDLOGO + + +class HdArtItem(TvItem): + KEY = fanart.TYPE.TV.HDART + + +class PosterItem(TvItem): + KEY = fanart.TYPE.TV.POSTER + + +class BannerItem(TvItem): + KEY = fanart.TYPE.TV.BANNER + + +class TvShow(ResourceItem): + WS = fanart.WS.TV + + @Immutable.mutablemethod + def __init__(self, name, tvdbid, backgrounds, characters, arts, logos, seasons, thumbs, hdlogos, hdarts, posters, + banners): + self.name = name + self.tvdbid = tvdbid + self.backgrounds = backgrounds + self.characters = characters + self.arts = arts + self.logos = logos + self.seasons = seasons + self.thumbs = thumbs + self.hdlogos = hdlogos + self.hdarts = hdarts + self.posters = posters + self.banners = banners + + @classmethod + def from_dict(cls, resource): + assert len(resource) == 1, 'Bad Format Map' + name, resource = resource.items()[0] + return cls( + name=name, + tvdbid=resource['thetvdb_id'], + backgrounds=BackgroundItem.extract(resource), + characters=CharacterItem.extract(resource), + arts=ArtItem.extract(resource), + logos=LogoItem.extract(resource), + seasons=SeasonItem.extract(resource), + thumbs=ThumbItem.extract(resource), + hdlogos=HdLogoItem.extract(resource), + hdarts=HdArtItem.extract(resource), + posters=PosterItem.extract(resource), + banners=BannerItem.extract(resource), + ) From 6a81f2241d7fa69ccf4e4138ddefbeb025ff3e28 Mon Sep 17 00:00:00 2001 From: Dan Boehm Date: Thu, 24 Apr 2014 15:02:29 -0500 Subject: [PATCH 250/468] Added option to run the Artwork Downloader addon during XBMC notify. This option will only work in XBMCv12 (Frodo) or later. It also requires the Artwork Downloader Addon. Since XBMC's API doesn't support notifications over HTML, there is no way for couchpotato to know when the Library Scan is complete. Since running the Artwork Downloader before the movie has been scanned won't solve anything, a delay timer can be adjusted to suit the user's needs. Squashed commit of the following: commit bd60ed585f77cc40c31fd67d4ae732e0845d31ab Merge: fcb092e b113a4d Author: Dan Boehm Date: Thu Apr 24 14:26:24 2014 -0500 Merge branch 'fanarttv' into artdlnotify commit b113a4def197a9ca8545bde9f5081c0591b93b36 Author: Dan Boehm Date: Thu Apr 24 14:24:12 2014 -0500 Bug-fix and code cleanup. Fixed a bug where the movie.info event would crash if there aren't any pictures to scrape in fanart.tv. commit fcb092e776e00ceabea016b3c26d9394e32d72b0 Author: Dan Boehm Date: Thu Apr 24 14:21:27 2014 -0500 Option to run the artwork downloader addon during XBMC notify. commit adf7a4675d472e9e95a316c6cccc681a52804f13 Author: Dan Boehm Date: Wed Apr 23 16:15:03 2014 -0500 Added support for extrafanart. Also, the main fanart will be taken from fanart.tv unless one does not exist. commit 1791e46c8602f40bb56fe0cf7ecb0607f35b4b12 Author: Dan Boehm Date: Wed Apr 23 15:13:14 2014 -0500 Couchpotato now downloads extrathumbs from the extra tmdb backdrops if they exist. This commit made some major changes to the core image creation functionality that makes writing multiple images to folders possible. commit c0858807873749dbc928c0260037138f51f894ca Author: Dan Boehm Date: Wed Apr 23 12:18:53 2014 -0500 Bug Fix & Implemented functionality to select bluray or dvd disc images. Currently, only blurays will be selected, unless there are no blurays. However, if a mechanism for determining the quality of the release is implemented, it would be simple to make this selection based on the quality. commit 786751371d243f53d0f5c6f2c38d92288d8608ba Author: Dan Boehm Date: Wed Apr 23 10:59:25 2014 -0500 Fixed a bug where non-HD clearart and logos couldn't be downloaded. commit feda8df483d13b5a5df3a869f25de8f2c7e6ffe3 Author: Dan Boehm Date: Wed Apr 23 10:12:31 2014 -0500 Fixed some problems that were missed with the previous merge. commit 5ddab6c40e69a5accc6c0336cd7485920ff82d8f Merge: 7273abf ff46aa0 Author: Dan Boehm Date: Wed Apr 23 10:02:11 2014 -0500 Merge branch 'develop' into fanarttv Conflicts: couchpotato/core/media/movie/providers/info/themoviedb.py couchpotato/core/providers/metadata/xbmc/__init__.py commit 7273abf827735cf245711c3d3199a6a173a964aa Author: dan Date: Thu Feb 27 13:29:57 2014 -0600 Downloads extra artwork from fanart.tv Downloads occur with correct filenaming when XBMC metadata is generated, but the image URLs are selected when the movie.info event is called. commit 9080d9d749c7e1ddbdc78f7b37a3c5f83195d580 Author: dan Date: Wed Feb 26 16:31:37 2014 -0600 Added basic functionality for fanarttv provider. This should be mostly done and is based on the tvdb provider. commit 1b39b246c2a9d65f9ef86c4e150a12d893e362c0 Author: dan Date: Wed Feb 26 14:50:17 2014 -0600 Updated fanarttv library with the correct package hierarchy (libs.fanarttv). commit 8abb7c8f8ad3347900debb9f6a6d5a7acb7df396 Author: dan Date: Wed Feb 26 13:12:48 2014 -0600 Added fanart.tv API python library (lib.fanarttv). The upstream for this library is at https://github.com/z4r/python-fanart. --- couchpotato/core/notifications/xbmc.py | 78 ++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/notifications/xbmc.py b/couchpotato/core/notifications/xbmc.py index 28439a75..db343068 100644 --- a/couchpotato/core/notifications/xbmc.py +++ b/couchpotato/core/notifications/xbmc.py @@ -3,6 +3,8 @@ import json import socket import traceback import urllib +import time +import os from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog @@ -36,7 +38,7 @@ class XBMC(Notification): if self.use_json_notifications.get(host): calls = [ - ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), + ('GUI.ShowNotification', None, {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ] if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): @@ -44,7 +46,7 @@ class XBMC(Notification): if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])): param = {'directory': data['destination_dir']} - calls.append(('VideoLibrary.Scan', param)) + calls.append(('VideoLibrary.Scan', None, param)) max_successful += len(calls) response = self.request(host, calls) @@ -66,6 +68,50 @@ class XBMC(Notification): except: log.error('Failed parsing results: %s', traceback.format_exc()) + + if self.conf('run_artwork_downloader') and data and self.use_json_notifications.get(host): + time.sleep(self.conf('run_artwork_downloader_delay')) + + if self.conf('force_full_scan'): + calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader'})] + max_successful += len(calls) + response = self.request(host, calls) + + try: + if response[0].get('result') and result['result'] == 'OK': + successful += 1 + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) + else: + calls = [('VideoLibrary.GetMovies', 'libMovies', {"filter":{"field": "title", "operator": "is", "value": data['media']['title'], "year": data['media']['info']['year']}})] + max_successful += len(calls) + response = self.request(host, calls) + + dbid = None + try: + if response[0].get('result'): + successful += 1 + dbid = response[0]['result']['movies'][-1]['movieid'] + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) + + if dbid is not None: + calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader', 'params':{'media_type': 'movie', 'dbid': str(dbid)}})] + + max_successful += len(calls) + response = self.request(host, calls) + + try: + if response[0].get('result') and result['result'] == 'OK': + successful += 1 + elif response[0].get('error'): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + except: + log.error('Failed parsing results: %s', traceback.format_exc()) return successful == max_successful @@ -75,7 +121,7 @@ class XBMC(Notification): # XBMC JSON-RPC version request response = self.request(host, [ - ('JSONRPC.Version', {}) + ('JSONRPC.Version', None, {}) ]) for result in response: if result.get('result') and type(result['result']['version']).__name__ == 'int': @@ -112,7 +158,7 @@ class XBMC(Notification): self.use_json_notifications[host] = True # send the text message - resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) + resp = self.request(host, [('GUI.ShowNotification', None, {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) for r in resp: if r.get('result') and r['result'] == 'OK': log.debug('Message delivered successfully!') @@ -184,12 +230,16 @@ class XBMC(Notification): data = [] for req in do_requests: - method, kwargs = req + method, id, kwargs = req + + if id is None: + id = method + data.append({ 'method': method, 'params': kwargs, 'jsonrpc': '2.0', - 'id': method, + 'id': id, }) data = json.dumps(data) @@ -273,6 +323,22 @@ config = [{ 'advanced': True, 'description': 'Also send message when movie is snatched.', }, + { + 'name': 'run_artwork_downloader', + 'label': 'Run the Artwork Downloader', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Runs the Artwork Downloader script to initialize/download artwork. (Requires the Artwork Downloader addon and XBMC Frodo or later)' + }, + { + 'name': 'run_artwork_downloader_delay', + 'label': 'Artwork Downloader delay', + 'default': 5, + 'type': 'int', + 'advanced': True, + 'description': 'Number of seconds to wait to start the Artwork Downloader script after notifying XBMC.', + }, ], } ], From df03409d7a60c9f20e41fcf310c27178e788b10c Mon Sep 17 00:00:00 2001 From: harrv Date: Sun, 27 Apr 2014 00:47:38 -0600 Subject: [PATCH 251/468] Added an mpaa_only rename replacement token The mpaa replacement token includes certifications from around the world. If the user wishes to limit the values to one of 'G', 'PG', 'PG-13', 'R', 'NC-17' or 'Not Rated' they can use the added mpaa_only replacement token. The original mpaa replacement token remains unchanged. --- couchpotato/core/plugins/renamer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 5424874b..363e81d0 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -314,8 +314,12 @@ class Renamer(Plugin): 'cd': '', 'cd_nr': '', 'mpaa': media['info'].get('mpaa', ''), + 'mpaa_only': media['info'].get('mpaa', ''), 'category': category_label, } + + if replacements['mpaa_only'] not in ('G', 'PG', 'PG-13', 'R', 'NC-17'): + replacements['mpaa_only'] = 'Not Rated' for file_type in group['files']: @@ -1207,7 +1211,8 @@ rename_options = { 'imdb_id': 'IMDB id (tt0123456)', 'cd': 'CD number (cd1)', 'cd_nr': 'Just the cd nr. (1)', - 'mpaa': 'MPAA Rating', + 'mpaa': 'MPAA or other certification', + 'mpaa_only': 'MPAA only certification (G|PG|PG-13|R|NC-17|Not Rated)', 'category': 'Category label', }, } From 860b6793fb5dbb090f03429bf8b861c488924969 Mon Sep 17 00:00:00 2001 From: Ricardo Amaral Date: Sat, 3 May 2014 17:37:13 +0100 Subject: [PATCH 252/468] Prevent duplicate subtitle language identifier --- couchpotato/core/plugins/renamer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 5424874b..21500624 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -412,8 +412,12 @@ class Renamer(Plugin): # Don't add language if multiple languages in 1 subtitle file if len(sub_langs) == 1: - sub_name = sub_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext'])) - rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) + sub_suffix = '%s.%s' % (sub_langs[0], replacements['ext']) + + # Don't add language to subtitle file it it's already there + if not sub_name.endswith(sub_suffix): + sub_name = sub_name.replace(replacements['ext'], sub_suffix) + rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) rename_files = mergeDicts(rename_files, rename_extras) From 84f28f3c54c761cc63360bd659a9865889049a5e Mon Sep 17 00:00:00 2001 From: Ricardo Amaral Date: Sat, 3 May 2014 17:50:12 +0100 Subject: [PATCH 253/468] Add advanced option to force download all languages --- couchpotato/core/plugins/subtitle.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/subtitle.py b/couchpotato/core/plugins/subtitle.py index 9fd7ef16..fdb640b1 100644 --- a/couchpotato/core/plugins/subtitle.py +++ b/couchpotato/core/plugins/subtitle.py @@ -32,7 +32,7 @@ class Subtitle(Plugin): for lang in self.getLanguages(): if lang not in available_languages: - download = subliminal.download_subtitles(files, multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir')) + download = subliminal.download_subtitles(files, multi = True, force = self.conf('force'), languages = [lang], services = self.services, cache_dir = Env.get('cache_dir')) for subtitle in download: downloaded.extend(download[subtitle]) @@ -72,6 +72,14 @@ config = [{ 'name': 'languages', 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'), }, + { + 'advanced': True, + 'name': 'force', + 'label': 'Force', + 'description': ('Force download all languages (including embedded).', 'This will also overwrite all existing subtitles.'), + 'default': False, + 'type': 'bool', + }, ], }, ], From 5260f42378c5a764d125ccdc74c5723636473a0f Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 4 May 2014 10:02:29 +0200 Subject: [PATCH 254/468] Fix Yifi this should fix #3114 --- couchpotato/core/media/_base/providers/torrent/yify.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index a6941004..b2010afa 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -43,12 +43,10 @@ class Base(TorrentProvider): try: for result in data.get('MovieList'): - try: - title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._') - title = title.replace('.-.', '-') - title = title.replace('..', '.') - except: - continue + if result['quality'] and result['quality'] not in result['MovieTitle']: + title = result['MovieTitle'] + ' ' + result['quality'] + else: + title = result['MovieTitle'] results.append({ 'id': result['MovieID'], From d6a86e8616698e09d72e691d8203e193a12af43a Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 4 May 2014 10:11:25 +0200 Subject: [PATCH 255/468] correct caps --- couchpotato/core/media/_base/providers/torrent/yify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index b2010afa..ddd4f49a 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -43,8 +43,8 @@ class Base(TorrentProvider): try: for result in data.get('MovieList'): - if result['quality'] and result['quality'] not in result['MovieTitle']: - title = result['MovieTitle'] + ' ' + result['quality'] + if result['Quality'] and result['Quality'] not in result['MovieTitle']: + title = result['MovieTitle'] + ' ' + result['Quality'] else: title = result['MovieTitle'] From 8c2960e8914cb67245575f12b2e84012a1f540a7 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 4 May 2014 11:14:54 +0200 Subject: [PATCH 256/468] Add Blu-ray to Bit HDTV provider Fixes #3171 --- couchpotato/core/media/_base/providers/torrent/bithdtv.py | 2 +- couchpotato/core/media/movie/providers/torrent/bithdtv.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py index 94aafcd1..bb12c66a 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -25,7 +25,7 @@ class Base(TorrentProvider): def _search(self, media, quality, results): - query = self.buildUrl(media) + query = self.buildUrl(media, quality) url = "%s&%s" % (self.urls['search'], query) diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv.py b/couchpotato/core/media/movie/providers/torrent/bithdtv.py index e7b16af9..da6954c8 100644 --- a/couchpotato/core/media/movie/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/movie/providers/torrent/bithdtv.py @@ -10,10 +10,14 @@ autoload = 'BiTHDTV' class BiTHDTV(MovieProvider, Base): + cat_ids = [ + ([2], ['bd50']), + ] + cat_backup_id = 7 # Movies - def buildUrl(self, media): + def buildUrl(self, media, quality): query = tryUrlencode({ 'search': fireEvent('library.query', media, single = True), - 'cat': 7 # Movie cat + 'cat': self.getCatId(quality)[0] }) return query From ffc99cd4f4d23d0e21d7c87d72ef7478cf907da0 Mon Sep 17 00:00:00 2001 From: mikke89 Date: Sun, 4 May 2014 23:20:01 +0200 Subject: [PATCH 257/468] Fix false positives from Sceneaccess and Torrentday --- .../core/media/_base/providers/torrent/sceneaccess.py | 4 ++-- .../core/media/_base/providers/torrent/torrentday.py | 7 ++++--- .../core/media/movie/providers/torrent/sceneaccess.py | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py index 2622e318..e488b63b 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -24,9 +24,9 @@ class Base(TorrentProvider): http_time_between_calls = 1 # Seconds - def _search(self, media, quality, results): + def _searchOnTitle(self, title, media, quality, results): - url = self.buildUrl(media, quality) + url = self.buildUrl(title, media, quality) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index bcc0593b..0f16dfd7 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -1,3 +1,4 @@ +from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -18,16 +19,16 @@ class Base(TorrentProvider): http_time_between_calls = 1 # Seconds - def _search(self, media, quality, results): + def _searchOnTitle(self, title, media, quality, results): - query = self.buildUrl(media) + query = '%s %s' % (title, media['info']['year']) data = { '/browse.php?': None, 'cata': 'yes', 'jxt': 8, 'jxw': 'b', - 'search': query, + 'search': tryUrlencode(query), } data = self.getJsonData(self.urls['search'], data = data) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py index dee4b32c..99157d73 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -17,13 +17,13 @@ class SceneAccess(MovieProvider, Base): ([8], ['dvdr']), ] - def buildUrl(self, media, quality): + def buildUrl(self, title, media, quality): cat_id = self.getCatId(quality)[0] url = self.urls['search'] % (cat_id, cat_id) arguments = tryUrlencode({ - 'search': fireEvent('library.query', media, single = True), - 'method': 3, + 'search': '%s %s' % (title, media['info']['year']), + 'method': 2, }) query = "%s&%s" % (url, arguments) From bcd2d22fbf98bd9f5df9a8255ed6482d8f78780c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 May 2014 20:38:47 +0200 Subject: [PATCH 258/468] Update bitsoup searchOnTitle --- .../core/media/_base/providers/torrent/bitsoup.py | 9 +-------- .../core/media/movie/providers/torrent/bitsoup.py | 8 ++------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index 9aeb6243..1c762b77 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -1,7 +1,6 @@ import traceback from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -24,13 +23,7 @@ class Base(TorrentProvider): def _searchOnTitle(self, title, movie, quality, results): - q = '"%s" %s' % (simplifyString(title), movie['info']['year']) - arguments = tryUrlencode({ - 'search': q, - }) - url = "%s&%s" % (self.urls['search'], arguments) - - url = self.urls['search'] % self.buildUrl(movie, quality) + url = self.urls['search'] % self.buildUrl(title, movie, quality) data = self.getHTMLData(url) if data: diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py index c351ab9f..e9d69fe5 100644 --- a/couchpotato/core/media/movie/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py @@ -1,6 +1,5 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.event import fireEvent from couchpotato.core.media._base.providers.torrent.bitsoup import Base from couchpotato.core.media.movie.providers.base import MovieProvider @@ -18,12 +17,9 @@ class Bitsoup(MovieProvider, Base): ] cat_backup_id = 0 - def buildUrl(self, media, quality): + def buildUrl(self, title, media, quality): query = tryUrlencode({ - 'search': '"%s" %s' % ( - fireEvent('library.query', media, include_year = False, single = True), - media['info']['year'] - ), + 'search': '"%s" %s' % (title, media['info']['year']), 'cat': self.getCatId(quality)[0], }) return query From 8e35c02763c719d9e2e9812567a73810848ae3dd Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 May 2014 21:18:30 +0200 Subject: [PATCH 259/468] Cleanup searchOnTitle queries --- couchpotato/core/media/_base/providers/torrent/torrentday.py | 2 +- couchpotato/core/media/movie/providers/torrent/iptorrents.py | 2 +- couchpotato/core/media/movie/providers/torrent/sceneaccess.py | 2 +- couchpotato/core/media/movie/providers/torrent/torrentday.py | 4 ---- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index 0f16dfd7..9304e552 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -21,7 +21,7 @@ class Base(TorrentProvider): def _searchOnTitle(self, title, media, quality, results): - query = '%s %s' % (title, media['info']['year']) + query = '"%s" %s' % (title, media['info']['year']) data = { '/browse.php?': None, diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index f06aadfc..89aeee80 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -18,6 +18,6 @@ class IPTorrents(MovieProvider, Base): ] def buildUrl(self, title, media, quality): - query = '%s %s' % (title.replace(':', ''), media['info']['year']) + query = '"%s" %s' % (title.replace(':', ''), media['info']['year']) return self._buildUrl(query, quality) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py index 99157d73..3eed04dd 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -22,7 +22,7 @@ class SceneAccess(MovieProvider, Base): url = self.urls['search'] % (cat_id, cat_id) arguments = tryUrlencode({ - 'search': '%s %s' % (title, media['info']['year']), + 'search': '"%s" %s' % (title, media['info']['year']), 'method': 2, }) query = "%s&%s" % (url, arguments) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday.py b/couchpotato/core/media/movie/providers/torrent/torrentday.py index 872a7d17..768d3043 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentday.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentday.py @@ -1,5 +1,4 @@ from couchpotato.core.logger import CPLog -from couchpotato.core.event import fireEvent from couchpotato.core.media._base.providers.torrent.torrentday import Base from couchpotato.core.media.movie.providers.base import MovieProvider @@ -16,6 +15,3 @@ class TorrentDay(MovieProvider, Base): ([3], ['dvdr']), ([5], ['bd50']), ] - - def buildUrl(self, media): - return fireEvent('library.query', media, single = True) From 07eb1f7f4c266ac754d4a2238f436410f23218a2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 May 2014 22:01:11 +0200 Subject: [PATCH 260/468] Allow page ordering --- .../_base/static/{2_manage.js => manage.js} | 1 + .../_base/static/{1_wanted.js => wanted.js} | 1 + couchpotato/core/plugins/log/static/log.js | 1 + .../plugins/userscript/static/userscript.js | 1 + .../core/plugins/wizard/static/wizard.js | 1 + couchpotato/static/scripts/couchpotato.js | 21 ++++++++++++++++--- couchpotato/static/scripts/page.js | 6 ++++++ couchpotato/static/scripts/page/settings.js | 3 ++- 8 files changed, 31 insertions(+), 4 deletions(-) rename couchpotato/core/media/movie/_base/static/{2_manage.js => manage.js} (99%) rename couchpotato/core/media/movie/_base/static/{1_wanted.js => wanted.js} (99%) diff --git a/couchpotato/core/media/movie/_base/static/2_manage.js b/couchpotato/core/media/movie/_base/static/manage.js similarity index 99% rename from couchpotato/core/media/movie/_base/static/2_manage.js rename to couchpotato/core/media/movie/_base/static/manage.js index be99a845..e8618999 100644 --- a/couchpotato/core/media/movie/_base/static/2_manage.js +++ b/couchpotato/core/media/movie/_base/static/manage.js @@ -2,6 +2,7 @@ Page.Manage = new Class({ Extends: PageBase, + order: 20, name: 'manage', title: 'Do stuff to your existing movies!', diff --git a/couchpotato/core/media/movie/_base/static/1_wanted.js b/couchpotato/core/media/movie/_base/static/wanted.js similarity index 99% rename from couchpotato/core/media/movie/_base/static/1_wanted.js rename to couchpotato/core/media/movie/_base/static/wanted.js index ca57f957..461a64e1 100644 --- a/couchpotato/core/media/movie/_base/static/1_wanted.js +++ b/couchpotato/core/media/movie/_base/static/wanted.js @@ -2,6 +2,7 @@ Page.Wanted = new Class({ Extends: PageBase, + order: 10, name: 'wanted', title: 'Gimmy gimmy gimmy!', folder_browser: null, diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index c5effeba..a08b105b 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -2,6 +2,7 @@ Page.Log = new Class({ Extends: PageBase, + order: 60, name: 'log', title: 'Show recent logs.', has_tab: false, diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 2ef1ea6a..d8caeb3f 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -2,6 +2,7 @@ Page.Userscript = new Class({ Extends: PageBase, + order: 80, name: 'userscript', has_tab: false, diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index b43e3985..fdd7b743 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -2,6 +2,7 @@ Page.Wizard = new Class({ Extends: Page.Settings, + order: 70, name: 'wizard', has_tab: false, wizard_only: true, diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index ffc0e9aa..e08e2c3d 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -137,19 +137,34 @@ createPages: function(){ var self = this; + var pages = []; Object.each(Page, function(page_class, class_name){ var pg = new Page[class_name](self, {}); self.pages[class_name] = pg; - self.fireEvent('load'+class_name); - - $(pg).inject(self.content); + pages.include({ + 'order': pg.order, + 'name': class_name, + 'class': pg + }); }); + pages.stableSort(self.sortPageByOrder).each(function(page){ + page['class'].load(); + self.fireEvent('load'+page.name); + $(page['class']).inject(self.content); + }); + + delete pages; + self.fireEvent('load'); }, + sortPageByOrder: function(a, b){ + return (a.order || 100) - (b.order || 100) + }, + openPage: function(url) { var self = this; diff --git a/couchpotato/static/scripts/page.js b/couchpotato/static/scripts/page.js index 58ba5acd..57b8b108 100644 --- a/couchpotato/static/scripts/page.js +++ b/couchpotato/static/scripts/page.js @@ -6,6 +6,7 @@ var PageBase = new Class({ }, + order: 1, has_tab: true, name: '', @@ -16,6 +17,10 @@ var PageBase = new Class({ // Create main page container self.el = new Element('div.page.'+self.name); + }, + + load: function(){ + var self = this; // Create tab for page if(self.has_tab){ @@ -26,6 +31,7 @@ var PageBase = new Class({ 'text': self.name.capitalize() }); } + }, open: function(action, params){ diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 09155486..de5ffcbc 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -2,6 +2,7 @@ Page.Settings = new Class({ Extends: PageBase, + order: 50, name: 'settings', title: 'Change settings.', wizard_only: false, @@ -112,7 +113,7 @@ Page.Settings = new Class({ }, sortByOrder: function(a, b){ - return (a.order || 100) - (b.order || 100) + return (a.order || 100) - (b.order || 100) }, create: function(json){ From 0def6fcfe37ef2a6cd86468f2239c057dcb9a4d3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 May 2014 22:36:41 +0200 Subject: [PATCH 261/468] Cleanup PR --- couchpotato/core/media/_base/searcher/main.py | 11 ++++++++--- .../core/media/movie/_base/static/movie.actions.js | 4 ++-- couchpotato/core/media/movie/_base/static/movie.js | 11 +++++++++++ couchpotato/core/plugins/quality/main.py | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 8f99ceae..e45ffb3f 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -87,17 +87,17 @@ class Searcher(SearcherBase): def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None): if not preferred_quality: preferred_quality = {} + found = {} + # Try guessing via quality tags guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), single = True) if guess: - return not guess['identifier'] == preferred_quality.get('identifier') - + found[guess['identifier']] = True # Hack for older movies that don't contain quality tag name = nzb['name'] size = nzb.get('size', 0) - found = {} year_name = fireEvent('scanner.name_year', name, single = True) if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): if size > 20000: # Assume bd50 @@ -110,6 +110,11 @@ class Searcher(SearcherBase): log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size) found['dvdrip'] = True + # Allow other qualities + for allowed in preferred_quality.get('allow'): + if found.get(allowed): + del found[allowed] + return not (found.get(preferred_quality['identifier']) and len(found) == 1) def correct3D(self, nzb, preferred_quality = None): diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 722855cc..8ac3fe03 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -78,7 +78,7 @@ MA.IMDB = new Class({ create: function(){ var self = this; - self.id = self.movie.get('imdb'); + self.id = self.movie.getIdentifier(); self.el = new Element('a.imdb', { 'title': 'Go to the IMDB page of ' + self.getTitle(), @@ -703,7 +703,7 @@ MA.Readd = new Class({ Api.request('movie.add', { 'data': { - 'identifier': self.movie.get('imdb'), + 'identifier': self.movie.getIdentifier(), 'ignore_previous': 1 } }); diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 86205efc..1a547f2f 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -300,6 +300,17 @@ var Movie = new Class({ self.el.removeClass(self.view+'_view') }, + getIdentifier: function(){ + var self = this; + + try { + return self.get('identifiers').imdb; + } + catch (e){ } + + return self.get('imdb'); + }, + get: function(attr){ return this.data[attr] || this.data.info[attr] }, diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 394ee2df..1e15be19 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -61,7 +61,7 @@ class QualityPlugin(Plugin): addEvent('app.initialize', self.fill, priority = 10) - addEvent('app.load', self.doTest) + addEvent('app.test', self.doTest) self.order = [] self.addOrder() From 16f603ced28c2b9ae0ea1ebc128cd7c206c5efe8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 May 2014 22:42:14 +0200 Subject: [PATCH 262/468] Cast float --- couchpotato/core/media/_base/providers/nzb/binsearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index fbb67bfc..84c7b314 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -50,8 +50,8 @@ class Base(NZBProvider): def extra_check(item): parts = re.search('available:.(?P\d+)./.(?P\d+)', info.text) - total = tryInt(parts.group('total')) - parts = tryInt(parts.group('parts')) + total = float(tryInt(parts.group('total'))) + parts = float(tryInt(parts.group('parts'))) if (total / parts) < 1 and ((total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower()))): log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total)) From 0284fa9b0a11da37276734e0809ee16de1a77a0d Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 14:31:24 +0200 Subject: [PATCH 263/468] Load correct beautifulsoup module --- .../core/media/movie/providers/userscript/filmstarts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/userscript/filmstarts.py b/couchpotato/core/media/movie/providers/userscript/filmstarts.py index 1e35be27..59027e03 100644 --- a/couchpotato/core/media/movie/providers/userscript/filmstarts.py +++ b/couchpotato/core/media/movie/providers/userscript/filmstarts.py @@ -1,8 +1,9 @@ -from BeautifulSoup import BeautifulSoup +from bs4 import BeautifulSoup from couchpotato.core.media._base.providers.userscript.base import UserscriptBase autoload = 'Filmstarts' + class Filmstarts(UserscriptBase): includes = ['*://www.filmstarts.de/kritiken/*'] From 7e4bc29b599f0ea25f88109da1766c73ef0103e0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 15:39:41 +0200 Subject: [PATCH 264/468] Chart cleanup --- .../media/movie/_base/static/movie.actions.js | 2 +- .../media/movie/providers/automation/imdb.py | 95 +++++++++++-------- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 8ac3fe03..26ee2421 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -78,7 +78,7 @@ MA.IMDB = new Class({ create: function(){ var self = this; - self.id = self.movie.getIdentifier(); + self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb'); self.el = new Element('a.imdb', { 'title': 'Go to the IMDB page of ' + self.getTitle(), diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index c0c2de88..092c2718 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -100,23 +100,28 @@ class IMDBAutomation(IMDBBase): enabled_option = 'automation_providers_enabled' - chart_urls = { - 'theater': 'http://www.imdb.com/movies-in-theaters/', - 'top250': 'http://www.imdb.com/chart/top', - 'boxoffice': 'http://www.imdb.com/chart/', - 'rentals': 'http://www.imdb.com/boxoffice/rentals', - } - chart_names = { - 'theater': 'IMDB - Movies in Theaters', - 'top250': 'IMDB - Top 250 Movies', - 'boxoffice': 'IMDB - Box Office', - 'rentals': 'IMDB - Top DVD rentals', - } - chart_order = { - 'theater': 2, - 'top250': 5, - 'boxoffice': 3, - 'rentals': 4, + charts = { + 'theater': { + 'order': 1, + 'name': 'IMDB - Movies in Theaters', + 'url': 'http://www.imdb.com/movies-in-theaters/', + }, + 'boxoffice': { + 'order': 2, + 'name': 'IMDB - Box Office', + 'url': 'http://www.imdb.com/chart/', + }, + 'rentals': { + 'order': 3, + 'name': 'IMDB - Top DVD rentals', + 'url': 'http://m.imdb.com/boxoffice_json', + 'type': 'json', + }, + 'top250': { + 'order': 4, + 'name': 'IMDB - Top 250 Movies', + 'url': 'http://www.imdb.com/chart/top', + }, } first_table = ['boxoffice'] @@ -125,23 +130,30 @@ class IMDBAutomation(IMDBBase): movies = [] - for url in self.chart_urls: - if self.conf('automation_charts_%s' % url): - data = self.getHTMLData(self.chart_urls[url]) + for name in self.charts: + chart = self.charts[name] + url = chart.get('url') + + if self.conf('automation_charts_%s' % name): + data = self.getHTMLData(url) + if data: - html = BeautifulSoup(data) try: - result_div = html.find('div', attrs = {'id': 'main'}) - try: - if url in self.first_table: - table = result_div.find('table') - result_div = table if table else result_div - except: - pass + if chart.get('type', 'html') == 'html': + result_div = html.find('table') - imdb_ids = getImdb(str(result_div), multiple = True) + try: + if url in self.first_table: + table = result_div.find('table') + result_div = table if table else result_div + except: + pass + + imdb_ids = getImdb(str(result_div), multiple = True) + else: + imdb_ids = getImdb(str(data), multiple = True) for imdb_id in imdb_ids: info = self.getInfo(imdb_id) @@ -158,14 +170,19 @@ class IMDBAutomation(IMDBBase): def getChartList(self): + # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) movie_lists = [] - max_items = int(self.conf('max_items', section='charts', default=5)) + max_items = int(self.conf('max_items', section = 'charts', default=5)) - for url in self.chart_urls: - if self.conf('chart_display_%s' % url): - movie_list = {'name': self.chart_names[url], 'url': self.chart_urls[url], 'order': self.chart_order[url], 'list': []} - data = self.getHTMLData(self.chart_urls[url]) + for name in self.charts: + chart = self.charts[name].copy() + + if self.conf('chart_display_%s' % name): + + chart['list'] = [] + + data = self.getHTMLData(chart.get('url')) if data: html = BeautifulSoup(data) @@ -173,7 +190,7 @@ class IMDBAutomation(IMDBBase): result_div = html.find('div', attrs = {'id': 'main'}) try: - if url in self.first_table: + if chart.get('url') in self.first_table: table = result_div.find('table') result_div = table if table else result_div except: @@ -183,15 +200,15 @@ class IMDBAutomation(IMDBBase): for imdb_id in imdb_ids[0:max_items]: info = self.getInfo(imdb_id) - movie_list['list'].append(info) + chart['list'].append(info) if self.shuttingDown(): break except: - log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) + log.error('Failed loading IMDB chart results from %s: %s', (chart.get('url'), traceback.format_exc())) - if movie_list['list']: - movie_lists.append(movie_list) + if chart['list']: + movie_lists.append(chart) return movie_lists From 66a149590bf2fe5a5ad351e12ac139e86a3a76bc Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 15:51:09 +0200 Subject: [PATCH 265/468] Chart fixes --- .../media/movie/providers/automation/imdb.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 092c2718..e9f06b17 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -138,11 +138,11 @@ class IMDBAutomation(IMDBBase): data = self.getHTMLData(url) if data: - try: + html = BeautifulSoup(data) if chart.get('type', 'html') == 'html': - result_div = html.find('table') + result_div = html.find('div', attrs = {'id': 'main'}) try: if url in self.first_table: @@ -177,26 +177,31 @@ class IMDBAutomation(IMDBBase): for name in self.charts: chart = self.charts[name].copy() + url = chart.get('url') if self.conf('chart_display_%s' % name): chart['list'] = [] - data = self.getHTMLData(chart.get('url')) + data = self.getHTMLData(url) if data: html = BeautifulSoup(data) try: - result_div = html.find('div', attrs = {'id': 'main'}) - try: - if chart.get('url') in self.first_table: - table = result_div.find('table') - result_div = table if table else result_div - except: - pass + if chart.get('type', 'html') == 'html': + result_div = html.find('div', attrs = {'id': 'main'}) - imdb_ids = getImdb(str(result_div), multiple = True) + try: + if url in self.first_table: + table = result_div.find('table') + result_div = table if table else result_div + except: + pass + + imdb_ids = getImdb(str(result_div), multiple = True) + else: + imdb_ids = getImdb(str(data), multiple = True) for imdb_id in imdb_ids[0:max_items]: info = self.getInfo(imdb_id) @@ -205,7 +210,7 @@ class IMDBAutomation(IMDBBase): if self.shuttingDown(): break except: - log.error('Failed loading IMDB chart results from %s: %s', (chart.get('url'), traceback.format_exc())) + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) if chart['list']: movie_lists.append(chart) From 377fdd9e5e06dc3dd703ff4b9d123437e6414cdf Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 16:08:36 +0200 Subject: [PATCH 266/468] Use correct event --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 66bf20a2..d32a6362 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -453,7 +453,7 @@ class Renamer(Plugin): try: if media.get('status') == 'active' and media.get('profile_id'): profile = db.get('id', media['profile_id']) - if fireEvent('release.isfinish', group['meta_data']['quality'], profile): + if fireEvent('quality.isfinish', group['meta_data']['quality'], profile, single = True): mdia = db.get('id', media['_id']) mdia['status'] = 'done' mdia['last_edit'] = int(time.time()) From 192c0200e56378cfafe1218fae0af0dcc5ca7e96 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 20:06:06 +0200 Subject: [PATCH 267/468] Disable top 250 chart --- couchpotato/core/media/movie/providers/automation/imdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index e9f06b17..220331d2 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -277,7 +277,7 @@ config = [{ 'type': 'bool', 'label': 'TOP 250', 'description': 'IMDB TOP 250 chart', - 'default': True, + 'default': False, }, { 'name': 'automation_charts_boxoffice', From 96c94f97f49902c1da3b284774f64adae0d2c15c Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 20:35:00 +0200 Subject: [PATCH 268/468] Filter out tvshows in charts --- couchpotato/core/media/movie/charts/main.py | 1 - .../core/media/movie/providers/automation/bluray.py | 7 +++++++ couchpotato/core/media/movie/providers/automation/imdb.py | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py index e3c46029..58133d83 100644 --- a/couchpotato/core/media/movie/charts/main.py +++ b/couchpotato/core/media/movie/charts/main.py @@ -36,7 +36,6 @@ class Charts(Plugin): 'charts': charts } - def updateViewCache(self): if self.update_in_progress: diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index 630d0cbb..0501c601 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -1,4 +1,5 @@ from bs4 import BeautifulSoup +from couchpotato import fireEvent from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog @@ -96,8 +97,14 @@ class Bluray(Automation, RSS): movie = self.search(name, year) if movie: + if movie.get('imdb') in movie_ids: continue + + is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True) + if not is_movie: + continue + movie_ids.append(movie.get('imdb')) movie_list['list'].append( movie ) if len(movie_list['list']) >= max_items: diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 220331d2..b52816a8 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -168,7 +168,6 @@ class IMDBAutomation(IMDBBase): return movies - def getChartList(self): # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) @@ -204,6 +203,11 @@ class IMDBAutomation(IMDBBase): imdb_ids = getImdb(str(data), multiple = True) for imdb_id in imdb_ids[0:max_items]: + + is_movie = fireEvent('movie.is_movie', identifier = imdb_id, single = True) + if not is_movie: + continue + info = self.getInfo(imdb_id) chart['list'].append(info) From f3b0346ba2621d8fb808354dc1d858139def3541 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 21:38:08 +0200 Subject: [PATCH 269/468] Use encoding as backup --- couchpotato/core/media/_base/providers/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 2b668931..6a8e59bb 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -88,10 +88,14 @@ class Provider(Plugin): if data and len(data) > 0: try: - data = XMLTree.fromstring(ss(data)) + data = XMLTree.fromstring(data) return self.getElements(data, item_path) except: - log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + try: + data = XMLTree.fromstring(ss(data)) + return self.getElements(data, item_path) + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) return [] From 5ab10ff97ac23ded626a2d30667c923d931b4ba5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 22:24:58 +0200 Subject: [PATCH 270/468] Change dognzb default url --- couchpotato/core/media/_base/providers/nzb/newznab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 31223103..b0f5d160 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -244,7 +244,7 @@ config = [{ }, { 'name': 'host', - 'default': 'api.nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', + 'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', 'description': 'The hostname of your newznab provider', }, { From ac8a13db22ddf06255fd4acb770e75ed4af116ce Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 May 2014 23:49:34 +0200 Subject: [PATCH 271/468] Remove orphaned releases --- couchpotato/core/media/_base/media/main.py | 9 ++++++-- couchpotato/core/plugins/release/main.py | 24 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 29a98fd1..2fc7348b 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -361,13 +361,18 @@ class MediaPlugin(MediaBase): media = db.get('id', media_id) if media: deleted = False + + media_releases = fireEvent('release.for_media', media['_id'], single = True) + if delete_from == 'all': + # Delete connected releases + for release in media_releases: + db.delete(release) + db.delete(media) deleted = True else: - media_releases = fireEvent('release.for_media', media['_id'], single = True) - total_releases = len(media_releases) total_deleted = 0 new_media_status = None diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 518cbce2..ad605e7c 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -3,6 +3,7 @@ import os import time import traceback +from CodernityDB.database import RecordDeleted from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent @@ -58,7 +59,7 @@ class Release(Plugin): # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanDone) - fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4) + fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 12) def cleanDone(self): log.debug('Removing releases from dashboard') @@ -68,6 +69,27 @@ class Release(Plugin): db = get_db() + # Get (and remove) parentless releases + releases = db.all('release', with_doc = True) + media_exist = [] + for release in releases: + if release.get('key') in media_exist: + continue + + try: + db.get('id', release.get('key')) + media_exist.append(release.get('key')) + except RecordDeleted: + db.delete(release['doc']) + log.debug('Deleted orphaned release: %s', release['doc']) + except: + log.debug('Failed cleaning up orphaned releases: %s', traceback.format_exc()) + + del media_exist + + # Reindex statuses + db.reindex_index('media_status') + # get movies last_edit more than a week ago medias = fireEvent('media.with_status', 'done', single = True) From 11126f808303a6a2ae539a31d25da2ae83a118de Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 7 May 2014 00:23:08 +0200 Subject: [PATCH 272/468] forceDefaults priority --- couchpotato/core/plugins/profile/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index a7fdcd41..640f9680 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -34,7 +34,7 @@ class ProfilePlugin(Plugin): }) addEvent('app.initialize', self.fill, priority = 90) - addEvent('app.load', self.forceDefaults) + addEvent('app.load', self.forceDefaults, priority = 110) def forceDefaults(self): From 8e220ededac07e23f65ffd3472f31760646156a5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 7 May 2014 00:25:18 +0200 Subject: [PATCH 273/468] Bitsoup: Allow param in search url --- couchpotato/core/media/_base/providers/torrent/bitsoup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index 1c762b77..f32e79e7 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -15,7 +15,7 @@ class Base(TorrentProvider): 'test': 'https://www.bitsoup.me/', 'login': 'https://www.bitsoup.me/takelogin.php', 'login_check': 'https://www.bitsoup.me/my.php', - 'search': 'https://www.bitsoup.me/browse.php?', + 'search': 'https://www.bitsoup.me/browse.php?%s', 'baseurl': 'https://www.bitsoup.me/%s', } From e6dfb3da16035a4599fad22f7a578e4893eb9638 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 7 May 2014 00:47:50 +0200 Subject: [PATCH 274/468] Allow last year films to be search after April --- couchpotato/core/media/movie/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index c41307fa..74b1ef79 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -289,7 +289,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): now_year = date.today().year now_month = date.today().month - if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): + if (year is None or year < now_year - 1 or (year <= now_year - 1 and now_month > 4)) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): return True else: From 7c4a59539a519d94bc0c12fa31e0f9dac60241e2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:50:04 +0200 Subject: [PATCH 275/468] Return brrip in Yify provider --- .../core/media/_base/providers/torrent/yify.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index ddd4f49a..82413fc5 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -35,7 +35,11 @@ class Base(TorrentProvider): def _search(self, movie, quality, results): - search_url = self.urls['search'] % (self.getDomain(), getIdentifier(movie), quality['identifier']) + domain = self.getDomain() + if not domain: + return + + search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier']) data = self.getJsonData(search_url) @@ -44,18 +48,18 @@ class Base(TorrentProvider): for result in data.get('MovieList'): if result['Quality'] and result['Quality'] not in result['MovieTitle']: - title = result['MovieTitle'] + ' ' + result['Quality'] + title = result['MovieTitle'] + ' BrRip ' + result['Quality'] else: - title = result['MovieTitle'] + title = result['MovieTitle'] + ' BrRip' results.append({ 'id': result['MovieID'], 'name': title, 'url': result['TorrentMagnetUrl'], - 'detail_url': self.urls['detail'] % (self.getDomain(), result['MovieID']), + 'detail_url': self.urls['detail'] % (domain, result['MovieID']), 'size': self.parseSize(result['Size']), 'seeders': tryInt(result['TorrentSeeds']), - 'leechers': tryInt(result['TorrentPeers']) + 'leechers': tryInt(result['TorrentPeers']), }) except: From b3b13899f100114fec7bb429297ee92367e1785c Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:50:59 +0200 Subject: [PATCH 276/468] Return found qualities --- couchpotato/core/media/_base/searcher/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index e45ffb3f..4e8dae2e 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -115,7 +115,10 @@ class Searcher(SearcherBase): if found.get(allowed): del found[allowed] - return not (found.get(preferred_quality['identifier']) and len(found) == 1) + if found.get(preferred_quality['identifier']) and len(found) == 1: + return False + + return found def correct3D(self, nzb, preferred_quality = None): if not preferred_quality: preferred_quality = {} From 3d277e1c014b6e408b3c5b0ce42e97ad37c9439a Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:51:12 +0200 Subject: [PATCH 277/468] Quality scoring and tests --- couchpotato/core/plugins/quality/main.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index e61aaea4..56598830 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -351,6 +351,10 @@ class QualityPlugin(Plugin): for allow in quality.get('allow', []): score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 + # Give panelty for all lower qualities + for q in self.qualities[self.order.index(quality.get('identifier'))+1:]: + score[q.get('identifier')]['score'] -= 1 + def isFinish(self, quality, profile): if not isinstance(profile, dict) or not profile.get('qualities'): return False @@ -406,8 +410,10 @@ class QualityPlugin(Plugin): 'Movie.Name.2004.GERMAN.AC3D.DL.1080p.BluRay.x264-Group': {'size': 8000, 'quality': '1080p'}, 'Movie.Name.2013.BR-Disk-Group.iso': {'size': 48000, 'quality': 'bd50'}, 'Movie.Name.2013.2D+3D.BR-Disk-Group.iso': {'size': 52000, 'quality': 'bd50', 'is_3d': True}, - 'Girl.Rising.2013.NTSC.DVD9-0MNiDVD': {'size': 7200, 'quality': 'dvdr'}, - 'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True} + 'Movie.Rising.Name.Girl.2011.NTSC.DVD9-GroupDVD': {'size': 7200, 'quality': 'dvdr'}, + 'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True}, + 'Movie Monuments 2013 BrRip 1080p': {'size': 1800, 'quality': 'brrip'}, + 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'}, } correct = 0 From 6172ce496038352c00a6c480f327c97853b01f55 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:51:26 +0200 Subject: [PATCH 278/468] Use contains other quality in log --- couchpotato/core/media/movie/searcher.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 74b1ef79..4fedd07c 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -231,8 +231,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) # Contains lower quality string - if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True): - log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) + contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True) + if contains_other: + log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other])) return False # Contains lower quality string From b37112600e1be69c764994cf7f8c8f759a189e70 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:51:42 +0200 Subject: [PATCH 279/468] Only cache ignored proxies for 1 day --- couchpotato/core/media/_base/providers/torrent/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/base.py b/couchpotato/core/media/_base/providers/torrent/base.py index 2ed496c5..9f5f2890 100644 --- a/couchpotato/core/media/_base/providers/torrent/base.py +++ b/couchpotato/core/media/_base/providers/torrent/base.py @@ -44,7 +44,8 @@ class TorrentProvider(YarrProvider): prop_name = 'proxy.%s' % proxy last_check = float(Env.prop(prop_name, default = 0)) - if last_check > time.time() - 1209600: + + if last_check > time.time() - 86400: continue data = '' From 73e74881a6a3f1166369b9e07f36b6cff3fe326d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:51:53 +0200 Subject: [PATCH 280/468] Always return handler --- couchpotato/core/media/_base/media/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 2fc7348b..beb9dd02 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -107,8 +107,7 @@ class MediaPlugin(MediaBase): def handler(): fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id)) - if handler: - return handler + return handler except: log.error('Refresh handler for non existing media: %s', traceback.format_exc()) From 3e2a2c3beef9bbb57a7508ccca15665728a9f852 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:52:03 +0200 Subject: [PATCH 281/468] Remove unused variable --- couchpotato/core/media/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index c491c016..f0abd28c 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -25,7 +25,6 @@ class MediaBase(Plugin): def onComplete(): try: - db = get_db() media = fireEvent('media.get', media_id, single = True) event_name = '%s.searcher.single' % media.get('type') From 18a870f8c306a9dd1197384504bc7623394cb140 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 14:57:26 +0200 Subject: [PATCH 282/468] Log if no quality is found --- couchpotato/core/media/movie/searcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 4fedd07c..d68fcc5b 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -232,8 +232,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # Contains lower quality string contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True) - if contains_other: - log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other])) + if contains_other != False: + log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality')) return False # Contains lower quality string From 1bd556fbb306c35fde4dc929cfbbbf5e14754058 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 15:39:40 +0200 Subject: [PATCH 283/468] Close DB on shutdown --- couchpotato/core/_base/_core.py | 9 +++++++-- couchpotato/core/database.py | 4 ++++ couchpotato/runner.py | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 1ca7d878..ca45ceb0 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -90,7 +90,11 @@ class Core(Plugin): def shutdown(): self.initShutdown() - IOLoop.current().add_callback(shutdown) + + if IOLoop.current()._closing: + shutdown() + else: + IOLoop.current().add_callback(shutdown) return 'shutdown' @@ -139,7 +143,8 @@ class Core(Plugin): log.debug('Safe to shutdown/restart') try: - IOLoop.current().stop() + if not IOLoop.current()._closing: + IOLoop.current().stop() except RuntimeError: pass except: diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 532befae..881b81e1 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -28,6 +28,7 @@ class Database(object): addEvent('database.setup_index', self.setupIndex) addEvent('app.migrate', self.migrate) + addEvent('app.after_shutdown', self.close) def getDB(self): @@ -37,6 +38,9 @@ class Database(object): return self.db + def close(self, **kwargs): + self.getDB().close() + def setupIndex(self, index_name, klass): self.indexes.append(index_name) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index dcdac5c8..1684eddb 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -262,8 +262,14 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Go go go! from tornado.ioloop import IOLoop + from tornado.autoreload import add_reload_hook loop = IOLoop.current() + # Reload hook + def test(): + fireEvent('app.shutdown') + add_reload_hook(test) + # Some logging and fire load event try: log.info('Starting server on port %(port)s', config) except: pass From e7fbff5b3fd789b268165468457951c5b1de1f5a Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 15:40:24 +0200 Subject: [PATCH 284/468] Only remove non-existing releases only once --- couchpotato/core/media/movie/searcher.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index d68fcc5b..61c39b22 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -126,6 +126,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True) found_releases = [] + previous_releases = movie.get('releases', []) too_early_to_search = [] default_title = getTitle(movie) @@ -196,9 +197,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase): ret = True # Remove releases that aren't found anymore - for release in movie.get('releases', []): + temp_previous_releases = [] + for release in previous_releases: if release.get('status') == 'available' and release.get('identifier') not in found_releases: fireEvent('release.delete', release.get('_id'), single = True) + else: + temp_previous_releases.append(release) + previous_releases = temp_previous_releases + del temp_previous_releases # Break if CP wants to shut down if self.shuttingDown() or ret: From 923c794e3909c07e834fa0c60b0a3a2dd87f5a95 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:16:42 +0200 Subject: [PATCH 285/468] Logs return list --- couchpotato/core/plugins/log/main.py | 43 +++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index 2565398e..bcdd7494 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -1,9 +1,10 @@ import os +import re import traceback from couchpotato.api import addApiView from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -71,10 +72,11 @@ class Logging(Plugin): if current_path: f = open(current_path, 'r') log_content = f.read() + logs = self.toList(log_content) return { 'success': True, - 'log': toUnicode(log_content), + 'log': logs, 'total': total, } @@ -93,13 +95,14 @@ class Logging(Plugin): break f = open(path, 'r') - reversed_lines = toUnicode(f.read()).split('[0m\n') - reversed_lines.reverse() + log_content = toUnicode(f.read()) + raw_lines = self.toList(log_content) + raw_lines.reverse() brk = False - for line in reversed_lines: + for line in raw_lines: - if type == 'all' or '%s ' % type.upper() in line: + if type == 'all' or line.get('type') == type.upper(): log_lines.append(line) if len(log_lines) >= total_lines: @@ -112,9 +115,35 @@ class Logging(Plugin): log_lines.reverse() return { 'success': True, - 'log': '[0m\n'.join(log_lines), + 'log': log_lines, } + def toList(self, log_content = ''): + + logs_raw = toUnicode(log_content).split('[0m\n') + + logs = [] + for log in logs_raw: + split = splitString(log, '\x1b') + if split: + try: + date, time, log_type = splitString(split[0], ' ') + timestamp = '%s %s' % (date, time) + except: + timestamp = 'UNKNOWN' + log_type = 'UNKNOWN' + + message = ''.join(split[1]) if len(split) > 1 else split[0] + message = re.sub('\[\d+m\[', '[', message) + + logs.append({ + 'time': timestamp, + 'type': log_type, + 'message': message + }) + + return logs + def clear(self, **kwargs): for x in range(0, 50): From fc71a03a129a8c4360bb1fadd1fa4165ca09e168 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:27:13 +0200 Subject: [PATCH 286/468] Just loop over log array --- couchpotato/core/plugins/log/static/log.js | 28 ++++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index a08b105b..a7caab6a 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -27,7 +27,7 @@ Page.Log = new Class({ 'nr': nr }, 'onComplete': function(json){ - self.log.set('html', self.addColors(json.log)); + self.log.adopt(self.createLogElements(json.log)); self.log.removeClass('loading'); new Fx.Scroll(window, {'duration': 0}).toBottom(); @@ -63,20 +63,22 @@ Page.Log = new Class({ }, - addColors: function(text){ + createLogElements: function(logs){ - text = text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/\u001b\[31m/gi, '') - .replace(/\u001b\[36m/gi, '') - .replace(/\u001b\[33m/gi, '') - .replace(/\u001b\[0m\n/gi, '

') - .replace(/\u001b\[0m/gi, ''); + var elements = []; - return '
' + text + '
'; + logs.each(function(log){ + elements.include(new Element('div.time', { + 'text': log.time + }).grab( + new Element('span', { + 'class': log.type.toLowerCase(), + 'text': log.message + }) + )) + }); + + return elements; } }); From 0590a0d722450b3ab3fccd6e5b8234468cf0c8b9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:30:58 +0200 Subject: [PATCH 287/468] Update log api --- couchpotato/core/plugins/log/main.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index bcdd7494..a357d572 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -23,7 +23,11 @@ class Logging(Plugin): }, 'return': {'type': 'object', 'example': """{ 'success': True, - 'log': string, //Log file + 'log': [{ + 'time': '03-12 09:12:59', + 'type': 'INFO', + 'message': 'Log message' + }, ..], //Log file 'total': int, //Total log files available }"""} }) @@ -35,7 +39,11 @@ class Logging(Plugin): }, 'return': {'type': 'object', 'example': """{ 'success': True, - 'log': string, //Log file + 'log': [{ + 'time': '03-12 09:12:59', + 'type': 'INFO', + 'message': 'Log message' + }, ..] }"""} }) addApiView('logging.clear', self.clear, docs = { From d301cde2662765bbb7a2fd73370eca7709020765 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:37:36 +0200 Subject: [PATCH 288/468] Newznab custom tag wasn't used. fix #3219 --- couchpotato/core/media/_base/providers/nzb/newznab.py | 2 +- couchpotato/core/media/movie/providers/nzb/newznab.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index b0f5d160..04915776 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -45,7 +45,7 @@ class Base(NZBProvider, RSS): def _searchOnHost(self, host, media, quality, results): - query = self.buildUrl(media, host['api_key']) + query = self.buildUrl(media, host) url = '%s&%s' % (self.getUrl(host['host']), query) nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) diff --git a/couchpotato/core/media/movie/providers/nzb/newznab.py b/couchpotato/core/media/movie/providers/nzb/newznab.py index 9783f8d5..fc94acbf 100644 --- a/couchpotato/core/media/movie/providers/nzb/newznab.py +++ b/couchpotato/core/media/movie/providers/nzb/newznab.py @@ -11,11 +11,16 @@ autoload = 'Newznab' class Newznab(MovieProvider, Base): - def buildUrl(self, media, api_key): + def buildUrl(self, media, host): + query = tryUrlencode({ 't': 'movie', 'imdbid': getIdentifier(media).replace('tt', ''), - 'apikey': api_key, + 'apikey': host['api_key'], 'extended': 1 }) + + if len(host.get('custom_tag', '')) > 0: + query = '%s&%s' % (query, host.get('custom_tag')) + return query From 66b4821f7f045ee322686dcaa792344e1edb148e Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:40:27 +0200 Subject: [PATCH 289/468] Profile references before assigned. fix #3220 --- couchpotato/core/plugins/renamer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index d32a6362..3d56aa9c 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -450,6 +450,7 @@ class Renamer(Plugin): remove_leftovers = True # Mark movie "done" once it's found the quality with the finish check + profile = None try: if media.get('status') == 'active' and media.get('profile_id'): profile = db.get('id', media['profile_id']) From df140321074bb0e70538322ebe4b9d565fb03c8a Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 16:59:41 +0200 Subject: [PATCH 290/468] Add offset for log partial --- couchpotato/core/plugins/log/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index a357d572..5206acd6 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -88,9 +88,10 @@ class Logging(Plugin): 'total': total, } - def partial(self, type = 'all', lines = 30, **kwargs): + def partial(self, type = 'all', lines = 30, offset = 0, **kwargs): total_lines = tryInt(lines) + offset = tryInt(offset) log_lines = [] @@ -113,14 +114,16 @@ class Logging(Plugin): if type == 'all' or line.get('type') == type.upper(): log_lines.append(line) - if len(log_lines) >= total_lines: + if len(log_lines) >= (total_lines + offset): brk = True break if brk: break + log_lines = log_lines[offset:] log_lines.reverse() + return { 'success': True, 'log': log_lines, From b56c897e4b6e299e2421d463e165bdbc2dfadc6f Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 May 2014 23:45:35 +0200 Subject: [PATCH 291/468] Don't give negative score for non matching size --- couchpotato/core/plugins/quality/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 56598830..1ca44424 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -325,8 +325,6 @@ class QualityPlugin(Plugin): if tryInt(quality['size_min']) <= tryInt(size) <= tryInt(quality['size_max']): log.info2('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) score += 5 - else: - score -= 40 return score From a7d3de766fa1721b3102c5c1b6a63da36ae4b087 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 00:58:52 +0200 Subject: [PATCH 292/468] Don't migrate release if quality doesn't exist --- couchpotato/core/database.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 881b81e1..f9607137 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -416,7 +416,10 @@ class Database(object): empty_info = True rel['info'] = {} - quality = quality_link[rel.get('quality_id')] + quality = quality_link.get(rel.get('quality_id')) + if not quality: + continue + release_status = statuses.get(rel.get('status_id')).get('identifier') if rel['info'].get('download_id'): From 24b822aecd30148738db9fca843cd4d3e983a4ec Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 11:44:58 +0200 Subject: [PATCH 293/468] Info2 log --- couchpotato/core/media/_base/providers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 6a8e59bb..b13b279d 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -302,7 +302,7 @@ class ResultList(list): old_score = new_result['score'] new_result['score'] = int(old_score * is_correct_weight) - log.info('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', ( + log.info2('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', ( is_correct_weight, old_score, new_result['score'] From c2dcd2f67d734df69338f75765bafa868ed39c1d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 11:46:32 +0200 Subject: [PATCH 294/468] Use the "wait for" option properly. fix #3224 --- couchpotato/core/media/movie/searcher.py | 3 ++- couchpotato/core/plugins/profile/main.py | 2 +- couchpotato/core/plugins/profile/static/profile.js | 11 ++++------- couchpotato/core/plugins/release/main.py | 4 ++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 61c39b22..a5b1f477 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -145,9 +145,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): index = 0 for q_identifier in profile.get('qualities'): quality_custom = { + 'index': index, 'quality': q_identifier, 'finish': profile['finish'][index], - 'wait_for': profile['wait_for'][index], + 'wait_for': tryInt(profile['wait_for'][index]), '3d': profile['3d'][index] if profile.get('3d') else False } diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 640f9680..8dc59336 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -87,7 +87,7 @@ class ProfilePlugin(Plugin): order = 0 for type in kwargs.get('types', []): profile['qualities'].append(type.get('quality')) - profile['wait_for'].append(tryInt(type.get('wait_for'))) + profile['wait_for'].append(tryInt(kwargs.get('wait_for', 0))) profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True) profile['3d'].append(tryInt(type.get('3d'))) order += 1 diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 8441b6a8..c62b137c 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -41,7 +41,7 @@ var Profile = new Class({ new Element('span', {'text':'Wait'}), new Element('input.inlay.xsmall', { 'type':'text', - 'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0 + 'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0 }), new Element('span', {'text':'day(s) for a better quality.'}) ), @@ -63,8 +63,7 @@ var Profile = new Class({ data.types.include({ 'quality': quality, 'finish': data.finish[nr] || false, - '3d': data['3d'] ? data['3d'][nr] || false : false, - 'wait_for': data.wait_for[nr] || 0 + '3d': data['3d'] ? data['3d'][nr] || false : false }) }); } @@ -126,8 +125,7 @@ var Profile = new Class({ data.types.include({ 'quality': type.getElement('select').get('value'), 'finish': +type.getElement('input.finish[type=checkbox]').checked, - '3d': +type.getElement('input.3d[type=checkbox]').checked, - 'wait_for': 0 + '3d': +type.getElement('input.3d[type=checkbox]').checked }); }); @@ -340,8 +338,7 @@ Profile.Type = new Class({ return { 'quality': self.qualities.get('value'), 'finish': +self.finish.checked, - '3d': +self['3d'].checked, - 'wait_for': 0 + '3d': +self['3d'].checked } }, diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index ad605e7c..3cf293a6 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -339,8 +339,8 @@ class Release(Plugin): def tryDownloadResult(self, results, media, quality_custom, manual = False): for rel in results: - if not quality_custom.get('finish', False) and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): - log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for'), rel['name'])) + if quality_custom.get('index') != 0 and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): + log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for') - rel.get('age'), rel['name'])) continue if rel['status'] in ['ignored', 'failed']: From 9d3425061a472e5f83726b649b6e517898848c6e Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 12:04:07 +0200 Subject: [PATCH 295/468] Resize thumbnail-less soon movies --- couchpotato/core/media/movie/_base/static/movie.css | 2 ++ couchpotato/static/scripts/page/home.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index 6718dc3a..05fcddf4 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -123,6 +123,7 @@ .movies.thumbs_list .movie { width: 16.66667%; height: auto; + min-height: 200px; display: inline-block; margin: 0; padding: 0; @@ -133,6 +134,7 @@ @media all and (max-width: 800px) { .movies.thumbs_list .movie { width: 25%; + min-height: 100px; } } diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index bf435c14..7cde7d2a 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -104,7 +104,7 @@ Page.Home = new Class({ // Make all thumbnails the same size self.soon_list.addEvent('loaded', function(){ - var images = $(self.soon_list).getElements('.poster'), + var images = $(self.soon_list).getElements('.poster, .no_thumbnail'), timer, highest = 100; From c0f1a3c60311684ae2ef590f3560fe90b3a17789 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 12:14:29 +0200 Subject: [PATCH 296/468] Show chart scrollbar only on hover --- couchpotato/core/media/movie/charts/static/charts.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index 4eb3f8c6..ce8ff0ab 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -12,10 +12,14 @@ width: 50%; vertical-align: top; max-height: 510px; - overflow-y: auto; + overflow: hidden; scrollbar-base-color: #4e5969; } +.charts .chart:hover { + overflow-y: auto; +} + .charts .chart .media_result.hidden { display: none; } From c4a9a13d6c7a29496de47ba7a82f7dad27c845f4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 14:30:06 +0200 Subject: [PATCH 297/468] Don't continue searching lower qualities of correct one is found --- couchpotato/core/plugins/release/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 3cf293a6..29252e6e 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -338,9 +338,11 @@ class Release(Plugin): def tryDownloadResult(self, results, media, quality_custom, manual = False): + wait_for = False for rel in results: if quality_custom.get('index') != 0 and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for') - rel.get('age'), rel['name'])) + wait_for = True continue if rel['status'] in ['ignored', 'failed']: @@ -357,7 +359,7 @@ class Release(Plugin): elif downloaded != 'try_next': break - return False + return wait_for def createFromSearch(self, search_results, media, quality): From 3c6b86ea2852fa4ca6d69ac68d54e0ed09d42362 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 15:53:07 +0200 Subject: [PATCH 298/468] Delay first search --- couchpotato/core/media/movie/searcher.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index a5b1f477..5a8d6155 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -54,7 +54,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): }) if self.conf('run_on_launch'): - addEvent('app.load', self.searchAll) + def on_load(): + time.sleep(.1) + self.searchAll() + addEvent('app.load', on_load, priority = 1000) def searchAllView(self, **kwargs): From e06b4ccb3f05063f0f48c9f75ab61f605e23b126 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 15:53:37 +0200 Subject: [PATCH 299/468] Ignore "wait for" for all if 1 is old enough --- couchpotato/core/plugins/release/main.py | 25 ++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 29252e6e..7b227127 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -339,11 +339,11 @@ class Release(Plugin): def tryDownloadResult(self, results, media, quality_custom, manual = False): wait_for = False + let_through = False + filtered_results = [] + + # If a single release comes through the "wait for", let through all for rel in results: - if quality_custom.get('index') != 0 and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): - log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for') - rel.get('age'), rel['name'])) - wait_for = True - continue if rel['status'] in ['ignored', 'failed']: log.info('Ignored: %s', rel['name']) @@ -353,6 +353,23 @@ class Release(Plugin): log.info('Ignored, score to low: %s', rel['name']) continue + rel['wait_for'] = False + if quality_custom.get('index') != 0 and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): + rel['wait_for'] = True + else: + let_through = True + + filtered_results.append(rel) + + # Loop through filtered results + for rel in filtered_results: + + # Only wait if not a single release is old enough + if rel.get('wait_for') and not let_through: + log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for') - rel.get('age'), rel['name'])) + wait_for = True + continue + downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True) if downloaded is True: return True From 3a4c191b118211a088f0ab14c6c78c5d673f264a Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 May 2014 16:53:25 +0200 Subject: [PATCH 300/468] Make logs filterable --- couchpotato/core/plugins/log/static/log.css | 38 +++++++++---- couchpotato/core/plugins/log/static/log.js | 61 ++++++++++++++++----- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index b706835a..e8797c69 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -16,10 +16,14 @@ display: inline-block; padding: 5px 10px; margin: 0; + } + + .page.log .nav li.select, + .page.log .nav li.clear { cursor: pointer; } - .page.log .nav li:hover:not(.active) { + .page.log .nav li:hover:not(.active, .filter) { background: rgba(255, 255, 255, 0.1); } @@ -51,13 +55,12 @@ line-height: 150%; font-size: 11px; font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; + color: #FFF; } - .page.log .container .error { - color: #FFA4A4; - white-space: pre-wrap; + .page.log .container select { + vertical-align: top; } - .page.log .container .debug { color: lightgrey; } .page.log .container .time { clear: both; @@ -68,10 +71,25 @@ position: relative; } - .page.log .container .time:last-child { display: none; } - - .page.log .container .time span { - float: right; - width: 86%; + .page.log [data-filter=INFO] .error, + .page.log [data-filter=INFO] .debug, + .page.log [data-filter=ERROR] .debug, + .page.log [data-filter=ERROR] .info, + .page.log [data-filter=DEBUG] .info, + .page.log [data-filter=DEBUG] .error { + display: none; } + .page.log .container .type { + margin-left: 10px; + } + + .page.log .container .message { + float: right; + width: 86%; + white-space: pre-wrap; + } + + .page.log .container .error { color: #FFA4A4; } + .page.log .container .debug { opacity: .4; } + diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index a7caab6a..670f5b75 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -27,25 +27,46 @@ Page.Log = new Class({ 'nr': nr }, 'onComplete': function(json){ + self.log.set('text', ''); self.log.adopt(self.createLogElements(json.log)); self.log.removeClass('loading'); - new Fx.Scroll(window, {'duration': 0}).toBottom(); + var nav = new Element('ul.nav', { + 'events': { + 'click:relay(li.select)': function(e, el){ + self.getLogs(parseInt(el.get('text'))-1); + } + } + }); - var nav = new Element('ul.nav').inject(self.log, 'top'); + // Type selection + new Element('li.filter').grab( + new Element('select', { + 'events': { + 'change': function(){ + var type_filter = this.getSelected()[0].get('value'); + self.log.set('data-filter', type_filter); + self.scrollToBottom(); + } + } + }).adopt( + new Element('option', {'value': 'ALL', 'text': 'Show all logs'}), + new Element('option', {'value': 'INFO', 'text': 'Show only INFO'}), + new Element('option', {'value': 'DEBUG', 'text': 'Show only DEBUG'}), + new Element('option', {'value': 'ERROR', 'text': 'Show only ERROR'}) + ) + ).inject(nav); + + // Selections for (var i = 0; i <= json.total; i++) { new Element('li', { 'text': i+1, - 'class': nr == i ? 'active': '', - 'events': { - 'click': function(e){ - self.getLogs(e.target.get('text')-1); - } - } + 'class': 'select ' + (nr == i ? 'active': '') }).inject(nav); } - new Element('li', { + // Clear button + new Element('li.clear', { 'text': 'clear', 'events': { 'click': function(){ @@ -57,7 +78,12 @@ Page.Log = new Class({ } } - }).inject(nav) + }).inject(nav); + + // Add to page + nav.inject(self.log, 'top'); + + self.scrollToBottom(); } }); @@ -68,17 +94,24 @@ Page.Log = new Class({ var elements = []; logs.each(function(log){ - elements.include(new Element('div.time', { + elements.include(new Element('div', { + 'class': 'time ' + log.type.toLowerCase(), 'text': log.time - }).grab( - new Element('span', { - 'class': log.type.toLowerCase(), + }).adopt( + new Element('span.type', { + 'text': log.type + }), + new Element('span.message', { 'text': log.message }) )) }); return elements; + }, + + scrollToBottom: function(){ + new Fx.Scroll(window, {'duration': 0}).toBottom(); } }); From c2eb50a7eef240b777c4018bac0a8425a766d433 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 May 2014 13:23:13 +0200 Subject: [PATCH 301/468] Log reporting --- couchpotato/core/plugins/log/static/log.css | 90 ++++++++- couchpotato/core/plugins/log/static/log.js | 211 +++++++++++++++++--- 2 files changed, 270 insertions(+), 31 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index e8797c69..63165128 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -43,6 +43,19 @@ } } + .page.log .nav li.hint { + text-align: center; + width: 400px; + left: 50%; + margin-left: -200px; + font-style: italic; + font-size: 11px; + position: absolute; + right: 20px; + opacity: .5; + bottom: 5px; + } + .page.log .loading { text-align: center; font-size: 20px; @@ -65,11 +78,18 @@ .page.log .container .time { clear: both; color: lightgrey; - padding: 3px 0; font-size: 10px; - border-top: 1px solid rgba(255, 255, 255, 0.2); + border-top: 1px solid rgba(255, 255, 255, 0.1); position: relative; } + .page.log .container .time.highlight { + background: rgba(255, 255, 255, 0.1); + } + .page.log .container .time span { + padding: 5px 0 3px; + display: inline-block; + vertical-align: middle; + } .page.log [data-filter=INFO] .error, .page.log [data-filter=INFO] .debug, @@ -91,5 +111,69 @@ } .page.log .container .error { color: #FFA4A4; } - .page.log .container .debug { opacity: .4; } + .page.log .container .debug span { opacity: .4; } +.do_report { + position: absolute; + padding: 10px; +} + +.page.log .report { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0,0,0,.7); + left: 0; + top: 0; + z-index: 99999; + font-size: 14px; + font-family: OpenSans,"Helvetica Neue",Helvetica,Arial,Geneva,sans-serif; +} + + .page.log .report .button { + display: inline-block; + margin: 10px 0; + padding: 10px; + } + + .page.log .report .bug { + width: 800px; + height: 80%; + position: absolute; + left: 50%; + top: 50%; + margin: 0 0 0 -400px; + transform: translate(0, -50%); + } + + .page.log .report .bug textarea { + display: block; + width: 100%; + background: #FFF; + padding: 20px; + overflow: auto; + color: #666; + height: 70%; + font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; + font-size: 12px; + } + +.page.log .container ::-webkit-selection { + background-color: #000; + color: #FFF; +} + +.page.log .container ::-moz-selection { + background-color: #000; + color: #FFF; +} + +.page.log .container ::-ms-selection { + background-color: #000; + color: #FFF; +} + +.page.log .container ::selection { + background-color: #000; + color: #FFF; +} diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 670f5b75..1e18c5b1 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -7,34 +7,54 @@ Page.Log = new Class({ title: 'Show recent logs.', has_tab: false, - indexAction: function(){ + report_text: '\ +### Steps to reproduce:\n\ +1. ..\n\ +2. ..\n\ +\n\ +### Information:\n\ +Movie(s) I have this with: ...\n\ +Quality of the movie being searched: ...\n\ +Providers I use: ...\n\ +Version of CouchPotato: ...\n\ +Running on: ...\n\ +\n\ +### Logs:\n\ +```\n{issue}```', + + indexAction: function () { var self = this; self.getLogs(0); }, - getLogs: function(nr){ + getLogs: function (nr) { var self = this; - if(self.log) self.log.destroy(); + if (self.log) self.log.destroy(); self.log = new Element('div.container.loading', { - 'text': 'loading...' + 'text': 'loading...', + 'events': { + 'mouseup:relay(.time)': function(e){ + self.showSelectionButton.delay(100, self, e); + } + } }).inject(self.el); Api.request('logging.get', { 'data': { 'nr': nr }, - 'onComplete': function(json){ + 'onComplete': function (json) { self.log.set('text', ''); self.log.adopt(self.createLogElements(json.log)); self.log.removeClass('loading'); var nav = new Element('ul.nav', { 'events': { - 'click:relay(li.select)': function(e, el){ - self.getLogs(parseInt(el.get('text'))-1); + 'click:relay(li.select)': function (e, el) { + self.getLogs(parseInt(el.get('text')) - 1); } } }); @@ -43,7 +63,7 @@ Page.Log = new Class({ new Element('li.filter').grab( new Element('select', { 'events': { - 'change': function(){ + 'change': function () { var type_filter = this.getSelected()[0].get('value'); self.log.set('data-filter', type_filter); self.scrollToBottom(); @@ -60,8 +80,8 @@ Page.Log = new Class({ // Selections for (var i = 0; i <= json.total; i++) { new Element('li', { - 'text': i+1, - 'class': 'select ' + (nr == i ? 'active': '') + 'text': i + 1, + 'class': 'select ' + (nr == i ? 'active' : '') }).inject(nav); } @@ -69,9 +89,9 @@ Page.Log = new Class({ new Element('li.clear', { 'text': 'clear', 'events': { - 'click': function(){ + 'click': function () { Api.request('logging.clear', { - 'onComplete': function(){ + 'onComplete': function () { self.getLogs(0); } }); @@ -80,6 +100,11 @@ Page.Log = new Class({ } }).inject(nav); + // Hint + new Element('li.hint', { + 'text': 'Select multiple lines & report an issue' + }).inject(nav); + // Add to page nav.inject(self.log, 'top'); @@ -89,29 +114,159 @@ Page.Log = new Class({ }, - createLogElements: function(logs){ + createLogElements: function (logs) { - var elements = []; + var elements = []; - logs.each(function(log){ - elements.include(new Element('div', { - 'class': 'time ' + log.type.toLowerCase(), - 'text': log.time - }).adopt( - new Element('span.type', { - 'text': log.type - }), - new Element('span.message', { - 'text': log.message - }) - )) - }); + logs.each(function (log) { + elements.include(new Element('div', { + 'class': 'time ' + log.type.toLowerCase() + }).adopt( + new Element('span', { + 'text': log.time + }), + new Element('span.type', { + 'text': log.type + }), + new Element('span.message', { + 'text': log.message + }) + )) + }); return elements; }, - scrollToBottom: function(){ + scrollToBottom: function () { new Fx.Scroll(window, {'duration': 0}).toBottom(); + }, + + showSelectionButton: function(e){ + var self = this, + selection = self.getSelected(), + start_node = selection.anchorNode, + parent_start = start_node.parentNode.getParent('.time'), + end_node = selection.focusNode.parentNode.getParent('.time'), + text = ''; + + var remove_button = function(){ + self.log.getElements('.highlight').removeClass('highlight'); + if(self.do_report) + self.do_report.destroy(); + document.body.removeEvent('click', remove_button); + }; + remove_button(); + + if(parent_start) + start_node = parent_start; + + var nodes = [start_node], + current_node = start_node; + + while(current_node != end_node){ + current_node = current_node.getNext('.time'); + nodes.include(current_node); + } + nodes.each(function(node, nr){ + node.addClass('highlight'); + node.getElements('span').each(function(span){ + text += self.spaceFill(span.get('text') + ' ', 6); + }); + text += '\n'; + }); + + self.do_report = new Element('a.do_report.button', { + 'text': 'Report issue', + 'styles': { + 'top': e.page.y, + 'left': e.page.x + }, + 'events': { + 'click': function(e){ + (e).stop(); + + self.showReport(text); + } + } + }).inject(document.body); + + setTimeout(function(){ + document.body.addEvent('click', remove_button); + }, 0); + + }, + + showReport: function(text){ + var self = this; + + var overlay = new Element('div.report', { + 'method': 'post', + 'events': { + 'click': function(e){ + overlay.destroy(); + } + } + }).grab( + new Element('div.bug', { + 'events': { + 'click': function(e){ + (e).stopPropagation(); + } + } + }).adopt( + new Element('h1', { + 'text': 'Report a bug' + }), + new Element('span').adopt( + new Element('span', { + 'text': 'Read ' + }), + new Element('a.button', { + 'target': '_blank', + 'text': 'the contributing guide', + 'href': 'https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md' + }), + new Element('span', { + 'text': ' before posting, then copy the text below' + }) + ), + new Element('textarea', { + 'text': self.report_text.replace('{issue}', text), + 'events': { + 'click': function(){ + this.select(); + } + } + }), + new Element('a.button', { + 'target': '_blank', + 'text': 'Create a new issue on GitHub with the text above', + 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new?body=Paste the text here' + }) + ) + ); + + overlay.inject(self.log); + }, + + getSelected: function(){ + if (window.getSelection) + return window.getSelection(); + else if (document.getSelection) + return document.getSelection(); + else { + var selection = document.selection && document.selection.createRange(); + if (selection.text) + return selection.text; + } + return false; + + }, + + spaceFill: function( number, width ){ + if ( number.toString().length >= width ) + return number; + return ( new Array( width ).join( ' ' ) + number.toString() ).substr( -width ); } }); From cc1096950694acc85fde9d8f3b225d8823f629c8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 May 2014 15:53:27 +0200 Subject: [PATCH 302/468] Keep log filter Pre-fill in issue when possible --- couchpotato/core/plugins/log/static/log.css | 48 +++++++++++++++------ couchpotato/core/plugins/log/static/log.js | 9 ++-- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 63165128..d7112163 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -81,6 +81,8 @@ font-size: 10px; border-top: 1px solid rgba(255, 255, 255, 0.1); position: relative; + overflow: hidden; + padding: 0 3px; } .page.log .container .time.highlight { background: rgba(255, 255, 255, 0.1); @@ -91,12 +93,12 @@ vertical-align: middle; } - .page.log [data-filter=INFO] .error, - .page.log [data-filter=INFO] .debug, - .page.log [data-filter=ERROR] .debug, - .page.log [data-filter=ERROR] .info, - .page.log [data-filter=DEBUG] .info, - .page.log [data-filter=DEBUG] .error { + .page.log[data-filter=INFO] .error, + .page.log[data-filter=INFO] .debug, + .page.log[data-filter=ERROR] .debug, + .page.log[data-filter=ERROR] .info, + .page.log[data-filter=DEBUG] .info, + .page.log[data-filter=DEBUG] .error { display: none; } @@ -111,7 +113,7 @@ } .page.log .container .error { color: #FFA4A4; } - .page.log .container .debug span { opacity: .4; } + .page.log .container .debug span { opacity: .6; } .do_report { position: absolute; @@ -158,22 +160,42 @@ font-size: 12px; } -.page.log .container ::-webkit-selection { +.page.log .container .time:not(.highlight) ::-webkit-selection { background-color: #000; color: #FFF; } -.page.log .container ::-moz-selection { +.page.log .container .time:not(.highlight) ::-moz-selection { background-color: #000; color: #FFF; } -.page.log .container ::-ms-selection { +.page.log .container .time:not(.highlight) ::-ms-selection { background-color: #000; color: #FFF; } -.page.log .container ::selection { - background-color: #000; - color: #FFF; +.page.log .container .time.highlight ::selection { + background-color: transparent; + color: inherit; +} + +.page.log .container .time.highlight ::-webkit-selection { + background-color: transparent; + color: inherit; +} + +.page.log .container .time.highlight ::-moz-selection { + background-color: transparent; + color: inherit; +} + +.page.log .container .time.highlight ::-ms-selection { + background-color: transparent; + color: inherit; +} + +.page.log .container .time.highlight ::selection { + background-color: transparent; + color: inherit; } diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 1e18c5b1..6560aefc 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -65,7 +65,7 @@ Running on: ...\n\ 'events': { 'change': function () { var type_filter = this.getSelected()[0].get('value'); - self.log.set('data-filter', type_filter); + self.el.set('data-filter', type_filter); self.scrollToBottom(); } } @@ -197,7 +197,8 @@ Running on: ...\n\ }, showReport: function(text){ - var self = this; + var self = this, + body = self.report_text.replace('{issue}', text); var overlay = new Element('div.report', { 'method': 'post', @@ -231,7 +232,7 @@ Running on: ...\n\ }) ), new Element('textarea', { - 'text': self.report_text.replace('{issue}', text), + 'text': body, 'events': { 'click': function(){ this.select(); @@ -241,7 +242,7 @@ Running on: ...\n\ new Element('a.button', { 'target': '_blank', 'text': 'Create a new issue on GitHub with the text above', - 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new?body=Paste the text here' + 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here') }) ) ); From 6ba25b54688d164f326ad41f5feb4bf9a4ae3c66 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 May 2014 15:58:29 +0200 Subject: [PATCH 303/468] Better highlight --- couchpotato/core/plugins/log/static/log.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index d7112163..8a809e32 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -67,7 +67,6 @@ overflow: hidden; line-height: 150%; font-size: 11px; - font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; color: #FFF; } @@ -83,6 +82,7 @@ position: relative; overflow: hidden; padding: 0 3px; + font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; } .page.log .container .time.highlight { background: rgba(255, 255, 255, 0.1); @@ -129,7 +129,6 @@ top: 0; z-index: 99999; font-size: 14px; - font-family: OpenSans,"Helvetica Neue",Helvetica,Arial,Geneva,sans-serif; } .page.log .report .button { @@ -156,21 +155,20 @@ overflow: auto; color: #666; height: 70%; - font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif; font-size: 12px; } -.page.log .container .time:not(.highlight) ::-webkit-selection { +.page.log .container .time ::-webkit-selection { background-color: #000; color: #FFF; } -.page.log .container .time:not(.highlight) ::-moz-selection { +.page.log .container .time ::-moz-selection { background-color: #000; color: #FFF; } -.page.log .container .time:not(.highlight) ::-ms-selection { +.page.log .container .time ::-ms-selection { background-color: #000; color: #FFF; } From 0587d2f8dbd72cc6ceb00b5d64fac8519b56010c Mon Sep 17 00:00:00 2001 From: mikke89 Date: Sat, 10 May 2014 22:26:41 +0200 Subject: [PATCH 304/468] Fix searches on torrentday and sceneaccess --- couchpotato/core/media/_base/providers/torrent/torrentday.py | 2 +- couchpotato/core/media/movie/providers/torrent/sceneaccess.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index 9304e552..a4eb3899 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -28,7 +28,7 @@ class Base(TorrentProvider): 'cata': 'yes', 'jxt': 8, 'jxw': 'b', - 'search': tryUrlencode(query), + 'search': query, } data = self.getJsonData(self.urls['search'], data = data) diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py index 3eed04dd..99157d73 100644 --- a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py @@ -22,7 +22,7 @@ class SceneAccess(MovieProvider, Base): url = self.urls['search'] % (cat_id, cat_id) arguments = tryUrlencode({ - 'search': '"%s" %s' % (title, media['info']['year']), + 'search': '%s %s' % (title, media['info']['year']), 'method': 2, }) query = "%s&%s" % (url, arguments) From 93f4b8b5372a2dab62f197d6c8984301690fed92 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 00:14:56 +0200 Subject: [PATCH 305/468] Don't log non existing properties --- couchpotato/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/settings.py b/couchpotato/core/settings.py index 16c827c8..a0bcd743 100644 --- a/couchpotato/core/settings.py +++ b/couchpotato/core/settings.py @@ -234,7 +234,7 @@ class Settings(object): propert = db.get('property', identifier, with_doc = True) prop = propert['doc']['value'] except: - self.log.debug('Property "%s" doesn\'t exist: %s', (identifier, traceback.format_exc(0))) + pass # self.log.debug('Property "%s" doesn\'t exist: %s', (identifier, traceback.format_exc(0))) return prop From 81f9302da1ac3393b5ae7a6a07780a1b1154200f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 00:19:05 +0200 Subject: [PATCH 306/468] Use super threaded db connection --- couchpotato/core/media/movie/searcher.py | 5 +---- couchpotato/runner.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 5a8d6155..a5b1f477 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -54,10 +54,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): }) if self.conf('run_on_launch'): - def on_load(): - time.sleep(.1) - self.searchAll() - addEvent('app.load', on_load, priority = 1000) + addEvent('app.load', self.searchAll) def searchAllView(self, **kwargs): diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 1684eddb..8c619896 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -10,7 +10,7 @@ import warnings import re import tarfile -from CodernityDB.database_thread_safe import ThreadSafeDatabase +from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase from argparse import ArgumentParser from cache import FileSystemCache from couchpotato import KeyHandler, LoginHandler, LogoutHandler @@ -89,7 +89,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db_path = toUnicode(os.path.join(data_dir, 'database')) # Check if database exists - db = ThreadSafeDatabase(db_path) + db = SuperThreadSafeDatabase(db_path) db_exists = db.exists() if db_exists: From 33e5dd1fdb9c2d364fc6bd17ef7430ff5276ca42 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 00:52:55 +0200 Subject: [PATCH 307/468] Speed up log highlight Allow reverse selection --- couchpotato/core/plugins/log/static/log.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 6560aefc..d39a20a9 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -7,6 +7,7 @@ Page.Log = new Class({ title: 'Show recent logs.', has_tab: false, + log_items: [], report_text: '\ ### Steps to reproduce:\n\ 1. ..\n\ @@ -48,7 +49,8 @@ Running on: ...\n\ }, 'onComplete': function (json) { self.log.set('text', ''); - self.log.adopt(self.createLogElements(json.log)); + self.log_items = self.createLogElements(json.log); + self.log.adopt(self.log_items); self.log.removeClass('loading'); var nav = new Element('ul.nav', { @@ -160,13 +162,20 @@ Running on: ...\n\ if(parent_start) start_node = parent_start; - var nodes = [start_node], - current_node = start_node; + var index = { + 'start': self.log_items.indexOf(start_node), + 'end': self.log_items.indexOf(end_node) + }; - while(current_node != end_node){ - current_node = current_node.getNext('.time'); - nodes.include(current_node); + if(index.start > index.end){ + index = { + 'start': index.end, + 'end': index.start + }; } + + var nodes = self.log_items.slice(index.start, index.end + 1); + nodes.each(function(node, nr){ node.addClass('highlight'); node.getElements('span').each(function(span){ From 675bee83caf4a1dc837895e82ace2a3979b44eab Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 16:02:49 +0200 Subject: [PATCH 308/468] Path encode --- CouchPotato.py | 2 +- couchpotato/core/_base/updater/main.py | 5 +++-- couchpotato/core/downloaders/rtorrent_.py | 2 +- couchpotato/core/plugins/renamer.py | 19 ++++++++---------- couchpotato/runner.py | 24 +++++++++++------------ 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/CouchPotato.py b/CouchPotato.py index 375a1d41..364d77a4 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -29,7 +29,7 @@ class Loader(object): # Get options via arg from couchpotato.runner import getOptions - self.options = getOptions(base_path, sys.argv[1:]) + self.options = getOptions(sys.argv[1:]) # Load settings settings = Env.get('settings') diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index cde17eb2..b2f670b4 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -10,7 +10,7 @@ from threading import RLock from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.helpers.encoding import ss +from couchpotato.core.helpers.encoding import ss, sp from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -182,7 +182,7 @@ class BaseUpdater(Plugin): def deletePyc(self, only_excess = True, show_logs = True): - for root, dirs, files in scandir.walk(ss(Env.get('app_dir'))): + for root, dirs, files in scandir.walk(Env.get('app_dir')): pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) py_files = set(filter(lambda filename: filename.endswith('.py'), files)) @@ -322,6 +322,7 @@ class SourceUpdater(BaseUpdater): return False def replaceWith(self, path): + path = sp(path) app_dir = ss(Env.get('app_dir')) data_dir = ss(Env.get('data_dir')) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index 86c09a40..5ea3a4de 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -238,7 +238,7 @@ class rTorrent(DownloaderBase): if torrent.is_multi_file() and torrent.directory.endswith(torrent.name): # Remove empty directories bottom up try: - for path, _, _ in scandir.walk(torrent.directory, topdown = False): + for path, _, _ in scandir.walk(sp(torrent.directory), topdown = False): os.rmdir(path) except OSError: log.info('Directory "%s" contains extra files, unable to remove', torrent.directory) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 3d56aa9c..6d2cd794 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -118,10 +118,7 @@ class Renamer(Plugin): to_folder = sp(self.conf('to')) # Get media folder to process - media_folder = release_download.get('folder') - - # Quality order for calculation quality priority - quality_order = fireEvent('quality.order', single = True) + media_folder = sp(release_download.get('folder')) # Get all folders that should not be processed no_process = [to_folder] @@ -149,9 +146,9 @@ class Renamer(Plugin): # Update to the from folder if len(release_download.get('files', [])) == 1: - new_media_folder = from_folder + new_media_folder = sp(from_folder) else: - new_media_folder = os.path.join(from_folder, os.path.basename(media_folder)) + new_media_folder = sp(os.path.join(from_folder, os.path.basename(media_folder))) if not os.path.isdir(new_media_folder): log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder) @@ -319,7 +316,7 @@ class Renamer(Plugin): '3d': '3D' if group['meta_data']['quality'].get('is_3d', 0) else '', '3d_type': group['meta_data'].get('3d_type'), } - + if replacements['mpaa_only'] not in ('G', 'PG', 'PG-13', 'R', 'NC-17'): replacements['mpaa_only'] = 'Not Rated' @@ -667,7 +664,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Tag all files in release folder elif release_download['folder']: - for root, folders, names in scandir.walk(release_download['folder']): + for root, folders, names in scandir.walk(sp(release_download['folder'])): tag_files.extend([os.path.join(root, name) for name in names]) for filename in tag_files: @@ -691,13 +688,13 @@ Remove it if you want it to be renamed (again, or at least let it try again) if isinstance(group, dict): tag_files = [sorted(list(group['files']['movie']))[0]] - folder = group['parentdir'] + folder = sp(group['parentdir']) if not group.get('dirname') or not os.path.isdir(folder): return False elif isinstance(release_download, dict): - folder = release_download['folder'] + folder = sp(release_download['folder']) if not os.path.isdir(folder): return False @@ -731,7 +728,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) if not release_download: return False - folder = release_download['folder'] + folder = sp(release_download['folder']) if not os.path.isdir(folder): return False diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 8c619896..d22f8fc8 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -16,14 +16,14 @@ from cache import FileSystemCache from couchpotato import KeyHandler, LoginHandler, LogoutHandler from couchpotato.api import NonBlockHandler, ApiHandler from couchpotato.core.event import fireEventAsync, fireEvent -from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getDataDir, tryInt from scandir import scandir from tornado.httpserver import HTTPServer from tornado.web import Application, StaticFileHandler, RedirectHandler -def getOptions(base_path, args): +def getOptions(args): # Options parser = ArgumentParser(prog = 'CouchPotato.py') @@ -86,7 +86,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En Env.set('encoding', encoding) # Do db stuff - db_path = toUnicode(os.path.join(data_dir, 'database')) + db_path = sp(os.path.join(data_dir, 'database')) # Check if database exists db = SuperThreadSafeDatabase(db_path) @@ -94,7 +94,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En if db_exists: # Backup before start and cleanup old backups - backup_path = toUnicode(os.path.join(data_dir, 'db_backup')) + backup_path = sp(os.path.join(data_dir, 'db_backup')) backup_count = 5 existing_backups = [] if not os.path.isdir(backup_path): os.makedirs(backup_path) @@ -114,7 +114,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En os.remove(os.path.join(backup_path, eb[1])) # Create new backup - new_backup = toUnicode(os.path.join(backup_path, '%s.tar.gz' % int(time.time()))) + new_backup = sp(os.path.join(backup_path, '%s.tar.gz' % int(time.time()))) zipf = tarfile.open(new_backup, 'w:gz') for root, dirs, files in scandir.walk(db_path): for zfilename in files: @@ -128,12 +128,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En db.create() # Register environment settings - Env.set('app_dir', toUnicode(base_path)) - Env.set('data_dir', toUnicode(data_dir)) - Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log'))) + Env.set('app_dir', sp(base_path)) + Env.set('data_dir', sp(data_dir)) + Env.set('log_path', sp(os.path.join(log_dir, 'CouchPotato.log'))) Env.set('db', db) - Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache'))) - Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python')))) + Env.set('cache_dir', sp(os.path.join(data_dir, 'cache'))) + Env.set('cache', FileSystemCache(sp(os.path.join(Env.get('cache_dir'), 'python')))) Env.set('console_log', options.console_log) Env.set('quiet', options.quiet) Env.set('desktop', desktop) @@ -245,13 +245,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En static_path = '%sstatic/' % web_base for dir_name in ['fonts', 'images', 'scripts', 'style']: application.add_handlers(".*$", [ - ('%s%s/(.*)' % (static_path, dir_name), StaticFileHandler, {'path': toUnicode(os.path.join(base_path, 'couchpotato', 'static', dir_name))}) + ('%s%s/(.*)' % (static_path, dir_name), StaticFileHandler, {'path': sp(os.path.join(base_path, 'couchpotato', 'static', dir_name))}) ]) Env.set('static_path', static_path) # Load configs & plugins loader = Env.get('loader') - loader.preload(root = toUnicode(base_path)) + loader.preload(root = sp(base_path)) loader.run() # Fill database with needed stuff From d5e19db5e6f1cdfddef25cc04e742ec41f372bde Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 16:03:27 +0200 Subject: [PATCH 309/468] Don't re-encode by filesystem encoding --- libs/scandir/scandir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scandir/scandir.py b/libs/scandir/scandir.py index 1e34f8e5..9761e9fb 100644 --- a/libs/scandir/scandir.py +++ b/libs/scandir/scandir.py @@ -365,7 +365,7 @@ elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: """Like os.listdir(), but yield DirEntry objects instead of returning a list of names. """ - dir_p = opendir(path.encode(file_system_encoding)) + dir_p = opendir(path) if not dir_p: raise posix_error(path) try: @@ -376,7 +376,7 @@ elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: raise posix_error(path) if not result: break - name = entry.d_name.decode(file_system_encoding) + name = entry.d_name if name not in ('.', '..'): yield PosixDirEntry(path, name, entry.d_type) finally: From 8999f51dc99dc940deef8cebd87bdebd7a67b88e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 16:07:23 +0200 Subject: [PATCH 310/468] Make quality guess debug message --- couchpotato/core/plugins/quality/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 1ca44424..e52e3d57 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -323,7 +323,7 @@ class QualityPlugin(Plugin): if size: if tryInt(quality['size_min']) <= tryInt(size) <= tryInt(quality['size_max']): - log.info2('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) + log.debug('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) score += 5 return score From b87c00c041828f949267b73a6d47f5de2efbc0f6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 17:18:27 +0200 Subject: [PATCH 311/468] Encode before path join --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 167e76cd..0568f5e6 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -151,7 +151,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in scandir.walk(folder, followlinks=True): - files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) + files.extend([sp(os.path.join(toUnicode(root), toUnicode(filename))) for filename in walk_files]) # Break if CP wants to shut down if self.shuttingDown(): From 0f8ab05fd434d3433a242a51d803eaf1dfd0c558 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 17:40:43 +0200 Subject: [PATCH 312/468] Clean up managed movies --- couchpotato/core/plugins/manage.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index f6b0ddd7..ab29e72d 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -120,7 +120,7 @@ class Manage(Plugin): if self.conf('cleanup') and full and not self.shuttingDown(): # Get movies with done status - total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) + total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True) for done_movie in done_movies: if getIdentifier(done_movie) not in added_identifiers: @@ -131,12 +131,16 @@ class Manage(Plugin): for release in releases: if release.get('files'): + brk = False for file_type in release.get('files', {}): for release_file in release['files'][file_type]: # Remove release not available anymore if not os.path.isfile(sp(release_file)): fireEvent('release.clean', release['_id']) + brk = True break + if brk: + break # Check if there are duplicate releases (different quality) use the last one, delete the rest if len(releases) > 1: @@ -147,10 +151,12 @@ class Manage(Plugin): already_used = used_files.get(release_file) if already_used: + # delete current one if already_used.get('last_edit', 0) < release.get('last_edit', 0): - fireEvent('release.delete', release['_id'], single = True) # delete current one + fireEvent('release.delete', release['_id'], single = True) + # delete previous one else: - fireEvent('release.delete', already_used['_id'], single = True) # delete previous one + fireEvent('release.delete', already_used['_id'], single = True) break else: used_files[release_file] = release From 5a2a9bbf9a1cad5d97cd580cf5b93928ba338429 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 17:40:55 +0200 Subject: [PATCH 313/468] Loop through release files properly --- couchpotato/core/plugins/release/main.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 7b227127..0218fa6a 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -180,15 +180,20 @@ class Release(Plugin): try: db = get_db() rel = db.get('id', release_id) + raw_files = rel.get('files') - if len(rel.get('files')) == 0: + if len(raw_files) == 0: self.delete(rel['_id']) else: - files = [] - for release_file in rel.get('files'): - if os.path.isfile(ss(release_file['path'])): - files.append(release_file) + files = {} + for file_type in raw_files: + + for release_file in raw_files.get(file_type, []): + if os.path.isfile(ss(release_file)): + if file_type not in files: + files[file_type] = [] + files[file_type].append(release_file) rel['files'] = files db.update(rel) From ce648c5d35c840f64c9112878d507f04ecf8e569 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 18:35:38 +0200 Subject: [PATCH 314/468] Post mass edit commands --- couchpotato/core/media/movie/_base/static/list.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index 91207505..83ac4ede 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -373,6 +373,7 @@ var MovieList = new Class({ (e).preventDefault(); this.set('text', 'Deleting..'); Api.request('media.delete', { + 'method': 'post', 'data': { 'id': ids.join(','), 'delete_from': self.options.identifier @@ -413,6 +414,7 @@ var MovieList = new Class({ var ids = self.getSelectedMovies(); Api.request('movie.edit', { + 'method': 'post', 'data': { 'id': ids.join(','), 'profile_id': self.mass_edit_quality.get('value') @@ -426,6 +428,7 @@ var MovieList = new Class({ var ids = self.getSelectedMovies(); Api.request('media.refresh', { + 'method': 'post', 'data': { 'id': ids.join(',') } From fe9998fb9d229e76cf3ebb0893132eea39e3e7d2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 18:53:13 +0200 Subject: [PATCH 315/468] Revert "Don't re-encode by filesystem encoding" This reverts commit d5e19db5e6f1cdfddef25cc04e742ec41f372bde. --- libs/scandir/scandir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scandir/scandir.py b/libs/scandir/scandir.py index 9761e9fb..1e34f8e5 100644 --- a/libs/scandir/scandir.py +++ b/libs/scandir/scandir.py @@ -365,7 +365,7 @@ elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: """Like os.listdir(), but yield DirEntry objects instead of returning a list of names. """ - dir_p = opendir(path) + dir_p = opendir(path.encode(file_system_encoding)) if not dir_p: raise posix_error(path) try: @@ -376,7 +376,7 @@ elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: raise posix_error(path) if not result: break - name = entry.d_name + name = entry.d_name.decode(file_system_encoding) if name not in ('.', '..'): yield PosixDirEntry(path, name, entry.d_type) finally: From 311a2798dd3a93315335244e9e6df1e0104b2b41 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 18:58:14 +0200 Subject: [PATCH 316/468] Revert "Encode before path join" This reverts commit b87c00c041828f949267b73a6d47f5de2efbc0f6. --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 0568f5e6..167e76cd 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -151,7 +151,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in scandir.walk(folder, followlinks=True): - files.extend([sp(os.path.join(toUnicode(root), toUnicode(filename))) for filename in walk_files]) + files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) # Break if CP wants to shut down if self.shuttingDown(): From b86853f06f4a07edf6d3683940621b559f98dcd0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 19:36:22 +0200 Subject: [PATCH 317/468] More path encoding --- couchpotato/core/_base/updater/main.py | 4 ++-- couchpotato/core/helpers/encoding.py | 2 +- couchpotato/core/plugins/base.py | 4 ++-- couchpotato/core/plugins/release/main.py | 4 ++-- couchpotato/core/plugins/renamer.py | 2 +- couchpotato/core/plugins/scanner.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index b2f670b4..9ebecce2 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -323,8 +323,8 @@ class SourceUpdater(BaseUpdater): def replaceWith(self, path): path = sp(path) - app_dir = ss(Env.get('app_dir')) - data_dir = ss(Env.get('data_dir')) + app_dir = Env.get('app_dir') + data_dir = Env.get('data_dir') # Get list of files we want to overwrite self.deletePyc() diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py index ff6a3e59..41591683 100644 --- a/couchpotato/core/helpers/encoding.py +++ b/couchpotato/core/helpers/encoding.py @@ -78,7 +78,7 @@ def sp(path, *args): # Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit) path = re.sub('^//', '/', path) - return toUnicode(path) + return path def ek(original, *args): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 13eb7caf..7cc4d1c6 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -113,7 +113,7 @@ class Plugin(object): fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f) def createFile(self, path, content, binary = False): - path = ss(path) + path = sp(path) self.makeDir(os.path.dirname(path)) @@ -131,7 +131,7 @@ class Plugin(object): os.remove(path) def makeDir(self, path): - path = ss(path) + path = sp(path) try: if not os.path.isdir(path): os.makedirs(path, Env.getPermission('folder')) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 0218fa6a..8af26fd2 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -7,7 +7,7 @@ from CodernityDB.database import RecordDeleted from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.encoding import ss, toUnicode +from couchpotato.core.helpers.encoding import ss, toUnicode, sp from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -190,7 +190,7 @@ class Release(Plugin): for file_type in raw_files: for release_file in raw_files.get(file_type, []): - if os.path.isfile(ss(release_file)): + if os.path.isfile(sp(release_file)): if file_type not in files: files[file_type] = [] files[file_type].append(release_file) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 6d2cd794..0b56ca33 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -757,7 +757,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) return False def moveFile(self, old, dest, forcemove = False): - dest = ss(dest) + dest = sp(dest) try: if forcemove or self.conf('file_action') not in ['copy', 'link']: try: diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 167e76cd..b8eca4c4 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -151,7 +151,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in scandir.walk(folder, followlinks=True): - files.extend([sp(os.path.join(root, filename)) for filename in walk_files]) + files.extend([sp(os.path.join(sp(root), ss(filename))) for filename in walk_files]) # Break if CP wants to shut down if self.shuttingDown(): From 1ac01456a97a4a58c783d1f853d9cb914d881048 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 May 2014 20:18:50 +0200 Subject: [PATCH 318/468] Don't show snatched movies in late section. fix #3013 --- couchpotato/core/plugins/dashboard.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index 7a26b2a0..776f24ec 100644 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -79,7 +79,20 @@ class Dashboard(Plugin): # Don't list older movies if ((not late and (media['info']['year'] >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or (late and (media['info']['year'] < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))): - medias.append(media) + + add = True + + # Check if it doesn't have any releases + if late: + media['releases'] = fireEvent('release.for_media', media['_id'], single = True) + + for release in media.get('releases'): + if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']: + add = False + break + + if add: + medias.append(media) if len(medias) >= limit: break From 06a8414f12a27bc882a2d16c748431686cb1f1a6 Mon Sep 17 00:00:00 2001 From: mano3m Date: Tue, 13 May 2014 20:04:28 +0200 Subject: [PATCH 319/468] Add 3D to download notification. fixes #3242 --- couchpotato/core/plugins/renamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 0b56ca33..50c55d23 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -609,7 +609,7 @@ class Renamer(Plugin): log.error('Failed removing %s: %s', (group_folder, traceback.format_exc())) # Notify on download, search for trailers etc - download_message = 'Downloaded %s (%s)' % (media_title, replacements['quality']) + download_message = 'Downloaded %s (%s%s)' % (media_title, replacements['quality'], (' ' + replacements['3d']) if replacements['3d'] else '') try: fireEvent('renamer.after', message = download_message, group = group, in_order = True) except: From 577bf098593b993576082c15530d3df259e9710c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 19 May 2014 13:35:44 +0200 Subject: [PATCH 320/468] Add fallback imdb filter. BeautifulSoup fails to load elements based on ID for the html imdb returns... Hacky way of filtering out the correct elements. --- .../core/media/movie/providers/automation/imdb.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index b52816a8..48b3569f 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -69,10 +69,16 @@ class IMDBWatchlist(IMDBBase): html = self.getHTMLData(w_url) try: - split = splitString(html, split_on="
")[1] - html = splitString(split, split_on="
")[0] + split = splitString(html, split_on="
") + html2 = BeautifulSoup(split[1]) + html = html2.find('div', attrs = {'class': 'list compact'}).contents + html = ''.join([str(x) for x in html]) except: - pass + try: + split = splitString(html, split_on="
")[1] + html = splitString(split, split_on="
")[0] + except: + pass imdbs = getImdb(html, multiple = True) if html else [] From 08f55314d5769acb0194013a6ef3d88fe80a08eb Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 19 May 2014 19:25:16 +0200 Subject: [PATCH 321/468] Re-use imdb page parser --- .../media/movie/providers/automation/imdb.py | 133 ++++++++---------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 48b3569f..c66d7665 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -28,6 +28,34 @@ class IMDBBase(Automation, RSS): def getInfo(self, imdb_id): return fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True) + def getFromURL(self, url): + log.debug('Getting IMDBs from: %s', url) + html = self.getHTMLData(url) + + try: + split = splitString(html, split_on = "
")[1] + html = splitString(split, split_on = "
")[0] + except: + try: + split = splitString(html, split_on = "
") + html = BeautifulSoup(split[1]) + for x in ['list compact', 'lister', 'list detail sub-list']: + html2 = html.find('div', attrs = { + 'class': x + }) + + if html2: + html = html2.contents + html = ''.join([str(x) for x in html]) + break + except: + log.error('Failed parsing IMDB page "%s": %s', (url, traceback.format_exc())) + + html = str(html) + imdbs = getImdb(html, multiple = True) if html else [] + + return imdbs + class IMDBWatchlist(IMDBBase): @@ -65,22 +93,7 @@ class IMDBWatchlist(IMDBBase): try: w_url = '%s&start=%s' % (watchlist_url, start) - log.debug('Started IMDB watchlists: %s', w_url) - html = self.getHTMLData(w_url) - - try: - split = splitString(html, split_on="
") - html2 = BeautifulSoup(split[1]) - html = html2.find('div', attrs = {'class': 'list compact'}).contents - html = ''.join([str(x) for x in html]) - except: - try: - split = splitString(html, split_on="
")[1] - html = splitString(split, split_on="
")[0] - except: - pass - - imdbs = getImdb(html, multiple = True) if html else [] + imdbs = self.getFromURL(w_url) for imdb in imdbs: if imdb not in movies: @@ -115,12 +128,12 @@ class IMDBAutomation(IMDBBase): 'boxoffice': { 'order': 2, 'name': 'IMDB - Box Office', - 'url': 'http://www.imdb.com/chart/', + 'url': 'http://www.imdb.com/boxoffice/', }, 'rentals': { 'order': 3, 'name': 'IMDB - Top DVD rentals', - 'url': 'http://m.imdb.com/boxoffice_json', + 'url': 'http://www.imdb.com/boxoffice/rentals', 'type': 'json', }, 'top250': { @@ -130,8 +143,6 @@ class IMDBAutomation(IMDBBase): }, } - first_table = ['boxoffice'] - def getIMDBids(self): movies = [] @@ -141,36 +152,19 @@ class IMDBAutomation(IMDBBase): url = chart.get('url') if self.conf('automation_charts_%s' % name): - data = self.getHTMLData(url) + imdb_ids = self.getFromURL(url) - if data: - try: - html = BeautifulSoup(data) + try: + for imdb_id in imdb_ids: + info = self.getInfo(imdb_id) + if info and self.isMinimalMovie(info): + movies.append(imdb_id) - if chart.get('type', 'html') == 'html': - result_div = html.find('div', attrs = {'id': 'main'}) + if self.shuttingDown(): + break - try: - if url in self.first_table: - table = result_div.find('table') - result_div = table if table else result_div - except: - pass - - imdb_ids = getImdb(str(result_div), multiple = True) - else: - imdb_ids = getImdb(str(data), multiple = True) - - for imdb_id in imdb_ids: - info = self.getInfo(imdb_id) - if info and self.isMinimalMovie(info): - movies.append(imdb_id) - - if self.shuttingDown(): - break - - except: - log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) + except: + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) return movies @@ -188,42 +182,25 @@ class IMDBAutomation(IMDBBase): chart['list'] = [] - data = self.getHTMLData(url) - if data: - html = BeautifulSoup(data) + imdb_ids = self.getFromURL(url) - try: + try: + for imdb_id in imdb_ids[0:max_items]: - if chart.get('type', 'html') == 'html': - result_div = html.find('div', attrs = {'id': 'main'}) + is_movie = fireEvent('movie.is_movie', identifier = imdb_id, single = True) + if not is_movie: + continue - try: - if url in self.first_table: - table = result_div.find('table') - result_div = table if table else result_div - except: - pass + info = self.getInfo(imdb_id) + chart['list'].append(info) - imdb_ids = getImdb(str(result_div), multiple = True) - else: - imdb_ids = getImdb(str(data), multiple = True) + if self.shuttingDown(): + break + except: + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) - for imdb_id in imdb_ids[0:max_items]: - - is_movie = fireEvent('movie.is_movie', identifier = imdb_id, single = True) - if not is_movie: - continue - - info = self.getInfo(imdb_id) - chart['list'].append(info) - - if self.shuttingDown(): - break - except: - log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) - - if chart['list']: - movie_lists.append(chart) + if chart['list']: + movie_lists.append(chart) return movie_lists From 0097167decd1ae96f5b30890f1c6e9b36699c422 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 19 May 2014 21:07:51 +0200 Subject: [PATCH 322/468] Fanart PR cleanup --- .../media/movie/providers/info/_modifier.py | 8 +- .../media/movie/providers/info/fanarttv.py | 58 +++++----- .../media/movie/providers/info/themoviedb.py | 24 +++-- .../media/movie/providers/metadata/base.py | 46 ++++---- .../media/movie/providers/metadata/xbmc.py | 100 ++++++++---------- couchpotato/core/notifications/xbmc.py | 77 ++------------ 6 files changed, 120 insertions(+), 193 deletions(-) diff --git a/couchpotato/core/media/movie/providers/info/_modifier.py b/couchpotato/core/media/movie/providers/info/_modifier.py index beb29e74..f6a3089b 100644 --- a/couchpotato/core/media/movie/providers/info/_modifier.py +++ b/couchpotato/core/media/movie/providers/info/_modifier.py @@ -29,11 +29,11 @@ class MovieResultModifier(Plugin): 'actors': {}, 'landscape': [], 'logo': [], - 'clearart': [], - 'discart': [], + 'clear_art': [], + 'disc_art': [], 'banner': [], - 'extrathumbs': [], - 'extrafanart': [] + 'extra_thumbs': [], + 'extra_fanart': [] }, 'runtime': 0, 'plot': '', diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index 07d1a009..bc273efc 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -5,9 +5,8 @@ from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.core.plugins.quality import QualityPlugin - -from libs.fanarttv.movie import Movie -import libs.fanarttv.errors as fanarttv_errors +from fanarttv.movie import Movie +import fanarttv.errors as fanarttv_errors log = CPLog(__name__) @@ -16,10 +15,11 @@ autoload = 'FanartTV' class FanartTV(MovieProvider): + MAX_EXTRAFANART = 20 def __init__(self): - addEvent('movie.extraart', self.getArt, priority=2) + addEvent('movie.extra_art', self.getArt, priority=2) # Configure fanarttv API settings os.environ.setdefault('FANART_APIKEY', self.conf('api_key')) @@ -35,7 +35,7 @@ class FanartTV(MovieProvider): try: try: exists = True - movie = Movie.get(id=identifier) + movie = Movie.get(id = identifier) except (fanarttv_errors.FanartError, IOError): exists = False @@ -49,23 +49,23 @@ class FanartTV(MovieProvider): return images - def _parseMovie(self, movie, isHD): + def _parseMovie(self, movie, is_hd): images = { - 'landscape': [], - 'logo': [], - 'discart': [], - 'clearart': [], - 'banner': [], - 'extrafanart': [] - } + 'landscape': [], + 'logo': [], + 'disc_art': [], + 'clear_art': [], + 'banner': [], + 'extra_fanart': [], + } images['landscape'] = self._getMultImages(movie.thumbs, 1) images['banner'] = self._getMultImages(movie.banners, 1) - images['discart'] = self._getMultImages(self._trimDiscs(movie.discs, isHD), 1) + images['disc_art'] = self._getMultImages(self._trimDiscs(movie.discs, is_hd), 1) - images['clearart'] = self._getMultImages(movie.hdarts, 1) - if len(images['clearart']) is 0: - images['clearart'] = self._getMultImages(movie.arts, 1) + images['clear_art'] = self._getMultImages(movie.hdarts, 1) + if len(images['clear_art']) is 0: + images['clear_art'] = self._getMultImages(movie.arts, 1) images['logo'] = self._getMultImages(movie.hdlogos, 1) if len(images['logo']) is 0: @@ -75,29 +75,30 @@ class FanartTV(MovieProvider): if fanarts: images['backdrop_original'] = fanarts[0] - images['extrafanart'] = fanarts[1:] + images['extra_fanart'] = fanarts[1:] # TODO: Add support for extra backgrounds - #extraFanart = self._getMultImages(movie.backgrounds, -1) + #extra_fanart = self._getMultImages(movie.backgrounds, -1) return images - def _trimDiscs(self, discImages, isHD): - ''' + def _trimDiscs(self, disc_images, is_hd): + """ Return a subset of discImages based on isHD. If isHD is true, only bluray disc images will be returned. If isHD is false, only dvd disc images will be returned. If the resulting list would be an empty list, then the original list is returned instead. - ''' + """ + trimmed = [] - for disc in discImages: - if isHD and disc.disc_type == u'bluray': + for disc in disc_images: + if is_hd and disc.disc_type == u'bluray': trimmed.append(disc) - elif not isHD and disc.disc_type == u'dvd': + elif not is_hd and disc.disc_type == u'dvd': trimmed.append(disc) if len(trimmed) is 0: - return discImages + return disc_images else: return trimmed @@ -121,9 +122,9 @@ class FanartTV(MovieProvider): for image in images: if image.lang == u'en': pool.append(image) - origPoolSize = len(pool) + orig_pool_size = len(pool) - while len(pool) > 0 and (n < 0 or origPoolSize - len(pool) < n): + while len(pool) > 0 and (n < 0 or orig_pool_size - len(pool) < n): best = None highscore = -1 for image in pool: @@ -147,6 +148,7 @@ class FanartTV(MovieProvider): return bool(qualityDef.get('hd')) return False + config = [{ 'name': 'fanarttv', 'groups': [ diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index 7ffba38b..35fae5e9 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -90,8 +90,8 @@ class TheMovieDb(MovieProvider): # Get extra artwork via Fanart.TV and merge into images dict try: - extraArt = fireEvent('movie.extraart', identifier)[0] - result['images'] = dict(result['images'].items() + extraArt.items()) + extra_art = fireEvent('movie.extra_art', identifier)[0] + result['images'] = dict(result['images'].items() + extra_art.items()) except IndexError: pass @@ -108,7 +108,7 @@ class TheMovieDb(MovieProvider): poster = self.getImage(movie, type = 'poster', size = 'poster') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') - extrathumbs = self.getMultImages(movie, type='backdrops', size='original', n=self.MAX_EXTRATHUMBS, skipfirst=True) + extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original', n = self.MAX_EXTRATHUMBS, skipfirst = True) images = { 'poster': [poster] if poster else [], @@ -116,7 +116,7 @@ class TheMovieDb(MovieProvider): 'poster_original': [poster_original] if poster_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [], 'actors': {}, - 'extrathumbs': extrathumbs + 'extra_thumbs': extra_thumbs } # Genres @@ -182,22 +182,24 @@ class TheMovieDb(MovieProvider): return image_url - def getMultImages(self, movie, type='backdrops', size='original', n=-1, skipfirst=False): - ''' + def getMultImages(self, movie, type = 'backdrops', size = 'original', n = -1, skipfirst = False): + """ If n < 0, return all images. Otherwise return n images. If n > len(getattr(movie, type)), then return all images. If skipfirst is True, then it will skip getattr(movie, type)[0]. This is because backdrops[0] is typically backdrop. - ''' + """ + image_urls = [] try: images = getattr(movie, type) if n < 0 or n > len(images): - numImages = len(images) + num_images = len(images) else: - numImages = n - for i in range(int(skipfirst), numImages + int(skipfirst)): - image_urls.append(images[i].geturl(size=size)) + num_images = n + + for i in range(int(skipfirst), num_images + int(skipfirst)): + image_urls.append(images[i].geturl(size = size)) except: log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie)))) diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index ec91b062..0d25fba4 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -44,27 +44,27 @@ class MovieMetaData(MetaDataBase): except: log.error('Unable to create %s file: %s', ('nfo', traceback.format_exc())) - for file_type in ['thumbnail', 'fanart', 'banner', 'discart', 'logo', 'clearart', 'landscape', 'extrathumbs', 'extrafanart']: + for file_type in ['thumbnail', 'fanart', 'banner', 'disc_art', 'logo', 'clear_art', 'landscape', 'extra_thumbs', 'extra_fanart']: try: if file_type == 'thumbnail': - numImages = len(movie_info['images']['poster_original']) + num_images = len(movie_info['images']['poster_original']) elif file_type == 'fanart': - numImages = len(movie_info['images']['backdrop_original']) + num_images = len(movie_info['images']['backdrop_original']) else: - numImages = len(movie_info['images'][file_type]) + num_images = len(movie_info['images'][file_type]) - for i in range(numImages): + for i in range(num_images): self._createType(meta_name, root, movie_info, group, file_type, i) except: log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) - def _createType(self, meta_name, root, movie_info, group, file_type, i):# Get file path + def _createType(self, meta_name, root, movie_info, group, file_type, i): # Get file path name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root, i) if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): # Get file content - content = getattr(self, 'get' + file_type.capitalize())(movie_info=movie_info, data=group, i=i) + content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group, i = i) if content: log.debug('Creating %s file: %s', (file_type, name)) if os.path.isfile(content): @@ -123,7 +123,7 @@ class MovieMetaData(MetaDataBase): def getNfoName(self, name, root, i): return - def getNfo(self, movie_info=None, data=None, i=0): + def getNfo(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} @@ -148,39 +148,39 @@ class MovieMetaData(MetaDataBase): def getFanart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='backdrop_original', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original', i = i) - def getBanner(self, movie_info=None, data=None, i=0): + def getBanner(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='banner', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'banner', i = i) - def getClearart(self, movie_info=None, data=None, i=0): + def getClearart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='clearart', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'clear_art', i = i) - def getLogo(self, movie_info=None, data=None, i=0): + def getLogo(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='logo', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'logo', i = i) - def getDiscart(self, movie_info=None, data=None, i=0): + def getDiscart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='discart', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'disc_art', i = i) - def getLandscape(self, movie_info=None, data=None, i=0): + def getLandscape(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='landscape', i=i) + return self.getThumbnail(movie_info = movie_info, data= data, wanted_file_type = 'landscape', i = i) - def getExtrathumbs(self, movie_info=None, data=None, i=0): + def getExtrathumbs(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrathumbs', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_thumbs', i = i) - def getExtrafanart(self, movie_info=None, data=None, i=0): + def getExtrafanart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} - return self.getThumbnail(movie_info=movie_info, data=data, wanted_file_type='extrafanart', i=i) + return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_fanart', i = i) diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index 318f79a7..079174ea 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -30,28 +30,28 @@ class XBMC(MovieMetaData): return self.createMetaName(self.conf('meta_banner_name'), name, root) def getClearartName(self, name, root, i): - return self.createMetaName(self.conf('meta_clearart_name'), name, root) + return self.createMetaName(self.conf('meta_clear_art_name'), name, root) def getLogoName(self, name, root, i): return self.createMetaName(self.conf('meta_logo_name'), name, root) def getDiscartName(self, name, root, i): - return self.createMetaName(self.conf('meta_discart_name'), name, root) + return self.createMetaName(self.conf('meta_disc_art_name'), name, root) def getLandscapeName(self, name, root, i): return self.createMetaName(self.conf('meta_landscape_name'), name, root) def getExtrathumbsName(self, name, root, i): - return self.createMetaNameMult(self.conf('meta_extrathumbs_name'), name, root, i) + return self.createMetaNameMult(self.conf('meta_extra_thumbs_name'), name, root, i) def getExtrafanartName(self, name, root, i): - return self.createMetaNameMult(self.conf('meta_extrafanart_name'), name, root, i) + return self.createMetaNameMult(self.conf('meta_extra_fanart_name'), name, root, i) def createMetaName(self, basename, name, root): return os.path.join(root, basename.replace('%s', name)) def createMetaNameMult(self, basename, name, root, i): - return os.path.join(root, basename.replace('%s', name).replace('%i', str(i + 1))) + return os.path.join(root, basename.replace('%s', name).replace('', str(i + 1))) def getNfo(self, movie_info=None, data=None, i=0): if not data: data = {} @@ -153,39 +153,25 @@ class XBMC(MovieMetaData): for image_url in movie_info['images']['poster_original']: image = SubElement(nfoxml, 'thumb') image.text = toUnicode(image_url) - fanart = SubElement(nfoxml, 'fanart') - for image_url in movie_info['images']['backdrop_original']: - image = SubElement(fanart, 'thumb') - image.text = toUnicode(image_url) - banner = SubElement(nfoxml, 'banner') - for image_url in movie_info['images']['banner']: - image = SubElement(banner, 'thumb') - image.text = toUnicode(image_url) - discart = SubElement(nfoxml, 'discart') - for image_url in movie_info['images']['discart']: - image = SubElement(discart, 'thumb') - image.text = toUnicode(image_url) - logo = SubElement(nfoxml, 'logo') - for image_url in movie_info['images']['logo']: - image = SubElement(logo, 'thumb') - image.text = toUnicode(image_url) - clearart = SubElement(nfoxml, 'clearart') - for image_url in movie_info['images']['clearart']: - image = SubElement(clearart, 'thumb') - image.text = toUnicode(image_url) - landscape = SubElement(nfoxml, 'landscape') - for image_url in movie_info['images']['landscape']: - image = SubElement(landscape, 'thumb') - image.text = toUnicode(image_url) - extrathumb = SubElement(nfoxml, 'extrathumb') - for image_url in movie_info['images']['extrathumbs']: - image = SubElement(extrathumb, 'thumb') - image.text = toUnicode(image_url) - extrafanart = SubElement(nfoxml, 'extrafanart') - for image_url in movie_info['images']['extrafanart']: - image = SubElement(extrafanart, 'thumb') - image.text = toUnicode(image_url) + image_types = [ + ('fanart', 'backdrop_original'), + ('banner', 'banner'), + ('discart', 'disc_art'), + ('logo', 'logo'), + ('clearart', 'clear_art'), + ('landscape', 'landscape'), + ('extrathumb', 'extra_thumbs'), + ('extrafanart', 'extra_fanart'), + ] + + for image_type in image_types: + sub, type = image_type + + sub_element = SubElement(nfoxml, sub) + for image_url in movie_info['images'][type]: + image = SubElement(sub_element, 'thumb') + image.text = toUnicode(image_url) # Add trailer if found trailer_found = False @@ -295,7 +281,7 @@ config = [{ { 'name': 'meta_banner', 'label': 'Banner', - 'default': True, + 'default': False, 'type': 'bool' }, { @@ -305,13 +291,13 @@ config = [{ 'advanced': True, }, { - 'name': 'meta_clearart', + 'name': 'meta_clear_art', 'label': 'ClearArt', - 'default': True, + 'default': False, 'type': 'bool' }, { - 'name': 'meta_clearart_name', + 'name': 'meta_clear_art_name', 'label': 'ClearArt filename', 'default': 'clearart.png', 'advanced': True, @@ -319,11 +305,11 @@ config = [{ { 'name': 'meta_disc', 'label': 'DiscArt', - 'default': True, + 'default': False, 'type': 'bool' }, { - 'name': 'meta_discart_name', + 'name': 'meta_disc_art_name', 'label': 'DiscArt filename', 'default': 'disc.png', 'advanced': True, @@ -331,7 +317,7 @@ config = [{ { 'name': 'meta_landscape', 'label': 'Landscape', - 'default': True, + 'default': False, 'type': 'bool' }, { @@ -343,7 +329,7 @@ config = [{ { 'name': 'meta_logo', 'label': 'ClearLogo', - 'default': True, + 'default': False, 'type': 'bool' }, { @@ -353,27 +339,29 @@ config = [{ 'advanced': True, }, { - 'name': 'meta_extrathumbs', + 'name': 'meta_extra_thumbs', 'label': 'Extrathumbs', - 'default': True, + 'default': False, 'type': 'bool' }, { - 'name': 'meta_extrathumbs_name', - 'label': 'Extrathumbs filename (%i is the image number, and must be included to have multiple images).', - 'default': 'extrathumbs/thumb%i.jpg', + 'name': 'meta_extra_thumbs_name', + 'label': 'Extrathumbs filename', + 'description': '<i> is the image number, and must be included to have multiple images', + 'default': 'extrathumbs/thumb.jpg', 'advanced': True }, { - 'name': 'meta_extrafanart', - 'lavel': 'Extrafanart', - 'default': True, + 'name': 'meta_extra_fanart', + 'label': 'Extrafanart', + 'default': False, 'type': 'bool' }, { - 'name': 'meta_extrafanart_name', - 'label': 'Extrafanart filename (%i is the image number, and must be included to have multiple images).', - 'default': 'extrafanart/extrafanart%i.jpg', + 'name': 'meta_extra_fanart_name', + 'label': 'Extrafanart filename', + 'default': 'extrafanart/extrafanart.jpg', + 'description': '<i> is the image number, and must be included to have multiple images', 'advanced': True } ], diff --git a/couchpotato/core/notifications/xbmc.py b/couchpotato/core/notifications/xbmc.py index db343068..8dbf936b 100644 --- a/couchpotato/core/notifications/xbmc.py +++ b/couchpotato/core/notifications/xbmc.py @@ -3,8 +3,6 @@ import json import socket import traceback import urllib -import time -import os from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.logger import CPLog @@ -54,7 +52,7 @@ class XBMC(Notification): response = self.notifyXBMCnoJSON(host, {'title': self.default_title, 'message': message}) if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): - response += self.request(host, [('VideoLibrary.Scan', {})]) + response += self.request(host, [('VideoLibrary.Scan', None, {})]) max_successful += 1 max_successful += 1 @@ -68,50 +66,6 @@ class XBMC(Notification): except: log.error('Failed parsing results: %s', traceback.format_exc()) - - if self.conf('run_artwork_downloader') and data and self.use_json_notifications.get(host): - time.sleep(self.conf('run_artwork_downloader_delay')) - - if self.conf('force_full_scan'): - calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader'})] - max_successful += len(calls) - response = self.request(host, calls) - - try: - if response[0].get('result') and result['result'] == 'OK': - successful += 1 - elif response[0].get('error'): - log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) - except: - log.error('Failed parsing results: %s', traceback.format_exc()) - else: - calls = [('VideoLibrary.GetMovies', 'libMovies', {"filter":{"field": "title", "operator": "is", "value": data['media']['title'], "year": data['media']['info']['year']}})] - max_successful += len(calls) - response = self.request(host, calls) - - dbid = None - try: - if response[0].get('result'): - successful += 1 - dbid = response[0]['result']['movies'][-1]['movieid'] - elif response[0].get('error'): - log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) - except: - log.error('Failed parsing results: %s', traceback.format_exc()) - - if dbid is not None: - calls = [('Addons.ExecuteAddon', None, {'addonid': 'script.artwork.downloader', 'params':{'media_type': 'movie', 'dbid': str(dbid)}})] - - max_successful += len(calls) - response = self.request(host, calls) - - try: - if response[0].get('result') and result['result'] == 'OK': - successful += 1 - elif response[0].get('error'): - log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) - except: - log.error('Failed parsing results: %s', traceback.format_exc()) return successful == max_successful @@ -232,14 +186,11 @@ class XBMC(Notification): for req in do_requests: method, id, kwargs = req - if id is None: - id = method - data.append({ 'method': method, 'params': kwargs, 'jsonrpc': '2.0', - 'id': id, + 'id': id if id else method, }) data = json.dumps(data) @@ -273,7 +224,7 @@ config = [{ 'list': 'notification_providers', 'name': 'xbmc', 'label': 'XBMC', - 'description': 'v11 (Eden) and v12 (Frodo)', + 'description': 'v11 (Eden), v12 (Frodo), v13 (Gotham)', 'options': [ { 'name': 'enabled', @@ -306,7 +257,7 @@ config = [{ 'default': 0, 'type': 'bool', 'advanced': True, - 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', + 'description': ('Only scan new movie folder at remote XBMC servers.', 'Useful if the XBMC path is different from the path CPS uses.'), }, { 'name': 'force_full_scan', @@ -314,31 +265,15 @@ config = [{ 'default': 0, 'type': 'bool', 'advanced': True, - 'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.', + 'description': ('Do a full scan instead of only the new movie.', 'Useful if the XBMC path is different from the path CPS uses.'), }, { 'name': 'on_snatch', - 'default': 0, + 'default': False, 'type': 'bool', 'advanced': True, 'description': 'Also send message when movie is snatched.', }, - { - 'name': 'run_artwork_downloader', - 'label': 'Run the Artwork Downloader', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Runs the Artwork Downloader script to initialize/download artwork. (Requires the Artwork Downloader addon and XBMC Frodo or later)' - }, - { - 'name': 'run_artwork_downloader_delay', - 'label': 'Artwork Downloader delay', - 'default': 5, - 'type': 'int', - 'advanced': True, - 'description': 'Number of seconds to wait to start the Artwork Downloader script after notifying XBMC.', - }, ], } ], From 7f48210c9755468228ef8f8e6d24a6f2295c3191 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 19 May 2014 21:16:58 +0200 Subject: [PATCH 323/468] Use libs as import --- couchpotato/core/media/movie/providers/info/fanarttv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index bc273efc..fe4bb93a 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -5,8 +5,8 @@ from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.core.plugins.quality import QualityPlugin -from fanarttv.movie import Movie -import fanarttv.errors as fanarttv_errors +import libs.fanarttv.errors as fanarttv_errors +from libs.fanarttv.movie import Movie log = CPLog(__name__) From 7ceb8dc79c65ad697f51607ea773d6a49bd66db0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 19 May 2014 21:49:08 +0200 Subject: [PATCH 324/468] ILoveTorrents search fix --- .../_base/providers/torrent/ilovetorrents.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index 56b7dac5..eac2c476 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -3,7 +3,7 @@ import traceback from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -15,7 +15,7 @@ class Base(TorrentProvider): urls = { 'download': 'https://www.ilovetorrents.me/%s', - 'detail': 'https//www.ilovetorrents.me/%s', + 'detail': 'https://www.ilovetorrents.me/%s', 'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s', 'test': 'https://www.ilovetorrents.me/', 'login': 'https://www.ilovetorrents.me/takelogin.php', @@ -47,17 +47,23 @@ class Base(TorrentProvider): data = self.getHTMLData(search_url) if data: try: - soup = BeautifulSoup(data) - results_table = soup.find('table', attrs = {'class': 'koptekst'}) + results_table = None + + data_split = splitString(data, '.+'')', i['href']).group('pageNumber')) for i in pagelinks] - total_pages = max(pageNumbers) - + page_numbers = [int(re.search('page=(?P.+'')', i['href']).group('page_number')) for i in pagelinks] + total_pages = max(page_numbers) except: pass From bda6f92a4d247a66b70f4dafb38b44b9dad99b97 Mon Sep 17 00:00:00 2001 From: kackar Date: Sun, 25 May 2014 21:34:59 +0200 Subject: [PATCH 325/468] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 10d3b773..9dd5f831 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for * Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files. * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`. * You can now start CP via `CouchPotatoServer\CouchPotato.py` to start -* Your browser should open up, but if it doesn't go to: `http://localhost:5050/` +* Your browser should open up, but if it doesn't go to `http://localhost:5050/` -OSx: +OS X: * If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/) * Install [GIT](http://git-scm.com/) @@ -27,19 +27,19 @@ OSx: * Go to your App folder `cd /Applications` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Then do `python CouchPotatoServer/CouchPotato.py` -* Your browser should open up, but if it doesn't go to: `http://localhost:5050/` +* Your browser should open up, but if it doesn't go to `http://localhost:5050/` -Linux (ubuntu / debian): +Linux (Ubuntu / Debian): * Install [GIT](http://git-scm.com/) with `apt-get install git-core` * 'cd' to the folder of your choosing. * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Then do `python CouchPotatoServer/CouchPotato.py` to start -* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` -* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato` -* Make it executable. `sudo chmod +x /etc/init.d/couchpotato` -* Add it to defaults. `sudo update-rc.d couchpotato defaults` -* Open your browser and go to: `http://localhost:5050/` +* To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` +* Change the paths inside the init script `sudo nano /etc/init.d/couchpotato` +* Make it executable `sudo chmod +x /etc/init.d/couchpotato` +* Add it to defaults `sudo update-rc.d couchpotato defaults` +* Open your browser and go to `http://localhost:5050/` FreeBSD : From 013705c318dbd4e8ac1ec253831d8bc628db0b5c Mon Sep 17 00:00:00 2001 From: kackar Date: Sun, 25 May 2014 21:40:49 +0200 Subject: [PATCH 326/468] Update contributing.md --- contributing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contributing.md b/contributing.md index 0d82d71c..821212c8 100644 --- a/contributing.md +++ b/contributing.md @@ -22,11 +22,11 @@ Before you submit an issue, please go through the following checklist: * What providers are you using? (While your logs include these, scanning through hundreds of lines of logs isn't our hobby) * Post the logs from the *config* directory, please do not copy paste the UI. Use pastebin to store these logs! * Give a short step by step of how to reproduce the error. - * What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed then when you use CP on OSX or Windows. + * What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed than when you use CP on OS X or Windows. * Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag. - * If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!! - -The more relevant information you can provide, the more likely it is the issue will be resolved rather than closed. + * If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else! + +The more relevant information you provide, the more likely that your issue will be resolved. ## Pull Requests Pull requests are intended for contributing code or documentation to the project. Before you submit a pull request, consider the following: From 6691c8ddd7ce4c601aa8691e211aab316d45ed42 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 29 May 2014 19:43:33 +0200 Subject: [PATCH 327/468] Convert underscore method to proper camelcase --- couchpotato/core/helpers/variable.py | 6 ++++++ .../core/media/movie/providers/metadata/base.py | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 459d0e9b..16207a64 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -307,3 +307,9 @@ def scanForPassword(name): if m: return m.group(1).strip('. '), m.group(2).strip() + + +under_pat = re.compile(r'_([a-z])') + +def underscoreToCamel(name): + return under_pat.sub(lambda x: x.group(1).upper(), name) diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index 0d25fba4..93a513d7 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -4,7 +4,7 @@ import traceback from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import sp -from couchpotato.core.helpers.variable import getIdentifier +from couchpotato.core.helpers.variable import getIdentifier, underscoreToCamel from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.metadata.base import MetaDataBase from couchpotato.environment import Env @@ -37,7 +37,7 @@ class MovieMetaData(MetaDataBase): root = os.path.dirname(root_name) movie_info = group['media'].get('info') - + for file_type in ['nfo']: try: self._createType(meta_name, root, movie_info, group, file_type, 0) @@ -59,12 +59,13 @@ class MovieMetaData(MetaDataBase): log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) def _createType(self, meta_name, root, movie_info, group, file_type, i): # Get file path - name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root, i) + camelcase_method = underscoreToCamel(file_type.capitalize()) + name = getattr(self, 'get' + camelcase_method + 'Name')(meta_name, root, i) if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None): # Get file content - content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group, i = i) + content = getattr(self, 'get' + camelcase_method)(movie_info = movie_info, data = group, i = i) if content: log.debug('Creating %s file: %s', (file_type, name)) if os.path.isfile(content): @@ -113,7 +114,7 @@ class MovieMetaData(MetaDataBase): def getLandscapeName(self, name, root, i): return - + def getExtrathumbsName(self, name, root, i): return From c094120f04930590341ee478bf184cdf12b2ea5b Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 29 May 2014 19:50:36 +0200 Subject: [PATCH 328/468] Do verify requests --- couchpotato/core/plugins/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 7cc4d1c6..1d9c0c42 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -205,7 +205,7 @@ class Plugin(object): method = 'post' if len(data) > 0 or files else 'get' log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data')) - response = r.request(method, url, verify = False, **kwargs) + response = r.request(method, url, **kwargs) if response.status_code == requests.codes.ok: data = response.content From e714604ec0929c1c22a915989b88ec472e53c3a6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 29 May 2014 20:01:22 +0200 Subject: [PATCH 329/468] Requests 2.3.0 --- libs/requests/__init__.py | 8 +- libs/requests/adapters.py | 44 +- libs/requests/api.py | 2 +- libs/requests/auth.py | 3 - libs/requests/compat.py | 6 +- libs/requests/cookies.py | 65 +- libs/requests/exceptions.py | 22 +- libs/requests/models.py | 88 ++- libs/requests/packages/README.rst | 8 + libs/requests/packages/charade/__main__.py | 7 - .../packages/{charade => chardet}/__init__.py | 36 +- .../packages/{charade => chardet}/big5freq.py | 0 .../{charade => chardet}/big5prober.py | 0 libs/requests/packages/chardet/chardetect.py | 46 ++ .../{charade => chardet}/chardistribution.py | 0 .../charsetgroupprober.py | 0 .../{charade => chardet}/charsetprober.py | 0 .../codingstatemachine.py | 0 .../packages/{charade => chardet}/compat.py | 0 .../{charade => chardet}/constants.py | 0 .../{charade => chardet}/cp949prober.py | 0 .../{charade => chardet}/escprober.py | 0 .../packages/{charade => chardet}/escsm.py | 0 .../{charade => chardet}/eucjpprober.py | 0 .../{charade => chardet}/euckrfreq.py | 0 .../{charade => chardet}/euckrprober.py | 0 .../{charade => chardet}/euctwfreq.py | 0 .../{charade => chardet}/euctwprober.py | 0 .../{charade => chardet}/gb2312freq.py | 0 .../{charade => chardet}/gb2312prober.py | 0 .../{charade => chardet}/hebrewprober.py | 0 .../packages/{charade => chardet}/jisfreq.py | 0 .../packages/{charade => chardet}/jpcntx.py | 2 +- .../langbulgarianmodel.py | 0 .../{charade => chardet}/langcyrillicmodel.py | 0 .../{charade => chardet}/langgreekmodel.py | 0 .../{charade => chardet}/langhebrewmodel.py | 0 .../langhungarianmodel.py | 0 .../{charade => chardet}/langthaimodel.py | 0 .../{charade => chardet}/latin1prober.py | 2 +- .../{charade => chardet}/mbcharsetprober.py | 0 .../{charade => chardet}/mbcsgroupprober.py | 0 .../packages/{charade => chardet}/mbcssm.py | 0 .../{charade => chardet}/sbcharsetprober.py | 0 .../{charade => chardet}/sbcsgroupprober.py | 0 .../{charade => chardet}/sjisprober.py | 0 .../{charade => chardet}/universaldetector.py | 18 +- .../{charade => chardet}/utf8prober.py | 0 .../requests/packages/urllib3/_collections.py | 106 ++- libs/requests/packages/urllib3/connection.py | 123 +++- .../packages/urllib3/connectionpool.py | 127 ++-- .../packages/urllib3/contrib/pyopenssl.py | 90 ++- libs/requests/packages/urllib3/exceptions.py | 5 + libs/requests/packages/urllib3/fields.py | 2 +- libs/requests/packages/urllib3/filepost.py | 11 +- .../packages/ssl_match_hostname/__init__.py | 2 +- libs/requests/packages/urllib3/poolmanager.py | 4 +- libs/requests/packages/urllib3/request.py | 1 - libs/requests/packages/urllib3/response.py | 18 +- libs/requests/packages/urllib3/util.py | 643 ------------------ .../packages/urllib3/util/__init__.py | 27 + .../packages/urllib3/util/connection.py | 45 ++ .../requests/packages/urllib3/util/request.py | 68 ++ .../packages/urllib3/util/response.py | 13 + libs/requests/packages/urllib3/util/ssl_.py | 133 ++++ .../requests/packages/urllib3/util/timeout.py | 234 +++++++ libs/requests/packages/urllib3/util/url.py | 162 +++++ libs/requests/sessions.py | 137 +++- libs/requests/structures.py | 26 +- libs/requests/utils.py | 92 ++- 70 files changed, 1463 insertions(+), 963 deletions(-) create mode 100644 libs/requests/packages/README.rst delete mode 100644 libs/requests/packages/charade/__main__.py rename libs/requests/packages/{charade => chardet}/__init__.py (57%) rename libs/requests/packages/{charade => chardet}/big5freq.py (100%) rename libs/requests/packages/{charade => chardet}/big5prober.py (100%) create mode 100644 libs/requests/packages/chardet/chardetect.py rename libs/requests/packages/{charade => chardet}/chardistribution.py (100%) rename libs/requests/packages/{charade => chardet}/charsetgroupprober.py (100%) rename libs/requests/packages/{charade => chardet}/charsetprober.py (100%) rename libs/requests/packages/{charade => chardet}/codingstatemachine.py (100%) rename libs/requests/packages/{charade => chardet}/compat.py (100%) rename libs/requests/packages/{charade => chardet}/constants.py (100%) rename libs/requests/packages/{charade => chardet}/cp949prober.py (100%) rename libs/requests/packages/{charade => chardet}/escprober.py (100%) rename libs/requests/packages/{charade => chardet}/escsm.py (100%) rename libs/requests/packages/{charade => chardet}/eucjpprober.py (100%) rename libs/requests/packages/{charade => chardet}/euckrfreq.py (100%) rename libs/requests/packages/{charade => chardet}/euckrprober.py (100%) rename libs/requests/packages/{charade => chardet}/euctwfreq.py (100%) rename libs/requests/packages/{charade => chardet}/euctwprober.py (100%) rename libs/requests/packages/{charade => chardet}/gb2312freq.py (100%) rename libs/requests/packages/{charade => chardet}/gb2312prober.py (100%) rename libs/requests/packages/{charade => chardet}/hebrewprober.py (100%) rename libs/requests/packages/{charade => chardet}/jisfreq.py (100%) rename libs/requests/packages/{charade => chardet}/jpcntx.py (99%) rename libs/requests/packages/{charade => chardet}/langbulgarianmodel.py (100%) rename libs/requests/packages/{charade => chardet}/langcyrillicmodel.py (100%) rename libs/requests/packages/{charade => chardet}/langgreekmodel.py (100%) rename libs/requests/packages/{charade => chardet}/langhebrewmodel.py (100%) rename libs/requests/packages/{charade => chardet}/langhungarianmodel.py (100%) rename libs/requests/packages/{charade => chardet}/langthaimodel.py (100%) rename libs/requests/packages/{charade => chardet}/latin1prober.py (98%) rename libs/requests/packages/{charade => chardet}/mbcharsetprober.py (100%) rename libs/requests/packages/{charade => chardet}/mbcsgroupprober.py (100%) rename libs/requests/packages/{charade => chardet}/mbcssm.py (100%) rename libs/requests/packages/{charade => chardet}/sbcharsetprober.py (100%) rename libs/requests/packages/{charade => chardet}/sbcsgroupprober.py (100%) rename libs/requests/packages/{charade => chardet}/sjisprober.py (100%) rename libs/requests/packages/{charade => chardet}/universaldetector.py (90%) rename libs/requests/packages/{charade => chardet}/utf8prober.py (100%) delete mode 100644 libs/requests/packages/urllib3/util.py create mode 100644 libs/requests/packages/urllib3/util/__init__.py create mode 100644 libs/requests/packages/urllib3/util/connection.py create mode 100644 libs/requests/packages/urllib3/util/request.py create mode 100644 libs/requests/packages/urllib3/util/response.py create mode 100644 libs/requests/packages/urllib3/util/ssl_.py create mode 100644 libs/requests/packages/urllib3/util/timeout.py create mode 100644 libs/requests/packages/urllib3/util/url.py diff --git a/libs/requests/__init__.py b/libs/requests/__init__.py index d5d258e8..bba19002 100644 --- a/libs/requests/__init__.py +++ b/libs/requests/__init__.py @@ -36,17 +36,17 @@ usage: The other HTTP methods are supported - see `requests.api`. Full documentation is at . -:copyright: (c) 2013 by Kenneth Reitz. +:copyright: (c) 2014 by Kenneth Reitz. :license: Apache 2.0, see LICENSE for more details. """ __title__ = 'requests' -__version__ = '2.1.0' -__build__ = 0x020100 +__version__ = '2.3.0' +__build__ = 0x020300 __author__ = 'Kenneth Reitz' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2013 Kenneth Reitz' +__copyright__ = 'Copyright 2014 Kenneth Reitz' # Attempt to enable urllib3's SNI support, if possible try: diff --git a/libs/requests/adapters.py b/libs/requests/adapters.py index b62f64c8..eb7a2d28 100644 --- a/libs/requests/adapters.py +++ b/libs/requests/adapters.py @@ -16,7 +16,7 @@ from .packages.urllib3.response import HTTPResponse from .packages.urllib3.util import Timeout as TimeoutSauce from .compat import urlparse, basestring, urldefrag, unquote from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers, - except_on_missing_scheme, get_auth_from_url) + prepend_scheme_if_needed, get_auth_from_url) from .structures import CaseInsensitiveDict from .packages.urllib3.exceptions import MaxRetryError from .packages.urllib3.exceptions import TimeoutError @@ -55,14 +55,16 @@ class HTTPAdapter(BaseAdapter): :param pool_connections: The number of urllib3 connection pools to cache. :param pool_maxsize: The maximum number of connections to save in the pool. - :param max_retries: The maximum number of retries each connection should attempt. + :param int max_retries: The maximum number of retries each connection + should attempt. Note, this applies only to failed connections and + timeouts, never to requests where the server returns a response. :param pool_block: Whether the connection pool should block for connections. Usage:: >>> import requests >>> s = requests.Session() - >>> a = requests.adapters.HTTPAdapter() + >>> a = requests.adapters.HTTPAdapter(max_retries=3) >>> s.mount('http://', a) """ __attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize', @@ -88,6 +90,11 @@ class HTTPAdapter(BaseAdapter): self.__attrs__) def __setstate__(self, state): + # Can't handle by adding 'proxy_manager' to self.__attrs__ because + # because self.poolmanager uses a lambda function, which isn't pickleable. + self.proxy_manager = {} + self.config = {} + for attr, value in state.items(): setattr(self, attr, value) @@ -196,13 +203,16 @@ class HTTPAdapter(BaseAdapter): proxy = proxies.get(urlparse(url.lower()).scheme) if proxy: - except_on_missing_scheme(proxy) + proxy = prepend_scheme_if_needed(proxy, 'http') proxy_headers = self.proxy_headers(proxy) if not proxy in self.proxy_manager: self.proxy_manager[proxy] = proxy_from_url( proxy, - proxy_headers=proxy_headers) + proxy_headers=proxy_headers, + num_pools=self._pool_connections, + maxsize=self._pool_maxsize, + block=self._pool_block) conn = self.proxy_manager[proxy].connection_from_url(url) else: @@ -276,10 +286,6 @@ class HTTPAdapter(BaseAdapter): username, password = get_auth_from_url(proxy) if username and password: - # Proxy auth usernames and passwords will be urlencoded, we need - # to decode them. - username = unquote(username) - password = unquote(password) headers['Proxy-Authorization'] = _basic_auth_str(username, password) @@ -304,10 +310,7 @@ class HTTPAdapter(BaseAdapter): chunked = not (request.body is None or 'Content-Length' in request.headers) - if stream: - timeout = TimeoutSauce(connect=timeout) - else: - timeout = TimeoutSauce(connect=timeout, read=timeout) + timeout = TimeoutSauce(connect=timeout, read=timeout) try: if not chunked: @@ -366,25 +369,20 @@ class HTTPAdapter(BaseAdapter): conn._put_conn(low_conn) except socket.error as sockerr: - raise ConnectionError(sockerr) + raise ConnectionError(sockerr, request=request) except MaxRetryError as e: - raise ConnectionError(e) + raise ConnectionError(e, request=request) except _ProxyError as e: raise ProxyError(e) except (_SSLError, _HTTPError) as e: if isinstance(e, _SSLError): - raise SSLError(e) + raise SSLError(e, request=request) elif isinstance(e, TimeoutError): - raise Timeout(e) + raise Timeout(e, request=request) else: raise - r = self.build_response(request, resp) - - if not stream: - r.content - - return r + return self.build_response(request, resp) diff --git a/libs/requests/api.py b/libs/requests/api.py index baf43dd6..01d853d5 100644 --- a/libs/requests/api.py +++ b/libs/requests/api.py @@ -26,7 +26,7 @@ def request(method, url, **kwargs): :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload. :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. - :param timeout: (optional) Float describing the timeout of the request. + :param timeout: (optional) Float describing the timeout of the request in seconds. :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed. :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. diff --git a/libs/requests/auth.py b/libs/requests/auth.py index 6664cd80..9f831b7a 100644 --- a/libs/requests/auth.py +++ b/libs/requests/auth.py @@ -11,7 +11,6 @@ import os import re import time import hashlib -import logging from base64 import b64encode @@ -19,8 +18,6 @@ from .compat import urlparse, str from .cookies import extract_cookies_to_jar from .utils import parse_dict_header -log = logging.getLogger(__name__) - CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' CONTENT_TYPE_MULTI_PART = 'multipart/form-data' diff --git a/libs/requests/compat.py b/libs/requests/compat.py index 0d61a572..84d703b6 100644 --- a/libs/requests/compat.py +++ b/libs/requests/compat.py @@ -4,7 +4,7 @@ pythoncompat """ -from .packages import charade as chardet +from .packages import chardet import sys @@ -75,7 +75,9 @@ is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess. try: import simplejson as json -except ImportError: +except (ImportError, SyntaxError): + # simplejson does not support Python 3.2, it thows a SyntaxError + # because of u'...' Unicode literals. import json # --------- diff --git a/libs/requests/cookies.py b/libs/requests/cookies.py index c465f552..831c49c6 100644 --- a/libs/requests/cookies.py +++ b/libs/requests/cookies.py @@ -198,30 +198,39 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping): self.set_cookie(c) return c + def iterkeys(self): + """Dict-like iterkeys() that returns an iterator of names of cookies from the jar. + See itervalues() and iteritems().""" + for cookie in iter(self): + yield cookie.name + def keys(self): """Dict-like keys() that returns a list of names of cookies from the jar. See values() and items().""" - keys = [] + return list(self.iterkeys()) + + def itervalues(self): + """Dict-like itervalues() that returns an iterator of values of cookies from the jar. + See iterkeys() and iteritems().""" for cookie in iter(self): - keys.append(cookie.name) - return keys + yield cookie.value def values(self): """Dict-like values() that returns a list of values of cookies from the jar. See keys() and items().""" - values = [] + return list(self.itervalues()) + + def iteritems(self): + """Dict-like iteritems() that returns an iterator of name-value tuples from the jar. + See iterkeys() and itervalues().""" for cookie in iter(self): - values.append(cookie.value) - return values + yield cookie.name, cookie.value def items(self): """Dict-like items() that returns a list of name-value tuples from the jar. See keys() and values(). Allows client-code to call "dict(RequestsCookieJar) and get a vanilla python dict of key value pairs.""" - items = [] - for cookie in iter(self): - items.append((cookie.name, cookie.value)) - return items + return list(self.iteritems()) def list_domains(self): """Utility method to list all the domains in the jar.""" @@ -378,29 +387,29 @@ def create_cookie(name, value, **kwargs): def morsel_to_cookie(morsel): """Convert a Morsel object into a Cookie containing the one k/v pair.""" + expires = None - if morsel["max-age"]: - expires = time.time() + morsel["max-age"] + if morsel['max-age']: + expires = time.time() + morsel['max-age'] elif morsel['expires']: - expires = morsel['expires'] - if type(expires) == type(""): - time_template = "%a, %d-%b-%Y %H:%M:%S GMT" - expires = time.mktime(time.strptime(expires, time_template)) - c = create_cookie( - name=morsel.key, - value=morsel.value, - version=morsel['version'] or 0, - port=None, - domain=morsel['domain'], - path=morsel['path'], - secure=bool(morsel['secure']), - expires=expires, - discard=False, + time_template = '%a, %d-%b-%Y %H:%M:%S GMT' + expires = time.mktime( + time.strptime(morsel['expires'], time_template)) - time.timezone + return create_cookie( comment=morsel['comment'], comment_url=bool(morsel['comment']), + discard=False, + domain=morsel['domain'], + expires=expires, + name=morsel.key, + path=morsel['path'], + port=None, rest={'HttpOnly': morsel['httponly']}, - rfc2109=False,) - return c + rfc2109=False, + secure=bool(morsel['secure']), + value=morsel.value, + version=morsel['version'] or 0, + ) def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True): diff --git a/libs/requests/exceptions.py b/libs/requests/exceptions.py index bc42b5ff..a4ee9d63 100644 --- a/libs/requests/exceptions.py +++ b/libs/requests/exceptions.py @@ -7,21 +7,29 @@ requests.exceptions This module contains the set of Requests' exceptions. """ +from .packages.urllib3.exceptions import HTTPError as BaseHTTPError class RequestException(IOError): """There was an ambiguous exception that occurred while handling your request.""" + def __init__(self, *args, **kwargs): + """ + Initialize RequestException with `request` and `response` objects. + """ + response = kwargs.pop('response', None) + self.response = response + self.request = kwargs.pop('request', None) + if (response is not None and not self.request and + hasattr(response, 'request')): + self.request = self.response.request + super(RequestException, self).__init__(*args, **kwargs) + class HTTPError(RequestException): """An HTTP error occurred.""" - def __init__(self, *args, **kwargs): - """ Initializes HTTPError with optional `response` object. """ - self.response = kwargs.pop('response', None) - super(HTTPError, self).__init__(*args, **kwargs) - class ConnectionError(RequestException): """A Connection error occurred.""" @@ -61,3 +69,7 @@ class InvalidURL(RequestException, ValueError): class ChunkedEncodingError(RequestException): """The server declared chunked encoding but sent an invalid chunk.""" + + +class ContentDecodingError(RequestException, BaseHTTPError): + """Failed to decode response content""" diff --git a/libs/requests/models.py b/libs/requests/models.py index 34dce181..9b16b9d8 100644 --- a/libs/requests/models.py +++ b/libs/requests/models.py @@ -8,7 +8,6 @@ This module contains the primary objects that power Requests. """ import collections -import logging import datetime from io import BytesIO, UnsupportedOperation @@ -20,9 +19,10 @@ from .cookies import cookiejar_from_dict, get_cookie_header from .packages.urllib3.fields import RequestField from .packages.urllib3.filepost import encode_multipart_formdata from .packages.urllib3.util import parse_url +from .packages.urllib3.exceptions import DecodeError from .exceptions import ( HTTPError, RequestException, MissingSchema, InvalidURL, - ChunkedEncodingError) + ChunkedEncodingError, ContentDecodingError) from .utils import ( guess_filename, get_auth_from_url, requote_uri, stream_decode_response_unicode, to_key_val_list, parse_header_links, @@ -30,12 +30,20 @@ from .utils import ( from .compat import ( cookielib, urlunparse, urlsplit, urlencode, str, bytes, StringIO, is_py2, chardet, json, builtin_str, basestring, IncompleteRead) +from .status_codes import codes +#: The set of HTTP status codes that indicate an automatically +#: processable redirect. +REDIRECT_STATI = ( + codes.moved, # 301 + codes.found, # 302 + codes.other, # 303 + codes.temporary_moved, # 307 +) +DEFAULT_REDIRECT_LIMIT = 30 CONTENT_CHUNK_SIZE = 10 * 1024 ITER_CHUNK_SIZE = 512 -log = logging.getLogger(__name__) - class RequestEncodingMixin(object): @property @@ -400,9 +408,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): is_stream = all([ hasattr(data, '__iter__'), - not isinstance(data, basestring), - not isinstance(data, list), - not isinstance(data, dict) + not isinstance(data, (basestring, list, tuple, dict)) ]) try: @@ -427,7 +433,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): else: if data: body = self._encode_params(data) - if isinstance(data, str) or isinstance(data, builtin_str) or hasattr(data, 'read'): + if isinstance(data, basestring) or hasattr(data, 'read'): content_type = None else: content_type = 'application/x-www-form-urlencoded' @@ -516,7 +522,7 @@ class Response(object): self._content = False self._content_consumed = False - #: Integer Code of responded HTTP Status. + #: Integer Code of responded HTTP Status, e.g. 404 or 200. self.status_code = None #: Case-insensitive Dictionary of Response Headers. @@ -525,7 +531,7 @@ class Response(object): self.headers = CaseInsensitiveDict() #: File-like object representation of response (for advanced usage). - #: Requires that ``stream=True` on the request. + #: Use of ``raw`` requires that ``stream=True`` be set on the request. # This requirement does not apply for use internally to Requests. self.raw = None @@ -540,6 +546,7 @@ class Response(object): #: up here. The list is sorted from the oldest to the most recent request. self.history = [] + #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". self.reason = None #: A CookieJar of Cookies the server sent back. @@ -549,6 +556,10 @@ class Response(object): #: and the arrival of the response (as a timedelta) self.elapsed = datetime.timedelta(0) + #: The :class:`PreparedRequest ` object to which this + #: is a response. + self.request = None + def __getstate__(self): # Consume everything; accessing the content attribute makes # sure the content has been fully read. @@ -566,6 +577,7 @@ class Response(object): # pickled objects do not have .raw setattr(self, '_content_consumed', True) + setattr(self, 'raw', None) def __repr__(self): return '' % (self.status_code) @@ -590,10 +602,16 @@ class Response(object): return False return True + @property + def is_redirect(self): + """True if this Response is a well-formed HTTP redirect that could have + been processed automatically (by :meth:`Session.resolve_redirects`). + """ + return ('location' in self.headers and self.status_code in REDIRECT_STATI) + @property def apparent_encoding(self): - """The apparent encoding, provided by the lovely Charade library - (Thanks, Ian!).""" + """The apparent encoding, provided by the chardet library""" return chardet.detect(self.content)['encoding'] def iter_content(self, chunk_size=1, decode_unicode=False): @@ -602,20 +620,20 @@ class Response(object): large responses. The chunk size is the number of bytes it should read into memory. This is not necessarily the length of each item returned as decoding can take place. - """ - if self._content_consumed: - # simulate reading small chunks of the content - return iter_slices(self._content, chunk_size) + If decode_unicode is True, content will be decoded using the best + available encoding based on the response. + """ def generate(): try: # Special case for urllib3. try: - for chunk in self.raw.stream(chunk_size, - decode_content=True): + for chunk in self.raw.stream(chunk_size, decode_content=True): yield chunk except IncompleteRead as e: raise ChunkedEncodingError(e) + except DecodeError as e: + raise ContentDecodingError(e) except AttributeError: # Standard file-like object. while True: @@ -626,12 +644,17 @@ class Response(object): self._content_consumed = True - gen = generate() + # simulate reading small chunks of the content + reused_chunks = iter_slices(self._content, chunk_size) + + stream_chunks = generate() + + chunks = reused_chunks if self._content_consumed else stream_chunks if decode_unicode: - gen = stream_decode_response_unicode(gen, self) + chunks = stream_decode_response_unicode(chunks, self) - return gen + return chunks def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None): """Iterates over the response data, one line at a time. When @@ -641,8 +664,7 @@ class Response(object): pending = None - for chunk in self.iter_content(chunk_size=chunk_size, - decode_unicode=decode_unicode): + for chunk in self.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode): if pending is not None: chunk = pending + chunk @@ -688,7 +710,12 @@ class Response(object): """Content of the response, in unicode. If Response.encoding is None, encoding will be guessed using - ``charade``. + ``chardet``. + + The encoding of the response content is determined based solely on HTTP + headers, following RFC 2616 to the letter. If you can take advantage of + non-HTTP knowledge to make a better guess at the encoding, you should + set ``r.encoding`` appropriately before accessing this property. """ # Try charset from content-type @@ -729,7 +756,14 @@ class Response(object): # a best guess). encoding = guess_json_utf(self.content) if encoding is not None: - return json.loads(self.content.decode(encoding), **kwargs) + try: + return json.loads(self.content.decode(encoding), **kwargs) + except UnicodeDecodeError: + # Wrong UTF codec detected; usually because it's not UTF-8 + # but some other 8-bit codec. This is an RFC violation, + # and the server didn't bother to tell us what codec *was* + # used. + pass return json.loads(self.text, **kwargs) @property @@ -765,8 +799,8 @@ class Response(object): raise HTTPError(http_error_msg, response=self) def close(self): - """Closes the underlying file descriptor and releases the connection - back to the pool. + """Releases the connection back to the pool. Once this method has been + called the underlying ``raw`` object must not be accessed again. *Note: Should not normally need to be called explicitly.* """ diff --git a/libs/requests/packages/README.rst b/libs/requests/packages/README.rst new file mode 100644 index 00000000..c42f376b --- /dev/null +++ b/libs/requests/packages/README.rst @@ -0,0 +1,8 @@ +If you are planning to submit a pull request to requests with any changes in +this library do not go any further. These are independent libraries which we +vendor into requests. Any changes necessary to these libraries must be made in +them and submitted as separate pull requests to those libraries. + +urllib3 pull requests go here: https://github.com/shazow/urllib3 + +chardet pull requests go here: https://github.com/chardet/chardet diff --git a/libs/requests/packages/charade/__main__.py b/libs/requests/packages/charade/__main__.py deleted file mode 100644 index 0fc37c34..00000000 --- a/libs/requests/packages/charade/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -''' -support ';python -m charade [file2] ...' package execution syntax (2.7+) -''' - -from charade import charade_cli - -charade_cli() diff --git a/libs/requests/packages/charade/__init__.py b/libs/requests/packages/chardet/__init__.py similarity index 57% rename from libs/requests/packages/charade/__init__.py rename to libs/requests/packages/chardet/__init__.py index 743c874d..e4f0799d 100644 --- a/libs/requests/packages/charade/__init__.py +++ b/libs/requests/packages/chardet/__init__.py @@ -15,7 +15,7 @@ # 02110-1301 USA ######################### END LICENSE BLOCK ######################### -__version__ = "1.0.3" +__version__ = "2.2.1" from sys import version_info @@ -30,37 +30,3 @@ def detect(aBuf): u.feed(aBuf) u.close() return u.result - -def _description_of(path): - """Return a string describing the probable encoding of a file.""" - from charade.universaldetector import UniversalDetector - - u = UniversalDetector() - for line in open(path, 'rb'): - u.feed(line) - u.close() - result = u.result - if result['encoding']: - return '%s: %s with confidence %s' % (path, - result['encoding'], - result['confidence']) - else: - return '%s: no result' % path - - -def charade_cli(): - """ - Script which takes one or more file paths and reports on their detected - encodings - - Example:: - - % chardetect.py somefile someotherfile - somefile: windows-1252 with confidence 0.5 - someotherfile: ascii with confidence 1.0 - - """ - from sys import argv - for path in argv[1:]: - print(_description_of(path)) - \ No newline at end of file diff --git a/libs/requests/packages/charade/big5freq.py b/libs/requests/packages/chardet/big5freq.py similarity index 100% rename from libs/requests/packages/charade/big5freq.py rename to libs/requests/packages/chardet/big5freq.py diff --git a/libs/requests/packages/charade/big5prober.py b/libs/requests/packages/chardet/big5prober.py similarity index 100% rename from libs/requests/packages/charade/big5prober.py rename to libs/requests/packages/chardet/big5prober.py diff --git a/libs/requests/packages/chardet/chardetect.py b/libs/requests/packages/chardet/chardetect.py new file mode 100644 index 00000000..ecd0163b --- /dev/null +++ b/libs/requests/packages/chardet/chardetect.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +""" +Script which takes one or more file paths and reports on their detected +encodings + +Example:: + + % chardetect somefile someotherfile + somefile: windows-1252 with confidence 0.5 + someotherfile: ascii with confidence 1.0 + +If no paths are provided, it takes its input from stdin. + +""" +from io import open +from sys import argv, stdin + +from chardet.universaldetector import UniversalDetector + + +def description_of(file, name='stdin'): + """Return a string describing the probable encoding of a file.""" + u = UniversalDetector() + for line in file: + u.feed(line) + u.close() + result = u.result + if result['encoding']: + return '%s: %s with confidence %s' % (name, + result['encoding'], + result['confidence']) + else: + return '%s: no result' % name + + +def main(): + if len(argv) <= 1: + print(description_of(stdin)) + else: + for path in argv[1:]: + with open(path, 'rb') as f: + print(description_of(f, path)) + + +if __name__ == '__main__': + main() diff --git a/libs/requests/packages/charade/chardistribution.py b/libs/requests/packages/chardet/chardistribution.py similarity index 100% rename from libs/requests/packages/charade/chardistribution.py rename to libs/requests/packages/chardet/chardistribution.py diff --git a/libs/requests/packages/charade/charsetgroupprober.py b/libs/requests/packages/chardet/charsetgroupprober.py similarity index 100% rename from libs/requests/packages/charade/charsetgroupprober.py rename to libs/requests/packages/chardet/charsetgroupprober.py diff --git a/libs/requests/packages/charade/charsetprober.py b/libs/requests/packages/chardet/charsetprober.py similarity index 100% rename from libs/requests/packages/charade/charsetprober.py rename to libs/requests/packages/chardet/charsetprober.py diff --git a/libs/requests/packages/charade/codingstatemachine.py b/libs/requests/packages/chardet/codingstatemachine.py similarity index 100% rename from libs/requests/packages/charade/codingstatemachine.py rename to libs/requests/packages/chardet/codingstatemachine.py diff --git a/libs/requests/packages/charade/compat.py b/libs/requests/packages/chardet/compat.py similarity index 100% rename from libs/requests/packages/charade/compat.py rename to libs/requests/packages/chardet/compat.py diff --git a/libs/requests/packages/charade/constants.py b/libs/requests/packages/chardet/constants.py similarity index 100% rename from libs/requests/packages/charade/constants.py rename to libs/requests/packages/chardet/constants.py diff --git a/libs/requests/packages/charade/cp949prober.py b/libs/requests/packages/chardet/cp949prober.py similarity index 100% rename from libs/requests/packages/charade/cp949prober.py rename to libs/requests/packages/chardet/cp949prober.py diff --git a/libs/requests/packages/charade/escprober.py b/libs/requests/packages/chardet/escprober.py similarity index 100% rename from libs/requests/packages/charade/escprober.py rename to libs/requests/packages/chardet/escprober.py diff --git a/libs/requests/packages/charade/escsm.py b/libs/requests/packages/chardet/escsm.py similarity index 100% rename from libs/requests/packages/charade/escsm.py rename to libs/requests/packages/chardet/escsm.py diff --git a/libs/requests/packages/charade/eucjpprober.py b/libs/requests/packages/chardet/eucjpprober.py similarity index 100% rename from libs/requests/packages/charade/eucjpprober.py rename to libs/requests/packages/chardet/eucjpprober.py diff --git a/libs/requests/packages/charade/euckrfreq.py b/libs/requests/packages/chardet/euckrfreq.py similarity index 100% rename from libs/requests/packages/charade/euckrfreq.py rename to libs/requests/packages/chardet/euckrfreq.py diff --git a/libs/requests/packages/charade/euckrprober.py b/libs/requests/packages/chardet/euckrprober.py similarity index 100% rename from libs/requests/packages/charade/euckrprober.py rename to libs/requests/packages/chardet/euckrprober.py diff --git a/libs/requests/packages/charade/euctwfreq.py b/libs/requests/packages/chardet/euctwfreq.py similarity index 100% rename from libs/requests/packages/charade/euctwfreq.py rename to libs/requests/packages/chardet/euctwfreq.py diff --git a/libs/requests/packages/charade/euctwprober.py b/libs/requests/packages/chardet/euctwprober.py similarity index 100% rename from libs/requests/packages/charade/euctwprober.py rename to libs/requests/packages/chardet/euctwprober.py diff --git a/libs/requests/packages/charade/gb2312freq.py b/libs/requests/packages/chardet/gb2312freq.py similarity index 100% rename from libs/requests/packages/charade/gb2312freq.py rename to libs/requests/packages/chardet/gb2312freq.py diff --git a/libs/requests/packages/charade/gb2312prober.py b/libs/requests/packages/chardet/gb2312prober.py similarity index 100% rename from libs/requests/packages/charade/gb2312prober.py rename to libs/requests/packages/chardet/gb2312prober.py diff --git a/libs/requests/packages/charade/hebrewprober.py b/libs/requests/packages/chardet/hebrewprober.py similarity index 100% rename from libs/requests/packages/charade/hebrewprober.py rename to libs/requests/packages/chardet/hebrewprober.py diff --git a/libs/requests/packages/charade/jisfreq.py b/libs/requests/packages/chardet/jisfreq.py similarity index 100% rename from libs/requests/packages/charade/jisfreq.py rename to libs/requests/packages/chardet/jisfreq.py diff --git a/libs/requests/packages/charade/jpcntx.py b/libs/requests/packages/chardet/jpcntx.py similarity index 99% rename from libs/requests/packages/charade/jpcntx.py rename to libs/requests/packages/chardet/jpcntx.py index 04634422..f7f69ba4 100644 --- a/libs/requests/packages/charade/jpcntx.py +++ b/libs/requests/packages/chardet/jpcntx.py @@ -169,7 +169,7 @@ class JapaneseContextAnalysis: def get_confidence(self): # This is just one way to calculate confidence. It works well for me. if self._mTotalRel > MINIMUM_DATA_THRESHOLD: - return float(self._mTotalRel - self._mRelSample[0]) / self._mTotalRel + return (self._mTotalRel - self._mRelSample[0]) / self._mTotalRel else: return DONT_KNOW diff --git a/libs/requests/packages/charade/langbulgarianmodel.py b/libs/requests/packages/chardet/langbulgarianmodel.py similarity index 100% rename from libs/requests/packages/charade/langbulgarianmodel.py rename to libs/requests/packages/chardet/langbulgarianmodel.py diff --git a/libs/requests/packages/charade/langcyrillicmodel.py b/libs/requests/packages/chardet/langcyrillicmodel.py similarity index 100% rename from libs/requests/packages/charade/langcyrillicmodel.py rename to libs/requests/packages/chardet/langcyrillicmodel.py diff --git a/libs/requests/packages/charade/langgreekmodel.py b/libs/requests/packages/chardet/langgreekmodel.py similarity index 100% rename from libs/requests/packages/charade/langgreekmodel.py rename to libs/requests/packages/chardet/langgreekmodel.py diff --git a/libs/requests/packages/charade/langhebrewmodel.py b/libs/requests/packages/chardet/langhebrewmodel.py similarity index 100% rename from libs/requests/packages/charade/langhebrewmodel.py rename to libs/requests/packages/chardet/langhebrewmodel.py diff --git a/libs/requests/packages/charade/langhungarianmodel.py b/libs/requests/packages/chardet/langhungarianmodel.py similarity index 100% rename from libs/requests/packages/charade/langhungarianmodel.py rename to libs/requests/packages/chardet/langhungarianmodel.py diff --git a/libs/requests/packages/charade/langthaimodel.py b/libs/requests/packages/chardet/langthaimodel.py similarity index 100% rename from libs/requests/packages/charade/langthaimodel.py rename to libs/requests/packages/chardet/langthaimodel.py diff --git a/libs/requests/packages/charade/latin1prober.py b/libs/requests/packages/chardet/latin1prober.py similarity index 98% rename from libs/requests/packages/charade/latin1prober.py rename to libs/requests/packages/chardet/latin1prober.py index 5e2c9f90..ad695f57 100644 --- a/libs/requests/packages/charade/latin1prober.py +++ b/libs/requests/packages/chardet/latin1prober.py @@ -129,7 +129,7 @@ class Latin1Prober(CharSetProber): if total < 0.01: confidence = 0.0 else: - confidence = ((float(self._mFreqCounter[3]) / total) + confidence = ((self._mFreqCounter[3] / total) - (self._mFreqCounter[1] * 20.0 / total)) if confidence < 0.0: confidence = 0.0 diff --git a/libs/requests/packages/charade/mbcharsetprober.py b/libs/requests/packages/chardet/mbcharsetprober.py similarity index 100% rename from libs/requests/packages/charade/mbcharsetprober.py rename to libs/requests/packages/chardet/mbcharsetprober.py diff --git a/libs/requests/packages/charade/mbcsgroupprober.py b/libs/requests/packages/chardet/mbcsgroupprober.py similarity index 100% rename from libs/requests/packages/charade/mbcsgroupprober.py rename to libs/requests/packages/chardet/mbcsgroupprober.py diff --git a/libs/requests/packages/charade/mbcssm.py b/libs/requests/packages/chardet/mbcssm.py similarity index 100% rename from libs/requests/packages/charade/mbcssm.py rename to libs/requests/packages/chardet/mbcssm.py diff --git a/libs/requests/packages/charade/sbcharsetprober.py b/libs/requests/packages/chardet/sbcharsetprober.py similarity index 100% rename from libs/requests/packages/charade/sbcharsetprober.py rename to libs/requests/packages/chardet/sbcharsetprober.py diff --git a/libs/requests/packages/charade/sbcsgroupprober.py b/libs/requests/packages/chardet/sbcsgroupprober.py similarity index 100% rename from libs/requests/packages/charade/sbcsgroupprober.py rename to libs/requests/packages/chardet/sbcsgroupprober.py diff --git a/libs/requests/packages/charade/sjisprober.py b/libs/requests/packages/chardet/sjisprober.py similarity index 100% rename from libs/requests/packages/charade/sjisprober.py rename to libs/requests/packages/chardet/sjisprober.py diff --git a/libs/requests/packages/charade/universaldetector.py b/libs/requests/packages/chardet/universaldetector.py similarity index 90% rename from libs/requests/packages/charade/universaldetector.py rename to libs/requests/packages/chardet/universaldetector.py index 7ccea84f..9a03ad3d 100644 --- a/libs/requests/packages/charade/universaldetector.py +++ b/libs/requests/packages/chardet/universaldetector.py @@ -74,10 +74,12 @@ class UniversalDetector: if aBuf[:3] == codecs.BOM: # EF BB BF UTF-8 with BOM self.result = {'encoding': "UTF-8", 'confidence': 1.0} - elif aBuf[:4] in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE): + elif aBuf[:4] == codecs.BOM_UTF32_LE: # FF FE 00 00 UTF-32, little-endian BOM + self.result = {'encoding': "UTF-32LE", 'confidence': 1.0} + elif aBuf[:4] == codecs.BOM_UTF32_BE: # 00 00 FE FF UTF-32, big-endian BOM - self.result = {'encoding': "UTF-32", 'confidence': 1.0} + self.result = {'encoding': "UTF-32BE", 'confidence': 1.0} elif aBuf[:4] == b'\xFE\xFF\x00\x00': # FE FF 00 00 UCS-4, unusual octet order BOM (3412) self.result = { @@ -90,10 +92,12 @@ class UniversalDetector: 'encoding': "X-ISO-10646-UCS-4-2143", 'confidence': 1.0 } - elif aBuf[:2] == codecs.BOM_LE or aBuf[:2] == codecs.BOM_BE: + elif aBuf[:2] == codecs.BOM_LE: # FF FE UTF-16, little endian BOM + self.result = {'encoding': "UTF-16LE", 'confidence': 1.0} + elif aBuf[:2] == codecs.BOM_BE: # FE FF UTF-16, big endian BOM - self.result = {'encoding': "UTF-16", 'confidence': 1.0} + self.result = {'encoding': "UTF-16BE", 'confidence': 1.0} self._mGotData = True if self.result['encoding'] and (self.result['confidence'] > 0.0): @@ -113,10 +117,8 @@ class UniversalDetector: if not self._mEscCharSetProber: self._mEscCharSetProber = EscCharSetProber() if self._mEscCharSetProber.feed(aBuf) == constants.eFoundIt: - self.result = { - 'encoding': self._mEscCharSetProber.get_charset_name(), - 'confidence': self._mEscCharSetProber.get_confidence() - } + self.result = {'encoding': self._mEscCharSetProber.get_charset_name(), + 'confidence': self._mEscCharSetProber.get_confidence()} self.done = True elif self._mInputState == eHighbyte: if not self._mCharSetProbers: diff --git a/libs/requests/packages/charade/utf8prober.py b/libs/requests/packages/chardet/utf8prober.py similarity index 100% rename from libs/requests/packages/charade/utf8prober.py rename to libs/requests/packages/chardet/utf8prober.py diff --git a/libs/requests/packages/urllib3/_collections.py b/libs/requests/packages/urllib3/_collections.py index 5907b0dc..9cea3a44 100644 --- a/libs/requests/packages/urllib3/_collections.py +++ b/libs/requests/packages/urllib3/_collections.py @@ -4,7 +4,7 @@ # This module is part of urllib3 and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from collections import MutableMapping +from collections import Mapping, MutableMapping try: from threading import RLock except ImportError: # Platform-specific: No threads available @@ -20,9 +20,10 @@ try: # Python 2.7+ from collections import OrderedDict except ImportError: from .packages.ordered_dict import OrderedDict +from .packages.six import itervalues -__all__ = ['RecentlyUsedContainer'] +__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] _Null = object() @@ -101,3 +102,104 @@ class RecentlyUsedContainer(MutableMapping): def keys(self): with self.lock: return self._container.keys() + + +class HTTPHeaderDict(MutableMapping): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 2616. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + + If you want to access the raw headers with their original casing + for debugging purposes you can access the private ``._data`` attribute + which is a normal python ``dict`` that maps the case-insensitive key to a + list of tuples stored as (case-sensitive-original-name, value). Using the + structure from above as our example: + + >>> headers._data + {'set-cookie': [('Set-Cookie', 'foo=bar'), ('set-cookie', 'baz=quxx')], + 'content-length': [('content-length', '7')]} + """ + + def __init__(self, headers=None, **kwargs): + self._data = {} + if headers is None: + headers = {} + self.update(headers, **kwargs) + + def add(self, key, value): + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + """ + self._data.setdefault(key.lower(), []).append((key, value)) + + def getlist(self, key): + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + return self[key].split(', ') if key in self else [] + + def copy(self): + h = HTTPHeaderDict() + for key in self._data: + for rawkey, value in self._data[key]: + h.add(rawkey, value) + return h + + def __eq__(self, other): + if not isinstance(other, Mapping): + return False + other = HTTPHeaderDict(other) + return dict((k1, self[k1]) for k1 in self._data) == \ + dict((k2, other[k2]) for k2 in other._data) + + def __getitem__(self, key): + values = self._data[key.lower()] + return ', '.join(value[1] for value in values) + + def __setitem__(self, key, value): + self._data[key.lower()] = [(key, value)] + + def __delitem__(self, key): + del self._data[key.lower()] + + def __len__(self): + return len(self._data) + + def __iter__(self): + for headers in itervalues(self._data): + yield headers[0][0] + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, dict(self.items())) diff --git a/libs/requests/packages/urllib3/connection.py b/libs/requests/packages/urllib3/connection.py index e240786a..5feb3322 100644 --- a/libs/requests/packages/urllib3/connection.py +++ b/libs/requests/packages/urllib3/connection.py @@ -4,13 +4,14 @@ # This module is part of urllib3 and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php +import sys import socket from socket import timeout as SocketTimeout try: # Python 3 - from http.client import HTTPConnection, HTTPException + from http.client import HTTPConnection as _HTTPConnection, HTTPException except ImportError: - from httplib import HTTPConnection, HTTPException + from httplib import HTTPConnection as _HTTPConnection, HTTPException class DummyConnection(object): "Used to detect a failed ConnectionCls import." @@ -24,9 +25,9 @@ try: # Compiled with SSL? pass try: # Python 3 - from http.client import HTTPSConnection + from http.client import HTTPSConnection as _HTTPSConnection except ImportError: - from httplib import HTTPSConnection + from httplib import HTTPSConnection as _HTTPSConnection import ssl BaseSSLError = ssl.SSLError @@ -38,6 +39,7 @@ from .exceptions import ( ConnectTimeoutError, ) from .packages.ssl_match_hostname import match_hostname +from .packages import six from .util import ( assert_fingerprint, resolve_cert_reqs, @@ -45,6 +47,88 @@ from .util import ( ssl_wrap_socket, ) + +port_by_scheme = { + 'http': 80, + 'https': 443, +} + + +class HTTPConnection(_HTTPConnection, object): + """ + Based on httplib.HTTPConnection but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + """ + + default_port = port_by_scheme['http'] + + # By default, disable Nagle's Algorithm. + tcp_nodelay = 1 + + def __init__(self, *args, **kw): + if six.PY3: # Python 3 + kw.pop('strict', None) + if sys.version_info < (2, 7): # Python 2.6 and older + kw.pop('source_address', None) + + # Pre-set source_address in case we have an older Python like 2.6. + self.source_address = kw.get('source_address') + + # Superclass also sets self.source_address in Python 2.7+. + _HTTPConnection.__init__(self, *args, **kw) + + def _new_conn(self): + """ Establish a socket connection and set nodelay settings on it. + + :return: a new socket connection + """ + extra_args = [] + if self.source_address: # Python 2.7+ + extra_args.append(self.source_address) + + conn = socket.create_connection( + (self.host, self.port), self.timeout, *extra_args) + conn.setsockopt( + socket.IPPROTO_TCP, socket.TCP_NODELAY, self.tcp_nodelay) + + return conn + + def _prepare_conn(self, conn): + self.sock = conn + # the _tunnel_host attribute was added in python 2.6.3 (via + # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do + # not have them. + if getattr(self, '_tunnel_host', None): + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + + +class HTTPSConnection(HTTPConnection): + default_port = port_by_scheme['https'] + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kw): + + HTTPConnection.__init__(self, host, port, strict=strict, + timeout=timeout, **kw) + + self.key_file = key_file + self.cert_file = cert_file + + # Required property for Google AppEngine 1.9.0 which otherwise causes + # HTTPS requests to go out as HTTP. (See Issue #356) + self._protocol = 'https' + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + self.sock = ssl.wrap_socket(conn, self.key_file, self.cert_file) + + class VerifiedHTTPSConnection(HTTPSConnection): """ Based on httplib.HTTPSConnection but wraps the socket with @@ -53,6 +137,7 @@ class VerifiedHTTPSConnection(HTTPSConnection): cert_reqs = None ca_certs = None ssl_version = None + conn_kw = {} def set_cert(self, key_file=None, cert_file=None, cert_reqs=None, ca_certs=None, @@ -67,31 +152,41 @@ class VerifiedHTTPSConnection(HTTPSConnection): def connect(self): # Add certificate verification + try: sock = socket.create_connection( - address=(self.host, self.port), - timeout=self.timeout, - ) + address=(self.host, self.port), timeout=self.timeout, + **self.conn_kw) except SocketTimeout: - raise ConnectTimeoutError( - self, "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout)) + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout)) + + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, + self.tcp_nodelay) resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs) resolved_ssl_version = resolve_ssl_version(self.ssl_version) - if self._tunnel_host: + hostname = self.host + if getattr(self, '_tunnel_host', None): + # _tunnel_host was added in Python 2.6.3 + # (See: http://hg.python.org/cpython/rev/0f57b30a152f) + self.sock = sock # Calls self._set_hostport(), so self.host is # self._tunnel_host below. self._tunnel() + # Override the host with the one we're requesting data from. + hostname = self._tunnel_host + # Wrap socket using verification with the root certs in # trusted_root_certs self.sock = ssl_wrap_socket(sock, self.key_file, self.cert_file, cert_reqs=resolved_cert_reqs, ca_certs=self.ca_certs, - server_hostname=self.host, + server_hostname=hostname, ssl_version=resolved_ssl_version) if resolved_cert_reqs != ssl.CERT_NONE: @@ -100,8 +195,10 @@ class VerifiedHTTPSConnection(HTTPSConnection): self.assert_fingerprint) elif self.assert_hostname is not False: match_hostname(self.sock.getpeercert(), - self.assert_hostname or self.host) + self.assert_hostname or hostname) if ssl: + # Make a copy for testing. + UnverifiedHTTPSConnection = HTTPSConnection HTTPSConnection = VerifiedHTTPSConnection diff --git a/libs/requests/packages/urllib3/connectionpool.py b/libs/requests/packages/urllib3/connectionpool.py index 72011b5a..95a53a7d 100644 --- a/libs/requests/packages/urllib3/connectionpool.py +++ b/libs/requests/packages/urllib3/connectionpool.py @@ -4,6 +4,7 @@ # This module is part of urllib3 and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php +import sys import errno import logging @@ -19,9 +20,11 @@ except ImportError: from .exceptions import ( ClosedPoolError, + ConnectionError, ConnectTimeoutError, EmptyPoolError, HostChangedError, + LocationParseError, MaxRetryError, SSLError, TimeoutError, @@ -31,6 +34,7 @@ from .exceptions import ( from .packages.ssl_match_hostname import CertificateError from .packages import six from .connection import ( + port_by_scheme, DummyConnection, HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, HTTPException, BaseSSLError, @@ -38,7 +42,6 @@ from .connection import ( from .request import RequestMethods from .response import HTTPResponse from .util import ( - assert_fingerprint, get_host, is_connection_dropped, Timeout, @@ -51,12 +54,6 @@ log = logging.getLogger(__name__) _Default = object() -port_by_scheme = { - 'http': 80, - 'https': 443, -} - - ## Pool objects class ConnectionPool(object): @@ -69,6 +66,9 @@ class ConnectionPool(object): QueueCls = LifoQueue def __init__(self, host, port=None): + if host is None: + raise LocationParseError(host) + # httplib doesn't like it when we include brackets in ipv6 addresses host = host.strip('[]') @@ -140,7 +140,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): def __init__(self, host, port=None, strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, - headers=None, _proxy=None, _proxy_headers=None): + headers=None, _proxy=None, _proxy_headers=None, **conn_kw): ConnectionPool.__init__(self, host, port) RequestMethods.__init__(self, headers) @@ -167,21 +167,26 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): self.num_connections = 0 self.num_requests = 0 + if sys.version_info < (2, 7): # Python 2.6 and older + conn_kw.pop('source_address', None) + self.conn_kw = conn_kw + def _new_conn(self): """ - Return a fresh :class:`httplib.HTTPConnection`. + Return a fresh :class:`HTTPConnection`. """ self.num_connections += 1 log.info("Starting new HTTP connection (%d): %s" % (self.num_connections, self.host)) - extra_params = {} - if not six.PY3: # Python 2 - extra_params['strict'] = self.strict - - return self.ConnectionCls(host=self.host, port=self.port, + conn = self.ConnectionCls(host=self.host, port=self.port, timeout=self.timeout.connect_timeout, - **extra_params) + strict=self.strict, **self.conn_kw) + if self.proxy is not None: + # Enable Nagle's algorithm for proxies, to avoid packet + # fragmentation. + conn.tcp_nodelay = 0 + return conn def _get_conn(self, timeout=None): """ @@ -238,8 +243,9 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): pass except Full: # This should never happen if self.block == True - log.warning("HttpConnectionPool is full, discarding connection: %s" - % self.host) + log.warning( + "Connection pool is full, discarding connection: %s" % + self.host) # Connection never got put back into the pool, close it. if conn: @@ -260,7 +266,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): def _make_request(self, conn, method, url, timeout=_Default, **httplib_request_kw): """ - Perform a request on a given httplib connection object taken from our + Perform a request on a given urllib connection object taken from our pool. :param conn: @@ -371,9 +377,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # TODO: Add optional support for socket.gethostbyname checking. scheme, host, port = get_host(url) + # Use explicit default port for comparison when none is given if self.port and not port: - # Use explicit default port for comparison when none is given. port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None return (scheme, host, port) == (self.scheme, self.host, self.port) @@ -412,10 +420,13 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): :param retries: Number of retries to allow before raising a MaxRetryError exception. + If `False`, then retries are disabled and any exception is raised + immediately. :param redirect: If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. :param assert_same_host: If ``True``, will make sure that the host of the pool requests is @@ -449,7 +460,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if headers is None: headers = self.headers - if retries < 0: + if retries < 0 and retries is not False: raise MaxRetryError(self, url) if release_conn is None: @@ -468,6 +479,10 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): headers = headers.copy() headers.update(self.proxy_headers) + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + try: # Request a connection from the queue conn = self._get_conn(timeout=pool_timeout) @@ -495,38 +510,41 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # ``response.read()``) except Empty: - # Timed out by queue + # Timed out by queue. raise EmptyPoolError(self, "No pool connections are available.") - except BaseSSLError as e: + except (BaseSSLError, CertificateError) as e: + # Release connection unconditionally because there is no way to + # close it externally in case of exception. + release_conn = True raise SSLError(e) - except CertificateError as e: - # Name mismatch - raise SSLError(e) + except (TimeoutError, HTTPException, SocketError) as e: + if conn: + # Discard the connection for these exceptions. It will be + # be replaced during the next _get_conn() call. + conn.close() + conn = None - except TimeoutError as e: - # Connection broken, discard. - conn = None - # Save the error off for retry logic. - err = e + if not retries: + if isinstance(e, TimeoutError): + # TimeoutError is exempt from MaxRetryError-wrapping. + # FIXME: ... Not sure why. Add a reason here. + raise - if retries == 0: - raise + # Wrap unexpected exceptions with the most appropriate + # module-level exception and re-raise. + if isinstance(e, SocketError) and self.proxy: + raise ProxyError('Cannot connect to proxy.', e) - except (HTTPException, SocketError) as e: - if isinstance(e, SocketError) and self.proxy is not None: - raise ProxyError('Cannot connect to proxy. ' - 'Socket error: %s.' % e) + if retries is False: + raise ConnectionError('Connection failed.', e) - # Connection broken, discard. It will be replaced next _get_conn(). - conn = None - # This is necessary so we can access e below - err = e - - if retries == 0: raise MaxRetryError(self, url, e) + # Keep track of the error for the retry warning. + err = e + finally: if release_conn: # Put the connection back to be reused. If the connection is @@ -536,8 +554,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if not conn: # Try again - log.warn("Retrying (%d attempts remain) after connection " - "broken by '%r': %s" % (retries, err, url)) + log.warning("Retrying (%d attempts remain) after connection " + "broken by '%r': %s" % (retries, err, url)) return self.urlopen(method, url, body, headers, retries - 1, redirect, assert_same_host, timeout=timeout, pool_timeout=pool_timeout, @@ -545,7 +563,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # Handle redirect? redirect_location = redirect and response.get_redirect_location() - if redirect_location: + if redirect_location and retries is not False: if response.status == 303: method = 'GET' log.info("Redirecting %s -> %s" % (url, redirect_location)) @@ -563,7 +581,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): When Python is compiled with the :mod:`ssl` module, then :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, - instead of :class:`httplib.HTTPSConnection`. + instead of :class:`.HTTPSConnection`. :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, ``assert_hostname`` and ``host`` in this order to verify connections. @@ -584,10 +602,14 @@ class HTTPSConnectionPool(HTTPConnectionPool): _proxy=None, _proxy_headers=None, key_file=None, cert_file=None, cert_reqs=None, ca_certs=None, ssl_version=None, - assert_hostname=None, assert_fingerprint=None): + assert_hostname=None, assert_fingerprint=None, + **conn_kw): + + if sys.version_info < (2, 7): # Python 2.6 or older + conn_kw.pop('source_address', None) HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, - block, headers, _proxy, _proxy_headers) + block, headers, _proxy, _proxy_headers, **conn_kw) self.key_file = key_file self.cert_file = cert_file self.cert_reqs = cert_reqs @@ -595,6 +617,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): self.ssl_version = ssl_version self.assert_hostname = assert_hostname self.assert_fingerprint = assert_fingerprint + self.conn_kw = conn_kw def _prepare_conn(self, conn): """ @@ -610,6 +633,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): assert_hostname=self.assert_hostname, assert_fingerprint=self.assert_fingerprint) conn.ssl_version = self.ssl_version + conn.conn_kw = self.conn_kw if self.proxy is not None: # Python 2.7+ @@ -646,10 +670,15 @@ class HTTPSConnectionPool(HTTPConnectionPool): extra_params = {} if not six.PY3: # Python 2 extra_params['strict'] = self.strict + extra_params.update(self.conn_kw) conn = self.ConnectionCls(host=actual_host, port=actual_port, timeout=self.timeout.connect_timeout, **extra_params) + if self.proxy is not None: + # Enable Nagle's algorithm for proxies, to avoid packet + # fragmentation. + conn.tcp_nodelay = 0 return self._prepare_conn(conn) diff --git a/libs/requests/packages/urllib3/contrib/pyopenssl.py b/libs/requests/packages/urllib3/contrib/pyopenssl.py index f78e7170..21a12c68 100644 --- a/libs/requests/packages/urllib3/contrib/pyopenssl.py +++ b/libs/requests/packages/urllib3/contrib/pyopenssl.py @@ -1,4 +1,7 @@ -'''SSL with SNI-support for Python 2. +'''SSL with SNI_-support for Python 2. Follow these instructions if you would +like to verify SSL certificates in Python 2. Note, the default libraries do +*not* do certificate checking; you need to do additional work to validate +certificates yourself. This needs the following packages installed: @@ -6,9 +9,15 @@ This needs the following packages installed: * ndg-httpsclient (tested with 0.3.2) * pyasn1 (tested with 0.1.6) -To activate it call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3`. -This can be done in a ``sitecustomize`` module, or at any other time before -your application begins using ``urllib3``, like this:: +You can install them with the following command: + + pip install pyopenssl ndg-httpsclient pyasn1 + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this:: try: import urllib3.contrib.pyopenssl @@ -18,13 +27,31 @@ your application begins using ``urllib3``, like this:: Now you can use :mod:`urllib3` as you normally would, and it will support SNI when the required modules are installed. + +Activating this module also has the positive side effect of disabling SSL/TLS +encryption in Python 2 (see `CRIME attack`_). + +If you want to configure the default list of supported cipher suites, you can +set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. + +Module Variables +---------------- + +:var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites. + Default: ``ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES: + ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS`` + +.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) + ''' from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT -from ndg.httpsclient.subj_alt_name import SubjectAltName +from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName import OpenSSL.SSL from pyasn1.codec.der import decoder as der_decoder -from socket import _fileobject +from pyasn1.type import univ, constraint +from socket import _fileobject, timeout import ssl import select from cStringIO import StringIO @@ -50,6 +77,23 @@ _openssl_verify = { + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, } +# A secure default. +# Sources for more information on TLS ciphers: +# +# - https://wiki.mozilla.org/Security/Server_Side_TLS +# - https://www.ssllabs.com/projects/best-practices/index.html +# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ +# +# The general intent is: +# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), +# - prefer ECDHE over DHE for better performance, +# - prefer any AES-GCM over any AES-CBC for better performance and security, +# - use 3DES as fallback which is secure but slow, +# - disable NULL authentication, MD5 MACs and DSS for security reasons. +DEFAULT_SSL_CIPHER_LIST = "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:" + \ + "ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:" + \ + "!aNULL:!MD5:!DSS" + orig_util_HAS_SNI = util.HAS_SNI orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket @@ -69,6 +113,17 @@ def extract_from_urllib3(): util.HAS_SNI = orig_util_HAS_SNI +### Note: This is a slightly bug-fixed version of same from ndg-httpsclient. +class SubjectAltName(BaseSubjectAltName): + '''ASN.1 implementation for subjectAltNames support''' + + # There is no limit to how many SAN certificates a certificate may have, + # however this needs to have some limit so we'll set an arbitrarily high + # limit. + sizeSpec = univ.SequenceOf.sizeSpec + \ + constraint.ValueSizeConstraint(1, 1024) + + ### Note: This is a slightly bug-fixed version of same from ndg-httpsclient. def get_subj_alt_name(peer_cert): # Search through extensions @@ -102,6 +157,13 @@ def get_subj_alt_name(peer_cert): class fileobject(_fileobject): + def _wait_for_sock(self): + rd, wd, ed = select.select([self._sock], [], [], + self._sock.gettimeout()) + if not rd: + raise timeout() + + def read(self, size=-1): # Use max, disallow tiny reads in a loop as they are very inefficient. # We never leave read() with any leftover data from a new recv() call @@ -119,6 +181,7 @@ class fileobject(_fileobject): try: data = self._sock.recv(rbufsize) except OpenSSL.SSL.WantReadError: + self._wait_for_sock() continue if not data: break @@ -146,6 +209,7 @@ class fileobject(_fileobject): try: data = self._sock.recv(left) except OpenSSL.SSL.WantReadError: + self._wait_for_sock() continue if not data: break @@ -197,6 +261,7 @@ class fileobject(_fileobject): break buffers.append(data) except OpenSSL.SSL.WantReadError: + self._wait_for_sock() continue break return "".join(buffers) @@ -207,6 +272,7 @@ class fileobject(_fileobject): try: data = self._sock.recv(self._rbufsize) except OpenSSL.SSL.WantReadError: + self._wait_for_sock() continue if not data: break @@ -234,7 +300,8 @@ class fileobject(_fileobject): try: data = self._sock.recv(self._rbufsize) except OpenSSL.SSL.WantReadError: - continue + self._wait_for_sock() + continue if not data: break left = size - buf_len @@ -329,6 +396,15 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, ctx.load_verify_locations(ca_certs, None) except OpenSSL.SSL.Error as e: raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e) + else: + ctx.set_default_verify_paths() + + # Disable TLS compression to migitate CRIME attack (issue #309) + OP_NO_COMPRESSION = 0x20000 + ctx.set_options(OP_NO_COMPRESSION) + + # Set list of supported ciphersuites. + ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST) cnx = OpenSSL.SSL.Connection(ctx, sock) cnx.set_tlsext_host_name(server_hostname) diff --git a/libs/requests/packages/urllib3/exceptions.py b/libs/requests/packages/urllib3/exceptions.py index 98ef9abc..b4df831f 100644 --- a/libs/requests/packages/urllib3/exceptions.py +++ b/libs/requests/packages/urllib3/exceptions.py @@ -44,6 +44,11 @@ class ProxyError(HTTPError): pass +class ConnectionError(HTTPError): + "Raised when a normal connection fails." + pass + + class DecodeError(HTTPError): "Raised when automatic decoding based on Content-Type fails." pass diff --git a/libs/requests/packages/urllib3/fields.py b/libs/requests/packages/urllib3/fields.py index ed017657..da79e929 100644 --- a/libs/requests/packages/urllib3/fields.py +++ b/libs/requests/packages/urllib3/fields.py @@ -15,7 +15,7 @@ def guess_content_type(filename, default='application/octet-stream'): Guess the "Content-Type" of a file. :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetimes`. + The filename to guess the "Content-Type" of using :mod:`mimetypes`. :param default: If no "Content-Type" can be guessed, default to `default`. """ diff --git a/libs/requests/packages/urllib3/filepost.py b/libs/requests/packages/urllib3/filepost.py index 4575582e..e8b30bdd 100644 --- a/libs/requests/packages/urllib3/filepost.py +++ b/libs/requests/packages/urllib3/filepost.py @@ -46,16 +46,15 @@ def iter_field_objects(fields): def iter_fields(fields): """ + .. deprecated:: 1.6 + Iterate over fields. - .. deprecated :: - - The addition of `~urllib3.fields.RequestField` makes this function - obsolete. Instead, use :func:`iter_field_objects`, which returns - `~urllib3.fields.RequestField` objects, instead. + The addition of :class:`~urllib3.fields.RequestField` makes this function + obsolete. Instead, use :func:`iter_field_objects`, which returns + :class:`~urllib3.fields.RequestField` objects. Supports list of (k, v) tuples and dicts. - """ if isinstance(fields, dict): return ((k, v) for k, v in six.iteritems(fields)) diff --git a/libs/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py b/libs/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py index 3aa5b2e1..dd59a75f 100644 --- a/libs/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py +++ b/libs/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py @@ -7,7 +7,7 @@ except ImportError: from backports.ssl_match_hostname import CertificateError, match_hostname except ImportError: # Our vendored copy - from _implementation import CertificateError, match_hostname + from ._implementation import CertificateError, match_hostname # Not needed, but documenting what we provide. __all__ = ('CertificateError', 'match_hostname') diff --git a/libs/requests/packages/urllib3/poolmanager.py b/libs/requests/packages/urllib3/poolmanager.py index c16519f8..f18ff2bb 100644 --- a/libs/requests/packages/urllib3/poolmanager.py +++ b/libs/requests/packages/urllib3/poolmanager.py @@ -1,5 +1,5 @@ # urllib3/poolmanager.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) # # This module is part of urllib3 and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -176,7 +176,7 @@ class ProxyManager(PoolManager): Behaves just like :class:`PoolManager`, but sends all requests through the defined proxy, using the CONNECT method for HTTPS URLs. - :param poxy_url: + :param proxy_url: The URL of the proxy to be used. :param proxy_headers: diff --git a/libs/requests/packages/urllib3/request.py b/libs/requests/packages/urllib3/request.py index 66a9a0e6..2a92cc20 100644 --- a/libs/requests/packages/urllib3/request.py +++ b/libs/requests/packages/urllib3/request.py @@ -45,7 +45,6 @@ class RequestMethods(object): """ _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS']) - _encode_body_methods = set(['PATCH', 'POST', 'PUT', 'TRACE']) def __init__(self, headers=None): self.headers = headers or {} diff --git a/libs/requests/packages/urllib3/response.py b/libs/requests/packages/urllib3/response.py index 6a1fe1a7..db441828 100644 --- a/libs/requests/packages/urllib3/response.py +++ b/libs/requests/packages/urllib3/response.py @@ -9,6 +9,7 @@ import logging import zlib import io +from ._collections import HTTPHeaderDict from .exceptions import DecodeError from .packages.six import string_types as basestring, binary_type from .util import is_fp_closed @@ -79,7 +80,10 @@ class HTTPResponse(io.IOBase): def __init__(self, body='', headers=None, status=0, version=0, reason=None, strict=0, preload_content=True, decode_content=True, original_response=None, pool=None, connection=None): - self.headers = headers or {} + + self.headers = HTTPHeaderDict() + if headers: + self.headers.update(headers) self.status = status self.version = version self.reason = reason @@ -249,17 +253,9 @@ class HTTPResponse(io.IOBase): with ``original_response=r``. """ - # Normalize headers between different versions of Python - headers = {} + headers = HTTPHeaderDict() for k, v in r.getheaders(): - # Python 3: Header keys are returned capitalised - k = k.lower() - - has_value = headers.get(k) - if has_value: # Python 3: Repeating header keys are unmerged. - v = ', '.join([has_value, v]) - - headers[k] = v + headers.add(k, v) # HTTPResponse objects in Python 3 don't have a .strict attribute strict = getattr(r, 'strict', 0) diff --git a/libs/requests/packages/urllib3/util.py b/libs/requests/packages/urllib3/util.py deleted file mode 100644 index 46a0c48d..00000000 --- a/libs/requests/packages/urllib3/util.py +++ /dev/null @@ -1,643 +0,0 @@ -# urllib3/util.py -# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) -# -# This module is part of urllib3 and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - - -from base64 import b64encode -from binascii import hexlify, unhexlify -from collections import namedtuple -from hashlib import md5, sha1 -from socket import error as SocketError, _GLOBAL_DEFAULT_TIMEOUT -import time - -try: - from select import poll, POLLIN -except ImportError: # `poll` doesn't exist on OSX and other platforms - poll = False - try: - from select import select - except ImportError: # `select` doesn't exist on AppEngine. - select = False - -try: # Test for SSL features - SSLContext = None - HAS_SNI = False - - import ssl - from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 - from ssl import SSLContext # Modern SSL? - from ssl import HAS_SNI # Has SNI? -except ImportError: - pass - -from .packages import six -from .exceptions import LocationParseError, SSLError, TimeoutStateError - - -_Default = object() -# The default timeout to use for socket connections. This is the attribute used -# by httplib to define the default timeout - - -def current_time(): - """ - Retrieve the current time, this function is mocked out in unit testing. - """ - return time.time() - - -class Timeout(object): - """ - Utility object for storing timeout values. - - Example usage: - - .. code-block:: python - - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout) - pool.request(...) # Etc, etc - - :param connect: - The maximum amount of time to wait for a connection attempt to a server - to succeed. Omitting the parameter will default the connect timeout to - the system default, probably `the global default timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: integer, float, or None - - :param read: - The maximum amount of time to wait between consecutive - read operations for a response from the server. Omitting - the parameter will default the read timeout to the system - default, probably `the global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: integer, float, or None - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: integer, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. Specifically, Python's DNS resolver does not obey the - timeout specified on the socket. Other factors that can affect total - request time include high CPU load, high swap, the program running at a - low priority level, or other behaviors. The observed running time for - urllib3 to return a response may be greater than the value passed to - `total`. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not ever trigger, even though the request will - take several minutes to complete. - - If your goal is to cut off any request after a set amount of wall clock - time, consider having a second "watcher" thread to cut off a slow - request. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT - - def __init__(self, total=None, connect=_Default, read=_Default): - self._connect = self._validate_timeout(connect, 'connect') - self._read = self._validate_timeout(read, 'read') - self.total = self._validate_timeout(total, 'total') - self._start_connect = None - - def __str__(self): - return '%s(connect=%r, read=%r, total=%r)' % ( - type(self).__name__, self._connect, self._read, self.total) - - - @classmethod - def _validate_timeout(cls, value, name): - """ Check that a timeout attribute is valid - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is used - for clear error messages - :return: the value - :raises ValueError: if the type is not an integer or a float, or if it - is a numeric value less than zero - """ - if value is _Default: - return cls.DEFAULT_TIMEOUT - - if value is None or value is cls.DEFAULT_TIMEOUT: - return value - - try: - float(value) - except (TypeError, ValueError): - raise ValueError("Timeout value %s was %s, but it must be an " - "int or float." % (name, value)) - - try: - if value < 0: - raise ValueError("Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than 0." % (name, value)) - except TypeError: # Python 3 - raise ValueError("Timeout value %s was %s, but it must be an " - "int or float." % (name, value)) - - return value - - @classmethod - def from_float(cls, timeout): - """ Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value passed - to this function. - - :param timeout: The legacy timeout value - :type timeout: integer, float, sentinel default object, or None - :return: a Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self): - """ Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, - total=self.total) - - def start_connect(self): - """ Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = current_time() - return self._start_connect - - def get_connect_duration(self): - """ Gets the time elapsed since the call to :meth:`start_connect`. - - :return: the elapsed time - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError("Can't get connect duration for timer " - "that has not started.") - return current_time() - self._start_connect - - @property - def connect_timeout(self): - """ Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: the connect timeout - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) - - @property - def read_timeout(self): - """ Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: the value to use for the read timeout - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if (self.total is not None and - self.total is not self.DEFAULT_TIMEOUT and - self._read is not None and - self._read is not self.DEFAULT_TIMEOUT): - # in case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), - self._read)) - elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self._read - - -class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])): - """ - Datastructure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. - """ - slots = () - - def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, query=None, fragment=None): - return super(Url, cls).__new__(cls, scheme, auth, host, port, path, query, fragment) - - @property - def hostname(self): - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self): - """Absolute path including the query string.""" - uri = self.path or '/' - - if self.query is not None: - uri += '?' + self.query - - return uri - - @property - def netloc(self): - """Network location including host and port""" - if self.port: - return '%s:%d' % (self.host, self.port) - return self.host - - -def split_first(s, delims): - """ - Given a string and an iterable of delimiters, split on the first found - delimiter. Return two split parts and the matched delimiter. - - If not found, then the first part is the full input string. - - Example: :: - - >>> split_first('foo/bar?baz', '?/=') - ('foo', 'bar?baz', '/') - >>> split_first('foo/bar?baz', '123') - ('foo/bar?baz', '', None) - - Scales linearly with number of delims. Not ideal for large number of delims. - """ - min_idx = None - min_delim = None - for d in delims: - idx = s.find(d) - if idx < 0: - continue - - if min_idx is None or idx < min_idx: - min_idx = idx - min_delim = d - - if min_idx is None or min_idx < 0: - return s, '', None - - return s[:min_idx], s[min_idx+1:], min_delim - - -def parse_url(url): - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - - Partly backwards-compatible with :mod:`urlparse`. - - Example: :: - - >>> parse_url('http://google.com/mail/') - Url(scheme='http', host='google.com', port=None, path='/', ...) - >>> parse_url('google.com:80') - Url(scheme=None, host='google.com', port=80, path=None, ...) - >>> parse_url('/foo?bar') - Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - - # While this code has overlap with stdlib's urlparse, it is much - # simplified for our needs and less annoying. - # Additionally, this implementations does silly things to be optimal - # on CPython. - - scheme = None - auth = None - host = None - port = None - path = None - fragment = None - query = None - - # Scheme - if '://' in url: - scheme, url = url.split('://', 1) - - # Find the earliest Authority Terminator - # (http://tools.ietf.org/html/rfc3986#section-3.2) - url, path_, delim = split_first(url, ['/', '?', '#']) - - if delim: - # Reassemble the path - path = delim + path_ - - # Auth - if '@' in url: - # Last '@' denotes end of auth part - auth, url = url.rsplit('@', 1) - - # IPv6 - if url and url[0] == '[': - host, url = url.split(']', 1) - host += ']' - - # Port - if ':' in url: - _host, port = url.split(':', 1) - - if not host: - host = _host - - if port: - # If given, ports must be integers. - if not port.isdigit(): - raise LocationParseError("Failed to parse: %s" % url) - port = int(port) - else: - # Blank ports are cool, too. (rfc3986#section-3.2.3) - port = None - - elif not host and url: - host = url - - if not path: - return Url(scheme, auth, host, port, path, query, fragment) - - # Fragment - if '#' in path: - path, fragment = path.split('#', 1) - - # Query - if '?' in path: - path, query = path.split('?', 1) - - return Url(scheme, auth, host, port, path, query, fragment) - - -def get_host(url): - """ - Deprecated. Use :func:`.parse_url` instead. - """ - p = parse_url(url) - return p.scheme or 'http', p.hostname, p.port - - -def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, - basic_auth=None, proxy_basic_auth=None): - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - Example: :: - - >>> make_headers(keep_alive=True, user_agent="Batman/1.0") - {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - >>> make_headers(accept_encoding=True) - {'accept-encoding': 'gzip,deflate'} - """ - headers = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ','.join(accept_encoding) - else: - accept_encoding = 'gzip,deflate' - headers['accept-encoding'] = accept_encoding - - if user_agent: - headers['user-agent'] = user_agent - - if keep_alive: - headers['connection'] = 'keep-alive' - - if basic_auth: - headers['authorization'] = 'Basic ' + \ - b64encode(six.b(basic_auth)).decode('utf-8') - - if proxy_basic_auth: - headers['proxy-authorization'] = 'Basic ' + \ - b64encode(six.b(proxy_basic_auth)).decode('utf-8') - - return headers - - -def is_connection_dropped(conn): # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - - :param conn: - :class:`httplib.HTTPConnection` object. - - Note: For platforms like AppEngine, this will always return ``False`` to - let the platform handle connection recycling transparently for us. - """ - sock = getattr(conn, 'sock', False) - if not sock: # Platform-specific: AppEngine - return False - - if not poll: - if not select: # Platform-specific: AppEngine - return False - - try: - return select([sock], [], [], 0.0)[0] - except SocketError: - return True - - # This version is better on platforms that support it. - p = poll() - p.register(sock, POLLIN) - for (fno, ev) in p.poll(0.0): - if fno == sock.fileno(): - # Either data is buffered (bad), or the connection is dropped. - return True - - -def resolve_cert_reqs(candidate): - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_NONE`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbrevation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_NONE - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, 'CERT_' + candidate) - return res - - return candidate - - -def resolve_ssl_version(candidate): - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_SSLv23 - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, 'PROTOCOL_' + candidate) - return res - - return candidate - - -def assert_fingerprint(cert, fingerprint): - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - # Maps the length of a digest to a possible hash function producing - # this digest. - hashfunc_map = { - 16: md5, - 20: sha1 - } - - fingerprint = fingerprint.replace(':', '').lower() - - digest_length, rest = divmod(len(fingerprint), 2) - - if rest or digest_length not in hashfunc_map: - raise SSLError('Fingerprint is of invalid length.') - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - hashfunc = hashfunc_map[digest_length] - - cert_digest = hashfunc(cert).digest() - - if not cert_digest == fingerprint_bytes: - raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' - .format(hexlify(fingerprint_bytes), - hexlify(cert_digest))) - -def is_fp_closed(obj): - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - if hasattr(obj, 'fp'): - # Object is a container for another file-like object that gets released - # on exhaustion (e.g. HTTPResponse) - return obj.fp is None - - return obj.closed - - -if SSLContext is not None: # Python 3.2+ - def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, - ca_certs=None, server_hostname=None, - ssl_version=None): - """ - All arguments except `server_hostname` have the same meaning as for - :func:`ssl.wrap_socket` - - :param server_hostname: - Hostname of the expected certificate - """ - context = SSLContext(ssl_version) - context.verify_mode = cert_reqs - if ca_certs: - try: - context.load_verify_locations(ca_certs) - # Py32 raises IOError - # Py33 raises FileNotFoundError - except Exception as e: # Reraise as SSLError - raise SSLError(e) - if certfile: - # FIXME: This block needs a test. - context.load_cert_chain(certfile, keyfile) - if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI - return context.wrap_socket(sock, server_hostname=server_hostname) - return context.wrap_socket(sock) - -else: # Python 3.1 and earlier - def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, - ca_certs=None, server_hostname=None, - ssl_version=None): - return wrap_socket(sock, keyfile=keyfile, certfile=certfile, - ca_certs=ca_certs, cert_reqs=cert_reqs, - ssl_version=ssl_version) diff --git a/libs/requests/packages/urllib3/util/__init__.py b/libs/requests/packages/urllib3/util/__init__.py new file mode 100644 index 00000000..a40185ee --- /dev/null +++ b/libs/requests/packages/urllib3/util/__init__.py @@ -0,0 +1,27 @@ +# urllib3/util/__init__.py +# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .connection import is_connection_dropped +from .request import make_headers +from .response import is_fp_closed +from .ssl_ import ( + SSLContext, + HAS_SNI, + assert_fingerprint, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import ( + current_time, + Timeout, +) +from .url import ( + get_host, + parse_url, + split_first, + Url, +) diff --git a/libs/requests/packages/urllib3/util/connection.py b/libs/requests/packages/urllib3/util/connection.py new file mode 100644 index 00000000..8deeab5c --- /dev/null +++ b/libs/requests/packages/urllib3/util/connection.py @@ -0,0 +1,45 @@ +from socket import error as SocketError +try: + from select import poll, POLLIN +except ImportError: # `poll` doesn't exist on OSX and other platforms + poll = False + try: + from select import select + except ImportError: # `select` doesn't exist on AppEngine. + select = False + +def is_connection_dropped(conn): # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + + :param conn: + :class:`httplib.HTTPConnection` object. + + Note: For platforms like AppEngine, this will always return ``False`` to + let the platform handle connection recycling transparently for us. + """ + sock = getattr(conn, 'sock', False) + if sock is False: # Platform-specific: AppEngine + return False + if sock is None: # Connection already closed (such as by httplib). + return False + + if not poll: + if not select: # Platform-specific: AppEngine + return False + + try: + return select([sock], [], [], 0.0)[0] + except SocketError: + return True + + # This version is better on platforms that support it. + p = poll() + p.register(sock, POLLIN) + for (fno, ev) in p.poll(0.0): + if fno == sock.fileno(): + # Either data is buffered (bad), or the connection is dropped. + return True + + + diff --git a/libs/requests/packages/urllib3/util/request.py b/libs/requests/packages/urllib3/util/request.py new file mode 100644 index 00000000..d48d6513 --- /dev/null +++ b/libs/requests/packages/urllib3/util/request.py @@ -0,0 +1,68 @@ +from base64 import b64encode + +from ..packages import six + + +ACCEPT_ENCODING = 'gzip,deflate' + + +def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, + basic_auth=None, proxy_basic_auth=None): + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + Example: :: + + >>> make_headers(keep_alive=True, user_agent="Batman/1.0") + {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + >>> make_headers(accept_encoding=True) + {'accept-encoding': 'gzip,deflate'} + """ + headers = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ','.join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers['accept-encoding'] = accept_encoding + + if user_agent: + headers['user-agent'] = user_agent + + if keep_alive: + headers['connection'] = 'keep-alive' + + if basic_auth: + headers['authorization'] = 'Basic ' + \ + b64encode(six.b(basic_auth)).decode('utf-8') + + if proxy_basic_auth: + headers['proxy-authorization'] = 'Basic ' + \ + b64encode(six.b(proxy_basic_auth)).decode('utf-8') + + return headers + + diff --git a/libs/requests/packages/urllib3/util/response.py b/libs/requests/packages/urllib3/util/response.py new file mode 100644 index 00000000..d0325bc6 --- /dev/null +++ b/libs/requests/packages/urllib3/util/response.py @@ -0,0 +1,13 @@ +def is_fp_closed(obj): + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + if hasattr(obj, 'fp'): + # Object is a container for another file-like object that gets released + # on exhaustion (e.g. HTTPResponse) + return obj.fp is None + + return obj.closed diff --git a/libs/requests/packages/urllib3/util/ssl_.py b/libs/requests/packages/urllib3/util/ssl_.py new file mode 100644 index 00000000..dee4b876 --- /dev/null +++ b/libs/requests/packages/urllib3/util/ssl_.py @@ -0,0 +1,133 @@ +from binascii import hexlify, unhexlify +from hashlib import md5, sha1 + +from ..exceptions import SSLError + + +try: # Test for SSL features + SSLContext = None + HAS_SNI = False + + import ssl + from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 + from ssl import SSLContext # Modern SSL? + from ssl import HAS_SNI # Has SNI? +except ImportError: + pass + + +def assert_fingerprint(cert, fingerprint): + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + # Maps the length of a digest to a possible hash function producing + # this digest. + hashfunc_map = { + 16: md5, + 20: sha1 + } + + fingerprint = fingerprint.replace(':', '').lower() + + digest_length, rest = divmod(len(fingerprint), 2) + + if rest or digest_length not in hashfunc_map: + raise SSLError('Fingerprint is of invalid length.') + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + hashfunc = hashfunc_map[digest_length] + + cert_digest = hashfunc(cert).digest() + + if not cert_digest == fingerprint_bytes: + raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' + .format(hexlify(fingerprint_bytes), + hexlify(cert_digest))) + + +def resolve_cert_reqs(candidate): + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_NONE`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbrevation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_NONE + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'CERT_' + candidate) + return res + + return candidate + + +def resolve_ssl_version(candidate): + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_SSLv23 + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'PROTOCOL_' + candidate) + return res + + return candidate + + +if SSLContext is not None: # Python 3.2+ + def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None): + """ + All arguments except `server_hostname` have the same meaning as for + :func:`ssl.wrap_socket` + + :param server_hostname: + Hostname of the expected certificate + """ + context = SSLContext(ssl_version) + context.verify_mode = cert_reqs + + # Disable TLS compression to migitate CRIME attack (issue #309) + OP_NO_COMPRESSION = 0x20000 + context.options |= OP_NO_COMPRESSION + + if ca_certs: + try: + context.load_verify_locations(ca_certs) + # Py32 raises IOError + # Py33 raises FileNotFoundError + except Exception as e: # Reraise as SSLError + raise SSLError(e) + if certfile: + # FIXME: This block needs a test. + context.load_cert_chain(certfile, keyfile) + if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI + return context.wrap_socket(sock, server_hostname=server_hostname) + return context.wrap_socket(sock) + +else: # Python 3.1 and earlier + def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None): + return wrap_socket(sock, keyfile=keyfile, certfile=certfile, + ca_certs=ca_certs, cert_reqs=cert_reqs, + ssl_version=ssl_version) diff --git a/libs/requests/packages/urllib3/util/timeout.py b/libs/requests/packages/urllib3/util/timeout.py new file mode 100644 index 00000000..4f947cb2 --- /dev/null +++ b/libs/requests/packages/urllib3/util/timeout.py @@ -0,0 +1,234 @@ +from socket import _GLOBAL_DEFAULT_TIMEOUT +import time + +from ..exceptions import TimeoutStateError + + +def current_time(): + """ + Retrieve the current time, this function is mocked out in unit testing. + """ + return time.time() + + +_Default = object() +# The default timeout to use for socket connections. This is the attribute used +# by httplib to define the default timeout + + +class Timeout(object): + """ + Utility object for storing timeout values. + + Example usage: + + .. code-block:: python + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout) + pool.request(...) # Etc, etc + + :param connect: + The maximum amount of time to wait for a connection attempt to a server + to succeed. Omitting the parameter will default the connect timeout to + the system default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: integer, float, or None + + :param read: + The maximum amount of time to wait between consecutive + read operations for a response from the server. Omitting + the parameter will default the read timeout to the system + default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: integer, float, or None + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: integer, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. Specifically, Python's DNS resolver does not obey the + timeout specified on the socket. Other factors that can affect total + request time include high CPU load, high swap, the program running at a + low priority level, or other behaviors. The observed running time for + urllib3 to return a response may be greater than the value passed to + `total`. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not ever trigger, even though the request will + take several minutes to complete. + + If your goal is to cut off any request after a set amount of wall clock + time, consider having a second "watcher" thread to cut off a slow + request. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT + + def __init__(self, total=None, connect=_Default, read=_Default): + self._connect = self._validate_timeout(connect, 'connect') + self._read = self._validate_timeout(read, 'read') + self.total = self._validate_timeout(total, 'total') + self._start_connect = None + + def __str__(self): + return '%s(connect=%r, read=%r, total=%r)' % ( + type(self).__name__, self._connect, self._read, self.total) + + + @classmethod + def _validate_timeout(cls, value, name): + """ Check that a timeout attribute is valid + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is used + for clear error messages + :return: the value + :raises ValueError: if the type is not an integer or a float, or if it + is a numeric value less than zero + """ + if value is _Default: + return cls.DEFAULT_TIMEOUT + + if value is None or value is cls.DEFAULT_TIMEOUT: + return value + + try: + float(value) + except (TypeError, ValueError): + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + try: + if value < 0: + raise ValueError("Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than 0." % (name, value)) + except TypeError: # Python 3 + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + return value + + @classmethod + def from_float(cls, timeout): + """ Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value passed + to this function. + + :param timeout: The legacy timeout value + :type timeout: integer, float, sentinel default object, or None + :return: a Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self): + """ Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, + total=self.total) + + def start_connect(self): + """ Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = current_time() + return self._start_connect + + def get_connect_duration(self): + """ Gets the time elapsed since the call to :meth:`start_connect`. + + :return: the elapsed time + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError("Can't get connect duration for timer " + "that has not started.") + return current_time() - self._start_connect + + @property + def connect_timeout(self): + """ Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: the connect timeout + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) + + @property + def read_timeout(self): + """ Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: the value to use for the read timeout + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if (self.total is not None and + self.total is not self.DEFAULT_TIMEOUT and + self._read is not None and + self._read is not self.DEFAULT_TIMEOUT): + # in case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), + self._read)) + elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self._read diff --git a/libs/requests/packages/urllib3/util/url.py b/libs/requests/packages/urllib3/util/url.py new file mode 100644 index 00000000..362d2160 --- /dev/null +++ b/libs/requests/packages/urllib3/util/url.py @@ -0,0 +1,162 @@ +from collections import namedtuple + +from ..exceptions import LocationParseError + + +class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])): + """ + Datastructure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. + """ + slots = () + + def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, query=None, fragment=None): + return super(Url, cls).__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self): + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self): + """Absolute path including the query string.""" + uri = self.path or '/' + + if self.query is not None: + uri += '?' + self.query + + return uri + + @property + def netloc(self): + """Network location including host and port""" + if self.port: + return '%s:%d' % (self.host, self.port) + return self.host + + +def split_first(s, delims): + """ + Given a string and an iterable of delimiters, split on the first found + delimiter. Return two split parts and the matched delimiter. + + If not found, then the first part is the full input string. + + Example: :: + + >>> split_first('foo/bar?baz', '?/=') + ('foo', 'bar?baz', '/') + >>> split_first('foo/bar?baz', '123') + ('foo/bar?baz', '', None) + + Scales linearly with number of delims. Not ideal for large number of delims. + """ + min_idx = None + min_delim = None + for d in delims: + idx = s.find(d) + if idx < 0: + continue + + if min_idx is None or idx < min_idx: + min_idx = idx + min_delim = d + + if min_idx is None or min_idx < 0: + return s, '', None + + return s[:min_idx], s[min_idx+1:], min_delim + + +def parse_url(url): + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + + Partly backwards-compatible with :mod:`urlparse`. + + Example: :: + + >>> parse_url('http://google.com/mail/') + Url(scheme='http', host='google.com', port=None, path='/', ...) + >>> parse_url('google.com:80') + Url(scheme=None, host='google.com', port=80, path=None, ...) + >>> parse_url('/foo?bar') + Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + + # While this code has overlap with stdlib's urlparse, it is much + # simplified for our needs and less annoying. + # Additionally, this implementations does silly things to be optimal + # on CPython. + + scheme = None + auth = None + host = None + port = None + path = None + fragment = None + query = None + + # Scheme + if '://' in url: + scheme, url = url.split('://', 1) + + # Find the earliest Authority Terminator + # (http://tools.ietf.org/html/rfc3986#section-3.2) + url, path_, delim = split_first(url, ['/', '?', '#']) + + if delim: + # Reassemble the path + path = delim + path_ + + # Auth + if '@' in url: + # Last '@' denotes end of auth part + auth, url = url.rsplit('@', 1) + + # IPv6 + if url and url[0] == '[': + host, url = url.split(']', 1) + host += ']' + + # Port + if ':' in url: + _host, port = url.split(':', 1) + + if not host: + host = _host + + if port: + # If given, ports must be integers. + if not port.isdigit(): + raise LocationParseError(url) + port = int(port) + else: + # Blank ports are cool, too. (rfc3986#section-3.2.3) + port = None + + elif not host and url: + host = url + + if not path: + return Url(scheme, auth, host, port, path, query, fragment) + + # Fragment + if '#' in path: + path, fragment = path.split('#', 1) + + # Query + if '?' in path: + path, query = path.split('?', 1) + + return Url(scheme, auth, host, port, path, query, fragment) + + +def get_host(url): + """ + Deprecated. Use :func:`.parse_url` instead. + """ + p = parse_url(url) + return p.scheme or 'http', p.hostname, p.port diff --git a/libs/requests/sessions.py b/libs/requests/sessions.py index 06e17d4b..df85a25c 100644 --- a/libs/requests/sessions.py +++ b/libs/requests/sessions.py @@ -12,27 +12,28 @@ import os from collections import Mapping from datetime import datetime +from .auth import _basic_auth_str from .compat import cookielib, OrderedDict, urljoin, urlparse, builtin_str from .cookies import ( cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) -from .models import Request, PreparedRequest +from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT from .hooks import default_hooks, dispatch_hook -from .utils import to_key_val_list, default_headers -from .exceptions import TooManyRedirects, InvalidSchema +from .utils import to_key_val_list, default_headers, to_native_string +from .exceptions import ( + TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError) from .structures import CaseInsensitiveDict from .adapters import HTTPAdapter -from .utils import requote_uri, get_environ_proxies, get_netrc_auth +from .utils import ( + requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies, + get_auth_from_url +) from .status_codes import codes -REDIRECT_STATI = ( - codes.moved, # 301 - codes.found, # 302 - codes.other, # 303 - codes.temporary_moved, # 307 -) -DEFAULT_REDIRECT_LIMIT = 30 + +# formerly defined here, reexposed here for backward compatibility +from .models import REDIRECT_STATI def merge_setting(request_setting, session_setting, dict_class=OrderedDict): @@ -63,6 +64,8 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict): if v is None: del merged_setting[k] + merged_setting = dict((k, v) for (k, v) in merged_setting.items() if v is not None) + return merged_setting @@ -89,11 +92,13 @@ class SessionRedirectMixin(object): i = 0 - # ((resp.status_code is codes.see_other)) - while ('location' in resp.headers and resp.status_code in REDIRECT_STATI): + while resp.is_redirect: prepared_request = req.copy() - resp.content # Consume socket so it can be released + try: + resp.content # Consume socket so it can be released + except (ChunkedEncodingError, ContentDecodingError, RuntimeError): + resp.raw.read(decode_content=False) if i >= self.max_redirects: raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects) @@ -121,7 +126,7 @@ class SessionRedirectMixin(object): else: url = requote_uri(url) - prepared_request.url = url + prepared_request.url = to_native_string(url) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 if (resp.status_code == codes.see_other and @@ -153,12 +158,19 @@ class SessionRedirectMixin(object): except KeyError: pass - extract_cookies_to_jar(prepared_request._cookies, - prepared_request, resp.raw) + extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw) + prepared_request._cookies.update(self.cookies) prepared_request.prepare_cookies(prepared_request._cookies) + # Rebuild auth and proxy information. + proxies = self.rebuild_proxies(prepared_request, proxies) + self.rebuild_auth(prepared_request, resp) + + # Override the original request. + req = prepared_request + resp = self.send( - prepared_request, + req, stream=stream, timeout=timeout, verify=verify, @@ -172,6 +184,68 @@ class SessionRedirectMixin(object): i += 1 yield resp + def rebuild_auth(self, prepared_request, response): + """ + When being redirected we may want to strip authentication from the + request to avoid leaking credentials. This method intelligently removes + and reapplies authentication where possible to avoid credential loss. + """ + headers = prepared_request.headers + url = prepared_request.url + + if 'Authorization' in headers: + # If we get redirected to a new host, we should strip out any + # authentication headers. + original_parsed = urlparse(response.request.url) + redirect_parsed = urlparse(url) + + if (original_parsed.hostname != redirect_parsed.hostname): + del headers['Authorization'] + + # .netrc might have more auth for us on our new host. + new_auth = get_netrc_auth(url) if self.trust_env else None + if new_auth is not None: + prepared_request.prepare_auth(new_auth) + + return + + def rebuild_proxies(self, prepared_request, proxies): + """ + This method re-evaluates the proxy configuration by considering the + environment variables. If we are redirected to a URL covered by + NO_PROXY, we strip the proxy configuration. Otherwise, we set missing + proxy keys for this URL (in case they were stripped by a previous + redirect). + + This method also replaces the Proxy-Authorization header where + necessary. + """ + headers = prepared_request.headers + url = prepared_request.url + scheme = urlparse(url).scheme + new_proxies = proxies.copy() if proxies is not None else {} + + if self.trust_env and not should_bypass_proxies(url): + environ_proxies = get_environ_proxies(url) + + proxy = environ_proxies.get(scheme) + + if proxy: + new_proxies.setdefault(scheme, environ_proxies[scheme]) + + if 'Proxy-Authorization' in headers: + del headers['Proxy-Authorization'] + + try: + username, password = get_auth_from_url(new_proxies[scheme]) + except KeyError: + username, password = None, None + + if username and password: + headers['Proxy-Authorization'] = _basic_auth_str(username, password) + + return new_proxies + class Session(SessionRedirectMixin): """A Requests session. @@ -255,7 +329,7 @@ class Session(SessionRedirectMixin): :class:`Session`. :param request: :class:`Request` instance to prepare with this - session's settings. + session's settings. """ cookies = request.cookies or {} @@ -319,7 +393,7 @@ class Session(SessionRedirectMixin): :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. :param timeout: (optional) Float describing the timeout of the - request. + request in seconds. :param allow_redirects: (optional) Boolean. Set to True by default. :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. @@ -466,8 +540,7 @@ class Session(SessionRedirectMixin): if not isinstance(request, PreparedRequest): raise ValueError('You can only send PreparedRequests.') - # Set up variables needed for resolve_redirects and dispatching of - # hooks + # Set up variables needed for resolve_redirects and dispatching of hooks allow_redirects = kwargs.pop('allow_redirects', True) stream = kwargs.get('stream') timeout = kwargs.get('timeout') @@ -481,8 +554,10 @@ class Session(SessionRedirectMixin): # Start time (approximately) of the request start = datetime.utcnow() + # Send the request r = adapter.send(request, **kwargs) + # Total elapsed time of the request (approximately) r.elapsed = datetime.utcnow() - start @@ -491,15 +566,20 @@ class Session(SessionRedirectMixin): # Persist cookies if r.history: + # If the hooks create history then we want those cookies too for resp in r.history: extract_cookies_to_jar(self.cookies, resp.request, resp.raw) + extract_cookies_to_jar(self.cookies, request, r.raw) # Redirect resolving generator. - gen = self.resolve_redirects(r, request, stream=stream, - timeout=timeout, verify=verify, cert=cert, - proxies=proxies) + gen = self.resolve_redirects(r, request, + stream=stream, + timeout=timeout, + verify=verify, + cert=cert, + proxies=proxies) # Resolve redirects if allowed. history = [resp for resp in gen] if allow_redirects else [] @@ -510,7 +590,10 @@ class Session(SessionRedirectMixin): history.insert(0, r) # Get the last request made r = history.pop() - r.history = tuple(history) + r.history = history + + if not stream: + r.content return r @@ -533,8 +616,10 @@ class Session(SessionRedirectMixin): """Registers a connection adapter to a prefix. Adapters are sorted in descending order by key length.""" + self.adapters[prefix] = adapter keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] + for key in keys_to_move: self.adapters[key] = self.adapters.pop(key) diff --git a/libs/requests/structures.py b/libs/requests/structures.py index a1759137..66cdad86 100644 --- a/libs/requests/structures.py +++ b/libs/requests/structures.py @@ -8,30 +8,7 @@ Data structures that power Requests. """ -import os import collections -from itertools import islice - - -class IteratorProxy(object): - """docstring for IteratorProxy""" - def __init__(self, i): - self.i = i - # self.i = chain.from_iterable(i) - - def __iter__(self): - return self.i - - def __len__(self): - if hasattr(self.i, '__len__'): - return len(self.i) - if hasattr(self.i, 'len'): - return self.i.len - if hasattr(self.i, 'fileno'): - return os.fstat(self.i.fileno()).st_size - - def read(self, n): - return "".join(islice(self.i, None, n)) class CaseInsensitiveDict(collections.MutableMapping): @@ -106,8 +83,7 @@ class CaseInsensitiveDict(collections.MutableMapping): return CaseInsensitiveDict(self._store.values()) def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, dict(self.items())) - + return str(dict(self.items())) class LookupDict(dict): """Dictionary lookup object.""" diff --git a/libs/requests/utils.py b/libs/requests/utils.py index c7e2b089..68e50cf0 100644 --- a/libs/requests/utils.py +++ b/libs/requests/utils.py @@ -24,10 +24,10 @@ from . import __version__ from . import certs from .compat import parse_http_list as _parse_list_header from .compat import (quote, urlparse, bytes, str, OrderedDict, unquote, is_py2, - builtin_str, getproxies, proxy_bypass) + builtin_str, getproxies, proxy_bypass, urlunparse) from .cookies import RequestsCookieJar, cookiejar_from_dict from .structures import CaseInsensitiveDict -from .exceptions import MissingSchema, InvalidURL +from .exceptions import InvalidURL _hush_pyflakes = (RequestsCookieJar,) @@ -61,25 +61,34 @@ def super_len(o): return os.fstat(fileno).st_size if hasattr(o, 'getvalue'): - # e.g. BytesIO, cStringIO.StringI + # e.g. BytesIO, cStringIO.StringIO return len(o.getvalue()) + def get_netrc_auth(url): """Returns the Requests tuple auth for a given url from netrc.""" try: from netrc import netrc, NetrcParseError - locations = (os.path.expanduser('~/{0}'.format(f)) for f in NETRC_FILES) netrc_path = None - for loc in locations: - if os.path.exists(loc) and not netrc_path: + for f in NETRC_FILES: + try: + loc = os.path.expanduser('~/{0}'.format(f)) + except KeyError: + # os.path.expanduser can fail when $HOME is undefined and + # getpwuid fails. See http://bugs.python.org/issue20164 & + # https://github.com/kennethreitz/requests/issues/1846 + return + + if os.path.exists(loc): netrc_path = loc + break # Abort early if there isn't one. if netrc_path is None: - return netrc_path + return ri = urlparse(url) @@ -457,9 +466,10 @@ def is_valid_cidr(string_network): return True -def get_environ_proxies(url): - """Return a dict of environment proxies.""" - +def should_bypass_proxies(url): + """ + Returns whether we should bypass proxies or not. + """ get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper()) # First check whether no_proxy is defined. If it is, check that the URL @@ -477,23 +487,36 @@ def get_environ_proxies(url): for proxy_ip in no_proxy: if is_valid_cidr(proxy_ip): if address_in_network(ip, proxy_ip): - return {} + return True else: for host in no_proxy: if netloc.endswith(host) or netloc.split(':')[0].endswith(host): # The URL does match something in no_proxy, so we don't want # to apply the proxies on this URL. - return {} + return True # If the system proxy settings indicate that this URL should be bypassed, # don't proxy. - if proxy_bypass(netloc): - return {} + # The proxy_bypass function is incredibly buggy on OS X in early versions + # of Python 2.6, so allow this call to fail. Only catch the specific + # exceptions we've seen, though: this call failing in other ways can reveal + # legitimate problems. + try: + bypass = proxy_bypass(netloc) + except (TypeError, socket.gaierror): + bypass = False - # If we get here, we either didn't have no_proxy set or we're not going - # anywhere that no_proxy applies to, and the system settings don't require - # bypassing the proxy for the current URL. - return getproxies() + if bypass: + return True + + return False + +def get_environ_proxies(url): + """Return a dict of environment proxies.""" + if should_bypass_proxies(url): + return {} + else: + return getproxies() def default_user_agent(name="python-requests"): @@ -530,7 +553,7 @@ def default_user_agent(name="python-requests"): def default_headers(): return CaseInsensitiveDict({ 'User-Agent': default_user_agent(), - 'Accept-Encoding': ', '.join(('gzip', 'deflate', 'compress')), + 'Accept-Encoding': ', '.join(('gzip', 'deflate')), 'Accept': '*/*' }) @@ -604,24 +627,31 @@ def guess_json_utf(data): return None -def except_on_missing_scheme(url): - """Given a URL, raise a MissingSchema exception if the scheme is missing. - """ - scheme, netloc, path, params, query, fragment = urlparse(url) +def prepend_scheme_if_needed(url, new_scheme): + '''Given a URL that may or may not have a scheme, prepend the given scheme. + Does not replace a present scheme with the one provided as an argument.''' + scheme, netloc, path, params, query, fragment = urlparse(url, new_scheme) - if not scheme: - raise MissingSchema('Proxy URLs must have explicit schemes.') + # urlparse is a finicky beast, and sometimes decides that there isn't a + # netloc present. Assume that it's being over-cautious, and switch netloc + # and path if urlparse decided there was no netloc. + if not netloc: + netloc, path = path, netloc + + return urlunparse((scheme, netloc, path, params, query, fragment)) def get_auth_from_url(url): """Given a url with authentication components, extract them into a tuple of username,password.""" - if url: - url = unquote(url) - parsed = urlparse(url) - return (parsed.username, parsed.password) - else: - return ('', '') + parsed = urlparse(url) + + try: + auth = (unquote(parsed.username), unquote(parsed.password)) + except (AttributeError, TypeError): + auth = ('', '') + + return auth def to_native_string(string, encoding='ascii'): From a42264b280018c35063c788758611c014a374d1d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 29 May 2014 20:09:16 +0200 Subject: [PATCH 330/468] Tornado 3.2.1 --- libs/tornado/__init__.py | 4 +- libs/tornado/auth.py | 5 + libs/tornado/autoreload.py | 2 +- libs/tornado/ca-certificates.crt | 3562 ----------------------------- libs/tornado/concurrent.py | 129 +- libs/tornado/curl_httpclient.py | 7 +- libs/tornado/escape.py | 2 +- libs/tornado/gen.py | 470 ++-- libs/tornado/http1connection.py | 624 +++++ libs/tornado/httpclient.py | 51 +- libs/tornado/httpserver.py | 561 ++--- libs/tornado/httputil.py | 401 +++- libs/tornado/ioloop.py | 118 +- libs/tornado/iostream.py | 624 +++-- libs/tornado/log.py | 10 +- libs/tornado/netutil.py | 51 +- libs/tornado/options.py | 18 +- libs/tornado/platform/asyncio.py | 36 +- libs/tornado/platform/auto.py | 4 + libs/tornado/platform/common.py | 3 +- libs/tornado/platform/kqueue.py | 2 +- libs/tornado/platform/select.py | 2 +- libs/tornado/platform/twisted.py | 25 +- libs/tornado/process.py | 14 +- libs/tornado/simple_httpclient.py | 267 ++- libs/tornado/speedups.c | 13 +- libs/tornado/stack_context.py | 12 + libs/tornado/tcpclient.py | 179 ++ libs/tornado/tcpserver.py | 24 +- libs/tornado/template.py | 11 +- libs/tornado/testing.py | 100 +- libs/tornado/util.py | 44 +- libs/tornado/web.py | 690 ++++-- libs/tornado/websocket.py | 153 +- libs/tornado/wsgi.py | 250 +- 35 files changed, 3584 insertions(+), 4884 deletions(-) delete mode 100755 libs/tornado/ca-certificates.crt create mode 100644 libs/tornado/http1connection.py create mode 100644 libs/tornado/tcpclient.py diff --git a/libs/tornado/__init__.py b/libs/tornado/__init__.py index c41ec97b..05174084 100755 --- a/libs/tornado/__init__.py +++ b/libs/tornado/__init__.py @@ -25,5 +25,5 @@ from __future__ import absolute_import, division, print_function, with_statement # is zero for an official release, positive for a development branch, # or negative for a release candidate or beta (after the base version # number has been incremented) -version = "3.2" -version_info = (3, 2, 0, 0) +version = "3.3.dev1" +version_info = (3, 3, 0, -100) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 9baac9ba..6892f91a 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -73,6 +73,11 @@ try: except ImportError: import urllib as urllib_parse # py2 +try: + long # py2 +except NameError: + long = int # py3 + class AuthError(Exception): pass diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 79cccb49..3982579a 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -14,7 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""xAutomatically restart the server when a source file is modified. +"""Automatically restart the server when a source file is modified. Most applications should not access this module directly. Instead, pass the keyword argument ``autoreload=True`` to the diff --git a/libs/tornado/ca-certificates.crt b/libs/tornado/ca-certificates.crt deleted file mode 100755 index a1ede895..00000000 --- a/libs/tornado/ca-certificates.crt +++ /dev/null @@ -1,3562 +0,0 @@ -# This file contains certificates of known certificate authorities -# for use with SimpleAsyncHTTPClient. -# -# It was extracted from the Mozilla source tree using libcurl's mk-ca-bundle -# script on Aug 13, 2013. -# -# This data file is licenced under the MPL/GPL. - -## -## ca-bundle.crt -- Bundle of CA Root Certificates -## -## Certificate data from Mozilla as of: Tue Aug 13 03:28:51 2013 -## -## This is a bundle of X.509 certificates of public Certificate Authorities -## (CA). These were automatically extracted from Mozilla's root certificates -## file (certdata.txt). This file can be found in the mozilla source tree: -## http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1 -## -## It contains the certificates in PEM format and therefore -## can be directly used with curl / libcurl / php_curl, or with -## an Apache+mod_ssl webserver for SSL client authentication. -## Just configure this file as the SSLCACertificateFile. -## - -# @(#) $RCSfile: certdata.txt,v $ $Revision: 1.87 $ $Date: 2012/12/29 16:32:45 $ - -GTE CyberTrust Global Root -========================== ------BEGIN CERTIFICATE----- -MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg -Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG -A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz -MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL -Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0 -IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u -sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql -HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID -AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW -M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF -NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ ------END CERTIFICATE----- - -Thawte Server CA -================ ------BEGIN CERTIFICATE----- -MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE -AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j -b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV -BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u -c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG -A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0 -ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl -/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7 -1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J -GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ -GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc= ------END CERTIFICATE----- - -Thawte Premium Server CA -======================== ------BEGIN CERTIFICATE----- -MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE -AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl -ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT -AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU -VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2 -aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ -cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 -aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh -Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/ -qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm -SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf -8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t -UCemDaYj+bvLpgcUQg== ------END CERTIFICATE----- - -Equifax Secure CA -================= ------BEGIN CERTIFICATE----- -MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEQMA4GA1UE -ChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoT -B0VxdWlmYXgxLTArBgNVBAsTJEVxdWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCB -nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPR -fM6fBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+AcJkVV5MW -8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kCAwEAAaOCAQkwggEFMHAG -A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UE -CxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoG -A1UdEAQTMBGBDzIwMTgwODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvS -spXXR9gjIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQFMAMB -Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAFjOKer89961 -zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y7qj/WsjTVbJmcVfewCHrPSqnI0kB -BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 -70+sB3c4 ------END CERTIFICATE----- - -Digital Signature Trust Co. Global CA 1 -======================================= ------BEGIN CERTIFICATE----- -MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE -ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMTAeFw05ODEy -MTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs -IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUA -A4GLADCBhwKBgQCgbIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJE -NySZj9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlVSn5JTe2i -o74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo -BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0 -dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw -IoAPMTk5ODEyMTAxODEwMjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQY -MBaAFGp5fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i+DAM -BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB -ACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lNQseSJqBcNJo4cvj9axY+IO6CizEq -kzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4 -RbyhkwS7hp86W0N6w4pl ------END CERTIFICATE----- - -Digital Signature Trust Co. Global CA 3 -======================================= ------BEGIN CERTIFICATE----- -MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE -ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMjAeFw05ODEy -MDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs -IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUA -A4GLADCBhwKBgQC/k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGOD -VvsoLeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3oTQPMx7JS -xhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo -BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0 -dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw -IoAPMTk5ODEyMDkxOTE3MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQY -MBaAFB6CTShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5WzAM -BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB -AEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHRxdf0CiUPPXiBng+xZ8SQTGPdXqfi -up/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVLB3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1 -mPnHfxsb1gYgAlihw6ID ------END CERTIFICATE----- - -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA -TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah -WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf -Tqj/ZA1k ------END CERTIFICATE----- - -Verisign Class 3 Public Primary Certification Authority - G2 -============================================================ ------BEGIN CERTIFICATE----- -MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO -FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71 -lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB -MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT -1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD -Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9 ------END CERTIFICATE----- - -GlobalSign Root CA -================== ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx -GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds -b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV -BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD -VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa -DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc -THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb -Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP -c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX -gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF -AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj -Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG -j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH -hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC -X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- - -GlobalSign Root CA - R2 -======================= ------BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv -YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh -bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT -aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln -bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 -ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp -s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN -S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL -TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C -ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i -YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN -BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp -9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu -01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 -9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 -TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== ------END CERTIFICATE----- - -ValiCert Class 1 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy -MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi -GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm -DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG -lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX -icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP -Orf1LXLI ------END CERTIFICATE----- - -ValiCert Class 2 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC -CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf -ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ -SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV -UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8 -W9ViH0Pd ------END CERTIFICATE----- - -RSA Root Certificate 1 -====================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td -3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H -BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs -3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF -V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r -on+jjBXu ------END CERTIFICATE----- - -Verisign Class 3 Public Primary Certification Authority - G3 -============================================================ ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV -UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv -cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy -dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAMu6nFL8eB8aHm8bN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1 -EUGO+i2tKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGukxUc -cLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBmCC+Vk7+qRy+oRpfw -EuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj -055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWuimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA -ERSWwauSCPc/L8my/uRan2Te2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5f -j267Cz3qWhMeDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC -/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565pF4ErWjfJXir0 -xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGtTxzhT5yvDwyd93gN2PQ1VoDa -t20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== ------END CERTIFICATE----- - -Verisign Class 4 Public Primary Certification Authority - G3 -============================================================ ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV -UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv -cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy -dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAK3LpRFpxlmr8Y+1GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaS -tBO3IFsJ+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0GbdU6LM -8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLmNxdLMEYH5IBtptiW -Lugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XYufTsgsbSPZUd5cBPhMnZo0QoBmrX -Razwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA -j/ola09b5KROJ1WrIhVZPMq1CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXtt -mhwwjIDLk5Mqg6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm -fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c2NU8Qh0XwRJd -RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG -UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== ------END CERTIFICATE----- - -Entrust.net Secure Server CA -============================ ------BEGIN CERTIFICATE----- -MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV -BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg -cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl -ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG -A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi -eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p -dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ -aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5 -gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw -ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw -CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l -dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF -bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu -dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw -NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow -HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA -BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN -Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9 -n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= ------END CERTIFICATE----- - -Entrust.net Premium 2048 Secure Server CA -========================================= ------BEGIN CERTIFICATE----- -MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u -ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp -bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV -BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx -NzUwNTFaFw0xOTEyMjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 -d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl -MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u -ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL -Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr -hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW -nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi -VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo3QwcjARBglghkgBhvhC -AQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGAvtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdER -gL7YibkIozH5oSQJFrlwMB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0B -AQUFAAOCAQEAWUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo -oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQh7A6tcOdBTcS -o8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18f3v/rxzP5tsHrV7bhZ3QKw0z -2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfNB/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjX -OP/swNlQ8C5LWK5Gb9Auw2DaclVyvUxFnmG6v4SBkgPR0ml8xQ== ------END CERTIFICATE----- - -Baltimore CyberTrust Root -========================= ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE -ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li -ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC -SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs -dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME -uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB -UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C -G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 -XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr -l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI -VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB -BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh -cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 -hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa -Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H -RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE----- - -Equifax Secure Global eBusiness CA -================================== ------BEGIN CERTIFICATE----- -MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp -bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx -HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds -b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV -PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN -qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn -hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs -MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN -I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY -NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV ------END CERTIFICATE----- - -Equifax Secure eBusiness CA 1 -============================= ------BEGIN CERTIFICATE----- -MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB -LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE -ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz -IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ -1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a -IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk -MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW -Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF -AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5 -lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+ -KpYrtWKmpj29f5JZzVoqgrI3eQ== ------END CERTIFICATE----- - -Equifax Secure eBusiness CA 2 -============================= ------BEGIN CERTIFICATE----- -MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEXMBUGA1UE -ChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0y -MB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoT -DkVxdWlmYXggU2VjdXJlMSYwJAYDVQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCB -nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn -2Z0GvxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/BPO3QSQ5 -BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0CAwEAAaOCAQkwggEFMHAG -A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUx -JjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoG -A1UdEAQTMBGBDzIwMTkwNjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9e -uSBIplBqy/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQFMAMB -Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAAyGgq3oThr1 -jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia -78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUm -V+GRMOrN ------END CERTIFICATE----- - -AddTrust Low-Value Services Root -================================ ------BEGIN CERTIFICATE----- -MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML -QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRU -cnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMwMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQsw -CQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBO -ZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ulCDtbKRY6 -54eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6ntGO0/7Gcrjyvd7ZWxbWr -oulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyldI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1 -Zmne3yzxbrww2ywkEtvrNTVokMsAsJchPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJui -GMx1I4S+6+JNM3GOGvDC+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8w -HQYDVR0OBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBlMQswCQYDVQQGEwJT -RTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEw -HwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxt -ZBsfzQ3duQH6lmM0MkhHma6X7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0Ph -iVYrqW9yTkkz43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY -eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJlpz/+0WatC7xr -mYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOAWiFeIc9TVPC6b4nbqKqVz4vj -ccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= ------END CERTIFICATE----- - -AddTrust External Root -====================== ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML -QWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYD -VQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEw -NDgzOFowbzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRU -cnVzdCBFeHRlcm5hbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0Eg -Um9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvtH7xsD821 -+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9uMq/NzgtHj6RQa1wVsfw -Tz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzXmk6vBbOmcZSccbNQYArHE504B4YCqOmo -aSYYkKtMsE8jqzpPhNjfzp/haW+710LXa0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy -2xSoRcRdKn23tNbE7qzNE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv7 -7+ldU9U0WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYDVR0P -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0Jvf6xCZU7wO94CTL -VBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRk -VHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB -IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZl -j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvCNr4TDea9Y355 -e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u -G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- - -AddTrust Public Services Root -============================= ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEUMBIGA1UEChML -QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSAwHgYDVQQDExdBZGRU -cnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAxMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJ -BgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5l -dHdvcmsxIDAeBgNVBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV6tsfSlbu -nyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nXGCwwfQ56HmIexkvA/X1i -d9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnPdzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSG -Aa2Il+tmzV7R/9x98oTaunet3IAIx6eH1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAw -HM+A+WD+eeSI8t0A65RF62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0G -A1UdDgQWBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29yazEgMB4G -A1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4 -JNojVhaTdt02KLmuG7jD8WS6IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL -+YPoRNWyQSW/iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao -GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh4SINhwBk/ox9 -Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQmXiLsks3/QppEIW1cxeMiHV9H -EufOX1362KqxMy3ZdvJOOjMMK7MtkAY= ------END CERTIFICATE----- - -AddTrust Qualified Certificates Root -==================================== ------BEGIN CERTIFICATE----- -MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEUMBIGA1UEChML -QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSMwIQYDVQQDExpBZGRU -cnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcx -CzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQ -IE5ldHdvcmsxIzAhBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwqxBb/4Oxx -64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G87B4pfYOQnrjfxvM0PC3 -KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i2O+tCBGaKZnhqkRFmhJePp1tUvznoD1o -L/BLcHwTOK28FSXx1s6rosAx1i+f4P8UWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GR -wVY18BTcZTYJbqukB8c10cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HU -MIHRMB0GA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6FrpGkwZzELMAkGA1UE -BhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29y -azEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlmaWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQAD -ggEBABmrder4i2VhlRO6aQTvhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxG -GuoYQ992zPlmhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X -dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3P6CxB9bpT9ze -RXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9YiQBCYz95OdBEsIJuQRno3eDB -iFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5noxqE= ------END CERTIFICATE----- - -Entrust Root Certification Authority -==================================== ------BEGIN CERTIFICATE----- -MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV -BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw -b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG -A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 -MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu -MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu -Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v -dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz -A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww -Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 -j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN -rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw -DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 -MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH -hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA -A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM -Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa -v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS -W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 -tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 ------END CERTIFICATE----- - -RSA Security 2048 v3 -==================== ------BEGIN CERTIFICATE----- -MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK -ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy -MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb -BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7 -Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb -WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH -KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP -+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/ -MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E -FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY -v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj -0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj -VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395 -nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA -pKnXwiJPZ9d37CAFYd4= ------END CERTIFICATE----- - -GeoTrust Global CA -================== ------BEGIN CERTIFICATE----- -MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK -Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw -MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j -LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo -BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet -8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc -T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU -vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk -DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q -zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 -d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 -mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p -XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm -Mw== ------END CERTIFICATE----- - -GeoTrust Global CA 2 -==================== ------BEGIN CERTIFICATE----- -MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN -R2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwHhcNMDQwMzA0MDUw -MDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j -LjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDvPE1APRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/ -NTL8Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hLTytCOb1k -LUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL5mkWRxHCJ1kDs6ZgwiFA -Vvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7S4wMcoKK+xfNAGw6EzywhIdLFnopsk/b -HdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNH -K266ZUapEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6tdEPx7 -srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv/NgdRN3ggX+d6Yvh -ZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywNA0ZF66D0f0hExghAzN4bcLUprbqL -OzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkC -x1YAzUm5s2x7UwQa4qjJqhIFI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqF -H4z1Ir+rzoPz4iIprn2DQKi6bA== ------END CERTIFICATE----- - -GeoTrust Universal CA -===================== ------BEGIN CERTIFICATE----- -MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN -R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 -MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu -Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t -JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e -RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs -7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d -8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V -qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga -Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB -Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu -KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 -ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 -XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB -hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc -aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 -qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL -oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK -xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF -KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 -DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK -xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU -p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI -P/rmMuGNG2+k5o7Y+SlIis5z/iw= ------END CERTIFICATE----- - -GeoTrust Universal CA 2 -======================= ------BEGIN CERTIFICATE----- -MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN -R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 -MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg -SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 -DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 -j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q -JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a -QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 -WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP -20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn -ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC -SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG -8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 -+/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E -BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z -dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ -4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ -mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq -A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg -Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP -pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d -FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp -gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm -X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS ------END CERTIFICATE----- - -America Online Root Certification Authority 1 -============================================= ------BEGIN CERTIFICATE----- -MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG -v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z -DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh -sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP -8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z -o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf -GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF -VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft -3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g -Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds -sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 ------END CERTIFICATE----- - -America Online Root Certification Authority 2 -============================================= ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en -fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8 -f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO -qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN -RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0 -gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn -6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid -FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6 -Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj -B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op -aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY -T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p -+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg -JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy -zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO -ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh -1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf -GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff -Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP -cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk= ------END CERTIFICATE----- - -Visa eCommerce Root -=================== ------BEGIN CERTIFICATE----- -MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQG -EwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2Ug -QXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2 -WhcNMjIwNjI0MDAxNjEyWjBrMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMm -VmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv -bW1lcmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h2mCxlCfL -F9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4ElpF7sDPwsRROEW+1QK8b -RaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdVZqW1LS7YgFmypw23RuwhY/81q6UCzyr0 -TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI -/k4+oKsGGelT84ATB+0tvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzs -GHxBvfaLdXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG -MB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUFAAOCAQEAX/FBfXxc -CLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcRzCSs00Rsca4BIGsDoo8Ytyk6feUW -YFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pz -zkWKsKZJ/0x9nXGIxHYdkFsd7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBu -YQa7FkKMcPcw++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt -398znM/jra6O1I7mT1GvFpLgXPYHDw== ------END CERTIFICATE----- - -Certum Root CA -============== ------BEGIN CERTIFICATE----- -MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQK -ExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQTAeFw0wMjA2MTExMDQ2Mzla -Fw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8u -by4xEjAQBgNVBAMTCUNlcnR1bSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6x -wS7TT3zNJc4YPk/EjG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdL -kKWoePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GIULdtlkIJ -89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapuOb7kky/ZR6By6/qmW6/K -Uz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUgAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7P -NSzVttpd90gzFFS269lvzs2I1qsb2pY7HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQUFAAOCAQEAuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+ -GXYkHAQaTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTgxSvg -GrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1qCjqTE5s7FCMTY5w/ -0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5xO/fIR/RpbxXyEV6DHpx8Uq79AtoS -qFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs6GAqm4VKQPNriiTsBhYscw== ------END CERTIFICATE----- - -Comodo AAA Services root -======================== ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS -R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg -TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw -MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl -c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV -BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG -C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs -i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW -Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH -Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK -Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f -BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl -cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz -LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm -7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z -8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C -12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- - -Comodo Secure Services root -=========================== ------BEGIN CERTIFICATE----- -MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS -R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg -TGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAw -MDAwMFoXDTI4MTIzMTIzNTk1OVowfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFu -Y2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAi -BgNVBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPMcm3ye5drswfxdySRXyWP -9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3SHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstc -rbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rC -oznl2yY4rYsK7hljxxwk3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3V -p6ea5EQz6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNVHQ4E -FgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w -gYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL1NlY3VyZUNlcnRpZmlj -YXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRwOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlm -aWNhdGVTZXJ2aWNlcy5jcmwwDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm -4J4oqF7Tt/Q05qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj -Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtIgKvcnDe4IRRL -DXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJaD61JlfutuC23bkpgHl9j6Pw -pCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDlizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1H -RR3B7Hzs/Sk= ------END CERTIFICATE----- - -Comodo Trusted Services root -============================ ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS -R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg -TGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEw -MDAwMDBaFw0yODEyMzEyMzU5NTlaMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1h -bmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUw -IwYDVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWWfnJSoBVC21ndZHoa0Lh7 -3TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMtTGo87IvDktJTdyR0nAducPy9C1t2ul/y -/9c3S0pgePfw+spwtOpZqqPOSC+pw7ILfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6 -juljatEPmsbS9Is6FARW1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsS -ivnkBbA7kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0GA1Ud -DgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21vZG9jYS5jb20vVHJ1c3RlZENlcnRp -ZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRodHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENl -cnRpZmljYXRlU2VydmljZXMuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8Ntw -uleGFTQQuS9/HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 -pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxISjBc/lDb+XbDA -BHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+xqFx7D+gIIxmOom0jtTYsU0l -R+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/AtyjcndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O -9y5Xt5hwXsjEeLBi ------END CERTIFICATE----- - -QuoVadis Root CA -================ ------BEGIN CERTIFICATE----- -MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE -ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 -eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz -MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp -cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD -EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk -J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL -F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL -YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen -AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w -PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y -ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 -MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj -YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs -ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh -Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW -Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu -BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw -FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 -tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo -fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul -LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x -gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi -5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi -5nrQNiOKSnQ2+Q== ------END CERTIFICATE----- - -QuoVadis Root CA 2 -================== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -QuoVadis Root CA 3 -================== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -Security Communication Root CA -============================== ------BEGIN CERTIFICATE----- -MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP -U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw -HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP -U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw -8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM -DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX -5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd -DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 -JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw -DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g -0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a -mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ -s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ -6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi -FL39vmwLAw== ------END CERTIFICATE----- - -Sonera Class 2 Root CA -====================== ------BEGIN CERTIFICATE----- -MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG -U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw -NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh -IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 -/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT -dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG -f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P -tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH -nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT -XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt -0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI -cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph -Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx -EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH -llpwrN9M ------END CERTIFICATE----- - -Staat der Nederlanden Root CA -============================= ------BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJOTDEeMBwGA1UE -ChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFhdCBkZXIgTmVkZXJsYW5kZW4g -Um9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEyMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4w -HAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxh -bmRlbiBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFt -vsznExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw719tV2U02P -jLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MOhXeiD+EwR+4A5zN9RGca -C1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+UtFE5A3+y3qcym7RHjm+0Sq7lr7HcsBth -vJly3uSJt3omXdozSVtSnA71iq3DuD3oBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn6 -22r+I/q85Ej0ZytqERAhSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRV -HSAAMDwwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMvcm9v -dC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA7Jbg0zTBLL9s+DAN -BgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k/rvuFbQvBgwp8qiSpGEN/KtcCFtR -EytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzmeafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbw -MVcoEoJz6TMvplW0C5GUR5z6u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3y -nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR -iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== ------END CERTIFICATE----- - -TDC Internet Root CA -==================== ------BEGIN CERTIFICATE----- -MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJESzEVMBMGA1UE -ChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTAeFw0wMTA0MDUx -NjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJu -ZXQxHTAbBgNVBAsTFFREQyBJbnRlcm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxLhAvJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20j -xsNuZp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a0vnRrEvL -znWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc14izbSysseLlJ28TQx5yc -5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGNeGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6 -otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcDR0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZI -AYb4QgEBBAQDAgAHMGUGA1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMM -VERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxMEQ1JM -MTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3WjALBgNVHQ8EBAMC -AQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAwHQYDVR0OBBYEFGxkAcf9hW2syNqe -UAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJKoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0G -CSqGSIb3DQEBBQUAA4IBAQBOQ8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540m -gwV5dOy0uaOXwTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+ -2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm899qNLPg7kbWzb -O0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0jUNAE4z9mQNUecYu6oah9jrU -Cbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38aQNiuJkFBT1reBK9sG9l ------END CERTIFICATE----- - -UTN DATACorp SGC Root CA -======================== ------BEGIN CERTIFICATE----- -MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UE -BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl -IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZ -BgNVBAMTElVUTiAtIERBVEFDb3JwIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBa -MIGTMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4w -HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRy -dXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ys -raP6LnD43m77VkIVni5c7yPeIbkFdicZD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlo -wHDyUwDAXlCCpVZvNvlK4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA -9P4yPykqlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulWbfXv -33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQABo4GrMIGoMAsGA1Ud -DwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRTMtGzz3/64PGgXYVOktKeRR20TzA9 -BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dD -LmNybDAqBgNVHSUEIzAhBggrBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3 -DQEBBQUAA4IBAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft -Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyjj98C5OBxOvG0 -I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVHKWss5nbZqSl9Mt3JNjy9rjXx -EZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwP -DPafepE39peC4N1xaf92P2BNPM/3mfnGV/TJVTl4uix5yaaIK/QI ------END CERTIFICATE----- - -UTN USERFirst Hardware Root CA -============================== ------BEGIN CERTIFICATE----- -MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UE -BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl -IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAd -BgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgx -OTIyWjCBlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0 -eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz -ZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlI -wrthdBKWHTxqctU8EGc6Oe0rE81m65UJM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFd -tqdt++BxF2uiiPsA3/4aMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8 -i4fDidNdoI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqIDsjf -Pe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9KsyoUhbAgMBAAGjgbkw -gbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKFyXyYbKJhDlV0HN9WF -lp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNF -UkZpcnN0LUhhcmR3YXJlLmNybDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUF -BwMGBggrBgEFBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM -//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28GpgoiskliCE7/yMgUsogW -XecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gECJChicsZUN/KHAG8HQQZexB2 -lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kn -iCrVWFCVH/A7HFe7fRQ5YiuayZSSKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67 -nfhmqA== ------END CERTIFICATE----- - -Camerfirma Chambers of Commerce Root -==================================== ------BEGIN CERTIFICATE----- -MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe -QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i -ZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAx -NjEzNDNaFw0zNzA5MzAxNjEzNDRaMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZp -cm1hIFNBIENJRiBBODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3Jn -MSIwIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0BAQEFAAOC -AQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtbunXF/KGIJPov7coISjlU -xFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0dBmpAPrMMhe5cG3nCYsS4No41XQEMIwRH -NaqbYE6gZj3LJgqcQKH0XZi/caulAGgq7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jW -DA+wWFjbw2Y3npuRVDM30pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFV -d9oKDMyXroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIGA1Ud -EwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5jaGFtYmVyc2lnbi5v -cmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p26EpW1eLTXYGduHRooowDgYDVR0P -AQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hh -bWJlcnNpZ24ub3JnMCcGA1UdEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYD -VR0gBFEwTzBNBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz -aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEBAAxBl8IahsAi -fJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZdp0AJPaxJRUXcLo0waLIJuvvD -L8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wN -UPf6s+xCX6ndbcj0dc97wXImsQEcXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/n -ADydb47kMgkdTXg0eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1 -erfutGWaIZDgqtCYvDi1czyL+Nw= ------END CERTIFICATE----- - -Camerfirma Global Chambersign Root -================================== ------BEGIN CERTIFICATE----- -MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe -QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i -ZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYx -NDE4WhcNMzcwOTMwMTYxNDE4WjB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJt -YSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEg -MB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUAA4IBDQAw -ggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0Mi+ITaFgCPS3CU6gSS9J -1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/sQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8O -by4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpVeAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl -6DJWk0aJqCWKZQbua795B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c -8lCrEqWhz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0TAQH/ -BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1iZXJzaWduLm9yZy9j -aGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4wTcbOX60Qq+UDpfqpFDAOBgNVHQ8B -Af8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAHMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBj -aGFtYmVyc2lnbi5vcmcwKgYDVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9y -ZzBbBgNVHSAEVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh -bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0BAQUFAAOCAQEA -PDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUMbKGKfKX0j//U2K0X1S0E0T9Y -gOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXiryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJ -PJ7oKXqJ1/6v/2j1pReQvayZzKWGVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4 -IBHNfTIzSJRUTN3cecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREes -t2d/AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== ------END CERTIFICATE----- - -NetLock Notary (Class A) Root -============================= ------BEGIN CERTIFICATE----- -MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQI -EwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 -dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9j -ayBLb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oX -DTE5MDIxOTIzMTQ0N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQH -EwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYD -VQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFz -cyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSM -D7tM9DceqQWC2ObhbHDqeLVu0ThEDaiDzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZ -z+qMkjvN9wfcZnSX9EUi3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC -/tmwqcm8WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LYOph7 -tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2EsiNCubMvJIH5+hCoR6 -4sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCCApswDgYDVR0PAQH/BAQDAgAGMBIG -A1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaC -Ak1GSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pv -bGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu -IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2Vn -LWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0 -ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFz -IGxlaXJhc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBh -IGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVu -b3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBh -bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sg -Q1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFp -bCBhdCBjcHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5 -ayZrU3/b39/zcT0mwBQOxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjP -ytoUMaFP0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQQeJB -CWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxkf1qbFFgBJ34TUMdr -KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM -8CgHrTwXZoi1/baI ------END CERTIFICATE----- - -NetLock Business (Class B) Root -=============================== ------BEGIN CERTIFICATE----- -MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg -VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD -VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv -bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg -VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB -iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S -o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr -1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV -HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ -RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh -dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0 -ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv -c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg -YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh -c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz -Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA -bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl -IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2 -YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj -cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM -43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR -stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI ------END CERTIFICATE----- - -NetLock Express (Class C) Root -============================== ------BEGIN CERTIFICATE----- -MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD -KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ -BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 -dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j -ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB -jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z -W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63 -euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw -DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN -RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn -YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB -IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i -aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0 -ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs -ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo -dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y -emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k -IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ -UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg -YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2 -xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW -gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A== ------END CERTIFICATE----- - -XRamp Global CA Root -==================== ------BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE -BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj -dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx -HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg -U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu -IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx -foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE -zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs -AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry -xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap -oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC -AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc -/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt -qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n -nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz -8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= ------END CERTIFICATE----- - -Go Daddy Class 2 CA -=================== ------BEGIN CERTIFICATE----- -MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY -VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG -A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g -RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD -ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv -2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 -qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j -YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY -vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O -BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o -atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu -MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG -A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim -PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt -I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ -HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI -Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b -vZ8= ------END CERTIFICATE----- - -Starfield Class 2 CA -==================== ------BEGIN CERTIFICATE----- -MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc -U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo -MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG -A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG -SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY -bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ -JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm -epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN -F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF -MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f -hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo -bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g -QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs -afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM -PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl -xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD -KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 -QBFGmh95DmK/D5fs4C8fF5Q= ------END CERTIFICATE----- - -StartCom Certification Authority -================================ ------BEGIN CERTIFICATE----- -MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN -U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu -ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 -NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk -LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg -U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y -o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ -Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d -eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt -2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z -6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ -osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ -untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc -UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT -37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE -FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 -Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj -YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH -AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw -Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg -U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 -LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl -cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh -cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT -dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC -AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh -3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm -vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk -fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 -fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ -EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq -yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl -1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ -lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro -g14= ------END CERTIFICATE----- - -Taiwan GRCA -=========== ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG -EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X -DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv -dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN -w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5 -BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O -1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO -htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov -J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7 -Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t -B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB -O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8 -lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV -HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2 -09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ -TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj -Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2 -Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU -D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz -DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk -Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk -7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ -CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy -+fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS ------END CERTIFICATE----- - -Firmaprofesional Root CA -======================== ------BEGIN CERTIFICATE----- -MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMxIjAgBgNVBAcT -GUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1dG9yaWRhZCBkZSBDZXJ0aWZp -Y2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FA -ZmlybWFwcm9mZXNpb25hbC5jb20wHhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTEL -MAkGA1UEBhMCRVMxIjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMT -OUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2 -ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20wggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5uCp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5V -j1H5WuretXDE7aTt/6MNbg9kUDGvASdYrv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJH -lShbz++AbOCQl4oBPB3zhxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf -3H5idPayBQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcLiam8 -NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcbAgMBAAGjgZ8wgZww -KgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lvbmFsLmNvbTASBgNVHRMBAf8ECDAG -AQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQAD -ggEBAEdz/o0nVPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq -u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36mhoEyIwOdyPdf -wUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzflZKG+TQyTmAyX9odtsz/ny4Cm -7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBpQWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YG -VM+h4k0460tQtcsm9MracEpqoeJ5quGnM/b9Sh/22WA= ------END CERTIFICATE----- - -Wells Fargo Root CA -=================== ------BEGIN CERTIFICATE----- -MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMCVVMxFDASBgNV -BAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN -MDAxMDExMTY0MTI4WhcNMjEwMTE0MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dl -bGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEv -MC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n135zHCLielTWi5MbqNQ1mX -x3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHESxP9cMIlrCL1dQu3U+SlK93OvRw6esP3 -E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4OJgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5 -OEL8pahbSCOz6+MlsoCultQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4j -sNtlAHCEAQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMBAAGj -YTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcBCzAyMDAGCCsGAQUF -BwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRwb2xpY3kwDQYJKoZIhvcNAQEFBQAD -ggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrv -m+0fazbuSCUlFLZWohDo7qd/0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0R -OhPs7fpvcmR7nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx -x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ33ZwmVxwQ023 -tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s= ------END CERTIFICATE----- - -Swisscom Root CA 1 -================== ------BEGIN CERTIFICATE----- -MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQG -EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy -dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4 -MTgyMjA2MjBaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln -aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIIC -IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9m2BtRsiM -MW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdihFvkcxC7mlSpnzNApbjyF -NDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/TilftKaNXXsLmREDA/7n29uj/x2lzZAe -AR81sH8A25Bvxn570e56eqeqDFdvpG3FEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkC -b6dJtDZd0KTeByy2dbcokdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn -7uHbHaBuHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNFvJbN -cA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo19AOeCMgkckkKmUp -WyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjCL3UcPX7ape8eYIVpQtPM+GP+HkM5 -haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJWbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNY -MUJDLXT5xp6mig/p/r+D5kNXJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw -HQYDVR0hBBYwFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j -BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzcK6FptWfUjNP9 -MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzfky9NfEBWMXrrpA9gzXrzvsMn -jgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7IkVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQ -MbFamIp1TpBcahQq4FJHgmDmHtqBsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4H -VtA4oJVwIHaM190e3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtl -vrsRls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ipmXeascCl -OS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HHb6D0jqTsNFFbjCYDcKF3 -1QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksfrK/7DZBaZmBwXarNeNQk7shBoJMBkpxq -nvy5JMWzFYJ+vq6VK+uxwNrjAWALXmmshFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCy -x/yP2FS1k2Kdzs9Z+z0YzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMW -NY6E0F/6MBr1mmz0DlP5OlvRHA== ------END CERTIFICATE----- - -DigiCert Assured ID Root CA -=========================== ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw -IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx -MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL -ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO -9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy -UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW -/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy -oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf -GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF -66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq -hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc -EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn -SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i -8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -DigiCert Global Root CA -======================= ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw -HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw -MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 -dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn -TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 -BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H -4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y -7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB -o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm -8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF -BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr -EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt -tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 -UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -DigiCert High Assurance EV Root CA -================================== ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw -KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw -MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ -MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu -Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t -Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS -OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 -MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ -NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe -h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB -Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY -JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ -V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp -myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK -mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K ------END CERTIFICATE----- - -Certplus Class 2 Primary CA -=========================== ------BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAwPTELMAkGA1UE -BhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFzcyAyIFByaW1hcnkgQ0EwHhcN -OTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2Vy -dHBsdXMxGzAZBgNVBAMTEkNsYXNzIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBANxQltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR -5aiRVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyLkcAbmXuZ -Vg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCdEgETjdyAYveVqUSISnFO -YFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yasH7WLO7dDWWuwJKZtkIvEcupdM5i3y95e -e++U8Rs+yskhwcWYAqqi9lt3m/V+llU0HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRME -CDAGAQH/AgEKMAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJ -YIZIAYb4QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMuY29t -L0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/AN9WM2K191EBkOvD -P9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8yfFC82x/xXp8HVGIutIKPidd3i1R -TtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMRFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+ -7UCmnYR0ObncHoUW2ikbhiMAybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW -//1IMwrh3KWBkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 -l7+ijrRU ------END CERTIFICATE----- - -DST Root CA X3 -============== ------BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK -ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X -DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 -cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT -rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 -UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy -xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d -utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ -MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug -dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE -GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw -RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS -fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ ------END CERTIFICATE----- - -DST ACES CA X6 -============== ------BEGIN CERTIFICATE----- -MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QxETAPBgNVBAsTCERTVCBBQ0VT -MRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0wMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NTha -MFsxCzAJBgNVBAYTAlVTMSAwHgYDVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UE -CxMIRFNUIEFDRVMxFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPuktKe1jzI -DZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7gLFViYsx+tC3dr5BPTCa -pCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZHfAjIgrrep4c9oW24MFbCswKBXy314pow -GCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4aahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPy -MjwmR/onJALJfh1biEITajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1Ud -EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rkc3Qu -Y29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnRy -dXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMtaW5kZXguaHRtbDAdBgNVHQ4EFgQU -CXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZIhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V2 -5FYrnJmQ6AgwbN99Pe7lv7UkQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6t -Fr8hlxCBPeP/h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq -nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpRrscL9yuwNwXs -vFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf29w4LTJxoeHtxMcfrHuBnQfO3 -oKfN5XozNmr6mis= ------END CERTIFICATE----- - -TURKTRUST Certificate Services Provider Root 1 -============================================== ------BEGIN CERTIFICATE----- -MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOcUktUUlVTVCBF -bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGDAJUUjEP -MA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykgMjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0 -acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMx -MDI3MTdaFw0xNTAzMjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsg -U2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYDVQQHDAZB -TktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBC -aWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GX -yGl8hMW0kWxsE2qkVa2kheiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8i -Si9BB35JYbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5CurKZ -8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1JuTm5Rh8i27fbMx4 -W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51b0dewQIDAQABoxAwDjAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46 -sWrv7/hg0Uw2ZkUd82YCdAR7kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxE -q8Sn5RTOPEFhfEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy -B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdAaLX/7KfS0zgY -nNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKSRGQDJereW26fyfJOrN3H ------END CERTIFICATE----- - -TURKTRUST Certificate Services Provider Root 2 -============================================== ------BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBF -bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP -MA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg -QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcN -MDUxMTA3MTAwNzU3WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVr -dHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEPMA0G -A1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls -acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqe -LCDe2JAOCtFp0if7qnefJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKI -x+XlZEdhR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJQv2g -QrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGXJHpsmxcPbe9TmJEr -5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1pzpwACPI2/z7woQ8arBT9pmAPAgMB -AAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58SFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/ntt -Rbj2hWyfIvwqECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 -Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFzgw2lGh1uEpJ+ -hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotHuFEJjOp9zYhys2AzsfAKRO8P -9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LSy3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5 -UrbnBEI= ------END CERTIFICATE----- - -SwissSign Gold CA - G2 -====================== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -SwissSign Silver CA - G2 -======================== ------BEGIN CERTIFICATE----- -MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT -BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X -DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 -aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG -9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 -N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm -+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH -6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu -MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h -qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 -FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs -ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc -celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X -CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB -tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 -cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P -4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F -kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L -3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx -/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa -DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP -e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu -WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ -DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub -DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u ------END CERTIFICATE----- - -GeoTrust Primary Certification Authority -======================================== ------BEGIN CERTIFICATE----- -MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG -EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD -ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx -CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ -cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN -b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 -nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge -RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt -tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI -hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K -Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN -NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa -Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG -1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= ------END CERTIFICATE----- - -thawte Primary Root CA -====================== ------BEGIN CERTIFICATE----- -MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE -BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 -aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 -MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg -SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv -KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT -FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs -oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ -1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc -q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K -aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p -afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD -VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF -AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE -uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX -xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 -jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH -z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== ------END CERTIFICATE----- - -VeriSign Class 3 Public Primary Certification Authority - G5 -============================================================ ------BEGIN CERTIFICATE----- -MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE -BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO -ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk -IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln -biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh -dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt -YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz -j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD -Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ -Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r -fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ -BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv -Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy -aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG -SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ -X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE -KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC -Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE -ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq ------END CERTIFICATE----- - -SecureTrust CA -============== ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy -dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe -BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX -OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t -DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH -GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b -01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH -ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj -aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ -KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu -SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf -mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ -nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -Secure Global CA -================ ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH -bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg -MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg -Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx -YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ -bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g -8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV -HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi -0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn -oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA -MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ -OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn -CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 -3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- - -COMODO Certification Authority -============================== ------BEGIN CERTIFICATE----- -MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE -BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG -A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb -MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD -T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH -+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww -xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV -4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA -1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI -rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k -b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC -AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP -OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ -RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc -IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN -+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== ------END CERTIFICATE----- - -Network Solutions Certificate Authority -======================================= ------BEGIN CERTIFICATE----- -MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG -EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr -IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx -MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu -MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx -jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT -aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT -crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc -/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB -AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP -BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv -bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA -A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q -4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ -GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv -wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD -ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey ------END CERTIFICATE----- - -WellsSecure Public Root Certificate Authority -============================================= ------BEGIN CERTIFICATE----- -MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoM -F1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYw -NAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN -MDcxMjEzMTcwNzU0WhcNMjIxMjE0MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dl -bGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYD -VQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+rWxxTkqxtnt3CxC5FlAM1 -iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjUDk/41itMpBb570OYj7OeUt9tkTmPOL13 -i0Nj67eT/DBMHAGTthP796EfvyXhdDcsHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8 -bJVhHlfXBIEyg1J55oNjz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiB -K0HmOFafSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/SlwxlAgMB -AAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jcmwu -cGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0PAQH/BAQDAgHGMB0GA1UdDgQWBBQm -lRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0jBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGB -i6SBiDCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRww -GgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg -Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEBALkVsUSRzCPI -K0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd/ZDJPHV3V3p9+N701NX3leZ0 -bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pBA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSlj -qHyita04pO2t/caaH/+Xc/77szWnk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+es -E2fDbbFwRnzVlhE9iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJ -tylv2G0xffX8oRAHh84vWdw+WNs= ------END CERTIFICATE----- - -COMODO ECC Certification Authority -================================== ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC -R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE -ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix -GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X -4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni -wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG -FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA -U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -IGC/A -===== ------BEGIN CERTIFICATE----- -MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD -VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE -Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy -MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI -EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT -STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2 -TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW -So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy -HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd -frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ -tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB -egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC -iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK -q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q -MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg -Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI -lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF -0mBWWg== ------END CERTIFICATE----- - -Security Communication EV RootCA1 -================================= ------BEGIN CERTIFICATE----- -MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDElMCMGA1UEChMc -U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMhU2VjdXJpdHkgQ29tbXVuaWNh -dGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIzMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UE -BhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNl -Y3VyaXR5IENvbW11bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSERMqm4miO -/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gOzXppFodEtZDkBp2uoQSX -WHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4z -ZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDFMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4 -bepJz11sS6/vmsJWXMY1VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK -9U2vP9eCOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HWtWS3irO4G8za+6xm -iEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZq51ihPZRwSzJIxXYKLerJRO1RuGG -Av8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDbEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnW -mHyojf6GPgcWkuF75x3sM3Z+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEW -T1MKZPlO9L9OVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 ------END CERTIFICATE----- - -OISTE WISeKey Global Root GA CA -=============================== ------BEGIN CERTIFICATE----- -MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE -BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG -A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH -bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD -VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw -IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5 -IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9 -Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg -Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD -d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ -/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R -LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ -KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm -MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4 -+vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa -hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY -okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0= ------END CERTIFICATE----- - -Microsec e-Szigno Root CA -========================= ------BEGIN CERTIFICATE----- -MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UE -BhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNyb3NlYyBMdGQuMRQwEgYDVQQL -EwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9zZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0 -MDYxMjI4NDRaFw0xNzA0MDYxMjI4NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVz -dDEWMBQGA1UEChMNTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMT -GU1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2uuO/TEdyB5s87lozWbxXG -d36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/N -oqdNAoI/gqyFxuEPkEeZlApxcpMqyabAvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjc -QR/Ji3HWVBTji1R4P770Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJ -PqW+jqpx62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcBAQRb -MFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3AwLQYIKwYBBQUHMAKG -IWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAPBgNVHRMBAf8EBTADAQH/MIIBcwYD -VR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIBAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3 -LmUtc3ppZ25vLmh1L1NaU1ovMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0A -dAB2AOEAbgB5ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn -AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABTAHoAbwBsAGcA -4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABhACAAcwB6AGUAcgBpAG4AdAAg -AGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABoAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMA -egBpAGcAbgBvAC4AaAB1AC8AUwBaAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6 -Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NO -PU1pY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxPPU1pY3Jv -c2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDtiaW5h -cnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuBEGluZm9AZS1zemlnbm8uaHWkdzB1MSMw -IQYDVQQDDBpNaWNyb3NlYyBlLVN6aWduw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhT -WjEWMBQGA1UEChMNTWljcm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhV -MIGsBgNVHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJIVTER -MA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDASBgNVBAsTC2UtU3pp -Z25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBSb290IENBghEAzLjnv04pGv2i3Gal -HCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMT -nGZjWS7KXHAM/IO8VbH0jgdsZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FE -aGAHQzAxQmHl7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a -86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfRhUZLphK3dehK -yVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/MPMMNz7UwiiAc7EBt51alhQB -S6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= ------END CERTIFICATE----- - -Certigna -======== ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw -EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 -MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI -Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q -XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH -GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p -ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg -DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf -Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ -tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ -BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J -SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA -hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ -ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu -PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY -1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE----- - -AC Ra\xC3\xADz Certic\xC3\xA1mara S.A. -====================================== ------BEGIN CERTIFICATE----- -MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT -AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg -LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w -HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+ -U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh -IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN -yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU -2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3 -4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP -2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm -8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf -HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa -Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK -5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b -czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g -ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF -BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug -cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf -AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX -EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v -/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3 -MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4 -3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk -eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f -/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h -RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU -Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ== ------END CERTIFICATE----- - -TC TrustCenter Class 2 CA II -============================ ------BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy -IENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYw -MTEyMTQzODQzWhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 -c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UE -AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jftMjWQ+nEdVl//OEd+DFw -IxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKguNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2 -xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2JXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQ -Xa7pIXSSTYtZgo+U4+lK8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7u -SNQZu+995OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3kUrL84J6E1wIqzCB -7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 -Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU -cnVzdENlbnRlciUyMENsYXNzJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i -SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iSGNn3Bzn1LL4G -dXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprtZjluS5TmVfwLG4t3wVMTZonZ -KNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8au0WOB9/WIFaGusyiC2y8zl3gK9etmF1Kdsj -TYjKUCjLhdLTEKJZbtOTVAB6okaVhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kP -JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk -vQ== ------END CERTIFICATE----- - -TC TrustCenter Class 3 CA II -============================ ------BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy -IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw -MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 -c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE -AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W -yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo -6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ -uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk -2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB -7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 -Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU -cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i -SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE -O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8 -yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9 -IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal -092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc -5A== ------END CERTIFICATE----- - -TC TrustCenter Universal CA I -============================= ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy -IFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcN -MDYwMzIyMTU1NDI4WhcNMjUxMjMxMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMg -VHJ1c3RDZW50ZXIgR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYw -JAYDVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSRJJZ4Hgmgm5qVSkr1YnwC -qMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3TfCZdzHd55yx4Oagmcw6iXSVphU9VDprv -xrlE4Vc93x9UIuVvZaozhDrzznq+VZeujRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtw -ag+1m7Z3W0hZneTvWq3zwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9O -gdwZu5GQfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYDVR0j -BBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0GCSqGSIb3DQEBBQUAA4IBAQAo0uCG -1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X17caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/Cy -vwbZ71q+s2IhtNerNXxTPqYn8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3 -ghUJGooWMNjsydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT -ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/2TYcuiUaUj0a -7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY ------END CERTIFICATE----- - -Deutsche Telekom Root CA 2 -========================== ------BEGIN CERTIFICATE----- -MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMT -RGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEG -A1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENBIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5 -MjM1OTAwWjBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0G -A1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBS -b290IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEUha88EOQ5 -bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhCQN/Po7qCWWqSG6wcmtoI -KyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1MjwrrFDa1sPeg5TKqAyZMg4ISFZbavva4VhY -AUlfckE8FQYBjl2tqriTtM2e66foai1SNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aK -Se5TBY8ZTNXeWHmb0mocQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTV -jlsB9WoHtxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAPBgNV -HRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAlGRZrTlk5ynr -E/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756AbrsptJh6sTtU6zkXR34ajgv8HzFZMQSy -zhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpaIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8 -rZ7/gFnkm0W09juwzTkZmDLl6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4G -dyd1Lx+4ivn+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU -Cm26OWMohpLzGITY+9HPBVZkVw== ------END CERTIFICATE----- - -ComSign Secured CA -================== ------BEGIN CERTIFICATE----- -MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAwPDEbMBkGA1UE -AxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0w -NDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwxGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBD -QTEQMA4GA1UEChMHQ29tU2lnbjELMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDGtWhfHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs -49ohgHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sWv+bznkqH -7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ueMv5WJDmyVIRD9YTC2LxB -kMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d1 -9guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUw -AwEB/zBEBgNVHR8EPTA7MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29t -U2lnblNlY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58ADsA -j8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkqhkiG9w0BAQUFAAOC -AQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7piL1DRYHjZiM/EoZNGeQFsOY3wo3a -BijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtCdsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtp -FhpFfTMDZflScZAmlaxMDPWLkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP -51qJThRv4zdLhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz -OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== ------END CERTIFICATE----- - -Cybertrust Global Root -====================== ------BEGIN CERTIFICATE----- -MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li -ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 -MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD -ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -+Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW -0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL -AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin -89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT -8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 -MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G -A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO -lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi -5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 -hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T -X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW -WL1WMRJOEcgh4LMRkWXbtKaIOM5V ------END CERTIFICATE----- - -ePKI Root Certification Authority -================================= ------BEGIN CERTIFICATE----- -MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG -EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg -Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx -MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq -MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs -IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi -lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv -qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX -12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O -WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ -ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao -lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ -vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi -Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi -MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH -ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 -1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq -KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV -xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP -NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r -GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE -xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx -gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy -sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD -BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE----- - -T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3 -============================================================================================================================= ------BEGIN CERTIFICATE----- -MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRSMRgwFgYDVQQH -DA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJpbGltc2VsIHZlIFRla25vbG9q -aWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSwVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ry -b25payB2ZSBLcmlwdG9sb2ppIEFyYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNV -BAsMGkthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUg -S8O2ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAeFw0wNzA4 -MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIxGDAWBgNVBAcMD0dlYnpl -IC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmlsaW1zZWwgdmUgVGVrbm9sb2ppayBBcmHF -n3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBUQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZl -IEtyaXB0b2xvamkgQXJhxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2Ft -dSBTZXJ0aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7ZrIFNl -cnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4hgb46ezzb8R1Sf1n68yJMlaCQvEhO -Eav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yKO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1 -xnnRFDDtG1hba+818qEhTsXOfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR -6Oqeyjh1jmKwlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL -hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQIDAQABo0IwQDAd -BgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmPNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4 -N5EY3ATIZJkrGG2AA1nJrvhY0D7twyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLT -y9LQQfMmNkqblWwM7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYh -LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M -dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI= ------END CERTIFICATE----- - -Buypass Class 2 CA 1 -==================== ------BEGIN CERTIFICATE----- -MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2 -MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh -c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M -cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83 -0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4 -0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R -uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P -AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV -1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt -7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2 -fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w -wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho ------END CERTIFICATE----- - -Buypass Class 3 CA 1 -==================== ------BEGIN CERTIFICATE----- -MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMyBDQSAxMB4XDTA1 -MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh -c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKx -ifZgisRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//zNIqeKNc0 -n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI+MkcVyzwPX6UvCWThOia -AJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2RhzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c -1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0P -AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFPBdy7 -pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27sEzNxZy5p+qksP2bA -EllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2mSlf56oBzKwzqBwKu5HEA6BvtjT5 -htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yCe/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQj -el/wroQk5PMr+4okoyeYZdowdXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 ------END CERTIFICATE----- - -EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 -========================================================================== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -certSIGN ROOT CA -================ ------BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD -VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa -Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE -CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I -JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH -rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 -ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD -0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 -AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B -Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB -AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 -SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 -x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt -vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz -TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD ------END CERTIFICATE----- - -CNNIC ROOT -========== ------BEGIN CERTIFICATE----- -MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJDTjEOMAwGA1UE -ChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2MDcwOTE0WhcNMjcwNDE2MDcw -OTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1Qw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzD -o+/hn7E7SIX1mlwhIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tiz -VHa6dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZOV/kbZKKT -VrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrCGHn2emU1z5DrvTOTn1Or -czvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gNv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrK -y5nLAgMBAAGjczBxMBEGCWCGSAGG+EIBAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscC -wQ7vptU7ETAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991S -lgrHAsEO76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnKOOK5 -Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvHugDnuL8BV8F3RTIM -O/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7HgviyJA/qIYM/PmLXoXLT1tLYhFHxUV8 -BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fLbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2 -G8kS1sHNzYDzAgE8yGnLRUhj2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5m -mxE= ------END CERTIFICATE----- - -ApplicationCA - Japanese Government -=================================== ------BEGIN CERTIFICATE----- -MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEcMBoGA1UEChMT -SmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRpb25DQTAeFw0wNzEyMTIxNTAw -MDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYTAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zl -cm5tZW50MRYwFAYDVQQLEw1BcHBsaWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAp23gdE6Hj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4 -fl+Kf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55IrmTwcrN -wVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cwFO5cjFW6WY2H/CPek9AE -jP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDihtQWEjdnjDuGWk81quzMKq2edY3rZ+nYVu -nyoKb58DKTCXKB28t89UKU5RMfkntigm/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRU -WssmP3HMlEYNllPqa0jQk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNV -BAYTAkpQMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOCseOD -vOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADlqRHZ3ODrs -o2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJhyzjVOGjprIIC8CFqMjSnHH2HZ9g -/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYD -io+nEhEMy/0/ecGc/WLuo89UDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmW -dupwX3kSa+SjB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL -rosot4LKGAfmt1t06SAZf7IbiVQ= ------END CERTIFICATE----- - -GeoTrust Primary Certification Authority - G3 -============================================= ------BEGIN CERTIFICATE----- -MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE -BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 -IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz -NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo -YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT -LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j -K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE -c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C -IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu -dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr -2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 -cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE -Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD -AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s -t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt ------END CERTIFICATE----- - -thawte Primary Root CA - G2 -=========================== ------BEGIN CERTIFICATE----- -MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC -VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu -IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg -Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV -MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG -b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt -IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS -LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 -8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU -mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN -G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K -rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== ------END CERTIFICATE----- - -thawte Primary Root CA - G3 -=========================== ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE -BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 -aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w -ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh -d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD -VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG -A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At -P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC -+BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY -7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW -vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ -KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK -A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu -t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC -8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm -er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= ------END CERTIFICATE----- - -GeoTrust Primary Certification Authority - G2 -============================================= ------BEGIN CERTIFICATE----- -MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu -Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD -ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 -OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg -MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl -b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG -BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc -KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ -EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m -ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 -npaqBA+K ------END CERTIFICATE----- - -VeriSign Universal Root Certification Authority -=============================================== ------BEGIN CERTIFICATE----- -MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE -BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO -ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk -IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u -IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV -UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv -cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj -1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP -MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 -9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I -AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR -tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G -CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O -a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud -DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 -Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx -Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx -P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P -wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 -mJO37M2CYfE45k+XmCpajQ== ------END CERTIFICATE----- - -VeriSign Class 3 Public Primary Certification Authority - G4 -============================================================ ------BEGIN CERTIFICATE----- -MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC -VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 -b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz -ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU -cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo -b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 -Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz -rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw -HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u -Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD -A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx -AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== ------END CERTIFICATE----- - -NetLock Arany (Class Gold) Főtanúsítvány -============================================ ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G -A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 -dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB -cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx -MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO -ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv -biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 -c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu -0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw -/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk -H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw -fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 -neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW -qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta -YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna -NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu -dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -Staat der Nederlanden Root CA - G2 -================================== ------BEGIN CERTIFICATE----- -MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE -CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g -Um9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oXDTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMC -TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l -ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ -5291qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8SpuOUfiUtn -vWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPUZ5uW6M7XxgpT0GtJlvOj -CwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvEpMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiil -e7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCR -OME4HYYEhLoaJXhena/MUGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpI -CT0ugpTNGmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy5V65 -48r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv6q012iDTiIJh8BIi -trzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEKeN5KzlW/HdXZt1bv8Hb/C3m1r737 -qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMB -AAGjgZcwgZQwDwYDVR0TAQH/BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcC -ARYxaHR0cDovL3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqGSIb3DQEBCwUA -A4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLySCZa59sCrI2AGeYwRTlHSeYAz -+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwj -f/ST7ZwaUb7dRUG/kSS0H4zpX897IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaN -kqbG9AclVMwWVxJKgnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfk -CpYL+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxLvJxxcypF -URmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkmbEgeqmiSBeGCc1qb3Adb -CG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvkN1trSt8sV4pAWja63XVECDdCcAz+3F4h -oKOKwJCcaNpQ5kUQR3i2TtJlycM33+FCY7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoV -IPVVYpbtbZNQvOSqeK3Zywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm -66+KAQ== ------END CERTIFICATE----- - -CA Disig -======== ------BEGIN CERTIFICATE----- -MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMK -QnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwHhcNMDYw -MzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlz -bGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgm -GErENx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnXmjxUizkD -Pw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYDXcDtab86wYqg6I7ZuUUo -hwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhWS8+2rT+MitcE5eN4TPWGqvWP+j1scaMt -ymfraHtuM6kMgiioTGohQBUgDCZbg8KpFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8w -gfwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0P -AQH/BAQDAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cuZGlz -aWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5zay9jYS9jcmwvY2Ff -ZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2svY2EvY3JsL2NhX2Rpc2lnLmNybDAa -BgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEwDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59t -WDYcPQuBDRIrRhCA/ec8J9B6yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3 -mkkp7M5+cTxqEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ -CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeBEicTXxChds6K -ezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFNPGO+I++MzVpQuGhU+QqZMxEA -4Z7CRneC9VkGjCFMhwnN5ag= ------END CERTIFICATE----- - -Juur-SK -======= ------BEGIN CERTIFICATE----- -MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA -c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw -DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG -SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf -TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC -+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw -UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa -Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF -MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD -HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh -AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA -cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr -AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw -cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE -FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G -A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo -ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL -abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678 -IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh -Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2 -yyqcjg== ------END CERTIFICATE----- - -Hongkong Post Root CA 1 -======================= ------BEGIN CERTIFICATE----- -MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT -DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx -NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n -IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 -ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr -auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh -qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY -V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV -HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i -h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio -l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei -IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps -T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT -c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== ------END CERTIFICATE----- - -SecureSign RootCA11 -=================== ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi -SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS -b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw -KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 -cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL -TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO -wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq -g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP -O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA -bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX -t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh -OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r -bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ -Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 -y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 -lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= ------END CERTIFICATE----- - -ACEDICOM Root -============= ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky -CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX -bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/ -D/xwzoiQ ------END CERTIFICATE----- - -Microsec e-Szigno Root CA 2009 -============================== ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER -MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv -c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE -BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt -U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA -fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG -0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA -pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm -1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC -AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf -QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE -FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o -lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX -I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 -yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi -LXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi -=================================================== ------BEGIN CERTIFICATE----- -MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG -EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz -ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3 -MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0 -cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u -aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY -8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y -jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI -JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk -9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG -SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d -F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq -D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4 -Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq -fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX ------END CERTIFICATE----- - -GlobalSign Root CA - R3 -======================= ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv -YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh -bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT -aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln -bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt -iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ -0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 -rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl -OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 -xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE -FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 -lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 -EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E -bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 -YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r -kpeDMdmztcpHWD9f ------END CERTIFICATE----- - -TC TrustCenter Universal CA III -=============================== ------BEGIN CERTIFICATE----- -MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy -IFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAe -Fw0wOTA5MDkwODE1MjdaFw0yOTEyMzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNU -QyBUcnVzdENlbnRlciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0Ex -KDAmBgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF5+cvAqBNLaT6hdqbJYUt -QCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYvDIRlzg9uwliT6CwLOunBjvvya8o84pxO -juT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8vzArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+Eut -CHnNaYlAJ/Uqwa1D7KRTyGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1 -M4BDj5yjdipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBhMB8G -A1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI4jANBgkqhkiG9w0BAQUFAAOCAQEA -g8ev6n9NCjw5sWi+e22JLumzCecYV42FmhfzdkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+ -KGwWaODIl0YgoGhnYIg5IFHYaAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhK -BgePxLcHsU0GDeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV -CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPHLQNjO9Po5KIq -woIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg== ------END CERTIFICATE----- - -Autoridad de Certificacion Firmaprofesional CIF A62634068 -========================================================= ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA -BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 -MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw -QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB -NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD -Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P -B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY -7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH -ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI -plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX -MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX -LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK -bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU -vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud -EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH -DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp -cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA -bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx -ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx -51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk -R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP -T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f -Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl -osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR -crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR -saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD -KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi -6Et8Vcad+qMUu2WFbm5PEn4KPJ2V ------END CERTIFICATE----- - -Izenpe.com -========== ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - -Chambers of Commerce Root - 2008 -================================ ------BEGIN CERTIFICATE----- -MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD -MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv -bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu -QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy -Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl -ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF -EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl -cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA -XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj -h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ -ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk -NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g -D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 -lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ -0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj -ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 -EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI -G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ -BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh -bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh -bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC -CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH -AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 -wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH -3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU -RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 -M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 -YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF -9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK -zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG -nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg -OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ ------END CERTIFICATE----- - -Global Chambersign Root - 2008 -============================== ------BEGIN CERTIFICATE----- -MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD -MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv -bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu -QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx -NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg -Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ -QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD -aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf -VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf -XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 -ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB -/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA -TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M -H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe -Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF -HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh -wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB -AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT -BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE -BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm -aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm -aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp -1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 -dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG -/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 -ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s -dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg -9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH -foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du -qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr -P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq -c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z -09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B ------END CERTIFICATE----- - -Go Daddy Root Certificate Authority - G2 -======================================== ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu -MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G -A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq -9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD -+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd -fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl -NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 -BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac -vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r -5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV -N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 ------END CERTIFICATE----- - -Starfield Root Certificate Authority - G2 -========================================= ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s -b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 -eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw -DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg -VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB -dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv -W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs -bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk -N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf -ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU -JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol -TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx -4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw -F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ -c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -Starfield Services Root Certificate Authority - G2 -================================================== ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s -b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl -IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV -BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT -dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg -Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 -h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa -hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP -LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB -rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG -SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP -E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy -xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza -YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 ------END CERTIFICATE----- - -AffirmTrust Commercial -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw -MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb -DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV -C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 -BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww -MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV -HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG -hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi -qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv -0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh -sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE----- - -AffirmTrust Networking -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw -MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE -Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI -dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 -/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb -h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV -HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu -UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 -12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 -WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 -/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= ------END CERTIFICATE----- - -AffirmTrust Premium -=================== ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy -OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy -dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn -BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV -5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs -+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd -GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R -p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI -S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 -6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 -/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo -+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv -MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg -Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC -6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S -L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK -+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV -BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg -IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 -g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb -zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== ------END CERTIFICATE----- - -AffirmTrust Premium ECC -======================= ------BEGIN CERTIFICATE----- -MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV -BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx -MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U -cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ -N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW -BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK -BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X -57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM -eQ== ------END CERTIFICATE----- - -Certum Trusted Network CA -========================= ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK -ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy -MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU -ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC -l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J -J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 -fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 -cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB -Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw -DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj -jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 -mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj -Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -Certinomis - Autorité Racine -============================= ------BEGIN CERTIFICATE----- -MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK -Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAkBgNVBAMMHUNlcnRpbm9taXMg -LSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkG -A1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYw -JAYDVQQDDB1DZXJ0aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jYF1AMnmHa -wE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N8y4oH3DfVS9O7cdxbwly -Lu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWerP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw -2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92N -jMD2AR5vpTESOH2VwnHu7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9q -c1pkIuVC28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6lSTC -lrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1Enn1So2+WLhl+HPNb -xxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB0iSVL1N6aaLwD4ZFjliCK0wi1F6g -530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql095gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna -4NH4+ej9Uji29YnfAgMBAAGjWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBQNjLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ -KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9sov3/4gbIOZ/x -WqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZMOH8oMDX/nyNTt7buFHAAQCva -R6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40 -nJ+U8/aGH88bc62UeYdocMMzpXDn2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1B -CxMjidPJC+iKunqjo3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjv -JL1vnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG5ERQL1TE -qkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWqpdEdnV1j6CTmNhTih60b -WfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZbdsLLO7XSAPCjDuGtbkD326C00EauFddE -wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/ -vgt2Fl43N+bYdJeimUV5 ------END CERTIFICATE----- - -Root CA Generalitat Valenciana -============================== ------BEGIN CERTIFICATE----- -MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE -ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290 -IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3 -WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE -CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2 -F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B -ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ -D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte -JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB -AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n -dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB -ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl -AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA -YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy -AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA -aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt -AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA -YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu -AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA -OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0 -dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV -BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G -A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S -b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh -TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz -Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63 -NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH -iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt -+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= ------END CERTIFICATE----- - -A-Trust-nQual-03 -================ ------BEGIN CERTIFICATE----- -MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJBVDFIMEYGA1UE -Cgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVy -a2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5RdWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5R -dWFsLTAzMB4XDTA1MDgxNzIyMDAwMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgw -RgYDVQQKDD9BLVRydXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0 -ZW52ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMMEEEtVHJ1 -c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtPWFuA/OQO8BBC4SA -zewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUjlUC5B3ilJfYKvUWG6Nm9wASOhURh73+n -yfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPE -SU7l0+m0iKsMrmKS1GWH2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4 -iHQF63n1k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs2e3V -cuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECERqlWdV -eRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAVdRU0VlIXLOThaq/Yy/kgM40 -ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fGKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmr -sQd7TZjTXLDR8KdCoLXEjq/+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZd -JXDRZslo+S4RFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS -mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmEDNuxUCAKGkq6 -ahq97BvIxYSazQ== ------END CERTIFICATE----- - -TWCA Root Certification Authority -================================= ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ -VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG -EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB -IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx -QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC -oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP -4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r -y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB -BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG -9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC -mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW -QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY -T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny -Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -Security Communication RootCA2 -============================== ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc -U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh -dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC -SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy -aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ -+T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R -3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV -spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K -EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 -QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB -CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj -u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk -3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q -tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 -mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -EC-ACC -====== ------BEGIN CERTIFICATE----- -MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE -BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w -ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD -VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE -CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT -BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 -MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt -SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl -Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh -cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK -w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT -ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 -HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a -E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw -0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD -VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 -Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l -dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ -lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa -Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe -l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 -E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D -5EI= ------END CERTIFICATE----- - -Hellenic Academic and Research Institutions RootCA 2011 -======================================================= ------BEGIN CERTIFICATE----- -MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT -O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y -aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z -IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT -AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z -IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo -IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI -1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa -71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u -8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH -3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ -MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 -MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu -b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt -XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 -TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD -/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N -7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 ------END CERTIFICATE----- - -Actalis Authentication Root CA -============================== ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM -BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE -AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky -MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz -IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ -wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa -by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 -zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f -YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 -oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l -EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 -hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 -EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 -jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY -iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI -WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 -JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx -K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ -Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC -4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo -2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz -lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem -OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 -vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -Trustis FPS Root CA -=================== ------BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG -EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 -IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV -BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ -RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk -H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa -cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt -o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA -AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd -BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c -GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC -yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P -8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV -l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl -iB6XzCGcKQENZetX2fNXlrtIzYE= ------END CERTIFICATE----- - -StartCom Certification Authority -================================ ------BEGIN CERTIFICATE----- -MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN -U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu -ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 -NjM3WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk -LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg -U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y -o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ -Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d -eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt -2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z -6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ -osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ -untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc -UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT -37uMdBNSSwIDAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFulF2mHMMo0aEPQ -Qa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCCATgwLgYIKwYBBQUHAgEWImh0 -dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cu -c3RhcnRzc2wuY29tL2ludGVybWVkaWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENv -bW1lcmNpYWwgKFN0YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0 -aGUgc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93d3cuc3RhcnRzc2wuY29t -L3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBG -cmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5 -fPGFf59Jb2vKXfuM/gTFwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWm -N3PH/UvSTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst0OcN -Org+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNcpRJvkrKTlMeIFw6T -tn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKlCcWw0bdT82AUuoVpaiF8H3VhFyAX -e2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVFP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA -2MFrLH9ZXF2RsXAiV+uKa0hK1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBs -HvUwyKMQ5bLmKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE -JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ8dCAWZvLMdib -D4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnmfyWl8kgAwKQB2j8= ------END CERTIFICATE----- - -StartCom Certification Authority G2 -=================================== ------BEGIN CERTIFICATE----- -MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMN -U3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -RzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UE -ChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8O -o1XJJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsDvfOpL9HG -4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnooD/Uefyf3lLE3PbfHkffi -Aez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/Q0kGi4xDuFby2X8hQxfqp0iVAXV16iul -Q5XqFYSdCI0mblWbq9zSOdIxHWDirMxWRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbs -O+wmETRIjfaAKxojAuuKHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8H -vKTlXcxNnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM0D4L -nMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/iUUjXuG+v+E5+M5iS -FGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9Ha90OrInwMEePnWjFqmveiJdnxMa -z6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHgTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJ -KoZIhvcNAQELBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K -2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfXUfEpY9Z1zRbk -J4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl6/2o1PXWT6RbdejF0mCy2wl+ -JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG -/+gyRr61M3Z3qAFdlsHB1b6uJcDJHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTc -nIhT76IxW1hPkWLIwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/Xld -blhYXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5lIxKVCCIc -l85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoohdVddLHRDiBYmxOlsGOm -7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulrso8uBtjRkcfGEvRM/TAXw8HaOFvjqerm -obp573PYtlNXLfbQ4ddI ------END CERTIFICATE----- - -Buypass Class 2 Root CA -======================= ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X -DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 -eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 -g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn -9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b -/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU -CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff -awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI -zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn -Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX -Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs -M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF -AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI -osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S -aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd -DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD -LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 -oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC -wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS -CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN -rJgWVqA= ------END CERTIFICATE----- - -Buypass Class 3 Root CA -======================= ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X -DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 -eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH -sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR -5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh -7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ -ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH -2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV -/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ -RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA -Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq -j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF -AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G -uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG -Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 -ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 -KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz -6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug -UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe -eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi -Cp/HuZc= ------END CERTIFICATE----- - -T-TeleSec GlobalRoot Class 3 -============================ ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM -IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU -cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx -MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz -dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD -ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK -9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU -NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF -iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W -0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr -AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb -fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT -ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h -P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== ------END CERTIFICATE----- - -EE Certification Centre Root CA -=============================== ------BEGIN CERTIFICATE----- -MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG -EwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEoMCYGA1UEAwwfRUUgQ2Vy -dGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIw -MTAxMDMwMTAxMDMwWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlB -UyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRy -ZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUyeuuOF0+W2Ap7kaJjbMeM -TC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvObntl8jixwKIy72KyaOBhU8E2lf/slLo2 -rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIwWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw -93X2PaRka9ZP585ArQ/dMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtN -P2MbRMNE1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/zQas8fElyalL1BSZ -MEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEF -BQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEFBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+Rj -xY6hUFaTlrg4wCQiZrxTFGGVv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqM -lIpPnTX/dqQGE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u -uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIWiAYLtqZLICjU -3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/vGVCJYMzpJJUPwssd8m92kMfM -dcGWxZ0= ------END CERTIFICATE----- diff --git a/libs/tornado/concurrent.py b/libs/tornado/concurrent.py index a9002b16..63b0a8c1 100755 --- a/libs/tornado/concurrent.py +++ b/libs/tornado/concurrent.py @@ -40,52 +40,132 @@ class ReturnValueIgnoredError(Exception): pass -class _DummyFuture(object): +class Future(object): + """Placeholder for an asynchronous result. + + A ``Future`` encapsulates the result of an asynchronous + operation. In synchronous applications ``Futures`` are used + to wait for the result from a thread or process pool; in + Tornado they are normally used with `.IOLoop.add_future` or by + yielding them in a `.gen.coroutine`. + + `tornado.concurrent.Future` is similar to + `concurrent.futures.Future`, but not thread-safe (and therefore + faster for use with single-threaded event loops). + + In addition to ``exception`` and ``set_exception``, methods ``exc_info`` + and ``set_exc_info`` are supported to capture tracebacks in Python 2. + The traceback is automatically available in Python 3, but in the + Python 2 futures backport this information is discarded. + This functionality was previously available in a separate class + ``TracebackFuture``, which is now a deprecated alias for this class. + + .. versionchanged:: 3.3 + `tornado.concurrent.Future` is always a thread-unsafe ``Future`` + with support for the ``exc_info`` methods. Previously it would + be an alias for the thread-safe `concurrent.futures.Future` + if that package was available and fall back to the thread-unsafe + implementation if it was not. + + """ def __init__(self): self._done = False self._result = None self._exception = None + self._exc_info = None self._callbacks = [] def cancel(self): + """Cancel the operation, if possible. + + Tornado ``Futures`` do not support cancellation, so this method always + returns False. + """ return False def cancelled(self): + """Returns True if the operation has been cancelled. + + Tornado ``Futures`` do not support cancellation, so this method + always returns False. + """ return False def running(self): + """Returns True if this operation is currently running.""" return not self._done def done(self): + """Returns True if the future has finished running.""" return self._done def result(self, timeout=None): - self._check_done() - if self._exception: + """If the operation succeeded, return its result. If it failed, + re-raise its exception. + """ + if self._result is not None: + return self._result + if self._exc_info is not None: + raise_exc_info(self._exc_info) + elif self._exception is not None: raise self._exception + self._check_done() return self._result def exception(self, timeout=None): - self._check_done() - if self._exception: + """If the operation raised an exception, return the `Exception` + object. Otherwise returns None. + """ + if self._exception is not None: return self._exception else: + self._check_done() return None def add_done_callback(self, fn): + """Attaches the given callback to the `Future`. + + It will be invoked with the `Future` as its argument when the Future + has finished running and its result is available. In Tornado + consider using `.IOLoop.add_future` instead of calling + `add_done_callback` directly. + """ if self._done: fn(self) else: self._callbacks.append(fn) def set_result(self, result): + """Sets the result of a ``Future``. + + It is undefined to call any of the ``set`` methods more than once + on the same object. + """ self._result = result self._set_done() def set_exception(self, exception): + """Sets the exception of a ``Future.``""" self._exception = exception self._set_done() + def exc_info(self): + """Returns a tuple in the same format as `sys.exc_info` or None. + + .. versionadded:: 3.3 + """ + return self._exc_info + + def set_exc_info(self, exc_info): + """Sets the exception information of a ``Future.`` + + Preserves tracebacks on Python 2. + + .. versionadded:: 3.3 + """ + self._exc_info = exc_info + self.set_exception(exc_info[1]) + def _check_done(self): if not self._done: raise Exception("DummyFuture does not support blocking for results") @@ -97,38 +177,16 @@ class _DummyFuture(object): cb(self) self._callbacks = None +TracebackFuture = Future + if futures is None: - Future = _DummyFuture + FUTURES = Future else: - Future = futures.Future + FUTURES = (futures.Future, Future) -class TracebackFuture(Future): - """Subclass of `Future` which can store a traceback with - exceptions. - - The traceback is automatically available in Python 3, but in the - Python 2 futures backport this information is discarded. - """ - def __init__(self): - super(TracebackFuture, self).__init__() - self.__exc_info = None - - def exc_info(self): - return self.__exc_info - - def set_exc_info(self, exc_info): - """Traceback-aware replacement for - `~concurrent.futures.Future.set_exception`. - """ - self.__exc_info = exc_info - self.set_exception(exc_info[1]) - - def result(self, timeout=None): - if self.__exc_info is not None: - raise_exc_info(self.__exc_info) - else: - return super(TracebackFuture, self).result(timeout=timeout) +def is_future(x): + return isinstance(x, FUTURES) class DummyExecutor(object): @@ -254,10 +312,13 @@ def return_future(f): def chain_future(a, b): """Chain two futures together so that when one completes, so does the other. - The result (success or failure) of ``a`` will be copied to ``b``. + The result (success or failure) of ``a`` will be copied to ``b``, unless + ``b`` has already been completed or cancelled by the time ``a`` finishes. """ def copy(future): assert future is a + if b.done(): + return if (isinstance(a, TracebackFuture) and isinstance(b, TracebackFuture) and a.exc_info() is not None): b.set_exc_info(a.exc_info()) diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index 0df7a7ee..fc7d7f26 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -268,6 +268,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): info["callback"](HTTPResponse( request=info["request"], code=code, headers=info["headers"], buffer=buffer, effective_url=effective_url, error=error, + reason=info['headers'].get("X-Http-Reason", None), request_time=time.time() - info["curl_start_time"], time_info=time_info)) except Exception: @@ -470,7 +471,11 @@ def _curl_header_callback(headers, header_line): header_line = header_line.strip() if header_line.startswith("HTTP/"): headers.clear() - return + try: + (__, __, reason) = httputil.parse_response_start_line(header_line) + header_line = "X-Http-Reason: %s" % reason + except httputil.HTTPInputException: + return if not header_line: return headers.parse_line(header_line) diff --git a/libs/tornado/escape.py b/libs/tornado/escape.py index 95c0f24e..48fa673c 100755 --- a/libs/tornado/escape.py +++ b/libs/tornado/escape.py @@ -75,7 +75,7 @@ def xhtml_unescape(value): # The fact that json_encode wraps json.dumps is an implementation detail. -# Please see https://github.com/facebook/tornado/pull/706 +# Please see https://github.com/tornadoweb/tornado/pull/706 # before sending a pull request that adds **kwargs to this function. def json_encode(value): """JSON-encodes the given Python object.""" diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py index aa931b45..4d1dc6e1 100755 --- a/libs/tornado/gen.py +++ b/libs/tornado/gen.py @@ -87,9 +87,9 @@ import itertools import sys import types -from tornado.concurrent import Future, TracebackFuture +from tornado.concurrent import Future, TracebackFuture, is_future, chain_future from tornado.ioloop import IOLoop -from tornado.stack_context import ExceptionStackContext, wrap +from tornado import stack_context class KeyReuseError(Exception): @@ -112,6 +112,10 @@ class ReturnValueIgnoredError(Exception): pass +class TimeoutError(Exception): + """Exception raised by ``with_timeout``.""" + + def engine(func): """Callback-oriented decorator for asynchronous generators. @@ -129,45 +133,20 @@ def engine(func): `~tornado.web.RequestHandler` :ref:`HTTP verb methods `, which use ``self.finish()`` in place of a callback argument. """ + func = _make_coroutine_wrapper(func, replace_callback=False) @functools.wraps(func) def wrapper(*args, **kwargs): - runner = None - - def handle_exception(typ, value, tb): - # if the function throws an exception before its first "yield" - # (or is not a generator at all), the Runner won't exist yet. - # However, in that case we haven't reached anything asynchronous - # yet, so we can just let the exception propagate. - if runner is not None: - return runner.handle_exception(typ, value, tb) - return False - with ExceptionStackContext(handle_exception) as deactivate: - try: - result = func(*args, **kwargs) - except (Return, StopIteration) as e: - result = getattr(e, 'value', None) - else: - if isinstance(result, types.GeneratorType): - def final_callback(value): - if value is not None: - raise ReturnValueIgnoredError( - "@gen.engine functions cannot return values: " - "%r" % (value,)) - assert value is None - deactivate() - runner = Runner(result, final_callback) - runner.run() - return - if result is not None: + future = func(*args, **kwargs) + def final_callback(future): + if future.result() is not None: raise ReturnValueIgnoredError( "@gen.engine functions cannot return values: %r" % - (result,)) - deactivate() - # no yield, so we're done + (future.result(),)) + future.add_done_callback(final_callback) return wrapper -def coroutine(func): +def coroutine(func, replace_callback=True): """Decorator for asynchronous generators. Any generator that yields objects from this module must be wrapped @@ -191,43 +170,56 @@ def coroutine(func): From the caller's perspective, ``@gen.coroutine`` is similar to the combination of ``@return_future`` and ``@gen.engine``. """ + return _make_coroutine_wrapper(func, replace_callback=True) + + +def _make_coroutine_wrapper(func, replace_callback): + """The inner workings of ``@gen.coroutine`` and ``@gen.engine``. + + The two decorators differ in their treatment of the ``callback`` + argument, so we cannot simply implement ``@engine`` in terms of + ``@coroutine``. + """ @functools.wraps(func) def wrapper(*args, **kwargs): - runner = None future = TracebackFuture() - if 'callback' in kwargs: + if replace_callback and 'callback' in kwargs: callback = kwargs.pop('callback') IOLoop.current().add_future( future, lambda future: callback(future.result())) - def handle_exception(typ, value, tb): - try: - if runner is not None and runner.handle_exception(typ, value, tb): - return True - except Exception: - typ, value, tb = sys.exc_info() - future.set_exc_info((typ, value, tb)) - return True - with ExceptionStackContext(handle_exception) as deactivate: - try: - result = func(*args, **kwargs) - except (Return, StopIteration) as e: - result = getattr(e, 'value', None) - except Exception: - deactivate() - future.set_exc_info(sys.exc_info()) + try: + result = func(*args, **kwargs) + except (Return, StopIteration) as e: + result = getattr(e, 'value', None) + except Exception: + future.set_exc_info(sys.exc_info()) + return future + else: + if isinstance(result, types.GeneratorType): + # Inline the first iteration of Runner.run. This lets us + # avoid the cost of creating a Runner when the coroutine + # never actually yields, which in turn allows us to + # use "optional" coroutines in critical path code without + # performance penalty for the synchronous case. + try: + orig_stack_contexts = stack_context._state.contexts + yielded = next(result) + if stack_context._state.contexts is not orig_stack_contexts: + yielded = TracebackFuture() + yielded.set_exception( + stack_context.StackContextInconsistentError( + 'stack_context inconsistency (probably caused ' + 'by yield within a "with StackContext" block)')) + except (StopIteration, Return) as e: + future.set_result(getattr(e, 'value', None)) + except Exception: + future.set_exc_info(sys.exc_info()) + else: + Runner(result, future, yielded) return future - else: - if isinstance(result, types.GeneratorType): - def final_callback(value): - deactivate() - future.set_result(value) - runner = Runner(result, final_callback) - runner.run() - return future - deactivate() - future.set_result(result) + future.set_result(result) return future return wrapper @@ -348,7 +340,7 @@ class WaitAll(YieldPoint): return [self.runner.pop_result(key) for key in self.keys] -class Task(YieldPoint): +def Task(func, *args, **kwargs): """Runs a single asynchronous operation. Takes a function (and optional additional arguments) and runs it with @@ -362,25 +354,25 @@ class Task(YieldPoint): func(args, callback=(yield gen.Callback(key))) result = yield gen.Wait(key) + + .. versionchanged:: 3.3 + ``gen.Task`` is now a function that returns a `.Future`, instead of + a subclass of `YieldPoint`. It still behaves the same way when + yielded. """ - def __init__(self, func, *args, **kwargs): - assert "callback" not in kwargs - self.args = args - self.kwargs = kwargs - self.func = func - - def start(self, runner): - self.runner = runner - self.key = object() - runner.register_callback(self.key) - self.kwargs["callback"] = runner.result_callback(self.key) - self.func(*self.args, **self.kwargs) - - def is_ready(self): - return self.runner.is_ready(self.key) - - def get_result(self): - return self.runner.pop_result(self.key) + future = Future() + def handle_exception(typ, value, tb): + if future.done(): + return False + future.set_exc_info((typ, value, tb)) + return True + def set_result(result): + if future.done(): + return + future.set_result(result) + with stack_context.ExceptionStackContext(handle_exception): + func(*args, callback=_argument_adapter(set_result), **kwargs) + return future class YieldFuture(YieldPoint): @@ -414,10 +406,14 @@ class YieldFuture(YieldPoint): class Multi(YieldPoint): """Runs multiple asynchronous operations in parallel. - Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of + Takes a list of ``YieldPoints`` or ``Futures`` and returns a list of their responses. It is not necessary to call `Multi` explicitly, since the engine will do so automatically when the generator yields - a list of ``YieldPoints``. + a list of ``YieldPoints`` or a mixture of ``YieldPoints`` and ``Futures``. + + Instead of a list, the argument may also be a dictionary whose values are + Futures, in which case a parallel dictionary is returned mapping the same + keys to their results. """ def __init__(self, children): self.keys = None @@ -426,7 +422,7 @@ class Multi(YieldPoint): children = children.values() self.children = [] for i in children: - if isinstance(i, Future): + if is_future(i): i = YieldFuture(i) self.children.append(i) assert all(isinstance(i, YieldPoint) for i in self.children) @@ -450,18 +446,127 @@ class Multi(YieldPoint): return list(result) -class _NullYieldPoint(YieldPoint): - def start(self, runner): - pass +def multi_future(children): + """Wait for multiple asynchronous futures in parallel. - def is_ready(self): - return True + Takes a list of ``Futures`` (but *not* other ``YieldPoints``) and returns + a new Future that resolves when all the other Futures are done. + If all the ``Futures`` succeeded, the returned Future's result is a list + of their results. If any failed, the returned Future raises the exception + of the first one to fail. - def get_result(self): - return None + Instead of a list, the argument may also be a dictionary whose values are + Futures, in which case a parallel dictionary is returned mapping the same + keys to their results. + + It is not necessary to call `multi_future` explcitly, since the engine will + do so automatically when the generator yields a list of `Futures`. + This function is faster than the `Multi` `YieldPoint` because it does not + require the creation of a stack context. + + .. versionadded:: 3.3 + """ + if isinstance(children, dict): + keys = list(children.keys()) + children = children.values() + else: + keys = None + assert all(is_future(i) for i in children) + unfinished_children = set(children) + + future = Future() + if not children: + future.set_result({} if keys is not None else []) + def callback(f): + unfinished_children.remove(f) + if not unfinished_children: + try: + result_list = [i.result() for i in children] + except Exception: + future.set_exc_info(sys.exc_info()) + else: + if keys is not None: + future.set_result(dict(zip(keys, result_list))) + else: + future.set_result(result_list) + for f in children: + f.add_done_callback(callback) + return future -_null_yield_point = _NullYieldPoint() +def maybe_future(x): + """Converts ``x`` into a `.Future`. + + If ``x`` is already a `.Future`, it is simply returned; otherwise + it is wrapped in a new `.Future`. This is suitable for use as + ``result = yield gen.maybe_future(f())`` when you don't know whether + ``f()`` returns a `.Future` or not. + """ + if is_future(x): + return x + else: + fut = Future() + fut.set_result(x) + return fut + + +def with_timeout(timeout, future, io_loop=None): + """Wraps a `.Future` in a timeout. + + Raises `TimeoutError` if the input future does not complete before + ``timeout``, which may be specified in any form allowed by + `.IOLoop.add_timeout` (i.e. a `datetime.timedelta` or an absolute time + relative to `.IOLoop.time`) + + Currently only supports Futures, not other `YieldPoint` classes. + + .. versionadded:: 3.3 + """ + # TODO: allow yield points in addition to futures? + # Tricky to do with stack_context semantics. + # + # It's tempting to optimize this by cancelling the input future on timeout + # instead of creating a new one, but A) we can't know if we are the only + # one waiting on the input future, so cancelling it might disrupt other + # callers and B) concurrent futures can only be cancelled while they are + # in the queue, so cancellation cannot reliably bound our waiting time. + result = Future() + chain_future(future, result) + if io_loop is None: + io_loop = IOLoop.current() + timeout_handle = io_loop.add_timeout( + timeout, + lambda: result.set_exception(TimeoutError("Timeout"))) + if isinstance(future, Future): + # We know this future will resolve on the IOLoop, so we don't + # need the extra thread-safety of IOLoop.add_future (and we also + # don't care about StackContext here. + future.add_done_callback( + lambda future: io_loop.remove_timeout(timeout_handle)) + else: + # concurrent.futures.Futures may resolve on any thread, so we + # need to route them back to the IOLoop. + io_loop.add_future( + future, lambda future: io_loop.remove_timeout(timeout_handle)) + return result + + +_null_future = Future() +_null_future.set_result(None) + +moment = Future() +moment.__doc__ = \ + """A special object which may be yielded to allow the IOLoop to run for +one iteration. + +This is not needed in normal use but it can be helpful in long-running +coroutines that are likely to yield Futures that are ready instantly. + +Usage: ``yield gen.moment`` + +.. versionadded:: 3.3 +""" +moment.set_result(None) class Runner(object): @@ -469,35 +574,55 @@ class Runner(object): Maintains information about pending callbacks and their results. - ``final_callback`` is run after the generator exits. + The results of the generator are stored in ``result_future`` (a + `.TracebackFuture`) """ - def __init__(self, gen, final_callback): + def __init__(self, gen, result_future, first_yielded): self.gen = gen - self.final_callback = final_callback - self.yield_point = _null_yield_point - self.pending_callbacks = set() - self.results = {} + self.result_future = result_future + self.future = _null_future + self.yield_point = None + self.pending_callbacks = None + self.results = None self.running = False self.finished = False - self.exc_info = None self.had_exception = False + self.io_loop = IOLoop.current() + # For efficiency, we do not create a stack context until we + # reach a YieldPoint (stack contexts are required for the historical + # semantics of YieldPoints, but not for Futures). When we have + # done so, this field will be set and must be called at the end + # of the coroutine. + self.stack_context_deactivate = None + if self.handle_yield(first_yielded): + self.run() def register_callback(self, key): """Adds ``key`` to the list of callbacks.""" + if self.pending_callbacks is None: + # Lazily initialize the old-style YieldPoint data structures. + self.pending_callbacks = set() + self.results = {} if key in self.pending_callbacks: raise KeyReuseError("key %r is already pending" % (key,)) self.pending_callbacks.add(key) def is_ready(self, key): """Returns true if a result is available for ``key``.""" - if key not in self.pending_callbacks: + if self.pending_callbacks is None or key not in self.pending_callbacks: raise UnknownKeyError("key %r is not pending" % (key,)) return key in self.results def set_result(self, key, result): """Sets the result for ``key`` and attempts to resume the generator.""" self.results[key] = result - self.run() + if self.yield_point is not None and self.yield_point.is_ready(): + try: + self.future.set_result(self.yield_point.get_result()) + except: + self.future.set_exc_info(sys.exc_info()) + self.yield_point = None + self.run() def pop_result(self, key): """Returns the result for ``key`` and unregisters it.""" @@ -513,25 +638,27 @@ class Runner(object): try: self.running = True while True: - if self.exc_info is None: - try: - if not self.yield_point.is_ready(): - return - next = self.yield_point.get_result() - self.yield_point = None - except Exception: - self.exc_info = sys.exc_info() + future = self.future + if not future.done(): + return + self.future = None try: - if self.exc_info is not None: + orig_stack_contexts = stack_context._state.contexts + try: + value = future.result() + except Exception: self.had_exception = True - exc_info = self.exc_info - self.exc_info = None - yielded = self.gen.throw(*exc_info) + yielded = self.gen.throw(*sys.exc_info()) else: - yielded = self.gen.send(next) + yielded = self.gen.send(value) + if stack_context._state.contexts is not orig_stack_contexts: + self.gen.throw( + stack_context.StackContextInconsistentError( + 'stack_context inconsistency (probably caused ' + 'by yield within a "with StackContext" block)')) except (StopIteration, Return) as e: self.finished = True - self.yield_point = _null_yield_point + self.future = _null_future if self.pending_callbacks and not self.had_exception: # If we ran cleanly without waiting on all callbacks # raise an error (really more of a warning). If we @@ -540,46 +667,105 @@ class Runner(object): raise LeakedCallbackError( "finished without waiting for callbacks %r" % self.pending_callbacks) - self.final_callback(getattr(e, 'value', None)) - self.final_callback = None + self.result_future.set_result(getattr(e, 'value', None)) + self.result_future = None + self._deactivate_stack_context() return except Exception: self.finished = True - self.yield_point = _null_yield_point - raise - if isinstance(yielded, (list, dict)): - yielded = Multi(yielded) - elif isinstance(yielded, Future): - yielded = YieldFuture(yielded) - if isinstance(yielded, YieldPoint): - self.yield_point = yielded - try: - self.yield_point.start(self) - except Exception: - self.exc_info = sys.exc_info() - else: - self.exc_info = (BadYieldError( - "yielded unknown object %r" % (yielded,)),) + self.future = _null_future + self.result_future.set_exc_info(sys.exc_info()) + self.result_future = None + self._deactivate_stack_context() + return + if not self.handle_yield(yielded): + return finally: self.running = False - def result_callback(self, key): - def inner(*args, **kwargs): - if kwargs or len(args) > 1: - result = Arguments(args, kwargs) - elif args: - result = args[0] + def handle_yield(self, yielded): + if isinstance(yielded, list): + if all(is_future(f) for f in yielded): + yielded = multi_future(yielded) else: - result = None - self.set_result(key, result) - return wrap(inner) + yielded = Multi(yielded) + elif isinstance(yielded, dict): + if all(is_future(f) for f in yielded.values()): + yielded = multi_future(yielded) + else: + yielded = Multi(yielded) + + if isinstance(yielded, YieldPoint): + self.future = TracebackFuture() + def start_yield_point(): + try: + yielded.start(self) + if yielded.is_ready(): + self.future.set_result( + yielded.get_result()) + else: + self.yield_point = yielded + except Exception: + self.future = TracebackFuture() + self.future.set_exc_info(sys.exc_info()) + if self.stack_context_deactivate is None: + # Start a stack context if this is the first + # YieldPoint we've seen. + with stack_context.ExceptionStackContext( + self.handle_exception) as deactivate: + self.stack_context_deactivate = deactivate + def cb(): + start_yield_point() + self.run() + self.io_loop.add_callback(cb) + return False + else: + start_yield_point() + elif is_future(yielded): + self.future = yielded + if not self.future.done() or self.future is moment: + self.io_loop.add_future( + self.future, lambda f: self.run()) + return False + else: + self.future = TracebackFuture() + self.future.set_exception(BadYieldError( + "yielded unknown object %r" % (yielded,))) + return True + + def result_callback(self, key): + return stack_context.wrap(_argument_adapter( + functools.partial(self.set_result, key))) def handle_exception(self, typ, value, tb): if not self.running and not self.finished: - self.exc_info = (typ, value, tb) + self.future = TracebackFuture() + self.future.set_exc_info((typ, value, tb)) self.run() return True else: return False + def _deactivate_stack_context(self): + if self.stack_context_deactivate is not None: + self.stack_context_deactivate() + self.stack_context_deactivate = None + Arguments = collections.namedtuple('Arguments', ['args', 'kwargs']) + + +def _argument_adapter(callback): + """Returns a function that when invoked runs ``callback`` with one arg. + + If the function returned by this function is called with exactly + one argument, that argument is passed to ``callback``. Otherwise + the args tuple and kwargs dict are wrapped in an `Arguments` object. + """ + def wrapper(*args, **kwargs): + if kwargs or len(args) > 1: + callback(Arguments(args, kwargs)) + elif args: + callback(args[0]) + else: + callback(None) + return wrapper diff --git a/libs/tornado/http1connection.py b/libs/tornado/http1connection.py new file mode 100644 index 00000000..9f474e1b --- /dev/null +++ b/libs/tornado/http1connection.py @@ -0,0 +1,624 @@ +#!/usr/bin/env python +# +# Copyright 2014 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Client and server implementations of HTTP/1.x. + +.. versionadded:: 3.3 +""" + +from __future__ import absolute_import, division, print_function, with_statement + +from tornado.concurrent import Future +from tornado.escape import native_str, utf8 +from tornado import gen +from tornado import httputil +from tornado import iostream +from tornado.log import gen_log, app_log +from tornado import stack_context +from tornado.util import GzipDecompressor + + +class HTTP1ConnectionParameters(object): + """Parameters for `.HTTP1Connection` and `.HTTP1ServerConnection`. + """ + def __init__(self, no_keep_alive=False, chunk_size=None, + max_header_size=None, header_timeout=None, max_body_size=None, + body_timeout=None, use_gzip=False): + """ + :arg bool no_keep_alive: If true, always close the connection after + one request. + :arg int chunk_size: how much data to read into memory at once + :arg int max_header_size: maximum amount of data for HTTP headers + :arg float header_timeout: how long to wait for all headers (seconds) + :arg int max_body_size: maximum amount of data for body + :arg float body_timeout: how long to wait while reading body (seconds) + :arg bool use_gzip: if true, decode incoming ``Content-Encoding: gzip`` + """ + self.no_keep_alive = no_keep_alive + self.chunk_size = chunk_size or 65536 + self.max_header_size = max_header_size or 65536 + self.header_timeout = header_timeout + self.max_body_size = max_body_size + self.body_timeout = body_timeout + self.use_gzip = use_gzip + + +class HTTP1Connection(httputil.HTTPConnection): + """Implements the HTTP/1.x protocol. + + This class can be on its own for clients, or via `HTTP1ServerConnection` + for servers. + """ + def __init__(self, stream, is_client, params=None, context=None): + """ + :arg stream: an `.IOStream` + :arg bool is_client: client or server + :arg params: a `.HTTP1ConnectionParameters` instance or ``None`` + :arg context: an opaque application-defined object that can be accessed + as ``connection.context``. + """ + self.is_client = is_client + self.stream = stream + if params is None: + params = HTTP1ConnectionParameters() + self.params = params + self.context = context + self.no_keep_alive = params.no_keep_alive + # The body limits can be altered by the delegate, so save them + # here instead of just referencing self.params later. + self._max_body_size = (self.params.max_body_size or + self.stream.max_buffer_size) + self._body_timeout = self.params.body_timeout + # _write_finished is set to True when finish() has been called, + # i.e. there will be no more data sent. Data may still be in the + # stream's write buffer. + self._write_finished = False + # True when we have read the entire incoming body. + self._read_finished = False + # _finish_future resolves when all data has been written and flushed + # to the IOStream. + self._finish_future = Future() + # If true, the connection should be closed after this request + # (after the response has been written in the server side, + # and after it has been read in the client) + self._disconnect_on_finish = False + self._clear_callbacks() + # Save the start lines after we read or write them; they + # affect later processing (e.g. 304 responses and HEAD methods + # have content-length but no bodies) + self._request_start_line = None + self._response_start_line = None + self._request_headers = None + # True if we are writing output with chunked encoding. + self._chunking_output = None + # While reading a body with a content-length, this is the + # amount left to read. + self._expected_content_remaining = None + # A Future for our outgoing writes, returned by IOStream.write. + self._pending_write = None + + def read_response(self, delegate): + """Read a single HTTP response. + + Typical client-mode usage is to write a request using `write_headers`, + `write`, and `finish`, and then call ``read_response``. + + :arg delegate: a `.HTTPMessageDelegate` + + Returns a `.Future` that resolves to None after the full response has + been read. + """ + if self.params.use_gzip: + delegate = _GzipMessageDelegate(delegate, self.params.chunk_size) + return self._read_message(delegate) + + @gen.coroutine + def _read_message(self, delegate): + need_delegate_close = False + try: + header_future = self.stream.read_until_regex( + b"\r?\n\r?\n", + max_bytes=self.params.max_header_size) + if self.params.header_timeout is None: + header_data = yield header_future + else: + try: + header_data = yield gen.with_timeout( + self.stream.io_loop.time() + self.params.header_timeout, + header_future, + io_loop=self.stream.io_loop) + except gen.TimeoutError: + self.close() + raise gen.Return(False) + start_line, headers = self._parse_headers(header_data) + if self.is_client: + start_line = httputil.parse_response_start_line(start_line) + self._response_start_line = start_line + else: + start_line = httputil.parse_request_start_line(start_line) + self._request_start_line = start_line + self._request_headers = headers + + self._disconnect_on_finish = not self._can_keep_alive( + start_line, headers) + need_delegate_close = True + header_future = delegate.headers_received(start_line, headers) + if header_future is not None: + yield header_future + if self.stream is None: + # We've been detached. + need_delegate_close = False + raise gen.Return(False) + skip_body = False + if self.is_client: + if (self._request_start_line is not None and + self._request_start_line.method == 'HEAD'): + skip_body = True + code = start_line.code + if code == 304: + skip_body = True + if code >= 100 and code < 200: + # TODO: client delegates will get headers_received twice + # in the case of a 100-continue. Document or change? + yield self._read_message(delegate) + else: + if (headers.get("Expect") == "100-continue" and + not self._write_finished): + self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n") + if not skip_body: + body_future = self._read_body(headers, delegate) + if body_future is not None: + if self._body_timeout is None: + yield body_future + else: + try: + yield gen.with_timeout( + self.stream.io_loop.time() + self._body_timeout, + body_future, self.stream.io_loop) + except gen.TimeoutError: + gen_log.info("Timeout reading body from %s", + self.context) + self.stream.close() + raise gen.Return(False) + self._read_finished = True + if not self._write_finished or self.is_client: + need_delegate_close = False + delegate.finish() + # If we're waiting for the application to produce an asynchronous + # response, and we're not detached, register a close callback + # on the stream (we didn't need one while we were reading) + if (not self._finish_future.done() and + self.stream is not None and + not self.stream.closed()): + self.stream.set_close_callback(self._on_connection_close) + yield self._finish_future + if self.is_client and self._disconnect_on_finish: + self.close() + if self.stream is None: + raise gen.Return(False) + except httputil.HTTPInputException as e: + gen_log.info("Malformed HTTP message from %s: %s", + self.context, e) + self.close() + raise gen.Return(False) + finally: + if need_delegate_close: + delegate.on_connection_close() + self._clear_callbacks() + raise gen.Return(True) + + def _clear_callbacks(self): + """Clears the callback attributes. + + This allows the request handler to be garbage collected more + quickly in CPython by breaking up reference cycles. + """ + self._write_callback = None + self._write_future = None + self._close_callback = None + if self.stream is not None: + self.stream.set_close_callback(None) + + def set_close_callback(self, callback): + """Sets a callback that will be run when the connection is closed. + + .. deprecated:: 3.3 + Use `.HTTPMessageDelegate.on_connection_close` instead. + """ + self._close_callback = stack_context.wrap(callback) + + def _on_connection_close(self): + # Note that this callback is only registered on the IOStream + # when we have finished reading the request and are waiting for + # the application to produce its response. + if self._close_callback is not None: + callback = self._close_callback + self._close_callback = None + callback() + if not self._finish_future.done(): + self._finish_future.set_result(None) + self._clear_callbacks() + + def close(self): + if self.stream is not None: + self.stream.close() + self._clear_callbacks() + if not self._finish_future.done(): + self._finish_future.set_result(None) + + def detach(self): + """Take control of the underlying stream. + + Returns the underlying `.IOStream` object and stops all further + HTTP processing. May only be called during + `.HTTPMessageDelegate.headers_received`. Intended for implementing + protocols like websockets that tunnel over an HTTP handshake. + """ + self._clear_callbacks() + stream = self.stream + self.stream = None + return stream + + def set_body_timeout(self, timeout): + """Sets the body timeout for a single request. + + Overrides the value from `.HTTP1ConnectionParameters`. + """ + self._body_timeout = timeout + + def set_max_body_size(self, max_body_size): + """Sets the body size limit for a single request. + + Overrides the value from `.HTTP1ConnectionParameters`. + """ + self._max_body_size = max_body_size + + def write_headers(self, start_line, headers, chunk=None, callback=None): + """Implements `.HTTPConnection.write_headers`.""" + if self.is_client: + self._request_start_line = start_line + # Client requests with a non-empty body must have either a + # Content-Length or a Transfer-Encoding. + self._chunking_output = ( + start_line.method in ('POST', 'PUT', 'PATCH') and + 'Content-Length' not in headers and + 'Transfer-Encoding' not in headers) + else: + self._response_start_line = start_line + self._chunking_output = ( + # TODO: should this use + # self._request_start_line.version or + # start_line.version? + self._request_start_line.version == 'HTTP/1.1' and + # 304 responses have no body (not even a zero-length body), and so + # should not have either Content-Length or Transfer-Encoding. + # headers. + start_line.code != 304 and + # No need to chunk the output if a Content-Length is specified. + 'Content-Length' not in headers and + # Applications are discouraged from touching Transfer-Encoding, + # but if they do, leave it alone. + 'Transfer-Encoding' not in headers) + # If a 1.0 client asked for keep-alive, add the header. + if (self._request_start_line.version == 'HTTP/1.0' and + (self._request_headers.get('Connection', '').lower() + == 'keep-alive')): + headers['Connection'] = 'Keep-Alive' + if self._chunking_output: + headers['Transfer-Encoding'] = 'chunked' + if (not self.is_client and + (self._request_start_line.method == 'HEAD' or + start_line.code == 304)): + self._expected_content_remaining = 0 + elif 'Content-Length' in headers: + self._expected_content_remaining = int(headers['Content-Length']) + else: + self._expected_content_remaining = None + lines = [utf8("%s %s %s" % start_line)] + lines.extend([utf8(n) + b": " + utf8(v) for n, v in headers.get_all()]) + for line in lines: + if b'\n' in line: + raise ValueError('Newline in header: ' + repr(line)) + future = None + if self.stream.closed(): + future = self._write_future = Future() + future.set_exception(iostream.StreamClosedError()) + else: + if callback is not None: + self._write_callback = stack_context.wrap(callback) + else: + future = self._write_future = Future() + data = b"\r\n".join(lines) + b"\r\n\r\n" + if chunk: + data += self._format_chunk(chunk) + self._pending_write = self.stream.write(data) + self._pending_write.add_done_callback(self._on_write_complete) + return future + + def _format_chunk(self, chunk): + if self._expected_content_remaining is not None: + self._expected_content_remaining -= len(chunk) + if self._expected_content_remaining < 0: + # Close the stream now to stop further framing errors. + self.stream.close() + raise httputil.HTTPOutputException( + "Tried to write more data than Content-Length") + if self._chunking_output and chunk: + # Don't write out empty chunks because that means END-OF-STREAM + # with chunked encoding + return utf8("%x" % len(chunk)) + b"\r\n" + chunk + b"\r\n" + else: + return chunk + + def write(self, chunk, callback=None): + """Implements `.HTTPConnection.write`. + + For backwards compatibility is is allowed but deprecated to + skip `write_headers` and instead call `write()` with a + pre-encoded header block. + """ + future = None + if self.stream.closed(): + future = self._write_future = Future() + self._write_future.set_exception(iostream.StreamClosedError()) + else: + if callback is not None: + self._write_callback = stack_context.wrap(callback) + else: + future = self._write_future = Future() + self._pending_write = self.stream.write(self._format_chunk(chunk)) + self._pending_write.add_done_callback(self._on_write_complete) + return future + + def finish(self): + """Implements `.HTTPConnection.finish`.""" + if (self._expected_content_remaining is not None and + self._expected_content_remaining != 0 and + not self.stream.closed()): + self.stream.close() + raise httputil.HTTPOutputException( + "Tried to write %d bytes less than Content-Length" % + self._expected_content_remaining) + if self._chunking_output: + if not self.stream.closed(): + self._pending_write = self.stream.write(b"0\r\n\r\n") + self._pending_write.add_done_callback(self._on_write_complete) + self._write_finished = True + # If the app finished the request while we're still reading, + # divert any remaining data away from the delegate and + # close the connection when we're done sending our response. + # Closing the connection is the only way to avoid reading the + # whole input body. + if not self._read_finished: + self._disconnect_on_finish = True + # No more data is coming, so instruct TCP to send any remaining + # data immediately instead of waiting for a full packet or ack. + self.stream.set_nodelay(True) + if self._pending_write is None: + self._finish_request(None) + else: + self._pending_write.add_done_callback(self._finish_request) + + def _on_write_complete(self, future): + if self._write_callback is not None: + callback = self._write_callback + self._write_callback = None + self.stream.io_loop.add_callback(callback) + if self._write_future is not None: + future = self._write_future + self._write_future = None + future.set_result(None) + + def _can_keep_alive(self, start_line, headers): + if self.params.no_keep_alive: + return False + connection_header = headers.get("Connection") + if connection_header is not None: + connection_header = connection_header.lower() + if start_line.version == "HTTP/1.1": + return connection_header != "close" + elif ("Content-Length" in headers + or start_line.method in ("HEAD", "GET")): + return connection_header == "keep-alive" + return False + + def _finish_request(self, future): + self._clear_callbacks() + if not self.is_client and self._disconnect_on_finish: + self.close() + return + # Turn Nagle's algorithm back on, leaving the stream in its + # default state for the next request. + self.stream.set_nodelay(False) + if not self._finish_future.done(): + self._finish_future.set_result(None) + + def _parse_headers(self, data): + data = native_str(data.decode('latin1')) + eol = data.find("\r\n") + start_line = data[:eol] + try: + headers = httputil.HTTPHeaders.parse(data[eol:]) + except ValueError: + # probably form split() if there was no ':' in the line + raise httputil.HTTPInputException("Malformed HTTP headers: %r" % + data[eol:100]) + return start_line, headers + + def _read_body(self, headers, delegate): + content_length = headers.get("Content-Length") + if content_length: + content_length = int(content_length) + if content_length > self._max_body_size: + raise httputil.HTTPInputException("Content-Length too long") + return self._read_fixed_body(content_length, delegate) + if headers.get("Transfer-Encoding") == "chunked": + return self._read_chunked_body(delegate) + if self.is_client: + return self._read_body_until_close(delegate) + return None + + @gen.coroutine + def _read_fixed_body(self, content_length, delegate): + while content_length > 0: + body = yield self.stream.read_bytes( + min(self.params.chunk_size, content_length), partial=True) + content_length -= len(body) + if not self._write_finished or self.is_client: + yield gen.maybe_future(delegate.data_received(body)) + + @gen.coroutine + def _read_chunked_body(self, delegate): + # TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1 + total_size = 0 + while True: + chunk_len = yield self.stream.read_until(b"\r\n", max_bytes=64) + chunk_len = int(chunk_len.strip(), 16) + if chunk_len == 0: + return + total_size += chunk_len + if total_size > self._max_body_size: + raise httputil.HTTPInputException("chunked body too large") + bytes_to_read = chunk_len + while bytes_to_read: + chunk = yield self.stream.read_bytes( + min(bytes_to_read, self.params.chunk_size), partial=True) + bytes_to_read -= len(chunk) + if not self._write_finished or self.is_client: + yield gen.maybe_future( + delegate.data_received(chunk)) + # chunk ends with \r\n + crlf = yield self.stream.read_bytes(2) + assert crlf == b"\r\n" + + @gen.coroutine + def _read_body_until_close(self, delegate): + body = yield self.stream.read_until_close() + if not self._write_finished or self.is_client: + delegate.data_received(body) + + +class _GzipMessageDelegate(httputil.HTTPMessageDelegate): + """Wraps an `HTTPMessageDelegate` to decode ``Content-Encoding: gzip``. + """ + def __init__(self, delegate, chunk_size): + self._delegate = delegate + self._chunk_size = chunk_size + self._decompressor = None + + def headers_received(self, start_line, headers): + if headers.get("Content-Encoding") == "gzip": + self._decompressor = GzipDecompressor() + # Downstream delegates will only see uncompressed data, + # so rename the content-encoding header. + # (but note that curl_httpclient doesn't do this). + headers.add("X-Consumed-Content-Encoding", + headers["Content-Encoding"]) + del headers["Content-Encoding"] + return self._delegate.headers_received(start_line, headers) + + @gen.coroutine + def data_received(self, chunk): + if self._decompressor: + compressed_data = chunk + while compressed_data: + decompressed = self._decompressor.decompress( + compressed_data, self._chunk_size) + if decompressed: + yield gen.maybe_future( + self._delegate.data_received(decompressed)) + compressed_data = self._decompressor.unconsumed_tail + else: + yield gen.maybe_future(self._delegate.data_received(chunk)) + + def finish(self): + if self._decompressor is not None: + tail = self._decompressor.flush() + if tail: + # I believe the tail will always be empty (i.e. + # decompress will return all it can). The purpose + # of the flush call is to detect errors such + # as truncated input. But in case it ever returns + # anything, treat it as an extra chunk + self._delegate.data_received(tail) + return self._delegate.finish() + + +class HTTP1ServerConnection(object): + """An HTTP/1.x server.""" + def __init__(self, stream, params=None, context=None): + """ + :arg stream: an `.IOStream` + :arg params: a `.HTTP1ConnectionParameters` or None + :arg context: an opaque application-defined object that is accessible + as ``connection.context`` + """ + self.stream = stream + if params is None: + params = HTTP1ConnectionParameters() + self.params = params + self.context = context + self._serving_future = None + + @gen.coroutine + def close(self): + """Closes the connection. + + Returns a `.Future` that resolves after the serving loop has exited. + """ + self.stream.close() + # Block until the serving loop is done, but ignore any exceptions + # (start_serving is already responsible for logging them). + try: + yield self._serving_future + except Exception: + pass + + def start_serving(self, delegate): + """Starts serving requests on this connection. + + :arg delegate: a `.HTTPServerConnectionDelegate` + """ + assert isinstance(delegate, httputil.HTTPServerConnectionDelegate) + self._serving_future = self._server_request_loop(delegate) + # Register the future on the IOLoop so its errors get logged. + self.stream.io_loop.add_future(self._serving_future, + lambda f: f.result()) + + @gen.coroutine + def _server_request_loop(self, delegate): + try: + while True: + conn = HTTP1Connection(self.stream, False, + self.params, self.context) + request_delegate = delegate.start_request(self, conn) + try: + ret = yield conn.read_response(request_delegate) + except (iostream.StreamClosedError, + iostream.UnsatisfiableReadError): + return + except Exception: + # TODO: this is probably too broad; it would be better to + # wrap all delegate calls in something that writes to app_log, + # and then errors that reach this point can be gen_log. + app_log.error("Uncaught exception", exc_info=True) + conn.close() + return + if not ret: + return + yield gen.moment + finally: + delegate.on_close(self) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 9b42d401..94a4593a 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -25,6 +25,11 @@ to switch to ``curl_httpclient`` for reasons such as the following: Note that if you are using ``curl_httpclient``, it is highly recommended that you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum supported version is 7.18.2, and the recommended version is 7.21.1 or newer. +It is highly recommended that your ``libcurl`` installation is built with +asynchronous DNS resolver (threaded or c-ares), otherwise you may encounter +various problems with request timeouts (for more information, see +http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCONNECTTIMEOUTMS +and comments in curl_httpclient.py). """ from __future__ import absolute_import, division, print_function, with_statement @@ -34,7 +39,7 @@ import time import weakref from tornado.concurrent import TracebackFuture -from tornado.escape import utf8 +from tornado.escape import utf8, native_str from tornado import httputil, stack_context from tornado.ioloop import IOLoop from tornado.util import Configurable @@ -166,7 +171,7 @@ class AsyncHTTPClient(Configurable): kwargs: ``HTTPRequest(request, **kwargs)`` This method returns a `.Future` whose result is an - `HTTPResponse`. The ``Future`` wil raise an `HTTPError` if + `HTTPResponse`. The ``Future`` will raise an `HTTPError` if the request returned a non-200 response code. If a ``callback`` is given, it will be invoked with the `HTTPResponse`. @@ -259,14 +264,27 @@ class HTTPRequest(object): proxy_password=None, allow_nonstandard_methods=None, validate_cert=None, ca_certs=None, allow_ipv6=None, - client_key=None, client_cert=None): + client_key=None, client_cert=None, body_producer=None, + expect_100_continue=False): r"""All parameters except ``url`` are optional. :arg string url: URL to fetch :arg string method: HTTP method, e.g. "GET" or "POST" :arg headers: Additional HTTP headers to pass on the request - :arg body: HTTP body to pass on the request :type headers: `~tornado.httputil.HTTPHeaders` or `dict` + :arg body: HTTP request body as a string (byte or unicode; if unicode + the utf-8 encoding will be used) + :arg body_producer: Callable used for lazy/asynchronous request bodies. + It is called with one argument, a ``write`` function, and should + return a `.Future`. It should call the write function with new + data as it becomes available. The write function returns a + `.Future` which can be used for flow control. + Only one of ``body`` and ``body_producer`` may + be specified. ``body_producer`` is not supported on + ``curl_httpclient``. When using ``body_producer`` it is recommended + to pass a ``Content-Length`` in the headers as otherwise chunked + encoding will be used, and many servers do not support chunked + encoding on requests. New in Tornado 3.3 :arg string auth_username: Username for HTTP authentication :arg string auth_password: Password for HTTP authentication :arg string auth_mode: Authentication mode; default is "basic". @@ -319,6 +337,11 @@ class HTTPRequest(object): note below when used with ``curl_httpclient``. :arg string client_cert: Filename for client SSL certificate, if any. See note below when used with ``curl_httpclient``. + :arg bool expect_100_continue: If true, send the + ``Expect: 100-continue`` header and wait for a continue response + before sending the request body. Only supported with + simple_httpclient. + .. note:: @@ -334,6 +357,9 @@ class HTTPRequest(object): .. versionadded:: 3.1 The ``auth_mode`` argument. + + .. versionadded:: 3.3 + The ``body_producer`` and ``expect_100_continue`` arguments. """ # Note that some of these attributes go through property setters # defined below. @@ -348,6 +374,7 @@ class HTTPRequest(object): self.url = url self.method = method self.body = body + self.body_producer = body_producer self.auth_username = auth_username self.auth_password = auth_password self.auth_mode = auth_mode @@ -367,6 +394,7 @@ class HTTPRequest(object): self.allow_ipv6 = allow_ipv6 self.client_key = client_key self.client_cert = client_cert + self.expect_100_continue = expect_100_continue self.start_time = time.time() @property @@ -388,6 +416,14 @@ class HTTPRequest(object): def body(self, value): self._body = utf8(value) + @property + def body_producer(self): + return self._body_producer + + @body_producer.setter + def body_producer(self, value): + self._body_producer = stack_context.wrap(value) + @property def streaming_callback(self): return self._streaming_callback @@ -423,8 +459,6 @@ class HTTPResponse(object): * code: numeric HTTP status code, e.g. 200 or 404 * reason: human-readable reason phrase describing the status code - (with curl_httpclient, this is a default value rather than the - server's actual response) * headers: `tornado.httputil.HTTPHeaders` object @@ -466,7 +500,8 @@ class HTTPResponse(object): self.effective_url = effective_url if error is None: if self.code < 200 or self.code >= 300: - self.error = HTTPError(self.code, response=self) + self.error = HTTPError(self.code, message=self.reason, + response=self) else: self.error = None else: @@ -556,7 +591,7 @@ def main(): if options.print_headers: print(response.headers) if options.print_body: - print(response.body) + print(native_str(response.body)) client.close() if __name__ == "__main__": diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index 34e7b768..469374e1 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -20,70 +20,55 @@ Typical applications have little direct interaction with the `HTTPServer` class except to start a server at the beginning of the process (and even that is often done indirectly via `tornado.web.Application.listen`). -This module also defines the `HTTPRequest` class which is exposed via -`tornado.web.RequestHandler.request`. +.. versionchanged:: 3.3 + + The ``HTTPRequest`` class that used to live in this module has been moved + to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias. """ from __future__ import absolute_import, division, print_function, with_statement import socket -import ssl -import time -import copy -from tornado.escape import native_str, parse_qs_bytes +from tornado.escape import native_str +from tornado.http1connection import HTTP1ServerConnection, HTTP1ConnectionParameters +from tornado import gen from tornado import httputil from tornado import iostream -from tornado.log import gen_log from tornado import netutil from tornado.tcpserver import TCPServer -from tornado import stack_context -from tornado.util import bytes_type - -try: - import Cookie # py2 -except ImportError: - import http.cookies as Cookie # py3 -class HTTPServer(TCPServer): +class HTTPServer(TCPServer, httputil.HTTPServerConnectionDelegate): r"""A non-blocking, single-threaded HTTP server. - A server is defined by a request callback that takes an HTTPRequest - instance as an argument and writes a valid HTTP response with - `HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does - not necessarily close the connection in the case of HTTP/1.1 keep-alive - requests). A simple example server that echoes back the URI you - requested:: + A server is defined by either a request callback that takes a + `.HTTPServerRequest` as an argument or a `.HTTPServerConnectionDelegate` + instance. + + A simple example server that echoes back the URI you requested:: import tornado.httpserver import tornado.ioloop def handle_request(request): message = "You requested %s\n" % request.uri - request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % ( - len(message), message)) - request.finish() + request.connection.write_headers( + httputil.ResponseStartLine('HTTP/1.1', 200, 'OK'), + {"Content-Length": str(len(message))}) + request.connection.write(message) + request.connection.finish() http_server = tornado.httpserver.HTTPServer(handle_request) http_server.listen(8888) tornado.ioloop.IOLoop.instance().start() - `HTTPServer` is a very basic connection handler. It parses the request - headers and body, but the request callback is responsible for producing - the response exactly as it will appear on the wire. This affords - maximum flexibility for applications to implement whatever parts - of HTTP responses are required. + Applications should use the methods of `.HTTPConnection` to write + their response. `HTTPServer` supports keep-alive connections by default (automatically for HTTP/1.1, or for HTTP/1.0 when the client - requests ``Connection: keep-alive``). This means that the request - callback must generate a properly-framed response, using either - the ``Content-Length`` header or ``Transfer-Encoding: chunked``. - Applications that are unable to frame their responses properly - should instead return a ``Connection: close`` header in each - response and pass ``no_keep_alive=True`` to the `HTTPServer` - constructor. + requests ``Connection: keep-alive``). If ``xheaders`` is ``True``, we support the ``X-Real-Ip``/``X-Forwarded-For`` and @@ -143,407 +128,169 @@ class HTTPServer(TCPServer): servers if you want to create your listening sockets in some way other than `tornado.netutil.bind_sockets`. + .. versionchanged:: 3.3 + Added ``gzip``, ``chunk_size``, ``max_header_size``, + ``idle_connection_timeout``, ``body_timeout``, ``max_body_size`` + arguments. Added support for `.HTTPServerConnectionDelegate` + instances as ``request_callback``. """ def __init__(self, request_callback, no_keep_alive=False, io_loop=None, - xheaders=False, ssl_options=None, protocol=None, **kwargs): + xheaders=False, ssl_options=None, protocol=None, gzip=False, + chunk_size=None, max_header_size=None, + idle_connection_timeout=None, body_timeout=None, + max_body_size=None, max_buffer_size=None): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol + self.conn_params = HTTP1ConnectionParameters( + use_gzip=gzip, + chunk_size=chunk_size, + max_header_size=max_header_size, + header_timeout=idle_connection_timeout or 3600, + max_body_size=max_body_size, + body_timeout=body_timeout) TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, - **kwargs) + max_buffer_size=max_buffer_size, + read_chunk_size=chunk_size) + self._connections = set() + + @gen.coroutine + def close_all_connections(self): + while self._connections: + # Peek at an arbitrary element of the set + conn = next(iter(self._connections)) + yield conn.close() def handle_stream(self, stream, address): - HTTPConnection(stream, address, self.request_callback, - self.no_keep_alive, self.xheaders, self.protocol) + context = _HTTPRequestContext(stream, address, + self.protocol) + conn = HTTP1ServerConnection( + stream, self.conn_params, context) + self._connections.add(conn) + conn.start_serving(self) + + def start_request(self, server_conn, request_conn): + return _ServerRequestAdapter(self, request_conn) + + def on_close(self, server_conn): + self._connections.remove(server_conn) -class _BadRequestException(Exception): - """Exception class for malformed HTTP requests.""" - pass - - -class HTTPConnection(object): - """Handles a connection to an HTTP client, executing HTTP requests. - - We parse HTTP headers and bodies, and execute the request callback - until the HTTP conection is closed. - """ - def __init__(self, stream, address, request_callback, no_keep_alive=False, - xheaders=False, protocol=None): - self.stream = stream +class _HTTPRequestContext(object): + def __init__(self, stream, address, protocol): self.address = address + self.protocol = protocol # Save the socket's address family now so we know how to # interpret self.address even after the stream is closed # and its socket attribute replaced with None. - self.address_family = stream.socket.family - self.request_callback = request_callback - self.no_keep_alive = no_keep_alive - self.xheaders = xheaders - self.protocol = protocol - self._clear_request_state() - # Save stack context here, outside of any request. This keeps - # contexts from one request from leaking into the next. - self._header_callback = stack_context.wrap(self._on_headers) - self.stream.set_close_callback(self._on_connection_close) - self.stream.read_until(b"\r\n\r\n", self._header_callback) - - def _clear_request_state(self): - """Clears the per-request state. - - This is run in between requests to allow the previous handler - to be garbage collected (and prevent spurious close callbacks), - and when the connection is closed (to break up cycles and - facilitate garbage collection in cpython). - """ - self._request = None - self._request_finished = False - self._write_callback = None - self._close_callback = None - - def set_close_callback(self, callback): - """Sets a callback that will be run when the connection is closed. - - Use this instead of accessing - `HTTPConnection.stream.set_close_callback - <.BaseIOStream.set_close_callback>` directly (which was the - recommended approach prior to Tornado 3.0). - """ - self._close_callback = stack_context.wrap(callback) - - def _on_connection_close(self): - if self._close_callback is not None: - callback = self._close_callback - self._close_callback = None - callback() - # Delete any unfinished callbacks to break up reference cycles. - self._header_callback = None - self._clear_request_state() - - def close(self): - self.stream.close() - # Remove this reference to self, which would otherwise cause a - # cycle and delay garbage collection of this connection. - self._header_callback = None - self._clear_request_state() - - def write(self, chunk, callback=None): - """Writes a chunk of output to the stream.""" - if not self.stream.closed(): - self._write_callback = stack_context.wrap(callback) - self.stream.write(chunk, self._on_write_complete) - - def finish(self): - """Finishes the request.""" - self._request_finished = True - # No more data is coming, so instruct TCP to send any remaining - # data immediately instead of waiting for a full packet or ack. - self.stream.set_nodelay(True) - if not self.stream.writing(): - self._finish_request() - - def _on_write_complete(self): - if self._write_callback is not None: - callback = self._write_callback - self._write_callback = None - callback() - # _on_write_complete is enqueued on the IOLoop whenever the - # IOStream's write buffer becomes empty, but it's possible for - # another callback that runs on the IOLoop before it to - # simultaneously write more data and finish the request. If - # there is still data in the IOStream, a future - # _on_write_complete will be responsible for calling - # _finish_request. - if self._request_finished and not self.stream.writing(): - self._finish_request() - - def _finish_request(self): - if self.no_keep_alive or self._request is None: - disconnect = True + if stream.socket is not None: + self.address_family = stream.socket.family else: - connection_header = self._request.headers.get("Connection") - if connection_header is not None: - connection_header = connection_header.lower() - if self._request.supports_http_1_1(): - disconnect = connection_header == "close" - elif ("Content-Length" in self._request.headers - or self._request.method in ("HEAD", "GET")): - disconnect = connection_header != "keep-alive" - else: - disconnect = True - self._clear_request_state() - if disconnect: - self.close() - return - try: - # Use a try/except instead of checking stream.closed() - # directly, because in some cases the stream doesn't discover - # that it's closed until you try to read from it. - self.stream.read_until(b"\r\n\r\n", self._header_callback) - - # Turn Nagle's algorithm back on, leaving the stream in its - # default state for the next request. - self.stream.set_nodelay(False) - except iostream.StreamClosedError: - self.close() - - def _on_headers(self, data): - try: - data = native_str(data.decode('latin1')) - eol = data.find("\r\n") - start_line = data[:eol] - try: - method, uri, version = start_line.split(" ") - except ValueError: - raise _BadRequestException("Malformed HTTP request line") - if not version.startswith("HTTP/"): - raise _BadRequestException("Malformed HTTP version in HTTP Request-Line") - try: - headers = httputil.HTTPHeaders.parse(data[eol:]) - except ValueError: - # Probably from split() if there was no ':' in the line - raise _BadRequestException("Malformed HTTP headers") - - # HTTPRequest wants an IP, not a full socket address - if self.address_family in (socket.AF_INET, socket.AF_INET6): - remote_ip = self.address[0] - else: - # Unix (or other) socket; fake the remote address - remote_ip = '0.0.0.0' - - self._request = HTTPRequest( - connection=self, method=method, uri=uri, version=version, - headers=headers, remote_ip=remote_ip, protocol=self.protocol) - - content_length = headers.get("Content-Length") - if content_length: - content_length = int(content_length) - if content_length > self.stream.max_buffer_size: - raise _BadRequestException("Content-Length too long") - if headers.get("Expect") == "100-continue": - self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n") - self.stream.read_bytes(content_length, self._on_request_body) - return - - self.request_callback(self._request) - except _BadRequestException as e: - gen_log.info("Malformed HTTP request from %r: %s", - self.address, e) - self.close() - return - - def _on_request_body(self, data): - self._request.body = data - if self._request.method in ("POST", "PATCH", "PUT"): - httputil.parse_body_arguments( - self._request.headers.get("Content-Type", ""), data, - self._request.body_arguments, self._request.files) - - for k, v in self._request.body_arguments.items(): - self._request.arguments.setdefault(k, []).extend(v) - self.request_callback(self._request) - - -class HTTPRequest(object): - """A single HTTP request. - - All attributes are type `str` unless otherwise noted. - - .. attribute:: method - - HTTP request method, e.g. "GET" or "POST" - - .. attribute:: uri - - The requested uri. - - .. attribute:: path - - The path portion of `uri` - - .. attribute:: query - - The query portion of `uri` - - .. attribute:: version - - HTTP version specified in request, e.g. "HTTP/1.1" - - .. attribute:: headers - - `.HTTPHeaders` dictionary-like object for request headers. Acts like - a case-insensitive dictionary with additional methods for repeated - headers. - - .. attribute:: body - - Request body, if present, as a byte string. - - .. attribute:: remote_ip - - Client's IP address as a string. If ``HTTPServer.xheaders`` is set, - will pass along the real IP address provided by a load balancer - in the ``X-Real-Ip`` or ``X-Forwarded-For`` header. - - .. versionchanged:: 3.1 - The list format of ``X-Forwarded-For`` is now supported. - - .. attribute:: protocol - - The protocol used, either "http" or "https". If ``HTTPServer.xheaders`` - is set, will pass along the protocol used by a load balancer if - reported via an ``X-Scheme`` header. - - .. attribute:: host - - The requested hostname, usually taken from the ``Host`` header. - - .. attribute:: arguments - - GET/POST arguments are available in the arguments property, which - maps arguments names to lists of values (to support multiple values - for individual names). Names are of type `str`, while arguments - are byte strings. Note that this is different from - `.RequestHandler.get_argument`, which returns argument values as - unicode strings. - - .. attribute:: query_arguments - - Same format as ``arguments``, but contains only arguments extracted - from the query string. - - .. versionadded:: 3.2 - - .. attribute:: body_arguments - - Same format as ``arguments``, but contains only arguments extracted - from the request body. - - .. versionadded:: 3.2 - - .. attribute:: files - - File uploads are available in the files property, which maps file - names to lists of `.HTTPFile`. - - .. attribute:: connection - - An HTTP request is attached to a single HTTP connection, which can - be accessed through the "connection" attribute. Since connections - are typically kept open in HTTP/1.1, multiple requests can be handled - sequentially on a single connection. - """ - def __init__(self, method, uri, version="HTTP/1.0", headers=None, - body=None, remote_ip=None, protocol=None, host=None, - files=None, connection=None): - self.method = method - self.uri = uri - self.version = version - self.headers = headers or httputil.HTTPHeaders() - self.body = body or "" - - # set remote IP and protocol - self.remote_ip = remote_ip + self.address_family = None + # In HTTPServerRequest we want an IP, not a full socket address. + if (self.address_family in (socket.AF_INET, socket.AF_INET6) and + address is not None): + self.remote_ip = address[0] + else: + # Unix (or other) socket; fake the remote address. + self.remote_ip = '0.0.0.0' if protocol: self.protocol = protocol - elif connection and isinstance(connection.stream, - iostream.SSLIOStream): + elif isinstance(stream, iostream.SSLIOStream): self.protocol = "https" else: self.protocol = "http" + self._orig_remote_ip = self.remote_ip + self._orig_protocol = self.protocol - # xheaders can override the defaults - if connection and connection.xheaders: - # Squid uses X-Forwarded-For, others use X-Real-Ip - ip = self.headers.get("X-Forwarded-For", self.remote_ip) - ip = ip.split(',')[-1].strip() - ip = self.headers.get( - "X-Real-Ip", ip) - if netutil.is_valid_ip(ip): - self.remote_ip = ip - # AWS uses X-Forwarded-Proto - proto = self.headers.get( - "X-Scheme", self.headers.get("X-Forwarded-Proto", self.protocol)) - if proto in ("http", "https"): - self.protocol = proto + def __str__(self): + if self.address_family in (socket.AF_INET, socket.AF_INET6): + return self.remote_ip + elif isinstance(self.address, bytes): + # Python 3 with the -bb option warns about str(bytes), + # so convert it explicitly. + # Unix socket addresses are str on mac but bytes on linux. + return native_str(self.address) + else: + return str(self.address) - self.host = host or self.headers.get("Host") or "127.0.0.1" - self.files = files or {} + def _apply_xheaders(self, headers): + """Rewrite the ``remote_ip`` and ``protocol`` fields.""" + # Squid uses X-Forwarded-For, others use X-Real-Ip + ip = headers.get("X-Forwarded-For", self.remote_ip) + ip = ip.split(',')[-1].strip() + ip = headers.get("X-Real-Ip", ip) + if netutil.is_valid_ip(ip): + self.remote_ip = ip + # AWS uses X-Forwarded-Proto + proto_header = headers.get( + "X-Scheme", headers.get("X-Forwarded-Proto", + self.protocol)) + if proto_header in ("http", "https"): + self.protocol = proto_header + + def _unapply_xheaders(self): + """Undo changes from `_apply_xheaders`. + + Xheaders are per-request so they should not leak to the next + request on the same connection. + """ + self.remote_ip = self._orig_remote_ip + self.protocol = self._orig_protocol + + +class _ServerRequestAdapter(httputil.HTTPMessageDelegate): + """Adapts the `HTTPMessageDelegate` interface to the interface expected + by our clients. + """ + def __init__(self, server, connection): + self.server = server self.connection = connection - self._start_time = time.time() - self._finish_time = None + self.request = None + if isinstance(server.request_callback, + httputil.HTTPServerConnectionDelegate): + self.delegate = server.request_callback.start_request(connection) + self._chunks = None + else: + self.delegate = None + self._chunks = [] - self.path, sep, self.query = uri.partition('?') - self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) - self.query_arguments = copy.deepcopy(self.arguments) - self.body_arguments = {} + def headers_received(self, start_line, headers): + if self.server.xheaders: + self.connection.context._apply_xheaders(headers) + if self.delegate is None: + self.request = httputil.HTTPServerRequest( + connection=self.connection, start_line=start_line, + headers=headers) + else: + return self.delegate.headers_received(start_line, headers) - def supports_http_1_1(self): - """Returns True if this request supports HTTP/1.1 semantics""" - return self.version == "HTTP/1.1" - - @property - def cookies(self): - """A dictionary of Cookie.Morsel objects.""" - if not hasattr(self, "_cookies"): - self._cookies = Cookie.SimpleCookie() - if "Cookie" in self.headers: - try: - self._cookies.load( - native_str(self.headers["Cookie"])) - except Exception: - self._cookies = {} - return self._cookies - - def write(self, chunk, callback=None): - """Writes the given chunk to the response stream.""" - assert isinstance(chunk, bytes_type) - self.connection.write(chunk, callback=callback) + def data_received(self, chunk): + if self.delegate is None: + self._chunks.append(chunk) + else: + return self.delegate.data_received(chunk) def finish(self): - """Finishes this HTTP request on the open connection.""" - self.connection.finish() - self._finish_time = time.time() - - def full_url(self): - """Reconstructs the full URL for this request.""" - return self.protocol + "://" + self.host + self.uri - - def request_time(self): - """Returns the amount of time it took for this request to execute.""" - if self._finish_time is None: - return time.time() - self._start_time + if self.delegate is None: + self.request.body = b''.join(self._chunks) + self.request._parse_body() + self.server.request_callback(self.request) else: - return self._finish_time - self._start_time + self.delegate.finish() + self._cleanup() - def get_ssl_certificate(self, binary_form=False): - """Returns the client's SSL certificate, if any. + def on_connection_close(self): + if self.delegate is None: + self._chunks = None + else: + self.delegate.on_connection_close() + self._cleanup() - To use client certificates, the HTTPServer must have been constructed - with cert_reqs set in ssl_options, e.g.:: + def _cleanup(self): + if self.server.xheaders: + self.connection.context._unapply_xheaders() - server = HTTPServer(app, - ssl_options=dict( - certfile="foo.crt", - keyfile="foo.key", - cert_reqs=ssl.CERT_REQUIRED, - ca_certs="cacert.crt")) - By default, the return value is a dictionary (or None, if no - client certificate is present). If ``binary_form`` is true, a - DER-encoded form of the certificate is returned instead. See - SSLSocket.getpeercert() in the standard library for more - details. - http://docs.python.org/library/ssl.html#sslsocket-objects - """ - try: - return self.connection.stream.socket.getpeercert( - binary_form=binary_form) - except ssl.SSLError: - return None - - def __repr__(self): - attrs = ("protocol", "host", "method", "uri", "version", "remote_ip") - args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) - return "%s(%s, headers=%s)" % ( - self.__class__.__name__, args, dict(self.headers)) +HTTPRequest = httputil.HTTPServerRequest diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 2575bc56..6e110d90 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -14,20 +14,31 @@ # License for the specific language governing permissions and limitations # under the License. -"""HTTP utility code shared by clients and servers.""" +"""HTTP utility code shared by clients and servers. + +This module also defines the `HTTPServerRequest` class which is exposed +via `tornado.web.RequestHandler.request`. +""" from __future__ import absolute_import, division, print_function, with_statement import calendar import collections +import copy import datetime import email.utils import numbers +import re import time from tornado.escape import native_str, parse_qs_bytes, utf8 from tornado.log import gen_log -from tornado.util import ObjectDict +from tornado.util import ObjectDict, bytes_type + +try: + import Cookie # py2 +except ImportError: + import http.cookies as Cookie # py3 try: from httplib import responses # py2 @@ -43,6 +54,13 @@ try: except ImportError: from urllib.parse import urlencode # py3 +try: + from ssl import SSLError +except ImportError: + # ssl is unavailable on app engine. + class SSLError(Exception): + pass + class _NormalizedHeaderCache(dict): """Dynamic cached mapping of header names to Http-Header-Case. @@ -212,6 +230,337 @@ class HTTPHeaders(dict): return HTTPHeaders(self) +class HTTPServerRequest(object): + """A single HTTP request. + + All attributes are type `str` unless otherwise noted. + + .. attribute:: method + + HTTP request method, e.g. "GET" or "POST" + + .. attribute:: uri + + The requested uri. + + .. attribute:: path + + The path portion of `uri` + + .. attribute:: query + + The query portion of `uri` + + .. attribute:: version + + HTTP version specified in request, e.g. "HTTP/1.1" + + .. attribute:: headers + + `.HTTPHeaders` dictionary-like object for request headers. Acts like + a case-insensitive dictionary with additional methods for repeated + headers. + + .. attribute:: body + + Request body, if present, as a byte string. + + .. attribute:: remote_ip + + Client's IP address as a string. If ``HTTPServer.xheaders`` is set, + will pass along the real IP address provided by a load balancer + in the ``X-Real-Ip`` or ``X-Forwarded-For`` header. + + .. versionchanged:: 3.1 + The list format of ``X-Forwarded-For`` is now supported. + + .. attribute:: protocol + + The protocol used, either "http" or "https". If ``HTTPServer.xheaders`` + is set, will pass along the protocol used by a load balancer if + reported via an ``X-Scheme`` header. + + .. attribute:: host + + The requested hostname, usually taken from the ``Host`` header. + + .. attribute:: arguments + + GET/POST arguments are available in the arguments property, which + maps arguments names to lists of values (to support multiple values + for individual names). Names are of type `str`, while arguments + are byte strings. Note that this is different from + `.RequestHandler.get_argument`, which returns argument values as + unicode strings. + + .. attribute:: query_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the query string. + + .. versionadded:: 3.2 + + .. attribute:: body_arguments + + Same format as ``arguments``, but contains only arguments extracted + from the request body. + + .. versionadded:: 3.2 + + .. attribute:: files + + File uploads are available in the files property, which maps file + names to lists of `.HTTPFile`. + + .. attribute:: connection + + An HTTP request is attached to a single HTTP connection, which can + be accessed through the "connection" attribute. Since connections + are typically kept open in HTTP/1.1, multiple requests can be handled + sequentially on a single connection. + + .. versionchanged:: 3.3 + Moved from ``tornado.httpserver.HTTPRequest``. + """ + def __init__(self, method=None, uri=None, version="HTTP/1.0", headers=None, + body=None, host=None, files=None, connection=None, + start_line=None): + if start_line is not None: + method, uri, version = start_line + self.method = method + self.uri = uri + self.version = version + self.headers = headers or HTTPHeaders() + self.body = body or "" + + # set remote IP and protocol + context = getattr(connection, 'context', None) + self.remote_ip = getattr(context, 'remote_ip') + self.protocol = getattr(context, 'protocol', "http") + + self.host = host or self.headers.get("Host") or "127.0.0.1" + self.files = files or {} + self.connection = connection + self._start_time = time.time() + self._finish_time = None + + self.path, sep, self.query = uri.partition('?') + self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.query_arguments = copy.deepcopy(self.arguments) + self.body_arguments = {} + + def supports_http_1_1(self): + """Returns True if this request supports HTTP/1.1 semantics. + + .. deprecated:: 3.3 + Applications are less likely to need this information with the + introduction of `.HTTPConnection`. If you still need it, access + the ``version`` attribute directly. + """ + return self.version == "HTTP/1.1" + + @property + def cookies(self): + """A dictionary of Cookie.Morsel objects.""" + if not hasattr(self, "_cookies"): + self._cookies = Cookie.SimpleCookie() + if "Cookie" in self.headers: + try: + self._cookies.load( + native_str(self.headers["Cookie"])) + except Exception: + self._cookies = {} + return self._cookies + + def write(self, chunk, callback=None): + """Writes the given chunk to the response stream. + + .. deprecated:: 3.3 + Use ``request.connection`` and the `.HTTPConnection` methods + to write the response. + """ + assert isinstance(chunk, bytes_type) + self.connection.write(chunk, callback=callback) + + def finish(self): + """Finishes this HTTP request on the open connection. + + .. deprecated:: 3.3 + Use ``request.connection`` and the `.HTTPConnection` methods + to write the response. + """ + self.connection.finish() + self._finish_time = time.time() + + def full_url(self): + """Reconstructs the full URL for this request.""" + return self.protocol + "://" + self.host + self.uri + + def request_time(self): + """Returns the amount of time it took for this request to execute.""" + if self._finish_time is None: + return time.time() - self._start_time + else: + return self._finish_time - self._start_time + + def get_ssl_certificate(self, binary_form=False): + """Returns the client's SSL certificate, if any. + + To use client certificates, the HTTPServer must have been constructed + with cert_reqs set in ssl_options, e.g.:: + + server = HTTPServer(app, + ssl_options=dict( + certfile="foo.crt", + keyfile="foo.key", + cert_reqs=ssl.CERT_REQUIRED, + ca_certs="cacert.crt")) + + By default, the return value is a dictionary (or None, if no + client certificate is present). If ``binary_form`` is true, a + DER-encoded form of the certificate is returned instead. See + SSLSocket.getpeercert() in the standard library for more + details. + http://docs.python.org/library/ssl.html#sslsocket-objects + """ + try: + return self.connection.stream.socket.getpeercert( + binary_form=binary_form) + except SSLError: + return None + + def _parse_body(self): + parse_body_arguments( + self.headers.get("Content-Type", ""), self.body, + self.body_arguments, self.files, + self.headers) + + for k, v in self.body_arguments.items(): + self.arguments.setdefault(k, []).extend(v) + + def __repr__(self): + attrs = ("protocol", "host", "method", "uri", "version", "remote_ip") + args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) + return "%s(%s, headers=%s)" % ( + self.__class__.__name__, args, dict(self.headers)) + + +class HTTPInputException(Exception): + """Exception class for malformed HTTP requests or responses + from remote sources. + + .. versionadded:: 3.3 + """ + pass + + +class HTTPOutputException(Exception): + """Exception class for errors in HTTP output. + + .. versionadded:: 3.3 + """ + pass + + +class HTTPServerConnectionDelegate(object): + """Implement this interface to handle requests from `.HTTPServer`. + + .. versionadded:: 3.3 + """ + def start_request(self, server_conn, request_conn): + """This method is called by the server when a new request has started. + + :arg server_conn: is an opaque object representing the long-lived + (e.g. tcp-level) connection. + :arg request_conn: is a `.HTTPConnection` object for a single + request/response exchange. + + This method should return a `.HTTPMessageDelegate`. + """ + raise NotImplementedError() + + def on_close(self, server_conn): + """This method is called when a connection has been closed. + + :arg server_conn: is a server connection that has previously been + passed to ``start_request``. + """ + pass + + +class HTTPMessageDelegate(object): + """Implement this interface to handle an HTTP request or response. + + .. versionadded:: 3.3 + """ + def headers_received(self, start_line, headers): + """Called when the HTTP headers have been received and parsed. + + :arg start_line: a `.RequestStartLine` or `.ResponseStartLine` + depending on whether this is a client or server message. + :arg headers: a `.HTTPHeaders` instance. + + Some `.HTTPConnection` methods can only be called during + ``headers_received``. + + May return a `.Future`; if it does the body will not be read + until it is done. + """ + pass + + def data_received(self, chunk): + """Called when a chunk of data has been received. + + May return a `.Future` for flow control. + """ + pass + + def finish(self): + """Called after the last chunk of data has been received.""" + pass + + def on_connection_close(self): + """Called if the connection is closed without finishing the request. + + If ``headers_received`` is called, either ``finish`` or + ``on_connection_close`` will be called, but not both. + """ + pass + + +class HTTPConnection(object): + """Applications use this interface to write their responses. + + .. versionadded:: 3.3 + """ + def write_headers(self, start_line, headers, chunk=None, callback=None): + """Write an HTTP header block. + + :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`. + :arg headers: a `.HTTPHeaders` instance. + :arg chunk: the first (optional) chunk of data. This is an optimization + so that small responses can be written in the same call as their + headers. + :arg callback: a callback to be run when the write is complete. + + Returns a `.Future` if no callback is given. + """ + raise NotImplementedError() + + def write(self, chunk, callback=None): + """Writes a chunk of body data. + + The callback will be run when the write is complete. If no callback + is given, returns a Future. + """ + raise NotImplementedError() + + def finish(self): + """Indicates that the last body data has been written. + """ + raise NotImplementedError() + + def url_concat(url, args): """Concatenate url and argument dictionary regardless of whether url has existing query parameters. @@ -310,7 +659,7 @@ def _int_or_none(val): return int(val) -def parse_body_arguments(content_type, body, arguments, files): +def parse_body_arguments(content_type, body, arguments, files, headers=None): """Parses a form request body. Supports ``application/x-www-form-urlencoded`` and @@ -319,6 +668,10 @@ def parse_body_arguments(content_type, body, arguments, files): and ``files`` parameters are dictionaries that will be updated with the parsed contents. """ + if headers and 'Content-Encoding' in headers: + gen_log.warning("Unsupported Content-Encoding: %s", + headers['Content-Encoding']) + return if content_type.startswith("application/x-www-form-urlencoded"): try: uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) @@ -405,6 +758,48 @@ def format_timestamp(ts): raise TypeError("unknown timestamp type: %r" % ts) return email.utils.formatdate(ts, usegmt=True) + +RequestStartLine = collections.namedtuple( + 'RequestStartLine', ['method', 'path', 'version']) + + +def parse_request_start_line(line): + """Returns a (method, path, version) tuple for an HTTP 1.x request line. + + The response is a `collections.namedtuple`. + + >>> parse_request_start_line("GET /foo HTTP/1.1") + RequestStartLine(method='GET', path='/foo', version='HTTP/1.1') + """ + try: + method, path, version = line.split(" ") + except ValueError: + raise HTTPInputException("Malformed HTTP request line") + if not version.startswith("HTTP/"): + raise HTTPInputException( + "Malformed HTTP version in HTTP Request-Line: %r" % version) + return RequestStartLine(method, path, version) + + +ResponseStartLine = collections.namedtuple( + 'ResponseStartLine', ['version', 'code', 'reason']) + + +def parse_response_start_line(line): + """Returns a (version, code, reason) tuple for an HTTP 1.x response line. + + The response is a `collections.namedtuple`. + + >>> parse_response_start_line("HTTP/1.1 200 OK") + ResponseStartLine(version='HTTP/1.1', code=200, reason='OK') + """ + line = native_str(line) + match = re.match("(HTTP/1.[01]) ([0-9]+) ([^\r]*)", line) + if not match: + raise HTTPInputException("Error parsing response start line") + return ResponseStartLine(match.group(1), int(match.group(2)), + match.group(3)) + # _parseparam and _parse_header are copied and modified from python2.7's cgi.py # The original 2.7 version of this code did not correctly support some # combinations of semicolons and double quotes. diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index e7b84dd7..86c4a9f2 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -41,10 +41,11 @@ import threading import time import traceback -from tornado.concurrent import Future, TracebackFuture +from tornado.concurrent import TracebackFuture, is_future from tornado.log import app_log, gen_log from tornado import stack_context from tornado.util import Configurable +from tornado.util import errno_from_exception try: import signal @@ -156,6 +157,15 @@ class IOLoop(Configurable): assert not IOLoop.initialized() IOLoop._instance = self + @staticmethod + def clear_instance(): + """Clear the global `IOLoop` instance. + + .. versionadded:: 3.3 + """ + if hasattr(IOLoop, "_instance"): + del IOLoop._instance + @staticmethod def current(): """Returns the current thread's `IOLoop`. @@ -244,21 +254,40 @@ class IOLoop(Configurable): raise NotImplementedError() def add_handler(self, fd, handler, events): - """Registers the given handler to receive the given events for fd. + """Registers the given handler to receive the given events for ``fd``. + + The ``fd`` argument may either be an integer file descriptor or + a file-like object with a ``fileno()`` method (and optionally a + ``close()`` method, which may be called when the `IOLoop` is shut + down). The ``events`` argument is a bitwise or of the constants ``IOLoop.READ``, ``IOLoop.WRITE``, and ``IOLoop.ERROR``. When an event occurs, ``handler(fd, events)`` will be run. + + .. versionchanged:: 3.3 + Added the ability to pass file-like objects in addition to + raw file descriptors. """ raise NotImplementedError() def update_handler(self, fd, events): - """Changes the events we listen for fd.""" + """Changes the events we listen for ``fd``. + + .. versionchanged:: 3.3 + Added the ability to pass file-like objects in addition to + raw file descriptors. + """ raise NotImplementedError() def remove_handler(self, fd): - """Stop listening for events on fd.""" + """Stop listening for events on ``fd``. + + .. versionchanged:: 3.3 + Added the ability to pass file-like objects in addition to + raw file descriptors. + """ raise NotImplementedError() def set_blocking_signal_threshold(self, seconds, action): @@ -372,7 +401,7 @@ class IOLoop(Configurable): future_cell[0] = TracebackFuture() future_cell[0].set_exc_info(sys.exc_info()) else: - if isinstance(result, Future): + if is_future(result): future_cell[0] = result else: future_cell[0] = TracebackFuture() @@ -463,7 +492,7 @@ class IOLoop(Configurable): The callback is invoked with one argument, the `.Future`. """ - assert isinstance(future, Future) + assert is_future(future) callback = stack_context.wrap(callback) future.add_done_callback( lambda future: self.add_callback(callback, future)) @@ -490,6 +519,47 @@ class IOLoop(Configurable): """ app_log.error("Exception in callback %r", callback, exc_info=True) + def split_fd(self, fd): + """Returns an (fd, obj) pair from an ``fd`` parameter. + + We accept both raw file descriptors and file-like objects as + input to `add_handler` and related methods. When a file-like + object is passed, we must retain the object itself so we can + close it correctly when the `IOLoop` shuts down, but the + poller interfaces favor file descriptors (they will accept + file-like objects and call ``fileno()`` for you, but they + always return the descriptor itself). + + This method is provided for use by `IOLoop` subclasses and should + not generally be used by application code. + + .. versionadded:: 3.3 + """ + try: + return fd.fileno(), fd + except AttributeError: + return fd, fd + + def close_fd(self, fd): + """Utility method to close an ``fd``. + + If ``fd`` is a file-like object, we close it directly; otherwise + we use `os.close`. + + This method is provided for use by `IOLoop` subclasses (in + implementations of ``IOLoop.close(all_fds=True)`` and should + not generally be used by application code. + + .. versionadded:: 3.3 + """ + try: + try: + fd.close() + except AttributeError: + os.close(fd) + except OSError: + pass + class PollIOLoop(IOLoop): """Base class for IOLoops built around a select-like function. @@ -528,26 +598,24 @@ class PollIOLoop(IOLoop): self._closing = True self.remove_handler(self._waker.fileno()) if all_fds: - for fd in self._handlers.keys(): - try: - close_method = getattr(fd, 'close', None) - if close_method is not None: - close_method() - else: - os.close(fd) - except Exception: - gen_log.debug("error closing fd %s", fd, exc_info=True) + for fd, handler in self._handlers.values(): + self.close_fd(fd) self._waker.close() self._impl.close() + self._callbacks = None + self._timeouts = None def add_handler(self, fd, handler, events): - self._handlers[fd] = stack_context.wrap(handler) + fd, obj = self.split_fd(fd) + self._handlers[fd] = (obj, stack_context.wrap(handler)) self._impl.register(fd, events | self.ERROR) def update_handler(self, fd, events): + fd, obj = self.split_fd(fd) self._impl.modify(fd, events | self.ERROR) def remove_handler(self, fd): + fd, obj = self.split_fd(fd) self._handlers.pop(fd, None) self._events.pop(fd, None) try: @@ -566,6 +634,8 @@ class PollIOLoop(IOLoop): action if action is not None else signal.SIG_DFL) def start(self): + if self._running: + raise RuntimeError("IOLoop is already running") self._setup_logging() if self._stopped: self._stopped = False @@ -666,9 +736,7 @@ class PollIOLoop(IOLoop): # two ways EINTR might be signaled: # * e.errno == errno.EINTR # * e.args is like (errno.EINTR, 'Interrupted system call') - if (getattr(e, 'errno', None) == errno.EINTR or - (isinstance(getattr(e, 'args', None), tuple) and - len(e.args) == 2 and e.args[0] == errno.EINTR)): + if errno_from_exception(e) == errno.EINTR: continue else: raise @@ -685,15 +753,17 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - self._handlers[fd](fd, events) + fd_obj, handler_func = self._handlers[fd] + handler_func(fd_obj, events) except (OSError, IOError) as e: - if e.args[0] == errno.EPIPE: + if errno_from_exception(e) == errno.EPIPE: # Happens when the client closes the connection pass else: self.handle_callback_exception(self._handlers.get(fd)) except Exception: self.handle_callback_exception(self._handlers.get(fd)) + fd_obj = handler_func = None finally: # reset the stopped flag so another start/stop pair can be issued @@ -771,7 +841,11 @@ class _Timeout(object): if isinstance(deadline, numbers.Real): self.deadline = deadline elif isinstance(deadline, datetime.timedelta): - self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline) + now = io_loop.time() + try: + self.deadline = now + deadline.total_seconds() + except AttributeError: # py2.6 + self.deadline = now + _Timeout.timedelta_to_seconds(deadline) else: raise TypeError("Unsupported deadline %r" % deadline) self.callback = callback diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 5d4d08ac..3874bf75 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -31,21 +31,27 @@ import errno import numbers import os import socket -import ssl import sys import re +from tornado.concurrent import TracebackFuture from tornado import ioloop from tornado.log import gen_log, app_log from tornado.netutil import ssl_wrap_socket, ssl_match_hostname, SSLCertificateError from tornado import stack_context -from tornado.util import bytes_type +from tornado.util import bytes_type, errno_from_exception try: from tornado.platform.posix import _set_nonblocking except ImportError: _set_nonblocking = None +try: + import ssl +except ImportError: + # ssl is not available on Google App Engine + ssl = None + # These errnos indicate that a non-blocking operation must be retried # at a later time. On most platforms they're the same value, but on # some they differ. @@ -66,12 +72,31 @@ class StreamClosedError(IOError): pass +class UnsatisfiableReadError(Exception): + """Exception raised when a read cannot be satisfied. + + Raised by ``read_until`` and ``read_until_regex`` with a ``max_bytes`` + argument. + """ + pass + + +class StreamBufferFullError(Exception): + """Exception raised by `IOStream` methods when the buffer is full. + """ + + class BaseIOStream(object): """A utility class to write to and read from a non-blocking file or socket. We support a non-blocking ``write()`` and a family of ``read_*()`` methods. - All of the methods take callbacks (since writing and reading are - non-blocking and asynchronous). + All of the methods take an optional ``callback`` argument and return a + `.Future` only if no callback is given. When the operation completes, + the callback will be run or the `.Future` will resolve with the data + read (or ``None`` for ``write()``). All outstanding ``Futures`` will + resolve with a `StreamClosedError` when the stream is closed; users + of the callback interface will be notified via + `.BaseIOStream.set_close_callback` instead. When a stream is closed due to an error, the IOStream's ``error`` attribute contains the exception object. @@ -80,24 +105,48 @@ class BaseIOStream(object): `read_from_fd`, and optionally `get_fd_error`. """ def __init__(self, io_loop=None, max_buffer_size=None, - read_chunk_size=4096): + read_chunk_size=None, max_write_buffer_size=None): + """`BaseIOStream` constructor. + + :arg io_loop: The `.IOLoop` to use; defaults to `.IOLoop.current`. + :arg max_buffer_size: Maximum amount of incoming data to buffer; + defaults to 100MB. + :arg read_chunk_size: Amount of data to read at one time from the + underlying transport; defaults to 64KB. + :arg max_write_buffer_size: Amount of outgoing data to buffer; + defaults to unlimited. + + .. versionchanged:: 3.3 + Add the ``max_write_buffer_size`` parameter. Changed default + ``read_chunk_size`` to 64KB. + """ self.io_loop = io_loop or ioloop.IOLoop.current() self.max_buffer_size = max_buffer_size or 104857600 - self.read_chunk_size = read_chunk_size + # A chunk size that is too close to max_buffer_size can cause + # spurious failures. + self.read_chunk_size = min(read_chunk_size or 65536, + self.max_buffer_size // 2) + self.max_write_buffer_size = max_write_buffer_size self.error = None self._read_buffer = collections.deque() self._write_buffer = collections.deque() self._read_buffer_size = 0 + self._write_buffer_size = 0 self._write_buffer_frozen = False self._read_delimiter = None self._read_regex = None + self._read_max_bytes = None self._read_bytes = None + self._read_partial = False self._read_until_close = False self._read_callback = None + self._read_future = None self._streaming_callback = None self._write_callback = None + self._write_future = None self._close_callback = None self._connect_callback = None + self._connect_future = None self._connecting = False self._state = None self._pending_callbacks = 0 @@ -142,98 +191,162 @@ class BaseIOStream(object): """ return None - def read_until_regex(self, regex, callback): - """Run ``callback`` when we read the given regex pattern. + def read_until_regex(self, regex, callback=None, max_bytes=None): + """Asynchronously read until we have matched the given regex. - The callback will get the data read (including the data that - matched the regex and anything that came before it) as an argument. + The result includes the data that matches the regex and anything + that came before it. If a callback is given, it will be run + with the data as an argument; if not, this method returns a + `.Future`. + + If ``max_bytes`` is not None, the connection will be closed + if more than ``max_bytes`` bytes have been read and the regex is + not satisfied. + + .. versionchanged:: 3.3 + Added the ``max_bytes`` argument. The ``callback`` argument is + now optional and a `.Future` will be returned if it is omitted. """ - self._set_read_callback(callback) + future = self._set_read_callback(callback) self._read_regex = re.compile(regex) - self._try_inline_read() + self._read_max_bytes = max_bytes + try: + self._try_inline_read() + except UnsatisfiableReadError as e: + # Handle this the same way as in _handle_events. + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=True) + return future + return future - def read_until(self, delimiter, callback): - """Run ``callback`` when we read the given delimiter. + def read_until(self, delimiter, callback=None, max_bytes=None): + """Asynchronously read until we have found the given delimiter. - The callback will get the data read (including the delimiter) - as an argument. + The result includes all the data read including the delimiter. + If a callback is given, it will be run with the data as an argument; + if not, this method returns a `.Future`. + + If ``max_bytes`` is not None, the connection will be closed + if more than ``max_bytes`` bytes have been read and the delimiter + is not found. + + .. versionchanged:: 3.3 + Added the ``max_bytes`` argument. The ``callback`` argument is + now optional and a `.Future` will be returned if it is omitted. """ - self._set_read_callback(callback) + future = self._set_read_callback(callback) self._read_delimiter = delimiter - self._try_inline_read() + self._read_max_bytes = max_bytes + try: + self._try_inline_read() + except UnsatisfiableReadError as e: + # Handle this the same way as in _handle_events. + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=True) + return future + return future - def read_bytes(self, num_bytes, callback, streaming_callback=None): - """Run callback when we read the given number of bytes. + def read_bytes(self, num_bytes, callback=None, streaming_callback=None, + partial=False): + """Asynchronously read a number of bytes. If a ``streaming_callback`` is given, it will be called with chunks - of data as they become available, and the argument to the final - ``callback`` will be empty. Otherwise, the ``callback`` gets - the data as an argument. + of data as they become available, and the final result will be empty. + Otherwise, the result is all the data that was read. + If a callback is given, it will be run with the data as an argument; + if not, this method returns a `.Future`. + + If ``partial`` is true, the callback is run as soon as we have + any bytes to return (but never more than ``num_bytes``) + + .. versionchanged:: 3.3 + Added the ``partial`` argument. The callback argument is now + optional and a `.Future` will be returned if it is omitted. """ - self._set_read_callback(callback) + future = self._set_read_callback(callback) assert isinstance(num_bytes, numbers.Integral) self._read_bytes = num_bytes + self._read_partial = partial self._streaming_callback = stack_context.wrap(streaming_callback) self._try_inline_read() + return future - def read_until_close(self, callback, streaming_callback=None): - """Reads all data from the socket until it is closed. + def read_until_close(self, callback=None, streaming_callback=None): + """Asynchronously reads all data from the socket until it is closed. If a ``streaming_callback`` is given, it will be called with chunks - of data as they become available, and the argument to the final - ``callback`` will be empty. Otherwise, the ``callback`` gets the - data as an argument. + of data as they become available, and the final result will be empty. + Otherwise, the result is all the data that was read. + If a callback is given, it will be run with the data as an argument; + if not, this method returns a `.Future`. - Subject to ``max_buffer_size`` limit from `IOStream` constructor if - a ``streaming_callback`` is not used. + .. versionchanged:: 3.3 + The callback argument is now optional and a `.Future` will + be returned if it is omitted. """ - self._set_read_callback(callback) + future = self._set_read_callback(callback) self._streaming_callback = stack_context.wrap(streaming_callback) if self.closed(): if self._streaming_callback is not None: - self._run_callback(self._streaming_callback, - self._consume(self._read_buffer_size)) - self._run_callback(self._read_callback, - self._consume(self._read_buffer_size)) - self._streaming_callback = None - self._read_callback = None - return + self._run_read_callback(self._read_buffer_size, True) + self._run_read_callback(self._read_buffer_size, False) + return future self._read_until_close = True - self._streaming_callback = stack_context.wrap(streaming_callback) self._try_inline_read() + return future def write(self, data, callback=None): - """Write the given data to this stream. + """Asynchronously write the given data to this stream. If ``callback`` is given, we call it when all of the buffered write data has been successfully written to the stream. If there was previously buffered write data and an old write callback, that callback is simply overwritten with this new callback. + + If no ``callback`` is given, this method returns a `.Future` that + resolves (with a result of ``None``) when the write has been + completed. If `write` is called again before that `.Future` has + resolved, the previous future will be orphaned and will never resolve. + + .. versionchanged:: 3.3 + Now returns a `.Future` if no callback is given. """ assert isinstance(data, bytes_type) self._check_closed() # We use bool(_write_buffer) as a proxy for write_buffer_size>0, # so never put empty strings in the buffer. if data: + if (self.max_write_buffer_size is not None and + self._write_buffer_size + len(data) > self.max_write_buffer_size): + raise StreamBufferFullError("Reached maximum read buffer size") # Break up large contiguous strings before inserting them in the # write buffer, so we don't have to recopy the entire thing # as we slice off pieces to send to the socket. WRITE_BUFFER_CHUNK_SIZE = 128 * 1024 - if len(data) > WRITE_BUFFER_CHUNK_SIZE: - for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE): - self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE]) - else: - self._write_buffer.append(data) - self._write_callback = stack_context.wrap(callback) + for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE): + self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE]) + self._write_buffer_size += len(data) + if callback is not None: + self._write_callback = stack_context.wrap(callback) + future = None + else: + future = self._write_future = TracebackFuture() if not self._connecting: self._handle_write() if self._write_buffer: self._add_io_state(self.io_loop.WRITE) self._maybe_add_error_listener() + return future def set_close_callback(self, callback): - """Call the given callback when the stream is closed.""" + """Call the given callback when the stream is closed. + + This is not necessary for applications that use the `.Future` + interface; all outstanding ``Futures`` will resolve with a + `StreamClosedError` when the stream is closed. + """ self._close_callback = stack_context.wrap(callback) + self._maybe_add_error_listener() def close(self, exc_info=False): """Close this stream. @@ -251,13 +364,9 @@ class BaseIOStream(object): if self._read_until_close: if (self._streaming_callback is not None and self._read_buffer_size): - self._run_callback(self._streaming_callback, - self._consume(self._read_buffer_size)) - callback = self._read_callback - self._read_callback = None + self._run_read_callback(self._read_buffer_size, True) self._read_until_close = False - self._run_callback(callback, - self._consume(self._read_buffer_size)) + self._run_read_callback(self._read_buffer_size, False) if self._state is not None: self.io_loop.remove_handler(self.fileno()) self._state = None @@ -269,6 +378,25 @@ class BaseIOStream(object): # If there are pending callbacks, don't run the close callback # until they're done (see _maybe_add_error_handler) if self.closed() and self._pending_callbacks == 0: + futures = [] + if self._read_future is not None: + futures.append(self._read_future) + self._read_future = None + if self._write_future is not None: + futures.append(self._write_future) + self._write_future = None + if self._connect_future is not None: + futures.append(self._connect_future) + self._connect_future = None + for future in futures: + if (isinstance(self.error, (socket.error, IOError)) and + errno_from_exception(self.error) in _ERRNO_CONNRESET): + # Treat connection resets as closed connections so + # clients only have to catch one kind of exception + # to avoid logging. + future.set_exception(StreamClosedError()) + else: + future.set_exception(self.error or StreamClosedError()) if self._close_callback is not None: cb = self._close_callback self._close_callback = None @@ -282,7 +410,7 @@ class BaseIOStream(object): def reading(self): """Returns true if we are currently reading from the stream.""" - return self._read_callback is not None + return self._read_callback is not None or self._read_future is not None def writing(self): """Returns true if we are currently writing to the stream.""" @@ -309,16 +437,22 @@ class BaseIOStream(object): def _handle_events(self, fd, events): if self.closed(): - gen_log.warning("Got events for closed stream %d", fd) + gen_log.warning("Got events for closed stream %s", fd) return try: + if self._connecting: + # Most IOLoops will report a write failed connect + # with the WRITE event, but SelectIOLoop reports a + # READ as well so we must check for connecting before + # either. + self._handle_connect() + if self.closed(): + return if events & self.io_loop.READ: self._handle_read() if self.closed(): return if events & self.io_loop.WRITE: - if self._connecting: - self._handle_connect() self._handle_write() if self.closed(): return @@ -334,13 +468,20 @@ class BaseIOStream(object): state |= self.io_loop.READ if self.writing(): state |= self.io_loop.WRITE - if state == self.io_loop.ERROR: + if state == self.io_loop.ERROR and self._read_buffer_size == 0: + # If the connection is idle, listen for reads too so + # we can tell if the connection is closed. If there is + # data in the read buffer we won't run the close callback + # yet anyway, so we don't need to listen in this case. state |= self.io_loop.READ if state != self._state: assert self._state is not None, \ "shouldn't happen: _handle_events without self._state" self._state = state self.io_loop.update_handler(self.fileno(), self._state) + except UnsatisfiableReadError as e: + gen_log.info("Unsatisfiable read, closing connection: %s" % e) + self.close(exc_info=True) except Exception: gen_log.error("Uncaught exception, closing connection.", exc_info=True) @@ -381,42 +522,108 @@ class BaseIOStream(object): self._pending_callbacks += 1 self.io_loop.add_callback(wrapper) + def _read_to_buffer_loop(self): + # This method is called from _handle_read and _try_inline_read. + try: + if self._read_bytes is not None: + target_bytes = self._read_bytes + elif self._read_max_bytes is not None: + target_bytes = self._read_max_bytes + elif self.reading(): + # For read_until without max_bytes, or + # read_until_close, read as much as we can before + # scanning for the delimiter. + target_bytes = None + else: + target_bytes = 0 + next_find_pos = 0 + # Pretend to have a pending callback so that an EOF in + # _read_to_buffer doesn't trigger an immediate close + # callback. At the end of this method we'll either + # estabilsh a real pending callback via + # _read_from_buffer or run the close callback. + # + # We need two try statements here so that + # pending_callbacks is decremented before the `except` + # clause below (which calls `close` and does need to + # trigger the callback) + self._pending_callbacks += 1 + while not self.closed(): + # Read from the socket until we get EWOULDBLOCK or equivalent. + # SSL sockets do some internal buffering, and if the data is + # sitting in the SSL object's buffer select() and friends + # can't see it; the only way to find out if it's there is to + # try to read it. + if self._read_to_buffer() == 0: + break + + self._run_streaming_callback() + + # If we've read all the bytes we can use, break out of + # this loop. We can't just call read_from_buffer here + # because of subtle interactions with the + # pending_callback and error_listener mechanisms. + # + # If we've reached target_bytes, we know we're done. + if (target_bytes is not None and + self._read_buffer_size >= target_bytes): + break + + # Otherwise, we need to call the more expensive find_read_pos. + # It's inefficient to do this on every read, so instead + # do it on the first read and whenever the read buffer + # size has doubled. + if self._read_buffer_size >= next_find_pos: + pos = self._find_read_pos() + if pos is not None: + return pos + next_find_pos = self._read_buffer_size * 2 + return self._find_read_pos() + finally: + self._pending_callbacks -= 1 + def _handle_read(self): try: - try: - # Pretend to have a pending callback so that an EOF in - # _read_to_buffer doesn't trigger an immediate close - # callback. At the end of this method we'll either - # estabilsh a real pending callback via - # _read_from_buffer or run the close callback. - # - # We need two try statements here so that - # pending_callbacks is decremented before the `except` - # clause below (which calls `close` and does need to - # trigger the callback) - self._pending_callbacks += 1 - while not self.closed(): - # Read from the socket until we get EWOULDBLOCK or equivalent. - # SSL sockets do some internal buffering, and if the data is - # sitting in the SSL object's buffer select() and friends - # can't see it; the only way to find out if it's there is to - # try to read it. - if self._read_to_buffer() == 0: - break - finally: - self._pending_callbacks -= 1 + pos = self._read_to_buffer_loop() + except UnsatisfiableReadError: + raise except Exception: gen_log.warning("error on read", exc_info=True) self.close(exc_info=True) return - if self._read_from_buffer(): + if pos is not None: + self._read_from_buffer(pos) return else: self._maybe_run_close_callback() def _set_read_callback(self, callback): - assert not self._read_callback, "Already reading" - self._read_callback = stack_context.wrap(callback) + assert self._read_callback is None, "Already reading" + assert self._read_future is None, "Already reading" + if callback is not None: + self._read_callback = stack_context.wrap(callback) + else: + self._read_future = TracebackFuture() + return self._read_future + + def _run_read_callback(self, size, streaming): + if streaming: + callback = self._streaming_callback + else: + callback = self._read_callback + self._read_callback = self._streaming_callback = None + if self._read_future is not None: + assert callback is None + future = self._read_future + self._read_future = None + future.set_result(self._consume(size)) + if callback is not None: + assert self._read_future is None + self._run_callback(callback, self._consume(size)) + else: + # If we scheduled a callback, we will add the error listener + # afterwards. If we didn't, we have to do it now. + self._maybe_add_error_listener() def _try_inline_read(self): """Attempt to complete the current read operation from buffered data. @@ -426,18 +633,14 @@ class BaseIOStream(object): listening for reads on the socket. """ # See if we've already got the data from a previous read - if self._read_from_buffer(): + self._run_streaming_callback() + pos = self._find_read_pos() + if pos is not None: + self._read_from_buffer(pos) return self._check_closed() try: - try: - # See comments in _handle_read about incrementing _pending_callbacks - self._pending_callbacks += 1 - while not self.closed(): - if self._read_to_buffer() == 0: - break - finally: - self._pending_callbacks -= 1 + pos = self._read_to_buffer_loop() except Exception: # If there was an in _read_to_buffer, we called close() already, # but couldn't run the close callback because of _pending_callbacks. @@ -445,9 +648,15 @@ class BaseIOStream(object): # applicable. self._maybe_run_close_callback() raise - if self._read_from_buffer(): + if pos is not None: + self._read_from_buffer(pos) return - self._maybe_add_error_listener() + # We couldn't satisfy the read inline, so either close the stream + # or listen for new data. + if self.closed(): + self._maybe_run_close_callback() + else: + self._add_io_state(ioloop.IOLoop.READ) def _read_to_buffer(self): """Reads from the socket and appends the result to the read buffer. @@ -472,32 +681,42 @@ class BaseIOStream(object): return 0 self._read_buffer.append(chunk) self._read_buffer_size += len(chunk) - if self._read_buffer_size >= self.max_buffer_size: + if self._read_buffer_size > self.max_buffer_size: gen_log.error("Reached maximum read buffer size") self.close() - raise IOError("Reached maximum read buffer size") + raise StreamBufferFullError("Reached maximum read buffer size") return len(chunk) - def _read_from_buffer(self): - """Attempts to complete the currently-pending read from the buffer. - - Returns True if the read was completed. - """ + def _run_streaming_callback(self): if self._streaming_callback is not None and self._read_buffer_size: bytes_to_consume = self._read_buffer_size if self._read_bytes is not None: bytes_to_consume = min(self._read_bytes, bytes_to_consume) self._read_bytes -= bytes_to_consume - self._run_callback(self._streaming_callback, - self._consume(bytes_to_consume)) - if self._read_bytes is not None and self._read_buffer_size >= self._read_bytes: - num_bytes = self._read_bytes - callback = self._read_callback - self._read_callback = None - self._streaming_callback = None - self._read_bytes = None - self._run_callback(callback, self._consume(num_bytes)) - return True + self._run_read_callback(bytes_to_consume, True) + + def _read_from_buffer(self, pos): + """Attempts to complete the currently-pending read from the buffer. + + The argument is either a position in the read buffer or None, + as returned by _find_read_pos. + """ + self._read_bytes = self._read_delimiter = self._read_regex = None + self._read_partial = False + self._run_read_callback(pos, False) + + def _find_read_pos(self): + """Attempts to find a position in the read buffer that satisfies + the currently-pending read. + + Returns a position in the buffer if the current read can be satisfied, + or None if it cannot. + """ + if (self._read_bytes is not None and + (self._read_buffer_size >= self._read_bytes or + (self._read_partial and self._read_buffer_size > 0))): + num_bytes = min(self._read_bytes, self._read_buffer_size) + return num_bytes elif self._read_delimiter is not None: # Multi-byte delimiters (e.g. '\r\n') may straddle two # chunks in the read buffer, so we can't easily find them @@ -506,37 +725,40 @@ class BaseIOStream(object): # length) tend to be "line" oriented, the delimiter is likely # to be in the first few chunks. Merge the buffer gradually # since large merges are relatively expensive and get undone in - # consume(). + # _consume(). if self._read_buffer: while True: loc = self._read_buffer[0].find(self._read_delimiter) if loc != -1: - callback = self._read_callback delimiter_len = len(self._read_delimiter) - self._read_callback = None - self._streaming_callback = None - self._read_delimiter = None - self._run_callback(callback, - self._consume(loc + delimiter_len)) - return True + self._check_max_bytes(self._read_delimiter, + loc + delimiter_len) + return loc + delimiter_len if len(self._read_buffer) == 1: break _double_prefix(self._read_buffer) + self._check_max_bytes(self._read_delimiter, + len(self._read_buffer[0])) elif self._read_regex is not None: if self._read_buffer: while True: m = self._read_regex.search(self._read_buffer[0]) if m is not None: - callback = self._read_callback - self._read_callback = None - self._streaming_callback = None - self._read_regex = None - self._run_callback(callback, self._consume(m.end())) - return True + self._check_max_bytes(self._read_regex, m.end()) + return m.end() if len(self._read_buffer) == 1: break _double_prefix(self._read_buffer) - return False + self._check_max_bytes(self._read_regex, + len(self._read_buffer[0])) + return None + + def _check_max_bytes(self, delimiter, size): + if (self._read_max_bytes is not None and + size > self._read_max_bytes): + raise UnsatisfiableReadError( + "delimiter %r not found within %d bytes" % ( + delimiter, self._read_max_bytes)) def _handle_write(self): while self._write_buffer: @@ -563,6 +785,7 @@ class BaseIOStream(object): self._write_buffer_frozen = False _merge_prefix(self._write_buffer, num_bytes) self._write_buffer.popleft() + self._write_buffer_size -= num_bytes except (socket.error, IOError, OSError) as e: if e.args[0] in _ERRNO_WOULDBLOCK: self._write_buffer_frozen = True @@ -572,14 +795,19 @@ class BaseIOStream(object): # Broken pipe errors are usually caused by connection # reset, and its better to not log EPIPE errors to # minimize log spam - gen_log.warning("Write error on %d: %s", + gen_log.warning("Write error on %s: %s", self.fileno(), e) self.close(exc_info=True) return - if not self._write_buffer and self._write_callback: - callback = self._write_callback - self._write_callback = None - self._run_callback(callback) + if not self._write_buffer: + if self._write_callback: + callback = self._write_callback + self._write_callback = None + self._run_callback(callback) + if self._write_future: + future = self._write_future + self._write_future = None + future.set_result(None) def _consume(self, loc): if loc == 0: @@ -593,10 +821,19 @@ class BaseIOStream(object): raise StreamClosedError("Stream is closed") def _maybe_add_error_listener(self): - if self._state is None and self._pending_callbacks == 0: + # This method is part of an optimization: to detect a connection that + # is closed when we're not actively reading or writing, we must listen + # for read events. However, it is inefficient to do this when the + # connection is first established because we are going to read or write + # immediately anyway. Instead, we insert checks at various times to + # see if the connection is idle and add the read listener then. + if self._pending_callbacks != 0: + return + if self._state is None or self._state == ioloop.IOLoop.ERROR: if self.closed(): self._maybe_run_close_callback() - else: + elif (self._read_buffer_size == 0 and + self._close_callback is not None): self._add_io_state(ioloop.IOLoop.READ) def _add_io_state(self, state): @@ -680,7 +917,7 @@ class IOStream(BaseIOStream): super(IOStream, self).__init__(*args, **kwargs) def fileno(self): - return self.socket.fileno() + return self.socket def close_fd(self): self.socket.close() @@ -714,7 +951,9 @@ class IOStream(BaseIOStream): not previously connected. The address parameter is in the same format as for `socket.connect `, i.e. a ``(host, port)`` tuple. If ``callback`` is specified, - it will be called when the connection is completed. + it will be called with no arguments when the connection is + completed; if not this method returns a `.Future` (whose result + after a successful connection will be the stream itself). If specified, the ``server_hostname`` parameter will be used in SSL connections for certificate validation (if requested in @@ -726,6 +965,9 @@ class IOStream(BaseIOStream): which case the data will be written as soon as the connection is ready. Calling `IOStream` read methods before the socket is connected works on some platforms but is non-portable. + + .. versionchanged:: 3.3 + If no callback is given, returns a `.Future`. """ self._connecting = True try: @@ -738,14 +980,83 @@ class IOStream(BaseIOStream): # returned immediately when attempting to connect to # localhost, so handle them the same way as an error # reported later in _handle_connect. - if (e.args[0] != errno.EINPROGRESS and - e.args[0] not in _ERRNO_WOULDBLOCK): - gen_log.warning("Connect error on fd %d: %s", + if (errno_from_exception(e) != errno.EINPROGRESS and + errno_from_exception(e) not in _ERRNO_WOULDBLOCK): + gen_log.warning("Connect error on fd %s: %s", self.socket.fileno(), e) self.close(exc_info=True) return - self._connect_callback = stack_context.wrap(callback) + if callback is not None: + self._connect_callback = stack_context.wrap(callback) + future = None + else: + future = self._connect_future = TracebackFuture() self._add_io_state(self.io_loop.WRITE) + return future + + def start_tls(self, server_side, ssl_options=None, server_hostname=None): + """Convert this `IOStream` to an `SSLIOStream`. + + This enables protocols that begin in clear-text mode and + switch to SSL after some initial negotiation (such as the + ``STARTTLS`` extension to SMTP and IMAP). + + This method cannot be used if there are outstanding reads + or writes on the stream, or if there is any data in the + IOStream's buffer (data in the operating system's socket + buffer is allowed). This means it must generally be used + immediately after reading or writing the last clear-text + data. It can also be used immediately after connecting, + before any reads or writes. + + The ``ssl_options`` argument may be either a dictionary + of options or an `ssl.SSLContext`. If a ``server_hostname`` + is given, it will be used for certificate verification + (as configured in the ``ssl_options``). + + This method returns a `.Future` whose result is the new + `SSLIOStream`. After this method has been called, + any other operation on the original stream is undefined. + + If a close callback is defined on this stream, it will be + transferred to the new stream. + + .. versionadded:: 3.3 + """ + if (self._read_callback or self._read_future or + self._write_callback or self._write_future or + self._connect_callback or self._connect_future or + self._pending_callbacks or self._closed or + self._read_buffer or self._write_buffer): + raise ValueError("IOStream is not idle; cannot convert to SSL") + if ssl_options is None: + ssl_options = {} + + socket = self.socket + self.io_loop.remove_handler(socket) + self.socket = None + socket = ssl_wrap_socket(socket, ssl_options, server_side=server_side, + do_handshake_on_connect=False) + orig_close_callback = self._close_callback + self._close_callback = None + + future = TracebackFuture() + ssl_stream = SSLIOStream(socket, ssl_options=ssl_options, + io_loop=self.io_loop) + # Wrap the original close callback so we can fail our Future as well. + # If we had an "unwrap" counterpart to this method we would need + # to restore the original callback after our Future resolves + # so that repeated wrap/unwrap calls don't build up layers. + def close_callback(): + if not future.done(): + future.set_exception(ssl_stream.error or StreamClosedError()) + if orig_close_callback is not None: + orig_close_callback() + ssl_stream.set_close_callback(close_callback) + ssl_stream._ssl_connect_callback = lambda: future.set_result(ssl_stream) + ssl_stream.max_buffer_size = self.max_buffer_size + ssl_stream.read_chunk_size = self.read_chunk_size + return future def _handle_connect(self): err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) @@ -755,14 +1066,19 @@ class IOStream(BaseIOStream): # an error state before the socket becomes writable, so # in that case a connection failure would be handled by the # error path in _handle_events instead of here. - gen_log.warning("Connect error on fd %d: %s", - self.socket.fileno(), errno.errorcode[err]) + if self._connect_future is None: + gen_log.warning("Connect error on fd %s: %s", + self.socket.fileno(), errno.errorcode[err]) self.close() return if self._connect_callback is not None: callback = self._connect_callback self._connect_callback = None self._run_callback(callback) + if self._connect_future is not None: + future = self._connect_future + self._connect_future = None + future.set_result(self) self._connecting = False def set_nodelay(self, value): @@ -841,7 +1157,7 @@ class SSLIOStream(IOStream): peer = self.socket.getpeername() except Exception: peer = '(not connected)' - gen_log.warning("SSL Error on %d %s: %s", + gen_log.warning("SSL Error on %s %s: %s", self.socket.fileno(), peer, err) return self.close(exc_info=True) raise @@ -907,19 +1223,33 @@ class SSLIOStream(IOStream): # has completed. self._ssl_connect_callback = stack_context.wrap(callback) self._server_hostname = server_hostname - super(SSLIOStream, self).connect(address, callback=None) + # Note: Since we don't pass our callback argument along to + # super.connect(), this will always return a Future. + # This is harmless, but a bit less efficient than it could be. + return super(SSLIOStream, self).connect(address, callback=None) def _handle_connect(self): + # Call the superclass method to check for errors. + super(SSLIOStream, self)._handle_connect() + if self.closed(): + return # When the connection is complete, wrap the socket for SSL # traffic. Note that we do this by overriding _handle_connect # instead of by passing a callback to super().connect because # user callbacks are enqueued asynchronously on the IOLoop, # but since _handle_events calls _handle_connect immediately # followed by _handle_write we need this to be synchronous. + # + # The IOLoop will get confused if we swap out self.socket while the + # fd is registered, so remove it now and re-register after + # wrap_socket(). + self.io_loop.remove_handler(self.socket) + old_state = self._state + self._state = None self.socket = ssl_wrap_socket(self.socket, self._ssl_options, server_hostname=self._server_hostname, do_handshake_on_connect=False) - super(SSLIOStream, self)._handle_connect() + self._add_io_state(old_state) def read_from_fd(self): if self._ssl_accepting: @@ -978,9 +1308,9 @@ class PipeIOStream(BaseIOStream): try: chunk = os.read(self.fd, self.read_chunk_size) except (IOError, OSError) as e: - if e.args[0] in _ERRNO_WOULDBLOCK: + if errno_from_exception(e) in _ERRNO_WOULDBLOCK: return None - elif e.args[0] == errno.EBADF: + elif errno_from_exception(e) == errno.EBADF: # If the writing half of a pipe is closed, select will # report it as readable but reads will fail with EBADF. self.close(exc_info=True) diff --git a/libs/tornado/log.py b/libs/tornado/log.py index 36c3dd40..70664664 100755 --- a/libs/tornado/log.py +++ b/libs/tornado/log.py @@ -83,10 +83,10 @@ class LogFormatter(logging.Formatter): DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s' DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S' DEFAULT_COLORS = { - logging.DEBUG: 4, # Blue - logging.INFO: 2, # Green - logging.WARNING: 3, # Yellow - logging.ERROR: 1, # Red + logging.DEBUG: 4, # Blue + logging.INFO: 2, # Green + logging.WARNING: 3, # Yellow + logging.ERROR: 1, # Red } def __init__(self, color=True, fmt=DEFAULT_FORMAT, @@ -184,7 +184,7 @@ def enable_pretty_logging(options=None, logger=None): """ if options is None: from tornado.options import options - if options.logging == 'none': + if options.logging is None or options.logging.lower() == 'none': return if logger is None: logger = logging.getLogger() diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 8ebe604d..a9e05d1e 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -20,18 +20,26 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os +import platform import socket -import ssl import stat from tornado.concurrent import dummy_executor, run_on_executor from tornado.ioloop import IOLoop from tornado.platform.auto import set_close_exec -from tornado.util import u, Configurable +from tornado.util import u, Configurable, errno_from_exception + +try: + import ssl +except ImportError: + # ssl is not available on Google App Engine + ssl = None if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ ssl_match_hostname = ssl.match_hostname SSLCertificateError = ssl.CertificateError +elif ssl is None: + ssl_match_hostname = SSLCertificateError = None else: import backports.ssl_match_hostname ssl_match_hostname = backports.ssl_match_hostname.match_hostname @@ -44,6 +52,11 @@ else: # thread now. u('foo').encode('idna') +# These errnos indicate that a non-blocking operation must be retried +# at a later time. On most platforms they're the same value, but on +# some they differ. +_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN) + def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): """Creates listening sockets bound to the given port and address. @@ -77,13 +90,23 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags family = socket.AF_INET if flags is None: flags = socket.AI_PASSIVE + bound_port = None for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)): af, socktype, proto, canonname, sockaddr = res + if (platform.system() == 'Darwin' and address == 'localhost' and + af == socket.AF_INET6 and sockaddr[3] != 0): + # Mac OS X includes a link-local address fe80::1%lo0 in the + # getaddrinfo results for 'localhost'. However, the firewall + # doesn't understand that this is a local address and will + # prompt for access (often repeatedly, due to an apparent + # bug in its ability to remember granting access to an + # application). Skip these addresses. + continue try: sock = socket.socket(af, socktype, proto) except socket.error as e: - if e.args[0] == errno.EAFNOSUPPORT: + if errno_from_exception(e) == errno.EAFNOSUPPORT: continue raise set_close_exec(sock.fileno()) @@ -100,8 +123,16 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags # Python 2.x on windows doesn't have IPPROTO_IPV6. if hasattr(socket, "IPPROTO_IPV6"): sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + + # automatic port allocation with port=None + # should bind on the same port on IPv4 and IPv6 + host, requested_port = sockaddr[:2] + if requested_port == 0 and bound_port is not None: + sockaddr = tuple([host, bound_port] + list(sockaddr[2:])) + sock.setblocking(0) sock.bind(sockaddr) + bound_port = sock.getsockname()[1] sock.listen(backlog) sockets.append(sock) return sockets @@ -124,7 +155,7 @@ if hasattr(socket, 'AF_UNIX'): try: st = os.stat(file) except OSError as err: - if err.errno != errno.ENOENT: + if errno_from_exception(err) != errno.ENOENT: raise else: if stat.S_ISSOCK(st.st_mode): @@ -154,18 +185,18 @@ def add_accept_handler(sock, callback, io_loop=None): try: connection, address = sock.accept() except socket.error as e: - # EWOULDBLOCK and EAGAIN indicate we have accepted every + # _ERRNO_WOULDBLOCK indicate we have accepted every # connection that is available. - if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + if errno_from_exception(e) in _ERRNO_WOULDBLOCK: return # ECONNABORTED indicates that there was a connection # but it was closed while still in the accept queue. # (observed on FreeBSD). - if e.args[0] == errno.ECONNABORTED: + if errno_from_exception(e) == errno.ECONNABORTED: continue raise callback(connection, address) - io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) + io_loop.add_handler(sock, accept_handler, IOLoop.READ) def is_valid_ip(ip): @@ -381,6 +412,10 @@ def ssl_options_to_context(ssl_options): context.load_verify_locations(ssl_options['ca_certs']) if 'ciphers' in ssl_options: context.set_ciphers(ssl_options['ciphers']) + if hasattr(ssl, 'OP_NO_COMPRESSION'): + # Disable TLS compression to avoid CRIME and related attacks. + # This constant wasn't added until python 3.3. + context.options |= ssl.OP_NO_COMPRESSION return context diff --git a/libs/tornado/options.py b/libs/tornado/options.py index 1105c0e9..fa9c269e 100755 --- a/libs/tornado/options.py +++ b/libs/tornado/options.py @@ -56,6 +56,18 @@ We support `datetimes `, `timedeltas the top-level functions in this module (`define`, `parse_command_line`, etc) simply call methods on it. You may create additional `OptionParser` instances to define isolated sets of options, such as for subcommands. + +.. note:: + + By default, several options are defined that will configure the + standard `logging` module when `parse_command_line` or `parse_config_file` + are called. If you want Tornado to leave the logging configuration + alone so you can manage it yourself, either pass ``--logging=none`` + on the command line or do the following to disable it in code:: + + from tornado.options import options, parse_command_line + options.logging = None + parse_command_line() """ from __future__ import absolute_import, division, print_function, with_statement @@ -360,6 +372,8 @@ class _Mockable(object): class _Option(object): + UNSET = object() + def __init__(self, name, default=None, type=basestring_type, help=None, metavar=None, multiple=False, file_name=None, group_name=None, callback=None): @@ -374,10 +388,10 @@ class _Option(object): self.group_name = group_name self.callback = callback self.default = default - self._value = None + self._value = _Option.UNSET def value(self): - return self.default if self._value is None else self._value + return self.default if self._value is _Option.UNSET else self._value def parse(self, value): _parse = { diff --git a/libs/tornado/platform/asyncio.py b/libs/tornado/platform/asyncio.py index 162b3673..552476bc 100644 --- a/libs/tornado/platform/asyncio.py +++ b/libs/tornado/platform/asyncio.py @@ -10,21 +10,31 @@ unfinished callbacks on the event loop that fail when it resumes) """ from __future__ import absolute_import, division, print_function, with_statement -import asyncio import datetime import functools -import os -from tornado.ioloop import IOLoop +# _Timeout is used for its timedelta_to_seconds method for py26 compatibility. +from tornado.ioloop import IOLoop, _Timeout from tornado import stack_context +try: + # Import the real asyncio module for py33+ first. Older versions of the + # trollius backport also use this name. + import asyncio +except ImportError as e: + # Asyncio itself isn't available; see if trollius is (backport to py26+). + try: + import trollius as asyncio + except ImportError: + # Re-raise the original asyncio error, not the trollius one. + raise e class BaseAsyncIOLoop(IOLoop): def initialize(self, asyncio_loop, close_loop=False): self.asyncio_loop = asyncio_loop self.close_loop = close_loop self.asyncio_loop.call_soon(self.make_current) - # Maps fd to handler function (as in IOLoop.add_handler) + # Maps fd to (fileobj, handler function) pair (as in IOLoop.add_handler) self.handlers = {} # Set of fds listening for reads/writes self.readers = set() @@ -34,19 +44,18 @@ class BaseAsyncIOLoop(IOLoop): def close(self, all_fds=False): self.closing = True for fd in list(self.handlers): + fileobj, handler_func = self.handlers[fd] self.remove_handler(fd) if all_fds: - try: - os.close(fd) - except OSError: - pass + self.close_fd(fileobj) if self.close_loop: self.asyncio_loop.close() def add_handler(self, fd, handler, events): + fd, fileobj = self.split_fd(fd) if fd in self.handlers: - raise ValueError("fd %d added twice" % fd) - self.handlers[fd] = stack_context.wrap(handler) + raise ValueError("fd %s added twice" % fd) + self.handlers[fd] = (fileobj, stack_context.wrap(handler)) if events & IOLoop.READ: self.asyncio_loop.add_reader( fd, self._handle_events, fd, IOLoop.READ) @@ -57,6 +66,7 @@ class BaseAsyncIOLoop(IOLoop): self.writers.add(fd) def update_handler(self, fd, events): + fd, fileobj = self.split_fd(fd) if events & IOLoop.READ: if fd not in self.readers: self.asyncio_loop.add_reader( @@ -77,6 +87,7 @@ class BaseAsyncIOLoop(IOLoop): self.writers.remove(fd) def remove_handler(self, fd): + fd, fileobj = self.split_fd(fd) if fd not in self.handlers: return if fd in self.readers: @@ -88,7 +99,8 @@ class BaseAsyncIOLoop(IOLoop): del self.handlers[fd] def _handle_events(self, fd, events): - self.handlers[fd](fd, events) + fileobj, handler_func = self.handlers[fd] + handler_func(fileobj, events) def start(self): self._setup_logging() @@ -107,7 +119,7 @@ class BaseAsyncIOLoop(IOLoop): if isinstance(deadline, (int, float)): delay = max(deadline - self.time(), 0) elif isinstance(deadline, datetime.timedelta): - delay = deadline.total_seconds() + delay = _Timeout.timedelta_to_seconds(deadline) else: raise TypeError("Unsupported deadline %r", deadline) return self.asyncio_loop.call_later(delay, self._run_callback, diff --git a/libs/tornado/platform/auto.py b/libs/tornado/platform/auto.py index e55725b3..ddfe06b4 100755 --- a/libs/tornado/platform/auto.py +++ b/libs/tornado/platform/auto.py @@ -30,6 +30,10 @@ import os if os.name == 'nt': from tornado.platform.common import Waker from tornado.platform.windows import set_close_exec +elif 'APPENGINE_RUNTIME' in os.environ: + from tornado.platform.common import Waker + def set_close_exec(fd): + pass else: from tornado.platform.posix import set_close_exec, Waker diff --git a/libs/tornado/platform/common.py b/libs/tornado/platform/common.py index d9c4cf9f..b409a903 100755 --- a/libs/tornado/platform/common.py +++ b/libs/tornado/platform/common.py @@ -15,7 +15,8 @@ class Waker(interface.Waker): and Jython. """ def __init__(self): - # Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py + # Based on Zope select_trigger.py: + # https://github.com/zopefoundation/Zope/blob/master/src/ZServer/medusa/thread/select_trigger.py self.writer = socket.socket() # Disable buffering -- pulling the trigger sends 1 byte, diff --git a/libs/tornado/platform/kqueue.py b/libs/tornado/platform/kqueue.py index ceff0a43..de8c046d 100755 --- a/libs/tornado/platform/kqueue.py +++ b/libs/tornado/platform/kqueue.py @@ -37,7 +37,7 @@ class _KQueue(object): def register(self, fd, events): if fd in self._active: - raise IOError("fd %d already registered" % fd) + raise IOError("fd %s already registered" % fd) self._control(fd, events, select.KQ_EV_ADD) self._active[fd] = events diff --git a/libs/tornado/platform/select.py b/libs/tornado/platform/select.py index 8bbb1f4f..9a879562 100755 --- a/libs/tornado/platform/select.py +++ b/libs/tornado/platform/select.py @@ -37,7 +37,7 @@ class _Select(object): def register(self, fd, events): if fd in self.read_fds or fd in self.write_fds or fd in self.error_fds: - raise IOError("fd %d already registered" % fd) + raise IOError("fd %s already registered" % fd) if events & IOLoop.READ: self.read_fds.add(fd) if events & IOLoop.WRITE: diff --git a/libs/tornado/platform/twisted.py b/libs/tornado/platform/twisted.py index 0c8a3105..889aa3c4 100755 --- a/libs/tornado/platform/twisted.py +++ b/libs/tornado/platform/twisted.py @@ -91,6 +91,11 @@ from tornado.netutil import Resolver from tornado.stack_context import NullContext, wrap from tornado.ioloop import IOLoop +try: + long # py2 +except NameError: + long = int # py3 + @implementer(IDelayedCall) class TornadoDelayedCall(object): @@ -365,8 +370,9 @@ def install(io_loop=None): @implementer(IReadDescriptor, IWriteDescriptor) class _FD(object): - def __init__(self, fd, handler): + def __init__(self, fd, fileobj, handler): self.fd = fd + self.fileobj = fileobj self.handler = handler self.reading = False self.writing = False @@ -377,15 +383,15 @@ class _FD(object): def doRead(self): if not self.lost: - self.handler(self.fd, tornado.ioloop.IOLoop.READ) + self.handler(self.fileobj, tornado.ioloop.IOLoop.READ) def doWrite(self): if not self.lost: - self.handler(self.fd, tornado.ioloop.IOLoop.WRITE) + self.handler(self.fileobj, tornado.ioloop.IOLoop.WRITE) def connectionLost(self, reason): if not self.lost: - self.handler(self.fd, tornado.ioloop.IOLoop.ERROR) + self.handler(self.fileobj, tornado.ioloop.IOLoop.ERROR) self.lost = True def logPrefix(self): @@ -412,14 +418,19 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): self.reactor.callWhenRunning(self.make_current) def close(self, all_fds=False): + fds = self.fds self.reactor.removeAll() for c in self.reactor.getDelayedCalls(): c.cancel() + if all_fds: + for fd in fds.values(): + self.close_fd(fd.fileobj) def add_handler(self, fd, handler, events): if fd in self.fds: - raise ValueError('fd %d added twice' % fd) - self.fds[fd] = _FD(fd, wrap(handler)) + raise ValueError('fd %s added twice' % fd) + fd, fileobj = self.split_fd(fd) + self.fds[fd] = _FD(fd, fileobj, wrap(handler)) if events & tornado.ioloop.IOLoop.READ: self.fds[fd].reading = True self.reactor.addReader(self.fds[fd]) @@ -428,6 +439,7 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): self.reactor.addWriter(self.fds[fd]) def update_handler(self, fd, events): + fd, fileobj = self.split_fd(fd) if events & tornado.ioloop.IOLoop.READ: if not self.fds[fd].reading: self.fds[fd].reading = True @@ -446,6 +458,7 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): self.reactor.removeWriter(self.fds[fd]) def remove_handler(self, fd): + fd, fileobj = self.split_fd(fd) if fd not in self.fds: return self.fds[fd].lost = True diff --git a/libs/tornado/process.py b/libs/tornado/process.py index 942c5c3f..0f38b856 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -21,7 +21,6 @@ the server into multiple processes and managing subprocesses. from __future__ import absolute_import, division, print_function, with_statement import errno -import multiprocessing import os import signal import subprocess @@ -35,6 +34,13 @@ from tornado.iostream import PipeIOStream from tornado.log import gen_log from tornado.platform.auto import set_close_exec from tornado import stack_context +from tornado.util import errno_from_exception + +try: + import multiprocessing +except ImportError: + # Multiprocessing is not availble on Google App Engine. + multiprocessing = None try: long # py2 @@ -44,6 +50,8 @@ except NameError: def cpu_count(): """Returns the number of processors on this machine.""" + if multiprocessing is None: + return 1 try: return multiprocessing.cpu_count() except NotImplementedError: @@ -136,7 +144,7 @@ def fork_processes(num_processes, max_restarts=100): try: pid, status = os.wait() except OSError as e: - if e.errno == errno.EINTR: + if errno_from_exception(e) == errno.EINTR: continue raise if pid not in children: @@ -283,7 +291,7 @@ class Subprocess(object): try: ret_pid, status = os.waitpid(pid, os.WNOHANG) except OSError as e: - if e.args[0] == errno.ECHILD: + if errno_from_exception(e) == errno.ECHILD: return if ret_pid == 0: return diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index 73bfee89..06d7ecfa 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -1,23 +1,23 @@ #!/usr/bin/env python from __future__ import absolute_import, division, print_function, with_statement -from tornado.escape import utf8, _unicode, native_str +from tornado.concurrent import is_future +from tornado.escape import utf8, _unicode from tornado.httpclient import HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy -from tornado.httputil import HTTPHeaders -from tornado.iostream import IOStream, SSLIOStream +from tornado import httputil +from tornado.http1connection import HTTP1Connection, HTTP1ConnectionParameters +from tornado.iostream import StreamClosedError from tornado.netutil import Resolver, OverrideResolver from tornado.log import gen_log from tornado import stack_context -from tornado.util import GzipDecompressor +from tornado.tcpclient import TCPClient import base64 import collections import copy import functools -import os.path import re import socket -import ssl import sys try: @@ -30,7 +30,23 @@ try: except ImportError: import urllib.parse as urlparse # py3 -_DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt' +try: + import ssl +except ImportError: + # ssl is not available on Google App Engine. + ssl = None + +try: + import certifi +except ImportError: + certifi = None + + +def _default_ca_certs(): + if certifi is None: + raise Exception("The 'certifi' package is required to use https " + "in simple_httpclient") + return certifi.where() class SimpleAsyncHTTPClient(AsyncHTTPClient): @@ -47,7 +63,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): """ def initialize(self, io_loop, max_clients=10, hostname_mapping=None, max_buffer_size=104857600, - resolver=None, defaults=None): + resolver=None, defaults=None, max_header_size=None): """Creates a AsyncHTTPClient. Only a single AsyncHTTPClient instance exists per IOLoop @@ -74,6 +90,9 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.active = {} self.waiting = {} self.max_buffer_size = max_buffer_size + self.max_header_size = max_header_size + # TCPClient could create a Resolver for us, but we have to do it + # ourselves to support hostname_mapping. if resolver: self.resolver = resolver self.own_resolver = False @@ -83,11 +102,13 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): if hostname_mapping is not None: self.resolver = OverrideResolver(resolver=self.resolver, mapping=hostname_mapping) + self.tcp_client = TCPClient(resolver=self.resolver, io_loop=io_loop) def close(self): super(SimpleAsyncHTTPClient, self).close() if self.own_resolver: self.resolver.close() + self.tcp_client.close() def fetch_impl(self, request, callback): key = object() @@ -119,7 +140,8 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _handle_request(self, request, release_callback, final_callback): _HTTPConnection(self.io_loop, self, request, release_callback, - final_callback, self.max_buffer_size, self.resolver) + final_callback, self.max_buffer_size, self.tcp_client, + self.max_header_size) def _release_fetch(self, key): del self.active[key] @@ -142,11 +164,12 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): del self.waiting[key] -class _HTTPConnection(object): +class _HTTPConnection(httputil.HTTPMessageDelegate): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) def __init__(self, io_loop, client, request, release_callback, - final_callback, max_buffer_size, resolver): + final_callback, max_buffer_size, tcp_client, + max_header_size): self.start_time = io_loop.time() self.io_loop = io_loop self.client = client @@ -154,13 +177,15 @@ class _HTTPConnection(object): self.release_callback = release_callback self.final_callback = final_callback self.max_buffer_size = max_buffer_size - self.resolver = resolver + self.tcp_client = tcp_client + self.max_header_size = max_header_size self.code = None self.headers = None - self.chunks = None + self.chunks = [] self._decompressor = None # Timeout handle returned by IOLoop.add_timeout self._timeout = None + self._sockaddr = None with stack_context.ExceptionStackContext(self._handle_exception): self.parsed = urlparse.urlsplit(_unicode(self.request.url)) if self.parsed.scheme not in ("http", "https"): @@ -183,42 +208,31 @@ class _HTTPConnection(object): host = host[1:-1] self.parsed_hostname = host # save final host for _on_connect - if request.allow_ipv6: - af = socket.AF_UNSPEC - else: - # We only try the first IP we get from getaddrinfo, - # so restrict to ipv4 by default. + if request.allow_ipv6 is False: af = socket.AF_INET + else: + af = socket.AF_UNSPEC + + ssl_options = self._get_ssl_options(self.parsed.scheme) timeout = min(self.request.connect_timeout, self.request.request_timeout) if timeout: self._timeout = self.io_loop.add_timeout( self.start_time + timeout, stack_context.wrap(self._on_timeout)) - self.resolver.resolve(host, port, af, callback=self._on_resolve) + self.tcp_client.connect(host, port, af=af, + ssl_options=ssl_options, + callback=self._on_connect) - def _on_resolve(self, addrinfo): - if self.final_callback is None: - # final_callback is cleared if we've hit our timeout - return - self.stream = self._create_stream(addrinfo) - self.stream.set_close_callback(self._on_close) - # ipv6 addresses are broken (in self.parsed.hostname) until - # 2.7, here is correctly parsed value calculated in __init__ - sockaddr = addrinfo[0][1] - self.stream.connect(sockaddr, self._on_connect, - server_hostname=self.parsed_hostname) - - def _create_stream(self, addrinfo): - af = addrinfo[0][0] - if self.parsed.scheme == "https": + def _get_ssl_options(self, scheme): + if scheme == "https": ssl_options = {} if self.request.validate_cert: ssl_options["cert_reqs"] = ssl.CERT_REQUIRED if self.request.ca_certs is not None: ssl_options["ca_certs"] = self.request.ca_certs else: - ssl_options["ca_certs"] = _DEFAULT_CA_CERTS + ssl_options["ca_certs"] = _default_ca_certs() if self.request.client_key is not None: ssl_options["keyfile"] = self.request.client_key if self.request.client_cert is not None: @@ -236,21 +250,16 @@ class _HTTPConnection(object): # but nearly all servers support both SSLv3 and TLSv1: # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html if sys.version_info >= (2, 7): - ssl_options["ciphers"] = "DEFAULT:!SSLv2" + # In addition to disabling SSLv2, we also exclude certain + # classes of insecure ciphers. + ssl_options["ciphers"] = "DEFAULT:!SSLv2:!EXPORT:!DES" else: # This is really only necessary for pre-1.0 versions # of openssl, but python 2.6 doesn't expose version # information. ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 - - return SSLIOStream(socket.socket(af), - io_loop=self.io_loop, - ssl_options=ssl_options, - max_buffer_size=self.max_buffer_size) - else: - return IOStream(socket.socket(af), - io_loop=self.io_loop, - max_buffer_size=self.max_buffer_size) + return ssl_options + return None def _on_timeout(self): self._timeout = None @@ -262,7 +271,13 @@ class _HTTPConnection(object): self.io_loop.remove_timeout(self._timeout) self._timeout = None - def _on_connect(self): + def _on_connect(self, stream): + if self.final_callback is None: + # final_callback is cleared if we've hit our timeout. + stream.close() + return + self.stream = stream + self.stream.set_close_callback(self._on_close) self._remove_timeout() if self.final_callback is None: return @@ -302,16 +317,22 @@ class _HTTPConnection(object): self.request.headers["User-Agent"] = self.request.user_agent if not self.request.allow_nonstandard_methods: if self.request.method in ("POST", "PATCH", "PUT"): - if self.request.body is None: + if (self.request.body is None and + self.request.body_producer is None): raise AssertionError( 'Body must not be empty for "%s" request' % self.request.method) else: - if self.request.body is not None: + if (self.request.body is not None or + self.request.body_producer is not None): raise AssertionError( 'Body must be empty for "%s" request' % self.request.method) + if self.request.expect_100_continue: + self.request.headers["Expect"] = "100-continue" if self.request.body is not None: + # When body_producer is used the caller is responsible for + # setting Content-Length (or else chunked encoding will be used). self.request.headers["Content-Length"] = str(len( self.request.body)) if (self.request.method == "POST" and @@ -320,20 +341,47 @@ class _HTTPConnection(object): if self.request.use_gzip: self.request.headers["Accept-Encoding"] = "gzip" req_path = ((self.parsed.path or '/') + - (('?' + self.parsed.query) if self.parsed.query else '')) - request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method, - req_path))] - for k, v in self.request.headers.get_all(): - line = utf8(k) + b": " + utf8(v) - if b'\n' in line: - raise ValueError('Newline in header: ' + repr(line)) - request_lines.append(line) - request_str = b"\r\n".join(request_lines) + b"\r\n\r\n" - if self.request.body is not None: - request_str += self.request.body + (('?' + self.parsed.query) if self.parsed.query else '')) self.stream.set_nodelay(True) - self.stream.write(request_str) - self.stream.read_until_regex(b"\r?\n\r?\n", self._on_headers) + self.connection = HTTP1Connection( + self.stream, True, + HTTP1ConnectionParameters( + no_keep_alive=True, + max_header_size=self.max_header_size, + use_gzip=self.request.use_gzip), + self._sockaddr) + start_line = httputil.RequestStartLine(self.request.method, + req_path, 'HTTP/1.1') + self.connection.write_headers(start_line, self.request.headers) + if self.request.expect_100_continue: + self._read_response() + else: + self._write_body(True) + + def _write_body(self, start_read): + if self.request.body is not None: + self.connection.write(self.request.body) + self.connection.finish() + elif self.request.body_producer is not None: + fut = self.request.body_producer(self.connection.write) + if is_future(fut): + def on_body_written(fut): + fut.result() + self.connection.finish() + if start_read: + self._read_response() + self.io_loop.add_future(fut, on_body_written) + return + self.connection.finish() + if start_read: + self._read_response() + + def _read_response(self): + # Ensure that any exception raised in read_response ends up in our + # stack context. + self.io_loop.add_future( + self.connection.read_response(self), + lambda f: f.result()) def _release(self): if self.release_callback is not None: @@ -351,43 +399,39 @@ class _HTTPConnection(object): def _handle_exception(self, typ, value, tb): if self.final_callback: self._remove_timeout() + if isinstance(value, StreamClosedError): + value = HTTPError(599, "Stream closed") self._run_callback(HTTPResponse(self.request, 599, error=value, request_time=self.io_loop.time() - self.start_time, )) if hasattr(self, "stream"): + # TODO: this may cause a StreamClosedError to be raised + # by the connection's Future. Should we cancel the + # connection more gracefully? self.stream.close() return True else: # If our callback has already been called, we are probably # catching an exception that is not caused by us but rather # some child of our callback. Rather than drop it on the floor, - # pass it along. - return False + # pass it along, unless it's just the stream being closed. + return isinstance(value, StreamClosedError) def _on_close(self): if self.final_callback is not None: message = "Connection closed" if self.stream.error: - message = str(self.stream.error) + raise self.stream.error raise HTTPError(599, message) - def _handle_1xx(self, code): - self.stream.read_until_regex(b"\r?\n\r?\n", self._on_headers) - - def _on_headers(self, data): - data = native_str(data.decode("latin1")) - first_line, _, header_data = data.partition("\n") - match = re.match("HTTP/1.[01] ([0-9]+) ([^\r]*)", first_line) - assert match - code = int(match.group(1)) - self.headers = HTTPHeaders.parse(header_data) - if 100 <= code < 200: - self._handle_1xx(code) + def headers_received(self, first_line, headers): + if self.request.expect_100_continue and first_line.code == 100: + self._write_body(False) return - else: - self.code = code - self.reason = match.group(2) + self.headers = headers + self.code = first_line.code + self.reason = first_line.reason if "Content-Length" in self.headers: if "," in self.headers["Content-Length"]: @@ -404,17 +448,12 @@ class _HTTPConnection(object): content_length = None if self.request.header_callback is not None: - # re-attach the newline we split on earlier - self.request.header_callback(first_line + _) + # Reassemble the start line. + self.request.header_callback('%s %s %s\r\n' % first_line) for k, v in self.headers.get_all(): self.request.header_callback("%s: %s\r\n" % (k, v)) self.request.header_callback('\r\n') - if self.request.method == "HEAD" or self.code == 304: - # HEAD requests and 304 responses never have content, even - # though they may have content-length headers - self._on_body(b"") - return if 100 <= self.code < 200 or self.code == 204: # These response codes never have bodies # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 @@ -422,21 +461,9 @@ class _HTTPConnection(object): content_length not in (None, 0)): raise ValueError("Response with code %d should not have body" % self.code) - self._on_body(b"") - return - if (self.request.use_gzip and - self.headers.get("Content-Encoding") == "gzip"): - self._decompressor = GzipDecompressor() - if self.headers.get("Transfer-Encoding") == "chunked": - self.chunks = [] - self.stream.read_until(b"\r\n", self._on_chunk_length) - elif content_length is not None: - self.stream.read_bytes(content_length, self._on_body) - else: - self.stream.read_until_close(self._on_body) - - def _on_body(self, data): + def finish(self): + data = b''.join(self.chunks) self._remove_timeout() original_request = getattr(self.request, "original_request", self.request) @@ -472,19 +499,12 @@ class _HTTPConnection(object): self.client.fetch(new_request, final_callback) self._on_end_request() return - if self._decompressor: - data = (self._decompressor.decompress(data) + - self._decompressor.flush()) if self.request.streaming_callback: - if self.chunks is None: - # if chunks is not None, we already called streaming_callback - # in _on_chunk_data - self.request.streaming_callback(data) buffer = BytesIO() else: buffer = BytesIO(data) # TODO: don't require one big string? response = HTTPResponse(original_request, - self.code, reason=self.reason, + self.code, reason=getattr(self, 'reason', None), headers=self.headers, request_time=self.io_loop.time() - self.start_time, buffer=buffer, @@ -495,40 +515,11 @@ class _HTTPConnection(object): def _on_end_request(self): self.stream.close() - def _on_chunk_length(self, data): - # TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1 - length = int(data.strip(), 16) - if length == 0: - if self._decompressor is not None: - tail = self._decompressor.flush() - if tail: - # I believe the tail will always be empty (i.e. - # decompress will return all it can). The purpose - # of the flush call is to detect errors such - # as truncated input. But in case it ever returns - # anything, treat it as an extra chunk - if self.request.streaming_callback is not None: - self.request.streaming_callback(tail) - else: - self.chunks.append(tail) - # all the data has been decompressed, so we don't need to - # decompress again in _on_body - self._decompressor = None - self._on_body(b''.join(self.chunks)) - else: - self.stream.read_bytes(length + 2, # chunk ends with \r\n - self._on_chunk_data) - - def _on_chunk_data(self, data): - assert data[-2:] == b"\r\n" - chunk = data[:-2] - if self._decompressor: - chunk = self._decompressor.decompress(chunk) + def data_received(self, chunk): if self.request.streaming_callback is not None: self.request.streaming_callback(chunk) else: self.chunks.append(chunk) - self.stream.read_until(b"\r\n", self._on_chunk_length) if __name__ == "__main__": diff --git a/libs/tornado/speedups.c b/libs/tornado/speedups.c index 8a316c58..174a6129 100644 --- a/libs/tornado/speedups.c +++ b/libs/tornado/speedups.c @@ -1,21 +1,24 @@ +#define PY_SSIZE_T_CLEAN #include static PyObject* websocket_mask(PyObject* self, PyObject* args) { const char* mask; - int mask_len; + Py_ssize_t mask_len; const char* data; - int data_len; - int i; + Py_ssize_t data_len; + Py_ssize_t i; + PyObject* result; + char* buf; if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) { return NULL; } - PyObject* result = PyBytes_FromStringAndSize(NULL, data_len); + result = PyBytes_FromStringAndSize(NULL, data_len); if (!result) { return NULL; } - char* buf = PyBytes_AsString(result); + buf = PyBytes_AsString(result); for (i = 0; i < data_len; i++) { buf[i] = data[i] ^ mask[i % 4]; } diff --git a/libs/tornado/stack_context.py b/libs/tornado/stack_context.py index b1e82b0e..2e845ab2 100755 --- a/libs/tornado/stack_context.py +++ b/libs/tornado/stack_context.py @@ -266,6 +266,18 @@ def wrap(fn): # TODO: Any other better way to store contexts and update them in wrapped function? cap_contexts = [_state.contexts] + if not cap_contexts[0][0] and not cap_contexts[0][1]: + # Fast path when there are no active contexts. + def null_wrapper(*args, **kwargs): + try: + current_state = _state.contexts + _state.contexts = cap_contexts[0] + return fn(*args, **kwargs) + finally: + _state.contexts = current_state + null_wrapper._wrapped = True + return null_wrapper + def wrapped(*args, **kwargs): ret = None try: diff --git a/libs/tornado/tcpclient.py b/libs/tornado/tcpclient.py new file mode 100644 index 00000000..d49eb5cd --- /dev/null +++ b/libs/tornado/tcpclient.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +# +# Copyright 2014 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A non-blocking TCP connection factory. +""" +from __future__ import absolute_import, division, print_function, with_statement + +import functools +import socket + +from tornado.concurrent import Future +from tornado.ioloop import IOLoop +from tornado.iostream import IOStream +from tornado import gen +from tornado.netutil import Resolver + +_INITIAL_CONNECT_TIMEOUT = 0.3 + + +class _Connector(object): + """A stateless implementation of the "Happy Eyeballs" algorithm. + + "Happy Eyeballs" is documented in RFC6555 as the recommended practice + for when both IPv4 and IPv6 addresses are available. + + In this implementation, we partition the addresses by family, and + make the first connection attempt to whichever address was + returned first by ``getaddrinfo``. If that connection fails or + times out, we begin a connection in parallel to the first address + of the other family. If there are additional failures we retry + with other addresses, keeping one connection attempt per family + in flight at a time. + + http://tools.ietf.org/html/rfc6555 + + """ + def __init__(self, addrinfo, io_loop, connect): + self.io_loop = io_loop + self.connect = connect + + self.future = Future() + self.timeout = None + self.last_error = None + self.remaining = len(addrinfo) + self.primary_addrs, self.secondary_addrs = self.split(addrinfo) + + @staticmethod + def split(addrinfo): + """Partition the ``addrinfo`` list by address family. + + Returns two lists. The first list contains the first entry from + ``addrinfo`` and all others with the same family, and the + second list contains all other addresses (normally one list will + be AF_INET and the other AF_INET6, although non-standard resolvers + may return additional families). + """ + primary = [] + secondary = [] + primary_af = addrinfo[0][0] + for af, addr in addrinfo: + if af == primary_af: + primary.append((af, addr)) + else: + secondary.append((af, addr)) + return primary, secondary + + def start(self, timeout=_INITIAL_CONNECT_TIMEOUT): + self.try_connect(iter(self.primary_addrs)) + self.set_timout(timeout) + return self.future + + def try_connect(self, addrs): + try: + af, addr = next(addrs) + except StopIteration: + # We've reached the end of our queue, but the other queue + # might still be working. Send a final error on the future + # only when both queues are finished. + if self.remaining == 0 and not self.future.done(): + self.future.set_exception(self.last_error or + IOError("connection failed")) + return + future = self.connect(af, addr) + future.add_done_callback(functools.partial(self.on_connect_done, + addrs, af, addr)) + + def on_connect_done(self, addrs, af, addr, future): + self.remaining -= 1 + try: + stream = future.result() + except Exception as e: + if self.future.done(): + return + # Error: try again (but remember what happened so we have an + # error to raise in the end) + self.last_error = e + self.try_connect(addrs) + if self.timeout is not None: + # If the first attempt failed, don't wait for the + # timeout to try an address from the secondary queue. + self.on_timeout() + return + self.clear_timeout() + if self.future.done(): + # This is a late arrival; just drop it. + stream.close() + else: + self.future.set_result((af, addr, stream)) + + def set_timout(self, timeout): + self.timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, + self.on_timeout) + + def on_timeout(self): + self.timeout = None + self.try_connect(iter(self.secondary_addrs)) + + def clear_timeout(self): + if self.timeout is not None: + self.io_loop.remove_timeout(self.timeout) + + +class TCPClient(object): + """A non-blocking TCP connection factory. + """ + def __init__(self, resolver=None, io_loop=None): + self.io_loop = io_loop or IOLoop.current() + if resolver is not None: + self.resolver = resolver + self._own_resolver = False + else: + self.resolver = Resolver(io_loop=io_loop) + self._own_resolver = True + + def close(self): + if self._own_resolver: + self.resolver.close() + + @gen.coroutine + def connect(self, host, port, af=socket.AF_UNSPEC, ssl_options=None, + max_buffer_size=None): + """Connect to the given host and port. + + Asynchronously returns an `.IOStream` (or `.SSLIOStream` if + ``ssl_options`` is not None). + """ + addrinfo = yield self.resolver.resolve(host, port, af) + connector = _Connector( + addrinfo, self.io_loop, + functools.partial(self._create_stream, max_buffer_size)) + af, addr, stream = yield connector.start() + # TODO: For better performance we could cache the (af, addr) + # information here and re-use it on sbusequent connections to + # the same host. (http://tools.ietf.org/html/rfc6555#section-4.2) + if ssl_options is not None: + stream = yield stream.start_tls(False, ssl_options=ssl_options, + server_hostname=host) + raise gen.Return(stream) + + def _create_stream(self, max_buffer_size, af, addr): + # Always connect in plaintext; we'll convert to ssl if necessary + # after one connection has completed. + stream = IOStream(socket.socket(af), + io_loop=self.io_loop, + max_buffer_size=max_buffer_size) + return stream.connect(addr) diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index c0773732..427acec5 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -20,13 +20,19 @@ from __future__ import absolute_import, division, print_function, with_statement import errno import os import socket -import ssl from tornado.log import app_log from tornado.ioloop import IOLoop from tornado.iostream import IOStream, SSLIOStream from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket from tornado import process +from tornado.util import errno_from_exception + +try: + import ssl +except ImportError: + # ssl is not available on Google App Engine. + ssl = None class TCPServer(object): @@ -81,13 +87,15 @@ class TCPServer(object): .. versionadded:: 3.1 The ``max_buffer_size`` argument. """ - def __init__(self, io_loop=None, ssl_options=None, max_buffer_size=None): + def __init__(self, io_loop=None, ssl_options=None, max_buffer_size=None, + read_chunk_size=None): self.io_loop = io_loop self.ssl_options = ssl_options self._sockets = {} # fd -> socket object self._pending_sockets = [] self._started = False self.max_buffer_size = max_buffer_size + self.read_chunk_size = None # Verify the SSL options. Otherwise we don't get errors until clients # connect. This doesn't verify that the keys are legitimate, but @@ -230,16 +238,20 @@ class TCPServer(object): # catch another error later on (AttributeError in # SSLIOStream._do_ssl_handshake). # To test this behavior, try nmap with the -sT flag. - # https://github.com/facebook/tornado/pull/750 - if err.args[0] in (errno.ECONNABORTED, errno.EINVAL): + # https://github.com/tornadoweb/tornado/pull/750 + if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL): return connection.close() else: raise try: if self.ssl_options is not None: - stream = SSLIOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size) + stream = SSLIOStream(connection, io_loop=self.io_loop, + max_buffer_size=self.max_buffer_size, + read_chunk_size=self.read_chunk_size) else: - stream = IOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size) + stream = IOStream(connection, io_loop=self.io_loop, + max_buffer_size=self.max_buffer_size, + read_chunk_size=self.read_chunk_size) self.handle_stream(stream, address) except Exception: app_log.error("Error in connection callback", exc_info=True) diff --git a/libs/tornado/template.py b/libs/tornado/template.py index db5a528d..4dcec5d5 100755 --- a/libs/tornado/template.py +++ b/libs/tornado/template.py @@ -180,7 +180,7 @@ with ``{# ... #}``. ``{% set *x* = *y* %}`` Sets a local variable. -``{% try %}...{% except %}...{% finally %}...{% else %}...{% end %}`` +``{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}`` Same as the python ``try`` statement. ``{% while *condition* %}... {% end %}`` @@ -367,10 +367,9 @@ class Loader(BaseLoader): def _create_template(self, name): path = os.path.join(self.root, name) - f = open(path, "rb") - template = Template(f.read(), name=name, loader=self) - f.close() - return template + with open(path, "rb") as f: + template = Template(f.read(), name=name, loader=self) + return template class DictLoader(BaseLoader): @@ -785,7 +784,7 @@ def _parse(reader, template, in_block=None, in_loop=None): if allowed_parents is not None: if not in_block: raise ParseError("%s outside %s block" % - (operator, allowed_parents)) + (operator, allowed_parents)) if in_block not in allowed_parents: raise ParseError("%s block cannot be attached to %s block" % (operator, in_block)) body.chunks.append(_IntermediateControlBlock(contents, line)) diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py index 8355dcfc..dc30e94f 100755 --- a/libs/tornado/testing.py +++ b/libs/tornado/testing.py @@ -17,7 +17,7 @@ try: from tornado.httpclient import AsyncHTTPClient from tornado.httpserver import HTTPServer from tornado.simple_httpclient import SimpleAsyncHTTPClient - from tornado.ioloop import IOLoop + from tornado.ioloop import IOLoop, TimeoutError from tornado import netutil except ImportError: # These modules are not importable on app engine. Parts of this module @@ -38,6 +38,7 @@ import re import signal import socket import sys +import types try: from cStringIO import StringIO # py2 @@ -48,10 +49,16 @@ except ImportError: # (either py27+ or unittest2) so tornado.test.util enforces # this requirement, but for other users of tornado.testing we want # to allow the older version if unitest2 is not available. -try: - import unittest2 as unittest -except ImportError: +if sys.version_info >= (3,): + # On python 3, mixing unittest2 and unittest (including doctest) + # doesn't seem to work, so always use unittest. import unittest +else: + # On python 2, prefer unittest2 when available. + try: + import unittest2 as unittest + except ImportError: + import unittest _next_port = 10000 @@ -95,6 +102,36 @@ def get_async_test_timeout(): return 5 +class _TestMethodWrapper(object): + """Wraps a test method to raise an error if it returns a value. + + This is mainly used to detect undecorated generators (if a test + method yields it must use a decorator to consume the generator), + but will also detect other kinds of return values (these are not + necessarily errors, but we alert anyway since there is no good + reason to return a value from a test. + """ + def __init__(self, orig_method): + self.orig_method = orig_method + + def __call__(self): + result = self.orig_method() + if isinstance(result, types.GeneratorType): + raise TypeError("Generator test methods should be decorated with " + "tornado.testing.gen_test") + elif result is not None: + raise ValueError("Return value from test method ignored: %r" % + result) + + def __getattr__(self, name): + """Proxy all unknown attributes to the original method. + + This is important for some of the decorators in the `unittest` + module, such as `unittest.skipIf`. + """ + return getattr(self.orig_method, name) + + class AsyncTestCase(unittest.TestCase): """`~unittest.TestCase` subclass for testing `.IOLoop`-based asynchronous code. @@ -157,14 +194,20 @@ class AsyncTestCase(unittest.TestCase): self.assertIn("FriendFeed", response.body) self.stop() """ - def __init__(self, *args, **kwargs): - super(AsyncTestCase, self).__init__(*args, **kwargs) + def __init__(self, methodName='runTest', **kwargs): + super(AsyncTestCase, self).__init__(methodName, **kwargs) self.__stopped = False self.__running = False self.__failure = None self.__stop_args = None self.__timeout = None + # It's easy to forget the @gen_test decorator, but if you do + # the test will silently be ignored because nothing will consume + # the generator. Replace the test method with a wrapper that will + # make sure it's not an undecorated generator. + setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName))) + def setUp(self): super(AsyncTestCase, self).setUp() self.io_loop = self.get_new_ioloop() @@ -352,6 +395,7 @@ class AsyncHTTPTestCase(AsyncTestCase): def tearDown(self): self.http_server.stop() + self.io_loop.run_sync(self.http_server.close_all_connections) if (not IOLoop.initialized() or self.http_client.io_loop is not IOLoop.instance()): self.http_client.close() @@ -414,18 +458,50 @@ def gen_test(func=None, timeout=None): .. versionadded:: 3.1 The ``timeout`` argument and ``ASYNC_TEST_TIMEOUT`` environment variable. + + .. versionchanged:: 3.3 + The wrapper now passes along ``*args, **kwargs`` so it can be used + on functions with arguments. """ if timeout is None: timeout = get_async_test_timeout() def wrap(f): - f = gen.coroutine(f) - + # Stack up several decorators to allow us to access the generator + # object itself. In the innermost wrapper, we capture the generator + # and save it in an attribute of self. Next, we run the wrapped + # function through @gen.coroutine. Finally, the coroutine is + # wrapped again to make it synchronous with run_sync. + # + # This is a good case study arguing for either some sort of + # extensibility in the gen decorators or cancellation support. @functools.wraps(f) - def wrapper(self): - return self.io_loop.run_sync( - functools.partial(f, self), timeout=timeout) - return wrapper + def pre_coroutine(self, *args, **kwargs): + result = f(self, *args, **kwargs) + if isinstance(result, types.GeneratorType): + self._test_generator = result + else: + self._test_generator = None + return result + + coro = gen.coroutine(pre_coroutine) + + @functools.wraps(coro) + def post_coroutine(self, *args, **kwargs): + try: + return self.io_loop.run_sync( + functools.partial(coro, self, *args, **kwargs), + timeout=timeout) + except TimeoutError as e: + # run_sync raises an error with an unhelpful traceback. + # If we throw it back into the generator the stack trace + # will be replaced by the point where the test is stopped. + self._test_generator.throw(e) + # In case the test contains an overly broad except clause, + # we may get back here. In this case re-raise the original + # exception, which is better than nothing. + raise + return post_coroutine if func is not None: # Used like: diff --git a/libs/tornado/util.py b/libs/tornado/util.py index a2fba779..7fe5b9b2 100755 --- a/libs/tornado/util.py +++ b/libs/tornado/util.py @@ -33,7 +33,7 @@ class ObjectDict(dict): class GzipDecompressor(object): """Streaming gzip decompressor. - The interface is like that of `zlib.decompressobj` (without the + The interface is like that of `zlib.decompressobj` (without some of the optional arguments, but it understands gzip headers and checksums. """ def __init__(self): @@ -42,14 +42,24 @@ class GzipDecompressor(object): # This works on cpython and pypy, but not jython. self.decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS) - def decompress(self, value): + def decompress(self, value, max_length=None): """Decompress a chunk, returning newly-available data. Some data may be buffered for later processing; `flush` must be called when there is no more input data to ensure that all data was processed. + + If ``max_length`` is given, some input data may be left over + in ``unconsumed_tail``; you must retrieve this value and pass + it back to a future call to `decompress` if it is not empty. """ - return self.decompressobj.decompress(value) + return self.decompressobj.decompress(value, max_length) + + @property + def unconsumed_tail(self): + """Returns the unconsumed portion left over + """ + return self.decompressobj.unconsumed_tail def flush(self): """Return any remaining buffered data not yet returned by decompress. @@ -132,6 +142,24 @@ def exec_in(code, glob, loc=None): """) +def errno_from_exception(e): + """Provides the errno from an Exception object. + + There are cases that the errno attribute was not set so we pull + the errno out of the args but if someone instatiates an Exception + without any args you will get a tuple error. So this function + abstracts all that behavior to give you a safe way to get the + errno. + """ + + if hasattr(e, 'errno'): + return e.errno + elif e.args: + return e.args[0] + else: + return None + + class Configurable(object): """Base class for configurable interfaces. @@ -243,6 +271,16 @@ class ArgReplacer(object): # Not a positional parameter self.arg_pos = None + def get_old_value(self, args, kwargs, default=None): + """Returns the old value of the named argument without replacing it. + + Returns ``default`` if the argument is not present. + """ + if self.arg_pos is not None and len(args) > self.arg_pos: + return args[self.arg_pos] + else: + return kwargs.get(self.name, default) + def replace(self, new_value, args, kwargs): """Replace the named argument in ``args, kwargs`` with ``new_value``. diff --git a/libs/tornado/web.py b/libs/tornado/web.py index b22b11fe..5a875fd9 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -74,9 +74,11 @@ import traceback import types import uuid -from tornado.concurrent import Future +from tornado.concurrent import Future, is_future from tornado import escape +from tornado import gen from tornado import httputil +from tornado import iostream from tornado import locale from tornado.log import access_log, app_log, gen_log from tornado import stack_context @@ -105,6 +107,39 @@ except ImportError: from urllib.parse import urlencode # py3 +MIN_SUPPORTED_SIGNED_VALUE_VERSION = 1 +"""The oldest signed value version supported by this version of Tornado. + +Signed values older than this version cannot be decoded. + +.. versionadded:: 3.2.1 +""" + +MAX_SUPPORTED_SIGNED_VALUE_VERSION = 2 +"""The newest signed value version supported by this version of Tornado. + +Signed values newer than this version cannot be decoded. + +.. versionadded:: 3.2.1 +""" + +DEFAULT_SIGNED_VALUE_VERSION = 2 +"""The signed value version produced by `.RequestHandler.create_signed_value`. + +May be overridden by passing a ``version`` keyword argument. + +.. versionadded:: 3.2.1 +""" + +DEFAULT_SIGNED_VALUE_MIN_VERSION = 1 +"""The oldest signed value accepted by `.RequestHandler.get_secure_cookie`. + +May be overrided by passing a ``min_version`` keyword argument. + +.. versionadded:: 3.2.1 +""" + + class RequestHandler(object): """Subclass this class and define `get()` or `post()` to make a handler. @@ -128,6 +163,7 @@ class RequestHandler(object): self._finished = False self._auto_finish = True self._transforms = None # will be set in _execute + self._prepared_future = None self.path_args = None self.path_kwargs = None self.ui = ObjectDict((n, self._ui_method(m)) for n, m in @@ -141,10 +177,7 @@ class RequestHandler(object): application.ui_modules) self.ui["modules"] = self.ui["_tt_modules"] self.clear() - # Check since connection is not available in WSGI - if getattr(self.request, "connection", None): - self.request.connection.set_close_callback( - self.on_connection_close) + self.request.connection.set_close_callback(self.on_connection_close) self.initialize(**kwargs) def initialize(self): @@ -235,7 +268,9 @@ class RequestHandler(object): may not be called promptly after the end user closes their connection. """ - pass + if _has_stream_request_body(self.__class__): + if not self.request.body.done(): + self.request.body.set_exception(iostream.StreamClosedError()) def clear(self): """Resets all headers and content for this response.""" @@ -245,12 +280,6 @@ class RequestHandler(object): "Date": httputil.format_timestamp(time.time()), }) self.set_default_headers() - if (not self.request.supports_http_1_1() and - getattr(self.request, 'connection', None) and - not self.request.connection.no_keep_alive): - conn_header = self.request.headers.get("Connection") - if conn_header and (conn_header.lower() == "keep-alive"): - self._headers["Connection"] = "Keep-Alive" self._write_buffer = [] self._status_code = 200 self._reason = httputil.responses[200] @@ -455,7 +484,7 @@ class RequestHandler(object): @property def cookies(self): - """An alias for `self.request.cookies <.httpserver.HTTPRequest.cookies>`.""" + """An alias for `self.request.cookies <.httputil.HTTPServerRequest.cookies>`.""" return self.request.cookies def get_cookie(self, name, default=None): @@ -524,7 +553,8 @@ class RequestHandler(object): for name in self.request.cookies: self.clear_cookie(name, path=path, domain=domain) - def set_secure_cookie(self, name, value, expires_days=30, **kwargs): + def set_secure_cookie(self, name, value, expires_days=30, version=None, + **kwargs): """Signs and timestamps a cookie so it cannot be forged. You must specify the ``cookie_secret`` setting in your Application @@ -539,32 +569,50 @@ class RequestHandler(object): Secure cookies may contain arbitrary byte values, not just unicode strings (unlike regular cookies) + + .. versionchanged:: 3.2.1 + + Added the ``version`` argument. Introduced cookie version 2 + and made it the default. """ - self.set_cookie(name, self.create_signed_value(name, value), + self.set_cookie(name, self.create_signed_value(name, value, + version=version), expires_days=expires_days, **kwargs) - def create_signed_value(self, name, value): + def create_signed_value(self, name, value, version=None): """Signs and timestamps a string so it cannot be forged. Normally used via set_secure_cookie, but provided as a separate method for non-cookie uses. To decode a value not stored as a cookie use the optional value argument to get_secure_cookie. + + .. versionchanged:: 3.2.1 + + Added the ``version`` argument. Introduced cookie version 2 + and made it the default. """ self.require_setting("cookie_secret", "secure cookies") return create_signed_value(self.application.settings["cookie_secret"], - name, value) + name, value, version=version) - def get_secure_cookie(self, name, value=None, max_age_days=31): + def get_secure_cookie(self, name, value=None, max_age_days=31, + min_version=None): """Returns the given signed cookie if it validates, or None. The decoded cookie value is returned as a byte string (unlike `get_cookie`). + + .. versionchanged:: 3.2.1 + + Added the ``min_version`` argument. Introduced cookie version 2; + both versions 1 and 2 are accepted by default. """ self.require_setting("cookie_secret", "secure cookies") if value is None: value = self.get_cookie(name) return decode_signed_value(self.application.settings["cookie_secret"], - name, value, max_age_days=max_age_days) + name, value, max_age_days=max_age_days, + min_version=min_version) def redirect(self, url, permanent=False, status=None): """Sends a redirect to the given (optionally relative) URL. @@ -598,12 +646,15 @@ class RequestHandler(object): Note that lists are not converted to JSON because of a potential cross-site security vulnerability. All JSON output should be wrapped in a dictionary. More details at - http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx + http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ and + https://github.com/facebook/tornado/issues/1009 """ if self._finished: raise RuntimeError("Cannot write() after finish(). May be caused " "by using async operations without the " "@asynchronous decorator.") + if not isinstance(chunk, (bytes_type, unicode_type, dict)): + raise TypeError("write() only accepts bytes, unicode, and dict objects") if isinstance(chunk, dict): chunk = escape.json_encode(chunk) self.set_header("Content-Type", "application/json; charset=UTF-8") @@ -769,14 +820,10 @@ class RequestHandler(object): Note that only one flush callback can be outstanding at a time; if another flush occurs before the previous flush's callback has been run, the previous callback will be discarded. - """ - if self.application._wsgi: - # WSGI applications cannot usefully support flush, so just make - # it a no-op (and run the callback immediately). - if callback is not None: - callback() - return + .. versionchanged:: 3.3 + Now returns a `.Future` if no callback is given. + """ chunk = b"".join(self._write_buffer) self._write_buffer = [] if not self._headers_written: @@ -785,19 +832,32 @@ class RequestHandler(object): self._status_code, self._headers, chunk = \ transform.transform_first_chunk( self._status_code, self._headers, chunk, include_footers) - headers = self._generate_headers() + # Ignore the chunk and only write the headers for HEAD requests + if self.request.method == "HEAD": + chunk = None + + # Finalize the cookie headers (which have been stored in a side + # object so an outgoing cookie could be overwritten before it + # is sent). + if hasattr(self, "_new_cookie"): + for cookie in self._new_cookie.values(): + self.add_header("Set-Cookie", cookie.OutputString(None)) + + start_line = httputil.ResponseStartLine(self.request.version, + self._status_code, + self._reason) + return self.request.connection.write_headers( + start_line, self._headers, chunk, callback=callback) else: for transform in self._transforms: chunk = transform.transform_chunk(chunk, include_footers) - headers = b"" - - # Ignore the chunk and only write the headers for HEAD requests - if self.request.method == "HEAD": - if headers: - self.request.write(headers, callback=callback) - return - - self.request.write(headers + chunk, callback=callback) + # Ignore the chunk and only write the headers for HEAD requests + if self.request.method != "HEAD": + return self.request.connection.write(chunk, callback=callback) + else: + future = Future() + future.set_result(None) + return future def finish(self, chunk=None): """Finishes this response, ending the HTTP request.""" @@ -833,10 +893,9 @@ class RequestHandler(object): # are keepalive connections) self.request.connection.set_close_callback(None) - if not self.application._wsgi: - self.flush(include_footers=True) - self.request.finish() - self._log() + self.flush(include_footers=True) + self.request.finish() + self._log() self._finished = True self.on_finish() # Break up a reference cycle between this handler and the @@ -1194,6 +1253,7 @@ class RequestHandler(object): self._handle_request_exception(value) return True + @gen.coroutine def _execute(self, transforms, *args, **kwargs): """Executes this request with the given output transforms.""" self._transforms = transforms @@ -1208,52 +1268,52 @@ class RequestHandler(object): if self.request.method not in ("GET", "HEAD", "OPTIONS") and \ self.application.settings.get("xsrf_cookies"): self.check_xsrf_cookie() - self._when_complete(self.prepare(), self._execute_method) - except Exception as e: - self._handle_request_exception(e) - def _when_complete(self, result, callback): - try: - if result is None: - callback() - elif isinstance(result, Future): - if result.done(): - if result.result() is not None: - raise ValueError('Expected None, got %r' % result.result()) - callback() - else: - # Delayed import of IOLoop because it's not available - # on app engine - from tornado.ioloop import IOLoop - IOLoop.current().add_future( - result, functools.partial(self._when_complete, - callback=callback)) - else: - raise ValueError("Expected Future or None, got %r" % result) - except Exception as e: - self._handle_request_exception(e) + result = self.prepare() + if is_future(result): + result = yield result + if result is not None: + raise TypeError("Expected None, got %r" % result) + if self._prepared_future is not None: + # Tell the Application we've finished with prepare() + # and are ready for the body to arrive. + self._prepared_future.set_result(None) + if self._finished: + return + + if _has_stream_request_body(self.__class__): + # In streaming mode request.body is a Future that signals + # the body has been completely received. The Future has no + # result; the data has been passed to self.data_received + # instead. + try: + yield self.request.body + except iostream.StreamClosedError: + return - def _execute_method(self): - if not self._finished: method = getattr(self, self.request.method.lower()) - self._when_complete(method(*self.path_args, **self.path_kwargs), - self._execute_finish) + result = method(*self.path_args, **self.path_kwargs) + if is_future(result): + result = yield result + if result is not None: + raise TypeError("Expected None, got %r" % result) + if self._auto_finish and not self._finished: + self.finish() + except Exception as e: + self._handle_request_exception(e) + if (self._prepared_future is not None and + not self._prepared_future.done()): + # In case we failed before setting _prepared_future, do it + # now (to unblock the HTTP server). Note that this is not + # in a finally block to avoid GC issues prior to Python 3.4. + self._prepared_future.set_result(None) - def _execute_finish(self): - if self._auto_finish and not self._finished: - self.finish() + def data_received(self, chunk): + """Implement this method to handle streamed request data. - def _generate_headers(self): - reason = self._reason - lines = [utf8(self.request.version + " " + - str(self._status_code) + - " " + reason)] - lines.extend([utf8(n) + b": " + utf8(v) for n, v in self._headers.get_all()]) - - if hasattr(self, "_new_cookie"): - for cookie in self._new_cookie.values(): - lines.append(utf8("Set-Cookie: " + cookie.OutputString(None))) - return b"\r\n".join(lines) + b"\r\n\r\n" + Requires the `.stream_request_body` decorator. + """ + raise NotImplementedError() def _log(self): """Logs the current request. @@ -1367,8 +1427,6 @@ def asynchronous(method): from tornado.ioloop import IOLoop @functools.wraps(method) def wrapper(self, *args, **kwargs): - if self.application._wsgi: - raise Exception("@asynchronous is not supported for WSGI apps") self._auto_finish = False with stack_context.ExceptionStackContext( self._stack_context_handle_exception): @@ -1395,6 +1453,40 @@ def asynchronous(method): return wrapper +def stream_request_body(cls): + """Apply to `RequestHandler` subclasses to enable streaming body support. + + This decorator implies the following changes: + + * `.HTTPServerRequest.body` is undefined, and body arguments will not + be included in `RequestHandler.get_argument`. + * `RequestHandler.prepare` is called when the request headers have been + read instead of after the entire body has been read. + * The subclass must define a method ``data_received(self, data):``, which + will be called zero or more times as data is available. Note that + if the request has an empty body, ``data_received`` may not be called. + * ``prepare`` and ``data_received`` may return Futures (such as via + ``@gen.coroutine``, in which case the next method will not be called + until those futures have completed. + * The regular HTTP method (``post``, ``put``, etc) will be called after + the entire body has been read. + + There is a subtle interaction between ``data_received`` and asynchronous + ``prepare``: The first call to ``data_recieved`` may occur at any point + after the call to ``prepare`` has returned *or yielded*. + """ + if not issubclass(cls, RequestHandler): + raise TypeError("expected subclass of RequestHandler, got %r", cls) + cls._stream_request_body = True + return cls + + +def _has_stream_request_body(cls): + if not issubclass(cls, RequestHandler): + raise TypeError("expected subclass of RequestHandler, got %r", cls) + return getattr(cls, '_stream_request_body', False) + + def removeslash(method): """Use this decorator to remove trailing slashes from the request path. @@ -1439,7 +1531,7 @@ def addslash(method): return wrapper -class Application(object): +class Application(httputil.HTTPServerConnectionDelegate): """A collection of request handlers that make up a web application. Instances of this class are callable and can be passed directly to @@ -1491,12 +1583,11 @@ class Application(object): """ def __init__(self, handlers=None, default_host="", transforms=None, - wsgi=False, **settings): + **settings): if transforms is None: self.transforms = [] if settings.get("gzip"): self.transforms.append(GZipContentEncoding) - self.transforms.append(ChunkedTransferEncoding) else: self.transforms = transforms self.handlers = [] @@ -1508,7 +1599,6 @@ class Application(object): 'Template': TemplateModule, } self.ui_methods = {} - self._wsgi = wsgi self._load_ui_modules(settings.get("ui_modules", {})) self._load_ui_methods(settings.get("ui_methods", {})) if self.settings.get("static_path"): @@ -1534,7 +1624,7 @@ class Application(object): self.settings.setdefault('serve_traceback', True) # Automatically reload modified modules - if self.settings.get('autoreload') and not wsgi: + if self.settings.get('autoreload'): from tornado import autoreload autoreload.start() @@ -1634,64 +1724,15 @@ class Application(object): except TypeError: pass + def start_request(self, connection): + # Modern HTTPServer interface + return _RequestDispatcher(self, connection) + def __call__(self, request): - """Called by HTTPServer to execute the request.""" - transforms = [t(request) for t in self.transforms] - handler = None - args = [] - kwargs = {} - handlers = self._get_host_handlers(request) - if not handlers: - handler = RedirectHandler( - self, request, url="http://" + self.default_host + "/") - else: - for spec in handlers: - match = spec.regex.match(request.path) - if match: - handler = spec.handler_class(self, request, **spec.kwargs) - if spec.regex.groups: - # None-safe wrapper around url_unescape to handle - # unmatched optional groups correctly - def unquote(s): - if s is None: - return s - return escape.url_unescape(s, encoding=None, - plus=False) - # Pass matched groups to the handler. Since - # match.groups() includes both named and unnamed groups, - # we want to use either groups or groupdict but not both. - # Note that args are passed as bytes so the handler can - # decide what encoding to use. - - if spec.regex.groupindex: - kwargs = dict( - (str(k), unquote(v)) - for (k, v) in match.groupdict().items()) - else: - args = [unquote(s) for s in match.groups()] - break - if not handler: - if self.settings.get('default_handler_class'): - handler_class = self.settings['default_handler_class'] - handler_args = self.settings.get( - 'default_handler_args', {}) - else: - handler_class = ErrorHandler - handler_args = dict(status_code=404) - handler = handler_class(self, request, **handler_args) - - # If template cache is disabled (usually in the debug mode), - # re-compile templates and reload static files on every - # request so you don't need to restart to see changes - if not self.settings.get("compiled_template_cache", True): - with RequestHandler._template_loader_lock: - for loader in RequestHandler._template_loaders.values(): - loader.reset() - if not self.settings.get('static_hash_cache', True): - StaticFileHandler.reset() - - handler._execute(transforms, *args, **kwargs) - return handler + # Legacy HTTPServer interface + dispatcher = _RequestDispatcher(self, None) + dispatcher.set_request(request) + return dispatcher.execute() def reverse_url(self, name, *args): """Returns a URL path for handler named ``name`` @@ -1728,6 +1769,113 @@ class Application(object): handler._request_summary(), request_time) +class _RequestDispatcher(httputil.HTTPMessageDelegate): + def __init__(self, application, connection): + self.application = application + self.connection = connection + self.request = None + self.chunks = [] + self.handler_class = None + self.handler_kwargs = None + self.path_args = [] + self.path_kwargs = {} + + def headers_received(self, start_line, headers): + self.set_request(httputil.HTTPServerRequest( + connection=self.connection, start_line=start_line, headers=headers)) + if self.stream_request_body: + self.request.body = Future() + return self.execute() + + def set_request(self, request): + self.request = request + self._find_handler() + self.stream_request_body = _has_stream_request_body(self.handler_class) + + def _find_handler(self): + # Identify the handler to use as soon as we have the request. + # Save url path arguments for later. + app = self.application + handlers = app._get_host_handlers(self.request) + if not handlers: + self.handler_class = RedirectHandler + self.handler_kwargs = dict(url="http://" + app.default_host + "/") + return + for spec in handlers: + match = spec.regex.match(self.request.path) + if match: + self.handler_class = spec.handler_class + self.handler_kwargs = spec.kwargs + if spec.regex.groups: + # Pass matched groups to the handler. Since + # match.groups() includes both named and + # unnamed groups, we want to use either groups + # or groupdict but not both. + if spec.regex.groupindex: + self.path_kwargs = dict( + (str(k), _unquote_or_none(v)) + for (k, v) in match.groupdict().items()) + else: + self.path_args = [_unquote_or_none(s) + for s in match.groups()] + return + if app.settings.get('default_handler_class'): + self.handler_class = app.settings['default_handler_class'] + self.handler_kwargs = app.settings.get( + 'default_handler_args', {}) + else: + self.handler_class = ErrorHandler + self.handler_kwargs = dict(status_code=404) + + def data_received(self, data): + if self.stream_request_body: + return self.handler.data_received(data) + else: + self.chunks.append(data) + + def finish(self): + if self.stream_request_body: + self.request.body.set_result(None) + else: + self.request.body = b''.join(self.chunks) + self.request._parse_body() + self.execute() + + def on_connection_close(self): + if self.stream_request_body: + self.handler.on_connection_close() + else: + self.chunks = None + + def execute(self): + # If template cache is disabled (usually in the debug mode), + # re-compile templates and reload static files on every + # request so you don't need to restart to see changes + if not self.application.settings.get("compiled_template_cache", True): + with RequestHandler._template_loader_lock: + for loader in RequestHandler._template_loaders.values(): + loader.reset() + if not self.application.settings.get('static_hash_cache', True): + StaticFileHandler.reset() + + self.handler = self.handler_class(self.application, self.request, + **self.handler_kwargs) + transforms = [t(self.request) for t in self.application.transforms] + + if self.stream_request_body: + self.handler._prepared_future = Future() + # Note that if an exception escapes handler._execute it will be + # trapped in the Future it returns (which we are ignoring here). + # However, that shouldn't happen because _execute has a blanket + # except handler, and we cannot easily access the IOLoop here to + # call add_future. + self.handler._execute(transforms, *self.path_args, **self.path_kwargs) + # If we are streaming the request body, then execute() is finished + # when the handler has prepared to receive the body. If not, + # it doesn't matter when execute() finishes (so we return None) + return self.handler._prepared_future + + class HTTPError(Exception): """An exception that will turn into an HTTP error response. @@ -1886,8 +2034,9 @@ class StaticFileHandler(RequestHandler): cls._static_hashes = {} def head(self, path): - self.get(path, include_body=False) + return self.get(path, include_body=False) + @gen.coroutine def get(self, path, include_body=True): # Set up our path instance variables. self.path = self.parse_url_path(path) @@ -1912,9 +2061,9 @@ class StaticFileHandler(RequestHandler): # the request will be treated as if the header didn't exist. request_range = httputil._parse_request_range(range_header) + size = self.get_content_size() if request_range: start, end = request_range - size = self.get_content_size() if (start is not None and start >= size) or end == 0: # As per RFC 2616 14.35.1, a range is not satisfiable only: if # the first requested byte is equal to or greater than the @@ -1939,18 +2088,26 @@ class StaticFileHandler(RequestHandler): httputil._get_content_range(start, end, size)) else: start = end = None - content = self.get_content(self.absolute_path, start, end) - if isinstance(content, bytes_type): - content = [content] - content_length = 0 - for chunk in content: - if include_body: + + if start is not None and end is not None: + content_length = end - start + elif end is not None: + content_length = end + elif start is not None: + content_length = size - start + else: + content_length = size + self.set_header("Content-Length", content_length) + + if include_body: + content = self.get_content(self.absolute_path, start, end) + if isinstance(content, bytes_type): + content = [content] + for chunk in content: self.write(chunk) - else: - content_length += len(chunk) - if not include_body: + yield self.flush() + else: assert self.request.method == "HEAD" - self.set_header("Content-Length", content_length) def compute_etag(self): """Sets the ``Etag`` header based on static url version. @@ -2130,10 +2287,13 @@ class StaticFileHandler(RequestHandler): def get_content_size(self): """Retrieve the total size of the resource at the given path. - This method may be overridden by subclasses. It will only - be called if a partial result is requested from `get_content` + This method may be overridden by subclasses. .. versionadded:: 3.1 + + .. versionchanged:: 3.3 + This method is now always called, instead of only when + partial results are requested. """ stat_result = self._stat() return stat_result[stat.ST_SIZE] @@ -2255,7 +2415,7 @@ class FallbackHandler(RequestHandler): """A `RequestHandler` that wraps another HTTP server callback. The fallback is a callable object that accepts an - `~.httpserver.HTTPRequest`, such as an `Application` or + `~.httputil.HTTPServerRequest`, such as an `Application` or `tornado.wsgi.WSGIContainer`. This is most useful to use both Tornado ``RequestHandlers`` and WSGI in the same server. Typical usage:: @@ -2279,7 +2439,7 @@ class OutputTransform(object): """A transform modifies the result of an HTTP request (e.g., GZip encoding) A new transform instance is created for every request. See the - ChunkedTransferEncoding example below if you want to implement a + GZipContentEncoding example below if you want to implement a new Transform. """ def __init__(self, request): @@ -2296,16 +2456,24 @@ class GZipContentEncoding(OutputTransform): """Applies the gzip content encoding to the response. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 + + .. versionchanged:: 3.3 + Now compresses all mime types beginning with ``text/``, instead + of just a whitelist. (the whitelist is still used for certain + non-text mime types). """ - CONTENT_TYPES = set([ - "text/plain", "text/html", "text/css", "text/xml", "application/javascript", - "application/x-javascript", "application/xml", "application/atom+xml", - "text/javascript", "application/json", "application/xhtml+xml"]) + # Whitelist of compressible mime types (in addition to any types + # beginning with "text/"). + CONTENT_TYPES = set(["application/javascript", "application/x-javascript", + "application/xml", "application/atom+xml", + "application/json", "application/xhtml+xml"]) MIN_LENGTH = 5 def __init__(self, request): - self._gzipping = request.supports_http_1_1() and \ - "gzip" in request.headers.get("Accept-Encoding", "") + self._gzipping = "gzip" in request.headers.get("Accept-Encoding", "") + + def _compressible_type(self, ctype): + return ctype.startswith('text/') or ctype in self.CONTENT_TYPES def transform_first_chunk(self, status_code, headers, chunk, finishing): if 'Vary' in headers: @@ -2314,7 +2482,7 @@ class GZipContentEncoding(OutputTransform): headers['Vary'] = b'Accept-Encoding' if self._gzipping: ctype = _unicode(headers.get("Content-Type", "")).split(";")[0] - self._gzipping = (ctype in self.CONTENT_TYPES) and \ + self._gzipping = self._compressible_type(ctype) and \ (not finishing or len(chunk) >= self.MIN_LENGTH) and \ (finishing or "Content-Length" not in headers) and \ ("Content-Encoding" not in headers) @@ -2340,42 +2508,16 @@ class GZipContentEncoding(OutputTransform): return chunk -class ChunkedTransferEncoding(OutputTransform): - """Applies the chunked transfer encoding to the response. - - See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 - """ - def __init__(self, request): - self._chunking = request.supports_http_1_1() - - def transform_first_chunk(self, status_code, headers, chunk, finishing): - # 304 responses have no body (not even a zero-length body), and so - # should not have either Content-Length or Transfer-Encoding headers. - if self._chunking and status_code != 304: - # No need to chunk the output if a Content-Length is specified - if "Content-Length" in headers or "Transfer-Encoding" in headers: - self._chunking = False - else: - headers["Transfer-Encoding"] = "chunked" - chunk = self.transform_chunk(chunk, finishing) - return status_code, headers, chunk - - def transform_chunk(self, block, finishing): - if self._chunking: - # Don't write out empty chunks because that means END-OF-STREAM - # with chunked encoding - if block: - block = utf8("%x" % len(block)) + b"\r\n" + block + b"\r\n" - if finishing: - block += b"0\r\n\r\n" - return block - - def authenticated(method): """Decorate methods with this to require that the user be logged in. If the user is not logged in, they will be redirected to the configured `login url `. + + If you configure a login url with a query parameter, Tornado will + assume you know what you're doing and use it as-is. If not, it + will add a `next` parameter so the login page knows where to send + you once you're logged in. """ @functools.wraps(method) def wrapper(self, *args, **kwargs): @@ -2640,29 +2782,103 @@ else: return result == 0 -def create_signed_value(secret, name, value): - timestamp = utf8(str(int(time.time()))) +def create_signed_value(secret, name, value, version=None, clock=None): + if version is None: + version = DEFAULT_SIGNED_VALUE_VERSION + if clock is None: + clock = time.time + timestamp = utf8(str(int(clock()))) value = base64.b64encode(utf8(value)) - signature = _create_signature(secret, name, value, timestamp) - value = b"|".join([value, timestamp, signature]) - return value + if version == 1: + signature = _create_signature_v1(secret, name, value, timestamp) + value = b"|".join([value, timestamp, signature]) + return value + elif version == 2: + # The v2 format consists of a version number and a series of + # length-prefixed fields "%d:%s", the last of which is a + # signature, all separated by pipes. All numbers are in + # decimal format with no leading zeros. The signature is an + # HMAC-SHA256 of the whole string up to that point, including + # the final pipe. + # + # The fields are: + # - format version (i.e. 2; no length prefix) + # - key version (currently 0; reserved for future key rotation features) + # - timestamp (integer seconds since epoch) + # - name (not encoded; assumed to be ~alphanumeric) + # - value (base64-encoded) + # - signature (hex-encoded; no length prefix) + def format_field(s): + return utf8("%d:" % len(s)) + utf8(s) + to_sign = b"|".join([ + b"2|1:0", + format_field(timestamp), + format_field(name), + format_field(value), + b'']) + signature = _create_signature_v2(secret, to_sign) + return to_sign + signature + else: + raise ValueError("Unsupported version %d" % version) + +# A leading version number in decimal with no leading zeros, followed by a pipe. +_signed_value_version_re = re.compile(br"^([1-9][0-9]*)\|(.*)$") -def decode_signed_value(secret, name, value, max_age_days=31): +def decode_signed_value(secret, name, value, max_age_days=31, clock=None, min_version=None): + if clock is None: + clock = time.time + if min_version is None: + min_version = DEFAULT_SIGNED_VALUE_MIN_VERSION + if min_version > 2: + raise ValueError("Unsupported min_version %d" % min_version) if not value: return None + + # Figure out what version this is. Version 1 did not include an + # explicit version field and started with arbitrary base64 data, + # which makes this tricky. + value = utf8(value) + m = _signed_value_version_re.match(value) + if m is None: + version = 1 + else: + try: + version = int(m.group(1)) + if version > 999: + # Certain payloads from the version-less v1 format may + # be parsed as valid integers. Due to base64 padding + # restrictions, this can only happen for numbers whose + # length is a multiple of 4, so we can treat all + # numbers up to 999 as versions, and for the rest we + # fall back to v1 format. + version = 1 + except ValueError: + version = 1 + + if version < min_version: + return None + if version == 1: + return _decode_signed_value_v1(secret, name, value, max_age_days, clock) + elif version == 2: + return _decode_signed_value_v2(secret, name, value, max_age_days, clock) + else: + return None + + +def _decode_signed_value_v1(secret, name, value, max_age_days, clock): parts = utf8(value).split(b"|") if len(parts) != 3: return None - signature = _create_signature(secret, name, parts[0], parts[1]) + signature = _create_signature_v1(secret, name, parts[0], parts[1]) if not _time_independent_equals(parts[2], signature): gen_log.warning("Invalid cookie signature %r", value) return None timestamp = int(parts[1]) - if timestamp < time.time() - max_age_days * 86400: + if timestamp < clock() - max_age_days * 86400: gen_log.warning("Expired cookie %r", value) return None - if timestamp > time.time() + 31 * 86400: + if timestamp > clock() + 31 * 86400: # _cookie_signature does not hash a delimiter between the # parts of the cookie, so an attacker could transfer trailing # digits from the payload to the timestamp without altering the @@ -2679,8 +2895,62 @@ def decode_signed_value(secret, name, value, max_age_days=31): return None -def _create_signature(secret, *parts): +def _decode_signed_value_v2(secret, name, value, max_age_days, clock): + def _consume_field(s): + length, _, rest = s.partition(b':') + n = int(length) + field_value = rest[:n] + # In python 3, indexing bytes returns small integers; we must + # use a slice to get a byte string as in python 2. + if rest[n:n + 1] != b'|': + raise ValueError("malformed v2 signed value field") + rest = rest[n + 1:] + return field_value, rest + rest = value[2:] # remove version number + try: + key_version, rest = _consume_field(rest) + timestamp, rest = _consume_field(rest) + name_field, rest = _consume_field(rest) + value_field, rest = _consume_field(rest) + except ValueError: + return None + passed_sig = rest + signed_string = value[:-len(passed_sig)] + expected_sig = _create_signature_v2(secret, signed_string) + if not _time_independent_equals(passed_sig, expected_sig): + return None + if name_field != utf8(name): + return None + timestamp = int(timestamp) + if timestamp < clock() - max_age_days * 86400: + # The signature has expired. + return None + try: + return base64.b64decode(value_field) + except Exception: + return None + + +def _create_signature_v1(secret, *parts): hash = hmac.new(utf8(secret), digestmod=hashlib.sha1) for part in parts: hash.update(utf8(part)) return utf8(hash.hexdigest()) + + +def _create_signature_v2(secret, s): + hash = hmac.new(utf8(secret), digestmod=hashlib.sha256) + hash.update(utf8(s)) + return utf8(hash.hexdigest()) + + +def _unquote_or_none(s): + """None-safe wrapper around url_unescape to handle unamteched optional + groups correctly. + + Note that args are passed as bytes so the handler can decide what + encoding to use. + """ + if s is None: + return s + return escape.url_unescape(s, encoding=None, plus=False) diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 9bec9bba..3767a207 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -32,15 +32,20 @@ import tornado.escape import tornado.web from tornado.concurrent import TracebackFuture -from tornado.escape import utf8, native_str +from tornado.escape import utf8, native_str, to_unicode from tornado import httpclient, httputil from tornado.ioloop import IOLoop from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log -from tornado.netutil import Resolver from tornado import simple_httpclient +from tornado.tcpclient import TCPClient from tornado.util import bytes_type, unicode_type +try: + from urllib.parse import urlparse # py2 +except ImportError: + from urlparse import urlparse # py3 + try: xrange # py2 except NameError: @@ -108,20 +113,17 @@ class WebSocketHandler(tornado.web.RequestHandler): def __init__(self, application, request, **kwargs): tornado.web.RequestHandler.__init__(self, application, request, **kwargs) - self.stream = request.connection.stream self.ws_connection = None + self.close_code = None + self.close_reason = None - def _execute(self, transforms, *args, **kwargs): + @tornado.web.asynchronous + def get(self, *args, **kwargs): self.open_args = args self.open_kwargs = kwargs - # Websocket only supports GET method - if self.request.method != 'GET': - self.stream.write(tornado.escape.utf8( - "HTTP/1.1 405 Method Not Allowed\r\n\r\n" - )) - self.stream.close() - return + self.stream = self.request.connection.detach() + self.stream.set_close_callback(self.on_connection_close) # Upgrade header should be present and should be equal to WebSocket if self.request.headers.get("Upgrade", "").lower() != 'websocket': @@ -144,9 +146,26 @@ class WebSocketHandler(tornado.web.RequestHandler): self.stream.close() return + # Handle WebSocket Origin naming convention differences # The difference between version 8 and 13 is that in 8 the # client sends a "Sec-Websocket-Origin" header and in 13 it's # simply "Origin". + if "Origin" in self.request.headers: + origin = self.request.headers.get("Origin") + else: + origin = self.request.headers.get("Sec-Websocket-Origin", None) + + + # If there was an origin header, check to make sure it matches + # according to check_origin. When the origin is None, we assume it + # did not come from a browser and that it can be passed on. + if origin is not None and not self.check_origin(origin): + self.stream.write(tornado.escape.utf8( + "HTTP/1.1 403 Cross Origin Websockets Disabled\r\n\r\n" + )) + self.stream.close() + return + if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"): self.ws_connection = WebSocketProtocol13(self) self.ws_connection.accept_connection() @@ -160,6 +179,7 @@ class WebSocketHandler(tornado.web.RequestHandler): "Sec-WebSocket-Version: 8\r\n\r\n")) self.stream.close() + def write_message(self, message, binary=False): """Sends the given message to the client of this Web Socket. @@ -220,18 +240,70 @@ class WebSocketHandler(tornado.web.RequestHandler): pass def on_close(self): - """Invoked when the WebSocket is closed.""" + """Invoked when the WebSocket is closed. + + If the connection was closed cleanly and a status code or reason + phrase was supplied, these values will be available as the attributes + ``self.close_code`` and ``self.close_reason``. + + .. versionchanged:: 3.3 + + Added ``close_code`` and ``close_reason`` attributes. + """ pass - def close(self): + def close(self, code=None, reason=None): """Closes this Web Socket. Once the close handshake is successful the socket will be closed. + + ``code`` may be a numeric status code, taken from the values + defined in `RFC 6455 section 7.4.1 + `_. + ``reason`` may be a textual message about why the connection is + closing. These values are made available to the client, but are + not otherwise interpreted by the websocket protocol. + + The ``code`` and ``reason`` arguments are ignored in the "draft76" + protocol version. + + .. versionchanged:: 3.3 + + Added the ``code`` and ``reason`` arguments. """ if self.ws_connection: - self.ws_connection.close() + self.ws_connection.close(code, reason) self.ws_connection = None + def check_origin(self, origin): + """Override to enable support for allowing alternate origins. + + The ``origin`` argument is the value of the ``Origin`` HTTP + header, the url responsible for initiating this request. This + method is not called for clients that do not send this header; + such requests are always allowed (because all browsers that + implement WebSockets support this header, and non-browser + clients do not have the same cross-site security concerns). + + Should return True to accept the request or False to reject it. + By default, rejects all requests with an origin on a host other + than this one. + + This is a security protection against cross site scripting attacks on + browsers, since WebSockets are allowed to bypass the usual same-origin + policies and don't use CORS headers. + + .. versionadded:: 3.3 + """ + parsed_origin = urlparse(origin) + origin = parsed_origin.netloc + origin = origin.lower() + + host = self.request.headers.get("Host") + + # Check to see that origin matches host directly, including ports + return origin == host + def allow_draft76(self): """Override to enable support for the older "draft76" protocol. @@ -489,7 +561,7 @@ class WebSocketProtocol76(WebSocketProtocol): """Send ping frame.""" raise ValueError("Ping messages not supported by this version of websockets") - def close(self): + def close(self, code=None, reason=None): """Closes the WebSocket connection.""" if not self.server_terminated: if not self.stream.closed(): @@ -739,6 +811,10 @@ class WebSocketProtocol13(WebSocketProtocol): elif opcode == 0x8: # Close self.client_terminated = True + if len(data) >= 2: + self.handler.close_code = struct.unpack('>H', data[:2])[0] + if len(data) > 2: + self.handler.close_reason = to_unicode(data[2:]) self.close() elif opcode == 0x9: # Ping @@ -749,11 +825,19 @@ class WebSocketProtocol13(WebSocketProtocol): else: self._abort() - def close(self): + def close(self, code=None, reason=None): """Closes the WebSocket connection.""" if not self.server_terminated: if not self.stream.closed(): - self._write_frame(True, 0x8, b"") + if code is None and reason is not None: + code = 1000 # "normal closure" status code + if code is None: + close_data = b'' + else: + close_data = struct.pack('>H', code) + if reason is not None: + close_data += utf8(reason) + self._write_frame(True, 0x8, close_data) self.server_terminated = True if self.client_terminated: if self._waiting is not None: @@ -789,18 +873,25 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): 'Sec-WebSocket-Version': '13', }) - self.resolver = Resolver(io_loop=io_loop) + self.tcp_client = TCPClient(io_loop=io_loop) super(WebSocketClientConnection, self).__init__( io_loop, None, request, lambda: None, self._on_http_response, - 104857600, self.resolver) + 104857600, self.tcp_client, 65536) - def close(self): + def close(self, code=None, reason=None): """Closes the websocket connection. + ``code`` and ``reason`` are documented under + `WebSocketHandler.close`. + .. versionadded:: 3.2 + + .. versionchanged:: 3.3 + + Added the ``code`` and ``reason`` arguments. """ if self.protocol is not None: - self.protocol.close() + self.protocol.close(code, reason) self.protocol = None def _on_close(self): @@ -816,8 +907,12 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): self.connect_future.set_exception(WebSocketError( "Non-websocket response")) - def _handle_1xx(self, code): - assert code == 101 + def headers_received(self, start_line, headers): + if start_line.code != 101: + return super(WebSocketClientConnection, self).headers_received( + start_line, headers) + + self.headers = headers assert self.headers['Upgrade'].lower() == 'websocket' assert self.headers['Connection'].lower() == 'upgrade' accept = WebSocketProtocol13.compute_accept_value(self.key) @@ -830,6 +925,9 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): self.io_loop.remove_timeout(self._timeout) self._timeout = None + self.stream = self.connection.detach() + self.stream.set_close_callback(self._on_close) + self.connect_future.set_result(self) def write_message(self, message, binary=False): @@ -913,12 +1011,15 @@ def _websocket_mask_python(mask, data): else: return unmasked.tostring() -if os.environ.get('TORNADO_NO_EXTENSION'): - # This environment variable exists to make it easier to do performance comparisons; - # it's not guaranteed to remain supported in the future. +if (os.environ.get('TORNADO_NO_EXTENSION') or + os.environ.get('TORNADO_EXTENSION') == '0'): + # These environment variables exist to make it easier to do performance + # comparisons; they are not guaranteed to remain supported in the future. _websocket_mask = _websocket_mask_python else: try: from tornado.speedups import websocket_mask as _websocket_mask except ImportError: + if os.environ.get('TORNADO_EXTENSION') == '1': + raise _websocket_mask = _websocket_mask_python diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 615f2e1f..47a0590a 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -20,9 +20,9 @@ WSGI is the Python standard for web servers, and allows for interoperability between Tornado and other Python web frameworks and servers. This module provides WSGI support in two ways: -* `WSGIApplication` is a version of `tornado.web.Application` that can run - inside a WSGI server. This is useful for running a Tornado app on another - HTTP server, such as Google App Engine. See the `WSGIApplication` class +* `WSGIAdapter` converts a `tornado.web.Application` to the WSGI application + interface. This is useful for running a Tornado app on another + HTTP server, such as Google App Engine. See the `WSGIAdapter` class documentation for limitations that apply. * `WSGIContainer` lets you run other WSGI applications and frameworks on the Tornado HTTP server. For example, with this class you can mix Django @@ -32,15 +32,14 @@ provides WSGI support in two ways: from __future__ import absolute_import, division, print_function, with_statement import sys -import time -import copy import tornado +from tornado.concurrent import Future from tornado import escape from tornado import httputil from tornado.log import access_log from tornado import web -from tornado.escape import native_str, parse_qs_bytes +from tornado.escape import native_str from tornado.util import bytes_type, unicode_type try: @@ -48,11 +47,6 @@ try: except ImportError: from cStringIO import StringIO as BytesIO # python 2 -try: - import Cookie # py2 -except ImportError: - import http.cookies as Cookie # py3 - try: import urllib.parse as urllib_parse # py3 except ImportError: @@ -83,11 +77,84 @@ else: class WSGIApplication(web.Application): """A WSGI equivalent of `tornado.web.Application`. - `WSGIApplication` is very similar to `tornado.web.Application`, - except no asynchronous methods are supported (since WSGI does not - support non-blocking requests properly). If you call - ``self.flush()`` or other asynchronous methods in your request - handlers running in a `WSGIApplication`, we throw an exception. + .. deprecated: 3.3:: + + Use a regular `.Application` and wrap it in `WSGIAdapter` instead. + """ + def __call__(self, environ, start_response): + return WSGIAdapter(self)(environ, start_response) + + +# WSGI has no facilities for flow control, so just return an already-done +# Future when the interface requires it. +_dummy_future = Future() +_dummy_future.set_result(None) + + +class _WSGIConnection(httputil.HTTPConnection): + def __init__(self, method, start_response, context): + self.method = method + self.start_response = start_response + self.context = context + self._write_buffer = [] + self._finished = False + self._expected_content_remaining = None + self._error = None + + def set_close_callback(self, callback): + # WSGI has no facility for detecting a closed connection mid-request, + # so we can simply ignore the callback. + pass + + def write_headers(self, start_line, headers, chunk=None, callback=None): + if self.method == 'HEAD': + self._expected_content_remaining = 0 + elif 'Content-Length' in headers: + self._expected_content_remaining = int(headers['Content-Length']) + else: + self._expected_content_remaining = None + self.start_response( + '%s %s' % (start_line.code, start_line.reason), + [(native_str(k), native_str(v)) for (k, v) in headers.get_all()]) + if chunk is not None: + self.write(chunk, callback) + elif callback is not None: + callback() + return _dummy_future + + def write(self, chunk, callback=None): + if self._expected_content_remaining is not None: + self._expected_content_remaining -= len(chunk) + if self._expected_content_remaining < 0: + self._error = httputil.HTTPOutputException( + "Tried to write more data than Content-Length") + raise self._error + self._write_buffer.append(chunk) + if callback is not None: + callback() + return _dummy_future + + def finish(self): + if (self._expected_content_remaining is not None and + self._expected_content_remaining != 0): + self._error = httputil.HTTPOutputException( + "Tried to write %d bytes less than Content-Length" % + self._expected_content_remaining) + raise self._error + self._finished = True + + +class _WSGIRequestContext(object): + def __init__(self, remote_ip, protocol): + self.remote_ip = remote_ip + self.protocol = protocol + + def __str__(self): + return self.remote_ip + + +class WSGIAdapter(object): + """Converts a `tornado.web.Application` instance into a WSGI application. Example usage:: @@ -100,121 +167,83 @@ class WSGIApplication(web.Application): self.write("Hello, world") if __name__ == "__main__": - application = tornado.wsgi.WSGIApplication([ + application = tornado.web.Application([ (r"/", MainHandler), ]) - server = wsgiref.simple_server.make_server('', 8888, application) + wsgi_app = tornado.wsgi.WSGIAdapter(application) + server = wsgiref.simple_server.make_server('', 8888, wsgi_app) server.serve_forever() See the `appengine demo - `_ + `_ for an example of using this module to run a Tornado app on Google App Engine. - WSGI applications use the same `.RequestHandler` class, but not - ``@asynchronous`` methods or ``flush()``. This means that it is - not possible to use `.AsyncHTTPClient`, or the `tornado.auth` or - `tornado.websocket` modules. + In WSGI mode asynchronous methods are not supported. This means + that it is not possible to use `.AsyncHTTPClient`, or the + `tornado.auth` or `tornado.websocket` modules. + + .. versionadded:: 3.3 """ - def __init__(self, handlers=None, default_host="", **settings): - web.Application.__init__(self, handlers, default_host, transforms=[], - wsgi=True, **settings) + def __init__(self, application): + if isinstance(application, WSGIApplication): + self.application = lambda request: web.Application.__call__( + application, request) + else: + self.application = application def __call__(self, environ, start_response): - handler = web.Application.__call__(self, HTTPRequest(environ)) - assert handler._finished - reason = handler._reason - status = str(handler._status_code) + " " + reason - headers = list(handler._headers.get_all()) - if hasattr(handler, "_new_cookie"): - for cookie in handler._new_cookie.values(): - headers.append(("Set-Cookie", cookie.OutputString(None))) - start_response(status, - [(native_str(k), native_str(v)) for (k, v) in headers]) - return handler._write_buffer - - -class HTTPRequest(object): - """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications.""" - def __init__(self, environ): - """Parses the given WSGI environment to construct the request.""" - self.method = environ["REQUEST_METHOD"] - self.path = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) - self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) - self.uri = self.path - self.arguments = {} - self.query_arguments = {} - self.body_arguments = {} - self.query = environ.get("QUERY_STRING", "") - if self.query: - self.uri += "?" + self.query - self.arguments = parse_qs_bytes(native_str(self.query), - keep_blank_values=True) - self.query_arguments = copy.deepcopy(self.arguments) - self.version = "HTTP/1.1" - self.headers = httputil.HTTPHeaders() + method = environ["REQUEST_METHOD"] + uri = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) + uri += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) + if environ.get("QUERY_STRING"): + uri += "?" + environ["QUERY_STRING"] + headers = httputil.HTTPHeaders() if environ.get("CONTENT_TYPE"): - self.headers["Content-Type"] = environ["CONTENT_TYPE"] + headers["Content-Type"] = environ["CONTENT_TYPE"] if environ.get("CONTENT_LENGTH"): - self.headers["Content-Length"] = environ["CONTENT_LENGTH"] + headers["Content-Length"] = environ["CONTENT_LENGTH"] for key in environ: if key.startswith("HTTP_"): - self.headers[key[5:].replace("_", "-")] = environ[key] - if self.headers.get("Content-Length"): - self.body = environ["wsgi.input"].read( - int(self.headers["Content-Length"])) + headers[key[5:].replace("_", "-")] = environ[key] + if headers.get("Content-Length"): + body = environ["wsgi.input"].read( + int(headers["Content-Length"])) else: - self.body = "" - self.protocol = environ["wsgi.url_scheme"] - self.remote_ip = environ.get("REMOTE_ADDR", "") + body = "" + protocol = environ["wsgi.url_scheme"] + remote_ip = environ.get("REMOTE_ADDR", "") if environ.get("HTTP_HOST"): - self.host = environ["HTTP_HOST"] + host = environ["HTTP_HOST"] else: - self.host = environ["SERVER_NAME"] - - # Parse request body - self.files = {} - httputil.parse_body_arguments(self.headers.get("Content-Type", ""), - self.body, self.body_arguments, self.files) - - for k, v in self.body_arguments.items(): - self.arguments.setdefault(k, []).extend(v) - - self._start_time = time.time() - self._finish_time = None - - def supports_http_1_1(self): - """Returns True if this request supports HTTP/1.1 semantics""" - return self.version == "HTTP/1.1" - - @property - def cookies(self): - """A dictionary of Cookie.Morsel objects.""" - if not hasattr(self, "_cookies"): - self._cookies = Cookie.SimpleCookie() - if "Cookie" in self.headers: - try: - self._cookies.load( - native_str(self.headers["Cookie"])) - except Exception: - self._cookies = None - return self._cookies - - def full_url(self): - """Reconstructs the full URL for this request.""" - return self.protocol + "://" + self.host + self.uri - - def request_time(self): - """Returns the amount of time it took for this request to execute.""" - if self._finish_time is None: - return time.time() - self._start_time - else: - return self._finish_time - self._start_time + host = environ["SERVER_NAME"] + connection = _WSGIConnection(method, start_response, + _WSGIRequestContext(remote_ip, protocol)) + request = httputil.HTTPServerRequest( + method, uri, "HTTP/1.1", headers=headers, body=body, + host=host, connection=connection) + request._parse_body() + self.application(request) + if connection._error: + raise connection._error + if not connection._finished: + raise Exception("request did not finish synchronously") + return connection._write_buffer class WSGIContainer(object): r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server. + .. warning:: + + WSGI is a *synchronous* interface, while Tornado's concurrency model + is based on single-threaded asynchronous execution. This means that + running a WSGI app with Tornado's `WSGIContainer` is *less scalable* + than running the same app in a multi-threaded WSGI server like + ``gunicorn`` or ``uwsgi``. Use `WSGIContainer` only when there are + benefits to combining Tornado and WSGI in the same process that + outweigh the reduced scalability. + Wrap a WSGI function in a `WSGIContainer` and pass it to `.HTTPServer` to run it. For example:: @@ -281,7 +310,7 @@ class WSGIContainer(object): @staticmethod def environ(request): - """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment. + """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment. """ hostport = request.host.split(":") if len(hostport) == 2: @@ -327,3 +356,6 @@ class WSGIContainer(object): summary = request.method + " " + request.uri + " (" + \ request.remote_ip + ")" log_method("%d %s %.2fms", status_code, summary, request_time) + + +HTTPRequest = httputil.HTTPServerRequest From 7c0870b6b80e9fb7b9bdab0efddaf121758741cf Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 29 May 2014 20:44:15 +0200 Subject: [PATCH 331/468] Capitalize --- .../core/media/movie/providers/metadata/base.py | 16 ++++++++-------- .../core/media/movie/providers/metadata/xbmc.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py index 93a513d7..7968000b 100644 --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -103,22 +103,22 @@ class MovieMetaData(MetaDataBase): def getBannerName(self, name, root, i): return - def getClearartName(self, name, root, i): + def getClearArtName(self, name, root, i): return def getLogoName(self, name, root, i): return - def getDiscartName(self, name, root, i): + def getDiscArtName(self, name, root, i): return def getLandscapeName(self, name, root, i): return - def getExtrathumbsName(self, name, root, i): + def getExtraThumbsName(self, name, root, i): return - def getExtrafanartName(self, name, root, i): + def getExtraFanartName(self, name, root, i): return def getNfoName(self, name, root, i): @@ -156,7 +156,7 @@ class MovieMetaData(MetaDataBase): if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'banner', i = i) - def getClearart(self, movie_info = None, data = None, i = 0): + def getClearArt(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'clear_art', i = i) @@ -166,7 +166,7 @@ class MovieMetaData(MetaDataBase): if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'logo', i = i) - def getDiscart(self, movie_info = None, data = None, i = 0): + def getDiscArt(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'disc_art', i = i) @@ -176,12 +176,12 @@ class MovieMetaData(MetaDataBase): if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data= data, wanted_file_type = 'landscape', i = i) - def getExtrathumbs(self, movie_info = None, data = None, i = 0): + def getExtraThumbs(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_thumbs', i = i) - def getExtrafanart(self, movie_info = None, data = None, i = 0): + def getExtraFanart(self, movie_info = None, data = None, i = 0): if not data: data = {} if not movie_info: movie_info = {} return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_fanart', i = i) diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index 079174ea..c25b5891 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -29,22 +29,22 @@ class XBMC(MovieMetaData): def getBannerName(self, name, root, i): return self.createMetaName(self.conf('meta_banner_name'), name, root) - def getClearartName(self, name, root, i): + def getClearArtName(self, name, root, i): return self.createMetaName(self.conf('meta_clear_art_name'), name, root) def getLogoName(self, name, root, i): return self.createMetaName(self.conf('meta_logo_name'), name, root) - def getDiscartName(self, name, root, i): + def getDiscArtName(self, name, root, i): return self.createMetaName(self.conf('meta_disc_art_name'), name, root) def getLandscapeName(self, name, root, i): return self.createMetaName(self.conf('meta_landscape_name'), name, root) - def getExtrathumbsName(self, name, root, i): + def getExtraThumbsName(self, name, root, i): return self.createMetaNameMult(self.conf('meta_extra_thumbs_name'), name, root, i) - def getExtrafanartName(self, name, root, i): + def getExtraFanartName(self, name, root, i): return self.createMetaNameMult(self.conf('meta_extra_fanart_name'), name, root, i) def createMetaName(self, basename, name, root): From a5ee362fc0afdfb801bb7b2527e089e3ae85884c Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 11:53:24 +0200 Subject: [PATCH 332/468] Remove scandir lib, use os.walk --- couchpotato/core/_base/updater/main.py | 7 +- couchpotato/core/downloaders/rtorrent_.py | 3 +- couchpotato/core/plugins/base.py | 3 +- couchpotato/core/plugins/file.py | 3 +- couchpotato/core/plugins/renamer.py | 15 +- couchpotato/core/plugins/scanner.py | 3 +- couchpotato/runner.py | 5 +- libs/scandir/.gitattributes | 1 - libs/scandir/.gitignore | 4 - libs/scandir/__init__.py | 0 libs/scandir/_scandir.c | 373 ------------------ libs/scandir/scandir.py | 456 ---------------------- 12 files changed, 16 insertions(+), 857 deletions(-) delete mode 100644 libs/scandir/.gitattributes delete mode 100644 libs/scandir/.gitignore delete mode 100644 libs/scandir/__init__.py delete mode 100644 libs/scandir/_scandir.c delete mode 100644 libs/scandir/scandir.py diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 9ebecce2..8164dc59 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -16,7 +16,6 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from dateutil.parser import parse from git.repository import LocalRepository -from scandir import scandir import version from six.moves import filter @@ -182,7 +181,7 @@ class BaseUpdater(Plugin): def deletePyc(self, only_excess = True, show_logs = True): - for root, dirs, files in scandir.walk(Env.get('app_dir')): + for root, dirs, files in os.walk(Env.get('app_dir')): pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) py_files = set(filter(lambda filename: filename.endswith('.py'), files)) @@ -329,11 +328,11 @@ class SourceUpdater(BaseUpdater): # Get list of files we want to overwrite self.deletePyc() existing_files = [] - for root, subfiles, filenames in scandir.walk(app_dir): + for root, subfiles, filenames in os.walk(app_dir): for filename in filenames: existing_files.append(os.path.join(root, filename)) - for root, subfiles, filenames in scandir.walk(path): + for root, subfiles, filenames in os.walk(path): for filename in filenames: fromfile = os.path.join(root, filename) tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, '')) diff --git a/couchpotato/core/downloaders/rtorrent_.py b/couchpotato/core/downloaders/rtorrent_.py index 5ea3a4de..822501ae 100644 --- a/couchpotato/core/downloaders/rtorrent_.py +++ b/couchpotato/core/downloaders/rtorrent_.py @@ -12,7 +12,6 @@ from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.logger import CPLog from bencode import bencode, bdecode from rtorrent import RTorrent -from scandir import scandir log = CPLog(__name__) @@ -238,7 +237,7 @@ class rTorrent(DownloaderBase): if torrent.is_multi_file() and torrent.directory.endswith(torrent.name): # Remove empty directories bottom up try: - for path, _, _ in scandir.walk(sp(torrent.directory), topdown = False): + for path, _, _ in os.walk(sp(torrent.directory), topdown = False): os.rmdir(path) except OSError: log.info('Directory "%s" contains extra files, unable to remove', torrent.directory) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 1d9c0c42..f548cb29 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -16,7 +16,6 @@ from couchpotato.environment import Env import requests from requests.packages.urllib3 import Timeout from requests.packages.urllib3.exceptions import MaxRetryError -from scandir import scandir from tornado import template from tornado.web import StaticFileHandler @@ -144,7 +143,7 @@ class Plugin(object): def deleteEmptyFolder(self, folder, show_error = True): folder = sp(folder) - for root, dirs, files in scandir.walk(folder): + for root, dirs, files in os.walk(folder): for dir_name in dirs: full_path = os.path.join(root, dir_name) diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py index 92d85c50..0d6483bb 100644 --- a/couchpotato/core/plugins/file.py +++ b/couchpotato/core/plugins/file.py @@ -9,7 +9,6 @@ from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from scandir import scandir from tornado.web import StaticFileHandler @@ -49,7 +48,7 @@ class FileManager(Plugin): for x in file_dict.keys(): files.extend(file_dict[x]) - for f in scandir.scandir(cache_dir): + for f in os.listdir(cache_dir): if os.path.splitext(f.name)[1] in ['.png', '.jpg', '.jpeg']: file_path = os.path.join(cache_dir, f.name) if toUnicode(file_path) not in files: diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 50c55d23..4ba07a26 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -14,7 +14,6 @@ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from scandir import scandir from unrar2 import RarFile import six from six.moves import filter @@ -195,7 +194,7 @@ class Renamer(Plugin): else: # Get all files from the specified folder try: - for root, folders, names in scandir.walk(media_folder): + for root, folders, names in os.walk(media_folder): files.extend([sp(os.path.join(root, name)) for name in names]) except: log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc())) @@ -664,7 +663,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Tag all files in release folder elif release_download['folder']: - for root, folders, names in scandir.walk(sp(release_download['folder'])): + for root, folders, names in os.walk(sp(release_download['folder'])): tag_files.extend([os.path.join(root, name) for name in names]) for filename in tag_files: @@ -704,7 +703,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Untag all files in release folder else: - for root, folders, names in scandir.walk(folder): + for root, folders, names in os.walk(folder): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) if not folder: @@ -712,7 +711,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find all .ignore files in folder ignore_files = [] - for root, dirnames, filenames in scandir.walk(folder): + for root, dirnames, filenames in os.walk(folder): ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and delete if found @@ -741,11 +740,11 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Find tag on all files in release folder else: - for root, folders, names in scandir.walk(folder): + for root, folders, names in os.walk(folder): tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore']) # Find all .ignore files in folder - for root, dirnames, filenames in scandir.walk(folder): + for root, dirnames, filenames in os.walk(folder): ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag)) # Match all found ignore files with the tag_files and return True found @@ -1102,7 +1101,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) check_file_date = False if not files: - for root, folders, names in scandir.walk(folder): + for root, folders, names in os.walk(folder): files.extend([sp(os.path.join(root, name)) for name in names]) # Find all archive files diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index b8eca4c4..c24d64f6 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -13,7 +13,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info -from scandir import scandir from subliminal.videos import Video import enzyme from six.moves import filter, map, zip @@ -150,7 +149,7 @@ class Scanner(Plugin): check_file_date = True try: files = [] - for root, dirs, walk_files in scandir.walk(folder, followlinks=True): + for root, dirs, walk_files in os.walk(folder, followlinks=True): files.extend([sp(os.path.join(sp(root), ss(filename))) for filename in walk_files]) # Break if CP wants to shut down diff --git a/couchpotato/runner.py b/couchpotato/runner.py index d22f8fc8..3ddd4ac6 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -18,7 +18,6 @@ from couchpotato.api import NonBlockHandler, ApiHandler from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getDataDir, tryInt -from scandir import scandir from tornado.httpserver import HTTPServer from tornado.web import Application, StaticFileHandler, RedirectHandler @@ -99,7 +98,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En existing_backups = [] if not os.path.isdir(backup_path): os.makedirs(backup_path) - for root, dirs, files in scandir.walk(backup_path): + for root, dirs, files in os.walk(backup_path): for backup_file in files: ints = re.findall('\d+', backup_file) @@ -116,7 +115,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Create new backup new_backup = sp(os.path.join(backup_path, '%s.tar.gz' % int(time.time()))) zipf = tarfile.open(new_backup, 'w:gz') - for root, dirs, files in scandir.walk(db_path): + for root, dirs, files in os.walk(db_path): for zfilename in files: zipf.add(os.path.join(root, zfilename), arcname = 'database/%s' % os.path.join(root[len(db_path) + 1:], zfilename)) zipf.close() diff --git a/libs/scandir/.gitattributes b/libs/scandir/.gitattributes deleted file mode 100644 index 176a458f..00000000 --- a/libs/scandir/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/libs/scandir/.gitignore b/libs/scandir/.gitignore deleted file mode 100644 index 48878c73..00000000 --- a/libs/scandir/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.pyc -*.pyd -benchtree -build diff --git a/libs/scandir/__init__.py b/libs/scandir/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/scandir/_scandir.c b/libs/scandir/_scandir.c deleted file mode 100644 index 26c81eda..00000000 --- a/libs/scandir/_scandir.c +++ /dev/null @@ -1,373 +0,0 @@ -// scandir C speedups -// -// TODO: this is a work in progress! -// -// There's a fair bit of PY_MAJOR_VERSION boilerplate to support both Python 2 -// and Python 3 -- the structure of this is taken from here: -// http://docs.python.org/3.3/howto/cporting.html - -#include -#include - -#ifdef MS_WINDOWS -#include -#endif - -#if PY_MAJOR_VERSION >= 3 -#define INITERROR return NULL -#define FROM_LONG PyLong_FromLong -#define FROM_STRING PyUnicode_FromStringAndSize -#else -#define INITERROR return -#define FROM_LONG PyInt_FromLong -#define FROM_STRING PyString_FromStringAndSize -#endif - -#ifdef MS_WINDOWS - -static PyObject * -win32_error_unicode(char* function, Py_UNICODE* filename) -{ - errno = GetLastError(); - if (filename) - return PyErr_SetFromWindowsErrWithUnicodeFilename(errno, filename); - else - return PyErr_SetFromWindowsErr(errno); -} - -/* Below, we *know* that ugo+r is 0444 */ -#if _S_IREAD != 0400 -#error Unsupported C library -#endif -static int -attributes_to_mode(DWORD attr) -{ - int m = 0; - if (attr & FILE_ATTRIBUTE_DIRECTORY) - m |= _S_IFDIR | 0111; /* IFEXEC for user,group,other */ - else - m |= _S_IFREG; - if (attr & FILE_ATTRIBUTE_READONLY) - m |= 0444; - else - m |= 0666; - if (attr & FILE_ATTRIBUTE_REPARSE_POINT) - m |= 0120000; // S_IFLNK - return m; -} - -double -filetime_to_time(FILETIME *filetime) -{ - const double SECONDS_BETWEEN_EPOCHS = 11644473600.0; - - unsigned long long total = (unsigned long long)filetime->dwHighDateTime << 32 | - (unsigned long long)filetime->dwLowDateTime; - return (double)total / 10000000.0 - SECONDS_BETWEEN_EPOCHS; -} - -static PyTypeObject StatResultType; - -static PyObject * -find_data_to_statresult(WIN32_FIND_DATAW *data) -{ - PY_LONG_LONG size; - PyObject *v = PyStructSequence_New(&StatResultType); - if (v == NULL) - return NULL; - - size = (PY_LONG_LONG)data->nFileSizeHigh << 32 | - (PY_LONG_LONG)data->nFileSizeLow; - - PyStructSequence_SET_ITEM(v, 0, FROM_LONG(attributes_to_mode(data->dwFileAttributes))); - PyStructSequence_SET_ITEM(v, 1, FROM_LONG(0)); - PyStructSequence_SET_ITEM(v, 2, FROM_LONG(0)); - PyStructSequence_SET_ITEM(v, 3, FROM_LONG(0)); - PyStructSequence_SET_ITEM(v, 4, FROM_LONG(0)); - PyStructSequence_SET_ITEM(v, 5, FROM_LONG(0)); - PyStructSequence_SET_ITEM(v, 6, PyLong_FromLongLong((PY_LONG_LONG)size)); - PyStructSequence_SET_ITEM(v, 7, PyFloat_FromDouble(filetime_to_time(&data->ftLastAccessTime))); - PyStructSequence_SET_ITEM(v, 8, PyFloat_FromDouble(filetime_to_time(&data->ftLastWriteTime))); - PyStructSequence_SET_ITEM(v, 9, PyFloat_FromDouble(filetime_to_time(&data->ftCreationTime))); - - if (PyErr_Occurred()) { - Py_DECREF(v); - return NULL; - } - - return v; -} - -static PyStructSequence_Field stat_result_fields[] = { - {"st_mode", "protection bits"}, - {"st_ino", "inode"}, - {"st_dev", "device"}, - {"st_nlink", "number of hard links"}, - {"st_uid", "user ID of owner"}, - {"st_gid", "group ID of owner"}, - {"st_size", "total size, in bytes"}, - {"st_atime", "time of last access"}, - {"st_mtime", "time of last modification"}, - {"st_ctime", "time of last change"}, - {0} -}; - -static PyStructSequence_Desc stat_result_desc = { - "stat_result", /* name */ - NULL, /* doc */ - stat_result_fields, - 10 -}; - -static PyObject * -scandir_helper(PyObject *self, PyObject *args) -{ - PyObject *d, *v; - HANDLE hFindFile; - BOOL result; - WIN32_FIND_DATAW wFileData; - Py_UNICODE *wnamebuf; - Py_ssize_t len; - PyObject *po; - PyObject *name_stat; - - if (!PyArg_ParseTuple(args, "U:scandir_helper", &po)) - return NULL; - - /* Overallocate for \\*.*\0 */ - len = PyUnicode_GET_SIZE(po); - wnamebuf = malloc((len + 5) * sizeof(wchar_t)); - if (!wnamebuf) { - PyErr_NoMemory(); - return NULL; - } - wcscpy(wnamebuf, PyUnicode_AS_UNICODE(po)); - if (len > 0) { - Py_UNICODE wch = wnamebuf[len-1]; - if (wch != L'/' && wch != L'\\' && wch != L':') - wnamebuf[len++] = L'\\'; - wcscpy(wnamebuf + len, L"*.*"); - } - if ((d = PyList_New(0)) == NULL) { - free(wnamebuf); - return NULL; - } - Py_BEGIN_ALLOW_THREADS - hFindFile = FindFirstFileW(wnamebuf, &wFileData); - Py_END_ALLOW_THREADS - if (hFindFile == INVALID_HANDLE_VALUE) { - int error = GetLastError(); - if (error == ERROR_FILE_NOT_FOUND) { - free(wnamebuf); - return d; - } - Py_DECREF(d); - win32_error_unicode("FindFirstFileW", wnamebuf); - free(wnamebuf); - return NULL; - } - do { - /* Skip over . and .. */ - if (wcscmp(wFileData.cFileName, L".") != 0 && - wcscmp(wFileData.cFileName, L"..") != 0) { - v = PyUnicode_FromUnicode(wFileData.cFileName, wcslen(wFileData.cFileName)); - if (v == NULL) { - Py_DECREF(d); - d = NULL; - break; - } - name_stat = Py_BuildValue("ON", v, find_data_to_statresult(&wFileData)); - if (name_stat == NULL) { - Py_DECREF(v); - Py_DECREF(d); - d = NULL; - break; - } - if (PyList_Append(d, name_stat) != 0) { - Py_DECREF(v); - Py_DECREF(d); - Py_DECREF(name_stat); - d = NULL; - break; - } - Py_DECREF(name_stat); - Py_DECREF(v); - } - Py_BEGIN_ALLOW_THREADS - result = FindNextFileW(hFindFile, &wFileData); - Py_END_ALLOW_THREADS - /* FindNextFile sets error to ERROR_NO_MORE_FILES if - it got to the end of the directory. */ - if (!result && GetLastError() != ERROR_NO_MORE_FILES) { - Py_DECREF(d); - win32_error_unicode("FindNextFileW", wnamebuf); - FindClose(hFindFile); - free(wnamebuf); - return NULL; - } - } while (result == TRUE); - - if (FindClose(hFindFile) == FALSE) { - Py_DECREF(d); - win32_error_unicode("FindClose", wnamebuf); - free(wnamebuf); - return NULL; - } - free(wnamebuf); - return d; -} - -#else // Linux / OS X - -#include -#define NAMLEN(dirent) strlen((dirent)->d_name) - -static PyObject * -posix_error_with_allocated_filename(char* name) -{ - PyObject *rc = PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); - PyMem_Free(name); - return rc; -} - -static PyObject * -scandir_helper(PyObject *self, PyObject *args) -{ - char *name = NULL; - PyObject *d, *v, *name_type; - DIR *dirp; - struct dirent *ep; - int arg_is_unicode = 1; - - errno = 0; - if (!PyArg_ParseTuple(args, "U:scandir_helper", &v)) { - arg_is_unicode = 0; - PyErr_Clear(); - } - if (!PyArg_ParseTuple(args, "et:scandir_helper", Py_FileSystemDefaultEncoding, &name)) - return NULL; - Py_BEGIN_ALLOW_THREADS - dirp = opendir(name); - Py_END_ALLOW_THREADS - if (dirp == NULL) { - return posix_error_with_allocated_filename(name); - } - if ((d = PyList_New(0)) == NULL) { - Py_BEGIN_ALLOW_THREADS - closedir(dirp); - Py_END_ALLOW_THREADS - PyMem_Free(name); - return NULL; - } - for (;;) { - errno = 0; - Py_BEGIN_ALLOW_THREADS - ep = readdir(dirp); - Py_END_ALLOW_THREADS - if (ep == NULL) { - if (errno == 0) { - break; - } else { - Py_BEGIN_ALLOW_THREADS - closedir(dirp); - Py_END_ALLOW_THREADS - Py_DECREF(d); - return posix_error_with_allocated_filename(name); - } - } - if (ep->d_name[0] == '.' && - (NAMLEN(ep) == 1 || - (ep->d_name[1] == '.' && NAMLEN(ep) == 2))) - continue; - v = FROM_STRING(ep->d_name, NAMLEN(ep)); - if (v == NULL) { - Py_DECREF(d); - d = NULL; - break; - } - if (arg_is_unicode) { - PyObject *w; - - w = PyUnicode_FromEncodedObject(v, - Py_FileSystemDefaultEncoding, - "strict"); - if (w != NULL) { - Py_DECREF(v); - v = w; - } - else { - /* fall back to the original byte string, as - discussed in patch #683592 */ - PyErr_Clear(); - } - } - name_type = Py_BuildValue("ON", v, FROM_LONG(ep->d_type)); - if (name_type == NULL) { - Py_DECREF(v); - Py_DECREF(d); - d = NULL; - break; - } - if (PyList_Append(d, name_type) != 0) { - Py_DECREF(v); - Py_DECREF(d); - Py_DECREF(name_type); - d = NULL; - break; - } - Py_DECREF(name_type); - Py_DECREF(v); - } - Py_BEGIN_ALLOW_THREADS - closedir(dirp); - Py_END_ALLOW_THREADS - PyMem_Free(name); - - return d; -} - -#endif - -static PyMethodDef scandir_methods[] = { - {"scandir_helper", (PyCFunction)scandir_helper, METH_VARARGS, NULL}, - {NULL, NULL}, -}; - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_scandir", - NULL, - 0, - scandir_methods, - NULL, - NULL, - NULL, - NULL, -}; -#endif - -#if PY_MAJOR_VERSION >= 3 -PyObject * -PyInit__scandir(void) -{ - PyObject *module = PyModule_Create(&moduledef); -#else -void -init_scandir(void) -{ - PyObject *module = Py_InitModule("_scandir", scandir_methods); -#endif - if (module == NULL) { - INITERROR; - } - -#ifdef MS_WINDOWS - stat_result_desc.name = "scandir.stat_result"; - PyStructSequence_InitType(&StatResultType, &stat_result_desc); -#endif - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} diff --git a/libs/scandir/scandir.py b/libs/scandir/scandir.py deleted file mode 100644 index 1e34f8e5..00000000 --- a/libs/scandir/scandir.py +++ /dev/null @@ -1,456 +0,0 @@ -"""scandir, a better directory iterator that exposes all file info OS provides - -scandir is a generator version of os.listdir() that returns an iterator over -files in a directory, and also exposes the extra information most OSes provide -while iterating files in a directory. - -See README.md or https://github.com/benhoyt/scandir for rationale and docs. - -scandir is released under the new BSD 3-clause license. See LICENSE.txt for -the full license text. -""" - -from __future__ import division - -import ctypes -import os -import stat -import sys - -__version__ = '0.3' -__all__ = ['scandir', 'walk'] - -# Shortcuts to these functions for speed and ease -join = os.path.join -lstat = os.lstat - -S_IFDIR = stat.S_IFDIR -S_IFREG = stat.S_IFREG -S_IFLNK = stat.S_IFLNK - -# 'unicode' isn't defined on Python 3 -try: - unicode -except NameError: - unicode = str - -_scandir = None - - -class GenericDirEntry(object): - __slots__ = ('name', '_lstat', '_path') - - def __init__(self, path, name): - self._path = path - self.name = name - self._lstat = None - - def lstat(self): - if self._lstat is None: - self._lstat = lstat(join(self._path, self.name)) - return self._lstat - - def is_dir(self): - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFDIR - - def is_file(self): - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFREG - - def is_symlink(self): - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFLNK - - def __str__(self): - return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) - - __repr__ = __str__ - - -if sys.platform == 'win32': - from ctypes import wintypes - - # Various constants from windows.h - INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value - ERROR_FILE_NOT_FOUND = 2 - ERROR_NO_MORE_FILES = 18 - FILE_ATTRIBUTE_READONLY = 1 - FILE_ATTRIBUTE_DIRECTORY = 16 - FILE_ATTRIBUTE_REPARSE_POINT = 1024 - - # Numer of seconds between 1601-01-01 and 1970-01-01 - SECONDS_BETWEEN_EPOCHS = 11644473600 - - kernel32 = ctypes.windll.kernel32 - - # ctypes wrappers for (wide string versions of) FindFirstFile, - # FindNextFile, and FindClose - FindFirstFile = kernel32.FindFirstFileW - FindFirstFile.argtypes = [ - wintypes.LPCWSTR, - ctypes.POINTER(wintypes.WIN32_FIND_DATAW), - ] - FindFirstFile.restype = wintypes.HANDLE - - FindNextFile = kernel32.FindNextFileW - FindNextFile.argtypes = [ - wintypes.HANDLE, - ctypes.POINTER(wintypes.WIN32_FIND_DATAW), - ] - FindNextFile.restype = wintypes.BOOL - - FindClose = kernel32.FindClose - FindClose.argtypes = [wintypes.HANDLE] - FindClose.restype = wintypes.BOOL - - def filetime_to_time(filetime): - """Convert Win32 FILETIME to time since Unix epoch in seconds.""" - total = filetime.dwHighDateTime << 32 | filetime.dwLowDateTime - return total / 10000000 - SECONDS_BETWEEN_EPOCHS - - def find_data_to_stat(data): - """Convert Win32 FIND_DATA struct to stat_result.""" - # First convert Win32 dwFileAttributes to st_mode - attributes = data.dwFileAttributes - st_mode = 0 - if attributes & FILE_ATTRIBUTE_DIRECTORY: - st_mode |= S_IFDIR | 0o111 - else: - st_mode |= S_IFREG - if attributes & FILE_ATTRIBUTE_READONLY: - st_mode |= 0o444 - else: - st_mode |= 0o666 - if attributes & FILE_ATTRIBUTE_REPARSE_POINT: - st_mode |= S_IFLNK - - st_size = data.nFileSizeHigh << 32 | data.nFileSizeLow - st_atime = filetime_to_time(data.ftLastAccessTime) - st_mtime = filetime_to_time(data.ftLastWriteTime) - st_ctime = filetime_to_time(data.ftCreationTime) - - # Some fields set to zero per CPython's posixmodule.c: st_ino, st_dev, - # st_nlink, st_uid, st_gid - return os.stat_result((st_mode, 0, 0, 0, 0, 0, st_size, st_atime, - st_mtime, st_ctime)) - - class Win32DirEntry(object): - __slots__ = ('name', '_lstat', '_find_data') - - def __init__(self, name, find_data): - self.name = name - self._lstat = None - self._find_data = find_data - - def lstat(self): - if self._lstat is None: - # Lazily convert to stat object, because it's slow, and often - # we only need is_dir() etc - self._lstat = find_data_to_stat(self._find_data) - return self._lstat - - def is_dir(self): - return (self._find_data.dwFileAttributes & - FILE_ATTRIBUTE_DIRECTORY != 0) - - def is_file(self): - return (self._find_data.dwFileAttributes & - FILE_ATTRIBUTE_DIRECTORY == 0) - - def is_symlink(self): - return (self._find_data.dwFileAttributes & - FILE_ATTRIBUTE_REPARSE_POINT != 0) - - def __str__(self): - return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) - - __repr__ = __str__ - - def win_error(error, filename): - exc = WindowsError(error, ctypes.FormatError(error)) - exc.filename = filename - return exc - - def scandir(path='.', windows_wildcard='*.*'): - """Like os.listdir(), but yield DirEntry objects instead of returning - a list of names. - """ - # Call FindFirstFile and handle errors - data = wintypes.WIN32_FIND_DATAW() - data_p = ctypes.byref(data) - filename = join(path, windows_wildcard) - handle = FindFirstFile(filename, data_p) - if handle == INVALID_HANDLE_VALUE: - error = ctypes.GetLastError() - if error == ERROR_FILE_NOT_FOUND: - # No files, don't yield anything - return - raise win_error(error, path) - - # Call FindNextFile in a loop, stopping when no more files - try: - while True: - # Skip '.' and '..' (current and parent directory), but - # otherwise yield (filename, stat_result) tuple - name = data.cFileName - if name not in ('.', '..'): - yield Win32DirEntry(name, data) - - data = wintypes.WIN32_FIND_DATAW() - data_p = ctypes.byref(data) - success = FindNextFile(handle, data_p) - if not success: - error = ctypes.GetLastError() - if error == ERROR_NO_MORE_FILES: - break - raise win_error(error, path) - finally: - if not FindClose(handle): - raise win_error(ctypes.GetLastError(), path) - - try: - import _scandir - - scandir_helper = _scandir.scandir_helper - - class Win32DirEntry(object): - __slots__ = ('name', '_lstat') - - def __init__(self, name, lstat): - self.name = name - self._lstat = lstat - - def lstat(self): - return self._lstat - - def is_dir(self): - return self._lstat.st_mode & 0o170000 == S_IFDIR - - def is_file(self): - return self._lstat.st_mode & 0o170000 == S_IFREG - - def is_symlink(self): - return self._lstat.st_mode & 0o170000 == S_IFLNK - - def __str__(self): - return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) - - __repr__ = __str__ - - def scandir(path='.'): - for name, stat in scandir_helper(unicode(path)): - yield Win32DirEntry(name, stat) - - except ImportError: - pass - - -# Linux, OS X, and BSD implementation -elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform: - import ctypes.util - - DIR_p = ctypes.c_void_p - - # Rather annoying how the dirent struct is slightly different on each - # platform. The only fields we care about are d_name and d_type. - class Dirent(ctypes.Structure): - if sys.platform.startswith('linux'): - _fields_ = ( - ('d_ino', ctypes.c_ulong), - ('d_off', ctypes.c_long), - ('d_reclen', ctypes.c_ushort), - ('d_type', ctypes.c_byte), - ('d_name', ctypes.c_char * 256), - ) - else: - _fields_ = ( - ('d_ino', ctypes.c_uint32), # must be uint32, not ulong - ('d_reclen', ctypes.c_ushort), - ('d_type', ctypes.c_byte), - ('d_namlen', ctypes.c_byte), - ('d_name', ctypes.c_char * 256), - ) - - DT_UNKNOWN = 0 - DT_DIR = 4 - DT_REG = 8 - DT_LNK = 10 - - Dirent_p = ctypes.POINTER(Dirent) - Dirent_pp = ctypes.POINTER(Dirent_p) - - libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) - opendir = libc.opendir - opendir.argtypes = [ctypes.c_char_p] - opendir.restype = DIR_p - - readdir_r = libc.readdir_r - readdir_r.argtypes = [DIR_p, Dirent_p, Dirent_pp] - readdir_r.restype = ctypes.c_int - - closedir = libc.closedir - closedir.argtypes = [DIR_p] - closedir.restype = ctypes.c_int - - file_system_encoding = sys.getfilesystemencoding() - - class PosixDirEntry(object): - __slots__ = ('name', '_d_type', '_lstat', '_path') - - def __init__(self, path, name, d_type): - self._path = path - self.name = name - self._d_type = d_type - self._lstat = None - - def lstat(self): - if self._lstat is None: - self._lstat = lstat(join(self._path, self.name)) - return self._lstat - - # Ridiculous duplication between these is* functions -- helps a little - # bit with os.walk() performance compared to calling another function. - def is_dir(self): - d_type = self._d_type - if d_type != DT_UNKNOWN: - return d_type == DT_DIR - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFDIR - - def is_file(self): - d_type = self._d_type - if d_type != DT_UNKNOWN: - return d_type == DT_REG - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFREG - - def is_symlink(self): - d_type = self._d_type - if d_type != DT_UNKNOWN: - return d_type == DT_LNK - try: - self.lstat() - except OSError: - return False - return self._lstat.st_mode & 0o170000 == S_IFLNK - - def __str__(self): - return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name) - - __repr__ = __str__ - - def posix_error(filename): - errno = ctypes.get_errno() - exc = OSError(errno, os.strerror(errno)) - exc.filename = filename - return exc - - def scandir(path='.'): - """Like os.listdir(), but yield DirEntry objects instead of returning - a list of names. - """ - dir_p = opendir(path.encode(file_system_encoding)) - if not dir_p: - raise posix_error(path) - try: - result = Dirent_p() - while True: - entry = Dirent() - if readdir_r(dir_p, entry, result): - raise posix_error(path) - if not result: - break - name = entry.d_name.decode(file_system_encoding) - if name not in ('.', '..'): - yield PosixDirEntry(path, name, entry.d_type) - finally: - if closedir(dir_p): - raise posix_error(path) - - try: - import _scandir - - scandir_helper = _scandir.scandir_helper - - def scandir(path='.'): - for name, d_type in scandir_helper(unicode(path)): - yield PosixDirEntry(path, name, d_type) - - except ImportError: - pass - - -# Some other system -- no d_type or stat information -else: - def scandir(path='.'): - """Like os.listdir(), but yield DirEntry objects instead of returning - a list of names. - """ - for name in os.listdir(path): - yield GenericDirEntry(path, name) - - -def walk(top, topdown=True, onerror=None, followlinks=False): - """Like os.walk(), but faster, as it uses scandir() internally.""" - # Determine which are files and which are directories - dirs = [] - nondirs = [] - try: - for entry in scandir(top): - if entry.is_dir(): - dirs.append(entry) - else: - nondirs.append(entry) - except OSError as error: - if onerror is not None: - onerror(error) - return - - # Yield before recursion if going top down - if topdown: - # Need to do some fancy footwork here as caller is allowed to modify - # dir_names, and we really want them to modify dirs (list of DirEntry - # objects) instead. Keep a mapping of entries keyed by name. - dir_names = [] - entries_by_name = {} - for entry in dirs: - dir_names.append(entry.name) - entries_by_name[entry.name] = entry - - yield top, dir_names, [e.name for e in nondirs] - - dirs = [] - for dir_name in dir_names: - entry = entries_by_name.get(dir_name) - if entry is None: - # Only happens when caller creates a new directory and adds it - # to dir_names - entry = GenericDirEntry(top, dir_name) - dirs.append(entry) - - # Recurse into sub-directories, following symbolic links if "followlinks" - for entry in dirs: - if followlinks or not entry.is_symlink(): - new_path = join(top, entry.name) - for x in walk(new_path, topdown, onerror, followlinks): - yield x - - # Yield before recursion if going bottom up - if not topdown: - yield top, [e.name for e in dirs], [e.name for e in nondirs] From b630b84ab0fb333c1b5bc2fc69857a381dcbf106 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 12:09:55 +0200 Subject: [PATCH 333/468] Get proper poster image from tmdb --- .../core/media/movie/providers/info/themoviedb.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index 35fae5e9..9b002af9 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -105,7 +105,7 @@ class TheMovieDb(MovieProvider): if not movie_data: # Images - poster = self.getImage(movie, type = 'poster', size = 'poster') + poster = self.getImage(movie, type = 'poster', size = 'w154') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original', n = self.MAX_EXTRATHUMBS, skipfirst = True) @@ -181,12 +181,12 @@ class TheMovieDb(MovieProvider): log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) return image_url - + def getMultImages(self, movie, type = 'backdrops', size = 'original', n = -1, skipfirst = False): """ - If n < 0, return all images. Otherwise return n images. + If n < 0, return all images. Otherwise return n images. If n > len(getattr(movie, type)), then return all images. - If skipfirst is True, then it will skip getattr(movie, type)[0]. This + If skipfirst is True, then it will skip getattr(movie, type)[0]. This is because backdrops[0] is typically backdrop. """ @@ -200,7 +200,7 @@ class TheMovieDb(MovieProvider): for i in range(int(skipfirst), num_images + int(skipfirst)): image_urls.append(images[i].geturl(size = size)) - + except: log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie)))) From 90a618bd7e41ea65746f99e0bf5a3c37ba2dec52 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 12:33:35 +0200 Subject: [PATCH 334/468] Allow search on imdb urls --- couchpotato/core/media/_base/search/main.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/search/main.py b/couchpotato/core/media/_base/search/main.py index 81897b5f..1d0603cb 100644 --- a/couchpotato/core/media/_base/search/main.py +++ b/couchpotato/core/media/_base/search/main.py @@ -1,6 +1,6 @@ from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.variable import mergeDicts +from couchpotato.core.helpers.variable import mergeDicts, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -35,12 +35,21 @@ class Search(Plugin): elif isinstance(types, (list, tuple, set)): types = list(types) + imdb_identifier = getImdb(q) + if not types: - result = fireEvent('info.search', q = q, merge = True) + if imdb_identifier: + result = fireEvent('movie.info', identifier = imdb_identifier, merge = True) + result = {result['type']: [result]} + else: + result = fireEvent('info.search', q = q, merge = True) else: result = {} for media_type in types: - result[media_type] = fireEvent('%s.search' % media_type) + if imdb_identifier: + result[media_type] = fireEvent('%s.info' % media_type, identifier = imdb_identifier) + else: + result[media_type] = fireEvent('%s.search' % media_type, q = q) return mergeDicts({ 'success': True, From 1c7edc94875d7a8c7eced65a2d8d0f54a6ed7edc Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 13:18:11 +0200 Subject: [PATCH 335/468] TPB and Kickass proxy update --- .../_base/providers/torrent/kickasstorrents.py | 4 ++++ .../media/_base/providers/torrent/thepiratebay.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index a711502a..65bab4d1 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -32,8 +32,12 @@ class Base(TorrentMagnetProvider): proxy_list = [ 'https://kickass.to', 'http://kickass.pw', + 'http://kickassto.come.in', + 'http://katproxy.ws', 'http://www.kickassunblock.info', 'http://www.kickassproxy.info', + 'http://katph.eu', + 'http://kickassto.come.in', ] def _search(self, media, quality, results): diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index b840fc71..487ce36b 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -24,15 +24,18 @@ class Base(TorrentMagnetProvider): http_time_between_calls = 0 proxy_list = [ - 'https://tpb.ipredator.se', + 'https://nobay.net', + 'https://thebay.al', 'https://thepiratebay.se', - 'http://pirateproxy.ca', - 'http://tpb.al', + 'http://thepiratebay.cd', + 'http://thebootlegbay.com', 'http://www.tpb.gr', - 'http://bayproxy.me', - 'http://proxybay.eu', + 'http://tpbproxy.co.uk', + 'http://pirateproxy.in', 'http://www.getpirate.com', 'http://piratebay.io', + 'http://bayproxy.li', + 'http://proxybay.pw', ] def _search(self, media, quality, results): @@ -110,7 +113,7 @@ class Base(TorrentMagnetProvider): html = BeautifulSoup(full_description) nfo_pre = html.find('div', attrs = {'class': 'nfo'}) description = '' - try: + try: description = toUnicode(nfo_pre.text) except: pass From bf81b5cacc75d6fab9cdadde1bb817b8bc544cf0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 13:24:23 +0200 Subject: [PATCH 336/468] Move automation provider --- .../movie}/providers/automation/popular_movies/__init__.py | 0 .../{ => media/movie}/providers/automation/popular_movies/main.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename couchpotato/core/{ => media/movie}/providers/automation/popular_movies/__init__.py (100%) rename couchpotato/core/{ => media/movie}/providers/automation/popular_movies/main.py (100%) diff --git a/couchpotato/core/providers/automation/popular_movies/__init__.py b/couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py similarity index 100% rename from couchpotato/core/providers/automation/popular_movies/__init__.py rename to couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py diff --git a/couchpotato/core/providers/automation/popular_movies/main.py b/couchpotato/core/media/movie/providers/automation/popular_movies/main.py similarity index 100% rename from couchpotato/core/providers/automation/popular_movies/main.py rename to couchpotato/core/media/movie/providers/automation/popular_movies/main.py From 9d011b42a947b79ec259333cbb05cb383e8db520 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 13:30:40 +0200 Subject: [PATCH 337/468] Moved PopularMovies automation to single file --- .../automation/popular_movies/__init__.py | 24 ---------- .../automation/popular_movies/main.py | 22 ---------- .../providers/automation/popularmovies.py | 44 +++++++++++++++++++ 3 files changed, 44 insertions(+), 46 deletions(-) delete mode 100644 couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py delete mode 100644 couchpotato/core/media/movie/providers/automation/popular_movies/main.py create mode 100644 couchpotato/core/media/movie/providers/automation/popularmovies.py diff --git a/couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py b/couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py deleted file mode 100644 index e2eace30..00000000 --- a/couchpotato/core/media/movie/providers/automation/popular_movies/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from .main import PopularMovies - -def start(): - return PopularMovies() - -config = [{ - 'name': 'popular_movies', - 'groups': [ - { - 'tab': 'automation', - 'list': 'automation_providers', - 'name': 'popular_movies_automation', - 'label': 'Popular Movies', - 'description': 'Imports the top titles of movies that have been in theaters.', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/automation/popular_movies/main.py b/couchpotato/core/media/movie/providers/automation/popular_movies/main.py deleted file mode 100644 index 6c56cf5a..00000000 --- a/couchpotato/core/media/movie/providers/automation/popular_movies/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from couchpotato.core.logger import CPLog -from couchpotato.core.providers.automation.base import Automation -import datetime - -log = CPLog(__name__) - - -class PopularMovies(Automation): - - interval = 1800 - url = 'https://s3.amazonaws.com/popular-movies/movies.json' - - def getIMDBids(self): - - movies = [] - retrieved_movies = self.getJsonData(self.url) - - for movie in retrieved_movies.get('movies'): - imdb_id = movie.get('imdb_id') - movies.append(imdb_id) - - return movies diff --git a/couchpotato/core/media/movie/providers/automation/popularmovies.py b/couchpotato/core/media/movie/providers/automation/popularmovies.py new file mode 100644 index 00000000..a1418c2b --- /dev/null +++ b/couchpotato/core/media/movie/providers/automation/popularmovies.py @@ -0,0 +1,44 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media.movie.providers.automation.base import Automation + +log = CPLog(__name__) + +autoload = 'PopularMovies' + + +class PopularMovies(Automation): + + interval = 1800 + url = 'https://s3.amazonaws.com/popular-movies/movies.json' + + def getIMDBids(self): + + movies = [] + retrieved_movies = self.getJsonData(self.url) + + for movie in retrieved_movies.get('movies'): + imdb_id = movie.get('imdb_id') + movies.append(imdb_id) + + return movies + + +config = [{ + 'name': 'popular_movies', + 'groups': [ + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'popular_movies_automation', + 'label': 'Popular Movies', + 'description': 'Imports the top titles of movies that have been in theaters. Script provided by Steven Lu', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] From 6c4184d1f5b6722654f928a9a09b0ae6e3b2e602 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 13:37:30 +0200 Subject: [PATCH 338/468] Use minimal requirements for popular movie automation --- .../core/media/movie/providers/automation/popularmovies.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/automation/popularmovies.py b/couchpotato/core/media/movie/providers/automation/popularmovies.py index a1418c2b..486922f7 100644 --- a/couchpotato/core/media/movie/providers/automation/popularmovies.py +++ b/couchpotato/core/media/movie/providers/automation/popularmovies.py @@ -1,3 +1,4 @@ +from couchpotato import fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.automation.base import Automation @@ -18,7 +19,9 @@ class PopularMovies(Automation): for movie in retrieved_movies.get('movies'): imdb_id = movie.get('imdb_id') - movies.append(imdb_id) + info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True) + if self.isMinimalMovie(info): + movies.append(imdb_id) return movies From 0145aecab42528a707f82b865dd2f9504bc4760a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 13:53:45 +0200 Subject: [PATCH 339/468] data['size'] sometimes doesn't exist --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index c24d64f6..2b8d1bd1 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -471,7 +471,7 @@ class Scanner(Plugin): data['size'] = data.get('size', 0) + self.getFileSize(cur_file) data['quality'] = None - quality = fireEvent('quality.guess', size = data['size'], files = files, extra = data, single = True) + quality = fireEvent('quality.guess', size = data.get('size'), files = files, extra = data, single = True) # Use the quality that we snatched but check if it matches our guess if release_download and release_download.get('quality'): From 4cf62f73dab3e24f26714a9a09afa1cc7b70f07b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 19:51:58 +0200 Subject: [PATCH 340/468] Use proper conf variable --- .../core/media/movie/providers/automation/popularmovies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/popularmovies.py b/couchpotato/core/media/movie/providers/automation/popularmovies.py index 486922f7..eb46ecef 100644 --- a/couchpotato/core/media/movie/providers/automation/popularmovies.py +++ b/couchpotato/core/media/movie/providers/automation/popularmovies.py @@ -27,12 +27,12 @@ class PopularMovies(Automation): config = [{ - 'name': 'popular_movies', + 'name': 'popularmovies', 'groups': [ { 'tab': 'automation', 'list': 'automation_providers', - 'name': 'popular_movies_automation', + 'name': 'popularmovies_automation', 'label': 'Popular Movies', 'description': 'Imports the top titles of movies that have been in theaters. Script provided by Steven Lu', 'options': [ From 616434a00f83f7bfc7c36b09e04a457bfe20ccbd Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 21:31:06 +0200 Subject: [PATCH 341/468] Delay release cleanup --- couchpotato/core/plugins/release/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 8af26fd2..3cf136de 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -58,11 +58,12 @@ class Release(Plugin): addEvent('release.for_media', self.forMedia) # Clean releases that didn't have activity in the last week - addEvent('app.load', self.cleanDone) + addEvent('app.load', self.cleanDone, priority = 1000) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 12) def cleanDone(self): log.debug('Removing releases from dashboard') + time.sleep(3) now = time.time() week = 262080 From 9ccd4a5e84c7e0219c0019f3e7b037f0ed412181 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 21:56:37 +0200 Subject: [PATCH 342/468] Shutdown logging --- couchpotato/core/logger.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/couchpotato/core/logger.py b/couchpotato/core/logger.py index 6bbe1953..fba8d623 100644 --- a/couchpotato/core/logger.py +++ b/couchpotato/core/logger.py @@ -25,6 +25,12 @@ class CPLog(object): self.Env = Env self.is_develop = Env.get('dev') + from couchpotato.core.event import addEvent + addEvent('app.after_shutdown', self.close) + + def close(self, *args, **kwargs): + logging.shutdown() + def info(self, msg, replace_tuple = ()): self.logger.info(self.addContext(msg, replace_tuple)) From 84a7cfe07db0557e2c46929a0ebcee04257f3d47 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 22:09:18 +0200 Subject: [PATCH 343/468] Add CP version by default in logs --- couchpotato/core/plugins/log/static/log.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index d39a20a9..8b97dab4 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -17,7 +17,7 @@ Page.Log = new Class({ Movie(s) I have this with: ...\n\ Quality of the movie being searched: ...\n\ Providers I use: ...\n\ -Version of CouchPotato: ...\n\ +Version of CouchPotato: {version}\n\ Running on: ...\n\ \n\ ### Logs:\n\ @@ -207,7 +207,10 @@ Running on: ...\n\ showReport: function(text){ var self = this, - body = self.report_text.replace('{issue}', text); + version = Updater.getInfo(), + body = self.report_text + .replace('{issue}', text) + .replace('{version}', version ? version.version.repr : '...'); var overlay = new Element('div.report', { 'method': 'post', From ba65700aad24f1306f1ac9b44c5ababc3bafcc2d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 31 May 2014 23:46:34 +0200 Subject: [PATCH 344/468] Use textarea value for log posting --- couchpotato/core/plugins/log/static/log.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 8b97dab4..11acb5ca 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -210,7 +210,8 @@ Running on: ...\n\ version = Updater.getInfo(), body = self.report_text .replace('{issue}', text) - .replace('{version}', version ? version.version.repr : '...'); + .replace('{version}', version ? version.version.repr : '...'), + textarea; var overlay = new Element('div.report', { 'method': 'post', @@ -243,7 +244,7 @@ Running on: ...\n\ 'text': ' before posting, then copy the text below' }) ), - new Element('textarea', { + textarea = new Element('textarea', { 'text': body, 'events': { 'click': function(){ @@ -254,7 +255,17 @@ Running on: ...\n\ new Element('a.button', { 'target': '_blank', 'text': 'Create a new issue on GitHub with the text above', - 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here') + 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new', + 'events': { + 'click': function(e){ + (e).stop(); + + var body = textarea.get('value'), + bdy = '?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here'), + win = window.open(e.target.get('href') + bdy, '_blank'); + win.focus(); + } + } }) ) ); From d6d0ff724a24cf85ddd6f5ea41c5ad6517f4186b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 00:11:09 +0200 Subject: [PATCH 345/468] Change label --- couchpotato/core/media/movie/_base/static/movie.actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index 26ee2421..b3878fd5 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -252,7 +252,7 @@ MA.Release = new Class({ self.trynext_container.adopt( new Element('span.or', { - 'text': 'This movie is snatched, if anything went wrong, download' + 'text': 'If anything went wrong, download' }), lr ? new Element('a.button.orange', { 'text': 'the same release again', From 47749c2d73d27d7d80dd3416c95e8640b63f2bf3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 00:23:46 +0200 Subject: [PATCH 346/468] Transmission login failed. #1110 --- couchpotato/core/downloaders/transmission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index 49c8bc58..d941cca6 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -174,8 +174,8 @@ class TransmissionRPC(object): self.session = {} if username and password: password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password) - opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager)) + password_manager.add_password(realm = 'Transmission', uri = self.url, user = username, passwd = password) + opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager)) opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')] urllib2.install_opener(opener) elif username or password: From a28ee58a1f793d6b463ef029901c8819b62ac712 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 00:26:37 +0200 Subject: [PATCH 347/468] Remove digestauth header --- couchpotato/core/downloaders/utorrent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index 08b8d5a5..7ff5930d 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -229,7 +229,6 @@ class uTorrentAPI(object): password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password) self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager)) - self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager)) elif username or password: log.debug('User or password missing, not using authentication.') self.token = self.get_token() From e6b4d32506cd854cac42e416133a67135daa1ee8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 11:37:57 +0200 Subject: [PATCH 348/468] IMDB Watchlist count was off --- couchpotato/core/media/movie/providers/automation/imdb.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index c66d7665..37d762bc 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -3,6 +3,7 @@ import re from bs4 import BeautifulSoup from couchpotato import fireEvent +from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import getImdb, splitString, tryInt from couchpotato.core.logger import CPLog @@ -51,7 +52,7 @@ class IMDBBase(Automation, RSS): except: log.error('Failed parsing IMDB page "%s": %s', (url, traceback.format_exc())) - html = str(html) + html = ss(html) imdbs = getImdb(html, multiple = True) if html else [] return imdbs @@ -104,13 +105,14 @@ class IMDBWatchlist(IMDBBase): log.debug('Found %s movies on %s', (len(imdbs), w_url)) - if len(imdbs) < 250: + if len(imdbs) < 225: break - start += 250 + start = len(movies) except: log.error('Failed loading IMDB watchlist: %s %s', (watchlist_url, traceback.format_exc())) + break return movies From 27f331a1fc48c80b16ae6242e3581b0f25c35fe3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 14:30:45 +0200 Subject: [PATCH 349/468] Don't verify ssl for downloaders --- couchpotato/core/downloaders/nzbvortex.py | 4 ++-- couchpotato/core/downloaders/sabnzbd.py | 2 +- couchpotato/core/plugins/base.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/downloaders/nzbvortex.py b/couchpotato/core/downloaders/nzbvortex.py index 9094055f..92ece6d6 100644 --- a/couchpotato/core/downloaders/nzbvortex.py +++ b/couchpotato/core/downloaders/nzbvortex.py @@ -130,7 +130,7 @@ class NZBVortex(DownloaderBase): url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call try: - data = self.urlopen('%s?%s' % (url, params), *args, **kwargs) + data = self.urlopen('%s?%s' % (url, params), *args, verify_ssl = False, **kwargs) if data: return json.loads(data) @@ -154,7 +154,7 @@ class NZBVortex(DownloaderBase): url = cleanHost(self.conf('host')) + 'api/app/apilevel' try: - data = self.urlopen(url, show_error = False) + data = self.urlopen(url, show_error = False, verify_ssl = False) self.api_level = float(json.loads(data).get('apilevel')) except URLError as e: if hasattr(e, 'code') and e.code == 403: diff --git a/couchpotato/core/downloaders/sabnzbd.py b/couchpotato/core/downloaders/sabnzbd.py index cd51cb87..57e1dca5 100644 --- a/couchpotato/core/downloaders/sabnzbd.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -194,7 +194,7 @@ class Sabnzbd(DownloaderBase): 'output': 'json' })) - data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) + data = self.urlopen(url, timeout = 60, show_error = False, verify_ssl = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) if use_json: d = json.loads(data) if d.get('error'): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index f548cb29..dae522f4 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -161,7 +161,7 @@ class Plugin(object): log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) # http request - def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True): + def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, verify_ssl = True): url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]") if not headers: headers = {} @@ -200,6 +200,7 @@ class Plugin(object): 'data': data if len(data) > 0 else None, 'timeout': timeout, 'files': files, + 'verify': verify_ssl, } method = 'post' if len(data) > 0 or files else 'get' From 8ac851555d0fcced9744a6c0604f286d514c3bed Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 16:14:53 +0200 Subject: [PATCH 350/468] Can't trigger same api call Thread never closes --- couchpotato/api.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index 5fdee986..f251841c 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,5 +1,3 @@ -from functools import wraps -from threading import Thread import json import threading import traceback @@ -9,7 +7,6 @@ from couchpotato.core.helpers.request import getParams from couchpotato.core.logger import CPLog from tornado.gen import coroutine from tornado.web import RequestHandler, asynchronous -import tornado log = CPLog(__name__) @@ -23,16 +20,6 @@ api_docs = {} api_docs_missing = [] -def run_async(func): - @wraps(func) - def async_func(*args, **kwargs): - func_hl = Thread(target = func, args = args, kwargs = kwargs) - func_hl.start() - return func_hl - - return async_func - - # NonBlock API handler class NonBlockHandler(RequestHandler): @@ -101,17 +88,12 @@ class ApiHandler(RequestHandler): try: del kwargs['t'] except: pass - # Add async callback handler - @run_async - def run_handler(callback): - try: - res = api[route](**kwargs) - callback(res) - except: - log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) - callback({'success': False, 'error': 'Failed returning results'}) - - result = yield tornado.gen.Task(run_handler) + # Fire api handler(s) + try: + result = api[route](**kwargs) + except: + log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) + result = {'success': False, 'error': 'Failed returning results'} # Check JSONP callback jsonp_callback = self.get_argument('callback_func', default = None) From 9d9630a27ad8b908c77d303f9e030ce32da5fe8a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 16:46:21 +0200 Subject: [PATCH 351/468] Sorted backup files --- couchpotato/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 3ddd4ac6..3cdd8648 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -99,7 +99,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En if not os.path.isdir(backup_path): os.makedirs(backup_path) for root, dirs, files in os.walk(backup_path): - for backup_file in files: + for backup_file in sorted(files): ints = re.findall('\d+', backup_file) # Delete non zip files From 18807191c003b0688580ee8c1738e7746dd4c397 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 1 Jun 2014 17:36:34 +0200 Subject: [PATCH 352/468] Don't reindex on startup --- couchpotato/core/plugins/release/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 3cf136de..8cd485c2 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -63,7 +63,6 @@ class Release(Plugin): def cleanDone(self): log.debug('Removing releases from dashboard') - time.sleep(3) now = time.time() week = 262080 @@ -88,9 +87,6 @@ class Release(Plugin): del media_exist - # Reindex statuses - db.reindex_index('media_status') - # get movies last_edit more than a week ago medias = fireEvent('media.with_status', 'done', single = True) From 8dead66b58921c0275b7085f2f2111fe07e83fb3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 12:59:21 +0200 Subject: [PATCH 353/468] Migration fixes --- couchpotato/core/database.py | 15 +++++++++------ couchpotato/core/media/movie/_base/main.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index f9607137..40b29285 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -289,13 +289,16 @@ class Database(object): for profile_type in types: p_type = types[profile_type] if types[profile_type]['profile_id'] == p['id']: - new_profile['finish'].append(p_type['finish']) - new_profile['wait_for'].append(p_type['wait_for']) - new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier']) + if p_type['quality_id']: + new_profile['finish'].append(p_type['finish']) + new_profile['wait_for'].append(p_type['wait_for']) + new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier']) - new_profile.update(db.insert(new_profile)) - - profile_link[x] = new_profile.get('_id') + if len(new_profile['qualities']) > 0: + new_profile.update(db.insert(new_profile)) + profile_link[x] = new_profile.get('_id') + else: + log.error('Corrupt profile list for "%s", using default.', p.get('label')) # Qualities log.info('Importing quality sizes') diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 860ec965..4c5c2f29 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -105,7 +105,7 @@ class MovieBase(MovieTypeBase): 'imdb': params.get('identifier') }, 'status': status if status else 'active', - 'profile_id': params.get('profile_id', default_profile.get('_id')), + 'profile_id': params.get('profile_id') or default_profile.get('_id'), 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None, } From 40f01dca6f04eb48f0b9940774a3ba3cc834a2f4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 13:31:18 +0200 Subject: [PATCH 354/468] Use async request for all api calls --- couchpotato/api.py | 58 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index f251841c..7b863559 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,3 +1,5 @@ +from functools import wraps +from threading import Thread import json import threading import traceback @@ -5,7 +7,6 @@ import urllib from couchpotato.core.helpers.request import getParams from couchpotato.core.logger import CPLog -from tornado.gen import coroutine from tornado.web import RequestHandler, asynchronous @@ -20,6 +21,24 @@ api_docs = {} api_docs_missing = [] +def run_async(func): + @wraps(func) + def async_func(*args, **kwargs): + func_hl = Thread(target = func, args = args, kwargs = kwargs) + func_hl.start() + + return async_func + +@run_async +def run_handler(route, kwargs, callback = None): + try: + res = api[route](**kwargs) + callback(res) + except: + log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) + callback({'success': False, 'error': 'Failed returning results'}) + + # NonBlock API handler class NonBlockHandler(RequestHandler): @@ -65,7 +84,7 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): # Blocking API handler class ApiHandler(RequestHandler): - @coroutine + @asynchronous def get(self, route, *args, **kwargs): route = route.strip('/') if not api.get(route): @@ -88,31 +107,40 @@ class ApiHandler(RequestHandler): try: del kwargs['t'] except: pass - # Fire api handler(s) - try: - result = api[route](**kwargs) - except: - log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) - result = {'success': False, 'error': 'Failed returning results'} + # Add async callback handler + run_handler(route, kwargs, callback = self.taskFinished) + except: + log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) + self.write({'success': False, 'error': 'Failed returning results'}) + self.finish() + + api_locks[route].release() + + post = get + + def taskFinished(self, result): + + if self.request.connection.stream.closed(): + return + + try: # Check JSONP callback jsonp_callback = self.get_argument('callback_func', default = None) if jsonp_callback: self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')') self.set_header("Content-Type", "text/javascript") + self.finish() elif isinstance(result, tuple) and result[0] == 'redirect': self.redirect(result[1]) else: self.write(result) - + self.finish() except: - log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) - self.write({'success': False, 'error': 'Failed returning results'}) - - api_locks[route].release() - - post = get + log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc())) + try: self.finish({'success': False, 'error': 'Failed returning results'}) + except: pass def addApiView(route, func, static = False, docs = None, **kwargs): From 3bd831782cac6a09b5132d6534601497d9f5e456 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 14:02:55 +0200 Subject: [PATCH 355/468] Release lock inside thread --- couchpotato/api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index 7b863559..cd0bb046 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -33,10 +33,10 @@ def run_async(func): def run_handler(route, kwargs, callback = None): try: res = api[route](**kwargs) - callback(res) + callback(res, route) except: log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) - callback({'success': False, 'error': 'Failed returning results'}) + callback({'success': False, 'error': 'Failed returning results'}, route) # NonBlock API handler @@ -115,11 +115,11 @@ class ApiHandler(RequestHandler): self.write({'success': False, 'error': 'Failed returning results'}) self.finish() - api_locks[route].release() + api_locks[route].release() post = get - def taskFinished(self, result): + def taskFinished(self, result, route): if self.request.connection.stream.closed(): return @@ -142,6 +142,8 @@ class ApiHandler(RequestHandler): try: self.finish({'success': False, 'error': 'Failed returning results'}) except: pass + api_locks[route].release() + def addApiView(route, func, static = False, docs = None, **kwargs): From 0314910bbeb234a2a2cde0ab8033484324d9e7e1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 14:11:26 +0200 Subject: [PATCH 356/468] Don't migrate empty library items --- couchpotato/core/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 40b29285..1b9501c2 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -372,10 +372,10 @@ class Database(object): m = medias[x] status = statuses.get(m['status_id']).get('identifier') - l = libraries[m['library_id']] + l = libraries.get(m['library_id']) # Only migrate wanted movies, Skip if no identifier present - if not getImdb(l.get('identifier')): continue + if not l or not getImdb(l.get('identifier')): continue profile_id = profile_link.get(m['profile_id']) category_id = category_link.get(m['category_id']) From 6d048e0003a44b7c8377dfd7fe9056738e0bce6d Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 14:35:06 +0200 Subject: [PATCH 357/468] Don't try to parse faulty IMDB page --- couchpotato/core/media/movie/providers/automation/imdb.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 37d762bc..9b4c1171 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -39,6 +39,11 @@ class IMDBBase(Automation, RSS): except: try: split = splitString(html, split_on = "
") + + if len(split) < 2: + log.error('Failed parsing IMDB page "%s", unexpected html.', url) + return [] + html = BeautifulSoup(split[1]) for x in ['list compact', 'lister', 'list detail sub-list']: html2 = html.find('div', attrs = { From c82b1f51e3fe7802d8d0db43f513b5017d4d103e Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 14:44:09 +0200 Subject: [PATCH 358/468] Get messages from last 7 days, not just unread. fix #3331 --- couchpotato/core/notifications/core/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 0cd8e579..526099fd 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -258,14 +258,14 @@ class CoreNotifier(Notification): messages = [] - # Get unread + # Get last message if init: db = get_db() - notifications = db.all('notification_unread', with_doc = True) + notifications = db.all('notification', with_doc = True) for n in notifications: - if n['doc'].get('time') > (time.time() - 259200): + if n['doc'].get('time') > (time.time() - 604800): messages.append(n['doc']) return { From 681d8b1ddc736ccc90e118cc10a91c95dfa41bac Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 22:23:26 +0200 Subject: [PATCH 359/468] Simplify fanart provider --- .../media/movie/providers/info/fanarttv.py | 95 +++++++-------- libs/fanarttv/__init__.py | 110 ------------------ libs/fanarttv/core.py | 44 ------- libs/fanarttv/errors.py | 15 --- libs/fanarttv/immutable.py | 46 -------- libs/fanarttv/items.py | 68 ----------- libs/fanarttv/movie.py | 103 ---------------- libs/fanarttv/music.py | 80 ------------- libs/fanarttv/tv.py | 108 ----------------- 9 files changed, 40 insertions(+), 629 deletions(-) delete mode 100644 libs/fanarttv/__init__.py delete mode 100644 libs/fanarttv/core.py delete mode 100644 libs/fanarttv/errors.py delete mode 100644 libs/fanarttv/immutable.py delete mode 100644 libs/fanarttv/items.py delete mode 100644 libs/fanarttv/movie.py delete mode 100644 libs/fanarttv/music.py delete mode 100644 libs/fanarttv/tv.py diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index fe4bb93a..1d7fbce3 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -1,12 +1,9 @@ -import os import traceback +from couchpotato import tryInt from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.base import MovieProvider -from couchpotato.core.plugins.quality import QualityPlugin -import libs.fanarttv.errors as fanarttv_errors -from libs.fanarttv.movie import Movie log = CPLog(__name__) @@ -16,16 +13,17 @@ autoload = 'FanartTV' class FanartTV(MovieProvider): + urls = { + 'api': 'http://api.fanart.tv/webservice/movie/%s/%s/JSON/all/1/2' + } + MAX_EXTRAFANART = 20 def __init__(self): - addEvent('movie.extra_art', self.getArt, priority=2) + addEvent('movie.info', self.getArt, priority = 1) - # Configure fanarttv API settings - os.environ.setdefault('FANART_APIKEY', self.conf('api_key')) + def getArt(self, identifier = None, **kwargs): - def getArt(self, identifier): - # FIXME: I believe I should be registering a cache here... I need to look into that. log.debug("Getting Extra Artwork from Fanart.tv...") if not identifier: return {} @@ -33,23 +31,24 @@ class FanartTV(MovieProvider): images = {} try: - try: - exists = True - movie = Movie.get(id = identifier) - except (fanarttv_errors.FanartError, IOError): - exists = False + url = self.urls['api'] % (self.conf('api_key'), identifier) + fanart_data = self.getJsonData(url) - if exists: - images = self._parseMovie(movie, True) + if fanart_data: + name, resource = fanart_data.items()[0] + log.debug('Found images for %s', name) + images = self._parseMovie(resource) except: log.error('Failed getting extra art for %s: %s', (identifier, traceback.format_exc())) return {} - return images + return { + 'images': images + } - def _parseMovie(self, movie, is_hd): + def _parseMovie(self, movie): images = { 'landscape': [], 'logo': [], @@ -59,56 +58,48 @@ class FanartTV(MovieProvider): 'extra_fanart': [], } - images['landscape'] = self._getMultImages(movie.thumbs, 1) - images['banner'] = self._getMultImages(movie.banners, 1) - images['disc_art'] = self._getMultImages(self._trimDiscs(movie.discs, is_hd), 1) + images['landscape'] = self._getMultImages(movie.get('moviethumb', []), 1) + images['banner'] = self._getMultImages(movie.get('moviebanner', []), 1) + images['disc_art'] = self._getMultImages(self._trimDiscs(movie.get('moviedisc', [])), 1) - images['clear_art'] = self._getMultImages(movie.hdarts, 1) - if len(images['clear_art']) is 0: - images['clear_art'] = self._getMultImages(movie.arts, 1) + images['clear_art'] = self._getMultImages(movie.get('hdmovieart', []), 1) + if len(images['clear_art']) == 0: + images['clear_art'] = self._getMultImages(movie.get('movieart', []), 1) - images['logo'] = self._getMultImages(movie.hdlogos, 1) - if len(images['logo']) is 0: - images['logo'] = self._getMultImages(movie.logos, 1) + images['logo'] = self._getMultImages(movie.get('hdmovielogo', []), 1) + if len(images['logo']) == 0: + images['logo'] = self._getMultImages(movie.get('movielogo', []), 1) - fanarts = self._getMultImages(movie.backgrounds, self.MAX_EXTRAFANART + 1) + fanarts = self._getMultImages(movie.get('moviebackground', []), self.MAX_EXTRAFANART + 1) if fanarts: images['backdrop_original'] = fanarts[0] images['extra_fanart'] = fanarts[1:] - # TODO: Add support for extra backgrounds - #extra_fanart = self._getMultImages(movie.backgrounds, -1) - return images - def _trimDiscs(self, disc_images, is_hd): + def _trimDiscs(self, disc_images): """ - Return a subset of discImages based on isHD. If isHD is true, only - bluray disc images will be returned. If isHD is false, only dvd disc - images will be returned. If the resulting list would be an empty list, - then the original list is returned instead. + Return a subset of discImages. Only bluray disc images will be returned. """ trimmed = [] for disc in disc_images: - if is_hd and disc.disc_type == u'bluray': - trimmed.append(disc) - elif not is_hd and disc.disc_type == u'dvd': + if disc.get('disc_type') == 'bluray': trimmed.append(disc) - if len(trimmed) is 0: + if len(trimmed) == 0: return disc_images - else: - return trimmed + + return trimmed def _getImage(self, images): image_url = None highscore = -1 for image in images: - if image.likes > highscore: - highscore = image.likes - image_url = image.url + if tryInt(image.get('likes')) > highscore: + highscore = tryInt(image.get('likes')) + image_url = image.get('url') return image_url @@ -120,7 +111,7 @@ class FanartTV(MovieProvider): image_urls = [] pool = [] for image in images: - if image.lang == u'en': + if image.get('lang') == 'en': pool.append(image) orig_pool_size = len(pool) @@ -128,10 +119,10 @@ class FanartTV(MovieProvider): best = None highscore = -1 for image in pool: - if image.likes > highscore: - highscore = image.likes + if tryInt(image.get('likes')) > highscore: + highscore = tryInt(image.get('likes')) best = image - image_urls.append(best.url) + image_urls.append(best.get('url')) pool.remove(best) return image_urls @@ -142,12 +133,6 @@ class FanartTV(MovieProvider): return True return False - def _determineHD(self, quality): - for qualityDef in QualityPlugin.qualities: - if quality == qualityDef.get('identifier'): - return bool(qualityDef.get('hd')) - return False - config = [{ 'name': 'fanarttv', diff --git a/libs/fanarttv/__init__.py b/libs/fanarttv/__init__.py deleted file mode 100644 index 773703ca..00000000 --- a/libs/fanarttv/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -__author__ = 'Andrea De Marco <24erre@gmail.com>' -__version__ = '1.4.0' -__classifiers__ = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development :: Libraries', -] -__copyright__ = "2012, %s " % __author__ -__license__ = """ - Copyright %s. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" % __copyright__ - -__docformat__ = 'restructuredtext en' - -__doc__ = """ -:abstract: Python interface to fanart.tv API -:version: %s -:author: %s -:contact: http://z4r.github.com/ -:date: 2012-04-04 -:copyright: %s -""" % (__version__, __author__, __license__) - - -def values(obj): - return [v for k, v in obj.__dict__.iteritems() if not k.startswith('_')] - -BASEURL = 'http://api.fanart.tv/webservice' - - -class FORMAT(object): - JSON = 'JSON' - XML = 'XML' - PHP = 'PHP' - - -class WS(object): - MUSIC = 'artist' - MOVIE = 'movie' - TV = 'series' - - -class TYPE(object): - ALL = 'all' - - class TV(object): - ART = 'clearart' - LOGO = 'clearlogo' - CHARACTER = 'characterart' - THUMB = 'tvthumb' - SEASONTHUMB = 'seasonthumb' - BACKGROUND = 'showbackground' - HDLOGO = 'hdtvlogo' - HDART = 'hdclearart' - POSTER = 'tvposter' - BANNER = 'tvbanner' - - class MUSIC(object): - DISC = 'cdart' - LOGO = 'musiclogo' - BACKGROUND = 'artistbackground' - COVER = 'albumcover' - THUMB = 'artistthumb' - - class MOVIE(object): - ART = 'movieart' - LOGO = 'movielogo' - DISC = 'moviedisc' - POSTER = 'movieposter' - BACKGROUND = 'moviebackground' - HDLOGO = 'hdmovielogo' - HDART = 'hdmovieclearart' - BANNER = 'moviebanner' - THUMB = 'moviethumb' - - -class SORT(object): - POPULAR = 1 - NEWEST = 2 - OLDEST = 3 - - -class LIMIT(object): - ONE = 1 - ALL = 2 - -FORMAT_LIST = values(FORMAT) -WS_LIST = values(WS) -TYPE_LIST = values(TYPE.MUSIC) + values(TYPE.TV) + values(TYPE.MOVIE) + [TYPE.ALL] -MUSIC_TYPE_LIST = values(TYPE.MUSIC) + [TYPE.ALL] -TV_TYPE_LIST = values(TYPE.TV) + [TYPE.ALL] -MOVIE_TYPE_LIST = values(TYPE.MOVIE) + [TYPE.ALL] -SORT_LIST = values(SORT) -LIMIT_LIST = values(LIMIT) diff --git a/libs/fanarttv/core.py b/libs/fanarttv/core.py deleted file mode 100644 index 9cd1cad7..00000000 --- a/libs/fanarttv/core.py +++ /dev/null @@ -1,44 +0,0 @@ -import libs.requests as requests -import libs.fanarttv as fanart -from libs.fanarttv.errors import RequestFanartError, ResponseFanartError - - -class Request(object): - def __init__(self, apikey, id, ws, type=None, sort=None, limit=None): - self._apikey = apikey - self._id = id - self._ws = ws - self._type = type or fanart.TYPE.ALL - self._sort = sort or fanart.SORT.POPULAR - self._limit = limit or fanart.LIMIT.ALL - self.validate() - self._response = None - - def validate(self): - for attribute_name in ('ws', 'type', 'sort', 'limit'): - attribute = getattr(self, '_' + attribute_name) - choices = getattr(fanart, attribute_name.upper() + '_LIST') - if attribute not in choices: - raise RequestFanartError('Not allowed {0}: {1} [{2}]'.format(attribute_name, attribute, ', '.join(choices))) - - def __str__(self): - return '/'.join(map(str, [ - fanart.BASEURL, - self._ws, - self._apikey, - self._id, - fanart.FORMAT.JSON, - self._type, - self._sort, - self._limit, - ])) - - def response(self): - try: - response = requests.get(str(self)) - rjson = response.json() - if not isinstance(rjson, dict): - raise Exception(response.text) - return rjson - except Exception as e: - raise ResponseFanartError(str(e)) diff --git a/libs/fanarttv/errors.py b/libs/fanarttv/errors.py deleted file mode 100644 index 95a71e35..00000000 --- a/libs/fanarttv/errors.py +++ /dev/null @@ -1,15 +0,0 @@ -class FanartError(Exception): - def __str__(self): - return ', '.join(map(str, self.args)) - - def __repr__(self): - name = self.__class__.__name__ - return '%s%r' % (name, self.args) - - -class ResponseFanartError(FanartError): - pass - - -class RequestFanartError(FanartError): - pass diff --git a/libs/fanarttv/immutable.py b/libs/fanarttv/immutable.py deleted file mode 100644 index 170de370..00000000 --- a/libs/fanarttv/immutable.py +++ /dev/null @@ -1,46 +0,0 @@ -class Immutable(object): - _mutable = False - - def __setattr__(self, name, value): - if self._mutable or name == '_mutable': - super(Immutable, self).__setattr__(name, value) - else: - raise TypeError("Can't modify immutable instance") - - def __delattr__(self, name): - if self._mutable: - super(Immutable, self).__delattr__(name) - else: - raise TypeError("Can't modify immutable instance") - - def __eq__(self, other): - return hash(self) == hash(other) - - def __hash__(self): - return hash(repr(self)) - - def __repr__(self): - return '%s(%s)' % ( - self.__class__.__name__, - ', '.join(['{0}={1}'.format(k, repr(v)) for k, v in self]) - ) - - def __iter__(self): - l = self.__dict__.keys() - l.sort() - for k in l: - if not k.startswith('_'): - yield k, getattr(self, k) - - @staticmethod - def mutablemethod(f): - def func(self, *args, **kwargs): - if isinstance(self, Immutable): - old_mutable = self._mutable - self._mutable = True - res = f(self, *args, **kwargs) - self._mutable = old_mutable - else: - res = f(self, *args, **kwargs) - return res - return func diff --git a/libs/fanarttv/items.py b/libs/fanarttv/items.py deleted file mode 100644 index 64848076..00000000 --- a/libs/fanarttv/items.py +++ /dev/null @@ -1,68 +0,0 @@ -import json -import os -import libs.requests as requests -from libs.fanarttv.core import Request -from libs.fanarttv.immutable import Immutable - - -class LeafItem(Immutable): - KEY = NotImplemented - - @Immutable.mutablemethod - def __init__(self, id, url, likes): - self.id = int(id) - self.url = url - self.likes = int(likes) - self._content = None - - @classmethod - def from_dict(cls, resource): - return cls(**dict([(str(k), v) for k, v in resource.iteritems()])) - - @classmethod - def extract(cls, resource): - return [cls.from_dict(i) for i in resource.get(cls.KEY, {})] - - @Immutable.mutablemethod - def content(self): - if not self._content: - self._content = requests.get(self.url).content - return self._content - - def __str__(self): - return self.url - - -class ResourceItem(Immutable): - WS = NotImplemented - request_cls = Request - - @classmethod - def from_dict(cls, map): - raise NotImplementedError - - @classmethod - def get(cls, id): - map = cls.request_cls( - apikey=os.environ.get('FANART_APIKEY'), - id=id, - ws=cls.WS - ).response() - return cls.from_dict(map) - - def json(self, **kw): - return json.dumps( - self, - default=lambda o: dict([(k, v) for k, v in o.__dict__.items() if not k.startswith('_')]), - **kw - ) - - -class CollectableItem(Immutable): - @classmethod - def from_dict(cls, key, map): - raise NotImplementedError - - @classmethod - def collection_from_dict(cls, map): - return [cls.from_dict(k, v) for k, v in map.iteritems()] diff --git a/libs/fanarttv/movie.py b/libs/fanarttv/movie.py deleted file mode 100644 index fe473d4d..00000000 --- a/libs/fanarttv/movie.py +++ /dev/null @@ -1,103 +0,0 @@ -import libs.fanarttv as fanart -from libs.fanarttv.items import LeafItem, Immutable, ResourceItem -__all__ = ( - 'ArtItem', - 'DiscItem', - 'LogoItem', - 'PosterItem', - 'BackgroundItem', - 'HdLogoItem', - 'HdArtItem', - 'BannerItem', - 'ThumbItem', - 'Movie', -) - - -class MovieItem(LeafItem): - - @Immutable.mutablemethod - def __init__(self, id, url, likes, lang): - super(MovieItem, self).__init__(id, url, likes) - self.lang = lang - - -class DiscItem(MovieItem): - KEY = fanart.TYPE.MOVIE.DISC - - @Immutable.mutablemethod - def __init__(self, id, url, likes, lang, disc, disc_type): - super(DiscItem, self).__init__(id, url, likes, lang) - self.disc = int(disc) - self.disc_type = disc_type - - -class ArtItem(MovieItem): - KEY = fanart.TYPE.MOVIE.ART - - -class LogoItem(MovieItem): - KEY = fanart.TYPE.MOVIE.LOGO - - -class PosterItem(MovieItem): - KEY = fanart.TYPE.MOVIE.POSTER - - -class BackgroundItem(MovieItem): - KEY = fanart.TYPE.MOVIE.BACKGROUND - - -class HdLogoItem(MovieItem): - KEY = fanart.TYPE.MOVIE.HDLOGO - - -class HdArtItem(MovieItem): - KEY = fanart.TYPE.MOVIE.HDART - - -class BannerItem(MovieItem): - KEY = fanart.TYPE.MOVIE.BANNER - - -class ThumbItem(MovieItem): - KEY = fanart.TYPE.MOVIE.THUMB - - -class Movie(ResourceItem): - WS = fanart.WS.MOVIE - - @Immutable.mutablemethod - def __init__(self, name, imdbid, tmdbid, arts, logos, discs, posters, backgrounds, hdlogos, hdarts, - banners, thumbs): - self.name = name - self.imdbid = imdbid - self.tmdbid = tmdbid - self.arts = arts - self.posters = posters - self.logos = logos - self.discs = discs - self.backgrounds = backgrounds - self.hdlogos = hdlogos - self.hdarts = hdarts - self.banners = banners - self.thumbs = thumbs - - @classmethod - def from_dict(cls, resource): - assert len(resource) == 1, 'Bad Format Map' - name, resource = resource.items()[0] - return cls( - name=name, - imdbid=resource['imdb_id'], - tmdbid=resource['tmdb_id'], - arts=ArtItem.extract(resource), - logos=LogoItem.extract(resource), - discs=DiscItem.extract(resource), - posters=PosterItem.extract(resource), - backgrounds=BackgroundItem.extract(resource), - hdlogos=HdLogoItem.extract(resource), - hdarts=HdArtItem.extract(resource), - banners=BannerItem.extract(resource), - thumbs=ThumbItem.extract(resource), - ) diff --git a/libs/fanarttv/music.py b/libs/fanarttv/music.py deleted file mode 100644 index df634c67..00000000 --- a/libs/fanarttv/music.py +++ /dev/null @@ -1,80 +0,0 @@ -from libs.fanarttv.items import Immutable, LeafItem, ResourceItem, CollectableItem -import libs.fanarttv as fanart -__all__ = ( - 'BackgroundItem', - 'CoverItem', - 'LogoItem', - 'ThumbItem', - 'DiscItem', - 'Artist', - 'Album', -) - - -class BackgroundItem(LeafItem): - KEY = fanart.TYPE.MUSIC.BACKGROUND - - -class CoverItem(LeafItem): - KEY = fanart.TYPE.MUSIC.COVER - - -class LogoItem(LeafItem): - KEY = fanart.TYPE.MUSIC.LOGO - - -class ThumbItem(LeafItem): - KEY = fanart.TYPE.MUSIC.THUMB - - -class DiscItem(LeafItem): - KEY = fanart.TYPE.MUSIC.DISC - - @Immutable.mutablemethod - def __init__(self, id, url, likes, disc, size): - super(DiscItem, self).__init__(id, url, likes) - self.disc = int(disc) - self.size = int(size) - - -class Artist(ResourceItem): - WS = fanart.WS.MUSIC - - @Immutable.mutablemethod - def __init__(self, name, mbid, albums, backgrounds, logos, thumbs): - self.name = name - self.mbid = mbid - self.albums = albums - self.backgrounds = backgrounds - self.logos = logos - self.thumbs = thumbs - - @classmethod - def from_dict(cls, resource): - assert len(resource) == 1, 'Bad Format Map' - name, resource = resource.items()[0] - return cls( - name=name, - mbid=resource['mbid_id'], - albums=Album.collection_from_dict(resource.get('albums', {})), - backgrounds=BackgroundItem.extract(resource), - thumbs=ThumbItem.extract(resource), - logos=LogoItem.extract(resource), - ) - - -class Album(CollectableItem): - - @Immutable.mutablemethod - def __init__(self, mbid, covers, arts): - self.mbid = mbid - self.covers = covers - self.arts = arts - - @classmethod - def from_dict(cls, key, resource): - return cls( - mbid=key, - covers=CoverItem.extract(resource), - arts=DiscItem.extract(resource), - ) diff --git a/libs/fanarttv/tv.py b/libs/fanarttv/tv.py deleted file mode 100644 index 9b1b08a7..00000000 --- a/libs/fanarttv/tv.py +++ /dev/null @@ -1,108 +0,0 @@ -import libs.fanarttv as fanart -from libs.fanarttv.items import LeafItem, Immutable, ResourceItem -__all__ = ( - 'CharacterItem', - 'ArtItem', - 'LogoItem', - 'BackgroundItem', - 'SeasonItem', - 'ThumbItem', - 'HdLogoItem', - 'HdArtItem', - 'PosterItem', - 'BannerItem', - 'TvShow', -) - - -class TvItem(LeafItem): - @Immutable.mutablemethod - def __init__(self, id, url, likes, lang): - super(TvItem, self).__init__(id, url, likes) - self.lang = lang - - -class SeasonedTvItem(TvItem): - @Immutable.mutablemethod - def __init__(self, id, url, likes, lang, season): - super(SeasonedTvItem, self).__init__(id, url, likes, lang) - self.season = 0 if season == 'all' else int(season or 0) - - -class CharacterItem(TvItem): - KEY = fanart.TYPE.TV.CHARACTER - - -class ArtItem(TvItem): - KEY = fanart.TYPE.TV.ART - - -class LogoItem(TvItem): - KEY = fanart.TYPE.TV.LOGO - - -class BackgroundItem(SeasonedTvItem): - KEY = fanart.TYPE.TV.BACKGROUND - - -class SeasonItem(SeasonedTvItem): - KEY = fanart.TYPE.TV.SEASONTHUMB - - -class ThumbItem(TvItem): - KEY = fanart.TYPE.TV.THUMB - - -class HdLogoItem(TvItem): - KEY = fanart.TYPE.TV.HDLOGO - - -class HdArtItem(TvItem): - KEY = fanart.TYPE.TV.HDART - - -class PosterItem(TvItem): - KEY = fanart.TYPE.TV.POSTER - - -class BannerItem(TvItem): - KEY = fanart.TYPE.TV.BANNER - - -class TvShow(ResourceItem): - WS = fanart.WS.TV - - @Immutable.mutablemethod - def __init__(self, name, tvdbid, backgrounds, characters, arts, logos, seasons, thumbs, hdlogos, hdarts, posters, - banners): - self.name = name - self.tvdbid = tvdbid - self.backgrounds = backgrounds - self.characters = characters - self.arts = arts - self.logos = logos - self.seasons = seasons - self.thumbs = thumbs - self.hdlogos = hdlogos - self.hdarts = hdarts - self.posters = posters - self.banners = banners - - @classmethod - def from_dict(cls, resource): - assert len(resource) == 1, 'Bad Format Map' - name, resource = resource.items()[0] - return cls( - name=name, - tvdbid=resource['thetvdb_id'], - backgrounds=BackgroundItem.extract(resource), - characters=CharacterItem.extract(resource), - arts=ArtItem.extract(resource), - logos=LogoItem.extract(resource), - seasons=SeasonItem.extract(resource), - thumbs=ThumbItem.extract(resource), - hdlogos=HdLogoItem.extract(resource), - hdarts=HdArtItem.extract(resource), - posters=PosterItem.extract(resource), - banners=BannerItem.extract(resource), - ) From aa804471a7203b4d6f12323a61826f91aa35640b Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 22:27:56 +0200 Subject: [PATCH 360/468] Prioritize image info --- .../movie/providers/info/couchpotatoapi.py | 2 +- .../media/movie/providers/info/fanarttv.py | 25 ++----------------- .../media/movie/providers/info/themoviedb.py | 9 +------ 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/couchpotato/core/media/movie/providers/info/couchpotatoapi.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py index 272fab9b..4c65bf8c 100644 --- a/couchpotato/core/media/movie/providers/info/couchpotatoapi.py +++ b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py @@ -29,7 +29,7 @@ class CouchPotatoApi(MovieProvider): api_version = 1 def __init__(self): - addEvent('movie.info', self.getInfo, priority = 1) + addEvent('movie.info', self.getInfo, priority = 2) addEvent('movie.info.release_date', self.getReleaseDate) addEvent('info.search', self.search, priority = 1) diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index 1d7fbce3..e443318d 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -14,7 +14,7 @@ autoload = 'FanartTV' class FanartTV(MovieProvider): urls = { - 'api': 'http://api.fanart.tv/webservice/movie/%s/%s/JSON/all/1/2' + 'api': 'http://api.fanart.tv/webservice/movie/b28b14e9be662e027cfbc7c3dd600405/%s/JSON/all/1/2' } MAX_EXTRAFANART = 20 @@ -31,7 +31,7 @@ class FanartTV(MovieProvider): images = {} try: - url = self.urls['api'] % (self.conf('api_key'), identifier) + url = self.urls['api'] % identifier fanart_data = self.getJsonData(url) if fanart_data: @@ -132,24 +132,3 @@ class FanartTV(MovieProvider): log.error('No API key provided.') return True return False - - -config = [{ - 'name': 'fanarttv', - 'groups': [ - { - 'tab': 'providers', - 'name': 'fanarttv', - 'label': 'fanart.tv', - 'hidden': True, - 'description': 'Used for all calls to fanart.tv.', - 'options': [ - { - 'name': 'api_key', - 'default': 'd788b4822b9e1f44068026e05557e5d9', - 'label': 'API Key', - }, - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index 9b002af9..45b7701c 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -16,7 +16,7 @@ class TheMovieDb(MovieProvider): MAX_EXTRATHUMBS = 4 def __init__(self): - addEvent('movie.info', self.getInfo, priority = 2) + addEvent('movie.info', self.getInfo, priority = 3) addEvent('movie.info_by_tmdb', self.getInfo) # Configure TMDB settings @@ -88,13 +88,6 @@ class TheMovieDb(MovieProvider): except: log.error('Failed getting info for %s: %s', (identifier, traceback.format_exc())) - # Get extra artwork via Fanart.TV and merge into images dict - try: - extra_art = fireEvent('movie.extra_art', identifier)[0] - result['images'] = dict(result['images'].items() + extra_art.items()) - except IndexError: - pass - return result def parseMovie(self, movie, extended = True): From 32bcf6e61521eca9915daa928f2fa44b27b41b30 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 22:36:09 +0200 Subject: [PATCH 361/468] Requests 2.3.1 --- libs/requests/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/requests/models.py b/libs/requests/models.py index 9b16b9d8..5aad8ce4 100644 --- a/libs/requests/models.py +++ b/libs/requests/models.py @@ -309,8 +309,8 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): p = PreparedRequest() p.method = self.method p.url = self.url - p.headers = self.headers.copy() - p._cookies = self._cookies.copy() + p.headers = self.headers.copy() if self.headers is not None else None + p._cookies = self._cookies.copy() if self._cookies is not None else None p.body = self.body p.hooks = self.hooks return p From 937036611239c5d8a08616ca7209f3ee8893cdbd Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 22:57:27 +0200 Subject: [PATCH 362/468] Don't limit fanart calls --- couchpotato/core/media/movie/providers/info/fanarttv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index e443318d..8f067d44 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -18,6 +18,7 @@ class FanartTV(MovieProvider): } MAX_EXTRAFANART = 20 + http_time_between_calls = 0 def __init__(self): addEvent('movie.info', self.getArt, priority = 1) From 6b06caf00d12f76a5f8788b4aea25d43df0338c6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 2 Jun 2014 22:57:45 +0200 Subject: [PATCH 363/468] Api call release lock never triggered --- couchpotato/api.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index cd0bb046..17942583 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -112,8 +112,11 @@ class ApiHandler(RequestHandler): except: log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) - self.write({'success': False, 'error': 'Failed returning results'}) - self.finish() + try: + self.write({'success': False, 'error': 'Failed returning results'}) + self.finish() + except: + log.error('Failed write error "%s": %s', (route, traceback.format_exc())) api_locks[route].release() @@ -121,26 +124,24 @@ class ApiHandler(RequestHandler): def taskFinished(self, result, route): - if self.request.connection.stream.closed(): - return + if not self.request.connection.stream.closed(): + try: + # Check JSONP callback + jsonp_callback = self.get_argument('callback_func', default = None) - try: - # Check JSONP callback - jsonp_callback = self.get_argument('callback_func', default = None) - - if jsonp_callback: - self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')') - self.set_header("Content-Type", "text/javascript") - self.finish() - elif isinstance(result, tuple) and result[0] == 'redirect': - self.redirect(result[1]) - else: - self.write(result) - self.finish() - except: - log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc())) - try: self.finish({'success': False, 'error': 'Failed returning results'}) - except: pass + if jsonp_callback: + self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')') + self.set_header("Content-Type", "text/javascript") + self.finish() + elif isinstance(result, tuple) and result[0] == 'redirect': + self.redirect(result[1]) + else: + self.write(result) + self.finish() + except: + log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc())) + try: self.finish({'success': False, 'error': 'Failed returning results'}) + except: pass api_locks[route].release() From 81de9529c32bf77d397ec1a11b5360a85c8490eb Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 16:52:44 +0200 Subject: [PATCH 364/468] Force folder creation on startup --- couchpotato/runner.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 3cdd8648..060b7ad3 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -126,13 +126,23 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En else: db.create() + # Force creation of cachedir + log_dir = sp(log_dir) + cache_dir = sp(os.path.join(data_dir, 'cache')) + python_cache = sp(os.path.join(cache_dir, 'python')) + + if not os.path.exists(cache_dir): + os.mkdir(cache_dir) + if not os.path.exists(python_cache): + os.mkdir(python_cache) + # Register environment settings Env.set('app_dir', sp(base_path)) Env.set('data_dir', sp(data_dir)) Env.set('log_path', sp(os.path.join(log_dir, 'CouchPotato.log'))) Env.set('db', db) - Env.set('cache_dir', sp(os.path.join(data_dir, 'cache'))) - Env.set('cache', FileSystemCache(sp(os.path.join(Env.get('cache_dir'), 'python')))) + Env.set('cache_dir', cache_dir) + Env.set('cache', FileSystemCache(python_cache)) Env.set('console_log', options.console_log) Env.set('quiet', options.quiet) Env.set('desktop', desktop) From daa06628694df7f4d28d84fa0a8515e8dd8ed12a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 16:54:08 +0200 Subject: [PATCH 365/468] XMPP was importing itself --- couchpotato/core/notifications/{xmpp.py => xmpp_.py} | 2 ++ 1 file changed, 2 insertions(+) rename couchpotato/core/notifications/{xmpp.py => xmpp_.py} (99%) diff --git a/couchpotato/core/notifications/xmpp.py b/couchpotato/core/notifications/xmpp_.py similarity index 99% rename from couchpotato/core/notifications/xmpp.py rename to couchpotato/core/notifications/xmpp_.py index f9916cd0..9cee8c5a 100644 --- a/couchpotato/core/notifications/xmpp.py +++ b/couchpotato/core/notifications/xmpp_.py @@ -5,6 +5,8 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification import xmpp +print xmpp + log = CPLog(__name__) From b894139ca10fda63272294ffa408372f7cf3f221 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 16:54:21 +0200 Subject: [PATCH 366/468] Make full path for logs --- CouchPotato.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CouchPotato.py b/CouchPotato.py index 364d77a4..f21b9393 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -50,7 +50,7 @@ class Loader(object): # Create logging dir self.log_dir = os.path.join(self.data_dir, 'logs'); if not os.path.isdir(self.log_dir): - os.mkdir(self.log_dir) + os.makedirs(self.log_dir) # Logging from couchpotato.core.logger import CPLog From 1b44fc40af53e70f2ca2b237ddacc2414c43be07 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 17:26:38 +0200 Subject: [PATCH 367/468] Properly delete from late-list. fix #3350 --- couchpotato/core/media/_base/media/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index beb9dd02..6da14565 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -387,7 +387,7 @@ class MediaPlugin(MediaBase): db.delete(release) total_deleted += 1 - if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active'): + if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active') or (not new_media_status and delete_from == 'late'): db.delete(media) deleted = True elif new_media_status: From a3bffb58678ce4a32babec24cd1dceacbed6d6da Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 17:32:53 +0200 Subject: [PATCH 368/468] Use searchOnTitle for TorrentLeech. fix #3351 --- .../core/media/_base/providers/torrent/torrentleech.py | 4 ++-- .../core/media/movie/providers/torrent/torrentleech.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py index fe427985..a5916272 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -24,9 +24,9 @@ class Base(TorrentProvider): http_time_between_calls = 1 # Seconds cat_backup_id = None - def _search(self, media, quality, results): + def _searchOnTitle(self, title, media, quality, results): - url = self.urls['search'] % self.buildUrl(media, quality) + url = self.urls['search'] % self.buildUrl(title, media, quality) data = self.getHTMLData(url) diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py index 9ab1580e..191ceba8 100644 --- a/couchpotato/core/media/movie/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py @@ -1,4 +1,3 @@ -from couchpotato import fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.torrentleech import Base @@ -21,8 +20,8 @@ class TorrentLeech(MovieProvider, Base): ([12], ['dvdr']), ] - def buildUrl(self, media, quality): + def buildUrl(self, title, media, quality): return ( - tryUrlencode(fireEvent('library.query', media, single = True)), + tryUrlencode(title.replace(':', '')), self.getCatId(quality)[0] ) From 2396fadf04e40dd9c3311d5d98373663289bcc3f Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 20:25:57 +0200 Subject: [PATCH 369/468] Remove debug print --- couchpotato/core/notifications/xmpp_.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/couchpotato/core/notifications/xmpp_.py b/couchpotato/core/notifications/xmpp_.py index 9cee8c5a..f9916cd0 100644 --- a/couchpotato/core/notifications/xmpp_.py +++ b/couchpotato/core/notifications/xmpp_.py @@ -5,8 +5,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification import xmpp -print xmpp - log = CPLog(__name__) From 4b236c6ed690ef5f6249ce48dcf06f187a3db830 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 22:25:17 +0200 Subject: [PATCH 370/468] Only cleanup source folders --- couchpotato/core/_base/_core.py | 3 ++- couchpotato/core/plugins/base.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index ca45ceb0..91cb97e9 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -77,7 +77,8 @@ class Core(Plugin): return True def cleanUpFolders(self): - self.deleteEmptyFolder(Env.get('app_dir'), show_error = False) + only_clean = ['couchpotato', 'libs', 'init'] + self.deleteEmptyFolder(Env.get('app_dir'), show_error = False, only_clean = only_clean) def available(self, **kwargs): return { diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index dae522f4..af314bfd 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -140,13 +140,31 @@ class Plugin(object): return False - def deleteEmptyFolder(self, folder, show_error = True): + def deleteEmptyFolder(self, folder, show_error = True, only_clean = None): folder = sp(folder) + allowed_dirs = [] + if only_clean: + for item in os.listdir(folder): + full_path = os.path.join(folder, item) + if item in only_clean and os.path.isdir(full_path): + allowed_dirs.append(full_path) + for root, dirs, files in os.walk(folder): for dir_name in dirs: full_path = os.path.join(root, dir_name) + + if only_clean: + allow = False + for allowed_dir in allowed_dirs: + if allowed_dir in full_path: + allow = True + break + + if not allow: + continue + if len(os.listdir(full_path)) == 0: try: os.rmdir(full_path) From ab2b2cfe6ea13ef5287cc59bc9497320a536f22e Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 22:49:12 +0200 Subject: [PATCH 371/468] Cleaner empty dir cleanup --- couchpotato/core/plugins/base.py | 36 +++++++++++--------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index af314bfd..5198b464 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -143,34 +143,22 @@ class Plugin(object): def deleteEmptyFolder(self, folder, show_error = True, only_clean = None): folder = sp(folder) - allowed_dirs = [] - if only_clean: - for item in os.listdir(folder): - full_path = os.path.join(folder, item) - if item in only_clean and os.path.isdir(full_path): - allowed_dirs.append(full_path) + for item in os.listdir(folder): + full_folder = os.path.join(folder, item) - for root, dirs, files in os.walk(folder): + if not only_clean or (item in only_clean and os.path.isdir(full_folder)): - for dir_name in dirs: - full_path = os.path.join(root, dir_name) + for root, dirs, files in os.walk(full_folder): - if only_clean: - allow = False - for allowed_dir in allowed_dirs: - if allowed_dir in full_path: - allow = True - break + for dir_name in dirs: + full_path = os.path.join(root, dir_name) - if not allow: - continue - - if len(os.listdir(full_path)) == 0: - try: - os.rmdir(full_path) - except: - if show_error: - log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) + if len(os.listdir(full_path)) == 0: + try: + os.rmdir(full_path) + except: + if show_error: + log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) try: os.rmdir(folder) From 6b9b446e3db68a30d3f09e30b8947734382c62a5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 22:53:30 +0200 Subject: [PATCH 372/468] Quality guess keyerror. fix #3347 --- couchpotato/core/plugins/quality/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index e52e3d57..a055f312 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -351,7 +351,8 @@ class QualityPlugin(Plugin): # Give panelty for all lower qualities for q in self.qualities[self.order.index(quality.get('identifier'))+1:]: - score[q.get('identifier')]['score'] -= 1 + if score.get(q.get('identifier')): + score[q.get('identifier')]['score'] -= 1 def isFinish(self, quality, profile): if not isinstance(profile, dict) or not profile.get('qualities'): From 426155e65c7805d3615a0d642ad560d712ba0e35 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Jun 2014 23:34:32 +0200 Subject: [PATCH 373/468] Add extra score if size is unique. fix #3344 --- couchpotato/core/plugins/quality/main.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index a055f312..4c86f6ca 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -207,15 +207,22 @@ class QualityPlugin(Plugin): self.calcScore(score, quality, contains_score, threedscore) - # Evaluate score based on size + size_scores = [] for quality in qualities: - size_score = self.guessSizeScore(quality, size = size) - self.calcScore(score, quality, size_score, penalty = False) - # Try again with loose testing - for quality in qualities: + # Evaluate score based on size + size_score = self.guessSizeScore(quality, size = size) loose_score = self.guessLooseScore(quality, extra = extra) - self.calcScore(score, quality, loose_score, penalty = False) + + if size_score > 0: + size_scores.append(quality) + + self.calcScore(score, quality, size_score + loose_score, penalty = False) + + # Add additional size score if only 1 size validated + if len(size_scores) == 1: + self.calcScore(score, size_scores[0], 10, penalty = False) + del size_scores # Return nothing if all scores are <= 0 has_non_zero = 0 @@ -325,6 +332,8 @@ class QualityPlugin(Plugin): if tryInt(quality['size_min']) <= tryInt(size) <= tryInt(quality['size_max']): log.debug('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max'])) score += 5 + else: + score -= 5 return score @@ -413,6 +422,7 @@ class QualityPlugin(Plugin): 'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True}, 'Movie Monuments 2013 BrRip 1080p': {'size': 1800, 'quality': 'brrip'}, 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'}, + 'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True}, } correct = 0 @@ -420,7 +430,7 @@ class QualityPlugin(Plugin): test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {} success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False) if not success: - log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier'))) + log.error('%s failed check, thinks it\'s %s', (name, test_quality.get('identifier'))) correct += success From e717a49c0cac32f74c6d6decc88e7416d352875e Mon Sep 17 00:00:00 2001 From: Clinton Hall Date: Wed, 4 Jun 2014 13:29:20 +0930 Subject: [PATCH 374/468] Fix instructions for Ubuntu --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dd5f831..4dbe75bb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ Linux (Ubuntu / Debian): * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Then do `python CouchPotatoServer/CouchPotato.py` to start * To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` -* Change the paths inside the init script `sudo nano /etc/init.d/couchpotato` +* Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato` +* Change the paths inside the default file `sudo nano /etc/default/couchpotato` * Make it executable `sudo chmod +x /etc/init.d/couchpotato` * Add it to defaults `sudo update-rc.d couchpotato defaults` * Open your browser and go to `http://localhost:5050/` From f0daee669b35e4b3f625de45d5c2803aeeb7b9f8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 10:53:08 +0200 Subject: [PATCH 375/468] Add opener to env --- couchpotato/core/plugins/base.py | 3 +-- couchpotato/environment.py | 3 +-- couchpotato/runner.py | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 5198b464..de6af555 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -40,7 +40,6 @@ class Plugin(object): http_time_between_calls = 0 http_failed_request = {} http_failed_disabled = {} - http_opener = requests.Session() def __new__(cls, *args, **kwargs): new_plugin = super(Plugin, cls).__new__(cls) @@ -184,7 +183,7 @@ class Plugin(object): headers['Connection'] = headers.get('Connection', 'keep-alive') headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0') - r = self.http_opener + r = Env.get('http_opener') # Don't try for failed requests if self.http_failed_disabled.get(host, 0) > 0: diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 8c1ee17c..658b744d 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -24,8 +24,7 @@ class Env(object): _quiet = False _daemonized = False _desktop = None - _engine = None - _session = None + _http_opener = None ''' Data paths and directories ''' _app_dir = "" diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 060b7ad3..b2b918a6 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -18,6 +18,7 @@ from couchpotato.api import NonBlockHandler, ApiHandler from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getDataDir, tryInt +import requests from tornado.httpserver import HTTPServer from tornado.web import Application, StaticFileHandler, RedirectHandler @@ -141,6 +142,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En Env.set('data_dir', sp(data_dir)) Env.set('log_path', sp(os.path.join(log_dir, 'CouchPotato.log'))) Env.set('db', db) + Env.set('http_opener', requests.Session()) Env.set('cache_dir', cache_dir) Env.set('cache', FileSystemCache(python_cache)) Env.set('console_log', options.console_log) From c90a42301212daaba78f2118997b26f4f6c95d7d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 11:23:21 +0200 Subject: [PATCH 376/468] Disable SSL verification --- couchpotato/core/plugins/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index de6af555..311ce1ec 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -205,7 +205,7 @@ class Plugin(object): 'data': data if len(data) > 0 else None, 'timeout': timeout, 'files': files, - 'verify': verify_ssl, + 'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates.. } method = 'post' if len(data) > 0 or files else 'get' From fa7e59e8423bcbaaf21d0af5262b95e34802e690 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 17:26:45 +0200 Subject: [PATCH 377/468] Don't save profile order twice --- .../core/plugins/profile/static/profile.css | 6 +++--- .../core/plugins/quality/static/quality.js | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index 073fa573..f8a1b422 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -159,9 +159,6 @@ } #profile_ordering li { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.2); padding: 0 5px; } @@ -183,6 +180,9 @@ background: url('../../images/handle.png') center; width: 20px; float: right; + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; } #profile_ordering .formHint { diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 285ed1d9..601438d3 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -135,20 +135,28 @@ var QualityBase = new Class({ }); // Sortable + var sorted_changed = false; self.profile_sortable = new Sortables(profile_list, { 'revert': true, - 'handle': '', + 'handle': '.handle', 'opacity': 0.5, - 'onComplete': self.saveProfileOrdering.bind(self) + 'onSort': function(){ + sorted_changed = true; + }, + 'onComplete': function(){ + if(sorted_changed){ + self.saveProfileOrdering(); + sorted_changed = false; + } + } }); }, saveProfileOrdering: function(){ - var self = this; - - var ids = []; - var hidden = []; + var self = this, + ids = [], + hidden = []; self.profile_sortable.list.getElements('li').each(function(el, nr){ ids.include(el.get('data-id')); From cce92dc1f87102c7fee28cc18e686d97356c50e3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 18:04:13 +0200 Subject: [PATCH 378/468] Don't test for redirect. fix #3381 --- couchpotato/core/media/_base/providers/nzb/newznab.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 04915776..d3c4e34f 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -184,16 +184,7 @@ class Base(NZBProvider, RSS): return 'try_next' try: - # Get final redirected url - log.debug('Checking %s for redirects.', url) - req = urllib2.Request(url) - req.add_header('User-Agent', self.user_agent) - res = urllib2.urlopen(req) - finalurl = res.geturl() - if finalurl != url: - log.debug('Redirect url used: %s', finalurl) - - data = self.urlopen(finalurl, show_error = False) + data = self.urlopen(url, show_error = False) self.limits_reached[host] = False return data except HTTPError as e: From 7d2b2b9809a4d96787472019546bc41a4b6efd30 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 18:09:17 +0200 Subject: [PATCH 379/468] Metadata fixes --- couchpotato/core/media/movie/providers/info/fanarttv.py | 2 +- couchpotato/core/media/movie/providers/metadata/wmc.py | 2 +- couchpotato/core/media/movie/providers/metadata/xbmc.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py index 8f067d44..2aeaad65 100644 --- a/couchpotato/core/media/movie/providers/info/fanarttv.py +++ b/couchpotato/core/media/movie/providers/info/fanarttv.py @@ -74,7 +74,7 @@ class FanartTV(MovieProvider): fanarts = self._getMultImages(movie.get('moviebackground', []), self.MAX_EXTRAFANART + 1) if fanarts: - images['backdrop_original'] = fanarts[0] + images['backdrop_original'] = [fanarts[0]] images['extra_fanart'] = fanarts[1:] return images diff --git a/couchpotato/core/media/movie/providers/metadata/wmc.py b/couchpotato/core/media/movie/providers/metadata/wmc.py index 8faa6d2a..3cb9e3c7 100644 --- a/couchpotato/core/media/movie/providers/metadata/wmc.py +++ b/couchpotato/core/media/movie/providers/metadata/wmc.py @@ -8,7 +8,7 @@ autoload = 'WindowsMediaCenter' class WindowsMediaCenter(MovieMetaData): - def getThumbnailName(self, name, root): + def getThumbnailName(self, name, root, i): return os.path.join(root, 'folder.jpg') diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py index c25b5891..ff0c119f 100644 --- a/couchpotato/core/media/movie/providers/metadata/xbmc.py +++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py @@ -303,7 +303,7 @@ config = [{ 'advanced': True, }, { - 'name': 'meta_disc', + 'name': 'meta_disc_art', 'label': 'DiscArt', 'default': False, 'type': 'bool' From 0b5976bdb12780d6ae1796af16df69a90f1a14f5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 18:51:51 +0200 Subject: [PATCH 380/468] Catch HTTPError properly in trailer search. fix #3388 --- couchpotato/core/media/movie/providers/trailer/hdtrailers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py index ad040a3b..79425332 100644 --- a/couchpotato/core/media/movie/providers/trailer/hdtrailers.py +++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py @@ -1,5 +1,4 @@ from string import digits, ascii_letters -from urllib2 import HTTPError import re from bs4 import SoupStrainer, BeautifulSoup @@ -7,6 +6,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import mergeDicts, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider +from requests import HTTPError log = CPLog(__name__) From f6789f79ead25e2587234d923fe08ade12d47c10 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 20:14:57 +0200 Subject: [PATCH 381/468] Import cleanup --- couchpotato/core/media/_base/providers/nzb/newznab.py | 3 +-- couchpotato/core/plugins/base.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index d3c4e34f..13655eed 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -1,9 +1,7 @@ -from urllib2 import HTTPError from urlparse import urlparse import time import traceback import re -import urllib2 from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.rss import RSS @@ -13,6 +11,7 @@ from couchpotato.core.media._base.providers.base import ResultList from couchpotato.core.media._base.providers.nzb.base import NZBProvider from couchpotato.environment import Env from dateutil.parser import parse +from requests import HTTPError log = CPLog(__name__) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 311ce1ec..234302df 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -1,3 +1,4 @@ +from urllib import quote from urlparse import urlparse import glob import inspect @@ -5,7 +6,6 @@ import os.path import re import time import traceback -import urllib2 from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toSafeString, \ @@ -167,7 +167,7 @@ class Plugin(object): # http request def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, verify_ssl = True): - url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]") + url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]") if not headers: headers = {} if not data: data = {} From 2eac2946439aa3430736049bd02811a287b8bf86 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 20:42:01 +0200 Subject: [PATCH 382/468] Allow already deleted releases --- couchpotato/core/plugins/release/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 8cd485c2..d84f3941 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -167,6 +167,9 @@ class Release(Plugin): rel = db.get('id', release_id) db.delete(rel) return True + except RecordDeleted: + log.error('Already deleted: %s', release_id) + return True except: log.error('Failed: %s', traceback.format_exc()) From 05f4b2b8ced304bb353ba8df8be6a3838eca330c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 21:36:35 +0200 Subject: [PATCH 383/468] Allow full scan and quick scan separately --- couchpotato/core/plugins/manage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index ab29e72d..54683c85 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -4,7 +4,6 @@ import sys import time import traceback -from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.helpers.encoding import sp @@ -71,7 +70,8 @@ class Manage(Plugin): return self.updateLibrary(full = False) def updateLibrary(self, full = True): - last_update = float(Env.prop('manage.last_update', default = 0)) + last_update_key = 'manage.last_update%s' % ('_full' if full else '') + last_update = float(Env.prop(last_update_key, default = 0)) if self.in_progress: log.info('Already updating library: %s', self.in_progress) @@ -162,7 +162,7 @@ class Manage(Plugin): used_files[release_file] = release del used_files - Env.prop('manage.last_update', time.time()) + Env.prop(last_update_key, time.time()) except: log.error('Failed updating library: %s', (traceback.format_exc())) From bff05925e8e865635af4f190ea7555494d4dcd79 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 21:37:16 +0200 Subject: [PATCH 384/468] Only allow 3d tag as single word, not partial. fix #3368 --- couchpotato/core/plugins/quality/main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 4c86f6ca..7bb33791 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -37,7 +37,7 @@ class QualityPlugin(Plugin): threed_tags = { 'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'], 'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'], - '3d': ['2d3d', '3d2d'], + '3d': ['2d3d', '3d2d', '3d'], } cached_qualities = None @@ -290,14 +290,14 @@ class QualityPlugin(Plugin): tags = self.threed_tags.get(key, []) for tag in tags: - if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in cur_file.lower()): + if isinstance(tag, tuple): + if len(set(words) & set(tag)) == len(tag): + log.debug('Found %s in %s', (tag, cur_file)) + return 1, key + elif tag in words: log.debug('Found %s in %s', (tag, cur_file)) return 1, key - if list(set([key]) & set(words)): - log.debug('Found %s in %s', (key, cur_file)) - return 1, key - return 0, None def guessLooseScore(self, quality, extra = None): @@ -423,6 +423,8 @@ class QualityPlugin(Plugin): 'Movie Monuments 2013 BrRip 1080p': {'size': 1800, 'quality': 'brrip'}, 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'}, 'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True}, + '/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': False}, + '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True} } correct = 0 From e5cfafdb0094bb1e06c2d5df547baaf803ea088f Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jun 2014 22:25:16 +0200 Subject: [PATCH 385/468] Update Tornado 3.2.2 --- libs/tornado/auth.py | 89 ++++++++++++++++++++++--------- libs/tornado/http1connection.py | 52 ++++++++++++++----- libs/tornado/ioloop.py | 15 +++--- libs/tornado/util.py | 43 +++++++++++++++ libs/tornado/web.py | 92 ++++++++++++++++++++++++++++++--- libs/tornado/websocket.py | 38 +------------- 6 files changed, 240 insertions(+), 89 deletions(-) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 6892f91a..f15413e5 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -34,15 +34,29 @@ See the individual service classes below for complete documentation. Example usage for Google OpenID:: - class GoogleLoginHandler(tornado.web.RequestHandler, - tornado.auth.GoogleMixin): + class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, + tornado.auth.GoogleOAuth2Mixin): @tornado.gen.coroutine def get(self): - if self.get_argument("openid.mode", None): - user = yield self.get_authenticated_user() - # Save the user with e.g. set_secure_cookie() + if self.get_argument('code', False): + user = yield self.get_authenticated_user( + redirect_uri='http://your.site.com/auth/google', + code=self.get_argument('code')) + # Save the user with e.g. set_secure_cookie else: - yield self.authenticate_redirect() + yield self.authorize_redirect( + redirect_uri='http://your.site.com/auth/google', + client_id=self.settings['google_oauth']['key'], + scope=['profile', 'email'], + response_type='code', + extra_params={'approval_prompt': 'auto'}) + +.. versionchanged:: 3.3 + All of the callback interfaces in this module are now guaranteed + to run their callback with an argument of ``None`` on error. + Previously some functions would do this while others would simply + terminate the request on their own. This change also ensures that + errors are more consistently reported through the ``Future`` interfaces. """ from __future__ import absolute_import, division, print_function, with_statement @@ -61,6 +75,7 @@ from tornado import httpclient from tornado import escape from tornado.httputil import url_concat from tornado.log import gen_log +from tornado.stack_context import ExceptionStackContext from tornado.util import bytes_type, u, unicode_type, ArgReplacer try: @@ -108,7 +123,14 @@ def _auth_return_future(f): if callback is not None: future.add_done_callback( functools.partial(_auth_future_to_callback, callback)) - f(*args, **kwargs) + def handle_exception(typ, value, tb): + if future.done(): + return False + else: + future.set_exc_info((typ, value, tb)) + return True + with ExceptionStackContext(handle_exception): + f(*args, **kwargs) return future return wrapper @@ -166,7 +188,7 @@ class OpenIdMixin(object): url = self._OPENID_ENDPOINT if http_client is None: http_client = self.get_auth_http_client() - http_client.fetch(url, self.async_callback( + http_client.fetch(url, functools.partial( self._on_authentication_verified, callback), method="POST", body=urllib_parse.urlencode(args)) @@ -338,7 +360,7 @@ class OAuthMixin(object): http_client.fetch( self._oauth_request_token_url(callback_uri=callback_uri, extra_params=extra_params), - self.async_callback( + functools.partial( self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri, @@ -346,7 +368,7 @@ class OAuthMixin(object): else: http_client.fetch( self._oauth_request_token_url(), - self.async_callback( + functools.partial( self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri, callback)) @@ -383,7 +405,7 @@ class OAuthMixin(object): if http_client is None: http_client = self.get_auth_http_client() http_client.fetch(self._oauth_access_token_url(token), - self.async_callback(self._on_access_token, callback)) + functools.partial(self._on_access_token, callback)) def _oauth_request_token_url(self, callback_uri=None, extra_params=None): consumer_token = self._oauth_consumer_token() @@ -460,7 +482,7 @@ class OAuthMixin(object): access_token = _oauth_parse_response(response.body) self._oauth_get_user_future(access_token).add_done_callback( - self.async_callback(self._on_oauth_get_user, access_token, future)) + functools.partial(self._on_oauth_get_user, access_token, future)) def _oauth_consumer_token(self): """Subclasses must override this to return their OAuth consumer keys. @@ -645,7 +667,7 @@ class TwitterMixin(OAuthMixin): """ http = self.get_auth_http_client() http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), - self.async_callback( + functools.partial( self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None, callback)) @@ -703,7 +725,7 @@ class TwitterMixin(OAuthMixin): if args: url += "?" + urllib_parse.urlencode(args) http = self.get_auth_http_client() - http_callback = self.async_callback(self._on_twitter_request, callback) + http_callback = functools.partial(self._on_twitter_request, callback) if post_args is not None: http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args), callback=http_callback) @@ -820,7 +842,7 @@ class FriendFeedMixin(OAuthMixin): args.update(oauth) if args: url += "?" + urllib_parse.urlencode(args) - callback = self.async_callback(self._on_friendfeed_request, callback) + callback = functools.partial(self._on_friendfeed_request, callback) http = self.get_auth_http_client() if post_args is not None: http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args), @@ -861,6 +883,10 @@ class FriendFeedMixin(OAuthMixin): class GoogleMixin(OpenIdMixin, OAuthMixin): """Google Open ID / OAuth authentication. + *Deprecated:* New applications should use `GoogleOAuth2Mixin` + below instead of this class. As of May 19, 2014, Google has stopped + supporting registration-free authentication. + No application registration is necessary to use Google for authentication or to access Google resources on behalf of a user. @@ -931,7 +957,7 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): http = self.get_auth_http_client() token = dict(key=token, secret="") http.fetch(self._oauth_access_token_url(token), - self.async_callback(self._on_access_token, callback)) + functools.partial(self._on_access_token, callback)) else: chain_future(OpenIdMixin.get_authenticated_user(self), callback) @@ -950,6 +976,19 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): class GoogleOAuth2Mixin(OAuth2Mixin): """Google authentication using OAuth2. + In order to use, register your application with Google and copy the + relevant parameters to your application settings. + + * Go to the Google Dev Console at http://console.developers.google.com + * Select a project, or create a new one. + * In the sidebar on the left, select APIs & Auth. + * In the list of APIs, find the Google+ API service and set it to ON. + * In the sidebar on the left, select Credentials. + * In the OAuth section of the page, select Create New Client ID. + * Set the Redirect URI to point to your auth handler + * Copy the "Client secret" and "Client ID" to the application settings as + {"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}} + .. versionadded:: 3.2 """ _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" @@ -963,7 +1002,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): Example usage:: - class GoogleOAuth2LoginHandler(LoginHandler, + class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, tornado.auth.GoogleOAuth2Mixin): @tornado.gen.coroutine def get(self): @@ -990,7 +1029,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): }) http.fetch(self._OAUTH_ACCESS_TOKEN_URL, - self.async_callback(self._on_access_token, callback), + functools.partial(self._on_access_token, callback), method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) def _on_access_token(self, future, response): @@ -1031,7 +1070,7 @@ class FacebookMixin(object): @tornado.web.asynchronous def get(self): if self.get_argument("session", None): - self.get_authenticated_user(self.async_callback(self._on_auth)) + self.get_authenticated_user(self._on_auth) return yield self.authenticate_redirect() @@ -1117,7 +1156,7 @@ class FacebookMixin(object): session = escape.json_decode(self.get_argument("session")) self.facebook_request( method="facebook.users.getInfo", - callback=self.async_callback( + callback=functools.partial( self._on_get_user_info, callback, session), session_key=session["session_key"], uids=session["uid"], @@ -1143,7 +1182,7 @@ class FacebookMixin(object): def get(self): self.facebook_request( method="stream.get", - callback=self.async_callback(self._on_stream), + callback=self._on_stream, session_key=self.current_user["session_key"]) def _on_stream(self, stream): @@ -1167,7 +1206,7 @@ class FacebookMixin(object): url = "http://api.facebook.com/restserver.php?" + \ urllib_parse.urlencode(args) http = self.get_auth_http_client() - http.fetch(url, callback=self.async_callback( + http.fetch(url, callback=functools.partial( self._parse_response, callback)) def _on_get_user_info(self, callback, session, users): @@ -1265,7 +1304,7 @@ class FacebookGraphMixin(OAuth2Mixin): fields.update(extra_fields) http.fetch(self._oauth_request_token_url(**args), - self.async_callback(self._on_access_token, redirect_uri, client_id, + functools.partial(self._on_access_token, redirect_uri, client_id, client_secret, callback, fields)) def _on_access_token(self, redirect_uri, client_id, client_secret, @@ -1282,7 +1321,7 @@ class FacebookGraphMixin(OAuth2Mixin): self.facebook_request( path="/me", - callback=self.async_callback( + callback=functools.partial( self._on_get_user_info, future, session, fields), access_token=session["access_token"], fields=",".join(fields) @@ -1349,7 +1388,7 @@ class FacebookGraphMixin(OAuth2Mixin): if all_args: url += "?" + urllib_parse.urlencode(all_args) - callback = self.async_callback(self._on_facebook_request, callback) + callback = functools.partial(self._on_facebook_request, callback) http = self.get_auth_http_client() if post_args is not None: http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args), diff --git a/libs/tornado/http1connection.py b/libs/tornado/http1connection.py index 9f474e1b..edaa5d9e 100644 --- a/libs/tornado/http1connection.py +++ b/libs/tornado/http1connection.py @@ -31,6 +31,26 @@ from tornado import stack_context from tornado.util import GzipDecompressor +class _QuietException(Exception): + def __init__(self): + pass + +class _ExceptionLoggingContext(object): + """Used with the ``with`` statement when calling delegate methods to + log any exceptions with the given logger. Any exceptions caught are + converted to _QuietException + """ + def __init__(self, logger): + self.logger = logger + + def __enter__(self): + pass + + def __exit__(self, typ, value, tb): + if value is not None: + self.logger.error("Uncaught exception", exc_info=(typ, value, tb)) + raise _QuietException + class HTTP1ConnectionParameters(object): """Parameters for `.HTTP1Connection` and `.HTTP1ServerConnection`. """ @@ -155,9 +175,10 @@ class HTTP1Connection(httputil.HTTPConnection): self._disconnect_on_finish = not self._can_keep_alive( start_line, headers) need_delegate_close = True - header_future = delegate.headers_received(start_line, headers) - if header_future is not None: - yield header_future + with _ExceptionLoggingContext(app_log): + header_future = delegate.headers_received(start_line, headers) + if header_future is not None: + yield header_future if self.stream is None: # We've been detached. need_delegate_close = False @@ -196,7 +217,8 @@ class HTTP1Connection(httputil.HTTPConnection): self._read_finished = True if not self._write_finished or self.is_client: need_delegate_close = False - delegate.finish() + with _ExceptionLoggingContext(app_log): + delegate.finish() # If we're waiting for the application to produce an asynchronous # response, and we're not detached, register a close callback # on the stream (we didn't need one while we were reading) @@ -216,7 +238,8 @@ class HTTP1Connection(httputil.HTTPConnection): raise gen.Return(False) finally: if need_delegate_close: - delegate.on_connection_close() + with _ExceptionLoggingContext(app_log): + delegate.on_connection_close() self._clear_callbacks() raise gen.Return(True) @@ -478,7 +501,8 @@ class HTTP1Connection(httputil.HTTPConnection): min(self.params.chunk_size, content_length), partial=True) content_length -= len(body) if not self._write_finished or self.is_client: - yield gen.maybe_future(delegate.data_received(body)) + with _ExceptionLoggingContext(app_log): + yield gen.maybe_future(delegate.data_received(body)) @gen.coroutine def _read_chunked_body(self, delegate): @@ -498,8 +522,8 @@ class HTTP1Connection(httputil.HTTPConnection): min(bytes_to_read, self.params.chunk_size), partial=True) bytes_to_read -= len(chunk) if not self._write_finished or self.is_client: - yield gen.maybe_future( - delegate.data_received(chunk)) + with _ExceptionLoggingContext(app_log): + yield gen.maybe_future(delegate.data_received(chunk)) # chunk ends with \r\n crlf = yield self.stream.read_bytes(2) assert crlf == b"\r\n" @@ -508,7 +532,8 @@ class HTTP1Connection(httputil.HTTPConnection): def _read_body_until_close(self, delegate): body = yield self.stream.read_until_close() if not self._write_finished or self.is_client: - delegate.data_received(body) + with _ExceptionLoggingContext(app_log): + delegate.data_received(body) class _GzipMessageDelegate(httputil.HTTPMessageDelegate): @@ -610,11 +635,12 @@ class HTTP1ServerConnection(object): except (iostream.StreamClosedError, iostream.UnsatisfiableReadError): return + except _QuietException: + # This exception was already logged. + conn.close() + return except Exception: - # TODO: this is probably too broad; it would be better to - # wrap all delegate calls in something that writes to app_log, - # and then errors that reach this point can be gen_log. - app_log.error("Uncaught exception", exc_info=True) + gen_log.error("Uncaught exception", exc_info=True) conn.close() return if not ret: diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 86c4a9f2..cd59bfee 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -32,6 +32,7 @@ import datetime import errno import functools import heapq +import itertools import logging import numbers import os @@ -585,7 +586,8 @@ class PollIOLoop(IOLoop): self._closing = False self._thread_ident = None self._blocking_signal_threshold = None - + self._timeout_counter = itertools.count() + # Create a pipe that we send bogus data to when we want to wake # the I/O loop when it is idle self._waker = Waker() @@ -835,7 +837,7 @@ class _Timeout(object): """An IOLoop timeout, a UNIX timestamp and a callback""" # Reduce memory overhead when there are lots of pending callbacks - __slots__ = ['deadline', 'callback'] + __slots__ = ['deadline', 'callback', 'tiebreaker'] def __init__(self, deadline, callback, io_loop): if isinstance(deadline, numbers.Real): @@ -849,6 +851,7 @@ class _Timeout(object): else: raise TypeError("Unsupported deadline %r" % deadline) self.callback = callback + self.tiebreaker = next(io_loop._timeout_counter) @staticmethod def timedelta_to_seconds(td): @@ -860,12 +863,12 @@ class _Timeout(object): # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons # use __lt__). def __lt__(self, other): - return ((self.deadline, id(self)) < - (other.deadline, id(other))) + return ((self.deadline, self.tiebreaker) < + (other.deadline, other.tiebreaker)) def __le__(self, other): - return ((self.deadline, id(self)) <= - (other.deadline, id(other))) + return ((self.deadline, self.tiebreaker) <= + (other.deadline, other.tiebreaker)) class PeriodicCallback(object): diff --git a/libs/tornado/util.py b/libs/tornado/util.py index 7fe5b9b2..49eea2c3 100755 --- a/libs/tornado/util.py +++ b/libs/tornado/util.py @@ -12,11 +12,19 @@ and `.Resolver`. from __future__ import absolute_import, division, print_function, with_statement +import array import inspect +import os import sys import zlib +try: + xrange # py2 +except NameError: + xrange = range # py3 + + class ObjectDict(dict): """Makes a dictionary behave like an object, with attribute-style access. """ @@ -303,6 +311,41 @@ class ArgReplacer(object): return old_value, args, kwargs +def _websocket_mask_python(mask, data): + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. + Returns a `bytes` object of the same length as `data` with the mask applied + as specified in section 5.3 of RFC 6455. + + This pure-python implementation may be replaced by an optimized version when available. + """ + mask = array.array("B", mask) + unmasked = array.array("B", data) + for i in xrange(len(data)): + unmasked[i] = unmasked[i] ^ mask[i % 4] + if hasattr(unmasked, 'tobytes'): + # tostring was deprecated in py32. It hasn't been removed, + # but since we turn on deprecation warnings in our tests + # we need to use the right one. + return unmasked.tobytes() + else: + return unmasked.tostring() + +if (os.environ.get('TORNADO_NO_EXTENSION') or + os.environ.get('TORNADO_EXTENSION') == '0'): + # These environment variables exist to make it easier to do performance + # comparisons; they are not guaranteed to remain supported in the future. + _websocket_mask = _websocket_mask_python +else: + try: + from tornado.speedups import websocket_mask as _websocket_mask + except ImportError: + if os.environ.get('TORNADO_EXTENSION') == '1': + raise + _websocket_mask = _websocket_mask_python + + def doctests(): import doctest return doctest.DocTestSuite() diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 5a875fd9..dd2b5ef5 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -72,7 +72,6 @@ import time import tornado import traceback import types -import uuid from tornado.concurrent import Future, is_future from tornado import escape @@ -84,7 +83,7 @@ from tornado.log import access_log, app_log, gen_log from tornado import stack_context from tornado import template from tornado.escape import utf8, _unicode -from tornado.util import bytes_type, import_object, ObjectDict, raise_exc_info, unicode_type +from tornado.util import bytes_type, import_object, ObjectDict, raise_exc_info, unicode_type, _websocket_mask try: from io import BytesIO # python 3 @@ -1076,16 +1075,87 @@ class RequestHandler(object): as a potential forgery. See http://en.wikipedia.org/wiki/Cross-site_request_forgery + + .. versionchanged:: 3.2.2 + The xsrf token will now be have a random mask applied in every + request, which makes it safe to include the token in pages + that are compressed. See http://breachattack.com for more + information on the issue fixed by this change. Old (version 1) + cookies will be converted to version 2 when this method is called + unless the ``xsrf_cookie_version`` `Application` setting is + set to 1. """ if not hasattr(self, "_xsrf_token"): - token = self.get_cookie("_xsrf") - if not token: - token = binascii.b2a_hex(uuid.uuid4().bytes) + version, token, timestamp = self._get_raw_xsrf_token() + output_version = self.settings.get("xsrf_cookie_version", 2) + if output_version == 1: + self._xsrf_token = binascii.b2a_hex(token) + elif output_version == 2: + mask = os.urandom(4) + self._xsrf_token = b"|".join([ + b"2", + binascii.b2a_hex(mask), + binascii.b2a_hex(_websocket_mask(mask, token)), + utf8(str(int(timestamp)))]) + else: + raise ValueError("unknown xsrf cookie version %d", + output_version) + if version is None: expires_days = 30 if self.current_user else None - self.set_cookie("_xsrf", token, expires_days=expires_days) - self._xsrf_token = token + self.set_cookie("_xsrf", self._xsrf_token, + expires_days=expires_days) return self._xsrf_token + def _get_raw_xsrf_token(self): + """Read or generate the xsrf token in its raw form. + + The raw_xsrf_token is a tuple containing: + + * version: the version of the cookie from which this token was read, + or None if we generated a new token in this request. + * token: the raw token data; random (non-ascii) bytes. + * timestamp: the time this token was generated (will not be accurate + for version 1 cookies) + """ + if not hasattr(self, '_raw_xsrf_token'): + cookie = self.get_cookie("_xsrf") + if cookie: + version, token, timestamp = self._decode_xsrf_token(cookie) + else: + version, token, timestamp = None, None, None + if token is None: + version = None + token = os.urandom(16) + timestamp = time.time() + self._raw_xsrf_token = (version, token, timestamp) + return self._raw_xsrf_token + + def _decode_xsrf_token(self, cookie): + """Convert a cookie string into a the tuple form returned by + _get_raw_xsrf_token. + """ + m = _signed_value_version_re.match(utf8(cookie)) + if m: + version = int(m.group(1)) + if version == 2: + _, mask, masked_token, timestamp = cookie.split("|") + mask = binascii.a2b_hex(utf8(mask)) + token = _websocket_mask( + mask, binascii.a2b_hex(utf8(masked_token))) + timestamp = int(timestamp) + return version, token, timestamp + else: + # Treat unknown versions as not present instead of failing. + return None, None, None + elif len(cookie) == 32: + version = 1 + token = binascii.a2b_hex(utf8(cookie)) + # We don't have a usable timestamp in older versions. + timestamp = int(time.time()) + return (version, token, timestamp) + else: + return None, None, None + def check_xsrf_cookie(self): """Verifies that the ``_xsrf`` cookie matches the ``_xsrf`` argument. @@ -1106,13 +1176,19 @@ class RequestHandler(object): information please see http://www.djangoproject.com/weblog/2011/feb/08/security/ http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails + + .. versionchanged:: 3.2.2 + Added support for cookie version 2. Both versions 1 and 2 are + supported. """ token = (self.get_argument("_xsrf", None) or self.request.headers.get("X-Xsrftoken") or self.request.headers.get("X-Csrftoken")) if not token: raise HTTPError(403, "'_xsrf' argument missing from POST") - if self.xsrf_token != token: + _, token, _ = self._decode_xsrf_token(token) + _, expected_token, _ = self._get_raw_xsrf_token() + if not _time_independent_equals(utf8(token), utf8(expected_token)): raise HTTPError(403, "XSRF cookie does not match POST argument") def xsrf_form_html(self): diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 3767a207..c0065c79 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -20,7 +20,6 @@ communication between the browser and server. from __future__ import absolute_import, division, print_function, with_statement # Author: Jacob Kristhammar, 2010 -import array import base64 import collections import functools @@ -39,7 +38,7 @@ from tornado.iostream import StreamClosedError from tornado.log import gen_log, app_log from tornado import simple_httpclient from tornado.tcpclient import TCPClient -from tornado.util import bytes_type, unicode_type +from tornado.util import bytes_type, unicode_type, _websocket_mask try: from urllib.parse import urlparse # py2 @@ -988,38 +987,3 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future - - -def _websocket_mask_python(mask, data): - """Websocket masking function. - - `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length. - Returns a `bytes` object of the same length as `data` with the mask applied - as specified in section 5.3 of RFC 6455. - - This pure-python implementation may be replaced by an optimized version when available. - """ - mask = array.array("B", mask) - unmasked = array.array("B", data) - for i in xrange(len(data)): - unmasked[i] = unmasked[i] ^ mask[i % 4] - if hasattr(unmasked, 'tobytes'): - # tostring was deprecated in py32. It hasn't been removed, - # but since we turn on deprecation warnings in our tests - # we need to use the right one. - return unmasked.tobytes() - else: - return unmasked.tostring() - -if (os.environ.get('TORNADO_NO_EXTENSION') or - os.environ.get('TORNADO_EXTENSION') == '0'): - # These environment variables exist to make it easier to do performance - # comparisons; they are not guaranteed to remain supported in the future. - _websocket_mask = _websocket_mask_python -else: - try: - from tornado.speedups import websocket_mask as _websocket_mask - except ImportError: - if os.environ.get('TORNADO_EXTENSION') == '1': - raise - _websocket_mask = _websocket_mask_python From a2b3677c5920c02a1a1fc677a3977713ff5d7c5f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Jun 2014 08:48:11 +0200 Subject: [PATCH 386/468] Settings.save doc update. closes #3391 --- couchpotato/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/settings.py b/couchpotato/core/settings.py index a0bcd743..f7d3d63e 100644 --- a/couchpotato/core/settings.py +++ b/couchpotato/core/settings.py @@ -49,7 +49,7 @@ class Settings(object): 'desc': 'Save setting to config file (settings.conf)', 'params': { 'section': {'desc': 'The section name in settings.conf'}, - 'option': {'desc': 'The option name'}, + 'name': {'desc': 'The option name'}, 'value': {'desc': 'The value you want to save'}, } }) From cfc49e286bdbaa4e611a68b5b6fc30e0cf1f52cb Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 8 Jun 2014 11:57:53 +0200 Subject: [PATCH 387/468] Allowed datadir giving false positive. fix #3399 --- couchpotato/core/_base/_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 91cb97e9..1171478d 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -8,7 +8,7 @@ import webbrowser from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.variable import cleanHost, md5 +from couchpotato.core.helpers.variable import cleanHost, md5, isSubFolder from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -71,7 +71,7 @@ class Core(Plugin): return value if value and len(value) > 3 else uuid4().hex def checkDataDir(self): - if Env.get('app_dir') in Env.get('data_dir'): + if isSubFolder(Env.get('data_dir'), Env.get('app_dir')): log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.') return True From 1b1c77d225c0aa96fe1690df717365bda19fe6cd Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 11:13:18 +0200 Subject: [PATCH 388/468] Use magnetprovider for yify #3406 --- couchpotato/core/media/_base/providers/torrent/yify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 82413fc5..b1380cd8 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -2,13 +2,13 @@ import traceback from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.base import TorrentProvider +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider log = CPLog(__name__) -class Base(TorrentProvider): +class Base(TorrentMagnetProvider): urls = { 'test': '%s/api', From f5f3cfba50f26e49e8d6d5edd758543ad8505234 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 11:14:19 +0200 Subject: [PATCH 389/468] More general logs --- couchpotato/core/media/_base/providers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index b13b279d..6bcf3332 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -181,7 +181,7 @@ class YarrProvider(Provider): try: return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False) except: - log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc())) + log.error('Failed getting release from %s: %s', (self.getName(), traceback.format_exc())) return 'try_next' From 2769fc28d323d3218c0d0589b01f0ca22f226141 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 13:40:51 +0200 Subject: [PATCH 390/468] Catch RecordNotFound error. fix #3373 --- couchpotato/core/media/_base/media/main.py | 33 +++++++++++++--------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 6da14565..c1fdd92b 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,6 +1,7 @@ import traceback from string import ascii_lowercase +from CodernityDB.database import RecordNotFound from couchpotato import tryInt, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent @@ -119,25 +120,31 @@ class MediaPlugin(MediaBase): def get(self, media_id): - db = get_db() + try: + db = get_db() - imdb_id = getImdb(str(media_id)) + imdb_id = getImdb(str(media_id)) - media = None - if imdb_id: - media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] - else: - media = db.get('id', media_id) + media = None + if imdb_id: + media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] + else: + media = db.get('id', media_id) - if media: + if media: - # Attach category - try: media['category'] = db.get('id', media.get('category_id')) - except: pass + # Attach category + try: media['category'] = db.get('id', media.get('category_id')) + except: pass - media['releases'] = fireEvent('release.for_media', media['_id'], single = True) + media['releases'] = fireEvent('release.for_media', media['_id'], single = True) - return media + return media + + except RecordNotFound: + log.error('Media with id "%s" not found', media_id) + except: + raise def getView(self, id = None, **kwargs): From 17ba9ee96b7ad097382215d3c24ef5c63b2a307f Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 13:54:31 +0200 Subject: [PATCH 391/468] Allow full library refresh interval. fix #2807 --- couchpotato/core/plugins/manage.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 54683c85..5b1cf0ef 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -52,6 +52,20 @@ class Manage(Plugin): if not Env.get('dev') and self.conf('startup_scan'): addEvent('app.load', self.updateLibraryQuick) + addEvent('app.load', self.setCrons) + + # Enable / disable interval + addEvent('setting.save.manage.library_refresh_interval.after', self.setCrons) + + def setCrons(self): + + fireEvent('schedule.remove', 'manage.update_library') + refresh = tryInt(self.conf('library_refresh_interval')) + if refresh > 0: + fireEvent('schedule.interval', 'manage.update_library', self.updateLibrary, hours = refresh, single = True) + + return True + def getProgress(self, **kwargs): return { 'progress': self.in_progress @@ -162,6 +176,10 @@ class Manage(Plugin): used_files[release_file] = release del used_files + # Break if CP wants to shut down + if self.shuttingDown(): + break + Env.prop(last_update_key, time.time()) except: log.error('Failed updating library: %s', (traceback.format_exc())) @@ -308,6 +326,14 @@ config = [{ 'advanced': True, 'description': 'Do a quick scan on startup. On slow systems better disable this.', }, + { + 'label': 'Full library refresh', + 'name': 'library_refresh_interval', + 'type': 'int', + 'default': 0, + 'advanced': True, + 'description': 'Do a full scan every X hours. (0 is disabled)', + }, ], }, ], From 36d82253891bad27191c96a83104c906d1cc6a32 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 21:02:48 +0200 Subject: [PATCH 392/468] Trigger search change on paste. fix #3416 --- couchpotato/core/media/_base/search/static/search.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/media/_base/search/static/search.js b/couchpotato/core/media/_base/search/static/search.js index 1892ad1e..a7cd3640 100644 --- a/couchpotato/core/media/_base/search/static/search.js +++ b/couchpotato/core/media/_base/search/static/search.js @@ -13,6 +13,9 @@ Block.Search = new Class({ self.input = new Element('input', { 'placeholder': 'Search & add a new media', 'events': { + 'input': self.keyup.bind(self), + 'paste': self.keyup.bind(self), + 'change': self.keyup.bind(self), 'keyup': self.keyup.bind(self), 'focus': function(){ if(focus_timer) clearTimeout(focus_timer); From 699c562d3446985d6b39d016e0f1f366266ec7f3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 22:47:23 +0200 Subject: [PATCH 393/468] Return default resolution if nothing found --- couchpotato/core/plugins/scanner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 2b8d1bd1..b75f0bcc 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -850,7 +850,9 @@ class Scanner(Plugin): if key in filename.lower() and key != 'default': return self.resolutions[key] except: - return self.resolutions['default'] + pass + + return self.resolutions['default'] def getGroup(self, file): try: From d7f6fad3dddb12e05785143219edc90515263faf Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 10 Jun 2014 22:51:15 +0200 Subject: [PATCH 394/468] Unicode filenames before saving release. fix #3383 --- couchpotato/core/plugins/release/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index d84f3941..0d22c165 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -7,7 +7,7 @@ from CodernityDB.database import RecordDeleted from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent -from couchpotato.core.helpers.encoding import ss, toUnicode, sp +from couchpotato.core.helpers.encoding import toUnicode, sp from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -143,7 +143,7 @@ class Release(Plugin): }) # Empty out empty file groups - release['files'] = dict((k, v) for k, v in group['files'].items() if v) + release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v) db.update(release) fireEvent('media.restatus', media['_id']) From 7788669de179037e0a02873dcf159c96b4cfde0c Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 09:31:29 +0200 Subject: [PATCH 395/468] Fill in profiles & qualities when they are empty. fix #3396 --- couchpotato/core/plugins/profile/main.py | 11 +++++- couchpotato/core/plugins/quality/main.py | 43 ++++++++++++++---------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 8dc59336..10987198 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -38,9 +38,18 @@ class ProfilePlugin(Plugin): def forceDefaults(self): + db = get_db() + + # Fill qualities and profiles if they are empty somehow.. + if db.count(db.all, 'profile') == 0: + + if db.count(db.all, 'quality') == 0: + fireEvent('quality.fill', single = True) + + self.fill() + # Get all active movies without profile try: - db = get_db() medias = fireEvent('media.with_status', 'active', single = True) profile_ids = [x.get('_id') for x in self.all()] diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 7bb33791..63049243 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -1,5 +1,6 @@ import traceback import re +from CodernityDB.database import RecordNotFound from couchpotato import get_db from couchpotato.api import addApiView @@ -51,6 +52,7 @@ class QualityPlugin(Plugin): addEvent('quality.order', self.getOrder) addEvent('quality.ishigher', self.isHigher) addEvent('quality.isfinish', self.isFinish) + addEvent('quality.fill', self.fill) addApiView('quality.size.save', self.saveSize) addApiView('quality.list', self.allView, docs = { @@ -152,24 +154,31 @@ class QualityPlugin(Plugin): order = 0 for q in self.qualities: - db.insert({ - '_t': 'quality', - 'order': order, - 'identifier': q.get('identifier'), - 'size_min': tryInt(q.get('size')[0]), - 'size_max': tryInt(q.get('size')[1]), - }) + existing = None + try: + existing = db.get('quality', q.get('identifier')) + except RecordNotFound: + pass - log.info('Creating profile: %s', q.get('label')) - db.insert({ - '_t': 'profile', - 'order': order + 20, # Make sure it goes behind other profiles - 'core': True, - 'qualities': [q.get('identifier')], - 'label': toUnicode(q.get('label')), - 'finish': [True], - 'wait_for': [0], - }) + if not existing: + db.insert({ + '_t': 'quality', + 'order': order, + 'identifier': q.get('identifier'), + 'size_min': tryInt(q.get('size')[0]), + 'size_max': tryInt(q.get('size')[1]), + }) + + log.info('Creating profile: %s', q.get('label')) + db.insert({ + '_t': 'profile', + 'order': order + 20, # Make sure it goes behind other profiles + 'core': True, + 'qualities': [q.get('identifier')], + 'label': toUnicode(q.get('label')), + 'finish': [True], + 'wait_for': [0], + }) order += 1 From 7950c4bdb403f3daa9c732cffa91e55570584f10 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 09:34:06 +0200 Subject: [PATCH 396/468] Update fedora service init --- init/couchpotato.fedora.service | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/init/couchpotato.fedora.service b/init/couchpotato.fedora.service index 7df166bc..d3b52ba5 100644 --- a/init/couchpotato.fedora.service +++ b/init/couchpotato.fedora.service @@ -1,12 +1,13 @@ [Unit] Description=CouchPotato application instance +After=network.target [Service] -ExecStart=/usr/lib/CouchPotatoServer/CouchPotato.py --daemon +ExecStart=/var/lib/CouchPotatoServer/CouchPotato.py --daemon GuessMainPID=no Type=forking User=couchpotato Group=couchpotato [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target From 2898a066feaa3bea8c50aeb8b9fed13ddf88b7e9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 09:49:30 +0200 Subject: [PATCH 397/468] Prevent threading from GC before proper close. fix #3420 --- libs/axl/axel.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/axl/axel.py b/libs/axl/axel.py index d0f069ab..64d29779 100644 --- a/libs/axl/axel.py +++ b/libs/axl/axel.py @@ -1,6 +1,7 @@ # axel.py # # Copyright (C) 2010 Adrian Cristea adrian dot cristea at gmail dotcom +# Edits by Ruud Burger # # Based on an idea by Peter Thatcher, found on # http://www.valuedlessons.com/2008/04/events-in-python.html @@ -11,12 +12,14 @@ # Source: http://pypi.python.org/pypi/axel # Docs: http://packages.python.org/axel -from couchpotato.core.helpers.variable import natsortKey -import Queue +from Queue import Empty, Queue import hashlib import sys import threading +from couchpotato.core.helpers.variable import natsortKey + + class Event(object): """ Event object inspired by C# events. Handlers can be registered and @@ -140,7 +143,7 @@ class Event(object): def fire(self, *args, **kwargs): """ Stores all registered handlers in a queue for processing """ - self.queue = Queue.Queue() + self.queue = Queue() result = {} if self.handlers: @@ -239,9 +242,9 @@ class Event(object): order_lock.release() if self.queue.empty(): - raise Queue.Empty + raise Empty - except Queue.Empty: + except Empty: break def _extract(self, queue_item): From ef407bcb3c5ade146fbc8e797bab74c8755866af Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 09:53:52 +0200 Subject: [PATCH 398/468] Don't clear pyc when develop --- couchpotato/core/_base/updater/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 8164dc59..5a47fc06 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -142,7 +142,9 @@ class Updater(Plugin): } def doShutdown(self): - self.updater.deletePyc(show_logs = False) + if not Env.get('dev'): + self.updater.deletePyc(show_logs = False) + return super(Updater, self).doShutdown() From ba9c975335c9b470d74d6ba87de63b9f885bf5f6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 10:11:36 +0200 Subject: [PATCH 399/468] Allow empty quality --- couchpotato/core/plugins/quality/static/quality.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 601438d3..29324c22 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -29,9 +29,14 @@ var QualityBase = new Class({ }, getQuality: function(identifier){ - return this.qualities.filter(function(q){ - return q.identifier == identifier; - }).pick(); + try { + return this.qualities.filter(function(q){ + return q.identifier == identifier; + }).pick(); + } + catch(e){} + + return {} }, addSettings: function(){ @@ -104,7 +109,7 @@ var QualityBase = new Class({ var profile_list; self.settings.createGroup({ 'label': 'Profile Defaults', - 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' + 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' }).adopt( new Element('.ctrlHolder#profile_ordering').adopt( new Element('label[text=Order]'), From 79cb716cedd7041e0547c0d693ae5a37ecc4d021 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 10:34:52 +0200 Subject: [PATCH 400/468] Update Mootools --- couchpotato/static/scripts/couchpotato.js | 6 +- .../static/scripts/library/mootools.js | 391 +++++++++++------- .../static/scripts/library/mootools_more.js | 141 +++++-- 3 files changed, 353 insertions(+), 185 deletions(-) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index e08e2c3d..b71c1f0d 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -54,7 +54,7 @@ }, pushState: function(e){ - if((!e.meta && Browser.Platform.mac) || (!e.control && !Browser.Platform.mac)){ + if((!e.meta && Browser.platform.mac) || (!e.control && !Browser.platform.mac)){ (e).preventDefault(); var url = e.target.get('href'); if(History.getPath() != url) @@ -63,7 +63,7 @@ }, isMac: function(){ - return Browser.Platform.mac + return Browser.platform.mac }, createLayout: function(){ @@ -322,7 +322,7 @@ var url = 'http://www.dereferer.org/?' + el.get('href'); - if(el.get('target') == '_blank' || (e.meta && Browser.Platform.mac) || (e.control && !Browser.Platform.mac)) + if(el.get('target') == '_blank' || (e.meta && Browser.platform.mac) || (e.control && !Browser.platform.mac)) window.open(url); else window.location = url; diff --git a/couchpotato/static/scripts/library/mootools.js b/couchpotato/static/scripts/library/mootools.js index 9917ad32..838edee6 100644 --- a/couchpotato/static/scripts/library/mootools.js +++ b/couchpotato/static/scripts/library/mootools.js @@ -20,7 +20,7 @@ description: The heart of MooTools. license: MIT-style license. -copyright: Copyright (c) 2006-2012 [Valerio Proietti](http://mad4milk.net/). +copyright: Copyright (c) 2006-2014 [Valerio Proietti](http://mad4milk.net/). authors: The MooTools production team (http://mootools.net/developers/) @@ -36,8 +36,8 @@ provides: [Core, MooTools, Type, typeOf, instanceOf, Native] (function(){ this.MooTools = { - version: '1.4.5', - build: 'ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0' + version: '1.5.0', + build: '0f7b690afee9349b15909f33016a25d2e4d9f4e3' }; // typeOf, instanceOf @@ -50,7 +50,7 @@ var typeOf = this.typeOf = function(item){ if (item.nodeType == 1) return 'element'; if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? 'textnode' : 'whitespace'; } else if (typeof item.length == 'number'){ - if (item.callee) return 'arguments'; + if ('callee' in item) return 'arguments'; if ('item' in item) return 'collection'; } @@ -267,7 +267,7 @@ var force = function(name, object, methods){ if (!methodsEnumerable) for (var i = 0, l = methods.length; i < l; i++){ fn.call(prototype, prototype[methods[i]], methods[i]); } - for (var key in prototype) fn.call(prototype, prototype[key], key) + for (var key in prototype) fn.call(prototype, prototype[key], key); }; } @@ -275,7 +275,7 @@ var force = function(name, object, methods){ }; force('String', String, [ - 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'quote', 'replace', 'search', + 'charAt', 'charCodeAt', 'concat', 'contains', 'indexOf', 'lastIndexOf', 'match', 'quote', 'replace', 'search', 'slice', 'split', 'substr', 'substring', 'trim', 'toLowerCase', 'toUpperCase' ])('Array', Array, [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice', @@ -325,11 +325,13 @@ Object.each = Object.forEach; Array.implement({ + /**/ forEach: function(fn, bind){ for (var i = 0, l = this.length; i < l; i++){ if (i in this) fn.call(bind, this[i], i, this); } }, + /**/ each: function(fn, bind){ Array.forEach(this, fn, bind); @@ -421,7 +423,7 @@ description: Contains Array Prototypes like each, contains, and erase. license: MIT-style license. -requires: Type +requires: [Type] provides: Array @@ -564,7 +566,7 @@ Array.implement({ if (this.length != 3) return null; var rgb = this.map(function(value){ if (value.length == 1) value += value; - return value.toInt(16); + return parseInt(value, 16); }); return (array) ? rgb : 'rgb(' + rgb + ')'; }, @@ -594,7 +596,7 @@ description: Contains String Prototypes like camelCase, capitalize, test, and to license: MIT-style license. -requires: Type +requires: [Type, Array] provides: String @@ -603,14 +605,16 @@ provides: String String.implement({ + // + contains: function(string, index){ + return (index ? String(this).slice(index) : String(this)).indexOf(string) > -1; + }, + // + test: function(regex, params){ return ((typeOf(regex) == 'regexp') ? regex : new RegExp('' + regex, params)).test(this); }, - contains: function(string, separator){ - return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : String(this).indexOf(string) > -1; - }, - trim: function(){ return String(this).replace(/^\s+|\s+$/g, ''); }, @@ -669,6 +673,8 @@ String.implement({ }); + + /* --- @@ -1063,37 +1069,47 @@ provides: [Browser, Window, Document] var document = this.document; var window = document.window = this; -var ua = navigator.userAgent.toLowerCase(), - platform = navigator.platform.toLowerCase(), - UA = ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/) || [null, 'unknown', 0], - mode = UA[1] == 'ie' && document.documentMode; +var parse = function(ua, platform){ + ua = ua.toLowerCase(); + platform = (platform ? platform.toLowerCase() : ''); -var Browser = this.Browser = { + var UA = ua.match(/(opera|ie|firefox|chrome|trident|crios|version)[\s\/:]([\w\d\.]+)?.*?(safari|(?:rv[\s\/:]|version[\s\/:])([\w\d\.]+)|$)/) || [null, 'unknown', 0]; - extend: Function.prototype.extend, + if (UA[1] == 'trident'){ + UA[1] = 'ie'; + if (UA[4]) UA[2] = UA[4]; + } else if (UA[1] == 'crios') { + UA[1] = 'chrome'; + } - name: (UA[1] == 'version') ? UA[3] : UA[1], + var platform = ua.match(/ip(?:ad|od|hone)/) ? 'ios' : (ua.match(/(?:webos|android)/) || platform.match(/mac|win|linux/) || ['other'])[0]; + if (platform == 'win') platform = 'windows'; - version: mode || parseFloat((UA[1] == 'opera' && UA[4]) ? UA[4] : UA[2]), + return { + extend: Function.prototype.extend, + name: (UA[1] == 'version') ? UA[3] : UA[1], + version: parseFloat((UA[1] == 'opera' && UA[4]) ? UA[4] : UA[2]), + platform: platform + }; +}; - Platform: { - name: ua.match(/ip(?:ad|od|hone)/) ? 'ios' : (ua.match(/(?:webos|android)/) || platform.match(/mac|win|linux/) || ['other'])[0] - }, +var Browser = this.Browser = parse(navigator.userAgent, navigator.platform); +if (Browser.ie){ + Browser.version = document.documentMode; +} + +Browser.extend({ Features: { xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector), json: !!(window.JSON) }, + parseUA: parse +}); - Plugins: {} -}; - -Browser[Browser.name] = true; -Browser[Browser.name + parseInt(Browser.version, 10)] = true; -Browser.Platform[Browser.Platform.name] = true; // Request @@ -1126,18 +1142,7 @@ Browser.Request = (function(){ Browser.Features.xhr = !!(Browser.Request); -// Flash detection -var version = (Function.attempt(function(){ - return navigator.plugins['Shockwave Flash'].description; -}, function(){ - return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); -}) || '0 r0').match(/\d+/g); - -Browser.Plugins.Flash = { - version: Number(version[0] || '0.' + version[1]) || 0, - build: Number(version[2]) || 0 -}; // String scripts @@ -1756,7 +1761,7 @@ local.setDocument = function(document){ // native matchesSelector function - features.nativeMatchesSelector = root.matchesSelector || /*root.msMatchesSelector ||*/ root.mozMatchesSelector || root.webkitMatchesSelector; + features.nativeMatchesSelector = root.matches || /*root.msMatchesSelector ||*/ root.mozMatchesSelector || root.webkitMatchesSelector; if (features.nativeMatchesSelector) try { // if matchesSelector trows errors on incorrect sintaxes we can use it features.nativeMatchesSelector.call(root, ':slick'); @@ -2590,12 +2595,12 @@ license: MIT-style license. requires: [Window, Document, Array, String, Function, Object, Number, Slick.Parser, Slick.Finder] -provides: [Element, Elements, $, $$, Iframe, Selectors] +provides: [Element, Elements, $, $$, IFrame, Selectors] ... */ -var Element = function(tag, props){ +var Element = this.Element = function(tag, props){ var konstructor = Element.Constructors[tag]; if (konstructor) return konstructor(props); if (typeof tag != 'string') return document.id(tag).set(props); @@ -2779,7 +2784,7 @@ Array.mirror(Elements); /**/ var createElementAcceptsHTML; try { - createElementAcceptsHTML = (document.createElement('').name == 'x'); + createElementAcceptsHTML = (document.createElement('').name == 'x'); } catch (e){} var escapeQuotes = function(html){ @@ -3112,7 +3117,28 @@ var pollutesGetAttribute = (function(div){ return (div.getAttribute('random') == 'attribute'); })(document.createElement('div')); -/* */ +var hasCloneBug = (function(test){ + test.innerHTML = ''; + return test.cloneNode(true).firstChild.childNodes.length != 1; +})(document.createElement('div')); +/* */ + +var hasClassList = !!document.createElement('div').classList; + +var classes = function(className){ + var classNames = (className || '').clean().split(" "), uniques = {}; + return classNames.filter(function(className){ + if (className !== "" && !uniques[className]) return uniques[className] = className; + }); +}; + +var addToClassList = function(name){ + this.classList.add(name); +}; + +var removeFromClassList = function(name){ + this.classList.remove(name); +}; Element.implement({ @@ -3122,7 +3148,8 @@ Element.implement({ setter(this, value); } else { /* */ - if (pollutesGetAttribute) var attributeWhiteList = this.retrieve('$attributeWhiteList', {}); + var attributeWhiteList; + if (pollutesGetAttribute) attributeWhiteList = this.retrieve('$attributeWhiteList', {}); /* */ if (value == null){ @@ -3194,17 +3221,27 @@ Element.implement({ return this; }, - hasClass: function(className){ + hasClass: hasClassList ? function(className){ + return this.classList.contains(className); + } : function(className){ return this.className.clean().contains(className, ' '); }, - addClass: function(className){ - if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean(); + addClass: hasClassList ? function(className){ + classes(className).forEach(addToClassList, this); + return this; + } : function(className){ + this.className = classes(className + ' ' + this.className).join(' '); return this; }, - removeClass: function(className){ - this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1'); + removeClass: hasClassList ? function(className){ + classes(className).forEach(removeFromClassList, this); + return this; + } : function(className){ + var classNames = classes(this.className); + classes(className).forEach(classNames.erase, classNames); + this.className = classNames.join(' '); return this; }, @@ -3279,6 +3316,37 @@ Element.implement({ }); + +// appendHTML + +var appendInserters = { + before: 'beforeBegin', + after: 'afterEnd', + bottom: 'beforeEnd', + top: 'afterBegin', + inside: 'beforeEnd' +}; + +Element.implement('appendHTML', ('insertAdjacentHTML' in document.createElement('div')) ? function(html, where){ + this.insertAdjacentHTML(appendInserters[where || 'bottom'], html); + return this; +} : function(html, where){ + var temp = new Element('div', {html: html}), + children = temp.childNodes, + fragment = temp.firstChild; + + if (!fragment) return this; + if (children.length > 1){ + fragment = document.createDocumentFragment(); + for (var i = 0, l = children.length; i < l; i++){ + fragment.appendChild(children[i]); + } + } + + inserters[where || 'bottom'](fragment, this); + return this; +}); + var collected = {}, storage = {}; var get = function(uid){ @@ -3344,7 +3412,7 @@ Element.implement({ } /**/ - if (Browser.ie){ + if (hasCloneBug){ var co = clone.getElementsByTagName('object'), to = this.getElementsByTagName('object'); for (i = co.length; i--;) co[i].outerHTML = to[i].outerHTML; } @@ -3357,13 +3425,7 @@ Element.implement({ [Element, Window, Document].invoke('implement', { addListener: function(type, fn){ - if (type == 'unload'){ - var old = fn, self = this; - fn = function(){ - self.removeListener('unload', fn); - old(); - }; - } else { + if (window.attachEvent && !window.addEventListener){ collected[Slick.uidOf(this)] = this; } if (this.addEventListener) this.addEventListener(type, fn, !!arguments[2]); @@ -3398,10 +3460,14 @@ Element.implement({ }); /**/ -if (window.attachEvent && !window.addEventListener) window.addListener('unload', function(){ - Object.each(collected, clean); - if (window.CollectGarbage) CollectGarbage(); -}); +if (window.attachEvent && !window.addEventListener){ + var gc = function(){ + Object.each(collected, clean); + if (window.CollectGarbage) CollectGarbage(); + window.removeListener('unload', gc); + } + window.addListener('unload', gc); +} /**/ Element.Properties = {}; @@ -3446,11 +3512,13 @@ Element.Properties.html = { }; +var supportsHTML5Elements = true, supportsTableInnerHTML = true, supportsTRInnerHTML = true; + /**/ // technique by jdbarlett - http://jdbartlett.com/innershiv/ var div = document.createElement('div'); div.innerHTML = ''; -var supportsHTML5Elements = (div.childNodes.length == 1); +supportsHTML5Elements = (div.childNodes.length == 1); if (!supportsHTML5Elements){ var tags = 'abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video'.split(' '), fragment = document.createDocumentFragment(), l = tags.length; @@ -3460,7 +3528,7 @@ div = null; /**/ /**/ -var supportsTableInnerHTML = Function.attempt(function(){ +supportsTableInnerHTML = Function.attempt(function(){ var table = document.createElement('table'); table.innerHTML = '

VJ0RN+~5u zJ0|;)0<{6f`Bo>kf+k+g$p2pT!(-+&w-9re5bF&8 z9Q$Z494#NwaiUZkWe(XhWVexb!^r-wj?}ZvbIZ>Jp{W|>8EM2BP30h_vn+XgELcvC zl$snq2nVaKJT=UDKLlby02jkpA1ubA4k7Uu(Hx6E^3X?|{Nf?nzaQ-dDWtI`tFF*s zCODCKa1}3(QZn4|4nqwkOA3+gntZjKEd=OUFfV9)`wDXmcTh|zu^Mf&R5xVjv8^7~ZjR-J@w8TBbpEap^ zb9t%~_=W4~ND0Q=1Z-zT{sHP%HX&6ofVYl%&Aknclq z$!2=KUJa4Q!mj4QuX<%0!J>Vlql3`p(gm)3yHRI96xCHy9x6}=LBmi2@+_03oCq)f z4ff@+STqptrOHYSwX$SMTQH>he(~GGK_yaZ(9Z0!zk#ECkbE95j_~A+#la>%$QyM6 z#jlK&oUD|(#c|l?1$X>%V%&1-=@Pz^S8`9hfHObtg;-U9qx{%L!i9IW_q(XDxk5F# zA59x96-$#rhL=*H*?t^Y0KAw2O(onqZH6WRYSarvy)s0#z;Nxv+DDtpr^GnN%2XI8 zjOvThpM0_XkR12joC=jnPw_Ylx!m!w_%_({I3I3fKce?!z212or%$4PV&o$z2agAb z^Czd}QHV}U!y4SwaR_2TtUsTt>J140Bv~lITv|ESXrCu$=*Q7-dn0sEJfti#7YMhU zK)@{lAWP2kOOD&Jmg6FzoR^VCUkPAMfeQTqW+JJ$hyWu$n22KYt}^Vbt=4-U_WQi3 z&bauYHFdCi+JDOa?70;tAdGH$YimvMO}N8zCWa+*XF`*vBWxo{+xGX%Pf46(HT~-2iU-_cM)^&%-L>CG;e`Zfp&vQo5UAir9nlQE z&iF#$K0^W{?5P{*fg4Tob=m|J(2Xd1h7hWA6eF1{9>-;vVij zPn{o1jVXxx_GKV6L)iwaTpm1h>NYGoS>X+9L;951OqaNIaYW!N4C;ozhMIfYr~MG; zxMfnkBQfZdM>CHFJGhZV|kaVBM%NOXG_;qFD$0>z*i z6c85$lt6@A9s;R}&G#4;YyGI1W`Qz(a8{G?IzNa2VVvb}3c-8ab8F$lRgS#3&WR}Gz7>E(BuO1;4~ zaZPrQt`!ye3Em=+`-`GReVzRD!RD~WJz8~FWWSR`nRDV&9c zIn^57>9%N#?C@wx;z}c%CazwWRf9=H5qc!X^B;=0LB+GOS~>z1E8igd zC-o86h+xD#39x zB6h{8V81p_+_$HUN1n=iDc!Y#e^5)uoc1Jox)oFquUMjQOngKmL^Magk_&Qh}yz_(B^ zvI|Eg5p1Zl#h9x7goHDh==!~oX%o7bV7?dDlD%z=0{k8!o_oS-w>!snk0>`{iDH;a<07Tv_idO==`rZ3;e9 zchj+2FL{1$WdAE-|L$6n_xuP+uiy7lO-)!TM*|-qL9Daj4C341D3}@n%xbxp=x=N? z-e2Hfd%V-B0^QQc{HE0ZMkP6pttN@TI5W0;A93+z_0H2uLzJu*rB06&_dVf@r7kfw zfxw=EZlLBq68zenx*ZCFgm-i8a1tl3IP}TnxJ}q`e)lN&?>Qos5F%(v-GrjjC4wI! z;U1RXZ!)|k&AmTGzQuU$UGE=A8T@|YNPL==lUua9ClP1MxF7kh)NjL(Cd{j2?!!P7 zcZy)|ym>*Z=|cC5oYksji_`Cqx+Y`zIppc8X#REFtJ|4W!;sQf2%vX|UtXMIX;dxV zXh@DOii1#=w9f#jidK$F3Pf*~+T;~P@S>Tffk-V!1V>?EH`0E#$+~QRrFmJ#am$Y@ zb|UYcVOnz8&YOQzJ?F=}5~cP%pY+Yo(rW6RSQ_fvV{yi)KXOMCN4;z(#73d6~&q3uZc*K z${^Lck}d}if}oxItA%>mqAwG-cQ5pbDhKAQ#*j}-FV!BL>X#{hn|*O}<2W0};%8Z^ zYT_xZpd@z_h27uS7&3s1&lIa_Q}dn+J{OahJR=H!nHvaSc_(m%Ij8c{!PgObJfJ86@(>zbwHbjF_6 z(Tarw9JU8lluSn2W>8>}ENg2f8DUeM0lh41XCslUBxhhOvz=D{r2)>^K|agzhC`(u z|1*dJMnF?=hGgT39!%hy1_)kgCBz{FwO{dN9o8m6bZ46ZLNJ6~)w^?|YbWy# zALA36^N_P*&BWfc1d*cYiN^B8!3h(QMu3U5cK443v4mGu2}`trpg^f25lgfl2P3LuCTFqdPSC{P6uAmHgtPG!$jCfnU&9IMERym zFfT02>OggeRp|YpKs;hQPaI#eSs>B#qvSQrCJA>gp$_~GIh3m|dbiX=tY{-#{h0$u zYG^$@)^YaYppw%1Rhwek3IJ+uY6F3`7O($&v&mY`zcOu8|KA6l+K+qGkmI>-$+mmx z^p(4_igVV$Y&}=8+p!nfw&uXQ%r~w|-pz4S{W`oPC~Kg4H#_+5-3+5>%}TtK@G3Am zt^@~CVYc-bHp$q>pH!SvDr!Mmg9O zoRs;Q<(0NyPcI|uIDua>rhGXYd^{-7(yO>IrSM}>_r_Ir?`#`$M^5-P_KmYli?iom z?0gsBJaWu$IJ_On7OUzz{=l9-n4Y5vi-uc`Hs_a#=Z?3oewzNGN3%O!Js#-$0C(XPL^U>#*>cW4ke1nNgGw;HxUM|c0;u$Q{q;$b%M=XSwhd-+t_735Hdfu(hdK#qCS-Wwp zPW7^4)Ld0{Y_$Ip09%Z3lRh#ZBO!_rwE)(TE2FvRwM2N_& zMAxiz?ipVD5N~OV%=*cmDgIcY){=cA^)!cOp-_WHJm7t{kivF+yns?kHcO1=IT>W7 zqFkE>1i^2pzOnI*ri+>di3-n=GQC<a@A!I!nSPB=_2J!aZCVqs$r&5h(+7^>!0DLt+B&{tAUyPs z@6L={n%OXkrN|%R1t8_$2%BbZWK5s9@w4kc&YA2UeZOcE=6G?c2GP6&s3}Bh`Qmm? zywLR;Im}LuY5H`P?=#`=*TxFj0aWwDD@POk@9y4IY}|XEokj7;^e)jD+-~W*y(0M6 zQp?hD=0+uyNu1G;`C1`<*eja}z_H?I{fqmS6B656kN{eXQNSuDY|m*D4+Gd&>}gy4V# zKiAFn8?IFY>+9Tlx8xZ#$Z2~-g>bvgTz{s^+)sqxum!>GyMkK771z~GE@of9i6bnp zn7YhmRW=2`X0A-EbDA+7Wcp&yJWq#;Z6H=$63yXlg6Dm?^IQqsLr- z7b1l{t4j~Q^XreBeGdJKe|_#VwQ^_aXeVS^!ce>52cE5pa7*%`NI;3rEQQU7fUlf- zlpdcHt&Cb74VMc(=_OuSUQMOF5_@&eJU^-57B z>EFq9nHAp~^ZRtn9s=JN#CrZ!%aQ~BtNr-Tat7tPR3u=2+M)d{ZNDT(q)jh#$KsiP zZb&ry06P;{s|6!p+SS6mQB4z?`7@^X)TWDX>lIWfA(Kdk+lt>>)rR#c{kN;71{c1! zrS)sP+yqp5ue+l3ycqI>ZoEA2tWJ|2hYnYPZ8qbi=lRDIdJwjq7iBQv=HKLWU3AtR z)CY21@)rdvCvItXXx6~{Ox%A_uYLvSIMJdENcBp0+Q$7D`p?x&+*!{t6(yF6 zZI3&|!^pVvQprW~coa{(mja&y+qHdFv=8IeX`v1;q=~=Ovl`Zib^R#rc7bx;bJ|$; z7B<@HQbq~A)ZQyBLpcn>T_$R%4KJxmDc%^Vy1#okGrTh2#4F`21xr;PhEVOeFDbu- zIBoZ#I+l?5kdZVb&Or~Xj{@JUA5rqt*KO8!>btDpj5-M89EEX8T_ZPnMsN4&yZqJH zpB+g$)7Pm6>(AoU&hXmRbaZnD6nltf3k>Qy8VBZ|f7tR0L$K6`9Hz z8Ji#+n>b9BP&95;jd9Jxq@7@ri!jcInDFZH#6?VkX-rlhG>L~QH3DaKhX1byXHktS ztcOs@Mn&xqlRRAE{zOrQY|;L(*$OUc7h>XvFI<%05ZBHwL*7w9Jy?L>1m%bJH#!O zndJFH`ln5DRb!3tCgv+Rw~bh(L$E7m`XUKGg~`KVs;APLvrG1I#w!rgNlNN(oEd;> zn(lRJJ+6NZmn%OxT!brc#}@=bXaB}59MhXEiW*EXuh_^91SI77B^1rI&8-=yhxAys zD_K`4zv*gf+fK1-fiy=?Wi*?Z+e3ylru$ED6Uip!+YpNt+(G-LLT{=k5b`qFeE0-1 zsxN8)(<;zW!ORIz3p_4OZjLntVofx) zZZ?@zvf{|LGRNb%g66nltuA=h&COS=tSMAFvvWz!a|oBYw3X?__c%K@vyaeeBe|kK z5uAa6I3s%;xyanG8fO=hpX6l{eg=7+2;LAMf+}0mDJ}>^h|jao=3*0y+(ANJc< z*g77bt!p2{wmEJ?wV)1OD6}M~QM$AoXkkBD6WCtQ35ZZ}iNvaTSy>=s4gD4mhUeH) zrjoWH>?t@?7h7vgj2?bE$c&rI9r)bianckgxbyZ-YpLW&7V;5+c6tX|hR z-W0@0p|Y_AXf@B_hhvpyW4Lz4<90AQA-HER@mBU$iFa3$0NLi_vIztbwO4}@0H_6B zOxZP00{{R}KzJ9pyw7NO|E+#rtez!6^9&5e;2f&)-<)4BFV^4M!OJtIO@+aPkTsB6 z=t<9Tokm-?V-2JO57q%8#b4cLA$|LC<#`CG@+XweN9Y3@#2;e(s`g5*Ty*POiScu^ zQV{g|Pmz(8*LSbNDg_o4)h@AV7P?+1MyI6Pb~T%dNff^UJFJGbCdM&I+y3{BmkxvJ zE;AR88??@bR(y8Uu8fo+g@%)xdvL8HMtSJEKX`oh1pe+^8f6?W4492H;{VTz(f;yV zi&X{yZ75VRcUPWuM6tQeB}u!anu6zwmlLb!LQ-xfX5;bxP^0FFoB+I{I97iJC)^y* z7lKZ-w@ZcEu$_UfH$%$XCl-Ah%qUc@hcRYnF;r(@mjgSC_x7*%E7B~%5e}f2hrmSR zsxEu_@bI?pUAHEX9EJ8x3j3{{g1H?CMr~tSk?O>Q)wOQTB-0@%Y|HVKK%`M;kWYN^ zjJD$tndCM?+Z&m4M|Vlw9YhaG_#9Oqx$l+`wxGn-NI!$O4S%la&+zsS1ECsmA$HYl zx^C$uSc-3zpIZ->)^dYy$#S<4`dj_xn{mPYt@rPknQn1naDu5RQQDh?qD(())DFL0 z7S0G@dxMxQBD~4K)G9%z;80#_LjmI>ai)G-rU)t}d!S+LvK4kKcQ}^KJ_#Ej+sa$Kss3aOg8^GymW}0#@Dpmz2j=FK|FLz}e^EVe8$VlYP)jc* zh#;x1gfuMOT}y~`2}%eeik@A%mu^@}8UaB;K^6ogM7lvq1r!Anj1Ldr-=Fyl&b;Q# zyk_p}y5E6+shfau{i zBu*6pC`6r@u1!km!}#!ko<1~-AIx5bi^fOkOn`+ZaBMRY9v@|Pn!ZXQvjmb%1D*M& z700AUbPnKb3J}7C@hugElo(kamLwVuJ#X&XDa0&zKvIN2r8~%?l%?E>cOQ59QgYA< z(uLmy$wy-uIzqWfMc*SB&?7uyd>;z)+v&9yw>`~$KikgO|^vz zuKBdz&-swY{5%`qa!OmvBd3&*jiJlMEh&fb+67$!z#xSusGo zCQ8X1lmOsj9Rr4gQS5@CKE>UVvZ+@?=Agj@j<(nW!OJw5^2KcnbNJeg({&C|cr#>t zw6CNOa}L~pbC74g1KV9OwX7kdj>sJPQ4pUfHVWuw8m%K1g$N{>HACoVFayGu-a8PL zAt*MFtd0fv#JqAz7EmHgZw+Begur(oVn;Cbz(X7saMp*@#lue2n?dco53&nzNdllp zv4>B<{scR#6Qdn=v!vm~Tt zD0qq`sj4KiQ>ZwT>6_x&!k_Vbol4ig66(cz6FNuoCC!tS9yxx$KAlJ1vq&c5#(ueXzPj@RRNHx`5QE%UN-g7Qg^erg!^YcXt?1#+uRz}IY} zIea{zlq1F1`9JsJ8&^JaP&PS+;Ff~$_}uXLtC09@2sC~sJ~CY4{*e-8)7a;jLkv`m z-8_i(H1Rn~){c^o-QLUzM+5@$H;#;JV1?sfuAK|dom?P=!A zya9Cz8SsJf;7)0v@GBGG;ePbDQ22>jJPgPzH_3~!-;P$$Co|yz2xW z4KeNU6Qd;VzZ=_scRwEwweluQPVCGxpN3tYs9z?8YKFFvpQgNL1BvC@Xx8(_)FT%+ zIGSC|ii#tVw;4F3Pji-rkG9a6B5GLa!GNv-#|^LCt-=pH_YLl7%P_ znFOF|sKy#L@f5N}gI<|^nNq~VmC%}RwN^SLn9dbjY+|KD=6TJ0YN3FfVJJT@rsLbB z$K87mg-h`6OxraX6*?sfB5oj)tTj0nDyhAJW){S@4x>el-4HyPdFw!QD#cTar|5Ms znf)F(^h&rys+@Mhb6={@0O<=%B?!QY=|cjLzb(U<2w1!ND#O&)p&Qa~MfYPOuVIv>4!6zqDHeEVVsx$Z-vrhQ)N!*&ouRqkN36E&cdy4p z3Tc2(*UTrYUjJ8wta#N}XD2Aua|^9M?H+ z96Ps&7!M~O4-X$NpD?cw8}Iome1hlsq%HZ+i1Q0c2%VBY&tE=|<2lbOd|pE4yrhz- zh^Uw_pP00Qgp{1*>A0vQDQ_SpEp|a#<$|2a1#O#)vYMCWG^MfZ(z2>DQp~cfB64El za!NXKs)q8sBJz@Q@^ZQgGLj1BzKZgvs*A6h zJy~7fM%}4-w;QXYA|a z?Hd;7Cv?$2IMP2P$^TZEe<<0XloH@`IUvA4;6`vju-^?P-kYABfj7;9c<@09&A~T9 zLay6~`1^!}2Hf&fxb?pg5qj=QXhhs?F45bezISfjx)Xl;Zp7WYFduO zon6m+2cP$ibq!3t9GUGO92t1MG&nN>{_tyc`|G>)t*x!y?Y$rS zhrhlqzWa4_^ylZ^fB*iSK3V?XfjR*W(R}*N)ZQo*n}pL)bJ@!{>^Z|C{g(29B%F-j z%212DUf>z6L_UMo%Hd2gt2(FQ)~eB5y!%Lz!Q+Qxg$f~iE5nbg$4d!O7=FXHn#l_N zOo^)_ZMD-K1#%=NdgJ0E`-r)njg zF?rUuJ6&T{?>zRb{o{PI`)G;D^C$bu9T&!vyf2+mz5Dzw{V&x^PY*Z4E5xt8>AEAk zHBr&i?v+v`yEijz|GeaC!S^p~FOyCL%wBXIf7_X^&%OVs`_Hd~FO8*UJ>GvJz8t>F zz1Gw7py1cSW(jz-^6Tvb)cKC7kgtq5M(NKSHEYrAsn!f?hr}o*wVw-7qY_6Judc-i z=lHOw9M!-^V)*+`W#!&2%d`a9xg&FMV8}urKeuo$MfNBAH1bTnwfkneK94N3jJLKX zz2c7}X(LK_^X0_b2424Icc*|1qqv_(WTQBN604#dpPcw>iX}&Vt`zsBOe4u}$$9UK zD>yJG2|8e!C*;UKt%sOg*y+NCo-OeU{%$M$69}#Pbh6 zKIj*`NmqLR7r&IxAid{`>yVW9lXs*07T<$LWv|nH8VE8`$iMU+?xN*pA+XT#7u} zzJq8weh=n8{-qUh*YxSmwtgy;qMCb?i%5>P_3+1LyI_M)t=Fs*#2Or-Bm0l)nKH$S z?|ns|E48-#E0W_BZ1{E1@f7eeq5f|8`=p-wzaLXpY2DuzydOo1(xTp}{(5)&MD=)F znfc%E$Bvq)@9Ea8=s({aS=GM}ISQ$N?A&Bl6&wE({iCD02f{jW7r9dMbEx{oVg8r9 z*&T01o2u3RSv0e;L|^+)C@8@w@PPSiQoq35(-+7vGIr@)zaXwRUZbJ;0nWJepGw|y z^}vJsXPmAHYg|K|v0qCgq&y)! ziH3etYs|H&6$5ay&mWOjc{p{MtE9UHz7u676$lAY%5e zZ92uqzfNctWpP(6DEY~Xua*6=E~My38o4cs{|$9slIV&=UVg7-x@64C$A%cwmcm$% zWYJZom$CSWo&7@`!0%brYLAJ_1=S6yU-_1!!m8N+lWtwvUZeIZ|&%sJPp3iS zLHXwk`u8_NDjc@(8h>bZTh0*FYn$`Z1{BMC4YXzv{HI&lUqL7#2tWeTP!tH{-ry>M zf^HG(!yezSC=Qu92WO@K^{^qQmzzg#K$`D)3Nhaqx+5j65~t!NRCqk>@HbfgF@p}v zwL2%H$aoq%hyQ4z1hGH)-Z#mo$)1jb`RSoycpVMyw}_dH@%a1Sax`kU869s^c-!l* z!iwIS8fKKo-|*HsUip9!03Zk;6vFp!$0}_4+0g1o4qL{BB=)(^qyfED-v)O($(*^C zw}%F{SP+3bo{VMqAz19>DK5XbVDs~0c4o1&xVPfCxd$PGjRuG@Exx{{CUfuUKTm1o z(e#4aw=(ALZ9lf(Bb%n6EO(Tce9wfvUL#Z92@GYiLkd0FMNLt|7OR zSZ~;%lLrfz{d44j9L4>NhrHwQG?J)@W5JRj?2GdUPK43kg>S`C7DIYpZe4i#aohZc zUBPl_#OvWt*B=Tfo)Q9E%X*9b1L?2s?!CYDLfmgh?T^E*CsG+QRq|6UgeS`vxvASp z@8>cf`Fxo_d`3B7Qti*e7i8%l+K1HJA2;}C?yiO#Ts0wMCkh7nAE-2H{FlKDTD(7} z-nt$a@IC+K^tb){Ti4IWTC{}|L`h&fBycLc~(*IHVhA_>IHiok(6^I6yjp9R!3Y{@KX2? zawE@F=aq-oZJ+CU9?E)m!^QN>bK_QXymYy47<9y*l9WE#-doCzPwa>%&IJ^n+zx&w zA4v1EBUJkY#)g#Ag=y;thU$fj=IZi>BE6e~w(VHX9zfU$&@%^+t2^uh;BC1NAub(P zTNOfaxRA$i4Y^^Zz9ns2&&2z8|Mwg%>%DGBE;-G0y{!2;zAgkgKM5X zsPYc^u9s9J>amPS7c@u~438D)xP7ID+|HF^=1t~RAkq7f0aPY8E>rF#A@g!)rgSG+ zbusfIDvS9bQ@xfPJD8+qkY%7qVq449&(E6v^&sMQ_=>2PjeICEA=_a09_w)G9xC>( zTq4XO5tbgqbQ$TRh)ks9gwQw|qnN39jGG0zf`TchD(A2~7}8H}9ZDr2(>3*yTev)4 z=G}1<3xN{vy84h+^)iQdNJ6?9&-Bw(a+AM_W`2#&(oM)$xsz#loN-D4I9HQZWS_b7 zBwGRp(aJ9<9F8&@PSOz1Y!nAoI}42o4-`8=x#Ns~dWBV7kxU7| za6-{ZoKtyCkcT2m2n9_h6qC)1aT@5Q+F~-bScL`UB#3=}a*orEhRUJQ1qUK>NKj!e z6MDhem+`lmc0;&3gQfFBxb2g{PGEATbfAMIyjub$l&Xj(E4~W(QkxDPDLoNS9vUh= zGANj2FHl!3z#C+7I+PU;=Zg>vJ8Gj^eX}|gqa+ow+R#}~xy#v($^;||W%G-UQAH!- zr7*4vc}4OC!vg92BKqc{)6$;Iv#WGvC7fj_-H6IW3fiC!g-l1A{;teAsI;uB)C@wz z1tPpBkamISav~-<5K)f8l-E?16VXvrctj}r!2wFdwaU&MUC)W)y^Qe}#D-BZBpO|I zAl+@6W`rj)mxj6PhrCWhh2vwaj*!9JxQY`hMw9|D;*HV^Le!8{;rXOv?%KcNnH>1C z&%>om3l-`Pr?V~9S4_@RN`@$9APdqP4TFc#>2!f*Nc~LUsJ0wUyY%hH>8>Mj(NE^h-ee&iLN ze5RUOwgrHWQu=|Xz^t?MFS>ClG2)Fv##%6vk5p+|P-&WowUVeR!&X^Ju-j5Fw`p{w zKtwh0u!@2K{g8GeD7ykDWIg&i0pWy2`zx{DjG(ilR^6*(@dqCrO&}X+C~p_^?LbuB z0a`l|SsRG{7mkd`u0C^;gd|U*>zbj(1?k`Kl&kudHtp38;Y@1mAQ^sb?H?YiI^@+j z)b`^Z>DZPYI+XrSC~X~StSM-Exs(NKE_y7{Mn6$Ypl>|PPwz9#C>)`7?Uc0owEbHQ z3FM7EHfa23P@kIr1X-8&7*+Jquk6w>*q7IM#2qsDERSuc;;B-GI(@@_e)3Jn23DSA zdBu)HbQ!}EAiQ3=Y=B0wV5q#9d}?u-K2MhX$}uW3kRLnj_7O!A8yyO)Ewl1hREA4X!{XN3AH3F5Lpbip1L&SK;~os^;8QH zHCcn5xaKbF6{cnDp%5r8{@)cRz->{wX~y==`o&mSW%b0aNy?uy!x8Y_}l& zqf!rJbKzfcn%eaS#J#646YI1J(*x*x>_({x_H|o+9Xxvt0}23C<%vdp6VooG<9BK9 zvvQ{R(t*>*>PS+RQauU}8BuKb&7Hivm!bKg_uyIiZUJfSdWVbfBNj~h8I5AYg38&1 zV&rAy>~JSkx#}%9d+-Fh3Xi#4r{YaR*a4_hKGtnKsyq;N8;_|UMED0*nM@$l!NL>VLaK`9?1XpAG#pq9r9inICEYQCoZZYWj}u$@PC=E1B#jMpG@M`HZ$leXRdy z(2i=TJFIJHOn&mXt1;1d7|RV&T?W{&wC7#JG^f&Tr?!rJz;o``G{5$hr9S4okvGXS z6UV!c>w1kBvkHn{KW2D+$O94T)q#MJEIiWYmKejSaMnU+cT49~LE}{Rv$57;2B1ku z_8LJ9G#yh5V5-6KT0EvY5Q9m_v`{cLAnTD7wvK|~nM5ZgVyF}2p^BJtm+`;%#xbf` zIOAl}Z>~`82R{;$J(L^%@xHEm)>J5!%;Yj9+&v}LJ>2tpip~XKqBixN7*A~|O`|4i zm5S3ZtxlJhiNEm+WeibxOXx-&43U+H|iW&}j5_eW}A;@B%}H zaQ@W!#-f$FsWU~L>Wj4>>e_4lXP&CC0w6Pk3o>M4j_&fQA~+v9_U1a?&xABy4qzfL zVE#)U=e>%pF<*$f!20tZnrgnl*2wZts^q>Z=DzAA7?7KFA(skZ64Nn?_$R7&^YWj! zNvcl&S2)E;SduMXQtw_8UY+I^pRF4k>Q9R-Hb4Hr>Sdy7R)aWO2`mM7zz$DI5Cb zW|OJQjivm2uO%8w*(OGNi>dv@7mNPWKBF_|@{16JQZvlX0WIhigLBjZf&{bsE+S~8N?{ruuSt2pH_{rLX{;2)=AIX@Cz_rrA z$+NxV|L#rjrcY2VOae_4b(D#6!G#=tG~yyAcC4nx1$w9iuu=;jjg9tp5BHAE{8ztG zm$D?u-?K5cG`QNt6hc2TwVUL__$*g zz^+X(0$ZIWn?H8mpXdk>rZs?* zuw2?S23h$?OI}jF`)N#VnhAtdcmc%Ev(hOLW^l{ec^O98ukIeTEcv+hW^?S~-fO1U z*oiUPcOV*L3A2EPQQjkhP`~)!RhvVJy%>%+<0(t)bq83E`-faTSX|E`PJjh_ZM|V_ z4Q6qOPRDRPTEkA(h?Y(qIjvWjV-Sp-RVN_5G+|4*cz-|@a=H1N%DZpMW-!_2ZyW$r z+YH8vg`6|}E{KI_kAF8D|E3`Ts~9ZBn(t%r9d-|Xum=86YWkur0Fyn9jwZk!!=Vhu z(}L7*>W{wkH*GF96|w+7FN19;^B=Zp5XAt?<$_zudJQfZ&y2dvGVa6FU0l1an7aVx=TOv z9vo-%e0$clF&4Q5J_4SXeCcW0D=*nQ2QmH_fPi1Zvbi9-HL(!d`%jAgoE$c$tfHN}I&uB!UGMAs`y|vYdI@ccqp=a=M z3cUOgK(j{V@Quup>@p+xY5nZ4vZNdzZjI=`Z-4dB_7NKdf>@$RXf}pGMqlXz-7F%D z7Hxj%$MuC4&mKAexOPHPyAEw@VqEJ@MIS-G(@}j*AoIA3ji)_($00kDRc6)Bs}Ubh z{}liEH~)Tr++TkwN~C})$6w!7<#Gu)+#{n{SZk`Uz5DMI0x2x}4?W`-g*JDflCfJE zIytYHjInH1!Xf4thd0VUwGQ*NnM>3E^do3xnDZFmlTqgkrLe)}WoLONYF@U|ZPuI< z&FJ_lE>{OlauMYF7+)cMhNVm_Ixn^lQ&}ea`w}gI$9i|){9aT;LOJYXfKqZ zpcPcIbNBo)yX!STh(Z$)Wi|{B0T>&XsDWy-lVk^VjQiR^BBd7z@4c2$&LC8&tCldN6yGUfaOm6{YNdN4RS;}|BC5%+$ovSU?OyrONFXk= zQctv>&Yf>&2cUkEt{qEfLIcP~hS;C}Z90)0HBOf>Z(d9k`27i3xZlVtkhDctr_>#i zGJCUgnzKj^nHWQ@{l|oqc!#SSdc0W_a^s3O-8T>XW1hgFuGsj%327q^#>p#WNRf&Z zzEsV6&1uH&M&59r@=st;Osvwl?`Ej5D>m7+26mKF17Gw_+f_d%r9J_=`VbJ zhA|gen6#21!nfjS*HAL38BWJ1*fUk7;D*9f<>*1tB29s%cMM7i3QtX7f_Xsn+Hzfz zOqk+ygGOgV%!SVw#{9Q4%`s@-YwGE&jo@qza%=F)ONm~1%wuvRM>R9054lK?J5j)D zdIxw1alG^JlwUBuASE_D8~LUcWP4T@&}z@wvUEj`xIHG-xwO8*htm{P(-VtNvU$*~ z8KrO_9{exKiD6+tM$LdOxF~^S{g$J1v20yvB+7p8^}sXQt*yZBsH>ptOHq}72@z7) zN7h$-IH=}n5lna*#QJ-lN~Z`NwpP3eki}ein2q<$I>K(75m5M{@yr5B0yUeMbI=na>T`JE^snIzB^AXtl?= zmH(Qw%A^=Z6`(S8p%Y9Y;0)Z42EJzpP>&|H1t^*ARxX0^({N_B-(NE!r}bZ@fKgt| zH8#GK=<`mEilg3AttJ4Y&W&49rat~VS8hI$xw(?OY~ZDDa_>Hy7bnoW{`2K{Wx6o* zfX52P4zMboaY;3}PS0bak-ljmXfVi|aQ;p(Hp9M>Vpg~Kg%;if-ft6UE5q^_-Xc}X ziNFVC-Xe&<^>U?$-B}{&dGHfvwVflSzYX7tE!L=gFH61C1M6iN{Kd*MlokQQgK+$y zbhHu({qHMZ*Tvz-5^I8~C=~+()9U6%Sp)rR3=Bx(dr+!XN2uWM>X)3Y2Bpp_=l_=K z^k6TiH&ypVjolfWf#yO~lPj5Rrx#Wr1Ci`HFVEdi6My})$m)RZ1Yj7du8Ek4!ov08 zsz|Q{093 zTC%F8L=xca6RfvMThyLSF-?()2{FUU+aA%2om=u)b>Ftz0il~;Y`Ju+BH5u_fxa9) z+T58>eAOjGYY`U#d`arOje#GZ$zsOh1KkTjDveE_&+`lsEgq$qOco`rAu?Gt*3ON_ z_+8vR8o{Ab(j}$2(fYfSP7Vg{xm;zif>;ODscY(rp=#_iy0qMAjoA1ha5imSEBPXjG4i%)ra z@*~+@{62rK4!>RH%!^BQBq`7oWNf1F;x%z4R|5rZ>J8`59G_0K)l zFqo5|f(+kD92brt=equaQXcCV<&IC@UKJ2Yr*+Kjk~eU6Dj;MG72wGu@fl(flEOLL z$~~#FH?<^_imu3(L2Rwe_spEqatXO~yS6E*c02DAE(WZ&-TUIF!gowx_u@~Dy;;g( zJ~gX+^Pe1?-F#rA%M<*^cj-0dqSKtcOV#X?OFV}7wc?TVPVZT++4Ss=Cw(ju1rRF+ zEE0m@EAEnlT?rq)PAaa9)KsfORf{Ys=bkqx=Lx)oq%sEN-*cX3J>YYkiNIF3I(}wd zKzD5m-IN6*UOZU&0v7MR&87>1M&`Yg2|Cd_(A{^js6JZrn8zj>%PtNx?-*UPRp2dm zoZiOJw8Y=g;t4h6kWym5z)xDdq3kL0T2V`)X#!cI_X>c~U}`(}0OTjZ*4^rpTL-jw zIl$hWUy3@h|EZ`9hE1p_EB@V{J5rSvERnn@Zr2IyL@>xwAudTz4mU2E^s!voxR|BH zWwHS>94R64XmnO`jDbMg=oP(i7AzI0<88Ol;zJW_EAhS3;n*k5c%~$Ax1=XNA$6GO zE1&%mSp`XWi*zNIT30qXMNF^$vIgcTHzA&a)PTp~fg6&-|M_K9`#!2p;<)IK85?3~ zuY;O=)fmty#uC&0jg%MZD^9YW)jt>!`TA9*da7kYfw#3Wp5 z(@J`~FbX%sq}`i_@JWH^si~mI1RUbRPBb$BH=~`BH{lns2yRsL;)H_W8u@n!69Na^ zQ;1ev#+oR^NCE?v$pZ^i^4wAM(^^oP?J<-9z-yQQ7Fu9I3auk);G2q@V=bFIp(X0d zFR~E4H@bGS_Ym4wJtYjSC72p@ha5g*!M8WVRDU@T`juHmu?6tL!B@&fdCw&ISC|>& zo9Tx$SkbSx$_9PhH6}waXa1^mp3^L2;gJnjs#8{y`O8n{tDyPt(OF>WYay)o(+}2Q zB&bAG0+9@QC%Gd-J}-w3%Tb->DxNa>1CzqulcjRnaFb`i6H9_mCNSQi>>;euow?MwFls|4nqQo0we4DFq(t| z2%l&^9Gnmzvp7T+qs8eGqmV=r2SsNd4=4vp(V2q+N2e}&2*KK@T^kfMN2uhs}@{*lYlhe_U3(!2Y zjxSJ6L>IS9VLaIvt6dXS=~v%>x9d4x-|g0=UmGg2Vz5}*S2n$`l7{u8gogtEYMk5> zeoi7L&9WCr6JR4V`Y!bZ)IpbGR?T>nq#og%>M#R1Y3!0I@oS_l$5=HP0`cR zEUJ|Bhd)FEtQ|P^3gDbm)yP`Hy&2;J_U`dQ1L#f*`ZN^!CGtC4_Bt(2hZ?TP29nFK)7W1WJ%j7(I0xL8EyecJ9njAlL8Tqsqg92){fNOtPQ>j>7ph8&LZ8P4k}-# z0G~~jqg!xi0@lhUDY6Tk_wT~u9%U8Le5LC14`>WS01xUpPTTS%cLrd})3>YVvkHV~ zIcfN>5X4L@bI$6r)WhH!*s`-Eo1nypJC@?xmS6WQ|8Di-j9+qCRLxeBFveZm{d!hO zmP|**TJ1wo;6)$tyvl-?3SZl~UvIxK7j6%hB-!@=e20)mn0xlrS)vTsPMvZX9wtK5+)CeM;8IGm@ zo5y^8gbBF=$#wxS0F?2_K&3f~1C=1AKiXUgh3H-cW(qDlh1qGMy6FbisR zCpFWXBr8ud|25A1r?RsJidE)t(UWjPGdjhymG)gEN{!6#=~;GE(k;v_ZSnMfD(O`- z+6a^Dnf7)A3H0Gsg+2z9lqGtUMj>wLL)VzM2gU!81)cl`ojhe7^X6QPX*L7tYyXs# z$#^9QGBu--Yd}>Xh-aZnorSpP6SHNPGdzct;GFku6i*Suij*k1<|s83Lb(GbsSy3A z8D_Eu70{P5q#-M;)gG_R?bCFS*(v)zjF+{*)5?}t!l_RbM2G+kD8pmQ~M2o}Ik zz(<~qp2|jWeNaLfL=#t?Wfx8?t!*bF0YqpTngBF%XMY`94OFodo;B8q9IhVIr&qRS zCBUpYreMPvZoY85~b0Oc9V6?zBRqe2obMJ zusWM|fqcwf&1r?AW31z|o!I0`sb zjA8LtZ&u07AGw{-2^PnwJ_h1J%fI!oyiGb3Ap6+8i96@CkMsy!m{YilB#_>$U07q} zRR1jK;%gp{u%e%{YRz7&=6}>Y4HoNIGU+?2uFHLmSVn*LX!)UX5Vp~g)uA58sAn^H8g(5SszA@L*X#rQoXf%k zPy93tSo3siF1#0C^)S7^*o`A`J%FRBJV(PWvrx>3SP4oJ06b zCG;ztFUF(m$%UFTHF7hx9LxP$*~VD7M!egDpWZ@eHiS5%M;NnX3pe1|X{d$NBZaLU z3Pipm>V!idyxO)?`W-6o62+xYQV#4vDS&fp17BUhcQKn=IGwuhK{OB!F8y=A%mna7Je6v5@6nQZX}WxX zpcsPKAEyj}D8d)rJ1H`OjPy{yqd_M1w@lDLi2~fVbxFeWaL1h)KVcEwwotfZJcGSh zl*F0`>qS@C{b_j-d-+hSzZa(&a3oHG)!(^~#>EM-%{ipuGae=A%31%;D(j3C*QySa z{p=WWr)xiFchv;o5f+HM;UUimVfCSi1=|?MPYGOcxchcStUKvkH$A-=iT|>hSomew z(+_nqK__z`eZv;D`Elc>z0autLeRcIp`AM{T?u~>JJH`~yM~6ZOaq(lJN)_Mkz?eQR7mYGuwBrED4XCgZkK`5wooOLZq$M-l>@3=tyj^gOIH^oes;-igD z%bEb>8y;j7iwQ+QJ-@;Y!=VP@jOsoqMnm8n<@Y?EY($B&{0mcAfaJf5<7g*oijgF- zh%2->8Q}C@1Vo22njOH|u_Uc#n21l0EpqRB^ZDfWdw&LhIQ#+wPwwI#{U`trjOcz2 zxg@v=9LI?+-ZT3vZ}W*cZc+Llz_b8zh5xk53}g7&g}bO)c9^b2Bc1I}Ct*=jqW|K| zXIaI@Z`lS}VNn+z95GSz@4NY{3cKujY27C_NW#ops|Ggk-`#O36w?u)J#?Sa7Swcp%_r8CV&^hW&$$tB zf}?!?_2A^^&6D4GCrE(fPwr=^3!p|94`*RT8G~O0`=T+2>t(^aQ`M|64l(N5!|^IL z4OxEwEDBUp3i(pl8f4BYj*+ly_1_IH6TX*wa2W@V#~Me+VT8$YG3J_Pu>jwvv*5^R zrD3jgz~1*=BQlChP*AWJ7RSt|6u86FcYVHze=w21UIT_?6qb#y*%;!yl?;?@47o@r`YVx${=Sgj*fWwpz&NSAVfGB`=5FSdk6PYZVz~57x^QlYP5H(sp3t z3J&>qT@}96<7?zYuzJNAY*lz=Ac3tD7Q`tq(Jm3d2Wvkfc^JAnbcbwB*iSewM}&I|zpG zho3HTcZ$s_)u~a$^@i}wdi?c)Z3`rl)!}v_GHPS6H=x58??ZI(q-JBPyaYAr_@y4^ zy1RRVT8ufv4tm@+Tj_DAz@4Q()i_cV-U1Q%F@Ppd#SKRKc35R3BBpy*L7{f(3aTdnnVr&(E z`Jp~Ia*^zR0$3IJK;u}baOvJb-J)^>Tqc~9Wf;gO8=rO5kU^o>H4bhq3I9aW6Sf0f ztT<8j;42oG_WCeGkWiMMlyg_$porI}UfCtz`fkfO(TCwA5`X3Gl9k!=c|T|BjnKDt zd>tHI;n4rGckLNp{&KQo?umP3YcEcCiQ;Xe_R-e_r#Lg~}T|fTP zi!ldKi@Wa}S2+KU2v48O56tfaBF0kw1@(+zs{!rnMvjDSl&85fXd~6t^{}ntkJH1b z4C+a3e@f%NW#WILGzcq293dtMrSAaP*EWqY7Dg})8sTAphz0f!jjCp@_EYq^LS6dLo4Q=!dG4F<|5FfC^plL#@4v=bXN{wXOn4Ziu_v=Hb3h4Ch8baqgsq3cg{$0vN&w>N-ty#PLFx`8z$Io=^}%1S<9tUsUvQR$c7Aa3iFCIeW@l zq)OYdN_xorQeV!aR;QZPd%FYDGa1yUQ|~kr_PP%CGsDgexVlN>$(#%tsF5sNXB&g( zmveeU((2AL>4T?HNiu~3|H`>$-Rj+kw;M5ruK%?vO6)U*RBG##;kY^z?$FgU;by!7 znUe?hq^)hD5~NKf%U++YB41PEgpc*`BFR*v z-Px()8y>Zc7)|e$c{6h(q+yf)8_}%W#Kl&2XMOJC!b=sK>@(GpHCT*wIKPR%YyLN# z^z&g)W`7(YpD0K3yp`+NrvJ+DiWv83u$M*7ydEWn$N7{U^u%sg^JyI=24x7fyRL|~ zNjuEGzR3QF-iG1Yrv3X=TRMUQ{2R&j<9cL)Ld%<^i_bHrB^|RS3N^KjZOWdl%~=xQ zMB8l!6OB@qQ{1OlpCH=22<43Wa+M(UA*8(UUDy4Vnzv4E$cToISHC@*M;-^9uhw#B zcEZi~-#?J_{5N1cnL&L>t3sMnG+E-Q)h96_Den1p`>8a9*+Q__;YqCtgY##Y+z_OR zK_lNz-J!eSs!zMo?~&sdZ(hWVz4Lq3{ULC4VJQAA>C0epvkNzrWwXQ;xsYW3bB~1< zs?%E=Z?dRAKh4rt&(L_e`)d80XJbi&(r9gdE=x+sMQ3eD;=2Jq!<%jxg}^GZdr1#{ zJ$FUoAoWp5)3@Spe@%Jf6?43a9tatQ9`0tF%u;@&-?!j0aoH#Go)>OO1ur8kaZs=M z1iRMzV7GgvaZ@mdEwhK z$+m<1t3jp2)OsC`Yk#@|nHMl&z0cgMYe7tTZlLk>%3Y=8DEo2S2-hKI!?lfAgOI0$1T6a_}!%mEZenXMM$F${%E6 zqs!VC=g)*kb@Y88V&r=G9#ReePSqQg`Xb`9Z_j@WtE-`{^R6rnspXCql=rT8l^kql zNwG24e!jMIO#N7($!|#H*6R`+zcY9KZtbqi!pSb&Kv0`-~r2O@BT8jhQw~18rJ@< z*LS)%aHiQG+JIf8^E$iUJ>kwc^Bfz$vFm7S&5`Si*s7$VD_2`RW8Y=lpQ)Vdy|92b zUp`X;3jDWzX0R9?PY`Ui4zKn^AjSj3744xU)7 zPPQiz?D@oJ5-avgn4i^UNda%a2F~+xO zQ5e(sj&$q-_2RTnp|Km1=!x05RNi+1rS;rU(rXHHDlcc))%jx=x&~Tr{1TzlhB4Xj z#jc}Q0N0QGe1;;z#93>vaE>xX=(&yT-^XR>qZ3!ABAV(_g<1iKZl6Np(2+Y+lhc=3wyqK(Mm~GLh=X6AhVP)gO^9E10d-`S?%EKL7Lg3qi2xUz)_Cu6Y zF@nF((hmXjueY5>Xi8I6YVSey0veMs>6rzcM<_d@*~MsSmOk3knAPRV#s-UPd5s0# z8H|W~39Z6I9`$zBvzlC3K|TU?FcjOmwSoZe`U~+uVxK zCib;jtYC{`8GdfdJe5X&K(II?($jJ-XwZw3M!z*{uUP^zKPkQb*)_+a3f1tIKcRrV zEZ^uWFK?`;O*hDpe0mrCKZ?%8AF8(v_}^`v;uuIrry0=W}1z{k@noUjpS4ygEMs6Iv9h zpKASDRM8u=CYy>D?~dSodb-$(QTrLlCIQ7m`pWBqEF`bDBj^=tR-y6GK&-7>xZ+WB zR9vL|ZRZ|RQ>o}P|C!vN_`!LNDwXV{rLcc&9$6@VGlX*en)9=Q-0LAu)2OMjV7VLs z`g*X_GqvkYY@ zQDNSGqC&|DU+;Q0s-5n0myO%m<@oOneLuqOmwbt5I_vk#uN^^^0S<$dQm1P#v|K}$ zFi5TC^eD^b9Ja+;J<`h?rWB05&klzPS;5utc|iQWM#Y${vv=i(Z4TiG=wJ2qhZg8S zxlcVuKe6)J*=ZUL4+6snKb%-&QNly>gT;Yqg60Cy)Z1Yfk2jRkdc^3baNk;hEfwnk zlqus7*4>+f560SlK+Y(|wMeO}jW_y!3^%%Q&Hw$Uwx#3-^&>3@nuF;m#HV?;cj(kG z`RY*l>;GAUf_pv;t5`E|L-TvrluuUC_YQ}21*lQW1*DnnM#&=X6S1r2* za!uFFzkFU{m7%`KPk-#S)pv{y)fGgQ9KyRs2v)(KZ z`s3=9_eXPIUk}T@8X~Xvxp9-#>he*xG0a>q`1oW20rTnIKQ_J&(5$BoteKfQQROMc z#mOvNEF4MwNA>Rv^Z%(lqr%NQs;pDZW(8k*x2AnOivF;3<#A2qi!}P)ck?9+91NcV z#V91yEcZ;Zn`iR2uO1a2idZ2AZY-=Ubk?XAWpl zKHjX5*$Do;()~$?QlmxvDLenu@iOx3a6nXJI~$GQpxvIK2|aiLKxQpwMmGvO6m*je z0z3lBlnYP-0D4{X4QRgNCLO=TwrF8uXTsbHElKmh@57%E8}F5W-9W9f*V43OKgCo` zMvkJCd@Wa+oq_XOx<7-zBFkcZEpKL+d|S3%94-Bj(-MQd&eyp@(}~wX9bZ9$^_Otp zbtLZS-iT2>%hgcp@tckrEmf15ZfTy@yIrJ)T9^IdaV+nx_v_1acvGW%usDUd0-vcp zozF%!0F{;*M0((;NBrp8$Bqlzi{cf{8T$`Yt{?mUk9zflZrj^B^*_3y1)-FGVfUic ziGtWh0YHSr!TZGe4IWL|xSjX=78V@j@8ROVwrHUchXTw~9M#+;xfyfK7Hc1`1g@3h z7&0LO)9u;K-t`l~u_^C{2lmDvd>7l;QX8zoRkw?lT)(;7LVx+^Y;hia*K?un_c#x$ z0gr36sss?Gmah@R@PBX(mmO9yt#Rd3?S&I;WC0yj$B?R|owiUJ_Hfqrmd9oevp5TaXNqaHrN5t* zHF*}%A4^dTV*j^!0#IaVTVTGT_$5w@`Yb+W@RqB@U8H~=-p9rg*3c967#`cgF?9c2 z1a8Cqa6sJQO~v3HuQyHcL;umT$;jqs8e`+@v$iWyyY7;+lf3qET(=EE1qny`48CNA zd1|)Nq~nuzL62X-Xe9I^`(iY-82Mt3-Z)mvzhUKm@`V4>H0u&}G6FH9kgs zRPbQ9{>q;Y^Q^GF3v|>_Gj;20m*4UY&7YLD8`8(r#bV;nGjzi4Rtr#2MenRTzCGia z$?GD^`Je2NKaC-ZEf|^T*YBz~_Z#{D)O`3)@w}@Gv7v{s`U5dzR8eb8eiMTW;bm$n1!b1{9Qu9NE!uLIweES)3+FcU zfyA?qy8KJ`?hIRXZvu4YLE1}PBjG(A8u+_@y4gg!A%;s_vQ)3@d1jG-Q|qCWaFu=9 zFjIWomN|Nfp}wVY?dy)Uw?~e|iLOY+KB4c(TB!0^aV5~wB160%7$RVu1w+u{>K>Jp zGR15R*>jB?SEgD>^~hClxx^jq&lOeiLm+E%(c149ZVDbLS1~fWTpm(XFVf)DHwAlwr0c#ZK(00RaqS+2g1O`9HZxDg zByK4c5$mW>6MHqNun-)U$AO@G>k1Vcrg|95!qo)ax)O&ELe>LaUC*7NLxZ*)Em%GsZ5 z*!@}*w%b3`C|;;^;J^!|LRAkV-Hhh{N>0r7*GGS+ZeJ&Ow#>rB z=pKCtF{5M-U8WGE^FTdeD7|Ov`sml5GjU51_eed&&k$MLoWAO8g-zF5Z7_Pd)!?OH z@l&HWBxT}LI{4{RlP|6D&&(DLnv0)mHD-v|YN=b^Bt77MhLZF1GzU1gUe~iTJPKyp zzYM+R?g?JVUF=Ans@9PpPUQr9y6maHR>?@qx$5cifin;PAuOmt7@}eo>PyDeTrXMp zkQrtXHIg5oBknq@aS!lRgosZcy1rBG=R_9wo{HAbl>&RhM@Qtd(+pU$Vd>e{D`8gieV_?#i)42V?hT&cXYP#%@#{CqO{ z-KCJnasMQjcs3vBMQt@CkCrIk9MFZ!G@t5xBl!8W1ulQW4YVhv^oYyFeT(joq@u4D zUn5H<%#y0&hTlY21twj%CcEz}t%UPxHB#Or zA*}Yr1&yicYq$r^ji;^|B_EP38MmjLJlOs!Tb@f%Kya$U8QVEI-n3b$Y>5}KsiV_G zm-;nU(LVXq*Gy{Dt$)GDEob1;Jr>PAf zK77NFLmk|!6zyy7wl}oL9NV!{0*M)`+ywt2Op|C4Du@ozu|5xNiGO?nQX`#d_SDx1 zAm_Ornl88Gp8R|6sWWO&T5>}Gw+`hJQwfhG8t91NZTp=*ryJ$V(XW~hawi{48A@>9 z+QM^4A5NDq=!!8i_JlHWo}}p$E)EG#-!3_-m1*R3=&Cw9LXVV>W^@+=D0wq7tBZcwx5aoG|A^ny0wH zAmwqFLN9j|V*GQ}y1?urTIwh1%R#M#_)F+*kGScgH$fls%@Or@ONY{kkAND0<{1~a zOQo9=Pbh+e0fgv-`|mtZzZl~EIfy-Z)txj*EZv4FGjRUe`ai&*E)yRDaP=3ZR zxMprh#Yp6;KRQt*)9pu|SoeFcQ`-elL4bR_cqU_QxVO?eh>CyJ^XbA4sIujzSfAw2 zgT96{tN{q2pMLV3+FlWa6XhJzRlg+S`9&tfS2NGu%N~Dy60Dum%sS|;6>o8Bt6flK4GPW1dDW8c;_k^FfQ~aak zbwUbKdn*H>dEkH&`W?T}VjoQOZ{0%=gK>H5+vERmTw)15Lo4+Z$M!=+<;9C!@fcqx zqQ9I1yd|>h#R}UB>E4I_;5wjwhqdoF- zxAQ$J&Cz`AOyUxA50{CR{<`TZ<#z1z0jnsE2Jv#9x#Vwi;yz}T?jO+1m?hGBXR`3 zb&IJthrVGje2vw?GK_RPd%2w_ZsH_u!a)C{tS|IPfmjEekD6+n$E5-)(ingKokPxr z4QJ37iU4>e7P6{3FN+~r&Jz0vke%+4XadTGRx5Ma(k%=PDI|W7B{hHJrq1x@)DUw; zIbY?dM(N<$cVFRv=!LE24}Q$r6gv*My&`K%pBRL2cYyyFH*jgQNf`Ygy=pD>2Y-f^bI#MknfY@s3 znsmZV06q^W-QGi9qM(-q5}gd0DTWN6Dzz?zln5o%^2JNps+R=F5qj&6;E@49&Is{8 z0hqHu>DKCd?No9kMS@=~bDc$I=OYS$lF8QRXXsK{9CYhW^!&2=6ax5IH$S?MvjVl} zT5k>6ZwZ-ry}IE#A$DCrlJrn$cJ1CR0(uJt9TSReN73PtUp}`8ey8z}@v;BBmgD`< z2W=|s0#(fvF~jS#0|g}0p*UEx7}=|)FA z!@BdWs;=s?wAQ%vwFl2Y>1^ei8l=6bVjB}Z;%ab@j<)thx3IBV3<(4S?ZLjTw*YzY z5mvxNmjTc}c_LS}kgyU&DHEL|fILvQFc-jTLLf`RD(gmLe)jp{;yR;q8a~C62Sg6H z#GgpTCBeM5&NA^o=_o%QB7=?EcKn7NXCg_f0i?iPR6YXpUFZh zg59g}&Xy0HE&a?aC$vO0CUc-h=hTyQOK^=TML2$qg28<@cBc*YjBf54b-0d@V9P(= zKJ-j)tnm6BJu=S-T?;rHMcP;-TF=7pwFCvKc&fgxNi``+2vp%g%m6?L1symmD#rs5 z0WjtFkbgp4I2~IB*eAMXlE_3xGkfmdIj-g*p&cV_rEk#Y<q~#pKgC8(#6Q^Wrr{;jx~+=3H1~0B zXz^+^y_jwNz6G&uVu@O!W4GA2GT~oZoBH%Eq{tmaDM0)JkLG0uiu#gLb90%O{l@1L zvnm9c2e2+;qKg2?Xv$7;VoOR4F9kMrw~nh7Ei1%$KEf&5+}WA};9b0tCZaEUH3Pq? znO3wNkED30*Y10>4kQZpSsWoPcKz{P%K7WjOk}43DPZE` zzWOyqytS=Hk4N`xhB6t}OWbQ?kpTR2NL_0~L-s)mCGB21%%5*2QGeFAg_ zKpqiO#GUszck@7$>Jn|2t}-8Vi*9g>?bN|!-Dirh7Qf-{1>T8r{;qaDZoeLa4HOrw z7`)$CXd9%Mjvp5U{h%CuMF%ReL+MPAH9#VGKQT)HO#p^)?tN_)w3P~yTCN?h;lHWr z6%5}5oxf}TobFVS#JV(zbxdTvl`wx;G#RvPYyyIV8;!2- z&HvZBray27)yXUDkJE@=)H(&X`Z}OQS^bj3q+h0^gnCwHTomXYsf&GR`!t<~54J`3 z?HVvdCliIGJadV#!V@T@_{SIR0)Oc*VGq0uD}_oq08B4HKB3{TCM0RI0cxp^h18`&{R~Zki_LvQ)daUbyb+4cVu}tCOq$4zEyg@dM(in2nJEW z>r4^3ovpV+ul*Micd6!LGdhj!*Z3*qw0L4gHfYy)=)ghN6W27u-?(-kd|7GQM}gOC z%7r(g;SY5NVWJmqvvGGzg#nVtpJxgG05=@=8j&X!XNw;9SxO$SiRg9az0S{%Kor~OE$@B#X=O;B4K5W{K+WM^Moc8O;LNKAY% z#}&;MNvMVs6u~3Heft!#;i349H+|h~RVxSDzzyi+^OQ=vHG?9wI8yr60hY~$Gam&y zUnva|cS)})*m2RuF1ER%8|~wBSk)#LKvJ*0;P+F=maZhMQ?7~~=Rf}Om-$F#)HF)| zW0)5p(OaNi`K<4x80|p*-(h9scGixJ$g@7jhG+ag;}qL^0mtW+_qB{s3RKZ0Xu_fA zxc4m=0rH)K%N4xKA6{=w@hT%0!xALAwc60DEM(knn{1)rk2eB$+~koM=#+q7!9Xt`OD6)$w05Qds1O)_`b1EO9)`Da4f2ye$=L~yS4ouIt$ zwO!FI=5j(|3?D^IWIGI(;*!*mnu+06w|MXz+b`hyvCO()@9Ix1jm7+Z$l;Pq@% zupYl}J+5fvtaNXG-dd_`W0=n@nVE$?6tg)d%*~vKRRS{pl%6dYAeMMK{{b*}na|yh zpOyUuPms-g@`BdRvq+!WQOhpBk;H7WAHHCZrhbY{xvjq2@7O6KvPwoigMty#@mV~m z%=ATcU-&@g-*;c{4YZ%T3Ha}J!lYpe0rcy=bQ0lgIsc&^U$a~6yvw}`F5<3Wk^YyK z#Df<|%g?hHzT(6JIbvQc{SzNucJfK+RI#`_G&jyT)=JC+Ncyy4;4w>D$1S`Zw$b!E zer36v^(g-F%64dTsB(c_!>CwunUj+{?_N8Jx_;OQt6g;~x$| zMQPdrv?y+Dmt+%=hFuj-{owJy2V&lKZ#h;iZ*DH(20#5O>OHLcX?q409e~R#kT*_; zf7~#1S^i^Fy`&A)RRGGDr5Plr6NAz1{f{ zi__bn`j15_=lXS=N)tZT)aQ&!8%2_5eAY9uH4H8R8E9ogAUk-I+PFV^#nScrM?7Lof{Rqvzq$RRj)aqu^z=2Kl`#H zA^&K}Kd$GG86}V&-VO?Lmp@%$dKzcmx&CqD#-WQ3`i_xO-R~aW#_jD+aIf*GRyd8C zQ2YMLrrN~9k+kkL_xa7k^A->P{T<(#!yt~^m1LGo+Z-fRTN5Duv6)0)HC+OOXatqq z7D4`|yD@PcJz3HLgdi{`{^Kgyj*9$H>~NA)Lv(ULjDch`_{zm{#@xek(c#uFb%Tw-7Yk1IN<%-ZJWvl<}M zk=8q|%Dn+gR(6t!B~tl*39}#Ne%|aaBA5&{WLul+-nJHb_?FjCyo5UY#qNu^vRYrA zZhbG-P4aQf0TFntRlk+9x(!(~iapb-BB<3_GcP zL1QaNicT4*hxeR<3T?Hl(7)2&hAl?S2sA2CDGf53sN2^n_43|w@h-dh0dF6+L7G%EKMNDdK~Vg3_`%^5t|?ndT3$IZnOc$5 zN;wiQ+hP7b&=13Au9#R}l@H<=zUc$WZUCgT8lE8? z+8N}n)$IP{YBaMw_x#l#T}^xpy;QVdu0&Bc?_5~24&QNGWWx^9##h3eaSV7gz6!Hk zibVjYLB!GI`)P@@y6Z#?IgN`qFzhjWe*|-xI04y)vXI6^(Xdu7L@A+XmkX2oXoUSR zB4t3`*wYX2b*=b{Ru4J>P~acUH-rvn;4Qtl?rEct$@a5cVDE4)jO+9~cJ<)?_Fma0 zItU|72X6QPQF6lGP)B{U2A=C|-iNA(19K!f(tv9Fo_(Jt22TGwS#}_!2&yCuMH+r~ z5Z?;q?~DWH;D;xa+608tAFI)lg;R$~M$LfR!2v$WVA4}crn<>2+<7WpzT66GWk_}O zC*%>)nULzOOr-V3e|z*bXy-wEsH-T~VR8F_+W9Y3CsvOo`Q56SC6k4CrC@1upPRA3 z0U{G;EhXYXS=!FGik+7%$?N|G7*YcuQ*FF2$0dP4;_>@c<zXW^-fe3I zNEb*e-CWW^(8;c_7*WEMmdgpY)JRn3P0hS6aj`r?hJtemg45O~n*`tM961Dgx` zI=C`9d{Vs|R3nX&iz^Kz-5wn%tN>^6abGnRQkwm{gjc<|2QNDzzE7^TW`6l#tr{t5 z&3>c7=+1~jCn9yq6CC`?h?dw!P0MQWPcTb^YiJihpWKYV<{3c(b7&q^L&U;V+3PS@ z;*qg3+Q*gh?W>xf>RZn&T=UT4&v!D}>dq4cA~p1b&Gu(CufE{D+qwpr03n(+K6>%t zyvgqU`U2(Zj{-nwA31Ub;etMg2kjL-rui>&=H%NXv;4I8S!V zDny0Zb28OzthQVTRh=h4vgUzW-D0v$o0uSI8KBTXFs1qVP)5}dkYY{lF$K(lotA{M zCXDRUU!^g?oxJ+I(MVi{55=$l@jihdOFM{jIc)GT6Qh<5v_ksPI1r)S!a9J}Ip2tsSWdMk0( z>5jM?2)zs-gwPrB#ICOsH~#In5^_XG_&_-X4!uF{CG0=OOblM{J}jv^uGt2SXR)3} z3Zfsi@dph5XsT{jMp-dc_2snDFvY@Rc#c_(syZ+?yg$9Q=r9e}K_!Hikzz&=P!_IBDS zY8rGQY2v$>@v4N_pu~??x-mSs2ODqU7CjaWzNxam;fJa-^i*08^isbf_P9!Lyh3)RsJgJ0d59g$=?T6iYNYsT*N7_l5Gi=*(O&9p?%w*ujYoEA zFj$$2Xyl?q+n~h4tIYlZ8=uJtV_1@cZlKoC(PfF)Br0igkZ8q&{VX!6VjK?Oo;t=0 zCmaYt964^yhQx6rvdf^py^<$~*zH-#>{$C^+kg}9UI9V{iM#M{O86LTJJ!+r_|vhY zHg3^JGb1u!w7K{w5XbfPg~LEEgT_pSv4!xq{St*Gl99ZW5h6g84q8n@?xz8txdZH$ z&i%I}lSTv8x}h!`f?_DUmxjO|_Oe<8WIYr7f)rrIIPYcS8(srt-j$>*?)vMaaaBQN zNkIf~Ni{4~CzWivY-L-TF5yFnANrwSQ>Mgm(@b750gC$ zDQ_3c4N}cEW(^vhx!hB9FC^h0!}K~Vumm@{U5!5)h{RbEi4Sgbf$p*~4wCA^T%c8a%%wFmgixZc%mlZ3udy z6d*dp%JqRb0z#q{vJMrf`UuYdZMal^P;o?6B;r*B3v7DJJJ6R8{j&(iiNWk1p9yp~ zxTY6+s6TY>UU0h#v~4i{7(GG`M!a#At9q%(ZvnK!+Zq@wDs1=ADlGi-tKjAy;%<9( zZDzEYBn7(&X)lfTdo0;N_FkWpNDPrk>w$=TfOybtT>0LuiyTK%Sl6SIiN_rkZad_L z90+T(*HzRWvo(BZ#0*x5NSuWkd(wS-pmH7(+#MDSVcRnQC^U`z_u|CIwi^EQQ6Ez8p-2;sCwcd4n4%VuY^oj`CM{!@trI=+FU(1B3D3f#m zo{v)!E2&W0teMZ~{(tG-u8i^>5nb$}O46*p3!vnTh-syJ#0RV6u2fx0^v9oEt+tU`4+g%8He z`Me#|lnmFbryx^AcjA?UuA+P!d8q63e2(XK*`~;mMVrc&q|76&Dmul%3%!>_7*78{ zSy-S^SUXj^MLJ+U7tWQ)zZx=>X(8tmYs?Nz=Gz4BuXuAJZryTM>jKoCrkz-%KHFx0 zMrrS5pAfS|?}O8kHZeA7{PF`5u*zB3=~c5o83$5MXe;Sz+6z^X>V&Lp+a^uqNk+LNUY z!d!9Pz4qQA+#^7x80h&nSc*XNW~clW3aYdK)jy8B&AXE?hMWhYHdf{Cj-Qm-pSd0q zXy%b3LL$4g?ule*{EdSzD3BM3B6`)^sRo~5Cak@6o`#CwL{HrH@Gvlsi_>PtXatIr zZ{OJg!NU@ZwZ}kmNOWfW>YO(A`a@z#Al7u+u!N8}hsY)V?g#oBr-uB~;<6#K#_a zFNgET#khVdMCoh37Zz0pBvP+^B!C>61ioz{h`UdQrK)N*s7rdTt1#W;`x_^(CC)*s z74$c9MW5dbzSB{M?1J1+hcjv0F4#Y$d9lN?9Z8O{5}Sr8`#uKX#8k9-pj}L{SC2zo+(HQA z9#3yWp8?xm);j{fAIB&+0=@j7mmTrvCw-a}dm$|sM}bFB;9imPg{z3OY`h~A*Y1=yD~ z%xO2(5H9>71({0N#?%wqZ3@sg=uihhPt}s{TDU>ri+K^pkU^ebu)RGVH&27 zjTxe09*jbdbwhoHss}l`$5zq9ZYbqhSlj5%Ns#?+sQFe>8Ud5WR`92zi$$NX->q`5^1Ut5SKXWCql*kFom2;(ZM6>lUu>Thr@3hpr|=?phwvH-q+ z_{fn++5M3NWLddF0@AS*%5Xz*?_NAQGA17u_I|>lEW}%ZqjKX%1G5x1MZ>@1!9{NG zC3-zIOi#@KJgs>R{TG*Xs2?^&!91=8*B9VAnPBHtNNWtXlYW@CCUyn_TNLiB0wjRF z4LvA;WC*dHG|cBw$K6!zAP49fFSQdMkRlpxnvd>{!MzZ|t$RSJLTt|}{OKr4O8``Ee!qvT&M@dF8 zDn{Drjx139DlT&rMCL%J_-OfAg)7ytU?Jvl47PY5 z=3NXftw2K7=8YCX*I3x&6zl!H!{PpG^<%teU>B$NeI{U{3no6$u}}O^%|gsQ z8s?c0(?x+K&cU9J?kpMVfe`b8KKG1*pQ2$u^DxidFn_!-&v=-&0@$D#Mm+$M?1r6Q zMI0_fe-)xLX_%)%%%Tu|numGF9MM^UOfWGg`RMT&%zXlkHVg0NqnV?q$8LyB3TBWt zwL-vr5x`z?C5>a4!xUWPEao#E^L!N@P&7N}hI&;4PNQSLv$5xekPabkjlWZdnDc@U zPYF*ugxD8?k-)#WcXUi!Ov(LGbP*HAjY_N4=)KpB`O*Pr$s6!42`T&sH&? zY1sEMIJrC6#{`>QbLg+5X8l6kf1{YM6imk`?j>#Ka7@u(+(V&7VF@9cf?XhBUKU{M zd7@}Yu<{@z=y9J1KND&X&FJ;qlgLaK=C*fSe5-5x9~EBoLFNn-{$2-uCI)_e5#%w7 z7-NCbVxis>GCEAPT@#&GX8fQtH$&V^#- ztaG+wO4PEjvU{D$?llZ8kg{K?R#Z^+FGdhQBj(s(mYnA0JRu%Sm6_Vl}K6A^UzgtrJxXxR>qrJV!)(Rir z-t_M0?mH9GXyuQd*5H01Lx%vDw|CuMw}YsAE1s_ zlE{$tUr;hGe0S?XA!mh zsoQ(|pS7JR*(Yrr6|m}a=#{ffwRXl2(wV`s!>N+NRJ9fZkez$$T=}LhWjt;0Uw2?n; zeft76V@i=K3d;+`8=|DDMN}C;n=@L7F&pAuHy>ppUe4izh&I9!TBOBRx-*h4l{Zh# z87Wz+Hh3A7^E5}mXCU(P2nxApqWL+lh0A7-m2^s0Yf6(RCE-8$i}DkfF+OgB6?wi;S5Xiv7Fxv7nu#eZ7VzqmX^+MCxyON zu861#TfF4)ZcLdR3#+!0?&z-$)`=*&q=Gdc9|iG4Qb%VqEzZt8%dqSIZ9@#dI;L__ zBF;gzh{7$X5f2}Hd{xYRi`I8Os$@_0Nh@QD|xKpbvnUNBf+xl#}+O4 zvia8NU}a?09tM&%pJFRXPHU&4&C5Gr6~P6pkn->m^~^Im-K(IpI@TJ2NTqV0xp194 zL3yO=8it#LdJA~kFZvB;KuSB@Ci$#x_=2gQj%4%gj4XbnLP<;L=!ZuDgO z$7Gqd+&&ZES}WGKdeJoQC*qN?5A7s?OJho5mb<~y3FJ(p>)YFl%g6~kTPx!Y``@i(bJq|L_kx1j~75}SkA7s`$7 z1;FlpWFU+iM!$INDOB6RSGzMs-Yo-ix_)TK-0L1w*#EoJ6k|!O&lys)<(xNO;)pa% z^`V7;Tu+~8#H4kwTt;rTzK}{RRI{e6v*EUtbHuZweLDUJOn1M{ue@6!OHR9&ah8s7 zP2D6otojgZ|n@+l8wLNoPob`v@HN!Q@)h;`{nlEwZ)K%^S<1M0$e`piCOEy5 zmpL=^p0&1WBa!qC7nNEo_h6?*8T#Q2WxZFn-sgbkyF#qd@Du#0*8^rnJ=w9XefS&h zS!U%_d=2IZh2C_|sIwp|q(l*We(1cFXfoE4`58Oq#YYlXR5%8BM0tf;M(*~?(DvH4$EPCt=q)NPy&N9ok@AdeaH}GH^_<=Dh?RFa ze@r-pAS$M#lIAXgcF^Duik7Ss-8p$&tvXRCa3JA8%Cp^H)+%o_A#C+}Au1Vlu8JIj zh+`44#ODh>b$7bNlpx20@?P@1z{cNp7M(xF73WYgj9$~T?Fd{#`|5zk3LRoZ>c#s} zKw?`o#$J*Fv1sFiPLex9Rj7!tS>nq7c0<)Ua>bea7^+-hma$J6u6}A@mzHymOC1&K z^P-oiwuL6d^}xIUaq6hqOqK2)Omd7>^f!7Y1;Ih>=tPMYs@E7*0lS5Arpr;SYY#Vml%h4sK`?WKqecaVgaCDbUVSE3`?TE}q~ zdgC@{q_sZEey@@=O*o16uV^OOKZfrOvbPFzJ2sPl2-N%W+_94tAGD4qWZl0^oxZ1k zOU>mQ(ko}K8{BBEns#u7Qz9*Pjbfh&2PvzaIvKMZb?d~KlIbf>wc_zlIrfnN*YhOm zW2Y&QL>VRG0uvkI$LourNZ}9j8F*qHSbkgpYrYM%irm5B?PL}vUTCdr`5h$MP++*X zY!;imwB3shzz{Vk9F*)FLLzN6L%S{reZSsHLS+P|H9s*FAQ(-Lb|Y%f2;kqkm97Ng z5}gzn2*!q#SnInCy+1(teQK7XW%S{;6e`C4bU!Ek=6@MYGFHBVg0y`KeyMdp45sCz z1fPP8jyWQ=914?8B|zjc8VDzZrFuhQzIpQOIYHZK);kIm`(^fF_X6nFDT3PhoVy6@ z#Wv|Fz;SK0S*!l89zJ?SM1koavHwnxJ%9g*F=Ez=iMVT!L_)ec9>Htkm;Ranp>CIhF%Igl@(9WS8&dKt( zPw$Z~VE{#Rk~3IpP>p1+9{8pz4}=DSRn@o`5a$dE+NKa7?cG(%?6#tUwhK4)D#G55b>u=aLAx%fb06oyLQ5~4fJ$&n;ARrd0~}XvIKIiA?$T4qLtB$ zyZ1D`@3A24@l~{}0^e@sSnU{s4=Pp_AV6=aITK*yBpSrsP5^1JEtDuZH0Iq+$NL4T z0|zoh4umU{b3p_jU>}f!q;Tr{hnu?ZO{ElPl@0iGU!Hk=`Ov&n-8b13P|e)$fs)YO z!1w~zdV3qH&_KYQsv%&;%HxCX-zq#O;lzE|FQwja@lq_eK^o!w38X|f(b@p2kgM=Y zcXW2=c==j$DZ_mvqrwe>bOT)}2BccXpwj6|8%`)7)&c>h5TVEb{?Ox_N8gX4P*4pNOnC}aQi8ii&KOpO_T>yka>wa*kOS6EF5>o?7Q$S`h)Pg5w zrd{PGn%Ws}xW%KlibqG1x#MRx2*0+wf&ZiET>M&Y|3AL-)(*AKYn`^W&Zl)wg&ivE zXptnLb(~ICk`Q)i>!8vJA*@u2qzFmap)#qggt#5n0deEb;dYO2_uX&5zhIxowd?wP zuIqih-p^+8HSfVHK+lJ#C zhA$8KZ3+i3lhM@#bY!@I_rEhFrGQL0J0GTVL+c>#4YT#1{2qAE8Yg(QMK&@7a`9hl zE`jc9gstfTPHy0bHi|bX>*FNixEUd(NI>qF9;LQ%f#8wpGuBcN*BgPB30`)M@zn$T zMMQ(44w)m0a~QK@3d}5gClgoQ|1R`HfL9aI8@>s*v6^F(Pg@3q_K%->i55~E6mo9` zrKlvG0z-99i2DLvia_4Wi02xnHe@Q^S8^>+5Z5mbrGV9^sE+DZLVq-Ah3D^*B240l zX}88VKfdzMX?y#kwRVUH_LcYk4zhg{c88$e$jX2IS3yvC-3yX(^Oif78x8_s{!e0! zw{FUL0x1i=vgr?KNd2D|H*La;x(if)@SklRLI`-+qkr|iYxwnu;Z9If|pRS8~y z1lc*n*v)`i+pgHp1e+rS><;nTB}3z~irT4xL~dyu3+%|+Ob@U%2Y~ZBM6c@gOz2Dq+>>Yvex^(T`F=&tiq%{d^PL_Uk&r1P;s#aJweb z%Pj}w;!x(FABy7U&mZwSu<-4<|#zc;LQmpr?E z=1h$>JC-;{B~Dd=cSvC0Jz!b!)oZ!pT{w7h&GDW7 zrEfV_R4lJtN-D75hoj;&fs5I8i1Op0@<`qr!wQpd@u*g8I$rlC^($-&fXy zW`2*|+w^E{a6lHi+)h&Cp0@gk#1nhS5{vi)QN+{n=K+{cU<82f&!hu(i=31ap)Mqp zxhM)<6sDK_5yKIsFH$9(it0tgF-4VX(~+_ZYd&luDpf~+2$E|`IS5Iv^lXTuO&%7W zMu6;7!M3r)dm3TcH9O^}!~-+Xtd$P&;TKTz8R#yoc$ajJ+#$+YOs>f+N#Mf62%t2L znC%ZwbASW@z^OIxZA#Hzgt%u^%x>(;B4`H)evJvwV8V7GVA>x$oiDb#3){gJ=go*x z`^8%vH#>5iMn8)7X+&|Hx4Q{2{Ktv{Z06}-+B>+eX)bK@Em#`2^tN5wT8_8?3sbg> zH<`fLoYI~B^E(<{l6zVS9n?3DB6guTiC1ZK3bv!?Y@!O5Nx|;WXde~4V@4bnC*I10 zYngkYOmRU?iLDbPlWCU1fpd$*ZW_@Y6_6bg(S9tlD{SBqWc-PuwWtO3ZTSzgVtNg8 zy{Ge{%<&0yrHBVvdLMg+XGjIYxmp`9pqy9h(TB zYOt(|IejHU$0GSbVmy2Y3!XJ2SX(46N~%t*f$w68Z%I)3okw=Y=Tca((1)kJn2hpxY(N=%>S`ILy9~OoyO<;oHlR~zVyALPcQB*1#Hudp$&6okLErPXp zimhuPIapYj`shc3F1b zl6SKIpSzQG#Qjg0M-!}GI(ARM>NNw?8~< zr7+WE-HC3e64H#Q0GFQ0gL!plXHvy+5&`)vEL9_-Qcs4}>ST0mt>TP{E9pS!0LB7vr-8&zN8*X3q|ek*f-v3V`pRI2_pmf8KsHqfxu! za(1YIZvC&~mc$8w%qW{oN9NZYrRdJ%z8y?Bz40slX0warp=bE#kUTS&B5?Z$&{`Zk zg9>WeR~mE(NawuGz-ed4*Bv#VO*o=mN@yw*cE?RXUizG^PTRYP+@%uxth_^xJOtrx zK1wPo%~FbCy(h!cuY3Os4!#ErqMW-w0$-!akFpWa`(d&O=eo*Y= zF4-wfp!(@4rXmOX1Fo zwVPi6GgAx`Dt=l6`IbDv?O*L)J%US3KVLpO$a%Qc*0q0n>*gm}t8ZAq_F-XeJ+Pv9 zQ6>kT7%$q}Bc@YIcjL5g)>2M9G_MCn?|>Fk#2HGc*`i>1{#A zBTi(U>LFYnc}of%U$0MITX^exT*ZsQ&<7J?na3u!^z33+OfN7(;sY|@A3FB?U6h&6 z>f+bDuU_L@FKzp)JL;itXIwzebZbC}$q35WF=?r2i?K&o_g=*h?El_%ZR=SZbNJ5x z&Uc+@P5jbv>yJ$i=GQZyO{>D!b-nO*N)ws=`EK2d+nI+?5s0Ld%rJ6vQ?_<1@i5(e zo132_>6B+JA-JhOZ)!*<&3^k*`Q1E5L9JWx((J_L1Ac$KzIzzF0>69yezm0!Ye6}i z5>jC`dPTEHD~5`pKoC!ApP~OHB)_FW$Ia@TWnlh7<@;@<@CKiQYb*!yFRc6h`{!Tt zkFFeEyYkw`vAB#bMifFJ3#ANM^v@LRFXnBOgOZLss3lKP3j=@NcSzz1bL#_CCr=%}eI zNmCv4fnbBt-|qb#Z5q)qH4*TcW1~ZSp|nY9h`MWo@fk6i4EL-5L^|qqXqi<)eoj8W z&U3b7a&*;);`xcd(Nsg2{vDQLoq@b*`fFjP+ch+lYp$Aq-@0;< z-C&+w^!2{eQ3p0&f61VC$ly@WH<&?K2_Is-y=3;j)yn1NuiCgSK|hSVz2seR>PO$% z5pq<|*L9}bI39cPk*%{Ir?xG7j8USx@~horgTLK3JFKQb4YW0YLj8)zVr_)=WzCQw z9U)uie1Pd$ZnnJ=1GD~KI4e$XQ+*TTi#4<2Kex4dRC=yw=2v?%)iUhVF3n^(v8bfi z*m&#izhTz2-6lG&hq-S<7T4wlEhRsMY6;E2gF8O?OyrduTHT}B%-!1((8z_w7j~g?72N);-~er z2#LT$-F(fhu>$ix1*}Reg+IIHhGCuu=@(0cQ_D#@4G-j_>56^}TRt)gVPI8Bxd9-` z4qv}@d_WuTBg{y>o#*MMjp{XoQ=_N^6~kj;vg%?b36Uy{wzS?1e0t{#-_(InAvg4yc4CL0p%RKx$LWm=guBK0 z-B7oot(9(g@jCuSNIPQHH^Em;kC{vMRRoay%3-^4Y9Hin0eQI5kt*O(uu+vum~ zAVatGAPf$XuyIsAG#0@tCV4j_$L&XdY*s!G`kb~M4O z;S_N!{rlCXUyoW=PLyvM4BkC%?J~V4O{u_7?d!#~e-A3`R}{J+%KlogLt6YODKz=b zGyCxvc@8ja@014|RE=^6>Mypb+JAaKak|)x% zAI{Bo#$qy)UDQcBoBnc19DbLkcj_dCEaO2bt^C@j8`~0ZaxSX#?8@4|gQohgZgMIx z^8Rn*34=Z{#LptlOH(B9|2ippyXK#Z6@RopCfzLI(vF{UJN+(9Q6r`t59Za?R$*P7 z%}kB_{85Q3LW{1Xv(8QT&ny*uhJK|93>J{Gbu;r`(PkZ5ZU5CB8YOllr*R z`WSCVLIUbkD#PqHd;Sf>;2NS}iSAua7_mRH*Bf!sYnMsC%mr+5c5lCM??Me!^wn#J zhWtHfMgKPd+Wy#3;?QiF&*DQMC*I)S_|s3Y#+R%(-b+XDw_Fk?3!3{>C*pjeM38wB9TsoTNv>d872rSU_!CxryUqv_Z>a~`Zq~g7em(vS#l1Xuv7V;L2EDF zRxp0p*~c%=oHZzxmSv%ZCU+(Oj8?D?;zR3;WVg@Wd}4Rfu7LQ*aPeOURvm$?1t33k zL*|y^6yi990<-FjW7_d0@P$XQC&Hx7ST{p)s9F@LDG%f@0yKQXf=!#-fR@Z~(?OsK ztHN}V?^z_YDiWFGh@JL@yx50&RxJ83ZE6K!xGl;A%@}=^+`r)wL8NcU6}NK4h7@2f zLbmX-G4&JBQ_cUy8DENrP5V4KGZ|r6RzJFI`K%X~QLbxV4E6j? z!)poS4h&)!O0y@{t&8C}EjG!Ki9F6*bLVZwXKoar)Q~VvZ zV!rBO9dPoTM=J5{YyVkcbTct`Mi%&rzb}z(G!4l8A%%=RUqSzf*trwrY#}G2Es&9^ z;TKzrC^AfIC4UTi%X`baVYXx^4couogDdna16xN5gN7JhOEeFEp>3rws8YC!Af)s# z0{es>L2|D?aF9}{bu9S~Dgsm>Vvxc=D1?ZRz2wm<`xyZ}<$ma}K$U!A7U)xk4ARit zQW*h5-~bk>u}|S+B=Cq8lDS|lG|aO=PT>dx`xM?oWkD2nt6FKE4nogX zMURH$^CQfLW$6snGyWv^Wzy|eyY%Jv(S_t-^B6)rI?XU1N5|E38UPX++ z;yQAom|Rxjk5>4URd`EMI2!$*$AW1$-gmj4>+5J^9%V}P~W#z?J#Fx^Bf0ASX=AfzuEn9%fnn=v}dIsF7q>Ql-X!)KcA#YuCMGQ4q$ z0Dob?g514};e`{HE(=!S=6r&LJ`kb5ztAm6=o=}F>z4a{5?pyAUxj|+=dEz96nZoW zeEq>zjR}-Wuxl4R00*u|E6AgCDu|2|ZPE?|ueZLk}b< zoDvnji3*Baxa+du^K#f)Um+)(MrmSDBEh{b5#Ih!r~~i2Fwaw8v36#UM`Uitd^WpP ziw2OX7$s0%Gj=ZrJJKYuh~&$Q&B8~Y=g@!$EaZ+9dB|0o9)v2SbMCa0ao)}K<0C${ zWfr5&=koXz4&#qBMo=tcW3qxG1?RN$%wvUK1hA)4YNNH@1%W-$V9%cUfF$9Hf1kpO zr3gr5P&#A=+5EOopuIPQUL76QGdvFtV~$KCLxkR`!Yz@(*9(Nc@gj^je~Nk0w~OJ^ zDD;mP9>LOx(`D{Vu%-0W2AtwbB*QZm?Aic2byw*1L+;x^|FW~nP`wT350sb)%pr_` zMWAt{!fz>i140o%R`_+uUIsUM&O`@PGCcc6Z^d^~2r~16XlHM*Pb2w zKLqjJlZ1!ywZ$6}9uk=K!;xdhA^}`yo60|xO>zCBGJ61M<-;r(X(6!mu598{?>f{B zL8rCUXq};xcanE;ssy$cAj_FjQwY)A1Thx;ispHIx)Hy-P@&huU!`jIh}H2N68d@z z(aSWy#7|xt0I^FrT(_u=Gy1a_6g1eU0YqE|`6$62{j!B+`s`E4!>M5mLE+y@yGT|9 z5*QSzqTw$1MXb<|8iMuz;{Jo>TVwA@{p1x1)?cOtAQWz?LhC`EwQHs?6P!X8R=F@J zLm%8Z;0%L!r<`*}l3*eQxIQQpu~7(}2U7wRK4Vd)Nis+}n5+?a^(*`u1SV1PRXDBM zh)%&We2axliinI>toId?gBV^K;EIE<6M321jRko$39TjSz(u*QmOeVDa9@`Ds)bm| z$7}*^?G2D`qyobNLI-Gr)oGOD_P=9{Qr&rN!DRkQzI8OS|c86icK8sw*70I54 zum1u}3E)5rZqn0s&oZGq;evMkdoN0DD#55g?G#{CIVlJlfiG7W_nYQZIdpr_mHgu) z;5LIe!}FXeF(vT5TfycC#W{!~kVi9>I9X+No+iBaDie~K^p$9Zw-MhDC3G)VI7P0< zI(+tv6nKm=R$M>P1Gjg3AwF4Y3B6>nM+fj(3E$`?-Cv7I{UHd{w)1pd_UIQ7Gl_bJ@FLfn)P&%TlUR$lqFs9>Kb+0;$my)q?6=`)Mn ze$;&@FJJhRN&t90XsmZ~MuQ`z($&%?59vuCi`*ue@ee7qf$g646S)QCP$@XrG3yQW zJzE=U$Q-9_4{J6{-dQB89?>pj0VN1bp>@$(;Ar#&V7|w+4qe3B-8{*Y4y5UMT5u)%SL&QNsGC}AO@9nc7 zAQyvug5+j*Xr4@=^)$$*sN4(v(5I=_tAnQB!S^DAt$Q{IR<+Me3w(`023RQ0UlE|r zbVV~f3BWb2Kobq08pI8pX7-8rDc{N|UG&hU2dl8cw05DNijJ3xygtd&XPALYt>l-y zs#b;XG|1$o+y}wHvlO1GG`B%vj{Ji)aW5aF?_E^dei7S8S=>%7g zxa*WStXg5M2BTg0dR)F|kivi}575w&F9ETyco<~>{y6~6l3DTG^=GJlYpe{o$H+kv zYYOclfuHTo@8rI+(nyaStXE;`tUmEE{?7=a@xzzL&3;KSjUxwanWH#lE}8Za!Oz9^ z8WH%YejZkufH^*77z89&wx=fndxocXh=A4$uxCvt$Qu4rwaiM*H&UgTF42hD(2EB* z{ar#M8UfK|Vx-IRLJ<^_EW?h?j1ESujNBgM&1h+&3R4YVcNtI>voW$TZsf`sc9}Q& z-pV*lhW+$Pj{?2po9hm!-tkqAv?t};Yd-^X}>->lVk4Ag3=V8!O0}jpgq{}O_O|R|{ zi}nCryTB}~7F{OB{;2pN!K~v>t_6v(hMHuWREtI)zd18wLCyt}%gc%=6CVT!{2m)rrY7AM+B|7T`J8N?R<{1tTUW9BV=crAh zaJLu5`$aJ;t7L}RF}MP~Dp}Bqp?a=lzsd7K6zA>he>(>`^Uw7Lm%pVff77FVH$439 z{n@mfeYM|DtSklH|18BXFKh3Ebq2Q^70Mlo3R`yEY1#PilO1;+M+7{*{p)<%E9s>eW0NhOc_c%($Wz?emXe72O3iU>vqrJ*#D=z| zR|LUd>lkJFHsJ+ZN^}g!kxd1)pm&%?^4c76>Kydk2{+%vec3JASb<$gzspX+m9RRy zRq%ju%@0TY(^02Q^piK1VT;$+H(3_>me<>Fkax@yp_l8)k%tBj3ZRt@e!Jv6oyU(K zMmxrqbwjV=@^DU?*R0uF{*^1IDaHF>u=(Mzj8oA|ME(3{53nv=1+#X_%zZmCa%jM4 zit?epHS$pE$&)oVYFfh%!r}iZ{PO14im{Fn!K)Mm-`ax2z%%>z{Q0ampI%Nn{yQ(g^g%*aAO9OjQHI~d`11mB z@a2&!GNln3?JKRRbfBy(J+B-YFWRrTGJIC$Lh0{_zwC4MOMa*8&7O{!s)xB20v)^U z&PU`VvBq`sc|v}9nR$s}a+yo?FBR<;k(-`u+VWYq#G$Ye zXuS&Nd6XBI_vDMwe?JfC(GY)|t5(NGKQIpbjcs@|llEjQm#|6N_<;T{;WwXH?vT)8 zQ+2Ap<-A=3VeWxAAysFZw5F}F?Rg<1x%_m}Zf*Z)#av$RL_oyctCkm#^L8glQ3&h2 zlMC|l&UQry@u(4L?pCR>V8Z3b)6s{nPcn-^V}t0ZwDZQ*nd>*!pWqfpU>wi( z|Jgl>-E`bmwX)vj3T;{s)ZZb7lJwsYUk=p{*nwyHU^HkH^b7a%m&$tDo5`d7YXd2s2x!Z;dn`p1?Gb^a+L z??J-Ky-wCt`UVT|z(PpvhH6m)sl;WWmp&MVY_Aboh$tl|H#2dTz9n%-kI*vqYq?9}>{`J7N7RNrH?7e0wG!sAp+o5Nme(~5GPxPFdt+LVl}oGq2V%!HNbzxq%6|(N@{`~HR{sUMn`pA_w1x=mf*FdW>hgJ*zs&$)sR|& zdo2-J*=;WKTXfK=Q;I!HYLEEwVAhA20;8X-vem3z0b1~x&D_<}Q0)d!>;JlBRYBc+ zJ1$*sjVr`zvF2&tH@;5n62l90>-g#=HbunxXcInJRugXG?f5sNN<)1{Pil_d^9pQG zM}gRv%wBHT1Kwud3=7H0Jd{jJVl?)Y+>T831EXg$cg~M(`_Il1iKgp^4_h46HRYm` z06OVfi)egv>Ck6g>tdBou~KbtVv#{W$jU60JZ#V>p}p>8qV?5TR^ie8wZMOQbRNQZ7fHXu<{jJP-RY6ie($NwB*?& za!u2``CR{qbzGkA-Y$mKgp{s7$U3sO?>v5-SsL`Bwu;_x5j)*D;yqPc9R`u%9&0l^ zQ(vnJ7UT$D8QABAM|DA8i|H1V{J^`!qdSf0=6~Vt2OjdMv^_j;*;X^OY9cQsjUdx0 z;*Qk4@u-T61e+ytM1Ii^=6Cg{Og)bmc}_KB_G(&;pChgX?w|aS-IQWpB|TzD_*#|I zF<^Ale<`rZqX%kW?@N!Tdu|oh3iqUM{}Ryfcf$`7&&P{+t;r}j-B4c>aN_x`WB{1T z5-udjbjROS>m_?2KF$H5o*KSIK3AYUf9S-;E0Befl_+{*Gxj`lnAnPme#whMQW7Sk zZfISvMxTw(MoRhfh5jqgc99Yg0!wy&hxnBHTq^L&ZWNoJ*7?t99j?0Afwve4P_ zOtH?i($njt>jRqrPey9tZO)`kvHC#=@y$0&l5X{Snh+^XC zRT=D%+O#6uqKSv>lW7^8W*a1^ZW+!Wu6LG=7fAJnWM-#;W))O}5%yOB5s^=Wpn#aY zvkfoV_~Kb}I~uHH)|}*Fd6hUcAaTXh%#P3EU-EHh`yL?Q!L$%p-Q$y`1wh-GqgxOe^>)GQvPt&_M*i!krY?0GNeOResOHmb z(=wW6$qLZylnmZaSm4Z#3M57^h^BImfk94@Uu0Xv(ST8vUUh1{yF}Ae z?zJ+RWs}VO22ZD3W{%K?C!6tovXw3u^NX|Y-Lv?mS=Jx{H$*hO3pA@q(i@ZEZoJc9 z^QE?sXnuo@Z%s0l)65dI!#}Iv!g^OSYdXRIJTY4?8&G5zQ=f7T?-Dp1?ct~N{*VP83tLYf$2&50GjR>H8Op->klfdU{<#<33EIGldW#r&(n92!Fg)rQ>nQR z$4Hlm1k9rQrK=Yhcb3=~F`*=!ubYqru6&D1Omb?0+Ko7n=6ZkjYvHX#E9_C67-)J? zW)UvKtG}DI$Y7^X8f0AQP^xU z0bJM+gS!f}KQwDu$6uuPSr+n5CfH7IiDqSNE6!EBJ5tjm%tJBl9mUY?Iv*)y>vgcL z3TcSJQ8T;Q`7!B#${yp&S<8zQ3p*{e_$Wp|vl0-$saLr8l35EA8%7q};%u|o0vngj zL&&9O7x@UQX7_TUc``P{J>3BH%90;t%n; zC2Tt(5i^iv+etGkWCv)ZI5FQuOf+vNn!Y3;v^Zk{dy#U@xI~7okV5f9v%7q}9T8m# zK;!lBXKM_qhz3hMT(L|qUxu4fo1Wt1%Yau)Xz9^6uor1&O+@`X6{=8XdYr0fcLm$f z{6NrbV)qw82-F)>o96S`7i73rcD4;4B5KwznVqjBqCb&z5@crGL^G51TRUZDhh#dL zt|q344P<&uM=Gg?=agRitXmLPBX!;Y-xFO*SM9cq-IL!5hES=xYk*kXES3d`jUk>% zrj@dY#?dY3!dGa4uC&ifWqjA4oPhIu#R*y;)|;vG>&`ajJDNfYlk|gVA^;moeX==#X7OrXA4$)(YBrn$8~y%J z_zS#dIm}2M&s_rA`Ywv6GPaO`r2`V~2qN4>TvYDolv;{f&LMe|N{;cef`U;E)Zid8 z`L5jg?A$uH4#xz4?|ZhxabjfQJ3A`McTS|LZmnNpvqRrHC6+jaUql686P;w#-Wai4 z0z$U|0;nkOTB+bG;dmV(inVmJkw8z@ITvGdgMg8QTMV(a5*^eHE!clGVv-A@yOpGYTy_gjzaaT!M2`trUfngED-eW^F zUWm{AT`rD$n^R|NY%&lifo9FL(Q~W(9p+f1Jv!gUwa;h61qC-a+-M~c(d}7IxBdxA zJt@59NPTR>(x$gzoXi%-@RJ##j1$-7`-grd)$xug!^6&|WHj+qtqGkM8q?}Z8}wi+ zf|IEcX)ZSsy3XwP7$n&ANl?)v`Gnb1*Q6w_USgkm>+xG*xsV+}0;itVhTHcexT%N% zLdqzq?~v3tH`q5>cr~TvobW-u_X0ejHDqqMvNhSYVzI1lB`QBpq(v`V<7N%l0Ie@x zZrl2H(`Tw-{Qq<*)EtN38`rV1AezxTyLcD27zVnt3^tNap3~^GZ3;1->NH3OuK5kP zacf?u{qF>>qCG2tfgNpsKTNt2vNhS+sb%ip;{ux-@~uudoo@Ai{YfWJ@4xhY2vq^& zG{CzI_hxkvo;OMAtTs8n#_vG5N==`g_(RZT_^k7d6k~*hUeK%x|KCj~%__n3&P|<& z7PAbyxs8=J2=BQo#usf^suQ+}UFBrwF*8>x%5_PAlnx@PJ$K-In!jMIt6BfDH#M3a1G8OPMxrTGjq!6=@8OT-Ci{Da1#p%Qp-mZ_cqFnT@3 z5_;;IEeZAGEvv`ahC;IG6KyTPZ#9K)Jx$7AMYbWG%$i7&3vb=|OYd$c0F~S8b(YX& zn|Sd}?W)(3W40FV8wTwVu9u&YcATOk5{i0&7WM-?%VcCG0971jJ zKG~+7-Io78U1N*tNxHJ2}y`hAi*i` zx!{UKCPWxIB`SNG6{Q&2vID&qF>D)gl`QLTdthQ`b+Rl~%Vn*1S(~fTOo|>FkVqR} z^youIj!-5l9d+bh2x6gv_rqeeo>X@}q^>Az1G%imJVeggYCIHHf1WADMfi<3-rObH zKUFXX3B#HOcdQVI^vB5tB+*^7+p|9}BP*f?c+K>ZobkKsP8;Pt{`BJJKCRO5KxwIc z-6dLu#$gVEYIIFF-n#EbZp3jDhnU|r&A}0m2?v2F*kh57J{fv0$dQ!F3@1h>aQEAH zBI@nK>iU@*{uuLN((8hrhkUN}rAO5o4csWbfIL;)9&=#7&9@`w?g?Xq!1K`0*7Op} zudl+e{S`~|TV0ZS-4AN8-<<~qM`LWMaV3&BJctQL!jGBBQ#xL-n1bsdv{&l@cpQgF zPvEhK9X%l)OuZN-U(dNkolT~{F{*B8Gb zTyI0ZBXBZ^SB1N5fqvimIn%J+I=8DX!tJ`oY>XVX{59fC@u$gfH@1mEW#Hq;Z#aX@ zw{y{yU98Dbzj4=r2aT#CSdzWn~|{?Ef1ULmaU z%`qX7qz1s@;}p_;c_mbL12`zY!zS6=e=4OtRIV=kM{a&rB7GS z^>V*(f54Iax0%Ec!sNxE>FoL_XL+&LxZjZG>Wu&GpxsrT^APYzN-#A_KextZEre+L z7q8TNq4&t9;(7BYGcy$LQcOTL5T(SHy50DS&Mr7_bT1z6+0Vwz-m$S9DI@rf(5kj2 z3h@)1`|k3k5^K(w-n}FOd60G|hZ~K%JTv0mT3fw6QDOPI18Uf$qU22H8Z!w)ey`b8 zzibtIE>TCkG#C(1QsA{VL!Nn z#p_?=G>(k(FCl#9iaMdv%tMhc?7OkhcorX;F(bF!%!LOz0IjolD?n|JHq?y6*Wc13 zH-6evW>S=n+`Gtkh|Mdril+se{w^``m4Jd;=P-86RyRwMDESrIrq-+6uwMnOFKG7J z_gqeS%15n6&sz*AlkkfK-7Kt)xd=DBs=c|gu=2e5U23Q+DX(g$nz&^|BKG(%sWPwN zlX>TEF=ZmJdf!h{>ht)JfJ0x!QZgSoEeWMH5k%{vz?SthjDU_j%ts>uZj^b3(v;_T zL_?Gu>VRJGx2?Sk!>%s&%K4pyijcU5ziA_54sl_<(eO*wR4BlMc` z&=#MPvDy6+&qaRKUJBUkTT!T6OYM=JT{gH!%J)ty7iZCHrspi4&AfDPm_+)iD8+3o zabSbT`n2p6Q*F`XU@HqgwOC$W1Vz+e??_*uoBd4={oYO-*qu6W z`lx1IOwLPre3XX~Sb$GO-3w>Wk+WTq|M?~*kj2w-iC$T4axgs-=l)1@}zNvlp@ z4|yRoDC(CR&+y=Mr&+y=IEZOCw={YvPNy_pY34Es59EQ2TCgJ35*r#?1~Nkb8ZztV zhLr@93`*u*v;Mc(dPWAfk-n5ugX&D}vzr^=TZS$kCzS8`B*48;fh>FYnB>TL*kR3x zSCh8d7;9ts442_s!WKrEoj1RQpi?^dE2T_n{l@1?alm8!aspOiUZn|jt)ZSE6Noy~ zV6c827Q**AZ{BcGTvEjbk%LI4gV%QY?x&qz%M#)!oM7KUVs&1u!u&Zk)K^;yOiKD2 z@8kyAHs#kvNPMCf(x26HN`E%{2b4dq5to&Rm8NB;SWZjaH!2dV^AYoAL-BH(ZW`LM zfo^eE0`;AUsfyFKjI)=(o6J96+&R5RFPJ*wS4gbN#Oj%yk6-63Ce+Ozo3p4Zi!_Ym zSJ!Qtp9(sY6xhlyS=-7meCCT7CrO+{XvOLBj&=?gK;uIW{9V2N8 z?oEQO={|rIOOl_rF_ku?+LuWgQKX1N21V94hf*@j8~<@?H>}XAj+S|E`ijK9KUfue zA}jIHJte+Pw6{s#D$1A#Oh17y&xF_@vp46W;m^4DOae+ySqi1zxR-b_P85c?v z^D$w}7Hl!`DTdQqy%sAo{YwSb|2?QnFXEStcEF7LL?Rl&#%ji))Ga)xDnFiK@KN-U zGC--wM5nCiw5ed8%*!b2|H*7Z2qS@aCy!)OdQ84Jgkb5>RRvvwRwZYb>s6|m&Epo! zz9OLh05vaxE5z4Q#H$4S%1io zMDTKB{2RjL7hi{&sDpOp{iL7Ge#c-N{&MkXZgj0Rm8o>+R8bRD_+6~tG(q>%MjSUL z@UcpF&BAln z^j03L<6Z#rKO{kNC75Sg%m9}4dC4AZgL$Juy0IV!So{#ldKs5J%1f0=Fnw4^ksAF1 zi%vGuc`8BYVyOpXP~Wj8c1m~%3-gABJm)J^*3hGRTw4@o13kKrYG`9T%p8U0y@*yw zASDvas1%ZzA@;MQ7f3hIRLE~Nm^v1Q%|v}8?0ZDfqSo?WQuIEukYCl9HiEeVtJg-* zd&WWrOQDx)%Kl8!{Z7_f#r7ZyxiDt}QN#5ajwd`V%zZeL`~7Cl%J=&@GwZTDR60K{ zV0$Gw$9uvrN|3g7$X*uqkDrH7wTE7pW-E>(pY5qKo!aIL-cFc=#Id)pJj36Qwe9ZL zy)HrbQ4wovU^*XA{VZf{4>F47Rwr?*twD|aKh!M$C>2tqLe}>nMFj0;1WGK2pTMH~ zzrzo!kX2t1mw9@BP9d)^A=2_z7gJFqHCpP~&RQM{um!0F=;0mFT~z3Ht;U6-llgW1 zXMuL~-S;n{9uW2%?m-sxWcs(FZ_=SC`{XPd| z(BTCvWan{|7T?#_18rkz{|VGR391cXxt-Ox5Peym;5c`eHgbzBXdHA2_&pe0Kv zMGtB}0n)VB%Nc5I*n8lv=E#rPBftBbV4lFZ9$28H(u)htSLy1LO$4&_T|K zlRkf+{pb5LQ29~TocRym$ooGICxB|U61G1q*uJIaaFTY){KUCjH>t0KY|b;H@r?N% zj_I?whAH?afiW@Dghy-nAcU;2)zD}xBuovtJImH2pO39E`e%-9U1&tdw#HITj32xdhW@LNXF5(uzF|75RgA{Mfi0@=c1d};=-t%2TXHtQN}`UlKt zhH%5Jag*&b9otW{+e~V`29*c?=1b0g#2WJSNcmaBDfrnWXz}hdMoB(THJ#{LUo;)Q zpW1j)ihMDWqv^=`Q;Af*J*VRVF~`EHC@pR(5WE-YroM#CL$=lA!e}|2J=HUNCEYK` z&S}=U$nBe_^j9KP`md1&5h`dO7JV^P-!uX8BEk0KZ`{X$7T53=;zg58TFW{b)U~ZU z71&A*zp`$YE$h|);IY{{&Lo<9p7#4nb4zz30LDz#Qf~EYfxRIrXo_ds6@JqP8(cWc zHjRjj<=SpjLw8DHv}5}2{0nV-XhMxy#tTT%er>4)>c;QR9q=-o_1Ds3n02<**C1}a z(06<6#1&R(yzLeOBy_93Hnq>b2yuvjM4xUis_0%1T8HCrk%#GD%D=oj_VV^DtRC0f zxd;5f0e(&efA+vHg9m*4&vjHDqKmg~rxbpy2CDUf@ujeHJqYs*cqM>9OM+eC!qK(B zm19_h8Rh>dx)*<@zdsJ(+w97)x!(=L$YtbyYn$6#5)u+Mm*kd9sobjFU83faB;+1K zNJXe^?j^ZZl*&Do%B|Av>$l%Ou*V*I?0i1wywCgnd?A<|p^q7`N&;M^yb)0WG2jZp zXb=t_eqv7`3J+QaaiDPux3p95?KEh*Q^hMbgEyn@ z5Cb$H?72hcLMkS@uAn)NQ7DW;<&I~I3)N8dem7|yJe&*mj<4c+$?$@~yw6~<+dbB) zntp0EvLf02*A+n93SR=inzTDubXy{R|4|OQS>PFFdQCeMKTak9b<&GJa}96ZzdO!M6ThD8JUq zC(+yk0NV*SnJ}X}`zb92d9tpDtCg(vsK7`2;;U{CT+(m_`_4Ci{saNJyEmntPzB!F z^AxfvQh&mg=e7<$OhwaM+xsJ>Z2w$3RH5k~Z%Cm+qID(Oh$F!qXn&xhtS)ags#P#Q zdVXV|0Mcc8u&o-szZE<%trsiZ9qC00OkS&pk-Gg)E0nk1;9nd%kFX@h7{;K zxUf1~I6Yp7Lmibqb@83!;mf_1>1n7vSqgZ(d>y)V>4u#;4g51N-#o>zVI2-`@nw+y1VV;B+qJ{p7D}dSyQV*hdH5}8=y6w3LIJI zP!3UuLI-%F+_)_V@D*~cXCqr6kqwaJ!U~5yN8G6pFJlS&IdI{YliO#BIcYGRreKkx z=~#aE=sNT$3YxeMjVy;I0-zMCc*=L7|58;C6Y8u`udG@mB#ks3QLpw86;;owI$ToW zXx?)4EZOCmt2*z|+{b->K5SHy1W1XmM?s?#gzDc)9c|eoRtmQHqd>|6e;R*9#=KFaG_o1M+zn`kCfIk{l8?29Oez~Ll43g@p?WoWl6UWo9Cc4z4SmP zl@@npvg{#o>riO6IEDrZ;wU{zP_?}T>Ao&a9vCuyyq(&NOV~?&<`i#2@QymX z%Q>XD8vOcjK41|E0CE9H%;Nbap+Le+0|#owy+CaNDsqJs@rc|pgdi?hbq>_tchjm& z)urK*Q|qN9;=rXeDGbj$Ene&*3T(Izf|V+$ofb8x2({-_*^JA$1BL~MM_qh1c@7kW z5#>>2ZiiGk6Tz?kNfA9!1?u2un@~eaRX^y1#uG^cLZeyyTN|qnX2o{+4w%3xrFiI6 zyp=rhRR|9dT6hBqWfJl@b^AQ+&GE0SbI@j#u_|`sH8FAvb3~T9rd{5JEcZLiqCffX|ALEYN$(o^7-`r7JJHNCG4#rX>NOoibB_ zE>{qQ0^LDPp8?0?q0XLgmv@L7Jluu?jR!36s-Sx#(Y_w{u67Uk8EKv;LSAw0+Sf&% z;YDXAWFzsSVh0%);)P=I$XOp@7qk*9-cg|8p4Nnj=8FxZkbKH!-#X0M8ChJ3loVo^ z2l%PmBjW_`X@EViT&IVEeEwFXaa!ab6C6HP5Qjl_5g_>}Pe*gnP6C8QLC#L?`DQBm z43i~L<@+(>3v)gKruGQ|Zp9IKYL5&Yt92X`?kz{|p7uu~{e+c{I<_4Ye(ovygnDg( zApRVK2y=!1`HuX6M;-wn{=7iuwcL2(`SZWpo#zx}F94cX?%2mMI5{Dpa5`vk0&$td_^%En)(Mn9{l0j9c+H%__L_G_J~ z_m7+atI&zIU*1moa;7I9H8+X)HWintx}D1DWjB`IKlY|zS>o23?2M*EAvoXf%Zzmb z|7Gyjz01XIXP&nt^sz-H?w{-byDGOGqihtl#dLYi{vvz7<<))B$(2Um#o&dPzlvXu zT)Q9qXrSkGQ&WKFoo|0BGplP#P0{cE{_U9mP;ytj^X0s^uiuTf53f(@G%q)9SRKEl ziO}KOy5xSH>z8%0zVKIEjWo8)ef92N>dwaU^ION>|9AfHuVonAlX?-1B83G3Gg1n=Yk4JI~!PB8tVE9?@Zm-+oGcc7Inj?GA-4fRbpiE64_Yed# z%imEYprI3*Eg+z@k?M7Iu2)Rhom$u89vQI}&ZQwf<^(alp4=d3!9n6t8THGG!6OmN zrM^teKtp1!*QXNm*CJY_&UHmvH*VJ^tSHA_E?yEh>n+bK#xpzaUQO;svlXIdkyX+s znxW8SH0-JlfjESETEjLnPl6ceIshmJb-nt}nG53}fsbw;>=v*C7OVmnu zQ_CnMf=9o83Nx%O;?gG{E<#Qekac0jx3!8&@9#L4M0SrKM+CPdL+gzgk=n9`#mVX7 zx^yoSWzxg)0gdDWwG~;dj?*N$li1w^cCkqr`h)z*Iz_E}r|7LtFM?afG)sMBhe9&9 zpM1HDSii7(*?J55-+cP1nLq2n85#rH0Dv52j)rhWd4^oJ{xNOurH<$%ditXsr9AaA z`|^+oaLpqa@z+vlCJz_lAlZMUGE?2(VWC#D2!~d489iK69O)22DmuLb2O%Tt5Om>V zBM2wO#OjZ1g<~az5b^xtai<$(-!z6ouv36$hReI_%#5&Un-dCz`sC35pxOB1T*7$R}9?H5~6H^?1@-o(m#U5PpC)hwmVA)YOb zZoE5?WAKpkL)_{mG5k`vJU-+&kF+cAh(m84b;7IVx-jJ2P@eoDY-dL;0o!lb^WS!^ErdR_{d?9Jt}YR zQsTNAVrJ)!`L0$g%nYHSNQB$JS8s*go~?#c`<@uC80EMKMu}~v2237}XS;2OC`rt) zM8mc+SZI=8hrhE<8wUM!i7x(P%t@gezUb*uWrw9$kSjXT!GjKzqvdMHF9(+} zP$lC|dy6aB$GSCA96Nw=4I46~>I%!RBzH>=dVg%o%F*5-KG`)vs20m{a3>t-)q<%kr7@H+JJ)W@!2TzuH#FbqY5GAa!JpGeo4r_cmS>{6f*DVI6aU_F!sv2jFjI8AZqRzf%h7PDQhBPy z6??yEC^5`lWosQGQCpaOu3AI622NLP*tndZR)zK(W?P?~2Ww-A0T;?i?}fdpqpL$@ zhl`vfN-&7l3yVrk@uBE-yr{v85S7Q=Dr36mrL${%*|>tAi*0a2=T( z5vo!X0qlkobIv4HWd#Emik{L4o4#JTM7aU0h=j|h>sDm@IL?+(dsx7bkYd>^$>3e< z<#Q2EvZ3Px)=OxmQ@<9JYsP%7UMGmTmPkrm4g*!Kcly*CTNu*QYQm-z`~B9z2Sx3- z5$Y2&NuYqX}VvdTkr{jb|(@Uq_R$c*tJ*6T!7ol5hj?~ zQcm0hxLlZ_`(J7IxX(IWvD*o36Z2dAsDN}m>y_oos6b`? z5K2n&Wu{E3=|@J~Q}NiJ;mv7~%EMVG7mh*pCRT89r0M-)JB!=ZgHI7Me9=>X6LtdK z34-}5gfHM!!Q=;MY&=l>-Cj+LI)boP2LlJ+X5rhpNO=lV*=!wRwoPDJc>IE#iBc)M zy83<&f%A;hs=@3K9DG*k85!OE9M9mP{ks&BL^gp{uf~feM$r9ADPXLUC4xw(+n+H9 zHf$W?nW0mwdIG@ORPAp`KcX19gmaHH=D&o*;W>+gOlxbJh)+gaMn+5j4PYX|)t@h# z*urd&p#>iqBFRp0AU4N-jrsuWWw`{HVS61SFo`c-#s%xiDG5LjI_Yi;OFu$Q#DC02 zlKB!s|9xS|BZ7od$rb%6>L70al`oS?1?$0h6&_r&WC>N{y_?d`Trf$pkSflq)zo(w z<1bFOf%X3q4B?ODS58t?=pIUlpkuiMifNKWyCQ2xXG=KQQwT5%{7;elg8)*YGJbLy zE;yzvoFU6)T$vER#@J=b0EDC|Ec#vP|X4?oKrZYkWz=;{DeY=cKugpCCZ7(;`QB{^M2@HQ~U z5^F{`^CZ*{(%bJoOtX-4aHdGB0>sN6CM@!C4a+C5z#Zah(0l5chE z`i7(LSn|)sfF3XOt7h-%v>!6OJ*b*yH*mzgGZo+33wwOm?s>Meq2P1`ObRC;<(^1;31^d+bUl9O9Ej+m7G2nfrK-FCUv zHL4hoQ|zEQ;}y#4??kRQ+}dFI4mrbm2OWDdwdQs&`;$(dx91=6^xT+cfe80!kP!4*c91>Ifx}27Ky5Cww1f=K>xllnD zsj>0zv(>i9Pk!1ZzvPae?o$qcB;X*=Q|SS?w?~Znv~Q`ye8GAb}r!Y;_DBe z#qhYS!9?C&E0|RW86C+RRD8EE>D+yW*YJj6KYU4bsNe*bu7|lYYK8tv76;waXh3bapmG0d5K)lS|Oq17O=Q@cAy0;c?Vj^xpi&`+L*xu*mhhQQW4}Nl>eDk#il;0L(oDlyvec^o3_m5QgqeE{MZH>bHu62n~sxbmw?b zh+O)aF}CxLS`Hm$h&cd4l075}>u;e*~D7kbl4zg+i zJ4OYcw1C0FZ^T%n$H}oW?%uLFb`<4=M3m&twnlpxt6Ylg5?jE=66vO zVW=)KJy8z2PY&v>&Q>dQ1n@bOUNd5G*}rFc}scpAktYID~@UCbGZhK~15% zpM!WdE@RLbOit7gQ+Rou*H+w$i8!+&GMi@#WXxrc%J1Il`i|Hc4*MjU@UaK($v z3UFctc&zoQuz2jcn)451vuAut$54<0^(4@xpi5q3BIXg$D8SxQU*Uh>h4nXe_YJWd z3$vozO1yD&-;85%bu9gkCX4?V>P3xbj-bPa8p6tD`alJZq-)w$RZ~O|DIOY)VV){V zZ@(|SedALfOZ1F#dh`zTbUE9gkg3=Lj2!L~UxD$GR?@?-@&og}1P z#C}xA7LEGHY`nq%zGCP()-!}%091^xcv7_Q)C*q4? zf0`dhN@DVuveWa%FJlAbU`xG1>UFGhIHtxCbQv3Vp+Yw@ej_{{f#(W2Jcpjx5o})A z(L?n8FWHaQ8E>txMtaT$7X^NhhqaX7@Lt!A)B(%Ll?$uvre_MsQ24E$V5LY7EO?zB zEeDg8m9(XN#u~8fUqNongIcE{r{$n>J3@aa1UxOxJARL1lKOFSdUV}(WD?jVK66u; zu^^IVj;gwrl>XpJS3m?M&Oae+ogT!EJ(&@5oJza*0T!9gaGZN_W>C2r=v$)IT~4x^G%)A9XM;fnlT$)hz?ttnX$7??7#m7^GbGnK4KZ`KkvQsn=5D z9dBz}Uj2rXGWd%fTu#4j02ch~7Nem1irC3R&FA77KyON$Gkv(>PGULa#18vV#~$xf zYy%wf7%n?79`yV*1g52V{1xKF4&=;9*a;kSZ*p2_ImoX0t8>xT<+DI_ic;`2Bd)SC zh@*y0O4Hk-$F=1cc!mc@Kraj<2)XRh0f|Tq`{d)2{WsEsF?6#U#yO}1%I6={k*J6Myn^)9(ptR^sTO9 z*F;aMw%+tTl4C&4jp~u0uCpJ1V2LEwN@wDf@ULO5Z*Ly>_O);D^7kv0o!NXh+-nBE z-kr3`whf-p6xC1u{906`Eh?}+P=6V*MF%5tgAcwPzESWw(7NGEL9)h@=5)utw28>x z(=Bfwcy|Rfr`Fz@vP$SGtln7OpNCEO`c8jqdE}M2TGvv|X=mg}Nb=@|tCc@S-*$R? z6n7sgY))*keSg-7xOt+ah2s(NF!o$*fP(qVXwP2f{jZ%059D;B+OitDlY6sF)F%2~ zSw}w`XvG$;1f#Va^Qs3ws2t0~e!31%-t~W+!TI_7=hnCP`?z2*m_q>k2k|Vwh|ul8 zFhs4UF#x!og&IpbV2FaD;oHY19p{+Y6?^<-3vo7Bcj0{-F7n8aqV z7D_^AbXrk@D3{Ab`*_mQu}Li-n35#yJXo3xAIB0?Oe$fL9!ag$SIHJ$WE~3J#Zl75 zbKXHCw|kx?73|(7EPb@Y#Zfkvs@5;VA?HD4jtKf44N}5#S!I;!RtMQwG?6J8B-q&L zBw=_E*|%rr2X!e_ppI%@hfbzq?7}=$SwMS26)AJ$PjmzE0y#4m1sd>69OLy%?4&J& zCF2S<;WBhPnxo%^f?fV#ip$z~mIBElm#NC4svSeQbWp)6`OJARnjEuHSr}xa0G9P> z`JJxn6R(!NH>M-uvXP6_CR{q)WiI*&^1e)cR(jLl3=l{OOTa zCAxI@-^~1Xm)}q(X8Rnw{EyhSw)^o7t?~jJ+wW7iD%+mv zO|DjcaMM#8`)N8lo%{jRX&#y)vNs`_|L=RZ-g3pIuiHft59Pm4{?f2qDGHKmYAW+t zHf=h8GP*(H#zNG!E|tzEII$&2vIxv`ps68~y2{_rQoC->SH(rH@>As=bQolRM8rpx zj4w#qjDK-wUClqdVxrCY>c~knw)d7hH7~MH4tUl-Yi`9vcJJu8mecFISI&Q$wENkD zLy$TSoR503+X_KZ0bpZKK_hjK{;AeWNB@S%y7>0w<$#di+pa(3psLEQi#qoRN@Xgf zPPOSoF{%v5RnW2w(EYE0aM@&sd*nzRWa-1xopX4Bk=! zY1D8rCMetZ)1;08hX?R?`#tFVBkz|Ap!k_*wvqV21(}}~yU5`4E_KVli#(rVZHO)& ziFvCgetl(n#Iwe?{YC)3OamLDuphrlI90+b@|4HfYLU{X@8}MH^>Tx44_`@HUu*?O zco6}0>T;9yqi5Rgmi|RoZ4X%Vd>+qmib~7)F$LF{*w8B}YeY-7t#iI3c!iu$(8ZSz z8sWPtjpnZ2c^DR!q)uW5UY* z2^72MxRgr+y@2fa$&^rk7C~8jEmM2eyU>bbtrrvtW-YO1MzkWOx65I>S^@Z?{k!|D z|IMXRi!o2WBa1iX`3CYbz14j3&6k?W+W9)<+m>f5FR@C@<((dk4`*jCr9M3$67IJB zvd*qg^2FBb*DnEZYV(gC!wXwos>z$z%4Regk49>6N?B*l2gwbXL$3}rZ@b$;mXGIU zU=UHb(8I5*mC*{hIXNA@%99aH$@C{+9V}6F6xVO~t`$~q`Sncp(_&el+YqB@6dP}UipvF;9Uk^hZ1FjuQ|%VG;(hIK65rtCmh&^w zzEn5wDC68IcO5<7R?fi*zMPE1%!Y49L1-|8k&KtEKY{2wt#V{J74UDf^ZA8ck6So} zrDI>)GsYgKdMUC-+OX{*aXAsX3UDcy-< zN$J)>N9};fIJmvq{f$dUt9uUzWA-j_`n=Kzt|CVSKq4^(N`Cb6o~7MK*Y|B^9ge7i ztka6mg^%lCMSc!loSe%mMb+t#34Bw4J$%sVaR0AC#CVgYNoxI5DC26{-#qc)lX4EP zjWVwv>d`fa8dEwP^NV=jdKp`bn0mceHQL4U^qCRjttSGPo~9$MSr8kROM(qgUG6A zM#agP9Df%htlIVV%T><{;)jmtpVR417v_ottS@LYo0as6j6{OsLi&8v*od?7#gnlc zO)tidi@9xGa1*|!^m6$7+E-rn^YwCE8*A|a*mRmFR*CtovT80-S;y2=M`!xDi%VDG zu3ucx$@0hsN!=>qYDut+bl#_Smwl@N>6U|V{v@s4TF?`ljL)U~weMU+AN^tmXmh?7 z7skV^rsIl^{`j7b^L&4S3es&M4cF1P-G}rhj%trAY*`Lz{60ENQDqFNe5ZnNb@Z(o zlC}*=2IX< zF%T^+sD_*1}t>LaDgBVEL?+1!ma}~5LO$CE?a?qrNV=*xMMcJ>N0kf+>{Zeo?88Kz~&@xoG80<%aM_=;X^hE@bUl>&|M zfdA-5bD1%ux2j{{U(ky>|D7{*Qoq z_6qXe#Ft8pI*1>3!90U@A%QY#Y0?Y$4Md|z|2IfGdR3%=fZ9xsk|m71B(v*XTWcmq?WBe%*9W?NMZf=chbb zyWG<{$4x9VMXpvD2G2-0sEK>h&?fol*&vf=^Y$}p{MiZxgu`hwIg;%yKlK->LDTki z%L8dgSHBQM1wB7=elp!5*fjvcohytd=?2oE4g_(j52^kH@d9@-A1-(nC|g3ra=6SP zqMjh{4p)mTpkIvVDUXn3;Cuy~;K)6wTLggnP%k!eby@%r2d;7&UD_QWNS4v{r$eZK z(p|d@Fq1323c_#%2WK90h%4OTtVyOb$_a>as@&oCV(tLM98uE;04oP*%o5>DfD~NY zZkpQQO;*66{l&x|Po@8^zV5#u^q)?zxdlx=t=GhpW<1nu>IqcMX6)VJnN;`UO&AuQ z)j|>it+iM6FDWLl+{}|Fp zRuiN+G>5z(zC-b5Ro1wAtjddm*{?kg&R4t7=ahIBO2xm39S>^wj+AkilJ*@d!Afx8 z$J1`*sDoLh@5c&va5Jr@0)VC|(i*^jcm1q|d>6$xH7nAtv-2iMk31q@UtdjJy z0oZJiMg*wd4sK8aZ27^J9wDt*4GC`p_fV*+-8^CN;!p-(BZByPvo;k;muMu219;jI ze4PlISjXVkU+D&GvAPTPa#B(%1F#Fz^5)-jHy?@Ht?`Zj()S`KOyEKiHYC#}5-N#? zkDoJ*=*8QBOf&4kU-{x>kXa+ce1|TpWH30aJ`f9Z-ATHVXQ6lO{PTmQHCIeM?&|o=L^j5223P2bAdchg zs*BS+Ws2vXVSra;+G5t%yydl)y;TnVJ5$lA@2l{&&NHtl`=#E&C&4R?>7@vVl%?Re zVPdDuke8$iN@;yhZB7m(y%Q?{6iNVuu-q3H=(>S?>28=zH4w2)mDmQT6@X@r(muWB z>Gbi1=4fz1aF|_&(gl}-Z5b_2dazg<|RN*7hie9pF5NH;uCm!gF9U+De4{#3kL936HCr6e3^LQ|>c ziLT{{#?Vx$0#A`EK(J;10_CV+x9>xuV466Xh-z`|M9~y2ctS=l#$5pIR%cYlmXa22 zmLMV1@^Z?kG1^k^<-P2by4;_A?x^kfE8mMClcy!LD%@Fx_lA97ducb4XuGpHyVw!w z0M`bumk0ilO)RhB%|Dqtq>2Hq%>L%p`^{YA=e&LB*~D z;Hzz`DRl860CtrihI*eGRx4dX5<${ZyEjDYr0v2s)Zz4tef)(BJPebd+Q`KW@idOm zq$5b$``{XD1Q7yVdYh_+++=J6H0tQ!Robi9Jf;arfJVwQKL&nwMg`J??ohSJL}+=L z(T{Z1RjvZqP);uL-3!qx@RM`rFDEaj%Xvs@6J7S6zVtH;$W3}~pENjzMJF^hO$29RX1lzdThry;legS zz+hm=bd^S+o;z9aB!|V*3@AM5_14rmf=m)guZ{@qNLTIy$w)O^XygjwPSkFnfjXSU z1`^@!z~vM^c!YqQP7*@`u}b_S>{vz#QScrWLy~j?i3pz`$B)s~N4e^&u$F!TviiG9 z7#9)9$0Z8h0t8__zTPTq4t3X}?QBK#Rif?Nwiw!^-m{{T>S)8!efB|*;}2ErJ{?VE zdYo+?94lz&2srkBWXbg2=#$S9&0Dgm;%2Ype|MAbqiFFz@lU66X4ice{c~zO)$$H= zWaBU27#n?SQnY{v$^=&I{b$sR)U1Swdw0fTdN9v7C79Qb>^<}Hki2N7j&+yqvJy!D z7Hlh}PP}{n!9|3AxAN`M3R&{sZRW8duoSo7=OoaiWa}!@;8Ep8&nn$=I*z>fLkF(A zs;(PyOs|&yDXSH`@@IQB-9So1i$0?44-|I?eXbzsncOs?gMRcJ{YDi3@jZ3hQk9im zbwMv~NiUMYeV*6B`I9CccU2=F^)5LQfi}HrF0EITjm4gsk}0|J6IFuReWtomZT&ZL zlF9Esc)0Ehc{@paTW9|ft9=fFtxa=_qWZnyg*nFc%$bMh+eI8l^_TNH{}rUR&tsZ` zuQXR&q#RQJxAx?@*J*=yAL_=lCF=EoQ|Es=DbK?uerB8x9@z-AV5`DEe7`=>HMk|O zx*lm`^r(zcko~m>W(qajA%?GgPuJ94)SFNr9HA<^lQhbYnPlDkw$b(3^yVky=bwYZ zHK$TD^;{V9t^s!N-R_>Z_tNejt}B}UtuC6=EuXKfF8XWiZUZjnx&6fNm}eu~a{RZ> zD&^E`4o(_GZnuBCK&fqfoKVS#tLIe>+#6Y)QP_8a#KhBCn%PfA`s%i@BH` zl7=``^ZI9cdsWQ4!~gz$F8!8va03L+$GYWViFpnf2`w^QOQ+3&j8-FOc@|`dhdSjb zTAtnBL5Dd@VM&E1^=OEQ5D+9~k-He?etoZfrG%!g#Hf#oBkkeL!Ku1D+wj>pK+JlJ}vkId5#xF7TNZ}QVT&5CO# ztzArYM>W(na$-=mdhXMDmVh_Qy_B!jRqC0r(4Q$~=-*8>ik%fL+GV+PlPzaiS#eC4 zGJNu8pT-&5x73%@By_x!A`&;5507muJg^V5z|jJbaWvj_a)$;=0sseyo2k!oc%^t(P3?w>-!Ap8t`DE=@hTwo zYk64_mkT_6yq2ZJEuVbUQtWBdxq2t;VWy6c4;s>N*$3QTk4gTNX@7sg`)NaqxNl?JQ(L*4Ej%_m8`8pa(tw*zQzRGC)75qPy;C@_nW~qW>+I3V z%ylIoz#50VgPFNa1G-Ph_Z^5e`B&6Cv7FlrR$$c{y;ff>@7^wtJDl~09$D`;L-Y*) zTM^2BJgm2O74w|1s5h!vB&W}vk;;lt7kr^1gBjhdp=R*ozNeQeuLx=6n9Rm&Ak9l~ zm6uimlTB-BC(o*kruJ$Mr2no2XuLYj5}VLNx-O7>cn8|p;O;K8r{z?$f59E;M`KZO|0qd5s0hfa1g2BoLYBf25 z%aUiydzGE$n-dO$P*h5YPQdX5SF} zI<->rmW87xC^(atSr zqUVA0V&_&J^e8kireFbW?n7y=6H>k0{lV=(JSc(W`=o-X<$pr>YS8qQmU)7jNqpY* zi0~h`pRS%Xt=FOd3)jG@@93R?B_EJ|8QN}Q^32Okgdtn($$U>%OYs=Wxf6RxMXUOm zb#?I-Y5?nYH?l%RLFZs-Qhd$jM$uj7Nqh1eI~M8+o_QQh`w;0rplr$;2w$06vYVlU zoH$I;w#5_qR^#}A@8vMe@UZZyIiMM!68juP6eq{Cyk}|PJ#{!-qbEaQ2#AuOqdI${ zGF(0PXk1w*h>#8Pw!XGG% zr@#bpsTzoWXZPc$KvG3q4cZ(Cx{e0va~Gsy@I-8u$=H{I{RJhbo?p99Wqm@Qjtcnw zII4&>i=6c@ z#GEg7(D%-LA`S4Q!NF93G(L6?`g%4;574PVY=#IEr~n&5cwx^xvI+n{kVb+W80*)V zBEe;bf@NMGiW526!tzY;WI-*j*4J@z2bPXUi1d++kxSx$eGbA)p@yl z;1rM~oUjW?;RuQ7QF=y2m4GHBD#)D*5b*~9+Z0zG_k+Z(>uAz;;~+I~_N9PTp*^#+ zAEIU`)@c)8LQ~d?euY#Uz0MZ5o3PrGov}IoDtBA5`C4x0JDIB|em`%rVz8j84|$An zI`f1t|7Bpfb;ZPux}JaU*m#QD6xpbg-QPRMpemtpy_t;o})JZC{0aU zb{(#Y*L zZC7jw;5;^8fL;l+f)??(9}$Pi@pUdI4;$bs5MDCR2v~Yddbg6_ZfDJJ`1krN%GyZn z`)A}z@VRpssD<`a(JL-BbW~!6S33N>ZtD6yKjnes(aSCuFwxaK(?i-F+4L~KET`=A`KEgS9=tx?Gwp$cra}2(>;g_A-qn|nX$Ik`Ed}aoiR9~N= zygCb3_e}`i zM}BguL1l+9yi_ynck1-C?!w2c1=rBA5B)!wNU~2jEq4wO@LO?bD`sbdwd-b1*_@{<(CcBzghv-Ch9^5!jX^3-}b-O zT?%K5=tUQLdU$)szV?hTU3I|hVB=y|df~kjKDVu!dnZp_ZWCKIU@k_R?Kk>Or^yYK z+K8G&REA1<79jCjW#y$7LxbI+=1{u7?kF~`Nln0$>(T+~q>knVq&fvFfdLZA!DImV zzqsTHWap>muYK_bSfaiU&9PlQ$e0u|h7z4Dw*z2L;_M%-MGwWh?=p-tK)WR0zVHcy zIL7^9A8z#>I}MTh1V8eAle1NYp3NRmNZHrWHxHd3pxl5$44YGq?OcG!Uu`5_{iWDq zar5vim56EK&;l8wl=}eq<146*mPfVCrG;V-C~lv_LM);5&uK9Te(c)R0+I} zEN9_e=m2!>6&5~tbYm|$VK6MSuxM_{rp3hDBfM@j);AP?!3iQM#+}WiHQKkgR23DK zTeeUiQ(}H&$nc}_uUY`AgesAO%lZ?oVFQ`_!WIsyWbFQK)Y|uB(V#>RQ81ZTLTLG)?qrg#s+M;pA>&Kq~sLwuq1*RBl`G*A;Rcd>y$(X6nWq5xR%<&#``}Et* z@V|vk8$`^?XCH$rdp>GRHjE-FBSn4-tDb9|!p0Q3+IuWi`&-p{ggd?Y9cq@=@5jcZ zsIObOOSmK9s(h+fh`yTT5Uo4(RrAOF{LL9X@%>NG*x2m$4u%g!f9L+?#WaX)S5x3u z0HjhtKvBU3&<4geH7A8i}(RN*L06x@*nDx?LUB*v;R!@|1w)o1`o^udNIhtT7Vz>_lk3 z#7*6=HMG3AbVOIJJcR-xV}xVUMKFDxBq<;J0!@P@QsB_TCoKR8%vbh7gupWHTNYkD zE+scFoWd0|nHLJ6ierJQGDM)0z>@@Oq)>%DD^6$mbWKwi?>p1Ags=(>%LBEC9hEQi86ZsmX*o`QNEGbVhUDy=o}(mAu{FpnAqKm_J$^PKR~ z9P((vO$TWJf8=5L#x8Iwd&Gnvy1WiNUS+516Bo)S3OlAu;furvxq12*U$vWHcTH88F79gL8>%x|8?gtkx#2SHlSzHJb~y zCs8a{@YE3C!5h3bs+j^xnG-96qskjP)!Ot|o$qXhj%6;e<&5kRnv**(F5MM^6YWlWJ0?Rd`!LD~8IglSNh&kp%GuZE9BBh8qP55HN9L&DVxSWK-`u%B6jAnr&ZTn$@S@`8Z|)Ssz&9KR|!84pg6O~Wb{Sm1ALkCc$2yCV2GbWT6Bm$+87HyFKoMvh zPC&>WRj$}AuV@54g9;S9m(KGDuDRyO!~ROz%|~NsNA$5bhyRPlJk{~2a{cyzM{@?< z@AS`HV&>h0GvZX|omTItp8vzeos4G&zPVj&U;0jm^y0qi)3*-rxR&LE>G~wq`p(&Cd1dEOF{XdIt*HR=x$>D14DX0@ba zKKVJV*mfx5dSv?8LLW=A>#fhk)@|kbfDz>UrJeiUD<);%$Ua{|8!*hqyAjV`9@}eS zV6$)+$g_$|L+Z&YE&Ht8a|10~hw&S~h_Ml@^^%|$V61YKs&=kV!DQ7O3*lRygH^H) zggJNnM+?-inMsLd?5&Y9xE!p@WD#HULD13ZgZ1+T$*AKbJq%10$QH2n^xJBbO2AXS zwJ(e-AWZ-yGxEfw4I4GV{Pg~f-t_-)DymH1^jmonbz;GxKjoRPC9)dt<8AM&>_i6B z-nRmygLUum9L4kw@YwvI>$m)9yZ;O;ae!+%)4`4l$NgypoqDQMwS<1B zj(!)Bwd{<3T}poiHneq-RWzeNRW6|MA#3Wun>r@evP9j~kdBcuCw|-8Ps&D`{?ML! zO>X?rDJna0;;Idpwd)PV#@<@T%p@pV5KE4qBPxlQXB*;_ZEG5SWZKuXNP9R0DW+iN7WN020RBl1>GzK3$Md0uzzo{~2(~V@4&E&0k?g5Cg>#U1Y*8&D{hbuz-c~rFp|8PW19e!=+u3l?{=VuS?56 zZY~`yt^77*F!e11PZ8+<0PI)*w>Kc77(^zNzy_|^NM(xEKY8`R^JopD_8TtVQ0nfN-AXRF;D)%db`MN0(8)aJVJX$>-y8V0X_V4K`p{}#XRcC+q zt_aUm{+ST}_u}^P>yJF05F0&(Oa})@PoS-bVBHphrx7+Ri3ncu5QC{ z(ny}Tu)($U>1n7%j+kM|;(IlCo}^`wZd$#1+IX?L!h$VX<-uh6nH$G{;F|SQRi<&( zI8-8#2mqAm&!*MMB=VmHu0M+ZB}0azcqQH1se|dFj;&?D!C?IKnqcPuNmVT!T^r8MdQ$u@APz+&={%H(2fGIW)rVz(x0wA>F?TJi9j>=P zip&I-p3YpeXPBA!!ua!w^kx|Ssj`J$T()Xm*+F(X45G7u?tp;aVz`xZw`ABNy4zjP zs`{m5e!G>GV4jEm(R{XjRB^{Zet}Rd+gWpxmxDP&3+j-iQJ$2t~~{;f`7)@z(eKV+m^)>m>2nSnlymTgSV;m3tWf zG8n5FZv8PsCAQwlRe8nBD+U0HF?GsCTzj12Jzuuy?7#Kr=%{fn1*N+WH?%j zq&gsqsQuUmH7dypZx6%+T+ObqxyaPVVAAKxGrtc@P?ILEF7%PBLhSfH3*^`-KYqJr zn#6{-)0k<##HKKGDr-`9UyQWSTG}$Ei%iFS%-~6JTWs%UFLk!xU^DQ-<8s6T@SjXX zU67v`!A8gZjC_Cgjm_%BxNQ^OfTQiU$}jDE+v#88yPMzKir?&hXs^Hh?$OI}8 zA$^nEh*`Tu6M#^~ziT<87G?mNOC%66eEnT?RqKA{kW?~jhLFf7b|?0gszfwoWOZkF zc3`^duGhxe;ulSeR!Q>Yox2TQ`(B@lWLxj1JW~+aFOeOT-cikHiY`L6==MmOtECi2 zud%a!uhAZAvP)}l6Jp9vR>lTMm+YWXv8P?CG#r9q!#x@L&cZ~GCHt}}-Q0!TukUg1 zE?+_^#3cDq&z{Qd^QSd>>vN5kJ2kG3{tFUMH?>@-#@s#zj5)#BbceyUyyi^Sj)eov zXJG{i-i9=@saHv8eze*hn+uF;$8N8Bx^VYNDpVQ3dCyN6BiJ=;c4mhErJA|g)w|-B z+-)HfmqMdU7vK8nyns)JcsvyTxE91d0=K`K{jKC9Mn=D_dor5-L(L?=T5otdOV)dh zJ$B1PhpI!A3M0Ail#bZw&fL+4j0Ta)-2d|ns>eOXIdQHM>h8{JCq5a*wB*~I05l8! z6_3hu#Cp6fq03jEP8IoCc`Rp!GcD$UX=Gx>;ZIBk*Q4su85h3P z_OafpV9}j+*pv5F>|zZP{1dh&hUvacmp4G7t1;tUeIE90QO;g|QcYd-o!O|0g62Kj zChh6bcd{kF)B*!Pc|2`!eKhk+?P5XGiF<#~`F7BOT1Ey&Sr!0r{gbE9X$D z>&twGI8c4aKU}K!^ImG_JT)e$vp4m?=y7u@WR)+!A>{jcOJAZrBFU*nfZ4S?MlLi? zzS?Q)fb;%aB}wb)G`D;ZSJPli(3Zq6D==z&K)h$0T*;LR&e3*OHElS zq;N1;Iu>U}1}(R4=sJEtVdA1-@R4$4PT)B7xGX`34<35jfjW1))gfIEn4kU`!i9W8A z%G&X=i0?!%FbFx6C)aU!kBeKPlj!|nHtC_vkt8Q9+>ph8PjvncWtQVqzZgin-=*@qZ& zmm&4D9a!6u`yXMe4@P-DFNV#md8+%u?c4h$q!aJP;E&bvykE6^6z8}$eE;oJ*y9JD zEteb;_1`G1Ty2qAVz+EdoN5VAsdc`vYdKf8arUd$xbw}Zl0=WU+>NcsITjBkaH*F(1a zuN+0CXCwR9oAB$-#j?NZuCM-DLIMBP0HAK$Cw@HNI2wRlOX7nZ2l(_9jy0$dKbie4 zGH#KJ^oaL`w~`vajx_TLi4}*$DRipciyJmM=xDq7dDr*G;@E)bldrvBZ@>P%!g~8~ zP^ItNJ&r%iOe+Utl@>qBU;kM+d-m}4i@#^afgGzg{yegq#`7t*78%crn3O#8so~@B zU@Vw_Is7fZ7ck5JtqA`&nd_&5n+5YP$xC%?y(cL%SMD+ep2Fy)uprqf@QDsKn+ns} zhbiM(60*T8{EDfAihQQfcw>ku0VIG0iZPSL`N@J*AjT3XLLiI12MSLCmA1$V?}6fg zL?wQZFbyb1NYu6jNe-V*Ajm53$pU~Rf&D~uDp@=>Ni>*j;YPOMPZHe+icSGVsX)p0 zMAy{h^F_%XC&9_*f{EOOWI}54xuRrIEGQ`Wj_VeXFnk9_0PsctFYwo`23VT_J{4l( z_A^xXGkkd=s%Y>aGkA;{(l?x;J_UYW4>p?0=-UFTGr(h8VEt^UJ|khZkh~jMbylgfG_yXgm-n1Bx3+e*a-LKsOF$905nz{`@P|R+x?z61|*q&l< zPqnjs2e6f?YUB@t?f28Q;0g-Acpf_`K&H5X!Fm1Pp;>t5+oJfzvkBB@ODUT5vw3z==`dS`0g%1`WwRZMTF4$x(u=nB0ia7%C+w zgc8P}gcehL^(fZ_U<3y23Zw7}66P-l^<}`41z-G)lh-zviaNj7 zK9GcGo%xP<4KNe~8_s}As^m)UTa}bH6qgqB#~91h$SK8rWW9NU1-{2|CW-ThCt7?i zyk0iN{*l1;Edmh{!Me&||FF;Yr5!OM_eNU$!7xSXq_pM*^U#;sjrX z3yy*1Zh+Y%x%)&8V>x)PDoZthg>fYQ_z_pMvyBUY;s`GwV4>%`3vX=0{6-4A+Y5a| zD*YQOqvWXI9ZDjg5L`luN~5@L!$Ptv3CJQ}EED{gdMl0TPH|;8w&-SY(G@Dw-4Iw1 zqbi=pM1;dGVEIX_;CNL8&wVMknNw%A>SJe>u8(F+KO~#Jg|Xt;W^x27Kia(9XF4Bo z%`TgCBlWU5L;YY5MmvS+5)sPVEWJqfS5wUCscd5>XpYe-=8>tgGR9>FXY7u9-ru^s zIXExt6i=1~AV42ePvmHnbcRD7gbQK>ETN{zN>>5c;Z&t}ToFmW{s3O}SD-#Ypeh6j z%V>aI-mZ^PWxjGmiNQ7m9Z^CVlo%?EORzru80NO$P|{OCYGk@CS5Fjxg;G!N3Kma= z-I{7_9Bq`|QM8A0vCBETX>~5wXwu5}rHS&XgUCNcib~r~|p?QKvZM z#7YF9)WvzJZC>JSXwUQQ|NwxL+Qp;!3*)|X9jXFe z(27er*{Dd7=?E4Ep5=N3jEYwa=598Nk{tGsT0~3zKBs}~_KPM<7%`q`g|A+_T zM&zXe*5?P_XQk|$qmI)o5Xpcko#9EBiv3U~wyQbr1s1u3K>YREo}5>LIX z*WiY1xa&c=+|ZHNNDVv*Y2fsr1cnscZm8nefrdvsyMiz3v3b_B3$@4Bd!<$9Z9{-a zM46n(bUVmljdkn>tczwVh)3+SOMi1>UyA7clB@FghvZxn8{Zo?gP!|GF^^1~5aIwp z7m@AVK4O9ZxZtU%k;5s7bElYHzZ`)$7wTR{%%uTxzcAR9N8oQ&Q&&b)H+5j#LtQL~ zXQ{yf|Ly=-scpPi(5m}`vwGU;O^HqBY@?xzrp(^)jAEy z1-!umvM=sT-wGPJ9>pY00G(&}ikSeh1RaJB%9tT@N#{w}94w2W?oYLdm|`xY!Lyj* zpZnoY0r29z%m1A>5>*_H%wgGz%l{;$%)+&c~s1r8NN3Tf6gR;~Cbtg7>X2U!glm%NRcWe)n zJ~c?7sMCN_RZ2~hudi4bJpE(U?G$z%!}QeX>4Gqv8B~P}KxbTJs3IT{P(v)p`h>=I z$qP9&1>OnJGpf-uk~DpB+%yo~Ymm=&@uzS>bg#P~+oUQRR~(doDBNnwpLt_gH%OLM zYlanSw4h;RaXLT3YW5)BnBt%m2G%l-!{1+^uI(wE|Wn9ng|5|yET%Hfn+<+U=^-A zA9MA*uh#hlDcAW8{&G9}zdQ(DB%o!AZC8PfnEB~?)R4VHovRUmDGSqcD>L&GcE!A6t3 z0>0f6m%0a@7uw<>1*MgC&GFD(h%(!AmB`H}#|?|P1|!9Wu%ii~-^)B8f!g@m?hq`f!+s zu^kXeF&p)4ZJX2%(!=sfQ6XVO&xm$lxGLxt8m%2eD!AT7P$3?yi?*Ev!Fos4AUw{!`ev z)0i;T$n~{QP@n!Teu108TD;2d^B}A{C%?^JOR^7>woiK<;co$ZCohNCrZ@Q= z+a!hUohm~5lqWtlt#qv=)Z$kEt&eI0@W)EB5d>JlsY5T5iL#G6kFGc*f)t3L^HWS| zc`Vo3LDCVRt__g#KFn;2xfbx{8sJOHJ^+n|+(1GzvVUIVeExH>Fe7~>L$Js~xF_BI zA=UF)%suGd(x>_lpYl8!vW}ON*c%U9U|A)wqWHng7eyr{MStyHr_DFkHZpO3t*TFl zT4j^Wt)Vyu^oB&(H%7D_W1Y0Z`d_H2~ zo2ShtnCmLv&jE*@PbV}ktF((vfhn|~MmgX_G?NwqT8VuWn+?2fJOQ?S_m64+YCd9?hC2f zC*iHgrZQae-2WgZe2;|mqs=lxB|??!onk(|Z4mktBW!T>)64#753NT1YkPH_D2v%p z=a}tDD~;ocMV+f#oo2s3QmsxI{0yF{wY$w;73fkkp@mQAA%lj}(YH+(9UlkXf%87N zS-RZr>yYDEX`ZFOk!D=wa7i?nA2{NiwKCh{^XgVHyboh}i<#4@#`5r+abGFl3&<%q z;N#GB731|+_b|2|s`KiStSIr{$*Lo3On@U+B<+OdPyaJ9(4X)=#c_?V{YQsjm9!sQ ziTD>~2i9b`Oc4PiBOb$&!y;cogTs}>sHfe-FtMaHu>@hKW4dw;6$~7=Md-2S1(h;QpGz>G$v-G z+o?n;a4ZU-_Xz^f00k^&PJ#}SoXz#X{A_qF0~=`mN4y@C8}H<3mal0cXU1hIDYud1 zsGSJOHFp(|N-=PI*qrZ(2X1hl^TXBVTS|2!xDu3yno)Z7Zxp%CVFb)_9r;Bb&FwDI z5QE+wv7oW%WsLFn{v8SQ0gkLCdYbai0_ZF zx1(lG2C5-~Z!Dw|&6}azLtR@=c-EfjGe+4Wv&t}%uRpxHl$FaO197D&KhJMrwULii z{Q=jQ$lp8h%hGx2=AW)0TBtrD8t&(Lsy?cM=q9h{O-g-p@9qTgN5olymIH`Dkf5q^JJcy^G{tf9OqE3^{Jn6!cnGH^SrINTp*WzPihj+xF`@>;3$fN7T8Ax&URV} zjx^_uR=-!|vB9LR=UAAMt_DA|_%J!gp2hamJyQH-)D{0?<~ftzqTuQb=_gc$(Th*( z6~8|FrgqJP2LJC8GmYaIY0L&2&gT}*b0h1*@ii}An zW=rA;0LFrzxKZ`l+>EE=9%^XHWgj;b;%NFU{%wy<#_j1=1Du(ndDW9KO(oZYG-y@L zQ`%I_B|PIhK9yOfH3&BqF`CX85S3QqICsgHJpjFt`55<{yD7;E?=_hDIBzP8U54dH zS=ycU$U z8dK(LvR)$A#@ymC(^NGa!LjF^Ot}`_Z?@Bf?+mYZESHJoda{gFYSjhix#ycCSbVKO zQ5vzqWW2fkx}9vfcL`MgtEw+tyz~97pWnp=y?0Ga4uNXL=;RwWXUYm1Dl)tWHFRzU zT|{BUf?|04K&MH`FczPq&?gRt)~_%5J{*ipxXPlCKt#S07((0KuXo6n=K?|qid?D1 z?P+V=6}Tbs0M&GZJqFSRIcxe_PjAE!Us88wu}k~G*aT;!?*p4MTD)0XRMeQsf5+Ld zvk>J-&Q`aW=`@u%E*D>R#bT8h(UqUt^yj(`UMU}>6?779-b_R#p)r()B`3A#6ykl| zOTkUs=IM0>7uo=*(`9!stWV7wb`w%WZ@8%imMs!}_zRO(8Vjrz=*&-sn0yI!DNa^DrI@I?no z(o|Ta_lYn?^j*~OIK+|&;&K6Kpk#qiO(f`#b{7PN-33V#NhgL%v=&xclOYP}@GKGw zJWojCh$oUQ-B$b0W1ZRDBJc3Hz;2fbm?;{qf_RE(&_X#VS16vWeAv%qwgr^WC6LFv z&|t0X0kQb|0z8ZX2re~Q#2b)!w9IxDZHt0BuvGUgadqR?mA}Fs=$?1Yi0n%zxNQV> zcc008AbXQ+^EnsAeU-hNL&H(!MW}vUoOz1dfh%7P7P?9XdQtpcuRW-BR=XnIU~rXJ z;iiYR@?0)DP5?B(_}wf9YaM;>=piNVIPc&B;=Ui0RId;TPe`a%w|TfH#ZS6d>ux^# zG|05o`9hl0ZF*;Q-?&ulO}GueQ;x|Gup~bxGgrb13x^b8PzF#YqENPm#MNg?=|!`c z7hzfQ(4Bz?0I>NW226xY$peC*1uBZnn32pcME)g<*dv_`n(scWihYa(e*E_4CdbObELK4 z?@BVJ*^^)VE5|rm*bGTHO}}zxj=^UIn@aNgLT&2FS1H}bY#t1d)Yd8sbt^#TC!yS> z#*f38*+i}nozyw@9_mvxm{p8`@!{>*PlWhEoaBf!|H?XO%gYpY^K7V90)40Au=~n; zY~gU$ADueIE{e_Enf!ydSK14L?2B2JIqViS_b*dB9{I=rv8~g1@O*s0*m>-y2P`SH zRrq1i#*OT!Z{2jogLtGSox{SY zU0tYaFhuVt`Ll8JXYxg}smym=*L-d1z#>0~D zr;Ogz!$;;@-o$FKhVq_M7d~Oum#U2Kn|@Z-m}{u@t#E08YVZ*`P~2GFgHW6<+x_vJ z@XgQx)m=yCPjT$B&(L%^-ELsvR2h_hq1xMm>8hDAd@ZZ&oZde(ENPi0gg-Ru$q{Og z#AoT1_&@G zX#@+RwvwRD>^R}%vj7m+7Rh5b$$mfMne>g9mv@?P*Yp&U)L?n)I0`-f>A*v6hY@Js@kI zgw*^)E7^pur}o^*7VSKa_~af=G)^&jLYMu*HssutegazGR58mcwjRHI|ISPeVG*;P zho1V7;lVN>v?u4{RKE5DZ3NZ~=s~Ce5;5zUe%m0E0qcYa>%cTh$hP&^X=%Y|GbHr~8%7*ssAhAGUJuuku>+~w!7LPNCn6YXW80ORCe(KXZA zYFz@0QA=!9*c$l8jVv?VyW?J#H;CBP!FXzN>fm7vJ zi39b;Wnal(CN+HthQy}3LF=V&F1@6asy)+{oSmv%SumG7*m1vk$F?&c4l=cc+Qdqh zC9<|6eR=(=3Kfrg#m5n!;DwXz!keZ879TWb=SwScsapn83+kAe57bLlhEKU2IxyBa z1bSDuz;PJFKSfeRLj*KTU6H0Q^TFChkPB0%j9dLCR#RZ7EV(&JY>LGDo~R@Xbxei+ z7pHcdR$1kO!Ws+ml?vmlD*rT`?bt>v2t%jT1;C9aA2iMPWXt^KjU)7GNlm(&52sJs zszFWG4;v)eLaZhk->3g$u`s&f5S_#K>1VddZm)C=eO$FwdKMAv*SPCnd(o>Q_D}!E zQMngthW^C}p)5zmX}#CXIhA7thOuCpFw}UMtcgzI3kF&qf(+Y}jPYQJ_leWNN$B?g ziNhqED*NPmqQw@(N|&snDu|bbN?8J}`pJr^(8!dvJy`tsqeTc8zU!u@q#ru*{q_6V`q%q3{Gfg-YW7Z z+0#HG02!sJ#8bv64}&xv3zmo`*$oKe8|2E(oK!ld&f#AeY97!Gm?`U6R@TTk!N!H?PT9lMTg40LuW%i)3b37 z<5q@>+tQ+Ely`*0b1qPw4ikqS7P|2#DQqQ)Ws_~Y5f_F*E$2Qwo_JoiRetF7FrO}c z;NmVH+L-@+qR&xxF*ko~h zqH6@t*c4PMVrd1s4CTUnB9CE?OMf&{1$l%UE>lijrEl_#+U5Bc>9h61!aO}Qv~Nf` zqsFoBlPBfwA|R9Js~KSm!Xtzxl?LFDN7IScd>kb zHd>FNjXey#azO{0T-#4YUAGqT)ePnF4)JXXlNE$f9ma%>oz0R4O1}iL+6N8pf-J9+ zUVU8CboE~$s;GI6sR%g~Kzo0L8eUWqf3Sqp+Vg&S*^ncBWjE^)A&YK!z}Lst)bbOg z$ge!mJ=1)KpmEcN;;&cUIWzFholC!*-r%R!t0xxTSgIf{|IfVuB<#smsPzTWSXB}x zl&nomDuh7K;dxx@HawOQVPlzMGYw*+{anlPL1_!mu2zTUC@U&U|Jd+vP_%!sK`7KW zF}kH0Y2`HrsAjC0t)waoeMj8>_$n2JS?(`Ci3nf81ub51-IS6h+z&JQ>C*RJegN~` z*NIZ-YAyBY@y2*Px0P&Z`$)bKH`FRN=?3qX2SwyHkJrz5Ka~QTz#2zc{iS)9fy9p= z2uIm@H+X}>eP>B4jro+Y97ZCa0{tBf)r*`C7)!>z?#Q&s(O|HfR5+vCksrNp+!!gR-ZR zRYI7$^yvAXe1E%5y8oGKFne`qcW3mGe^+m?G%u{{Y6S9 zf|D(8X>t6r^l?@<;i~xrPR_`4W?4A&#Im$XO3OhpceZHOKB9TYKsV7uH#xhevSXen zX>F1};Ioxnmd^;H-{9vHvQbI7V+Oc(t^kC?re;88{RLEHz!%?p3pw~3Usj2eU3qq-(nW`wLb6EZ75)&Cw%o;M%iEoB-9`kgvFobN&brFb3emKi`jdb*%z!_ zW>YDz?8qLe@YbKOG!}}B$`^UPceSN;kn5BEW?(;)^6kk2Sfr=mZrqWwf|JUpO-*)~ z#=T2LY%}kMU`@HnntkG%_D`y;fAq&v{7ys`*F3*z{=VWtc`F_Pl*9w0Ysq}fAR$_! zW2BaNVa&O6y2XDH+k&CBF`uN&+TJX!Xsq(GbM5y9_saW-XrB$3Fa7i@nX>sP9I@uA z?(!|4mrb+nlFvk-i;tf`-`BQ5pAgR1ZvrpR8*i-_be*g88YO*uL5d6neiqFJKQG0! zvw_aIB?~b?>xX+*pAN>G;3s}J1D0Js^Z%k(Z1gJs^8B*rV>xiI#(HHBV1!;$88wbZNq>QTQMb^+GR7?ZQl{=992%3~-EqMIfpx5tj-TWd>j9 z0`t+Ze8UGLI|uieyaQGy9C$<8OB#4Iq z&Hx>?UG&U%?T$*(*pdi!|1=rhvG9fztCbditN406pAl=N(UxEq=6UNbmiHF$aPKZU z;}*F+(=WDf!Sany?_$GAy-wp3mxpWM&nC1=XDZk{8*+}2q=6@!$mahB|DOChrgy#K z&wl@q_O*5GrQ|lp$m5N^c6Q{kdA_s8`A?Vg{QpQ`ehW*o;XeFV(sQ?~4N=>?FStOi zx^kRib-eX`wJEEW+cjYf*XLw^jkG*9&YNssZ|kPU{d(@->)WvfHh+Bm<`YwtKNTFw zLJi-J9KoVyCd9*Y3VXM6FaFd&?rxz5JBHjIQLZ@2Vsz>e3Exc_G#Q67bWCS5oL3qy zCLO7sgr+Cjvm05d?_-QfATjB^+^K5IY-yVk)6{#@;t8ZL@AV`?H*FKNudIzXM9aLc zFuDz5laG;|t~S4Sf>G)bx0Mhl;{V3x#mLV#y0-nZEmo~|&E^%mrI^}L{i@AgSY9(@ z2F{ax$#E^18{@Th-?jRnF8AMe%rJWxW0~A?L+5LS^b45wF6Fyf z7MFs)n$bEYPuZ5a`tqNnXs=<9+JGk^MH=&2-nLvye+F%Q1vY4fDW_XQ_Gy6{&Of#{a&sJL|Zphb%p;@U+xH{mDexmGT~!~(V^QUu~lLwMG2 z3WUF}h*3^^XCYQsi=oQq;N5Bq&eNYT3D?H>z2vl@k!p+lCZWp#3KfCVZoWCmt}&H2 zlSLe2muh@(U~bC1w<8pN5{-tX6^S@H#j1H%-H_s5w6eC>;Vl`ceMHIOJ8}z(f6>65 z8eD^utCjU93pUmp*)>?pH|AH5%HHp^Ce+;O6uV1qP*ai~6tom5-dt*2)hpNqi{TPWWDssYno>9>Xs1 zrTq8khG5XlW|~qg>CxL)BG=0l0()id@0Aemr$$+is)!{wT*Zy*xW8cXC4Fv&$Mw&) z=ErBEs*KH^2~f?ffH1#g@ttoi+4RP5)uK%WVO-bLFbg*2EX)t*e1xP%pEa~+vd9ze zfBw+rF4g$Jo*x*`*&_Z~c0K0o4X>Gs*P!GjPg8*lqUFw4q?P&w3}2QH96h-soFQ^O zkaCZydocC(&G(MqAB03NcYmt<^cH&F>9k6mPSmJCQ6o+=W|Dcwq{<>?&y}vBfGe6m z6C`_D1W&x%ohD>?624wj>U^g^{m>!j2+?Wd;LTY(LUs1({k3hKGdS5TD3u%3q%l|j zreXu`Q!yKL&mqfzC#d-46mQsn7Y`4VAAgkYynOjKru5yfUngKyBG>V3ymg`29Klo) z;_h{m%Gg8`ji~9QJw{HsnVOd*F>=)v_qFaAs*xI90YoaP6+1d+(x|u7Jo4G^!@X%#; zQDd;AW%If;N!Y*fP~c0MVoHf%R(DMbUAkH=OTm`Vvq=#?)RmUHT3TJ4dM8)8`HB`< z+-=D8gxq&YWXwKSPvFAwkKFON(CAPv$t~`<{9@BInIBGB64ybe^=W#bo>gKrmqvTG z>i5e`tQ&JLQ<=7kBe6AJQt$C<>6Vy{q=JBAqOv=W=N&s4Un;ZjewWA$r%HZYz{am& zJ(P{AU;I0pK+L%j!JxVz9Wp)%4XMftnXD?gi#OH&G-Y?i)fpL$Hw zQ?p5rshZh14=a$g5gwIF*)w%6@q?TdATdoZ1|SpLtNxZCuF6a;DQch@+8uUmjOISl)_&j(a8kF9lj%lM< zDj3N}I(Fs8$P4s{Jhavs9ptqvAQ6K!&wjEk>TEoov#aVcY|~uRWjh$p}*f{u)LEgBn+A{?Rqij1yXNXOudJW;pVi0mzqXj?eRbs z0fYaZzi%;>S-7!MEvsM>Q?*!Zjmag57GtT{h6nYgh9$#XGKx!KN^&k94m1!Qsr;EV z61J&v$@oFgmj-Eqc&f&AH<|LKk<_>-Z-@^ab^YBB8J7%i{oSK3X4nyX*dS#J7pmc>VR;O~d=RS&BNT33I7!^$`D&8xt6v>8ybVwcL| z#}@ETZbVBw^*mxc!dNdNF5(Zj53m5^A1*^;FJ>1ptUl z9QT4=6H}pzEQAysf`IH`z-J=4q$vCIk9&uh0P0gI&NU=fA@P%$)3=8-!L;lRC7w$qSwm)bIa(H(!JfxkHU=r*wq18r;|dHCPGtotimKGNZRqX|>dY z{56TWK|$+wX17=`lvrgyyJdJnHYZ7U3lxo@P7&%seaWT>sshne4ccF2ESz5eA9k zi9BT>5k;V%@SV1zrDQyi3&F+M7<_@Wa)@>E0*s1mpQOnYlpMPa&JuHvEzw4ghCWP` zSDtb0T(n;{%P84OinC;0wt6|U2U)t1^vEkqh-dj3P?THg9$0B~^D~2WG|NnJ9QD)6 z;t;a-cko3AJ<{&H*qPM~JKw(Y3fXq!XGP0AGmveo5vc%idqkN-z9=P7e8Kz%8| z!mQ9PE_x|Cl3o{-lC8!}_9O71 z*4lZ@!dnCuCpHd*#dwKv(>KmhhtGKf3D@0BA65Wa>Pry&y7gz?4_-$l7)tK~re zML$1a5e;=@Ikc1gvP=YZpq8zw| z^WLV30F0Rx=`xBSX2t+`ktF*b$L)nkctsH8c^6}XohKWWCayatZdt-73*?FD zpD$mUMFaWJKrRTW&5JZ2a?fhr)CUW&sPE|oG*f8 zlFhsjIF9$CM;MCNvGH-U&uzz5McY})7fp#(!}zU7xF;7;on-z-al6>M5Q;SKnrCP_ zJ>(jNW=0mQI2Y<&8l>+_>lbe>2$Em)z4UTbur5e)T(tSZ2ZeGvrfg9{*-v4T*uwTP z%89Np-Y@M3KoV%e!59hs5f*OZNWX+sGY&XT)IG807+sJ0Ar9i z+t*kK03HUeb-UUw&{@?;2E*>Js{Bcq`@DKkZm`{ambja25K)Lg6T}0Ae*&0^#$pU! z5$p+#rOf!hTA+9{4Xg-4pTg_LaR@IUnwiFNxFEhhe_f5pqiS3GpVIDjW>Y=x)VhhvTM7cJgKBFhYirUsgv4u|F}OMp`R$~dwuC^J zAcuxCB0UoM|f5oKoT zWqx2f*tU5A9<>~_eu3|h<2juQP-QnZ* z;0wH_5NOX=CnfRJ{(OMw`R=>By)gIgYK5+MP2Kj=eRmWTGIt|py`t3plz6f9R+?}= zC5lDj>SRCtBDZVu3&Ih6q z#&A>8)PXyX*Jl|9ZT5J+s-K=wVLbJcC`_2c>;agOOEQ2H5HqCV!vYZe7Qp=$aM~s& zLL>?c3!)!Mpnu{NZRH-u8B4{2(5EbhGgx35F1Z5t(FymlWZOa;XX}I$gOEhZmIPvJ ziYrLP)|b}FUy8}fo8ihO)!P=K%6$29e7dBqTQz(UKtJ;@n^Cw=Q6#=HlJ~Om-m&$L z@{Yy3FCUfVzf6Yjm@Drsw-)~k|3dyUU*2F5)VfKo`FYT~ZPd2ycjcE;|Gx6hLHw2f zqOMdZH*T_j{leAui(KPecbo1QvFbQ!fX zd#ZMsw?Bmy3z2=p4oaVx8~01EVGe*uJQk^&)-n{)>Ps8+Uc1`q2W+oKhxy5MuEiKV zcvazb!`p9I*-yp~ASDapE?bOtGS>RG#_jw~SQP*mrwRV~G(naL{uLIAD-DN-hf9$c zDWqR@mi1+<6Ke245Snn_;=CIFb8i?BsS2;^uc9naiMieH#LoFauh z2v_!()5o_B`U?2^^`B8|Tjtk;o$tR|<5Yga9v&NA+4=rm*;$3JousZJFGLIf@~-nZ zUF8S)tJ2p_&4F<38($9Czlso4gvvy;#U6UcNB*@M5GVlf z6QBq)&uPf*oBhzIpQ!p^g;iSO&elLCYQ zA@mj?p?B#ZH3_}dkG2f2nh-ap(KRPSfVgeC@D>0F*z|w1#w{^34T5al(+=FWHcn@ z)MOZ8GU6y%8Ch91Em?INIRyy5WTWX;swKUAN^qjTqE}yl!prdD^qi?BWWUi~K zpnFzJ7iX(ys-@?dtZ(dOVCrFD;bLIni8r#rn>yo7J@A%ZMmics`esHJF2?k-@i#dO zH__ELaSSwb^e{Klv(UG-G&i-h4zjfMvJ#NAGI6mo@wBpbwnp$;)64OkwR51g`z7nB z*Va)@Hjb8dvMToO5%%Y=*+*78TDdy9208|%J2`thd4)LJDml~3 zX_3TNYw5Lb-|bGRy`5*ZpC6&4*8NnfT{Ty%(5%=M&e{4&?B z#l=R2T&FL^-H0RQ-iV8iPe@CQi6kZ8NlLqy!YiG|B9L|?IW4y+{a$%a)o3o4Wd6gl zdj*9L`NRrKs~$aj{q#~`c~Ne8xo3IR+h=rz^6QqGH(jp>58gI+H#Idk_q}fE7-;Dl z>ul@j?&<9rm>3wF8T|Zp#K34`bYSB1TOcnaBVw&=lz50tRUOyWuWHMQv z?R)*)E@=bVR%Y%j-u=WpPcHi8z}xt_jC^ML9ZtEfa@`lXf8O#)etzlKc}LjlZS_?0 zH%_0Z&7cOh>a9|=x8b!)Ufb2Wn+{nECDF!ztmy0OkbIgwpHxrzs6+$*n@n>hIH zOjm0fTg=_GoUNI@V&TMr7Il@4PZiw{GcBj0HopjcxCzl}e!D&M&TRRa;*G7)=?340 zwdCf;?a>5`Q}P_w*~4!mcWoKXT7R#uPx6V)%!fAr+?{`yMKfmnr+fWiv9R?@Pg~2M zgQ*pkjPIUpHaG#(hc1<>)dIAMnh$x1}F^rODE@u>q+shhj_hFH?8nmQHzAu9x;F#YH5WqKJcwX!BmrAJm&8cX?aQDd)hUwpp0brNmWKV5Y_>Hqu7-{`<)C%)R{B=T8ol z-mn>cHw{(M=Q)$V-)%VR^-^tFTO7x51W>TBUT&Oh-fNWquz;>T8mqqD_;miR)9*f| z-T~Dzwot=EZ`Tnv4#ft$VuBd*%`(}j1oIXal*y9+Ufh;pDHHNhH`Tg5Q~TrJ7l)(z z0*^*|(n@UJI#z`oW@v_g*om19e|m37Yel(pP&@vRrZ_Tm;Wac%@OlVb=D(3WYl|I`_!4Pb(Aiy zs|Id*{M*#<`wXjiy8FtYzZThPkYiarGBK`!G{UCJiSCq6QMWy#voZNyN$2)erk$9S z$TNHE|GiXDdE+O z`UiK7QJ>3_zbJdLRXKELpeo$;Ux=y4V-VNOAuuf}Uh6Xln{V0EL}7f+@if)q*J^f@ z=IN^RLczoLOY$ivuT0TfvG;^_IWoD714L*A6a&Fglg$&0&S@3)Foz2KR_w?fZ|d36 zsXD=52Z7SBoiaiI4qvMEg|^ZMfu7#$baEcD2eam^uvOOiFI z8#TgT$D}vx&5G>D%fepBJl=-oyw_}^X@=vSvIf#Ws_%4~;cX9#hpeSSD5=($>+&Rl zyUd0WuatPYVL6~yJI!8!(e$`+4C|u{YFWQV2FdCr!spE`>A~kWYS+s{P-|vi6UTCD zqn>Fil8x;89a4BVo}~~kS^2;JSfmltniVk@Midi%lGScb*fDIvzRQe`_qr!Uo{hm)=%~z8c%d|9ae6_ zx2?YYei&j5DH_#Q@Q>VdNyaBumw99-$f@T-jxFj!&tJaFoA!{wg^VwHw2>&qQZj0~ zu72jt<^`Kv!HM_Ib(?U-OP^(CnrMu!!m2f4DX&(`qJ!Y3iXqbVSMsaUmM%3Gjny=a z+;QbDaY47cTpR&zeb-XUV}fHST3NH~d*&`d8?|U2T+n)yJDhoN z^}DP|YX8`SYt9?my~ZPPeSd!c`~V;Z5(&)gG7JSqV9UA$F1=g^U3V-~rgF$dZYAc0 zZ|&??l#}^Fmk>fdrt8$_DX%;I;zG5th_m$xW`hBZT}P+|z$fjgQvhpWtca{Z{cU@_ z6_Kp-Cd-y78bO0DVOWUvB7srWIAKL7h1#QzQz05iqG=~5{Q({ho_#7orUZ=?DPD7p z;vBY&Mg9(Szp{;(Ao}`XBFztZMfKjdqWq}YKH+nW@}exx85%C)ijup?L_0ms;1+u! z!IF_Pv6$>IZ=>O;Om#a~= zy~0<1uc7*4si{(+&L`{gWD!1S=7kW5fT2-jN--{3La+VO4Q?YY2@L0TI#=z`k4gRC zBJ=r*@2*4IU6@#6hR2OUDa-etYmQqk_lZy++A@wpV(XYuvv5KAV7!7kB~c?x1G9r= z#AueVbYVLA4(gJ{5cR3}TYnZMJW_^krCh4ZjcnMlN9t#%F1yc2Gtc_o@=Ew=Es>!gsL{kTqT;zYzzzLdu)4XD33i`tttDzgX`~+1O%4A7#y&m4BzyN z(jo?yD#PapQSSSihuMfWEMxO$mg>bsDw;_E#w(y1@>~Vchh%LJCpX_GlP8I9X%M@v zFT5{6h+HhAHvzer?JKb#`S30;Bgp^2{-%Wt0-Vh#&!jmf&3+pI=ECtu_KzK<48B%MAhHzF3(4B=5w)MTZT>vp%$My}G#y4iQkj*xSOxmJV#0mWnT=rw8yX#j* z)U#q02a&L$@GM;r7bi|;ViDf9mv8?o|A`Emy8}_gnDcJ^0$=HE?H+{c^$VXX{5>8> z(^N25Vge)X-BXUjCm|7*4iTV)JWCvgx#zB0Q!Mg9-hWMbFEEIzVt9Tw-PjH9tApnQ ztgq{C;LgTh!^o^=>(AI*K}+s>KDgImioBG1FTx|j&_Bwr&!6UhU;aV>cPZj(6W7(4 z2k!n4JY_?6`R>J?6|1v|i%ew8t3UHcOz4}kih)1hDZfNfhpdz8UBufc7XJqi1%fp| zmw4UogTd%hO?2^#A_M>H>e_{xxZsHE59==}80Gl>zU#Y0bClc#0@E+?sUZ1s zE^Jf%Uur$ndlnG#;~u;DlgX(^lQ~bAR5&=7xLq6*IC2oS#>lNHl>>Y{~m-xXE#W1))RL_&qvk!TvOTnXv&1{LS zI>cJGt17=;3H>8Do%o^eu}uHtoOVCBgKBQ{AJ!yM~LsIQfO+31no@f)LYdoxqG zjRv>alybS#oip>s<>BL&cE8I!LX6-%{$s{wW#ex87@MuAav{*&4&?KQO_tV0mX1Z1 zCNyh_a{P^)r@8y$Qz|(gj9xu%7m3H7Bsup}lZxN8BgesV#O_{&i&(`2+zT3yopI8O zImZg~5)WL&e>M~D7GGUH7H6K2Q7bN7FW^EM?Skn7JpK_JH6THb5){g^sunKx>Y-@A zim?7e?hl*(uQ#je?^H?kBbSc(mt2HvSGXRJCTf;&^ga~)G%7eCSoYkr=F>y&Qk?M3 z_NwJ_)ObYI^!u8Feg3?NswGO*WWTAdXX)o0(W0{IFM=<>mLnTWq$$9;q2vX}r!zFu6nN&y4cq{+iyWWzOe;dSA!OOY&EEDf6S4ThIr zzf^{s3#FJhH&_UzX!yU@v1%}2X?O~}F}Oj84X|M48(!GIG1z*I=1h5M&+N1Hrj+nT z>kpT)5XAuscdaeEEt#9Ew&9t`$Ud-_~Uf(KwcenW6Mft|tmtWrv zZ=hSqt%Oo6|G+c7-sDy_Sn4*}6gCtHHRXG~y*JQQXw{Un)s(CIUU#Oc>~d4)pQig> zO;0bszsvGIqv~BjVN=FHLyge;8msqr_h3obx8_#tCC#skts4EBInZ_B-Wy&~ciggC3`XA2N`&AEDAmK@H{=f!_~; zrIjJ2*6bubq_lER-f4G2^`KLFpZ)d#Dgd0P2X@p0e@q`7_4abD2BOm2c|r%)SbOV2 zhd1NsdMNa(j?ixyL?(Nv-Fxtab#R1rs6%h$qe73-spO+{YLRu{ z_$=3POSf4|SK{&@SRZLV3ouans?&$19(7H>VaL(jhpU#G*O)J*6*_k`^Ygj!cfxXTs?^H=qs%5!^ZcyZF9M{d^XjSu{-^Y zRe;(l41=;qrT_Np&3<{6J_bTg%Il9M&^fHL{mN{gz4&rK$` zev)}KrMJ^7K0Ap!H(Bs^?7rfp8f)*@^mfDjaVtgo2!6U!7&2ElZaFvYbQ5f;IPt-n zy=rd!$ZxQ9Zn|m*F> z+TY2484D?M3qNdnQ(OD>)2Gr}rwnSQ%37y1|1Lgi?O@Ja;PeJ$tpVlJPlrQ`26Lav z^``{mr-fTM+-q8vS!TUz#zzK+UMW(g{3hOIEWd7Tp=5v>^|`$MPJFNd2W8K_0_N}r ztHy{q&&#Xz0LvriRcpi?Zy-xUI2bv+C@Q@4;M`Z$zl-j>OQ+{1p?*_<&sRZ_HL}ej zYhhIEx8X&>`31?0B|v#Cy6uZv=r@s@-%56-#9P-IXV-FDKgHY{yR^F) zOGAtaihSckeS0|6uRt94XdTE#%}9+*-+w%wpa*`fKSTMu@x*pR3pWnNP|;Y%CgiG3 z+h$$(YF!*J`K6gz3!#PcMgKBxT#ss1CoBjF7s8Vshl4!^Z2wj z=v?n}GhTaBtpA>tD45|v+PBU;{9UE<(+!stJY~U9ZDq(1FJf!1NMW}RpBZ9_a z0b(^1<<8C>p3Bp3T$?JRb$Jv(Z@eZ-B?&zL5nd5LF7x8)fd{sTEo7+boxI3_|)%@)`dVgG+dYrc&`BwefHx zayWhZ-S8?mO&+u?SgcULnag6r?X)fbvdXhXPO-8gF0Hb~5_(y_&Qjf~=JjwK+shi6 znF#e(3GrNN4XbWgUu!^f+UpjC@}T)F)e-2Md5j7(n<`k%?N0 zZtYQBG;gge3mcHD+1{~kVGO=Q=DgClB!-1+CI5c2D3>|JFNeFP{^s9sX2iUD+e>r9!VLQI1&j z&aY7m>Az*%rD>YKgUhTQoac=r$Mk>N&)01yF;CXfOve8mxd)XQpf?g(>qK+2(o+)gUIQyIbt^*{45!?(^*ZG|Oc(K*wtSN>a5b_&MD;ymC=%%2&DRTCDX> z4eJBnJ6w_1S46F19xonV4-$xx-`Frh;M>*`E~Ki0LC``s^b(`JH=Y2ISOg%ksgMLb zktJvmyaBqCqHa%MGG3Gvam2FNu0qix3viKFG()62L0m=!I{E%{$}gN4_j^yMfO{#U z!=e~(RjH`ZZZX-AQhKE!8>)}SFg1)Oh9R#*1y8VKXH=De>MVoAxH8;x63iKzO~N4d zbG)$$p*`>CDQ;v6=QIDXv}9(j0+gq>)Vo z+1h*4_7ugEPmCnx_L&B7B*S-&CNdI4CUxy)_D9+#Jes3S8Wb_^t1{UNwk0rI*94a5 z6p9e+tHuP6(k+^7p3;86<9CbF&6Vj2XPT?{w7+!gOy4!vTsyk( zSnPuJ&0`p7FXZB3PpVSVO@^XBtQYsG9xXZ;pfHN))*QpFbaB`5!Lf14smN8s3Vq|V z5>zbXk1s?aKX4+{8x0nsQtTA=xG*mCFybE$j2HG2Alz`6Y8@C6E+PGQ!&6B_g2@P3 z%Jt5^Q{4RS6TRhSo{T7eQ3Qrrm6Cu=t7DkPW4cmE386CTBoUqV6y<^?{Ht~L87WMq zrTuYQWS%IGl>l7VfJf|RcGu}TCdDA%k}br~NOJpiURLp*gw*J)kzg02hW!LGmuiCk ziao1(gkw)7k!22yOz{Oc8haM7tXLU-$3P~4@0@iK>p2c)=i~%rYGR4QMtxt;*A5;+ z*!r3jw@&_uD({8Y>|y_<4*0CA1{?-F+C|8{LjV1Ae`IEk+LdY-BDY<<^^mWC+tm6$ zxY~p6xks8i*I!=Gcpd2HL1?LSo12pieSgre)cdM%ux9+VW%q2q9`ld)w+=LYcDOGZ zysYAdvid!4iLCW3?b9XRVgarE{nqxls?`D72q?W9dr6)*Le}*6a;{Idh(c@X5LOv<+@NB3Q^q*Kww)1A zE#a!OH=&dmbjIh_)6<0g@} zRHI8)qTbYmqWJdZSB;lp;JbXWX-yJO|HDO_)pEMH#s0*vz3Q9VzAZ9S*rs=;<#AY8 z6)Q}UyL+I}>5hxuL6vJ*=Q12apFfzl7NAiO<`$y)KuSy|j% z@ImKPty5-1o_1OJy0Q#ebIdUo#f2;>2L4m^=Ol;g|B@x0;~Qtj7lFq^^RrM##jDP< z%iQs|x@9z()0`w6Ip2kwtX#n~>22cr=~6=DAWY>sy;AmmK6kWHX%iZ!JwE4stiCYum*k%26W$l8O1Rh3#|Yk*%WvMAl!&`EhaG+ zQE@g~478s(#2$%P#rH|7bWhbrf_*evsX(lu4%}$hxyTIcn;?#?I!{a^FIIqoUT*SkUDv z0~pnC-bm`$No0Ha5v;E3K>WEynbJ0w0defZuA25As}ILd+ctl9?M>^u>NOs+v>hL| zG8A|3;@{bM6Wpy0T~W`zTLGYvZa-_2s{-9~MBr<)v2T&VL36i5kam(#onN}?67Jo) z*N4l{Hm7*4?@k6c@}8hC7yc<>N=t)rqOs{!73r2(b7mZQF%eeAMAzS@G*aLNW&jKa z_%SfY!{W+%Q?P)6pCF+u1r8vQ>i{R2fVy1i6)vts((#~Foi;wy1gFwoLb}{7Qd7Df z21PuT9%nA#r!(H}Vz=?71-zc`JHS);+=eV%XVn+ZN6lwDP8*Tr*qU4lDwV?EKFRD+ zoFv_1G3=1-{-E<2yz3sU>#jt{Q~Rz5;l`*aV}*yHL?-~|PViZ6bxk+CHe(zU3A~i( zS@t5>a2qZsc5hCbZhbO2+%j

'; return true; @@ -3469,7 +3537,7 @@ var supportsTableInnerHTML = Function.attempt(function(){ /**/ var tr = document.createElement('tr'), html = ''; tr.innerHTML = html; -var supportsTRInnerHTML = (tr.innerHTML == html); +supportsTRInnerHTML = (tr.innerHTML == html); tr = null; /**/ @@ -3514,11 +3582,12 @@ if (testForm.firstChild.value != 's') Element.Properties.value = { var tag = this.get('tag'); if (tag != 'select') return this.setProperty('value', value); var options = this.getElements('option'); + value = String(value); for (var i = 0; i < options.length; i++){ var option = options[i], attr = option.getAttributeNode('value'), optionValue = (attr && attr.specified) ? option.value : option.get('text'); - if (optionValue == value) return option.selected = true; + if (optionValue === value) return option.selected = true; } }, @@ -3572,17 +3641,24 @@ provides: Element.Style (function(){ -var html = document.html; +var html = document.html, el; // // Check for oldIE, which does not remove styles when they're set to null -var el = document.createElement('div'); +el = document.createElement('div'); el.style.color = 'red'; el.style.color = null; var doesNotRemoveStyles = el.style.color == 'red'; + +// check for oldIE, which returns border* shorthand styles in the wrong order (color-width-style instead of width-style-color) +var border = '1px solid #123abc'; +el.style.border = border; +var returnsBordersInWrongOrder = el.style.border != border; el = null; // +var hasGetComputedStyle = !!window.getComputedStyle; + Element.Properties.styles = {set: function(styles){ this.setStyles(styles); }}; @@ -3596,16 +3672,25 @@ var setVisibility = function(element, opacity){ element.style.visibility = opacity > 0 || opacity == null ? 'visible' : 'hidden'; }; +// +var setFilter = function(element, regexp, value){ + var style = element.style, + filter = style.filter || element.getComputedStyle('filter') || ''; + style.filter = (regexp.test(filter) ? filter.replace(regexp, value) : filter + ' ' + value).trim(); + if (!style.filter) style.removeAttribute('filter'); +}; +// + var setOpacity = (hasOpacity ? function(element, opacity){ element.style.opacity = opacity; } : (hasFilter ? function(element, opacity){ - var style = element.style; - if (!element.currentStyle || !element.currentStyle.hasLayout) style.zoom = 1; - if (opacity == null || opacity == 1) opacity = ''; - else opacity = 'alpha(opacity=' + (opacity * 100).limit(0, 100).round() + ')'; - var filter = style.filter || element.getComputedStyle('filter') || ''; - style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity; - if (!style.filter) style.removeAttribute('filter'); + if (!element.currentStyle || !element.currentStyle.hasLayout) element.style.zoom = 1; + if (opacity == null || opacity == 1){ + setFilter(element, reAlpha, ''); + if (opacity == 1 && getOpacity(element) != 1) setFilter(element, reAlpha, 'alpha(opacity=100)'); + } else { + setFilter(element, reAlpha, 'alpha(opacity=' + (opacity * 100).limit(0, 100).round() + ')'); + } } : setVisibility)); var getOpacity = (hasOpacity ? function(element){ @@ -3622,15 +3707,27 @@ var getOpacity = (hasOpacity ? function(element){ return opacity; })); -var floatName = (html.style.cssFloat == null) ? 'styleFloat' : 'cssFloat'; +var floatName = (html.style.cssFloat == null) ? 'styleFloat' : 'cssFloat', + namedPositions = {left: '0%', top: '0%', center: '50%', right: '100%', bottom: '100%'}, + hasBackgroundPositionXY = (html.style.backgroundPositionX != null); + +// +var removeStyle = function(style, property){ + if (property == 'backgroundPosition'){ + style.removeAttribute(property + 'X'); + property += 'Y'; + } + style.removeAttribute(property); +}; +// Element.implement({ getComputedStyle: function(property){ - if (this.currentStyle) return this.currentStyle[property.camelCase()]; + if (!hasGetComputedStyle && this.currentStyle) return this.currentStyle[property.camelCase()]; var defaultView = Element.getDocument(this).defaultView, computed = defaultView ? defaultView.getComputedStyle(this, null) : null; - return (computed) ? computed.getPropertyValue((property == floatName) ? 'float' : property.hyphenate()) : null; + return (computed) ? computed.getPropertyValue((property == floatName) ? 'float' : property.hyphenate()) : ''; }, setStyle: function(property, value){ @@ -3652,7 +3749,7 @@ Element.implement({ this.style[property] = value; // if ((value == '' || value == null) && doesNotRemoveStyles && this.style.removeAttribute){ - this.style.removeAttribute(property); + removeStyle(this.style, property); } // return this; @@ -3663,20 +3760,25 @@ Element.implement({ property = (property == 'float' ? floatName : property).camelCase(); var result = this.style[property]; if (!result || property == 'zIndex'){ - result = []; - for (var style in Element.ShortStyles){ - if (property != style) continue; - for (var s in Element.ShortStyles[style]) result.push(this.getStyle(s)); + if (Element.ShortStyles.hasOwnProperty(property)){ + result = []; + for (var s in Element.ShortStyles[property]) result.push(this.getStyle(s)); return result.join(' '); } result = this.getComputedStyle(property); } + if (hasBackgroundPositionXY && /^backgroundPosition[XY]?$/.test(property)){ + return result.replace(/(top|right|bottom|left)/g, function(position){ + return namedPositions[position]; + }) || '0px'; + } + if (!result && property == 'backgroundPosition') return '0px 0px'; if (result){ result = String(result); var color = result.match(/rgba?\([\d\s,]+\)/); if (color) result = result.replace(color[0], color[0].rgbToHex()); } - if (Browser.opera || Browser.ie){ + if (!hasGetComputedStyle && !this.style[property]){ if ((/^(height|width)$/).test(property) && !(/px$/.test(result))){ var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0; values.each(function(value){ @@ -3684,10 +3786,15 @@ Element.implement({ }, this); return this['offset' + property.capitalize()] - size + 'px'; } - if (Browser.ie && (/^border(.+)Width|margin|padding/).test(property) && isNaN(parseFloat(result))){ + if ((/^border(.+)Width|margin|padding/).test(property) && isNaN(parseFloat(result))){ return '0px'; } } + // + if (returnsBordersInWrongOrder && /^border(Top|Right|Bottom|Left)?$/.test(property) && /^#/.test(result)){ + return result.replace(/^(.+)\s(.+)\s(.+)$/, '$2 $3 $1'); + } + // return result; }, @@ -3709,7 +3816,7 @@ Element.implement({ Element.Styles = { left: '@px', top: '@px', bottom: '@px', right: '@px', width: '@px', height: '@px', maxWidth: '@px', maxHeight: '@px', minWidth: '@px', minHeight: '@px', - backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', color: 'rgb(@, @, @)', + backgroundColor: 'rgb(@, @, @)', backgroundSize: '@px', backgroundPosition: '@px @px', color: 'rgb(@, @, @)', fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', clip: 'rect(@px @px @px @px)', margin: '@px @px @px @px', padding: '@px @px @px @px', border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)', borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)', @@ -3738,6 +3845,7 @@ Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, bor Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)'; }); +if (hasBackgroundPositionXY) Element.ShortStyles.backgroundPosition = {backgroundPositionX: '@', backgroundPositionY: '@'}; })(); @@ -3779,7 +3887,7 @@ var DOMEvent = this.DOMEvent = new Type('DOMEvent', function(event, win){ if (type.indexOf('key') == 0){ var code = this.code = (event.which || event.keyCode); this.key = _keys[code]; - if (type == 'keydown'){ + if (type == 'keydown' || type == 'keyup'){ if (code > 111 && code < 124) this.key = 'f' + (code - 111); else if (code > 95 && code < 106) this.key = code - 96; } @@ -4001,23 +4109,27 @@ Element.NativeEvents = { gesturestart: 2, gesturechange: 2, gestureend: 2, // gesture focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, paste: 2, input: 2, //form elements load: 2, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window + hashchange: 1, popstate: 2, // history error: 1, abort: 1, scroll: 1 //misc }; -Element.Events = {mousewheel: { - base: (Browser.firefox) ? 'DOMMouseScroll' : 'mousewheel' -}}; +Element.Events = { + mousewheel: { + base: 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll' + } +}; + +var check = function(event){ + var related = event.relatedTarget; + if (related == null) return true; + if (!related) return false; + return (related != this && related.prefix != 'xul' && typeOf(this) != 'document' && !this.contains(related)); +}; if ('onmouseenter' in document.documentElement){ Element.NativeEvents.mouseenter = Element.NativeEvents.mouseleave = 2; + Element.MouseenterCheck = check; } else { - var check = function(event){ - var related = event.relatedTarget; - if (related == null) return true; - if (!related) return false; - return (related != this && related.prefix != 'xul' && typeOf(this) != 'document' && !this.contains(related)); - }; - Element.Events.mouseenter = { base: 'mouseover', condition: check @@ -4035,12 +4147,12 @@ if (!window.addEventListener){ Element.Events.change = { base: function(){ var type = this.type; - return (this.get('tag') == 'input' && (type == 'radio' || type == 'checkbox')) ? 'propertychange' : 'change' + return (this.get('tag') == 'input' && (type == 'radio' || type == 'checkbox')) ? 'propertychange' : 'change'; }, condition: function(event){ - return this.type != 'radio' || (event.event.propertyName == 'checked' && this.checked); + return event.type != 'propertychange' || event.event.propertyName == 'checked'; } - } + }; } /**/ @@ -4080,10 +4192,12 @@ var bubbleUp = function(self, match, fn, event, target){ var map = { mouseenter: { - base: 'mouseover' + base: 'mouseover', + condition: Element.MouseenterCheck }, mouseleave: { - base: 'mouseout' + base: 'mouseout', + condition: Element.MouseenterCheck }, focus: { base: 'focus' + (eventListenerSupport ? '' : 'in'), @@ -4190,8 +4304,8 @@ var delegation = { }; var elementEvent = Element.Events[_type]; - if (elementEvent && elementEvent.condition){ - var __match = match, condition = elementEvent.condition; + if (_map.condition || elementEvent && elementEvent.condition){ + var __match = match, condition = _map.condition || elementEvent.condition; match = function(target, event){ return __match(target, event) && condition.call(target, event, type); }; @@ -4226,7 +4340,7 @@ var delegation = { if (_map.remove) _map.remove(this, _uid); delete stored[_uid]; storage[_type] = stored; - return removeEvent.call(this, type, delegator); + return removeEvent.call(this, type, delegator, _map.capture); } var __uid, s; @@ -4344,7 +4458,9 @@ Element.implement({ }, getOffsets: function(){ - if (this.getBoundingClientRect && !Browser.Platform.ios){ + var hasGetBoundingClientRect = this.getBoundingClientRect; + + if (hasGetBoundingClientRect){ var bound = this.getBoundingClientRect(), html = document.id(this.getDocument().documentElement), htmlScroll = html.getScroll(), @@ -4364,27 +4480,9 @@ Element.implement({ position.x += element.offsetLeft; position.y += element.offsetTop; - if (Browser.firefox){ - if (!borderBox(element)){ - position.x += leftBorder(element); - position.y += topBorder(element); - } - var parent = element.parentNode; - if (parent && styleString(parent, 'overflow') != 'visible'){ - position.x += leftBorder(parent); - position.y += topBorder(parent); - } - } else if (element != this && Browser.safari){ - position.x += leftBorder(element); - position.y += topBorder(element); - } - element = element.offsetParent; } - if (Browser.firefox && !borderBox(this)){ - position.x -= leftBorder(this); - position.y -= topBorder(this); - } + return position; }, @@ -4666,13 +4764,17 @@ var Fx = this.Fx = new Class({ }, resume: function(){ - if ((this.frame < this.frames) && !this.isRunning()) pushInstance.call(this, this.options.fps); + if (this.isPaused()) pushInstance.call(this, this.options.fps); return this; }, isRunning: function(){ var list = instances[this.options.fps]; return list && list.contains(this); + }, + + isPaused: function(){ + return (this.frame < this.frames) && !this.isRunning(); } }); @@ -4745,7 +4847,7 @@ Fx.CSS = new Class({ from = element.getStyle(property); var unit = this.options.unit; // adapted from: https://github.com/ryanmorr/fx/blob/master/fx.js#L299 - if (unit && from.slice(-unit.length) != unit && parseFloat(from) != 0){ + if (unit && from && typeof from == 'string' && from.slice(-unit.length) != unit && parseFloat(from) != 0){ element.setStyle(property, to + unit); var value = element.getComputedStyle(property); // IE and Opera support pixelLeft or pixelWidth @@ -4817,11 +4919,13 @@ Fx.CSS = new Class({ search: function(selector){ if (Fx.CSS.Cache[selector]) return Fx.CSS.Cache[selector]; var to = {}, selectorTest = new RegExp('^' + selector.escapeRegExp() + '$'); - Array.each(document.styleSheets, function(sheet, j){ - var href = sheet.href; - if (href && href.contains('://') && !href.contains(document.domain)) return; - var rules = sheet.rules || sheet.cssRules; + + var searchStyles = function(rules){ Array.each(rules, function(rule, i){ + if (rule.media){ + searchStyles(rule.rules || rule.cssRules); + return; + } if (!rule.style) return; var selectorText = (rule.selectorText) ? rule.selectorText.replace(/^\w+/, function(m){ return m.toLowerCase(); @@ -4833,6 +4937,13 @@ Fx.CSS = new Class({ to[style] = ((/^rgb/).test(value)) ? value.rgbToHex() : value; }); }); + }; + + Array.each(document.styleSheets, function(sheet, j){ + var href = sheet.href; + if (href && href.indexOf('://') > -1 && href.indexOf(document.domain) == -1) return; + var rules = sheet.rules || sheet.cssRules; + searchStyles(rules); }); return Fx.CSS.Cache[selector] = to; } @@ -5369,10 +5480,10 @@ var Request = this.Request = new Class({ if (trimPosition > -1 && (trimPosition = url.indexOf('#')) > -1) url = url.substr(0, trimPosition); if (this.options.noCache) - url += (url.contains('?') ? '&' : '?') + String.uniqueID(); + url += (url.indexOf('?') > -1 ? '&' : '?') + String.uniqueID(); - if (data && method == 'get'){ - url += (url.contains('?') ? '&' : '?') + data; + if (data && (method == 'get' || method == 'delete')){ + url += (url.indexOf('?') > -1 ? '&' : '?') + data; data = null; } @@ -5526,10 +5637,14 @@ JSON.encode = JSON.stringify ? function(obj){ return null; }; +JSON.secure = true; + + JSON.decode = function(string, secure){ if (!string || typeOf(string) != 'string') return null; - - if (secure || JSON.secure){ + + if (secure == null) secure = JSON.secure; + if (secure){ if (JSON.parse) return JSON.parse(string); if (!JSON.validate(string)) throw new Error('JSON could not decode the input; security is enabled and the value is not secure.'); } diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index 77b3a7a1..2ff49cb3 100644 --- a/couchpotato/static/scripts/library/mootools_more.js +++ b/couchpotato/static/scripts/library/mootools_more.js @@ -1,6 +1,16 @@ -// MooTools: the javascript framework. -// Load this file's selection again by visiting: http://mootools.net/more/0f75cfbac1aabbedaba7630beef8d10c -// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical +/* +--- +MooTools: the javascript framework + +web build: + - http://mootools.net/more/0f75cfbac1aabbedaba7630beef8d10c + +packager build: + - packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical + +... +*/ + /* --- @@ -31,8 +41,8 @@ provides: [MooTools.More] */ MooTools.More = { - 'version': '1.4.0.1', - 'build': 'a4244edf2aa97ac8a196fc96082dd35af1abab87' + version: '1.5.0', + build: '73db5e24e6e9c5c87b3a27aebef2248053f7db37' }; @@ -48,7 +58,7 @@ license: MIT-style license authors: - Arian Stolwijk -requires: [Core/Class.Extras, Core/Slick.Parser, More/MooTools.More] +requires: [Core/Class.Extras, Core/Slick.Parser, MooTools.More] provides: [Events.Pseudos] @@ -211,7 +221,7 @@ authors: requires: - Core/Object - - /MooTools.More + - MooTools.More provides: [Object.Extras] @@ -280,8 +290,8 @@ authors: requires: - Core/Events - - /Object.Extras - - /MooTools.More + - Object.Extras + - MooTools.More provides: [Locale, Lang] @@ -444,7 +454,7 @@ authors: - Aaron Newton requires: - - /Locale + - Locale provides: [Locale.en-US.Date] @@ -1079,7 +1089,7 @@ authors: - Scott Kyle requires: - - /Date + - Date provides: [Date.Extras] @@ -1235,10 +1245,8 @@ var special = { 'S': /[ŠŞŚ]/g, 't': /[ťţ]/g, 'T': /[ŤŢ]/g, - 'ue': /[ü]/g, - 'UE': /[Ü]/g, - 'u': /[ùúûůµ]/g, - 'U': /[ÙÚÛŮ]/g, + 'u': /[ùúûůüµ]/g, + 'U': /[ÙÚÛŮÜ]/g, 'y': /[ÿý]/g, 'Y': /[ŸÝ]/g, 'z': /[žźż]/g, @@ -1263,7 +1271,16 @@ tidy = { '-': /[\u2013]/g, // '--': /[\u2014]/g, '»': /[\uFFFD]/g -}; +}, + +conversions = { + ms: 1, + s: 1000, + m: 6e4, + h: 36e5 +}, + +findUnits = /(\d*.?\d+)([msh]+)/; var walk = function(string, replacements){ var result = string, key; @@ -1325,6 +1342,13 @@ String.implement({ if (trail) string += trail; } return string; + }, + + ms: function(){ + // "Borrowed" from https://gist.github.com/1503944 + var units = findUnits.exec(this); + if (units == null) return Number(this); + return Number(units[1]) * conversions[units[2]]; } }); @@ -1348,8 +1372,8 @@ authors: requires: - Core/Element - - /String.Extras - - /MooTools.More + - String.Extras + - MooTools.More provides: [Element.Forms] @@ -1493,7 +1517,7 @@ authors: requires: - Core/Element.Style - Core/Element.Dimensions - - /MooTools.More + - MooTools.More provides: [Element.Measure] @@ -1710,13 +1734,15 @@ var local = Element.Position = { }, setOffsetOption: function(element, options){ - var parentOffset = {x: 0, y: 0}, - offsetParent = element.measure(function(){ - return document.id(this.getOffsetParent()); - }), - parentScroll = offsetParent.getScroll(); + var parentOffset = {x: 0, y: 0}; + var parentScroll = {x: 0, y: 0}; + var offsetParent = element.measure(function(){ + return document.id(this.getOffsetParent()); + }); if (!offsetParent || offsetParent == element.getDocument().body) return; + + parentScroll = offsetParent.getScroll(); parentOffset = offsetParent.measure(function(){ var position = this.getPosition(); if (this.getStyle('position') == 'fixed'){ @@ -1896,7 +1922,7 @@ authors: requires: - Core/Element.Style - - /MooTools.More + - MooTools.More provides: [Element.Shortcuts] @@ -1976,7 +2002,7 @@ requires: - Core/Fx - Core/Element.Event - Core/Element.Dimensions - - /MooTools.More + - MooTools.More provides: [Fx.Scroll] @@ -2014,7 +2040,6 @@ Fx.Scroll = new Class({ set: function(){ var now = Array.flatten(arguments); - if (Browser.firefox) now = [Math.round(now[0]), Math.round(now[1])]; // not needed anymore in newer firefox versions this.element.scrollTo(now[0], now[1]); return this; }, @@ -2148,7 +2173,7 @@ authors: requires: - Core/Fx - Core/Element.Style - - /MooTools.More + - MooTools.More provides: [Fx.Slide] @@ -2325,7 +2350,7 @@ requires: - Core/Element.Event - Core/Element.Style - Core/Element.Dimensions - - /MooTools.More + - MooTools.More provides: [Drag] ... @@ -2371,10 +2396,10 @@ var Drag = new Class({ this.mouse = {'now': {}, 'pos': {}}; this.value = {'start': {}, 'now': {}}; - this.selection = (Browser.ie) ? 'selectstart' : 'mousedown'; + this.selection = 'selectstart' in document ? 'selectstart' : 'mousedown'; - if (Browser.ie && !Drag.ondragstartFixed){ + if ('ondragstart' in document && !('FileReader' in window) && !Drag.ondragstartFixed){ document.ondragstart = Function.from(false); Drag.ondragstartFixed = true; } @@ -2559,7 +2584,7 @@ authors: requires: - Core/Element.Dimensions - - /Drag + - Drag provides: [Drag.Move] @@ -2586,10 +2611,7 @@ Drag.Move = new Class({ element = this.element; this.droppables = $$(this.options.droppables); - this.container = document.id(this.options.container); - - if (this.container && typeOf(this.container) != 'element') - this.container = document.id(this.container.getDocument().body); + this.setContainer(this.options.container); if (this.options.style){ if (this.options.modifiers.x == 'left' && this.options.modifiers.y == 'top'){ @@ -2606,6 +2628,13 @@ Drag.Move = new Class({ this.addEvent('start', this.checkDroppables, true); this.overed = null; }, + + setContainer: function(container) { + this.container = document.id(container); + if (this.container && typeOf(this.container) != 'element'){ + this.container = document.id(this.container.getDocument().body); + } + }, start: function(event){ if (this.container) this.options.limit = this.calculateLimit(); @@ -2670,7 +2699,9 @@ Drag.Move = new Class({ if (container != offsetParent){ left += containerMargin.left + offsetParentPadding.left; - top += ((Browser.ie6 || Browser.ie7) ? 0 : containerMargin.top) + offsetParentPadding.top; + if (!offsetParentPadding.left && left < 0) left = 0; + top += offsetParent == document.body ? 0 : containerMargin.top + offsetParentPadding.top; + if (!offsetParentPadding.top && top < 0) top = 0; } } else { left -= elementMargin.left; @@ -2754,7 +2785,7 @@ authors: requires: - Core/Fx.Morph - - /Drag.Move + - Drag.Move provides: [Sortables] @@ -2773,7 +2804,8 @@ var Sortables = new Class({ clone: false, revert: false, handle: false, - dragOptions: {} + dragOptions: {}, + unDraggableTags: ['button', 'input', 'a', 'textarea', 'select', 'option'] }, initialize: function(lists, options){ @@ -2839,6 +2871,24 @@ var Sortables = new Class({ return list; }, this)); }, + + getDroppableCoordinates: function (element){ + var offsetParent = element.getOffsetParent(); + var position = element.getPosition(offsetParent); + var scroll = { + w: window.getScroll(), + offsetParent: offsetParent.getScroll() + }; + position.x += scroll.offsetParent.x; + position.y += scroll.offsetParent.y; + + if (offsetParent.getStyle('position') == 'fixed'){ + position.x -= scroll.w.x; + position.y -= scroll.w.y; + } + + return position; + }, getClone: function(event, element){ if (!this.options.clone) return new Element(element.tagName).inject(document.body); @@ -2859,7 +2909,7 @@ var Sortables = new Class({ }); } - return clone.inject(this.list).setPosition(element.getPosition(element.getOffsetParent())); + return clone.inject(this.list).setPosition(this.getDroppableCoordinates(this.element)); }, getDroppables: function(){ @@ -2884,7 +2934,7 @@ var Sortables = new Class({ if ( !this.idle || event.rightClick || - ['button', 'input', 'a', 'textarea'].contains(event.target.get('tag')) + (!this.options.handle && this.options.unDraggableTags.contains(event.target.get('tag'))) ) return; this.idle = false; @@ -2915,14 +2965,16 @@ var Sortables = new Class({ end: function(){ this.drag.detach(); this.element.setStyle('opacity', this.opacity); + var self = this; if (this.effect){ var dim = this.element.getStyles('width', 'height'), clone = this.clone, - pos = clone.computePosition(this.element.getPosition(this.clone.getOffsetParent())); + pos = clone.computePosition(this.getDroppableCoordinates(clone)); var destroy = function(){ this.removeEvent('cancel', destroy); clone.destroy(); + self.reset(); }; this.effect.element = clone; @@ -2935,8 +2987,9 @@ var Sortables = new Class({ }).addEvent('cancel', destroy).chain(destroy); } else { this.clone.destroy(); + self.reset(); } - this.reset(); + }, reset: function(){ @@ -3125,7 +3178,7 @@ authors: requires: - Core/Request - - /MooTools.More + - MooTools.More provides: [Request.Periodical] From 876c60271082c2f701882db6108934a66ab1e104 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 12:29:31 +0200 Subject: [PATCH 401/468] Code cleanup --- couchpotato/__init__.py | 2 +- couchpotato/core/_base/downloader/main.py | 3 +++ .../_base/downloader/static/downloaders.js | 5 +++-- couchpotato/core/_base/updater/main.py | 2 +- couchpotato/core/downloaders/nzbvortex.py | 4 ++-- couchpotato/core/downloaders/sabnzbd.py | 2 +- couchpotato/core/downloaders/synology.py | 1 + couchpotato/core/downloaders/utorrent.py | 2 +- couchpotato/core/media/__init__.py | 2 +- couchpotato/core/media/_base/media/main.py | 1 - .../core/media/_base/providers/base.py | 3 +++ .../core/media/_base/providers/nzb/newznab.py | 2 +- .../_base/providers/torrent/ilovetorrents.py | 1 + .../_base/providers/torrent/torrentday.py | 1 - .../core/media/_base/search/static/search.css | 1 - couchpotato/core/media/movie/charts/main.py | 1 + .../core/media/movie/charts/static/charts.css | 1 - .../media/movie/providers/info/fanarttv.py | 17 +++++--------- .../media/movie/providers/info/themoviedb.py | 2 +- .../movie/providers/torrent/sceneaccess.py | 1 - .../media/movie/providers/trailer/base.py | 3 +++ .../core/media/movie/suggestion/main.py | 1 - couchpotato/core/plugins/base.py | 2 +- couchpotato/core/plugins/file.py | 4 ++-- couchpotato/core/plugins/log/main.py | 4 ++-- couchpotato/core/plugins/log/static/log.css | 2 +- couchpotato/core/plugins/renamer.py | 2 +- .../core/plugins/wizard/static/wizard.js | 4 ++-- couchpotato/core/settings.py | 1 - couchpotato/static/scripts/api.js | 4 ++-- .../static/scripts/library/question.js | 22 +++++++++---------- couchpotato/static/scripts/page/settings.js | 4 ++-- couchpotato/static/style/main.css | 2 ++ 33 files changed, 56 insertions(+), 53 deletions(-) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 092bd406..fb6b4dc3 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -45,7 +45,7 @@ class WebHandler(BaseHandler): self.write({'success': False, 'error': 'Failed returning results'}) -def addView(route, func, static = False): +def addView(route, func): views[route] = func diff --git a/couchpotato/core/_base/downloader/main.py b/couchpotato/core/_base/downloader/main.py index 80890e4a..70e5cc9c 100644 --- a/couchpotato/core/_base/downloader/main.py +++ b/couchpotato/core/_base/downloader/main.py @@ -72,6 +72,9 @@ class DownloaderBase(Provider): return return self.download(data = data, media = media, filedata = filedata) + def download(self, *args, **kwargs): + return False + def _getAllDownloadStatus(self, download_ids): if self.isDisabled(manual = True, data = {}): return diff --git a/couchpotato/core/_base/downloader/static/downloaders.js b/couchpotato/core/_base/downloader/static/downloaders.js index ec85ce3d..45215158 100644 --- a/couchpotato/core/_base/downloader/static/downloaders.js +++ b/couchpotato/core/_base/downloader/static/downloaders.js @@ -40,15 +40,16 @@ var DownloadersBase = new Class({ button.set('text', button_name); + var message; if(json.success){ - var message = new Element('span.success', { + message = new Element('span.success', { 'text': 'Connection successful' }).inject(button, 'after') } else { var msg_text = 'Connection failed. Check logs for details.'; if(json.hasOwnProperty('msg')) msg_text = json.msg; - var message = new Element('span.failed', { + message = new Element('span.failed', { 'text': msg_text }).inject(button, 'after') } diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 5a47fc06..27f9917a 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -10,7 +10,7 @@ from threading import RLock from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.helpers.encoding import ss, sp +from couchpotato.core.helpers.encoding import sp from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env diff --git a/couchpotato/core/downloaders/nzbvortex.py b/couchpotato/core/downloaders/nzbvortex.py index 92ece6d6..9094055f 100644 --- a/couchpotato/core/downloaders/nzbvortex.py +++ b/couchpotato/core/downloaders/nzbvortex.py @@ -130,7 +130,7 @@ class NZBVortex(DownloaderBase): url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call try: - data = self.urlopen('%s?%s' % (url, params), *args, verify_ssl = False, **kwargs) + data = self.urlopen('%s?%s' % (url, params), *args, **kwargs) if data: return json.loads(data) @@ -154,7 +154,7 @@ class NZBVortex(DownloaderBase): url = cleanHost(self.conf('host')) + 'api/app/apilevel' try: - data = self.urlopen(url, show_error = False, verify_ssl = False) + data = self.urlopen(url, show_error = False) self.api_level = float(json.loads(data).get('apilevel')) except URLError as e: if hasattr(e, 'code') and e.code == 403: diff --git a/couchpotato/core/downloaders/sabnzbd.py b/couchpotato/core/downloaders/sabnzbd.py index 57e1dca5..cd51cb87 100644 --- a/couchpotato/core/downloaders/sabnzbd.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -194,7 +194,7 @@ class Sabnzbd(DownloaderBase): 'output': 'json' })) - data = self.urlopen(url, timeout = 60, show_error = False, verify_ssl = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) + data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) if use_json: d = json.loads(data) if d.get('error'): diff --git a/couchpotato/core/downloaders/synology.py b/couchpotato/core/downloaders/synology.py index 125d750a..2c12536f 100644 --- a/couchpotato/core/downloaders/synology.py +++ b/couchpotato/core/downloaders/synology.py @@ -90,6 +90,7 @@ class SynologyRPC(object): self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port) self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port) + self.sid = None self.username = username self.password = password self.destination = destination diff --git a/couchpotato/core/downloaders/utorrent.py b/couchpotato/core/downloaders/utorrent.py index 7ff5930d..3164681c 100644 --- a/couchpotato/core/downloaders/utorrent.py +++ b/couchpotato/core/downloaders/utorrent.py @@ -168,7 +168,7 @@ class uTorrent(DownloaderBase): status = 'busy' if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000: status = 'seeding' - elif (torrent[1] & self.status_flags['ERROR']): + elif torrent[1] & self.status_flags['ERROR']: status = 'failed' elif torrent[4] == 1000: status = 'completed' diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index f0abd28c..6c17e6db 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,7 +1,7 @@ import os import traceback -from couchpotato import get_db, CPLog +from couchpotato import CPLog from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.plugins.base import Plugin diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index c1fdd92b..3c1bf694 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -125,7 +125,6 @@ class MediaPlugin(MediaBase): imdb_id = getImdb(str(media_id)) - media = None if imdb_id: media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc'] else: diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py index 6bcf3332..587545c8 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -129,6 +129,9 @@ class YarrProvider(Provider): else: return [] + def buildUrl(self, *args, **kwargs): + pass + def login(self): # Check if we are still logged in every hour diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 13655eed..628a55f2 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -91,7 +91,7 @@ class Base(NZBProvider, RSS): # Extract a password from the description password = re.search('(?:' + self.passwords_regex + ')(?: *)(?:\:|\=)(?: *)(.*?)\|\n|$', description, flags = re.I).group(1) if password: - name = name + ' {{%s}}' % password.strip() + name += ' {{%s}}' % password.strip() except: log.debug('Error getting details of "%s": %s', (name, traceback.format_exc())) diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index eac2c476..33edff7f 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -51,6 +51,7 @@ class Base(TorrentProvider): results_table = None data_split = splitString(data, ' .spinner, .mask{ text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; font-size: 15px; color: #FFF; } From a2da42877754775c79d1a16179009ef1aa3ea815 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 14:40:22 +0200 Subject: [PATCH 402/468] Chart css cleanup Tiny webkit scroll --- .../core/media/movie/charts/static/charts.css | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index 7ac372e2..e3f11fa4 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -3,26 +3,43 @@ margin-bottom: 30px; } -.charts > h2 { - height: 40px; -} + .charts ::-webkit-scrollbar { + width: 5px; + position: absolute; + } -.charts .chart { - display: inline-block; - width: 50%; - vertical-align: top; - max-height: 510px; - overflow: hidden; - scrollbar-base-color: #4e5969; -} + .charts ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.4); + -webkit-border-radius: 5px; + border-radius: 5px; + } -.charts .chart:hover { - overflow-y: auto; -} + .charts ::-webkit-scrollbar-thumb { + -webkit-border-radius: 5px; + border-radius: 5px; + background: rgba(255,255,255,0.3); + } -.charts .chart .media_result.hidden { - display: none; -} + .charts > h2 { + height: 40px; + } + + .charts .chart { + display: inline-block; + width: 50%; + vertical-align: top; + max-height: 510px; + overflow: hidden; + scrollbar-base-color: #4e5969; + } + + .charts .chart:hover { + overflow-y: auto; + } + + .charts .chart .media_result.hidden { + display: none; + } .charts .refresh { clear:both; @@ -36,30 +53,30 @@ text-align:center; } - .charts .refresh a { - text-align: center; - padding: 0; - display: none; - width: 30px; - height: 30px; - position: absolute; - right: 10px; - top: -40px; - opacity: .7; - } + .charts .refresh a { + text-align: center; + padding: 0; + display: none; + width: 30px; + height: 30px; + position: absolute; + right: 10px; + top: -40px; + opacity: .7; + } - .charts .refresh a:hover { - opacity: 1; - } + .charts .refresh a:hover { + opacity: 1; + } -.charts p.no_charts_enabled { - padding: 0.7em 1em; - display: none; -} + .charts p.no_charts_enabled { + padding: 0.7em 1em; + display: none; + } -.charts .chart h3 a { - color: #fff; -} + .charts .chart h3 a { + color: #fff; + } .charts .chart .media_result { From dcd0364ecc01831c58968867a43d83167a4f0cdc Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 14:59:06 +0200 Subject: [PATCH 403/468] Re-use tiny scroller for webkit --- .../core/media/movie/_base/static/movie.css | 4 ---- .../core/media/movie/_base/static/movie.js | 2 +- .../core/media/movie/charts/static/charts.css | 22 ----------------- .../core/media/movie/charts/static/charts.js | 6 +++-- couchpotato/static/style/main.css | 24 +++++++++++++++++++ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index 05fcddf4..52f503c3 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -357,12 +357,8 @@ top: 30px; clear: both; bottom: 30px; - overflow: hidden; position: absolute; } - .movies .data:hover .description { - overflow: auto; - } .movies.list_list .movie:not(.details_view) .info .description, .movies.mass_edit_list .info .description, .movies.thumbs_list .info .description { diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 1a547f2f..679bbc2c 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -158,7 +158,7 @@ var Movie = new Class({ 'text': self.data.info.year || 'n/a' }) ), - self.description = new Element('div.description', { + self.description = new Element('div.description.tiny_scroll', { 'text': self.data.info.plot }), self.quality = new Element('div.quality', { diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css index e3f11fa4..261169f6 100644 --- a/couchpotato/core/media/movie/charts/static/charts.css +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -3,23 +3,6 @@ margin-bottom: 30px; } - .charts ::-webkit-scrollbar { - width: 5px; - position: absolute; - } - - .charts ::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.4); - -webkit-border-radius: 5px; - border-radius: 5px; - } - - .charts ::-webkit-scrollbar-thumb { - -webkit-border-radius: 5px; - border-radius: 5px; - background: rgba(255,255,255,0.3); - } - .charts > h2 { height: 40px; } @@ -29,14 +12,9 @@ width: 50%; vertical-align: top; max-height: 510px; - overflow: hidden; scrollbar-base-color: #4e5969; } - .charts .chart:hover { - overflow-y: auto; - } - .charts .chart .media_result.hidden { display: none; } diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js index d04f1c2c..a04e248f 100644 --- a/couchpotato/core/media/movie/charts/static/charts.js +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -22,9 +22,11 @@ var Charts = new Class({ 'events': { 'click': function(e) { e.preventDefault(); - self.el.getChildren('div.chart').destroy(); + + self.el.getElements('.chart').destroy(); self.el_refreshing_text.show(); self.el_refresh_link.hide(); + self.api_request = Api.request('charts.view', { 'data': { 'force_update': 1 }, 'onComplete': self.fill.bind(self) @@ -72,7 +74,7 @@ var Charts = new Class({ Object.each(json.charts, function(chart){ - var c = new Element('div.chart').grab( + var c = new Element('div.chart.tiny_scroll').grab( new Element('h3').grab( new Element('a', { 'text': chart.name, 'href': chart.url diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 84f933b5..c7850b22 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -53,6 +53,30 @@ input:-moz-placeholder { font-style: italic; } +.tiny_scroll { + overflow: hidden; +} + + .tiny_scroll:hover { + overflow-y: auto; + } + + .tiny_scroll::-webkit-scrollbar { + width: 5px; + } + + .tiny_scroll::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.4); + -webkit-border-radius: 5px; + border-radius: 5px; + } + + .tiny_scroll::-webkit-scrollbar-thumb { + -webkit-border-radius: 5px; + border-radius: 5px; + background: rgba(255,255,255,0.3); + } + a img { border:none; } From 963ce356fb03f04007ac684572f3a1d68d578b11 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 15:19:25 +0200 Subject: [PATCH 404/468] MediaBrowser metadata --- .../movie/providers/metadata/mediabrowser.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 couchpotato/core/media/movie/providers/metadata/mediabrowser.py diff --git a/couchpotato/core/media/movie/providers/metadata/mediabrowser.py b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py new file mode 100644 index 00000000..02a14c35 --- /dev/null +++ b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py @@ -0,0 +1,36 @@ +import os + +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData + + +autoload = 'WindowsMediaCenter' + + +class MediaBrowser(MovieMetaData): + + def getThumbnailName(self, name, root, i): + return os.path.join(root, 'folder.jpg') + + def getFanartName(self, name, root, i): + return os.path.join(root, 'backdrop.jpg') + + +config = [{ + 'name': 'mediabrowser', + 'groups': [ + { + 'tab': 'renamer', + 'subtab': 'metadata', + 'name': 'mediabrowser_metadata', + 'label': 'MediaBrowser', + 'description': 'Generate folder.jpg and backdrop.jpg', + 'options': [ + { + 'name': 'meta_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] From 9b7c1db509b37a9d626af42123daeba9751d4ebf Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 15:20:21 +0200 Subject: [PATCH 405/468] Sony PS3 metadata --- .../media/movie/providers/metadata/ps3.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 couchpotato/core/media/movie/providers/metadata/ps3.py diff --git a/couchpotato/core/media/movie/providers/metadata/ps3.py b/couchpotato/core/media/movie/providers/metadata/ps3.py new file mode 100644 index 00000000..0ff0d854 --- /dev/null +++ b/couchpotato/core/media/movie/providers/metadata/ps3.py @@ -0,0 +1,33 @@ +import os + +from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData + + +autoload = 'WindowsMediaCenter' + + +class SonyPS3(MovieMetaData): + + def getThumbnailName(self, name, root, i): + return os.path.join(root, 'cover.jpg') + + +config = [{ + 'name': 'sonyps3', + 'groups': [ + { + 'tab': 'renamer', + 'subtab': 'metadata', + 'name': 'sonyps3_metadata', + 'label': 'Sony PS3', + 'description': 'Generate cover.jpg', + 'options': [ + { + 'name': 'meta_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] From 60fb3e33aed4d79d89bc14d8b25e791f888b8aea Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 15:22:07 +0200 Subject: [PATCH 406/468] Sony PS3 metadata --- couchpotato/core/media/movie/providers/metadata/mediabrowser.py | 2 +- couchpotato/core/media/movie/providers/metadata/ps3.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/providers/metadata/mediabrowser.py b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py index 02a14c35..6e40e4c1 100644 --- a/couchpotato/core/media/movie/providers/metadata/mediabrowser.py +++ b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py @@ -3,7 +3,7 @@ import os from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData -autoload = 'WindowsMediaCenter' +autoload = 'MediaBrowser' class MediaBrowser(MovieMetaData): diff --git a/couchpotato/core/media/movie/providers/metadata/ps3.py b/couchpotato/core/media/movie/providers/metadata/ps3.py index 0ff0d854..05df0a53 100644 --- a/couchpotato/core/media/movie/providers/metadata/ps3.py +++ b/couchpotato/core/media/movie/providers/metadata/ps3.py @@ -3,7 +3,7 @@ import os from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData -autoload = 'WindowsMediaCenter' +autoload = 'SonyPS3' class SonyPS3(MovieMetaData): From 553f8d6ccdf341b80737a339dc5e46a57fa3e4ef Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 17:05:08 +0200 Subject: [PATCH 407/468] Ignore ETA every 7 days on search --- couchpotato/core/media/movie/searcher.py | 37 ++++++++++++++++++--- couchpotato/core/notifications/base.py | 1 + couchpotato/core/notifications/core/main.py | 1 + 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index a5b1f477..56a0ff9b 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -128,6 +128,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): found_releases = [] previous_releases = movie.get('releases', []) too_early_to_search = [] + outside_eta_results = 0 + alway_search = self.conf('always_search') + ignore_eta = False default_title = getTitle(movie) if not default_title: @@ -135,6 +138,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('media.delete', movie['_id'], single = True) return + # Ignore eta once every 7 days + if not alway_search: + prop_name = 'last_ignored_eta.%s' % movie['_id'] + last_ignored_eta = float(Env.prop(prop_name, default = 0)) + if last_ignored_eta > time.time() - 604800: + ignore_eta = True + fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) db = get_db() @@ -154,9 +164,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase): index += 1 - if not self.conf('always_search') and not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']): + could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']) + if not alway_search and could_not_be_released: too_early_to_search.append(q_identifier) - continue + + # Skip release, if ETA isn't ignored + if not ignore_eta: + continue has_better_quality = 0 @@ -177,15 +191,19 @@ class MovieSearcher(SearcherBase, MovieTypeBase): break quality = fireEvent('quality.single', identifier = q_identifier, single = True) - log.info('Search for %s in %s', (default_title, quality['label'])) + log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if alway_search or ignore_eta else '')) # Extend quality with profile customs quality['custom'] = quality_custom results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] - if len(results) == 0: + results_count = len(results) + if results_count == 0: log.debug('Nothing found for %s in %s', (default_title, quality['label'])) + # Keep track of releases found outside ETA window + outside_eta_results += results_count if could_not_be_released else 0 + # Check if movie isn't deleted while searching if not fireEvent('media.get', movie.get('_id'), single = True): break @@ -193,8 +211,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase): # Add them to this movie releases list found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True) + # Don't trigger download, but notify user of available releases + if could_not_be_released: + if results_count > 0: + log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) # Try find a valid result and download it - if fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): + elif fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): ret = True # Remove releases that aren't found anymore @@ -214,6 +236,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if len(too_early_to_search) > 0: log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) + if outside_eta_results > 0: + log.info('Found %s releases, but before ETA. Use dashboard to download manually', outside_eta_results) + message = 'Found %s releases for "%s" before ETA. Check them out on the dashboard.' % (outside_eta_results, default_title) + fireEvent('media.available', message = message, data = {}) + fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) return ret diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 0bfb337c..725704e0 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -15,6 +15,7 @@ class Notification(Provider): test_message = 'ZOMG Lazors Pewpewpew!' listen_to = [ + 'media.available', 'renamer.after', 'movie.snatched', 'updater.available', 'updater.updated', 'core.message.important', diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 526099fd..5190218e 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -28,6 +28,7 @@ class CoreNotifier(Notification): m_lock = None listen_to = [ + 'media.available', 'renamer.after', 'movie.snatched', 'updater.available', 'updater.updated', 'core.message', 'core.message.important', From f68c35694457230b200ec2d9e8c61d57a26f410b Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 21:07:02 +0200 Subject: [PATCH 408/468] Update title index --- couchpotato/core/media/_base/media/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index fd1affca..1b77b706 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -99,7 +99,7 @@ from couchpotato.core.helpers.encoding import simplifyString""" class TitleIndex(TreeBasedIndex): - _version = 3 + _version = 4 custom_header = """from CodernityDB.tree_index import TreeBasedIndex from string import ascii_letters @@ -128,7 +128,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" title = title[len(prefix):] break - return str(nr_prefix + title).ljust(32, '_')[:32] + return str(nr_prefix + title).ljust(32, ' ')[:32] class StartsWithIndex(TreeBasedIndex): From 5d3b0deb4d18c76b0d55bf0f5fd763bf03747e57 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 21:10:20 +0200 Subject: [PATCH 409/468] Simpler progress update --- couchpotato/core/plugins/manage.py | 10 +++++----- couchpotato/core/plugins/scanner.py | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 5b1cf0ef..ff69727c 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -210,14 +210,14 @@ class Manage(Plugin): 'to_go': total_found, }) + self.updateProgress(folder, to_go) + if group['media'] and group['identifier']: added_identifiers.append(group['identifier']) # Add it to release and update the info fireEvent('release.add', group = group, update_info = False) fireEvent('movie.update_info', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier'])) - else: - self.updateProgress(folder) return addToLibrary @@ -228,7 +228,6 @@ class Manage(Plugin): if not self.in_progress or self.shuttingDown(): return - self.updateProgress(folder) total = self.in_progress[folder]['total'] movie_dict = fireEvent('media.get', identifier, single = True) @@ -236,10 +235,11 @@ class Manage(Plugin): return afterUpdate - def updateProgress(self, folder): + def updateProgress(self, folder, to_go): pr = self.in_progress[folder] - pr['to_go'] -= 1 + if to_go < pr['to_go']: + pr['to_go'] = to_go avg = (time.time() - pr['started']) / (pr['total'] - pr['to_go']) pr['eta'] = tryInt(avg * pr['to_go']) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index b75f0bcc..0b461862 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -364,6 +364,7 @@ class Scanner(Plugin): if return_ignored is False and identifier in ignored_identifiers: log.debug('Ignore file found, ignoring release: %s', identifier) + total_found -= 1 continue # Group extra (and easy) files first @@ -384,6 +385,7 @@ class Scanner(Plugin): if len(group['files']['movie']) == 0: log.error('Couldn\'t find any movie files for %s', identifier) + total_found -= 1 continue log.debug('Getting metadata for %s', identifier) @@ -429,7 +431,7 @@ class Scanner(Plugin): # Notify parent & progress on something found if on_found: - on_found(group, total_found, total_found - len(processed_movies)) + on_found(group, total_found, len(valid_files)) # Wait for all the async events calm down a bit while threading.activeCount() > 100 and not self.shuttingDown(): From fdec80f6769a0e589c74905be53089536f46b856 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 21:21:32 +0200 Subject: [PATCH 410/468] Set last_force_eta time --- couchpotato/core/media/movie/searcher.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 56a0ff9b..f8e27244 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -138,14 +138,15 @@ class MovieSearcher(SearcherBase, MovieTypeBase): fireEvent('media.delete', movie['_id'], single = True) return + fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) + # Ignore eta once every 7 days if not alway_search: prop_name = 'last_ignored_eta.%s' % movie['_id'] last_ignored_eta = float(Env.prop(prop_name, default = 0)) if last_ignored_eta > time.time() - 604800: ignore_eta = True - - fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) + Env.prop(prop_name, value = time.time()) db = get_db() @@ -238,7 +239,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if outside_eta_results > 0: log.info('Found %s releases, but before ETA. Use dashboard to download manually', outside_eta_results) - message = 'Found %s releases for "%s" before ETA. Check them out on the dashboard.' % (outside_eta_results, default_title) + message = 'Found %s releases for "%s" before ETA. You can check them out on the dashboard.' % (outside_eta_results, default_title) fireEvent('media.available', message = message, data = {}) fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) From 2d243d51e4bacbf74e0b819ad8ec59461c24b473 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 22:03:38 +0200 Subject: [PATCH 411/468] Ignore ETA on manual refresh --- couchpotato/core/media/__init__.py | 2 +- couchpotato/core/media/movie/searcher.py | 16 +++++++++------- couchpotato/core/plugins/release/main.py | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 6c17e6db..4a3eb684 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -28,7 +28,7 @@ class MediaBase(Plugin): media = fireEvent('media.get', media_id, single = True) event_name = '%s.searcher.single' % media.get('type') - fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id)) + fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id), manual = True) except: log.error('Failed creating onComplete: %s', traceback.format_exc()) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index f8e27244..4b2d2e4b 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -58,13 +58,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase): def searchAllView(self, **kwargs): - fireEventAsync('movie.searcher.all') + fireEventAsync('movie.searcher.all', manual = True) return { 'success': not self.in_progress } - def searchAll(self): + def searchAll(self, manual = False): if self.in_progress: log.info('Search already in progress') @@ -91,7 +91,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): media = fireEvent('media.get', media_id, single = True) try: - self.single(media, search_protocols) + self.single(media, search_protocols, manual = manual) except IndexError: log.error('Forcing library update for %s, if you see this often, please report: %s', (getIdentifier(media), traceback.format_exc())) fireEvent('movie.update_info', media_id) @@ -130,7 +130,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): too_early_to_search = [] outside_eta_results = 0 alway_search = self.conf('always_search') - ignore_eta = False + ignore_eta = manual default_title = getTitle(movie) if not default_title: @@ -217,7 +217,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if results_count > 0: log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) # Try find a valid result and download it - elif fireEvent('release.try_download_result', results, movie, quality_custom, manual, single = True): + elif fireEvent('release.try_download_result', results, movie, quality_custom, single = True): ret = True # Remove releases that aren't found anymore @@ -239,8 +239,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if outside_eta_results > 0: log.info('Found %s releases, but before ETA. Use dashboard to download manually', outside_eta_results) - message = 'Found %s releases for "%s" before ETA. You can check them out on the dashboard.' % (outside_eta_results, default_title) - fireEvent('media.available', message = message, data = {}) + + if not manual: + message = 'Found %s releases for "%s" before ETA. You can check them out on the dashboard.' % (outside_eta_results, default_title) + fireEvent('media.available', message = message, data = {}) fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 0d22c165..970e90df 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -341,7 +341,7 @@ class Release(Plugin): return True - def tryDownloadResult(self, results, media, quality_custom, manual = False): + def tryDownloadResult(self, results, media, quality_custom): wait_for = False let_through = False @@ -375,7 +375,7 @@ class Release(Plugin): wait_for = True continue - downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True) + downloaded = fireEvent('release.download', data = rel, media = media, single = True) if downloaded is True: return True elif downloaded != 'try_next': From 41052ae50826a4d29b817f65cc9053fcad204084 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 22:14:13 +0200 Subject: [PATCH 412/468] Use same before ETA message --- couchpotato/core/media/movie/searcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 4b2d2e4b..24aa73e2 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -238,10 +238,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) if outside_eta_results > 0: - log.info('Found %s releases, but before ETA. Use dashboard to download manually', outside_eta_results) + message = 'Found %s releases for "%s" before ETA. Select and download via the dashboard.' % (outside_eta_results, default_title) + log.info(message) if not manual: - message = 'Found %s releases for "%s" before ETA. You can check them out on the dashboard.' % (outside_eta_results, default_title) fireEvent('media.available', message = message, data = {}) fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) From c0297f10cb188b12bbaf97a6f4329ea7d7da8024 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 22:24:11 +0200 Subject: [PATCH 413/468] Force download on "best release" selection --- couchpotato/core/media/movie/searcher.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 24aa73e2..81d62004 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -109,7 +109,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.in_progress = False - def single(self, movie, search_protocols = None, manual = False): + def single(self, movie, search_protocols = None, manual = False, force_download = False): # Find out search type try: @@ -216,8 +216,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if could_not_be_released: if results_count > 0: log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) + # Try find a valid result and download it - elif fireEvent('release.try_download_result', results, movie, quality_custom, single = True): + if (force_download or not could_not_be_released) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): ret = True # Remove releases that aren't found anymore @@ -364,13 +365,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase): def tryNextReleaseView(self, media_id = None, **kwargs): - trynext = self.tryNextRelease(media_id, manual = True) + trynext = self.tryNextRelease(media_id, manual = True, force_download = True) return { 'success': trynext } - def tryNextRelease(self, media_id, manual = False): + def tryNextRelease(self, media_id, manual = False, force_download = False): try: db = get_db() @@ -382,7 +383,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): movie_dict = fireEvent('media.get', media_id, single = True) log.info('Trying next release for: %s', getTitle(movie_dict)) - self.single(movie_dict, manual = manual) + self.single(movie_dict, manual = manual, force_download = force_download) return True From 83e8ae392d76f6c364dea9b575bc4e1ff7ea27e2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 23:17:59 +0200 Subject: [PATCH 414/468] Don't create a new "done" release on rename. fix #3250 --- couchpotato/core/plugins/manage.py | 9 ++-- couchpotato/core/plugins/release/main.py | 58 +++++++++++++++--------- couchpotato/core/plugins/renamer.py | 7 ++- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index ff69727c..97f66a78 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -32,7 +32,7 @@ class Manage(Plugin): # Add files after renaming def after_rename(message = None, group = None): if not group: group = {} - return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files']) + return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'], release_download = group['release_download']) addEvent('renamer.after', after_rename, priority = 110) addApiView('manage.update', self.updateLibraryView, docs = { @@ -254,7 +254,7 @@ class Manage(Plugin): return [] - def scanFilesToLibrary(self, folder = None, files = None): + def scanFilesToLibrary(self, folder = None, files = None, release_download = None): folder = os.path.normpath(folder) @@ -263,7 +263,10 @@ class Manage(Plugin): if groups: for group in groups.values(): if group.get('media'): - fireEvent('release.add', group = group) + if release_download and release_download.get('release_id'): + fireEvent('release.add', group = group, update_id = release_download.get('release_id')) + else: + fireEvent('release.add', group = group) def getDiskSpace(self): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 970e90df..08f02480 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -104,7 +104,7 @@ class Release(Plugin): elif rel['status'] in ['snatched', 'downloaded']: self.updateStatus(rel['_id'], status = 'ignore') - def add(self, group, update_info = True): + def add(self, group, update_info = True, update_id = None): try: db = get_db() @@ -120,27 +120,43 @@ class Release(Plugin): 'profile_id': None, }, search_after = False, update_after = update_info, notify_after = False, status = 'done', single = True) - # Add Release - release = { - '_t': 'release', - 'media_id': media['_id'], - 'identifier': release_identifier, - 'quality': group['meta_data']['quality'].get('identifier'), - 'is_3d': group['meta_data']['quality'].get('is_3d', 0), - 'last_edit': int(time.time()), - 'status': 'done' - } - try: - r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] - r['media_id'] = media['_id'] - except: - r = db.insert(release) + release = None + if update_id: + try: + release = db.get('id', update_id) + release.update({ + 'identifier': release_identifier, + 'last_edit': int(time.time()), + 'status': 'done', + }) + except: + log.error('Failed updating existing release: %s', traceback.format_exc()) + else: - # Update with ref and _id - release.update({ - '_id': r['_id'], - '_rev': r['_rev'], - }) + # Add Release + if not release: + release = { + '_t': 'release', + 'media_id': media['_id'], + 'identifier': release_identifier, + 'quality': group['meta_data']['quality'].get('identifier'), + 'is_3d': group['meta_data']['quality'].get('is_3d', 0), + 'last_edit': int(time.time()), + 'status': 'done' + } + + try: + r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] + r['media_id'] = media['_id'] + except: + log.error('Failed updating release by identifier: %s', traceback.format_exc()) + r = db.insert(release) + + # Update with ref and _id + release.update({ + '_id': r['_id'], + '_rev': r['_rev'], + }) # Empty out empty file groups release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v) diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 2da985fa..11585802 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -227,6 +227,7 @@ class Renamer(Plugin): for group_identifier in groups: group = groups[group_identifier] + group['release_download'] = None rename_files = {} remove_files = [] remove_releases = [] @@ -504,13 +505,15 @@ class Renamer(Plugin): if release_download['status'] == 'completed': # Set the release to downloaded fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) + group['release_download'] = release_download elif release_download['status'] == 'seeding': # Set the release to seeding fireEvent('release.update_status', release['_id'], status = 'seeding', single = True) elif release.get('identifier') == group['meta_data']['quality']['identifier']: - # Set the release to downloaded - fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) + # Set the release to downloaded + fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) + group['release_download'] = release_download # Remove leftover files if not remove_leftovers: # Don't remove anything From 8ae1e586148e74e5b355f29335da58d9a8ba36f7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 11 Jun 2014 23:34:17 +0200 Subject: [PATCH 415/468] Don't call parent init for synoindex --- couchpotato/core/notifications/nmj.py | 3 ++- couchpotato/core/notifications/synoindex.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/notifications/nmj.py b/couchpotato/core/notifications/nmj.py index 7579cafa..665837f1 100644 --- a/couchpotato/core/notifications/nmj.py +++ b/couchpotato/core/notifications/nmj.py @@ -22,10 +22,11 @@ class NMJ(Notification): # noinspection PyMissingConstructor def __init__(self): - addEvent('renamer.after', self.addToLibrary) addApiView(self.testNotifyName(), self.test) addApiView('notify.nmj.auto_config', self.autoConfig) + addEvent('renamer.after', self.addToLibrary) + def autoConfig(self, host = 'localhost', **kwargs): mount = '' diff --git a/couchpotato/core/notifications/synoindex.py b/couchpotato/core/notifications/synoindex.py index cd4ff8f9..b14e1a03 100644 --- a/couchpotato/core/notifications/synoindex.py +++ b/couchpotato/core/notifications/synoindex.py @@ -1,6 +1,7 @@ import os import subprocess +from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification @@ -16,7 +17,8 @@ class Synoindex(Notification): index_path = '/usr/syno/bin/synoindex' def __init__(self): - super(Synoindex, self).__init__() + addApiView(self.testNotifyName(), self.test) + addEvent('renamer.after', self.addToLibrary) def addToLibrary(self, message = None, group = None): From cbecb7430783501b446af94e0cab7496581e7104 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 14 Jun 2014 18:57:27 +0200 Subject: [PATCH 416/468] Show ETA on soon list. fix #2702 --- .../core/media/movie/_base/static/movie.css | 26 +++++++++++++++++++ .../core/media/movie/_base/static/movie.js | 19 ++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css index 52f503c3..311b111d 100644 --- a/couchpotato/core/media/movie/_base/static/movie.css +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -365,6 +365,32 @@ display: none; } + .movies .data .eta { + display: none; + } + + .movies.details_list .data .eta { + position: absolute; + bottom: 0; + right: 0; + display: block; + min-height: 20px; + text-align: right; + font-style: italic; + opacity: .8; + font-size: 11px; + } + + .movies.details_list .movie:hover .data .eta { + display: none; + } + + .movies.thumbs_list .data .eta { + display: block; + position: absolute; + bottom: 40px; + } + .movies .data .quality { position: absolute; bottom: 2px; diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index 679bbc2c..47880089 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -136,6 +136,21 @@ var Movie = new Class({ self.el.addClass('status_'+self.get('status')); + var eta = null, + eta_date = null, + now = Math.round(+new Date()/1000); + + if(self.data.info.release_date) + [self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){ + if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now))) + eta = timestamp; + }); + + if(eta){ + eta_date = new Date(eta * 1000); + eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear(); + } + self.el.adopt( self.select_checkbox = new Element('input[type=checkbox].inlay', { 'events': { @@ -161,6 +176,10 @@ var Movie = new Class({ self.description = new Element('div.description.tiny_scroll', { 'text': self.data.info.plot }), + self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', { + 'text': eta_date, + 'title': 'ETA' + }) : null, self.quality = new Element('div.quality', { 'events': { 'click': function(e){ From 9dd9f850c6a27498e3c18a48107bf28584cd3bc4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 14 Jun 2014 18:59:52 +0200 Subject: [PATCH 417/468] Treat seeding as "done" --- couchpotato/static/scripts/page/home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 7cde7d2a..aff8fbb3 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -54,7 +54,7 @@ Page.Home = new Class({ }) ), 'filter': { - 'release_status': 'snatched,seeding,missing,available,downloaded' + 'release_status': 'snatched,missing,available,downloaded' }, 'limit': null, 'onLoaded': function(){ From 58bd9cd7a1af2ff01d3657618d92a7f32e2a98ec Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 15 Jun 2014 14:59:02 +0200 Subject: [PATCH 418/468] Unable to hide & reorder profiles. fix #3437 --- couchpotato/core/plugins/quality/static/quality.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 29324c22..d233b1ce 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -8,6 +8,7 @@ var QualityBase = new Class({ self.qualities = data.qualities; + self.profiles_list = null; self.profiles = []; Array.each(data.profiles, self.createProfilesClass.bind(self)); @@ -35,7 +36,7 @@ var QualityBase = new Class({ }).pick(); } catch(e){} - + return {} }, @@ -106,14 +107,13 @@ var QualityBase = new Class({ createProfileOrdering: function(){ var self = this; - var profile_list; self.settings.createGroup({ 'label': 'Profile Defaults', 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' }).adopt( new Element('.ctrlHolder#profile_ordering').adopt( new Element('label[text=Order]'), - profile_list = new Element('ul'), + self.profiles_list = new Element('ul'), new Element('p.formHint', { 'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.
First one will be default.' }) @@ -133,7 +133,7 @@ var QualityBase = new Class({ 'text': profile.data.label }), new Element('span.handle') - ).inject(profile_list); + ).inject(self.profiles_list); new Form.Check(check); @@ -141,7 +141,7 @@ var QualityBase = new Class({ // Sortable var sorted_changed = false; - self.profile_sortable = new Sortables(profile_list, { + self.profile_sortable = new Sortables(self.profiles_list, { 'revert': true, 'handle': '.handle', 'opacity': 0.5, @@ -163,7 +163,7 @@ var QualityBase = new Class({ ids = [], hidden = []; - self.profile_sortable.list.getElements('li').each(function(el, nr){ + self.profiles_list.getElements('li').each(function(el, nr){ ids.include(el.get('data-id')); hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked'); }); From a196a499ae0ec48fe22fdba81bb54af46a13d09e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 15 Jun 2014 22:13:01 +0200 Subject: [PATCH 419/468] Only cache qualities if list length is correct --- couchpotato/core/plugins/quality/main.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 63049243..677ee48f 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -95,15 +95,14 @@ class QualityPlugin(Plugin): db = get_db() - qualities = db.all('quality', with_doc = True) - temp = [] - for quality in qualities: - quality = quality['doc'] - q = mergeDicts(self.getQuality(quality.get('identifier')), quality) + for quality in self.qualities: + quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc'] + q = mergeDicts(quality, quality_doc) temp.append(q) - self.cached_qualities = temp + if len(temp) == len(self.qualities): + self.cached_qualities = temp return temp From e659aba176bc2c2cb53bdde41503e2b4420c09e5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 15 Jun 2014 22:22:34 +0200 Subject: [PATCH 420/468] Clean .pyc files before starting --- CouchPotato.py | 7 +++++- couchpotato/core/_base/updater/main.py | 29 +++---------------------- couchpotato/core/helpers/variable.py | 30 +++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/CouchPotato.py b/CouchPotato.py index f21b9393..f5b9c046 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -19,7 +19,12 @@ base_path = dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(base_path, 'libs')) from couchpotato.environment import Env -from couchpotato.core.helpers.variable import getDataDir +from couchpotato.core.helpers.variable import getDataDir, removePyc + + +# Remove pyc files before dynamic load (sees .pyc files regular .py modules) +removePyc(base_path) + class Loader(object): diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 27f9917a..093977da 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -11,6 +11,7 @@ from threading import RLock from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import sp +from couchpotato.core.helpers.variable import removePyc from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env @@ -143,7 +144,7 @@ class Updater(Plugin): def doShutdown(self): if not Env.get('dev'): - self.updater.deletePyc(show_logs = False) + removePyc(Env.get('app_dir'), show_logs = False) return super(Updater, self).doShutdown() @@ -181,30 +182,6 @@ class BaseUpdater(Plugin): def check(self): pass - def deletePyc(self, only_excess = True, show_logs = True): - - for root, dirs, files in os.walk(Env.get('app_dir')): - - pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) - py_files = set(filter(lambda filename: filename.endswith('.py'), files)) - excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files - - for excess_pyc_file in excess_pyc_files: - full_path = os.path.join(root, excess_pyc_file) - if show_logs: log.debug('Removing old PYC file: %s', full_path) - try: - os.remove(full_path) - except: - log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc())) - - for dir_name in dirs: - full_path = os.path.join(root, dir_name) - if len(os.listdir(full_path)) == 0: - try: - os.rmdir(full_path) - except: - log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) - class GitUpdater(BaseUpdater): @@ -328,7 +305,7 @@ class SourceUpdater(BaseUpdater): data_dir = Env.get('data_dir') # Get list of files we want to overwrite - self.deletePyc() + removePyc(app_dir) existing_files = [] for root, subfiles, filenames in os.walk(app_dir): for filename in filenames: diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 16207a64..66e01f55 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -6,8 +6,9 @@ import random import re import string import sys +import traceback -from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss +from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp from couchpotato.core.logger import CPLog import six from six.moves import map, zip, filter @@ -313,3 +314,30 @@ under_pat = re.compile(r'_([a-z])') def underscoreToCamel(name): return under_pat.sub(lambda x: x.group(1).upper(), name) + + +def removePyc(folder, only_excess = True, show_logs = True): + + folder = sp(folder) + + for root, dirs, files in os.walk(folder): + + pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) + py_files = set(filter(lambda filename: filename.endswith('.py'), files)) + excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files + + for excess_pyc_file in excess_pyc_files: + full_path = os.path.join(root, excess_pyc_file) + if show_logs: log.debug('Removing old PYC file: %s', full_path) + try: + os.remove(full_path) + except: + log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc())) + + for dir_name in dirs: + full_path = os.path.join(root, dir_name) + if len(os.listdir(full_path)) == 0: + try: + os.rmdir(full_path) + except: + log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) From 530d3cd91ecc79c384da922e5e2d2655242ee377 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 15 Jun 2014 22:37:18 +0200 Subject: [PATCH 421/468] Update rentals URL --- couchpotato/core/media/movie/providers/automation/imdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 9b4c1171..783e8f50 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -263,7 +263,7 @@ config = [{ 'name': 'automation_charts_rentals', 'type': 'bool', 'label': 'DVD Rentals', - 'description': 'Top DVD rentals chart', + 'description': 'Top DVD rentals chart', 'default': True, }, { @@ -312,7 +312,7 @@ config = [{ 'name': 'chart_display_rentals', 'type': 'bool', 'label': 'DVD Rentals', - 'description': 'Top DVD rentals chart', + 'description': 'Top DVD rentals chart', 'default': True, }, { From 3faece0b4cdac07c7efb4c47a2c6445c9ce58b08 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 16 Jun 2014 21:21:55 +0200 Subject: [PATCH 422/468] Don't log already deleted releases. --- couchpotato/core/plugins/manage.py | 12 ++++++------ couchpotato/core/plugins/release/main.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index 97f66a78..f0e0faf4 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -136,6 +136,7 @@ class Manage(Plugin): # Get movies with done status total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True) + deleted_releases = [] for done_movie in done_movies: if getIdentifier(done_movie) not in added_identifiers: fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all') @@ -165,12 +166,11 @@ class Manage(Plugin): already_used = used_files.get(release_file) if already_used: - # delete current one - if already_used.get('last_edit', 0) < release.get('last_edit', 0): - fireEvent('release.delete', release['_id'], single = True) - # delete previous one - else: - fireEvent('release.delete', already_used['_id'], single = True) + if release_id not in deleted_releases: + release_id = release['_id'] if already_used.get('last_edit', 0) < release.get('last_edit', 0) else already_used['_id'] + fireEvent('release.delete', release_id, single = True) + + deleted_releases.append(release_id) break else: used_files[release_file] = release diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 08f02480..db2b0c89 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -149,7 +149,7 @@ class Release(Plugin): r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] r['media_id'] = media['_id'] except: - log.error('Failed updating release by identifier: %s', traceback.format_exc()) + log.debug('Failed updating release by identifier "%s". Inserting new.', release_identifier) r = db.insert(release) # Update with ref and _id @@ -184,7 +184,7 @@ class Release(Plugin): db.delete(rel) return True except RecordDeleted: - log.error('Already deleted: %s', release_id) + log.debug('Already deleted: %s', release_id) return True except: log.error('Failed: %s', traceback.format_exc()) From 76126271fc743ff26a306c016e7287a7dc50b53e Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 16 Jun 2014 22:06:39 +0200 Subject: [PATCH 423/468] Don't add default profile if status is done --- couchpotato/core/media/movie/_base/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 4c5c2f29..2740e0dc 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -90,7 +90,7 @@ class MovieBase(MovieTypeBase): # Default profile and category default_profile = {} - if not params.get('profile_id'): + if not params.get('profile_id') and status != 'done': default_profile = fireEvent('profile.default', single = True) cat_id = params.get('category_id') From f65ddbbb9e35a3fccc521787cee8cd7d377f2292 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 16 Jun 2014 22:39:02 +0200 Subject: [PATCH 424/468] Encode environments args in html --- couchpotato/environment.py | 8 ++++++-- couchpotato/templates/index.html | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 658b744d..1000d489 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -2,6 +2,7 @@ import os from couchpotato.core.database import Database from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings @@ -38,8 +39,11 @@ class Env(object): return Env._debug @staticmethod - def get(attr): - return getattr(Env, '_' + attr) + def get(attr, unicode = False): + if unicode: + return toUnicode(getattr(Env, '_' + attr)) + else: + return getattr(Env, '_' + attr) @staticmethod def all(): diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index b4328001..0d8acbc1 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -73,10 +73,10 @@ App.setup({ 'base_url': {{ json_encode(Env.get('web_base')) }}, - 'args': {{ json_encode(Env.get('args')) }}, + 'args': {{ json_encode(Env.get('args', unicode = True)) }}, 'options': {{ json_encode(('%s' % Env.get('options'))) }}, - 'app_dir': {{ json_encode(Env.get('app_dir')) }}, - 'data_dir': {{ json_encode(Env.get('data_dir')) }}, + 'app_dir': {{ json_encode(Env.get('app_dir', unicode = True)) }}, + 'data_dir': {{ json_encode(Env.get('data_dir', unicode = True)) }}, 'pid': {{ json_encode(Env.getPid()) }}, 'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }} }); From cfb77a1076c9dc5a66b3809f2b530d84e9aed8e6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 14:22:42 +0200 Subject: [PATCH 425/468] Don't use extension to test for quality tags. fix #3457 --- couchpotato/core/plugins/quality/main.py | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 677ee48f..ef18647b 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -22,12 +22,12 @@ class QualityPlugin(Plugin): } qualities = [ - {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, - {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, + {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, + {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, - {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, - {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, - {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, + {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip')], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, + {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, + {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]}, @@ -256,6 +256,9 @@ class QualityPlugin(Plugin): cur_file = ss(cur_file) score = 0 + extension = words[-1] + words = words[:-1] + points = { 'identifier': 10, 'label': 10, @@ -275,7 +278,7 @@ class QualityPlugin(Plugin): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) score += points.get(tag_type) - if isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower(): + if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words: log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) score += points.get(tag_type) / 2 @@ -285,8 +288,8 @@ class QualityPlugin(Plugin): # Check extention for ext in quality.get('ext', []): - if ext == words[-1]: - log.debug('Found %s extension in %s', (ext, cur_file)) + if ext == extension: + log.debug('Found %s with .%s extension in %s', (quality['identifier'], ext, cur_file)) score += points['ext'] return score @@ -432,7 +435,9 @@ class QualityPlugin(Plugin): 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'}, 'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True}, '/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': False}, - '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True} + '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True}, + '/volume1/Public/3D/Moviename/Moviename (2009).3D.SBS.ts': {'size': 7500, 'quality': '1080p', 'is_3d': True}, + '/volume1/Public/Moviename/Moviename (2009).ts': {'size': 5500, 'quality': '1080p'}, } correct = 0 @@ -440,7 +445,10 @@ class QualityPlugin(Plugin): test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {} success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False) if not success: - log.error('%s failed check, thinks it\'s %s', (name, test_quality.get('identifier'))) + log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name, + test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''), + tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '') + )) correct += success From 664ce6421fd03ead21f03a99fef712d975b3a4de Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 15:21:08 +0200 Subject: [PATCH 426/468] Try only parse filename for release name --- couchpotato/core/plugins/scanner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index 0b461862..ce7facd7 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -903,6 +903,7 @@ class Scanner(Plugin): log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc())) # Backup to simple + release_name = os.path.basename(release_name.replace('\\', '/')) cleaned = ' '.join(re.split('\W+', simplifyString(release_name))) cleaned = re.sub(self.clean, ' ', cleaned) From 648ac7793ffc43cb7f450d05cbd11ef23586e546 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 15:21:30 +0200 Subject: [PATCH 427/468] Add multiple 3d tags to clean regex --- couchpotato/core/plugins/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index ce7facd7..d5937eae 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -105,7 +105,7 @@ class Scanner(Plugin): 'HDTV': ['hdtv'] } - clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ + clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 From 1857e047b0e4e4a1dfddf92dc144571c0a00a554 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 15:22:20 +0200 Subject: [PATCH 428/468] Remove moviename words when scanning for 3d tags. fix #3395 --- couchpotato/core/plugins/quality/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index ef18647b..ef885340 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -1,12 +1,12 @@ import traceback import re -from CodernityDB.database import RecordNotFound +from CodernityDB.database import RecordNotFound from couchpotato import get_db from couchpotato.api import addApiView -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt +from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.quality.index import QualityIndex @@ -192,7 +192,7 @@ class QualityPlugin(Plugin): # Create hash for cache cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) - cached = self.getCache(cache_key) + cached = None #self.getCache(cache_key) if cached and len(extra) == 0: return cached @@ -208,6 +208,10 @@ class QualityPlugin(Plugin): for cur_file in files: words = re.split('\W+', cur_file.lower()) + name_year = fireEvent('scanner.name_year', cur_file, file_name = cur_file, single = True) + if name_year and name_year.get('name'): + split_name = splitString(name_year.get('name'), ' ') + words = [x for x in words if x not in split_name] for quality in qualities: contains_score = self.containsTagScore(quality, words, cur_file) @@ -438,6 +442,9 @@ class QualityPlugin(Plugin): '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True}, '/volume1/Public/3D/Moviename/Moviename (2009).3D.SBS.ts': {'size': 7500, 'quality': '1080p', 'is_3d': True}, '/volume1/Public/Moviename/Moviename (2009).ts': {'size': 5500, 'quality': '1080p'}, + '/movies/BluRay HDDVD H.264 MKV 720p EngSub/QuiQui le fou (criterion collection #123, 1915)/QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'}, + 'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'}, + 'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) half-sbs 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True}, } correct = 0 From 3786b5435f05847e94cdf1786e9dda4509a59126 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 15:36:18 +0200 Subject: [PATCH 429/468] Only remove movie title from 3d words check --- couchpotato/core/plugins/quality/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index ef885340..dd820cf0 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -192,7 +192,7 @@ class QualityPlugin(Plugin): # Create hash for cache cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) - cached = None #self.getCache(cache_key) + cached = self.getCache(cache_key) if cached and len(extra) == 0: return cached @@ -209,13 +209,14 @@ class QualityPlugin(Plugin): for cur_file in files: words = re.split('\W+', cur_file.lower()) name_year = fireEvent('scanner.name_year', cur_file, file_name = cur_file, single = True) + threed_words = words if name_year and name_year.get('name'): split_name = splitString(name_year.get('name'), ' ') - words = [x for x in words if x not in split_name] + threed_words = [x for x in words if x not in split_name] for quality in qualities: contains_score = self.containsTagScore(quality, words, cur_file) - threedscore = self.contains3D(quality, words, cur_file) if quality.get('allow_3d') else (0, None) + threedscore = self.contains3D(quality, threed_words, cur_file) if quality.get('allow_3d') else (0, None) self.calcScore(score, quality, contains_score, threedscore) From dd61c7dc211852d910c627d5ad3cc8eda1faf953 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 16:27:22 +0200 Subject: [PATCH 430/468] Compact DB every 7 days if needed --- couchpotato/core/database.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/couchpotato/core/database.py b/couchpotato/core/database.py index 1b9501c2..d753d87f 100644 --- a/couchpotato/core/database.py +++ b/couchpotato/core/database.py @@ -26,7 +26,9 @@ class Database(object): addApiView('database.document.update', self.updateDocument) addApiView('database.document.delete', self.deleteDocument) + addEvent('database.setup.after', self.startup_compact) addEvent('database.setup_index', self.setupIndex) + addEvent('app.migrate', self.migrate) addEvent('app.after_shutdown', self.close) @@ -140,8 +142,14 @@ class Database(object): success = True try: + start = time.time() db = self.getDB() + size = float(db.get_db_details().get('size', 0)) + log.debug('Compacting database, current size: %sMB', round(size/1048576, 2)) + db.compact() + new_size = float(db.get_db_details().get('size', 0)) + log.debug('Done compacting database in %ss, new size: %sMB, saved: %sMB', (round(time.time()-start, 2), round(new_size/1048576, 2), round((size-new_size)/1048576, 2))) except: log.error('Failed compact: %s', traceback.format_exc()) success = False @@ -150,6 +158,18 @@ class Database(object): 'success': success } + # Compact on start + def startup_compact(self): + from couchpotato import Env + + db = self.getDB() + size = db.get_db_details().get('size') + prop_name = 'last_db_compact' + last_check = int(Env.prop(prop_name, default = 0)) + if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days + self.compact() + Env.prop(prop_name, value = int(time.time())) + def migrate(self): from couchpotato import Env From 67fbcc8238b951378ea4614aa318480f29f5c66a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 17 Jun 2014 23:32:27 +0200 Subject: [PATCH 431/468] Tag filter index --- couchpotato/core/media/_base/media/index.py | 21 +++++++++++++++++++++ couchpotato/core/media/_base/media/main.py | 18 ++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 1b77b706..2166c0ac 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -176,3 +176,24 @@ class MediaChildrenIndex(TreeBasedIndex): if data.get('_t') == 'media' and data.get('parent_id'): return data.get('parent_id'), None + +class MediaTagIndex(MultiTreeBasedIndex): + _version = 1 + + custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" + + def __init__(self, *args, **kwargs): + kwargs['key_format'] = '32s' + super(MediaTagIndex, self).__init__(*args, **kwargs) + + def make_key_value(self, data): + if data.get('_t') == 'media' and len(data.get('tags', [])) > 0: + + tags = set() + for tag in data.get('tags', []): + tags.add(self.make_key(tag)) + + return list(tags), None + + def make_key(self, key): + return md5(key).hexdigest() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 3c1bf694..bd0d53c5 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -9,7 +9,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex +from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex log = CPLog(__name__) @@ -21,6 +21,7 @@ class MediaPlugin(MediaBase): 'media': MediaIndex, 'media_search_title': TitleSearchIndex, 'media_status': MediaStatusIndex, + 'media_tag': MediaTagIndex, 'media_by_type': MediaTypeIndex, 'media_title': TitleIndex, 'media_startswith': StartsWithIndex, @@ -177,7 +178,7 @@ class MediaPlugin(MediaBase): log.debug('No media found with identifiers: %s', identifiers) - def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): + def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None): db = get_db() @@ -188,6 +189,8 @@ class MediaPlugin(MediaBase): release_status = [release_status] if types and not isinstance(types, (list, tuple)): types = [types] + if with_tags and not isinstance(with_tags, (list, tuple)): + with_tags = [with_tags] # query media ids if types: @@ -214,11 +217,17 @@ class MediaPlugin(MediaBase): # Add search filters if starts_with: - filter_by['starts_with'] = set() starts_with = toUnicode(starts_with.lower())[0] starts_with = starts_with if starts_with in ascii_lowercase else '#' filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)] + # Add tag filter + if with_tags: + filter_by['with_tags'] = set() + for tag in with_tags: + for x in db.get_many('media_tag', tag): + filter_by['with_tags'].add(x['_id']) + # Filter with search query if search: filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)] @@ -271,7 +280,8 @@ class MediaPlugin(MediaBase): release_status = splitString(kwargs.get('release_status')), status_or = kwargs.get('status_or') is not None, limit_offset = kwargs.get('limit_offset'), - starts_with = kwargs.get('starts_with'), + with_tags = kwargs.get('with_tags'), + starts_with = splitString(kwargs.get('starts_with')), search = kwargs.get('search') ) From 0b5dfe826abaeaeec521d824ed5f0ebfc0b4f73b Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 19 Jun 2014 14:15:42 +0200 Subject: [PATCH 432/468] Fix BitSoup parsing. close #3465 --- couchpotato/core/media/_base/providers/torrent/bitsoup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index f32e79e7..d70579e0 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -1,6 +1,6 @@ import traceback -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, SoupStrainer from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.providers.torrent.base import TorrentProvider @@ -20,6 +20,7 @@ class Base(TorrentProvider): } http_time_between_calls = 1 # Seconds + only_tables_tags = SoupStrainer('table') def _searchOnTitle(self, title, movie, quality, results): @@ -27,7 +28,7 @@ class Base(TorrentProvider): data = self.getHTMLData(url) if data: - html = BeautifulSoup(data) + html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags) try: result_table = html.find('table', attrs = {'class': 'koptekst'}) From 57ca5067ffd5b7f42db1cdf7f634e558f13c3861 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 19 Jun 2014 16:48:30 +0200 Subject: [PATCH 433/468] Insert themoviedb original_title in by default. --- couchpotato/core/media/movie/providers/info/themoviedb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/providers/info/themoviedb.py b/couchpotato/core/media/movie/providers/info/themoviedb.py index 16299f2e..4a397edd 100644 --- a/couchpotato/core/media/movie/providers/info/themoviedb.py +++ b/couchpotato/core/media/movie/providers/info/themoviedb.py @@ -153,8 +153,10 @@ class TheMovieDb(MovieProvider): movie_data = dict((k, v) for k, v in movie_data.items() if v) # Add alternative names + if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']: + movie_data['titles'].insert(0, movie_data['original_title']) + if extended: - movie_data['titles'].append(movie.originaltitle) for alt in movie.alternate_titles: alt_name = alt.title if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None: From 5797348bb379a92bd43864aa91eed703f0a3bcd1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 12:10:40 +0200 Subject: [PATCH 434/468] Update tag index --- couchpotato/core/media/_base/media/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/index.py b/couchpotato/core/media/_base/media/index.py index 2166c0ac..b40e162b 100644 --- a/couchpotato/core/media/_base/media/index.py +++ b/couchpotato/core/media/_base/media/index.py @@ -178,7 +178,7 @@ class MediaChildrenIndex(TreeBasedIndex): class MediaTagIndex(MultiTreeBasedIndex): - _version = 1 + _version = 2 custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" @@ -187,7 +187,7 @@ class MediaTagIndex(MultiTreeBasedIndex): super(MediaTagIndex, self).__init__(*args, **kwargs) def make_key_value(self, data): - if data.get('_t') == 'media' and len(data.get('tags', [])) > 0: + if data.get('_t') == 'media' and data.get('tags') and len(data.get('tags', [])) > 0: tags = set() for tag in data.get('tags', []): From 6f766aae8c424208614eddd255d032044e3a7e9d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 12:13:54 +0200 Subject: [PATCH 435/468] Tag and untag dashboard media --- couchpotato/core/media/_base/media/main.py | 40 ++++++++++++++++++++++ couchpotato/core/media/movie/_base/main.py | 1 + couchpotato/core/media/movie/searcher.py | 5 +++ couchpotato/core/plugins/release/main.py | 4 +++ couchpotato/core/plugins/renamer.py | 13 +++++++ couchpotato/static/scripts/page/home.js | 3 +- 6 files changed, 65 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index bd0d53c5..b4c8ced0 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -82,6 +82,8 @@ class MediaPlugin(MediaBase): addEvent('media.list', self.list) addEvent('media.delete', self.delete) addEvent('media.restatus', self.restatus) + addEvent('media.tag', self.tag) + addEvent('media.untag', self.unTag) def refresh(self, id = '', **kwargs): handlers = [] @@ -469,3 +471,41 @@ class MediaPlugin(MediaBase): return True except: log.error('Failed restatus: %s', traceback.format_exc()) + + def tag(self, media_id, tag): + + try: + db = get_db() + m = db.get('id', media_id) + + tags = m.get('tags') or [] + if tag not in tags: + tags.append(tag) + m['tags'] = tags + db.update(m) + + return True + except: + log.error('Failed tagging: %s', traceback.format_exc()) + + return False + + def unTag(self, media_id, tag): + + try: + db = get_db() + m = db.get('id', media_id) + + tags = m.get('tags') or [] + if tag in tags: + new_tags = list(set(tags)) + new_tags.remove(tag) + + m['tags'] = new_tags + db.update(m) + + return True + except: + log.error('Failed untagging: %s', traceback.format_exc()) + + return False diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 2740e0dc..9475ec09 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -149,6 +149,7 @@ class MovieBase(MovieTypeBase): m['profile_id'] = params.get('profile_id', default_profile.get('id')) m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) m['last_edit'] = int(time.time()) + m['tags'] = [] do_search = True db.update(m) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 81d62004..0f4b4179 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -131,6 +131,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): outside_eta_results = 0 alway_search = self.conf('always_search') ignore_eta = manual + total_result_count = 0 default_title = getTitle(movie) if not default_title: @@ -199,6 +200,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] results_count = len(results) + total_result_count += results_count if results_count == 0: log.debug('Nothing found for %s in %s', (default_title, quality['label'])) @@ -235,6 +237,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if self.shuttingDown() or ret: break + if total_result_count > 0: + fireEvent('media.tag', movie['_id'], 'recent', single = True) + if len(too_early_to_search) > 0: log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index db2b0c89..e3c93b42 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -104,6 +104,8 @@ class Release(Plugin): elif rel['status'] in ['snatched', 'downloaded']: self.updateStatus(rel['_id'], status = 'ignore') + fireEvent('media.untag', media.get('_id'), 'recent', single = True) + def add(self, group, update_info = True, update_id = None): try: @@ -346,6 +348,8 @@ class Release(Plugin): mdia['last_edit'] = int(time.time()) db.update(mdia) + fireEvent('media.tag', media['_id'], 'recent', single = True) + return True # Assume release downloaded diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py index 11585802..8b57103e 100644 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -457,9 +457,15 @@ class Renamer(Plugin): mdia['last_edit'] = int(time.time()) db.update(mdia) + # List movie on dashboard + fireEvent('media.tag', media['_id'], 'recent', single = True) + except: log.error('Failed marking movie finished: %s', (traceback.format_exc())) + # Mark media for dashboard + mark_as_recent = False + # Go over current movie releases for release in fireEvent('release.for_media', media['_id'], single = True): @@ -506,14 +512,21 @@ class Renamer(Plugin): # Set the release to downloaded fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) group['release_download'] = release_download + mark_as_recent = True elif release_download['status'] == 'seeding': # Set the release to seeding fireEvent('release.update_status', release['_id'], status = 'seeding', single = True) + mark_as_recent = True elif release.get('identifier') == group['meta_data']['quality']['identifier']: # Set the release to downloaded fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) group['release_download'] = release_download + mark_as_recent = True + + # Mark media for dashboard + if mark_as_recent: + fireEvent('media.tag', group['media'].get('_id'), 'recent', single = True) # Remove leftover files if not remove_leftovers: # Don't remove anything diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index aff8fbb3..792b4a07 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -54,7 +54,8 @@ Page.Home = new Class({ }) ), 'filter': { - 'release_status': 'snatched,missing,available,downloaded' + 'release_status': 'snatched,missing,available,downloaded,done,seeding', + 'with_tags': 'recent' }, 'limit': null, 'onLoaded': function(){ From 8e17b9aea59e8cf43714c80031ff380e5dcaaca1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 14:12:43 +0200 Subject: [PATCH 436/468] Remove BoxCar --- couchpotato/core/notifications/boxcar.py | 69 ------------------------ 1 file changed, 69 deletions(-) delete mode 100644 couchpotato/core/notifications/boxcar.py diff --git a/couchpotato/core/notifications/boxcar.py b/couchpotato/core/notifications/boxcar.py deleted file mode 100644 index a7acc882..00000000 --- a/couchpotato/core/notifications/boxcar.py +++ /dev/null @@ -1,69 +0,0 @@ -import time - -from couchpotato.core.helpers.encoding import toUnicode -from couchpotato.core.logger import CPLog -from couchpotato.core.notifications.base import Notification - - -log = CPLog(__name__) - -autoload = 'Boxcar' - - -class Boxcar(Notification): - - url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications' - - def notify(self, message = '', data = None, listener = None): - if not data: data = {} - - try: - message = message.strip() - - data = { - 'email': self.conf('email'), - 'notification[from_screen_name]': self.default_title, - 'notification[message]': toUnicode(message), - 'notification[from_remote_service_id]': int(time.time()), - } - - self.urlopen(self.url, data = data) - except: - log.error('Check your email and added services on boxcar.io') - return False - - log.info('Boxcar notification successful.') - return True - - def isEnabled(self): - return super(Boxcar, self).isEnabled() and self.conf('email') - - -config = [{ - 'name': 'boxcar', - 'groups': [ - { - 'tab': 'notifications', - 'list': 'notification_providers', - 'name': 'boxcar', - 'options': [ - { - 'name': 'enabled', - 'default': 0, - 'type': 'enabler', - }, - { - 'name': 'email', - 'description': 'Your Boxcar registration emailaddress.' - }, - { - 'name': 'on_snatch', - 'default': 0, - 'type': 'bool', - 'advanced': True, - 'description': 'Also send message when movie is snatched.', - }, - ], - } - ], -}] From 578b74f2c0068bb047c48dfec98da2dfe31ded18 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 14:14:10 +0200 Subject: [PATCH 437/468] Fix PushBullet url. fix #3470 --- couchpotato/core/notifications/pushbullet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/pushbullet.py b/couchpotato/core/notifications/pushbullet.py index 56cf2288..77738c32 100644 --- a/couchpotato/core/notifications/pushbullet.py +++ b/couchpotato/core/notifications/pushbullet.py @@ -14,7 +14,7 @@ autoload = 'Pushbullet' class Pushbullet(Notification): - url = 'https://api.pushbullet.com/api/%s' + url = 'https://api.pushbullet.com/v2/%s' def notify(self, message = '', data = None, listener = None): if not data: data = {} From 3f92ed0ea0d20bad145a48d810e8a7c381911ffd Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 14:33:20 +0200 Subject: [PATCH 438/468] Don't autodownload releases with no file size. fix #3467 --- couchpotato/core/plugins/release/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index e3c93b42..9e7e4ca7 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -375,7 +375,11 @@ class Release(Plugin): continue if rel['score'] <= 0: - log.info('Ignored, score to low: %s', rel['name']) + log.info('Ignored, score "%s" to low: %s', (rel['score'], rel['name'])) + continue + + if rel['size'] <= 50: + log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name'])) continue rel['wait_for'] = False From cf95e417f12a700be5c2a942567765a4c9a1b60b Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 17:11:11 +0200 Subject: [PATCH 439/468] Delete publichd --- .../media/_base/providers/torrent/publichd.py | 136 ------------------ .../media/movie/providers/torrent/publichd.py | 14 -- 2 files changed, 150 deletions(-) delete mode 100644 couchpotato/core/media/_base/providers/torrent/publichd.py delete mode 100644 couchpotato/core/media/movie/providers/torrent/publichd.py diff --git a/couchpotato/core/media/_base/providers/torrent/publichd.py b/couchpotato/core/media/_base/providers/torrent/publichd.py deleted file mode 100644 index 1090be2b..00000000 --- a/couchpotato/core/media/_base/providers/torrent/publichd.py +++ /dev/null @@ -1,136 +0,0 @@ -from urlparse import parse_qs -import re -import traceback - -from bs4 import BeautifulSoup -from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode -from couchpotato.core.helpers.variable import tryInt -from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider -import six - - -log = CPLog(__name__) - - -class Base(TorrentMagnetProvider): - - urls = { - 'test': 'https://publichd.se', - 'detail': 'https://publichd.se/index.php?page=torrent-details&id=%s', - 'search': 'https://publichd.se/index.php', - } - http_time_between_calls = 0 - - def search(self, movie, quality): - - if not quality.get('hd', False): - return [] - - return super(Base, self).search(movie, quality) - - def _search(self, media, quality, results): - - query = self.buildUrl(media) - - params = tryUrlencode({ - 'page': 'torrents', - 'search': query, - 'active': 1, - }) - - data = self.getHTMLData('%s?%s' % (self.urls['search'], params)) - - if data: - - try: - soup = BeautifulSoup(data) - - results_table = soup.find('table', attrs = {'id': 'bgtorrlist2'}) - entries = results_table.find_all('tr') - - for result in entries[2:len(entries) - 1]: - info_url = result.find(href = re.compile('torrent-details')) - download = result.find(href = re.compile('magnet:')) - - if info_url and download: - - url = parse_qs(info_url['href']) - - results.append({ - 'id': url['id'][0], - 'name': six.text_type(info_url.string), - 'url': download['href'], - 'detail_url': self.urls['detail'] % url['id'][0], - 'size': self.parseSize(result.find_all('td')[7].string), - 'seeders': tryInt(result.find_all('td')[4].string), - 'leechers': tryInt(result.find_all('td')[5].string), - 'get_more_info': self.getMoreInfo - }) - - except: - log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) - - def getMoreInfo(self, item): - - cache_key = 'publichd.%s' % item['id'] - description = self.getCache(cache_key) - - if not description: - - try: - full_description = self.urlopen(item['detail_url']) - html = BeautifulSoup(full_description) - nfo_pre = html.find('div', attrs = {'id': 'torrmain'}) - description = toUnicode(nfo_pre.text) if nfo_pre else '' - except: - log.error('Failed getting more info for %s', item['name']) - description = '' - - self.setCache(cache_key, description, timeout = 25920000) - - item['description'] = description - return item - - -config = [{ - 'name': 'publichd', - 'groups': [ - { - 'tab': 'searcher', - 'list': 'torrent_providers', - 'name': 'PublicHD', - 'description': 'Public Torrent site with only HD content. See PublicHD', - 'wizard': True, - 'options': [ - { - 'name': 'enabled', - 'type': 'enabler', - 'default': True, - }, - { - 'name': 'seed_ratio', - 'label': 'Seed ratio', - 'type': 'float', - 'default': 1, - 'description': 'Will not be (re)moved until this seed ratio is met.', - }, - { - 'name': 'seed_time', - 'label': 'Seed time', - 'type': 'int', - 'default': 40, - 'description': 'Will not be (re)moved until this seed time (in hours) is met.', - }, - { - 'name': 'extra_score', - 'advanced': True, - 'label': 'Extra Score', - 'type': 'int', - 'default': 0, - 'description': 'Starting score for each release found via this provider.', - } - ], - }, - ], -}] diff --git a/couchpotato/core/media/movie/providers/torrent/publichd.py b/couchpotato/core/media/movie/providers/torrent/publichd.py deleted file mode 100644 index 9fed4619..00000000 --- a/couchpotato/core/media/movie/providers/torrent/publichd.py +++ /dev/null @@ -1,14 +0,0 @@ -from couchpotato.core.logger import CPLog -from couchpotato.core.event import fireEvent -from couchpotato.core.media._base.providers.torrent.publichd import Base -from couchpotato.core.media.movie.providers.base import MovieProvider - -log = CPLog(__name__) - -autoload = 'PublicHD' - - -class PublicHD(MovieProvider, Base): - - def buildUrl(self, media): - return fireEvent('library.query', media, single = True).replace(':', '') From 07063d855ab0130be7a2ccb98c0a6a6a930fd341 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 17:35:15 +0200 Subject: [PATCH 440/468] Add icons to torrent providers --- couchpotato/core/media/_base/providers/torrent/awesomehd.py | 1 + couchpotato/core/media/_base/providers/torrent/bithdtv.py | 1 + couchpotato/core/media/_base/providers/torrent/bitsoup.py | 1 + couchpotato/core/media/_base/providers/torrent/hdbits.py | 1 + .../core/media/_base/providers/torrent/ilovetorrents.py | 1 + couchpotato/core/media/_base/providers/torrent/iptorrents.py | 1 + .../core/media/_base/providers/torrent/kickasstorrents.py | 1 + .../core/media/_base/providers/torrent/passthepopcorn.py | 1 + couchpotato/core/media/_base/providers/torrent/sceneaccess.py | 1 + couchpotato/core/media/_base/providers/torrent/thepiratebay.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentbytes.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentday.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentleech.py | 1 + .../core/media/_base/providers/torrent/torrentpotato.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentshack.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentz.py | 1 + couchpotato/core/media/_base/providers/torrent/yify.py | 3 ++- 17 files changed, 18 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd.py b/couchpotato/core/media/_base/providers/torrent/awesomehd.py index 1372f6cf..ccdf766b 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd.py @@ -80,6 +80,7 @@ config = [{ 'name': 'Awesome-HD', 'description': 'See AHD', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py index bb12c66a..34a21f9d 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -95,6 +95,7 @@ config = [{ 'name': 'BiT-HDTV', 'description': 'See BiT-HDTV', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index d70579e0..2b1a0e71 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -90,6 +90,7 @@ config = [{ 'name': 'Bitsoup', 'description': 'See Bitsoup', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index 6cf8d57d..0887f97e 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -72,6 +72,7 @@ config = [{ 'list': 'torrent_providers', 'name': 'HDBits', 'description': 'See HDBits', + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index 33edff7f..b6ccecd1 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -146,6 +146,7 @@ config = [{ 'name': 'ILoveTorrents', 'description': 'Where the Love of Torrents is Born', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPUlEQVR4AYWM0U9SbxjH3+v266I/oNvWZTfd2J1d0ZqbZEFwWrUImOKs4YwtumFKZvvlJJADR2TCQQlMPKg5NmpREgaekAPnBATKgmK1LqQlx6awHnZWF1Tr2Xfvvs+7z+dB0mlO7StpAh+M4S/2jbo3w8+xvJvlnSneEt+10zwer5ujNUOoChjALWFw5XOwdCAk/P57cGvPl+Oht0W7VJHN5NC1uW1BON4hGjXbwpVWMZhsy9v7sEIXAsDNYBXgdkEoIKyWD2CF8ut/aOXTZc/fBSgLWw1BgA4BDHOV0GkT90cBQpXahU5TFomsb38XhJC5/Tbh1P8c6rJlBeGfAeyMhUFwNVcs9lxV9Ot0dwmyd+mrNvRtbJ2fSPC6Z3Vsvub2z3sDFACAAYzk0+kUyxEkyfN7PopqNBro55A+P6yPKIrL5zF1HwjdeBJJCObIsZO79bo3sHhWhglo5WMV3mazuVPb4fLvSL8/FAkB1hK6rXQPwYhMyROK8VK5LAiH/jsMt0HQjxiN4/ePdoilllcqDyt3Mkg8mRBNbIhMb8RERkowQA/p76g0/UDDdCoNmDminM0qSK5vlpE5kugCHhNPxntwWmJPYTMZtYcFR6ABHQsVRlYLukVORaaULvqKI46keFSCv77kSPS6kxrPptLNDHgz16fWBtyxe6v5h08LUy+KI8ushqTPWWIX8Sg6b45IrGtyW6zXFb/hpQf9m3oqfWuB0fpSw0uZ4WB69En69uOk2rmO2V52PXj+A/mI4ESKpb2HAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index 0433d3ef..e68808d3 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -122,6 +122,7 @@ config = [{ 'name': 'IPTorrents', 'description': 'See IPTorrents', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index 65bab4d1..d611ddf1 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -134,6 +134,7 @@ config = [{ 'name': 'KickAssTorrents', 'description': 'See KickAssTorrents', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index 80e23488..0a490756 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -189,6 +189,7 @@ config = [{ 'name': 'PassThePopcorn', 'description': 'See PassThePopcorn.me', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAARklEQVQoz2NgIAP8BwMiGWRpIN1JNWn/t6T9f532+W8GkNt7vzz9UkfarZVpb68BuWlbnqW1nU7L2DMx7eCoBlpqGOppCQB83zIgIg+wWQAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py index e488b63b..1ee6e47a 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -91,6 +91,7 @@ config = [{ 'name': 'SceneAccess', 'description': 'See SceneAccess', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 487ce36b..d963e22e 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -131,6 +131,7 @@ config = [{ 'name': 'ThePirateBay', 'description': 'The world\'s largest bittorrent tracker. See ThePirateBay', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAA3UlEQVQY02P4DwT/YADIZvj//7qnozMYODmtAAusZoCDELDAegYGViZhAWZmRoYoqIDupfhNN1M3dTBEggXWMZg9jZRXV77YxhAOFpjDwMAPMoCXmcHsF1SAQZ6bQY2VgUEbKHClcAYzg3mINEO8jSCD478/DPsZmvqWblu1bOmStes3Pp0ezVDF4Gif0Hfx9///74/ObRZ2YNiZ47C8XIRBxFJR0jbSSUud4f9zAQWn8NTuziAt2zy5xIMM/z8LFX0E+fD/x0MRDCeA1v7Z++Y/FDzyvAtyBxIA+h8A8ZKLeT+lJroAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py index 85a4fda1..0d95f5fb 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -92,6 +92,7 @@ config = [{ 'name': 'TorrentBytes', 'description': 'See TorrentBytes', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index e6909259..7e8eeb87 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -70,6 +70,7 @@ config = [{ 'name': 'TorrentDay', 'description': 'See TorrentDay', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py index a5916272..3f2edaea 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -82,6 +82,7 @@ config = [{ 'name': 'TorrentLeech', 'description': 'See TorrentLeech', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py index 84718f04..fe0d5be9 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py @@ -134,6 +134,7 @@ config = [{ 'order': 10, 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', 'wizard': True, + 'icon': '', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index 2a285b2a..639beaff 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -81,6 +81,7 @@ config = [{ 'list': 'torrent_providers', 'name': 'TorrentShack', 'description': 'See TorrentShack', + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/torrentz.py b/couchpotato/core/media/_base/providers/torrent/torrentz.py index 3937f64e..51e505de 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentz.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentz.py @@ -80,6 +80,7 @@ config = [{ 'name': 'Torrentz', 'description': 'Torrentz is a free, fast and powerful meta-search engine. Torrentz', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQklEQVQ4y2NgAALjtJn/ycEMlGiGG0IVAxiwAKzOxaKGARcgxgC8YNSAwWoAzuRMjgsIugqfAUR5CZcBRIcHsWEAADSA96Ig020yAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index b1380cd8..0daf20a0 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -49,7 +49,7 @@ class Base(TorrentMagnetProvider): if result['Quality'] and result['Quality'] not in result['MovieTitle']: title = result['MovieTitle'] + ' BrRip ' + result['Quality'] - else: + else: title = result['MovieTitle'] + ' BrRip' results.append({ @@ -79,6 +79,7 @@ config = [{ 'name': 'Yify', 'description': 'Free provider, less accurate. Small HD movies, encoded by Yify.', 'wizard': False, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACL0lEQVR4AS1SPW/UQBAd23fxne/Ld2dvzvHuzPocEBAKokCBqGiQ6IgACYmvUKRBFEQgKKGg4BAlUoggggYUEQpSHOI7CIEoQs/fYcbLaU/efTvvvZlnA1qydoxU5kcxX0CkgmQZtPy0hCUjvK+WgEByOZ5dns1O5bzna8fRVkgsxH8B0YouIvBhdD5T11NiVOoKrsttyUcpRW0InUrFnwe9HzuP2uaQZYhF2LQ76TTXw2RVMTK8mYYbjfh+zNquMVCrqn93aArLSixPxnafdGDLaz1tjY5rmNa8z5BczEQOxQfCl1GyoqoWxYRN1bkh7ELw3q/vhP6HIL4TG9KumpjgvwuyM7OsjSj98E/vszMfZ7xvPtMaWxGO5crwIumKCR5HxDtJ0AWKGG204RfUd/3smJYqwem/Q7BTS1ZGfM4LNpVwuKAz6cMeROst0S2EwNE7GjTehO2H3dxqIpdkydat15G3F8SXBi4GlpBNlSz012L/k2+W0CLLk/jbcf13rf41yJeMQ8QWUZiHCfCA9ad+81nEKPtoS9mJOf9v0NmMJHgUT6xayheK9EIK7JJeU/AF4scDF7Y5SPlJrRcxJ+um4ibNEdObxLiIwJim+eT2AL5D9CIcnZ5zvSJi9eIlNHVVtZ831dk5svPgvjPWTq+ktWkd/kD0qtm71x+sDQe3kt6DXnM7Ct+GajmTxKlkAokWljyAKSm5oWa2w+BH4P2UuVub7eTyiGOQYapY/wEztHduSDYz5gAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', From 21a1770f3f4f4ef011692b5dd4d2e590a13c8356 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 18:14:08 +0200 Subject: [PATCH 441/468] Nzb icons --- couchpotato/core/media/_base/providers/nzb/binsearch.py | 1 + couchpotato/core/media/_base/providers/nzb/newznab.py | 1 + couchpotato/core/media/_base/providers/nzb/nzbclub.py | 1 + couchpotato/core/media/_base/providers/nzb/nzbindex.py | 1 + couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py | 1 + 5 files changed, 5 insertions(+) diff --git a/couchpotato/core/media/_base/providers/nzb/binsearch.py b/couchpotato/core/media/_base/providers/nzb/binsearch.py index 84c7b314..c61b72d3 100644 --- a/couchpotato/core/media/_base/providers/nzb/binsearch.py +++ b/couchpotato/core/media/_base/providers/nzb/binsearch.py @@ -100,6 +100,7 @@ config = [{ 'name': 'binsearch', 'description': 'Free provider, less accurate. See BinSearch', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAATklEQVQY02NwQAMMWAXOnz+PKvD//3/CAvM//z+fgiwAAs+RBab4PP//vwbFjPlAffgEChzOo2r5fBuIfRAC5w8D+QUofkkp8MHjOWQAAM3Sbogztg2wAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index 628a55f2..7db7e865 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -222,6 +222,7 @@ config = [{ Spotweb, NZBGeek, \ SmackDown, NZBFinder', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub.py b/couchpotato/core/media/_base/providers/nzb/nzbclub.py index 06ae7fec..a2660899 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub.py @@ -80,6 +80,7 @@ config = [{ 'name': 'NZBClub', 'description': 'Free provider, less accurate. See NZBClub', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACEUlEQVQ4y3VSMWgUQRR9/8/s7OzeJSdnTsVGghLEYBNQjBpQiRBFhIB2EcHG1kbs0murhZAmVocExEZQ0c7CxkLINYcJJpoYj9wZcnu72fF21uJSXMzuhyne58/j/fcf4b+KokgBIOSU53lxP5b9oNVqDT36dH+5UjoiKvIwPFEEgWBshGZ3E7/NOupL9fMjx0e+ZhKsrq+c/FPZKJi0w4FsQXMBDEJsd7BNW9h2tuyP9vfTALIJkMIu1hYRtINM+dpzcWc0sbkreK4fUEogyraAmKGF3+7vcT/wtR9QwkCabSAzQQuvk0uglAo5YaQ5DASGYjfMXcHVOqKu6NmR7iehlKAdHWUqWPv1c3i+9uwVdRlEBGaGEAJCCrDo9ShhvF6qPq8tL57bp+DbRn2sHtUuCY9YphLMu5921VhrwYJ5tbt0tt6sjQP4vEfB2Ikz7/ytwbeR6ljHkXCUA6UcOLtPOg4MYhtH8ZcLw5er+xQMDAwEURRNl96X596Y6oxFwsw9fmtTOAr2Ik19nL365FZpsLSdnQPPM8aYewc+lDcX4rkHqbQMAGTJXulOLzycmr1bKBTi3DOGYagajcahiaOT89fbM0/dxEsUu3aidfPljWO3HzebzYNBELi5Z5RSJlrrHd/3w8lT114MrVTWOn875fHRiYVisRhorWMpZXdvNnLKGCOstb0AMlulVJI19w/+nceU4D0aCwAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/nzb/nzbindex.py b/couchpotato/core/media/_base/providers/nzb/nzbindex.py index ddcce8ad..58f4b23f 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbindex.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbindex.py @@ -105,6 +105,7 @@ config = [{ 'name': 'nzbindex', 'description': 'Free provider, less accurate. See NZBIndex', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAo0lEQVR42t2SQQ2AMBAEcUCwUAv94QMLfHliAQtYqIVawEItYAG6yZFMLkUANNlk79Kbbtp2P1j9uKxVV9VWFeStl+Wh3fWK9hNwEoADZkJtMD49AqS5AUjWGx6A+m+ARICGrM5W+wSTB0gETKzdHZwCEZAJ8PGZQN4AiQAmkR9s06EBAugJiBoAAPFfAQcBgZcIHzwA6TYP4JsXeSg3P9L31w3eksbH3zMb/wAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py index acd12cf9..bac0614d 100644 --- a/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py +++ b/couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py @@ -74,6 +74,7 @@ config = [{ 'name': 'OMGWTFNZBs', 'description': 'See OMGWTFNZBs', 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAIAAADAAbR1AAADbElEQVR4AZ2UW0ybZRiAy/OvdHaLYvB0YTRIFi7GkM44zRLmIfNixkWdiRMyYoxRE8/TC7MYvXCGEBmr3mxLwVMwY0wYA7e6Wso4lB6h/U9taSlMGIfBXLYlJMyo0S///2dJI5lxN8/F2/f9nu9737e/jYmXr6KTbN9BGG9HE/NotQ76UWziNzrXFiETk/5ARUNH+7+0kW7fSgTl0VKGOLZzidOkmuuIo7q2oTArNLPIzhdIkqXkerFOm2CaD/5bcKrjIL2c3fkhPxOq93Kcb91v46fV9TQKF4TgV/TbUsQtzfCaK6jMOd5DJrguSIIhexmqqVxN0FXbRR8/ND/LYTTj6J7nl2gnL47OkDW4KJhnQHCa6JpKVNJGA3OC58nwBJoZ//ebbIyKpBxjrr0o1q1FMRkrKXZnHWF85VvxMrJxibwhGyd0f5bLnKzqJs1k0Sfo+EU8hdAUvkbcwKEgs2D0OiV4jmmD1zb+Tp6er0JMMvDxPo5xev9zTBF683NS+N56n1YiB95B5crr93KRuKhKI0tb0Kw2mgLLqTjLEWO8424i9IvURaYeOckwf3+/yCC9e3bQQ/MuD+Monk0k+XFXMUfx7z5EEP+XlXi5tLlMxH8zLppw7idJrugcus30kC86gc7UrQqjLIukM8zWHOACeU+TiMxXN6ExVOkgz4lvPEzice1GIVhxhG4CrZvpl6TH55giKWqXGLy9hZh5aUtgDSew/msSyCKpl+DDNfxJc8NBIsxUxUnz14O/oONu+IIIvso9TLBQ1SY5rUhuSzUhAqJ2mRXBLDOCeUtgUZXsaObT8BffhUJPqWgiV+3zKKzYH0ClvTRLhD77HIqVkyh5jThnivehoG+qJctIRSPn6bxvO4FCgTl9c1DmbpjLajbQFE8aW5SU3rg+zOPGUjTUF9NFpLEbH2c/KmGYlY69/GQJVtGMSUcEp9eCbB1nctbxHTLRdTUkGDf+B02uGWRG3OvpJ/zSMwzif+oxVBID3cQKBavLCiPmB2PM2UuSCUPgrX4VDb97AwEG67bh4+KTOlncvu3M31BwA5rLHbCfEjwkNDky9e/SSbSxnD46Pg0RJtpXRvhmBSZHpRjWtKwFybjuQeXaKxto4WjLZZZvVmC17pZLJFkwxm5++PS2Mrwc7nyIMYZe/IzoP5d6QgEybqTXAAAAAElFTkSuQmCC', 'options': [ { 'name': 'enabled', From 643be197118467e9abfaf9b965d0edef33ceb06d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 20:45:17 +0200 Subject: [PATCH 442/468] Update descriptions --- couchpotato/core/media/_base/providers/torrent/bithdtv.py | 2 +- couchpotato/core/media/_base/providers/torrent/bitsoup.py | 2 +- couchpotato/core/media/_base/providers/torrent/hdbits.py | 2 +- couchpotato/core/media/_base/providers/torrent/iptorrents.py | 2 +- .../core/media/_base/providers/torrent/kickasstorrents.py | 2 +- .../core/media/_base/providers/torrent/passthepopcorn.py | 2 +- couchpotato/core/media/_base/providers/torrent/sceneaccess.py | 2 +- couchpotato/core/media/_base/providers/torrent/thepiratebay.py | 2 +- couchpotato/core/media/_base/providers/torrent/torrentbytes.py | 2 +- couchpotato/core/media/_base/providers/torrent/torrentday.py | 2 +- couchpotato/core/media/_base/providers/torrent/torrentleech.py | 2 +- couchpotato/core/media/_base/providers/torrent/torrentshack.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/bithdtv.py b/couchpotato/core/media/_base/providers/torrent/bithdtv.py index 34a21f9d..57bc2218 100644 --- a/couchpotato/core/media/_base/providers/torrent/bithdtv.py +++ b/couchpotato/core/media/_base/providers/torrent/bithdtv.py @@ -93,7 +93,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'BiT-HDTV', - 'description': 'See BiT-HDTV', + 'description': 'BiT-HDTV', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/bitsoup.py b/couchpotato/core/media/_base/providers/torrent/bitsoup.py index 2b1a0e71..9519e58b 100644 --- a/couchpotato/core/media/_base/providers/torrent/bitsoup.py +++ b/couchpotato/core/media/_base/providers/torrent/bitsoup.py @@ -88,7 +88,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'Bitsoup', - 'description': 'See Bitsoup', + 'description': 'Bitsoup', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index 0887f97e..a09fe7d7 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -71,7 +71,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'HDBits', - 'description': 'See HDBits', + 'description': 'HDBits', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=', 'options': [ { diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index e68808d3..0915ca31 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -120,7 +120,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'IPTorrents', - 'description': 'See IPTorrents', + 'description': 'IPTorrents', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index d611ddf1..730bb608 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -132,7 +132,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'KickAssTorrents', - 'description': 'See KickAssTorrents', + 'description': 'KickAssTorrents', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py index 0a490756..609ef2d4 100644 --- a/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py +++ b/couchpotato/core/media/_base/providers/torrent/passthepopcorn.py @@ -187,7 +187,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'PassThePopcorn', - 'description': 'See PassThePopcorn.me', + 'description': 'PassThePopcorn.me', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAARklEQVQoz2NgIAP8BwMiGWRpIN1JNWn/t6T9f532+W8GkNt7vzz9UkfarZVpb68BuWlbnqW1nU7L2DMx7eCoBlpqGOppCQB83zIgIg+wWQAAAABJRU5ErkJggg==', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py index 1ee6e47a..e172f6ad 100644 --- a/couchpotato/core/media/_base/providers/torrent/sceneaccess.py +++ b/couchpotato/core/media/_base/providers/torrent/sceneaccess.py @@ -89,7 +89,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'SceneAccess', - 'description': 'See SceneAccess', + 'description': 'SceneAccess', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index d963e22e..57bcfbdc 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -129,7 +129,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'ThePirateBay', - 'description': 'The world\'s largest bittorrent tracker. See ThePirateBay', + 'description': 'The world\'s largest bittorrent tracker. ThePirateBay', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAA3UlEQVQY02P4DwT/YADIZvj//7qnozMYODmtAAusZoCDELDAegYGViZhAWZmRoYoqIDupfhNN1M3dTBEggXWMZg9jZRXV77YxhAOFpjDwMAPMoCXmcHsF1SAQZ6bQY2VgUEbKHClcAYzg3mINEO8jSCD478/DPsZmvqWblu1bOmStes3Pp0ezVDF4Gif0Hfx9///74/ObRZ2YNiZ47C8XIRBxFJR0jbSSUud4f9zAQWn8NTuziAt2zy5xIMM/z8LFX0E+fD/x0MRDCeA1v7Z++Y/FDzyvAtyBxIA+h8A8ZKLeT+lJroAAAAASUVORK5CYII=', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py index 0d95f5fb..156243eb 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentbytes.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentbytes.py @@ -90,7 +90,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'TorrentBytes', - 'description': 'See TorrentBytes', + 'description': 'TorrentBytes', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/torrentday.py b/couchpotato/core/media/_base/providers/torrent/torrentday.py index 7e8eeb87..a3e9b78c 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentday.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentday.py @@ -68,7 +68,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'TorrentDay', - 'description': 'See TorrentDay', + 'description': 'TorrentDay', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/torrentleech.py b/couchpotato/core/media/_base/providers/torrent/torrentleech.py index 3f2edaea..5f59dab7 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentleech.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentleech.py @@ -80,7 +80,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'TorrentLeech', - 'description': 'See TorrentLeech', + 'description': 'TorrentLeech', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index 639beaff..bab508ab 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -80,7 +80,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'TorrentShack', - 'description': 'See TorrentShack', + 'description': 'TorrentShack', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC', 'options': [ { From 1e183625c9581966ebb4035782c1f5dc39889d32 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 20:45:26 +0200 Subject: [PATCH 443/468] Description update --- couchpotato/core/media/_base/providers/torrent/awesomehd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/awesomehd.py b/couchpotato/core/media/_base/providers/torrent/awesomehd.py index ccdf766b..78c46488 100644 --- a/couchpotato/core/media/_base/providers/torrent/awesomehd.py +++ b/couchpotato/core/media/_base/providers/torrent/awesomehd.py @@ -78,7 +78,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'Awesome-HD', - 'description': 'See AHD', + 'description': 'AHD', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=', 'options': [ From 9df14bd55ad5e92b155b256747d81b22fa499004 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 21:04:24 +0200 Subject: [PATCH 444/468] Cleanup provider lists --- .../_base/providers/torrent/torrentpotato.py | 2 +- couchpotato/static/scripts/page/settings.js | 25 ++++++++++++---- couchpotato/static/style/settings.css | 30 ++++++++++++++++--- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py index fe0d5be9..d1426765 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentpotato.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentpotato.py @@ -134,7 +134,7 @@ config = [{ 'order': 10, 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', 'wizard': True, - 'icon': '', + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABSElEQVR4AZ2Nz0oCURTGv8t1YMpqUxt9ARFxoQ/gQtppgvUKcu/sxB5iBJkogspaBC6iVUplEC6kv+oiiKDNhAtt16roP0HQgdsMLgaxfvy4nHP4Pi48qE2g4v91JOqT1CH/UnA7w7icUlLawyEdj+ZI/7h6YluWbRiddHonHh9M70aj7VTKzuXuikUMci/EO/ACnAI15599oAk8AR/AgxBQNCzreD7bmpl+FOIVuAHqQDUcJo+AK+CZFKLt95/MpSmMt0TiW9POxse6UvYZ6zB2wFgjFiNpOGesR0rZ0PVPXf8KhUCl22CwClz4eN8weoZBb9c0bdPsOWvHx/cYu9Y0CoNoZTJrwAbn5DrnZc6XOV+igVbnsgo0IxEomlJuA1vUIYGyq3PZBChwmExCUSmVZgMBDIUCK4UCFIv5vHIhm/XUDeAf/ADbcpd5+aXSWQAAAABJRU5ErkJggg==', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 054f5319..b9f72aba 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -120,7 +120,13 @@ Page.Settings = new Class({ var self = this; self.tabs_container = new Element('ul.tabs'); - self.containers = new Element('form.uniForm.containers').adopt( + self.containers = new Element('form.uniForm.containers', { + 'events': { + 'click:relay(.enabler.disabled h2)': function(e, el){ + el.getPrevious().getElements('.check').fireEvent('click'); + } + } + }).adopt( new Element('label.advanced_toggle').adopt( new Element('span', { 'text': 'Show advanced settings' @@ -285,14 +291,23 @@ Page.Settings = new Class({ }) } + var icon; + if(group.icon){ + icon = new Element('span.icon').grab(new Element('img', { + 'src': 'data:image/png;base64,' + group.icon + })); + } + + var label = new Element('span.group_label', { + 'text': group.label || (group.name).capitalize() + }) return new Element('fieldset', { 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '') }).grab( - new Element('h2', { - 'text': group.label || (group.name).capitalize() - }).grab(hint) - ); + new Element('h2').adopt(icon, label, hint) + ); + }, createList: function(content_container){ diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 1f77b8e1..50b305eb 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -92,6 +92,22 @@ border-bottom: 1px solid #333; box-shadow: 0 1px 0 rgba(255,255,255, 0.15); } + + .page fieldset h2 .icon { + vertical-align: bottom; + position: absolute; + left: -25px; + top: 3px; + background: #FFF; + border-radius: 2.5px; + line-height: 0; + overflow: hidden; + } + + .page fieldset.enabler:hover h2 .icon { + display: none; + } + .page fieldset h2 .hint { font-size: 12px; margin-left: 10px; @@ -200,17 +216,23 @@ .page .option_list .enabler.disabled { display: inline-block; - margin: 3px 3px 3px 20px; - padding: 4px 0; - width: 173px; + padding: 4px 0 5px; + width: 24%; vertical-align: top; } + .page .option_list .enabler:not(.disabled) .icon { + display: none; + } .page .option_list .enabler.disabled h2 { + cursor: pointer; border: none; box-shadow: none; - padding: 0 10px 0 25px; + padding: 0 10px 0 0; font-size: 16px; + position: relative; + left: 25px; + margin-right: 25px; } .page .option_list .enabler:not(.disabled) h2 { From 7db291fc93807e1ddb180a63e016c5c3b0b25049 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 21:07:17 +0200 Subject: [PATCH 445/468] Show all in wizard --- couchpotato/core/media/_base/providers/torrent/hdbits.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentshack.py | 1 + couchpotato/core/media/_base/providers/torrent/torrentz.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/providers/torrent/hdbits.py b/couchpotato/core/media/_base/providers/torrent/hdbits.py index a09fe7d7..ebf2899b 100644 --- a/couchpotato/core/media/_base/providers/torrent/hdbits.py +++ b/couchpotato/core/media/_base/providers/torrent/hdbits.py @@ -71,6 +71,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'HDBits', + 'wizard': True, 'description': 'HDBits', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index bab508ab..0cfa04d1 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -81,6 +81,7 @@ config = [{ 'list': 'torrent_providers', 'name': 'TorrentShack', 'description': 'TorrentShack', + 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC', 'options': [ { diff --git a/couchpotato/core/media/_base/providers/torrent/torrentz.py b/couchpotato/core/media/_base/providers/torrent/torrentz.py index 51e505de..8a5455c9 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentz.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentz.py @@ -85,7 +85,7 @@ config = [{ { 'name': 'enabled', 'type': 'enabler', - 'default': False + 'default': True }, { 'name': 'verified_only', From 33884deb6c5de6273b79945ed1913b7a095b093c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 20 Jun 2014 21:29:43 +0200 Subject: [PATCH 446/468] Send single Pushbullet when no device is selected. fix #3471 --- couchpotato/core/notifications/pushbullet.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/couchpotato/core/notifications/pushbullet.py b/couchpotato/core/notifications/pushbullet.py index 77738c32..361294e4 100644 --- a/couchpotato/core/notifications/pushbullet.py +++ b/couchpotato/core/notifications/pushbullet.py @@ -25,11 +25,7 @@ class Pushbullet(Notification): # Get all the device IDs linked to this user if not len(devices): - response = self.request('devices') - if not response: - return False - - devices += [device.get('id') for device in response['devices']] + devices = [None] successful = 0 for device in devices: From 96275adaff4c9e53578921aa5b7b7594c847edc3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 21 Jun 2014 18:44:09 +0200 Subject: [PATCH 447/468] Use always search and ignore ETA. fix #3475 --- couchpotato/core/media/movie/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 0f4b4179..1053ec3d 100644 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -220,7 +220,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) # Try find a valid result and download it - if (force_download or not could_not_be_released) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): + if (force_download or not could_not_be_released or alway_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): ret = True # Remove releases that aren't found anymore From 443866ef048183ff0b166c29aa14a90ceeb9acba Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 21 Jun 2014 18:50:36 +0200 Subject: [PATCH 448/468] Use default title for search query. fix #3477 --- couchpotato/core/media/movie/library.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/media/movie/library.py b/couchpotato/core/media/movie/library.py index a6e29f32..28cb1b46 100644 --- a/couchpotato/core/media/movie/library.py +++ b/couchpotato/core/media/movie/library.py @@ -1,4 +1,5 @@ from couchpotato.core.event import addEvent +from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media._base.library.base import LibraryBase @@ -17,7 +18,9 @@ class MovieLibraryPlugin(LibraryBase): if media.get('type') != 'movie': return + default_title = getTitle(media) titles = media['info'].get('titles', []) + titles.insert(0, default_title) # Add year identifier to titles if include_year: From 5a08fed0b621e6d692fac0dfbca3b4892ff8dad3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 15:53:43 +0200 Subject: [PATCH 449/468] Manage release_id not assigned. fix #3479 --- couchpotato/core/plugins/manage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py index f0e0faf4..bec204b0 100644 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -166,10 +166,9 @@ class Manage(Plugin): already_used = used_files.get(release_file) if already_used: + release_id = release['_id'] if already_used.get('last_edit', 0) < release.get('last_edit', 0) else already_used['_id'] if release_id not in deleted_releases: - release_id = release['_id'] if already_used.get('last_edit', 0) < release.get('last_edit', 0) else already_used['_id'] fireEvent('release.delete', release_id, single = True) - deleted_releases.append(release_id) break else: From 37214dd41351f34c71d740f491a6662d31918e80 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 16:15:36 +0200 Subject: [PATCH 450/468] Put Pushover in config. close #3480 --- couchpotato/core/notifications/pushover.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index ea4b00a3..75073962 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -13,7 +13,6 @@ autoload = 'Pushover' class Pushover(Notification): - app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy' def notify(self, message = '', data = None, listener = None): if not data: data = {} @@ -22,7 +21,7 @@ class Pushover(Notification): api_data = { 'user': self.conf('user_key'), - 'token': self.app_token, + 'token': self.conf('api_token'), 'message': toUnicode(message), 'priority': self.conf('priority'), 'sound': self.conf('sound'), @@ -49,7 +48,7 @@ class Pushover(Notification): log.error('Pushover auth failed: %s', response.reason) return False else: - log.error('Pushover notification failed.') + log.error('Pushover notification failed: %s', request_status) return False @@ -70,6 +69,12 @@ config = [{ 'name': 'user_key', 'description': 'Register on pushover.net to get one.' }, + { + 'name': 'api_token', + 'description': 'Register on pushover.net to get one.', + 'hidden': True, + 'default': 'YkxHMYDZp285L265L3IwH3LmzkTaCy', + }, { 'name': 'priority', 'default': 0, From 964144996f1e18c674b75af35496ff0da4a6c4e1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 16:17:11 +0200 Subject: [PATCH 451/468] Advanced not hidden. --- couchpotato/core/notifications/pushover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py index 75073962..d9ef226c 100644 --- a/couchpotato/core/notifications/pushover.py +++ b/couchpotato/core/notifications/pushover.py @@ -72,7 +72,7 @@ config = [{ { 'name': 'api_token', 'description': 'Register on pushover.net to get one.', - 'hidden': True, + 'advanced': True, 'default': 'YkxHMYDZp285L265L3IwH3LmzkTaCy', }, { From 5617953d39018f25b0c8d6e48481333aef2aa622 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 20:23:47 +0200 Subject: [PATCH 452/468] Mark as done missing. #3472 --- couchpotato/core/media/movie/_base/static/movie.actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index b3878fd5..ff71f31d 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -302,7 +302,7 @@ MA.Release = new Class({ self.movie.data.releases.each(function(release){ if(has_available && has_snatched) return; - if(['snatched', 'downloaded', 'seeding'].contains(release.status)) + if(['snatched', 'downloaded', 'seeding', 'done'].contains(release.status)) has_snatched = true; if(['available'].contains(release.status)) From fdcdf07fa6ee5664a550ab1023127d2ef9baa67e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 20:35:19 +0200 Subject: [PATCH 453/468] Untag on delete from dashboard --- couchpotato/core/media/_base/media/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index b4c8ced0..1340aa6a 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -411,6 +411,8 @@ class MediaPlugin(MediaBase): elif new_media_status: media['status'] = new_media_status db.update(media) + + fireEvent('media.untag', media['_id'], 'recent', single = True) else: fireEvent('media.restatus', media.get('_id'), single = True) From 0b6843a1b95df88ac34a4c0d076166217ae6bb9b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 20:58:56 +0200 Subject: [PATCH 454/468] Force readd not adding with proper profile --- couchpotato/core/media/movie/_base/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 9475ec09..6e323861 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -90,7 +90,7 @@ class MovieBase(MovieTypeBase): # Default profile and category default_profile = {} - if not params.get('profile_id') and status != 'done': + if (not params.get('profile_id') and status != 'done') or params.get('ignore_previous', False): default_profile = fireEvent('profile.default', single = True) cat_id = params.get('category_id') @@ -146,7 +146,7 @@ class MovieBase(MovieTypeBase): else: fireEvent('release.delete', release['_id'], single = True) - m['profile_id'] = params.get('profile_id', default_profile.get('id')) + m['profile_id'] = params.get('profile_id') or default_profile.get('_id') m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) m['last_edit'] = int(time.time()) m['tags'] = [] From 474cd45fc544988237d6b4768b12673a2efe51d6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 21:14:31 +0200 Subject: [PATCH 455/468] Reset profile to default when old one is empty or doesn't exist anymore --- couchpotato/core/media/movie/_base/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 6e323861..07c47514 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -2,6 +2,7 @@ import os import traceback import time +from CodernityDB.database import RecordNotFound from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent @@ -117,8 +118,17 @@ class MovieBase(MovieTypeBase): media['info'] = info new = False + previous_profile = None try: m = db.get('media', 'imdb-%s' % params.get('identifier'), with_doc = True)['doc'] + + try: + db.get('id', m.get('profile_id')) + previous_profile = m.get('profile_id') + except RecordNotFound: + pass + except: + log.error('Failed getting previous profile: %s', traceback.format_exc()) except: new = True m = db.insert(media) @@ -146,7 +156,7 @@ class MovieBase(MovieTypeBase): else: fireEvent('release.delete', release['_id'], single = True) - m['profile_id'] = params.get('profile_id') or default_profile.get('_id') + m['profile_id'] = (params.get('profile_id') or default_profile.get('_id')) if not previous_profile else previous_profile m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) m['last_edit'] = int(time.time()) m['tags'] = [] From b11bb9cdac1f0fd63fc93c7efd0507d1dcbd0ae7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 21:32:52 +0200 Subject: [PATCH 456/468] Catch missing profile in restatus --- couchpotato/core/media/_base/media/main.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 1340aa6a..1e882d15 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -454,17 +454,20 @@ class MediaPlugin(MediaBase): else: move_to_wanted = True - profile = db.get('id', m['profile_id']) - media_releases = fireEvent('release.for_media', m['_id'], single = True) + try: + profile = db.get('id', m['profile_id']) + media_releases = fireEvent('release.for_media', m['_id'], single = True) - for q_identifier in profile['qualities']: - index = profile['qualities'].index(q_identifier) + for q_identifier in profile['qualities']: + index = profile['qualities'].index(q_identifier) - for release in media_releases: - if q_identifier == release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): - move_to_wanted = False + for release in media_releases: + if q_identifier == release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): + move_to_wanted = False - m['status'] = 'active' if move_to_wanted else 'done' + m['status'] = 'active' if move_to_wanted else 'done' + except RecordNotFound: + log.debug('Failed restatus: %s', traceback.format_exc()) # Only update when status has changed if previous_status != m['status']: From 15980471b0b17407928bd303189d05a13eaf4966 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 22:41:56 +0200 Subject: [PATCH 457/468] Create api lock on the fly --- couchpotato/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/couchpotato/api.py b/couchpotato/api.py index 17942583..99a2c6ad 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -91,6 +91,10 @@ class ApiHandler(RequestHandler): self.write('API call doesn\'t seem to exist') return + # Create lock if it doesn't exist + if route in api_locks and not api_locks.get(route): + api_locks[route] = threading.Lock() + api_locks[route].acquire() try: From fb9080c18a17b3554de29c8cc9eee34b8c34b7bc Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 23:38:51 +0200 Subject: [PATCH 458/468] Except value error --- couchpotato/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index b2b918a6..8e872b13 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -314,6 +314,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En continue else: return + except ValueError: + return except: pass From a26abd0dbb0e81181cf4b088363f1a4968c9b8fc Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 23:39:43 +0200 Subject: [PATCH 459/468] Don't use nonblock requests results if empty --- couchpotato/core/notifications/core/static/notification.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 81bf9500..93bfa15d 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -122,9 +122,12 @@ var NotificationBase = new Class({ startPoll: function(){ var self = this; - if(self.stopped || (self.request && self.request.isRunning())) + if(self.stopped) return; + if(self.request && self.request.isRunning()) + self.request.cancel(); + self.request = Api.request('nonblock/notification.listener', { 'onSuccess': function(json){ self.processData(json, false) @@ -149,7 +152,7 @@ var NotificationBase = new Class({ var self = this; // Process data - if(json){ + if(json && json.result){ Array.each(json.result, function(result){ App.trigger(result._t || result.type, [result]); if(result.message && result.read === undefined && !init) From 1c5f19a68a5d2aed400319b80b55579fa7da30b7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 22 Jun 2014 23:41:31 +0200 Subject: [PATCH 460/468] Better reload hook name --- couchpotato/runner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 8e872b13..ed291a99 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -277,9 +277,9 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En loop = IOLoop.current() # Reload hook - def test(): + def reload_hook(): fireEvent('app.shutdown') - add_reload_hook(test) + add_reload_hook(reload_hook) # Some logging and fire load event try: log.info('Starting server on port %(port)s', config) From 94064ac7dab2fc7d96008677a6263e4c51a3a1b4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 01:09:32 +0200 Subject: [PATCH 461/468] Rework restart methods --- CouchPotato.py | 6 +++--- couchpotato/core/_base/_core.py | 8 +++++--- couchpotato/core/_base/scheduler.py | 4 ++-- couchpotato/core/_base/updater/main.py | 4 ++-- couchpotato/core/plugins/base.py | 2 +- couchpotato/runner.py | 11 +++++++---- couchpotato/static/scripts/couchpotato.js | 15 +++++++++++---- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/CouchPotato.py b/CouchPotato.py index f5b9c046..7049cda5 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -72,10 +72,11 @@ class Loader(object): signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) from couchpotato.core.event import addEvent - addEvent('app.after_shutdown', self.afterShutdown) + addEvent('app.do_shutdown', self.setRestart) - def afterShutdown(self, restart): + def setRestart(self, restart): self.do_restart = restart + return True def onExit(self, signal, frame): from couchpotato.core.event import fireEvent @@ -103,7 +104,6 @@ class Loader(object): # Release log files and shutdown logger logging.shutdown() - time.sleep(3) args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:] subprocess.Popen(args) diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 1171478d..852c42c2 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -118,7 +118,7 @@ class Core(Plugin): self.shutdown_started = True - fireEvent('app.do_shutdown') + fireEvent('app.do_shutdown', restart = restart) log.debug('Every plugin got shutdown event') loop = True @@ -143,9 +143,11 @@ class Core(Plugin): log.debug('Safe to shutdown/restart') + loop = IOLoop.current() + try: - if not IOLoop.current()._closing: - IOLoop.current().stop() + if not loop._closing: + loop.stop() except RuntimeError: pass except: diff --git a/couchpotato/core/_base/scheduler.py b/couchpotato/core/_base/scheduler.py index 16a3a4f2..271a2d81 100644 --- a/couchpotato/core/_base/scheduler.py +++ b/couchpotato/core/_base/scheduler.py @@ -33,9 +33,9 @@ class Scheduler(Plugin): except: pass - def doShutdown(self): + def doShutdown(self, *args, **kwargs): self.stop() - return super(Scheduler, self).doShutdown() + return super(Scheduler, self).doShutdown(*args, **kwargs) def stop(self): if self.started: diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 093977da..9f6613ab 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -142,11 +142,11 @@ class Updater(Plugin): 'success': success } - def doShutdown(self): + def doShutdown(self, *args, **kwargs): if not Env.get('dev'): removePyc(Env.get('app_dir'), show_logs = False) - return super(Updater, self).doShutdown() + return super(Updater, self).doShutdown(*args, **kwargs) class BaseUpdater(Plugin): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index ac5ca87e..bc66123f 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -263,7 +263,7 @@ class Plugin(object): def afterCall(self, handler): self.isRunning('%s.%s' % (self.getName(), handler.__name__), False) - def doShutdown(self): + def doShutdown(self, *args, **kwargs): self.shuttingDown(True) return True diff --git a/couchpotato/runner.py b/couchpotato/runner.py index ed291a99..6560f55a 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -286,13 +286,14 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En except: pass fireEventAsync('app.load') + ssl_options = None if config['ssl_cert'] and config['ssl_key']: - server = HTTPServer(application, no_keep_alive = True, ssl_options = { + ssl_options = { 'certfile': config['ssl_cert'], 'keyfile': config['ssl_key'], - }) - else: - server = HTTPServer(application, no_keep_alive = True) + } + + server = HTTPServer(application, no_keep_alive = True) try_restart = True restart_tries = 5 @@ -301,6 +302,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En try: server.listen(config['port'], config['host']) loop.start() + server.close_all_connections() + server.stop() except Exception as e: log.error('Failed starting: %s', traceback.format_exc()) try: diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index b71c1f0d..19b9f455 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -272,11 +272,18 @@ (function(){ - Api.request('app.available', { - 'onFailure': function(){ - self.checkAvailable.delay(1000, self, [delay, onAvailable]); - self.fireEvent('unload'); + var onFailure = function(){ + self.checkAvailable.delay(1000, self, [delay, onAvailable]); + self.fireEvent('unload'); + } + + var request = Api.request('app.available', { + 'timeout': 2000, + 'onTimeout': function(){ + request.cancel(); + onFailure(); }, + 'onFailure': onFailure, 'onSuccess': function(){ if(onAvailable) onAvailable(); From 50d6882a9826ae99e38d8dd373fe2a8e4c02241f Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 01:17:06 +0200 Subject: [PATCH 462/468] Close all attached after start --- couchpotato/runner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 6560f55a..3a779ba2 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -304,6 +304,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En loop.start() server.close_all_connections() server.stop() + loop.close(all_fds = True) except Exception as e: log.error('Failed starting: %s', traceback.format_exc()) try: From fab9b96c8e64790b2ff7a5bc5397ff7fc9f0639a Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 13:16:05 +0200 Subject: [PATCH 463/468] Keep done releases when removing from wanted/dashboard. fix #3488 --- couchpotato/core/media/_base/media/main.py | 2 +- couchpotato/core/plugins/release/main.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 1e882d15..4cfe597c 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -405,7 +405,7 @@ class MediaPlugin(MediaBase): db.delete(release) total_deleted += 1 - if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active') or (not new_media_status and delete_from == 'late'): + if (total_releases == total_deleted and media['status'] != 'active') or (not new_media_status and delete_from == 'late'): db.delete(media) deleted = True elif new_media_status: diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 9e7e4ca7..cb16c5ae 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -477,7 +477,7 @@ class Release(Plugin): rel = db.get('id', release_id) if rel and rel.get('status') != status: - release_name = rel['info'].get('name') + release_name = None if rel.get('files'): for file_type in rel.get('files', {}): if file_type == 'movie': @@ -485,6 +485,9 @@ class Release(Plugin): release_name = os.path.basename(release_file) break + if not release_name and rel.get('info'): + release_name = rel['info'].get('name') + #update status in Db log.debug('Marking release %s as %s', (release_name, status)) rel['status'] = status From a37517bf6a09ab9e1ccdb5d4a459c70f09bc1554 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 13:37:13 +0200 Subject: [PATCH 464/468] Use ssl startup options. fix #3490 Thanks @sjmcinness --- couchpotato/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 3a779ba2..8a446051 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -293,7 +293,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En 'keyfile': config['ssl_key'], } - server = HTTPServer(application, no_keep_alive = True) + server = HTTPServer(application, no_keep_alive = True, ssl_options = ssl_options) try_restart = True restart_tries = 5 From 28ffad10ab5f562c8d3fd7e24fdf2d44680b25d0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 13:43:33 +0200 Subject: [PATCH 465/468] Standardize path for list directory api call. #3487 --- couchpotato/core/plugins/browser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/plugins/browser.py b/couchpotato/core/plugins/browser.py index 6880f3b4..013a4823 100644 --- a/couchpotato/core/plugins/browser.py +++ b/couchpotato/core/plugins/browser.py @@ -3,6 +3,7 @@ import os import string from couchpotato.api import addApiView +from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.plugins.base import Plugin import six @@ -50,6 +51,7 @@ class FileBrowser(Plugin): path = '/' dirs = [] + path = sp(path) for f in os.listdir(path): p = os.path.join(path, f) if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)): From 5350dbf0ce2e737a2f18949efb2c2d1cb2d08224 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 14:13:32 +0200 Subject: [PATCH 466/468] Filter out extended and try other result on determine media. fix #3489 --- couchpotato/core/plugins/scanner.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index d5937eae..de80d27e 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -105,7 +105,7 @@ class Scanner(Plugin): 'HDTV': ['hdtv'] } - clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ + clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 @@ -636,6 +636,10 @@ class Scanner(Plugin): if name_year.get('name') and name_year.get('year'): movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1) + # Try with other + if len(movie) == 0 and name_year.get('other') and name_year['other'].get('name') and name_year['other'].get('year'): + movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year.get('other'), merge = True, limit = 1) + if len(movie) > 0: imdb_id = movie[0].get('imdb') log.debug('Found movie via search: %s', identifier) @@ -938,8 +942,11 @@ class Scanner(Plugin): pass if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')): + cp_guess['other'] = guess return cp_guess elif guess == {}: + cp_guess['other'] = guess return cp_guess + guess['other'] = cp_guess return guess From eaaa8dc834bc528b3810320e078a3404a972e61f Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 14:15:00 +0200 Subject: [PATCH 467/468] Only try other if it's different --- couchpotato/core/plugins/scanner.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py index de80d27e..01a88fbb 100644 --- a/couchpotato/core/plugins/scanner.py +++ b/couchpotato/core/plugins/scanner.py @@ -634,11 +634,14 @@ class Scanner(Plugin): name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None) if name_year.get('name') and name_year.get('year'): - movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1) + search_q = '%(name)s %(year)s' % name_year + movie = fireEvent('movie.search', q = search_q, merge = True, limit = 1) # Try with other if len(movie) == 0 and name_year.get('other') and name_year['other'].get('name') and name_year['other'].get('year'): - movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year.get('other'), merge = True, limit = 1) + search_q2 = '%(name)s %(year)s' % name_year + if search_q2 != search_q: + movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year.get('other'), merge = True, limit = 1) if len(movie) > 0: imdb_id = movie[0].get('imdb') From 8d85dde2c67f0fdcd14dcd7e027fede600dbd2a6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 23 Jun 2014 16:19:40 +0200 Subject: [PATCH 468/468] Don't use empty name_year return for moviemeter. fix #3493 --- .../media/movie/providers/automation/moviemeter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/movie/providers/automation/moviemeter.py b/couchpotato/core/media/movie/providers/automation/moviemeter.py index 883fcfa1..b06046fb 100644 --- a/couchpotato/core/media/movie/providers/automation/moviemeter.py +++ b/couchpotato/core/media/movie/providers/automation/moviemeter.py @@ -21,11 +21,15 @@ class Moviemeter(Automation, RSS): for movie in rss_movies: - name_year = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True) - imdb = self.search(name_year.get('name'), name_year.get('year')) + title = self.getTextElement(movie, 'title') + name_year = fireEvent('scanner.name_year', title, single = True) + if name_year.get('name') and name_year.get('year'): + imdb = self.search(name_year.get('name'), name_year.get('year')) - if imdb and self.isMinimalMovie(imdb): - movies.append(imdb['imdb']) + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) + else: + log.error('Failed getting name and year from: %s', title) return movies
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%0AX&%5u2 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

mTO|%a5&3TW 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

%5Ke9^@h@l=nBp($G7fdsAvAK-M$h| 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

J*C; z=E9G9tXe!nW;jdrGJUQO4=6gcc9ENZ`Z?lcr|8$;J*(eL6LGwOrIb;wYJ8THsm_6A ztk!T-Qf1FYcJmqfljtpEMA)ndVA|p8l(!DEU{9<59%TheOo^()?=au5!1w zxcI_`^rHQ9s;qZ}T*5|d{`nUSTJK6$g?;vl4k+5Q-c#5M8;|-IPy)5tSLF+zOpgvM z=d<~$;}Smo{9j^Vqqxa zz2AmY94Q0AAApy!|H*d|NtTrXnKQ$J+4rwcg~Wbu=tz;?2nAC4`^F*z?LkP_PsGFp z@QWs)xc)Yf>Yg3wSha*>JOo?X_vG$M{zFo}D?F5U0;fWM06r%t{Q3Gc${+s$InhOm zzwK|3eC>hz_x)q`GYaWL9F;x3iL)y-6CY6vIN>4#zrlxxcM&hFq_^;d?~0Vx0GjeD zT7*)TISz5FJJb?z!3Dhp;}Wg_<&NEQ0m`9{-3k0Tq^|iu?j*8g$;;{P;9)FNjxx** z2F2n?q*RgxRrrfk1nX6V-m3_Ys)#JBh#slj_^TpDp(>76l@L~yyrn9ot14}+D&wh2 zkd0K8OH-9EQoUKPs_g~U(SPC^|w3>>rn(8ezHC;7zYc&l|HORU7JdDeGi@eE>UL5x510~NU<1~|1CD4?CkhSSzv|9Xb@wARTq6gZO=xT> z9=pCD_&>D0byQP<|NlQ=BgO_Jr5hxbQcyw==@w90K)M7$Ktkz`5z^f$qr1BsX-21X zNx1L)eBR$U-|z3=-)nd0?A-tEJ-a(+ug5cN?%!CY_ExaJXDSPJn)Jj#zR4N*@VIKj{n2We&NQgG1z_Y2;(LrOGFl%BM8Trw+@fEy<@J%4gimXF?RRXcV%!6>>xsa+MVF^cC`L6$(5R z3d0qOQWc6z6-t^FN{1E7mK1&-DwN+VARvkrG>XcLWR-&P_71UC+#$Xp6gB#a@cKae z{)~GJ9GgsZLb76G?vM>`kiS!)u^M(*Y&T+QsAYJ_63}ojuhgiCZM_?4A1rMh-1Cij zIF^DnH+QHZRk24)W;#4tI?}*B63P|0x|E@X58oywF(1wdKq!t)D>p(y2K0ME z1+b@vqqjXN_^c97Olvfou^%Lrr-32&KFVP!%I_T$T`9)^UvW?GGqx+n!lK7C&MB1F z7~u|dK}P8(g7hcN@DRyn8(!7(VR{?2@e3B5K22=1!oZDG>3|$;!~V=@Tr43upav31 zOq`Nzsd6hgemqTqkwkQV!Kg71f%=*d;H~0{r$&C6dFxE^Cr-V~X}F;o+d^CILUioD zfyO$sex+LtN`^-u?a$mfoX{WQ>NDvOJc%MA;i^%^z>9UAnB<1WKH*7U=Tm#SHL1Zo z4oG^-t3?=yceM^&1elm8K@gXiz zemSuVYx3T4m?>ghba}|YZ3d{N($PFF#e*lftES4p$c=~lA63@c|Dnpl#K6MDqsAto zfPgSb@j#?ZyktZK?%$y4JT<8&y z;~^z@!o&Mi&XSMonIQWkLH5VbD)FDI=m|d;dMPOQa-3L1kV{llL`+6OJe1_MkkIQ1 z8mZS(QkB%wLI4?lSp|6oMWZ04=TDVJ*pwZyRkB%CWrWpX*cv8!nyRXr8lN<+EVN_+ z?h1vPd-TB?yh4a6QT>OhC{Y#5WOM9Ek8=I>uYir9Z z>x=9EBw5#Y=#J;@QRFtd$oX(3ti!W| z^M`+uEEMYZ@zBxf`H4s1>HhZV!QtuQ>AAcPy4?Bv@865@p^Nk5i}OF1+c%fD7gziH z*KOU`2TM1rTep{scUL=icUSipHxG@C|72KEiKv2d)WiQTVyyoq?n0}wy8c_0<%dhc zso#&t>I%YjsxV=VUVrlzVWcw*s>tb!Vvq`@k*~}hh-KF()*qd-l8W^JO9jIDz~t597yks}w&X<$8Bpqj6y*tu^7&M7L&msvZctSR|bZqyY@t9X?) zQeZX(ZkGSTrB|CXJ(#9g_j9?)?fgm0<^aiuHZQRV%o*1cbq)9bR%K19=h0LNN;On& z4VkmptPD3)?Zjux1kjQFV$2(3tNRi*YlqewEST8Xuz67JFV;I9tc>X8Aso7Wadyz1 z*JVZ>Q8=#@O6$&d#&hslcoc32kuC1?3av^>yvB9zAk=%A=9|jX!AYE;eDp82*-+_F zhU%;G0)_8lGmk0nn|Mu7>QW=F^I5Jd{96$T5Wf8eS%~MIH2a$W2lea>sa8{TiVErr zyCHw)9Huhw05^L&KVj#!K}b7GPS}${;6^%c-)`kvgz)ywBrEy;F5F>tuQQWpeff2` zTMNTAo$pXwVR%?$oZE7k_&cku9}t5ZB7r6(h5nP->TG<$aY2%ZC!L!y;og(De>S~e z^UIBV=7^XZey+V;WRV_sflDMXtW=-oFsL1?NKq*GxsNvB{+HsD$o2{xPWEkndDa5K zDF`ouVlr4ET|xlN;t_AXUxdxLG31oYbg}Y$9EOS==IOM<&crZp9r{QXLDi(c#H3>NA36Z~( z%=3jpSR@H-XeRYFJe3XJk9BFqKd_!(dKM_w4)Z3gQIiTpto8Fj=mdt5K5Q+d)x45L zxN(ErZK4cvoFnj;@&X-d%*r&}xiMhDHKc>AAU#eUgXF21cOVCY|3()_k|l3D^>iU9 z*kNE|UY0&W?aWG;eTuej$!oObY{hd3RnY!eQC~uyg$&84hLMygDe-7}X%fswOYa{l zQ8(N|95doY!sRti_k3nv7{4)(1H#u|F84f8)^<)mQz zEg`TAhp{ulF64x5`|~n*noFO+o-Pz($erGoEcK%R_w=uScBJU$GC)S7PU3mIul;2n=qydB1il(%lW?S6njZUeux+y>C^S*S zX^xygPmT@W3lQiSKSiTwTYxX7ltcCDW3c_^8g53r4DAyKc;E|vjI~T3!OjVA7akC2 zgd%`Qf6mX(Em{Dv%XCd3-(|k*5CIQ2{g~W=Z*0r_sIFh;IiwR`aOM+ZyISw_m z8`DKuA;D@9W!^ss`9^g$bWfO) zYBQxxtg=)x%}FULDGt@_2IfpK*Xf;f;BQJFb;5H-*dtc8)ULY*Z z9mAn%v5iSy87$3hPZXAlcwN{7EqBXe)_kXy{VF+hwsN*9+#D{i;p+R8PZxhB*rn^o zT79|X(a?wc$m_PrJz6dTb(*e)$?&a)U7nceuxl1e%5>qUx*J8S_gB}Qh$3EZp~KX( zm%cxG`66@rr=ga-Hwit)tXd%*%*lZtJNrD91CO~j{=V*OYwF+&QjaxwN0F`lfI-Oj z83W8m@q%!U_pM{N6aM;1VgADu-)jf`kC1brmp3yfcBvH~m*O75QcNtrRtkhLP8;Ex zdCNSpErRGy!%-9yg+P|k+gYC_%tV{RKmkLN6w0akUV>=;*H5zWB)^+cCjhFUSa3m@5?(s{hEFmGBv_{co?IUN($d0qgOCEyKC z%Nd7MST3c(O;V1S#)>$%6!~ISG;h3dxMBtpB-AhS)ZY=RM=bT?b4mG|OMGg|6x|^o zb7zy%3eXGTF3MV$a3%-da0AsEcQ1<4;3!EjU$_r0HG2oPE(13AW(PSd6ZXddBD*Zk z%0zkTM0+8Wt=jh}MaZr9V>1h2t?J`CtB3Jdjt~?K!Noix>=x0CnK4Nq|P-epNU`TP~qk>VO&v*mnQ`_tb7V z6N4-h+Leh;7X;#*guR6kjH(3+KZf-Nhp%}<4cfyG9}zsD9!Gr85-V>9=Q-dUR7TjV zfi~*#o7y3*?W|4SkxmXE9~jhi9A_CI^dnefhS?6%0sfO1b*D}8R~WY314ach7tTSy z12DfbgpyZzkQK$+ZOIgWc1?86ux zrMveP#z+eK>J`Po2!VqU(Uj_mewqM9n?LcXMWkXJU%^$&i9{erRB0v={APRSIE=qJ9gh@_vkD_-Ih5fXuX*{F*7aMlvrC{y7d_QUi*@f$|O@;$)z- z1BhY}C;9|3#NbS~2S}45E|sum0aJu38 zKVcAb-6PxqSHd0l6*w2&nO&}i*NMzp0{}=eLl{oMtf5idFJUa&;7Iwrcl)s#V6rk3 z((){Y`x1Go&1Uw;DL$;-uD12Ba~>FmQ) z)T!PNW?-t*3YnNa%vgXd^21S7#5wKYvLE@~65iKtX(oH%;hESGX}lpMQ1P}1SF#X7 zM5S4g2VyLY4}OwwP5Y2X6}(W0cu8E5Nj8UvL+nZ<7E%PcO&QHISO(#tb&WU<=s#CD zjpW2HjKSFsB;E>e!#|~kzQz?V%b0yAvej_oU^-*7&j*YZyNwkq#?jYVq>>3-%fyNm zCeA%2hhb2zse#{76*o071|x9;khpw z7zfItb$0+@#5hO;hVLbTfrWTA1Ade@K@`nM8l`~9HZBx;tMP0`q`h6+oK=%wi=GW% zQ=0?vj)}BT8lH}Ni+f>;>y4<&=&2G}#QE%iw>*xktOl+q5|_j%94ICmzJ=vsQ2OeU zL{U)*XXl+;<)J-;7d|8dp=s7@q$-Ty!e_OHTvdWZe10i4xdo9vxdlBb(C zju!{R&xT<;FbJ_BqxT1^y%;mNHt>Wb>uo#UaAa8db6QssG5n2V6;@Kq)-Q01J| znuMh#DQ27~)Ztj+28`oW!YP^5^?)5@FmWC_*O4R{MF1|pRv zO-<*N;ETPlX~-OMOuRrioPm!D4g?N1HP$XShZb~a!Vs*IKdUV%%Lfv*I(srTOcIDA_lhSsJR%I=={k>~r zGsc8s0KMVZ{1O=aNEjyrCQcTgoH3?wCKhKVA16aQz_UZ%xC4IzC-oZmObrV>+>V{q zAwz^&&;i7>#bgQZ;D(U??t*!#;dS#uQxI4LjF?RNFfJw1XA(&f-sm`Jtmpdwlp7>V zopQ~@Pn$ckCGco`0wF$K97-_G<}N`-oD?KZ&?aO@h-cgziz}(yM6}mbzt`Ne_k02) zzz9kZ(ra(aO2L3*0>JvD-zV;iflk=uZii99FgUz1Txj}7B{AtF`XOr=0nOdMsOJ7U zz82bX49IDJ)Np@XI8_W};797fna@D-(m?wCKqk##w&-APGcnrSSeQCkkUCgeI#?)* zVXQG&xkOxMi&3IKgv*CfXFF80G+0+U)Iu{%&O1a=I@Ajp?(-bxj2*o-C*v9cEckGE7QVn+M- zw652T!N`n3+CMkClHIIn#HGM~CUpZ+kPNxP6O zwvcPEkngq7C>c?>JXYMY@bh5-LAzKfwpeYj`0}lFncZS$b7AAdB<b5dXU&LAUu@eDjUrrp&iZx%5qi)=j0AO_jf!F)cc3f8+6AY`y#TPll!2 zx~0FeW%zf?h;G|NeB0D;+x*)$K`Y1-U61a)ohyy)@C0ZtuKURwTaRJKi9q|)3AUb^ z+}FQ5-gLXZ;=BHayMafo*eE1M5P=ppawqJYCX^Q|DpHdw6C-v-0}3O60x%LrHIfWx z)96$cDF_hkFz7fSlWsq{dk@pt3#zuC90|bA+>hzrU*kOh8u>FA?GZ2>RMWj>fn#9t z?Y9{o_I^9;Pd^-NJse&+9Q}JZPWO9K{P(os@7Zs^=hA;KwEkXN`MvV@H#IH_CQCiamr)$|F?$QvdvFBfUPT4V8ruKAdegG1yko zDVQ)-erBP6O4DIv!iG8~>-V9NiGi#;s&}iMi>#hML!EbInF_qRc=O>x=KF)}4wGEH zvPWGx&|iOkb?yA&+U5JTTgJ6}+qLKF^*7YDH~o$8s~i6hH-X=8f-`PH+it>FZz54Q zaQZ*%dP_0KL}f%b^9Uqi7L+hy!T9!1Nb=(N3IylK@HmgdpTr*sMuVC9-BYTgV-OT^X4p#jp=0cB z0dU{o$7J^UP5^b4ZM=WqtO{-ZaNAbSF{VnL%mg{-x$AGEc%u8!Gv97y^zI@9n)`|E zWahR#>aQCJ))`FeH_N>w>5oVDP6#D?tut;#YRuw2WO zT%5Hv7f5|!^E}0T%0j?yDw2r#9o%v_f#>NHv*NOpsRH$c#@xi|eXUZR8vCtFHc_6%nUJ%E&c0U(Hm_ zlv}K>h^?(ldR%5()z@~U3P(l!mUk{QHA`OWVF;eC$0FX#_v>M`CNQSxzdk#XLD|Ii>;05<^u2uQ>X#t3}$ z=n*wFH9b8&6B83FD=QlD{BzzvdE&|PG)rDHKR>6SAh)=wKH#9UfHr6yXH8nTawYE02wzjsnH+6J$bhdZ=BN?5YXs`J{XZi5(*x1Xf);1*7(Ewt5dt=AhYiDO?SLWUB?(W_J`Z(B+ zN!#DwKRDPuKo1%&DCi#U{XX13_{Sc8Zyg>T93CD0-b4M~Kl*)$Hi7RS*}5DZ93P!s z9i9I^IzK)-KRZ4;Jw7=yKlQ&u?UV$e^hbKqJCuheem#3$vN9gyNn$g+$ z)!D_x+4cGP#r4J6$;H|E#rf66#qs6&!{x=^<;C^Y)%n%U&GnzFKYwp;@9*zajqcEM zkGgxfxqCpLxPQ1uJv`h#JUpNx6HsaSsO~Y;^y>d&5C8869F#YktDk>BU{G*KXjphe zWE4C)CN}Ozd_rPUa!P7idPZhec1~_yenDYTaY<>}&vHaXWmUB+b4`6iV^ecWYa9Ab zqmBK&ef@9gfO@7ZtkCHwL4tDjz7 z-~74F5z(=}17Lw^>J_^ADBj}!qakZ7er0m{$x?k;Jz*3A)=T{rIkLu#yaD%eXd2?d zKo@Zj%+4E%Mbi*LD~o>~buyP-cvZnz#(@hanub{BrqRc6{^*%1oGMTw`hski?9znN zWLqx}e)$ntZq(*Nt5{n)Uu8Mf*_2sZwpizi;%xt)IAni$sIGjq%^M4!PN^QT)@DY) zWf-pVWTOwxteCF!4~Haf8m$a}P1qjIl&Xq3*Qwr{nzg@a$K=4?|Ml@%g1unMFX&wT z`RwS(f0@)bDkua+>reZ*^+{ii(tHWsib!R6JJEb=OwRq*QZ;M$ig9^p1V|V0EnRl{)$LpT|bR+Be>j3|MS8@4bR~3z2@VFL2lV ze-}Ou$HRSFxTZ$@_H$<-L;errV3v4GreLnLLMF|X3jXy_-q}l&aKT&6*Jv72xDhFO zwYw1|fz7cAe?xAy87;?Fw26vQdcL0-(Hw9BG4I57=-c+TOn8uIyp*IIJ3a&Gj2G30}0tn4Jr?E^b_~vH=#j9F$=# zhT0r8@IDkDH6GR;95tQvave8c)P@~JV{Mikx87eJd}_VM)*r#lSCK%sSZ}bjcS3{? z5dd?d@cj-vlB*L`4~0c3U+?_7HE$=`SoLW?OM-1B0E0WVa*&{0zw#Nr1TSAdV;^bz zi11G7`KahMI+#M7Fqa2JKCXX0A=iW@F$&<0aGFvQ@e-JRsR+Fo;njg($Sb?jO8ipu zYpI_55GGbTZz*IzT&MfVU>nmF@!Lh->X+U1vded%YcYAXSY4vWsxb!k_b5uY)qt=8 z0AuiD0&`>}7TqIAT{N!4>EQlcrB~DkG^Md>iuKFp zmD!$`1-u1yE&rMR@69Ityf^TTg}3MJmLjj{?FmVdJ7{j|LmRw!ryTY7^%*@Nb`#KH z){aF@!4T6w(+p)%^O5=Neb4qL8LWHaOEH{@E!5vZ;C|xAFr8`mZUc)r86m+20fFCZ zzb60Li^o#xOIX+rB%VFNE6MVOdI7pH`6Pp0ns&y#mg{DsIt`W^&NKv9zhXV`?rlKB=_)^D}?e={1t}x!08YYT>mP7>s7~vW%PqaqW<8 z9LgjGHkAr^dgcaS*nqT>d!>KsJ>z3}w;@qX$GA?kNuB2+UMoK)l2y}~uaA9zmOo3v zn}&t^+Oi9?f+S@z+=4?LPx0g1iXVN>StM!j9_2~&qUoNAQfQ#-TQ*7hnbI!fHKrnG zBXLrik?{-amujvriQq@6#xm;tL*JP+vlt9M8q(YYwRnM6biPoTh!knrdW=Uo^oUq6=zX{q)Gl-?-vyqyVaxpH1>=OQu2%R{ z)bDi#&y5&J_!i$^6x%IappvsuZQe(@b)G-+iR$&NlIhH%PON9v%Eu zux*y4;+*H>c`KKx5EqZpV~y9qE=9BdHr&M2`oo_=nU31ikaZ;vRh{JNzR%HtceX_` zIJfegaxRtT%VD|(bAyEn)!CxMISioCu4eKdvBLfgoO2Sv!u`h3cIZ!sAjPHi?L2U_ zcd5kAf)d*_A}M*Ak;2&nZxZ4g7oKDzQ$wMlJs49mU5)wWna^?$;v%CicaLYnW5sFV zOLG!?xk<3@MC0clO`@Jw^cHe*EuBKdA^u9vc-8~*x3F?F*80w6tkCQIC@qS07xhjW zzrUAX9Unjcjo+j5v52=-4cW`g+R_IB5CMv4%vL{%_xjVXwxa_K%6z{orv`(2*LsCj zHa=z#0e6ooa;ApO2(4)FXsGAwES-4;b-;$-+=VKp4w(W&?t__kga-l}4rnFt#~ZCD zTPRr>MHH3HWVQeFzI^`Fj&@!ZaYYht6ZtV|#B*Dai>;%C(K_hy-sZ$hmq91P?xbkX z-Y$qsrYf$}tA?h@3zvjIQ-W$HY`dMVaN(bFTGdYq{vats-%rNYR#H)Zl%gFxnKbgZ zFEHEnck-oT{z+@0We_$uL-3q}-Ik3YWPkJhSZ;K?y^R#l(A#t>4+>X1=Y&V71Dhm$ zm+UY1MVt{23nTqjE6t`5jYA zp0Mo~S0|hENW}fxxVZb+o*aaiP&2mliThwd%UNWT*47qXZ;H^ssGXu0bEgYqec!`r z#hpF{^Ukgl)}PA=8rq#6&%4n}dB|h@npjcWCK-oxr`Djm#81DB687W(L=W zbrMwwCeG*5D6cm!Iev;4^Qpe!Xq=ciF;Gx%&0}uaFXEhwNS72c^ER8Jb>wuHV7h9}nsXr-LkU zv}LgSzhL32D1Pw?hkd_ukmcTzArbcN005;WfTPktR!yL^nkHD*H_Qm-8-M{s_i>9$ z0N?Q93c7g;C;CzL!``q%Br~aGwfvxUX3~YYFXbqn!GHolKdBr4<2qx2w$F=`0Hd3L zE&x!F7Z)Vw7pUfg-1K_^u)OUJcq8@T~zkuvXu9sJt^V}vaH5((|6{esNM^1I6K3Q>?+~EZ-C4WW~m@ zq!PkO6pYIa<)eZy5!?7sgrsb3SnB_*crC3@;42k0c`Qev5ZO;SbYhb0jeyhtkKNXbGMw9Y5F<0WVA zB^2*b{m4qv$4K2$OKAv6asQgyZIv39l$xCsUp$Z;IvracLXT9$sdJPU}3{# zdlu7v`tqOj9F7dWlhpP3WdF6SUWUv^x>M3w%C#<~ z<1572`P8vpO3nyOL%4-DzMH1P@21lBAp=W0pg?O>KQ-0-U~J0C38fp=Wry%DrN&At zROqM?toOX2b=`8VZGBBO(99e4XFWfUV6vD8Wh2G78V(2zHK0bjhd?gE)Y)1YS&lVS z3l-<38-ns;rS~3qYFxGI9@YDyIu?oyg{Mx^(zU;08|O90i#741ce)d59-4fp$Rr`-4q+r zxf(JTu~Mjgl1dtK6zkJY8j8cPBoHA5CG~kjkaGN{I6$K?tf4lnN!S5PMYt)kDX4X^ zs3HtptlkX3Q|YmZF7jv^UThv+Z0fUVR+6pg6losl37SZ0>7~Z&U2IvnYgwUgMW(cf zA;OJ?Ti4NKgu3}qq)}419T42l#m zhu|^acfiLxd1<=%mpVg`DB4B*LM~4{f#xpJr7rRNE{qqrgyCIu!z~h?|7B9=h7WMH zeN66(M0BG~>Y7T?fp{@>yWPrPsV(Pw)S3ykN;{Zz5)8Rx(2q;7e1i6Ud^B(G$J7MF z)ZVDCVR1UWpU_tI!(MwxpHpeCscoM@X`k~_;y2G851sx`kR<2fK2TCY?_8p5YG(?( z-_^6nQ)wVdf8g72|9AZ`zVQL;=01<5zVPq?pYWbsnk0Xv{%V`bLYhH8rC!I7#5kRy z1f`*`o`Z}pun0qkYDI^_l7<@YGuxYU>%s>^If5cRalDdxJvj!7d50&LhT86je3}QR z4u=pl!(a4=YPnG(h}5CM)Pg4r5Hvnny&o>oAKp(LnOhn?EFC%K9vctud>tNL=9%9+ z{D{Z5k+V4!u0QHfk-QQ;W=0j`R*`$fJ?>{TjswN#qM9HUo8T%YTC-}QNSmN)`G-U3 z9wr!QCz-@1SqvuGye8SxCNr!*KMiottd_qABD9JpJ9(#O5N~B>v{ckQ53Q%B0nRia zjZPtXy&iS_eEGv%JL1{b$Sv>0M|w>{QhLI9&@Ai7YarJT^?dTL*KbU>oG|d1S zH+B0A;7rMRuOiOx z0RfgzYEas8Ee48#4*YpDZ0PkcnT)=(yiZFG$9sk)Y0cV8EdZ-?GE}N2PoIN*A$HRe zH%MMTPTjj35g8zz;FU|GuE#ICfcM_58mfl#T+L%v)!%ZLObmXz96+=Lbkfb%9- zo(7keTydIp(^_B17TLFnaI-f>cx!KJ129#$%W#(X#i1~aQyEoyWa%ZatBH7(0c57Ulmzl^J;D*Y{D zSWt43Km&cj^&A%`ojCPD^I0e5g$OgLMIK$fH!I@UK5&cAWo3$<{u@8GT{wiNwQ1*P z(;RliJl_9Y9e0(+d8DXoIJ?*5p%Q7valL3yJHN7{I&Sf~25PXwx#l%(u3hux`iH#9 z-&hT~De~wY_Z67u$`1bNj<*U7O++x&cLNDv!RfoVh~4l=!pOh7SFpVp0>U`My(5Rc zL~%OV`n|i%z4UYfGz8h<-6zCUa#gb|)OzDyv5!~7{FCk=P5e=1t1`myp#I70mq`bO zKbf2V9*|A%w;v%{_z&Bj9QHn0d=_)q5P3L^cHy5L)*Ky98d|m*{{E@*d+yttx>nTh z*u38>_WNMsqt(`f&C&f|-;RpRj`sh4A6hw50Ue#RN-m2ZGqN6cQ5^5tA2S3T=T&3p zV(4R(A5V-PUymNO15dEpPL#Edv7f3szQpcBYAWcQl7Bx{K>Mh(-`E?2%rZenNDw0- z_6H=0?fV&96v)^cq&0qKriQHw1M)vT7YBffBu*(Z&M8+ zOXCxe=J=(d8pts7%;V`lvSJwp{w!gcyM}${d`hNyBRq2+{E6Je0Y|d!@=eCIF$|mi zA77bLI=*;jdR5epU7vAh-;V7!d)JD(>s7g>d4;9di`0|EHf1<7M1ThA(HD%q7iVlJ zpq@8qh7g2qbmvnNFl1UAD5?BQ~d zHp7M1_^r0lnK1xYPfuGbTw(Xx$wx7oBKTmy*dm^qPt~vZ~W81B(Yv`wCyuE9zxw8|f zsHi4qt}A(Y`BFRDLDZ zi&FAcf1vB;2ePzE9#^3^wlxPrfmWGiAp&KS&*&|`x*H5V*GT%zQzHE0QQglN7PlYl zYn4E1Roa){VAlO!_9+!*5-yedJ&jj=Q0luTPDB}cVJmPP>lX;E5t=#DY~q`gwHhf@ zHR&LJKR`13MQ?Sc={+7U+*S+=M~wV`T2(IpSye!Qe9Rz>AWRGl5DqRr7A8I(2%nG$ zLjD8_fe;hm6B7^<6G2HxNXT(PloXFBSvaXk@u)CKaImp)LU1^rnQ+r^qlbq_fLAh{H!P3eC|!V)Ku}QdnXuwB6^CaUQO~*9grrr4 z`MF*QJr_ZbG|y{MJ1KOyu$YYWb1rE`BN@f_vLer9r9|XpW#tvk6x8*U(DTkr`F*I0 zz*7~iOjRuxbv&rLGT&Pjbq(}8)wGGyk`{TdDE(ej@x8Xidow2;ZEgLJ+WJ8i zW_q^fTIl>?N(((L3v+V|k8Vp-T`MbB8yjnz&r!B+CH4+q?9=8xf$%=*s5wLrI0le9 zh5mH5S9P&5b9HfbbqjJc*L8EYb_+rjx7Q{LgOkWkohZGjo6{Tnu6%~}06_-|ymDcb7jE_JdepZ!7 zRgIiiS69_mm(=y^*XJkJ*Z0-;PBt{wH+vN{_cXWkz&pg1x+@a9ySsXt_jmF| z^~II-jrDn(_2s4Ym5ufF^^J|?%?Xx&VZ<})TMGkQi!<9xd)vFaJ9~e2t{--{26ngR z_U3H&7SH!LHx3TA4$gKC&mVsOK108bb`eMDIXycm$UB*xI9&=k-3&WB`hA8bPN%=m zPS4KI?=J2hF8AgykEgBs;RmKw&}4WlA^q=BuP#EvnXpjmNB;>B zUQX7e`HxlQzu`==!B8zrL7m;kKdXxUTwUtnf3cYlDXGnWY5%dAG)~51)1G?Ab@vdR z30=$LilRZ>r&YzJI$6RUK~iCK%C~D9fhlCK1hoj`JE!tK4y7yWF&+B+5aYYj+^;m5 zh3c%v7yhX{UFeVHsuQ`dEZ7Tb?rX2mfQq!QI@?3JQC9iRPwO`Kc}o=(}0e{p+Q z^VytvxVG~?^mm=d02V=sCCA6G^2J;5QKLmn@{>BEhM}Wo-4V`rd`s*2)ceOhaqQqkLKn#$M1K)-j|ODlAE+DSl5 zb2dopqJP#!*=0Lq%KZzHGYFjUbsWYVS>o*_S+hMKzka4)IUpxQ6Fpw@XT5s#<=yLx z>C17Z@Tb-62C7|k+9QI#>QWKUX5M^Ey_}b~Q0iH*`M`%w`rg|SMCMer?6UN}Uio@O za~g#OB2~DD)+2pTJfJn-$3Jh@!ygeyuYQe^bX~UJQLdASEf>Sya&c|F+VIslt=swH z@uYD#yx^zseqmIr+d)po-ns=d)52XIlT;#MJLy>x;><8+NN8sHhVqs-M9JTn)Jd*~a~l@K+0H~%pe@b%nlD&Nzk|Rk(U(ZABIq63 zs$$13_2lP7AV0i=dIs%Y(5(pM5XK-_uJ0q8-~T4;A0OiqTXr%IArg!J=`PDE?oGgBw9OFsrX#`gNJJ+_-0VtMNWaYsr) z?2Y^=*nQD*<;V035+H^|0lZx$OxU>9s7vE{+H<+rqGLrW48sCxr%DCl^GPUGub(6t zcf&g(ivudIvrcI!=K>;ZFpw~LD(iPOdfDAL9!MpLCsAlAhW~QHPbDskSh`U5)xs!! z+;lF4wn#*Z>TQh7bUN>|!WWOM{X@+)^S9p=iG6R^a8igXAl52S{W<5i$w z@NzP*aVi&9Q}8Nbe>yl)uvm3u@3nF=Fx`a=m%WT#wQpg{9}^=-CUNJ5#=b@j8(9HL zX-EBw`*b96^-t&sm*MZw_odsUrO*~1qve+q)XpbhyLv1-OCvw~Y&D|QW-02Q$d2^a z&Im%g0vy`Lc>>*8L^_Da;+%7Cf{MI4E5O<)t5K)Jg|~{H)=oyu(w`bNeN}F>%xz^> z@_z9)KEL(S&Ik{GF0l5ZvV7T2N`zu&LSZ_Kjn)R@X05$J@UAXt=C?WZoywuLYeSZp zosr_5!Lo`Fwh?QId(qwEEO}jB11pbG#evSo)w`M>Glx<+CAt^eS_KIYpFY{C8H{(5 z*PZa}>)}|ViCBCUK!!(05#KOfC82S{z|u@e>DQ>aP}5P~iG&`$@gFcn-rUS5Z)H*a zUzPO@M;A{lEIdDuwUM{o{y6@^mtxXVJlD;i|C;*ZB*?9^9XsqMpXo1y4yV3Q!*>Hm zSJZ*djILX?sh72mH1)>z?7M=GW~Wki?#l~rDXK2T>}>jq4TxjwP{Th1EE5M7*N^kx zja0Fox?SooaH>+Y9on6`oi~mMWqj=uVi$CB^;kp_G>*NR%XcpFSQExcXdd=*$`^>( z6>n=84Lou#{AFvU{$01*J?%uwu6?OehO+h>=)`)+X3lOw06U_rgV4FWupJxT<1*49 zYOTdE?UKh8FYFi~q%A7A&~-wDe^5n7;p3Xk>G{?M}V48u;@X>Zzt1 zh<}CroA1*}r1B{=N_UN<&do~I0V`6mdEIyGri&x}^y@118a?V){UAx}89FXxx*<)- zW{TQCDU4#wSIc>5mBx=^I&+6l7}Lkj!I!?i9n>gZZWN{l+$ZlC_o=ze!V~(f?9sOdF(RcvD5ouEv`}l0t8*Hs++sN2-#HJ^@Mc*g<6)Nud$f0posK35D?Bp|I zDg_<XOiU0sSogNVn)=e?3y=^v$AMNmIOGap}7k zE#iDp?lVr`uEU<9qqdPiM&Y&FnL6>Sx-l;%m=8K`y9u>=`r9$$ns#%W{_6^(_}|Mg z)ICv<#~t%GKMof1<>!SyP~46yvR3NV#|7W;AGsdLx{?GrVpF(qCVYM*`{^++&N+f$ z1x|3lKzIfxI5qMit0y2T^r4dTp`su>fDtmW6Yj$ZPms`EbkUck&m&8pJqE&+cHc*G zK5O1SLKJ>$2!g$FKOqW2(nNwa2EucIA2WpyQ@s!OoZmsapMWp9v*Q7)Ieoo#-#v?@P8DC|@5;i5nEa zPAHEX9JU*HDFHG<;Bqs8k0fcADrhG^LFYcc-4-P}j9Z56Ye*nFI)F*G+^$2V(&-m$)mFQECC-N{pc~2(xf69P<9EMGV zgxS#H%sOfE6`3{^t%)!4>_4;7t#D?sXisV34(!?B{-QOdeB;R? zAFU#*Qof;EadPTe4@%KH8l6qWRsc$&4i0&txYl7&fm>;@qHicwA-$9q#VcKJp;l~ucR%zvu3RPdoB}b7oj5o0yF4}+b%Yi3`{LezZ=gSq!(8O&UtkMHm)$B} zhgN)E5C1|rvf>?V)a?MGzQTru__9>~Ljyi@Bw0_8=Rhm)zzB@9O6Gxbw(})xJI<(R6W9GNAx@p4yA>{Lo!OOD zV&ukU{=OiGi0|Zth}05lS=EtJg*tX7HdGX-B~tCIK0JSxY+15zQoPZ(*jB4pzl^_X zPm$Jw*w0tw5uzL|MrHSlu5*;)9UK~Ir6H=NA&+V71t<0@huyQHonfUiq0`Oktsc4Td&EW(@LKC! zCQkUMqh4^dL24m<(pP`2w4u!MyzV>rkVRwa*@|nW^}V6>Uwu%=B$zw}&{BHXaxA8^ z^uU*igSNx4FL8}3u?;FWn&wiII~$umgjU==n(P8K|4hAr$f}rFs{akm|U0mPRmA z64Qx29Kx}q_%|GU52y8TSnGW{@jkuv>F~uha+sYW*AiEMX9{LUgLh_EgyvM_l(mI~ zH|gEygy~$k)p;`PeHj#%H}ks!KX)GW?tGWF5O@E`?NI2uZ~gns{Ig*d?eizMpS!`k z+w^)qKeDMQ{bp0#5wrk;{|<)#dq3uNGAw*`^V7z*Ic)QMd_(WiW(roJbh1S0 zq{@+G`Pt^aJ)M;|+v;^r50=HDQZLMwUit=!9kEx452|lHI71I9ypwa;WWM^RrRXrW z+A*|9_x+iZ4OgtSs}i+J94)GP-q#RT)%?a4?TSBx2|u$gp6Ie{F1G0}G3y{iWf50q zSKZxkTfU*P_t};IkE-b0`NrOBdd^A4QR)@eTID#Je7j1lwljs>B@A2zk6&|p?_~Sn z>J6IG8A)rvgVr88Hjo5UpeDH-{8MSS|IJpe7`}!E>+tJvq``}uI>@CRHju3JpQhUj z&9=>LX2P~J1N)D*!W<+$IgM>D5|~9qb5EzNr?bsGvB5l|ch@u72KVMOt2#eqby)pv z6Frk<<>V>4N~6_!&%;g2d&!eMp5?va=h{0vdug}Y=2DUI7cu!taA6KpK*t0) z!@{2RI#S`8|Fjto!fa_Uk0UT`0B*$T6X?P-mfd02l0I!#PtNha^ky*cZ=VqdrjTOH z=rCI;OqAU$(!ntxD>I3Ogt&XnqS)O$?b$El|(DjR?ORqI5mzWAp3OCplq_Hpkn{r1u@^h^l9uTR*miZ|^;M zskh`%x~ z^>$8(!+R~H7yiw{9+aXFOE9OTxQ!_{J?bBxqT)UK(S^egvT2wucU$@#VL@0tD#e|G zxL-&k|Hczheh(^^IcNo%?6@!>hmQY7BIZC?%qs3iAEj(6{_8N|&doG(z~DDF(I}+l zEzNd|MWKlL;EWY{S;#W;edHk`9FY)K_!f5SC@+W!a+-2)K&6N4^Y)xGx=JOwHlrIYE6EFdQuY-$ zT#-XZG1;=1=BB(wi28lgE_)c&yr?7|Mu~<|681IBaYx-*d&D)nh(LQqU}gG%(v3xY zwH#jzjCBZ=G(RAUCD<%zj4MGm(4yow@F0&z~ojboe?te60|H zTt1UHH_3_&X#DC1UsZn3Q4Y6f$z+xjAs>6A6MLM>VPQk}!#w6XJD=EcdMm;w$ffrh zANKA#HxQWE6P(xXAZQPLHt8UT(OP>-_C5*Q-ox$c@xxy~F!o;B0*kwWo(P2>ERQ+- zcW-&ulZ+D+DIZ|=>=S!Qn9C$g76~(5@iA)=cUg#O5`UDp@SvJpj_-iC3=8zKO zBTlniDfS_rOM(T8mFNL~JJ|15awd-bH@W^+Pq>YL*w6xPxfpR8n640`kI=szlVe*K z(VHchV{Evh5m}Xm@>v8`Ij~O~mp^TUIpy{r3zQiFB%6pldC>?|*#{uj`si`NAdn7V=PA8-YUOSC~x8Kx*k-b^i714uB+B4G5u?O^t~ z+Wo2=yl(MnQ0XjDZ7wV99f z{7)NmRe!E8s(N+mzZ!i(oLl?e7t-1-E{}>0+3!1j3yX=54R^_3k&D!H&e*IumAj$n z`keBG^}lPsd@;^FZQ=1BMW7KcwUeAnwEi7kk*qCPxyF~q-78%Co%*?5;l_PUT=Vds zS!aLt?;bT@gra5TpCzdF+}^;VPs9zoXZWNwYQARYCi3C2Hxk{!2d?uP1?Vp2eg`Gh zu-q+zGpP@K*WcVL*?=zR;9FI6KjNFe4w!e`8l@+9I>v5*>-gXIbp6_XTCsr@FT6#@z)ojhe&@9=PURkqnMhWCoK zakMh+%u(8iS83FE=M!&TBJ&frzTLb%t`!m7HFJ5>!A5ZZ|l zv++F6&mb%GZKVmGA+zA~o!(xFMy(s?-q-foU0d^u{fIkIYVVWqef z%GNZ@uJOS{H)m@LO2~nfH{{e^a{x9OMK(a@tlZH+%~*%QHL15OEhy_GLI$X7ysl`Q zj2c`vwIT7$4AN}J{x|XUl`XYrLZWvqqqc3m7U-0)d&kh5gqAIDcAahX&ij)X44Xuf z4Cus#xb*c7=<+0K{O5e{kkWh->-z5=jg3nobuQuUp6hkigoMs2uQ+Q#)(d+}OH+>D zp;PPWJ#llE=v&qC#&6$fR&2$F|HLNxskR1!GwW#ub%7iI>)WCehVsu)mRjsXxiQ&p z`RX1T1~ppYoo@GiHLlq;owMk6+fb@vHxo_vEwd#O<#G)&bLJE-10g7U)s+5}SJn6*^}E>F z*}Z+QY6UZQJ?`!$CZ!;(a|`fM?nW9N!)f&*GRDBQTfKv{wV|D!)i`8{SYw_!H0q?* z~kR25_t1@!vUZ z7ZIgxwGNgd7;O%Mxi-U90x=?^7*5s(%pSdC?L5ml0BK%NMbb8b#JyJtzCfjs4@;&R zL@y|!)*7)=Wcb&kTI}T!Dz+OD;1@`Tbsdr;6s2>UiXnuqkfDAI;HkAP3ixajQ`hc+ zp{Xj^mZ}8i#aW-M6nfb~;&je; z-Ws}`@6sBXmk2}>##nb9?g{<)73_1S+qfuGIfg}z#8jp9V!o5|qgtbe>ey*+;%=go zbWlA0y2kHl3GDp*W0V;L;s;?_&?g2VnzrAU{D-I6ZlKvRfgHQ{)5+P-)c#>R>hk$7 z?{}=|(aP2Od0EFZQHnqXY1Ga>9^965?TI0oN#bHKHvUAhn(h>YHg(^xy>l^AC3_L+ zj@X0)z-;B#EY<&{Be{D;uZUB$yDy{A`Nm(n-53A$w2zBa3bpoBKT0!NE*yNVhrAc< z$3H5t_c}}b-`<%Y<9RUdJM6X(ky@ujd`HJfbht>S(JSYhJe>LHkjmE}UPoq2pXSC$ znJVQyUJk|G+0!__`v*I3kIe3nH;&P7Kqv3I7pi&dsZ8x}(Eau#gKTezNq#0>VMn*a z?1Y%g&qFd+v2rqxNTZbJ%+slR?XuSPzH%#+AUv*q$yiSve%Vr;w#Pp*E3pr*d(di2 zNugEU%F1Z}7ny|z@Av+CwKszw|MgzEB*9emo<(5YjP1Dv5J^jU<)Qs2@8LUP?~=wq zSGAePdlV;qqTb}X~qjJ?sNZCX$Auftf`ob3TY~w-3bP+qrpUnqzc5w;7Fnz zsTN{wVj<#LeqDsQ)-csRw+k#W48*!&xatI8?}KEDkgLU@mJuvmiyPFAaFV3kiTUh$ zn6@O{UIJ&+d4`+}dm550<+{hgLL9DW0WfpP^#c1=29y!xgFI3%u*^hxvCJYm1&9OS z1`)!L4rYk8f{kiGQKQQKYocg zFBI%rvRa5l`m@r#f#wZMM5U|9D^V)Dq=-;J*j^|IWg|`cZ5VVx_+0vtxQtMVvKJi@ z+lo@WVj?9dZwde5feW08^bm<4KIC4Kv?*?)-Gi!dO9?i!GE%53Ce4xIAw-o9H{W4P zOR)0xhRSzUg$I3Xmt{>pDu$|-*bEJe%7v+Xms0mnvdX&~ibh36u5KBl^=QjJ_Ye{) zJ_Qk=K#s7|*8=x)x1=Xd6phaMdcW#Vlw7W~9;np4U2X06(te<_*s@Z0U{8qAE1i<9 z6G);GSACKDLP^k=Y7}C0<&DyVx6#O0L05b+DLf7pDS~ehBX_nVj3sAaB1o0A_U3fW z*(;(5@Ik>UP$z{qyZTB51Gdv1bY0}TWTrdUudwZb>kV^PwF?-uG&>>i#DGVlfX4K+ z9ip^E4wdZ+s)eN6Pw>M@pqf-_FRhIg^J@(p)mpEc@=GIEas5OBMPKAK3_qTQ^pe(Y z6Wv?3lLEeT0{yYpJfFZf%^3zk@J*|^6baZGg7Bdt+9-VY;%qZrkc{Dm@O#3V(zY+k zNE86gfk>|DUalK|CPQ9JDBE`Er#}=RM#zhRpC3F{`Unn~qc>x?-y#4rN>6Yf@@i$< z>8NvReLcM4hiYj>og`dloPV*o-LU|DP?%xH$Ve4t9LhxBwnv8#^L0lJ__+on zp&6-}=r7ruM>e$|N^w2pN}#?O#@!ux^9psy6`dlsecQh|8M;7$ZAx~{Sf<^!BAi!g zlx@;}PSpQ_BDJ-=Y20*wLxajxq3l_qwH;cfh}0X7XfWMhTN>Q8A~gPCImVN8Ul}-k z8UEPmE}B-2cg@%>KAmVzQt&)>&jrO}p47Iwz1(d4a%wCQ^CU27YrN!g@b;}i>Q91_ z((7Z4GLH6a4@qmfRfgkN&%z%#&Tiljc#0aX>xg+)fGN8QW+?$_ad2}nXbiwhrWSWKA~cxn%5@f_Rxg6C?E(r1)B(~NLU3KX zz%B%lAc5PrBbnxKM8q(WF4#k@Un^2fB`6vlo*;w~toh6C5|l|h+!*2=rV0)PIPhUS zBVj;-Dp!3NB8Lb<%u9Vm2z?)}vCErLE0gKsMgyl> zRq|B6;UN`^C#*xP{{`1+ZQs0#ftKa;-Akw#9Y+1aKJVF8r1L!A{0egCuz|i)k$Hiw zrJ|kaN*}5}$a(~o6es*39wxpaE+6cr9)i#*?1ImRJ5qeMke(0krM{ruB|4{OJQt}r z&ur3RR@inE*St~L=X%SgqmOLp?oD{f7<&APV9=$c`Z)8kcA@*?`Ax25_kr2rWXag| z*8aff*ORK18~y}^)hYu-_DdBx@L~+qkT09a>&I?~RF8lLu3i)+b-}W!R@7df_apekD^YMw;?m_R~V?v7KGNP`!0e+I+s8aI154i z66mE!(TS1UD2Qb>37^eM3z5KSTaa5ZNF&Zu{lIHFELc}Sn1W2QO2S`EfYbGPn*mhR z65L%1-$qJLZl0YSb38EQ`PP_uB@B9pI5e&F#Gtn*T@gCm%gWfBiB3v6|JKg8SYX@dA)H$^vT#Xt1Z$wT zYGg~qJIk$T8@G}7u#CsT^!=Yu2O;!(nZc}{L6U3b^WoU%PtqT4RV8nJd^>u}davh` zN&3r9PWl>JMQ`o$%p0C}bohLH()Z?QOULi&{P9?C1sS~KU2%=-eFxvg&c34L=|6Yf zH!dRoItllmv4npaJAbBe|J!#LQ0($gFE6~9hkc1ZIUaw0N7H}jQSosZA#C;zaalvl zo=spwV`>`7ftz@u6!~wYBoN#6W9Ih9nHs&wPhJ7;iI6&FM!n@aF@$?Vqb^22ig0*l z1q-Goe|GzAppq1N@g73?OrdC8)xALXb*QIKWIN6#(m+8Ts?9)WLBu3QxTL(jwULNfPB7cZ2;SHf^RG`Mf3GtJb%^S&3B72Gr< zFQv**r!JfK_-$Mg=}moPH4_82BB7x1u+~fHb|+py{gmQC4S)m)0JwT7!(Iq9c|Qx7 zL$FamlQLCR5KDM*=O{8-JffH9k%Lt zPCAjHjjMc}t>`wpJAdN&$BEGt+NfpaJ4dX+>(R>W(7Pe7!28`o(oJ;qmhC?Tj~6`278(=z{-jRSjH(Ok~jNyTsEERV>K{ z9~W<0O{#c5zgbfL(&^3c`t7&0j;iLa@!jwP;XRe^_VMM)hA-6*U)TjNELZNZs`yl> zeM6;dOI$pZ5i%@8y)e%ls8%`&?s9(baru4CiTA3ek-uL=ceGr;u5VwI_g;-2*&dj% z#wWdH+teI?>T0K9vWRNBVUQ1IgZDA`QgeoiW^SHoc_zi#oQx?H}Xh`v7wYP;CETR0&5u3&M z?U;>mO-Q=W-nEyV@p1g4qKwoKbn5WPA!sviZOYNO6yD0Eky7;XQS&85oH6ZGiec?i ziutIb*(`NwX*m>qv@a#dc+}A3w_v@V`S}#Xp{0zAOT5qd1|{gD(CBHGCBySef*Y1i zn^Vkhr~j|qQp9Be(PDgr|wMpUUO2?1ghd;d_~3a^Ka(|UOGFyOb#{BPhUTDdq?te_uFuy6bN|t zdg|9Vm33SLs~OJiA4EBXTKvvjaZ+p}QU|)F%=;*3G=x$=!1Vxg#Vk zS}fSog2uAmTn>1a7XmtAxUYl4S&~P2KEL)Xa@*1nJ|WLY0A{6uu`7n$H`%hiEuy2b z#0P^@Gj!#UBKN=YGRdJu7Ji>As1X`^1{RL(kLf$D@6GcnRWbHIQ|Q;(9KXf%!I_+( z7q@s2amjqA)KPfpK&Z@BNhsh0RrQFAsbjJy-pSx`eL zbvr(O^@(SkP=5`7Vs!h>jE&Q46Mu|)3`MUXc6#8k&`jD6tlh@})42`>XXkNv90GUH$!sL)#wyF)jBRicHtKHhDN* z_sG54%!owO-gMrrkC#wk$&YMe$p;2Lt}BlnG%cL_S(APUD8y{1HE5Ulw*2FCVp+pxiPW#-MOE)c19b!%%nYd{`uv;g!ApMUK|*% znhB1+WH`OK`cl=4!i@oLFA^TOe0XO75j*Gl25y{tQhj2b7RNAi_s2HAL&nnN=(sKC z!^iCjwy~dV@|~XUx}WFzs_WEH#CT}N`G`+lE+=)5V^%8vob2*_w>{FioC71OoL!%J zRCB}A&sTfX*sv2OU9bENjQ%;I$dIYHLg0_Tw_pQc9CNv!=Mo$(ctmNfM1fSqO9=^t zu^eM_GYyDj-Z$Zqj@O$W>{r!&cI(y;rw3`5ctPLFCgg9*pZtEX-{Pw7%ZXi#_w=Te zot_WBpv-P|*u@^nnLRfXx8(jyE=h>@jCg*)yw_m!Bi<@wLzT34$G5ps#MeU2jx%j# zHS!vhFI*3`rC(pQwhaHH11OiLv+4X~oqu0dVAB=3>79UOgeJu9D z8+wp&*Vks-6n4yp8+=|wXLJKmIT2ow13=nbGdpX~EZyYuwd`o2AtF}rvY!$IGb%5j z`w5{_KBWM5#y{3tht0>&@nH&haT;69EHt&h`Xu4NKu<7wN{dTKWFP3Sl%Brz+ZDfU zQ)T?0&4`7cRZwG*i0EWGu`q5(VL)Kya-RA@Ytq)fq-!EmVQg1bGKi8mtUB^hXSE$C z_y)n@(iO`($DFK?%%7Lwxl@)A59(a+gwhd7>P0L5nR?h&S8^qc@L`)9G_hY}W!VG& zggCADp(&_+@ZW>6&;R=!oLHDAj{T_Pu{C^Lx5c^Q+U-j#`p2Op!vdVrGdMnKctqhH z_Bd?L_x6hi!j|r3H7Up0G6Jmnw{u_eW&qh*KZCQ$+%gm41-Hxvs;*>N z^M)*AhW4f;8A32hAOu428~)lyp=GH6JRxXJpN}-mHO3#N1GlF;oRCh z1$O&R)HgL=^|_u99!JVjns$2>`rx!@;>lTy-HxjH&D}#!M|=IIlLmf2V(Rj>KS?su zErO#|;4gLSIexJH^4y+UjLG|hD|R#|ueHBMhZcfQlZFk701>FSWBft@-4HZ>-|z@U zg9u0xVCBj_1xSEuCy9YFcZ&A3uIbR4q(PfK5X!F-ubazO`AULs)uJY0yx8qXx@_gP zMF2^o`g@lZgsohbA`<0M`fs5$^X39HIu}sBLgPj~)t=CB`#n*0Xp`gGTN8u3e_o(W zC?s^I!N67Vfsj^x{e+vU*mwG#LGi`KAR?D!(Dh%_>$U10$UwS zIh z;j-`fdF{8I6L+Q$T}V)G86Bf}J^3?v%yYb1J|(s~rNH7@R5*sp8+HIn=lX5%0C9`05xeULWIH`LYOQyhP}&#eCE<#x5< z{K@;BoutW&0lR?uZ=t)UI2QlNG&IKu0Bp(E4IgsuX!Ndv~$yi8lRH*DI!@Ox*MFF(Lt?i=E*{k3=NZhbT>FVI!odpl)@4 zKJ_o4nkzGQ1+cA5i@UzfbAGoY=}6qfvwpwlY0o1kvvdBApq=Ry4qv-os*2|+XNKtb zgZfe!x~7||P;dNUHWQ%1v=nLFtpLN{6^V)n)B5F*vH&Qo1yB%3Vs&dzgX$B@0Hu{t zQVQs^JWU`jPSCA8*iDd!2ekEB%;V5|D_j}*QI)=En#?_yN2D<+F?7|zZoT8(lw2+{ zm#d;1sZztx_)j{12+(5jl?P$}xoJg2o=4pCqu+k)lBJp(vEvOvVb#vW<(6g@R6vYL zbff;K{_wGx9cF7DqJmzHd!Lz2dS#sdcsf;e^?tqWp*!9+lw^^&SZAMk9e;kjzv^Y$ zbHjB}QCTHtpBuh8@i+r38gDDg@_i9z@R*bL>P}LWgZ{Jek5SftCUfvQ4))OjPl{d0 znMXc|3B7>Q7`@k@X9%6vV}TXTsu4D`$ia-d=j%_l2DKz@7T^I$clpAj6h;FXV=;*b zL{ev;ms+4Y7xRQ`Exhl_`jz%_)mHVcNXT1vn@F0qf-vpu&a=*xt#$`h>2Tu+re(3r zCReJK*kj1@)C%%6i;$VnnNEYpCS=(EWmwXfX256#Ht}y2@h#JDj=1MK*Ko=UrA_Arb z^q2&B8pOez;$+U&c=EJq=9*pTF=$my7~-$gMF`XPVq#)|-5&MG{~9hWnQqZ|cQ1f_ z$-jK1hzC#H4j*LNec)Om;6|CIc8d(l@31B(sk#(Kard;I;A&?=2pWKB&qR%JBSslo z6VjDJP(Ola(&}mA!?cQHuG9tbk{(SVgEl9HE4DbNyQym3$ohAeieu4+)JH7yfsgSqY72X_vzlqow{&94TKmTtZ$cVycXnKC4BDh_^1j&d{gqNb z@m@Vb=6KDfxTA-g@1L|*xCZ<3t^3a;9?gGs?_V7e)tq+^R|oAB`M>V^nsBWy8?kEg zeqjB6t$Qx_AlE%egTIWs$GMd$@&ldKCl+OY*ed;yww|?5e=+y;6r=G?*tgsM!0+68 zEHIRkiAdohynFy_=_V6n&&nV43n(sUQ-Pq%glwQCsKj-CX0_eu^OY=x;F zmznp%RhqeRCq1<$R_I6=nyt)63$99LmPiT&?xst-L8o4rp1Bm+zJ=d%scT?4t5Tlb zIOns~88S?SlgB*aLayDgOx-;9k_Oz`pP?=X&E1pK`*<3+Ky5nH+8E>xN@ej<6Nur_ zrw#Uk){2gu5SaTgcmF&{%Y{(6Jv!Dc*cjk>OShx0Y}8zzKakY|HCLh{j6xWM8kmP} zw{BdIK}xr_99mb!wf2IW9tU;A3}?CwnM(&VWj4)wT{D>sg!R4y*>f-a<_7wa<-k?u zrw}(Qsaw+)oXMaqCnOY$3es8fep0{KSJL~Tatt>RCM674T_R!ZRI0h#qLeN+D|x<} z4T*NtzEmjNTGDx2QG-=?)ixU{BbCgqdOJy`SFuKbpa6JQm&`Cvo(XZKSz}#8-wyt>ezlycn=+II<9N_~5@Du}6H6DBke^i^{ zlql2s9VgTvS-fW2CSJ&5eQFTl5V;Vp9_tu(4n^;w%+Zlhx4RrrX@whmF|Bd|)n52& zM@YXJPHFWtNCu69ct%nNtynwNE(q zils*k-=!T=`U^9j;2LvkQ9}CaTg#vd!qe;)*AUOt<#-w_&cE|BZ847HeN8~uFm-bY`= zo*u}6L-i>?aGHtBOgDBR+`L>Rg$u_^VPZN}7hYhP*LQqty`%(L4&hn*5iWqvk}9km z(8>f*aR_5`2HH!iNnxP>(zSf(T5{01xLa+m+X!Nysi3tq=|Ni$P?X$t%~#g(UNYwqm&dRQz*VGd|+BNPz7b+ zLEQkX=laTUK&2OewM!|&2$c`rh7;X5cW`7L(q=AaPe!}|lbH)rT2j-uS%9M<%8R!7R|J1Vz?c-kz{MhBMF^8V4 zr+(MBVDTgxJR_FORAg_Q*hix?P1Izx#rjkh(;5#qA;B^%VORFaX#W4SqHh|x!pwVR z2Ez>Pzuk6o%q;V*m}#G4*12MQKR%aU-Ht8Oq$7uQk+fw1bOaD203AVxCH{fA0u7eZ z0dEy>x(^jY2a`d?7Rl|kyAV_A=&Xygzv`{B^6Gs-lC;H`4ztFgjioybq%u9= zPOv&`u|MFl?#ta$m=Ar_d$jjkcJoaqVe;SF>5keM-+L}tLXCo4=Tu&N9ERZ8`slf} zGe$BMK~MljgEWP_@U4(yd|`N*VHk55Ce$`@RScj3V*qC?(^7-V>aj=PVOA#idiZ3Y zp&hE-`tPQ5o#PMIJh_N1dEZq#(e<&q|NOQ8KK#>BaW+0*=b$$%~dV*#Mg8Zc9;H^%^CPQE^dyBGWS`LW|Oj}Co!3P*@L zH7@^gWy)3vPN%@VZYyaTvLtMf*^N`|6(0B@8SjbcDBpT(_m`=*=!f~c+^>&>uFEsH zT%wx#)o^`*j`zZWzW0%XUMh;U7@Gu>m8(!&Mct4YqSBdQNu>kKG#ts;ahF13dPlVFGcRPe62IIDxbI{^lNCHNn2&9K;!_d3w8#~@P z*SkIC4V9A+zw{Hgfb!m($}gtRUt8Keq(-4oXi|W69r>Xvk6_>me~Y)BKFkW1!Lb?? zbGr46IUg~ldi7%tD|ROe0j#(&Ni~C04O4z~u^BWobF2YrcS-@IVuryRlgx05G=wcB zdAI~?BA2P#wsUACmvVVpDP178sJx!on0@O`S!2fK7o;d^)=_hkb;puW6}RS`{25{S zqFzzY$a>v0e{+{thuw*5&Uf~_UoS{DCi<9Pz>oTxpRchu8!{YDSU)A$17z%`)dPHD zClsTsqumZK@Ou~s<$4I)JqtXFsaEw}dj@<#gzbg}s;08Gn)Il(xQehwt)UXBu_wff z?1Sy7&a)d})>nvC4m<$Ub+h}`ckGzirM}(IY3tcy%dkURPt#Xr)fOl3Uw1Y%-l=VD z>w!t+ScVFg>rfeU&3C1G5bd<>qlL`?&R1g-)1rI$9Fy#YaN}XUUFn}~CR&d{Tv+?7 z2Pm4F6u__Uil#U8ktXEe?Z&IwH*0@RCD{_Cas2G=>}wXVeL7O*%c$?#uFEiczq?AO9;iYWM)Rqun#X0*Bel#(Ct5U$u-8?I-W+xm3*J z)Wg<=?n3P)Ro>7_D|GJmVs1C+#&j$>o+X^05OmYq?rW`HtM_AU>%}jUa;e%kP_;A8 zJ1-<86n>W0o0SIlg+>q*Z+`=0ZtJ#|rk0zl=*2i{`)gR5@iV9emdwwQ`))2)ltexgKI--ij|UL5-p?7s7^>Z;R`^DM%VzO%5q!-6gUj3>=D?Gq z1*pS=mHJ6!O||h&*}I3M)N)7=T!qfXVdwy2PHOUrDjLSD%T%}wVES*hZ#BPl-e<8X zoWs8?Bc+2jq8`0I?u7%_rEx?CQiW*?>sIIxa|RK8 z-Y6!?6|FGNWMO*kc2-tOk@@@5A3sxsFM3yxdpkt45b883g*TKI<1B@iFx9G0*?SJT zrRsO*R_lz;++Y9e_uzhkV8yim&>B~!*_vnZ<<F;rC9HBgRmzAv4yu^xK`A(PTGEmjFv{%KZe(Brnj_tlI2vl>v@~4E5G=(N zx!TeWVV}sv@}vUl6dUX19|>A4De&$bzOlPZHM$+R$-1s_CX|ZIE1h?_ldSo~8fg_N zLXq$7M*|p>%faUB&@DpZw!tcuBB2b7pkr9cT>LZEy%k{`xK~OQ!J!>yy3Q``fHTt8 zc(s<-);jI0dY@A)qq8wyK)2!by$(0swZK3jZrvOm6XYt>qDE#}8CMcYM3fc!j7+6r zHX%od&_XOnX1N6cxTH7;_Z{LI`+OkB)X$j2%n+G18?Xp5cNkH074_8%L16(0Tdj6r zrUB#;@NLH75&tg54p^E|xxIrNh(gg}fue92;293Zl)eMDRmLxEv@Lm-1C78>}M>GE8XdZG->e1JD!qb+r$wjPQ zV@xD^HFR0uMd?R>%$7AP+Peu^960|LDbs;AhrhgA=%{K?=`~!&NUm>lv>;=a!_G3w zx_-7fcF@tm?`-xp?fs!u&YIyu*$8e+tYc7%bNfBIop znDW?!RC{7SYMmISdR0WHks{%HBBFF5DyZLlC2OBrm0J4}-=ud2?j8Xn)G9oY)J^I3 z0)Y1_+c})}lybc}&jLZZR)5VEwx^z{zQ@%IWyykI{s!7E5>HoK5bu%csis4^Ytkpo z-kj^H>*FIcH@%V`9`{P~D3z~fy9mAZ4tH(bM5wO*I7~ctW0mHLJzq&ihi(dUf#3<0 z8lLJ!Rf|?W+MZ@~56ogBW>wPI_f_Xec=zO2I;SV#PCo36ovzCOW}3p)lUHVV%cBrH z(LM8fc1Y9XQEDn72&=|V_AkUACRI;O3Ow2`nw*#e zTot!K7B#c~2J1HS{=+yDSP%agcfs1-zjTSP_iEUpk6u+`f7pqt7vCF{*4})Xr2MsH z=U+>!6{&b*2kPycyE*B>OL-UUJd-~F!%kF9r+?DFOY#g{GyR7z|uI?DAo6QFOT7W>g zGQC@y!o^n@AOgjjCn0syNN^pMf-=yNQ&k{P*$`B|X8^~EiP;P?)d0~fR-;1L^cj5i zjD~fk*2x)_la({J)cHTkv3u*5aq>BR7H@%zZ_m@F8)(?GaXJvLldBOoL!>cO4CREF z8KtU997c>g8mT)B075{kMy$U`S5IW9Qy8uPRW18vE;m*$Q05R1b|>nyT!Zepf8vc$ z2i)-h9%5h-R9YWhJy8BIYDVig8yOghv7~C;qZ7gy2mxC+j1C?Jv14*gV>Xgg2@(x7 zw5ixSF7i8D+mfO2SAboXRN~K6;$hHI{@!Iu(lUjLBzcC>F1z6GA3q9|Iref63=#x# z(KOrBQevUzg~8wFID28thq2RCLwxMr2`B`xHF9i_9J2)A1L?>`2!GE&gQB1(QBgi( zV22oM$p+=ZdC^0#^5b0Z{kn0sax?l^ir-@lS9!_ARZSiv{%XI?)KxN@=hz4S?NxZo ztX1S75ALBR1#7%vD6xeKU~>U$V*=Qmhy(gCR<}o8bg8Z?Vqjr5W+G}tTxjV=@6{)P z%YR;jFe-5>5^2i5vV#g5ivgh=>x20phR!mq=`IZ8E5@kN9gdI&N4JjdMp_)*FuHYP zbdGKuAtfT+;OG=7MamaM0TB_gFnM=f`?7u9Ip_a>&hy;&@1{?5r61*o$Rjwa0W2x5 zKw%=Q-yZJ;9Y@d@9PY})Mg+EC7!j_376g+lmTGd0Cc~A{3qduFrM4x(-NsSfI1|QUdS6LSjwb%`jwT`b+jHYQFi%_)PwY_T4w0+!y z5uu~}Y|{KpJN#@GBp{4P^NOEM62~=*r5)u58(_ICI(fCm7`R-)?PI*M0Q#$bSLiT` zPt}#`1k1%&!^}gZF#`gZ0au3@M&v$|kS0?Gki`OEWsT82=(_DRE&9^`HVu(>I(WZ) z4F0DZFpc15LjzPvOa|ig)i~HGf`bdl{f5Y}R|6*im|DgdEL>S8af}VF%ofCG;hftK ztQ7p73)~LZWtUPuc%iW}@pcx=Rqe_&2xPR;v@^#s5M3!G(Od>-pj!>?F_D$yE3XxK zW4lsU5_wTT&evoA?r8$^v3#AnT$5Nnd2;Ospn6k7=PaY!d~y5kH-o#kK`dm;xs}U` zmgGvUR8MWRm%LO0OW-D(etNhbJAj$99r#1`rjlq(jtwNJLHa88z+*1lST6rIg_X;@ zA`jEz_tbU>xlm)y{G5BtJUz7(YhWaz=({Qjia}AzB7jCq=2QqE8~`Fc^v6$_OZKRP zn1Q2Ap?Exi;h{y;JfN>J?&ilEF04{dFT%sv#UEVUi0`xHR)|{Zl-{;VTeb?{GWh`_Z4G-8uI0AqWFc?9iI78F!+=e=1 zL3-pN2@!||(29Sev_exr0bm=_u_M{5jv_zgsDl8oc`UWFJaSVYf^|b=fGv2)7Fpf^ z^dZvapeTGuFcJk&l}Mrb9b$z5D5EHZu;8nD5whENXm_6MA|F>i~|Mn*}LNm0P7$m#i0~?lkAEJ>#83yL&Z1e@i&V-Z#>PnNluf_Q%buh zFO^q9h+v5YIuP!6+YEib!Lb~bAgk>1mD@5r|G+w=tNXUdW}2XVfs{((5v#8DW{9Q= zLPn*KaU=h(0@AfF=Px9B=sRHedyL&c#D`bK%_LD)SAahj!W6;O?M;9AP^xC50J0XH z`;(CB5K~lUS>aGdr4pUaUVfF!S^jL*I#7i_yw9HwpvJVPGAo+ffiEE6ArG6(|J0|# zN#;RmX1Y-~mu-MrHsQ~;4ur533o(H82^Hys5bzH&O&h>~NM5chK#N647^aE7PXUoq z%iRE{7gDf2sTB+Wjs@8q145&UG<;Ljzk~*I*8d1i^W>}tf61{Ft@Hf`$N?cOd^Ct4 zQ~cM|#BTD7O+$UooT!ydizrq@u|jbY3xdVkN@%nMm+PbQdfe*{F;`!SJ#Y3gpgXY!0d+UDRe(|&E@1*R~Pf_I|X?_K9 z8=rnPLuHKP0pfPD^Z~?V5BNtYD$_CMH)MbnQGDdsl|A11WKgOiw*0?*LgSX%x8Lo5 zD>}jIQH;MtPrk&&IfZAtH9f2t zt;mR<5HtdMCHt|Y>|!k^uAB*}%OrpP^#*PJER_b3V%;Ij(f#)=(Fjd>jPp zz_U-jPpeE-6G!RQy)T@4P`6l9TZLB}__SrNG@O&iQ~z_gK3AaU_uG`EnC_#AsB5fY z%&{^(iHidj{R7;l?8K_MvRTvEhwpzk@pDHyaOHjotsYUS`#2;e5&QJ>?@@iF;+6cT zV%ui-3um|Ur%HZwHV+ph~38y+x}NtqYpa!{u?Z-Qqwq$9ZTSi zOitMN#?x_iKOs6IVY4V<;)%*=-H3VdM$xrz%eew|oJAYEs>_Ubs+72v@pXOa@fO#i zltzCh?>msKAPkA(2HA0)al=zz{w{jQm;ELe&<@iSe@d^#)!*9Qmf=liiCSzYPUn|SE~C~7CmTDW zwkmxN_cpFnoJOz4&Gvtvr79icT#hoi2I~aYBz&Wjm7dk8z5V#Fd?W7s!O(kir5%Nx z&)EXy64xRgtoFWByGUn02UiWg7p&vv>s$$+3_(EA0O&o3wUc@)zVgzayZ?y|bx^(T z@aCEM^0%V~zZaoF@yxl;f8@P`>X!NFTZfp3?AzO7E*rN-Vuo^lZOe*_?;Z@5N9?MO zZa9CY`|rH$=2pqoL6^(XUwLtm>Y{7kNtxQwBfi6e%1^93-FIASYPL5u)%w)9KLf|H zKnN7ZgGj^fp&$?@q!1BNGnvlBYA??rv#*uGLWNQ%>|v+Cln8NyJtR;kms(x8-v@=m zL1eCc^TW|-5~!SthrmNFSQm+u*e!@5Q@v$A74*-kFG9?q(6X%7M$CdZl_qNE(w+9L zexumKQ{WXfnQ~X;Y1dYtp0{sYc9+}5=KV#$Ziyt_pvLcBqCpB$iYZ5|*mG{ulO|um zaL?7aUw{t8){O2rNRgpaT~uFtJd>1X$Xrj;FZrKD*mud!5D+xWFw;k3%RM#25>(Um zt6BwpxZJ9aUxO^k4Ui6|(vNsthWBs3HpRhVK|D?82f|MOmgavCU49+~!)QOP)CHe{ zp4mXafB*ON0g84-9}!K#2jkObE)yw?&ZBx|>H9hPi2Ev- zMUUcT)DKV3_28|LV}2ZWzLG$URM}g^J=ugP{(DCbK6^3$6%`7^$WPldxF}Ey*#&CCI+A(&*|wtWVCK*oB*Y~TP8APo4HW$0DrRh{lt z?$T;H#)8iVdL@wqzcT7)@J;B?^T~~ zX5{;@*d;@(A;GF6#b41w#VIh8p;MpR?~f+zS~Jsy{-dP!UzFFHYN`DnxA&_0x3!M8 zNVN65$#~Q{^mN)zujc@+^`z(dP2<4UCL`k;BO(l-{PZ}-!%gI?loq&3@o?A zDz%yY4mcm}xX}2qHm=GY^XO)n+J|A9=NUKNKr{a!pM9|Pk6b8sdIaMLbmnxrYRZ%h z-RAKmxn+p8?gY(Cwvs~CJ`H|an*TJo5c>Pm@E7=>kMopwaAK$9orIvW_{v=+kA^xt>uMxZ8xWBEX!T)~l1poK<`_WqN z&_&hrgoTO!E?)h(^<^$M{-R*&Gg@XLmm^&es#X|<7VxYrxL5QUlLVyPhbj4-!MlIe7 za#PLesD|bhtqbw18O~}l2^M71J`~gTrEz`bR#e0DPSje(z@F)XjV!ws3yv{zLf{F# zlb$!?V&)AGR}1tg>muI5o zCs)~I@=m;;@? z9^UsT?m;X2QRezj5Fg#^C(GEAbvEHZQGH#%<>dMByjcq{&n<-+DN}(+&sTbioi%j6 zI=8uPUO<&w2D4j?ZnxML`Pn|Awr%w*YH@yeVmB6hyDJvl>dESFzvgF)G0<)cuX=QQ z%+k*H_;#DsyARG+85;v{idyi0{cYZDWj_^Wdtw(T>HOH}RUo#Gdv{sS6P#%m3@C0( z?`cuuh}&3}?QP3qJw@yLZ!8wS?2h^?>97BYF02&aY5rK!lRs{A%?RJyJ8}{jX#64| zT)Y2S|4Gm@e&>bP5gj>Gk}iSUvZadSd%gQqxRCa$LZ?&j9*;S#AJJE?{LK0)nU zCIVaUX!|>kWfZriW}n>smf*6vcVzt50q&&4y{o~)nc#uV2-Dc}_nu2A`g z7Oy;M`D{8%F5Zo2hGSY)SSDf4&u-%4TgxG0;)D-}XP!hryJIE=S;i=>r`)*@4JcLH zvT`u?DtyQ7oh45y;WzHbEZ#9oRd17|kEx&F_Iuq~Ho|x)UBb!xIXbLlMDr*d6Z0%o zZ>H?su(#?_46mzncg=@@W1SBUD`~d+r7yC4blDwwcQD;`RCm0!)~;fn$3nx@ml$)S zu&TUCX5H%D&L;-UT&rnI^3C6Pd<_?3Uv|UgnpQ$`wCWCcw=^h~y(FB%E`M!|7Y8cE z(gnC)m3JJSM|3`CJax5mcV9o|b9XkPYvNCiR_gZZXUtcXX${}-#);Ku zw)$-or)QWazqbb>yPIedawP0HH`dO=RbN;6TPh80M*Qn80=D@F-`{>&ckf5d1kVRs z?!jjfZIP{kRmbsmYP`FIo{0&|L#?~p2biVM;sYM9;9<450uR2=G1GZ#tL?m${M6s` zDLyA#;@3gCVrucDI~Q504o7bPqG}hM17o{}m)VuN^PWJ&RaG4ruac0rRY@ImH*YvD z+1~t8{?779B2493^mo@WwODJ|vlIicg%(3lO-7@(gtCaAm@WIcgl&?Q{E);KW!Fwaq|HfeCoi@up z#UK41`62!L*Q?E!Z6C(+;I7)vtJ!@&>OWWcg#Gt==cp+`bvz^|2=L&L=6O#$@GMY6 zP5rssQqS0}zrGgie|F}6gnkpt{ac6sbC}|#vTDwELHHE+Tel?Uao|n=YT@# zcU}Gwj)wjSP~QLXDfnlh2+up$-hu6-N`JfJVDRj{LT{T^LGJuLk8aB=aX++Y^mk7` zVISX@5kBnRF8YzFsqpKRUI{Nf?+ep2a3tO@j{GO=gZA~$M-;}tHw?oDZ&C_Jh$J~Q zcuIQ59GCbY8WZh&e2B9N4E^}R{wS@>;ONM7`O@(Gm!5kBPw|qgME!o8?yLLDUjC)_ z{(mWe8C?72b!D5{;v{B=E!pJUyh_#o8@@ zPfICv7;OkGIi}NhNHd6mKS;Lwqn~7J8+b$H-lx9%E*w{(LG0mw8biM{g|&%#x%&DC zDTV3S6+0e=IjR&!UJ|NAy*R1T^M!L&l)@K|iVi9BTooMO+Xz1deYHZF zXQbcLxkNb*-%`WCy{6oApXep;@Ri0&#INA|&3l|B5Avr=&QjsGXLCDda-Ho`+!ZVD zPG@k76@E>xsOzsQ(63jcu770jA0AU5MRi{$Dl{|L;mvOeRk~Aqp}dMjJ#RmAT(a4vHVM8 zW&4l&GwF4^-X))72p7r?^ewPZ2$=d3MhnZb)6Z_#F^8fWRtN$oj zh%=;9Ic}uchuq8%i+=E}86Or|7a<(%Tzuu`w-^STPFQlc8L{y zso~IcqG(#|!LLfJsCSfVsA#lzJxBQ8?+M0^Whu7MG|mX-U$9Ipw_iKl=6c!nSsyh! zpV#F+w8r>bvEE88$ty}>U!ua1v+Xk8F;TxB|7r}o(_3V#=y*^TDSliaM_C^y?muGG z);--RwcVLX*%h1KdDYNKo%wKMF7#~_@osG3k97YQN1t~;gRUCy$)^@w8u(vR>ni*a z_Mb>)81xCZN*L!{I>NY7@`z5gFQk$4Dzr8&_oHgk-4FM4Y1%?FDn6s2m_iS#ek{m8 z!YZP@(81?8)xPqDU3o;&SqWWYv}fyP^Pyi+xNd*r3uis{pkA@yy_ALzD8gGxZ&zx^ zx`=>EjN?oz$H2;sVUe9(wq-k$J-V z&7qGah6>3UQNGpjibMJX4-1MOIt}6UIZK?>98&Cu55>w-XVacGq|Xiwn@Zl}_QQ)K~ENb_?+$i@0_D?slW@J$)vVvF1W6bx)djh&fxO z5z~&o1goWR@)!fpSVFfxSpkrU`v zYNp{^ecCi{3N%a$rA$5iEnWf~3Y5)4%xk*XcD z%pIsk_iVMU{W;NMmG1Ex5lwyFl;#4J*+mif=6!7T<&f2!eTnGPx^GoK{M$?tp4M-a zi@&FPurc~$Xg%xOdQr>O(<;BBgMk-6)cTW4pSqf6e*ds;SXN!F>hy`b!Jz7C{!eH3 zDhFf#r-+Tf%J&7SQ2m)AaE#vcPIu%CrKVlzU0m32 zrF{liD0{rwv$N2*vmpN6X5r5~hu&iK$Yx{qR`cVn=In)*-L2L@+DB5`MI&3|l8aC7 zY_6hWENCObzV^~_(Az5XWxa&r5&;}YvQAGBoiFBXB?1D7kiRVd6%A+N+;}Xw5ONJ{ zu^TB1V8wtfu)qv0upt6uvkam}2`}ynDnF-ItdKg_fDTI(&UdKGeG=0U!4{L(zF23_Y?vFlwfLS+|b||8)(Tk?v znYJl=vvg#?t)xdLSwMT(h( zBc(<3#>lJ6ga`q$A^}l}9F8>r+H*iz9s}kdv%A&NHwIx;2v80U5e)%bErUFlK~&fa zve5IgEm=CXsr&oGdFo1?rr4iGc_pcFKN~h8sS9|VPMWK8wFeSzWF)+RW-P@PRFs82 zY`%J;JVel=?CgfZTbxT#_RV&EUbT&P#M$E)QX z{+lN)Zwp)KQkc!&r1fj|xLL+L{F?TN=eu;(H`EYJ`GrhocwSL1Wu z`0EeZEM|ECRxFT};RuWc@{j;gZhQ+e?DUAgWIW0$nbr>2^~pc98v_~vAk;{(6#-&F zgg`Bh#&U&~0O>ENim&xGd^4(i?ojlzBv~b`+tZ-IPTaTGw+E1Ch9-v|jZ=LszB=R4 z!qKd;28f>5Xr(h>Q)P)>cGgQjVruvj33t}b=ewn-qu=RnP$V_MYf=^@IEOj5ZvcyA z70XxhJ}-;g%okF{?2>rDy_9OU!t7_2t5KivsHR$sl}-39@vh`47m2MIE);i`eGq71 z{{G^XU2k9szyG$0loLAe9XgqA8HS>GpVK@6f-;wtR#8x5 zh(L>0GaWTw6k(ypj5UJtMwm*qZP)>APyk#z;Q`#)|N zerv#|u!Vk>vVkUeFd6bWc;u?glc*{K7sX&z(FROs>Q2BtK*rUzaceAyqFzv%>fgIM7&%l!s^i7#TD=g_DIWL_?Ao zaP|<^ERpIUw$S6ssC$CG4dH`j5u%*8C9U|G%uVL_OYixg5X+ke?Rf1C6T?i_CG$la z@W>1?oto0OeGM2z+KasmlSB3z>f6o(b&39d9TT~OEV73pn~R5{ftRb9&xbo1O}VQ7 zv5K}l%38H-w$&w@dN9MEg+;?^|FQGnQsmpC;WIhpU4a>eR-mGua)o__K?5kLHNHSo4t?ehSo)50gxORe~TY03x2 z?VjTFiy`jPSM#5S52|SbdKqRaEPAEy8lMKZsmBOA+@e!}zT%6iU)P0|Y1379^NW+4 zUq!vj6hQzmjAq;&3q+?DwDAGnO11aDYrGBz(GZ!xV}F*GC;~pE8l$(VXX8joG~)Is zAOvoY0{kWi@q#Fdt@o`Jf<%AB9Df+7kqB0#&Nl)GGj>%bdFj+HXBvgKQ&~Q9we+V} z?X7`RGkGXI{mC#z-SCiy?6{Mjw>MRM%a{x>)Yr-)KWlv# z^j=@9+)SoStiE2dj-8Y&e4U-zN^=$se9#wO=@#ZGGr|S=6b^=@oeLJRyL~n$aIUm zYOSsHITxm!HqAipbC{*lYhd10YRHMCf?7?M9$OuctBJ6L^PUYnL#7n9>Z#hvm+w~{{j{EYpCMbOe$2dvH|d7YN{VG$Av^*iDQNZxX05f_E}4SoW_b%$ z>OamFyOVyy5&Q;t{X%*2s|jaG1-Yl}nmwU2A+6d$;n_JR>@;gC9ix>ZmDEA2%$6rF z{=!&pT)a{9Uw}nM^MRMB!bbn0ZiwewT7lnIcD?F)PeMJ7Ws7nBUWCuCnwE1y4tTtA z`3Yk||8Ahih2ei8LVR_!)j%CYGI{t#aSH_mSL=_gXnnGUp#dOT(lAEG82I?>^{&?& z8W>-iJFG}t-=ZCmy6|cy;aAsfP_YW0JAMqF;e!W$_pahLaJOw`o>kXdfu7iI+{rK1 zXJR6PtkqZW5*1YIj_cXjf5yIzA@esWLGb7R?WRu_`gw|06WPLV^T?cMz^52wp%f-r z2{%WACR*aLEQ0dJE2eO9ZHj6kV@bIc!H_pB?j8z$t*_FTmxRl!7zKHIeWoqFD2mA~ z_h5Zaho|G+41pZl9M_?GN?9Q^Zt@59)91P}(V&MOmVONlZ07JC)0Jt=!VE)}|Z*Ih!X`r2iClFV)D2Kw9i@qr;%A z$mP&?-_+mTJOX}WEWSZcz`h85ZcAf;LMFchd{$YDAAIo=%wv)JYcW*$T^EVD8VS=C z1%c}&&6E#T)GCs+dCOjrpaN(dR0jouASM*+Er4{+QZUSIZ0}f%~{PK5z`Kg;CE7}W`rsA<374zbrh0-G!6)Dj;^L@;dP62 z&V}($&qb5i#Xpnbw}~7pO(_ZnD-bRF39fEs2(QpSjpL#gTSt?gZq{1yYmj8t6Mb$1NdbRunEI{83V8&anOK%rj7>=bq9E2i0GtEIt&G!n1(hQ98edXa zTO@Or2{xE}(&;P{(tMU@Y`*X>AGO35G!;;yA^pKiTIP8iDC^LP1-WQ^>z~ zMW-g0QU+pD9W+y3#87dK<%t<(Abq*Ql*RJDz_7*zw!jQ|73x}x35mSgI&?+{cYl7D zcB2?|n|+gcM#hyUmwI=;NGGv`lp&GVoVU^s8zj(%uyTIT^u5}Hh5#VE7D--dspR?n z55E)-B&CzGhI?G93P&Q(IkxdbvJ;sd+MgVa8Fy-@a9V~2+Nb%Tag8*IsumDV01mMS zRAzv1!}NI2kcj0>H=lw^>y~hXDmT;+su^nFGvqIts)NDHG(j!<6E)kRY8-fr(j*yd zlGC2BXf)pJI7zN4$({&OIEVOR)UD1zy7th((q!#furUfRg8++NCK{$us49aU`tcHb zU`LK5v3`)aeNu;`-~yWV$ar)-(+*bSNR~e*?ZZTH07TQ+RCH;o{W;J_a4^<4 zk=6$wdklasV^#N3e4A3V`T>+^03#~N_Z;eL0YdaA-ckl3VdB)|O1DcB)k-M?P++-s zkj^tvRia-9s!UX!`;)r75P|M@U=9s@!ZlIqbv z7I+Rdd7E?-lN9o5#u80NpK382P=%9&Ltd#cpeUj*DI{ib=)z=6Q5*sQWax*4S*IFf zanj1kw%s5;RI)dBTS+a2BL~d^2F46_49PNW{#J0hbNAVWe5)Z7(0O2LYi|iDuAAF zK%im6aJMn%@=NlE9gU(T^gLDv{OX4TeXaqcu@pY!LxI0#4>9>d;>M=$wx_@DPPtKN zLM4i~B|;Zyab$!c^D!360eoKwqJ-f%(}3_atOATO5CxW5hBM540V=|t_TY^;CzYohW^+5wR%xNh>?;yO<Q?I^>todOLUbq1k56_r}oUC)64nuxL*+9%!W&)&hp z^b25jtAyc5(6|j)jDQ1*qMWsCe`BtFAd6r6uUm-=gVaeBO<0KfwSFBSnM)B^1Y}tU z3i#lHJW*eo;okuzp?3fP1``F2ByCcXkip_Vm9$3$1eqTBC@4gyFuz{l%8o|<iCG1ULBZWF})SBd@jJvdfS%WDkPcO9FkwgCg*j z3`v%#6rVI`f?-A!jBHFx4(kUq_)rA+4C(=ZD#{eX9EoU#7Vavj&N&v*p2(jD;p|Ba z(@R!;1-yl_(hJna7}_@t!ZXfoq^ zrZ*06kExsNn$eC}> zu-<){Ili)99A0rd`);p~%^Rn6jjP|U7t-tUJm$tSxQ!_ci_LC!#`36uGGC|=Khoee ztrM9qM;~TMei0n|Dww_iQlvoPge<^Xul8t4Uxs{}-{%1Q;IMORp?tc2tzG^DrrFk( z`5H6Hc(549h9R%>2LVLlXv6;Cy737C?%F~B;nn=PD^|unNmEq+iiXd%08w_{=!r~? zCb&`9gKdZ@Wkq9*1{5)~csLeo)ee#Eg+vfj6*+KpNEyaGtYB$sME|P?Orj3$_Q|E2 zQ4C4Qy%cHt#O*A+bW>^!7OzbBSeA_UcnghAqmb=~lw%ma6jCeH^S7l5;AlJ>Iwo*K zOt}`?&41JLa2Xd#88uhVIxKkYrsRHQo?v1iW9A2s*1de(!CAiGzz1O%<6Aq^GOuZz-ui}+eK4I&EmNprT8I|5%=*jNCQNz)TNK^TCJ$ra=jN?(k&TM0b;aIswC13dKg}L`~ z>jR9pd#{i(I(Fe6X*L@R&>Eg}f+*UOOMc^W)GCkeVuB9XlWzJ1jedmXM7C*KfCcx8 z{Sf}LV5|;=RN~U<5Vn7gkutc?ku)%>rX{tQJ-IfGl zhT-&bQf2IWxtMX#CR|i>x-YV~IW1X?;2!ZRB``X~FgjHk0ls;bsBA$QPQoi2yp6=B zDxZVNR_(}3s02eY(^wkTJ)?P3%;KJM_Jjvvi27b9Rc?QN{j2`V$#l8yrF-r=tyiT2 z9AismA_JN3I*rc+{!C7M&JoEYG7^vT%r8E9!7!-vcXt&rXt)>oUJk(H0~A=ibjAtpWwak)wNqCx2|gEBYg)L zE95g$LJ+Hc5=l;^lnMHzO}b<=1H9Q~N+vu>>p00mImr}}y2(r7f6P5kpG3aQYKbQK zBa+pK6d?vlQ9KZR4ye^ESi%&d{0wZ%u|6Fw@%j4bp8MC*Kd;q4RTAFX^!f61{a!bH zR!*{_$>hnZxcSw1-=G+Ja`tf~`_JDVq;DxiZ4BvKz_ur?$$8?~>Ufn6G$lYxN|SWD zz_Lv-;1pOzxsh)OpL1TKiakV)kjQer_ZR}elF+q$V~J2YTLXI}|3nguM~DLvG6n#G zpgc$nX$%KpHL>YG*U~ExHlK_){HaS@c*DH9P*T8p+Jy6li8Df!5%3(yNMjq0c1x}@ z%!KyrHHV8?BlcEnz^T$Ziox4$)7&qUfaNahTVrunn%d$q!6p{y>!-RD+*x`x?F`zS z9gK8-!!(rAJbmH2dsF4IxlVm|<5Fgue@L4Z#rCyMmi>U!SI3;}&B;v|gboG!a6R0v zkmeZ-zc*6z;qdXc3g-3qaxlAErOJ!fe)tc@RkifbP$o19!#UXv%t}HBs=-_eS>V8w2?@~Rm#6Pkvlmr4NWTJmYTCqWV2X3F}^xC2jdsdOTf=n0|x(-s(iWH&BHX`DU*u;FH>kI3}DMjxVKeB$E zVKH^M5xZ)09ge8{3j8l2VVUxp>{+y&<4&;umo3GIz7s|~{grFGith)Q&DGC_yS~c< zs^$KueXeJwS6&qrTudJq1y|iQte9?T+!S3~EXY-4HdUw+%1`5cv9p?`wkf17D6`T; zWg_?G$>xacYSJw?`9sX7^9hX{x2v|<+7#web zXCj+(DPd@!)E*o6hZ;icq(lyc5k~zc85wa$;tutYt(FUs9O}GJ6Fke#|KB-gjc!>3#3896{dLRbkcf_je+eR=BtJrj^Y*DlxUS-0#1_Lu08-19(m>Tq#%- zyF;8*4PuAhgbQ+xFHe6o5qWTv#M01IKri+RI24_vO&nZ-hUh`7!-wn2{%W$RF5z;b z7@kBQVNzAnj^O!;V7)0Mqg4Gq7h4(B^sm#}kK&aqmw3G4f9)v>=SsH%bObrGjF@GD z@*12Z!JMHP(^u>Ax0D`>aek@G%VwHw2xy&a{U%ogw|tnTSwx+j+_F}~SjuKN?5VjZBemcP02LLq=bt4P^EN%XgLrMtt!WdzqZP1oTDhcPBe3a!tv2x&(GDCWpZQICwLmt zDK#AB=wl&8d?LNH7@NwRK>GFR>YaemLDT8j*WPk^+?xyxgxrJPKUez1< zCaTxI=2Ju$*ezXg^pL1|(#5{3@nwS=Zk_|~;rg|lpEbX8OZ@_&%@nmgNqP0sE)s$N za#bI?L#PoRaOMF(rjal;(Q$Za>kOS2cK4J?XNV#XXqvH?DDVm*bUuiJNs}(3^OES? z(O~XX=&l~QEFtfi%OWl5x$0raO#I-1-7Sh`RhX-qO`Cm?`w+g5? z^@I7INt*5ea^MjdcW&}1L8#?TZ&0`yZ$mmAX0YcfjG2o+R4hSj8(K6x;TY-*S-+B<=OoFxpp8U=WQ z)AICwl%;U(vGMHz+LTvSXY{9H)_KhwR>nF3QfSG;VC_sd97N!~TF zMUb-&2!dLt1DG|D(Y(tUNL|pp{9rU;DiBj=F)1b#_X}a&aU1*j3`+exq!< zZX6>-zdfQoN5ZQinJwv_cW}_i6%{r;1Z&XdH8$h7Thqdn*Q=9ftWmmfx4h8k&7z;y zMW&P?i<>UQ*ZW_yGdx$W3!C|Th2COZ2qL5eF%<6dm{k^S2A$Vt=h>LW;AIxI6n(T+ z$_3SJEE~!M|CxX*2u1P+V3p!J`NcX#^?5~;33&n>`Ih(AYPw1g_|Mn@ipk?^DXc{U zPEvUO^Do0+Qjx6v1Q9SH>j54fs~ep|xAz#zRL)fbz*kw}=0xPnOhg5!!|*Qw@%3~| zcGeaa^Bg>C*tj!Sr=DTJ^|F~@0A?z_s{fB@lR<5`P4UplG^4(8eVHm@&-Edp-e#V5 zWVsn@(bP}%D1b$R&sbTMCP-?WUXk(1*ZFG(mg* z=zk?}?BuGU4vr2BBI6HWrFcdy&UzHcr^*_YJn`bSc-0;CA{)j$JzoPCsZQ7z1HMO8 z>qjicfrr@_0j1}X^!49z)Y{!7IuF(A!Qnb(SH6XmM+171WM91x7bDPF7xzkMY+9Gj zOv}={>>=f@viI#a14dJqkZ8`xUb3HY{rxUfx(ymCYIe5E*VL?C7(xE;%WqtQB#%WT z+d-0xTK88$Qlv!5(?;>8aY@9nWK$yc>`3ym5&N$(u_aNg6qLxG)s%Q9 za=%jxOO3zn`X=VBp_v2^zlNWdY#^*vKi>vmy`&Fm|ifqkNsEvE6?xYJDxv`llX4Iy=tyX4u zxR?&60#i)C?ZGin!Hp<8(6#;LvU>$rp-|^j0*?1|%(fb3ck_RW&O0Ef_5b7S4Wgm~ zih&bnI5JlSuH4}&Gb?bVW`#M@>ULSq99U**Szrz_>!Ouu>w=n@sbQMgykKTlRyJ?- z?dFf)-{-$`&U4Q5yg#4M>osAq2vm4(Ft7+)90n)2-VGd8QZEUJBo-))FQ-&lPS2fwQa>V zNLpF|fL^p!c;AGhq({};$?m`7%vg4BT_V~ZP7E^ctE`-8+kWuz&Ux5V+St2#SoDvQ zxxnTqmzd17n$?OXuiklwH&gGkUufoLpGq*_I)io9u;=N|Gx4=r_1OM4l4fqq;pT`@ zE3EW&P?vjqZa`pM{P;hMn*;4i=1DpM)mZtZaL>VibocttZ?=NS&O>$m{BEIRc-Hdm z{fmzEez%3`LmePTImqGU;$WlSNJrLizGG2j)=;L<79x+6J?}_;a9AF?>A?mMsnO=V zds|oeGsnvSO(||ny>O2$?VM1!eQ2|~8Z=SHyqR!eqHNQhJO8Qf^i5bnA3T8+o)4e5 zI-wp0sSu~6P7S*yNCQ!3SqkiB8TO-To0JU+)Qj04T03q9rA^0JOn;-66kd9pjXSw< zmXCGUw^=NY`V~qtv(2o7+5b0doE-&-1sLhjvG{%pQH9}EpnO^S_2?_~8KC1}tNCDo z{-DKK+SzgzHAFxJ80h$&2d&e{qbiz|W$rj{$^cT+S%(7W&kaHT?&UAz5}7dfCP4@U zlx47ip;YOpjBEjt$(+ib&|@^;FL1E3xck`ZT%qsq%8{cPYX4n7N&kK{&&qH1_AR;- zyd%;jW@)Z%&d4NoFmnG4c2#cG%{GzuRLq|#k$Dxcmlc@ZRqf9fDz7JOURb{(LW zk%k=mh?U*Jg}Ehn7B?S|lbNe(^PUEMK*sKyirKs1uu5FAX6+q-xp5d-^owF#Mm=$c zj=Fp+q1?dVtpy3amEiKrPj4j^c(W|@W^gH4a*P}2&8~J&eD`1829I8_hofj!C$ds$ z$#6fBLQ?AD z9_yHkt6VU8<{>Jr(DXR5CylL}>otA1f?EggoXf@|@<)#1HzH7)+MaSPs__} z2MZ%repKd~Q6_YhY0X!uh`QJq;_G%;KV2mEdOF8H|yJ5zS67 z^;P76-H&a)jLiq|k9;o-9NAo)?RP5c0@kx1Tx)cA+3D)mIBC!#ui2lQ;3@Y%{57)9 z2+dCydBqohE<5+T4ilz79v0T_eSPMjfxdO*KsL_NZ79eJl(i@p-5HEn(&gy(mGs7e zr780+Gb8?Ry*Cpceqz9b5!b~miK9p8#I&VRw=+Aj*}?u=ugYf%bI&D00(yM{x+Jb> z^Oll^Eq|}dZUH+XM7uUxANIueJwp8ARZ;;xP76GlqTfOeGUvElw-cHpKo05^45t68 z*twPQ10F>B7sWrPzUW;X+Yb1~u&f28E&I8}gH}9MSw9tCRLR4e7k&j(zoe_r1cMzp zl~w?eO}d{473{O*<}?Fh1X%q~F0^{8S~jl*5z*3r2?M`|jRHrirAtev7}e;R50&}y-H8@STBWA+vEgB}ReLj9u`>YVjB?K!;!ujSonhQT|3Ledw*dSaa$*H} zR0#zz6FoX!??nT?4}w#a&YPHbiEYkg^`zDNSkJrY*UPz%ouirhJ%H695b>HP{i*ai>M#m}znX76zDn$F9M1z>uj z&=r9WcBqUi4c}b%$Tmo{Y?U(5HF&hTec&gxUv2#0@yUVEGnU5%B6!H77sveUn5TC4 zigX{y)#>iTj#+mIdfj}mWAH5x=C+^vOFXiYn&7WozYAlth#sXN3xv=bAnI{F^Z9Ia z=gX-(R(h)sM!wseuotJROvHCBnQJ)ayXo*e`Owq&+P_LL8_x&(Vssyth=nhYemv-^ zbRWs;3M5E@k1$TWpGBey>}OuM16E(VSmypzSUR_Km|@&b6j_0q5iOQ&U z^e19qg+}Ea?y~%QL2%PwMOK5_|7ZHI`+)oCi_g8K?u@wSXXn7%ZmrAj`t>y7Y2(@B z-gRyCmk`1Bp_HKnIeppT z(Fy(xNbuX?Kh`E%DTd{8`pwv?7-peqqF{A8*ge3F)G2W41cx;Z)M#tv*yrGbqr zF)Skhn5!P}W)Hc?FMA-&-I|E2l{3CeE7x?YoD%;mJZbP5V#Mrx(X*R!IZxZtm%Irx zsS7wY+`C-s(Bm=CzPvN*H&9mZTyoSltgn3ToA-9A^~CDxZP!JU{jHzt-X~t!y`XNL zY>2V#&3UdAxb1UJ@0G6q1NJU${AqNKRxQ{n@kY!tVV^`Kl zt@V~-w247aZ|vsiw5(|NpjG6T3lA_StgcT+zeaYp%j|t~tp4Z_1{gSH+^yhVJ+ppl z*(pqnu!~;x?nu)bFXxDBw{T>H<6X(EvcZMu*7Q@~7ov*J*1ClBh-t1p2S|^f-|dX( zzK_0(cMUsemudO>J^{a1y7|P$4%Df<8gi?_s<6i`v31LxDlxp;g42V)HIW=LK?zar zzWnfd!u=It9U9Bbwu*X}OSg{D(i*zTwHq3q>rC{@F3Dya=J0&E;zWb;{_FPKsrqES zK%N2!Bf2*P7=#$pQmQ=WfVWKx@2k$^W5_V#`E&R=d!hz@&1QxE-@~aS)?$dCj#HHxW%Z(`oU5xmELV0W^_Yk zc2cRzu*vPVv+-JMB{)xrCv>$CL*6T_n+jUEf%+jWA0c&tRv{(EO;>@h^zEf;yAy$O zX;M>8R-vCk=6*)BB=rXOfLky5iJz6MT+NN-5-=Pu}@no72HqHHtGAGeh=iqowr z+$FWH!7H;gxi;&KrmfazDfkv8gjgoh&phOMo01T82VtJCX%Ls#<2SdPhRw?RtGBs# z1)hqJr4=XX6!NJAU%WsM?&^qy9W`Dbm6jCsg}+D7*ZHH3S% z{m1T@{$+FN2R+iKwY(!I7iDYyv=?-n&Ko)3p+p=jOKV6~^re=1LVBz)$+7xiQc#ur zj5v*_O2azzs#;By!+;m%fmfMHxB&8Ij9vmi%EmPOF#!CMp=5(mVSHLL#+$jbts&c< z1UlB=UM4PCr6#^ax%&Mrsz}OGha)-ppU1AGir=bt0elPevH<)XTD1KTs@ z-b5hfXzGPtDM9jn?lD3w+Ty;PjNc{gr#K6A+a?-rS3S(KOlEXP)Rw?35jbQBEh1cKXsS62naTKLqm%%5wJk-UZG zf?;{k8nF_SvIJ%0`w=EQzW$H|Mj#+#fzyU+}^5!0~xnIgSvNsvc6%bqu5VH;W#U@$## zZ2Ngj^X_0j({zvOj7w)I%Z==Q%6$OTCsu%YuWLm6CKa!tvh=iykk$SAxSaSit6#C< z&&NOFvMQ1hy#N7CAt-jrlA$XZmDX)8;uy!>=rAQzPoogqf3dKA?@6S4C*4cOfN*&; zehgg`qN5`aCufRQEOfy<2f1h-!q2MyN)gR{mSA|+pCQ$t_ls#pGxeYsJTBDJlVCE% z(^Y$CB~5{6P-*G`db749e^zT4&i5zG@((-l0eW)Jq7}_}NEjjs8Lt@hUg=WJaO~gt zwBWWEGq57f{g#agwBe~_BZjxeF{)r0fBCs_HnCA-2^6S zPKw?tU-o~qUmr==g<|U&5X61~>hyVm=|pLeo3BeLD_(ZNU%ELM`NVr)fWN{1(gD1N zQ?x#70JZP3(D7+hQP3jXR7e%k>hznUFe>~69)fs-hwJPW@V(!aKRfUh|HLa+s}X@X zjk>7Dd0g*P{G!2Cd_^`D{KsjAzljiE64t3TctQjRmPpFCQDyq7dOtg70VGyBi_nSF z?0Ge1;q|ObtYGL49=^<4BQrtGR{wcFiJHt`5~ha)xmHY^=mN|LZ>7VK8A%s-qb*p6 zOM(&(qdA1*SG-FSg4;r4?UYDcss-YbA00FL&yhLo!Pt)m>lkh3MAjTB1I^Pk5`Amcod$N(Pqs{kE32v_# zR#*HLXlQ;oeSp$Dfo(oHK>5HN2srJAvyoa5dznxKTUND=pN4=w0$O&cQLz}B!DoEA zPZ|)lyJg^1H>clI#6h?tnvHb9G~O z1R{S?ODNa(afj97at*LLR?NljX9l>>QA=L9Ct>A`0OD?1i4$8yu2}*b3#pf>9Gbz~ z_yt=vySz&fF`(6lMDu4Wb9@5>16<18TVG$$;__ zq#GH|#0ivjw-?Of5xJgoLea(qv`1#zCR+gxFRkk7n=%-k+b)zcbmlnc!gvZeo=OeHPGzuTHU z0iEqn#)rlCuR9N{OrHUz%jE-o0~}<*GGC^TzY(yq17Eg{uQEIyyvF|N{c^h&Kl~ni z5viYAV*LeV($0U67Z0&gsg_Cv`R;C}XD^^Sm5DExApU97bmUr^r|=YL7G%KSG7D=r zQdXQ}@S9lJ8&tKw-|El=%+Qwu+s+p0>ueG>_L+c~*c9mAlgUos>Thp;V{yDfgHO{Q zZdB@b3b3b?|D|yW^9%SPEgrxfspliVS+PMc(KCFkyaIcnbHluR81y>0KK*x93NTGM zsN~>v{S`1iq%%p9A&trTRU5Om4M#fDE-;b@~m^OlWV=tki1* zq7{5h0?=fX5C0`KeNM|7XARf@_0yqd($rnbOgsK;$NYT949sq~uu}ozWpPQ2BT{1XfH6;cp%FmBYhgsz8@9|uR~75v;M9Cg8wXb!V(?9AkqyMv0`$^k zLJsa9OH45gG}mn=*V?J&El0Y_S4A+oJKK*F#T{a%CXVOejX;W7a_ zzb)&~nbqA~gAS_Y5gEE88S$8dc>~bf7|p_my?xHZ!Y-iHfR)ieOeoN_USs%Niwa^H z4pWVW-~L^tG1Typ(|}4z<$k`55DdWWm*qJRvnp6R?v6!jxw13^n*Ma;< z+cZDyV~t)s;(t~Z{l7H<*Y!#`R=;xnu)}+TAXI1?6WhF8iOSD{jqvg5R3L;SIYkE( zUW3v-sB`~RZ!^%R>b7*xnB$|u3s>}VA_`Ou~ zbS~T~+jL51`rEPbIZby^$29IncP<0;I>5+T4P}95cyQuHGXU44A+%A?ssXq*LE#dW z=osA-2{g^(kQ-xLf-=c%G|dr}L3C0KY&_y0E`k3RoG^iE$wuLKBi<_w%}_7!0)2&Y z!%Bg9ii3gotXT%ud`@6JotD@Fdd2r!;g$EwJFoM5(9hwKb=_P?zKmF*RWt(iYIWyc z78x%?&oc3KGQw}F!5gW`ZkF9j8g8D0b7T>wH8@C+0a326pc#GB=*^8Qh^lTp@Orq4 z91k`jb8+thxJcE$A+8yli`MO6Q&>wndH_a-p4aF#Y7O2f@sU9N{j`sDEK}T|uv?&0 zZm$#ww#F<2^%gn$2`r<k?G9)SLIFm#_iqxcluLJo9Xmvh!Aw}ncTWVVo2#;K5#N_#eUtUGsIavV zLz^rEW38Uf$u_GhW*WNJ)RhDof^!GS#xg`SRi{0|!niO4DHO>3y`K(v^Owg;!1BSY zM({_|q}X!)&Wx0(>55IfVcnA_eHk@Lr;T@5kaTS2EE&<*(OSGLFs9!z z=BSJtxjO-HN{@DJW#)1Mnq_Fzomp}8U-|NOg1uh2*mjuWAj~3hbs-2I(@w902Xyq5n%`z^83mN5d(Uc)qsaia?*I zrFFi($JrF=q&23;83$+y1S51Tji?qF&mA`}W#O(9jGVP5Qz}ENZ-iMXPEIqL1>okS zCUQakyVryz4tkD5ma|MpX`~=p<8_tkeqhsR7wL`kv@_7enRTM$oC#6Z&Wkfz$%=Zb zF$lIWoRymN1GnZn?-|#c^|L6agbp)7FB_X}YiD;=EjMrat!uaMr#rXU+o$XqAKBB< zVZUVKNU$<(+)F~Wz+TP9RtmP=^CLIXOgp*j5_gbVbT%Fqd9l9oRXybr&2+Js6j>(9 zpRf9(UDP6QL=`%B0>w(MZZ(=kW-uFpL4oy--CD=Tq2I+HYP*5DY=o={HRY@JHL55& zDpxtzbg96(MfVzSCj0d{j#$**207V3$~(B@m}%iLp9SMtKBi6Uh&Ob#Nf#y6r8exT z`=5i@poJu~*mLsHD{_`z!8DA=hwwKB*r)=oZV1fpcgriySd#^eR^p^w<0QrygDGltY*P0p#;wrHt(TY;5YxaD>8yzv``4daE zxq;Sg*=LAJ`G6yqJY+KJ#LfEBIBQ&E{xqU9;*D9a!1U+-&SxsKZ3E&P+Rg{6 zh}VKyv;UbPw{$&7IRD>r<0Y)c$;Ty%Y>N}SH!23~?aQ0zW5sPscm;|(1|<5D z;*dwKp&t=Z0d-s9H!spi&A?i{mMXo%b+cp_2_MbiN&n&F$O(HsOY!(d8HtQV(F{?lJwN1C z;~aML)6)G2-8yrO=r5GBIYS-6@{U{CqM1y+Emjl3)lwFM&4gsJWCvoG8&G0*b#<|~q znvifNe4BFhpOdGr4TfD0sD2;pb%P3?WMb9KJ$f`foEHCFiZ=sR-Cs8Q67uKgvx_5( z0c>0mf-MEjo`mSeZw@LE;Q-Vj<<}pHQ5?zrb)Q={3C#@_Df2+e3zqpTaPpd0NxGBu zyv%%Fn>Nn1ju$#eU!qR8J||V$G$)t~AAT44na{B-LzB!?k|+y;KsBu~ad7_e#Wl|a z^vLpQ9nt9NB6@|;F1NYc^vBd1DD`^All%)(uU!jj)suPb$-dl$p1=TGyqQk7aL+m% zUNh^FgPh2F0gF80QSg4eA?IJ`t-X1~$)P9CK%X%!SrgES?jX%)}4omgYCSB#xXWy z_1)L@9X8sMZ!)v>=5SrA>F6esYs$!?hkxDq%@?yq>YOh2jJj+~mDIeuf${2c{rmVC zyC>HjNnO__8bc!6Iv;JnQ}-eC+BxEhWNwb-%CM~kn_bd}D#naY&0KNe9K0BEEw!S1 z_jQSNSXz;@h<0WaJ4)p&2+SstAtFZyYxsb#xeKxq07e6V*wjl{ zWkw52{pf&8!2$Xa?!e>wtNH6a2 z{;Y6CxK|{0M|vNx?Zz<<@?A=7pDT}|EUvHz#?4v>?WX^F7Cu=S7}qo~8TuwXc{0%c z!54>3c-25u!0k5wG4};|tihzu6}`<)PrG+ed!-_?B423MwgtA*6};5`sNe1<~)k4NXieHChr_k3^nH8H_9iaJ&PEqbXO`XHoG@vYz$Ak zL$DmcJUcw+XhqJYV`+J=*@iKghwa)`&$kZF z}yaIzzJ@>az_fv;5U~x*u=6` zsO@yF01RFjmjd+=+h@^6o!^C+l^(LdiiDKE{`ls*+?_pCxm`iKS?yNy zo7JS$V;gUNEFwNMi5f(s=|lhs2*9VUffM;UU=NsVB?J9;u4GwvvRh3Wv41@S^HuBX zMI~~vrAxOu?#rqeR3Py)U4)Gm;0j02n177F?K|RHT|f*aXW;{g{diCNB^qWY8}2g9 zhOXg_n8e9J7Sj_Ka@8W@PR{`MX?#hcuEJHI8KNkqC5)$9lTW1sNGsMLODr=n%P#f5 z`myRnEY$3YdMLE_qsy-JPU^tkY5zn0`uR5|OhfM+_nFmql$7pt^3R6%eM>RKy}x5N z$zJb=ccuQ3H(>f)0$a7@86qtmp_)W3mwS!eBJj7O=&MvOe0q#s1_h~#yom+wJ+_m!W$gG^0x zIr?zS^9Zkr;NpDo$FWDbyD}c>Lme}1R_~h2Ax$d8MjgD8El-PF9>f>hO*1bg$%VQu zrii4QZTH4rckVrL#&+ha@u{@>TWk6cqDYZDR{RPF8UC+9Y%$M-S?r!`r;y6Ywe3_K zCP_cDUIk?<`S9x+bWkr7v<%T$H$;h$*w+- zKsR}fhdd2ZQ$BG5j3nGD=Ugu3d8uf{{wQdW8f|Ehpkz?t&c=KGHP zu}?J;4E)B`>}C3Hgli4*MQY#EE`xo1fAZ7xkQ-MoujCgcZ!er0j-GO@Q>3@jEN@%6ULBxw5%i}cNgg~ z9cpH8T9M%nCO)E9BnP}84|9XEYC;lnbA#qJvFp8)(G_dA49uVHBCVXRAH-?cMV`c$ zX}}XfE3=9(W^i6mJO^Md9ZO{=o`v5_k16)}!h?8nfXF=hkcZX9%A`b)-VG*n6;B}E z1_0ZsmM)D4byQ4B+8NtoObe}cU_M!iR>s8)@yFww%Uk?);9&nhk`z}%eEkfoOP?(+ zJgG@qF0o<-LFjEQA`R4rDZnMvuCZGPUeEpS zIjQqYOV7OEydU}g&zajdBk6aZ|C_e`C<@q0zg2zcq1z(#4J!^pnE;6M#1}g1uuAS9%92-|)r3 zgFwAD>Pd>Ten~7O8#~0V^x8a$dH;4a<5@;dO8Se2uCFlf4R2k1KZM*{WqO8uRN=Ss z7Y7<3W|g)o0?dZ_C9$1clu+{LPCP7)gDzF->pxN7sJwDEZQ6CBtv3;vs&0gZ;PB`isc=+Zk|N z9w6qcb?jw;-IBEt6S9E;jw;&qU|NT5!WDx;Q`ciVq=_{g=rmN1UIRPL+*+dqpOs*9 z@bEYew3i1t&V=QuQ747CFH}TJFg8yCn*w0g{fp~WZ}`H)ec_|i@bLBZxFrtu-WA*k z6Be;r@7}+Od2IM-DK>2hDw2Tn6xdH1_#riB`JD^lkK@MCE`<4Lqn$PD8JCTX9-6-w z=%=ytKX_72x%-d;efN#VRqe(%2rl%aRj<0Y$J7{u8rGM8*%qcXCxf@MqwrBQiRFAX z{!UmX&tyKQhR!aUD4QRDxVL81SkMFs_xc8+>}6?K^X=F(cpua+Onq{^w?#bp9$s^Q7=n^I;}h&EJ0QC zVP~mPy?jKR5-ewL{@M!1cwXbD0*)Js)0<~Q z^Z3~1K0j>#OE{MSZ&pBA8uVvALP*6@XwI+H`*N&oL3gUdRMpM{u7sM?_<;ffZF@$| zJ_|uu-YeIQtSZdLeVHzVw3_619~o|}WIU_pXCSAumLA3`O=pm}N=f>oEnR#tZ>M0R6oR651V(J)>8$6U#A!n%<5 zFN)U*t;i161+7p){W_2WfL;Q;_lkQ@vH(%RI0X@%0Rj|QV6fl1!WJUw` zznHuI$C>b9Ro28HxrIO=6x)&RJBS2JSKZw?+}mCXwe@{kf$Z6 zjd*wsHMal{S8-Td@W^5{B87*10)X)(brp(~8ay)D3+?^_>8nH>ehkf4B7!*rPab*_ z4d2MX*6<*`YUtw!h#EEe2-jqJ{#qL3^6D?{ULRUIbmcDWoe;b}?vqkg>fD3BY~2W-7_nC^0htVB1{EBn0c+<0A6{QXGGl+bUBqd8IrKGdOvsF= zYpkJ**}Zl*ru%DK(_&83lftH1UK7ossbF}|y4gJ|O)bk`0=8NV_k(RV^#a@&unZ2w z25@P_KYJm1`@9l=XD_mujY_+?{sR-;D`E8g+|jB;RC3Voc_?2FLPZUJCBdGRqKNgu ze=i~G)Tm<*q^(T!yaIEUcjpS;sV5qGl%c1>BhN5UhTgir39^xcJdDCPs<3A{*e>Z- zxqDwWNb;Xw&GKBmlUsI;tHAZ$exq5}@xtRS1X{sAod<9YE#C5R&uO~zi!dbV+6N;e zX}bXsc|A=wjYJ?Yc+I&Cq8X01J(zFvekS#LF{G9S(kW%IUaIw5)TO;nR8n#b?Xgce? z%)mGP)c4ti*L$7kN)a6R0#%@o05TMa?L1%*8$y?WbRM^fI*=X*zzL6gH0{d`?n?P? z!Dxo&-ztvkw)#A@RU znqhVfFluVuh+A16!2p-z`Z}<+rhPqbVEada-bX58ry4!QWROU<&QqcPTsMB!(b01`F>xPf`7uO48YW5YD7>Ap5bIQR3sPm!FJ%=prol9LLl> zQGX^-<_S?(_}DvCOe+uV`rS#pXj@XV=|1yY6-72X`;`(n(1lX2YQ< zX{c9w!a&`N`db|ISx)K*9=XR3-oZqll_U%T<|{df4l3pu8^2$ zd1XAzMUPJzJbTf*b8^M94EB-d^5++w8PeB?2Nf%a*+xa%Ji$6`+c0*n6!^(VOo5=%V=`FaFUqb>~T5JklRzwS1@VWG3cmz*3QF;Fb+9 zew*ui92-)y1f^5KDA3~((dlxC!6L}uA4mNhD}7jx!4lOdLW^7CyFW~ld^=(cjy8lu zum70lq0oJuJ_j{~|2INc#dmzqz^U+QyO56KMQBaiUdKPFd?N&fJ~3Hkw;8;Zy=wm_~%ZTV|MlgrJY;@3%&?)7WE*j|S^ zyG+_u^;*@p=*Wl1Vu%Oki3h%YRVuIt@=Y4PeSJ+ZTRu>Xp45E(a}V~G5~o(9iIUH3kml=os@!!r`uN1RBhC%4STD`V7iXY3IHMU*L1sJVN@kr?Lxbi z^Q^zgr_A};wOkxU==SiPndcsa?e6d$Zp*ardPASxpXVc=^q+dI7UVRjU&qK_^*4nt zI5j=UH8=G5I{n~W@ScV14R7tgC2wqX-n=cZuqPwBH~yMko_*e%2f5+0ZLMUU7rpeL7Fsm5_xlX*BWl|IN{YbcUS7R;{0N->w;8v zdSef!{#BN;(ziR( z!#@|U?e5b`TwqtU`wi#a9@RQj-^dc*No&1*YB`|<<6}GlDbayFfrf<$xl&AZZm}~>MLnw`m zFBq3gcz=wcYk^E?a*95C*upG68URljPBvau&IW@4Fy>w=Ras_hQ$i-+X)1e)JZs0> zozx+#^4<$KFCOQ{x!#!~Il3tKZXG^Zd%Y`ImSg_-tyw*Jr`$BPegEN$9n!{cX9wq- z9bCIEd>T5lXW&iSxjN^52gTsV<^4OmaI25jJU%?d4WTHzGds?oC%!G-hWpT6-g?$~ z;Ei+6dm-xF<=^M){Ksdzis}#k<^8HV*JiYB^?zT$&LqTJpL?s4Tf8o;deZ!3adm3T z#JjoYYY*W2pn;+>w`3_;eNpXu7k8;K_5FJ{ZhcJK+SG3Dhq-@;ccyth{1w(m{1_Yr zYe#pHF9<;dOE`5qVf%TBKy3{3I zoyvGo6=LJXr$LXNISboo~n^favMB%UvC53+Ish|1AJ^>b*uQl?C3Tk6Z??+;!5 zEEjAXe;)`pFVG05S9)qf2)WDORqB<`hqsQcejgwTcYR3sja~JLu@>twg9ra>yNw}3 zSftd;O4pu1)ZW8yqb7wpjeqw;lc;&0KoN)KOaKr7_`_i5P(e}+3E*iELTy2Ip6D_6 z8`&m$_8{Nk;`O&yU*6mqB) zHj)nx5BD=HjTxj&Nse#645hR`M!d|b6}l5ukYxEFZHNITaG0n}wh9orZ)`tC6AjWO zP8U)@BykKZZm$4o&lg(oLeSg4M}K(EEA<}*MwAqYj$eIM6u3%&7|%I{Y~CR>8Rmov zP=a-rIpQ_1nX;l#g3m3+xQUnpq!kR9u{pvoP8w*kq-fQBKv5K*8w=?Zoc-i?tR#5Q zEV2*{+B~x?5#!Hs5R1V#6vvpC#9%XH(kS|#_S_!rUmIWf-Bn5V>f|G6fn#eq?A+IN zI`dLV=#RUbtBU-F)oQyGU>)A8R&mmqVWR=XN1e}GdA>v9YF`+-sUp6?rdw^N8+DI% z&N=^rh;>f~VTjmg zDW%=eCVL!##;q9Hiz|;G)P%Q`g#=q1FSqm$7M`8zAs(fo0Dffe_Tv9N2; z1uSzu=RPJFt=@4Qfa{e)T^E?pXeyX+oey)S`{}2!D2fy`9N{Zd%X~;8UZ5R2so){ax4tYM@S$;Oe)I}<~7$WRm-mwpj zHI5N^Z1`XHjtL3Ge18-sLc=A$iCRT}R$mbwd7f0Rv(qo}4}U(HPr5|KyLYNk_R41W z#cccoTtFu3zs}Ut*^<4ETQa2-xZj~#nCGSLb&R{ob&Z}NZKN0(!u(gyDf8sEq}-K- za$pEtN5yPm7l(ukam@WnB4R>FI2w85tEUoVplA^yG(frp2q*IRU^W11$`GKt33Poz zD4O%Lss7DbXp#3yFpm8Uq)871A%uT8& z@5n&_v7SKhtSRvr^)ry#55L~c`TP3GBDt;hNoc?8v07Ki-iYN(valULQ%-;uw-{n> zUorS@0`izhh}1#S7c894hOXIuL?x|1GfQ+%mN+XL>(=x0=GuO(X*=;~MXb1hq1x^+ zQXOIu8~Kuvn#lg}df1_WdFGa#E`r_3^t1Ophkc(OK~_oz2}&Aoxozed zX4NLC zJV;Q*Hd4m}az>;BGX127>9d)0haykBPV2mV&+$aV=$s8@rsw=_1ufMhAN5T& zPhwHb2@bjP+*1A_Dj9e8!63&V*nG|{n0hWSTx_?Y@5C{Is7@M(c(ldc6O z@Xpb98MeB-7h*$#rysOw+*Z94&&`}6bPKf;XVa|HAg+XRNPmJIk(i) z2!OA}l&p0mvcv%uGyb?YWkSK=aD3@*W$DUar9#$V>vz8(-|Mbo6Kc7G%Fy@n(d)Ur z(!TiSf+ds82FY_&W8)X={Cs30C`>Li>=lLqiakn!41Q6V z9ArubpQq{mT&tedW2*|QQV*D3v8#UTiC?T=6Z7-ZoTTSH-f0a{xKRNMRtP!DTUPa= zG`Zeue69AZ|27qQT2N6?CDq~;%LaRdO;BYmIv4<8@FAX_I1PV4vkwZOzxxQP%4v58^f9C4{&;by1Xyd? z_o_>jtq_IO>!K#nPW#dop2oktTzSKIZg2SLXt++0k~A|qcFF4A_3$KR_?F@DBzgq5 zpnl41Y{F_x{Uv<2g5u+EHsDuW|h2B(AQZK}A80O0t#wbNJCD@7$jZ=yodZBAP zK|1Q`c6QI@km4r64Y$1wZg#*lQK^QCtttnV0 zCdqHcQboE6MTP>h#j_GwBnlb^u45PZ9fnxR!8Qu$hVP&trNEmg4DJWFs1P}5$~ z7Nx*b4%)(ie)Mca>g#>`d@;nk^BEo&4Qk9vx8AfLwfU-OjT&;&_ex?T#5hBgZ`gLJ zz@+e^K9~XWQGho~irQjzOjr?Bpdf@9WQ78dZtYk(z}>nO>dA*o`8L^=a=D7w(LUO>~zPp>8xdGTSJm&6`y{-hv0M%8s==4?|rCgE$(7Nfx}TbIkXK-&`EJ=or=6UT4d zH4_l@bTDMfGy?&@ft=or-kAQ!bmFm%8ATg+-q@IRYdX_tx`{lUg`Ey*iQE3)9iGvq z1nk4?*0|)eaS0FnPcj~Er##HNQQp)zm@9>HCBAVL@TK^_Qx~9Xo~ymaYYV9p|xhF=ocpadr=lZgYND9>5es*`6zBG#LyxC}zk)zML`b+CGX6to%M?sEOol!3%lhhndCiOaIPUv8F0SkRp6BPZ?f=zimuoD@2HA3_tQes1y+8wb*Ekta zcMm|*S%UvDsd>oyy%^{~m4qZU26UF@7D`ym5*Dd6W=`T!UgvSsguw?bt4>7#O5N;7 zuAxP?ypnKV$oA^e@LsSH>4IyZ!j`VoE$F#fhBKNNS0{7PIVo&?{4-$HM9z?6$p-cqxR(lTdUb zjuc`!DYlS8;<=@p;s1vbcBP8^Lrde~;9y^`?o-|d4b?%&8J*(fw-U_R@YtOnn>5KV2}5Z z7%4P&@I{2~g_rWCT{Y0e6XYV3aAm-Uw40_$`0dwW?rTbjqzkdRriC|;pBJ{@X3FjX zj``O~+l9o1O9}I%uZk>CUrLD$&E*Ykk1h*|X}i?*99_}MS3bo-p&jd(kgVm9%;i54 z_Pm$_oz|Qnp+dcg|Go+SxhiWtRQFHnh0@GG-p`;_@v`n=dGj;R77v+6QzXZ=NZ={Ujlw`_8ZvCUeKQb(%Did3TiP+lCdH4~h} zgLFlUSIWS_Jj0tiU<^IB8vZdP2~6RDtfi%!nPOKaG!Y<1t^%3zK$&_rv386!SGtNJ zw)O>e-EpRkt^L${J)T|FvnK5QU^T|?*gaW=Z30Aj!Nz#fMh$$!`_e^z8Y_kaz)T?j zZKK{O?+8ETCKLyRgCkz-uDB!QY zVv!rKB*@ z7j?SdI__##uERFI=jD84EAPqtrZ2g)kUWR0s;{)-@XD;@pw>d&hJTO!V@~T;@of8E zN`%cX6r~xx&<*yb)=Wq!0G1$?=xOU&fHF-YbYo`e25#vFK-okOwD=Ts<6P-a*^xI( zp&>{ueDBkj^V=FOR5dqT&x(Zce4)vOWt-=qX-n3bva-$G()dm&k8^EuuzA&A(2dlV z2l1u3ov`Sg(D)>?xFcV4c?UoBFK(DyF5Mz8^?hgfPtM`h&qzJf7gukq3fpo0R1-O! z5xinWzwMop|I|Qp1>|k(ctGd)$8M@TSRo<_?dEOVxMJIRAk_M1>TaQkR}1D|fh65@ zY4Ee1Km8_4Y{xt8#uyIdfx#;zZ*HFU>ijFWx|Az`M7fw0#+n2ho%UFG>+xO^4KL+p z7M8ia^GG)FUW#*=_rd#G)P4oJ$x8xB(A~09?@Ta54gfPO#FJY;yf|d$aq%qjsz`Y} z|3^S$*%)nYb3@0M;m)RVDpmRM!qMvE;Jn|r(;Nf5sK(9hG`FjMU%PD&IhsBRNdGZ+ zC%amB`A+UzOOy}MM&SCb3OjV;nm0!-q|PbNe;ia*KWJm zpsp=zA38j*#bgSeXTlq%l3d!m4DSPChYih@wIAQkL$@ieeQqmT9(2kA{P7un&FWl- z&hZEWVWw3a4(!{1+THc)>q^^Ak`ToZX>h})=5b<;!R{Z=TdQ}Ni$LsZyMRLRpKk$p znSQzux!$t3vsiCJz`rmmS=lt+|X+Mz-&#-U$5^Xu~O+!oWo;}ZHMF=S$w&r zQ0BgqbIA0{hQ7x4kKa#}V>d+?yc^$pj~s2UEzlKxK%dXIk7@P$19R?~*QW7;=&RY# zBCn5rP485#p2xA-A-EvKIgyREZ#1K1a-4|l_5#x@H zCSA`MtzSsI9+iA0(pl612a$ef4V~C*L`^eu@9#k$ww?^&RlRmaD zf6Mp&8#`a#{+~(E^7Fr!7BwU1?UwJGn|ZuYR2XeLHbUGv&bb=3{T88Q#N(yA-D!`1 z!|N~}171OL_sL|vIHpQV9WA=XKdAF~3O`%9XBh68l1FcT^1OH^L~7}N|Bm!zS?yjZ z-hbDHhXWReGE>wD#-to!8xq|twa~j)t9*X*`;q4Vr3Ywy6cA5^5kfh919Q*8d40ft zP#^@P#fHBDfqZ#lY)EDiN)E z=Modta4x}T3=z0U`phbkY-_gKk;F~q(9(yexLSu2PV9V`+v{JJz zT=yUVB9NhIh(A^&!n$TE2gn9*Vg_kP$M{+HN8&w(?9WDd44MWHYJ<%VN~|KC&&Jmp zd^?JFi?%t+L^_;5?(bF6I>W6GH{04cT}oVw1GajkxsJL4%o9yE@e#=o**wp~>h{-=4}B5`#>1 z5w>9WW1rJ5mwawFnO9bR*V`gTNl^BHRQg~gE(CmdBs|o!_Vu+ZHEpEs?y~aUvmWo~ z!`P{s6i-I5Je>*N$%``@FRHuxx$&+~l#_PfV#ev`_SPQzfhmE7E$}-~1njIrL#N)5 z2DGs(*Zf2z>b{sb>>tZFtX7#+%3%S;{Qm>18c_fMlZ}AgUH=S%uv`&lj){WNIL+7x z0i5(Jv?fxieWDaRBlq@d-8i{?9N*^p#?wHUf%xa4^=gi4>a>J znYh=)Nkz!cKF;aZ@;pD+^)aRKvt4l7*;PZF>WRSXJ}J7YP2RoC z5(GahuWX`CdBNN;8}3dQ_xO?&Le2@T#u}b$5U^w%_Oi6rtir*>V2X-~iaf1 zdbM9RzDru#>YwX?SvQNh83#7+XOsYadq7~}9N3X7GOdnAXBUFndBUd2yaen5pBI!e66Ks*~lp?Flby_ z)9NDQ$KD2=BB%1Kyn7g>UiBbUBu7jLIXW8WIPi|X8eN*CMEqVf6YO)o!JBap_A=wOTu^NTOMTU5`FY8O?b2<% zCsq;?g%%m`d74sq3wlE}{|P~*ophB{_R$7?8txb#=oXb_*8xes>ZhBtlm}l^-fgqq z_Ov(u-lyKZ2X{LEd|o8`H)kZ{EbpV`h5dm;LVf-4Bs{HPSa_p`SgK zc;g+5N;pAgU$nfB$-eS6hM5xgmE1_bW}KyKxtV5Of|LZ`aX-EVUfCJ2vJz~mmEWOX zt_Hws8WORF7FrS{|1>9OV%ufP;u1-E2dC|X1dEdtF#pHl@9}vB#w`5C+ z!NJCKY2kq0a2ytFQ1e2B&1YEtZ(<%6@XQ^*iS2bqP#80f*nJEPQFPJtW&C586uGRp7Xm%)qT zrMDgKmPZlCTR0UGW8KJI3i(N@O$j}TWi4Y7ra%mx7}hIKC+tr6TJ>rDvwPL#VRQ50N2?cT+;| z#R|VPD3k!jAtIB~tZx{OfSMBXD}<4-^%+G*6K5`8plL(4#dOGwNwOD7qq zM;_`e6FcIdSAxqt3dL?q!-1bP;Z|UO`$%6L_-A;jOQHDmDA`vb4%<q_n8arZOzNz?M-NJKqvaib39sD!dNp8vopg0V3;Z z1PqX?KI}3tT%oYVQdw?GV*PsQj4|jwI*u4SoYw%)q#c8Ml&{mAs~=&_EZd9JMhMrK zXv>0i1Mg9c?Hk=O3TPkziC3CNsXS$33oUc5SUH?5v=M7S9g`?L%On$c;Lm8k5Xsn( zO1w&RQYu1_lyqMeMF?8mrKWd^tTkfmHqqBgwP*3#1-Pm;lN7v|Vu_Owy+o#kdN(o4 zNbn*IpUTfcBNkoHqgehrHnGAA3Q12BBX1E7yu` zr0pJa4IWyMb0NrSNv|FZlrtQh6lJGR!JT*@=OwoC1K!7&1kkvc?Wkc9ASKYBZ-@ZC697E_=rBV!O5VLj)CkYbZH1?BGRH;_HQE(b zP>ak)rSljsXE8rfLovUe(~EL^yv1*k3Egla!`ZrH#I9gefwb6j~+)nG0E7R52!A ztcP?Gs+BH-LGD5zi7PTA%^Nb5o?4(D^({pToJmR&g<<(iY&OX>rYOyNf=uREP046~ zeZva}wEqOCYrWcrC$^(rJa?{yQ7EQoo}+8osgo-EjVeRCi)mU=jTB_NQOxL{z5G*9 zgD;8w)NY3>x6_1jQrPX;Dx0(*-!yQyx4ZL%#2x3}{Uy8QsmewxzAh>8D&PiIvaJe% z<|Ckv|8RjaRpNl!IY||pr1HptaeD@0I-h3GN?X?u>TofcOVw>}&ZI0$PDD=(5Pleq zdHr8Sj|l__A5=OplooI{se?rc%)wLsw2`qOmx0zoXR=-%MY9mNKZl$R(h;%uYB~0$ z)G^!RaAMA|Ags$G|A}nFo{3N}_)lyx+d;y@^?ML!&k<&sI0%qZpu`3%Cw8;2Q7%@M z-M(RLJ;>FQ!g3(d%$AgP^;ZeOEQ@-8@c`@pa2M4|8zB?ntaNBZzrd|?-)25?3r;N$ zyZs2JJc>VRbh(~dWvdfnxTcous@)DlM?yX`k*GJr5S!*p&1*1Zxc^k{QGicp^N(&(nSU9oL^1(H2 z$UxQ|ZlgM&N9{%d+xUWIJ5+8_*-8C$u0S3AP4Y)POpg(_pHdihDE&wDqs1As2?dQ2 z2Y#-gX@NMV$Pxe!j1M9-3Qa!&SJ;EdQvg~#$e1FEwFX*h#Kut~OKBd>i$%@^8ETZi zfl3Q)$?Y_2uKu`olC5+|H!l&}b+|_h)wmLs%?P^%fy4jJaLJtY(>L_&6T8f6lmd4v zlLu=U7aXK2k54Ew({j7zmKEH_7Q>S&EbDK>Eg+{f@Uf@jB-b+k0`TkoK>-@Dhm`Hr zi*rh1yElS=gmca&jHJo#CB;X1Kf@*b!iAcly3#_{CwIr=ON0|J#uR8Luf#H5^wes* zQ#=bZuQQUWjB`XZ9?NtBXjTCBp5!?yfN8BN2j+JAFZTA9PY0J`B+M}Ty?5_b@%9D7 zU&*C;jj?I&$1=vk%C)0b9k!NKpmhNY-^iwuK;!F0E417784=2$(wN5r?@$t_&QXf} z9Fx?Ziy&i?m^Q(}7C2XK;L)p@;J4>a#cihtt8f#dd?~H`*CkKXNrv?rfnVazUzJkX z0v2k-U8a1tj9*0P0EkSL%k;SGx7yoN_(DP)ps^?l%`|ZqJ9mO<@-Zq$iRx%vIb;e@ zNZJSQqu?wPFD1ECDPFlY_EAcFd{Fa@lCcD^5`ax8Knon}#k|67Sp)Vez|H=v z@dlK73qkftAg5rnu4Z@Kx+wp~78~E(jEPy7{d-LKU5w5Uf67P0)I9&j8+OcRw`3&_ zg<|``zl}!0zL{@Ue3W=Fvhvk0k|$Mr+#cP~&s+e#0R^x6@?O~|vB^jF*A%wINXg-E z@Zrz6K8iC7k2GwxQWMv!qMh!PKU^#Ykpi7E8}c(JhI zK2TM`#?C8T&xi>^0DQjtAjLm}|9D^h`3jx07(V_=Ld|?(^uKw7sd>yx4SXxH#I``` z)x)M}dH8xJf(N8%Sleb+6LWx{!$g#ve8eEowJr}EEbdsIViP_A;Aj?Ir?Q#*vRa># zodsCS*`~T%K46HH8p#D(+oEaiDMR(NjiEUWon?3G6Y$@0%^nrQB(u1;4>sH=m>xeD z6xQ9+@4cJo5}OCQi-@XYjK-&(UlS;J8F3=`;MlwS-T63baPu*kyH+7a6Qbs*;oJjb5V7+Td(1|K2UQ&t)Z}MZMbF?bVZ7$lMHqF zV8L)i<6V%!8^YzQ_I>`mH`=E+|J_uVwa-X$-twE1x7K`w?J)P>`Js2=twmLu@1FZA zs>8LaE5_`i1Gv8YRBVIE!7tU}Haj=`zBQq*=VZ(55xT>^iZ!fm1m5_A^bR6W+#p79 zisgDqy`ytLy0Z8ETZ%yOBiu4QDhp0b4~{rbN<7hZPF!LmlGzv!cR^6+1*1z==^5$vSF)83yIsYa9>J^ghJgB(&9j=y;pvv>E;_jYH*X2aJ3QZpu? zRU^A&mTX&7iZkOffF-C~AF?l=Qm$uqpf>xWT-1+?&f)^b+a#wVq=NuxKU;64+v?ch zN91CYf0Dy)k#g{1Sj4$neShz=P5oWR3zaU{J*;P(t+>4r!ebwE<)@K~LUG5#vel`T z5g9chwcc%+%InQLw6>=UuKzHxM=23oeF_wN-%$!D$!|$p3l^rx5yMYg8s4=8gr6#y z07ieS?PU#}${hrNNjs)$N|81)ZIESntuO^nhj*nOPV6tgV)nCRZ|GrqJvZeD@3D-H z;0(8=lH5MPVTZjYW57uJLVc>(VX<)Qx9!bAcPWAv_Xl~XU7xy7zKbrny^zzA)fnl2 zu4egwZS9t|(G!>~E?l&sc4N50{cu5nI*&OW3%qnzkptkGjRUa9g`Sih`u-Gcb?7e3 zC5F!eJuI+`Tcz_cE?DmT%---N^dHG<%)O_7(d{qP?E;;%_Cd6hMhDA+cjO*Qb9OYl zp}5n1BKJ_jsb*97`w%BCUm8b}kU}(2*N8`lT(TQz$;#+z51j*hl>su-^D5P}2w_@{-#_ANpXoOfqy;KR2sJh()Q%urO6 zZwY_0w#45Gbz}-mMT8=6b5&(La4P}nEad|^9eyTM{jw| z7o?gow%_&93!C@%Mw{OC9dPb(KfI^lH6I-}?adaS63hpSS3QLVj_@(tJI?u>Epvfe(zxpI_H#osf7L1n40 z1}?M8%X@>FFJw7Be*WQyJ{Yiu9^KE|n7!pxMuD+et@oAjh1yd1@5wbk!=vrj*Q)8V ziCTD&Sy1I!iz9Wj*$T~?^{6(&(8%qzSclLn zfySHNEprnea6>&$JiyUjJgHpzvhMaGDQq*MynjW_V~-BjMd|E~N(k}sMIw86-MKx9 z*X_%0^^~GFz}05AxG*pI^r1{nWc4gz);>I|Gu`1(azF_+<+ zc|<|$x$QX*R2duRCAN5AV;1w>BckY5V2?Fw2eJZuof|B0EM#6P;p zTxs92P_+mC*n&g}-}d*>F0l2|n^i zbeP3FS50Ws9n1>edhs~4-RFDu#82|olB>JxIL(f+`Og33rhBv-24qg=ckb+ZHPG>M zjCKBLY-VTu>+2_zl4{B0S;7M;XLvvUI5$1Hb8VfcGuLL%Z_t7J(U@0=wyn!Ob;{P= zbo(DT7s-O1ZmIQ;kuPqRN9%S;pCX$-)G-^_UHjR zeRfT&q&S>)$BQpSAnzrkMY;bwa;-9DQw!N)pq^?Z#-%Malaky9JY7AU(xR5ACYOJT zL`@ahbK58@ZmnCHgFS4t)%uNf)xHkR;FN*gA*Mhl21Pq?Vp-VJOusq2cOeRuxHc0_moIYkKW^8+tWEXg0No(It~ z_{h~lC7jq>%pp&j>`J?9J|O>+#p5dTnX@lUM)E&dr$3E)7K8RbtQQsUKxw zw*UKKd)4<%*XiSQi&n>>h?M6ALG+2cejm1)O(>T;+Yj{qNiln8QE}K(C9mR7*t3DYge)GA?)t~;rg2+2)>(Fn}F`tN=q4AqbX}-?OZ=|2CEJxnYUVP)r zt{ku-GfPRG*kms&GU)BcFZRhe_NOZN$Kl8jeDk_kEUhzoi}W(-fbuVVb5f->~ezrAN;E45$86zWuxN&)Oqf9rfp3Imi+KrBlI` zI>c-ZoC3tBD6u77g!gZ*my ze+p=6FKLSqw|%gqv;yIOh|KBtd`2X?>JGa;We4(JTh5RoG}^ zr0=i$Wf^mJ)XW~UXzs@0W$dB-zW3N44%3PGYv<|H#D~#=hkAPHGm?_&FYRSXVbL!S zJM5dIzdYO-Z51&xPy1w#EoTvS@RJYli6=rfNU=vJBexv8)9))@N&a*gUAj&V{7i-7 znLuJZ(5ym&Idd!4{2|(9AxM^~W0}GVb`( zx)L|0OUq)oFm8&yt?hvvd^QN*Vw46MENzT^;_PM))Bz7Ljd<2X_%w=ng<=4(c$n+# zsyO*91~oo|?qM4AemdEr3&EtG2^Cok0Plmw&uB8XSGO8df+)n)?att=XSSg}L9pxP zTB01(>;-}8pc4v|lxcwF6TO-+832%M=ByM1uW0G&ueBI4wA=N;j5Gajj)!UXmo-le zqhC$fHcaR1{vzc?ET6BU5!+cWL0{P~-(A!fBZSNZeQ%d6RXEtSnBQ6p$p>E?Ya+Mj zJ)Ib)9H@BS*W};hNSQm#4_0};X2BeSEoz5-UTUslzedYu?LOD)0h%vfvdo&t_m3UE zlrw0NM=$v#;rC-8BJuv{>@H1psqpB2PAJOha(AZS`j^$ca$Iys%;rfCw&EbZkp(ue#;coN(-7SbPofD9t<(YSs)i z|Exo_@ZcW+q#aGk2b8A6k<|ll5+=1~g-q<(ugM#8d6we-x79wssfhDbgGwf19za-c zV;Wn|j&4tre-P6`dLw|AJGcS%MoM;NGHjY-YEZ;KkI=+P*_+(7lX)MjL07Zv+ z*9W=%1#s4(7)nz)-}n;`ouLEydz_4|cc2J=L@8W18@f!0?@Orw>?UfVe*vWz|dGB~nJ=J9$d!Xk+qHrrHm^Z2frCJ=3hx!1RL6;Ps^ zwwPgAG`@BWNwAnRZR4^x>;hkyd>8d$kLy(>rO?ISY^!DWaGq3c(a*Q2`e0E(xR{q_ z`Pt5O#oL0)YsYN=vco!s{kk$;#<=S7^_ub3{m6Tr&iNjGDDktqkA0mEMx-1p3JN)( z_J28>*qHvI5su)<39fvQKpvY0G)v`^#h;APcksc}#zGNMTp1?imxu9*Ei6JH&?JII zXa<^{;A55)W`ja>goxNl!dI|%ZBdhw^oM8&K9rA)71hIGM~tm#SkSW7Bpj=sw&Fwv zA9&`(CRDT%@1?{`nkrZF1zs%t4%Uf47Cw-b{y)9qBO>I82!YD#QWha&n#Y(% zr&twB(?5ogq%^MAtLoD$YefWCpwVTfSqk6yu_7R6x;zX>^sr+2f&MB;uOIuk|G(jm znKaC2p5Za0RN)BOUCF+%aK)=W?bwJnObB#jB8hrtr3eROA!8Llsd&eqyPbr#U{@-n zK;c}W3rSdumQo?aFTf}!tbhqStrP6aqu^iXj)s5N0NfWpH9DTi`)66n~B(Gg&zt)oV)aOcm zu^rrBs4B>Ii0T`*5H#UC+JKuziOti6u{45J6XBU7ER|vGs=vq2Q8z~xYiC&7{^l9EzP&p_I#J#s!)gDwOMRl;>i^ReE==yE1*?_6#<0gw|BFbeES^Q*A{xl&^ z*;-1~ht~kcG*cO$ps|7gnD8RAC$AO@Q6h?-0DLRr5>pA6@sRYU_|X<~OQ6YH8u>HR zXr`&-GO}}mZ*l9!X(!=Yc_}I5PN~g#oxCyhwv&{HLw{IaGEb-B7ipItD0C$9bxU?^eiy+v~$hgzC zgIDE`5J0#C%_(4}<)CJM151O04@~Cho0QEtR3{_f;Ju-2oVcV&F%km=^b>t2{ zJa)RW@fVd-m8h#;_n)htiI3@%n+{*Zul|iRFGoJ6Vv9uRJ}T)m(=eWfCecXGbl==q zCf|kV5Axe0Ai|YqTrVU%7Vcg@O?)gzYD!5(z-5y{IlhR7tzr^>$zjCbD8s*#{{tX9 zdGMFYdNozQpIOyIdw+jenHFVi-Yko3p1Klbk(WhYONHg=8dlLxcz*7=+;zgu4FFRB z0Bj8@q$lcHOkCL@0DvYx-UoMY))b)}GkOKnrB$|TdK&$#Ll0FR1VEUO>ZZYy-c{^` zy!x<1RYf3(vo^Oa?tDb)OMD(=(MIc4I^OSR9cNHuY9FP?ssI!vC<0MZ-KEHY8LW|d z#iSZcSYQ#ht05rQO?`swM;ErEt~_hqcJssIN8cQutjsO-kjOR8$4^3cvi)$K5$i51 zZsO}c1V?V@75w$`+H+V%gy*VtE#zg-gb&0XFaD0K62ixI9ZBbk{{gD0Q%@5)gz5 z$YSsZ0zNeOa@;^Mx=YC~7KaiNidA+dnZ?Fo*iA`%C`FnX7iL-m zald1hnIyjFGT9+ASlNbl*AV5ssUei0>Rfy4L>Xv=xG6q$6juWeit4z7^Ze*>hi+Zz z(P@0Ol5r1LcabqT=y8zH#!NkIpXoJ@q$Fsj!+iGWWjd<3bG(c;Q6Rc&laTZvgiu*+ zWMO!CaXiFSKj^7+Z_|l_9Tl9q3VOm~-mqC=b62RTKt4W*pVWwgEm+)=LpHqlu24s- zFDIPb`=T3L8A@+G8+4&%<=YL@ga6-vD^?@CCv^y0nb2y;YR^bl?)sel<rIV(8mT#@NJATr%Dz=*4D6oE*BLV&mv4DLCkcxOM;OS;&Bq2Ar@hGORZov znHrnx(^a`LC3>ne+G|iw%WerGKTOhC&hpDw_6Ok_hw;{}3<;IKutAzsLJ$ked3tA< zEa0wRvxL6W84(^*VeG4YU~TncF4UrXYQ3r^)R!IZvgHJMG_woMNVt%iT^`oJOYchQ za7YXjn0U{nN&VxM#>a3W*8g&$ze(Bn@5U1Hj8+rY)9H<{PR`M%1w4@+&tAhzf zF{P%DltsrB5X&;^L09;QpD8PnvvtiX9*tNBJ@j9szq^5 zr1?^(G(@LMqt8ohyp$ftpVBPTq7IJs$9SHc?w|psaF8>##9PQFKFlg1xyrA$G^7I& z0w&B+05n-rIIUk&nNQA_+R7Ec+Cvp!J)+*!O9}P&-L#6W1Kl{Bf^U+R`eGOA&L|G= z2r~*Y+RTJld7T3hk~ph=9h+#E33E{MhnB%?9dxCZZ)^~*cVpuKLrozrI^Vm;C%SGH zaIGnG!hx0kMzT%LZwW1eAef}ubn_=G910$6C+P&la>b^XlkPZDfrvEU-4?emR(g-P zBORO~jtHt|P!;nK32(YQqd&-eYkin= zk5IC?FxBEudkDhy0e1d7+o)FW6`XX32M4#A97sCojD3KjMZCskO~NcxxXNTbs^pst zOU4@=@rI{Zrr|XHRrT1ys2Nn|T!?j&7XF)J8vjsu(7L*_ERk++d5i}lx62{C_<@QE zStxePO{{qX*!+MmBoMdEg5?xG#PupdnyrjV3+_x?bQgk&Xi){PG53(KW3A6=`oNH^ zq*AZBA950)9!A6*mS22m$~h-8B8`hi@Cupb8$5) zi`!U-#E)5$o(a*bc>?SZQ-fCtprMl>%oZKX^zl=eJ(p$V(Fe7<%q(@FT9FEcVmzd# zVkM&qxm}ODjAvDDo@4Dx;6OV!V>(hmvE)8>C}YxqYD=QK$fPpf zqC9*9oMqrYXde3yObDHQlx53djs#VU zOgVqy>NsjlqEG~M4&Lg+K}D`KeLCWI%wIa@cc7CLr=!C%Hk*! zj3+M4TIU9tzwd+v_A@2^kRY;L8bT}N9?WgKfw?Di_-M3no2%nn-uHz_`dMgMPpV;M zl7an%pu7kTLhn&PR=pKUi#n!_Uiyyv={vLbGHAvEPCS0Mz;!WB4Xc3<_zv*P86Bw> zoZjH{c`ekvm0E-o{=vh(6Jo?ngD(u%#d$;|4pPKJ#Lx`{3V1FRNwF)0#j z_}|uRn(131;@^zJe8lvOwDjzFdZ>qOa&yMC09&|7TxXn7oQCdcbprZ0-M~2wcA^yw z%ndC{p+$SlU>{Ut#&MWY9(IikJ)}h|w5W#ygjf!bT*evvp+!B_>TNa{#RPil8&yx@ zdfbWW6=Fu2=!aCRhkz)BLNW!A{0`ctl-RDekJ8|sv;d4!i@GPrXte0p50H0wXayI2 zhbxInLf%thp7Pvzq_Q0{wCN-CumXNrjJ&0F8qk&MYpfn}D^}#8Jj9sKdN!w2GBhu_ zq(zNu(I=P69sptnpJc0Fvl~2=y}f zC`iwK8*6b8yItsIAOOI*!0kNDV+NuFfD9D?%irK!?Tp|8e_awV=AqG6rcV|w!BYq{ zl<($v0^GNP)>IoD0ANZcri%ei7a*bl(0r~whevy+fA7Wy3LdtVXV5K#7x8TF zaq({wwmhyj*e*nD>x6aC4YP3v^5+e3R%N>c5lO+A7JK*}EvijVUZomP0q7|qyjF{v zvNZ@=R`8GEAS~|xCaEVQ|Bozat0$#??tyGzVDt#LBrWtf&R`P{?mP}X#yi-oh3j*Z zm$YI|tu<9es6LSMFm9XDYMThpxlKhonhCVJgV#IZ=c>;TQ@5!g9DJXxRGz8O zp>7GAea)O!aheQwq1v_5PEb=E$?3Yb3HUA&o9%v2=ycoIdB;G2#Wx~#+1jI42zKXz z83Kfp+T+N)$FZ-c6Rm75eB%7#;=VBe22^mZd|Mp=)5U{0->CWoH!{>aP&&PY;B{+s z(5(uXXjBVHNSeU6I1s9GY&BP#6(G`hR@}Q0xBXE2ipel z+O!wt;)&?e{{%IB5@ry{MRr#kWNKmU3_}^uK)cLHf1$%3#KC$P7+|PDgB4=C5VnVj z-cya75@MfHs|%UG&1M=D3Q)iI=`YrXrv!*80VPua-6u!Z@{nH`h-QXi1pxVC0Xr&> zU{MVw6qutlvM=AIF3x{F-4#{I+ z1UP6U*I+_`+=PR?nt-2^W3S^3VsOx8dd6>7p(*jF)?TN(C5a=u=^2Y+pV}J{#OX`Hq|apn7A`PXUsa zkG(>Tt+6BeQXxvg#aKl2sm9OPASLbdwCvh5|1_YLc+RcG3?m!&TY6V2uA?>dzhC77>Se04(lAKh~k-T#TFn#&+)B+kPIUI}hT; zFSLO}V?&DD13`8tfzsP_qp6t%9@yjz!rr6Ox;0*udVhWY$6K~Krsq1g* z&||pzJ~=9Wc?8wZL|zo2RzW&ycG+y5ja?9~eoHlcPc{55I`CDF{flY%QNH?lC)U;+ zRVOgWs78Ko!hYo){@#Uo^&b0*v~k_G-t#74$ONK!=7(L8U2G_afa2$iTP6&FSz8-)SM>v**PN6~o( zG_iGEIE55K2%!fE5PFADq^oIE0Yg)o*U&)$Ls3D|Hw8kG5)>3E8jupO1w=(eZwN(H zN+=>CdP7q{MeOL+%a`x}%=|esv(MRkt!I6uYP`}LzJ`?^D*pElA6{F6D(d~;l< zCEKt6RJ+$GEljq-Nyu&z(huSHu7BV)4NEKHpgR%N~Do%S+6J33uZ|=SvJ?!M0NCGS|-ZnVroo+homAyiu_3BQ%00 zlZ%tP1#lS0!EwhOQIZta&^F^oHZ247lHvJA8XMs^cwd@WSv1Gr)~+1sL6CkTZu174#K@^ll-n7j2Q@|; zs!tl_QC0KucURn^D7qVi@~;63Rhx@!4&Si+qKb zukJVaC~!&4LrqKM>nsi5OHdoCsA{Cbjw$m0=EKPD_7|tfRzn^iC8))nPjCH3rFCAP z!bx5=s7-VAU(^3_J=}4y>z{R+C9y*s^j^e3zrp+2k;V`Cn!0mg^Whlg<97NcUJ-90g3yko|q7i9Uut%+bQ z&h}nzI(c9FklderNPxEALzQJBCU5Vy`SLA&@2&(tCdJPMW$ALG=_a(HukyjaQ4^(@ zy%wW|hgl5^krRh9CM^Gm9oLV{yu9yhb@(yw4EWdOerMyMY81ALc6^h5z;*iLeY>-S zmQ?zUM8mlkn9DkEr`4$!TEtff_E0rHrN~oCM$*6VCg5~~VyM?q4{6K$C=e{wof7tA z89>!dfE)vud1~h7ArS4l3bsQr&m5Xeho^2rW6(3^z59&?4k)}*nz>Zp53s|7?Fno4 zbwFbYBnK@wE<_zIDNB8|hD--{70_cv9(DcGtDA2BNTvQ}FZuO@;ZbrosB)d!AA8Xs z#{`HaA!^Al{4Lwl1FP_2d-LkNfIc&PV4b$*!Rs|?Mdpm=8yNPbQbF@dPv{912q5sX60C~W!I_N{L z{0y((linAy;#3!)6sVY~Mo?vB0#8?j*wJTHCmaIUp>~8nT4vyu|3F!)?V-otg~0Y7 zc_x!jhabW94VH=Qe>-W&0uHGIPfRo$El;&-PF@Go$0UfJ}17K zzO*8qQ7biX*}!gF`m8ShjHwE^*;%!Y~8r8$=j?Qtf{C!lxdq| z_n`Vlb3(C7B>;i@NbF6$DIIK75eV2hi*2aFTKI&-k6ko@$NCJ zQ)tpJ;h7`-n@_ZCdSiTZ$L~kx0ilH0l#XT!=-*87u1^4&@m*RlLxqRXzk`RW+gl8j z>W|LM#+G}~oq^jhC3hf1d9UHcxN^tk7oN5)0sZ{jj!GbVfb86+M{UZbWN`k81b(YO zU@p%-z-7VRn<0bc%>uw$kspgCmBd9!Lfqt=A`Ye_BuleeJcp}F~*9era zUbv|zWA&LvE>I_Nr}NO(d=sE@0-XSWVKG9cep59NqVd;`g){CkviaJ_5;WpCKoG#b zL94}g%zk)G_VCr&o}Vqse^F}gG@Wg~y{~EiU$V{h^j>1INwyukwLYi}?xC~qY*?($ zk(r7_?c>nECY_w+i;0)ACmRYb9bGO?ymWdhr{Ge~wCt=T>vR3vx>Ik7iFealqCq8i zQBuM-)=>F;NZj{urTO-tE$)#);0$wY3}k#u9tq9sg7>e!%`y_66F$lnX`~ zTraOFQHw49s_9TXHzjt{+SaP*{ns#$>kcybIwpEZbg1|!{CRBC4Fdew@!)TD%FPGE zKo!rwMK(ymXXrNh%_F$%ycQ`In7kxIU<%)-zSMy5a0_;+=&;nXiM<+RZsinD)|&zqYU5sk>v3o_YOy^DZldMaR>{~ma_9c%GY>#&yQ%v|&;!09p z$*?0EzDQ}Y^Y5rhsL7}}qw?smcZaw0;*4@9VZ~KKm3KYerl}4ZHCiY{i5+igMj!Lc z$d@ZW@tu4bo?hVQ(%7o-ZwomK$#{seSYQW$A$ZUbf?O#t?PLj-&GA%{(!KxL|!Q_LQHeL=wvX-U1y*hkef*2k)qf>ah&ox5hN{~{J2 zNlGxjxiI252Sgb0hzjblWH-Y=jtiG4ADElxfR8RH4cAA;g^|tXZ3`T``cRz&GG>ko z_0MJ^Q@I|R(*zjabV(s~_=!hLAlP!6sQOMaJ|5uLwXprqkstgwZ;Kg=Fly zUXis#U)qX(WKbignviaMyv4G44{ZtCdT+?ccdEp-2dMbqO^eZqm9rLa-TrB|y=pi9 z^P*h2!JSU22N&;N>v7`ej?PV}vzkwYc%*i4JH9(E>EKi3tM5L#8{%8;^FG{VVSjx* zy1%9>7+Pp_x0`W8nPBo4w`jay`zN8$?U&1^()ju4r&R^yc4g`={{kJL(G|?R-YWOV zwY{4qi5ga&5PVE!&*p3z+3fZBYRZC+?8>m3(l5J{mWgg_%ht(i57_#K(QM2t0Ll~R z8RKn>Jm!Hpb2Nlsf<{+$Q&*TefNfS>Ax~XqLhYy|1eOk1e-)Bz4q*UL^oKrsBlwdi zgX_eJEPaRQtEvp$%YJ~r8y3xE8l}_E8fsopz7V*26&B_%X@$`wfb0{rYgjZ$1>FZv zai&<#MVE#piC{<^;Q){h=1{j29lR@^-a43Bc9=^lFbyBXyPiE0F)KE_$rx}N$k67+ zw`gyQ2QZe3Y&SRywFDG@ANM5T#p8BCs{LTZQzImLykpcWu| z`LT*V)dPBGdB{kyzDwjqj;=tEkEfC~7X$-}e4>2HNQ2_!0}u5@e0jVCsPI`VgY7+2 z_+f~t^@$N5TVtm^H|$i^lKdpOPW{Q)`>4tWpGTb;U#c&U=B>?VKbp9{?_%7y0mq&^ zJ!e0%T1Rrx;YVw8>H6O_XvbPif`Ut4p8wtzH(I(As>(IX>qEp3AK187ee!j92XfzR zU z1v^dV0vt!~-@NiP7WLqq49Xb+-b-67I@Fc<+4qTZ#N^~Stw)GW=qL$Ci%>CKmLio3 z1Ut|DNu6Ait};M+^gRtr-ws=H2x$D7WHEsM zwA$sk|Bq%PWpj{C_D0UqWD^S5{B&%c;)j}t05zdsBTv?>#9s{FV(KN<%a z>{JWFaNboHZ5Or`W&k00A=Ex2&ncT@jsx%E=259qQWz`@4tAyI;RShn2r_tSlgyhP z&Mi2`&)ZSW$rgj8h|L6EflD^ahi89$m=li|9I-9P2`ixCfFVf*hX6nqv3h7VE13&D zA$|BJJExlCyvjN%mht^{An{P+Dw)bm(>7fYpP&bkV0o)hUcm8SbKW84zUv(B|J`!c_>|gSV7;eeQC@<149f21d z?dF%9-G$-pqISzr3FKW#;F8&gGplDm?t#mq{|?7MJgQ}Oc>k@@&G>$Dcbz5!?r>RV z_MBYx4qrL*Z;#J?vDI}mBd7~hlxG=oVqk5hlp1kPo&eOwGgZVu4MAmFgcL!lt0I&J zx&Sni&havozvck7H-WY^_JJHuIzP{m%Zd(kOxV*?Kz9tBLmb)U2*nW90mpPHVNwk3 za=&{72TP=J4*SStta2V0It_Af>WO5`ovWX@vX6<`7HFBBfPCLNkO0l z8A>&1C&Oxo0PIzmZ%zPD*5uh@5aAfG8-91{7TcxUC^CsXwhjuR!MwK^GzP*wjD3{H zNy5nJU|4(L1)dm|V>ied4t<)!wxeG>b&=y01nZ z<%7wcCp1lqpVofD*?w`h)qPpspQCM{%+-9_V#VfbToM1+8c#e_ZuPDeu=C-kO{%h@ zA~w+U7nN#hVVl~c2+`&PQY(HGwjPQpj~_O{1ftc*SlK}6b`165d|Szp1#P2fCC;^n zd!&TME~O8VHcy4{-H~R%YUXZ0zG*3hN(Jj)aWg{Bv*xln~(@Jv=PGPearvCsDR7PX&Lct_SgXeA(-Tdp(6B*2H)mOKCdc* z=YfUv)#k-?oK)&;j-R_JU*7y@?&hg-oBQa<)d*hbT+4&G*4q&SfiJW5hhrws>{zY$ z5HOW#!>0N6<{h`L-*e8Ig>JVu7#$G~v-u1Q{HPBXau@??i2-jE38m91E2GP7g)`);fwtY3jivCu zVX!wQe!>iu z0g1e}q1r7eP-hJLS3?}5k|G}3Yd5ok#E^Zt~ zbTS%#iUR2ACEG`Uu7mX1*QPe@dZFORo?-g6&ThE$3fxRjDz{b#g`f0BJ)@QeskHpC zlv|>Y9{{!dXf7?_zqs<#?qG;f6PiQIjSy6E@5Rq;awD5OBHOH%dh;V;fn4?U*!%Ka zem=VQCs!+U$sr-~-UqwOsV3LrTerLCcBQ?vcZ|L9Eb<|6`HEGnL%@=`Ru}ceY||cZ ze*#F9>g~@MHhu!OoIt$k7&h|P0}`0F-BJ!iUO26QxXH2w_%w%@iBCBIvL7R6D|gi2 zMglB+UOWyse838j_o6D;WhdLoA3UEQtUWu4Fs;<8R#xGw?!e@cj^_FD^Q0WpmD^xc zR@b&Jzqq-u<`+V~lL0%N%?aZ|lEkj#e2sB>Sr(1fo?IZsHU@6W7a6^%W;8VPyJTg< zG8sk3hY`ml#qW%q>P{A8I0!b!@)-kugCTi%w(!$4PPVr^^fV0IuaR9{U07JnYeSZL zK20Sg)qT=j&%RVmhgIWY*{fF{Omkqbp=!n}=`V`wCX6yoLyvIJyrv$1S;z`D^^lV&3uUS}j9Sy%SG@Fj60w(Ei7 z4Wo!V^>H`jBjOl`?f1oAz1`Y#CqnC#cEiynpEt2jfDPsF3-6BGPJ|4xb5EN zcP&TX-Q5={IQ8;N5;U4$3^R6pD>siz9qvCRW6>~Nv9Zg>C0FzhOA4uOawl-E(P}|JQzH8*`6Cr@P6Y&&vU?S7-uKIgp8e~%D<9X zJ#*+)mC|C|3c8#W%JHro%=~!y0)3I!ENCbKFIAOGI9{b=o-O-}4c<|6FDkHfU(Fe4}|6 z_tKhTd#qA=bms1OM&7oL6_lpf9*-UAi`9IP-y3f_pz(5G=;e*j$mc$$Le#QWDEH=p zh}$hMy^O!y+m-V7&`XE^#kPz>V^&|Ttrq1Zu^pylwCK0Spl#+ChbxAVyU1-SnhX?` zrB%(e3mY-Uf!s`GbT`e}aK?+4RNye+d@e9j@yRY(T!Zw;3unrs3EE*6OU4_{J2#x1 z*(P+RRXoGJ1MmsRc1MG4&|m`rJ0P2rGLz?vlRVX(M?V^CMkEpxeN_v6eN2~>lWumVNVfv&E{X5f}8^B z{492pBgHNoalxNXA&$Wz)0&9#!zmg@JsJL2xWJcF!|YbION)VPT9+0t$`l!PpUu$elBpS%|E5X> zz_=_Q0a%yIB4OD2n@k(DT9~?SgWV3_HeY3)dEK<`^sA%9g;d%$@V^Pr`{yisXRs8N zqo4f-A)TmVLUu^N`h?v!bQx-4J_!!-c9tPkGd~C3$=@t_w>h?sd#3)~hT#l5^70;Q z4OK(FAfSop3yb%bX5_JhS*lYxa(A!S^NkO;&o{KDU2@NP=bOfUjtXweF`TH+byCoF z&Do3^3x57V`$6WK42gXU5+Lh2)xJHjHkq&8WlBGvRdjr;_HOpqq{&R!_@RAQZQc$> zs*ly(%X;Dd;`1B(S8j2^8-6DXjK|L6ZY@@9KN#OSHUUA zdhHJ-v9!u0^R2k~jMJE(YUCOw^hM0zq_jONnF!DHso8VcaOD~Dz1<}by4oWS`1xqe zhqTutO#2HjwVW3vLvzL(A0&NkV+xb{L+;owXQuPx$;K&e$2abz9Q}wo?UsG;b^a+Y z8^6$wkCLgYx;GzI=X_gy{PMHWZuj56md6X!B_~Cz1+PzjUb(Bw`U#wsoZg=Je2}4J zXS(n|iE8F$JCXM-txR1VhrL?4?>pezC=(%oZNj_e{MazJOL5#$!Xq2_JH4Lu-03z0 zW|YIK=|BL8s)*2{2pWdeW4wXID()nW=E~sjrb4h*2%04uxsTch)QA)G$*C}k01&w4 zCIMpH%VVS5lqM+35rul5${xKw@jFe21+fqXJdaVSZb~dviQ)G_^~Uo^aKkEe3{0N9 zp3|S+`zMQAuu!)oc6r5Hnu

kVE;S4Vq8X=go-gd*9LdIFB#)aRVURsCzV!SY?r>nI*{+jZxm=(#w z$}*LonSOtc>pX?BdW@Ah+$Qoj@v|SKY)SH8-M(9I^G3D01b-Jt_B*jO0UFJ|BaV5^ z*yt$8e!cSI?T_QXcFyWDz?$AX#=NTc&r@4w&X<e(7txCG*11 z*U}#u*VNI#+sGXPIKtTawXIj`j{iKBKU6ndF(}^!+62JB0ErO8IZ;ud$t~>jbUcXU zwQdE21O$jih@cN*({#BH1b1h=`FB%JK!6@>xd~QGf6_Ln5rZbejM0~oe+jN^?UVr6 zBw*vTG`Bo&QG>TSpl5C?OTt700b*sekXZX_D=&C`tB8!9luuKkXuqBHLaOpz zLiF~uVOiW4(G;V`Es&#(ZGN(8BeEumjG3Xz9#1Mq9(EqUS`s;qT|~8!pyTT&J%)1dl_55n_1%8pJ)!&`_2j=u%a97ZEzst~NXyiF@i z!-){3RD@28q#!sYR%;CnHRVOuu#hD5=_HDcb$Z@j@fE~n??KfxVmZE&6}xH*QC`Nu zJRMlDLOyulH<#^M*QiLv45F8XzG=Nz5~Mlc@bDyA;xC})Od(l&eF3&JuV4FPVLoP# zE^FS^xBrOr(AbWbwV7q2eFb2HYC(xaoTM)zKBA;#Vx=BrXO*@I3LRXB34j>rMxd(xT!3Qu6#Gi74`twWQdDgpR5C@L0vt9m+8cwf` z)IKp{?=mUxy&LkYmTA5`{v&TAqg?I))BQrwRQ!#Mg~!l>^Zkz7wZ(-B+2Vnspk-PA zl0~I~Gm3KQA?;}cPqao~O8qJ{hRiR%&Ov92&S$EuH{C{I2Gd^#!auwjRJ3O!Qdo(l zGk(bmf$t1h>H~C+eNuFQYjz*RTmk^XrTN%lgg>)!pf4Z30sn#!AQ%bYb|V8GI}FzI z1QrJlG%7b#4_LmJG9{5d0RNQ>h!8PhJDq1@04EO+7P8O7=7DJHE*}x znbb)|)G;AgMJkU_^6V(Fqw1l8li1HZ135k;3v#jEY#lD6V8<*IwSN|@p$6pG4gl3R z1)jEi1BFB0{o0LEvX#WP*t(0QlavJ6u^L!}rED+a7PwG@4UF8*Urbn-P$W8rE5J!Z z)=z}NoWu>oalApjFWD4ST_@OgT@M*4Ve093K=(H4sou+dWZSA(@>gXeqFdr==8cxs zxn_VVp%Jk%w&lqVOC~$)Ar`M2%8uvi!$LR7=uP?=XMBuK31Prtx&f7x+o1dm2R4ud z%G!M;>4bEXHQIp+hO;r+FeCC?@A2lc(g>E`VXMbGx7OjmG4)-!eaHO7CIhstjJA~ZVgXfv4Z z{yLm?>fZ?QzaaS56kr3vkYXx92s%S1ftF`83sm7b58#XyrJ-IiDzSJHXuaWu-VRm3 zZvOQb)wj;iNfJ^t%u+v(7&|`yHSg$Qd#HY;l^&WUX|8dH9M0xIUAkD>otWothv?Q`JUyl9tq`Zx z$ow2ZBjRKAklA;OVslGkTty*RhaV_eF(j)zqz}7>O%!_4TNI~+94mbwh>Tf;->rrz z?W)*wd_WI5&GWReROBT4Eh!mx%b?y6) z3i~yw{IVp&pBjfv^WBa6JabH$J>#@TDusFqW>NYW9>SH+$b3UD{(yBb*LpFW5bXGL zs^wc;?dU}^GMWEq@(punS+n47fLy z?Qg5XCRJ2kcG;$ZrIQB5;f2a+K}Ez3CXb(9vqXWP8i1|vrrSP6Tsm6$c1!uqS9xSh zM9V$gX-c8M1*ec!ICon2^_QXBxF01DJnkg+95m(MJj^=>mQ4{Gz(8i@L< zzN*}rpcO(2brI=_iJDlJfeI1eT-DpAhv;TV+q$wef@W?Xq*|!#N7BlAtX#s<#`)?9 zh<a?QC&QaRu(*!MxD-f4^D!iTtA)V0ZY5>dS`Q?Lzo z3lphMgRS^L4KzrV3&apauB$-Q0Ld+{4~rD3bBUM|k*S1Xgkx)SNLpN=>@?BM8C2N} z9vxuo3KVyw^xK6oRo@HM1Z>kv0Nk=qNe29=xBsq`q=zKpg;Gl>5k0U6O(Ck#fI3^u z+%||E17szI0i@^@#H&k#Ff?)jYDsLTEGfmYPYJ_z?Pj}4Sl&<|)`5h{>i_VJa4(E# zI1C&F%qmR-cZy5gqzKb%OXOZ~*+t@>?pP^X&!BqQt%Q8`RKI4fsF%T1i2`cSK$wsv z#U_w@cAwEmq1BDpx$dc-4f|aD2SO^OjZ`SLRnkRclqTE7*oj0V& zsM7@0>aD6D35ACUK_TU^RRLnH{1 zzOj#$n*b^RX5J9?YuFQ_pPuR@<c1K!aMc9c}>dXVrGlEGF*0Ja@Vkz#~Ft^?rEIxs6R&I4-X zNFzr{Sh~YN^I@{9g!D-R3j0 zv2DX7&LwPsd1dmii|M37ct!*ZP@5W4=DRZbk9S1I22P-tnAtv^3QbM-or8UTBlj@n zgiNGYYGLB5c{$kvr)%U5OIFbqYLh|ga#_$`Cc0xVozLK%fsmY<)^ZskfM?b04vebb z2IA`aYAx&TLjb*_ty_dT-^u32|z_6XBXiw*?nIPr7Y78h)kVgc9f={Nasld5=~TyBkFzyuFsGay3^F+h)@8_ zu|(~Ir57ozA&sGI^UE}&`uzAO{ll`bR#Vl|YewNJ+TkhS^j;~*cCfVt?1XtC`+$9? zxgXvJPK~Q#9tuZH96AIM zL^QYWx|%;0$1=sNC`GXvzMCARHJ|lDkrGx%)lX_IiIzH8J-m3 z(i>GkuivLy+l`+0YO{UY_Ccb5wczICmel_u#O7}W-!4w-bb=>A|7q0b4nB#H1Sr

HOf3{jUA>v&9UpvQ*{UO(2)^kBW!w zE=0IoEL3n885RoFhgte1OwDOo07NX8#{lz-z#-VVpjCY}w7l~oN0R~0=F1QOoS4Q( z5A+axq7&z-ULHXKPFxqI?drFF|6TZgu~F_7`~$$Ntxoyy#i{2=uk_W$FigSEj+xOa zU3rtB0)mWOj@7SDwYHgIe1gDw=%%XxebjI=Iq4~pK#^@`yVa3jh6^wgZ4}|_v~0is zI`dPjxMXqi{MM0#I<_=cHKJw3RkR5=p@XmH?;T)&&0ufulOjJi9mH#`G`#$+F2`#B ze;`+g(*dBD=i4S_C?#nBZv6l^%D1`mEvwr@*6rtJryGB+a8j%MxkHPlQ>97yQfFYl ze@AI%LZQ_NXqyN&m-H#Ym({vJc*Fjf83GCk)T|_7xdc@tb2#YJt~alxtP9mu16fO^ zZWvH*lOP?api}zLS$()zwy6~4x+z367z9m{(Qly=1*jk-VmAq>_d-p4-{}Q_0Wtu& zA%U$Yq}thys=YOmOFcdE9;IZGsdG2eCei0r%h7{QnKSQOzSZ1R)%r6y`mL<+sD7WU zgi-9ScKd-{;PuO&xkp38z?zK5xnHWv89oZD0Nk=Y?knS80727vudUXXtq=e-g@GIv z8HNxLy#$yP`if*hsF_TqZKeX6sVGC`PCtv@DLi1s04F_`8)l$J2#VDN2#!&XW|ZTF z5P}dw2Y|yCk@EzudmpZj?oa{*@Y2@X({}%xtwtApZl``4iYKk!_*MTRwO84LKWGUp z_;t}J-EK=-zU=?LXg>aYoGql&Aqw4&iQQj?0!dY89&~Dqbb>^`^bmh6}jX+ z`pIlZ!TE|mP`Oix>f^U5;(*oxe^2+I`>Tr)7pN82jd%(t(l7&VNQY2<@cn9VI22YL z3H?x#?~(%Ekyk;C8fwy_gd3jywa>=CSI)mg+%u<=DkzS3_Db6lME;v3Dk+_tJ{S>P zfWB{3rj4lAGddj^S<9W+!2MsjZQ1Dg-KUiQ1+t%UlwM3xK~5Qr#pNP6HLH z`;?}Y;9Wr5y6?n&eMUkeia|hE63|lKc?sC+6ak%0AfTA$DNI-t$Z~Z_ZCR)rMnbG5xJ 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 z zstGWP+Y!0N@2olvPOQH5tbXInW9!K@xZ$0M_Fp#h4~%(v{WWpPiEZOE4#0g)g8TcM zIB9Q6j&;;o6B{h-D}X{|Bb$TwV#ARV%+@x*z(Zn38M2TgDe-?q?=|W@%F=3-$2=sd z4BwHRxw?TQ7$zWb8(<2OC^Jc>USpkE(-;0Asi8TpjW+ePA@gS^;G2CY7#TcHGs&VE z8%%GN@YQM-tf;o%e#Y@huS}@-~9b}yKZXY*_6bp{`1yI z{NM7DPwn0{4qhDk!aKlfkZj=*fIA8_Dk^r# zhvL=PX&g`8!0nS*qU12o(*owZ`KLs-{?)_+RJ=Lrn^(-sMisFD10qQu##U)9GcN|Z z%YoYT)W5IJdIvsrHqYc5*uALXvyIKTJt9~U0HOC4GOa3*|)cm1=~?r$-22`*|H zhlO3fcx&_h9lN1Bv**=f-Zy7Be3)ZR)GMBNhxSTu{{K{_*mVsPwTw%IY1j(KDiZ8# z(a=x{5{$ia!(+jZYwx<1@gV| zi6GGvnnImFtFJp=W54yBsBfomrhhqJV>lp1BH~8U4zZlp0dMLXzCHY6CNJB4t zF;=BmA9|xc6;lh=j@0iPKNX?7e;kvQU)9a`&A@kSkgUIWHK?ZjZLF2ZQ|>}jA`QCv zUUWaxlU~Fqlcs3@r7P+*!TyO;v90N;jgk7SPJU$1A71=~L9JUFUoLA+s>U8IA6E$w z*@0X4iIu$}&Ffx{M{BFrQ^?vw#oYJt@(Oz$$27X6C5|49GOwanMi7e`ypM_-y^Fio z0=`}DIEz-*vQ~(;cu;QDsm>=fl62>#^C|zeYs3`2BFDJ~6bz55jnLm%27KA8S{HJzqMG$8S*4>5OJ3W3C~q<3y^-fxxl$YK?M7PS znpS&Nx7!8friBY9`>RQuwCB>K+jO3fMdDnBzULhFp!t}*`dO`5i-5aD&VzYs^RaCmfF81G7&X>dg)M zRy^qoJU}R%F&#-QALxvUmqE_&bi0mZ=OM%%_bU`@VP1MFt#2Yj=aUIZeCa0m^>*iq zLP8BtXP&}yjeISOgaWniO4sc2g;l9r?OIZLEaB)EWE!8N^AZPfZ)Ty?%BQu~U{ki^ zwTk@8u#RFhXf5TQE zCTb{97p>c*ou;lKE0(max{Omo-SXDQnXtSn*qXP)+LM(--S&d)6iC9~`)`YX&Fs*( zud^unvKM{bur;b*IqrG-i6lUn2)ffpv>vTcptF>l%1!%GvjBv1jF;8 z_1Ug57BYb;=Hwb(r{DWkd%8i~MdL%}89;-hUl2A$b!bk}q|40^h1||PM?ZHQcN`^E zW@dQmXvd{P?}osYKnALZ0aO0J-Ztm)NpPYZTV)(qZq7_JHr7knUH4^2+;qEQxcQ{3 z&g_Hae7?H7%Gp3w0?+QKMvulrh;>;=d)_W175_zf_*c2(ueN8}HLf&{qa7ItjxI^_ z=@&DC;qNeJ>o;RF1E1PD?U7{T9~eomDTOwQP;veMv*Zs1b-sBhEbDN4*N2$MA)Fay zPMrEzB_Qk$#^p#8boXLaWf*4YV(;`@tCsS{yx;$8;MBq3(=qS$tUk({(x|E9cB>!dvwJhHpJb9jHlKTqX46ogQd| z>jl+adUWM{Lfq%1(AtzV1Dkzw%7bT@Ade0m(#rfcs#|>h=e9P;ZcBUMgBItibCT~s zkkZwDjRMJs$~5@X5P!Tvr^o(M%=ynpNqoCZ70+9~!!J>$1_;Z|tAB0~Cb0s!6IsSa zPu5sG=l+n8n?)NoV>Mv+5nOMYhYTN;^Cc|y<>z8J=)z@r^ai$UZ$Z26QZCQ-#Q{NX z>NyPpj%Cp=tU9pSYNr)6Y4>2I=L@St_xV-{)^w$kshgjKF7yd$W*tb-6(SDNMl}zt zgcb5TbkCv3?E*>9^kuPz>-Z@O)~bqk^7Gt`f<^`TU}bF4Ce8fs<-S2WxoIw{q@Y^^ zJBNJ&+4;-HIcp+yrlRWV1)o^Bist735}-+nc%%*fX|vDGG9hGCy15V&qSpQCz`dh6Gc=$PMNf=W*ApZ}~V zZU6o9<-Z$$-ZEBXRs4SY@;^E1zrQ|jf1>}pZuj4RcmMrVti9`#<>08Su2Id$?2la% zRpVTMgH$z|p^g)&dEkI%xxfIShHRrDtH6mi0C^h#BXECsY^su}05c(Yw@BRw2Z>t& z?j?bC%IorM4*0LAN8@I9p^w^b0=-GHiAuP)NW&HlH=#xgWOJXJK)h5ni;+9Rwzxrl zT?5xhoG#Nax=t`|PL}sd%rKeU&?#F{NfxVZ_liMJSi#Ex)msd;CUFZ_(7kf1l|s$$QeeH< zq?bPPm2NC6y$_@#>x(g1dDvH}LJ<+ZPS*iRAQU>>)&M1I5wDjygT?4|y7D2q$}~eo zupDG7QTu&XqgqbcjfKgOSN)cYbd(u~0@aOT5Je1flc+se*7=B4Y8M~`a+q-_x7nZe zNkPAkF~2L(gl%FnC@Eu}UIY(ZA(*$vrrgvA8)zX}q2Fnz@CCRrR`O ztki1X*IvszhPAz(yzN}O|K%tdo_}?(*eK^%N8fO;lk);Tvf}~7lQjLR)FX2%C)lZ7 zxchQGtu%%aNPqGiG2F+=-wsKuF+ zmTd>LjpZ$`ezjN<=#954sKRwTRxngstyFoNI!nz_s5dcLgSu*i^3_m;YU;kla6j2r z32hBvn#l~?Fq!Nw*_$u)@(89CdePqZ+m_0E-Lck8Wlg6_G+ung*h^H!HV1x(YVc&y zKZ#wysQRR}H}_Jrews<8fp_wXC-sue36gyp%gNQeEA>9-FjP-Nk&r)&r*x2~-~D*i z-5+nzyvun1c3_UlO%|t&q*l1BQdF#HmQ`!*7G(ZI(v06PIp+-`MfnndV3jNC{ zK2fPyfYP4RS{PUON`&7aDtQP{a&}6UT}FilsC7A2vAmiOQE5mFjAfw<EXZbgAYcRM79v0YIal7qbvYkpt$b(2SoIw;^I9>6}-Y#7>YMQ6}J zS0Dzl>8Ssp&>^A{536J?gl>pI--|u}^@8L-2u&6%+LF{Xdekvy`kzDfwZim2+XwIr z0!|8T!^p^=Av-SmLjpfT0_}dh4>W%u{?VLNdIQJmOsv2J#I`G$`gULRIdFF}QL^2B z_h;efz?iY&)-nevAczraG?8L4+0ElpP8pvx`WDU*jrIjrEkMe5s|6&_3RRq0+BKw5 zlT$i9ES(yb&J&@YtX#mw>5OAR*i)8{{lieJf>2-eFuQ_qyDQ-qKSFCIp+@Rq5%Ljx zox($|=xMN2&DFzeg?ficq2-Lwl8Dgf0zF;RvPmN?56Rp~vwYnR?BoVExq>sdfvHjT zo?Y;wu6mWpJ5O2P+Qa>(n`J!s5mw~bpjZ*@I;XOT-{UGZvDlg>J?g$lSJ(VU*k}*aV$yQ zLk`9+1{n$!+p!o!2CTwBrJt?|TY>S3@S9l7O*#x7)&=l>q9M)z!^5h)kh)p4Ln~Jl0*T`kx`II5qDzAJi^r=B)R76C?!%)0aM1(nZJH5utgfR4tvtSn6TtSlS+25hf4SZ!DMvStK;GqOLd{dY}?_`xIte-TMjEF)l1_ zAVNbLQTi|<)>}1Br_eaRQA`EJ-!CZj&WkcysZAEDzfgITAr#lhdyCI%TyiSCy&y?+ zi0Q+;Il5SSyWjfO!c%Mwn8K6VI zV%6K16$*uJT{$|V@~SwA0=rnTe;Ija1vOr$c*6iOPRz?+QQ^tf3kK9sh~X`#8?9(n zzQGm?;gbRfl03{!qM1xon=A&Yu?+ji)hETk?^q?7B>*yju}Nr|9*Q+kur*M4FQ?i| zN3ItmT5HwWSXf1qF7>BMGgjrlPqhg^HLL_PDp77GD&vK!l|(hP0m!kBe1xb$4N=Z_ zRN{$Sa)mVq8CvUwthzzibA3P!MH5;eXNpVB5s^qlwYIVBSbIPS-f+ z4r2FWVnUgn#X8XB+P>XcCxW(7#)q!$GxS~!?7Ljr%j4ZZQ}^#Pb^T?qH|bg-y!?`9 zu5C1w+eYJC7gJVyO`u*`!8og5)d-6UljUwzRqDkHgyRX!YtRXVVML zRW+acqnXt-qG!xd6}a`Dcw%McKg2do-bpI^i$;6+BPv^7>!!TcVN#ro2Dtkx>{nW; zM7|=E6gSa`I#Dz?$e0r}ipy9!gk2wJ-&DhmcWSwi)bzNIPhuAD~HEqR;sA^mXc zSyTf&^;V>0a8_2J&JVqata&Q7)>wugW-I;6bzFD z=ZG~OS&BYlpd}st9Shp#V^w;Wb#{tn|3IBXV#SJUm`#xO_>@|aSV<&AWC;MpM0CGc zb*OxQwM4IZ+5dvP8k?vZN`gf1fmb}mRtn)D8H^>A?eT(dFd8Q0ReI=}HwA#3`{BIe ztGDV9Ju++7;Qtsq&!#5cuK}kMS_r*k=p90jfDn2|dKc*;ARR<3Pke z_ApumqU+zU*EJ5+y)xi{-Rw%i+Ap}UW> zbiC~9n19JlS>Z4zw~QCl83&RyoO)IqH?Ee-dKv(Wdotx&lJXp3L_Of zd-73RCwx9@LnMRlJWI~o20|tkd=3Vju?0G^!+i1olb@-@IYrwHap6R{A%Rme!!#y$ zHm>A%(-$nWD}qc!xiy=f!*+QclEq?Ud7>-SPHhZ0x41bK207(A#nC`MZQ$F{qY|Cu zP#c3Y&I@1nkl|w*#B?RkB5D>QgzpR`cq_w&3{%N4ajq?WZke?3)FT7Zxd5n;s6l;)I z9(!~pvVYXb6jvb+ke?)7RSjx(@6hAsC{$e22wRS}?1QmV=R%vkF7@$zrY;0e=fvL) zWG+Maq|^`$eAByx^C=(lRKkXR08m zawRwJeS>`Cs$XyE-}qX8Jc0Ms^;-suBOzUqs>Rmd17FFPJo1|jdk>Ok=2H)w47fT5 zEmnFHkpFml;q9&VA3>BhqpD%Q)NDbWfNx}={@|aVs@5Z?gwwk<#<-=T(8O$Y_e=gZ z>Z=f0lE8SpEW{Li&N zc6Owd`&Zr=XB&1pme#mwt44ef%yfL9e!b-9m13NO982AU_W7LiI&IJ#4R3ctYsHBr z4s%!u5)T6eQDai!-oam4Ri5;uWNBK%+bmlKaL3bHpW*B5eNGpi=9t$HvvBcY%izdbSpWdQwPGyO0|3B~KAaDczzck>X*MW>WGN(1P&(c3kf}_2npifx$WAVx*DIOdji#z)}7Y z8cYVZl^=rgWhp)Z0^E=+O2IfjVge8VZ^aY(X+Qu5q`E{i5t`USI%0qqEXnE41I7{v z3|_%&7^KA3l0fO5Z75POx;MEU41#vXw?HVJc|f6j8!9hve+ZylWSw?FbwxHQiK*}R#ue`J1jyMI45&Vlo9ktn0X65Tp^*939N&4^b-%;|6e zrrDWyKcF+3Td{tOOAeKGE)obSk}+f|sbDhwmV!XZfH19ekkP~zgBSoJQ?`+2bb?}~ zP-w`K(Y&Oaw9^xQ038Hz3lQIEDa1GN@6809OU1noSK$ z+;1JZpLUd<(-Xi7GEWxJoM#S?^hb!f8A(b|pdoxcr#${BtZ2^>9vWPMXrbT)sRnQm zI^~ICTaA^|OJ?Wht^2$$EL{?Zt)DY83T(;{6?@sOMNRy*r;HycI+=32FM-h4Dm>a? z(Ttoz23(iXLx}z(M$x$q#s}sE&I~LBTOEjMM~^AJ#S*We)zK;ZBp-S`zyFF2r9`|m z6mwjP(>pB7^nR#7%VWvVv~l$mtT*l(7Qgg~rK+?o_;Se*_ZrTLB0eB$X+a_Qcy|xSs#40eT2(9H!A({84a2g_kz<+DZ?2%iX0tqAMcxaR7 ztg+k)ZbLZ{?>WGk&x9B)EKYOJ17X4t91ynw<7~BKj_>z59H6DZK>(Z~3e0#l(nW&@ z0G8%U7ExCsTDep(xqeG|(%h41)$$L1eVQsXmfd??T7I{`aJy$Vw>R&RJ@Y+bqu&ZK z*8YF;<^4b55E6XDya9GB5hfredx{ZTw0){*(H8HO4pWH)3dEp#xOky+KaS(t#W^1q z&nh8nDUEnejloEV8*RZ13Br%q5&t@c^X@ZO=JAv+U!0){lb0{HY^q)YrrZv)|Cf4k zmWK29=YN;SACc#9rQ>|8fu@oq9(`~JNQE|4IbNXPwa9qgG`#+SNZ*)tIZCG-z-Sr> zJD^24I_o-qbz)LIjTnvLdTDxX*-`gry!Zt_lus9d&ri=WC}KZG&k^dvk`kt%DlXx7 z&8;ZnYwfkSRbsj+VjgME2SzS&qg=N4IA@UvL!kJq59_WSW9Npalo``7jqwryOaw4i zY+$K0=1zLd6bj@>N$84}et}~GM;ay|0mC$AF_e*vnf~!J21zZ}x*~8`E2x?VbHp-M zM0#-9Mdzu;%0Fh2PxX|46*D->=+z2{X=h|gWhySp?5D?$d}Xzh_Iusx_3S^^WHLaV z2BCHW%7Yu^OTo23moRG{#CCfVjb8-U>5vVPv*r$hL#$(&GDKty+*t13q`B+pQ+XlcG7} zCh197{y2kz>oMC&GXUGMQCHXQTNgW!u0X`JPbjMi%Q}il700Mchu)Y2zp-O920-n| z&=fRm3(H_cW~#^H`|O}jWTp|i`ksUc@E*=VoENprW7VNv7o*8L8c_FJ&?tg-5niVUuXj+~aRUql0z`j8pa8%zK;djv0H_6J0aHOB5ERD91OYKIb22l+ zSk8i5k(Gmqm4l0em6?N;gOi<=larH&0m{Q8z$>H4r{d1f#UvokCCJSw$j>b(C@yFo zAtWRyEG8}{A|Wm;ATBO0DZnEwCL|*PmK9c#m64W{*HU0%P>>W;P*6}(zo>)>Rz_>6 z_~xt2Nvf->sas!DcTLk!VAGVB(b6{6GQEgVQ^06yX`k2BHg(lG3uCmgrh>7xmx+ms ziItP7j;5)urKydtxsi^!nW?#hn}s^7g|)MVQ;>z*bxW%NOB+vXk5pR*ri(gWc3SFo zR!sJ`_6`@79K*7mOmv+bqn!hbo~rQf62?sJF_*w z*($&@Eg--@$RjMs)A@?O*Od@r@Rci99jvd0UcVZb8tTa%78VvRD;pje7oJ!bk#;YV zl{+#-Es9q%Dz+dx!Zy12c}!MwY-~(ipy&0_pzCor5@JFVZjf$}iV_p!ZxYgzl5Qs9 zWRmdVcZR;DB;Ug0QwZ^)q?_rf*X>hp#;20f($nv!=QU;!Z)9W;Gh-t%3ktJhl(Np^ zZbtSErX1dL?q#Ol&$TT~=P7(pSNO22uy(14 z2rWvJF3MFcDl96h$$3yeR(wCbnA-EOq~u|x_ru!ylBC>{st2WIm8BI`<&iwqIc(L% z8Pz34)ulDnRV6j|lxu1mYZ|I+8X9UEJ8G-qYOB*~YisK2N2wuKskOyaYB#lOy#7IC zeQiU1eM3W$LqnxfLv8ih{NK-$fB*jd?+gEb1pE}R&nRTx zN*TgKUbZ<>TQ&_-Sp~&P%-bqQGx-=AZl)ey8p&q85S~=nRy}!F#<9U`vc2X-A=>|W z$z+R^ahzIA*@%T>-Nk$7_}_9(9@foge3-lSiBElNzRs@7vedGx;bqzv5uUDJG4ih- zDLu@HV{(z6XcN5mRpCW<^V;Lc?HNJ&$Gfk46QG(-57lM0yF!jz{;X+z|=A8@RGn(~V@Ap{+FWF`AeK=TqD^ zZ_Ll{`v~3NL(gVz!{ZM&Q&G^E74tZE#Y8p@8AleK~Vkf{e59s&f68N4><4I zC|$J{ogv2{{JaGoSLOWuJ1$xnyL|P^m1~0mm&#NW-|y#lW4>l*IeeWgzH?Z8Hz()l z0De&i@K*MHbwRh5u=duUk$`;57RL!Kq65R5YVwP5B@3wPI>+4hZOL2~t zW~IRow1cPyeGLVTex3ic_i6*99kL4^yS$&#AvTc_9AZvL9J&U<`LQd`_KJM_x~Wu{ z$-57eE6oG#BRjf*B5KwJk25~3-~Ljg6s*c1UZX_69m^KuV7n_@D(&o;0sO7x>J$@npJSIt}rAs&4M?c10I*y_gonE z%Afk@Bg_q*PjAB6V#IvJbNShd)R=+yJ_%J0 z>msVVUiiFt&N036dYx7Ai0h@TY52*yArnSA` zG%Et9pK)_@e72xB-u*rqx3~RgxtY*mt)&&7GtUuTa&#;aJ#GY7I z4e1ul4Fn4L8Kw2T`N?B0a{OQ<`7ei=mU)Gmc{8i{M+^y;+m;jPYc7(zzy?V8NZ#9oHj?=^}z3%0Vj(%h*#hNSlv1i!z`0^elm?74NOf{w0y^WSG z>fnNPlAE&4TI4KjG&}FPR_yt_T`@)V4fRP2L|eUC85=@KP2Br*g?O}pGSuW__HQyW zc^_OQ^>cCTUvPU4Z5774mD& zVeEdS{Gx>5XX=9a=M*pP>yEiw$6(aWYtBemaT)AUy_S`YQS}aw@)l~lujt4I6b!gU zTRupMMLq2IabC-tO#H`ik7t*f+9HR%A$G68?9H~GGpBRC(tLen+ze*?J%?j_TNCSO zjcsV9?bXAjTHow_k2hM=iQdLkXJ?tvy@ur;?Pc4gks2!@lhqsj_fY41ktLd;RWCdw zJ6u4D^_G5B3qdZO6^hrcG#h)otc^FDQO^i!j;$C!Ro$~Jx%N(1z{GuN`MzY9#;)p< zb~BCq0Ef(^_m;lett+(loC%RoJ}$Nk!K9LR;1q z>+O~vBjK&NPfnp(95+r*#<7TY-49{`=RQ_XnA^7J-%A=6Jx&-y^^-6c-)9TvIgi;Y zP3GUR-Vr-@wQLO*9*!&=rUp5T9K7RUq7Xa6dUx;b8`_g2M~yuPCz z%0G1sZ!fv~XWwA|yza*2*Rlbj{rAd^-PpNhxaXvdJOLs-oAYv!sMezOc>eFJAv_#EiP7C#fU+X)rF}*9M z%bg9WZ+7&aZ3}(&Q9tAGe^6h0qF4s&_WNYQl>E(?y*i1$m{RgxUgESUnaLcT%U;@lJ-+3?`lr$KqU&`M z(|`)hl4$Pr5ionwMh>QZbNf2lkhjmKeby=k7HGW5MnA~WrsGlr=|E=VR~@60l4%}v zFY)7q5tiSyvUbh;@){>YY$0e8`@Mmv+#IV0wS2@4DjqQk$gS)>&M-aU6Ct@l4Hpz@ zGpXVtI}?WJUU39^V?@XeP1J4ak5lZb;>ks&k|*cWJVv^oq#lf7Eku2qeznPM0mi2r z#3Q~&r3%J<%n>s`VmQ}l#J+~MVBivE+hdY|WuzJKSmtM)qfua68#!7@t#~xO-#%kb zRX0tAs7f&u`bwG6>0u|z>?8|)p+QcS>;|%pAck&78$^plZ#d?YrjT=FkIvv+Rsz@H z*I2`G(G2^~30&LAi0jB&N3jDs12oDqvO<)l4XgJU%!)<)Faz9d1;j}sKy;AQCYT)s z7R4c>$e?2+gJc^(&JLy#iTFxp5ki9#C_p*NjW3Y^sZB%&4dRDpc#C2y127)ZSUu2? z5W4nV^zCCGRu3{mEC8nd6;Tq&@deFj3&4D_gI)JwC8J<{ad%E0OYXEXY@*l>$V@$G z=6W1r;5x&uAv@iNAzI~(27~mTWBw4I_@qh18aklqSOpd%!(5IPDJ006GqaeSGB=#sC2@=P_+N&E&; z8&L=Vidi29mL{7P7qPEbApFV9Z;lY@b8x*^@JA~#Mlx*sI9MR!aD`SE4mf z+psU^Ky&_pJroAs#w_9!{FQE5fM$D%W*he5uxEf-k`e2^XHKAGtyET4dY;yQ2!Ic3 zHV2yz8IbJmRCpvMQlc4|^ zcTa}-4Tbe`E9?3u!X4+6Y??mWWvlpA0JRuGCo$22#SE-X$1 z9+44nEF5JFXNtTJk1SAhWq9(6qn}&E4_!(-@!zq7^^$MJ(F(uVF;Gv@&e>vY^~gJV z%dFmH!_3K|Ng&%TEK+}&K~0_Q!Q_LY2jxHCKhXB&M9k~3QLMz!Y_{}JP+oE0D60xC zw)HC0G5TSb1q?)k(NQcA6fE~E_!dr-9SLaMK)e7XYtmBb8;r-45;_`?*y@>u2J)u^ z(rF37*tDDg#2(pj9|^shz_p-qBNG65GbfA(z=GVEttmHtys~<8Hlo~Wdh{}9(V7`3!C3lcK!xbs{k$nHc|J^O)J^v%%! zfmljL$XXzLHxQ3A8YhMj1CJT%QSdXy`sfDXDvEKDQ+XGCLG~-NIh}PJTI(mr-V1JC z)kgkUK1+HcpQz1D%+0o>VA?xK1W=mkf*8fS1TJUfqBd-gq#BY)9L3?M3>qMioc8hKQ$! ze0IAgIi6c074`$vLJhih=CHlCmLv09#?Y#CZ8Jt%kbC6*6%(G2YEwfiotkQ7dEPNR z1&z$Of5tAc+@Z8Av9qpjuy*gUs#YR&dO3sokc@3j`6*X!bzfEa*!}kCN~%l5by(!L z_-kQ0eaxO+ym^6J_Jx`+uEM2*(nE!Q*<O=-{ z#D@>OH_xWEuuQoqWX?@^$|21Rprpr2If@6i$@is9lItY(ja-^sMy8 zcR>~oV~y9IIcBHE5hTE_%hk4korCqMVS=V;+%JqmI8T>av9&3KNAae9{_tc?eD z*SsUZ=C8@1Pi||HXylg&Gxs-`o)1k3j+#zu<08#ZrWy#=Z3_MM99dD| z3G^I535`J6hX+6LiKpU({0tBooxam^ zh&QGNYk+wI`@;KEmU&oEH=#{vN=4K*mT#}0UvhK)S3ke-^i#n%bEeJw?O{SZm*zKw zX3k%lGyOdEQc&V=KR-h=|KDG{qJND^z?XkL=VqaEW!`PpY8tJkzkt51d3M-s^nU?Js=j>R{?AWyI*oo}g&Ft7J?OJ*7$eZoB+k#}VJJOLL7w=tX zkzF6#9Tm#1gx#Lc;GSG0NWSju*o!*ci$v_lBKGBY_oHkFr-WEj#cL*}qb@XLNf{4hi;AdY|)lFY|CO^foBJSJ3MGP+`N6b zHS_tC(&2})!^3}{G1$Y;wujp};P;3x@0B2XIbZh6zWgV0coI%$V55I8qwnf{VaYv& zf1!Uu9KnTQ2GKYL zfJ1q+H6xBSBagN39HU#o+INzaTaU+G!RMbIYkdLFW4>wLNz$QwGd=yLo%>BoA8c|i zTNRnDhW@Tb{(f=xyB_Vk=Ed)x<=-_?U^zQ*oDw+X&Uf8+KkQC_L`Hnm!F|&~e$!O` zuHo~a-6=@Z?pq?;cW1Wm<`=)aoy$&$I5s#v;|79NX<#j7aAw5GyxaHW+!LLR;~SU3 zIp?y|XTjtz#}CVYM(O{me)nCC@&!gj@+#iwQKji-VC5Qdp9q~hs678A$ zqeJ<3o&Im92(ZtcAJTLGUH1Nl#)8!Wkg15T+5iX!0J(qzUqC}HME-qs8Dfh2yMCJc z_w~EK>$6`rl>e?g{ktCVZ z2ZwSsqPC<}!Fva4;LC;eqb-v)VV#zC4G;0p9+jG5Th)_B@3YEn-Yr;bi`P4gsL~gl zO1?Gw@!#fte6OO%H1KJrM#N#r@%VCse*F5&_uKTX`S|&n0FPHKx#j4vq?-Ewh^N`S zn$EO2AOxNxicJBlI2>JPLtP;0227OvG~8#F*VYP#B<8+ftdH1#JJ5SfdRr=;+LNS{ z^X|+zG}U&yYCQX6)F*ojhHR^%D-jLO?2AtWDM28zN+s9bblMAmE}b?_@@uV~`;3S&|G6anMc><@bie8QoEJ)PLHlS1%&(v2qcVj~TUe;J`4JrA*~Xh%jE9vSlS zhpL!>uRkZ7)x2)lRD4_{Fl7$R_4?2Wt5W!=Ugg~yS<7#0_d=tz2V$ksqPzG$b^SJO z)4DGjzX|2{#u#Yw1>llNyRfgU=RY;{nZti#GA`=v{NjqWGd%x6G*zG3`83wvR8V-S zYDb_K4$l)B2(GzD=X=In6y$Ye#NvE#af}gP(l#PB$;6K34{b8AsAw;QbV5UaD|c^qKD^6X)k$MY`Q1BD{EmvG&`BZSJKBky8!g73*IN74W6$(^9Kp zP17%6H-ScxY)*FO-i3jY;{kcZc(uPvmP{m0Tl4264!Scdi3D3n!@Nto>okz1FiM>x zaEg4J`!3{?;FlpA{-`Qv;cn5~UCZHE$v>29Qx*};$%0_c4JhmBJ6)7I+D*o)* zaj|e$^Xl*;(oK?JI9x1EQtDAsI*(k0fwFKcI&DK&c#tnu2xaLco@43yWp>e?LDtv! z5^rbR($)VsCDyK)CB*)RXc?GY5N+X0&B@eyTYcdl#Xt8x=H8?cFsk6+7{cVG`>T%~ zi@N=!hL%|gzNKR z0W-?H5QPmUGdC>z!tCuQt>^w0AL;XJ8u9Bmk(5wHPp_UlB2BtgsJbK=`$1aym9sAY z3z;wkwzu+4yZLd~1&;=Csqmk?ILA~jLkmLsr)zRmaMu{<`;9~j_y_m_Ld}OY2hW_g z1^$A~6-2o|o5-6yJo>V)T%vUWblALCS{>pxzN|00lGMaiX za74)%OEa@G*e0qX!tLI&;Wp0e8bHpE{N#-1;Vxe}3b#8_Nl{=8e6d*Ha{X3{XX!3uXhB?hY}^sWnu&v)xpU zbSK5zJ&1W57KpxE#^Q&adGOLaP%X_Mg=^bJ=>5lsm0~~v1mDw>O%U(IfuG=7T9&s& z-fhNF6Q%q#mHK{hAr?f#+koCj+?1hyBT3#fz#r2l1!s z|77Lj2blybCi*Vko9U!LtxYc;OFzB$cBvLDS#NSVVS)XOfffw$;dsV3<|i>QKG zEFI#6l!_ySb&@c*(NF{36s~%dysWuBV@K`Frh2=N9&5jrUHb1eN7Q`uyx0bJ{sa!bf&{|C{Br~h{ZjvP%TSCzl_1xIRn*>Cy+N(k6J3;A=;gPq zdhjVx1%3r%(Fj^7i@(xfq8Jjlm+~Plt;@_xQR{LX|1H}`Mt}cEGu%wh3_!(JDnn5S zlY_aRCfvM?rLrp3Y0g#NcSb3&#kkZkebAPU0Ed0He{ML!W z`Z@gXZ4Z_QGv6K`XH%UYF}jQB>cyx zLlZMxO^%e3D@k_@ILRlxkyrck%QcwKpSMBAgcH{adS^g@Sx^-MCD*o0H8{uA_TWLv znX>mNqAb=j%~ZWLe{M1k(yaJ7Y~Th6CGGBHe(LfyU#CD>W}DMYOI09ZyKZ6kS109M zRz)0;dg-LW2`6Gx$(7#tbiWK)FP62b>esW!)u2u_D1DE+Te0w`evWGXGCJ~>+~T{# zf;yqF%0EHH-WwuuG?+7q??JC|-9Og`JL0#>wI1W@!zZ}4lMpwH%?u}ngrQ)`QCXy} zOZQAyQy?+2@m!LtGj)rv0X^1q+`webBEC$r$?@08&*NP@5mV*)VJ6@i7T`EFw&?XX z=yJ5j@`upL8p)wBs}}*hCAcmly=3P2`;Q{yb=Gtm%W@-P#U<|29)^FYWif2Uvz6#v zM%g_B?(@#4?9EOuzM=l3hT5l9BKc>#f=~ZN&(Ei(EZsq^A7~UX4c#`=;EyRgW=_v^ zO63qrk#-*AkmY-(1yY}dvxW=a^f)N{(8-Iua6VSwJTfz*ZNbW9+hJ8Erf`jY57>Qh z+JE#?|F$-F3#QS_v+aXZV=-5hqWq%$1sCe9b8{3?`;uA|+}7G~c?{5WE*zN*>!h*W z*}$_h`H2X%VpdZcxJGkqwtXJ`ZF`|t7Gyx0UQS$lC`Xfi_w!#evH%6avn4TlBTt4t zex&1hbvN0|6SPVxke^ILjPQ3SW1f1Km683POqWwQD1C4Cf>hyuES}~pgLpfNB6!&n zvL)X>;8HLVSNZpN0kklf*^>bM+kcugoZX_%-3&O^2!6Nb)f9WXr|z8L-KPtAN`>{} zf25Thd>5%a;OctHU#=nDQNKTrc)Bn*RGlt9^%!|HqdZc6GuiX2-&6tD(eDil746|t zhLh@F(9>_@e5Pj1vQ7Dw+5#0?&((juhfz8pPF7G$542Tlt5Gnz=58;OguJt7S3^u! zw>Hs+59)kC@aHqor{mys0M|F1lCJZel5C!*Ba~Ds6wB2sUl!t* zP8Mwz^=y~f?n9!ISxx}7n10Cs-SM0K(pwY44IUM2EGqszwHOgiiv0%@mylXTE~c0r|5wL^})xdd6nleT;f^& zl^H1%FRUD-^O2t&w$%{Q-avH17#YhDP2gaIIS?#BgXOFRRlzLQ+AKDVR_Cd4u}9vP zkLaI9v~ML!rr{*h@V=3F9y(4Rl}n-k1<=VkK5gCpeZpr5ZR>84Pkg`J%I>eUaiWsL zINMRjnx})AK;GbFR3yX<12&5U=L%WmD_GpswJ4CVa5JCGjm4|EO{%$p*x)J1$mC*{ zcF`A;tbAZr!vUQGyvqc^DS_}&P~QUu3vJc&;sd(|llTn5%629OUNY?;pzYDn&6|+V z;*Glk+O8D6#+_$I2~fQ$LMRHVo$@a>{IIV3E6LND3^%uvjaBE6ZKa-su6< z3NzwvPTc(f*2YloMHBT7@ZM)qj|8GeB-9H9wZh^DKM~E4)T2zQcQDko2MVx+v1G|y z6KXuEC%RJ!auZe`1qZ9GDqrpldQ*t5MZ}gQ=xawgWaSe{4{@q?RS>4iFHQ8c4)Ro` z9RXF(mu6Gk_4u9BXi+U{;C_kZ%Q%HsGQ?MPkSaqL3Zv%@9eK@fRicegY#m zbt0tCVmKz53!Z|ZPZ**m?dcQt8xS)z%xnX~mThJWu=kg-=e%Igbzz=egJifehx|Tg zA8eoB2{p3=vEkaqOD1B)fh>Grp>|RLdh}fbRM(Etrr_C`$VP<*Rt~llwj}+gLWT+D zAB4b)s41NT=tVxT0nYe07O&$5G3v+jhLA!xo;hJCBMPw7VQZpS59!)^0Zed2-Ah(d z7_9B)sE#3cuPhn};jO7kpFb{y7l~mcNA!>o4Xl+04L`DEteik_r+{s^2+D69J39$_ z=0@5H1RdOSOC+UCz*untY=s;_WAR?WM1yZBR0Y^oD$&T?nQCpM5e#$SJ1fbgT#7^pcM zkDW_dJ$_zg1@#Cfy2^}K3C=d|OvPM!6=O4gXg%&ufwFzW-zb@tB;zCxAh^#2j?UzB zZfW$MBD{hfk=R6#oC9m52fb($W^PaCYCp{o?uXeV7cN0^KD*h#p=8!oH#jjb8;ZMk z(G%zIL`aO6v%9Ifo#NZRygAU2m zal~q%^dz8;)>UdI;7&Q%<9<>D5u)y~i;b=4mOW(!U>UwKs&mLkUN9dwf{sk!okSDUZb&%#k^z=* zZQ`X08q!jV`TUW%)A#m6+;SSYmEPNMl+p7gx;_woes82AoC-uuEVs2ol;=R|RFJM4 z#g{$3Rq z>gTlewUIfaFe~M4%0|AN+$4Yovo*BGr(jj$@B zZV%pb1r~k+HXm6xvBO`>205IJxuXdxSfU^ZX3_)p5(bA-iK=wSZu(&jH{n{s=!GZiI$HYRwi49Uc9%4K3f3F>&*2y)+F3U3V@kU;Dd^Y*n@gq3e*RAUAL#&a)snjyVMyLAAlt&Tp+CLRj6e~ zeBwB{s%r~Kl#M6?qBg!b$&hp~1kxABQ@tdJ22$_)t{%LQ{)KCB+S+wXC%7?|=s~uz z?6zUNEbGy_w<0k}fWfRWtGt>pH#msx0KnFICT)?*4OU>xf&Xkh`i^|Bx5{_Um90=j zq@YZNADkFq<+d5zhnj6F0Qmkvsv=u|HA?rhnFD#vH%mW<*l!Scf^jeaj%R~_F$aKx zap2(Jm=)q5iQizK-xeoeHWaSw%5TnZKz%+H+u+~bY#@JHNZF4M<}zDlP~fO*5(gl; z911*N7vpnKSyoI^l!Jwv!a`EwWamWe z>aoSPt_D4eZMN>=LB7dTsMrp>kt(Q~Y62mw8@s_s#P^K8;4R zUfk=~{4Z#{dD2$%;eX+N_2!o@+YYY${&e*4=A-OJH&w6ikEFLX|Gd_iR;cn+oA|t( zr9LY2%tm!O^7_L#IL|W*p_@ISG_Khu=Hd0esJhadC=&He_)&U~*G=gLH%~2%Kdo@t z7A`P{A;WjSZ7gWs+}{ko`E?%qXoKpntj)3dLT+Haoe7;0t7>UkJR(*jQ-$%%l)Jq` z2eTR-7#m&0;ABs^olRKhxmd9UPnI5F>z~v*n4PZ)dWc~Wl`gpv-c{#v^H0>cYcOHR z*gfEa-F**@sfQz(quBxohoRn-quZ+O;p2{Lx8>wNw_R31o+g%4_$2v50546zI>Sqpxh; zd^K9pj4`Q?o1g(%z81%QpHc&t7}@rU$jP5H;~8uEaX#^I%<5#Lmy3M71=EYjuNSCQ{&x&u!(b!`;LN#qRq)>N)C%4}ZgQn>s2MqT6 z`>?!jsYB&G5i*R49f-wLi`1_q)7A(Db)KJno)WcG0o=}QoWN?`DSRSh#VpXlN zH9A5ul#1PJCECe)zt5nI8Q$;q#i^q(OEAkts#>)K*Tv`DH5hh=MivA><-~}gb)j%r z7^QL?Y@ngLTWDbok{6+lo!de(*z!GNe^BvIYJCGZo4f&5QgDKH&aqzgdUkp2W}(KC zNy!^BJN6&QRJ&&O98d_YLD`_LZ4oF!r(U8j`FvgUXf= zLekhJN|KOt2Fad;?2RR|hE%p{#+E%wvNu$sB$fKAl;8aRKkqfyHRnC&eV*ss_vdyy z_=eJa{qdrr`ma1|=ddj1Q*ok(2u3@H6tE+KETlZiyJ}_p)5iUDkFU2ZKjrQ*nDh0`}e<%sgqYKIm+9?VM$zAnajo1Q0XI$; zsQ^3>P&vUt9xM}%4U9u*L~=`f`N4&x}tWK z_{jSpxM=z!%38Qvz?X-*NK2sN8AN_)WhL zLc>|$>#>Rm=X)34t2=cEIWy4u#5jtEJ*)lW=b7 z%5HEzQ4@%pbsqc)xmTKA@2sWa+~63|;L*G0`YhU+>*-R{^|;3Ep1R9EbS|-7WX)IY z_|*RGSFOkV*28z%%+;#@Br4{y#9C;`mQn$MW#{gwkyPYn&8^3uoWEQe$L0PgJ1h|C zdhgO=?VJB{xlbYtx`jrmOsf_-p)AUVQ0`cQvSs~Y1zWTIxWPST7Ln@EUWubZ?aFbU z>81F2bJ?GvsFtWjl$qb+&OQ2=S!xM`{IGdZ_2AcQ8KdQnS%xBaRXGW`;c_5WC(XM4 z1uGZI*7?2#Q*qD9d1ApMlyAh>zk?Q(+L7Tp!yKd(8$fx0&s@&|c@In=NH8CQrvp$j z(_i;m6x6E{gf%YtqlOKKeLcVDb=w9Q#7B*aMSe%{=HBl=|LggM$=#(L9)T!(_IO%s z3rw9IuGpvUxze$@r&K93qL zUEZuVG-aK**Y-McVNdb-^dKXY#Bk2fmXe$Gr!4=AdVNj*LB(umFoQ7jB1Bp5w#r7p z0kN5pSN`D-9_?tYTbwr@_hr=NxOZ)6A~jG@FAk-7Ns7IsgZ_ zaDWFw6T*cO|4f*e(BD1X_7X}^1935Px1OafX_azvPz_QO0+DC#T8!|Uf%tGu$>PDo zkD6cZ|M0N#EC7>S0mnH{u+BFkPnMI%P2-@bg;iSfimzyBx;#b0B@)aO;&BikN5*7G`h z>3#K+t(^xdGT)Le#?0w#0GO3|m=^6a&R{G~Fp>?E2xQ$wWLx_C6J*F)B`1r3chriHK?s%~b$#SCX?Q09$U~AO&ucPR#35>*d zwOe8i{l&T>!p9MyXtq4kKpB)YfZWNp@E#Ot4SRL3bF=jH!it=w(ZUZ|p5g7O-Z6wP zxZoy@_Mjuyq!&pgkUsmx)%m|(;P+K&c%01qs}qyl;uI^k#b+$U^8}v3Lu?hlx3jm~QjBcnAzG<8cjn7XYI#<`S?WzU|HLoF3gUiMn3o`o?vsO(bleW19z6MDka&NY2q>%fMH zQ*lq}1_^=2cwjJ6(qTlQGgpsoa?KqeNM=6K-4mzDwTbMz898@S&rK@$ndW!Sk zWvt>B7s8p#>; z96@yQ?2D;;m3g{L%v+|^RX66|uzZBPbdEFd60(T#<}X>mYVY_MS~I6i*{|HFi)~EA zu)@k**?z%Nll3x#*m8uBoKEap_PEbp<#IFkd8|;mX;}IA4~19N3I`>m7caln4pW%) zlFs!yv0tCP``drJO^U^9Sg=EVeZdhc@_v|}2#;}@_v7Vd)fEsIbdWu1u{3s>A0y#_hg)gl6)8u#NlPVS|9Y>OMVxI!T{u z5+UAhB>|0;gk=CYM-D+e%De2iebW+(dIHU%e~wCi!onm#5RS`M4niI>LT}=sC(&x}yBS5_ z{&Fx2%>5sWbq}7DpM0r0)s@EeaOvZEGXW6~&dFf9s0Bc{aa(zG&StvEHpdK?-~{X8 z>ZC#E6XHajd9jJhFSMlF<=!DcKzMB@54KGgDWa2qs+^afCj>$qUMau0{C2fVqMY2$Yw=TAsOv;zx|2Qt5zIL8tbSo1I}?n zs8);mffEUj7u1Yn{57VBjd1Ay@M8)W~g36&X|5wQ4!YV(^N z5v}-Ahl+MmLx+N*og6`IzD@~Sa9v)ofk&tiK)1?-nk_^oN<%&dV2otj{iuchiZ;5^ zH$-IwM^`=j>`fz75RTu?jKj!*PcB%+Wep@?5yb6^{nfR| ztB2dy7uMZYu155lyX`>syC_(AgYa)K5S1@fdhkVXY0 zg+t7aYnKLKs+^iqi9+>dcql;F>QV`Z=?r%%Q_ZENIIz1cP0hE27e25h1)PhIb+qb5 zZ{A*)cZZg$y!UZnQf9lQx-_Zg_?F{vCMc6>hE(O`UImplArQR+q&*)`NWT?z7m*0a z*75TtFc>0T*Ci;=3WVVSs3sk13lQtN&<22xC>^@t+G2M4R0BKMn_9S};qbRO#K;#B zvjfNn!$e|06(<%liBs#vfdtM%E$DP@I?4dJQ27>D2bE!(TTQF)3ac=x3^=7+jx&?B zJXjwcQHfomPP8dwMb%oCsD_TdU$x6~gOp9zd2e)ftvInQ|4Ey4DVjvoG`sU)6xFBi zUm`viet~c$yEFtlHU2!=RlqAvFm%GYh3p+v*xZxiF?=BHz4gL2LzKKG+rsf4ri;c% z0hzZ+A>Jy@DV7g7+N~!6G$*6aHQUz#$DB@dDGph_o$`x{_XHs(Al1Ws{VR0u1_w^# zim@32wp_x>X|e@Z))Ew%VoBI?QITBfNLKzjPcW8FqA_T5T-+KU7rbKz=Kx9*40zGo zc_Ux*cIEn+N-K9JV!`LMQ3bZk2WbL~Xq$^9a0I8g0w9Buz!JO*kspRp(ikG|=rSf| zlqnXLMiK_3UT)qC zx5ajUgfD2J&aU``ovrFp!0@Q}yI=Bn7EWZpTU$G`GJ^wcU1u@%U#5!2WGm7iAJo|^ z&)fS@=HS4A`m~jKkE`&zW};GdciyRb>M^7&0XL*ux{^b+RQ|}XUSX>Z_>fX|k$mPu z`eZZnd~{e=96fwrB|M1+-{z{Xan+olx@!PoxQnW1ra+C;V1lH`Zdp|ral=eb52~~V z(T;@3szOjq;FfbSECJk=$wK?mMb^x4b%2s83r#NH@T}~QXn?0E6h^P;r>===D7>iW z$rp7&>^WF>hT2`OT_HokiLvW0LtfELt{*C?XC}MGQ7-DDBtS6a83c_3YvjtsFr@S# zw%r_zeVjy57j>;(!oEvg5xS>h2avRnlT(C9DDIJ+195RQ>!ar)4hDpHem8M!LS%6y zh9O!p5RDiXt}#v?ZziwTrCvmrU*Tf(x|EsbQmS3ToC^pOP;G;uk-#Nyd^L~zG4YSu zZdma_klo)dqr3cx55Rzaqrze<^t#?&*v&lfaMQ=g9~n-!A-Eg;q~`vs^8U%IlTf$t z|FCyy?s36=H zd}YeP!Of+Tfc!5|l{}VA5{Q}$2~U*N;j2DH6e0rZ$wN^DM?l+!uB-`#SO8EHKr!2l ztOr$p2g!L3l~{qOhjuAa0irKdI2fv&z{eZ{2r~Fkhc8Zq$fq%+T3G^(T;c{pA?dT< zE|_Fp7p{||xy6ySH^U|YGK%qH*(_WrSn}-D2baC*E55tUo1SFQ>M#@3rbv~1##imN)Hlif+xsgV=Zj0F>KPs_2NR~gB>ZY(AY zTICFCtHuFay^tw7i8V-OSa|@|D1&u#iN|}gK+Z8>F=at*8=z#WX%~Uw*>NZ;ptM0( z3Xa>u;bcED+oQ+DG9luj46=t=!xl&Y^WpY%j0wy+wM0BGPIH0<9gc@}KFYYyXS21P zaw)WeJt;X<1*ZVEZ|E=zKq@j*awq^op96}H0 z`96-6Ez42>6wZv(qe0}D=F%&4(YySO&4C%f#ETxIlK_G(W7lgII&GHwl!q%Z{yO`p zLhF$X9zuvT+sollY~!U(Afg;VBQQSXKd7t;L_8CsJHD0bn7 zvA^pV|2xt#xP{N>oBv$g#VfV0`YrrcTkMH83=ZX!C@k;tcx-qAYFB7z=%U$GEJZLV zv!B*=Rgh>Hv6)l~OTfV6m&8YMJNAk^* zMfXIL)yAW2^7SwL&R)H38vb*QakDoChLuwCU`KLyST2|Vi&H6N_3XP2S4{xdXxXD7gUqgVmZUTU)S>BIB97g~p2oIIwbXPc(ZCmgKuIZ?&SUDpi=0+Dr zh+rBWOH0LGL(&_#;JG}3J4C}uuWry!JTvHAW22YvGYnrrp}Cmfn9WTMIVZuS(G_Ou zNW#kY?2R!N96Phi7MP#+iM)A&o3%a{^#{i9w?~?Nkj@k)vv1q$ehs!>{jti|uCf54 zQtZ(RvRG_jZwk^Nl1^}6pHfIe*{vMNC+<^ye_ivRy^jc5CbW`BQPoMno3E*M-!ig5 zUq2xFXeqE(Os<96VV$^&ufsP(Dk#9i%PWf?|K*nNTo^tUpK-qyyu zTqAjUU>}yVRChphq?@-}YyaCMhzLFNT;El7`?DZcF%y8mOoY*+3C-p_cB3LzqXUcQ zQDF&W3a3KbP~!dpl&2+z|E#mQN|u+BR_|1##5q-3or21%+L%f&ITS^I%bsvNyywAn zr>ZWP2J)eXRvk`$g^6(;HmenOz@B1(aKTJac&|ht4kBt(CG!_D zr0#0V->^K2?r>G=0_-q?jVzq`1Rx);ju(fcDB+>Z6w`lW5acfOFMcalLxWwHmfr*@42P&g)*Sr!kA;3#D3F!P}C0{xk#P%7GM38gpHFMoCKhpTrr>YvabnLO!8dYp2?D<+gsVBnLy+oYe)!ss>5 z?||{@nYSXc1ujsblt8}kQVPr?SIQ_#|8Odx|HWLc(0g#Is>tZUso2{~N8VKZXXRJ@ zFOU6OR-L9}=3HTRt(LZEVEfhbaiFv4P@oyPST#X`?~66yF+_?e3DQe63_;MF5IJ>K zeMzX%Dw9aKv|`R#p`i~%(&FPeF#fg4jy+tg*H*mFpa@1bNS-`w2GX}C#Ov(=&n6@9 zG*udeWDCO5e$7MlnY$Y;KmZ-efe=H1OerQHD$IgRfyroJj=UI?4v;e;m1H)Al!-=A zfZXX`$9onib|rMaZ&Vm?og<&i6;^5dnCD4)p4dFJ9uqA^->B06!j{ zV!?*I7GWm2hl4Hp%>b6kPDR=Kz=W6}B$N++X97qhP2=f!#`XF_G1*mwCge(~LL+bP)(9Tu4H$!$EfdM_=!K+3WjpxlDerun){46Pe zf6Vr}!q~?pVj(-lK0*f&N-~#|plT3$7AXP5Tqt=A6zt&&cvVP~b1VxecN!G1 zG*^VgRamYq_Ndl);NfUujiW&65dX zIC00QOt=x<>lT|Qj-+w2KG%rByhGz)ndg&9=kPx4B$r@OCJ^i)exacpXtAS0=*}Em z1icsm=gGm*^r*TIz`=7Wn!;l#wGBtzj1%wZTrgQOHxRDv{5x*c8}rT!MO3|}xf_5} z0a(Ms1}vF(J%bD2^)ta7x3D9#Qqlp(Z?(IQd^+%GyL?wi(&T4*o3lCfb!QB9RiR`y zP+0*MNr96JC3{+qn=Bucq7nAESLH+7I7Ac&fM#DSx->NE#|@kE>AG;8>`lVxz$K%#^|ksNLLKeC3OGy)$4aG#H%f@>iQf-9B^EQb!BJ;`Pf`-QKl)q zvfT+tkb1{PAE26%LM6;x+_={--#>Fvo*JiQu)(L;A5b}{gFO`5C3?tDJuj!m$u4b) za@4Oc|MsAH)tLuF=R8wyrGLd84~J^`NY^UoInS?g@GWQq2A;U_$k>w#?BR7w2hku$ z<(V?@F|PEtP?QXXMU-NKq7Pbh1uf{!@bm@2W@@G;TTat`v!W#9*Q5T0ac7nCNn|B0 zQEiO$m^Cc(*`t@T1CLxJ_#~ZhMb|)$=H&3kOfwN;2k{pc&KHE2t+ap7G%ek8BfyUX z8C7@lSkE~!IUiiAbv#}tl7X1~TYjNe`izYy$QH{plMP44AuM=Vk6xu*Q{ky6w5mg2 zb01#Fa69yYAFF*a50ZTYCVJ&hbH{fB4dLL!PzUvQModFst@HBG8Zbpb{sh(CsVt=} z26>-J;^$ym@@`iHq2fkbi(;MKevhe$;-p?7ck0lYKT&?vyg0GE&^V!&c5*0?o9i@~ zwe^^wtn+YX;y~{{IblTKp|(JR%b3G;&l4X_1&tlU$Kn+W*=UWr6v%y$6aDza;9UDR z`};*7ni6*&2uD4?1UL3dMW@77ml^b_whm8d&&rQpocy!&{mezva-|dUlryC_Izl23 z*EY+PtiwdTu34#;!PpsKirl#rU3fgvk9Po0z-GwCmq29 z5H?3lpG9g}H$S`~Ymzg}qA=!Hf9qT11ShsF@eWNG`+IE~S7&Frhw; zeCA=qCFrMdkhXvX=fl{?56S>6oJpty;qC%&6Z^A*I7Sk^}95k zf1|D}`M3lXJ<2~Yg$kB903iIhd0Ad46a0CZ3g4iT{Apx|DlI^Ie_zw>qo zga*P_lIEF2JI&;byfUMjDy&HXW%1VJynC5VA8lEx}=dq3AT5^i;d`cXhN!DhK*2Xpxx%$rSP+XBevSVR58V?r#V zI~|_DLzOUv{(men@jp4lPJn=B392%YU^=0hCkXe#%Tv!R91zf@p4p-jYQXDNaD#|f+%3)xv{(IscgwXgiVticx zaw#caUg%~WI*WL@_y2jhf9L-G zbku&tVB{z>Dpv*F<^QYBrdjb5mBEHZW=^>FED>#xDB*<` zf{=<>B9Q=6pGz!blc1&KGC-r<2hw_2_;Q0vcbQrP3^lE#3BFJR&$MOOAul%oAiyvB z9|(;I57GrpN(CcJE5{Xd1l!#MrD1STH1MEHL7dNmwLyj!S5yKUZpa5f@rRn9>#&Zx&7$$*LKKS#V%+KLg_R zp(gCWtCNALKLc<4Je=tgsKhx{?h+I)8k93>S`{u_8>prlguJILDOrlQW(gEAFeRx} zGd5ndRAh?E2hk#|sgy{z5V;g0!xUfR%Z_-$aiw@62yUmyD{YD`G6CLZQ%TBHRAMQ{ zge&}wCu+?g&=m1f^>R&7$QE(u3U4v7Wxd(0pY!H~|1|ikRcFhUJDs?_q;f}3t9WD9NRS)z@)r?AN=ecdKhLl9>o`k z#VT!n~dxp&LA}W-Lh@3iE zZFf0Pp2@1iH8W(ksHArgnQ!!%G&(AciB9m3Sx2Q@h(lWH8}6M{Uifk1y|c0_l=Mzc zhFFRMbd+hL22FHIb}L?8|4uknwT2`0j*<45O{iJQ7^mXvm=NEA4;2U88~W~*RlUhu z%}l&|UBD2E&)1P)1A=NdihJE;+_GeU-MDjgHn^{wCa!;ZBs118pgLSwF0OMs<$>oH zTy`mcJ|*POg2CBGPO&i(OyC<`ypc(f2cTsPj6Osd4nVaR5hW~orl>bHdh_a@j?ZC3ZnJ_+=u!n)*9)j&ilZ3{ zm^h537wRAY`^gl*gE*QOn9KwkdBU;Wh*O|g3=f!M3TLz7JRaGR3jOV*at)2VvGFrDEGZ zG?=3r)zfO3CH{6R8-(Q4-{B=icT$ney0+u)ql4{tu0s0? zQ`ZOX#f)Yjii)~p452UqPf#pEjwA$;)Mvmb<^qij(k4^93=oJcg;-N1?lMs|UP7u~ zf_R?zAOk){rx@@sgAm~)s{R&J2;iajl|r5X67B#xyOgja$`#t;5!@Hadh`iAHEP%1 z^Hme3)zR2&20=(pczx350b5i81ZaF$zej!uKLW^0a4UjXxkzvU>B~$(*F`ZNM~uvc zm8A-I5FzmC5B@RbI?*akE<-jWfXRCu~dmPF88JI!_w*e6a2>;-&{&*2Kc3i zNOJb zImt~PA`VLGmm>}G0}&TzO4+2lrjj#Lm3q0zO{(<6QqIQl`Q^>F_I5oG$>6rRzZfm=h=4 zpBbLVQxD>xI+@s=1VVk*KjZwe4?3@}lSE*#Ac4t5#xhZ%3lLu_91Z?hhQI|uxDy*@ z#KCUR(YjPv9tW+*Vf#5hqQWwqD;8f3eE3VJw~#l9HuAg0{ZHrGURLxi52 zZ3V&dn0P2v*@-5W zV{9%9fpGy#%~2Ur25G$3B%SiUbUpp(?W86J%QU*SgyD4gGosVi;Z>j++Mg+Re&@wN zN7PZtbh)La#q3+!&r^;F` z+qB53!KKoo-i5v6c)t>sytu~0uSVpCkST@92pl2gbPP0&gF;|r`k_!B2Lu2zoIe(dWFZDIe75nMt#hyO zQmTL{q%=0}muc1ViJ3@r(M3A6Sq=oDC^DT6N`!d#m`6d=SU3vQ~oD^Umpdnh}?)ON9$BbJR_mvH!$$$%Sa6oM_$~^IwJN%UZ zl@lSr6y?H(C}H!AoX@YlbJ612K!9guZUIywrQb8(f2k5Y+cK`oH?XxQQ!hQaQ0bnF zmUksW30B$NR$>l%jt9-*DdtuQo_Z^#(&FP))_R&R9W9I@qyt7aIbMCb-&*gdh;MMp zapWjp1%iC=R5(t`zvAsp>iM2-L5bUQ2{=*LJca8rv;8oFyuZ_uoyk(*qSygL9`?FS zXx?`|PfrNoeth$&>`4CtR=G>(5* z?kiA3dGZa`Q8d^ixwWR#FYBcg;tKqJHePtC#*_2Mnur4zUO0S^_Fp_er^DBDR=!8$ zD%YCMSYFqkA9{9qfOGD!M*8&md4bTmYZ#N&(&6BRhd_j9WxhW{Cc;1L7S6*z?99u< zG4=%~BHY~KYLr|-XrA+7um_{5dK{>pPLs}r;2Y@~ts>MD7Z*Zq?S-nujM01RPfY*J z!RL5nrjdjbiVN9=r~JbPa;%E3DeaBP-=S-C3@og!krd_6JWz7F)cwW^aIiQdjg2`d zznCdxN#CTISnkqMw-7Yt(YxJ0u56gwnp=>L*c)=}59oi*jE`5j9h^E?HRK_G-N17U zFhBea{;kH)r*U&smd0s3-Tp1IEJtu9<|IPKBLQhdz19da<1TaG#+pZ_hz|qzbO8v` zp>a@ukli2!Hw3hu5`lIaSxpoRKn=&o?kS8kMaF0ll)I8=C@V*+KL2vVg- zfpBG|TXqFR?Q-=xn^!to>>JS-_v*85{j6l%LT=CFRjuT<(hNWOXS&KRHnp1~zSN7roxD9*l1u8c09y*OT``UtjuT-L(rR z)E&6&@|O3Cs*vYinfz}SQUnr^R-bw_PTT5XKN0 zn9CCgzRxK7)b)KYLFt))Ows7foM`Bi zzC~V>DdW~2@k>4C%!c4>cxDx0K5=^VYbN?~iNUW}=#7>5V{!`O%C>j&p>qE7oQH>K zC2PJW#(A=8%Py+T7tSK8N0quKY57{{(0lf*Icjt~r$0*B42?PSkN=xU` zV6F4RXQ(pA`z4MC6z`7?4z$~rC)HM#k29Yczo@pmfaW|XsBTDFS`?p_dZrtHXY|5e z-Mp}VtbWJCb^0f@`hfqGA3PV*VLTWA{5B@F-05dnepyN`p<+A9W| z>pvX+lsu-NbMx8GHAI-v8;6YX1--tN=nnvLrI_TB7cXD&QsL`_W!XNc@CEF11>*++ z)`3Ka^XNs?x;sQR@>Lb)pq?agE?Y&YDVLdNCNr|d-LFwopI&KR{cPa<-eCB7n z)C*2<+QpF(`f^?VoMS+Ug#{tvNqo_|#i6Wk48fB6TJe*f>-XdM|MiD+ZlurY&z(=t z1hQXRmKE>h24a$Y?<7z4uhY($@^yRx`)4h!#l|O2)J+G7|N3Zpv%R%XT8k3!dbTsY zpk-+L{Hf#oyC_5QAy}86$WL|UXswuE=(%5%zO~%Cp7Z=%?B)<>H-Oq4bv@A7ZyC7J zdBNq3@^0m4keA@O3isquze zan32t=`tLE^p$$dV)`74vYFTA$J|TQBsxb8d!ye!w8~xdLS6cgOT0cE?^_ot<5KYZ z{?3D<_E-Pum)4v-(dz!8z4GwL@;;T<n~0A4|fwGTxf*e+CZ7Kpy=-qN0}iiNpWfh2RFEJ zS}Sn?5s>*Fr*0X)CosO?Yuv^;8N>c~?a283=!A;#c=MHbMpC>*Qi9dp1Pee4SQNPEp>r62UhH1}9S70YTtQb5`;8;u zLSt0ONjv?0~>mn)QTTAZg#x32t;{SBL7QD`*rvFpZ@DDD=7_0 zNI)(1scYJ!Kvain=6^{UZz8X6ZKe2ar25e^A0(v;t)>P7H~vQ6fG6K*RZa8Byg@#d zDq?s;3Ui~$73naBg1XwLErmFvZ^}8u>`F%XfQXa!d}6SM0S)fYL6v~$7mf&fBH|>F zZJ3NsXCn1z$S58rOb+QxL+ob3obBP}Y}n-~lG0bqEL;5=Z3iy;?xf<4qx(7Dx+H`J z3NGYDa;74=M0|`rQq%z+>i~BKZ^i4vkJ553or3RA#wHLEX-lF98-*jSvK9P}q;w z$wm3e=u809+E#eS4U@zxcs-5Ip%+%T6{c>WQ<)|FUn>kPS&Kps{wldGe*5}H$q&QZ zn})Zq5pVxiyZzrlQ5`?f2`Gjcv5S-Qm+~(e~1@fzo2N5;C?}R=rGRu&`w{AFEzcSDmjolP@t?N>neY;FMxg%9pD1 ziPlB>(FJ|0JnCNYWHmn>C^wHTIAB!Kp;mMa zL>r<=!VJCz*x{9pw0&@v6EEF@1UzVL^v|-2-=~311Sw z`|Kqw)cVLSrpPKW`@5so<*!*J9fpY-N&MDP4Siw9Y9B*dvLQ`)ULRg!ggCHa@+AJ2 z9xd674cplRk9`5l2!f}(!EG~zROEJQOy5xy43KUhu9{O@LB~G+U0cno?VPFYQLpQh zsOyWa8?>$)7_58xv39h*t~aG_!o6-xqJFxjz8hOVTvPYrWBp5shRzx6oA&x?^|}u= z4dVq3pK9tqW9z;QHjEfG{E%pP|GRE0=-#{bdyi}G{g|mA_*i>?tiH>-5us7nz^QHE zQ;d1o7G5Lv6BcWO6EJQPs%@+Wnuw`Q(z8vZOHDn$@8O2(9#HEPJ~eh)<4S;L;lgI| z)TSPZX3bB{V#n|66gE|FU@JIqt>h!$uU0Y&Nl*6*jlQfAg{YqKf+{DH&d61LeO2}R zD7@0zgk{|#+(fFlOF8n@)Sk8@{H4Wv7uVZ>NZQ4Ccr{GOnF+O$Jh|lrKRX+8->B;1 zOqE-tNslmpy-Mg1JajfB%?y3=0Vev9;(F-##~`k44A-;K`WVEg&$iz9 z)S8sq+6Llo%(gww#Px%?TeWTZxVEI(*4v+2b1&h_Pq*DIY-`kLyOoM(7q%ALw6`{5 zt8Cf|X4~%+w)T#-wsBeqGI4Fp&R%+FrcHaspVq-kxQ)Q;9(=Hs5!)&T(j0L0DD zwmnG2=h)zKj<-LuZ?6S9lWN;nB|BC-o_rj7QX|>kU)z~w^Mudexn_eOpyM-*+lP(Y zAK}`nhFT#~Z9U))9-fA)PvuNq;$Vt64{_Wc=Hu7L@qM!$-%>fLsT{u2DeZWhjAj>k zjXU{?`_cygoYpnM?$q?akI=iwnw`6YySirakEoqJp*YPVoXA?Y=yvx2wa0v|TgkS| zV3@zKOS!WrZ5D63-QCsEJ!sRpI|lcx1DoEME7D|k3}<>QvN9-I)#neX`+Q4%&7(7t zHGT3|!_;eTM+?P!!(BJ3&exER*}%-j1q4J0C1VKF4QN<&%heYoLqAoebGg1lW(PN* z!g43eBu@UzESVT^k&V(x{)x??9h7Fn>pdl_Fhg)&rEx#Xnz6uWqk?x@a`_gb2 z4?Hn#2j6w6lbrVSn(p_kMWSVupLgIp6ZFSJih$#b7Mto1bR= z%nqz9B_=#sXbb*OWV~jjh4k$LX+D?anA9(Hd9PIFA2>TGvnI-fVq zJ^r&cZQt2GJ~wP=(hby)iENL&vmIx|a0vpVLt`WFhes!A0`;9^TJxim$49<&zHFuT zlu35oKR(yw@wCf$s1eT{pv@#5e_Ewtkmt z;|a89rM8a;w>>BvyNTlm=q!{=4zHvS4E_YCR3iEAP{Zx3G*a#I~lpMDfkkWy{X+7=}6%y{D7+Him85O;R?r%flNbDHBZ zeP42Hw^aL}^oO0H<&mP+N> z>{aRGnD?30J`x^(W%ns-d(UdGIvc`_UOue&{D;@#!7jhK~v+cRzSadHvv<)z+ZnzLXDe&)nZ zhuih<9d&a}Mf^3-%TqrRJ)Se)d>=YKc1^PLT>KwI7UsMa}Hu88Zx39}(r^_a$ zJ41V%xBgkr^u^I(-kuR2Ev;MX$!|&1$A%t{XZHPgWRE#LpmJ=4#Dex+Tk6$`YVr8` z$5m@%KkltnD#>I1t!u~R&7y5j+t-^3{b&D9UJ=^p`SQ@F;n9T?mD;N&m!T4}SKeKA z{N`NpS7qEhlT~|^jV~UW+BY}TC@|F3Ip4ND)V|PtqhtD)=8qiX9l$|Ttl7H;QQ>kg$_$eEu71ta_dMI$-9Oia1|6$t zeKOK?_#-J_rKg4_P+bZ<-eZcTa!Fqr`C?AMP#_2RlJ3g%yQ(^ z%z@L=3U=CwzjWSv=cb0NXOqKTuNQ$@7f!97E-f}Qe}ei%ui8?ISsR6l8H2@5PuwfGjOJ1mv9BJq@(9jg1> zB<$|Jn-cP>`M$QhLzF$dqtzPX$o$Roa6|HLeVIFdgqaBK{8;^X)9hnSLx!%#p^}Ov zY*ia^BB-{x{aaA~y@%>yPj7Xa`D?cy9BQm3%UyD0^ayB-Ux~_U4?;4@s`UE1&C@!)21u#CE9GGw74a#L zHQqe^3|B8Lm{Xo?lotO}q#8>Sv$SD5M;Ob$KA=#vLDA5+qJDT~iaGNqZGX@Kwq<+Z z;HN{-&t*lAeni}VVuXE~KcGL@FOib#ka+)x)$0wX01kah@SkkqO=1TiD=+G8S;+ogLO^+`n@uq(1DrHASIG7Z z!Xaz3^0tVXm1c4WHqIzp!(ta-|XpGFZUM()<8<2(NxBH z`K0PPhi9kye$7bz*eW!FqR_;-a+yC&^p81~DIdD@X6AwR+wnVM75zO5x(>;4#Aj%y$VEZzs5bROjyJ%(G-dtGdp=5A$ z_3+t)H;ZcZJ{Z2T8Vzr_U-U7!%J^;DXvFCE{I$-2eJ7KTT>N?T_DZ;tL9g1_j=Qzv z%^F1&?VC|n_KreA`I~;b_D@?mEq@~=fpC@GH?Y*WyUCZ&9Z>fCn&Qz@c_pXdunerN z5E4}7XdYQ$eaz_Ss#MTvJta2cRkw`R#CsoUCA79+Q&Qy2`J4)>lDuHFt&Oi<7RoO` zBfWj>u=4lYM;^fCjq)8F8^Q`+NvS*Zs!x`NHP_}S_R2R;JK6Y~)!b98(RlUI+0oV9 z=OyUkG4nr)&c%_b|NrA>SF_D*n9W?;-0#gbO0}8GT*_RVOPXs4jnr4AKHHf4WhADg zxg?^JB>Jc}w~&fLD%FTmsgJH7^{M>!`xDOD`<%Vs=k<6#o^dV?7g|h?(PP@DgcWrX ze|O@AY4@CEf4s6LwK%r@OFyD!>BrI$%d~b+ouc8JwtkFap(STNp8c?^c*i^MiP)_b zh2Cm0wqCaR*o&ab$ew<@!Lc0e#7DIsq3fMxb2f6WxkE&)W_8)kQ4IO;qW~}eiQee{ zAV=6k-3!i5jfANF113x!DhB{Tf&IYXmdMX=9`FiA10J4^{H6%yuiya?lJf%0+XZ}4 zp=B)kg-StJheKFOu~hRE{zv% zfPY$IYDbSq%LYHa)#D_0_Tw+*2vc7b4&HybIi~l6A$`p6g-7M4xtk3Y7C|aXTNet0 zn)o>&cqo0%ZyBXI-@v#ioH)+Fu+7dH{sj6O5Gta19;^g5iP< ze=kDzQ`LE9GAA_JzL&RlDD zxad{?KK#v%k)HbwmxMRphd;Y9QqyzIsPNePlFe#|1I=yAJx|5{!OIeMgakdR+BfI! zuJUYn=*#Rx^94rp;0=JpqF=y%bUndSCUXF=J?90Wab)7q@c(XoJ0k}x7;AMg00Apb zo-7^^|K&?P#GaaS^wIVf8Em2C<(H>g5B{s# z4<0=fC^W z76<1R0y@rU{iX!Es+@Wfz552biKBO;>_I~Q-xW>8=KfK>x5q7=Du$LbkWt0_T8vY^ zB-gsTkhr#f2HbZ7n@_f{#ql^5wmHJ&WvUtWrCxLu&V1?D8f9ouxo?(+DFE=BMN&f+1~`m#@x0ViN%6L0PWpxgmA=725Oz*X6%PJe#vG9s&> zzI-{Kbm5Y2eciHG-K;lKQlK_SD0ICH=k@kOD?-nXU?6P0Jc`qM4Aho3i# zufQiphJt(?#>JYYVt5|S!TxA|kQf%maP+htT6l6JEK~UGh|q%rzVWxC70PA*-C=o; z89uf$WlH7DQ-#Q}PyLg2=##Pt_9mBSz(|ZeoXdkaZT$D=&AV;=&{-Z-(sTSkzyE0^ z(CXH^s17%X6H>yve;T-VIu8yED4X{X^FiQMzU7F(Y#HdB+|;RjE1z~v-Cy)X?cy4} zc)?j^bpRi&tks){djt&0PL+LR=t?J?3;YYeXP+)rrak23gO&Bqu?TwWm816sNEg2I zuXp#zx_?;QUWSFOpd;VH!oS=*@oS`s; ziQ=yg?*7}mw{e&@3i2yAxH!6n8_|ldZ3OlEe3JTD`}Fs>(Orm;f2pX33H|U;JC`=l z#ugBg%cEIPkDx|I$2ZNQv=u$ZIZaoOx7k6j+JAa_)%`Sp0eX~|U62Z$YgZRo1Wf(n zV*t=bsZ2}C{HvCXUxqnrIZs3x;u_Eo!z^EE5%W9BZZj=sX=qs0gG1_k? z-IlkjN=9@%Na&K@lkDCz1h49o$qjk+7f)*=GbS6TpNQynGrb{_jZ~yCtHNfG#@-Ev zvCmp|Ns2TA^@}rUXVTQ;M)KoX@WO<%@zRr(3Bo-C4trmnR(*2qS?5IsSwg1Do{4>c zBlN$=OKy#OfSwK;YMiFA@Y*_8_e~I5GdEXEff`^<1lv?_}y|ew|4Z!7qk{nP0_?YFs``Y{*=tck-$%^5rkU?sze2kRt!U6v~ zX7{E7ln0c=U-1rG76wy`y4r?ke<&csg$Ti2lz90Yv#1nfT?5hy{}JNT4(>#bVkh8}?7 zGE_FHf!E(!shlyp9W%W+=7GhZ4#rJhduw)vTHGI2up!I(xB>t9z{5?Cx||z5k2H;s zLa9HZdCDxF&Kad=zsA99n|P0SIBo!@hi7z-_ZY&wYj_%B0boS{J|ARE%-)K~-i!)F zJ{?iLlD`iNrLGBVB)jaLTwG1zcut2mYbuc&^y!nkKvq{qhId$=DWesRCtO8Xh*$tmQ)t*ix%WBVB5$ zH4j%@;=%&fS&w4ZV79Z6I8&d}W5UXtu&>{Cq%gtiP69j?q`oQ?b_AtJ;3)`rGy}0V5nCfu=!WOrf{b;nq(?0MFy>ZWygVt&>b@-7sJhhA9p%aK@&XX7Lc)G^hJfLkt5Kj z`DXRnH6pm{EC|n1II(%EPJDFZ@7o@CCDM9yDe8WC{Z;h~rnSCJ<++lAXl1jA7FfiZ&;;PSEDLLE zVBRA_&T?!3N5y?QcXQ9n-&SB2SLnS84OolylXEyML?Sm=wS$Le^Bp)jsvJI^$FnBq z5L(`-O=QPUaCYB?I#+2Lw86qUtST-H*V_mdJN|B)XT49TJm0F(Qh)cApfuG`vb4Hk z*H#QIJ(vW5u|`l=^7ZdbW&V%0ZuxZNZU8F=ZIB2o#h_z`fN;H5Y5P@|Vhx9v2cqFE zU9kV&ULLw`6AM^wN^(sS0CYYWP2(v|WIwr>yIIbIaCq1eh~Kg>WEG^16>Q4{ zQ96RW&cR~IFx^!_TxRY@CxO9)0?&gp*dYCh?6{WPz!8X3LayQ|Q22gbC2&rQfI=3g zt`^Pu`&?%i3YS$+8r(n6iu|(Wb|@9{MM1t4^SXj$7kCD)iPuRsfV7+%?n+g+x zrwz*L8u6uOpMX9JFKSL)S`~c?_OKLHLRR~i1p5i0=U=)8f)EEDi6;xirCZTXpbe}T z^JO5n*p=$mHT!5&%yZ8pT?<+2r!74Is|X%KvGeQU;Q+pdBBXHh>hZgbU))DY9fD%T8P_Vgsdy+$z6)8|?$SlYS3n8(EfgJ)YHe2!V`$`0A zSV*cgH<-uYG9g6SJXwf3^Wh9}>3Qg6&o{Gz!beeq;eH5jom!TIX0x<cjn`YirY}EVa3U&gfl6N3Q0BPx& zWi?1^B>lq2bb`|>1ey2WPZz8Eb|CC$#4=CeAXiw%pISz2^4u3k#O)IwfUWKil;kF6 zf-xSY`n81Xt-?4&kb?94H3uk?TLx=jOMv^}9DFYi9qA-gHE9 zNCR;*;rdv}-|xUS*xWcdER+X!V&{gN=4}Q9Zl>FYK0Cj1M8Cp(Jh!Twlll5M?)4eN z<3mi*e|F=oJs%k3yM3pMa!1`>>!9rz*KdDvj%xiVkX(z*gvG7Fyw;fzTT#xYOvqjlNzt`Ls$2TxiYM_SPmXXQa?4 zncRN(`#8R}v?&0O(&`Mg**6r>dtv(S$+lfRyyg5>1J5!Yr__bs{ayYcolg1b>A=(7|y(Wor6^tBGo?ZW@@G9_O>iVoF*`AXc;KG3! zY>lPXrFYXO(grl#=o;SrEjy0(Q`{Q399L6XWgbMyO?Ii|2MZLJhuYY!{7ln#*VTwA z$KR42Ha9sj_Z$`YYoWSj@|R+Dw~R+6#GM)HXvevHMKjNAvTcrDDxc{a-h*YJt$P;* zzBXnRVjmkreS=*ELnG7rwkeYd4Mjs@W^u8V+6kYCUZ`1PvBgiZexdopd{l)A+&QK= zeRL?g%s!w%zkIZbInJ~GUNO!y*Jq%uYUTt*wl78WT*C?{{eshpYcv6RPtVtJ`+Li3 zJJqnf1{I2?gvM0%Y^Ns>Y@O0Z>ABSUVdT1I1jtj(4I9-@+E+;{Z+*~GH z`V=(UyuS16m_@JD{shixqA4(lrYp=2VJzL44%zx?Q9baMT6p^)Tw*}|d^VO&h>bCBLW+-%f_^Hc#`XB9OA z%03({1ru0uh;H`RgZ!?@QJz5rH?+FgVF1dP655vgRW^BXS84h)Tv?=XewQ4iBg?UK zhhciseGvFc?6}q7a)poG`aK}K0QWfSqOSv&O&hU~M%SxY?beyRXB%C4rNnxg`)G*t z)MWs*(cjQjEq~@~!$PrJh?YUQMa5`iVcMtVIMLIFgyBCyL&QInVT@di4;j@~jJtf9svh?+CViFCA-k6G;XcB*<#VEyDz4H@*9U zyyR~Ugvj)Y<=QFK`bM6Goi3-J_=lRiA%h3}SLmzcDCK2t%>&RK1SOBxfJ*TSScgly zrFp1>r{&90l*ob9qyVO;4Pq_ktF~}p=2RKdw}ps5E``jl(BQT_m5~4u|2fpd2|y4P zn>U7v0%gyoLiK{kwF#p%_fa0|V0wIsR}&u;NmJ}E>Gq96#Sbe6P6c531Stip)5Alj z&LgoLD`M*-cbii(3T3rbtFI#))(v11IUv_oCxO@hPRic=;;SB|4(dIu5Q455xzBmykmnBaa8yMP@h2G! zDgxw|9Jn^oH~L*C0|s2li^6MRJ*zgppxW&I6}Asd!h%%9@rA*$0)==5Zsj{)xVy=9 z_!eSVcTftu{ua2UFTZ?Gz;S5gvH?@FTe6Tb;E)*pEd+Z*GuL9rlw z?G#*9gU7UBa`c*dhBcT9t*4U>^kcm{%v>Vaw;UZ#@lt$?=N5&?qcN&PSA8_UzeXF+Yephw4*L+86U)`60Vq%`5 zLEIycif6Cw8gn+MlkOk-u|ub%qsdSo)|%0+jV+5Dyz{G9U%dx_TeEE*DBZSG*}s`V ziM)cmp1{z4n6KkdXj76%9#D(R9L_DGl$iN5v>#1)6c)@E1ZRnf8|rck+zrhGYQ}sh zlqhDvBr9Z<@) zEdzJn`a;x06lOBFzZW*HN)WP^T;1bx$S#dW?c51ITC*e{`Me*oy#;*sVGgPy=#_>- zL%)ewosKDL!p?yd0$-hg~7(7S9dYDFKoQOe#5zUAL&5aw;j9rrz;yj3lhVtiXsGr zcC^yKmmT|gCR!I>MV&}sr$1T^$lJPt+B4CxG$nSvx3@0p--F^q_L`HH9@DsAZ`m(Q zKyNJqKAG#kks7D26=(QnXA zYs=W7f{M|RL+p#`7aSx9pn#O8o4^B`2KwGK2Hd{`Y0&dt^pSv78$vNJHgyN#Rw-mn zV-6?^=@9I`jm@R<_~;``@6``Y{P20^@rdN~6-;6s;p3Em+L}9HM{IWIiVSIc;>)g} zo8hxV37aXsqB)+UN+L7n|J!YCFU_y3>`&N7GN!5QVL3%qkxu1B`F>>tK&a$uhC&Yn=g>DWqcP3lyR7-(f&xxE- z<=Q@3whdS`N_42auP9Mb%vx>6BmP<%v1*?iJ3Ko!L@p6Bnb`Z@YqhOVa@nf5>|##a z*9Yt3Ak6q!r4~v(I&_BOqY+T^IAzHD?O?f?5dXSTGdgsZFC-)SLo0Oygj)Hl+BKsn z5Q`R6FVmCI>k?_v<2-bR!v8JDzMQ{1!Uc3nr3m7SJA+b{6&|ujdisiz{B4x#K}GHx zSPBIZT%I;2>Jhk;4Q8NZ#KNF7H>EEW%9YliyDLc>2Kx^0|4Q+n2dQW9VyA?=#%bY( zjQPQ#_*Sw52XZ#KtlMP|b%)Yog3n!Cfpvn4)z|)+5B9l*wfLKp~^LKA> zm=f5W(kT-S+8Yj7H5J$n%E;}m8`CP8BZ%tQDzS!FKYp}qcGsX;X=!FD)Vg+s`^c`m z0Q_gy(Uj5Uu4?S&h_^0*$EKMsse$2hBaVl?-WsySa#v@kflXNh3=c!M;{zL=7|x5X z7ENT^tl9Bm3<2`KBS z#TjmZ^_;79pR4gW_l4}XEJDdVyHylpMQhzy}lx&ABmDxPrt*VoI6Y-$6A*y|@8 zY#Gebhgiu4ZR}a|pq3-KtY710`REbH8aMN0j7V9_7r|Ex6JFs`pzcdk^KOOUbFk^4 zM|4r~i^0b`;&vgx*S^yI<9xQ=5Q;~Jo^DlAme4g5=wvBNDZ?H4i-&C2k2o#_GKT7P z^_2|kc9SAG$_sn8#;3^R$RB~2kZ}WBkd|9?RO$KI$s?dk{Xa* zOphr}7+CT3dON$PZSvWo{eZnG!+PYUlPyMCu*@Jcn4HVQ?~zpnJ+YhkJk z@3-fk*s7o+HY?kF+5k>{wDHQR0`i>0MP~STY1Lq~LR+ZFPaE2U4cYt5VqFcA+kV;i zUw7;`WA_`^x*7%bch$M}#tV!(EdEXv^afZM23d4(bN#!!?OHt8uzjCVdz+CbT zE4FQ-rf?D#YE7P3dzz!}x|?+)VW&&7&pnOGn~JaJ=Q`y8mc1KlW4R7~p1>9fuVp?7 z%Nbp=#M@-~TxflPmh&|$?uL7DSiJE+V^wa4@yfI-_s?L(V+1;S_ORCD7M57x}Itx*0aBhZ2{C!BoL0-B- z!o{jeP|J4k_57F;(sbw{lbZD0d*E9EwB#7uYb8V^87a*0+BhpO(tuDU1~v%{>j-ff zYtLKz?2m~vtx_`jHP~cY^>ApZ0?~Q9Of1`Bl5bk}&c3vyvidD+PpLh|vDLUjqc-4T z67^gD{M<9!i}z*ady6M_njGALxaVp-weMCu_;#490UNx2TkW4ur}i23y*CKzr%?Yf z{`LM-fQ7!(Mtz;jx4r-Qx~*S7;~&be_qX-HhEo=Q|B+EDk@=;ftC|D0zK~x)jSN$x zSh{}JyN6t{8r>Ry!m!?xgTtRM@61|ZCr@lxO(6O*-T_U^_uadoQm8LKENcQSqJaTG z`Ii*U;#GS1-2d2q@;Gn9ap~^Vw==d+r>#>6{>At-f5TiEVMzU2qz1I1SO%LPkl;_toh=%r|bN?%-DOx!ju~u=rWFqTc zQceDeJ~PbH_?`ku_1o`Et)I-&)t6QYJ|-X)YvLQrE`jXEnB{Ds6*~bzS+<<$H#z#* z-P~IG!TFt@ zp!>5$8S+<{3HH(r~| z=(zH-^ORm(NJNJiwNuG~e94hKFn%!GK3TKj_(4?aOwz}yWlM+`0+Y$i&R@nYuS`~U zJw0bVn;{;tG-lYR50oa(+{9-a_}#IKz2Im+V3qtWvW-k_A#W1D4taCozvW94_s%wY zU_Q(tB8r%Z0_+E<*F4v8@5_;roI~>~%+?Me)_ZSq|9;#Js2W@pxS$bWYx(DrssB`q z{d>nQG?l0ZUWqFzGgZBqJF#wsFnd`Ga0r;tc1E%nsm670})|1VOnd|)g zD14axL1UGt>y%C4gH+`*ORfw<4KpV8KV0Bj7K89v{N4T_0{i0h(~eU|B9HHDFm9%) zuHRK9NC96s1pNuUc1Aacj!vL?TjU~LzL(@HTerVA6M=pA2IUf-`yL(i%ycrN^~dk% zA-9~*o9x%`a3`|^q^R`7sX z)vz4$seWBK0sbyz8Lwcdi~YfOQp7;E z*XmxBt)rC)ysl?5V8;~^7x^ag^Hu>d2fH|CO|Bb5(&t@LOWqO`qv@AjWVz3#?5watbY0;98&Vx7EMZ5I81Yi4j5c(0CY;2$H&;+(t-M=w{VR8-J{Li&t zqSAFER-PCtApGKGf3{Qak=R+1Fm7}MOU93qEA|oDa~avztbxLf+5d(Pyc-;GxaIz~ z9lY?GuDlE~GR(HM%%*Z7R5YLJ3!&=p%`CI$Sb_!QfGzW;7w4>{W&aYGfx0iaaj`#a z&w!(tuGM$PVLX5uOZ+cR?1(;QXUWL0f*5gaH;JJ?t+Kx<|F#Xvvuf_&5TQhMVW{cI za2%P5+i%;i0{e_?Y64^C4c*d;kqaAG#X+9#W&gFB_jtUyuiULse>Cgcu1#KVuBZhT zb-_wkwBeuSo>+0_ll&cj*cLfcK3|(KBZopPmr|9MG!H#zVcb*Re6Of)3Gsch-u`HF zXz|L0cQzN1^J$OuKrPal`#7`_fY511_MBrg|J}Ua{@|z9p?{;0S+qOki{vmx%($QM z!OVbBH8rk;Gz7t)5BY6E18|Ia*HtP2!Pdtr2%l1h8lBT zzaQhi#hwdL_rG!}+wi8BOe zGv~aY?U_;0_aC3K4KHokCN^z!+?PiVx>32*FT5M9=kh`=rsa);J7X+>0< z`r)GO(5iv5R)dE&`o?X+?KJ35IiX`Xs}q0tD^@wsxQ4u5U(j#ina zniA!7MkkXF{gEx&7BLpx6x;QV+Y}-C1a7hcacDw~%?);j>!NgQYaP=o zx6Fq5Hm;a~re_ZXB_A<5dE&ThGd^H~JZM@`8|Wb-wR;VdVi7+;t$~V?brrWRO7p2C z=#_em#qm{eo+)o$Lq&UmM_2iED9i-wgGmjK-$ z>Vb_n=S2OJCOV??(On;g@ViFBVfi{iIad%m>7sytpC5oOxW9;5@xNolul~%OkMZry zd{L6}f=vf2=crmvBC`8oUVa_OH*Y4%x$47WSV`nfzhHxpZzyCw!Whek;5^pI!fpSe zgC?|*&!h5e94GfEyV=o&8=BU=9k;N+>-s}5TT5!NUxK`)g_esb98+ox5U552ZOZmM z3Q7xLAXqr|6A&6rL;M>gDb7fV3mqD9v$~X)Z-ZFC9dptqB}}4xR_Ad|PHx79{M>Ew zep1uyzy@x;dIfn2=fEt8IW9qSrWD00`Cx1Ur6j3xLDjb>m(oy?Z(a*ljU4GG_R;ew z6Vclbjl%XSdVhAg#L&S=C1OYi9F@%1bP?Y-v5o>c+Zzy#Ck7N1i%3m!147aRRryLT zsrF~Y-xz^WTj+%w5`0B_SPaK*a+fVtfZxMr;7ZAPF5e}%Cr$!&nykwwVnL}jAs4sJ zpXmlQz^!9>10-J&B4`#J-plfEsQiRB=Fri_nYkJ>SeX7YA0e4_#k*Xwi$WOS0(!ux zK^DB%(@5#0e{(2}6)ED(N}l?~NsJFLRtf#<4C2;&=JM-`s6!p-HhUNT(x6CC_&uTm_R zuIvj8SZbD=Cp_UJ_mCk3y;-TUAXpH@612e6A=})^6y4gua2O4XVS`lB%yk;jW0GB& z9OtTZ(B0-I`9x3_6YC6WCt&zRDR_p;u@06^w4f-^(r5SSRr)5DZHfCYIVt?Dhf@S7 zdxw1^Njfs1(M``cwQR)2iQIMjsJSi~5S3u=(DB7iE#H+W)vg{`Zp(a;Vc%zjCKIYs zRgdDx`=Jt+hgpI&7mr}{gtKK1@oCTy!)(&przrZ_dKFhg`d$ks@Wak4xeZqYI4@qd z+43QnGY^a?nf1UA%27naXs!McnO!GH=$EyiRNR)MJVVWM5bG0-p6s;C;G^P1{fNO? zxD7E%;LNO6VFOCfm{)`b84xW`jtpNa%G)~vf+;Rh0pWQRtc@7|Q%%BS zfAfNVhE$@+Tc---`&qf21IF!=?Rsc^bq)tiSlf%BvgaS>wq)zJOQBbq3z5iQ<^+z_ z1yJET_?MWraQbYy*nIo9^k}w>H{}=^7y;JMbc)p+;|OE1^N38qL%Hd7&c>`LjOGN~O7O&W!wVtl z{<2_##@}Q&Ayqb77HpJ$f+5;v2{~K9rev*Bv&Buw$#YN*$plP2%k_2}dDt>v4n6Z@ zL4EF94*Ar4ZEU8#lFEr;U2HNRKQ0?cZgjsh*->C1mLrW>*;vQ$v zeamx{C#GML4ymcwn>S9ZAH9_+`JdY9GoD7~-{vtU=X|o4^{Zj?PtF;v$$Z~Rz}O~^ z5~nA-{;;>nw=~7JT1-1zu{p@FM-C~!MMp=Iv)658NWWE?!oQTu^p_zcCT`)|CBLb> zdQ2?q-~}mipY0R~jq=?N^}Lo%#NJkHvk+HY9^yd794jo6{9JS+qjpHAqu6 z1!1z*DL6R|xkx5hMrm>RwFw}iU|y5S$8tgF?ejy(Qq9_V^%-gGYZ|d|#Qqco{}cinxg zsq!ob!=fnMtXk$20tg=~5-jD#)Bb|@^8;YNq&!r=-+xY>zd0$$=o0kP{lAF_fi#J% za4{c3KHla^fs(;lQgS~>3I6c#S92pdm_RiFdqru6(LsTHP^dmWlA;wJWx6TWEr1TP z6XjRbYgW#ae)<$P^R=t^sOcf|M6@=qUaN{vYNA+F(zSlQBGp0&g86>Zu;zK$$Pr7U zd>NsEqV<)b-Ay4{gRlyFBhfRy7Ccs+VbDutL;FDs<<(PV_r z0Yp=f5m^0Wvw=2EX1jl0GizRJEq!wq-*zKE`QtiRyLn#C*N!LvY4M^o)!sAGK^EKW zh+QC!hN#mlihVYJoA;xQ=VLZ^CsyS|Y1Ks`UrRMe&4h(k!$XsCNGS2IazdLO5$3k} zQ|hYb;`S7yxaUkD>$*+r(`Y9Bxhm_Oaf1wkC ztcUnF9{lH&{%hp2!8Jd_XJx+ZRRZT!<^= zlk$@-x)-fI8Z6TMM-~Sk%5@KcW(&v&zSAVn{^RFH!xoTMCBz!4s`aPqP8Gy@ zO_}hn(JFD?s;bEHmHo~WbnT{jXKyfpgeQvXHOH_p=aYeHvbY?}Q2S{sCdFb!AcueS zbH0X`NB1@(h)oG{(L!9eK&@1npirJB&@Ea5X|<1tiB}1~TL}g^2S|plh5oL_vG=zr zeMdvJU`ZpjxbN@m@f^?k*z@jFkKB7!JQAc^os+(9+qmtvWoA$L)|)GhpRXSFDAe77 zL|ev=E?a(2r=V-kgN}1>n(OnJb~22nfCK>*DF@_~uyX_osL78N|Fb6F+iIIZv9M}B zBvrbfH0=sAc%q8#m$=`NPqFP|!>2G|UG7mQ^&GR|^5#M3 z10cn12AX*qjv#ap4Kh~^mCBW(jQ~2>v#bnaD}dNaTg2Xs!!%{MfV?e3;0E*Xtc=<6 zeacZ7>(i}J=3>L9etT%lw3o47=Y?>(jqnSAngkTUL)uGOI#SO&ZkJaL>Jko;Zcx-O z0H_8Pvr`mR5(uAOj~nNyxhxurKkf8tK#u?lz+Gg{SH2+EI^;^k@>Or8lP+L!r{=kD z*D<&ij^OJ8OlSq=m1!(p~;v zND7x9)f^?~o?z{P8|71?E9iufd8^EEE9aA0Hs?z*HFe7cCwDZI7MI(uCT7HI(QRW5 zn?Wuw{ayC_&%|-@VCS{^AVa0%Ck^e78yYr^w>;i<*e&9*UCn{mqo?oOL(Hq*x<2v_ zZ{j5&E|>4r0MMEs_GU@5sT3xbDp_+vmLD5H$)Jy$Cfp=2y^D`@AK%ih0tFdhMzx(j_{Li8c%1BR|p*#ZreFK&6?6 zipL_;$w-#WR3Z5OEYVs^B1P0AFCmc~WCiMiu9Bg0ddrufWG}SD9UJ zU@uB*+W~o&L&fuk{ixuy=anp_QsSxl^wuc(%&4!`sG-ODDdJ*#C+=BS$;v<8uKy65 zuARZ1g>R&NPo%Syd@nd`_YhQ{QKzTYxOf`oiK>+=|JP95JO1v3b{cK>_QK2~yxyYN znuCTB`(uxs4)t!S+1X-<{X?^y`=q<;0^!zSUuz!bI~g4>fovv&HOVkBV4%i3ngOf( z(sb+nojR2vz7C=R$uvy!eEYT681ty@!*aLMDXlu0 zy0=`do1;`o!EvNE)x*j;^@|Es$RLdfFJ;ZI^APcTQbnp(CBMeB9w8@_F097C&PABg zaj;gvWYKQarS5h6Xs_FjCCkdkE}O5})y-uj{eHC_$-4NyF`Z;+y>m-d%F&?2@Eupb zj)x}Dc_8iYY?|Zs>!+iGwDSx<3gQDiw>;Ll7Wa6BxTu@rdp4xzpG$dlj!GG|6O#@?;tO^3%jPaoa&pox`queXVjk=Ezj zZxCCEp@|^@@(>UCGrg%lVD7DKZ$IlGmg&QGH{tE54F1lH7?=X8Ifc`#mEnF$wR?WQ zlgo6O%80l>8u|d)KAsQ_S8ISQK7h5XbJ(B%@zmC3D_t7&PlU0#q$E0_fyZU<6m>+Y zW1^6e3`8nLJyMSE5@`3+wa2B1tSn^ZJhx6p@SR6A$VcviG$NJp#*2RQ0P03PzBxjx zR*KGwS}EmWvnV=nP*pBXwS}Tn3Sf{E^le|1bmMm({x@ZMie`WQ&Y>&YawjV$yL`)V z+h4oVPf?euRn$;l!vDb&ZHUk z?%(<)4ftZQ=Sv#r@txT1FW1p3UC^oCn$84OL;ZfFBSo@nw!5CQ ztm$?!Qmc7r7q4|eza{IKdEhX?Cgtpn7F?C3)+P>nJmU1Vey{6^3U=XWx5pS(|AAz& zBgVDRHsUa1(mWUa|Ed1W*T}H;gv<3k`7o8IsO1OIy&|)l2T`V1E(DzPIQi}b+q%*1 zFMY=MgUM47jDmHe9c>!BI?s+y{V{a>*K(@Pd$jb}`;*BpBYKX^3yxIIPDb3U)cpJ0 zEH&s_%FRnc^wgW@4Wjag(>mW$-$?en()2kw)-q9_w&nc0lVb|#^>PU4NBuy4xsFr% z`xp2AzW&ckX*P<0S=@vr4VZLVHKE_hJ-mx; z8~5nG3F?F{03b6hL*HWQ473Wc`4>wD@D9BPW3e)*I#)_p(l3^f)i%woLh(UdI$TSU z8t~BNM6mCW4`nRRlgWaSd`dRe7EwT{%U8}nIO~6>=pdV|_-8N_(^$m@>6uz(`U zI?e!&Z(kqg454i@02=6H=$kzU%`4AqQgy9fdExfO8o*EzR-lSQ5U+nsM^d>us>?BK zP{K(Tqui7$S*}>>o}f=&_NYnuJ(G=4b+mb0{*`;~w`f<&!=-=$T=Nk*Tc|$Iw#(6a zkeTk~cwj>0i91M6hoa7zLh^_=zvlP`1_vKX3Z&hHv2DB@uGNHkPhYqj&H7XsbjjyT zSfyXW;kfBtQS*XRL5T-Xp4ydk3?Cd3>pavRFp8L|ZcDm&u#&@E>YoiR;`>yyTs~=3 zviB;#C&Vr19!d=wH;%Imy=m`GVtLZrLj9Yj|9fiD3zTPLoDh75LglIXydS-B6Ewqq z-TrZH>Q>t0)gj`0_~77ZXy55ODyaV5|Mr;7Xu4@XSkppe}Pq}SWUbUr6`kWMMqJC&f6XRyF;&@c8?TPQTDTRxbt z*Pl-z+6UARKHV}Xe^mxn`~N90f^`M}23K=b?O3ZZC#FGGjKKYPj4$=M_>(Kfd3oN) z_L^@aMCTUtm6vi|M}RBNXq8&B(TJJmWjn>X%2eK{^Tvxx{RKcY`&r;2l*=MT#VlBo zn(HtMWS3~U`P!A}*|RXJud%9JjB0Iy8DJ%)75oKpad)QA=dLJ#d3I zPKtXL!q+DVe!}+BgrDVKp;eKq!s0klgS~X~SVTpX*2A9HdwMzjQmzD&6PN6aI~tW* z`bwvCv?FHIcvR}20me#3-j=@rKVmyw`dK4}^xCU|&}G1*nnoiPor)F9SApMaWYE_3 z({shmd`&JH#_^@2Q#-OvT;>yFr7TjETQ^+}Ql+>~k~M5RFWI=_PEh>|+Tr>Hl?Hc` zK7^2vW1u&u2+p!;Wf(H==hO!;D78W}OVV`xgDEgmzD_@UBoCczXh66@h3RAbmG_7R zq_nglle+q%V8iH>1#F0ZJilCRWL|BS26qrmA{;2Lgc}J%8?bz29(h6BBp$X@E6?}N z6nM`99{NW*lu{8fm>jm7e#K*iyCoQReGO_>H2-sMM>9IZ2tb~)mrhe z6TV=??rHI65nokKEpH5K4|w^fAx^VC+1Ttz>xPTJ0?Z{q{O(q-dy00{>YUW~RK*ze zYD+r)ScQ*u2Uukd?%jW`!<_B%f{V*ppKzs z6hRxjpVn+sjL(a9oT^VxAP1e>j8{@UF+Z}7o>V$fZ)sy;=Xdu-@8F6d2g#R!VNJ7q z4VD}*+F+kw+nQBa52DxBCUIe)F^ z&bHwe*H{U-^iT&x(Ln>wFpz@Ap7L~+@4+|-2AWA#RRG>V-Mv`$cQBy_{Q8WC;jr~t zx=PAO6)CO-Y|f(K6EzyOJcBmGi}D9whGmmxo%Fezw9q!bGW?VnZX;s4ewc5_cwIk? zi~A4iI38s)opio^&Sb7W%OiMiC?S|QAXffvv`ZWP@@Tbd`?{H^&!@FGWD$HTqoDcB z>#@QF5snbkdM>h;d9VA{(E8SU$**@ayp}`9Dk`UU##hc<{fpp=`Xd*9FSdXC$`HgQ4Xp%pN}{)4jmm?$?gR>>pcLz2!r ztWF?Qf8g~b*7P0e$?UILdsGE7eom%hEdl*>V^NnC49#I~(&BoN~L;%XD>R^s^ zHZGKTBH?8G1Weoe|0p`ouqL*zi)SW<009yRB=m$Ds-Xu#O@Yuc^s0s`AbJC$qN1h| zLQ{|;iX{{^C~81dP}GE?NKsH!Q1k{bA}ZcL){B)l?}vG2KA)L6XFq$d^;^}54VklL zNiYVk?E8gU@dRets5;|Wo#^b?XZ=l*KE0cy4M|!qMJqjeZqP;Z z_PIevyVD{2K-k}rPd@3#7B@Hw+GHK}HJx8Xho*Th>o5Pzcm86;bicBPBOEKoZ#6$n zigwx5Gk48lgEv<2M^i&4KJ73*v;FV=DQs3?M@^0)Vqg3Gk=YBWq!_~flwo{Q%`!;dK3E9UtQn6u{-Od@xhK^Z z5bm};0g#HTj%v(t38qyJcN&r-cM=ATNi@3Ar>e=*_cMKzigz3X%t@`LE9bW>G~D-q z2lsOXSQ1YS?b`BeKWkchR0+YTV2Ela=fx80Co7@<9XD9HvgU{)Dn3jZvL()L zBICPFg7XG>_i0O0+G$QB@qIL=#c&T`x~9xW-hI8_ zE3|Q15Dh1l={m(8cwcmF+3Ngj+i{p?`4hivjql{o_wv1y+`l!hx#0D}urA5p{%6aR zl!Kl1N%-!*zf&*$Ui?pQ_uHq*FGX>%pRROibm6amoq|K3#!-hW&UknihdF0GqMv%n z{HMRC1C=fRXv9E?%moKl&86{{)MAPe301DLjO;)Iw<#AOjjLu#g%?&?)vBUB*%|Rz zca9zbL)Nii6O70O-fjg!t_hoAI}ATrY@5$qMJEahh$-V1D{)U%H)4*>PG>aKnGD^! zy}*u5u%jkue}e6P3}{t>7_`5S9PWk&3O`%gp%J}R@U>_JmaGGZ6s|c73a;T=`*T_? z_1_Y$eLJ{4;O-g8hUZt3Z)fbj9Gfm>AuJ}pKunF`|e)l zGOF>noAoy7J*BRaK3fK^@w6CQDpe?E>%d}8;XgX zIK1xd6y4L+Tv!g7dt`e0;rml^pc>#T( zLjk+o0ChZ5uKCF17tX}wZuH7^pyHYsDxNkjJDPSC8RI|LaH!41;#%4|8>F>BjTGf% z;D>w0puZ=&!KfgcTlqLkZSN7#XbH-eZRfURB*G3>cmiLW0IMp{iVB*N0ecR>h-3Q7 zplDs9!xVAVVPZ0y>C0xWBk!^!gQgJBP6B#K2<_g$L>ur;u~SQznTf~}OjczK0+j_s zZ#}|y8vHCvGi3?dxRifZzwxsKw6%6<@iHhcOt(o8iQ`LOr<5u(_DD*zMLb+(1YVJE zUwoPS0#}z;&Cxzh{b04$@loPS<~k!0^~~(L#2~Y;nMZtfANg^Hsxg^!0u$-SWDUJ4 z3hQtP%5GZum?|>8v+uueYu+{+TG-P^MJeUo`^V1M|8LUh(sCK(Gl$70-<*I(=$lp_ zZE&`!(0fzfzib?7Wa@5U5|(HZH*9?5UK%W(yNi(i<+20sqrss!+|zJ~IvE;8@DHC$ zr}P6mAqYKm@t3z|%B&z53z9emSV>f05Qvij_7Ko&+_UyIXe)t*QsEz_GIsCw{>5Zs0)|Gr?lJU80Fd--M4L$<(IAp)(*ePeU3AFUEPeGEOEMw zwZ5`#d&FOQU-8O-ZDmap)|jVRYgZNwa-EkE#wE7qx^g1uzo-0b2@S?NG-n za`F}je(4_8*j>5M&YVPj|K6EEi^$C0Sd57r{ICA*>g%mZ*U#*|E-e?cr}h3;TNl_- zs~61nZxx$Q9)?}n_LX6DVM0@urx{9x!djsI><$W~bMR+}VOsC^Hzrg`FYX9~!E(7j zVr=Ep>E-Uq<8WU^)Zb^aP0yV1eFd(Q9Ac9L_6ER3Y`77+xNuL&TH(Lub9hp5RX|Tt-Suj< z-6gi->Sb|WkJ#zGxI!!{6^j_j*F$oz`yan9z+Yk|qYgQUM`CUecWYm?}_W2!PQ66B&e| z2jIAgdPNWj88l=;_B*I%9aXtoh}magl&DxI8BwGJ>94Mta-ciOnmZ-P)++TVr8)IH z_*;Yey6MJvsFC>>+C$&5R?b)(XKZECaqRK&v5Dhj&y&ZV`QCqoy8mo(>~+Q1Wb*y% zH^$zh#ui7%J}i!XS-k(X;{J@u{kJA#KTRHd4}I`SMcza`_|<#=WA1~MqVdl&50*#A zwJXPzp-0|=hR>p~7XmeV8>4(OjP;H0h z$R|$>0xoiRBTKE7qtQr4tRQ7)I@*F>M_goL9M04Gzdmf4Wv)MDiswy)^YS=rBUY70 z+{H#m+={iojd<2{VVa+(>5=Q?(l$MQ*0uHKbfYQ0GVED?Q%&oM8n>8f{aAac*1jI6 z=k7(4Cl>mxYc8C!?2JFKZpXtuMY+uflRi?#qmf=k{LRn5jbS7tZ5}Rb zu8E%36Sy2(-)ZT_MHEVqdm&ldRWB7zcv9WQ+^Sj$5T*-kZvkA%x1yGyZcETm8T19d zc$ekXkcK#BgXKo-)s>DJ$c1UO16*znRr4}OZRZkvS5;VrjJR*kBJ1-GANEe(d1NCS zPW?0qqDPb2$Q@+OdYM{|40zBGwRh5K)Z-@S0 zz=c%Wsw~>Rv>@k;51qb8nXO7}zO7bE4RQ{1+4yC?HtpZiWg(W{wD%MY2b)`$e(}xc zzNW8m*3#Ft#2$y`(o&7|I&v9)>D}6}qYnPl7t7oFF|qJj zFYd3_OC3>H^2l3#O?tW;HmrWXx}afF@%}N<6^1l(xbao0s2}=w*V!vT;?l`T8~+qz zAop?Xv>9YYLv9t~By^S?94J+6t=YM5d^v6emg|tvZg|y%aK3>swT+1gXTX019#4lH z+98LfaS?1TB4G=%M2Ui}{7VEav(m1umw=nQkUP=0cL*jemtc7;H4#hmEElnN35q{p zYIOU;v+MmA@{jyJw_7CreEW6MwxKUJ(<&-A-ddl<_FntQ)%glvJXUqs^7bXhAw%V+ zWH9*C>%6kr|9z%h`ok~$^6sxcVgLIRq%+_TtXzs6R56zWVd#-EEyt^)Ml_)chCkw8 z5#P@n;1C_^%TxP{hd68e>KBP^aZo!~tJvK1i>Yl{SKhsTS9d;%Ua&C4g9aQt=DFkF zNpIl_V!eb`0(-0uySrWzSn8bd?DE?vsZR;>bmHU9F=zPT*4K=ZgY~?d;@!CWm-?z5Gk%1E8V5O zISn=4)cMaHB#wkUP-f0Pk-J46@4Qx8}Nq1Hybf6v>ii~dEKLB z`aYHr)V6T~zucDnjs!C8D{xMDu>JK$D*TxcX=o7gMDuljYHFT-O`z;(S0=P^a24}* z!o4M^j%fXp+v^l7Lz?7uLMgF|`U~Cr;AU#8-8|uk+03e@>@STn!wlWdOuB~J$~jIS z>OakDiNL;AhltcEVh!rs&Q%TKrW>UVm5!a_mSo3fsfA&lpQtt(sp}AsK5B1Q7oC{j zRu|LI5Ts_gN7|;=e$FcAT#GES%)UkX%+=oL`sCY?lUeeI!DqR;dSNFG68z9K)HL*v zPn+18XX{x0*%9$Mn zdXjUEp@(RoORKi^HcF^_XnJ0z+f$}nEBaZavGdhc$>;kI(wNV~yh6#d7qn+Z&lSbS z`>FSfUqc9TZ_KtVeg)0*Xh!`SDQin4q+3(z1JT8C?oAx|@$YQh9$+YL!|QG*~AvI#%7<5lQbn9T$6VFvwI$Za#B- zDm|*rtuphSy>w4}jlF%{M*FuXhTNmxop>qye8^`UnRfKxovm3`zm|`mb?O4x%5KAx z)P4=!$mLme^FWI7(rpcva#b)qOD+rjtFBVBuLpk+^&FuPL^F);3d)U>S*acj&*`~q z@%u7_Q)BOk>f%!aG(Ejiw7$bX_xXen5pz2^J7t@_#f0K}A6Kg24GDyaS0!5D)I}eC zmeE4E(DQJX@4<&@s}x)QWE@dKy=Ina`_4u%|K_<)dNIJ~@ZCSM7n64V&{fYz@6q{B zPnw3+{KQG;Pn8@^TtAPWfj^yZO{l;UdZRi{+u=e3zDnnDmT$m#6)?R)=8+ zHTMQK4~A^))F6xwAXhIa#+%Q$yQYK*Eqk9KZ>IOztG^244Kd8eAqF+^16F#($i(i? zw%1s44Hq#DKmSSS7@tydKT+Pg^aKYu-u=r2uh7CW;Qeixagr7-Gb1k~1D&c@J|)n% zxCVVYcgIjZ@i=Q^anIvl^Patc_BL`}*```&N*;Gid*vht>aZvQwgjcZCQTbx{{Fs=jis_a^rg(fOwFWU9gp~q@#F&2m)8awattorvfTdjAw7_jQY z#E8Gv`Kr=05yU^0IvZ6f9SXn9=H*^eO3#<^0Zz6S*ty`RGu*K6TiWe-jmt&QY%{B% zdP#~!Yr0XMc<%i)E0qE6-} zHpQwEukiKdYqVS*P)Fy_UhORg*DWcu-l0Y274|5PhG;bs6#`&Ympth*Qo_}qk+^g= z87$BP8vl0J*5k1TGNnuY_LIDVvx?D+qokGgzjOc96iA@k_UJht?{uN06OQX2Y!PCt zB!D^|1+f?hv!G` z*_QJYU&igu=3OfK{id$pSj^LXSng>V^5Pv8kLgoKt!b~JDm@!e}$6w0gcFLia!F&9DxwC53kFrcr zyr`!fg~rvC0h{m??^qdzQu*_eW9TE}m#AiR=WY;3@h`@r>Hrh2^x2;&zUfkzb|=@~ z?%NU+O;+yOV#h^6x-f@b{I2>Zu3u#kX|U&Xs)4`#JwC`hm7Xh9 z;&#tJv2@LED~929!XaK{Q2dX%J|mWy&2790y+2~-9&meR39`0gg1M^xZ$?msFo66c z`I7$btU-6zJn>i4+a2@V{m~@0FHH04N_UI%MGYuX?7KKy?Aupk)t}0^+I`yTKoCIR$1OQ5N=DQd9$na|aq!zFN-Xzj$Xa|8rv#y?|Dc zgl;kFohr7^QDkgDelySu=w{r1hi2!_8Tb!JILAh`rpKqPadWjMj}{;P!-C>9Rbn3A zL`4Rx#o&Cr|DK^^3CR+e#TZi5qamtK>TSlYT3s&3JX~)Uw4i^0x#)HD=v3V0A9LW- zxZM@u=JhAwDUcO@ri;y=ubEawugL$KbNBoxPBm%P;gjw;{-`PqUTQL+Uw6nhzL41H z4O3}|+?zHvnr)rm?Yla>7`|bmH6)<)L@2OB%A;R7Q#<~VzWdqS`Q|HBj+V|wX=}D= z>D=2_G6-RrhT1O?`%6+Zq8F%YMdLiUBe!=zpL+^M;G+od5y8}CQ;k`s|ID@4$BBM; z`W1CDhYF9L3cZraDd)~4erp14OgU(`(?va0g5Wo~nyKH&OE9Yqq$G1;W@!F1BdB?n za9*);C~-mj72T6ALAd)6I-7BwuBCsnib*6@e+!0}KtD-~dy?RsVa%mR2$;s9jfJqT zMnvqpVihRqceiN%s3JNGkuDR>_lV*s!i~%LEm@-Z-EbP3!nQ|5QeY*tQlIzMd?_rX z8?l&e7D5#+$%`3MVGfTioaeB=3MBIAg#QS?ru6c*J~^>*W;vwnPI19^|DpG-m_J{dafEqS-y8K`*|2@ zgkzgny}lH0j~J-tS2;$edy?GUv~EIEgW~Cc6h=|tJ~BXyWuQCG)m&I9zq(YjF393* zRITyU%_)}F_Hwdgr%B%|FdzZ7rg&pW=!iF5mE2}86GTx+E63moQ3*x z-cmVJTlCR2oYt_BjtD5OG!=Fj^su$xx3AEDl%>t_oZ^w2=@(8xmRt>9jkB5Z=NAEa2-Y zLX+H(R23aDUKlWSG*wa@EfuaSgkAM3PaG0fNyW)X5nC<_<3PP20;z^DhK0zH3B6lY zz&e2#3yVWqrwrdHmvo4wP0(OT$Gu{;RMjSgETih=?EX`yh8tyARF7WC4{qe2sEJMnAM_HJrhzz;giH!WD8uv8@cM0@^XiTF!E4 zD5r8nE=(9&i%28Cpm;H}Rw`35>nfX#TkY+EA+GT_cb;>wKMPD%2-5@MA#%Z5Hu&n( zL71VAHpWo;8JQ{(ZH^y`U=;^72$SOxq4_%C=xv!?wf3T@LePv2;+0UM(@i}VG#v?b zkQVzE@_t!JNrmujS;ck)lJFQ0R|srW@|`OIe=gujDaNz;_(EVU1mVEq85QzemBo0H zfZcuQ5BW%Jyl^eWgGTPllom4<9#O}Hd67OhenIJBrPM1UYmvGnVGUIu7T`3pwpec| zKAMssd;sfT$0AbH568w&dhLdF1;V3!CPNpBGYD`xAzJaSHB*9MPy{v-Fmeh;j}c|d zS3);T#nAAKApJA4!ZAO~Qcfz8R$%xoeysU&b?>92R!v<=5F^zP)2`Gu)K4$mF&vW! z?9TBeNOWhrbR!*}?w0qiTZR_t_FT%juz*@)-~icN3Ne=n$SfhXfrpiW?qZ$_e+naELmz(4Xl^3nx9GctKq&2sNarvO!2~~DPW8Y^OyR+of58=qR4Dvf>Mit9?zS! zTehg*YYDaPZn;y%!>}N0kEv-j@bL|5a7jRArwlR|aH)Y0#=G#Mfs40^HP!zNr-MS;&1N1ylx%36FxZKmtpEQ}R}4fey<8mHj$>iRS`wT_K4Q#~-a7 zWB3>Ag#@DG7NY1<*ittllB-7>r~Md$ja$P-L81VHaJ@e=G;H**vS`a(oXM|Y#<;it zzM5cpapuEdeqcBLv?zxw%7!55%cHz1MqGnvHTJp<_KA%O7b3BuJNPYZH7u7G-?r>} z|GGLs92>_`M-2nSA)bnWX+aHdGaPKe^Ygyyz68N-YnqxD4zG&X)J%?x{5(lj3N8ZW zB~>Gcvej20%&sQVHG+yT2n~_KeW$`#7p{Q1c-|G{)fCM;)~OE&RrMNfCf}{<$z);G z8oS@4YjJYu3JhA|A&ip4$a2BfFY$UWD-P?oc_wP5Hp!wVq^2w$o?tHz7urtCmK<^$;Dlvh z4O*l^Es~LHltSLfGB27XP!kI%4PbJ-=;$2WL=J9)AZ)65<_m#=+{48Jz_CGyRYEt& zc`6HpwY)fn1gN%5*>1ZCP|BKo%0Y@4COgPbTB?)&I#6xm;0B10v~GvBvHi zUo=dGrw!;9tVPe?*n{~JNH9#gV0aF5iV@r3?N=i{x{=!fo9M`I?n;)1HC7!SEH5*Y zZE~XheMZ`Q$LGF%0?@Jm9I%P|W$H1Ib_a5Is~AF4fUg)>7tGC?<=ZI1P?EwQ4|SU2f{^ZZ*P$yvbh4?4 zT=EJ+^;#TvCYD4OCy9p^j~@EGbUnGd_;4OFha<8Z0{qa3Bj0%I@K6Uq2@?q+T~k$0?r)7ealU&_%(X*ol{@%z`=t3Fg!c%PHw<_Y( z{K?BzgvP0Wz|4!G~^-%W(jQ{7!8>uS5u0d}V#JG#HVkR>> zlr9N3Ip(2|@T0``{-lTlm3MCyVtF5G6zDtiY>U$0wRZ?6sU3V;RSZtdMa2tg%KwLS zmRvANP)=3vj3=Elc&60K6SjGt(>mBZp4to8yyC8n;368hI~uuIG;g|x7j6iPlnK`% zp*93y-E=;$z~#-3L#PynWg|RIDZG5=mC}qFX1W0U@+2E#e`J>F>|ZnPE{3#%g|twz(mFif`FdONuCnnDmFhyEHIby zbmbs>30SRldhhq%3%X|U!47(WPBJT|#nin+uEXOVec-e5?6R^`D8u2jH1}YT9o=6; z`W%=^xI-Lt#%*^}P9} z@4UWc;3SCyy>xh4t4#1mux7ceHs#INz>}%Unv(mwHn;Ko8;V0YqA(f%(*q?Tmd_My5?wjoacP3o*6>cBDbpAK z4OlN#tXq9s!Q$ThuBqR^S7%%31oE(AZJ!2s$O2So0SNzBbMfEnJc2`I`}xgyF}vYj zBz23M*OnwDERFMly#$YDkFvXu>@e#u9qG}DUO^}hcg~JEt{^cPnWPBYx2?C0u(ada;arcVxxY*Ko^f*;lk>egU*$)C-uREF ze-*m393kUxp&=6Gg6p#otw0NSRouJC2UedNQ`1(Zr|e#?sBE(_v1fsy-+5`%fcX^Q zEV)F3h}Q@f(N+{`xG9@Mm~T{JQUx8}?Re zy&m+1Ea0gSACP5D+~shna9xog1=UQ?-A} z;g8jtUW+mTGug3q$fgYMT4kz@@k7v2-d{#+Fy7Hf!=l3qIL_z;xCde`n5-CSn{(qAhg_p5)!wwZ#K zO4pvEt4G#fUx<7Xzl|4M>3PUw`*Y5D-m$H&m6aX_9z#|2C-(g43>Zt>p`P(fcF*H1 z%468BBGfhD${_jnsMh22tZNY4+O!up-)zk4n2;uBb?2&e?s-;R@nWacLzM6QIXf<& z_4eM?Qw|rGcZs!EV@ZD6{`dugmf?Ou+;VX1#yDBhc{o^c2=i5_x1N=Z}GQ(;Z0Fx$5U-=<;YgbZkWJ{eTAD+sbP zT<@+Z_7DEGHIuoPwL-HsYh+n!9^XlIuQo+a+(Y`E2#L0p9R5CjIzSlC(S>MKU}-P7 zwn_ohX@-5*qP`H*4_|XqO5gc;Pw0rf;<@iV^1$Z0I;TsCb-hk4=^pAfHMtg=c5U7C za@*s?$K{nfdYC6J4}4(aJJSa<_q#m`_ldrj``V$>b9ZTdxvT8LC+ybJx~JzkgOJc? z?#o`cP9>+Migt&j4X3Wku)DiCbqu;ns=7n#scy#0zdUHZ<%KfP<=(#(& zi(5LYwTfhVZc7wq`oW0xn(M9}`G6{Cl8o28RL!~?W!id-J=8gn^Ud538Mv8AvCz&} zN~HUwYN3ncdZ#m2h<1M*=}f4R;3D21F(Jv0E@I*Iqp!H!{|D`;;b*=iDc-`pyZT4) z)Ai@ro{*)X01c_aER$B2pqy1FG*Dp%vTK)v>>!yT#*mAx5R6LaD)TCwptx2pD~gHPx2&y z9Vt0rQC8|NoQ2_*C;mNFY}y>puscUF`$U=OoiC-wUMj02`C-cEHw+qd@NLdhQc$7f zOsCH5k)+N?sI4<}qjl(%-;s2P5%x&-x1AhxmjRa@+>c5<&IwM;jz1!7ndJcz|vtvs}41~;2EyP{&Ac3 zhc;ae%aV&yqU*tligNS-@n4Au-%~5*|i|Yuv?McDqGK@Yg(YY-9Av&51ofvdd}VpBl8KW`iPE5!T1CTF~y_-JE!I| zHWl|-b_-MucPh7eNbjo!-fSY91z zbyIK7o+zwrXx_8&4Kj_>xBCv6NyIA3|5d(f`ZpT4BHdoORJLSyZ7MXSA;YC_S;z3Y z;i<@hH%3{r&2dkV%AR+cw?BIdjQ)2*}f=6_6E!x8D`f@2d}83Wda52{4@L#~xu z3M@;W9)-KcHnl%%{m=6Wdj$IYuxuV3kTCmxgY;FSOI@1{$vbAOph#?X^3Evp{lxvf zqcz8Fb&Y2@er`+P;r&*qbR@Kohpv$LX)4N91T^uZZWBPfGbyY4upCm+Ie5#TPn zK|^U(wXvV_(t!_2U@{dRW}K?GwX+{c@kn{dh+gxW?4OYYfbEZ@vN*UhoQkuO&D?U>2!UsD!M}XM8rDxVCyP z*M7M8-^-d!8XLaauWA(2v>P~TGZiz5yN3EbB3A%}Zz#j)n|Y2@;!zK~`O>vvkl5v6jhNnfEp19%bV zQe=F!$oThA@zz>EhbqT()7928^yd`z?)@6=5EwWsLtcy7q-AH=(Q{q8!G5zFXny zSi-U@zR?s@27>?CcU^+#cJ57Zy$7o>rCntj#fqo6gBbk%+nuOB#$HTzI$lz`&lsxRLc&QR> z8O4J}1(3&u8kd)~y^ix;Lg^kihw~?bP2IU_lt!oHJV=$o?pO0?xyGe?!vqd!r|gTW z3ig07xLE@HFS)(4k4T_fR`p?#JljdSHI;5h%Ho;%K~uj3e8%G6Bg=;g6hmc^atcvM<|&K*e%da<7sBgpeLJd=qn&p(BuhF^6`iE z5Tw-#OsBCBv>)ZasqX9Z=(9)P^_T&iSRiUa;o8o}HE^-88B^8F%2#eH;U9oQbtnq! zaUIVpntCnJHVW$3LJXEcd(z#mDv;a@+WGdoagW=o^s(-Iv!XuR?!LPdeQu))lfu3- zJO4C!!_F_sS`(9+*~B|Tc#Vl@t-;A1(tw-je(fvc##gIyT0y7f0Q;^!hXp_nAE9fP zKm;v>s80*ngBB1}%D>H1I4XgwpTsEQDy zsy>@g#joE~7%Qx1F~;fsL>-#@%FaNoKz$JsJoiEsw^Vn~O0MmDpI!v$tSrCsMaq3S z5PdReGj}%AWq?W+&d2*PccPluB6u=Plf={E@Jy8o^JVH^-=CZv zEoVU(9w>p!(le6d5fZj<)#OoQ@+RAE9)t_n6mlWmJo6=^OQn2EtSW>q$}NyV9vC!b zLLJBo`}jVcIjX+Ox-vvHOy`>p&`g%8ddaUnBnrI&Yl|V!a6zsU-{&e-V7tKUwaJtL zg(jPqOJSJyQq5Qb9l6{!9mMyZHf`XM*-*>5RtL!G&UkRqA23*uW2LHummqxti2nuA ziwE?`G?l)&)T!21rqaguxz6wmOcX{ezO$piVP#aIA>rFC$643PO=N%-3seDnu~eEH zOyEg@8jNOa_~EZ=Zg`o{^|Kg;<8lotk8HxnG^mtlbbTyc#@y4*X0S=QD4uy0Fm>Bb6c586u){9$8zS!VP zhs?EK#drkXc*PaNHa~7hnq$vC&!)M37e)*)MZ=gfmGw0lqF$)@GNrJIP&8-Rx_WQ6 zP^W_9W3P_hJHq0nvlPMO7B8(J$RbGE_vXK|%*+Zl?mrf3LKvV$8{mcN`UOF#89B0m zTR(m_e8G&cX4svnO4BoNe8#g_5%O@qAmGB-z(LsmhDOw5o17>69791{t2ob$8mB-y z6vi{J=yMt1`<%?uM9R%MbVnf=LxzcFA34eFi*oyPk;0xucNynr<}pnC0Q+=MZ%N@<0pOz*=2=j@RN?8zGqM_S zS^&MiMBtPAOsG_gKpKw4w~J<&EOAv2Z6YY~7Zp1WLABG#zEA@d1EcEn`Ba5#!#f>a zy6FlBYVN0S;{uvPin|z|9=8wE`wo`{b+F=Lx%5S6hy8P)^$($pV19>N!C4qlpI{bI zmcsYV?scpgCXiiZMC>tC+1TC5=~0Iz+7=r_Z9?@8i?N$b{oFPhwoZyUEDJh z+W6xTbv6WO9SfIoIr^i>xBLS%s9WtnkBtOKcPepU!O)Ly>`v9g(xEE;mE|(cae)6N ztj`%cvU?5uJ`w6b;?4X4oYNILwE%Wp@p5OsW4x_dDBlbJ7eUB#6f*SXfCs7%J4ZE} z8cFMZ+V;f27}@WL>?02Vs0N;!FQ6sq!%p%$g#yzV9yuCnIn;I(F|(LsKo;^0{rYXP zXx6#V)8`aKAp?)@^OPvaxc;u_e?6Avs+0gsfkO3v8K+x$ISNo&t5m-s5liz-@7Ga9 ziR}QeNN7T2|9Opmdlqz48O=ogmwqn8+C-rt1{~TU7*#V!4j7<83JYo};lh6b))brn zkdB4e@`Q*jsJH(NamUiH$7i*Ub0Rq5yBzkdaCFKhSSC+KizZXJjxp*u1+Vo)UBtv+ z%}eTW9)gAQoSoBV-06>oSF+xb2OmZNST2~QaD;Wd`3+H9SbQ<{5OX#Gu%ZUl4!GsZ zjay20i(YDX%i#;tIWG*G$-^)%M2Fi!6Mq~w6CM-N-k-1ysi10LjhaswK^!4UG;v#7(W#GrG>CW^@8yWGHH0EgApD?x$_;O0X)RnE1hOI$=4V1EXJvI+xY)B z)0`doIQG=27O0K{tR;O`E3FW;J00NjG>drFY#y16vf%<43qf@*->6oRH%T|;0G^XH zLqF)MDw=*g{pB7Sfdkc>Q+NsiGuVuiuf1_D56*t=rpz(Sx}-PC6L}?@qv;moa-(*d z4%^B?2GO+Q+xbF?61m2x!j)9+iSAR4?*alC7DGHN3a~KY;fE9kqddHv=2Zwf3@I>0 zh2UQSq5omai?>{PGCBTQZDKZH$MFE8i+6W-q4tr~BCsRI?*a}?R^*uRxxQKtOe$jm zi<{t>xqTom>`F|MN9_Onj(k+aZlUF|N`~;R@u$S`cOgW%K9T%d+X|9|JGFgYA5iMz z?s{NX8rA3W+S8Y)D6Sk%ft>Hz;gsW|CY3iw9Gkmv67K-+xA#0Dell12CVPjxnFF!# zrlOF#@4tjW$+~JBo@3!l8y0PL1GVf6O!4GVVYlWbD%AUa7P^pX61~-`OI~`N*0dYq zeXGnex6dv#BvDc>8yM8x$Z*9vS;YhBBHlG9&!`F{F4730pw}E=&gr+|@>FGo1qF&H zE3oc;9ZaO}4(fD;M)Z!5Yq?QOIllT_;l zzL`vpk@M_e3_}yucKFto4QNOpi3bPWs`z^C5Y1eGB<7WmWLvNSO^MuSlxFEJuq~oZ zKcJ;H#H4W^+^H%-Zn#^}H5J^(>N-pekIRH7;ZbkV`_LyilXup~KbSmy#uhuxCC8Y> zM%1dOo;Wl#clo?9djClV=43z}>gV_PoBkcL(K7w(rC){+2dOrGh3C&i+|F0;qF!Wz$9cpF$V zB8^}$wHiGtMvgu1jLm)Kv0*g&on}Q9Yk(gLTlR zt2xNGDX;(?lYj0V@j77kyO7}i@Y=4GwQKg8xQmq10~>D@ef{t5`;rxN`G4GaBK6a} zx5}EYpUgSQFi5bfqJE?Hmp^{C2k9}*iW<>GyaBkXGH`0HHZ<_);6Wr34dEJW__D;n z0caKcTDO5J)Q%L-BCLyMhzKnk{9y>hfW+m2Ad<~gi=Ik>;h2HMVdM5Wl@9hDen8vz zof52Yyb#KRYA37crrIik5EQ3^n5n$&Vy-~jqT8+5mR#*$9|il~#5K^Q$oyd$oS~+4 z^oDSYRP1BxhMYo-R4*Q;3pa>Q#jFfg2a))0}XUm$71~re8$+>mctzg+<1P^ z>y8-0=@>(H?L@}W9K-~OTF-B@SG8{JY>sAgN~Pzrvx2br;DU}}*VN<*|06dp=FA)T zUASj`;F^8!War}K=D?4BZJ97Z!Lig( zmyEiYa4ke#axCq?bL_5^c$}OSky)2ffLW}0%xRm#Mej+G5+9qq8|*8I|LWAguygN+ zzQ%(sS{|wK`@<-8r9r3lOOpFfca`MUJA20!?uYA|=0m_bwc zcEKs+>6`{@li@?`DCQZ{zkWKql8CfB}_`@_Qv)5{H{_e3Q z$0kl6OUZp}b(qC8^{9^@z-`hCyRYL<$m#3x$8lF_4SNF_jsUNI}cU5L120JFw%qLvhGl_pJn>)(Lm`edRI_- z`at(!yF?%rt;hz8KZhW)8ZJxIHt3XmXGALwPxO87L3rJS9NS)BToM0zz?L0k(X1>c z&5R>5?aQC%*6SNznW`YRMU5lAOwQ)T8jh-nSH<61w_ zpQK8V@4-)d``{JvJF1e#s9MgseN~gBbAl1V9sA)=4Ges~&Xpx05%sYo;0wyIl^}HO z&MOP!S{s&A1s0r-h9NU4dNY0gPP&ifv&!bAk9&~4My{2jp*!NoK4+P)`HTJk%%unhYwBC4Kx~DZNlg2)?I?L7_tYmb*tD5WW z%R+A$)DaS3CR{b&v>PptLA${7-!bcECtA`BjR`A5YPCnL(+RLVHQ(qgFVB_!Y0)h( z2!4eG!QmMW9C{f!oqH5J)IJS9<*IYofxGPWPK35K|6P|2^P#@bK9+sPbWw>g!Yk%e zxgpvWk_M-4nkGeJ?STK>bL~AjM$ye-#?G)aY;U5`K=tg_npFpKHN`B=Mc@qY&YF-APDH&vzvLhtS(irk%D`;(T~ z8nlgrHd1uDrYyxsj_$`@tX5Nz9l4d`s^ECC8fz&dA^d{3MNk2~w)+bi|mz}$p3rQHhdMx+X zQKt%|op{|MO)IgR`PWR2k;yRwn)Jk7STCzQft9A`JN`nO#F8ec?PbD9ROq~)EoF9R zujh|s4R;9-YjQ-xm2y!tSq8O6UJKrAk#RzZ6R65=jr9~$yKBZk#I6AZq!d6W zOAGDf4?QC4RrFEpmPEH=B3)_496*c%CBF(R(Y;^Bc<%sO5Sm27q0or|N1Z&lD62)&r*6l zT27lA>aQ~i`k9-4rP?7U(xp85*ZI}g(0x0vM+sLqCsx%IubEE&-_*0X57Ve;xr9I5 zp~=93iS65wGWFLMsQ;trT)bL-{6GHQ{l2!gYO7@3?^*YY%GO=2tcz~Ky6PrVA%yq3 z->p=NKCBCpN)d{%b<Q z%#AtoMps#5u$aocvMSGW6{viMrauCc&mPAI%u3|4ytL(6m^+ll2QUj(Qt~f7M6Kc9 zaDXJh7+FIQRVeE;f{r)!&=SKX zM7(6}DhUZu)R(M=+}0fhd5hwb3ut!lAN07{`2(?n)YC{QQ>s2u{u5;3N^R$&;x-4@980n}kJ{;s-Q4WLk&z419rC+Q?C=k4g{y5T&c z+?Nhi&cdaYsz%GC`qF?#W2#f^xyCH$0pF9o)-!d5CZFZJdh=A@OywYxcyFyDV7BGX zhFTvAVtVVFG8O*}Qv$=kf#SAg5o*tY@9;i{urbFMnMx^S+OOA2Pcm7_CrUm8$G!_iagYdmvm%ZVK7m>|?9224v6HZZNBTPgO z(C){+d|WM<*Zbg}eOFu6Gg?A3I;;K(8X%W0lCP|l zJIo-hu#kI7sR2tm=@NX67!%8)PkkZnVbS#F>0%ziWL`8oA%l$JN5v}ZA{>j2|H2~n zigjGRhnzgd(?%;l`{39*JiJe>qM3l$^B5&sROvm)ZaeTI zDGMTlE`~0uy5?)Vo9D+avSOp$7H+yxK_87PbBCpzyT(TrOd)nG4`nITg0)f5;88vV z^AjNsiw~_YQbRwW6h+UM6Vz4;?GC9FOz>Ae+ffU@U?x)AEq0w9R{D zs;gcAQ z)v!1dRa4;qne#AswV*%~K`5q#zy5ME=VVoVYvO>l7MYk-FfP#) zmALAVH|^lFKao#Hl}}s0uHW_roq)}(+jr*}o(_*!`}i2oo$_eqIgSm*(e zTExd=!zo!J><2LxD!|yYRsO{EO@Oo+F>c+2a)W`D?buCfmH+S(4=fPtc=RM02U83$ z0OgV)#6TGeQjDonmpjJ8H?y$g7IcH0ha7aFvc8B!i2m&}rkFiSLWtC^=flZc0u!?x zDXSk!{LgBNEX?Bgy@mYs=-@+EKBm4tFzUYrv3uB4N#@qBEyV>Q z>z6QL$+w^&_Q%fJcMcqR7nykOkeZqci4i5Y#K1R)Z}fP=qV=AsH<6qF*mFp>~F4EhI?sMfJYW*whFo+st#a;!mY2cwj@yQ_& zFHC?Z?(4ek1xvn?UNORQ2T^v9_}wjY5n^n!h)5N<;AQ#{9;C8iU6u4Oy)F|fH*O%) zH`e;iiOC-Ticeo4FHm2K-?ETE!`4zPB;+EAwnKsQq_)B*;8c7#1u9W4O2U`%6$iwm z_}YDs7;*;lOA{cqBb>6rz~kq82E^|?!pV(nl}0wXF%2)QmHR2OOcqePMZ{&1d~l+i zGFz!ETrp5e>4c~xcgmp8q_TO%PvQ;SC$U1tyh5iHY0XeWbs<=`Z4K7W*3vkxD0`Pj zoT>fEe`S*CrJohg`aWF_jz_jatFOO}Usc^csrPu&C#BV~tEKYImn>Lb%%K>uM=2}v zWU8Cpo?6ASdWEq0=~Qu>)BhCvk-@poZe%?>`?xnalkONlCe(|v)ES;^&0TNUhjlX| zSwZO3aBQO(n-0Rztn_K$OuHLm=01n!ngQB|VBM)1@_5OY%O9+-<;|M$=b0N=<+ zyHLnk-to^{@Od7j=;tJqVgf3m*bv+jy#V7-Zxo{Sz{15l+SOqzjj5GUEV z{@|`iUpsUNqqp!zY}9gE^U+A-W12+~nm;@>^8<^NkvoppEM2?*!ID^UeIcjWt6dN!^tHBu%%J=q@y9}L{#ddH@7(+c zc67h@(hXKU&}D%>x~_2*)(m2(eu~^IteAD!2IStRJ3cY}vv!MHm?Jm9S?-j`qp$5^L+Cp27HM@6b!VxO`Su~TPLqhAyh!NJ|{EJz+P78&!9NgR`GdbaS>3G5fXg% z$%Z|$;YOKdN8uCeJ3rW!<3h&~<$&InfUOf?u`*kVTDHbUn;<*I`)sAEce)(kkoOM# z5B8McZT26Gn9O0gW2U5?Cl>#zK93tOeyPI?-xbvz+)(+V0%FF4ExR3w5TnE*l)ntd z>qkbaQ}FY|MsXrk%B^WU##Cxm5A#!-Gg;79N)3;C&FMdfR1ecjQ1=O-TRv^Jvl_FraJH5+wD;Km_|)0< zoTEy%DzxXYj@&=?a0$DtUG9aBrQ79#y4RtZ|6|d3@N^L}LUbP~K_!WZD^lbV{1{)w z@#@%1A9v3w7x>RZ71S#n+9UUsv)FVk!g|o>eYu>HKG%+7zzu}UgHF0qXjPwUw;`@a z^v?wb@`IQZB_WRfl3NmI$iY<&Z4~;TSGzyR4w|Jg<6ncZ!<|+!4A1B?U z8;#N%%;a<)&`s*DfH8>AAW3Vd;jtxphU9y`!8eD!RwT4al*DMvY=fMxPOcOEPdbY! z5&duKhE@S&_}x2M1Rchx?@dtG&04y0rB+EzU7_SZdJlw5QlE2>nV?z3Ca6Lih*)G9 zH-rn^Oy~{a54W+Gfdy(SFRu-5m{UY+kLWM+)pRwWJ7-NhwhyO{oTV%7O0WHpjaRvB z5bURraQUMaGtB6q`uQrup!Tr7WcA}A(>h}jmZi^KJVq~#Ua;T&l%tqhJ9_adNAcVC z)R^{w>wAuRz8T)xy4fyE%KUN*UD6&kQh6!dswGs_8Lax>{9)r4w^E;sU)tKx`uF|x zk+=WD*)XNfg^C#YRCn*HXm;taOHRVPi}6>3Af!wa>>SS4N``aAn~Pj)Q&-h#%;8=eK~F0A1zx2gB{0G@`QkF20Nq4&okA4 zW@9KMcw?9p5D>$i%uhw2rmx!NAfMOPYXEP5G~pVn@=h(ZMBGUt<1SB_kLkhje%MW^ zBNl!5My~mPKio%+C2P9XQD!Y2`SQV3Q#b4Wu-CS|li`3fD2-Rmg9PhmvC{t`0ug%;14cx4PNJK$JaOyfG!)b}o?2X84g~7J)wX zV-_>Pkz*w@Tz}4%3}MDat8uFDfVm`K5`i@mI&Bch5rf-tJ7#3$5VL1tk;PUo&|kOH z^M8xS%=63DM=j^O8yS|JM#&z|cyDtY)iofwnC@!R?xZ%t2q4mZ8ObANua;4bdBeZo zzbi4nzj%gIN>-TRmiBiDol4bRrXwBR!;MV6S7&jij<3oKUHorNdHT0@+`y9TXo429 z?V?zTbP99$Sx5##(Pa0VPv4C9yemm~diQP9o6zo6#TZ&dT3W5P_x**bL{lxr*ZuKT zrpz9Am2WMYX3B@SQ3CDThvdEjj063Z5MUSb#(tyoP(q$qf>PzkXadBv8W2ry;JqN2 zsGJQ;x$G+14TF<}z#&Qz3wnggWNxq_l?p5fgt`vq%I(MsN2uMTWjtb}Et5l|zl^!B z+QS3%&1*DN$R z8YI0lMP}F1oIETqiRVYmqnXdW&3)1h+%+Te+6T-_FD0YT#h|`sV|G`WgUm%S|u;$u0#y)E`lRTes1cjTBS zC#i=bSJxKgAVKKYI2A>$#_QrG!$$UOY*0glg42ZLR?!5keKyMwUi(zD1_lH8KFRVE%>SK;(w5!j3nK5%F9} zL~6P>jLH-gpJ1FObk;v=oCcv5fH0%47#DUJrrpb4tg}ePQ?_z=(U%a z$Hs05xF*9-i>fW0=`sn9Vbu!WjmB0wwbezTiHY3GhvCD-Jxa@5lnYCO+bKlQPO=L1 z0|c0yL6`y78ZHyPSId9tmt7x*PUTR1{%7-9T{i%4Q=U>?s-sNUul zZ5`a=5iKh<$d?xGv0voa$002raC0o1B}(~vY>jjXB2Wv8o;HMPd_Rl#>t!t| zMKWx7ZVJ0&u`btb0zym%6mK<*usviBpxCP^DDaFh&!Az3l~|BnGs;NJ1U1&+~>yPzu(a8J>ci z%{591Rqdnrl`}@T_BI_!K;8p1hk8ycGUT4qu*=h@*W!3E+WA?CVrpAn6muRGFEb?1 zh!C7$HaW5_L!pD4qtvL54bPMH2uRuXOaV4xy)SF;hJ=)`6rnJ`j)X6gh;AgAE|}+} zp+_#UtKt#o@^+g_xkw%lik&1mW4LYE`=}DQ9}|KbCS^x2Wt{34^;c`v;q7cbqg{fX zYn!BS8r~AuGJ&L#R*OvE%;O#~5eB4HsI_GsJVhk2;!2QeskQi*!8xZ&utlDX2=y1d zY)k(gJ!eXkF5~ z`QnMc#}fN5y#(Dd_CUJ_{8_j{rD8`~xS@xy@g{)1%Le!#$^BlRZp87MFV*(*4Zov3 zRGmnIS=gp--qlCaI`ya+G~IZ5ZUddD1R&B(ZBV8ddl~#XK#YexFuu-~f7UtZodkF$uy#RF{;h|D}|B7)##zhW-%YvUFG zkYLMCAPiw(Dj(a>hLUA3H(7iACm`Ec5NL|!*Ok3mQ}Xx40fz^-Jk&@$>`KMmJ}Hq8 za#)pb$|iA8_@g4&D;Z)=#%7el`~mnW9zp*$<_QlT$wGND;cg%%2Y?-8;XFm?DiT5l z4@u+_4z)oq3aw@s1U?sjjg@wb4|nH5eOQEM0HO#&^;j5(Izqk}mV9H!!(OOj8#b#A z>JCEvnD&Rv$v4~3K2S{Pt0C>6>eveCd=})-jh8g+7?h*>@+C#bRCy_CfV^55F%TjH_6Me{ZrQo1H zP6}69*b5VZH7{UXFs8V#dHF3=c{f5Bhg>p_RTF47M z#ZS_%GkRziY$B2W9OI7R(UnO^-U{5CgbI|xOeY{Ea5ba6l;7FrN6S5QNir-02B8U& zfoN|5NPPg=03bD42p$U=AVw(cQ>H6HtSAr!5Oy;>Gmz%_A({z@0hEkO+6HJbVdibg z$3aY`2rWb5IfA7sEQI|A-!h1@`>U+dLv}u#M!A#f<$zM)1?ZTmJ70Tp@_&!ET>Hk` zXHI^?B?OuhIw!EjAWkSorE{^vq{9?9?C5Pw9)J>qg#0$_V=4Xym+<;Bs+>#c6`>m2 zG^bba7gsP{ZTKN3A*BtO%s@S6Vb_nIz9zyyUco;QV|TS7%K4c5Hsme_YJ`MOVWNT< zs0$$0{@*P(Md)gt*N}+tc?b(R=eRz!tr0+Bn{bao!k`p)0jSLfunl}dIf&JLBN74F zP4O3j5yz$YCqne+KEf3d`j`}RO^EH5MJRrR z8yhUlHBsopp0W-NYoV{!Rs-bgV)&E}oTUZs+^+cE ztURGM1lhQsn_3|l1_QRco@Q+kw7D)>QLx|TjRm*>D3U_iJ$2B$N*%%!BMMv&dWa!k zg)(c~*;HFoXv*37OHk`JOpXYm{|@TdCX-V@{U?$~*a&`y>Q=rAWdfqitNhALhO!`= zNHD8Tm@>%|P6DVPoy;b~d#4Z1?d=|ItpFqAc(Acw9 z(aA0D|IX%Dki<@9pOk`*D!5@wN%6|`S6{uf%m`mCI7~~;R0_pxqqKfRg#YN4HVoqD zq^HfzbcR!LkzX)+;SDduUV5b*BVYWCuwM5dNl#tFHXhp@V6E?qpAusCIT2bf83md# z)Ku{5FY#Ry_)ekGjL3*shwEw+D1W@}+QjJY5U3<B(*Y-3a0N);|?eqX& zhYP8wKja9Z+_lNzA7Q7PE`)_6a zx>Y()0iz9jb%HD#&X6GF$-~B{wG~fngenGg-GQSkl5r*{?)A?ObbSRIWg6V0{G_KO z=aN!iqjszbn{a5wbl%S6Ap%JGjf^9~J%q@41K{h*p2)DK|8~0JL=a^b^x84Vqf3yj zZLZ2J#CZ}@UkSFA1T*EUJSYXMBh3jc^G!UJooUTl=1i(HMCL|Unt-54kWDP8!30DG zjXu1hG01cuGsL|4**$KE`4ggZpN|vPp1&s_w0W`2`}{*w_yg^L2mQCVWOVAPxoVb0 zFqC)o_N7wX6t3FZX=r5N3|F@hBhIUb;q@VOnF2A;>2g$0l6{c1W6fqAGxCgR{DeYc z%*`9Wd$%noXg}vXmS^`yYC=t(3F1_Tr2q=$`bOk5M#uU{CPA+a!nEI~Zz;vCfyAoV;CwxvZoO5T% z%p@{wppb+r{#7(xL)Y>0$>d1z0D>Ml5#O#zUD z3|N>0WE&qFC<3;M@6x5v&HJ*?$;!MF?lNJdF)Q1eKeR&(QR2}xt>tvDWXm(u;7reY zixGho;m@layEbx<(|Vsn5zo;&yOKU1n*#1rwm{^MdzAHm>AT&P^stX~g@o)75pGU+ zjtyp(-di0}<7@Yov+dZW?4>CH>_&+!K-Vj+pm)(JQ+AAY0v z(6psZAt_3y&r&k6eETuZ?Xflc@%!_Sd%Afa4axA9$1&!qoxf@>Xiv3{792AENR+qzcl%vH075`q^VLU zN2w;kHM_R)-EBcn+|8q%*tJnK0ccH~b6>!=vPrFTFWgOem|*}`={X3Twfsv3D1S~z zh#<$g7&HF%!%U>A5ZDIdBU#OxM8otekiT#3`8&Gu=_E8a6^8v6b}^p51>H;pUMsP7 z?bz^uY?Gl;nax$E3!5IyEEu$w@idwu$FMOxo=T!V-DvenZm(jH{@M-LOWfadhln;p z3IGXa;5SET-USgwx04K$2`T^T?wBGpP2sbIXjdWj84Gu61$#<^aOUKF37?QVelt9o z(AG<^OTO`pLAcMLyu}s&5G1yxBz+P6>T)V=|1D47f40>o4B97(%bXrIyu}7m?*qXe?`YvORQ`)olW!Gx zzEb=JX??}?%eVcnF5gK|AIvLo-B%O3qHLrSr?K`M;kilm1Y9L$6e1m-m&quiHdxqO z6Ie?CK#86hCs~H=25d72W$bsXHB9piU@F7E^D-^YxIczGvvvkfL~YthgPGif?U+~$ zgaO=>U%p75P+uRwF!p>ooU;4V{ARV*IM&`u@(6dK57dWc2tB<1K(^}M6gz1C)&b~fL>!OTekk*($*Wmd+pFM{$YK! zwbDrkmVCYUe-2LzKW=H~y-WA%-G6g^-sr)}liGed;}Iu69!}`o03JU`x_@t~Y}c=^ z>6TP)uT$qwOx+vk>*qY+s`g)VlZBDB-`r>r)nrq!TH5l!Dumozc)ZcHLwl_`cr9;YG9lm7=9)|F znQYs{*d_ha_j2>jw#h9ThYjOyMvUoc8pjY)+M@>f>dpgGx>I}XPbUp&RHZ z)mS+t&&1>HpBhHb@9lTVC4aXml7gG}{z?D&?ftYE()Z`j`udLz6q5x(bsRwdht7H| z2DU}Cu~FMX;bpFh*Ap9(RK{z^=wE-d8cnBDo_@9CTG_6bzq(xg`0HmG` zP+yj~rq@S$q(ug_1BaruE932k%_pMFyf0tK`mp`V`vvN-UTsv}sAU7+_p&mNv|WQt zxh;&q?#|JJ6e`q+p@oT!^el|ANZga}QaPzj)?;AaQj}+d*>av7haSxHyvdkz3Bx33 zm0chC&~h&xFA^Q?5dxv^xIEZDK4x+gZ$8HS*nB%G^*H6~8{yXd%1?steYcqKOi|QH z@^Z#@;#U_(JI5N&6zj}XRy+-DXqXJV`w~2NR=0|m@oA6!!Q>&`t?nNYN`Bq-#y;l; zj$hik!y_)MJnZBIdz*SenjHJr@AC&X(1mZd@*_)f#j}y6?tiu{{^xYr^vzMVkVO)9 zqmxg6-1_yZ^LSGl(>vc0Dxi8qu^2!_?w`ETlDmC+kW0Rcz5;}W2mlIAdKy9P>U6(M zxpJv;t#0GGIuxlqEDt$Tf&?=NinBlJO}&#F8;w94Nbg`Ae@Z}hh2QR?_z(-M%UZ|RrN~LpXT>Fwz=atJBm%WT zvO#KL8&TldUsx!-1MWmkbx!V6B_!&Se$wV@=%#p~Hbs6cmcc$(fM)g~@unVd4@$mJ z*2=VCKzuT$SlBOc1HX0%I@V)5QdD>Qkd`J@Pdu@REbDUEVSk^AebC@csiyVox!V@_ zra@Wn(b0R~hN_yZvofQpFoj?##O7_A?2IYKCojD59swMF5Z^Flvxo(@8(K$Ax zR|$;e(&G;f*u~9oW6$k8H)U^ljQ{0=^bgghuJf|hlBQ=&hWW#VngpL2*ql#KeT}$3 ze~&}$v!7wk1^;U#?|5NbWvDpF^t0ijoy2V77k#$8V&g@0&uP?0m=oj&3Jv{lpVRSm zvPT;yI~5I!i$$s&)P~_LlJnfnmpW-A?xJ?)4k)DaCY8E+pS#^cxA;g`tZJ z3YCn0^9%D>m#78p>uGr7yw#$Rxt{h4TWs|5(;`Q^En%^J%QfhKtrw`Mt;%-;6HmVP zvhn$>bGRqSyY9pFzXGlVuZ&#k8ofn%vp%-xaKKpVZ^ZfguW+GNblS#JgY_YOBMQE^ zANFz@c2Z|1LL;?x(5;h=cRCyO^pc6d=|yQbM~`%BwwKmAukxWwhdv`NR#c;wc5VV> z$&LVSuW_q3=}s*EOPtbk#XgelL8!6^o73fnFbgO}Ss!8{Wb2UJ#XE&xNy;x&wK6x} zWyk2o!N(58ejl%%E8Khj)@GloBPO5kYw25*5=;V`Bc5A~m`T#yJPU3;as0cEezy}5 zbh_*{{}57dnCoT=KOd=J_*%J4a=}VtS|#2kKW zo09lHN}PC8zv!oaJQiwf2B(pC3gEGaI^6b=YZbbXv~U6U1^^g;0j4u(iwR*KgG6N}So6THhwMYI7COv_J=nOq zH+v*MzP@g8al@pzw|rO+Va^^HJsei2 zGN&xGWB%~>yXYKae`iOHdG*`a3$J#8tJhC7pU{;dFnuKZpYC#4_5YJz7GP`gTblj) zU(SkRSW1Yqt@rk9d*{E=jNhznpR1)q%vYPC>Kz%g1UgBLz;(M<6Zv0z7r6YAOZ_7| zmb4-7`_`*S%g?!a;8;!JuNY-!o8qEs4tOHKdJJC*(_d#@uV5j~xatJ>ANCo82_cfL z_1r^KoEgOC? zm+;Rze+`|7#7G+|RHiz!bl!|mZe_kdWAhIF6?<_#zUfEVo0Y8Y)M1^}@n_`+|J!Om z-SH2%@$8w#P)Fy+i}@F>?Atd^TCur0xi<-I{A{NTb818CIhM?7cYUnt=+L+}Y#sc* zd}I0t;;tjOd=KNb=t^>YB>bzpfNrJuS~-@a{{k(Rg;^)ww

C-hR$SnCAjlU1EWl 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

F$0{g#NwN>YdPHoYSiQvH9y* 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

-5#_BI)CT$H9#ja+0XnF84qiuY6!ij$a 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̿|}IhYRR5luC@Z6Lk6#NF9qhA6~7F`r;qk`1^q-c+M;wBIq%hE zzTezd585m|r7!bdRkL$x%Gp#^_X#OTP5)2jfqL1?&PKxFDoxQ z9Iya?yPU|jJ85<8=t$a;{FJ!cvM&cg4j#CP*5|3w8g7SsG!jRbQLLe z$%H!S76OtvQaz@S)tGKS3Ks98D@DF($f7-6Je+-uFhDUtj0aoe1~3bMS--G%$yD``?pNedyxiWVdFZFA-EqhNKl?1Wy;&4#Si zw?D;GLLnBHD$}WKW_AvjO_>%(wTv#|Ipq*F_`8FyhIb_U66}Vw{f;n(AN0>8E^vOc{vQ6mAzo_DMc z=lr_Y>>nO7Pw;rXAU9&QeugIg`p9iz2vNNFt%?c4RJuW4_GvC_(3ArbZ^&uXTc+Tx z7E%w!Sx?4pLzOJD#W;I%vAi|XDC29G_c`2)=8!$3LE>Uwyjyw2r`5vt5LpjeU2;g* z0Qu9m^NRG+b>#Zsks%59%P021FRmBIiAq{o>Q%|`3!FCQ8$vmyrh~57l z%O~5()$jV>Wu3sN+I&$%oIi|OVZF;TL_*) zX8E_DnqWyqGXU<$y|D`c6_+nt~~=%*;}a^X-lO}Rx~nA zPW!WilY1Tq>N4#9qH!yrun4%K^Go`V!?TN}GIm3Q^M%bP*0bIbu_wT5*czEYx8|baiB6|D0UVfEK zAnf~k?Zk(J-D7#Il|5l*3qGg{Nl2bJwK4EkHcCH0T2U^IsA`aLcq3>#Gy0BF+!(5` z$1@WI{L9G)QsbT@N8bpN(aDji&sa$b6ES;fg7)NVxG0 zTw#~Vw&lX^a#Nl9Q}!|l$MdgU4!9{1kz$vqAUng>JamI*sBA?^{brat=g4*kQul@` zQMpv!6^IjtZL$USn9iUBu=PraHJ6QA5q0LK+VWD&rXimNyABZ9K?$j58I4x?sXmmO zW*KaEEp|XOJhA4Q)2OtjB=k6!9b0(43&`GSa(?WWnHdnud7Muq|H^V|M>bJ1x6L`t z(*hm0?U>HaYY|;>bH~Xj@%oBh5O5Za+i7`bmkl?KhdAU*_;knP- zb>UUEpIKUn>9q`(jDR)u9UN8qN!y)Eb>OWVZPBllc#R`Szz~h(6TH zWKrP3rdS`XR8I$ZhermNPAg5$GF#N|#8Ipp*}U0dUQRfbF24aolrq-65AhpK}l1INZ1eY431jv)q|o7;|MrkN=T z7R?UhHCjWt?&CE)WH23k&)W7KP&Qm^N$qNKYU%N`eA1ZIOuG8m=$gxg#O6a0tX<_U zMly9fkJXyHjEzXbZ!!9v(P1_0>k zo;~(L2C0|cGvpfV_%$_?*}e%UJ^Lw!#Pn0=31B8G3$oo<9s~?vvt>}XP&a5$&LEDvb87~UHHHsyvH*I9xq!*YuI$tgv6%Tg&ulssZHj>M^ zb9pv9coC-X7U9A#_NmM&4}Ebzd#IHoR%Cjpg_M(u$q_Xay~4>lF(%XHjVm`V-hut4 z?eZLN>vU$*P}(IUGqlb2Ho~ZV3QqyWfz8!bnfNprbbIeM|%pN1GHBkgG3OR6wkjN04>!q;n~3_pcC}g zU|y^BZ^(&~Nr&J`6b4d=ED9{Q!7;}I`v;HCW~Wv-{bGv_DQasZisi96k}|~XRPUDM z2O<}cJGNqbtFSsf(Xq{Gie5BbS<8TJEk>w*-gPMjm zTb#w~hfh?e9&G|2-qY1tBt!afc6f+9QQeBF3oCz4d$W7popY7dGbuZG=1p~~PvPI; zhXKZS1lldJHMd`*r-STxak>Up9#0PmgPSJ(djJHFA-cwBn6jcLKp|Yuc!yUVd!AwN zY*C(Yaj_3Dj>WG5!Ne)|>(B#Qo&e9^y)ETH0`&ODCw{dI(Q-fn*N;x{Ks2x-mds1p zSt&VPL1g&WpN@Kq{s{N9Af6t#VoCdF0QfIAJ36giI&fzX+Nrd2Am4X+&k;CDb2$a& zxcB06P(q+V9xVjn7>HlytiURR2287#5_DevEYpy`zX$F9r$tkGd$}+uC?mk+>59|O0Q0-S+E%v~H?$Qt zw6#nvjqm5=j@1m%YK%wT93t3t7Yz>)L+YeRWBGQ78{(MXI^Xpm67H5ytHvv1YkQzG zMiVt@nMOB1oaiWQ{@1G0V`JI<9Ti<))1M3;J!4$^n`xK;4xeY~aai=vWqlQNQ2j(E zsK-nUwibEGVOA$Te)K|ME)0gS%0s6N_stD2I(MsJH`WPr;ui0aKQ zJlc^H3bi_F?k-oIjnd0N8Znmj~-q}(?Atjx>8N3eAf`IhLbuRaQF>WxZ7!ywYL)w z-wossPuyUQ<(Ba;|F*dWcHCg4)p%28&NM-TD@C)$ZMs@4;rv+7rb_2ZowL z>?h3BPQIXYjvK05FIt4nIisnJ;r-@UfJFLZtW^ZLcpSL_xh z;wCKBt_ZF|^gqz)!brVI#)A^l>j^nuJhXY_S^Me5rDQ-O!{6(I8_1y#`}7$)|2^#9 zssEMu$I}`2PuL3G517Hpeknc z{3`b-Mn3AReEL_VH!#JUR3}P+TpzN!>SIiFaV65~d!1%wfvM1A3_HXExeN{UctN116)8se9 zO91Xcti_w}76-p6i^T0Ii-D~HSc&Gvh3c1)q3cgSrTzy$c;h4<897CHH$)uv`yBkA{kpyW zdfIB-j7jIwDDlx{XF=S|%lYzk`zV(aUw))*y2%P1UT1Hc|MZU#7Z}R@@$=YD^AN|x zAj*M;Jxeqo`fS}4Gy&SG=LXesgN!mhSTbNDz=2SRmpEg2c=5_-e`Or#pf~{8f_QI9 zn0(uF;5vY&E!~WtL@^j2rNeeYpy#$D17=b@vR(qqlL@&ex(LkvUdo! zp9!3F4zza}+M>h4vP90!MDDfq_`$+=lx$#W6UQkeC z-jSqA+23iGlF}rS3Pe6|MZTghC*2eIS|joM=0jnh?pO4|gzB>Y&@@JGS7*i2=45Z2 z%m3mYdWEv$&%5hGOc3$av)!(QGADM;K=SE{)i^CW)qhr#ex`+lS9V>Y+3Sqoq}GH7wz&O zGUf!cSZnJH9hIzp`slKtWS53v&e6hEYOdL-*tu$26(6mLRKGI5qBX93;2!z9`|2q< zt%F=+1E0w?>@K&;eP=&xI?CzLraev%w&qOSaw{~B8UGn7d1rt5Zo8!7^<(!>@=f+2 z@2tQ1pWB>?qv7=K!#XD`cidDy#&6ISOi;>J5oP3n#|{Snt$)HgozU{Iz7{-N66rY- zb@^YgZd>fJT_z?`{=ch?W_KOFr{%Zp(HPLd^%Ai+hkbdYZ6{^$vS=;0TuCwz&F1*0 zi0NOqPMy6d`jP_$rWwQ7>@%RIrztByWjXhUij@$DzAr zjtW&CxOPAq_Ug)xbh-3oIgh5-km}`LedP%J1jk>)_mCY`Bh4$6`os26EDLthD}tOV z7g8@=qxCvW!W&|Kf+@6}Sc2&DE0yvtIsx2fVyPkI!POJsY)8Ngs?@^*wWjGuD+Z z>&T_TH4_#GSvQma%SZqh?;TKFN@Ayg04U{#)CLGZUif*Maq}X8GR!P`I!E?n`V-rT zDKaiJ1{FgG#@N!{Cr2TBZn57&kpv>G5T`Q?Kp?4F)C@CG+X6U7i-!hFkQV@mN}8_* zREHV2gK@yW-7KQ|NU8hVyA|GJbfY=Y19HZ``F@7Wj06|RO zbgS4)%|yISKV&4nXY!D;UJ)FszCbAnyox9;RKFUxO4}D&%8fZZ`LjE)`PAzBYsX)| zC~jsi?zz{*35%~eeoPCg5Rr1_p`BOsKbfr)b&Ga#mG-}U;Dp4AQt0Y{b#GflrMpwx z=}0B7>e{Z;y<4?3*h??zSe@{XVXdD#aL4Ywc?k1<^!3j4X-A66JM3XO-ScujLOgK& zu*TAyLMXAakfIV!q?XEu_9;H90s;@fpr!xyD}GF>vW2cjVeG*xAkLuL``*1&@~`)M zcQSOo!T_Fla)B~foOhxOm{x?En5_E)C;fGOM2dOrj2{#fk<5g7 z0(%f`Rly3Tw*;CrzPhc5qf(f|m=cAhfvpPNeMQtUT2NKA?UJrLqXI1*g-C|dbEOk_ z6BR#gwOUuHV(y1e9*#WBeNySP8hh``3^ohd3d-}&Pc50m)a<^-AF9f;hVpVlbRV8_I+Ul{*}Q2_s!ddP z=K$Ix6laOm9JCAZNJCn}qV$3oOuLl&^lOEL)yfzJuS0o`A@h&tCD>l*ZDwvareFF| zL(z99b9R2|%FHc2Q^qFr$j9qxD0uV0xI^h|9w79>LBL8LGqH`|RtFyXI-iU3^oRc?rJP|GszbXmbSg(_oIGqdCSnc zZ_TfH&EdC?3p#@74dJd6v_+AuGp_@)=;lw}yPmt@gN**UspGq(8QV)@U9iiX-<*9U zI&tlWlF$?MzE0X2Hbj9xgpu8OOOn6UFYiE-UzDMx{aZ3Z z&?Y~^O^?U|sA$ds;szXIG$Ga4t(HMvzlFoh9U@W|WN9s>6)X|H7E`A0da9>^Jwz#~bokbW3 zM^8EGO@-JrbcxrH)A#gqB=7bExcPjjvbw5N{6-f_2?H@m=8IS)%|cBZGW4StV7n;- zseO$1avss>eMF|j|NE;VC>ZW&mlQc4YRmya?H>nbq5l+=aE+&85_;96=^4tlfXHb~ z3bcf1rz8}!4d?>d*_~0Qcr*ZVB&Uf`7BisU>CZoL-H*xP9yY4{Xs}kvs+z1VUZ1bg zt8^E?rsOr}EuUWWpjjit`Xzg2#pFq*6N}-cc4crOB;IXochE{z$+9ab@oqtoS3pY4 zFqSX+LGRaq?nTn`IlaUo-${S_bDf&Q&)j?Goc0{}UhZbbEJ=H>D9n?)n-Qgv;IUA0 zXyZkl`u6(X5d6rU-FAD|W`a#*Z`^1}!aOW)L7gQ2WU+9?Ncwby%&vAw^xenWsV^Y_T>372z8TLLo=?8Y*_q=YwC>bG1GK2 z4QmA#F@oZu&u3o;1oy?qiOsw}e4(n`3Pb^03d9kNHZoO#mkw+nZUcDm?&D7^t#`W9 zfw}Tm;ta(FmW`f+ZQ_dp?cuLhlstk@XZ0kQ##2?NYHL9hx&>%sfnEXt=LmLwF*HoU zPG<9p3_dAgvVqTlIyiL7Fpz83U_AzMQ$RH3t8f@%8**RFgm2kdfP3&lK+4lWqT$rc zqN?t3&Ipu50aaLQLnE@LEy zf|fwR5-c6ZQ#Gc_lfWulEX7YCeGFKqf`PQP!A>&_$|=wW5O5KMLZEKSlx&FJJY+S7 zR>`$9E1)L40OPGCSD3rHS@JxlF-kzp0V{F@vIOuhgg|o(WPoBRc2nSV53wMY5=TIU zrdaJBaoU6OeL6XgoszUVG-Nq?=&KUt?g8sf`j>4EnOTN0Iqcss;7NfI2+n*@xc@Yz zdIf2)=)~8~OVo3Io$MREq0$>8Tblt3?ceb_mE+|>_Z?{OVM`t!;M;{nR)+XwDVL^d zTtFKCKwms%ckX&GA(DUEKE#gz3-Uw1G3Kj(g{^HJTc1olRnCuM_cWFh*wmNoLzRJk z7H^Le zQYvz+!B?(}2H`|M2^4J^9#{BUb6};q8{vut{fIXlJq0Uy@lfUq1wB`k{jdU+fyq3g zI%$o>^T69YJq8PECgjQO2yA}>lD;6VIx3(J(jriCT)sNP8XXEquqkj;z%hh*5zfGQ zfQ^Ef>QI}bntY`?0oa>i%;l$dP@qIA&X}(?%}mi6CMe2V{k**)F4NV9^EIa{8}rc# zV51cVUT6rzGsXLXf2XOs{a_VarhE{{AXI>Hq-c7x+NW6RC<=@+GSM>CDWie4liV;H z;V8`)Wt3dO@=niWMXoD&QsCXMdwj}Y?mZ17EKt3tS2=CtJ`~LeEia=Bg~thl5$gPA zHv&CQHa_NVe6U^LDc`euQY(CAE66Y`WtCn`2wGB6_($}&z30UXIZ4uN@*c0jAO)vj zkv=;`J@38ZzgE=a0zPjY)2&8EZT21xmAc=Oc9MK>QVzzx?t6^3SZxZ@2zZ^wFV1o5 zr$hqNpy6Hl;I{JTAe1$}bC(4dEM1kZ0moWwAHJ}C@JY7_3fiUC(3dcJ81P`|OCwGt5EMiJaS=3s%1h6OuwVR@w&`D6KAH8()jgLz~*{#sUf(ED2j1{4`It zg{ADv-)-!mjk1QU-GB^FDsp(E{ggc`Z_40P(yp$DE{MNYDnU!0&ST-m_5mSZ^(GRI zfU0=f&=loOz80I02=lhp5{)Jf_83$&{tvNs$-@OOq)u(hfs40j##g z#CM(|8FwKVlx+j15~&*IO)=pzkvZ17T&mI*Ulw5l9@Faykn9;>=E%RL=l~)FYYdU1 zWSJtt;Axs#;~5~47J+dm1&8mFO7_7S+bARp8}uOKiy)nCYdi-Z@}$72MCZ$0{R!0i zZ^0x)+-{94dzkx@aouZJv<%PV^L`vNk#TzPQ0hx}Z+$~ZSLI;Xd{0KhYeJg!M*Gny(^SW>=cnAu_}46@RuX)cOPt|H3q%x|9cn6CNxJA zyrQ#C4Exf}o!{6hM!UTBAJzwyVQE(;G{Ua`HW_(&LG>JC=bc&`Z}xhL)*J{zU`X#5 zz{$M+7tfDq@RB(tpt{K;b`;nguzORW7%_F^4nq<%3JqoHgI%=LG0z)ZaFrtvQwk!3 zGPZgD&BGk2P5@#{5g{{=_wbUt8EglT@IMmO;zKqWYNpmQp8&!rNHUrtMuVWqK%cRY zCJVAKr9g;)5RoEjN&y*DB=ucEE&3C6-(J#7l^%c7z3rM~pM&z|DcQVsXTm5TjSOmT!>LzC0sM-!g8^?Ap;B~VFwWN0ErTKC=^SL!-o!2U>7Me$vkv9NP|a3w#c9i znd%vQ44tYQ!hnXpa2|X6vc>4jK1tmNXTVhykV{cLr94VBFaET9Kr}k$w90$Zke(tT z+R%;cO1|(*p~bB?g7`6Vq*BdrqcuKDb`TjlnV0<6(aSiRDE}NLws3TFUHJEw+855q#zUcpC<8(yg?q&_}?af5N z@ih+^9h`HTF*+_R&0(qn%33u;pf>keahfSSq14n_igD_fHe1!W0^%@L6=AKqwMT?eK+#McTOh-cA|}azHb6q|mx3zZYD=h3&`Ztt13G_B|L!$- zPRBW)-TOv|p?wJl)}kba1KUsijsj~PlFK*7zvEaz{coOqND*3 z7MLPXAjXtI7XY$01&NCR5gDS!fU`d%SP^(#)2w8{Q1Ut_*$g=j-aQy+5UGDOGv`>}SC~QL}ZZIsOGGH?8wbt1^RnBq zNg;u0?nmg+(x%oJ34{1Arzcu4P~tg@_OH9Dz}guSq| zKv>^iFq9E23V6%3vClnXUTZ?GP@oAhWkT8~B>`s4a41oms`G<%UISO`h=*vLPgCZB z5WHZSMy2!rIa5uYr_uo;U~(W%w)U0|?mif63Zl6}9NKgO0MT;FV9aXQM67-Dx3o5Eq@RmDSN%X#99rjf*#q?AsefOZ z7%kGFROyVrvIJy{Pi2?+Gv(NxJKgZ-iU$pkw})yTXqZluUT~Gi1tiMZE95-$oLt;> z2t-IOyhF(|@P8>$4pM~nY2_jfj)N-MAvgh*5XZ+wfFvWBauGYSQPh1aZ@&0LPa08s zuaS41c{a#A4!ik9C2AArJN)I8pM9`8)$Bd>#h6CwiH;t3*Lcg~URFo_34sQ?Lo>&m zl)=(!G5_``sZMQwLpJ!q_m27^d?sI@5en8u?Z4FVl~wLdxc46fVL4&SYdW0p#sruR z0zmx?B~xn@N9H+}0rc}^Kk=2wdK&lyX#XQWkS&A>L)!M|6H{g2@--{|Wke=hRLT$$ zmrtRlfIle_7+e4Zh9RXa6Gj(6={f` zv}+3=e2Pv48q)Kb{S~%}lnll8vwA&c=-#V8))tm};39Yr2il@)fdMH$0eT)E9(qMl zMT=Twn=(@#9_Y44BRUpoyH*%CUikA+`t`%J7LEM#kJhzmgV?=!=Ld_`Tdz%LjCB5t zecw^GQ)+e1DSp@fpcdlNc6`L_%WCy|?wY@f^`4x6^N-s7cwNtxgF5K<#`O1M%go`M z^$N%j6_|q$mATVPozNjI0q;}O`G73tFwU)gZ)2PD{-^Gc(0F-)4scC0W%GxBSf zd{DjyGIXQA@H(t;Az0D_cT`;xkjGQNP&Yd|(`gx`k@NpSs8kn^~XLnSPNk`flu`o7@xCr z?>eN2AFbneW5Vke1KYk0TO73doA&W^`|IS7&-bGqyb1_2LRY<_m=?ZbA*{#mrU>d@ zy_yv|+N$s7>2ip9NTY`h=f(clzL<7d8Mj?2+SGa`*=#QC&-Et~*(o*$l+WK3|Je0z zx8-BRHHN`J-HGI+9m=jBZBNCGliI#HL7`z3aJ^33qJ+UKl!mKm+0i#8!StA$Zr&@G zr19R2YUQ0@Z;NkUvM3XMdudIYt`(Wj0O#1M|44Flp>UYx>VTnhA|6&%nRb97(9S<9 z9!uCha?>40w6QXArjsBsqhslRALmQnnBGJZ&iUO5!p(06ofU258$V5 z)HcZ}cw<$mfKZCdZdO5IF z6vwt>1ru#QE#@}YEE5?vN|Y7TnexMwzhm-dXJk7yJs^a6peu?DeYiG*uu{GXCe&|q z0@Zm4+XlFK4Kp*g*albm>iOKaqKaKmLdiGJ)00}#*X1MT~LV%Q8>Pn6XJ30kyZWK zUVeex_;Sl_*Gl_g$T;g?q5@vq(GjwAS-dXY%D9{3bXF1pC5~d5WFEvXL!kVR4(*to z67%PLR9@|P**?##KL^y&cZk#ce=4XnjZgGH9bO$tmGzg1C-BHn;2NV_^D34J?-Y?4 z%n1e|MpF#k_)zhUK8q}4Qh?YMZ`+tFJhu3Xv+gzH6*ZL^$@l?wkaxACy}fll?9&n^oi0#R zWw2nOy3#6jJ#nu_vyM1Wq|&xPnphR_&>T9PL${S8?JQ=vXQ+s}aCwidC-kpP26Yxm zMs)#qWk1L6Eiz_`9%(wIk+ar>Gr$8DP~m8bDF|IoXY1oDMJWUUx*N~J(!QoTQc^I9 zlvLO!j5ws5E{o^c7}u9&!YZh^;*4H&d;^kdN|e0QP3g4!g7x?A65T&a6VrsM z{+Oh|_Hdb;nbo(-LWL!%-d?H3Ki3okLho7dwSL42G38Z?ZaA{Ie~O?oQ2#MoKqvVK z;88QKGEx1HDtqzjsgjVZFV0Te?Liu(?igw95;f%TWVw*rx`bQuQ+SMI5Rk4nNfdi_ z1H5<8K>T2*$Sa*t{`p>MV*jaJ$EXyR`BdWVPwJrBd6(xML$sTpT%v^Z;~Q&FS50?W zX-m$$QsRYuv&wa#(@Lr-Fje}MArxUzCK@D=6-F5B+Fp>j)CSt!8J*@lw@@mc>1sP( zFc3-y&rZofrYj|7NVmiXqiuHkHGtrqFTp3Gp?ZWx=!rHSxL3&b)szIGbC$&W)~t!- zL+LIRAoBMC5~vI~hnj2UXlXZ)kRHbbg*?DoEWz9eOw<*kO0R=I>ec zjtWMmx>lDYr)~PY5Nmt5Vn=|?4DAL@62(c~OClhx6q1jBI=Ip5SoElxaLB7?mbu?C}fpe7oLCK9DNZ9U>TQ8s{oM92>`{n*};<<#H57k=f!?I0K;H=doXePMyI9rcQ%VP zf7HGv;w<8~eDch`gQj0h&O|aE8Ly&tjdX`Udh|wHaf9*r+@*Q>D8dR<2kVdf`AIB- zXFj`%X#gp-P!$^d_i+Jq9Tn8R7#FdO3{e5e=FgJ!KvpUeZYPB)`QTa*x``rHLsd{8 z1s_!r{~d_ksq>d5l24oR7K$;j0-=&AXOI5f_6~lB(U(A<~YQWr}qk<|G z>Qzpo{V5Ujvrhq;7QXz`9{HL@aI3#;ax`I-iCpGM>$rkynV1Zscn0s)%BjDM+k21f zIuIG;P?&Nc$<{=to61;uT?k1khW;&8-?_@E+oYqrIU=@~ySDFZcfPyd!BR)^7&;3o zN~#Yc&qysDW0W3;7i|kLJzq4OZUG*2>>3XSFrXc&5F~(6=VSXNq}_o3au}k3KZrua znNvV$D*PWH52cHe{XwG~tP}-KroyHHIZeJNiU&ql;&ecU-M6|-zWf;+_eXO8bKqe} zx5JX&xf{sXrSaEy!>-2#o!x#gT>intG${N=ipl2)yPXQk^R2p=fZGzCl|pqwaonHK z#!uh;L>Tus=&O{^py2zdhGR@ZFNjdb#Ps?T45|1*e}a;e;gAY$fx;d2=l*n&oNW-T zt0eG1ilJwokNb1mRHW;P(w!=b7mN0^R-)Tf6x%A%Tso>jMX`k`(aTrJ;Tt+1HW*?m z;6Rof1|kPUsF}crOc;;($9Ebj@<7rpAS2mkLJJe~A>V2|Uva5PJSHftsyDW3*UrVD z#E5R2tM3x?x=yP>x0*CjU5WzGu%I2oV!LB8-x*-=v?OR-poZjui>Yz|Up!1j%A5iq zd8;0k5;?-WLL&ZZflMqNag{1XW=g12;ey6#YBhEF>ZWubfrwJ3OdLiqr)*Y$y91|>>PIc=Qx7}9QZfVGG&XULEMXgL!KrN|X7z`6dC zY^F4B!F>ltf#0CQ@F2N_1v$+{1PLZL%ITtku|uCdG>RWnR6%XSp-q~OA4K<^hwOf! z78c7M=@fS;1nkSR953D@k5t=jeA@B$e_l^{R>4r< zFJ6`-1jn9{6{6StN`sw?k2MgRF!W&IDU08KMya5ewjw+oeQ3^oO05thO z5PbpAWQf!-U}y~}WkD$MK!}VaWe$);Iq%I7X`mo&d2xFkwJ2YuPROVmlzp=;c30vh z835_VM7TF#qr!kX@URv{IGG~6`9jbXNIvBpwO%BN4mRe9l+&9N7^2BU2v);efX1pM z7~<)Mnrfc<_!NE9*j@DaHw-7^1<_eyJyy1D4M5B(kR9uVc-H{7t0@#sk3UI)xFw|R zXF$jcG;|Mg9|cs$!wL)ZXHC8XFJq4yC#R%-LoyH~s6~hx9U{H&6)H9j61n_h&O?3o zpUby5B2JjqAX=7p!qfoK3HU5g6jy1SS%A2~32d(v6PVxx4~<(v(&LK?B$Z0bMTNmi zd3BW_FJ@356({61;68|qsSuXv*DoH~tG#mTE`?jqmyB2t>*o`&e31^uY)GYq(;3-s z41}=X`hj zt?!THNk}9FvA4$Fdxg>_5qs1Ys}%{2R$CNBwM{}I_9jL(_Er=%TYD?jR_W9#r4wEF zwm(0A-sidZpYz;v?>V3IdB0xV=+=;u#L1)jwfJ%z|BE5+?(=!F&vDnmw3Jw;PEq-H zi8~ql5g!fjV8rCnMG1QO3Eqa@sd!?fQZy|7hnuh!@OZ1(8N!DKIn#mA!14_`?aT&{ zuY_n120)Mia|RKD2B?>WaIW>iDZ#1={l;f10t%eF(i8p^mtWaX;zmY`06a4A064`1 zSDVX+w6+7w1BMI$hw=bPot1XX$6g>s>C4hHra`Qxb3x;@2@x9T0YG~gmjH0qfcHwx zlO@e(#_O+IkrK?Bk3N-FA^l(&UQ0DhMugoM5=0xUUdRg-Fp!zUNEoWa`tlaismC`6RkjejfTSnG zFZ7K?JJ=BBS_d&EdIXhj`l;+8Dt2wTX~|2Pa;4F;5|W{usfiF$0-1Up9MY}6S* zXbF-cdj8`dSI5pMRX#2%Hxip8#Mc@W-T7{6H7cp_;LtxM7Nm@v${HB|8drM)z=_Qq zQTiLj58#M_VdXiSq@_-QU-R-@pRcFz9wt5%d3{&xzega&_G=t>1_d zK3e^H*}Tu=e|Mp59E+10{`z2Y!+hfO->HU#k>`EmYGv~#S))3_Q8y0Pbv%eTNQ=Ww zl65WH!Tiog(~%8x>(e>jq7CoQ)NI)80r1wNr_GwFn6*ZN`fpIsW%UlB|;!HIR(iVbLg{ z%z)g$)m5p^VYB1kRix=I7_-n;A+m<%^^lT6iyFP7NIk+x%>agzE=aQ}yx;!gj^f%` zkVt)Ne7f~`vvmLiRLiOBut=#wD%@gb3DC#WA}StbihcDekEQf1R^t9b^{c0{ zxcCsNaUIo^d}W7j$2fiExkAv9f4L#W;3KunoL3qF_^Co=gAf3v@Q%%y66Kr+QK^O@ z4n?Y^UR@H=D)w~{=vq|DRW*kSlN<5@y+SRk)y{NTt!;rC00-s_U-qHFm1)ka|AGFc zKKX24NNkWim5yqEKj!F}d&6`MJ=KsV)RSIXq-HyvcCyIBMGym!KO^WIs!vZHk@rYN zCkZ|#SgE+w=j(AyIu`3UDqurF5#2(|%?;w`@B*n9SA0w6Kp!(+c3pnn8T}cOS!R6c z)m%ZTD344}_{u~9$K?DAqp0on2n~~OKD80+n&awq83+OS<)zS4Yd*8L;Wm#4Xzep$Yrfllovod(2WEsUBKJLdxQyEw&b@+zX$ zlmrp(->C4D71FC-6ml^q_+f(4!<)s{-EcC+Agzvgj)1j8Y^~jtZ*`0F=yJFt+_z0mnJ%Q z=*9{~dCmzzAAmNp+Klz-6t!F;k3e@+-lMoR|0=kUF$yE6z9SO^H(U8YrHj8({c4BW za?DFUv7EpDI&Rr8avf*&G-M-UWL}i>cF3A>Tv3wSOW#xi2wz+KJF*116DpaJFY=oz%3nMKUc0U2?on@c zLfvgv<;v$~$lQyfYO0{R__sGgLwrt2?L>fG?(L-2?@_zIioSib-1zI|6L;p;zaE8) zO?AkK(+RD)BaQiAdDSV+2FLJt5fNJv3e?GVgulckNefW7R&JmzR$4y zpjbEQnM8v%+1~DqqU)u<;|V=3d$~k#n`j0x+|20u&8`_v2?U;y9Zuy&S6&|kR{Yr% zPG~6`<|phbSd0>qgYG72kZ1^Z)lqSCLW-9zhtx82Tw9aC8&0m`v(uFn_TLBV+HPZb z+A&9hqQiXBnB%S8(1aK`2x69x32CQ{ik1&Cum~D2Ze>(HgplELTrxS=2FxtW=2Q{a z&uvF0otQyg z^`&vhfATWJ@U|;b{4INR`4KKj(Oj<1E}X3J z8gkzjl$_o7imiPFt~L(?^7-sY%ax!xRoe*gOM7*eUZG{4nUCu8OZ`hTmA!O$tC1er zYGLJVcb_AZQNq1Ug`WVtp6)x|FxIFP)B!)*0c=mc6h{2D$S@K2YEKRb3zq4}&pyEL z7X^PIm|n+^8h5vqW@r*cW>-zyfStDs!lX1cd}p4GV`&yHbQp#m4TzD@owwYAQG}Ja+`K{ zdzws&?NGeHWXSLKq8ndqQGPE9vT;h#-7x&izGo#~zv?txxi6K*zbX_psUx&}int%Y z(ha%a@?`#-b5XzGTjTq+PVKOSkABNNn?|kM^Pe3*Uti)ViXOT$2(=ohfSnPavigdv zF;D!)J$t_R4lnX(-Bd!@=@ay-}TC$f8}yuq69kNZ>2B5x*o)2-bqj{dnVqV=_BXTDBc znJlh;0+!|?bHyccd_kE@?;Pjz28yL*>B_+4Zne!l3`ezwW}hQ-4EtY77i z^rKF-!iTY2$M0lE?SmgpZ!g}kxg}J$^5=(B4otsv8pZ3=zV;)PEO^l$_3v>1K*+jO zLI?Lvn1KK1-MW{>@say#h>-SmSzSLbD-l;61<_k75bu9W;SF*}UVZso*U?;YXs;_A zc9%a$J$$P3!3OLXC@q7B1hA#L2+>^vv4oF^A*v+oo$DnpQ{+}~q$p8t9Ay9@3co}B zFPW@#Udn&nD?HdRD8}$=jpW5#vRVkOqdmwfTSZI`8c%^Z?C?C)J+_STxRU9Uf%Ie2 zpH~mvUzHv}rW0b}1G;d+6#Y0M{m_L6A4_+IGD5@|Q-I`sJYiV=)whO?Mg>v;;q>7MnB5*we#*n}lsbH>7c{Nom~6^C;x6e)y@P_6q0)k(X}zqJhpd$Tsnj+U zy{Mk2X$S1h1^yG1DASEN!G%51eLnU+CeJb${*IugOw?OOz3x$J(IcuQN5YBNZEvr$ zl~QF@l8th@SN7~L?_CSuFs&uTfc2uznyAzhl8imwO)9A}Wa)d=XJqP;r^vzlRN9p# zSNJuSk)9VD&I>r`5}_& zdXP-E&3}j}FzL;6+fODi^G(%`3WCK`9JULhr;aaD$SHL99(q5S(T+$#--$uR`LpkXve^lk1h4dMPxWB#<0Gfu=f!`lvm*)o>v!j$g9G5(8#h zH7Ko3#cQo7uIGqOS}o*x!D2KN>BI6(i;EJs3+#H?eLJj#&;mjAf^hKy;uO!+4g>ay z;mRt|Pf5;ZRu=XanW$AdHPA_F1t(Y8MGf?XUgp)%qSWoGL}p>Dc&g8SK8m|SgS#rL zw?L&a*+RX@`9*c4L`B1X@}*)XZo49@haI}l@`6 z_K6NdUJYK$;(KQs^PKRY+GXf5HQgYGBUGyVQ|2YXnAQW#L1;9tAR>H?7QGQ{h$4~Og#uc83-$j~7$+&!mr zwF3oh9B7vsZ#ch%k4W$>Em_yG2@ZZh@)9b#8MW6i@E9xu<+^5aH5%OHZXEp2^r#F2 zjRN=H4nahcUMi6^*fQNpFb<+8#KEej$)l2D;!Tn%#9;W%$&$-GAA)+n8LLR@kRO=bSC19u)hU77q3oW6cKS zOREGwHIGx$yW6MENpAjDu#LlC(n>su*t&30>5ax=6Ts=Ra_CTWu;+uWng~!1I z>y7=KV~~7iH`z;a_5OCfEwv7s2p6h6w>xc>D$X&%l{D) zHaTMpOtCM|eDm}62+5T26wu!+M^1K)hDyuy4$hfoNrYUT@g$jq3?r>?jmkkYT-daW z;EJi_s)ODq7y7+yc~1D{Nj0`&LJRlI3dd&qn$x&L;`{W=S}-$=$?(*%wzLc%o-=>P zOySAo)fU_0-k;p#X9iL}r81kD{e<*3g85z9|EgO5s|wbfNM0?tE?&t0Ie)VLSON^c zzT;ZnD2@D+j#SAWuaGw0%JID>t$RHwvS{+LIMFB9-n0~=sS+tjnA*g1o)ta+H~+TG zO5HuVYwvAUZhX#ZH|)f%D05?IW5f64O^6CAnVC--l$re?+|Z*DJ|8lKQ)0 zqFq=2H~52J4rC>wb^6xfhmO04k;|WNbspUACYZ7uOFcroXlu~8+~zYV#^;&lR~PO= znMmT65#DB z^v<l3xKjJbNnOWS0nS2B-4`uq6Ba`x3rtgL_a#>(Wb!eM zJ^l?v;TXw7ke;Jj|ZA6gTmi>+L5Omu9>!%>aHy{k4f8zD#tQ zFO_Hp%O^iDzxY=1+WAt)QkSodlF9a8v@*i>z{Z>B?;o#!OvJC9%eW7mDVHeA=8q&v znB2M8a-Ki({O6Nh5zmwXn%87*f=7}BIK9A?S~ZvEn4L{8L`#Yt)LI06Q+&*eVoh1> zxSsEt3wr~ruKvloH;aCl!#fe<{~Jj2m|M(}c*#_K+4r^8uKaEazNqVZdt}2zk;O!h z!^?El_3x5}W(Nbc#jb09PqrI$8^Yf9z2ag7Lg|!E#u_Z_GAz{vN@qi(+2A|tH;GG6 z$>1l;^Hb_v)BkH2Ru(4TdwSdX^`!TFsqEk3o5sWK7ZOXuGLMn9HUUq5SiF@lnp)I+ z{jKFpoJPi5r^J78Qg;SQoo)|%{Tt+XFpC#-iC2ydlRt8Zka_-ezU2DvVc)dR|2Z`_ z;AoSdMzk8tycYW#>KB?W=HLA_4mKQQR>#)&lwjpH!od=rmn$?7rCO)(9~lqt9qS# zH|po}HPQow#vd?oO+Zs@`RenGzUGL{mf6kyx$(iS8_PM!FTv%+OyiXdh3C4z=I=$C zyw&^ubWN*^xS@pb$@ai@w_TP#>lE?$->qMMD(${UIsQY|{7bX_1D{Ta{f^lTCT(;G z6g}XYiZrmj8k_$4-=Ej|%Oi2}(4%qAP*_TX1AX5s>|;pS$BEFp-IwZ>FMfgSkBDB} z+5EH0*zaHd^9d5p5{12EgtZwTyekYVHx9dXH*BspjCr(Qc;=w^R@luemnIpP+6oWH zyAPi)AFjq+YC8u@3qPzs7uLmpshR)3yWhha@BH_G|6rEF6D3Et^b~N|2MH2Ho^aQzV>jr8=5r4a~;ij>K5d7pM6eP4*!C#MAl|1dpBVX zcHs?I^sxkek?Y(L#CZrD?*18ZcNhSyVdiN#!#nyTkAfW~kK?3gsksR5{3HY3yHq<}pmwFtzhRGLpQC#bqNOHSVVfgq zByS}#^0NisC|)~EU;^OD14_u7ue*Kci0-fbFO|M6}6`CY3A|J(of{qy$| zkO$|6F2YZP6lNDJ&N58D4{&fqX+Helo;$z(&T?AY!%5}NyXgP@tXg=$^Y%ZbLeKOA z`IIKK{B(73s(O=E8nm%0)9_Gt3~>tHkjQsp+Xa_nwS-sY?QY%2wv;WQ*5!sfk+x}O zP+u}Une!fxe4cs-thEU{!<;RA^rEA=5D(g}s`**Wb}U@g`zYYrYflQy*5#OW6$xhJiCI=L08){bF(BlQ z2znI&3Gem+rbci161zl}@n>c9*MOq&;p`QOSXLkm0p0I@?Qeyu74;R=yfWaEmGY~I zxHfu?h5zyE`GcQdqR*HIM7;|>HMvJWW0|Dz(%VZ=YSepZVsHMrUJ_U{*2{<3h4;D@ z_wJY)EfxbW#vdt|;`YZzy}dG@S9y}MIhOyN%O@IN+1$T5oy035phZ@~sknuz=Ble7 zreoo|)qDnhxnoQB1_5K_h3b=e^QVPpY!7YMJOlXchAE%ny(IKR?`SYTN?}yJzi{q%}Gh z{O^Kte{R`NkKq+f+i8U?!RdySeeI`vmA~^fs*L*IH!hr1)1PX?-MqE#nQD~DI!^TO zi~3z#Eyg5q08ndy%XexX#1m_DynHy0dvU3k(lSI7fY9(Y0I2fJ5Rhx>{1Jd7`=7DD zY<%pgM5PcHER3u0xd_GIhY23M8Ke7$TyObpw|L058Vy-?2;ucupvfy>GL&3Ma&Ab9 zWDoi$+L{2WVh_Rh<*tL7v?kTZIxd7%$1OQnKF!YCQ66D1xJ3>hfr?D9nbH#S+B`6~ z%n8wILvl4=@)QoidxXKMlUfkI3i868L)8W@RGIU6!oo`~T(TlL+~>C}<+vjtx#<2Nl`F#jNShJCRiDgkE11xepfRB( z7$Z~PbSWU{kD61LqDH5X9bU60Kj%YmwvV}pRckiWTG*O@xQX@<)ZIaq(TV%t)govD zlJ)~b0W6Q!{OZ0&`K2^;<2Z2u3M_5{kR6VRILFBlisR4SJs?*g8uZ57BA9mwD9;-H zja%dp$5X+OwL6xKX&O*3JV`2HVcLxOXsYa9DO}t)vQkRXjA@5` zROb>e?5y#iJLG-!!io-EO381s$L-{zn+_G{5;HNfck}tprh9N;${Di zp4@+!k!5y}@+cYj=gz^?^AE;|{x?J|14Rx$s*?qHK$Y_9-c+oA)V0y7!xjhAy(E4E zeTWukM6E9}$>!3}v5MSXYm0;{*`E+187$sfn4X`{QZ3XB1+=}fH7dtF{G{Nwp-lqR zE-n%0A^Au*`pSH@+X;Wcc3o$ch>|?1`y`dt%g>t=3b^exuZhumpqfFef@03mjh!@n z17TXwp&mpy<(Yft!oe)AyrVLsI`qtWxm2X&?nz9KdV#S<`9x5`N`O|k_c}L*gKvkz zg*u;P|?e#FCE<`07w#ri%*v#9M9%dex+Y_v`&IY z7QghnaX1?ke+qdOtn~_hD2k<78_rbz$3v5K6sunZOU)ofN(>9KC>jn<>OrE~u5x_7 zvjTRl;S9$Q^{LorzFFZ>OtWlUnCzecKTi%LDSH?I2@%cMi8(;r*^`_iT`)nQNUPW~NbSL=g zIWuFw%xmNJ74nrudMB{%_%%epd3~ZVm!TVd55ZKwi)AIX&@e`}JP8_R&|* zIHda@Y*`@Zy(embhPw0frvh*Nx#)+HJ!1ag($i~`;s5mtRl$3*Ya z)tZ?u+I_Y)puGCxEf;~pDS@{;{cH7OFX|MH#%yeEeChcCwIR7*7g-4ZoMCw3d6jXc z+>#5FQ-#xpxVycg_9D42&`j&PeOix-jpa#Ej0$OvSSD+d9nyqq3fo#QZjA&lJg&)E zjG-m=8rHc7i(7{-K1<|z?=_9tZ(+=RtJu^Rvm!jpT^W>eR15kZb@XWe3gQ@mpxpFc zpiR^fa&Vt-h)>@9ck$F1YNFs5fER`KnIxfSTIw_jyGy|1=qBXIyy;YGmBdG_YXBZ| z1V}@X0shCMVj5RILf0c>_@B^yk)1$&!y8mqu3Z-5#XaC(@W&wSclKuQiU3KTw{)|jJNNyLdc(dIQ>V zpJ0Gwc8B|yW_T=M-?G;?M-BM9_M!$WcGO*W!_Y9FGWpIvpn2 z8fB(Z?_fBEW^8m6ak`-)hwPC^6^nI)jEK7WL{{+MVnO?C&wcbhz$QR9QEgEV3rja9 zC}_kBp!7IWKgr2?wD~;gk|s4Mz0c8Dt5lM#l9uLSfv`3-3tH^mJQ_{+J?IX!D@oND zSD{)NFYvai?9_qe>+MmM6d^`iRYroYDwI}bCM3Du?bML3L(ZpdrPMZ7SdFMZk(*M{ zgC6U6<4!6rHCZqhO;B=aEsd6q;;`8x;GjlS&Isz1UOnx`F6-i7uzy@numk|U1(e+f z@Up0go$|fev0W+AWL5`nCS@JuEWxiZP1Q7=Y!r9H;vMB1`Yez%G{-rJU1z+ z$J2feiIFd%J;N#Gl{Q+E<_eEhnFAm_tgs4xK-?#PRc^F!O0O?GC#uRgU=}u;mdEol zg}P#l;kMjMs=iA|vpy=8&UX>C3e{VX;#o+{y*;FE(=eLtA$eV4@L#jAvAGa#SbM0V zt?xjCb}>z#fA=%&ZozDWTlBzBNq@n&kW=$4ovQp_nMu4 zWL=|h+j03x@#x~aR9uAJWtkc|p(J@B4&^|V+A#e=IPe6TCV>JeE>eOBMcy+wj>IzD@e!_Yn1BCsPELI}g) zOQ#2s;J&eRPnKKmV|q|vHp7%b7H7D{f+x_Bef$Gl+Qg->nifwaq%-Tz)gkmo)(^mB z!#}X43{{iet-Y9HISfsidQ={scgv${C%5E3Npp{9Ckxe+4&FCh|4^aLJ3*d0e7p58`q5-#}adu8;!?{Obx* zn*wr(13>lOJo8%fC2#h9Was5gV^+#37Owm|0HJ-#?k|8)28H{mo??{uSUK@5M?F9& z8)S~=7*ed=*mjM_(mC~>gpe72JyZ@`5RnBAB{SsER4^F?b)kU6IY_n)|3yv+lfp3s zFcYW85`fZyG#m?fq7CfB<|nl!DIpTM7>SbmK)YELFID`{cAM?phdLpJDL_A-zUac=qPZg|fAx;qLoGz-fr(Yfyc? zJS%+ANVA0X;u&FC)C?Mt}N7+h0dyTtbuFdTqC@DOxts||rM4H}Xkv}k^ zabZ9rsN0c)7=5)G6C&tK42ao(6|kXExu4o|6q)y*JoffwC?%T^A5P-gLX> zO^|U&8)wmdM+({P4mS$VzfVEzQ^aDaq5lI2=~8*8DA4S+UmGG@GL)aez{DSk*4Y%9 z@zbTmI|I6A~&862c#&Fqe*b8+bBA8g))O9Li}+Be21u zo)k_BK+u-qYKsa&aGZ!`;HT)CLrL1%R7ZA_^csg2nd;a>*Ife`+cM4pz?$MfY&L^D z#VN6vSYoIDKk}UKwVVbAGtEas zf3^UAjc)Q&fEOT_FHD4lX}^Wyd+&dF?jL%NYoB5i%W=9MU;4!1?`MZkm30IpgNS6< z^-w{vAfFX5=ne2@D~In`;$b;UqK z&Y2f)MZHGb>MIlVX=+xdhygqo+ipgf1ih9$ zeNo$Gf#nM=i)B757{n~gSVVfw5WC@iucQ3R25P&`4Ju58BtJ83@8y;0l8T(U>1Flq zgwEvM>L8D>07buz2=~o(B&Y4WJks}gXB#=5Jm@8l4#z3gOUd=3IqSiOx+vL2fH9Hs zxoZ)~l-)jRDI~lY3kRtjMLgz2zwPvRh=2mchp2LZUvkiIYY#Tzv95a#m%jz{idxZz z1UWTrqsM(edeQh}moK~aP`OkST@j3gH$d1Lz#|-DJ_O)wllkSP$U zwL~x>>sPq-9gc{@9SJfHjXT*%-<7ELtAn`NM>S@7+g_L?gHaSt)D+Jusebuysq_E1 z>lQ(ED-o3HA0C_QbQkDc?FCg~qC%F`vQ?z-y9nj2Mqytljl5*50BjU zblP$QwySyz;!bG?U-Gn>(P%e%MM}blP4YE0F#HTj^UtEcWS&m6#t>t8uG(Inzu1!Y zP(9s*oT$WNU+4ZAU`G+w1sRePjn=4?fS)UyCj_1F?&p4fO#y@(e=B%$UE=li@Z$gO zy#hF&p^8ndNsEJISEzC>i8m&IZb!$@x{A@?5Vu|S65kXWfUE%mVC! zv@Iat7ywmqx+{}G-v6b-%TN!eLRG;1o~jV|5I|%q=2pa$q=qCV+t7eD zfx1t8ja=Sx~GK_xtZ^ zjzdYlqF5uTF28C~gR{XrT0Q!c70tN|+8J7a6sDaN5_N6-`EJq>9~bi#wsCQ>#d!fY zAZSu4+KZGZNs%zBcvkBiC@QyAE$|MP@`l~McX9VUA^pUWphAp(aE-+x97`l|03Zkkbw)OLB#R8sqtNCl%tZv-^y8?`F#vf;>lR$IQLxho~79Q4t7#v zL0HJqFkmMKZm@{6Oi~<>Hh}A7zhhNpW|?*q^)@Uf(G<@HRPZ)AmBQ{Ns^RugN_ zLF-{BC`Ho8#noEuJo;IlweQzXLE9(^(u6qQ?Kp-lUyIrjRv1Hi?(R4~mMLlT5;+(CxbC+8r6?gh6Jk~8cd-Gy?0zKqSi(KPpX{3N^d-GQ#m)RisEDj1Mj+AR1VZZ7umMis7dAOTsN6Tfa+4xS%^;%rc=dZc}_{W?bFH>qf7}IZe9_ zTSBr@_j-1kk3cydL}ufp5Wcxus1`J27p)da7 z3%|k%6CXl@nuH%w;-Gng#%c0`}vW}w3E{`$`S$LiU04R-q?&*E| z_}d`IluaKC{C4e28(ZP}=hu56kW;#Ts^QWX#4xBBMI5_dpqmJW=4r8++U;lx1w8W8 z)+x(G-uuNdajJPKfqY?HZh0qEO+KAg>{gR5`2LC%vwp zV>>fQ${Y~4`xAtP9I&*neU?FG+UpyeWI5^gR;gv_EYFNx_&{KYMn`Wy^^SU9{M?E= zcmv^IJEL?celGo#z}9T{%(j)XNXZsEbSOBNJ-7tRJ9CcP%BnSf{HQ3v~-kyKZ!*%!a6RiB;|Ezx8`Zc3@FH|q;Q$wgPsqt6Sj}5nT{!U;z zGDAEDFD0yON9rn|!zlpnBsTXYG>2sT8s+H_11z_K?j13pL}SoG#Q73n9%-$C!HLJY(!IHj|95ckkvbtanA z7&?mGZdlZd6yPvaRS-|u#|U|abDmD8@tj0rPlN!um07#6J^)?dCOS#K(}@?SyReoy z)}tVB*)GdbQ0cbtG`ECZcy`JMr3gjEF)d~LCU&l5Wj5G=Mdc3IhY1^kPM>`@?RGtf z+ZyS1>RZ7X{F72{N5k9!=Ns>MKD$H3epaiT>J4R~4+4JRM}+NR;{_d8MZRmF7rpm% zJBr6AwdiN%V6ebZwS4aw6qAir4?mlwwE9FyZ?LXmGor?AFTw8IM3nN201VG)Nt(r= z&jLwJot?ryC(o(JW#|FC;+6|P!YTu}+St4?XbxFCTIWR@Adxei4U|R#V_gwuayw|~ z1*i<%>-hVOZ+N)XwL}@Da6$5DB3}Zzg!MI1u4q2KpegB192}yv4}d6SyFuDUiJh+8 z(c*1Dah$wBI(`H)7d@t?&s6>~lbzWvfdLSo}Uytdhm0yt_f?OA`LqT)|rQl}`6*?x_R)maUIO%T? z;s_PaNqnx_)A#kOSZbbV?!H&gNbp{#_DQ!3Csq{{Fxrt9#Id3?E)cEeNS&Drw^^Ou z4(e@=I9_rjv|6kOM<)iBCy|~r{-cXUhW0OMPqf>96R>RiB=nph}=#Z-yg0+L-X7_*6 zy&dJI?y(CN6{s<{^0(gtq}fTPYjOf)RVPT0L1KhlSH&}*c?YFfx141c6QhLTezX~e zH@`E`6UsNbOi3<-obR664Lx&RT~9n=i1T|Y+Qz*P1ISM^Tc)-n2JxO3sdd39(2>^L zWvEf{0&AEXNyKwhpj;HJ4-4f55B62apV%d}^vnr3^3ldi_QEYVWdKjnufN5=B`t}D!Pd`~Jo`%Pn|0}K5!1Y+_9O`8^jn798Lk-9c z4qnt0(FdF?mM)H`yo$TAtL~Kckktco!r31vqkufbjIzs?aMX_^hsq=!WnGM@j)~0z zZr47>7s>64r>mGag}6#LoYx{^`?hwi%C6a+FDeT+|0+?V@aps3*5Zqo8`)V-;W<2Q zr)moJf8{EtMUJ3(6_`L}!c6I3dXmbM(?CZyw-+=~p>d6ZkY*cL1P()vf;dV5Lm@^I z=wqCbY$*2hr}ie?xZQh5~7q?*`T zvxAa3fT0W3$Yq4z5enqDyo!_NZME5a*W`Z-1OL&Md^e%xQ|Fk`%`W!^LgFx0 zA-cIa)3y`3uk6$7M5?Ugno4ii7%KG@$1K^}f7KD6^;NOc$US{nO58fzW8eQJ=WZ-G z*jo?ogP?IAhXK6?f&!LhX4UZorr)W$h3p=&y<$s`Jwa?fVbeshvM5b|!}Nl;k-OV1 z?9=TdKl6L9)Z*TW{_bqZ6-i5IEjJc8zv=Vw)I;_QceQtUZHbqT5?o)>hY}8-iT~Wb zT!*W1_@Brdk+NWx#TKxl-0_v9gxR}n2_z>OYAGT(oXDF%AL$L$ChFOO@O|HwN_qftR_USEjgcMlNtEp6|j6we44DsxTqy37*e9_Q}SxNR6!4tj~@~B8c}Z^ zku@BPc=o)v!q;^Bti|>l?8=hu7T9~A?@aU9cETHR{PUyos4+H@?z)rOI_0e6By{dC zpF8w@<Q758}6nBnOIy>Dc=ZZgc2C$97#h}W5Ktui406S`_3nAYu*Z3lwe69Ch@b!*CobvI@+<)P?T*=;a&LF$cHu}sxO_n zOe`x_eoRvf8&M9cP$Z40Yo1k)2JT%-irEHln~$Tbg-pM`*=~Co-|p*(iD2o;UmnBx*}`7{;X43+aiAcSEvTD5fmOTtDoJrp|e5yWj2~SPMZF&b@C;YgFiU*eRyLoxK_5D zfs-Qv%3(p(nMu_mk=2=T;8}p z1sm$+-L`%H(N%2I$h&;ZIW=U=9QK|*Q|dC8i%z*JIl{;mvJE>R6puGT!_c*^Q$k@W$r<^ZkrxB3TYN=ZRLb&bUFfw$i( z7WV>+ZM88-7t#OX!*=z0(tG?3rJ&ntIVc;^w>5z)s8ib z6y0R@*(>0%9!50TPHNDW(?hB?RFgU)>ONi|iPz0|P&i*P20Rc^%vVRlwZfVh zz7JHjDP_?QL(*`LcbgR94_NsR0Kt8XHgkA*4j*ny6(p;}?G!lvC(O4481tWlZ199t zUx-Ew@$mxqP-yTzMkGD0nE{DokyJftt)dr||I);Mt81ZQ`5#)<%4lk1G_`z?60?wQ zXnCaNL5q7Tp=FE3eF7;dVr3CDHE2rDvQLSXXgqF2zCKWBb4_7*P2k0Og)ohgl8cIt zDQaJ+PXy$i!YvJL!!*Z0dUGF?7qPneT z-6b~p)m(!mT)}Zo+KV4XCa@YGs6RtS09?;7$jE?nMH zct$Tm-cZRQ7}{==#1*M^DVTDGF_EOJRdi!Rw~|tH16*2esJBaBxD}mOV@Ug@ggyFQ zeCgFT^o6rHkLvN1z#Ox#UUqQst5Sg}RscK?hh`Pmhp)B=4O2HR@S!g7tqm({`t!|E zPOxYq1GW75AO*ynR6b1vfgIhp9v)~I{lc0qSlm>e98VHI2}J{bo{5B@xx>($NyEq? z3PjQWv7O4XMZ87w^OwssHDsdx0M?1`M+cu}`y<}(pFsLyBSHHjN=%?2MfCnpfWGah z)=vO`Ct6Swlo9^SCt!I+$n4t#x~ebEp|kiIUyN1QV$%b$qKg1tVW2WJQJ2lt`2wVe zu$21%H24yzj~vCP7$)q2)Td~xi`4fp&goCM4NOpju-cjQ)cl~cUvWvvFNA(3!1g-D z_zgIN0eQZki?B)7b!&v)hz3VpAvFCAelulj+_^_itUe(k4dPc9=1yMe+8umSlpb;F zpzBFw^bfEYF>(AF5WFJDz*zmCqVtYRYXAQ@QxOos!EgfZjW}{;;NIcLl@>U1WaY}V z%nK;)foATyIMcFnWm;Ca(X1@Z)W#Rh%FN34xZUfI-#f^U$5uuZuSTF zx^x``9w>*O#1O-!0y*;CY_t1sqYK()mL`w<@fwZ}hgT`$E6{QUBRa?^)H++YWkfy% zsGR7iDbi7|0_8Q)cd*;-^6s2=*AWzQ1QmC2T8)l9Zi32X%d9_ZgbRVS36ka!SS}4Z zOw-&JN(D*BIoCOyREtXFst=ERzfJGnq-!pGRbhOLbm!tXMkp4eIWno%EIy9;Bw@!qvraEI(k%noudr_OR()^X9JZGM`s7o+2nUW7TsmD z%ndBY4B5)s43JD3T>>g1Y)RnbKxHGYg#}2h3aCa~Jg4TQZ!xNmwlLms)a)0^ZE(y~ z!P+z+W*n%q1*ES;8_kVi(Oi=ZkaqV7PQorr&Rccf35aJ&Ky)$K$lMlAj$4{h4~jQoJYNr&Qt<`Noe-QP;3qLFd}j+WR;`_71A zV4qw8kZ)(8%pO(k0xA=Cr5wC6ee2m>s9>y$@#c_XJOvOt&;l5bnzG*q$)N3!Rq65D zy=k_xQE6(XwT{O2V@ndjV|MS6FQW#cAEvbXDjRgYTXfAoT>i!4W8oPX5D%M{^4xBXZ+h_y$d;3!p0|7*}yM`EQ19b2VTb^c+B! z0m4)PB<)+e(IXP(G=3bYu>l~=0d)Qdu^RxW>L{s82;Tr0ZUf9r={h1uSq5Dp*B!F~ zkS&oc*hd2xi0}|(7Cnlp8d2}(THqZpNgUk;_UbYEj*-!Ag%S`sZN&GKqxv5q&c8U+5~OZqpf2t?21bJsc!?FBgc%{ifh$76{=!I%lg&)%5%g_dMF7K!e@<;WzWSWM<#jK8~QHAELi8Q;&{Q< zR*IR)(N5gpM9em5^WGyq@@9 z@Xa*7)?9JXRoci=&}8YBnD%K)w-9e4ccoD)laYG z+Um`*&Zt?hYAN`dh@ER#fqK!r2C@GsJG5=t~2ujiOvr0Q*Z<&BzpaEKLMh1!I zSk?BQL$nbN*#Z(@SwwV{4@#V4RVkraytC@w^g}Xx(>h1^5mXx>l4>Gyu4WaOwPCh~ zi+KgQOKhil6HzxwW^aL5i*K|O`LHQB)Ro?fP%{y=AeBWS<9OZM#M}aJ*brCS9Nx@r z%I|=63e1()M1GAZ+Z?~LN7hkD<3|Mxer|-AuW*m6f}*Ap`QPMuQ^y!-Eq}R$$Ibks z$~g$P-J_j?+{Bmy-M^ii3tQ_Qd~$Tc4CJ#3M(<-vWwH3%5i0VudT@Ay~F8do`wZxjJDJXEqz-Cb9at=VC3aL>mPL zAxQvs4iFDu zO3M&dOI1KNy}d%nf9QntOvrq7Hsl3*eIQZ`<5c7AI8IgeJjBfFjMDowf{#gvpCkkZ zZfn&st31Zxhaet~1h4VIi83p}&r!V9K!|qTAKH2Ye>VWRT@#-UBTp(B*>GyQ{;~m{ ziHfsnDpv~8=a(oqn#CZTySrj)eaM}>>O+yFRw=c#kfvhu4dw*KJpI!+jY5g1=hM3P zI_|Z&vE-8H;!1c0iAeuNQux?&DLU6S7pA4)9oH0d)|lQpAy2NXJ5+C44BB;z9qfMa{Jz@lzN3Gh6oZy><%mh^ZUXXG7 zd08Q>NVDYP0!&}KlQW!zY<8@<$e>TA|HmO&{(hGw6Kmf5jt0B ztm96Ht^1oP)QX|jBB0!BnvC|AxGGi8MTsxWHQ4TfdkUSj@EoZ<3y%)%?D|4@(2brw z?=lkWw=s+MW&r0Ir3PTV7^x6SzMR&UG(*wiz;3v^T9lDsU$;my153{9 zTl`$D4axmMXs3E&d_bvc1vNvcNx3%&N7Mh_}+fQVubPiESLKNWt z=e~RO92&ScV|EFmDxMP%=Y&Y-xg6vSrS#Cx5m1(RoKb-*z#2IcysdL6jr?4T5-!X~ zz=fkYg?K?EzWpa1_gV`Piq65d6P*LNeh9ChH0-E&=M8j?304`Tvo#}lDnY^;lP%{OCjj9C0e1=D+0BE7!%K5p4WRHjiMTAFYlkI4mIeCBzS3J zBww&v2ZkfB4h+gC{_A;T;dga&{qdpJHsN2;a4 zG9*H1uD}39!`q>B08-9IV0junVf{0ZQCV4~LDWWVr_7u`I}5Yv0GXa8LVvkSA=10S z>WgS^{lxZI;Vf6}k3j04GfP_Vizc;GHqf18)@Lb=7(r}u^GNG=6O<#0K4zUL@vmA_ zxHB9L@J!^hGc8?C`KZ|imBa_@`KfUiv_wUyBn;i$U+_< z9#yJh=IO#ls$9Cf< z`IUGxIEYbe2#%jG!`fmwjtR#bb93R5(H>}0lBBeyzurrz-Wb1RDOt|2StT1EfU zYvLvuR(6_Mlk~Q_KcwD@~bu0u^zdauOto|(eT(?u* z)|}>^R`RhJ$7dF|HwV`KEll`w)-=huLfh)jK4rC@D-QKXZh1G=oceh7s?0D4J474P z*(C&946{Iz4vLZRJ_}GZAQIWZ-mkbdjVn2~6_uZKAc>!jt))tz@2QigKI>)PM7hS@4HB6dhT|ivh9Chyk7>I2Ktej{zYq>Ck+r;H zN01wb3zKk@not5kj`&D#2Ot-&?Uw`! z41sBBwFT(i5AYx5efRM*(AGvygpcZJTQ{$&9vw2dUr>t}$n6-|jJsJjaor@6JScqQ zyt8)0xud|~RRM90?CYo-7zcBU4Bhs5nW0Ud)6-%YzH}$+bWlpewEMGp`(cqUv%U5w zMe3D|mcNLxdi_d&3wnEU6yVIp1hvDRiBJn-{kAZ!*LMu&h zOu^QpEAR7!=4EW@5;2V2^5G&8IV$|Hc~o|r_A$t;_V;}h*8|%_gcs;<=P|dWwzdx; z0U+8W5+H3AFPlVsH~T6f8tnX~^7Y^ws zuHr=He`-NqOUd{vV5K;EMy;Q%s6~<1sye+_J&J?3=*Id26S-II>mQsz`r{^Z@K)?MY4V)~e@FJ#E7abL2;8|L9_b0}4LY=C@I>>b&tLNc)ZdQF781gC$Ye3%&4RkuD&B ze8aH(C?cpES`q>(>KN6Hbcy&Khx)#)-wcxUZ-+}s1ZhXP>K)pMyocOH;ijtJB!mb; zB2D7@O#A@~Hki?E^7}j*yx^^Qwg-Gv0N=X*L87W|v6Vs}%JCKC{6AUODU3TCQM1mR zWisPn%)0D{IG_C!^t%PTWd(udtkskGT(AKIE zjZ6}uz6n22f@=giG>kfn=O1)y=~d8iFvz*Nw>Sq+0-u8`lYFD@{mu2y$bB>QdSa^m zrN%cuWm^kpXNzp-uZ>!JGxB=XqWnotS_DEB{Uhd_dXlYsWs769?se~skk5(7x50)lu^C#1Uhfz$4w2U!n*%&W z0j`Jr6B)2;L~(Anzx&2DwE6mNp^*!;I1N^qfC#g8JCoGI7a&CBq3OaYt20P;{usi+YIlQx|4z+>-q8(w#?@o zk7dL`?=e8NK#@Fxeo6*A3gH=IAhp@(A}Cmz2(oAfCCo#%$k18eI0v)$Nlci#8TG}L zSocq(i$t51o@V%p)oKpM`sZMzF4Q`EH_2BXfZzjPFgOY>+E#tdj>imWPLu*^?Fp; zbu{OJ&iaF@WZPP^J(YCqh7f5{;1$d@Z~{rGj51WUWq-3#_bvJ;m-=XA?M;Dy(Is(; zp5A2Q^+!yMf3rbSGfENojApX>I~_^bSKuv@c{U*TTYwtmBX*;ZMv>%Ku#z=svR6Wxr0o3M@+HAA&*4fAmXkK0+4{~4;`+g{02lYMgx=F_kESuj>aJs zfyZTWW$RR}r4e06CGA=HkpC8Rth6x&&3ez;HkJ-s+?#-tS*Q$Rg@gj^PlFHq*hJ}Q zW@Chhwg3h+5LF1((y=_+S5@{+q2e*s;bARxY}dzKWBwS~ZGfypiOs)4IUV4{Hjynm zs012RdhaV+c_y@2Ny?k1*&7fJCIZmYcW$qltXRIe(s{r;1)o93rg6|!%w}(rULlE) zMU(O#!B+yhvPTF%`+n7P^uJCXNGHk_5M^>%N_??wmjp7*Qfe1K1x!Uj6e58rC-iG! z=EJ&oh}d2L{Qf7|?=->+O-7yrn-M5BHpyDEQ9*BHD~W`fW|CKK+_}ryTH6j53>R5{ z+eo1L*>wHtu@jKo)c+DoPb}&)M*cj*@5x}@-EPs&rz;E1=C)obZWOvrUnIVJ1w3o& z3>nwDUah}b?llSC(^~FHdI#OV&45q55WWk5lQK@A#I`49KYT}0T~>wt$85N2j|AvP z?CLv!jE_M^^fm7tEYfP<^~7aa#gY9fyb@)u+8`4{#APUr9Q*s%G41O&1+=s~?A0Q+ z_CQL$?-jC8Ag8#B#x~U|@emflm|8;PKg|3^J-7#VuOc18CTLF*khg!O6f?3R1&Pv-sJc{Ow6XR?$~k}9z}5(T?O4B9sP9;|Jy8v{AMu)Y5hfy{c{84v2WT@ z^EDd-Yd1!U0TxV|(I0n$EafGM5F^(RulVdE6_V1_>=s&WH&Mx_$GUtTrNgn&UA@qz zGyxgx=)0zrtc=i$l6JsI(TE__YYCn?yblFYoV0uADDeV?GjoWR7L9xsd{AlK?ZZ?l)#9`#|<&@t79odzROc}UT@E2b2aucXP$HZPd`REF@h56Nknm$OA8ewT$-h!u8EGP9Bsq{KLFH7P0?oE9J{8slDiZ6Pv0^rH|~ zAn5a>)BFi0?C?y&`ktAmRv6l`JE}fohbbuKE1VxFvx}-;vlPu1O^8tF-1^F zFo6*7ovRQhlsU9YE2krh1cW%!-(M88)0<$vO!yiNThGC80TOgD8rBNi6wH^Fc||U6 zaIvcMx8zJiE(vlK$Q4I*@HN*DDqGS7Oh%2&=dTf=aL2aZ+}X|1aOF zfv2Zk%lA4&(Xoo%p8FUt97Rx5gp|_;a#M&NzA0DhhPiEb-p(OKmXm}4Hs)L({zrg@ zudW{+N`g+*d8juQ+N8)e-7ssu{f|(#wl2v>Jtf{=h^QG4&cY;!Cf~7-?Q=Nq@t@@P zxGb>drAi}j8&UfU;k$f($|mF10l%{A#Ag7}l;F+7p(Kv_MCOyXy5Z@%jb~pjqkjw0 zw}+An)z8VTk`MeSkP0V-hu8QW!1%w|I##p4+hKErNqq6OsmHKf-G!t)f|)yfvM}9B ztI7jdVSof6Xe1MMp-EXhF`a)ca?CsKZ?)7_TiT*5+5hyt`MbRHetnP!WH53-A z*yBr$N&WDl#jMO!-P!YInLyq&;*D>F%W0i&zo>8S*V)D_4tIK08{0AjjHVP`QMDnn zn62jZy@=2qesu>=4U9EbbK==zHv{rNtk@oh_uQ&-r<@pM4tujJ8&T_KZOL5A`UwPsN+Or%p#Kg_${spQrV{nohIvkuQW zeW|nBpHIx?E2c`Ew#lT&r%|O%W9L59e4j2|g@n{T{~i``wAIWtZOdAELP^fb{qM}E zQaUMQE6Xe|`m4($QGVeDCr`X-9Qz_*3+Cqw>jOLk= z_(Sy(D-U(5LJ|vkitf(I<7g*IlpByG=@bB=V9Y0&C(Q>Cay1r}P! zy3A9&{v|8kxPxrD;$t@bPhg?nkjk-}UseJY{mt$b7nyWyN8VLWjElhr$CpBRAyGT0 zzbTy#%ay8$YTIXj;6O{yv0=fx#Iwt#-=NWj z$Nvq_d0nKLnmVCu;j6l+b{ie-xqlwQza2CvpZflE$4jP7 z(QTdLj3UD3E&!yEOd1&Y6D9=68nDI@1W6BGOIa@?!1x++Rtn=T=8&r;2ajhuSFQGv*X?gks0+3R7liqlo zt6H|8NtgnjZti`uqgCKHx65FY9C#F+P;iFu%V?uSthGOs`Ca3S^{7)RI=8@4dZKag zOWO5IlG!95q$(zYmEGAu3&7~gvaidwBnc!TOpw}$5aFp2Os=%9k(h)C>2_?qt);Uk zw=gr3`=Wl)xzOU8qj%!ynA@!dxc#_{ZvqW;J`cbJOHN}Zy8$1lln$j4F^nZ1k^b+k z9>(2(dF^Mj2^>@CDRLxzpt=}En{|6@wcx93z4UhqDrA%ZTqT^=q@iMRfs0YI!P&Vn zGQ$iW7_!z?UEr>x)l=X-oQoe5jmtq+k%6j?@|B`-y);&#J7ZMAeseI;!yxr>a@`xFbkz(mS=a>+r_e2*>zV~xZ!v|=rD3+PsO$J$vY~PJ?)iE;< zIKss`-Fwn1Wz~b+&P&`+y#0-Ho4H{??TO!^fzg%9AbkILUKj3_Wt1A1xNFAUj~ z6zB%#%Gd`-vkfdjGAJ=nu1L~mpG%YZHT=IlU|WVlme^h0y>fSQ5=7q_=s8vVNO>?{ z=PheN6yJQla^6Cwv{$A@Gvai7k_o-oEHj_x8wO#?oO{F#|HC32E?Wg)_h*0rZ-lV+|b&N%&VS!YbZ zyP3g3o?CFlMiCu#>PfPun`?Lk@OVe8e-)3pifFN(i8}izZVF^nY-d&z1Gah>dTIq5 z%mT+gr1tb8^%}msg@=H^^}?ORjjDLIra{d)DX;WsVa8j}i7Qqb2x!@|e0o`;e-7a_ z_NJ!z=)hp{X@Xbfy{8p2-<618G)$`c>duTI4Zy(~mpF%?+hb_`tjBXXV#0lD0{m1>$gq{|fF#4kykn!d&|E}h}hd`8!S6)B7 zE)v59 z@&h#fenmk$V(+w*!3EhY zAX)7@Yi|{K=Wsh2)h(5}4YFZN@2Uo?hyf~MFo_1(=_k8N=L*4K9W;n62C8fWZP;Mb zZJ?7tD!?lGSycM7KJBM1_QA0_g+$oGn0@S;bkbUsO}9iZIgU6UW77?`0puFZMcOd( zHTVujTyP-~s?G+V9@Uk&dy8~4`o%6i4CJ_QPs0yH@1%)nTmI~#%b!$aGdA8fnM?yJ z78k_UP3`H%#ivZ}DYZ;BTRnMRFXI|Jv-`XJrHzDtdqegDi#I{VUHB}%o>G8?DZ@<3 z0uy*p(KS#gg)oVP&H{s=t`WZ(~m<5wMPla54vHg{_7_hgTs0yxK*&?+!vQ z>><$Nv9-;fvGCc)r<*Ogd7qR3A}_49?@lswOV8>n#$0wP2~5=(!CJY*k?u%wE|Sz< zusAGD;-U-j!$A4#Pa5+1%D8rA)!wR}O^0gJp) zn65{ml%zd8Q!FhB#$^+wg80xR82EU;E>WCjDhBDHK^a`IA2Vim_1-HAAUC$8cLpql z27ELJsI#Sp&{8&RDG8|DQmLSvTO3#+aRX-VON;Ybcd0)?-)XiA9cCa;ExPne7p1)} ztn!a-l0J1~JHX4Y&Kwh3F?}j9Fw|e`@G#7j))ew>%~@ch^vAK%Cs0x7mUWg&>7~Ko zPSB`1ivG`X!{EQh(#wKri8J`pJyD4d+Dm3&*Z_)+`jiSWPJKBJ|4&}Y|D^wV6ngH= z%g~mEvds`15yq>#jkz&Ni|xgFBWZgYoj#p15kC*ReJN4Bt>bQRNrs z&6_fvC_CN=bhFxLUQljybipH0$tz45Jp`Hu_>d64^6T;k>5K8wMGXX?Fyw_k& zkKJ*|ArgQQq6mC|j5rl62Eg{^+p{4d;^tJU_KwGpb~;;fOY6GHXtUT*mH^`W8Kh5y zN?;f&6rKhEWCs9IC=eaK*4Oz~r#YgZKyuEgVje4r^?+L{5(9|c&`WX91_yD2aO zvR>9&(-sXBKJL=iqHt_x&z@C9pNhCU45VRC?F3QL1B2<-y>N!AKenBbOVGDBOZ`Gk zEv(6?dlK^R7x_Fl#Y)@GFy{-CKTdVK76a#t{1;BSt-1`OT^n7XvB2zn3oKYZOg_Hs zurp~o&f_4u@$=7y{*z^|U9&;DmZXiN3X7`Y$x@erf5}EQjMG=I6S90G#AJZV||@~}!TX-lYz z5^XK4+6z)k*wUT=Fo^ZPIhhN8_ zlG0o%L1bEMSOQ2Aq@{F&S92hq0NpsIba-U1zkjvC{%ZdXpb{UX&X4+{Z!e|Y~w=TN@s zNU&0`5D@~*p1g5xr#-CvaA4V35Eee)@NFsajSDv_iuz)O8C^kocjMf44WmI7 zab{NycO8pvQVwI1b~?*VGF%^ak`(=qDF58stde*JWAOo7{`qZVYr_${-mWhzf5rB* z?R!ZEC!i{mHl^EA)D4rfF_qG6u(uFw#DrFDOIgpQqPC?%DZo?Hnhw-M|NF;ZqH-z- zfkr~0@-_g^mZXS4<^Zsb5KIBwp=?$~IBKU>>K=CAql5;2Z*f5K9VF5zdK;i306NNO zfB$-RZ$^xgGISpiN=_0ID1Z=t&X2T8O1G2%jXW%X2n5nS;F_#yP$hMsp}zC)$+#}j zwZ8{Uce2wjSotA!d*j-43LQBa0g%Y^I;NQ_E;+dIIJ>OIOq1Ur7uT%u$?Mm>!pS>a z#b6-xrqo{(qOQpho3*Nm=fS7{_dDC=OxNO4$-~L6tBR`IZORI+R%0Wazm>X_(z)mC zTn9a{o3?^SZls4E*pBG^|AU)<{KbB)@2j&k0Z-Xg(MSH*hV}URv7;~5mUF350w|!P zbaXWt9II0I?r?JhSFGqYcIl|x8mvKqeD_`F;Ks0#0Aj<2F7!!Jwt+}Cw2%m5Q=>Mc z&ICTyRVISW@&HM@fOyIs4Q3Px4Kkktdr`Dsyo_?9%<70iNHkC?sq*c7BwV6AqpNnv z-HgCG>5wFduPuZj4`l%yjL;x=X|Ov}vXMeEM5sN^GDB?WJ_fuZ=|qxE&tcDjw`;yu z;U*HNZC9vbg-c~bf9#Fz4Bg&L7f!}`Y#`-RoP)R0D6XT}^3<)ktoE%C;jU25jq&=G z@xO|*I@n70W2K_l=gyp|_y+ThH=7DizVSIu{oZ8BmE`c3{a#T5O99E|+YHJnpgh%R z;d&-%Nv3P^hSC}?C9!0x!k^jkoCAkY zWOCS0vH)_a1vvC)i8KS?$TK!y0_2jG;Y0vXunZysDA!^XyCiH45V?D1BB0tk0YpFp zA0+}p3^aRMmH|XPF7?o0%l)+8>b0&GJ6nj!^qI9Y;1VI&!BOJxk?aobIt_>hA?U=e zr+-bg=fO>zWkTOWPEzg17S$g9_ei1DO|j~Ayo~V+;-sP%sV<}Xx;Lr*z5n=QWeay6EZ?bjiRn0B;;x5hrIK5YEn+R?r>Ge~&l+Hg z{1walfy^fyT)`)T-E$51qiSDz@XJl5f(B6udDlX-Noip>{QZP| zx6T0Ng#gDjz~e4}^qj6lZb5eg$;to>5omphL354q@0NP<6lm>wwuE_NiwKl$J~1TJ zZez%J0iI{+gHnSO+UK?!x@FqCWvbEd7hY_|y1d_ca1=g|djI6g`}wl>Pu1U7Z*R?A zdB1Yt!+rJl&wM|;LcL$U|KWqlhdAx`|MUIuzbhY>d_Ua#@c!qA50B1&*tz{->GsF1 z4_lx9`|z*t`#qol4MZ&5BtHZLKqBS4;sEJ2Ie%iCI?>jQDSz^iR$%Bl{yv`=At z4t|nOTaE}y51#vG;9C-@-t>)Fh?Y4w58Fv)p0)RT{uQJkTYndlR1>!i z@25z66hhU7fCsY6@4sle)wH$$cmKne4Y?%1lUaZY1#I|PQw~t|0D0WMW5sDyau8?- z^MPh_U^T!jv>RL{zGtrkO<+{f4oQbHrC%YNtQoBmI)27B08DAt`VI02z%v-|?|-CD zF5mii4rp|DJ;e zF4H>|I=f)%_D|NTW0KZ6i}0o6mtW>SCvoE&O9-JUnqKL+kvW_G0;U=T6FQ(up!&SH zaazcX9twWgRFOVnZ#4XE^Lh2YYkF*GquJC)t~G6x40I56=1&H z{dV}tRN5!y_IEW(=|jimn4H(L+ODou#OWpu5Deae0hljTg`a`ovRI1W^IWjBh!Fjg z51y9SHb3-L)bw;XO$rk(#>;EZU{rO&qj&M`f8^p%-tO-i)mb#8e-@6)dFTPlFs^?X zXgQ9McU%BEA2lQBJ97!~rI>`9tGNr}~O!3fZNc-%eSMCf|3XLW~;CdZ`(UKWK z_iW+Jq`-03Jdzq+Vvf`=)V4sp%_*ydCwu7d;Hy!j+J3>yw#vrs0@6^sXH<#9fBq12 zE3f7?WWHxnN1Nk#pu4`yhMswIlW(OADNjD^;-l(8fWpOUEGViq;FIGfoQvHL>ooH% zooO;#VqKEcZH&7lmmcUpUfR3luVs>6E8kJ>bn!Q+r~DZ{9;vH3Q_$9x9TQ)1=Rbc~ zYsk0Au1oz@GspX92Hg`ZjDJ>Sbs9Erd zInrD6K2AnD9t>GK@jvWn1!W2_k5rY>QemU~FbA`{SD1tGqyZ;r4WIbR2)pi%wE~>$ zBuUzp{EGvQ#Z{+F3a#hx!7ob*_c)&V zc&^)G1MOf^#3(JIj5P1L7Q}s#aQAT~DjwIjU*`EGIg|Dsm?`)7AnaGz&Fg<_QiKt4 znFU6z+39bee{TQ5x6R_1aF=ITw9(xuev|eG3xZ=7+Z0dmUV8;oe6&=Xl>gfvwQ4-9 z`U;{;`2u5tgE~O+NfOmbOoWO$%YhKFDi|%4{4+v8nkGk12#{8L3Y2f9HL9))rM;Hu zmLgasFNm&#i; zjB|IknE4bVq%?^#DzEn`ulpNimnk2FU8KqMH<2)21;MEyNm@es2g`w`XQq&;X0QLq z$;HL~T0izpYiE9T%a3|iY#-mUGMJajefi{-@|$Mw6kn^pp>P8~eGlxXyRMdxR814< z`9|>`-k$o75JPj@HULyNrE-y>$`!iqdu2w&hz?a()^zl{_zwPdREFu{(=%eUPBV+0 zZnN*bczo4wf9T9WSc<`T_+3M5%9??0Tp$iHoUT3&;kec)G~PDR&*Pl!kj@SP%V|~R?NC6_2luO_ zCWD;`n+V06{_0$ezT40)Mzz0t+0z;wdg0yEGDlswH`ilbYOPl`9v!Xe+P|9SryYUj z$wscozZD&q=;WNV=FS{ab!DDQ(=@5%(FZ~`)#T}s$)IN{ca<8Mc3*a0JP2A2D{(sc z82zazCR~4&`+mi;<6Y!j`tRs{pgWApSOe`{?RFoP;!a||#5F~T?!JiJK2dkE*3js8 zc!|Pdu+sXgwg#DaTrj0YoEU$|2>qyRKy@|82w$A#GKmi%%8NTU7 zcd6i)$-~jVuRLsgB2z)6CIsb3EpK??0Ao)pgL6kSNUH}jU|>0aN!mM_2q7&2(x?rM zV2dgHJt|z~+1VUDCJ>QdI*LvqLM=Ofl^okp)}A9a;gBPj&xl-1!Z_WQKSyZ(4fXXe z?FB$%pR1>0EVTdoy=d8y3EM^KlAqh=77_#?Oa@(XR4mO{AKg(b6pzU32*<8ZyvH^2 ze3D)4JSIN9tEy;JO*AOH{?zJd)rm9ji=+Ql)*idz(s;PdKwCQT>Bc*J+1=p8%B)=n zM0rcRhRgf}4qRz@%`@!LqqyO~Swr?Ii@*+(di=GjGlKDFwKh3LQ`<*p*JorHcwFqd zv7eHuwth1Q*AsG4(a&$Pp3tZ#Joc#Csr3o><6=){Xi?n)Vd*(RRTg^cc-`;jf2eDa z!p!GKY_U%hPLJOx7@Ex)nJWQWfR5D^?aE=DC^ZaLuq?WHD(g!vXt)jOHUu@CA6eMo z?D>gQQ%v0h7-)8X8S!`+h0y+#uV}mJ+0MQOc%Z?HaLW_y_Sp{6>4>Zfh z)slo`-e|5oM_@24n9t}2YaXpAMqQ>SocYIbn1a_ZZ1~y0nYv!`#uV+jcdiwDQ-fSD4$orbWH=Duj+5J}so?SItPF(JgcXvx^ z+uF4}zxeKZ64f8G8DpoyIN*QmwnN^fBXyqQH}(BDwrXCbUuyR0%}&0t)M&IJ%rlC} zOS}L6(cmeEh?y!6ix;nc-2B$LcaFCG0;g&!CGB$Xi@B;f_zL{a@ho~q%QKP;?}u=C z>IA-&EygOupf;aqW>;tk2S5Osf_8=*sq2f#Jk|_>@tB3}G;s6@{ zsP4Mj5*GV!I>_Ac=D=u1{wQ377Dd#*Yj^ToPiUjxQ(?eg5oYZ`Ac z1f={O{eiVjBdmw&h5UZiSc>3NSfos}^I&mL({EssADHfWu(dWC3w zd#UpNNabsJ*I%yFn8tRqj=)`1-4c2?rSPwwv_4RTb61LCc6# z*|3aK)lO)bn0I-+L9KHKI^|K(6}bCtYUvGqlfdZ9L!`!)Qx&fR8t2y9Ui_mSx&SiR z_L|Q$X{$B)P&{?w%ax>Y+H*7_>zUc}4yEL9H;6*_C*|&0(Vpqnk{>PlvW-)qm%^%? zp8F+}vqHh?)S8l;CGlIko_7ngZi0*%lr#l;NwHfSn>)??8>eDTD5f20oh1M4VI>a6 zg^%2)m{RDno|Imz4sJ}bRnXeyzX*A)=5k;W$I_Z>zy#UHD5m&DEm)!YtZv*%C69zUv;|=3B4Qq$=&SqiLR9K#X6< zjF7T!rUUS%Cs^ERbC-cbrs$b>>`LdsPTeO?c|*V6tLdA!lXa^y$DLOYo;c;RuwHVq z(QO7GTNml0(N{t~MgQxo{@?x+0coWw>pj3ZwgH8!ngM3e@KdR5*$j@fNES%DR@mb% zBi^a~&hOh*IjyhQf3hy;l{deFM8J1&+cTA23oHyppgm$k2C*09F)U@tAAf4t;EcYx zX<*b1G7?GLNOG`^CU6pqBE~SO{c}7f@&8$QYXO)r zVaQX#Qe*)ydfTr%`tC||@JuH!SoCD&H&p3XD+}P#7wr{beb=Xd>2@b?9|g)-spv_p z*cUf3>LK;+i`Rt-mk&AIu&&PcF9$}W74rok(}&y_CDp$Z3i8mnZO?!dnH|KeULg33 z{3nC`>+$!VfA!&xT}8qRXYiEcto&TKYNbz-)b^0l68@lo1O zI()yNftr#L9y8a~9k00rkQQ;|nE4W1?!$Lx9U#IO)!f{Xe}{^OSn>@*z=i;xfrwq{ z!FPsPdd8t$CH||(++g9&AU*!!-S*<{I>GH-Ju8pvpalN=-6ydVn}v8PTM+0nTB zX$}4b5UV)=aM0*FknJ{V{KWI_D4oXyK-tDT{M)WEn@7siJ2hbxs0I-fCgACAa6GLg zBuuHtE6d8?8k8_z65=kC3EpkZMBsD6<}QcPq=Hh7{ZlG@mU5p^Dnlqd9|~_DTej&r z#H(Ai;WDKDxU8y`UsN%ZRDIb;B=#Gw@LYlfmhg0a{R293&+}xJ-ec}pLbu?OX!437 z^m;gQkL$G0s1MQA27IS=zDkJTYY?x&Mn|y!fVVa z$is!LgG35ls6(L?trdwT1d*f;mke6@hDOpeNh_6EKD#qSzv}eMSs$s%?v1lgx=i=z zE9O%Wld2kgMEAwPm(SQeNNUkDJ^Gxix3Trkh`Pe_%q^nZhMQqr+Z4->yKV*&0boDb z1PbVbG0#d!Ek$x59sAN54+NVe=?>I)+QHq<=+Bou7)kt!)r+#$T=$F{7xbZ#0*Kaj zjz2A@W|rrVwy+9!N{q3q>o@$N${mw{h2z-jAhs-};^(d5?*(0lC_H!fT;~GdbrJ@k zzIJ+9i#QUjOqY*gK>g6reSh?fA`b7jp_@7)ERHA3~~m7usQ*|Mrv zAf4Hzs*_p=mU1OS3CB5bDCEqjh0ft;1w2YYBVa#6A97AlyLx%nWZ;cAtBUg@pnO3uuVqu?yt@nIoMVJfdQXLHH!LgpexW2-J(o?T5z~ zI=>+|;8f%7%FN6WD_ZZflfLXz6$hJ|f<`O$EQzh_hS;t!J(L;83 z?jG=Dfb`iM+)uXL+z!BA0Off@$ai_3aj~9EbjB6PaFp`xzPgh-NsvNwOFWlUdqWk? zk)f2!FgdkmY}g>5<7uQ?R-deG=Knj?Y33~^TLk1V?}cy4Iug@+YVhZ+yu zY`{231#EB#3=m-n98-|1#s(@eb66c*ZEGl94A>0=Yi$E{limh|fHVq#h8sXELz2MH zqeuV{?i>$mAa$E3L5dUpkEM72XS#9x|99SO!^Sq7Q`-!~Foz_hZRVIX=VO{Vgj5Kr zOSR1*XLCrBY7Ui78c{lLP9YUSQn^MaNxh_Wb-FH}y}!5bKk)qFd3)ZThsX2rxZh>M zO;YG+pvZs@4pfROQe9q|BSes`Q{4aJIVlM|BY1uG5 zeQ(8T?^#KkU~}%GI3*WT7;j$aBT89csZtZBY`|PNS6J-b*7`HHxOArWR(6fQP3^<$ zDU+v*V;eu$0?4wrDCV-(ImGT-rMNl*aToET1oQIqg<_|L!e7rniCVwkp1$#R3N@w_ zmyRQax`nwLif1p}7YYA~`&k{BggRIFV>|Xjw?s$7Z%P54Q|3%g)sqeYJqOW(ZE*O;~0kz>3^UcXSIj@Whos zl8bL%z@W&y_2;4`lu-xvW`WHR;P$5cgJwhPX*=$JT?5~~sB5R!^t}$K35YHo1DIh% z`T`;2FUB@-eheMjJeF&`{P!9jc<|WWis-(W0SCj?{73+dpAF$pF9h;nUbh+~uq{UI zWtk=-k_r^voljSa*grrhiLRkzzO_>5Oa+os#csTUXfKdPp@>b)<6^*CiMd<}kd#|DB+2}Z%Z2yQp2t0w$zsXUPXmb$^&IC`$5Rby*z==e8^_c)b!l59K`^S)2bIV%G3 zUl3RFRws*KNr*SIeM>Zrr&xsV z|2q51Xq#EYzHI4dm1wf0zR9hxj)dWThoH_F8k^dO^&r#7%lPV`$4Rp?9(9P>)#+tMDZ4B)n{$|PO~-Izh3OY|_!Ua%`akjytox5sH__Bz(d@&x*uwx2%2{jnr36 z`mc+vo5XE3d>$9L)g*pBEYNT@4WSb%tSRux@$W)J$BM870Y4AoA9twmw$?UC8M~T_ zan7Gs{G7_va{iVMoZ42BY7@vURtCep55+a?ji!Dy4%oIxb@1?@=I2#M9Yv%4bbQUbm6&evbB-+93Gr9cTci^1OZvsmuQRm8$YR5-H}5LP3zEFq=6NtRwl>w~oMfX}nD>I@c-E>yEy)o%1S9B_w3Nmi z_-SN#bs1XKB0?4CaSUF{tsBbN67185?gT}-Mc7*u&CT~N?|Z)6^ax+)=8zZCK`T&; zOEV}6d+IYztR)-MeZTm)`wnbj>Wkw_x@qLISn$hb3zz8=2Sv@A{iaSwE&_;uF*1rGFHN9Vpiw9`6vNEHEj$;ZxK@R@tfoi8e$j4-1q_#Y)`VN``ra*WQQ%7_4bJ(P9 z3qTFx7>M}>j~<=|of?%s)5bO8T)KPcg1&oyRV5$osG6+mh(N~?+evr$J1%GVqa&tG zLN@+_?%LZjHS?#*JAo0LygfH!?)%i*`p)jTvp%RRg4Rk*2#S+9p;o{55VT@vYpz@A z^{UEakeaF(AA|1E!Bea}qvBN5ePqB@iLj51tt`6l!ogJGVCEUpUjHAQs??&;YQ3Qo z=IZC#n!T2&gy+K%2Rk0N9?QG^Gda~3+grK)^GXa=Z;RFjHfqi~-5h8k7~R1MGp0p$F-17xt7 zmjc_PJo>ii3S2wP)|tzEdFfp6^&v^V74M2_Cr%~aLwq#yVk7-q1XE2ihiEuP$?qsZYBT&0~+}oNd?; z)sBjeIskQu+q$cS1YUdmsjBL7i~hTiaHRGAq!ay!O?E#yKSS*Atyn-}qGMByeLj!- zG#p`9;xwF`yMtUA4D6>e#h`^&c6S!d`iA`6ge&MY(ZSQ z9DfSwU)Y4bFX^XOt^$%8S~u?k{3x-*x)P1F{b@Yi|+OIAqxygykrxsa3br zUJVb`vrz>IfBvRNZL=YVXz~>#1lR_h+h#D_?7~M3*DCO8yKj0`bq?OcN8LZ5+4pGR z_P@Hjrz5q;K;|w!RWDzhNx zwmZj>-;83%6$D80NPPJPll-doAaa(wAWnA^^Y@ zX50~ig8*n5K%*O=f$lLAa`c!$6b;bYKG<4WjNPqiqrwQj5nKP@Sbqcku6**dbw`*2 z^!rth>#v>|DA;nakkK$x$?_?Cxx><><)6Xg&h_N`ZxNj%9vWJqC0;GN`uMvi%53d( z8RXm)eD7{bu@#-amp%NizrH$Et%cQc_v}_=@)LEBm)03iN?yi+0{j{F$~zZ=)7Msg z?gVRUI_Y;Tx0KOTk5=||J_@ftH0E2SFPM(=kq$LBa zP-?y{dnoNacduxJRmI*smgT%WGwO8~UxDdng~(6f-Ut^8MeDdAn>!ra+2^!3C-&zR z?e_&qv4x+0yzWqX-R56-xDKbWw{%Q^e7@ISd&R%}=S|YTpnV%(_)dxQ%4^F8TQ8pF zmt<&enXSERrcD32V{1o6XRH-#yc_mhpq~G6qjdL>|BUsV1|E35 z6C|>y4$SRopzKxlKYsf#K}ZehjMqIkSKqN!m*B4BD$oNa94meqD;D4h)HCVslLzJk z5a)D$#R#bL>u$xLUNJUG7)U67s_$_w4i+B&Ej}J{I*A6_WgpeF_GX~=`8!~%(2()a z(l;9JC_I4l12EvbKYL{0H0oAOH1vTdei~}V<*UhA8q4zkT@dLm!|D3DuV~FjIzf$F zL3T5ubz$#?@@R`z#nyb_enX%}&U;h~>+?6u}3v?#6H9Zk+%mC-4$Z-aA|S*_hp`J?SO*+rY}$b+OC7ZEi~cebhx0 zhnKg%Oqq6+0T2Mt4;HPi$LvQiJ`cb#n@H&Z16vTm?a92f!sWBVI(7E`U$dAx7K9bF z_~GKIE1s@BN0+ddPOCRtQ9qvg5WPei6yA~nCs3`_YAovung!E81{5|QimErb z+>B2Yf|x=O0Zc_TG&KF%o+&SBg#K zAgjV2nvjFpg!Gvu_=YU>3Qm4sNJ&#W-s9a+(lEwcI_Yl+#@gcSGCEdU{?+*r0nOV6y8J^;EkKGC^DG;*7D$-1RiR>#G>6&y(GidR_75 z#)3yNj|6}dJ6#25Bbt(f_Ei7JdG{Ume$AYvS?EI}U((8B)gE}QgaCm0tc#zYHovms zir!z2HbTd-Adt!T;YUw(iD@cqZ#Ks^{lWSfwuMraWB|Vm&weqnwtlVR{mLPAa=#+m z&6IVkA%K>Rx@dh7s(WXytpc2473{w4TOr_Z$hJG!MDrZ+ooMmK-kx8> z*j~#N4g=j8K16Vj6?KygRz_08XdUZP39`KF3 zZLjaXJrNOmqhoRU3b<@5geg?xvk84QW^clmYT%CbLS0D2dhvr(L($7ruXH23Pf9=7 zLis)#3&zqt8bx})77Wm+187XM@vEJ>lGq*x#`cNwuCq{KEDRZ0To0;)0|WNQ_)U0&NC zBhp9K=Ab@*Mgj`M3>f*^@6iT&eS+~4gV0mjp4^9p$EmH`J>+S3YfhapJT;gmB6J2^ zIr-tK>-O!*ieU;>2t3ucb7W!|x=s0sQh(*@gZ_kPHmy&;Jhi=Lu7a^}e$(n3aTm=( zA6nkvsmIT}qJ}knxFaH>mxG6fjV~1f5`}mg$IMau&I_pF_|@#|05IjN@|%X^N&#GU zZ7JGFQwb1l_5^TQ8m%JYOGAP_5b@3L_z9sw*m)7=D-Bn;w9%rF&+fj*3dxqkX!3n@ zIn1$Mt&nwGo%Mx$4GXbz&Jr%p-uNNrLv{wcYNY(B()_Qg$5v(QK(nAOJ*Ig*>(?AEsbkWfMmgR&|11_8{Fi5wdV2 zb1BEHQ)1OpP95yAY$<0&im5M^yT0@o{^6Lk$|))Yag9Q)kNwYr!kRCJN_*&Dx%LZU z`?9czrHW^wDIKWceKm`jJglbNPn)D9B;e**U$et z0thTV0L1;q+Gk!>#Z*O}26{gjLSXfC8_cfxew2}alh<5nu1F~dg3uCx8*5x`q@ZNB`|)7hPX#V!B@CoNA#(?Cu7yPR!35hmrR0=H zk-M(rVN60zjNs>-e`Ep4L&t8GwU(giP(Re1PZ(zwV5lLo7_39`v>4+Uk|@GN3q+|E zZLJ7#@s&HtCC@DSbek{PFFFoM&(Vj~(B~JtE?%hU#VG z_YZF>|2~iQym^4NFx_ykZn1gSDe8?1o1n#9gB@L`%w9OvBd5&r`1KC-BgW(N%}$H- zr0F`I?iA*{d^xdY=Lzbt*~weKyuxy>F8m{H7zgB<9Yr=7(MvQU^4?s210`A1(2Eh5 z%QANFqC7~k`gqAAJ*y=2`b^a&?9F$HZl(81XFp!Orhy@gG)4huYaFKJZbe8KMqPNa zzbZwDKo`nIXl5@E{@Ag4jjBpq5iI6-n~5@aBwDC|sNH%DfHD36^0j>SBpEx-{*k$9 zyG+PGUDHYRn#)z!=L-QHWTr@ju;nX+A6hTT5ynfYG_-}lOSP349Il6a<$d@#emi;W zZQYKbu{U?Oxr&UB#osBE=X)Baovq#s=zfjWfpv3np&Zd$EoT#(w^q?s`7%NTeIr$a zI+L1E?X{T5F5NJjZK7i_is{uzp7}x5Vz3(DskuiK2{Z$F?6+GbK5VuWXL?+Jr$%MH>b9K0f4zw7-W~hMx1zC zj0_!&y_#)IkTp<@CJquG&h%fgX0dlz=k|0jjvo7KRiU0KMGzgbB+ZVB7}wh%Uv#>5 zd67G|c=?#@rM!msm>UgObrPn>tla4>5jB0@gTHu)+Eid%J|--bG4GT2f85kD&>%k@ zY6@}3@)G0eyPLF1*rk-Ci1>9x!H~z|N7O<(F^~m=`5NY%lz8>(OamE*4nfw8@)5=! z*KO_08F`i|7fwudo~yn7X2}BeoD?-bj@UG!KzcgY6CYEBaGKBn*$SeI6@_dx2&EyP z;hG@<;GhQpop=^BN&@OXwR?R)9UF#|JB-ULnbptr!TNw)<8K98r95|wh{odkP|v+t z#0CSgo4#hY+)VeD+a_RL$=J;2X?^qJ? zQKBYe_Nm&7Ob`?+%N6~HL0Y59$gBn3FPu3fSe=9SXr5(C5(_N3RAMl*2Y*-s-<(}c z2$uEe!Px4iIc$tzIp403g-G(HqMUz#%vYL91b5~GZmyC2);Tto_`Kw3698bF0&=c@ zN=!)qVE(%A9%A@EINj)~>N7m;YR%hI zTxoi~bZAC$4U~7_;N~kH}wQHFw{ebGg%jJN~7n`8F`E>q@!U zx;MSSzvOVAJtOACjcPh=%UH`6Aob7>(3;f_mFXNOCQnp(8}0N~&8hf-uhR7kYb|;gYHD zEx8B`QD%fqCR;vkE{XXA;(RIpcJ*~NGK>oZm=U?=93VjFB?Sl?W!;-=E_%oQ^Ka*)N~gi^1WB;z9A2_ZVCm0gQu=i>Fn zpQ^$Xe;;c^JBfzX*@zx17c9bn#>TqSIpn<5LbAPB=R{%-Myj$Ge`pHfO&90=Ggx_| ztG^^TutxKpjCAv-iFlbw}dFjq6aRM5{ zc++&Iz(hK`ReCFVe2@dMg#(<(Sf56mE=7kGgD@xCMo0`GP4PHsh}&k^Vp8{3^A9KfX`!R4^X@3Z*QE%RA&98nT;*@?2|x(5I?jKQQl zfZmZ~KLfx>DaZuqiiY6!h~}1;u@!Tw0tnGpq;6%bH7dnPq);heJ6eR8B@@1rv07|= zd5mwpNXvr@#my0V_-b)|L|ryoJVzAF=^zJ@4s5?Yb1JEBw~UOs&L<8FF^qVk3ia=> zL`)WGsTj^7K+iurwU)`cutrT;8?J+|HY3OBH)@F~SOl9$RA6~AIEjdmA~eY4<5c{% zDj?05MKg)p&PEsLT(EQBS>?`A8OQdCGWAvz1Fq1B=3 zp~C%=DGDLlf%5g75az@GWObI9a22t~eGTQK$@n(ZqyGWDAjOPqiz{b@j|-1=3{hy} z0Rr>Uul@*G1!X`@fEEug#`lhsAI;a!x zgcH@o9G1{WjdkrAD(D~bOE>aSog7Z}f|~7fcJ)Ey(*Y}%N^0UarTQq@x;)|C;YI?& z+!nwB&q#58A_7Z9WC3A@%4G8E1dtn`%>-0+QFRf4T>&qy`E}q9Bgv>MWp0uJ+trY*LCym)nS)A**RnbY1L8 zH6k5}h(yqsEo>y!H4+PB+)e=v4{gz{7ZLs_-Hs>-#XyozC&8!YovKuQ@jRimk(4|~ zR6#R)e6)6pbcj}(m2)~0;HHH*?y)(84@}LmUP7tp9wQzvkrL;}2q_d21INu*sII{^ zzx`}tw#Z}CB~l(+bqHd{TyctGIRzL{+PIwENc>|@ssh^od!9(XY{~1eXy98+P%94S z+fMeJ4!aOd5KyEmTV}*4v2|49yPm^udu~xzQ+w$5xr}#1YOQES*aZuRC6v|`OX*6m zacYn?I`pP8{J)XN0p+khkD()A7>w49mzwzRwdg1s_}*XHI$l?d4)a<+<`-<9nrUI8 zjVxyTB8rEL$&8zvozuY<4y&h4_JwLlLhn&`zDe3SlSyGpwEiU7L5npujoN7OVZL@+ zz7oPaDT-Yd6PRpUxUV^iwWk;iFOW~&N(CgyfREMB5&^JO@}2;7yHxcxc-U*i!AH~Q zFsL3rB3cQHmZt~E!Bn9Wmk)-#2B>hJF3Pm@efwO=NL?EpABt8s8R%i;(&ht6=URV5 z&MMV8RTSMlq61Ei+EyaXpO3OMxUN+0v{plAtnNKC}T(ZGKwAPn=)cDolu-N zkWDL8U3(GYvixX{J?WjpIPIHGeat~xONu|6(9%Z8sUb)jiS@LS$FbfeK+pOw#Hm=0 z`XuK{=a%1|2W^jE%Mel2`U&5fQht9uoWd?BR^Xz6>Fq%T@;?F7pEDP@guJ=K%14|p z{)Cj6%nUZ+v#B+&acNJB#=mw1@_x+*a%k#Xn%%LA9Fw(Go@U5HGxLm&;E(Y^Cv{9S zjv2I$R}3~)^iWs(dLBjuukPVk9yYdsmZ7Aq>O6dCnOc~%!$KDmp=h${l84`(A1f19BidWXni8hr)TXcsmC0Q`-to|FaI!wt@ zejS94i@d;&#UPRkcW$+f7D72>a4M^iB!~JDaI86E zVHOO>0(54(Lfs%OSk8Bl=1}e{F+?21UfXrd^T47{#Ybeynj@~3t)0%+{n2zpV?SrD zmgKUf7HMxP1= zsa$kT{w?!z(6L=Ho|TFtVm663MsT+!IC=%lhn44U953}qdwu1S;P|W4wbu0xb!8@% zrkR%#Ilup{p$Ltlt3e|WE1Ei~ocL|VH{=Wh5%hkH$m-pyZi0)ODvM{eZj}~V?gV>GL0lGL0qSs}&-orD)I|1cNjtK0n?4Tafo#yuVqo$rYIL z(+KzDSH(Zs72t4fCyGvLdzR_MmW@mTU2!dgt>rkr`?We=(3r9lYOkqt?BAej#YE;v zugjnFDSB^+DbIa2CT`JXW3*z3z7$O#8!ckFjsqJr`WolkNSbKZ>XYJVUvMht9bH7! zZOOF41=iOdtgi)=BG=0QH+pT=?nLz zu1pWEpfeS#g-9v6!t0pWF1VTP6u9qP^Up$a(U9m>SLpch3a1Fm*-*=b0TBFCGn<{e zHiR6_1^>3oYkpv@?*_mr)p^x~EDH#c48bg(8=H5zko#Qj@#4+^z?KC{-zNyPEGp@R z2m>#O?7%ovh(}h@Ci*1{`SY;RVmM#r=WIeK0f`c5)+mGn*lbHiE-MMTUx?{IbrYrY zblp^kk4E$DwObaQ;_r2T15P~o{c@KXMJLKrr^Rp4Dt^v|4K)Xn7;I<<-=j=a^DkNR z^c*p<(a-dmhg)aomcKj?&$+2+-BlvbT@%F9b6&?p1fRc%DQsx1P_0f*I?Gq95rSR{ z3E~*^IhM|>6p}&KEsVj|kUcOWZGDlNL!WwrLNPj#`Omd};FJVfd;;iD)%b7XyyL9v z)ex_}2R7>hH{n-r{ljS5aQgN}<7YP;sLN-z<0ITB8RjFeUONY_os52c>U87EA?H7% zBgaMu0LEWi6*@0(n?;_#&P4_Qa=M(X90_ zATC|u`M2EJBX+8=7?LrU@>6iD>-T1h)+fJnWWeXUOhv>&u;)d1O_GSnh=E*|k_Lp3 zVI}FXp1Kwg*91_b$81?)Y3d)*UQwzuR8M|3W>%;LfUfKi>6o*%h%wjy7OFESBq{}K zzWpGN4bAAwJ-?ts6@dVAI94MJxdCM*f0m?Bs{@dx`A}7eQ6(f@m*3TAbauU1yY#Yb z^Ou>VD+a%y7k{prms9rF$2W#J&H%n78C#AVe6T$zZGA`@hcYMtzRhR+2|x9}-=~c~ z>_GpKf4n{W`bh83{DC|u+EDV}k>FLp{0PZ16sc>Uoq#@W6Wf{iGksJ_FvVKvW`NG} z2gFfrsdJo4sarwRIFI4Atn4fH3y5Z9uf%iddL<(n2l~C6-79t7>z{gzT#fC`P}dvy zm^dDD|8P+ZeEr2Bs5tqAlY?g-*w6+I0AN@br`vfznI2O$2?oIsOuj@U0s@m%;1kRw z3#RQD{YI`8kI=xZE(k6Q)Z%30gv!w74^30C5MVomC$G(lBfrIy>1cK4VjOe2>zr0w0?5#%p;U>W&G z;!cnf8}o?g_*f6(w&|v$_y>OkICJv_GS{RTeYS)cHA3BLVL38?Qk_k%-ohTXABUPm zHpG+~1`YNE1$z9OHR1pFr@`0?*Z&Z-idX94BN7k1yO&1!>X&|EV6?xduD=IP+iPQzy~l}-nL(q(L-eM#!G=RQSZy3efzx*PKQQrt zx>TB`;CMCBL>HkL73dn}qA=-b4BR%%B& zvHf+6l^^oz{gV{^=R!v#j#87pDWJ}zPs!)B9q<#L>)d4#t-idFAnj|t5jZvaru9{W z`XLKiFY~;=XuY()vy=c0=WUeqnVcWx6yd3P@?5wYg1qNW{Gs)Lpv19NLd1#&h7B>1 zr}n`~UKoYS`XW)lu%*Fl1n4B6t6@T0QYh2_5;@E;jxPt2FsT5r22mJt-}4ds2Xd(60Glx7!DnaGuBthlm+2)m}RG zQ{zFEvp=T{B$wCT8RWY>-t(7)MF>Xc-x?IO_`82jbH}5SLtb zJRm03eE73En#=b5{#@tdAEKdX%+uiQ`7g%A$NSgZ4L;|N-P1c%cL1YPB42|Ni~09A z1x=j%;IeR*owL2E`WIl~j#Ct6?XTtA(I;~7=B*3X>c$tUe~xhPz0dR(?v!$Lk^YGH zYJGvb4@QGN$~N)an`xt$+&ufZ_n_xuw%l~;`?CPp{LhIBR3^Re50Y~)>d}*fH_h{Y zsrX*5tg{E7y2V`8qTdyJT{PYmFnivrDc@_k#k439tlPP2x5v`TY)&f_G|IOM%=SmR z42obxfc1gOe#Xv5oe0)HI05;53lH2EHV)d_67~El52=wKH z;Ds%BN9RG>0w2$EF|hHw{N)pDAY@K?WKQfO}eMJRoUAo{J?`BG`RRj_nc;yW+Ic*w!+TkmtdqvtY3k9IUA<-?9K( zV%5i%lG)qU9NGCcL^rfM(cLn!a#;Vvg3kILy62|kW`lmeM7Lj1%R)Q!%hlGbP3Fb> z3(`uBD!i+>JBn7T?QP4*R= zL-z+iXQ@@@{5~0|yEt={!{`aVFnO$%$EhsE#k92)56UvMicWJmntNGpsj^Aj87SQB zKHVr>W4Rz8h^ke@hv?SGLBu$D#b1y(^2gu@5=B1*?I;8x0JA_Q3#9eK3J3y3gS_OB zk4QF5-v~q)+yKE~PMG{}OP%^1cPm<~9B#5K1k0pAb+Yj8L=Rx=Ju)%?0j3)^Anf~C z01!Q%oHnHwJW%85*$8D+5Q?`+KsePHTjeMy$@#^>KNQZSJEg(>Q_d3@{SnOJwc64+ zj1lV5AMu7qJfw$=jLnn%-wZs@8Z!k?S>_UYgav?q`6#GQo>oX@X#fqOdEiIC{%C5A z00+heV{-8-!2S-j0D9RAWyiyX@SA)4+KX~?XqQqu9Pa&8Po83*4wk%J4AErU z6kD0Z)%>5TZG`W+ zOVz3Y80ems!+b`Y)JqHVY0JWbO{{42-a&{Rqw=UNZ>{HRASTz?yb{r9ldBa@@6}5N z4o6^e>1UUzy3wN29W(*#@v{vXs-r%Hkd)b}3IUv*p- z!#_-a==suRk<^q_0J&VYY zXcxSN2)aV!R^3`ABchn0g_9OHsjeyO#ZT;nr>ihKdEBye#K{20@^v%UFjqew{Lc5_ z>3zoO9U+^8gVVZL{Z&B|=PVMMpdwp#;aU|e_?Sh}Th|@OHb2kWyeogtg9NP0*#i8W z_RUGe!R7eAvzA<&bjm6&ZVcedgzuB4D^zG@J|v}uw1d6f&eUp^3f%Y)NG}km_8mwT zf?cGLWFhoe9W+r0aURw2J`Y0^VA*oAipmMT1_XU2z=hjmZ2+5OU<|MDC=Hq{1e<*= z(lD{qdWXkn#Sm+K95^`YS(G9bb!J)Zc&eIIf_hMi?qKRqbCLuE^)Z=XG)=8uhID?N z-3hjDCG+}^Z|f7HA4t$7^duNiv;rs1>-Qov6=%v+;Gm<%G7tiJ5 zTk}O6c2~ltM&r#zlkiyC<|%Mc!XT6pCN3tY_{n#tt>0vefbI@(jqI?<>_G76_wG-J z5;9CP-*VPX!{g@Z@!6Zx^7DY%u1~&3q}(+A^%U!~>c+}9+rDz_q*hQdInOQ3b*BVY zz}vK=aMN$?J?Y}$I|;d=7ETBtOwY>j1k_pIeB8%h;HUlE$_PXTDlAY0^+G9I!o6|N2{)ZiD|9VOtEK>h|B z6S;<-)O*JUNHXh`YUSv$0pc$`_58r8EafTRoe*nQrZ&?ysj@g=44{btllgrAO5k`t zv|6rnm2(Q_X2Gb%Z1f{?c5jUHy8Y-mVBl6P zO1vNWfQQylYtlYZ#i=&xOT}5nI-73PFL9$FRt-8aXonuV&(EA}S5bX)(!Xs2%-=eh zt+`WvzDWx-mtKp-J>UiQ=W6{hb%$du0&MAKjwd)lVZQ^zVc*Uj^bWa{DGsDZ?0D|# z6}*cJ)e1FB=`qPQtjv4+HT2!sZ>=p-4{qSsUZ@dp4AtH?y>rU>_+Y z5rc+TfdZ6%Mz8VRHv4u^tguP|Ux$Pz19_&IB)LYwPLQ?q>hFW#nIiB^_&U$E?}l?` zr{Dg>+nN&KQk6*+7!U){#S}@iA%=W-1pryiL`a2aY$Sjqaoab#p$G#f1bv=|`tOIcr%k11IQqd{li7N-WgyPipGN;>DaU3zMbJj1 z=&t9L_TQaA2XVNIm5ZLfm%rNe2BANPp`R$F?Xqm|IZMs(Kwjz*XR|AW#^jU>Zl4I; z_!U-L)~1imuJ{EP%pfPbiM^z}tifeZxnURGxSy(FBrN`&QP88ceJ7^CSnu_$Py5)a zf>S0y6hH3z`zaST=K9a;nz)-ThBmBA9`Jc>S9U+;PRW44M?!{E? z$;E=yLKw&?s_TPg*MXyg_LDA5Fa-l+JCwvQ5gYgb{Brw)A&n(-!k>=llI$HMA=3l^ z3b+xCeJz}#zD7BP}$@Scm0im4_YadsGU4S z2N{juh%-Nkd!OAhxLodzRlgwx?|TUq$xte1l8A-A$%5Gn6WrhF5#G)xGlLe0BqmF$?jj`0WDj_E%f}4_Vo)pTK(SCSFCMRQDX!P`!b1@y5 zOC~0P94vYc`*5H6)9U0S9K|aJ{;9sTKh@Q^(PSrwwX+JdU2H~+Geg_JPBcBPvwi$N z${h#1vf3N?tyGKN2>nvU&;e>e))eNgAy~h3%<=T(vR!7OGHt#0?rx|kw@Tb><;m|& zK&k`)$AF83aBI1@hXiCMg-|e_1&In_p9Tq#X{ZERjc91ebe8xex=cp{1H>yq3?W!- zQ?gqa zeR)U8q#(wGG0#DY2TfjLa1w-UmZf$V(1X{kU5vU(d$e}tU;h}ZQ1TT$?C zV5!xpa};rMjX!uv2|ggH+MEh?l7Y^qqPH@kwRe#v+L8ytY0DY(fDHMUL@jI--I0n& z5ekNtO?|1`ddX^4F0}r3C|8E;$H2M#=OR1(M;Ms54ACloDF`|8&wDK*T(l`3lks8X z!j$n>2EO+a=5y(z*bdBW>E8n25gSt>DpGJZ2Ayw*h@XWS$Vzl-?JjmE$?Wvcllx_Mw9g3p&4Eqnv!xj& z{hdm+FtSRJ2EN2XB`U!?WQe~oYQrqGY8L!729>P@=cxiv7P_ki{oGS6U=(yz{xF4! z)Eh#zlTn2%U{NganncZ6+Ept@onV1BZ9$(LMcl$Uw)0^2QWe7k8L6D*@%3Azdc*~~-KrNYv9kUCXCi3MZ} z5l3b4aTd&i2RX`{@8hYRWi`6D!<~7slVo*E3VQ1@WOz;1Rn3I>N)TmoSjaN6iVUMS zR-?9Fd8ThVt&as*Sz{sic`!E8mi}cE{kMT~W&X>9A!TqJ_Vl<#`o!N0m!B;9Dj$6` z`{HYCR&nm1+fT6N$=`ifZlqg$v1O1N=YH%wxB19u&}hLX;8B4r#tj?D^`BSx%XwIU zSsQT293X@VpSlUtU}W`5A?q}~Jho^4KZ?#XEa~?B!(WChK|vA070%q^$XpemxFW;7 z6%|d(e5}l@tREomkvlaloSCE0tXvhi(z3E_m@PCdGb=0WvGw@p|Hc=*@c@pS(D`{7ndHlTn7 zyK!OutFV2m>@q1LwOIDE2REWfqIv43l;&RL2MK3{d8wt6%O^YiuBHsAzbaYN_LooZ zgmsY-+c&tV3?Wn?hItf2Q>k7-RJpifS*!!$vI-3@mJgCb+PQK8Sh+G5ym|^J`Sk`bTc?aF( zm!6vDKpV@|i9qN2w>9m2Dhd%?#pXGiRF-YO{E$lfE>AMl_0aiA9%@y>>GmF?-tY+J>i1ObufkUtmWtd(>O5Mn#jd zs@PPCHj(pNNCXxc^I#w#yUBZS)|FQ>Jd!MbKYcc`pVWVtMEzB zRPsG}L9Tk6`4YHDWnJ7-T7dm&3H&vUOmN07Kz8Vf!dRujkWCwA&M)=$PM5C1q zFG`GJTwHsseze{*Xmv~t=snyjV0^gLWSJeOxQ+E-v@%d6)FbLF+ZmkNHWP1*Xx{#u zY0@$_wUTEs(&lO0Ed{q<*#G+15AzF@Z~2W6Y%&-{rff&sr76oo~(2dsq8C%^5u#FkgK7W&A{E-W3>Gd~+^(<_*S4 zDT;%2B$8b7ik)FpN0^T2Q$MOFpA01?i{~IcY@#}RZAGMeT|GHjvJEEE;c{E8)}zPsr4KMVOa;bG^xV z?+mopiyk&1fz5uUXsS@1SsxRBAp^08s!iXh3UboUfO&QcdCD%>pE`;$DHk#;V**M> zN+VgcxH3EJ`ys`3N?uS0AC0F%$$p|dSszDf$Z)Ksl8lZL4k@`5XPO2aMyL>n2`Ma^ zs!YhvnQ38E6hkf29ux$Q@D%C2Vy(OfP&2DG z85CdiRrMxwQQj{K<$tdt%b`gUwO0D{s@_=9l%Gj#f~X;gYJ2*w3);l503GUOV11uz zfH~0$@jctyX@FX>iE+;$v=S*Tf-$83`zOAG>lLDzQ5*JJmBc52ajSeZCX5msOLsk8 z*;f#gx-h8{HdgMC(yhRj#%$c#p6k)_=Sy6kOQFOkSnaW>WlL@qM9twT7JWAk89xyM z6u`N%MC ziVZh)=k50ISucwrR;u&wC14c*qD<@;=KOt!hSxjT>evR#M=4dOSp~9SrXWphB>7$dfsI zkAEz43m2jhWvo%94mOCAB0G}3yWW3gWdxNc6HSB?lqXBxgCzy)8%oUgN_2L%3Zbg; zpv_-5Tp(&A4Y)1RHtY{r#%(=jHSA!AxDn_o6P>Jh7uI9$F>6TXwq>vJNwYi1+xR$XT;>wn-lADxBsb084=3Dvbc|Nm*}dk)XX+T37lFmq%=&0 zZhFDZ@jQ>zZ#1bhZ{p<+5S$5FIkv`}dZmQ4dBTOU5m#%FSrfTA?j(m4c+bCLyJ0jg zWjuNFa9U=m1%K$(i6k|EX+`OEDp={w(ZN4}o3CEYi(G0lIM*K4`$)3rcsfWkMFA%K z7qs|K6GhJ%a(j35dYQ^wv&p0Km0OJ)6!wS%wPgac?Zk$%6zt@oIU>Q_)8%8}>7a?o z|HkNPJ}drfD3u5Mehij74YOAkW(O2JcQt6BH%M7An_r$iNTL5-epyJ|{AN9Kxc%kg zV1@e;p4NI@_5;cJKa<&q3Aqfh)u?)#Tz#)*%NOx80F1o{D;C01JVEjY)MasyDefF# z&q1$>;V#^aZH{E6x_9I5GYw{OW{`W?qdOR1tYBAK&vT0A-TJ~@6GB9louB*l zpr54lb|u3Cm4_w0SuCXXV4*B)x^qhIt^}j)hory9PWNHZ9yE@6mj5`U1;Nkv$kQ)X z^*NT8pLVg%q|YI_>a4C~bB1~I9Vf%?&VJ|qMIQkjJ8);fEOpg!+l#(KAV}S`J>_e^ zM-zo11vmQ&(1$X%1DM8kol74aJZ<_t(g)v#2)z0&{#rRYul8^A>-YNV(AAs6TUfMa*f4TEHD&N|0;GLD@uGzkK4UjtDe&?mWx`7c+6Xf)7XH+93e9(-sn#T!) zK3~4rGGo8F$`$P^*x0SnAGS8?u`=SRk)d!ruQA7LFd6l5U!nU{hHbIFmOv$htQIuE zk3|8^yzAjsTvb+v_A1vfjfYr0g%-;$-rZkk5RcwgCyZy>lQk5l0D`-~eG1Sw%yenx zU$qAtjd!W$oLN~{1KDv`)Pcc?Gpe%oNg9_Jny)Yeq^<%QY#!DS>liQRIU@)p3Q$zu z-^(}u9s)H%`Tt$UM9*n3ba4V> zRoQA*sSutE;@!SogNY_)X}&Q0WmJbZ$_g)*3vF+Hv0gj7KKI7j2e9;T_=rISd<|rLR8oOuox6AZas$P$ryZ51*oxK=;;f+6j6VZ65r+96sXTR7MzcWB(xhmK&CrU0t5>*h^vS}I zQ(QEH=Yti%?QUS$JXd#ttPbc-&A_s{%R0{2;<2z~j~mDv>t!A_ettSDaYUWAP;VDbp^YQ6mB{4cm;`p*K&JGqm zg$(uBX4az;VyGFKQDM@75p~G*no5*8xt`B4p*u^GpEI{CxK%8*4-6IG7BG8Ua3Qym z(O>TPyrg50cLM54pHJO+CeuUM`PrcFWRYP#YwbDxg(n9wT2Qrc#2x71rfqDLnIS*H z2wnIdO^x35{1>0Tl+ilHc+APJ8%CobF1bOYJAU!&$^u;5&>NSm`&V6dyVZJy4d#`` z0)BbH9C<-{>c$Tg8~Z?fvqGb|?pE$W$*_;Y8uqb=LFNA>#{~amBD5Q#U^f6}1O$ z5_a8gap8f8{OB~u%ucRGp#mfa^-mi=ip}eb%HB^0L&ia#Vm@XZWc8Q(qgdUX5el#Y zEB|iQDgzA)560qNN#=G;04J8uD->H3(lT5{{0-BpGr#xw+P%DKCkJ*<^Ss>QIrX!c zF>LtRs1Ecf(_O?5Y|WHGE$ykWhp{CgA}A|9Yam*J*Tegf>yDo(CC*&-3Bu`7GF4vO zMkDWp4q(y;d_xdfDi`#B^+|5DG0i9a@pE)VBiOIt>;AF1_0Mjr{FWO;+eXIb1|{T7 zCggnn*luJLp^bj6kT4l>y&WH%spFudcQ9{8P4B$O>@c)(<`KHi@nZAyzBT+alOWfY zc9#q5Pr}gh=2+J{6MF|Km@$}lkP#SOZhx`@RrZF@POK`qe(%Gi6SpMi-kbY&R`-Y2 zy<+eM+wfzh-|~ZdI_-v@I*&&l)OXF;b!o%IF5}ZmhPB;Xd8jMAS?B$lgPg)Mx$Ca) zoAUmhbbj95z!6+TR^%B_19uIA-D?@FWyn_SZc6b}K;gQ@tEmT$UC(o0p*rLYnPti^ zZ+@InNX-G71)x9AcUYa_`BSs>|DSJQDN@O3TGDpCS2l*z$HbR8Z-w9c9!ndBoejL%3J!!B#L6Ufu>~CCb)uvW-9)`*)ETD#j=V{{UP6vo^?R(#qiOHoGTeL-5be>Y@)6`MSIX zo6$C6M%>EicEI7iK)3o0yY2&n@+;5j(S4&&PU+V>Thi>M8dLReoNI>z&pXPaKe+Zi zzXW4AC3FrvHBFF^$6Ppi-+C@_^zyelK%E?*rZ>#O)2DJfb*RjY;TcEg;sE%rd&V_6 z`)usvIw)~Hde4_flb|l1ruGbcVE@eUYPUt2**=)od`%;ZLg?$Jgapwc%a}S`vr+;P z*4i1OGKwOEk8glxZUWFHBAOlfF(N36*{R(Kcg4K-{TDiPgBu`QLaO=0C7`Amf^4s_X4Py)U3-_s{8)8 z`mVRxRv|#5%4yoY4Nj9Yl7Xzr0yQ@1oVGw~iD%!MxvP~=!e;E6g4hJ{8B-Ysc03YR z5EcXpi5F;5`1Y(jobe3JDxNDb%RUFBXDG*N1?y6#^;w{8Gm`(o#9?)%?%Qx~)4t*- zw{G~v3(RBO9I1~7FfWV4o6VS&;@F2ANMXtA^T`3@tX*P2{`Mi#mwp1|%;}QO46`BB zhVVDo!5w1QuexhiZ$C(%GNk+Y;DJemE~PDR)BkjSfOUtgMJlHnRW=2GR5PdCX7}5v ziemDHS?}8otGCUoc2h*Jqs>o)99Xy6=#0S)6wz>e*;;&n0^g zLfqxh2TeL)fB#E?7KCNhJmz@F9-eP-Q}uPjN-Md`Rdnm@`&##1*JEqG$d|@|#Z@Z8k|^1e}~(NgPEkcl#6h>-@&4 zrOD(csBNbY=3bmRcG^9FxTXSL3>p-%~PAQL7^m~a2%w%vk;Ay}=;3EIs0DN67-Sd*@4wfGc;be*WXL7{ z*e9j(po%(=GLyB3GX@c3YFCZ#s~CqE@2#gVqGYM1D|TNDzc!4UBwS1<4JS2wmY6&X zCt1Vv9nDR)wNx8@yM5>ER{!oZq|5J!XY6(qbCO`#J)dKvZyOg*Kvsbk@ z*@?wm-f6gj9Uya=?ghgUSfZGmfmEhsQBPUvp-K@LRI5R903blZf@BaOZ%w>^Qh4#dA1b)|eEsgK5{Bs(R>`Dswau3?>+C<|36tc+ zs0{TnVd(^+aJAF}lU~ctwX7ar9o|57IbHU&Iy?T5fw{S=v{TD=(kc&?r+0v9WRk42 zlhFDI%%RnNzZOALlvJH7mH)mu(%H*Amvh|5Jb3Du(uo5zDx_GK)2DSZ7hED@u8)*4 zR|L?qn$4ia2m4pj50srT_4ZfsGwVaX4cghP|HOZ1u0S;~hBsOn8e-^L>TU!wXkj0B zP$_FTF}GF`lbZCM(Gt3-a-QRupk~BA;Uy;*qw&!w!26%>z0YDxI9Ew@Aup9_7X64o zJ=!0wJ&wMB5x0r1VpuCXVNO%iN5P`Tx+JvCzhJb5%aIYRlM9y$xd2KA_Iluoxscf| z2Q84PFnWP<_HXy5JrHes^hSlBc|zpzzmE^1%(AfB`Q`(7nYGSrgfgs}pTlMt_!fwq zF{Q;co*a|-HA8=p-N4soBn#oWRd~x`h27(HsOEsIA+H!ZLZ``FK|xZ>mN2o0X|N1h zQS9<75dFU|v!TsX{)FI5v~|phMdRaX9PiVi^p7J=5oecuSd{nGZ+Grd7*JtqdyJ~? z_K5o4>UW{#yPzMuQccGpzmABKe_=pVTd z-O?ZQrpVbcarecDmRlO}l!a%MD+1dBmA)Hm0Kt(fHQQI(_+KgT0=%lZxBYTNVpmU> zE>GSdT^5jaZml2NP~RMM+IyEdo6HFk;}tRrQpI&vpHF zn%#USiAi}k{(J8uZ#G=+gEH#^D1>&(D3#fj z7d=8hfy>R*59rGC?fIbkTiw-^Q*XV|u`isFPaah3mP{sC?4Vr^L}a^KtY|`Qs@ZDz z9<0E_5~f$wID7hqm2zs+oL-VOG&{~$r*v$7v`^lbKY?y6k_SF-_pYzwu#{?FR!7=#91coh*k?iqc=$Y2jiu$Hc zrX{U{cGHdQKHS(I;>bF~vgk|SEP^WIQGt+Tc0Y(>a_WMv!UqL%HHgTzzk5_6fwazZ8-I2-ksEtU z{YK5bUrZAZrl}ePH0kjB?%>aSm2>zrZR~f{5Pzt(?BuIOv^p(sx6YzZd2foD^+dF{ zX^rzn$=ckx5?Z;kO{Sq=A)`URPrgdep0wSz0hcQ?W!Yuu8BKgBF10K=pal#7Yh$e; z70k4}iU|})EJv0u;ABBQBbPaZJ@%DM&(#TNP-R1_et0ls+cyLiDo>O9UE^`*@WnlK zeH~Ay=!V6ON2f%P13v#%=H66{^YY++mzr%G_tMXdQ;D9YtJV+*WAR4*tSO(GWg&th z?om9r_e&CiP5wEORhVQIM2{NH*p1qE>UnC@l|k9+3o&=`ws81e<~wL z-1#G1?fxKGiT(UI<_D`deA{;S^*lk-XEWc7?8BSs=>|hOF-MO#^4?YEk?-HK*Y06W zpFdfB9PuvIP1BR?F`FkVc2_6N8gQqBjo&UEKF1r33cBM6=-;)T@01B?6%g&n)U9=d zf6Y;QTNNOB#pSa02I!WDUQsZ7{&a177v$%GN_BJ22bEOvlLf7qdcltWq}kgmwy41^ zUnGb2%U^bjn00eF<1MUQMn$I%5t(;h{QXoduZ>$u_*m;pnW71|*ndv=T^#?vS0TcE zXV>Z#>*7yn1)V!>du88;6zj}+6 z;*p_t|JTCBLr-tygdS0y=HBah_HXE0s^h?2wlwMdzX>a6)n|0i!@l2nKc#ZwBu!{` zWdA0_gV_t8pJHC_zfv8(Yk3-W?eV?3p84yAN?j~-rDIdfVoe?WZHS$KV$Qz3bf?uL zm)qK_!;0Vya&?+)8pekODacM1Alv~cF~DaRYxwGgPw) z3jI{3>^|G_MKImxa>)OA=gXJdPe>jo)CCfU!ijvH5#ijN;0aCWk*Id}M4cP?)lqGA zJ|^7Di0;|+GNyL6>kHfcf#k$C*Bx~MN8}hH+Viu)a`=ZYru1ibd=V%k{M@TMX-zkBMhqS^M!|4q%C9Mf?2d&1Gjls6T-h&EWNt8=d7Wf!<4 z?2{*#Rls{Cx%Z4hFZ4Y-G~v>n8FJo?uf7Ii_RQ96&prRnbEAew2$7E@8n541?|AH@ z?WAT;P&?SiR;vX?E|k;1)V}Pho~a^)uT*zjuM1xT5kCc9daU{A<(p=esP=7Ho~&01 z&vYjjqcr`>JE{XZ(aR5U0+{8(&U5g$km{3&rPbR9ohDD?rw04 z<$|jh!+$(1LqAhc_*|V`WMMEtMK|omXKQQ0bB%VUIW_FBbsJ<*gN{=56(pb5y>48f zeZqhcRRDtf|4}Wl&7Dvr7la7Q?kg3oR+Uz%TNt>|vCl$oY~&A@`0n9gVSS6C_fW|F zk)dFnG69yNboa+<^wjG79u8PN-DNsWP=#^D8D>3-r%{{9xVVsA>2o&o&n|8o^CUl2 zZ%|$2J?feZQ8qu*p*fQf_2y2sKuIT5KA`SNb)|MKPkT*5qNr{g5~zc?v97$fqOvG0J5vSnrMJF_u{z+<~t_>pSlp=!b2FPi5i@1B&0?0@@a z_GeA)%W565z;jSDKDx1bmZ2S#xv!b$*#9Qc=Oy|H2+xuP0JGB*qt-Qq^O$mJqWbyH z^C;BIBxA6x)vM0tr`oloi|@-Ouhbrz2x;wm0;#zGZ;`aN)b03rw|-mbd~8cMZ`W+$ zTQ$$;wcA54%oU&9_Wa)Bo7zuLl|iWLb%7vvLp}5RLWlc=Gqicx&L>G|=$Kx`fdY`6 zY(oQmJq8!#4g~F|3T)l{S}N2uX#OFK^6XehS}T;R11)%X6A74L+%9V^nrs;XFvUhQ z`>xOKyRrP?#XrZ+=|3=;W&@GCcN(8bI9hGtEMZ}5kWSN=?%`ZHr9QR^@Y z>-aT(N}t#<_V$DLo}wfmQPOtmm!RX;$&PWQ#0h@lgw~4W?T6dZ9e1=$M{k>sMz4(A zTiG+tkMhJ=DY!;gLRBYN-gdNOqd2#5sN#Xlm@2=W2VB&e1RiU?EttGB?q(5U z@N`h5yP4`SMzct6Tbmzqq4?kxfj@KOk>{!$&WM4V!!lrb$1bQII=SDg$wi?#a6bqa z?!KQME~oIGg|i$BTdDPmZ+V0wdag|qQ=k#D%prah$O5K1oZ9@5t4q|8=n``mQ(^)= z@x`!x1@dTC-{V`6o&>ts!fZ71;d=AW=x2LI_*&@#f<_kjQ*JH#L2xprUF5f{M+r$U zQEDCyWXer}QLf)dTURy&b!neer@SGj%HIu*?);fqnYW7A;dSSG-96+T@G0UVdrV{f zd+2%AtO&=IYaLrHHF~qnlyE zeGsL9a zIM0+8pziMJ;71W?s)A8hZU82@Fo+Vbz0=jB)l8*4a~45y(M~4JLH)4zbn-DCC9C>n z$=+f0%G$}=my>mRcpc@%rPq1Dq!_LD+Eatt-EojZhD$EE8hsxDWH(UBm%Jw;21y5C zLh2#YFY+%ofNwf9|2CRJl~^Ux?R&tbY=v_KXKHu+=YP*Kz&7W0=X{~fEdBI}wAF=f zz4WC*b7HG}Aut}Vx}+iBeNN&_uJTDXAfd}mtR_*_RU$C)=Z_uz+pJrUFydp3lWyYBm>+LULf)mdfzl#@^MqE=oT-j@pO(pN*^5!SY^>?R4 zf6i6=&LBjkwD;!ue9>EL-c&ls)9-?Or77(^9}kelUsxXs&GfN%?j=yp8NatuQnv7N zyW_cj_C^$qP*8;AHn28b@pk4yiCcUkzxce)Qn;KE;eACt|4^X%wZGlI>Y-cq7zWc@ z%g|?D|L)&ZiWCB=_cRqJ>_46}o5(KXh8;)yt~0L1kKFowcKXB6&UEgJhn!Ic)TRl2 z&_g;3axAJ!+m${9#cO!KQGhvyjgVgAxL>d@pB4JBJTvO@Soj`y7A{->trQ2a1uGn|p2__qE zH@ArQo}nWC986j3N&HM@sZt^F`K-_`mc{RETd^T1Y(SxPITL^mdhON_qgNKwQ^Gbb zTplzHaMTZo`hH9Hy}m;m*xzl2XWs7`JNl<(%9M1$oa6gIbls61^21x5(8Ku^r>~tfblP}JbBhOg3?4b5va0z&`_R2do_3xo{!JHh zCeHdaVI^k7u$Mc!-Xww;et$GIp(VRz_7lYDCSBd2sRysnJ<(^p1KA?dqMVcy*QR?t z>%)SM9$$VyxqENLMKX2Z?XMrd1TtE-Xx+hp^u_$2cDO(cPv?HlZCB&a6qp-d`j&qj z?r`}r`@=BmpD#V@!4%+Y*S(#F_Ses2K}py@Uv62FmHS?AqR*WBgYh`kd9+V8A*N&i zX6CZK&^)02FD&!QmY>W>PmQ!Y@~=P8gI-&tocWg`+9Z9NeKTe&ZEzFFVPcQXV0_~C zKGl=`r>`BJzHPkzStW&c+A{6U)E57E;dT}JV-MX6i<*Z6UWO-zN?$HBV$D(>zNm{` zZqnQa+UgaAkb|21c$1vh$`!E#Go6(^ALefaB&19aJ1n3W6!A*|4{I&yT<{SbQ-Vv;pD+02N$i zcTkp0hrt9;GIzlpcQXa^d&?n%?#OnwOwQZ>#3Ew6=&{CF{?kh#w~8LC*%mzMYQCAJ zvajuqfmQfu%KKmblS>x9?L7*IW=$(Eaz?Wr(;4kg9$X#nh<+Th)1>XJU#CuJo{ib_ z#*m}UACceQF`Gm4AJ12)m@vnJ@}o4z5)(Rt!b(QF*Y-YcKYRY9_M7j4st+SuyN&&8 z@;|VvZsmQUtIVb_Td&mMj`yn@cSbc&JH&=m240SC{}JYWaS?TB@qWGYx%JwenvA?( z=^oSXj@4P5`1J^+2unZ5I3a#yR1Pa>V5HnT)%UfzNPRVbxoJmv+&~&*gS|d{@s#tD zyK{*{dCWW1=7xcJv~g=PWe8EBo_Sjw6_0ejbjrE9QKt^s?qqslqV;U0r|anrUAdiYgfm5%?FQG+{`F{y zHcc2NL|y4ewTwsH-5bowF@$t-I@Z})Bx+EDl8)_#AZ3blvHPb_Vu+Rm*#H7VHX#nz@cwn$p5j2c6 z?{&Q7guDCu(~d8_+<6q`UVEb$7ry<}AZ|KHu#JIVD5A+VTEDSZHW{3!FB&WaEnl!# zII#Wa!f%~XeD(GRF6O=Nmi-pxrq5R{KWMQm)zZy%!EPHd>~`;3-et8P182VVlj@f%JVE5<9uYaedbT* z4J#C$#nR3XDR02{t*aOZMCAoShZP7?mrzl6p3LK<;+;E6sdQ8J|H52B@i8! zNXbJuViZAh+Ucj0*-qIMP*X3@LT9+O^yZtzDGzqL-^Y8GoV7>?W#h)_g#PLY15aMr z(&z%=nk=Adu1oM^n>vL1dSS0tk~dPvZy^&zUdJC&U<~XK_2o0Kqc5o0f5M{BQlrMh zcyDW*CW0C>)4;v0+hcx@iTm1Dl*7T-7523z5Zb0*IzSs$OAs!Je~MK+{OWO;h{RNv z1%1ovi4s44*?#2_jd|N^4d`aWL6N$Svkm}418%#%s)KZ}Ac$#Ovfx|j9CnKVERhUR zpAo-F=4R-+D_cX*SK=Z<8{`r$b9&^)QXo&b8Js82wVZ6r1%l_nN@kEAh3$HU8bV!9MMc0 z;8E@pB!LClzO+$KI!>lrUL|MjDYy@lmjXyCt-#g>ObTX=Y7#_TGtpk+Sg{1o6lOEm z#c=Bx7sW~}B*c^l!Nv=W+JFI)UIsxM0YVeH5Hj%)G#)XAA;>}#P*>H`DiF+_1&ebB z!xX4P#s~=*!xiGjWE^mq>^=Meq>uXq3d9yW#+=Vtf*NjQ}>p{wn6M%0d&HW@=nLO1ipS!mJ;t5++`Lx_J8f){rx&bV_n;dnC24 zN)^PJ_O0q)I+L><#SNUF`oZ0AfbnvwTzVf5n?~Lx(EuwoaWha;7eI#{xL`G*Y%yG< zskpQXYO=peyWj#SB#VV@M*(;`6{1rmhR)D{zug&f)<|YaS_xWb(#yoj9co3VW;18j zRI#fg=EYy~uudk>F5xh)W-cFPN{2NDaWO3I3|nG7p?nq4cAqNW*2-2*7dtcdi3`kD zsOcjjo^o4BCS|<=aiVIButF|WUX>Ue%pEmbk!CPVxrz^ZH{@VT*g_A^XH`@(A6=te zh+EM))+QQO{Ke*lFU&(E@y|L^@KNhaYv_T+z+;O zrY|mKmgsbjjlr5hgFd>U#}AzShB=^gSD}eG_ zWA;2MO;%kw211jM2y{gmpp=6Mkk=81?T4XY?9vODvm;HX(0PD?7rqfKWMdM!Vu1tq zRb2Avz6bN90MU?faaW#gVgrei1JO>S?ls}iiTzi>X2q5n{^i=ZksMb!Ruo9Rmj<&< z7GQitFw+?xHZd7y_JacU8j-w6ArIA^5P3%?gV58m_(KCt&e`b=ykRT$LR4jx4;eCXVAtB+$epM^b34#Q5kf$~xg!fQ!pC%R8 zIusJ;_*oLAvut}&zWqJwUZeG!5koKN;0#d{4r-bt|vx}Y2LI~B7=f+me9rJONy zq$fD`pQ$2{2Kcmi;r)mnM1u^$cP&rq?Gh#FwYXqSIzR}nk~+EetkIbJyzW`(N`MaN zn34C|7_ea@g&2rI^dSHWtT?v?FJRbq^VHraivsJytbmNP;H(o$K)Z>fge>o zHGVy^mP`~(9#HmTE7a7(`T^6)c}yEs#f3JN2q-sz@F%I{&^ahpqU0*XMFV&u z?Ry&;eQZu4hmEUY%V==WB5j;0pq$Jn#7j_VIOIvT>XaCjM8n7kt9(EPa8aBZ3aE)f zj*tnZQ7Y+Td@&8W4k$;Hanw1;vXFd4Vi+9-wx+2Eb8&W2_>H|$3eiHmU%g5x6|GQ@ zxJp)Ts8{h3tM*Hw%WkTP5_~BceUOHHB8BtG%3eZDLp@9?HbiB?&pF2geX^&!+6TmK zFNSfbaXM4-3ETCGl{hX}TP+m7S=bBgU8Chc#-r~QJFk93O^W+va_vX^uDgt&oJXvX z37Gl}Yinx-58tvh=nW-M64%QoZyo~$#;lAEC+g`LL<0veBrgd-3i>*FY@L+p)5c`z zcB{Hd8274pleBiyW%jv5)fU}-LwQ|t6;>#Awzo12Y>+`pKlHg;>yKQk+TWN zg_hcr+N3qA;=xYBDRQ4_}rgOb8u4tY7A1Dl|p1`@l+l(Tnf9vmfC^vMN*>Y9Mp@td30ViUB=@S z%G+rx#fjxaWD-;Yb}h%QgTKBaE7D_#H-N9lxUdIgXc&OE(pKc~9-gGBMc2cewB?ex z@S=L7|M3V_LdXJDg+PPMNWchf#jJ8<5eR2Wg^9r`X;C;IF{F)6oachwQl^WckAH9OglI>R7M!KYqC*P}zi9;OI8Q^1|sc zJ6IZ2NE#dVClBaG0tcz4RbYedauzPm2Hg2zej`N?tDMd~{-cK&Il8%KZu6RsO>sTR zGu22+x5%QAA{I%#Zgxvqs^y^2>D#1Q9h-8AWu7*vR6-=e45f6Fbg&Ayl1K#MH&%pl zaUfFIqW>(|cpPkZ5S*bVgU)f~?P#$X{}^$bjf(k%UO9G(#Gp_|=4}g0PbH#YMuJlY zWBTh6i^NwyND=z~s5<+5LTtZYO`5hPWP|L}ldrVA4N!+7J?1U{`R?4y+ODSTVrpc4 zVJxnfbs#}NcUN(+y_WX`i_X15@6BR3z`}O@oAR_}zfRP)Y$zp1U^F9(3vq1^C8=mu zMqJ3<*Hwq%}}L->(FohcoWx#~yv78XsU92I0g+DgLT7+n_!)01rp>(h0#o z41MH`YUekrIfpRyxle|ovgYux;rM}3$}IS39DO#L4^I*!;^q>J2q#N6o2)Z77i%j8 zk>y}f1Xqb&H{FKMBaE5JZU78=`6m5~l!9o({>-#!+E#-fHeR|04Pa97X!eAy&9Myk zV*TipXg{r9gQP{@|E(?vV-V*u{CBD3$1O0D7v9cKx!e;M*Ult2Lg=+`lgQJIdmmEn z5<`wQ?U%LAKkTF&)6v`9;5{=Jx+S;HP6$t>Lel{-SO||}r>qejk8|O!I?!WM5Rohc zXtTFUCHMn~4(XXV_Qhx>$dBuEgbGmrAm!XB#b>D-fw;q-w^$BRkdidYLq{JQTW&1{ zp+;HE6_8gnOt139)6>Sa&6|t$%?b#{aq}d=PW1Ne*41}giEnK2x`sVh2n#w^X?Dh* z!<4G?W-AS+_t{xA#F`Y0_(wb;md|0!qt;!9ZPV#tRdkbww~`M3!1H{L@`=^JbGpI*6eaj96xY%emS1>}TJSVE*s-jE=J4)qahZnpmd~qBC7TUL*vH=WUWW?ns8ZEe03?nI1&i^_ zD7`o-X*>6WZ9NXP2owvI6x`5E33R9lw9&>^c%*{y)P`%YVbCaLccF(K4{O7fiIjlt zfQoG0ZOg;_w*^${bxe*8u^~sm87i2~-n9x*o2h9r%&nVsDiz7mb^OGE(SW8@ix4|o z7Qt{uKg8xo$d&ob%uB{jOGGZ+hNEQlByel5n~iIu8DY5Y#LQ-b^Loz^yO>7!^(;uI z*^FM8rLP}52iAM48kD7d0-xbe90=yzpT+n+_sa+lr7z|Q34X2(7o%7zaU zsFfzd`K=sFlVy1B#~;zPt}!kuHoG|ixhw~IhE{tii_DuCR$z%WQ*Zqf3S7dq(~Ng1P=(FNy0xB>G>r922@B>pg#J%_l*%f zuP-S}{lnO2BQP#}=7>==-6B0EwBo064_zrhTWOrIH~i_fjC-k{=C>~BP?&P&{AkM% zTPs#TFgbQNEyF*8cG8Q+`v;MwFK1LVvSqKn5m7hn#-b6ErWC=rRR)SI&GCHOl5B zk63GYYj2RjMv7$XF&!gM8qZr>^<)NCZPwlimZuy@i}td7I^a4skT_O6_)a%bRO}Lc z;_39;>$5F5K~v#t`kr?}maPAX7nj4Eo44cw2npP;0Y?CVZK#lAQdOrFKuKFsGy)uy zVo;{?Q$n0!6hsF=iiBVyfP~ILit04uq^faT%^4vsl?&~z!={pfHZgo8Ne?Ong`iam zHT4vPhtUxiUtavEL9JAe*cuxQ9OIfl96GU^NP5M+`|{st-8Tl>(r1UQY{C{d*Gl}z z`mx_^ZK`;xRrMs|80W;i%)|UPQlCWsxVa{Z_)2<<{sUjd#{IZoSIAIZ3b3gHnQbr$ zdo$)qi8R7RFln_iDytq>E+wUAc9s1mzG6dv4B~=>7{f(ODUWoPhP*8#v2EA8b#9d} zzAvE@O6N!vwhHyQfoBv1Q3v%CpUPh(m1-}!KuESUbnP5|R`@89{$R!T?Kx6nYy&C2 z9du~798p3^Yps&`?2_W z3NiEpoE))vKBYjROenxsvQxlRtS1fnBsR5QdFCAvh-NEjL}QN0@c%p|8!}wP1to=H z4a6w`0Eqz2KBFoXi{VZkAU$pQbl^pBww@Lh>Oy@FC4&@hq;I^un2f#!A_LwU|D`B2 zUsTs3!`6g{6K5>Khp`6Ma{Td@$n#2fF9=owyMlD}s!Lt=%~-OAqdz{ly!=!Y)w71x z5r~u;o!Zs5SY>up>22eeb~fl)2Uvv*-y*CYuT-41Z%eAPi$XrW6xn`O_#g&ZeUS+_ zQ<;vv@IcEq|81gL0Q2ox&J|;o*~@LDZcqOIQFQKsO#S~KKfBru+uTOO%x&&-sf4u6 zCihDrq>SWV&84E`Gutq?8X-j)p%ED&m1^!3MO3Fx&4xb=*Z*d!z?>>~6Ip#oW8kKDt0OxD9#(otMCob)4+HAAj_g2b z6(cR$qMV;$3t)_L>hAWS4^MZ#t1$7b{LzyoMLyCo%0mq1tB)g3hqR||wfJ!_z+z`* z{L6Du`fjxz6x+7R_2hbo?@KdR6Jmyaeye?)?Jv;vz1{Qk>*9DU{lSI16}ACa3L{Qs zj+I%(Eu1>*d*{yjIw0Rca9^-?jAj+PeH+?&1M%OqRVTr2mqjdLMFx7I$pJu6jslPd zWVp@d!h>s>LcOILg*4?g8;~P>@iRQld;v#?1d-&%sS>vxEF}(+K;A>2(wg4+tDR0#4AaUV;`G1evzrr+a_7Tj5-E@28L+6Rn=mf3lhrr%@?WzPjJ z#A1wO4S^pwC-amINd6c@+f^x4{=m45U|N1qAzJsx9*q^U#%czTbUS`FLAdwEcH5NY z2?#>qq(6iq_e8iWGrvha)TzsU=ct|2JPhP-I2AIXSB22^joqu9~IGI z+!aNO@6+C=dI!~hp6DIa2sQrj{RunQ`&CvMvqY?O!0#8EHB1Zk%U)MbTrqT1b`sam z%$=QwcnIO6#JOrdC-^Yk5n3I?vs1t06lN@{Q><7om=hsvF1$b z7cZi%Sq;?inwW*Yv2q$d*r&kvb5&fF=3Bg|)l{G5Te^u+@#Z9zDWI2JS}bH4^eWWa zjDU~e;6%S#)gIa)H4T@G`vwXqCG=WeNX9rNu16@Nh5hP6lHx`Qpd_4Oseb`b`b3@@ zbH>G*R)g5|jg519sG=-pfkT#S)q915N|7}MyVC$W(P;RY0F&vqq&nOblwR* z%_=57%2PgKm!8wmHtf7h+^1<7SAF@7LP9XQ(kS5y6G~Mp^6CYDbh@ITM53!1Q31tO5IQ;^z-aYCK9XwiTe&i5 z1_`c+Apz;JPy|y70Xxy~>u&5FVRn9wh%h%m+ksmVCI@3r+y`~qPGxIN+=Rb4DOQru z;P>r73g$KXO6@{^v{Yf=VhyUEh{oPP$PSfQqTeLOc)(RFkneVQXUa(8y~m8*5k~#RZl$ z0>$a1ft{ns$lHEHNNR7PR#hL480&&BVdojw9RgJ#^N~gh4-;W#M`*boQZaIu8$*9UR1j4vFS23bNNQUF8@f-$ zS+t&Q&MW4{cwN+ztb&o>*f6tzdBvc-eH&CJ;r2}=%~1fRMV{;m4j3SWFUm;Ixcp54 zwTN8okkXn6p@yA9(I=FqdcJ0RxVAgG-qbQb9j*Jjd%JVZ^upB8?&>dZV-kW+pL%ll zc}L)dGpP3wt(m8DNLLNP2f7FLZ%^HVvFnI~s3EfO$s!P(y;uSTDgZ_*pw8hsg1It` z4mASsqJ@xWIgoA$pJ&4cE6cYWc4BKKDHmFufJg4#2vJFmg_8)@j?psSjX-4eb^V>^ zj3(~Q$b*=eMZZd{#{>pYPD`rf;tBgpT8MQ7@l(2iPaD*z2c&R9G{{TSA8xFD+$ZIt9 z!?D>~71Dg&DlYb{oNv0R4Z58I32L9o(q030?Hm!SMh-HS=}EcjTsp>UhN&99&XnS!Z0-lQBG(8a znDdIAjj)YIS)fg;eUJ!*tIDql3;|QCx>K$jyg^&vxJ-Lk^6F_TcYeyl$meXEfuUhB)8Jr1*6QHY~F;Y~3c&j;nN{R@yQpF*t_tfBU2ZX^MaLI%2uRA7@vSInd4=!LGx%~gf!#gswoS5jfr z@*Kj63o(lS3=74&{Cqg#GLZThlDV!DNoa8EqmX;~l$i+Cp7LzNyV5+CJw#RR+NNr$ z=UttM%s3{@(KUz=YE9En#X>kGc16za?9+^3gEpn9MMB=pbuc#Wn|j>R0MB6}wk z&iY}}`~p&`@Is~?-FD*(T=I@5x^K(0s|0(sFYR&f7I^PW`Hc$?2U6S?`EF?`E@{~d zxQzPQ@K}f~_YC&vL+bojxSR&1=Ldpwjw0Rytp;QO4BVP_MAjP}`3k^gn-2=@V$U2o z8msrB_ef%{URd8caQF#u#2Azw1J=TV$s};#3W}@29_3<2W#~>B`VKWu;j?Omy~Yh0 zMz&F7WLCW=SoTX4|7njU}MCokrm7+=cIyl-iR&c z5<&4C338B)Ntc?ot)Pe5iT&LCvPIk^2QyB^jM5M@bnT*rB!-IMc~3&#U#Wrc51g^mriIFkvd31Q?l}#XA(60(U+u|&1rdQWu*fVPQvSE^C|eQpQ?9` z;?>SplopCJvWV@_(aUs2NfCfBKxRAe|6E#V?> zJtn>1jybc6?#1cNJuKEuhdrTd8|~8eVV^b*(O!vj4|Lbjf2Vy;p!1)5_4lW`MFSVU zABLUp*!w3vyLZ>70unARB{l1MrcO;MSq3qeQyjUF`(!YZn=2LM67O#QSn8+7E!!KH ztL*^f^2vYa9FUxHIJ*{3(ur*>zqNC-GW%%SW>7u?Or=7cNbo|2KVG^^%SR3FD`;II{aaCW zH4Zx*qM_TpJue*_tZG>)+|`W8`JWqBEvrtUCUZV6e|&ZwQ`W84Bdd6uzN^x{ZWt0~ z{Q3OxUUY@fQ>k|Q?7LH=9Ly!sg@lWl`2F|;UrAAV=;LDdP^rhOUf8Sedrk|qVGcS= znqn6h@jShWIo0$xPunbguXw=DD(3RG7*h0U(ji*u_9s*wP(h&#?1Tm5jH$Sy9xOT^ zWqTv!RM1?Jnl+{=P{X(vGd4@bZfHyKubA7nj+;X?^l&geG~ygWZ`&NsR}1rc6+Mo1 zzPST?yiIMmA#?qgP1~%>BV@&eJWp_?@z#XOfAV&fAe+?ZijT9kfoDl!>=NC7FVEsU zKAhF||JwPq?Xr{c6coT*|j>n+_2%p3rRj_(NQWvhI)+6aOsiW ztF_BCKV`OWqb*36ons{fUwH`eSb=x2TmRh1v;BN#WS`qQ|LKI(p7H20EyCJCbF@p226Mz4B#TE!5B=-to_>7iN(XY`sPvrdR~^V-t^}u8y#=QpD9moT{`(xLfM= zR(vesw~fNRysD;5Gu6DKunS4~y_k{;iBzr2uXERm+pD(f|K^}Z>2`s6d(B6yt2^A^ zJUX42p!58Zz}vpxg_4TYx`tUghvjK_`oXlNASV)xZ3j6jiM+T0nEIug|9c?pNln5N zN7-p%w5E>m_w}${-3!(&VVi;1C~{!rjaj{Nl+>uP5_CM*kUVR;MF#P}4tz6h+kOn& zK!X;nVD7G{JPcM*z^zLXr!YXCyhmf1F}caC4&!f#9-WmR>xO<{431Y^OiP5$Va9f>$;7BtLH;-`KjX8!g@Ua8iMc;}sxcT>8Z4_u zT@Y#ASx9?kV*V%xkN4SlfH7CH@ z6Mr+bs}^+Zo5SHYo{a0_?YVg6U#Ln$*aiR+$c4qOz{#}o*h^c8`Q2I`I$kypGgJn> zBOj&(uXlSn-3Y5X{J$kV)AQHQ-!p)4d?I!k9+g}tGz{3E2NL>}VhV?pWjCTJ;EV!j zk~u7rsunP0HgO%5D1`=#6^27lL2T$rDK=;pQY6EauwfxmR67k3Ig3q^L61@MpvJJm zpKvc3bkbgHP^^$YfVoUn=wzwfp(;2{z=MjnH^}Ty%Ky3iAlYK{BnhR@0k?3_o7fOf zIlNMaX%NA-iq*pPN+ZQ`sQ0cd9mqT>iueBxL*(cSgog||AVMFKLep2!OaRn6Ue_Um z%YSWli!t>wxXpwXuG-IyM!f*-Zo=d{3NtjTQI?#&~2Fj>4FroB{i8M#$w zy3ND<%>Qh9{+9+RW#+;cZneXT)eET*6%y?wJX}Aw3@30=y zTnckufeNtTLKo<$NWp^y-#P)G;#5RRA(3oo1bh9TeX5G3QosyAU88|EO+XE1k*lYn zA)GfwBybQ3j>KV(NP#_KpOO_&1b3lJyqS6!nlb@Nr{SHspq;nX`d8pvSD*_544($| zmO?@RaBdXLodkEHA#Jb&WH#~+clr<)RJZZpNRj*wo2F3_)-re^wHQb4RDDN!XMSzk z=fe%)w%&`Ra4hue&pkg1OX7d;VG=xm@dsxOX6I@yudS8*d-7xJLf6%A;a#e1VY4R| zkBk+8+_1<(8nTQHn_P0(msdOAyV*2z1)&RyARa-4PwD%tkZk6||6SRr4Z?B3IQas# zV}5yM5W@bv@nwXCcXz^*+{FLZjWif?r4y+69ys$oBxqLQkXWIJLu$ajQfx=LOBEhS zzm`ZLK~#hZ4H`m4Rr#S0_`!N7FqVD_Oey-N7}Ux}6Gh1507MQr*I9v?ilE`LbCZDL zeky!w7P(dny-!0pa;V!U5GUbb&vw%TNwAGrg)uf-j_NhWDx|MNGN_9BROBfVgf3Db zk&wCDpb{!-CLOKs_k+MjnsP9`Y|u?M%$kZgx&qE*gIF}g9g%{u3?tlvIZc8^Nznp8 z;Sw9VjRu#aUE@Uxm$;DD2TJ>;&=U4|wrtx37<`s$8T%R}5QDEzU@Xd@hdK4Hf0Eu- zRas4~zf5BkPyCabK1!;8r;<+X+Jq4-uXKL@*|cp{clFULDp3q_?tklin#rH|nCXrl z&xY%;srxP%)6u1~5GOft7mBT?oL~Tw83|L7WBTkk?T9T*H5G-E$OYzKP_`U9K8jRimt0637WcWEUhepESFe$a_b@tC zh4-ZPtEP;R%uEdNb}H`BB9(ArLwTrAGfP?hSOrb6#iNZZ;jKf{X1NMRSog+ygB?iN z7Ca1$*g|p-@ZT~`%X%OOIM_|*U`!D(q$Mi?D2q0AQ&Qp5^uSErHwdm@2ul^eUGs$M zv>%MJ_<)a*dO(_I-g)qqnH72p1_LsEWx8ozUFi-}>+Avbt47FatAWg);lcSyv3W8Jh=E^)G*?qajcz<#sVUvlGRwN=~E?4E%3?CWwmOZDj zHJTKe=W0J)k0BJ!(nVUc*oF#r>1PmhTe2Scbo5Mw0bxu!(x-w%=tY)=M@_P{JS1PE zKrzJy)QzI3wJ7&G0;|F`LrR8v=0DBC?oHid^hxDEooK?Iv@O=G-gF$1 zrQsW8{VY84Y|<%h{Rf{5HXNTW_8Q}VOD?pnAnK=3{^?=dowVJo?PYp@k?3Jnyq0ut zYTp@cPg}FbsAA^Z*w-w@^_&z!=vxFmPgBY703;|Ra5f8i>@W!msbcm9oNV~7RDWld zbW|^-^OU3!WS$KbB1#ly^|p*`Q+xDIyUYdHjnozLLtidf|J`wE>pKxUcE@Hi$=Pbp&c|y+nY!h+?nAMCZu`$rfUfeGKdz%AMMMI zbR92lM#7V!Yo`HX~5Jxt2<(WOh}qxng5XHd#n{HWvK|%2ITxd zafDJaEr<7oRI?AOuW|k!-+1*6)VL=?r9>jI@#uvaFO1?l>|G7adkdICP)Iv=i2O`E z9F`cVdU0l8!z&U(^Ifkhndd1~y0#fxFFAArQJnJ(-!j-K%pRrWn3Hn#AG&`;iOjVf zQ@Jb!q*@NvVOMN&QEZmrcJ-i)O?0o}HD?F%R4CtL8Bm=M?j%{4DIE?4zg* zooI(-cHsfAq!xhL9Rh13vfybAP`lD>d^x+^r0RQ(Y9~T-gc?@pFx?QE zxp+wHA*?_yLXk^i8Z6u26&%yDk6Mgupv(_?6e z>xQoFFIFMBk16A)Iz`jq_u_@nC-AJ~(wlwphw5O)(xA4@q2dpwlZdSydS}uhIN?lx zfrkMVUnb!}lxzUV*jM1Ixe)&Csp$zYDKME-;->U)tDiN?V9v7?O$MC(u9N(66a+A9 zJQ<(}s&#gkWQ^_(&qBQ;0ct=X3cLd$5JQNwS@+5VKM_oaOomuK6W>SYmO+qUN+_K;G_g z-_w zuqf9jDzV=#8sGYtdgtfZo(o@_^!L2C+;)9@_uiDM|A@s7*BtIuTBb@aXP{hl9miWQ zd~b8Cx!?cY;!>Ph%!|7}(B-%KUR?*m+>}+W)m}dSvL)b!t=>_kj6apLBL4zcJ93q? z_{Yzg;>O&W%{|#m5@5*FaL+bqfFwUq`rxvJH27)eRA|=;N3fY($GHwW$;k5JO&?p0 z*kGoNXoUBS^9=)P6i3@RCb9l#_*;Cr=6~9h@r=C)T%0mu=r(vQOl3!*{c4*JowFG`?%`-x*Z}qZccIM zJ4pC)!avxH29eLFLHV#$nFE(b<gW#QUQN|r)#hHr8jezNa|Cyuj6L6zVyB-9e#x_&iMP;S zBay#Zh!WIam-Hmzo7Lz!5wU{JLa2^CkQ=KucF$>ieK%?qiEmicnYgK20Pyqxum-y{ zHxwCIqk3-k!gha3?n>)A3UD>qbYvANZ1`k-feQw&0Ao={s4BbYwc&*&;W-oV$GMY`c1U z`C=np&R6_>OjzV0v}v~5M64cH*%bIr_|6)1GPdk$r@)eN?q;JdNKyV9})Ma+}0&krrO?@lg0hTit4Ks~UIS-WE} z?3wB8yYs$@#YP+2R2yxYp;gE9?XT4hDBE`bFtaA56dPZ)-Z?G${nh3K#7pyYy%oSP zD6TqDNHleWM80r67zSk#h8V$uICCf~7&?+TlrrWTKQQdffn*epMGMB9XCW>VuF2D5 zBhc{}jCbS$pV2*>&Ky6uHa4>EG|ZYAa~|=EO!ba-+J0|(d~9rdT-7@x%JnsIJfg?@ z5p#T~YWSeQ+hxK#qi6f%^cYL(HEt)cn;GVh;qup7BD6-h3p_j0gREuVo!!3Q>N@`E zDW15Ff4xI}@ZP}p*rt70s5+@sxLB>{ZUw<|MPsUr03lDDAG))21wR^AS@x|2aPm@B zsTh5F3K3 z+5s=0KLMjJyx)Ht^H&V?x)l)cXN;-%$ZLFh`=9COWB0wbdi!kk_AHwAE%M$>f_fK? z?-~#I`fbc1797Frd*tvSiy8El>H7r=u>|<`Y`gu2zW?+zej~naH?}s!PB3wI^YY7n zAZJ9~6F{dGWc?mWv4Y#g&efb%*DQg;YhgR30`zNpzNe{l`c8Ji;LYSglb8P8vnG0f zL?RYWdGxN%`ECFE+!iTozk_6@fUE9g*uPIG*whGH6vO!Z9ok>5tA7nh%|rM%FCRO8 z+dfZa^COc`w-D^y(<752<9qF!O^EHLPd5i%)3Jc>U1-t1{&-*-HxSSB>kLr&CVQXg zI$?f4(ZG8%#Wm9I{&LUwY?NzqhRoI+!Z`22H z%p-8je!H0UBL35&#}iF%1&QVRk(XXxzpd`R?T88_7r|_r)%^)-E$$cTx$B&pB;vQ~duvK{_h|97B#iz9$^jJ9{)eRq71G7`FD!|0X4Vw+HglI}N*CB#jyPme```UNySR6PDTA{7f+2({VRuMRox$l3 zjT;m%pFj6h1sM|R5fL}qZXcmH{BGOr6pQe}67lOW*~aPPL|Y39KQYmKXc;o#z)!B) z9%&G^Z=E>Qd%){J>3HbYafayry}>Zsy{~QH0aHCM{xx@#@O&>IR8vS%@jJqY68+4ER|K zQLbO3EJ+X_^z*hy#Vk>uTP&15b3Qn=?atE;^PN_IYx1E~pd<|uf_85dqCy-I@Cy0m zM(r)$I-OGTu~-Oj{XXkep9|i!XKHWptb`9tl0I8yJCrfgv(6^wzewpsm!T9^%dpt! zBO?HT*0}RrO-Fa#PAHLd-;-dLW4!UwzV2;%V&T;u5iNs;tNqSbKnOe8E%S z_34OfBC%iHzgZjL8NWT1bmow^U`=5ZvBm@u1T#niQ@@ah(l?0*s z`F@*zNz5|ndsV!@QG3({(n}_)>dDoruePOg;T4xKqVO`r=&5ggxtwrZY_Ni`T5;Xt z$-;HjE!1khh#RZppNH4|s)!Gm9L0QSb?xoh1?oanpg^$~;EUBkTP9GX=rc21jN~IW z7%b^I?_rzyN(Bl3rVo#!e0uG8 zn=L$j>ifW}EpQK%oCEzddZaRx6e4ra*+YQs{nCiraywcWQt6BfCFS_qT|&3QLfqhy z!MIjIfo3u62ACM>SEZq!gYPFrr$wl2$%&T2A~;1mzCG-=citm~J)wC9=MWFl&p0ySCj}@OVx=XM6lfPtplGVyxq*os0V^Zmi z%Ku!GTH|(dckcQiakE-Z4wukk)JH(d94>|pk`LS>uRfH07C|rY4Pto)v4ThnOt%{x zz*Wb$?Fb=Gg^xhou}#6kMgl$O+Wbsie({Zgwor;-Yiv%~D$i;?7H+ROOL+hA!`ID` z!W>sBERF&*wvP*%hHZ)kkw^GZ6aRICi}C9le*4r$NLNb!LC@A?@01lp(~=rJ*n-a> z#%hQ`i(!dw#IZA2vk9#G3WUXm^nvaFz7mB>1;#6%!-KQ$6k{9v7$Lx+7%V!azzl!$ zo%Qbdk*tz!ACjlWXa~$?CfkSF9~va3 z{*#CKRN$d!^%sBS^0?KK-jOZb2@!&OvZ56cyvZ*wTA0s;aJYYyHpcHmwv2EC9)NUc zJUPWaM@`+1eKv@8iDPb(&QSN<$f^0oG9#Qf8T*2zZ@gP>y+L-5TB(bl0>zv0NI}7Rrt+ zzX!kMe>L`zgRAFbCFi90w30zD!qxCG*Cee?iEpm#>Bt{_X6SjwJjy@4=Ejbg$CQxK zOn38g{G-#>6>y&2Ij@R=vM$$0wS!(e%7ZTiP}ZN7)vHzORvke$5271FB8IRnY@>TK zlD5c;&*rn6gFALJZ?DV;URJ*v-FD78#Y=cs&2m>zxOM-p?`2lfT^Ik>jQHuf+)(`9 zxX;c1kjvqR(={$VN?#mq{ic4}xtL!4@?px?#~+BB55IVZX8lk57FPGg%ldr}wmSd! zb!GCmz}V`%^5Jhi{VAL4F08G-d)yK;oT+>J#Kc)+9o@;^UDXHwyxRm<>;Cz8Sox}Z zTDE!|CuuI5T?c~a5|VKP*-?vF1&t_9Z-j=<5|9(G>R>$=?<2L#x;&)SksJ1%zcH?KMP!x*~%t+l}5=pbBOIz;+$Q=0a5LS7-=5p$uGPYeQ1NCXW=; zJ&-u)#v)}Jo_XcF~$@$-? ze1pi0;!&+5=96yvo+A@|W}|-d5M|F*0D^X2EZMHSHQ2h)kh$;~N(o$@g`9G>BTq7` zVJaBC{ca2ac_^wx0P`V!f#Hk|BJNtH*y%xVXYKUe(du^OA*8C`{3vRZWEN_HQ`YxX zyvbpd8tl^7my5>aXjo10P>Le!pEVBGA8-=JLC`l>uHiUqvwLqs_wO1QUho{SI0w{ERTgETs2xd?e)I2z7*~=EHdE1 zt@_sxQ8Dz#JEAI3>(mJuq$*m}3%6~b%>sfhoU2FJuE-d01g#0-WeSGd%>pyi4W{FZ zT!b8M`It#laHZ`4u(G;URTG+ClaDP5XoS+5045iJjghK9AxaFvezB8AbQba{V^p@Q{?+8SIm+~Sf$Ld+q_*gi!Iu8SfThX|Rj zk#E(YBwkWsq^dr9o5TfeU(DY}V8FVZvMiQp0-8vkl`PCwN+e-yJJ_oGNem)XMPM*f ztKMI+8($iMvbN`Gy=of746LC3f0WXXTcnOXbIy!%5{0SdC9^LHy;83zRiEDTF;(G) z7cugpp<$!DZPvrR+Ti*-jsiXV&>gI?(u<}oje6|64=W$0)^B(d`#xD~=ee$rN4IW` z^W5yPqvZBew%iW3^<^#3w zEl#lxmc0_zovBsH0=3}xcvnP6UEIJT= zxCU)}nDt6EV#cmHHT0b3{uSW6WkiFRi+cNH6K#ZmZfv2+ZX+r2wbjHX@uHEDM+u~6;js4 z1sehbto%ugeML>A1>mBdRwFdx@KkJQ+3EpUs1{a^B$&zGkSH!N?v7B;mcF`XQ z&uT$4e4C)G96E)MNEO3WoO%nj?d*~yd5eN7cF$mo(Z%lffkrt`9?m`Tx!8l&`PV^v z=PU2~lIjb47P%fep3t&s+8^zv+-HtYYt9`JD_wXOP!umd_wiv{PFzdt-AP8q73DZu zkxFRCt*G!U*EpC9zy68BW^V5~GrD?hLmmy|Nj0QQL$`kU}P{vatas!m8G;pw}UsK8kcX%I=U`VDTp>7g}jF^R@_^Jp@Dv4)Yst2(NZiYI@ zm_BgxwphYEbhm6kJ)VZxZQh5vNE=iN@)vOAzCm9$Tr*QzU=kLgzBDbeeu$mu#`Q1U zCFQ6>Co#;&WMr8e94Yj(H!l6Yk-qrqNAT-sWP1?GkHb)@@)MBlYLMSzw*C2_{Lk+v z)qhs1*Vn!2e>Q@spR0i%;eU{9oL3h3vF^{gYRKL2ziqnfAF9__vwznZ2UR@5;(j=YsNL=}h{wJ?Y9POBn`sJe>~E&7W@8%?}hh zkwYe22$81cAWyu@6y!!RCoU+=9h?Xt$7q;uGKFuu8>v8%Yywmv8o2(r7*ngaiJ16C z4d8kV<>d_<%0LFA{jA6eZhj+`3yB5L-)L&Vy~GNJMV;X1VzG{9mVyLOAka;!z)JSL zWCBR5kVh5JQ4%gHs@IB2N3AJatlh;VGL$i4hFGwp8^e627d=x;2=2r5fNWO4fisHM z7#D53UUMQ(SqjpulA*$YYy*bf8lWNWQ}*K7t^ouuzNQ~-U9K2F`T_bq_b4lUO0q(? z5gtml(1;cCVmb>|C4<>#ktur(zsXFcAXpdxHAXOvUr*^FKd3PcOqYaRts7gpFT z_9BA;ZG?;FGVRt1U6Hb&r`iW=24E!KHbsa{ArCd3Wj>IFTI{pzDWH=2Fe6~24jC*= zrp)fch>E^EdUZmrFMF(yK+UqLgW6E(w2crgDi>)E=!zM5H?YmBjJySuTN>CshzKz} znb0;AZ0P%Q+%@=i&S$^gx=#23=JcG@b@$0PiB*PkQFFnOdI=Pzx=uuRU(W6?hH7hE zG@h=qM#B@LBaP@PSP+{0HnLqDNj6&FZ)&tJG&*X>+kHojVs6i(UWwJ(`FlKsYSF^) z95)~OT-W{$niO6FSwZ#LcH#g%H#marBTP8qdom+}@WbsV=5gV4`MDlZg~4M;6z%rm zJos3Rk>z>iF*z{}>4>F42J%j)p$r%kdXFa}>9nIt3*Z2?&%u64by5mhp`br!IV*oIj2qsPQ4517pWv|CH z%Y2!qCCPGIW0;c|C!aBF()#oSF!w*ND20rZfA{)M^a;N#26jLzt_+xn`%KrsBo4!9 z3}zHIWxe?B>|dRK_Jd6a#;r?v7%cDQ0ftEwuh0}`ngOaW`QPUZI+nU&u1RNw1mV2vDQ{pQ^ScuYkHQs30QA_{r?u)K*QyY)t-Ovys5MksESLC1s! z$MV6&#?{m?0}pOwwsC*azpzOm47TgQ{^fMW{G~PX;S(<)vIo%E(Mw|!lPDSniWGTW z+_{Lew(GBX%%iXM)%=2*&h+U#inVCIXO-bPKy=w7n1Avr&ckcpPSF!`kX z?7C0Y7;KTdJu1#3c(9KG`KK*z|3IIe{T0iFfpejQVHr8TSbveaicaT2N-g-!Qs24q zke?L?gTT!`rfWY;f9*xI zJX?zIiGS;qdwjw54R*t_+x_()T9U2Xmod_R5U1WaZP%{b|HV(3?e;*$_1$e=Z$~j* z8!%V3iBPbe3k|GM>l^wxK6LQH1`UV4lU1igRm#76)MK;m>PC;kQ)vpbyHP@#@~UjT zRrYToMFvT9*ZSLBESvz?etTIWnjHl zkdDFglcBYSMzY`C6Y>!;t%EeUlMHSzJ9IYxIB1b_az68{GVTx0LIisvsNJ^TSsE3uj`!xv?;L6lV)&VZ=c2z>|^|W)CCNm8r8O`yU6G&ihI0fg;N^xT*c1ol@RE zzjof;0%OM7hz9y}SWNR}lT*n%EwGE3<*esQqoUQF=ChxfD-zX61usValk8^0;!kmkI7-HY7B7;z z=Ag5cXE?Hl-?_b$BPCSi=?Uv06VW-<$AvU3!o0W8i%X!%AY3k-n5V3|5!ypnmO_*R z4p%^BAdw8}_wQiNXINlf<63iL*l1bC-H_uR5QMd(!Q*l{8P$sxc9a`tmSvJQG^K^x z+*zsjB%hl+&Rl3Oc7=-UlQ(~De>rgQd9+E@M`PwnY^G29`aeHMrn;VI#-E>ZZqZ@6 z#H^gR)_|C+95FvNZ|A|^d#%zE3o)-w_9V5M_b94|Lba9D?i#%e)P}XqfL*mg|sv!(k@Ge;Eh36 z_3ftX@t}A+khYz|wF?Pas^HZIx-PKcNQO^FRVYJ6xy_^lJnZv!dHd@Qvzwbh-G-+^ zlMuPx^sej`m7HM}l(|=@y>8fZx6`SKoTUVF@TxgO9|K+|{549xV;9le;dQwgBI|@N zND&#Mt#-iSK4nKQF4je+)t*>s@ENZl!(jT)eUll{xNwo|oS3f9eNnkA+aRDRIgN7g z0B(#nxaadd8=~R~5Aayc-?2H&`Ae5NK4Wd&_gTcO3jr2ptcuON5@sX+lvdbv8&*OSj>fMXT8|gS7xjPoZDhvXXZ7P`)|JPm$FFW) z#a}-5JpTA8tI(&Pe*c=i`J4T#_jg+B=BqZBC0ClVXIuZ%GGlF|yPm(S-n^Y(opq=% zOT}IxSA%*iYV9qS<6>-g2!puU7p^79*m&+y_no1*BW|5>RLy|hx+2i=?yM8oWtJ{j zb1YC}HX3=kX)nWE<46tZMe!+!JBDxFm=?2pJ$0Wq4YB$_DHg!1$RKIg;oN1oqzz&~ z8y>9$Ovu0(ptlNuq;!1_7r_MpFPTN;Mn@?3npe!;Vw!qD0lYggHXC_B697$&5KbM| ze1OpZvi07%n8QsG#s6^OLYjiX&u1?-t^ZHQ!OBI$24lHd&t=);Sja5yK5d_E%x|kO zR%XzE})B0V6%OIDh+Gv|8e&$>`6w;&yl~zd%7Av z-LTD#thwo&bcuK$dqk%*D#z$oV{T+BXwlSVm&bxJ$JqAD<*um<9|C~?toz@INd2G5 zpAv;P@0>O)!kFExzWG4ejAgR>_rX&w_ix^Q?O6YFNz?ptD|kS|GW<%IyX{-l8SAhs z=83DEF^w!34$mA}xqHT1!y5HMQhe{MSqyWpbxQW#Qx4g`q~@vhCu<#iRtw+l8}U2u zkn@Yu7vff2%gEts;Y~{=zSYd6zX1*%ChPu^P>G-BKB4b@BbZ9^_hT&dj9u6WT<;!i z>}f}AZ*vjE)l=Ih`F-rW_+$8hif<8QNoVu?>b4-Ig98K`0&e4Smp4ca~8xw=uB5&kx zylf>W5$x~D)SEIi(};{k=9~d1Y^g5~&zWRt>`twzSC3S3gx&rT`xC|pP%30;g%G{W zdqXk&L-YWG!COM8dse{ZEcsspIvW8d#A+I!k>3GgV){ zcCyY&9e(&H>-Jyq^nUuDv~;{nWL=!AdPoTqYrPuawd}qsEM}eY=cI}};t1x^1ikgs zFtq`Kmuork{j|K98A-?8-lm+=s=48So}`u6%^^<~{vSo>{g?Fi_;I!@6$BAn0q%`6 zH8a3H!;PkexX2ckm6f;H;S6V5)WoYVcSm6v?7n?Rd_u{B*Uh7uZ#=U;|{sHiS zAKq}zYdl}nU31EE{IRaKUcbZ#_oA_0@-uOlOc2Qu`Ziu=Ek1g&!slY5Ddn*LX?S}o ziusu;y2#qCOiFnenQ$1BIC$Uw&0@Q8Fw*H1BB7PGfDTOUnMZc;nLSR*?d8r+U6tlN zj_s>PN1Y9Y!0?|Vh z;Fnq(;2x7Dpg`Xs2Cmt&Upp@Vem9*LTJP6f{PQWSDSpkKA&c`@<{<3Les9_y#S6|} z_tx_dN=nv`dgWw?*IIp6oW>F%o*h+Iu|+Bpiw&R0JABpB7+$**UgF`0TKjKiu!P#H z8O59JkJ>8vwDF8oYgFYB@Qcy;1o46#L!0<>%|KDvtDBKeSRqBVgb)VPScnf;*ZstjUn^jQ#bY zQbHjwyE4DHMn*I^{kn)4Sc-w&xn5IdheEkxa!jLbXFcLgqGDA<#=$=6(Y0Q46$!8A zP;I>!lDwqcC0A(5xTkj6=nJt?XTZTlc0GqH7bpdQ)F^u<7S6p%MUx`K?crxcs%ZUG z(Ja$w=NUOU*Q0bZ(_E&H0A;os2ae2|3KHdkT7Jg8H;;Zh$Q-9eZ^?M<9BSUh!n7># zU8+Xw3C~}+?-{HpPkGP=x<0Bp2z}{M#t9uk?`axGkI||>avz=gN|SHP2N$)_ulo}~ zip?A_ZYK7ozYXagT9D^WrWaT%S0FBxo!}!BH98+7cq%9R8-mpa`_PcF9N4=aFJ z*6c9Nblz;3m!QKWIoNK#yLL*YpNiLZe(OS*c0ZN6@#`giKS7xG#YE4Dn%Jxbrn%2W zwS7sS2vsj4HnSv1BL*SKH>hM5!r=P5zK`sAx8mr{|XdCeG@ z!8*f^o?10A3Mp!uq(w{tI@!FJt@G%{@M zVX|87$^}!{iY_;*{wni)v@xfuP2TtbsPd$wTD3Wbg;hb_Q-@LM8ct)$lbDLGl*p{B zj?K|qK{P#M)DoS#A&VAXgyBjy4rkEpt05o(3WanUom=f z{g@HmTCfGj5yCV{mKrp9%YSY0#g~sytp{-cUB%`_y?-}-s(%cy;KainhfY9j0V@q_ z6+G$O{pds16l@HKVRDTuw%s}h-?E&JHepDJ;{r%E9Bi|9sXR?p-?C z)j6p`^bzI#lWequ1+&}F5!zRdBHa9iXo6??Dt%_zMMcNn)i;iaW5`XaFjp?{u(>PO z_|0&bla3?D5k(;3j2P(%ki}wJ;STURCu;yQt7#7UjwN)HXGDJoL-ks{xdCd9$MNp0%NM=`h+y`CWm2ntW}s;KS0pEz30y3+>h0e z51e;|VO@1#=29#4idJN*ppffTAk2NmI3hC$xt{;spmC)n#4wjEj@2@tUYs#1d~u}P z9Xaz0NeuL`8|~e*AU>0V2dGG?0WOW1>kB@6$v42+3-xd?;|h;6QX}T@Mfed*9c>`ji~n2!yPYK zXapMnMj!|d;F`1OfXW{!gAOFd~vexS7fk%kgRbkja+#&H= z0m^YQ)DYQM!Gp{jaF!W{m?B~30uvg4cO1IR2mYFLAYBxf5zg=sVm$+5Z%aW;azN(w*$}^=d3cEuiU{hfU@x{pD@uTlCez~o zr3nY1X4w#aS}n^`60LGJ%tXvc^O{~Q4MYV0ubtI80yLr>eV)< z7Loic6zSSa?w6y;)wAJST<^{e=uXexZOhDX(dWTwUfYeXbN{5I|aCIc=jjNbUz z0O52$m>QUAl*`nCg9SN0bQ21IYyT45bhp3{1_{+y-#Fm#d0;5ri)mkA4*(>Fdu6zL z#TUWs0dE|^S0-~dH^S;K=e)QX6LdSo+fU%TaUSTbyrk$gD9nzi2J&_7BpA>ja3#T5 zE`UZVO=4$Z@6?1?%3pW}?4-_23oy`YLi$IZDukLdNcJP}Tme+B+o6&$5^d>5BOo3J znyWT3nuWOQ9l=@TR}SH#5*|JRYZSnF0$8EIcn)ARBrvWWA(R5Y>c_W5%CB_bKP|CX zb3SyT2)d{<*dxScjGMtmVEGjD=n+V!8fG+WWDnfK{Di6;!Pg6*r2_ad^2bKC!Bc?Y zH9DyjU?`%XU9$sC`4W>X08vS zQ=%5>M7BiVF@@4b!teDG{3ynyWMjPmQcJ;QD523S9Oyj&5;;QL3P96Z_2mWAN}

bk4c)9O-R>kN)C#L`OgMZcTDZZgL8b2c|Q=HoPhGn$7)u@>d~&tC5^SB0+|( zN}WiOyjn~9V;42mg_(yM-~Lkrm)zT#9ohdrL#AQ51b6wK`SY~q?lK4yqoMs66V?EZ z;Ad}DJli;%bu1(+OlIqyyi-4~FAXC-v^oZ1VI&(HG?*Ak`sJ*D@$hS%oWp#GBTK^P zX+*hZ?{5IBC42$kr`3CA;1Zg`T(QDj3LK3#&8Huvbmr>W1?C?KrgjyYY!D$WQfS>J zW4_{_XIb;0t2`QLKEBeWi~Etr82vXnA+zxlnx#_B#T;lyUz}49+Fbg0^gSK zR&i!qfjaJM>;Bfa_oxMSNAIW^&A?ApUVWWXtVs^fX_J);qG~96SBp=`4u7)lsZD`c zFE}}vkjxGR5gD&23?5$h95nrO_{k#%r-!$&SdA(ml&O(<9N|shBF5~T5=gTt<+W9L zN8!Ef?-jG3VprES{){O_gp^+^8k#)(F786ggKIUH3+k@z^A0sMFp=^mpIL1RZTXNL zWGvCmfU_rFo}IDKxN<3M2Y;HW0l~DLjPzQbJAd$&nR< zs`A%#mZkD?*FVv&CO?#k+$Srby=HDX(wYh#Y^cfE)++t5<4FKuhnD%-`^(6H@uo8fo{D`M#n_vWC#<38+zjd|5K{p1_{>K=pU z{Qu^j*DJO2UzPXlz2e<>b(O1?&MVi#lHii zPXEVVYcvIz`zN=@?@X5cKv3#kKdm6jvJNCzm4>SzX`FtQ_pZ|>Z1X=K7JjZRxcR|W zva+ZT&opPO?uXCsG;tzpZY*mHyxX5?%X|*c%x-J!-kTJ$sq2korX7DX*q|5-`5Qf` zWieiTz%Wr8WX;evj}sghd+hrCn1gR*jh}$h1)xb6eMg&klOqPDK)j$PL!KjWp1Txz zuhERKVu6}cKTnfsSF2DqAl(t_nvKynuew2HBZw|P(p}mFpJD1M(WiRC+iu#-CstT= zEgV)|^YLEHwrQGtcT#nYaCkA}g-#{R27%Lc4oY9s0zO5V&?;YeejrH6(x{@lE^%p` zgdaKvpNG!czS?clW~l%IR9P4z4T$;O!(M=aH8e|Y+c@y^ry5C*Ri+e71m4Y3h0X6+ z^^}=>F+0*%S$yEkb;_h9-xUkW=X%E)KJzV$jc>LS-2Qq>W4jS%*X+IEMvUAB61BfN z#QCp%xJpNespbJ!rSz_54q-`Ff$;?8o+>(rjS2>wI*s3J$~qVZY}0`71&|0+%iZi4 z(1RMi^Pdmo4bYxBX)lWITi2>1Mg-f?i;DI40!_XFEP3xNN_v?4F6807$<&vvs@7o1 zPbP}D|9*FWz~5PaxX+*Pd7k%Lo@bMj=$N~SFOB@}R#I>i>^EPEANWp6om;xOF2kG9 z6#}r4jCYe6{-iE45YT#j=q!d`|4>1Kc%b?t;Kffe==_*yU(f$e#(4vl^Q~7A_E)w7 zvO^)W16LD$4MY4rv@_&O!QQ9gOqx#7uvxW(-@n1HU#$&FzZhG;>k)Yyo6{{eJZ03$ zsAd1FJMs~9Kk1aUx4kqZ@)PK6%QjuxRPDOWpKzb#r+RNXNPm#c+-9gjB`Qan<#lZ` z%ifHx^r8;lUg|%{4oAEb4l1CfeFF`NAP)RqnMrvkOcN#L!FTdc@pJZD=rfWT*D@6P zk%~Jgfq+jN+%yEzy+jt0qY=%Zt!oQ3jin) z*y^~Aowe&%x9Y~3yodB|X3WQXJ?FqF30xh@p$TqU?LA8&YF*`*ks!yjC7rr|r`RF7 zrwYpny?@)*Ua%^diaXNmYI4(Q=ZDAyyus_c`)%uYZRG6k-}aK(`|Wxja(-+av<;pIU*ORmbq@K?8%1z?lKHG+<4wHFyfmVfVg_K1T)Z56KC$>1fj6T zePSv?vSdP^q)OC;O{G!XGEF3ZlFNxlON9+U^!MT zWu4a0hRx%H=7I~x+z1W7T*e1xkus79#5;$5*yNBaRaD|}_8sbGb3Ylsa(S#nMNCz? zJ06dUG}QTL;G|3oWA()9JK zSxoTehlayU{M`dLHqKKHQRiR(bi0#@`kSJ4?4i$J&f00u=f1^#oN2$8_FMSvi)V{2 z?RqYKV{*!9NrSO>>j(4-3*Ep1N4{axB7Hya7+dx-_z`f9$PI=AKu8nO4`1hu&fgK- zad}9^IYb4KE5rzZ9WOra+X)ebgIpErW@k$-#K6^$yDU(nem^A=Q--%6k25z5q^eAa zfN^HnJE9z7)hkBwIHqIrw*^A`Ua;u`KrdCYlmT&T^;|A~!wwGhp|__nUdO6&{NkvW znd3eY?|&9lHvRkV)!OrekIHZQeyIEPZuPLeek$s9-BjiOc3-O1-FoN;@t@AVpMM7y zr*e%?eb~+I-?_O1-7L^12(^*~qGaqC9bkVqjDS^)UVvm&6sgzQ5RBJv2>kYfSH^tj z=+?-1Q^~`%EKu|Lej)@G|CSjk^x+G!!EGd32eDU#hlrRo5LU{zo~Mxb5^W;ea#G=w zsxmDT+J>>`ER=>3?Y6QuLsFEQU1gL3aod!pbdI)@Xf3jx(GRfb^|XypGMu0cnbN4f z4QDMh0C3!8K*I% zp8mRkf!R~B@~4V*ovZ&P$;&J<>6$D-WOdc%5$;NdfXw}H;XLZFL~ntm4VYy2D$NN> zW_~*g$40}21k5CMQbco#;n76~z3PB=CDDn4>Q%t%frbRGPOH%N_$THpfZ8fnqaUaa zf$b~{N$6mPf!&5pIP=xK`A@KhDI+deZN+KNQa2b*`l^G3yu9}%MD18Kos){JRUOz& zEp38QotqA=_VRTAXap++$_cN;Ni)B$ij5t=Yqeo$t%&8$EqIpoQ7(upsBEo*G*mtG zlKD1xdJwrDabS!t+3%IA?k43&iuk9W#eH8#mG??Ih2%iAvojg`lQ0dK*9^f&~!- zsk?pm#L+-VqHnMH@wDvU!!LTdkdS8c2WwdnwYyr2hGOC&X*+$75AD71DweM67M8U( z0_-Mhx1ax%(CI1Z1G}*=F@tBBVRVT*JvMmO{&<_-aRn~5c0SpBDBxr6gW5r<=*Vx; zFx?MFm?lPLLh!Ya3Znr9MUXV<8>^!cFZcTMT5%!TAtTNQ5D#v+5XBeh*jR{CI%+~B zrw@0ysl;jF792^bhrCoo;qLY7*jWg1lO`PUj+e%N&ZFd#eu11vDpt=AUaRV;4X~zN zCJM0?fMY}fBuo%Rgf>UgyeTX~hU%}k2#&Qhk_hlilk9Q727%LsePa1eJk=R`<_h<` z30GRKm0x8Mzj)!nxH5kg$SnqFB&az&&+1 zi7P!50pKCHZ@ClXNpOEljw97q~%!E0sxt)l4~8)OX!iCcdivCavN33ke)m zIHyAVpe~8qZ0$`L%sH6&cw3jp+oV2@_st25bO%2zo}hx&52Zl|H0LBV?qDP#G|9>j zAkc#oPb5b>d(C-uc>UYq1&W{W61`x)8MGz$2X>X0UuTY)_FT~E*~h(qQ^zge zGrLu+TXn;$^^3P+-u+yuTbe5PWqsdzZ~alLG2Az=VI^&0K0x9RB>^@G!qwaV(G@I2 z62Jyg$L;Ugj0a$yy6&CX*^<9L-@JlHM}QdUyu zovLFHEOI7x=zzeuB#!oRj`p9p=906kT;$7yM{~~}hi6NY#?Nqf#~Ut(9`NDj(>{d@yKFg}Uou1D@o&up*waog@Eq(3oL=LCc>{*vMht;$G-kGfM z@3E;Wjm%`k7OpW(#O{^6aOl~BQ78tZwIbkCC4d1x>l04fVwsua)`??+OyBYj^wFT# zl;=!EHIL6G(AXq`&{U%QcRkdgS8PraB722v-vGV(1zKflwGxQG$iDg5(_KwOw~9>Z zY~%>YDOgE|-$EkAtHvDyBtvAA4-BP9?5YLXCR_x5w}GnyT`i(jj~esASOE)506VI= zeVP#ApEbr=q`hcgmbO{dH#9>!0gB`^w-uAPebK>^N|HBtB{{aX&+LJb_xinr9}vIf z{oKg?J{6H@Jev~<)F!bh1SO5u_V1$5lqe)DbI|xHUYOE0?2~=3nTdagQ3Ti}6iAnI z^wQgyigveTAtf02^tr-jlB3OQgFA7MbPfvHZrvfUQ2~$?h*dJtT?HKe!dVO9s25|W zCXk8ZpxX-DsG^;)%rctg>IN^ytbo`iseS7aKjTuGB~^UPSM$>EM?=1OH7G!Yp4DH@ z-XMZEFqow|zZuwSK0H50rFQ7_iap+l)PyBtQJ5D^?g5HMg1vt%( zawQeG{5E2*QZKm8O{QbmE7I*{gO9X3E-Os?S=4@j;V?@_uBJ8G2;57S!zQjxB1Dov zH&{$e5@pAEnk`H?o>FT6VPV6BdVc_VBa57WLUs>Z?%ZNW=@TF6;Wmdo12k`kj_Lji zUG@rsgfq<`?5<7Bz3QPh-%Hip_!~Zuoema*%qE2ayuJImryal(H^yGu+lGg57$e^e zVxg2%B4jXUF&AQxuT1*MMjaQedUqHx9QPkyGN(aw0`2X+aa=kF=ZgE8U2m7OBEDz2NVG5YsUFt{4gJq04fh zwmH9X!oiQ8r5&9*|6U!IP{#5Rg!@4{w~X|3W_5xvg3q8=oh95LqU1}Y700#M2QJd8 zUOR8Xbxvo~n%fhWgk*{YLys|z1mRc^-{2oOwPkLtWDr1J!wOgji>d}Uzth|lVrH4r zn#9#%f*s|6%`Q`xawWW1fLi8rPkF+K9VVv$oZ2ghW|kRGM2-M^Fxs_Gv0wy|doLK~ zw2vbdz&rkTOe-Ytyo7oB*O6k#8d}?`=?h4~&0oBQu=YU77YN^{y5{UnYmVw(tV(ZZ zqNm61L(S3jt$wo?gCy6y+wfssZZeB=iIN-%-FXk)TRK{?1#-(7>J0F4KJJ|-hq}w5 ziA0#Q?178Cy-qAS82{T#(9Ue)aq>TSC23m^AK^cFFc{`81%%d35JxUbB%$gRzCL@K zr`|=q+d&<@+h*{soNI*^KJ}$c+2_Q!Z8w9s^SufC6U4dkVrPl>wNm%QX1Gi31D9Iw z%?ZbNs%xbrP{sQr53Qbi@S|E1D;up2oGb5mbHQ_j!qqED^pT03Q%l`Y`_sa&UMs(T zsoj&=q{s3C#Lb@KJQi3}*c{i;@i2zzH;g(l zS8(%w>q|3v470s@gZt>kMDCv!U%cmwH4=5D;gbB14-NY5{bNPlEl)SO zr$;^5sdire;!kD0d$RN&d%mYC?;j_YXUXv_CpmO&!*L&J*7ZG)mQp3AGwxdV*Le*e zkKtWq!WW*`)RAm%#e|74bazg*n5>r5s@qQMJ(O6kWzyDhlHUZL;pi#A<`TAbYjaC| z`^aC?3Xa%G&6>->t^uC)c{}o&c+SN3xOA{{zGqK1*H!Xxdat>c!nXIXtJ5DFV(Ufq zU{6N1n3*iLiR@o>Ro4#2K8$Mh=mWMu&$De4H_>qXDAnao>6WkBlXe1@0x!u)u=J}_`SpYSEA2{>ckQGqs98~VW zA8Lwnbl$l2UdDK9hL%fFv{#1b@Uy+zRT{f3#Z&l z<2Igmv4~SK4wwh#_ctv>kP}TRh{V~B!3ooBJGRZXk>V}pnNp8ODZ~W(=?_+=nJT-zPXI5PKYYuJ4s z8RPjAAeCQLiN3qw z{7rJN^yEa;E&7Xzd^3l6iS=6T9J>@>ns1xV_oJjvy)}a^s7o6Li+t1G*D zs?)NPU1T<$V}^8~i>|g{eXX(71JgND$J~+R%LUF=zCH#~A`9q+q_B)=llbV*_GhAd z&T*fXNPc*iAGS@5G5HmcG}v7djx*ISJ9TWZ$4YD@#^rt)8;6DCTH4Dp`hHF2M^CDw z5$sp{nm-}ebYR-eQ;sX!t&WVIn=wxO@+K5(o9I3zHftUeoj)?_7MqhXku#&inqkLe zIkq;m=er!v*qxVQSy_wtJ#A4Xb+Z)B=vjwsTw9&ge7n{tO%dEqwV57}+%_j-_iIH3 zT7av!bAavIn>AY~$j!x%KBKv=2N73dD+|J_tpVqr>hC(0aquE-nPqxIdkn*ncHG*&N)L~4pf&r^Hu^4Jvk1+vxcH1n(=aWCf0bOGx{Qr z9?H>Ajn2u!*~V4IUtm>#UGd#dtef4;7+IM`Fv6lVw(JNR_(=`_*)KZHse*fAidGN) zN5^pKM}5mI51|dW?!EW-__GYNZlwn6q5`t$}p8-15!i7?Z%UhRj_1B%p-RJ`3JF+b(_vEl+3+ zxoX5vtkBV>GoLB0Yh!Q3rxg8{+^!WDkPsu~VDt@(xZL91J!kz6{Oju~y$CZ&N8YbJ z+&k7?Lt#(0&Kv*kyDj}#I)S*B{<`>9zEMJTw|Dve2M4Jsvef^HmOnD?*OqKzrg)+@ zTs(M1)>rzyOq;u{s`1a?nf|e5pJs|@E*$D3&m4I)66ItvwxO}cqh!n;@u+y!(?Ff& za^mM=@x<4n5LE0(u;XPE*z((E+c~Mlv@$W~iUH~oVk~c`Z{e1S$s_o4Q(k?y6g&vReo`G(c=61vof7bInW(FiN{bQ)wV9uYDjxcjZu2!yGX*yjA zbE2~^9hLcBt~%{gOekKz@LOf`ceVlbN{StvtNSm~2c32k7e@VHK9LJFE-Hi=4N0ql zUOA$Yi8$hzLdCrz^c5zzUl{?woEru#_a~Rq-mr=TXMpG{4(3MmTwd^A%dmjx_N&>Y zCBG9z7dB+IX+FT@;Jia_WXxx39;jt^S%+$z04;7fbYa!U9T!$fkdw9l2K5nNv9PS? zq!_t+y1L3<`$;+Ea=As8gL`P}c83PDo!D-ccU|adP)Q?YtHnmv?kA-6*K|?V(2#&H z*sV})!eiBtqcj7{uXQ9=l|e%;E=Qb;Z6}l4eb){S3?&KL$?ShWGn*fTUF#EX=Kk%y z_o!uYaX>Z2eF~a(o*Fia(w4j=KlDjU|%Dn<}3q9IvVpxMO+S%9XHEN&l<|K?FDb2lj z>|#^vI?C<~nFe~>ink6|QqCY5UKN-<&pYA~$QX!a96Rm~onrW1rjflabX& zhn@VOR-lBUcdd+TC37m_MYNd{GqUKe1IE#EO+p3T+ww}W=18>U^j-qcr0<8uiFXvd zn3YYB?UzIlp5iL{y`3ZrdBLhr27!Y%bq}r&&5kxx-oaBs_ZnjNtTAYDut*7OdG~TU zz>TsAUAOUw<(6Kg(GMoFSGgHCb!nG$5j_8<{oKDgs+Ra%y)VH;$xg{oa)U^> zE;+|CosHbw&_+1Q&+jNyAzul=xIUT0-wEVj9SFww3Dsmg z8Fu*U-qdRwX#4#yN&W2{v0pBVA8dQN$o*OMcQ(XMIG%hY$Fvvkr}k-mclMLHbKZ+$(tj3E z6usXDZ&Bx0pJBepHGAu*fn<|%J|grxPQ^MR1619Um} zw^yKgxGk!=te>)S)3~2(EK%z8)#GtI{gKsurO0tYDqDLIVq%tK{H2e((nml7 zFAb9adR2K<9z`k>8)emRX84*#PO$x-7P>D|Pbx0N%F!x0V!0b#C`Z?`F3E??zty^5 zP$A0Y|8OYU%c%jANCdJH)_|oZF-|3&atp=!L?Ikgzis$StZ6I6)$tj26zzGw)~Nc@ z>nWM zZ+4&^zB7{2b$Q|w)gj8HFvyajAR+As>HO$X%}_(3N_F zN*oazJ(H-@t3dj3RfpBy?6B3_+B7|0<53H&^qLMDNlWA-43&twG$b=hPGBPr7Z9V9 zQ1ABYj0$#W4Egokb({X-(yKrp-e(dGh+fBmfu7;e^?Q~CTB9t~P!zhEk0QhoBhnr% zk7>yi>QOa0lcGl#L&Mp)SpJ4r3Pdv>Q%;KWr5QGkA@eKY_$cV;7%EzE;U25a4-zZN zZwVCXbggP_Jy>-Io@Us7$F?Aw^2&~uN`^XQ?X+sEzel=#{GOR=EVOq_aY_zV1z=MF z=%dBPtQUsm@}~N+K00NeT{I!-p!YG~r%s1 zgIbXPGtz^J_R(Iv{5&;^&lDja3n=k&!~L5(Y57p^h3AWasM>Kn`is#$k_kkKtW#p@ z6of85^wk(HRp{C%M8m&otu(aRdVQL%G;WOA#L1*Y)>ETd*w`_`VK%N&QIZNo$w=D0 zipRBbLp9cv^yPgao)!oz2ncZuKjM9Qfrh+_=4sGUD`AOeKjrcjDMv8QHIL#}qw&28 z&2k08I|^@GspV6NtzLFw9nh~A=uQif-YZnl>7xT>W>uBwM*dCjO8BX$wgoNJD-uF= zA5qTFn^B@C^#8gbm`EKb9M{yf9kyQWpbpP{kLDBbQ4l|jt|41#*7F!+W(*+PY($$t z$WM?!L_V1?%188$;kpDveI)H#ArdOVMU%7*E3t$1Yi5*eweg7?clD^!cKEij2zOk?~mQ<9L#d4=8u2`QmQMi|KJ3ZM9N!B@pfa)i|SomXoyO z`6$atxht(<5{Q1~@}z-e$0-m=END1TH;$xNr+}W3Bf_KfqHj`K z$@q8{BErzvQn^Q0iL4@PgB~Dw%7t=1WL|*pewDXj zTqV)^@y&FT!)z?i`Eh;J29*$R8+B;Q_%>w}as6u4&6Z!(E}$-E9_44KTR5)cyyen* zCBzEkiyuefqww$81l~9yE=q5Cx2F%;xJrm^c77f$B*@j9%0lcrE1SY)T$T`S$GK#y z#PEc8ZsnHjbGNpP;|v$du%cSOiPXVt7yB4pPv80-N4PJJ(mHhP&Vhw*JZ^tIMynN4 zqMxlRv^W{jE&UmYJ9cxvo_&QrRFl%2UC6F!ic)w}4>tiZ-?juTBsK+48jrARVh`D= zlr3B>LegGR$eqKHiMr+Y#p6*Ml)#PIl`?yg(FBnEfn4)=$a8x`OR%0U{eGAT1}5-ik;5kKC^F{jH-9=QcPr&G!UE@5vW;*?|cm zIrYyUd~mV+5PjPEYoKcH9=n5Z&2_8jVf)kBZ699#yN+hP$E5W_cwdwusY`p%lqeGJ z*(n@~jlwwqwVGI%4llzOT-;F-0tYn4D79|Nb!+!xQUJ7zWXuZ7a0aN=C@^SaK~J&p zYRhyq%FtT*{{rmF82ZeJR;z#>yxGKBsYNt1a*fhzlk2Fl-&aw@FMJ4oUbEsqqF#U0 zH2Whf8ck>GH<7fuNQ77+xJO+Nvdqp}FA(s!2ii;lU(Cr88 z?Crnk`%ipbJz=;8cX-{}Dij$;8y`Qcl-ntTXDSWj)c<0Za?l{jjsqJdq38IzETLv@ zyMC<}-u*T8u?G~lO4b?m$2kZg8|2y|rTWgSUnSJ+A$uanr}q>|D=myg<9NXMp%yuM zy;83$%9!qpKK?V|U;VH?^+K}S#G0(NXoMIv{5Hiu9#h|(K8?vW>xIIDRU7x^ zUYA2oOatA5|7R9v|Lyg$$O3h>J7-*#k&T=6NE@{Ev@GA}Yd8GlyEcr}_2|4$@~FAt z<}qYx*lrUG!|HOHP+ZmB8uJqQIek-2Px)F6&%V8lSz##urMQF26th=uqRY`z>-ev4 z=vFoRm6gJ)nkZjt_@ht59fq|wUqM^n>e`l+W}KQXK7ZEs!!?^F*(7@?ChzKzxi`=A z9ET^d{yAx_BLr-u0G`X9slJXxWbxkMb-ka(@V{@Xf&>eKcx9FTyC%Zzg@P|`U z?jB7J&F|H%XHO`5M~OvzX2vC#Jd=$Nnm!Lfrk-!QV7T^kU-?wa`TxB&o*Mv96%X0m zI-(l=9&sIt`e%9HYgMV$uD;fmrGfFJJGM1mCFE;okIr&Dy85cvBrxV3%7@ zNQ{l!XyRR8U{PG{9#Q9*>|K2Q${b_j$oJ!%PRsuK5HLeALP4OG>K%Piui)uuN;t6{ zMF|o*1UZkl>TER0;8L?q+TVm`8{Bbf4m7f;36q#3Po#7+(=w|esO$LQKIV3Y$lG{; z*62mmTV3bV?=+MyM!62j4#RdYMVbY~d50%VosPJ9rY8P^uFt!J(!XET?Adv;W$Ik~ zm1|e_5+8gjbQ!^fd1)66Z{BFZLpo~Wc=5pOwQs9+Hlj8nhdT7zBL=`I`^nqhVUQ4U zmUbG?L0gYA8-ns)a*{-()+S>|>3+1zOuhbLIoPVVLjb~YSf#!?PXlxiy!>Q#pt_x} zy_2~cZl7(i6(5ylKUeQqM37yX^|d~M3zZvAl_TE2G?a{v2j@W;IiO1Qa8pX+!kVao2s zYTu@+j(ZWCp4(M4yz6tHZTj@g<}-%c>-M;5(VEc{tBwMg)jk9|85+kM>eg>_@yI^sTmJO0G?3D<|0ct>RGM zi?^xMfpPDaCvwa+LNL)`4!2CS%)z9gVl*?)Sz_m?uKa5XIC7x-X9(kw_To}@B5 z>8XFMmrC?yk=Uuf40i6;5_Bt}E{(zhUg4D;6;k~;iG9s3l}JZA28KJHZ3%U6@@rva zT6j|jas!tuFC(HS`?>vvhnw_Y%w{r;*YOPHe=T+AQbV`x^Hid+JH*l(^|;D6L}yNmTCS$%F_#D4ux05vxp=~UgO%CzPd2j?5!cZ;!vGb09&2lKm#{RQ6+ zx(o#$^938f?zDBOCKn_mpMBV~usfyt_IsIJTq7mlI*gkA@H5bJ$Nclpfom@#_mgsr z*qN7@eFF^Lwf2U*u{@Jnh87J|jpcIMjS?GHbG->?@YT{RgNlB*gMzFbN66HH+85o{ zbJea57EwTljGZE*2`}R#-0#U@J6b>K{`IvTS$$g`Qf6-a&9%$2TrTzRR~Y8-`*D4X z(7bA@R}eejAb1kuDLI6x9-Fs!_m&apLhY{$9awzv=?p0akt-J#B#=5q9enSl2Cs89 zz=A{exRRw=^8>qnXbJU(<#64O)c-}6pVY11*$|)QUNTg`UV3|IE*|FLdZqsD@V9|) zm|IG;+xm3tZJFIioxXjm*)%{G+q0*1VFg zr5DfZWR3-EI7B{09`4upahNUMxSBMf_4{@kv5u7Ed@IN#vH@(-FMPcj2}USt+u;>* zi9uLRLTOqXGA;^k)q5EqR+epE&+w$V>Yv-3dHPLfSjKFj`Sh-y-IQ?P!%WY?1k0}DZw+v}mk;?C7N@uGtZL)= z#OOGTaon5L9a|Wh_PYr1y;BY39Wqg58e77I2rvC}PKazNg;~g0h@h}XhP-~TL0fm( zLTijxR)3~PY$YTVd3hCfhX5%|wa=@Y?SPJ^fbQJj7xS=_E=}_$e@Ap=5f?Xhn)M5Pmp{w*)!pas%E10 z`kw3!I^oTt$*K_8BBJWg^!0Y4c<1^b1s&+qb`N444^;J#x1b9vFB`pxpgKJWDLj4l zQaRMBYcaj2+BBj4@wg%@sS8?|nE)n!lX+`bg06g&7 ze(#6S1CUBBKLKv@D@ZLLdP{~L53xGpL;2pXsat~P@wK}8AsMmYaeL%e+15q}aPES} zB#E>*hFI0R4W%vsgVHvazJx)5F#18*VHWymKiD}Ke3pgo7MZW%gQ=M9P5?oiT&qR~ zzMQQ0GkW{ayS6oCP2CwSZEkQ|C!#bMCU-h~(Tg+r`t!iOWCF9bWE3DL~zkfkxu^(e^p1?2RWlk1Nh z%A`vdrxG}wJAdk$w+=D@C8#84?f0IT7zo-@M_s97<}e$6xcG$LUh|TKx$cO*2U)kv zDR2@(>e#gY4%TEQ6!lVu-r}_GJEQQe0(CIOyI%+0P0{{@+2C=``Y8a_zwBRKx;``t zX3qc>08mfm)>8n~n9Q$50J{vJqbMfdgHfA+d6#gwKb^cELnUu8=+zz!v3$VxRIG2? z0gGtKk#_K51jLY^E&r^UDs2p5nf8l zHzsbP;GXY6*&nm~&UaeMJB(jQuwo%^P}g|)thHCuouq8In3B=xP105*?dCgstPFTh zCEid!@locMx{mln%IujWu8M%ht;?@^K=_W$A?buCl)}at|11YL`yT82vJ($d4~ObO zGjpWi4(gJ!B5{&5szF-5ep3CfQU2V*p)aMUZDGbiaQrP3`3x3UCB)$BG;b-C?-+4E z;iEYx!nIIHEX}%!IrMfe_5-DEfq0_)MWbkC-88o%f1G5p z9(q6pIbvLp2yhfz8dJ=Tsbr)Sp_hYD6Khy;@Och`S6mVWzznkmY#Ano1s?=Z3v57N zh8+#nxygYqP)kb2Q}|2)D0e-4C&-n={xX=(N!%+jT9ryB1L&c1$TXIwk>jD|p_3{1 zB-ndZwP(L4d#aJ<))4)(E+MR)CRxrp9yl8j@+%dmBLeS;h#u+XuO+r4N^UL)T!R!7 zDD}KC>Gsi-alfY8!OP`Fj_oJgd}7LfKh<2n&`7N1zx?19-w6JyJ^tx2>xGm{5Rm-CD z@b^*mV5Q_QhM5=IG>`wR*=*`w31Kt31^Rl|u{E`RntOsY>&}ZBJ-FCnT-{R+aVQ6` zE5l*lH=XAK+qdHy0DLeN_JEC3qM*-7#NL*pM>xhGJ=?+sfIn&ZKfy% z$jNi?Lo$L2mHdHv^PvlUu?0!J9e!OEVwb{xnB5 zcUaF=fbCRtdOYzQ2Xv*PEl zY5#Q05;?r~ON!Wg&|KI5bf3qX8YR+ucF=2);X0SD?~Cpw?mqBuG(}}xT=?38pDWv> z&E3t>K^eps6w>ws01S|4N&+4t+*aqFr!pKQ!YRpc!E@+f5h}6(?*Y_fq=X?VnEC;w zB&>TXB-B&Epy$0$#SIg47!M&~Cji^d!bF}&ZRd2S_}`ikH>PU$DX}02QC&(??=EVh zCQ|d6L8r4=wUIM1C)(zNe-1bCFL%5Cu}i6NoII zkVUak8A5POf*DuemNgMt$05wi+BO)}oSisrq3q`Vf~nh@YBadnbBFWs@DnR_bf_PU z$0td@uQB`Sfq&l;*ThBH-ERLJm+~yujeO_CXOBSLXvm_|(CVX`v!Zh=ea)9S=dzZ^ ztjg;P)UGi=L$FN?znbdyfu>W2uTaL~*5}$PkUTaR9rx)TtUB2u>|bic?Wcezg}4LG z{o7_?%e+}!xeTT|hd!(g-*_HJ65*15qo?Q4x^sQk*}dyN^!_JlI8umjl3})U$NC_o zhvZ~RE3uE(+bkqB+$BDc=mRoXlc=ug;Ekpu6$m9V*OvTBhS89FuIC7mGMIw|_m+dy z6=7eqKu-!e0YDw-fXma$V>lu1EL30SgF7Dwd2@tA0z{b%F9q-@$y_;g6sYvNp z>Lr(N&dhehm(sOgY9CJuEp7uPp;eVegu77jl7JBsT@{g@*)gw+` zO+k9cEe6KDx=vV_#Qxg!X_EPza!6~$?epZ(?djvbnN~@YJG4VUYyK!Dy)5D5T_fQ8%z4b;O6VR;A`42j0z5O5q>5syO>@HizRUWurvq)1d& zQc}SnRn<+@)HF0zaGDw_S}ICf25MRcrrO$C20B^>dOC(`SVJ8nLqkJjO%)RaXkr~< zYGQ0|sAXYcVQFbg4c=udFMD4*bK+_&Zgq%obxgIrxv_(-xdS7>(No`1(BS0g$Z!eq zw72kfUhVDS%=GdK2n-FhwF=yDI4JQZYyBS9hMHh3F*tx4Dz6>Ip~+3_J)Jisq=cJj zhWmMjhx&#`Y>AABh>Q!1D!963z2BBd)h$swwnnW_+`2V!+rGs8C$_~$CY>7FnOw9h zId#|m^SdKh$q0P1aB$DApwt5KzMNzG6C(HTNJvZFo|d*hEiEsDpPC^A`04ez8AW;f zw&n5D^8_aZsp|y?_6qU@g8asU67k`^JC1Nx6`klW7M7LlMILP$IePWevEroT#X%=> z(oY=DJyDi_;$*{#lQ&M(T`NCv;-uEflMOu;H77);MruwN)IDCRKY6kK)bWPn`3?2; z4d;3qOH&$aD;m$;Y&u@pbb4J=(}hddM=ss`a9Pdrvbgz5=(?*9eq0k5UB8gre4+kE z+pe3<=WpIR;-t~X{GjLzH(z`)?mrlHG$LwBYg43A2Ojt)N%KYH}&@%5FDrOA&+ z2cFylpA7Uo?Y{N&+0U^7>a#)LXJb#FJ)3?uIrePk&+}gV^KbuNOuc*g^5xXav8k8S zZ~s>x-v0PAJ%pQn?Eh|N=H2^WGmmX%o(#-PKY#zE=EKvSAD(^txcF{%_U-J#-`T(a zKApe#Y1-t||LW_fud|;P7Cz5Dn0ps8|G(D@3k&~yE-w6D{QK|Q`(5AUAO3Rf+n>en zAE&?1{`&F#?a~+F&&6}UzCHWBIQv`vz8^h*zWx34XYt?P*?<53{cn-~e}!2F-ea|y zVo_T*M#(I?m&LZ<{pKofzOnnHIYeu z!BGySTh?Vw_CL8O&r`BBNjiV)+PjA$kHfEDDA>kTJOy_TmdYQ3cl~%TsoFh=(x^Cw^7zdHV{PyPTJC zvGwQ7yE>mc`(CyD{xLU+Kkf5))z+Wg1A`GM?}}9UPgAprp3|`7T7e^(HF%R+VY`nB zw7tF0U2&$EM1-B6TDt^aQi9&qx$K;!~ivFJ!`Gscromp7oQ&$s-P% z9zS0*mg#*mTDL4u+4>xqa>P;Ai6Fbo^CycOr!C%-TqZ1hg34G{eYIsM%auirYoAR8 zx=grWAC|AXz0No&^=+cE^+ScxR}8(q?>5G~vU@juCi8n=!x^|yjh|WRD2O*HrL-$r zixaa1)gH}P2i7n%luJBSPd_^4ZuHnhxDkLyDlv?XTvs@__|vh}ilZBPX3ev?B_~q5u8!!^{jPfa}v!!hcW&F(O07+ zKG1GBjsKu|re##YX>qYWgw+hm^ z2j2X|^`ED;&BU^7hLPL9K22=6Xul4r7h>uf`|t-R^r?%v{WBSXIrzeUXc<9r8aVi9 z(0)icFwM3Vdf+$S$uRh`=6jKrt$Wz|GtxWolk3Xko^P6ZU^kxXG3WjK(nVn}@;w}H zar^p%+wa9am(E*_5KH~3yAKzv`xIQFNU)Cg_9oOg+?qThou5(k)=r^5{G*-GYTcv# ztE|e$N8@z-D*OJiFku6>EGpPO_jL14hknt(W4lKd#&?2l1C!g&An)y7{`(c?2kEzF zcl`dmd}qKaE4b`_Tb$PA8Mtdv%n_9RoccWn6nL;LFS@oyr$;H=rqlXJEW1Pd3*0-% zcb#StrdI2NE=0jqaQUUwUD0vR$!pP2$hWZ8P_5Zx4}Tq>_?<3VrQ1uxM#dH1V;ZbznI~@H zR(t4-fEM>?xH3Wfx}z&qeyeuio&0N5OWB=9hg6SjVd+EFm^_E}WD-lxbJw5ErD`gd z`D`&&kw~jHrK%U{D}ZUbk0w;BnEg4UpG(i_y?ExEuH;4YrSNFs!M!N;iGx>6MoqmP zM|%BmBNHbwmy9e8^$zcpxWiO1G(c6%b>YiMi(Kz^uaNrwX9L>L*=RL4DIWil4MQ{e z)??NfBwKWHUg~Aa5Oe*a^EDEw5*$?Rlf%+OpxHF=xMcb8_f(WBkIwX(;W_bM4#i8f zjLg;gr59M$aR`N|)U2ER6*GZW4`ZGLRBS2S6Hf;*GjLd;1Purz7&zpvRw#r=bKBOZO}-PeWCBB%T@v zY0h*6_EzC6`4qh!Z9WQx(z`AMid_Pv^@ea&`$&CvK_9K0p7WUX=-i&_^Q}APKHOGJ zQ~_fW7REe_v(9)abQzqJfgb+*h_Q{IEu0om&Xsy_1-3hdChGC=Wbsj<-=PMfw^Q$M zk)}h8kCg4Mz@Y-B%nqGob^{uWDO|e@z_1iizJ1~t6sK;WdyXBiYLI4_;3F?L`|xk7E}T<>tN;`0gG%a*e~V}S$#QGUwX|G>4+ zG3gZ9aK#o(&E7lesN1(=w`bU8Bg&=jKp3rPg@~u)Nu#g2fk)^Hv+>crxeoW(u!KLe zren*ddVclc@3WLg$1qx>2dV-VDke?U3s&nzwphaXf;%r3Y2N;EV&oY@2M8bTfHuwJ zh!*1tUghqq42O<2j=3v;rZNcSDjmGrh8C|99ah;3F`-fe(jx(^DF=YtN_klAMxhFY zjdX32;O=dl_`$d7iYzPC?Fl zZY-eKzqCzbmf4ABaI>SN*@|Iv@7B?1=sgTQ?DQCOJL{{uc(CnsI)G<*4!r|Y0j>#~@+LYJd{#NMmBv*ANAvK>ReW z@XU++i6d9W$fMCzujm`9>Dh(5ALNWDJ@O(0o?BL=En zB>m0OcGXwZYtvQ$IJ%qezIk)RG|H!3zAur^%#u9GyRF zpam*%5tVrmQvm$`3PO=6McKe~g*H zQ)%tDb%@1vPtt^#bmb2`g{Qo1lr?cwy6QD_XpvgzG-i;VMSLZu_FpL)wO1V+rp_-x zmK&fyqoAdeQ6GEq3$5vM{2^^S$BhbgfukGn)1-*OIO z_PM%dL2rgjywi)`Ci5a3*QB`%+As<$PwZU(vtYr2qPgwZYWEZ86bk!?iDW$mZaM_& z>u!BkUX-t_cZKh2lZzN;?C;Z2>!Thh*KV6B8Qo_RdPN%=<{SAqzK_jGoz&?vH9U_k zbbkg(dUfRc(c*|xH++;@)lbUclmu%F&LmW$!BEV`dqyH}_k{BZ8IpwS=&I=}sb_mzf%4;SDH=i4-! zcG{fpHZw`!ViEw>Y0ZF4B)*ST5po5;>PN#l2y6a5!jH5=i2 z0r|g-rfJ49U!tMGrQy+Y&TWQoS{mkMq;nGdf%EZyHe5nf8NSPa=4FlkArQj3>5vn& z)dq6bEAr`f{-)V<{@3Ms@udS!m*3rPU@#b8GK}UaWSrlX%nuiSm^ETvG{oqW&iHyP zP>SX4KjD}S^rNgCRXCg1}bvAy}i#aE;GF;c_YE7=Cq9etquPH_oRD2AC z+QG7*d?cM_p%NfWhCXpW4YgTn?QV_V5255Js*Bu9i&AWgTo6e^e^nwsg)os4WCmx? zf12bmAb@JF`Bc=jpL)SU0V9J5_5-&w%five)VvN~NK&$d^DL0C#)<8BQR6pU{2;cU z8qyp(vrGZfX&(PRH2hE_zvCi2=WN%~$Tbp0x8Dt+6W7}`?<05z3KWSaDE2l+s4&)* zixk`lgA~ugzz#^@dg}ry7{e57E2R91g&)1DG{i;ua51hju%Bu(Bt(YNuu)RPS=pK! z5-<$H&}Daq=1}k-NG1n+z!w`2VN-=*pg8&B0eQkl$BaGsD;4bzVZtDgCtfLM^{ITl zQ(b~f00O=bDB3%VBcU~`V{?OG`0W)*d*)kjMna`jbib1)Dhdipx@kUaywMrDJcsXQ zDa~^c4&t^&3gSK+YX5bvS_rlRYl7I4>h*-L2y&_zHIhn7W}~OiU%0$10vl;I(HwM0 zR4RNP9Xuzw2!SXU@@W7BLvf1~%z~5<&q3NMVSFgKi-$o0m3&)>jD<+^B3<7J^lI+) z7!DHnhL?zynyDarJ*?r_4qj?*@ONAOM~lqzWgv~`G|G8v5qjm zBFDzW4n`Qr%+OA5)%C^w#~oUvkhv_(BWNH{hzW8+yHjw%TG)6tVM0oZ;h>kzN$&yF zpY`lCwU$8|s*Xx-rl6}SC>;rMfrX_&h}}wLDxDm|f%^;HJSB*9DndtuPOZ8MXkknt zxE>2{Sc6>!Abe6knUm5sc} z?)`PGIzfc2jYdDngG5!F3?{6BijMc~~W1HT#IJZ1h(u z-dmXdVm+8jB_HP^wM@vxGH{DGq?z4+kqef{ju_fv{bhagBAka5>#n!XpVrzLz%^}>JJ#4`cMv>wsq(rm~hMq&9 z0l2Xgwn%{?9}}Wz=%qbqYx&pX>{zWu_HYG*IM}H<)O~-l98?r0Li$rAmVV?oA)0*v z%@{(Kig2DR#3nInoC9J4$?qYQt%&fRMOvIgmWolDgstAx;XxWQOhk}psob9%JFu*3V3aR;4ihAr{d}Ct|LUX2@%#mAot!K#loExd&_XK z9$mzkYmv$+@RgQ=is!vZrL|_ztPG2nq81?}41&Rvfh-AJQ%roZEbj-ikfU>G%p497 zqPw}MMKK^1lk~-)fdP3Wl&B#>O~|lF2+=r)Tgd^oa)?3l<~xVv0>K-Fq+ofBsi&!u!3-=r#Oi4(pVz`(~ss!Xi83i;1&!P}~03cFKJO%-)tjITL zT+S_UgvrYnC?|2ho6o=N zh#N#SWIg(-oUu77y1KkeZAgj$|9g-tlmz9-XxyjBFho8j;01(MT}H)Fp_8&d+Wxo_ zcE_1z7p$Wg)h;6nAP{STwnL(r6fmdyYQ)^rHJ3H&>(L*_2oGjGfdiNNE2ejiOmh&> z;><=rW8o$nK9Xn^KPIj6u&f1*?|2Qy&LmFSN# zG>7efVvmoPV68Irqa#Ivm*vPJl6cSjTOyh_{}P*7Mu@^dK>GT5~ek^D7@D5mdzy(S}J=48#9S-}e4>`Zux z$?kxYvG6|S>K|r@Rh?3w7*-q~>DQ3VPrE80tj@Hq3B2Crr&ddfOi#|>`{i}#FI(@a zD9S8ZW6rEQ_F^XD=QrcR7ilFK;;-gX?K(=~3=;m;hvI0qudwTwR}k4~rcBy4U|&1h zclG__%Uj>P{(E3){_Vqt&2R3x2WADle)r~nlh-eXB33V3@oMkd-0M)22%*(3DCdvV zfVu^j+EAyiPM4VIx>D3>*z{@{g=^fH@6E@-({fJ0?83PfsV)(eEJ*fKAA!*h={lEI zeRP+Wwi`5a1PWj24eH#BHO{&68gFtg0wbntU-y?tbl6K3J*QWxV!SWH7UvFYSor$( zQJwC^zAIX-d90(r1bH?4OypHtW9_6fE*JBT96vb1bP21T6QaXjxuF$6!d)t#)fm)6u zyKdQq5fS&HG}zn_S^9(NX7lbKQaSAcYc9CnzvKs~V1!CK7pUp9GO3Tgunnew|HMOPQ8csg!Qf%UiEEQ|?8v zd@{2)qSxR==AM4!?d&r>{pFLS@R0MZd$v!SV(CoG?`zo=tyu`sOII!kWRnO@a01c_TRe4ELUiWW~be^#3u$jV!geBQ|s|en^ zG9(<5<)MUW53>PMxz(A7O&B&0etjnQOe`GAKC@3Ean+52i(XXH;`JinC3!|{i0|#- zMVP1N84D@kdw;aiizPaAlmliH+HrR&Y!`LCWj=+Kr879=PCm*$v6e87rAra3?}gxb zrh3?L2$exN3bG`|%8O}ms8`bVUfVs_QS4D!UXr6njlmStir>sAWIHdyV?U|WO`?|^ zEw%(7j`A=5VD%K|#2F`DRHvw&FuL;%0`*>1N86+(=k2)F5H-G?dErT%M>;i5*{ZO| zD5FGCET9)Ooy@_%Qy}l5dQd*GyI1vQt=$CG@9!3MJC^f~D=1Pmr8MtxT5x{UOtr?B zm|RP%WNe(kKz9g0nlfT+Gb9MrS3I;MgLibFI>SLuGQKMBtZlj;TRLWn%BUE|?Y>^4 zVSm`$MH+lKLNcM2gUjBhs!z^Xo`tJVFL~Lf4%>+b^;H_F!M4|);ev&9jl_~r+mK|x zhUPfc=Cp*6)}IeH)?PjNvdY^eb-rcGXqNKrxm@=xFT$V%GJ*G=f z%nM5$+Q*C1drS4wOY;1Oe2$z7=}>&Sdb3g1 zY9XD*VORcOJKq93grP6>mNj@%xbI58g0QCX3x_>fGtZgcPwwU^onz)OSkIw*-PyiZ z_+xGxoy+fJSVdg-AT!Z%=X<_}Xisy_Z<@?L^O)3~(sQb3eQEonO_x-t`5BeO;gJFH zgla7QeVKEGUTA4~P2u#Sc1-_sZK1BB-R^^* zb_jFbrZhRN9xXOeDCgH3!g1~f|BZ#q;5xOgKi3y0x;%Yd+VZ4%T4veVkF$RMHcWiY ze3eUv(d*ZNYHoM;ew-JZ2`@OQoC;r`kRF_Le!+_0M9{E$bM)$j;_rlOLqDxn{VaIV z>=v&Z^jwkucf{YUZmQ?dvLff$>Dl<5B311diqx#LvuD_qVcj`KJ93|z$1Cr@*-f60 zK3S&~|LPFw?yfpT=7Dc5t2(xJ$DetbZL9pw{9VMu1Dl%4e!dj4h3z*!B%7r^N>RyX zrMYIk-DZ1We)d3zUGFZ*&pFAp4V=)zj5jL!SGyM{DH~|ne(@aK@Zf9FvDT_*<(utm zOOB*a)!X2|9QwZ0q}0A?9rxS%>!AC<*^Q(7i{K}n9>4FQ*z5L24QEMjw!HrKgL}dG zRTaAb&ONVKW{t)W1w|brSvl2LMr?fJL@$6W6DM{JXdlVA z>0n0aLBy#K37j$$zkFvyH5zSWp0+QSUt7L;gjht!9+=8?FN`tV{3DoL_9bOGwFj}{ zXF56av1L$k{=}hAqc?kJd9ci$ANRf|3_HCg#{L^qn*1q&a(~_VmN}ER>EFVM=s)jl ze;XGs|AFTh(jx!Md=({N4d4EI&2`pUGUMUamf|vNe)s6eT{(OTrM>pUo79jSbr=nRK%zx@{y+ZoX0xp)CgK7E^SK zOxVvT9yCL@wd=5XN&hiJx3u$Ewq)?^JKOD2J?v-c=B>plX)v2;n3}ZR`Hu(fWwka0 z&rpZcmErO$q@H@GUin)3Ak1?Rmf_Ip&hK0jGnj*&{pk!|~g)pg~ z_fFR@-z$p)_e=fUJovG3eyRf44S)ZP=z%WfLDZ@K#!zSwe)qR#UNSVWmxJ8OL0xGb zd=QH6Aq0G*`8P!8cSaAaV8d=G2WZBTdkH-|N-#y5r?2g36^7&ul!9guOhLl);2>Wv z4lXCN10LW&UO5;W2P3j!(~QC9%)l9cKaOm*=AGQ#OsvmO)Q3zBBlG?-<$HW7@^0br z3!zOORJa1|p)$m?lOT7AQA^iR6H^xL@hVfD%@F&+9|BA{-0yK{z;dB4M+#P_3KEW0 z#M8jLf6V09sSingCZ>oGZVp?}`s*rc(GJe!BC$<-$qAxuA!>>OpvPj@pn_+YIn)YQ>{Ti(t_1R zm!o@g);F$;9t)U`9)Qwdk8ATBkg%-L92y(+m4bmH1RVp0OHjV*^l%wp-#>><<3~vO z2DRYEa=vk`|MAknd{Lh7Qqcep*@IiTYbHyXnH?=bE=401*x3o(hj9+5-v#UbrST|I zzgW)77%WV!`7wL`F(jy2RZFIe@-P)-bq-98oxNU!Ogs{n;egsahunVpam2>lq>ZRu zpGFcydEvwjI|PrDa~_6wJ&CwTX1}tDP{IVVSeJ%#LNVYb4-jzYhqYwWr8#S7ikJd9 z{W+(2qMF7H-G<@EC8#iFayJ5I3OkC!c%gxbq=3ZS(3cRTpKMrhZDaS1X)98~G^QR0Fz%(@B<}#!+ zlx0YhlZ=8LR6JtxAGxFktEIv0dvg+V!XiDNo<03I`E6L+r;t_6C`bjfG?kn3I3nq8 zaU&9D&GQdTRC!R3U#!E7+cc57N zrBkqzsgsy(KkKbWnQRuC|c_Y=a# z>hk=@Wu2)&byalBN09P0vdc3! zfrcU-%1x%_Cd*K}=GX%kT88_|ayVl?e{FBM_{XRj+3w zB6K#)=XAK(ZLZ%KsS>{VceumKVx$%8?;UxhPE|+fBtuLXs~RIj=0(kr=A~xpn`=O~2?t z3NnPmHFr5dhR~klRa;#*}=m-CmSz^#^ zhdRHd96u@2as~)wA$4UQ34I+FnHbGYx$CKLWFi+OhaoxeakTBVsxW3+o*l$PHFE27 zfejqwd9Mh&Bi#@1xD>0GhK)Fz0l6 z%4YSt6!M+0VjG)Pck9U!rrQnXklnAIb=Z4!1kNgBU_=_P{RY{B0n=*%H>Dw|jad!7 z8y#hlzi}`fQ)B`&moDO;nwIzgS+;++S){_Wh3vCAPa?c>y&)bTIgJ;6f?>E5ZoACJ zP?Wiy6Il)dxruJ7!*Cxe#t*Q&k+7W-ba!=K8# z6?^$Yb*0gPFTW*WJG#OoZZJy_X3$ejqVaw|;QR{bDVcgK%LgY4R2U=xRxbjp?q8PM zhExPR%R;XFD9WK)_D?%zbyaO$VAyVCq;yZb15%L;WH`*PVJ54r{j8UklS|=lNaLd< zpGcVO4Bjr;KIDtAC_ALsG$mO?!n(I4IhJ+^|Fx?Cycjgk!Nfxi;Mp{-`d68gu|SczgYZA%y#e)_IM=! zx6e7c!wQ6vMQB=AyPe5THZKKAOyU7N(hRWe#P<nna= zJNqhC>j0~Tf92ss^NCkC|N9+CIq>#X^|RHZEo*A-+>Z;G;XCi@`>*CyL>!_sD6^|) z$#U?gIpN2tRX=}0KT6j8NDeCA+K_qV3*9O2r`Q|XM&_OF`8l}iU~kDjvif%=Rxq@A ze+C!a|7CxA&!?$+G^r&^+mWliio1N!WETLzC{=lINZ>t}n$i2?Lf-y}mSW-<7Y_;W ziH9;053RoseBAx-XurKuiW4t!R-rN1@9h*4#uUxw9izQOCF`ah{!*s}oqu&OZFa zT6nA5M-fH=Trrmn6)+T_4FMz0XaMXg8UVmIxn=>ft+;&-7`J{y>Q;m{5rjl}w2`hx zT^CxV88p$+gd>J9#`Mg_#IZd`@20^gx-TU>y}I+`K1;#n-S0+JOxyDWt?Bax;jngu zN7LSqft3zFK-bQ#$mQbZ$h{oTe1wJvN}l)VrDQxtL99HgWq8cgu_enENI;Ml0T+PA zMrNJGq+RmbMsJkV7jHt&=+GFqst_nU?xW*-+?zD?V(SLSI`B$4hxV5;+t;nLds?w$EO%u6gJg(5&qVfj2s{Obs@@_Y9D;g29B@sdL8`j_&I<3kMUOnt$HR(YHw7$ zKUZDR6OYzUdZJ-@PnJb+t!nnsEKnLbmV-f@mgH;?_;gM$kGp-w*H{lFq&tRQX9OV% zZgg!sZoPfJ5lWDVUe#|nSHa0>_Ik=H)fx2-YELMg_tf~hQ^VlK%j!rLX0fzw?owaC zhLZho*8|J7=gt>2TeusT?6Y`~kj{p?*O>j@*njOGvfeB9;E$ARTFG~6Y1W%EHsTZn z7Ox&!zCb?5XC>6mKiv3|#QG720Kx+$v&sqQZZ2p6ez0Iilr zg&|bKX*&kgL>#PZf2~qKnbc|Y4W&#l#SJ~QDSH`qD;Gzx*=;-R={E|1>ShP?fii*ylzmMsR5V+-7r zHq6zOPAaHK3jcf^gSi?9g0K4~krd=P5=XKn^egwXidI|k5TygB&(6_y8DA#Bea1AO(1c9M2yX7! z8)TRelhK%kZW6a6=;?VDLSAWDS{C}cP`p+u4|3%}FO`5fWD z%s5+o@2D3;U(t)C?We!WxP5ycIqT*}jr|12hc`-X;o4*VRy2Jpu$kjgmoERa73NpX z%n^^^B|sXS_ZpCZU&SYAv-VZT=5}CGBY1Vax4eIBlPS%ya;RzUNAwnD+Eo(%%8Fr< zTBf^p8PuctdS1W=vUT5!I*hn;LB0{i=?iJua2FtFPpt%ZSCED2y$K_Qn+b(km6U*$ zy%+a1mtvlA!`LJKhgiaxVGRTq;V!$luigGPz zCliFejDaF(eCwH|!*q-sI8cY8vfLq-kVl&4u5h&n0*R(@73o}u$vp~UORK)}Bh#+c zd&GxBIstVtr)Pyz8z0Z7z^yUfW&-Oxo7s}EkWVBR1Bv&#BHQH0u??Rs#67*YRvoe( zXmGa@Kl~d0AvkNWY45G`uRaByu+lX(K|4rt6Dr@+dQDA{(QP^PwVf)uo<8c7VF!3i zfFjYs$MX$mGkMrRY5xV{eXe-TO>U}JJMzM$8>Mh9wMJ*(QWrGKDN+@Q)C3~5Dtj2` z#E(;(O#@eU&KI0j%|dHSqL=IH6|-yXCGgdzdFIb$7=@RVJyGie^qLRAE(GosLNnIk zL;E4-dNa75LmstXlZTws*WAZNy0)w-+yPFg_H()n^yl~+sYf;Vrkr8gUVHW zhBT7t`5JR5M|@dcwJVcJO3FulvStB?F2$8=Uxc@WsC+Kb&tdB0zup1MDB)@{18^h%bEl(Wawz*{8U55ERqz`n+38x9cS1QLUAW0i?i%wg?T$GDe(JDx|v*;q%-EOEg3_p-(clAbdD45muF+k zX~oqU0BTO)5gIH-#SO-iut3>IiEs30Iu5SJ3J9}3b?bvJqeWX9X?!o3Pl%=wo>EY* z5_Vpgq()sL%~F}2D{u!TxZAXLwR_b34TLI0Xc`Pfh2)}*7z#q1O`VxXcH`nng@Rm^ zo1|u_3Q*dDX}6py;XAivL(vW%M221b1MD6hY{{V;k_Njx_WQ4|Y&^b8iY&Y4qt@N~8kf$#>BMPY=A7KG z@=385e9aL0bE*J%A7}`1j{N2>i+_t~3by3aSAuGt0hRz5d@7UMEH*tx{%RWU@m;?|1UzGE zmMRFd%V{%=4p(e~8U29~QfV4f%layOno(vJA^ahd(5Cqkro(B&u_Q{pvC0Zrw6suZ zulK|v_jDVeCgFl0OpisUG|`CRFiaEhmDuK4AP;Q`JdxsYrVPDpU z*gEq;xM6FXR(!i{FWvMW7b$wAm-_+^Nl|Rc(TxD84PcTa9X|47K?A7(+)x5?7zkwm zESDfGXgaOSa0eeiGeQCbbsVfEKrJ6k4D+RKSsiAO6ey8%rNBR9dRrdo=}zRYH<+gI z{c^R?hTK;hB>>kzlL#QLZ$Z#V@Y-T5<@KI@c$kQ5n%Rb_4Nh>AfYOegn&PV4_)i~ljd7iw zSzX4xhi_-q1CKWpyGXDSm^S!Ef(6$dMXt23%cSc5!x~Mx`UY{B;F^dP8MP&)%QSdnXNZ9yi0BmzDQsoZP>3FONXHM<-EYz<4777#mCk z^0DDACh;ZkWocf~uM?73Oya_i1g+zGct^)1=%m2}0aeGV(O>S)T84Np1D;Q;-71=W zt|2tj0YFt)R`sR&*cQ&LpjCOnC<-10!*OmvFa-uF2pErizajmIS;?W5;4w0C;@ydr zv3WAx$2aV8E&yg3Da;y_0C`VoLW|*DeNrrR{A1{gk7D?YX!fx_KBjpR%?7I6k@bOl zzVBNqjCsD^+40-Rq;k|DP*&vg#dz8PJgA>ti6RPY&tTkKP{$93q*Q;w-k?ZrWn0 z&Oto-B+t@_S?u>R<}*PBHsw!h>wq8Dv&F$>Fln-d!s#Aa(_D!Umfz5s=>t|D7FS{p2U#>H0GJ5paA8Hn8k=% zcmoT+tVOW^kgZ8+o7&Vj2`qpQiC*G33CtB)T3ct)upnKu&L08i4r+lFwDa?{L53NT zKLza?g95zabrA?E^wlQ=2naNw_j1sw1x|Upah!5wP5RU}FnC1ntP6ok5tJQltTf4{ z$Uz>A3e}K0F^%TP(O2B>kpN2f@bEJ5^O<6_w?@Bf_L>&=zy+%@1R<0to6)`fo>!m9 z7P?P?7n=|dCBr>e=sv;?u0GvmCF@A4n~6Vn==zzV+LO7r9FC~yRleswwHD1N_j(_1 zyK=|9t*;Hnpk|#3b55eMcE9)R?e^mIcqSHjUh$}S_3!s(=pQ*DLihr_;|!d6#;4Qc zXYvKFg|C4CWn6yT4bF{{kIL-1)}(A2OohuVy-q3c52YK3NL1)eMvr5Bi`=;lUDxNd<_ z>SyPbd(k@tl*c0beuN$b23N=&SD-97LQBoxTo%enfI{5{?c?QaR=4915N8LT6Hu%c ziyRU`;%Ye@Dq=s@n%HTGt(qKBy35=QPG8vZZ-Q^^&GDtMD{zj$k$bR(vRqkByk>ss z4Tp_hnttbR(9@I`Iq~rif&aMkm$zSeHS>O1*7S!BzTduNT*zs^ApfLw$XrTkYaN~B zH3_XWpiO^A=G`sppZVHw&l^Ac22Xw3c>h!8UmYC-pO$kb#~B$@ z=jC2Nj}@ky%o#@wBXb+{R-uUHjzqq=fLNuSo29j{mb(cGaJm8qU4g%(JD@^~ez5YSgq5O6i(+q+J?Fodi z0TEWBSsEqU5)9`}?2?l8)!7MNWnH+w8k@eDEU@VXVs*(pz8#O?V6Wj-A-uMT0d z5j0D~079TIU|=HZL-35qLxOt97qWkd?DJ4_5fP*dp<=oN{9#&Y2*DV18S~Y<5P_5- zZbtJFyecXM;}z;n>S@~4x$wM17Cx8_VzR&~t4*Ye5LY!gP#)&0H$FT0 zCEN*@?Q7HaVvi6l)4ngQ+x}^b^6tuKAA#kUzYSY;w_^Eo0bB#!;nj_#()^^%(&m$J zk}|O6^^KMTmkI6PUR=7o(r{z>4nfp?wE07g8$mRi;7_xwDzcS3Ifd8`gL4`Hk~oAm z*zKa+@5x35zcC(Ov~9H&8x8if~tb1Faq(Br5?dsKD%rij?3Fe=L% z?LVVQaqx-qaJ)9RYmF%;YWLEqvobHkJ(ocdwgNbRuP4Y3?TI|Fhcy<%BBot|83i$O zLI<-&fRi@BPCH~$Sa}yiP~8P`D>MNBPz4H<8k9za9tUWT`1a<5**#wo-}&;z<6Ol1h$yTA+sU%jyHTf=m1=eoEH0qB&Sr^~}; zwaMr}a>o|gTl0TM7Uo>Z$^hs3H9pV3zADt^`DK_QSQj1mWYx{4!#B`bYqvUlBuH%e zzeJ$b??A&bn9?d#+hC@%Z9?G13;-20?-8Z&`6>tNka?{8Vc&E;b;oDGxzKly-X1ar zIQvqrd)VxdgpDf!1P5WJb|gDbaHhf4uk90+=?KP2;06!7?<_+&hmU?Ji`p1&Bb2Q%5-fyytSV*s`y0VPT^H)gX zQNg%r$UAeC(b990#zq3c)aZZUz*hm*5jLqktQxiNO&l%Vom2Hq0#X%FDSM zlCEw4`sw3$i!6y2jb8B0`m?O4IMhZ0t9`(Neu^$ILdjU4*t*(MgR}E_x???xvKor_bo|ov z)$aFC;}W7~0~e?_tyWk0-yRsAUE^jk%@e3KI@y}e1lMG&Z zj$mDtFKSVQL?J889I)eZrUgy(+#iD8+UVT=LT-MQHKEFM+`6K-0E&>v2))V@%jM0| zLW`tuPkUyL*Z|jB(iFIvJpsGQ0PLn^#&XaRDRneUHynwy!pV`F#PBAQ<^zs6gOmr4 zEH003^!674m~=w1>v7ZED$V5lFRhF|1TlunO= zO1yIP_m;_F$)VXUxN?1ODt6IXXv@RRTRrm7ALqnv{^y#-vmNA|+#!tneG5JA$C`Bo zv3MAsk)v@0z(aOD3U))VR!W2rgx%fJPf63eo1*pI{E=Cp1Az}%A#k-K0n>Lu_LbQH^-pPacQ^`I2G)!MB4sz*Gndi^QD;$ zSLoMweR*^7+41iMC{?Iwl=$D{KN>**LV!D~PW<=rkKb&5ej%>nY!FXutT72C2}6JT z`QMC3F>~^aOD}w(%2whqHd_kg6?h6?v|_&Td=Q61YZ5ZOW9%zK1PZQ zNfepJGKyx$kCFI?Dam%TEN_&mDAMYR)L}(Y!0;IDisL+9OEhPiG2g!zq}!T%@}^2} zG7J3uvIl3CO;wjL>z(%4IlL=bI$Vb~>$>4~Cj^pUW(oTu_Il-;RHi>ZK4mcR^LZt% zzy>_GewldlNu+Miw&CZzuhkv@5>s~UWKBTCr4fO5h7@J-6SkI*BIeIy?%y*LnqoJ5 zX=HR$*8ao86Al5lH2zzdRVr5Fm@y|(N}0vJ!YCGOF`-CTWL2H7vhmjl_b$7~Jlw$| zxRGx~FaMTPataA^N%h>Fz`0?dx;@(p;2@_UN1QP)lq>3$3_hql<+JH38Rbx?moKOv*+HM79|6J_h-+=cd1nQ($ z1(wAh`h3#1Rc0s-@E-Q#i~&@!rK9JMxehw`nM@%>wJV7GLc;FHix0fLQG+}6H%vZE zEeT|C9y0m^?pnqjIX;|45KuuTe2^;EJK+of!2wxZ?_k%6wyd9-tK&10+lQ@347jl0 zRn+zU!;K?(hB)%g`$mlk*jA&#uu;ffCUQFcY?Qx-e>>T!(r>%A&#zVZ>)Gq^r+t^W z`~|>tpu!#m(s;q;qYJnFvw8PB!cBKNYOigA^C^^Ue#*;t3CO9%$6WsWV~Nx%ES6QjMEqZA1ReXuvJPauidn`a>N;?|t%)nqTZ z{kHMYNZM(SU-MTydKI?3iV1+tr-2Pg@I|byNpkV8Uw3SlU_F#289=Ax3$-cQN0n{Gjw^aWg=kUgP$dCEl{xwQN@&yXmX=C)=H9b}XYMiVqTZ=5U4>q@^ZOub-J_v65Ozx}5 z5`S7aATIXAi0*uoXE**>Ih;>qE)ETnden+HhL#BTdT6@2qfUK3tlDvzdLd*Dh|F{J za?TF~jZ&-Ybb%{>CrMVXK60{1o4%~g1O`C=$`kM9w-Q|LKlJAp zx$~W|J(~bDdsI;Dg=yQ}M4KQ7B64b`vT!Ed+q0s!wcSVc&M0QV_zmnnt0=MDnO)CL zhJ-|CS3k|(AKU( z%U%M`GcAUm_Sm-g{Y3{CodE3w3r+UvL28<~AXs2auy?xVgxOC`XZ5sgQ>FPx}ghgnIt89HKVL@m3a1qlC-cwYu2F?SuWz!WGCdL1JHc$4ry6{ zVw|3!2r)OjBc)y)qYgsG?CA64Pz0nzR%3jxswlb04%H(2_dFj}S*&Zz&O`F0?eOBl zSkNg9Og???vE$KE*AC41i$(Py1wbbhqhk<{(afEN?tqmZj750U2-MUHMJQbJK!UlR z)+~i)_5cZI0E|0;Wy*MiQtO`xIut-9z>KtitoBO??SM)D8i==mJ+hZqC8Zt$NbX}6 z6DreM4aJ?$nUGpDWw@7o?ud$!c#b=OET+uv!q^SBs7!pNCKMTWz!(xFbVU)^8p<`tR&~%-KlCNdwYmGBdR=5n)q@mPmSne{+ zV}#cZT1z&jP1jlb)3^^nX0x851FarNjjw@al8l6tTI0ZPu~G^&#_IluW%Rf6K-w1i zH**)Ox-oM%8TXFN5(m;^#~2j=?F&L3oucBjmJgL|NJgLO*DwuQ(^lbgNld$w^9+-? zH(}4tzzSZwp4LxC>oN!X-*kJ62X!sy2pT|FepzShBY-Z(DABP1=CjBU`^RkpoYF0l zImys5y1Spo%_Io;KyQJwCBkaF+I zEZ&@_Jj)t(~lRsL#WA!fyV>2uf-z?cY3jqi87PNOH2MyYZ&sD$|x9}~O0q7S(s}&F? zNb>AZ5BjlJMPIY!amXw)z0CD4d7g?|ituzQ(t8Q-s*w5#hDKzZs}T96j4^Wt`b0{< zKB=Wxk5Mc@x-Uq-2b&pxs91P?fWW(sV0MALpAda)jF1YLMJeg|V}vcp%}MFhq|7A# z9Le>ahe}T|Dr`(CjwN&XO<4RzZoTFeH;7NwOU(1ej1Q5!LPn-(EmO{WWyG0>jB)$5 z=CN9Wk$XXqa|=DGJgIfR%n|^p$7S?hB`cQiyB2(0J4A0#ad^VV%>Z@(n6;NrOS|*A z;Yfvpw#2z;Cr`5M0mK}Di0*%bZKlArj4MB0bsJPbo2^gKz}&ex*dR@i6u|9*@a0hO zPWffY!OMlTWMf4)ODLx(=e?f89{};g-Rv3yafa=t8aIx@m1cIO=)}Few=kM!XPKa1 zG|+la&bNOp<{4Cs>#*e=B?(hU8$Zay$UQ3P zEr|hoR5iq8Z=sQzUYRfQFcuiS^~UcX(YcLf&|Y)>6H&_>PGn;Q7^K`eJzLK^Ipoh9a@T5rI1HkP3-0k3Y>fVc4SmZ z?y9&L5vRPLuGFv_9#Xd;xEg>vPla*nAP%a^1X7|=OL{DM@Cy0u3B<`CqkZ|6Q30kl z@>TOZJ?dJ1-T}D-LaRpr4HDiQkbeH6_d2FEr3lR;;vVC)rtw-vEk717HJ=%zauXLn z-fzOhY0YAV^m(4FOfADz$W4(lC_>8{|IkuFZI$p+it{lwf5X=_<~KTP@&M;99`i56 ze5+q+1>7+&D!+h=rEHz+qV#l?pkg%HGJseOklO&V2B8eH$?m^;RK10>1>czbpr>?x|&iG16y%)8+Q}V0IdX&FM zCoJ#fuR1km5hIMq;M0E-M(lLqeKI{#Dg7ll<(F+S)ib|oN89#yEX#E_EM(nPaRy}8 zUl1~Gj8Y~2e_BqTl={b*@&4dGm2!uL+^1v458=^gLg=2*>g~DOv2EPpuiODCH{oIa zEgAQos`io4fLa1w{)s`W zMW8sa@%|Z5phw-@%~Jpr9YX1s2%7UQ-+6I+@;pzdx53^p&w_xMie>fyCRfHA05EL8 zyfzb?djqfou%(Nv110F|H=uDoTit;3lwq#}X7Mu1X+IPAt9iSO)hh|>R#}ezLpGn? zsFSg;-n#tt-CAoUC^iOf9ci%+p!FqD`p&$v=6vJu%Ige#tF(cMwr#ERT&x2(m|j|M z8sOQg$H!w)^|#^xrXK6xxZ4VGCh74HLc}r3xfee)*l2)z4Re1PMdty;fv%=!%V^_D z%DQFb2mVR@;a3E2TsCZQ?b{TW=S5%(+pd2k9Ebnf^M+7UAw4L`Ne^plhl$;4Xk2C4 z4qv+snkhA;GXNz>Xe6MKI{?#3K6?NrcNhiJF|%$+`)^~JZqIhfS57I&NxdZbShHij zw=>lkia1&^b0)ow(Ou(Bt$-F(^f@&)INdlfP!o9G!zs#YMa#vEbH|RzxPhTi7xVbf z(3-(tSz*hcN6dX^XgPnWvaU|1G3@m)ylP23s_H*Aa5u6&>BlZJAwqi# z^%8cY9W-YUq=&TzB~YU3=4M~{fLe~=Tal)WWzm!|MTQmi4x8Mn33wq#+f5y3HSl)6 zZ5L>?tj3DBvHblx?h$;#maz)VQAo7@cXBdQqZHw9-Rq}SC_(4D;_!w44WH((I{Q*I zHa29ta+rqN=X6_DD!gv-UB!vAva-H-cZ|~@y}0zv?{?1CB2R&@`*X_Tmdj(R@{Eq( z`P`X=sMB|3bxE%-6yNt~h&p{$TDRxRPUjkCpv+VgH2B?)j2fc{ErVR&;3axYxwAMb zEXep3T%de<`{?EL(Ob29D1eMIuDOfmlB*%odl8BUp!UeiU%msHU5-XmB!;Kf7PT+%;5ch`1zWZ9EgMwU8_ciIb7^N}3PR(KcCKh&N7S|oUtW|xH>m!) zEBnjg+sgpYmagpn=lJ;V=S-^;#T)KmCiS%Adnm>=Mkl~^za9_kH-%3P{v5~8e0qva z(bum8D!5z!U1LRQ1E}K=Z33qKJVwbirD`DEIiMePl7tOj# zW!U*dby>T3j9j;z{8S3Yv@}oAX+vzUEexXkKe=V$i!O*?)ajU zgk(N-O1=>B?`UU+{;I48=h@J|&n~(qD4l827<0^jvxs<~kDy#;-kzDgDXWNxnoc0d zq#_^~@B#^yFBy?0cE8lTPy4odwyA6Ud1X4gq=FNWVo+&wu+dd6akOyZBHyS-HZ;#5 zNCF7u7%VAB3?o2XJ{!@^D)d%M^2cNP>isqz56Pp_@6LTA_8c!Re}2SzN6vBoTh(*7 z|J=bi?w9_g0ZZ<4t!_=d8v8oucE*F&l_!_}YlS;=Btoj)0?y@DJQO|~%zZL1*qSYP z_J{1rcc!IId`8(KCU@Xx%rK%=c%7QrCuq5WH@QztOcU786&r}uB_0D2m_SDs%$)8 zr0dZNV+h&Z=r=s^^uf zO{2#yU+s92abe1~B75iiQ@bO!zdALly!P>klWT!jRFsRx&h&uyUu!lE3EwI=#qM|t zl~(%V!%T1cJyXxW>ph*f^|lXv*H#l>t2OtJ?Rez$;iWhI0e+9~=(H7W=X6+zOK{h^ zP1OA7{NV*n_KlxSwvQB=?m07>dw-vZ)-jqH^7ImM!%Spp;>f+|bQA-?M*fJy+5Sct zL*YX0WtiZ6AqxhlIaA`Yl_JcV#3jTSv%M0i469gi!=!|d_^+7Q_bZ}$VD>TL%%P>U zl~WqwHZz@Zks{?%qu3>j5$-S+<#Z(F@GbNU$y6`pey!8Wj`v4g_J+<09qamVJM1xc z$tK$i(=jCQfJEi94w?P6fR*a6O#|xD?^h@?N=$a!SAAyvptuloC>R7G~KTe zVmHP`9!6~%x?qp;@JD*=q-WGZ5O{NU=BKB9NJX--(Yujo-dMh`0<*|j#)*XdHJ=oD zT@^g<1ImzG-y{1pJ*l1nF+rro4h@A<5GSZKnOUGU^_q;C^5@suF~pfL6-~j^)aR8N0ypKZr#5r*YTHuFQIlnG=$`?S=Lo` zCGJk1<-UyQS$avK4QYt9KINq4fBYpg1@i188*}Te2>~}>j4XD3Rpo4=X-=3^_ilGI z#+GqH9PR}iKC?k_a_Ri~SzK~1BXbAZ_Hb7(d}oN3NkFsLt)TlN*=2JvB8&Ub!P8cs zPZ!jpxkPD%@J9&!LV7{sckMF&rddAgg+u%dqcj)c_`6q?lvbtaAv-~c+29?ldte#X zUjmwDX)wdyGD@BX)!b}PdVB>HTqr!~FW@t8!)Q~7K8m=|p4kl)CXB0tPshYWMh%Q4 zwgQcfx_W}U0K`!=rul3o;BE_`qr&%E#C!R&OT6G7=e>VQ2Oi^Tz1&1VR+hO@SeS(Wo~I5v#6`k^MRe?I!+;bCFcOsWR~Rt zk&eTEoE{R@^~XL;>G}cV%3hbKVq0m;;uED>`*m4IWV$h8vT-YL*JNdGL=R_3P!iNC zGy2s+vQt8rhp9n&ZuN8Pf$F|B9ZI; z#mAOngN*F8`DVa=6N0e1-ugvz0>2&JYxh{n9!e|r)eCJ%t)DX9>V|3Cx||#0#8%H> z>!-P!sWaKE`rz#KrJT2t%LTdJD>fvPgHb}d+f_YCH+JO;7;K2YkGV1jB&^V&E#Woc zY41|qTBpz#6XhIf;(oy}vG^AOn(>R*y?r_cR-pR;MPEjlbu695hV49pOkh+;3X4;# z1f|uzj6eh^4QDxMzuZ68`^{Q%)nQ|=fZ(5GO3Th2;xANC93N<~T!+uZRmu?XFFU4K zEVs+@k;ZBhu;(^~vg`H2b`?QqCk>H9PgX z85pH8p#>IOx_az4oG;|9sN4Mn!g6)Zl(-lg9r)1{-Eif$@AxA>TpZ{k+Kff z_6CGXOJK%xQ*~FLW5fZzN)|fnVH)14MOoqdMo(FyD1JSUU+!p7(tFe9`)?U54AML| zZ|(|n&U;t6*yTC1REeI=l;Wc#1?cAKDEIC*ansv}rQ(f%kta6Dn zxlGBIgccjlC;5TOkjS*som{p2OScl7<9ln@>DdKU_>Gz{|86simLkv@NWZ1wAcF}G zZume-Q>g8MvDml7W4s09YQNWy?Cic^ZU1x80LeQPrnMBp%n^1smcsxn&l&8h2)*`; zWTg)Uwq`eUnAZ{PitM7*3#%Y(FibkR$0c>3zj6MY{W>Cloik6@jInpp67!)t)^r4R zcAh6OUw~q-nj<}UDsaC@oNVr+E{F_0Kb+@1t1&Tnu~VuZ8_t2r)}-Y%jy_m{0b9lC`&%2xD*~ayML>P;j2YibAbcrXIbck z)O^yvfW{souVHo5su?A|OyS-H%q+8?#Bdkh!Ze*Ybl*Vp1*`W;LR_Ydr^yrf%^O08 z86;LZ1jlUw==TLirXwW-!X+q(OElOF@eHw9kBNebS|C!HgV7o){%hl)&5amQ2Wr@*1qsi!maEGU;D=m7@q-C4=OQZ8sC?Gc*ExG!(gV;!1P@T{1yRry&7W#k)`Tzz5-IG5-nB|`V?5) zd2AhShcOALB! zu41V%kp@L~mVT#T(&5>g?`2Q_AofNPKtoh76x zW%#rjU9YF@6Qg#D@$(U4pyCK89s9d7Iycnj1weYa1-Cwnu@2aE2*xbY;eLmx>c45t zn)UumNeK|?of5kTS=R|uqhNZU25TfNM(N1YI`s4uIVy|XDIxr+$0q6Ub9LlCjeL(7 zyG7}o@F@ECTt+P;??nzu0HA*}vg`n~{diKy`BFPil;f`pX8uO)|H7i0V2v2isL%Lr z1Q)d*`?3%dGljpHRZi&%Ud_Os>#~ywW^hT_cC0z(FM8+Np8tjJ!$N65I1A;Fh2I2# zOh(1)GG=X7#aZ~b8V2E(lB&)kUIr-pyDO%rhzyW^O0wzzNaD_?Khh9n65MitIT}Iq z*AlNTAteha`vmw2r@RD){$Tut+7& z?Wpjb8~Ak9${%8`A; zSq@JWi~8{gwg?XBjLvB>HdIXdI)WdW#C?pQ*Q@d6FnSkKH4W!&fe6idY|(p0g&Ox; zxbQ!MNFA7#Fnh)un?sG(kyn-zqhabYQhlV}II^eqAvC!j)8bX{2a-0c@z->?1U1f> za+j-PycLsmHH0M)VV#0B3>eQXel0{XXsRqgs#36lSrr>9A-!b~0`<5u1>tu+CP8m> zzn>h2vQ@SwFC^qZB_V1XW37@f#UL&gV;8{Gb7iz1M)U7_TogpGVAG{aLZ=wpETP2e z3BPNwrx0R-pek6`7%Qk+HATD|S=FZ_U(>9gE1@LS?0Ru9iT%063Wf5}V2lNnFkSqdlcPvAa6PgZ%6W@ z?PHu$RM7X4{9D)?N<#4- zI8tmDV+^3rQ5>skFrM?8Dj{O$96F^S#4;3{*WsIv5Vpf~orG|G_qqjoT)BXh0OzfN z2vd5D2BHnCu?ERW**DBTFkLNa-lHT8D4X9Q)H)>egQSQGlGy3^P{pZfjS=WaE=Tr< z-rBbSCi*qipAt#))KO(%vK}izk1rGZ#O$PQa+7H?7ZxkJFcGp4h3t#043fL z=zMjnbGD3dRp6LDpQKe1O9d&keWX)LqJnJV$sjoVNnEHvmjd+LfbA+VZsZTzdnM6d zMO*>X+5VJ6N@HtH&llqg{$lUmC#-76q)PB>AX>B%XDcOLbV>W5t5c0pQyJGD;f!B6 zbdeHW|Ed>z8aMD4u3nFs{Ep2JEGXjiZWo|+09`Z%-vF@Fk^{+`9sbS`y;D+_3#ey4 z5`EPNIu7qWBf&Q531U6IS&jGByHq#|XQzB@ z!E58Rz?gRkMa$sX#u3XP`m}^x2onz?j8d4;8buc@rigqfl{KVae94zJd$j=hz;lu6 zyvWvsd*EAIJw%{sN!zD}`V?E2ePY--)2A4;X64!*fZVAdtk>h3rj}?Hz~yTEIvu%A zlXrr_omQsit|W+eF&K_ZLWqAKuV#GUh!ufVBLVIg$7ws>dew(BC^>Drl1cZ8(`yd(EJqV!wsm@NqAn z4Y^#mb$P0woZ4na5bxY0D5`W>2AV6ln2$I`;Zz`=Ql1aLf{<^iI9AIRVqM+k3TW7 zMKFbWynv8nYz^v)?1q7-{Z#@qLNn&v0r((jT!-E@v{O3zNr)kEX1Mj z1|z^!aaK^0>W?J;ApRZ7R%sg|`x)|Xuqp`SF@-Wp!bSNMk?6qNq~7wAf$8)>DJBvOUn#&I~CVOoAAf^mGl%o;mzz)v9+mbe5s z6h@k??%8eqylhUuCRJD&?Zu5vj_004 z)|D5Mdk+0GdNK1}U1W&M?Z14sKGf{t9;I@wZFs7Y1zWp~E|)(w>^$)4w!828hlZlY zlWv*5nFe*_ALf>C0`i|W9r;*%`>)Z=Dxcr#@fOQFGYnNk@6pQXOUVQAtCnwHu(T_^ zq-TGy-%{)JjRUP=1>4__2Ha~IB4J!Q+Tg6smH9=yowtug)_7&V8lL^_Q?tig-6kCj z0IeKMuT6I86hn16d`Qo zT?l#WlqKCRnDH@8KA=~G$O7#aLZ~eEc%fZk78J?|nV{Ldt;@M-U*UVN|3-a8e);S} znJMKB7Wjcv%A%UK{<`8`Rq^q%UEhe#a;WCyteW_8$Hv*SQO?m&{jvy^^PGF7sv@OT znO&6?>Ae&8+`XZjHqR)Lo^5U5TdVXtLbC71%iA*AGn&8`M>lrgim|?S2bG~5nyfo{ zVL&5vv>H=i3b+2N?a1W$wEfwCh(~y{@UEFnCb6!6IAPbeW?WAnY|+KPo~+$eC0x7k zPtn{8@yz<}p>Wq8C#;+G&G&ZH*74TJ`BpOL|C;9Xl>SADO~=+er-fq%Vy~pv=a1Xc z46{QZ3QH2_VrraBt+ael%mcdsr>~(`<_WM>jk8iMWd7wVeii6}Y z4z28Hz#ob*A~aaCWtWy7fs=~El*=(D+m6qTdK5Lw?-Hxbwv^2*nGfCQ%{jF{YqI=S zMaR=uuS?qUoo}CM-<7lc;(AWQl)b4* zsRK)N;TPWhF{Rt}e8-thO|tn>tE+mNj^w(UHUBEpEz9`F?+<0H>5KD=k3*D!<|3Xe z>@P!;3`6oVs?~_X#nce_5JP`Zd5&OLSH1Nq`TM4s@*UcKi%P$7MaUl9jJn~snVHXp z_AO(?e3&3*m7{|-5CfV*coQfXqycq+4h=GZXT<6$Imb?j!Y1zPy=q$=%_xA9v*exz zX^~ZRUD!!tA-lV%a*n^22E8<9t&0gVsE{|1*kfam7RyG2jA>nuIcbKE3$$yc`E(VA z*h;Zx8*XGK4UNX8wEI*_>}G|6_wI|{-*EH&$6@vA$jDxE=6*qXRPxohX?^>VjiIZp zas?sWL4A=3LA#Aa6tMxH!&_F!X*bBj^`S*Bz4oopcGOMpCfS}BDbEGP#A9AeM`~vp z)Aib|iqv-5$-v^GMOV(-NgsPw%&C@{RH}$}=Rr!(WBVnQEfs!?JS(DcEdiBY6#Fwk zQ6LK)(6GPQ{XtfcQT+ATX}BB5 zfQ*(pEc$tdjQGI5AnPZxll=N_k-@Xr1(8xry%IV7kaz|(YZm?#r2*d`o|~aI2vI{8 z!L+^;TQ8kNJ&)6XzTUi26FaQ+43*^pv@7y3a}_thvs%7C4Mc+f;m4#k)c@_zw?O<<7Iq0 zHmaU;t~tCJ>0K9he%ay&z2RrCpUR$SUcvt)K|AT{nSFx!-s_s;f6uMwHuo1)5<>P~ zV>jiMk&KeW>cf|S63?>jZzdn_?^+la6@P;DXR3yIdqB}>{Sv^kKB)_XDjMuI_52lS zFWTFpLR%$CutDoW=&MV6@`ls}Ov4zCOPJ|q4}UqD6r~z={v_!@slvxbTMoVy+nOHJ zW2U=qmBEw=EDP3ach--YDG!Pr4^4u{d@zK?l~HH=w%g%o%LB{kq;GGiiLYcnsLwH;396g zw8(x$~2yPKs`^6NdII>NKhsFF|as9g1{f zL~e#TPmUWNqa5tIVO6UyS-B&*D3u*Qv86$V0hO8Y`D#w4KA6(Amqhgrt-L z#%-q}Zp=`8zCqtLJ#r)R>qUo*-R!6;qGbp=aC#F!W_XCeJR z&-ULiaYgo1r%6Aa9_GEev(e}M6*0j_1d%UWn71Ml3)gBb$w||L6@dUj3Hpz?Q@k6# z&-z#XdW_L)ox=dZMMzR6|oJVi%$M#PH%`Y6=t}icj@l5MYwnM__z_&8gX&-3<0%9P~fJ- zr|Er;n-c1FI}BJ=-VLBE`D!*3%?-~DY?^z?kX=a5L<^P0j9m0$L%}>qm8VDf8L*CG zOg2y)$1a+kCSN~7j;jXa1;xvo5C1z?PeJg*Yz8 z#2~nQLy`G1%i=Wro-3H9$ljy9w`$wlhSMuw~aN#4MmHLWLv(fqIWehqK zHDjC*6f+BbO<9s2fpJWfpXx45(#g0wus)^8Dyz^kLIc4iaf$NwpaQ!~^P-d`nSAVq znnG6r2sz13yWC~{s3{oOyL2C^t^}8bY|NIOQJg5XA|RN;pJjzWdErmF-_RA0!uX=1 z_M)QUb8c^J-0iNFwikj&7`u|?TTNs*J>cDdN3$LMrxmz7aLsoH-9+`bb74rJ%dv?& zZ>MAB0@XjtSv~y~^@X@+Kykv1vUl39HX3P6=r8v_y1U^DxepF(I7;5AZV*$#IukGJ zzHr9B?psbOTmHCcr2)I_R3*P2FM)7d5v;PIWFaFwMqGHNJ32>+k2j$G4Cuvz;zeD$ z=r34F1Xh-XnXf5ar7T_)F=Te*`YWBo23w4szswbon*#EUsuH_I84f!9szJFu2Ec0o zN*U?1N+Byf=lJ7X$q$?V7Ioey7Zo*>tV23oZ!eN>bMjVq5-)YSDCI83_Qm=HDG%Wn z$~Nthi+ylw zriL#lOH!+?7ymaLyQFv(p5*QOOd`g?Zsht)XRbUHNRIt<@q?#xWx}B+53g zJ4sz?B1i8f`7g@P0xcsJSoq7{&6eZF!Ed^&+t6xY^)=bo6OruTdlw3I2l_o9`#G1& zHy=5?bJK}WLJM9RQW=V}^Y1ba3LStVSt*;5<7M%+Mlf{0Xiegqj4;CprAtwqJ7ubg zDDLG`6R{O1ua^G2F#I2d3U~*e@7g^esfcPBUUO^s{FkVJb>?~{B|ohsFu7#O`G&r- z;s<4kCz*uhGy5L(rE~sA(S7(Oy?=iI|FEYB2#SgXPMqP$Z86-M;VQF*tIVvdtjyaj zwrCDCD>ExlE1TEK%;p6gshOc!Sy>m%in`Xd^4@EGyZPhy2f%{|pZPxTbDpm>U$0^n zHp$Pc$WT9hFmc)>%|7JB?HVzUbe96+F{sWepEWX#uOJ{HB|_)cvaRKAvnn@RCfEbN zLadZ9XHT-i8xREK&#XI7{iO@X&oJjeG}epTc{U63LAE7FmS7{{&pP4E@vhi>{-=FY$qj4yZ1 zzzuB&G|7i24sqTM;LfbEM>8d|5iq|W?A1Rad_Xzy4KJQI;K8c0UkZir*hgR4mJt51 zwJOt@j`VeAg|sc)|LiuW-=^FV5|Tf{AVYYHr*0LPM^nI1u$zx z#+%72yXFprdfW<)x86v9d8P@oQAOG+L~M+ehpQ`Zc4fcquN;`nT7DYAFdvO$5I$YX z5O!L0NmzVV;7=uh1pZ$0$E#G}+pogUYF23ju*e5#V|a}w1noyL0|X}>kxvuf3ekL_ z{E`sG0zeueN)Z_91NObZv}J*)O!3dIsbC63q;P`~9lzLGngcIZ=9l@=^S14Je`Il1 zFB>Nsmn}4jyakO(u(FByon=J;VvS;1LZf+*W=--&V{NxQz8#XIkS7VC+vxJ9H}%br znxdZ9Z)8ioGr$}^eC9xzc>|SGE-yC7-0~;OUv7*iRBOhq4$3Z+?c7M$gBu&AaSagr zoq(T0`Zn?phPiK*^~Z5tFhAqNjsf0TH|jEcSS zu+UiXT9-AO^AQ^h$|QQY)su>?dU%g-z_(SnX$;g&CBJeN9CZxY2P!+U82eV>uniA! z8oaSJ7Ev_Ms>>gQ?+GqT%9rm_$ak@$!rxxec;k9L@TPV%htqrSeKcdAt@crI|pdlAwhuNdgP*DTr>IkFT8K zU(&Ql?Z(z#1Bv|_Bz*(ofZzVh+!V!Iq=PHY~oWCx_bRnfKDul&FaB4d^f^{RCDG4>f%p}GlJS5rB zUf4bt!8fmcq|3*_iTdcOmNGMAmtYb_2*oKHDNVmy)FoMK0&SUx%A4b=dFMATvXL8` zBq4gRO%brDu-&qJxw$3o@1?2cg~p;n0Op2-GywPSen*#BTJ;M9dW2{S2**At`6)tJ zg9vk_Z>MvI4}-qn5ic9xR2nQsYsLG)v85C!8_x?+)y%QQnSg8wWx-RBjC}TY|s8RV8%~uCnU_^O0~rzQ}`p%O5XwgUbt8a&cWyhTEBU@#=HO+>>>x^n7}0&7G{4BNoT5tm16>HSA0)-qm4@<-l3dJNUx(Lc>4b{~kEd zR`jCv*OoGiRY8RYUFq>six<~6W!s+oA5TgPDdi7xQ~!p&_fA_+T$*ZK^(x$_A~9qj zH4f!kaJ_zHM@sh?(;+p#^84S7{guh7>x1Y0PQ8eCDdrxlO1iN3+h|zrPo?`8FuV*t zOEOO>;Q|WCwHg1i_xeVfSa_`(1E99o zE3ZToWA}=ZTtbC$2#5MaE4#q?8GOt#_jvyH82l`qQ{xvp{`r6pN0kzV_jP7qBO;hB zvDiU6aRYHS@li(L=!`qrrKwnlI=LM?3yxslZeIYmq_EjsfFdxb;xS(w=eU6{ze25VkNDULB1O^E{M8CMW8Z2@vxllM|Gp=mRA9g-}CB>67!^tXuUaekB-lsW$e6Po+x!cfS&smdJ- z`yz9ShsBHDj*rD9H^=?9mk9PEuAm!rC`y1v3$sKP;XxK=GHkaYeP1>qQIMQ{s|hWY zMy^n8OWW(WhR_=o-j5h$4e^->TX{)#f8wXXP5qL)Uq6+5bQT92e+S*~@v)0yTP|R@ zAvM_tVxCPV`Iqf5@8K+Vz0u0&SN!p7Z|El`DLkrff9t?o|5HN+%ez`S0tTFS;3DK@ zkm2O#0S|nu)McaXzEy>*TjHP2^#Mkk!*}+y>>ux!A8)O-fCg64Qoh17Olpgw-0ltY zx8y%BsjKEgxAE;h=2r&AmR#4WoXm&>?#E3V)H^_on#CVRt)3BE1pok-!+z)I2IMUQ zAaC8ldrhDPe)vRY4TN{yl%-gbmcMSn+hk$Sle^s<>O7niaxabE^R3zZhFM!UI_I%5 zF52-8)xhct*azy`75myeFi(vDf4$mE;@J;mBpRD9Q}S4HmPDIsaV@#>j{5bM*;qL2 z&w7iZxn&Z$?aBwLt1BJ5 zO9i3g-CQ;X2#p+&-IlHIV#~&)SFU4-R`!1Yi2Rav81_7RIt=G13i zXBKL%%z$V3K21lzdLp)+OM6#eS0E@MeiV|Y|41Kruqx@4~2)XA`82gmR|3EOn^Cb zn6$O2+x}c)^wN9Hm3`Pg`%fl$z%9byZ;pMb+Pq74W!7rq4Tof+Y%0Z*KEm?ioXw|5WTm z9P9wQ*Z;^U!R9pZYDPt*m!GU-K2bh7dmdf=NmjV`4s8CD@GN(Nw)ssow_yA7<{vLS z=DGIj5jp~9wJt1b1vCAQ{Mq)ykHh1vSnAaRi)R{VhV8}SSoLQ}EcSkt6#v2*9q6;tn_^>IS zb?A%5vxl!7em>d$vGxkN@WL-~pIG-&w$#t(l`3**tDgPv5-_S)&VJ~6N|!AyAFihf znzvX!wAYt%EKlq?mp90QMNIHvzt^raWM#b(3@-1!#>pbBE#fY`qP@DdL5JR;S^2X6 z3+99jGXEcV+<+~d9$VB7ckP$EGrQ+sK69C6zm!7sx1%BZ{`fWBYB?z>V=6Aa4H+?4 zQEx`ySz5D_#vIe_?KzBMX{Y$dcafH3?W{opb|_gJnL+pqn@0U;xD+*#e7h_mlIyMH zc>`eE60rYJPr3Fu&t+A9+zb1G9(o6clB*^6JV~B;37L&~lV8uC9uIHg`73q!^0Doa zwqXG${D-*TOM0o!Ag{f)sfzCTZKqz;752V6&S7*&SWph*!b2zDfj1+(zmC%)Ou9P{ zAi*ZEZ}33)YM$dL?_eCyQ3tt;it_M+`D)BIc^oUYdCnyEX-EvO%&z|y%XE$U;u7`t zn%K9u+iSMBwN}T-Txaj4UDza|tGaFKb>89HQ)3#UvzX~6vbmu3$>3NhHMx7mxG^Dl zu5z$_WrlL8aObkXd@zX*@nC}@I>c6c**}hR99D~iGeN9zj>TTK!z|c=sYN(*gGpjD zNZ~L64pBk^^R>(Xl@*D*uwRZ_EhY=&9GMcz7~3L^>v^F9N0I#E0*Bvu~cnKIo6XC)PvMSU!M+I8(t4X^MxY*()jAzv1-!-|kTh z>V@uIBCB>CK>_lZ75Y|+of1L5p&*Y5utSC@xM4xSUM(r0F%VD!WJ%5iiyzJqmvNyZ zTV%(UP!c%{6ScTB^BkX+)}SMeiTr}a-;QgEO??X$I&1T?!6bCV9(vJ=3ZK*Nuhua$ zq8E&_Um|tb2JV6}jqSA1U1(agdc_J;TvW8oRY;F8xmJg3Ai*Ql$VBg45TY1O6ao>2 zK(M+M>!E=Z0TBj`KhuUxn5R2XN}o-ep0}4wwiT0e`(P5%W+qK4d>dTZm+fqzDeiO zuM8M!-tGY-)B~=SIzq!aD?P_Spfe8-@6XoRGP`~D%0o16p?k#^_3XAt$fD_*oJ4%! zu7`_+I%1Q^%3QZB)Z!*K*r$o+t=J-~i4C+gQ6CrN+NQ;h3;9gW?|px~m`iLd>9&~$ z?abtm6Ge{sZN5!fY$Aw|CUP4)kCTM?Wn-8dFsIC{!dJX9ahX}6SKy}pT~8V21e{f z?kPC1U6r=M5e83N_LkZUWkv3ppi_D{_H{-CS~hvZKBCPSTqN--lAO2#`Mk5&wuk30 z1z8EX4n3L$KSb8%o0LlEf{NExGjtYa?qShX$5;x9>uJYP#v3H80TFJd?JAEYhX_$zWeX*wA3NqhD+;N z-r1g1Pp`2Pfw_NvB`&z<-0kJ;8PR$sr3f3QcoU=q7bZGh)4_eE^xLj7caw6aSzrlWLEhFbBdT;R3(>|2!cL>p!oUr zCP0zI{Pt1knAnY6X5pgeJxl+y))@+-Yn_q2ut>0VJ%^r;K_;@VD8{PMwevt4a2@pi z>2qn$x1@|CzXdU3}Y~ zRCmd7)4xAGA~akNsb@sT`U`<4|GFmT>9|!>YF?+jGS8)ej5^oJ?Eq6 zZ>SS9rN?}xt^$v`7dD7-m14RV$IL$xN_Y#DY*ZT%F$YZl{F(s=UnzPLP)+)wX$%^f~P1id;>m@5@z@u_nsF<1oDm2-I1Z(GQTY0Ws9`0QQzWo!PGnf8K5jpU?t*Kfkk~yVJ zi*4WtCOBU`qF8CEj?3RJ{M~}Jn#aO+zwMQ<_MQ&??cwqc*FY#F#Cd|jxhdNOD0`-6+el=)JlkL?^?*kMtI zX-XqQ4z)jKJc}D;W}*qna|Sq=CgDEn$P>HeZ<%b!&}FPC znS@+%?2b#rN6($JjzTDFux=ZP~JvC{5qZmq^=FflOQP;|bx#efdV`XM_aB=(7i9K+ZN`N3iB;i+<_(e+lk3(_21R{&Z2XR2 zSA}kBhjJ10@vlv_s zTcNG_Z#87zH}>&Gpf8A?sb3@?6_P*yt9wQ7Wz9@%fZq;?UD_o|{~2u6A5`_fD%isG zA!_lzlT-1#r5u0N?*Wlut1b-RDB9&A&(KfL4dhP4zfUSXR%Tsz}l~oiH60bLC1}$O})5iN-kabm~ zjmbB=Tb+c}OGKmoL1Rt9+h(h2$5(tJM{K#WecQpo%}rjb-W#+S^uvkh7EVwf#(sI> z%FEOTy?@3pJTboKs|J3^E#|=CNaNIT^1p#w*8NU>_>=5Kx%9_%nbDedJL7Zqy&BQl z?k5(R^FgJr^m`snR9$_e8VoG>lUbD!I50lE=D@DdXCLyPnY4X}y;qrgF8+(Z6|J+}-5vuhJVN>|URPc!@2IhDu(wuDBb zOduJhdPR3#LE#9KS%JOWW0lT6`*xv6vq253X3bd&5q9i$k(Iz8gfw2#h-e9oM_~k? zQt|Y~mdQQuj&Fam@ypQ-N{;+k+xD}5gKm#kmL1==K6W^v_UQ*(#ejQLX%SsObk`ta9KC>=!*fyHnj%E0^$Ru{s zmf8}HQMMq-AXo#MN(Ur0OEoGm{gR3cr+b}sp+pF09=^P%Lv64dX>`A5yK}y2Th!qV zx#fO?ZAHeuMvs%p_sAQX_Q-6v9y$s?>w7=4W2ma5pmJ&8YvRG73(U>MOYeX^S*;u=`q&T_df@Mvop09kF2eG)JG9Ct79I&tqZw!Xt~(vGBfsJ z*M*`@qCY!q_(#H${PG1Z!%uUoZfy+vWMaUqw_@h&X?d}0E-hMf1>{P8QD?hsCiT&c zT~06Tg6xHG)BDb&>rD&^4eh};OaL`Y7HhyS(+!|K?-!MyVA5}AV?_%|>Q|Cx1FM?2 zX#Sl`E1;*P!p}Z z-f(VaIbQK&Fl3&msKtJ8&cT*? zy<7B*zs66_IQ~Z-f^4qOSaqr@i}HEYrzUyOM6E*+g-42Sh<1KOuLv!vre)?Hvj1~A zfXh`|{eG;+!Sq=SyNnV2+Y$r7@j?Jb2j&@Un4b`Y({FN`|F;59>1!w0PS+vWOfKir z3@kw?ww*i=>&E&bWeshN69C!{?u*2%i7lNS2S5{#g{W{gFqgoGl?1_UXAG7ck8Ttm zP`&8NredW;zwO&NtLMa$uKr%AivlBB+b#)v{cQN$WX0yau+3$T2`ZqMV7&`khC9VH zmv2TK-VQI@ntVU{;al8-QkR0QtBw(?kRw-qv={D=b7x9Yf=kDvZ6n%5?(v^RG-iv9 zjxNg))>>pJ#U!l?{wh|DG3WN+o}T5BgTJ5}{*l=n$&d0Ab1D*MbZ0J7q64KP=n)q) z=lDkIBlN{&6iuGf;^h|$Mk+TCb->pP=li@n+c9gfWa`biNN@SJDuH>He-#qp&*dOo z3Y}xmBKy*g%mG#c5NWh6${DGZTFrdQ^8@3(ZTZ6Df$eJ$1wx-aahB^oc{a@5XT#n` zSyE@{Xmm2n|3@}5JwuHBqviCX{aiWv+CS@jRArFoFmu|23Uu4^2v|P=@lXg6HL(St z*YD$3{FH5YqPhq5?W#mx(wP|m5V^7lz1oXsy;s9QSJKNpWHz_|JS6j4f7#+<`$p?o zN_2P+qG6XX$Eip2vi13sBU=M6%6izD)B2pkKBvV+%uNN#lPQ=yP~2X-FkkE2#+O>3&_lxbuZi{|ut&YoB2S-%ZlR0qO4V>Ls_7!!e)7H7I>GLHo^JHjKtxeo@-+ec+rBTb^b#HZ56@aRE{Md-XuB59 z(%!HK=2i2iFCesmjj^K((P?bx%L8EgEFJ5^rh~pj8nVZHal&0iwm&OnNc`PeULwoJ zL;`^;l@IfNTs8bxFZFD$t*#ydqvFTKuKT`N@Ta~}OPSXM;DcWTEDkabFDB|Z*OmYb zcznr)@X)NXJpoR{J*N>5-?YOsfK%8#za*agAn5lnw*syMZUi4TK)nybXhZE{@@Z8} z#)J`J3&Ke>9E8EOFtWG#E{#M%*@24?O=tfqds;OC&YLVD>mQ}-5WRdt?-9DWH zd1&=YM8de1L;}gu($YQ_sLC8Jw8KOBITqG-g)w)F&6Av zx(%A4&%$WBvQym<*qqhPh*SJ%2XXs>$R4%@UnQS-pR9215Fux~WR%-%%Z%azRwwFn zwwVA3A{wB%3aL*ZQSP|;-_#cain>d zP>A(PUdpTQCyLbrnBfON+T;?N0MfY0DFuAfLxnVn+-)zoX46N7_kc8rln4aYzyW!D0Vi+#Be}@C?<7;89|-WS~q?m z^2--uMcC7^zg39r)~l_B9M2PRHpObP02CM4Ow^0)3fQE=2!xQf-kMajFm0=OS`l z4Q>5p9F#+y7Fg=DEPn`Wo@fZ>0uCBvby$NodyOP5HXg)WuOZw9nGGp?y~-vKOz|+w zb|AYU4PonLwAoJRoCoZ_Xi(1ui0mZNT@7kF-sZCLTf57XPzKrTjy>oDtrep98=cT5 zx6wEI03kav;l$WSc2Qhzf7CTN+(HBV^8MbLiC#9#fM{oz48L=O3ZQy3pqcyP^!tSs z0Pm*^GFs*0Yk)46)Ap&NiLIbaVBJj?(UpV5v!m%wW)pS;@Zn_p7uzHb6luWcX5IP8 zRfVgma2R%l5;JMEx*|XvG@v?Fi^Xiq~t!h_8Q`#AUvrb zUm(PpD||DGUPwE`wq8I{fgD&y$QG?dlhD#82mKEl!{&r6%|e@m z?NYw2UW+|vAg)igIA^eAgV3inXdkVG%6xb$wzP7{#VU(X5&nk(r&3v-7{qA<=Jl0qJqUZs1~)wXiem>ml&;{vk{~$bO+$z zF=kQ7CdC`ABDI#Cz>tt*i@U%G5w!^_&LcTEjqZ>7d4%(SOF>wmvwzvOA*8nnQT~#GO&AD1=IKChY z_f~+Mt^rCndpy+Io>1R)HQ9~}pfj427Z}z*Ckd;y2bIn8<#-I3gP8^V<3xl`4e_Vy ztVWI132+~BEGR~+4&ZtZn^XwGP_j(?Q{=lwK1qTq)=+xH4sclw3btm$p3An&8pC+IW~9M-+u}$2I*F&ES&C=Q{Yvrem1T?i)6N2Cj^#T z)N(HoX4qh3&hA;vK@AH~rSk&2E8s8Igpi8{_6XGBE{jR~NZu+k_xY`|%M1ITI4u@t zugbEhP}?iiqzi(rU=YT}i6As$?g~(u+em|KRELmIZ%n1AeVSCneGI}CItDGWPu^t_ zsl|POyZJ0#gyGm+P$8bE$R0*><(M#|B(gy>W=R>Hb(Woq`NSrH)t~l(tr9_Qi%*g8 zaprH1^=+~LBv}7OWz(baehS#PT2c-RaKVh}P8I%iR-SE2+)L)mL^}3%9?_NI6I>Vn zUbF@PZjcPXluKvj6(lqF7-c2_=3I1OGr0Pq6JU%i@{24`Gluo^fo70rYE2-pf8yhf z;y9>j9)zbEEps}##{B}3|&oWl2P zEWP>c5GNf4T5Qr-25@kny3vanG)H5hEhD*_>p2R>R?rD;AmmXaVKJTXN`$uFguBG? zBKbd@Vvw3sDOiT!H}(7^1An*3y5bV%cedrtLW>iRJij=jsA>xp+ag~BpGxw!9wF^B zQinOD@W5h2mff@pZO}PPa6-Cu4tk@Xq!~AbGb5lDrmTb5?$}anwy3$N?G)vCE#)R- zeZ7`qJ>Yvi=0wvnQP$t=Rn}v~u1^odDUN)>8I~ z>?*Z2q%xZI1N&Vf90^3zmkVp$Jr|s)Mw`OCv_P=Amz`#y#HqECcfsqk`--Q@ zH7^Mc#RT2@54)0JyVh<_PGRh3(y@12tlAec^)4a+nrvY(_mo0#h8*iiI9AM^WS2A> zU?#?^l*w(NDK?7@uK@6&j!sWH7-=EQJ|SpbD=b$7wG%=X+d!5Zz~Sq`Ow~(#60|~N zwU$B3{V1es{$LidCO{jN4C})T>rpyn^t=;}1C7=oG?O}gtGBr)G7WEi7g*A_6F18y z^$78f9L!!ldQi3Jb(X~kw%wor_q~(cD5||SFT`mWWW3S7-IbUv#I%Vl_NcJ(PpAg` zhWnAQqee?8$9952e#@r#FhV&OUW^+F&0ncgY-|eW%O`+j;^4ka;~ce21IV7mAWR66 zQ%Ti-3UQ}n2`u9&j??!xPz;%aD9y5q%rZa8?2Qb9LWqnO*>$csCvHYlMYJ)M<#*uR z$~kjKei8Z%1?y$2=nc-i=llHz_O1%g=Gpdl1y#S!{v|4?uJZgZdBI&bw{$fIS4${h zg#SQENJ)$BCpl4Go%X7f6mx1z@+nFkYY@4rUxFtw z>>g{jx^a>V&p+z)9dTskmzR9UWm$h-dkeE@P+J#>NbU5aK|(O?U*DuQ>X_(mr3OLg zSch`0aU6JJ5HlO+^Cf#}4+s5*Ly2EXW8SptGGK;;wo)zHuZHL{+Z(&*@Fd(frp`F9OJ!LpvNL4SBGo}l)oAHW%bqMdslcS%hW^5)>a@7i zDpc`&Ak`#9DpxNxa5BWr_Y*l5rv*+YZk{_cxcyS7E%ukoQBi)t9#;QcLZ#Yfd1qQl z_c!9PjP^!aSBw4KoU^PK>9}K;AGL%ugD9tE6xO@i3iGQ=&LU`7)qb#j@TrXT=T=na zP)hU89|GwD5W9zd3@z}wcqe>ulE7({Ln+|AZcgjaZmn})`QufU>rB+MO8`n5)_Lsw zPk6gagzNP^053Qj+pn>5Zf^AA;9k*jlYm9!Bw}r=6U#Oc#Rdl&aPMlPJE*P9=k)p<~JW(jidYE)Ug3>Vx|XkR89(D;CnQKR`b|R zV2LaPNy8hL#$vbd3kumE6bfcDrN(Kw9p;+@|BAoQmJ5%555HWMb9uoT${@r~_c}B? z#=f9s#TCv<*5ZNn@mFGC8RuLcdiQEV^BzV)C<;dUmJOF3+}j8Dl2HG1W3Am0_}|4_ z#%~HZ#OXcEz9!0<=SSGD8Ks_|9MPwz#QMy_A2D|QLwcHWE1 zWf@?XG}aA=Y(qt;(JESHnbYAttSO^Iv$J(}7IYiKy8l-veT3LNg7)IbG^q}<$y$SXvWZ+WQ>bHyQ%+A^U3b9ra;HQ zubBHMXi?ZR2U`7ezVp6v+Pz?5-g(~>PPs8Gu}6Ao-s$+3KsxsI!ee3ayrCGvkMz4K zuNvR{b~3n~(-TE|_jo^PzwEVSG8I73>((Sh|JBqXn!Pl#s-*n|D|_Jf&eSf}O5X~M zjbmaoUv*Dp?_WWU+dk!$(rS*02sqPv{|HqeRX?qKbz$W~kZA(N&V6_SEGBAZ0147| zc6GnO*o?HaGPfA!^Fa_qscW#=DE$jjV@*v|tnqjqQ_l>qm{)Olc}hme?4_Bx3enmF z)@YBSMT9hso;-WvW=4jHc9=8U={;@Eq7cc#*2Vkmna=1NY_Q2by|dE#O8V6ErPipe zYy0+`Ub;{kv+zW4>Ez_mgO!bo;&vEKZ-(bGb3YzwORfp9+1jzq=$!s2FlpJIf+M|0 z6HX@Gx%u~>k5Lt~(4RFP8P2YFvk}yi0GP| zyq|ZkXY}a$j=9lizUS%PmH92-f-Hm2Vh`4a-E@&y6uzCcURHeOS#-^Dm$U!a9dBKo zQttXGmRIhoYW1^^=n05@=%WolEb|o2#ngmdq>twB>~}}3E-Chic^BK5a6X9v}5y1V#nmq%l~yA0U} z!dtZ5Wp06tWYzXBcrNPR8Sna4w!t}0Si8KvBi-B)Bo`50&NlWPKUMe{YFo{Bsvxi0 z%Q=2pO^f;ZIAg=bzt7J87#!J~d24oO%v#CVqS%fr@7DVLEsOBj?{0fwntK#_MZOQ* zz$)YP`fgjZObKEOLFmF&A)G?9>7riCS-S62Zmjd;Pd78xlstViD+I}J z&nlQ;ka2y@}&8Fn;P9eflQQmf&!>P3&HeIuI6D_8nlzAgTeYmO4j zq%YKSEQ6a!qv)h%9mI!TvU!#`)vum@C4o~(sv8WyCHsfK0}!f(%jL-j_OeTOk=7pptm?6u|Sja$}h>WkZ!Up-gVC*E>j1@E@HP|>kL z;*$07+1R5+e;4M4qgNjLm921y$N#w`kbsZy<0!zg7{E;t8QQ$_visjs0BS!DVzES< z|CZ5G_h^>ewS#PaXkZzN19+w3(9X}owfypm^wq^T9CNFqsB}jSuB-7z{5ct-_k4o@Y@e;}uUoNXR;RY}%cYHJB^wDUG=LY!N4ms^n zhQ#61Y`KRQ)Q&$6@sY*VBs6hsXYtU8^?{s>b8v}G6Bc%?TUU^o8hZDG-MLF5i}uSb z*{liIEsy?)`z)gv8T%t?i40p+v2xKNF4Rz9cB z&$cVmGO}@*GZJzSAC)H*V=>?QoQH%%ZeEe3*@j4>E~X}XI-4@t5VNs|p>R*sqQ@F6 z-0Inu1x-9$g9;v@_l0FOJ6V>i5bpn|uwl-fL}Kpnj!TdSn==2o^3kd9ltO)A=!2M| zGuUAy1gsz+hoT*qv7X<1K76V3YIm&M=aTwBu7=y$dFUei>61kz@%Loz?uW65VEmaP z15Wq?N$z`4xI0Mb8Me~2>%2?rUp~2N&dOMLVtdPJ zeo`d;rq7DV;*|3N1`r&0#SIbGGld}h5FH^IyNNfvOjETwB0}__`I_DJ-Q+V0+2Omz z=$(|Em{&}wFPeRb3I%ke-92rH3AN zv)pqb$KifP^ylhr=na)`t@g^w!am{QdyrhLY)0bks+BHN>d+Elk2N}35uY!@bu)Ps zLe8Mlo^o<#gf!l%3#`7BYNW^d4>ZimjMPK8{JuXY*Pv zUJE%cp$^YLeX?s0Y)C818*hU;{AgEl_fFcKWk=Ej80cuam~j5$JFck=xn_p@&n*8x z=er~31H+?DvdRLGpAiWG9uCKuvW<E!qO&}s(*+0uS!}fPdylN zJQG)>!G29StK9s_nQ>334cs^pZi@#+KUIKg1CPwRJbxdH`dG6_QLhZGr!Q0N@^ZhN zSZc_BDgtG)kx~GCRge3PZMo9y`e{HlzJ*P@O2S8MFgpR!h))OMX^X=#-oPDW8hbtV*GDqT$FVxL++;n?5_ldo2mi@`*yd7eP z3tGGMRJ*5H_9ff*k8j&Y1rh$f&ONUq1l`*=vtoa6%>Ex`WiGyfIS}scMGigz%RnhY zGcwtm=Kux3xhhyD3-y3*S;My6Is+#+5G)RfLZbK_M_>gAQtxE#qyc%4f_;jjxJ9rP zMNrH(>i{Bb(>Aeu2KKvR_p;C4r3^qSiL?}e+}I@Zb>XH5Qgq~n?fAn=>s}jF4IX_b z2ARlb&4=VIomL4t`3@tTvIiq_rxE$~oo-&&@nwE)#b4~+^6iH|%g-~+?WyMMfm~gd z{poFG$C}+u{l8u{y04BULdq2e^Bi|8ZItb=p#OyI%HU9N091l($>w@Sy^N5B5}WjR?dflI;O<7zZF5BKKJ#R?I-;K&qOB8UoO@Y)jaV z)FbJezi4oejlUkua{E1o2<%Cy^R3ZecI7^?``TWkQ`>z8j%2>3VvI6Fa`pVvTIIj@ z2$lm4zqD}!zBYVXf&Kfl@~?MgcJ8*+8=R`SRSRbqxFrh*vw21kG#n5A4Q~#fv6SeE zRyuOQ%7Z?i>0l)&yn$dZ23ZS0-nzA}GZj|b$$8tX^NZ?N=#jh2;5qG%=ix3FuJY+D z3XTpUu|ZJfp|cPhjM`bv#vRr;SJWL2K6KQPfPIUXPj}(pYmiOK9NGIspIUpP{>TFZ zyuj?k7GWlfFzGDV9Ru#80dCe{f2ZSKmAPSqYIc76b<7v`tCFyR1#4kBT*W&S&)9)& zEoWnh`Wa%OqvdaO_=@NgPdV6^BT6O8JiD-edc}h4eu|lx=J|t!mfUW|Kid>8wJ31D z7q~yawcNpzO-|JKg|lHhX5isMNX87f{ix`m8n6_At?A}-$y{Em$YDUN@npU)*s0y| zW=Var5)wO5@9>y0ap*KfA4z9No_h^EUFi)k5=r^aE0nPPdS^MSTFMt}Lnmh(R3bu% z-nGiNeAFp(6*_~sEyH(9J7O~n(u~I~S0c<7Wm?XSSSn!CvV3$=}}(j?WRWl37NB*Kh zJ~2Yu%pZ(#@K?cGSZFgoS)o5!J@e~LmEUa!GRxDpvj~={N2!E4Ry~)WN^56!AKmnN zU+qA1=_+%ArrOUVaGr_2cuH6HV369Kg28O_x)OsD&9poku_6js=VIYfA?!Q~d6EKu zCxpF^BEGeg1-UqPo;~KMCZlDbdtLneMvyHFoN7n;?s0lBZlRxHw}T%ElFIM_z&O>$ zi~f5a%Rn1LLbuHzb_g)||Dg}FPzT_V_B$?7Q>k0qYMy?E_TrIwW+*d7@;fB1`FcbGU}@$;f7K%99_JZC21x;q2hA?5tKouc;tg4DgM|2qIVFdKI$#Z{ko9YzG^i$&Z(L zI?VsNii2edtn3*PDgzM8 zbznZIq(Vx@z@6RK9Dia3GKiAZjw|aW9587z9YC|dPTKl_y8mP7-s4){|2TkucfYpQ zwrZuab-!Cz>&CWjlVn{`N!C>nmXcJ?+1A?DMHV3m>ne#RNr`oP`(+!2X z2PQbju7R|Nfyz?{L2EIhg>x;OgY9ydKkMxJ&_52%cGOeg8F*|A4RM!)9#LWrEI^Op zwGKP9?$gDIl#nz0gq0GSR+k*JlY?Aj;Ckia6GC_q7yDU>^<2Pch%XpAp%%Xi!_+FI zT<%o=H9L&a(B3r)D8aM6bW6p^REKFZvWPO&h{w$iS3eP;(GkH0F%V2T%8i3C+Z~mC zHsd}Irx%KSuy6xYjeR++MHFZeJRW}x*6s%Ep6r+ePd&EYJKF2nP57kshCBK~zMe2w zL%(>&r_*@SXJn~*_}7|1+;)-`*<);=85MN zK^5SY?mb{TB{T_-%Fshc^kPd_!RhL?T5gA*FngMTVSPK;ARo)?0>*U9kK@(c%CwFB)u)gE3b? z{Dz^M0vY`==^)9j)$3EGFQ3lJO-69iaN5U+z^GpnYc7Goee(WC?9 zC;*XY;6xLhOuF-n>}kqo--;3NH38A3dv{Td6CJy3asfqTvPn(c4=f?hLS-9Qj0! z9i{#FZiG|Q!nY3RX~6bZc&!oj1US?rmpwYWfbv`bmGE)NcxVhpYnY37IQr+(q|QG+ ze-dmy8*BKE^PfIAjoe%_Pu(P)`ePIW*0H+VQCCw7IzSA+ZnemXS-}BvF#uu!Iy7h$ z4cbYEuLDd7xKN)mwJ8Xy0MPnsKS!Czvp7JPK`*GKUPnT$aThmC616Cx_3>Z^$NXPy ze4O<`@S-IKmlKw!R%N_}`$)9sHq}LrO#7D@xW!hjJ!YbjP1U`SIb0&UoH#Y)va2+{ zv*gRnNb&QLn!sW!t0K&ckrgxcd1~)e75&)7z;s`iF59h{Z^Mh3b2bNq%B&-rqi*bb z_tLG_#?)&9Ka#%jOk47n%qrDl>b8?E4+w`hlp}+PhPwlFs6yP9NYna#pDxul5H2gW z{7iv}KZm0T$)piVb&Yl#JJxOg`-wBL*B=v^xqXd=W6e>QlJ~+;E;GhCFdUi2;GYL_ zKuCtBLq}TTkDhCiW$HNe$aJXChr|K&GJPu6uwHM8~%fxJeOB07gKSM@@0&ay1k&JIA<9q;tCn6$4aA&40@u zwHfq(BT}uNdlu15GLCd>CyEyAbrR{ajsxK-UVmE=Vpz*BwtENto znNL=F8!O_2uhkT;BV6kuaYZ^}6GBsp+3`5}7oo_Q^8$u84!%_fcWI(@>8$EuCl?VK z##KdImnz}{HP8GF`V|ubk>V6BsmS`N+lx)9)KGvd%}dXAz?)MdXO^pr_R~$CV(cqr4wL&?7Z-c16pz zCp@|;fSOf#|7|HcaV_YJRn_Nj;`14%b0W*<(=RM4%9q6p(a0x<&D>I_Q!Pup_J2UJ z4xe}Hx4I`ey1Qw&Z_lifSCG_hdONBlH`B0X!+ZT3SIxoXaFK`PyutWSkwHZA$}(TX zsvA?4xhHcv&})MfrsNxAa#cwx>5Krrwvq?YOOsz}fU3{EsaD&tDMxZ!DyyITJJR++ zYmV92{{wmlHWFm#ySb`zr!D>+8 zY~O0jc%fE>9HyI!gRiRx8p}Q))-za$O*9ZHxDIHy0U!i`4-E!@j;K{W$CaYR3dRUt&CpWDIUg6 zSq9xCmuNLF6vyRNLtU*woqsU3Iex*k8&6c$;%aDPxl$`-W+tGCZWYqiO>pg``m_=M zDzOH%u5J|LfyR@+dt1X6HDvtKO`*9=kh%(a@?hi^5SFJjMW-k)((tPd6l#ROv`ber zwT&VlHTij#4WI!&xCaf)fVSrvGWhU`WT?)q>R<1l-#;B8iY~1XkQ+usd7*WP%nlLp z02i^JV~W`b%)$% z+`a(y%PZH3B~1nd6`lNEsB=ye2e7s@T{)V;NH9Gzo@i{GUK}b1>*8xuZOW&Rfs65^ zBa|kyfGK3KhA+LtgM|JIV7Uy4-VwPZDo0S{R|3`yuA@Ug_Jk=81P~Y51)y!+l{Hmt z5XnH9?kEI-7+iwYXaQn29r%4eEM-7-8T71)k>jlfp(ErYH31;(vRf7D7Ntgm^PAWc zQK2c8dh$zM6GjxE7o|Kf2;sk{Ykm(5CHg24_+-u40>Ufpy;6zDO^Ny;Se>7f=7zeeo4tQJTuzyvJwrvvxC$S816cX!R(B@XZENlHR4Ip_2hat zQ)d6_XO(A%Dwj<6_yZR2t14e!$6;0 zmEbRh_AX}&#dci&K*wD3iX{xd_ceG7{AdN*oE^(bjC1Zq>po zi<)ljoy$UlM;t^_Fw`(!{e(okrWt7THSnS58^$;vm5)&m1%3Bj)##*U%$=5wkAvKb zLd9B9dsZ@HXM**7x?%|uUGk>XAm`go*zjLMGS;&1vX$@&u-UKY`sV_gG#wY0IG#yy zgwrag`ZC5&lMzdytLmrXjxuhjE;TF#3|69tl;$Ri9|xW}w+gv+EF^Z|=!t8l4VMlnv-X}gnYzf6EbL-RuxSq;XGi;@-?)9+ZymGL zvr2%BsPA)lq`>5mqfyGnCgLNYpj_A2=q5u#nXO3Q(x@c=)d;dJC6`zXcCXq=KOGQq zG$3MIZvDZg=>q@eJw@gTWc~BsTMJ6{IPM(=Ta3h_yW1{qeX=10l~wyc_bx8yL(E6w ziqlZ)?y}h{GN|Q(dMS2juZ|;>g?xYQ{fi4V|J?1mKZNmB3&ocyMcX2_?%<;T1B zRBnB9_S4!&H#SwD9`q$`uaG(~DYGMHUcWp}l=}kE1 zG5?+Jl=TS>mz@Qtxwrf*jW}A;pt@ZwEbIWgvMVp%FWQ>}R5M*V-kR^fB&$S9;!rC^ znKF26ia1k>NR^$~hlIy>z-tG@DN=D78!uDB4@nUop*opt1$3%{aEqoXh1lEJa!M(_ zq!g0`=o;&LOU~;l6Gh4643DyV@DsONnrd21;i$7G5_z8SGI3(|?dON@MP;n}`z<|Z zxz1sRRm^Xvh)$%w>bhjM`YFF)=^?u3mHQ80mp!(?UuTipbO2DV9XfF3X8xVuZ;Nz; za5r59g^>-WDNs-y!d2dk&8{@gN3Tg>{PDyt#PdewtjhU=X=O*ziREfbR$1|ERnu|} zxv(hXiz?L2zp=50HgW#sHRPq-b=oIxZ!;6eMfQ5!B3{C*-sZ>J(cFvcURj6T2tTv? zUdi&Uh)klzL|0)4{44dcYh9UV$x#p+K*{(ZzAwGm_onBSw_FBWWU+p}J%A+Fcz-%F zP)3Yp_9}=gIlqVlVB~8zcmSQZ8w%-O?}q=HvF?ly)?Z@zM@$6rXRz6vfW&6x_GW*N z)T&3;p4+gVR(S*Y&B&}Tb)J{lKbqL)izQ|g{+A#U+pm7EA1(yi^PvOlJA7S!Y+MT+ zTU*|#<+6vLplN5_t?BuL9Q1>@%B2uZ?_cEQ+=H@J3P4=rEi&3~%m%@0A7dWI_A0AKaM=x-?S+t(LZ$uUvy}*!YLAYBd1CEu_eHfQy($u=89kZs*pa)b zU<~_wkZlD;$mY!-d0x?sUMVk=*w-vyavXlIdXTZFJI9r8an_j;99*QlZ>GFolnv+s zAnfq^w`{(D9rEU#!Lz=FwIuye;<}SxgTIsXN;o>J<7H*Do35~nczg*Dk2pjwBE8tW zoQ7YXP%RcdK9*g!yc>U#1?-*)$TS;-PypBK!#*?J ztHV#Gnhv5W_}x&(gZMz-D&)am8thRUuK=dzn*+T8I2V92_}ii%hm5+@MnhLSJXozY z`Z$8~rpGd;qHhA|S0*}OKW$utF&whKt~-kR+k9lUq?u?8p)o>zp^%R}0g=*LIBegk z(P)|izXxyrHgo=-PT1P|cI1)px`DK0+Xo_w2MWxGM8PX?9vNLG4X2~{LG_5Ku_so#K+a$|jM2LO zd;8Vl@SspWL~7|!eP!z_=Xy94UYq=l35E4jPI0{;L0r81nzZppO;E|ZlD4t&OC-s@W#wkrWnDh#fXi0vlSlt}yY`e@>3@g$ImAH?5EoST zEFOK5a1wN>5KNmLy#j2@7`hq!eZ(HUh3^wR3KZ@cALpvXX-a0=@Pj=a@U&(4-dXXU z0r-CT%RS5R{R43DtD^dXjcFAt(RMGRJL+EUSDRn--`+GNzVzSAMDu~>VffX%;wwiU z?vn+zyxT(XU8`f;36S}LGxmvF7?27QNFBmBa_3R2R_cz{1fjNpT#{5{e5#!1K3AZT z>eH8PM;J5I>xxt-r2&ngV`8uMOf-WOjy_j0al`zwpr z`^XpCBKoF#rEQ;r6bGZP?09)qYh}o#=4IQt=;vD|ug;(mj@uE>c3hf=AN%#{uTHxX z>xpS=044*rKUklrJGk-!nbJGVnF26speJ#yzyBMgT#CsK{C<~@VaKdg@1&YcAMH2+ zX)kL$@^a#?__Fb@gNUiCi3_K-^THH+7XM=$SOn$!w;b;WqK zy?9&5hxSgRN_ulck$nAIJRibvPMv({ZTw)(<-pV(TODers{F@p-w~QEM->6jZ^Qwa zCwH3en;F^i_CJU1SAM;LKbuq(MCPQ#|91q9YmdM9R1N)XZY_S;ax&4bLDDn?zq&B< zzF;Hl^((X2)P$4ed7b_LULGl?tW2@X0(ETpTSlJbe0vQXf5x)t@16lN+Tv)lQ7yo8 z`~%KHW@0Li_3z0RbMf}1i6HHEQw}a6O`259i*EIKbrbq_L|Hh=fTg`Gz%dpi2iF8$b)7_V89nm=p(Mg@Dhhkr4T%AmeNt=Eey(OSF4F0z5-CX^SS6BEC z@9yQc7<2c+|2|Q+^Dq0<=EkGqfN&8!bkn9!7T3dCN22ttPo9nVxOxFF*}60SXfPaE zeRJv4ozdNUzWV;PDv0pl;ZTU;FsE-EjTrI3n;5%=%#V=DZ2!LJm(4&JiSUPH3_Sy* zeIA8nNEGtS#*mp)n@Oj4eUYBR7X~%^9b=2IrWsIf!HHEj4`@g_gE8iSBy+G6%Qm z)Y4q=AG@?mU!{>RG)cKf_@=t7&c1#JSO)f(_d*>@$#|sgQBaa&%chmP_=Jkj1%?i%2+Cv2bkQpTY`MiBu9Xdog^y zTb1;RjEC43_`NWDMW`DIc`uf%M+)2SRX5GF=41^L*z6}*_8u8L;}6WYcC(D4?6*2i(x?|jR!H%Fir8Zo8Y>f% z(j^VFg=6H^JCyKafMhkO(BZ>Vfk+aM6sO6>g-6B7coBuuv|ozg$rvtdaE|71g9~ML z(9R6Q&odBN<6^_{{34np83~W&N9uIKvpHhMb>fc5!hLvg@-XvohjJvP{qVS$$rYq3 zQ0d4Ogi)~ghL$987&a_LqycaTX=&mwvDJdr-VXg7Jn=03XT_n^+x`fu65qbL)H?5aYzb7GF~Ju%tlo2DkzsCOlJ8J zay<-Ie3&PZZiXK_CgHMkGdke^E+Dy5r~|F<$Fuq74)I~f?HbqA=JiX(*n?vFyNN5i zMnVEkHAF}By=pA@-Ir`Lc`#|$bC1=3o=Jud4j-JtH}edK zb%%j}8?xj4*sXw8*Zp^NKd(L9Gg8du8^Muy+5zP(;~z?w2t7CHQ4x$eq3^qrHH0B0 z^crU)1Qt5r8qFS3;?_<%2^rc+QMAGCCnx!11M1F;HXP{5C!AOeyHaiS9_5oYKPci; z>$bKr>Sv#(f1k}4i>6zf8G@A4x^}@~dv!8~gOK_q%98?+FY0dc8XjO{VHKI;Ik_32G=JK)A8r~@@Zk8 zY7lQxP8=t$D6X%oTQBL$^vGMk$D&i*0>6>kw?!rxtv`lG8Ukn4XVTh7vUyuf|1Cl%W(#6PU9#ahC&#Gz0}bS z_Ni9Ma6W6W;V9ylx^98BPeqN&paCivz%TNrS*C6kzMH9T!)8*GFXq!w&se(7nAffd z<}stVv<$fAV%nDh4;KaNvG;XVc!}L{@3IHX`N0(-*m6F5T9mOk{>9I0M#0B<40V3( z)4MRpcx-Ry_if;Y;L(X-11%je%rhp@^wBDIc^T3tX}Q3TC0Y*DCbkR{A+|Lf>Sk}N zJOX6@uiY~$!(dtW#@KI4;y}(KkWlJ)sl3{s9Orjk%WKyxkK=u~K-2TQ)w@h$x|sb% z>g}S~sJ-_JZDi1!mcjzN3U<4SEbnA<9#KLnMoT;jfRiCwQ*vU7OL|)R{p3&1BB44;Ic5z9ZCoxG~3a)Tsx76 zhiK<**bK%*DtsQyz-rQvCLm zF9pt@3a-uJ$D6+lq&ZH6`RbltwXnZftz|EZWpJQQZ5ZGBEMK*v^py!I``pvZGpu#?{C_a!+4Zy--V zu3-yZd#Y(pjnkBAMi1tJa;1PV7>}eY-x!4SI=`eGGubmNG|YYr%fatWLZJL8H9E(P zQo6Rj{@I5)&w-P&r;UA<8H5@tJYcZaz>R^hU@gF1Czzyz{C@Xg{^iZ=qv^OGn~!LM zYa8A(Am)us`~g`$8LB$&rmn+J_+9`~YyGf4W$wzd5~2g!cmD~QnenBg`#X;N;fpLB ziCr~abJP2@Jf6%GF4B$P9iW!hO3u<40@HLL!-NOI#w_HoCaGW!bOzjH0f2a`E3lEq zA7j??oq8R!6c8oElO}+Y5tt-#C-5IT?~8vgj&w zcO%n$CLQX+t@V-0`|V=l3L+w@)`N{;xACbG1Nk(OM(M@JP8IGtDKL3g(XH=yttqOL zVRDYEa&U)f@131OU8m&ho4QE0X)<&N=n&iEJR?IB5jhDdP7U%c2p$9@PcO6N(;)3k zg(-Bn!rwzQjM^=Qn3Rl{xHN^reJZB0@$5c#mH-wzUk!i24~#AUQs9y1i}{lVA?%XcH;R33QGboyXFJ9gA zcoKV7sk4X9ay&70;fH(x{;ZyfYCgN;Q31VE}!#B3IbSMSq)>@(y&Yo^J(MR1t6zj z!PanAEFjXYt5L)ZfY6yBZ4QWBo$P_#SO1#_E1ew8= zsV3JI0%BcUcXG-Uf=!3CH!s6;XhK-U;I0&$5V>M*hCg~AY`YIBB4rB60zhl88g$ew zFaA+gsrXJOC)Bm85xWu;CsdE;E=p;!}JP*zLs zj`>l|F+qK_vp5tB)LR?erifp*QmZjeDR;k~dnvp+wOYJ$qU;>ozc^;>i)eYR(qvS} zDcw&SjO1qT8?}UU*9j`N0}#D&{tC)8Pe}9|2cxqA@fz$~DE=IXP{D_hfwv%%`XGi1 zKwm=ylP7IHK*@($nW5gN;*JDSj(g4o}zooW|2IsP2`cT15&KmPI znJr#_yvn%Q@+9D&LxBUYR$ptH&OGO2VEB(DjUzO+?ovP7(v4eUfIo)(@%5#J%Ak>1 znvpV%(=6qjntxSi;7wyx9FF_1gwhm7qw#0!U%5Ggy~(+6SB>(IIIaoZ87b}jXt7xb zhzUGq;B+LQ-AfAKltTz}HK@L}A37@#%x1{hmTdN8-Qf_L;L<@_g9K_yc?9ir1m0{$ z!am%>Bdh)b?JG*U3SOzl7f_|dYU;0FI$sa{k7VV#pj_VxsE_1LK35g9I?sEhFiD*% zR}SRAYX}xePSg#wqA^MQ!Igt0zkD=BAJDx$`5q)mtj*x_g6NkAvB*D=zjfpwTWAV{ zf(?*E(Ol4@?zI}Jw-dA*rSsXMdo5o2*Fkm`Qv*esXmtn-7lm4+Dy?~jDrp;v8%y_l zXeeUZ0T63*P*EnwR`c?8A?|>$t$KrVqOUC<{Mo_JNh&+QKf$D^px%7EXp|ZAMyG;NrQU)y5+PD9KpD@?g$!^S zUvC8o!keq$(4D7@pkRt0D2-92L;w^3+!kr$Ir{quoHZzZt&#+Ea>u`)!+hO=a1?1J zQ83S@?JfPhXQJcg-msMU8332o_j1?5ByixI{ch3Z5jgsh=5{|GI*ry|XNDTh_n3*! z6~V81NC^M!%w_YXdsM zB~qmq(d9V7?9OJ>7L{ftOp{+=IsuGBYBmEwYq%hMPqm|kDym%QoY(DR19}5yq$oTE4k#5ahzxhSTIfLUjQUUl=;l zjV1xec`&C9Y)oT-22L!Pi9*tJ0#oGIb=C23b?Nz{Q_mZCOyY}x^? zZcx_e{(5tyZpUvyUv#Vf_$~R(*&V;qvEz5m`qghDvvxRhA@n?mQ5F&vY~ngms}-zT zW3Tcq5taROL8=5@ck(qT7ZAKb7Oqbviv-*hoj?lMjB){!ozL_JEc{qAzXqaVXzOMrBvNCJ z31Gp1-b~#Br^_lkk}{z>7FmBbyDub?A-;t>vw}BfDXJv%k0z@Kc?$uya>%4XvRhvb zyg};eylVSs{$F$Pz_WL*(-%DOMW9Czry1h&$w?Z;&k5`6J&w%}TTp$UBWO!|B>2 z*kHYB19=!?F(^cl0X-QAv0|i&19YLG%3~>U)*~t_+BCC{g$I;Ld2c@ACt-X5%?659 z5{T&CkHeG31C6;NrBw=ZNQo#uz_xGQG5()_-1!et(?wBNBMv!aOGvY*-iTtU)6h!j2L}i0KJ0R96D&q+O8Q*mw05&%ztS(v<%e)E8lWvEIxzJMt|l!sApwtO9J0%=hAMwNiGwaaa~TQQjP(r3@?@ z1J|=4HY8Sp($^L${BIayLrF@Y8=B4xGG$*w@_bm9&E^r0` zVow3v&ptIz0b3{f(iM&tc!Vmk-Sei$?!g_B?7r12PB=@C#d7l-si%1fF zvF@>=+vY>_9eFJ8Oo#=Cg^kf;r7_K|v6xv4meJ=}_g5a}B;!SrY()rhY9!ExAs)1D zgBg&SRG8rhF&k$(@qJV2P%{#%Aa4ha2eN^JWx9r@&DOaU`FpLQimK3cTTb34V<&0b7=LSyTy3$CW070BVn=-Mk<%i0Xk43T!R8cW}U~lNoqJ=$z^61+Yjo zCT^_DP0l0%R+>*A4QK*DcFnRA0AeO}W`(;bU8|+{k8*%E6F(en$ATZ9NdrLiH%B^F z`}m!E)hE@|np^O@jYVO;$L-8cs*JL`4d++}E9T6=p?HF~08bM-_9!)02qBG$mucK- z7T!MopDYP}SjEf~;U@%i5>_t}tbt51u2xe+$@sP|w`PIkU^j7|<(}zmZU!Qhn^Vys zi+L8dS;9_Y>3FN+X9~PhSa==_%TwX4pZ{p(YnD6ugAC&dJe9LYLy(+IviH?c!b20gv|l#iwwstg4P>Q8<;?)bB~cc${OzV9&4qwQOQiRJDE0!Jk;+H{ zvBwDXl&k|k{Q}ceg`!ry%zh1gOUxAAO9d0`dvv6)&3Ch?DX!}BsrY9mf4C1M^eGW? zhCfbj? zCMBQ>MunbILvXRY1=C#!2()5XGsu3R;>Sq-PX`eshA}3%0M$%U6oK{CKn=K7+(XUK+`;d6Hwq#4)IFELgVxG@NKLyz?JJukQ=1^ z2Xv*dmQz%Yw0NZ4x1vd-c~Z=jN(8h&qNm#=&cT3D+7);g4%sdEJ{_1~#2OX!y0uUb$?a$ugf+ z6$ib&T_299+du#Wo?5`h>7y`Zmp*d$fFg%Z@trjG1`=6jT|d=QEgYIm6eeDu!*s z-61z!<<5E(kyiu@$$6JsGGZ_mY%l;Z9Ag;w$jGEFyB_A6w4a(nB%cb^X~AU{pgQuf zo91Un{OEYh0cv?V0=XD)){>>jV&kAo!FGPGJz*N$d{pu)&dt5)hw~T%YT${I0bGUC=jquM7_Y?yt`RV?k&d0F{pC4iNu5 zdU#hY8^PxzT+Q>^x_o&8lK~r+f?>L{0NBa8zu%I0*FmT<0VUD@?qQ_ajM94J4Dz2i=uBow?;TP;=$r#ewRpfbCi~Rq1QR6A)5ZDFD=S{V-Omc&#*+ zRe*YT|3pJ?(TQNC-qtpD%Twc==kU98>6Q})wu_!43H5dO+oIR>FSjc=QT3T$#;nf< z9(!@eBBYHIw6r1iw%a$lk03#MzT<4;EMWFMjMH_v-1`1EbK5eaRbukUWmxZ^Hs=d4 zH>`ssH}1#WC}$#CsCUiR%sso2tKY&(t(?B^LM%0@G2iU^-Yt**BCZ$x_?$1oVb$G~ zMa7OD@u%&{Ie(k(ti8B5z3btmw^ir+p;xI78h6It+4{oOXMc6-^LFEC{WY+G#@hf?dkf>}v^HPC+P#3aCYutgDD{nL{0W zbSvQO^Puj_&b(P2C)Y8@Zw+UO*G3vv?Qdm%so!_|+N>{jZ=LLPROY;ctV{29Dt&r3 z-AYE}^97LESYBKKO7Y5(4>Uh?yz{vuH&s~B>YB#?T4T@UL!M=53qXU-mjyowJhg(% zABe;Ko@e1z#p+wB)O1RT88(zuiP-${L#H*Hh1p_%KKpZo^n`Pd+G2Q>#odkcOS4Y9 z!a5xXSH{}+u$#5criDTOzVR@)V90c@;>_M^1CJv&+Kms+o8#4WUBt(fp)*i8l`rQ) z$b9Kk!N+#H$6l}6pOijGs^5OtZS zHqNeZ?_2eE7tM_>JKHlELiw#GaLA-b+?MBd74#_TM{H!KX-&2CtHwoUaYe0@xB z=!)&0Vl|bX_}|aHXU`W*-D@0s*?NY2R?n`x!nb72!;d#^!7lF7i)EiLAfEVhVjG)h z9cPNk3FcE#=I;*x09YBUezof$?o&|ZH}skfxB!Jx*ga^(KW~lT9|yqgzM2WXSsQiM zPfMCQ24x$)zZ%Dgtd9IBghn)7b@Ew=ZC^vU__65JO689{BB56HLahq0$ngt(uPnf0 z`;U!^SM58$xl=xa6P|2IIbF1Hw~kQoA+5f|%hHQCQmly~(|lXU)C&hrTye7J95Cbh zQyR!cg#&JN1f67i%WDNQn_OL*N-pamTa7+Rx3+td(;o$Dhw!LaJ2`^bAuFJ*AO)Uo z`J2I2Tk3m7k0KANlsHX;3o()lkrWbq690CvMk#^NR^!k6BzOMD20$?RIY?fhHllG4 zwL1f(2|H@%_HJ|iw%Qxs!v5U)7j8L7DE;djijZr~V8ej2}i zKAoCG23weBoF@0QGB0Wl{D_$CqUKy7z`JNX}(vA_fo#%L`2$N=0#Iy~bTU z;c8hmHlxD#+V1u%AQ~U^P#D%qGTS&m@Rjs5s+VYK&^kvH(c?#!HtiRnlT%g8}1IcTn#-M?q*P8RMt$C($C123aof_ zructyx{Q!cg_NM$|B>&W555~t+HhiFGAD}r_f$#!@ zEiDrPoO-hI8Ei;C@^+mE1HAV$UbI=6fQwtHfN1s(VRjTSd2bZb_<=0nt4+_icc5$a zJ(_;EH?y>I0L*)I2Jh)jrKZzaT3I0NLyht95Vn*km+S1{`(pM)`V#zfprJi#oPgKw zJ>!Jb7>LC6coa5+j45L-pq0d9YG<%nw($#yjlA!s#U)K$gfm+As zN3A!@V4Pqu>4qA75Sx1yn#KSNZX5-R+1jwhFzWhy1kE1aw$)Dt2eR)ooDK)f#1cFp z1`F4~Z7w|!MJcG$g}(wJGgjxY`C!apCbpEXDKx&W%dY4)<0}f*@Q;}Yq}_|wqEd&7 zGq+G91>{)fvG1%njic1>MPlKsON1sKyk0xD9L`kmb*amKByIvOi{=Yi9QQ{?mx;1v zzL2R%U)-#Ycy;77WEaKX`xi$ixs$20nBwNqsDS!gS3mtD1+F_dEH+wZV)jBcw({Lm zP1l5-$Fe~hs~Agg*~dq=;X&7JZNm17oL@9-PdmRu(RIX2wE5gN)*uM&K%=6{hYK7% zr-UDoK(L}ynXo4Tym~?jwIr*!#2P-rz7EU?VMAy#+3LO!0JSz460KhkXY@mj6gTfW zt;j%ilcfhA1}py(B=wfs&A?c+jJ<7qWGGF5zn<;yd9e&1AQC)61@-96UV}My3dd2Z zofjS|VXL5qCi(4S2K$)D$eq(VK5VG1)M0Lo!gjVIAh0ej4t6-r7xJXyvt4W`=0Hph zI$G^-oN^VPtW@c={swy}Ik3Pyuwi;KTz8Cy4sDCqBHd@25}Xh^uqnMlGRv}~v}jGH zS6NxjZp6lKQ?Ifu_tzLYO{cp2c!})pi8H<|I{-63$LrV1@1eV#rW?~g(nAC+@$Iu;MQn?h;cr`kci|GOMSaQ8~IcL1u$Q3ZoR6(Md zn3-w=Z@`4D;hFxytrK9*Gmv#t1WuEwD;HIyUYKPfPH{26M7Ys7yg3c)$;5F%9=6rF znsYDHK>hc}zGB5>BjkDVd(;JmJK?P_Wxtuxew10jWrFBL7|P$?rL&noXKmp5t({NM59&;Ox{y;l>*)At05e!b{f@~G)-)0ngmd_XDJp& zMYU32qfx3c0c0YdBGy#v8rA%vfj@sy@avfRi-1lL%K)u1{YugPrF=0<)%hUDi$QwR zyH>qaW5HCikm=`I?d%+KtB9zjq;kPmtmH?0lAc5@yvuhR2zg zV|>ll5-`_!RBy9YhSpnWsVf9dvnd*8{K{}t++Q*ey@qfoql$O^EOemD7j@soV zUdt?KsZQ>R!zc))QB|9*iTk{u43IORE!+=1RRB1v;pY!P?Mrvnnd#_H!2>8$gER0% zU|FgSoOfPw?GLU?**%`1X?6oU${`WbVp0v!vs$lGK$e2YDx~?pqiZx5?@CU1aQ9bo zvYD60Ecv=-XRn#dsOGs7Jx=dHEl-bi(b|IgNK@S-AhHFd_k2i4{!cvL)q(=jELYF@ zQ}<~D$4lhrt!g^6j)yx)mlJGW|BpqS2Xi5evkVCaw|SDjZhOZO!V2) zeOS*WT&i+$P4iwKw(eVoz&t!KTjS_Lj5H432WLE2Zk>AE1kV5?0dNQfQ`G@Z;yv{%DEjf~0+mU{_nGBRTd!PK-YP^#@7)0L+EvW#~9ld{>nz zvIBs+Zz%*YD9w40!iR_@W2AUOU!jSJYQPRFlXmm5v+gy2`+V$L_lbo)8_~O9zV5Uu zr!Hvy{c!dy5jz);xljcUH60k(r(n5GtXzNO)@WFj=TV<5n z@B`bDpyjcq8$p(wG~G3~#qmdU6P%#IE~gUy7ak~NlE)TlLh}_9KTRkYL9DLnVJ zbRY}Q$hr!BohISl`0m&HeM3&4V({tz7WP7O+Ub)|?f1(cIA1&&JZ4Y)esGof*JZaHu9mhzBm!}ZW(AzS5DvrI!Z%tbqydaf3TV5RuJNU{C?UGW%&KMRcE`^ znL&c@or3R#tFc30`(yuxCq|&3xh?HuWO&?I{@IS-e8WS}6w#_w>VdaDH)Xoo$4{Pe zAAgqhC4O7R=|dj=&Kcd8pSg07)a^A-ft0YKg|U|CZ;Ga+mB~jIgZTD;X$a1VuwBl9 zQ&4$&Xxjje0m^Hk+uW?Mzqxa5`FGm$Sg%8X^Ff{Yh2-=T;h>MfRRdYrc1SRn8qAn+ zU*nW`hy^53tA}Xp_CuJ+^-9tqt>vwflL8_uvRF6Utj^uF#;wfe%xuDL`|0dU`I;cD z)LjjBSAwS%ITz~Oj&1WiVNX5vzbt^^ckX&&mV2rTB{;TUIvbZ;Y#+3E((lM)+xq#e zn2Y#p$CCdRr&XiV0+HML8=L17OQtnv15BHCQc@!+30#pwh+O~E>O^}0O_y8(ySQ^& zAjI$wjn?@*f)&q#H)E77t=A!p3KLUcK=?JP=+es0vci_q%^cOtu7gkP$}ZUo_G{(` z#CPhp!_}<9Q~lhw)V~fvxyKMSquyJs-add{gZ6KDp3E3LQH)3qshd7QoZj0=D*n_H zevsqgSV(#`<<&@XD+KhwOj>7k%KvVYtlb!<-}046(@ zwMG=bElB(*D*CgIV+u=~@8!oDlpyJcaj+cQUc-M_p#f3snRlG&!-+1}_X8=K-DnwFK*;F4Kcp;=j3A*h*I zgJxwr+JBy%sldTqUDf{QdA7FRI zrY-hAbbQ;GyO(~gbQk>vqbLAXPcTiMKPF9cZcff-TJBoE@5j~SuBNgdnV8KhIgrxA z%_WQLS?7`d2}X_j9n=wgAJG6G<P=R*HQ-FSLo84*wIx5Y;_MO>~ z4E9h)Ak&d83d6RO#^W^jRcnV98H_e)l+A&y2Aau;pvM|Yq0Er-2l6Tn4#_kd2Q51e zq@ix7()3JinZi)phC{QB3Tehet=JYBd{$z!k9cDz$fi_=aLc5A07!=}V+(<{puvhc zd-<15ZF6=>*GGcA{!PU*K>xnj7)V2XS!dT${_}q)vvvU>lvQ?Snr(;t^e(gkPX)Rj&xYYt%J@S?G7Wo(7 zwo`_F$hXaJHN?{`T8NnZG=s@H+kf>acn&5?rpLt-Am=c7GLLh)%XZd!D!Il4at>9Y ziJ!b^IHkZ0Y0OXXAN-*((zY5pl1o>oVPt6rxIui226>f(kD=TCrZEefG5>eiKvNU^ zu@%V&6YBQc<=yA>-3;~=-VU+(oMqa6t?c=_TR*9afIz@y@+^>tgmCy-QVn#v12Z>6 zF0ctf1As5ViTgUA9;zAJ_zhsIz?fWV@!1w%y!7Qzp8s+4(kYZ=2$KST_mrbK&H{L!Z9880+zTMKvM?9>2 zu>8t8aOOuP$MBb8-An!hsNIvr`WiD(+YcJHQDE6kbc^D^8(Ym~99V?{4;sdNQiS2& zcWQzx0O=3hN+EL(Tq`K8$}*%?U}nU8D@$zbvT>lot9_)_I%s1}vwK@a`aSHHZxy|E zU;{JvJS`v{T4RcVKxDMTpc-pN!#gdT|C+Ct$Xio6fIpouSjwx=@fxUexBwPBPLF&4Y{u&?#Z zhsOitI?4lLSIje{exh#uKhR4OX5yy!U5ShLr|?0yIwwxfI$MS8U0^@v#d=O0WkF0T zjlyT=dTmoum22j$PnxVHP)KoLbGh#~h|OsvJ%YOUD~^Tj z^=gFB83;PY9Xy$|TxPYHxKI@w-RE_f=s&NsGWOr^eIIvcynSfb7Q(dDo;}ZI>Z$Abr$EF{(l7~wbfeGboLq1kF*Z0IJ78EoIHO+?{~ih z@y5L5(dTdl(7-|1%wW;R?wn}-co+To;}d@DF7@l322CP28JN0+~O?LR8 z{GWFndoVoT!ITEgkwmqZ3`3LDzAx)P#TG8IeIBfQG~SLZ9jgeg_Zn;d%ZRRCoUHbm z8+KU6gaia_`>k8Ml*%S=WY31R_kxDa9s-#s=~ z8Hqt60rI>nW*nkDy*S{C#|G{_g6Rvm+JW9T5w-8_jl9;!7ix3khQB2LfXTleel~f8 ze+OY<&<%z~69-}DguF~PB<&=G+8dwbG}O^{#+Q9_3 zuHzQ4e~~~0LO(5srd*9NrX_9M?j&EO4++x#fO%aAnciR!!3KbWIUv}m)eUwdzeEta zToBTGueYl47>sas>(8i$deAp>L(W&wr*;6uUhXNrRc?Ev2RCQdc4}-q!`sc#?#~pi zlKg;K(irf};J5IQj}IJ*0PJ2pYyG^9MMmXYTZpg z(Q1bp+J;x=Za__A-(flYK`g6&akRP~F#^DBq3yQm%KZEGHQ-9hPTQsegp1t|rgb(d z{ff~RHQ%hy&7kK9LeOe(NNXKemtU!ggi%|jv2zo48EP?+tkPiD4or|=tpcjWO@UP6!Y)kTSr~EkxxO7 zln(oh`NXwniD`zQRwmhySMh%)l|bzbqsTsu!622xCROm+`B8y#A7KewweH_wg#!yOAm<4rc; z8A`+HFsvcFzRCvCz6F^Sp>(Vx28{t-PAT|7F(4?u?JiWzE?)?R0ynBfa0r|aQWB9~ z(`|{9qgV6GvNk{ltJ!~D$NsUhH^Llpl%3Wq>>E_18SHRcW+Yy_Sst~+Td)24mUJet ze4v3ivI!CFJ0`7L%{p59o@?|yBZYXo(Ei`SEN1qNXKG85#fk?T*FMl*x&8i9ie4zo z5RU;f^7&wED!IZ@qfbV1$p)8K(6RLpg1+GEMv2Fr(uo6z7q1+W=}*1tm8HS0NPwE5 zN8x$qj=bL_^5ruI_T3i0N+8iK#)xw`{I1OaXPXib*93?DuKC*2%XA7EPn#S&*U+=N zZ!&ls7zMcWu-z>5uDgM~X6&KL<_oEtyl3eiJIAr(rb+meqYv>vJ?Q(w&&;i%Mzh%J z=H!`;PXLs=tBgp9LE^Thbec*>X4ajOXQ)f&N&p^mHTsAJFq#L4cFrnBt#=S zEB2esPoMzE_#*(Q8DN4{yPI&+&`6Fk5 z8va@g`_W~d4L}vn^C1JAyQHvIaQL{!{1H~-)n!)~ciqEw4j^8URODSy^6zcaEO*ql z={loLL)L<%fUbmk@ex7`va-5ZRUDGB^Mogfe| z)RVsMTC!$rOXio;U^itD*@=l7a$_6p^mK_$woVY;sMN#ulwVW$=p4uZl+M2!FiH~; z6hgcn)4Q*k2y((R3CR_TB@f(@Xte+fU&9D3oEQMk@gWx(WhOc5ZwzW0EVe(?zLB}O zbpMdnT&>J$&;TLCvW;#5ieL-eo>5T7B7j{Smw6NngzDD8Xn+BUwLGd!~KY(H9OGsQ3mM2}oY-mu8ta55XZ2c0#-=DU3@f6g?`Tl>!j#31$5gI zoXd9_puqk3zHmEO2nBw4Fd0{Xs?k7>@xkLd0}dOPMza!Ttw>Nq7G+2_%{GAz&(^uF zmm$XF;GU$MZ}4iQ*Un7|0Ao3@*#X$N8=q}%y3U-xHx%On$iM^aupkf`0PvmxSZP4c zQvV(4t33h0WjLS-0ml1*p@0ez!*bIZ0vr!0zfjriR8QZc>mZAS2@ua9{n(8>V?!O~fZv zjebluPXvJbtP2L&foN;u;yj*3WPyoA2A6Ttx8|EgfTWSEw@0yi%sn8+hoPE$H$k;A z4R|PHy*Uq%-I-$m1n*j=A=5b}3N~_s{JUxoq$*gF;OE~UUQ?^PP1IaC8cX#m-~L0M_} zo6{UhOaOCS$G^Z-t#<_09tfZiHYa86dM}*$ap?7)E!n;VO+_{NswlXE-HQjGEr8Rp z2;H&_m8wJNu)M5zSU47<yYB^nvpJj~8rVq<+o3jC;K7<{nBQ@Qw&z%{WvxH# z0-wruGvGD*3mhYaSQAp>=q})`CG4y}fbEE%vtLsPxBDfoQI2^0a7a!a^q@gcq+YGw z;}>JXPdAYoOF~mlgkp2%b|Ys3jfS`WHJ0*74pF;ukZ~#5_DxI6!*nhq3s7%#sL{Ya^Yg9OwdOw^@s~oeaT36k;faDK zN!SygukO3v~yu z3!0W@AC4uupu41qP!p~}J+Ibj>z>aH>oCkm0PF(M?UP;b|K11FeaIC}*mN%&^HMe(>*Xw%{HKy?iTF&*FmZI9vBipTd?W1qJmZGTD zy{~ka4eM`?8d^M6-#l(qy_USnoR!?n4E1EMe)}9tZ#C^WPWv-6?8f>4GzDnR+rvpJ z=Df&q?mWbzG zbHro5V!2<9yuPy2_(@o;x*ybn9(s$$l?9D}qb@}Z4D3d~;-OvYQI9A$i(%;Jis0f- ztXFPthn4rq7e;12J}0YD!@S-}9%@p4U5mZxgjBxP4W6#PeBCW@$guAt_U1Dl+I0WX z@N^H6F=2BCWy0RXfS7O6W5;GuAa-UDTJqs#7UkJKJ&zbLH(lqsgk8{P-6Y`mIovig z#ypTB-4mU3*xz(C?{4r`8}PLwX$r#y=4&F#3yG!kuDeZFG(v7Z@mz(zYoB3Q`vFV1r+rYFMrKq`l?*#UZ40Zh{$~$IU7F;A%og5!J~8HK zgq{YVDK{TM>;hLH(cjGaENi;&&rkGZ-?|cp!ElBkyY=BGkmXFk@+08h(@EF{q6z<& zO23C_5)xXeaTBzJ9*4J4D%%s!Ngt>;wUd@e((X~@@Dy$ca}cU%eey6BR_ zFe3r`+QdWy22rqJh7?Tb0loPaa?cmC{yoqJ`#A1En}?d{$+H`seNFI6^eo6tNRAv* zY8H7hD~w@~G$WJ+#6}Jq;2esS8}W4@4;_4l1}7;%#aLvoF*p~CzMwyHsI&D@N)jJM z<%1X12H7;*V?5{r&!9UJ^RED%u7*s|t!F71Ee9S=!^Co+k#guY3T6)t!(3(~^v~8t z3UZqo98QBT@NoCo&{P`c1krF45tc4PB9B9beDo$6I7$Qg3u_oFhdQ#Mka|)n5p(B} zK@;1cMHlq@`0JYumkoh%iC)ymN2B20U$Cf4G-!$vds%~iLk!en(I-z`eyTuU!oqe^ zkguia35|gSYoKJK4`UHGILaAWud7?abKbU&pee(_V9RLCGnq}qX03yn>byH@0x)?` z`f^f=Bz(f&JJmfJg?nJe{i#RKTeTr zd`tm3p++vs`g<5iIRM;VWDr3_DtVwLEclw-$W7MIT)qX;YGy5rz&f2l#|gvBSXh=6 z;Uz^4xL{6ers=U*=4~)r2i(E8)oD;kGQ>AIH0Cn&H0Pl41o#-ca*<{jqBiQnLR{z* zWEy5hX4EEw^u!~gu&Cn{aHV43xG_d70}G`eAw94WIqENpTVDYrQsFE4^*IHM4tl|Z z0O9*}u$`JGgS1zZH1sL9)ZrF3m-yuw8!4APwR!RCg$|KQL8xf==6HxiK2)YN)( z;Ke)$n+VHQ8=BUkJ8l@AFF-N)=q?3F#s}||L5pZeItLcQ3mTHaFLRjJFT>8ZBG$8? zf0V(F#zXrGVC{OYx$N1#UDk^-Lmy7n7Ct&MAJN*3g?)ek3hs`b3d}!vrI+D3+2Frk zI}v@FWpHczx7m9cKiF@>>M$SoKqCLlh-h$Hdx~{D{{2|vJ^A*5CtsdlFMdC%+wgbZ zcyn!PS1{T9)5mUi&dAW-?_|_Tx1B8k)gq?{t;zGLHc>~uOXWo$Cl_Xp!0OyrZSxla|FGmfV$q>t3vtVwc@eHmO} z|91G~HZR+|hmX5m`bEIl-kNZT%mJdLsM~zA?7rDCgX)d1=nGRt%Tjde^3qHqq)(h!-eZve}U=Xu`aj(MqN~ z6WHz)&p8=;=3kGoW#IR%?RXe0><%^Gn&@u9(;lBVsl4dA&FV^QfY@ahV|x^iJi;4p za<@%`VWVLd8Ozcg@>122Wa2#`Hll{`&a@jH0I6FgUU2@cFx?AOcXpD(GQA!w5LLy* zTD--udwbs>cHdI(SnCO@e`~VyfBWZJ^0fcJtnzMqA;Ut=v-&2?{BqY0OU4KDI`A8O z*S6zbZC3hCrM}SmgooNU4L5boy|1+H;=OMrruQ?CF*^5>8VFk=C{hBunIzq*-3zi! z3B*>25DN?A4h;0vhZ=5Gns|hf$MNN?tMzvhClq$b?5~xyH!%3q>X%M;BMP1 zIw2wVq#HTCq+h_gP=AsPJhZfrtu9|>XO0B&pDAn7wAz_UhSJrq$27c*nm(G@QE$85 za5#JUIlHG@x*ThS($0K z7YxrQEOA7&^yYU4C=c5>c(=#SSukoBaYl$*ZF~HE%kKXzhII~J{HK~ydDr>o{D^zh zmo!sP(3P25?;R=DQGN$DwU@km5|K#wSWvrj_^+iH+r7kGry7Zd-qKh5a^dak!Y$9* z0t3gW245d8KAHNhDG<87<1uyjKK+%ix>;E13Ufc1i+s@YAz4EJlOiT%;Z1C0nscKU zF`%QKo*DVJb{EHAY`tx1R4L7I-`lZ`+ERZyGHtTn*`|9cAnPohxcwb=UE;l6zsWld ztCwp7YSxG`R5{_D6?72z@zS97-pI+kY~PSW|9!`VAHQwPx|`+Uv+B+Ph^e}yGMJKq zjz0ji)e^$)IJUc1_hs0=ql_?RtuFCY+=x6i%hBifSZa<#%f1Mg z+1>p&Ygv|!2w2FwdV@b>94wzMA#+w=tACsd+i}z`Jtxtg<@Asp*Le>=_hZ$t`90D> z4W+Tw*R0%YzyZHu`{T9fo~l*vJd9H9w_3vJXw4uHcd>c1Wr?_hnNo`hqq5Lhl6GDuhI}A0=a1NNKG4f*_|~D zDO51|-zedo@}6>w5;}64FZsao!<8T$u&U;%l8CJOy{uW>6&zGWfU1UKu#P7M;qcH& z|2wX+1ue|%)AOCigSFw-Ur*FTvUXb>?s8tu92)-^wd9{U`K4LfGv)Glt}U|=eDF1~ z#_ZYxB4SD$Eh>Q@z9If2`PWyARN*^^*E2O9^x2H_+6g;GLEWLh1ktAuF#n-P{#2d3lY6K3ZiyCVx{ zH{ZH<+^*{4?p3#3y*4W2k9XzonWz{Enw~Me)FHB+=Y$8muCL>EFl|2);oN6-hl?_q zSLV6lfzz!UiZUH6=ZTW+;diJ)ey#PK!k()V)N)7P+Wbig-+ssqvupoR8y$OO=Nmd2 zx<_>IjdtXce`W2zw;;W5pnrxnPJm5gfVRY1A)5>k!XK+a*ps{)ez8tn6WXRSIlC-)q*J(hg)w4l- zA+VgRmtrQakDLwuUcVtdzr{|F9v=98aDnr|M}m5l@%E9$xMTUrqn}69Z9ZHrEHAEV z2sKF8oT<&t6j5IE@9^!INL8PDd-;VrFQ?#qRuOH3<>s%wjq88bsKi5IU)N+m(Yg zc1l`V$8sFPC(%5Ei_#=p5%!pcC9!Q=0gOJrEJ`ZmsysjRl!s^m^nFw?UT*~tp|TwO zIMdm?$T>dNR6m`b@>7UK7nTSnZ&AWbpVNlyjy4Z~jvn*4ShF?guM_5L z#(Z0Xy0`y(B4ZQ>_JSUY@MeeKIYzRmwkze{jXBkt<;%thq#NEm7@0->2#ihZR zgkKh)tja8FR`vBPNO&*VTfzSX(DLs%Z)>-Drv68k=&|VU(0R|@71@?q+Ftz%5qI=T z-VwTo$=NmKM^^luKgT<3ytakv_kqN;o%#fIAqj1p`9g8L<#5P2SJ-*Zig9wA)|ldU9r zk{Ix$_2L&ln<|`0vDUF&b+{(t@nF5M_04EZa3Qb^ZfU#vmTqS{Jodz9Ub@+h=2~CK z`JMIpB+#OIPV_r_ojxhxi=N{n{KGo$lBPd?`ibsTDbMD_q%PSDt?AUA(tnZ2S+z zhYL=a7WivLI6@hj4GtXzXK4LJYNx<1VYCe9!iN=hf!!)rt>S_EPav~&qCqA+Nh*5K zTt&TIV!+C#;Rr^AP(Y(2<9${ow=YwDmX7!xS9xB#TWY2KrUIKqGW#lFTtMr z)H?B;PsQPEJ}8vWGH!)&r~}&ror_DLA5LH`fLnil;SxaJ{{lq9NF`O?XYrz>@uzj;IcnO+;6}###ZRJZsnuV?e z7?&$_WtYF|FLTC5m$Jn^ndL0$rCXmodo9bZwp@$T4NXbmiQMSWesm56w!x@(xlRow zlR!ZTv8PVtj@5s-a~W7}xpaDqOvECI64hd^o{Y;cOXNZ|qp(<+_=^>aB}J8X3Dbim zu@#Wb73DiL5OxV7ECOnoi;RmvtmT%M|AW{qgW$SAL`PV>R-DrzS*d|?`oV!3NNh<( zz81pb!gh0^@y+5yiiETD&}^I!Gltw+yK~85_l0OvQ0UES0T1- z273_4>iFerxT0V-I7cpWNfEiQAqi^W6d2|Pc6W}iu z%)xRVH@+^#RbLs2@{$x$Bx^p?mrD`w0sy=q#WleYURoe2@{{nW$xDLdf|TUalBZ1@ z&*pzgDyBUx(IoNsaG2rKrd_`ukBVg2;g{l4c!*?C*$PQk%2KQGk{7(}{hGW;?CvGv zOy=~llXzb9*{ubVTOi4fFVhWWD~^OrpD=_wMxT@bCa)R5hwNHs?)R~cO&!)c-;6xN zcc_g|H!i4<6v<#4m3@B@Dv1xQ%+6ntAmC%P_=l~HS>4FfAuWOg>QI~W-V*UDEf7Bq ziUNS0`yp|=YqnFtKBLYhwDqOjGFwVHb&n*FCk$Bv#cAV8`-fe*dR9Axs}^j2Eb#Bg zloCNdHu@!%ASc@;%h_t+`2&bJwlJCswx^W)=+G>Ll#C4;|9RTG9}+GHab<81Vruww zO_HqKjtA)hSGqO}QXW^V=3+C{!rc*K%PyfaR^qP(P6P+JY0JnvUk<UA75+*{78zLc%wCW4m|N(d$WdbL)7yy zz@1zFepLfKsDa()7{S<|PCn_mmvn13Y+^M2+TPWJxRHS>uYISFJ!po{IM$kOKWSP$ zIaJv=PNXic-QGR1)ad@I27Asba-qerygystrRx{3PW`h_`Fh?!eI$e|d ztR{r`Byo=@YTBRGEWH0EHE`*)9}gT(0moey`Zt5lKL_nj6j|uD^uMoI-6h<+ROW(^ zL?eXZzIw7RBt#}zEf@0op=*{vf!HfNPIFEaBts6uk)rf#R*v+w#HSK4{JV;T&tGY< zA||DF$){G`l6&3k=%@EpksFfDI{&tNTAW3w{LS^VO?Eo$S=E2#RTl`m35`z7>1ALR zv3z(`bS1^oE9GWXAyGt(NNwU)F(`AsbX6W8#$(fZ+Po`_cyM4!b zwT)L1=~1x`mM&M0w{DxSh~};J{1rKMHGbv7m&(qrICM< zf?YUeQ96-WjaWmuMpj4l#6ai@abQVVNs91aoiK?vdwy+AG_Qg)EkHJaYZ2uSO(a{{ zU=oE=iV(ZX$^j-&4keSZN3lXsOR*Fxdtd&5=?d`vs~vEbI1V)WC`r5|&`ZkHVV*dU#glw246 z=aYv2Q0D!Yg```=9Ar^db0P$xNPyC$*cC)6fFFH`~*GqLmv-@~*p zFFxqU?1s;qMpDz$jFV&sH|=bc&vYMn#>2-)^^1a*M2UQOyi5|rg|eE#Aqeqqx;Rl6 zAJQzMbwMh?a88OSpdTCtfW3{@=kFvO%IeHC$Z~9oCry<0CDc}^& zA3s2K>$vHRe(;*5YEBorB&GQW4y+e1=XAir)S?XpSY9J6^B2Ty3Aw!sQtwg5;zD;$ zK;tST`k#p+l@D~nV*#txwhy?{*oJ2K1(tyZP@ZjdHJ-Dud!{TJ0AI^4u<#Z+Xpb!g zn-@}^L}7Q$t*w0OVW2Tv(|92N@3gZ6J=w|r5|H?5$^V*o|1Ey9%`UlR2Bq)e9eoN~ z%zpRCTIiROT_lP7y}T-Gx)PPV10h*w?#!@zW?`|d+~T`LetyecE@L}fx>l0m@)QG- z6iqu++I(F1_cYVNz*@E1`r*jt+11P2qvlj+!BH}J!Va?v%CD>C3lz8?1(}F2-hHi7O zxcmGFZ{{$z^N+4)M`+w7W~d9!K8Z8%{@WHe8y9z{O6l^q-BdMcW+k-z!gE7eiHL?eMzUYdhzjuN)#7TT|=bCWuxJQA^%Ke%$%(LqKi#y3fq+*pI{DXpDPu z$)42@5$Ig!vfg9ucIM9Hruc8&)5b1`ZQWe9W%nt` zu`i#T-g0f%h14sb*LN<=9)Ix?Z#%W~*s&ax&G*YG_s`7TGx`}|JNHnhiMtx?*L}UD z=aLYHdff6!(!06+)YG3=@;k2{4=+Q1|Capx(XkH)BR2Z}{q=R_j@x0LR_l4guO7p$ zNtfcAzbY%4aL?cRT2OHfg->HQ>4W@}_r9U1Ix*8yfEyIWMm(Qde6Y{z%5JpZi1m<+ z9y;;u0h<{>T^nz*ov=1YGcDHpe<5isY$Q3rbR+jmvq`OJrUYEKijtLpV}ID*o<=n= z-Gd7wY|p5Re{FZXqj+-Vv&SiSCq#8C+Qvnwkg0cGRrZl;(HIW1nK4AlCo(Y3+3F*s zW-W+Ebv6go3DKsV6$wxWdHs=aQ+C(F{iK-xopmhi_6psY-G!eWA!)gi_1P+giCOkk z)(<>yQJVdJ{k1EiN}IL)6O92WJK&pw+hgh_zw0=rb;Q$)R~mzwOUoX2HVw8{ZyVg6 zZJoT}+g_2?w>j)t@ReZ)^!bbpN#`2k*IRDxn^?W+OYw};?vBKXW-!KmTgUTg*Cqn< zQT&(ju_|I0t0650ciLfgS^9NxI<`?IYm1>a^)XPs-FTn1(M;2uF{?zm9`I6B-s?5w0-)uTVfj2xhchKhC5TyE3wR!nqsJzZ>>UYZ~W~MI`H@X zJw}0>h>Tj|KF5rg%=QrksS+A)b^}*H0P0*jAYd`Ftz@lq3 z#xeIeqF|Xka z`6+w+V>=90%17>pwgiwKCxz+4Berv~v_{aY?U22J!uw-wI zDruqb=8Gz$9otYo&B~Rftq8xaLE}m`v-{Mp@;LlxQq?|Ou%Xwny+dED4-l148|^B{ z{d9{)3mB?mS%V1YhvS2%1olVe)ozs;_?%`?i{XUWnZvX$KFjd@!4dh_2n!zYTkK1L zweGQLuD>@c@#pv!r6nTU1fIy1Qx=&uFQ~ssD?6SKJRFTs6*Wf3Ux?7&Z=b6p&l=Cf zv(xJi_c4v5k44XmYH;d_*C(tl(7b-9KCJM1eY&>Pd&bGec8SZ3^& zqkHb@VO&!HmWft;K zTCukz4YP%pAy3{>))0Q{|-SGYTMHpbZP~k#$ zv|A@;Pl(8Ubo2ejJ4nZH{ULpWhI@%B7f^0lVAr5g{xsdLB*NdGyR1B<9 zW~N#l!|H^hEZQBI%Ho&dYy+M~@28n>z= z8z;)x<@H?*r$*wGoAHtBBO$2yj{P^uo2~~O)PYL%;#=F!hb4=YGBiI5;}XNP4gruY zWx(3ku|UIX3S#j?&asd^(>nxZ4BM*>==pdi>XJ-i;gN=P(O2vw;=3-dsi>@}!M)M_ zVpP(0U^gw5Um%3c1{KF8cT4v}aCJgUEf;%%EGmQHj(Z zwiPir+dH4@>e9Y{@8`{p$s@Ak6>o7{mQ?2Cg39ZSLW2)luxzmD82Mge=AtrhsyWD~d#CYCHfj7%m4Xl}gx*!-;YyuaP z+PTqhT4C%b3k$s>MROJv_?-Xx(1={5;J-laO_Lk4m{p5y%MDZiq?eiVZca82Mq1ewhnIk|xJKapR_Em{ru|)~dxP5C7M3ns9R!xqPtuJ?wAayQa6`nS zw>evRV*4lmgxjlkz?$ZWcEPV8_I=H^@@avqbrUh@=D zK4TD5!*H@0(?J3|8nox`ZuVCA*D4P^p%G^nlI; z-@>oks&U7ySGcHNG&#GZIuMYnMGjBch>}sdeT?Yb1Ey0duBBP%rN23>fo@X*lSGhP z_e;<2<2I0&D*}{A_43Qh!y?!2XQygFZX||%3;llgDRt#5gX|54v1Q540!Gs#OZTH^ zE)2dKSDCpB92Ny;DUleR)QqMo^`ox{U=ZAoCB)E8Y8du3I__&q^jTD%92{%>yuV)Y z?;K#^v@$6|b*ekwifxVRd*rY=jcPqeeZUNx2BLfghQ4(B3SbCDgl4N;rf4QLRvXx0 zhng374#N~Lq$8AO!IPv#I;Jolg$U!yz^*iv3!%a=D!rla^+d?9UuJb;^WB(2mTe0C zo!UGt4or3&r1T4{Jj@op4B|M#7aa35m0+8fbRV?}7d+|27Uk@xo07ofDVkFS*mpnB z*mqfA(g-q61R4{zZi*R%{{oSx`Nte*2Iwl}6n&V2AW0Q?_oSd-0!sqZz69u7VuBjK zXG;YdH`1({K{koX;Kp^I)G88N6vRzI)hP9RJhDt_wg~cbq*G|9RS)m&2@#w1i&p=z z$2Y#RTK!Vl6&@4Q=GmiR{SSxxXL6-Z&7_I!Nn@UkN~L(CJu;CJ%yN^K|NV=xf*-Y_ zS?TXy&g}=bAjV@RMZeEalbS`ra#cJe&A#6;ger8U3QM?UNBae@Xur3SRCO!MNb-iv zU&^}g^c6bw^8m*F{fziJAyp3ZD}2TNpYY)@I8d&v`;X!I?mp=R$j^GYHB_0Em}tK~ zC9=n0I64B-J>UEi2qqI$o*lwfv=v=7Lf3a0w`srcu;`>VGRUJM*Li~5e@d@GmHS%N z9U8My0TR_JPk(84jRBL{PW0=KHaNfcm4p8IPZ`&u3Sf0Q^ob}nO3yx1-x@upS>QlY zxx9bpIWP1c5_=~qnSH0%z67~-oVF*4yb%pviAuk5aD1J$v!l>q5#*{+1-CN8Qj@wk zLZ*}zK9n_I^U*P>qu8-Jt3q_a{jr=noY}ptferR^H{D%Wp5(apwV#Blb}1S@zTU~Bgv$LP+ z@o-yxCbKRT6IhaUXk6q)st)RQITWk%Z6x{O(IZrr_Z0AK8LYBpsEES~m|zN0P{D{8 zDuG$!=vtr$wJvIdV_^Bn?QB*Q)%t$X%aLKniti<&kP}S6Z-;{lndOe8y+vWiaW=y2 zVdDeazm!1pySI7X+9pVX`n?Rx&%C={I@a>n*+b~f0&?`uUuTb`LYuwA^W`p$ZF6l^ zVe$=a37qtv300{Y-aux0>Pebf`T$$BN`o1m)~ZXGK_rnlN7a5^CE68z0?2fOFhltw zMeqsV#u)N1#+eg{L-An$2(8$?=8#(CwYbc1W2wR`!IgaGD)(|*yCFYWbXeXQYOtk$ z^WCiv#n)p%j^3TN1XwU%)F;fPT0^4{5s@U7jh_7|J*QA;eGBJ$;=$?=*0El6`}5Cn zXU`r9J~z>_UAE}9E%ofFQ|n3t$5IsAaNo~o@=V{l5AXW*xs|bge>v1J#GsQk%XK$B zpR!)&Ufq{Bf9v^Y;p$}PsAGHg=SE70Bab%(jf#9JWe4?W-}!Z1UNjdXNy;qu{sj$* zUB4}8?#^i5UR?f8z{ zfe#9P8`+iz|MKqaU{Uyvh><&OBXaAmY;|2*RMqW`V_daiCy~5o0FAVX-uJNlt;ewP z-<1=U?tgN{&Ny+HLbU27(4vt}brj;7ffl1uUx?CV99YuFq+mgw<4Q-C%AL({;R4Me zAkR#tB~M_DRXHuvaBN|4jL?h&bQgoKAs8q}y7Lg&i92Z0m=KuC^kFG&#(`9#&@6Az zT|5X+q_32Moch6(7)H>P(i*F@9#>iivs@z9QHS&`ErA(@;akYCkP7r)64U_>R_mEB zK+oVo)3Ifh4G(DkQjOK~>h}vnUeo{KtNdhO8!p3JEHG;nP{x%jD4zcv1l#Flk~$&( z=vM<^*Bj8+_ea?cja7;h*e{N89omoij-PHbhBG_188OaY7&>!#3Wj{8!><`mUH#NA zrIL$KcFd$ZK?a?s2XO?ERf>Srh#T(9u(D2H@eWVPG{bWrSxZ62B%!N2&;}2Vjsv;q zl$9eQ`*E5bq0@w=bkr)=S~6kYN+*ucq(kYb#|}2qR?xr>dO)Ec= z=F@(;{r0I2|FS&=0vnR*vR5a%nPEzz+oTMt{u6q=1KMmD)Lo%F^XR4}NyTZ$T;~*y5A_hl>8dIa%P zutenj0zw{rwm!1fdY0Xu2dsEi{4$tip-3y;ew;mQo(d>e1>-iQs(#6l&DRc zhpE3!ET@5;-VvAxUz1)@@O(mJrv5;y!fO>GtzVkIX%;6d{45Jg%5oISP;da)i~d0w>sp)Hpco zr%To{o$1Vl8YfS!c<(f2^W{~0zfQi{7y8!0wSe$9igb=%#Cbqc(Yc>h(#0 zFmOCSvSJ3(bJ!tIU=l2_8H%9NL@$0ZOq-YKo{57MX0lXyuu)(V^P9c(pm}$I zGi|VB1uzit!(>rmO{nBc!w>x#dtlA(+yBScnZHB%{eS$Pea5WzeHu$bXvn_Q%-Hv_ zuW78=Vv>+lGh-Y3ppaBU5=Ci8rLiwbk|e1&5~EZSrPX&n|G@Y4+g#UuUH1>?+-I)y zI?wZXto?j9j`J0n&Hdz!KK`uTcKV{2SF(?-wo#7`w_A_oBbClq7fYy2&-7XK30J*InR z@(67wx#vkK$FL^z&m@OT&kQ{v;?!Fk2r@5S$o#{%CMVOhq1IPFo0GZL(PqBRIj8HiK{@g<4S(V-XEUr#n~E9vt~326g-AbjTZ7~* zi)1a!noL6%2Vs29d8K_F9+1XepeFSUu_L>YrZdPX+-tIXl)wDi^ytoXA zJ9Io?WIkj;Evvf1s37gi?gzFIvjPqkYnnYRJp6VDzlG#ulg9IAz&Ap!d>9 z9W|p5`_vA7eDLGp{POpLEo5!F59WE4=ujC+%gUz_L=*x(v z2E!;-hY_C@9_)2>$<7qlA) zcd@c9O>MdTqU@K~CTY8$XvHHELo8h`_2yl)ch`J+l>GXf*WF7+YK8s5qiRhq;zqGw zh1-X65`+(`Ro|WYT~YkjggM)P;<0f`bkM!Tx&G4@sg13Sj;JRedW9vgkM3KB%UD)1 zLwjh>`d12!}7iHyP-}$~TNB9F&(;yg!+d;xiD^ zV(uRo)+M}j4Q+8G_QeA?1Uf-sgFYv4`W>~;;s<^5e)coP{v&ViM0Hn-tM#Y1zxjA8 z`~>kH4r`mceLSvmeNsGm>sag*L_v8>J}OuF3MDdE zqM3Y2YNt~b=X_9u<{Tzn>T4K6lYT}+`9f3`Ros@Z*@cZ3^MK?Bb^mNoOztG8Hw31T z>asM(&s@eTr;wZ!!$l#uT%H zn(;_}cq*>7$=5%+?2d6@%oVH4XTu)W+TXd$MeROw#&hyp>~M$UsKd-mndO2QF-ha ztV)~QmG!|fo*Zd!?@(iRCTNC7*4zD|>J^SrL+CxFOxDlwlRMzwU3NIdqs~p*#c?~z zHd!w6!+yPIUE=oXg8Cd(?NC8g#x@`CHbzua-`G~C>eJwFPvp<-@Jv_j%O@W7F1=Kf z`qpgU#)L=WU*!~f?zx&*`>rc*JP%Dgr}Wa@{`lFXDfYwEm-q4>exG^#wO9Ua+_ugX zQ_62kkJOZ({WZz`u?VHq2Zwf?OaA`)>A4gKvHizBb^Sb%Q8r$>^gDSfX2-d;p2U-T z{|;Z#QvN9{-S+42lY19RlQT0G4(I9xf;yrN-yL6OR{ZX-Eaus$XRL7hf6RhAQzo*A za`gz)Fx7Id4V@@a>ag2f#rzp8M{bC5O24rdyXsg#JhBQltRdWrQ4h1WX|+~trj*3D zg~BbOdvrhWC{%wcHjm#gKc+6}r86gafo!cM6t~$@<%TIt^Fj@0=dvwlI7Gu=*#^mU zjQI#dj6MZ27Kmd*W*WteHEp+Fp_F(~*u+Y5k1n(prdjnw*M}0IDij6hG~R<4qi zIIfhLD0g9sBTELsBF7+@x;B{N4}M_?8Kitb2oe`DoK2wOO7$#`tiL#JRI6X_7GG(H zTrEPdl2q|<{r#@j68Po5chcAV5g+4UcFfe&80ic>{wn=Jxgy%wQEu`h}8Co9NWBFxfZWzjR`ej{S% zp&r^rA@B43Hu7?uvNwuK%WV0wr|it@pW%x3#P8V%kiz^>e^fgnK-(Vf)=Ur0p)ZDt z<&(fxWwnJKLLKoaB-Cn!lw(O+!yMqlt-AI5+_~MTI>La=3O#!t*IIFX%&s%69+Nr& z7JEp~kw_6oDp$?RJOmKNb)g7_^;*1FYxnk1a?#E_h;&qR^XqA#vhtM*e%{|&(e5kE zL){vkFUm*-J`;R9w2r1%6ZML!{dk?8cw#|wDs@cIStU;qjwnCn1Bueiq`ls^rGj(Clj>-&p zq>zRz_9tLtSJAunwd3Phh`?c5Ns4l%UQ^d&ojs+GH^-}AJ|q^d8%#@brEhhN`wXPN z2->&SP~1L{kdSiOLTa(2TYmC=_UyPMuCjueEv;5m(us3%C}m$Y4-zQztdq z_J%83_%nrl=XL)PAr|3Rnkj>}Z zx?Gw)aEEr9Z_R)TV}O|3NnU4$Pw0z9%e@meuEQ~Z4z-#~DfTcWe7RpJ)Ge$ny!-O6jF4=*&09nE;r#&l ziMBr7>WY|EHsmX7%T}v~kX9~beV6qGW;9ld5nOe-_aD;PE#>3KyHg6gDkc9edz`FF zc>tDg>OR%vlKn|c$4^<+H0#nP4=yAsdfj^@b*1Kpm|EMs_Xidf$Jv|Vb`&gelk`H{ z<%Rak1H&^m>;emperTcs4KBN$h1a9v1L$2i<%ofe&dd4sWIBJ_!T+SkfWo^@&X+!G z(%=APizT|157hJQckgl9ztXf=RSSz{%Ue>U16Me25A?ak3r1DmnKsMk)EyfH;m9l^ zK!Q6^m3Xu0EKpw#N7;MieQ@!`^~jVxE6}r{DN&bUxvEQ19CDhQht9CyH5J8Pw!#QV zF=Bau(yjDeT_KIEpuLzWoYS7SRty_dx=sC@6Q#uh zgOrr&!fvW4fyIz_x=yChDrREcM^wfZz{(NJ3N^eD7j?y(Dq4D6g>idgG8N}SRm!3h z>$=pN>k2ku&2poPAuT~U9ITi7#duw5Qx(Fs*fy4~gi-0_be?_^+iQ|4v&q`rGU)T3 zq(VtlZk)HtsV(q;uSVxgejb zD9&=lz2;D10x@P!L3gP8pDbbrSG|TQ8V}uB5YeUXNko|LR{zoqhj|^PZp2g0F(uWU z&3X-~FwH+%?&cr%2(*=YK}ul`^87BT6LTnWCx!7`(Ah5SG8M)8ImIy$kwa4)n^Q^) zBXxEy%}^^Mz^Gi#$#hrQ&tWowwUVmTN^YE_^7EG(+ht=tlw)tmWV=gDh8pa=B~!#l zPk=-Ren4=o5{V))p@M}{02_hf%O;SGKsvY!DFlI&=HxR-SfL6S7pjcwLY|+K+MJcI z=|aL8fXKk;$s!_IV&|#5b-iTTLU&)1{N8Zu7ni0vt=eO_))yw}+DHM#0>m~F?CqSi zCk24>5nRz>7zrXbi>epNUMK*EFV|1PZc$joFqUjuq-IAR(N2J;#bQWojF2raU;$7P zw7W#UZB{xk6c*1`3a*6#EJ&1!atH_`f{v95lqq5|hHm?fWBaD4$Sj83V5njgRbe3% z(@23zkr07%%5V@!7z$cp$=0c0fH|~3UnUBK0JDG=Oxc*(@- z%M=2;nNFk^xtl}g|v4OGxk zH%uTMKwRWTc+-o!O{`)%aZ9K;3AJ*o+U-6Ro3#u`Hv`BpwmwN|E$B1Qa;j{<=?(QIFDM_ZBve~wbb2!wAXJ7fkO`qeiEQP0z&dUc3a*u`%T{8ju-CI?f89y@!y*O)1cR=# zTjL;X27?fy0&eCjg|H>LwPLCy$vU$K|)+Y%ZY|nsVLM{9}LoOu(QK-V^@G*f@$(t-IG>kG$+t$wuF~6g76?IZa`$_FbrFr^0wUz$5 zi=y29NEq2R)jfxkSy!v=%2A&xJbxm7a{i)jvpsq5;8KQTPZd>Gvy}LmG{wA=UJ~cY zFHx)rRZ3EUNdW-B0ED&ZV7B7OkBos@5GhpVIe_cnqeoxP>1i_CBB+@`~cju%V1yDExw5|^e36=4kl_pYwMiQC@fTKbsn1cA< zMe@OuGC8WsRVwD5h4S&CgbW67(^#R7g}Ga+%$&tG%3uRoL^wq>eh5oy(S8+34hhvr z0yIEDGMWc}-#O?vaqy_hQmd-dk^x;*Pv-n7@v66)C9QE^b!E-^dl=5wsWcCSwU$=mkM zbG65cf2zzQ)%S4E^X@55%%e`WYxH)D-#MB@6<)1=C4NA5`{WKXeuEy0t=@J+mvp1j zEmL-lBNgwHaKQ0Gi=U+0ZqCj=X>-GgX z6tYsWVQ;ncj@F(fhJuP`k=1hnhi0X>G(Dk~phM*>;w%HRB9Cy8s?TYYS;1o`eO>O> z?%z3!-{T9nRLS383(PZQY(NEP*u*dy1Pw$`U`UwPO5Qe>WC&yeSL5wZ`CYqsa3iwi zqzbeRD8Zj1<_k(}Sz@suWmSsg$Xv|*WoapvxPywMIotkzN7;+CiyckTL!mOnMU67g z!_5<3wtntaKa@IE5);_)X+A!krJy0w6_L0Sq3OH2DxUD7$GQ%^WFb7js5r3hD*Zh6 z$YCr}H|KuF@PT9VpsewwoeE%IXO4PEcgwGp)Ti@q);`=ny|!&XYjPEJukFCRb~&q6 zyfe{uGVI`aph4#!EY)2C1-mz12?hfyKcO%H*Z_3!qHm@WTnNgAfIu(=5+x>yhJZ0B z7zT|d5F{kAXi0Ibl%%9I5-KCFAtx&@ucoFTFRdsgrYJ3`q^PK-tf(%FQdd_0U+D*G zD9Y@BKz67qX=`lP)zQ(_HQc>>x1lz{(8SdUg);IkGYYLU-l<{g$S^b5X>PA&&hE1? zH?y{KvN6)J^KP-XFtWF^weM7PG(ciUEF-#d=9z!7rK-6=qR+O%Pvo*!0S+p zcWjA|t)-8Tm#>$zpR=uBWU_zYfq(%2pfKN%{MLv7xkw-9$iVo>z`O%NJ_lk_qf;wm zA_8Ng55%f!#n((9JXCxzwJG7ykwh5!P=LoF-bhk{$Km|z$ytRdQGqFOQK9Y^%ABK>1v!-sH>*zNRaI41ow-~!@a>GUcJ;;b+Oy-e&yz2m zt!Oyga=HBQ<%+z^P4^lv>}#w))7045T;b7tbF8JJsO9XAYxh1}Z@qJUc%xNr*RAWt zcdj0Fq>wnfZu+%2}+y3(Rg9rD!w_Eh~_VnGT?C%&J=<665 zUL3sFIQURFI5;pk^m6d!uc5XZ!=hUJ`*`fr2{`l6!DD%bR z!Iu-mFQYs}bi{!avi4nCXRA)8GHSZil>HSe%)8y)ZMiFthmn zfA!)0_uq@Jt}RU%ED426%fEz^3Bs4I!o}(3Ct}O5Ha?{1et7Zt!(`FQ$M-91D=Xjr zuBwf7k#0 z`zM;_|9>I3fR898hgN=9CglDR>qbIt4`-@PtG8jby89ry9lHS63o}WWX?+p5*;+qT zuIY6pc8zs7&yN_6VQrvn$X!lW5;dJd?M`EVnb`4{}B=IIb~E=h7CNmaK0lTi5ZLXQtkJ zcByYKGMr?UM`i6VedlVp|I>QZK4$*yrAt*&S7KQ2x9<>7J_Ge4R2lVs!jLoA@YXW<@Rb|9T zC;>DzUE3lzJzk1(c;?XeF|M~F?sL0PJ2KvC09=mR>0Es$&ZxJI_r&1U!u=7-W1Vtm zJ-z66#TFg?XFqo#4{H;Wd*pWq-$F^k&h@M)lsi@sUaLPlyL!9K*%$4>a=xi9#dXwjl4@3-sN^>Y{r?P%zwz0u|pivnFP_F{p+o zUk7IetnDktmLyt^g*-!s&D<^e3GA&RnLSP4*9pKvU~V)=R5lk zlS?I+Z%iitm;UjWqyzcpWh<-fk4j&Jf^DORip`W}r6w8?WOFta z92BLR4bu*MhMsJl zx_JsB4$r)HNGaxLQuTjRc7|Wk#+#I+FrYy2xy%$C8Osm!-mRQ2W0rA;l#x zRms-Xa-X=nQ!KAY)ltEIsEPJ=en-=E{bkGdF;;Iu6ZQGhHYSntL3a&mPt$HwC+ZhR zKj>rU+DBcSMt) zx1=u08y9<=k7=VJHT*fs5A>BoT@3tUQvo z8W8`d)%J&Q=YSpZ978P?)*mt>>%GHgY!yP)0FyY}24(iM=oZ;HePm6klbR!Mppk=*= zP)xj#Ch?B=!_c#Ir}f3Y$N52W=O37VFis78U7xT~Osxe>>xGkuuT(6(>p#%%HqI$)4O(^!brDzOZt_*&X59Y*~*Dgks1@3^5CyfnY#p(r8;$CqcxKtb_hn2 zqUp>5N1m&x!D+2wwz3jEdkWSVr$5Kbbxc`rZTm$?kzJn~?l;ZNeNNNgad3w8CHK+m zkDY;b8~aD{Q)VLC!X9F080;L@QTBz!Xf z^dFU>4!g=k_JICcIPnD$dEQad?X9jD%i6ngSMq36%61M zvWmd?I8Ozh=S2v(iO_j3Vc4@bA~SUVffMg9!vgte$-7DiZg`yTSMfB!XOS6;Jm}YV zqK==5k2orngbnwBr!rwyGT40-)W#UDmk-_CK^UbVG7#vLSqa-#2$K&{Y(>=BMf~q3-3mtxLi)C&{BIp8;R32k(43|wlJVi%Z@}OxX4H5Ngg^s6@pnhbr z6*3}?OgKY=+un}S5yyQVOH7;+qW~HG0Q3kMe}e%%Ny)WkA?7GZe|}ss9iL%OC=r-! z2*gU|VmDaO!CY#3y@?AijG`(3wG|o0RI%+M4^fSUaQq78#QT?s-Z9+DEY3SxM)ybY zM(c6+`6%!zzju?u`KL?AZHM?)!uOC(Wy4z5HZocG0F0>JX3&08xVEs!|G z0--7Y!>Hn_k~VlS*cixzX}3W~%Co>h1kjT~C8PiZCqdwJykaXzc?|e1b^HY#e|1X^ zn@WQGYTJ`K20yc8?Lo?qq$9P!C@K@?!Ggy7;tvQUFq@FJG29IvbOS&gB*2cUiP^R8 z*_g%nkl{Uyr29ORjaA463ZZoj7S6r#H8cP$$$Z`LTj$Viv@~hVs7(5+jxa* zWPB?hFc65{Uri*^!M9hjmk0ojfSaKMN=)R&?6KWDP~ffB{5cZ-+G^qYY`Ktu+gL4M z5#Yj^usajD7-rh=7_3_kA0&dx1mK71FdY><7l4lQj#>i-)>MZJT+Zpy5nRQYLl!^}PO3KsHm;C0t zG>fU`Bhm$MJrW{`Y+^o)`Z`uzt(6(P3RA8|*{3PX`4LtbAUXp_BtyUyqmfoPn2u%9 z!6qcab{=f6KYmXuurVekMiHB6tx-_Lbp+sR0B}aD#3wJP(Q2a07$9h`sUZU-QvyIb zrrQcy5MU6iuyZnS42f`q1ZZx_iY6X17{IC1&;hFjYXTf9EzkSqo)tQrg+vGNPe;=6 z9{fs-V}a%kRypJJr&%!%CL(nVvcapkMuz0B#w?N%ek}ApG6FJ(x@(?v+XrthNOd@h z=p^p zMY6=LWwCvH*uh!E!P#>*i?FM05+07&B_13M!d_xPLrHi-qeQeVu2fLCL4bq=w6uz2 z3;3{)V)Eu!P{L^>! bquBZN8u%IR8xw&c0ghuhud(aiW3fcO1eSaSMonc>@R0jw zM=8*s=Eu+Y>jt2(<&7`pgC9Lc z9gOYFCE!Mx5OosvD;c3QhFSq2U_QK+UhOls$BPeupP^u@q9p)p)hbGX;3WAHIf3}l z)iO;@0H=v>pcMQy%J_MjRcw#g$I96Fc{5UAG6P_mN!Z1okRh%_8M%x)w(X6@)vsLG z-zY8aE%)aPgg&dN=c;^mgqZG*V<{@Q_|=|~f@_oEnAe&oEkhw-0dkFkF)}2aFPD&E zRFbmF6Y@bv2>9Z$hE+l51fYr*q_?#GZ-Z~K3cL!yn*iVqN`o$|K#TyXTkF-H!^MyQ zg?Teg(QQ-*!l|7Q$}Dn?l>`Q%Xe0>U@51{gOxP9y8o+>tlVn%|*iIFw2MNlmGHK;O z9SJZO0Io+x+6jg%TM>S#J>}xKJ(BYCtrsCGFi)QRrw-C{3&Qvg{3t~XGPzA5DNp+b z`jZ#_EdebaffHRq`OLq+LBYuG#J><=gaGpq0dR2}6=WC&Y+WEX6OB z>~FKU+oW!jp=8oL{|?X8N?%31TMYIf!&8y|<>)T$~RyhvrNABSM%Vzw}P835i5e zO)O_>QqAu1IwSUi2EKB~apc$iT6Kq1Xs_-cBRn1&Z?OwQDyP-?IvDJe{N495_O9e- z!W5@yM+YNeu>sx0c+v;ne*4^&uJ8NJ7M}@RXD)fFChbTK*ND0SKl$jK#NO{Scb*sx z7*Lh~Oux6)*Ch$nn-kx*T6wUgm*|RVAKSND>_qD)GsKqo^OxVkYU8_%CCmTDj9b0A z60Wm(`oA-IGCQF!K0JK$-Ri)+y7pt6dbc^FgkDuxi#u%JK7(hou%aoU{+#67S&3rz zcGx!9J}E-y9GE?|37sPm=T-O2zrN?L+&PQACea`Wx}H6!e8L^HhbE%Fb_1BrK9>Vs zQ^IbtxQxJF@v+go@6JfSp~n&f)WH3B$r$T}uw?A#^?-hTSG?^);%SNTxv+74<%q-Y z1NOWFoqA`yCp4q<&8{-5jae6?L$pCv?Xg)AO~vZfZc5A^{Vpc$g8`+VHXCy-C1ww$ zi|)*ETY5O_a(EAAc*};u(p%E4#iVxH#o`v#dR2pN3hiA#}UHk*Ce_` zgmH>9d(X;;&X2uBePf)kF=FwUC-QqI^7}vJX2O?0A;{k$$Wq1^&=&Gv$m(XX#HQOS z?BtifZpc5^zG!x>!k>QG3R%r~_65E5dGY<1-^E{0d%qI*K0d|)YcjyCxQBvy)d-s{_|K@x_ z%aR1OTwV9ryFnE|tSB2E`!~EEZ3I4rXo*98_HR5B`yS~2J@BMN(8&!;5+vxtdP0c< z{lZ2l{<|fAgYN$Q$o?PU`0uIsDzeH(!2Tbar9XnUZ6=@Ga2DYSf8zdw2>$!yyKTd5 z+wZ=Ue{Wv@)pz07?XBxS7Qevx*znndY}G4+ z6|9dqjo>LVwY9rYIcjF=E9GwbcK6QTRl=t-TCCVT)9trBq_RIeLEF2xMou*OBue+M zRJSs^_*y~xUfaHeOz}`C9!^lk%2671{}W zdDn7Yj;^F7?!$DcKQGIjEwX*@A_PjXuwiw{xQwP;=`4QJWkLc{0;|NXGhnJ~Bd*X% zfikt1h~Z*C$L-3+YLlD|_OD&y8EZE6mm6C;9v+?e1-_}az@dQ6NW9hpQ;9l%j$2NG z$9uZ<)zvK)j1;<=XeWeM?~r5~Q9*%t+btV$3y*M48 zKV*D1zCfHC#JsVuK`!?3llL=+k9wraA9-Q*F8hu7FFvuvPQvTFZLB`5By}dhriy*H zD^q6g9^HtuoM^4A8nu96baj{zU zXBlEm?>jhpI5Q)&5ayf=yYWTd%g@HkX^S-uRiWY`2e10RPP_xF3_5swXffz=S4(bi z(&IZ@=5JDN`M%G_`$yxlOD)+pXUgrDp=HsX;tSQ-M_LOCPBsDMN?1hft5dX74cfr`uId6pLlw9#?XvYxkt-Pj;Q;~Q*F-|N1^EiR>OKzc0UacEq#=K^~R6|ma zmejCF9Rf?#-ElpebX#2WOj2ygLnU@>uGNpJvu+w^8t%MkkPDi!s+?|qVYC2~+Cz}Z zmJ+rg;JJSqiX<3H#oH@rSwBB3?`|s0=6!~%4xF-p(jJ|+Ev$8|O#C3;TbXDq@AWs6 zr{lSi&i|?&c(gZ6{40)@RVLal3}RtUN}}(X{-! zWq`by1@#awuC?*IA2c;DGekIN?5o)!ZQc^&ddn7ju(aTiE+lScT;TzM_AqCYCN+jc z?93A1?ryteQC?k4k)h%SVw#9&R$*4rp{j6QRmn2VUR67fk{8Kun++AMaf2qsYrLrJ+TGzWxNMBC5`D+$rH`!CVu}YDKmlh{Ks4Fecpqk~EVv^=~vbX#%t=01gtyT*W+qEuIZD=J& zN6Lr|lN!+fg0ti5zr9v~eZqrU}kN|4O zPr$nIoivmr0Facd1N^eI_NqRev_?x+KL6;qX8Uy~gOQTK*lNw8OYEffva3~57tX73 zV87GK{c%zp2*JT9eaw|}yG|NUpjd%{XY4f5urV4bA*+Q+jb6E5yooOtj(Q%I&>+57 z<2nqg?9P^fUTBMVyqYinyjD&s_x-q|&bicaTrzO^+Lb7)U=I%4yBs*r%!1^1fGkK9 z5(vq%L5z8*64dIaDUt1IUDqOY+|MghKmlevdcgei?Q;OeY{J|-YXm}W%h>4@1$)^{ zFDQ9_#n;VEq^jWgG^TC$vQM2w)|Wv637Ej=sQM^%eQry-G5)}0#Q^7q@pWkQ_V@!< zuT@4*q^Lwnk1854%6uK_E?SO*HIAM_9$aYMSBZ^9j5_(|^2H0ztU|YIz6j!+0vF-6 z`brHv^4`h(Xd8NBvnw}tM(yY1fqpB;!?nluIpz7@e=UtR`&i1>Ii(J4Ftc_7bnncx zS?O18a~&0aK}+XOL)4on$M<`(IW@izh?#C`q9^+=uDA2wb|yO&EK*MKFCqoe)>oFf zzNV8kCTW0;I2oCB%BXP-+hV==TH^I&c;~k_0=P|Z36@vYN^P;HcNzQ3oQ3vB9RJF| zW`rwU?*$P}1r&u5UP40!VEaOd{h-1EEP)=x)*PigPI0QN%Cv9SPi8?&a2WWke{bOE zp~{EBGT5rw0sL}{! z1&NvVVT{V5Jfkrd?YjA})+7M?;PIbCdD^3sivl=DA)1OvA0{;4tZmOI!mKyZIf>*+gjzvat?XdE^Giek^AG)&B$25@g zQ()rvR;a#uD2Y3e2mcp;)aF3QH~Z^?3l>k;-TzSGwvF~uZ7dEvSOw`JuvT8Qs=~=e zpcUI_QicK;$>A2!YHUpovsH8S>Ojb+Ta%V|_5UoNrc8Y%D%;}$iVsXD;Mr!&0C*2j zkUlshI|qOz`3ZSmvr4ca{$sdK$wzFk)!*d-qkTF&gzro!Ay5-)!~(MR>2%BU8*O&Q z9>&TGVctAYCh!g2+Uh5Z+s5{@J!7DF#tx+s*oe2 zTYbjO^}~DZG^vy{(M3&|0YA&B5t=f) zw}Ms_N64xSfHE4-rV}{-%)yYWyA&q?WTAjieH%<}FDJ1M@u2H8KxMEaUxDR=r0ObC z*;NqpHqKTk0H!h!_*PaZ1MJJqKC3FGeab+g_xgq+`DQENjD#Fk1wf6NA%r~J=v*r< z*rKt>y)oBo9~WRswh^b54{Yu`Rv%L`~IC8(YyN`;Zz@$Clu8_O#=4-^ETO%cuy z05K>>Jv!H+F>4C!&IhIBi67C zWKQ6!tM0-RxW?qx$d3;INiq5ejk3ye^h6m(XD3I)4oh)W8#xGyNEFYup}=dR0W>mG z*bYdvfu*KcnnI|foD^z`vv1v2TdMWy#ZIx-+c%v0v1tV~wjR-80b@gg4l3xJ#AN&=|!b~}}j|D2wON2wt zBj@wfRiO1MLI8Eil5(qsuh$gJy+0erMW?S0@zJxdH2sT#>g z&W=2gGS8JNc=4y> zF?^P=&~3qz)fA6^eW+HN2LBvUD;-v=a3!}YsIot61SZ%uZ^OOx=P{fqdMk%!nzfC` zLPi6(b%3>?EE*ZSroiSyTxKkvUjOELZHzVC>-sC+?b?(}JGA4aRL8ISr@xx9f1I8K zg*%C{Lir850AD0qln{AB)A-7(xdN#Iddr4grV#>v&sNU<@MOg z)}#^m>XYkntY%B@uoSmB**0)9)1L{$tqy-U3)(l9Jye@D)RYS+u&{$blL((l06>GH zrWfm8Z~k?RIbzTm#Ye-*tO6%stNH%0P^trGTLji*jnxdpr^ZJj0KM%1tVoNclXd>s zlifzoTH1!2z4n%8SbeWjk>_|cls+rHsSrZqpwfotf&o&|bM~wYn*lV|fTEBPcWyQo z$$~OjFcv3<1p@IvBswIz8EPrCv|vFUfM;K(AtWTZIqgY@`g4ix1u|1$2SWC%V*u$m zz1i^bbQ;tlI+G@ZN|m#XSt7$*rnL~dSI)cpmzfk{VncnRnE~8_j*0t|<6juh%qi!=Q;%UtSTZ@sn3Sd5_!zbcN?Oe_ zkIsa(g4tD`TW_vHgEe6Xrp&KxzAz{P5PY=9Say0FM|bLaiR)xi3zpvIXtxSsxAj}k zW~FOp?`=dxtu+6NcV|eyRQkrUOb78{fwjuhb;{ERn-PlS0TketbUZMN!J=+>gQ`H@ zvzg9gu-rQ5ModxqRJKzigiT_Jhw5me!4klX}_+!-4cl~ z5J1uz5jXod=?sKl8`NbMh+^iV9okz=efX~#ENQgACd_uQvp4DOW$c?5(ICl-APgCy zJC*4j&648hO0=;oYc%?d{`+?1c85Bbr;D6Fo=4F* zB<0i@;qqO_ZZyK}^8$N1!W|ds;j8RSjP%+QNdrZCheY~L;(Zb#{fpmEr$q)_i_C7^ z88jNXkBA9=8@d1A`wC$sgBTSSf(q4(ig1fM&D$B75OrV=cWqN5Mz17&H9MxF>%eH# z!M9NfdQnUgLQ^$2AuX3Fvy>{gq_z@uBq1`K49CSDNGm>&b`1f|$DSqUFvxJ0I5H#T zz{!e8!!u$Dn_`48yjxT5N?U~J+gtd(WLlh?&+TM7t4h#TCo7MToypRK71y7*g z^zM7bT(|o$(W+%Qw44`(T3>}H2;d2gv2{Ss;l^Ce8}N8StovD@5dawB8+BS zQmjsnXWVt-EnZWoV}rz%1iQ~Lxfo4CJcZOXE0yn?XV9AI-wGEH#k$2a^`CvPF9xBa zo9jJ4p4Md1Ry1+~qq!ZL6dci!{rapR>J+h3CPN znKjAA$RQ%K>RPU57bdhDT*Xf4>}5E^r>xi}_Iczq1@uzx9* z3Df67lTAzfqxn8#_j6aXri8FSE(}R@3S+|et!9Te#@Ony`xS0-_}Udhjq3^VFv`cP zvG2+SPcTeibNdeN2Tec|NHS+(SS(Vj&ADi{#$lhESOj_2-rE!r$hvS^0l7bI{OVM3 zN-+Yb%55?|)5k|72(ypNaMhBK(cBB+C*Fl~QS7SRCI)ZvPNXM={un$5eEK;*uqtz|K^w<);vIp-k=8^y^FRisuozSLPE(GTAS=!!^Lk4bfti`k`*2VQo9#nU zm^oC_Oh5nZ-9VNP1!Cr(O{s#~^KX}uv$T1cHWUbj38Tnmu@fQTbeI`80x1W;6?rTr`0u^*NPLAz(r4<^qXWQ>}bQ+69QmJ8PD@Fsmrn0ZDN{S(Q z8oz-ZsOxV&vIEu1(ltRSiW`pLB0>-DWqL%(WzzjKw+Ue$gJ9d)%pK7`vZ`|Qq9gY1 z`C-|JP?q~Yiq1W(<^KQUpPkQ#YSmV2ZMAgHI-l9rIwI?&GwY~SOeaY<+gb-n7D)N&V`}2A|pN~gnM3KZ4`Jb1zj4=t@ zIw&q?K|1w8`AhGLiSX0Ac1MX2>~_hz7?__&k!KOSK!DgdyJdg-qx_(9o)Eg01$7B3 zSw}99&-lK%ZGmSAz$RPk1meG!i?Ko5^Kui+RxHn(b0qxl@=-cGV&4yOgG5IIga|9l z+FiwLo7>(9gY!-sT6{_?CF4IV(^;iUexg!Fdj(`Q@`Wjq*CXlq{K{vtjHw1a-CL29 zF-KH@oYbK3cB#GyY%7vP4nd57o%yVH-|wOPis0-@NN7=6F+%FBk#1_=qS#d#&63)- zBU0HAuCT29rH&7>90so}1|xLsGUn+jDcaB}dv>1DU#V*uZs)<)i1t97@pPeveM@2& zQ>W&CR)W9)vXk25^tu$Qds!M8)DZ}A>VtAl5)&*L3FKk0%C$^v$y$rCi+)fT4g>F;Av!%}VBZbW;pMkNcSGu8^O$ zOwYWr#s*q`Cc2G=h}EnRHt1}zQEvaI8C&=0-rkzfFB3)Yt_9$@o2o7Nt||GJ#uAi` z)hNo+gK;1KY!eN)_5ozUe)E=5BQiN{jb_}=KL4>Bw79YtG-MNao@PJ&dy z)|Kpq5u2UBw`#_sHmGR+t|Odad1j%9zCYL|Ru8}i%R#UpT&gkj3IjR@`A5kfL-y&P z96LlH(wB(2Yw7{NeD@g_tZS^JQUtqD^gmG8q};lcz4qaQHNGG2iW(xjn4E*2@>zK$ zy8sET*3Y8z0<0iWugWY|#T8p;AL8pJ`+ga@_`u;h_Tnm^#a3N2*9GZ;5S#1?NT}~y z#kx?>`DzHk=i|6Gv-!R*V(qyXif?LI^TQtR<%7`NEXk?C9+ z4Zlxb6kuxCLGHf7=zWoWtFd4feF^r{M1D)TWD*Vidk}sjkbepy_L227;SqbXp7-}H z;hblYeK?P?$?R{AlLGTF_mb0Tciq6p^!BjeA0T-d@*0!?!~X=Gn{ajy|8!dayteoD3Rq?R zE^A952rK6xVpFGMT(|K2qkOlKbzpb)@V4sn7F>MP2C{MIwO0IUWf`~i6)4m$&Tf0| z=7KFp5u^NiH=lA#NXwx6_ZN+Fg}5hD$0o;5wQd0;F~w@AX0lb_%i8YC{+$XS?sN> z@O`=&qmJK%6_MjkguAZUl3Km~x2*Plj_#P_6ush8z^u{c{8Hm*#2}aEQCzm&$E*i@ z#-%EQy*Wj%vTQ$4evDPz%*r-5igUK$^#_K?eJ?NswyA3_v%sVvUT|$(OD--7zO^?ykvWygro(k3ZF# zBzqM#QyF+_C@fS+4L_!7AQKFC?+xI#9KTRCFxIK#wtD@r(IeZ}ZtaFQ=eFk1zsw9g zq+P`Ya#M{l&s%?p>RtFd_a2lfx9yV9%9(xwv_TtoEb z>rTwd7{A%$ol|OlpqKk!iwB7vwZwD;23; zR^}jEXCFbVT=m|u4pVvC{I$^mke3qxlw`@cXW#Jka-ZJ65pe2qUIYEq568!(#XR$G zIVbC9FfTshJMR7Eke|swuXB0hdf&L*(d|_CF^luG$4Bc4xBsX5SeBa!9yNMQeHXM4 z0L^-;fRA^)QJ4KGFiy;y{WgDlveIJ$WEjgOZi?42=?~hBHIhD^!TYIitYB|QGRx*>U_Lam# zjDFn1Tt1#uYOuz7V^*O7F89L+>RSM@U#Y}pCUmDv%rbv8yFGf3;vIv&gIhnu)X44E zF*Y1MKS7-v`f=^h&9zxyck1Vh9kvUgu2vjeTSkC7l!0Gl^C(16m1kX&?%+Z{Rd=NR zfdS}}9gwnldPsyF+^U_tM<|9xU>u2VKfx+@bVwwuJ`l8RPuD;)4LRczaw5`lJrTb4 ziK}t)vff+v*!kWCNOr7Rp8S2&BTKq zY2x zi&cP4YGU`X#{FK_STDRJvP57eaQWc5z?;ZU2uoLmXUP}t4ju8ZT0!ag0I|u zX0(loB9u<64AonMwl!$YU>OzX&|UIv0?KXgk>mIBml3o1VB~tmD&un^qL1n{ZQ&Gx zRt?8x_S(G0#-8!oGIa97A#YCN=|J3^<9^#~-RdAV*sQTjN|VQsw?3`Kb>%U8Y=}m*^le@MK?dE)-Nl$VMfN*=v;9`PiCL95ea1vB;F8X%%Uf#A zdmy*_%~JdJx+VnCZv-M;k~zffRxPx(lm2tdVI*-o$dGq%_~Tqt<<+5H8SiCL)}(`s zQUa#76Qy?Zd_;&g-(1k+XTb-R!J<^8#d%&EBP?3?#+bb&HXgrxNYefASay-<*UT~DIQzw=`1D(Kru|%l-f>-`1YO3V zOe&ZEGRAb?1+fPG$)O-J8!QV(H7KBTF%znc^ATu4X_vm#Sd;yheFvRQddcaBXirWq zOKd4!>T~DFSbQ$C2+u@<)wRefWoslme_UV2XI}14Jd*y7AvJ~}q{CA+N z%OD_o70_=0u10~X6{>>@^L!38QWQtzvIKsNY>6dN@@ZVPjJf{~X$j0^nj*X4++Seb zN{&f2IP6lXPm#*G9;Eky9>LdP|)IE$m(h zV_s}L-sg}HHo;3N`QUOpq_dBhoe%b^n<0+)`}Qb_?aI&@MU2PadKw@+jhdYu;@bcc zX(2Iki2Zl5DH}wI5$lqZePv3?>Pm|Qv98^WgP_j(-2vA|KWi(HX*-Cj5aa364rZEf zJ1B;nyi3cdWpQ~!Do47?v_VXn;owOr5+LH*>qPDE`nMoVM~ezB(dd%mbl53mJEd;E za{8*j&d41|hV;pJDe!B&0m|0q0E?Rh!*5Mqhvd3TmF8!N0bK)#(|3duCXrZvmO3`hQ(J>RM;9oEaQIN1+z z>6l`xxdt6dBP?jyGY${kZ#v!wwn6eMO;^2`Aud#~XT*krQezJ>TbpS8aEPx8prOHr zb%~bGl!kQ@qj{yZLTuPAwgW09A0&7zhsGA`vQ!R{W9W}qx5*88$z0zKP99BK>!{Di zmKysgMI(L`nJQsoqvf*`318E)+v7>QM>kA}(EW#(#{I;eelPVwj3S7%03`h#|6==i zb`A!BD%L(M)kF2;GL{qu<3}N_Fbg|wHXcfo@s~XV+4h!BsQq#_H#!c*++A#BYg7a| zY&=b+$X6*!z_ec~^KK<==6II0&*hiMVn%ewS34kj?bz{>QBakwD>b8v%{n-E501%X zt6Gutm)ULigIv5!(wqQ};h`OR#4SjuJ`1eh*`9*!3kBo~HRwqW=gD@?vT+clL2RdW z1?HEMf3acMK8mS3Imt8Ubp{u+`*ahM){@)OD{;LM)t-Io*E0{Uy&3nBzmY|3A^ z#%_tr_};{Z_IdvVf6|e>bah;hA(aW!%{fB_Bs^pjaRF zelO_;bU*1MyD>+e>}5_Cfeh@(20oMQpmSMRJ_oz`GLfyt8ln^;ll~&3l4hkU)6w#ijh%f>!UIg-f|;>8RMWk)eY?NMyt9&i;}+RQE-qK+y>jn2!hzm zgQxaD4QCp8k$aKD8=i4;0CH^AJLa z6b5OLh3X@Y38gHn{Z7buv(dN*$Wn(+#&*&dE^qlz-BQHD1(+9w=TV2=ZJ}>RnR@vL zYo1fuA%J|N_xpG0+lb@Pw?f>DAd5W#Y8OIH(!Om|pEa$pS9=Q)4dYl zBUiJZH6kAR-|isq|2|robf7=7D}FWPQS}z0=U!JfB>t4lisEOj+>!-ML_3hY1p9U- z?fYTC^L;mW0wv6H*n7VxktbX0`z7K6tnO#=Qn>y<1`KB@g4x0zSK!7)a6LVR^)dsG zPt0gByN*K{5r=mV-~m1-;Z1<_6>fn&jqI;IUFK-Y*dr#&USAkmNC=4NN8E^W_1`km zgw>B9hTft=GyO-(T%6sd=%l;bkLhbI%`Tn#V- z)|Uz~`1{dB0p!5*bnmCE@#5mD=27hri>X8w?mdwg(`_H^x`^K6&>IR}ugpMxiO5bw26m-%29Y@bWzUUw_MpTJia zj)<&~^wzQAsQ^~Z^B>7rvdWEL3o?+o$47(6L*ogLAmmmKrIll3<+47j1&oveidz4y zJ$cfW>k2jj`>!&JXUqqG$M=U;DzV6;Jf3)5$5v!Dj1D`|=*2hwuJn|Fq_4!?X2_u- zyzaK5_alA5E&pakRwUYQvuI7^5((LQ`#c_B&N=Gn&i{6vG5$Mw--@np-s;uAU+A?l zw%HayOj<6m8;W*6TLO6$`Ng08vh$#5d{j)x7QK+O4QJ{8Je})52qD22n0ooXjhvDV z7aI0>X0PV-1_guIY=Edxez_E~aD~9)_(DDWV-RvA6eUnu&$5vTTwm!)KbEBNjs)Ki z@t83csr`I36>G-uly;du5ac0y-8e94cQSVQfu1;E%&ou6y96w4A>`h7yWsn{5;~sc zGgh2?e$O1SsYZhBIS31Vg3YnelexVxc*}N zKLuA`P54n;m8-={3xtZQRyJUu@+L+LyGV>tFaq;Iv7 z=&>X=9Ood(wuOFB^M?5|<9)1O2-mu*M(NK;DmW^@uSM-|lKs8$xY6nZZkMdCwe`Vc z$5bx=DJ`uwaT@k#r#!yLUpvts@UXquR-Csp>jjZmqB}6!a+7w z^)sy1aa^faBsM+p-S@z4P`|Wdci1mv8-W#L#Pco*>}QI&Odi;Z_ZL$LwiJPD>m(0X zN>_app@q<(R37LV2GhaYIN^tZHe{c**kmjgF2`XNj=&b$m)Oj*K-rjUlKn{ zrjp^KbhP4@Oq>i)87}q==bnO@k26V%re$*qBPn;5+3%_K2(tXFTA42*mZX6`B1lop zHUC<{&GFSrcY3S%k{@sU{0ghc-*XXvSy%c^c=OggWdF78g7a>3vW^1sjNLWJMs-M^ zoWF4*MkSYJo)VQBRc|~s^!v@qrJW{XyP+TJs9>B7jQ^E-lJw^Ub9}75IBrJND27&Ax}-3VxC{MV^W{weC`}epa7rD9bY}V&9VSwT`H>Tc4(XJi^@^ zeQezPR{NS0MbR3o?npxPT34Z+FXcZ&;RFbcpcZN6=Wu=V$nl7-(lSC>4E$a&q*C)> z56Pn5=Cw`At&v-h0Hx1<2=O24|`QiJ2KR&sf5xrGIo-W6pC{BBZ{g%rkT3GQt z-gI*P!hB6Ub2I+;&wp-UTKh(6@g{zgqe0AMQ^jpXJGM#M2!=IR+ClxPYF-{1W4Gd~ zKr&7e2_PhT`$iE6C=yh#hBG)$Wq@q!z*2Uv&LFkmS?!YLx^ha}{$lm1GX2%$+oiSZ z3#Y=GHg|w*jV@kDu^K+f>XOzL7dtMatxMH8r?)z&Yv$bEuWYA-#r!1Z%4N@=p+pC1 zA`KW~o&pMh>2tvtF_JrL&G*M9PI)N3`V`tD3T4L>&Iyf@?&qW0vp)!9mnk5zI%{C(1t zm=rxMN3K-~#HdH;cFQa7@20G1#edT3H4@ypxur*~^26yzwlsGwJCfJC`;BqNz1_>s zniRZ#W1Ov8Rn)YZ{ppmk7SM4rCA<9CfdyEMV$^*GorP4WibvV)W3*2lsC*(MpWFICt`d=eCD` z&A4{>&5_J%P(m)Pr8@Ul^?QoxtM(4@GNbH2>phYmv?mO<>a2*w$pM7$E`P;d z#z9}L1q~1~UMfeWDhZBDMkCO|`SO@Xdu(@%^!~F6--zE?f^^brs}sqMOs4i4PaY_O z{%>zi355DwQSLC_LbR;k8u+pke!ejuw;>Fb?jtsfxfXUc$!;38)}#OT<#yr*rqWwI z+wePR_dz8|4j}pRUaqBBx8D(r6pG+{xp<2W4>rCj1UJ{!i;(d^_#*Zyng;mWH;sPt zDF?^A1YrfWVf5(1@Dg=dOi3R;hhJ{#rdVz|XI1C5JHypFZnRj~MEhION0{4MeLc3N zf&LNhSzf!lSlUN^M5nr;8}4*_pjwI7A1*(pOGw_1NP9^S^qmt{no?D}sE-GhwQl#1 ziOsh;%?Vq23jPsVJA?u24~Zffd|=~4bC9p?u}ep$#a;(zWQHP9P(o?*NjekTmh^U1 z#+L@lGnf=Bkb%#an&R&uqoXkrKhuy-;dn}OkbX8%8AfoDDRm3f`%Rv8VU~}uQJgBV z0Y7Y?8&r(x7ECzY|6k`fUl^g?gJ*o1jmnLIP|0Tz`rkYE&~unUgPK0VPCn`u-oU1m zBLM5`njPPr%1gc!yqD?)AXDiDp%iiSjyKvxp7u@Q?i29Gb)F**`0>-A)M1OWwY_;z ze`LSsCw`ScF^$=rXSMHId%r_m%N}**`5jy1H_&M=Lv!obZ zkDNv;+aWGt4{@>o-dDQk4C_Bpm)nl}ku~f)`rr8kqw!MxcukPS&Y#kVm}I!5^Lzvx zA_7;ZG7RMkq>FGrd)U@u$QQ^p<%4hcpE;x5cK0Gr6AM<9;f=G0NP;l*N}nrC^GPLD z5JnLEQjxX^6enN6N4KlZsdDV3&opRK|L-3=8ZPe9Ef3qrl=qD+yd><88&nhVO=8 zwkRZE*EVDlTPmM(bT|oZE))pFnGP}NAo(oNm>v{B3P*Z%D-MuMCO7mwlT}K_Mj(%V z1We@a&FUs0gIQdYn$bN>^}lyJG%9Sm5E%)GTiPcj*pa^~=)VM5l?>iT9>o292G(}d z$Y5g^IpRJn-ME{t!rm-Ii@nP*6=b;0eqgsZatVZ7th~kA&n`<;tB~z<2r*RweU;6d zMSuC>@ZMGoCeuO79TZ6&uS?CK9Lv}t2{+a7AZ|qlJ+=YNQL!tS2(w2MI38tPCCpTi zgg+is83ev6b0@0w&h#RQtc6N!7t&lm z&}^vUYyVnQd&?aE7~R0`aOwZXv`qRS6#d!r$gsIOY37#o$=|hHHRE{HU#|iOssale z!U%^x@{DSfkY$ZI_dbiB)qoLOl-Q|IL%SWk?Ro{ zg(5B>nRyxg?;OZrriu26ja?eneq~X>k-OD@sw{;Losun@l1%#^PG3h7*=h>aE2p__ z++{Pk)u_!_Jw={KE~$L|0Xj=A2}Pgi%)6$n@_A!S?XY7GzVFO%uMA2r>fCZX*}i!y zFtXgZnJ?^Yx?9tddJ|-l@rK%ZJNXZG&xKdP?ZKVe%KFt{wu1n)gN`xV>`oNpUxb*G zz%x7+evD3Z>$G+oT3o-S^ji7e5H3TZ!7T$BJX4yOG0>Og+E2Sfs3sNEp#FMS(mNF> zOd@x14wd+enCUy`rruYJzoRR584MU4BYLxKe9#*Yot_Qu|0G-x+6`>Pmz8~QH2d|_ zGI#Q`(Y>j^<~Wq4HgSv{+@|0dXK-P*_P|yh1ZI*JKfr3~pRi_C}-As5H6BF;oXYI;sLfYt0<* zzRgHKG*DsA3`|c=H%=%wPKCD`sEnXP#>1+VYv%SPDC7R~#{Dhmw-v^nP0xW8ALK@R zXa)94QqrG*+%ax?GlNVHu&+rnKE1ZVZqPt!)-Zx^8~>aalYEUV5j)A@C4vQVbAk&W zVky>ZYfb=~UNthzZ{{+Vi4*Rjv0luwV$AW?U}!T81Bi(uO|)km3R;XZ%Vi{=Gg}Jp zTxuOP?oHb50XBrbrP{nPKg?j>=Q8Hc(5NT3HjKUP-ApY=JLI9IL8Mf%B0Sfn0s3Y; z8^PZ4q-5~h67JVSC1vuv&jc@MyVIg}Su#wAL$Y$vN-it=@vUlFiPn z>Z#veA$J?6&0&^*wRM@F7aJvv#`p{v?7q<8wUJC&%jkhntg8TBwtK-h*QHJ8Y4aH# z`pvfQle|tz=))&S$Ms;J!xD6@ok_`Tu9Q`?C!<7zBi*LHQ5x5Y|L3Iu{R}_Fx(k1- zcQeo zYUmf9H7(nrN}~tl3cr}A!KqK!h@A>_I0qP=W{N@9FQzQ+HXC1WsTy!_ZkhQoG6aw` zwmh6U$Rri)bSZ#!A_=mjB`_@ zPzIr#9aNV_Zj&C=oZ&r(8WxI4y&|lt>4fw=t&pJ)COYk#qCNwe>}TtIN`|L`3^fEp zyD3B=$WRc)nASI}%T+WIS4Z3)$E1jL-%|WCqFy$bV$kViIR@SF$V}sZz zmJ&U$(5(@fyH!E$KvaV%WDpyd%1K|EW1xeQ)_+??o^Z&zp+V%cb!1Nebo`6>#ii7Z zH8a@|etxRwG-%{h+{h5bNyO0 z_9BV)43wFqpY&cyY1W`mH<{RoNl!SiEthFhedGElT5po?+Jm8c>(dOmf=~-1ZXW73 zokRjB>;v?x71$M1h#D~oYKX2!>8FASbs{5@65XK0Hq!Mc4xTtJ-E}THE-cNkQGgs* zV8#`h?vfQk#v1uMQq_q~r#MFQY+OTA$s>WbJdYqv6Eo=gWUXyezA`&AR?&4NLHY;vq$foM1IgkrpwoFx}$$uj-3xBj)>jl=V+5js6X^pJA zmUNd<4mn+eDrX~}iAdENLL`;|<>+Jph&c|9B*wOiDMNDICwAEdbm)Y@aAZEZu6^y{ z6DxB8yepR>D>iU!&-;WH1TEpYlKEbJ}tnYli>1G$YD|ckiw)BAQTry#_6ZE zUP)W^+K8@a@<)S$hTbP}abNGD8#TI(3bdA5pb%5$0dy(|-+z!4uEdoqbeS6uHO@xc zT(Wd>wjMZ{XBE}vQ*0v?lUN*_T!i(?HplnS8U=^HYDlap5;c%Pp3+??C2hWg>j3n) zY@zpCU!(wL2B_EQ=ym>-a%WOH z{m2ys*(#~vEgSnpy)*|R%z%uJc%V|{w4EF)E$jQe^0&npk}tb#z3d0H@J>)WXd31u!zm1V44D0MLV9r1ffQ9h+#wsjxYQemQW^&s#*{8*lS(7cjiQ}mG1EkbQ- zqIEZ*=B5bgavD;M_*aAO@a)+Ndh$l;@K9mQ6r&89^cvV3Tjcu5;`6&Tm#Y;zRcuVR z200`$DV84xy+6LKikiW2d8}u%6(6jAkFUm_=F^GVH2f$9zv`6mnLa7Y|UHMV+AA-Z6~G^ye$i#Gm|cM_j4<#iJ}a zCUD@E_TVt{V1RG!x1py&HwzG_^N^WJy$^JBoVzyy7qay9}7Ji}f z>AJt<_$NZzJtbyN5OYg}x}YggHQ{#3(PmRVT)>#nznM|8ZrzP7QjpVuv|uTt*0gOiJTm7Pm*+b;+%-*}DDYX~ zG_)1;&*}SuZB4mVX@(`6Z!a0E^eYl$+qRJ3$QbwT?M(i*#_k03bhf1~aA-?wLVpwK z+8qz)X3|LF^IIh&u0!T4nx~FW!=H>9Me~vEs;IX~Rw>2dU5i!oi(h=e%C&*y9Y+?v9qPw$$2KNn#A@#LOA|61Ewri9N`KsV-9 zHyL(5xB2Rx1M7}I{Ay;{+a_HK4DCSfNKPGm?zt>UmpFCyMF?Z4#M#G>3Wd$;fSh^r z*n0|>&2*b80eV=VTh~NmPT}XK#Le7w|Gen$V^9Z_lx_xPJwt26)9BDBKA`%Eyx22` z%!lmWiWWDBqCD{o>eBrBb*Ay1CHOqIz%N1J3{kJ;AGnq2vb8rhzHVaF9(`_8XZ6(C znV@erS&$k41Y75Au`Ho^(Ll4y&1P?(Fjb~KcTIk$Bc_j+B(LqUK{nBffZ8@Q>Tq@B zIOu;5K$P(nlsCQq+tNfUUit9>$7Bvf8_`mnA2FZ<>v9%99a{R4wb}jqF$Opwy2w%d ze_I1)>>FYz#>l6Vm81-=Tqd@;m8t8;-ay=1jI?R-2yG-Xz>&*w4bBl&9|TRT*Glpt zu|1MZNbnWDT$jKD(O#{>hD_x{tb&7K{CjGqeT!lt}-x+`F98Vtbx@JpTcvVu$9kxIHdue1xorp{-Ygry4o0_ zHiWv)dTY2fA8T3A_&^q6y_I5aw7RrJQ)6=F-Pe*DyY|cPZeRF~t@4fS60f|dzX$*O zhgb5$c7tle&m9j-w;tW*x~gXVe{Zbc-Z=WGUS9n3*MI*--@fMpPENVY9+a2fb$!4) zpov?}YAUr@B-TG(+I2TcZ;i_%Udmnf31q}tR`Zm8S<;qH3BvVJ-K?I29Z^P*Bwdaf%$+W$d@Y4)d}>U_y(tEHf|lj+k9vr3wzhAir`;irtGo5 znwoN_@YeVKv_qFO0+%^mp?^cyzwe#C?eX>Op#HJEwzR3(v|EQv53bL7_;6=rmFr7+ zxzWsShX?&mXj{mrQ-k$#+D;lW{pvE+ckyeZSI<5YT^8>QL zyb+yn>LRI0wN!UixBtMlCry+(8(xPuOy>+%y4uXWfz30$^2{my=8tnjN5&9s{h48Fm%(cz&=!9`&PXC1y7wyB zVMf)nNu)U_(yE0!)onXQ2r8?D4l})r6`K|N}ojw08YS7;@25H40Ja=S@ zXqOM8{dBIT1hvz04Re!`R9g$TD^Obj&7Z-7G>-ha958BK9R@c%~*tZ<_3s4DK!4yN$ubKlP z?%_&}E=@`)aBjJj2Qn#9i;Z)MH7P~Q(5VW4U-r!3YbulL^TeD-$vV9Acm3VK)&_sw z<3n^O`XF12Dm_9RQD&52Ge}dPAzPxbUd;(vv{EAREaHEOawu`~CBE~j?ayw$$^Gv~ z?KkVRxiVhC7`#eEQ2Tj@t;y8qtRPHEQ&~}dGDa=%C++X-vjaec8}xn?WS7)oK9F|Q zxYFUSmWeoQ9{aniU$2<6cXcI0_TI7FB41&6ef%P=tUb^|zVx+{YQ@-VXAxxSrBf9~ zf*KtAt}^Rj!vltHp${TAeE-BpTOlv#R*6&$>G0Sq%6WRq_kPFY!pf9{WVlyPf70C4 zuq9W5tXP7;cVsE-V+aWJbPPKHYw?pckO=nLrGZMQ9&M-ulm!SLLaN_ zFMbwA2paFBj?LXScp}1tOGP>*F{O6rzDk$8KS+$SGE4PUI{4xdh{*$ph1UohcII?i z=;q8*yZYymWxA;th&7+oBP_d=E&iuYaf@+5A@v(?#swEijGrDI;Ld_~!hZPKR}a)B zF~vHkivD)&7@a7VSL^TUfxEW{5PkAXb+-u{v^{UcA2is+cvHwKcQ!Fd5pezz3V?P7 zn-UWQ2mt^M=m+fy?~!;^2KhOIw>H);?1PPW1j6S|J}%CX(+rE)>8A6d-;04a-UU^- zS)KH-7AT0;Q;(c3Y{f(gn~grR&3#o#rqPj8goGie%ahKsl~`%dqNbcYzsh_`cn-HX zi_Z6t-WSepSg~55jaMrWRQ8fmH?|c)4A%+oqUO@#J*AIql(XW>`!_RZ2E5nrJ-KbX z->95Y?lK`U{rV}{@PTY_wL**&YEwQ}(ycw)lB#QUrN&PyQEoP0py3}>)OJp_i&>t$ z7)UWjiANk2j@3#1rN+-=$a@ceJ+#yy$;$Es-0lE-^O(gB5DyVsS7AzMmm>qJg8b?} zpe?rbcS50B&bXCBUD1zCM{ckYb6sU_h+8QP&BsHHV#Bt` zJI1&%(k5E-tiab%a_zGkhgFp-lT4)?n;mWY_r4vg%WEt(R!t6g&TXv`G?x0mOF6X%nR_^g z1#y+hdCO~*&3V}^){UMcuB=943i%Z^&9GWi3L>ZeH>mfa9s@#ZAV$`Hq*W*HfY1uy zPqhHy+Q23AbSo~~$KEE!PFx0y8(joqSVXe;rms+h$u0ya?Ass}_6@2dX9P+?I4<~0 z4YAYusIAe5rhR0m8gAJ}H3ec0*LG|xn#LbjtH|?!-Ew5JVV7Q+>Cw_nj^CB!ZL*&} zo4{3m{A8M1ZdJWPXZpIjgT5dL@RhU6e1gm$6h7Ym=-s!%+^vNYOKh5Zk&0b(Qfc<@ z!==LW-8e;Op<0kt+EthqT?k=d)j}~EWcI8OH!&Y|l2HWxUT~!rRqbxG*U|vW2QTus zZ_@#wnpMkdVZYi8j&0Lj)Ij`Mu#Iel_hsXUm+bR2b}z#r(n}C?MVg@u3LFX148rFxx)~Y7f88ll{BWU?W-z_0uFGp_ zV?`kYJ5nyvnZ#68WLIvhAP-TX88{`o&@CM&jV81^R_-k#!ZXAwB92nILM_1J+Kptg zjN=SLLn2fvv>s+-8mtUyN0jQN)i5e8PUrMYY9M0R|-w-4H{#^2RY^TwgS%KNqzD z%FtW3jrcUraDAA;7DEEUPk;D-rgN?pzoQA`!j0mLefdd+BSIXi$Vds`lxoZX3)ecd zsgaZQh+brGR%9v>Ed>!xCrB0^#F2FDjw`rEhJ+F+rI|K#qKPOFMQ?7#s?u zUUvZi{uZL#?cC%ZS74772#lqJ(DQ&paCL$kXtJ9+(Z90s3dEv|Es(*Z6!uw3E3q4i z!|l!&VF&LhFug+KFPfV79706czJl6=dCHR8Czoi^yEpzXs6H*TzQPPEIVmb=JJ`)4|HL>y~H+Cu<9HHy5T+ZHq72+;3 zj8QZ|7!F>2n2WCRJ2K22I;C5&1ij@?{K`glT{N#KgkcWAwc_vwHsOH+A<<4bq6r-W zco-4xWf8Qf2^bBwvO3*tJKP9ig9?TC95CRtEMG8^Ff(SKGa+JYyHN1dDJSGer6lI1g5uf5v zo9LL`q{O=6^S|fLcNl2#wYZl6rce!GbwXM5fcqvx=zI!70g#12i$76w>M%rnfHHqP z*os3s3NoJuI=ncBiak@g9Xf_voqimC)r`I((@RK}@Bf0Rn1_Fuhk1AV{TkhN*Ilnd z)DHSr%Unh^Mr7=iBcqhqEMm}q|4;BoEbYaj`_NHFBy79fcKOkAYqs=ndzqaZnqZr zve#a1tHPtRh_~Ne>$1HLWq`KLWBvi`jw|f%904KNm-BT16up!kM<|P3L(*R9WQVnu z8eQ3O^n4Ly9ti1y-0&jO*lCtZu*yhYq|@rOn;rE4e{&t-gIWt0#P8Rx)A{g^EZ<%8 zh;w|xY7MGQ2$NY*%7_G+z_4tI*9h2$W9{WI2+T+hx@Bxz8X-8Q0IX95(rhNOL0&^t)uw9K&e8}4 zq&=fAmI(KyBF3b!Q;+P{f*nF;uM8*YZc&GS6=;E==z=f!gHFW5^N1og=EX(SBLP}6 zpI^kmALk!U^U#;l?aoYR9ReHJM-uAj$fPL!Z2)S61@71T1f%T1ZFbj_*aIi#F`qQ( zG#Tvc0Z@wf>7=lg3wa)T@dRrP#7VO!e7MS20FBYu-v$N7h@cT7SU_goJt=Zz8qzy^ z11;^ZyAy5?CA?A`gop!vGpx=;7;N0Sw6pRF_YdNFQT3gxCG|-x!9G6jlLnc{hi~D- zN*mX(VhAv8!h;QRRSa3PR`~8{HjwIA{ zvJWVlX)UH5*$2-i#C1K+c%Mc)lc^t#(*GZ4Q`C+-*FN00GJ++_U^@l)%qNhaX(O?3 zZnXG=?sQRqJA<#ggU@-45_0-n6{T8^$`QNRJR)->Tz7vH*jH57gG@W%W%+evm@Fl( z7nf&p*8606+uS|>``3NV(IoDYl;OCUML?;|$h92RKMy87xK%mcGzj#8&RDCqDX#T4G@N8n~Y@ z@Ov+JdDo-S$B%Z|Kl|4T1jvEJ`9W{iNE`r<0WhA5Pzzzz~ zN99H(UA`NbOHVJTQBk9)croHzMKB*gT<2pX!fWfY5NSlz4i>ylray|RJWmq!o+(AcIxrAP%Tu0wF$ep3N#6KdqG$#1T!7Uh%8}5rKw_*X|z8e&VjaQ4rP&34?DV zBEM!4Yt=72z%T97uw_Kppq7A6N974&)oz3x^Y9!ILMlSmvEbWe30HKkrGLTM1rWOE zkwx>t%uO(+4bn01(l#P$6&qP6gii1)(lW7`w^?R82l2p_wkJ=%I-_ccBdjN|Vq40y zSip<>JmSLhJ0+ZdQ&!&b1`O$kS9Ctve-L(LWV=r=U|nr^d|oNnpdM=18ya20!qWAg zz5?VF`lTEe&qoBS;bmHmEnn-oL-g9guZH^X=tRT=#&s;gsd&Pj0^%__xX2ITiNJG7@^=#LB;1(B(7U_BByLf-1>49isaogOR=%&0E*{#y72dHNH! z@*ylpz{&k@=9S7(OvDUXU@x(7diAgx0X~O5vQLD$p~me}FF|VOkpaXgg$9$XKz!tH z|3pS?Cx&~e{hOA3j0!WTm=6}(7;as(f^@-;mivz=kPIQ}qzt-Y5pqGKJpvGxUf93c z@OVeGiwFhZiFB63u_2}>G_Z4Zurf9L4U4dA1Hm?L-!8n_h_LYdIHp`&RHlP--TK8S z`!xR2^lB#r{|#jI?vckXZdQCkhsVR-3u&umc3B5tog!qc77ni&O=2UGL@=)oH{TzZ z4}j3?Uty1HP-y^UbtmZGyoG-);5qZiFgiS%uH|PSR?25~Dr{3VupuzCNr;copw6k` z+J~14I-!RBF&(syFIxd5!9r?^KP8vNaF)u3JBvThIXXKr1M!;{?bi1%b8R%T}7$5dRUrDbM?Bg3-NGPAM*Cz%zM^;lV+}of}4UWQ(E3RM`B&pJFIJa@+ri%C@CqXd1w5Gg!AiMp>Z2W z$Gx&mL<~`D#LRZA}}fnWd`>o?lPkED9mzD^k09 z3K^oo?c1Fkn&VvckGpPqC@zf?>y#TBVJU?&1$BLNr+?7bIGZ~dY_gcvgOi>3)L;1i zL3yT!xpPh{r8M^N<%AmrH!FgSR)>m3q z6_~%eQT{Ff)zJF4; zefR!rK1d-%;w`$O|N4J)ujAY^w5=txRB3RF!)euJ%jgo%gyc+51YnIJ6qT$iT3*Suo{_D zC9L%OHV}tMaqG)(ZZ^3*bARRSqa`QyEW7);iRm2senSPOS01--GN8w$$g}o{WAST= z>C_6FeOJ6=1cn<;?Hc5EFxu?<3}?suGd<~tqHjh3kvVc;rS0s4ekd9apd;M~T2hhS zw==^D*B@ujJ?`6v_?kRnb$Bt|#<)DI!ax5($ORY#ox^J?HI4P&%P01Bz6OABxbE{@ zUE1N!^1{&V%Tum(gw`x)^K&4^@wiFhv}>X41URp^SxcZ-#NO|`?Sg;vO4z*EP}KDA zZmSJ@z8q2SQfTptTTW0<8??YGq6Bu6hi@DbKpyq_wRhZOE7`}0d9GXyN zwJzAUq?4P(fGwz%#<7OWxAR>DrURt*df937aSo_3HW|$`-0zH*0UXQ~x;H@jD|2K- zyo#0z%B=zdI$>4X8R86jfsPh^xpak=FgDF2x);NVS9nw5fvSjm5qm z?dy+?NS%5()x1#QdYwR${6CoKzZ8Q6?kFdwZma%A>T6sdUt}=GFWXS~nGg>L25;ju z6L11;5B|08wLo-ok4D?KJ^csaU#6pE0FTyAnJNw|n0fM!+VT$K)X?MI-QT5-iC zj}Mf+1)}QsW#Jlt4oPEKG!hp;jV5{i@Xc=H{E zIX9z+mBPY}qKm2NTPxz&a^hQdFy2^Dy7ZkYTw%f;AiErd*%iW!ynT29HbtQkma-aC z93Ol$OwVAVBh{da4?Sq3ZSA0eC_cY4eP2I7=og@~Bp zV?Fq9!5=zjYNe(iY z&_LZ}uMsO}O3mBZgGF^qO3RJ1CK2)B@gl-&c$vH;B>fB6vt4l3*I3;9-+U)m>yHm` zgED5n=vY*K$zyH(kjBud7-_V@4FiTam0bO>rwiB~we)8?qby!CW&X>lyl?@3ZD1U6 z2&}2p<#EeMR2dO)Gv;G=Dxcj~1MnI={>?xJ@M^50ct0+`A7$%SG;!MGp(b$iIT>`@ zoTc&g<7do#x%*dEapB%a&zK(uzX~L9J1$neX3qkR;`vF#Glc`_+0Jsaq&~fc#X&Y} zo7Lw}KRbW54^PjMD=y$fBc$ewZnI<)yPamDsEexs+QQb(jiV=lGjmg(<8`v%0I?l2 zqOF;?AM8umxn%MLaR{V-3m%E!zwCt5G>R7&|I@%qI>g9l3Zy5NiwJ>B@cHv1H?a`; z(+6a)0zg7lLTC%MCH__(Jg-im8<}2gz?%otWKDMHJ@B13+O2wsn31x_Uk~3snC*DW z=ATQ@f_ChLfIy_uxv0z2O=9 z-H~ZTp6)%@ipd*AEUgt}2I`G9p7|DOMHrUBXv;zS^BO{MGg6pW{sX;jY^|Ksr(`K_ zY`XM`uPEoiwiS|2{SMyK^CJC%AtU)Q0G0%RQUTfvj^J*J^`$!-z4mu)G(73^UI|-V z%7MnS4`iQ~zCQj~)tx!Wo!>ZkPIM;zmPM`MKDUO7usuioQM&qJtD=;fXJBY-$I;J`AQeiQQc?92Nu&etPpte9($->leq?Vo=wQRl}2 z92c{*Z-M~O!<+6My_*B#?EP49Bx1tw)?$V9*p~yZK2KT=18nb-JLJL zorK-E>z>NFkY1Q{5Vd!9^{t{OS6({mj=Rs63&WmXUvc8(w!NQr-1?KTq34H98bBx9 z#q`G9(KokX0c#xxcfa-i-%r1YNK1R|C93K_O1kOxIdBzV++FwAi36Ee@WJ$VM_&Ei zvFy_QC3Hiz&0}{EmVYa8Z5tkpdvz=OZGeY(>pvT|Bs&f9p=bPV0j(7-?tqJdH|o#W zCj&(^nuw+n-j{;SLIsBWTY6&5=6?T|QvQn`>1Rg;8mlxMl|g8YPcVRtN7B()1z1$; zq!2>CSE%IzLx4bU&#m(+>g(r~kQq7#(x)4D@Oy2A4(s^DssNnz#-uKb;6>V>v{w9^8kixOTTYd ze?Ns;K~q?_mmKufH|cG&?Jf6^g4Gm|v5Y^+U#TslbhThj$_>Aofv}bV>=Ks`QAn{) z2!i=JFa@AP0rYNXSm=r<-+?SKf3OM=BEF>~<6qGzU!NUFZW&A=1Q@rM1oU=*G-NQX zvWRwg=8ppZzJeqd=xJB|Wqf!NUsKM{{L8Qy=pDT@nAH^=hUFiOBd;Bh4p0DK1%No) z$VLtk!oWn@P<%Q7&fyoN12%T`J$%#wC>+l28s3fV1F*e}ztfz%d;uSNc358@4PS}$ z#VU?oysAE;oIKI{%hENUkSmj{&GvJ>Uc5z9oXywW^#PhaADTT=oIMWh=_uYZUwohf znyXUf%1bK5rTC}CxVThz!Td7ycZb zJLb1OXLu9@IMBahxe@D-o9x1Ge$ULZD~1sH#UTnu;MD;yp+hPksV-r!WY4Q|;bGa* z+gIvZit_I8Xtyt6M=J9DjjO$L=R<3q!f)o>E=Pvf;li&vjb!6S{vwnXPW&7!!0lM@ z+3oz_V3ixz9URKnJm(wG&C=kow0Vi>Y{?cXtRm^+dG(blc}bQ`u=1Mpn_6d`ynl;& zxi-dR2G^;mf_dDgT^v?nSMmhe;0bLOFkRJhgnA9m;j}|h~!YvdXV;SV&q$n|la)AxzE*FK-V4;C!d$`5G zk9|z8zj?xe^VUBjBNH8Ju(1&H5O`O!`Lqx zO|}){wR_RztA-kxc zIC%y}FT7l_Mo_c*X>G<+V}5z=BJ_dyqB zz}v^BiyQVtTwU^qnFaJ*_6V;UC_Yh9oSRod&bj2%qPI-Do34NTJQV;R{|8Q^8%;QA zo6aUx0(+;UNoY`cj}SF3@a~cBoPm_jkDRB8%|d~sdC2j4!OFaQyEq`D9)b3uShEE| z9H=|#sk>CpSpy`nllwO(*Y4H(YIe5(AGPwXO54`i`BA^B<>>*e-htluAd8fD1JE>F zyA}f3+8ns1E!?=%+_;Lrp{2af5ka;AK%{&vF4w4jo%T#e(1Ap{(1Z`XV|D!O7`o&T zYtZOIjSakZhb~1sf=dF5RaO@kg`@PuErnu795{hg5?d#7oUt_)pmrT9PMd*bS(bfU zs)BMmpu8FAhPvW#6@*JF*^;O42LX(VYabg2UrmbT3|K51FnhZ76iCK5nHOLSv#SVURd|M>iQJ|y`YbQ@adf-yL5Mi9yaclK6C&P#TV z7k|Gi=8+N6D)D+5ls6Aek(V&XMP7VyY?5B0JkYijm=xxYB?VrkJ+8>J8Cpz)(D?v5 zAEXB8Q*^at<@8I(Hmi0HlKUC5SPxP#o0}eO19) zHwI7atF%8c1p%A_@78wQ23PyHl;vwKb($7V_MQop!cu_mE=#SfBy5r(de%RwSk&8M}OLCwm4U$@z=8prXt)KLji@9i8=%Q_t zwm7Hj0z(>kR-DoSxk3Z2fCI}n z-@O75CbpHBX~0nc5d|)k7>MobfXEm_l19i?FXvHpK9LYs8X$vKyonFp)+2I5Yf+FT zS*j1}?KYpb13nxw?mS$6=veLOC?BqhoLz(5degk;upfydN#lVnf=^!=2fb{Ag~6Xs zG)oG5z_tLPvrNZU19DrUI@yL^I#z4E@VNS&jX-zrXH*9O0~eUV1wU;CR}>EAG;y?a zwj5V1Hz@fx!LZ%DcwNWs6CP!SfRePC+l^yavN*6UyaV^2^AY1td+!1?=Zu%?ZzwO- zLSzzWg*1H87C;AXtE&LZzzNg}Fh@uock+dc-faRV(O}l<2- zQgxuUdR_YgutMz@nB5ip9+~&4A1?OhJrAde4K&49t4l&>AfD*Y9md7&tP(s2wj6yY zrUS0=G268W*wi5+QY4bS*ReB@eO@|$_LQ8(icuQyN;r5IO>`((?D2e(b4nah(0*cG zWYn2V>#pe&fRU7N1ZKXwBqSTQ%Q+19F?O29U&&e57sY6o4XTY9g=Fa&32h&78>7 zm1<|4U+ss>@p33ltxKbb3$;wppFjo=>_?USGAt&IA8?REPuDq=kBbi3KS9x9TPR>B z-1mKrViZTGZ3J@gHr`#X-7dXK|xNZD1fAcG)HbC{<@=3kOPd8-+F-il zd_#rxs&|4HwF0P?Mk)o8rNEcT?l3gSxxR#@%F@%KE zVY_%RCR`NQBXnFWD({HiCV#~ZL=F^>V=!i7$ zlgwSh@^PVaRfz-5EN)Rd(<%;9y#IHb%Sz6Z3Z7w@wcee(Qq;e_Q3v@L+9#XOuMXaa zqqlg~HwLqZ;h>}-g!HN&&b)Uw2KBk;zjLtk*TENLzimT4wBP^D-qwaUgBHmlHfn(z z4SMlsv2&r&Q9c*0-w-A`YkHFip3CXbNGJ?HUNxHGt!g^jJGSKnH=NXw-aB5fsB-JHxSm z1UGZ2kdsyi>SDt#q@D}O<@UC(zj5!m@79;hiyQ7YtvXxqk(Ksjysy&n=ZEgx7wzl* z6d#$qd}aI6f6nnuA;@~Ffjg3fId!|(KPJ^vS8+-#GF`s5hXBkB072Nl|Gr1tgPS*4 zkmwy<3~;@fhk8sc7~D_0ScWGIFOB4EwXP&lyEki}0U+a@sto-Y8z{TS1 zc!i8F<0z%`>=FVf<>n{s)24+c!uu*tA!kHRPb1)t(1R4Rm2mWsj{=azuu;^2J2$hLx)%8W(XRhhKWdPoU$m+HKYxeE_}~2U zY{we?8i~=Pb!;=Zv5G0kpo6~I< zzZ(Q6jWKly?2Q3%tj4SzhNBCB5Vbni=wE9I_q51Tbta_qz zO;TTJt$aDG+Q!7DhHm~TvF{YFf}tus_KEdeKfQAOUY|3$JvC892Ty%Xp0!EW@dYUB z&nt%j`+n-HjkGXXeqNtW$1{gOgJoiVX4g~-N(T;M*JPo$P1Hm2=ryQ|{?zKAo??S~ zU5lsTU8T5%(QlQdsm5O~_IlgdG+d>9>qUD#{@8QZ^B;TcmiwaRLl1W&qGscihm0fX zyLB*pzGRY>uO3EJo)^|k%s80&dszTu2flmDa_O_RQtd?Vi^Sc()?hV_ zZJvCOcl;`X>h`MhV@(( zu>`Ii2=jn8BP4nB9=>smOk08FRd^9L8LTKAK(=d2cZ~tz@6N#7UMi$Te<$_c&VY2Y zO_6Ew%MaDBeGrPB2Wn*|HC}9nJ{>S(j)Q=$>~Z_(Y_NfowzbPQ2NLsu?BB!g&}S)n zu58hUCo)}682}u7(EN%(k!l6lsP96JXh6zJdzh;L=&II#u&02;ae((KwU9*BPF^?9 z_gjVo>vk1t&>N|t$ctb|?%zguQ=Y_;h#5}V!^cd2h@*Th)Y%o^f1rQ^3gB}02Ua!e zjVy``u~W#9CtyQ?9BSk3Lm9V#G()$R2l>rD=)p#ky?8?6xEkU$(`ZgQ|1gBK3Rw=ALlNn7(NTJY}uVk7gDJ6yc*USYCgIc2$Q*yXF^xun-T62_N zdg{r5-#W0!MxBd$=;lA%rOSM^KUE0V{J2J8fne@0fJ}jx7UmfN69VZ0OnAX{H>mB6 zR10-e!Xb8cMU1hfB$y}EkENc$ZhYP+LTWBMN!A|;U+Aw12SR#n0B3`L$Dyqa!NIG~ zVDqMq-XnRQS&)BRwYsX0N2CKn+DdpyM!>mM)w@-xLQ^Y4-I%qXT=&U~J+ipyyvJ!q z6VejO5B16tz6kR_8q0fB;$rO9Z1jjD@$Y5m=Joa)6)pza@j6SGgjjQ76(?YN5=m1` zqM&XeSs5)c-MDl8o9xd?pB}6)vR2ryf2cS*f2!`b3bWF>6PS1DAt}rg0(rPum5t|u zCokFoa6Do71wVdUy*Vjrtv*0L>ydqm z`E|9q21{#{!I;`@OgK8-eyo5J~(_0TT3+jVcv@eW0qb z;6%V~V^RX@FntrXPiS&0X@RiN)>SJE|DqD_7mgHRaR$f^mRFPfTkDl%y;iD zC+@0boX5Sl(FRu~Dv0xs&7ZgZ54-NuKA$rdu_+I4?s?RT-s$uZpDjemL5uHqWvoseLtPWAPmNX73v#2iLd{8u;(htKBc| zS(p2HSHtxI_YDhcXep@uBWwJ=cJID-d9*CyPs6K|?{`8m@tzg$AuBxwjAN1P8@1}X zsU$OjJ_WtCWGo(~r@UQvu;`sB^O$M{G?jXCh2ep{*5Iw6lQ_6xY2NjKcgePsbEZB& zcOI+%j6d#edt>4!=I3A2ThwnX+V&-{w!IrQZ43Z}w_{l&Rp#;Fz`l0>Ek&g*?3N=N zRPo2xpNzdd{w;^9xby)iBzg;4qOJfF%(b)`Iu9H$qm6RSOIHn8Zryv}k8S3@KdVZ{ zU%WW3#jxG@_hs#=$94Y-UFM(jlk~s7C^;fa206W2Z2^$80g>r+ZE<_SY)!}Q8V11d z?YWxw_CpE)J$CT$v##M^Vb60`O)Lb7y|x;8@1lci0JH}Tat+9-;yI95*#($f`<;02 z*Gu$1?9Hp@&Wpm2W-L4fa^C4x=?>jYN-?+$viLYq9dUd)fQI4gf*!NzIyqVxAsyC{ z1g#skm}6MCOUO6xZH9-G{4H&2k&;s)ie=?j?Gdt>p=-CLwim5DI;`&Pu*{lVGru{) ze?m84rj{Ys1=Hw|F_C*hZ~#G3>Z2GkzL@O;F$$QVCrASyI~ql3=B{(tz|-U*>ff##JNh4OBLG;NnEJ{KmNv5 zMI~&^8$WX!-sH0Tr;f}G1jhYnj#E`KlY%mxqT6`@Fpi(l%15>EV`@g?-V1C)r?FLWoc{N=BA5++~hW@OuP&)WC*#8_VOICU_LA4d@9NGsOvDrHa= z-7$*JF21%b$Nj*B9Uog=q0mv^3F}Gtt$T=Swi;u9;4H2-LjTaE9dOyu|9*s#DN0n&5t6mQfCBOL7$c7=%m)IovVJ>jAa)wiGd_f(Z=_8q zQPX@0QDi(y#}fi=DwIYP!A@#Fj<3{#Gh7=2Nmaaa|4A_a30zCGVoWuvO^4v$7 zOls-vc}Ku(x;&qcng?J8_)r{us{JEn;sHkktMzn@AC1mhb(Qe>X99rk1-1ns4U`(k6jym&|4e3e?b{|TrXC)J1rj<5zz-42B z#C)CRRG|UdA%M_t6tBd5FEytC?dLKDBcLVwIi^<2haC$v&0cyJsv zG=pwa(WDLB(sjn!0Tlpnjp79*RxcEwZ>-f$K1ie}fNK@~o&$=S4-8TwoX8SsN>Ep; zs-5q%oOE;2 zaR@xx`J74RAG8Sp;rInU>)AE8)92#vFY3i6-F;#XLCynql?gnP8G=FHQvJ=xZUfx%yYiyj zoGP>#I1lFt8LECib^qIqv3iu&9`Ow3*m9Of=b!G+gF&Q!zoxBZs)DT*4nLaRXD?XP zLrvS6k(UbCs1tzfcgr7iTRiR2z;u}FbE~;W z)q+hNS^jgv8ws{vIF@&Lxd-~p|3;`@C(E~>XYB(aetqBkBUqsE?SGa>zD#NOq>2EO zbe?~=lCQPNN?MhrWr|Q4E`O%2O@=grK7-I6sK(6Rq>tm zr;5>(t~oly)^aN7b!T3cN3?Kb@=TZ{?0bZJM9IFHinX5ckMa|DFRb?X@}~@XVY9wo zul>DH!{;3{EB~|derleWr7)RZZtDV(FBF%mJ#qIXTSE1=CF%JVNh3Z_TZea9n;^2z zgq8(EQvbIj?c~>&H(ifR!}ekMhBGI7^K>EkvS9x~ledj8_}#cYZi#jabG5J6&JD$V z2)1;2lQ11Tv1&&C=5~{&IPLk5&F+vN6PLcd8F&H#pz@jb34m7y(xumr!4TTuFgiky zP>f55ZSrQ7Rz<82*|C2l5oPerfutw8^({$v!u85tPlDqwG6%4|S?+JUP zrGMWiQ~_-`-~e`?@8dp0$ebPADQFBN+Xyf5gGkF4myVUta&lw?)?vG~>R-h`lw$}_ zvGd_&gia2Z7Tu1*4-m_8GLvN2+SDDJ0NE0JRi+L-pJQW_?K0_uF7+Z9N;a+yY{t^h_~^mQaK z=ge6RDWHV!OhdUM^#mB_FNQVzTEZw^b!_jI?&Lxhg&k#QB!tVijB{A%Ui&rpGV+69)Qxx^X1dwf5NDm3tqjqT|t z)Cm0JZ7o}rfun)=zY6?B;;_+*-0gAT7U-+ruOI(sg$RAK7DtB+C|+NXM2~L!9Se}B z19VvQi$f6RvW5Ly`UKzl_InAb4`t$3`8*|(ktDrmHSb2jTh;rAjq>&8rCHVw@!eE*%-O`wjgPO@W+z_Qpe#1_JDihz zDf2<=4RbyPu|L;hESnDi@&RxRNq&USfL;b804{96N3FXF>6EYF3&l|V@a&>>$jX>F33*n{g zRRQrn%o$F6^eT%@)4BUt#xY?5+Y`Ue%oG`@i%p^x_IE75pl?XcRw%X>q>0vC{yaH+ zWaY_yd;k3WQes1w(S;KHtQ$}QTtjb^n0&qN>UMGTQq~FRYy0qSE%v6wYDV?IiSV?~ z?XyMEXtNz5Yo_$H5_%Oe+$XVwbP{(>H?oJ!HnnFS+diwTKDPOM+pu|dF7n_pUg30W z*Yd=P;jDf+R*<}2w~ zUwYN3&$+C;!Cf9};X(V^6VxX7>os2v1)5me5$??yR?|fY(40bH0w5fYV&QcRuSw%8TzqqYHx9&>cJH0HxT~sucl_?m(cwPb zE1)mO3r8|yEbm^ceX{;<{pijR>tBPdVfo=7Y&T@L9^Ixzbau9e|s0osXi_52?S?ON{-C30##qiTcWO!zL1Wu z>s)ct%JV_1biP79fwQX2e}3-mR@ktpja*H{JpMP~?b-X!+&1Lj>%=5(>Gg*;#GRXj z9?RXc7GBT3;4?JjWQw?Sg4c~rt|unHajV~S&j)4^8sT{Mn9KkE#eC>jE;qZ-V<{zx zGJxVnVGsI9b;ZikZuYmcp9T(XT%0)<_YG!RZ+Fd5WIHmL(v%d6OoCU!)Hz!~*S=l|VO=5?}?vDCkdpd0Mk zVR)cSM?`!nFL8@xq4LCOxNGR4>$KSN^}M&{N;#`#lu^ENhGY6&yFfYXR+)da#Qei{ zZbHF;VcJ;pUPH3x8L1VSqCP{6JNmMwhm6nN)4WUPyVSG7(3bms%KX8MgsR!7f91z&B@B0YbIeg=E2C>t*|@| zS;x8u?95k)aa}+{dmYMmY+kHA2dsn~A=-ef=bFUz&r$=?^K{hCX_cNY2V|XPS#3j| zMCbbTp?atXV#fk?Jvu<{17E-v5X)d%TC>?!`)WJS+k~aJxB(K9D-A`1EoNjdJ=K%? z+q+VAwaUKlphW$U0U?3QT#tTGeI~oB**K#xkb2FnvLG)Ny}*XL0b-=TbSj*lr;U5J z*F;S007q;uUvr@^|Ew7?^2C=mh>>-tSsb;|#G@rhZ$(dOk(y~{B@-jH=~`@uCG=rc zFsy~ET2!au!xuG1*0G1yt6#%vX+oRX7=(G1(9B(77+~Ef(tprIM7z~nYblSePSd)R zbpu8QZeqe-hRq!NA7<{;X!n%<4ezKSp7-*gfvQQQR*|USrRZ&^2Vzzivix|;vJ`dd zmi=ga~kW6H<@_*UdnSEIt9mPr9_6B+WyZIwt9lROr*9={IlU zUYAx0kbpp#<#!e;eyxk;ZnXA7Usck!mP_{Ki;OJ?5kX9wi+3wSeXFXA-JJ#ulaf?g z66?Cm^hUi*ipX&-8Ro&6)E^)XG1VM+V%{S(i{5Vrh(Yb7_gj?G1C6IU5Ua<)IKApX zhO7}gK@$;Xvi`|Lrt0Z`V)<)ES~x!5M<}4`9D9NZhxVGVrBGzw4S;F2VR@9&-hQhv#f9bv-yJX_}hrhq>CH@F*;#L8uE}l#I9^p z3$Ebk?iEHccf)`BMD4cGV^D;SCLoBJ){i2DKe0>&fNj;yXloJ!6Bz>^?Q*I4@jy;S zJu#wXA^{$tukvX(|sP*g%;G0+M4h2Ml5Ra>i$8f>cT zcU~=6iV7S6>Wgq8te0PqK2Fp6968KKztLb9SZMO9uGl+XP`p}ds?P_o;@@d%irUlE z-+}`zrvb$qkp4Fw%^efl{oOo~EMH&wu;Pkm6qXs(Vv%7csTj4&>SVu;DneY{e$wql zQAKmZ0zk*^VJ~_$-wb`0-ha$gDBbSgkJba~SkfLKe0nF%&K816HH?7*B-21OzpKc{ z5EdB8B<#-%@`bNI(AK0hcuRs(AE+W zPw!0#{9AbMQy4zt+S>J!qEGk#O}IoUv`0SJwOR}`na_imtJMg$hKQYjFFIi!NTj@?-Q~BQl``Y> zkoa4UhTCdNozm@sYUP2;zYVMLA10@NbP>&P-Y+FOGn0vj_6Nb%}!Zp zol`*#`MLaA3nf>9Z;e8AOQ^wlsgZ{9FW09BkcX)-r}8r|R2?Y~%%$jEAX*8&X2a?F%QEUT8P5amiHSY_OTLg zlFhf-n)5Q2?XZcw*b*nS4s8d9C7s&hvYeqc?S7RJsa*WIR5NEKvyM!hRp`l;hzW)M zSHPuiA&UA)zn5ej3UoiGWo}NQpDZc#qb%(LrYxqF#%Hv~4h1}^QGY~3!Anq=ndp4Z z>0S+5>bMCJ?KsV)8Fn%)L8gV}7+FlTmMLT)z|1yE;xnU1n`!v;wTO(UF^!cE(`2Kp z%W3(vI=It1C%gmZEKr4GHB{{ViU7skD${S+Er!dn%$g9j|C5K2b{z9<4}M zA{rFBQ3Bd`wN+N5#|N&b^CY@&65GH>$TgUrRGbE2USf4e3zqRRBGl)^Q6wg3Wbj$O z$Gl8WOh-S35+Y0STCT*=yF2QBrMnfWxznP$VT~G;`j)*N9fVbt>r24 z;)hl}+x$zHQq6e|Mi4GE5{P?CwO3B*_R>v@fT%Hr5sQ)QHEEc`G_CS5c{yn+WVk0a zTA<%JzIjbpOGa&S(BdL7=LKf__$JtJGn4zF`%ZKs^{N$n;E?qjiQ*Z1dY+NaPp=WjA`6PQjc62G=w&z5-js z)jiCIJ=0WobI4*orG@cS6$sZxpMP+02EdjckcWnIuvdXGsj;e2AO>WZ3sXk>GzN8P z&`)$zCjoR$rdO3s^P7U_%P?8I#1asyhORfO#GIFTdoG{7BZ_ZCWi?xwecU5z2GIa}xAZx}G5>m+tUX;UW zu*}^zxrc5}TC{wR`_}K|C|(zQ+3Mygyp_;5d%(AuNqnX>d`X9g%7~;!y($H!MTwf@ zlR}#j6PJl%5(3LK4;4`kyTxF1*1uYSr5>#AX)@w7V6$|b;~`4L9V1>hMN3s>OYKbh zF!XTKq7lbljdd%JgjHfjHP3ng1~mYqw+uaG7@W%W{IVM66d&0m!_1-Ethw6t-OZUY zgPhlIRKWGJKyJpnl$RN62{tj7)`9*oUu*BwmEJ^(H#C`5fLnTkau;u3eK`31!F(^# zM$3U6SEo=520phT{oVJCIFq6j8M>RVyI)}3szE-}BxZAUEtNy+GFTnoJ%DN6atV)O zkYWX5-)2Xt6Rcf>HUJveowah{qG|xA&NpGc&#y7<+jF;6oOjr<`ybM*M)XXfj}u%y zud$A5l(5ej$my^s;0C3#75>x6z|^D$V6dn_;sj8Wp8vga*-c8?eKC_f_+b{=m#ov% zXZnm|xSxT27J@~TnatCXNnBG0<>uu|Ep8junifYA;H$7)gh{d5oD@$!C+_pnZcxI5UGbD8VVbPWV$lWca= zyds;f9eEQ7{>P4G2}04Wj5jL0u#K5lQlC9hAi{r#=_*Y_GZV9(I3;&ee5Yxd?5y%B zp9@UrZDU7%u+#IcHXoWXz4VQHcSWjz7B7LWrSJ&a4^byq9``xHfx6qbwzHFA-fo`P z!EH@WeuOP(vs-FU|DX8^O~%Dlb!nMq@(kAbtFSK#EOgd5oER_fVXm3vKuR>INP+b^ z4Py3}?qJJrW{bs=?%T(0U*+C>HhI*m+r!1nyrMpPViNxhNbqG`779CvuHl>A0gfnR|^`zDAs$tt_f*zwkJFKQqSO-fUAJIF7YL{WO z*wrb4aWBVLTj5u=ZnQsj^m6JUP65sIhn&~JNQFd?ZFLX-B>4ilaCJ8&@Z6`b&u;jw z4F=fWJgvR|6l>)NC26P0=1yvhuYPI9-5?vw-Kt5Sdr-WZ%yfF9=%2J0$E2NZOuzK* zX!w(%SIzi~A(z^?>6y>)Rp^!7u$!j!eE8kvB`i19!0nah~L>nT1@1L zgS@GGHabfE^v2gDmg5%|kx-t`+p|9QF~nIga7BoH!Z(i#F@Hw;gF)2?G~M!Vu=uIg zzD+-+yioni=GClCd5c}W!Q_axNF)I*ny9;DKfh8i%oqRQ537fwqb&E6u@&5<7MSnL zJ$ezPw@IU*SL9o-fSeA2d55@d65L;V&D4aPfcwKy3Hm?wpS@Z#KR<0m0^#bE-HuCn zk2{%bwzn9VxDRh54M*h?iN|*w3XTQMpjKcJ905f=si*OGQYcMxorr6>@Uk?=C%@4R zN380XJMzg$rnCE7o2%#gC|ib~TmJiIk&n7bB2FmRHLMvvv{BDA6&YSch6qrsXHk}K zUPV%g`#3l!E>t{(+B~Iufsrs_wBqY?R$hx~6UQ_aZP7Yu8~LBpkH314D*YRO^z&!(Oa1OdlNAC~gF^pVh{@v4Dm?Zn-Q?`jH?3&N1A=yznyZzY{kX~g+N$ZZXUFU*IsS&VYuGY$7L3+ z>TEWrSbTV#`uK|)=}C?&9*sF*XdF%0Y3~z%Al;wK9_!f}x25J7nS3B<`ssta=8k)? zW|QxOPQ?lGR(xrDEgm`P*EUwAYwyZ#bS!TPu!!hxe|pK{{Ziw)JKg8jTnyB-y!RNW zH1*6L?YT{spLlW-^x}qyS%w-ZVlx{fW>^uYib6`%;ZsAU`{NJq>v$IXt6b)Iebz>| zM1Nht^2wKnE&6jJj%^OZT5O#vhPrSzRGdk8u}XINg+ipa%!Tdz<+W<~KU{*VXFv+! z{otj;rRrxr`_>)rww|vtIJn?@`BX_RaoUsgq0+DolDKXEv2^J#!11)%pNro`VafKN{w{pGBNv$_ zah$AD9yi!E!`@)O<#yV-fL!($-SGD5JjA|#G+%;95M9?Up=&d*3JoEko z@0rPDCNueAcQ!lObI#|w5(958+$vG+AKD%`7fjai)Q%&o?XCy)OJWsVWi(H;ndecP zm@#ab!K`w%p&_%UwEw<&%F^3KsoJt`GRcJqYW6NV7A&5*7IgUi`=yQ=|2?zI?j zT@HrWKN0$#`buW85?{Mj3Cl_$X7OEj%`Y>~cL@R$-7LPb)dou6&aU-A)@2G}UI2C2Cd3ERGhOD&e%&2-79f=xFDv6m*ghxS$pgpRw6 zt9(xB?+HBCYcQnb-ES}#FS$o2aDkO>&ZgVFP&q>B&XTnBFGT0iI%C`w3+&ds_kBXI z_hXwlM%`LB&ZrQH&0JNY^nRheL+g2F%;JlZi2tNsl&ZPB(C#&T&5T=`2tj#A6si)9 zla(%J(PXXfQVu2usuC@`=VlyNHwKIftZNZx4lnk6bUC=IUkReDHy!9B-+r57e}3i2 zEuRAF=!7HMbZCoxkzysh1>LP}Jzw~2Vu^EjkMbjSY>z>AJS5+GXk`wr`9-_Pz-frv zRJ1%~(eq_@A-eyRnrYUg+m1|<{(&>Ia$~~b38rDLmt(p@)ntxpW^j%Jqf)t282HWE zwM*hlrqYwh$?J~Vl`bj z`Fq3Sf9Vbi)pl7&eb(|!YgCjzQ1+NVQ87*(7@-O|Q=)uQACsM~(9P{Li5e(O!iFl$ zZ2HCXNlnM9s-tH_!Mg;GloJD@0@kG&(E_OHr2l#4zF#v7-*3vqmgsOe>?)IdJDm5* zjN&MHtKaM`DL)vSuH@-CDA_-D!G9@D>0A$-^qG`*k_n05p`sR-Ny5eVbel{pW$3i0 zo)4YiDSj0>pX~7=D7S|yJE^O=@Ky4Ry9$aYHm~SZiJ7S@cCs<*F4Qh<4#lX*i7NGc z+Z);i>llapLeJY)5NEuxS%etg>Jl{@^RPt_s(hp?!d3aep5`70eNj(@EePIy>TaC! zI?%83%D6z>^z)2NacQ;)aWJb@SN2rz4t+w(PiUjaEqp3<@A#G%w?t7ch)3w{|B+wp zjJ%YVjIi1Gl$R2JQ&Hw!C1!dw|0qvQ>3B^)Vw#vn72dy|cz*F|Td*0E+3iyyCu^}) zVnL8qSg-xjJTk;{FOD=@EH~M!en9!op1Yas447#hwv(J0%DI3I7qM}ePPSP#+H>30 zOdVCjV)s8R)CHhlwN(Jy&+xIQG2P8OzFkP!OvO*jML%1hSo-tR?mg3HyQ5|h);(SL z0PS>bh2QwtFni2F7MmKP0@rs;JGzR@QgG!Uf*dxVU+FOAjmUdijqgnzK`mMy!#l_^l$Zo>(56g?>K67H6qErm3VODbFF#? z%;X3RUrg+JSTDHXuIEVd<#lUjvhJn|Ri)^xp3r4G9}l*x#^@4UGm^el;Hi`&%_(v7 zzUqDchw`Uw8QfIWo+5~|5Twzc)NjdB!Q0jM?<{5pT6cidjq5>(Gya{up58fT{;Aem z6^>H*-*VLHImJRU(#pykZNyO_nDC%Vl@z+D)bx=bOxOO7$NhiIejQPryB2eIWzOKX zBjyy~q9I0;`9wS7-00mzY|k|-(NFXt9#~1e0wzwN;*YVQ7k5{%Ywl>Hw95O;hgL2n zgxI4VQ{YOGK!8G0gjmq&yUV6CBl7I=%8x$wO1G*bjI-Afhiagv9i#cl*&rheeu+k9 zi}E&MiGnbxORJBb6U5HU{)g+RzZ{cgcqWxZE6gD2&CiEy)m?61@b*xBfS!NAJYX?~ zatkp0868UKmyot{q}z6CrizzW9`jVptw4<_a|~=2$2e@0m+46!O!1aP>i4lEqPcrD zB%Cs&t3dgtOMKaOu9Jp6??T)qRm23>#8iFseS%b_a(7Ljf>Rd)_O5gK6g^!p$vxjl zDNU-SW?*s`)rQRo91bV8Gum2t8plPsjv>f3Ga7&qEEg;0xAQpU;sO9|*#6Nor^np~(HbswS zIS|aK9E=vHdS$9fX)63Kd1J;PsVRb6ie_&~9FbGCx*MFSCfA7sCwGaVmDuFwbfo|b zwmKC?^OE)g=>>#h~)X+|YVTmHTdR7YG)PsmrkiIuc7YQu4k?0GB0??I@;s?cet ztcs(Ygy)>gd!qg`d;9{^le@l$VMJhj#~lkhX|%Qzw~w$$)K5&qRmI@Unw_@5xc6k- z4JntCX?W5SK8*_(IqZ4O^KzFWlt~dDcRKX?nmsm5Pm4URZy^q#%RDCfer!Uk*HV#fj8bDzC80GJ5}vK^W^8jG(IK`ThOaUyJt z@A!_GS)p#L=VP{&i+ODee{R z6OvsH*GL?qVAteSPD>HKaP>9-tg{G$`sn%{*kjtG3RWS6L)_!OUjWCrU~@7wV4Cg# zVs;kZ+T*w+#J90RlP^KV-NX(0g1Ai0Kq!@>(uE;anPdN$RAeZE&$IjLU8)tkS6etM8Y@rdXYIjE+gM z=ahd4>hhK!A>@~on_K(_fb&S^92_K7S4o7)7dO+2Lz1s}$`@;9TmCz6;R7?dA3)dw zh^;m*pwT%hkTQpYY{A7z~m)!P8k%!IH235{-aL${M7Z&%<{RbAi3lF&dPI%$vlx6T-@^_+p6*HF3LO)Hz-{zJb zecaDj4OKtbZ+_!eg~_Fx_%%uWFG81QK)K!ZT+322^%kWoTW0O|-Ilh7C9Au- zIY-SUogJ6yODx2a3UR*)Hj4*7!Xjh5Jx@>|r&KUeGzsQSxJd;(G*saZ7jj*I3ZrAX znXNic3nti?2Yw`DxCM$?G%bO5LlfYJ!>wmX|&D7qt!Z-b{4f|Z%Sc~b|(a?-;=(MWuPH7#Cs&7=N zA9z#u;CEfVs6O^!Yb2)O7$_ASd_iSL+t$bShU;y53v?f<`?b^p0n1uwN}I8`A| z=gyf|5|Q`Ly(V4HKDzNuaqf2Jdj`Jxx$27Orp`=Ty>(^2N7XX=QH}>S*zp3vDojg- z=)yujVya-8ZrNPf)doao(crIA5u6z0gixiM;Zb0Zx|E5YV8Tu@VEJnIZQUhzY?y8^ zp)GWz-i)&6KEi3T@9dbAc50{?K;J}xr?D`{=!no_bXJO7L?`x}09AT>hnv8{n2#XO zq(n5b8M^7q6#O!WkM7{3srU^pe)qRpa3lZ7bG}q`YmcZFb*=MAT_-A+kNne#u1Als z_#47b8Ko|j4w;xG{DP=$YlU}ln&z{?!0(yayFkpm>v4`TfdBC3rjp@3(g42T-?njb zrKWUu{=o~Pl-#6SXBl!>-38n@qrJ9Gul70VJ%D@0Xs>>bKNQ$q@6ld=u)QZ8_uauI zHtbLv7ilYSJH^*{-Ne}CP2eVjt9vEf`A8-?>J$qx!EpkE&|i3P7kZ)?AlS^gXw{6X zRzda?2|NlkB?{`|0U14dvbuBII2olFOm^{+gwH~a=>%2&Ar%(HI+h$A1D0_}26t+^ zimEr5;A0gRVS4FgPb_-qxy%s%-f}HUZ)ll=M_t1!M-TPtbv1B0d$^co8g_*LoOixU z)w8p|9=#=>`{MQ67x-M6kITs`0B%uepZ2+zdbyh%(7horTq1@>f}^ChewJJv?VwZg0KaL|uv=qCQyZJ_#U5^8(1c(IxANPy|) z=@rG}t5q;&XR(WH+-ttW62%nNjicn@QQb^juct}m3`Bgr1;Ks%s{D^?0OhJNG5Ru%@7=6SD>&9m;eydhk z{{A7_oQ$ufK^us#KM4>l5&Fl0@{bSE1H|&bSIa-=l)pQQj1!}2L4D`wkoSCikpOy` zhQCLLg@(3Ih{rw@l+AMQ3YoB%1Ak1!EdV%bl+RukXoD_he*nNFeok-X_e$?D7t9l3 zl;~)KKcfp2T;*8%B3A$&HI%cu_>qAGlK>+jYJV^3kkaLVKkcgRdh67845hxSdmVgw zZc-h6e8Tp6uKScU7mPD-iDN!9BK#N`r{3ULTjdta=EgWGn8tgovo(K}R;Cr^yya(Y z&|C^V9lw%ofBYEV`i@`caDVLgJ{G?>SX<_I7Cy*V<}FX}I}NjyKxzAq#P)J8I zepZA|4H|lP2Oq(Ldq2UaHcZtrr_!!Z{k@7Wf4uL+An6$&e~SWcq~LGe`K9dNLgrEiy){zF_dFXorD0mb|=ipE0W9!I>zku~xku%bo2Zg3KE!xVzeAG81pvwnMd7 z)zT_(6;Xwrz3pS%06ocz{+;B7XX!VOCK&ZZ&37`ixXrAlfA_sGq}rmf?&nRt-itwl zfyb8u_FOX1!l~=j*>m5k2FU|?MHBqX4_kPz;kSas+v#r~9>3|ZIQ;HG>JJDMqVF~`on#IwjXKGNUW)0HYq|5OSXO+~WTgZ~_Btz`EONlBrm1&9 zz(h(m@?g24ql@Bm&{0j!iN;i(7T|foIJm4gyXVGkb=8 z_m*9Ez80aJzEE*{TTpzAr+gAHBP84cf^RFhM71$k+BH@Vjf(A*$eNTfdUd(#EZzBv z+qN=g$to5Kt^5^g#35z;!Ycs{w06M0#um1Qg*L#WCzo%xv%s@s~st*{N&B7)*Gi~ zQX(r;$21kgI7e??-o@hdT=F|Nr0F_+NJK7-J%5$YxpldI5w>r+u$xSdPzkAa%x=>- z9yazQqr^raq8(T5hZ>a`7dFlp8{Jnq!htM%S4bPpL&u1xmQf1YDNCandbTq&VX52G z+EeA?nnbLkxF$yBSW5gijj@z-hW%X2bDdJepz4Rd^8L%-9ONU0UOY^d4DwOQWLehU z3ELNOC`lSlUrwe;?4cGq%DScSTjKi}$%iJRt{tdL*dOzYTYi*N(k;KdEPuOv2kRE? zVzTootkj^D+kdhrxhk~^lNX%Ll_fs@>MIMVo;HK4vxw)g+G?^ZT zIj@l{OiEXhZ6-dVEOp{y(v?O-2-)hd@{+Xc6)g_VnuQ>5vH(Ek7Ze5nYk(dgu4-^D zC<_7x!x2af1dK(&uow&;FM*e#Na8S3B%G9_q%;a9D=RNA3sscFDN0K!%1bLMsw=9> zE2{5MQdCskrmnV4QC(eKQ$a>c?AB6K($Uh~VSGqe5~HhYqNk^4plCqZY5c#k@HElI zne6459O0VkYMC4Bn!D~Z_c&>>r`poY%-TtlYHdvoYILM}IhpA?``&Z0Hg$Edc5`v@ z-0i#D&|vqzxIKOsy!ROTpb0*)^}gadR%Fn~u-`B(cNZf$~5djAd1ggjehWG_# z-#wJrALcI?7L*fy@c5B^?hyy~MTP}LMnoJvnsAg^5M4HPJhASiSdJ4Df3jdG_V}LI z-1`aP0f|xJi5V9Yv+g9tA5BV3OiC_JPR=@=9{*48R>tYuSqV{DDFs1DjW`ir@RMc1@O5UM?@2;BMEyRaRGB6klCde7m~3rZ}gjs`SpC*4k_GwG}~i zb+z?Pg8B!`cdz8#+u?SsD2wzl53*MA;XUu|Fd?@@P4 zN5`X%=h08>clUHZ>bqau_o%h6<5_=y-#}~4z`(14$=`!5%)y1f&xSugfBt-E?cW#o zJYTfd506fayeJwQ;f;5EcsbPn>LK*i#Pe4#U%l>Ydp-4Q^0~+4)WX!D@zm0vsUKT! z1~6|vzMg)hG(G)tT3qvs??+F}O;66fU6_AeHUGa_p5Oel@Op6ZDr7^>$ zg{h^*rRBB7&u`W~f1Y0v*V^Bezgw%{S60{7zN~)#I`-o0yzbWp@2{U;%Lq0g?l-LHO8W)ZjicJTh~ksGvhWvJ}%Td_svXh6pi()5Gm zJ<|u?mRyQ(J*l!6KeVH{?)5!J_xb-fS&TetB<0_-@ZcEzM#AR>{hKcBwk_p7&kP$r zJY_*J%ARe!nR|+PO)bABW~s;Wz7C&N@U44)Im`IiZr96e3nN^I3(ET6b=r>Bcqf*b zC4O%%AMs5rYPD)veO>iDI3OK~?uHJn%+Ih#{)%Rbjv&hBM^ zQ_@EBMRUcVaEUz4iWUM5j$>oU#u?lhGM2SkVq7LU%LRoFTP-4`@TM0I ze8QRfx5s;3>3h+wZuP1oQK``4xSVEbbnB;uGR0jE7#EF-bT4bUAd>=1=Jose_Cf*j ziicl-PIbvrNhm(&GV+{ve5%^m73#TTH7Pyc`5p6Pu5tOfWvt0#i@mkRBk}i- zl+17;)vl?-TX8Q;B@)u{F7N-i>3FrRE`PqqKl18@F<+3j+uAG5c#(Q)QYCwv;5P`d zZDPzDu046}N3r|F+h-Eq-_HK>vYuf$som_%zckSlJ_Qen+7=L}*s=YYZb;Opg}Rfm z<)ZXw^5An=d+t)a#jD*>9VMtq=yk-|UM}We;;jtT<1L0i&r})6(M;b<8>$ zv#*2Nw{zb-(Rkasq8d|=mt}0WNB@{&*+u1>uZphX&8@!JY6%tGMU!+t1MXsslW=f#7x{uUDFF+=gEZ*GAaB76TJQ zT}W3f3#y78N{2$K)KYu&r#se$t-fi0>-BEM9%_4Vk3kvHnc{xjEnzyeb{?Gd0_>F^ zaoi_%QQ&e?^${eme%#8WY3)2MaLCx6B!1`_k+`E)h}8PVGaj)r&nE4*=#d(cG+LWT zvr}K!dic)d4=e;?^oWwBS;|0uWI+t%)3fKD$;kStFtyi6Dd!H*mGK@#*w}L}JP3X# zIi9KY=RVSB(i@X%60D4s10eoFE8VKy*;qBGI6+V5L(bal4<@^Spr3I?f-$=TWe16)?@c6hp zp&;Bfa@{>>x6(~MO;hUXd0FCj6;WglX2ClTkpnTf@f)VkGAsy}s=^Ov^C43b7tgnR zFB3|s4csMpylkL=^%(qRs1Z2Te2Mp{rjc9`QY@m_|W}A z%D4$jQuQ|7yiJsM??8$nA%xg1KT&^=wlpo_ca^>EDf2Fi1bVm0$Y_oTdJ*-cQ{nrY zedM2|@RLd7DqSHr4ZglpcgmeGeKl088g}_!whLA!1ljWhq0-0n8oC&F3Dx5A$>8ro z44K{G>^S~+*6Wao?ASAm@szc+J_18Z0V28ejT~SM8vl-*o98pTJbPo6jJI zeNGnwn|!%SaosaX52X)1ZHrUPHI zqdb0Tz@zvzSH6B}|D)e}mYF6u8jku0>?tL`d3iUx^jXn+R1(z|N9orm zSG8R78EpLl1tI<4_nYtC&hS8=!dEOG?fhM*_PX>FA+<*9iH!bZn)=Sjl_LuO$+Yqc zjytl=4>-zxiz~l*{cFC9bodePH@)v&E}bre=peSJmV0w6wX=wCKQ`zk_nX_Tl}`U5 ze~M{YLmK6je~r6AA%wnw8O4`4kgV>o^6f6=FS#qd`y?pbJ#kfe9cS-N_Pf{q>cQ|S za;g&LOHKQMt(&j!mi=9k-e&#g_3)3ggAS`;6#)wh{@EIrFLRzYoww(sY+!Vkgv|~1 zj@$bAsr<+H5*FW@JQE@xch^fFJ%ftU^v=94NRw$OkyMOQE$I5{s0=~>JDt%?jw3%I zpHFq6_Nd^;ub6%@^6XfFq?hhTk~H6w+42|97kv@^YaC@$L6Q|17M2LH6AO0e*rQBH6b;F&IQxwOqtkI>C%Qc)u#XHCn_3FZ(=~n0 zo<9|zAEL6VC0WMX^PVo1X@n{xChP>^L}UnBfSwg$z;sDH7C;8T`V2su5lCx7*bD!; zM8|k=p@;bJ*IX!@p0u(BAS`&8Zyf9%AT1t%wF89dj~SY(XmW{vh^r*W{tU%1)iO31 z+?i?E7yyoey`&(`zM)H@xF7-OGDV`93o`u%5)ZP%enKN@n3p_!D2+Wv!FXyDzX_0r zmu=z!s1qHU#5;R~pELe3J?Ap|jrKWx!z78eJ+eQJgJ+P_Aj!96q!vXiQN>YcE+&O| zOA0YS0IK0je6f(!KPB_uH)u2+H|>YmLssDvp;4mkD=d^93#)J$7B58509XwVUly51 zY)THDla#f0za8cdwZq(@VEDqkTk1GAizqX8kTS`F#AIrwhF&%ywa!}Tbi4?icg+Zl|?ClmS^Ot2m50aEHs7_puY zoS_h`NftSb`|j6kRYm?_4k>3*CV54yevd8$Tq06-i5#HcMwsEC6F(Ubp+1j`EW z?F=A9h+n0_F+_Z+Kd{G`aGwMFWSAKH@azf;kswcq=fc*zaS<%UEFH6-g?P=;nIPl8 z5ea)3sDF&e3?A$)f^bRzAu15)H1--F?+;WS<-*R}!S_&dpoLi5`2vYT^lrf!g!2B=5(OlR|B3j%HonT;XMgM#j zhy&k#WlY>@0MO6Cg>D@v#xSH>=l~2ws1!j*X$gZI*n@8TayneZ!$;F# z4YZ77g6x7xXcmC)q@NYhG4|xrx8{UBou zw|<6>uDq0@X#02I!lBY45rC58U;6}ze;P?g7(fNrn<(yH&>&bkRALsQ!~ozFf+@$c zj{@ZR+DeEIfy_J73kGoj5Un1d6|Yj@AfRI5m&pSCV$=hC2_0e&puX|p<;&Y0$k2Q` zK9qT5aEk`F7eeh>@aF&_gA3a^c=m-XG@pkbBt!R*iAy|$lMw18M9gw8w}_x_)>rkM ziJk!Zs|%6MxxbqNJx6vQS=KanC)%8sQ5uw#NozQ2UDa}@@O^11DmBjuovsH$hx-$) zf1a_fKpUVGI~H{p5{z2#gCC~B!$nUWWu;!yalz(^)3SvB z@$G&jvj9=Uy{F7t?Cumf78#)ou9cNEQ?AIAe zfzBqIni1y(&~6>Vow}3^7i&#V@(~yz^k+i3OCrg=TY^+KYyHGw7WK}Z*vX?FsR&=K zYwaUGIAYxpdz!dApw%EoxxzwxwN~x=M&|kvR|Uy8WfO^>iIAD(06StvVv0$-U#4tJ z_~ouMiiFP;92A?v)Jid_PN1%A@BeuYU6~Nxq4mx$F}Q<#c1wIJ(mI`}5(UvcS7T~c ztck}?%k&X@i)VW+KssS2NdYp?c1IKEJ)X^9=w0?PMQEum=t^CGK>n{H0VmOtozqL~t2!bjIQq<>tROBi1y1dQlSZQJCC{88p8Pp7dPZ`W zo}g&9=W1>#SzjVAPzDq7+s1s?74&9y7vZ)Mp&P)8)x-^6*gxUKN`_IY+nYZ@vm@Xn+!hw9oRs|e{IHg^aF60_`5;a^K!;&bgDP#Z6APb!bx@S z3;(*xHj@tuV1@EgeJOkivH{vU5@g94Ym4&O?fF{qf;>kBb_GQGU|J}}|kZ;cQ7 zsrC9N=2T~}l{&NX|HO8)Y>a~0hnDoo;LNS^rL4P4`h0NCd(GTKOW7xuQj`M= z&waXZS3b@Fb&0dMZV09FAr5pXS9#fixm=;V>_lF^mA`xk554oB=53qL&H2lh&wb7V zFSq0`-#1vU8~%JB550SCna29uhnK1Ra-B*h4&s-;g)iSxULCVpb)Z8k@Sn%dt-Mi| zYJalw+=kGO|5AZJOl7Q&o%k|dzHG|-GLQc(Cb-go&(S_qQxW9zp|78nzy3V5GXG@N z2ljQ{fY>|!W!>QG(_YA=^6JuV2$e6w9fp|DKRXGa&3K#WSbKY>y%!nkNruYY_$Kr9 ztGD#>ip`h#_pZI6BL2%SCaf=7_eAP@AltUIznj>~n25e0N4|8zzF8D})i3z^Q+eH( zei1e^uok%HI&#WZ1kv9EvE2jtaqjDn;q|XJ5aYw^ZP(TVK1g_-T%`%Vf5m?jz5mW{ z`2zk1CKiDA5WejsZg_nFYd+eDJuDeY_$Ic461UduKCIiae(c`*X$t&2^ZAU6?a$1e z#ND<$k~J}8&yP+G@J`|{9nR04qMyZEKlM3fQLp7hLb)+DLiwr9PKn0vYd;~uzYBJP z6^Xx88NV+7^K19cP3R2jG!1#0f=a$(cUt$i-ni?9r<-b-$T}Xn%NFrI9lDbOx%c7s zy$|3uPFd?GY|BxJ?s{b2an=83ydHxHhqktxxlK>5u5Kl|dxxZ_0uTNhL)6-HQvd1i1#c z)FY&{)?tyR8xjILV)I$ha)|xJvItu!&~zFhTYYbSxl2${p__JkXKcU@r_)Q(m8QoP z%o3u!o#mS^*@Z#X!=~U3ka;1rp_wSgYo9Ml5)^Cmd51W%uX4a=vYFtWw()gv6EjQ-c%Y5dL zt^V9McF!)STvE8?Yov--Vc9w3K9Y+GCf6o$Xo`K2|3tos<91F`9N6ZT^o-CJjw>o9m+7@*esTFD_JgT&ZxqR7NhXx}3b!t?m08 zIN11PlX4nea_k%iGrS#@X5O+CjHr#U;t^o$v&)PQ|IeB+8S z&rn26DJX-a@<{`!9pW8xpx{Re2`)D`prO)KG6?Gjjql4;ihZRKp_uS7@3l+P{6^uy ze0(Qt;_wke$H&RS0}*OYRR>T_{XFmbefIo{FhZP9aU815Z?LX-G5My8U#tvcb-ey;^q)mShiB zkoGh`Yn?MeqAa(r;GdRNZgYc8tq4YLrQg&-0Wrsd3aV(0SF zhSzd@oHxqdGSn$wDDdkB>IKF=>Xe35q_dDzz~_4_2Vmqv2apz8B`PK(fXnxu{k6G) z9~HiqWEh?4PP=iKH1`u>`zms$gi%Cy%SY1To(oOJ6x6ckSxh zUClRCn&sVeuDSZ+g5%zN8ca!gSim`oJ#`?@eed4h&JkyuCg;%bz5`^oIkSbFt;>x% z=w$Sa*kigwMWl^UC%#Tj0x~g|?J$~JSuVx8veOipeeoU8Wf?Lc7MXdbiqJCZJzKZ$ zvDijeKBXE_xFsXeQS=LyFEMs|k9}d?+&}$GvgX9zV>c@!EIV^ZUsht2XK1wt4dwfo9)ai!{D5{)O2`IGo`JdYW6{rUb5abHBQ<}EdB=0c}tSBH)l)bql_Ll$ih zt{QBxbr&!!5>E}$9#}o;7>uG1=}8$|yX!8JTD>;g(n`rqT4upv`;`w!mT!rmg~<|O z9+BYwdJ`jiGthZ=Im-dMP1EJ{+^Qe0{zEP*P-QX*z58>-?DoD&lJfoI z*LA(wlth>o5_H3lI3?G2s+;A7G~RD_XifEgPy5^_s*ymfS9fUC-v)%MF+)HjZGEiI z0F+pnidXL)FK-2?#w-ptlA)~NYHF{_U9RM(s5RYPWPLBM3PLftH+}yOUPBq1{@>aVF)tOE|R&vfU8}&XoXz^Y- zi4gc6QlYtPub$1XToxAs{pa6Tl#{QY`}r+@wRtq@vGr6Sd+pu(6zQ|8cJkn{+cwZ1 zTf1+c&TWvgm9AWwwziMA1g|fDhdRkoYa94LiRw8~uSu%O5o*Ep3;I8|30N@pT@t$E z(f2i=^AE{q3;!n&LKLl2-ErMWBpww2ZoGyY=-epx3f)NPkkKTBvD;ez*jql0 zvt$G51NMgDrHG#A8h=Z(lqMf`rbGRBPXM{p#DUZubAX)%dUg%iXI*%Rf>0rXyh;#S z{*Pfu8d3n9yaenlfys#ZGYm_Dg(+s>sr(sp4JN|tXjX7`Mgk2<;-|-#Ji0}r?OO*5 zBvLPuU5B#~h;C7Gv~Fa@qFL5_C( zAj{`Mb8-!U5u`;?+W*!8h_?RnHYehQ?#t7(N9XG<9)L)aVfqvhen703VqyJlTBmLI zm9e00wy2uaATB$Zo8D1Ng|46!haB>jSohS`Zcsg%*88Z@ozxDem!BQ|;;&8Y-Sr^- z_RAs`exN0p0#c7uRR7)E6{WM~a8JR59eNExW~G0rYd#u(=kQo__(kW>paN1XQvcM6Pel!hfwN$Fb*|l>M>^=Y(%R$Xb5DJqJdq%nf4?t5=r2$io z%{1l!UTd;=OK~#eSu7zT%;jVcRlx*A*aRS(;27<6`4X@glVBkrPz3B?0n3OFHQ7va zXIxguW@%1BoR-pvTvHMaW?z%4!~*SDN|WIL`f}hMlMq|3Si#gF_DM-jqEee3tc0me zRoVAFhatTkNM4Cwkz?ktzrw1a<(S1#ZNFkD!0)m zuPxY)oBYW)eVqFwSx#1c)ta(o182f0Ky}?R%CAQ1h(hXCJm2~6nCHzlKa?vdU@|>S zq^8Ma$MHZ<4WY?N;D8!76^RIBAY5yp$u+PzM!LghDhbIaaDiB9gdKxLAg0ALGo0Gk z*#`@qdt2}vctj-Na+!6A1s97k!~CH!l&gh;48Kh_k_!%40!O&Q|3-o^{s21C)-Pip zTRZKLAk8Tmpkc%`1&b^_MtT%46;qNmr^427eO17i*z>$Q@wn|!(J1h? z!krGO?9AL*f{5RQx>K^;S#T#>MwA>%dgdAFGE3b*(~X6=EMSo$eUBe}n{d|q^vl=D z8_sgaZEFKw%Wc@U1h)9xXUSG(pO*ID67EAAw2TH`%TjxR?ER>!@#mHw0J&0{m4`+J zxg#3FG~seL8NNWC>G_v&`yrF8FP0E%LE14Q*oKisLYiuEVM9o;X$?F`2)Ecwqt>Kb zwSmL9>7+<7Ir8c5I#BZF)VA}GWLLPgP^NHY2u~~5-~%WcB-#RQumq-RUu7(17zn`B zB$%BxEAd*EDLFmaHA{t(suu}Lehc^#gV&7#DiWSdPIqHu{mJ#(*H#hhpRPLzQ{}Vn z2B$N%*+IJK$G>e8&$c`|&(At_Vn54`hb(pvDx45wjRiVOiv{c5x6s za#We)ZgR){-bkaiPH))m3!PB?@>>2>jp=UWzK=dHTO9MqKF(erg{vRS856Hwn*Fr* z&vW&;{96NF0<)+w+zv*O8CgdsvP-+3+~NQuZ&`7~D)fN93KCqF!wywUQ~v}|OAtGS zaEBVk{!OS2Ez63MdPro8p4{sp1;z89+i!b;@t*iNG3t9iOo5ddyUBLq!UwaxylU7? zTKZ82gu0aOmxQpoT)V57NPOdP? zKRtOeBUt*9bxD@9+^p>JriTVZ_75+M5o%9%pWb^c;~bMoZ9Qr~?{;1fT+OZcm51 zsT^>qgu5uDx~t#}Le&3FwO+10FWJELKn(c)yCe85aak@15(@!HkR+HUvofAJ9}Q*{gD8=EJuhED)KTXq>)D5LdfLd&-;I*r2irfX^@u9dCG1=4cmAciUpUikWQN7~QuZ>25I z)*rY^badRT&AXOloSABzedAkw$L@sQyx&M&C>#tu-52ri9CSWy>o+!5&PIs`NlifG z%x7#9Ku~}<1$Y52#kYWX8n`YUL;`*2!tdA);7@w+MKl+y=(DxEifICY^7g>(X}C&o z3kigIgY}KTqK6*Idw!rr%{t? z;J#(x@wniXj$!CZ;A~28andp!51%1!-@`mR}b^;NapPf95uI_hb}V!-A=ZWt9@vot+E&xAy;4FkhVWZ}Jup z%lv^4n%lp4ACm^RoQ6L0=vl(*zJ7WSj`?THY0xz5+G#sR9Bk8G)Q&%K%E_JSp#_Cc zcJ2N8uyFiN7P1#G`P`CS_>C_pyuS@+fhns`Lau*{*aDz8L*ME-CC$>*QUlw_N-{($ zA+#d@+WSVrLn4tozy7+)jBXOVpawDJbQlDIB9ob1iBg6_3=gapaSts*A}G4PCk?!x z%P+v55~-&71^U#rEZW$L4%eA}mPV|yKbGCzO$hNIDG^9p2c}R6sQv27ztR_2mx^Hs3)qgc{b$BSwTk=Yf-&zoEGp_TKw0JAZrg2VTp=;WQ(iLd|s*zxJd z71W&;{w8DtnP75xU>pL$b3~wlW>Ux{cucB@*2iYXURc%)bOl&$5|Ytmldq}4oJt)P z+;(jcrN}BJCAQBPRk%m9+5>rQ7-|?zlR%PkkHY-6+|4y)({kt!7Y`1=f{o^eSy*Or zN=&Iokwz%sb!{6zhQM)!RLJI9LcLPx?OJ$A>yF@KYKVOtMf5j4&t zBn?~%vYg9pTaH{g8NOO(N1<*NXxEiCGYzQ% zb4`-Egx2JLW!!sd?}o$@kMNC&?N$aEfpl{*jfaX931o}D47d?2pBF!tFps53LhR?_ z8M)-v0a+lc?yGd6z$iL?mb;~U3G4jD1E+j>kWH2DDML}u-^>_BS^)T zqI1_qQ~&+XODi!haJ)(m%F&?x4I1Xr{vID+qK_=I+}!ROuu(~9njH#R{9=-&lp+VX zK{mZk(&f5x6qi71Pf~;ECwRA&=4YMK3mtOeWh&%<4&(jkBt;s}=y?PEJrrYic(i>p zAIx`2>4?yS>6^)qu?OXwG4RCUFWs&i3>0_Wo{OML{Wn0#^2Hi>M4yQ%jjaD)Beqny z4N)GmP^u66x+aj71VA`Vxo1VWl z(mWqu#bD`J77`zUp z)pKagm%w9v2+c#tv#Nl__X;ip(OMX?SLy`?ZHm$Jr> z+1y=0+|TsC5do6q`q0UEE0`4s+64k7p(!BDbQVBbYZI|&p6}seSFc zc^8?lFU1YOu-wr~zolAK(f};?v;Xp=r4f&$!bK#Z5f}}Z&@V(vJ3)McPIEoOmqR(j z?1bP~PhZ6frfs$PUVcr&s{1b?`W;Fn@d?Brc~hEIxeA4J*^e7P@v%MkR$TdPu(@)D z>lhw_oU+ci?+}1m*3oUd)ODdKqIq1oZ6p-Czm-o?Rv5#w?t0#KElqEpu$Y@a;F90U z-vga^@U^2iaH(6_(dJ15JN7+Q}a8|U=re!K&%<@&TY@H-v% z6PLPIJ#L;d6PP5`WV@A*&6rqSzA>12+r4IP#_RO#jfV%mAFP2nPxJI|Y7nd7b@~^l z0~U{JN)NCb7(3!cnz~07jTvWL!*Idr8y?mCVK)gLyxc0kIsC9P|6Irt!m2AbpIkby zdvJ1$nAJOKe#NMOqoot~jF_gh{PAd!Ij21Dt*70!?QMN~WG2S_*7HH)?zT6nGf6A| zeg5>e=d~*rKkP`q_4lhko*gy|79V6z`+eVl`d z9iekMkE^HJhYrX6bLRB#N4#Y$Z#1;DY59K3i0d~sVd00?r|dVnk`&;0`&Cg?L)}c` zvRh2>NYUke|GjTdy8Ha?*s*3@>uCS&TW$K57m5YOX7Ce8-EAz{7F};9`GkBK6PCaf zoUqY}senHNZ?lsC^Z*2aBnxvF422Tp&ttz&>wnVZ!(O}`N253?J4(fcuvcqz^$y)RCAFCt!^x$}3)(Bw}~h(!bZ9?6LKvdz;4F`6p-kYcAEzwU2ghEH5P~yFd8~%~>z+Rbr`dM?$UTvKM zWoAK{zaKpHhBTrFH|H!2WmXGQKU;F9s>fC zr~NZ$wp=%LM_ASOv^kS|)O)lMedOPoa#{DD)T#H74` zowbOHEcKgzMYLRQ&40-Wm3dKner3Zv2VQZat@!vFk0{s&L-_cPlUA&L#2*4VNk#7H zm!0Ys;q!NZqEMO=!aWUz0??>_m;nzr09drB_Bz)=jdBT|v644cT?f0Y`J22rarXW9 zem&^&svP6^*Yh-+e`Uwoq@5(Iw;+E#Ad4mVj`8pR-JR5fD=gwN0Wh@^L{xWUE0sES z){?{pu;ARpX$-*Y1v0M!7UdGUQ1x{FIj@D`Rx?6fNU3b9rgDy-G zK$r!LXCw>_gkV&^)OHg5Y3fB(!}HqMlkl-3Mdf%?u_%(2HhtzjJaxu zMwh(eB1J=-;f9s|H7nlL>(;5ymRRG5T0P?UtTUM-FE6XQ*i?ac`IvX}0>n`v@XL}7 zuIln|g7L_(?DF-7*`lFLP8W*`8#vbGq+^oC3p@dtdsY`q zqh|e&uim)T zlXjk->kaR6WmS7=`RtS)cYll52diG+=;D-gF@7&7y>-#>N||5~<~x6CYi7b8t(5Xr zNeqH824wi&eh(4M(@7u)a6V044g(N#v5c9a;R{ec zFzr7z|GRCfw^N!w(hq9~$SJ)hzX2GaC>{|(nkK)m*c?n$<{1`jxt9lx9^ym~Atr;7 zm7q}EglRG2Wl=YUBM%l0V=7g7LR&wLN)!d^FpE!CNvzN+V`^?U0%jOR@GX=W6sl6q zp-!FW_I_(>LBPC!4|uZ|_;vuFQ7-Ug^PRh49FfeABk&eArYh8gCdmgUnZZ}EIoirs zA@D`()RgM3Mw!_F&vzJVjD~tyOARZbb`3m6rTT6apRV(&C?PG)DyHs?ks~z=QhPNE z+;nMw(GA@yt z7kH+o$d@TI7pWbs`P2P8M`4%qfXbA* z!@0W4%ljNWT-$VQ>hbyW;JtpJT6uvewP5pbOc2IWPW;ymHwQPpvhl634x!}1m|Pfh zsGv%TYW{G1REeW@lYWDLV-X>_GzjTEM>K@ds9uMvP#K^N74Bl*#aXJVeWXMU$SPOY zpfez5OqXW^GIt@*QCy-#oJ+07bv+O;l6Wko>0O?+ujayEnF}s7K>Zh#AKx_KBaGf7 zg1HvTY??q^G>_6-R7rg0H@nSr80w{TGVhc*f;=k&*#oT1zCl78nX&7HdBw}@dm&c6 zI_;v=TLELY!1`j_!)#f9u=F~7>1E>{E2StZyV+0=$o*CN@ zi~6(S_YL}mr$*?vmyyLu&)sZ!-qSK8xWUd6sDb}VGF)Pcg1tu+9Ntq_pt;f?W56>TuN%gC;K3Nlg&9ez55B z>y-7B@^l>T>IQ+U(=Hp>E;Bo|T@=rht2WKy5B^d+h$J*J+;6_suvl`)2x@NNW}=a@ zI(QF_4!P0P`bwy^HQzwE7(7tg}Fl$=O0!mQh1(unt)yY8rmtGg{EWq5MO@QS3x&jcbRFGsV{im>9Sd6)#cq??vq#i zmhm|~?)oq_qw}9V{tHk8s!Fd@#qo#5_4MPm#Il=Unts)Tz%I82n8r%lX3%A4pk_A6 z?14kH9H=8(>e$puYk+Re6nqa)vuakGp?1;YWh`rGya;OBB(tBnX5XDj*U5}xVcsJ( z+VwEcEP-9u==vh{%7?+|0hq;$75dFwst53u&OCPa3o_19kkb_MG|<`oeWB5Egp*_2 zFXe?;o>9g-(_$4hUS&#i;W}ns*D#<}26Qynq^isLMxYJmAsX2OF=2jdWh2bh*k@iL*^TL!#65B zXrm8L;p0a>f9#IHr#kz@@hNuJo}IC2mDrlc`J?tmG1>RA(01hp2epZT1j9P=`JXRa zX5SSYP_E##=r-pNN_T-JYTcc8)ap;1VTnbh#H3lJU#v1P_eU(G@o7p&7%lkrb!bC3 z=MO30^Jqf-G99-15$?t)J{vrCgzz7^T?0IhB)24K<4QH5uVOI4Q9h zR{eBPQK{XF7fa4Dx)12*qOr4I`wB<@5X`(G1|v{TkXj#1=gY5fawPimD@0u<*+L0g zC9uwt7$vE6OErK)eS2jV^DP;?eHqR#Y=WZ`aM}8MbUlO54B@q`jxKfUl0916b7c;` z!Y;p-E?&V`kK8@si@GXH|8Xm}Uuo83vvbmioNM>XBJkQ~2@PHOB#+H76tUK0`|E~)4DTHqu5Y8B-T$xAW7FlQ{Ib8p zQTl&B+&Oa7@Z0zFDz8$5eV)62wf|!qrtr^}?)6{*I#E=HYkbgxo(0J^6b;PE@(*H{ zAZQ(OdKZFfJEc;NriX2I+m8KwYI)+TD%b6e%)XA?zA}5x;(`0DS_rflLm^WYyI&o= zb#l+meec3w1!m6%#^s_&(KUj(@bsr>?1}BquWWmkX}Wy%c-^m!aqA7-9j6Y0|L{MT@=#jgCHjTY&nK95g5e&pYC%KZV}u6lHU z7r4av;R}jYa>eJMGrLDl63vonH<49aMP+@ayST9{I8C-yc1sGa_J6F}8D8|TaaUSd z;Tb{eg>6HNeSUpXoL2rsp4`S%kpaiTj$}7iEiZSzY6a#k>1V>qZNt5J z5Mb)31|GMYBCPJ2&y^haDmTXee&(IGh^sb1E>y-CGzN^th*b%dTMUp>W21X- zbQp{FuF49LYt|>4Wc-;+ugSlCBfsivV>Z%soBAmFY~bnOV@&{wzj8_OuzO#fH2&r` ztLmNSgH~;iH_fta_~O^Qrl<(Itts-;zsPOP1}EYz!*=d3>xLbuEFcP|Aaw|$SrYV> z9)zY?ujipv=Hh$Lul3oPjt`Z0W>k;&oE=Z5G5QRGv>`Yq!z-L6qm$T}B5-oGLL2P64m!Cx#ZwN%?kBuh%d^7&+e?JrMpSXl8Vpf>` z%BVSY<_g9;RBva7yTFH8;ph$_0gP#+oh=#y!E!G^=HPm57@tBREO zORe5&4o=@bP#<;bPMMYM%$=QSOJc#NnP_l9r=SK}mf(tkWBA z7{7_?(9de`@(Zx<=M3uAtg*-1K0g;GQ0U=h)cBAel3`wGTH|i6UI;86;HAl@57vlMf{ar*=1^dvUk6TuRm9rDq0c4IB%O)I`dV$<}qz$D0@xU zDR1@KYyF1L48&4*O)b8B1V#>Lfe>~rhP%)U1h%hU5vK=0bk$hh2e1VH;G*4bYOV>= z73)3%(T`%nnNvsdoT?>;mAz2=3ysYtY%Qv+vH;H%gUti=Wl`DpSoN@@aY}1yzO=Mj z7PvYaOVWAqmFkz^avbR-t8O8!$j%BioDy6aJ6xR z0gIJm2SrZxl%kd|RJ4Wxh5&kD(GxsWc3pgY}HPJd(MM} zn(BKx;JVsHkzObe#u|AkVS!W$K6*OM@&g(X$W5_{>sGTrqr)hjEvC_9D%_#o8SY-m zA=igplqqEZlh}&R)r}j|EaM$6#gsaJ>m7}qv(8nn$@X!E!?py zsbKkV3t}lZ$ugnIKRhS~=TI&oM2n0zwpXi(1-p;|Nwpvr8)I{V@fu;_Xxqwu&_AMsg7?#CP_O7c+m8@!QFD z4!ON77Uy&Ec23+OdXDsZ?RaTQP>hL<5aHXa$!E^eDK>>r`z8ot6L*Zb%3JDO2I@I? zH=Z0+dTt*dD&_tjBdi(8v*{-z7~Q)126&hAQTf)PI|g-TL~1(P0`q<-z#icu4Uu0E zjt6*}`yG6T-TPdDe1dbR;JRYz zCXHOz3MIKL22b0Dk8;vE=N7*Mwz+{vLbZIBbVs7h08KaC5MwE&Kg1t4a*!Am*BTl% zd)|(rpdp0Y;6BATxn0}s9ZULhm;tRtQETC^Kx!K}qT!hb)AT7&)c0MM@-g%y5<>wP zPQ|coQ@)MISSHbNqY6T?mhuH8XYxqnTyr9woWjE!#o#W{C}momosTj(hW*2}Yyf-D zOR$ORpA};mAw4_@_%9ZGooQtXz(`RL3(-S#F^!U&VtEZ5xt5!cZ>_?+ipq}U$zzPI zAXp@Ebqk;Hhif=4nqCOz5pK`;Xy`_sJ3}&RQSAy+1AujiF`i;B8=kT6hxDh66}?hE z(CE?*5^drc@?uD>Gya!Ln=%S&K?j!Uwo1@?1|(o}NMRXb_R{ zU14~DZxO*{)F`p7R%8wMn`LEW8$-Di#L&dh>P0AFEsDiIBnC{h5YzU7`4_ccGX&U_ zm@UN;Bes=+?xFl6(&_PtuTu2>C@kt~^~yj}I0Rb<{O7{MHEW2Wttg{r)7#o2p`OJ; z6E`FYf}+wXRGx9B6Qd475O-0eBvP?Lr^2%oseTp?J^MILz0jzWUQ%h_$2z>*X1PH^7@?7R0a9=+ z-qnhdqQZ$G7MoKoZ_*4`^Kc(S(PgogYmd?{Bm&_g6v6XFYWP#pZ$;oP- zo&$sm9RR)|7)08@^=?VV6x@W~I3N(iF1uz_d7*f_4SEwj; zDvQ9~mYxt}1>}lm$I8In;r~Q#hJ!9#)ymneh$<}}AA_W7-Nw&uN(pvkQd~^q;4R~* zdE3P;+{Z28#(N;{Rk+RaSvYkYazV%BCiXOXPnZNhy3|zOGxMr-4{rOQLOHm)Zg*#;j4!!_s{IqU^>p2ZW|euUbcsTh z^o+OiEj?Z|XWieF9P=ausX$CC%nizriMlxmkT@+uI5=;VtZ`&xT*jfD3O-}E5cTuk z?XQQHC3OGeBEs}uBQ9hCkhb7Yx&JIJhBTMW4IVstv=GrXQXx4zFythi`Nrn(lcA@Og{$L-}oM4 zE-gv%^sRu{u5Bz*@K-5n&819V>|HuLVf&6QtBQLyqVb47Fvn!RZzPo7AoFcd)v3R6 zS0m{e*VEu4BvWO=sm0grM1Ug885)eLh}s!^LASYC(xBt~r9QD3&qt{5KTyA}@BEO7 zZBb(9^@%f$`Ec#KL|r6RgnsJY>B-2ln>Q&Px2xfqRzvWc1c{fW>60<7({J*I?HLZo z8Ax2-!uK;4f6m*{o>i9}IhFZgOWj3}Zb^FhL~ae#a-7%ZRuF9ZQRAECsaS43?l`Y`m5m)-Ap#dLH$i`e>8>g8n&JP`c37AW=8;0e za=lA7b5uKWnMHa2H zv-Yaq5OnOw=K{O;RZ;6Qw|M(_-me$Ps{L;=|M!7yH9B@Pw<2KzPtY@SiZ$7M&8|PL zq;o8X_{JJ^=W*)}wno{8zNv4i>xevK)-aJ&VYg31-Fu^7-hHr zTG2jldOO8(Sf}=*n@1zff*_8zR>GgQr#^dIml-?YG+w$!#MeQI9I7j7*#Swr z6HV$u7}vPCFQArYwI|Gv7Y>qJ5-r(UxTO-Z0mS8VVN_t_7J|F^wzDsupUsE`9|O27 zYr@FiHQy2m1GJ60;fom&*9<~2N9(i3^eZL~?^Bnq1Tl$PRGY{dzjsG1^jk_+@CXgz z5)-&WM7lfWAg5E%djpR%UNkdD2YJ{AE&+YXqCrbp@GtgU`c$TxY>|H^pX9q5}xj~H`SXBN-P>67Q``9EEoHSwt4*_(+mR#w6&j5Z6m%=w0$f6H_WEIBGPKS+Re&* z0AjH-^+%R3gP&<>G-i5U#clHAb5-QdQFxT7EFQ#JpKCmTakqtwCvE@o;jg#ry^R#G zzUJdCn@(Rnqxb99om#?Z$^>fsnjN5pSAa;X62O3iJY9!nUTMFo#ny3gv!MEI%r}LU za%`tbr^x4{0yQl9F(sjlYYk|6jPnZ1W2yIIa`JuRs)BxQiQmgu9a`N2efH1SdzZ(} z%jV6S#umR;CWojTX2 zr`kFnX{O^CB`Jnd%{$bWvn)|Jy0!wAM1xh{#Sl+5pNyTE=19&XkSU7)>E9ThJgLPD zOLUIMf6W+y5dA>a*m52va7>^2M?WgYpvB7dfD*~H4+8s+S9ZVL0kfdeiNjn%-5BvE zNEYi1FY$U#^vO8OZRI~*oW>XqV@Q_@J*k#-8@$ACL1jwx)S{qs^pV}`OxZEImjVfp z>Ba8S`$8j{K&V?M%-bZ?I+fewN{VO9+T=VO2XHw+TfbI`Nftrf*4uTv1OzIPfy&nj zivRt+ao3mp#fc4Mqyb|AzDcu;H}+Oy2wl*({WFGIHoN$bL(VKJk#^u@00hp3w7dhH zxKo=L_ZL4iF@_Mn14g!gSv%MMa#%b-b6k+L>d$p8O@cSpO z`EA}wOqvC!7Ile>7fJLkDX^t9lJ4g=sf>h2JPT{dx3z64k$Ws84IGs|a@#T}xF1t{ z=}FEll6LK#snXrj9WGj3i3Jv_MELRHwVg$P8>?6 zb?u&z-x7GeD)(-aU0C-q=8VL*j^#ki%Np07*i*VnN5g|L=D4k0#IMdt!`9K2EI8S2 zKrUH;xy8G7!BB%Wf zY8}@OU8CM>j#XD+*`cKjxQ%h#Bdd=zwT0Rr(dH&9h%2n<#yW0Mt`1fGR0@L?HvsYG zBO+O1RYGD{VT9<&y{+4s)o)|?k?32Y2gVeF2MZpLpf(>l z_j5cBJKTz95IuUMB6bSjP37S|Zf5vhQn<5uD1mXarW=b!8$Zgwwc&f0j;kZ$p9z_7 zIgxZWth$PSIy5vjSuV0#!H0eJUU{LCy}IY@qOc=K!S$FSeJBjP7;6w(2!FvF7nc!HPmmG&@6u53gpm1Stn60MyYF;P!+geKZ(8B-Hip)>D^a z3M}ek-3}*9<)6>dN(@%_(o6+u*>voQm_XBr-}2|n%g?=7Z)%6Xp;;g$TF(sq9g$qn zJhN{b>f(nbA&lK#52Kfqtb5HSJn(vIu(*UBxrGoudTrkuLT69t(aguAgYIG9{jN^@ z<~*uSFZLR&tp8UUtxFs-qeb)>WEeXY3S;Og$S|{jYZs6xHkWq>cM;+XKsET?)HMwA z*!(;LHF1vlfNG%5MJ2vNdD1qiE5s+v=V|_)v#Q2sR=$%+g)#AUnBLj|q!wJ<2j*X8tgg=OszW44pyc{T}07T2)?4aNhS)d&;9(;_@;qY$JDO zkkgp|mo^M$<{4ec@EkihSt#|M^@G_URe2`ON_be4U;!8FRkob2fF6NJ_y=e(U0V@p zI}Z}P-4E+SgN#-i^=AK8xUqCKl^pGIv*}ex-2G7hG_Ji#e4~356=swsGSO$! zU`s?T=ef5(n&HaWVL12ccB-0U?NQ<5HkA?Fph$EShU zxIKfkFZx9vJO3#mN?MYub2+9eW&S*XUMd{k>GBA~%}hZJdXHIb1y$Mp-Kb-ID$XPA2~?t_7OPTH zO(D59g>89H;5huzC#&j*Sq)u7R^Cku5@K`?9UVyv@M!WbIk&&w0(%3=@^n4ic(*P4 zO;^6nA4W+q(>i&5&YKlkKM7eK7;e8=zJAb#YTN4nFR}=fRM9Uv_tplbQLvzh=_J$Nb>BcEqK{IS}Jyw3^eTcd0IiWHE>C@nF}I zTlU9Uj73d7?Lg;M55fp@(`WeVTKU`ZNmFG)@9?Ok#5Fl{akN$)`Sx>kaP_VW^i=@N zrA}_WUkfu;fc)vR-~xO=R!U?^vKI?ZWJ5J{h894XYB%?x4ZI;i6ad{qCst}v)oLl9 z@b5Wyd_`^f!65!(sRZwodBavhPgs9qJ@Q{`zsh6cEo}+cvgPXb;i!D-un@z+4-r3T z5N11pykM>bCF0)Qxt%pygXHtxE4q!QR$&~NF{C*K!epmHpY|}0(hV**aWXer78GmQ zI_rODDyBd`o(mcNF9>Ei3vPO|+Y(9$50E0BTCWnVGuG@vx`=yn-TV*VDqGC;&;St4 zVV(M867pq~6;{(k##lc-GM&fSJ5;r)-L)ol z+a~M+qT<;tnQ_I?Saz05Z$n4m;u>F)!}K`!5L#E~lAxSJ8RTH*>667QQs%$}c5WVO zSyy}C?S;z7=N#NY+EQpomKimzePgX;7A5KTkgo(GZQspKn#?2uM+lfL%S6)RjCeZ1 z8$dYF)=JiD;LSK@)iS;+56#Y`suZ!|BB@hutmXPBzR^8xpVI&%vh<3;0zQyuqXaV| zD)adM=Z*v&>b4kH%(oWjeZFrLGLF5`!C@_3UHW$N*vRx7`qKTS>E`3OBP^*)I4^Hg z56|#~60%52BW`R`Vd~!O_?g&D7DmQc{x67c#Dx?u{t7&^bCnwH71JV9V)PcQNJVy2 zLoaST=sE!7a<5yAe~k9{0yWg89m!F`?J89m>Hw515=%tK;<2PXi*r6D!|b>_NttMW z&$?9fimw(1eImr5w*V#$@E?7G3HkxuZ54j%n?Z*~uL$MNe39^Jb5MN5 z#0|YsuItp*0m?mA;i{lhG* zD6X9aKchfKHNoGCQ9Rmy4+XA>MRMu&*l0}%*}b3c3W10LM9Y3-)jGFX7{!$o+=8_F z3cFf?$eUgf$3^buB7q+Ar@Q1gasnq739MTw8G%EdddJCqGFj**74|9_*C)(ENwOG^ zxsu7K-yWo95$cxf&N&wB8ykIAPIc#cRZ&F%cdK6ASqFv=O{;Kq#R9KGDG!G zs|x^OUbYGrd~UcPRa@N{)p3!HEhQMYuGaOT94(J%gI4*G|7pPCaAHRO=X{uPX>x;w*lZ(G>U zVIdJ}W1&SWPrLUYj>sd*Q^PC-8uwD@38})aIeOmty?0 z*(GPPL+7a*pLcCI7iq9Lm2)-H|GrYNeva^bwMWT$=jTV$p2|<@{DDu?*sm0!TJl1{ z^S<-fO(X7@*rc$~rQ+R>(zect0hq2KAkp*W!mt?A?Q-4!2c3W1)VoX{#XRk4RfN`a zJ5R?neK`m*RbY8VC2~ca7zsRsyUF#QJ)s)2pgMBKQe|NmKza`x%}>63!bZx45UZaB z5BG&6rZ3qz?6DF{d#xlU%q`iH8v1N?SmX23@&9+DhEfjI{pC@ysovTCc-mv3?R^k` zlYHvYZ29Y1{OcBW^WM<-gC}=nTV!Za<+B&M_yCc+?9_=(Myss*&L7_{cfHWcpv6&; zUZgK8vijtND_YY;S-cd z^Y?^@+H;NT8~h{KBZ}6yTtX6n;3<&zxfP>o;#(FzSLVs`GBYaT0e-YoQ?;(G=K;9Y2kg-oIyQ!Z7J-)e~awD9&8@}GZiW18KMd0h^6 z>m>{kE`oaru7rzX;;q6ZyBxP|b-VM~kEGNt^#6CLuf-{PoQu1ot-Tsbsq7)^II;m_ z#4B8E+#J46S=%M`_p>KdaFIIz#7#M>fJI8+Vz03_@IcrSF7}WX5i3L*RYUEa{9Y;5 zV*qZmNVVt)bTx=80AcoG6t))b)q;K|MlX>=lNGp?2MH_5`|ry)LifYGLF@)GYzY}N z1R}k}unG{jL<7^It$7YM5VhPR) zyOr&QW|^x7QcoUO`BCC4G;{ z#9w}dzvNh32&qqwK1gdgle50toH)zEH)#2y2=*&7aibD_LI^#pK&>DXf@#POIlNbh zJuXBf9Dzr10~+%<Tz;%(%86wyp(sR*ZFzu3!CVak&r^8>yefPe|LkB_>}F z`*eX9B1fL0G`dh2mD+aeIx^#nUaL~xHygI48ZY<3UXgURt|H^<=vr6NDHXgVqb5I) zj@1r-ct)-q$bTMU{!T%x<`VA;@tc;A?#)KV>5woUxb@`RtXV>a2>DWk;d3#WazvyO z`Ene;MT1z+MMaWxS1Ji-RJ3!m&+)E-=V;hk4rznsR2>UnU4ZMqI&=c@Jdv_71&XiJ zDl4?)jR*X(V{f*8dDFW4tu39@Do1Yx6k!ndbhum{W=rpB5-Q6262c5Z|Lhe z)_-YsrTl06iDlNJ3-3?vT77-VCkBs*<(r)q!dGa`;sNPtJk(|kv=Rf`Y=CE&&gYnx zd8~JtWQG&I06!yr4y_j5WPc=;5EC zXIY@$K6tFk^yU2Hf)E*@Lu@Q~6iI8;r<5M^IN|(IBA9FPgvV_cGin@jsdYa2){7hh>oQF2cUO zt1J)FAgHUqMzAd`Ek7Rn?)vd)vop}-V?z^S9Y=J~$Ouz)076Q$>^CBU0m$EzlL5f1 zHD`-HYWCqjErku-+47mVN`{M@3M;kF8kDNDx3{LuPlPbXvTLcSRvSucsQF3ZrA~gS zF{j_9hQ)ci-8~q*-0t!T^c%VJm))exRYAu{;Rmn|L5lmms-&i9+lu%@cV<1$_ilBy zc8h8+p(7ka6D$rUuL-sMgMRbL*p(QE&Cc}o+0AdeUEHk0?R}>b;wHNX96T5Y=FZ*X zhM$O@(>!=`t)cgL%;&F!EA{sg77?viDuitf$4VVnCiqCbHh&2V{Y)(RIa@-UU_pHc0qGW*wEPQP)jtx93)2^sIl4Z}5}}y0Zk85s@TEf0)|si8Z`-OKz`U%pckj46{vFu`}^GhCIPY`}3*`KD2w6cvX806+5f8HP_9KY@12Q zIJ<48(+0msdotm1QKP>2Q3fd-?vP$k2YZnHPx8*u73*y;RPE`aB-|KtZ)s^xycdHB zUIIeYnX8Tb`4r^TW7-Jg_1L1U-usdRv?9H{H!vXDXvIO&iijS-X_(6?;qBG*Fp) zhaE}CoT;pEZr%5cu(thdX#r{D$RyS=-7uEX60)!ILN;BB-&fo0zpan3v`imVYR;vW znCEqZ4CeI%|FdNQp^OedqLZ?RGb@o#W{ zXsFKSS*&TI<;UXS+DCS~nu|Sl)giWC66H3$ER$vVxqw?lXElNu+0KFW*_{z zY|U`Ps&$RqFER3t?xvb#RZKB$dz;1L`y9>(?Jr?Q_VqQ_(2=qEJm#E1?mKa{oReSq zJ?F{5@45z#x&r>tUAD&duz$(bOAq|^H@jBs>rudyhnV~f8yLs5-quG1Ckv{tSbbjq zmYbd*Q#i5Sav$WV`sI2|L?)GU{;S>+BKLGJD13#>2ptM60GbQDUlC&VMOz+LHlz;8J@Q)~CyfD*In`qv zn3Cdvior9x+B5Kv7{*XdNp}{RCX4PIwfy~`;T-3C1jqJ1x-L#v=j@w5`s4v%|CioL zH>ep+q_xwjhPm_Xx#$!ddbU>6=Ik8{{0 z*#P}Y9D5h9p&3#3XW(@5vA}@SvZdntJL|h9(V2y5gk}-Px|~&o{+(j9x$s;m`0PK&y17O+%=q$h{m-6)f zu^CuiXUDyx=9Nc}1%dlYdOtE{bry=*GtMU#wyVpc>t_C|D95bvJw9unUKnH*+7TpfYSvX9@gl_T5p_7R(bG9TiMFS47++n-||$eqs!(WsDtwQZXbMfbbdA; z*G>-is==b;66)>sntQi1O=-{tP*vM3lzylXOs{xr)}ev=&^bqAd#8v`05A`t{zlgH z@4BgoznKhCh;#&;XyLI0@hXoyo}Zq!5~Jr3=1P!hfzDe{U92Pi^C?iK2x!{JM+Utw z*w*@x_pJi%B>o3xsaIQkMMh^FQwf$vw88+_q-sFNe(snzDQIuJ0FuaeXwH zm05XNQO)h>KM{RvEp4ZZFbxy)g2>?1x-&%!jZNNu85@LoB^y{_XDmJLxh3eoCDd|0 z{Z!q*!Ex&GwI=V69?NDl|7f8u3zxAOv+41%R!HAiHnIp4>DHxzP{TMQ_XoJi%?4Cp zy4WmOzzn~+pt=|_9p$Z;_BCteMNGZ5O3bmOCz!8>)FHAvWOjFrGT#Kbc|W7iCh5^= z_yPbwE{X_T@&rPCFf#OKUiaDafCK-4e^BlM{f6HFQGcqf3M}jD1GVR-)s7FcMMbgH zpW-@;)F7N%!lB7_G*h2O;Q>5ZvY~jR4q$;37qKKeJD}VD4b^Ud3ljZHW?hs5_}ap| zx#Lo7+0pCpmZQVbxU4#|c(`O0x{o=~x$2MZD^e^{N$xVZXEbL*ud zd@Y13EZ#2y1?(jTsukHwe5sd7 zQ`CWz7bW4^UM`||OEZLNkgO7c;t_DqS?LChWYd6jxw0Y*jg4=Sky-ML4lqS0564J? z=0SO^GR7U-OrCsyK&e|50Hh|B4ghRKK)<&{3!^Pm3yk5NSg-Yd3UshY1UWUISc$u{ znILZ6RUD!NFsQ{=0MXXj(5tuQFa3&byp@G&C~e-~Qw`rGkZ&?9A22{og7X$*@fI~S znTOk>mM7|<5tWFFJbWet%+N~62+1ytqL5ciVuHyA&|U$YJKke+@NOZ(vS1vNHryYP z2vz`!cOYPAx}kXtMNvR`kpOxo9}%ssCJWk&_+a9aQhH-AVk1wyZPvQ1P~zDPOQ$LJ zHG|zcObj8;ze60<9HtN|GE~J#cI&_sXr2a^JbpC3O&-runC#1FYH+1>UjwfKmakk! zMM6)4F4ia*rdu=)^c(;OA>_=H;b~oj#qO#`C0)@W4cEY!i{RxeAbggT*9;3}Kv;iB zyyhi57C1puyi6&}=m0Uv;%H5AmO&mqj>|SdnFwfLV)2GpDOuB7)FBBUm-9!j+@zPL z2$W+q)V}OCg&@R9XXULGZ+~{$EK%y;3XNeM_j4w|L_qJ^nmN(=FmmKxtSku!geF4s z7XmW|pc{Db5~ZS03GG`#$%_hMNg9x+0kk>^k&1)lXMuc0k|;GiJaG+$dbEU(Dx`X- zZ;MwbD+@(3hh|72RYDfQGv^=dRW|Q(kcSHqt2-)p=s@1@?0vM5A2o0~S&p4_U6&8S z99Cv1AykIRla=`kz!89|w8Xe{hOH^gO<>4*v+`^JGceW!=i3A-hyH1kDO({y5IJca z$W*SRs^K0cd{ZUyDO}|-4#eI-GQv8)N}`u(xPJ)1X6GN=xg#+No6$NsldX8>+H?c( z+bQYM)0m1=X(h}=Ncw;@c>Wqo0HrIz5#y5ZL>YUV?C&R_1Xl4ecJYP|F=+s@a?-(m zsSOg%mqxTh@BR(Kyp|Rjq|`;57wsjkm9Y(~(5=ZQ+4yci?V*aY(Ev1XFCK18l$)?=(X9%OIOESO3dXCJsorixQA?EQFC1J2g05or&7gM-uf3u+o2b)bO zkI;e(=fQN{V9_`@hzee-v`_#xUn)SXr%6k1Lx@!Q>P7HYAwI@<|1w3sY#ih>j#KEQ zJ_1_>;=#oVg@6HBtyYBdr2?{)s!Z6XlC18KdB#e;lu?O^QrZ&BA?{vlyBRj9*D6{I z^(TX=xSk?1*d*GnL7aZ&(!|Ixqxi=(2xb4TjA~^WYS~&H)PEM}L55_~idPmsT#0~k z7}hJu^0aYzPb~C$o>{`Y#CH+oPnI*OwmGb=3IvQUTCZ`#W@%*cmzC~GeM*L|sJXp3 z9y9z=94L_OoILYRy>XMuuKz7=Jp-ILBu{2R!qi}(5WGQWD!GE1V$shI^cU!YN}J{0 z2-$KqM4$oRO$W!Q4STdwe=_9App*rGh6-VYEO3ON_G6EQukbe|@1mBN4wL%56jF!$cbRvk|mf171yr z(y8Z~SWzXl64siMLXbs94`EBVde0ykepOWU7_(q{;|cA;f#SzWYLV;}EHh8G%Je0phv3PEb?~$XFtuja*sb;H?vK1y7 zmM`NKN*y~Kk~*t-e2ASnJbqm4s2Ua`pe{5?%zT^z0nNK2@$7&_7^K0vRe=I&v_Tpz zkiOXpOYD$Qb<)B6sns;Hf^U}EK_Iq){TYzPcy~dSrU!c~Q&^leP_WnW&+2n%F)|;qO+Zn)&w83P{^Mp)p6Jlq(}{j!C#|^XzzIJsn7%}^{MoT8 zKy;>P9@2NcrV{0!DRW)mpPhh2vv#=or6&-iHj24 z(ktEHC(ULln1VM9-JqaD;?oRTDO9XxNIYl3g}NRWr6l`hk6=LR!jQxZ-#Y(8k&F;7 zUOu)m+8F=nH8Ki&rpfc<{-c+W%f2oUP9B+U7hL^28%0`rS4BxBcbz=dY=}xDYZaTW z-kNpOtt2-V3ObT=i9mqwD$##8YZ5!@Qxzw?4(Qg?(qdyR8+B(p=EduAyt<&u&7VR) z%KSd2!**hdLt(9~|5_}5iEKYojw(@XrsmLD_e%_ZX9hgN$J;9g9?XtGi&z>0}C$^L~q#XToI}+V$E}(kxJ`?KO2$$++^@m`Y zU7w1Ktt7j8&&-Ejx1KBPoIm7#Si1(>>E-Oa^szM2d0SgepY189d7x9`MhAh`X|^U1 z2c~mMM@r4SwHAs(`RXX=o%8aIv9KNU^5lW$jeOXaC7vtx=gNeC<{vhEJll=uV!@J5MiOM+U_T)h2) zQ+DEtQ=a2)2YsV=*RsR-|PKF!lWc_hltt z6^Kt1_;0rmPn3mUZ+4{C{N!BH0Wimj&6?Y-0LRgVeKtnXkze4Le=p%_wcoj={5CN> zVbDVh?3DoV3^AGyC=~(-XI0r2qJ4fhjLK-L7>f&7L--@*{U$Yh9*W+FSRL@Fg0+QTHK*fa@4=? zJTD&m$E&wfC{hQnx4SNZyz#q#`cK>2MW3dTGQ@gvcxijhTnodpwr&k!wFK=EPrk8i z$B>}z%x?3la!4K83Y13tnH* zP}EKghvU~N+lof1^=9(4vv;=Bp=Xo<4_{E|pku>-Y)C#f_uqd@ruhs}?3OQR`sSi9 z!=mWJZYoJ!NHbrS5tm`N(JXa*bnDePIsmly!G^P7;;@^J2_CjdEVU>j_gDt-{VeTR zw2xqOf8(MQ?LNMb4a1Ly>eQ%! zPHegZ=Pv#8notYda2Q^wzFx8}N2izDoYuJvk+;bbV2&5+*f7V8!tQW<4q!tc{$Ozd zml#~>g3|LQH+RSJlw)%NkYVP#Y@PeykY2rZduXdhYJsCbOi` zTL9Zu?_XiR{TE8sxOEVwi47HP#7_Zj{W_mza(zUsGfS8Aqy6qtpOmpv%il(YTE@91 z^TdpN*otK*Q>l)V4_wwzC?4Y@Oo6v4j;InoLWNhc{Nu435!fuZb;B;lgXJ{mSo&^0 z%EAd$@=4)Sh`^R!Tk-ij*5@Vg$KHo7k3PjbigZ}{;89exa`F-32l2*F217oY)=%74 z`;+zK)Zyhz4~?m4pU1M5C;Wb8NTZ+siV29J^)C2Z(ynj+eZdg%!s~JIL9l3w3BTiT z`BvoGt&iGY<_9^$8y}?gU{@zbsJHD6#8`ttRI;dqOHqk-fp)r>GpDpY$Gn;rr|z?d z)$zlJEby1C@cl&2LC1+h;t0>WJMqO%Sfuqe-ba2pJk&obzOU#Y-OrNBousX28s;?c z@L0_SSkvcu{V(pD$$M`tzOr$Fs*F9LmH{<`{` zpRVvSGc^F_H}mKypV6~hkAwXI5Cks_?%bmViU1~b6m-Wc%zYlsXSs?VCKy480V9B* z5ib?p;lX5sI_A!Ksr|SSY_AcSLqK5sI1N1aX-vVIBG}mGx|-%O6aegrl3}UiM_y#; zKVPnH>=O9sW69V_9GC750wEzXSETxYe@(2Iv~%xH6LD;X;_ z{J6fDsTIe66v?bqlNNhpBgyX(5DSg5jmOekCXJII4kfFSzYLw+w|ApB5lTTnYjsxD zMA2&V%D|`Z5w>O7%HsnseJaQxT=I<7QaN59fAC3dYWcDwR!2htVzX$ndGe>PC!a2q zOTc8&%$?r>D2)N`uBtl`#E|wdKfr?)*>+>B;y@+`sl>0DUdRVIIxl@#UWr5kBs}bn zo8gRZF`PLPcRaPjdEZ4$U);o_QWFboU4(%6++ic4nJ%`ah+fy421Mzwv+D>C8Xjil z7~b1sQ>A%DN2DOu7*kzRria|;g^1!(DHgAfXz~_QBkM;`KBg51VAIiAyarGIb1mYs zlntqa#Z95--g@2JY`fh9abfKrZ`bc5lwZR9i60~Hu!p4}V$rW0uBs#Lh7B_GA&Yai z`o}MWGM&+fbm5`bfT#TVl||lcWK^OQ-^Yioh&^q339^fEDc8G}EPvcR0rUk(iW11K zC_eFJgl$b>%`3;X!}pk{t#4oMNOQ5ROXD@-R{Y)TLRupDMMkaob4zJO=J|(-Eq=9e z&#TYe^wewLX8mw^(lU%C(B;=AsFMoc2iZ60?SR`-V5(~034WN6(P|U-lz(w3{7t-Gu05k(ILzoAWHu|3V zn+D{=n{R&0CqKJOX>H`$N2(j*PY-1cik1s z9Alc(cOz}eXE`sOCV?;B3!JlNLHYAQxM`&8{=TL3p99OE6*fkAz_0MclXcJPzQ?)1 zUtat9{Os}Uuy?wt@vAJ?n91N1LwK)q*NfwhPF2<>Hmz?dEzWrUrt^y~0dOV%g%frSIZ@pmsnyExW#qxnhMM+u zfJFGP*(1xHQAMM7PVQq!ux)$y@)!=!3M*cj?^be%!nR#S-|JAj$HRq%YfqNMo;mlh zR&g(V_ilirwDAd}IBV+xO&Ux7N8fCE@%rlLtN!ZjS?y!C3%D{B`lGYA<;L6GW58oJ zPQUlOsur`V=9ze;EBv#O)3D=`6_lZ&xogXsmKyG`o%U9NYjSnaLx*r;(XG+dh6Q7> zvXlCoF5t6T2c}N^kM8B>@*^XY={ecf7?b|n>qRd1xkwr%>oE&_bP4;C4~g2gViv=U zNbK0XRcP}AcYb9AP1BZCHTHMY+p)dbe{FkX`>m|!wZ0|dNL40tv)et}XC92gzBov)0Ps~i5B0vZ>||U1@GD!D5aKx$hww?{kpEPMdlA2% z4417ZPSN`PyW>zqG}q-RtJr6zZmHCv%rz~7pFnIR|58Mpvh=YD{SxkP-e&n4?{h3G z&qKB#7+!eTl#!)~{uiqr`L}(=`OCXs{IJ?rUHCG2y~lN;F&mJqLz`?6JJTh&RojEp z^~LfGkvSt}+jK8qk28h39pmy_fa74IZinF>JdxCVSccZ=5RhU}^V4Y$k z*G9Ri!&qD2JbTHzGr>H>?gOBru>H(MAaTB*jFq^J_h(@__9`r|S?{YmE^G!m>9{^d z1#1qJ`GD)RpyN%5amkV|!#%b%j%|XTa!Bu-@IU(_5N$>H(-f!hfl4}~CpsL_vF6^> zINo^MrtFpBw~vi2X<8iqJyusMmI+#_|L&}d<-cg2djL!j{c3cy$8j97-OEg_nUL++ z+k;b|$`R=KGSiR|Y-)Rx(T+g1X)H}{fSjpVE4z9OR=ixeO+YAa1D|H>_dI%sFh9x6E#MWVA zQU@46r*q)xZHb)b9LQ`I)Kj_r!UW3|qF*-waQR!|I^VDR8;GmmGMQ|e0KKZI;*!>3 zZASH~5pYcB=vdn*o!7H*7~CP=5gZ2YV21~RTZZrPEBIe-(c?JXiGL|o-;rTU zyVfG&DmFdyI0^|7?J}uQbfEjJaakG2b#5?3sCT`2Z+n56`FGYIS@j@R(hOC!<+5le zq8E95{pq%F?_sd-G|=%o2xkDU;&7Lb18s32x|VG>ETurWv}7=4TIBBfltS*Ip{4f5 zVO*5mjmvDOIS$j6i@{7X$=pEGiAoJ8Tn%=aomkG(Gc{mO85^sU;E4Kd+q)@+bzIe} z4H}MReYbfs=o7Nne1gsVr=L38%WO}@kG`@IIoS3-SmnHNcO0ZsZXHy>4K9$^Jj427 zFddb@1T&@suqwc_imQOlG~pkCB5<@LcTx5t4wCxC?f1n>C$jacrI9q=$QdF&lu;mk`t_;izl zCZ-C2!CEc`0%r2rUI{&`U>gb7#{6+TGzOT?k_KqZU7CTz?Rutnk3WN(cMj~wkOm{d z{EcG5Ea)ui-lh?0Kmj+Xoo#Cb1?eP9UtJs31Zd>;YMF3c2HA+0$oy34Eo#?;zsiy0 zVtgaJOb1Mxz%)E~3e1TJDY^hd$&lnNMYV6@SUt;=xt<9exA~~POI#+vq~uDMsU&U{ z;1$O1qG7Np(dMD#xROntp6RZhh|o+hX%Z5>fu78^VHYIpv^!dzNvjm0DvDiyIR7+p`0J#}G9%*$A(8KO;fmf!@78KLOn1JTjMcVP9h z8vOY)C6#*I`S#1FV#$w2Q1XoMDDfcI>_7M;)IjPHyVa7hkYr7zp=31pt}DcAWztD_HI^-uHsSgY8vc{1 z0xtPBvr+tq_8JMn$a*%Vb2P#|4zZD{VQUGb<{c>*&5sugByUZ^Z7;U_qFondGg1T8GPSJiF3; zJat|5>3dQe3B=JBCu)1f)t;)R2!CeEq$?rPN1LAvB?b3Zu%`bEnCt$_L@wg=o60jB z8g+K&gZ!rfM>w_e+(F-Ea1hK6A zYW!%OY|(?K`K$Sm=*M>FOoZr%wH|k>&4Ld|I@h!2@Yv4Snu}}fRUS{5Ug2v-g6@S3 z_qaE{W~xmpU$?<-^1AtWNQK^e7)00gfDdmcT^N4Iad_9a4tl*(RQBP_y$RUnHoKp| ze$;`Gupatcx9tMUc0ptZk$N(?A>+v}IJTZf7D^>@^9CQO;1DNtcHe7Ay&^jR_pjn! zhdV%D4VSLvP=8|w?H9NwuosfA3I7?&v%6%E`1)E&Ou>LSn+ z%?&Z^bTIBl_Uar=0qnxG70&Hu6(V~T2s}Is5MXoLJF39 zV*NZ5AFVP&&`&@TQ8N1Vg?O?KTBAZ6r&Aa}JHh9pPyi4hg4R_$%$2#F1tG^Hv4;1| z#MhWPwi^rhsi?;_mTg}US;vRaV!KHKF{_1b&Ct6txDg2;9;4f47`YIlw*hdE4EID` zs91F!TDj%Zi-j4SWCwk`+rNh6I*}1l0rm?6n*p#nO5A*U&kobggBOWZgFn(kXgSh8|;;DefBLFndo^Fe@ z(;6Vt#~M7Mp-00Q4p5&%dM6@#2^$7<=-^hGxQArDtw8VF(G!vm_EPGdbM#I|kcUw+ za#F%vVp|Kk1IIa*gZfH{zVfjn^5J&5LN^-3j=~?nEc+51qbYy`2DmWe!Zj16x-la`6+03TUo=wLcqQ#8LvqRYryUl!g zX5TqpXkg8IoM{L#wcxC^F&vu117HD=8&~$Y+#4S5u@eqeUj&Yfg9tKCNWr5&M!2R+ z#*6@CX&md`wI6Q)vGb^)4lr#Aczm7rkvU2FNY8TIfu>aHrP-nE)7H_K-)6t4(CqNr z4f{G)ml0RcvI-RnqG0MWKN2^|0F~z71;=(D|9AKC6#{WDtN7n8>FsBtO}Fn&M%EvU zTNve$yJFH16f|)1Zso;{rzzS(yGEdmC>F2~59D2GF8F7K6VzW68fg~hw7@pkvgyrw zYZ`mWTE}s!U|9*+Y5qkR`f|%*sgF_Qql&Sb6R#)~yTwYe>1-P%Fmyrmmn4)~u%pdJ zZo1V>T+f%z+s_i@U3Q!EHg2P->>MMwF?DBlbsfBgKU`#Vp+Q1{@`D;PNY?8+yys03 zu?IV^-hNtr(4c^H^xwX1-^D9fdApyoYw+N6vwIJp-sY7p7A;rWw42D_?;{17MfHdh z`tdXY4jz6xR;fAhY5yI=Fg}3gnc>eQ2!%r*6&;EG+%a6{Z+y1!y=uX~eJNPLF!sWqPkO3*Pjb+*#4mOM)PRPrrWv427ceQu{P5u^?@LV z5OcB`cC%`uwX5mhtj+LN8ig$?g3Lg*D+*t}(Cvp4f?|Rj06;0_p@V_{a!Fo9%NbZ~ zgP5J6XZ6Wy>rLrD)QR_YB1|Gp1h{O~?|Vo6@lMH&yf^Df$+ zFpu%ADcbwSA@F=v@aE9@+sY*rtfr8b^YjQZb6`IL0P2<-Kb%A!T&Dvfy$z=Gv;#ApdvyBQY8BzmOg-$49;P?*LnE3~5F6TOw(HXmD2)h;qKW{Z?VT}*ddD{jJ03KU595X=NU=+bf z13HM!)I>~U=&z>9@BaYcYY%%Zj%^&5Q{^cl#skc-9FzixBeSyb_Z$(8!q3;~pUzGCt+vt@s z$^Ag>ro@2{NxE%%wxwfZT_8x2^(b!npiLZV$&KOj_qA_b<;g2p(MGlT*4Ko?{LTz9 zMaw5m{&CZoG}2>McIyxFx)*;-d!nz8lkbJS1%0 zc_3KskzSYIRL6pEhXKDYtVt}7-wt3a9p3ts{7j)!B4El(M{$PGuRS})d%x<2d1hu? zaNK`ezuYal)ECJB8Gc!Z>n*3sWE^^ZINk4DIORC&=_J-HJkT@#bT%?g*G(8xorI#N z5C1M21NJNl6 zu_6M!L6#?C@<}tz?(C|p2Sx2^qz@uQpo?T!SSsB!MN_P>%eNBRK`NgeRR%Pr9LrqD zCrl9$%-)psM{9hY%hO4`VdO{&`jpzPa-6vs6tmkId*B;!fU`W60^9<27}NGGZ-XDN z5H&bF(G>^Z`52eu4R&z)(nsfbK0X@tituE%FSzNW`L2SVdb;hJsJ5r^a}RnP#td(R zW1c^*t;II^9b0CkH9k65A9heQ)oAS9k54H5lrz5tj31`SZ4%v`FTQ=}wM-lSFwmOk z^SC1XQ$RrZs0hXvgyX}M4h0%93LLTocfjVU@|_ciM|I(=mt9lwg9@}RR?3i_lL$rG zMeR2_&mP8B`Hun+Otsi<23X8zq`T<&v#kG+&&y*rRE^j4?sy>{-Z#=?VH2RlG`IDY z#`IcqgvaIH73F8*=?4kB-v&mN9@{0tI@~qS3fAUX=kQ)R(BC%)Ha?0w+m+yoce^5I zWSj4)>21)3d#pn?j24-AV*=spg|ung~p~FTC%6MjV?1k=l)YFaERH>UiNoDsDm@8FkYoHq6lae-j=d<)B2&x8TD$OQqCE4$p)sfR045pclI4>{;Uoa4bQ{u5dJRPoi}AsMz}JP(`@=1K}`l|@P(o5`w7p-H6= zGd%}gsFsdA7QyL~Nu_1*h4Y^~`dVIvSiWz993+{4xY%a@_^$)tj;EpzF%R=BK1X~z z@7xK?Us)Ajk==gFckSDvV^JG+VZZQRbij(<04??)-r29Wov5a{3}tOJbxR0YuV{~l zvR|XO?*Lf?A@fTOzcB$WN%j9@7K;K;YE#IM0bJ9$1;p&B27FLEfEuF3&@QoH!Vn0f zhHT9p=3*D@dTeIIf;PGNi~}f5D>2IqBG_@A+^*dKi9ZB1XZ2*;vm%2C>tY4P_#2=}#QOIRie6X{4N-vW#n9(j zjz-b9*v7rsT?UBll-_zMDUFobK^VzoNoAXz5lK`qy;*p}@~BJu9X&Pb%*W^xOY2d25mVzALH4k#rh)dXC6RfJ2zoioxr0wqvEQ?t zaXLP^U4$YCNq5MgJt7dk)wV{5H;Q5I6li^2H6qnoQU`v?#vCwWYxG2mx+`OouwK1` zu+9!LY35l6Gf*t8XxOqVAZ&}O z$dkCH%MFQbPs66ZtZVJ)IC@J~TJMi?;AcPM4>kFRHR#0bpr~JK))%~lKv--3IMfec z#R4)!c{(5GWaQ35!I|w1zp-|%MUX+xM&K$1SqAsJo9;SpbiJgvOok&&?39c(*G2B1 znVg%cLzGLc8%v-Cldzi_%m4O}?DS?cOOqDpdB>SL>lq5hC~~+WBL8Z%8A<`UI1{pU zC?nwdD9e6^;@Hk2PSsi1rKs1j%q%FRYm+d8*g6qlVI*7f>ddDpb~{C|-C8p3*zk-V z)2R6v4Zi_J9sg{ey7FbK$d7b4Ho0zY&9w*;GgX}i}9IHgoDgXiIOf&$n zq-@t&GJZyE-fXlW#-g2R$YK>4p~sA~N!ct=ikPgcgEq6wVvRO0>)^3`k_KpQk>Z|9 zCc4)_W8>ZaV$DMIz!czA6}Ug*-AuO8Z->!u7unzKI;_wL%hkYS8rYF5F#in!fuCVP z8xSC1<=sj8g(b85ocb@dbsaLZPDX3}p+}8jsDo9}Zi07GpD^dE z+b3<>S=c*#JVR(^+FB|F!eiN30SjAdv<InK^5=m3u60%8OSv7b0*9AtKYL?ATG0bGw`d+S%Wkc#y>>e+rm^SOH z+FR`V^tOjCqu*)lyZDv{qkA5IvYdsU$RjE1uIv=Ujgspyi6d;vI{5-Jn{6Z0*ik9a zZoS2<$N^G^UROZ+BqT)v&8@G52(EHwD9BVY?6@(G@&!TI7w>T+ZlflCBMX|Wi{Eys z^DdhZ>-oGjx?Y=kAzm^zHTZkfyGGIbZ`)ftk58ZVMcKc28@ypzzCcD67~wOMV48?- zVZ{4@cBZ%Nyw5;olej2UM;E(6b+lge z&Mx|$ecC(W&wuggLk<-~&qkvk*FJ=7S~5adEZEQ?5`Mx()$t@@WsWijTu7 zj<`LJ&nd)_c4x-!eRnC_sv!q!F8_Y$K-krix{Z$og;K<5Jteg)>|4G zxRG@t?gZb^3pCF)m6ftlz8g`^?D#Ym3Rj16rNMfCf|CL_m~AF*l2~-d+6IXwoO-)- z5!O1lw!AEAncg!k#o9lY>@DW1b8Qg$HmnpvYtI_xdwM{2gYq&vzx8~nPm{>AHtN~W z6TWtKPzHCISp?K$((t z`z`Z3NL!8RgmEbaui+nmLxJ%C@Z%cnO+Kj+VE3GibQWVdlZfY=q3E;VL8ICCY|9RB z*CYypOxYb{P9#&{W4V?GC}tuSBBu`51PF+%!+3u_wo+^z`_CHUxOmHhpAVqi+zS_u zv{q~_&F7x>9W&c!9D=}7acY(V0-+U#fa!q!Y0#48-L*!%TDs+iXMJVOQ+bAeRefd+ zg;VW5SY5N|QW_G>@jBOzZ)aR>EA6~wHGTFB&`~IY9Osiwi|||xd@*DLTnHDEVb59i zs4gt7?%puQ7Ds`iZapjqn$MP^yeZqcAbXh(uGAIYrVwX!$`e3H{1v-$fcaw<#<-MX zWwQ600VL-XFjI_K^=gfK$AyEX34tAr{)_gfqFa(%&#TOulTgojQ}*dwgx5sq(KJ%~ zCstt*$zRt})I*-Adv6hZnLAY-4H_b%?Ay;&{Tw_rEv|M&)>kxyc8z!UBA+BjlD*L# zexQ29rv*)<*9$Qg-_@e~yYryl^KzxjCH=l!>7L$y3*oN8o%1lr0dfJ`lwzX5w3obWz<`(!n@slQn{QXhJ!|=9nKLC*1k@$k?}1y zuo0iKEk*g{Au^`5nw+g0cBzMoOdvQ5K^`%a_7G`i&9@8>GRzn|qn5wL!y5t07Ph&D zMLI)4`hRi9Pgaw{jJmp@9|^I*WL|6)FYAOR**ZEr_u5s$Cs-|E#Y#)L{s4y_?zb6ZdRY zukue9=9#_Z-T($k1DaLbzg^>32qn0)t=yN;j@csYj8giRL*lWmKAf1OQn0ujJ8X*0 zf7t{^qg|pmQaGvptsFD|B4{S%F9C2!ptt_iYCC_JVj|hY^bQkZJ0|5S%ln=Sht$Q! zc2R7DQmpgK)WLcH`W8CpNGQ__>n%q8bIp+heC@(v&+xFlDFk=Db+$&-Iqb5XuU?F> z3On*ZR9EhGj(j}jddH+K`Wq&RO|CUz`s~279ro=SYr89C5!;GR>FK2q!%}+8>TUl^ zCN;Tw9dU8<^*m_e4(w8_rZn&ano*}vWR#d3$2I}?sQ*Ne=Nj!2!0rPX2H5g{6rFci zlKcC|*~0}Y?h(!qN4Qa`7+U5?oH+|kb5>|pYSv*1IC5L=(5%#~Fs;lMI8sv!8)jyP zWoBg_Pg=)1_4UW^f4DB-`aBQ!bKmd#^->k(8xdk(zmuXoIY#wwZWcEgOm9%o9U>%4 zq){5XZGocm$V8RLQtAP!6<`s2?`p-F=(S&qt^D0XZKtNCPiL;0g(vQ@>?nrhsZ`4W z%W)dwg(HU2w=C{xULqDxcD0<-{~@`#W87$ME2bWxx+rXL4n$gN_1~$UDcD&)j>zwIr$` z40-(Me__Sh%U`CtQqQK!YHuyiE@c0=?cncY0jmebryt+{b2JhO1pu%q&+h(okVkkA zTZU30yQ}S4@Jz&%M5T*#WFNIt#@qzT^(T~=uA0jmccXsal7KZyXemh*D~}cEd56cr zHl>_Z(!Dnp+;55uT08e2K}HSfVWZ+L1>kqS#YRkRYHUw=(H zXr8Iz5+gd!wU{WiS0=emH9u2E{#mI;%*mQ2_FsEWC?Ty3IHL3mg^Ezhfq@FXMjXK* zfN^8d8I4qhh`zIzoo?KW??uN)7Wp#i7M7q*Ft)A>i>$o&vvY5r* zuMzQvQ5jNPhV(yFJ0lDAI8@Fq(4VNM@EcrLzAuv3+Z_ls%9{9WEjmh%>ObOef+EQ%eK#%<2&A3Kwi8|x+Q4LdC+3i4yKKX86X-U z)&yXJiKSixRLdc%+v~{^uqB|ZABu{Xv5?9KJOY%RpQEL@kPu8t!Q!zgJ0OxIkA>jE z&=e?c`BDslQ9ogeNBTRySAJFRNJWo!3x|rE_I}-s4I`JAx}C`d4e5QzN(b9QbwVCm zs~9Z_RkPXbhjL4e4IF!ulI0_rh?u$ zgb5&hD|IU&n)X0;xxC33(0f#>bmjI}7Q&D5@~*l|SKH^1fIt!ChDO2K^AwKGI|G2r zbxK1feY0qkC3GP7A#h}CX(S!sS;f*g7!O=c(gBlPfyz7?NKeuP6IV6Rg#DT^1ix=H z>4GFooc~esVDrKR*H~Dfo%_!Y6M1QA%6zO6lN?}PPDZ4dLkyb5dyJX(sGZ?#<+v5~ zj#G0Q6$9Mti2R|=)H1~&V2k#uFc0&bwJjZ&X4jmwllmKdmN#=4dm6b@uN;u)#N4kB z`N_et0x2qy-^#0VH&lqx7urv=%+k({#e-fRxJ(~sK)*hfAQfOnaAs?(k#}=f3G-`M5_&9g_Kn}voe4q*GAP3$~-f0*? zY1=*U>T%#)TvkiZM<4%px)m}flV*j4l9k@HD=pcde6Y<=;PX6uqs0!JMXI?*q)O27KfEam_?!`;<7baAWIsqMjC&8TL zKuw+mV$GE8(!6e8A15gF$KU!E{%G$LqnEzz z2VAZ3CoFjt5kUB5VicY{{!ZuDZ%oK0Cde2Ni@-ERAG(_IqT_(YWrSqWuK)83@Yw$5 zRb1*d5Iro=3kytmgBHwuzCG}j0NETa#U}!I<2OSLi}h0uyxw2@&gmf&_Xi33?zhFMd1H z`hhq5g^PB(+#?#laN3rC`f$f1{bY|6@YcHb^d^D!t#8l&dn^WN&ht`NofpIMtN{0~ zpaF2j+z|TniIF3SbI`VwToTpO-UPzguiTf^^u)k~hVT7ulNkinUTZn^WfHg!G)P=vC0YJf%CKr2*jiP zMgkc}W8FcOlxu=nQKxRXSO({1o!2VB(e_^lqkKec)#|*ii#m;U0XdgQljJS^o0pBf zY-*MNs=Q|7j63(U@|VI`h&u@;Oak^~8$*bW9B_2|xtQDv>gdX%Vp60owvglYvla%T{=AKkrlPoX2CGa0wHr_V!R7aavk{Q6vgy7Ny!# z5^k)VpZXl1Z^Dsjm+MlMk1zH$G?Xg2RK}tLrXX6q624cWU_--S0^Wz&{ZHSQ_|Rvi zegl-$^@k@Oy|1bhKp6-;lmP&P=;R^IX22*70IuCG1~{Di`8ajK`@3#bo24;j`BAb$sBk8Cvy~w}TvwyinF;7(E3vt7O9Q z-f_DASZE;~oPHMlqRQwz*0PWsj9JK16Ykpiopp9TMUnN_=44FAf<0G&?qo=jBWjak zmsA9{R8-`Z*>E(+eMNCFfMuHjaD{{?3ACX`cRRn?Z*wNKXg`>g1YQL&I;p1G3{cvc zJ+xAV=nBq7<6cid1!N_EGUD2bQbe$-0Z@TIL77Pq8fGXt^(2+4vJU`L;NqTjZN3IT zB`{&Tu&2I}Pc3ZrPGKt9eN(y*wvJg*RynUZNdC*m5urdbMbVYSEaeCSYJ>%Sw^L~! zd-rsw+WAuS$eCn~@cd}AbQM?CTA}J2G<(?zVBO95PnvNXi0}j;TB{3MTfvvJ5)632 zHgAZk2+-#VP?E6QTEUw$!>%pnn6-kh4i#Kp%n=8fesn-}n1j{gUr zS6an_uhP{z9UxE9N=Ee9uXKa~0J@zFTb3afcfwlakZd$6ld&@qpp*vK^uAS-LH9PW zx6UMkhu=e%67o&m%FXqntM&tt44}cFaUTM)3Me8ZhsC`pui|pgY%cO-0UtmCRY$14 zE9LIYY$*|Cmu*t^QP<)dKpPG6-vQ2>J5f$Rc1nU@&B0~%sE2^uj|Amh(LrP&f_Pnh z3k=^wupTBL&82&#SY#%_`X*ZS2Uo%3QvO0$+M%xWL&LfgiyD={=c-h5C2)4B>fs(l z3KOx>Lr|6gP%~(FiU`p!582(R)pkL%M-DF_APwFl8pF#QKUpEHf(i*-P&s8I_!EzP~euG6BmYOiNM%I#$b!qeJ7r+VxykPyAM~AEvhKR zsXRb$cR5D>@}s7yIs7@&<=Ki#ZBN6@NW)jA+8a5(QMdLbGN4)F(&~lRBWwaoQc&pn zViUiRumlhR(24}uwi#W8`6V8$yC}^5)>+r`v<@Hv^u?dcm@{c{^mm|xy%hZ>1|l1y z+)v(dwHZ4Xsv#eAe*HoB*{3XnN5-2X8(&Q{yw;14QfJ4W&tE0f&gWad|EarSnbX|b zthFU4;JyPuJF7D6d%97kXv^(_>51$gdH@hJyDf>6%e8i=hc)Y*Kn4Q!oqE6299YIB^1rpB#xWsnwcwYBv9D>^NPszg_dVJ1V=@ z;CMmP{^?7RWtUF4<2ePF{;Jhdy58_a)KDAVYS7u*-q$)a(puP_UsUG}*EUSw?vQ=fJtc!&olmLlRksxZ=}LYZ?Ru9bnT#5JK%aNf z6a}I`#l0k~^TA&hH&=Y?q@Mq%d!rU;RkO#h^n!-F;-^)qT~r+)v?99gVNBM6j0D<)&2@tWT^O0yWLLm!8auCYrCYW z#Q=JA0~SreGzpZxt7+_&>ZOcJu7gdFl)?*Mgr|HKAMEW?yJIvR(S7=J$LU`k;%2O0 z9lCV7JE#4Ue_gBBe;oh=eCX@u()JsH1~;Oj+IIT3J^tLS+bf<Y;GLTp_M4DQAcmpHdHHF@-SWyeYBnnyO6!PMUiVXNf)Cpabp{Oy zZB(r~#=J(yj*TfytYhu3hXQG6 zpxFr035XB@9Ysa2K1U(h6|b8Bt4#n^^rMTdg>^dCZ8(XlNb`!}AWk4biG13fz`Ivc zf8~PEDv&g0$PE+KL?$b$>yTwEGM5d15|&tEi3)4Qc#s9Fbm)lY?!^_z$KwGtT1xu> zPjiftPqizE4@P@QD96*3jgL=3PGqoR<64stMsa^U6$>&%KUWojU08MQWZ-6qqzP2P z60vodz!A)#|G_(4+8)6;guIfP$b`{D!I^0GW~o8I54>ar%9g+ynXmvElqXk>DLqDh zVcAp)ZiG zprawkbVLAa*1ZXG;?Jy`W&f@`yWPl8OTgvrSpZOO_Sv0x$*Nqq_yO=#_U8X?er>97 zk7X#H+;y0#9X6)zhJ)@oz=`i*5?yyy0y`m|lbmf@!Kx+@USD{(sqg@dCV`zHBYNmy zcg?*UD*$Dx7@VV^LyBO>nM$Edg;YaXL1mo-Y`Y)>pB1a@6~Sw;u)pY_9GRL#^j8fF zJiiq+w*m=cL3LYTkxb+vRpQRF>Fza|Fc#GIp%!J48JU4s1g__JkeolAm*6L;Q~5|Kt6azOUXwJ z4kVzYawwe+^&p^*i#G2;!$(E1?}?ByfHIu{2~@yj>7bfY^ru_O7Gjl+^goaX1r!Cv z*BlG&Wt}2op@$mKY`I2SL%DZoM{!JTj{b+*~kPreN2GcG@LL&~E_k*U5p zVG<|$KecwNbjHP-lyA3JwmnJ*DrJas!fw=PHFiD6Y@%s%IuT5iwMQ#m!9qv=g@kNJJ;y@cR2w~)Bk*z%9u1pVfd(6LeuLfsf$EdXTj(2lIip9kpseNR7a8K;GzjfqLdo#Xp&?XK<0o?O_K>iXZy zfZ~R>K*y8lH&=sK+p8At`OVx-n4QnB_*h(v4|N)^auM21-^K4K)p62!8Y7JvKGQIL zlpcB``y2DxhMUTdyGK6WJhz|5zu1|)^!@LTZ{KeGwZYN%0c$QGV;i-U%A5F{UQv_H zb=_?r9A;k7x&`M zTe0E#g??aFb6)@uq@kMug48dLVY9b!HyiWhZ1}wrk>B6g)AGQQ?ca+XReiqCQ24#s zS(=YWx%XdRxLbYG330|eP6#q_+M1DgJEhpHOO*00Q<(#ioV0( z=vtt6GRPOF>_k`*!P}}Y!Bp2wMQkK~7|TKC3^0vVM}sE3#J`(m9Idcw06+tX7EvLj zdhAt>3~#-s3w@$<%Vh)^+nKFj$7oDYE?|TxXXOHbklOGdbK48Qy&FC2(mq9PZ1ZRx z*&LM;v^9Nb!+%R)*LsDd}4vrs)yK6oLDZ=^h;`j z>a#?geDu5PN+4zYV-ZK`zJo?+h#SU*rK4mD;5oRH#axlocRJXmpc;?r$^MIE88Qx5B7e!=@E1esMZc@uH9BTmt8Lhf&c}X*kr7& z5b%CTTXv_taWtsepofqr0=O7jd0#P^$|7)dT+by6V`JAYf9$)`eCFbdkFjZ-VN-fe zqh89!rC7d$=$OOo_S z9eb{!qt9YfkV@Lo!$Dd=@o@N%Qe=W%h~6lPVSIg+9CUzr$Kg3Y=WfN>)w^D#jrGTl zF|UJ;R0^mH$&YK^^xg;ZIg8AMFWa2wV^Jrk$~sGhX3&(WkX#p*B6<#n)C%AZ1iZ|P zMO8EO4JCQ9^ks7Nsz_?ANRbU;=8z!_a9pieffa??hv2|~UJ(>B=AGH@KKx_@*Q3sg zKm$P@ll;;iMOXy~)C2@+-liMK(gutcum-g~Rb@pOl^M*U_D@3?$K+qz<4)-$&^(@*8eqh z{$>^jBLYC#<{HMP-(!`dvaap33~l>et|x>=c4dc4u<#zOE++t|#U>4E9NrAWhjl85 zZ~(ub5eg*@1kO?f!WTgX2X3@oSBXU=nMkOfD=-tVT4nCQtJbLDi+w(J%>3^?heja@Y^#60k_6WVk=Ky?s95XCKmR zz0z_G(@x*30ajB2mF<(9$6S&C@l3RFJZ~rQsdOIyV!v7mDCPVG#(?TO2HM5;MvOsC zUFMY&F&h{x>~-?L*V9ZSag&IHb|~3MO_X+rj@1~1SX^mobBO- z(meZoAJd)Aa@W3#<0-;_iKdevv6hNcxFQ2}w^~^}8KHN2wn{GLTZT}TN$VU74ht5| z1UB-RAl!YXEE}>P?WarCPAV(16ibn3bYl{;pK{GbamH~(h+R%8$73eeBMm!9x*zL2JWVu&$K>Qe!DR#>oUs3Ex4%imd)rUdD#vylc{B zU`Ee=W!IGtfvX9L#Y-A@PAsej^wRlHcPw$JH9HHHT!EtJKi3r3j+KGFxZ(luWbNEb2cR;<(UIif<*cl-?Hj*v*N{8JJa*uLt%8Zz6#~q zbVzi_$1*ltg}kf09&Vz*wc{~trEE0Sf+Ni{gs{>6B2W2(NdXO^*wtZh^jK1`iMR}z z)(Qz&#ugI?*sdw)euL6<=(a4W#!VKL;t&Mg@jMJ=ub_l-&`3j14&*RSYUe=VFl05B zL+enZLt>R=6^_~porYHVw)@ry81>WL%c>d6-|jsp_-u0f`_+()(vVwEtP|b*ab}?_ z$*a^A<&5wAdKX1Xy}R(QsYT!R=x0QTCFFKElz z(3Zh_$c=D)etB`b2>q!MWP(uS<0Tlx{XsK_doXS=SV~Vcyl`O>OiOYn|Kz^Vk z@w}Zc`9)i^cpD=kKv1TY9MjINK6CCLA>QXsgohBCg+EyC*5)AGj0bEMnKQFD_ztZ= zmui91Z;ryKWVgb_baAK>C|(4LWVB`vqjuq-Ce^&`bzY!u!#VVI1*(#dxEd1M9hQo!ime;OiD|o3>T#x8=zkm+ELsul1tX=>I-f>FKc&+^RcFx3_pJ zsWJ)c|5S_UH& z2V!#kdN0_TKwvcjax2@m7e*jJ(h)EzSl~eCm|*!7`UZzP4GviZj`Z`a(4fNC`8xH2 zk11d_3-PKB?u>Rg&C+lt=M{#-a5#8k={>W0L5vV&NN1Y~c_dLjS`PHb-AZ8y;&C7p z8u&KVRGoneM8Hdaaj`gd0fOVZ$kt#2w~6>}*6nXUHMmyh{}$2mW{|H|VCUBJ_O9pc ztA+;BA%*WQ)FISkC-OW06LzO!{C^{h;%&8p%!?O`>)Vm>WYpTaij!OMi*K9^aK3@N zFog+EAJ*hD;Rh!WHssqW!j@?F;mW)Di9{ipC``rq__y<8MCJcusKve3sai#3Rm0L2 z`@ZzS4ompS0Qli*m>}AlxBiIZz2V)km={~>Z5Iqdg`6!@$-j>lSIe;12aty*G(I+& zxOV4ni`d0O2nwglUrbYLJ%!sMiZ50A7|*oYuN0k$7IeD_r*R{Vi!jXl*5bvA7oSn1 zzb_>A@VjQZQsq8r#PMYG_#KjOE5)~4u|^d?T9E5oQRsVnNue$(op;mO+-C75`$B_f zYZO+y|xy5zRs6XqPwFUVr98&RK@@ApgX;u?aP#SiJ& zW$Ok{Cc@KWEt(yK@Tv`C!0E=O>%ve6hs89(ktD%U7Qg;^z>(I9(&}xbJIL1^`S;hF z5{FTp^Ib0xzTXD|cv%4rEbp6ZkXs?U8+!uuVo=WO+8TdUKDx;sLXd$eNN=${-pwF$ zk&7jFW$8WsuHQ$IcO9EfmzOl`oUwPD$XxAcwIhc z(I#uL_)u#P*EDa7f>s$0b6|38s^Kv!9OEp|e~JO-3dpdBlZME{B0z<;f};Rv9d5$q zcJL2E*qw+#*Be)#t?>_qKkM`h(Ege)J^LaYxJO$bl>kUf|XQ$P{LlEq+tkU-gc#!TC2LTU91GB30vWeg# zYs7Y;AdJD?RNuJIi)&O0(hzcOKf!Dl!ABNDiq~)1*b1?0GMleJv-8lPEKqc{fR+Wy z0zg0ZIfO`pXe`h+S+Ro3YE6b68`kOl`uO|};|r&s7UO15xjnmBWfv`ZBwl*d_BHUu zSbl8JbjOwP7v^sBz_WYGAAh2le2vjLs2BWWn7s!JxzZp=6dJfBjwYG#s}zbJ#U?5gN4)Hp98tR6(qSJR>6Xuqe+V zm^)H+L4o-+y&v}}!+%>Bn7+b4CigMeCQV-F$KK1|SN$x%!<*@Td(DvkrMqS1SBXVd zUZK0t7L*@GfZ&dSvdMXx0Fd!wUMWNH_C`R^QdtxFH})D&PHZ!vXSbd{LePv0hqcz%rYoSaJ-nbE z{_@J+oxsPLO|@JsKTj(^OBZ_TLVN}?MP6nyQ(C*{SA|lS!MDNS{6@j2dsppSZ)UH+ zGS^`#9~MtPoS#Y?EyN^$dBmMeNa~E(hzshO-{l~TMRT_(04U*cIj?q~ahWY6#BXUM zoXD=b@DTYaoz{p6{=!{6nLc^>j2#XADjOhR4u76$F-$|J*2-eDuKG(JHoB}!fj@%t zr=YgO`|qFTe|TY(MYO=tKY2;Osine^!9?ccRIf(CGV`q2_oClL8rg>om{%v0LeoAq z*QWvmhm$_8MXMTKMEcLhd&GFh1^W>1)fzOIE^F?g2$?6V)fMN^F3 zqaZdO(9~^==&2sBm3#CRa6FxB>rkOEjxP2B;}P)FWVl6^kMmT|V!mK+t7|42Y^BSD zp90UIo4&-x9AM##xEm^&Oq2Be{yu&Tn^Sk_1@9}LPkNjB>b__hawv?3G!#^bEA2=HD z>6c<3Pl1Bw$R#a*H3}xbhKDd62zCgk6^`o^o56d{QLwM8SB#797Y~9BhS=M)xZBq$ z$HVy^fZItzen$O6r4k#LPy?s-eVJ>Ty|NVqAut##sbE(_$#(#e)UEpS2?MH!bb z*oVHI-NQB8f-oDtTZYaH0l=7x&`3V5w)m{|+g<&Sbq<>FFCI$S_3q=D)$%(d?mThghqJ#r$=NkK46)m%mnp z*XD}?Ry_m8oriousFb#>z;SotbsOEo-* z47T`}vt7p3z~z11&m)rgEdDVhvFiId;?7}C(;JF(fF z&sU#29XY(XcqnV#Gx6gQ#LuJu%W?@ga_s87U?Zz&%j=`dN2me*-{=2aQp-Q^6{h&| z2ksp%z4dwPf0|}f#{f5f*bd}*;kszU`=fd|I3eg8@fUa-)LHz`6LFrctT?NKm*J6z zYvtgFdGEvdbRqw5A%7P^@ZmMcY!MtU1Y5O&wgH4O&(}lBG!oC=*&fwN8n z<5{Xeso^`ltiRND>%r!#l&5!(ttRD}rhLawc0`&StGkt4;h0s`m^PksHLTQHGB!Rv z|0&YRLgFbsqMcG<-)1-HR_Xd=i~IrV&l#;>Dl?>kJRQeVi{l0^!{zt2e-_yX~*#$?Uc6>}MB`8l29Vy^&mdZSbP)ovwpdI`#*EUmlE1J3QGt7&fy` z%n&~OrEzTd-@lJs9yi#rbKA+;)g|<~eOvsl&+QbxFz)r8MWfoo()eS(K_hB9n`uG7 z)Qz}2xWOvwr;SF}qOw6TDRWpW=dOC5Z(=2IP%Dz8?(g(#7jceOR@vgEr%ItT(FAK^ zu+=IqXb0+GB?pY(T6sOsC^5RTP{BfU*?;_v{bW>};pcTUaV0#YG2|;&`!Pw|>c9+X zjs-4u^ELg1j`POvn6KQmP~GAdWrb8LTa9;%kBcydqg*`BK-KI|Zgpxr@?=mLnmrY| zv!Q5t`2FMh)!g9I#N2P8s&%Ts-k8{J4~ebBQ=yuTwIfh=pU#>1IZumK73z=<4gJ(d zoq_Ia@;GiT9nxvBaSg~8j0JmP?0U=}k)qb@pXjto;w-P`MG#RA(WI_nRrgaHfg@^X zo#yi`k9()bnY+r$E+H-bx_s^Qo(3+ntLtNTrz zKfgDSk{OfvEjMwq=ZAYWF9y`AcIrNO>s865eVca+vDha=50gR`sfAj}icqStG(2#H zmTS}pKbGF(q)ghQTelM*1NxKco8#0ys zzAyLLHoq2U^TcR^FeSAu-)FY(uF-L)#^{%pRS*lMfR(YQk4PWMT0SWY0PZLa9>D4T zXkB=yYWU?~a#C38J};r>ZdnivpPn^cM{~U9qsq8Kz`1U%EGCn6!X)CsukYLcNUoX0p}81Sb+v7| zpG^YDG?lz&YTmsx9wv3<-gT;AYm36&tvg`M*eT$5ztb%sC<*2%8->|-H=|5aUSSMC z)|wfeg>rAGL9PSJWvR(TK!7P4IR#sn*ysmeOp86eE13{G`=HJp^Gi32HFkU3vyW)R z&8QdK-RRdBreZ5ViLR;P`;AUquA9T(GC%Tr!Z2&8{6qN~(c{+@7B#Z`txXNtWN?@F zmth6dp~7RN zsX{~cF+x)BK;(pHbyzE*B&eII7bP6nw2+Rll~W80bq7fA7(8oTMXyWJudNTcGiCb@ zeUONJy=K~egjr?k!JvB`(iV!EhWS-YhHkfQdn|Y(L(ExcS;9K zo3)FMLYLZ#s5`&n@(4ps%zsi3{`UDC%(ae=9$DD%8C(r~^A1>iIO4ofQIuexTZpUs zIL+%r ziUrA~En^{}mFEyC4t2kQSS1hY=sy+M)|;wL6?%IA&#WLe*G#QT|Do3IK#jfE`~9?k zt}s~tmAZ-@;yqgEn3f-C{-5Ff!A5pPdaGiYR&oy!?`zO57M=@lzAsc3cwnZT&)T>& z^xpF^O9rdL;%*tTWB9fp`{At*_h0#>YZ^_mc98A|j)%3>XrI}@28ECJO6KE7d6!ZS_j@`_z*3o3rZssb!)wg0kS!a73O8`=tGTCkIT)V9%t7 zLnE`$nsK(ZNolXp(?Y&SHH*AIYqiy<**6TQF*zC|TZEA&)l0Bgu$F62@Ll;oPqIb( zM?V0Ec1q@oXCH!xOg3xI(8{-W=}i z#K$6c{5C=E#6b^GjLY^qd7|7Ger%Yw%isl1&L6osS>RM}UxoeO-9Y=7+N>g@}iCXF3tn zJ?FKmu;muh_JywVM9r@vFN0~ZFs2lw`y~tNtKBK|P>`aID@cHS`D)=ZkkJ|P3-*PT zwd|$1Z}*D#z4DO?jR)qGw5jkN%YfD9uKR@dvmFO~$EpjQeI`^*XjAQIxK(tYa*E>V z@X-OwouX;PNdoAvo7iWgqx~PRZ1y2dS2vm6GM=;~`8~as0p0f7)cB)Esdjr)A#u25 zyB*RXUkCg@A8(VP9U`b!^TrBaW2sq#D9Y1Q^W;~VDsWGoOXjU?4<3~d0bzc#)w?OK zVyV^u#k`(s>_;W7OHCF*`U*Ihh-y3|HPEHlRPMx;vR!j2dN%!74D9&F2J8;m9cxCCqmn5hy>G#f)6aMzU@gwPD`a)_04B(ikT zzl_A|)8@gQXQZ3DDYPZl?{V;}q&)qlaEmmccVHlm7)zVtV1!bGHNYm3bklFCUIE*d z$ir>O0Qxe??t4M#ZKv%q)AW!~Uz?zNQ!hT;@YIy_TM#@!ld*;bZ(0^x8xZ$f4z%Od zhS|`F=4}1D!z2h$kHaOJOC2El49tOVp%hCwz}%tlCX{MWeKzbjMaxbhMVZv{Gc{YH zdSrq1!a2IR6hv_&KB=kxjfA~MUJ0fBVM|>hG;}?umB7(82byI8H&II~oOb;<9d||R z#cQG#-Ue(Vl-hm+a-9cM>4tV9ptF#M7I8hQDNZZEzwfbs4D;|1wjF7w2cUoV(L#GP zU%zE;bV9Da*i7r-Qn3Bn3R6%kJ8vo$6|Zic2E4)RH!Y=@1F9B3peHvNd4JK%OkLn~ zBQ8zO!L)=}*?>2#n3)y%IFHk6DuG(_zy$S{^~!glqBzgB@i09NTl!lP5$F`n{^aaw z`;dZ4LTPn+8VbRi<|)<^`IgBh{q^`0zb#0HG!&g?Ug@vl*=OOYD9xYn0U;eWFy~h3 zhX|0`iZrerQwFlxI!>NC^k_6aTE~;7;Fw_yDV@)OBpaG%hKX(b1c}~n zqAS(brC{VBGr*qD_y)&fU%)sc+$9V zj)o_twZ`|p9~EWeX+95}=;$}il0LJwR0|nY9iTYJBi+|Mal?j(QwqZ)p%GM19EBTa znCMGn+ap>hU%cBiWy1Wt*Kny?e@W^zToM+-R?nhX=}M`(>>KGZN+P3zEa2t4LR%cr zICF0ZkE3KrIemsq6H`J-{YGgNj4p5ts=ets*~$~7)FSZX?X)6G)jiqz^&mBzs#z(; zV~Xn8N7cXn$dtFqA$rH5*{WH;$NYulE9#0#)0Ca0fo9WZ)yg^6?DOGkp8=2k*JIkh zq5WFUo;s&ui879{f>;sV*PwF2cq5u)=Se-+$RT!sJ#06*3xHTE=H9!y+$9-<4$!-Y>yf#5n*u`}NbO&`ez5Of~)axbZ80 ztEF(?0jftlDE||i-$b=CH#b}RWD(tGg-Nv*a_w3+h2ik{>SrB~xtH!!HVwSl1^8m% z338B7HJ17gH21q_(eA`~-FXKZ903oWOmXG~JC*j?ErQGxL|0r|Ru8wM@5@W|cUBUg zOU+<+(eo?qeHJ!KX^Z0xah2E4bOcZMz8;J8#AuNq{ab{l=fYLc|KxoI?zsM0q3bCd z$v|{9#-*TVPH62Q#_sgma2MRvIv5cWoGk>G$riHW$ByK3RUdJCyarw8JA5GpZq?7~ zWTT+(FP^$UoO$3Ear1Y62M~*F&kpyyg;aQjfQ{4hT_O42^Fs2n&1^>5bql4P7Dc1o zMLTc%$6=;3*O7qn(O3A_acgwzKUczL(q?_UpB$?Is6ruH4;>7bT!~B0A^rBuvxhTj z=aTX?<8;bw3eK4HH_sT{qD0%|Dp0VrTb^F2JX%;B)M2GQ#@Q1m0y}hbHm!jQJ5PI< z`+XRA<{+jR`vumJ=|r9uDVj@+2D{9j{yrTzf8g4>JhC}P<>nRo16i69luxP#52g7H z7e)2dPi=%lVmE|ELvFzlGf0Mf&ne?LloxICQi9T1$kP_?)~^M37KJz9SI@V%D8 z<~*px&GSt?m*nI(GC;FkBj=q`4q!$RBQ_b#YI%f!4JrW_uLF^b(%UonO#?LINib>n zMGB9j;|Fr~hLT#v``4gkrCoo$v2yu(?F!{;jd-*VG1%kphkhZvGcbI#=kUII+dy*B z*p3RNe^CE!Tizhp0TRNL#;>7tc5K{){7^USAzix|>-BN1&xQzc(#_kP3Dm)H>_b3C znbNQ{5Gf6$D|}80c}39e!%nf6#%Ma`{dzMJtpTbHk?Nt#HSea`sDS*c^4!Gz?jbbe z7MAgbWFPVx*pCggQUP1&QuUT7ILM^2;f_QW&!PopgrYf&(e#p}<^%bfl@tO=P(ggU z85ynf?>E2)8wNJ?0P>JfI*T$`-5{#813I_tAJo}7ut9~CiTd4E*g7H9=e8Vl2*=q> zaajVbct#G0%i04J3kYS?8Zag!{?i1z?>oh64Upv3r+*W&$TIJ;nxe=@M6>~_#cvC|xzs4U&y)moUf-0w7wi=B z>_Z;NT1YWo1gbF|wL)l{Sc-Q6e-kuv-88V@nT~KB$1k>ks-1mz_x5l6&Ktyhe*`-E zNGb2X45k!Pv~Ox;gmI7=o9?pJ(bLm$hrh-#FRF$Z<9ruo@%1+(Tk383> zN#<&|3f2tH-b=|4EwId0G%uQxUF+`(bO$V>S;3v$x4JHL+dj? zJUQAn{Sl$uXQ$s7pQC8Z095fnJ$jD$hUlmjPU;X1!qC~;GPWB6c~EZY9=cq-nu-23 zsD4=G^cKfo%a)=2y0HW9uHUn0!>0iPuH#srmMyK;51i!KzWFZIVStwNhq^_?wH-K4 zYw5EA^xL=S*)0WGjDhs;c;XGg?(zKrYkdc9_B-m%y76dELe7mY+Lo3+$9fuGNL9Sv z8A974UJh-c;vxO=F`6uNz#Wn1+TCYO{Lf9Hx~%Vao1$Vg)JS7q*27%;0V;lgS_=Ua z{lK=*7ouDGFk3MdPNjRL>urMHp);fTR?~kpTE<>^UoLr)VNh!ov`nU+856k2r|&u# zwB?TX>zF^mu$??-9=Bw}bC2KTbyewje?@RccY?TQ1nf}Sz55o#*=a=1 z%K=7kop`kBiK*?oXhb8bu@FM?<9Z3X$CCGi$R9Ygn7#YPlcZJq+csn-R(iT?e7Y~drJk;eNHx9 zDT?OCvpPiMdRv@4rg{Z@@17Yl-V6Hp2vDO?^Zs|#9{Jt}hacSa<#p0|;CJ;wuHk(; zJg^mlEoqj%+4$*As_YjM;O&(!w%Zonypm$#$;Pw%e4l^L0u89|D5EFWyj0tJ=kT(8 z+@qB+OS0U0W?U1FI!`-o;Ff~9)M>Kg$in@TQLcw;&TcPiACOvJYq)dl>zU9D;?ohz zaW#L_`ou$TdQ)F6#3-kz&Ga6YAL1N&)V)8XEx0Z0Mvhi$*p;Z;$G#Uh&t4AQKXS(T zg!b16LCVJq39l!<;yn-Cu&l+SsvQ{*a!!6faa5Pk(T_lxxh#n$`&L;{*rf8*`{4)t z@A^NKkGvmdgM$yH6?Hk@E4$?U+u&&J%Y=01N)0?u|Ekf}tCjA}alV!zdwo1>1fLQ4 z9%_S2#HT(pyEZyrc8r^??tHIib{{O=ULBWkPR8n(=8%KqE z8;(-T3b$z*j$Ew+qToo;tgy^*R9044X4*t^l?%(tY^k{_D_3Rf>!082|9br%|L!L| zI}hMK+~-`^^?ASBvU~ZawOTzp8jS)o?iJ|dl~IGK;jT{BL#t;4x{`>Er-2C&g zt3@x)D@8v^xuE;++9-l(pI)GsujhY|(1BiG#qW^$C*z}bpgxfMNN!X5+@oyADS}im zImNG%f9@6Vs3ADK>CralPNTBS`cwnrz8Uk!9nZwSW-cdRD7&DscW@CY2x~5n)!sk0 zx7;wX@mK7V^7?zJr;{&?+&Giw{V8_CMQ;T~%_!FtPwniSk;_UOW)`6*v@`CI6*i1z zMWMEty_ziXwFsLjF`5agJC<#BsY7S_n)4I8mzo_D3Y)yq?#Ib9!%^J*{Z4E80%P5R zsf*c>JpEz?@9am|3VL>3p-V&C6wi2O7%FQ$BDnaR(L4U3LhE;fyMYChAIZ5A>Iyshb_5P3RSqUlY&t~3>LA}Ww{q^E?YLD{En$%YFQ*Tm}MtXPc zf4<3f_odEbr*;PwZn%BzE(qb0zla7dw`D7x^}e)!EJcAB+U#xEARM(bn&cVmo{c=t zHu9=DFp2r}VgF|LUbU?wRkKqphlEE1FgYS2hV1b5~-c=PTbDhyieZcvF-k= z$kH#(H)hD@tCFr7t&A}Jf)Q`xes1;!)`e3)F7s-X{jN3SR|I>>KdK?ux301ADGQ-0 zmB!b^kIJ)0LkncXr#9r4F*8!*)PqfaL63RflelRb#aE)ZC;0)Di``mB9Xm2#?6Q5A zdiQFk_LJ8q-n>q>HM*^N=!0L+?m^?D9(yucbv7B_9rFs)%Ove=?v_dU9Z18~% z?JDuqPRB8jyE$G*@%h}yYZV>5jnXHqXwF@gXpYaP!nKysU8DlvHe|6(SH+awoYwY*Zay7;yJ?zA8J*l2ikxxZPEf&hGzUh&U zYLv(FgGKD8cK;ZQv~>VysrQbJHL3gM#@k923sP>z7H z@dChg$mnVg-KP`hE#R766IbgIy6nnmNL#DnT(^CY6ASTODktUhq{lHLgaTb($1gBl z*pR%gv0Jx-!BJR&sCnxkl!C{h3a*n%UWQJXucG2dyCT<6W=xjqt5^P+)iVV&6Qsi3 zZCjs0%BokUBFw|hyVCFJYIP9|Kie<4kW+h=JVy#i*ILWYigSsc*lN}F-SxR%=Zm`& zdrY2Yod_j=AcLCG3-u8__Ut~-112afpF2-eJ*PDu3AIlXRprjKz%=jBy3GVFXA@Sv zlrN8VZP?dTEc}hpn&{;;f8{>h?-y1xi z8NXfC7D-m#`?o{!xuu80g_^uzsSWoaU&MyQipu(L9aH>8zDZ0HHGzh1sJqa6M)og-H_DOWBp_`Zjzx+A3bpg~RA`q? zOUjo!B7aSuW;Zyw)}S2qw*yIRr75&R3Q%WB8sP-7BaL2=N`~RC3*4~XklqGK1lkX< z{#I+j(eXW`x0eCMDT#Bp-*X}01zgA^7;dc8p=0#3-4P0M=kiN5(&eUn&Nc-ml0W$J zf-8?|)#fFH>mvMNEq2%B<$~>}yokAZWN((`Fff zA3Q!wUvE^lt0Oo-j$iAnP`|=|^8LM5vP>_R$2ga+)Om8fIqLIO#hxdQgtd;Pg znw>=($iBV`?i*sE_c|jCJ)I4a`@cQtP(M;n?ucF*-2gi$*D=4;~q%p38lEOp_b!2^o0Fc@C^%J zzs}U$cb_DIa7OUsr?HXxR#Gm5yBO~4&{(sjzlNcpcVZ?{yLxurc?k*s)OehwoT?+I z<}ds~Lt90taN3odSp?IkJ$we7)2J<)z+Nutw8SF^gV*0?ZnVN%ug1LT(T%s5GwsY6T!u>N(b>fFRu?R1Pte_-012HG>ugD!ZMGp6F=wr|Mk*RfZxkpGaC%l59eR+Gd8rSUXC+ zT_@ZLqOYUm*shoZQP-w?%AZg}O+kuMRqD)-JrS8<3YQIcjQoKr$K#vRt8*}=Y9VBQTv05dw{$5-{G(0{=P)Kak8Rscj6Kh z3h4ZTBLH9-Fp+K%-~zN5!i7R12sBy-4#i>+IIN5;UQt$wq9}`1l$TXfR8T=9R19oX ziE8R<${I>I4P`}*|5D7|5lO=-3{HCvE#mVLA^5ut8DlQM5 zswg~F*>dXq4MBd6peRou5L8xPsO___N4G^l<=(N!kNa4dgd1or(HaecJXAv z#nUGm>MI*A-M)0`(&ZCDSE>vfZwy^MRebIC$Lpe&8%_Kh13zxvymIT-&6dk4EmzLp zR;RYM-fnH{Y3-J@_Rh6ksBHW2=T2wyy?ggMTlRK#c6K!hy6+5jci!!OJllKsR&Q@l z@8g%fqrV@wTzWkF?@24X?@3>O%jJH_{J_$$!QtmaH@t`LPCOmD{`~oi7u~Hd`nq2} zfAaFx@6lH?uRe^vn*00e=i1oKYh!Kjv2K~ylA71jHTCh$9hEl&5pUi+pL*dnHT`*7 zf}NIFO^-gAe)D2lx>o4C@I73bQ z^d`(gb8D3aX7I09b3;J6{N1FTcPyfOYn5H9tqPXnx(bYDP0!{u8xN#zzF(;2)6y6w zf^kp3<-H)kyv6*W8+`eeh8ur;fo-y5@wNBWfS%M7Mz<;LjroekyM0@B-K{8SlU@0I zs%g6Od9#0`O^U|td)Z&qO>N6AKYrqH^TGZ8JGVZ+6k*v1e}*)@ecF_!8gu$;rcLsF z7VO)BgMu;+uzvUn(90J`RkpWJKsN#X8T8*(a*H+e}1!QZKJ^Nb>lX{=7J8_ zPtVo|oSWS9``7M~^Zo++u5Z!rk6fg$TzZMwAXv$KeDLop+joAiz-Ph~th?a^2gzEl z%#)Y)Om@E+caPuA0)8sq&!Rm#VZu>q@8ZN&-3MoRSmOM__?_-&Q>vh`PLe?ec76^NWNlWlN)Q zf4ldbWn9gP#j!bS^}G-3y~{E1vdio89{JY1CLSDgsPccyLnvu&+{#t&&c_r-s!cvX3V_4AlD?RCsC>j?(Atb0a) zt$xDbUNv|U*>j-m;@95VLff9^FBz}5hpGA7e{{U*=kQJG&R(0Rf7hbF(iRS-eYh~A z@=-OZUT18Ws(tdy>Ic5ZgECjGyN?QtB=`RP*g8D3-p?$@F$lY!dx(8NN~4<&v1xJD zJ7#h{{*kQh^9rc1-4kKvW21K36u+(~Z7FnVLo(C%)gQrhD6(j2S&-}^D4U4kM8f38;x|8roPCa1$MxSrL zMKyr(q;1;PX)wp{!jK7Efb66UP@NRuqT?Zlpyaob?v0xK!eHOr9o~#~>*yMQhcP1( zrFJn-0CcP9MJ*b8C^RhK5=UzQf3drID~R#!uT?*4EF`RPLn|i3F4*?bB5lH7oCuRj zulfXC2D;H4)qa@FfS^aqo&b?UcM^OSSTgM4W{+JTR5f}#9iKE65-W7IbsbQoeU#F* z1G+N#(&65j38JXVg`!pSH1VWUr?XsXjHyelWfjZQ-QuY{PsAZ>mT~<7Sed63y12Qi zFa-GESfAy_@n>Ld#~CUoM*%}=hH2xV9=^=}r1O*ZBCA0??Q=XQ(|7H%X;r!MHlqm3 z@qj|Z@%F$zLxh1F1Q)hguUp3jQ?U+dS- z7+>5z&I+Y;GtNonMk}bSU&hgTQ!DEd>OLa$hC4_A6#E83oem;B(DjObB2dd0f_GmX zKNjb`JycSmc>BrsTcUvi`!8dG&TH>RPA@YeIWvz6vW2-?Md)@+4!MT3Lyh;mY zLiDTZ4pm41TreRo)36S^o(Dm%(BzC9Yc#(IcJn`7?S$}3^ z?`lu;@1PKNll*FGsPpj~7-o5=>|S)-h6fvD312uD$3d42@o>qR1@Z^%@tBtLb5q8# zi-qRLv$bpCH%@oCOm!JPTD#_<5_#g7i&f@gmiE>a*^{i}PB~xd8Dp(MXZ|J-S5HRL zrpO38ho%|#I0JQPyBoz{rZ{^%Qf+uOs7mX;yc^`Adau`0n`ZL{`#v*jrt|lC*Km4O zW4rYcWRQT3ED^fBbo&3J1v^!wN0B4rMt=pN^A9f=JYC5rG zzUuE~RC?^@@UiW)^|rZdwM(>>{;^itA>~@IwIeRMvf#DjX6nN>i~CPYoTqsg&DjCP zkz~imfc(%aq5D~rzZ%*2!VyxX)};{HBmw`PV`!Z0mld1i&flvpj-PKE9jGRs{^9%oDPDj7@;d_) zQX=)Gjd?Wquefil>a-hlv;6&2{PX_8;r9p2`OBLhl+tUt598gAFB=rOA2F!kcDm40FL@WTG1lUp{d{=xe*EBCDZw>tFj zzn>dxrduB`&Igi_Iv}dd7!d(dSm3e{bmoA#-lK#p*Wy!$Q=K(r%Z%}>phgW#ew3vh zPv~nREIYG4*MdZ%4YHRNTgEn8Wtjq%Cw|1;?U}a-@=5}+yMfI}eJ;cj zAI^XW-g0Mq<}HD|WtxVeExVz~Hr zG$GSV!Ol>|g%*8t0r6KL*G-FF07?tm2wNpGZc-JNAt6hGfH|Sb8}QdDa4v~ZD}qV_uk|- zbHOO)ybu*MqG!}ml*d3#knl4?__8_voDfM}Bb`|$NyCQjANj~^5dzw@Wf_8R79n2J zz-J)pXdpI=fK3!D_%L8;H24lr!r}svHx4!7Lz_vU1p($lkmaq)?j>M(BB+}v+=7p~ z-B8XGS3d{P?n1c#V~jv7xA071`vPuJh@mdP6PcK18d8cQ;sJ770qPC_@f#sSrA0pl zVHdq{z5p6ph6`q(rs?P?8o^s4Q)q{;kigqP-G*^kEl?I9j(_}$_oT)vbwZ=G*X(hY z{9ar9Ntz5k9*LyLEDJFR8obgR7C-=EM6w?UFzABJGz|xnAaLMmC?cD)9uwv*LT-~OUErTFjX=GSLJui$R{&Wq#s<^i+a<{7Vt5YUc%F&+1IIsJ zfNvvV5`^%(Mp$2fw9LotCLo6ZR4k3)D!hP=Kt)jS{d{8{0da|P?y)o@BP#2p*MzBW zT+R#O$Cq}eT6H9B?R;qWcmrXc?w=@O$fcYAX^In zzoO-2#E>E(uIUw`jetRnpKf!;0!D-y2|Sxw#zEk%Un}fD;HQM}I1t-Uzv&HJUZ%nK ziNNy+Vc->{J00%FJiP$QP{oB)04hOSaMhjVoC&&LUeA3FAUI8NKiIc2;D@u zWVC>omq1*_I712iKaTrFF)rAU5FCSA5@IwKP^QD@l5Fr;iVWJYta?3R0RzC>r6Fy( zDIqZXN-mBL+-Q?Qj8}XFWVTV{+9I*x0+H+MHDXBkSrST~px4Z7 zhC1Q=DeD3na8CAw!y-iBYvfXxf@z4tO-Fpr0{kN%XT!n-5Vo4mATlWD;KP+kRT_g* z1gBkbag(3f^T4*45MHp#0R;O+fd7)nb2Su%n#*)mO#2?qpdG;)x1D@TG{hixC^2#_XzBHW`*TquI#Yh_CGQ;Oiqvhd!CNH z$?wWEWks;i&_il$*-$e*Su-~R;v~VFI-vgylW++c$wVdyk+N$Li{HxlBiRq20sK6v ze}u==7*MZiC@NvyYo=7go!(7o37G-pQ?K^Soy`Luz3)lV8fLsKg1o(Dy%#otGq~@m zF5h+wx(;XXH1Agv<}n)^Uzo^wj}_<%dy7UF^E|2yPGN3vA5YxbrKCBiXvb?^lhyb= zqdfFCQ084A*hMHN_n0*$jY3p(vIv`MxhTWD*wrE&n)~^Sy8cqxZPy2Fp{MdwxVr-; zjlYs{)r361otg7dgQh*%*3+qM7BN|oTK;-p@BQ}P{%4S4>SO*E^mM>G*0}}QSMG}7 ziKOC9g#Fv2ceS6qy;SrdzA&IwS9y>hSDl+#$x?nb)3f#$W0IyQsq$Zlk<-bWOPm19 z-_2a7&r+KF51gNMX_k@l-y)C`W=%35RX)}Df4Z<)t2$Zc+`CWpUq6YCe7c%^^u$*d zC*8|>V(ykTeqcNbD*rLB{iCQ!leJwj^kv4r+XGEyPMIAk{EKn?hPbV9QMYz~dC`)1 zGbZGT#BYE(o0p+An-$c}_#k8cb(w)1s@nOP^HoZv98UUL_F(VV6G2nSUyPHZ`y@Dn zZQqPylBoWQ<1d2CZ1%R?`trA$7*^qS)9qUeV&99Ir}+KfZoTt6Z9~vG5ctlFM)mic zky{u;#tC9~zMlEqHjERKmPiki*8HP2)^?*dE{5It_HFIJti^j}9&?!ocu^lNQ@1Km zw_?gbG#6O0D_!Q-%iH3Yy|-LlpicXJBV zlmd9jJRt0d+&m-?wPwZb69PLjiK(9B#lw6h1*zr#kN&&*OFaNqcl7tYhkv3j$zLgjiLU=Xe)P9+ z?a$3~e_L+1ZcM4`_R_H|ZV1MtO!)}RJ?!_j4gW82Zg!~ZvqV<8 zF!`;$+a`{r6*BTMsKL?UNHN@EvQOPIxv@%zdetG@bC7!NZxo2)Su9GXj@tTg_6_;}JTiP~Ns|y`!g=@((Z(c3W zKJe%&2)bC(Z0|{0(-HVF!u0E*b3>au@BLW) zICRcd_s>xPrnGP&@*Zo37M~x`ng1D?zF>Og!RI7uXSg7OBOfUi-T$er&dQTDS1QZH zy8flR{-gE}b)t5rFLt4nH%Msl_rgR@#I(O&9r>Zmc7z&7Fv}q;vE1a{6r2L(xr~&h z#|GRDNN=NvO0GA#G~L2S>+Z@@U#pVN4I)cuhpep6VYYjy9-55iDe-{gB(+e6EZJ+O zjN_4igyV!H?)L0ZG~lwj=q_3cYjM?c>IX&w_UDH+3nKH&mLwbGZ>9SCJA6;q3uW`7 zhOspo4S+Q?2B@c~vMz_P#`<`FQR|=1xz{JhXwU=aD zg+Y(!L`gPS>Q+fg?q)YCNP>foZi*dcacOChGVrE-eIw)OrM&nzWtL5?R#s)X1V0q&6FXOVwavjMs1qG%-q~KCWsSlLO4no zdk!gQeB52Ut0qrAI-V85*K1k(o$&BR4Ye||-w~;&8rg6>uVhR0^O|kqvgngxC)@O= zPX?i#I44skiJgLP{E5<_$n3Zi;RJ~DMJrxJEbqm=Gh7?s@S^=_ePwb^^`)5jmxz+4 z7l)Q_x4g>N3QOo0)UMmfh)5U-3OO6W6+hN54Xg;8QP!PN=?&L4lU(vw+n(h-qpI@j zLvid1#?h{PR7Qoxs$U zwaORl|0EyC+jH@v2eU>t7WXki-dCXBq2^8Ac&HDa z#=~wj0=t}j0Z!cEgbAjsL7$x`A-?;t&ZaI|sVYJCG*Dbr<|$B(aj`<2 z9dRURtep_scu5MRG$l;R?8|Z`8`ZPtX{cKdeyD!C3UG`Y&Y{kSvgOqmIqMltn9U0; zsZ9%x<*{UTh(LuAO2JtS)V^~#E*ZN6rL%j2r7KF5K*|iZ4fPT7D#L+*eHx?_y950NQ?C6g+ zewjsyU~DC#97e+%Mm;Y&`jzTs?LcK;@)+6FPjETzT7B~7V~;Ea0P5Lmq8k4sdpxD| zVDx$8s~g&>YtcbxUKc;w=}qBp+y2aes|i76{pyvAEGr!>BzzDY_hlVY(COucYt5$g z?Dbzh{ik>w^Y>CNY1I<#qHcKe2b%3Nqj>Ijv;jD7ho|dyYIH0hIvi8BCd?c^r`~CZ z{;|s1pS-dG^8Ml#L@JxT^r?ij(BQnwC{T!FXg|i$0Du6h3Sl103rLH|upY$V?HGIBc4)cC1;N^RAN}&I<_KJK9Pnv-(s<`au!#LVGQy=WqzXp@tPtk`e@dx# z)qyqArtf(aa7ReZkrf#4gipj<`;Z!YSHzhl-cI?{m)mcoUn7lP!O#{cw~;N^FK@f5 z>eUx^rpmgq@cu`~Hxcm1GM(TFLLuuijK07{YcV5->G|KyxAf304hhGo2cI+wHKvlh3|0=f2X3d1sPi0DQ$~?S$dW zEu?#!m6i#@Q^34~$F~a|3|yh5p6sw$NYXSTQnTqy*P4Ss3{Rs^Dqj-k*@Q|Grx_s3 zubPS+GvyG?a=MV&4BqzHPKqhuxH)K++{vg1rcf#X`qbCu~M5Ql2R;b|HUI z6)@exO&)+)t4O5*!`uynjNHAw&MJ3_U1s^?g}xFov4!HH<0+3&FcBb^$0=HK64u7V zFz8G7rUPoVF-`k3-tJlXp_(|K-1^#e((kJnGxhgf)VRhF(ym>v85qtMN7!}t!ptWF zcxjY&`6)UC8xG;61hXFXSN?G{+=Rz6!Cp;Xx0MA0ikywq&KO@rJN*)CqL4p+9d`{$-_|bNKpsh}Sv02$5;LeP7JZvt2sISN!XY zMN<>kA>VT>NgT^jq$U+!9F=b%K^-J(_PLl1}2HPbDJT=`a;EB#Dnw2O;~iIGZId zEKih^fUA*SSkTv+&49rGPW%~2_&CQ&3|FDt*C3oW%7VMZ=cZ(F6ao91U$iJR*NMtf zUvBy7a+fComU3w<&n+u|I;?O#JRVIdo0X$;e$*Q7;Xk^shsN}7+2YCS`JFdSf$3yf zsEk5YRzV{Y;^BuYMuzNZ5tton%OZIe9Jkn)2pZC0-jZA+Lx}aLqB)Um1VGlTrKMl6 zHN~aMQIczB2*IpE4GdXG3e-gew!W+=!*2z-zY{bWURj1WZ5f%tfU%>>{@h1Megi~{080PYEjgEzwGKr{m| zbOBmBL3t_#L+FQ6L^xpJwUVz*bJP7p=(XJWP2);Dvff~$uYax(kRf?uMIes(=ts`= zdOO8vungMkkCAGGsT4G^Y2FclW%-{=v_N};a1cQAz}KAsS_pnG24pA}Xu})2eZVTG z4@#0AwL`NU>HoIbZ4Mg41NC$J+N*$>DwGV`_njWPQWtn#3_eKrF+4a(J_za^2bQW( z-}?KJrgWP|aK2x9akNeF5R?HTJuNg22B5ad$$A3tEP#+ipPpC1fS`53^48FkBJS|{zeYzh&Tm!H) zC6Q_MbF&^qGW=2T60wzqj0ozwz%t{Z(W8wfF|74n=HK58Tkbn%oCJ$^v5@H<-gs$9 zHx1Br%0J&;UFfa8hglY82tWVXSB43|#E_l6d2U3m4~2GnJdj1-D%${^dj@>30^JY$ z|8%rSP~OH42VjKGYzE5F6Hu}Q;f%CxtgbqNZOnpJ$=>%UE9n7YIP{t4^36TDruu#BI%;|6$#)N`vIy8={7<^a7 zeOKW#(*Whq_5Y0CwsA2C#{^J@3h<@V(n7TAUZmCNHYmXyMrEh-S;}=R`>Y~8pY1pX zAR7pcX)0;3!)^9_B^La|AeaAqT&}vYF#Z>+LK<4laxBC;*itxi2j^J|Ye! z6f})T#KfbZc|I7zWtHziM!SYIgXDxsEUkZoI~lj~38B#K#^`A7_r%_KtBT;LvA`PkLmj0Qv+i0->L@C3J;oEtv0Z4ea;Xdd)K z^a2$|m&(~?JE@%TUc|mu+So30*DOR*D181v(KurF(8@Cy?A@gDv-*GjzqSDF_P*1y!ghWhM|yVta{^C>}el72NEZ4<~r)(>O^&IQ0lRQ0h!Q z7BTeqgWhFd{B%?m14Pkc4$aJ>sL@jC8eBZ9;)Dtt zcXI)0TKU*)WXml2_glk3%#mx1p}+CG$FNTG#Pk!1ci!=`N+z3a(3yqk71x-R5zWI_ z6Wg`-3DNHz{dw&oxDV*ok-V>GyZ=h@d~fm;hrTZ`YhVB1z8}v*9-9G=GotUmOB~Qn zdiwS9p!?RL>Us8p+^|{F%OgpNzdsNC_gRYc^%B8582d)wCB1XkT^lk3NBaFG*Fp@o z&HK+KP5FO?{3g>AzRq0x(r%bMU7h@?<#cIl^8ACZPpVOGuO)x`@2g#eMvD$|(H+ef z%KA`~7k2Oe@j_ne0ZGW(|En4mN8v52puZ>Y|MzY`P{7Rq>a(QPv{G&+gO=NS{Oinq z#GVD!fPK;kKe}`QaDt;wF|iMsGeV|87gO%r!XN=hG-u-DEVGFzLeFp6z25-&MFraw zh0Mi)Y!sW2Li~1F<$4OakgIYrMKklf#<#_RGH#Y+5qgdbU6-n}ZYf)`82f9H)tX|6 zN-f}{3{Iss>U;|$;zN8(F3=$(wU?A9uI$V47t;XdHX2&F|@3q-k)pq)2JpRAlM8@#b>p~=h_HiCFf8S z`l8w_<{;`@YSy=;UL;e%O=9qp$2W(ASu>24>Uf{8M!ia}`$27hbrfqe0-~ke40>)V zkBw^ux6_w2wyflhFXkDbU5+lX=&K6*Q}Ql;&ZNS~QkVQVOKTKyX)7<50VR(@@jcYrNTC%5<rr0_lPkB+h zj=d3lz?LjB0G<(V=B#lP;n}UM`o`#XE4+4#Tg}X-3in0b?qYS@)cLtZytFrmP6}3k#@vc1buL2c&Nzv_VP3b3T~U7Be>74GV*>%U3((b1{6oUZt2_p5zDP^4aP zO;3c0Q*`>QNHa)KxAK3g%*I{n8+u>V$lrR5MnLJ7@nylPI ze#l?!(#xbw7U-6Jx?_C(euh$)qaWV@erhIQ#OcHwd6;NHUPw;lpIhIQ}WQU zwJQ4yJT6wXKHjCNWV)pDBqq_|L$O1U)xz`ZO%6kra+&D^)FoQ~K*A*NgPi?bYm}Vb zp({6S9X{4a*-RD~KcIRvuh8Tztt{HbXq)p8XShOj6BH)zNN>lD=0AcU4<{&XweaWE z?bp|#4a*p{R6)DeAE4D>splI5kY*nkde{%`M-k~Sl}=7wGT+g*j7>l6dee{k{@${$ z<*b$BpyTV+m}54tLiFy1j7KXjI=+v7cG&&(xa{UXUlPfu&m~c6IA%-qrA|fXLB+KK zMK_?x)J?p=vEE149&TXk5iAQgaCC5cB%RhO5G2JaQd=zOfq$pSh4I=Q_NT2&TzYQ3 z)$a>k$-H{oN~Utgf~;2F>wIhN7j?<;E*e7(6P7IzE-)6|_&NRkujWPBW6@E(O^%TZ z#XpHksng--DQ`V~CE;+R-iOWc7FvONyunp>*$t@= z)*XpDJmnJY)fq>!^)}Jl-#Ilo1#%tUv>)El|Kv5pr3PPJ?y#+&r@K|f=XrD8nr7x9 zSt`9n)-2XV{ponM+z@s1-i9WP=i~WS4G`~R@pbA}^}U2!i_PlEQHPuz-~T%Km}=Ks zQ&F5aov=OIK8cMd_VpsnnJgpalf^~HrgW{Z{L-^NvRM3HnD5r>C9fV^r%{fp-7?T4 zYqt4|#yF*RbDuufGD{Kv#_AEVTcobs04kKuu`%iZP$64x(OA)gio2xF|C*Ab>o;J+ zETb}4(!TnXZ2D(bKur2}-dxIrbaAVY~TtHe=g)Gw`o+l;n zjm1uKc~qDVJq2Y=KOt97`r`PMCQ~S6$xFvZ^sHE1Das1FdsPO|WLoLC>dKmn0f>!# z>9Nfj)(ST-T3l(`UVd*$H=;`h<8VjW_(6Vwk`Y(w`}3B=WzB8G$H{h6H9dD?%-7>< z103njb%)AEIDJ)lWSa<7rFTEEM3k!+J5fhHi6fRsUwJRHFuvkPn#e&Ug*|bK?;4~L zH|6e--YfsmqQ!`RxDh~)1MtS_8-^sv4vIpqX-a{NpQK_<6<1R{HrbA?AfeQ^=jZR` zvu{=ix*a7T_f@SsNIih+oa?s zOJU{iuc*`NlRN_2naFiIE4$B;I#k3A?q(j#c_to7GHaSG4UN)VM0+?)>O7PA@S{Vm zx(8;%2vZV_Z_x)Uo5EW->zjRJ@&xWm#S)G_u|qR{0jhe3j|*f@U~gA*)oKg#wbLeg zTJD>M03<+7K!P!aowB>{M}ISc5#RGs4t{G2c*i)hc9qCA%C{q}9bipjQI@g-r*lWKvyRt|OA-e*%vpG;y9lFO*j)iroxDfr3)TLAn z5t&4@)GG{K4R;6RT`hwvmPlawW)Ud={<4vsEE6|?0zTeu1Jlns;NDRyuVcl+`cX3! zYYBPIf;vLqg=u{lkt52nYx+4B(P$;=W`BxRF_*X_=YglA4)eIi`&@Y!>dg zK!YpZ3w^ie2jDSIR(A-TOoKu^_`9+o)|e+?f_YH2Vs@c`ep)ZDvR3vK8g%XyN_8ulojd3(*Y)cL#dfcRUyw ze0n}eL=S{e^Fd~lBDVz&K>)G}lVs43Q8E-4*Hmw<18N5#Hu-E=Gyp=??n?{spmCN7 zpG5f-*`cShX`vZIS)gwNj}b5;oDW#CBS^ z`$n{`EE#0pr8XXd;FH<5^I%ghiae?DwPRZ<)y}b^!_a{M6~wz89HiaqE@WE{uF96CXBTPDVJs4ajJ^B+yNf(AlMVekaoRw}aZ4KQVCoLQjdu{|b&b<*+p zPNHcLQsr;0f+PzE2YdeP2HegFfyY2lNpuZaltSdVd5F&coGc0wnIg^Y=VR#OV7fwN zkwFVwdF*6<5Q5Aem;q5o1`I0uZMqauN{D|nIDK49Zd)pK<8jQjw4HW;-FU^pc!-I- z-Zyo<%e&PU{XKL*yxjv#NCsIi2w5o_pIEg;3B=4TfT$fbJ*0E)?s1;i*AhX-z`6_p zWSt7`?=K;avn>}m?%ithWHqh?Y|iYVN=3^p7(tVo-?~Bcp&lHUefWpku38QIBzKL~ zS(DV%AAmcv$M=Vb$bG(|okML9jXhoKx1cfh(2!yhEF-Zt7NVezW5IT6MmyW7h;5~B zHFt_VS!#P`4~dI(NhttSlL7+(+>Qw1Kk#p7xm{vfzE{XWO0oemDg zvTYXt?s0Y|zSqA~O#Jr9KUVWwXMj6FQ`Q0|^e0u@n41<&+t{-FCzX$8IDL7Va+=+3Y z;VqA5e>$h8DA}eS+}qh;LIbjyrM9qRo6FemaO&SOq2?_bvuL#$fo(gaPOQ5 zOij~JSWDIAr6NkpO`u~;B(g2aV|g8e`Ag3*BOg$&{Muq#-!N1cm92AptG{r^P_f1&s>~r zXOwAHH1zZwfnGCsdE4%uTSdKBb&qRnkNf<9Z?D8R7rHJ18uwF<+0o;b--D%8ug}}b zmA*-$vmiVcVgw~Cg_dznT#aB8()66De}h^;-cLmV+imqv67SpW8{f$)c(Hy~P5r6@ zO~yZ}Zj@Ay-{;o;A)u@YeBr4mC3!M}OM0}oxqtX_PYad;CLf*JIUjbq4jPpHc4y}X zQxV?O7jiFD;@$%BCI_s?-3sAC{@gws*B6eaNA>!UA9>Y0`u+mKQx!Klu5kikd7#B~ z_WeB3;t<3l?7sipu1{|#HX4XOyJ_sQHr^<*Qc+d-eill>#`yzG%x2Beuuw(q`|jO2 zDoJf+T;7phT5Di#mNfVNv7IBkuYe3QI(t^MhtJkD&iWPQwJu-7jbAvrZhb+|SK07= zFNv!lz@68#wGjF(4dTg@d~3i3=Y~W6kUsKz&~?VSL$Q;qwId21n6h?XxIl^;=qU?_ zT1^TzK4qr)9-#@OKC52w>{I|C!d0vM32eA<>G*et^-hnLZElhJ`)wwDtAEco%{fjW z)oxXDALbq~t<@dNS-CkF^$hvk2)YyU4Ry=6twK~GWq%8DYYIO>jG|O2UGBt!BUuQ! zetB}2%M&g7?daYQ%E{1vjQLycrdc+{wCW+<&X-c;AY$@uF7kbJo9a$9tEgFvKY_Y7 z0Jcd!%h-SGzsoK&MN;I`cO`Xuf~V8{QcPR3R1q&^m`7S1JO{tBW|U^X>j{thw4vjFI22bD zxu4Qmf4}2b*|B5q>)oyy`#3?&l_IOJFrRKn7U1sffnO6X^_nDjCvPDqs}pFOygJ7N zQlV9aXCYm79&aW+_CD~>HnD#y@_OhIM9`qC7X5`9pK^CDcia66DFgs)Z>dncK!JMk z`{&R;bc9uy$hS9uRCmh1Hz8e1ab8=r+WX_u)T5;N`rJ!VoF^SB=gzI!zlZJ&@e^nW zU24KO`*eQHt3wh`#dfa8-R1w@Pw1VG`Lj0^A_Q9ti$7f87eNDe(d=}VFo{nm?7BUB4U&KE3WVj9u*ugg#XVSokuIu+$_KW@OW zufS7L;&Z(MG<9kb&8 z#;4w$QCH6X;fV8xytP;R#)2{3pzu((s;M*Q$N6uuRg&!xpM2axVu5)E*jg|cBrV1F z`Uas|UB1CGV;J@+Vm#9k;~&tY_6h3uBhdcSTo~Q4<}_9jQad0qrUCIW1%YbNx~j3D z$t7hx&IH~=0lZx2^h`NV4x@gOH70gtQ)Ja z5r_;3AioG{rZ3yjLwGgSZ_VU`^5>xHVMv>v2e+sR5{gfk@8>$@U6+6zN5ECL9@zc- zBg(-4;pPd4AIoh0CFR|mj;eU3c75F+x8CiESpDkM)?SV6Os_wS87jUVaUsAC2#tgxD2uquXHNt4Qb&5092Gyo;KBy|%S_F)q^1x6IF z?626wz(N!IO`W;8q+mU7F6ZOdtT@k9#6MAYHsDXM@kpLpT5E0PhVhc) zf?yyJ07TC76B`RL#Ef1aZb1>nEoEl%NJKjypd^AAdP+NfeL!=tb=hP#&&GJN;uu49+$5!YTpzwAoAJZNun^u(rfk4N`D zZM$$Xc%Ap9?8S&-_m}(Q{*m~JHa~f6_FpX~g`1^I41^Wf#cZ;Oep1sckzg#+^ryV0 z$~Jzaj=dLAYT=uUBs#PdRE{bH1aZQ}CoY1e$*!~jNuYLjYUnnanmfc}3DJkN~bg(XA8u%jOdDV!~ zZ!a3EhW!4We@&5gP+lF|7gv0zkNU2gdn&c_(aDxpa>tIowvI<98!~EK3(c!HL0>kQ z5s&WT$(!p$m~hmNsea6N?g6?@^xM`#?3i&NDdN0b^09S(Z|G|(I z**JV>6?`dSLC{{7JLQJUr{=5wKW|P`-340Kka@ZOWu+i{YF9|cvawgzD=n`&o3%z* zoh*tS$O=B1UNZWsYq~QpJ8199Jq&atujxdgsTnq63BIWJ9W`Gfr11+VS`Gj*+QekT z$k_I~a5A)V8WjxO8%8-$+uLB#^>v5K@(f*jjOaqS00sH^Kg!F7-w)ZfWj@}q%p%3_ zRU$Q&_S3fER8D4FL*u3&!J`G&3lYl%XV#o*NMDr&r{=0Ch*O!@*QDBD*|B~nT&`o? z@E$^~*yP{HZFLNggO?hLBGXvs0KA@cjhNEZNxL#XiT`%(Uq~!86}NGPV1kbpWFpmy zX*W;zZ@_l0T(ZdadzF^j=p_&`OdQiEz_ob~7CK5z#W_4nG!l(J=HtAe^TIzFPJP#dMA?XN@N zUIQ=$rx$yh3%7a8(#t(>gAhK?&saADpvGH;A2teqxGBV4Z6QL$ZNMN-OY07tg%K0L zE*TG@w)qLv*t(E-GF=Az#Fk}MPX{y#%A5w&#A9F8zsZ=!+uC&od?gt9H=@8#nSgto zd&r}?0_){Jjo+{&)Q}D`4IN7usJ%XYm}f$%6-#Gu?yH)8l6lx^LQrPpDz#(0hb^xI zInPr7aO9<$!fpi^-LLYu;9s%T&zT%|icLx`VPP43$TB4;uD`dd$m0a127prS)WtDM zFySbxz!qpxMIJP1KqYH@UtQHvu#0Fdz7@a|*n=?Vgw~9guR)Zoo^{%0a3?ssl%sRC&N9FBx8rDa~2OrszfAyKj zKYfX(!%L^JHS&@?JG#-eG_Iv=p(JcvNRiozk2{STgy&D0w2ok*NFnekYP`@kSA@+R zfjTe~P&;D5CO7wE-<8l?Q|IoR)?gv{3>C#zxE-H%a<{PKg4*4?3>f+0LD>x3V|Y)7NZs*`+_! z5VN+^2FpFv@Tx04pZhCNG2~`Ou3k;XQX3B7qeuZTm>ml8q>y$0nvS?THQ%j#cT~Fh zOxvmP{@Lo?)m9ea<}SfFJ{5%0<|FJHur*qk$BgIdJtJfO2bUh5{Ygl?s-VH_$+!X= zfNhiu1SQ>O8x>EAnD+;rTn@TMZpzN-rjy>w*b=+3s3i022Ddr+Rp`n59_zjf_qME) z2L0&hyX2+=6V6zyXVwYh8Zz}xDul6FL{=BW1Q*k`s)c94yVjA^HM8qB|^Qor>uo?Ke8MDX^!#f4MVvU3vz! z7nRoVU_bopDnx_o&-9v4?R{IV_C`v~Y?4R+QB;{NrPl?RU9HiBKt-Ea!%0AX&%5u2 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

mTO|%a5&3TW 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

%5Ke9^@h@l=nBp($G7fdsAvAK-M$h| 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

J*C; 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

HOf3{jUA>v&9UpvQ*{UO(2)^kBW!w zE=0IoEL3n885RoFhgte1OwDOo07NX8#{lz-z#-VVpjCY}w7l~oN0R~0=F1QOoS4Q( z5A+axq7&z-ULHXKPFxqI?drFF|6TZgu~F_7`~$$Ntxoyy#i{2=uk_W$FigSEj+xOa zU3rtB0)mWOj@7SDwYHgIe1gDw=%%XxebjI=Iq4~pK#^@`yVa3jh6^wgZ4}|_v~0is zI`dPjxMXqi{MM0#I<_=cHKJw3RkR5=p@XmH?;T)&&0ufulOjJi9mH#`G`#$+F2`#B ze;`+g(*dBD=i4S_C?#nBZv6l^%D1`mEvwr@*6rtJryGB+a8j%MxkHPlQ>97yQfFYl ze@AI%LZQ_NXqyN&m-H#Ym({vJc*Fjf83GCk)T|_7xdc@tb2#YJt~alxtP9mu16fO^ zZWvH*lOP?api}zLS$()zwy6~4x+z367z9m{(Qly=1*jk-VmAq>_d-p4-{}Q_0Wtu& zA%U$Yq}thys=YOmOFcdE9;IZGsdG2eCei0r%h7{QnKSQOzSZ1R)%r6y`mL<+sD7WU zgi-9ScKd-{;PuO&xkp38z?zK5xnHWv89oZD0Nk=Y?knS80727vudUXXtq=e-g@GIv z8HNxLy#$yP`if*hsF_TqZKeX6sVGC`PCtv@DLi1s04F_`8)l$J2#VDN2#!&XW|ZTF z5P}dw2Y|yCk@EzudmpZj?oa{*@Y2@X({}%xtwtApZl``4iYKk!_*MTRwO84LKWGUp z_;t}J-EK=-zU=?LXg>aYoGql&Aqw4&iQQj?0!dY89&~Dqbb>^`^bmh6}jX+ z`pIlZ!TE|mP`Oix>f^U5;(*oxe^2+I`>Tr)7pN82jd%(t(l7&VNQY2<@cn9VI22YL z3H?x#?~(%Ekyk;C8fwy_gd3jywa>=CSI)mg+%u<=DkzS3_Db6lME;v3Dk+_tJ{S>P zfWB{3rj4lAGddj^S<9W+!2MsjZQ1Dg-KUiQ1+t%UlwM3xK~5Qr#pNP6HLH z`;?}Y;9Wr5y6?n&eMUkeia|hE63|lKc?sC+6ak%0AfTA$DNI-t$Z~Z_ZCR)rMnbG5xJ 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 z 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

C-hR$SnCAjlU1EWl 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

F$0{g#NwN>YdPHoYSiQvH9y* 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

-5#_BI)CT$H9#ja+0XnF84qiuY6!ij$a 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̿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